Panel → Adapter Migration Plan¶
Tracking issue: #1240 (Tier 2 of #1063; parent #1238).
Current state¶
- 18 panels (Svelte components + 2 helper modules) under
frontend/src/components-v2/panels/still import directly from$lib/stores/*— verified 2026-04-29 against commitd48e8705onmain. - This breaks the layering rule from
CLAUDE.md("Frontend layering"): panels and layouts must not reach into stores; they must consume props fromlib/runtime/adapters/. - ESLint (
no-restricted-imports) currently bans$lib/transport/*and$lib/audio/audio-managerfrom panels; tightening to also ban$lib/stores/*is out of scope for this issue (Tier 3). - Source-of-truth count comes from
grep -rln '$lib/stores' src/components-v2/panels/minus tests (__tests__/files). Test files are not in scope — they exercise the store layer directly by design.
Existing adapters (frontend/src/lib/runtime/adapters/)¶
| File | Purpose |
|---|---|
audio-adapter.ts |
RX audio view-model + handlers (used by RxAudioPanel). |
panel-adapters.ts |
Per-panel derive*Props() / get*Handlers() for AGC, Mode, Antenna, RF Front End, RIT/XIT, Scan, Meter, CW, DSP, TX, Filter, Band Selector. |
scope-adapter.ts |
Audio-scope WS channel lifecycle + binary-frame parsing. |
tx-adapter.ts |
Audio TX lifecycle callbacks for PTT components. |
vfo-adapter.ts |
VFO view-model props (frequency / mode / split / RX badges). |
Note: panel-adapters.ts already exports a derive-fn for most "capability"
panels in this audit (CW, DSP, Meter, TX, RF Front End, Filter). The panels
have been partially migrated — they consume derive*Props() for their
state but still call ad-hoc capability helpers (hasCapability,
hasTx, getAttValues, etc.) directly from the store. Closing that
remaining gap is the bulk of this migration.
Cluster inventory¶
Cluster A — capabilities-only (8 files)¶
| File | Stores accessed | Read/Write |
|---|---|---|
AudioRoutingControl.svelte |
capabilities.svelte (receiverLabel) |
read |
CwPanel.svelte |
capabilities.svelte (hasCapability) |
read |
DspPanel.svelte |
capabilities.svelte (hasCapability) |
read |
MeterPanel.svelte |
capabilities.svelte (hasTx) |
read |
TxPanel.svelte |
capabilities.svelte (hasTx, hasCapability) |
read |
RfFrontEnd.svelte |
capabilities.svelte (hasCapability, getAttValues, getAttLabels, getPreValues, getPreLabels) |
read |
filter-controls.ts |
capabilities.svelte (getControlRange) |
read |
meter-utils.ts |
capabilities.svelte (getMeterCalibration, getMeterRedline) |
read |
Adapter status: new adapter required. Today, the existing
panel-adapters.ts exposes typed view-model props per panel (it already
calls runtime.caps internally), but each of these files also reaches
into capabilities.svelte for ad-hoc booleans / option lists / numeric
ranges. Two complementary moves:
- Move per-panel capability flags into the
derive*Props()return value (e.g.CwProps.showBreakIn,RfFrontEndProps.attOptions). Most already have a "props.has*" shape — extend it to cover everything the panel currently asks for. - Add a thin shared
capabilities-adapter.tsfor the pieces that genuinely don't fit a single panel's view-model:meter-utils.tsandfilter-controls.tsare helper modules used from many places, so exposinggetMeterCalibration/getMeterRedline/getControlRangeviaruntime.caps.*(already available — these helpers just need to accept caps as an argument or read fromruntime.caps) is enough.receiverLabel, used byAudioRoutingControl, can be inlined into the existing audio routing props or moved to the same shared adapter.
Complexity: trivial-to-moderate. No state, no writes — pure derived
data. The Svelte panels already use $derived(...) against the helpers,
so swapping the import to a runtime adapter is a one-line change per
call site once the adapter exposes the same shape.
Migration order: first — lowest risk, unlocks the rest.
Cluster B — radio live state (5 files; was 6 in scout — see note)¶
| File | Stores accessed | Read/Write |
|---|---|---|
audio-scope/AudioSpectrumPanel.svelte |
radio.svelte (radio.current), capabilities.svelte (getCapabilities) |
read |
MemoryPanel.svelte |
radio.svelte (radio.current) |
read |
lcd/AmberScope.svelte |
radio.svelte, capabilities.svelte (hasAudioFft, hasDualReceiver, getCapabilities, hasCapability) |
read |
lcd/AmberTelemetryStrip.svelte |
radio.svelte |
read |
lcd/VfoControlPanel.svelte |
radio.svelte, capabilities.svelte (hasCapability) |
read |
Note on AmberCockpit double-listing. The decomposition scout placed
AmberCockpit.sveltein both this cluster and the qsy-history cluster. It is one file with two responsibilities: it readsradio.currentfor view-model state (this cluster) and it writes toqsyHistoryfrom a tuning effect (Cluster D). Migrating it requires both adapters to land first; it is therefore listed under Cluster D as the merge point and excluded from this row to avoid double-counting. Total panel count remains 18.
Adapter status: partial. vfo-adapter.ts already covers VFO props,
but no adapter exposes a generic radio.current-flavored read (active RX,
freq/mode by slot). Three options:
- Extend per-panel adapters in
panel-adapters.ts—deriveMemoryPanelProps(),deriveAudioSpectrumProps(),deriveAmberScopeProps(),deriveAmberTelemetryProps(),deriveVfoControlProps(). Each returns the small slice the panel actually needs (e.g. MemoryPanel only needs{ activeFreqHz, activeMode }for the "store VFO → channel" flow). This keeps panels presentational. - Single shared
radio-state-adapter.tsexposinggetActiveRx()/getRadioSnapshot(). Rejected — bypasses the typed-props conventionpanel-adapters.tshas established. - Combine 1 + the capabilities work from Cluster A. Recommended:
the same per-panel
derive*Props()already consumesruntime.caps(Cluster A target), so consumingruntime.statealongside is the identical pattern.
Complexity: moderate. Write paths exist (MemoryPanel calls
runtime.send(...) for set_memory_mode / memory_to_vfo /
memory_write / memory_clear), but those go through the runtime
already — only the read of radio.current needs adapting.
AmberCockpit straddles this and Cluster D — see Cluster D.
Migration order: second.
Cluster C — LCD chrome (2 files)¶
| File | Stores accessed | Read/Write |
|---|---|---|
lcd/LcdContrastControl.svelte |
lcd-contrast.svelte (LCD_CONTRAST_PRESETS, applyLcdContrast, getLcdContrastPreset, setLcdContrastPreset, stepLcdContrast, LcdContrastPreset type) |
read+write |
lcd/LcdDisplayModeControl.svelte |
lcd-display-mode.svelte (LCD_DISPLAY_MODES, getLcdDisplayMode, setLcdDisplayMode, LcdDisplayMode type) |
read+write |
Adapter status: no adapter exists. Both stores are pure UI-chrome
state (LCD vintage skin), local to the browser, persisted in
localStorage. They have no equivalent in runtime.state because
nothing in runtime cares about LCD contrast or display mode.
Two reasonable shapes:
lcd-chrome-adapter.tsunderlib/runtime/adapters/exposing a thin façade that just re-exports the store API. This satisfies the layering rule mechanically but adds a pass-through indirection.- Treat
lib/stores/lcd-*.svelte.tsas "skin-local UI state" and exempt them from the rule. Move them tofrontend/src/skins/amber-lcd/state/(or similar) and update the ESLint rule to ban$lib/stores/*while allowing skin-local imports.
Recommendation: (1) for this migration, with an open question (see below) whether (2) is the durable answer. (1) is mechanical and unblocks ESLint tightening; (2) is a separate refactor.
Complexity: trivial. Two small files, no derived state.
Migration order: third (after Cluster A so the adapter pattern is settled).
Cluster D — qsy-history (2 files)¶
| File | Stores accessed | Read/Write |
|---|---|---|
lcd/AmberCockpit.svelte |
radio.svelte, capabilities.svelte, qsy-history.svelte (qsyHistory.record) |
read (radio, caps) + write (qsyHistory) |
lcd/AmberMemoryStrip.svelte |
qsy-history.svelte (qsyHistory.recent) |
read |
Adapter status: no adapter exists. qsyHistory is a small
ring-buffer with two entry points: record(freq, mode) (write,
debounced — see issue #836) and recent (read, latest 3 reversed). The
write happens from a tuning effect inside AmberCockpit whenever
activeFreqHz / activeMode changes.
Sketch: qsy-history-adapter.ts exporting:
export function deriveQsyRecent(): readonly QsyEntry[]
export function recordQsy(freqHz: number, mode: string): void
AmberCockpit is the merge point for Cluster B (radio state), Cluster A
(capabilities), and Cluster D (qsy-history). It should migrate last,
after the per-panel adapter for it (deriveAmberCockpitProps +
getAmberCockpitHandlers) is in place. The handler bundle exposes
onTuningChange(freq, mode) → recordQsy(...) so the panel never
imports the qsy store.
Complexity: moderate. The debounce semantics in qsyHistory.record
must be preserved (issue #836). AmberMemoryStrip is trivial —
read-only.
Migration order: fourth (depends on Cluster A capability flags being
available on runtime.caps and Cluster B radio.current adapter).
Cluster E — connection (1 file)¶
| File | Stores accessed | Read/Write |
|---|---|---|
RxAudioPanel.svelte |
connection.svelte (isAudioConnected), capabilities.svelte (hasDualReceiver) |
read |
Adapter status: partial. audio-adapter.ts already supplies
deriveRxAudioProps() / getRxAudioHandlers(). The two remaining
direct-store reads (isAudioConnected, hasDualReceiver) should fold
into RxAudioProps — both are derivable from runtime.audio.connected
and runtime.caps.hasDualReceiver respectively.
Complexity: trivial. One panel, two booleans.
Migration order: fifth (smallest; can ship anytime once Cluster A
lands hasDualReceiver on runtime.caps).
Per-panel detail¶
| # | Panel / module | File | Stores imported | R/W | Adapter target | Complexity |
|---|---|---|---|---|---|---|
| 1 | AudioRoutingControl | panels/AudioRoutingControl.svelte |
capabilities.receiverLabel |
R | extend panel-adapters (audio-routing) |
trivial |
| 2 | CwPanel | panels/CwPanel.svelte |
capabilities.hasCapability |
R | extend CwProps in panel-props |
trivial |
| 3 | DspPanel | panels/DspPanel.svelte |
capabilities.hasCapability |
R | extend DspProps |
trivial |
| 4 | MeterPanel | panels/MeterPanel.svelte |
capabilities.hasTx |
R | extend MeterProps |
trivial |
| 5 | TxPanel | panels/TxPanel.svelte |
capabilities.{hasTx,hasCapability} |
R | extend TxProps |
trivial |
| 6 | RfFrontEnd | panels/RfFrontEnd.svelte |
capabilities.{hasCapability,getAtt*,getPre*} |
R | extend RfFrontEndProps (att/pre options) |
moderate |
| 7 | filter-controls (helper) | panels/filter-controls.ts |
capabilities.getControlRange |
R | inject caps arg or new capabilities-adapter |
trivial |
| 8 | meter-utils (helper) | panels/meter-utils.ts |
capabilities.{getMeterCalibration,getMeterRedline} |
R | inject caps arg or new capabilities-adapter |
trivial |
| 9 | AudioSpectrumPanel | panels/audio-scope/AudioSpectrumPanel.svelte |
radio.current, capabilities.getCapabilities |
R | new deriveAudioSpectrumProps in panel-adapters |
moderate |
| 10 | MemoryPanel | panels/MemoryPanel.svelte |
radio.current |
R | new deriveMemoryPanelProps ({activeFreqHz, activeMode}) |
moderate |
| 11 | AmberScope | panels/lcd/AmberScope.svelte |
radio, capabilities.{hasAudioFft,hasDualReceiver,getCapabilities,hasCapability} |
R | new deriveAmberScopeProps |
moderate |
| 12 | AmberTelemetryStrip | panels/lcd/AmberTelemetryStrip.svelte |
radio |
R | new deriveAmberTelemetryProps |
trivial |
| 13 | VfoControlPanel | panels/lcd/VfoControlPanel.svelte |
radio, capabilities.hasCapability |
R | extend existing vfo-adapter (or deriveVfoControlProps) |
moderate |
| 14 | LcdContrastControl | panels/lcd/LcdContrastControl.svelte |
lcd-contrast (5 fns + type) |
R+W | new lcd-chrome-adapter (contrast facet) |
trivial |
| 15 | LcdDisplayModeControl | panels/lcd/LcdDisplayModeControl.svelte |
lcd-display-mode (3 fns + type) |
R+W | new lcd-chrome-adapter (display-mode facet) |
trivial |
| 16 | AmberCockpit | panels/lcd/AmberCockpit.svelte |
radio, capabilities.{hasAudioFft,hasDualReceiver,getCapabilities,hasCapability}, qsy-history.record |
R+W | new deriveAmberCockpitProps + getAmberCockpitHandlers (depends on qsy-history-adapter) |
hard |
| 17 | AmberMemoryStrip | panels/lcd/AmberMemoryStrip.svelte |
qsy-history.recent |
R | new qsy-history-adapter (deriveQsyRecent) |
trivial |
| 18 | RxAudioPanel | panels/RxAudioPanel.svelte |
connection.isAudioConnected, capabilities.hasDualReceiver |
R | extend RxAudioProps (audio-adapter) |
trivial |
Proposed migration batches¶
Each batch ≤ 4 panels. Sub-issues open from this plan after #1240 closes.
Batch 1 — capability flags into existing panel props (4 panels)¶
- Panels: CwPanel, DspPanel, MeterPanel, TxPanel
- Why first: lowest risk, smallest diff. Each panel already calls
derive*Props(); the migration just extends the typed return shape to include thehas*booleans currently read inline. No new adapter file. Establishes the pattern. - Sub-issue title:
refactor(frontend): fold capability flags into Cw/Dsp/Meter/Tx panel props (Tier 2 batch 1/5) - Touches:
lib/runtime/props/panel-props.ts(extend types), 4.sveltefiles. ≤200 LOC delta, exactly 4 files (within guardrails after type/file split).
Batch 2 — capability helpers shared adapter (4 files)¶
- Panels: RfFrontEnd, AudioRoutingControl, filter-controls, meter-utils
- Why second: the helpers (
filter-controls.ts,meter-utils.ts) are the trickiest of Cluster A — they're not Svelte components and they feed multiple panels. Doing them together withRfFrontEnd(which needs option lists, not just booleans) andAudioRoutingControl(which usesreceiverLabel) lets one adapter file land all the remaining capability surface. New file:lib/runtime/adapters/capabilities-adapter.ts. - Sub-issue title:
refactor(frontend): extract capabilities-adapter for RF Front End + helpers (Tier 2 batch 2/5)
Batch 3 — radio-state per-panel adapters (4 panels)¶
- Panels: AudioSpectrumPanel, MemoryPanel, AmberTelemetryStrip, VfoControlPanel
- Why third: Cluster B without
AmberScope/AmberCockpit(those also need qsy-history). All four readradio.currentand at most one capability call — small per-panelderive*Props()additions. - Sub-issue title:
refactor(frontend): per-panel radio-state adapters for AudioSpectrum/Memory/AmberTelemetry/VfoControl (Tier 2 batch 3/5)
Batch 4 — LCD chrome + qsy-history (4 panels)¶
- Panels: LcdContrastControl, LcdDisplayModeControl, AmberMemoryStrip, AmberScope
- Why fourth: bundles the trivial leftover panels.
LcdContrastControl+LcdDisplayModeControlneed a newlcd-chrome-adapter.ts.AmberMemoryStripneeds theqsy-history-adapter.ts(read side).AmberScopemigrates its radio-state read at the same time so Cluster B and the qsy-history read side are fully gone after this batch. Two new adapter files. - Sub-issue title:
refactor(frontend): LCD chrome adapter + qsy-history read adapter + AmberScope (Tier 2 batch 4/5)
Batch 5 — AmberCockpit + RxAudioPanel finisher (2 panels)¶
- Panels: AmberCockpit, RxAudioPanel
- Why last:
AmberCockpitis the only "hard" panel (writes qsy-history from a tuning effect; reads radio + multiple capabilities). It needs the qsy-history-adapter write side, plus its own panel adapter. BundlingRxAudioPanel(the trivial Cluster E finisher) lets the batch close out the migration and unblock ESLint tightening (Tier 3). - Sub-issue title:
refactor(frontend): AmberCockpit + RxAudioPanel — close panel→adapter migration (Tier 2 batch 5/5)
Open questions¶
-
Where does LCD chrome state belong long-term? Cluster C stores (
lcd-contrast.svelte.ts,lcd-display-mode.svelte.ts) are skin- local UI state, not radio state. Two answers: (a) keep them inlib/stores/and front them with a thinlcd-chrome-adapter.ts(plan-of-record above); (b) relocate toskins/amber-lcd/state/and let panels import skin-local state directly with a tightened ESLint rule that bans$lib/stores/*but allows$lib/skins/<skin>/state/*. (b) is architecturally cleaner but requires the file move + ESLint config change. Decision punted to whoever owns Batch 4. -
Should
panel-adapters.tskeep growing, or split per cluster? It already has 12 panels' worth of derive/handler exports. Adding AmberScope / AmberCockpit / AmberTelemetryStrip / AudioSpectrum / Memory / VfoControl pushes it past ~25 panels. Suggested split: keeppanel-adapters.tsas a barrel re-export; move actual per-panel functions intolib/runtime/adapters/panels/<panel>.ts. Out of scope for #1240 itself — flag for the planner of Batch 3.