krz/hutch

an ios client for sourcehut

clone: git clone https://gitbay.org/krz/hutch.git

cb1ea5ea4f163d87053285d3fb7999b12a3558b9

verified · cmc

author: Christian Cleberg <hello@cleberg.net> · 2026-04-12T05:15:13Z

harden system status and app reliability
 Hutch.xcodeproj/project.pbxproj                    |  16 +-
 Hutch/App/AppStorageKeys.swift                     |   2 +
 Hutch/Networking/SystemStatusCacheStore.swift      |  59 +++++++
 Hutch/Networking/SystemStatusRepository.swift      | 172 ++++++++++++++++++---
 Hutch/Networking/SystemStatusService.swift         | 118 ++++++++++----
 Hutch/Views/Home/HomeView.swift                    |  48 ++----
 Hutch/Views/Home/HomeViewModel.swift               |  28 +++-
 Hutch/Views/More/MoreView.swift                    |  27 +++-
 Hutch/Views/More/MoreViewModel.swift               |  39 +++++
 .../SystemStatus/SystemStatusSummaryRow.swift      | 119 ++++++++++++++
 Hutch/Views/SystemStatus/SystemStatusView.swift    |   8 +
 .../Views/SystemStatus/SystemStatusViewModel.swift |  34 +++-
 HutchTests/AppStateTests.swift                     |  11 ++
 HutchTests/SystemStatusRepositoryTests.swift       | 147 ++++++++++++++++++
 HutchTests/SystemStatusServiceTests.swift          |  63 ++++++++
 15 files changed, 790 insertions(+), 101 deletions(-)

diff --git a/Hutch.xcodeproj/project.pbxproj b/Hutch.xcodeproj/project.pbxproj
index 2846239..ed3e455 100644
--- a/Hutch.xcodeproj/project.pbxproj
+++ b/Hutch.xcodeproj/project.pbxproj
@@ -515,7 +515,7 @@
 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
 				CODE_SIGN_ENTITLEMENTS = Hutch/Hutch.entitlements;
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 29;
+				CURRENT_PROJECT_VERSION = 30;
 				DEVELOPMENT_TEAM = ZCNAX3VL9D;
 				ENABLE_PREVIEWS = YES;
 				GENERATE_INFOPLIST_FILE = YES;
@@ -532,7 +532,7 @@
 					"$(inherited)",
 					"@executable_path/Frameworks",
 				);
-				MARKETING_VERSION = 2.13.0;
+				MARKETING_VERSION = 2.13.1;
 				PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				STRING_CATALOG_GENERATE_SYMBOLS = YES;
@@ -552,7 +552,7 @@
 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
 				CODE_SIGN_ENTITLEMENTS = Hutch/Hutch.entitlements;
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 29;
+				CURRENT_PROJECT_VERSION = 30;
 				DEVELOPMENT_TEAM = ZCNAX3VL9D;
 				ENABLE_PREVIEWS = YES;
 				GENERATE_INFOPLIST_FILE = YES;
@@ -569,7 +569,7 @@
 					"$(inherited)",
 					"@executable_path/Frameworks",
 				);
-				MARKETING_VERSION = 2.13.0;
+				MARKETING_VERSION = 2.13.1;
 				PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				STRING_CATALOG_GENERATE_SYMBOLS = YES;
@@ -632,7 +632,7 @@
 				APPLICATION_EXTENSION_API_ONLY = YES;
 				CODE_SIGN_ENTITLEMENTS = HutchWidgetExtension/HutchWidgetExtension.entitlements;
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 29;
+				CURRENT_PROJECT_VERSION = 30;
 				DEVELOPMENT_TEAM = ZCNAX3VL9D;
 				GENERATE_INFOPLIST_FILE = NO;
 				INFOPLIST_FILE = HutchWidgetExtension/Info.plist;
@@ -642,7 +642,7 @@
 					"@executable_path/Frameworks",
 					"@executable_path/../../Frameworks",
 				);
-				MARKETING_VERSION = 2.13.0;
+				MARKETING_VERSION = 2.13.1;
 				PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchWidgetExtension;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				SKIP_INSTALL = YES;
@@ -661,7 +661,7 @@
 				APPLICATION_EXTENSION_API_ONLY = YES;
 				CODE_SIGN_ENTITLEMENTS = HutchWidgetExtension/HutchWidgetExtension.entitlements;
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 29;
+				CURRENT_PROJECT_VERSION = 30;
 				DEVELOPMENT_TEAM = ZCNAX3VL9D;
 				GENERATE_INFOPLIST_FILE = NO;
 				INFOPLIST_FILE = HutchWidgetExtension/Info.plist;
@@ -671,7 +671,7 @@
 					"@executable_path/Frameworks",
 					"@executable_path/../../Frameworks",
 				);
-				MARKETING_VERSION = 2.13.0;
+				MARKETING_VERSION = 2.13.1;
 				PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchWidgetExtension;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				SKIP_INSTALL = YES;
diff --git a/Hutch/App/AppStorageKeys.swift b/Hutch/App/AppStorageKeys.swift
index dcf38ba..c1b39c4 100644
--- a/Hutch/App/AppStorageKeys.swift
+++ b/Hutch/App/AppStorageKeys.swift
@@ -6,4 +6,6 @@ enum AppStorageKeys {
     static let wrapRepositoryFileLines = "wrapRepositoryFileLines"
     static let lookupHistory = "lookupHistory"
     static let hutchStatsBaseURL = "hutchStatsBaseURL"
+    static let systemStatusSnapshotCache = "systemStatusSnapshotCache"
+    static let systemStatusIncidentCache = "systemStatusIncidentCache"
 }
