# Metering WS-1 Implementation Plan

Status: Draft  
Scope: Minimal WebSocket live-telemetry path for metering in `fm-rds-tx`

## 1. Goal

Add the smallest practical live metering transport on top of the existing snapshot system.

The target is:
- keep `GET /measurements`
- add `GET /ws/telemetry`
- stream only `measurement`
- use the same underlying measurement truth as `/measurements`
- keep the realtime path safe
- make the browser prefer WS, but fall back to snapshot polling

This is WS-1, not the final telemetry system.

---

## 2. Existing Pieces We Already Have

The current codebase already provides the most important semantic building blocks:

- `internal/offline/generator.go`
  - produces `MeasurementSnapshot`
  - stores `latestMeasurement`
- `internal/app/engine.go`
  - carries measurement in `EngineStats`
- `internal/control/control.go`
  - exposes `GET /measurements`
- `internal/control/ui.html`
  - already consumes `/measurements`
  - already contains browser-side meter smoothing / peak-hold style logic

That means WS-1 does not need a new meter model.
It only needs a new live transport path.

---

## 3. Hard Rules

## 3.1 Single source of truth

There must be only one latest measurement truth:
- generator/engine produces the snapshot
- `/measurements` exposes it
- WebSocket streams it

WebSocket must not introduce a second, UI-specific measurement calculation path.

## 3.2 Realtime safety

The producer path must never block on telemetry.

Allowed on the realtime side:
- reading latest measurement pointer
- comparing sequence numbers
- one non-blocking publish step

Forbidden on the realtime side:
- JSON encoding
- HTTP work
- WebSocket writes
- blocking channels
- slow shared locks

## 3.3 WS-1 stays small

WS-1 should include only:
- one endpoint: `GET /ws/telemetry`
- one message class: `measurement`
- one small broadcaster/hub
- one bounded queue per client
- one drop policy: drop old, keep newest
- browser snapshot bootstrap + WS preference

No topics, bundles, runtime-event multiplexing, or speculative protocol machinery.

---

## 4. Proposed Files

## New file
### `internal/control/telemetry.go`

Contains:
- `TelemetryMessage`
- `TelemetryHub`
- subscriber management
- non-blocking publish logic

## Modified files
### `internal/control/control.go`
- add telemetry hub to `Server`
- add `GET /ws/telemetry`
- add WS handler

### `internal/app/engine.go`
- add optional measurement publisher hook
- publish new measurement snapshots when sequence advances

### `cmd/fmrtx/main.go`
- wire engine publisher to control telemetry hub

### `internal/control/ui.html`
- add `connectTelemetryWS()`
- bootstrap from `/measurements`
- prefer WS while connected
- fall back to polling when disconnected

---

## 5. Message Shape

WS-1 should use a minimal typed envelope.

Example:

```json
{
  "type": "measurement",
  "ts": "2026-04-13T07:00:53.842Z",
  "seq": 128,
  "data": {
    "timestamp": "2026-04-13T07:00:53.842Z",
    "sampleRateHz": 228000,
    "chunkSamples": 11400,
    "chunkDurationMs": 50,
    "sequence": 128,
    "flags": {
      "stereoEnabled": true,
      "stereoMode": "DSB"
    },
    "lrPreEncodePostWatermark": {
      "lRms": 0.27,
      "rRms": 0.27,
      "lPeakAbs": 0.51,
      "rPeakAbs": 0.51
    },
    "compositeFinalPreIq": {
      "peakAbs": 0.63,
      "pilotInjectionEquivalentPercent": 9.0
    }
  }
}
```

Rule:
- `data` should be semantically identical to `/measurements.measurement`
- envelope adds only:
  - `type`
  - `ts`
  - `seq`

---

## 6. TelemetryHub Design

## 6.1 Minimal responsibilities

The hub should:
- accept published measurement snapshots
- fan them out to connected clients
- use bounded per-client queues
- never block the publisher

## 6.2 Minimal internal shape

A small subscriber model is enough for WS-1.

Conceptually:

```go
type TelemetryMessage struct {
    Type string      `json:"type"`
    TS   time.Time   `json:"ts"`
    Seq  uint64      `json:"seq"`
    Data interface{} `json:"data"`
}

type telemetrySubscriber struct {
    ch chan TelemetryMessage
}

type TelemetryHub struct {
    mu sync.Mutex
    subscribers map[*telemetrySubscriber]struct{}
}
```

