Preserve exact upstream 23bf99e1ba and all reviewed local history. Resolve the sole agent-lifecycle contract conflict by retaining both independent accounts; no frontend source resolution is required. Bound this transaction to the upstream frontier recorded at batch start.
Change-source: pulse-maintainer
The Thresholds page builds its PUT /api/alerts/config body from the
settings it edits. It has no control for flapping detection, alert TTL
cleanup or custom rules, so it never sends flappingEnabled,
maxAlertAgeDays, maxAcknowledgedAgeDays, autoAcknowledgeAfterHours or
customRules. The handler decoded the body into a zero AlertConfig, so
every thresholds save turned flapping detection and TTL cleanup off,
reset the flapping tuning, dropped custom rules, and persisted that.
The page's flapping, aggregation and ioNormalization objects are not
AlertConfig fields and never enabled anything.
The handler now hands the body to Manager.ApplyConfigUpdate, which
reads the stored config, merges and applies it under the manager lock,
so two concurrent partial saves cannot revert each other. The body is
decoded on its own, so the keys it carries mean exactly what
encoding/json makes of them; every top-level field it left out is
copied from a JSON round trip of the stored config, sharing no maps
with it. ApplyConfigUpdate returns a cloned snapshot, which the handler
persists, because SaveAlertConfig normalizes the config it is handed in
place and used to write into the live config's maps. Saves run one at
a time so alerts.json ends with the last applied config. A body that
is not a JSON object, null included, is a 400. Stored configs are not
rewritten, so a value an earlier save zeroed stays as it is.
On a private stack, a node CPU change saved from the Thresholds page
zeroed all four fields in memory and in alerts.json on main; with this
change the same save kept them and stored the new threshold.
Treat explicit empty origins as a durable same-origin policy, preserve omitted fields and deployment overrides, and expose the effective value. Resolve webhook masks consistently across Create, Update and Test; reject unresolved or ambiguous identities before mutation or delivery while retaining masked responses, per-key edits, clears and encrypted persistence. Include real middleware/reload and HTTP Test/edit/delivery controls with all affected and shared dependent contracts.
Change-source: pulse-maintainer
The combined-tree race check exposed the retained clear-header fixture still expecting an empty Content-Type. Upstream correctly removes cleared headers so the JSON default survives; align only that fixture expectation, and fix the strict helper comment. Preserve source-rs9sv1rz failure and do not change production normalization or rejection behavior.
Contract-Neutral: Test expectation and helper-comment reconciliation only; cleared header normalization is already supplied by reviewed upstream.
Change-source: pulse-maintainer
Rename the strict update helper definition to match its resolved callers and compare the masked API response with its decoded custom-field projection. Upstream create/test restoration and retained update rejection checks stay separate.
Contract-Neutral: Integration-only helper binding and test expectation corrections.
Change-source: pulse-maintainer
Give the retained strict update helper a distinct name from upstream create/test restoration and close the retained connected test before the upstream tests. This additive correction repairs the integration only; both reviewed behaviors remain represented.
Contract-Neutral: Integration syntax and helper naming only; strict update-mask validation and upstream response masking remain unchanged.
Change-source: pulse-maintainer
Preserve exact 1896f1f2ba history. Resolve the webhook overlap by retaining the reviewed strict update restoration for unknown or ambiguous masks and upstream masked responses plus create/test restoration. Preserve both test groups; align response assertions with masked output and test clearing separately from the retained unknown-mask rejection cases.
Contract-Neutral: Conflict resolution preserves per-key webhook edits, persistence-before-publication, strict uncertain-mask rejection and response confidentiality; no route or feature is added.
Change-source: pulse-maintainer
The webhook editor sends back the ***REDACTED*** values the list returns
for every header, custom field and signing secret the user did not
retype. Three paths mishandled them:
- Update restored the whole saved header or custom-field map whenever any
value was masked, silently dropping a header or field added in the same
edit.
- Update echoed the restored saved custom fields, so a Pushover token and
user key came back in the response even though the list masks them, in
breach of the api-contracts webhook response clause. Create echoed the
request, and could store a masked value verbatim.
- The editor's Test button posts the form, so testing an edit after a
reload sent the literal mask as the token or header.
Resolve each masked value to the saved value of the same key on update and
on a form test that carries a saved webhook's id, keep values typed in the
same request, and drop masks with no saved value. Build list, create and
update responses from one masked shape. Normalization now drops blank
headers, so clearing a header removes it instead of sending an empty
Content-Type that generic receivers cannot parse.
Construct replacement-map requests explicitly rather than applying WebhookConfig omitempty before the handler. Distinguish omitted maps from empty maps while retaining the original expected clearing semantics. The complete first connected lifecycle control passes; both initial aggregate fixture failures remain retained.
Change-source: pulse-maintainer
The initial focused run stopped at a mock oracle expecting an empty map although existing JSON and configuration normalisation retain nil. Keep its failed proof, preserve replacement-map deletion semantics, and check the canonical submitted response fields.
Change-source: pulse-maintainer
Use the real update entry point for unrecoverable masks and let the connected control retain both response-confidentiality and durable-edit failures against the exact parent handler.
Change-source: pulse-maintainer
Restore only the submitted masked header or custom-field value instead of silently reverting the whole map. Reject unresolved masks before saving, retain explicit removals and case-correct header edits, and withhold restored custom-field credentials from the response. Connect encrypted form editing/reload to grouped and individual firing/recovery and keep admitted queue snapshots unchanged.
Change-source: pulse-maintainer
Handle nullable retry-row diagnostics while checking the actual attempt audit and DLQ error; verify omitted stored API keys through the established redacted response type and preserve authorised endpoint/target round-trips. Retain the original diagnostic repair and its adverse proof.
Change-source: pulse-maintainer
Suppress raw CLI targets/output, HTTP response bodies and credential-bearing endpoint paths across firing, recovery, tests, queue audits and settings logs. Preserve exact delivery inputs, typed causes and retry classification; retain structured status, counts and safe validation reasons.
Contract-Neutral: Shared agent-lifecycle and storage-recovery references are unchanged; this diagnostic repair alters no agent or storage behaviour, API schema, destination admission or delivery policy.
Change-source: pulse-maintainer
Recovery handlers retained the startup monitor after router reload, unlike normal notification handlers. Stopping that monitor cleared its queue and left Retry/Dismiss returning 503 even when the replacement notifier was available. Refresh the stable handler in both replacement paths and synchronise its owner reads. Add stopped-owner regression, concurrent replacement and scope proofs; keep queue persistence semantics unchanged.
Change-source: pulse-maintainer
Filter canonical history before selecting occurrences, preserve source evidence
and expose bounded reads and failures. Reconcile duplicate saved shells without
splitting one alert lifecycle, and keep note identity and canonical risk intact.
Carry attributed operator notes into Assistant. Preserve mobile investigations
across layout changes, transfer composer focus on handoff and keep long event
text readable. Record scoped qualification and its unresolved wider limits.
Refs #1782
Literal-only query matching and the separate resolved ntfy transport caller leaked recognised URL credentials. Decode each query name once and project ntfy transport errors before logging or returning them, without changing destinations or error causes. Add synthetic sink-matrix and HTTP projection regressions plus the bounded notification contract in this commit.
Change-source: pulse-maintainer
Pin useful diagnostic context and credential masking at the HTTP boundary, and document the API, agent and retained-history boundaries omitted from a2dbb27. This supplement must be packaged with that source commit for per-commit governance; it does not clear the historical hosted failure.
Change-source: pulse-maintainer
PR #1959 exposed that the URL-only redactor was called with a complete delivery error, causing safe but unhelpful replacement of the whole diagnostic. Redact embedded webhook URLs separately so credentials stay masked and non-secret failure context remains available; malformed URLs continue to fail closed.
Change-source: pulse-maintainer
Adds internal/alerts/eventlog: a SQLite-backed, additive event log that
records lifecycle transitions (resolved, acknowledged, unacknowledged,
escalated, flapping detected) and notification decisions (dispatched,
deferred by quiet hours, suppressed — with the mechanism that held
them). Appends never block alert evaluation: a full buffer drops the
event and counts the drop; a store that fails to open degrades to
recording nothing. 90-day retention, hourly prune.
The manager emits at the existing funnels only — dispatchAlert and the
safe-call resolve/ack/escalate seams — so no lifecycle behavior
changes. Lifecycle "fired" is deliberately not recorded yet: the
active-alert store funnel also runs on persisted restore, so firing
waits for the explicit activation seam in a later phase. The monitoring
bootstrap enables the log per manager; ephemeral managers and tests
record nothing unless they opt in.
GET /api/alerts/events (monitoring:read) reads the log with
alertIdentifier/type/since/limit filters, newest first.
Phase 0 of docs/ALERT_ENGINE_EVOLUTION.md (coverage gap
alert-engine-suppression-observability).
GET /api/alerts/delivery-diagnosis without alertIdentifier now returns
the diagnosis array for every active alert in one manager pass, so list
surfaces do not need a request per alert. Extracts the per-alert
diagnosis into a locked helper shared by both paths; single-alert
behavior is unchanged.
First slice of coverage gap alert-engine-suppression-observability
(docs/ALERT_ENGINE_EVOLUTION.md Phase 0).
GET /api/notifications/apprise and the PUT echo returned the saved API key
verbatim, so any settings:read token could exfiltrate a write-capable
secret while the sibling email handler blanks the SMTP password under the
same scope. Responses now drop the key and report hasApiKey instead, the
update and test-send paths fall back to the saved key when the request
leaves it blank (matching the email password contract), and the settings
form tells the user a saved key is kept when the field stays empty.