diff --git a/Hutch/Networking/SystemStatusCacheStore.swift b/Hutch/Networking/SystemStatusCacheStore.swift
new file mode 100644
index 0000000..63c64b9
--- /dev/null
+++ b/Hutch/Networking/SystemStatusCacheStore.swift
@@ -0,0 +1,59 @@
+import Foundation
+
+actor SystemStatusCacheStore {
+    private let snapshotCacheKey = "systemStatusSnapshotCache"
+    private let incidentCacheKey = "systemStatusIncidentCache"
+    private let defaults: UserDefaults
+
+    init(defaults: UserDefaults = .standard) {
+        self.defaults = defaults
+    }
+
+    func loadSnapshotHTML() -> (html: String, timestamp: Date)? {
+        guard let html = defaults.string(forKey: snapshotCacheKey) else {
+            return nil
+        }
+
+        let timestampValue = defaults.double(forKey: snapshotTimestampKey)
+        guard timestampValue > 0 else {
+            defaults.removeObject(forKey: snapshotCacheKey)
+            defaults.removeObject(forKey: snapshotTimestampKey)
+            return nil
+        }
+
+        return (html, Date(timeIntervalSince1970: timestampValue))
+    }
+
+    func saveSnapshotHTML(_ html: String, timestamp: Date) {
+        defaults.set(html, forKey: snapshotCacheKey)
+        defaults.set(timestamp.timeIntervalSince1970, forKey: snapshotTimestampKey)
+    }
+
+    func loadIncidentFeedData() -> (data: Data, timestamp: Date)? {
+        guard let data = defaults.data(forKey: incidentCacheKey) else {
+            return nil
+        }
+
+        let timestampValue = defaults.double(forKey: incidentTimestampKey)
+        guard timestampValue > 0 else {
+            defaults.removeObject(forKey: incidentCacheKey)
+            defaults.removeObject(forKey: incidentTimestampKey)
+            return nil
+        }
+
+        return (data, Date(timeIntervalSince1970: timestampValue))
+    }
+
+    func saveIncidentFeedData(_ data: Data, timestamp: Date) {
+        defaults.set(data, forKey: incidentCacheKey)
+        defaults.set(timestamp.timeIntervalSince1970, forKey: incidentTimestampKey)
+    }
+
+    private var snapshotTimestampKey: String {
+        "\(snapshotCacheKey).timestamp"
+    }
+
+    private var incidentTimestampKey: String {
+        "\(incidentCacheKey).timestamp"
+    }
+}
diff --git a/Hutch/Networking/SystemStatusRepository.swift b/Hutch/Networking/SystemStatusRepository.swift
index 48c5b16..5f51315 100644
--- a/Hutch/Networking/SystemStatusRepository.swift
+++ b/Hutch/Networking/SystemStatusRepository.swift
@@ -1,57 +1,193 @@
 import Foundation
 
