cpatpa ships five phases in one squash: admin API, local storage, Ollama adapter, Docker, and rate-limiting
A single mega-commit (`9166a01d`, 1349 added lines across 24 files) bundles Phases 4-8: the admin backend, an AES-256-GCM local storage driver, an Ollama adapter, Docker Compose deployment, and per-user rate limiting. A follow-on commit adds tool-call support to the Ollama adapter.
Phase 4 adds routes/admin.ts behind requireAuth + requireAdmin. User management covers list, role change, status change, and delete with last-admin protection and self-mutation refused at the route layer. GET/PATCH /admin/org-settings has typed per-field validation; every change emits an admin.* audit event. GET /admin/audit is paginated with optional action and user_id filters.
Phase 5 is the highest-value piece for direct porting. lib/storageLocal.ts stores files as [12-byte IV][16-byte GCM tag][ciphertext] under a configurable root path. Master key is SHA-256(STORAGE_ENCRYPTION_KEY). Path traversal is defended against by resolving the target to an absolute path and confirming it stays under the root before any read or write. lib/storage.ts becomes a facade keyed on STORAGE_DRIVER (local defaults on Docker; R2/MinIO remains available). Signed URLs return null in local mode; callers fall back to the /download/:token route. The storage module is about 250 lines self-contained. The test confirmation from the changelog is concrete: a 25-byte plaintext produces a 53-byte on-disk file (12 IV + 16 tag + 25 ciphertext), and the plaintext does not appear in the file.
Phase 6 adds lib/llm/local.ts for OpenAI-compatible endpoints (Ollama, vLLM, LM Studio). Models with a local/ prefix route here; the suffix is forwarded verbatim as the upstream model id. The initial commit ships streaming but drops tool definitions silently. A follow-on commit (9fe6a8e9) fills that gap: tools are forwarded in the request body, streamed tool_calls are assembled from index-keyed argument fragments, the caller's runTools callback is invoked, role=tool result messages are appended, and the loop continues until the model produces content with no further tool calls or maxIterations is reached.
Phase 7 gives the fork a complete Docker Compose deployment. The backend Dockerfile is multi-stage with LibreOffice baked in and runs as a non-root user. docker-entrypoint.sh runs npm run migrate before starting the server (toggle via MIGRATE_ON_BOOT). The compose file brings up postgres, backend, frontend, ollama, caddy, and a backup sidecar that runs nightly pg_dump. Caddy handles Let's Encrypt TLS for ${PIP_DOMAIN}; internal-CA users swap in an explicit tls cert key directive.
Phase 8 fixes two audit findings. Rate limiters now key on res.locals.userId for the chat, chat-create, and upload paths - behind a corporate NAT, one user's burst no longer drains the bucket for the whole firm. extractStructureTree output goes through sanitiseStructureTitle (strip control chars, escape angle brackets, length cap) before reaching downstream renderers.
Spotted something wrong? Or know the PR text has fresher detail than the writeup above?