- Gamma lift 0.70 pushes ring peaks toward full #FF5C00 saturation
- Faster ring-factor curve (^0.32) keeps more of each band at full brightness
- Tight 9px glow + wide 26px halo screen-blended for the orange fire feel
- Gap darkness stays near-black (^2 × 0.025) for maximum ring contrast
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the 3-bar horizontal design with the original radar/signal
icon — concentric orange rings fading to near-black at center and edge,
15 ring bands, warm dark background. Matches the "Signal Intelligence" J icon.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the custom 3-bar SwiftUI drawing with UIImage(named:"AppIcon")
clipped to a circle, matching the Wenza splash pattern exactly:
- Icon bounces in with spring (scale 0.82→1, opacity 0→1)
- "jarvis" wordmark fades in 320ms later
- Whole screen fades out at 1.6s, onDismiss fires at 1.9s
- Main app renders underneath so it's warm when the splash clears
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Every pill (F1, Tech, AI, Cloud, HomeLab…) now shows the same hero card
layout as "All" — first story is the big featured card, rest follow as rows
- "All" retains its full multi-section front page (US / Tech / AI / Sport…)
- Topic pill headers use the pill name instead of "TOP STORIES"
- filteredStories now falls back to cached SwiftData rows while loading, so
switching to F1 or Tech shows cached cards instantly instead of a blank list
- Logo splash screen (3 pill bars + jarvis wordmark) shows on launch and fades
out after setup completes; minimum 1.3 s so it's always readable
- JarvisWordmark moved to Theme.swift so SplashView and feed can share it
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- listen(): capture `task` as local `t` before the Sendable receive closure
so the @MainActor-isolated property isn't referenced inside a Sendable context
- startPing(): dispatch to @MainActor inside the timer block before accessing `task`
- scheduleReconnect(): capture `self` weakly in the reconnect Task
XcodeGen regenerated to include BackgroundRefreshManager.swift in the project.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>
Reading experience:
- Body text #CECECE (dark) / #1A1916 (light) — was near-invisible #4A4A4A
- 30pt heavy headline, 17pt body at 8.5pt line-spacing
- Skeleton pulse loading state replaces dark spinner box
- Hero image fades in on load (0.3s ease-in)
- Orange reading progress bar (2pt) below nav bar
- Max reading width 680pt for iPad legibility
Appearance:
- AppearanceMode enum (system/light/dark) in Theme.swift
- @AppStorage("appearanceMode") applied once in JarvisApp root
- Appearance picker (Picker in Menu) in reader toolbar
- Removed .preferredColorScheme(.dark) from all 12 child views
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Orange glow bleeds 15px upward beyond the bar boundary.
Y_START=(1024-370+15)/2=334 compensates so the visible
content sits with exactly 319px negative space above and below.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mathematical centre (512px) reads as low due to orange glow blooming
upward. Shifting the bar group up 22px balances perceived top/bottom
negative space (305px above, 349px below → visually equal).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Each pill is now positioned at x = (1024 - bar_width) / 2,
giving symmetric left/right margins regardless of bar length.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bars are now left-aligned with decreasing widths (600/440/300px),
matching the reference where the orange top bar is longest and each
gray bar steps down in width like a news feed preview.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Orange top bar + two gray bars on dark background. Cleaner and more
legible at small sizes (notification, Settings, Spotlight) than the
previous concentric-rings design.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Switches from .system(.heavy) to Courier New Bold — classic typewriter look
- 'j' is now lowercase (was 'J'), still in KisaniOrange
- 'i' renders in white with an orange Circle overlaid at the tittle position,
so the dot reads orange against the black background
- Implemented as HStack of individual Text segments so each character can
be independently coloured; 'i' uses .overlay(alignment: .top) with a
size-proportional Circle offset to sit at glyph-tittle height
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Both StoryDetailView and ArticleReaderView now show three icon buttons in
the navigation bar trailing area:
- Share (square.and.arrow.up) — story headline+summary+URL / article
headline+sourceUrl, passed to UIActivityViewController via ShareSheet.
- Save (bookmark / bookmark.fill, orange when active) — toggles SavedStory
in SwiftData; StoryDetailView also ensures a CachedStory exists so the
Saved tab can display it offline.
- Mark read/unread (checkmark.circle / checkmark.circle.fill, orange when
read) — toggle mirroring the swipe action already present in the feed.
The read state in StoryDetailView is still auto-marked on open (.task
→ markRead()); the toolbar button lets users undo that without going back
to the feed. ArticleReaderView targets the parent story (route.storyId).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Dark squircle with 16 concentric rings rising from near-black at the
outer edge and center to KisaniOrange (#FF5C00) at the midpoint, with a
Gaussian screen-blend glow pass. Mimics the sonar/radar ring style of
the reference logo using the Jarvis black-and-orange palette.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Render the #1 Top Story as an Apple-News-style hero card (flat surface, 4pt
stripe, 24pt headline, summary, sources, prominent score). Prettify category
labels (slug -> Title Case, with AI/US/Formula 1 special-cases) across rows,
hero, and detail. Per Emil's framework, no row entrance/press motion on a
constantly-scrolled surface — invest in hierarchy and defaults instead.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>