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

Compare changes

Choose any two refs to compare.

+20 -1
README.md
··· 1 # Herald ๐ŸŽ 2 3 - ![email from harold](https://l4.dunkirk.sh/i/Ck271POS5n0k.webp) 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 + ![herald web interface](https://l4.dunkirk.sh/i/TuJ7meLh1JB9.webp) 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 + ![email from harold](https://l4.dunkirk.sh/i/Ck271POS5n0k.webp) 145 146 <p align="center"> 147 <img src="https://raw.githubusercontent.com/taciturnaxolotl/carriage/main/.github/images/line-break.svg" />
+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
···
··· 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
··· 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 },
··· 1 { 2 "nodes": { 3 "nixpkgs": { 4 "locked": { 5 "lastModified": 1767892417, ··· 18 }, 19 "root": { 20 "inputs": { 21 "nixpkgs": "nixpkgs" 22 } 23 } 24 },
+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
··· 49 if err := fang.Execute( 50 context.Background(), 51 rootCmd, 52 fang.WithNotifySignal(os.Interrupt, os.Kill), 53 ); err != nil { 54 os.Exit(1)
··· 49 if err := fang.Execute( 50 context.Background(), 51 rootCmd, 52 + fang.WithVersion(version), 53 + fang.WithCommit(commitHash), 54 fang.WithNotifySignal(os.Interrupt, os.Kill), 55 ); err != nil { 56 os.Exit(1)
+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
··· 288 } 289 290 func formatRelativeTime(t time.Time) string { 291 - now := time.Now() 292 diff := t.Sub(now) 293 294 if diff < 0 {
··· 288 } 289 290 func formatRelativeTime(t time.Time) string { 291 + now := time.Now().UTC() 292 diff := t.Sub(now) 293 294 if diff < 0 {
+1 -1
ssh/scp.go
··· 182 } 183 184 func calculateNextRun(cronExpr string) (time.Time, error) { 185 - return gronx.NextTick(cronExpr, false) 186 } 187 188 type configFileInfo struct {
··· 182 } 183 184 func calculateNextRun(cronExpr string) (time.Time, error) { 185 + return gronx.NextTick(cronExpr, true) 186 } 187 188 type configFileInfo struct {
+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
··· 42 ID: id, 43 PubkeyFP: pubkeyFP, 44 Pubkey: pubkey, 45 - CreatedAt: time.Now(), 46 }, nil 47 } 48
··· 42 ID: id, 43 PubkeyFP: pubkeyFP, 44 Pubkey: pubkey, 45 + CreatedAt: time.Now().UTC(), 46 }, nil 47 } 48
+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
···
··· 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
··· 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
··· 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>
··· 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="/favicon.svg" type="image/svg+xml"> 8 <link rel="stylesheet" href="/style.css"> 9 </head> 10 <body>
+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 + =&gt; https://example.com/feed RSS/Atom feed URL 43 + =&gt; 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 + =&gt; https://dunkirk.sh/atom.xml 50 + =&gt; https://news.ycombinator.com/rss 51 </pre> 52 53 <h2>COMMANDS</h2>
+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
··· 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
··· 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>
··· 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="/favicon.svg" type="image/svg+xml"> 8 <link rel="stylesheet" href="/style.css"> 9 </head> 10 <body>
+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>