- Phase 6.2 CONTEXT.md: cloud-delete propagation, SHARE-03/05, audit log CSV export fix, daily export UI, user handle display - Fix: admin create_user missing session.flush() before write_audit_log caused FK violation on PostgreSQL (silent on SQLite) - Regression test: test_create_user_writes_audit_log in test_admin_api.py Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
13 KiB
Phase 6.2: Close v1 sharing + cloud-delete + CSV export gaps - Context
Gathered: 2026-05-31 Status: Ready for planning
## Phase BoundaryClose three categories of v1 gaps discovered during manual UAT:
-
Admin user creation 500 (fixed during discussion — regression test added):
create_usercalledwrite_audit_logbefore flushing the new User to the DB, causing a FK violation on PostgreSQL. Fix:await session.flush()added beforewrite_audit_loginbackend/api/admin.py. -
Sharing: SHARE-05 "shared" badge mismatch (frontend checks
doc.share_countbut backend sendsdoc.is_shared); SHARE-03 permission level control missing (backend hardcodespermission="view"; no UI or PATCH endpoint to change it). -
Cloud-delete:
delete_document()inservices/storage.pyalways calls MinIOdelete_object()regardless ofdoc.storage_backend. Cloud-stored documents (Google Drive, OneDrive, Nextcloud, WebDAV) are removed from the DB but the file is never deleted from the provider. -
Audit log / CSV export: Export button uses
window.location.hrefwhich cannot carry the in-memory access token → 401. Audit log table shows raw UUIDs instead of user handles. User filter accepts any string but backend expects a UUID (422 silently swallowed → empty results). Daily Celery exports land in MinIO with no UI to list or download them.
No new user-facing features. All changes close existing v1 requirement gaps.
## Implementation DecisionsBug Fixed During Discussion (pre-phase)
- D-00:
backend/api/admin.pycreate_user— addedawait session.flush()aftersession.add(quota)and beforewrite_audit_log. Mirrorsauth.py:177pattern. Regression testtest_create_user_writes_audit_logadded totests/test_admin_api.py. This fix is already committed.
Cloud-Delete Propagation
- D-01: Default delete (existing delete button) propagates to the cloud provider.
delete_document()inservices/storage.pymust checkdoc.storage_backendand, for cloud docs, callget_storage_backend_for_document(doc)to get the correct cloud backend, then callcloud_backend.delete_object(doc.object_key). - D-02: "Remove from app" is a separate, distinct action — removes the DocuVault DB record only; the file on the cloud provider is preserved. This requires a new API endpoint (e.g.,
DELETE /api/documents/{id}?remove_only=trueor a separatePOST /api/documents/{id}/remove-local). Claude decides the cleanest route. - D-03: Cloud provider delete failure handling: show a warning modal to the user ("Cloud delete failed. Remove from app anyway?"). User chooses. If user confirms removal, delete the DB record without deleting the cloud file (best-effort). The delete endpoint must return a structured error response that the frontend can distinguish from a hard failure.
- D-04: Cloud documents do NOT affect MinIO quota — unchanged from existing design. Cloud uploads already skip the quota UPDATE; deletes should also skip it.
- D-05 (DEFERRED): Persistent local Celery cache in MinIO for cloud docs — not part of this phase. When Celery analyses a cloud doc, the temp file is discarded as today; no persistent local copy is created or quota-tracked.
Sharing — SHARE-05 Badge Fix
- D-06:
frontend/src/components/documents/DocumentCard.vue:31— changev-if="doc.share_count > 0"tov-if="doc.is_shared". The backend already returnsis_shared: boolin the document list response. No backend change needed.
Sharing — SHARE-03 Permission Level Control
- D-07: Permission levels:
view(default) andedit. TheShare.permissioncolumn already exists. No migration needed. - D-08: Permission set at share creation time: add a
view/editdropdown toShareModal.vuebefore submitting the share. The existingPOST /api/sharesendpoint already accepts apermissionfield. - D-09: Permission changeable after creation: add a View/Edit toggle per share row in the existing shares list inside
ShareModal.vue. Calls a newPATCH /api/shares/{id}endpoint with{ permission: "view" | "edit" }. Backend must enforce ownership (share owner only; 404 for wrong owner). - D-10: The backend
permissionfield is already stored but the POST handler hardcodespermission="view"(line 97 ofshares.py). Fix: readpermissionfrom the request body (add it to the request Pydantic model).
Audit Log — User Handles
- D-11:
_audit_to_dict()inbackend/api/audit.pycurrently returnsuser_idandactor_idas UUID strings. Extend it to also returnuser_handleandactor_handleby joining theUsertable. The frontend already tries to renderentry.user_handle || entry.user_id || '—'. - D-12: The
user_idfilter inGET /api/admin/audit-logcurrently expects a UUID. Change it to accept a handle string: look upUser.handle == handle, resolve to UUID, then apply the filter. If no user with that handle exists, return empty results (not an error).
Audit Log — CSV Export Fix
- D-13: Replace
window.location.href = ...inAuditLogTab.vue:exportCsv()with afetch()+ Blob URL pattern. The access token lives in Pinia memory and must be sent as anAuthorizationheader — a browser navigation cannot do this. - D-14: Add
adminExportAuditLogCsv(params)tofrontend/src/api/client.js. This function must NOT callres.json()— it must callres.text()(orres.blob()) to receive CSV content. Then create an object URL and trigger an<a>click to download.
Audit Log — Daily Export UI
- D-15: Add
GET /api/admin/audit-log/daily-exportsendpoint: list available daily export files from the MinIOaudit-logsbucket. Returns[{ date: "2026-05-30", key: "audit-logs/2026-05-30.csv" }]sorted descending. - D-16: Add
GET /api/admin/audit-log/daily-exports/{date}endpoint: serve a specific daily export file from MinIO as a streaming text/csv response. Uses the same auth gate (get_current_admin). The filename key pattern isaudit-logs/{date}.csv(as written byaudit_tasks.py:79). - D-17: Frontend
AuditLogTab.vue: add a searchable date dropdown populated fromadminListDailyExports()API call. A "Download" button fetches the selected date viafetch()+ Blob URL (same pattern as D-13/D-14).
Claude's Discretion
- Exact API shape for "remove from app only" vs "delete from provider" — Claude picks the cleanest route (
?cloud_only=falsequery param vs separate endpoint). - Whether
PATCH /api/shares/{id}accepts the full share body or just{ permission: "view"|"edit" }— minimal body preferred. - Exact error response shape for cloud delete failure — must be distinguishable from a hard 4xx/5xx by the frontend.
- MinIO
list_objectspagination — Claude handles if the audit-logs bucket has more than 1000 files.
<canonical_refs>
Canonical References
Downstream agents MUST read these before planning or implementing.
Phase Goal and Requirements
.planning/ROADMAP.md§"Phase 6.2" — Goal and success criteria (TBD; use decisions above as the source of truth)..planning/REQUIREMENTS.md§SHARE-01..05 — Sharing requirements (SHARE-03, SHARE-05 are the open ones)..planning/REQUIREMENTS.md§ADMIN-06 — Admin audit log with filters and export..planning/phases/06.1-close-v1-audit-gaps/06.1-VERIFICATION.md— Gap #10 (filter behavioral test) and Gap #11 (STORE-06 integration gate).
Security Mandates
CLAUDE.md§"Key Architectural Rules" — JWT in Pinia memory only (never localStorage); admin never returns document content; atomic quota UPDATE pattern.CLAUDE.md§"Security Protocol" — bandit/pip audit/npm audit gates; admin endpoint whitelist.
Existing Implementation — Backend
backend/api/shares.py— current share API (POST/GET/DELETE);permission="view"hardcoded at line 97; PATCH endpoint missing.backend/api/audit.py—_audit_to_dict(),_build_filtered_query(), both endpoints; CSV StreamingResponse pattern.backend/services/storage.py:143—delete_document(): MinIO-only delete path; cloud backend routing missing.backend/tasks/audit_tasks.py— Celery daily export; key patternaudit-logs/{yesterday.isoformat()}.csv; bucketaudit-logs.backend/db/models.py—Share.permissioncolumn (line 256);AuditLogmodel (line 267).
Existing Implementation — Frontend
frontend/src/components/documents/DocumentCard.vue:31—share_count > 0bug; fix tois_shared.frontend/src/components/sharing/ShareModal.vue— share creation form and existing shares list with Revoke button.frontend/src/components/admin/AuditLogTab.vue— filter UI,exportCsv()withwindow.location.href(broken);fetchLog().frontend/src/api/client.js—request()function (always callsres.json());adminListAuditLog().frontend/src/views/SharedView.vue— "Shared with me" view (SHARE-02).
Cloud Backend Patterns
backend/storage/__init__.py—get_storage_backend_for_document()factory; use this for cloud-aware delete routing.backend/api/admin.py:481—delete_user()cloud cleanup pattern: iterates cloud connections, gets backend, callsdelete_user_files()— reference for how to call cloud backends.backend/storage/cloud_utils.py—decrypt_credentials()HKDF pattern used when constructing cloud backends.
</canonical_refs>
<code_context>
Existing Code Insights
Reusable Assets
backend/services/storage.py:delete_document()— extend this function; it already handles MinIO delete + quota decrement. Add cloud routing before the MinIO branch.backend/storage/__init__.py:get_storage_backend_for_document()— already resolves the correct backend given a Document ORM object. Use it directly in delete_document().backend/api/audit.py:_build_filtered_query()— shared filter logic already works; only the handle→UUID resolution is missing.backend/db/models.py:Share.permission— column exists, default "view". No migration needed.frontend/src/components/sharing/ShareModal.vue— shares list with Revoke button already rendered; add View/Edit toggle to each row.
Established Patterns
backend/api/admin.py:delete_user()— cloud cleanup: get_storage_backend_for_document() + backend.delete_user_files() pattern to follow for per-document cloud delete.backend/api/auth.py:177—await session.flush()before audit log write — the fix already applied to admin.py follows this pattern.- Cloud document content proxy in
backend/api/documents.py— usesfetch()+ streaming viaget_storage_backend_for_document(); similar pattern for cloud delete. backend/tasks/audit_tasks.py:put_object_raw()— how daily exports write to MinIO; reverse: useget_object()or presigned GET URL for the download endpoint.
Integration Points
backend/services/storage.py:delete_document()— add cloud routing here; the caller (api/documents.py:delete_document) doesn't need to change.backend/api/audit.py— add two new GET endpoints for daily export listing and download.backend/api/shares.py— add PATCH/api/shares/{id}endpoint + fix permission field on POST.frontend/src/api/client.js— addadminExportAuditLogCsv()andadminListDailyExports()+adminDownloadDailyExport()functions (returning text/blob, not JSON).
</code_context>
## Specific Ideas- Cloud delete failure UX: Warning modal with two options — "Delete from app only" and "Cancel". This mirrors the existing
UserDeleteConfirmpattern in Phase 5. - Daily export dropdown: Searchable
<select>or combobox, populated on tab open. Sorted newest-first. Date format:YYYY-MM-DD. If bucket is empty, show "No daily exports yet". - Audit log user display: Backend returns
user_handleandactor_handlealongside the UUID fields. Frontend table shows handle; UUID shown as tooltip or hidden. Filter input is a plain text field labeled "User handle". - PATCH /api/shares/{id}: Minimal body
{ "permission": "view" | "edit" }. Owner-only; 404 for wrong owner (same IDOR pattern as DELETE).
- Persistent local Celery cache for cloud docs with quota tracking — user wants cloud doc analysis to create a persistent MinIO copy that counts against quota, removable via "Remove download". Requires architectural changes to the Celery task and quota system. Future phase.
- Celery local cache "Remove download" button — depends on the deferred item above.
- SHARE-03 edit permission beyond view/edit — if more granular permissions are needed (e.g., comment, reshare), that's a future phase.
Phase: 6.2-Close v1 sharing + cloud-delete + CSV export gaps Context gathered: 2026-05-31