Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
12db9f6
feat(api): add /api/ws canonical listener route; keep /ws as compat a…
revtex Apr 24, 2026
3a8e4c8
refactor(backend): extract admin CRUD into internal/admin package (#16)
revtex Apr 24, 2026
5cc5d04
ci: add custom CodeQL workflow for backend/ Go module (#17)
revtex Apr 24, 2026
8ff74f7
ci: allow manual CodeQL runs via workflow_dispatch
revtex Apr 24, 2026
129b3bb
ci: bump codeql-action v3 -> v4 (Node 24)
revtex Apr 24, 2026
2eacfa9
ci: point setup-go cache at backend/go.sum
revtex Apr 24, 2026
3f8cb6f
ci: pin GITHUB_TOKEN to read-only in CI workflow
revtex Apr 24, 2026
9dff28f
refactor(backend): Phase 3 — decompose internal/api into feature-scop…
revtex Apr 25, 2026
9eebbce
fix(docker): update swag invocation to scan internal/handler
revtex Apr 25, 2026
2622909
fix(admin): drop Active badge, fix MP3 default, correct audio-convers…
revtex Apr 25, 2026
9204409
refactor(frontend): group services into services/ws and services/audi…
revtex Apr 25, 2026
4af92e6
refactor(frontend): split hooks/ into shared, scanner, admin (Phase 5…
revtex Apr 25, 2026
d9b1c95
refactor(frontend): split types/index.ts into topic-scoped modules (P…
revtex Apr 25, 2026
3a54c8f
refactor(frontend): post-Phase-6 layout polish (slices, pages/Admin, …
revtex Apr 25, 2026
27a658b
refactor(backend): split calls.go and middleware.go by concern (#24)
revtex Apr 25, 2026
d1ab971
chore: mark docs/plans/ as local-only in agent instructions
revtex Apr 25, 2026
6f49a8a
feat(auth): add os_session cookie + dual-auth on /api/calls/:id/audio…
revtex Apr 25, 2026
0ef1aff
feat(ws): drop embedded audio from CAL messages (#26)
revtex Apr 25, 2026
37619be
feat(audio): switch playback to <audio src=…> via /api/calls/:id/audi…
revtex Apr 25, 2026
b2c1a90
feat(sw): pass audio fetches through to the network (#28)
revtex Apr 25, 2026
b5c83e5
docs: clarify deployment notes after audio HTTP migration (#29)
revtex Apr 25, 2026
cfbdcb2
fix(audio): silently retry on 401 from /api/calls/:id/audio (#30)
revtex Apr 25, 2026
17ba0bf
fix(auth): raise per-user JWT cap to 20; quiet WS reconnect race (#31)
revtex Apr 25, 2026
3c9f3a4
chore(dirmonitor): rename local 'real' to 'realPath' to avoid shadowi…
revtex Apr 25, 2026
a4495d6
fix(admin): drop stale 'Active = in use' from Options legend (#33)
revtex Apr 25, 2026
115f8f1
fix(admin): lock primary admin's Allowed Systems to all (#34)
revtex Apr 25, 2026
1cd16ca
chore(release): cut v1.2.0
revtex Apr 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 15 additions & 21 deletions .github/agents/docs-expert.agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,16 @@ These are **instructional**, written for operators and end users — not for con
- Screenshots are fine to reference by filename but are not required; text must stand alone.
- Accuracy is non-negotiable. Paths, flag names, env var names, URLs, and ports must match the code exactly. If the code says `--listen`, the doc says `--listen`, not `--address`.

### Design docs and specs (`docs/plans/*.md`)
### Design docs and specs (`docs/plans/*.md`) — LOCAL ONLY

These are for contributors and maintainers. Use technical language, reference code paths, include Mermaid diagrams, cite file paths with line numbers. The audience rules above do **not** apply here.
The `docs/plans/` directory is **gitignored**. Files there are local-only working notes used while implementing a feature; they are never committed and never visible to anyone other than the author.

Rules:

- Place new design docs / specs / phase plans under `docs/plans/` only when the user explicitly asks for a plan. Do not stage or commit them.
- **Never reference `docs/plans/*` paths from tracked files** (CHANGELOG, committed docs, commit messages, PR descriptions, code comments). The link would 404 for everyone else.
- If you encounter an existing tracked file that links into `docs/plans/`, treat that link as a bug and remove it.
- Inside a plan doc itself, technical language, code paths, Mermaid diagrams, and line-number citations are fine — the audience is just you and the user.

### Quick check before submitting a user guide

Expand All @@ -57,25 +64,12 @@ These are for contributors and maintainers. Use technical language, reference co

## Doc Files

| File | Purpose |
| -------------------------- | -------------------------------------------------------------------- |
| `docs/admin-guide.md` | UI walkthrough for the admin dashboard |
| `docs/deployment-guide.md` | Bare metal, Docker, reverse proxy, Let's Encrypt, secrets encryption |
| `docs/recorder-guide.md` | Per-recorder setup instructions |
| `docs/plans/` | Design plans and specs (architecture, API, etc.) |

### Plans Directory (`docs/plans/`)

| File | Purpose |
| --------------------------------------- | ------------------------------------------------- |
| `docs/plans/plan.md` | Master project plan and UI design spec |
| `docs/plans/architecture.md` | System diagram, component descriptions, data flow |
| `docs/plans/api.md` | Full API endpoint reference |
| `docs/plans/recorder-integration.md` | Recorder integration design |
| `docs/plans/transcription.md` | Transcription feature design (go-whisper) |
| `docs/plans/refresh-token-auth-plan.md` | Refresh token auth flow design |
| `docs/plans/security-hardening-plan.md` | Security hardening roadmap |
| Other plan files | Feature-specific implementation plans |
| File | Purpose |
| -------------------------- | ----------------------------------------------------------------------------- |
| `docs/admin-guide.md` | UI walkthrough for the admin dashboard |
| `docs/deployment-guide.md` | Bare metal, Docker, reverse proxy, Let's Encrypt, secrets encryption |
| `docs/recorder-guide.md` | Per-recorder setup instructions |
| `docs/plans/` | **LOCAL ONLY** — gitignored working notes; never reference from tracked files |

## Key Diagrams to Maintain

Expand Down
2 changes: 1 addition & 1 deletion .github/agents/react-expert.agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ frontend/

## UI Design Principles

Full visual specification with ASCII wireframes, color palette, component mapping, responsive breakpoints, and animations is in `docs/plans/plan.md` § "Web UI Design". Key points:
Local-only design notes (in the gitignored `docs/plans/` working directory) may contain extended ASCII wireframes and palette spec. The canonical, in-repo summary follows. Key points:

- **Dark-first** — custom DaisyUI `openscanner` theme; `base-100` (#121212), `base-200` (#1e1e1e), `base-300` (#2d2d2d), `primary` (#00e676 green), `secondary` (#ff9100 orange), `error` (#ff1744 red)
- **Scanner page** — vertically-stacked single column, max-width 640px, 24px padding:
Expand Down
2 changes: 1 addition & 1 deletion .github/agents/reviewer.agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ This reviewer covers the whole application. Sections below map to subsystems:
### A07 — Identification & Authentication Failures

- [ ] Login rate limiter: 3 failures → 10-minute lockout per IP
- [ ] Max 5 concurrent JWT tokens per user enforced (oldest invalidated on 6th login)
- [ ] Max `auth.MaxRefreshFamilies` (20) concurrent JWT tokens per user enforced (oldest invalidated on overflow)
- [ ] JWT tokens are invalidated on logout (server-side token tracker)
- [ ] Refresh token family rotation: reuse of an old token revokes the entire family
- [ ] Refresh cookie flags: `HttpOnly`, `Secure` in production, `SameSite=Lax`
Expand Down
18 changes: 16 additions & 2 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ Detailed conventions live in the individual agent files. The non-negotiables for
12. Secrets at rest (downstream API keys, VAPID private key, webhook secrets) encrypted with AES-256-GCM using the `enc::` prefix; startup fails fast on missing/wrong encryption key
13. Refresh tokens stored as SHA-256 hashes with family rotation; reuse revokes the entire family; delivered in httpOnly/Secure/SameSite=Lax cookies
14. All outbound HTTP (downstream, webhooks, push) uses `safehttp.Client` — redirects disabled, timeouts enforced, response body capped. Private-address blocking is opt-in via `OPENSCANNER_BLOCK_INTERNAL_HTTP=1` (default allows LAN/loopback because OpenScanner is a self-hosted homelab tool)
15. Max 5 concurrent JWT tokens per user; 3-strike lockout (10 min) on login; hourly cleanup goroutine for expired refresh tokens
15. Max 20 concurrent JWT tokens per user (`auth.MaxRefreshFamilies`); 3-strike lockout (10 min) on login; hourly cleanup goroutine for expired refresh tokens

## Tooling Conventions

Expand All @@ -117,13 +117,27 @@ Detailed conventions live in the individual agent files. The non-negotiables for
- Do not commit or push unless the user explicitly asks
- Do not delete files as a shortcut; if a file looks unfamiliar, read it first

## Local-only planning docs (`docs/plans/`)

- The entire `docs/plans/` directory is **gitignored** (see [.gitignore](../.gitignore)). Files there are local working notes only.
- **Never** mention plan files, plan filenames, or contents of `docs/plans/*` in:
- `CHANGELOG.md`
- committed docs (`docs/*.md` outside `docs/plans/`)
- commit messages
- PR titles or PR descriptions
- code comments
- any other tracked file
- Treat plan docs the way you'd treat a personal scratchpad: useful while working, invisible to anyone reading the repository.
- When asked to write a plan, place it under `docs/plans/` and do not stage or commit it. Do not link to it from tracked files (the link would 404 for everyone else).
- If you find an existing tracked file that links into `docs/plans/`, treat that link as a bug and remove the reference.

## Changelog

- User-visible changes (new features, fixes, config/schema changes, security patches) **must** add a bullet under the `[Unreleased]` section of `CHANGELOG.md` in the same PR
- Group bullets under `### Added`, `### Changed`, `### Fixed`, `### Security`, `### Removed`, or `### Deprecated` (Keep a Changelog format)
- Pure internal refactors, CI-only tweaks, and typo fixes can skip the CHANGELOG — the PR should be labeled `skip-changelog`
- The `changelog` CI job blocks merges into `main` when `CHANGELOG.md` wasn't touched and the label isn't applied
- Full release process: [docs/plans/release-guide.md](../docs/plans/release-guide.md)
- CHANGELOG bullets describe **what changed in the product**, never **what plan was followed**. Don't reference plan filenames or phase numbers from `docs/plans/`.

## Releases

Expand Down
6 changes: 5 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ on:
pull_request:
branches: [main, dev]

permissions:
contents: read

jobs:
changelog:
name: CHANGELOG updated
Expand Down Expand Up @@ -35,11 +38,12 @@ jobs:
- uses: actions/setup-go@v6
with:
go-version: "1.25"
cache-dependency-path: backend/go.sum
- name: Install swag
run: go install github.com/swaggo/swag/cmd/swag@latest
- name: Generate Swagger docs
working-directory: backend
run: swag init -d cmd/server,internal/api -g main.go --parseDependency --parseInternal -o docs
run: swag init -d cmd/server,internal/handler -g main.go --parseDependency --parseInternal -o docs
- name: Test
run: cd backend && go test ./...

Expand Down
67 changes: 67 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: CodeQL

on:
push:
branches: [main, dev]
pull_request:
branches: [main, dev]
schedule:
- cron: "23 5 * * 1"
workflow_dispatch:

jobs:
analyze:
name: Analyze (${{ matrix.language }})
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write

strategy:
fail-fast: false
matrix:
include:
- language: go
build-mode: manual
- language: javascript-typescript
build-mode: none

steps:
- name: Checkout
uses: actions/checkout@v5

- name: Set up Go
if: matrix.language == 'go'
uses: actions/setup-go@v6
with:
go-version: "1.25"
cache-dependency-path: backend/go.sum

- name: Install swag
if: matrix.language == 'go'
run: go install github.com/swaggo/swag/cmd/swag@latest

- name: Generate Swagger docs
if: matrix.language == 'go'
working-directory: backend
run: swag init -d cmd/server,internal/handler -g main.go --parseDependency --parseInternal -o docs

- name: Initialize CodeQL
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}

# The Go module lives in backend/ and its swagger docs package is
# generated above. Autobuild cannot locate the module from the repo
# root, so build explicitly from backend/ here.
- name: Build Go
if: matrix.language == 'go'
working-directory: backend
run: go build ./...

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
with:
category: "/language:${{ matrix.language }}"
3 changes: 2 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ jobs:
- uses: actions/setup-go@v6
with:
go-version: "1.25"
cache-dependency-path: backend/go.sum

- name: Install swag
run: go install github.com/swaggo/swag/cmd/swag@latest
Expand All @@ -56,7 +57,7 @@ jobs:

- name: Generate Swagger docs
working-directory: backend
run: swag init -d cmd/server,internal/api -g main.go --parseDependency --parseInternal -o docs
run: swag init -d cmd/server,internal/handler -g main.go --parseDependency --parseInternal -o docs

- name: Build binary
working-directory: backend
Expand Down
112 changes: 112 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,118 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [1.2.0] — 2026-04-25

### Added

- Session cookie (`os_session`) issued on login and refresh, cleared on
logout. The `GET /api/calls/:id/audio` route now accepts authentication
via either the existing `Authorization: Bearer` header or the new
cookie, so `<audio>` element playback can be authenticated without
client-side header injection. Cookie is httpOnly, Secure when served
over HTTPS, `SameSite=Strict`, scoped to `/api`. Cross-site requests
are rejected via a `Sec-Fetch-Site` check; invalid or expired cookies
fall through to anonymous so existing `publicAccess=true` deployments
continue to work unchanged.
- Canonical `GET /api/ws` listener WebSocket route. The existing `GET /ws`
remains as a compatibility alias that delegates to the same handler. The
frontend now connects to `/api/ws`, and the Vite dev proxy covers both
paths.

### Changed

- WebSocket `CAL` messages no longer carry embedded base64 audio. Audio
is fetched on demand from the existing `GET /api/calls/:id/audio` HTTP
endpoint, authenticated via the `os_session` cookie introduced in the
previous release entry. Frontend playback rewrite ships as the
immediately-following release entry.
- HTTP handlers have been decomposed from the monolithic `internal/api`
package into feature-scoped subpackages under `internal/handler/`
(`auth`, `calls`, `bookmarks`, `share`, `setup`, `health`,
`admin/{imports,radioreference,transcriptions}`). Route registration
now lives in `internal/handler/routes`, and shared swagger DTOs and
helpers live in `internal/handler/shared`. No route paths, methods,
middleware ordering, response shapes, or status codes changed.
- Backend file-level cleanup: `internal/handler/calls/calls.go` (~1500 LOC)
split into `upload.go`, `audio.go`, `search.go`, `transcript.go`, and a
slim `calls.go` retaining the `Handler` struct and constructor;
`internal/middleware/middleware.go` split into `cors.go`, `auth.go`,
`logging.go`, `limits.go`. Same package, same exports, no behaviour
change.
- Admin CRUD business logic has been extracted from `internal/ws` into a
new transport-agnostic `internal/admin` package. The WebSocket layer
now only routes `ADM_REQ` frames to `admin.Operations` methods; the
wire protocol, action names, and response shapes are unchanged.
- Deployment guide reverse-proxy instructions now list `/api/ws` alongside
`/ws` and `/api/admin/ws` as paths that need WebSocket-upgrade forwarding.
- Admin Options panel no longer shows an "Active" badge on every wired
setting; only "Planned" badges are rendered for not-yet-implemented
options.
- Admin Options "Audio Conversion" description now reads "Convert incoming
audio with FFmpeg before storing. Select the codec and bitrate below."
to reflect that MP3 and AAC outputs are both supported via the encoding
preset.
- Frontend `services/` directory grouped into `services/ws/` (`client.ts`,
`client.test.ts`, `adminClient.ts`) and `services/audio/` (`player.ts`,
`beep.ts`). All `@/services/*` imports across components and hooks have
been updated to the new paths. No runtime behaviour change.
- Frontend `hooks/` directory split into `hooks/shared/` (`useAuthInit`,
`useTheme`, `useTokenRefresh`, `useWebSocket`), `hooks/scanner/`
(`useScanner`, `useAudioPlayer`, `useTGSelectionSync`, `useActiveUnit`),
and `hooks/admin/` (`useAdminWebSocket`, `useAdminWsOps`,
`useAdminActivity`, `useAdminLogs`, `useWsQuery`), each with a barrel
`index.ts`. All call sites have been updated to the new specific paths.
No runtime behaviour change.
- Frontend `types/index.ts` god-file split into topic-scoped modules
(`call.ts`, `config.ts`, `ws.ts`, `auth.ts`, `api.ts`, `admin.ts`,
`ui.ts`). The original `index.ts` is now a barrel that re-exports
everything, so all existing `@/types` imports keep working unchanged.
New code can also import from a specific module (e.g. `@/types/admin`).
- Frontend layout polish on top of the directory restructure:
`app/slices/` split into `shared/` (`authSlice`), `scanner/`
(`scannerSlice`, `callsSlice`, `shareSlice`), and `admin/`
(`adminSlice`, `activitySlice`); `components/admin/AdminLayout.tsx`
inlined into `pages/Admin.tsx` (replacing the 5-line shim);
`components/admin/NavigationGuardContext.tsx` relocated to
`hooks/admin/useNavigationGuard.tsx`; and `services/downloadFilename.ts`
moved to `services/util/downloadFilename.ts`. All call sites updated;
no runtime behaviour change.
- Frontend audio playback now uses the platform `<audio>` element backed
by `MediaElementAudioSourceNode`. Each call is fetched on demand from
the existing `/api/calls/:id/audio` endpoint authenticated via the
session cookie. Drops the WebSocket-embedded base64 path, fixing
Mobile Edge AAC playback and dramatically reducing per-call memory
pressure on the client.
- Service worker now passes audio fetches (`/api/calls/:id/audio` and
`/api/shared/:token/audio`) straight through to the network instead of
intercepting them. Lets the browser handle Range requests natively for
the `<audio>` element and avoids buffering full call bodies in the
worker.

### Fixed

- Lock the primary admin's Allowed Systems selector in the user editor; the first user always has access to every system and the badges are now read-only with all systems shown as allowed.
- Default `audioEncodingPreset` seeded into the settings table is now
`mp3_32k` (matching the dropdown's "(default)" label and the Go
`ParseEncodingPreset` fallback) instead of `aac_lc_32k`. New installs
enabling audio conversion will now default to MP3 32 kbps as the UI
advertises.
- Audio playback now silently recovers from a 401 on `/api/calls/:id/audio`
by triggering a single token refresh and retrying the same call. Fixes
the case where a sibling device login (phone, tablet, second tab) pushes
a desktop's access JWT out of the per-user concurrent-token cap and
leaves \<audio\> playback failing until the next scheduled refresh.
- Per-user concurrent JWT cap raised from 5 to 20 (`auth.MaxRefreshFamilies`).
With a 15-minute access TTL refreshing roughly four times per hour, the
old limit pushed a desktop's session off the active list within an hour
of normal multi-device use. 20 leaves headroom for desktop + phone +
tablet without inflating the deny list.
- WebSocket clients (listener and admin) now detach handlers and wait for
`open` before closing a still-`CONNECTING` socket during reconnect.
Suppresses the cosmetic "WebSocket is closed before the connection is
established" browser console warning that appeared after a token-expiry
reconnect.

## [1.1.2] — 2026-04-24

### Security
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ COPY backend/ .
# Copy the built frontend dist into the go:embed target path
COPY --from=node-builder /src/frontend/dist ./internal/static/dist/
# Generate Swagger docs (gitignored, must be built in CI)
RUN swag init -g cmd/server/main.go --parseDependency --parseInternal
RUN swag init -d cmd/server,internal/handler -g main.go --parseDependency --parseInternal
RUN go build -ldflags="-s -w -X github.com/openscanner/openscanner/internal/config.Version=${VERSION}" -o /openscanner ./cmd/server

# Stage 3: Minimal runtime image
Expand Down
2 changes: 1 addition & 1 deletion backend/.golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ issues:
- "internal/db/models\\.go$"
- "internal/db/db\\.go$"
- "internal/db/querier\\.go$"
- "internal/api/swagger_models\\.go$" # swagger stubs — field names must mirror wire shape for doc readability
- "internal/handler/shared/dto\\.go$" # swagger stubs — field names must mirror wire shape for doc readability
exclude-rules:
# Test files: relax a handful of rules that don't add value there.
- path: _test\.go
Expand Down
2 changes: 1 addition & 1 deletion backend/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ VERSION?=$(shell git describe --tags --always --dirty 2>/dev/null || echo dev)
LDFLAGS=-s -w -X github.com/openscanner/openscanner/internal/config.Version=$(VERSION)

build:
swag init -d cmd/server,internal/api -g main.go --parseDependency --parseInternal -o docs
swag init -d cmd/server,internal/handler -g main.go --parseDependency --parseInternal -o docs
go build -ldflags="$(LDFLAGS)" -o $(OUTPUT) $(CMD)

dev:
Expand Down
Loading
Loading