Skip to content

Commit ac15763

Browse files
committed
refactor(web): narrow analytics route response boundaries
1 parent b9d1bad commit ac15763

8 files changed

Lines changed: 332 additions & 140 deletions

ACTIVE_REFACTOR_PLAN.md

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,24 +41,24 @@ Historical detail is archived elsewhere and should not be copied here.
4141
9. Do not run separate test-only cleanup campaigns.
4242

4343
## Latest Completed
44-
- 2026-06-03: completed `packages/web-server/tests/web-server.functional.test.ts web server route adapter composition guardrail follow-up`.
45-
- 2026-06-03: completed `packages/web-server/src/websocket/ws-server.ts websocket bridge runtime adapter boundary follow-up`.
46-
- 2026-06-03: completed `packages/web-server/tests/ws-server.functional.test.ts websocket bridge runtime adapter functional guardrail follow-up`.
47-
- `ws-server.ts` now consumes an explicit `WebSocketBridgeApi` instead of the concrete `BotBridgeService`, so websocket reads and `bot-event` subscriptions cross one narrowed runtime adapter seam.
48-
- `index.ts` now composes route dependencies and websocket delegates through `createWebServerRuntimeDependencies(...)`, keeping runtime wiring in one place before `WebServer` starts sockets and file-watchers.
49-
- `web-server.functional.test.ts` now guards that the web-server runtime bundle carries both explicit route dependencies and the shared websocket delegate surface.
50-
- `ws-server.functional.test.ts` now proves the websocket bridge adapter preserves status reads, position reads, and `bot-event` unsubscribe behavior independent of the concrete bridge class.
44+
- 2026-06-03: completed `packages/web-server/src/routes/analytics.routes.ts analytics route runtime adapter boundary follow-up`.
45+
- 2026-06-03: completed `packages/web-server/src/routes/route-response.ts web server shared route response runtime boundary follow-up`.
46+
- 2026-06-03: completed `packages/web-server/src/routes/analytics.constants.ts analytics route runtime constants boundary follow-up`.
47+
- `analytics.routes.ts` now consumes an explicit route dependency bundle split into `journal`, `sessions`, `strategy`, and `curves`, so the HTTP layer no longer depends on one flat file-watcher-shaped read surface.
48+
- `analytics.constants.ts` now owns analytics route paging, recent-window, and fallback-message constants instead of leaving route magic numbers and fallback strings inline.
49+
- `route-response.ts` now builds an explicit route response context before writing success or error envelopes, keeping request-id normalization and shared route execution behavior on one runtime seam.
50+
- `route-response.test.ts` now guards the shared response helper directly, while `web-server.functional.test.ts` proves the analytics route bundle stays on the narrowed delegates.
5151

5252
## Latest Verification
53+
- 2026-06-03: `npm test -- --runInBand packages/web-server/tests/route-response.test.ts` (1 suite, 4 tests)
5354
- 2026-06-03: `npm test -- --runInBand packages/web-server/tests/web-server.functional.test.ts` (1 suite, 53 tests)
54-
- 2026-06-03: `npm test -- --runInBand packages/web-server/tests/ws-server.functional.test.ts` (1 suite, 21 tests)
5555
- 2026-06-03: `npm test -- --runInBand position-monitor` (6 suites, 59 tests)
5656
- 2026-06-03: `npm run build`
5757

5858
## Next Step
5959
- Continue with the next active component from `REFACTOR_COMPONENT_CHECKLIST.md`.
60-
- Start with `packages/web-server/src/routes/analytics.routes.ts analytics route runtime adapter boundary follow-up`.
61-
- Keep the next batch on the web-server runtime/error boundary stream before returning to the remaining docs guardrail items.
60+
- Start with `packages/web-server/src/swagger.config.ts web server OpenAPI contract surface boundary follow-up`.
61+
- Keep the next batch on the web-server runtime/error boundary stream before returning to the remaining core docs guardrail items.
6262

