- Mark-read now immediately expands the READ shelf so the story is visible
- Orange signal stripe is fully removed (not dimmed) when isRead=true
- Pull-to-refresh clears seen snapshot → full reshuffle by signal score
- BackgroundRefreshManager fires a local notification for the top new
high-signal story after each cache refresh; Tech/AI/HomeLab stories
get their category in the notification title (≥65 signal threshold)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Registers com.kisani.jarvis.feed.refresh as a BGAppRefreshTask.
On each fire it fetches the latest 40 stories and upserts CachedStory
rows so the SwiftData cache is warm before the user opens the app.
Already-cached rows have signalScore / sourceCount refreshed in-place.
Single ModelContainer owned by JarvisApp and shared via
BackgroundRefreshManager.container to avoid multi-container conflicts.
BG task ID registered in AppDelegate and declared in project.yml.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Retention now mirrors the backend: cached stories age out at RECENT_HOURS (72h,
the active feed window), seen/read markers at 96h (window + buffer), and cached
article bodies at 14 days (raw-article retention). Saved bookmarks stay protected.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CacheMaintenance prunes the SwiftData caches so they don't grow forever:
seen markers >30d, read >90d, cached articles >30d, cached stories >14d —
always protecting saved bookmarks. Runs on launch. Settings gains a
"Clear offline cache" action (keeps saved) with a count + confirmation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the keyword/source-majority Interest system with a StoryPill enum that
matches stories by canonical category slugs. StorySummary gains a tolerant
tags[] (optional, decode-safe); effectiveTags uses tags, falling back to topic
during the backend transition. No more headline keyword-searching on the client —
the backend decides classification; the client asks slug membership.
Pills: All, F1, Sport, AI, Cloud, HomeLab, Tech, Uganda, South Africa, Canada, US.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Equal-score stories reshuffled on every pull-to-refresh because the sort
was unstable. Add StorySummary.feedOrder (signalScore desc, then id desc,
mirroring the backend's signal_score DESC, id DESC) and use it for the feed,
the WebSocket re-sort, and the Saved/Search tabs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Don't blank the feed on refresh: keep current stories until new data
arrives, and ignore benign URLError.cancelled (-999) so a network blip
no longer wipes the list. Coalesce WebSocket story.created bursts into a
single debounced refresh.
- Empty state now distinguishes polling, couldn't-reach-server (with the
error + host + Retry), offline, and genuinely-empty.
- Track read stories in SwiftData (ReadStory); mark a story read when opened
and drop/dim its signal stripe across the feed and Latest/Saved/Search.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
SwiftUI client for a self-hosted RSS news-correlation platform: signal feed,
story detail, article reader, feed manager, and LAN⇄Tailscale connectivity.
Project generated from project.yml via XcodeGen.
Includes CI build matrix (macOS 14/15 × Debug/Release), issue templates,
backlog, and API/backend handoff docs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>