aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-07-16 17:08:45 -0500
committerGitHub <[email protected]>2026-07-16 17:08:45 -0500
commita5d1064ff798cda8a237f2a11877634cd81f7ac3 (patch)
tree2a57a9f152a05ae61307ca8be1220c2a1ee2d708
parentcd8742a0b932627401467bef8b7d5c376798382e (diff)
parent7b817a1d2aed0e8b89f36b410f2779f4a6b35407 (diff)
downloadhutch-a5d1064ff798cda8a237f2a11877634cd81f7ac3.tar.gz
hutch-a5d1064ff798cda8a237f2a11877634cd81f7ac3.tar.bz2
hutch-a5d1064ff798cda8a237f2a11877634cd81f7ac3.zip
Merge pull request #20 from zerolabsco/home-status-badge
Move Home system status into a title-bar status badge
-rw-r--r--Hutch.xcodeproj/project.pbxproj12
-rw-r--r--Hutch/Views/Home/HomeView.swift73
-rw-r--r--Hutch/Views/Home/HomeViewModel.swift23
-rw-r--r--ROADMAP.md1
4 files changed, 72 insertions, 37 deletions
diff --git a/Hutch.xcodeproj/project.pbxproj b/Hutch.xcodeproj/project.pbxproj
index eba1ba7..409df4b 100644
--- a/Hutch.xcodeproj/project.pbxproj
+++ b/Hutch.xcodeproj/project.pbxproj
@@ -614,7 +614,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 3.8.1;
+ MARKETING_VERSION = 3.8.2;
PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = YES;
@@ -651,7 +651,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 3.8.1;
+ MARKETING_VERSION = 3.8.2;
PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = YES;
@@ -724,7 +724,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
- MARKETING_VERSION = 3.8.1;
+ MARKETING_VERSION = 3.8.2;
PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchWidgetExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
@@ -753,7 +753,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
- MARKETING_VERSION = 3.8.1;
+ MARKETING_VERSION = 3.8.2;
PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchWidgetExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
@@ -782,7 +782,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
- MARKETING_VERSION = 3.8.1;
+ MARKETING_VERSION = 3.8.2;
PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchSafariExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
@@ -811,7 +811,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
- MARKETING_VERSION = 3.8.1;
+ MARKETING_VERSION = 3.8.2;
PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchSafariExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
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()
diff --git a/ROADMAP.md b/ROADMAP.md
index 43c3e7c..53e8f6c 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -142,6 +142,7 @@ so "breaking change" does not apply. These buckets track *user-visible scale*.
| Version | Contents | Why here |
| --- | --- | --- |
| v3.8.1 | SonarCloud triage; housekeeping | No behaviour change at all |
+| v3.8.2 | Home system status moved to a title-bar status badge | Small UI relocation, no new surface |
| v3.9.0 | "What's cooking" ingest; doc truth-up; deploy keys | Ships one feature, corrects the map |
| v3.10.0 | hub.sr.ht writes: projects, discovery, `mailingListSubscribe` | Provisional — gated on what v3.9.0 finds |
| v3.11.0 | Accessibility | Independent, device-verified |