4runr-os 2.10.39 → 2.10.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/apps/gateway/dist/apps/gateway/src/index.js +14 -4
  2. package/apps/gateway/dist/apps/gateway/src/index.js.map +1 -1
  3. package/apps/gateway/dist/apps/gateway/src/metrics/monitoring-detail.d.ts +18 -0
  4. package/apps/gateway/dist/apps/gateway/src/metrics/monitoring-detail.d.ts.map +1 -0
  5. package/apps/gateway/dist/apps/gateway/src/metrics/monitoring-detail.js +117 -0
  6. package/apps/gateway/dist/apps/gateway/src/metrics/monitoring-detail.js.map +1 -0
  7. package/apps/gateway/dist/apps/gateway/src/middleware/log-capture.d.ts +2 -0
  8. package/apps/gateway/dist/apps/gateway/src/middleware/log-capture.d.ts.map +1 -0
  9. package/apps/gateway/dist/apps/gateway/src/middleware/log-capture.js +54 -0
  10. package/apps/gateway/dist/apps/gateway/src/middleware/log-capture.js.map +1 -0
  11. package/apps/gateway/dist/apps/gateway/src/routes/monitoring.d.ts +15 -0
  12. package/apps/gateway/dist/apps/gateway/src/routes/monitoring.d.ts.map +1 -0
  13. package/apps/gateway/dist/apps/gateway/src/routes/monitoring.js +164 -0
  14. package/apps/gateway/dist/apps/gateway/src/routes/monitoring.js.map +1 -0
  15. package/apps/gateway/package-lock.json +204 -353
  16. package/apps/gateway/src/index.ts +27 -8
  17. package/apps/gateway/src/metrics/monitoring-detail.ts +162 -0
  18. package/apps/gateway/src/middleware/log-capture.ts +70 -0
  19. package/apps/gateway/src/routes/monitoring.ts +298 -0
  20. package/dist/gateway-client.d.ts +2 -0
  21. package/dist/gateway-client.d.ts.map +1 -1
  22. package/dist/gateway-client.js +22 -0
  23. package/dist/gateway-client.js.map +1 -1
  24. package/dist/tui-handlers.js +498 -0
  25. package/dist/tui-handlers.js.map +1 -1
  26. package/mk3-tui/src/app/render_scheduler.rs +111 -112
  27. package/mk3-tui/src/app.rs +1078 -295
  28. package/mk3-tui/src/debug_log.rs +131 -124
  29. package/mk3-tui/src/io/mod.rs +63 -66
  30. package/mk3-tui/src/io/protocol.rs +14 -15
  31. package/mk3-tui/src/io/stdio.rs +31 -32
  32. package/mk3-tui/src/io/ws.rs +25 -32
  33. package/mk3-tui/src/main.rs +774 -212
  34. package/mk3-tui/src/monitoring/mod.rs +428 -0
  35. package/mk3-tui/src/screens/mod.rs +53 -39
  36. package/mk3-tui/src/storage/cache.rs +221 -224
  37. package/mk3-tui/src/storage/mod.rs +5 -6
  38. package/mk3-tui/src/ui/agent_builder.rs +1148 -922
  39. package/mk3-tui/src/ui/agent_list.rs +344 -295
  40. package/mk3-tui/src/ui/boot.rs +145 -148
  41. package/mk3-tui/src/ui/connection_portal.rs +121 -98
  42. package/mk3-tui/src/ui/help.rs +340 -284
  43. package/mk3-tui/src/ui/layout.rs +966 -803
  44. package/mk3-tui/src/ui/mod.rs +1 -1
  45. package/mk3-tui/src/ui/portal_monitoring.rs +1027 -147
  46. package/mk3-tui/src/ui/run_manager.rs +784 -764
  47. package/mk3-tui/src/ui/safe_viewport.rs +236 -235
  48. package/mk3-tui/src/ui/settings.rs +414 -362
  49. package/mk3-tui/src/ui/setup_portal.rs +158 -101
  50. package/mk3-tui/src/websocket.rs +315 -308
  51. package/package.json +2 -2
@@ -72,7 +72,9 @@ import path from 'path';
72
72
  import { registerDevKitRoutes } from './devkit/routes.js';
73
73
  import { mfaRoutes } from './routes/mfa.js';
