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

4
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
···
41
```bash
42
scp feeds.txt user@herald.dunkirk.sh:
43
```
44
45
### SSH Commands
46
···
123
- `HERALD_SMTP_USER`
124
- `HERALD_SMTP_PASS`
125
- `HERALD_SMTP_FROM`
126
127
<p align="center">
128
<img src="https://raw.githubusercontent.com/taciturnaxolotl/carriage/main/.github/images/line-break.svg" />
···
1
# Herald ๐
2
3
+

4
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
···
41
```bash
42
scp feeds.txt user@herald.dunkirk.sh:
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`
57
58
### SSH Commands
59
···
136
- `HERALD_SMTP_USER`
137
- `HERALD_SMTP_PASS`
138
- `HERALD_SMTP_FROM`
139
+
140
+
## Screenshots
141
+
142
+
here is an example of what an email digest looks like:
143
+
144
+

145
146
<p align="center">
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
{
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
"nixpkgs": {
22
"locked": {
23
"lastModified": 1767892417,
···
36
},
37
"root": {
38
"inputs": {
39
-
"flake-utils": "flake-utils",
40
"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
}
57
}
58
},
+68
-24
flake.nix
+68
-24
flake.nix
···
3
4
inputs = {
5
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
6
-
flake-utils.url = "github:numtide/flake-utils";
7
};
8
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
-
];
24
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
-
);
33
}
···
3
4
inputs = {
5
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
6
};
7
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
+
);
43
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
+
};
77
}
+2
main.go
+2
main.go
+6
-6
scheduler/scheduler.go
+6
-6
scheduler/scheduler.go
···
179
}
180
}()
181
182
-
now := time.Now()
183
configs, err := s.store.GetDueConfigs(ctx, now)
184
if err != nil {
185
s.logger.Error("failed to get due configs", "err", err)
···
257
s.logger.Debug("RunNow: feed metadata updated")
258
259
s.logger.Debug("RunNow: calculating next run")
260
-
now := time.Now()
261
-
nextRun, err := gronx.NextTick(cfg.CronExpr, false)
262
if err != nil {
263
return stats, fmt.Errorf("calculate next run: %w", err)
264
}
···
277
func (s *Scheduler) collectNewItems(ctx context.Context, results []*FetchResult) ([]email.FeedGroup, int, error) {
278
var feedGroups []email.FeedGroup
279
totalNew := 0
280
-
maxAge := time.Now().Add(-itemMaxAge)
281
feedErrors := 0
282
283
for _, result := range results {
···
486
}
487
}
488
489
-
now := time.Now()
490
-
nextRun, err := gronx.NextTick(cfg.CronExpr, false)
491
if err != nil {
492
return fmt.Errorf("calculate next run: %w", err)
493
}
···
179
}
180
}()
181
182
+
now := time.Now().UTC()
183
configs, err := s.store.GetDueConfigs(ctx, now)
184
if err != nil {
185
s.logger.Error("failed to get due configs", "err", err)
···
257
s.logger.Debug("RunNow: feed metadata updated")
258
259
s.logger.Debug("RunNow: calculating next run")
260
+
now := time.Now().UTC()
261
+
nextRun, err := gronx.NextTick(cfg.CronExpr, true)
262
if err != nil {
263
return stats, fmt.Errorf("calculate next run: %w", err)
264
}
···
277
func (s *Scheduler) collectNewItems(ctx context.Context, results []*FetchResult) ([]email.FeedGroup, int, error) {
278
var feedGroups []email.FeedGroup
279
totalNew := 0
280
+
maxAge := time.Now().UTC().Add(-itemMaxAge)
281
feedErrors := 0
282
283
for _, result := range results {
···
486
}
487
}
488
489
+
now := time.Now().UTC()
490
+
nextRun, err := gronx.NextTick(cfg.CronExpr, true)
491
if err != nil {
492
return fmt.Errorf("calculate next run: %w", err)
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
InlineContent: inline,
50
RawText: rawText,
51
NextRun: sql.NullTime{Time: nextRun, Valid: true},
52
-
CreatedAt: time.Now(),
53
}, nil
54
}
55
···
78
InlineContent: inline,
79
RawText: rawText,
80
NextRun: sql.NullTime{Time: nextRun, Valid: true},
81
-
CreatedAt: time.Now(),
82
}, nil
83
}
84
···
219
return err
220
}
221
222
-
nextRun, err := gronx.NextTick(cfg.CronExpr, false)
223
if err != nil {
224
return fmt.Errorf("calculate next run: %w", err)
225
}
···
49
InlineContent: inline,
50
RawText: rawText,
51
NextRun: sql.NullTime{Time: nextRun, Valid: true},
52
+
CreatedAt: time.Now().UTC(),
53
}, nil
54
}
55
···
78
InlineContent: inline,
79
RawText: rawText,
80
NextRun: sql.NullTime{Time: nextRun, Valid: true},
81
+
CreatedAt: time.Now().UTC(),
82
}, nil
83
}
84
···
219
return err
220
}
221
222
+
nextRun, err := gronx.NextTick(cfg.CronExpr, true)
223
if err != nil {
224
return fmt.Errorf("calculate next run: %w", err)
225
}
+1
-1
store/users.go
+1
-1
store/users.go
+21
-21
web/handlers.go
+21
-21
web/handlers.go
···
60
_, _ = w.Write(css)
61
}
62
63
type userPageData struct {
64
Fingerprint string
65
ShortFingerprint string
···
684
return hostPort
685
}
686
687
func (s *Server) handleKeepAlive(w http.ResponseWriter, r *http.Request, token string) {
688
// Only allow GET requests
689
if r.Method != http.MethodGet {
···
701
// Calculate new expiry date (90 days from now)
702
expiresAt := time.Now().AddDate(0, 0, 90).Format("January 2, 2006")
703
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)
725
}
726
}
727
···
60
_, _ = w.Write(css)
61
}
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
+
73
type userPageData struct {
74
Fingerprint string
75
ShortFingerprint string
···
694
return hostPort
695
}
696
697
+
type keepAlivePageData struct {
698
+
ExpiresAt string
699
+
}
700
+
701
func (s *Server) handleKeepAlive(w http.ResponseWriter, r *http.Request, token string) {
702
// Only allow GET requests
703
if r.Method != http.MethodGet {
···
715
// Calculate new expiry date (90 days from now)
716
expiresAt := time.Now().AddDate(0, 0, 90).Format("January 2, 2006")
717
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
}
726
}
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
//go:embed templates/*
19
var templatesFS embed.FS
20
21
const (
22
// HTTP rate limiting
23
httpRequestsPerSecond = 10
···
56
57
mux.HandleFunc("/", s.routeHandler)
58
mux.HandleFunc("/style.css", s.handleStyleCSS)
59
mux.HandleFunc("/health", s.handleHealth)
60
mux.HandleFunc("/metrics", s.handleMetrics)
61
···
18
//go:embed templates/*
19
var templatesFS embed.FS
20
21
+
//go:embed public/*
22
+
var publicFS embed.FS
23
+
24
const (
25
// HTTP rate limiting
26
httpRequestsPerSecond = 10
···
59
60
mux.HandleFunc("/", s.routeHandler)
61
mux.HandleFunc("/style.css", s.handleStyleCSS)
62
+
mux.HandleFunc("/favicon.svg", s.handleFaviconSVG)
63
mux.HandleFunc("/health", s.handleHealth)
64
mux.HandleFunc("/metrics", s.handleMetrics)
65
+1
-1
web/templates/404.html
+1
-1
web/templates/404.html
···
4
<meta charset="utf-8">
5
<meta name="viewport" content="width=device-width, initial-scale=1">
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>">
8
<link rel="stylesheet" href="/style.css">
9
</head>
10
<body>
+37
-2
web/templates/index.html
+37
-2
web/templates/index.html
···
3
<head>
4
<meta charset="utf-8">
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>">
8
<link rel="stylesheet" href="/style.css">
9
</head>
10
<body>
···
13
<h2>NAME</h2>
14
<pre>
15
herald - RSS-to-Email via SSH
16
</pre>
17
18
<h2>COMMANDS</h2>
···
3
<head>
4
<meta charset="utf-8">
5
<meta name="viewport" content="width=device-width, initial-scale=1">
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">
24
<link rel="stylesheet" href="/style.css">
25
</head>
26
<body>
···
29
<h2>NAME</h2>
30
<pre>
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
51
</pre>
52
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
padding: 0;
4
box-sizing: border-box;
5
}
6
html, body {
7
font-family: monospace;
8
-
background: #0a0a0a;
9
-
color: #e0e0e0;
10
}
11
body {
12
max-width: 80ch;
13
margin: 2rem auto;
14
padding: 1rem;
15
line-height: 1.5;
16
}
17
a {
18
-
color: #8ab4f8;
19
text-decoration: underline;
20
}
21
-
a:hover {
22
-
color: #fff;
23
-
}
24
pre {
25
white-space: pre-wrap;
26
margin: 1rem 0;
27
}
28
h1 {
29
font-weight: bold;
30
font-size: 1rem;
31
-
border-bottom: 2px solid #333;
32
padding-bottom: 0.5rem;
33
margin-bottom: 1rem;
34
}
35
h2 {
36
font-weight: bold;
37
font-size: 1rem;
38
margin-top: 1.5rem;
39
margin-bottom: 0.5rem;
40
}
41
p {
42
margin: 0.5rem 0;
43
}
44
ul {
45
list-style: none;
46
margin: 0.5rem 0 1rem 2rem;
47
}
48
li {
49
margin: 0.25rem 0;
50
}
51
strong {
52
font-weight: bold;
53
}
54
.inactive {
55
text-decoration: line-through;
56
opacity: 0.6;
57
}
58
footer {
59
display: flex;
60
justify-content: space-between;
61
margin-top: 2rem;
62
padding-top: 1rem;
63
-
border-top: 1px solid #333;
64
font-size: 0.9rem;
65
opacity: 0.8;
66
}
···
3
padding: 0;
4
box-sizing: border-box;
5
}
6
+
7
html, body {
8
font-family: monospace;
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
+
}
63
}
64
+
65
body {
66
max-width: 80ch;
67
margin: 2rem auto;
68
padding: 1rem;
69
line-height: 1.5;
70
}
71
+
72
a {
73
text-decoration: underline;
74
}
75
+
76
pre {
77
white-space: pre-wrap;
78
margin: 1rem 0;
79
}
80
+
81
h1 {
82
font-weight: bold;
83
font-size: 1rem;
84
padding-bottom: 0.5rem;
85
margin-bottom: 1rem;
86
}
87
+
88
h2 {
89
font-weight: bold;
90
font-size: 1rem;
91
margin-top: 1.5rem;
92
margin-bottom: 0.5rem;
93
}
94
+
95
p {
96
margin: 0.5rem 0;
97
}
98
+
99
ul {
100
list-style: none;
101
margin: 0.5rem 0 1rem 2rem;
102
}
103
+
104
li {
105
margin: 0.25rem 0;
106
}
107
+
108
strong {
109
font-weight: bold;
110
}
111
+
112
.inactive {
113
text-decoration: line-through;
114
opacity: 0.6;
115
}
116
+
117
footer {
118
display: flex;
119
justify-content: space-between;
120
margin-top: 2rem;
121
padding-top: 1rem;
122
font-size: 0.9rem;
123
opacity: 0.8;
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
<meta charset="utf-8">
5
<meta name="viewport" content="width=device-width, initial-scale=1">
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>">
8
<link rel="stylesheet" href="/style.css">
9
</head>
10
<body>
+17
-1
web/templates/user.html
+17
-1
web/templates/user.html
···
4
<meta charset="utf-8">
5
<meta name="viewport" content="width=device-width, initial-scale=1">
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>">
8
<link rel="stylesheet" href="/style.css">
9
</head>
10
<body>
···
4
<meta charset="utf-8">
5
<meta name="viewport" content="width=device-width, initial-scale=1">
6
<title>HERALD - {{.ShortFingerprint}}</title>
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">
24
<link rel="stylesheet" href="/style.css">
25
</head>
26
<body>