91 Commits

Author SHA1 Message Date
curo1305 0f760c379d fix: remove obsolete /data/documents and /config dirs from Dockerfiles
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>
2026-04-21 13:45:12 +02:00
curo1305 f13ef88711 Merge feat/storage-service: dedicated storage service with pluggable backends
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>
2026-04-21 13:41:26 +02:00
curo1305 0d8e0366c6 docs: always use port 5173 for feature stacks (no per-branch ports)
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>
2026-04-21 11:48:22 +02:00
curo1305 3a66aeeec5 fix: rename download_file import to storage_download to avoid shadow
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>
2026-04-21 11:48:04 +02:00
curo1305 248b2bb9d7 fix: remove unused imports in StorageAdminPage
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 11:25:59 +02:00
curo1305 cfec3bb906 feat: Phase 4+5 — admin storage UI, backend proxy, CLAUDE.md enforcement
- backend/app/routers/storage_config.py: 5 admin-only endpoints proxying
  storage-service config + migration API (GET/PATCH/POST/DELETE)
- backend/app/main.py: register storage_config router
- frontend/src/api/client.ts: StorageStatus, MigrationStatus,
  StorageBackendConfig interfaces + 5 API functions
- frontend/src/pages/StorageAdminPage.tsx: full admin UI — backend health
  dot, driver selector (local/S3/WebDAV), conditional credential fields,
  Test & Migrate button, live 2s-poll migration progress bar, Cancel
- frontend/src/App.tsx: /admin/storage route (AdminRoute guard)
- CLAUDE.md: storage enforcement rule, updated Docker tables (6 services,
  3 volumes), §20 in merge checklist
- backend/CLAUDE.md, frontend/CLAUDE.md, doc-service/CLAUDE.md,
  ai-service/CLAUDE.md: updated to reflect storage-service integration
- tests/ALL_TESTS.md + tests/storage-service_tests.md: §20 (20 tests)
- backend/STATUS.md, frontend/STATUS.md: updated with new endpoints/routes
- changelog/2026-04-20_storage-service.md: full change log

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 16:13:05 +02:00
curo1305 4c35d7a2a4 feat: migrate app_config volume to storage-service config bucket (Phase 3)
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>
2026-04-20 16:02:57 +02:00
curo1305 2f3efb9bf9 feat: migrate doc-service to use storage-service for file I/O (Phase 2)
- storage.py: replace aiofiles filesystem ops with httpx calls to
  storage-service PUT/GET/DELETE /objects/documents/{key}
- Document model: rename file_path → storage_key (plain object key, no path prefix)
- Migration 0008: ALTER COLUMN + data migration strips /data/documents/ prefix
- documents.py: update upload, delete, download endpoints; _extract_pdf_text
  now takes bytes (pdfplumber.open(BytesIO)) instead of a filesystem path
