--- 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 `