Old stories were reading "1 hr ago" because updatedAt bumps every time the backend attaches another article to a cluster, however loosely related. firstSeenAt is when the cluster actually first appeared and is already in the API response — just wasn't decoded. Verified live: story_1a37c858235b (F1 Austria GP recap) has firstSeenAt 2026-06-16 but updatedAt 2026-07-11, freshnessScore 0.96 — looked brand new despite being 25 days old. Also confirmed the API's isStale field is unusable as a stopgap (always false regardless of age). Ranking still surfaces old stories at the top — that's server-side (signalScore/freshnessScore) and out of scope for the client per the architecture rule against re-ranking. Logged in BACKLOG #15. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
6.0 KiB
Backlog & status
Source-of-truth tracker. The Gitea issue tracker is currently empty (0 issues) —
run scripts/create-issues.sh with a token to populate it. [client] = iOS,
[backend] = the platform service on the Docker host, [ci] = build/test.
Status: 🔴 open · 🟡 mitigated client-side (real fix still pending) · 🟢 done.
Backend — data quality (highest priority)
- 🟡 [backend] F1 mis-classified into tech/finance/politics. No sport category, so
F1 (100% F1 sources) scatters across topics: "Monaco Grand Prix…" →
tech, "F1 reports 35% CO2…" →finance, "McLaren launch appeal…" →politics. Add asport/f1topic and route motorsport sources there. Client mitigation shipped: F1 pill matches clear F1 headlines + a majority of F1 sources, and F1 is excluded from All/Tech/Finance. Backend still mis-tags. - 🔴 [backend] Catastrophic over-clustering — unrelated articles merged on a single shared word. Newly confirmed on-device: a story merged a Russian-film-director article + "Who's afraid of Michael Jackson?" + "Chelsea Jackson Roberts" (wellness) — grouped on the surname Jackson. Another merged "Gunfire at Niger airport" + "L.A. County Whiteman Airport crash" + "Niger fintech". Result: wrong headline, summary, topic, and inflated signal. Raise the TF-IDF threshold hard (~0.22 → 0.45–0.55) and require multiple shared distinctive terms / named-entity overlap, with a tight time window. If still over-merging, move to sentence embeddings (all-MiniLM-L6-v2, cosine ≥ ~0.6). Make canonical headline + summary come from the same representative article. No client fix is possible — the story object itself is wrong.
- 🔴 [backend] Add accurate per-story tags (topic + region). So the client can read tags (sport, uganda, south-africa, …) instead of keyword-guessing. Unblocks #6.
- 🔴 [backend] Topic-classifier coverage for the 47 feeds. science/world/tech mapping.
- 🔴 [backend] Freshness score decays from
lastUpdatedAt(cluster last touched), not from the actual news event age — old stories never leave the top of the feed. Confirmed live on/api/v1/stories: 13 of the top 20 stories are 5+ days old byfirstSeenAtbut still score 90+; the #1 story (story_511c67eb064d) hasfirstSeenAt: 2026-06-17,lastUpdatedAt: 2026-07-09(21 days later),freshnessScore: 1.0, signal 100. Compounds with #2 (over-clustering): a loose cluster keeps absorbing tangential articles indefinitely (this story's timeline includes "Security forces ready for June 30 marches: Ramaphosa" — unrelated to the football story it's attached to), each attachment resetslastUpdatedAtto now, so the cluster's clock never runs out and its canonical headline drifts to whatever was clustered last (list view showed "South Africa stun South Korea…"; story detail showed "Updated FIFA rankings after Bafana Bafana exit" for the samestory_id). Fix: decay freshness fromfirstSeenAt(or the newest article whose relevance to the cluster core is verified), not from "last touched." Tightening the TF-IDF threshold in #2 should also reduce how often unrelated articles keep a stale cluster's clock alive. Also confirmed the API's ownisStalefield is unusable as a stopgap — it readsfalseon every story checked regardless of actual age, so it isn't computed from a real staleness threshold. Client mitigation shipped: age display (row meta line, hero card) now reads fromfirstSeenAtinstead ofupdatedAt, so the UI no longer shows "1 hr ago" on stories that are actually weeks old. Ranking is still wrong — old stories still surface at the top of the feed, becausesignalScore/freshnessScore(used for ordering) are computed server-side and the client must not re-rank (architecture rule). The real fix is still backend-side.
Backend — features
- 🔴 [backend] Server-side push for breaking alerts (APNs or ntfy). For breaking-story notifications while the app is closed. Client does local notifications + background refresh only.
Client
- 🔴 [client] Move interest filters server-side once tags (#3) exist. Today's pills are client-side keyword/headline matches on loaded stories. Refined since: regions match headline-only (no polluted summaries/outlets), regions exclusive, F1 by source majority, East Africa removed.
- 🔴 [client] SSID-based trusted networks. Needs Access WiFi Information entitlement (device + paid account). Approximated by LAN reachability today.
- 🔴 [client] Optional in-app WireGuard tunnel. NetworkExtension; entitlement + device. Deferred.
- 🔴 [client] App icon + launch assets.
AppIconis an empty placeholder. - 🔴 [client] Flesh out Latest / Search tabs. Saved is now bookmarks; Latest/Search still minimal.
- 🔴 [client] Verify hero image loading.
AsyncImagevs real feedimageUrls. - 🔴 [client] Offline-first polish. Pre-cache top stories.
CI
- 🔴 [ci] Add unit/UI tests + a test matrix leg. Build matrix compiles, runs no tests.
- 🔴 [ci] Backend CI build matrix. Python 3.11–3.13 + lint + Docker build/health probe.
🟢 Shipped
Signal feed + detail + reader + feed manager · LAN⇄Tailscale connectivity · notifications
(morning/evening briefings + breaking alerts) with Open-Meteo weather · device code signing
· feed resilience (no blank-on-refresh, ignore cancelled, coalesced WS refresh) · clearer
empty states (polling / unreachable+host / offline / caught-up) · deterministic feed order ·
6-line summary preview · pinned filters (F1 split out, regions exclusive & headline-matched,
Finance/Tech by topic) · swipe actions (short→Save / long→Mark read, right→Share) ·
read/seen handling (seen-tracking with per-load snapshot, read stories collapse into a
"Read" shelf with compact rows, "all caught up" state).