The page an administrator uses to bring a node online: name it, get a one-line command and a six-digit code, hand both to whoever is at the machine. That is the last piece between "someone runs a command" and self-service onboarding. POST /api/compute/pairing-script now returns JSON - script, code, expiry and the command - rather than the bare script. There are two callers with two needs: a person at a terminal wants a script to pipe to sh, and the console wants all three values so it can show them once and then forget them. Returning text would have forced the console to parse a shell script to recover a six-digit number, which works until someone reformats the script. The code is shown once and there is no button to fetch it again, because the controller keeps only an Argon2id hash. That is the server's limit, not a UI choice, and offering a "show code" button would be a lie about what it can do. ## Three defects the template flow exposed Running `curl ... | sh` on node 238 - the flow the console hands out - found that the template could never pair, for three separate reasons: - It had no prompt and no code. `000000` is the placeholder that means "nobody filled this in", so the script now asks. A rendered script from the console has the code substituted and skips the prompt, so nobody is asked for a number they do not need. - It had no node name, and the name is what the code is bound to. It now asks too. The first version failed with "check the code has not expired" while the code was fine and the *name* was the placeholder - an error pointing at the wrong thing. - It hardcoded `controller.invalid` as its controller address, so every deployment but one got a script that could not resolve its own controller. The address now comes from the Host header the template was fetched with, and `UMBRA_CONTROLLER` overrides it. The error message itself was part of the problem: a DNS failure reported "check the code has not expired", which is a worse fault than no message because it sends an operator to check the one thing that was working. ## Verified `curl -fsSL http://192.168.123.14:8442/api/node/bootstrap | sh` on 238, code and name typed at the prompts: paired, agent installed, service registered, node `online`. `/api/node/bootstrap` is unauthenticated and returns only the inert template - a script that can check a machine and then stop. It is listed by name in the test that pins unauthenticated node routes, alongside /pair, with the reason each is safe. ## Also found, not fixed `VIEWS` in web/assets/admin.js is `['dashboard','accounts','organizations','audit']` but `view-projects` exists in the markup, so the projects view can never be activated. It predates this work and I have left it alone rather than change navigation behaviour as a side effect of adding a page. 525 passed, 27 skipped.
UmbraTrans web application
The browser client and the account/organization API it talks to.
This is not a mock. Every page calls the real backend under /api. The
three routes below are served from the same origin as the API by
umbratrans_web, because the session cookie is HttpOnly and the CSRF token
travels as a header, and the application deliberately installs no CORS
middleware. A separately hosted frontend could not call it at all.
What is real
| Surface | Backend |
|---|---|
| Sign in, forced first-login password change, sign out | Real, Argon2id and server-side sessions |
| Accounts: list, filter, provision, suspend, reactivate, deactivate, revoke sessions | Real |
| Organizations: list, create, rename, re-slug, suspend, reactivate, archive | Real |
| Audit log | Real |
| Dashboard counts | Real, scoped to the caller |
| Project workspace contents, maintenance, statistics | Not built yet |
The unbuilt areas render as explicit empty states that name the missing backend work. No page displays a fabricated name, count, or metric.
Running it
In development the API serves this directory. From the repository root:
cd /home/umbra/src/UmbraTrans
UMBRA_ENV=development \
UMBRATRANS_REPOSITORY=json \
UMBRATRANS_REPOSITORY_PATH=.local/data/development/umbratrans-web/dev-accounts.json \
UMBRATRANS_STATIC_ROOT=/home/umbra/src/UmbraTrans/web \
UMBRATRANS_ALLOWED_HOSTS=127.0.0.1,localhost \
/home/umbra/.local/bin/uv run python -m umbratrans_web serve \
--host 127.0.0.1 --port 8400 --repository json
There is no first administrator until one is provisioned through the operator command, which reads the one-time password from a hidden prompt:
UMBRA_ENV=development \
/home/umbra/.local/bin/uv run umbratrans-bootstrap-admin \
--username Umbraadmin \
--repository-path .local/data/development/umbratrans-web/dev-accounts.json
--repository json keeps accounts across restarts. --repository memory starts
empty every time.
Routes
/— sign in, and the mandatory password change for a temporary-password account/uadmin/— control panel: dashboard, accounts, organizations, audit/workspace.html— project workspace
Security properties the client relies on
- No secret is ever sent by a client.
POST /api/uadmin/accountshas no password field; the service generates the one-time password. The request schema refuses unknown fields, so a submittedpasswordis rejected before a handler sees it. - A one-time password is shown exactly once, in the
otp-modalpanel. It is held in one variable, cleared when the panel closes and onpagehide, and is never written tolocalStorage,sessionStorage, a cookie, the URL, the page title, or the console. - Every unsafe request carries
X-CSRF-Token, read fresh from the readableumbratrans_csrfcookie.GET /api/healthon page load bootstraps that cookie, because the middleware does not issue one on an unsafe method andPOST /api/auth/loginis exempt. - No inline script, no inline style, no
innerHTML. The server sendsscript-src 'self'; style-src 'self', so an inline<script>or astyle=attribute would be refused by the browser. All server-supplied text is rendered withtextContentthrough theel()helper inapi.js. - Nothing is trusted from the browser. The UI hides controls a role cannot use, but the service re-checks every one of them; a forged identifier is refused with the same generic error as any other forbidden request.
Not a production deployment
The application fails closed in production: the PostgreSQL adapter does not
exist, so create_app raises rather than starting against an unreviewed store.
Serving this directory is therefore a development arrangement, and the reviewed
migration in umbratrans_web/migrations/ has not been applied.
Systemd
umbratrans-web-prototype.service currently serves this directory with
python3 -m http.server, which is static only and cannot reach the API. It
exists so the port stays occupied and the public hostname keeps answering. See
systemd/umbratrans-web-prototype.service.