Skip to main content

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.
  • ownercrew/<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.
The manifest document is the same document 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

Anyone in 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

Two 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 is crewship 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, following saved-views and missions. Every panel key in the document reaches the wire — the only rename is slasla_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 + payloads
  • POST /api/v1/pages — create
  • PATCH /api/v1/pages/{slug} — update
  • DELETE /api/v1/pages/{slug} — delete
  • PUT /api/v1/pages/{slug}/panels/{id}/data — the producer write path
  • GET /api/v1/pages/{slug}/panels/{id}/actions — what a panel offers
  • POST /api/v1/pages/{slug}/panels/{id}/actions/{actionId} — dispatch one; answers 202 with 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.name is required; metadata.slug must 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 id must be slug-shaped and unique within the page — a duplicate id means one of the two could never be pushed to.
  • schema must be a producible member of the closed set.
  • owner must parse as crew/<slug>.
  • producer must parse as <kind>/<ref> with a known kind.
  • sla must parse as a duration and be greater than zero.
  • span must be within 1–12.
  • icon, when declared, must be a member of the closed set. Case is not folded and synonyms are not guessed: Memory and ram are 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 — Odezva and odezva are two tabs a reader sees as one. Names are trimmed, never case-folded.
  • Action ids are slug-shaped and unique within the page; kind is closed and an undeclared one is a refusal, not a warning.
  • A call names a routine slug; a link carries an entity ref and never anything URL-shaped; a toggle targets panels that exist on this page; a custom action 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 — secret is refused.
  • A narrative.v1 panel declaring actions is refused.
The manifest layer adds the checks only it can make, resolving each reference against the rest of the bundle plus what the server already has:
  • every panel’s owner crew 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 call action’s routine must 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:
  1. public is 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 whose public flag was flipped in the UI will not be reported as drifted.
  2. A sealed panel is compared on id, span and tab only. 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 (36001h). 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 export does not yet call it. The export CLI knows only Crew and Workspace today — 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 as producer: routine/<slug>; routines are created before the pages that name them.
  • kind: Agent — referenced as producer: agent/<slug>.
  • kind: Crew — every panel’s owner is 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.