fancy-dns-console is a second binary holding the web UI, authentication and every edit. It writes ClientGroup, BlockList and DNSZone objects through the new kubeedit package and forwards all other /api routes to a running resolver with its own key, dropping the browser's credentials, so it can run in the cluster or on a workstation with a kubeconfig and nothing about the resolver's placement matters. The resolver never imports kubeedit and the console is not on the DNS path. kubeedit validates every spec with the runtime's own converters (kubesource.CheckGroup/CheckList/CheckZone) before writing, so the console cannot store an object the resolver would reject. Zone edits keep the file-based writer's behaviour: duplicate and below-a-delegation records are refused, a delegation in a new zone also gives the zone its servers, and a zone with no apex NS is never stored. Each object records the editing user in an annotation; updates retry on conflict. The resolver reports groups from ClientGroup objects as source "kubernetes" and counter-only rows (the "(default)" bucket, or a group removed since) as "history", and lists DNSZone zones as the editable ones. The console uses that to leave the YAML's groups alone and to drop history rows, which the UI would otherwise send back on save. Its GET /api/clients overlays the cluster's current objects on the resolver's list, because the resolver only sees an edit after its watch fires and a save made just after another would delete the first. An end-to-end test runs both halves against one fake apiserver: a group saved through the console refuses queries, a zone built record by record answers authoritatively, and a request with the resolver's own key is rejected by the console. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The console
Vite + React + TypeScript + Tailwind, built into internal/web/dist and served
by the Go binary from an //go:embed. There is no separate deployment: the
binary is the console.
Day-to-day
make web-check # npx tsc -b --noEmit && npm run build (what CI runs)
make web-dev # Vite dev server, proxying /api to 127.0.0.1:8080
make web # production build into internal/web/dist
For make web-dev to show anything, a fancy-dns has to be listening on
127.0.0.1:8080 with the API key the console sends. deploy/config.example.yaml
binds there, but on port 53; rewrite the ports (or use make smoke, which does
it for you) before starting it.
Two traps that have cost time, both from AGENTS.md:
tsc -breplays cached diagnostics fromnode_modules/.tmp/*.tsbuildinfo. After a large editrm -f node_modules/.tmp/*.tsbuildinfo, or you will read errors that no longer exist and miss ones that do.npm run buildemptiesinternal/web/distand deletes its.gitkeep. Restore it (git checkout -- internal/web/dist/.gitkeep). The Go build still works while the directory is empty, so a console rebuild in the middle of someone else'sgo buildis not fatal — but the commit is.
The API contract
src/lib/types.ts is a hand-written mirror of the Go json tags in
internal/api and internal/app/hooks.go. apiFetch ends in
JSON.parse(text) as T, so the compiler checks the console against its own
declarations and nothing else: a field that loses its json tag, or a response
a struct never had, shows up as an empty table in a browser and nowhere else.
That is not hypothetical — a round of this found the Sources table, the ACL
group cards and both Save buttons broken exactly that way.
So, when you touch either side:
- Go: add or keep the
jsontag.internal/apphas contract tests that assert the exact key set of every admin endpoint; they are the spec, and a rename fails them. - Console: change the interface in the same commit. If the server is wrong,
fix the server — do not "adapt" in the client, and do not paper a mismatch
over with
as any, an optional field or a?? 0default. Those casts are precisely what hides drift. - Never rename a field on one side only. The dashboard, the ServiceMonitor queries and the Grafana panels are consumers of the same JSON.
The durable fix is to generate types.ts from the Go structs, or to publish an
OpenAPI document from the server and generate from that. It is in the plan at
the end of STATUS.md.
What is checked
npx tsc -b --noEmit (strict, with noUnusedLocals) and oxlint (two rules:
react/rules-of-hooks and react/only-export-components). There is no test
runner and no component test — the console's correctness rests entirely on its
types, which are only as good as the mirror above. Adding vitest and a handful
of tests for lib/format.ts (the percent/latency/duration formatters, which
have already shipped a units bug) is cheap and worth doing.