Version goes from 5.0.0.dev0 to 5.0.0 in the four places that carry it. `system.check_updates` now defaults to on. It was off with the note "the user opts in rather than being opted in", which is the right instinct aimed at the wrong thing: the check is made by the operator's browser and never by the server, so it carries nothing about the deployment. What it opts you into is finding out you are out of date, and an instance that never learns that is the more common harm on a self-hosted tool. Air-gapped boxes turn it off. Added the notice that goes with it: after signing in, once per 24 hours per browser, a modal if a newer release exists. Never for the demo account — a visitor cannot upgrade anything, and telling them how the operator runs their box is a small leak for no benefit. The comparison moved to lib/version.ts and got fixed on the way. The old one was an equality check that treated *anything* different as newer, so an instance built from main at 5.1.0.dev0 was told to upgrade to the older 5.0.0. It now understands that a pre-release is behind the release of the same numbers, which is exactly the 5.0.0.dev0 → 5.0.0 case this release creates. Seven cases checked, including that one. Both READMEs get an Updating section: pull, migrate, up, and the fact that named volumes mean the identity pool and archive stay put. Also two files for publishing releases — a bilingual template and the 5.0.0 notes written against it. English first, breaking changes above the feature list, and every entry says what the reader should do. Not committed here: nothing is tagged or published yet.
dtk console
React SPA for the dtk v5 API. Built to static files and served by the api container - no Node runtime in production, no SSR.
Specs this implements: docs/design/07-frontend.md
(what the pages are), docs/design/12-design-system.md
(what they look like - authoritative for anything visual),
docs/design/14-i18n.md (language rules) and
docs/design/06-api-auth-mcp.md (the API).
Commands
npm install
npm run dev # http://localhost:5173, proxies /api to http://127.0.0.1:8000
npm run build # typecheck, then emit web/dist
npm run preview
npm run typecheck
npm run lint # eslint + locale parity/ICU/error-code coverage
npm run verify # all three, in the order CI runs them
DTK_API_TARGET overrides the dev proxy target. VITE_API_BASE_URL is only
needed when the console is served from a different origin than the API.
Layout
src/
main.tsx theme + i18n bootstrap, root render
App.tsx shell, router, setup gate
lib/ api client, formatting, i18n, theme, tokens of behaviour
components/ the design-system inventory (import from '@/components')
hooks/ shared hooks (import from '@/hooks/...')
locales/{en,zh}/ common | console | errors | setup
pages/ one file per route
styles/ tokens.css, base.css, utilities.css
@/ resolves to src/. TypeScript runs strict plus noUncheckedIndexedAccess
and verbatimModuleSyntax, so index access is T | undefined and type-only
imports need import type.
Non-negotiables
These are enforced by lint or by review; do not work around them.
- No colour literals outside
styles/tokens.css.local/no-raw-hex-colorfails the build on a bare hex in any source file. Usevar(--accent),var(--danger),var(--bg-raised). Both themes are complete: anything you add must be readable on#0B0D10and on#FBFCFD. - No CJK in
.ts/.tsx.local/no-cjk-sourcecovers comments, strings and identifiers. Chinese lives insrc/locales/zh/*.jsononly. - Every user-visible string goes through
t(), anden/zhkey sets must match exactly -npm run lintfails otherwise. Never translate error codes, state enum values, field names, endpoint paths or config keys; translate their display labels instead. - Status is colour + icon + text. Use
<StatusBadge kind="identity" value={row.state} />rather than colouring a cell.business_errorrenders muted, never red: a deleted video is not a system fault. - Ids and JSON are mono.
<CopyableId>for any id,mark columns mono, and<CodeBlock json={...}>for payloads. - Polled values never animate. Pass
flashValuetoDataTableso a row highlights once when its status changes; do not add transitions on refresh. - Focus rings stay. No
outline: nonewithout a visible replacement. Everything must be reachable and operable with the keyboard, and overlays must close on Esc.
Calling the API
import { apiGet, apiPost, apiRequest, isApiError } from '@/lib/api'
import { paths } from '@/lib/endpoints'
import { useApiQuery, useApiMutation } from '@/hooks'
import { POLL } from '@/lib/query'
// Query with polling; errors arrive as ApiError.
const identities = useApiQuery<Identity[]>({
key: ['identities', filter],
path: paths.identities.list,
params: { state: filter },
poll: POLL.fast, // 5s. Use POLL.normal / POLL.slow, do not invent one.
})
// Meta (cursor, request_id, duration) when you need it:
const { data, meta } = await apiRequest<RequestLogRow[]>(paths.logs.requests)
The client handles the async-first protocol for you: a 202 {task_id} is polled
against /api/v1/tasks/{id} and the promise resolves with the finished result.
Pass awaitTask: false to get the raw task id (batch submits do their own
orchestration), or use waitForTask / openTaskEvents directly.
It also sends Accept-Language from the current UI language, records the
X-RateLimit-* and Retry-After headers (getRateLimit()), and routes a
401 to the login page.
Failures: error.code is the stable enum (ApiErrorPayload is a discriminated
union, so switch (error.payload.code) narrows), error.kind separates an API
failure from network, timeout, aborted and malformed. Render one with
<ErrorState error={query.error} onRetry={query.refetch} /> or
toast.apiError(error); both show the code, the localized message, the hint and
the request_id.
Formatting
const f = useFormatters() // bound to the active language
f.compact(12800) // en: 12.8K zh: 1.28万 (Intl, not a translation)
f.number(1234) f.percent(0.982) f.bytes(1234567)
f.timestamp(row.ts) f.relative(row.ts) f.duration(ms) f.latency(ms)
f.timeZone() // { iana, short } for chart axes
Missing values render as an em dash. null and 0 are different facts - never
coerce one into the other.
Components
Button Input Select Textarea Checkbox Switch Field · StatusBadge
ErrorCodeBadge CopyableId MaskedSecret · Card PageHeader MetricTile
EmptyState ErrorState Skeleton · DataTable CodeBlock TimeSeriesChart
· Modal Drawer ConfirmDialog Toast Menu · Sidebar TopBar
ThemeToggle LanguageSwitcher ErrorBoundary.
DataTable gives you sorting, a column picker, a density toggle, a sticky
header, skeleton, empty and error states, optional selection, and degrades to
cards under 768px. Give it a storageKey so the viewer's column and density
choices persist.
TimeSeriesChart is hand-rolled SVG. Colour series from the status palette:
success rate var(--success), risk rate var(--danger), volume var(--accent),
categorical comparisons var(--series-1..6).
Pages
src/pages/*.tsx are scaffold placeholders that render a PageHeader. Replace
the body, keep the console:page.<name>.* keys, and add page-specific keys to
locales/{en,zh}/console.json in the same commit. Routes live in App.tsx and
navigation entries in lib/nav.ts.
Setup gate
App.tsx reads GET /api/setup/status. While initialized is false, every
route redirects to /setup. If that call fails at the transport level the
console fails open rather than locking the operator out of the pages that would
explain the outage - only a hard network error renders a full-page error.
Responsive
< 768px sidebar becomes a drawer and tables become cards; 768-1280px the
sidebar is icons only; >= 1280px the full 240px sidebar with a 1440px content
column. The console must stay usable on a phone: that is where the 3am alert
lands.