- file_watcher.py: store storage_key instead of file_path on ingestion
- doc-service config: add STORAGE_SERVICE_URL env var

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 15:57:29 +02:00
curo1305 5349f21752 feat: add storage-service container with pluggable backends (Phase 1)
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>
2026-04-20 15:50:31 +02:00
curo1305 50d2348b36 refactor: rename MERGE_CHECKLIST to ALL_TESTS + add per-service test files
- tests/MERGE_CHECKLIST.md → tests/ALL_TESTS.md (git rename, updated header + index of sub-files)
- tests/backend_tests.md — §1–9, §18 (auth, users, admin, groups, appearance, service health, plugins, AI/doc settings, infra/security)
- tests/frontend_tests.md — §19 (UI & routing)
- tests/doc-service_tests.md — §10–16 (upload/processing, list/filtering, slide-over, sharing, categories, bulk actions, watch directory)
- tests/ai-service_tests.md — §17 (AI queue & providers)
- CLAUDE.md: updated merge checklist section, file tree, and self-update checkpoint with mandatory test-file update rule
- settings.local.json: added docker inspect/ps, curl, lsof, git merge/branch/log/diff/status/config/mv permissions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 02:19:51 +02:00
curo1305 d345ace86d fix: admin delete bypass + update merge checklist for new features
- 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>
2026-04-19 02:12:40 +02:00
curo1305 c59718171c Merge: resolve conflicts between feat/document-delete-permissions and feat/category-scopes-group-admin
- 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>
2026-04-19 01:06:04 +02:00
curo1305 99d22660f9 Merge branch 'feat/category-scopes-group-admin' 2026-04-18 22:36:55 +02:00
curo1305 fcfc06cda9 fix: rename existing system categories to PascalCase-with-dashes via migration
Migration 0007 converts all scope='system' category names in-place
(e.g. "invoices" → "Invoices", "vendor-invoices" → "Vendor-Invoices").

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 22:33:16 +02:00
curo f5bc28cda2 Merge pull request 'feat: document delete permissions + three-dots menu portal fix' (#2) from feat/document-delete-permissions into main
Reviewed-on: #2
2026-04-18 22:27:17 +02:00
curo1305 1c8b35399c fix: capitalize watch-folder names to PascalCase-with-dashes on ingest
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>
2026-04-18 22:26:24 +02:00
curo1305 ebf97b6f4a fix: show manage controls for system categories when user is superuser
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>
2026-04-18 22:23:30 +02:00
curo1305 fec3953009 feat: category scopes, group-admin role, and permission model
- Three category scopes: personal / group / system (watch)
- PascalCase-with-dashes naming convention enforced at backend + frontend
- is_group_admin flag on GroupMembership; PATCH endpoint for admins to toggle it
- Categories router: scope-based list/create/rename/delete with _check_can_manage_cat
- Documents router: delete uses is_admin + can_delete share flag + group-admin check; remove_category requires doc ownership; assign_category accepts group/system categories
- Proxy layers inject x-user-is-admin and x-user-admin-groups headers
- Frontend: ManageCategoriesDialog grouped by scope with lock icons; SourcePanel scope picker + client-side name validation; AdminGroupsPage group-admin checkbox

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 22:16:49 +02:00
curo1305 6e5e5c08bf feat: document delete permissions + three-dots menu portal fix
- 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>
2026-04-18 21:39:01 +02:00
curo1305 05d79d3d21 Fix 401 redirect loop on login page
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>
2026-04-18 21:16:45 +02:00
curo1305 75b7ae6062 Merge feat/replace-axios-with-fetch: replace Axios with native fetch + 401 redirect 2026-04-18 21:05:59 +02:00
curo1305 479108779f Replace Axios with native fetch; add global 401 session-expiry redirect
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>
2026-04-18 21:04:18 +02:00
curo1305 c5976882be Split monolithic CLAUDE.md into per-service sub-files
Root CLAUDE.md now contains only project-wide concerns (stack, architecture,
Docker, workflows, security hook). Service-specific details moved to:
- backend/CLAUDE.md — DB models, API endpoints, JWT/bcrypt, naming conventions
- frontend/CLAUDE.md — routes, TanStack Query patterns, XSS prevention
- features/ai-service/CLAUDE.md — queue endpoints, provider notes
- features/doc-service/CLAUDE.md — document models, PDF limits, proxy endpoints

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 13:10:10 +02:00
curo1305 64808e0928 Edit the Workflow to include a plan phase and branching. 2026-04-18 12:53:50 +02:00
curo1305 94901fc30f Redesign doc service UX for scale + add group-based document sharing
- Three-column layout: Sidebar + SourcePanel (views + searchable category tree) + main
- DocumentSlideOver (480px right panel): inline editing, type picker, AI suggestion confirm/reject,
  categories combobox, tags editor, sharing section, raw text, re-analyse/delete actions
- ManageCategoriesDialog: inline rename, delete with confirm, search filter
- DocumentsPage rewrite: filter chip system, multi-file upload queue, drag-and-drop overlay,
  bulk actions bar (share/delete), smart TanStack Query polling, URL-driven view state
- Sidebar simplified: per-category NavLinks removed; Documents = single NavLink under Apps
- Backend: document_shares table (migration 0004), share CRUD endpoints, shared-with-me view,
  N+1-safe share_count via GROUP BY, recipient download access, X-User-Groups header enforcement
- Gateway proxy: injects X-User-Groups header into all document + category proxy requests
- Backend users: GET /api/users/me/groups endpoint for share picker combobox
- CLAUDE.md, STATUS.md files, and changelog updated

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 12:46:43 +02:00
curo 08e7caac4c Merge pull request 'colorThemes' (#1) from colorThemes into main
Reviewed-on: #1
2026-04-18 11:05:41 +02:00
curo1305 f16c290b92 Consolidate doc-service settings to a single Save changes button
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>
2026-04-18 10:49:46 +02:00
curo1305 c45236651b Add service admin groups, combined settings pages, single Settings button
- 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>
2026-04-18 02:49:57 +02:00
curo1305 003fbee20f Move plugin settings access from sidebar to app card
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>
2026-04-18 02:31:12 +02:00
curo1305 18a638bc3a Fix plugin list bug and switch watcher to PollingObserver
- 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>
2026-04-18 02:25:16 +02:00
curo1305 00466a9801 Add generic plugin architecture and watch-directory feature
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>
2026-04-18 02:09:50 +02:00
curo1305 2d7207b62f Fix missing save_appearance_config import in settings router
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 01:52:35 +02:00
curo1305 608b0b7fe8 Add theming system: custom palettes, per-user colour mode, admin appearance page
- 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>
2026-04-18 01:46:17 +02:00
curo1305 da9b911f1e Add CLAUDE.md self-update checkpoint
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>
2026-04-17 21:27:47 +02:00
curo1305 b2faf24ccc Rewrite CLAUDE.md as permanent authoritative session context
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>
2026-04-17 21:26:23 +02:00
curo1305 ab15c17ffb Add customizable home dashboard with per-user pinned apps
- 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>
2026-04-17 21:15:33 +02:00
curo1305 6d626ff266 Make bcrypt work factor explicit (13 rounds)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 20:58:02 +02:00
curo1305 a28f847572 Reduce retry count and show errors on admin pages
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>
2026-04-17 20:55:13 +02:00
curo1305 4e9ed97b05 Add Groups management and split Admin navigation
- 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>
2026-04-17 20:49:54 +02:00
curo1305 2bb1e03adf Update gitignore 2026-04-17 20:36:08 +02:00
curo1305 714dc718f2 Remove 'All documents' sub-item; Documents label now links to /apps/documents
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 18:30:05 +02:00
curo1305 151773ab51 Fix health check loop silently dying on uncaught exception
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>
2026-04-17 17:36:58 +02:00
curo1305 3248607790 Add service health checks and dynamic Apps page
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>
2026-04-17 17:31:36 +02:00
curo1305 1f8f866414 Split Apps sidebar item: label links to /apps, chevron toggles sub-nav
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 17:25:57 +02:00
curo1305 d2042153a7 Add re-analyse button and POST /documents/{id}/reprocess endpoint
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>
2026-04-17 17:00:17 +02:00
curo1305 7d0edbd5e7 Add sidebar app sub-nav with categories, category filter, and re-analysis on category creation
- 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>
2026-04-17 16:57:35 +02:00
curo1305 bc7a74062d Add reset-to-default button and how-to docs to system prompt editor
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>
2026-04-17 15:17:55 +02:00
curo1305 1d01cc3b0e Add per-service system prompts with AI Settings tab view
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>
2026-04-17 15:11:40 +02:00
curo1305 3a501f7e05 Always render text fields with white bg + black text
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>
2026-04-17 15:04:36 +02:00
curo1305 07c2428609 Improve button visibility and darken dark mode text further
- 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>
2026-04-17 14:55:36 +02:00