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>
6.0 KiB
Jarvis API Contract
Base URL: http://<host>:<port>/api/v1
Authentication
None. Local network / self-hosted only. All endpoints are open. Secure at the network level (VPN, firewall).
REST Endpoints
GET /stories
Returns paginated stories sorted by signal score descending.
Query params
| Param | Type | Default | Description |
|---|---|---|---|
| limit | int | 20 | Max stories per page (max 100) |
| after | string | — | Cursor for next page |
| topic | string | — | Filter by topic slug e.g. finance, tech |
| min_signal | int | — | Only return stories above this score |
Response 200
{
"data": [
{
"id": "story_2847",
"headline": "Bank of Uganda cuts interest rates",
"summary": "The Bank of Uganda reduced its benchmark lending rate by 50 basis points, citing easing inflation and the need to stimulate credit growth.",
"topic": "finance",
"signalScore": 91,
"scoreBreakdown": {
"sourceAuthority": 28,
"freshness": 22,
"localRelevance": 15,
"crossSourceConfirmation": 16,
"topicImportance": 10
},
"sourceCount": 7,
"sources": [
{
"id": "src_001",
"name": "Daily Monitor",
"url": "https://monitor.co.ug",
"publishedAt": "2026-06-18T08:03:00Z",
"isBreaking": true
}
],
"consensus": "All 7 sources confirm the rate cut. Daily Monitor and NilePost agree on the 50bp figure.",
"conflict": null,
"cachedAt": null,
"updatedAt": "2026-06-18T09:45:00Z",
"createdAt": "2026-06-18T08:03:00Z"
}
],
"nextCursor": "cursor_abc123",
"hasMore": true,
"total": 248
}
GET /stories/:id
Returns full story detail including article timeline.
Response 200
{
"id": "story_2847",
"headline": "Bank of Uganda cuts interest rates",
"summary": "...",
"topic": "finance",
"signalScore": 91,
"scoreBreakdown": {
"sourceAuthority": 28,
"freshness": 22,
"localRelevance": 15,
"crossSourceConfirmation": 16,
"topicImportance": 10
},
"sourceCount": 7,
"consensus": "All 7 sources confirm the rate cut.",
"conflict": "Two regional outlets dispute impact on mortgage rates.",
"timeline": [
{
"articleId": "art_001",
"source": "Daily Monitor",
"headline": "BoU cuts lending rate by 50 basis points",
"publishedAt": "2026-06-18T08:03:00Z",
"isBreaking": true
},
{
"articleId": "art_002",
"source": "NilePost",
"headline": "Central bank eases policy amid inflation slowdown",
"publishedAt": "2026-06-18T08:21:00Z",
"isBreaking": false
}
],
"updatedAt": "2026-06-18T09:45:00Z",
"createdAt": "2026-06-18T08:03:00Z"
}
GET /articles/:id
Returns full article content for offline caching.
Response 200
{
"id": "art_001",
"storyId": "story_2847",
"source": "Daily Monitor",
"sourceUrl": "https://monitor.co.ug",
"headline": "BoU cuts lending rate by 50 basis points",
"body": "Full article text...",
"imageUrl": "https://...",
"author": "John Doe",
"publishedAt": "2026-06-18T08:03:00Z"
}
GET /feeds
Returns all configured RSS feeds with health state.
Response 200
{
"data": [
{
"id": "feed_001",
"name": "Daily Monitor",
"url": "https://monitor.co.ug/rss",
"health": "active",
"pollIntervalSeconds": 300,
"failureCount": 0,
"lastFetchedAt": "2026-06-18T09:40:00Z",
"articleCountToday": 44
}
]
}
POST /feeds
Add a new RSS feed.
Request
{
"url": "https://example.com/rss",
"name": "Example News",
"pollIntervalSeconds": 900
}
Response 201
Returns the created feed object.
DELETE /feeds/:id
Remove a feed.
Response 204 No content.
GET /health
Server health check. Used by the app on launch to verify connection.
Response 200
{
"status": "ok",
"version": "1.0.0",
"storiesCount": 248,
"feedsCount": 37,
"uptime": 86400
}
WebSocket
Connect
ws://<host>:<port>/ws
No auth headers required.
Events (server → client)
story.updated
Signal score changed or new article joined the story.
{
"type": "story.updated",
"storyId": "story_2847",
"signalScore": 94,
"sourceCount": 9,
"updatedAt": "2026-06-18T10:01:00Z"
}
App behaviour: refetch GET /stories/story_2847, update local cache.
story.created
New story formed from a new cluster.
{
"type": "story.created",
"storyId": "story_2901",
"headline": "Parliament passes new budget",
"signalScore": 42,
"topic": "politics",
"createdAt": "2026-06-18T10:05:00Z"
}
App behaviour: insert into feed, resort by signal score.
story.stale
Story has gone cold — no new articles in decay window.
{
"type": "story.stale",
"storyId": "story_2800",
"updatedAt": "2026-06-18T10:10:00Z"
}
App behaviour: dim story in feed or remove based on user preference.
feed.health
A feed's health state changed.
{
"type": "feed.health",
"feedId": "feed_012",
"health": "failing",
"failureCount": 3,
"updatedAt": "2026-06-18T10:12:00Z"
}
App behaviour: update feed health indicator in feed manager.
Ping / Pong
Server sends {"type":"ping"} every 30s.
Client must respond with {"type":"pong"}.
No response in 60s → server closes connection.
Client should reconnect with exponential backoff: 1s → 2s → 4s → 8s → max 60s.
Error shapes
All errors follow this shape:
{
"error": {
"code": "story_not_found",
"message": "No story with id story_9999",
"status": 404
}
}
| Code | Status | Meaning |
|---|---|---|
story_not_found |
404 | Story ID does not exist |
feed_not_found |
404 | Feed ID does not exist |
invalid_cursor |
400 | Pagination cursor is malformed |
invalid_url |
400 | Feed URL is not a valid RSS feed |
server_error |
500 | Internal server error |