Commit 972217316c
Unsigned
Layout: unified · split
Hutch.xcodeproj/project.pbxproj +6 −6
| @@ -614,7 +614,7 @@ | ||
| 614 | 614 | "$(inherited)", |
| 615 | 615 | "@executable_path/Frameworks", |
| 616 | 616 | ); |
| 617 | MARKETING_VERSION = 3.8.1; | |
| 617 | MARKETING_VERSION = 3.8.2; | |
| 618 | 618 | PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch; |
| 619 | 619 | PRODUCT_NAME = "$(TARGET_NAME)"; |
| 620 | 620 | STRING_CATALOG_GENERATE_SYMBOLS = YES; |
| @@ -651,7 +651,7 @@ | ||
| 651 | 651 | "$(inherited)", |
| 652 | 652 | "@executable_path/Frameworks", |
| 653 | 653 | ); |
| 654 | MARKETING_VERSION = 3.8.1; | |
| 654 | MARKETING_VERSION = 3.8.2; | |
| 655 | 655 | PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch; |
| 656 | 656 | PRODUCT_NAME = "$(TARGET_NAME)"; |
| 657 | 657 | STRING_CATALOG_GENERATE_SYMBOLS = YES; |
| @@ -724,7 +724,7 @@ | ||
| 724 | 724 | "@executable_path/Frameworks", |
| 725 | 725 | "@executable_path/../../Frameworks", |
| 726 | 726 | ); |
| 727 | MARKETING_VERSION = 3.8.1; | |
| 727 | MARKETING_VERSION = 3.8.2; | |
| 728 | 728 | PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchWidgetExtension; |
| 729 | 729 | PRODUCT_NAME = "$(TARGET_NAME)"; |
| 730 | 730 | SKIP_INSTALL = YES; |
| @@ -753,7 +753,7 @@ | ||
| 753 | 753 | "@executable_path/Frameworks", |
| 754 | 754 | "@executable_path/../../Frameworks", |
| 755 | 755 | ); |
| 756 | MARKETING_VERSION = 3.8.1; | |
| 756 | MARKETING_VERSION = 3.8.2; | |
| 757 | 757 | PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchWidgetExtension; |
| 758 | 758 | PRODUCT_NAME = "$(TARGET_NAME)"; |
| 759 | 759 | SKIP_INSTALL = YES; |
| @@ -782,7 +782,7 @@ | ||
| 782 | 782 | "@executable_path/Frameworks", |
| 783 | 783 | "@executable_path/../../Frameworks", |
| 784 | 784 | ); |
| 785 | MARKETING_VERSION = 3.8.1; | |
| 785 | MARKETING_VERSION = 3.8.2; | |
| 786 | 786 | PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchSafariExtension; |
| 787 | 787 | PRODUCT_NAME = "$(TARGET_NAME)"; |
| 788 | 788 | SKIP_INSTALL = YES; |
| @@ -811,7 +811,7 @@ | ||
| 811 | 811 | "@executable_path/Frameworks", |
| 812 | 812 | "@executable_path/../../Frameworks", |
| 813 | 813 | ); |
| 814 | MARKETING_VERSION = 3.8.1; | |
| 814 | MARKETING_VERSION = 3.8.2; | |
| 815 | 815 | PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchSafariExtension; |
| 816 | 816 | PRODUCT_NAME = "$(TARGET_NAME)"; |
| 817 | 817 | SKIP_INSTALL = YES; |
Hutch/Views/Home/HomeView.swift +52 −21
| @@ -10,6 +10,7 @@ struct HomeView: View { | ||
| 10 | 10 | @State private var isOpeningRecentItem = false |
| 11 | 11 | @State private var selectedPinnedProject: Project? |
| 12 | 12 | @State private var selectedPinnedUser: User? |
| 13 | @State private var isShowingSystemStatus = false | |
| 13 | 14 | |
| 14 | 15 | var body: some View { |
| 15 | 16 | Group { |
| @@ -20,6 +21,19 @@ struct HomeView: View { | ||
| 20 | 21 | } |
| 21 | 22 | } |
| 22 | 23 | .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 | } | |
| 23 | 37 | .navigationDestination(isPresented: Binding( |
| 24 | 38 | get: { selectedPinnedProject != nil }, |
| 25 | 39 | set: { isPresented in |
| @@ -44,6 +58,9 @@ struct HomeView: View { | ||
| 44 | 58 | UserProfileView(user: selectedPinnedUser) |
| 45 | 59 | } |
| 46 | 60 | } |
| 61 | .navigationDestination(isPresented: $isShowingSystemStatus) { | |
| 62 | SystemStatusView() | |
| 63 | } | |
| 47 | 64 | .task { |
| 48 | 65 | guard let currentUser = appState.currentUser else { return } |
| 49 | 66 | await ensureViewModel(currentUser: currentUser).loadDashboard() |
| @@ -64,7 +81,6 @@ struct HomeView: View { | ||
| 64 | 81 | @ViewBuilder |
| 65 | 82 | private func content(_ viewModel: HomeViewModel) -> some View { |
| 66 | 83 | List { |
| 67 | systemStatusSection(viewModel) | |
| 68 | 84 | workSection(viewModel) |
| 69 | 85 | recentSection |
| 70 | 86 | buildsSection(viewModel) |
| @@ -84,26 +100,6 @@ struct HomeView: View { | ||
| 84 | 100 | } |
| 85 | 101 | } |
| 86 | 102 | |
| 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 | ||
| 107 | 103 | private func workSection(_ viewModel: HomeViewModel) -> some View { |
| 108 | 104 | Section("Work") { |
| 109 | 105 | NavigationLink(value: HomeRoute.work(scope: .all)) { |
| @@ -490,6 +486,41 @@ private struct HomeSummaryRow: View { | ||
| 490 | 486 | } |
| 491 | 487 | } |
| 492 | 488 | |
| 489 | private 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 | ||
| 493 | 524 | private struct HomeRecentRow: View { |
| 494 | 525 | let item: RecentActivityEntry |
| 495 | 526 | |
Hutch/Views/Home/HomeViewModel.swift +13 −10
| @@ -365,6 +365,19 @@ final class HomeViewModel { | ||
| 365 | 365 | async let inboxUnreadTask = loadInboxUnreadSnapshot(forceRefresh: forceRefresh) |
| 366 | 366 | async let systemStatusTask = loadSystemStatusSnapshot(forceRefresh: forceRefresh) |
| 367 | 367 | |
| 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 | ||
| 368 | 381 | let projectsResult = await projectsTask |
| 369 | 382 | switch projectsResult { |
| 370 | 383 | case .success(let projects): |
| @@ -404,16 +417,6 @@ final class HomeViewModel { | ||
| 404 | 417 | unreadInboxThreadCount = inboxUnreadSnapshot?.unreadCount |
| 405 | 418 | unreadInboxThreads = inboxUnreadSnapshot?.threads ?? [] |
| 406 | 419 | 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 | |
| 417 | 420 | lastRefreshed = Date() |
| 418 | 421 | persistNeedsAttentionSnapshot() |
| 419 | 422 | persistSystemStatusWidgetSnapshot() |
ROADMAP.md +1
| @@ -142,6 +142,7 @@ so "breaking change" does not apply. These buckets track *user-visible scale*. | ||
| 142 | 142 | | Version | Contents | Why here | |
| 143 | 143 | | --- | --- | --- | |
| 144 | 144 | | 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 | | |
| 145 | 146 | | v3.9.0 | "What's cooking" ingest; doc truth-up; deploy keys | Ships one feature, corrects the map | |
| 146 | 147 | | v3.10.0 | hub.sr.ht writes: projects, discovery, `mailingListSubscribe` | Provisional — gated on what v3.9.0 finds | |
| 147 | 148 | | v3.11.0 | Accessibility | Independent, device-verified | |