Control stack
{{ control.project }}{{ controlError }}
| service | state | health | image |
|---|---|---|---|
| {{ s.name }} | {{ s.state || '—' }} | {{ s.health || '—' }} | {{ s.image || '—' }} |
loading…
Deployments
all →{{ listError }}
{{ summary.unresolved }} deployment(s) could not be resolved in this listing —
their spec needs ${VAR} values. Open one and set its variables.
“unknown” means the server could not determine busy or
running for that row, not that it is idle.
Recent jobs
all →{{ jobsError }}
- {{ j.state }} {{ j.action }} {{ j.stack }} {{ stamp(j.startedAt) }}
- no jobs yet — jobs are held in memory, so a server restart clears the history
Deployments
+ submit{{ listError }}
| id | kind | stack | state | updated |
|---|---|---|---|---|
| {{ d.id }} | {{ d.kind }} | {{ d.stack }} needs variables | busy running unknown idle | {{ stamp(d.updatedAt) }} |
| nothing submitted — use Submit to PUT a spec | ||||
Resolved spec
loading…
- stack{{ detail.stack || '—' }}
- kind {{ detail.kind }} {{ kindBlurb }}
- compose file {{ detail.compose ? detail.compose.file : '— (no compose section)' }}
- profiles {{ p }} none — a bare up starts every service
- overlays {{ o }} none
- axes{{ detail.axes.length }}
- requires{{ detail.requires.length }}
- created{{ stamp(detail.createdAt) }}
- updated{{ stamp(detail.updatedAt) }}
:id is the registry id (a directory name). stack is what the
spec resolved to — the compose project name, and $STACK in every hook.
Variables for this deployment
The env table below lists what the spec declares, which is not
the same as what it needs: stack: pr-${PR} consumes
PR without declaring it. So this editor is free-form, and the 400 message
on the resolve error is the authoritative list of what is missing.
Declared env (after interpolation)
unavailable until the spec resolves.
| key | value | visibility | length |
|---|---|---|---|
| {{ e.key }} | {{ e.value }} | {{ e.visibility }} | {{ e.length }} chars never set |
this spec declares no env: block |
|||
A masked value was redacted on the host because its name reads as a secret. The real value was never sent to this page, so there is nothing to reveal — only its length is shown. Read it on the host if you need it.
Axes (provisioned in order, destroyed in reverse)
unavailable until the spec resolves.
up provision · assert_live it exists ·
down destroy · assert_gone proven gone.
Hook bodies are never sent to this page — a hook is a shell string that
routinely carries a token inline.
| axis | hooks | teardown provable? |
|---|---|---|
| {{ a.name }} | {{ h }} | assert_gone unverifiable |
| no axes — a shared singleton has nothing to isolate and nothing to prove gone | ||
Requires (checked before anything is created)
unavailable until the spec resolves.
Preconditions run first, in order, and up stops at the first one that
fails — by name, before a single resource exists. The assert command
itself stays on the host; only the name and its hint are sent here.
| requirement | hint shown on failure |
|---|---|
| {{ r.name }} | {{ r.hint || '— (no hint authored)' }} |
| none declared — an isolated deployment that borrows shared infrastructure usually wants at least one | |
Compose logs
unavailable until the spec resolves.
This spec has no compose: section, so there are no compose logs to read.
Its axes may still have created resources — check those at their own source.
Fetched on demand, never polled: an auto-refresh against a chatty stack would hammer
the host through docker compose logs. tail is clamped to
1–2000 by the server.
{{ logs.text || '(no output)' }}
press Fetch to read the last {{ tail }} lines.
Redacted on the host before sending: credentials in URLs, NAME=value pairs
whose name reads as a secret, and this server's own token.
Lifecycle
Actions are unavailable until the spec resolves — the server cannot act on a deployment whose stack name it cannot compute. Fix the variables under config first.
A job is in flight for {{ detail.stack }}. One job per stack, deliberately not
queued — a down racing an up over the same database
branch is corruption, not contention.
Sent with {{ varsQuery || 'no variables' }}. change
Teardown will not check for leaks. A teardown that silently half-worked is the exact
failure this tool exists to catch — run verify yourself afterwards.
Forget this deployment
Unavailable until the spec resolves — the server counts this stack's containers before it will forget the record, and it cannot name the stack without its variables.
DELETE removes the record — the stored
spec.yml, compose.yml and meta.json.
It never tears anything down. The server refuses with 409 while any
container for this stack still exists, and refuses just as firmly when docker did not
answer: “cannot confirm” is not evidence of absence.
After forgetting, this deployment disappears from the control plane. If a resource an axis created is still alive, nothing here knows how to remove it any more.
Submit or replace a deployment
PUT /api/deployments/:id. The spec is parsed before anything
touches disk, so a rejected submission leaves nothing behind — and on a replace,
a good record is never destroyed over a typo. kind is read from the spec,
never from this form.
Only spec.yml and compose.yml ever live in a deployment
directory, and hooks run from there — so a hook must be inline shell or an
absolute path. up: ./hooks/db.sh cannot work here.
env (variables used to validate the spec)
These validate the spec now; they are not stored with it. On success they are
saved as this deployment's variables in this browser, so the later
up/down from this page use the same ones.
{{ submitError }}
Jobs
Held in memory, most recent first, capped at the last 50. A server restart clears the history — a job record is the transcript of an attempt, not the truth about what exists. That truth lives in docker and in each axis's own probe.
{{ jobsError }}
| state | action | stack | started | took | steps |
|---|---|---|---|---|---|
| {{ j.state }} | {{ j.action }} | {{ j.stack }} | {{ stamp(j.startedAt) }} | {{ took(j) }} | {{ j.outcome ? j.outcome.steps.length : '—' }} |
| no jobs recorded | |||||
- action{{ job.action }}
- stack{{ job.stack }}
- started{{ stamp(job.startedAt) }}
- ended{{ job.endedAt ? stamp(job.endedAt) : '— still running' }}
- took{{ took(job) }}
Log
{{ hhmmss(l.at) }} {{ l.message }} no log lines
The live stream dropped without a completion frame (a restart, a proxy timeout, or a job longer than the server's 240s idle window). The job record above was re-fetched, so the state is current even though the tail of the log may be missing.
Steps
| axis / requirement | phase | result | |
|---|---|---|---|
| {{ stepMark(s) }} | {{ s.axis }} | {{ s.phase }} | {{ stepText(s) }} |
✓ ok ·
✗ failed ·
! leaked — survived teardown ·
? unverifiable — nothing to check, not a pass.
A down step marked ok may still carry a non-fatal: note:
teardown is best-effort by design, so aborting halfway would leave more behind than
continuing.
This job produced no step results — it failed before the first step ran.
loading…
No such view
#/{{ route.raw }} does not match any view in
this UI. Every non-/api/ path serves this same page, so a deep link into a
path that does not exist here lands you here rather than on a 404.