Skip to Content
ApplicationsOrbit Webapp

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

ConcernChoice
FrameworkNext.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 renderingnext-mdx-remote + remark-gfm — YAML prose fields and companion .md files are compiled to MDX server-side
Diagramsmermaid (for MDX-fenced content), d3 (programmatic React diagrams, e.g. the entity ERD graph), plus @xyflow/react / elkjs / @dagrejs/dagre
Code viewingmonaco-editor / @monaco-editor/react (the Code Explorer)
StylingOne global stylesheetsrc/app/globals.css (~6900 lines), organised by surface. No CSS modules, no styled-components; this is a deliberate design decision
YAMLyaml (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 via ORBIT_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. instance is a per-boot UUID on the agent and version a 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 to app/error.tsx.
  • /api/orbit-events — a route handler that proxies the agent’s SSE stream, filtered to solution.updated events. solution-refresh.tsx listens and calls router.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/tree and /sources/file endpoints, 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: full etc., from solution.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 to first / … / 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 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 via replaceState.
  • 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/replaceState are called only from user-intent functions, never from a state-keyed effect, and the popstate handler only dispatches state and never touches history — this prevents feedback loops.
  • Explicit stage state. stage is 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 Location of view + focusId + an opaque ctx bag 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), and useHistoryContext (custom capture/restore). ctx.caption is the one reserved field — the breadcrumb’s sub-state segment.
  • OrbitContext. useNavigation().context exposes 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 entryView / componentRenders
Solution homesolution-home.tsxThe landing page — solution-level rollups (open/preserved finding counts, hottest component, etc.)
Chartercharter-view.tsxcharter.md — the business overview
Glossaryglossary-view.tsxglossary.yaml — shared vocabulary

As-Is stage

Rail entryView / componentRenders (solution artefacts)
Sourcessources-view.tsxv1.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
Landscapelandscape-view.tsxstage/as-is/system-overview.md, landscape.md and companions — section selector + content
Domainsdomains-view.tsxdomains/{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
Workflowsworkflows-view.tsxstage/as-is/workflows/{slug}.md — filterable list + markdown detail
Entitiesentities-view.tsxstage/as-is/entities/ — card list + d3 ERD graph toggle, Details / Neighborhood detail tabs
Componentscomponents-view.tsxstage/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
Technologiestechnologies-view.tsxRoot technologies.yaml — per-stage technology guide
Findingsfindings-inbox.tsx / finding-detail.tsxstage/as-is/findings/{f-NNN-slug}/finding.yaml (+ finding.md Notes tab) — Severity / Category / Concerns facets, confidence badges
Assessmentsassessments-view.tsxv1.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
Concernsconcerns-inbox.tsxv1.7 per-instance negative outcomes: stage/as-is/concerns/{c-NNN-slug}/concern.yaml (+ concern.md Notes tab); rail badge counts open concerns only
Attestationsattestations-inbox.tsxv1.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
Questionsquestions-inbox.tsx / question-detail.tsxv1.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

EntryRenders
Code explorercode-explorer.tsx + Monaco — browses the solution’s source repositories via the agent’s /sources/* endpoints; evidence refs deep-link into it
Ask Orbit AgentChat affordance (no view of its own)
Memorymemory-view.tsx — the solution’s memory.md running log
SettingsPlaceholder affordance

Honesty-layer surfaces (v1.8)

  • Confidence badges (confidence-badge.tsx) — confidence: confirmed | inferred | gap on 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 means full).

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):

WhereFields
domain.yamldescription
component.yamldescription
Component sub-items (dependencies / integrations / stores)description, dependency notes, store schema-shape
auth.yamlpolicy / binding / guard / interceptor descriptions, absent-auth[].rationale, CORS / anonymous-endpoint / security / dev-permissions notes
permissions.yamlcatalogue, group, and permission description
entity.yamldescription
Touchpointstouchpoints[].description
finding.yamldescription, recommendation, rationale
technologies.yamlas-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 for summary.md, packaging.md, orientation bodies, etc.) starts a panel at each h2 and 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 a file: 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. :root holds the paper (light) tokens; :root[data-theme="dark"] is a token-only override. Zero per-component data-theme rules — if something looks wrong in one theme, fix token values, never fork component rules.
  • One accent. A single Prussian ink-blue (--accent: #2C4F73 paper / #93B0CC graphite) 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-title masthead 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.ts hook.