V2/LCD Runtime Boundary Audit¶
Date: 2026-04-11
Status: Research in progress
Issue: #641
Base commit: f0e78cc0ea0fcfbafdadbb4a20167ba0bc110d6a (origin/main)
Goal¶
Establish whether v2 main and LCD are truly two presentations over one shared frontend runtime, or whether runtime responsibilities have leaked into layout and leaf components.
Current topology¶
Shared app entrypoint¶
frontend/src/App.svelte:24-52 initializes one frontend runtime path for v2:
- polls
/api/v1/stateviastartPolling(...) - loads
/api/v1/capabilities - opens the control WebSocket
/api/v1/ws - mounts
RadioLayoutV2whenuiVersion === 'v2'
This means LCD is not a separate app in current origin/main; it is a layout variant inside v2.
V2 layout split¶
frontend/src/components-v2/layout/RadioLayout.svelte:191-195 chooses between:
MobileRadioLayoutLcdLayout- standard
RadioLayoutcontent
frontend/src/components-v2/layout/LcdLayout.svelte:47-67 reuses the same sidebars as standard layout:
LeftSidebarRightSidebarStatusBarKeyboardHandler
Only the center content changes:
- standard layout renders
SpectrumPanel - LCD layout renders
AmberLcdDisplay
Shared state/command machinery already present¶
The main shared abstraction is:
frontend/src/components-v2/wiring/state-adapter.tsfrontend/src/components-v2/wiring/command-bus.ts
Example: RightSidebar.svelte:56-85 derives props through state-adapter and binds UI events through command-bus. RxAudioPanel.svelte:8-23 is mostly presentation-only and receives props/callbacks.
This is the right architectural direction, but it is not yet the only path.
Confirmed boundary violations¶
1. Presentation components still own transport connections¶
These components open their own WS channels instead of consuming data from a shared runtime/controller:
frontend/src/components/spectrum/SpectrumPanel.svelte:310-318Opens/api/v1/scopedirectly withgetChannel('scope').frontend/src/components-v2/panels/audio-scope/AudioSpectrumPanel.svelte:55-72Opens/api/v1/audio-scopedirectly withgetChannel('audio-scope').frontend/src/components-v2/panels/lcd/AmberLcdDisplay.svelte:145-168Also opens/api/v1/audio-scopedirectly withgetChannel('audio-scope').
Consequence: scope/audio-scope lifecycle is tied to which component happens to be mounted, not to one runtime owner.
2. Leaf components still dispatch backend commands directly¶
These components bypass command-bus and call sendCommand(...) themselves:
frontend/src/components/spectrum/SpectrumPanel.svelte:287-298Sendsset_freqduring drag tuning.frontend/src/components-v2/panels/lcd/AmberLcdDisplay.svelte:284Sendsset_tuner_statusdirectly.frontend/src/components-v2/panels/MemoryPanel.svelte:56-80Sends memory commands directly.
Consequence: behavior can diverge by component even if state derivation is shared.
3. TX audio lifecycle is split between command handlers and components¶
RX audio is controlled in command-bus:
frontend/src/components-v2/wiring/command-bus.ts:536-581makeRxAudioHandlers()ownsaudioManager.startRx(),stopRx(), and browser volume updates.
TX audio is not centralized the same way. These presentation/layout components still call audioManager directly:
frontend/src/components-v2/panels/TxPanel.svelte:55-80CallsaudioManager.startTx()/stopTx()inside PTT logic.frontend/src/components-v2/layout/MobileRadioLayout.svelte:290-297Also callsaudioManager.startTx()/stopTx()directly.
Consequence: RX and TX do not share one consistent ownership model. Mobile and desktop TX can evolve differently.
4. Layout/presentation components still own HTTP side effects¶
These UI components call backend HTTP endpoints directly:
frontend/src/components-v2/layout/LcdLayout.svelte:27-38Calls/api/v1/radio/power.frontend/src/components-v2/layout/StatusBar.svelte:47-78Calls connect/disconnect and power endpoints.frontend/src/components-v2/layout/StatusBar.svelte:87-108Calls/api/v1/eibi/identify.
Consequence: system/runtime actions are not consistently routed through one orchestration layer.
5. LCD still contains logic that is not expressed through shared view-models¶
frontend/src/components-v2/panels/lcd/AmberLcdDisplay.svelte contains custom derivation beyond presentation:
- band lookup by frequency:
:38-64 - active meter source state and cycling:
:87-109 - LCD-specific DSP activity heuristics:
:111-121 - AGC label mapping:
:133-140
Some of this is valid presentation logic, but some of it is semantic UI/view-model logic that should be explicit in adapters if parity across skins matters.
6. Scope frame parsing is duplicated across UI surfaces¶
Equivalent parseScopeFrame(...) logic appears in:
frontend/src/components-v2/panels/audio-scope/AudioSpectrumPanel.svelte:18-27frontend/src/components-v2/panels/lcd/AmberLcdDisplay.svelte:27-36frontend/src/components/spectrum/spectrum-logic.ts
Consequence: protocol handling is duplicated in presentation surfaces.
What is actually shared today¶
The current codebase is not "two separate frontends". It already shares meaningful core pieces:
- one top-level app bootstrap (
App.svelte) - one control WS path
- one polling/state store path
- one capabilities store path
- one
state-adapter - one
command-busfor a large portion of controls - shared sidebars between standard and LCD layouts
So the problem is not lack of any shared architecture. The problem is incomplete enforcement of that architecture.
Current assessment¶
The code on origin/main is in an intermediate state:
- directionally correct toward shared runtime + presentation adapters
- not yet strict enough to guarantee parity
- still vulnerable to model drift because transport, audio lifecycle, and commands can be reintroduced into UI components
That explains how "same backend" can still produce different behavior by layout or mounted component path.
Implications for next issues¶
For #642 (audio failure trace)¶
Need to verify whether LCD audio failure is caused by:
- a capability/UI gating mismatch
- a decode/playback path issue in
audioManager/rx-player - component-mount ownership of audio/scope side effects
- a browser/user-gesture issue
For #643 (target runtime contract)¶
The target contract should make these ownership rules explicit:
- layouts do not open WS connections
- panels do not import
audioManager - panels do not call
sendCommand(...)directly - panels do not call backend HTTP endpoints directly
- protocol parsing does not live in rendering components
For #646 (presentation architecture)¶
Need to separate:
- presentation-only formatting/styling logic
- semantic view-model derivation
- runtime transport/command ownership
Without that split, "skin" work will keep reintroducing behavioral forks.
Provisional conclusion¶
On current origin/main, LCD and standard v2 are partially unified, but not fully architecture-safe.
The main architectural defect is not that LCD is a separate app; it is that runtime ownership is still distributed across layout and leaf components. That is enough to cause layout-dependent regressions even when backend transport is shared.