Skip to content

Release v1.3.0#42

Merged
revtex merged 10 commits intomainfrom
dev
Apr 29, 2026
Merged

Release v1.3.0#42
revtex merged 10 commits intomainfrom
dev

Conversation

@revtex
Copy link
Copy Markdown
Owner

@revtex revtex commented Apr 29, 2026

Cuts v1.3.0 with the native /api/v1/* REST + WebSocket surface and follow-up fixes.

Highlights

Added

  • Native /api/v1/* REST surface with structured error envelope ({error:{code,message,details}}) and stable string codes.
  • v1 call upload (POST /api/v1/calls) with native multipart field names and RFC 3339 startedAt enforcement.
  • v1 listener, calls, share, bookmark, setup, health, and auth endpoints.
  • v1 admin endpoints for imports, RadioReference preview, transcription status, Swagger session.
  • Native JSON-object framed WebSocket protocol on /api/v1/ws/{listener,admin}.
  • RFC 8594 deprecation headers on every legacy /api/* route.
  • Admin endpoint /api/v1/admin/legacy-usage + dashboard banner surfacing 24-hour legacy-API usage.

Changed

  • Frontend now uses /api/v1/* for every REST and WS interaction.
  • v1 upload routes accept only Authorization: Bearer; legacy headers/query/form key methods stay on legacy routes.
  • Swagger UI documents every native v1 endpoint, not just the three previously annotated handlers.

Fixed

  • Swagger UI session cookie now scoped to /api so it's sent on the v1 docs URL the admin tools panel opens.
  • Default audioEncodingPreset matches the UI default (mp3_32k).
  • Audio playback recovers silently from 401 on /api/calls/:id/audio.
  • Lock primary admin's Allowed Systems selector in the user editor.

See CHANGELOG.md for the full list.

revtex and others added 10 commits April 26, 2026 18:19
test(api): freeze legacy wire contract before native API split

Phase N-0 of the Native API plan: adds table-driven regression tests pinning the current REST and WebSocket wire shapes so the upcoming /api/v1/* work cannot drift the legacy surface accidentally.

REST contract tests cover: /api/call-upload (all three legacy API-key transports: X-API-Key header, ?key= query, key= form), /api/trunk-recorder-call-upload alias, test=1 connectivity check, /api/calls envelope, /api/calls/:id/audio, /api/calls/:id/transcript, /api/calls/:id/share CRUD, /api/shared/:token{,/audio}, /api/bookmarks{,/calls,POST}, /api/auth/{login,refresh,logout,password,me,tg-selection} including os_session/refresh_token cookie issuance and clearance, /api/admin/import/{talkgroups,units,groups,tags}, /api/admin/radioreference/preview/csv, /api/admin/transcriptions/status.

WS contract pins byte-exact JSON for every server-emitted legacy command constructor in internal/ws (CAL/CFG/VER/LSC/XPR/MAX/LFM/TRN, ADM_RES, ADM_RES error) plus a structural pin for ADM_EVT (volatile timestamp asserted as positive int64).

Tests-only; no production code changes. Skips CHANGELOG per project policy for pure internal refactor / regression-test additions.
Introduces the native API surface alongside the existing legacy routes,
without breaking any current consumer.

Routing & middleware:
- New /api/v1 route group with V1Marker() (sets apiVersion=v1 in the
  gin context) and V1ErrorEnvelope() (rewrites legacy {"error":"..."}
  responses into the native {"error":{"code","message","details"}}
  envelope; already-native and 2xx responses pass through unchanged).
- shared.WriteAPIError + APIError/APIErrorResponse types with stable
  string codes (validation_failed, unauthorized, forbidden, not_found,
  conflict, unprocessable, rate_limited, internal). 5xx envelopes
  inject details.requestId from the request-id middleware.

Auth:
- APIKeyAuth on v1 paths accepts ONLY Authorization: Bearer <api-key>;
  legacy paths keep accepting X-API-Key, ?key=, and form key=.
- JWT-shaped Bearer values on v1 API-key routes are rejected with the
  invalid_credentials envelope so clients surface the right error.

Endpoints:
- POST /api/v1/calls — native upload with field names systemId,
  talkgroupId, startedAt (RFC 3339 only — unix timestamps rejected),
  frequencyHz, durationMs, unitId. POST /api/v1/calls/test returns 204.
- Listener: GET/PUT /api/v1/listener/tg-selection (renamed from
  /api/auth/tg-selection), plus calls list/audio/transcript, share,
  bookmarks, and unauth health/setup/auth endpoints.
- Admin: /api/v1/admin/{import/*, radioreference/preview (no /csv
  suffix), transcriptions/status, docs/session}, all JWT+admin gated.

Tests: shared/errors_test.go and calls/v1_test.go cover the envelope
shape and v1-specific upload validation (including unix-startedAt
rejection).
Implements the native JSON-object framed WebSocket protocol alongside
the existing legacy 3-letter array-framed protocol on /ws, /api/ws,
and /api/admin/ws. The legacy paths are unchanged and remain
byte-identical (Phase N-0 contract tests still green).

Backend:
- New JSON message constructors in internal/ws/messages_v1.go for every
  legacy command: connection.welcome (VER), scanner.config (CFG),
  call.new (CAL), call.transcript (TRN), listener.count (LSC),
  listener.feedMap.snapshot/update (LFM), session.expired (XPR),
  connection.rejected (MAX), admin.event (ADM_EVT), admin.request
  (ADM_REQ in), admin.response (ADM_RES out).
- Per-client protocol-version field; the hub stays single-implementation
  and the per-client encoder picks legacy vs. v1 at connect time.
- Routes /api/v1/ws/listener and /api/v1/ws/admin registered on the
  root router (NOT the v1 group) — V1ErrorEnvelope buffers HTTP bodies
  and would corrupt the WebSocket upgrade.
- admin.response error envelope mirrors the REST error envelope
  ({code,message,details?}) so clients can share a discriminator.

Frontend:
- Listener WS client connects to /api/v1/ws/listener; admin WS client
  connects to /api/v1/ws/admin.
- Discriminated-union types over the native message set; dispatch
  switches on msg.type instead of array position.
- Outbound listener.feedMap.update and admin.request emit JSON objects.

Tests: messages_v1_test.go (13 tests) shape/byte-pin every native
constructor and exercise listener+admin v1 handlers end-to-end through
a real httptest server. Existing legacy contract tests
(messages_test.go, legacy_contract_test.go) remain green.
Adds RFC 8594 deprecation headers, structured per-request warn logs, and
an admin dashboard banner listing API keys that still hit the legacy
surface. No legacy behaviour changed beyond the additive headers.

Backend:
- middleware.Deprecated(successor, sunset) attaches Deprecation: true,
  Sunset, Link rel=successor-version, and Cache-Control: no-store to
  every legacy /api/* and legacy WS route.
- Per-request slog.Warn("legacy endpoint hit", method, path,
  apiKeyIdent) — apiKeyIdent is the truncated identifier (set by
  APIKeyAuth alongside apiKeyID), never the raw key.
- New /api/v1/admin/legacy-usage endpoint returning a 24h aggregate
  ({method, path, apiKeyIdent, count, lastSeen}) backed by an in-memory
  ring buffer; no schema change.

Frontend:
- LegacyUsageBanner reads /api/v1/admin/legacy-usage on the admin
  dashboard, polls every 60s, dismissable per session via
  sessionStorage. Expandable details table with method, path, API key,
  count, last-seen relative time.

Tests: middleware/deprecation_test.go covers header emission and the
ring buffer; legacyusage/handler_test.go covers the aggregate endpoint;
LegacyUsageBanner.test.tsx covers loading, empty, populated, and
dismissed states.
* feat(frontend): migrate REST/audio/SW to /api/v1

The frontend has been carrying legacy /api/* deprecation traffic since
the native v1 surface landed. Move every client-side caller over to v1
so production logs stop showing self-inflicted hits in the legacy-usage
report:

- RTK Query base URL flips to /api/v1
- /api/v1/listener/tg-selection (was /api/auth/tg-selection)
- /api/v1/admin/radioreference/preview (was /admin/radioreference/preview/csv)
- /api/v1/admin/legacy-usage (was /v1/admin/legacy-usage relative to /api)
- raw fetch() in main.tsx auth-recovery and ToolsPanel Swagger session
- audio download URLs in player.ts, BookmarksPanel, SearchPanel
- service worker passthrough regex accepts both /api/v1/{calls,shared}
  and the legacy variants during the transition window
- vite dev proxy forwards /api/v1/ws WebSocket upgrades

Legacy /api/* routes remain available with deprecation headers for
external consumers; this only moves the embedded frontend.

All 201 vitest specs pass. tsc clean. Backend unchanged.

* fix(frontend): move no-control-regex disable to the right line

The directive was on the const declaration; ESLint reports it unused
because the actual regex literal sits on the next line. Move the
comment onto the .replace() call that owns the control-class regex.
- frontend/.npmrc: move virtual-store-dir to ~/.cache/pnpm-vstore so it
  survives /tmp wipes on container/codespace restarts (9p workspace mount
  still cannot host the vstore — copy mode + overlay path keeps both
  constraints satisfied).
- .devcontainer/post-create.sh: pre-create the cache dir on fresh containers.
- backend/internal/ws/client.go: drop dead buildCFGMessage helper
  (callers use buildCFGFrames).
The os_swagger session cookie was scoped to /api/admin/docs, but the
admin Tools panel opens Swagger UI at /api/v1/admin/docs/index.html.
The browser refused to send the cookie for that path so the docs
middleware rejected the request with 'swagger session required'.

Widen the cookie path to /api so it's delivered on both the legacy
/api/admin/docs/* and v1 /api/v1/admin/docs/* routes.
frontend/.npmrc pinned virtual-store-dir to /home/vscode/.cache/...
which exists in the dev container but not on GitHub Actions runners,
breaking 'pnpm install --frozen-lockfile' with EACCES on /home/vscode.

Move the override to the dev container's containerEnv as
NPM_CONFIG_VIRTUAL_STORE_DIR so it only applies inside the container.
CI runners now use pnpm's default node_modules/.pnpm location.
Swagger UI previously showed only POST /v1/calls, POST /v1/calls/test,
and GET /v1/admin/legacy-usage because the rest of the v1 routes — auth,
listener, setup, health, bookmarks, share, calls read paths, admin
imports, RadioReference preview, transcription status, docs session —
inherited only the legacy /api/* annotations from their handlers.

Add a comma-separated v1-* tag and a second @router /v1/... line to
each shared handler's swag block. swag emits both the legacy and v1
operations from one godoc block, so each appears under both tag
sections in Swagger UI. Pure documentation change; routing and
behavior unchanged. The legacy annotations will be removed in a
future cleanup pass after the legacy routes are retired.
@revtex revtex merged commit e70f3e4 into main Apr 29, 2026
13 checks passed
@revtex revtex deleted the dev branch April 29, 2026 00:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant