---
title: "Dashboard app — local dev and deploy"
description: "Mock-mode local dev, the @virdx/ui-components build prerequisite, and the image-tag + Keel deploy model."
image: "https://docs.virdx.dev/img/virdx-social-card.png"
---

> Documentation Index
> Fetch the complete documentation index at: https://docs.virdx.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Dashboard app — local dev and deploy

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](https://keel.sh) (`keel.sh/trigger: poll`, `@every 5m`, `policy: force`)
  — see
  [Keel auto-redeploy](/knowledge/wiki/workstreams/infrastructure/sops/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:

```dockerfile
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).

Source: https://docs.virdx.dev/knowledge/wiki/workstreams/infrastructure/sops/dashboard-app-dev-and-deploy/index.mdx
