kind: Page
What it is
kind: Page declares a page: a named grid of typed, permissioned
panels that producers push data into. A page holds no query, no
datasource and no credentials — it renders the last payload something
pushed to it. That is the whole design: everything a page shows is
reachable because the producer already runs next to the data, inside a
crew container.
Each panel declares four things that carry the entire contract:
schema— what shape the payload has, from a closed set (metric.v1,status.v1,table.v1, …). A new panel kind is a server release, never a user-supplied string.owner—crew/<slug>, the permission anchor. A viewer who is not in that crew gets a sealed placeholder in the panel’s grid slot, filtered server-side, so the page has the same shape for everyone.producer—<kind>/<ref>, who is permitted to WRITE the panel. Producer authority is separate from viewer authority: a crew member who can see a panel cannot write it.sla— a duration. When nothing has arrived within it, the panel renders as stale. There is no default that means “never mind”; a panel without an SLA does not validate.
crewship page create --file takes. One shape, two doors: author a page in a manifest and
apply it, or post the identical YAML with the CLI. The Go type behind
spec.panels is the authoring type itself
(internal/pages.PanelSpec), so the two cannot drift.
YAML schema
metadata
spec.panels[]
Panel ORDER is the layout. The grid is declared, never dragged, so two
pages with the same panels in a different order are two different
pages, and reordering the list is a real change the plan will report.
With
tab: on the panels, that order is also the order of the bar.
A tab HIDES panels, which is why it is not only a layout choice. Each
tab carries the worst freshness state of its own panels — failed over
stale over never_produced over fresh — as a glyph beside its name,
and the page’s own freshness summary is computed over EVERY tab rather
than the visible one. A panel that stops reporting on a tab nobody is
looking at still says so. A tab whose panels are all sealed to a reader
still appears on that reader’s bar, carrying its placeholders: the page
has the same shape for everyone, and a bar that reflowed per viewer
would disclose, by what it left out, whose data was on it.
spec.panels[].actions[]
A
link never reaches the server; a toggle is client-side panel state;
a custom action resolves to a handler compiled into the web client and
takes fixed params only. Only a call is dispatchable, and dispatching
anything else is refused.
narrative.v1 panels may not declare actions in this release. A panel
that both renders agent-written prose and can trigger an operation is the
combination the security rules refuse.
spec.panels[].wake[]
Each gate compiles, at save time, to a row in
automations named
page <slug>/<panel> wake <n> (see
Automations). Those rows are DERIVED: the page
spec owns them, every save rewrites them, and deleting the page deletes
them. Editing one directly does not stick.
refresh compiles the same way, to a row named
page <slug>/<panel> refresh <value> whose action runs the panel’s own
producer routine — so a refresh inherits that table’s debounce, hourly
burst brake and loop pricing rather than getting a second eventing path.
Neither wake nor on_failure is echoed by the read path — nor is
actions, nor refresh — so, exactly like public, the plan cannot
diff any of them. All five are sent on every create and update, so a
change to a gate, a button or a trigger lands as soon as anything else
on the page changes; changing only one of them and nothing else reports
no drift.
Examples
A crew status board
script/watch-services.sh is a script inside the lookout crew’s
container; it pushes with crewship page set fleet-201/services --data -.
routine/error-rollup is a kind: Routine declared in the same
manifest — the apply orders the routine first (see
Apply behavior).
A cross-crew page
engineering but not in devops sees the build panel and
a sealed placeholder where incidents is — same grid, same two slots,
no payload and no producer name for the panel they may not see.
A page with tabs
Síť then Odezva, the order they first appear — under the
breadcrumb, one screen at a time instead of one long scroll. Adding a
third is one word on the panel that needs it. Deleting every tab: puts
the page back to a single grid with no bar.
CLI reference
The per-entity command iscrewship page, defined in
cmd/crewship/cmd_page.go. The manifest is the other door onto the
same endpoints.
REST endpoint mapping
Routes are workspace-unscoped; the workspace comes from the request context, followingsaved-views and missions.
Every panel key in the document reaches the wire — the only rename is
sla → sla_seconds. That matters more than it looks: PATCH replaces
spec.panels wholesale and reconciles the page’s automations rows
against the panel list it was sent, so a key the applier drops is not a
key the server ignores, it is a gate or a button the apply DELETES.
Endpoints:
GET /api/v1/pages— index (counts and freshness rollup, not panels)GET /api/v1/pages/{slug}— one page with panels + payloadsPOST /api/v1/pages— createPATCH /api/v1/pages/{slug}— updateDELETE /api/v1/pages/{slug}— deletePUT /api/v1/pages/{slug}/panels/{id}/data— the producer write pathGET /api/v1/pages/{slug}/panels/{id}/actions— what a panel offersPOST /api/v1/pages/{slug}/panels/{id}/actions/{actionId}— dispatch one; answers202with a pending id
Validation rules
Structural rules are enforced by the same validator the API and the CLI use, so a document that validates here validates everywhere:metadata.nameis required;metadata.slugmust be slug-shaped.- At least one panel, at most 24. A page with no panels renders nothing and can be pushed to by nobody.
- Panel
idmust be slug-shaped and unique within the page — a duplicate id means one of the two could never be pushed to. schemamust be a producible member of the closed set.ownermust parse ascrew/<slug>.producermust parse as<kind>/<ref>with a known kind.slamust parse as a duration and be greater than zero.spanmust be within 1–12.icon, when declared, must be a member of the closed set. Case is not folded and synonyms are not guessed:Memoryandramare both refused, because accepting either would teach a spelling that is not the vocabulary.tab, when declared, must be one readable line: not blank, no control characters, at most 32 characters, and at most 8 distinct tabs per page. Two names that differ only in case are refused —Odezvaandodezvaare two tabs a reader sees as one. Names are trimmed, never case-folded.- Action ids are slug-shaped and unique within the page;
kindis closed and an undeclared one is a refusal, not a warning. - A
callnames a routine slug; alinkcarries an entityrefand never anything URL-shaped; atoggletargets panels that exist on this page; acustomaction takes fixed params only. - An input’s name is
[a-z_][a-z0-9_]*, must not collide with a fixed param, and its type is one of the five —secretis refused. - A
narrative.v1panel declaring actions is refused.
- every panel’s
ownercrew must be declared in this manifest or already exist, producer: routine/<slug>must resolve to a declared or remote routine,producer: agent/<slug>must resolve to a declared or remote agent.- every
callaction’sroutinemust resolve to a declared or remote routine — a button that only resolves at click time is discovered mid-incident.
script/… and webhook/… producers are not checked. A script is a
path inside a crew container and a webhook token is minted after the
fact, so neither names anything the manifest models; silence on those is
correct rather than an omission.
Every failing reference in a document is reported at once, so a manifest
with three typos costs one apply --dry-run, not three.
Apply behavior
Pages are planned after crews, agents and routines, and torn down
before them, so a panel never points at a producer that does not exist
yet or has already gone.
crewship apply --dry-run reports the planned action per page and names
what drifted (update page "fleet-201" (name, panels)).
Renaming
metadata.slug is the page’s address. The server refuses a PATCH that
changes it — “a page’s slug is its address” — because a rename would
silently break every producer script pushing to it. Changing the slug in
a manifest therefore creates a SECOND page; delete the old one
deliberately if that is what you meant.
metadata.name renames freely.
Drift detection
The plan compares name, description and the full panel list (in order), and PATCHes the whole panel set when anything differs — the update endpoint replaces panels wholesale, so a partial list would delete the panels it left out. Two deliberate blind spots, both stated here rather than left to be discovered:publicis never compared. The read path does not serialise it, so the remote value always looks false. The declared value IS sent on create and update and does take effect; it simply cannot be verified from the manifest side, so a page whosepublicflag was flipped in the UI will not be reported as drifted.- A sealed panel is compared on
id,spanandtabonly. That is all a placeholder carries — the three fields that are the page’s SHAPE rather than the panel’s data, and the page has the same shape for everyone. If you apply a manifest containing a panel owned by a crew you are not in, drift in its schema, producer, title, icon or SLA is invisible — and treating “cannot see” as “must differ” would PATCH the page on every single apply, minting a version nobody asked for. Apply as a member of the owning crews if you need the full diff.
Round-trip via export
ExportPages reads the index, fetches each page, and emits one
kind: Page document per row with sla_seconds rendered back into the
duration string a human wrote (3600 → 1h).
Two notes:
- Export refuses a page containing a panel sealed to the exporting account, naming the panel. Emitting the document without it would produce YAML that silently deletes that panel the next time anyone applied it, and losing somebody’s panel is worse than failing loudly.
crewship exportdoes not yet call it. The export CLI knows onlyCrewandWorkspacetoday — the same is true of every per-kind exporter in the manifest layer — and the kinds path is scheduled work. The function ships written and tested so that Page is not the one kind missing from it on the day that lands.
See also
kind: Routine— referenced asproducer: routine/<slug>; routines are created before the pages that name them.kind: Agent— referenced asproducer: agent/<slug>.kind: Crew— every panel’sowneris a crew, and crew membership is what decides who sees the panel.kind: SavedView— the other read surface over workspace data. A saved view filters rows the app already has; a page renders whatever a producer pushed.