diff options
| author | Christian Cleberg <[email protected]> | 2026-04-20 12:42:09 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-04-20 12:42:09 -0500 |
| commit | 454c459394437f346a2d08d59a5504c0f6ec299f (patch) | |
| tree | dd7dc1521aec859cb7b093c922ea61ea5efac7a9 /Hutch/Views/Home | |
| parent | 32c0e55b0713a15094c1640e7af70e0ed19d78d6 (diff) | |
| download | hutch-454c459394437f346a2d08d59a5504c0f6ec299f.tar.gz hutch-454c459394437f346a2d08d59a5504c0f6ec299f.tar.bz2 hutch-454c459394437f346a2d08d59a5504c0f6ec299f.zip | |
feat: user-defined time period for recent builds card
- limit Home failed-build counts to a configurable lookback window
and add the setting under Behavior.
- make the Recent and Builds rows fully tappable across the
entire cell and add coverage for the new failed-build filtering.
Fixes: https://todo.sr.ht/~ccleberg/hutch/67
Diffstat (limited to 'Hutch/Views/Home')
| -rw-r--r-- | Hutch/Views/Home/HomeView.swift | 33 | ||||
| -rw-r--r-- | Hutch/Views/Home/HomeViewModel.swift | 78 |
2 files changed, 78 insertions, 33 deletions
diff --git a/Hutch/Views/Home/HomeView.swift b/Hutch/Views/Home/HomeView.swift index 242eeac..406e2ad 100644 --- a/Hutch/Views/Home/HomeView.swift +++ b/Hutch/Views/Home/HomeView.swift @@ -3,6 +3,8 @@ import SwiftUI struct HomeView: View { @Environment(AppState.self) private var appState @Environment(\.scenePhase) private var scenePhase + @AppStorage(AppStorageKeys.homeFailedBuildLookbackDays, store: .standard) + private var failedBuildLookbackDays = HomeViewModel.defaultFailedBuildLookbackDays @State private var viewModel: HomeViewModel? @State private var recentItems: [RecentActivityEntry] = [] @State private var isOpeningRecentItem = false @@ -142,7 +144,7 @@ struct HomeView: View { title: buildsTitle(viewModel), summary: buildsSummary(viewModel), systemImage: "hammer", - tint: viewModel.failedBuildCount > 0 ? .orange : .secondary, + tint: failedBuildCount(viewModel) > 0 ? .orange : .secondary, emphasis: .monitoring ) } @@ -208,7 +210,7 @@ struct HomeView: View { } private func buildsTitle(_ viewModel: HomeViewModel) -> String { - let failed = viewModel.failedBuildCount + let failed = failedBuildCount(viewModel) let running = viewModel.activeBuildCount if failed == 0 && running == 0 { @@ -221,18 +223,18 @@ struct HomeView: View { } private func buildsSummary(_ viewModel: HomeViewModel) -> String { - let failed = viewModel.failedBuildCount + let failed = failedBuildCount(viewModel) let running = viewModel.activeBuildCount if failed == 0 && running == 0 { - return "No failures • \(buildTimeframeLabel(viewModel))" + return "No failures • \(buildTimeframeLabel())" } if failed > 0 && running > 0 { - return "\(failed) failed • \(running) running • \(buildTimeframeLabel(viewModel))" + return "\(failed) failed • \(running) running • \(buildTimeframeLabel())" } if failed > 0 { - return "\(failed) failed • \(buildTimeframeLabel(viewModel))" + return "\(failed) failed • \(buildTimeframeLabel())" } - return "\(running) running • \(buildTimeframeLabel(viewModel))" + return "\(running) running now" } private func pinnedItems(_ viewModel: HomeViewModel) -> [HomePinnedItem] { @@ -251,15 +253,12 @@ struct HomeView: View { } } - private func buildTimeframeLabel(_ viewModel: HomeViewModel) -> String { - let calendar = Calendar.current - let buildDates = viewModel.recentBuilds.map(\.job.updated) - - guard !buildDates.isEmpty else { - return "today" - } + private func buildTimeframeLabel() -> String { + HomeViewModel.failedBuildLookbackLabel(days: failedBuildLookbackDays) + } - return buildDates.allSatisfy(calendar.isDateInToday) ? "today" : "this week" + private func failedBuildCount(_ viewModel: HomeViewModel) -> Int { + viewModel.recentFailedBuilds(lookbackDays: failedBuildLookbackDays).count } private func hasHomeContent(_ viewModel: HomeViewModel) -> Bool { @@ -464,6 +463,8 @@ private struct HomeSummaryRow: View { Spacer(minLength: 8) } + .frame(maxWidth: .infinity, alignment: .leading) + .contentShape(Rectangle()) .padding(.vertical, verticalPadding) } @@ -508,6 +509,8 @@ private struct HomeRecentRow: View { Spacer(minLength: 8) } + .frame(maxWidth: .infinity, alignment: .leading) + .contentShape(Rectangle()) .padding(.vertical, 1) } diff --git a/Hutch/Views/Home/HomeViewModel.swift b/Hutch/Views/Home/HomeViewModel.swift index ec54759..4bd4f32 100644 --- a/Hutch/Views/Home/HomeViewModel.swift +++ b/Hutch/Views/Home/HomeViewModel.swift @@ -172,6 +172,9 @@ struct HomeBuildItem: Identifiable, Hashable, Sendable { @Observable @MainActor final class HomeViewModel { + nonisolated static let defaultFailedBuildLookbackDays = 7 + nonisolated static let allowedFailedBuildLookbackDays = [1, 3, 7, 14, 30] + private(set) var projects: [Project] = [] var assignedTickets: [HomeAssignedTicket] = [] var recentBuilds: [HomeBuildItem] = [] @@ -434,14 +437,7 @@ final class HomeViewModel { } var failedBuildCount: Int { - recentBuilds.filter { - switch $0.job.status { - case .failed, .timeout: - return true - default: - return false - } - }.count + recentFailedBuilds().count } var activeBuildCount: Int { @@ -532,6 +528,19 @@ final class HomeViewModel { return parts.joined(separator: " • ") } + func recentFailedBuilds( + lookbackDays: Int? = nil, + now: Date = .now, + calendar: Calendar = .current + ) -> [HomeBuildItem] { + Self.failedBuilds( + in: recentBuilds, + lookbackDays: lookbackDays ?? Self.failedBuildLookbackDays(), + now: now, + calendar: calendar + ) + } + var systemSummaryText: String { guard let systemStatusSnapshot else { return systemStatusErrorMessage ?? "System status unavailable" @@ -949,20 +958,12 @@ final class HomeViewModel { } private func persistNeedsAttentionSnapshot() { + let failedBuildCount = recentFailedBuilds().count NeedsAttentionSnapshotStore.save( NeedsAttentionSnapshot( unreadInboxThreads: unreadInboxThreadCount, assignedOpenTickets: assignedTicketsError == nil ? assignedTickets.count : nil, - failedBuilds: recentBuildsError == nil - ? recentBuilds.filter { - switch $0.job.status { - case .failed, .timeout: - true - default: - false - } - }.count - : nil, + failedBuilds: recentBuildsError == nil ? failedBuildCount : nil, updatedAt: .now ), accountID: accountID @@ -1013,6 +1014,47 @@ final class HomeViewModel { } } + nonisolated static func failedBuildLookbackDays(defaults: UserDefaults = .standard) -> Int { + let value = defaults.object(forKey: AppStorageKeys.homeFailedBuildLookbackDays) as? Int + guard let value, allowedFailedBuildLookbackDays.contains(value) else { + return defaultFailedBuildLookbackDays + } + return value + } + + nonisolated static func failedBuilds( + in builds: [HomeBuildItem], + lookbackDays: Int, + now: Date = .now, + calendar: Calendar = .current + ) -> [HomeBuildItem] { + let normalizedLookbackDays = allowedFailedBuildLookbackDays.contains(lookbackDays) + ? lookbackDays + : defaultFailedBuildLookbackDays + let startOfToday = calendar.startOfDay(for: now) + let windowStart = calendar.date(byAdding: .day, value: -(normalizedLookbackDays - 1), to: startOfToday) ?? startOfToday + + return builds.filter { build in + guard build.job.updated >= windowStart else { return false } + switch build.job.status { + case .failed, .timeout: + return true + default: + return false + } + } + } + + nonisolated static func failedBuildLookbackLabel(days: Int) -> String { + let normalizedDays = allowedFailedBuildLookbackDays.contains(days) + ? days + : defaultFailedBuildLookbackDays + if normalizedDays == 1 { + return "today" + } + return "last \(normalizedDays) days" + } + nonisolated static func sortBuildItemsForTriage(_ lhs: HomeBuildItem, _ rhs: HomeBuildItem) -> Bool { let lhsPriority = buildPriority(for: lhs.job.status) let rhsPriority = buildPriority(for: rhs.job.status) |
