Commit 91f87b483b
91f87b483b12d57711fb1372a31a9e8e9e141207
parent: 0e94907674
Unsigned
cmc <hello@cleberg.net> · 2026-08-22 23:02 UTC
committer: <noreply@github.com>
Start new installs with an empty watch list (#45)
The placeholder seeded every fresh install with ccleberg/cleberg.net,
so a new user's first fetch was someone else's repo, usually failing.
The empty state now points at the gear menu instead of claiming there
are no open alerts.
Refresh no longer treats an empty watch list as a complete poll. It
carries no information, and counting it as complete would prune every
dismissal and record every tracked alert as resolved — reachable before
by removing all repos, and the default path now.
Layout: unified · split
octosentry/PersistedState.swift
+4 −1
| @@ -93,8 +93,11 @@ nonisolated struct PersistedState: Codable { |
| 93 | history = try container.decodeIfPresent(AlertHistory.self, forKey: .history) ?? AlertHistory() |
93 | history = try container.decodeIfPresent(AlertHistory.self, forKey: .history) ?? AlertHistory() |
| 94 | } |
94 | } |
| 95 | |
95 | |
| |
96 | /// A fresh install watches nothing until the user adds a repo. Seeding a |
| |
97 | /// specific repo here meant every new install started by fetching someone |
| |
98 | /// else's alerts, which it usually can't read. |
| 96 | static let placeholder = PersistedState( |
99 | static let placeholder = PersistedState( |
| 97 | watchedRepos: [WatchedRepo(fullName: "ccleberg/cleberg.net", accountID: 0)], |
100 | watchedRepos: [], |
| 98 | seenEventIDs: [], |
101 | seenEventIDs: [], |
| 99 | lastFetchByRepo: [:], |
102 | lastFetchByRepo: [:], |
| 100 | minimumSeverity: .low |
103 | minimumSeverity: .low |
octosentry/SecurityEventListView.swift
+8
| @@ -277,6 +277,14 @@ struct SecurityEventListView: View { |
| 277 | tint: .secondary, |
277 | tint: .secondary, |
| 278 | message: "\(store.totalFetchedCount) alert(s) are below your minimum severity filter" |
278 | message: "\(store.totalFetchedCount) alert(s) are below your minimum severity filter" |
| 279 | ) |
279 | ) |
| |
280 | } else if store.watchedRepos.isEmpty { |
| |
281 | // "No open alerts" would be misleading when nothing is |
| |
282 | // being watched in the first place. |
| |
283 | StatusView( |
| |
284 | systemImage: "plus.circle", |
| |
285 | tint: .secondary, |
| |
286 | message: "No repositories watched yet — add one from the gear menu" |
| |
287 | ) |
| 280 | } else { |
288 | } else { |
| 281 | StatusView(systemImage: "checkmark.shield", tint: .green, message: "No open security alerts") |
289 | StatusView(systemImage: "checkmark.shield", tint: .green, message: "No open security alerts") |
| 282 | } |
290 | } |
octosentry/SecurityEventStore.swift
+5 −1
| @@ -172,7 +172,11 @@ final class SecurityEventStore { |
| 172 | |
172 | |
| 173 | // Only prune against a complete picture: if a repo failed this round |
173 | // Only prune against a complete picture: if a repo failed this round |
| 174 | // its alerts are missing, and pruning would forget they were hidden. |
174 | // its alerts are missing, and pruning would forget they were hidden. |
| 175 | if fetchedEventsByRepo.count == Set(state.watchedRepos.map(\.fullName)).count { |
175 | // An empty watch list is not a complete picture either — it carries no |
| |
176 | // information, and treating it as one would drop every dismissal and |
| |
177 | // record every tracked alert as resolved. |
| |
178 | let watchedRepoNames = Set(state.watchedRepos.map(\.fullName)) |
| |
179 | if !watchedRepoNames.isEmpty, fetchedEventsByRepo.count == watchedRepoNames.count { |
| 176 | let now = Date() |
180 | let now = Date() |
| 177 | state.triage = state.triage.pruned( |
181 | state.triage = state.triage.pruned( |
| 178 | presentEventIDs: Set(fetchedEvents.map(\.id)), |
182 | presentEventIDs: Set(fetchedEvents.map(\.id)), |
octosentryTests/PersistedStateTests.swift
+5
| @@ -112,6 +112,11 @@ struct PersistedStateTests { |
| 112 | } |
112 | } |
| 113 | } |
113 | } |
| 114 | |
114 | |
| |
115 | // A fresh install must not arrive watching somebody else's repo. |
| |
116 | @Test func placeholderWatchesNothing() { |
| |
117 | #expect(PersistedState.placeholder.watchedRepos.isEmpty) |
| |
118 | } |
| |
119 | |
| 115 | @Test func placeholderStartsWithNoSeenStateAndNoRepoScope() { |
120 | @Test func placeholderStartsWithNoSeenStateAndNoRepoScope() { |
| 116 | #expect(PersistedState.placeholder.seenEventIDs.isEmpty) |
121 | #expect(PersistedState.placeholder.seenEventIDs.isEmpty) |
| 117 | #expect(PersistedState.placeholder.lastFetchByRepo.isEmpty) |
122 | #expect(PersistedState.placeholder.lastFetchByRepo.isEmpty) |