Skip to content
Dashboard Gallery
Get Support

What is removed before publishing

Each dashboard reaches the gallery as two artifacts — a screenshot and a Data Studio export bundle — and both are rewritten before anything is published. The rule the whole design is built around is short:

An unredacted original is never publicly reachable.

That is enforced in the API rather than in the page. The gallery’s own pages are static files served to anyone who asks for them, so they can never be the thing deciding what is allowed.

cleanstill matchesUploadscreenshot + export bundlePrivate storageoriginals, never servedOCR proposes rectanglesA person reviews themFlatten to a new image fileScrub the bundleRe-scan what was justwrittenPublic storageNothing is written

In sentences: the upload goes to private storage, where nothing public can reach it. The image is redacted by OCR proposing rectangles, a person reviewing them, and the browser flattening the result into a new file. The bundle is scrubbed and then audited by re-scanning what the scrubber just wrote — if the audit still finds a match, nothing is written to public storage at all. Only the flattened image and a clean bundle reach the public side.

Every bar in a published screenshot covers pixels that were never written into the file. The published image is not the original with rectangles drawn over it — it is a new image, drawn to a canvas with opaque rectangles filled in and saved. There is nothing underneath a bar to uncover.

Bars are opaque, not blurred. Blur on small text is a weaker guarantee than it looks.

The detection step runs in the browser, and the OCR engine is served from the gallery’s own origin rather than a CDN — the unredacted image never leaves the machine it was opened on. Each recognised word is checked against the same pattern set the bundle scrubber uses, and a match becomes a proposed rectangle that a person accepts, moves, resizes or deletes. Running detection again only adds proposals; it never removes a bar placed by hand.

Some screenshots genuinely have nothing to cover, so publishing with zero rectangles is allowed — but only after a second confirmation, recorded explicitly. To publish, a dashboard needs a flattened image and either at least one rectangle or that explicit record.

The rectangles are counted, not merely checked for: an editor run that covered nothing still saves an empty list, and a list existing is not evidence that anything was covered.

  • The original screenshot, under a storage prefix that the code serving public assets cannot construct a key into.
  • The rectangle coordinates. They are kept only so a redaction can be reopened and adjusted, and are never used to render a public view.
  • The scrub report (below).

Redacting only the screenshot would publish the same data in plain YAML — the export carries tenant identifiers, owner identifiers, owner email addresses and hardcoded filter values. So the bundle is rewritten too.

An import assigns ownership to whoever runs it, so there is nothing left for these to do. A pseudonymised owner id would be a lie the importer then has to notice.

tenant_ids · tenant_id · owner_ids · owners · created_by · changed_by · created_by_fk · changed_by_fk · last_saved_by · email · username · user_id · certified_by · certification_details

Filters on tenant-scoped columns are dropped

Section titled “Filters on tenant-scoped columns are dropped”

Some columns hold tenant data whatever value they contain. A venue named “Home” is an ordinary English word that no shape-based rule can find, so these clauses are matched on the column name instead, and the whole clause is removed:

zoneName · venueName · venue · zone · ssid · networkName · wlanName · apName · apMac · apGroupName · apSerialNumber · clientMac · clientIp · userName · userId · deviceName · hostname · serialNumber · switchName · switchMac · tenantId · tenantName · accountName · customerName

Matching is case-insensitive, because bundles are inconsistent about zoneName versus zonename.

Remaining strings are checked against the same pattern set the screenshot editor uses. Each distinct real value maps to the same replacement every time, so a filter still matches the chart beside it.

What is matchedReplaced withRange
MAC address00:00:5e:00:53:xxRFC 7042 documentation block
IPv4 address192.0.2.xRFC 5737 TEST-NET-1
IPv6 address2001:db8::xRFC 3849 documentation prefix
Email addressuser1@example.comReserved example domain
Hostname / FQDNhost-1.example.comReserved example domain
UUID / tenant id00000000000000000000000000000000
Serial numberSERIAL000001

Replacements are shaped like what they replace rather than being [REDACTED], for two reasons. A filter whose comparator became [REDACTED] would import cleanly and then render nothing. And because they keep their shape, the audit re-finds them all — which is how “scrubbed correctly” is told apart from “still leaking”.

Two places that are easy to miss, and are not missed

Section titled “Two places that are easy to miss, and are not missed”
  • The nested copy. One chart field holds a JSON document inside a YAML string, carrying a second copy of the chart’s parameters and filters — tenant identifiers included. It is parsed, scrubbed by the same rules and written back. A scrubber that walked the YAML tree alone would publish that copy intact.
  • The dialect. Files are re-serialised as YAML 1.1, which is what the importer reads. A default YAML 1.2 writer leaves unquoted precisely what a 1.1 reader retypes: an exported timestamp comes back as a date rather than a string, and the importer rejects the whole archive before reading a single chart. The same gap turns yes/no/on/off into booleans and 10:30:00 into a number.

The archive the scrubber just wrote is re-opened and scanned again over its raw text. Replacement values are recognised as replacements rather than flagged again. If anything else still matches a pattern, nothing is written to public storage at all — storing it and relying on a published check to keep it unreachable would put a leaking object in the public namespace with one query between it and the world.

The report, and what only a person can catch

Section titled “The report, and what only a person can catch”

The report lists every removed field, every dropped filter with the value it held, every real-to-replacement pair, and any file that could not be inspected. It stays with the record and is never published.

It is read before the dashboard is, because some things no rule here can find: a venue name, a network name or a site label sitting in a chart title or a block of prose. Those have no shape, and the read is what catches them.

MechanismWhat it prevents
Two storage prefixes — originals private, published artifacts publicNothing public shares a namespace with an original
Asset URLs take an id, a revision and one of three fixed filenamesThere is no input that can address an object outside the public namespace
Every public query bakes status = 'published' into its SQLA route cannot forget to filter for published-ness
Only the revision a record currently points at is servedA superseded, less redacted revision cannot be reached by guessing the previous number
Public responses list what goes out rather than deleting what must notA new column is private until someone decides otherwise

That last row is why original keys, the scrub report and the rectangle coordinates are absent from every public response: they were never added to the list of things that go out.