Skip to content

Dashboard

The conformance dashboard is the human-facing view over the check store, served by the same binary at /dash. It answers: how conformant is our generated content, is it trending the right way, and where is the drift coming from?

Overview (/dash):

  • KPI tiles — pass rate and check volume, new violations, and baselined debt (accepted violations still being tracked).
  • Time-range control — 7d / 30d / 90d scopes the KPIs, trend, breakdowns, and failures; the range is a query parameter, so every view is bookmarkable.
  • Pass-rate trend — rendered server-side as SVG.
  • Outdated assets — how many distinct assets still carry each category of outdated-asset rule (logos / colors / fonts), counted from each asset’s latest check so fixed assets drop off. Hidden when no outdated rules have fired. Latest-check counts describe current state, so the range doesn’t apply.
  • Breakdowns — checks and failures sliced several ways, each total split into checks vs still-actionable/failed:
    • By repository — assets committed to a project (a discovered .stylist.yaml, or a confirmed git repo root for the pre-commit hook).
    • Loose assets — checks with no repo at all: one-off, agent-generated content that’s never entered a project, grouped by ruleset instead (there’s no repo to group by, and the ruleset is what identifies the client/brand).
    • By person — the verified submitter (from stylist login), when known; checks authenticated only by a shared CI/org token show as unattributed rather than being hidden.
    • By adapter — git-hook / claude-code / codex / cli / baseline.
    • By audienceinternal vs. external, when declared; undeclared checks show as “not declared” rather than being hidden. That’s a different fact from “unattributed” above, so it gets its own label.
    • Violations by check type — colors / fonts / terms / etc.
  • Recent failures — the latest failing checks with their new violation counts, linking to the repo asset page or the loose asset page depending on whether the check had a repo.

Drill-downs — repo names, ruleset names, person emails, asset names, and check names all link onward:

  • Repository (/dash/repos/{repo}) — the same KPIs and trend scoped to one repo, plus an assets table judged by each asset’s latest check: last result (pass / fail / advisory), actionable and baselined violation counts.
  • Loose (/dash/loose/{ruleset}) — the repository page’s counterpart for loose assets, scoped by ruleset instead of repo (no Outdated Assets KPI here: that query is repo-keyed).
  • Person (/dash/users/{email}) — one verified submitter’s checks, spanning both project and loose assets (the same person might run the CLI against a repo one day and generate a loose asset for a client the next).
  • Asset (/dash/repos/{repo}/asset?path=…, or /dash/loose/{ruleset}/asset?path=… for a loose asset) — the latest check’s facts (result, when, adapter, ruleset) and its full violation list (baselined rows stay visible, muted and tagged), plus the asset’s check history — every check event, each with its own audience value, since audience is a property of the check, not a fact that gets overwritten on the asset (an asset checked once with no audience declared and later with one set shows both in its history, not just the latest).
  • Rule impact (/dash/checks/{check}) — a rule’s blast radius: every asset whose latest check still violates it, grouped by repo. Fix an asset and it drops off on its next clean check.

The UI is server-rendered HTML and CSS with no external assets: nothing to CDN, works air-gapped. A vendored htmx upgrades link navigation to in-place swaps; with JavaScript disabled everything still works as plain links. All styling lives in one stylesheet driven by a design-token block, so reskinning is a one-file edit.

The dashboard needs two things the API doesn’t:

  1. A check store (serve -db …) — no store, no data, no dashboard routes.
  2. A license with the dashboard feature (STYLIST_LICENSE / serve -license). Without one, /dash shows a locked page and leaks no data; checks and the API keep working. See Licensing.

Two mechanisms share the same session layer:

OIDC / SSO (recommended) — any OpenID Connect provider (Okta, Entra, Google Workspace, …):

Terminal window
stylist serve \
-oidc-issuer https://your-idp.example \
-oidc-client-id stylist-dashboard \
-dash-url https://stylist.internal
STYLIST_OIDC_CLIENT_SECRET= # env-only, like all secrets

Register https://stylist.internal/dash/oauth2/callback as the redirect URI with your provider. /dash/login then sends users through the provider; the callback verifies the ID token (signature, issuer, audience, expiry) and issues an HttpOnly session cookie. Provider discovery is lazy, so an unreachable IdP never stops the check API from serving.

Multi-org SSO. For a hosted deployment where different customers sign in to the same dashboard, add -oidc-org-claim naming the ID-token claim that carries the signed-in user’s org (your IdP or an org-mapping step in front of it needs to actually assert this claim; stylist doesn’t infer it). Once set, a session carries that org, and every dashboard page a signed-in user sees is scoped to it the same way /v1/check already is under -multi-org. See Multi-org mode. Leaving it unset (the default) means every session is unscoped, which is correct for self-host and any single-org deployment.

Access token (fallback / small teams) — set STYLIST_DASH_TOKEN (or serve -dash-token); users enter it once on the login form. With neither OIDC nor a token configured the dashboard is open. This is reasonable only when it’s reachable solely from a private network.

Sessions live in the check store (only a SHA-256 of the cookie token is persisted), so they survive server restarts and every replica behind a load balancer honors the same cookie. A session lasts 7 days from sign-in; when it expires, OIDC users are silently bounced through the IdP and back — while your IdP session lives, you never retype anything. The header’s Sign out deletes the session server-side, so a copied cookie stops working everywhere immediately.