Skip to content

`majordomus ui`

`majordomus ui`, the primary surface: how it differs from the Cockpit, where every value on it comes from and how a missing one reads, how the first screen stays fast with slow panels on routes of their own, the page without JavaScript, the assets and gates that hold it, and how a screen is added

Rendered from docs/MAJORDOMUS_UI.md — the same Markdown GitHub shows.

How the delivery is shown to a person: what the UI is, how it relates to the Cockpit, where every value on it comes from, how it stays fast and current, and how a screen is added. It describes behaviour as implemented and tested; where the two disagree, this document is wrong and changes in the same commit as the fix.

  • The decision is ADR 0135.
  • The rule the UI is held to is project.ui-derived-state.
  • The feature record is .ai/repo/features/ui.md (stable since 2026-10-12, when the owner accepted ADR 0135).
  • The claim a reader can check is ui-dashboard-cites-its-sources (docs/claims/ui-dashboard-cites-its-sources.md), proven by apps/majordomus-cli/tests/ui_surface.rs.
  • The owner's brief, with its reference screens, is campaigns/majordomus-ui-complete-opus-fable-v3-20261011/.

The product and its technical reference

The shared server of a checkout serves two surfaces a person reads.

SurfaceMountWhat it is
The UI/uithe product: the delivery in the project's own words, dense, linked and clickable
The Cockpit/cockpitits technical reference: the registry rendered, one page per capability, object, graph and route

The UI is where a person is sent first, and it is not a view of the registry: its navigation is its screens, its words are the delivery's. It refers to the Cockpit only as the technical view of what a screen read (ADR 0135), and in the frame:

  • the "technical view" link on each source line, to the Cockpit's page for that capability;
  • the technical view of an object a screen names and the UI has no screen of its own for (a document, a test, a declaring file, a feature's or a deployment's record), to the Cockpit's page for that object;
  • the Cockpit entry among the other surfaces in the tool bar, and the tool bar's search, which is the Cockpit's search (/cockpit/search) until the UI has a search of its own.

Both surfaces are projections (ADR 0012): they hold no data, they compose capabilities through the executor every transport calls, and the same process serves them under one content-security policy. The UI is a native surface of the web topology (web.surfaces lists it), declared beside the Cockpit in apps/majordomus-cli/src/web/discover.rs.

The screens

