Skip to content

Dashboard v0.5.0: full UI + auth + admin runtime control + security hardening#28

Merged
dvcdsys merged 12 commits intomainfrom
dashboard
May 4, 2026
Merged

Dashboard v0.5.0: full UI + auth + admin runtime control + security hardening#28
dvcdsys merged 12 commits intomainfrom
dashboard

Conversation

@dvcdsys
Copy link
Copy Markdown
Owner

@dvcdsys dvcdsys commented May 4, 2026

Summary

Full web dashboard at `/dashboard`, first-class auth, admin-side runtime control of the embedding sidecar, and a security hardening pass. Spans 9 commits (PR-A through PR-E + two security iterations + cancel-gate refinement).

Highlights

  • Full web dashboard — login, projects, search, API keys, users, settings, server admin
  • First-class auth — sessions, bootstrap admin via env, change-password flow, role-based gating
  • Runtime control via UI — admin can tune embedding model, n_ctx, n_gpu_layers, n_threads, batch size, concurrency live; restart sidecar without SSH
  • Drift indicator — projects indexed with a now-changed embedding model render with a red border + 'Stale model' badge
  • Security hardening — login rate limiter (5/(IP,email)/15min + 60/IP/min), 1 MiB body cap (64 MiB on /index/files), sha256-hashed session IDs, admin-only PATCH/DELETE on projects
  • Docs — new `doc/SECURITY_DEPLOYMENT.md` covering trusted-proxy posture, TLS, password policy, what the server explicitly does NOT do

Breaking changes

🚨 Read before upgrading:

  1. Bootstrap admin env vars REQUIRED on a fresh DB. `CIX_BOOTSTRAP_ADMIN_EMAIL` + `CIX_BOOTSTRAP_ADMIN_PASSWORD` must be set together when the users table is empty; the server now refuses to start otherwise. Existing deployments with users in DB are unaffected.
  2. `CIX_MAX_EMBEDDING_CONCURRENCY` default 1 → 5. The new value pipelines host-side prep with device inference. Drop back to 1 if you observe contention.
  3. Session cookies issued before the upgrade become invalid (the `sessions.id` column now stores `sha256(token)` instead of the raw token). All users will be logged out once after upgrade — single re-login fixes it. No schema migration required.

CLI compatibility

