Imported from ricoberger/Alertmanager (
AGENTS.md). Install upstream withnpx skills add ricoberger/Alertmanager. Copyright stays with the author.
AGENTS.md
Guidance for coding agents working in this repository.
Project
Alertmanager is a native macOS app (SwiftUI + SwiftData) for viewing alerts from Prometheus Alertmanager and Grafana Alerting.
- Bundle ID:
de.ricoberger.Alertmanager - Swift: 5.0, macOS deployment target: 26.4
- Project file:
Alertmanager.xcodeproj(no Swift Package Manager dependencies) - Persistence: SwiftData store at
~/Library/Application Support/de.ricoberger.Alertmanager/default.sqlite - The app is non-sandboxed by design — token retrieval via shell commands
(
/bin/sh -c …) requiresProcesswhich is unavailable in the sandbox.
Build, test, run
All commands run from the repo root.
# Build (Debug, macOS)
xcodebuild -project Alertmanager.xcodeproj -scheme Alertmanager -configuration Debug build
# Build (Release)
xcodebuild -project Alertmanager.xcodeproj -scheme Alertmanager -configuration Release archive -archivePath build/Alertmanager.xcarchive
# Run all tests
xcodebuild -project Alertmanager.xcodeproj -scheme Alertmanager test
# Unit tests only (Swift Testing)
xcodebuild test -project Alertmanager.xcodeproj -scheme Alertmanager -only-testing:AlertmanagerTests
# UI tests only (XCTest)
xcodebuild test -project Alertmanager.xcodeproj -scheme Alertmanager -only-testing:AlertmanagerUITests
Unit tests under AlertmanagerTests/ use the Swift Testing framework
(import Testing, @Test). UI tests under AlertmanagerUITests/ use
XCTest (XCUIApplication, XCTAssert…). Don't mix the two — match whatever
the surrounding file uses.
Running xcodebuild locally is the source of truth for green.
Code layout
Alertmanager/
AlertmanagerApp.swift @main entry point; wires ModelContainer,
MenuBarExtra, Settings scene, menu commands.
ContentView.swift Main window NavigationSplitView + lifecycle hooks
(polling start, import/export, notification taps).
MenuBarContentView.swift 500x600 popup shown from the menu bar extra.
Models/
Alertmanager.swift @Model — a configured backend (Prom or Grafana).
AuthenticationType + TokenSource enums live here.
Alert.swift GettableAlert DTO mirroring /api/v2/alerts.
Filter.swift @Model — predicate set; LabelMatcher parser.
Services/
AlertmanagerService.swift Stateless HTTP client. Prom + Grafana variants,
auth header construction, token resolution.
AlertsManager.swift Singleton. Polling timers + per-AM alert cache.
Posts `.alertsDidUpdate` after every fetch.
NotificationService.swift Singleton. Diffs alerts per filter, posts
local UNNotifications with deep-link actions.
AlertAggregator.swift Pure helper: collect+dedupe+filter from cache.
AlertDeepLinks.swift Pure helper: source/silence/dashboard/panel URLs.
AlertMarkdown.swift Pure helper: alert → markdown snippet for copying.
SettingsManager.swift @Published wrapper around UserDefaults.
ImportExportManager.swift JSON v1.0 export/import; references by name.
UpdateCheckService.swift Singleton. One-shot GitHub release probe that
feeds the in-app update banner.
APIServer.swift Singleton. Opt-in loopback HTTP API (NWListener)
exposing alertmanagers/filters/alerts + Analyze.
APIModels.swift Pure HTTP request/response + routing + JSON DTOs
for APIServer (unit-testable, no app state).
Views/ SwiftUI views (form sheets, detail panes, rows).
AlertmanagerTests/ Swift Testing — model + service logic.
AlertmanagerUITests/ XCTest — driven via accessibilityIdentifiers.
Architecture notes that aren't obvious from the code
- Singletons by design:
AlertsManager.shared,NotificationService.shared, andSettingsManager.sharedare intentionally process-wide. Polling state and notification baselines must outlive any individual view and be shared between the main window and theMenuBarExtra. Don't refactor them to per-view objects. - Polling: one repeating
TimerperAlertmanager.id, keyed inAlertsManager.refreshTimers.startMonitoringis idempotent and safe to call from multiple.onAppearhandlers — it replaces any existing timer. Concurrent fetches for the same AM are deduplicated viainFlightTasks. - Two backend shapes: a "standard" Prometheus Alertmanager hits
/api/v2/alertsdirectly; a "Grafana" entry queries the single datasource UID stored ingrafanaAlertmanagervia/api/alertmanager/{uid}/api/v2/alerts. Alerts are not tagged with their source — views andNotificationServicere-resolve the producing AM by scanning the per-AM caches (AlertAggregator.alertsWithSources,resolveAlertmanager). Grafana silence deep-links need the datasource name, whichAlertmanagerService.resolveSilenceURLresolves from the UID at link-build time (the built-in"grafana"value is used verbatim). - Notification flow:
AlertmanagerApp.onAppearcallsNotificationService.shared.configure(with:).NotificationServicesubscribes to.alertsDidUpdateand re-evaluates every filter on every fetch (not just the one currently visible).- First fetch for a filter is a silent baseline — no notifications. A
filter is only baselined once all the AMs it covers have completed at
least one fetch attempt — successful or failed
(
AlertsManager.hasCompletedFetchByAlertmanager) — so partial data doesn't seed a bad baseline, while a permanently failing AM can't block notifications for the healthy ones. Stale AM IDs that no longer resolve are ignored; an empty selection covers all AMs. - 31 notification categories (
ALERT_1…ALERT_31) are pre-registered (ALERT_0— no actions — is deliberately left unregistered); each notification picks the one whose action set matches the URLs it actually has (bitmask: source=1, silence=2, runbook=4, dashboard=8, panel=16). When adding a new deep-link action, update bothregisterNotificationCategoriesandcategoryIdentifier(for:).
- Menu commands → NotificationCenter:
AlertmanagerApp.commandspostsNotification.Nameevents (.addAlertmanager,.importConfiguration,.openAlertDetail, …) whichContentViewobserves. This indirection exists because theCommandGroupbuilder can't capture view bindings. All custom event names are declared inAlertsManager.swiftat file top. - SwiftData + associated-value enums: SwiftData can't persist enums with
associated values (
AuthenticationType,[LabelMatcher]). The pattern used here is: store a privateData?backing property, expose a@Transientcomputed accessor that JSON-encodes/decodes on access, fall back to a safe default on decode failure with aprintlog. Mirror this pattern if you need to add another such field. @MainActoris the project-wide default. Most types are implicitly main-actor isolated. Delegate methods that the system calls on background queues (e.g.UNUserNotificationCenterDelegate) are markednonisolatedand hop back to@MainActorviaTask { @MainActor in … }.Alertmanager(@Model) is notSendable. Where it must cross an isolation boundary (e.g. into aTimerclosure or asyncTask), usenonisolated(unsafe) let ref = …and hop back to@MainActorimmediately inside the closure. SeeAlertsManager.startMonitoringfor the canonical pattern.- Import/export references by name, not UUID:
ExportFilterstoresalertmanagerNames(not IDs) andExportSettingsstoresmenuBarFilterName(not UUID). This lets an export be re-imported into a store with different IDs. Preserve this when changing the export format and bumpExportData.versionon incompatible changes. - HTTP API server (
APIServer): opt-in (off by default;SettingsManager.apiServerEnabled). A hand-rolled HTTP/1.1 server on Network.framework'sNWListener— no SPM dependency — bound to127.0.0.1:9093(loopback only, no auth; port collides with a local Prometheus Alertmanager by design).APIServer.shared.startFromSettings()subscribes to the toggle and starts/stops the listener view-independently;start()also (idempotently)startMonitorings every AM so responses don't depend on any view having appeared. The class is@MainActorfor state access; the socket read/write loop isnonisolatedand hops to the main actor only to route a parsed request. Transport primitives, routing, and the encode-only JSON DTOs live inAPIModels.swift(pure, unit-tested). Endpoints (/api, nested REST): list alertmanagers/filters, alerts per AM/filter (each alert carries curated fields + credential-free markdown + keyedactions+analysisstate + fullrawpayload),GET /api/analyses(all analysis files on disk, parsed from filenames), plusPOST …/analyze(202/409/422/404) andGET …/analysis. Bind failures are logged viaprintonly. When adding an endpoint, extendAPIRoute.match(path→route) andAPIServer.route(dispatch) together.
Conventions
- Style: 4-space indent for
.swift, 2 for everything else (see.editorconfig). LF line endings, final newline, trim trailing whitespace. - Doc comments: this codebase has thorough triple-slash (
///) docs on types, methods, and non-trivial properties. New public-ish API should match that style. Don't strip existing docs. - Inline comments: explain why (non-obvious invariants, ordering constraints, workarounds). Don't explain what — the code already does.
- Errors: bubble typed
LocalizedErrorcases (AlertmanagerError) soerrorDescriptionis directly displayable. Don'ttry?-swallow in service code; log + propagate. - Logging:
print(…)is used throughout. There is noos.Loggerwrap; follow the existing style and prefix the source ("NotificationService: …") for grep-ability. - Tests:
- Unit tests use Swift Testing (
@Test func,#expect). Test files@testable import Alertmanager. - UI tests rely on
accessibilityIdentifierstrings (e.g."sidebar-list","menubar-no-filter-state"). When you add or rename a view that a UI test exercises, update the identifier in lockstep.
- Unit tests use Swift Testing (
Things to be careful with
- Don't sandbox the app. Adding entitlements that re-enable the sandbox
breaks
TokenSource.command(usesProcess+/bin/sh -c). - Don't change the
buildServer.jsongitignore policy without thought — it's intentionally ignored because it embeds an absolute DerivedData path. - Don't reorder polling lifecycle: in
ContentView.deleteAlertmanagers,AlertsManager.stopMonitoringmust run beforemodelContext.deleteso the timer can't fire against a deleted entity. - Don't
unionthe seen-set inNotificationService.checkForNewAlerts. Replacing (not unioning) lets resolved-then-refiring alerts re-notify, which is the intended UX. The current code replaces on purpose. - Don't add build/test CI workflows without confirming intent — the
previous
.github/workflows/build.yamlwas removed deliberately in the reimplementation branch. The existingcontinuous-delivery.yamlandrelease.yamlworkflows cover packaging and releases only.
When in doubt
Match the surrounding code. The codebase is small, internally consistent, and heavily commented; the existing patterns are usually the answer.