Skip to content

Release v1.2.0#35

Merged
revtex merged 27 commits intomainfrom
dev
Apr 25, 2026
Merged

Release v1.2.0#35
revtex merged 27 commits intomainfrom
dev

Conversation

@revtex
Copy link
Copy Markdown
Owner

@revtex revtex commented Apr 25, 2026

Cuts v1.2.0. See CHANGELOG.md for the full list. Themes: cookie/dual-auth, audio HTTP migration (off WebSocket), service-worker pass-through, backend handler decomposition (internal/api → internal/handler/*), frontend services/hooks/types/slices restructure, raised per-user JWT cap to 20, audio 401 silent retry, MP3 32k default, Options/Users panel polish.

revtex and others added 27 commits April 24, 2026 14:22
…lias (#15)

- Register GET /api/ws (canonical) alongside GET /ws (compat alias).
  Both delegate to the same ws.HandleListenerWS so behavior is identical.
- Frontend listener client now connects to /api/ws.
- Vite dev proxy covers both /api/ws and /ws with WebSocket upgrade.
- Add api_test.TestListenerWSAlias asserting both routes are registered
  and share the same handler.
- Deployment guide reverse-proxy section now lists /api/ws alongside
  /ws and /api/admin/ws as paths needing WS-upgrade forwarding.

The /ws alias is kept for existing Trunk-Recorder / SDRTrunk / rdio-
scanner-shaped clients during the legacy-API transition.
Phase 2 of the directory restructure. The WebSocket layer is now
protocol-only; admin CRUD / config / import-export business logic
lives in a new transport-agnostic internal/admin package.

Changes:

- New internal/admin/ package with Operations struct. Files split by
  feature: users.go, systems.go, talkgroups.go, tags.go, groups.go,
  units.go, api_keys.go, dirmonitors.go, downstreams.go, webhooks.go,
  shared_links.go, settings.go, transcription.go, radioreference.go,
  filesystem.go, imports.go, exports.go. Does not import internal/ws
  or net/http.
- internal/admin/operations.go defines the EventSink interface
  (BroadcastAdminEvent, BroadcastCFG, DisconnectByUser, ClientCount).
  ws.Hub implements it; Operations.New takes it at construction.
- internal/ws/admin_router.go replaces admin_ops.go as the transport
  adapter. The adminOpHandlers map preserves every wire-protocol op
  name (users.list, systems.create, config.update, export.config,
  etc.) byte-identically. Live-state ops (activity.stats,
  activity.chart, logs.query, logs.level, activity.top-talkgroups)
  stay on *Client because they read hub in-memory state.
- internal/ws/admin_ops.go deleted (3,201 lines removed).
- Hub construction unchanged at the call site: NewHub(queries,
  version, HubDeps{...}) still works. HubDeps is now a type alias
  for admin.Deps.
- cmd/server/main.go updated: SensitiveSettingKeys moved from ws to
  admin package.
- Tests: admin_ops_settings_test.go split into
  internal/admin/settings_test.go (CRUD semantics) and
  internal/ws/admin_router_test.go (dispatch + error envelope).

No wire-protocol, auth, or route changes. All frames, error
envelopes, and action names are byte-identical to before.
Default CodeQL setup runs autobuild from the repo root, which cannot
locate the Go module (go.mod lives in backend/), causing 'package could
not be found: github.com/openscanner/openscanner/docs' warnings.

This workflow mirrors ci.yml: installs swag, regenerates
backend/docs/docs.go, then runs 'go build ./...' from backend/ under
CodeQL's manual build mode. Also analyzes JS/TS with build-mode: none.
Module lives in backend/, so setup-go's default cache lookup at the
repo root fails with 'Dependencies file is not found'. Set
cache-dependency-path on all three workflows (codeql, ci, release).
CodeQL (actions/missing-workflow-permissions) flagged ci.yml jobs
as not restricting the default GITHUB_TOKEN scopes. Add a
workflow-level 'permissions: contents: read' block — none of the
CI jobs write to the repo.
…ed handler packages (#18)

refactor(backend): decompose internal/api into feature-scoped handler packages

- handler/{auth,calls,bookmarks,share,setup,health}/... for listener + auth routes
- handler/admin/{imports,radioreference,transcriptions}/... for the admin REST surface
  (11 WS-only admin features remain in internal/admin + internal/ws, already extracted in Phase 2)
- handler/shared/ for swagger DTOs and common helpers
- handler/routes/ owns route registration, including /ws and /api/ws
- internal/api/ removed
- Swag invocation in Makefile + 3 workflows updated to scan internal/handler
- Swagger regenerated
- No route, method, body, header, or middleware changes
Phase 3 moved handlers from internal/api to internal/handler, but the
Dockerfile's 'swag init' call was missed when updating the other
swag invocations (Makefile, ci/codeql/release workflows). Without
'-d cmd/server,internal/handler' swag cannot resolve handler types
like shared.ErrorResponse, breaking the Docker image build.
…ion copy (#19)

- OptionsPanel: remove green 'Active' badge from wired settings; only
  'Planned' badges render now.
- OptionsPanel: reword 'Audio Conversion' description to reflect that
  MP3 and AAC outputs are both supported via the encoding preset.
- seed: default audioEncodingPreset is now mp3_32k (matching the
  dropdown's '(default)' label and audio.ParseEncodingPreset's
  fallback) instead of aac_lc_32k.
- audio/worker.go: move the '(default)' comment marker from
  PresetAACLC32k onto PresetMP3_32k to match the seed and parser.
…o (Phase 4) (#20)

Phase 4 of the directory restructure plan. Pure file moves with
import-path updates; no runtime behaviour change.

- services/wsClient.ts        -> services/ws/client.ts
- services/wsClient.test.ts   -> services/ws/client.test.ts
- services/adminWsClient.ts   -> services/ws/adminClient.ts
- services/audioPlayer.ts     -> services/audio/player.ts
- services/beepPlayer.ts      -> services/audio/beep.ts

services/downloadFilename.ts stays put (not WS, not audio).

All @/services/* import sites across components, hooks, and tests
have been updated to the new paths. tsc --noEmit clean, 188/188
unit tests pass.
#21)

Phase 5 of the directory restructure plan. Pure file moves with
import-path updates and new barrel index files; no runtime behaviour
change.

Moves (15 files, all tracked as renames):
- hooks/{useAuthInit,useTheme,useTokenRefresh,useWebSocket}.* -> hooks/shared/
- hooks/{useScanner,useAudioPlayer,useTGSelectionSync,useActiveUnit}.ts -> hooks/scanner/
- hooks/{useAdminWebSocket,useAdminWsOps,useAdminActivity,useAdminLogs,useWsQuery}.ts -> hooks/admin/

Added barrel index.ts in each subfolder plus hooks/index.ts (root
safety net). All 31 @/hooks/* import sites across components, pages,
hooks, and tests have been updated to specific paths.

tsc --noEmit clean, 188/188 unit tests pass.
…hase 6) (#22)

Phase 6 of the directory restructure plan. The 413-line types/index.ts
god-file is split into seven topic-scoped modules; the original path
becomes a barrel that re-exports everything so all existing @/types
import sites continue to compile unchanged.

New layout:
- types/call.ts     - Call, TranscriptionSegment
- types/config.ts   - SystemConfig, TalkgroupConfig, ScannerConfig
- types/ws.ts       - WsCommand, ConnectionStatus
- types/auth.ts     - LoginResponse, RefreshResponse, ChangePasswordRequest
- types/api.ts      - SetupStatus
- types/admin.ts    - All Admin* DTOs, Capabilities, ConfigResponse,
                      CreateUserPayload, UpdateUserPayload, RR*,
                      SharedLinkAdmin, ServerDirectory*, Transcription*
- types/ui.ts       - AvoidEntry
- types/index.ts    - barrel re-export

No type signatures or behaviour changed. tsc --noEmit clean,
188/188 unit tests pass.
…util, hooks) (#23)

- app/slices/ split into shared/ (authSlice), scanner/ (scannerSlice,
  callsSlice, shareSlice), admin/ (adminSlice, activitySlice)
- components/admin/AdminLayout.tsx inlined into pages/Admin.tsx;
  default export renamed to Admin; test file moved to pages/Admin.test.tsx
- components/admin/NavigationGuardContext.tsx relocated to
  hooks/admin/useNavigationGuard.tsx and added to the hooks/admin barrel
- services/downloadFilename.ts moved to services/util/downloadFilename.ts

All 188 tests pass; tsc --noEmit clean. No runtime behaviour change.
internal/handler/calls/calls.go (1518 LOC) split into:
- calls.go (104) — Handler struct, New(), getLimiter, apiKeyLimiter, consts
- upload.go (833) — PostCallUpload + helpers
- audio.go (113) — GetCallAudio
- search.go (440) — GetCalls
- transcript.go (80) — GetCallTranscript

internal/middleware/middleware.go (392 LOC) split into:
- middleware.go (2) — package doc only
- cors.go (69) — CORS
- auth.go (192) — JWTAuth, OptionalJWTAuth, RequireAdmin, APIKeyAuth, SwaggerCookieAuth
- logging.go (73) — RequestID, Logger, requestLogLevel
- limits.go (83) — RateLimit, MaxBodySize, RateLimitByIP

Same package, same exports, no behaviour change. go vet, go build, go test ./... all pass.
- copilot-instructions.md: add 'Local-only planning docs' section forbidding
  references to docs/plans/* in CHANGELOG, committed docs, commit messages,
  PR descriptions, or code comments
- docs-expert.agent.md: rewrite design-docs section as LOCAL ONLY;
  remove stale file table listing now-ignored plan files
- react-expert.agent.md: soften plan reference to 'local-only design notes'
- CHANGELOG.md: remove two pre-existing references to plan documents
  ('audio-http-migration-plan.md' and 'native-API design plan')
…#25)

- New auth.SetSessionCookie / ClearSessionCookie helpers (HttpOnly,
  Secure when HTTPS, SameSite=Strict, Path=/api)
- POST /api/auth/login and /api/auth/refresh issue os_session alongside
  the existing access JWT response; POST /api/auth/logout clears it
- New middleware.OptionalJWTOrSessionAuth resolves identity from, in
  priority order: bearer header, os_session cookie (guarded by
  Sec-Fetch-Site), anonymous
- GET /api/calls/:id/audio swapped to the new middleware; every other
  route is unchanged. Bearer flow continues to work everywhere.
- Routes.RegisterRoutes now promotes deps.Hub into the WSDisconnecter
  interface only when the concrete pointer is non-nil, fixing a
  pre-existing typed-nil interface footgun on the logout path.

Tests:
- backend/internal/auth/cookie_test.go: SetSessionCookie /
  ClearSessionCookie flag matrix
- backend/internal/handler/routes/auth_test.go: login/refresh/logout
  cookie issuance and rotation
- backend/internal/handler/routes/audio_test.go (new): full dual-auth
  matrix on the audio route — bearer, cookie+same-origin, cookie+missing
  Sec-Fetch-Site, cookie+cross-site (publicAccess on/off), stale cookie
  fallthrough (publicAccess on/off), anonymous (publicAccess on/off)

go vet, go build, go test ./... all clean.
Backwards compatible — no frontend changes.
- NewCALMessage signature changed to (payload) — second []byte parameter
  removed; payload is metadata only, no "audio" field.
- ws.Hub.BroadcastCAL no longer touches the disk: docstring updated to
  reflect that audio is fetched on demand by clients via
  GET /api/calls/:id/audio. The hub already received pre-built bytes,
  so no signature change there.
- handler/calls/upload.go and dirmonitor/watcher.go: removed the
  os.Root + io.LimitReader audio read whose sole consumer was the WS
  payload. Unused 'io' / 'os' imports cleaned up.
- ws/messages_test.go: dropped TestNewCALMessage_WithAudio; updated
  TestNewCALMessage to call the new single-arg signature and added a
  bytes.Contains guard asserting the marshalled frame contains no
  "audio": substring.
- CHANGELOG: bullet under [Unreleased] ### Changed.

Frontend client will be updated in the next PR — current SPA build will
lose live audio playback once this merges; ship the frontend rewrite PR
immediately after.
#27)

- Replace base64-over-WS + Web Audio decode pipeline with a single
  persistent hidden HTMLAudioElement, wired through
  MediaElementAudioSourceNode → GainNode → AudioContext.destination so
  the existing volume slider and gain graph keep working.
- WS client no longer parses or holds an audio callback; on CAL it
  dispatches callReceived(call) to Redux. A new listener middleware
  drives the player off the calls slice.
- Per-call playback flow: audio.src = /api/calls/:id/audio, preload
  auto, play() on canplay, advance queue on ended/error.
- Download buttons in BookmarksPanel and SearchPanel are now plain
  <a download> anchors pointing at the same authenticated endpoint.
- Shared-call page uses <audio src=/api/shared/:token/audio> directly.
- Autoplay-unlock now also primes the persistent <audio> element
  inside the user-gesture handler (play().then(pause)) so subsequent
  programmatic play() calls succeed on Mobile Edge / Mobile Safari.
- Tests updated to drive the new flow; 188 frontend tests pass.

Pairs with backend PR #26 dropping audioData from CAL frames; merge
that one first, then this.
Skip event.respondWith() for /api/calls/:id/audio and
/api/shared/:token/audio so the browser's native Range-request
handling reaches the server. Without this the SW would buffer the
full call body in memory and the <audio> element would lose its
ability to seek.
Update three lines in deployment-guide.md so the WS-vs-HTTP split is
accurate: WebSockets carry CAL metadata (and admin events); audio is
fetched separately from /api/calls/:id/audio and only requires
ordinary HTTPS forwarding.
When a sibling device (phone, tablet, second tab) logs in and pushes
the desktop's access JWT out of the per-user 5-token concurrent cap,
the desktop's session cookie now carries a revoked JWT. The WS
connection survives because it auths once at connect time, but every
subsequent <audio> fetch returns 401 — and unlike RTK Query traffic
those don't go through the auto-refresh path.

Add a recovery hook on the audio player: when the element fires
'error', POST /api/auth/refresh once. The fresh Set-Cookie installs
a new os_session, and we retry the same call. Subsequent failures
on the same item give up and skip to the next, so we can't loop.
Two related fixes for the per-user concurrent JWT cap:

1. Raise auth.MaxRefreshFamilies (and TokenTracker.MaxTokens) from 5
   to 20. With a 15-min access TTL refreshing roughly four times an
   hour, the old limit pushed a desktop's session off the active list
   within an hour of normal multi-device use (desktop + phone). 20
   leaves headroom without inflating the deny list, and the TTL still
   bounds the impact of a stolen refresh family.

2. WebSocket clients (listener and admin) no longer call .close() on a
   socket still in CONNECTING state during a reconnect. Detach the
   handlers and route the close through onopen instead, suppressing
   the cosmetic 'WebSocket is closed before the connection is
   established' browser console warning seen after a token-expiry
   refresh.

Tests updated to derive from auth.MaxRefreshFamilies instead of
hard-coded 5. All Go and frontend tests pass.
…ng builtin (#32)

Silences revive's redefines-builtin-id warning on the inner closure in
the delete-after path. No behaviour change.
The Active badge was removed earlier but the legend still referenced
it. Keep only the Planned hint.
The first user (id=1) is the primary admin and always has access to every system. Disable the Allowed Systems badges in the edit modal (all shown as selected, non-clickable, with explanatory text) and force systemsJson=null on save for editingId===1 so any pre-existing restriction gets cleared.
@revtex revtex merged commit 1d9a095 into main Apr 25, 2026
13 checks passed
@revtex revtex deleted the dev branch April 25, 2026 22:39
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