Orbit Webapp
The Orbit webapp is the read-only viewer — “the lens” — over a single Trajectory solution tree. It renders one solution at a time: its domains, workflows, components, entities, findings, assessments, concerns, attestations, sources, and questions, across the As-Is / R2B / To-Be stages.
Location: implementation/orbit-webapp/ (code in codebase/, images in
dockerfiles/).
The read-only contract
The webapp never mutates the solution. Authoring belongs exclusively to the Orbit Agent (“the hand”, the single writer). Since the 2026-07-02 refactor the webapp does not even mount the solution repository:
- It has no filesystem access to the customer or internal repo.
- It fetches the raw solution tree from the agent over HTTP and receives live-update notifications over SSE.
- All view state (navigation, theme, filters) lives in the browser and
localStorage; nothing is written back to the solution.
This split lets the reader scale horizontally (0–3 replicas on Azure, each
with its own render cache) while the agent stays a maxReplicas: 1 singleton
that owns the in-memory solution model.
Tech stack
| Concern | Choice |
|---|---|
| Framework | Next.js 16 (App Router) + React 19, TypeScript |
| Solution-tree loading | @maxq/trajectory-loader — the shared loader package (implementation/shared/trajectory-loader/, a file: dependency); since 2026-07-02 it runs inside the agent, the webapp consumes its RawSolutionTree output |
| Prose rendering | next-mdx-remote + remark-gfm — YAML prose fields and companion .md files are compiled to MDX server-side |
| Diagrams | mermaid (for MDX-fenced content), d3 (programmatic React diagrams, e.g. the entity ERD graph), plus @xyflow/react / elkjs / @dagrejs/dagre |
| Code viewing | monaco-editor / @monaco-editor/react (the Code Explorer) |
| Styling | One global stylesheet — src/app/globals.css (~6900 lines), organised by surface. No CSS modules, no styled-components; this is a deliberate design decision |
| YAML | yaml (parsing lives mostly in the shared loader) |
The codebase layout is documented in-repo at
implementation/orbit-webapp/codebase/docs/folder-structure.md: src/app/
holds the App Router entry points (page.tsx loads OrbitData server-side,
orbit-app.tsx is the client shell), src/components/orbit/ is a flat folder
of one file per surface, and src/lib/ holds the data types
(orbit-data.ts), the transformer (lib/solution/), the source-repository
client (lib/sources/), and the MDX pipeline (lib/mdx/).
Data access architecture
Summary only — the full contract (endpoints, ETag semantics, reload reasons, failure modes) is on the Data flow page.
Key pieces on the webapp side:
src/lib/solution/agent-source.ts— the server-only HTTP client for the agent. Configured viaORBIT_AGENT_URL(required); on Azure this is the agent’s in-env address (http://agent-<h>— the agent has internal ingress only since 2026-07-03, so no edge bypass header is involved). The browser never talks to the agent directly.getOrbitData()(src/lib/solution/index.ts) — one version check per request; the tree fetch → transform → MDX render pipeline runs only when the (instance, version) pair moved, coalesced behind a single in-flight promise.instanceis a per-boot UUID on the agent andversiona per-process monotonic counter, so cache identity is always the pair, never the version alone. If the agent is down and a cached model exists, the webapp serves stale with a warning; with no cache it throws toapp/error.tsx./api/orbit-events— a route handler that proxies the agent’s SSE stream, filtered tosolution.updatedevents.solution-refresh.tsxlistens and callsrouter.refresh()— an in-place RSC reconcile, so navigation state survives — and shows a “Solution updated” notice. The EventSource is recreated with backoff when it closes./api/sources/*— thin proxies to the agent’s/sources/treeand/sources/fileendpoints, feeding the Code Explorer.
There is deliberately no filesystem watcher on the agent side: after
out-of-band edits to the solution repo in local dev, trigger
POST /solution/reload on the agent. Task runs reload automatically.
Shell layout
The app shell never scrolls. It is composed of four fixed regions plus the page surface:
┌──────────────────────────────────────────────────────────────────┐
│ rail │ header │ never scrolls
│ ├───────────────────────────────────────────────────────────┤
│ │ breadcrumbs │ pinned
│ │ page title (cx-title: eyebrow + h1 + right-side meta) │ pinned
│ ├────────────┬──────────────────────────────────────────────┤
│ │ aside │ content — the ONLY scrolling column │
│ │ (pinned) │ │
│ ├────────────┴──────────────────────────────────────────────┤
│ │ agent strip │ never scrolls
└──────────────────────────────────────────────────────────────────┘- Rail (
rail.tsx) — the collapsible left navigation, grouped by stage (see the inventory below), with per-entry counts and severity dots. - Header (
header.tsx) — solution card, stage selector, search, role switcher, actions, the per-stage analysis-depth badge (depth: fulletc., fromsolution.yaml → analysis-depth, v1.8), and the theme toggle. - Breadcrumbs (
breadcrumbs.tsx) — a single shell-level breadcrumb that renders the active scope’s click trail (collapsing long trails tofirst / … / last 3). - Agent strip (
agent-strip.tsx) — the bottom strip showing agent activity.
Pages sit on the shared cx-surface-scroll shell: breadcrumbs, page title,
and the left aside stay pinned; exactly one column scrolls per page. The
right column comes in two chromes — bordered (1px border, rounded,
paper-bg panel; used when the column is a single panel with a pinned head and
scrolling body) and bare (cx-surface-scroll-bare; used when the column is
a list of self-bordered rows). Every scroll container opts into scroll
restoration via useRestoreScroll (see navigation below).
The two page patterns
The design contract for top-level pages lives with the code, in
implementation/orbit-webapp/codebase/docs/ (README.md,
folder-structure.md, and page-patterns/*.md). Those docs are the
contract: settled UI/behaviour decisions are recorded there, and any new
decision must update the doc in the same change. Two patterns cover every
surface:
Detail with tabs
For surfaces built around examining one entity at a time with rich,
multi-aspect content. The aside is a numbered list of all items; the right
column shows either an overview (KPI tiles, severity mix, leaderboards —
when nothing is selected) or a detail: a bordered scroll panel with a
tab bar (cp-aspectbar) and body. The first tab is always the atomic
“Details”; subsequent tabs are list aspects rendered with ListShell (a
320px master column + detail pane, each scrolling independently, with a
pinned identity head on the detail pane).
Used by: domains-view.tsx, components-view.tsx (a 10-tab version:
Details / Operations / Dependencies / Integrations / Stores / Entities /
Auth / Permissions / Packaging / Findings), and sources-view.tsx.
List with filters
For surfaces that are a flat collection of peers to scan, filter, and click through. The aside holds multi-select filter facets (with an explicit “All” clear per facet); the index right column is a bare scroller of self-bordered row cards, each with a status accent gutter on the leading edge; the detail flips to the bordered panel. In detail mode the aside can append a sibling list of items in the current filter.
Used by: workflows-view.tsx, findings-inbox.tsx + finding-detail.tsx,
concerns-inbox.tsx, questions-inbox.tsx + question-detail.tsx,
attestations-inbox.tsx, and entities-view.tsx (with a card/ERD-graph
view-mode toggle on the index and a 2-tab detail: Details / Neighborhood).
Navigation model
Navigation was refactored (2026-05-29) into a single controller under
src/components/orbit/navigation/ — no URL router; orbit-app.tsx is the
only holder of cross-page navigation state, driven by NavProvider +
useNavigation().
- Scope = stage × role. Three stages (
as-is,r2b,to-be) × three roles (analyst,builder,operator) = nine independent history stacks. Switching stage or role swaps stacks and restores that scope’s last location — unless you’re on a global view (home, charter, glossary, code, memory), in which case the target scope resets to its fresh root. - Click semantics. Rail click =
resetTo(truncate the scope to a single root). List or cross-entity click =navigate(push). Breadcrumb crumb =jumpTo(go to entry, truncate forward). Tab/aspect switches are deliberately not Back steps — they merge into the current entry viareplaceState. - Browser Back integration. Each history entry stores a tiny cursor
(scope + pointer + sequence) in
history.state; the authoritative stacks live in React state +localStorage(orbit.nav). The load-bearing invariant:pushState/replaceStateare called only from user-intent functions, never from a state-keyed effect, and thepopstatehandler only dispatches state and never touches history — this prevents feedback loops. - Explicit stage state.
stageis persisted state, not derived from the view. Navigating to a stage-bound view sets the stage; global views leave it untouched. - Page-owned history context. Each entry is a
Locationof view + focusId + an opaquectxbag the page owns. Going Back/forward/reload restores where you were inside the page — active tab, in-page sub-selection, filter sets, scroll offsets. Pages use small hooks:useCtxTab(persisted tab group + breadcrumb caption),useRestoreScroll(per-scroller offset, auto-namespaced by tab), anduseHistoryContext(custom capture/restore).ctx.captionis the one reserved field — the breadcrumb’s sub-state segment. OrbitContext.useNavigation().contextexposes a serializable snapshot — stage, role, scope key, current view/focus/tab/label/entity kind, and the trail — intended as the “where is the user right now?” hook for the embedded agent chat.
Page and rail inventory
The rail is grouped: a Solution group, a per-stage middle group, and a
bottom utility group. Every entry maps to a View literal in
components/orbit/types.ts and a render branch in orbit-app.tsx.
Above the Solution group sits a Mission Control link — it opens the
agent-webapp console via the same-origin /mission-control route, which
redirects to the per-solution URL from the runtime AGENT_WEBAPP_URL env var.
Solution group (stage-neutral)
| Rail entry | View / component | Renders |
|---|---|---|
| Solution home | solution-home.tsx | The landing page — solution-level rollups (open/preserved finding counts, hottest component, etc.) |
| Charter | charter-view.tsx | charter.md — the business overview |
| Glossary | glossary-view.tsx | glossary.yaml — shared vocabulary |
As-Is stage
| Rail entry | View / component | Renders (solution artefacts) |
|---|---|---|
| Sources | sources-view.tsx | v1.7.1 source-orientation: sources/{source-id}/orientation.md (its 11 ## sections render as panels), plus Notes / Subsystem-cards tabs when notes.md / subsystem-cards/*.md exist |
| Landscape | landscape-view.tsx | stage/as-is/system-overview.md, landscape.md and companions — section selector + content |
| Domains | domains-view.tsx | domains/{id}/domain.yaml + per-domain findings, touchpoints (stage/as-is/touchpoints/{domain-id}.yaml), and a Notes tab from stage/as-is/domain-notes/{id}/notes.md |
| Workflows | workflows-view.tsx | stage/as-is/workflows/{slug}.md — filterable list + markdown detail |
| Entities | entities-view.tsx | stage/as-is/entities/ — card list + d3 ERD graph toggle, Details / Neighborhood detail tabs |
| Components | components-view.tsx | stage/as-is/components/{id}/ — component.yaml, operations (incl. the operation.md Behaviour tab with mermaid sequence diagrams), stores, dependencies, integrations, auth.yaml, permissions.yaml, packaging.md, testing.md, per-component findings |
| Technologies | technologies-view.tsx | Root technologies.yaml — per-stage technology guide |
| Findings | findings-inbox.tsx / finding-detail.tsx | stage/as-is/findings/{f-NNN-slug}/finding.yaml (+ finding.md Notes tab) — Severity / Category / Concerns facets, confidence badges |
| Assessments | assessments-view.tsx | v1.7 registry rollup over assessment-types: runs at stage/as-is/assessments/{type-id}/{run-id}/ with a run-history strip, the run summary.md, and type-scoped Concerns / Attestations tabs |
| Concerns | concerns-inbox.tsx | v1.7 per-instance negative outcomes: stage/as-is/concerns/{c-NNN-slug}/concern.yaml (+ concern.md Notes tab); rail badge counts open concerns only |
| Attestations | attestations-inbox.tsx | v1.7 positive outcomes: stage/as-is/attestations/{a-NNN-slug}/attestation.yaml (+ attestation.md Notes tab), with a provenance run-timeline and clickable resolves-concerns links |
| Questions | questions-inbox.tsx / question-detail.tsx | v1.8 questions loop: stage/*/questions/{q-NNN-slug}/question.yaml (+ question.md Notes tab) — Status / Stage / Kind facets, default filter open; rail badge counts open questions |
R2B and To-Be stages
R2B renders stage-empty-view.tsx until real R2B data flows through the
loader. The To-Be group has: Design hub (to-be-view.tsx),
Capabilities (to-be-capability-view.tsx), Revisions
(to-be-revision-view.tsx), and Iterations (to-be-iteration-view.tsx),
fed by stage/to-be/ features/revisions/stories and iterations/. When
to-be is not-started, these render the empty-stage view — no To-Be data is
mocked.
Bottom group
| Entry | Renders |
|---|---|
| Code explorer | code-explorer.tsx + Monaco — browses the solution’s source repositories via the agent’s /sources/* endpoints; evidence refs deep-link into it |
| Ask Orbit Agent | Chat affordance (no view of its own) |
| Memory | memory-view.tsx — the solution’s memory.md running log |
| Settings | Placeholder affordance |
Honesty-layer surfaces (v1.8)
- Confidence badges (
confidence-badge.tsx) —confidence: confirmed | inferred | gapon findings/capabilities/revisions, plus per-field`<field>-confidence`/`<field>-source-ref`sidecars on component / operation / store / entity, surfaced in findings inbox and detail, component operation/store panels, entities, and to-be capability/revision headers. Absent means confirmed. - Analysis-depth badge — the header shows the active stage’s depth grade
(
essential | full | detailed; absent meansfull).
Finding enums (v1.9 alignment)
Finding status is draft | confirmed | addressed | dismissed and category
has nine values (security, correctness, availability, performance,
maintainability, compliance, operational, documentation, data-quality) —
mirrored from the schema in src/lib/orbit-data.ts (FindingStatus,
FINDING_CATEGORIES). The single definition of an open finding is the
isOpenFinding helper: negative polarity AND active status
(draft or confirmed). Positive-polarity findings count as “preserved” and
are excluded from open counts and severity rollups.
MDX rendering of YAML prose
Selected YAML string fields are compiled through the MDX pipeline
(src/lib/mdx/compile.ts) and rendered server-side via
MdxMarkdown — so they support real Markdown (paragraphs, lists, emphasis,
code, links, tables). The pattern is uniform: each prose field on the
UI-facing types carries a *Rendered: ReactNode companion, populated by
render passes in lib/solution/index.ts; views prefer the rendered node and
fall back to plain text.
Fields rendered as MDX include (authoritative list in
memory/mdx-rendering.md):
| Where | Fields |
|---|---|
domain.yaml | description |
component.yaml | description |
| Component sub-items (dependencies / integrations / stores) | description, dependency notes, store schema-shape |
auth.yaml | policy / binding / guard / interceptor descriptions, absent-auth[].rationale, CORS / anonymous-endpoint / security / dev-permissions notes |
permissions.yaml | catalogue, group, and permission description |
entity.yaml | description |
| Touchpoints | touchpoints[].description |
finding.yaml | description, recommendation, rationale |
technologies.yaml | as-is[].notes |
Whole companion .md files are also MDX-rendered: charter.md, workflow
markdown, packaging.md / testing.md, finding.md, domain notes.md,
operation operation.md, question.md, concern.md, attestation.md,
assessment-run summary.md, and source orientation.md / cards. Companion
tabs are hidden when the file is absent (2026-06-14 rule), and the chosen
tab is sticky across sibling switches with a content-aware fallback.
Two conventions worth knowing:
- Panel documents must use
##(H2) section headers. The section-panel plugin (active forsummary.md,packaging.md, orientation bodies, etc.) starts a panel at eachh2and silently drops everything before the first one — a doc authored only with#headings renders blank. - Inline
file:refs are clickable everywhere. Any inline code token that parses as afile:evidence ref (e.g.`file:src/Foo.cs:10-20`) renders as a button that jumps into the Code Explorer — in any rendered markdown.
Editorial design language
The webapp’s visual language (restyled 2026-06) is an editorial broadsheet: light-first warm cream paper as the default theme, with a warm-graphite dark companion. The contract, condensed:
- Token-only theming.
:rootholds the paper (light) tokens;:root[data-theme="dark"]is a token-only override. Zero per-componentdata-themerules — if something looks wrong in one theme, fix token values, never fork component rules. - One accent. A single Prussian ink-blue (
--accent:#2C4F73paper /#93B0CCgraphite) does all interactive work — links, active nav bars, focus rings, tab underlines, code refs. The rainbow brand stops survive only in the logo image and the agent-strip pulse. - Tint formula. Badges/pills/chips use
background: color-mix(in srgb, var(--TOKEN) 12%, transparent), border at 24%, text at the full token. Hardcoded black/white rgba washes are banned (use the--veil-*tokens). - Shape code. Severity badges are square (3px radius, “print stamps”); status pills are round. Square = severity, round = state — a deliberate, load-bearing distinction.
- Banned idioms. No glass (
backdrop-filter), no glow shadows, no hover translate/scale/brightness lifts, no gradients outside the logo and the agent-strip pulse. Elevation = hairlines + flat paper shadows; row hover = a paper tint + stronger border. - Typography. Serif display for detail titles and KPI numerals
(Instrument Serif); tabular numerals on mono counts; the
cx-titlemasthead carries a double rule (2px ink + 1px hairline). - The semantic palette is print-annotation: severity brick/sienna/ochre/stone,
status moss/steel/wine, stages ochre/plum/verdigris/steel — per-theme values
live only in the token blocks. Mermaid and Monaco follow the active theme
via a shared
use-document-theme.tshook.