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:
214
Jarvis/Models/Models.swift
Normal file
214
Jarvis/Models/Models.swift
Normal file
@@ -0,0 +1,214 @@
|
||||
// Models.swift
|
||||
// Jarvis — all Codable structs matching the API contract
|
||||
|
||||
import Foundation
|
||||
import SwiftData
|
||||
|
||||
// MARK: - Signal Score
|
||||
|
||||
struct SignalScore: Codable, Hashable {
|
||||
let total: Int
|
||||
let sourceAuthority: Int
|
||||
let freshness: Int
|
||||
let localRelevance: Int
|
||||
let crossSourceConfirmation: Int
|
||||
let topicImportance: Int
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case total = "signalScore"
|
||||
case sourceAuthority, freshness, localRelevance
|
||||
case crossSourceConfirmation, topicImportance
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Source
|
||||
|
||||
struct StorySource: Codable, Hashable, Identifiable {
|
||||
let id: String
|
||||
let name: String
|
||||
let url: String
|
||||
let publishedAt: Date
|
||||
let isBreaking: Bool
|
||||
}
|
||||
|
||||
// MARK: - Timeline Entry
|
||||
|
||||
struct TimelineEntry: Codable, Hashable, Identifiable {
|
||||
let articleId: String
|
||||
let source: String
|
||||
let headline: String
|
||||
let publishedAt: Date
|
||||
let isBreaking: Bool
|
||||
|
||||
var id: String { articleId }
|
||||
}
|
||||
|
||||
// MARK: - Story Summary (list view)
|
||||
|
||||
struct StorySummary: Codable, Identifiable, Hashable {
|
||||
let id: String
|
||||
let headline: String
|
||||
let summary: String
|
||||
let topic: String
|
||||
let signalScore: Int
|
||||
let scoreBreakdown: ScoreBreakdown
|
||||
let sourceCount: Int
|
||||
let sources: [StorySource]
|
||||
let consensus: String?
|
||||
let conflict: String?
|
||||
let updatedAt: Date
|
||||
let createdAt: Date
|
||||
}
|
||||
|
||||
struct ScoreBreakdown: Codable, Hashable {
|
||||
let sourceAuthority: Int
|
||||
let freshness: Int
|
||||
let localRelevance: Int
|
||||
let crossSourceConfirmation: Int
|
||||
let topicImportance: Int
|
||||
}
|
||||
|
||||
// MARK: - Story Detail (full, with timeline)
|
||||
|
||||
struct StoryDetail: Codable, Identifiable {
|
||||
let id: String
|
||||
let headline: String
|
||||
let summary: String
|
||||
let topic: String
|
||||
let signalScore: Int
|
||||
let scoreBreakdown: ScoreBreakdown
|
||||
let sourceCount: Int
|
||||
let consensus: String?
|
||||
let conflict: String?
|
||||
let timeline: [TimelineEntry]
|
||||
let updatedAt: Date
|
||||
let createdAt: Date
|
||||
}
|
||||
|
||||
// MARK: - Article (full content, for offline cache)
|
||||
|
||||
struct Article: Codable, Identifiable {
|
||||
let id: String
|
||||
let storyId: String
|
||||
let source: String
|
||||
let sourceUrl: String
|
||||
let headline: String
|
||||
let body: String
|
||||
let imageUrl: String?
|
||||
let author: String?
|
||||
let publishedAt: Date
|
||||
}
|
||||
|
||||
// MARK: - Feed
|
||||
|
||||
struct Feed: Codable, Identifiable {
|
||||
let id: String
|
||||
let name: String
|
||||
let url: String
|
||||
let health: FeedHealth
|
||||
let pollIntervalSeconds: Int
|
||||
let failureCount: Int
|
||||
let lastFetchedAt: Date?
|
||||
let articleCountToday: Int
|
||||
}
|
||||
|
||||
enum FeedHealth: String, Codable {
|
||||
case active, failing, dead
|
||||
}
|
||||
|
||||
// MARK: - Server Health
|
||||
|
||||
struct ServerHealth: Codable {
|
||||
let status: String
|
||||
let version: String
|
||||
let storiesCount: Int
|
||||
let feedsCount: Int
|
||||
let uptime: Int
|
||||
}
|
||||
|
||||
// MARK: - Paginated Response
|
||||
|
||||
struct PaginatedStories: Codable {
|
||||
let data: [StorySummary]
|
||||
let nextCursor: String?
|
||||
let hasMore: Bool
|
||||
let total: Int
|
||||
}
|
||||
|
||||
// MARK: - WebSocket Events
|
||||
|
||||
enum WSEventType: String, Codable {
|
||||
case storyUpdated = "story.updated"
|
||||
case storyCreated = "story.created"
|
||||
case storyStale = "story.stale"
|
||||
case feedHealth = "feed.health"
|
||||
case ping, pong
|
||||
}
|
||||
|
||||
struct WSEvent: Codable {
|
||||
let type: WSEventType
|
||||
let storyId: String?
|
||||
let feedId: String?
|
||||
let signalScore: Int?
|
||||
let sourceCount: Int?
|
||||
let headline: String?
|
||||
let topic: String?
|
||||
let health: FeedHealth?
|
||||
let failureCount: Int?
|
||||
let updatedAt: Date?
|
||||
let createdAt: Date?
|
||||
}
|
||||
|
||||
// MARK: - SwiftData cached models
|
||||
|
||||
@Model
|
||||
final class CachedStory {
|
||||
@Attribute(.unique) var id: String
|
||||
var headline: String
|
||||
var summary: String
|
||||
var topic: String
|
||||
var signalScore: Int
|
||||
var sourceCount: Int
|
||||
var consensus: String?
|
||||
var conflict: String?
|
||||
var updatedAt: Date
|
||||
var cachedAt: Date
|
||||
|
||||
init(from summary: StorySummary) {
|
||||
self.id = summary.id
|
||||
self.headline = summary.headline
|
||||
self.summary = summary.summary
|
||||
self.topic = summary.topic
|
||||
self.signalScore = summary.signalScore
|
||||
self.sourceCount = summary.sourceCount
|
||||
self.consensus = summary.consensus
|
||||
self.conflict = summary.conflict
|
||||
self.updatedAt = summary.updatedAt
|
||||
self.cachedAt = Date()
|
||||
}
|
||||
}
|
||||
|
||||
@Model
|
||||
final class CachedArticle {
|
||||
@Attribute(.unique) var id: String
|
||||
var storyId: String
|
||||
var source: String
|
||||
var headline: String
|
||||
var body: String
|
||||
var imageUrl: String?
|
||||
var author: String?
|
||||
var publishedAt: Date
|
||||
var cachedAt: Date
|
||||
|
||||
init(from article: Article) {
|
||||
self.id = article.id
|
||||
self.storyId = article.storyId
|
||||
self.source = article.source
|
||||
self.headline = article.headline
|
||||
self.body = article.body
|
||||
self.imageUrl = article.imageUrl
|
||||
self.author = article.author
|
||||
self.publishedAt = article.publishedAt
|
||||
self.cachedAt = Date()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user