Skip to content

Background task queue with admin API, WebSocket updates, and a Maintainer dashboard page - #54

Open
JustSamuel wants to merge 7 commits into
developfrom
feat/issue-322-background-task-queue
Open

Background task queue with admin API, WebSocket updates, and a Maintainer dashboard page#54
JustSamuel wants to merge 7 commits into
developfrom
feat/issue-322-background-task-queue

Conversation

@JustSamuel

Copy link
Copy Markdown
Collaborator

Ported from GEWIS/sudosos-backend#918 and GEWIS/sudosos-frontend#830.

These two PRs were companions stuck in separate repos, connected only by a published @gewis/sudosos-client npm package. Backend#918 was sitting CONFLICTING against old backend develop, and frontend#830 was a draft waiting on it. Building both together for the first time, in one repo, with the OpenAPI client generated in-repo instead of published separately, turned up a real drift: the frontend's task detail dialog rendered task.payload, but the backend deliberately never puts payload in the API response (notification params can contain reset tokens). I removed the payload viewer from the detail dialog (template, the formatPayload helper, and the details.payload key in en/nl/pl) instead of inventing a backend field to feed it.

Other things that came up during the port:

  • Regenerated packages/sudosos-client off the merged backend to add TasksApi, folded into the "expose task admin API" commit.
  • A real conflict in backend#918, unrelated to the monorepo move: websocket-service.ts and its test had already picked up an unrelated TerminalPaymentResponse room/event registration on develop since this PR's branch was cut. Merged both features' room registrations and emit methods side by side.
  • The backend/package.json/pnpm-lock.yaml conflicts from dropping bullmq/ioredis were just a lockfile regeneration. The pnpm overrides block lives at the repo root now, not per-package, so there was nothing to re-add there.
  • An unrelated @typescript-eslint/no-unnecessary-type-assertion failure showed up in the new tasks.store.ts -- the monorepo's eslint config is stricter here than either old repo's was. Fixed it.

pnpm build and pnpm run backend:test both pass (2579 tests passed, 38 skipped, 0 failed). backend:lint and frontend:lint are clean too.

JustSamuel and others added 7 commits August 14, 2026 13:26
Adds a generic database-backed task queue (Laravel-style) and uses it to
replace the existing BullMQ + Redis email queue. The `task` row is the
source of truth for state (pending, processing, completed, failed) and
carries enough JSON payload to run the task independently.

Workers concurrently drain the table via an atomic SELECT-then-UPDATE
claim: a candidate is picked, then `UPDATE task SET status='processing'
WHERE id=? AND status='pending'` accepts it only if affected_rows == 1.
That makes concurrent workers (in this process or across replicas)
safe without explicit row locking or SKIP LOCKED tricks, and works
across SQLite, MySQL/MariaDB, and Postgres.

BullMQ, ioredis, the redis-connection helper, the mail-queue worker,
and the dual-mode Mailer all go away. The Mailer is now a thin
nodemailer wrapper that the task worker calls directly; emails flow
through the same `task` row that admins see in the UI, which is the
unit failure reporting + retry operate on.

A `send-notification` task type is registered so Notifier callers have
a handler ready to dispatch into in the next commit. Existing tests
that asserted on the BullMQ mail-queue add now assert on the dispatched
task row in the DB, and a new race-condition test verifies that two
parallel `claim()` calls produce exactly one winner.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the /v1/tasks controller (list, get, retry, stats) gated behind the
new `Task` RBAC resource. The retry endpoint resets a failed task back
to pending so an admin can re-run it after fixing the underlying cause.

Surfaces `failedTaskCount` on the existing /ping response so monitoring
tools can treat a non-zero queue of failed tasks as a health signal --
the issue calls this out as an explicit acceptance criterion.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Splits Notifier.notify into a synchronous pre-check that resolves
eligible channels and a `notifySync` worker path that owns the actual
rendering and channel dispatch. The pre-check keeps the original
behaviour (throwing when no channel applies) so call-sites and tests
that assert on that branch are unaffected, while the heavy work --
template rendering, SMTP -- now happens inside the `send-notification`
task handler.

The big win is fine-notify and other fan-out flows no longer block the
HTTP request handler on N rendering passes; they enqueue N tasks and
return immediately.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Registers a `tasks:all` room (policy gated on `get:all:Task`) and a
`task:updated` event in the event registry. TaskService already emits
through the generic `WebSocketService.emit` on every state transition;
this commit gives that emit somewhere to land so subscribed admin
clients refresh in real time instead of polling /v1/tasks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lets the dashboard call into the new /v1/tasks admin endpoints (list,
get, retry, stats) through the standard authenticated axios instance,
matching every other API on the service.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…s work

Dashboard `setupWebSocket()` was being called without a `getToken`
callback, so the socket connected anonymously and any room with a
policy (e.g. the new `tasks:all`) returned "Authentication required
for this room." Mirror the POS app's pattern: pass a getter that reads
the JWT from storage on every (re)connect, so token refreshes are
picked up automatically.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New /maintainer/tasks page (renamed Maintainer to a dropdown with
Maintenance and Background tasks) for admins with the `Task` RBAC
resource. The page shows:

- A stats grid (pending / processing / completed / failed) wired
  to /v1/tasks/stats.
- A paginated, filterable DataTable of tasks. Filters: status (multi-
  select) and type (free-text). Each row exposes a (i) info button
  that opens a modal with the full task -- id, type, status tag,
  attempts, every timestamp, pretty-printed JSON payload, full last
  error. Failed rows additionally show a Retry button that calls
  POST /v1/tasks/:id/retry.
- A "LIVE" indicator dot. The view subscribes to the `tasks:all`
  WebSocket room on mount and listens for `task:updated` events --
  rows update in place, new dispatches appear at the top, and a
  debounced fetchStats keeps the counters fresh without polling.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

SudoSOS Coverage Report

Commit: 72c83ea
Base: develop@587e808

Type Base This PR
Total Statements Coverage  93.09%  93.42% (+0.33%)
Total Branches Coverage  87.92%  88.31% (+0.39%)
Total Functions Coverage  93.83%  94.16% (+0.33%)
Total Lines Coverage  93.09%  93.42% (+0.33%)
Details (changed files)
FileStatementsBranchesFunctionsLines

The full per-file breakdown is omitted to stay under GitHub's 65,536-character comment limit. Download the coverage artifact from this workflow run for the complete report, or view it on Coveralls.

@coveralls

Copy link
Copy Markdown

Coverage Report for CI Build 31796994125

Coverage increased (+0.3%) to 92.535%

Details

  • Coverage increased (+0.3%) from the base build.
  • Patch coverage: 4 uncovered changes across 1 file (782 of 786 lines covered, 99.49%).
  • 11 coverage regressions across 4 files.

Uncovered Changes

File Changed Covered %
backend/src/service/task-service.ts 377 373 98.94%
Total (15 files) 786 782 99.49%

Coverage Regressions

11 previously-covered lines in 4 files lost coverage.

File Lines Losing Coverage Coverage
backend/src/mailer/messages/user-type-updated.ts 4 78.38%
backend/src/mailer/messages/welcome-with-reset.ts 3 85.0%
backend/src/mailer/messages/transaction-notification.ts 2 76.32%
backend/src/mailer/messages/user-debt-notification.ts 2 85.37%

Coverage Stats

Coverage Status
Relevant Lines: 24919
Covered Lines: 23280
Line Coverage: 93.42%
Relevant Branches: 5237
Covered Branches: 4625
Branch Coverage: 88.31%
Branches in Coverage %: Yes
Coverage Strength: 812.89 hits per line

💛 - Coveralls

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.

2 participants