Evidence — Todo 15: NtfyPublisher + ntfy access snippet ======================================================= Files created: - crates/news-server/src/notify/mod.rs - crates/news-server/src/notify/ntfy.rs - deploy/ntfy-access-snippet.md Files modified: - crates/news-server/src/lib.rs (added `pub mod notify;`) - crates/news-server/Cargo.toml (reqwest, url, tokio time feature, wiremock dev-dep) - Cargo.toml (workspace `url = "2"`) Verification commands --------------------- 1) cargo fmt --all --check Output: (no output) → exit 0 2) cargo clippy --workspace --all-targets -- -D warnings Output tail: Checking news-server v0.1.0 (...) Finished `dev` profile [unoptimized + debuginfo] target(s) in 5.34s → exit 0 3) cargo test -p news-server Output tail: running 20 tests test notify::ntfy::tests::normal_lane_sends_priority_three ... ok test notify::ntfy::tests::bypass_lane_sends_priority_five ... ok test notify::ntfy::tests::request_has_required_headers ... ok test notify::ntfy::tests::retries_once_on_server_error_then_err ... ok ... test result: ok. 20 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out Ntfy tests added: - notify::ntfy::tests::request_has_required_headers - notify::ntfy::tests::bypass_lane_sends_priority_five - notify::ntfy::tests::normal_lane_sends_priority_three - notify::ntfy::tests::retries_once_on_server_error_then_err Key implementation notes ------------------------ - Header shape mirrors movie_recs_notify: `Title`, `Tags`, `Priority`, `Markdown: yes`, `Content-Type: text/plain; charset=utf-8`, plus ntfy-specific `Click` and `Actions`. - Actions format follows ntfy docs short form with `; ` separators: `view, Open article, ; http, Interested, /api/feedback, method=POST, body='{"story_cluster_id":"...","kind":"interested"}'; http, Not interested, ...` - JSON bodies are wrapped in single quotes so the comma/colon-rich payload does not break ntfy's simple action parser. - Auth: `NTFY_TOKEN` → `Authorization: Bearer ...`; `NTFY_USER`+`NTFY_PASS` → reqwest `.basic_auth(...)` (Basic base64); otherwise none. - Retry: transport errors and 5xx sleep 500 ms and retry exactly once; 4xx returns `Err` immediately. No unwrap/expect/panic. - `feedback_base_url` is read from `NEWS_API_BASE_URL` with default `http://localhost:3000`. --- deploy/ntfy-access-snippet.md full content --- # ntfy access provisioning for `news-triage` The ntfy server is configured with a deny-by-default posture: ```yaml # /home/connor/data/ntfy/etc/server.yml auth-default-access: "deny-all" ``` Because of that posture, the `news-bot` user must be created and granted read/write access to the `news-triage` topic before `news-server` can publish notifications. These are one-time operator actions. Run from the host that owns the `ntfy` container: ```bash # Create the publishing user (interactive password prompt). sudo podman exec -it ntfy ntfy user add news-bot # Grant read/write access to the news-triage topic. sudo podman exec ntfy ntfy access news-bot news-triage rw ``` Store the resulting password in the deployment secret path (`~/.env` for the `news-backend` compose block) as `NTFY_USER` and `NTFY_PASS`.