rename web/ to admin/, along with the web-named build and cli identifiers

This commit is contained in:
2026-08-16 00:17:58 +02:00
parent 5b3d1cd65c
commit 1e97c80f6b
136 changed files with 196 additions and 196 deletions
+22 -22
View File
@@ -119,39 +119,39 @@ jobs:
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
cache: npm cache: npm
cache-dependency-path: web/package-lock.json cache-dependency-path: admin/package-lock.json
- name: Install dependencies - name: Install dependencies
working-directory: web working-directory: admin
run: npm ci run: npm ci
- name: Check formatting - name: Check formatting
working-directory: web working-directory: admin
run: npm run format:check run: npm run format:check
- name: Lint - name: Lint
working-directory: web working-directory: admin
run: npm run lint run: npm run lint
- name: Typecheck - name: Typecheck
working-directory: web working-directory: admin
run: npm run typecheck run: npm run typecheck
- name: Run tests - name: Run tests
working-directory: web working-directory: admin
run: npm test run: npm test
- name: Build - name: Build
working-directory: web working-directory: admin
run: npm run build run: npm run build
# The licence inventory has to cover every package whose bytes ship, and # The licence inventory has to cover every package whose bytes ship, and
# the lockfile does not answer that question: it lists what could be # the lockfile does not answer that question: it lists what could be
# reached, not what rollup kept. The bundle is what this reads. The logic # reached, not what rollup kept. The bundle is what this reads. The logic
# lives in web/scripts/, unit-tested by `npm test`, so it runs on a laptop # lives in admin/scripts/, unit-tested by `npm test`, so it runs on a laptop
# exactly as it runs here (milestone-14 deviation 24). # exactly as it runs here (milestone-14 deviation 24).
- name: Assert the packages bundled into web/dist are the recorded ones - name: Assert the packages bundled into admin/dist are the recorded ones
working-directory: web working-directory: admin
run: npm run assert-bundled run: npm run assert-bundled
# The package and container jobs consume this bundle instead of building # The package and container jobs consume this bundle instead of building
@@ -164,16 +164,16 @@ jobs:
# #
# A later move to v4 has to add `include-hidden-files: true` here. # A later move to v4 has to add `include-hidden-files: true` here.
# `npm run build` writes the freshness stamp to the hidden file # `npm run build` writes the freshness stamp to the hidden file
# web/dist/.src-hash (milestone-15 ruling 5), and v4.4.0 and later drop # admin/dist/.src-hash (milestone-15 ruling 5), and v4.4.0 and later drop
# dotfiles by default. It is inactive today — v3 keeps them, and the # dotfiles by default. It is inactive today — v3 keeps them, and the
# package job's target path skips the stamp check regardless — but a move # package job's target path skips the stamp check regardless — but a move
# to v4 that also pointed the download back at web/dist would fail with # to v4 that also pointed the download back at admin/dist would fail with
# "web/dist is stale". # "admin/dist is stale".
- name: Upload the built web UI - name: Upload the built web UI
uses: actions/upload-artifact@c24449f33cd45d4826c6702db7e49f7cdb9b551d # v3.2.1-node20 uses: actions/upload-artifact@c24449f33cd45d4826c6702db7e49f7cdb9b551d # v3.2.1-node20
with: with:
name: web-dist name: admin-dist
path: web/dist path: admin/dist
if-no-files-found: error if-no-files-found: error
package: package:
@@ -201,22 +201,22 @@ jobs:
- name: Create the fetch temp dir zig assumes - name: Create the fetch temp dir zig assumes
run: mkdir -p "${ZIG_GLOBAL_CACHE_DIR:?}/tmp" run: mkdir -p "${ZIG_GLOBAL_CACHE_DIR:?}/tmp"
# `dist` refuses web/dist-placeholder (ruling 4), so a real bundle has to # `dist` refuses admin/dist-placeholder (ruling 4), so a real bundle has to
# exist before the packaging gate runs. It arrives from the frontend job, # exist before the packaging gate runs. It arrives from the frontend job,
# already formatted, linted, typechecked, tested and licence-checked. # already formatted, linted, typechecked, tested and licence-checked.
# #
# The target is deliberately not `web/dist`: build.zig runs the freshness # The target is deliberately not `admin/dist`: build.zig runs the freshness
# stamp check for that exact path and no other (milestone-15 ruling 5), # stamp check for that exact path and no other (milestone-15 ruling 5),
# and the check shells out to `node`. Here it would buy nothing — the # and the check shells out to `node`. Here it would buy nothing — the
# stamp hashes the web/ sources, not the bundle, so against a checkout of # stamp hashes the admin/ sources, not the bundle, so against a checkout of
# the same commit that built the bundle it can only agree. An explicit # the same commit that built the bundle it can only agree. An explicit
# path is the case build.zig documents for a bundle built elsewhere, and # path is the case build.zig documents for a bundle built elsewhere, and
# taking it keeps node out of this job entirely. # taking it keeps node out of this job entirely.
- name: Download the web UI built by the frontend job - name: Download the web UI built by the frontend job
uses: actions/download-artifact@ad191675b41f6a5b46da9a048cb6893812da158b # v3.1.0-node20 uses: actions/download-artifact@ad191675b41f6a5b46da9a048cb6893812da158b # v3.1.0-node20
with: with:
name: web-dist name: admin-dist
path: web-dist-ci path: admin-dist-ci
# Ruling 2: build.zig.zon is the only place besides the tag that carries # Ruling 2: build.zig.zon is the only place besides the tag that carries
# the version, and ruling 5 makes verify-dist assert the two agree. The # the version, and ruling 5 makes verify-dist assert the two agree. The
@@ -239,7 +239,7 @@ jobs:
zig build dist \ zig build dist \
-Dversion-string="$CI_VERSION" \ -Dversion-string="$CI_VERSION" \
-Dgit-commit="$GITHUB_SHA" \ -Dgit-commit="$GITHUB_SHA" \
-Dweb-dist=web-dist-ci \ -Dadmin-dist=admin-dist-ci \
-Doptimize=ReleaseSafe -Doptimize=ReleaseSafe
# verify-dist owns every assert the CI shell used to make: ELF static # verify-dist owns every assert the CI shell used to make: ELF static
@@ -255,7 +255,7 @@ jobs:
zig build verify-dist \ zig build verify-dist \
-Dversion-string="$CI_VERSION" \ -Dversion-string="$CI_VERSION" \
-Dgit-commit="$GITHUB_SHA" \ -Dgit-commit="$GITHUB_SHA" \
-Dweb-dist=web-dist-ci \ -Dadmin-dist=admin-dist-ci \
-Doptimize=ReleaseSafe -Doptimize=ReleaseSafe
# deploy/docker/Dockerfile copies both of these trees and nothing else # deploy/docker/Dockerfile copies both of these trees and nothing else
+4 -4
View File
@@ -269,10 +269,10 @@ jobs:
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
cache: npm cache: npm
cache-dependency-path: web/package-lock.json cache-dependency-path: admin/package-lock.json
- name: Build the web UI - name: Build the web UI
working-directory: web working-directory: admin
run: | run: |
npm ci npm ci
npm run build npm run build
@@ -283,7 +283,7 @@ jobs:
zig build dist zig build dist
-Dversion-string="$VERSION" -Dversion-string="$VERSION"
-Dgit-commit="$TAG_COMMIT" -Dgit-commit="$TAG_COMMIT"
-Dweb-dist=web/dist -Dadmin-dist=admin/dist
-Doptimize=ReleaseSafe -Doptimize=ReleaseSafe
- name: Verify the release artifacts - name: Verify the release artifacts
@@ -291,7 +291,7 @@ jobs:
zig build verify-dist zig build verify-dist
-Dversion-string="$VERSION" -Dversion-string="$VERSION"
-Dgit-commit="$TAG_COMMIT" -Dgit-commit="$TAG_COMMIT"
-Dweb-dist=web/dist -Dadmin-dist=admin/dist
-Doptimize=ReleaseSafe -Doptimize=ReleaseSafe
# Step 9. Extracted and validated before anything is pushed anywhere, so # Step 9. Extracted and validated before anything is pushed anywhere, so
+4 -4
View File
@@ -1,7 +1,7 @@
.zig-cache/ .zig-cache/
zig-out/ zig-out/
zig-pkg/ zig-pkg/
web/node_modules/ admin/node_modules/
web/dist/ admin/dist/
web/dist-sourcemap/ admin/dist-sourcemap/
web-dist-ci/ admin-dist-ci/
+3 -3
View File
@@ -34,10 +34,10 @@ One trap: running a cached test binary by hand with `--listen=-` aborts with `in
## Regenerating the contract samples ## Regenerating the contract samples
`web/src/lib/contractSamples.gen.ts` is a committed golden of canonicalized API responses, byte-compared against the live server by a `-Dintegration` test and type-checked by `tsc`. After a deliberate API contract change, regenerate it with: `admin/src/lib/contractSamples.gen.ts` is a committed golden of canonicalized API responses, byte-compared against the live server by a `-Dintegration` test and type-checked by `tsc`. After a deliberate API contract change, regenerate it with:
``` ```
zig build test -Dintegration -Dcontract-samples-out="$PWD/web/src/lib/contractSamples.gen.ts" zig build test -Dintegration -Dcontract-samples-out="$PWD/admin/src/lib/contractSamples.gen.ts"
``` ```
then update `web/src/lib/types.ts` to match and commit both. Never edit the generated file by hand. then update `admin/src/lib/types.ts` to match and commit both. Never edit the generated file by hand.
+1 -1
View File
@@ -242,7 +242,7 @@ src/
rules.zig local.zig lookup.zig pause.zig settings.zig rules.zig local.zig lookup.zig pause.zig settings.zig
upstream_health.zig certs.zig health.zig version.zig upstream_health.zig certs.zig health.zig version.zig
web/ # Vite + React + TS + StyleX + React Aria + TanStack admin/ # Vite + React + TS + StyleX + React Aria + TanStack
vendor/ # sqlite3 amalgamation, mbedtls (pinned) vendor/ # sqlite3 amalgamation, mbedtls (pinned)
docs/ # tutorial/ how-to/ reference/ explanation/ (Diátaxis) docs/ # tutorial/ how-to/ reference/ explanation/ (Diátaxis)
tests/ # dns/ integration/ fuzz/ tests/ # dns/ integration/ fuzz/
+5 -5
View File
@@ -60,20 +60,20 @@ Full install instructions, including the systemd path and the Pi 5 recipe, are i
Requires [Zig 0.16.0](https://ziglang.org/download/) and Node.js 24 (for the web UI). C dependencies (SQLite, mbedTLS) are vendored and built by `zig build`. Requires [Zig 0.16.0](https://ziglang.org/download/) and Node.js 24 (for the web UI). C dependencies (SQLite, mbedTLS) are vendored and built by `zig build`.
```sh ```sh
(cd web && npm ci && npm run build) # web UI -> web/dist (cd admin && npm ci && npm run build) # web UI -> admin/dist
zig build -Dweb-dist=web/dist # native binary -> zig-out/bin/nxdns zig build -Dadmin-dist=admin/dist # native binary -> zig-out/bin/nxdns
zig build test --summary all # unit tests zig build test --summary all # unit tests
``` ```
The release artifacts come out of the same build graph, so the whole release build runs on a laptop exactly as it runs on the CI runner: The release artifacts come out of the same build graph, so the whole release build runs on a laptop exactly as it runs on the CI runner:
```sh ```sh
(cd web && npm ci && npm run build) # required: dist refuses the placeholder (cd admin && npm ci && npm run build) # required: dist refuses the placeholder
VERSION=$(sed -n 's/^[[:space:]]*\.version[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' build.zig.zon) VERSION=$(sed -n 's/^[[:space:]]*\.version[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' build.zig.zon)
zig build dist -Dversion-string="$VERSION" -Dgit-commit=$(git rev-parse HEAD) \ zig build dist -Dversion-string="$VERSION" -Dgit-commit=$(git rev-parse HEAD) \
-Dweb-dist=web/dist -Doptimize=ReleaseSafe # tarballs -> zig-out/dist/ -Dadmin-dist=admin/dist -Doptimize=ReleaseSafe # tarballs -> zig-out/dist/
zig build verify-dist -Dversion-string="$VERSION" -Dgit-commit=$(git rev-parse HEAD) \ zig build verify-dist -Dversion-string="$VERSION" -Dgit-commit=$(git rev-parse HEAD) \
-Dweb-dist=web/dist -Doptimize=ReleaseSafe # the release checks -Dadmin-dist=admin/dist -Doptimize=ReleaseSafe # the release checks
``` ```
The version comes from `build.zig.zon` because `verify-dist` asserts the two agree; a tag sets both. The version comes from `build.zig.zon` because `verify-dist` asserts the two agree; a tag sets both.

Before

Width:  |  Height:  |  Size: 303 B

After

Width:  |  Height:  |  Size: 303 B

View File
+2 -2
View File
@@ -1,11 +1,11 @@
{ {
"name": "nxdns-web", "name": "nxdns-admin",
"version": "0.0.0", "version": "0.0.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "nxdns-web", "name": "nxdns-admin",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@stylexjs/stylex": "0.19.0", "@stylexjs/stylex": "0.19.0",
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"name": "nxdns-web", "name": "nxdns-admin",
"private": true, "private": true,
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",

Before

Width:  |  Height:  |  Size: 262 B

After

Width:  |  Height:  |  Size: 262 B

@@ -1,11 +1,11 @@
#!/usr/bin/env node #!/usr/bin/env node
// The set of npm packages whose bytes reach web/dist must be exactly the set // The set of npm packages whose bytes reach admin/dist must be exactly the set
// recorded in licenses/dependency-identity.txt (milestone-14 ruling 3). // recorded in licenses/dependency-identity.txt (milestone-14 ruling 3).
// //
// The shipped build carries no sourcemaps, so this makes a second build with // The shipped build carries no sourcemaps, so this makes a second build with
// them into its own directory: the `sources` list of each chunk names the // them into its own directory: the `sources` list of each chunk names the
// modules that went into it, and the artifact `npm run build` produced stays // modules that went into it, and the artifact `npm run build` produced stays
// untouched. Runs from web/ as `npm run assert-bundled`, on a laptop exactly as // untouched. Runs from admin/ as `npm run assert-bundled`, on a laptop exactly as
// on the runner. // on the runner.
import { execFileSync } from "node:child_process"; import { execFileSync } from "node:child_process";
@@ -85,10 +85,10 @@ try {
const recorded = recordedPackages(identity); const recorded = recordedPackages(identity);
if (recorded === null) { if (recorded === null) {
fail("assert-bundled: licenses/dependency-identity.txt has no '[npm packages bundled into web/dist]' section"); fail("assert-bundled: licenses/dependency-identity.txt has no '[npm packages bundled into admin/dist]' section");
} }
if (recorded.length === 0) { if (recorded.length === 0) {
fail("assert-bundled: the '[npm packages bundled into web/dist]' section is empty"); fail("assert-bundled: the '[npm packages bundled into admin/dist]' section is empty");
} }
const { added, removed } = comparePackages(recorded, bundled); const { added, removed } = comparePackages(recorded, bundled);
@@ -96,12 +96,12 @@ if (added.length !== 0 || removed.length !== 0) {
process.stderr.write(`${formatDiff(recorded, bundled)}\n\n`); process.stderr.write(`${formatDiff(recorded, bundled)}\n\n`);
fail( fail(
[ [
"the set of npm packages in web/dist has changed (-recorded +current).", "the set of npm packages in admin/dist has changed (-recorded +current).",
"Work out what the change means for licenses/inventory.zon first, then record", "Work out what the change means for licenses/inventory.zon first, then record",
"the new list in that section of licenses/dependency-identity.txt.", "the new list in that section of licenses/dependency-identity.txt.",
].join("\n"), ].join("\n"),
); );
} }
process.stdout.write(`web/dist bundles exactly the ${bundled.length} recorded packages:\n`); process.stdout.write(`admin/dist bundles exactly the ${bundled.length} recorded packages:\n`);
for (const name of bundled) process.stdout.write(`${name}\n`); for (const name of bundled) process.stdout.write(`${name}\n`);
@@ -14,7 +14,7 @@
// brace inside a string blinds the stripper; and with several stylesheets it // brace inside a string blinds the stripper; and with several stylesheets it
// judges each alone, not their load order in the document. // judges each alone, not their load order in the document.
// //
// This check runs from web/ as part of `npm run build`. // This check runs from admin/ as part of `npm run build`.
import { readdirSync, readFileSync } from "node:fs"; import { readdirSync, readFileSync } from "node:fs";
import { dirname, join } from "node:path"; import { dirname, join } from "node:path";
@@ -110,7 +110,7 @@ for (const sheet of sheets) {
} }
if (failed) { if (failed) {
console.error("Wrap it in a layer declared before StyleX's, as web/src/styles.css does."); console.error("Wrap it in a layer declared before StyleX's, as admin/src/styles.css does.");
process.exit(1); process.exit(1);
} }
console.log( console.log(
@@ -8,7 +8,7 @@
// lockfile, no version and no dependency set changes — only the bundle does. So // lockfile, no version and no dependency set changes — only the bundle does. So
// the bundle is what this reads. // the bundle is what this reads.
const sectionHeading = "[npm packages bundled into web/dist]"; const sectionHeading = "[npm packages bundled into admin/dist]";
// A sourcemap `sources` entry for a dependency ends in // A sourcemap `sources` entry for a dependency ends in
// `node_modules/<name>/<file>` or `node_modules/@<scope>/<name>/<file>`. Only // `node_modules/<name>/<file>` or `node_modules/@<scope>/<name>/<file>`. Only
@@ -48,7 +48,7 @@ describe("recordedPackages", () => {
"[some earlier section]", "[some earlier section]",
"ignored", "ignored",
"", "",
"[npm packages bundled into web/dist]", "[npm packages bundled into admin/dist]",
"react", "react",
"@tanstack/react-query", "@tanstack/react-query",
"", "",
@@ -64,7 +64,7 @@ describe("recordedPackages", () => {
it("distinguishes a missing section from an empty one", () => { it("distinguishes a missing section from an empty one", () => {
expect(recordedPackages("[other]\nx\n")).toBeNull(); expect(recordedPackages("[other]\nx\n")).toBeNull();
expect(recordedPackages("[npm packages bundled into web/dist]\n\n[next]\n")).toEqual([]); expect(recordedPackages("[npm packages bundled into admin/dist]\n\n[next]\n")).toEqual([]);
}); });
}); });
@@ -1,6 +1,6 @@
#!/usr/bin/env node #!/usr/bin/env node
// Freshness stamp for web/dist (milestone-15 ruling 5). A stale dist has // Freshness stamp for admin/dist (milestone-15 ruling 5). A stale dist has
// already shipped a crashing settings page once. Write mode runs from web/ as // already shipped a crashing settings page once. Write mode runs from admin/ as
// part of `npm run build`; check mode runs from the repository root as a // part of `npm run build`; check mode runs from the repository root as a
// build.zig system command. Every path resolves from this file's own location // build.zig system command. Every path resolves from this file's own location
// so both working directories hash the same set. // so both working directories hash the same set.
@@ -13,7 +13,7 @@ import { fileURLToPath } from "node:url";
const webRoot = dirname(dirname(fileURLToPath(import.meta.url))); const webRoot = dirname(dirname(fileURLToPath(import.meta.url)));
const distDir = join(webRoot, "dist"); const distDir = join(webRoot, "dist");
const stampFile = join(distDir, ".src-hash"); const stampFile = join(distDir, ".src-hash");
const stampRelative = "web/dist/.src-hash"; const stampRelative = "admin/dist/.src-hash";
const inputDirs = ["src", "public"]; const inputDirs = ["src", "public"];
const inputFiles = [ const inputFiles = [
@@ -26,7 +26,7 @@ const inputFiles = [
"tsconfig.node.json", "tsconfig.node.json",
]; ];
const staleMessage = "web/dist is stale: rebuild the frontend (npm run build)"; const staleMessage = "admin/dist is stale: rebuild the frontend (npm run build)";
function fail(message) { function fail(message) {
process.stderr.write(`${message}\n`); process.stderr.write(`${message}\n`);
@@ -39,7 +39,7 @@ function walk(relativeDir) {
try { try {
entries = readdirSync(absolute, { withFileTypes: true }); entries = readdirSync(absolute, { withFileTypes: true });
} catch (err) { } catch (err) {
fail(`stamp-dist: cannot read web/${relativeDir}: ${err.message}`); fail(`stamp-dist: cannot read admin/${relativeDir}: ${err.message}`);
} }
const found = []; const found = [];
for (const entry of entries) { for (const entry of entries) {
@@ -57,9 +57,9 @@ function inputSet() {
const paths = [...inputFiles, ...inputDirs.flatMap(walk)]; const paths = [...inputFiles, ...inputDirs.flatMap(walk)];
for (const path of inputFiles) { for (const path of inputFiles) {
try { try {
if (!statSync(join(webRoot, path)).isFile()) fail(`stamp-dist: web/${path} is not a file`); if (!statSync(join(webRoot, path)).isFile()) fail(`stamp-dist: admin/${path} is not a file`);
} catch (err) { } catch (err) {
fail(`stamp-dist: cannot stat web/${path}: ${err.message}`); fail(`stamp-dist: cannot stat admin/${path}: ${err.message}`);
} }
} }
// Sorted by path so the digest does not depend on directory order. // Sorted by path so the digest does not depend on directory order.
@@ -9,7 +9,7 @@
// declare, and a string outside a literal union. // declare, and a string outside a literal union.
// //
// Regenerate with: // Regenerate with:
// zig build test -Dintegration -Dcontract-samples-out="$PWD/web/src/lib/contractSamples.gen.ts" // zig build test -Dintegration -Dcontract-samples-out="$PWD/admin/src/lib/contractSamples.gen.ts"
import type { import type {
Blocklist, Blocklist,
@@ -6,4 +6,4 @@
pub const bytes = @embedFile("contractSamples.gen.ts"); pub const bytes = @embedFile("contractSamples.gen.ts");
/// Repo-relative path, so a failing assertion names the file to regenerate. /// Repo-relative path, so a failing assertion names the file to regenerate.
pub const path = "web/src/lib/contractSamples.gen.ts"; pub const path = "admin/src/lib/contractSamples.gen.ts";
+1 -1
View File
@@ -4,7 +4,7 @@
* those rules are written against. * those rules are written against.
* *
* The reset is not optional and not cosmetic. Tailwind's preflight used to * The reset is not optional and not cosmetic. Tailwind's preflight used to
* supply it, so every StyleX style in `web/src` is written assuming border-box * supply it, so every StyleX style in `admin/src` is written assuming border-box
* sizing, no default margins, unstyled lists and form controls that inherit * sizing, no default margins, unstyled lists and form controls that inherit
* their font. Deleting this block does not restore browser defaults it * their font. Deleting this block does not restore browser defaults it
* silently changes the meaning of every size and spacing value in the app. * silently changes the meaning of every size and spacing value in the app.

Some files were not shown because too many files have changed in this diff Show More