Knowledge

Saved and scheduled reports

A Saved report is a named query against a Vigilo data set — changes, incidents, audit log, DORA snapshot, custom-field roll-up — with a frozen set of…

Last updated

Overview

A Saved report is a named query against a Vigilo data set — changes, incidents, audit log, DORA snapshot, custom-field roll-up — with a frozen set of filters and an optional schedule that emails a rendered PDF to a list of recipients on a cron cadence. Saved reports live at /ws/{slug}/reports and underpin the weekly-summary, monthly-compliance, and quarterly-board cadences most organisations need.

Where the live analytics view is for interactive investigation, a SavedReport is for repeatable, auditable, batched delivery — the kind your CISO wants in their inbox every Monday at 09:00 without having to log in.

Why it exists

Manually exporting CSVs and pasting them into emails is the dominant failure mode of every monitoring tool. People forget, formats drift, the wrong filters get applied, and there's no audit trail of what was sent to whom. The SavedReport model collapses report definition, schedule, rendering, and delivery into one workspace-scoped object so a Monday 09:00 report is the same Monday 09:00 report week after week, with a delivery log.

Key concepts

SavedReport model

A SavedReport (SavedReport: workspace, name, report_type, config JSONField, filters JSONField, schedule (cron string), email_to JSONField (list), email_cc JSONField, next_send_at, last_sent_at, is_active, created_by, created_at) is a workspace-scoped, RLS-isolated row.

  • report_type — one of changes_summary, incidents_summary, dora_snapshot, audit_log, custom_query.
  • filters — JSON blob holding the same filter keys the live list view supports (status, date range, assignee, tags). Serialised verbatim into the underlying ORM query at render time.
  • schedule — a cron string (0 9 * * 1 = Monday 09:00). Empty schedule means "manual run only".
  • email_to — list of recipient email strings; CC and BCC supported too.
  • next_send_at — denormalised pointer to the next due send time, recomputed every dispatch.
  • last_sent_at — last successful delivery, for audit and "stale report" alerts.

Dispatcher (Celery beat)

A Celery beat task vigilo.reports.dispatch_due (WB.19) runs every minute. It selects all SavedReport rows where is_active=True AND next_send_at <= now(), renders each in turn, sends the email, updates last_sent_at, and recomputes next_send_at from the cron string. Failures are retried with exponential backoff (max 5 attempts) and a permanently failed dispatch ends in a ReportDeliveryFailure row that surfaces in the reports list with a red badge.

WeasyPrint PDF rendering

The renderer is WeasyPrint — HTML+CSS to PDF — so the report template is a server-rendered Django template that mirrors the live UI styling. The renderer supports headers/footers, page numbers, embedded charts (via SVG so the PDF stays vector-crisp), and the workspace logo.

If WeasyPrint is unavailable on the host (missing native libs libpango, libcairo), the dispatcher falls back to a text-bytes attachment — a plain-text rendering of the report sent as .txt with a banner explaining the fallback. The report still arrives; the formatting is just plainer. Operators can verify WeasyPrint health under Settings → Reports → Health.

Recipient management

Recipients are stored as a JSON list of strings — there is no user-binding requirement, so reports can go to non-Vigilo users (auditors, vendors) without provisioning them. For internal users, the picker offers an @member autocomplete that resolves to the member's primary email at send time, so an email-change in the user profile automatically propagates.

Workspaces with a configured SMTP override send through the workspace SMTP credentials; otherwise the platform EMAIL_HOST fallback is used.

Scheduled email delivery

The dispatcher renders the PDF (or fallback text), builds the email with a workspace-branded HTML body and the report attached, and hands it to the SMTP layer. The email subject defaults to [Vigilo] {report.name} — {date} and is configurable in config.subject_template with {date} and {name} placeholders.

Common workflows

Save a report from a live view

  1. Open /ws/{slug}/changes, apply your filters (status=completed, last 30 days, team=payments).
  2. Click Save as report in the toolbar. Give it a name ("Payments completed CHGs — weekly").
  3. Optionally add a schedule (0 9 * * 1 — Monday 09:00) and recipients.
  4. Click Save. The report appears under /ws/{slug}/reports.

Schedule an existing report

  1. Open the report, click Schedule, pick a cadence from the picker (or paste raw cron).
  2. Add email_to recipients.
  3. Click Activate. next_send_at is computed and the report appears in the dispatcher's queue.

Run on demand

  1. Open the report, click Run now. The dispatcher renders synchronously and either sends the email (if recipients are set) or downloads the PDF to the browser.

Pause and resume

Toggle is_active=False to pause scheduling without losing the report definition. Toggle back on to resume; next_send_at is recomputed from now().

Permissions

  • Viewers can read reports and the delivery log but cannot create or schedule.
  • Engineers can create reports for their own use; they can run on demand and download.
  • Approvers / admins can schedule and add external recipients.
  • Owners can delete reports; deletion preserves the delivery log for 90 days for audit.

External recipients in email_to are not workspace members and have no Vigilo access — they only receive the PDF attachment.

Troubleshooting

Report did not arrive — Open the report detail, check the Deliveries tab. Common causes: SMTP misconfiguration (test under Settings → Email), bad recipient address (the SMTP layer returns a 5xx, logged as ReportDeliveryFailure), or the dispatcher is down (check Celery beat).

PDF arrived but tables are mangled — WeasyPrint fell back to text bytes. Install libpango/libcairo on the backend host and run python -c "import weasyprint" to verify. Re-run the report on demand to confirm.

Schedule says "Monday 09:00" but report arrives at 14:00 — Cron is in UTC by default. Set Workspace.settings.reports_timezone to your preferred timezone (e.g. Europe/Berlin) and the dispatcher will interpret the cron in that zone.

next_send_at is in the past but report not running — The dispatcher beat task may be paused. Check Flower or celery inspect scheduled. Manually trigger via python manage.py dispatch_reports_now --workspace {slug}.

External recipient bounced — The SMTP layer logs the bounce as a ReportDeliveryFailure. The report owner is notified; the recipient is not automatically removed (you might intend to retry tomorrow). Owners can prune recipients from the report editor.

Subject line missing the dateconfig.subject_template must contain the {date} placeholder. The default does. If you customised it, restore the placeholder.

Related