6363
## Archive
6464
- Frozen archive of the previous oversized active plan: `REFACTOR_PLAN_01.md`

NEXT_SESSION_PROMPT.md

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ Work directly on local `main`. Do not create worktrees. If the current branch is
1414

1515
## Current Batch
1616
Start with these three active queue items:
17-
1. `packages/web-server/src/routes/analytics.routes.ts analytics route runtime adapter boundary follow-up`
18-
2. `packages/web-server/src/routes/route-response.ts web server shared route response runtime boundary follow-up`
19-
3. `packages/web-server/src/routes/analytics.constants.ts analytics route runtime constants boundary follow-up`
17+
1. `packages/web-server/src/swagger.config.ts web server OpenAPI contract surface boundary follow-up`
18+
2. `packages/web-server/src/runtime-discovery-guidance.ts web server runtime discovery guidance boundary follow-up`
19+
3. `packages/web-server/src/middleware/request-logging.middleware.ts web server runtime logging boundary follow-up`
2020

2121
If one of these turns out to be too small, merge it with the next adjacent runtime,
2222
initializer, or websocket boundary item from `REFACTOR_COMPONENT_CHECKLIST.md` and keep
@@ -53,15 +53,15 @@ After all three slices are complete:
5353
5. Commit the batch after tests, smoke, build, and docs updates pass.
5454

5555
## Last Completed
56-
- 2026-06-03: completed `packages/web-server/tests/web-server.functional.test.ts web server route adapter composition guardrail follow-up`.
57-
- 2026-06-03: completed `packages/web-server/src/websocket/ws-server.ts websocket bridge runtime adapter boundary follow-up`.
58-
- 2026-06-03: completed `packages/web-server/tests/ws-server.functional.test.ts websocket bridge runtime adapter functional guardrail follow-up`.
59-
- `ws-server.ts` now depends on an explicit `WebSocketBridgeApi` seam for status reads, position reads, and `bot-event` forwarding instead of binding directly to the concrete bridge service.
60-
- `index.ts` now composes route dependencies and websocket delegates through `createWebServerRuntimeDependencies(...)`, keeping runtime ownership explicit before startup.
61-
- `web-server.functional.test.ts` and `ws-server.functional.test.ts` now guard the shared websocket delegate seam, including unsubscribe behavior and runtime bundle wiring.
56+
- 2026-06-03: completed `packages/web-server/src/routes/analytics.routes.ts analytics route runtime adapter boundary follow-up`.
57+
- 2026-06-03: completed `packages/web-server/src/routes/route-response.ts web server shared route response runtime boundary follow-up`.
58+
- 2026-06-03: completed `packages/web-server/src/routes/analytics.constants.ts analytics route runtime constants boundary follow-up`.
59+
- `analytics.routes.ts` now depends on a nested analytics route bundle (`journal`, `sessions`, `strategy`, `curves`) instead of a flat file-watcher-shaped read surface.
60+
- `analytics.constants.ts` now owns route defaults and fallback strings for analytics paging, recent-history reads, and curve endpoints.
61+
- `route-response.ts` now creates an explicit route response context before writing shared success/error envelopes, and `route-response.test.ts` directly guards request-id normalization plus fallback error behavior.
6262

6363
## Last Verification
64+
- `npm test -- --runInBand packages/web-server/tests/route-response.test.ts`
6465
- `npm test -- --runInBand packages/web-server/tests/web-server.functional.test.ts`
65-
- `npm test -- --runInBand packages/web-server/tests/ws-server.functional.test.ts`
6666
- `npm test -- --runInBand position-monitor`
6767
- `npm run build`

REFACTOR_COMPONENT_CHECKLIST.md

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,26 @@ Legend:
1515
- `func` = functional test exists
1616