74
74
  import { gdprRoutes } from './routes/gdpr.js';
75
+ import { registerMonitoringRoutes } from './routes/monitoring.js';
75
76
  import { ddosProtection } from './middleware/ddos-protection.js';
77
+ import { wrapLoggerForMonitoring } from './middleware/log-capture.js';
76
78
  import { initializeDatabase, shutdownDatabase } from './db/init.js';
77
79
  import type { PrismaClient } from '@prisma/client';
78
80
 
@@ -86,7 +88,10 @@ if (persistenceMode === 'memory') {
86
88
  }
87
89
 
88
90
  // Create structured logger (will be updated per-request with correlation ID)
89
- const baseLogger = isMemoryMode ? memoryLogger : createLogger('Gateway');
91
+ let baseLogger = isMemoryMode ? memoryLogger : createLogger('Gateway');
92
+
93
+ // Wrap logger to capture logs for monitoring endpoints (Phase 0)
94
+ baseLogger = wrapLoggerForMonitoring(baseLogger);
90
95
 
91
96
  const PORT = env.PORT;
92
97
  const HOST = env.HOST;
@@ -274,11 +279,16 @@ if (dbResult.client && persistenceMode !== 'memory') {
274
279
 
275
280
  // Health endpoint (liveness probe - no auth required)
276
281
  fastify.get('/health', async (request, reply) => {
277
- healthCheckTotal.inc({ endpoint: '/health', status: '200' });
278
282
  const alive = isAlive();
279
283
  const { getMetricsSummary } = await import('./metrics/index.js');
280
284
  const summary = await getMetricsSummary();
281
285
 
286
+ const statusCode = alive ? 200 : 503;
287
+ reply.status(statusCode);
288
+
289
+ // Track actual HTTP status (not just 200)
290
+ healthCheckTotal.inc({ endpoint: '/health', status: String(statusCode) });
291
+
282
292
  return {
283
293
  ok: alive,
284
294
  status: alive ? 'alive' : 'dead',
@@ -368,16 +378,16 @@ fastify.get('/metrics', async (request, reply) => {
368
378
  const runQueue = getQueue();
369
379
  if (runQueue) {
370
380
  try {
381
+ // BullMQ v4+ getJobCounts: pass state names as separate args, returns exact keys
371
382
  const counts = await runQueue.getJobCounts(
372
- 'wait',
373
- 'paused',
374
- 'active',
375
- 'delayed',
376
383
  'waiting',
384
+ 'active',
377
385
  'completed',
378
- 'failed'
386
+ 'failed',
387
+ 'delayed',
388
+ 'paused'
379
389
  ) as Record<string, number>;
380
- const waiting = counts['waiting'] ?? counts['wait'] ?? 0;
390
+ const waiting = counts['waiting'] ?? 0;
381
391
  const activeJobs = counts['active'] ?? 0;
382
392
  queueJobsWaiting.set({ queue: 'run-execution' }, waiting);
383
393
  queueJobsActive.set({ queue: 'run-execution' }, activeJobs);
@@ -420,6 +430,15 @@ fastify.get('/api/monitoring/summary', async (request, reply) => {
420
430
  };
421
431
  });
422
432
 
433
+ // Register advanced monitoring routes (Phase 0)
434
+ // SECURITY: In production, these require auth. For local dev (127.0.0.1), consider skipping auth.
435
+ // Deployment guidance: use network policy to restrict access or enable requireAuth.
436
+ const isLocalDev = HOST === '127.0.0.1' || HOST === 'localhost';
437
+ registerMonitoringRoutes(fastify, {
438
+ requireAuth: isLocalDev ? false : requireAuth, // Explicitly false for local, function for prod
439
+ readRateLimit: readRateLimit,
440
+ });
441
+
423
442
  // Apply DDoS protection globally (before other middleware)
424
443
  fastify.addHook('onRequest', ddosProtection);
425
444
 
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Phase 3: structured snapshots from prom-client metrics for /api/monitoring/metrics/*
3
+ * (no Prometheus text parsing — reads live registry values.)
4
+ */
5
+
6
+ import {
7
+ httpRequestTotal,
8
+ httpRequestErrors,
9
+ runsCreated,
10
+ runsStarted,
11
+ runsCompleted,
12
+ runsActive,
13
+ sseConnectionsOpened,
14
+ sseConnectionsClosed,
15
+ sseActiveConnections,
16
+ sseMessagesTotal,
17
+ dbConnectionsActive,
18
+ dbConnectionsIdle,
19
+ redisConnectionsActive,
20
+ } from './index.js';
21
+
22
+ function sumCounterValues(m: { values: Array<{ value: number }> }): number {
23
+ return m.values.reduce((s, x) => s + x.value, 0);
24
+ }
25
+
26
+ function gaugeValue(m: { values: Array<{ value: number }> }): number {
27
+ return m.values[0]?.value ?? 0;
28
+ }
29
+
30
+ function isFailedRunStatus(status: string): boolean {
31
+ const normalized = status.trim().toLowerCase();
32
+ return [
33
+ 'failed',
34
+ 'failure',
35
+ 'error',
36
+ 'errored',
37
+ 'cancelled',
38
+ 'canceled',
39
+ 'timeout',
40
+ 'timed_out',
41
+ ].includes(normalized);
42
+ }
43
+
44
+ export async function getMonitoringHttpPayload(): Promise<Record<string, unknown>> {
45
+ const req = await httpRequestTotal.get();
46
+ let totalRequests = 0;
47
+ const byRoute: Array<{ method: string; route: string; status_code: string; count: number }> =
48
+ [];
49
+ for (const x of req.values) {
50
+ totalRequests += x.value;
51
+ byRoute.push({
52
+ method: String(x.labels.method ?? ''),
53
+ route: String(x.labels.route ?? ''),
54
+ status_code: String(x.labels.status_code ?? ''),
55
+ count: x.value,
56
+ });
57
+ }
58
+ byRoute.sort((a, b) => b.count - a.count);
59
+
60
+ const err = await httpRequestErrors.get();
61
+ let totalErrors = 0;
62
+ const errorsByRoute: Array<{
63
+ method: string;
64
+ route: string;
65
+ error_type: string;
66
+ count: number;
67
+ }> = [];
68
+ for (const x of err.values) {
69
+ totalErrors += x.value;
70
+ errorsByRoute.push({
71
+ method: String(x.labels.method ?? ''),
72
+ route: String(x.labels.route ?? ''),
73
+ error_type: String(x.labels.error_type ?? ''),
74
+ count: x.value,
75
+ });
76
+ }
77
+ errorsByRoute.sort((a, b) => b.count - a.count);
78
+
79
+ const errorRate = totalRequests > 0 ? totalErrors / totalRequests : 0;
80
+
81
+ return {
82
+ placeholder: false,
83
+ summary: {
84
+ totalRequests,
85
+ totalErrors,
86
+ errorRate: Number(errorRate.toFixed(4)),
87
+ },
88
+ topRoutes: byRoute.slice(0, 25),
89
+ errorsByRoute: errorsByRoute.slice(0, 15),
90
+ timestamp: new Date().toISOString(),
91
+ };
92
+ }
93
+
94
+ export async function getMonitoringRunsPayload(): Promise<Record<string, unknown>> {
95
+ const created = sumCounterValues(await runsCreated.get());
96
+ const started = sumCounterValues(await runsStarted.get());
97
+ const completedByStatus = (await runsCompleted.get()).values.map((x) => ({
98
+ status: String(x.labels.status ?? ''),
99
+ count: x.value,
100
+ }));
101
+ const completed = completedByStatus.reduce((s, x) => s + x.count, 0);
102
+ const failedByStatus = completedByStatus.filter((x) => isFailedRunStatus(x.status));
103
+ const active = gaugeValue(await runsActive.get());
104
+
105
+ return {
106
+ placeholder: false,
107
+ summary: {
108
+ created,
109
+ started,
110
+ completed,
111
+ failed: failedByStatus.reduce((s, x) => s + x.count, 0),
112
+ active,
113
+ },
114
+ byStatus: completedByStatus,
115
+ failedStatusLabels: failedByStatus.map((x) => x.status),
116
+ timestamp: new Date().toISOString(),
117
+ };
118
+ }
119
+
120
+ export async function getMonitoringSsePayload(): Promise<Record<string, unknown>> {
121
+ const opened = sumCounterValues(await sseConnectionsOpened.get());
122
+ const closed = sumCounterValues(await sseConnectionsClosed.get());
123
+ const active = gaugeValue(await sseActiveConnections.get());
124
+ const messages = sumCounterValues(await sseMessagesTotal.get());
125
+
126
+ return {
127
+ placeholder: false,
128
+ summary: {
129
+ connectionsOpened: opened,
130
+ connectionsClosed: closed,
131
+ activeConnections: active,
132
+ messagesSent: messages,
133
+ },
134
+ timestamp: new Date().toISOString(),
135
+ };
136
+ }
137
+
138
+ export async function getDependencyPoolsPayload(): Promise<{
139
+ database: { active: number; idle: number };
140
+ redis: { active: number };
141
+ telemetry: {
142
+ source: string;
143
+ bestEffort: boolean;
144
+ note: string;
145
+ };
146
+ }> {
147
+ return {
148
+ database: {
149
+ active: gaugeValue(await dbConnectionsActive.get()),
150
+ idle: gaugeValue(await dbConnectionsIdle.get()),
151
+ },
152
+ redis: {
153
+ active: gaugeValue(await redisConnectionsActive.get()),
154
+ },
155
+ telemetry: {
156
+ source: 'prom-client connection gauges',
157
+ bestEffort: true,
158
+ note:
159
+ 'Connection gauges are best-effort instrumentation; zeros can mean the gauges are not being updated, not necessarily that dependencies are disconnected.',
160
+ },
161
+ };
162
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Log capture middleware for monitoring log buffer
3
+ * Intercepts logger calls and feeds them to the monitoring log buffer
4
+ */
5
+
6
+ import { addMonitoringLog } from '../routes/monitoring.js';
7
+
8
+ /**
9
+ * Wrap a logger to capture log entries for monitoring
10
+ * Note: Uses duck typing to avoid strict type checks (compatible with both Logger and memoryLogger)
11
+ */
12
+ export function wrapLoggerForMonitoring(logger: any): any {
13
+ // Skip if this doesn't look like a standard logger (e.g., memoryLogger)
14
+ if (!logger || typeof logger.info !== 'function') {
15
+ return logger;
16
+ }
17
+
18
+ const originalInfo = logger.info.bind(logger);
19
+ const originalWarn = logger.warn.bind(logger);
20
+ const originalError = logger.error.bind(logger);
21
+ const originalDebug = logger.debug?.bind(logger);
22
+
23
+ logger.info = (message: string, data?: Record<string, unknown>) => {
24
+ addMonitoringLog({
25
+ timestamp: new Date().toISOString(),
26
+ level: 'info',
27
+ message,
28
+ context: logger.context || 'Gateway',
29
+ ...(data && { data }),
30
+ });
31
+ return originalInfo(message, data);
32
+ };
33
+
34
+ logger.warn = (message: string, data?: Record<string, unknown>) => {
35
+ addMonitoringLog({
36
+ timestamp: new Date().toISOString(),
37
+ level: 'warn',
38
+ message,
39
+ context: logger.context || 'Gateway',
40
+ ...(data && { data }),
41
+ });
42
+ return originalWarn(message, data);
43
+ };
44
+
45
+ logger.error = (message: string, data?: Record<string, unknown>) => {
46
+ addMonitoringLog({
47
+ timestamp: new Date().toISOString(),
48
+ level: 'error',
49
+ message,
50
+ context: logger.context || 'Gateway',
51
+ ...(data && { data }),
52
+ });
53
+ return originalError(message, data);
54
+ };
55
+
56
+ if (originalDebug) {
57
+ logger.debug = (message: string, data?: Record<string, unknown>) => {
58
+ addMonitoringLog({
59
+ timestamp: new Date().toISOString(),
60
+ level: 'debug',
61
+ message,
62
+ context: logger.context || 'Gateway',
63
+ ...(data && { data }),
64
+ });
65
+ return originalDebug(message, data);
66
+ };
67
+ }
68
+
69
+ return logger;
70
+ }
@@ -0,0 +1,298 @@
1
+ /**
2
+ * Advanced monitoring routes for Portal Monitoring (Phase 0)
3
+ * Provides structured logs, dependency details, and extended metrics
4
+ *
5
+ * SECURITY: All monitoring endpoints require authentication in production.
6
+ * For local development, consider network binding (127.0.0.1 only).
7
+ */
8
+
9
+ import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
10
+ import { getQueue } from '../queue/index.js';
11
+ import { getRedisClient } from '../db/redis.js';
12
+ import { getPrismaClient } from '../db/prisma.js';
13
+ import { performHealthChecks } from '../health/index.js';
14
+ import { createLogger } from '@4runr/shared';
15
+ import {
16
+ getDependencyPoolsPayload,
17
+ getMonitoringHttpPayload,
18
+ getMonitoringRunsPayload,
19
+ getMonitoringSsePayload,
20
+ } from '../metrics/monitoring-detail.js';
21
+
22
+ const logger = createLogger('Gateway:Monitoring');
23
+
24
+ interface LogEntry {
25
+ timestamp: string;
26
+ level: 'error' | 'warn' | 'info' | 'debug';
27
+ message: string;
28
+ context?: string;
29
+ data?: Record<string, unknown>;
30
+ }
31
+
32
+ // In-memory ring buffer for logs (last 1000 entries)
33
+ const LOG_BUFFER_SIZE = 1000;
34
+ const logBuffer: LogEntry[] = [];
35
+
36
+ /**
37
+ * Add log entry to buffer (called by logger middleware or manually)
38
+ */
39
+ export function addMonitoringLog(entry: LogEntry): void {
40
+ logBuffer.push(entry);
41
+ if (logBuffer.length > LOG_BUFFER_SIZE) {
42
+ logBuffer.shift();
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Register monitoring routes
48
+ * @param fastify Fastify instance
49
+ * @param options Optional configuration for route security
50
+ */
51
+ export function registerMonitoringRoutes(
52
+ fastify: FastifyInstance,
53
+ options?: {
54
+ requireAuth?: boolean | any; // boolean false or auth handler function
55
+ readRateLimit?: any;
56
+ }
57
+ ): void {
58
+ const { requireAuth, readRateLimit } = options || {};
59
+
60
+ // Determine if auth is required (requireAuth can be false, function, or undefined)
61
+ const authHandler = requireAuth === false ? null : requireAuth;
62
+ const preHandler = authHandler && readRateLimit
63
+ ? [authHandler, readRateLimit]
64
+ : authHandler
65
+ ? [authHandler]
66
+ : [];
67
+
68
+ /**
69
+ * GET /api/monitoring/logs
70
+ * Returns last N Gateway logs for TUI display
71
+ * SECURITY: Requires authentication (contains operational data)
72
+ */
73
+ fastify.get('/api/monitoring/logs', {
74
+ ...(preHandler.length > 0 && { preHandler }),
75
+ }, async (request: FastifyRequest, reply: FastifyReply) => {
76
+ const query = request.query as { limit?: string; level?: string };
77
+ const limit = Math.min(parseInt(query.limit || '100', 10), LOG_BUFFER_SIZE);
78
+ const levelFilter = query.level as LogEntry['level'] | undefined;
79
+
80
+ let logs = logBuffer.slice(-limit);
81
+
82
+ if (levelFilter) {
83
+ logs = logs.filter(log => log.level === levelFilter);
84
+ }
85
+
86
+ return {
87
+ logs,
88
+ count: logs.length,
89
+ bufferSize: logBuffer.length,
90
+ timestamp: new Date().toISOString(),
91
+ };
92
+ });
93
+
94
+ /**
95
+ * GET /api/monitoring/dependencies/detail
96
+ * Extended dependency info (connection pools, queue stats, etc.)
97
+ * SECURITY: Requires authentication (exposes infrastructure details)
98
+ */
99
+ fastify.get('/api/monitoring/dependencies/detail', {
100
+ ...(preHandler.length > 0 && { preHandler }),
101
+ }, async (request: FastifyRequest, reply: FastifyReply) => {
102
+ const health = await performHealthChecks();
103
+ const details: {
104
+ database?: {
105
+ status: string;
106
+ latency?: number;
107
+ poolAvailable?: boolean;
108
+ pool?: { active: number; idle: number };
109
+ telemetry?: { source: string; bestEffort: boolean; note: string };
110
+ };
111
+ redis?: {
112
+ status: string;
113
+ latency?: number;
114
+ connectionCountAvailable?: boolean;
115
+ connections?: { active: number };
116
+ telemetry?: { source: string; bestEffort: boolean; note: string };
117
+ };
118
+ queue?: {
119
+ status: string;
120
+ jobs?: {
121
+ waiting: number;
122
+ active: number;
123
+ completed: number;
124
+ failed: number;
125
+ delayed: number;
126
+ };
127
+ };
128
+ } = {};
129
+
130
+ const pools = await getDependencyPoolsPayload();
131
+
132
+ // Database details
133
+ if (health.checks.database) {
134
+ const dbLatency = health.checks.database.responseTime;
135
+ details.database = {
136
+ status: health.checks.database.status,
137
+ ...(dbLatency !== undefined && { latency: dbLatency }),
138
+ poolAvailable: true,
139
+ pool: {
140
+ active: pools.database.active,
141
+ idle: pools.database.idle,
142
+ },
143
+ telemetry: pools.telemetry,
144
+ };
145
+ }
146
+
147
+ // Redis details
148
+ if (health.checks.redis) {
149
+ const redisLatency = health.checks.redis.responseTime;
150
+ details.redis = {
151
+ status: health.checks.redis.status,
152
+ ...(redisLatency !== undefined && { latency: redisLatency }),
153
+ connectionCountAvailable: true,
154
+ connections: {
155
+ active: pools.redis.active,
156
+ },
157
+ telemetry: pools.telemetry,
158
+ };
159
+ }
160
+
161
+ // Queue details (real data from BullMQ)
162
+ if (health.checks.queue) {
163
+ details.queue = {
164
+ status: health.checks.queue.status,
165
+ };
166
+
167
+ try {
168
+ const queue = getQueue();
169
+ if (queue) {
170
+ // BullMQ v4+ getJobCounts: pass state names as separate args
171
+ // Returns object with keys matching exact state names passed
172
+ const counts = await queue.getJobCounts(
173
+ 'waiting',
174
+ 'active',
175
+ 'completed',
176
+ 'failed',
177
+ 'delayed'
178
+ ) as Record<string, number>;
179
+
180
+ details.queue.jobs = {
181
+ waiting: counts['waiting'] ?? 0,
182
+ active: counts['active'] ?? 0,
183
+ completed: counts['completed'] ?? 0,
184
+ failed: counts['failed'] ?? 0,
185
+ delayed: counts['delayed'] ?? 0,
186
+ };
187
+ }
188
+ } catch (err) {
189
+ logger.error('Failed to fetch queue job counts', {
190
+ error: err instanceof Error ? err.message : String(err),
191
+ });
192
+ }
193
+ }
194
+
195
+ return {
196
+ details,
197
+ telemetry: pools.telemetry,
198
+ timestamp: new Date().toISOString(),
199
+ };
200
+ });
201
+
202
+ /**
203
+ * GET /api/monitoring/metrics/http
204
+ * Detailed HTTP metrics with route breakdown
205
+ * SECURITY: Requires authentication
206
+ */
207
+ fastify.get('/api/monitoring/metrics/http', {
208
+ ...(preHandler.length > 0 && { preHandler }),
209
+ }, async (_request: FastifyRequest, _reply: FastifyReply) => {
210
+ return await getMonitoringHttpPayload();
211
+ });
212
+
213
+ /**
214
+ * GET /api/monitoring/metrics/runs
215
+ * Detailed run metrics
216
+ * SECURITY: Requires authentication
217
+ */
218
+ fastify.get('/api/monitoring/metrics/runs', {
219
+ ...(preHandler.length > 0 && { preHandler }),
220
+ }, async (_request: FastifyRequest, _reply: FastifyReply) => {
221
+ return await getMonitoringRunsPayload();
222
+ });
223
+
224
+ /**
225
+ * GET /api/monitoring/metrics/sse
226
+ * SSE connection / message counters (Phase 3)
227
+ */
228
+ fastify.get('/api/monitoring/metrics/sse', {
229
+ ...(preHandler.length > 0 && { preHandler }),
230
+ }, async (_request: FastifyRequest, _reply: FastifyReply) => {
231
+ return await getMonitoringSsePayload();
232
+ });
233
+
234
+ /**
235
+ * GET /api/monitoring/metrics/queue
236
+ * Detailed queue metrics with job states
237
+ * SECURITY: Requires authentication
238
+ */
239
+ fastify.get('/api/monitoring/metrics/queue', {
240
+ ...(preHandler.length > 0 && { preHandler }),
241
+ }, async (request: FastifyRequest, reply: FastifyReply) => {
242
+ try {
243
+ const queue = getQueue();
244
+ if (!queue) {
245
+ return {
246
+ available: false,
247
+ message: 'Queue not initialized (Redis required)',
248
+ timestamp: new Date().toISOString(),
249
+ };
250
+ }
251
+
252
+ // BullMQ v4+ getJobCounts: returns exact keys matching state names passed
253
+ const counts = await queue.getJobCounts(
254
+ 'waiting',
255
+ 'active',
256
+ 'completed',
257
+ 'failed',
258
+ 'delayed',
259
+ 'paused'
260
+ ) as Record<string, number>;
261
+
262
+ return {
263
+ available: true,
264
+ jobs: {
265
+ waiting: counts['waiting'] ?? 0,
266
+ active: counts['active'] ?? 0,
267
+ completed: counts['completed'] ?? 0,
268
+ failed: counts['failed'] ?? 0,
269
+ delayed: counts['delayed'] ?? 0,
270
+ paused: counts['paused'] ?? 0,
271
+ },
272
+ queueName: (queue as any).name || 'run-execution',
273
+ timestamp: new Date().toISOString(),
274
+ };
275
+ } catch (error) {
276
+ logger.error('Failed to get queue metrics', {
277
+ error: error instanceof Error ? error.message : String(error),
278
+ });
279
+ return {
280
+ available: false,
281
+ error: error instanceof Error ? error.message : String(error),
282
+ timestamp: new Date().toISOString(),
283
+ };
284
+ }
285
+ });
286
+
287
+ logger.info('Monitoring routes registered (Phase 0)', {
288
+ authRequired: preHandler.length > 0,
289
+ endpoints: [
290
+ '/api/monitoring/logs',
291
+ '/api/monitoring/dependencies/detail',
292
+ '/api/monitoring/metrics/http',
293
+ '/api/monitoring/metrics/runs',
294
+ '/api/monitoring/metrics/sse',
295
+ '/api/monitoring/metrics/queue',
296
+ ],
297
+ });
298
+ }
@@ -102,5 +102,7 @@ export declare class GatewayClient {
102
102
  * Raw Prometheus text from GET /metrics (public on Gateway; no auth).
103
103
  */
104
104
  prometheusMetrics(): Promise<string>;
105
+ /** GET JSON (monitoring APIs, etc.). Uses same auth headers as the rest of the client. */
106
+ getJson(path: string): Promise<unknown>;
105
107
  }
106
108
  //# sourceMappingURL=gateway-client.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"gateway-client.d.ts","sourceRoot":"","sources":["../src/gateway-client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"gateway-client.d.ts","sourceRoot":"","sources":["../src/gateway-client.ts"],"names":[],"mappings":"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"}
@@ -224,5 +224,27 @@ export class GatewayClient {
224
224
  const body = response.data;
225
225
  return typeof body === 'string' ? body : String(body ?? '');
226
226
  }
227
+ /** GET JSON (monitoring APIs, etc.). Uses same auth headers as the rest of the client. */
228
+ async getJson(path) {
229
+ const response = await this.http.get(path, {
230
+ validateStatus: () => true,
231
+ timeout: 15000,
232
+ });
233
+ if (response.status >= 400) {
234
+ const body = response.data;
235
+ const detail = body && typeof body === 'object'
236
+ ? String(body.error ??
237
+ body.message ??
238
+ '')
239
+ : typeof body === 'string'
240
+ ? body.slice(0, 240)
241
+ : '';
242
+ const authHint = response.status === 401 || response.status === 403
243
+ ? ' (authentication failed or expired; reconnect to the Gateway and retry)'
244
+ : '';
245
+ throw new Error(`${path} returned HTTP ${response.status}${authHint}${detail ? `: ${detail}` : ''}`);
246
+ }
247
+ return response.data;
248
+ }
227
249
  }
228
250
  //# sourceMappingURL=gateway-client.js.map