From 00cc231e9b419d27b412036d93457c2cbabe0b16 Mon Sep 17 00:00:00 2001 From: Christian Cleberg Date: Sat, 11 Apr 2026 23:36:35 -0500 Subject: Add SourceHut system status screen and home disruption banner --- Hutch/Views/Home/HomeView.swift | 47 +++++++++++++++++++++++++++++++++++- Hutch/Views/Home/HomeViewModel.swift | 20 ++++++++++++++- 2 files changed, 65 insertions(+), 2 deletions(-) (limited to 'Hutch/Views/Home') diff --git a/Hutch/Views/Home/HomeView.swift b/Hutch/Views/Home/HomeView.swift index f9d3de5..695a31d 100644 --- a/Hutch/Views/Home/HomeView.swift +++ b/Hutch/Views/Home/HomeView.swift @@ -33,7 +33,11 @@ struct HomeView: View { if let viewModel { vm = viewModel } else { - let newViewModel = HomeViewModel(currentUser: currentUser, client: appState.client) + let newViewModel = HomeViewModel( + currentUser: currentUser, + client: appState.client, + systemStatusRepository: appState.systemStatusRepository + ) viewModel = newViewModel vm = newViewModel } @@ -51,6 +55,7 @@ struct HomeView: View { @ViewBuilder private func content(_ viewModel: HomeViewModel) -> some View { List { + systemStatusBannerSection(viewModel) projectsSection(viewModel) assignedTicketsSection(viewModel) recentBuildsSection(viewModel) @@ -75,6 +80,20 @@ struct HomeView: View { } } + @ViewBuilder + private func systemStatusBannerSection(_ viewModel: HomeViewModel) -> some View { + if let bannerTitle = viewModel.systemStatusBannerTitle { + Section { + Button { + appState.openSystemStatus() + } label: { + HomeSystemStatusBanner(title: bannerTitle) + } + .buttonStyle(.plain) + } + } + } + @ViewBuilder private func projectsSection(_ viewModel: HomeViewModel) -> some View { if !viewModel.projects.isEmpty { @@ -233,6 +252,32 @@ 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 8b4e7aa..a06df20 100644 --- a/Hutch/Views/Home/HomeViewModel.swift +++ b/Hutch/Views/Home/HomeViewModel.swift @@ -143,6 +143,7 @@ final class HomeViewModel { private(set) var projects: [Project] = [] var assignedTickets: [HomeAssignedTicket] = [] var recentBuilds: [HomeBuildItem] = [] + private(set) var systemStatusSnapshot: SystemStatusSnapshot? private(set) var hasUnreadInboxThreads = false private(set) var unreadInboxThreadCount: Int? private(set) var isLoadingProjects = false @@ -153,6 +154,7 @@ final class HomeViewModel { private let currentUser: User private let client: SRHTClient + private let systemStatusRepository: SystemStatusRepository private let projectService: ProjectService private let ticketFetchConcurrencyLimit = 6 private let inboxUnreadConcurrencyLimit = 4 @@ -266,9 +268,10 @@ final class HomeViewModel { } """ - init(currentUser: User, client: SRHTClient) { + init(currentUser: User, client: SRHTClient, systemStatusRepository: SystemStatusRepository) { self.currentUser = currentUser self.client = client + self.systemStatusRepository = systemStatusRepository self.projectService = ProjectService(client: client) } @@ -283,6 +286,7 @@ final class HomeViewModel { async let jobsTask = loadRecentJobs() async let assignedTicketsTask = loadAssignedTickets() async let inboxUnreadTask = loadInboxUnreadCount() + async let systemStatusTask = loadSystemStatusSnapshot() let projectsResult = await projectsTask switch projectsResult { @@ -320,9 +324,15 @@ final class HomeViewModel { unreadInboxThreadCount = await inboxUnreadTask hasUnreadInboxThreads = (unreadInboxThreadCount ?? 0) > 0 + systemStatusSnapshot = await systemStatusTask persistNeedsAttentionSnapshot() } + var systemStatusBannerTitle: String? { + guard let systemStatusSnapshot, systemStatusSnapshot.hasDisruption else { return nil } + return systemStatusSnapshot.bannerSummary + } + func resolveTicket(_ ticket: HomeAssignedTicket) async { let input: [String: any Sendable] = [ "status": TicketStatus.resolved.rawValue, @@ -420,6 +430,14 @@ final class HomeViewModel { } } + private func loadSystemStatusSnapshot() async -> SystemStatusSnapshot? { + do { + return try await systemStatusRepository.snapshot() + } catch { + return systemStatusSnapshot + } + } + private func fetchUnreadInboxThreadCount() async throws -> Int { let mailingLists = try await fetchInboxMailingLists() guard !mailingLists.isEmpty else { return 0 } -- cgit v1.2.3