Skip to content

Commit 75d1817

Browse files
committed
refactor(web): narrow runtime docs and logging boundaries
1 parent ac15763 commit 75d1817

9 files changed

Lines changed: 262 additions & 180 deletions

ACTIVE_REFACTOR_PLAN.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,23 +41,23 @@ 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/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.
44+
- 2026-06-03: completed `packages/web-server/src/swagger.config.ts web server OpenAPI contract surface boundary follow-up`.
45+
- 2026-06-03: completed `packages/web-server/src/runtime-discovery-guidance.ts web server runtime discovery guidance boundary follow-up`.
46+
- 2026-06-03: completed `packages/web-server/src/middleware/request-logging.middleware.ts web server runtime logging boundary follow-up`.
47+
- `runtime-discovery-guidance.ts` now owns the runtime discovery paragraphs, OpenAPI description assembly, server description copy, and shared docs HTML section so the docs and OpenAPI layers consume one runtime-copy boundary.
48+
- `request-logging.middleware.ts` now resolves logging config once and delegates finish/error lifecycle logging through focused helpers while still emitting the same shared HTTP log payloads.
49+
- `swagger.config.ts` now relies on `swagger-contract-helpers.ts` for shared schema, request-body, and envelope builders so the file stays focused on the exported OpenAPI contract surface.
50+
- `request-logging.middleware.test.ts` now guards config normalization directly, and `web-server.functional.test.ts` continues to prove the runtime discovery and OpenAPI surfaces stay behaviorally stable.
5151

