Commit 972217316c

972217316ca14474ca11dd9190d0ee68e49aa4f6

parent: 882862f5f3

Unsigned

cmc <hello@cleberg.net> · 2026-07-16 21:59 UTC

Move Home system status into a title-bar status badge

Replace the disruption-only banner on the Home tab with a persistent
circular status indicator in the navigation bar, next to the "Home"
title. The badge reflects the existing status logic — green/check when
operational, orange/exclamation on disruption, gray otherwise — and taps
through to System Status. SystemStatusSummaryRow is unchanged and still
used by the More tab.

Resolve the system-status result first in HomeViewModel.loadDashboard so
the badge settles from cache immediately instead of waiting on the
slower projects/jobs/tickets/inbox loads, which had left it spinning for
several seconds.

Bump MARKETING_VERSION to 3.8.2 and record it in ROADMAP.

Layout: unified · split

Hutch.xcodeproj/project.pbxproj +6 −6
@@ -614,7 +614,7 @@
614614 "$(inherited)",
615615 "@executable_path/Frameworks",
616616 );
617 MARKETING_VERSION = 3.8.1;
617 MARKETING_VERSION = 3.8.2;
618618 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch;
619619 PRODUCT_NAME = "$(TARGET_NAME)";
620620 STRING_CATALOG_GENERATE_SYMBOLS = YES;
@@ -651,7 +651,7 @@
651651 "$(inherited)",
652652 "@executable_path/Frameworks",
653653 );
654 MARKETING_VERSION = 3.8.1;
654 MARKETING_VERSION = 3.8.2;
655655 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch;
656656 PRODUCT_NAME = "$(TARGET_NAME)";
657657 STRING_CATALOG_GENERATE_SYMBOLS = YES;
@@ -724,7 +724,7 @@
724724 "@executable_path/Frameworks",
725725 "@executable_path/../../Frameworks",
726726 );
727 MARKETING_VERSION = 3.8.1;
727 MARKETING_VERSION = 3.8.2;
728728 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchWidgetExtension;
729729 PRODUCT_NAME = "$(TARGET_NAME)";
730730 SKIP_INSTALL = YES;
@@ -753,7 +753,7 @@
753753 "@executable_path/Frameworks",
754754 "@executable_path/../../Frameworks",
755755 );
756 MARKETING_VERSION = 3.8.1;
756 MARKETING_VERSION = 3.8.2;
757757 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchWidgetExtension;
758758 PRODUCT_NAME = "$(TARGET_NAME)";
759759 SKIP_INSTALL = YES;
@@ -782,7 +782,7 @@
782782 "@executable_path/Frameworks",
783783 "@executable_path/../../Frameworks",
784784 );
785 MARKETING_VERSION = 3.8.1;
785 MARKETING_VERSION = 3.8.2;
786786 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchSafariExtension;
787787 PRODUCT_NAME = "$(TARGET_NAME)";
788788 SKIP_INSTALL = YES;
@@ -811,7 +811,7 @@
811811 "@executable_path/Frameworks",
812812 "@executable_path/../../Frameworks",
813813 );
814 MARKETING_VERSION = 3.8.1;
814 MARKETING_VERSION = 3.8.2;
815815 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchSafariExtension;
816816 PRODUCT_NAME = "$(TARGET_NAME)";
817817 SKIP_INSTALL = YES;
Hutch/Views/Home/HomeView.swift +52 −21
@@ -10,6 +10,7 @@ struct HomeView: View {
1010 @State private var isOpeningRecentItem = false
1111 @State private var selectedPinnedProject: Project?
1212 @State private var selectedPinnedUser: User?
13 @State private var isShowingSystemStatus = false
1314
1415 var body: some View {
1516 Group {
@@ -20,6 +21,19 @@ struct HomeView: View {
2021 }
2122 }
2223 .navigationTitle("Home")
24 .toolbar {
25 ToolbarItem(placement: .topBarTrailing) {
26 Button {
27 isShowingSystemStatus = true
28 } label: {
29 HomeSystemStatusIndicator(
30 snapshot: viewModel?.systemStatusSnapshot,
31 isLoading: viewModel?.isLoadingSystemStatus ?? false
32 )
33 }
34 .accessibilityLabel("System status")
35 }
36 }
2337 .navigationDestination(isPresented: Binding(
2438 get: { selectedPinnedProject != nil },
2539 set: { isPresented in
@@ -44,6 +58,9 @@ struct HomeView: View {
4458 UserProfileView(user: selectedPinnedUser)
4559 }
4660 }
61 .navigationDestination(isPresented: $isShowingSystemStatus) {
62 SystemStatusView()
63 }
4764 .task {
4865 guard let currentUser = appState.currentUser else { return }
4966 await ensureViewModel(currentUser: currentUser).loadDashboard()
@@ -64,7 +81,6 @@ struct HomeView: View {
6481 @ViewBuilder
6582 private func content(_ viewModel: HomeViewModel) -> some View {
6683 List {
67 systemStatusSection(viewModel)
6884 workSection(viewModel)
6985 recentSection
7086 buildsSection(viewModel)
@@ -84,26 +100,6 @@ struct HomeView: View {
84100 }
85101 }
86102
87 @ViewBuilder
88 private func systemStatusSection(_ viewModel: HomeViewModel) -> some View {
89 if viewModel.systemStatusSnapshot?.hasDisruption == true {
90 Section {
91 NavigationLink {
92 SystemStatusView()
93 } label: {
94 SystemStatusSummaryRow(
95 snapshot: viewModel.systemStatusSnapshot,
96 isLoading: viewModel.isLoadingSystemStatus,
97 errorMessage: viewModel.systemStatusErrorMessage,
98 isShowingStaleData: viewModel.isShowingStaleSystemStatus
99 )
100 }
101 .buttonStyle(.plain)
102 .themedRow()
103 }
104 }
105 }
106
107103 private func workSection(_ viewModel: HomeViewModel) -> some View {
108104 Section("Work") {
109105 NavigationLink(value: HomeRoute.work(scope: .all)) {
@@ -490,6 +486,41 @@ private struct HomeSummaryRow: View {
490486 }
491487}
492488
489private struct HomeSystemStatusIndicator: View {
490 let snapshot: SystemStatusSnapshot?
491 let isLoading: Bool
492
493 var body: some View {
494 if isLoading && snapshot == nil {
495 ProgressView()
496 .controlSize(.small)
497 } else {
498 Circle()
499 .fill(color)
500 .frame(width: 22, height: 22)
501 .overlay {
502 Image(systemName: glyph)
503 .font(.system(size: 11, weight: .bold))
504 .foregroundStyle(.white)
505 }
506 }
507 }
508
509 private var color: Color {
510 if let snapshot {
511 return snapshot.hasDisruption ? .orange : .green
512 }
513 return .secondary
514 }
515
516 private var glyph: String {
517 if let snapshot {
518 return snapshot.hasDisruption ? "exclamationmark" : "checkmark"
519 }
520 return "questionmark"
521 }
522}
523
493524private struct HomeRecentRow: View {
494525 let item: RecentActivityEntry
495526
Hutch/Views/Home/HomeViewModel.swift +13 −10
@@ -365,6 +365,19 @@ final class HomeViewModel {
365365 async let inboxUnreadTask = loadInboxUnreadSnapshot(forceRefresh: forceRefresh)
366366 async let systemStatusTask = loadSystemStatusSnapshot(forceRefresh: forceRefresh)
367367
368 // Resolve system status first so the Home title-bar status badge can
369 // settle from cache without waiting on the slower list data below.
370 let systemStatusResult = await systemStatusTask
371 switch systemStatusResult {
372 case .success(let result):
373 systemStatusSnapshot = result.value
374 isShowingStaleSystemStatus = result.isStale
375 systemStatusErrorMessage = result.isStale ? result.refreshErrorMessage : nil
376 case .failure(let error):
377 systemStatusErrorMessage = error.userFacingMessage
378 }
379 isLoadingSystemStatus = false
380
368381 let projectsResult = await projectsTask
369382 switch projectsResult {
370383 case .success(let projects):
@@ -404,16 +417,6 @@ final class HomeViewModel {
404417 unreadInboxThreadCount = inboxUnreadSnapshot?.unreadCount
405418 unreadInboxThreads = inboxUnreadSnapshot?.threads ?? []
406419 hasUnreadInboxThreads = (unreadInboxThreadCount ?? 0) > 0
407 let systemStatusResult = await systemStatusTask
408 switch systemStatusResult {
409 case .success(let result):
410 systemStatusSnapshot = result.value
411 isShowingStaleSystemStatus = result.isStale
412 systemStatusErrorMessage = result.isStale ? result.refreshErrorMessage : nil
413 case .failure(let error):
414 systemStatusErrorMessage = error.userFacingMessage
415 }
416 isLoadingSystemStatus = false
417420 lastRefreshed = Date()
418421 persistNeedsAttentionSnapshot()
419422 persistSystemStatusWidgetSnapshot()
ROADMAP.md +1
@@ -142,6 +142,7 @@ so "breaking change" does not apply. These buckets track *user-visible scale*.
142142| Version | Contents | Why here |
143143| --- | --- | --- |
144144| v3.8.1 | SonarCloud triage; housekeeping | No behaviour change at all |
145| v3.8.2 | Home system status moved to a title-bar status badge | Small UI relocation, no new surface |
145146| v3.9.0 | "What's cooking" ingest; doc truth-up; deploy keys | Ships one feature, corrects the map |
146147| v3.10.0 | hub.sr.ht writes: projects, discovery, `mailingListSubscribe` | Provisional — gated on what v3.9.0 finds |
147148| v3.11.0 | Accessibility | Independent, device-verified |