1717
## Active Components
18-
- [ ] `packages/web-server/src/routes/analytics.routes.ts analytics route runtime adapter boundary follow-up`
19-
- [ ] `packages/web-server/src/routes/route-response.ts web server shared route response runtime boundary follow-up`
20-
- [ ] `packages/web-server/src/routes/analytics.constants.ts analytics route runtime constants boundary follow-up`
2118
- [ ] `packages/web-server/src/swagger.config.ts web server OpenAPI contract surface boundary follow-up`
2219
- [ ] `packages/web-server/src/runtime-discovery-guidance.ts web server runtime discovery guidance boundary follow-up`
23-
- [ ] `packages/core/src/__tests__/web/web-boundary.test.ts web server adapter contract guardrail follow-up`
24-
- [ ] `packages/core/src/__tests__/core/readme-entrypoint-boundary.functional.test.ts runtime handoff docs guardrail follow-up`
25-
- [ ] `packages/core/src/__tests__/core/architecture-entrypoint-boundary.functional.test.ts runtime handoff docs guardrail follow-up`
26-
- [ ] `packages/core/src/__tests__/core/legacy-entrypoint.functional.test.ts legacy wrapper runtime barrel guardrail follow-up`
2720
- [ ] `packages/web-server/src/services/config-management.service.ts web server config lifecycle boundary follow-up`
2821
- [ ] `packages/web-server/src/services/file-watcher.service.ts web server analytics watcher runtime boundary follow-up`
2922
- [ ] `packages/web-server/src/middleware/request-logging.middleware.ts web server runtime logging boundary follow-up`
23+
- [ ] `packages/web-server/tests/request-logging.middleware.test.ts web server runtime logging guardrail follow-up`
3024
- [ ] `packages/web-server/src/middleware/error-handler.middleware.ts web server structured error middleware boundary follow-up`
25+
- [ ] `packages/web-server/tests/error-handler.middleware.test.ts web server structured error middleware guardrail follow-up`
3126
- [ ] `packages/web-server/src/errors/api-error-response.ts web server structured error contract boundary follow-up`
27+
- [ ] `packages/web-server/tests/api-error-response.test.ts web server structured error contract guardrail follow-up`
3228
- [ ] `packages/web-server/src/logging/request-scoped-error-log.ts web server request-scoped logging contract boundary follow-up`
29+
- [ ] `packages/core/src/__tests__/web/web-boundary.test.ts web server adapter contract guardrail follow-up`
30+
- [ ] `packages/core/src/__tests__/core/readme-entrypoint-boundary.functional.test.ts runtime handoff docs guardrail follow-up`
31+
- [ ] `packages/core/src/__tests__/core/architecture-entrypoint-boundary.functional.test.ts runtime handoff docs guardrail follow-up`
32+
- [ ] `packages/core/src/__tests__/core/legacy-entrypoint.functional.test.ts legacy wrapper runtime barrel guardrail follow-up`
3333

