Skip to main content
Version: Next

0025: The web surface moves to React + TypeScript + Fluent UI 2, built at dev time and committed like the current UI

  • Status: Accepted (Navjyot, 2026-10-06; proposed as a spike 2026-09-29)
  • Date: 2026-09-29
  • Related: 0024 (the local-auth contract this change does not touch)

Context​

orion web's front end has been hand-built four times over on the same stack β€” vendored Preact 10.24.3 + htm 3.1.1, plain CSS custom properties, no build step β€” and each attempt at an enterprise-grade look has fallen short of what was asked for. The pattern is not a one-off miss: every corporate-feeling component (data table, tabs, drawer, command palette, theming, focus management, accessibility) had to be hand-built in raw CSS and class-component Preact, with no library backing any of it. Effort went into the attempt; the result did not close the gap.

Separately, OR-492's re-tune (2026-09-26 β†’ 2026-09-29) found the current UI already drifted from internal/ui's own vocabulary after the orion watch redesign (see [B9] OR-572) β€” a second data point that a hand-maintained parallel implementation keeps falling behind its source of truth.

Hard requirement carried into this decision, non-negotiable: orion web must keep starting exactly as it does today β€” one binary, one command, no Docker, no separate server process, no Node or npm needed to run it. Whatever frontend approach is chosen, this does not change.

Decision​

Rebuild the web surface on React 19 + TypeScript + Fluent UI 2 (@fluentui/react-components), built with Vite, with the build output committed to the repository and served the same way static/ is today.

  • New source tree: internal/web/ui-next/ β€” a normal Vite + React + TS app, npm-managed, with its own node_modules/ (gitignored) and package.json.
  • npm run build writes straight into internal/web/static/next/ (vite.config.ts's outDir), which is committed, not gitignored.
  • internal/web/assets_next.go embeds static/next via //go:embed, exactly mirroring assets.go's reasoning for why static/ itself is committed: a //go:embed pattern matching nothing is a build error, not an empty directory, so the compiled-in tree must exist in the repository before anyone has ever run the front-end build on that checkout.
  • Served at /next/, alongside the existing UI at /. Nothing about the current route, static/, or any existing handler changes.
  • Node and npm are needed only to edit the new UI (npm run dev, npm run build) β€” never to run orion web. go build ./... and orion web both work identically on a machine with no Node installed, because the compiled-in tree is what ships, not the source.

This was verified directly during the spike: go build ./... was run with PATH stripped of every Node/npm/nvm/homebrew entry and still produced a working orion binary that served both / and /next/ with live /api/snapshot data, in both themes, at 1512px and 390px, with zero console errors and zero overflowing elements.

Alternatives considered​

  • Stay on Preact + htm + plain CSS, invest more effort. Rejected: this is the fourth attempt on that stack, not the first, and the pattern is that hand-building each corporate-feeling primitive (data table, tabs, drawer, palette, theming, focus/a11y) from raw CSS does not converge on an enterprise look no matter how much CSS is written. The problem is the approach, not the effort spent on it.
  • IBM Carbon. Strong data-grid and dashboard components, genuinely enterprise/analytical in feel. Passed over in favour of Fluent UI 2 as the team's stated preference (2026-09-29); revisit if Fluent's component set proves insufficient for a later story (e.g. [S37] OR-537's data table).
  • Atlassian Design System. Familiar to Jira users, which fits this project's own tracker. Passed over: some packages are less open to use outside Atlassian's own products, and licensing needs checking before any commitment.
  • shadcn/ui + Tailwind. Modern and fully owned (components are copied into the repo, not a dependency), but reads as a SaaS product rather than an enterprise/IT-department one, and every component still needs individual polish β€” closer to the current hand-built problem than a clean escape from it.
  • A CSS-only vendored design system (no React), staying on Preact. Would keep the no-build-step property but still leaves every interactive component (tabs, command palette, focus trap, live regions) hand-wired in Preact class components with no framework support for any of it. Rejected for the same reason as "invest more effort" above β€” the primitives are the gap, not just the visual layer.

Consequences​

  • Node becomes a required tool for anyone editing internal/web/ui-next/. It was never required to run orion web, and still is not β€” this ADR does not change that. CI and any packaging job need Node only to rebuild static/next/ when the UI source changes, not to build or run the orion binary itself.
  • A stale committed build is now a real failure mode. If ui-next/src changes without static/next/ being rebuilt in the same commit, the binary keeps serving old JavaScript with no error β€” the same risk assets.go already carries for static/, and a CI check should assert git diff --exit-code internal/web/static/next after a clean npm run build, the same way a stale-build check would be added for the existing UI if one does not exist already.
  • The "hand-written source only" guard now has one exception. TestBuildHasNoFrontEndToolchainOutputInStatic rejects any tracked file under static/ with a line over 400 characters, to stop bundler output entering the old UI's directory. A minified React build is exactly that, so the test skips static/next/ and nothing else. Everything outside it is still checked. Accepting this ADR accepts that narrowing.
  • Two front ends coexist during the transition. / (Preact/htm) keeps working unmodified while /next/ (React/Fluent) is built out story by story under OR-492. The cutover point (retiring / and moving /next/ to /) is a separate decision, made once /next/ covers what / covers today β€” not implied by this ADR.
  • OR-492's stories need re-planning onto this stack once accepted. Roughly eight stories (themes, type scale, nav, command palette, the history data table, accessibility) shrink or fold into "use the library's version" rather than "hand-build it"; the data and watch-parity stories (OR-572–OR-576, S24–S28, S38–S41) are unaffected β€” they are server-side or data-shape work, not rendering.
  • The vendored Preact + htm runtime (internal/web/ui/vendor/, VENDOR.md) is not removed by this ADR. It stays in place as long as / is served from it. Its removal is a follow-on decision for whenever the cutover happens, not this one.
  • This does not touch 0024. /next/ is registered HandleReadOnly, GET/HEAD only, exactly like / and /vendor/ β€” no new write endpoint, no change to the token/Origin/Host middleware.

Status of this decision​

Proposed. OR-577's spike (Overview page only, against live /api/snapshot data, reviewed in both themes and at 390px/1512px) is built to let this be judged against a real page rather than a description. Accepting this ADR means re-planning OR-492's remaining stories onto this stack; rejecting it means reverting to the Preact/htm approach and closing OR-577 without adopting its output.