ScreenRouteReads
Dashboard/uirepository.info, health.report, release.version, distribution.releases, plan.status, plan.roadmap, mesh.peers; the queue on its own route
Command/ui/commandcommands.list (its origin filter is the screen's tabs), executions.list; capabilities.list for the capability a command is
Intent workspace/ui/iddintents.list, plan.roadmap, plan.issues, plan.waves, plan.next: one intent (?intent=, else the first intents.list reports executing, then verifying, planned, declared — the rule is printed) with its inspector, the milestones it names in the roadmap's order with what each waits for, every issue of those milestones or naming the intent by wave with its status, priority, blockers and the move its status allows (on the issue's Cockpit page, where plan.transition makes it), the waves those issues occupy with the scope conflicts that serialise them, and their dependency graph over a table of the same edges. The screen decides no status, wave or membership of its own
Roadmap/ui/roadmapplan.roadmap, plan.status, intents.list; one milestone (?milestone=) in the inspector, with plan.issues: its issues, what can start, its critical path and its neighbours; one intent (?intent=) in its inspector: its statement, invariants and criteria with their evidence, and the milestones and issues that serve it
Issues & PRs/ui/workplan.status, plan.issues; the queue on its own route, one pull request (?pr=) in its detail
Issues/ui/issuesplan.issues, plan.status, plan.roadmap: every issue, narrowed by ?status= (the plan's own statuses as tabs), ?milestone=, ?priority= and ?q=, each leading to its inspector
PR Drain/ui/prsplan.status, plan.issues (one issue, ?issue=); integration.queue and integration.events on its own route (/ui/panels/drain), narrowed by ?disposition=, ?lane= and ?q=, one pull request (?pr=) in the inspector with what the executor did to it, the lane's recent activity, and its merges per day over the fortnight the trail ends on
Capabilities/ui/capabilitiesrepository.info, capabilities.list by kind; one capability (?id=) from capabilities.describe
Mesh/ui/meshmesh.peers, mesh.events, mesh.state (live claims); one machine (?node=) in the inspector, and who holds each seat of rule project.the-cooperation-model-is-the-owners-word read from the claims
CI/CD & Releases/ui/deliverydistribution.releases, release.version, deploy.list, gates.model, integration.timings (each check's median, 90th percentile and longest run, and the open heads that waited longest, each leading to its pull request)
Deployments/ui/deploymentsdeploy.list (what the layer declares); deploy.observed (what the forge recorded: where each environment stands, every deployment to every environment — GitHub Pages among them — and every release, each commit, ref, environment, run and release a link); delivery.report on its own route
Evidence/ui/evidenceevidence.report; one claim (?claim=) in its inspector from evidence.claim: what settles it, its last run, what changed since, how to reproduce it, and the claims its test also proves
Knowledge/ui/knowledgeknowledge_base.status, knowledge_base.candidates: the candidates awaiting review, newest first and bounded with what is left out counted, the subjects they are about, and whether the deriver is still writing. The review's commands are named as text; the screen promotes and rejects nothing
Documentation/ui/docsobjects.list (features, documents), web.surfaces
Governance/ui/governancerules.report, each rule named and leading to its inspector; one rule (?rule=) from rules.show: what it states, how it is enforced, the tests that prove it, what is missing, and the rules around it
Observatory/ui/observatory, and /ui/kiosk as its presetmesh.peers, mesh.events, mesh.state, plan.roadmap, distribution.releases, release.version, health.report, economics.runs, repository.info, rules.report, evidence.report, quality.report, plan.waves, distribution.status, capabilities.list: fifteen scenes, each drawn from what its sources answered or named as unavailable with the source's reason (The Observatory)

The navigation is SCREENS in apps/majordomus-cli/src/ui/mod.rs and nothing else. A test compares it with the dispatcher's arms, so a screen without a route, or a route without a screen, fails. A screen of the owner's concept that has no producer yet is not in the navigation: a link to nothing is a dead end.

Every screen has the same shape:

  • a hero band, with the screen's glyph, title, tagline and lead, the brand mark lit behind them, and a row of KPI tiles;
  • a grid of panels;
  • the frame around both: at the top the site's navbar (below), beneath it the sidebar of screens with the version card at its foot, and the tool bar with the search, the time the screen was read, and the other surfaces (documentation, API, Cockpit).

The navbar comes from the site

majordomus.dev is the reference for how the product looks, and the bar at the top of every screen — and of every page of the Cockpit — is the site's own navbar (project.the-ui-wears-the-sites-navbar), not a bar drawn after it: the brand and the version, the site's four questions as menus, the search, the theme toggle, the product, GitHub and the call to action.

flowchart LR
  nav["site/data/nav.toml<br>the declaration"] --> partial["site/templates/partials/navbar.html<br>the one renderer"]
  partial --> built["site/public/404.html<br>the built bar"]
  built -->|"scripts/ui navbar --write"| copy["apps/majordomus-cli/src/ui/navbar.html<br>compiled in"]
  copy --> shell["ui/shell.rs<br>every screen"]
  copy -->|"@source"| css["share/ui/ui.css<br>the same utilities"]
  sitejs["site/nav.js, site/theme.js"] -->|"scripts/cockpit-assets"| uijs["share/ui/nav.js, theme.js"]
WhatWhere it comes from
the markupthe site's partial over site/data/nav.toml, cut out of the built site by scripts/ui navbar --write into apps/majordomus-cli/src/ui/navbar.html and compiled in (ui/navbar.rs)
the stylesthe utilities that file names, compiled into share/ui/ui.css and share/cockpit/cockpit.css from the same Tailwind, Flowbite theme and design sheets as the site's app.css
the menus and the theme togglethe site's nav.js and theme.js, served from share/ui/ and share/cockpit/ as byte copies scripts/cockpit-assets writes and --check holds; the theme contract is on <html>, where the site's script reads it
the search buttonthe surface's: on the UI it, / and Ctrl/Cmd+K lead to the tool bar's search; on the Cockpit it opens the palette
the version in the badgethe executable's, where the site names the release it offers
the product entrythe UI itself, where the site leads to its published snapshot
every other linkthe site, at its own address

To change the bar, change the site — the declaration or the partial — then rebuild and let the copy follow: scripts/site-build && scripts/ui navbar --write && scripts/cockpit-assets. scripts/ui navbar, which scripts/ui audit runs first in the ui job, refuses a copy that is not the built bar, and then compares the bar a browser draws on the site's front page with the ones it draws on /ui and /cockpit: every element, its box and its computed styles, in both themes, at every width the design declares, with the bar closed and with a menu open.

KPI tiles lead to the screen that explains them. A panel whose whole is on another screen says "View all". A milestone row leads to that milestone on the Roadmap, where the row it lands on is lit. A capability row opens its detail (?id=), a pull request of the queue opens its detail (?pr=), and a machine of the mesh — in its table or as a node of its graph — opens its inspector (?node=); every selection is a URL, so it survives a reload and the browser's Back.

Every entity a screen names leads somewhere (I2481):

What is namedWhere it leads
an issue, a pull request, a milestone, an intent, a claim, a rule, a capability, a machineits inspector in the UI
a gateits row in the CI model, /ui/delivery#gate-<id>; the gates it implies lead to theirs
a deploymentits row on Deployments, /ui/deployments#deployment-<id>
a deployed commit, the ref it was deployed from, an environment, the run that deployed it, a published releasethe forge's page for it, built from the repository address the forge itself gave when it was observed (deploy.observed)
an issue or a milestone that a milestone's own prose namesits inspector; the prose is the record's, drawn with its markup (ui/prose.rs)
a command the registry also serves as a capabilitythat capability's inspector; the two are one when the capability's declared command-line path is the command's invocation
a releasethe record the distribution publishes for it, at the address distribution.releases gives
a document, a test, a feature, a rule's or a deployment's declarationits technical view, the Cockpit's page for that object
the public address the delivery report probedthat address

A link to a technical view is drawn only when the index holds the object: the source said so (a rule's own URI, a test the proof found on disk), or objects.get resolves it. What the index does not hold is text, so no link leads to a page that answers 404. Two kinds of name stay text because nothing derives where they lead: a commit outside the Deployments screen, since no capability the other screens ask at first paint states the forge's address (the PR Drain, which asks the queue, and Deployments, which asks what was observed, do link the forge), and a path that is not an object of the index.

What the forge recorded is read from one observation, taken by majordomus served forge and kept beside the pull-request observation, which majordomus prs refresh takes. A checkout that has never asked says so, with that command, instead of drawing empty tables; the published snapshot's job asks before it reads, so the public Deployments and PR Drain are not empty.

A list a screen cuts short says so: "The first 8 of 31; 23 more not shown here", with a link to the screen that shows them all when there is one. Case 1091 and apps/majordomus-cli/tests/ui_links.rs read every screen through a real server and follow every link under /ui and every technical view it draws; one that does not answer fails them.

A pull request's detail is read from the queue's own answer, so it costs no second call: what the pull request is, every gate with its verdict, the evidence each verdict was read from, why it stands where it stands, the next action that would move it, the paths it authors and the factors of its rank. A number the queue did not observe is said to be unobserved.

Where a value comes from

Every value is a reading: the value at an RFC 6901 pointer into one capability's answer (apps/majordomus-cli/src/ui/source.rs). A tile shows its capability and pointer under its value. A panel closes with its sources. Each source is named with how long it took, and with every other way to ask the same question, as the capability declares them through capabilities.describe:

  • API: the route, linked, so a browser opens the same answer as JSON;
  • CLI: the command a person types, majordomus <path>;
  • MCP: the tool an agent calls;
  • technical view: the capability's page in the Cockpit.

Nothing is computed on the UI's behalf:

  • A list it shows is a list a capability returned.
  • A count is the number at a pointer, or the length of the list there.
  • A meter is a picture of the two numbers written beside it.
  • A donut draws the counts its legend lists.
  • A status word is the source's own word, styled by the design system's status vocabulary.
  • A window is the source's own. The merge tiles are labelled with the queue's throughput/window_days, and the PR Drain's throughput chart counts the trail's merges per day over that same window; when the queue states no window, the tile says so and no day is counted. The chart's bars are the one thing a screen tallies itself, each from the trail's own merge_succeeded events of that day, with the count written beside the bar.

Classes that overlap, such as a rule report's coverage, are listed and never summed. What is missing says so:

The sourceThe UI shows
answered, and the pointer holds a valuethe value (a fraction rounded to two places)
answered, and the pointer holds nothingnone
answered a row without its status wordunknown
could not answernot answered, with the reason

None of them is ever drawn as zero or healthy. The ui-integrity and cockpit-projection gates hold the UI's sources exactly as they hold the Cockpit's: scripts/ci/cockpit-projection-check scans both directories. Cases 389 and 547 prove it refuses, in the UI, a read of the index or the registry and an absent count drawn as zero.

Fast, and current

A screen does not wait for its sources (ADR 0142, rule project.no-screen-waits-for-a-source). It asks for the latest reading of each one (Context::read, capability::readings): what the process knows now, handed over from memory. Handlers, git and files are touched by a worker behind the reader, never on the path of the request.

The source wasWhat the reader getsWhat happens behind them
read in the repository as it is nowthe reading, at onceread again once it is older than a second
read before the repository last changedthe new reading if it lands within 50 ms, otherwise the old one, marked staleread again, with every other source read in the last five minutes
never read in this processthe first reading, waited fornothing more; past a minute it is said to be pending

The repository's own reload is not waited for either. When the repository has changed, a screen is drawn from the generation the server has, opens with "The repository changed: this screen shows it as it was, and is being read again", and the next generation loads behind it. An answer over the API, MCP or the command line still waits for the generation its caller saw.

A reading says how it stands on its source line. A current one older than five seconds says its age ("read 12 s ago"). A stale one says "read 12 s ago, before the repository changed; reading again", and its line carries data-catching-up. ui.js reads a screen with such a line again after 0.4 s, then after twice that, up to 6.4 s, until none is left. A stale value is never drawn as current.

The first reader of the UI starts one pass over every screen and panel in the background, so the screen opened next has its sources read before it is asked for. The pass walks SCREENS and PANELS; no other list of sources exists.

An answer is not a reading. execute, and so the API, MCP and the command line, still run the handler or return exactly what it would return. Two switches settle a doubt:

  • MAJORDOMUS_CAPABILITY_CACHE=off makes every reading run its handler and wait for it.
  • MAJORDOMUS_READING_PATIENCE_MS=wait keeps the readings and makes every read wait for a current one. A number sets the 50 ms.

Sources that have no reading yet are asked at once, each on a thread of its own, so a first paint costs its slowest source rather than the sum of them (source::ask_all).

A source too slow for a first paint is a panel with a route of its own. The pull-request queue (integration.queue) is /ui/panels/pull-requests:

  • with ?fragment the route answers the panel alone, and ui.js puts it in place once the screen has painted;
  • without JavaScript the placeholder is a link to the same route.

While a screen is visible, ui.js asks the server for it again every minute, puts the new rendering in place, and updates the time in the tool bar. That time is when the page was rendered; each source line says when its own reading is older. This is a refresh and is said as one; nothing claims a live stream. A streamed page and one stream of changed readings replace it in I2487.

How fast is measured, not written here. majordomus bench derives a target from every screen in SCREENS and every panel in PANELS, cold and warm:

  • system.http.ui.<route> reads the page over a pinned context.
  • system.http.live.ui.<route> reads a screen over the live view that serve answers from, so it pays the per-request git stamp. system.http.live.index measures that stamp alone, on /.
  • A panel is measured only by --profile full, because its source is the reason it is a panel.

The targets are listed in generated/benchmarks.md.

A screen over its budget fails the build (I2489). The benchmark policy, .ai/repo/benchmarks/rust/policy.yaml, declares that a warm screen answers within 100 ms at p95, for both views. majordomus bench --check holds every budget with or without a baseline and on every host, prints each bounded screen with its value and its bound, and exits 10 when one is over. The ui job of CI runs it before the browser audit:

majordomus bench system.http. --transport system --profile ci --no-write --check

A regression threshold could not do this: a screen that was always slow never regresses, and a platform with no baseline compares nothing. The first run of the check found /ui/command at 400-660 ms, where every other screen read 1-21 ms. The first request after the repository moved is not measured by the bench yet (I2485).

Relationship graphs

A relationship the source records is drawn as a graph: the dependencies between milestones (laid out in their direction; a node opens that milestone's inspector), the open pull requests that depend on one another (a dependency already merged is not drawn; one the queue does not hold is a small node that leads to the forge), and the mesh's machines, the runtimes they run and the sessions those hold (laid out by force, sessions as small leaves labelled when selected). The server puts the nodes and edges on the page (view::graph). The vendored Cytoscape, loaded only by a screen that has a graph, draws them:

  • in the tones of the design system, read from the stylesheet;
  • with a layout that does not animate under reduced motion;
  • with nodes that lead where their row is.

The table of the same edges stays beneath the graph, and it is what a reader without JavaScript, or without the library, reads. Only nodes that take part in an edge are drawn.

The Observatory

The other screens are rows for a person who came to decide something. The Observatory is the same facts as a picture, for a display nobody is sitting at (ADR 0158). It is one screen and it adds no capability:

RouteWhat it is
/ui/observatorythe screen, inside the application's frame
/ui/kioskthe same screen, projection and script, filling the window over the frame, for a display left running
/ui/screensavera permanent redirect to /ui/kiosk, its better name
/ui/obsrvatorya permanent redirect to /ui/observatory

The scenes are one list, SCENES in apps/majordomus-cli/src/ui/observatory.rs. Each declares its label, the capabilities it requires, what its marks mean, the screen that shows the same facts as rows, how long it stays, and its character:

  • a semantic scene binds marks to records: a machine, an event, a milestone, a release;
  • an ambient scene is decoration, is labelled as decoration, and stands for no fact.
SceneLabelRequiresIts rows
meshMesh Cosmosmesh.peersMesh
activityEvent Rivermesh.eventsMesh
intentsIntent Nebulaplan.roadmapRoadmap
tokensToken Spectrumeconomics.runsEvidence
heartbeatSystem Pulsehealth.reportDashboard
deliveryDelivery Treedistribution.releasesCI/CD & Releases
coopCooperation Choreographymesh.stateMesh
proofProof Latticerules.reportGovernance
claimsClaim Ledgerevidence.reportEvidence
surfaceSurface Atlasquality.reportCapabilities
wavesWave Frontplan.wavesRoadmap
matrixRelease Matrixdistribution.statusCI/CD & Releases
registryRegistry Treemapcapabilities.listCapabilities
ladderMilestone Ladderplan.roadmapRoadmap
ambientAmbient Intelligencenothing: it is decorationDashboard

The table is a reading aid; the list in the module is what the page, the script and case 3011 read, and the page prints each scene's own requires.

The last seven (proof to ladder, ADR 0160) read what the repository proves and need no mesh, so they are drawn on the public site too. They are counted, never estimated: a mark is a rule, a kind of claim, a measured count, a wave or a check, and where a source reports more items than a display holds the scene groups them and says how many it drew. They are drawn with p5 in instance mode onto a surface of their own that the director composes onto its stage, from share/ui/observatory-kiosk.js; p5 is the pinned library scripts/cockpit-assets copies into share/ui/vendor/, is not committed, and is loaded only when one of these scenes is shown. A checkout built without it says "p5 is not built here" on the stage and keeps the same facts in the table below it.

The page carries its whole projection as one document, observatory/v1, in the data-observatory attribute of its section: when it was observed, the revision, every source asked with the state it answered in (answered or failed, with the reason and the time taken), and every scene with its model. The rule of the screen is in that document:

A scene's sourcesThe scene
answered, with something to drawavailable, with a model
answered with nothing to draw, or could not answerunavailable: no model, the source's own reason, and the script does not rotate to it

A scene is never drawn empty, never drawn with stand-in marks and never replaced by a picture of health. An event of the mesh is projected as its identity (stream and sequence), its kind, its node, its logical time and its wall time, and never as its body.

The status word says how the page was read:

  • LIVE: read again within 90 seconds. The script asks for its own page every 60 seconds, and between those readings follows the mesh journal from a cursor every 3 seconds through observatory.events, which answers only the events above the cursor, projected exactly as the scene projects them. An event delivered twice is one mark, the river holds at most 200, and both readers back off to 120 seconds when the server does not answer;
  • RESYNCED: for ten seconds after the cursor named a clock the journal never reached (another runtime's journal, or one that restarted); the page was read again at once;
  • STALE: not read again within the bound;
  • REPLAY: a scene of the mesh drawn from the repository's recording, with the time it was recorded;
  • SNAPSHOT: the public site, which reads nothing again and names its revision.

Without JavaScript the same projection is a table: one row per scene, with its state in words (available, decorative or unavailable), its meaning or its reason, what it requires and the link to its rows.

A reader's choice of a scene holds for five minutes, then the rotation resumes. New events of the journal interrupt the rotation for the river once the scene shown has had 12 seconds, and at most once every 120 seconds, so a busy journal cannot hold the display; a reader's choice is never interrupted. A scene whose renderer throws is quarantined for the rest of the display's life: the rotation skips it, the others go on, and the scene says QUARANTINED with what failed when a reader asks for it. Nothing on the screen starts work or writes a record.

The display's own state is readable for an inspector or an evidence run: the section's observatory property answers data() (the projection it is drawing now), quarantined() and trace() (the last 24 choices of the rotation with their reasons). The evidence of the gates is taken through it by scripts/lib/ui-observatory.mjs.

On the public site the process that takes the snapshot runs no mesh. There mesh, activity and coop draw the recording the repository carries (.ai/repo/observatory/recording.json, read by observatory.replay), and the status says REPLAY with the time it was recorded and not live. A scene the recording does not carry is unavailable and says why; a recording that cannot be read is a failed source, never a scene. tokens is unavailable while no run is recorded.

The recording is the answer of observatory.capture on a server that runs the mesh, saved as it came and committed. It names nothing by its identity: machines are machine 1, machine 2 in the order the mesh listed them, runtimes and sessions are ordinals, and an event keeps its kind, its logical clock and its time. Its manifest names the capabilities it was read from, the window of events it holds (first and last time and logical clock) and what was removed and why; observatory.replay refuses a recording with an event that lacks its kind, clock or time, naming the event. A recording is a moment; it is replaced by capturing again. What is still open is in What it does not do yet.

Without JavaScript

Every screen is complete HTML, and the menu of a narrow screen opens with a checkbox and its label. share/ui/ui.js is the only script of the UI's own: it pulls in slow panels, refreshes the screen, draws graphs, reveals the bar's search button and keeps the sidebar beneath the bar. The bar's menus and theme toggle run on the site's own two scripts. The one inline script is the theme statement the policy allows by its digest, the same one the Cockpit ships. A served test asserts that the UI and the Cockpit carry one identical policy.

Assets and design

The UI's files live in share/ui/.

FileHow it is madeCommitted
ui.csscompiled from share/ui/src/ui.css by scripts/cockpit-assets, beside the Cockpit's stylesheet; --check fails when either is staleyes
nav.js, theme.jscopied byte for byte from site/nav.js and site/theme.js by scripts/cockpit-assets; --check fails when either differsyes
favicon.svgwritten from the canonical mark by majordomus generateyes
vendor/cytoscape.min.jscopied from the pinned npm package by scripts/cockpit-assetsno

The ui class of the CI model (.ai/repo/ci/gates.yaml) selects the asset check, the probe, the design gate and the UI audit whenever a file under share/ui/ or apps/majordomus-cli/src/ui/ changes.

The stylesheet imports the tokens, the status vocabulary, the primitives and the kit from share/design/, where the site and the Cockpit import the same files. Every colour is a role, a status or a tone; none is a literal. The markup names what a thing is, never a utility class, because the stylesheet compiles only what is written in it — except the site's bar, which is the site's markup and is compiled from its copy. The icons are the kit's, declared once in site/data/kit/icons.toml and compiled into the crate by majordomus generate (apps/majordomus-cli/src/ui/icons.toml); a test holds the two equal.

Choices are addresses

What a reader picks on a screen is in its URL: a tab, a search, a row, a dependency, an issue. Every control is a link or a GET form to the same screen with the choice in its query, so a view can be bookmarked, shared and reloaded, the back button undoes a choice, and the screen needs no script to be used. A value a link carries is read as the source's own vocabulary or dropped: a disposition or lane is a word of the queue (lower case, digits, underscores), an issue is I and four digits, a pull request is a number; anything else is not echoed. On the PR Drain the choices travel to the deferred panel's route, so the slow part of the screen answers the same question as the rest of it.

Adding a screen

  1. Add a module under apps/majordomus-cli/src/ui/ that asks its sources with source::ask_all and composes view components from readings, never a value of its own.
  2. Add it to SCREENS in ui/mod.rs with its icon, and its arm to the dispatcher.
  3. A source too slow for a first paint becomes a panel route in PANELS, drawn as a placeholder with data-panel-src. Either list makes the route a benchmark target; nothing else is edited.
  4. Lead somewhere: a tile with kpi_to, a panel with panel_to, a row with a link.

On the public site: a snapshot

The UI is a projection a running process answers, and the public site is files. So the site carries the UI as a snapshot at /ui/ (ADR 0141): the UI's own answers, read once at the commit the site publishes, written down.

majordomus web report ui-snapshot --limit 20

It writes into target/web/ui-snapshot. --limit bounds the pages and --budget-seconds the time spent on the pages the screens link to; the command's own help states the defaults.

The command builds the same UI the server routes to, in-process, and asks it for every screen and panel, then for the pages those link to: one page per family in turn (every rule's inspector, every claim's, every issue's) until --limit pages are written or --budget-seconds have passed, whichever is first. The screens and panels are written whatever they cost. It renders nothing itself. What it changes is only what a file cannot do that a route can:

In the UIIn the snapshot
a link to another screen or inspectora relative link to that page's file; an inspector's query is a directory (governance/_rule/<id>/)
a link to a page past the bound, a filter of several fields, a form, the APIthe snapshot's page about the live UI, carrying the path the link meant
a link to a page of the Cockpit: the "technical view" of a source or of an objectthat page of the Cockpit's own snapshot at /cockpit/ when it holds it (ADR 0147), and the page about the live UI when it does not
a link to a surface a publication holds (/rustdoc, /openapi.json)unchanged
a link into the served documentation (/docs/…)the same page of the published site
the script reads the screen again every minute and pulls in slow panelsit does neither: the body carries data-snapshot
"Read at" in the tool baralso a notice on every page: the commit, the time, the executable, and a link to the live UI

A snapshot knows what the UI could read where it was taken. A Pages runner has no mesh, no recorded forge observation and no running execution, so those panels read none, unknown or not answered, never zero. A snapshot is not evidence of the live state of anything: for that, run majordomus serve in a checkout and open /ui.

Evidence is the exception. A Pages runner does have a record of the tests: the evidence artifact the validation of the same commit, or of its nearest ancestor, recorded. The snapshot job puts that run's ledger in place before the UI is read. A publication whose evidence screen would still show more claims as not run than the declared baseline is refused (scripts/pages published-evidence, rule project.published-evidence-is-what-ci-recorded).

The directory declares itself (surface.json) as the surface ui-snapshot, mounted at /ui, published-only, built from the commit it was read at. A process serves the UI at /ui and a publication holds the snapshot there, and majordomus web validate holds both without a collision. snapshot.json beside it lists every page written, every page left to the live UI, and what ended the reading (complete, limit or budget). test/cases/1092_the_ui_snapshot_holds_every_screen.sh and test/cases/1093_the_ui_snapshot_links_nothing_absent.sh hold it to this.

The Cockpit is published the same way, by the same exporter (ADR 0147, COCKPIT.md). It is written after the UI's snapshot: it reads first the Cockpit pages the UI's snapshot wanted to link to, and then leads those links to the pages it wrote, so a capability a screen cites is a page a reader can open. Each surface is read once. test/cases/2982_two_snapshots_link_what_the_other_holds.sh holds it.

The published snapshot is held to the trunk (project.the-published-ui-is-the-trunks). The publication model declares that a publication holds it (deploy.snapshots in .ai/repo/ci/pages.yaml), scripts/site-deploy refuses to publish a site without one read at HEAD, and scripts/pages verify-ui asks the public site for it after a deploy and inside the pages-live gate: the commit surface.json, the banner and snapshot.json name, and every page the record lists. A snapshot that stopped at its bound passes and the probe states what it left out; one that is missing, names an older commit or does not say what ended its reading fails. docs/GITHUB_PAGES_ARCHITECTURE.md has the details and test/cases/2983_the_published_ui_is_the_trunks.sh holds them.

The first screen leads only with figures the reader of a snapshot can decide. That process is no server, so health.report says unknown for the attached clients and the mesh runtime and therefore for the whole report. The Dashboard's health figure is the report's repository verdict instead: the worst of the dimensions that describe the repository and not the process serving it. The Health panel below it still lists every dimension with its own word. Where no mesh runtime is active the mesh figure reads Mesh active: no and counts no machines. The probe refuses a published first screen with an undecided figure, and test/cases/1092_the_ui_snapshot_holds_every_screen.sh holds the snapshot to the same.

What it does not do yet

  • majordomus ui as a command, which ensures the shared server and opens this surface, is I2484. Until it lands, open /ui on the address majordomus serve status prints.
  • Nothing on the UI starts work yet: launching a capability, an issue or a milestone through the executor and following its run live is the execution control plane of the V4 pack. An action on a mesh node is the same, behind the executor (V5); the inspector says so.
  • There is no Settings screen: no capability holds a reader's settings.
  • CI timings are what the open pull requests' latest runs took, measured from the recorded forge observation (integration.timings); there is no history of runs yet, so no trend is drawn.
  • Milestones carry no dates, so the roadmap is a sequence and a graph, never a calendar.
  • The Observatory's replay on the public site is as old as the last capture that was committed; nothing captures one on a schedule. Between readings of its page it follows the journal by asking for the events above its cursor; nothing is pushed to it. Its gates and the measurements behind them are in .ai/repo/benchmarks/observatory/manifest.json; a frame rate there is headless Chromium's on the machine it names, not a panel's.

Claims this document defines

Each links to its own page with implementation, test and where it is used.

Searches the titles and descriptions of every page this site publishes.