+**Mobile experience** (`src/components/mobile/`, <768px via `useIsDesktop`): a dedicated touch UI rendered by `MobileApp` from App.tsx's mobile branch — the desktop layout is untouched. Screens: `MobileRunList` (uniform cards; group tap opens the group page; the search field segues to a flat search screen) → `MobileRunView` (header: group crumb / title → `MobileRunInfoSheet` with notes + config + copyable identifiers; bell → `MobileAlertsSheet`, severity-filterable, tap jumps to the node sheet; gear → `MobileSettingsSheet` sliders over the shared `Settings` keys). Body toggles DAG ⇄ Feed via the shared `viewMode` store key ('graph'/'flat', so `nb.ui(view=)` still applies): `MobileDagCanvas` is a custom dagre + pan/pinch canvas (not ReactFlow; no explicit `setPointerCapture` — it would retarget the derived click and swallow node taps), `MobileFeed` is a stage-rail + type-filter card feed whose expanded charts reuse `SingleRunChart` (exported from `NodeMetrics`). `MobileTracker` renders the persistent heat-strip bar + scrub sheet over `useStreams`. Node taps and alerts open `MobileNodeSheet`, which wraps the desktop `LoggableTabContainer` for full tab parity. Store invariant: **every run mutator clones the run object** (`runs.set(id, { ...run, field })` — see the comment above `setRuns`), so `s.runs.get(id)` selectors are sound and fire only for that run; never mutate a stored run in place. Components that read one slice should select the leaf field (`s.runs.get(id)?.logs`) so unrelated mutations don't re-render them. Phone-width embeds (`?run=`, `&dag`, `&flat`) render the mobile layout via `EmbeddedMobileLayout`.
0 commit comments