Share an interactive HTML report

You built a dashboard. It has a chart that filters when you click the legend, a table that sorts, three tabs. It works perfectly in file:///Users/you/report.html.

Then you try to give it to someone.

  • Email strips the scripts, or the client refuses the attachment outright for having them.
  • Slack shows a filename. The recipient downloads it, their browser warns them, they open it, and now there are two copies drifting apart.
  • CI artifacts hand back a zip behind a login, on a retention clock.
  • Confluence disabled the HTML macro years ago — most Cloud instances can't render one at all without an extra app.
  • GitHub renders markdown in a job summary and .ipynb as static output; a job summary strips scripts and iframes outright.
  • Jenkins serves HTML Publisher output under a Content-Security-Policy that blocks inline script and style, which is why your report shows up as unstyled markup.
  • GitHub Pages works — and publishes it to the entire internet, unless you're on Enterprise Cloud.

So people screenshot the chart into Slack and the interactivity, which was the whole point, never leaves the laptop it was built on.

What Comma does instead

Comma stores report HTML verbatim — scripts, inline handlers, embedded data and all — and renders it inside an iframe with sandbox="allow-scripts" and no allow-same-origin.

That combination is the whole design. The frame gets an opaque origin: your report's JavaScript executes normally, but it cannot read the app's DOM, cookies, localStorage, or session — there is no same-origin relationship to exploit. The sandbox is the security boundary, so the HTML doesn't have to be scrubbed to be safe to host. An earlier version of the API did strip <script> on ingest; it broke interactive reports without adding anything the sandbox wasn't already covering, so it's gone.

Practically: a Chart.js dashboard is a Chart.js dashboard at the link.

curl -fsS -X POST https://commareports.com/api/v1/reports \
  -H "Authorization: Bearer $COMMA_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d "$(jq -n --rawfile html dashboard.html \
        '{title: "Q3 pipeline dashboard", html: $html}')"

The response carries the report id and its share URL. Or skip the curl and drag the file into the app — same result.

What works, and the one thing that doesn't

Works:

  • Inline <script>, inline event handlers, ES modules in the document.
  • Chart.js, Plotly, D3, ECharts, Vega-Lite — inlined or loaded from a CDN. Subresources fetched inside the sandboxed frame aren't governed by the app's CSP.
  • Tabs, accordions, sortable tables, client-side filters, <details>, CSS animation, responsive media queries.
  • Reports exported by tools that produce one self-contained file: Lighthouse, pytest-html --self-contained-html, ydata-profiling, Quarto with embed-resources: true, nbconvert, dbt docs generate --static.

Doesn't work: a report that fetches a sibling file at view time.

An opaque origin has no base URL, so fetch("data/results.json") has nothing to resolve against. Allure is the classic case — its UI is a single-page app that loads its results over XHR, which is also why it's blank when you open it over file://. Two fixes:

  1. Inline the data. Most generators have a self-contained mode. It's one flag and it makes the artifact durable everywhere, not just here.
  2. Upload the siblings as assets. Drop the whole directory (or a zip of it) in, and relative src, href, srcset and url() references are rewritten to the uploaded copies — enough for <script src>, stylesheets, fonts and images, though not for a runtime fetch.

Size: the HTML body is capped at 5 MB; assets are 25 MB per file, 250 MB and 500 files per report.

The part that isn't just hosting

A static host gives you a URL. What it can't give you is the sentence someone was about to type in Slack.

Comma renders the report inside that sandbox with an anchored comment layer on top. A reader highlights the row, the paragraph, or the chart caption and pins a thread to it — "this cohort is double-counted after the July migration" lands on the number it describes. Threads survive republishing: PATCH the same report id with a regenerated file and the URL stays, a revision is appended, and open threads carry across so a reader can see what changed and what was already argued about.

And it's private by default. A dashboard built from real data belongs behind an access level — private, invite-only, any signed-in user — not on a public static host that happens to be convenient. See the sharing model.

For agents and pipelines

The same endpoint is what an agent or a CI job uses. A Claude Code or Cursor agent attached through Comma's MCP server calls create_report with the HTML it just generated and returns the link; publishing from CI is one curl in the job. Interactive or static, it's the same write path and the same scoped token.

Try it

Comma is free — unlimited reports, unlimited commenters, unlimited revision history. Take the dashboard you were about to screenshot and publish the file instead.

Publish your first report →

Related