PRD 0001: Restructure api.js into per-domain service modules#
Source of truth for this epic. Tracker: epic issue. The epic links here; this file is not embedded in the issue body.
Problem#
src/services/api.js is meant to be the central transport layer: it creates the
Axios instance, resolves the base URL (runtime config → Vite env), attaches the
bearer token, and auto-logs-out on 401. That part is genuinely cross-cutting —
every feature depends on it and it carries no domain knowledge.
Over time the file has also accumulated domain-specific endpoint catalogs exported as named objects:
diagnose— 7 admin-diagnose methods (includes the 404 →not_foundnormalization that is the single home for that response shape).tokens— 12 token-management methods.plan— ~38 write/mutation methods spanning eight distinct domains (sources, instruments/modules/lines, obs-units, programs, pre-scheduled slots, obs-modes, setup/switch-plan, obs-config/CHAI).scheduler— 1 method (check_observable).
plan in particular is the planner feature’s entire write surface parked in the
central module, so every new planner endpoint edits the same file that owns auth
and 401 handling — coupling that has nothing to do with transport. A second smell
compounds it: the abstraction is only half-applied. The planner components still
issue ~30 raw inline api.get('/obs/…'), api.get('/instruments/…'),
api.get('/sources/…') reads that never made it into the plan object, so
plan wraps only the mutations while the matching reads stay scattered across the
components. A reader of api.js sees the writes and has no idea the reads exist.
This diverges from the repo’s own convention of grouping by domain everywhere
else — src/store/modules/<domain>, src/components/<domain>/.
Solution#
Return api.js to a transport-only module and give each domain its own
service file, mirroring the store/component layout the repo already uses:
src/services/
api.js → Axios instance + interceptors only; `export default api`
diagnose.js → export const diagnose = { … } (404→not_found kept intact)
tokens.js → export const tokens = { … }
plan.js → export const plan = { …writes AND reads for the planner domain… }
scheduler.js → export const scheduler = { checkObservable }
Each domain file does import api from './api' and exports the same named object
under the same name, so importers change only their import path, not their
call sites (e.g. import { plan as planApi } from '@/services/api' becomes
from '@/services/plan'). Behavior is unchanged — no endpoint URL, param, or
logic changes — with one deliberate deepening: the planner domain’s inline reads
are folded into plan.js so the module owns reads and writes together and the
components stop calling raw api.get(...).
This is a mechanical, behavior-preserving refactor. Its value is structural:
api.js stops being a change-magnet coupled to every feature, and each feature’s
endpoint surface lives with that feature.
Precondition#
This epic is a follow-up to PR #71 (branch
22-include-scheduler_loop-options-in-operational_config), which introduces
plan/scheduler and itself edits api.js. All slices assume post-#71
develop; starting earlier would only collide with that PR.
Implementation decisions#
api.jskeeps only transport: theaxios.create(...)instance,baseURLresolution,paramsSerializer, the bearer-token request interceptor, the 401 auto-logout response interceptor, andexport default api. Nothing else.One file per domain, each exporting the existing named object verbatim so call sites are untouched. Names stay
diagnose,tokens,plan,scheduler.diagnose.jspreserves thegetOperationReport404 →not_foundnormalization as-is — it is the single home for thenot_foundreport shape, shared byFailureAlertPanel.vueandDiagnoseView.vue; it must not drift.Importer updates are path-only. Known importers to migrate:
diagnose:store/modules/transfer.js(import api, { diagnose }),components/transfer/FailureAlertPanel.vue,views/DiagnoseView.vue.tokens:store/modules/tokens.js.plan: the 9 planner components undersrc/components/planner/(ChaiInparPanel, ChaiLinesPanel, InstrumentsPanel, ObsConfigDialog, ObsSetupPanel, ObsUnitsPanel, ProgramsPanel, SlotsPanel, SourcesPanel).scheduler:components/observer/CheckObservablePanel.vue.The many
import api from '@/services/api'default-import consumers are unaffected — the default export stays put.
Sequencing: every extraction removes its block from the same
api.js, so the slices form a linear chain (diagnose → tokens → plan+scheduler → fold reads) rather than parallel PRs that would conflict onapi.js. The plan+scheduler slice is the one that leavesapi.jstransport-only.Reads-folding (final slice): move the ~30 inline planner
api.get(...)reads intoplan.jsas named read methods (e.g.getSources,getObsUnits,getLines,getInstrumentModules, …); planner components call those instead of rawapi.get. Endpoint strings and params are copied verbatim.
Testing decisions#
The repo has no test framework configured, so verification is behavioral via the build/lint gates plus a targeted smoke of the affected screens:
npm run lintandnpm run buildmust pass for every slice (a moved/renamed import that misses a call site fails the build — the primary safety net for a path-only refactor).Manual smoke of each affected surface after its slice: the Diagnose drawer and
/admin/diagnoseview (diagnose), token management (tokens), the planner panels and observer check-observable panel (plan/scheduler), confirming the same requests fire with the same params (network tab) and the 404 → not_found path still renders.Prior art for “behavior lives at the boundary, not the file layout”: the existing
diagnose/tokensexports and their consumers (FailureAlertPanel.vue,DiagnoseView.vue,store/modules/tokens.js) — their call sites should be byte-for-byte identical after migration.
Out of scope#
No change to endpoint URLs, request params, response handling, or any runtime behavior.
No migration of the default-import
apiconsumers (they already depend only on transport and stay as-is).No new nesting/reshaping of the
planobject’s method names beyond adding the folded read methods (noplan.sources.createrestructuring).No backend (
ops-db-api) changes.Doing the refactor before PR #71 merges.