Feedback on the add-node page: the command was hard to read and hard to copy. Both the command and the six-digit code now use .otp-value-row / .otp-value - the wide monospace value with its copy button beside it that the provisioning dialog already uses. Reusing that rather than writing new CSS, because a second styling of the same idea is how the two drift apart. The command was a <textarea> carrying a class named `code-block` that exists nowhere in the stylesheet - I had invented it - so it rendered as a default textarea rather than the monospace block it was meant to be. It is now a <code>, which means it is written with setText rather than .value; setting .value on a <code> silently does nothing, which is the same class of silent failure as the missing toast element. The standalone "Copy the command" and "Copy the code" buttons at the bottom are gone: each value now has its own button next to it, so there is one obvious way to copy each thing and no need to match a button to a field. Done is now labelled to say what it does - "clear this from the page" - because the code is only in the page's memory and the controller cannot produce it again.
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.