The organization dropdown rendered empty. Three separate defects behind it, found from the service's own request log rather than by looking at the page: - /api/uadmin/organizations returned 200 - the data was arriving - while /api/compute/nodes returned 422 on every load. ORGANIZATION_ID was only set when the account could see exactly one organization, so for anyone who can see several the node list was requested with no organization and the compute routes refused, as they are designed to. The list is now fetched when a choice is made, not at load. - The page had no case for *no* organizations. A node belongs to exactly one, so there is nowhere to put it: the form is now disabled and a banner explains why, with a link to the organizations screen. A dropdown with nothing in it and no explanation is the same as a broken page. - A 401 - an expired session - was indistinguishable from an empty list. It now says the session ended, which is the one failure the operator can act on. Also removed eight CSS classes I had invented rather than looked up: card, card-body, card-note, field, form-actions, table-wrap, alert--warn and button--quiet. None exist in styles.css, so the page was rendering unstyled. Replaced with what the working console page uses - admin-card, form-field, modal-actions, admin-table-wrap, admin-banner-- warning, button--ghost. Every class on the page is now verified present in the stylesheet rather than assumed, and the markup is checked as balanced by a parser rather than by counting tags by eye. 0005 was applied to the deployment to unblock startup: the tenant-schema contract check runs at boot and correctly refused to start a service whose schema it could not find. That is the check working. Backup taken and checksum-verified first; three tenant schemas created, one per organization, with users=2 orgs=3 nodes=9 audit=46 unchanged. 547 passed, 29 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.