docs(03-05): complete frontend presigned upload + quota bar plan

- 03-05-SUMMARY.md: plan summary covering 3-step XHR upload, QuotaBar.vue, UploadProgress error block, STORE-03/04/05 completed
- STATE.md: advance to plan 5 checkpoint pending; add 5 key decisions from plan 03-05
This commit is contained in:
curo1305
2026-05-23 22:02:03 +02:00
parent 23c568ae89
commit 254e756cb8
2 changed files with 173 additions and 7 deletions
+11 -7
View File
@@ -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 | <a href="/settings"> used instead of <router-link> 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 590 | 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` |
@@ -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 0100 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 0100 → visual range 590 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 <a href='/settings'> used in UploadProgress quota error block instead of <router-link> 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 `</nav>` 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 `<a href="/settings">`.
- **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 `<a href="/settings">` in quota error block rather than `<router-link>` — 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 | `<router-link to="/settings">` or plain `<a>` | Plain `<a href="/settings">` | Plan explicitly notes "use plain `<a href='/settings'>` 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 `<QuotaBar` and `QuotaBar from './QuotaBar.vue'`
- Frontend build exits 0 (`npm run build` — verified)
- No legacy `uploadDocument` references in `frontend/src/` (grep clean)
- Task 1 commit `eb18428` exists
- Task 2 commit `23c568a` exists
---
*Phase: 03-document-migration-multi-user-isolation*
*Completed: 2026-05-23*