summaryrefslogtreecommitdiff
path: root/Hutch/Views/Home
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-04-20 12:42:09 -0500
committerChristian Cleberg <[email protected]>2026-04-20 12:42:09 -0500
commit454c459394437f346a2d08d59a5504c0f6ec299f (patch)
treedd7dc1521aec859cb7b093c922ea61ea5efac7a9 /Hutch/Views/Home
parent32c0e55b0713a15094c1640e7af70e0ed19d78d6 (diff)
downloadhutch-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.swift33
-rw-r--r--Hutch/Views/Home/HomeViewModel.swift78
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)