# StreamHub Qt Client Implementation Plan

> **For agentic workers:** implement task-by-task. Each task ends with a buildable
> deliverable. Build with `cmake -B build && cmake --build build -j` from
> `Client/streamhub-qt/`.

**Goal:** Native Qt5/6 oscilloscope client equivalent to the ImGui
`Client/streamhub/` client, same StreamHub WS protocol.

**Architecture:** Qt Widgets + custom `QPainter` plot widget + `QtWebSockets`.
Reuse `../streamhub/Protocol.{h,cpp}` and `SignalBuffer.h` verbatim. Single GUI
thread; `QWebSocket` signals drive a model; 60 Hz `QTimer` repaints plots.

**Tech Stack:** C++17, Qt5 or Qt6 (autodetect), CMake ≥3.16.

## Global Constraints

- Must compile against Qt5 **and** Qt6 — use `Qt${QT_VERSION_MAJOR}::` targets,
  no version-specific APIs without a guard.
- Do not modify `Client/streamhub/` (reference). Reuse its `Protocol.cpp`,
  `Protocol.h`, `SignalBuffer.h` by include path / direct source compile.
- Domain model uses `QColor` (not `ImVec4`).
- Reuse `LTTBDecimate` and `SignalBuffer` from `SignalBuffer.h`.

---

### Task 1: Project scaffold + CMake autodetect + empty MainWindow

**Files:** Create `CMakeLists.txt`, `main.cpp`, `MainWindow.{h,cpp}`,
`Theme.{h,cpp}`, `Model.h`.

- CMake: `find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets WebSockets)`,
  `find_package(Qt${QT_VERSION_MAJOR} ...)`, `CMAKE_AUTOMOC ON`, C++17. Sources
  include `${CMAKE_CURRENT_SOURCE_DIR}/../streamhub/Protocol.cpp`; include dir
  `../streamhub`. Link `Qt${QT_VERSION_MAJOR}::Widgets`,
  `Qt${QT_VERSION_MAJOR}::WebSockets`.
- `Model.h`: `SignalView{SignalMeta meta; SignalBuffer buf; QColor color; double
  lineWidth; int marker; bool visible;}`, `Source`, `VScale`, `PlotAssignment`,
  `PlotLayout` enum + `layoutDims`/`layoutNames`/`layoutCount`.
- `Theme`: apply Catppuccin-Mocha `QPalette` + stylesheet; `tracePalette(i)`.
- `main.cpp`: parse `-host`/`-port`, build `QApplication`, apply theme, show
  `MainWindow`.
- **Deliverable:** builds, opens an empty dark window.

### Task 2: WsClient + Hub controller (data model + WS dispatch)

**Files:** Create `WsClient.{h,cpp}`, `Hub.{h,cpp}`.

- `WsClient`: wraps `QWebSocket`; `connectTo(host,port)`, `reconnect`, `sendText`;
  3 s reconnect `QTimer`; signals `connectedChanged(bool)`,
  `textReceived(QString)`, `binaryReceived(QByteArray)`.
- `Hub`: owns `WsClient` + `std::vector<Source>` + `TriggerCfgState` +
  `CaptureFrame` + per-plot zoom caches + `HistoryInfoMsg`. On connect sends
  `getSources/getStats/historyInfo`. Routes text via `ParseType` to
  `onSources/onConfig/onStats/onTriggerState/onZoom/onHistoryZoom/onHistoryInfo/`
  `onMaxPointsUpdated`; binary via first byte (v2 capture vs v1 push). Command
  senders mirror `Build*` in `Protocol.h`. Emits signals listed in the spec.
  Holds a `findSource/findSignal/slotKey`. Zoom caches keyed by plot index
  (`requestZoom`, `requestHistoryZoom`, `nextZoomReqId`).
- **Deliverable:** builds; connecting to a hub logs received message types
  (temporary `qDebug`), model populates (verified via a debug dump).

### Task 3: PlotWidget — static render + live scroll

