Skip to content

Post-event reports

The Post-event reports module renders a branded end-of-event report — the document you email a sponsor on Monday morning. It’s assembled from the analytics Flagpost already holds, so there’s nothing to instrument: run the event, end it, generate the report.

Reports are an optional module, enabled per competition (on by default) under Settings → Modules. Generating one needs the generate_report permission — held by Judge and Administrator.

On the Reports tab you:

  1. Pick the sections — choose an audience preset or toggle individual sections:

    • Executive summary — the headline numbers.
    • Participation & engagement — sign-ups, active players, activity over time.
    • Results & scoreboard — final standings (the “Standings shown” count defaults to the top 10, adjustable 3–100).
    • Challenge analysis — solves, attempts and difficulty per challenge.
    • Support & operations — ticket volume and handling.
    • Feedback & sentiment — survey results and ratings.

    The Executive preset is the first three; Technical and Full cover everything.

  2. Choose formatsPDF, HTML, or both.

  3. Generate.

Rendering runs off the request path: a new versioned report is queued, the scheduler renders it, and the history list self-polls from Pending to Running to Ready (or Failed). Each generation is a fresh, numbered version — regenerate as often as you like; the newest is at the top, and you can delete an old version.

Finished reports stream through the API, not through presigned object-storage URLs:

GET /api/competitions/{id}/reports/{report_id}/download/{pdf|html}

That’s a deliberate choice — it means downloads work on single-origin deployments, behind a Cloudflare Tunnel, or anywhere MinIO isn’t browser-reachable, and generate_report is re-checked on every fetch rather than baked into a shareable link. Files are served as an attachment with nosniff and a sandboxing CSP.

A report is HTML composed from your data (Jinja2), turned into PDF by WeasyPrint — the same HTML is also a downloadable format, so the two can never drift. Charts are drawn as pure inline SVG (no browser, no image service). Branding is inherited from your site settings: the platform name, the accent colour, and a raster logo inlined into the document (SVG logos are skipped). A subtle “Powered by Flagpost” footer renders on every page. The renderer’s asset loader is locked down — it resolves only inline data: URIs and the bundled fonts, and refuses every network or filesystem fetch, so a report can’t be turned into an SSRF probe.

The data layer is a read-only aggregation over the existing analytics. Standings are read live, so a lingering scoreboard freeze never hides the final numbers from the report.

Two follow-ups aren’t built yet, worth stating so nobody waits on them: report anonymisation (rank-only, no names) and a size guard for very large events.

Generated reports live in object storage until downloaded or purged. If you run without Docker, the backend needs WeasyPrint’s system libraries installed before you can generate reports — the official image already bundles them.

Generated report files are not included in a platform backup; they’re transient artefacts of a finished event, cleaned up by retention or when you delete a version.

A completed render emits report.generated (competition_id, report_id, version, user_id), usable as an automation trigger under generate_report — for example, on report.generated → post a webhook to your staff channel.