254e756cb8
- 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
163 lines
9.2 KiB
Markdown
163 lines
9.2 KiB
Markdown
---
|
||
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 <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*
|