Moves phases 08–11 execution artifacts from .planning/phases/ to .planning/milestones/v0.2-phases/ to keep .planning/phases/ clean for the next milestone. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
25 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, tags, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | tags | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 08-stack-upgrade-backend-decomposition | 07 | execute | 2 |
|
|
true |
|
|
|
Purpose: CODE-04 (decomposition) + CODE-08 (single definition of the auth+retry pattern instead of 3 copies).
Output: 1 new utils.js, 7 new domain modules, client.js reduced to ~12 lines.
<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/08-stack-upgrade-backend-decomposition/08-CONTEXT.md @.planning/phases/08-stack-upgrade-backend-decomposition/08-RESEARCH.md @.planning/phases/08-stack-upgrade-backend-decomposition/08-PATTERNS.md @frontend/src/api/client.js @CLAUDE.md Function to domain map (locked per RESEARCH.md §"Frontend API Client Decomposition" and PATTERNS.md §"frontend/src/api/*"):utils.js: request(path, options) — moved from client.js lines 11-57 to break circular dep fetchWithRetry(url, options) — new consolidator for blob-download 401-retry
documents.js (consumers: stores/documents.js, DocumentCard.vue, DocumentView.vue, DocumentPreviewModal.vue, etc): listDocuments, getDocument, deleteDocument, deleteDocumentRemoveOnly, classifyDocument, getUploadUrl, confirmUpload, uploadToCloud, fetchDocumentContent (refactored to use fetchWithRetry), getDocumentContentUrl
auth.js (consumers: stores/auth.js, SettingsAccountTab.vue, LoginView.vue, etc): login, register, refreshToken, logout, logoutAll, getMe, changePassword, totpSetup, totpEnable, totpDisable, passwordResetRequest, passwordResetConfirm, getMyPreferences, updateMyPreferences, getMyQuota
admin.js (consumers: AdminUsersTab.vue, AdminQuotasTab.vue, AdminAiConfigTab.vue, AuditLogTab.vue, etc): adminListUsers, adminCreateUser, adminDeactivateUser, adminReactivateUser, adminResetUserPassword, adminGetUserQuota, adminUpdateQuota, adminUpdateAiConfig, adminDeleteUser, getAiConfig, saveAiConfig, testAiConnection, getAiModels, adminListAuditLog, adminExportAuditLogCsv (refactored to use fetchWithRetry), adminListDailyExports, adminDownloadDailyExport (refactored to use fetchWithRetry)
folders.js (consumers: stores/folders.js, FolderTreeItem.vue, AppSidebar.vue): listFolders, createFolder, getFolder, renameFolder, deleteFolder, moveDocument
shares.js (consumers: SharedView.vue, ShareModal.vue): createShare, updateSharePermission, listShares, deleteShare, getSharedWithMe
cloud.js (consumers: stores/cloudConnections.js, SettingsCloudTab.vue, CloudCredentialModal.vue, CloudProviderTreeItem.vue, CloudFolderTreeItem.vue): listCloudConnections, disconnectCloud, connectWebDav, updateDefaultStorage, getCloudFolders, initiateOAuth, getConnectionConfig
topics.js (consumers: stores/topics.js, SearchableModelSelect.vue is unrelated): listTopics, createTopic, updateTopic, deleteTopic, suggestTopics
client.js (after refactor — barrel only): export * from './documents.js' export * from './auth.js' export * from './admin.js' export * from './folders.js' export * from './shares.js' export * from './cloud.js' export * from './topics.js' export { fetchWithRetry, request } from './utils.js'
Critical: All function names across these 8 files are unique — no export * collisions (RESEARCH.md §Pitfall 5 confirms).
(A) Export async function request(path, options = {}) — copy the body verbatim from client.js lines 11-57. Keep the lazy await import('../stores/auth.js') to avoid the Pinia bootstrap cycle. Keep the noRefreshPaths array as a local const inside the function (or as a module-level const above the function — either works, but match the original pattern from client.js).
(B) Export async function fetchWithRetry(url, options = {}, _retry = false) per PATTERNS.md §"frontend/src/api/utils.js" template. The function: lazy-imports useAuthStore, copies headers, injects Authorization: Bearer ${authStore.accessToken} if present, calls fetch(url, { ...options, headers, credentials: 'include' }), on 401-and-not-already-retried calls authStore.refresh() and recurses with _retry=true, on refresh failure clears authStore.accessToken + authStore.user and throws 'Session expired', otherwise returns the raw Response (caller decides how to consume it — text(), blob(), etc.).
Do NOT modify client.js in this task — task 9 owns the barrel rewrite.
cd frontend && node -e "import('./src/api/utils.js').then(m => { if (typeof m.request !== 'function') throw new Error('request missing'); if (typeof m.fetchWithRetry !== 'function') throw new Error('fetchWithRetry missing'); console.log('ok'); }).catch(e => { console.error(e); process.exit(1); })"
<acceptance_criteria>
- File frontend/src/api/utils.js exists
- grep -c "export async function request" frontend/src/api/utils.js returns 1
- grep -c "export async function fetchWithRetry" frontend/src/api/utils.js returns 1
- grep -c "await import('../stores/auth.js')" frontend/src/api/utils.js returns at least 1 (lazy import preserved)
- grep -c "noRefreshPaths" frontend/src/api/utils.js returns at least 1
- grep -c "_retry" frontend/src/api/utils.js returns at least 2 (request uses options._retry, fetchWithRetry uses positional _retry)
</acceptance_criteria>
utils.js exists with both helpers, identifiable by export grep, module loads without error.
(A) documents.js: move these functions VERBATIM from client.js (preserve every parameter, every default value, every URL path, every body shape): listDocuments, getDocument, deleteDocument, deleteDocumentRemoveOnly, classifyDocument, getUploadUrl, confirmUpload, uploadToCloud, getDocumentContentUrl. Then refactor fetchDocumentContent to use fetchWithRetry: the new body becomes export async function fetchDocumentContent(docId, options = {}) { const res = await fetchWithRetry('/api/documents/' + docId + '/content', options); if (!res.ok) throw new Error('Failed to fetch document content: ' + res.status); return res; } — preserving the public contract (returns raw Response, throws on non-ok).
(B) auth.js: move these functions VERBATIM: login, register, refreshToken, logout, logoutAll, getMe, changePassword, totpSetup, totpEnable, totpDisable, passwordResetRequest, passwordResetConfirm, getMyPreferences, updateMyPreferences, getMyQuota.
(C) topics.js: move these functions VERBATIM: listTopics, createTopic, updateTopic, deleteTopic, suggestTopics.
Do NOT remove the functions from client.js yet — task 9 owns the barrel rewrite and deletion of the original bodies. This task only adds new files.
cd frontend && node -e "Promise.all([import('./src/api/documents.js'), import('./src/api/auth.js'), import('./src/api/topics.js')]).then(([d, a, t]) => { ['listDocuments','getDocument','deleteDocument','classifyDocument','getUploadUrl','fetchDocumentContent'].forEach(n => { if (typeof d[n] !== 'function') throw new Error('documents.' + n + ' missing'); }); ['login','register','refreshToken','logout','getMe','changePassword','totpSetup','passwordResetRequest','getMyQuota'].forEach(n => { if (typeof a[n] !== 'function') throw new Error('auth.' + n + ' missing'); }); ['listTopics','createTopic','suggestTopics'].forEach(n => { if (typeof t[n] !== 'function') throw new Error('topics.' + n + ' missing'); }); console.log('ok'); }).catch(e => { console.error(e); process.exit(1); })"
<acceptance_criteria>
- Files exist: frontend/src/api/documents.js, frontend/src/api/auth.js, frontend/src/api/topics.js
- grep -c "^export function listDocuments\\|^export async function fetchDocumentContent" frontend/src/api/documents.js returns at least 2
- grep -c "import { request, fetchWithRetry } from './utils.js'" frontend/src/api/documents.js returns 1
- grep -c "import { request } from './utils.js'" frontend/src/api/auth.js returns 1
- grep -c "import { request } from './utils.js'" frontend/src/api/topics.js returns 1
- grep -c "^export function login\\|^export function register" frontend/src/api/auth.js returns at least 2
- The node -e import check above prints ok (all named exports resolvable)
</acceptance_criteria>
Three domain files exist with all expected exports; verified by node import.
(A) admin.js: move these functions VERBATIM from client.js: adminListUsers, adminCreateUser, adminDeactivateUser, adminReactivateUser, adminResetUserPassword, adminGetUserQuota, adminUpdateQuota, adminUpdateAiConfig, adminDeleteUser, getAiConfig, saveAiConfig, testAiConnection, getAiModels, adminListAuditLog, adminListDailyExports. Then refactor adminExportAuditLogCsv and adminDownloadDailyExport to use fetchWithRetry (per PATTERNS.md §"frontend/src/api/admin.js"). The new adminExportAuditLogCsv(params = {}) body: build the URLSearchParams exactly as before, call const res = await fetchWithRetry('/api/admin/audit-log/export?' + searchParams), on !res.ok throw 'Export failed: ' + res.status, then call res.text(), create the Blob, trigger the download via temporary anchor click, revokeObjectURL on a setTimeout. The new adminDownloadDailyExport(date) body: const res = await fetchWithRetry('/api/admin/audit-log/daily-exports/' + date), on !res.ok throw, then download the blob the same way. Drop the _retry boilerplate — fetchWithRetry handles it.
(B) folders.js: move these functions VERBATIM: listFolders, createFolder, getFolder, renameFolder, deleteFolder, moveDocument.
(C) shares.js: move these functions VERBATIM: createShare, updateSharePermission, listShares, deleteShare, getSharedWithMe.
(D) cloud.js: move these functions VERBATIM: listCloudConnections, disconnectCloud, connectWebDav, updateDefaultStorage, getCloudFolders, initiateOAuth, getConnectionConfig.
Do NOT remove the functions from client.js yet — task 4 owns the barrel rewrite.
cd frontend && node -e "Promise.all([import('./src/api/admin.js'), import('./src/api/folders.js'), import('./src/api/shares.js'), import('./src/api/cloud.js')]).then(([ad, fo, sh, cl]) => { ['adminListUsers','adminCreateUser','adminUpdateQuota','getAiConfig','saveAiConfig','testAiConnection','adminListAuditLog','adminExportAuditLogCsv','adminDownloadDailyExport'].forEach(n => { if (typeof ad[n] !== 'function') throw new Error('admin.' + n + ' missing'); }); ['listFolders','createFolder','moveDocument'].forEach(n => { if (typeof fo[n] !== 'function') throw new Error('folders.' + n + ' missing'); }); ['createShare','updateSharePermission','getSharedWithMe'].forEach(n => { if (typeof sh[n] !== 'function') throw new Error('shares.' + n + ' missing'); }); ['listCloudConnections','initiateOAuth','getConnectionConfig'].forEach(n => { if (typeof cl[n] !== 'function') throw new Error('cloud.' + n + ' missing'); }); console.log('ok'); }).catch(e => { console.error(e); process.exit(1); })"
<acceptance_criteria>
- Files exist: frontend/src/api/admin.js, frontend/src/api/folders.js, frontend/src/api/shares.js, frontend/src/api/cloud.js
- grep -c "import { request, fetchWithRetry } from './utils.js'" frontend/src/api/admin.js returns 1
- grep -c "import { request } from './utils.js'" frontend/src/api/folders.js returns 1
- grep -c "import { request } from './utils.js'" frontend/src/api/shares.js returns 1
- grep -c "import { request } from './utils.js'" frontend/src/api/cloud.js returns 1
- grep -c "fetchWithRetry" frontend/src/api/admin.js returns at least 2 (one each for adminExportAuditLogCsv and adminDownloadDailyExport)
- grep -c "_retry" frontend/src/api/admin.js returns 0 (boilerplate dropped — fetchWithRetry owns retry)
- grep -c "^export function initiateOAuth" frontend/src/api/cloud.js returns 1 (named import is used by SettingsCloudTab.vue)
- The node -e import check above prints ok
</acceptance_criteria>
Four domain files exist with all expected exports; blob-download functions use fetchWithRetry; node import succeeds.
/**
- API client — barrel re-export.
- The HTTP transport (request) and 401-retry consolidator (fetchWithRetry) live in utils.js
- to avoid the circular import that would arise if domain modules imported request from here
- while this file re-exported from those same domain modules.
- All 35+ consumer files continue using one of:
- import * as api from '...api/client.js' — namespace pattern
- import { funcName } from '...api/client.js' — named import pattern
- without any changes. */ export * from './documents.js' export * from './auth.js' export * from './admin.js' export * from './folders.js' export * from './shares.js' export * from './cloud.js' export * from './topics.js' export { fetchWithRetry, request } from './utils.js'
After writing the new file contents, run cd frontend && npm test to confirm the test suite passes. The frontend tests use the import { ... } from '../api/client.js' and import * as api from '../api/client.js' patterns — both must resolve every previously-exported name through the barrel. If any test fails with TypeError: ... is not a function or similar, the symptom is a missing or misspelled export in one of the domain modules — diagnose by re-checking the function-to-domain map.
After tests pass, smoke the dev build: cd frontend && npm run build (Vite production build). Vite will fail loudly if export * from produces ambiguous names or unresolved modules. If the build fails, fix and rerun.
cd frontend && wc -l src/api/client.js && npm test 2>&1 | tail -20 && npm run build 2>&1 | tail -10
<acceptance_criteria>
- wc -l < frontend/src/api/client.js returns less than 25 (was 635; barrel is ~15-20 lines)
- grep -c "^export \\* from " frontend/src/api/client.js returns 7
- grep -c "export { fetchWithRetry, request } from './utils.js'" frontend/src/api/client.js returns 1
- grep -c "async function request" frontend/src/api/client.js returns 0 (request moved to utils.js)
- grep -c "noRefreshPaths" frontend/src/api/client.js returns 0 (lived inside request, now in utils.js)
- cd frontend && npm test exits 0
- cd frontend && npm run build exits 0
- grep -rn "from '../api/client.js'\\|from '../../api/client.js'\\|from '../../../api/client.js'" frontend/src/stores/ frontend/src/components/ frontend/src/views/ 2>/dev/null | wc -l returns same count as before this plan (35+ files; none modified)
- No file under frontend/src/stores/, frontend/src/components/, or frontend/src/views/ is listed in git diff for this plan
</acceptance_criteria>
client.js is a thin barrel; frontend tests pass; production build succeeds; zero consumer files modified.
<threat_model>
Trust Boundaries
| Boundary | Description |
|---|---|
| Browser → backend API | All requests carry Bearer token from authStore memory (never localStorage per CLAUDE.md) |
| Domain module → utils.js request | In-process import; no untrusted data crosses |
| fetchWithRetry → authStore.refresh on 401 | Refresh flow uses httpOnly cookie; on failure the in-memory access token is cleared |
STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|---|---|---|---|---|
| T-08-07-01 | Spoofing | Bearer token injection | mitigate | request body copied verbatim from client.js — preserves if (authStore.accessToken) headers['Authorization'] = ... and lazy import of authStore |
| T-08-07-02 | Tampering | Circular import producing undefined exports | mitigate | request lives in utils.js; client.js never re-exports request from itself; domain modules import from utils.js (PATTERNS.md §"Frontend domain module import line") |
| T-08-07-03 | Repudiation | 401-refresh-retry loop | mitigate | _retry flag is preserved in both request and fetchWithRetry; recursion is bounded |
| T-08-07-04 | Information Disclosure | Token stored in JS storage | mitigate | CLAUDE.md rule preserved: token from authStore.accessToken (memory only), never read from localStorage/sessionStorage |
| T-08-07-05 | Tampering | export * name collision |
mitigate | RESEARCH.md §Pitfall 5 verified all current client.js function names are unique; acceptance criterion runs frontend tests + production build which would fail on collision |
| T-08-07-06 | Denial of Service | Consumer files break | mitigate | Barrel re-export preserves every named export; acceptance criterion verifies zero consumer files in stores/components/views were modified |
| T-08-07-SC | Supply Chain | No new npm packages | accept | This plan installs zero new packages |
| </threat_model> |
<success_criteria>
- 8 new files in
frontend/src/api/(utils.js + 7 domain modules) - client.js reduced to ~15 lines
- Frontend tests + production build pass
- Zero consumer files modified (the entire point of the barrel pattern)
- fetchWithRetry consolidates 3 blob-download patterns (CODE-08) </success_criteria>