Skip to content

Dashboard app — local dev and deploy

Mock-mode local dev, the @virdx/ui-components build prerequisite, and the image-tag + Keel deploy model.

Covers apps/dashboard-api + apps/dashboard-frontend (includes the Compute section: Argo workflows, Tekton builds, Coder workspaces — mono PR #248).

Local dev

  • dashboard-api has a mock mode for most endpoints: DASHBOARD_MOCK=true makes /builds, /workflows, /coder, etc. take an if settings.mock: fast-path returning canned data from mock_data.py, and require_user returns a fake dev.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.py has no blog fixture and routers/notion.py has no settings.mock fast-path (unlike every other router), so blog work needs either a real Notion token or the fixture-driven headless render recipe below — do not expect DASHBOARD_MOCK=true alone to unblock it.
  • @virdx/ui-components (the shared 56-component library) must be built before the frontend runs: after a fresh bun install there is no dist/, and Vite fails to resolve @virdx/ui-components/ui. Run bunx nx build ui-components first. After editing/rebuilding it, clear node_modules/.vite and restart the dev server — Vite will not re-optimize a workspace dependency’s dist automatically.
  • scripts/dashboard-stack-up.sh uses a process-compose TUI that dies when launched in the background (open /dev/tty: device not configured) — pass -t=false to run it headless. It also relaunches vxdata+postgres, which collides if you already have your own vxdata stack up (its process-compose REST 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 with VITE_DASHBOARD_API_PROXY_TARGET pointed at it.
  • Real Keycloak login does not work locally: the dashboard Keycloak client (infra_ansible inventories/group_vars/keycloak/keycloak.yaml) only whitelists https://dashboard.fra.virdx.dev/* as a redirect URI, not http://localhost:5173/*. Use mock mode instead of trying to fix this per-session. Also needed for a full local stack: DASHBOARD_TLS_CA_FILE on macOS, and a separate DASHBOARD_CODER_TOKEN for 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-math only 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 default singleDollarTextMath: true turns any single dollar sign into math (e.g. “storage costs 5 dollars per scan” breaks if written with literal $ signs) — set singleDollarTextMath: false and 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 resolves react/jsx-dev-runtime relative to the script’s own directory, so running it from $TMPDIR fails.
  • 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:check
  • bun 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}.yaml pin :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 from main. To trigger a build manually, run apps/dashboard-{frontend,api}/deployments/build.sh — submits a Tekton buildkit-image PipelineRun (revision main, tag latest).
  • 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-api still requires an api image rebuild. Check with git 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 via src/features/platform/api/query.ts (queryAPI.queryResources), querying BenchmarkingResult resources. See hooks/useBenchmarkSeries.ts.
  • Prometheus (/api/prometheus): src/features/platform/api/prometheus.ts exposes queryRange() hitting /api/prometheus/api/v1/query_range. The ClusterUsageCard (on the Activity page, hook useClusterUsage.ts) queries GPU/CPU/memory as a percent of the argo-workflows quota 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 no kube_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 the virdxcluster:* 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).

Navigation

Type to search…

↑↓ navigate↵ selectEsc close