krz/octosentry
macOS menu bar app to monitor GitHub security alerts
clone: git clone https://gitbay.org/krz/octosentry.git
b4dbbd307fab4edf0958d35032dbaa0f6450785b
signed_unknown_key
author: Christian Cleberg <hello@cleberg.net> · 2026-08-22T23:06:41Z
committer: <noreply@github.com>
octosentry/SecurityEventListView.swift | 2 +- octosentry/SecurityEventRow.swift | 15 +++++++++------ octosentry/SecurityEventStore.swift | 17 +++++++++++++---- 3 files changed, 23 insertions(+), 11 deletions(-) @@ -313,7 +313,7 @@ struct SecurityEventListView: View { : nil, isHidden: store.triage.isHidden(event.id, now: .now), snoozedUntil: store.triage.snoozedUntil(event.id, now: .now), - onMarkSeen: { Task { await store.markSeen(event.id) } }, + onToggleSeen: { Task { await store.setSeen(event.id, !event.seenLocally) } }, onDismiss: { Task { await store.dismiss(event.id) } }, onSnooze: { duration in Task { await store.snooze(event.id, until: duration.date(from: .now)) } @@ -13,7 +13,7 @@ struct SecurityEventRow: View { var attribution: String? var isHidden = false var snoozedUntil: Date? - var onMarkSeen: () -> Void + var onToggleSeen: () -> Void var onDismiss: () -> Void = {} var onSnooze: (SnoozeDuration) -> Void = { _ in } var onRestore: () -> Void = {} @@ -80,6 +80,9 @@ struct SecurityEventRow: View { } .padding(10) .contentShape(Rectangle()) + // Seen alerts stay in the feed but recede, so the + // acknowledgement is visible rather than silently discarded. + .opacity(event.seenLocally ? 0.5 : 1) } .buttonStyle(.plain) @@ -93,12 +96,12 @@ struct SecurityEventRow: View { .padding(.top, 12) .padding(.trailing, 10) } else { - Button(action: onMarkSeen) { - Image(systemName: "checkmark.circle") + Button(action: onToggleSeen) { + Image(systemName: event.seenLocally ? "checkmark.circle.fill" : "checkmark.circle") } .buttonStyle(.plain) - .foregroundStyle(.secondary) - .help("Mark as seen") + .foregroundStyle(event.seenLocally ? Color.accentColor : .secondary) + .help(event.seenLocally ? "Mark as unseen" : "Mark as seen") .padding(.top, 12) .padding(.trailing, 6) @@ -144,6 +147,6 @@ struct SecurityEventRow: View { updatedAt: .now, seenLocally: false ), - onMarkSeen: {} + onToggleSeen: {} ) } @@ -266,13 +266,22 @@ final class SecurityEventStore { /// Local-only triage state (spec §11) — no API write, no scope beyond /// read needed. Removes the event from the active stream. - func markSeen(_ eventID: String) async { + /// Acknowledges an alert without hiding it — the row stays, rendered as + /// seen. Dropping it from the feed only lasted until the next poll put it + /// back looking untouched; hiding an alert is what dismiss and snooze are + /// for. Toggleable, so acknowledging isn't a one-way door. + func setSeen(_ eventID: String, _ seen: Bool) async { var state = await persistenceStore.load() - state.seenEventIDs.insert(eventID) + if seen { + state.seenEventIDs.insert(eventID) + } else { + state.seenEventIDs.remove(eventID) + } await persistenceStore.save(state) - rawEvents.removeAll { $0.id == eventID } - totalFetchedCount = rawEvents.count + if let index = rawEvents.firstIndex(where: { $0.id == eventID }) { + rawEvents[index].seenLocally = seen + } applyFilters() }