Move the console onto ReUI PRO

Install ReUI (MCP, skill, @reui registry, Tailwind v4, shadcn base-nova) and move
the shared UI onto it: Button, Tag, Check, Input/Textarea, Segmented, Alert, Panel,
Dialog, Tooltip, Menu and Select (Popover), Toast (sonner), the Table frame
(DataGrid), the Dashboard KPI strip, the shnt Ops palette, Geist and the type scale.

Design only: behaviour, texts and roles are unchanged. Tests that asserted the old
DOM were updated; the tests now format numbers in English wherever they run.
docs/reui-migration-plan.md records the plan and its decisions.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
DenozordecandClaude Sonnet 5.5 committed 2026-10-27 00:06:06 +07:00
commit f3531c64f9
680 files changed
+139836

No files matched your search

+142
View File
@@ -0,0 +1,142 @@
# Development harness
Two Technitium instances in Docker, to develop and verify the console redesign
without touching any real DNS server.
| Instance | Port | What it serves |
|---|---|---|
| `dev` | http://127.0.0.1:5380 | `dist/` mounted from the repo |
| `ref` | http://127.0.0.1:5381 | the `www` from the official image, untouched |
| `node2` | http://127.0.0.1:5382 | `dist/` too: the second node of the cluster |
User `admin`, password `technitium-ui-dev` on all three.
`node2` mounts the same build as `dev` on purpose: it is the only way to see a
screen **from the other side of a cluster**, and that is where half of `Cluster`'s
behaviour lives.
## The installer has its own bench
`sh dev/installer-probe.sh` measures `install.sh` against
`docs/2026-09-07-installer-contract.md`: thirty-one cases, one throwaway
container each off the same official image, exit code = cases failed. It does not
use the three containers below and does not touch them. C26 to C30 (2026-10-01)
also build the Docker init image from `dist/` and bring up a compose project of
their own, `installer-probe-<pid>`; they pull `busybox:stable` and `docker:cli` the
first time, and C30 mounts the Docker socket into `docker:cli`. `INSTALLER=<file>`
measures another `install.sh`: that is how the cases added on 2026-09-30 were
seen failing on the old script before they were seen passing on the new one.
The mode B cases need a server that honours
`DNS_SERVER_WEB_SERVICE_WWW_FOLDER_PATH`. **Since v15.5 (2026-09-19) the official
image does**, so they run against `technitium/dns-server:latest` with no build:
30 met, 1 not applicable (C15, which needs a server that ignores the variable).
They are not skipped by decree: the bench asks the image and believes the answer.
Before v15.5 the only server that honoured it was one built from the fork's
`feat/configurable-www-folder` branch, and later from `upstream/develop`. How that
was built (`TechnitiumLibrary` from its `develop` branch beside the server, five
`dotnet build` and a `dotnet publish` in `mcr.microsoft.com/dotnet/sdk:10.0`) is
in this file's history (`git log -p dev/README.md`); it is not needed any more.
## The cluster is DOWN: it was up on purpose, and it did its job
`dev` and `node2` formed a real two-node cluster: domain `cluster.test`, `dev`
primary and `node2` secondary from 2026-09-04 until Administration was built.
**It was not incidental.** `screens/admin/Cluster.tsx` is 1,629 lines, the largest
screen in the console, and without a cluster the harness only ever shows "Cluster
Not Initialized" and two buttons. Building against that would have meant building
only its empty state. With it, the screen was read **from both sides**: and that is how the
`Edit Node` rule turned out to depend on *who is looking*, not on the row:
```
isPrimary ? n.state === 'Self' : n.state === 'Self' || n.type === 'Primary'
```
### Taken down on 2026-09-04, and how
The condition it was kept for (**Administration built, recaptured and the gate
green**) was met: the section was built from both sides, `Edit Node` was checked on
the primary (one row) and on a secondary (two, its own and the primary's), and the
four `Force` dialogs were opened against a real cluster.
It came down **through the product's own path and without forcing**, which is also
the last check those two endpoints get:
```bash
# 1 · the secondary leaves, coordinating with the primary
T2=$(curl -s 'http://127.0.0.1:5382/api/user/login?user=admin&pass=technitium-ui-dev' | jq -r .token)
curl -s "http://127.0.0.1:5382/api/admin/cluster/secondary/leave?token=$T2&forceLeave=false&node="
# 2 · with no secondaries left, the primary deletes the cluster
TOK=$(curl -s 'http://127.0.0.1:5380/api/user/login?user=admin&pass=technitium-ui-dev' | jq -r .token)
curl -s "http://127.0.0.1:5380/api/admin/cluster/primary/delete?token=$TOK&forceDelete=false&node="
# 3 · and the zone the initialisation created, which upstream leaves behind
# ("no data loss except for the Cluster configuration")
curl -s "http://127.0.0.1:5380/api/zones/delete?token=$TOK&zone=cluster.test"
```
**Order matters and `force` is not needed**: unforced, `primary/delete` only works
once there are no secondaries, which is exactly why step 1 goes first. Doing it
this way is the only end-to-end exercise those two endpoints have had against a
real cluster.
The harness is back to its usual state: **six zones on `dev`, no cluster**, the
three containers up and the server domains back to `dev` and `node2`.
```bash
# how it was created, if it ever has to be rebuilt
TOK=$(curl -s 'http://127.0.0.1:5380/api/user/login?user=admin&pass=technitium-ui-dev' | jq -r .token)
curl -s -G http://127.0.0.1:5380/api/admin/cluster/init \
--data-urlencode "token=$TOK" \
--data-urlencode 'clusterDomain=cluster.test' \
--data-urlencode 'primaryNodeIpAddresses=172.23.0.4' # the docker IP of `dev`
T2=$(curl -s 'http://127.0.0.1:5382/api/user/login?user=admin&pass=technitium-ui-dev' | jq -r .token)
curl -s -X POST http://127.0.0.1:5382/api/admin/cluster/initJoin \
--data-urlencode "token=$T2" \
--data-urlencode 'secondaryNodeIpAddresses=172.23.0.3' \
--data-urlencode 'primaryNodeUrl=https://dev.cluster.test:53443/' \
--data-urlencode 'primaryNodeIpAddress=172.23.0.4' \
--data-urlencode 'ignoreCertificateErrors=true' \
--data-urlencode 'primaryNodeUsername=admin' \
--data-urlencode 'primaryNodePassword=technitium-ui-dev' \
--data-urlencode 'primaryNodeTotp='
```
Two things that cost a try each: the domain cannot be one that already exists as a
non-primary zone (`lab.test` was rejected) and **the join needs the primary's
DOMAIN NAME, not its IP**, even though it takes the IP as a separate parameter.
The IPs are the container ones and change if the network is recreated:
`docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' technitium-ui-dev`.
```bash
docker compose up -d # start
./check-parity.sh # compare dev's front page against ref
./check-parity.sh /api/ # compare another path
docker compose down -v # tear everything down, config included
```
`ref` is the reference truth: the project's constraint is that behaviour must not
change, so any non-visual divergence between the two is a bug.
Port 53 is not mapped (`systemd-resolved` holds it on WSL) and DHCP is not
enabled: only the web console is exercised here.
## Why the comparison normalises line endings
`check-parity.sh` strips `\r` before hashing, so it compares content and not
bytes. This is necessary, not cosmetic:
Upstream's `.gitattributes` declares `* text=auto`. Git stores **LF** in the
repository blobs and converts on checkout according to the platform. Our tree on
Linux has LF (`git status` sees it clean, it is byte-correct with respect to
the repository), while **the official Docker image ships CRLF**, because it was
built on Windows.
Without normalising, two files with identical content come out different by one
byte per line: in `index.html` that is 7,426 bytes of difference out of 619,718.
Found when running the first parity check.