rss email digests over ssh because you're a cool kid herald.dunkirk.sh
go rss rss-reader ssh charm

feat: make the keep alive page match the style and make theme more responsive

dunkirk.sh cd2647f5 39c25ab7

verified
Changed files
+135 -29
web
+11 -21
web/handlers.go
··· 684 684 return hostPort 685 685 } 686 686 687 + type keepAlivePageData struct { 688 + ExpiresAt string 689 + } 690 + 687 691 func (s *Server) handleKeepAlive(w http.ResponseWriter, r *http.Request, token string) { 688 692 // Only allow GET requests 689 693 if r.Method != http.MethodGet { ··· 701 705 // Calculate new expiry date (90 days from now) 702 706 expiresAt := time.Now().AddDate(0, 0, 90).Format("January 2, 2006") 703 707 704 - // Return success message 705 - w.Header().Set("Content-Type", "text/html; charset=utf-8") 706 - w.WriteHeader(http.StatusOK) 707 - if _, err := fmt.Fprintf(w, `<!DOCTYPE html> 708 - <html> 709 - <head> 710 - <meta charset="utf-8"> 711 - <meta name="viewport" content="width=device-width, initial-scale=1"> 712 - <title>Digest Active</title> 713 - <style> 714 - body { font-family: system-ui, sans-serif; max-width: 600px; margin: 50px auto; padding: 20px; text-align: center; } 715 - .success { color: #059669; font-size: 24px; margin-bottom: 20px; } 716 - .details { color: #6b7280; font-size: 16px; } 717 - </style> 718 - </head> 719 - <body> 720 - <div class="success">✓ Success!</div> 721 - <div class="details">Your digest will stay active until <strong>%s</strong>.</div> 722 - </body> 723 - </html>`, expiresAt); err != nil { 724 - s.logger.Error("failed to write response", "error", err) 708 + data := keepAlivePageData{ 709 + ExpiresAt: expiresAt, 710 + } 711 + 712 + if err := s.tmpl.ExecuteTemplate(w, "keepalive.html", data); err != nil { 713 + s.logger.Warn("render keepalive", "err", err) 714 + http.Error(w, "Internal Server Error", http.StatusInternalServerError) 725 715 } 726 716 } 727 717
+18
web/templates/keepalive.html
··· 1 + <!DOCTYPE html> 2 + <html> 3 + <head> 4 + <meta charset="utf-8"> 5 + <meta name="viewport" content="width=device-width, initial-scale=1"> 6 + <title>HERALD - Digest Active</title> 7 + <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🎏</text></svg>"> 8 + <link rel="stylesheet" href="/style.css"> 9 + </head> 10 + <body> 11 + <h1>HERALD</h1> 12 + <h2>SUCCESS</h2> 13 + <p>Your digest will stay active until <strong>{{.ExpiresAt}}</strong>.</p> 14 + <footer> 15 + <span><a href="/">Return to home</a></span> 16 + </footer> 17 + </body> 18 + </html>
+106 -8
web/templates/style.css
··· 3 3 padding: 0; 4 4 box-sizing: border-box; 5 5 } 6 + 6 7 html, body { 7 8 font-family: monospace; 8 - background: #0a0a0a; 9 - color: #e0e0e0; 9 + } 10 + 11 + /* Dark theme (default) */ 12 + @media (prefers-color-scheme: dark) { 13 + html, body { 14 + background: #0a0a0a; 15 + color: #e0e0e0; 16 + } 17 + 18 + a { 19 + color: #8ab4f8; 20 + text-decoration: underline; 21 + } 22 + 23 + a:hover { 24 + color: #fff; 25 + } 26 + 27 + h1 { 28 + border-bottom: 2px solid #333; 29 + } 30 + 31 + footer { 32 + border-top: 1px solid #333; 33 + } 34 + } 35 + 36 + /* Light theme */ 37 + @media (prefers-color-scheme: light) { 38 + html, body { 39 + background: #fafafa; 40 + color: #1a1a1a; 41 + } 42 + 43 + a { 44 + color: #1a73e8; 45 + text-decoration: underline; 46 + } 47 + 48 + a:hover { 49 + color: #000; 50 + } 51 + 52 + h1 { 53 + border-bottom: 2px solid #ddd; 54 + } 55 + 56 + footer { 57 + border-top: 1px solid #ddd; 58 + } 59 + 60 + .inactive { 61 + opacity: 0.5; 62 + } 10 63 } 64 + 11 65 body { 12 66 max-width: 80ch; 13 67 margin: 2rem auto; 14 68 padding: 1rem; 15 69 line-height: 1.5; 16 70 } 71 + 17 72 a { 18 - color: #8ab4f8; 19 73 text-decoration: underline; 20 74 } 21 - a:hover { 22 - color: #fff; 23 - } 75 + 24 76 pre { 25 77 white-space: pre-wrap; 26 78 margin: 1rem 0; 27 79 } 80 + 28 81 h1 { 29 82 font-weight: bold; 30 83 font-size: 1rem; 31 - border-bottom: 2px solid #333; 32 84 padding-bottom: 0.5rem; 33 85 margin-bottom: 1rem; 34 86 } 87 + 35 88 h2 { 36 89 font-weight: bold; 37 90 font-size: 1rem; 38 91 margin-top: 1.5rem; 39 92 margin-bottom: 0.5rem; 40 93 } 94 + 41 95 p { 42 96 margin: 0.5rem 0; 43 97 } 98 + 44 99 ul { 45 100 list-style: none; 46 101 margin: 0.5rem 0 1rem 2rem; 47 102 } 103 + 48 104 li { 49 105 margin: 0.25rem 0; 50 106 } 107 + 51 108 strong { 52 109 font-weight: bold; 53 110 } 111 + 54 112 .inactive { 55 113 text-decoration: line-through; 56 114 opacity: 0.6; 57 115 } 116 + 58 117 footer { 59 118 display: flex; 60 119 justify-content: space-between; 61 120 margin-top: 2rem; 62 121 padding-top: 1rem; 63 - border-top: 1px solid #333; 64 122 font-size: 0.9rem; 65 123 opacity: 0.8; 66 124 } 125 + 126 + /* Mobile responsiveness */ 127 + @media (max-width: 768px) { 128 + body { 129 + margin: 1rem auto; 130 + padding: 0.75rem; 131 + font-size: 0.95rem; 132 + } 133 + 134 + h1 { 135 + font-size: 0.95rem; 136 + } 137 + 138 + h2 { 139 + font-size: 0.95rem; 140 + margin-top: 1rem; 141 + } 142 + 143 + footer { 144 + flex-direction: column; 145 + gap: 0.5rem; 146 + font-size: 0.85rem; 147 + } 148 + 149 + ul { 150 + margin-left: 1rem; 151 + } 152 + } 153 + 154 + @media (max-width: 480px) { 155 + body { 156 + margin: 0.5rem auto; 157 + padding: 0.5rem; 158 + font-size: 0.9rem; 159 + } 160 + 161 + pre { 162 + font-size: 0.85rem; 163 + } 164 + }