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 byapps/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.
| Surface | Mount | What it is |
|---|---|---|
| The UI | /ui | the product: the delivery in the project's own words, dense, linked and clickable |
| The Cockpit | /cockpit | its 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
| Screen | Route | Reads |
|---|---|---|
| Dashboard | /ui | repository.info, health.report, release.version, distribution.releases, plan.status, plan.roadmap, mesh.peers; the queue on its own route |
| Command | /ui/command | commands.list (its origin filter is the screen's tabs), executions.list; capabilities.list for the capability a command is |
| Intent workspace | /ui/idd | intents.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/roadmap | plan.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/work | plan.status, plan.issues; the queue on its own route, one pull request (?pr=) in its detail |
| Issues | /ui/issues | plan.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/prs | plan.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/capabilities | repository.info, capabilities.list by kind; one capability (?id=) from capabilities.describe |
| Mesh | /ui/mesh | mesh.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/delivery | distribution.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/deployments | deploy.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/evidence | evidence.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/knowledge | knowledge_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/docs | objects.list (features, documents), web.surfaces |
| Governance | /ui/governance | rules.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 preset | mesh.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"]
| What | Where it comes from |
|---|---|
| the markup | the 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 styles | the 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 toggle | the 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 button | the 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 badge | the executable's, where the site names the release it offers |
| the product entry | the UI itself, where the site leads to its published snapshot |
| every other link | the 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.
What a screen names is a link
Every entity a screen names leads somewhere (I2481):
| What is named | Where it leads |
|---|---|
| an issue, a pull request, a milestone, an intent, a claim, a rule, a capability, a machine | its inspector in the UI |
| a gate | its row in the CI model, /ui/delivery#gate-<id>; the gates it implies lead to theirs |
| a deployment | its 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 release | the 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 names | its inspector; the prose is the record's, drawn with its markup (ui/prose.rs) |
| a command the registry also serves as a capability | that capability's inspector; the two are one when the capability's declared command-line path is the command's invocation |
| a release | the 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 declaration | its technical view, the Cockpit's page for that object |
| the public address the delivery report probed | that 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 ownmerge_succeededevents 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 source | The UI shows |
|---|---|
| answered, and the pointer holds a value | the value (a fraction rounded to two places) |
| answered, and the pointer holds nothing | none |
| answered a row without its status word | unknown |
| could not answer | not 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 was | What the reader gets | What happens behind them |
|---|---|---|
| read in the repository as it is now | the reading, at once | read again once it is older than a second |
| read before the repository last changed | the new reading if it lands within 50 ms, otherwise the old one, marked stale | read again, with every other source read in the last five minutes |
| never read in this process | the first reading, waited for | nothing 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=offmakes every reading run its handler and wait for it.MAJORDOMUS_READING_PATIENCE_MS=waitkeeps 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
?fragmentthe route answers the panel alone, andui.jsputs 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 thatserveanswers from, so it pays the per-request git stamp.system.http.live.indexmeasures 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:
| Route | What it is |
|---|---|
/ui/observatory | the screen, inside the application's frame |
/ui/kiosk | the same screen, projection and script, filling the window over the frame, for a display left running |
/ui/screensaver | a permanent redirect to /ui/kiosk, its better name |
/ui/obsrvatory | a 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.
| Scene | Label | Requires | Its rows |
|---|---|---|---|
mesh | Mesh Cosmos | mesh.peers | Mesh |
activity | Event River | mesh.events | Mesh |
intents | Intent Nebula | plan.roadmap | Roadmap |
tokens | Token Spectrum | economics.runs | Evidence |
heartbeat | System Pulse | health.report | Dashboard |
delivery | Delivery Tree | distribution.releases | CI/CD & Releases |
coop | Cooperation Choreography | mesh.state | Mesh |
proof | Proof Lattice | rules.report | Governance |
claims | Claim Ledger | evidence.report | Evidence |
surface | Surface Atlas | quality.report | Capabilities |
waves | Wave Front | plan.waves | Roadmap |
matrix | Release Matrix | distribution.status | CI/CD & Releases |
registry | Registry Treemap | capabilities.list | Capabilities |
ladder | Milestone Ladder | plan.roadmap | Roadmap |
ambient | Ambient Intelligence | nothing: it is decoration | Dashboard |
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 sources | The scene |
|---|---|
| answered, with something to draw | available, with a model |
| answered with nothing to draw, or could not answer | unavailable: 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 throughobservatory.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/.
| File | How it is made | Committed |
|---|---|---|
ui.css | compiled from share/ui/src/ui.css by scripts/cockpit-assets, beside the Cockpit's stylesheet; --check fails when either is stale | yes |
nav.js, theme.js | copied byte for byte from site/nav.js and site/theme.js by scripts/cockpit-assets; --check fails when either differs | yes |
favicon.svg | written from the canonical mark by majordomus generate | yes |
vendor/cytoscape.min.js | copied from the pinned npm package by scripts/cockpit-assets | no |
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
- Add a module under
apps/majordomus-cli/src/ui/that asks its sources withsource::ask_alland composesviewcomponents from readings, never a value of its own. - Add it to
SCREENSinui/mod.rswith its icon, and its arm to the dispatcher. - A source too slow for a first paint becomes a panel route in
PANELS, drawn as a placeholder withdata-panel-src. Either list makes the route a benchmark target; nothing else is edited. - Lead somewhere: a tile with
kpi_to, a panel withpanel_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 UI | In the snapshot |
|---|---|
| a link to another screen or inspector | a 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 API | the 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 object | that 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 panels | it does neither: the body carries data-snapshot |
| "Read at" in the tool bar | also 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 uias a command, which ensures the shared server and opens this surface, is I2484. Until it lands, open/uion the addressmajordomus serve statusprints.- 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.