+protocol SystemStatusServing: Sendable {
+    func fetchSnapshotHTML() async throws -> String
+    func fetchIncidentFeedData() async throws -> Data
+}
+
+struct CachedSystemStatusValue<Value: Sendable>: Sendable {
+    let value: Value
+    let lastSuccessfulAt: Date
+    let isStale: Bool
+    let refreshErrorMessage: String?
+}
+
 actor SystemStatusRepository {
-    private let service: SystemStatusService
+    private let service: any SystemStatusServing
     private let ttl: TimeInterval
+    private let cacheStore: SystemStatusCacheStore
+    private let now: @Sendable () -> Date
 
     private var snapshotCache: CacheEntry<SystemStatusSnapshot>?
     private var incidentsCache: CacheEntry<[StatusIncident]>?
+    private var hasLoadedPersistentCache = false
 
-    init(service: SystemStatusService = SystemStatusService(), ttl: TimeInterval = 10 * 60) {
+    init(
+        service: any SystemStatusServing = SystemStatusService(),
+        ttl: TimeInterval = 10 * 60,
+        cacheStore: SystemStatusCacheStore = SystemStatusCacheStore(),
+        now: @escaping @Sendable () -> Date = Date.init
+    ) {
         self.service = service
         self.ttl = ttl
+        self.cacheStore = cacheStore
+        self.now = now
     }
 
     func snapshot(forceRefresh: Bool = false) async throws -> SystemStatusSnapshot {
-        if let cached = snapshotCache, !forceRefresh, !cached.isExpired(ttl: ttl) {
-            return cached.value
+        try await snapshotResult(forceRefresh: forceRefresh).value
+    }
+
+    func recentIncidents(forceRefresh: Bool = false) async throws -> [StatusIncident] {
+        try await recentIncidentsResult(forceRefresh: forceRefresh).value
+    }
+
+    func snapshotResult(forceRefresh: Bool = false) async throws -> CachedSystemStatusValue<SystemStatusSnapshot> {
+        await loadPersistentCacheIfNeeded()
+
+        if let cached = snapshotCache, !forceRefresh, !cached.isExpired(ttl: ttl, now: now) {
+            return CachedSystemStatusValue(
+                value: cached.value,
+                lastSuccessfulAt: cached.timestamp,
+                isStale: false,
+                refreshErrorMessage: nil
+            )
         }
 
         do {
-            let snapshot = try await service.fetchSnapshot()
-            snapshotCache = CacheEntry(value: snapshot, timestamp: Date())
-            return snapshot
+            let html = try await service.fetchSnapshotHTML()
+            let snapshot = try SystemStatusService.parseSnapshotHTML(html, fetchedAt: now())
+            let entry = CacheEntry(value: snapshot, timestamp: now())
+            snapshotCache = entry
+            await cacheStore.saveSnapshotHTML(html, timestamp: entry.timestamp)
+            return CachedSystemStatusValue(
+                value: snapshot,
+                lastSuccessfulAt: entry.timestamp,
+                isStale: false,
+                refreshErrorMessage: nil
+            )
         } catch {
             if let cached = snapshotCache {
-                return cached.value
+                return CachedSystemStatusValue(
+                    value: cached.value,
+                    lastSuccessfulAt: cached.timestamp,
+                    isStale: true,
+                    refreshErrorMessage: refreshErrorMessage(from: error)
+                )
             }
             throw error
         }
     }
 
-    func recentIncidents(forceRefresh: Bool = false) async throws -> [StatusIncident] {
-        if let cached = incidentsCache, !forceRefresh, !cached.isExpired(ttl: ttl) {
-            return cached.value
+    func recentIncidentsResult(forceRefresh: Bool = false) async throws -> CachedSystemStatusValue<[StatusIncident]> {
+        await loadPersistentCacheIfNeeded()
+
+        if let cached = incidentsCache, !forceRefresh, !cached.isExpired(ttl: ttl, now: now) {
+            return CachedSystemStatusValue(
+                value: cached.value,
+                lastSuccessfulAt: cached.timestamp,
+                isStale: false,
+                refreshErrorMessage: nil
+            )
         }
 
         do {
-            let incidents = try await service.fetchIncidentFeed()
-            incidentsCache = CacheEntry(value: incidents, timestamp: Date())
-            return incidents
+            let feedData = try await service.fetchIncidentFeedData()
+            let incidents = try await SystemStatusService.parseIncidentFeedXML(feedData)
+            let entry = CacheEntry(value: incidents, timestamp: now())
+            incidentsCache = entry
+            await cacheStore.saveIncidentFeedData(feedData, timestamp: entry.timestamp)
+            return CachedSystemStatusValue(
+                value: incidents,
+                lastSuccessfulAt: entry.timestamp,
+                isStale: false,
+                refreshErrorMessage: nil
+            )
         } catch {
             if let cached = incidentsCache {
-                return cached.value
+                return CachedSystemStatusValue(
+                    value: cached.value,
+                    lastSuccessfulAt: cached.timestamp,
+                    isStale: true,
+                    refreshErrorMessage: refreshErrorMessage(from: error)
+                )
             }
             throw error
         }
     }
+
+    private func loadPersistentCacheIfNeeded() async {
+        guard !hasLoadedPersistentCache else { return }
+        if let persistedSnapshot = await cacheStore.loadSnapshotHTML(),
+           let snapshot = try? SystemStatusService.parseSnapshotHTML(persistedSnapshot.html, fetchedAt: persistedSnapshot.timestamp) {
+            snapshotCache = CacheEntry(value: snapshot, timestamp: persistedSnapshot.timestamp)
+        }
+        if let persistedFeed = await cacheStore.loadIncidentFeedData(),
+           let incidents = try? await SystemStatusService.parseIncidentFeedXML(persistedFeed.data) {
+            incidentsCache = CacheEntry(value: incidents, timestamp: persistedFeed.timestamp)
+        }
+        hasLoadedPersistentCache = true
+    }
+
+    private func refreshErrorMessage(from error: any Error) -> String {
+        if let error = error as? SRHTError {
+            switch error {
+            case .graphQLErrors(let errors):
+                let firstMessage = errors.first?.message.lowercased() ?? ""
+                if firstMessage.contains("unauthorized") || firstMessage.contains("forbidden") {
+                    return "You do not have permission to do that."
+                }
+                if firstMessage.contains("not found") || firstMessage.contains("no rows in result set") {
+                    return "That content is no longer available."
+                }
+                return "Something went wrong. Please try again."
+            case .httpError(let code):
+                if code == 401 {
+                    return "Please sign in again."
+                }
+                if code == 403 {
+                    return "You do not have permission to do that."
+                }
+                if code == 404 {
+                    return "That content is no longer available."
+                }
+                if (500...599).contains(code) {
+                    return "The server is unavailable right now. Please try again."
+                }
+                return "Something went wrong. Please try again."
+            case .invalidAuthenticatedURL:
+                return "That request could not be completed."
+            case .decodingError:
+                return "The response could not be loaded right now."
+            case .networkError(let underlyingError):
+                return refreshErrorMessage(from: underlyingError)
+            case .unauthorized:
+                return "Please sign in again."
+            }
+        }
+
+        let nsError = error as NSError
+        switch nsError.code {
+        case NSURLErrorNotConnectedToInternet,
+             NSURLErrorNetworkConnectionLost,
+             NSURLErrorTimedOut,
+             NSURLErrorCannotFindHost,
+             NSURLErrorCannotConnectToHost,
+             NSURLErrorDNSLookupFailed,
+             NSURLErrorInternationalRoamingOff,
+             NSURLErrorDataNotAllowed:
+            return "Check your connection and try again."
+        default:
+            return "Something went wrong. Please try again."
+        }
+    }
 }
 
-private struct CacheEntry<Value: Sendable>: Sendable {
+struct CacheEntry<Value: Sendable>: Sendable {
     let value: Value
     let timestamp: Date
 
-    nonisolated func isExpired(ttl: TimeInterval) -> Bool {
-        Date().timeIntervalSince(timestamp) > ttl
+    nonisolated func isExpired(ttl: TimeInterval, now: @escaping @Sendable () -> Date = Date.init) -> Bool {
+        now().timeIntervalSince(timestamp) > ttl
     }
 }
diff --git a/Hutch/Networking/SystemStatusService.swift b/Hutch/Networking/SystemStatusService.swift
index fd38fd3..5dd27b5 100644
--- a/Hutch/Networking/SystemStatusService.swift
+++ b/Hutch/Networking/SystemStatusService.swift
@@ -13,15 +13,23 @@ struct SystemStatusService: Sendable {
     }
 
     func fetchSnapshot() async throws -> SystemStatusSnapshot {
-        let html = try await fetchText(from: Self.statusURL, accept: "text/html,application/xhtml+xml")
+        let html = try await fetchSnapshotHTML()
         return try Self.parseSnapshotHTML(html, fetchedAt: now())
     }
 
     func fetchIncidentFeed() async throws -> [StatusIncident] {
-        let data = try await fetchData(from: Self.feedURL, accept: "application/rss+xml,application/xml,text/xml")
+        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 {
@@ -62,6 +70,8 @@ struct SystemStatusService: Sendable {
     }
 }
 
+extension SystemStatusService: SystemStatusServing {}
+
 extension SystemStatusService {
     nonisolated static func parseSnapshotHTML(_ html: String, fetchedAt: Date) throws -> SystemStatusSnapshot {
         let services = parseServices(in: html)
@@ -96,7 +106,7 @@ extension SystemStatusService {
     nonisolated private static func parseServices(in html: String) -> [StatusServiceState] {
         firstMatches(
             in: html,
-            pattern: #"<div class="component" data-status="([^"]+)">([\s\S]*?)</div>"#
+            pattern: #"<div\b(?=[^>]*\bclass\s*=\s*["'][^"']*\bcomponent\b[^"']*["'])(?=[^>]*\bdata-status\s*=\s*["']([^"']+)["'])[^>]*>([\s\S]*?)</div>"#
         ).compactMap { captures in
             guard captures.count >= 2 else { return nil }
 
@@ -104,16 +114,18 @@ extension SystemStatusService {
             let content = captures[1]
             guard let linkCaptures = firstMatches(
                 in: content,
-                pattern: #"<a[^>]*href="([^"]+)"[^>]*>\s*(.*?)\s*</a>"#
+                pattern: #"<a\b[^>]*\bhref\s*=\s*["']([^"']+)["'][^>]*>([\s\S]*?)</a>"#
             ).first,
-                  linkCaptures.count >= 2,
-                  let statusText = firstMatch(in: content, pattern: #"<span class="component-status">\s*(.*?)\s*</span>"#) else {
+                  linkCaptures.count >= 2 else {
                 return nil
             }
 
             let href = linkCaptures[0]
             let cleanedName = cleanText(linkCaptures[1])
-            let readableStatus = cleanText(statusText)
+            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(
@@ -129,19 +141,19 @@ extension SystemStatusService {
     nonisolated private static func parseHTMLIncidentCards(in html: String) -> [StatusIncident] {
         firstMatches(
             in: html,
-            pattern: #"<a href="([^"]+)" class="issue no-underline">([\s\S]*?)</a>"#
+            pattern: #"<a\b(?=[^>]*\bclass\s*=\s*["'][^"']*\bissue\b[^"']*["'])(?=[^>]*\bhref\s*=\s*["']([^"']+)["'])[^>]*>([\s\S]*?)</a>"#
         ).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: #"<h3>\s*([\s\S]*?)\s*</h3>"#),
-                  let titleAttribute = firstMatch(in: content, pattern: #"<small class="date[^"]*" title="([^"]+)">"#),
-                  let publishedAt = htmlIssueDateFormatter.date(from: cleanText(titleAttribute)) else {
+            guard let titleHTML = firstMatch(in: content, pattern: #"<h[1-6][^>]*>\s*([\s\S]*?)\s*</h[1-6]>"#)
+                    ?? firstMatch(in: content, pattern: #"<strong[^>]*>\s*([\s\S]*?)\s*</strong>"#)
+                    ?? firstMatch(in: content, pattern: #"<span[^>]*>\s*([\s\S]*?)\s*</span>"#),
+                  let publishedAt = publishedIncidentDate(in: content) else {
                 return nil
             }
 
             let url = URL(string: href, relativeTo: statusURL)?.absoluteURL
-            let isActive = content.localizedCaseInsensitiveContains("This issue is not resolved yet")
             return StatusIncident(
                 id: url?.absoluteString ?? cleanText(titleHTML),
                 title: cleanText(titleHTML),
@@ -149,7 +161,7 @@ extension SystemStatusService {
                 url: url,
                 publishedAt: publishedAt,
                 updatedAt: nil,
-                isActive: isActive
+                isActive: isActiveIncidentCard(content)
             )
         }
     }
@@ -157,12 +169,12 @@ extension SystemStatusService {
     nonisolated private static func parseActiveIncidentSummaries(in html: String) -> [String: String] {
         firstMatches(
             in: html,
-            pattern: #"<div class="announcement-box"[\s\S]*?<div class="padding">([\s\S]*?)</div>\s*<hr class="clean announcement-box">"#
+            pattern: #"<div\b(?=[^>]*\bclass\s*=\s*["'][^"']*\bannouncement-box\b[^"']*["'])[^>]*>([\s\S]*?)</div>\s*(?:<hr\b[^>]*\bannouncement-box\b[^>]*>)?"#
         ).reduce(into: [:]) { partialResult, captures in
             guard let content = captures.first,
                   let titleLinkCaptures = firstMatches(
                     in: content,
-                    pattern: #"<a href="([^"]+)"><strong class="bold">([\s\S]*?)</strong></a>"#
+                    pattern: #"<a\b[^>]*\bhref\s*=\s*["']([^"']+)["'][^>]*>([\s\S]*?)</a>"#
                   ).first,
                   let href = titleLinkCaptures.first else {
                 return
@@ -197,17 +209,22 @@ extension SystemStatusService {
     }
 
     nonisolated private static func statusLevel(fromLabel label: String) -> StatusLevel {
-        switch label.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() {
-        case "operational":
-            .operational
-        case "disrupted", "degraded":
-            .degraded
-        case "down", "major outage":
-            .majorOutage
-        case "maintenance":
-            .maintenance
+        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:
-            .unknown
+            StatusLevel.unknown
         }
     }
 
@@ -252,6 +269,47 @@ extension SystemStatusService {
             .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: #"<time\b[^>]*\bdatetime\s*=\s*["']([^"']+)["'][^>]*>"#),
+           let parsed = parseISO8601Date(cleanText(timeValue)) {
+            return parsed
+        }
+
+        if let titleAttribute = firstMatch(
+            in: content,
+            pattern: #"<(?:small|time)\b(?=[^>]*\bclass\s*=\s*["'][^"']*\bdate\b[^"']*["'])[^>]*\btitle\s*=\s*["']([^"']+)["'][^>]*>"#
+        ) ?? firstMatch(in: content, pattern: #"\btitle\s*=\s*["']([^"']+UTC)["']"#) {
+            return htmlIssueDateFormatter.date(from: cleanText(titleAttribute))
+        }
+
+        return nil
+    }
+
+    nonisolated private static func isActiveIncidentCard(_ content: String) -> Bool {
+        let normalizedContent = cleanText(content).lowercased()
+        return normalizedContent.contains("not resolved yet")
+            || normalizedContent.contains("ongoing")
+            || normalizedContent.contains("investigating")
+    }
+
+    nonisolated fileprivate static func parseISO8601Date(_ value: String) -> Date? {
+        let formatter = ISO8601DateFormatter()
+        formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
+        return formatter.date(from: value) ?? {
+            let fallbackFormatter = ISO8601DateFormatter()
+            fallbackFormatter.formatOptions = [.withInternetDateTime]
+            return fallbackFormatter.date(from: value)
+        }()
+    }
+
     nonisolated private static let htmlIssueDateFormatter: DateFormatter = {
         let formatter = DateFormatter()
         formatter.locale = Locale(identifier: "en_US_POSIX")
@@ -259,9 +317,9 @@ extension SystemStatusService {
         formatter.dateFormat = "MMM d HH:mm:ss yyyy zzz"
         return formatter
     }()
+
 }
 
-@MainActor
 private final class SystemStatusFeedParser: NSObject, XMLParserDelegate, @unchecked Sendable {
     private var incidents: [StatusIncident] = []
     private var currentItem: FeedItem?
@@ -315,7 +373,7 @@ private final class SystemStatusFeedParser: NSObject, XMLParserDelegate, @unchec
             currentItem.guid = value
         case "description":
             currentItem.description = value
-        case "pubDate":
+        case "pubDate", "dc:date":
             currentItem.pubDate = value
         case "category":
             currentItem.category = value
@@ -345,7 +403,7 @@ private final class SystemStatusFeedParser: NSObject, XMLParserDelegate, @unchec
         func makeIncident() -> StatusIncident? {
             let cleanedTitle = title.replacingOccurrences(of: "[Resolved] ", with: "")
             guard !cleanedTitle.isEmpty,
-                  let publishedAt = SystemStatusFeedParser.pubDateFormatter.date(from: pubDate) else {
+                  let publishedAt = SystemStatusFeedParser.parsePubDate(pubDate) else {
                 return nil
             }
 
@@ -382,6 +440,10 @@ private final class SystemStatusFeedParser: NSObject, XMLParserDelegate, @unchec
         return decodeHTMLEntities(stripped)
     }
 
+    nonisolated private static func parsePubDate(_ value: String) -> Date? {
+        pubDateFormatter.date(from: value) ?? SystemStatusService.parseISO8601Date(value)
+    }
+
     nonisolated private static let pubDateFormatter: DateFormatter = {
         let formatter = DateFormatter()
         formatter.locale = Locale(identifier: "en_US_POSIX")
diff --git a/Hutch/Views/Home/HomeView.swift b/Hutch/Views/Home/HomeView.swift
index ab0cdf8..632e804 100644
--- a/Hutch/Views/Home/HomeView.swift
+++ b/Hutch/Views/Home/HomeView.swift
@@ -78,19 +78,25 @@ struct HomeView: View {
         .refreshable {
             await viewModel.loadDashboard()
         }
+        .connectivityOverlay(hasContent: viewModel.hasDashboardContent) {
+            await viewModel.loadDashboard()
+        }
     }
 
     @ViewBuilder
     private func systemStatusBannerSection(_ viewModel: HomeViewModel) -> some View {
-        if let bannerTitle = viewModel.systemStatusBannerTitle {
-            Section {
-                NavigationLink {
-                    SystemStatusView()
-                } label: {
-                    HomeSystemStatusBanner(title: bannerTitle)
-                }
-                .buttonStyle(.plain)
+        Section {
+            NavigationLink {
+                SystemStatusView()
+            } label: {
+                SystemStatusSummaryRow(
+                    snapshot: viewModel.systemStatusSnapshot,
+                    isLoading: viewModel.isLoadingSystemStatus,
+                    errorMessage: viewModel.systemStatusErrorMessage,
+                    isShowingStaleData: viewModel.isShowingStaleSystemStatus
+                )
             }
+            .buttonStyle(.plain)
         }
     }
 
@@ -252,32 +258,6 @@ private struct HomeInboxToolbarIcon: View {
     }
 }
 
-private struct HomeSystemStatusBanner: View {
-    let title: String
-
-    var body: some View {
-        HStack(spacing: 12) {
-            Image(systemName: "exclamationmark.triangle.fill")
-                .foregroundStyle(.orange)
-            VStack(alignment: .leading, spacing: 2) {
-                Text("SourceHut service disruption")
-                    .font(.subheadline.weight(.semibold))
-                    .foregroundStyle(.primary)
-                Text(title)
-                    .font(.caption)
-                    .foregroundStyle(.secondary)
-                    .lineLimit(1)
-            }
-            Spacer()
-            Image(systemName: "chevron.right")
-                .font(.caption.weight(.semibold))
-                .foregroundStyle(.tertiary)
-        }
-        .padding(.vertical, 4)
-        .contentShape(Rectangle())
-    }
-}
-
 private struct HomeProjectRow: View {
     let project: Project
 
diff --git a/Hutch/Views/Home/HomeViewModel.swift b/Hutch/Views/Home/HomeViewModel.swift
index a06df20..ba00a36 100644
--- a/Hutch/Views/Home/HomeViewModel.swift
+++ b/Hutch/Views/Home/HomeViewModel.swift
@@ -144,6 +144,9 @@ final class HomeViewModel {
     var assignedTickets: [HomeAssignedTicket] = []
     var recentBuilds: [HomeBuildItem] = []
     private(set) var systemStatusSnapshot: SystemStatusSnapshot?
+    private(set) var isLoadingSystemStatus = false
+    private(set) var isShowingStaleSystemStatus = false
+    private(set) var systemStatusErrorMessage: String?
     private(set) var hasUnreadInboxThreads = false
     private(set) var unreadInboxThreadCount: Int?
     private(set) var isLoadingProjects = false
@@ -279,8 +282,11 @@ final class HomeViewModel {
         isLoadingProjects = true
         isLoadingAssignedTickets = true
         isLoadingRecentBuilds = true
+        isLoadingSystemStatus = true
         assignedTicketsError = nil
         recentBuildsError = nil
+        isShowingStaleSystemStatus = false
+        systemStatusErrorMessage = nil
 
         async let projectsTask = loadProjects()
         async let jobsTask = loadRecentJobs()
@@ -324,13 +330,21 @@ final class HomeViewModel {
 
         unreadInboxThreadCount = await inboxUnreadTask
         hasUnreadInboxThreads = (unreadInboxThreadCount ?? 0) > 0
-        systemStatusSnapshot = await systemStatusTask
+        let systemStatusResult = await systemStatusTask
+        switch systemStatusResult {
+        case .success(let result):
+            systemStatusSnapshot = result.value
+            isShowingStaleSystemStatus = result.isStale
+            systemStatusErrorMessage = result.isStale ? result.refreshErrorMessage : nil
+        case .failure(let error):
+            systemStatusErrorMessage = error.userFacingMessage
+        }
+        isLoadingSystemStatus = false
         persistNeedsAttentionSnapshot()
     }
 
-    var systemStatusBannerTitle: String? {
-        guard let systemStatusSnapshot, systemStatusSnapshot.hasDisruption else { return nil }
-        return systemStatusSnapshot.bannerSummary
+    var hasDashboardContent: Bool {
+        !projects.isEmpty || !assignedTickets.isEmpty || !recentBuilds.isEmpty || systemStatusSnapshot != nil
     }
 
     func resolveTicket(_ ticket: HomeAssignedTicket) async {
@@ -430,11 +444,11 @@ final class HomeViewModel {
         }
     }
 
-    private func loadSystemStatusSnapshot() async -> SystemStatusSnapshot? {
+    private func loadSystemStatusSnapshot() async -> Result<CachedSystemStatusValue<SystemStatusSnapshot>, Error> {
         do {
-            return try await systemStatusRepository.snapshot()
+            return .success(try await systemStatusRepository.snapshotResult())
         } catch {
-            return systemStatusSnapshot
+            return .failure(error)
         }
     }
 
diff --git a/Hutch/Views/More/MoreView.swift b/Hutch/Views/More/MoreView.swift
index 284b1ad..6325954 100644
--- a/Hutch/Views/More/MoreView.swift
+++ b/Hutch/Views/More/MoreView.swift
@@ -7,6 +7,7 @@ struct MoreView: View {
         ("chat.sr.ht", SRHTWebURL.chat)
     ]
 
+    @State private var viewModel: MoreViewModel?
     @State private var showAccountSwitcher = false
 
     var body: some View {
@@ -29,9 +30,14 @@ struct MoreView: View {
                 NavigationLink(value: MoreRoute.pastes) {
                     Label("Pastes", systemImage: "doc.on.clipboard")
                 }
-                
+
                 NavigationLink(value: MoreRoute.systemStatus) {
-                    Label("System Status", systemImage: "server.rack")
+                    SystemStatusSummaryRow(
+                        snapshot: viewModel?.systemStatusSnapshot,
+                        isLoading: viewModel?.isLoadingSystemStatus ?? true,
+                        errorMessage: viewModel?.systemStatusErrorMessage,
+                        isShowingStaleData: viewModel?.isShowingStaleSystemStatus ?? false
+                    )
                 }
             }
 
@@ -58,6 +64,12 @@ struct MoreView: View {
             }
         }
         .navigationTitle("More")
+        .refreshable {
+            await ensureViewModel().loadSystemStatus(forceRefresh: true)
+        }
+        .task {
+            await ensureViewModel().loadIfNeeded()
+        }
         .toolbar {
             ToolbarItem(placement: .topBarTrailing) {
                 Button {
@@ -71,4 +83,15 @@ struct MoreView: View {
             AccountSwitcherView()
         }
     }
+
+    @MainActor
+    private func ensureViewModel() -> MoreViewModel {
+        if let viewModel {
+            return viewModel
+        }
+
+        let newViewModel = MoreViewModel(repository: appState.systemStatusRepository)
+        viewModel = newViewModel
+        return newViewModel
+    }
 }
diff --git a/Hutch/Views/More/MoreViewModel.swift b/Hutch/Views/More/MoreViewModel.swift
new file mode 100644
index 0000000..8f23975
--- /dev/null
+++ b/Hutch/Views/More/MoreViewModel.swift
@@ -0,0 +1,39 @@
+import Foundation
+
+@Observable
+@MainActor
+final class MoreViewModel {
+    private let repository: SystemStatusRepository
+
+    private(set) var systemStatusSnapshot: SystemStatusSnapshot?
+    private(set) var isLoadingSystemStatus = false
+    private(set) var isShowingStaleSystemStatus = false
+    private(set) var systemStatusErrorMessage: String?
+
+    init(repository: SystemStatusRepository) {
+        self.repository = repository
+    }
+
+    func loadIfNeeded() async {
+        guard systemStatusSnapshot == nil, !isLoadingSystemStatus else { return }
+        await loadSystemStatus()
+    }
+
+    func loadSystemStatus(forceRefresh: Bool = false) async {
+        isLoadingSystemStatus = true
+        defer { isLoadingSystemStatus = false }
+        isShowingStaleSystemStatus = false
+        systemStatusErrorMessage = nil
+
+        do {
+            let result = try await repository.snapshotResult(forceRefresh: forceRefresh)
+            systemStatusSnapshot = result.value
+            isShowingStaleSystemStatus = result.isStale
+            systemStatusErrorMessage = result.isStale ? result.refreshErrorMessage : nil
+        } catch {
+            if systemStatusSnapshot == nil {
+                systemStatusErrorMessage = error.userFacingMessage
+            }
+        }
+    }
+}
diff --git a/Hutch/Views/SystemStatus/SystemStatusSummaryRow.swift b/Hutch/Views/SystemStatus/SystemStatusSummaryRow.swift
new file mode 100644
index 0000000..5d94dad
--- /dev/null
+++ b/Hutch/Views/SystemStatus/SystemStatusSummaryRow.swift
@@ -0,0 +1,119 @@
+import SwiftUI
+
+struct SystemStatusSummaryRow: View {
+    let title: String
+    let snapshot: SystemStatusSnapshot?
+    let isLoading: Bool
+    let errorMessage: String?
+    let isShowingStaleData: Bool
+
+    init(
+        title: String = "System Status",
+        snapshot: SystemStatusSnapshot?,
+        isLoading: Bool = false,
+        errorMessage: String? = nil,
+        isShowingStaleData: Bool = false
+    ) {
+        self.title = title
+        self.snapshot = snapshot
+        self.isLoading = isLoading
+        self.errorMessage = errorMessage
+        self.isShowingStaleData = isShowingStaleData
+    }
+
+    var body: some View {
+        HStack(spacing: 12) {
+            icon
+                .frame(width: 20)
+
+            VStack(alignment: .leading, spacing: 3) {
+                Text(title)
+                    .font(.subheadline.weight(.semibold))
+                    .foregroundStyle(.primary)
+
+                Text(primaryMessage)
+                    .font(.caption)
+                    .foregroundStyle(primaryMessageColor)
+                    .lineLimit(2)
+
+                if let metadataMessage {
+                    Text(metadataMessage)
+                        .font(.caption2)
+                        .foregroundStyle(.tertiary)
+                        .lineLimit(1)
+                }
+            }
+
+            Spacer(minLength: 8)
+        }
+        .padding(.vertical, 4)
+        .contentShape(Rectangle())
+    }
+
+    @ViewBuilder
+    private var icon: some View {
+        if isLoading && snapshot == nil {
+            ProgressView()
+                .controlSize(.small)
+        } else {
+            Image(systemName: iconName)
+                .foregroundStyle(iconColor)
+        }
+    }
+
+    private var primaryMessage: String {
+        if let snapshot {
+            return snapshot.hasDisruption ? snapshot.bannerSummary : snapshot.overallStatusText
+        }
+        if let errorMessage, !errorMessage.isEmpty {
+            return errorMessage
+        }
+        if isLoading {
+            return "Loading system status…"
+        }
+        return "System status is unavailable right now."
+    }
+
+    private var metadataMessage: String? {
+        if let snapshot {
+            if isShowingStaleData {
+                return "Updated \(snapshot.lastUpdated.relativeDescription) • Showing saved data"
+            }
+            return "Updated \(snapshot.lastUpdated.relativeDescription)"
+        }
+        if errorMessage != nil {
+            return "Open System Status to retry."
+        }
+        return nil
+    }
+
+    private var iconName: String {
+        if let snapshot {
+            return snapshot.hasDisruption ? "exclamationmark.triangle.fill" : "checkmark.circle.fill"
+        }
+        if errorMessage != nil {
+            return "exclamationmark.triangle"
+        }
+        return "server.rack"
+    }
+
+    private var iconColor: Color {
+        if let snapshot {
+            return snapshot.hasDisruption ? .orange : .green
+        }
+        if errorMessage != nil {
+            return .secondary
+        }
+        return .secondary
+    }
+
+    private var primaryMessageColor: Color {
+        if snapshot != nil {
+            return .secondary
+        }
+        if errorMessage != nil {
+            return .secondary
+        }
+        return .secondary
+    }
+}
diff --git a/Hutch/Views/SystemStatus/SystemStatusView.swift b/Hutch/Views/SystemStatus/SystemStatusView.swift
index 053210e..08a46eb 100644
--- a/Hutch/Views/SystemStatus/SystemStatusView.swift
+++ b/Hutch/Views/SystemStatus/SystemStatusView.swift
@@ -31,6 +31,14 @@ struct SystemStatusView: View {
     @ViewBuilder
     private func content(_ viewModel: SystemStatusViewModel) -> some View {
         List {
+            if viewModel.isShowingStaleData, let staleDataMessage = viewModel.staleDataMessage {
+                Section {
+                    Label(staleDataMessage, systemImage: "clock.arrow.trianglehead.counterclockwise.rotate.90")
+                        .font(.subheadline)
+                        .foregroundStyle(.secondary)
+                }
+            }
+
             if let snapshot = viewModel.snapshot {
                 summarySection(snapshot)
                 servicesSection(snapshot)
diff --git a/Hutch/Views/SystemStatus/SystemStatusViewModel.swift b/Hutch/Views/SystemStatus/SystemStatusViewModel.swift
index 646d7ca..7700208 100644
--- a/Hutch/Views/SystemStatus/SystemStatusViewModel.swift
+++ b/Hutch/Views/SystemStatus/SystemStatusViewModel.swift
@@ -8,6 +8,8 @@ final class SystemStatusViewModel {
     private(set) var snapshot: SystemStatusSnapshot?
     private(set) var recentIncidents: [StatusIncident] = []
     private(set) var isLoading = false
+    private(set) var isShowingStaleData = false
+    private(set) var staleDataMessage: String?
     var errorMessage: String?
 
     init(repository: SystemStatusRepository) {
@@ -25,12 +27,24 @@ final class SystemStatusViewModel {
         defer { isLoading = false }
 
         errorMessage = nil
+        staleDataMessage = nil
+        isShowingStaleData = false
 
-        async let snapshotTask = repository.snapshot(forceRefresh: forceRefresh)
-        async let incidentsTask = repository.recentIncidents(forceRefresh: forceRefresh)
+        async let snapshotTask = repository.snapshotResult(forceRefresh: forceRefresh)
+        async let incidentsTask = repository.recentIncidentsResult(forceRefresh: forceRefresh)
+
+        var refreshWarnings: [String] = []
 
         do {
-            snapshot = try await snapshotTask
+            let result = try await snapshotTask
+            snapshot = result.value
+            if result.isStale {
+                isShowingStaleData = true
+                staleDataMessage = "Showing the last saved system status snapshot."
+                if let warning = result.refreshErrorMessage {
+                    refreshWarnings.append(warning)
+                }
+            }
         } catch {
             if snapshot == nil {
                 errorMessage = error.userFacingMessage
@@ -38,11 +52,23 @@ final class SystemStatusViewModel {
         }
 
         do {
-            recentIncidents = try await incidentsTask
+            let result = try await incidentsTask
+            recentIncidents = result.value
+            if result.isStale {
+                isShowingStaleData = true
+                staleDataMessage = staleDataMessage ?? "Showing the last saved incident history."
+                if let warning = result.refreshErrorMessage {
+                    refreshWarnings.append(warning)
+                }
+            }
         } catch {
             if errorMessage == nil && recentIncidents.isEmpty {
                 errorMessage = error.userFacingMessage
             }
         }
+
+        if hasContent, let firstWarning = refreshWarnings.first {
+            errorMessage = "Showing cached system status. \(firstWarning)"
+        }
     }
 }
diff --git a/HutchTests/AppStateTests.swift b/HutchTests/AppStateTests.swift
index ca34df2..9cf4e13 100644
--- a/HutchTests/AppStateTests.swift
+++ b/HutchTests/AppStateTests.swift
@@ -22,4 +22,15 @@ struct AppStateTests {
 
         #expect(appState.deepLinkError == "The ticket could not be found or is inaccessible.")
     }
+
+    @Test
+    @MainActor
+    func openSystemStatusSelectsMoreTabAndQueuesNavigation() {
+        let appState = AppState()
+
+        appState.openSystemStatus()
+
+        #expect(appState.selectedTab == .more)
+        #expect(appState.pendingTabNavigation == .systemStatus)
+    }
 }
diff --git a/HutchTests/SystemStatusRepositoryTests.swift b/HutchTests/SystemStatusRepositoryTests.swift
new file mode 100644
index 0000000..fb08732
--- /dev/null
+++ b/HutchTests/SystemStatusRepositoryTests.swift
@@ -0,0 +1,147 @@
+import Foundation
+import Testing
+@testable import Hutch
+
+struct SystemStatusRepositoryTests {
+
+    @Test
+    func fallsBackToPersistedSnapshotWhenRefreshFails() async throws {
+        let defaultsName = "SystemStatusRepositoryTests-\(UUID().uuidString)"
+        let defaults = try #require(UserDefaults(suiteName: defaultsName))
+        defaults.removePersistentDomain(forName: defaultsName)
+        defer { defaults.removePersistentDomain(forName: defaultsName) }
+
+        let cachedSnapshot = SystemStatusSnapshot(
+            services: [
+                StatusServiceState(id: "git.sr.ht", name: "git.sr.ht", slug: "git.sr.ht", status: .degraded, description: nil)
+            ],
+            activeIncidents: [],
+            lastUpdated: Date(timeIntervalSince1970: 120)
+        )
+        let cacheStore = SystemStatusCacheStore(defaults: defaults)
+        let initialRepository = SystemStatusRepository(
+            service: TestSystemStatusService(
+                snapshotHTMLHandler: { Self.cachedSnapshotHTML },
+                incidentsDataHandler: { Data(Self.emptyRSS.utf8) }
+            ),
+            cacheStore: cacheStore,
+            now: { Date(timeIntervalSince1970: 120) }
+        )
+
+        _ = try await initialRepository.snapshotResult(forceRefresh: true)
+
+        let fallbackRepository = SystemStatusRepository(
+            service: TestSystemStatusService(
+                snapshotHTMLHandler: { throw SRHTError.httpError(503) },
+                incidentsDataHandler: { Data(Self.emptyRSS.utf8) }
+            ),
+            ttl: 0,
+            cacheStore: cacheStore,
+            now: { Date(timeIntervalSince1970: 180) }
+        )
+
+        let result = try await fallbackRepository.snapshotResult(forceRefresh: true)
+
+        #expect(result.value == cachedSnapshot)
+        #expect(result.isStale)
+        #expect(result.lastSuccessfulAt == Date(timeIntervalSince1970: 120))
+        #expect(result.refreshErrorMessage != nil)
+    }
+
+    @Test
+    func fallsBackToPersistedIncidentsWhenRefreshFails() async throws {
+        let defaultsName = "SystemStatusRepositoryTests-\(UUID().uuidString)"
+        let defaults = try #require(UserDefaults(suiteName: defaultsName))
+        defaults.removePersistentDomain(forName: defaultsName)
+        defer { defaults.removePersistentDomain(forName: defaultsName) }
+
+        let cachedIncidents = [
+            StatusIncident(
+                id: "incident-1",
+                title: "builds.sr.ht outage",
+                summary: "Builds are failing.",
+                url: nil,
+                publishedAt: Date(timeIntervalSince1970: 200),
+                updatedAt: nil,
+                isActive: true
+            )
+        ]
+        let cacheStore = SystemStatusCacheStore(defaults: defaults)
+        let initialRepository = SystemStatusRepository(
+            service: TestSystemStatusService(
+                snapshotHTMLHandler: { Self.cachedOperationalHTML },
+                incidentsDataHandler: { Data(Self.cachedIncidentRSS.utf8) }
+            ),
+            cacheStore: cacheStore,
+            now: { Date(timeIntervalSince1970: 220) }
+        )
+
+        _ = try await initialRepository.recentIncidentsResult(forceRefresh: true)
+
+        let fallbackRepository = SystemStatusRepository(
+            service: TestSystemStatusService(
+                snapshotHTMLHandler: { Self.cachedOperationalHTML },
+                incidentsDataHandler: { throw SRHTError.httpError(504) }
+            ),
+            ttl: 0,
+            cacheStore: cacheStore,
+            now: { Date(timeIntervalSince1970: 260) }
+        )
+
+        let result = try await fallbackRepository.recentIncidentsResult(forceRefresh: true)
+
+        #expect(result.value == cachedIncidents)
+        #expect(result.isStale)
+        #expect(result.lastSuccessfulAt == Date(timeIntervalSince1970: 220))
+        #expect(result.refreshErrorMessage != nil)
+    }
+}
+
+private struct TestSystemStatusService: SystemStatusServing {
+    let snapshotHTMLHandler: @Sendable () async throws -> String
+    let incidentsDataHandler: @Sendable () async throws -> Data
+
+    func fetchSnapshotHTML() async throws -> String {
+        try await snapshotHTMLHandler()
+    }
+
+    func fetchIncidentFeedData() async throws -> Data {
+        try await incidentsDataHandler()
+    }
+}
+
+private extension SystemStatusRepositoryTests {
+    static let cachedSnapshotHTML = #"""
+    <div class="component" data-status="disrupted">
+      <a href="/affected/git.sr.ht/">git.sr.ht</a>
+      <span class="component-status">Disrupted</span>
+    </div>
+    """#
+
+    static let cachedOperationalHTML = #"""
+    <div class="component" data-status="ok">
+      <a href="/affected/meta.sr.ht/">meta.sr.ht</a>
+      <span class="component-status">Operational</span>
+    </div>
+    """#
+
+    static let cachedIncidentRSS = #"""
+    <rss version="2.0">
+      <channel>
+        <item>
+          <title>builds.sr.ht outage</title>
+          <link>https://status.sr.ht/issues/1/</link>
+          <pubDate>Thu, 01 Jan 1970 00:03:20 +0000</pubDate>
+          <guid>incident-1</guid>
+          <description>&lt;p&gt;Builds are failing.&lt;/p&gt;</description>
+        </item>
+      </channel>
+    </rss>
+    """#
+
+    static let emptyRSS = #"""
+    <rss version="2.0">
+      <channel></channel>
+    </rss>
+    """#
+}
diff --git a/HutchTests/SystemStatusServiceTests.swift b/HutchTests/SystemStatusServiceTests.swift
index 58ace33..d1577b6 100644
--- a/HutchTests/SystemStatusServiceTests.swift
+++ b/HutchTests/SystemStatusServiceTests.swift
@@ -19,6 +19,18 @@ struct SystemStatusServiceTests {
         #expect(snapshot.activeIncidents[0].url?.absoluteString == "https://status.sr.ht/issues/2026-04-06-ddos-attack/")
     }
 
+    @Test
+    func parsesStatusHTMLWithClassOrderChangesAndTimeElements() throws {
+        let snapshot = try SystemStatusService.parseSnapshotHTML(Self.variantHTML, fetchedAt: .now)
+
+        #expect(snapshot.services.count == 2)
+        #expect(snapshot.services[0].status == .operational)
+        #expect(snapshot.services[1].status == .majorOutage)
+        #expect(snapshot.activeIncidents.count == 1)
+        #expect(snapshot.activeIncidents[0].title == "builds.sr.ht outage")
+        #expect(snapshot.activeIncidents[0].publishedAt == ISO8601DateFormatter().date(from: "2026-04-07T12:00:00Z"))
+    }
+
     @Test
     func parsesIncidentFeedRSS() async throws {
         let incidents = try await SystemStatusService.parseIncidentFeedXML(Data(Self.sampleRSS.utf8))
@@ -32,6 +44,15 @@ struct SystemStatusServiceTests {
         #expect(incidents[1].updatedAt != nil)
     }
 
+    @Test
+    func parsesIncidentFeedWithISO8601Dates() async throws {
+        let incidents = try await SystemStatusService.parseIncidentFeedXML(Data(Self.variantRSS.utf8))
+
+        #expect(incidents.count == 1)
+        #expect(incidents[0].title == "Status feed moved")
+        #expect(incidents[0].publishedAt == ISO8601DateFormatter().date(from: "2026-04-07T15:30:00Z"))
+    }
+
     @Test
     func bannerSummaryPrefersSpecificServiceThenCount() {
         let operational = SystemStatusSnapshot(
@@ -102,6 +123,32 @@ struct SystemStatusServiceTests {
     </html>
     """#
 
+    private static let variantHTML = #"""
+    <html>
+    <body>
+      <div class="component extra" data-status="ok">
+        <a class="no-underline" href="/affected/meta.sr.ht/">meta.sr.ht</a>
+        <small class="component-status secondary">All systems operational</small>
+      </div>
+      <div data-status="down" class="extra component">
+        <a href="/affected/builds.sr.ht/" class="link">builds.sr.ht</a>
+        <div class="component-status badge">Outage</div>
+      </div>
+      <div class="announcement-box">
+        <div class="padding">
+          <p><a href="/issues/2026-04-07-builds-outage/"><strong>builds.sr.ht outage</strong></a></p>
+          <p><strong>Build jobs are currently failing.</strong></p>
+        </div>
+      </div>
+      <a class="issue no-underline urgent" href="/issues/2026-04-07-builds-outage/">
+        <time class="date" datetime="2026-04-07T12:00:00Z">Apr 7</time>
+        <h4>builds.sr.ht outage</h4>
+        <span>Investigating elevated failures</span>
+      </a>
+    </body>
+    </html>
+    """#
+
     private static let sampleRSS = #"""
     <?xml version="1.0" encoding="utf-8" standalone="yes"?>
     <rss version="2.0">
@@ -126,4 +173,20 @@ struct SystemStatusServiceTests {
       </channel>
     </rss>
     """#
+
+    private static let variantRSS = #"""
+    <?xml version="1.0" encoding="utf-8" standalone="yes"?>
+    <rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/">
+      <channel>
+        <title>sr.ht status</title>
+        <item>
+          <title>Status feed moved</title>
+          <link>https://status.sr.ht/issues/2026-04-07-feed-moved/</link>
+          <dc:date>2026-04-07T15:30:00Z</dc:date>
+          <guid>https://status.sr.ht/issues/2026-04-07-feed-moved/</guid>
+          <description>&lt;p&gt;Use the new feed endpoint.&lt;/p&gt;</description>
+        </item>
+      </channel>
+    </rss>
+    """#
 }