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>
324 lines
6.0 KiB
Markdown
324 lines
6.0 KiB
Markdown
# 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**
|
|
|
|
```json
|
|
{
|
|
"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**
|
|
|
|
```json
|
|
{
|
|
"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**
|
|
|
|
```json
|
|
{
|
|
"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**
|
|
|
|
```json
|
|
{
|
|
"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**
|
|
|
|
```json
|
|
{
|
|
"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**
|
|
|
|
```json
|
|
{
|
|
"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.
|
|
|
|
```json
|
|
{
|
|
"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.
|
|
|
|
```json
|
|
{
|
|
"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.
|
|
|
|
```json
|
|
{
|
|
"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.
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```json
|
|
{
|
|
"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 |
|