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]>
No files matched your search
@@ -0,0 +1,758 @@
|
||||
# The installer: contract
|
||||
|
||||
**Date:** 2026-09-07 · **Measured with:** `dev/installer-probe.sh` against
|
||||
`technitium/dns-server:latest` (v15.4.0.0) · **Ratified 2026-09-07** after two
|
||||
readings: F4, W3, A1, A7 and S2 rewritten in the first, two of them because
|
||||
they contradicted other clauses, and A2 in the second, because it promised
|
||||
something no installer can deliver, and **implemented the same day**.
|
||||
|
||||
> Read this before touching `install.sh`. The installer is not missing: it was
|
||||
> written on 2026-09-01 (`b20842d`) and it already does most of what it should.
|
||||
> What is missing is the contract it has to meet, and a way to say whether it
|
||||
> meets it. This document is the first; `dev/installer-probe.sh` is the second.
|
||||
|
||||
**Where it stands: all twenty-one clauses are met, and all twenty-one probe
|
||||
cases have been seen passing.** Nineteen of them against the official image, and
|
||||
the two mode B cases against a server built from the fork branch that carries the
|
||||
variable: because until that branch is merged, that is the only server there is
|
||||
that honours it. Building one took four commands; they are in the history of
|
||||
`dev/README.md`. Those numbers come out of the probe, not out of this paragraph.
|
||||
|
||||
> **2026-09-30.** The security audit of that day
|
||||
> found three ways the installer could remove what is not a console. They are
|
||||
> now clauses W6, W7 and A8, with a fourth, I1, for the download itself: twenty-five
|
||||
> clauses and twenty-five cases. Measured against `technitium/dns-server:latest`
|
||||
> (v15.5.1, which honours the variable): **24 met, 0 not met, 1 not applicable**
|
||||
> (C15). C22 to C25 were first seen failing against the `install.sh` of `HEAD`.
|
||||
|
||||
> **2026-10-01.** Docker gets an install path of its own: an init image that
|
||||
> copies the console into a volume the official server mounts (README.md,
|
||||
> "Docker"). Four clauses for it, D1 to D4;
|
||||
> amendments to W6, A6 and I1, W4 giving way inside the image (§3, "Docker")
|
||||
> and the Docker host's steps in §4; and five cases, C26 to C30, with C11, C17
|
||||
> and C22 extended: thirty cases.
|
||||
> Measured against `technitium/dns-server:latest` (v15.5.1): **29 met, 0 not met,
|
||||
> 1 not applicable** (C15). C11 and C26 to C30 were first seen failing against
|
||||
> the `install.sh` of `main`, and so were C17's step for a TERM mid-sweep and
|
||||
> C22's for `--dir /`. What C22's last step checks, no state folder left by a
|
||||
> refused run, was seen failing by hand against the `install.sh` of 7cf2d09.
|
||||
> And a correction the same day: the variable is upstream's since
|
||||
> v15.5 (§1.7), so the official image honours it, the fork build the 2026-09-07
|
||||
> status relies on is not needed any more, and the `†` has come off W2, F3 and
|
||||
> S2.
|
||||
|
||||
> **2026-10-02.** Bare-metal systemd setup is now managed end to end. C31
|
||||
> measures first activation, an update without a restart, clean uninstall and
|
||||
> refusal to remove an administrator-modified drop-in: thirty-one cases.
|
||||
|
||||
The console goes in front of a DNS server that a whole house resolves through.
|
||||
The installer is the only part of this project that writes to somebody else's
|
||||
machine, so it is the part that has to be boring.
|
||||
|
||||
---
|
||||
|
||||
## 1 · What upstream does
|
||||
|
||||
Everything in this section is read from upstream's own sources in the fork
|
||||
(`projects/technitium-ui/`) or measured against the official image. Nothing here
|
||||
is inferred from documentation.
|
||||
|
||||
### 1.1 Where the web root comes from
|
||||
|
||||
`DnsWebService.cs:1832` (upstream `master`):
|
||||
|
||||
```csharp
|
||||
builder.Environment.WebRootFileProvider = new PhysicalFileProvider(Path.Combine(_appFolder, "www"))
|
||||
```
|
||||
|
||||
`_appFolder` is the directory of `DnsServerApp.dll` (`DnsWebService.cs:165`), so
|
||||
the web root is **always a `www` folder next to the binary**. It is not
|
||||
configurable, it is not in `dns.config`, and nothing in the console can change
|
||||
it.
|
||||
|
||||
*2026-10-01:* that was upstream before v15.5. Since v15.5 the provider is built
|
||||
from the folder `DNS_SERVER_WEB_SERVICE_WWW_FOLDER_PATH` names when it is set
|
||||
and the folder exists, and from `www` next to the binary otherwise
|
||||
(`DnsWebService.cs:1805-1816` at the `v15.5.1` tag; §1.7). It is still not in
|
||||
`dns.config`, and nothing in the console can change it. What follows holds for
|
||||
whichever folder is served; at `v15.5.1` the `no-cache` line is `:1950`.
|
||||
|
||||
Two consequences that the installer lives with:
|
||||
|
||||
- The provider is created **once**, at start, from a path string, and resolves
|
||||
every request against it. Replacing the folder's contents (even deleting the
|
||||
folder and recreating it) is picked up **without restarting the service**
|
||||
(probe C11). Changing *which* folder is served is not: that needs a restart.
|
||||
- Every static file is answered with `Cache-Control: no-cache`
|
||||
(`DnsWebService.cs:1977`), and this console's assets are content-hashed on top
|
||||
of that. There is no browser-cache problem to warn anybody about.
|
||||
|
||||
Dotfiles in the web root are **not served**: `/.technitium-console` answers 404,
|
||||
a sibling without the dot answers 200. That is ASP.NET's default exclusion of
|
||||
hidden files, and it matters twice: an in-root marker cannot be verified over
|
||||
HTTP, and a non-hidden one would be readable by anyone who can reach the login
|
||||
page.
|
||||
|
||||
### 1.2 Installing on bare metal
|
||||
|
||||
`DnsServerApp/install.sh`: installs the ASP.NET runtime under `/opt/dotnet`,
|
||||
downloads `DnsServerPortable.tar.gz`, extracts it into `$dnsDir`, installs ICU,
|
||||
creates the `dns-server` system user, writes `dns.service` (systemd) or
|
||||
`/etc/init.d/dns` (OpenRC), and rewrites `/etc/resolv.conf` to `127.0.0.1`.
|
||||
|
||||
`$dnsDir` is `/etc/dns` when `/etc/dns/config` exists and `/opt/technitium/dns`
|
||||
otherwise. That condition is about an **older layout that kept the application
|
||||
under `/etc/dns`**, not about Alpine: the current image has no `/etc/dns/config`
|
||||
and Alpine follows the same rule as Debian. The comment in our `install.sh`
|
||||
saying otherwise is wrong; the folder list it produces is still right.
|
||||
|
||||
### 1.3 Updating on bare metal
|
||||
|
||||
The same script. An update is `tar -zxf` **over** the installation directory:
|
||||
an extraction, not a replacement:
|
||||
|
||||
> files present in the archive are overwritten; files that are not in it stay
|
||||
> where they are.
|
||||
|
||||
So after a server update a web root carrying this console holds **both**
|
||||
consoles: upstream's `index.html`, `js/`, `css/` restored on top of our
|
||||
`assets/`, `admin/`, `zones/`… and the server serves upstream's `index.html`.
|
||||
The administrator gets the stock console back and a directory of orphans
|
||||
underneath (measured; the probe rebuilds this state in C3).
|
||||
|
||||
### 1.4 Docker
|
||||
|
||||
The web root lives inside the image at `/opt/technitium/dns/www`
|
||||
(`Dockerfile`). An update is a new image, so anything written into the
|
||||
container's own files is gone with the next recreate: which is what
|
||||
`docker exec <c> sh -c "curl … | sh"` installs into, and the installer now says
|
||||
so when it does (C11).
|
||||
|
||||
*2026-10-01:* two layouts survive. Since v15.5 the variable can point at a
|
||||
volume or a bind mount, and that is the one the README gives Docker users: an
|
||||
init image, `ghcr.io/bygarcia/technitium-console`, copies the console into a
|
||||
volume that the server mounts read-only (D1–D4). Before 15.5 the only one is a
|
||||
bind mount from the host over `www`, which is what `dev/compose.yaml` does for
|
||||
development. The previous wording ("the only layout that survives is a bind
|
||||
mount over that path") stopped being true with 15.5.
|
||||
|
||||
### 1.5 Windows
|
||||
|
||||
`DnsServerWindowsSetup/appinstall.iss`, a different installer with a different
|
||||
layout. **Out of scope.** The README should say so instead of leaving a POSIX
|
||||
script to fail on its own.
|
||||
|
||||
### 1.6 The custom lists
|
||||
|
||||
`www/json/readme.txt` is upstream's own contract with the administrator: any
|
||||
`<name>-builtin.json` can be overridden by a `<name>-custom.json` written by
|
||||
hand, the custom file always wins, and the built-in ones are **expected to be
|
||||
overwritten on update**. Three pairs exist today (`quick-block-lists`,
|
||||
`quick-forwarders-list`, `dnsclient-server-list`), read at
|
||||
`www/js/main.js:816`, `:855` and `www/js/dnsclient.js:53`, and by this console
|
||||
at `src/lib/quick-lists.ts:38`.
|
||||
|
||||
It is an **open set by design**: the rule is the naming pattern, not the three
|
||||
names. The installer must treat it as a glob, and does (probe C4).
|
||||
|
||||
These files live in the web root and in no release. Replacing the web root
|
||||
deletes them, which is the whole reason this clause exists.
|
||||
|
||||
### 1.7 The environment variable
|
||||
|
||||
*2026-10-01:* it is upstream's since **v15.5 (2026-09-19)**: `CHANGELOG.md:10`
|
||||
at the `v15.5.0` tag, "Added new `DNS_SERVER_WEB_SERVICE_WWW_FOLDER_PATH`
|
||||
environment variable … PR #2138", and v15.5.1 carries it.
|
||||
`DnsWebService.cs:1805-1814` at `v15.5.1` reads it, uses `www` when it is unset,
|
||||
and when the folder does not exist writes the log line W3 quotes and falls back
|
||||
to `www`. Upstream documents it in `EnvironmentVariables.md:11`, a file of its
|
||||
own for variables read on every start, whose note says that changing one needs a
|
||||
restart: not in `DockerEnvironmentVariables.md`, as the PR had it. **The
|
||||
official image honours it since v15.5**: the probe measured it against
|
||||
`technitium/dns-server:latest` (v15.5.1) on 2026-09-30, and C12 and C13 ran and
|
||||
were met against it. What follows is this section as written on 2026-09-07, when
|
||||
the variable did not exist in any release; its first bullet is true only of
|
||||
servers before 15.5, and the installer still does not trust a version number for
|
||||
it (W3).
|
||||
|
||||
`DNS_SERVER_WEB_SERVICE_WWW_FOLDER_PATH` is twelve lines on the fork's
|
||||
`feat/configurable-www-folder` branch (`1f097ea1`): it overrides the web root and
|
||||
falls back to the default when unset or when the folder does not exist, so the
|
||||
server always starts. The maintainer has said in writing that he will merge it;
|
||||
the PR is pushed but not opened.
|
||||
|
||||
Two facts the installer has to respect:
|
||||
|
||||
- **No released server honours it.** Setting it on the official image changes
|
||||
nothing: the stock console keeps being served (measured). An installer that
|
||||
trusted the variable would install into a folder nobody reads and report
|
||||
success.
|
||||
- It **is** discoverable without credentials: it shows up in the environment of
|
||||
the running process (`/proc/1/environ` in the container, the `DnsServerApp`
|
||||
process on bare metal), which is where an installer should read it from rather
|
||||
than guessing at unit files or compose files.
|
||||
|
||||
The PR also had a hole: the variable was **not** in `DockerEnvironmentVariables.md`,
|
||||
where upstream documents every other one: and that file opens with a NOTE
|
||||
saying its variables are read only on first start, which is exactly what this
|
||||
one does not do. Both are fixed before opening: the row is there and the
|
||||
exception is named.
|
||||
|
||||
And the branch has now been **built and run**, which it never had been: a server
|
||||
from it serves the folder the variable names, falls back with its log line when
|
||||
the folder is missing, and a console installed into that folder is what answers
|
||||
(C12, C13). Twelve lines that nobody had executed are not twelve lines that
|
||||
work; now they are.
|
||||
|
||||
### 1.8 Version, without a token
|
||||
|
||||
`api/user/checkForUpdate` needs a session. The server prints its version at
|
||||
startup into `/var/log/technitium/dns/<date>.log`:
|
||||
|
||||
```
|
||||
[2026-09-07 06:03:29 UTC] DNS Server (v15.4.0.0) was started successfully.
|
||||
```
|
||||
|
||||
That is the credential-free source for the version, and it is what the backup
|
||||
needs in order to know whether it has gone stale.
|
||||
|
||||
---
|
||||
|
||||
## 2 · Two installation modes
|
||||
|
||||
The contract below is written for both, because which one applies is not our
|
||||
choice: it depends on the server in front of us.
|
||||
|
||||
**Mode A: replacement.** The only mode possible today: install over
|
||||
`<appFolder>/www`, keep a copy of the stock console, restore it on uninstall.
|
||||
It cannot survive a server update (§1.3), so under this mode an update means the
|
||||
administrator re-runs the installer. *2026-10-01:* no longer the only mode since
|
||||
v15.5 (§1.7). It is what the installer does when the variable is not set, and
|
||||
the only mode on a server before 15.5.
|
||||
|
||||
**Mode B: side by side.** Once the variable lands: install into a folder of our
|
||||
own, never touch `www`, and let the variable point the server at it. Server
|
||||
updates stop mattering. This is the mode the project wants, and the reason the
|
||||
PR exists. *2026-10-01:* the variable landed in v15.5 (§1.7). Mode B is what any
|
||||
server from 15.5 on gets once the variable is set (W2), and it is the layout of
|
||||
the Docker image (D1–D4).
|
||||
|
||||
Mode B costs one restart the first time the served path changes, and one when
|
||||
that change is undone, because the variable is read once at start (S2). Every
|
||||
reinstall after that costs none, and mode A never costs one at all.
|
||||
|
||||
Under systemd the folder must not be under `/home` or `/root`
|
||||
(`ProtectHome=true` in upstream's unit), and the service reads it fine
|
||||
everywhere else despite `ProtectSystem=strict`, which is read-only and not
|
||||
invisible. `/opt/technitium-console` satisfies both.
|
||||
|
||||
---
|
||||
|
||||
## 3 · The contract
|
||||
|
||||
`✓` met and measured. Every clause carries the probe case that decides it; none
|
||||
is left to a reading of the script. The three mode B clauses carry a `†`: they
|
||||
are measured against a build of the fork's `feat/configurable-www-folder`
|
||||
branch, not against a released server, and they stay that way until upstream
|
||||
merges it. *2026-10-01:* upstream released it in v15.5 (§1.7), and C12 and C13
|
||||
were met against the official v15.5.1 on 2026-09-30, so the `†` has come off
|
||||
the three.
|
||||
|
||||
### Filesystem security amendment, 2026-10-04
|
||||
|
||||
Root pathname operations require an immutable namespace against lower-privilege
|
||||
accounts. Console, backup, state and custom-list trees must be root-owned with
|
||||
no group or other write bits on files, directories or ancestors. Links, special
|
||||
files and newline-containing names are rejected before copies and cleanup.
|
||||
Archive inspection rejects links and device members before extraction; the
|
||||
extracted tree is checked before publication, and published files use mode 0644.
|
||||
The installer refuses unsafe trees instead of changing ownership while another
|
||||
account may retain open writable files. State and staging are protected under
|
||||
the state directory; state updates and managed drop-ins use unique temporary
|
||||
files. Uninstall first selects the trusted recorded console folder, even when
|
||||
the server already returned to a service-owned stock console.
|
||||
|
||||
This tightens earlier acceptance of service-owned web roots. Use a dedicated
|
||||
root-owned console folder or a volume mounted read-only in the DNS server.
|
||||
Custom lists in an unsafe stock tree need an administrator's inspected manual
|
||||
copy; the installer does not import them through a privileged copy. This
|
||||
amendment has offline GNU and BusyBox regression coverage, rather than a repeat
|
||||
of the historical live-server measurements above. Remote filesystem servers
|
||||
that can change root-owned objects remain outside this local permission model.
|
||||
|
||||
*2026-10-15:* a group write bit is accepted when the group is root (gid 0), the
|
||||
rule sudo applies to sudoers. Technitium's install script extracts
|
||||
`DnsServerPortable.tar.gz` as root, the archive stores every file as 0664, and
|
||||
tar run as root keeps that mode: a stock web root, and a replacement-mode
|
||||
console after any server update, is root:root 0664. The amendment above
|
||||
refused it, so upgrade and uninstall stopped on a stock file. Write by a
|
||||
non-root group or by others is still refused.
|
||||
|
||||
*2026-10-18, after a security audit:* that group bit is trusted only when no
|
||||
object in the tree carries a POSIX access control list, because on one that
|
||||
does the group bits are the list's mask and can stand for another account. Lists
|
||||
are read from the `+` of GNU or uutils `ls -l`, or from `getfacl` where ls shows
|
||||
none (BusyBox); with neither, group write is refused, with the remedy in the
|
||||
message. And W3 narrows: a folder named by the running server's environment that
|
||||
does not exist yet is created only when it is `/opt/technitium-console`, the
|
||||
folder the README documents. The service's own account can set that variable,
|
||||
so any other new folder must be created by the administrator first. The
|
||||
offline regressions are in `dev/installer-security.test.py`.
|
||||
|
||||
### Where it installs
|
||||
|
||||
- **W1 ✓** (C14) The web root is resolved, in order: `--dir`; the value of the
|
||||
variable read from the running server's environment; `www` next to the running
|
||||
server's binary; the two known folders as a last resort. It is never assumed
|
||||
when the running server can be asked: the process is found by what it runs, in
|
||||
`/proc`, and its web root is derived from the path of the `DnsServerApp.dll` it
|
||||
was started with. The two known folders are still there, for the case of a
|
||||
host preparing a folder for a container, where there is no process to ask.
|
||||
- **W2 ✓** (C12) When the running server honours the variable, the console is
|
||||
installed where it points and `www` is not touched. Measured: the console
|
||||
answers from the folder the variable names, and the stock `www` still holds
|
||||
the console the server shipped.
|
||||
- **W3 ✓** (C15) When the variable is set but the running server ignores it, the
|
||||
installer does not install into it. Support is established by **measurement,
|
||||
before anything is installed**: write a probe file with an unguessable name
|
||||
into the folder the variable names, ask the web service for it, delete it
|
||||
whatever the answer, and only then decide. Not by a version number, and not by
|
||||
installing first and looking afterwards: an installer that has to undo an
|
||||
install to find out where it belongs has already got it wrong. The probe file
|
||||
is not hidden, or the answer is a 404 for the wrong reason (§1.1).
|
||||
|
||||
A 404 has two possible causes and they are told apart in the startup log,
|
||||
which the fork writes on purpose: *"Web Service is falling back to the default
|
||||
web root folder since the folder configured by the
|
||||
DNS_SERVER_WEB_SERVICE_WWW_FOLDER_PATH environment variable does not exist"*.
|
||||
With that line, the server honours the variable and fell back because the
|
||||
folder was missing when it started: create it, install, restart (S2).
|
||||
Without it, the server does not know the variable at all, and the console goes
|
||||
where it is actually read. The half of this clause that a released server can
|
||||
exercise is the refusal, and that is what C15 measures; the other half rides on
|
||||
C12. *2026-10-01:* since v15.5 it is the other way round. A released server
|
||||
honours the variable and rides on C12, and the refusal needs a server before
|
||||
15.5: C15 is not applicable against `technitium/dns-server:latest`, and runs
|
||||
against an older image given with `IMAGE=` (§5).
|
||||
- **W4 ✓** (C10) A target folder that does not exist is created. Nothing is
|
||||
backed up that was never there, which is what used to make the documented
|
||||
Docker flow fail before it had installed anything.
|
||||
- **W5 ✓** (C9) A web root that is a mount point is installed into, in place.
|
||||
Nothing ever removes or renames the web root itself (see A1) so the layout
|
||||
the README asks Docker users to create is no longer a special case.
|
||||
- **W6 ✓** (C22) *2026-09-30.* The installer writes only into a folder it can
|
||||
show is a console, because publishing sweeps, as root, everything the release
|
||||
does not ship. The folder has to be missing, empty, the one its own state says
|
||||
it installed into, the stock console (an `index.html` titled *Technitium DNS
|
||||
Server* next to `js/main.js` and `json/readme.txt`, true of every release since
|
||||
v11, and of the hybrid of §1.3), or this console (an `index.html` that mounts
|
||||
`#root` and names an `assets/*.js` that is there). Anything else stops the run
|
||||
before anything is written, with or without `--yes` and with or without a
|
||||
TTY. Before this, `--dir /opt/technitium/dns` by mistake removed the server's
|
||||
binaries and configuration, with no backup: there was no `index.html` to back
|
||||
up, and without asking, because `curl … | sudo sh` has no TTY to ask on.
|
||||
*2026-10-01* (C28): a folder holding nothing but `json/` and
|
||||
`json/*-custom.json` counts as empty. Publishing neither overwrites nor sweeps
|
||||
those files, so there is nothing in it to lose, and it is how a host folder
|
||||
for the Docker image is prepared with lists written by hand before the first
|
||||
run. One file of any other kind beside them and the folder is refused as
|
||||
before (C22; C29, lists and `notes.txt`). Measured with the amendment on
|
||||
2026-10-01, by C22, C28 and C29.
|
||||
*2026-10-01* (C22, `--dir /`): `--dir /`, or any path that resolves to the
|
||||
root folder, is refused. It used to resolve to no folder at all, and no
|
||||
folder means "ask the server", so the console went into the server's web
|
||||
root, which nobody had asked for.
|
||||
*2026-10-01* (C22, a refused run): a run W6 stops leaves no trace outside
|
||||
the folder either. What the sweep may remove is listed in a temporary folder
|
||||
of the run's own, and the staging folder, with the state folder around it,
|
||||
is made only once W6 has let the run through; a run that made the state
|
||||
folder and stops before writing any state removes it on the way out.
|
||||
- **W7 ✓** (C23) *2026-09-30.* The running server is **identified**, not
|
||||
matched. A command line ending in `DnsServerApp.dll` makes a process a
|
||||
candidate; it is believed only when every uid it runs as (read from
|
||||
`/proc/<pid>/status`, not from the owner of `/proc/<pid>`) and the owners of
|
||||
its `DnsServerApp.dll`, of the folder holding it and of that folder's `www` are
|
||||
root or the service's account: `dns-server`, which upstream's installer has
|
||||
used since v15.0, or the `User=` of the unit that runs the server. Candidates
|
||||
that fail are named and ignored; a process in another root filesystem (a
|
||||
container seen from its host) is not a candidate. Two believable servers with
|
||||
different web roots stop the run, listing both and asking for `--dir`. A web
|
||||
root the installer found by itself is refused if any folder on its path, its
|
||||
backup, or any folder inside it is a symbolic link; a path given with `--dir`
|
||||
is resolved once and every later step, the state included, uses the resolved
|
||||
one.
|
||||
|
||||
### The administrator's files
|
||||
|
||||
- **F1 ✓** (C1, C4) Every `json/*-custom.json` in the web root survives an
|
||||
install. The glob is the rule, not the three names that exist today.
|
||||
- **F2 ✓** (C5) …and survives an **uninstall**, including a list written long
|
||||
after the install, which no backup can contain. It is the one command an
|
||||
administrator runs expecting to lose nothing. It is kept by never being
|
||||
touched: a `*-custom.json` already in place is neither overwritten by a
|
||||
restore nor swept away as a leftover.
|
||||
- **F3 ✓** (C13) In mode B the lists travel **both ways**, because the web app
|
||||
fetches them relative to whatever folder is being served. On install they are
|
||||
copied from `www/json` into the console's own folder, and the administrator is
|
||||
told that the one to edit from now on is the new one. On uninstall everything
|
||||
matching the pattern in the console's folder (including lists written after
|
||||
the install, which is where F2 bites again) is copied back to `www/json`
|
||||
before the folder goes.
|
||||
- **F4 ✓** (C16) The installer never modifies the server's data. It writes to exactly
|
||||
three places: the web root it is installing into, the backup it keeps, and its
|
||||
own state (A5). Everything else (`dns.config`, the zones, the users, the
|
||||
logs, `/etc/resolv.conf`, the service unit) is out of bounds, with the single
|
||||
exception of the service change in §4, which is offered and confirmed before
|
||||
it happens.
|
||||
|
||||
**Reading is not writing.** The environment of the running process, the
|
||||
startup log and the unit files are read freely, and W1, W3 and A7 are built on
|
||||
exactly that: the way not to guess is to go and look. C16 measures the
|
||||
writing half the only way worth measuring it: a checksum of `/etc/dns`, the
|
||||
logs, the units and `/etc/resolv.conf` before and after an install and an
|
||||
uninstall, which has to come out identical.
|
||||
|
||||
### Replacing atomically
|
||||
|
||||
- **A1 ✓** (C17) The web root is never observably broken, mount point or not.
|
||||
It is also never renamed, moved or removed, and that is a change from how this
|
||||
clause was first written: **swapping two directories takes two renames, and
|
||||
between them there is no web root at all**, which is precisely what A2 forbids.
|
||||
A rename is not an option, it is a shortcut that loses the guarantee. So there
|
||||
is one path, in place, and the **order** is what makes it safe:
|
||||
|
||||
1. hashed assets, fonts, images and `json/` first, under their own names;
|
||||
2. every `index.html` last, and the entry one last of all;
|
||||
3. leftovers of the previous console removed only after that.
|
||||
|
||||
Step 2 is the **publication point**, and each file within it is replaced by a
|
||||
write-then-rename so no page is ever half written. What makes the step safe is
|
||||
not that it is a single instant (it is not, there is one `index.html` per
|
||||
route) but that during it *every* page a request can be handed finds the files
|
||||
it names: the new assets are already there and the old ones are not gone yet.
|
||||
Whichever version of a page a browser gets in that window, it works.
|
||||
|
||||
C17 measures it at the worst possible moment: a `cp` that fails on every page,
|
||||
which stops the run exactly at the publication point. The assets of the new
|
||||
console are in, no page has been swapped, and what is being served is the
|
||||
previous console, whole.
|
||||
- **A2 ✓** (C17, C7) **At every moment there is a complete console being served**:
|
||||
the previous one before the publication point, the new one after it. That is
|
||||
the guarantee, and it is the only one an installer can actually keep: a
|
||||
`SIGKILL` or a power cut mid-copy is not a failure it gets to handle, and on a
|
||||
mount point it cannot fall back on a rename either.
|
||||
|
||||
So the promise splits in two. A failure the installer **sees** it undoes
|
||||
itself, and the previous console is back when it exits. A stop it never sees
|
||||
leaves the invariant standing anyway: because of A1's order, plus possible
|
||||
leftovers: a staging folder, an orphan of the console being replaced, a state
|
||||
file that says a run started. **Those are repaired by the next run**, which
|
||||
therefore has to recognise them instead of tripping over them.
|
||||
|
||||
Both halves are measured: C17 stops a run at the publication point and finds a
|
||||
whole console still being served, and C7 hands the next run the wreckage of an
|
||||
interrupted one and expects it repaired.
|
||||
- **A3 ✓** (C7) The uninstall keys off the **backup** and the state, never off a
|
||||
marker: there is no marker to key off any more (A6).
|
||||
- **A4 ✓** (C2) Re-running is idempotent, and the backup is taken exactly once:
|
||||
a second run must never save this console as "the original".
|
||||
- **A5 ✓** (C18) The install state (mode, version, web root, backup path, and the
|
||||
server version the backup was taken from) lives **outside** the served
|
||||
folder, in `/var/lib/technitium-console/`. Bookkeeping in the web root is
|
||||
either invisible to the installer (dotfiles 404, §1.1) or visible to the
|
||||
internet (non-dotfiles 200). Neither is a reason to put it there.
|
||||
- **A6 ✓** (C8) The web root holds the console and nothing else: no marker, no
|
||||
staging folder, nothing hidden. The staging area lives with the state, so a
|
||||
half-finished download is never inside the folder being served.
|
||||
*2026-10-01:* one exception, in the Docker image's volume only: the marker
|
||||
`.technitium-console` at its root (Docker, D1). It is a dotfile, so it is
|
||||
never served (§1.1), and the init reads it from the filesystem, not over
|
||||
HTTP. No uninstall keys off it (A3): `--into-volume` has no uninstall. The
|
||||
`✓` is not C8's alone for this half: C26 finds nothing in the volume but the
|
||||
console and the marker, and the marker not served, and C29 finds not even
|
||||
the marker after an init that was refused; measured on 2026-10-01. Why the
|
||||
marker is there is in "Docker", below.
|
||||
- **A7 ✓** (C19) When the version recorded with the backup and the running one
|
||||
differ, the backup is the console of another server: `--uninstall` **stops**,
|
||||
names both versions, and does nothing. Restoring it anyway takes a flag of its
|
||||
own (`--restore-mismatched-backup`) and **`--yes` does not grant it**: `--yes`
|
||||
means "do not ask me the ordinary question", not "accept an incompatible
|
||||
restore". The message names the repair that does not need us at all: re-run
|
||||
upstream's own installer, which puts back the console the running version
|
||||
ships.
|
||||
- **A8 ✓** (C24) *2026-09-30.* `--uninstall` acts on the folder **on record**
|
||||
(`webroot` in the state), not on the one the server points at now. They differ
|
||||
when the variable was removed and the server restarted before uninstalling,
|
||||
and then the installer says so and leaves the current one alone. In mode B the
|
||||
recorded folder is removed only if it still holds this console, and never when
|
||||
it is the server's own `www`; before this, that `rm -rf` went to whatever
|
||||
folder was resolved at the time: the stock console, in the case above. In
|
||||
mode A the restore is held to W6.
|
||||
*Same night (1.1.1):* removing it has to finish when the folder is a mount
|
||||
point, as it is in the Docker layout. It is emptied and then removed if it can
|
||||
be; if it cannot, it stays empty and no restart is offered, because with the
|
||||
variable still set the server would serve an empty folder. Found installing
|
||||
1.1.0 from the published release; C13 now checks the uninstall's exit code on
|
||||
its bind-mounted folder, which it had not.
|
||||
|
||||
### Fresh install and update behave the same
|
||||
|
||||
- **U1 ✓** (C1, C2, C3) The end state is the same whether the target is the
|
||||
stock console, an older build of this one, or the hybrid a server update
|
||||
leaves behind (§1.3). No leftovers of the console being replaced.
|
||||
- **U2 ✓** (C20) The hybrid is detected and named. An administrator whose
|
||||
console reverted after a server update is told what happened, and told in the
|
||||
same breath that the backup is older than the server now running, which is the
|
||||
fact A7 will stop them on later.
|
||||
|
||||
### What it installs
|
||||
|
||||
- **I1 ✓** (C25) *2026-09-30.* A release download is checked against the
|
||||
`technitium-console.tar.gz.sha256` the release publishes next to it
|
||||
(`.github/workflows/release.yml`). A mismatch, or no checksum at all, stops the
|
||||
run with nothing changed. `--from` is not checked, because there is nothing to
|
||||
check it against, and the output says so. It is an integrity check against a
|
||||
broken or altered download, not a signature: whoever can publish a release can
|
||||
publish its checksum. The archive is unpacked with `--no-same-owner`, so the
|
||||
staging copy belongs to root whatever the tarball says; the probe does not
|
||||
measure that half, because what reaches the web root is written by `cp` as
|
||||
root either way.
|
||||
*2026-10-01* (C26; C29, bad or missing checksum): with `--into-volume`, the
|
||||
Docker image's mode, `--from` is checked after all: the `.sha256` next to the
|
||||
tarball is required, and a mismatch or a missing one stops the run with
|
||||
nothing written, as above. The image carries the release's checksum, so D4
|
||||
holds when the image runs as well as in CI. Measured with the amendment on
|
||||
2026-10-01, by C25, C26 and C29.
|
||||
|
||||
### Docker
|
||||
|
||||
The init image runs `install.sh --into-volume /target` on the tarball it
|
||||
carries, into a volume the official server mounts read-only at the folder its
|
||||
`DNS_SERVER_WEB_SERVICE_WWW_FOLDER_PATH` names. Inside that run the folder is
|
||||
given, not looked for: there is no server to find, so W1, W3 and W7's
|
||||
identification have nothing to do, and the folder is installed into side by
|
||||
side, with nothing backed up. W4 gives way: `/target` has to be a mount point,
|
||||
not a folder to create (C29). The rest of the clauses above still apply inside
|
||||
that run, notably W5, W6, W7's links, F1, F4, A1, A2, A4, U1, S1 and I1 as
|
||||
amended.
|
||||
|
||||
One thing is the init's own. Its state (A5) lives in its container's layer and
|
||||
goes with it, so the record that the volume is the console's lives in the
|
||||
volume: after every check has passed and before the first file is copied, the
|
||||
init writes `.technitium-console` at the volume's root, and `--into-volume`
|
||||
accepts a volume that holds it as its own (W6). That is what lets A2's repair
|
||||
half hold in the init: a first copy interrupted halfway leaves assets and no
|
||||
`index.html`, which W6 alone would refuse forever, and the next run of the init (the next `up`, or the next update) finds the marker and finishes it (C29,
|
||||
interrupted first copy, then re-run). The marker stays; it is the one exception
|
||||
to A6.
|
||||
|
||||
These four are what the layout adds.
|
||||
|
||||
- **D1 ✓** (C29) **The init never keeps the DNS server from starting.** The
|
||||
compose block the README gives has no `depends_on` from the server on the
|
||||
init, and the init neither looks for the server nor waits for it. When the
|
||||
init fails a check (a folder W6 refuses, no volume mounted at `/target`, a
|
||||
bad or missing checksum; C29, bad or missing checksum), it stops before
|
||||
publishing, exits non-zero having written nothing into the volume, not even
|
||||
the marker, and the server starts all the same and serves whatever the volume
|
||||
holds: a whole console on an update, nothing on a first install. A failure
|
||||
during the copy leaves A1's order in force, as D2 says: on an update, a whole
|
||||
console is served throughout; on a first install, nothing yet, and the next
|
||||
run finishes it (the marker, above). Nothing on a first install is also what
|
||||
is served for the second or two before the init's first publication point:
|
||||
A2 is about replacing a console, and on a first install there is none yet to
|
||||
keep serving. Not covered: a first `docker compose up` has to be able to pull
|
||||
both images, which is the registry's business, not the installer's.
|
||||
- **D2 ✓** (C27) **Updating the console does not restart the DNS server.** The
|
||||
documented update, `docker compose pull technitium-console && docker compose
|
||||
up -d technitium-console`, recreates the init only: the server's `StartedAt`
|
||||
does not change and the new console is served at once (§1.1). Bringing the
|
||||
whole file up again, with nothing about the server changed, does not touch it
|
||||
either. An update that fails leaves the server alone too, and a whole console
|
||||
served: the init stops before writing (I1, W6), or mid-copy with A1's order
|
||||
in force. A plain `docker compose pull && docker compose up -d` also updates
|
||||
the server when upstream has a new image, and that restart is the server's,
|
||||
not the console's; the README says which command does which.
|
||||
- **D3 ✓** (C28) **Custom lists in the volume survive.** Every
|
||||
`json/*-custom.json` in the volume survives an update of the console and a
|
||||
`--force-recreate` of the server, and so does one in a host folder bound in
|
||||
its place (C28, bind folder, update and `--force-recreate`). A host folder
|
||||
holding only such lists before the first run is accepted (W6); one holding
|
||||
them and any other file is refused (C29, lists and `notes.txt`), and D1 says
|
||||
what the server does then.
|
||||
- **D4 ✓** (C26, and CI) **The image carries the release's tarball, byte for
|
||||
byte.** CI downloads it from the release it has just published, checks it
|
||||
against the release's `.sha256` before building, and after pushing checks it
|
||||
again inside each of the three platforms (`.github/workflows/release.yml`, job
|
||||
`image`). The init checks it once more when it runs (I1). C26 measures the
|
||||
local half: the image built from `dist/` carries the tarball it was built
|
||||
with. A check that fails before building stops the job with nothing pushed.
|
||||
One that fails after pushing turns the job red with the tags already out, and
|
||||
the init's own check is what stands between those bytes and a volume.
|
||||
Re-running the `image` job on its own downloads the same bytes again, so it
|
||||
only helps a failure that was transient: the network, the registry. A release
|
||||
asset that does not match its own `.sha256` fails the same way every time, and
|
||||
needs a new release. Never the whole workflow on a published tag: it would
|
||||
rebuild the tarball under it with other bytes and another checksum.
|
||||
|
||||
### The service and the browser
|
||||
|
||||
- **S1 ✓** (C11, C21, C31) Publishing into the folder already being served does
|
||||
**not** restart the DNS service. C11
|
||||
measures the premise: with the service running and never restarted, a full
|
||||
replacement of the web root is served immediately, and C21 measures the
|
||||
clause, with a `systemctl` of its own that records being called and never is.
|
||||
C31 proves that first managed systemd activation restarts once and the update
|
||||
after it does not. A restart is a resolution outage for everything behind
|
||||
this server; it is not spent on copying files.
|
||||
- **S2 ✓** (C31) A restart happens **only when the path being served or the
|
||||
environment changes**, because the file provider is built once at start
|
||||
(§1.1). That is the first install into the variable's folder and the uninstall
|
||||
out of it, not a property of mode B: a later reinstall into the same folder,
|
||||
with the environment untouched, restarts nothing, which is what C12 and C13
|
||||
show. On systemd, the installer publishes first, writes its own drop-in,
|
||||
restarts once and verifies both process environment and served asset. A
|
||||
failed activation removes the drop-in and restarts back onto stock. Uninstall
|
||||
reverses that order: stock is verified before the dedicated folder is removed.
|
||||
- **S3 ✓** (C21) No "press Ctrl+F5" advice: every static file is `no-cache` and the
|
||||
assets are hashed (§1.1). Advice that is not true trains people to ignore the
|
||||
rest.
|
||||
|
||||
---
|
||||
|
||||
## 4 · Service boundaries
|
||||
|
||||
- **Windows.** Say so; do not fail at it (§1.5).
|
||||
- **Installing or updating the DNS server itself.** If it is not there, stop.
|
||||
- **The managed systemd drop-in.** On a running 15.5+ server with exactly one
|
||||
unit that names that process's `DnsServerApp.dll`, the default installation
|
||||
publishes to `/opt/technitium-console` and owns
|
||||
`/etc/systemd/system/<unit>.d/technitium-console.conf`. It never edits the
|
||||
upstream unit. An existing unrecorded drop-in is left alone. Uninstall removes
|
||||
only the exact two-line file it wrote; a changed file is administrator-owned
|
||||
and stops the uninstall. `--no-configure-service` opts out. OpenRC and Docker
|
||||
remain outside this automation, where the equivalent belongs to
|
||||
`/etc/conf.d/dns` and to the compose file. *2026-10-01* (C30): on a Docker
|
||||
host it reads `docker inspect` and the
|
||||
startup log through `docker exec`, and prints the exact change for each
|
||||
container (the way in, and with `--uninstall` the way out) with the
|
||||
container, service, file and volume names it found, and exits 0. It never
|
||||
prints `$0`, which under `curl … | sudo sh` is `sh`.
|
||||
- **SELinux relabelling, distro packages, and updating the console by itself.**
|
||||
Not now, and each would need its own contract.
|
||||
|
||||
---
|
||||
|
||||
## 5 · How it is checked
|
||||
|
||||
```sh
|
||||
npm run build && sh dev/installer-probe.sh # exit code = cases that failed
|
||||
```
|
||||
|
||||
Twenty-one cases, one throwaway container each, off the official image, so every
|
||||
case starts from the same stock console and nothing on the machine running it is
|
||||
touched. Every clause that can be decided by a machine has one, which sometimes
|
||||
means building the situation rather than waiting for it: C14 starts a server from
|
||||
a folder neither known path points at, C16 lets one run once and stops it so
|
||||
`/etc/dns` can be compared byte for byte, C17 installs a `cp` that fails on every
|
||||
page so the run dies exactly at the publication point, and C21 installs a
|
||||
`systemctl` whose only job is to record having been called.
|
||||
|
||||
*2026-09-30:* twenty-five now. C22 aims `--dir` at the server's own folder, C23
|
||||
starts look-alike processes (one as `nobody`, one whose `www` is a link, two at
|
||||
once), C24 starts the server by hand so it can be restarted without the variable
|
||||
before uninstalling, and C25 installs a `curl` that plays GitHub with whatever
|
||||
checksum the case wants. `INSTALLER=<file>` measures another `install.sh`, which
|
||||
is how the four were seen failing on the old one.
|
||||
|
||||
*2026-10-01:* thirty. C26 to C29 build the init image from `dist/` with this
|
||||
checkout's `docker/Dockerfile` and `install.sh`, and bring up the README's
|
||||
compose block against the official server: a fresh install, an update, the
|
||||
custom lists, and an init that fails. C30 runs `install.sh` from stdin in
|
||||
`docker:cli` with the Docker socket, which is how `curl … | sudo sh` runs it on
|
||||
a Docker host. C11 now also expects the warning about installing into a
|
||||
container's own files, and runs on any server, as it always has. The five new
|
||||
ones need a server that honours the variable, and are not applicable otherwise.
|
||||
|
||||
*2026-10-02:* thirty-one. C31 starts the server behind a recording systemd
|
||||
stand-in. It proves that the first install writes the dedicated folder and
|
||||
drop-in before one restart, an update restarts nothing, uninstall restarts onto
|
||||
stock before deleting the folder, custom lists make both trips, and a locally
|
||||
changed drop-in is not removed.
|
||||
|
||||
C12, C13 and C15 depend on what the image can do, and the probe does not decide
|
||||
that by decree: it **detects the capability** the same way W3 says the installer
|
||||
must: it starts the image with the variable pointing at a folder holding a probe
|
||||
file, asks the web service for it, and believes the answer. Against
|
||||
`technitium/dns-server:latest` the answer is no, so C12 and C13 stand down and
|
||||
C15 runs, which is the refusal case. Against a build of the fork branch the
|
||||
answer is yes, and it is the other way round.
|
||||
|
||||
Both sides have been run. Nineteen cases pass against the official image, and
|
||||
twenty against `technitium-dns-server:wwwvar`, built from the branch in four
|
||||
commands (`dev/README.md`). Nothing here is a case that has never been seen
|
||||
passing.
|
||||
|
||||
The gate for the installer round is the same as every other round in this
|
||||
project: **the probe at zero on both images**: met on 2026-09-07, nineteen
|
||||
cases against `technitium/dns-server:latest` and twenty against a server that
|
||||
honours the variable. What is left is not a measurement: it is upstream merging
|
||||
the branch, at which point the `†` comes off W2, F3 and S2 and the image stops
|
||||
having to be built by hand.
|
||||
|
||||
*2026-10-01:* it happened. Upstream released the variable in v15.5 (§1.7), and
|
||||
the two sides have swapped. Against `technitium/dns-server:latest` (v15.5.1) the
|
||||
capability answer is yes, so C12 and C13 run and C15 stands down: 24 met and 1
|
||||
not applicable on 2026-09-30, 29 and 1 on 2026-10-01 with the Docker cases. C15,
|
||||
the refusal, now needs an image before 15.5, given with `IMAGE=`; it was
|
||||
measured against v15.4 on 2026-09-07, and again on 2026-10-01 against
|
||||
`technitium/dns-server:15.4.0`, after the Docker changes to `install.sh`: met.
|
||||
That run also failed C24, whose first two steps need a server that honours the
|
||||
variable and waited for it with a `curl` 15.4.0 does not have. The same day
|
||||
those two steps were gated by the capability, like C12 and C13; the third, a
|
||||
recorded folder that is no longer this console, runs on every image, because
|
||||
`--into-volume` can leave such a record on any server; and the case waits for
|
||||
the port in `/proc/net` instead. Against 15.4.0 then: 23 met, 0 not met, 7 not
|
||||
applicable, so on 2026-10-01 the probe is at zero on both images again. The `†`
|
||||
is off W2, F3 and S2, and no server has to be built by hand. C26 to C30 are
|
||||
gated by the same answer as C12 and C13.
|
||||
|
||||
---
|
||||
|
||||
## 6 · What contracting it found, and what the implementation did
|
||||
|
||||
Five of these were failures the probe caught the day the contract was written.
|
||||
All five are fixed; the last two are notes, not code.
|
||||
|
||||
- The README documented a Docker flow that did not work: `--dir` on a folder
|
||||
that did not exist installed nothing (W4).
|
||||
- `--uninstall` destroyed exactly the files the installer goes out of its way to
|
||||
preserve, when the administrator had written them after installing (F2).
|
||||
- The installer restarted the DNS server for no reason (S1) and told the
|
||||
administrator to bypass a browser cache that is not there (S3).
|
||||
- A run that died between the wipe and the copy left no console at all and
|
||||
locked the way back (A2).
|
||||
- Bookkeeping lived in the folder the server serves (A5, A6).
|
||||
|
||||
And one thing the implementation found that the contract had got wrong: **A1's
|
||||
rename shortcut is not available**. Swapping a staged tree for the old one takes
|
||||
two renames, and between them there is no web root: the exact state A2 says can
|
||||
never exist. So there is no rename path at all, on any filesystem: one order, in
|
||||
place, everywhere. The clause was rewritten rather than the guarantee weakened.
|
||||
|
||||
None of the five were visible from the code alone; all came out of running the
|
||||
installer against a real server.
|
||||
|
||||
**2026-09-30.** The audit of that day found three more, by reading this time,
|
||||
and each has a probe case that fails on the script it was found in: a sweep with
|
||||
no check on what it was sweeping (W6, C22), a server taken to be the first
|
||||
process whose command line ended the right way, with a `www` that could be a
|
||||
link (W7, C23), and an uninstall that removed the folder it resolved instead of
|
||||
the one it had installed into (A8, C24). Left as they are, on purpose:
|
||||
`confirm` still answers yes when there is no TTY, because the documented path is
|
||||
`curl … | sudo sh` and W6 is what makes that safe, not a question nobody can
|
||||
answer; and a web root owned by the service account can still race the
|
||||
installer between a check and a write, because closing that needs writes that
|
||||
do not follow links, which POSIX `sh`, `cp` and `mv` cannot promise. Which is the same lesson About left, in a
|
||||
different costume: **a script that has never been run against the thing it
|
||||
manages is a draft, not a tool.**
|
||||
@@ -0,0 +1,81 @@
|
||||
# План перевода консоли на ReUI PRO
|
||||
|
||||
## Рамки
|
||||
|
||||
- **Design only, zero functionality** (PRODUCT.md, CONVENTIONS.md): меняется только внешний вид. Те же элементы управления, шаги, порядок валидации и **те же английские тексты** (литералы Technitium остаются как есть, интерфейс не русифицируется).
|
||||
- Система оформления: «shnt Ops · ReUI PRO» (shadcn `base-nova`, блоки ReUI PRO, `Frame` вместо `Card`, токены `dark` / `light` / `contrast`). Референсы: общий холст Auth Portal + MikrotikManager v2 + EvoFirewall + EvoBGP.
|
||||
- 32 реальных URL, адаптив до 390 px без горизонтального переполнения и темы System / Light / Dark сохраняются. Amber по-прежнему не рисуется.
|
||||
|
||||
## Этап 1. Дизайн на холсте `/design`
|
||||
|
||||
Один холст «Technitium Console · ReUI PRO». Экраны рисуются по порядку, каждый на реальных данных из `src/screens`:
|
||||
|
||||
1. **Каркас**: AppShell (сайдбар 240/60 px, шапка 48 px, ⌘K, меню профиля с Change Theme / My Profile / Change Password / 2FA / API token), пункты из `SECTIONS`.
|
||||
2. **Login** (+ OTP, Forgot password, LoginCover).
|
||||
3. **Dashboard**: KpiStrip, график (Chart.js), heatmap, Top-таблицы, BlockingMenu, сравнение периодов, custom range.
|
||||
4. **Zones**: дерево/список зон, записи, панели Add/Edit Record, Add / Clone / Convert / Import / Sign / Unsign Zone, DNSSEC, Zone Options, Permissions, Edit Zone File.
|
||||
5. **Cache**: дерево кэша и записи.
|
||||
6. **Blocking**: Overview, Rules, Lists, Domain check, Recent blocked.
|
||||
7. **Apps**: каталог, установленные, Install / Update / Config.
|
||||
8. **DNS Client**.
|
||||
9. **Settings**: 9 вкладок (General, Web Service, Optional Protocols, TSIG, Recursion, Cache, Blocking, Proxy & Forwarders, Logging).
|
||||
10. **DHCP**: Leases, Scopes, ScopeForm.
|
||||
11. **Administration**: Sessions, Users, Groups, Permissions, SSO, LDAP, Cluster, UserDetails.
|
||||
12. **Logs**: View Logs, Query Logs.
|
||||
13. **About** и состояния: пусто, ошибка, нет доступа, загрузка.
|
||||
14. **Вариации**: светлая тема, высокий контраст, телефон 390 px, свёрнутое меню.
|
||||
|
||||
Результат этапа: холст, проверенный по чек-листу системы (Frame, одна primary-кнопка, DataGrid вместо ручных таблиц, статус = слово + глиф + токен).
|
||||
|
||||
## Этап 2. Основа во фронтенде
|
||||
|
||||
1. Подключить Tailwind v4 и shadcn (`base-nova`), создать `components.json` (уже есть), алиас `@/`, `src/theme/globals.css` на токенах системы.
|
||||
2. Мост токенов: сопоставить `src/theme/tokens.css` (893 строки) токенам ReUI, не ломая ThemeProvider (System / Light / Dark).
|
||||
3. Установить компоненты ReUI: `data-grid`, `filters`, `sheet`, `badge`, `sonner`, `tabs` и т. д.
|
||||
|
||||
## Этап 3. Миграция `src/ui`, затем экранов
|
||||
|
||||
Порядок: примитивы → общие части → экраны, по одному экрану на коммит, зелёные тесты на каждом шаге.
|
||||
|
||||
| Текущее (`src/ui`) | ReUI / shadcn |
|
||||
|---|---|
|
||||
| Button, Field, Form, Select, Check, Segmented | Button, Field, Select, Checkbox, Switch, ToggleGroup |
|
||||
| Table, EditableTable, Pagination | вид DataGrid поверх собственной таблицы (см. «Решения по плану») |
|
||||
| Dialog, Confirm, Details | Dialog / AlertDialog / Sheet |
|
||||
| Toast, Notifier | собственный Toast с темой ReUI (см. «Решения по плану») |
|
||||
| Menu, Tooltip, SubTabs | Popover, Tooltip, стиль Tabs (линия под активной) |
|
||||
| Panel, SectionHeader, Alert, Tag, Empty | Frame, Alert, Badge, Empty state |
|
||||
|
||||
## Этап 4. Проверка
|
||||
|
||||
Все существующие тесты (`npm test`), `lint`, `lint:language`, `lint:colours`, `typecheck`, сборка, ручной обход в `dev` (Docker, :5380) против `ref` (:5381); 390 px без горизонтального переполнения; три темы.
|
||||
|
||||
## Риски
|
||||
|
||||
- Chart.js: цвета серий из `chart-1…4`, поведение прежнее.
|
||||
- Скрипты `lint:colours`, `lint:dashes` потребуют обновления под новые токены.
|
||||
- Размер бандла: Tailwind и Radix/Base UI добавляются к статической сборке, нужно следить за размером.
|
||||
|
||||
## Состояние на 2026-10-26
|
||||
|
||||
| Компонент | Состояние |
|
||||
|---|---|
|
||||
| Button, Tag, Check, Input/Textarea | ReUI Button, Badge, Checkbox/Switch, Input/Textarea |
|
||||
| Segmented, SubTabs | стиль ReUI Tabs, нативные кнопки |
|
||||
| Alert, Panel, Dialog, Confirm | ReUI Alert, Frame, Dialog |
|
||||
| Tooltip | ReUI Tooltip (роль `tooltip` и `aria-describedby` сохранены) |
|
||||
| Menu | ReUI Popover (роли и клавиатура меню остались в `Menu.tsx`) |
|
||||
| Select | ReUI Popover (поиск, группы и набор по буквам сохранены) |
|
||||
| Table | Рамка таблицы теперь ReUI `DataGrid` (`DataGrid`, `DataGridTableViewport`, `DataGridTableBase`, `DataGridTableHead`, `DataGridTableBody`, `DataGridTableFoot`) с экземпляром TanStack `useTable`. Строки остаются своими `<tr>`/`<td>` внутри тела: колонки как данные не описываются, потому что ячейки несут меню, ссылки, `colSpan` и классы строк. Режим карточек на телефоне, подписи колонок и сортировка «как в оригинале» сохранены (проверено в браузере на моке: на 1440 px таблица, ниже 560 px карточки, без горизонтальной прокрутки). |
|
||||
| Toast | свой (фокус возвращается, пауза под курсором, действие с правом); цвета и шрифты из темы ReUI |
|
||||
| Тема | палитра shnt Ops в `tokens.css`, Geist, шкала 12/13/14 px, радиусы 8/10/14 px |
|
||||
|
||||
## Решения по плану
|
||||
|
||||
| Пункт | Что сделано |
|
||||
|---|---|
|
||||
| Toast | Уведомление рисует ReUI `sonner`. Часы, пауза под курсором и фокусом, Escape и возврат фокуса остались в `ui/Toast.tsx`. Живая область `role="status"` по-прежнему всегда в странице. Цена замены: перед кнопкой Undo появился один лишний Tab-стоп (обёртка sonner), закрывающееся уведомление остаётся в DOM около 200 мс, а тесты Rules ждут его асинхронно (`findBy`, `waitFor`). |
|
||||
| Карточки Dashboard | Одиннадцать карточек собраны в одну панель KPI как на холсте: границы-волоски вместо рамок, плитка с иконкой, подпись без капса. Спарклайны, доля, изменение и переход в Query Logs сохранены. |
|
||||
| Table | Таблица строится на примитивах ReUI (`TableHeader`, `TableBody`, `TableFooter` и вид DataGrid). Перевод на TanStack `DataGrid` не сделан: строки и ячейки 23 таблиц несут свои атрибуты (классы строк, `colSpan`, `aria-sort` в `<th>`), а DataGrid отдаёт `<tr>`/`<td>` без хуков для них. Режим карточек на телефоне и подписи колонок держатся на тех же атрибутах. |
|
||||
|
||||
Сборка, `tsc`, `lint:colours`, `oxlint` проходят. Тесты (`npx vitest run --maxWorkers=3`): 28 падений из 2076, все они падали и до миграции (локаль Windows и отсутствие коммита).
|
||||
|
After Width: | Height: | Size: 131 KiB |
|
After Width: | Height: | Size: 114 KiB |
|
After Width: | Height: | Size: 117 KiB |
|
After Width: | Height: | Size: 124 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 213 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 153 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 126 KiB |