## 6.3 Publish policy

Per client:
- channel size should be tiny, e.g. `1` or `2`
- if the channel is full:
  - discard older unsent frame
  - keep newest

In short:
- latest state wins
- producer never blocks

---

## 7. Preferred Publish Path

The realtime path should not write WebSocket frames directly.

Preferred path:
- generator finalizes latest measurement snapshot
- engine notices a new sequence
- engine calls a small measurement publisher hook
- hub receives the snapshot non-blockingly
- WS clients receive the transport copy later

That keeps transport outside the hot path.

---

## 8. Engine Integration

## 8.1 Publisher hook

Add a small hook to `Engine`, conceptually like:

```go
SetMeasurementPublisher(func(*offpkg.MeasurementSnapshot))
```

The engine should publish only when:
- a new snapshot exists
- the sequence advanced

## 8.2 Why the engine is a good handoff point

The generator is still the source of truth.

But the engine is a good place to bridge from:
- chunk production
- to control-plane telemetry transport

because it already sits between signal generation and external control/runtime reporting.

---

## 9. Initial Snapshot on Subscribe

On WebSocket connect, the server should:
1. upgrade the connection
2. subscribe the client
3. immediately send the latest known measurement snapshot if one exists
4. continue streaming subsequent updates

This avoids the UI showing an empty state until the next natural update arrives.

---

## 10. WebSocket Handler Behavior

## Endpoint
- `GET /ws/telemetry`

## Handler rules
- upgrade connection
- create subscriber
- send latest known snapshot immediately if available
- loop over subscriber channel
- write typed messages with write deadlines
- on write failure or broken client:
  - close connection
  - unsubscribe

Additional safety rule:
- a persistently slow or broken client may be dropped rather than buffered around

---

## 11. UI Behavior

The browser flow should be:

1. load snapshot from `GET /measurements`
2. render immediately from snapshot
3. connect to WebSocket
4. if WS is healthy, prefer streamed updates
5. if WS drops, keep last known state and resume snapshot polling

## UI state additions
Conceptually:

```js
S.telemetry = {
  ws: null,
  wsConnected: false,
  wsRetryTimer: null,
  snapshotPollingActive: true
}
```

---

## 12. Polling Strategy with WS

Current UI behavior polls `/measurements` aggressively.

WS-1 should change that to:

### While WS is healthy
- no continuous `/measurements` polling
- keep `/runtime` and `/config` polling as needed

### While WS is disconnected
- resume `/measurements` polling
- lower fallback rate is acceptable

This preserves `/measurements` without turning it into a redundant live-stream duplicate.

---

## 13. Observability

WS-1 logging should stay minimal.

Useful:
- client connected
- client disconnected
- client dropped due to write failure/backpressure

Not useful:
- logging every telemetry frame

---

## 14. Risks

### A. Too much logic in the producer path
Avoid.

### B. Slow client causing sticky WS writes
Use deadlines and disconnect.

### C. Drift between `/measurements` and WS
Prevent by using the same underlying snapshot source.

### D. Overengineering WS-1
Avoid topics, bundles, and speculative protocol work.

---

## 15. Implementation Sequence

### Step 1
Create `internal/control/telemetry.go` with:
- `TelemetryMessage`
- `TelemetryHub`
- subscribe/unsubscribe/publish

### Step 2
Add telemetry hub ownership to `Server` in `internal/control/control.go`.

### Step 3
Add `GET /ws/telemetry` handler in control.

### Step 4
Add measurement publisher hook to `Engine`.

### Step 5
Wire engine publisher to telemetry hub in `cmd/fmrtx/main.go`.

### Step 6
Update `internal/control/ui.html`:
- snapshot bootstrap
- WS connect
- WS preference
- fallback polling

### Step 7
Test behavior manually:
- no-data case
- connect while TX idle
- connect while TX running
- disconnect/reconnect
- fallback back to polling
- slow/broken client handling

---

## 16. Recommendation

Implement WS-1 in the smallest possible form.

That means:
- keep `/measurements`
- add one WS endpoint
- stream one message type
- keep one truth
- protect the realtime path
- let freshness win over completeness

That gives `fm-rds-tx` a real live-metering transport backbone without turning the first step into a giant telemetry framework.
