chore: archive v0.2 phase directories to milestones/v0.2-phases/

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>
This commit is contained in:
curo1305
2026-06-17 14:34:52 +02:00
co-authored by Claude Sonnet 4.6
parent e008bf7dae
commit 123ae5b29b
101 changed files with 759 additions and 4 deletions
@@ -0,0 +1,348 @@
---
phase: 08-stack-upgrade-backend-decomposition
plan: 07
type: execute
wave: 2
depends_on: [08-03]
files_modified:
- frontend/src/api/utils.js
- frontend/src/api/documents.js
- frontend/src/api/auth.js
- frontend/src/api/admin.js
- frontend/src/api/folders.js
- frontend/src/api/shares.js
- frontend/src/api/cloud.js
- frontend/src/api/topics.js
- frontend/src/api/client.js
autonomous: true
requirements: [CODE-04, CODE-08]
tags: [frontend-decomposition, api-client, barrel-reexport]
must_haves:
truths:
- "frontend/src/api/utils.js exists, exports request(), exports fetchWithRetry() — the consolidated 401-retry helper"
- "Each of documents.js / auth.js / admin.js / folders.js / shares.js / cloud.js / topics.js exists and imports `request` from './utils.js'"
- "fetchWithRetry() consolidates the 3 blob-download patterns (adminExportAuditLogCsv, adminDownloadDailyExport, fetchDocumentContent) into one helper"
- "client.js is reduced to a barrel re-export: `export * from './documents.js'` etc., plus `export { fetchWithRetry, request } from './utils.js'`"
- "Every function name from the old client.js is exported by exactly one domain module (no duplication, no missing names)"
- "Zero consumer files (35+) under frontend/src/stores/ frontend/src/components/ frontend/src/views/ are modified — all `import * as api from '../api/client.js'` and `import { name } from '../api/client.js'` continue to resolve"
- "Frontend test suite passes"
artifacts:
- path: "frontend/src/api/utils.js"
provides: "HTTP transport: request() with bearer injection + 401-refresh-retry, fetchWithRetry() for raw Response endpoints"
contains: "export async function request"
- path: "frontend/src/api/documents.js"
provides: "Document domain functions including fetchDocumentContent (now using fetchWithRetry)"
contains: "export function listDocuments"
- path: "frontend/src/api/auth.js"
provides: "Auth domain functions: login, register, refreshToken, logout, totp*, password*, preferences, quota"
contains: "export function login"
- path: "frontend/src/api/admin.js"
provides: "Admin domain functions including blob-download admin endpoints"
contains: "export function adminListUsers"
- path: "frontend/src/api/folders.js"
provides: "Folder domain functions"
contains: "export function listFolders"
- path: "frontend/src/api/shares.js"
provides: "Share domain functions"
contains: "export function createShare"
- path: "frontend/src/api/cloud.js"
provides: "Cloud connection domain functions including initiateOAuth"
contains: "export function listCloudConnections"
- path: "frontend/src/api/topics.js"
provides: "Topic domain functions"
contains: "export function listTopics"
- path: "frontend/src/api/client.js"
provides: "Barrel re-export — preserves zero-change consumer contract"
contains: "export * from './documents.js'"
key_links:
- from: "frontend/src/api/client.js"
to: "all 7 domain modules + utils.js"
via: "export * from"
pattern: "export \\* from './(documents|auth|admin|folders|shares|cloud|topics)\\.js'"
- from: "each domain module"
to: "frontend/src/api/utils.js"
via: "import { request } from './utils.js'"
pattern: "import \\{ request"
- from: "blob-download functions (admin.js, documents.js)"
to: "frontend/src/api/utils.js fetchWithRetry"
via: "import { fetchWithRetry } from './utils.js'"
pattern: "import \\{.*fetchWithRetry"
---
<objective>
Decompose the 635-line `frontend/src/api/client.js` monolith into 7 domain modules + `utils.js` (HTTP transport + 401-retry consolidation) per locked D-12/D-13/D-14. `client.js` is reduced to a barrel re-export so the 35+ consumer files do not require any edits. The 3 blob-download 401-retry copy-paste patterns are consolidated into a single `fetchWithRetry()` helper in `utils.js`.
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.
</objective>
<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>
<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
<interfaces>
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).
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Create utils.js with request() and fetchWithRetry()</name>
<files>frontend/src/api/utils.js</files>
<read_first>
- frontend/src/api/client.js lines 11-57 (current `request` function — copy verbatim)
- frontend/src/api/client.js lines 428-471 (adminExportAuditLogCsv blob pattern)
- frontend/src/api/client.js lines 492-529 (adminDownloadDailyExport blob pattern)
- frontend/src/api/client.js lines 552-581 (fetchDocumentContent blob pattern)
- .planning/phases/08-stack-upgrade-backend-decomposition/08-PATTERNS.md §"frontend/src/api/utils.js" (full code template for both functions)
</read_first>
<action>
Create `frontend/src/api/utils.js`. Add a top-of-file JSDoc comment explaining: "HTTP transport + 401-retry consolidator. `request()` moved from client.js to break circular dependency (domain modules import request from here; client.js re-exports from domain modules). `fetchWithRetry()` consolidates 3 blob-download patterns. Security: Bearer from authStore memory only (CLAUDE.md)."
(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.
</action>
<verify>
<automated>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); })"</automated>
</verify>
<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>
<done>utils.js exists with both helpers, identifiable by export grep, module loads without error.</done>
</task>
<task type="auto">
<name>Task 2: Create domain modules documents.js, auth.js, topics.js</name>
<files>frontend/src/api/documents.js, frontend/src/api/auth.js, frontend/src/api/topics.js</files>
<read_first>
- frontend/src/api/client.js (full file — confirm exact function bodies and signatures for each function listed in the function-to-domain map in &lt;interfaces&gt;)
- .planning/phases/08-stack-upgrade-backend-decomposition/08-PATTERNS.md §"frontend/src/api/documents.js" and §"frontend/src/api/auth.js"
</read_first>
<action>
For each file, write a small header docstring naming the domain (e.g., "Document API — listing, fetching, uploading, content streaming"). Add `import { request, fetchWithRetry } from './utils.js'` for documents.js (because fetchDocumentContent uses fetchWithRetry); for auth.js and topics.js use only `import { request } from './utils.js'`.
(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.
</action>
<verify>
<automated>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); })"</automated>
</verify>
<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>
<done>Three domain files exist with all expected exports; verified by node import.</done>
</task>
<task type="auto">
<name>Task 3: Create domain modules admin.js, folders.js, shares.js, cloud.js</name>
<files>frontend/src/api/admin.js, frontend/src/api/folders.js, frontend/src/api/shares.js, frontend/src/api/cloud.js</files>
<read_first>
- frontend/src/api/client.js (full file — confirm exact function bodies and signatures)
- .planning/phases/08-stack-upgrade-backend-decomposition/08-PATTERNS.md §"frontend/src/api/admin.js" (blob refactor pattern using fetchWithRetry)
</read_first>
<action>
For each file, add a header docstring naming the domain. Import statements:
- admin.js: `import { request, fetchWithRetry } from './utils.js'` (audit-log CSV + daily-export use fetchWithRetry)
- folders.js, shares.js, cloud.js: `import { request } from './utils.js'`
(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.
</action>
<verify>
<automated>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); })"</automated>
</verify>
<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>
<done>Four domain files exist with all expected exports; blob-download functions use fetchWithRetry; node import succeeds.</done>
</task>
<task type="auto">
<name>Task 4: Rewrite client.js as barrel re-export and run frontend tests</name>
<files>frontend/src/api/client.js</files>
<read_first>
- frontend/src/api/client.js (the current 635-line file — you are about to replace its contents)
- .planning/phases/08-stack-upgrade-backend-decomposition/08-PATTERNS.md §"frontend/src/api/client.js (transport + barrel, request-response) — MODIFIED" (the full final-state template)
</read_first>
<action>
Replace the ENTIRE contents of `frontend/src/api/client.js` with this final form (preserves all previous exports via `export *`):
/**
* 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.
</action>
<verify>
<automated>cd frontend && wc -l src/api/client.js && npm test 2>&1 | tail -20 && npm run build 2>&1 | tail -10</automated>
</verify>
<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>
<done>client.js is a thin barrel; frontend tests pass; production build succeeds; zero consumer files modified.</done>
</task>
</tasks>
<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>
<verification>
- `cd frontend && npm test` — zero failures
- `cd frontend && npm run build` — succeeds
- `wc -l frontend/src/api/client.js` — under 25 lines
- `grep -rn "import.*from '../api/client.js'\\|import.*from '../../api/client.js'\\|import.*from '../../../api/client.js'" frontend/src/stores/ frontend/src/components/ frontend/src/views/` returns the same line count as before this plan
- `git diff --stat HEAD frontend/src/stores/ frontend/src/components/ frontend/src/views/` returns no entries
</verification>
<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>
<output>
Create `.planning/phases/08-stack-upgrade-backend-decomposition/08-07-SUMMARY.md` when done. Include: (a) line counts before/after for client.js and the 8 new files, (b) list of all consumer files (stores + components + views) confirmed untouched, (c) confirmation that the 3 old blob-download _retry boilerplate blocks were consolidated into the single `fetchWithRetry` helper.
</output>