+20
-1
README.md
+20
-1
README.md
···
1
1
# Herald ๐
2
2
3
-

3
+

4
4
5
5
This was inspired by the sunsetting of [pico.sh/feeds](https://blog.pico.sh/ann-033-moving-rss-to-email-pico-plus) being available outside of `pico+`. It is a totally understandable move from them as their email costs were skyrocketing and they needed to pay for it somehow. This was created to allow me to still get my rss feeds delivered to me each day by email which I have grown quite accustomed to. The config is completely compatible with the `pico.sh` format as of `2026-01-09` and should stay fairly stable. It is also configured over ssh with the slight addition that you can view your feeds on a website as well as I found myself wanting to hot load my feeds into my website :)
6
6
···
41
41
```bash
42
42
scp feeds.txt user@herald.dunkirk.sh:
43
43
```
44
+
45
+
### SSH Configuration
46
+
47
+
Add this to your `~/.ssh/config` for easier access:
48
+
49
+
```ssh-config
50
+
Host herald
51
+
HostName herald.dunkirk.sh
52
+
Port 2223
53
+
User herald
54
+
```
55
+
56
+
Then use: `scp feeds.txt herald:` and `ssh herald ls`
44
57
45
58
### SSH Commands
46
59
···
123
136
- `HERALD_SMTP_USER`
124
137
- `HERALD_SMTP_PASS`
125
138
- `HERALD_SMTP_FROM`
139
+
140
+
## Screenshots
141
+
142
+
here is an example of what an email digest looks like:
143
+
144
+

126
145
127
146
<p align="center">
128
147
<img src="https://raw.githubusercontent.com/taciturnaxolotl/carriage/main/.github/images/line-break.svg" />
+40
crush.json
+40
crush.json
···
1
+
{
2
+
"$schema": "https://charm.land/crush.json",
3
+
"lsp": {
4
+
"gopls": {
5
+
"options": {
6
+
"gofumpt": true,
7
+
"codelenses": {
8
+
"gc_details": true,
9
+
"generate": true,
10
+
"run_govulncheck": true,
11
+
"test": true,
12
+
"tidy": true,
13
+
"upgrade_dependency": true
14
+
},
15
+
"hints": {
16
+
"assignVariableTypes": true,
17
+
"compositeLiteralFields": true,
18
+
"compositeLiteralTypes": true,
19
+
"constantValues": true,
20
+
"functionTypeParameters": true,
21
+
"parameterNames": true,
22
+
"rangeVariableTypes": true
23
+
},
24
+
"analyses": {
25
+
"nilness": true,
26
+
"unusedparams": true,
27
+
"unusedvariable": true,
28
+
"unusedwrite": true,
29
+
"useany": true
30
+
},
31
+
"staticcheck": true,
32
+
"directoryFilters": [
33
+
"-.git",
34
+
"-node_modules"
35
+
],
36
+
"semanticTokens": true
37
+
}
38
+
}
39
+
}
40
+
}
+100
emoji_u1f38f.svg
+100
emoji_u1f38f.svg
···
1
+
<?xml version="1.0" encoding="utf-8"?>
2
+
<!-- Generator: Adobe Illustrator 25.2.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
3
+
<svg version="1.1" id="Layer_2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
4
+
viewBox="0 0 128 128" style="enable-background:new 0 0 128 128;" xml:space="preserve">
5
+
<path style="fill:#FBC219;" d="M7.2,20.67l6.85-0.61l12.94,100.19c0,0-0.45,2.79-3.05,3.05c-3.05,0.3-3.81-2.59-3.81-2.59L7.2,20.67
6
+
z"/>
7
+
<path style="fill:#E07F16;" d="M2.67,17.47c0.63,5.16,5.25,6.72,8.34,6.51c3.43-0.23,7.88-2.88,7.31-8.45
8
+
c-0.67-6.54-6.4-7.77-9.6-6.97C5.07,9.47,2.1,12.79,2.67,17.47z"/>
9
+
<path style="fill:#FFC020;" d="M11.35,10.84C8.8,9.02,5.64,11.3,4.73,13.59S4.5,17.7,5.75,18.73s4,0.69,5.6-1.26
10
+
C12.85,15.65,13.75,12.56,11.35,10.84z"/>
11
+
<path style="fill:#016BA2;" d="M30.43,31.98c0,0,4.11-6.63,12.22-12s17.02-9.25,32.79-9.03c10.74,0.16,17.02,0.96,23.42,1.03
12
+
c10.62,0.11,17.2-3.89,18.85-1.26c2.28,3.66-6.34,14-6.17,17.02c0.23,4,11.54,2.74,11.65,6.97c0.08,2.86-6.28,10.05-15.99,12.45
13
+
S85.5,49,77.84,50.14c-7.65,1.14-21.14,5.5-25.53,6.9C45.9,59.09,30.43,31.98,30.43,31.98z"/>
14
+
<path style="fill:#DDE2E1;" d="M26,37.96l5.03-6.95c0,0,1.36,1,3.44,1.63c2.08,0.63,11.7,2.68,16.3,9.42
15
+
C55.85,49.49,52.82,57,52.82,57s-4.15,1.97-11.5,3.04c-4.76,0.69-7.79-0.17-7.79-0.17s-5.85-10.19-5.94-10.47S26,37.96,26,37.96z"/>
16
+
<path style="fill:#016BA2;" d="M42.56,39.08c-1.01-0.41-2.66,0.64-2.79,3.51c-0.24,5.17,5.35,6.42,5.35,6.42s0.66,2.8,2.56,3.98
17
+
c3.45,2.14,4.7,0.12,4.46-0.71c-0.24-0.83-1.43-0.77-2.5-2.2c-0.74-0.99-1.13-3.09-1.43-3.51s-2.5-0.18-4.22-3.09
18
+
C42.77,41.42,43.15,39.32,42.56,39.08z"/>
19
+
<path style="fill:#016BA2;" d="M27.22,37c-2.5,0.71-3.15,6.64-1.01,13.38c1.66,5.23,5.77,10.58,9.39,9.39
20
+
c3.15-1.03,4.04-6.66,1.19-13.85C33.94,38.73,30.29,36.12,27.22,37z"/>
21
+
<path style="fill:#DDE2E1;" d="M28.65,39.86c-1.04,0.26-0.59,6.78,0.71,9.87s3.92,7.37,5.41,6.72c0.76-0.34,0.65-5.41-0.71-9.33
22
+
C32.69,43.19,29.6,39.62,28.65,39.86z"/>
23
+
<path style="fill:#FFFFFF;" d="M53.64,25.39c-3.66,2.11-4.29,7.27-2.18,11.06c1.81,3.26,5.36,4.85,8.86,3.5
24
+
c3.37-1.3,4.79-6.38,2.89-10.63C61.4,25.28,56.77,23.59,53.64,25.39z"/>
25
+
<path style="fill:#2D2D2E;" d="M55.16,28.62c-2.21,1.31-2.27,4.03-1.14,6.03c1.24,2.19,3.99,2.19,5.46,1.28
26
+
c1.73-1.06,2.12-3.31,1.24-5.32C59.67,28.24,57.01,27.53,55.16,28.62z"/>
27
+
<path style="fill:#F26D31;" d="M37.9,75.09c0,0,4.35-6.95,14.41-10.58c15.3-5.52,29.65-4.43,40.23-4.67
28
+
c18.46-0.41,21.88-6.64,24.68-4.15c2.36,2.09-4.46,11.92-3.94,15.76c0.29,2.15,1.24,4.04,4.15,4.87s6.4,1.25,6.74,2.8
29
+
c0.41,1.87-5.08,7.88-15.55,11.92c-8.61,3.33-18.66,3.63-27.27,4.98c-7.28,1.14-15.1,3.63-19.39,4.98
30
+
c-5.6,1.76-10.26,2.07-10.26,2.07s-9.54-24.68-9.85-24.99C41.53,77.79,37.9,75.09,37.9,75.09z"/>
31
+
<path style="fill:#DDE2E1;" d="M38.1,74.51c0,0,6.12-0.37,11.59,3.55c5.08,3.64,10.91,10.83,10.66,17.51
32
+
c-0.14,3.81-2.4,6.52-2.4,6.52s-3.67,1.52-9.45,1.18c-7.18-0.42-9.22-3.05-9.22-3.05l-5.75-15.06c0,0,0.51-4.99,1.78-6.94
33
+
C36.58,76.29,38.1,74.51,38.1,74.51z"/>
34
+
<path style="fill:#F26D31;" d="M32.98,91.27c1.35,5.56,5.57,10.78,8.95,9.99c3.12-0.73,4.21-5.49,2.39-12.7
35
+
c-1.77-7.03-5-11.29-9.02-10.33C31.53,79.15,31.78,86.33,32.98,91.27z"/>
36
+
<path style="fill:#DDE2E1;" d="M36.16,81.49c-1.41,0.42-1.1,4.77-0.1,8.9c0.73,3.02,2.55,7.86,4.22,7.44
37
+
c1.67-0.42,1.09-5.67,0.31-8.69C39.8,86.12,37.89,80.97,36.16,81.49z"/>
38
+
<path style="fill:#F26D31;" d="M48.7,81.95c-2.13,0-1.62,4.38-0.26,6.56c1.82,2.91,4.06,3.54,4.06,3.54s0.49,2.79,2.19,4.68
39
+
c0.88,0.99,2.21,1.71,2.76,1.35c0.6-0.39-0.07-2.12-0.57-4.11c-0.68-2.65-0.78-4.27-0.78-4.27s-2.55-1.51-3.85-3.33
40
+
S50.79,81.95,48.7,81.95z"/>
41
+
<path style="fill:#F8FFFD;" d="M59.32,72.22c-3.5,2.13-3.64,7.13-0.94,11.09c2.71,3.96,7.08,3.8,9.42,2.45
42
+
c2.34-1.35,3.85-6.09,1.2-10.67C66.6,70.95,61.97,70.61,59.32,72.22z"/>
43
+
<path style="fill:#2D2D2E;" d="M61.61,74.77c-1.87,0.83-2.29,4.32-0.68,6.45c1.61,2.13,3.8,2.18,5,1.25
44
+
c1.87-1.46,1.57-4.11,0.68-5.72C65.56,74.88,63.31,74.02,61.61,74.77z"/>
45
+
<g>
46
+
<path style="fill:#B6CED5;" d="M13.96,56.39c-2.52,0-3.81-0.94-3.97-1.05c-0.66-0.51-0.78-1.45-0.27-2.1
47
+
c0.51-0.66,1.45-0.77,2.1-0.27l0,0c0,0,0.85,0.55,2.73,0.4c2.18-0.17,3.55-1.04,3.56-1.05l0.27-0.14l12.74-5.01
48
+
c0.77-0.3,1.64,0.08,1.95,0.85c0.3,0.77-0.08,1.64-0.85,1.95l-12.61,4.96c-0.53,0.32-2.26,1.24-4.83,1.44
49
+
C14.5,56.38,14.22,56.39,13.96,56.39z"/>
50
+
</g>
51
+
<g>
52
+
<path style="fill:#B6CED5;" d="M20.29,98.16c-2.62,0-4.37-0.89-4.56-0.99c-0.73-0.39-1.01-1.29-0.63-2.03
53
+
c0.39-0.73,1.29-1.01,2.03-0.63c0,0,1.28,0.64,3.16,0.64c1.62,0,2.87-1.4,2.88-1.42l0.27-0.31l0.4-0.14l14-4.63
54
+
c0.79-0.26,1.64,0.17,1.9,0.95c0.26,0.79-0.17,1.63-0.95,1.89l-13.65,4.51C24.44,96.71,22.67,98.16,20.29,98.16z"/>
55
+
</g>
56
+
<path style="fill:#39BDE3;" d="M73.1,35.27c-1.38,0.83,0.52,2.61-0.06,8.01c-0.5,4.7-4.17,8.84-4.17,8.84s2.4-0.67,3.72-0.94
57
+
c1.33-0.28,3.48-0.72,3.48-0.72s1.22-2.54,1.27-6.91C77.41,39.35,75.23,33.99,73.1,35.27z"/>
58
+
<path style="fill:#39BDE3;" d="M75.37,21.18c-0.17,0.31,0.44,1.88,0.83,3.43c0.39,1.55,0.64,3.68,1.11,3.87
59
+
c0.66,0.28,3.91-1.57,2.93-5.08C79.18,19.63,75.7,20.57,75.37,21.18z"/>
60
+
<path style="fill:#39BDE3;" d="M83.77,25.49c-0.77,0.88,0.33,2.04,0.72,3.59c0.39,1.55,0.48,3.18,0.94,3.37
61
+
c0.66,0.28,3.87-1.33,3.26-4.48C87.95,24.15,84.22,24.97,83.77,25.49z"/>
62
+
<path style="fill:#39BDE3;" d="M81.32,15.9c-0.48,0.97,0.52,2.02,0.9,4.01c0.3,1.57,0.53,3.56,0.99,3.76
63
+
c0.66,0.28,4.19-1.95,3.2-5.47C85.37,14.44,81.62,15.28,81.32,15.9z"/>
64
+
<path style="fill:#39BDE3;" d="M90.93,20.21c-0.17,0.31,0.44,1.88,0.83,3.43c0.39,1.55,0.64,3.68,1.11,3.87
65
+
c0.66,0.28,3.91-1.57,2.93-5.08C94.74,18.67,91.26,19.61,90.93,20.21z"/>
66
+
<path style="fill:#39BDE3;" d="M96.18,15.57c-0.15,0.32,0.52,2.51,0.9,4.06c0.39,1.55,0.75,3.29,1.22,3.48
67
+
c0.66,0.28,3.87-1.71,3.04-5.53C100.51,13.78,96.64,14.61,96.18,15.57z"/>
68
+
<path style="fill:#39BDE3;" d="M99.05,24.6c-0.19,0.33,0.64,2.42,1.07,4.04c0.43,1.63,0.59,3.38,1.1,3.62
69
+
c1,0.47,4.52-1.69,3.28-5.34C103.38,23.61,99.42,23.96,99.05,24.6z"/>
70
+
<path style="fill:#39BDE3;" d="M93,29.58c-0.17,0.34,0.66,1.84,1.16,3.7c0.45,1.68,0.67,4.01,1.13,4.22
71
+
c0.66,0.31,4.08-1.36,3.13-5.21C97.39,28.17,93.33,28.92,93,29.58z"/>
72
+
<path style="fill:#39BDE3;" d="M77.97,30.63c-0.17,0.31,0.39,2.21,0.77,3.76c0.39,1.55,0.62,3.32,1.09,3.51
73
+
c0.66,0.28,3.82-1.54,2.93-5.08C82,29.8,78.3,30.02,77.97,30.63z"/>
74
+
<path style="fill:#39BDE3;" d="M86.18,35.46c-0.48,0.86,0.41,1.91,0.79,3.45c0.39,1.55,0.69,3.62,1.14,3.84
75
+
c0.96,0.47,3.91-1.66,3.28-4.83C90.64,34.1,86.52,34.86,86.18,35.46z"/>
76
+
<path style="fill:#39BDE3;" d="M101.24,35.08c-0.17,0.3,0.42,2.19,0.93,3.67s1.08,3.17,1.56,3.32c0.68,0.22,4.11-2.36,2.92-5.75
77
+
C105.59,33.34,101.67,34.33,101.24,35.08z"/>
78
+
<path style="fill:#FEC516;" d="M80.7,77.14c-0.16,0.32,0.5,1.88,0.95,3.51c0.42,1.55,0.91,3.36,1.38,3.54
79
+
c0.67,0.26,3.87-1.67,2.81-5.18C84.7,75.26,81.02,76.52,80.7,77.14z"/>
80
+
<path style="fill:#FEC516;" d="M86.27,71.68c-0.17,0.31,0.79,2.03,1.31,3.45c0.55,1.5,0.64,3.68,1.11,3.87
81
+
c0.66,0.28,3.43-1.35,2.59-4.9C90.43,70.49,86.6,71.07,86.27,71.68z"/>
82
+
<path style="fill:#FEC516;" d="M78.11,67.53c-0.18,0.34,0.54,2,1,3.64c0.47,1.64,0.72,3.07,1.15,3.42c1.13,0.9,4.38-0.71,3.36-4.71
83
+
C82.59,65.82,78.46,66.87,78.11,67.53z"/>
84
+
<path style="fill:#FEC516;" d="M84.16,62.1c-0.66,0.47,0.32,1.9,0.6,3.47c0.28,1.57,0.3,3.4,0.77,3.58c1.35,0.51,4.27-0.3,3.55-4.45
85
+
C88.4,60.82,84.73,61.7,84.16,62.1z"/>
86
+
<path style="fill:#FEC516;" d="M92.23,66.89c-0.23,1.07,0.97,1.48,1.66,3.57c0.6,1.83,0.47,3.09,0.95,3.24
87
+
c1.46,0.45,4.65-2.1,3.47-5.12C96.91,64.97,92.38,66.17,92.23,66.89z"/>
88
+
<path style="fill:#FEC516;" d="M98.66,62.3c-0.17,0.31,0.33,1.38,1.02,3.26c0.55,1.5,0.64,3.68,1.11,3.87
89
+
c0.66,0.28,3.8-1.55,2.82-5.07C102.55,60.6,98.99,61.69,98.66,62.3z"/>
90
+
<path style="fill:#FEC516;" d="M101.52,71.6c-0.17,0.31,0.51,1.78,0.9,3.06c0.46,1.53,0.41,3.18,1.12,3.66
91
+
c1.07,0.73,4.17-1.48,3.04-4.96C105.54,70.13,101.86,70.99,101.52,71.6z"/>
92
+
<path style="fill:#FEC516;" d="M95.6,76.05c-0.17,0.31,0.38,1.91,0.64,3.5c0.28,1.77,0.27,3.34,0.72,3.55
93
+
c1.07,0.51,4.64-0.82,3.77-4.62C99.87,74.72,95.93,75.45,95.6,76.05z"/>
94
+
<path style="fill:#FEC516;" d="M103.86,80.66c-0.17,0.31,0.44,1.88,0.83,3.43c0.39,1.55,0.7,3,1.1,3.3c0.9,0.68,4.06-0.9,3.22-4.56
95
+
C108.13,79.02,104.19,80.05,103.86,80.66z"/>
96
+
<path style="fill:#FEC516;" d="M88.88,81.06c-0.48,0.65,0.6,2.13,0.79,3.72c0.23,1.86,0.28,3.27,0.73,3.49
97
+
c1.29,0.62,4.81-1.41,3.61-4.95C92.79,79.68,89.29,80.5,88.88,81.06z"/>
98
+
<path style="fill:#FEC516;" d="M72.8,97.85c0,0,3.47-3.86,3.39-6.75c-0.11-4.33-1.35-6.47,0.34-7.09c1.4-0.51,2.93,1.75,3.49,4.17
99
+
c1.35,5.82,0.06,8.1,0.06,8.1s-2.13,0.46-3.78,0.79C74.89,97.34,72.8,97.85,72.8,97.85z"/>
100
+
</svg>
-34
flake.lock
-34
flake.lock
···
1
1
{
2
2
"nodes": {
3
-
"flake-utils": {
4
-
"inputs": {
5
-
"systems": "systems"
6
-
},
7
-
"locked": {
8
-
"lastModified": 1731533236,
9
-
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
10
-
"owner": "numtide",
11
-
"repo": "flake-utils",
12
-
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
13
-
"type": "github"
14
-
},
15
-
"original": {
16
-
"owner": "numtide",
17
-
"repo": "flake-utils",
18
-
"type": "github"
19
-
}
20
-
},
21
3
"nixpkgs": {
22
4
"locked": {
23
5
"lastModified": 1767892417,
···
36
18
},
37
19
"root": {
38
20
"inputs": {
39
-
"flake-utils": "flake-utils",
40
21
"nixpkgs": "nixpkgs"
41
-
}
42
-
},
43
-
"systems": {
44
-
"locked": {
45
-
"lastModified": 1681028828,
46
-
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
47
-
"owner": "nix-systems",
48
-
"repo": "default",
49
-
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
50
-
"type": "github"
51
-
},
52
-
"original": {
53
-
"owner": "nix-systems",
54
-
"repo": "default",
55
-
"type": "github"
56
22
}
57
23
}
58
24
},
+68
-24
flake.nix
+68
-24
flake.nix
···
3
3
4
4
inputs = {
5
5
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
6
-
flake-utils.url = "github:numtide/flake-utils";
7
6
};
8
7
9
-
outputs = { self, nixpkgs, flake-utils }:
10
-
flake-utils.lib.eachDefaultSystem (system:
11
-
let
12
-
pkgs = nixpkgs.legacyPackages.${system};
13
-
in
14
-
{
15
-
devShells.default = pkgs.mkShell {
16
-
buildInputs = with pkgs; [
17
-
go_1_24
18
-
golangci-lint
19
-
gotools
20
-
gopls
21
-
delve
22
-
goreleaser
23
-
];
8
+
outputs =
9
+
{ self, nixpkgs }:
10
+
let
11
+
allSystems = [
12
+
"x86_64-linux" # 64-bit Intel/AMD Linux
13
+
"aarch64-linux" # 64-bit ARM Linux
14
+
"x86_64-darwin" # 64-bit Intel macOS
15
+
"aarch64-darwin" # 64-bit ARM macOS
16
+
];
17
+
forAllSystems =
18
+
f:
19
+
nixpkgs.lib.genAttrs allSystems (
20
+
system:
21
+
f {
22
+
pkgs = import nixpkgs { inherit system; };
23
+
}
24
+
);
25
+
in
26
+
{
27
+
packages = forAllSystems (
28
+
{ pkgs }:
29
+
{
30
+
default = pkgs.buildGoModule {
31
+
pname = "herald";
32
+
version = "0.1.1";
33
+
subPackages = [ "." ];
34
+
src = self;
35
+
vendorHash = "sha256-IE7JMJ4DehwYxrkh5YCgw7yWdybxtCCxWQO7M/u6bno=";
36
+
ldflags = [
37
+
"-X main.commitHash=${self.rev or self.dirtyRev or "dev"}"
38
+
"-X main.version=0.1.1"
39
+
];
40
+
};
41
+
}
42
+
);
24
43
25
-
shellHook = ''
26
-
echo "Herald development environment"
27
-
echo "Go version: $(go version)"
28
-
echo "golangci-lint version: $(golangci-lint version)"
29
-
'';
30
-
};
31
-
}
32
-
);
44
+
devShells = forAllSystems (
45
+
{ pkgs }:
46
+
{
47
+
default = pkgs.mkShell {
48
+
buildInputs = with pkgs; [
49
+
go_1_24
50
+
gopls
51
+
gotools
52
+
go-tools
53
+
golangci-lint
54
+
delve
55
+
goreleaser
56
+
];
57
+
58
+
shellHook = ''
59
+
echo "Herald development environment"
60
+
echo "Go version: $(go version)"
61
+
echo "golangci-lint version: $(golangci-lint version)"
62
+
'';
63
+
};
64
+
}
65
+
);
66
+
67
+
apps = forAllSystems (
68
+
{ pkgs }:
69
+
{
70
+
default = {
71
+
type = "app";
72
+
program = "${self.packages.${pkgs.system}.default}/bin/herald";
73
+
};
74
+
}
75
+
);
76
+
};
33
77
}
+2
main.go
+2
main.go
+6
-6
scheduler/scheduler.go
+6
-6
scheduler/scheduler.go
···
179
179
}
180
180
}()
181
181
182
-
now := time.Now()
182
+
now := time.Now().UTC()
183
183
configs, err := s.store.GetDueConfigs(ctx, now)
184
184
if err != nil {
185
185
s.logger.Error("failed to get due configs", "err", err)
···
257
257
s.logger.Debug("RunNow: feed metadata updated")
258
258
259
259
s.logger.Debug("RunNow: calculating next run")
260
-
now := time.Now()
261
-
nextRun, err := gronx.NextTick(cfg.CronExpr, false)
260
+
now := time.Now().UTC()
261
+
nextRun, err := gronx.NextTick(cfg.CronExpr, true)
262
262
if err != nil {
263
263
return stats, fmt.Errorf("calculate next run: %w", err)
264
264
}
···
277
277
func (s *Scheduler) collectNewItems(ctx context.Context, results []*FetchResult) ([]email.FeedGroup, int, error) {
278
278
var feedGroups []email.FeedGroup
279
279
totalNew := 0
280
-
maxAge := time.Now().Add(-itemMaxAge)
280
+
maxAge := time.Now().UTC().Add(-itemMaxAge)
281
281
feedErrors := 0
282
282
283
283
for _, result := range results {
···
486
486
}
487
487
}
488
488
489
-
now := time.Now()
490
-
nextRun, err := gronx.NextTick(cfg.CronExpr, false)
489
+
now := time.Now().UTC()
490
+
nextRun, err := gronx.NextTick(cfg.CronExpr, true)
491
491
if err != nil {
492
492
return fmt.Errorf("calculate next run: %w", err)
493
493
}
+1
-1
ssh/commands.go
+1
-1
ssh/commands.go
+1
-1
ssh/scp.go
+1
-1
ssh/scp.go
+3
-3
store/configs.go
+3
-3
store/configs.go
···
49
49
InlineContent: inline,
50
50
RawText: rawText,
51
51
NextRun: sql.NullTime{Time: nextRun, Valid: true},
52
-
CreatedAt: time.Now(),
52
+
CreatedAt: time.Now().UTC(),
53
53
}, nil
54
54
}
55
55
···
78
78
InlineContent: inline,
79
79
RawText: rawText,
80
80
NextRun: sql.NullTime{Time: nextRun, Valid: true},
81
-
CreatedAt: time.Now(),
81
+
CreatedAt: time.Now().UTC(),
82
82
}, nil
83
83
}
84
84
···
219
219
return err
220
220
}
221
221
222
-
nextRun, err := gronx.NextTick(cfg.CronExpr, false)
222
+
nextRun, err := gronx.NextTick(cfg.CronExpr, true)
223
223
if err != nil {
224
224
return fmt.Errorf("calculate next run: %w", err)
225
225
}
+1
-1
store/users.go
+1
-1
store/users.go
+21
-21
web/handlers.go
+21
-21
web/handlers.go
···
60
60
_, _ = w.Write(css)
61
61
}
62
62
63
+
func (s *Server) handleFaviconSVG(w http.ResponseWriter, r *http.Request) {
64
+
svg, err := publicFS.ReadFile("public/favicon.svg")
65
+
if err != nil {
66
+
http.Error(w, "Not Found", http.StatusNotFound)
67
+
return
68
+
}
69
+
w.Header().Set("Content-Type", "image/svg+xml")
70
+
_, _ = w.Write(svg)
71
+
}
72
+
63
73
type userPageData struct {
64
74
Fingerprint string
65
75
ShortFingerprint string
···
684
694
return hostPort
685
695
}
686
696
697
+
type keepAlivePageData struct {
698
+
ExpiresAt string
699
+
}
700
+
687
701
func (s *Server) handleKeepAlive(w http.ResponseWriter, r *http.Request, token string) {
688
702
// Only allow GET requests
689
703
if r.Method != http.MethodGet {
···
701
715
// Calculate new expiry date (90 days from now)
702
716
expiresAt := time.Now().AddDate(0, 0, 90).Format("January 2, 2006")
703
717
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)
718
+
data := keepAlivePageData{
719
+
ExpiresAt: expiresAt,
720
+
}
721
+
722
+
if err := s.tmpl.ExecuteTemplate(w, "keepalive.html", data); err != nil {
723
+
s.logger.Warn("render keepalive", "err", err)
724
+
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
725
725
}
726
726
}
727
727
+100
web/public/favicon.svg
+100
web/public/favicon.svg
···
1
+
<?xml version="1.0" encoding="utf-8"?>
2
+
<!-- Generator: Adobe Illustrator 25.2.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
3
+
<svg version="1.1" id="Layer_2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
4
+
viewBox="0 0 128 128" style="enable-background:new 0 0 128 128;" xml:space="preserve">
5
+
<path style="fill:#FBC219;" d="M7.2,20.67l6.85-0.61l12.94,100.19c0,0-0.45,2.79-3.05,3.05c-3.05,0.3-3.81-2.59-3.81-2.59L7.2,20.67
6
+
z"/>
7
+
<path style="fill:#E07F16;" d="M2.67,17.47c0.63,5.16,5.25,6.72,8.34,6.51c3.43-0.23,7.88-2.88,7.31-8.45
8
+
c-0.67-6.54-6.4-7.77-9.6-6.97C5.07,9.47,2.1,12.79,2.67,17.47z"/>
9
+
<path style="fill:#FFC020;" d="M11.35,10.84C8.8,9.02,5.64,11.3,4.73,13.59S4.5,17.7,5.75,18.73s4,0.69,5.6-1.26
10
+
C12.85,15.65,13.75,12.56,11.35,10.84z"/>
11
+
<path style="fill:#016BA2;" d="M30.43,31.98c0,0,4.11-6.63,12.22-12s17.02-9.25,32.79-9.03c10.74,0.16,17.02,0.96,23.42,1.03
12
+
c10.62,0.11,17.2-3.89,18.85-1.26c2.28,3.66-6.34,14-6.17,17.02c0.23,4,11.54,2.74,11.65,6.97c0.08,2.86-6.28,10.05-15.99,12.45
13
+
S85.5,49,77.84,50.14c-7.65,1.14-21.14,5.5-25.53,6.9C45.9,59.09,30.43,31.98,30.43,31.98z"/>
14
+
<path style="fill:#DDE2E1;" d="M26,37.96l5.03-6.95c0,0,1.36,1,3.44,1.63c2.08,0.63,11.7,2.68,16.3,9.42
15
+
C55.85,49.49,52.82,57,52.82,57s-4.15,1.97-11.5,3.04c-4.76,0.69-7.79-0.17-7.79-0.17s-5.85-10.19-5.94-10.47S26,37.96,26,37.96z"/>
16
+
<path style="fill:#016BA2;" d="M42.56,39.08c-1.01-0.41-2.66,0.64-2.79,3.51c-0.24,5.17,5.35,6.42,5.35,6.42s0.66,2.8,2.56,3.98
17
+
c3.45,2.14,4.7,0.12,4.46-0.71c-0.24-0.83-1.43-0.77-2.5-2.2c-0.74-0.99-1.13-3.09-1.43-3.51s-2.5-0.18-4.22-3.09
18
+
C42.77,41.42,43.15,39.32,42.56,39.08z"/>
19
+
<path style="fill:#016BA2;" d="M27.22,37c-2.5,0.71-3.15,6.64-1.01,13.38c1.66,5.23,5.77,10.58,9.39,9.39
20
+
c3.15-1.03,4.04-6.66,1.19-13.85C33.94,38.73,30.29,36.12,27.22,37z"/>
21
+
<path style="fill:#DDE2E1;" d="M28.65,39.86c-1.04,0.26-0.59,6.78,0.71,9.87s3.92,7.37,5.41,6.72c0.76-0.34,0.65-5.41-0.71-9.33
22
+
C32.69,43.19,29.6,39.62,28.65,39.86z"/>
23
+
<path style="fill:#FFFFFF;" d="M53.64,25.39c-3.66,2.11-4.29,7.27-2.18,11.06c1.81,3.26,5.36,4.85,8.86,3.5
24
+
c3.37-1.3,4.79-6.38,2.89-10.63C61.4,25.28,56.77,23.59,53.64,25.39z"/>
25
+
<path style="fill:#2D2D2E;" d="M55.16,28.62c-2.21,1.31-2.27,4.03-1.14,6.03c1.24,2.19,3.99,2.19,5.46,1.28
26
+
c1.73-1.06,2.12-3.31,1.24-5.32C59.67,28.24,57.01,27.53,55.16,28.62z"/>
27
+
<path style="fill:#F26D31;" d="M37.9,75.09c0,0,4.35-6.95,14.41-10.58c15.3-5.52,29.65-4.43,40.23-4.67
28
+
c18.46-0.41,21.88-6.64,24.68-4.15c2.36,2.09-4.46,11.92-3.94,15.76c0.29,2.15,1.24,4.04,4.15,4.87s6.4,1.25,6.74,2.8
29
+
c0.41,1.87-5.08,7.88-15.55,11.92c-8.61,3.33-18.66,3.63-27.27,4.98c-7.28,1.14-15.1,3.63-19.39,4.98
30
+
c-5.6,1.76-10.26,2.07-10.26,2.07s-9.54-24.68-9.85-24.99C41.53,77.79,37.9,75.09,37.9,75.09z"/>
31
+
<path style="fill:#DDE2E1;" d="M38.1,74.51c0,0,6.12-0.37,11.59,3.55c5.08,3.64,10.91,10.83,10.66,17.51
32
+
c-0.14,3.81-2.4,6.52-2.4,6.52s-3.67,1.52-9.45,1.18c-7.18-0.42-9.22-3.05-9.22-3.05l-5.75-15.06c0,0,0.51-4.99,1.78-6.94
33
+
C36.58,76.29,38.1,74.51,38.1,74.51z"/>
34
+
<path style="fill:#F26D31;" d="M32.98,91.27c1.35,5.56,5.57,10.78,8.95,9.99c3.12-0.73,4.21-5.49,2.39-12.7
35
+
c-1.77-7.03-5-11.29-9.02-10.33C31.53,79.15,31.78,86.33,32.98,91.27z"/>
36
+
<path style="fill:#DDE2E1;" d="M36.16,81.49c-1.41,0.42-1.1,4.77-0.1,8.9c0.73,3.02,2.55,7.86,4.22,7.44
37
+
c1.67-0.42,1.09-5.67,0.31-8.69C39.8,86.12,37.89,80.97,36.16,81.49z"/>
38
+
<path style="fill:#F26D31;" d="M48.7,81.95c-2.13,0-1.62,4.38-0.26,6.56c1.82,2.91,4.06,3.54,4.06,3.54s0.49,2.79,2.19,4.68
39
+
c0.88,0.99,2.21,1.71,2.76,1.35c0.6-0.39-0.07-2.12-0.57-4.11c-0.68-2.65-0.78-4.27-0.78-4.27s-2.55-1.51-3.85-3.33
40
+
S50.79,81.95,48.7,81.95z"/>
41
+
<path style="fill:#F8FFFD;" d="M59.32,72.22c-3.5,2.13-3.64,7.13-0.94,11.09c2.71,3.96,7.08,3.8,9.42,2.45
42
+
c2.34-1.35,3.85-6.09,1.2-10.67C66.6,70.95,61.97,70.61,59.32,72.22z"/>
43
+
<path style="fill:#2D2D2E;" d="M61.61,74.77c-1.87,0.83-2.29,4.32-0.68,6.45c1.61,2.13,3.8,2.18,5,1.25
44
+
c1.87-1.46,1.57-4.11,0.68-5.72C65.56,74.88,63.31,74.02,61.61,74.77z"/>
45
+
<g>
46
+
<path style="fill:#B6CED5;" d="M13.96,56.39c-2.52,0-3.81-0.94-3.97-1.05c-0.66-0.51-0.78-1.45-0.27-2.1
47
+
c0.51-0.66,1.45-0.77,2.1-0.27l0,0c0,0,0.85,0.55,2.73,0.4c2.18-0.17,3.55-1.04,3.56-1.05l0.27-0.14l12.74-5.01
48
+
c0.77-0.3,1.64,0.08,1.95,0.85c0.3,0.77-0.08,1.64-0.85,1.95l-12.61,4.96c-0.53,0.32-2.26,1.24-4.83,1.44
49
+
C14.5,56.38,14.22,56.39,13.96,56.39z"/>
50
+
</g>
51
+
<g>
52
+
<path style="fill:#B6CED5;" d="M20.29,98.16c-2.62,0-4.37-0.89-4.56-0.99c-0.73-0.39-1.01-1.29-0.63-2.03
53
+
c0.39-0.73,1.29-1.01,2.03-0.63c0,0,1.28,0.64,3.16,0.64c1.62,0,2.87-1.4,2.88-1.42l0.27-0.31l0.4-0.14l14-4.63
54
+
c0.79-0.26,1.64,0.17,1.9,0.95c0.26,0.79-0.17,1.63-0.95,1.89l-13.65,4.51C24.44,96.71,22.67,98.16,20.29,98.16z"/>
55
+
</g>
56
+
<path style="fill:#39BDE3;" d="M73.1,35.27c-1.38,0.83,0.52,2.61-0.06,8.01c-0.5,4.7-4.17,8.84-4.17,8.84s2.4-0.67,3.72-0.94
57
+
c1.33-0.28,3.48-0.72,3.48-0.72s1.22-2.54,1.27-6.91C77.41,39.35,75.23,33.99,73.1,35.27z"/>
58
+
<path style="fill:#39BDE3;" d="M75.37,21.18c-0.17,0.31,0.44,1.88,0.83,3.43c0.39,1.55,0.64,3.68,1.11,3.87
59
+
c0.66,0.28,3.91-1.57,2.93-5.08C79.18,19.63,75.7,20.57,75.37,21.18z"/>
60
+
<path style="fill:#39BDE3;" d="M83.77,25.49c-0.77,0.88,0.33,2.04,0.72,3.59c0.39,1.55,0.48,3.18,0.94,3.37
61
+
c0.66,0.28,3.87-1.33,3.26-4.48C87.95,24.15,84.22,24.97,83.77,25.49z"/>
62
+
<path style="fill:#39BDE3;" d="M81.32,15.9c-0.48,0.97,0.52,2.02,0.9,4.01c0.3,1.57,0.53,3.56,0.99,3.76
63
+
c0.66,0.28,4.19-1.95,3.2-5.47C85.37,14.44,81.62,15.28,81.32,15.9z"/>
64
+
<path style="fill:#39BDE3;" d="M90.93,20.21c-0.17,0.31,0.44,1.88,0.83,3.43c0.39,1.55,0.64,3.68,1.11,3.87
65
+
c0.66,0.28,3.91-1.57,2.93-5.08C94.74,18.67,91.26,19.61,90.93,20.21z"/>
66
+
<path style="fill:#39BDE3;" d="M96.18,15.57c-0.15,0.32,0.52,2.51,0.9,4.06c0.39,1.55,0.75,3.29,1.22,3.48
67
+
c0.66,0.28,3.87-1.71,3.04-5.53C100.51,13.78,96.64,14.61,96.18,15.57z"/>
68
+
<path style="fill:#39BDE3;" d="M99.05,24.6c-0.19,0.33,0.64,2.42,1.07,4.04c0.43,1.63,0.59,3.38,1.1,3.62
69
+
c1,0.47,4.52-1.69,3.28-5.34C103.38,23.61,99.42,23.96,99.05,24.6z"/>
70
+
<path style="fill:#39BDE3;" d="M93,29.58c-0.17,0.34,0.66,1.84,1.16,3.7c0.45,1.68,0.67,4.01,1.13,4.22
71
+
c0.66,0.31,4.08-1.36,3.13-5.21C97.39,28.17,93.33,28.92,93,29.58z"/>
72
+
<path style="fill:#39BDE3;" d="M77.97,30.63c-0.17,0.31,0.39,2.21,0.77,3.76c0.39,1.55,0.62,3.32,1.09,3.51
73
+
c0.66,0.28,3.82-1.54,2.93-5.08C82,29.8,78.3,30.02,77.97,30.63z"/>
74
+
<path style="fill:#39BDE3;" d="M86.18,35.46c-0.48,0.86,0.41,1.91,0.79,3.45c0.39,1.55,0.69,3.62,1.14,3.84
75
+
c0.96,0.47,3.91-1.66,3.28-4.83C90.64,34.1,86.52,34.86,86.18,35.46z"/>
76
+
<path style="fill:#39BDE3;" d="M101.24,35.08c-0.17,0.3,0.42,2.19,0.93,3.67s1.08,3.17,1.56,3.32c0.68,0.22,4.11-2.36,2.92-5.75
77
+
C105.59,33.34,101.67,34.33,101.24,35.08z"/>
78
+
<path style="fill:#FEC516;" d="M80.7,77.14c-0.16,0.32,0.5,1.88,0.95,3.51c0.42,1.55,0.91,3.36,1.38,3.54
79
+
c0.67,0.26,3.87-1.67,2.81-5.18C84.7,75.26,81.02,76.52,80.7,77.14z"/>
80
+
<path style="fill:#FEC516;" d="M86.27,71.68c-0.17,0.31,0.79,2.03,1.31,3.45c0.55,1.5,0.64,3.68,1.11,3.87
81
+
c0.66,0.28,3.43-1.35,2.59-4.9C90.43,70.49,86.6,71.07,86.27,71.68z"/>
82
+
<path style="fill:#FEC516;" d="M78.11,67.53c-0.18,0.34,0.54,2,1,3.64c0.47,1.64,0.72,3.07,1.15,3.42c1.13,0.9,4.38-0.71,3.36-4.71
83
+
C82.59,65.82,78.46,66.87,78.11,67.53z"/>
84
+
<path style="fill:#FEC516;" d="M84.16,62.1c-0.66,0.47,0.32,1.9,0.6,3.47c0.28,1.57,0.3,3.4,0.77,3.58c1.35,0.51,4.27-0.3,3.55-4.45
85
+
C88.4,60.82,84.73,61.7,84.16,62.1z"/>
86
+
<path style="fill:#FEC516;" d="M92.23,66.89c-0.23,1.07,0.97,1.48,1.66,3.57c0.6,1.83,0.47,3.09,0.95,3.24
87
+
c1.46,0.45,4.65-2.1,3.47-5.12C96.91,64.97,92.38,66.17,92.23,66.89z"/>
88
+
<path style="fill:#FEC516;" d="M98.66,62.3c-0.17,0.31,0.33,1.38,1.02,3.26c0.55,1.5,0.64,3.68,1.11,3.87
89
+
c0.66,0.28,3.8-1.55,2.82-5.07C102.55,60.6,98.99,61.69,98.66,62.3z"/>
90
+
<path style="fill:#FEC516;" d="M101.52,71.6c-0.17,0.31,0.51,1.78,0.9,3.06c0.46,1.53,0.41,3.18,1.12,3.66
91
+
c1.07,0.73,4.17-1.48,3.04-4.96C105.54,70.13,101.86,70.99,101.52,71.6z"/>
92
+
<path style="fill:#FEC516;" d="M95.6,76.05c-0.17,0.31,0.38,1.91,0.64,3.5c0.28,1.77,0.27,3.34,0.72,3.55
93
+
c1.07,0.51,4.64-0.82,3.77-4.62C99.87,74.72,95.93,75.45,95.6,76.05z"/>
94
+
<path style="fill:#FEC516;" d="M103.86,80.66c-0.17,0.31,0.44,1.88,0.83,3.43c0.39,1.55,0.7,3,1.1,3.3c0.9,0.68,4.06-0.9,3.22-4.56
95
+
C108.13,79.02,104.19,80.05,103.86,80.66z"/>
96
+
<path style="fill:#FEC516;" d="M88.88,81.06c-0.48,0.65,0.6,2.13,0.79,3.72c0.23,1.86,0.28,3.27,0.73,3.49
97
+
c1.29,0.62,4.81-1.41,3.61-4.95C92.79,79.68,89.29,80.5,88.88,81.06z"/>
98
+
<path style="fill:#FEC516;" d="M72.8,97.85c0,0,3.47-3.86,3.39-6.75c-0.11-4.33-1.35-6.47,0.34-7.09c1.4-0.51,2.93,1.75,3.49,4.17
99
+
c1.35,5.82,0.06,8.1,0.06,8.1s-2.13,0.46-3.78,0.79C74.89,97.34,72.8,97.85,72.8,97.85z"/>
100
+
</svg>
+4
web/server.go
+4
web/server.go
···
18
18
//go:embed templates/*
19
19
var templatesFS embed.FS
20
20
21
+
//go:embed public/*
22
+
var publicFS embed.FS
23
+
21
24
const (
22
25
// HTTP rate limiting
23
26
httpRequestsPerSecond = 10
···
56
59
57
60
mux.HandleFunc("/", s.routeHandler)
58
61
mux.HandleFunc("/style.css", s.handleStyleCSS)
62
+
mux.HandleFunc("/favicon.svg", s.handleFaviconSVG)
59
63
mux.HandleFunc("/health", s.handleHealth)
60
64
mux.HandleFunc("/metrics", s.handleMetrics)
61
65
+1
-1
web/templates/404.html
+1
-1
web/templates/404.html
···
4
4
<meta charset="utf-8">
5
5
<meta name="viewport" content="width=device-width, initial-scale=1">
6
6
<title>HERALD - {{if .Title}}{{.Title}}{{else}}404{{end}}</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>">
7
+
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
8
8
<link rel="stylesheet" href="/style.css">
9
9
</head>
10
10
<body>
+37
-2
web/templates/index.html
+37
-2
web/templates/index.html
···
3
3
<head>
4
4
<meta charset="utf-8">
5
5
<meta name="viewport" content="width=device-width, initial-scale=1">
6
-
<title>HERALD</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>">
6
+
<title>HERALD - RSS-to-Email via SSH</title>
7
+
<meta name="description" content="Herald delivers RSS feeds to your inbox via SSH. Upload feed configs, manage subscriptions, and receive updates by email.">
8
+
9
+
<!-- Open Graph -->
10
+
<meta property="og:type" content="website">
11
+
<meta property="og:title" content="HERALD - RSS-to-Email via SSH">
12
+
<meta property="og:description" content="Herald delivers RSS feeds to your inbox via SSH. Upload feed configs, manage subscriptions, and receive updates by email.">
13
+
<meta property="og:url" content="{{.Origin}}">
14
+
<meta property="og:image" content="https://l4.dunkirk.sh/i/AQ0z6BnQAwWu.webp">
15
+
<meta property="og:site_name" content="HERALD">
16
+
17
+
<!-- Twitter Card -->
18
+
<meta name="twitter:card" content="summary_large_image">
19
+
<meta name="twitter:title" content="HERALD - RSS-to-Email via SSH">
20
+
<meta name="twitter:description" content="Herald delivers RSS feeds to your inbox via SSH. Upload feed configs, manage subscriptions, and receive updates by email.">
21
+
<meta name="twitter:image" content="https://l4.dunkirk.sh/i/AQ0z6BnQAwWu.webp">
22
+
23
+
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
8
24
<link rel="stylesheet" href="/style.css">
9
25
</head>
10
26
<body>
···
13
29
<h2>NAME</h2>
14
30
<pre>
15
31
herald - RSS-to-Email via SSH
32
+
</pre>
33
+
34
+
<h2>FILE FORMAT</h2>
35
+
<pre>
36
+
Feed configs are plain text files with directives:
37
+
38
+
=: email you@example.com Recipient email address
39
+
=: cron 0 8 * * * Standard cron (5 fields)
40
+
=: digest true Combine items (default: true)
41
+
=: inline false Include content (default: false)
42
+
=> https://example.com/feed RSS/Atom feed URL
43
+
=> https://blog.com/rss "Blog" Feed with display name
44
+
45
+
Example feeds.txt:
46
+
=: email you@example.com
47
+
=: cron 0 8 * * *
48
+
=: digest true
49
+
=> https://dunkirk.sh/atom.xml
50
+
=> https://news.ycombinator.com/rss
16
51
</pre>
17
52
18
53
<h2>COMMANDS</h2>
+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="/favicon.svg" type="image/svg+xml">
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
+
}
+1
-1
web/templates/unsubscribe.html
+1
-1
web/templates/unsubscribe.html
···
4
4
<meta charset="utf-8">
5
5
<meta name="viewport" content="width=device-width, initial-scale=1">
6
6
<title>HERALD - Unsubscribe</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>">
7
+
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
8
8
<link rel="stylesheet" href="/style.css">
9
9
</head>
10
10
<body>
+17
-1
web/templates/user.html
+17
-1
web/templates/user.html
···
4
4
<meta charset="utf-8">
5
5
<meta name="viewport" content="width=device-width, initial-scale=1">
6
6
<title>HERALD - {{.ShortFingerprint}}</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>">
7
+
<meta name="description" content="Herald dashboard for user {{.ShortFingerprint}}. View feed configs and delivery status.">
8
+
9
+
<!-- Open Graph -->
10
+
<meta property="og:type" content="website">
11
+
<meta property="og:title" content="HERALD - {{.ShortFingerprint}}">
12
+
<meta property="og:description" content="Herald dashboard for user {{.ShortFingerprint}}. View feed configs and delivery status.">
13
+
<meta property="og:url" content="{{.Origin}}/{{.Fingerprint}}">
14
+
<meta property="og:image" content="https://l4.dunkirk.sh/i/AQ0z6BnQAwWu.webp">
15
+
<meta property="og:site_name" content="HERALD">
16
+
17
+
<!-- Twitter Card -->
18
+
<meta name="twitter:card" content="summary_large_image">
19
+
<meta name="twitter:title" content="HERALD - {{.ShortFingerprint}}">
20
+
<meta name="twitter:description" content="Herald dashboard for user {{.ShortFingerprint}}. View feed configs and delivery status.">
21
+
<meta name="twitter:image" content="https://l4.dunkirk.sh/i/AQ0z6BnQAwWu.webp">
22
+
23
+
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
8
24
<link rel="stylesheet" href="/style.css">
9
25
</head>
10
26
<body>