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:
323
API/contract.md
Normal file
323
API/contract.md
Normal file
@@ -0,0 +1,323 @@
|
||||
# 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 |
|
||||
Reference in New Issue
Block a user