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() |