import Foundation
struct SystemStatusService: Sendable {
nonisolated static let statusURL = SRHTWebURL.status
nonisolated static let feedURL = SRHTWebURL.statusIncidentFeed
private let session: URLSession
private let now: @Sendable () -> Date
nonisolated init(session: URLSession = .shared, now: @escaping @Sendable () -> Date = Date.init) {
self.session = session
self.now = now
}
func fetchSnapshot() async throws -> SystemStatusSnapshot {
let html = try await fetchSnapshotHTML()
return try Self.parseSnapshotHTML(html, fetchedAt: now())
}
func fetchIncidentFeed() async throws -> [StatusIncident] {
let data = try await fetchIncidentFeedData()
return try await Self.parseIncidentFeedXML(data)
}
func fetchSnapshotHTML() async throws -> String {
try await fetchText(from: Self.statusURL, accept: "text/html,application/xhtml+xml")
}
func fetchIncidentFeedData() async throws -> Data {
try await fetchData(from: Self.feedURL, accept: "application/rss+xml,application/xml,text/xml")
}
private func fetchText(from url: URL, accept: String) async throws -> String {
let data = try await fetchData(from: url, accept: accept)
guard let text = String(data: data, encoding: .utf8) else {
throw SRHTError.decodingError(
DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "Response is not UTF-8 text"))
)
}
return text
}
private func fetchData(from url: URL, accept: String) async throws -> Data {
var request = URLRequest(url: url)
request.setValue(userAgent, forHTTPHeaderField: "User-Agent")
request.setValue(accept, forHTTPHeaderField: "Accept")
let (data, response): (Data, URLResponse)
do {
(data, response) = try await session.data(for: request)
} catch {
throw SRHTError.networkError(error)
}
if let http = response as? HTTPURLResponse,
!(200...299).contains(http.statusCode) {
throw SRHTError.httpError(http.statusCode)
}
return data
}
private var userAgent: String {
let bundle = Bundle.main
let name = (bundle.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String)
?? (bundle.object(forInfoDictionaryKey: "CFBundleName") as? String)
?? "Hutch"
let version = (bundle.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String) ?? "dev"
return "\(name)/\(version) (System Status)"
}
}
extension SystemStatusService: SystemStatusServing {}
extension SystemStatusService {
nonisolated static func parseSnapshotHTML(_ html: String, fetchedAt: Date) throws -> SystemStatusSnapshot {
let services = parseServices(in: html)
let incidents = parseHTMLIncidentCards(in: html)
let summaries = parseActiveIncidentSummaries(in: html)
let activeIncidents = incidents
.filter { $0.isActive == true }
.map { incident in
let summary = incident.url.flatMap { summaries[$0.absoluteString] } ?? incident.summary
return StatusIncident(
id: incident.id,
title: incident.title,
summary: summary,
url: incident.url,
publishedAt: incident.publishedAt,
updatedAt: incident.updatedAt,
isActive: incident.isActive
)
}
return SystemStatusSnapshot(services: services, activeIncidents: activeIncidents, lastUpdated: fetchedAt)
}
nonisolated static func parseIncidentFeedXML(_ data: Data) async throws -> [StatusIncident] {
try await MainActor.run {
let parser = SystemStatusFeedParser()
return try parser.parse(data: data)
}
}
nonisolated private static func parseServices(in html: String) -> [StatusServiceState] {
firstMatches(
in: html,
pattern: #"
]*\bclass\s*=\s*["'][^"']*\bcomponent\b[^"']*["'])(?=[^>]*\bdata-status\s*=\s*["']([^"']+)["'])[^>]*>([\s\S]*?)
"#
).compactMap { captures in
guard captures.count >= 2 else { return nil }
let rawStatus = captures[0]
let content = captures[1]
guard let linkCaptures = firstMatches(
in: content,
pattern: #"]*\bhref\s*=\s*["']([^"']+)["'][^>]*>([\s\S]*?)"#
).first,
linkCaptures.count >= 2 else {
return nil
}
let href = linkCaptures[0]
let cleanedName = cleanText(linkCaptures[1])
let readableStatus = firstMatch(
in: content,
pattern: #"<(?:span|small|div)\b(?=[^>]*\bclass\s*=\s*["'][^"']*\bcomponent-status\b[^"']*["'])[^>]*>([\s\S]*?)(?:span|small|div)>"#
).map(cleanText) ?? firstStatusLabel(in: content) ?? ""
let level = statusLevel(fromHTMLStatus: rawStatus)
return StatusServiceState(
id: normalizedSlug(from: href, fallback: cleanedName) ?? cleanedName,
name: cleanedName,
slug: normalizedSlug(from: href, fallback: cleanedName),
status: level == .unknown ? statusLevel(fromLabel: readableStatus) : level,
description: nil
)
}
}
nonisolated private static func parseHTMLIncidentCards(in html: String) -> [StatusIncident] {
firstMatches(
in: html,
pattern: #"]*\bclass\s*=\s*["'][^"']*\bissue\b[^"']*["'])(?=[^>]*\bhref\s*=\s*["']([^"']+)["'])[^>]*>([\s\S]*?)"#
).compactMap { captures in
guard captures.count >= 2 else { return nil }
let href = captures[0]
let content = captures[1]
guard let titleHTML = firstMatch(in: content, pattern: #"]*>\s*([\s\S]*?)\s*"#)
?? firstMatch(in: content, pattern: #"]*>\s*([\s\S]*?)\s*"#)
?? firstMatch(in: content, pattern: #"]*>\s*([\s\S]*?)\s*"#),
let publishedAt = publishedIncidentDate(in: content) else {
return nil
}
let url = URL(string: href, relativeTo: statusURL)?.absoluteURL
return StatusIncident(
id: url?.absoluteString ?? cleanText(titleHTML),
title: cleanText(titleHTML),
summary: nil,
url: url,
publishedAt: publishedAt,
updatedAt: nil,
isActive: isActiveIncidentCard(content)
)
}
}
nonisolated private static func parseActiveIncidentSummaries(in html: String) -> [String: String] {
firstMatches(
in: html,
pattern: #"]*\bclass\s*=\s*["'][^"']*\bannouncement-box\b[^"']*["'])[^>]*>([\s\S]*?)
\s*(?:
]*\bannouncement-box\b[^>]*>)?"#
).reduce(into: [:]) { partialResult, captures in
guard let content = captures.first,
let titleLinkCaptures = firstMatches(
in: content,
pattern: #"]*\bhref\s*=\s*["']([^"']+)["'][^>]*>([\s\S]*?)"#
).first,
let href = titleLinkCaptures.first else {
return
}
let paragraphs = firstMatches(in: content, pattern: #"([\s\S]*?)
"#)
.compactMap(\.first)
.map(cleanText)
.filter { !$0.isEmpty }
let summary = paragraphs.dropFirst(2).first ?? paragraphs.dropFirst().first
guard let summary, !summary.isEmpty else { return }
if let url = URL(string: href, relativeTo: statusURL)?.absoluteURL {
partialResult[url.absoluteString] = summary
}
}
}
nonisolated private static func statusLevel(fromHTMLStatus status: String) -> StatusLevel {
switch status.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() {
case "ok":
.operational
case "disrupted":
.degraded
case "down":
.majorOutage
case "notice":
.maintenance
default:
.unknown
}
}
nonisolated private static func statusLevel(fromLabel label: String) -> StatusLevel {
let normalizedLabel = label
.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased()
.replacingOccurrences(of: #"\s+"#, with: " ", options: .regularExpression)
return switch normalizedLabel {
case "operational", "all systems operational":
StatusLevel.operational
case "disrupted", "degraded", "partial outage":
StatusLevel.degraded
case "down", "major outage", "outage":
StatusLevel.majorOutage
case "maintenance", "scheduled maintenance", "under maintenance":
StatusLevel.maintenance
default:
StatusLevel.unknown
}
}
nonisolated private static func normalizedSlug(from href: String, fallback name: String) -> String? {
if href.contains("/affected/") {
let trimmed = href.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
if let slug = trimmed.split(separator: "/").last {
return String(slug)
}
}
return name.isEmpty ? nil : name
}
nonisolated private static func firstMatch(in text: String, pattern: String) -> String? {
firstMatches(in: text, pattern: pattern).first?.first
}
nonisolated private static func firstMatches(in text: String, pattern: String) -> [[String]] {
guard let regex = try? NSRegularExpression(
pattern: pattern,
options: [.caseInsensitive, .dotMatchesLineSeparators]
) else {
return []
}
let range = NSRange(text.startIndex..., in: text)
return regex.matches(in: text, range: range).map { match in
(1.. String {
let stripped = text.replacingOccurrences(of: #"<[^>]+>"#, with: " ", options: .regularExpression)
let decoded = decodeHTMLEntities(stripped)
return decoded
.replacingOccurrences(of: #"\s+"#, with: " ", options: .regularExpression)
.replacingOccurrences(of: #"\s+([.,!?;:])"#, with: "$1", options: .regularExpression)
.replacingOccurrences(of: "→", with: "")
.trimmingCharacters(in: .whitespacesAndNewlines)
}
nonisolated private static func firstStatusLabel(in text: String) -> String? {
cleanText(text)
.split(separator: "•")
.map(String.init)
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
.first(where: { statusLevel(fromLabel: $0) != .unknown })
}
nonisolated private static func publishedIncidentDate(in content: String) -> Date? {
if let timeValue = firstMatch(in: content, pattern: #"