wip: Phase 3 UAT in progress — 2/10 tests pass, upload XHR bug open

Fixes applied this session:
- frontend/src/api/client.js: noRefreshPaths exclusion prevents auth 401s
  from triggering session-expired error on login/register/refresh
- frontend/src/router/index.js: async beforeEach with silent refresh()
  restores session from httpOnly cookie on page reload

UAT state: 2 pass (cold-start, admin block), 1 open (XHR upload network error),
7 pending. MinIO PUT fails in browser — needs console output to diagnose.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
curo1305
2026-05-24 21:26:50 +02:00
parent a5994d9ff4
commit b5dde2aad9
6 changed files with 299 additions and 5 deletions
+63
View File
@@ -0,0 +1,63 @@
{
"version": "1.0",
"timestamp": "2026-05-24T19:25:23Z",
"phase": "3",
"phase_name": "03-document-migration-multi-user-isolation",
"phase_dir": ".planning/phases/03-document-migration-multi-user-isolation",
"plan": "UAT",
"task": "Test-2-of-10",
"total_tasks": 10,
"status": "paused",
"completed_tasks": [
{"id": "UAT-1", "name": "Cold Start Smoke Test", "status": "pass"},
{"id": "UAT-7", "name": "Admin blocked from document content", "status": "pass"},
{"id": "BUG-1", "name": "Fix login Session expired bug (api/client.js noRefreshPaths)", "status": "done", "commit": "uncommitted"},
{"id": "BUG-2", "name": "Fix re-login on page reload (router/index.js async beforeEach)", "status": "done", "commit": "uncommitted"}
],
"remaining_tasks": [
{"id": "DIAG-1", "name": "Get browser console output for XHR PUT MinIO failure", "status": "blocked_human_action"},
{"id": "BUG-3", "name": "Fix XHR PUT to MinIO network error", "status": "not_started"},
{"id": "UAT-2", "name": "Upload with XHR progress bar", "status": "issue_open"},
{"id": "UAT-3", "name": "QuotaBar displays in sidebar", "status": "pending"},
{"id": "UAT-4", "name": "Quota rejection error block", "status": "pending"},
{"id": "UAT-5", "name": "Quota decrements on document delete", "status": "pending"},
{"id": "UAT-6", "name": "Cross-user document isolation", "status": "pending"},
{"id": "UAT-8", "name": "Topics namespace-scoped", "status": "pending"},
{"id": "UAT-9", "name": "Settings page static placeholder", "status": "pending"},
{"id": "UAT-10", "name": "AI classification uses per-user provider", "status": "pending"}
],
"blockers": [
{
"description": "XHR PUT to MinIO fails in browser — 'Network Problem' error on upload",
"type": "technical",
"workaround": "Need browser DevTools console output (F12 → Console + Network tab during upload attempt) to confirm if CORS, connectivity, or presigned URL issue"
}
],
"human_actions_pending": [
{
"action": "Open browser DevTools, attempt upload, paste Console + Network tab errors",
"context": "XHR PUT to MinIO fails with network error. Need actual error to diagnose: CORS preflight failure, MinIO not at localhost:9000, or Content-Type mismatch in presigned URL signature.",
"blocking": true
}
],
"decisions": [
{
"decision": "noRefreshPaths exclusion in api/client.js request()",
"rationale": "Auth endpoints returning 401 for bad credentials (login, register, refresh) must not trigger the auto-refresh-on-401 logic",
"phase": "3"
},
{
"decision": "router.beforeEach made async with silent refresh attempt",
"rationale": "Standard SPA pattern for httpOnly refresh token cookies — access token is memory-only and gone on reload; refresh cookie persists",
"phase": "3"
}
],
"uncommitted_files": [
"frontend/src/api/client.js",
"frontend/src/router/index.js",
".planning/phases/03-document-migration-multi-user-isolation/03-UAT.md",
".planning/STATE.md"
],
"next_action": "Ask user for browser DevTools console output during upload attempt to diagnose MinIO XHR PUT failure",
"context_notes": "UAT for Phase 3 is in progress. 2 of 10 tests passed, 1 issue open (upload XHR), 7 pending. Two bugs fixed this session (login error message, session restore on reload). The upload XHR failure is the main blocker — once fixed, most remaining tests should pass quickly."
}
+1 -1
View File
@@ -4,7 +4,7 @@ milestone: v1.0
milestone_name: milestone milestone_name: milestone
current_phase: 3 current_phase: 3
status: executing status: executing
last_updated: "2026-05-23T23:47:54.258Z" last_updated: "2026-05-24T19:21:17.122Z"
progress: progress:
total_phases: 5 total_phases: 5
completed_phases: 3 completed_phases: 3
@@ -0,0 +1,130 @@
---
context: phase
phase: 03-document-migration-multi-user-isolation
task: UAT-in-progress
total_tasks: 10
status: in_progress
last_updated: 2026-05-24T19:25:23Z
---
# BLOCKING CONSTRAINTS — Read Before Anything Else
> These are not suggestions. Each constraint below was discovered through failure.
> Acknowledge each one explicitly before proceeding.
- [ ] CONSTRAINT: XHR-MINIO-CORS — The XHR PUT to MinIO from the browser fails with a network error ("Network Problem"). Root cause not yet confirmed — need browser DevTools console output (CORS error vs connection refused vs 4xx). Do NOT assume it's fixed until user confirms upload works.
- [ ] CONSTRAINT: UAT-PARTIAL — The Phase 3 UAT session is in progress (03-UAT.md exists). Tests 310 are pending. Do NOT mark Phase 3 complete until UAT is finished and all blockers resolved.
**Do not proceed until all boxes are checked.**
## Critical Anti-Patterns
| Pattern | Description | Severity | Prevention Mechanism |
|---------|-------------|----------|---------------------|
| Auth 401 intercept on login | `request()` in api/client.js was intercepting 401 from the login endpoint and trying to refresh — throwing "Session expired" instead of real error | advisory | noRefreshPaths exclusion now in place; already fixed |
| Router sync-only guard | `router.beforeEach` was synchronous — on page reload access token is null (memory-only), guard redirected to /login before refresh cookie could restore session | advisory | Fixed: guard is now async and attempts refresh() before redirecting |
<current_state>
Phase 3 UAT is in progress. Two bugs found and fixed this session:
1. Login "Session expired" bug — fixed in frontend/src/api/client.js (noRefreshPaths exclusion)
2. Re-login on page reload — fixed in frontend/src/router/index.js (async beforeEach with refresh())
One bug is OPEN and blocking UAT test 2 (upload with XHR progress bar):
- Uploading a file shows "Network Problem" error
- The document appears as PENDING after switching topics (upload-url step worked, XHR PUT failed)
- No quota update (fetchQuota never called because upload threw before that)
- Root cause: XHR PUT to MinIO at localhost:9000 is failing — likely CORS preflight or connectivity issue
- NEXT ACTION: Ask user to open browser DevTools (F12 → Console + Network tab), attempt upload, and paste what errors appear
Tests 210 are pending in the UAT file. Test 7 (admin blocked from doc content) was marked pass from user observation.
</current_state>
<completed_work>
This session (UAT + bug fixes):
- Created `.planning/phases/03-document-migration-multi-user-isolation/03-UAT.md` with 10 tests
- Test 1 (Cold Start Smoke Test): PASS
- Test 7 (Admin blocked from document content): PASS — "Admin accounts cannot access document content" correct behavior
- Fixed BUG-1: Login shows "Session expired" on wrong credentials
- Root cause: `request()` intercepts 401 from /api/auth/login and tries refresh → throws "Session expired"
- Fix: `noRefreshPaths = ['/api/auth/login', '/api/auth/register', '/api/auth/refresh']` in client.js:24
- Verified: user confirmed login now works
- Fixed BUG-2: Re-login required on every page reload
- Root cause: `router.beforeEach` was sync, no refresh attempt — access token gone after reload
- Fix: Made `beforeEach` async, attempts `authStore.refresh()` before redirecting to /login
- Not yet verified by user (fix applied at end of session)
</completed_work>
<remaining_work>
IMMEDIATE — before resuming UAT:
1. Ask user to open DevTools Console + Network tab, attempt upload, paste the error
2. Diagnose XHR PUT failure (likely one of):
- CORS preflight blocked: MinIO not returning Access-Control-Allow-Origin for PUT
- MinIO not accessible at localhost:9000 from browser
- Content-Type mismatch in presigned URL signature vs XHR header
3. Apply fix based on console output
4. Verify upload works (progress bar, document appears with status=uploaded, not pending)
5. Verify QuotaBar updates after upload
THEN continue UAT:
- Test 2 (Upload with XHR progress bar) — re-test after fix
- Test 3 (QuotaBar displays in sidebar) — should work after upload fix
- Test 4 (Quota rejection error block) — need user to set very small quota via admin
- Test 5 (Quota decrements on delete)
- Test 6 (Cross-user document isolation — 404 not 403)
- Test 8 (Topics namespace-scoped)
- Test 9 (Settings page static placeholder)
- Test 10 (AI classification uses per-user provider)
</remaining_work>
<decisions_made>
- noRefreshPaths exclusion pattern: auth endpoints that return 401 for bad credentials (not expired tokens) must skip the auto-refresh logic in request()
- Router beforeEach made async to support silent session restore on page reload — this is the standard SPA pattern for httpOnly refresh token cookies
- "Network Problem" root cause not yet confirmed — need console output before applying a fix (don't guess)
</decisions_made>
<blockers>
- XHR PUT to MinIO fails — "Network Problem" in browser. User needs to provide browser console output to confirm root cause. Likely CORS or connectivity issue with localhost:9000.
- UAT tests 310 blocked behind Test 2 (upload must work first for quota, isolation, and topic tests)
</blockers>
## Required Reading (in order)
1. `.planning/phases/03-document-migration-multi-user-isolation/03-UAT.md` — current UAT state (tests, gaps, fixes applied)
2. `frontend/src/api/client.js` lines 2035 — the noRefreshPaths fix applied this session
3. `frontend/src/router/index.js` lines 5060 — the async beforeEach fix applied this session
4. `frontend/src/stores/documents.js` — upload() function: 3-step flow, where "Network Problem" originates
## Infrastructure State
- docker compose: should be running (user did cold start smoke test — Test 1 passed)
- Frontend dev server: `cd frontend && npm run dev` on localhost:5173
- MinIO: accessible at localhost:9000 (port mapped in docker-compose) — BUT browser XHR PUT failing
- Test account: `testuser@docuvault.example` / `TestUser1234!` — confirmed working after login fix
## Pre-Execution Critique Required
N/A — not pausing between design and execution.
<context>
We are doing UAT for Phase 3 (Document Migration & Multi-User Isolation). Phase 3 had all 5 plans executed with SUMMARY files. The checkpoint from Plan 03-05 required browser testing, which we started doing.
Two bugs were found and fixed:
1. Login error message bug (noRefreshPaths in client.js)
2. Session restore on page reload (async router guard)
The upload flow (XHR PUT to MinIO) is broken in the browser but we ran out of context before diagnosing it. The document store's uploadToMinIO() function rejects with "Network error during upload" when the XHR onerror fires. The user sees "Network Problem" (likely paraphrasing). The MinIO CORS config is set via MINIO_API_CORS_ALLOW_ORIGIN env var in docker-compose, but something isn't working.
The PENDING document appearing in the list is because the upload-url endpoint creates a Document row with status='pending' before the XHR PUT. If the XHR fails, confirm is never called, but the pending row may still be returned by the list endpoint.
</context>
<next_action>
Start with: Ask user to open browser DevTools (F12), go to Console and Network tabs, attempt to upload a file, then paste:
1. Any red errors in Console (especially CORS errors)
2. The failed network request in Network tab (URL, status code, response)
This will tell us exactly what's failing in the MinIO PUT.
</next_action>
@@ -0,0 +1,93 @@
---
status: testing
phase: 03-document-migration-multi-user-isolation
source: 03-01-SUMMARY.md, 03-02-SUMMARY.md, 03-03-SUMMARY.md, 03-04-SUMMARY.md, 03-05-SUMMARY.md
started: 2026-05-24T00:00:00Z
updated: 2026-05-24T00:00:00Z
---
## Current Test
[paused — investigating XHR upload network error; session restore fix applied]
## Tests
### 1. Cold Start Smoke Test
expected: Kill any running services (docker compose down). Start fresh with docker compose up. All three services (PostgreSQL, MinIO, FastAPI backend) start without errors. Health checks pass. The frontend dev server (npm run dev) starts. Opening the app in the browser shows the login page with no console errors.
result: pass
### 2. Upload with XHR progress bar
expected: Log in as a regular user (testuser@docuvault.example / TestUser1234!). Drop or select a file to upload. A progress row appears for the file showing a progress bar that moves — starting near 5%, climbing to ~90% during the MinIO PUT, then jumping to 100% when confirmed. The file appears in the document list when complete.
result: issue
reported: "Shows error 'Network Problem' on upload. Document appears only after switching topics and back (PENDING row visible). No quota update shown. Page reload requires re-login."
severity: major
### 3. QuotaBar displays in sidebar
expected: After the upload completes, look at the left sidebar. A quota bar widget is visible below the navigation links. It shows used/total storage (e.g. "1.2 MB / 100 MB") with an indigo-colored fill bar. No error state or broken layout.
result: [pending]
### 4. Quota rejection error block
expected: Upload a file that would push usage over the user's quota limit (create a user via admin with a very small quota, e.g. 1 byte, or use an account already near-full). The upload row shows a red "Not enough storage" error block with role="alert", showing the rejected file size, current used bytes, and quota limit. A "Manage storage →" link appears. The quota bar does NOT increase past the limit.
result: [pending]
### 5. Quota decrements on document delete
expected: Note the current quota usage shown in the QuotaBar. Delete a document from the list. The QuotaBar updates to show reduced usage — the freed bytes are reflected immediately (or after a brief reload). No stale quota value persists.
result: [pending]
### 6. Cross-user document isolation (404 not 403)
expected: Log in as User A, upload a document, and copy its document ID from the URL or API. Log in as User B (register a second account if needed). Try to GET /api/documents/{that_id} as User B. The response is 404 — not 403, not the document content. User B cannot see User A's document through any URL manipulation.
result: [pending]
### 7. Admin blocked from document content endpoints
expected: Log in as admin (admin@docuvault.example / Admin1234!). Navigate to the main document list — it should be empty (admin has no personal documents). Try to access a regular user's document via GET /api/documents/{id} (e.g. via browser dev tools or curl with the admin JWT). The response is 403, not document content.
result: pass
### 8. Topics are namespace-scoped
expected: Log in as User A, upload a document. AI-suggested topics appear in the topic list filtered to User A's view. Log in as User B. Any custom topics created by User A are NOT visible to User B. System-wide topics (created by admin via /api/admin/topics) appear for all users. No cross-user topic leakage.
result: [pending]
### 9. Settings page shows static placeholder
expected: Log in as a regular user and navigate to /settings. The page shows a card with text indicating AI configuration is managed by the administrator — no editable form, no API key input fields, no provider dropdown. The page does not make any API calls for settings data.
result: [pending]
### 10. AI classification uses per-user assigned provider
expected: In the admin panel, assign a specific AI provider and model to a test user (e.g. ollama / llama3.2). Upload a document as that user. The document gets classified — check the backend logs or the document's topic tags. Classification ran with the user's assigned provider, not a global default from a settings file. (If no AI service is running, the Celery task may fail gracefully — verify the task attempted the correct provider.)
result: [pending]
## Summary
total: 10
passed: 2
issues: 1
pending: 7
skipped: 0
blocked: 0
## Gaps
- truth: "Regular user can log in with email/password and see the real error message on failure"
status: fixed
reason: "User reported: I cannot log into that user, I get the error message 'Session expired' — account exists in admin panel but login fails"
severity: major
test: 2
root_cause: "api/client.js request() intercepts every 401 and attempts token refresh — including the 401 from POST /api/auth/login on bad credentials. Refresh also fails → throws 'Session expired' instead of the real error."
artifacts:
- path: "frontend/src/api/client.js"
issue: "noRefreshPaths exclusion missing — auth endpoints must skip the refresh-on-401 logic"
missing:
- "Add noRefreshPaths guard: skip auto-refresh when path is /api/auth/login, /api/auth/register, /api/auth/refresh"
fix_applied: "Added noRefreshPaths = ['/api/auth/login', '/api/auth/register', '/api/auth/refresh'] exclusion in the 401 intercept block"
debug_session: ""
- truth: "New user can register via the UI and immediately log in with the created credentials"
status: fixed
reason: "User reported: cannot create a new user, or can create it but cannot login with that user"
severity: major
test: 2
root_cause: "Same root cause as above — successful registration followed by login attempt returns 401 (first login with fresh account) which triggers the buggy refresh path → 'Session expired' instead of completing login"
artifacts:
- path: "frontend/src/api/client.js"
issue: "Same noRefreshPaths exclusion missing"
missing: []
fix_applied: "Same fix — noRefreshPaths exclusion covers /api/auth/login"
debug_session: ""
+4 -1
View File
@@ -21,7 +21,10 @@ async function request(path, options = {}) {
const res = await fetch(path, { ...options, headers, credentials: 'include' }) const res = await fetch(path, { ...options, headers, credentials: 'include' })
// 401 → attempt refresh → retry once // 401 → attempt refresh → retry once
if (res.status === 401 && !options._retry) { // Skip refresh for auth endpoints: login/register return 401 for bad credentials (not expired tokens),
// and refresh itself must not retry to avoid an infinite loop.
const noRefreshPaths = ['/api/auth/login', '/api/auth/register', '/api/auth/refresh']
if (res.status === 401 && !options._retry && !noRefreshPaths.includes(path)) {
try { try {
await authStore.refresh() await authStore.refresh()
return request(path, { ...options, _retry: true }) return request(path, { ...options, _retry: true })
+7 -2
View File
@@ -46,12 +46,17 @@ const router = createRouter({
}) })
// Navigation guard (D-10): redirect unauthenticated users to /login. // Navigation guard (D-10): redirect unauthenticated users to /login.
// Preserves the intended destination via ?redirect= query param. // On page reload the access token is gone (memory-only per CLAUDE.md), so we attempt
router.beforeEach((to) => { // a silent refresh via the httpOnly cookie before concluding the session is gone.
router.beforeEach(async (to) => {
const authStore = useAuthStore() const authStore = useAuthStore()
if (!to.meta.public && !authStore.accessToken) { if (!to.meta.public && !authStore.accessToken) {
try {
await authStore.refresh()
} catch {
return { path: '/login', query: { redirect: to.fullPath } } return { path: '/login', query: { redirect: to.fullPath } }
} }
}
}) })
export default router export default router