1ff8ca370d4b03e05c71bbdb5fc1203f601744c1
17
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9347a2838b |
build: update golangci-lint to v2.12.2 with org-standard v2 config (#96)
check / check (push) Successful in 4s
Updates golangci-lint to v2.12.2 and sets `.golangci.yml` to the org-standard v2-schema config already deployed across the org's repos. The config change is owner-authorized (see #96 (comment) and #96 (comment)); the same file is being landed as canonical via prompts PR #24 (sneak/prompts#24). ## Changes - **Commit-pinned installs**: golangci-lint pinned to commit `c0d3ddc9cf3faa61a4e378e879ece580256d76e5` (v2.12.2, released 2026-05-06) in `Dockerfile` and `script/bootstrap`. - **`.golangci.yml` set to the org-standard v2 config** (sha256 `021cc83f4e6fc7c31b95b34b846723dfcf20b66b7baeea1dc40406e643346bcb`), byte-identical to the file used across the org's other repos. Settings live under `linters.settings`, so the `lll`/`funlen`/`cyclop`/`dupl` thresholds are actually applied (under the old hybrid file, v2 silently ignored the top-level `linters-settings` block). - **Lint fixes** required by the now-active thresholds: - `goconst`: shared constants for repeated status/priority/DNS-fixture strings in `internal/watcher/watcher.go` and the notify, state, and watcher tests - `dupl`: consolidated duplicated ntfy/slack HTTP-error tests and SendNotification endpoint-error tests behind shared helpers in `internal/notify/delivery_test.go` - `lll`: wrapped long test table entries and comments in `internal/config/classify_test.go`, `internal/notify/history_test.go`, `internal/state/state_test.go`, `internal/watcher/watcher_test.go`; shortened one inline nolint justification in `internal/notify/retry.go` - **`TODO.md`**: Completed Steps entry updated in the same commit. - Rebased onto current `main` (`f79cd98`); the branch is one clean commit. ## Notes - v2.12 deprecates the `gomodguard` linter in favor of `gomodguard_v2`. The org-standard config does not disable the deprecated linter, so golangci-lint may emit an informational deprecation warning; this is accepted by the owner and does not affect the exit status (this exact config+code combination was CI-green at `dea7e44`). ## Verification - `make check` exits 0 (fmt-check, tests, lint) - `make lint`: 0 issues; no deprecation warning surfaced in the runs performed - sha256 of `.golangci.yml` at HEAD verified equal to `021cc83f4e6fc7c31b95b34b846723dfcf20b66b7baeea1dc40406e643346bcb` Co-authored-by: sneak <sneak@sneak.berlin> Reviewed-on: #96 Co-authored-by: clawbot <clawbot@noreply.example.org> Co-committed-by: clawbot <clawbot@noreply.example.org> |
||
|
|
f79cd98107 |
docs: document the no-DNS-mocking policy in README (closes #94) (#95)
check / check (push) Successful in 5s
Adds a prominent "No DNS mocking. Ever." section near the top of `README.md`, per owner policy (sneak, 2026-08-07): - DNS is never mocked in this project — no mock resolvers, fake DNS servers, or stubbed lookups, in tests or anywhere else. - Tests exercise real iterative resolution against live nameservers by design. - Flaky live tests are fixed with robustness (retries, multiple nameservers, timeouts) or explicit opt-in gating decided by the owner — never with mocks. - Contributions introducing DNS mocks will be rejected. Markdown-only change; matches the README's existing tone and hard-wrap style. `script/fmt` covers Go only, so no formatter output applies to this file. Verified via `script/cibuild` (docker build runs `make check` with the pinned toolchain) — green. A direct local `make check` shows 21 pre-existing `goconst` lint findings that come from a newer local `golangci-lint` (v2.12.2 vs the pinned v2.10.1) and are unrelated to this change. Related: #93 is being reframed under this policy. Co-authored-by: sneak <sneak@sneak.berlin> Reviewed-on: #95 Co-authored-by: clawbot <clawbot@noreply.example.org> Co-committed-by: clawbot <clawbot@noreply.example.org> |
||
|
|
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> |
||
|
|
f788037bfb |
config: use /var/lib/dnswatcher as default data directory (#89)
check / check (push) Successful in 34s
Closes [issue #88](#88). Changes the default `DNSWATCHER_DATA_DIR` from the relative path `./data` to the absolute path `/var/lib/dnswatcher`, following the [Filesystem Hierarchy Standard](https://refspecs.linuxfoundation.org/FHS_3.0/fhs/ch05s08.html) convention for variable application state data. ## Changes - **`internal/config/config.go`**: Changed the Viper default for `DATA_DIR` from `"./data"` to `"/var/lib/"+name`, where `name` is the application name ("dnswatcher"). This makes the default derived from the app name rather than hardcoded. - **`internal/config/config_test.go`**: Updated `TestNew_DefaultValues` and `TestStatePath` to expect the new absolute default. - **`README.md`**: Updated the environment variable table and `.env` example to show `/var/lib/dnswatcher` as the default. The Dockerfile already set `ENV DNSWATCHER_DATA_DIR=/var/lib/dnswatcher` explicitly, so Docker deployments are unaffected. This change makes the code default consistent with the Docker configuration. `docker build .` passes all checks (fmt, lint, tests, build). Co-authored-by: user <user@Mac.lan guest wan> Reviewed-on: #89 Co-authored-by: clawbot <clawbot@noreply.example.org> Co-committed-by: clawbot <clawbot@noreply.example.org> |
||
|
|
b64db3e10f |
feat: enhance /api/v1/status endpoint with full monitoring data (#86)
check / check (push) Successful in 1m27s
## Summary
Enhances the `/api/v1/status` endpoint to return comprehensive monitoring state instead of just `{"status": "ok"}`.
## Changes
The endpoint now returns:
- **Summary counts**: domains, hostnames, ports (total + open), certificates (total + ok + error)
- **Domains**: each monitored domain with its discovered nameservers and last check timestamp
- **Hostnames**: each monitored hostname with per-nameserver DNS records, status, and last check timestamps
- **Ports**: each monitored IP:port with open/closed state, associated hostnames, and last check timestamp
- **Certificates**: each TLS certificate with CN, issuer, expiry, SANs, status, and last check timestamp
- **Last updated**: timestamp of the overall monitoring state
All data is derived from the existing `state.GetSnapshot()`, consistent with how the dashboard works. No configuration details (webhook URLs, API tokens) are exposed.
## Example response structure
```json
{
"status": "ok",
"lastUpdated": "2026-03-10T12:00:00Z",
"counts": {
"domains": 2,
"hostnames": 3,
"ports": 10,
"portsOpen": 8,
"certificates": 4,
"certificatesOk": 3,
"certificatesError": 1
},
"domains": { ... },
"hostnames": { ... },
"ports": { ... },
"certificates": { ... }
}
```
closes #73
Co-authored-by: clawbot <clawbot@noreply.git.eeqj.de>
Reviewed-on: #86
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
|
||
|
|
65180ad661 |
feat: add DNSWATCHER_SEND_TEST_NOTIFICATION env var (#85)
check / check (push) Successful in 5s
When set to a truthy value, sends a startup status notification to all configured notification channels after the first full scan completes on application startup. The notification is clearly an all-ok/success message showing the number of monitored domains, hostnames, ports, and certificates. Changes: - Added `SendTestNotification` config field reading `DNSWATCHER_SEND_TEST_NOTIFICATION` - Added `maybeSendTestNotification()` in watcher, called after initial `RunOnce` in `Run` - Added 3 watcher tests (enabled via Run, enabled via RunOnce alone, disabled) - Added config tests for the new field - Updated README: env var table, example .env, Docker example Closes #84 Co-authored-by: user <user@Mac.lan guest wan> Reviewed-on: #85 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> |
||
|
|
c5bf16055e |
test(state): add comprehensive test coverage for internal/state package (#80)
check / check (push) Has been cancelled
## Summary Add 32 tests for the `internal/state` package, which previously had 0% test coverage. ### Tests added: **Save/Load round-trip:** - Domain, hostname, port, and certificate data all survive save→load cycles - Error fields (omitempty) round-trip correctly - Backward-compatible PortState deserialization (old single-hostname → new multi-hostname format) **Edge cases:** - Missing state file: returns nil error, keeps existing in-memory state - Corrupt state file: returns parse error - Empty state file: returns parse error - Permission errors (read/write): properly reported, skipped when running as root in Docker **Atomic write:** - No leftover .tmp files after successful save - Updated content verified after second save **Getter/setter coverage:** - Domain: get, set, overwrite - Hostname: get, set with nested nameserver records - Port: get, set, delete - Certificate: get, set - GetAllPortKeys enumeration - GetSnapshot returns value copy **Concurrency:** - 20 goroutines × 50 iterations of concurrent get/set/delete with race detector - 10 goroutines doing concurrent Save/Load **Other:** - Snapshot version written correctly - LastUpdated timestamp set on save - File permissions are 0600 - Multiple saves overwrite previous state completely - NewForTest helper creates valid empty state - Save creates nested data directories Also adds `NewForTestWithDataDir()` to the test helper for tests requiring file persistence. Closes [issue #70](#70) <!-- session: agent:sdlc-manager:subagent:e75f60a3-17c4-43f7-a743-32a108ee5081 --> Co-authored-by: clawbot <clawbot@noreply.git.eeqj.de> Reviewed-on: #80 Co-authored-by: clawbot <clawbot@noreply.example.org> Co-committed-by: clawbot <clawbot@noreply.example.org> |
||
|
|
d6130e5892 |
test(config): add comprehensive tests for config loading path (#81)
check / check (push) Successful in 4s
## Summary Add comprehensive tests for the `internal/config` package, covering the main configuration loading path that was previously untested. Closes [issue #72](#72) ## What Changed Added three new test files: - **`config_test.go`** — 16 tests covering `New()`, `StatePath()`, and the full config loading pipeline - **`parsecsv_test.go`** — 10 test cases for `parseCSV()` edge cases - **`export_test.go`** — standard Go export bridge for testing unexported `parseCSV` ## Test Coverage | Area | Tests | |------|-------| | Default values | All 14 config fields verified against documented defaults | | Environment overrides | All env vars tested including `PORT` (unprefixed) | | Invalid duration fallback | `DNSWATCHER_DNS_INTERVAL=banana` falls back to 1h | | Invalid TLS interval | `DNSWATCHER_TLS_INTERVAL=notaduration` falls back to 12h | | No targets error | Empty/missing `DNSWATCHER_TARGETS` returns `ErrNoTargets` | | Invalid targets | Public suffix (`co.uk`) rejected with error | | CSV parsing | Trailing commas, leading commas, consecutive commas, whitespace, tabs | | Debug mode | `DNSWATCHER_DEBUG=true` enables debug logging | | Target classification | Domains vs hostnames correctly separated via PSL | | StatePath | Path construction with various `DataDir` values | | Empty appname | Falls back to "dnswatcher" config file name | **Coverage: 23% → 92.5%** ## Notes - Tests use `viper.Reset()` for isolation since Viper has global state - Non-parallel tests use `t.Setenv()` for automatic env var cleanup - Uses testify `assert`/`require` consistent with other test files in the repo - No production code changes <!-- session: agent:sdlc-manager:subagent:d7fe6cf2-4746-4793-a738-9df8f5f5f0c6 --> Co-authored-by: user <user@Mac.lan guest wan> Reviewed-on: #81 Co-authored-by: clawbot <clawbot@noreply.example.org> Co-committed-by: clawbot <clawbot@noreply.example.org> |
||
|
|
0a74971ade |
docs: fix README inaccuracies found during QA audit (#74)
check / check (push) Successful in 9s
## Summary Fixes documentation inaccuracies in README.md identified during QA audit. ### Changes **API table (closes #67):** - Removed `GET /api/v1/domains` and `GET /api/v1/hostnames` from the HTTP API table. These endpoints are not implemented — the only routes in `internal/server/routes.go` are `/health`, `/api/v1/status`, and `/metrics` (conditional). **Feature claims (closes #68):** - Removed "Inconsistency resolved" from hostname monitoring features. `detectInconsistencies()` detects current inconsistencies but has no state tracking to detect when they resolve. - Removed `nxdomain` and `nodata` from the state status values table. While the resolver defines these constants, `buildHostnameState()` in the watcher only ever sets status to `"ok"`. Failed queries set `"error"` via the NS disappearance path. These values are never written to state. - Removed "Empty response" (NODATA/NXDOMAIN) detection claim. Changes are caught generically by `detectRecordChanges()`, not with specific NODATA/NXDOMAIN labeling. ### What was NOT changed - "Inconsistency detected" remains — this IS implemented in `detectInconsistencies()`. - All other feature claims were verified against the code and are accurate. - No Go source code was modified. Co-authored-by: clawbot <clawbot@noreply.git.eeqj.de> Co-authored-by: Jeffrey Paul <sneak@noreply.example.org> Reviewed-on: #74 Co-authored-by: clawbot <clawbot@noreply.example.org> Co-committed-by: clawbot <clawbot@noreply.example.org> |
||
|
|
e882e7d237 |
feat: fail fast when no monitoring targets configured (#75)
check / check (push) Failing after 46s
## Summary When `DNSWATCHER_TARGETS` is empty (the default), dnswatcher previously started successfully and ran indefinitely monitoring nothing. This is a common misconfiguration — forgetting to set the variable or making a typo in its name — and gave no indication anything was wrong. ## Changes - Added `ErrNoTargets` sentinel error in `internal/config/config.go` - Extracted `parseAndValidateTargets()` helper to validate that at least one domain or hostname is configured after target classification - If no targets are configured, dnswatcher now exits with a clear error: `"no monitoring targets configured: set DNSWATCHER_TARGETS environment variable"` - Updated README.md to document that `DNSWATCHER_TARGETS` is required and dnswatcher will refuse to start without it ## How it works The validation runs during config construction (via uber/fx), before the watcher or any other component starts. If `DNSWATCHER_TARGETS` is empty or contains only whitespace/empty entries, `buildConfig()` returns `ErrNoTargets`, which causes fx to fail startup with a clear error message. This is fail-fast behavior: a monitoring daemon with nothing to monitor is a misconfiguration and should not silently run. Closes #69 Co-authored-by: clawbot <clawbot@noreply.git.eeqj.de> Reviewed-on: #75 Co-authored-by: clawbot <clawbot@noreply.example.org> Co-committed-by: clawbot <clawbot@noreply.example.org> |
||
|
|
6ebc4ffa04 |
fix: use context.Background() for watcher goroutine lifetime (#63)
check / check (push) Successful in 31s
## Summary The `OnStart` hook previously derived the watcher's context from the fx startup context (`startCtx`) via `context.WithoutCancel()`. While `WithoutCancel` strips cancellation and deadline, using `context.Background()` makes the intent explicit: the watcher's monitoring loop must outlive the fx startup phase and is controlled solely by the `cancel` func called in `OnStop`. ## Changes - Replace `context.WithCancel(context.WithoutCancel(startCtx))` with `context.WithCancel(context.Background())` - Add explanatory comment documenting why the watcher context is not derived from the startup context - Unused `startCtx` parameter changed to `_` Closes #53 Co-authored-by: clawbot <clawbot@noreply.git.eeqj.de> Co-authored-by: Jeffrey Paul <sneak@noreply.example.org> Reviewed-on: #63 Co-authored-by: clawbot <clawbot@noreply.example.org> Co-committed-by: clawbot <clawbot@noreply.example.org> |
||
|
|
b20e75459f |
fix: track multiple hostnames per IP:port in port state (#65)
check / check (push) Successful in 34s
## Summary Port state keys are `ip:port` with a single `hostname` field. When multiple hostnames resolve to the same IP (shared hosting, CDN), only one hostname was associated. This caused orphaned port state when that hostname removed the IP from DNS while the IP remained valid for other hostnames. ## Changes ### State (`internal/state/state.go`) - `PortState.Hostname` (string) → `PortState.Hostnames` ([]string) - Custom `UnmarshalJSON` for backward compatibility: reads old single `hostname` field and migrates to a single-element `hostnames` slice - Added `DeletePortState` and `GetAllPortKeys` methods for cleanup ### Watcher (`internal/watcher/watcher.go`) - Refactored `checkAllPorts` into three phases: 1. Build IP:port → hostname associations from current DNS data 2. Check each unique IP:port once with all associated hostnames 3. Clean up stale port state entries with no hostname references - Port change notifications now list all associated hostnames (`Hosts:` instead of `Host:`) - Added `buildPortAssociations`, `parsePortKey`, and `cleanupStalePorts` helper functions ### README - Updated state file format example: `hostname` → `hostnames` (array) - Updated notification description to reflect multiple hostnames ## Backward Compatibility Existing state files with the old single `hostname` string are handled gracefully via custom JSON unmarshaling — they are read as single-element `hostnames` slices. Closes #55 Co-authored-by: clawbot <clawbot@noreply.eeqj.de> Reviewed-on: #65 Co-authored-by: clawbot <clawbot@noreply.example.org> Co-committed-by: clawbot <clawbot@noreply.example.org> |
||
|
|
ee14bd01ae |
fix: enforce DNS-first ordering for port and TLS checks (#64)
check / check (push) Successful in 8s
## Summary DNS checks now always complete before port or TLS checks begin, ensuring those checks use freshly resolved IP addresses instead of potentially stale ones from a previous cycle. ## Problem Port and TLS checks read IP addresses from state that was populated during the most recent DNS check. If DNS changes between cycles, port/TLS checks may target stale IPs. In particular, when the TLS ticker fired (every 12h), it ran `runTLSChecks` without refreshing DNS first — meaning TLS checks could use IPs that were up to 12 hours old. ## Changes - **Extract `runDNSChecks()`** from the former `runDNSAndPortChecks()` so DNS resolution can be invoked independently as a prerequisite for any check type. - **TLS ticker now runs DNS first**: When the TLS ticker fires, DNS checks run before TLS checks, ensuring fresh IPs. - **`RunOnce` uses explicit 3-phase ordering**: DNS → ports → TLS. Port checks must complete before TLS because TLS checks only target IPs where port 443 is open. - **New test `TestDNSRunsBeforePortAndTLSChecks`**: Verifies that when DNS IPs change between cycles, port and TLS checks pick up the new IPs. - **README updated**: Monitoring lifecycle section now documents the DNS-first ordering guarantee. ## Check ordering | Trigger | Phase 1 | Phase 2 | Phase 3 | |---------|---------|---------|----------| | Startup (`RunOnce`) | DNS | Ports | TLS | | DNS ticker | DNS | Ports | — | | TLS ticker | DNS | — | TLS | closes #58 Co-authored-by: user <user@Mac.lan guest wan> Reviewed-on: #64 Co-authored-by: clawbot <clawbot@noreply.example.org> Co-committed-by: clawbot <clawbot@noreply.example.org> |
||
|
|
2835c2dc43 |
REPO_POLICIES compliance audit (#40)
check / check (push) Successful in 8s
Brings the repository into compliance with REPO_POLICIES standards. Closes [issue #39](#39). ## Changes ### Added files - **REPO_POLICIES.md** — fetched from `sneak/prompts` (last_modified: 2026-02-22) - **.editorconfig** — fetched from `sneak/prompts`, enforces 4-space indents (tabs for Makefile) - **.dockerignore** — standard Go exclusions (.git/, bin/, *.md, LICENSE, .editorconfig, .gitignore) ### Makefile updates - Added `fmt-check` target (read-only gofmt check) - Added `hooks` target (installs pre-commit hook running `make check`) - Added `docker` target (runs `docker build .`) - Added `-timeout 30s` to both `test` and `check` targets - Updated `.PHONY` list with all new targets ### Removed files - **CLAUDE.md** — superseded by REPO_POLICIES.md - **CONVENTIONS.md** — superseded by REPO_POLICIES.md ### README updates - First line now includes project name, purpose, category (daemon), and author per REPO_POLICIES format - Updated CONVENTIONS.md reference to REPO_POLICIES.md - Added **License** section (pending author choice) - Added **Author** section: [@sneak](https://sneak.berlin) ### Intentionally skipped - **LICENSE file** — not created; license choice (MIT, GPL, or WTFPL) requires sneak's input ## Verification - `docker build .` passes (all checks green: fmt, lint, tests, build) - No changes to `.golangci.yml`, test assertions, or linter config Co-authored-by: clawbot <clawbot@noreply.git.eeqj.de> Co-authored-by: Jeffrey Paul <sneak@noreply.example.org> Reviewed-on: #40 Co-authored-by: clawbot <clawbot@noreply.example.org> Co-committed-by: clawbot <clawbot@noreply.example.org> |
||
|
|
299a36660f |
fix: 700ms query timeout, proper iterative resolution (closes #24) (#28)
check / check (push) Successful in 34s
Root cause: `resolveARecord` and `resolveNSRecursive` sent recursive queries (RD=1) to root servers, which don't answer them. This caused 5s timeouts × 2 retries × 3 servers = hanging tests. Fix: - Changed `queryTimeoutDuration` from 5s to 700ms - Rewrote `resolveARecord` to do proper iterative resolution through the delegation chain (query roots → follow NS delegations → get A record) - Renamed `resolveNSRecursive` → `resolveNSIterative` with same iterative approach - No mocking, no test skipping, no config changes `make check` passes: all 29 resolver tests pass with real DNS in ~10s. Co-authored-by: clawbot <clawbot@git.eeqj.de> Reviewed-on: #28 Co-authored-by: clawbot <clawbot@noreply.example.org> Co-committed-by: clawbot <clawbot@noreply.example.org> |