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]>
143 lines
6.9 KiB
Markdown
143 lines
6.9 KiB
Markdown
# 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.
|