5252
## Latest Verification
53-
- 2026-06-03: `npm test -- --runInBand packages/web-server/tests/route-response.test.ts` (1 suite, 4 tests)
53+
- 2026-06-03: `npm test -- --runInBand packages/web-server/tests/request-logging.middleware.test.ts` (1 suite, 8 tests)
5454
- 2026-06-03: `npm test -- --runInBand packages/web-server/tests/web-server.functional.test.ts` (1 suite, 53 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/swagger.config.ts web server OpenAPI contract surface boundary follow-up`.
60+
- Start with `packages/web-server/src/services/config-management.service.ts web server config lifecycle boundary follow-up`.
6161
- Keep the next batch on the web-server runtime/error boundary stream before returning to the remaining core docs guardrail items.
6262

6363
## Archive

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/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`
17+
1. `packages/web-server/src/services/config-management.service.ts web server config lifecycle boundary follow-up`
18+
2. `packages/web-server/src/services/file-watcher.service.ts web server analytics watcher runtime boundary follow-up`
19+
3. `packages/web-server/src/middleware/error-handler.middleware.ts web server structured error middleware 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/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.
56+
- 2026-06-03: completed `packages/web-server/src/swagger.config.ts web server OpenAPI contract surface boundary follow-up`.
57+
- 2026-06-03: completed `packages/web-server/src/runtime-discovery-guidance.ts web server runtime discovery guidance boundary follow-up`.
58+
- 2026-06-03: completed `packages/web-server/src/middleware/request-logging.middleware.ts web server runtime logging boundary follow-up`.
59+
- `runtime-discovery-guidance.ts` now owns the runtime discovery paragraph set, docs HTML section, OpenAPI description assembly, and default runtime server description instead of leaving those strings split across `index.ts` and `swagger.config.ts`.
60+
- `request-logging.middleware.ts` now resolves logging config through one shared boundary and delegates finish/error event logging through focused helpers over the existing request-scoped payload builders.
61+
- `swagger.config.ts` now consumes `swagger-contract-helpers.ts` for shared OpenAPI envelope, schema-alias, and request-body builders so the file stays focused on the exported contract map.
6262

6363
## Last Verification
64-
- `npm test -- --runInBand packages/web-server/tests/route-response.test.ts`
64+
- `npm test -- --runInBand packages/web-server/tests/request-logging.middleware.test.ts`
6565
- `npm test -- --runInBand packages/web-server/tests/web-server.functional.test.ts`
6666
- `npm test -- --runInBand position-monitor`
6767
- `npm run build`

REFACTOR_COMPONENT_CHECKLIST.md

Lines changed: 6 additions & 3 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/swagger.config.ts web server OpenAPI contract surface boundary follow-up`
19-
- [ ] `packages/web-server/src/runtime-discovery-guidance.ts web server runtime discovery guidance boundary follow-up`
2018
- [ ] `packages/web-server/src/services/config-management.service.ts web server config lifecycle boundary follow-up`
2119
- [ ] `packages/web-server/src/services/file-watcher.service.ts web server analytics watcher runtime boundary follow-up`
22-
- [ ] `packages/web-server/src/middleware/request-logging.middleware.ts web server runtime logging boundary follow-up`
2320
- [ ] `packages/web-server/tests/request-logging.middleware.test.ts web server runtime logging guardrail follow-up`
2421
- [ ] `packages/web-server/src/middleware/error-handler.middleware.ts web server structured error middleware boundary follow-up`
2522
- [ ] `packages/web-server/tests/error-handler.middleware.test.ts web server structured error middleware guardrail follow-up`
2623
- [ ] `packages/web-server/src/errors/api-error-response.ts web server structured error contract boundary follow-up`
2724
- [ ] `packages/web-server/tests/api-error-response.test.ts web server structured error contract guardrail follow-up`
2825
- [ ] `packages/web-server/src/logging/request-scoped-error-log.ts web server request-scoped logging contract boundary follow-up`
26+
- [ ] `packages/web-server/src/swagger-contract-helpers.ts web server OpenAPI shared builder boundary follow-up`
27+
- [ ] `packages/web-server/tests/web-server.functional.test.ts web server runtime docs and logging guardrail follow-up`
28+
- [ ] `packages/web-server/src/index.ts web server docs helper delegate boundary follow-up`
2929
- [ ] `packages/core/src/__tests__/web/web-boundary.test.ts web server adapter contract guardrail follow-up`
3030
- [ ] `packages/core/src/__tests__/core/readme-entrypoint-boundary.functional.test.ts runtime handoff docs guardrail follow-up`
3131
- [ ] `packages/core/src/__tests__/core/architecture-entrypoint-boundary.functional.test.ts runtime handoff docs guardrail follow-up`
3232
- [ ] `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/swagger.config.ts web server OpenAPI contract surface boundary follow-up` | prod: yes | tests: yes | func: yes
36+
- [x] `packages/web-server/src/runtime-discovery-guidance.ts web server runtime discovery guidance boundary follow-up` | prod: yes | tests: yes | func: yes
37+
- [x] `packages/web-server/src/middleware/request-logging.middleware.ts web server runtime logging boundary follow-up` | prod: yes | tests: yes | func: yes
3538
- [x] `packages/web-server/src/routes/analytics.routes.ts analytics route runtime adapter boundary follow-up` | prod: yes | tests: yes | func: yes
3639
- [x] `packages/web-server/src/routes/route-response.ts web server shared route response runtime boundary follow-up` | prod: yes | tests: yes | func: yes
3740
- [x] `packages/web-server/src/routes/analytics.constants.ts analytics route runtime constants boundary follow-up` | prod: yes | tests: yes | func: yes

packages/web-server/src/index.ts

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,9 @@ import {
4949
import { createRuntimeServiceLogPayload } from './logging/request-scoped-error-log.js';
5050
import {
5151
API_DOCS_PATH,
52+
createDocsRuntimeDiscoverySectionHtml,
5253
OPENAPI_DOCUMENT_PATH,
5354
RUNTIME_CONFIG_PATH,
54-
RUNTIME_DISCOVERY_GUIDANCE_LINES,
5555
} from './runtime-discovery-guidance.js';
5656
import type { ServerRuntimePorts } from './routes/config-route-contracts.js';
5757

@@ -67,6 +67,7 @@ export interface WebServerConfig {
6767

6868
export type { IBotInstance } from './services/bot-bridge.service.js';
6969
export type { IWebApiAdapter } from './services/web-api-adapter.types.js';
70+
export { createDocsRuntimeDiscoverySectionHtml } from './runtime-discovery-guidance.js';
7071

7172
type WebServerRuntimeConfig = Required<WebServerConfig>;
7273
type ShutdownProcess = Pick<NodeJS.Process, 'on' | 'off' | 'exit'>;
@@ -145,16 +146,6 @@ export function createDocsQuickReferenceHtml(): string {
145146
return DOCS_QUICK_REFERENCE_ENDPOINTS.map((endpoint) => createDocsEndpointCardHtml(endpoint)).join('');
146147
}
147148

148-
export function createDocsRuntimeDiscoverySectionHtml(): string {
149-
return `
150-
<div class="endpoint" style="margin-top: 20px;">
151-
<h3>Browser Runtime Discovery</h3>
152-
<p>${RUNTIME_DISCOVERY_GUIDANCE_LINES.sameOrigin}</p>
153-
<p>${RUNTIME_DISCOVERY_GUIDANCE_LINES.websocketFallback}</p>
154-
<p>${RUNTIME_DISCOVERY_GUIDANCE_LINES.legacyRetry}</p>
155-
</div>`;
156-
}
157-
158149
export function createDocsOpenApiLinkHtml(): string {
159150
return `
160151
<div class="swagger-ui-link">

packages/web-server/src/middleware/request-logging.middleware.ts

Lines changed: 66 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -24,29 +24,37 @@ export interface LoggingConfig {
2424
maxBodyLength?: number;
2525
}
2626

27-
const defaultConfig: LoggingConfig = {
27+
export const DEFAULT_LOGGING_CONFIG: Required<LoggingConfig> = {
2828
logBody: false,
2929
logHeaders: false,
3030
excludePaths: ['/health'],
3131
maxBodyLength: 500,
3232
};
3333

34+
export function resolveLoggingConfig(config: LoggingConfig = {}): Required<LoggingConfig> {
35+
return {
36+
...DEFAULT_LOGGING_CONFIG,
37+
...config,
38+
excludePaths: config.excludePaths ?? DEFAULT_LOGGING_CONFIG.excludePaths,
39+
};
40+
}
41+
3442
function isRecord(value: unknown): value is Record<string, unknown> {
3543
return typeof value === 'object' && value !== null;
3644
}
3745

38-
function getRequestBodyLogData(req: Request, config: LoggingConfig): unknown {
46+
function getRequestBodyLogData(req: Request, config: Required<LoggingConfig>): unknown {
3947
if (!config.logBody || !isRecord(req.body) || Object.keys(req.body).length === 0) {
4048
return undefined;
4149
}
4250

4351
const bodyStr = JSON.stringify(req.body);
44-
return bodyStr.length > (config.maxBodyLength || 500)
52+
return bodyStr.length > config.maxBodyLength
4553
? bodyStr.substring(0, config.maxBodyLength) + '...'
4654
: req.body;
4755
}
4856

49-
function getHeaderLogData(req: Request, config: LoggingConfig): Record<string, unknown> | undefined {
57+
function getHeaderLogData(req: Request, config: Required<LoggingConfig>): Record<string, unknown> | undefined {
5058
if (!config.logHeaders) {
5159
return undefined;
5260
}
@@ -64,73 +72,92 @@ export function createRequestLogEntry(
6472
config: LoggingConfig,
6573
responseBody: unknown,
6674
): Record<string, unknown> {
75+
const resolvedConfig = resolveLoggingConfig(config);
6776
return createHttpLogPayload({
6877
method: req.method,
6978
path: req.path,
7079
query: Object.keys(req.query).length > 0 ? req.query : undefined,
7180
statusCode: res.statusCode,
7281
durationMs,
7382
responseSize: res.get('content-length') || 'unknown',
74-
requestBody: getRequestBodyLogData(req, config),
75-
headers: getHeaderLogData(req, config),
83+
requestBody: getRequestBodyLogData(req, resolvedConfig),
84+
headers: getHeaderLogData(req, resolvedConfig),
7685
requestId: resolveRequestId(req.headers['x-request-id']),
7786
responseBody,
7887
});
7988
}
8089

90+
function shouldSkipRequestLogging(req: Request, config: Required<LoggingConfig>): boolean {
91+
return config.excludePaths.some((path) => req.path === path);
92+
}
93+
94+
function createResponseFinishLogger(
95+
req: Request,
96+
res: Response,
97+
config: Required<LoggingConfig>,
98+
startHrTime: [number, number],
99+
getResponseBody: () => unknown,
100+
): () => void {
101+
return () => {
102+
const [seconds, nanoseconds] = process.hrtime(startHrTime);
103+
const durationMs = seconds * 1000 + nanoseconds / 1000000;
104+
const logData = createRequestLogEntry(req, res, durationMs, config, getResponseBody());
105+
106+
if (res.statusCode >= 400) {
107+
console.error(`[HTTP] ${res.statusCode} ${req.method} ${req.path}`, logData);
108+
return;
109+
}
110+
111+
console.log(`[HTTP] ${res.statusCode} ${req.method} ${req.path}`, logData);
112+
};
113+
}
114+
115+
function createResponseErrorLogger(
116+
req: Request,
117+
res: Response,
118+
startTime: number,
119+
getResponseBody: () => unknown,
120+
): (error: unknown) => void {
121+
return (error: unknown) => {
122+
const duration = Date.now() - startTime;
123+
const logData = createHttpResponseErrorLogPayload({
124+
method: req.method,
125+
path: req.path,
126+
statusCode: res.statusCode,
127+
durationMs: duration,
128+
requestId: resolveRequestId(req.headers['x-request-id']),
129+
error,
130+
responseBody: getResponseBody(),
131+
});
132+
console.error(`[HTTP_ERROR] ${req.method} ${req.path}`, logData);
133+
};
134+
}
135+
81136
/**
82137
* Create request/response logging middleware
83138
*/
84139
export function createRequestLoggingMiddleware(config: LoggingConfig = {}) {
85-
const finalConfig = { ...defaultConfig, ...config };
140+
const finalConfig = resolveLoggingConfig(config);
86141

87142
return (req: Request, res: Response, next: NextFunction) => {
88-
// Skip excluded paths
89-
if (finalConfig.excludePaths?.some((path) => req.path === path)) {
143+
if (shouldSkipRequestLogging(req, finalConfig)) {
90144
return next();
91145
}
92146

93-
// Start timer
94147
const startTime = Date.now();
95148
const startHrTime = process.hrtime();
96149

97-
// Capture response
98150
const originalSend = res.send;
99151
let responseBody: unknown = null;
152+
const getResponseBody = () => responseBody;
100153

101154
res.send = function (data: unknown) {
102155
responseBody = data;
103156
return originalSend.call(this, data);
104157
};
105158

106-
// Log on response finish
107-
res.on('finish', () => {
108-
const [seconds, nanoseconds] = process.hrtime(startHrTime);
109-
const durationMs = seconds * 1000 + nanoseconds / 1000000;
110-
const logData = createRequestLogEntry(req, res, durationMs, finalConfig, responseBody);
111-
112-
// Log response
113-
if (res.statusCode >= 400) {
114-
console.error(`[HTTP] ${res.statusCode} ${req.method} ${req.path}`, logData);
115-
} else {
116-
console.log(`[HTTP] ${res.statusCode} ${req.method} ${req.path}`, logData);
117-
}
118-
});
119-
120-
// Log on error
121-
res.on('error', (error: unknown) => {
122-
const duration = Date.now() - startTime;
123-
const logData = createHttpResponseErrorLogPayload({
124-
method: req.method,
125-
path: req.path,
126-
statusCode: res.statusCode,
127-
durationMs: duration,
128-
requestId: resolveRequestId(req.headers['x-request-id']),
129-
error,
130-
responseBody,
131-
});
132-
console.error(`[HTTP_ERROR] ${req.method} ${req.path}`, logData);
133-
});
159+
res.on('finish', createResponseFinishLogger(req, res, finalConfig, startHrTime, getResponseBody));
160+
res.on('error', createResponseErrorLogger(req, res, startTime, getResponseBody));
134161

135162
next();
136163
};
Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,39 @@
11
export const API_DOCS_PATH = '/api/docs';
22
export const OPENAPI_DOCUMENT_PATH = `${API_DOCS_PATH}/openapi.json`;
33
export const RUNTIME_CONFIG_PATH = '/api/config/server';
4+
export const RUNTIME_DISCOVERY_GUIDANCE_HEADING = 'Browser Runtime Discovery';
45

56
export const RUNTIME_DISCOVERY_GUIDANCE_LINES = {
67
sameOrigin: 'Web clients resolve the API from the current origin first, then fall back to the default runtime port.',
78
websocketFallback: 'WebSocket fallbacks should match the active browser protocol when runtime discovery is unavailable.',
89
legacyRetry: 'The legacy compatibility config endpoint is retried only if runtime discovery fails.',
910
} as const;
1011

11-
export const RUNTIME_DISCOVERY_GUIDANCE_DESCRIPTION = [
12+
export const RUNTIME_DISCOVERY_GUIDANCE_PARAGRAPHS = [
1213
RUNTIME_DISCOVERY_GUIDANCE_LINES.sameOrigin,
1314
RUNTIME_DISCOVERY_GUIDANCE_LINES.websocketFallback,
1415
RUNTIME_DISCOVERY_GUIDANCE_LINES.legacyRetry,
15-
].join(' ');
16+
];
1617

17-
export const DEFAULT_RUNTIME_API_SERVER_DESCRIPTION = `Default runtime API server. ${RUNTIME_DISCOVERY_GUIDANCE_LINES.sameOrigin}`;
18+
export function createRuntimeDiscoveryGuidanceDescription(
19+
paragraphs: readonly string[] = RUNTIME_DISCOVERY_GUIDANCE_PARAGRAPHS,
20+
): string {
21+
return paragraphs.join(' ');
22+
}
23+
24+
export function createRuntimeApiServerDescription(): string {
25+
return `Default runtime API server. ${RUNTIME_DISCOVERY_GUIDANCE_LINES.sameOrigin}`;
26+
}
27+
28+
export function createDocsRuntimeDiscoverySectionHtml(
29+
paragraphs: readonly string[] = RUNTIME_DISCOVERY_GUIDANCE_PARAGRAPHS,
30+
): string {
31+
return `
32+
<div class="endpoint" style="margin-top: 20px;">
33+
<h3>${RUNTIME_DISCOVERY_GUIDANCE_HEADING}</h3>
34+
${paragraphs.map((paragraph) => `<p>${paragraph}</p>`).join('\n ')}
35+
</div>`;
36+
}
37+
38+
export const RUNTIME_DISCOVERY_GUIDANCE_DESCRIPTION = createRuntimeDiscoveryGuidanceDescription();
39+
export const DEFAULT_RUNTIME_API_SERVER_DESCRIPTION = createRuntimeApiServerDescription();

0 commit comments

Comments
 (0)