krz/octosentry

macOS menu bar app to monitor GitHub security alerts github macos menubar security

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 {
313313 : nil,
314314 isHidden: store.triage.isHidden(event.id, now: .now),
315315 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) } },
317317 onDismiss: { Task { await store.dismiss(event.id) } },
318318 onSnooze: { duration in
319319 Task { await store.snooze(event.id, until: duration.date(from: .now)) }
octosentry/SecurityEventRow.swift +9 −6
@@ -13,7 +13,7 @@ struct SecurityEventRow: View {
1313 var attribution: String?
1414 var isHidden = false
1515 var snoozedUntil: Date?
16 var onMarkSeen: () -> Void
16 var onToggleSeen: () -> Void
1717 var onDismiss: () -> Void = {}
1818 var onSnooze: (SnoozeDuration) -> Void = { _ in }
1919 var onRestore: () -> Void = {}
@@ -80,6 +80,9 @@ struct SecurityEventRow: View {
8080 }
8181 .padding(10)
8282 .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)
8386 }
8487 .buttonStyle(.plain)
8588
@@ -93,12 +96,12 @@ struct SecurityEventRow: View {
9396 .padding(.top, 12)
9497 .padding(.trailing, 10)
9598 } else {
96 Button(action: onMarkSeen) {
97 Image(systemName: "checkmark.circle")
99 Button(action: onToggleSeen) {
100 Image(systemName: event.seenLocally ? "checkmark.circle.fill" : "checkmark.circle")
98101 }
99102 .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")
102105 .padding(.top, 12)
103106 .padding(.trailing, 6)
104107
@@ -144,6 +147,6 @@ struct SecurityEventRow: View {
144147 updatedAt: .now,
145148 seenLocally: false
146149 ),
147 onMarkSeen: {}
150 onToggleSeen: {}
148151 )
149152}
octosentry/SecurityEventStore.swift +13 −4
@@ -266,13 +266,22 @@ final class SecurityEventStore {
266266
267267 /// Local-only triage state (spec §11) — no API write, no scope beyond
268268 /// 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 {
270274 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 }
272280 await persistenceStore.save(state)
273281
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 }
276285 applyFilters()
277286 }
278287