feat(notifications): ntfy/Slack/Discord/generic webhook for the push events - #523
Open
opticon454 wants to merge 2 commits into
Open
opticon454 wants to merge 2 commits into
opticon454 wants to merge 2 commits into
Conversation
…events Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…rowser test Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Contributor
Author
|
Heads-up on merge order. I checked my open PRs against each other with trial merges. Every overlap is a textual conflict in a shared registry or list (adjacent insertions), not a behavioural interaction, but whichever lands first, the others need a rebase:
I'm not changing this PR for it. As the others merge I will rebase this one onto master, resolve the conflicts, re-run the full gate and push. If you have a preferred merge order, tell me and I will follow it. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Opt-in webhook notifications. The same events that trigger Web Push (permission prompts, questions, errors, respawn blocked, crash loop, task complete, idle, response complete) can also be posted to ntfy, Slack, Discord, or any JSON URL, so a headless server can reach a phone with no browser tab open and no push subscription.
Settings → Notifications → "Webhook (ntfy, Slack, Discord)": enable, pick the service, paste the URL, choose "Needs attention" (skips the routine "response complete") or "Everything", Save, Send test. Off by default.
Design decisions worth a look
GET /api/settingsis readable by every logged-in user. So it is not a setting: it lives in~/.codeman/webhook.json(0600, tmp+rename, likecustom-model-hosts.json), is only written throughPUT /api/webhook, and is never returned (the API and UI show scheme + host only). The URL box is write-only; changing only the service or scope never needs the secret re-sent. All three routes are admin-only in multi-user mode (the channel receives every session's events, the same reach an admin's own Web Push has).webviewFetch): link-local and cloud-metadata targets are refused on the resolved address at connect time; save-time validation also rejects non-http(s),user:pass@, and those targets. Loopback and LAN stay allowed on purpose (a local ntfy is the feature). Redirects are not followed, the call times out after 5 s, and error text never contains the URL.allowed_mentions: { parse: [] }, Slack control characters (<!channel>,<@U…>,<url|text>) are escaped, and ntfy headers are RFC 2047-encoded so a title can't inject a header.sendPushNotifications(that is the headless case), never delays Web Push, dedupes the same event+session within 3 s, and caps in-flight requests at 5. The notification body text moved into a small static method so both channels share it; Web Push output is unchanged.Tests
test/webhook-notify.test.ts(39): URL validation, masking, scope, each service's request format, store (0600, tightens an existing 0644 file, coerces bad values), sender (status/redirect/timeout/network errors with no URL leak), notifier (dedupe, in-flight cap, test send), plus real delivery to a local HTTP server throughwebviewFetchand refusal of a metadata address / redirect-to-metadata.test/routes/webhook-routes.test.ts(17): masking, partial updates, clearing, 400s, strict schema, test route, multi-user gating.test/webhook-settings.browser.test.ts(6): real server + real Chromium + a local receiver: the UI saves, masks, sends a test that arrives with the ntfy headers, and shows a failing endpoint without exposing the URL.git-clonetests timed out once under load and pass alone, unrelated to this change).🤖 Generated with Claude Code