feat(dashboards): scheduled dashboard delivery worker (PMS-471) #349
No reviewers
Labels
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
psa-systems/mokosh-server!349
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/PMS-471-scheduled-dashboards"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
PMS-453 phase 1 shipped per-user saved dashboards (
saved_dashboardstable +/api/v1/dashboardsCRUD). The widget rendering surface (phase 2a) is in flight separately; this PR adds the phase-2b delivery surface so a user can mark a saved dashboard "deliver this weekly to my email" and a background worker materialises a snapshot at the cron tick.What landed:
Migration
076_scheduled_dashboards.sqladdsscheduled_dashboards (id, tenant_id, dashboard_id, user_id, cron_expr, channel='email', recipient_email, is_active, last_run_at, next_run_at, last_error, created_at, updated_at). Bothdashboard_idanduser_idcascade on delete so removing a dashboard or user removes its schedules. The hot-path index is partial onis_active = trueso a tenant that disables every schedule keeps its rows out of the worker scan.DTOs in
dashboards/models.rs:ScheduledDashboardResponse,CreateScheduledDashboardRequest,UpdateScheduledDashboardRequest. The cron expression is validated via the existingutils::validation::validate_cron; the recipient override is validated as an email.dashboards/service.rsgainsschedule_create/list/get/update/deleteplus a module-localcompute_next_runhelper. The create path runs the existinggetvisibility check first so a user can only schedule their own dashboards (saved dashboards are private by design); mutation methods are owner-scoped viauser_id = $3in the WHERE.New
dashboards/worker.rs:ScheduledDashboardsWorkerimplementsJoband ticks every 60s. Each tick locks up to 10 due rows viaSELECT ... FOR UPDATE SKIP LOCKED, fetches the dashboard via the visibility-checking service path, renders a text snapshot of the layout JSONB (extracts widget keys from the SPA-owned blob: handles{widgets: [{key}]},{widgets: ["..."]}, and nested{rows: [{widgets}]}shapes), and INSERTs anemailrow intonotifications. The DispatcherWorker handles the SMTP send + 1m/5m/30m/2h/6h retry backoff. The cadence advances regardless of outcome - a failing render stampslast_errorbut does NOT stall the next firing.The "materialise" step is intentionally shallow at v1 - the SPA widget render path doesn't exist yet (phase 2a). The snapshot today carries the dashboard name + widget keys; once 2a lands the worker can swap in a richer renderer without touching the schedule machinery.
Schedule routes registered on the dashboards router:
GET/POST /api/v1/dashboards/{id}/schedules(list / create under the parent),GET/PATCH/DELETE /api/v1/dashboards/schedules/{id}(per-schedule mutation lives on a flat path so a SPA "pause this schedule" PATCH does not need the parent id in the URL).main.rsregisters the worker on the sharedSchedulerat 60s intervals, mirroring the dispatcher / rmm / sla / calendar-reminder cadence.Integration tests at
tests/scheduled_dashboards.rs:schedule_create_returns_next_run_at: POSTing a schedule returns a row withis_active=true, a populatednext_run_at, and the parentdashboard_id.worker_tick_materialises_due_schedule: a back-dated schedule is picked up on the next tick, onenotificationsrow of channelemaillands with the widget keys in the body, and the schedule'slast_run_atis populated whilenext_run_atadvances with nolast_error.worker_tick_skips_disabled_schedule: a schedule withis_active=falseis NOT examined by the tick, produces no notification, and is left untouched.#PMS-471
965f8b46ad371c12a495