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