// dashboard
Web dashboard
Served by the daemon itself at / — a handful of static files
embedded at compile time. No build step, no bundler, and no request that
leaves the host: Alpine.js is vendored, there is no webfont and no icon
font. Deep links like /ui/projects/1 or
/ui/environments/5?tab=logs survive a hard refresh.
Everything the panel does goes through the same HTTP API the CLI uses. There is no privileged back channel; anything you can do here you can script.
The views
| Section | What you'll find |
|---|---|
/ui/onboarding | Setup wizard — auto-opens on first visit. Five steps, all of them also available from the CLI. Re-run anytime from Setup in the top bar. |
/ui/environments | Every environment across projects — filter by project, state or branch; pause, wake and destroy inline; auto-refresh. |
/ui/environments/{id} | Detail: address, commit, timestamps, plus tabs for live logs (SSE) and that environment's history. |
/ui/projects | Registered projects, their live branches and their detected stack tag. |
/ui/projects/{id} | Settings (idle/lifetime policy, private-repo git token), a deploy form, per-branch environments, and for a monorepo the list of detected services with the active one marked. |
/ui/secrets | Global and project-scoped secrets. Write-only — values are never shown back. |
/ui/queue | Deploys waiting for host capacity, oldest and highest-priority first. |
/ui/audit | Cross-project audit trail with search and deploy durations. |
/ui/diagnostics | The daemon's own health: Docker capacity, network and Traefik wiring, what is missing and how to fix it. |
/ui/admin | Backup download, master-key rotation, named API tokens. |
First five minutes — the wizard
The first visit redirects to /ui/onboarding. Every step has a CLI
equivalent; the wizard exists so a first install does not require reading these docs.
| # | Step | What it does | Behind the scenes |
|---|---|---|---|
| 1 | Token | Paste the OXID_API_TOKEN — the installer printed it, or the daemon can hand it over directly depending on OXID_BOOTSTRAP_TOKEN_ACCESS. | GET /api/v1/setup/token, verified with GET /api/v1/stats |
| 2 | Infrastructure | Checks the Docker network, Traefik and the wake catch-all router. One click fixes whatever is missing. | GET /api/v1/infra/status → POST /api/v1/infra/bootstrap (idempotent) |
| 3 | First project | Register by Git URL (https://… or scp-style git@host:org/repo.git; private repos take an encrypted PAT), then deploy main with live polling. | POST /api/v1/projects → POST /api/v1/projects/{id}/deploy |
| 4 | Webhooks | Pick a provider and copy the URL and secret into your Git host. | GET /api/v1/setup/webhook-secret (master token only) |
| 5 | CLI | A copy-paste oxid context add line and a curl snippet for programmatic registration. | — |
Direct-publish mode (no OXID_DOCKER_NETWORK) reports “nothing to
bootstrap” at step 2. That is a valid topology — each environment gets its own
host port and no DNS is needed — with scale-to-zero off by design.
Installable, and it works offline
The panel is a PWA: manifest.webmanifest, a service worker and two SVG
icons are served from the same binary. Installed, it opens like an app; on a phone
with no connection it still opens.
What the service worker caches is the shell — HTML, CSS, JS, icons. It
never caches /api/. A cached environment list is a lie
about live cluster state, and a lie about whether something is running is worse than
an error, so API requests go to the network and fail honestly when it is not there.
Tests assert that exclusion rather than trusting the comment.
| Asset | Size |
|---|---|
index.html | 57.7 KB |
app.js | 54.5 KB |
vendor/alpine.min.js | 53.2 KB |
i18n.js | 46.2 KB |
style.css | 24.0 KB |
| Whole shell | 240 KB uncompressed |
Service-worker registration is best-effort: it needs a secure context, which a daemon on a plain-HTTP LAN address does not have. The panel works either way; only offline opening depends on it.
Small screens, coarse pointers, weak hardware
The layout is fluid from 320px to ultrawide, and verified there rather than assumed.
- Below 760px the tables become cards. Each
<td>carries adata-labelholding its column header, so the labels have exactly one source and cannot drift from the table. - The nav becomes a thumb-scrollable strip instead of stacking into a wall of links.
@media (pointer: coarse)raises controls to a 44px target. A checkbox needs a wrappinglabel.checkfor this, since padding does not enlarge a replaced element.- No webfont, no icon font, no CDN. Nothing to download and nothing to fail on a bad connection.
Language
English and Spanish. The switcher re-renders every binding without a reload, because
t() reads the active locale on each call. Language is chosen as:
previous choice (localStorage), else navigator.languages,
else English.
Three things are deliberately not translated, each for a reason worth
keeping: --json output and API field names (scripts parse them), log
lines (aggregators match on their text), and anything wrapping a
git2/bollard/sqlx error — those strings
come from those libraries and are what an operator searches for.
Catalogs are guarded by tests that fail on a missing key, a dropped placeholder, an invented one, or a key the UI asks for that no catalog defines.
Authentication
If the daemon has a bearer token, paste it into the token field in the top bar. It is
kept in the browser's local storage and sent as
Authorization: Bearer … on every request — it is never written to
the daemon, and clearing site data removes it.
A project-scoped token works here too: other projects' routes answer 404 and node-wide pages (admin, diagnostics) answer 403, so the panel shows a scoped operator only their own projects. See token scoping.