Bumps [globals](https://github.com/sindresorhus/globals) from 15.15.0 to 17.12.0. - [Release notes](https://github.com/sindresorhus/globals/releases) - [Commits](https://github.com/sindresorhus/globals/compare/v15.15.0...v17.12.0) --- updated-dependencies: - dependency-name: globals dependency-version: 17.12.0 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com>
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.