Skip to content

feat(web): rebuild the dashboard with React and Fluent UI - #277

Open
Menci wants to merge 582 commits into
mainfrom
fluent-dashboard
Open

feat(web): rebuild the dashboard with React and Fluent UI#277
Menci wants to merge 582 commits into
mainfrom
fluent-dashboard

Conversation

@Menci

@Menci Menci commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Summary

  • rebuild the dashboard as a React Router SPA on Vite 8 with Fluent UI, English and Simplified Chinese localization, official Segoe UI Web fonts, responsive navigation, accessible interactions, and a consistent compact table/action system
  • make route transitions atomic: loaders resolve all route resources before URL and component-tree commit, with no content-level loading state after bootstrap
  • opt every scrollable region into OverlayScrollbars, eliminate document scrolling, and keep navigation, request inspection, and the two-pane upstream workspace geometrically stable
  • rebuild upstream/model editing, lazy Monaco YAML editing, aliases, proxies, API keys, request dumps, Playground, Usage, Performance, Users, Search, backup/restore, and API Docs on shared Fluent patterns
  • share chart callout tables across Usage, Search Usage, and Performance; preserve exact time-range boundaries, edge labels, compact points, and dual TTFT/output-speed performance details
  • cover the complete public data-plane surface in API Docs from the same method/path manifest used by gateway registration
  • make control-plane response contracts gateway-owned and projection-specific, including upstream records, usage views, dump pagination, API keys, backup validation, and generation-safe sessions

Verification

  • pnpm run lint
  • pnpm run typecheck
  • pnpm run test — 429 files, 4,781 tests passed
  • pnpm run test:agent-setup-installers — 105 passed, 4 skipped
  • pnpm --filter @floway-dev/web run build — includes the Monaco lazy-chunk assertion
  • headless Chrome at 1600×1000 and 390×844: 13 primary dashboard routes opened without ErrorBoundary, Vite overlay, body overflow, or document-level horizontal scrolling
  • headless interaction checks: route-progress restart, model edit/detail/flags switching, manual-model add/delete, Monaco YAML persistence, Azure endpoint retention, dialog save/exit stability, proxy validation isolation, request inspection, chart callouts, and exact chart range boundaries

Menci added 12 commits July 29, 2026 15:19
# Conflicts:
#	apps/web/__tests__/components/settings/SearchConfigSection_test.ts
#	apps/web/src/api/types.ts
#	apps/web/src/components/settings/ImportSection.vue
#	apps/web/src/components/settings/SearchConfigSection.vue
#	apps/web/src/pages/dashboard/settings.vue
#	packages/gateway/__tests__/control-plane/data-transfer/routes_test.ts
#	packages/gateway/__tests__/control-plane/search-config/routes_test.ts
#	packages/gateway/__tests__/control-plane/search-usage/routes_test.ts
#	packages/gateway/__tests__/data-plane/alpha-search/routes_test.ts
#	packages/gateway/__tests__/data-plane/chat/responses/interceptors/server-tool-shim_test.ts
#	packages/gateway/__tests__/data-plane/tools/web-search/config_test.ts
#	packages/gateway/__tests__/data-plane/tools/web-search/domain-normalize_test.ts
#	packages/gateway/__tests__/data-plane/tools/web-search/provider_test.ts
#	packages/gateway/__tests__/data-plane/tools/web-search/providers/microsoft-grounding_test.ts
#	packages/gateway/__tests__/data-plane/tools/web-search/providers/microsoft-web-iq_test.ts
#	packages/gateway/__tests__/data-plane/tools/web-search/providers/web-iq_test.ts
#	packages/gateway/__tests__/data-plane/tools/web-search/search_test.ts
#	packages/gateway/__tests__/repo/search-usage_test.ts
#	packages/gateway/src/control-plane/schemas.ts
#	packages/gateway/src/control-plane/search-usage/routes.ts
#	packages/gateway/src/data-plane/chat/responses/interceptors/server-tools/web-search.ts
#	packages/gateway/src/data-plane/tools/web-search/config.ts
#	packages/gateway/src/data-plane/tools/web-search/domain-normalize.ts
#	packages/gateway/src/data-plane/tools/web-search/operations.ts
#	packages/gateway/src/data-plane/tools/web-search/provider.ts
#	packages/gateway/src/data-plane/tools/web-search/providers/jina.ts
#	packages/gateway/src/data-plane/tools/web-search/providers/microsoft-grounding.ts
#	packages/gateway/src/data-plane/tools/web-search/providers/microsoft-web-iq.ts
#	packages/gateway/src/data-plane/tools/web-search/providers/web-iq.ts
#	packages/gateway/src/repo/sql.ts
#	packages/gateway/src/shared/web-search-providers.ts
@Menci
Menci changed the base branch from main to rename-microsoft-web-iq July 29, 2026 08:27
@Menci
Menci marked this pull request as draft July 29, 2026 08:27
Menci added 30 commits July 30, 2026 16:20
# Conflicts:
#	apps/web/src/root.tsx
#	apps/web/src/theme.ts
WinUI holds a chromeless button's label at the primary text fill where
Fluent runs it at the secondary one, and the layer said so by redefining
the token Fluent reads. A custom property handed to the root reaches
every descendant, and a button is free to hold more than a label: the
usage summary puts a two-line tile inside one, and its caption -- which
asks for the secondary fill by name -- was being answered with the
primary, so the same caption read darker there than beside the identical
tile on the performance page, which is not a button.

