--- phase: 04-folders-sharing-quotas-document-ux plan: 09 type: execute wave: 7 depends_on: - "04-08" files_modified: - frontend/src/views/FolderView.vue - frontend/src/views/SharedView.vue - frontend/src/views/SettingsView.vue - frontend/src/views/DocumentView.vue - frontend/src/views/HomeView.vue - frontend/src/views/AdminView.vue - frontend/src/components/folders/FolderRow.vue - frontend/src/components/folders/FolderBreadcrumb.vue - frontend/src/components/folders/FolderDeleteModal.vue - frontend/src/components/sharing/ShareModal.vue - frontend/src/components/documents/DocumentPreviewModal.vue - frontend/src/components/documents/SearchBar.vue - frontend/src/components/documents/SortControls.vue - frontend/src/components/admin/AuditLogTab.vue - frontend/src/components/layout/AppSidebar.vue - frontend/src/components/documents/DocumentCard.vue - frontend/src/api/client.js autonomous: false requirements: - FOLD-01 - FOLD-02 - FOLD-03 - FOLD-04 - FOLD-05 - SHARE-01 - SHARE-02 - SHARE-03 - SHARE-04 - SHARE-05 - ADMIN-06 - DOC-01 - DOC-02 must_haves: truths: - "Folders section visible in AppSidebar above topics; top-level folders clickable to navigate" - "Shared with me entry in AppSidebar above folders; shows count badge when non-empty" - "Document list shows SearchBar and SortControls above; sort persists until changed" - "DocumentCard shows share button on hover; shared indicator pill when doc.share_count > 0" - "Share modal: handle input, Share button, current recipient list with Revoke button" - "FolderView renders sub-folders as FolderRow components + breadcrumb + document list" - "FolderDeleteModal shows document count and requires explicit confirmation before delete" - "PDF documents open in DocumentPreviewModal (in_app mode) or new tab (new_tab mode)" - "SettingsView Document Preferences card shows pdf_open_mode radio; auto-saves on change" - "Admin AuditLog tab is accessible from AdminView; filters and CSV export work" artifacts: - path: "frontend/src/views/FolderView.vue" provides: "Folder contents: sub-folder rows + breadcrumb + document list + new subfolder inline input" - path: "frontend/src/views/SharedView.vue" provides: "Shared with me virtual folder view" - path: "frontend/src/components/folders/FolderRow.vue" provides: "Folder row with three-dot menu (rename inline, delete folder) per UI-SPEC" - path: "frontend/src/components/folders/FolderBreadcrumb.vue" provides: "Breadcrumb nav with truncation at depth > 4; each segment clickable" - path: "frontend/src/components/folders/FolderDeleteModal.vue" provides: "Destructive confirmation modal with document count" - path: "frontend/src/components/sharing/ShareModal.vue" provides: "Share by handle + current recipients list + revoke" - path: "frontend/src/components/documents/DocumentPreviewModal.vue" provides: "In-app PDF preview via iframe" - path: "frontend/src/components/documents/SearchBar.vue" provides: "Debounced search input; clears on Escape" - path: "frontend/src/components/documents/SortControls.vue" provides: "Name / Date / Size sort toggle buttons" - path: "frontend/src/components/admin/AuditLogTab.vue" provides: "Paginated audit log table with date/user/action filters + CSV export button" key_links: - from: "frontend/src/views/FolderView.vue" to: "frontend/src/stores/folders.js" via: "useFoldersStore for folder data and navigation" pattern: "useFoldersStore" - from: "frontend/src/components/sharing/ShareModal.vue" to: "frontend/src/stores/documents.js" via: "shareDocument, revokeShare, listShares actions" pattern: "useDocumentsStore" - from: "frontend/src/components/layout/AppSidebar.vue" to: "frontend/src/stores/folders.js" via: "top-level folder list + currentFolderId for active state" pattern: "useFoldersStore" --- Build all Phase 4 Vue components and wire them into the existing views. This is the final frontend plan — all stores, API client, and routes are established by plan 04-08. Purpose: Deliver the complete document management UX visible to users and admins. Output: 10 new components + 6 modified views following 04-UI-SPEC.md exactly. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/phases/04-folders-sharing-quotas-document-ux/04-UI-SPEC.md @.planning/phases/04-folders-sharing-quotas-document-ux/04-CONTEXT.md @.planning/phases/04-folders-sharing-quotas-document-ux/04-PATTERNS.md @frontend/src/components/layout/AppSidebar.vue @frontend/src/components/documents/DocumentCard.vue @frontend/src/views/HomeView.vue @frontend/src/views/AdminView.vue @frontend/src/views/SettingsView.vue @frontend/src/views/DocumentView.vue @frontend/src/components/admin/AdminUsersTab.vue From 04-UI-SPEC.md — exact Tailwind classes for each new component are specified. Executor must read 04-UI-SPEC.md fully before writing any component template. Key design tokens from 04-UI-SPEC.md: - Modal overlay: `fixed inset-0 bg-black/40 flex items-center justify-center z-50` - Modal panel: `bg-white rounded-2xl shadow-xl p-6 max-w-md w-full mx-4` - Primary button: `bg-indigo-600 hover:bg-indigo-700 text-white text-sm px-4 py-2 rounded-lg` - Destructive button: `bg-red-600 hover:bg-red-700 text-white text-sm font-semibold px-4 py-2 rounded-lg min-h-[44px]` - FolderRow: `flex items-center gap-3 px-4 py-3 bg-white border border-gray-200 rounded-xl hover:border-indigo-300 hover:shadow-sm transition-all cursor-pointer` - Share badge (SHARE-05): `bg-indigo-50 text-indigo-600 text-xs font-medium px-2 py-1 rounded-full` - Shared with me icon: `bg-purple-50 text-purple-500` (only purple UI element) - Section label: `text-xs font-semibold text-gray-400 uppercase tracking-wider mb-1` - Accessibility: all icon-only buttons have aria-label; modals have role="dialog" aria-modal="true" Copywriting (from 04-UI-SPEC.md): - Share modal title: "Share document" - Handle input placeholder: "Enter username handle" - Share submit: "Share document" - Empty share state: "Not shared with anyone yet." - User not found: "User not found. Check the handle and try again." - Non-empty folder delete body: "This folder contains {N} document{s}. Deleting it will permanently delete all documents inside. This cannot be undone." - Folder delete confirm: "Delete folder and documents" - Folder delete dismiss: "Keep folder" - Search placeholder: "Search documents..." - PDF pref section heading: "Document Preferences" - PDF open modes: "Open documents in-app" / "Open documents in new tab" - Audit log tab label: "Audit Log" - Sort labels: "Name" / "Date" / "Size" Task 1: Create new components (FolderRow, FolderBreadcrumb, FolderDeleteModal, ShareModal, DocumentPreviewModal, SearchBar, SortControls, AuditLogTab) frontend/src/components/folders/FolderRow.vue, frontend/src/components/folders/FolderBreadcrumb.vue, frontend/src/components/folders/FolderDeleteModal.vue, frontend/src/components/sharing/ShareModal.vue, frontend/src/components/documents/DocumentPreviewModal.vue, frontend/src/components/documents/SearchBar.vue, frontend/src/components/documents/SortControls.vue, frontend/src/components/admin/AuditLogTab.vue frontend/src/components/admin/AdminUsersTab.vue — read the ENTIRE file; extract the form pattern (reactive form state, loading/error refs, onMounted fetch, table with filters, action buttons, inline confirm pattern); use this as the template for ShareModal and AuditLogTab frontend/src/components/ui/ConfirmBlock.vue — read fully for the confirm/cancel button pattern frontend/src/components/documents/DocumentCard.vue — read for the card layout pattern and SVG icon style frontend/src/views/AdminView.vue — read to understand how tabs are currently implemented; extract the tab switch pattern Create each component file in the exact directory specified. All use Vue 3 Options API with Composition API (script setup) — follow the same pattern as existing components (read the existing files to confirm which style is used and replicate it exactly). FolderRow.vue (per UI-SPEC FolderRow section): - Props: folder {id, name, doc_count}, onNavigate (function), onRename (function), onDelete (function) - Template: flex container with folder icon (bg-gray-100), folder name + doc count, three-dot menu button - Three-dot menu: dropdown with "Rename" and "Delete folder" items; closes on outside click (use @click.outside or a boolean ref + document listener) - Rename mode: toggles to inline input pre-filled with current name; Enter=save, Escape=cancel, empty=rejected with inline error - No modal for rename — inline text input replaces the name display FolderBreadcrumb.vue (per UI-SPEC FolderBreadcrumb section): - Props: segments Array of {id, name} - Emits: navigate(folderId) — folderId is null for root - Computed: visibleSegments — if segments.length > 4, return [segments[0], {id: null, name: '...'}, ...segments.slice(-2)]; otherwise return segments as-is - Template: nav with aria-label="Folder navigation", ol list structure (accessibility), separator chevron SVG between segments - All segments except the last are clickable (emit navigate); last segment is non-clickable (current folder) - Wrap with nav aria-label="Folder navigation" and ol per UI-SPEC accessibility contract FolderDeleteModal.vue (per UI-SPEC FolderDeleteModal section): - Props: folder {id, name, doc_count}, onConfirm (function), onCancel (function) - Template: fixed overlay, warning icon (red exclamation), heading "Delete folder?", body text with doc_count interpolated, two buttons: "Keep folder" (neutral) and "Delete folder and documents" (red, min-h-[44px]) - Emits: confirm, cancel - Accessibility: role="dialog" aria-modal="true" aria-labelledby="modal-title-id" ShareModal.vue (per UI-SPEC ShareModal section): - Props: doc {id, filename} - Emits: close - Script state: handle ref(''), submitting ref(false), error ref(null), shares ref([]) - onMounted: load current shares via useDocumentsStore().listShares(props.doc.id); set shares.value - submitShare(): calls store.shareDocument(props.doc.id, handle.value); clears handle on success; updates shares list; shows error for 404/409 - revokeShare(shareId): calls store.revokeShare(shareId); removes from shares array optimistically; on error re-adds - Template: overlay, panel, title "Share document", handle input + Share document button, separator, recipient list OR empty state, close X button - Error messages per UI-SPEC copywriting (User not found, already shared) - Accessibility: role="dialog" aria-modal="true"; close button aria-label="Close" DocumentPreviewModal.vue (per UI-SPEC DocumentPreviewModal section): - Props: doc {id, filename} - Emits: close - Script: proxyUrl computed = `/api/documents/${props.doc.id}/content` - Template: fixed overlay (bg-black/60 z-50 flex flex-col), header bar with doc name + close button, flex-1 iframe filling the remaining height - Close on overlay click (check event.target === overlay element) and Escape key (keydown listener on mounted/unmounted) - Accessibility: role="dialog" aria-modal="true" aria-label="Document preview" SearchBar.vue (per UI-SPEC SearchBar section): - Props: modelValue (String), placeholder defaults to "Search documents..." - Emits: update:modelValue - Template: input type="search", role="search" on wrapper div, aria-label="Search documents" - Clears on Escape key (@keydown.escape) - Width: w-56 SortControls.vue (per UI-SPEC SortControls section): - Props: sort ('name'|'date'|'size'), order ('asc'|'desc') - Emits: change({sort, order}) - Template: three text buttons; active sort has `text-indigo-600 font-semibold` class; direction indicator appended (arrow up/down unicode or ↑/↓ text) - Clicking the already-active sort toggles order; clicking a different sort switches to it with desc order - aria-pressed="true" on active button; aria-label includes direction (e.g., "Sort by name, ascending") AuditLogTab.vue (per UI-SPEC AuditLogTab section): - Script state: entries ref([]), total ref(0), page ref(1), loading ref(false), filters reactive({start:'', end:'', user_id:'', event_type:''}) - onMounted: fetchLog() - fetchLog(): calls api.adminListAuditLog({...filters, page: page.value}) from api/client.js (add this function — GET /api/admin/audit-log with query params) - exportCsv(): sets window.location.href to `/api/admin/audit-log/export?format=csv&${new URLSearchParams(activeFilters)}` — triggers browser download - Template: filter bar (date inputs, user dropdown placeholder, event_type dropdown, Apply button, Export CSV button), paginated table (Timestamp | User | Action Type | IP Address), Previous/Next pagination, empty state - Action type pill badge colors per UI-SPEC: auth=bg-blue-50 text-blue-600, document=bg-gray-100 text-gray-600, folder/share=bg-purple-50 text-purple-600, admin=bg-amber-50 text-amber-700 - Table headers with scope="col"; timestamp cell uses font-mono text-xs Also add adminListAuditLog function to frontend/src/api/client.js (append): GET /api/admin/audit-log with query params start, end, user_id, event_type, page, per_page. Run: find /Users/nik/Documents/Progamming/document_scanner/frontend/src/components/folders /Users/nik/Documents/Progamming/document_scanner/frontend/src/components/sharing -name "*.vue" 2>/dev/null Expected: FolderRow.vue, FolderBreadcrumb.vue, FolderDeleteModal.vue appear; ShareModal.vue appears. Also: find /Users/nik/Documents/Progamming/document_scanner/frontend/src/components/documents /Users/nik/Documents/Progamming/document_scanner/frontend/src/components/admin -name "*.vue" | sort Expected: DocumentPreviewModal.vue, SearchBar.vue, SortControls.vue, AuditLogTab.vue appear. - All 8 component files exist in their respective directories (verify with find) - FolderBreadcrumb.vue has nav aria-label="Folder navigation" and ol list structure (grep: `aria-label.*Folder navigation` and `ol` in template) - ShareModal.vue has role="dialog" and aria-modal="true" (grep: `role="dialog"` and `aria-modal="true"`) - FolderDeleteModal.vue contains "Keep folder" and "Delete folder and documents" button text (grep) - DocumentPreviewModal.vue contains iframe with :src bound to proxyUrl (grep: `iframe` and `proxyUrl`) - SearchBar.vue has @keydown.escape handler (grep: `keydown.escape` or `keydown.esc`) - SortControls.vue has aria-pressed (grep: `aria-pressed`) - AuditLogTab.vue has window.location.href for CSV export (grep: `window.location.href`) - frontend/src/api/client.js contains adminListAuditLog function (grep) All new components created following UI-SPEC; accessibility contracts met; CSV export uses window.location.href. All frontend views and components for Phase 4: - AppSidebar.vue extended with "Shared with me" entry (purple icon, count badge) and Folders section with "New folder" CTA; folder links navigate to /folders/:id - DocumentCard.vue extended with share button (hover, opacity-0 group-hover:opacity-100) and shared indicator pill - HomeView.vue wired with SearchBar + SortControls above document list; folder-aware fetchDocuments - FolderView.vue: breadcrumb + sub-folder FolderRow list + document list + inline new-folder input - SharedView.vue: filtered document list from GET /api/shares/received; empty state - DocumentView.vue: PDF click triggers DocumentPreviewModal (in_app) or window.open (new_tab) based on pdf_open_mode preference - SettingsView.vue: Document Preferences card with radio buttons auto-saved via PATCH /api/auth/me/preferences - AdminView.vue: AuditLog tab added alongside existing tabs This checkpoint also triggers human verification of all visual and interactive Phase 4 features before the phase is marked complete. Before this checkpoint is reached, the executor MUST complete the following modifications: AppSidebar.vue: read the entire file; add "Shared with me" entry (above folders section, inline inbox icon, purple bg-purple-50 text-purple-500 icon container, count badge when sharedCount > 0); add Folders section below (section label "FOLDERS", "New folder" button, top-level folder list from useFoldersStore); import useFoldersStore; add sharedCount computed using getSharedWithMe() or a dedicated ref. DocumentCard.vue: read the entire file; add `group` class to outer container; add share button (opacity-0 group-hover:opacity-100 transition-opacity, aria-label="Share document", min-h-[44px] min-w-[44px]); add ShareModal component import and v-if=showShareModal; add shared indicator pill below metadata line if doc.share_count > 0. HomeView.vue: read the entire file; add SearchBar component above document list with v-model=docsStore.searchQuery; add SortControls with sort=docsStore.sortField, order=docsStore.sortOrder, @change handler; add useFoldersStore; update onMounted to also call foldersStore.fetchFolders(null). FolderView.vue: create new file; uses useFoldersStore and useDocumentsStore; on route param folderId change, calls foldersStore.navigateTo(folderId) and docsStore.fetchDocuments({folderId}); renders FolderBreadcrumb + FolderRow list + document list + inline new-folder input. SharedView.vue: create new file; on mounted calls api.getSharedWithMe(); renders document list using same DocumentCard layout but with owner handle shown; empty state per UI-SPEC copywriting. DocumentView.vue: read the entire file; find where PDF documents would be clicked or previewed; add logic — if current_user.pdf_open_mode === 'in_app', show DocumentPreviewModal; if 'new_tab', call window.open(getDocumentContentUrl(doc.id), '_blank'); load preference via api.getMyPreferences() on mount. SettingsView.vue: read the entire file; add Document Preferences section after existing sections; radio group with v-model=pdfOpenMode; watch pdfOpenMode to auto-call api.updateMyPreferences. AdminView.vue: read the entire file; add "Audit Log" tab button alongside existing tabs; add AuditLogTab component conditional display. AFTER completing all view modifications, start docker compose up and navigate to the application: 1. Navigate to the home page — verify Folders section and "Shared with me" appear in sidebar 2. Create a folder via sidebar "New folder" — verify it appears in the folder list 3. Navigate into the folder — verify FolderView loads with breadcrumb showing the folder name 4. Upload a document (use existing upload flow) — verify it appears in the current folder 5. Click the share button on a DocumentCard — verify ShareModal opens with handle input and empty state 6. In ShareModal, type a non-existent handle — verify "User not found" error appears 7. Delete a non-empty folder — verify FolderDeleteModal shows the document count and correct copy 8. Navigate to Settings — verify Document Preferences card with radio buttons appears 9. Navigate to Admin — verify Audit Log tab appears; click Apply filters — verify table loads 10. Click Export CSV in audit log — verify file downloads 11. Open a PDF document (set preference to in_app first) — verify DocumentPreviewModal with iframe appears Type "approved" after verifying all 11 checkpoints pass. Describe any issues for the executor to fix before approving. ## Trust Boundaries | Boundary | Description | |----------|-------------| | Browser → /api/documents/{id}/content | All document content access goes through proxy; iframe src is the proxy URL, not a presigned URL | | Share modal → shares API | Recipient handle is user-supplied text; backend validates existence; no autocomplete/search that could enumerate users | | Admin audit log export | window.location.href triggers a download authenticated by the existing httpOnly cookie; no token in URL | ## STRIDE Threat Register | Threat ID | Category | Component | Disposition | Mitigation Plan | |-----------|----------|-----------|-------------|-----------------| | T-04-09-01 | Information Disclosure | iframe src reveals presigned URL in browser | mitigate | DocumentPreviewModal iframe src = `/api/documents/${docId}/content` (the proxy URL, never presigned); Content-Disposition: inline drives rendering | | T-04-09-02 | Information Disclosure | Share modal autocomplete reveals user handles | mitigate | D-04: exact handle input only; no autocomplete API; 404 response reveals only that the handle does not exist | | T-04-09-03 | Broken Access Control | Audit log tab visible to regular users in AdminView | mitigate | AuditLogTab only visible inside AdminView which is already guarded by admin-only route; backend enforces get_current_admin | | T-04-09-04 | Information Disclosure | window.location.href for CSV export embeds sensitive params in URL | accept | Params are only filter values (dates, event types, user IDs) — not auth tokens; the request is authenticated via httpOnly cookie already set | | T-04-SC | Tampering | npm/pip/cargo installs | accept | No new packages installed in this plan | 1. Component existence: find frontend/src/components/folders frontend/src/components/sharing -name "*.vue" 2. Security: grep -rn "presigned" frontend/src/components/documents/DocumentPreviewModal.vue — expect zero matches 3. Accessibility: grep -n "role=\"dialog\"\|aria-modal" frontend/src/components/sharing/ShareModal.vue frontend/src/components/folders/FolderDeleteModal.vue 4. Share IDOR prevention: grep -n "aria-label" frontend/src/components/documents/DocumentCard.vue — share button must have aria-label 5. Human checkpoint: all 11 scenarios verified by the developer in a running instance - All 10 new component files exist - AppSidebar has "Shared with me" (purple icon) and Folders section - DocumentCard has share button (hover-reveal) and shared indicator pill - DocumentPreviewModal uses proxy URL (/api/documents/{id}/content), not presigned URL - AuditLogTab is accessible only inside AdminView (protected route) - Developer approves all 11 manual verification checkpoints Create `.planning/phases/04-folders-sharing-quotas-document-ux/04-09-SUMMARY.md` when done.