Introduces a manifest contract so feature containers self-describe their
settings (JSON Schema + access rules). Backend and frontend gain generic
plugin proxy and dynamic Extensions UI with zero feature-specific code.
Doc-service is the first plugin consumer: exposes /plugin/manifest and
/plugin/settings, adds a watchdog-based file watcher that auto-ingests
PDFs from a mounted directory, maps subfolders to categories, supports
AI-suggested folder/filename (user-confirmed), and enforces a no-remove
policy. Access is gated by is_superuser or doc-service-admin group.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 4 built-in themes (Default, Pastel, High Contrast, Ocean Blue) seeded as
JSON files in /config/themes/ on startup; custom themes can be created,
edited, and deleted via the new admin Appearance page
- All theme tokens applied via JS inline CSS properties (no hardcoded CSS blocks)
- New `color_mode` column on users table (migration dd6ad2f2c211); users can
override the admin-set global default in Settings
- Backend: GET/PATCH /settings/appearance, full CRUD on /settings/themes
- Frontend: AdminAppearancePage with theme grid + colour pickers, SettingsPage
replaces placeholder with mode selector, useTheme rewritten to fetch from API
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Users can pin/unpin any available service on their home page via a
Customize mode; preferences persisted via PATCH /api/users/me/preferences
- Time-aware greeting renders the user's display name through React JSX
(HTML-escaped by design — no dangerouslySetInnerHTML used)
- Added dashboard_app_ids JSON column to users table (migration c7e8f9a0b1d2)
- /settings now routes to a placeholder page
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
TanStack Query's default 3 retries + exponential backoff hid backend
errors behind 5-8s of "Loading…". Now retries once and surfaces the
error message immediately on failure.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New backend: Group + GroupMembership models, schemas, CRUD router at
/api/admin/groups (list, create, get detail, update, delete, add/remove members)
- New Alembic migration: groups and group_memberships tables
- Frontend: Admin sidebar item is now an expandable accordion with
Users and Groups sub-items; AdminPage redirects to /admin/users;
new AdminUsersPage and AdminGroupsPage with inline member management panel
- API client: 7 new group functions + TypeScript types
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wrap check_all() call inside the loop with try/except so a transient error
cannot exit the while-True and freeze all health statuses. Add transition
logging (HEALTHY / UNHEALTHY) so docker logs show when a service changes
state. Also add refetchIntervalInBackground on the frontend query so the
poll continues even when the browser tab is not focused.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend polls each registered service's /health endpoint every 30 s via a
background asyncio task. GET /api/services exposes the live status snapshot.
The Apps page now renders from this endpoint — showing "Unavailable" (dimmed,
non-clickable) when a service is registered but its container is unreachable.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Resets status to pending, clears error_message, and re-enqueues the
background AI extraction task. Button is disabled while the document
is already pending or processing; returns 409 in that case from the API.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Sidebar: Apps accordion expands to Documents, which expands to list all
user categories; clicking a category navigates to /apps/documents?category_id=<id>
- DocumentsPage: reads category_id from URL and applies filter; shows active
category chip in FilterBar with dismiss; removed TagEditor (deferred)
- doc-service GET /documents: new category_id query param filters via subquery
- doc-service POST /documents/categories: detects similar category names and
triggers background re-analysis of affected documents so the new category
surfaces as a pending AI suggestion on relevant docs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each service prompt card now shows:
- A collapsible how-to panel with placeholder docs, required JSON
response keys, and usage notes
- A "Reset to Default" button (with confirmation step) that restores
the built-in prompt without saving, letting the admin review first
- A "Using the built-in default prompt" indicator when unchanged
Backend includes default_system / default_user_template in the
system-prompts API response so the frontend never duplicates defaults.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each feature service owns its system prompt in its config JSON on the
shared volume. The AI Settings page now has General and System Prompts
tabs — admins can view and edit any service's prompts at runtime with
changes taking effect within 30 s (config cache TTL).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Input fields keep white background (#fff) and slate-900 text in all
colour modes. Light gray text on white (dark mode bleedthrough) was
unreadable. Applies to both the shadcn Input component and raw
<input>/<textarea>/<select> elements in older pages.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Dark mode text-primary: slate-200 → slate-300 (#CBD5E1)
- Ghost button: add border + explicit text colour so it is always
visible as a button (not just on hover)
- Outline button: stronger hover border for more feedback
- button:not([class]): global baseline for unstyled <button> elements
(Tailwind Preflight strips all native appearance; this restores a
visible border, bg-surface fill, and rounded corners so buttons in
older pages are always recognisable)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Near-white (#F8FAFC) in input fields was too harsh against the
slate-800 surface. slate-200 (#E2E8F0) is readable but not glaring.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Introduce async priority queue service in ai-service; all /chat calls now route through it
- Refactor chat router to separate execute_chat (core logic) from the HTTP handler
- Add /queue endpoints (status, pause, resume, cancel) for queue management
- Update ai-service config to use Pydantic v2 model_config style
- Add STATUS.md files for backend, ai-service, doc-service, and frontend
- Document STATUS.md workflow in CLAUDE.md
- Update doc-service documents router and schemas; frontend DocumentsPage and API client
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
AI now returns a short descriptive title per document (e.g. "ACME Corp
Invoice April 2026"). Title is stored in a new documents.title column
(migration 0002), shown in the row header instead of the raw filename,
and editable inline via PATCH /documents/{id}/title. Filename is shown
as a subtitle when a title exists.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each document's tags are now editable inline: click Edit to enter a tag
editor (Enter/comma to add, × to remove, Save to persist). The View
button opens the PDF in a new browser tab via blob URL. Both features
work through the existing proxy — no proxy changes needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All feature containers now POST messages to ai-service (port 8010) instead
of calling AI providers directly. ai-service routes to LM Studio, Ollama,
or Anthropic based on /config/ai_service_config.json. doc-service AI
providers removed; replaced by httpx ai_client.py. Backend settings
restructured to /api/settings/ai. Frontend gets dedicated AIAdminSettingsPage
and AI Service card in AppsPage.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- pytest suite for doc-service: 20+ tests covering category CRUD,
document upload/get/delete/patch, ownership isolation, category
assignment, AI processing (mock), and live PDF tests (auto-skipped
when tests/pdfs/ is empty)
- Minimal in-memory PDF builder in conftest so tests run without any
fixture files; real PDFs can be dropped into tests/pdfs/ to activate
live extraction tests
- AI prompt updated to return suggested_categories (2–5 short names)
- Frontend: SuggestionChip component in DocumentRow shows AI-suggested
categories after processing; "Assign" links to an existing category,
"Create & Assign" creates it first, ✕ dismisses locally
- Default AI provider changed to LM Studio at
http://host.docker.internal:1234/v1 (host.docker.internal resolves
to the macOS host from inside Docker Desktop)
- tests/pdfs/ directory tracked via .gitkeep; *.pdf excluded by .gitignore
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New `features/doc-service` FastAPI microservice: PDF upload, async
text extraction (pdfplumber), AI classification via Anthropic/Ollama/
LM Studio, per-user categories, file download
- Alembic migration isolated with `alembic_version_doc_service` table
- Main backend: httpx proxy routers for /api/documents/* and
/api/documents/categories/*, admin settings API at /api/settings/*
- Runtime config in /config/doc_service_config.json (shared Docker
volume); api_key masking on reads; atomic write with os.replace()
- Frontend: DocumentsPage, DocumentAdminSettingsPage, updated AppsPage
launcher hub, simplified Nav (removed Settings link), new routes
- docker-compose: doc-service service, doc_data + app_config volumes,
removed internal:true from backend-net for outbound AI API calls
- Fix pre-commit hook: probe Docker socket path so git subprocess picks
up Docker Desktop on macOS
- Fix security_check.py: use sys.executable for bandit so venv python
is used instead of system python
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- deps.py: get_current_admin returns 404 Not Found for non-superusers instead
of 403 Forbidden — hides endpoint existence from unauthorised callers
- App.tsx: AdminRoute redirects non-admins to /login instead of /, making
the route indistinguishable from a non-existent page
Layer 3 (network-level IP restriction via Traefik) tracked in TODO.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend:
- schemas/user.py: is_admin (validation_alias=is_superuser) on UserOut and
UserAdminOut; UserAdminCreate extends UserCreate with is_admin flag
- deps.py: get_current_admin dependency — 403 for non-superusers
- routers/admin.py: GET/POST /api/admin/users, DELETE and PATCH /active per
user; self-delete and self-deactivate blocked
- main.py: register /api/admin router
- scripts/seed.py: seed test user with is_superuser=True; promotes existing
user if already created without the flag
Frontend:
- api/client.ts: UserData type with is_admin, admin API functions
- components/Nav.tsx: Admin link visible only when user.is_admin is true
- pages/AdminPage.tsx: user table with add-user form, delete, toggle active
- App.tsx: AdminRoute guard (403-redirects non-admins to /); /admin route
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- frontend prod: USER root for adduser, then USER appuser (1001:1001); fixes
build failure caused by nginx-unprivileged already setting USER nginx
- docker-compose: frontend user updated to 1001:1001 (was 101:101)
- CLAUDE.md: add infrastructure change protocol (update README + test both
stacks after any Dockerfile/compose/nginx change); fix stale passlib ref
- README: container table shows nginx-unprivileged image, UID column, internal
port 8080 note; Current State notes all containers run as non-root
Both dev and prod stacks tested and verified (health, login, /users/me,
frontend serving, all containers confirmed non-root via docker inspect).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- backend: appuser UID/GID 1001 via useradd, USER directive, --chown on COPY
- frontend builder: appuser UID/GID 1001 via adduser, USER directive
- frontend prod: switch to nginxinc/nginx-unprivileged:alpine (nginx UID 101), listen on 8080
- docker-compose: explicit user: for all services (70:70 db, 1001:1001 backend/frontend-dev, 101:101 frontend-prod)
- nginx.conf: listen 8080 to match unprivileged image
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- vite.config.ts: proxy target via VITE_API_TARGET env var (falls back to localhost)
- docker-compose.dev.yml: set VITE_API_TARGET=http://backend:8000
- Add /login-success and /register-success placeholder pages
- Show real API error messages in login/register forms
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Generate frontend/package-lock.json (required by npm ci)
- Add network: host to BuildKit build stages to fix DNS in pip installs
- Switch pyproject.toml build backend to setuptools.build_meta (stable)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>