Files
UmbraTrans/web
umbra audio 14eba11ffe An empty organization list is now said out loud, and the page stops pretending
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.
2026-10-03 15:28:30 -07:00
..

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/accounts has no password field; the service generates the one-time password. The request schema refuses unknown fields, so a submitted password is rejected before a handler sees it.
  • A one-time password is shown exactly once, in the otp-modal panel. It is held in one variable, cleared when the panel closes and on pagehide, and is never written to localStorage, sessionStorage, a cookie, the URL, the page title, or the console.
  • Every unsafe request carries X-CSRF-Token, read fresh from the readable umbratrans_csrf cookie. GET /api/health on page load bootstraps that cookie, because the middleware does not issue one on an unsafe method and POST /api/auth/login is exempt.
  • No inline script, no inline style, no innerHTML. The server sends script-src 'self'; style-src 'self', so an inline <script> or a style= attribute would be refused by the browser. All server-supplied text is rendered with textContent through the el() helper in api.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.