Skip to content

feat(dashboard): add an HTML widget for author-supplied HTML, CSS, and JS - #2971

Merged
simlarsen merged 2 commits into
masterfrom
claude/dashboard-html-widget-cdfaf7
Aug 3, 2026
Merged

feat(dashboard): add an HTML widget for author-supplied HTML, CSS, and JS#2971
simlarsen merged 2 commits into
masterfrom
claude/dashboard-html-widget-cdfaf7

Conversation

@simlarsen

Copy link
Copy Markdown
Contributor

Adds a widget you can paste arbitrary HTML, CSS, and JavaScript into.

How it renders

The author's content becomes the srcdoc of an iframe with sandbox="allow-scripts" and never allow-same-origin. That puts the document on an opaque origin: it can build DOM, run timers, fetch, draw — and it cannot touch the dashboard's DOM, cookies, localStorage, or API session, nor navigate the tab.

That boundary is the feature's whole security story, because a new widget type is public by construction — App/FeatureSet/PublicDashboard re-exports the same canvas with no allow-list, and it is served from the same origin as /dashboard and /api. allow-same-origin together with allow-scripts is equivalent to no sandbox at all (the frame can strip its own attribute), so getSandboxAttribute() has no input that emits it, and a test asserts that across every permission combination.

Internal/Roadmap/SessionReplay.md argues against allow-scripts without allow-same-origin. That conclusion is specific to replay, where the rrweb Replayer runs in the parent and writes into contentDocument — it needs same-origin and no script execution. This widget is the exact inverse, and the code says so where a reviewer will look.

What the author gets

  • Three code editors — HTML, CSS, JavaScript — reusing the existing Monaco FormFieldSchemaType.HTML/CSS/JavaScript fields. Three new ComponentInputType members map to them; no Common/UI changes.
  • Dashboard variables via {{variableName}} in any of the three fields, resolved through the same DashboardVariableInterpolation the query widgets use.
  • window.ONEUPTIME — resolved variables plus the dashboard's time range as ISO strings.
  • Reload on dashboard refresh, so a widget that fetches its own data keeps up.
  • Three permission toggles — Run JavaScript (on), Open links in a new tab (on), Allow forms to submit (off). Tokens are added to an empty sandbox, never removed from a permissive one.

Notes for review

  • Common/Utils/Dashboard/HtmlWidgetDocument.ts is pure string transforms, so the sandbox construction and the </style> / </script> containment escaping are testable without a browser. The React component only hands the result to the iframe.
  • The useMemo deps are serialized keys, not the variables/dashboardStartAndEndDate object references. A relative range resolves against the clock, so rebuilding on an unrelated render would reload the frame and discard whatever the author's script had built.
  • The iframe gets pointer-events: none in edit mode — otherwise it swallows the clicks the canvas needs to select, drag, and resize the widget.
  • No new dependencies.

Tests

233 assertions, all passing, plus the full existing dashboard suites (Common/Tests/Utils/Dashboard 143, App/Tests/Dashboard 1875).

  • Common/Tests/Utils/Dashboard/HtmlWidgetDocument.test.ts (62) — sandbox tokens across all 8 permission combinations (never allow-same-origin, never allow-top-navigation, only tokens asked for, always a string so React can't drop the attribute); variable resolution; interpolation edge cases (regex-metacharacter names, $& in values, no recursive expansion); element containment asserted by parsing the built document with DOMParser and checking no <img> appeared and the script count is still 2.
  • Common/Tests/Utils/Dashboard/DashboardHtmlComponent.test.ts (16) — defaults, argument declarations, and registry dispatch.
  • App/Tests/Dashboard/HtmlWidgetSandbox.test.ts (13) — source-level guards: the renderer derives its sandbox from the util, neither file mentions allow-same-origin, no dashboard widget uses dangerouslySetInnerHTML, and the type is wired into all five registries. Each omission there fails differently and none fails at compile time — one of them renders a silently empty card.

Verified in a real browser

Built the actual srcdoc + sandbox from the util and loaded it in headless Chrome:

Case Result
Scripts on script ran; ONEUPTIME.variables.environment == "production"; {{environment}} interpolated into HTML and JS; origin: opaque (null); parent.document, parent.localStorage, localStorage, document.cookieSecurityError
Scripts off sandbox=""; inline <script> and onerror= in the author's markup did not run; the JS field was omitted from the document entirely
Break-out attempt </style><img onerror> in CSS and </script><img onerror> in JS both stayed inert data; CSS still applied

Docs & i18n

dashboards/widgets.md gets an ## HTML section — what it can and cannot do, variables, permissions, and who-can-edit-vs-who-runs-it — in all 16 locales. Both new catalog strings added to all 16 Locales/*.json; npm run i18n:validate and npm run docs:check-anchors pass.

…d JS

Adds a widget whose body is markup, styles, and script written by the
dashboard author. It renders inside an iframe with sandbox="allow-scripts"
and never allow-same-origin, so the document runs on an opaque origin with
no access to the parent DOM, cookies, localStorage, or API session.

That isolation is the whole security boundary, and it has to hold: the
public dashboard re-exports the same canvas and is served from the same
origin as /dashboard and /api, so every widget type becomes anonymously
viewable the moment it is registered.

The document builder is a pure module (Common/Utils/Dashboard/
HtmlWidgetDocument.ts) so the sandbox construction and the element-
containment escaping are testable without a browser.

Also supports {{variableName}} interpolation across all three fields, and
exposes the resolved variables plus the dashboard's time range to script
as window.ONEUPTIME. The widget reloads on dashboard refresh.
@simlarsen

simlarsen commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

…ml-widget-cdfaf7

# Conflicts:
#	App/FeatureSet/Dashboard/src/Components/Dashboard/Toolbar/AddWidgetModal.tsx
@simlarsen
simlarsen merged commit 7bce465 into master Aug 3, 2026
71 of 78 checks passed
@simlarsen
simlarsen deleted the claude/dashboard-html-widget-cdfaf7 branch August 3, 2026 16:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants