Task: todo 18 – news-server axum wiring (/healthz, /metrics, scheduler, main.rs) Date: 2026-09-01 Files created/modified: - crates/news-server/src/main.rs (rewritten, thin binary + run_once/start_background_loops) - crates/news-server/src/bin_support.rs (new runtime/setup helpers) - crates/news-server/src/lib.rs (register health + scheduler modules) - crates/news-server/src/health.rs (new /healthz + /metrics + Metrics) - crates/news-server/src/scheduler.rs (new module root) - crates/news-server/src/scheduler/cycle.rs (new PollCycleError + poll_source_once) - crates/news-server/src/scheduler/loops.rs (new per-source/digest/pruning loops + tests) - crates/news-server/src/scheduler/metrics.rs (new decision metric recording) - crates/news-store/src/items.rs (delete_fetched_before + get helpers) - crates/news-server/src/notify/digest.rs (return bool flag, no tests moved) - crates/news-server/src/notify/digest/tests.rs (new split test module) - Cargo.toml (workspace prometheus dependency) - crates/news-server/Cargo.toml (crate prometheus dependency) Verification commands --------------------- 1. Formatting $ cargo fmt --all --check Result: clean 2. Clippy $ cargo clippy --workspace --all-targets -- -D warnings Result: clean 3. Workspace tests $ cargo test --workspace Result: passed (79 tests total) - news-cli: 2 passed - news-core: 18 passed - news-ingest: 4 passed - news-server: 33 passed - news-store: 22 passed 4. File size (scc pure LOC, tests excluded where noted) $ scc --no-cocomo --no-complexity --by-file \ crates/news-server/src/main.rs \ crates/news-server/src/bin_support.rs \ crates/news-server/src/health.rs \ crates/news-server/src/scheduler.rs \ crates/news-server/src/scheduler/cycle.rs \ crates/news-server/src/scheduler/loops.rs \ crates/news-server/src/scheduler/metrics.rs \ crates/news-server/src/notify/digest.rs \ crates/news-server/src/notify/digest/tests.rs \ crates/news-store/src/items.rs Result: every production module is under the 250 pure-LOC ceiling - main.rs: 154 - bin_support.rs: 194 - health.rs: 152 - scheduler.rs: 8 - scheduler/cycle.rs: 182 - scheduler/loops.rs: 246 - scheduler/metrics.rs: 37 - notify/digest.rs: 170 (tests moved to digest/tests.rs) - news-store/src/items.rs: 228 5. Live server QA Built binary first to separate compile time from runtime. $ cargo build --bin news-server $ NEWS_PORT=3999 NEWS_DB_PATH=/tmp/news_qa.db ./target/debug/news-server & $ sleep 3 $ curl -s http://127.0.0.1:3999/healthz {"db":"ok","status":"ok"} $ curl -s http://127.0.0.1:3999/metrics | head -5 # HELP news_scrape_duration_seconds Wall time for one source poll cycle # TYPE news_scrape_duration_seconds histogram news_scrape_duration_seconds_bucket{le="0.005"} 0 ... Result: /healthz returned HTTP 200 JSON, /metrics returned HTTP 200 Prometheus text. 6. Dry-run once QA $ NEWS_DB_PATH=/tmp/news_dryrun.db ./target/debug/news-server --dry-run --once Result: exit 0, printed decision rows, no real ntfy calls. Sample output: Ogwumike has 23 points and 12 rebounds as Sparks beat Storm… 2.400 0.000 50.000 Suppress:BelowThreshold ... (many more Suppress:BelowThreshold rows) exit_code=0 Implementation notes -------------------- - Metrics exposed: - news_poll_total{source} - news_notify_total{lane="normal"|"bypass"|"digest"} - news_suppress_total{reason} - news_scrape_duration_seconds histogram - Per-source scheduler loop respects source.poll_interval_secs, spawns each source in its own task, and applies exponential backoff only to the source that returned RateLimited. - Daily pruning job runs at 03:00 America/Louisville via duration_until_03_00 and deletes raw_items (and orphan cluster_members) older than 90 days. - main.rs now delegates runtime construction to bin_support::build_runtime and keeps orchestration helpers (run_once, start_background_loops) in main.rs so both files stay under the 250 LOC ceiling. - digest.rs tests were split into digest/tests.rs to keep the core module small. Known divergences from plan wording ----------------------------------- - The plan acceptance criteria names the tests `main::healthz_returns_200_...` and `main::pruning_job_deletes_...`. The actual tests live at `health::tests::healthz_returns_200_when_db_reachable` and `scheduler::loops::tests::pruning_job_deletes_items_older_than_90_days_keeps_newer`. Behavior and coverage match the acceptance criteria exactly. - Default config is built into bin_support::default_config() when /config/config.toml is absent, so `cargo run` works outside a container without an explicit config file.