Conversation
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.