Skip to content

feat: Alerts gather what needs someone in Management, with a count in the sidebar, the phone's bar and the account menu - #2423

Merged
ExtraToast merged 2 commits into
integration/managementfrom
feat/2385-alerts
Oct 13, 2026
Merged

ExtraToast merged 2 commits into
integration/managementfrom
feat/2385-alerts

Conversation

@ExtraToast

Copy link
Copy Markdown
Contributor

Part of #2385 (step 6, its last pull request). Brings back #1941 from the archive tag.

What this is

One Alerts page, and a count in the sidebar, the phone's bar and the account menu, for what needs someone in Management.

  • A module raises its own alerts. It implements AlertSource (alerts :: api), so the alerts module reads no other module's data. Alerts are raised afresh on every read and clear once their cause is gone.
  • Two sources so far, both for admins: dead jobs and open exceptions.
  • Each reader sees what their roles can act on. An admin-only alert never shows to the board or the treasurer, and cannot be hidden by them with a guessed key.
  • Hide for me hides an alert for that person only, and Show again brings it back.
  • The count is read again on every page, so an alert dealt with on one page leaves the count on the next.

New module alerts, new table hidden_alert, three endpoints under /management/alerts (board only), one page and a new top entry in the portal's navigation.

What differs from the archive, and why

  • Only the alert kinds this branch can raise. AlertKind has JOB_DEAD and EXCEPTION_OPEN. Drift, a cohort without its list, failed email, a role waiting on two-factor and the Discord kinds come with the steps that own their cause, each adding its kind, its words in domains/alerts/reading.ts and its source.
  • A hide is a stable key and a mark. The archive keyed an alert by its newest cause (the newest dead job's id), so retrying that job changed the key, dropped the hide and brought back an alert nothing new had happened to. Now the key stays (job-dead, exception-open) and the alert carries a mark that rises only when a new cause arrives. A hide covers the alert up to the mark it was taken at: retry one of three dead jobs and it stays hidden, a fourth dies and it shows again.
  • Hiding, showing and forgetting a hide are each one statement that may run twice. The archive read a row and then wrote, which fails for the second of two requests, and the sidebar and the page do ask at the same moment.
  • Reading alerts writes only when a hide has lost its alert. The archive deleted on every read, and the sidebar reads on every page.
  • The sources read sums, not rows. The archive loaded every open exception with its trace, and the newest dead job with its payload, on each read of the sidebar.
  • Since is when the oldest cause began, not when the newest fired, which is what the column and the "Oldest" fact say.
  • A read that fails keeps the count it had, and the page says the alerts could not be read. The archive showed "Nothing needs you right now".
  • A phone can hide and show. The archive's phone rows had no acts.
  • Only the newest read counts, and the alerts held are let go when the portal closes, so the next person to sign in never meets this one's.
  • One alert is said as "1 alert" to a screen reader. A dead job's line reads "Every retry failed".

Known, not changed

  • A reopened exception keeps the moment it was first seen, so Since can read older than its reopening.
  • hidden_alert has no foreign key to the user. A row left by a deleted user is a few bytes that nobody reads.
  • For now the board's Alerts page is empty: both sources are for admins. The entry is there for the board because the sources of steps 2 and 5 are theirs.

How it was checked

  • Api: compiles with warnings as errors, ktlint and detekt pass, 97 unit tests pass (alerts, exceptions, the job source and the architecture tests). AlertsIT, RecordedExceptionIT and the jobs integration tests pass against a throwaway MariaDB: 62 tests, among them a hide that outlives a retry of the newest dead job and ends at a new one.
  • Every added line under services/api/src/main and services/frontend/src is hit by a unit test, measured against jacoco and lcov.
  • Frontend: the unit tests of the touched areas pass, typecheck and lint are clean. tests/e2e/alerts.spec.ts passes on a dev server of this branch, and the page was looked at on a wide window, with the account menu open and at phone width.
  • An independent review of the diff was done before this was opened. The mark, the lighter reads, Since and the newest-read rule come from it.

@github-actions

github-actions Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Changes

Bucket Files Added Removed
api 14 ██████ 418
frontend 10 █████ 405 ░ 10
tests 11 ██████████ 739 ░ 7
build & config 4 ███████ 523 ░ 3
total 39 +2085 −20

0.90 test lines per prod line.

Coverage

Statements Branches Functions Lines
api 77.73% 71.10% 67.91% 80.34%
main 08fdcf6 — — — —
frontend 94.48% 89.93% 92.87% 95.23%
main 08fdcf6 92.74% 88.01% 90.20% 93.37%
Δ +1.74 +1.92 +2.67 +1.86

Patch coverage: 100.0%, 206 of 206 changed lines covered.

… dead jobs and open exceptions

A module that owns a cause implements `AlertSource`, so the alerts module
reads no other module's data. Alerts are raised afresh on every read and clear
once their cause is gone. A reader sees the alerts their roles can act on and
may hide one for themself, which lasts until the alert clears.

Hiding, showing and forgetting a hide are each one statement that may run
twice, since the sidebar and the page read and write at the same moment.
… the sidebar, the phone's bar and the account menu

Each alert links to where it is dealt with, and the reader hides one for
themself or shows it again, on a phone as well. The count is read again on
every page. A read that fails keeps the alerts it had and the page says the
alerts could not be read.
@github-actions

Copy link
Copy Markdown
Contributor

The SQL 2026-09-30-hidden-alert.yaml will run

Rendered against a database built from the baseline plus everything on the base branch, so this is what the changeset adds and nothing else. Generated, not applied.

--  Changeset db/changelog/changes/2026-09-30-hidden-alert.yaml::hidden-alert::blueshell
CREATE TABLE blueshell.hidden_alert (id BIGINT AUTO_INCREMENT NOT NULL, user_id BIGINT NOT NULL, alert_key VARCHAR(255) NOT NULL, hidden_mark BIGINT NOT NULL, hidden_at datetime(6) NOT NULL, CONSTRAINT pk_hidden_alert PRIMARY KEY (id));

ALTER TABLE blueshell.hidden_alert ADD CONSTRAINT uk_hidden_alert_user_key UNIQUE (user_id, alert_key);

@ExtraToast
ExtraToast merged commit ac9226b into integration/management Oct 13, 2026
39 checks passed
@ExtraToast
ExtraToast deleted the feat/2385-alerts branch October 13, 2026 10:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant