Instead of one long firehose, "All" now renders Apple/Google-News-style:
a Top Stories lead block, then category sections (World, Politics, Business,
AI, Technology, Africa, Sport, Science, Entertainment, Health, Lifestyle) with
the top few stories each and a "See all" jump to the matching pill. Sections are
built from backend tags[]; specific pills keep the flat list. "All" preloads up
to ~100 stories to fill sections.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The "All" feed stuttered because every row appearing ran markSeen, which did a
disk save(), an O(n) contains over a @Query'd SeenStory array, and triggered a
full feed re-render on each insert. Now markSeen does an O(1) insert into a
non-observed reference Set, no per-row save (the container autosaves), and
SeenStory is no longer @Query'd by the view. cacheStories also does one fetch +
an in-memory index instead of a query per story.
Co-Authored-By: Claude Opus 4.8 <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>
Add tags[] to CachedStory (defaulted for lightweight migration) and persist
them on upsert; offline StorySummary now carries the cached tags instead of
falling back to topic. Backend stays the source of truth.
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>
- New Sport pill: detect sports by clear headline terms or majority
sports-outlet sources, and exclude sports (and F1) from All/Tech/regions so
World Cup/soccer no longer pollutes Finance/Tech.
- New client keyword pills: Canada, US, AI, Cloud, HomeLab (headline-matched).
- Remove the Finance pill; keep Tech (backend topic).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Left swipe: short reveals Save (tap), full swipe marks read. Right swipe
still shares.
- Read stories leave the main feed and collapse into a "Read" shelf at the
bottom (collapsed by default), shown as slim one-line rows; swipe to mark
unread. An "all caught up" state shows when everything loaded is read.
- StoryRowView gains a compact presentation for the shelf.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Sink already-seen and read stories to the bottom of the feed (new SeenStory
model + per-load snapshot so order is stable while scrolling and refreshes
sink what you've seen) so fresh content surfaces.
- F1 matching: clear F1 headline OR a majority of F1-feed sources, replacing the
broad keyword list that leaked crypto in (fia -> "deFIAnt"). Dropped unused
matchSources path.
- Remove the East Africa pill.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Swipe left bookmarks a story (toggle) into the Saved tab; swipe right opens
the iOS share sheet and marks the story read.
- New SavedStory SwiftData model; Saved tab now lists explicit bookmarks
instead of the whole offline cache.
- Convert the feed to a styled List (native swipeActions require it), keeping
the flat look, pull-to-refresh, and infinite scroll. Add a ShareSheet wrapper.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Show a 6-line cross-source summary preview in each feed row.
- Pinned, mutually-exclusive filter pills: All, F1, Uganda, East Africa,
South Africa, Finance, Tech. "All" is news-only (F1 lives only under F1);
regions don't overlap (East Africa excludes Uganda & South Africa).
- Categorization correctness: regions match the headline only (the backend's
summaries/outlets are polluted by over-clustering), and F1 takes precedence
over every other pill since the backend mis-tags F1 as tech/finance/politics.
- Finance/Tech match the backend topic; sparse filters auto-pull more pages.
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>
- Settings hub (gear → SettingsView) hosting Notifications and Connectivity
(Connectivity previously had no entry point).
- NotificationsView: permission flow, breaking/new-story alerts with a
min-signal threshold, morning & evening briefings with time pickers, and a
weather toggle + location, plus a "send test briefing" action.
- NotificationManager: schedules daily briefings, builds "Jarvis" content
(weather + top signal stories), fires breaking alerts off WebSocket
story.created events, and refreshes via a BGAppRefresh task.
- WeatherService: Open-Meteo geocoding + daily forecast (no API key/entitlement).
- AppDelegate registers the background-refresh handler at launch; Info.plist
gains UIBackgroundModes + BGTaskSchedulerPermittedIdentifiers.
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>