Repository navigation
refactor(frontend): singleton consolidation phase 2 — API clients, observer patterns, module-scoped composables, cache boundary (from #11634 audit) #11682
Description
Activity
Read-only audit (audit-first, awaiting approval before any code change)
API-client singletons vs canonical
apiClient(utils/ApiClient.tsProxy singleton)Client Real call-sites Verdict AdvancedControlApiClient.ts0 DEAD CODE — wire-in-or-remove decision (dead-code rule) VisionMultimodalApiClient.ts~8 SAFE-TO-CONSOLIDATE but HIGH-risk — lacks 401-auto-logout + token-expiry that canonical has; requestFormDatamulti-field upload must fold intoApiClient.uploadFile(currently single-file) to avoid regressingcombineModalitiesSecretsApiClient.js5 SAFE — mechanical: instantiates a second new ApiClient()instead of the sharedapiClient; 1-line swapFeatureFlagsApiClient.ts1 runtime ( FeatureFlagsSettingsPanel)SAFE-TO-CONSOLIDATE; shares a verbatim-duplicated base-URL-init block with Vision Orphaned singletons (0 consumers — dead-code decisions)
utils/ObserverPatterns.js(eventObserver/stateObserver+ 6 more exports) — 0 imports anywhere. Never wired since introduction. HMR listener-accumulation risk is moot (no subscribers).utils/CacheManager.ts/cacheManager— 0 call-sites. (Distinct fromservices/CacheService.ts, which has 1 real caller,SettingsPanel.)
Latent composable bug (real, worth fixing)
useVoiceConversation.tsregisterswatch(isSpeaking)(L842) +watch(prefLanguage)(L850) inside the composable body → one watcher per call. With 3 real callers (Panel + Overlay + ChatInterface) potentially mounted together,_resumeAutoListening()can double-fire on TTS-completion — the exact double-fire class the code's own comments say was fixed once (#6823) on a different path. Same pattern (unguarded per-callwatch) inuseActionQueue.ts:99(1 caller today, latent).useConfirmDialog/useVoiceConversationalso lack test-reset seams.Proposed staged plan (each a separate reviewable PR)
LOW-risk / mechanical (do first, on approval):
SecretsApiClient→ use sharedapiClient(1-line, transparent).- Document
CacheManagervsCacheServiceboundary (docblocks, no code). - File wire-in-or-remove discovery issues for the 3 orphans (AdvancedControlApiClient, ObserverPatterns, CacheManager).
- Add
_resetForTests()seams to the 4 module-scoped composables (additive). - Extract the duplicated base-URL-init block shared by Vision + FeatureFlags.
HIGH-risk / app-wide (needs manual QA, separate PRs):
6. Fold Vision + FeatureFlags clients intoapiClient(picks up 401-redirect + expiry; ~14 files; needs QA on screen-analysis/OCR/image-audio + feature-flag admin).
7. Fix thewatch()-per-call accumulation inuseVoiceConversation/useActionQueue(needs live QA of voice modes; intricate state machine).WebSocket consolidation (#6488) explicitly untouched. No code changed — awaiting your go-ahead on which steps to take (I'd suggest starting with steps 1-4, and the 3 orphan-disposition decisions).
- addedarea: frontend-unificationWave 4 · cluster B — Frontend unification & design systemWave 4 · cluster B — Frontend unification & design system
on Sep 1, 2026 - added a parent issue
on Oct 11, 2026
Phase-2 of the frontend singleton consolidation from the #11634 audit (phase 1 = #11640: health monitors + repository instances, merged). Evidence from the 2026-07-11 frontend singleton inventory; WebSocket manager consolidation stays in #6488 (excluded here).
Confirmed duplications / drift (file evidence)
apiClient(src/utils/ApiClient.tsProxy singleton):AdvancedControlApiClient.ts,VisionMultimodalApiClient.ts,SecretsApiClient.js,FeatureFlagsApiClient.ts— eachexport const x = new X(). No documented rule for when to use which; risk of divergent auth/base-url handling.src/utils/ObserverPatterns.jsexportseventObserver+stateObserver(module-level Map-based pub/sub and state watchers) — responsibilities that belong to Pinia stores / the event bus (useEventBuswrapping LiveEventService per refactor(frontend/ws): unify three WebSocket managers (useWebSocket + useGlobalWebSocket + useLiveEvents) into single useEventBus #6488 phase 1). Module-level Maps also accumulate listeners across HMR.useConfirmDialog,useActionQueue(localStorage-persisted),usePushNotifications,useVoiceConversationholdref()state at module scope — no test isolation, survives unmount; candidates for Pinia stores or documented intentional singletons with reset seams.CacheManager(browser-level: storage/service-worker caches) vsCacheService(in-app LRU) — likely intentional separation, but nothing states the boundary; name proximity invites misuse.Acceptance Criteria
apiClientmodules or keep them with an explicit "when to use which" note + shared base (no divergent auth/base-url logic); grep evidence of consumers migratedeventObserver/stateObserver: consumers inventoried; migrate to Pinia/useEventBus or delete-if-orphaned per dead-code rule (wire-in first)vue-tsc+ frontend suites clean