Stated as a colour instead. It inherits too, but a descendant naming its
own wins over it, which is the distinction that was missing.
Stating the chromeless foreground as a colour outranked what Fluent
paints on a checked one, so a checked subtle button kept the accent fill
and lost the on-accent label with it -- and a caller that hands that
label down by inheriting from the root, as the usage summary does, got
the near-black primary fill over accent blue.

The rule now names the unchecked states only. A checked chromeless button
paints itself from the accent family, which already carries its own
label colour.
The tile was a ToggleButton, so choosing a metric filled it with the
accent -- the heaviest mark WinUI has, and the right one for a switch
that is on. Picking one item out of a set is not that. Every other
selected thing in this dashboard says so the same way: the subtle fill it
would take under the pointer, held, with an accent bar down its leading
edge, and its text left alone.

The fill and the bar are stated at the call site rather than in the
layer, which paints a ToggleButton as WinUI paints one and is right to.
The bar carries the indicator's 16px as the inset it leaves against a
36px row, so it grows with a tile that is taller than one.
Replacing the fill left the rest of it behind. The value inherited the
on-accent label the layer sets on the root, so it stayed white over a
pale fill, and the accent's elevation stroke stayed too -- its heavier
bottom edge reading as a rule under the tile.

The layer names the checked state at a specificity a call site cannot
reach, which is right for a rule meant to hold across the dashboard, so
undoing it here is what the escape hatch is for.
WinUI states the navigation indicator as a 16px rectangle, but it states
it against a 36px item, so the length is a consequence of the room the
item gives it rather than a value of its own. The list row and the usage
tile already carry it that way; the navigation pill was the last one
still naming a length, and a length cannot follow an item that grows.

Every stretch in the animation is a multiple of the bar's own length, so
that length is now derived from the item alongside them.
The bar's length is the room its item leaves it, and the layout has
already worked that out. Recomputing it from the inset duplicated the
arithmetic CSS had just done and would have drifted from it the moment
either side moved.
Fluent animates its overlays from JavaScript through react-motion, so
none of it was reachable from the stylesheet layer and the dialog was
still entering Fluent's way: growing in from 0.85 while fading across
the whole 250ms. WinUI does the opposite -- it settles down from 1.05,
and its fade is a separate 83ms linear run, so the surface is already
opaque while it is still moving. Hiding takes 167ms to showing's 250.

The motion slot forbids `as`, so the animating component cannot be named
directly; it does take a render function, which receives the props
Fluent would have handed its own motion component. That is the seam.

The duration and easing vocabulary moves to winui/motion.ts, which the
token sheet now interpolates, so the CSS custom properties and the
keyframe numbers are one value. The selection-indicator timing joins it
from lib/, where it never belonged.
The backdrop was on Fluent's relaxed fade, 300ms, which outlasts even
the dialog's own 250ms entrance -- the page was still darkening after
the dialog had arrived and settled. WinUI gives the smoke layer its own
keyframes and nothing but an opacity leg, 83ms linear, symmetric in both
directions; the scale it pairs that with targets the dialog's background
element, so the backdrop itself never moves.

