MinIO's community edition has been trimming features and its licensing
direction has grown less predictable. Garage (AGPL, purpose-built for
self-hosting) is a drop-in S3-compatible replacement — no changes needed
to backend/app/minio_client.py, which talks to the S3 API generically.
Deployed independently from the app stack (storage/docker-compose.yml),
same separation MinIO had. Single-node config (replication_factor = 1)
matching this project's learning/small-team scope. init-garage.sh handles
the one-time layout assignment + bucket creation Garage requires that
MinIO didn't (MinIO auto-creates buckets via API; Garage needs an
explicit `garage bucket create` step).
MinIO stays out of this compose file per the design spec — it's deployed
independently, with MINIO_ENDPOINT overridable via env for wherever it
actually lives. Meilisearch uses the official image directly (no
Dockerfile needed); frontend's VITE_API_BASE_URL is left empty since
nginx.conf already proxies /api/ to the backend service.
- Default CORS_ORIGINS to http://localhost:5173 instead of "*" (still overridable)
- Reject ".." and leading "/" in document keys/paths across get/save/delete/upload,
mapped to HTTP 400 via new InvalidPathError
- Add a delete button to DocumentEditor wired to deleteDocument + onDeleted,
giving the existing DELETE API an actual UI entry point
- Share a single lru_cache'd Meilisearch (and Minio) SDK client instance instead
of constructing duplicates in dependencies.py
- Add a startup check that creates the MinIO bucket if missing, tolerating
MinIO being unreachable at boot without crashing the app
Adds frontend/src/styles.css extracted from the confirmed prototype
(paper/ink/pine palette, Fraunces/Inter/IBM Plex Mono type system,
catalog-tag motif) and makes the minimal JSX adjustments needed for
each component to pick up the styling: wordmark heading, search icon,
upload button icon + modal header/footer, folder chevrons + file
icons, doc-path + save-status in the editor, catalog tags and
no-results treatment in search results.
Also adds .gitignore (venv/node_modules/dist/pycache) and commits the
frontend package-lock.json that was previously untracked.
- client.test.ts: use globalThis.fetch instead of global.fetch and drop the
unused describe import, fixing the broken `npm run build`
- DocumentEditor: separate load-error (replaces view) from save-error (shown
inline, editor and unsaved content stay mounted so a failed save no longer
destroys in-progress edits); expose dirty state via onDirtyChange
- App: prompt via window.confirm before switching documents while dirty;
increment a refreshKey on successful upload so FileExplorer re-fetches the
tree; track isSearching so the "no results" overlay doesn't flash during
the search debounce window
- FileExplorer: accept a refreshKey prop to force a root-listing re-fetch
- SearchBar: convert to a controlled component (value prop) so App can clear
the input after jumping to a search result, fixing input/state desync
- UploadButton: surface upload failures inline instead of swallowing the
rejected promise, and keep the modal open for retry
- main.py: remove duplicate /health endpoint definition
- text_extract.py: HTML-escape extracted plain text before indexing so raw
markup in uploaded documents can't render as live HTML via search snippets
(stored XSS fix)
- document_service.py: base64-encode content when UTF-8 decoding fails
instead of raising UnicodeDecodeError (500) on binary files; add upload
validation for file extension (allowlist) and size (10MB max)
- routers/documents.py: map new validation errors to HTTP 400 with a clear
detail message instead of letting them 500
- add/extend tests covering escaping, binary get_document, and upload
validation rejection + acceptance paths