Add configurable custom frontends for Trackio#531
Conversation
🪼 branch checks and previews
|
🦄 change detectedThis Pull Request includes changes to the following packages.
|
🪼 branch checks and previews
Install Trackio from this PR (includes built frontend) pip install "https://huggingface.co/buckets/trackio/trackio-wheels/resolve/88ca781fe1808b2529cf61ed69317c92cd92194d/trackio-0.24.2-py3-none-any.whl" |
|
The docs for this PR live here. All of your documentation changes will be reflected on that endpoint. The docs are available until 30 days after the last update. |
There was a problem hiding this comment.
Pull request overview
Adds a configurable “custom frontend directory” mechanism to Trackio so users can swap the dashboard UI (locally and on Spaces) while continuing to use the existing /api/* backend.
Changes:
- Introduces frontend resolution (arg → env → persisted config → bundled → starter fallback) and a persisted config file under
HF_HOME. - Refactors frontend serving to mount an arbitrary static directory (SPA-style fallback to
index.html). - Wires
frontend_dir/--frontendthroughtrackio.show(), CLI commands, and Spaces/static deploy flows; ships starter + example theme frontends with docs/tests.
Reviewed changes
Copilot reviewed 37 out of 41 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| trackio/server.py | Passes frontend selection into Starlette app build. |
| trackio/frontend_server.py | Serves arbitrary static frontend directories (SPA fallback). |
| trackio/frontend_config.py | Adds resolver + persisted config for frontend directories. |
| trackio/init.py | Exposes frontend_dir on trackio.show() and resolves it. |
| trackio/cli.py | Adds --frontend flags and trackio config subcommands. |
| trackio/deploy.py | Deploys selected frontend to Gradio/static Spaces flows. |
| trackio/frontend_templates/starter/index.html | Starter fallback frontend HTML template. |
| trackio/frontend_templates/starter/styles.css | Starter fallback frontend styles. |
| trackio/frontend_templates/starter/app.js | Starter fallback frontend JS (calls /api/*). |
| tests/unit/test_frontend_config.py | Tests for persisted config + resolver precedence/fallback. |
| tests/unit/test_deploy.py | Tests for deploying custom frontends to Spaces/static Spaces. |
| scripts/launch_custom_frontend_themes.py | Script to run demo themes locally on multiple ports. |
| pyproject.toml | Packages frontend templates into wheel artifacts. |
| plans/issue-491-custom-frontends-plan.md | Implementation plan document for issue #491. |
| docs/source/quickstart.md | Documents --frontend / frontend_dir usage. |
| docs/source/launch.md | Documents custom frontend + persistent config CLI. |
| docs/source/environment_variables.md | Documents TRACKIO_FRONTEND_DIR. |
| docs/source/deploy_embed.md | Documents deploying custom frontend via sync/freeze. |
| README.md | README updates for custom frontend + deploy flows. |
| .changeset/little-cats-bet.md | Minor version changeset for the feature. |
| examples/custom-frontends/README.md | Explains example frontends (non-runtime). |
| examples/custom-frontends/brutalist-lab/frontend/index.html | Demo theme frontend markup. |
| examples/custom-frontends/brutalist-lab/frontend/styles.css | Demo theme styling. |
| examples/custom-frontends/brutalist-lab/frontend/app.js | Demo theme bootstrap JS. |
| examples/custom-frontends/brutalist-lab/frontend/shared-theme.js | Demo theme logic + plotting. |
| examples/custom-frontends/signal-console/frontend/index.html | Demo theme frontend markup. |
| examples/custom-frontends/signal-console/frontend/styles.css | Demo theme styling. |
| examples/custom-frontends/signal-console/frontend/app.js | Demo theme bootstrap JS. |
| examples/custom-frontends/signal-console/frontend/shared-theme.js | Demo theme logic + plotting. |
| examples/custom-frontends/sunrise-cards/frontend/index.html | Demo theme frontend markup. |
| examples/custom-frontends/sunrise-cards/frontend/styles.css | Demo theme styling. |
| examples/custom-frontends/sunrise-cards/frontend/app.js | Demo theme bootstrap JS. |
| examples/custom-frontends/sunrise-cards/frontend/shared-theme.js | Demo theme logic + plotting. |
| examples/custom-frontends/editorial-grid/frontend/index.html | Demo theme frontend markup. |
| examples/custom-frontends/editorial-grid/frontend/styles.css | Demo theme styling. |
| examples/custom-frontends/editorial-grid/frontend/app.js | Demo theme bootstrap JS. |
| examples/custom-frontends/editorial-grid/frontend/shared-theme.js | Demo theme logic + plotting. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| for source, candidate in _configured_frontend_candidates(frontend_dir): | ||
| if is_valid_frontend_dir(candidate): | ||
| if source == "config" and announce: | ||
| _announce_config_frontend(candidate) | ||
| return ResolvedFrontend( | ||
| path=candidate, | ||
| source=source, | ||
| is_custom=True, | ||
| ) | ||
| print( | ||
| f"* Trackio frontend from {source} is invalid: {candidate}. " | ||
| f"Falling back to starter template at {STARTER_FRONTEND_DIR}." | ||
| ) | ||
| return ResolvedFrontend( | ||
| path=STARTER_FRONTEND_DIR, | ||
| source="starter", | ||
| is_custom=True, | ||
| used_fallback=True, | ||
| requested_path=candidate, | ||
| ) |
There was a problem hiding this comment.
resolve_frontend_dir() returns immediately on the first invalid candidate inside the loop, so later sources (env/config) are never considered. For example, if TRACKIO_FRONTEND_DIR is set but invalid, a valid persisted config (or bundled frontend) will never be used.
Consider continuing to the next candidate when a higher-precedence source is invalid, and only doing an immediate starter fallback for an explicitly provided argument (if that’s the intended behavior).
| for (const record of runRecords.slice(0, 8)) { | ||
| const item = document.createElement("div"); | ||
| item.className = "item"; | ||
| item.innerHTML = ` | ||
| <strong>${record.name || "Unnamed run"}</strong> | ||
| <div class="meta">Created: ${record.created_at || "unknown"} | Updated: ${record.updated_at || "unknown"}</div> | ||
| `; |
There was a problem hiding this comment.
This starter template renders run names/timestamps using innerHTML with values coming from the Trackio API (record.name, record.created_at, etc.). Since run names are user-controlled, this enables XSS in the fallback UI (including when deployed on Spaces).
Use textContent (or build DOM nodes) for untrusted values, or HTML-escape them before inserting into innerHTML.
| } catch (error) { | ||
| projectTitle.textContent = "Frontend error"; | ||
| runsEl.innerHTML = `<div class="item"><strong>Could not load Trackio data</strong><div class="meta">${error.message}</div></div>`; | ||
| metricsEl.innerHTML = ""; |
There was a problem hiding this comment.
error.message is interpolated directly into innerHTML in the error handler. If the backend returns an error string containing HTML, this becomes another XSS vector.
Prefer setting the message via textContent (or escaping) instead of inserting it into innerHTML.
| function renderRuns(runRecords) { | ||
| runsEl.innerHTML = ""; | ||
| if (!runRecords.length) { | ||
| runsEl.innerHTML = '<div class="item"><strong>No runs yet</strong><div class="meta">Log a run and refresh this page.</div></div>'; | ||
| return; | ||
| } | ||
|
|
||
| for (const record of runRecords.slice(0, 8)) { | ||
| const item = document.createElement("div"); | ||
| item.className = "item"; | ||
| item.innerHTML = ` | ||
| <strong>${record.name || "Unnamed run"}</strong> | ||
| <div class="meta">Created: ${record.created_at || "unknown"} | Updated: ${record.updated_at || "unknown"}</div> | ||
| `; |
There was a problem hiding this comment.
The UI labels this section as "Latest Runs", but get_runs_for_project returns runs ordered by created_at ASC (oldest-first), and the template displays runRecords.slice(0, 8) (the oldest runs). Also, run records from the API only include created_at (no updated_at), so record.updated_at will always render as "unknown".
Either adjust the backend/API to return the needed fields/order, or update the template copy/logic to match what the API actually provides (e.g., reverse/sort client-side and remove/rename the updated timestamp).
| if args.config_command == "set": | ||
| frontend_dir = set_persisted_frontend_dir(args.frontend) | ||
| print(f"Saved Trackio default frontend: {frontend_dir}") | ||
| print("Reset with `trackio config unset frontend`.") | ||
| return |
There was a problem hiding this comment.
trackio config set frontend ... can raise a ValueError from set_persisted_frontend_dir() for invalid directories, which will currently bubble up as a traceback. Other CLI flows often convert user errors into a clean message/exit code.
Consider catching ValueError here and routing it through error_exit(...) (or equivalent) for a friendlier CLI experience.
Preserve frontend source precedence when env/config paths are invalid, harden starter frontend rendering against XSS, and surface config-set validation errors as clean CLI messages. Made-with: Cursor
|
nice! can we also set e.g. the width of the plots? |
For sure, the frontend is just vanilla js/css/html, so you can modify anything now. I'll vary up these 4 examples a bit more so we can see what's possible |
Reformat touched frontend server and related script/test files to satisfy the format check without changing behavior. Made-with: Cursor
Apply Ruff import sorting so the format CI job passes. Made-with: Cursor
|
PR should be ready now! |
|
Going to do a release to get the Windows fix out. Will go ahead and merge this in (I've tested this fairly robustly & it doesn't interact much with core Trackio library so its unlikely to have cause issues) |
Closes #491
Usage:
trackio show --frontend ./my-trackio-frontend.If the directory passed to
--frontenddoes not exist, or exists but is empty, Trackio copies in the starter frontend automatically, prints that it did so, and then serves that directory. The starter is a complete plain-HTML/CSS/JS template: it calls the Trackio API, loads projects and runs, fetches metric values, and draws simple charts that you can replace with your own UI.Example Frontends:
Signal Console
Sunrise Cards
Editorial Grid