Files
steipete--codexbar/Sources/CodexBar/UsageStore+Status.swift
T
2026-07-13 12:22:33 +08:00

492 lines
20 KiB
Swift

import CodexBarCore
import Foundation
/// Shared, lock-guarded ISO8601 formatters for status feeds. Allocating a fresh
/// `ISO8601DateFormatter` per decoded date field is a measurable share of decoding the
/// Google Workspace incidents feed, which can run to hundreds of kilobytes (#1399).
private final class StatusISO8601FormatterBox: @unchecked Sendable {
let lock = NSLock()
let withFractional: ISO8601DateFormatter = {
let fmt = ISO8601DateFormatter()
fmt.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
return fmt
}()
let plain: ISO8601DateFormatter = {
let fmt = ISO8601DateFormatter()
fmt.formatOptions = [.withInternetDateTime]
return fmt
}()
}
private enum StatusFeedDateParser {
static let box = StatusISO8601FormatterBox()
static func parse(_ text: String) -> Date? {
self.box.lock.lock()
defer { self.box.lock.unlock() }
return self.box.withFractional.date(from: text) ?? self.box.plain.date(from: text)
}
static func decodingStrategy() -> JSONDecoder.DateDecodingStrategy {
.custom { decoder in
let container = try decoder.singleValueContainer()
let raw = try container.decode(String.self)
guard let date = StatusFeedDateParser.parse(raw) else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid ISO8601 date")
}
return date
}
}
}
extension UsageStore {
/// Status feeds decode off the main actor: the Google Workspace incidents payload alone
/// can be hundreds of kilobytes and cost 150-340ms to decode (#1399), and these helpers
/// touch no store state.
@concurrent
nonisolated static func fetchStatus(
from baseURL: URL,
transport: any ProviderHTTPTransport = ProviderHTTPClient.shared)
async throws -> ProviderStatus
{
let apiURL = baseURL.appendingPathComponent("api/v2/status.json")
var request = URLRequest(url: apiURL)
request.timeoutInterval = 10
let (data, _) = try await transport.data(for: request)
struct Response: Decodable {
struct Status: Decodable {
let indicator: String
let description: String?
}
struct Page: Decodable {
let updatedAt: Date?
private enum CodingKeys: String, CodingKey {
case updatedAt = "updated_at"
}
}
let page: Page?
let status: Status
}
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = StatusFeedDateParser.decodingStrategy()
let response = try decoder.decode(Response.self, from: data)
let indicator = ProviderStatusIndicator(rawValue: response.status.indicator) ?? .unknown
return ProviderStatus(
indicator: indicator,
description: response.status.description,
updatedAt: response.page?.updatedAt)
}
/// Resolves the provider's status and component list.
///
/// OpenAI's status page is powered by incident.io, whose native feed groups components
/// (APIs / ChatGPT / Codex / FedRAMP) — so we try that first. Classic Atlassian
/// statuspage.io pages (Claude, Cursor, GitHub) expose a flat `api/v2` feed, which we fall
/// back to. `components.json` is used for the flat list because `summary.json` omits unlisted
/// components such as "FedRAMP".
@concurrent
nonisolated static func fetchStatusSummary(
from baseURL: URL,
transport: any ProviderHTTPTransport = ProviderHTTPClient.shared)
async throws -> (status: ProviderStatus, components: [ProviderStatusComponent]?)
{
// incident.io native feed (grouped): https://<host>/proxy/<host>
if let host = baseURL.host,
let proxyURL = URL(string: "https://\(host)/proxy/\(host)")
{
var proxyRequest = URLRequest(url: proxyURL)
proxyRequest.timeoutInterval = 10
if let (data, _) = try? await transport.data(for: proxyRequest),
let parsed = try? Self.parseIncidentIOSummary(data: data)
{
// The proxy feed derives the indicator from component leaves but carries no
// top-level description or timestamp; fetch the summary endpoint so callers
// get the full status banner.
let overlay = try? await Self.fetchStatus(from: baseURL, transport: transport)
let status = ProviderStatus(
indicator: parsed.status.indicator,
description: overlay?.description,
updatedAt: overlay?.updatedAt)
return (status, parsed.components)
}
}
// Classic statuspage.io fallback.
var summaryRequest = URLRequest(url: baseURL.appendingPathComponent("api/v2/summary.json"))
summaryRequest.timeoutInterval = 10
var componentsRequest = URLRequest(url: baseURL.appendingPathComponent("api/v2/components.json"))
componentsRequest.timeoutInterval = 10
let (summaryData, _) = try await transport.data(for: summaryRequest)
let status = try Self.parseStatuspageStatus(data: summaryData)
let components: [ProviderStatusComponent]? = if let (componentsData, _) = try? await transport
.data(for: componentsRequest)
{
try? Self.parseStatuspageComponents(data: componentsData)
} else {
nil
}
return (status, components)
}
/// Parses incident.io's native status-page summary (`/proxy/<host>`). Groups come from
/// `structure.items`; per-component statuses come from `affected_components` (anything not
/// listed there is operational). A group's status aggregates the worst of its children.
nonisolated static func parseIncidentIOSummary(
data: Data)
throws -> (status: ProviderStatus, components: [ProviderStatusComponent])
{
// The incident.io payload mirrors a deeply nested JSON shape; the response models follow it
// 1:1 for clarity, which exceeds the default type-nesting depth.
// swiftlint:disable nesting
struct Response: Decodable {
struct Summary: Decodable {
struct AffectedComponent: Decodable {
let componentID: String
let status: String?
private enum CodingKeys: String, CodingKey {
case componentID = "component_id"
case status
}
}
struct Structure: Decodable {
struct Item: Decodable {
struct Group: Decodable {
struct Child: Decodable {
let componentID: String
let name: String?
let hidden: Bool?
private enum CodingKeys: String, CodingKey {
case componentID = "component_id"
case name, hidden
}
}
let id: String
let name: String?
let hidden: Bool?
let components: [Child]?
}
struct Component: Decodable {
let componentID: String
let name: String?
let hidden: Bool?
private enum CodingKeys: String, CodingKey {
case componentID = "component_id"
case name, hidden
}
}
let group: Group?
let component: Component?
}
let items: [Item]?
}
let affectedComponents: [AffectedComponent]?
let structure: Structure?
private enum CodingKeys: String, CodingKey {
case affectedComponents = "affected_components"
case structure
}
}
let summary: Summary?
}
// swiftlint:enable nesting
let response = try JSONDecoder().decode(Response.self, from: data)
guard let summary = response.summary,
let items = summary.structure?.items,
!items.isEmpty
else {
throw URLError(.cannotParseResponse)
}
var statusByID: [String: String] = [:]
for affected in summary.affectedComponents ?? [] {
statusByID[affected.componentID] = affected.status
}
func leaf(id: String, name: String) -> ProviderStatusComponent {
let raw = statusByID[id] ?? "operational"
return ProviderStatusComponent(
id: id,
name: name,
indicator: ProviderStatusComponent.indicator(forStatuspageStatus: raw),
status: raw)
}
var topLevel: [ProviderStatusComponent] = []
for item in items {
if let group = item.group, group.hidden != true {
let children = (group.components ?? [])
.filter { $0.hidden != true }
.compactMap { child -> ProviderStatusComponent? in
guard let name = Self.normalizedStatusComponentName(child.name) else { return nil }
return leaf(id: child.componentID, name: name)
}
guard let groupName = Self.normalizedStatusComponentName(group.name) else { continue }
let worst = children.max { Self.indicatorRank($0.indicator) < Self.indicatorRank($1.indicator) }
topLevel.append(ProviderStatusComponent(
id: group.id,
name: groupName,
indicator: worst?.indicator ?? .none,
status: worst?.status ?? "operational",
children: children))
} else if let component = item.component,
component.hidden != true,
let name = Self.normalizedStatusComponentName(component.name)
{
topLevel.append(leaf(id: component.componentID, name: name))
}
}
let leaves = topLevel.flatMap { $0.isGroup ? $0.children : [$0] }
let overall = leaves.max { Self.indicatorRank($0.indicator) < Self.indicatorRank($1.indicator) }
let status = ProviderStatus(
indicator: overall?.indicator ?? .none,
description: nil,
updatedAt: nil)
return (status, topLevel)
}
nonisolated static func parseStatuspageStatus(data: Data) throws -> ProviderStatus {
struct Response: Decodable {
struct Status: Decodable {
let indicator: String
let description: String?
}
struct Page: Decodable {
let updatedAt: Date?
private enum CodingKeys: String, CodingKey {
case updatedAt = "updated_at"
}
}
let page: Page?
let status: Status
}
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = StatusFeedDateParser.decodingStrategy()
let response = try decoder.decode(Response.self, from: data)
let indicator = ProviderStatusIndicator(rawValue: response.status.indicator) ?? .unknown
return ProviderStatus(
indicator: indicator,
description: response.status.description,
updatedAt: response.page?.updatedAt)
}
nonisolated static func parseStatuspageComponents(data: Data) throws -> [ProviderStatusComponent] {
struct Response: Decodable {
struct Component: Decodable {
let id: String
let name: String
let status: String
let group: Bool?
let groupID: String?
let position: Int?
private enum CodingKeys: String, CodingKey {
case id, name, status, group, position
case groupID = "group_id"
}
}
let components: [Component]?
}
let response = try JSONDecoder().decode(Response.self, from: data)
let raw = (response.components ?? [])
.filter { Self.normalizedStatusComponentName($0.name) != nil }
.sorted { ($0.position ?? 0) < ($1.position ?? 0) }
func makeRow(
_ component: Response.Component,
children: [ProviderStatusComponent]) -> ProviderStatusComponent
{
ProviderStatusComponent(
id: component.id,
name: Self.normalizedStatusComponentName(component.name) ?? component.name,
indicator: ProviderStatusComponent.indicator(forStatuspageStatus: component.status),
status: component.status,
children: children)
}
// Children keyed by their parent group id, preserving position order.
var childrenByGroup: [String: [ProviderStatusComponent]] = [:]
for component in raw where component.group != true {
guard let groupID = component.groupID else { continue }
childrenByGroup[groupID, default: []].append(makeRow(component, children: []))
}
// Top-level rows: groups (with their children) and ungrouped leaf components, in order.
return raw.compactMap { component in
if component.group == true {
return makeRow(component, children: childrenByGroup[component.id] ?? [])
}
// Skip leaves that belong to a group; they are rendered inside the group's dropdown.
if component.groupID != nil { return nil }
return makeRow(component, children: [])
}
}
private nonisolated static func normalizedStatusComponentName(_ name: String?) -> String? {
guard let name = name?.trimmingCharacters(in: .whitespacesAndNewlines), !name.isEmpty else { return nil }
return name
}
@concurrent
nonisolated static func fetchWorkspaceStatus(
productID: String,
transport: any ProviderHTTPTransport = ProviderHTTPClient.shared,
beforeDecoding: (@Sendable () -> Void)? = nil)
async throws -> ProviderStatus
{
guard let url = URL(string: "https://www.google.com/appsstatus/dashboard/incidents.json") else {
throw URLError(.badURL)
}
var request = URLRequest(url: url)
request.timeoutInterval = 10
let (data, _) = try await transport.data(for: request)
beforeDecoding?()
return try Self.parseGoogleWorkspaceStatus(data: data, productID: productID)
}
nonisolated static func parseGoogleWorkspaceStatus(data: Data, productID: String) throws -> ProviderStatus {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = StatusFeedDateParser.decodingStrategy()
let incidents = try decoder.decode([GoogleWorkspaceIncident].self, from: data)
let active = incidents.filter { $0.isRelevant(productID: productID) && $0.isActive }
guard !active.isEmpty else {
return ProviderStatus(indicator: .none, description: nil, updatedAt: nil)
}
var best: (
indicator: ProviderStatusIndicator,
incident: GoogleWorkspaceIncident,
update: GoogleWorkspaceUpdate?)
best = (indicator: .none, incident: active[0], update: active[0].mostRecentUpdate ?? active[0].updates?.last)
for incident in active {
let update = incident.mostRecentUpdate ?? incident.updates?.last
let indicator = Self.workspaceIndicator(
status: update?.status ?? incident.statusImpact,
severity: incident.severity)
if Self.indicatorRank(indicator) <= Self.indicatorRank(best.indicator) { continue }
best = (indicator: indicator, incident: incident, update: update)
}
let description = Self.workspaceSummary(from: best.update?.text ?? best.incident.externalDesc)
let updatedAt = best.update?.when ?? best.incident.modified ?? best.incident.begin
return ProviderStatus(indicator: best.indicator, description: description, updatedAt: updatedAt)
}
private nonisolated static func indicatorRank(_ indicator: ProviderStatusIndicator) -> Int {
switch indicator {
case .none: 0
case .maintenance: 1
case .minor: 2
case .major: 3
case .critical: 4
case .unknown: 1
}
}
private nonisolated static func workspaceIndicator(status: String?, severity: String?) -> ProviderStatusIndicator {
switch status?.uppercased() {
case "AVAILABLE": return .none
case "SERVICE_INFORMATION": return .minor
case "SERVICE_DISRUPTION": return .major
case "SERVICE_OUTAGE": return .critical
case "SERVICE_MAINTENANCE", "SCHEDULED_MAINTENANCE": return .maintenance
default: break
}
switch severity?.lowercased() {
case "low": return .minor
case "medium": return .major
case "high": return .critical
default: return .minor
}
}
private nonisolated static func workspaceSummary(from text: String?) -> String? {
guard let text else { return nil }
let normalized = text
.replacingOccurrences(of: "\r\n", with: "\n")
.replacingOccurrences(of: "\r", with: "\n")
let lines = normalized.split(separator: "\n", omittingEmptySubsequences: true)
for rawLine in lines {
let trimmed = rawLine.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmed.isEmpty { continue }
let lower = trimmed.lowercased()
if lower.hasPrefix("**summary") || lower.hasPrefix("**description") || lower == "summary" {
continue
}
var cleaned = trimmed.replacingOccurrences(of: "**", with: "")
cleaned = cleaned.replacingOccurrences(
of: #"\[([^\]]+)\]\([^)]+\)"#,
with: "$1",
options: .regularExpression)
if cleaned.hasPrefix("- ") {
cleaned.removeFirst(2)
}
cleaned = cleaned.trimmingCharacters(in: .whitespacesAndNewlines)
if !cleaned.isEmpty { return cleaned }
}
return nil
}
private struct GoogleWorkspaceIncident: Decodable {
let begin: Date?
let end: Date?
let modified: Date?
let externalDesc: String?
let statusImpact: String?
let severity: String?
let affectedProducts: [GoogleWorkspaceProduct]?
let currentlyAffectedProducts: [GoogleWorkspaceProduct]?
let mostRecentUpdate: GoogleWorkspaceUpdate?
let updates: [GoogleWorkspaceUpdate]?
var isActive: Bool {
self.end == nil
}
func isRelevant(productID: String) -> Bool {
if let current = currentlyAffectedProducts {
return current.contains(where: { $0.id == productID })
}
return self.affectedProducts?.contains(where: { $0.id == productID }) ?? false
}
}
private struct GoogleWorkspaceProduct: Decodable {
let title: String?
let id: String
}
private struct GoogleWorkspaceUpdate: Decodable {
let when: Date?
let status: String?
let text: String?
}
}