doc-service and ai-service no longer use local filesystem directories —
all file and config I/O goes through storage-service. Update README and
CLAUDE.md to reflect 6-service architecture, new volumes, and add
storage-service step to the "Adding a new resource" checklist.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All file/blob persistence now routes through storage-service (port 8020).
Replaces doc_data and app_config volumes. Supports local (default), S3-compatible,
and WebDAV backends with zero-data-loss migration flow.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Update feature branch workflow: stop main stack before starting feature
stack, always use :5173. Simplify feat override template — no port
remapping needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The route handler async def download_file() shadowed the storage import
of the same name, causing the endpoint to call itself recursively.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All JSON config files (AI settings, doc settings, appearance, themes) now live
in the 'config' bucket of storage-service instead of a shared Docker volume.
- backend/core/config_storage.py: new async HTTP helpers for config bucket r/w
- backend/core/app_config.py: fully async rewrite; all load_*/save_*/seed_*
functions use config_storage instead of filesystem
- backend/routers/settings.py: all asyncio.to_thread() wrappers removed; direct
await calls throughout; update_theme reads via load_theme_by_id()
- backend/main.py: await seed_builtin_themes() directly (no to_thread)
- ai-service: remove CONFIG_PATH, add STORAGE_SERVICE_URL; config_reader now
fetches from storage-service via httpx
- doc-service: config_reader rewritten to fetch/write via storage-service
- docker-compose: remove app_config volume; add storage-service depends_on for
ai-service; remove DATA_DIR and CONFIG_PATH from doc-service
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New FastAPI microservice (port 8020) providing unified blob storage via
PUT/GET/DELETE/LIST HTTP API. Local filesystem backend is the default (zero
extra deps). S3-compatible and WebDAV backends are built in. Backend is
switchable at runtime via POST /migrate, which copies all objects to the new
backend, verifies each one, atomically switches, then cleans up the old backend.
WebDAV XML parsing uses defusedxml to prevent XXE attacks.
Wired into docker-compose (storage_data volume) and registered in the backend
service-health poller as 'storage-service'.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix doc-service delete endpoint: admins could not delete non-owned,
non-shared documents — they hit 404 because the initial query filtered
by owner/watch/group even before the is_admin bypass was checked.
Admins now get an unconditional fetch, consistent with intent.
- Add 18 new checklist tests covering: group admin role (4.9–4.10),
delete permission variants (12.16b–12.16e), can_delete sharing
(13.11–13.14), category scopes / PascalCase naming (14.7–14.17),
and three-dots portal fix (19.11).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>
Folder names like "invoices" and "vendor-invoices" are now converted to
"Invoices" and "Vendor-Invoices" when the watcher auto-creates categories,
matching the naming convention enforced on user-created categories.
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>
- Fix: list_plugins imported _REGISTRY as a direct reference to the
empty list that existed at import time; register_services() replaces
_REGISTRY with a new list so the imported reference was always [].
Added get_registry() helper so callers access the live list via the
module namespace. GET /api/plugins now correctly returns accessible
plugins for the current user.
- Fix: switch watchdog from InotifyObserver to PollingObserver. Inotify
events from the macOS host are not forwarded through the Docker bind
mount, so new files were only detected via the startup scan. PollingObserver
(1s default interval) works reliably on all platforms including
macOS+Docker bind mounts.
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>
Adds an explicit rule at the top of CLAUDE.md requiring a check after
every codebase change: routes, models, migrations, files, limits,
security patterns, Docker infra, and stack versions each map to the
specific section that must be updated.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Full codebase analysis embedded: file tree, all API endpoints, all DB
model columns+constraints, schema conventions, security standards (JWT,
bcrypt, sanitization, XSS/SQLi prevention, admin 404 pattern), frontend
patterns (Axios client, TanStack Query keys/mutations, route guards),
naming conventions, HTTP status codes, default limits, Docker infra,
and all workflow checklists in one place.
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>