Commit 3787cde3da

3787cde3dad82f80acce5059b8ffe8a65ed2bd9b

parent: d5dec7be8a

Unsigned

cmc <hello@cleberg.net> · 2026-07-16 15:13 UTC

fix: honor forceRefresh for projects and system status on the dashboard

loadDashboard(forceRefresh:) fanned the flag out to five loaders, but
loadProjects and loadSystemStatusSnapshot dropped it: they called
fetchProjects() and snapshotResult() with no policy, so pull-to-refresh
returned cached projects and status while the other three sections
refreshed. SonarCloud flagged both params as unused (swift:S1172).

Thread forceRefresh through ProjectService.fetchProjects into the page
policy (refreshIgnoringCache when forced), and pass it to
snapshotResult, which already accepted it. ProjectsListView carried the
same latent bug via its own .refreshable — fixed there too now that
fetchProjects can force.

Layout: unified · split

Hutch/Networking/ProjectService.swift +4 −4
@@ -269,15 +269,15 @@ struct ProjectService: Sendable {
269 self.client = client 269 self.client = client
270 } 270 }
271 271
272 func fetchProjects() async throws -> [Project] { 272 func fetchProjects(forceRefresh: Bool = false) async throws -> [Project] {
273 try await fetchProjectSummaries().map(Self.makeSummaryProject) 273 try await fetchProjectSummaries(forceRefresh: forceRefresh).map(Self.makeSummaryProject)
274 } 274 }
275 275
276 func fetchProjectDetail(rid: String) async throws -> Project { 276 func fetchProjectDetail(rid: String) async throws -> Project {
277 try await fetchProjectDetailPayload(rid: rid) 277 try await fetchProjectDetailPayload(rid: rid)
278 } 278 }
279 279
280 private func fetchProjectSummaries() async throws -> [ProjectSummaryPayload] { 280 private func fetchProjectSummaries(forceRefresh: Bool) async throws -> [ProjectSummaryPayload] {
281 var results: [ProjectSummaryPayload] = [] 281 var results: [ProjectSummaryPayload] = []
282 var cursor: String? 282 var cursor: String?
283 283
@@ -295,7 +295,7 @@ struct ProjectService: Sendable {
295 cacheKey: APICacheKeys.projects(cursor: cursor), 295 cacheKey: APICacheKeys.projects(cursor: cursor),
296 resourceType: .userProfile, 296 resourceType: .userProfile,
297 ttl: APICacheTTLs.projectList, 297 ttl: APICacheTTLs.projectList,
298 policy: .cacheFirstThenRefresh 298 policy: forceRefresh ? .refreshIgnoringCache : .cacheFirstThenRefresh
299 ) 299 )
300 let response = cached.value 300 let response = cached.value
301 301
Hutch/Views/Home/HomeViewModel.swift +2 −2
@@ -683,7 +683,7 @@ final class HomeViewModel {
683 683
684 private func loadProjects(forceRefresh: Bool) async -> Result<[Project], Error> { 684 private func loadProjects(forceRefresh: Bool) async -> Result<[Project], Error> {
685 do { 685 do {
686 return .success(try await projectService.fetchProjects()) 686 return .success(try await projectService.fetchProjects(forceRefresh: forceRefresh))
687 } catch { 687 } catch {
688 return .failure(error) 688 return .failure(error)
689 } 689 }
@@ -716,7 +716,7 @@ final class HomeViewModel {
716 716
717 private func loadSystemStatusSnapshot(forceRefresh: Bool) async -> Result<CachedSystemStatusValue<SystemStatusSnapshot>, Error> { 717 private func loadSystemStatusSnapshot(forceRefresh: Bool) async -> Result<CachedSystemStatusValue<SystemStatusSnapshot>, Error> {
718 do { 718 do {
719 return .success(try await systemStatusRepository.snapshotResult()) 719 return .success(try await systemStatusRepository.snapshotResult(forceRefresh: forceRefresh))
720 } catch { 720 } catch {
721 return .failure(error) 721 return .failure(error)
722 } 722 }
Hutch/Views/Projects/ProjectsListView.swift +3 −3
@@ -25,14 +25,14 @@ final class ProjectsListViewModel {
25 } 25 }
26 } 26 }
27 27
28 func loadProjects() async { 28 func loadProjects(forceRefresh: Bool = false) async {
29 guard !isLoading else { return } 29 guard !isLoading else { return }
30 isLoading = true 30 isLoading = true
31 error = nil 31 error = nil
32 defer { isLoading = false } 32 defer { isLoading = false }
33 33
34 do { 34 do {
35 projects = try await service.fetchProjects() 35 projects = try await service.fetchProjects(forceRefresh: forceRefresh)
36 } catch { 36 } catch {
37 if projects.isEmpty { 37 if projects.isEmpty {
38 self.error = error.userFacingMessage 38 self.error = error.userFacingMessage
@@ -115,7 +115,7 @@ struct ProjectsListView: View {
115 ) 115 )
116 ) 116 )
117 .refreshable { 117 .refreshable {
118 await viewModel.loadProjects() 118 await viewModel.loadProjects(forceRefresh: true)
119 } 119 }
120 .connectivityOverlay(hasContent: !viewModel.projects.isEmpty) { 120 .connectivityOverlay(hasContent: !viewModel.projects.isEmpty) {
121 await viewModel.loadProjects() 121 await viewModel.loadProjects()