From 705c6029ab30adf094e6324006b9fb682d8189f2 Mon Sep 17 00:00:00 2001 From: Christian Cleberg Date: Fri, 17 Jul 2026 17:34:22 -0500 Subject: Add repo picker, update checker, and distribution tooling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #11-#15 (milestones 0.6.0, 0.7.0, 1.0.0). - Repo picker: on-demand broader OAuth scope (security_events repo), requested only when the "Browse your repos" action is used, never by default. Lists /user/repos via the existing pagination helper. Granted scope persisted with backward-compatible decoding for existing state files. Fixed a bug where a failed re-auth force-signed-out a user who already had a valid narrower-scope token. - Update checker: polls this repo's GitHub Releases API, surfaces a banner linking to new releases. Skipped on the Mac App Store build via a runtime receipt check rather than a separate build configuration. - Fixed MARKETING_VERSION, stuck at Xcode's default "1.0" this whole time unrelated to our git tags — now 1.0.0, matching this release. - Added PrivacyInfo.xcprivacy (no tracking, no collected data). - Added scripts/build-dmg.sh (archive, Developer ID export, notarize, staple) and Casks/octosentry.rb (Homebrew Cask template), plus DISTRIBUTION.md documenting both channels end to end. Entitlements were already identical across all builds — no divergence needed there. What remains for actual App Store submission and notarized DMG builds is account-specific (Apple Developer Program membership, certificates, App Store Connect submission) and can't be done from here; documented clearly in DISTRIBUTION.md. --- octosentry/SecurityEventListView.swift | 123 +++++++++++++++++++++++++++++++-- 1 file changed, 116 insertions(+), 7 deletions(-) (limited to 'octosentry/SecurityEventListView.swift') diff --git a/octosentry/SecurityEventListView.swift b/octosentry/SecurityEventListView.swift index d817370..84e6638 100644 --- a/octosentry/SecurityEventListView.swift +++ b/octosentry/SecurityEventListView.swift @@ -9,6 +9,7 @@ import SwiftUI struct SecurityEventListView: View { var store: SecurityEventStore var authStore: AuthStore + var updateStore: UpdateStore var isStandaloneWindow: Bool = false @State private var showingRepoManager = false @Environment(\.openWindow) private var openWindow @@ -16,6 +17,9 @@ struct SecurityEventListView: View { var body: some View { VStack(alignment: .leading, spacing: 0) { header + if let release = updateStore.availableRelease { + UpdateBanner(release: release) + } Divider() if !authStore.isSignedIn { SignInView(authStore: authStore) @@ -30,6 +34,9 @@ struct SecurityEventListView: View { await store.refresh() store.startPolling() } + .task { + await updateStore.checkForUpdate() + } } private var header: some View { @@ -142,6 +149,10 @@ private struct RepoManagerView: View { var store: SecurityEventStore var authStore: AuthStore @State private var newRepoText = "" + @State private var isBrowsingRepos = false + @State private var availableRepos: [String] = [] + @State private var isLoadingRepos = false + @State private var browseErrorMessage: String? var body: some View { VStack(alignment: .leading, spacing: 10) { @@ -171,13 +182,24 @@ private struct RepoManagerView: View { Divider() - HStack { - TextField("owner/repo", text: $newRepoText) - .textFieldStyle(.roundedBorder) - .onSubmit(addRepo) + if isBrowsingRepos { + browsingContent + } else { + HStack { + TextField("owner/repo", text: $newRepoText) + .textFieldStyle(.roundedBorder) + .onSubmit(addRepo) + + Button("Add", action: addRepo) + .disabled(newRepoText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) + } - Button("Add", action: addRepo) - .disabled(newRepoText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) + Button(action: startBrowsing) { + Label("Browse your repos", systemImage: "list.bullet") + .font(.caption) + } + .buttonStyle(.plain) + .foregroundStyle(Color.accentColor) } if let errorMessage = store.watchListErrorMessage { @@ -200,6 +222,75 @@ private struct RepoManagerView: View { .frame(maxWidth: .infinity, alignment: .leading) } + @ViewBuilder + private var browsingContent: some View { + VStack(alignment: .leading, spacing: 6) { + HStack { + Text("Your Repositories") + .font(.caption.weight(.semibold)) + Spacer() + Button { + isBrowsingRepos = false + } label: { + Image(systemName: "xmark.circle") + } + .buttonStyle(.plain) + } + + if isLoadingRepos { + ProgressView() + .controlSize(.small) + .frame(maxWidth: .infinity) + } else if let browseErrorMessage { + Text(browseErrorMessage) + .font(.caption2) + .foregroundStyle(.red) + } else { + let selectableRepos = availableRepos.filter { !store.watchedRepos.contains($0) } + if selectableRepos.isEmpty { + Text("All visible repos are already watched.") + .font(.caption2) + .foregroundStyle(.secondary) + } else { + ScrollView { + LazyVStack(alignment: .leading, spacing: 4) { + ForEach(selectableRepos, id: \.self) { repo in + Button { + Task { await store.addRepo(repo) } + isBrowsingRepos = false + } label: { + Text(repo) + .font(.callout) + .frame(maxWidth: .infinity, alignment: .leading) + } + .buttonStyle(.plain) + } + } + } + .frame(maxHeight: 160) + } + } + } + } + + private func startBrowsing() { + guard authStore.hasRepoAccess else { + authStore.requestRepoAccess() + return + } + isBrowsingRepos = true + isLoadingRepos = true + browseErrorMessage = nil + Task { + do { + availableRepos = try await store.fetchAccessibleRepos() + } catch { + browseErrorMessage = (error as? LocalizedError)?.errorDescription ?? error.localizedDescription + } + isLoadingRepos = false + } + } + private func addRepo() { let text = newRepoText newRepoText = "" @@ -207,6 +298,24 @@ private struct RepoManagerView: View { } } +private struct UpdateBanner: View { + let release: UpdateChecker.LatestRelease + + var body: some View { + Button { + NSWorkspace.shared.open(release.htmlURL) + } label: { + Label("Update available: \(release.version)", systemImage: "arrow.down.circle.fill") + .font(.caption) + .frame(maxWidth: .infinity, alignment: .leading) + } + .buttonStyle(.plain) + .foregroundStyle(.blue) + .padding(8) + .background(.blue.opacity(0.1)) + } +} + private struct ErrorBanner: View { let messages: [String] @@ -262,6 +371,6 @@ private struct StatusView: View { } #Preview { - SecurityEventListView(store: SecurityEventStore(), authStore: AuthStore()) + SecurityEventListView(store: SecurityEventStore(), authStore: AuthStore(), updateStore: UpdateStore()) .frame(width: 380, height: 420) } -- cgit v1.2.3