- Keep HEAD's get_user_admin_groups dep and richer delete permission logic (can_delete via share OR group admin path)
- Use sa.text("false") for migration server_default (correct SQLAlchemy form)
- Preserve 0006/0007 migration entries in doc-service CLAUDE.md
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
canManage() returned false for system-scope categories unconditionally.
Superusers can manage all categories (backend already permits it), so
check is_admin from getMe() and short-circuit to true.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add can_delete column to document_shares (migration 0005)
- Inject x-user-is-admin header from backend proxy to doc-service
- Add get_user_is_admin() dep in doc-service
- Delete endpoint now allows: owner, admin, or group member with can_delete=true
- Watch documents (user_id='watch') deletable by admins only
- DocumentOut gains viewer_can_delete (computed per-request)
- Share UI: 'Allow group members to delete' checkbox + trash badge on shares
- RowActionsMenu dropdown portaled to document.body — fixes overflow-hidden clipping
- Delete mutation onError handler — no more silent failures
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The 401 handler was redirecting to /login unconditionally, causing an
infinite reload loop when useTheme fired unauthenticated API calls on
the login page itself. Now only redirects if not already on /login.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All API calls now go through a thin request() wrapper around native fetch.
Removes the axios dependency entirely. The wrapper injects the JWT on every
request and — the key fix — clears localStorage and redirects to /login on
any 401 response, so expired sessions no longer leave users on broken pages.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Lift state to page level, fire both upload-limits and watch-directory
mutations from one button. Add noSaveButton and onChange props to
PluginSchemaForm to support this pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Auto-create {service-id}-admin groups at startup (group_bootstrap.py)
- get_service_admin() dep: grants access to superusers OR service group members
- /api/settings/ai and /api/settings/documents/limits now allow service admins
- AI service exposes /plugin/manifest (ai-service-admin access group)
- DocServiceSettingsPage: combined upload limits + watch directory on one page
- ServiceAdminRoute in frontend guards new /apps/documents/settings and /apps/ai/settings
- Single Settings button per app card (visible to admins and service group members)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove the "Extensions" section from the sidebar nav. Instead, each app
card on the Apps page shows an "Extension" button when the current user
has access to that app's plugin (matched by service ID). The button links
to /settings/plugins/:id alongside the existing admin Settings button.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>