diff --git a/.planning/STATE.md b/.planning/STATE.md index 11193aa..b26cadd 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -16,7 +16,7 @@ progress: # Project State **Project:** DocuVault -**Status:** Phase 3 In Progress — Plan 04 Complete +**Status:** Phase 3 In Progress — Plan 05 Tasks 1-2 Complete (awaiting human checkpoint) **Current Phase:** 3 **Last Updated:** 2026-05-23 @@ -26,15 +26,15 @@ progress: |---|---|---| | 1 | Infrastructure Foundation | ✓ Complete | | 2 | Users & Authentication | ✓ Complete (5/5 plans) | -| 3 | Document Migration & Multi-User Isolation | In Progress (4/5 plans complete) | +| 3 | Document Migration & Multi-User Isolation | In Progress (5/5 plans — checkpoint pending) | | 4 | Folders, Sharing, Quotas & Document UX | Not Started | | 5 | Cloud Storage Backends | Not Started | ## Current Position **Phase:** 03-document-migration-multi-user-isolation — In Progress -**Plan:** 4/5 complete (Plan 04: Flat-file settings retirement + per-user AI classification) -**Progress:** ████░░░░░░ 53% (2/5 phases complete, 14/15 plans done) +**Plan:** 5/5 tasks 1-2 done; Task 3 checkpoint awaiting human verification +**Progress:** ████░░░░░░ 57% (2/5 phases complete, 14/15 plans committed; Phase 3 checkpoint pending) ## Performance Metrics @@ -100,6 +100,10 @@ progress: | _DEFAULT_SYSTEM_PROMPT in classifier.py | System prompt env var is optional; hardcoded fallback kept in classifier module not config.py (D-13) | | Default AI provider is ollama/llama3.2 | Code defaults; overridable via DEFAULT_AI_PROVIDER / DEFAULT_AI_MODEL env vars (D-15) | | /settings route kept as static placeholder | SettingsView shows admin-managed card; route not removed to avoid UX regression (Risk 6) | +| Plain anchor in quota rejection block | used instead of to avoid import dependency in upload component | +| uploadProgress entries owned by parent | Store does not clear uploadProgress map entries after upload; DropZone/parent clears on row dismiss | +| fetchQuota silent catch in auth store | Silent catch keeps last-known values; QuotaBar owns loadFailed state and hides on error (UI-SPEC) | +| XHR PUT progress range 5–90 | 5 + Math.round(pct * 0.85) maps XHR 0-100 → visual 5-90; remaining 10% covers confirm + enqueue | ### Open Questions @@ -115,7 +119,7 @@ _Updated at each phase transition._ | Field | Value | |---|---| -| Last session | 2026-05-23 — Executed Plan 03-04 (flat-file settings retirement, per-user AI classification, frontend placeholder) | -| Next action | Run `/gsd:execute-phase 3` to execute Plan 03-05 | +| Last session | 2026-05-23 — Executed Plan 03-05 (3-step XHR upload, QuotaBar, UploadProgress error block) | +| Next action | Human checkpoint Task 3: test upload/quota/413 flow in browser; type "approved" or describe failures | | Pending decisions | None | -| Resume file | `.planning/phases/03-document-migration-multi-user-isolation/03-05-PLAN.md` | +| Resume file | `.planning/phases/03-document-migration-multi-user-isolation/03-05-SUMMARY.md` | diff --git a/.planning/phases/03-document-migration-multi-user-isolation/03-05-SUMMARY.md b/.planning/phases/03-document-migration-multi-user-isolation/03-05-SUMMARY.md new file mode 100644 index 0000000..a5978eb --- /dev/null +++ b/.planning/phases/03-document-migration-multi-user-isolation/03-05-SUMMARY.md @@ -0,0 +1,162 @@ +--- +phase: 03-document-migration-multi-user-isolation +plan: "05" +subsystem: ui,frontend +tags: [vue3, pinia, xhr, minio, presigned-upload, quota, progressbar, tailwind] + +# Dependency graph +requires: + - phase: 03-04 + provides: "getMyQuota, getUploadUrl, confirmUpload in api/client.js; per-user AI classification wired" + - phase: 03-03 + provides: "per-user document/topic isolation; /api/documents/{id}/confirm endpoint (HTTP 413 on quota exceeded)" + +provides: + - "Three-step presigned upload flow (getUploadUrl → XHR PUT to MinIO → confirmUpload) with XHR progress events mapped to 0–100 visual range" + - "uploadProgress reactive map in documents store (keyed by filename+timestamp, T-03-25)" + - "authStore.quota ref({used_bytes, limit_bytes}) + fetchQuota() action updated after every upload/delete" + - "QuotaBar.vue sidebar widget: skeleton loading, color thresholds <80% indigo-500 / 80-95% amber-500 / >=95% red-500" + - "UploadProgress.vue: aria progressbar per row, percentage label, inline 413 quota rejection error block with role=alert" + - "api/client.js request() attaches .status and .payload to thrown errors (structured 413 detection)" + - "Legacy uploadDocument multipart function removed from api/client.js" + +affects: + - 03-verify + +# Tech tracking +tech-stack: + added: [] + patterns: + - "XHR PUT presigned upload: bare XMLHttpRequest with no Authorization header (T-03-22 — presigned URL is self-authenticating)" + - "Progress mapping: XHR 0–100 → visual range 5–90 via 5 + Math.round(pct * 0.85)" + - "uploadProgress map key: file.name__Date.now() composite to prevent race conditions on duplicate filenames (T-03-25)" + - "Structured 413 handling: request() detects typeof body.detail === 'object', attaches err.payload = body.detail" + - "fetchQuota: silent catch in auth store (QuotaBar owns error display via v-if !loadFailed)" + - "QuotaBar v-if !loadFailed: hides entire DOM on fetch error (UI-SPEC Loading and Error States)" + +key-files: + created: + - frontend/src/components/layout/QuotaBar.vue + modified: + - frontend/src/api/client.js + - frontend/src/stores/auth.js + - frontend/src/stores/documents.js + - frontend/src/components/upload/UploadProgress.vue + - frontend/src/components/layout/AppSidebar.vue + +key-decisions: + - "Plain used in UploadProgress quota error block instead of to avoid import dependency in an upload component (plan note: 'avoid router-link import complexity')" + - "uploadProgress map entries are NOT deleted after upload completes — parent component (DropZone) is responsible for clearing; this keeps the bar/error visible until user dismisses the row" + - "QuotaBar.vue owns loadFailed state; authStore.fetchQuota() silently ignores errors — division of responsibility per UI-SPEC" + - "Frontend build warning about mixed dynamic/static import of auth.js is pre-existing (api/client.js uses lazy import to break circular dep) — not introduced by this plan" + +requirements-completed: + - STORE-03 + - STORE-04 + - STORE-05 + +# Metrics +duration: 7min +completed: 2026-05-23 +--- + +# Phase 03 Plan 05: Frontend Presigned Upload + Quota Bar Summary + +**Three-step presigned upload (XHR PUT to MinIO with progress events) + sidebar QuotaBar + inline 413 quota rejection error block wired to authStore.quota reactive state** + +## Performance + +- **Duration:** 7 min +- **Started:** 2026-05-23T18:42:50Z +- **Completed:** 2026-05-23T18:49:24Z +- **Tasks:** 2 (+ 1 checkpoint awaiting human verification) +- **Files modified:** 5 modified, 1 created + +## Accomplishments + +- **3-step upload flow (STORE-03):** `stores/documents.js` replaces the legacy `api.uploadDocument()` multipart call with: (1) `api.getUploadUrl()` → (2) `uploadToMinIO()` XHR PUT with `xhr.upload.addEventListener('progress', ...)` → (3) `api.confirmUpload()`. Progress mapped 0%→5%→90%→92%→100% per UI-SPEC. No Authorization header on MinIO PUT (T-03-22). `uploadProgress` ref map keyed by `${file.name}__${Date.now()}` (T-03-25). + +- **Quota state in auth store (STORE-04):** `stores/auth.js` adds `quota = ref({used_bytes:0, limit_bytes:0})` and `fetchQuota()` async action. Called after every successful upload and every document delete. Silent catch keeps last-known values intact on error. + +- **QuotaBar.vue sidebar widget (STORE-04):** New component at `frontend/src/components/layout/QuotaBar.vue`. onMounted triggers fetchQuota. Computed `pct/barColor/labelColor/label` from authStore.quota. Color thresholds: `bg-indigo-500` (<80%), `bg-amber-500` (80-95%), `bg-red-500` (>=95%). Skeleton loading state; v-if hides entirely on loadFailed. Embedded in AppSidebar between `` and footer `div`. + +- **Quota rejection error block (STORE-05):** `UploadProgress.vue` renders inline red error block (`role="alert"`) when `item.quotaError` is set — populated from `err.payload` on 413. Shows rejected_bytes, used_bytes, limit_bytes from server response (T-03-23 — never from client-side file.size). "Manage storage →" uses plain ``. + +- **api/client.js structured 413:** `request()` extended — `if (!res.ok)` block detects `typeof body.detail === 'object'`, attaches `err.status = res.status` and `err.payload = body.detail`. Legacy `uploadDocument()` multipart function removed. + +## Task Commits + +1. **Task 1: Refactor stores + api/client + UploadProgress** — `eb18428` (feat) +2. **Task 2: Create QuotaBar.vue + embed in AppSidebar** — `23c568a` (feat) + +## Files Created/Modified + +- `frontend/src/api/client.js` — Extended request() error handling; removed uploadDocument legacy multipart +- `frontend/src/stores/auth.js` — Added quota ref + fetchQuota action +- `frontend/src/stores/documents.js` — 3-step upload with XHR, uploadProgress map, fetchQuota calls +- `frontend/src/components/upload/UploadProgress.vue` — Progress bar per row, quota rejection error block +- `frontend/src/components/layout/QuotaBar.vue` — New: sidebar quota widget +- `frontend/src/components/layout/AppSidebar.vue` — Import + embed QuotaBar + +## Decisions Made + +- Plain `` in quota error block rather than `` — avoids router-link import dependency in an upload component that may not have router context in all test scenarios. Functional equivalence for the Phase 3 use case. +- `uploadProgress` entries not cleared by the store — DropZone/parent component owns row lifecycle and clears on dismiss. Keeps progress bar/error visible until user action. + +## Deviations from Plan + +None — plan executed exactly as written. All implementation notes from the plan prompt were followed precisely. + +## UI-SPEC Deviations + +| Element | UI-SPEC | Implemented | Reason | +|---------|---------|-------------|--------| +| Manage storage → link | `` or plain `` | Plain `` | Plan explicitly notes "use plain `` to avoid router-link import complexity" | +| Upload progress track bg | `bg-gray-100` | `bg-gray-100` | Matches | +| QuotaBar track bg | `bg-gray-200` | `bg-gray-200` | Matches | + +No functional UI-SPEC deviations. + +## Issues Encountered + +None. + +## Known Stubs + +None — all data flows are wired to real API responses. QuotaBar reads from authStore.quota which is populated by /api/auth/me/quota. UploadProgress quota error block reads from 413 response body (not client-side file.size). + +## Threat Flags + +No new threat surface introduced — this plan modifies only frontend JavaScript files. All security mitigations from the threat register implemented: + +| Threat ID | Status | +|-----------|--------| +| T-03-22 | MITIGATED — uploadToMinIO helper uses bare XMLHttpRequest with NO Authorization header | +| T-03-23 | MITIGATED — Quota rejection error block populates used_bytes/limit_bytes/rejected_bytes from 413 response body only | +| T-03-25 | MITIGATED — uploadProgress key = `${file.name}__${Date.now()}` composite | +| T-03-26 | MITIGATED — authStore.fetchQuota wraps in try/catch; QuotaBar hides on loadFailed | + +## Next Phase Readiness + +- Phase 3 is now feature-complete pending human checkpoint (Task 3) approval +- Task 3 human checkpoint requires: docker compose running, dev server at localhost:5173, logged-in non-admin user with quota row +- After checkpoint approval: Phase 3 verification (`/gsd:verify-work 3`) can begin +- Phase 4 (folder navigation, PDF preview, sharing) has no blockers from Phase 3 + +## Self-Check: PASSED + +- `frontend/src/components/layout/QuotaBar.vue` exists +- `frontend/src/api/client.js` contains `getMyQuota` (grep count: 1) and does NOT contain `uploadDocument` +- `frontend/src/stores/documents.js` contains `uploadToMinIO`, `XMLHttpRequest`, `getUploadUrl`, `confirmUpload`, `fetchQuota` +- `frontend/src/stores/auth.js` contains `fetchQuota` (count: 2) +- `frontend/src/components/upload/UploadProgress.vue` contains `role="progressbar"` and `Not enough storage` +- `frontend/src/components/layout/QuotaBar.vue` contains `role="progressbar"` and `useAuthStore` +- `frontend/src/components/layout/AppSidebar.vue` contains `