Measured in a browser: 0 to 1 in five linear steps ending at 89ms, and
the same going out.
The ramp was transcribed once and then described twice, in two comments
that contradicted each other: one still said Floway's brand fill stood
in for it, the other stated the literals that had replaced that. The
second is what the code does. The first is gone, and with it the reason
the layer had for keying anything to Fluent's theme class -- no value
here reads a Fluent variable any more, so the whole vocabulary sits on
:root and the selected-text highlight takes the unmodified accent step
WinUI actually names for it.

Light1 and Dark3 complete the seven steps. That closes three deferrals
the old comment justified: the accent text ramp's secondary and tertiary
stops, the toast action's link colour, and the accent edge a focused
text field draws, whose stop is the same step the accent fills take.

The nav sheet's two remaining "out of reach" notes go too. The sidebar
stopped forcing item fills and the layer implements the subtle ramp
above them; the selected label came off semibold; and the pill has been
on the accent fill since it became a measured element.
WinUI does not move a menu into place, it wipes it in: the presenter
slides from half its own height while a clip slides the other way by
exactly as much, pinning the visible window to the final layout box so
only the content travels through it. A menu below its trigger starts as
its own bottom half drawn in the top half of the box and grows down.
Nothing fades in. Closing is the opposite kind of thing entirely -- a
bare 83ms linear fade with no transform, which is why the two directions
are written as an unmatched pair.

The slide goes on `translate` rather than into `transform`, because
`transform` is where the positioning engine already put the popover's
coordinates; a keyframe naming it plays the whole reveal at the origin
of the containing block instead. Measured: the clip and the offset agree
to seven digits at every frame, and the positioning transform is
untouched throughout.

Popover keeps Fluent's motion. Its WinUI counterpart reads its timings
out of the Windows theme at runtime, so there is nothing to transcribe.
Four faults, two causes.

The chromeless selector was built as a comma-joined list and then had
each state suffixed onto the joined string. A suffix attaches to a
selector list's last item alone, so the pressed rule -- the last one
written -- matched every subtle button in every state, and the rest rule
above it never won. A subtle button's label sat at the secondary text
fill at rest while the transparent button beside it, which the file
declares to be the same WinUI style, sat at the primary one. The state
is now appended per appearance. The same list carried no disabled
exclusion, which is why a disabled chromeless button was pixel-identical
to an enabled one; it now excludes both spellings of disabled and lets
Fluent's disabled foreground through.

The primary appearance had only its hover and pressed steps moved onto
the accent, leaving the rest fill on the product's brand. WinUI states
all three as one colour at 1.0, 0.9 and 0.8 opacity, so they cannot
differ in hue -- and in dark the mismatch put the on-accent label, which
is black against a light accent, on a dark brand fill. Stating the rest
fill also settles the second half: a checked ToggleButton already read
the accent, so the two sat side by side in different blues.

Measured, both schemes: subtle rest #000000e4 / #ffffff, disabled
#0000005c / #ffffff5d, and primary rest, primary hover at 90% and a
checked toggle all resolving to #0067c0 / #4cc2ff.
Every fill and indicator rule in the sheet named NavItem alone, so a
selected category row kept Fluent's opaque grey and Fluent's own pill --
4x20 and fully rounded, in the brand foreground -- next to a WinUI item
carrying a 3x16 accent bar. The fill ramp now names both.

The pill stays a pseudo-element rather than being cleared the way an
item's is. Clearing an item's is only right because the sidebar draws a
measured one in its place to animate between items; a category opens and
closes rather than navigating, so it is never an endpoint of that
animation and has nothing to hand over to.

Its colour has to be substituted, not declared. Fluent grows the pill in
with a keyframe whose 100% stop reads the compound brand foreground,
filled in both directions, and an animation outranks every normal rule
in the cascade -- a background-color of ours was measured losing to it.
The variable is declared on the pseudo-element itself, so it reaches the
one declaration that reads it and nothing the row contains.

Measured, both schemes: fill #9 / #ffffff0f, pill 3px at a 2px
radius in #0067c0 / #4cc2ff.
The severity fill is the most visible thing an InfoBar has and it was
the one part of the control never implemented: the bar was a pale wash
of the brand ramp in all four severities, and its glyph was Fluent's
tint rather than the system fill the validation glyph beside it on the
same page already used. In dark that put WinUI yellow next to Fluent
orange for the same idea.

