- East Africa pill: merges Uganda + east-africa tags; Uganda pinned first via sub-sections
- Southern Africa pill: covers SA, Namibia, Botswana, Zimbabwe, Zambia, Mozambique; SA/Namibia/Botswana prioritized
- StoryStore.setPill now resets lastSyncedAt to bypass 30s rate-limit on filter switch
- sectionSupplement background fetch populates sparse regional sections in All digest
- SignalFeedView digestContent: flat hero+scroll path for pills without subSections (was showing max 5)
- Theme: removed standalone Uganda pill, wired new sub-section layouts
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Offline caching:
- BackgroundRefreshManager.preCacheArticles() fetches top article per story
(score ≥60, up to 20 stories) and stores as CachedArticle in SwiftData
- Runs after every BG fetch AND after every foreground sync via StoryStore
- Articles are available in ArticleReaderView offline without ever opening them
Pull-to-refresh reshuffle when nothing new:
- loadStories(refresh:) now returns newCount (genuinely new story IDs)
- If server returns 0 new stories, feed merges in any BG-cached stories not
already in the live feed, then re-sorts by signal score (reshuffle)
- seenSnapshot is always cleared so everything re-ranks fresh
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
StoryStore gains startForegroundSync() / stopForegroundSync():
- Polls loadStories(refresh:true) every 5 minutes while app is active
- WebSocket handles real-time storyCreated events between polls
- Timer cancels when app goes to background to save battery
JarvisApp wires scenePhase:
- .active → start 5-min timer; immediate refresh if last sync > 2 min ago
- .background → cancel timer
- On launch → timer starts after setup completes
Net result: feed replenishes automatically; user never hits an empty list.
Co-Authored-By: Claude Sonnet 4.6 <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>