// // FeedEnvelope.swift // iris // // Created by Codex. // import Foundation import WeatherKit struct FeedEnvelope: Codable, Equatable { let schema: Int let generatedAt: Int let feed: [FeedItem] let meta: FeedMeta enum CodingKeys: String, CodingKey { case schema case generatedAt = "generated_at" case feed case meta } } struct FeedItem: Codable, Equatable { enum Bucket: String, Codable { case rightNow = "RIGHT_NOW" case fyi = "FYI" } let id: String let type: FeedItemType let title: String let subtitle: String let priority: Double let ttlSec: Int let condition: WeatherKit.WeatherCondition? let startsAt: Int? 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 startsAt = "starts_at" case bucket case actions } init(id: String, type: FeedItemType, title: String, subtitle: String, priority: Double, ttlSec: Int, condition: WeatherKit.WeatherCondition? = nil, startsAt: Int? = 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.startsAt = startsAt 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(FeedItemType.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) startsAt = try container.decodeIfPresent(Int.self, forKey: .startsAt) 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) try container.encodeIfPresent(startsAt, forKey: .startsAt) 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 allQuiet(now: Int, reason: String = "no_candidates", source: String = "engine") -> FeedEnvelope { let item = FeedItem( id: "quiet-000", type: .allQuiet, title: "All Quiet", subtitle: "No urgent updates", priority: 0.05, ttlSec: 300, condition: nil, startsAt: nil, bucket: .rightNow, actions: ["DISMISS"] ) return FeedEnvelope(schema: 1, generatedAt: now, feed: [item], meta: FeedMeta(winnerId: item.id, unreadCount: 1)) } func winnerItem() -> FeedItem? { feed.first(where: { $0.id == meta.winnerId }) ?? feed.first } }