Both halves of the blocker are gone. The intent reaches the DOM as
`data-winui-intent`, and the filled glyph the chokepoint swaps in is
already WinUI's two-layer shape -- a disc with the symbol as negative
space -- so only the colour was missing. What shows through the knock-out
is a hole onto the bar's own wash where WinUI paints TextFillColorInverse;
the two differ by a few percent of lightness and closing it would mean
stacking an element behind a glyph that is already the right silhouette.

The vocabulary gains the four SystemFillColor*Background steps and the
success and attention hues. Attention is a step of the accent ramp
rather than a literal, and not the step the accent fills take.

Measured, both schemes: every bar and glyph resolves to its XAML value,
and the stroke stays the card stroke every severity shares.
A link kept Fluent's brand foreground while the toast's link-shaped
action had moved onto WinUI's accent TEXT ramp, so one screen carried
two link colours. WinUI walks a hyperlink down that ramp -- primary,
secondary on pointer, tertiary while pressed -- and it is a different
ramp from the accent fill a button takes, darkened in light and
lightened in dark so it reads as type rather than as a surface. The
disabled step was already there; the three enabled ones join it.

The check box was drawn at Fluent's 2px. Its sheet said the radius was
"already ControlCornerRadius through the themed borderRadiusSmall",
which the theme layer's own comment contradicts -- it leaves the small
step on Fluent's 2px because no WinUI radius is that small -- and the
browser sided with the theme. Stating 4px means naming the square shape
so `shape="circular"` keeps its circle, which is one more axis Fluent
resolves in JavaScript.

That made four axes, so the three near-identical stamping functions
collapse into one that takes the prop, the attribute, the default and
the slots. Checkbox needs the slot list for the same reason Input does:
its root is not its primary slot, and the stamp otherwise lands on the
inner input.

The request list's row separator named the neutral stroke, which maps
onto the CARD stroke -- black at 10%, invisible against a dark page --
where every other row separator in the dashboard reads the divider. Its
key selector hardcoded a 7px radius; WinUI has 4 and 8, and the selector
heads a card, so it takes the card's.

Measured, both schemes: link #003e92/#99ebff resting and #001a68/#99ebff
on pointer, check box 4px, key selector 8px.
The drop zone predates the layer and sat entirely outside it: a plain
button with no fui- class, so not one rule in winui/controls could reach
it, holding Fluent brand colours, an ad-hoc 150ms transition and a 0.6
opacity for disablement.

Neither library ships a drop target, so it is composed rather than
transcribed. It is a button, and WinUI paints a chromeless button by
stepping the subtle fill ramp; the outline takes the strong control
stroke, the family an unchecked check box reads, because this is an
outline that has to be seen rather than a surface edge; the radius is
the overlay step the card around it takes; and dragging a file over it
is the accepting state, so that one takes the accent. Disablement steps
the fill and the foreground the way WinUI disables everything except a
list item, instead of fading the element whole.

The dashed pattern is the one thing with no WinUI provenance. It is the
affordance itself and nothing in the corpus describes a drop target.

The label and glyph drop their own foreground so the zone's disabled
state actually reaches them, and the summary tiles take the card ramp's
secondary step at the control corner rather than a hand-picked grey at
a 6px radius WinUI has no counterpart for.

Measured, both schemes: stroke #72 / #ffffff8b, radius 8px, rest
transparent stepping to #9 / #ffffff0f, label #0000009e /
#ffffffc5, 83ms on cubic-bezier(0, 0, 0, 1).
Each was removed in a live page and the computed value re-read after the
cascade settled, on every dashboard page, in both colour schemes, at two
viewports, and again with hover, active and focus forced on the element
and on six levels of its ancestors. None of the six moved a pixel.

  Panel's radius map        every card takes the overlay corner from the
                            layer, so the 8px entry restated it; the one
                            caller wanting 12px now says so itself, and
                            the prop goes with the map
  sidebar item radius       4px, which is what the medium radius already
                            resolves to
  tooltip icon colour       `inherit !important`, which is what an icon
                            does anyway once the button states a colour
  usage tile `!h-auto`      height:auto is the initial value
  usage graph opacity       1, already the initial value
  request selector shadow   none, and the layer flattens that elevation

Measured with transitions zeroed. Reading a computed value in the same
task as the CSSOM edit returns the pre-edit value while a transition is
running, which is what made an earlier pass of this call six more
declarations dead than really are.
Each was measured twice. First by dropping the importance from the live
rule and re-reading the computed value on every dashboard page, in both
colour schemes, at two viewports, with the states its own selector names
forced through the debugger -- 81 rules carried importance and 26 of
them turned out to change nothing without it. Then, after the edit, by
diffing a full computed-style snapshot of every element on every page
against the same snapshot taken with the change reverted: 28 properties,
20 page and scheme combinations, zero differences.

