From 24dc36a013cfcca236785d97341fe9dd71e6c576 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=BF=20corey?= Date: Sun, 12 Apr 2026 21:21:30 -0700 Subject: [PATCH 01/11] feat(glyph): add ntfy + Slack notification relay, migrate from Pushover - Add ntfy-sh service (localhost:2586) as a unified notification bus - Add ntfy-slack-relay systemd service that subscribes to the homelab topic and forwards to Slack via chat.postMessage; sender identity driven by NTFY_TITLE header so each service appears distinctly - Migrate transmission torrent-done script from Pushover to ntfy POST - Add slack-bot-token agenix secret; remove pushover-{app,user}-token Co-Authored-By: Claude Sonnet 4.6 --- hosts/glyph/secrets/slack-bot-token.age | Bin 0 -> 382 bytes hosts/glyph/services/default.nix | 1 + hosts/glyph/services/ntfy.nix | 58 ++++++++++++++++++++++++ hosts/glyph/services/torrents.nix | 38 ++-------------- lib/secrets/glyph.nix | 3 +- 5 files changed, 64 insertions(+), 36 deletions(-) create mode 100644 hosts/glyph/secrets/slack-bot-token.age create mode 100644 hosts/glyph/services/ntfy.nix diff --git a/hosts/glyph/secrets/slack-bot-token.age b/hosts/glyph/secrets/slack-bot-token.age new file mode 100644 index 0000000000000000000000000000000000000000..3f0364b983b7938cde5264e89cff60e62da97f9b GIT binary patch literal 382 zcmYdHPt{G$OD?J`D9Oyv)5|YP*Do{V(zR14F3!+RO))YxHMCSH3NF$va#S!XFDVX9 zOmPWt3H8ZwuSiY_@HX-exAZnlukv(H3-T#849*O2cXBk(u;41q4+=AL&C}1&PD;%; zOmR*rb51SwNR0|IwRCs3OfN5XNh~!F3dq$CC`Y%=*fl&OFHj-8JS-qLIlLs?P`kXs ztkT!p%&@@1*udGxr_4w{J6Jz2)y37>BF(Kl$AHVpI6XD9Dk{V%!X(+yCn?uIJ;Ev2 zIXKD7x3bVQrJyP#M>{tv&CfWpJex~bS69K**G1dNxiHlz!rP$Iv?Rx?vNXsiyr3wn zqQo#gJGaWX%Beir!=gAN(2?uqTfx8oXFboHd$54TVcO)9Ef(*TcyCu7D4OuN_1<6Q zuNrc3lXlFVzQl%WzKxvj(+A(;l&z-9`l?O87 Date: Sun, 12 Apr 2026 21:25:25 -0700 Subject: [PATCH 02/11] fix(glyph): use full URL for ntfy subscribe topic ntfy subscribe takes the server as part of the topic URL, not a separate --url flag. Co-Authored-By: Claude Sonnet 4.6 --- hosts/glyph/services/ntfy.nix | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/hosts/glyph/services/ntfy.nix b/hosts/glyph/services/ntfy.nix index 638a1d69..77eae35b 100644 --- a/hosts/glyph/services/ntfy.nix +++ b/hosts/glyph/services/ntfy.nix @@ -50,8 +50,7 @@ in { }; script = '' exec ${pkgs.ntfy-sh}/bin/ntfy subscribe \ - --url ${ntfyUrl} \ - ${ntfyTopic} \ + ${ntfyUrl}/${ntfyTopic} \ ${ntfyToSlack} ''; }; From 64e5de2e696f5fb8d274ade83783abbb78315db4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=BF=20corey?= Date: Sun, 12 Apr 2026 21:31:08 -0700 Subject: [PATCH 03/11] fix(glyph): route ntfy Slack notifications to #updates Co-Authored-By: Claude Sonnet 4.6 --- hosts/glyph/services/ntfy.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hosts/glyph/services/ntfy.nix b/hosts/glyph/services/ntfy.nix index 77eae35b..ed776983 100644 --- a/hosts/glyph/services/ntfy.nix +++ b/hosts/glyph/services/ntfy.nix @@ -5,7 +5,7 @@ }: let ntfyUrl = "http://127.0.0.1:2586"; ntfyTopic = "homelab"; - slackChannel = "#homelab"; + slackChannel = "#updates"; ntfyToSlack = pkgs.writeShellScript "ntfy-to-slack" '' SLACK_TOKEN=$(cat ${config.age.secrets.slack-bot-token.path}) From 5da8ef8d87444f70101ff92adaf0414464952b55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=BF=20corey?= Date: Sun, 12 Apr 2026 21:35:01 -0700 Subject: [PATCH 04/11] fix(glyph): subscribe to ntfy JSON stream via curl instead of ntfy CLI ntfy subscribe executes commands via sh internally, which is not in the service PATH. Replace with a direct curl subscription to the /topic/json SSE endpoint, parsing each event with jq and forwarding to Slack ourselves. Co-Authored-By: Claude Sonnet 4.6 --- hosts/glyph/services/ntfy.nix | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/hosts/glyph/services/ntfy.nix b/hosts/glyph/services/ntfy.nix index ed776983..1ea3b47a 100644 --- a/hosts/glyph/services/ntfy.nix +++ b/hosts/glyph/services/ntfy.nix @@ -49,9 +49,18 @@ in { RestartSec = "10s"; }; script = '' - exec ${pkgs.ntfy-sh}/bin/ntfy subscribe \ - ${ntfyUrl}/${ntfyTopic} \ + ${pkgs.curl}/bin/curl -sN "${ntfyUrl}/${ntfyTopic}/json" | \ + while IFS= read -r event; do + event_type=$(${pkgs.jq}/bin/jq -r '.event // "message"' <<< "$event") + [ "$event_type" != "message" ] && continue + + NTFY_MESSAGE=$(${pkgs.jq}/bin/jq -r '.message // empty' <<< "$event") + NTFY_TITLE=$(${pkgs.jq}/bin/jq -r '.title // "Homelab"' <<< "$event") + [ -z "$NTFY_MESSAGE" ] && continue + + export NTFY_MESSAGE NTFY_TITLE ${ntfyToSlack} + done ''; }; } From 10ffe2867fdc9e32af6f29c6a2aef92cff28a95c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=BF=20corey?= Date: Sun, 12 Apr 2026 22:04:30 -0700 Subject: [PATCH 05/11] refactor(glyph): rename ntfy topic from homelab to notifications Co-Authored-By: Claude Sonnet 4.6 --- hosts/glyph/services/ntfy.nix | 2 +- hosts/glyph/services/torrents.nix | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/hosts/glyph/services/ntfy.nix b/hosts/glyph/services/ntfy.nix index 1ea3b47a..1ad40642 100644 --- a/hosts/glyph/services/ntfy.nix +++ b/hosts/glyph/services/ntfy.nix @@ -4,7 +4,7 @@ ... }: let ntfyUrl = "http://127.0.0.1:2586"; - ntfyTopic = "homelab"; + ntfyTopic = "notifications"; slackChannel = "#updates"; ntfyToSlack = pkgs.writeShellScript "ntfy-to-slack" '' diff --git a/hosts/glyph/services/torrents.nix b/hosts/glyph/services/torrents.nix index eb956af8..037dcdc2 100644 --- a/hosts/glyph/services/torrents.nix +++ b/hosts/glyph/services/torrents.nix @@ -19,7 +19,7 @@ curl -s \ -H "Title: Transmission" \ -d "$TR_TORRENT_NAME finished downloading." \ - http://127.0.0.1:2586/homelab + http://127.0.0.1:2586/notifications # Copy .mkv files to Unsorted for Jellyfin UNSORTED="/mnt/media/Unsorted" From b33f0f5512bc99f23988cfa7848ac58dd9626cc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=BF=20corey?= Date: Mon, 13 Apr 2026 13:13:16 -0700 Subject: [PATCH 06/11] fix(glyph): add pipefail to relay script so curl failures trigger restart Without pipefail, curl | while always exits 0 (the while loop's exit code), so systemd never restarts the relay on connection failure. With pipefail, a curl failure propagates as non-zero, triggering Restart=on-failure. Co-Authored-By: Claude Sonnet 4.6 --- hosts/glyph/services/ntfy.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/hosts/glyph/services/ntfy.nix b/hosts/glyph/services/ntfy.nix index 1ad40642..75d537bb 100644 --- a/hosts/glyph/services/ntfy.nix +++ b/hosts/glyph/services/ntfy.nix @@ -49,6 +49,7 @@ in { RestartSec = "10s"; }; script = '' + set -o pipefail ${pkgs.curl}/bin/curl -sN "${ntfyUrl}/${ntfyTopic}/json" | \ while IFS= read -r event; do event_type=$(${pkgs.jq}/bin/jq -r '.event // "message"' <<< "$event") From 2c6086abf793be7da02528636983d5b61590cfbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=BF=20corey?= Date: Mon, 13 Apr 2026 14:47:15 -0700 Subject: [PATCH 07/11] feat(glyph): append torrents link to download complete notification Co-Authored-By: Claude Sonnet 4.6 --- hosts/glyph/services/torrents.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hosts/glyph/services/torrents.nix b/hosts/glyph/services/torrents.nix index 037dcdc2..b4271ae2 100644 --- a/hosts/glyph/services/torrents.nix +++ b/hosts/glyph/services/torrents.nix @@ -18,7 +18,7 @@ script-torrent-done-filename = pkgs.writeShellScript "torrent-done.sh" '' curl -s \ -H "Title: Transmission" \ - -d "$TR_TORRENT_NAME finished downloading." \ + -d "$TR_TORRENT_NAME finished downloading. " \ http://127.0.0.1:2586/notifications # Copy .mkv files to Unsorted for Jellyfin From a4919f2dec6305573f66332298b5a47376f711bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=BF=20corey?= Date: Mon, 13 Apr 2026 15:13:49 -0700 Subject: [PATCH 08/11] feat(glyph): bold torrent name in download complete notification Co-Authored-By: Claude Sonnet 4.6 --- hosts/glyph/services/torrents.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hosts/glyph/services/torrents.nix b/hosts/glyph/services/torrents.nix index b4271ae2..1321a0e2 100644 --- a/hosts/glyph/services/torrents.nix +++ b/hosts/glyph/services/torrents.nix @@ -18,7 +18,7 @@ script-torrent-done-filename = pkgs.writeShellScript "torrent-done.sh" '' curl -s \ -H "Title: Transmission" \ - -d "$TR_TORRENT_NAME finished downloading. " \ + -d "*$TR_TORRENT_NAME* finished downloading. " \ http://127.0.0.1:2586/notifications # Copy .mkv files to Unsorted for Jellyfin From 1310115fd1d290e32e6d7afed38e0b8bef3c90d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=BF=20corey?= Date: Mon, 13 Apr 2026 15:51:19 -0700 Subject: [PATCH 09/11] feat(glyph): add per-service icon_emoji to Slack notifications via ntfy tags Senders set a Tags header on their ntfy POST; the relay extracts the first tag and passes it as icon_emoji to chat.postMessage. Transmission uses :transmissionic:. Co-Authored-By: Claude Sonnet 4.6 --- hosts/glyph/services/ntfy.nix | 4 +++- hosts/glyph/services/torrents.nix | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/hosts/glyph/services/ntfy.nix b/hosts/glyph/services/ntfy.nix index 75d537bb..4a9007ca 100644 --- a/hosts/glyph/services/ntfy.nix +++ b/hosts/glyph/services/ntfy.nix @@ -10,6 +10,7 @@ ntfyToSlack = pkgs.writeShellScript "ntfy-to-slack" '' SLACK_TOKEN=$(cat ${config.age.secrets.slack-bot-token.path}) TITLE="''${NTFY_TITLE:-Homelab}" + ICON=":''${NTFY_TAGS%%,*}:" ${pkgs.curl}/bin/curl -s -X POST \ "https://slack.com/api/chat.postMessage" \ @@ -18,8 +19,9 @@ -d "$(${pkgs.jq}/bin/jq -n \ --arg channel "${slackChannel}" \ --arg username "$TITLE" \ + --arg icon_emoji "$ICON" \ --arg text "$NTFY_MESSAGE" \ - '{channel: $channel, username: $username, text: $text}')" + '{channel: $channel, username: $username, icon_emoji: $icon_emoji, text: $text}')" ''; in { age.secrets.slack-bot-token = { diff --git a/hosts/glyph/services/torrents.nix b/hosts/glyph/services/torrents.nix index 1321a0e2..0eec1197 100644 --- a/hosts/glyph/services/torrents.nix +++ b/hosts/glyph/services/torrents.nix @@ -18,6 +18,7 @@ script-torrent-done-filename = pkgs.writeShellScript "torrent-done.sh" '' curl -s \ -H "Title: Transmission" \ + -H "Tags: transmissionic" \ -d "*$TR_TORRENT_NAME* finished downloading. " \ http://127.0.0.1:2586/notifications From fd28c18ba67a99bf0cf6f4fea79a32db07499d56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=BF=20corey?= Date: Mon, 13 Apr 2026 15:57:17 -0700 Subject: [PATCH 10/11] fix(glyph): extract NTFY_TAGS from JSON stream for icon_emoji support Tags were never parsed from the ntfy JSON event, so NTFY_TAGS was always empty and icon_emoji resolved to "::" causing a generic avatar. Co-Authored-By: Claude Sonnet 4.6 --- hosts/glyph/services/ntfy.nix | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hosts/glyph/services/ntfy.nix b/hosts/glyph/services/ntfy.nix index 4a9007ca..b7926807 100644 --- a/hosts/glyph/services/ntfy.nix +++ b/hosts/glyph/services/ntfy.nix @@ -59,9 +59,10 @@ in { NTFY_MESSAGE=$(${pkgs.jq}/bin/jq -r '.message // empty' <<< "$event") NTFY_TITLE=$(${pkgs.jq}/bin/jq -r '.title // "Homelab"' <<< "$event") + NTFY_TAGS=$(${pkgs.jq}/bin/jq -r '.tags // [] | join(",")' <<< "$event") [ -z "$NTFY_MESSAGE" ] && continue - export NTFY_MESSAGE NTFY_TITLE + export NTFY_MESSAGE NTFY_TITLE NTFY_TAGS ${ntfyToSlack} done ''; From 90fa6b3141f9d6ae1560f96811d57d5c78280ea2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=BF=20corey?= Date: Mon, 13 Apr 2026 16:16:04 -0700 Subject: [PATCH 11/11] chore(glyph): remove orphaned Pushover secret files Co-Authored-By: Claude Sonnet 4.6 --- hosts/glyph/secrets/pushover-app-token.age | 7 ------- hosts/glyph/secrets/pushover-user-token.age | Bin 353 -> 0 bytes 2 files changed, 7 deletions(-) delete mode 100644 hosts/glyph/secrets/pushover-app-token.age delete mode 100644 hosts/glyph/secrets/pushover-user-token.age diff --git a/hosts/glyph/secrets/pushover-app-token.age b/hosts/glyph/secrets/pushover-app-token.age deleted file mode 100644 index cd56f42f..00000000 --- a/hosts/glyph/secrets/pushover-app-token.age +++ /dev/null @@ -1,7 +0,0 @@ -age-encryption.org/v1 --> ssh-ed25519 rSr+rA DFhqDiFW44RqMF1tYITw+SRh1z9n2U86kDwmGE/LRiE -I/svO6AIKzxWEmTTiDfcermHSk+lX6r0Q9zKGm/Hj54 --> ssh-ed25519 3EWhnQ FvqdgY7RJ4dLrKPhq2b/Bq/etmyfiUHmN+AWjvKH6lE -Yuj7ChgnBlBIpkgVWBZ6LvobIvj39oGAbddp2c/muc4 ---- piuZZ76hH8VjCFW58QRXUAbVyPOjuvrMWtwIydOjxrM -^]Lcmʦ$O"5ѻ;Nib hc5l\̥ H'Yà6 \ No newline at end of file diff --git a/hosts/glyph/secrets/pushover-user-token.age b/hosts/glyph/secrets/pushover-user-token.age deleted file mode 100644 index b89c3cef2393c2301dcec5b71cbbec0df8f62fba..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 353 zcmYdHPt{G$OD?J`D9Oyv)5|YP*Do{V(zR14F3!+RO))YxHMCSH3NF$va#S$N&CkpY z3e+}>GuuQYQj4v2F1wX}!`adoOn%|^G)*fl&OFHoU4R68-K)U`0xEHFQ% z*eKhy(%;c9C$hji(I`95xzgXgJgqp`z$MJvu$ar)+o(7@$UVd