Detailed specs for the Scheduled Jobs console. (proposed) markers mean
the screen/endpoint does not exist in source; everything else cites
file:line. Data shapes are quoted from scheduler.service.ts and
dlq.setup.ts.
Layout (desktop, ≥ 1024 dp)
┌──────────────────────────────────────────────────────────────┐
│ Admin Settings · Scheduled Jobs [+ New] │
│ 10 default · 2 custom · 0 dead letters (summary strip) │
├──────────────────────────────────────────────────────────────┤
│ [status] job name queue cron tz ⋮ │
│ ● running audit-flush audit-write */1 * * * * UTC ⋮ │
│ ● ok daily-digest emails 0 9 * * * UTC ⋮ │
│ ▲ failed attendance-report-daily report-generate 0 7 * * * │
└──────────────────────────────────────────────────────────────┘
Spec Detail
Data GET /scheduler → {queue, name, pattern, tz} (scheduler.service.ts:152-186); enrich (proposed) with lastRunAt, lastStatus, nextRunAt
Sort default: name asc; toggle group by queue (drop-down)
Status derivation (proposed) — from BullMQ job states of last run; today the API has no status field
Row tap → S2 (/admin/scheduler/jobs/:queue/:name)
Menu Run now (S5) · View logs (S4) · Remove — remove calls DELETE /scheduler?queue=&name=&pattern= (scheduler.controller.ts:37-46, scheduler.service.ts:188-196)
Empty "No scheduled jobs — the 10 defaults register on boot (scheduler.service.ts:43-45)"
Refresh pull + auto-poll 60 s; offline → AppOfflineBanner
Permission scheduler.read (403 → AppEmptyState permission variant)
Spec Detail
Header name, CronChip(pattern), tz chip, queue chip, status badge
Definition block retention: removeOnComplete {age:3600, count:100}, removeOnFail {age: 7d} (scheduler.service.ts:145-146); global retry: attempts: 3, exponential backoff 5 s (bullmq.module.ts:60-65); trigger payload envelope {eventType, tenantId:'system', correlationId, actorId:'scheduler'} (scheduler.service.ts:136-142)
Run history (proposed) table: startedAt · status (ok/failed/in-progress) · duration · error excerpt. Source (proposed) GET /scheduler/runs?queue=&name=
Retention note history window is 1 h / 100 completed jobs for the trigger , and 14 d for failed jobs globally (bullmq.module.ts:63-64) — display accordingly
CTAs Run now · View logs · Remove schedule (confirm)
Spec Detail
Trigger FAB "+ New" (S1) → AppBottomSheet
Fields queue (AppDropdown, 11 whitelisted: create-schedule.dto.ts:4-16), jobName (required, dto:30-32), pattern (5-field cron, dto:20-24; monospace; hint row of preset chips e.g. 0 6 * * *), tz (default UTC dto:38-41), payload (JSON textarea, optional)
Validation inline per field; server re-validates (dto:19-41); pattern regex error message "Invalid cron pattern (5 fields required)" (dto:22)
Submit POST /scheduler (scheduler.controller.ts:30-35) → queue.add with repeat {pattern, tz} (scheduler.service.ts:198-208)
Success sheet closes, snackbar, new row at top (optimistic, rollback on fail)
Duplicate registerRepeatable skips existing name+pattern (scheduler.service.ts:128-133) — UI mirrors: warn if identical row exists
Spec Detail
Route /admin/scheduler/jobs/:queue/:name/logs
Source (proposed) GET /scheduler/jobs/:queue/:name/logs?level=&tail=
Content timestamp · level · message · jobId; monospace; auto-scroll; "tail live" AppSwitch
Filter level chips (debug/info/warn/error); default info
Empty "No logs kept — completed trigger jobs live 1 h (scheduler.service.ts:145)"
Note worker-side logs exist via nestjs-pino (AGENTS.md stack); surfacing them is (proposed)
Spec Detail
Trigger row menu "Run now" or S2 CTA
Content confirm: "Trigger daily-digest → send-daily-digest on queue emails now? (Deliveries may duplicate — worker idempotency must hold.)"
Action (proposed) POST /scheduler/:queue/:name/trigger → re-enqueues via job class (daily-digest.job.ts:12-25 pattern)
States in-flight (button spinner, dialog not dismissible twice), success snackbar "Triggered — watch run history", failure inline
Idempotency guard IdempotencyService (bullmq.module.ts:76) must de-dupe on replay — QA item 14 §6
Spec Detail
Route /admin/scheduler/dlq
Source (proposed) GET /scheduler/dlq — records written by setupDlqListener (dlq.setup.ts:5-27)
Card fields originalQueue, originalJobName, originalJobId, failedReason, attemptsMade, failedAt, data (JSON expandable)
Actions Replay → (proposed) POST /scheduler/dlq/:id/retry (re-add to original queue with same data); Delete → confirm dialog
Empty "No dead letters"
Note DLQ condition: attemptsMade >= (job.opts.attempts ?? 3) (dlq.setup.ts:8-9); replay must preserve correlationId for idempotency