+11
-21
web/handlers.go
+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
+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
+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
+
}