Initial commit: Jarvis iOS app

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>
This commit is contained in:
Robin Kutesa
2026-06-18 18:04:59 +03:00
commit 27d0e22767
45 changed files with 4913 additions and 0 deletions

147
docs/KICKOFF_PROMPT.md Normal file
View File

@@ -0,0 +1,147 @@
# Jarvis iOS — Claude Code Kickoff Prompt
Paste this entire prompt into Claude Code to begin.
---
## Prompt
You are building **Jarvis** — a SwiftUI iOS app that connects to a self-hosted RSS news correlation platform and displays stories ranked by signal score.
Read `CLAUDE.md` before writing a single line of code. It contains every architecture decision already made. Do not deviate from it.
---
## Your job
Build the remaining SwiftUI views. The networking layer, models, stores, and onboarding screen are already written. You are building the UI on top of them.
### Views to build
1. **SignalFeedView** — the home screen
- Wordmark `Jarvis` with `J` in #FF5C00, rest white, weight 800, kerning -2
- Live/Offline connection indicator (driven by `WebSocketManager.shared.connectionState`)
- Sync bar: "Synced X min ago · N cached" when live, "Last synced X min ago" when offline
- Topic filter pills: All · Finance · Tech · Politics · Africa (calls `store.setTopic()`)
- Column headers: Story / Signal ↓
- List of `StoryRowView` sorted by `signalScore` descending
- Pull to refresh calls `store.loadStories(refresh: true)`
- Infinite scroll: call `store.loadMore()` when last row appears
- Tap row → navigate to `StoryDetailView`
2. **StoryRowView** — one row in the signal feed
- Left edge stripe: 3pt wide, color opacity tied to signal score (97 = #FF5C00, fades to #1a1a1a at 0)
- Story title, dimmed proportionally to score (97 = white, 20 = #444)
- Signal score number, same fade behaviour
- Source count, topic, time ago in monospaced font
- Source chips (first 2 named, rest "+N")
- Small green dot if story is fully cached offline
3. **StoryDetailView** — full story
- Back button "Signal feed"
- Category label + signal score pill (e.g. "Finance · 97 signal")
- Large headline, weight 800
- Source chips
- "All N articles cached · available offline" line if cached
- Consensus block: orange left border, dark orange background
- Conflicting block: dark red left border, dark red background (only if `conflict != nil`)
- Coverage timeline: chronological list of `TimelineEntry`, first item has orange node
- Tapping a timeline entry → navigate to `ArticleReaderView`
4. **ArticleReaderView** — full article
- Back button showing parent story headline (truncated to 30 chars)
- Source pill in #FF5C00, timestamp in monospaced
- Cached badge inline (green, only if article is in SwiftData)
- Large headline weight 800
- Hero image placeholder (AsyncImage with grey fallback)
- Body text, #4a4a4a, size 16, line spacing 1.65
- Divider then "More from this cluster" section
- 23 other articles from same story as tappable rows
5. **FeedManagerView** — feed health and management
- Wordmark `Jarvis` header + "+ Add feed" button
- Platform connection card: server URL, live/offline dot (from `WebSocketManager`)
- Search bar to filter feed list
- Two sections: "Healthy" and "Needs attention"
- Each row: RSS icon, feed name, poll interval + article count, health dot
- Health dot: green (#2a5a2a) = active, amber (#AA6600) = failing with retry countdown, red (#5a1a1a) = dead
- Swipe to delete calls `APIClient.shared.deleteFeed(id:)`
- "+ Add feed" sheet: URL input + name field + confirm button
---
## Design rules — non-negotiable
- Background: `Color.black` / `#000000` everywhere
- Accent: `Color("KisaniOrange")` = `#FF5C00`
- Surface cards: `#111111`, `#1A1A1A`
- All screens: `.preferredColorScheme(.dark)`
- Font weight: 800 for headlines, 700 for labels, 400 for body — never use 600
- Monospaced font for: timestamps, signal scores, server URLs, feed metadata
- No gradients. No shadows. Flat surfaces only.
- Minimum tap target: 44pt
- Signal score stripe and text both fade: score 80100 = full orange, 5079 = dimmed orange, 2049 = dark brown, 019 = near invisible
---
## Data flow rules — non-negotiable
- Views never call `APIClient` directly — always go through `StoryStore` or a dedicated `@StateObject` ViewModel
- Signal scores are server-computed — never calculate or modify them in the UI
- WebSocket events trigger REST refetches — the store handles this already
- SwiftData (`CachedStory`, `CachedArticle`) is the offline source — query it when the server is unreachable
- Use `@EnvironmentObject var store: StoryStore` in views that need stories
- Use `@EnvironmentObject var ws: WebSocketManager` for connection state
---
## Xcode project setup
1. Create new Xcode project: iOS App, named **Jarvis**, Swift, SwiftUI
2. Set minimum deployment target: **iOS 17.0** (required for SwiftData)
3. Add all provided `.swift` files to the project
4. In `Assets.xcassets` add a new Color Set named `KisaniOrange`:
- Any / Dark: `#FF5C00`
5. Add `Assets.xcassets` Color Set `AppBackground`: `#000000`
6. The SwiftData model container is already configured in `JarvisApp.swift`
---
## File structure to produce
```
Jarvis/
Views/
Home/
SignalFeedView.swift
StoryRowView.swift
Story/
StoryDetailView.swift
Reader/
ArticleReaderView.swift
Feeds/
FeedManagerView.swift
AddFeedSheet.swift
Shared/
SignalStripe.swift ← reusable 3pt stripe component
SourceChips.swift ← reusable chips row
CachedBadge.swift ← reusable green cached indicator
ConnectionBanner.swift ← offline warning banner
```
---
## Start here
1. Read `CLAUDE.md`
2. Create the Xcode project and add all existing files
3. Build `SignalStripe.swift`, `SourceChips.swift`, `CachedBadge.swift` — shared components used by multiple views
4. Build `StoryRowView.swift`
5. Build `SignalFeedView.swift`
6. Build `StoryDetailView.swift`
7. Build `ArticleReaderView.swift`
8. Build `FeedManagerView.swift` + `AddFeedSheet.swift`
9. Run on simulator, fix layout issues, verify dark mode renders correctly
10. Confirm WebSocket connection state drives the live/offline indicator on the home screen
Do not skip steps. Do not simplify the signal stripe fade logic. Do not use placeholder colours — every hex value is specified above.