Commit 0dca8dc832
0dca8dc832adedf57c8a6e9516fb258b0f681638
parent: 91f87b483b
Unsigned
cmc <hello@cleberg.net> · 2026-08-22 23:06 UTC
committer: <noreply@github.com>
Render seen alerts as seen instead of dropping them (#46)
Marking an alert seen removed the row for the session, then the next
poll put it back rendered identically, so the action looked like it had
done nothing.
The row now stays and recedes: dimmed, with a filled checkmark. Hiding
an alert is what dismiss and snooze are for. The control is a toggle, so
acknowledging isn't a one-way door.
markSeen becomes setSeen(_:_:).
Layout: unified · split
octosentry/SecurityEventListView.swift
+1 −1
| @@ -313,7 +313,7 @@ struct SecurityEventListView: View { |
| 313 | 313 | : nil, |
| 314 | 314 | isHidden: store.triage.isHidden(event.id, now: .now), |
| 315 | 315 | snoozedUntil: store.triage.snoozedUntil(event.id, now: .now), |
| 316 | | onMarkSeen: { Task { await store.markSeen(event.id) } }, |
| 316 | onToggleSeen: { Task { await store.setSeen(event.id, !event.seenLocally) } }, |
| 317 | 317 | onDismiss: { Task { await store.dismiss(event.id) } }, |
| 318 | 318 | onSnooze: { duration in |
| 319 | 319 | Task { await store.snooze(event.id, until: duration.date(from: .now)) } |
octosentry/SecurityEventRow.swift
+9 −6
| @@ -13,7 +13,7 @@ struct SecurityEventRow: View { |
| 13 | 13 | var attribution: String? |
| 14 | 14 | var isHidden = false |
| 15 | 15 | var snoozedUntil: Date? |
| 16 | | var onMarkSeen: () -> Void |
| 16 | var onToggleSeen: () -> Void |
| 17 | 17 | var onDismiss: () -> Void = {} |
| 18 | 18 | var onSnooze: (SnoozeDuration) -> Void = { _ in } |
| 19 | 19 | var onRestore: () -> Void = {} |
| @@ -80,6 +80,9 @@ struct SecurityEventRow: View { |
| 80 | 80 | } |
| 81 | 81 | .padding(10) |
| 82 | 82 | .contentShape(Rectangle()) |
| 83 | // Seen alerts stay in the feed but recede, so the |
| 84 | // acknowledgement is visible rather than silently discarded. |
| 85 | .opacity(event.seenLocally ? 0.5 : 1) |
| 83 | 86 | } |
| 84 | 87 | .buttonStyle(.plain) |
| 85 | 88 | |
| @@ -93,12 +96,12 @@ struct SecurityEventRow: View { |
| 93 | 96 | .padding(.top, 12) |
| 94 | 97 | .padding(.trailing, 10) |
| 95 | 98 | } else { |
| 96 | | Button(action: onMarkSeen) { |
| 97 | | Image(systemName: "checkmark.circle") |
| 99 | Button(action: onToggleSeen) { |
| 100 | Image(systemName: event.seenLocally ? "checkmark.circle.fill" : "checkmark.circle") |
| 98 | 101 | } |
| 99 | 102 | .buttonStyle(.plain) |
| 100 | | .foregroundStyle(.secondary) |
| 101 | | .help("Mark as seen") |
| 103 | .foregroundStyle(event.seenLocally ? Color.accentColor : .secondary) |
| 104 | .help(event.seenLocally ? "Mark as unseen" : "Mark as seen") |
| 102 | 105 | .padding(.top, 12) |
| 103 | 106 | .padding(.trailing, 6) |
| 104 | 107 | |
| @@ -144,6 +147,6 @@ struct SecurityEventRow: View { |
| 144 | 147 | updatedAt: .now, |
| 145 | 148 | seenLocally: false |
| 146 | 149 | ), |
| 147 | | onMarkSeen: {} |
| 150 | onToggleSeen: {} |
| 148 | 151 | ) |
| 149 | 152 | } |
octosentry/SecurityEventStore.swift
+13 −4
| @@ -266,13 +266,22 @@ final class SecurityEventStore { |
| 266 | 266 | |
| 267 | 267 | /// Local-only triage state (spec §11) — no API write, no scope beyond |
| 268 | 268 | /// read needed. Removes the event from the active stream. |
| 269 | | func markSeen(_ eventID: String) async { |
| 269 | /// Acknowledges an alert without hiding it — the row stays, rendered as |
| 270 | /// seen. Dropping it from the feed only lasted until the next poll put it |
| 271 | /// back looking untouched; hiding an alert is what dismiss and snooze are |
| 272 | /// for. Toggleable, so acknowledging isn't a one-way door. |
| 273 | func setSeen(_ eventID: String, _ seen: Bool) async { |
| 270 | 274 | var state = await persistenceStore.load() |
| 271 | | state.seenEventIDs.insert(eventID) |
| 275 | if seen { |
| 276 | state.seenEventIDs.insert(eventID) |
| 277 | } else { |
| 278 | state.seenEventIDs.remove(eventID) |
| 279 | } |
| 272 | 280 | await persistenceStore.save(state) |
| 273 | 281 | |
| 274 | | rawEvents.removeAll { $0.id == eventID } |
| 275 | | totalFetchedCount = rawEvents.count |
| 282 | if let index = rawEvents.firstIndex(where: { $0.id == eventID }) { |
| 283 | rawEvents[index].seenLocally = seen |
| 284 | } |
| 276 | 285 | applyFilters() |
| 277 | 286 | } |
| 278 | 287 | |