// // FeedEnvelope.swift // iris // // Created by Codex. // import Foundation import WeatherKit struct FeedEnvelope: Codable, Equatable { let schema: Int let generatedAt: Int let feed: [FeedCard] let meta: FeedMeta enum CodingKeys: String, CodingKey { case schema case generatedAt = "generated_at" case feed case meta } } struct FeedCard: Codable, Equatable { enum Bucket: String, Codable { case rightNow = "RIGHT_NOW" case fyi = "FYI" } let id: String let type: WinnerType let title: String let subtitle: String let priority: Double let ttlSec: Int let condition: WeatherKit.WeatherCondition? let bucket: Bucket let actions: [String] enum CodingKeys: String, CodingKey { case id case type case title case subtitle case priority case ttlSec = "ttl_sec" case condition case bucket case actions } init(id: String, type: WinnerType, title: String, subtitle: String, priority: Double, ttlSec: Int, condition: WeatherKit.WeatherCondition? = nil, bucket: Bucket, actions: [String]) { self.id = id self.type = type self.title = title self.subtitle = subtitle self.priority = priority self.ttlSec = ttlSec self.condition = condition self.bucket = bucket self.actions = actions } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decode(String.self, forKey: .id) type = try container.decode(WinnerType.self, forKey: .type) title = try container.decode(String.self, forKey: .title) subtitle = try container.decode(String.self, forKey: .subtitle) priority = try container.decode(Double.self, forKey: .priority) ttlSec = try container.decode(Int.self, forKey: .ttlSec) bucket = try container.decode(Bucket.self, forKey: .bucket) actions = try container.decode([String].self, forKey: .actions) if let encoded = try container.decodeIfPresent(String.self, forKey: .condition) { condition = WeatherKit.WeatherCondition.irisDecode(encoded) } else { condition = nil } } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(id, forKey: .id) try container.encode(type, forKey: .type) try container.encode(title, forKey: .title) try container.encode(subtitle, forKey: .subtitle) try container.encode(priority, forKey: .priority) try container.encode(ttlSec, forKey: .ttlSec) try container.encode(bucket, forKey: .bucket) try container.encode(actions, forKey: .actions) if let condition { try container.encode(condition.irisScreamingCase(), forKey: .condition) } } } struct FeedMeta: Codable, Equatable { let winnerId: String let unreadCount: Int enum CodingKeys: String, CodingKey { case winnerId = "winner_id" case unreadCount = "unread_count" } } extension FeedEnvelope { static func fromWinnerAndWeather(now: Int, winner: WinnerEnvelope, weather: Candidate?) -> FeedEnvelope { var cards: [FeedCard] = [] let winnerCard = FeedCard( id: winner.winner.id, type: winner.winner.type, title: winner.winner.title.truncated(maxLength: TextConstraints.titleMax), subtitle: winner.winner.subtitle.truncated(maxLength: TextConstraints.subtitleMax), priority: min(max(winner.winner.priority, 0.0), 1.0), ttlSec: max(1, winner.winner.ttlSec), condition: nil, bucket: .rightNow, actions: ["DISMISS"] ) cards.append(winnerCard) if let weather, weather.id != winner.winner.id { let weatherCard = FeedCard( id: weather.id, type: weather.type, title: weather.title.truncated(maxLength: TextConstraints.titleMax), subtitle: weather.subtitle.truncated(maxLength: TextConstraints.subtitleMax), priority: min(max(weather.confidence, 0.0), 1.0), ttlSec: max(1, weather.ttlSec), condition: weather.condition, bucket: .fyi, actions: ["DISMISS"] ) cards.append(weatherCard) } return FeedEnvelope( schema: 1, generatedAt: now, feed: cards, meta: FeedMeta(winnerId: winner.winner.id, unreadCount: cards.count) ) } } extension FeedEnvelope { static func allQuiet(now: Int, reason: String = "no_candidates", source: String = "engine") -> FeedEnvelope { let card = FeedCard( id: "quiet-000", type: .allQuiet, title: "All Quiet", subtitle: "No urgent updates", priority: 0.05, ttlSec: 300, condition: nil, bucket: .rightNow, actions: ["DISMISS"] ) return FeedEnvelope(schema: 1, generatedAt: now, feed: [card], meta: FeedMeta(winnerId: card.id, unreadCount: 1)) } func winnerCard() -> FeedCard? { feed.first(where: { $0.id == meta.winnerId }) ?? feed.first } func asWinnerEnvelope() -> WinnerEnvelope { let now = generatedAt guard let winnerCard = winnerCard() else { return WinnerEnvelope.allQuiet(now: now) } let winner = Winner( id: winnerCard.id, type: winnerCard.type, title: winnerCard.title.truncated(maxLength: TextConstraints.titleMax), subtitle: winnerCard.subtitle.truncated(maxLength: TextConstraints.subtitleMax), priority: min(max(winnerCard.priority, 0.0), 1.0), ttlSec: max(1, winnerCard.ttlSec) ) return WinnerEnvelope(schema: 1, generatedAt: now, winner: winner, debug: nil) } }