540c4590a95063bbe48839bbb5ecf499d8dc32bd
6
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
5e2c456e85 |
notify: the log names a notification endpoint, not its webhook URL (closes #288)
check / check (push) Canceled after 0s
With debug logging on, each delivery logged the full Slack, Mattermost or ntfy URL, whose path or query lets anyone who has it post to the channel or topic. The two debug lines now give the endpoint's name and the title; sendSlack is told which of Slack or Mattermost it sends to. A webhook URL that does not parse gave a startup error quoting the whole URL; it now gives only the reason. Errors from a failed request come from the transport, not from an HTTP client, and carry no part of the path or query; a test now checks that too. Model: opus-5-5 |
||
|
|
26c9c74d8e |
notify: a failed Mattermost delivery's error names Mattermost (closes #227)
check / check (push) Canceled after 0s
Mattermost is sent by the Slack sender, which wrapped every HTTP error status in ErrSlackFailed, so a Mattermost endpoint answering 503 was logged as "slack notification failed". The sender now takes the error to wrap: the Slack endpoint passes ErrSlackFailed and the Mattermost endpoint passes ErrMattermostFailed, which was defined but unused. A new delivery test sets both endpoints to a stand-in server answering 503 and checks the error logged for each names its own endpoint. Model: opus-5-5 |
||
|
|
ae06f7e3a1 |
notify: drain in-flight deliveries at shutdown (closes #106)
check / check (push) Successful in 1m22s
Shutdown waits for deliveries already in flight instead of dropping them. Reviewed and green; squashed to next by the dispatcher, dnswatcher having no manager. Model: opus-5 |
||
|
|
23f115053b |
feat: add retry with exponential backoff for notification delivery (#87)
check / check (push) Successful in 37s
## Summary Notifications were fire-and-forget: if Slack, Mattermost, or ntfy was temporarily down, changes were silently lost. This adds automatic retry with exponential backoff and jitter to all notification endpoints. ## Changes ### New file: `internal/notify/retry.go` - `RetryConfig` struct with configurable max retries, base delay, max delay - `backoff()` computes delay as `BaseDelay * 2^attempt`, capped at `MaxDelay`, with ±25% jitter - `deliverWithRetry()` wraps any send function with the retry loop - Defaults: 3 retries (4 total attempts), 1s base delay, 10s max delay - Context-aware: respects cancellation during retry sleep - Injectable `sleepFn` for test determinism ### Modified: `internal/notify/notify.go` - Added `retryConfig` and `sleepFn` fields to `Service` - Updated `dispatchNtfy`, `dispatchSlack`, `dispatchMattermost` to wrap sends in `deliverWithRetry` - Structured logging: warns on each retry, logs error only after all retries exhausted, logs info on success after retry ### Modified: `internal/notify/export_test.go` - Added test helpers: `SetRetryConfig`, `SetSleepFunc`, `DeliverWithRetry`, `BackoffDuration` ### New file: `internal/notify/retry_test.go` - Backoff calculation tests (exponential increase, max cap with jitter) - `deliverWithRetry` unit tests: first-attempt success, transient failure recovery, exhausted retries, context cancellation - Integration tests via `SendNotification`: transient failure retries, all-endpoints retry independently, permanent failure exhausts retries ## Verification - `make fmt` ✅ - `make check` (format + lint + tests + build) ✅ - `docker build .` ✅ - All existing tests continue to pass unchanged - No DNS client mocking — notification tests use `httptest` servers closes #62 Co-authored-by: clawbot <clawbot@noreply.git.eeqj.de> Reviewed-on: #87 Co-authored-by: clawbot <clawbot@noreply.example.org> Co-committed-by: clawbot <clawbot@noreply.example.org> |
||
|
|
1076543c23 |
feat: add unauthenticated web dashboard showing monitoring state and recent alerts (#83)
check / check (push) Successful in 4s
## Summary Adds a read-only web dashboard at `GET /` that shows the current monitoring state and recent alerts. Unauthenticated, single-page, no navigation. ## What it shows - **Summary bar**: counts of monitored domains, hostnames, ports, certificates - **Domains**: nameservers with last-checked age - **Hostnames**: per-nameserver DNS records, status badges, relative age - **Ports**: open/closed state with associated hostnames and age - **TLS Certificates**: CN, issuer, expiry (color-coded by urgency), status, age - **Recent Alerts**: last 100 notifications in reverse chronological order with priority badges Every data point displays its age (e.g. "5m ago") so freshness is visible at a glance. Auto-refreshes every 30 seconds. ## What it does NOT show No secrets: webhook URLs, ntfy topics, Slack/Mattermost endpoints, API tokens, and configuration details are never exposed. ## Design All assets (CSS) are embedded in the binary and served from `/s/`. Zero external HTTP requests at runtime — no CDN dependencies or third-party resources. Dark, technical aesthetic with saturated teals and blues on dark slate. Single page — everything on one screen. ## Implementation - `internal/notify/history.go` — thread-safe ring buffer (`AlertHistory`) storing last 100 alerts - `internal/notify/notify.go` — records each alert in history before dispatch; refactored `SendNotification` into smaller `dispatch*` helpers to satisfy funlen - `internal/handlers/dashboard.go` — `HandleDashboard()` handler with embedded HTML template, helper functions (`relTime`, `formatRecords`, `expiryDays`, `joinStrings`) - `internal/handlers/templates/dashboard.html` — Tailwind-styled single-page dashboard - `internal/handlers/handlers.go` — added `State` and `Notify` dependencies via fx - `internal/server/routes.go` — registered `GET /` route - `static/` — embedded CSS assets served via `/s/` prefix - `README.md` — documented the dashboard and new endpoint ## Tests - `internal/notify/history_test.go` — empty, add+recent ordering, overflow beyond capacity - `internal/handlers/dashboard_test.go` — `relTime`, `expiryDays`, `formatRecords` - All existing tests pass unchanged - `docker build .` passes closes [#82](#82) <!-- session: rework-pr-83 --> Co-authored-by: user <user@Mac.lan guest wan> Co-authored-by: clawbot <clawbot@noreply.git.eeqj.de> Reviewed-on: #83 Co-authored-by: clawbot <clawbot@noreply.example.org> Co-committed-by: clawbot <clawbot@noreply.example.org> |
||
|
|
1843d09eb3 |
test(notify): add comprehensive tests for notification delivery (#79)
check / check (push) Successful in 50s
## Summary Add comprehensive tests for the `internal/notify` package, improving coverage from 11.1% to 80.0%. Closes [issue #71](#71). ## What was added ### `delivery_test.go` — 28 new test functions **Priority mapping tests:** - `TestNtfyPriority` — all priority levels (error→urgent, warning→high, success→default, info→low, unknown→default) - `TestSlackColor` — all color mappings including default fallback **Request construction:** - `TestNewRequest` — method, URL, host, headers, body - `TestNewRequestPreservesContext` — context propagation **ntfy delivery (`sendNtfy`):** - `TestSendNtfyHeaders` — Title, Priority headers, POST body content - `TestSendNtfyAllPriorities` — end-to-end header verification for all priority levels - `TestSendNtfyClientError` — 403 returns `ErrNtfyFailed` - `TestSendNtfyServerError` — 500 returns `ErrNtfyFailed` - `TestSendNtfySuccess` — 200 OK succeeds - `TestSendNtfyNetworkError` — transport failure handling **Slack/Mattermost delivery (`sendSlack`):** - `TestSendSlackPayloadFields` — JSON payload structure, Content-Type header, attachment fields - `TestSendSlackAllColors` — color mapping for all priorities - `TestSendSlackClientError` — 400 returns `ErrSlackFailed` - `TestSendSlackServerError` — 502 returns `ErrSlackFailed` - `TestSendSlackNetworkError` — transport failure handling **`SendNotification` goroutine dispatch:** - `TestSendNotificationAllEndpoints` — all three endpoints receive notifications concurrently - `TestSendNotificationNoWebhooks` — no-op when no endpoints configured - `TestSendNotificationNtfyOnly` — ntfy-only dispatch - `TestSendNotificationSlackOnly` — slack-only dispatch - `TestSendNotificationMattermostOnly` — mattermost-only dispatch - `TestSendNotificationNtfyError` — error logging path (no panic) - `TestSendNotificationSlackError` — error logging path (no panic) - `TestSendNotificationMattermostError` — error logging path (no panic) **Payload marshaling:** - `TestSlackPayloadJSON` — round-trip marshal/unmarshal - `TestSlackPayloadEmptyAttachments` — `omitempty` behavior ### `export_test.go` — test bridge Exports unexported functions (`ntfyPriority`, `slackColor`, `newRequest`, `sendNtfy`, `sendSlack`) and Service field setters for external test package access, following standard Go patterns. ## Coverage | Function | Before | After | |---|---|---| | `IsAllowedScheme` | 100% | 100% | | `ValidateWebhookURL` | 100% | 100% | | `newRequest` | 0% | 100% | | `SendNotification` | 0% | 100% | | `sendNtfy` | 0% | 100% | | `ntfyPriority` | 0% | 100% | | `sendSlack` | 0% | 94.1% | | `slackColor` | 0% | 100% | | **Total** | **11.1%** | **80.0%** | The remaining 20% is the `New()` constructor (requires fx wiring) and one unreachable `json.Marshal` error path in `sendSlack`. ## Testing approach - `httptest.Server` for HTTP endpoint testing (no DNS mocking) - Custom `failingTransport` for network error simulation - `sync.Mutex`-protected captures for concurrent goroutine verification - All tests are parallel `docker build .` passes ✅ <!-- session: agent:sdlc-manager:subagent:6158e09a-aba4-4778-89ca-c12b22014ccd --> Co-authored-by: user <user@Mac.lan guest wan> Co-authored-by: Jeffrey Paul <sneak@noreply.example.org> Reviewed-on: #79 Co-authored-by: clawbot <clawbot@noreply.example.org> Co-committed-by: clawbot <clawbot@noreply.example.org> |