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 <noreply@anthropic.com>
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

+4
View File
@@ -0,0 +1,4 @@
github: byGarcia
ko_fi: bygarcia
liberapay: bygarcia
custom: ["https://www.paypal.com/paypalme/adriangmolina"]
+45
View File
@@ -0,0 +1,45 @@
name: Bug report
description: Something in the console looks or works wrong.
labels: [bug]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time. This console aims to look different from the stock one and behave exactly like it, so anything that behaves differently is a bug too.
- type: textarea
id: what
attributes:
label: What happened
description: Which screen, what you did, what you expected and what you got. A screenshot helps a lot.
validations:
required: true
- type: input
id: console
attributes:
label: Console version
description: Shown at the bottom of the sidebar, next to Web Console.
placeholder: "1.2.0"
validations:
required: true
- type: input
id: server
attributes:
label: Technitium DNS Server version
description: Shown at the bottom of the sidebar, next to DNS Server.
placeholder: "15.6.2"
validations:
required: true
- type: dropdown
id: install
attributes:
label: How it is installed
options:
- One-line installer
- Docker image
- By hand
- Other
- type: input
id: browser
attributes:
label: Browser and window width
placeholder: "Chrome 141, 1440 px"
+8
View File
@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Questions
url: https://github.com/byGarcia/technitium-console/discussions
about: How to install it, how something works, or anything that is not a bug or a request.
- name: Technitium DNS Server
url: https://github.com/TechnitiumSoftware/DnsServer/issues
about: Problems with the DNS server itself, not with this console.
+20
View File
@@ -0,0 +1,20 @@
name: Feature request
description: Something the console could do or show better.
labels: [enhancement]
body:
- type: markdown
attributes:
value: |
The console changes how Technitium looks, not what it does: it calls the same server API as the stock console, with the same actions and wording. A request that needs the server to do something new belongs in [Technitium DNS Server](https://github.com/TechnitiumSoftware/DnsServer/issues).
- type: textarea
id: problem
attributes:
label: What you are missing
description: What you are trying to do and what gets in the way.
validations:
required: true
- type: textarea
id: idea
attributes:
label: What you would like
description: How you picture it. Screenshots or examples from other tools are welcome.
@@ -0,0 +1,37 @@
# The console's tarball and its .sha256, built from the checked-out commit.
# The release job and the develop image both use this, so a develop image
# carries exactly what a release built from the same commit would carry.
name: Console tarball
description: Checks and builds the console, then packs dist/ as technitium-console.tar.gz with its .sha256.
runs:
using: composite
steps:
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: npm
- shell: bash
run: npm ci
# The console goes in front of a DNS server, so a build that fails its own
# checks must not exist.
- shell: bash
run: npm run typecheck
- shell: bash
run: npm run lint
- shell: bash
run: npm test
- shell: bash
run: npm run build
# Flat archive: install.sh unpacks it straight into the web root, so the
# files have to be at the root of the tarball and not inside dist/.
- name: Pack
shell: bash
run: |
tar -czf technitium-console.tar.gz -C dist .
sha256sum technitium-console.tar.gz > technitium-console.tar.gz.sha256
echo "Routes emitted: $(find dist -name index.html | wc -l)"
+154
View File
@@ -0,0 +1,154 @@
# The init image for Docker (README, "Docker"): busybox, the install.sh of the
# checked-out commit and a console tarball, checked against its own .sha256
# before building and again inside every platform after pushing (contract D4).
# Only then do the moving tags (`floating`) point at it, so a failed check
# leaves them where they were. For a release, which of them move is decided in
# that same step, from the repo's tags as they are then (floating-tags.sh).
# Shared by the release and develop workflows, so both publish the same image
# the same way.
name: Init image
description: Builds docker/Dockerfile around a console tarball, pushes it for three platforms, checks the bytes in each, then moves the floating tags.
inputs:
image:
description: The image name, without a tag.
required: true
tarball-dir:
description: The folder holding technitium-console.tar.gz and technitium-console.tar.gz.sha256.
required: true
tags:
description: The tags pushed with the build, as docker/metadata-action `tags` lines. Never floating ones.
required: true
floating:
description: Space-separated tag names moved to the build once its bytes are checked. Empty moves none.
required: false
default: ''
release-tag:
description: A release's git tag (vX.Y.Z). Its X.Y and latest are added to `floating` when floating-tags.sh says so, decided from origin's tags right before they move.
required: false
default: ''
token:
description: The token that pushes to GHCR.
required: false
default: ${{ github.token }}
outputs:
version:
description: The version docker/metadata-action read from the first tag.
value: ${{ steps.meta.outputs.version }}
digest:
description: The digest of the manifest list that was pushed and checked.
value: ${{ steps.build.outputs.digest }}
runs:
using: composite
steps:
- name: The build context, with the tarball checked against its checksum
id: context
shell: bash
env:
TARBALLS: ${{ inputs.tarball-dir }}
run: |
ctx="$RUNNER_TEMP/init-image"
rm -rf "$ctx"; mkdir -p "$ctx"
cp install.sh docker/Dockerfile "$ctx/"
cp "$TARBALLS/technitium-console.tar.gz" "$TARBALLS/technitium-console.tar.gz.sha256" "$ctx/"
(cd "$ctx" && sha256sum -c technitium-console.tar.gz.sha256)
echo "dir=$ctx" >> "$GITHUB_OUTPUT"
# latest is never this step's: it moves, with the other floating tags, at the end.
- id: meta
uses: docker/metadata-action@v6
with:
images: ${{ inputs.image }}
flavor: latest=false
tags: ${{ inputs.tags }}
labels: |
org.opencontainers.image.title=technitium-console
org.opencontainers.image.description=Copies the technitium-console web console into a volume shared with Technitium DNS Server, then exits.
org.opencontainers.image.licenses=GPL-3.0-or-later
- uses: docker/setup-buildx-action@v4
- uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ inputs.token }}
# No QEMU: the Dockerfile has no RUN, so nothing executes for the foreign
# platforms. No attestations: they show up as unknown/unknown entries in
# the manifest list, and older engines on NAS boxes trip over them.
- id: build
uses: docker/build-push-action@v7
with:
context: ${{ steps.context.outputs.dir }}
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true
provenance: false
sbom: false
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
# By digest: what was pushed, whatever a tag points at by now. Each
# platform is pulled by ITS OWN manifest digest, not the index's: the
# runner's containerd store refuses a second platform under the same
# index digest ("cannot overwrite digest", run 36910489933).
- name: Every platform carries the tarball, byte for byte (D4)
shell: bash
env:
IMAGE: ${{ inputs.image }}
DIGEST: ${{ steps.build.outputs.digest }}
CONTEXT: ${{ steps.context.outputs.dir }}
run: |
ref="$IMAGE@$DIGEST"
want="$(cut -d' ' -f1 "$CONTEXT/technitium-console.tar.gz.sha256")"
index="$(docker buildx imagetools inspect --raw "$ref")"
jq -r '.manifests[].platform | "\(.os)/\(.architecture)\(if .variant then "/" + .variant else "" end)"' <<<"$index" \
| tee "$RUNNER_TEMP/platforms"
if ! grep -qx 'linux/amd64' "$RUNNER_TEMP/platforms" || ! grep -Eqx 'linux/arm64(/v8)?' "$RUNNER_TEMP/platforms" \
|| ! grep -qx 'linux/arm/v7' "$RUNNER_TEMP/platforms"; then
echo "::error::the manifest list does not hold the three platforms"; exit 1
fi
for p in linux/amd64 linux/arm64 linux/arm/v7; do
d="$(jq -r --arg p "$p" 'first(.manifests[]
| select((.platform.os + "/" + .platform.architecture + (if .platform.variant then "/" + .platform.variant else "" end)) as $q
| $q == $p or ($p == "linux/arm64" and $q == "linux/arm64/v8"))
| .digest) // empty' <<<"$index")"
[ -n "$d" ] || { echo "::error::no manifest for $p"; exit 1; }
docker pull -q --platform "$p" "$IMAGE@$d" >/dev/null
cid="$(docker create --platform "$p" "$IMAGE@$d")"
got="$(docker cp "$cid:/usr/share/technitium-console/technitium-console.tar.gz" - | tar -xO | sha256sum | cut -d' ' -f1)"
docker rm "$cid" >/dev/null
echo "$p $d $got"
[ "$got" = "$want" ] || { echo "::error::$p does not carry the tarball"; exit 1; }
done
# A release's are decided here and not by an earlier job: "Re-run failed
# jobs" hands an earlier job's outputs back as they were, and a release
# tagged since then must keep latest and its X.Y. Anonymous ls-remote: the
# repo is public and checkout kept no token; a failure fails the step.
- name: The floating tags, now that the bytes are checked
if: inputs.floating != '' || inputs.release-tag != ''
shell: bash
env:
IMAGE: ${{ inputs.image }}
DIGEST: ${{ steps.build.outputs.digest }}
FLOATING: ${{ inputs.floating }}
RELEASE_TAG: ${{ inputs.release-tag }}
run: |
if [ -n "$RELEASE_TAG" ]; then
decided="$(git ls-remote --tags --refs origin 'v*' | sh "$GITHUB_ACTION_PATH/floating-tags.sh" "$RELEASE_TAG")"
FLOATING="${FLOATING:+$FLOATING }$decided"
fi
read -r -a names <<< "$FLOATING"
if [ "${#names[@]}" -eq 0 ]; then echo "No floating tags to move."; exit 0; fi
args=()
for t in "${names[@]}"; do args+=(--tag "$IMAGE:$t"); done
docker buildx imagetools create "${args[@]}" "$IMAGE@$DIGEST"
want="$(docker buildx imagetools inspect --raw "$IMAGE@$DIGEST" | jq -c '[.manifests[].digest]')"
for t in "${names[@]}"; do
got="$(docker buildx imagetools inspect --raw "$IMAGE:$t" | jq -c '[.manifests[].digest]')"
echo "$t $got"
[ "$got" = "$want" ] || { echo "::error::$t does not point at the images just checked"; exit 1; }
done
@@ -0,0 +1,36 @@
#!/bin/sh
# Which of Docker's floating tags the release TAG moves, printed on one line
# (`X.Y latest`, `X.Y`, or nothing). The repo's tags come on stdin, as
# `git ls-remote --tags --refs origin 'v*'` prints them or one name a line.
#
# Floating tags only ever move forward: latest goes to the highest release in
# the repo and X.Y to the highest of its own line, so re-running an old tag or
# tagging a backport points neither back in time. Prereleases are not releases
# here: they move neither. Called right before the tags move, never from an
# earlier job's outputs, which "Re-run failed jobs" would hand back unchanged.
set -eu
[ $# -eq 1 ] || { echo "usage: git ls-remote --tags --refs origin 'v*' | $0 vX.Y.Z" >&2; exit 2; }
tag="$1"
releases="$(sed 's|.*refs/tags/||' | { grep -Ex 'v[0-9]+\.[0-9]+\.[0-9]+' || true; } | sort -V)"
if ! printf '%s\n' "$tag" | grep -Eqx 'v[0-9]+\.[0-9]+\.[0-9]+'; then
echo "$tag is not a release (vX.Y.Z): no floating tags" >&2
exit 0
fi
# A release that cannot find itself among the tags has not read them.
if ! printf '%s\n' "$releases" | grep -Fqx "$tag"; then
echo "::error::$tag is not among the tags read from origin" >&2
exit 1
fi
line="$(printf '%s\n' "$tag" | cut -d. -f1-2)."
top="$(printf '%s\n' "$releases" | tail -n1)"
top_of_line="$(printf '%s\n' "$releases" | awk -v p="$line" 'index($0, p) == 1' | tail -n1)"
floating=""
if [ "$tag" = "$top_of_line" ]; then floating="$(printf '%s\n' "${tag#v}" | cut -d. -f1-2)"; fi
if [ "$tag" = "$top" ]; then floating="${floating:+$floating }latest"; fi
echo "$tag: highest release $top, highest of its line $top_of_line -> floating tags: ${floating:-none}" >&2
printf '%s\n' "$floating"
+85
View File
@@ -0,0 +1,85 @@
# An init image of whatever is on develop, to pull and try before a release:
# ghcr.io/bygarcia/technitium-console:develop and :develop-<sha7>. It is built
# exactly as a release is (the same tarball action and the same image action)
# and never touches latest, X.Y or X.Y.Z. No release, no notes. Not for
# production: develop moves with every push.
#
# Two jobs, so that the one holding the package token never runs npm: the
# dependencies' install scripts, the tests and the build run in `tarball`,
# which can read the repo and nothing else; `image` only gets their output.
name: Develop image
on:
push:
branches: [develop]
workflow_dispatch:
permissions: {}
# The newest push to develop wins; an older run still going is dropped. Keyed
# on the ref, so a manual run from another branch (which builds nothing)
# cannot cancel a develop build.
concurrency:
group: develop-image-${{ github.ref }}
cancel-in-progress: true
jobs:
tarball:
# develop of this repository only: a manual run from another branch, or a
# fork's own develop, builds nothing.
if: github.repository == 'byGarcia/technitium-console' && github.ref == 'refs/heads/develop'
runs-on: ubuntu-latest
permissions:
contents: read
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: ./.github/actions/console-tarball
- uses: actions/upload-artifact@v7
with:
name: console-tarball
path: |
technitium-console.tar.gz
technitium-console.tar.gz.sha256
if-no-files-found: error
# A day: "Re-run failed jobs" of `image` after that fails at the
# download; "Re-run all jobs" builds the tarball again.
retention-days: 1
image:
needs: tarball
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
# bash -eo pipefail: a failing command in a pipe fails the step.
defaults:
run:
shell: bash
steps:
# For install.sh, docker/Dockerfile and the image action; nothing here
# installs or runs the repo's dependencies.
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/download-artifact@v8
with:
name: console-tarball
path: ${{ runner.temp }}/tarball
# init-image checks the tarball against its .sha256 before building.
# develop-<sha7> is pushed with the build; develop moves to it once
# every platform has been checked.
- uses: ./.github/actions/init-image
with:
image: ghcr.io/bygarcia/technitium-console
tarball-dir: ${{ runner.temp }}/tarball
tags: type=sha,prefix=develop-,format=short
floating: develop
+200
View File
@@ -0,0 +1,200 @@
# Builds the console and attaches it to the release, because that tarball is
# what install.sh downloads. Without this there is nothing to install.
#
# npm (the dependencies' install scripts, the tests, the build) runs only in
# `tarball`, which can read the repo and nothing else. The job that can write
# to the repo, `release`, gets just the tarball and its checksum.
name: Release
on:
push:
tags: ['v*']
workflow_dispatch:
permissions: {}
jobs:
tarball:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
# The same tarball the develop image is built from (develop-image.yml).
- uses: ./.github/actions/console-tarball
- uses: actions/upload-artifact@v7
with:
name: console-tarball
path: |
technitium-console.tar.gz
technitium-console.tar.gz.sha256
if-no-files-found: error
# A day: "Re-run failed jobs" of `release` after that fails at the
# download; "Re-run all jobs" is the way.
retention-days: 1
release:
needs: tarball
# Tags only: a manual run from a branch must not publish a release named
# after it. The image job below has the same guard.
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
permissions:
contents: write
steps:
# For CHANGELOG.md and the tags; no npm here. The token is not left in
# .git/config: only the release step below gets it, as an input.
- uses: actions/checkout@v4
with:
persist-credentials: false
# Outside the workspace: the artifact comes from the job that runs npm,
# and nothing in it may land over the checkout whose script and
# CHANGELOG.md the steps below use. Same place as develop-image.yml.
- uses: actions/download-artifact@v8
with:
name: console-tarball
path: ${{ runner.temp }}/tarball
# Exactly the two files the tarball job packs, and the bytes checked
# against the checksum, before anything is published.
- name: The tarball, checked against its checksum
working-directory: ${{ runner.temp }}/tarball
run: |
set -eu
found="$(find . -mindepth 1 | LC_ALL=C sort | tr '\n' ' ')"
expected="./technitium-console.tar.gz ./technitium-console.tar.gz.sha256 "
if [ "$found" != "$expected" ] || [ -n "$(find . -mindepth 1 ! -type f)" ]; then
echo "The artifact holds something other than the tarball and its checksum: $found" >&2
exit 1
fi
sha256sum -c technitium-console.tar.gz.sha256
# Docker's floating tags only ever move forward (latest to the highest
# release, X.Y to the highest of its line; prereleases move neither), as
# .github/actions/init-image/floating-tags.sh decides. The notes below
# need to know whether latest will be this release; the image job does not
# take it from here, it decides again right before moving the tags.
- name: Which floating tags this release moves
id: floating
shell: bash
run: |
# Anonymous: the repo is public and checkout kept no token. If it ever
# cannot read the tags, pipefail or the script's guard fails the release.
floating="$(git ls-remote --tags --refs origin 'v*' | sh .github/actions/init-image/floating-tags.sh "$GITHUB_REF_NAME")"
latest=false
case " $floating " in *" latest "*) latest=true ;; esac
echo "latest=$latest" >> "$GITHUB_OUTPUT"
# The release page is what people read, so it carries this version's own
# section of CHANGELOG.md. A tag with no section there fails here instead of
# publishing a release that says nothing.
- name: Release notes
id: notes
shell: bash
env:
LATEST: ${{ steps.floating.outputs.latest }}
run: |
tag="${GITHUB_REF_NAME}"
# A prerelease (vX.Y.Z-rc.N) reads the section of the version it previews.
base="${tag%%-*}"
awk -v head="## ${base} " 'index($0, head) == 1 { on = 1; next } on && /^## v/ { exit } on' CHANGELOG.md \
| sed '/^---$/d' > section.md
# Release pages keep every newline, so the wrapped paragraphs of
# CHANGELOG.md are joined back into one line each.
python3 - <<'PY'
out = []
for line in open('section.md').read().split('\n'):
cont = line.startswith(' ') and not line.lstrip().startswith(('- ', '|', '#'))
plain = line and not line.startswith(('- ', '#', '|', '```')) and not line.startswith(' ')
if out and out[-1] and not out[-1].startswith(('#', '|')) and (cont or plain):
out[-1] += ' ' + line.strip()
else:
out.append(line)
open('notes.md', 'w').write('\n'.join(out))
PY
[ -s notes.md ] || { echo "CHANGELOG.md has no section for ${base}"; exit 1; }
# The installer's default is the latest release, which a prerelease
# never is: it is installed by naming it.
# shellcheck disable=SC2016 # the backticks are Markdown, not a command
if [ "$base" != "$tag" ]; then
printf '\n**A prerelease, for testing:** the one-line installer from the [README](https://github.com/byGarcia/technitium-console#installing) installs it with `sh -s -- --version %s`, and goes back with `--version` and the release you had. Which console goes with which server: [CHANGELOG](https://github.com/byGarcia/technitium-console/blob/main/CHANGELOG.md#which-version-for-which-server).\n' "$tag"
else
printf '\n**Install or update:** run the one-line installer from the [README](https://github.com/byGarcia/technitium-console#installing) again. Which console goes with which server: [CHANGELOG](https://github.com/byGarcia/technitium-console/blob/main/CHANGELOG.md#which-version-for-which-server).\n'
fi >> notes.md
echo "prerelease=$([ "$base" != "$tag" ] && echo true || echo false)" >> "$GITHUB_OUTPUT"
# The image job below publishes this same release for Docker. Pulling
# only brings it when it is the one latest points at.
# shellcheck disable=SC2016 # the backticks are Markdown, not a command
if [ "$LATEST" = true ]; then
printf '\n**On Docker:** `docker compose pull technitium-console && docker compose up -d technitium-console`, or pin `ghcr.io/bygarcia/technitium-console:%s`. See [Docker](https://github.com/byGarcia/technitium-console#docker).\n' "${tag#v}"
else
printf '\n**On Docker:** pin `ghcr.io/bygarcia/technitium-console:%s`. See [Docker](https://github.com/byGarcia/technitium-console#docker).\n' "${tag#v}"
fi >> notes.md
# The title is the tag and the section's opening bold sentence.
lead="$({ grep -m1 -o '^\*\*[^*]*\*\*' notes.md || true; } | sed 's/^\*\*//; s/\*\*$//; s/\.$//')"
echo "title=${tag}${lead:+: ${lead}}" >> "$GITHUB_OUTPUT"
cat notes.md
- uses: softprops/action-gh-release@v2
with:
token: ${{ github.token }}
files: |
${{ runner.temp }}/tarball/technitium-console.tar.gz
${{ runner.temp }}/tarball/technitium-console.tar.gz.sha256
name: ${{ steps.notes.outputs.title }}
body_path: notes.md
generate_release_notes: true
# A prerelease is never the one releases/latest, and so install.sh's
# default, points at. A release leaves it unset, as before: an explicit
# true would also move latest back when an older tag is re-run.
prerelease: ${{ steps.notes.outputs.prerelease }}
make_latest: ${{ steps.notes.outputs.prerelease == 'true' && 'false' || '' }}
# The init image for Docker (README, "Docker"), from the tarball the job
# above has just published: X.Y.Z is pushed and checked in every platform
# (contract D4), and only then do X.Y and latest move, decided in that same
# step from the tags as they are then, so a re-run of this job alone never
# moves them back. The steps live in .github/actions/init-image, shared with
# the develop image. GITHUB_TOKEN is the only credential. A package's first
# publication is private: it is made public by hand, once.
image:
needs: release
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
# Two runs of one tag never push over each other.
concurrency:
group: image-${{ github.ref }}
cancel-in-progress: false
# bash -eo pipefail: a failing command in a pipe fails the step.
defaults:
run:
shell: bash
steps:
# For install.sh, docker/Dockerfile and the image action; no npm here.
- uses: actions/checkout@v4
with:
persist-credentials: false
# The bytes people download, not a second build of them.
- name: The release's own tarball
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release download "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" --dir "$RUNNER_TEMP/release" \
--pattern technitium-console.tar.gz --pattern technitium-console.tar.gz.sha256
# vX.Y.Z is pushed as X.Y.Z alone; a prerelease as its own version only.
- uses: ./.github/actions/init-image
with:
image: ghcr.io/bygarcia/technitium-console
tarball-dir: ${{ runner.temp }}/release
tags: type=semver,pattern={{version}}
release-tag: ${{ github.ref_name }}