diff options
| author | Christian Cleberg <[email protected]> | 2026-07-17 16:41:52 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-07-17 16:41:52 -0500 |
| commit | 2f72d3d0736a6fac306d4e5d9406cb33b08bc2a0 (patch) | |
| tree | ca6bfe8277883d87bc6e45a9393f19c642f3a502 /octosentry/SecurityEventListView.swift | |
| parent | d8862a9b78b0d6cf4e11cf537be7678794e08811 (diff) | |
| download | octosentry-0.3.0.tar.gz octosentry-0.3.0.tar.bz2 octosentry-0.3.0.zip | |
Add persistence, multi-repo watch list, and background polling0.3.0
Closes #1-#5 (milestones 0.2.0, 0.3.0).
- Flat JSON persistence (PersistedState/PersistenceStore) in Application
Support, chosen over SwiftData since the dataset is small and the
existing model types are plain Codable value types passed across actor
boundaries.
- Repo watch list, per-event seen-state, and last-fetch timestamps are
now persisted instead of living only in memory.
- Configurable minimum severity filter, applied from a cached raw fetch
so changing it doesn't require a network round-trip.
- Multi-repo support: SecurityEventStore now loops over a persisted watch
list instead of one hardcoded repo, with an in-popover UI (gear button)
to add/remove repos.
- Background polling every 15 minutes, layered on top of the existing
refresh-on-open and manual refresh, so the feed stays fresh even while
the popover is closed.
Diffstat (limited to 'octosentry/SecurityEventListView.swift')
| -rw-r--r-- | octosentry/SecurityEventListView.swift | 105 |
1 files changed, 100 insertions, 5 deletions
diff --git a/octosentry/SecurityEventListView.swift b/octosentry/SecurityEventListView.swift index 4542275..a5dba3e 100644 --- a/octosentry/SecurityEventListView.swift +++ b/octosentry/SecurityEventListView.swift @@ -8,16 +8,22 @@ import SwiftUI struct SecurityEventListView: View { var store: SecurityEventStore + @State private var showingRepoManager = false var body: some View { VStack(alignment: .leading, spacing: 0) { header Divider() - content + if showingRepoManager { + RepoManagerView(store: store) + } else { + content + } } .frame(width: 380, height: 420) .task { await store.refresh() + store.startPolling() } } @@ -33,13 +39,34 @@ struct SecurityEventListView: View { Spacer() + if !showingRepoManager { + Picker("Minimum severity", selection: Binding( + get: { store.minimumSeverity }, + set: { newValue in Task { await store.setMinimumSeverity(newValue) } } + )) { + ForEach(SecurityEventSeverity.allCases, id: \.self) { severity in + Text(severity.displayName).tag(severity) + } + } + .pickerStyle(.menu) + .labelsHidden() + .fixedSize() + + Button { + Task { await store.refresh() } + } label: { + Image(systemName: "arrow.clockwise") + } + .buttonStyle(.plain) + .disabled(store.isLoading) + } + Button { - Task { await store.refresh() } + showingRepoManager.toggle() } label: { - Image(systemName: "arrow.clockwise") + Image(systemName: showingRepoManager ? "xmark.circle" : "gearshape") } .buttonStyle(.plain) - .disabled(store.isLoading) Button("Quit") { NSApplication.shared.terminate(nil) @@ -56,7 +83,15 @@ struct SecurityEventListView: View { StatusView(systemImage: "exclamationmark.triangle", tint: .orange, message: store.errorMessages.joined(separator: "\n\n")) } else if store.events.isEmpty && !store.isLoading { VStack(spacing: 8) { - StatusView(systemImage: "checkmark.shield", tint: .green, message: "No open security alerts") + if store.totalFetchedCount > 0 { + StatusView( + systemImage: "line.3.horizontal.decrease.circle", + tint: .secondary, + message: "\(store.totalFetchedCount) alert(s) are below your minimum severity filter" + ) + } else { + StatusView(systemImage: "checkmark.shield", tint: .green, message: "No open security alerts") + } if !store.unavailableNotices.isEmpty { NoticeBanner(messages: store.unavailableNotices) .padding(.horizontal) @@ -84,6 +119,66 @@ struct SecurityEventListView: View { } } +private struct RepoManagerView: View { + var store: SecurityEventStore + @State private var newRepoText = "" + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + Text("Watched Repositories") + .font(.subheadline.weight(.semibold)) + + if store.watchedRepos.isEmpty { + Text("No repos watched yet.") + .font(.callout) + .foregroundStyle(.secondary) + } else { + ForEach(store.watchedRepos, id: \.self) { repo in + HStack { + Text(repo) + .font(.callout) + Spacer() + Button { + Task { await store.removeRepo(repo) } + } label: { + Image(systemName: "minus.circle.fill") + .foregroundStyle(.red) + } + .buttonStyle(.plain) + } + } + } + + Divider() + + HStack { + TextField("owner/repo", text: $newRepoText) + .textFieldStyle(.roundedBorder) + .onSubmit(addRepo) + + Button("Add", action: addRepo) + .disabled(newRepoText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) + } + + if let errorMessage = store.watchListErrorMessage { + Text(errorMessage) + .font(.caption2) + .foregroundStyle(.red) + } + + Spacer() + } + .padding(12) + .frame(maxWidth: .infinity, alignment: .leading) + } + + private func addRepo() { + let text = newRepoText + newRepoText = "" + Task { await store.addRepo(text) } + } +} + private struct ErrorBanner: View { let messages: [String] |