`cli/v0.5.0` is fully compatible with `server/v0.5.0`. No CLI tag bump needed.

  • All existing index endpoints (`/index/{begin,files,finish,cancel}`) preserved.
  • Body cap of 64 MiB on `/index/files` covers the CLI's default-batch payload (~11 MiB for `batch=20 × max-file=512 KiB`) with 6× headroom.
  • `POST /index/cancel` is open to any authenticated user (matched the CLI's defer-cleanup pattern — see commit 26fea44).

Release order

⚠️ Merge `#27` (CI build-context fix) FIRST, then this PR, then tag `server/v0.5.0`. Otherwise the CI release run will fail at the dashboard build stage.

Test plan

  • All Go tests pass (`go test ./...` green)
  • Dashboard typechecks + builds (`tsc -b && vite build` green, ~151 KB gz)
  • Manual smoke tested on dev stack (Portainer `cix` stack on RTX 3090, image `dvcdsys/code-index:cu128-dev`):
    • Login → forced password change → home page with status strip
    • Server admin → Save & Restart → sidecar cycles running → restarting → running
    • Drift indicator turns red after embedding-model change
    • Manual reindex via CLI clears drift
    • API key + user CRUD round-trips
  • Reviewer: smoke-test the CI image once `server/v0.5.0` is built
  • Reviewer: bump Portainer prod stack image tag to `:0.5.0-cu128` (or `:cu128`) and verify health

🤖 Generated with Claude Code

dvcdsys and others added 9 commits May 2, 2026 23:19
…icit auth gating

doc/openapi.yaml describes all 18 endpoints; oapi-codegen v2 generates
typed request/response models and a chi-server ServerInterface, committed
in internal/httpapi/openapi/. Handlers were rewritten as methods on a new
Server struct that implements the generated interface — wire format is
byte-identical to the previous closure-based handlers.

NDJSON streaming on POST /index/files keeps both content-types under one
spec entry with Accept-header negotiation; the embedded spec is served at
/openapi.json (no file duplication — pulled from openapi.GetSpecJSON).

Swagger UI 5.18.2 is embedded via embed.FS and served at /docs (public,
same-origin). Makefile gains openapi-gen, openapi-check, swagger-ui-fetch
targets. CI can run openapi-check to catch drift between spec and
generated code.

Auth gating made explicit: the implicit empty-API-key bypass is removed.
config.Validate now refuses to start with CIX_API_KEY="" unless
CIX_AUTH_DISABLED=true is set. The middleware panics on empty-key
construction as a defense-in-depth check; the router wires it only when
auth is not explicitly disabled. Production Docker images are unaffected
(they always set CIX_API_KEY); local dev gets a loud WARN and a clear
opt-out flag instead of silent open auth.

Local-dev defaults for SQLitePath / ChromaPersistDir resolve to
~/.cix/data/... so make run works without editing .env. Container images
explicitly override these to /data/... — no production behaviour change.
The CPU Dockerfile gains the same ENV CIX_SQLITE_PATH / CIX_CHROMA_PERSIST_DIR
that Dockerfile.cuda already had, so both images share one data layout.

.gitignore exception added for server/internal/httpapi/docs/ so the
embedded Swagger UI bundle ships with the source tree (top-level docs/
remains gitignored per CLAUDE.md).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The CLI's defer-cleanup on early exit (Ctrl-C, network drop) calls
POST /index/cancel to free the per-project run lock. Gating cancel
behind the admin role would strand viewer-owned runs until the 1-hour
TTL elapses — worse UX than the theoretical DoS we'd be preventing.

PATCH /projects/{path} and DELETE /projects/{path} stay admin-only;
those are genuinely destructive. If we ever need owner-scoped cancel,
key off projects.indexing_run.started_by_user_id in a follow-up.

- Drop mustBeAdmin gate from IndexCancel; add comment explaining why.
- Update OpenAPI summary/description; drop 403 from cancel responses.
- Regenerate openapi.gen.go.
- Test: TestIndexCancel_AnyAuthenticatedUser pins the new policy
  (viewer cancels their own project → 200, not 403).
- Trim cancel from TestProjectMutations_AdminOnly cases.
- Doc: SECURITY_DEPLOYMENT.md "what the server does NOT do" section
  now lists PATCH/DELETE (not PATCH/DELETE/index-cancel) as admin-only.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Comment thread server/internal/apikeys/apikeys.go Dismissed
Comment thread server/internal/httpapi/server.go Fixed
dvcdsys and others added 3 commits May 4, 2026 15:05
CI: Server / test was failing on `go vet` because
//go:embed all:dist in dashboard/embed.go found no matching files —
the .gitkeep marker that .gitignore promises to preserve was never
actually committable: a stray bare `dist/` rule in the Python section
matched any `dist/` directory at any depth, and Git can't re-include
a file under a broadly-excluded parent. Narrow the bare rule to
/dist/ (root only) and commit the marker so a fresh clone passes
the test suite without first building the React bundle.

CodeQL go/uncontrolled-allocation-size flagged
server.go:419 — `make([]openapi.FileResultItem, 0, limit)` where
limit derives from a user-supplied JSON field. Cap with a new
clampLimit helper at maxResultLimit = 1000 — well above any legit
dashboard / CLI page size, well below any value that would balloon
the slice or stall the SQL LIMIT clause. Apply to the four other
endpoints that derive `limit` from `body.Limit` for defense in
depth (search, files, references, definitions, symbols).

derefIntOrDefault keeps the old contract (still used for
non-capacity metadata fields like body.TotalFilesDiscovered).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…nale

Two-part change in response to PR review:

1) Lengthen the random portion of issued keys from 24 → 32 bytes,
   so the on-the-wire token is now `cix_<43-char-base64url>` (47 chars
   total) instead of `cix_<32-char>` (36 chars). Matches GitHub-class
   PAT verbosity and pushes the entropy floor from 192 → 256 bits.
   Existing keys keep working — the lookup column is the hash, not
   the length.

2) Expand the comment on hashKey() to spell out why SHA-256 is the
   correct primitive for hashing a server-issued, high-entropy token
   and explicitly refute the CodeQL `go/insufficient-password-hash`
   alert. Adding a slow KDF (bcrypt/argon2/PBKDF2) would tax every
   authenticated request 25–250 ms without raising the security floor
   a single bit, because brute-forcing the hash at 256-bit pre-image
   entropy is computationally indistinguishable from brute-forcing
   the underlying random bytes. Same pattern GitHub / Stripe / AWS
   use for their machine-issued tokens.

Test asserting body length updated 32 → 43 to match the new size.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Both Dockerfile and Dockerfile.cuda have `COPY --from=openapi
openapi.yaml /spec/openapi.yaml` in the dashboard build stage (added
when the dashboard started consuming the OpenAPI spec for type
generation). The release workflow's docker/build-push-action calls were
missing the corresponding `build-contexts: openapi=doc` parameter, so
the next `server/v*` tag push (or workflow_dispatch) would fail at the
dashboard stage with "stage openapi not found".

`make docker-build-cuda-dev` works because it passes
`--build-context openapi=$(ROOT)/../doc` directly. This brings CI to
parity.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@dvcdsys dvcdsys closed this May 4, 2026
@dvcdsys dvcdsys deleted the dashboard branch May 4, 2026 14:30
@dvcdsys dvcdsys restored the dashboard branch May 4, 2026 14:33
@dvcdsys dvcdsys reopened this May 4, 2026
@dvcdsys dvcdsys merged commit 6e5d779 into main May 4, 2026
12 checks passed
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.

2 participants