3434
## Completed History
35+
- [x] `packages/web-server/src/routes/analytics.routes.ts analytics route runtime adapter boundary follow-up` | prod: yes | tests: yes | func: yes
36+
- [x] `packages/web-server/src/routes/route-response.ts web server shared route response runtime boundary follow-up` | prod: yes | tests: yes | func: yes
37+
- [x] `packages/web-server/src/routes/analytics.constants.ts analytics route runtime constants boundary follow-up` | prod: yes | tests: yes | func: yes
3538
- [x] `packages/web-server/tests/web-server.functional.test.ts web server route adapter composition guardrail follow-up` | prod: yes | tests: yes | func: yes
3639
- [x] `packages/web-server/src/websocket/ws-server.ts websocket bridge runtime adapter boundary follow-up` | prod: yes | tests: yes | func: yes
3740
- [x] `packages/web-server/tests/ws-server.functional.test.ts websocket bridge runtime adapter functional guardrail follow-up` | prod: yes | tests: yes | func: yes
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,16 @@
11
export const DEFAULT_EQUITY_CURVE_STARTING_BALANCE = 1000;
2+
export const DEFAULT_ANALYTICS_JOURNAL_PAGE = 1;
3+
export const DEFAULT_ANALYTICS_JOURNAL_LIMIT = 50;
4+
export const MAX_ANALYTICS_JOURNAL_LIMIT = 500;
5+
export const DEFAULT_ANALYTICS_RECENT_JOURNAL_HOURS = 24;
6+
7+
export const ANALYTICS_ROUTE_FALLBACK_MESSAGES = {
8+
journal: 'Failed to fetch journal',
9+
recentJournal: 'Failed to fetch recent journal',
10+
journalStats: 'Failed to fetch journal statistics',
11+
sessions: 'Failed to fetch sessions',
12+
compareSessions: 'Failed to compare sessions',
13+
strategyPerformance: 'Failed to fetch strategy performance',
14+
pnlHistory: 'Failed to fetch PnL history',
15+
equityCurve: 'Failed to fetch equity curve',
16+
} as const;

packages/web-server/src/routes/analytics.routes.ts

Lines changed: 61 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,34 @@ import {
2525
requireNonEmptyParam,
2626
sendAsyncRouteRead,
2727
} from './route-response.js';
28-
import { DEFAULT_EQUITY_CURVE_STARTING_BALANCE } from './analytics.constants.js';
28+
import {
29+
ANALYTICS_ROUTE_FALLBACK_MESSAGES,
30+
DEFAULT_ANALYTICS_JOURNAL_LIMIT,
31+
DEFAULT_ANALYTICS_JOURNAL_PAGE,
32+
DEFAULT_ANALYTICS_RECENT_JOURNAL_HOURS,
33+
DEFAULT_EQUITY_CURVE_STARTING_BALANCE,
34+
MAX_ANALYTICS_JOURNAL_LIMIT,
35+
} from './analytics.constants.js';
2936

3037
type AnalyticsRouteDerivedReadApi = {
3138
getPnlHistory(): Promise<PnlHistoryPoint[]>;
3239
getEquityCurve(): Promise<EquityCurvePoint[]>;
3340
};
3441

35-
export type AnalyticsRouteReadApi = FileWatcherAnalyticsReadApi & AnalyticsRouteDerivedReadApi;
42+
type AnalyticsRouteJournalReadApi = Pick<
43+
FileWatcherAnalyticsReadApi,
44+
'getJournalPaginated' | 'getJournalFromLastHours' | 'getJournalStats' | 'readJournal'
45+
>;
46+
type AnalyticsRouteSessionReadApi = Pick<FileWatcherAnalyticsReadApi, 'readSessions' | 'compareSessions'>;
47+
type AnalyticsRouteStrategyReadApi = Pick<FileWatcherAnalyticsReadApi, 'getStrategyPerformance'>;
48+
type AnalyticsRouteCurveReadApi = AnalyticsRouteDerivedReadApi;
49+
50+
export type AnalyticsRouteReadApi = {
51+
journal: AnalyticsRouteJournalReadApi;
52+
sessions: AnalyticsRouteSessionReadApi;
53+
strategy: AnalyticsRouteStrategyReadApi;
54+
curves: AnalyticsRouteCurveReadApi;
55+
};
3656
type SessionComparisonRouteQuery = { id1: string; id2: string };
3757

