Covers apps/dashboard-api + apps/dashboard-frontend (includes the
Compute section: Argo workflows, Tekton builds, Coder workspaces — mono PR
#248).
Local dev
dashboard-apihas a mock mode for most endpoints:DASHBOARD_MOCK=truemakes/builds,/workflows,/coder, etc. take anif settings.mock:fast-path returning canned data frommock_data.py, andrequire_userreturns a fakedev.user(auth fully bypassed). For compute-tab work you don’t need vxdata/postgres at all — just dashboard-api (mock) + the frontend. The blog is the one endpoint mock mode does not cover:mock_data.pyhas no blog fixture androuters/notion.pyhas nosettings.mockfast-path (unlike every other router), so blog work needs either a real Notion token or the fixture-driven headless render recipe below — do not expectDASHBOARD_MOCK=truealone to unblock it.@virdx/ui-components(the shared 56-component library) must be built before the frontend runs: after a freshbun installthere is nodist/, and Vite fails to resolve@virdx/ui-components/ui. Runbunx nx build ui-componentsfirst. After editing/rebuilding it, clearnode_modules/.viteand restart the dev server — Vite will not re-optimize a workspace dependency’s dist automatically.scripts/dashboard-stack-up.shuses aprocess-composeTUI that dies when launched in the background (open /dev/tty: device not configured) — pass-t=falseto run it headless. It also relaunches vxdata+postgres, which collides if you already have your own vxdata stack up (itsprocess-composeREST API defaults to port 8080, same as dashboard-api). For compute-only work, skip the script entirely: run dashboard-api (mock) on a free port + the frontend withVITE_DASHBOARD_API_PROXY_TARGETpointed at it.- Real Keycloak login does not work locally: the
dashboardKeycloak client (infra_ansibleinventories/group_vars/keycloak/keycloak.yaml) only whitelistshttps://dashboard.fra.virdx.dev/*as a redirect URI, nothttp://localhost:5173/*. Use mock mode instead of trying to fix this per-session. Also needed for a full local stack:DASHBOARD_TLS_CA_FILEon macOS, and a separateDASHBOARD_CODER_TOKENfor the Coder card.
UI-components reuse
The compute PR initially rolled its own UI via a local
apps/dashboard-frontend/src/components/ui/* shadcn copy rather than the
shared @virdx/ui-components. That’s split-brain debt: prefer importing
from @virdx/ui-components/ui (Badge/Button/Card/Table/Skeleton/Dialog
etc.) in new/touched dashboard-frontend code, and treat a full migration off
the local copy as an open follow-up (only compute-PR files have been
migrated so far). Do not add app-specific variants to the shared
library from a feature PR — if you need a color/variant unavailable in the
shared Badge (e.g. no success variant), tint a neutral variant instead
(e.g. className="bg-success/10 text-success dark:bg-success/20" on
secondary) rather than extending the shared component from a feature
branch.
Blog rendering (Notion rich text + LaTeX)
dashboard-api’s services/notion.py converts Notion blocks to Markdown for
dashboard-frontend’s react-markdown pipeline (features/blog/markdown.tsx,
using remark-math + rehype-katex). Two non-obvious facts, each costly to
establish empirically (mono#295 -> PR #296):
remark-mathonly treats a double-dollar fence as display math when the fence sits on its own line — emitting the double-dollar delimiters inline around an expression produces inline math instead.remark-math’s defaultsingleDollarTextMath: trueturns any single dollar sign into math (e.g. “storage costs 5 dollars per scan” breaks if written with literal$signs) — setsingleDollarTextMath: falseand emit inline equations wrapped in doubled dollar-sign delimiters instead of single ones.
There is no way to fetch live Notion block JSON from a headless session (ntn
CLI isn’t logged in; the Notion OAuth connector needs an interactive run;
1Password lookups for the token are blocked by the permission classifier) —
don’t burn turns trying. Work from the documented Notion rich-text schema and
a hand-built block fixture instead.
Validating rendering changes headlessly (no browser needed): pipe a
Notion-block fixture through the real Python to_markdown, then pipe the
resulting Markdown through the real React plugin chain via
renderToStaticMarkup, run under bun. Gotchas:
- The script must live inside
apps/dashboard-frontend/— bun resolvesreact/jsx-dev-runtimerelative to the script’s own directory, so running it from$TMPDIRfails. - Assert on counts in the emitted HTML (
class="katex",katex-display,katex-error,<strong>, literal dollar-sign survival) rather than eyeballing it.
Frontend typecheck/lint
There are no nx targets for these (project.json only defines
dev/build/format) — run from apps/dashboard-frontend/:
bun run typecheck:checkbun run lint:check
Both have pre-existing failures on main as of 2026-07: ~171 biome errors
repo-wide (scope to touched files) and 2 tsc --noEmit errors
(DataSourcePicker.tsx, useBenchmarkSeries.ts). Stash-and-rerun to confirm
the baseline before chasing anything that looks like your own breakage.
Likewise ruff format --check already flags services/notion.py and
tests/test_blog_service.py on main — don’t “fix” pre-existing formatting
in an unrelated feature PR.
Deploy
- Dashboard apps deploy by image tag, not semver — no version bump
needed for a normal change. Manifests
infra_k8s/current/dashboard/{api,frontend}.yamlpin:latest+ Keel (keel.sh/trigger: poll,@every 5m,policy: force) — see Keel auto-redeploy for how Keel itself is deployed and the full annotation set. - Builds normally fire automatically from a scheduled argo-events poll
(
dashboard-poll) building frommain. To trigger a build manually, runapps/dashboard-{frontend,api}/deployments/build.sh— submits a Tektonbuildkit-imagePipelineRun (revisionmain, taglatest). - When advising on what needs rebuilding, scope to the whole merged
diff, not just your own commit — e.g. a frontend-only refactor commit
landing inside a PR that also touched
dashboard-apistill requires an api image rebuild. Check withgit diff <base> origin/main -- apps/<app>/. - Known transient failure: Tekton buildkit push to zot can flake with
error writing layer blob: unknown: blob upload unknown to registry— just resubmit the PipelineRun. Before diagnosing any build failure from a pasted log snippet, read the full build-pod log (kubectl logs <pipelinerun>-build-pod -n tekton-builds) — install/build steps can succeed while a later step (e.g. the registry push) is what actually failed; a snippet from the wrong step is misleading. - Keel rolls pods within ~5 minutes of a successful push.
Metrics sources
Dashboard charts pull from two backends:
- vxData query API (
/api/platform): most live series come from here viasrc/features/platform/api/query.ts(queryAPI.queryResources), queryingBenchmarkingResultresources. Seehooks/useBenchmarkSeries.ts. - Prometheus (
/api/prometheus):src/features/platform/api/prometheus.tsexposesqueryRange()hitting/api/prometheus/api/v1/query_range. TheClusterUsageCard(on the Activity page, hookuseClusterUsage.ts) queries GPU/CPU/memory as a percent of theargo-workflowsquota over the last 7 days (virdxcluster:{gpu,cpu,memory}:used / kube_resourcequota{...}).
Two different Prometheus instances (important)
prometheus.fra.virdx.dev= infra/VM-level Prometheus (node_exporter, postgres, keycloak). Has nokube_resourcequota,virdxcluster:*recording rules, or GPU/dcgm metrics.monitoring/kube-prometheus-stack-prometheus(in-cluster ClusterIP, not externally exposed) = the k8s/cluster Prometheus with kube-state-metrics,nvidia-dcgm-exporter, and thevirdxcluster:*recording rules. This is Grafana’s datasource and the source for the usage card. Locally:kubectl port-forward svc/kube-prometheus-stack-prometheus -n monitoring 9090:9090.
Prod wiring (infra_k8s): the dashboard nginx ConfigMap routes
/api/platform/→vxdata-api and /api/→dashboard-api. A
location /api/prometheus/ → in-cluster Prometheus is needed to serve the
usage card in prod (unverified: may already exist as of 2026-08-06).
Containerfile COPY allowlist
apps/dashboard-frontend/Containerfile copies root-level files by an
explicit allowlist, not the whole dir:
COPY apps/dashboard-frontend/index.html apps/dashboard-frontend/vite-env.d.ts apps/dashboard-frontend/vite.config.ts apps/dashboard-frontend/vite-plugin-runtime-base.ts apps/dashboard-frontend/tsconfig.json apps/dashboard-frontend/When you add a new root-level file that vite.config.ts imports (e.g. a vite
plugin), you must add it to this COPY line or the Tekton build will fail with
“Could not resolve …” even though bunx vite build locally succeeds (local
sees the whole dir; the container only has the allowlisted files).
bunx vite build locally is NOT equivalent to the container build. Always
verify with docker build -f apps/dashboard-frontend/Containerfile . (build
context = repo root) before trusting a green local build.
History: mono PR #163 added the runtimeBase vite plugin but did not add
vite-plugin-runtime-base.ts to the COPY line → Tekton build failed; fixed
in PR #171 (2026-07-03).