Most were Uno utilities prefixed to beat a Griffel atom that does not
exist -- widths, paddings, text alignment, overflow. The rest were the
sidebar item's geometry and the usage chart's SVG overrides, which win
on their own selector, and the usage tile's fill and label, whose
attribute selector already outranks what it displaces. The tile's border
colours keep theirs: those really are outranked, and the measurement
says so.
A scan of the vocabulary against every var() in the app finds one
declared property with no reader: SystemAccentColorLight1. It went in
with the rest of the ramp, and unlike Dark3 -- which the accent text
ramp's secondary step spends -- nothing has asked for it.

The header's note about token blocks that must be declared where Fluent
declares its theme goes too. There are no such blocks left.
The dashboard has been restyled onto WinUI 3 for a while and the file
still described it as a Fluent web app with no mention of the layer, its
scoping rules, or the two shims that make Fluent's JavaScript-resolved
props and overlay motion reachable at all.
The sidebar's footer separator is reached through an inline style
attribute, not through an `!important` declaration -- those came off
when the measurements showed they changed nothing.
Each names a value the element already had: `tracking-normal` and
`font-fui-regular` are the initial letter-spacing and weight, the three
`leading-*` steps repeat the line-height their type ramp already sets,
and `inline-block` loses to a display the same element is given
elsewhere.

Measured by removing all thirteen and diffing a full computed-style
snapshot -- 60 page, scheme and viewport combinations, every element, 28
properties -- against the same snapshot of the unchanged tree. The two
differ on one span, by 2px of text width, and two snapshots of the
unchanged tree differ on that same span by the same amount: it is the
harness's noise floor, not this change.

Eight further candidates were tested the same way and put back. The
sidebar footer's transparent fill, the two grid row templates and the
chart height all moved real pixels -- the row templates let the document
grow past the viewport -- and `font-mono` reaches twenty-one files where
the measurement covered one.
The panel asked for 12px, which is not a radius WinUI has: the corpus
states exactly two, ControlCornerRadius at 4 and OverlayCornerRadius at
8, and a card-shaped surface takes the second. The layer already gives
every Card that value, so the override went rather than moving to a
call site.

Measured at both schemes: 12px to 8px on all four corners, with the
padding and the fill unchanged.
The sheet's charter is to carry what still disagrees with WinUI after
the theme layer re-points Fluent's tokens. These eight no longer
disagree: the checked check box and radio under pointer and press, the
disabled radio label, the ColorArea outline, and the tab foreground at
rest and disabled -- the last two as one unit, since the disabled rule
existed only to outweigh the rest rule.

Each was adjudicated on its own, per page and per scheme, with the
state its selector names forced through the debugger. Two sentinels
separate the outcomes an earlier pass had conflated: an injected
`!important` rule on the same selector proves the selector is matching
in that state, and a sentinel on the candidate's own declaration proves
which rule wins. Only where both showed does removal mean anything, and
removal was then measured to change nothing in every such combination.

Measured after the fact, both schemes: the tab foregrounds still resolve
to TextFillColorSecondary and TextFillColorDisabled, the indicators to
AccentFillColorDefault, the ColorArea to ControlStrokeColorDefault.

The disabled link keeps its rule. It is redundant by the same test, but
only because the accent and neutral disabled literals coincide, and
unpicking it also unpicks a forced-colors guard that exists to outweigh
it -- a path none of this measures.
`gap-[16px]` asked four panels for a 16px row gap and they render 12 --
Fluent's own card gap atom carries equal weight and comes later, so the
value was never the one in effect. `max-[1100px]:min-h-0` is the same
shape: the floor it tries to lift stays where it was. Removing either
changes nothing, which the panels confirm at 12px afterwards; a later
caller that actually wants those values can mark them important, which
is what the neighbouring utilities on the same elements already do.

The selector bar's rest foreground restates the primary text fill its
items already inherit. Its pointer and pressed steps stay, because those
do move the colour.

Adjudicated per rule with the state its selector names forced, then
re-measured on the elements: gaps 12px in both schemes, selector items
still TextFillColorPrimary.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant