Files
kite/.planning/phases/03-document-migration-multi-user-isolation/03-05-SUMMARY.md
T
curo1305 254e756cb8 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
2026-05-23 22:02:03 +02:00

163 lines
9.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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*