Files
Evil0ctal b4f6ff5c15 feat: 5.0.0, and tell people when there is a newer one
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.
2026-09-10 07:07:35 -07:00
..

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.

  1. No colour literals outside styles/tokens.css. local/no-raw-hex-color fails the build on a bare hex in any source file. Use var(--accent), var(--danger), var(--bg-raised). Both themes are complete: anything you add must be readable on #0B0D10 and on #FBFCFD.
  2. No CJK in .ts/.tsx. local/no-cjk-source covers comments, strings and identifiers. Chinese lives in src/locales/zh/*.json only.
  3. Every user-visible string goes through t(), and en/zh key sets must match exactly - npm run lint fails otherwise. Never translate error codes, state enum values, field names, endpoint paths or config keys; translate their display labels instead.
  4. Status is colour + icon + text. Use <StatusBadge kind="identity" value={row.state} /> rather than colouring a cell. business_error renders muted, never red: a deleted video is not a system fault.
  5. Ids and JSON are mono. <CopyableId> for any id, mark columns mono, and <CodeBlock json={...}> for payloads.
  6. Polled values never animate. Pass flashValue to DataTable so a row highlights once when its status changes; do not add transitions on refresh.
  7. Focus rings stay. No outline: none without 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.