The tree was a debranded snapshot of upstream taken around 2026-02-20, and upstream has moved about five months since. This replaces it with upstream master, mechanically re-derived: bin/debrand does the rename, and the pipeline and squashed migrations are restored from the shipped tree. Not a merge. The previous import replaced the tree wholesale in a single commit, so there is no shared content history to merge against. Reproducing the rename on the new tree is what actually converges. What survives: .hanzo/workflows, the Dockerfiles and bin/docker* entrypoints, compose files, LLM.md, and insights/migrations. The squashed initial migration is load-bearing — the live database records it by name, so upstream's ~918 individual migrations cannot come back. Nine products are dropped as unreferenced. Twenty-one more that we do not run are kept anyway because the core app still imports them; unpicking those is a refactor and does not belong in a rebase. This base does not build yet, and the reasons are not in the rename: upstream now wants Python 3.13.13 against our 3.12.12 and Django 5.2 against our 4.2, and the dependency manifests still name upstream packages that we republish under our own names. Those are decisions, not renames.
8.5 KiB
Shared alerting architecture
Use this reference to decide where code belongs before editing it.
Layer map
| Layer | Location | Owns |
|---|---|---|
| Pure lifecycle decisions | products/alerts/backend/state_machine.py |
State transitions, policy decisions, and notification actions |
| Shared alert infrastructure | products/alerts/backend/ |
Scheduling math, destination configuration and persistence, internal-event delivery, email transport, insight alert models/API, and insight evaluation |
| Product adapter | products/<name>/backend/ |
Domain evaluation, model snapshots, the single mutator, event payloads, allowed destinations, due queries, history, and orchestration |
| Shared alert creation UI | frontend/src/lib/components/Alerting/AlertWizard/ |
Reusable InsightsFunction destination, trigger, and configuration flow |
| Shared product alert UI | products/alerts/frontend/components/ |
Container-agnostic editor layout, definition primitives, advanced options, destination editor, schedule presentation, and evaluation chart |
| Product UI | products/<name>/frontend/ or frontend/src/scenes/<name>/ |
Form logic, API calls, product fields, normalized adapters, entry points, detail tables, and wizard configuration |
products/logs is the reference adopter for fixed-cadence scheduling, InsightsFunction destinations, delivery rollback, product-owned Temporal orchestration, and the shared product alert editor components.
Logs and insight alerts both adapt their product state to the shared lifecycle engine. Insight alerts are also the reference adopter for shared calendar anchors, schedule restrictions, weekend skipping, and email delivery. Their model, API, query evaluation, and Django scheduling adapters live in products/alerts/backend/ and insights/tasks/alerts/. The evaluation package is shared across insight query kinds, but it is not a generic evaluator for unrelated products.
Frontend contract
Shared product alert UI lives in products/alerts/frontend/components/. It is presentation infrastructure, not a frontend product registry.
AlertEditor,AlertEditorFormDetails, andAlertEditorSectionprovide the container-agnostic form shell.AlertDefinition*components provide composable definition, schedule, next-evaluation, and timezone presentation.AlertAdvancedOptionsowns shared collapse and enabled-count behavior.AlertNotificationDestinationEditorrenders normalized saved and pending destinations.AlertEvaluationHistoryChartrenders normalized evaluation points and current thresholds.
Products own keyed kea logic, API calls, form schemas, source/filter controls, supported destination types, InsightsFunction payloads, threshold conversion, enabled-count calculation, and history tables. Keep modals, scene widths, and embedded-section sizing in the product surface.
AlertWizard remains the shared InsightsFunction creation flow. Adopters provide keyed alertWizardLogic props with supported sub-template IDs, WizardTrigger[], WizardDestination[], and optional URL or preset behavior. Backend destination support alone does not make an option available in the wizard.
Read frontend-alerting.md before adding or extending a product alert frontend. Follow frontend/src/AGENTS.md, use generated API types, and guard network submissions from double clicks.
Lifecycle contract
products/alerts/backend/state_machine.py is pure Python.
CheckInputnormalizes one product evaluation.AlertSnapshotcontains only fields needed for lifecycle decisions.AlertPolicyrepresents deliberate differences between adopters. Its defaults are existing behavior, not a menu of speculative options.evaluate_alert_check(...)andevaluate_alert_failure(...)returnAlertCheckOutcomewithout persistence.- Control-plane helpers such as
apply_enable,apply_disable,apply_snooze, andapply_threshold_changereturnControlPlaneOutcome. NotificationActiontells the product which event to deliver.
The product converts its model to a snapshot and applies the outcome through one local apply_outcome function. Extend .semgrep/rules/security/alert-state-must-go-through-state-machine.yaml when another product adopts this contract.
Error behavior is load-bearing:
- Failed checks do not clear an already firing alert.
- Inconclusive checks preserve state and failure count.
- Transient errors are silent unless the policy opts into counting them.
- Error notification normally occurs on the first failure edge.
- Delivery failure must not consume a lifecycle or failure-counter edge that should be retried.
Destination contract
Product-facing destination setup is exported from products.alerts.backend.facade.api:
validate_destination_databuild_alert_destination_configcreate_alert_destination_insights_functionssoft_delete_alert_destinationssoft_delete_all_alert_destinationssend_alert_email
EventKindSpec describes destination-neutral content for one event kind. The shared builder converts it into Slack, Discord, webhook, or Microsoft Teams InsightsFunction payloads. Products own event IDs, event properties, wording, actions, and their allowed destination list.
Deletion is fail-closed. Always scope it with team_id, alert_id, and the product's allowed event IDs.
Delivery contract
InsightsFunction notification workers use products.alerts.backend.destinations directly:
produce_alert_internal_event(...)returns aProduceResultorNone.flush_alert_internal_events(...)flushes the shared producer. Batch workers should flush once per produced batch.alert_internal_event_delivered(...)checks whether the producer acknowledged each internal event after the flush.- The product persists notification-dependent lifecycle changes only for acknowledged internal events.
This acknowledgement confirms production to the internal-event transport, not downstream InsightsFunction execution or final Slack, Discord, webhook, or Microsoft Teams delivery. The helpers log and capture producer failures. The product owns rollback, retry timing, schedule advancement, and check-history semantics.
Email callers use send_alert_email(...) through the facade. The caller owns recipients, authorization, subject, template, context, error handling, and a stable campaign_key for the required retry and deduplication behavior.
Scheduling contract
products/alerts/backend/scheduling.py is pure Python and owns reusable scheduling math:
compute_shard_offset_seconds(...)deterministically assigns a UUID-keyed alert to scheduler ticks.advance_next_check_at(...)advances from the prior schedule, skips missed intervals, snaps to the midnight-anchored cadence grid, and applies the shard offset.CalendarIntervalandto_calendar_interval(...)define the product-facing interval contract.next_calendar_check_time(...)computes fixed-cadence or team-local daily, weekly, and monthly anchors while preserving wall-clock behavior across DST changes.validate_and_normalize_schedule_restriction(...)andparse_blocked_windows_tuples(...)validate product payloads and produce the pure blocked-window contract.scan_next_unblocked_utc(...),is_utc_datetime_blocked(...), andis_weekend(...)apply timezone-aware restrictions without importing Django models.
Use the scheduler's real interval when computing shards. Use the same shard function when creating, updating, and advancing an alert.
Products translate their enums and stored payloads into the shared contract. Due eligibility, schedule persistence, retry behavior, and orchestration stay product-specific because model fields, states, snooze behavior, and tenant constraints differ.