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?
What’s on it
Section titled “What’s on it”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 audience — internal 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.
- By repository — assets committed to a project (a discovered
- 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.
Requirements
Section titled “Requirements”The dashboard needs two things the API doesn’t:
- A check store (
serve -db …) — no store, no data, no dashboard routes. - A license with the
dashboardfeature (STYLIST_LICENSE/serve -license). Without one,/dashshows a locked page and leaks no data; checks and the API keep working. See Licensing.
Sign-in
Section titled “Sign-in”Two mechanisms share the same session layer:
OIDC / SSO (recommended) — any OpenID Connect provider (Okta, Entra, Google Workspace, …):
stylist serve … \ -oidc-issuer https://your-idp.example \ -oidc-client-id stylist-dashboard \ -dash-url https://stylist.internalSTYLIST_OIDC_CLIENT_SECRET=… # env-only, like all secretsRegister 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 and sign-out
Section titled “Sessions and sign-out”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.