**Files:** Create `PlotWidget.{h,cpp}`.

- `PlotWidget(Hub*, int plotIdx, QWidget*)`. `paintEvent`: draw frame, grid,
  ±4-div Y axis with 9 ticks, X axis with 10 ticks. Live mode: X =
  `[wallNow-windowSec, wallNow]`. For each assigned slot: read ring
  (`buf.readLast(maxPoints)`), clip to visible X, `LTTBDecimate` to 2400,
  normalize (normal/digital/mixed via ported `resolveVScale`/`normalizeY`/
  `bandNormalize`), map to pixels, `drawPolyline` in trace color/width; optional
  markers. Badge row painted at top (trace name + V/div).
- **Deliverable:** dropping handled later; for now a hard-coded assignment shows
  a live trace scrolling against wall clock.

### Task 4: PlotWidget — interaction (zoom/pan/cursors/active/vmode) + drag-drop

**Files:** Modify `PlotWidget.{h,cpp}`; add small per-plot toolbar (Live/Back/
Fit, N/D/M, V-scale controls) as child widgets or painted buttons.

- Mouse: wheel = Y zoom active trace (X zoom if none); Ctrl+wheel = X
  zoom/window; Shift+wheel = Y pan; right-drag = X pan (live→non-live); box-zoom
  optional. Click a badge = toggle active slot; context menu on badge = color/
  width/marker/digital-in-mixed/remove. Zoom history Back/Fit/Live.
- Global A/B cursors (drawn + draggable) with readouts; pause snapshot.
- `dragEnterEvent`/`dropEvent` accept a signal-ref mime → append assignment.
- Hi-res WS zoom + history zoom requests through `Hub` (live-narrow throttle,
  non-live debounce) and prefer cached hi-res/history data when it covers view.
- **Deliverable:** full single-plot interaction parity.

### Task 5: PlotGrid + SourceSidebar + MainWindow toolbar

**Files:** Create `PlotGrid.{h,cpp}`, `SourceSidebar.{h,cpp}`; expand
`MainWindow.{h,cpp}`.

- `PlotGrid`: nested `QSplitter`s realizing each `PlotLayout`; rebuild on change.
- `SourceSidebar`: `QTreeWidget` sources→signals; drag a signal (mime with
  src/sig index); "Add source" + remove; reflects `Hub` signals.
- MainWindow toolbar: sidebar toggle, layout picker (menu of the 8 layouts),
  pause, cursors toggle, trigger-bar toggle, history-bar toggle, window-preset
  combo, stats button, connection (host/port + reconnect), status LED.
- Add-source dialog (label/host/port/multicast/dataPort) → `Hub::addSource`.
- **Deliverable:** drag-drop from sidebar to any layout cell works end to end.

### Task 6: TriggerBar + StatsDialog + HistoryBar

**Files:** Create `TriggerBar.{h,cpp}`, `StatsDialog.{h,cpp}`,
`HistoryBar.{h,cpp}`; wire into `MainWindow`.

- `TriggerBar`: signal combo (full keys incl array index), edge combo, threshold
  spin, window combo (100 µs…10 s), pre% slider, normal/single → `setTrigger`;
  Arm/Disarm/Rearm/Stop; badge from `triggerStateChanged`. Capture frame routes
  to plots' trigger view.
- `StatsDialog`: table of metrics per source + custom 20-bin histogram widget
  (`QPainter` bars), refreshed on `statsChanged`.
- `HistoryBar`: Live, range readout, pan ←/→, jump presets, All; drives plots'
  non-live X range + history-zoom.
- **Deliverable:** trigger capture, stats histogram, history browse all match
  the ImGui client.

### Task 7: Build, run, verify, document

- Clean Release build on Qt6 (and confirm Qt5 configure path).
- Launch against a live hub; verify the spec's parity checklist.
- Add a `Client/streamhub-qt` build note to `CLAUDE.md` and `ARCHITECTURE.md`.
- **Deliverable:** working client + docs.
