aboutsummaryrefslogtreecommitdiff
path: root/Hutch
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-07-16 16:59:55 -0500
committerChristian Cleberg <[email protected]>2026-07-16 16:59:55 -0500
commit7b817a1d2aed0e8b89f36b410f2779f4a6b35407 (patch)
tree2a57a9f152a05ae61307ca8be1220c2a1ee2d708 /Hutch
parentcd8742a0b932627401467bef8b7d5c376798382e (diff)
downloadhutch-7b817a1d2aed0e8b89f36b410f2779f4a6b35407.tar.gz
hutch-7b817a1d2aed0e8b89f36b410f2779f4a6b35407.tar.bz2
hutch-7b817a1d2aed0e8b89f36b410f2779f4a6b35407.zip
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.
Diffstat (limited to 'Hutch')
-rw-r--r--Hutch/Views/Home/HomeView.swift73
-rw-r--r--Hutch/Views/Home/HomeViewModel.swift23
2 files changed, 65 insertions, 31 deletions
diff --git a/Hutch/Views/Home/HomeView.swift b/Hutch/Views/Home/HomeView.swift
index 3e95f0b..f0ec9ff 100644
--- a/Hutch/Views/Home/HomeView.swift
+++ b/Hutch/Views/Home/HomeView.swift
@@ -10,6 +10,7 @@ struct HomeView: View {
@State private var isOpeningRecentItem = false
@State private var selectedPinnedProject: Project?
@State private var selectedPinnedUser: User?
+ @State private var isShowingSystemStatus = false
var body: some View {
Group {
@@ -20,6 +21,19 @@ struct HomeView: View {
}
}
.navigationTitle("Home")
+ .toolbar {
+ ToolbarItem(placement: .topBarTrailing) {
+ Button {
+ isShowingSystemStatus = true
+ } label: {
+ HomeSystemStatusIndicator(
+ snapshot: viewModel?.systemStatusSnapshot,
+ isLoading: viewModel?.isLoadingSystemStatus ?? false
+ )
+ }
+ .accessibilityLabel("System status")
+ }
+ }
.navigationDestination(isPresented: Binding(
get: { selectedPinnedProject != nil },
set: { isPresented in
@@ -44,6 +58,9 @@ struct HomeView: View {
UserProfileView(user: selectedPinnedUser)
}
}
+ .navigationDestination(isPresented: $isShowingSystemStatus) {
+ SystemStatusView()
+ }
.task {
guard let currentUser = appState.currentUser else { return }
await ensureViewModel(currentUser: currentUser).loadDashboard()
@@ -64,7 +81,6 @@ struct HomeView: View {
@ViewBuilder
private func content(_ viewModel: HomeViewModel) -> some View {
List {
- systemStatusSection(viewModel)
workSection(viewModel)
recentSection
buildsSection(viewModel)
@@ -84,26 +100,6 @@ struct HomeView: View {
}
}
- @ViewBuilder
- private func systemStatusSection(_ viewModel: HomeViewModel) -> some View {
- if viewModel.systemStatusSnapshot?.hasDisruption == true {
- Section {
- NavigationLink {
- SystemStatusView()
- } label: {
- SystemStatusSummaryRow(
- snapshot: viewModel.systemStatusSnapshot,
- isLoading: viewModel.isLoadingSystemStatus,
- errorMessage: viewModel.systemStatusErrorMessage,
- isShowingStaleData: viewModel.isShowingStaleSystemStatus
- )
- }
- .buttonStyle(.plain)
- .themedRow()
- }
- }
- }
-
private func workSection(_ viewModel: HomeViewModel) -> some View {
Section("Work") {
NavigationLink(value: HomeRoute.work(scope: .all)) {
@@ -490,6 +486,41 @@ private struct HomeSummaryRow: View {
}
}
+private struct HomeSystemStatusIndicator: View {
+ let snapshot: SystemStatusSnapshot?
+ let isLoading: Bool
+
+ var body: some View {
+ if isLoading && snapshot == nil {
+ ProgressView()
+ .controlSize(.small)
+ } else {
+ Circle()
+ .fill(color)
+ .frame(width: 22, height: 22)
+ .overlay {
+ Image(systemName: glyph)
+ .font(.system(size: 11, weight: .bold))
+ .foregroundStyle(.white)
+ }
+ }
+ }
+
+ private var color: Color {
+ if let snapshot {
+ return snapshot.hasDisruption ? .orange : .green
+ }
+ return .secondary
+ }
+
+ private var glyph: String {
+ if let snapshot {
+ return snapshot.hasDisruption ? "exclamationmark" : "checkmark"
+ }
+ return "questionmark"
+ }
+}
+
private struct HomeRecentRow: View {
let item: RecentActivityEntry
diff --git a/Hutch/Views/Home/HomeViewModel.swift b/Hutch/Views/Home/HomeViewModel.swift
index 3789c11..7779bea 100644
--- a/Hutch/Views/Home/HomeViewModel.swift
+++ b/Hutch/Views/Home/HomeViewModel.swift
@@ -365,6 +365,19 @@ final class HomeViewModel {
async let inboxUnreadTask = loadInboxUnreadSnapshot(forceRefresh: forceRefresh)
async let systemStatusTask = loadSystemStatusSnapshot(forceRefresh: forceRefresh)
+ // Resolve system status first so the Home title-bar status badge can
+ // settle from cache without waiting on the slower list data below.
+ 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
+
let projectsResult = await projectsTask
switch projectsResult {
case .success(let projects):
@@ -404,16 +417,6 @@ final class HomeViewModel {
unreadInboxThreadCount = inboxUnreadSnapshot?.unreadCount
unreadInboxThreads = inboxUnreadSnapshot?.threads ?? []
hasUnreadInboxThreads = (unreadInboxThreadCount ?? 0) > 0
- 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
lastRefreshed = Date()
persistNeedsAttentionSnapshot()
persistSystemStatusWidgetSnapshot()