3858
function createPnlHistory(journal: WebApiJournalEntry[]): PnlHistoryPoint[] {
@@ -68,17 +88,27 @@ function createEquityCurve(journal: WebApiJournalEntry[]): EquityCurvePoint[] {
6888
}
6989

7090
export function createAnalyticsRouteReadApi(readApi: FileWatcherAnalyticsReadApi): AnalyticsRouteReadApi {
71-
return {
72-
getJournalPaginated: (page, limit) => readApi.getJournalPaginated(page, limit),
73-
getJournalFromLastHours: (hours) => readApi.getJournalFromLastHours(hours),
91+
const journal = {
92+
getJournalPaginated: (page: number | undefined, limit: number | undefined) => readApi.getJournalPaginated(page, limit),
93+
getJournalFromLastHours: (hours: number | undefined) => readApi.getJournalFromLastHours(hours),
7494
getJournalStats: () => readApi.getJournalStats(),
75-
readSessions: () => readApi.readSessions(),
76-
compareSessions: (id1, id2) => readApi.compareSessions(id1, id2),
77-
getStrategyPerformance: () => readApi.getStrategyPerformance(),
78-
getPnlHistory: async () => createPnlHistory(await readApi.readJournal()),
79-
getEquityCurve: async () => createEquityCurve(await readApi.readJournal()),
8095
readJournal: () => readApi.readJournal(),
8196
};
97+
98+
return {
99+
journal,
100+
sessions: {
101+
readSessions: () => readApi.readSessions(),
102+
compareSessions: (id1, id2) => readApi.compareSessions(id1, id2),
103+
},
104+
strategy: {
105+
getStrategyPerformance: () => readApi.getStrategyPerformance(),
106+
},
107+
curves: {
108+
getPnlHistory: async () => createPnlHistory(await journal.readJournal()),
109+
getEquityCurve: async () => createEquityCurve(await journal.readJournal()),
110+
},
111+
};
82112
}
83113

84114
function requireSessionComparisonQuery(
@@ -95,7 +125,7 @@ function requireSessionComparisonQuery(
95125
return { id1, id2 };
96126
}
97127

98-
export function createAnalyticsRoutes(fileWatcher: AnalyticsRouteReadApi): Router {
128+
export function createAnalyticsRoutes(analyticsApi: AnalyticsRouteReadApi): Router {
99129
const router = Router();
100130

101131
/**
@@ -104,36 +134,36 @@ export function createAnalyticsRoutes(fileWatcher: AnalyticsRouteReadApi): Route
104134
*/
105135
router.get('/journal', async (req: Request, res: Response<ApiResponse<JournalPagePayload>>) =>
106136
sendAsyncRouteRead(res, async () => {
107-
const page = parsePageQuery(req.query.page);
108-
const limit = parseLimitQuery(req.query.limit, 50, 500);
109-
return fileWatcher.getJournalPaginated(page, limit);
110-
}, { fallbackMessage: 'Failed to fetch journal' }));
137+
const page = parsePageQuery(req.query.page, DEFAULT_ANALYTICS_JOURNAL_PAGE);
138+
const limit = parseLimitQuery(req.query.limit, DEFAULT_ANALYTICS_JOURNAL_LIMIT, MAX_ANALYTICS_JOURNAL_LIMIT);
139+
return analyticsApi.journal.getJournalPaginated(page, limit);
140+
}, { fallbackMessage: ANALYTICS_ROUTE_FALLBACK_MESSAGES.journal }));
111141

112142
/**
113143
* GET /api/analytics/journal/last24h
114144
* Get trades from last 24 hours
115145
*/
116146
router.get('/journal/last24h', async (_req: Request, res: Response<ApiResponse<WebApiJournalEntry[]>>) =>
117-
sendAsyncRouteRead(res, () => fileWatcher.getJournalFromLastHours(24), {
118-
fallbackMessage: 'Failed to fetch recent journal',
147+
sendAsyncRouteRead(res, () => analyticsApi.journal.getJournalFromLastHours(DEFAULT_ANALYTICS_RECENT_JOURNAL_HOURS), {
148+
fallbackMessage: ANALYTICS_ROUTE_FALLBACK_MESSAGES.recentJournal,
119149
}));
120150

121151
/**
122152
* GET /api/analytics/journal/stats
123153
* Get overall journal statistics
124154
*/
125155
router.get('/journal/stats', async (_req: Request, res: Response<ApiResponse<JournalStatsPayload>>) =>
126-
sendAsyncRouteRead(res, () => fileWatcher.getJournalStats(), {
127-
fallbackMessage: 'Failed to fetch journal statistics',
156+
sendAsyncRouteRead(res, () => analyticsApi.journal.getJournalStats(), {
157+
fallbackMessage: ANALYTICS_ROUTE_FALLBACK_MESSAGES.journalStats,
128158
}));
129159

130160
/**
131161
* GET /api/analytics/sessions
132162
* Get all sessions
133163
*/
134164
router.get('/sessions', async (_req: Request, res: Response<ApiResponse<WebApiSessionStats[]>>) =>
135-
sendAsyncRouteRead(res, () => fileWatcher.readSessions(), {
136-
fallbackMessage: 'Failed to fetch sessions',
165+
sendAsyncRouteRead(res, () => analyticsApi.sessions.readSessions(), {
166+
fallbackMessage: ANALYTICS_ROUTE_FALLBACK_MESSAGES.sessions,
137167
}));
138168

139169
/**
@@ -146,8 +176,8 @@ export function createAnalyticsRoutes(fileWatcher: AnalyticsRouteReadApi): Route
146176
return;
147177
}
148178

149-
await sendAsyncRouteRead(res, () => fileWatcher.compareSessions(comparisonQuery.id1, comparisonQuery.id2), {
150-
fallbackMessage: 'Failed to compare sessions',
179+
await sendAsyncRouteRead(res, () => analyticsApi.sessions.compareSessions(comparisonQuery.id1, comparisonQuery.id2), {
180+
fallbackMessage: ANALYTICS_ROUTE_FALLBACK_MESSAGES.compareSessions,
151181
});
152182
});
153183

@@ -156,23 +186,27 @@ export function createAnalyticsRoutes(fileWatcher: AnalyticsRouteReadApi): Route
156186
* Get performance breakdown by strategy
157187
*/
158188
router.get('/strategy-performance', async (_req: Request, res: Response<ApiResponse<StrategyPerformancePayload[]>>) =>
159-
sendAsyncRouteRead(res, () => fileWatcher.getStrategyPerformance(), {
160-
fallbackMessage: 'Failed to fetch strategy performance',
189+
sendAsyncRouteRead(res, () => analyticsApi.strategy.getStrategyPerformance(), {
190+
fallbackMessage: ANALYTICS_ROUTE_FALLBACK_MESSAGES.strategyPerformance,
161191
}));
162192

163193
/**
164194
* GET /api/analytics/pnl-history
165195
* Get PnL over time for charting
166196
*/
167197
router.get('/pnl-history', async (_req: Request, res: Response<ApiResponse<PnlHistoryPoint[]>>) =>
168-
sendAsyncRouteRead(res, () => fileWatcher.getPnlHistory(), { fallbackMessage: 'Failed to fetch PnL history' }));
198+
sendAsyncRouteRead(res, () => analyticsApi.curves.getPnlHistory(), {
199+
fallbackMessage: ANALYTICS_ROUTE_FALLBACK_MESSAGES.pnlHistory,
200+
}));
169201

170202
/**
171203
* GET /api/analytics/equity-curve
172204
* Get equity curve data (cumulative balance over time)
173205
*/
174206
router.get('/equity-curve', async (_req: Request, res: Response<ApiResponse<EquityCurvePoint[]>>) =>
175-
sendAsyncRouteRead(res, () => fileWatcher.getEquityCurve(), { fallbackMessage: 'Failed to fetch equity curve' }));
207+
sendAsyncRouteRead(res, () => analyticsApi.curves.getEquityCurve(), {
208+
fallbackMessage: ANALYTICS_ROUTE_FALLBACK_MESSAGES.equityCurve,
209+
}));
176210

177211
return router;
178212
}

0 commit comments

Comments
 (0)