clientRank already existed specifically to stop stale high-scorers from leading the feed, but computed age from updatedAt — the same timestamp that keeps resetting whenever the backend attaches any article to a cluster (BACKLOG #15). A fossil's age always read as ~0, silently defeating the whole mechanism. Fixed to use firstSeenAt. clientRank's decay is soft (max 20% penalty) and isn't always enough against a high signalScore alone, so added StorySummary.isFossil (>5 days old by firstSeenAt) as a hard backstop: mainStories and makeDigest now sink fossils below current stories, so one can never take the hero/lead slot regardless of score. Still shown in the feed, just not in the most prominent position. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
6.9 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 mitigations shipped:- Age display (row meta line, hero card) now reads from
firstSeenAtinstead ofupdatedAt, so the UI no longer shows "1 hr ago" on stories that are actually weeks old. - Found that
StorySummary.clientRank— an existing time-decay mechanism, already in the codebase specifically to stop stale high-scorers from leading the feed — was itself silently neutralized by this same bug: it computed age fromupdatedAt, so a fossil's age always read as ~0. Fixed to usefirstSeenAt. This isn't new client-side ranking logic; it's un-breaking a pre-existing, intentional one. - Added
StorySummary.isFossil(firstSeenAt> 5 days old) as a hard backstop —clientRank's decay is soft (max 20% score penalty) and isn't always enough to overcome a highsignalScorealone. Fossils are sunk below current stories inmainStoriesandmakeDigest, so a stale story can never occupy the hero/lead slot even if the server still ranks it #1. They still appear in the feed, just not in the most prominent, most misleading position. Still wrong at the source — the server's ownsignalScore/freshnessScoreordering (what page 2+, pagination cutoffs, andmin_signalfiltering all rely on) is unaffected; these are display/ordering patches downstream of it. The real fix is still backend-side.
- Age display (row meta line, hero card) now reads from
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).