Skip to content

feat(create-a-container): notification queue + inbound webhook (#434) - #442

Open
runleveldev wants to merge 6 commits into
mieweb:mainfrom
runleveldev:feat/434-notification-queue
Open

feat(create-a-container): notification queue + inbound webhook (#434)#442
runleveldev wants to merge 6 commits into
mieweb:mainfrom
runleveldev:feat/434-notification-queue

Conversation

@runleveldev

Copy link
Copy Markdown
Collaborator

Closes #434. Part of #431 (memory-exhaustion thrash); the integration target for the lxc-oomd packaged hook script.

What

A decoupled notification mechanism in the create-a-container manager: node-side tools POST a structured event; the web app surfaces it per owner.

Deliverables

  • Inbound webhook POST /api/v1/notifications — admin-API-key authenticated (rejects unauthenticated 401 / non-admin 403). Mirrors the agent check-in auth precedent.
  • PersistenceNotifications Sequelize model + migration (source, severity, node, ctid, owner, action, message, evidence JSON, eventAt, ack fields; owner/ack + createdAt indexes). Migrate up/down verified against SQLite.
  • Layered resource (resources/notifications/, per docs/mvc-manifesto.md): validator/repository/service/serializer/controller/router.
    • GET /api/v1/notifications — owner-scoped, unacked-first + newest-first (badge derives from the response).
    • POST /api/v1/notifications/all/ack and POST /:id/ack — owner-scoped acknowledge (idempotent; 404 on foreign). all/ack registered before :id/ack.
  • UI surface — a notification bell replacing the unused header search button, with an unread badge and an ack-capable dropdown (severity, related node/CT, relative time, per-item ack + "mark all as read"). Polls every 30s.
  • Contract docs — OpenAPI paths + Notification schema, and docs/notification-webhook.md documenting the payload contract + curl example for the hook script.

Payload contract

{
  "source": "lxc-oomd",
  "severity": "warning|critical",
  "node": "opensource-phxdc-pve1",
  "ctid": 392,
  "owner": "mbachelder",
  "action": "freeze|kill|bump|quarantine|detect",
  "message": "CT 392 frozen: memory PSI full avg10=83 for 45s",
  "evidence": { "psiFullAvg10": 83.6 },
  "ts": 1771234560
}

ts (epoch seconds) is recorded as eventAt; ctid accepts int/string; when owner is omitted it is resolved best-effort from node+ctid via the Containers table.

Acceptance criteria

  • Webhook accepts and persists the payload; rejects unauthenticated calls.
  • Events appear in the UI with owner/container/severity and can be acked.
  • Contract documented so the lxc-oomd hook script can target it.
  • Email sink deferred — the issue lists it as optional; deferred per maintainer direction. Easy to add later as a post-commit utils/email.js call.

Tests

24 new (18 API + 6 service): auth (401/403), persistence, coercions (ts, ctid), owner resolution + null fallback, owner-scoped listing, ack flows. client tsc + vite build pass.

Notes

  • Owner-only visibility: events with an unresolved owner persist but surface in no bell — documented as a known limitation in the webhook doc.
  • One pre-existing, unrelated test failure (middlewares/__tests__/mcp-proxy.test.js) and a pre-existing multi-suite SQLite teardown crash exist on main, independent of this change.

Copilot AI review requested due to automatic review settings August 3, 2026 17:03

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a decoupled notifications mechanism to the create-a-container manager, allowing node-side tools to POST structured events via an inbound webhook that are then persisted and surfaced per-owner in the web UI.

Changes:

  • Introduces a new notifications resource (router/controller/service/repository/serializer/validator) with ingest, list, and acknowledge endpoints.
  • Adds Notification persistence (Sequelize model + migration) and documents the webhook contract + OpenAPI paths/schema.
  • Updates the client to display an owner-scoped notification bell with unread badge and ack actions.

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
create-a-container/routers/api/v1/index.js Registers the notifications router under /api/v1/notifications.
create-a-container/resources/notifications/validator.js Defines Zod schemas for ingest payload, list query, and id param coercion.
create-a-container/resources/notifications/service.js Implements ingest (owner resolution + ts→Date) and ack/list service logic.
create-a-container/resources/notifications/serializer.js Serializes Notification rows to API JSON shape.
create-a-container/resources/notifications/router.js Wires routes + auth (admin ingest; owner-scoped list/ack).
create-a-container/resources/notifications/repository.js Adds DB access helpers for list/create/ack and owner resolution.
create-a-container/resources/notifications/controller.js Implements HTTP handlers for create/list/ack endpoints.
create-a-container/resources/notifications/tests/service.test.js Unit tests for ingest coercions and ack-all behavior.
create-a-container/resources/notifications/tests/notifications.api.test.js Integration tests for auth, persistence, listing, and ack flows.
create-a-container/openapi.v1.yaml Documents Notification schema and notification endpoints.
create-a-container/models/notification.js Defines the Notification Sequelize model and indexes.
create-a-container/migrations/20260731120000-create-notifications.js Adds Notifications table + indexes via migration.
create-a-container/docs/notification-webhook.md Documents webhook contract and curl example for node-side hook scripts.
create-a-container/client/src/lib/types.ts Adds client types for notifications (avoids DOM Notification clash).
create-a-container/client/src/lib/queries.ts Adds notification list/ack query helpers and query keys.
create-a-container/client/src/app/NotificationsBell.tsx Implements the polling bell dropdown UI with per-item and bulk ack.
create-a-container/client/src/app/Header.tsx Replaces unused header search action with the NotificationsBell.

Comment thread create-a-container/resources/notifications/repository.js Outdated
Comment thread create-a-container/client/src/app/NotificationsBell.tsx Outdated
@runleveldev
runleveldev marked this pull request as draft August 3, 2026 18:51
@runleveldev
runleveldev marked this pull request as draft August 3, 2026 18:51
@runleveldev
runleveldev marked this pull request as draft August 3, 2026 18:51
@runleveldev
runleveldev marked this pull request as draft August 3, 2026 18:51
@runleveldev
runleveldev marked this pull request as draft August 3, 2026 18:51
@runleveldev
runleveldev marked this pull request as draft August 3, 2026 18:51
Adds a decoupled notification mechanism so node-side tools (e.g. lxc-oomd)
can report events to the manager without hard-coupling detection to email.

- Notifications model + migration (source, severity, node, ctid, owner,
  action, message, evidence JSON, eventAt, ack fields).
- resources/notifications/ (layered per the MVC manifesto):
  - POST /api/v1/notifications: inbound webhook, admin-API-key auth. Coerces
    ts->eventAt and ctid->string; resolves owner from node+ctid when omitted.
  - GET /api/v1/notifications: owner-scoped, unacked-first + newest-first.
  - POST /notifications/all/ack and /:id/ack: owner-scoped acknowledge.
- UI: notification bell (replaces the unused header search button) with an
  unread badge and an ack-capable dropdown; polls every 30s.
- OpenAPI paths/schema + docs/notification-webhook.md contract for the
  lxc-oomd hook script.
- Tests: 24 (18 API + 6 service).

Part of mieweb#431. Closes mieweb#434.
…ive time

Replace the hand-rolled relative-time helper in the notification bell with
the existing formatLastSeen formatter from @mieweb/ui (the project's UI/util
library), rather than maintaining bespoke date math.
… the bell

Replace the hand-rolled row/list/empty/loading markup in the notification
bell with the shared @mieweb/ui NotificationCenter component, kept inside the
Dropdown for the trigger + unread badge. Adapts AppNotification to the
component's Notification shape (severity -> type/priority, a source/node/CT
title) and wires onMarkRead/onMarkAllRead to the ack endpoints.

Verified end-to-end in a browser: badge count, mark-one, and mark-all-read
all update correctly.
… DB/model layer

Replace app-only validation of the notification columns with real constraints:

- severity is now a DB ENUM('info','warning','critical') (matching the Jobs
  and Services precedent) with an isIn model validator; the migration drops
  the ENUM type on down (Postgres only).
- source/message get notEmpty validators; all STRING columns are bounded to
  STRING(255) instead of unbounded, matching ResourceRequest.
- action stays a bounded free-form string (node-side tools may emit new
  actions without a schema migration).
- The zod validator reuses Notification.SEVERITIES as the single source of
  truth for the severity vocabulary.

Adds model-level tests asserting the DB/model rejects an out-of-enum severity
and empty source/message even when the HTTP validator is bypassed.
…tification endpoints

- Add operationId to all four /notifications operations (ingest_notification,
  list_notifications, acknowledge_all_notifications, acknowledge_notification),
  matching the snake_case verb-first convention used across the spec.
- Replace prose-only response descriptions with schema-backed content: success
  responses reference the Notification schema (or the { acknowledged } object)
  wrapped in the standard { data } envelope, and error responses reference the
  shared Error schema — so the spec actually lists the fields each endpoint
  returns.
@runleveldev
runleveldev force-pushed the feat/434-notification-queue branch from 6c26ea3 to b97c341 Compare August 6, 2026 19:32
…tion ordering

The unacked-first ORDER BY hardcoded a double-quoted "acknowledgedAt"
identifier, which MySQL interprets as a string literal (unless ANSI_QUOTES),
silently breaking the sort. Quote the column via the active dialect's
queryGenerator.quoteIdentifier() so it works on Postgres, MySQL and SQLite.

Addresses PR mieweb#442 review feedback.
@runleveldev
runleveldev marked this pull request as ready for review August 6, 2026 19:35
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.

Notification queue + inbound webhook in create-a-container

3 participants