- adminMilestones was reached only because adminConfig is mounted on /api/admin ahead of it and guards the whole path. adminMiddleware checks req.user.role and nothing else, so it failed closed (403) rather than open — but on mount order, not intent. It now states the requirement, with a test covering all four admin routers. - TODO.md records the whole audit: what was verified working (live transcription round trip, voice mode wiring), what was fixed, the two advisories that are unreachable and why, and the CI/CD and Kubernetes work worth doing before scaling out. Security review found nothing else exploitable: parameterised SQL throughout (the one interpolated table name is allowlisted), CORS refuses to start open in production, JWT_SECRET refuses to start unset in production, rate limits on /api and each auth route, a real CSP, and no secrets in the repo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
8.1 KiB
TODO
Live state as of 2026-09-11. Everything not listed under Open is deployed and green (674 tests, three consecutive clean runs).
Open
Needs your decision
- Replace Cloudflare Turnstile. Used on registration and password reset
only (
src/routes/auth.js); login is not gated, it relies on a 10-per-15-min limit and a constant-time credential check. Recommended replacement: ALTCHA — open source, self-hosted, proof-of-work, no third-party calls and no tracking, which also lets three CSP entries andframeSrcgo away. Alternatives: mCaptcha (open source, self-hosted, heavier to run) and Cap (newer, smaller). hCaptcha is neither Google nor open source, so it trades one third party for another. - Kubernetes / CI-CD hardening. Details under Deployment readiness.
- Basic index has no reader.
MilvusVectorStore.search()exists, but no tool calls it. Decide where the query path lives: pymilvus inside the deliberately-leannextcloud-basic-mcpimage, or a query API from the indexer container. Nothing can read that index until this is settled. Source:/home/danvics/docker/nextcloud-basic-mcp - Apply the restored clinical vector-store compose. Written, committed
and validated at
/home/danvics/docker/nextcloud-mcp-server, deliberately NOT applied —up -drecreates the live clinical Milvus.
Known gaps
- Multi-collection, ped-ai half. The MCP side is deployed
(
clinical_semantic_search(collection=…)+clinical_list_collections, allowlisted byMILVUS_COLLECTIONS). ped-ai still searches one collection per request. Needs an admin setting for which collections to search, then fan-out and merge —dedupeSourcesinsrc/utils/clinicalRetrieval.jsalready merges and renumbers. Seeclinical-assist/COLLECTIONS.md. - Confirm mail indexing end to end. Six separate breaks are fixed and the
scan reaches mail, but no full file pass has completed since the Milvus
rebuild, so no mail is indexed yet. Watch for
[SCAN-*] Mail messages: N seen, M queued. image-sizeDoS advisory (high), no upstream fix. Every published version up to 2.0.2 is affected;npm audit fixonly offers a breaking downgrade of pptxgenjs. Not reachable here: the onlyaddImagecall (src/routes/learningAI.js) is fed PNGs the app generated itself and fetched with an ownership check, never an uploaded file. Uploads are allowlisted to jpeg/png/gif and checked against their magic bytes. Revisit when pptxgenjs ships a patched dependency.uuidadvisory (moderate) via gaxios via Google auth. Not reachable: the flaw needs v3/v5/v6 with an explicit buffer, and gaxios calls onlyv4()for a multipart boundary. Forcing an override risks Vertex auth.
Deployment readiness (CI/CD and Kubernetes)
What already exists: ci.yml (tests on PR and main), security.yml (weekly
audit), docker-publish.yml and Android/versioning workflows, a Dockerfile
HEALTHCHECK, and /api/health.
Worth doing before Kubernetes, roughly in order:
- Fail CI on vulnerabilities.
security.ymlruns weekly and does not gate merges.npm audit --audit-level=highinci.ymlwould have caught the nodemailer advisories at the PR. - Build the image on PRs too.
docker-publish.ymlonly runs on tags, so a Dockerfile break is found at release time. - Run the e2e suite in CI.
scripts/e2e.shanddocker-compose.e2e.ymlexist but nothing calls them. - Separate liveness from readiness.
/api/healthis one endpoint; Kubernetes wants liveness (process up) apart from readiness (database, gateway and MinIO reachable), or rollouts take traffic too early. - Graceful shutdown. No SIGTERM handler, so a rolling update can cut off an in-flight transcription or image job.
- Externalise state. Uploads and audio backups assume local paths and a single instance; more than one replica needs them all in MinIO/Postgres.
- Config as secrets. Everything is env vars in compose today, which maps
to ConfigMap/Secret cleanly, but
JWT_SECRET, gateway keys and database credentials should be a Secret from the start. - Pin the base image by digest and keep the SBOM the build already has.
Done since this file was written
2026-09-11
-
Live transcription: proved working end to end against the live gateway —
local-kokoro-ttsproduced 92KB of speech andmistral-voxtral-mini-transcribereturned the sentence back verbatim. The Settings picker offered six hardcoded ids that do not exist on this gateway (local-whisper-large-v3-turbo→ 400 Invalid model name); it now lists the nine the gateway advertises, cached, with the admin default marked. -
Assistant voice mode is wired end to end: record → browser recognition, falling back to server transcription → send → spoken answer. All four helpers it needs exist.
-
Recordings can be exported (server and local copies) and a recorder that dies — an error, or the microphone taken by another app, unplugged or revoked — now says so instead of appearing to record silence. No wake lock, deliberately: stopping on sleep or sign-out is the behaviour you want.
-
nodemailer 9.0.1 → 9.1.1, clearing four high advisories, two of them delivery bugs that can route mail to an attacker-controlled domain.
-
Admin routers state their own authentication.
adminMilestonesrelied onadminConfigbeing mounted first on/api/admin; it failed closed, but on mount order rather than intent. -
Test suite made deterministic. A file failed about one run in four with "Unable to deserialize cloned data": node:test parses each child's stdout, and page/server logging was landing inside those frames. Every test child's stdout is now pure TAP.
-
iOS: text fields are 16px on phones, so Safari no longer zooms the page on focus — which was also why fixed chrome (the menu button) scrolled away.
-
Settings claim corrected: there is no "use my normal physical exam" trigger, and the prompt forbids copying template content.
-
Storage stack is under version control (
/home/danvics/docker/ped-ai-storage), secrets verified excluded, with a README recording the misleading project names and the MinIO/separate-etcd requirements. -
Both Milvus stores keep objects in MinIO; verified end to end on the basic side (16 objects in the bucket, rows queryable, collection Loaded).
-
The operator image is built from source (
Dockerfile.operator), so the driftedcheck.pythat broke image generation can no longer be run by accident.
Worth knowing
- Four repos were rescued from container images today:
nextcloud-basic-mcp,clinical-assist, the deletednextcloud-mcp-servercompose file, and the operator'scheck.pydrift. Prefer building from a repo over a live container. - Two Milvus instances, historically misleading names.
Clinical index =
nextcloud-mcp-server-milvus-1(MinIO-backed, collectionmcp_bge_m3_1024). Nextcloud basic index =ped-ai-storage-basic-milvus-1(dbbasic, collectionbasic_bge_m3_1024_v2). - Embedded etcd is unusable with authorization on. Every non-root Milvus user
failed
etcdserver: invalid auth token. Both stores now run a separate etcd container, matching the profile that always worked. - Both Milvus stores now keep objects in MinIO, matching what clinical always
did.
COMMON_STORAGETYPE=localwrote segment files relative to the working directory, so a recreate destroyed them while etcd kept referencing them and the collection hung at Loading forever. S3 semantics also mean either store can be pointed at a managed bucket without touching Milvus — which is what makes a Terraform-managed deployment straightforward. - Milvus object-store credentials live in
milvus-user.yamlin the protected secrets dir, not in the compose, because Milvus has no file-based option for them and the compose should stay reviewable.
Co-Authored-By: Claude Opus 5 noreply@anthropic.com