robodev 0.35.0 → 0.37.0

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.
package/src/dev-server.ts CHANGED
@@ -32,7 +32,13 @@ import type {
32
32
  } from "@robodev-ai/sdk";
33
33
  import type { RuntimeJob } from "@robodev-ai/runtime";
34
34
  import { devRouteClients } from "./dev-clients.js";
35
- import { isInspectPath, type DevInspectStore } from "./dev-inspect.js";
35
+ import {
36
+ buildDashboardState,
37
+ isInspectPath,
38
+ shouldCountRequestHistogram,
39
+ truncateInspectPreview,
40
+ type DevInspectStore,
41
+ } from "./dev-inspect.js";
36
42
 
37
43
  /** One compiled generation of the project. Swapped wholesale on reload. */
38
44
  export type DevRuntime = {
@@ -151,26 +157,52 @@ async function resolveSocketUser(
151
157
  return { ok: true, user };
152
158
  }
153
159
 
160
+ function recordSocketHandshake(
161
+ inspect: DevInspectStore,
162
+ input: {
163
+ path: string;
164
+ name?: string;
165
+ userId?: string | null;
166
+ error?: string;
167
+ },
168
+ ): void {
169
+ inspect.record({
170
+ kind: "socket",
171
+ socketKind: "connect",
172
+ path: input.path,
173
+ name: input.name,
174
+ userId: input.userId,
175
+ error: input.error,
176
+ });
177
+ }
178
+
154
179
  async function handleLocalSocket(
155
180
  socket: WebSocket,
156
181
  request: FastifyRequest,
157
182
  options: DevServerOptions,
158
183
  getRuntime: () => DevRuntime | null,
159
184
  ): Promise<void> {
185
+ const pathname = pathnameOf(request.url);
160
186
  const runtime = getRuntime();
161
187
  if (!runtime) {
188
+ recordSocketHandshake(options.inspect, { path: pathname, error: "not_found" });
162
189
  socket.close(SOCKET_CLOSE_POLICY, "not_found");
163
190
  return;
164
191
  }
165
- const pathname = pathnameOf(request.url);
166
192
  const matched = matchSocketRoute(pathname, runtime.sockets);
167
193
  if (!matched) {
194
+ recordSocketHandshake(options.inspect, { path: pathname, error: "not_found" });
168
195
  socket.close(SOCKET_CLOSE_POLICY, "not_found");
169
196
  return;
170
197
  }
171
198
 
172
199
  const auth = await resolveSocketUser(options, matched.socket.def, request);
173
200
  if (!auth.ok) {
201
+ recordSocketHandshake(options.inspect, {
202
+ path: pathname,
203
+ name: matched.socket.name,
204
+ error: "unauthorized",
205
+ });
174
206
  socket.close(SOCKET_CLOSE_POLICY, "unauthorized");
175
207
  return;
176
208
  }
@@ -210,12 +242,24 @@ async function handleLocalSocket(
210
242
  onClose: matched.socket.def.onClose,
211
243
  });
212
244
  if (!session.ok) {
245
+ recordSocketHandshake(options.inspect, {
246
+ path: pathname,
247
+ name: matched.socket.name,
248
+ userId: auth.user?.id ?? null,
249
+ error: session.reason,
250
+ });
213
251
  if (socket.readyState === socket.OPEN) {
214
252
  socket.close(session.code, session.reason);
215
253
  }
216
254
  return;
217
255
  }
218
256
 
257
+ recordSocketHandshake(options.inspect, {
258
+ path: pathname,
259
+ name: matched.socket.name,
260
+ userId: auth.user?.id ?? null,
261
+ });
262
+
219
263
  let alive = true;
220
264
  const pingTimer = setInterval(() => {
221
265
  if (socket.readyState !== socket.OPEN) {
@@ -240,18 +284,52 @@ async function handleLocalSocket(
240
284
  });
241
285
  socket.on("message", (data, isBinary) => {
242
286
  if (isBinary) {
287
+ options.inspect.record({
288
+ kind: "socket",
289
+ socketKind: "message",
290
+ path: pathname,
291
+ name: matched.socket.name,
292
+ userId: auth.user?.id ?? null,
293
+ error: "binary",
294
+ });
243
295
  session.onBinary();
244
296
  return;
245
297
  }
246
298
  const text = typeof data === "string" ? data : Buffer.from(data as Uint8Array).toString("utf8");
299
+ options.inspect.record({
300
+ kind: "socket",
301
+ socketKind: "message",
302
+ path: pathname,
303
+ name: matched.socket.name,
304
+ userId: auth.user?.id ?? null,
305
+ preview: truncateInspectPreview(text),
306
+ });
247
307
  session.onText(text);
248
308
  });
249
- socket.on("close", () => {
309
+ socket.on("close", (code, reason) => {
250
310
  clearInterval(pingTimer);
311
+ options.inspect.record({
312
+ kind: "socket",
313
+ socketKind: "close",
314
+ path: pathname,
315
+ name: matched.socket.name,
316
+ userId: auth.user?.id ?? null,
317
+ code,
318
+ reason: reason?.toString() || undefined,
319
+ });
251
320
  session.onClose();
252
321
  });
253
322
  }
254
323
 
324
+ function recordStorageServe(
325
+ inspect: DevInspectStore,
326
+ key: string,
327
+ ok: boolean,
328
+ error?: string,
329
+ ): void {
330
+ inspect.record({ kind: "storage", op: "serve", key, ok, error });
331
+ }
332
+
255
333
  async function serveLocalStorage(
256
334
  request: FastifyRequest,
257
335
  reply: FastifyReply,
@@ -259,12 +337,13 @@ async function serveLocalStorage(
259
337
  getRuntime: () => DevRuntime | null,
260
338
  pathname: string,
261
339
  ): Promise<void> {
340
+ const encodedKey = pathname.slice("/storage/objects/".length);
262
341
  const runtime = getRuntime();
263
342
  if (!runtime) {
264
343
  await reply.code(503).send({ error: "not_ready" });
344
+ recordStorageServe(options.inspect, encodedKey, false, "not_ready");
265
345
  return;
266
346
  }
267
- const encodedKey = pathname.slice("/storage/objects/".length);
268
347
  let key: string;
269
348
  try {
270
349
  key = decodeURIComponent(encodedKey);
@@ -273,9 +352,12 @@ async function serveLocalStorage(
273
352
  const mapped = codedErrorBody(error);
274
353
  if (mapped) {
275
354
  await reply.code(mapped.status).send(mapped.body);
355
+ const mappedError = typeof mapped.body.error === "string" ? mapped.body.error : "invalid_key";
356
+ recordStorageServe(options.inspect, encodedKey, false, mappedError);
276
357
  return;
277
358
  }
278
359
  await reply.code(400).send({ error: "invalid_key" });
360
+ recordStorageServe(options.inspect, encodedKey, false, "invalid_key");
279
361
  return;
280
362
  }
281
363
 
@@ -286,6 +368,7 @@ async function serveLocalStorage(
286
368
  });
287
369
  if (!resolved) {
288
370
  await reply.code(404).send({ error: "not_found" });
371
+ recordStorageServe(options.inspect, key, false, "not_found");
289
372
  return;
290
373
  }
291
374
 
@@ -301,16 +384,18 @@ async function serveLocalStorage(
301
384
  );
302
385
  if (!resolved.row.public && !user && !signed) {
303
386
  await reply.code(404).send({ error: "not_found" });
387
+ recordStorageServe(options.inspect, key, false, "not_found");
304
388
  return;
305
389
  }
306
390
 
307
391
  await reply.type(resolved.row.content_type).send(resolved.body);
392
+ recordStorageServe(options.inspect, key, true);
308
393
  }
309
394
 
310
395
  /**
311
396
  * A single-project HTTP host. Unlike the hosted project host there is no `Host` lookup
312
397
  * and no static SPA serving (local Vite does that). Fastify stays `logger: false`;
313
- * project/auth/docs requests are recorded on the process-local inspect page after reply.
398
+ * project/auth/docs requests are recorded on the process-local dashboard after reply.
314
399
  * Routes, validation, and OpenAPI all come from `@robodev-ai/runtime`.
315
400
  */
316
401
  export async function startDevServer(
@@ -377,8 +462,19 @@ export async function startDevServer(
377
462
  const method = request.method.toLowerCase();
378
463
 
379
464
  if (isInspectPath(pathname)) {
380
- if (pathname === "/_robodev/logs.json") {
381
- return reply.send(options.inspect.list());
465
+ if (pathname === "/_robodev/logs") {
466
+ return reply.redirect("/_robodev");
467
+ }
468
+ if (pathname === "/_robodev/state.json" || pathname === "/_robodev/logs.json") {
469
+ return reply.send(
470
+ await buildDashboardState({
471
+ inspect: options.inspect,
472
+ runtime: getRuntime(),
473
+ projectId: options.projectId,
474
+ googleClientId: options.googleClientId,
475
+ googleClientSecret: options.googleClientSecret,
476
+ }),
477
+ );
382
478
  }
383
479
  return reply.type("text/html; charset=utf-8").send(options.inspect.html());
384
480
  }
@@ -389,15 +485,20 @@ export async function startDevServer(
389
485
  } finally {
390
486
  const status = reply.statusCode;
391
487
  const durationMs = Date.now() - started;
392
- options.inspect.record({
393
- kind: "request",
394
- method: request.method,
395
- path: pathname,
396
- status,
397
- durationMs,
398
- });
399
- // Print here (not via `log`) so request lines stay off the Inspect Logs list.
400
- console.log(`${request.method} ${pathname} ${status} ${durationMs}ms`);
488
+ if (!isInspectPath(pathname)) {
489
+ options.inspect.record({
490
+ kind: "request",
491
+ method: request.method,
492
+ path: pathname,
493
+ status,
494
+ durationMs,
495
+ });
496
+ // Print here (not via `log`) so request lines stay off compile/reload logs.
497
+ console.log(`${request.method} ${pathname} ${status} ${durationMs}ms`);
498
+ if (shouldCountRequestHistogram(pathname, getRuntime(), request.method)) {
499
+ options.inspect.incrementRequestHistogram(status);
500
+ }
501
+ }
401
502
  }
402
503
  return reply;
403
504
 
package/src/dev.ts CHANGED
@@ -192,6 +192,16 @@ export async function main(args: string[]): Promise<void> {
192
192
  projectId: DEV_PROJECT_ID,
193
193
  jwtSecret: DEV_JWT_SECRET,
194
194
  }),
195
+ onJobFinished: (event) => {
196
+ inspect.record({
197
+ kind: "job_run",
198
+ name: event.name,
199
+ status: event.outcome,
200
+ payload: event.payload,
201
+ error: event.error ?? undefined,
202
+ });
203
+ inspect.incrementJobHistogram(event.outcome);
204
+ },
195
205
  });
196
206
 
197
207
  async function compileAndActivate(input: { syncSchema: boolean }): Promise<void> {
@@ -323,7 +333,7 @@ export async function main(args: string[]): Promise<void> {
323
333
  console.log(`Storage ${relative(project.root, storageDir) || storageDir}`);
324
334
  console.log(`API ${apiUrl}`);
325
335
  console.log(`Swagger ${apiUrl}/_robodev/docs`);
326
- console.log(`Inspect ${apiUrl}/_robodev/logs`);
336
+ console.log(`Dashboard ${apiUrl}/_robodev`);
327
337
  console.log(
328
338
  `Project ${relative(process.cwd(), project.backendDir) || "."} (${project.layout})`,
329
339
  );
@@ -12,7 +12,7 @@
12
12
  "@robodev-ai/sdk": "^0.11.0"
13
13
  },
14
14
  "devDependencies": {
15
- "robodev": "^0.35.0",
15
+ "robodev": "^0.37.0",
16
16
  "rulesync": "^8.18.0",
17
17
  "typescript": "^5.9.2"
18
18
  }
@@ -11,7 +11,7 @@
11
11
  "@robodev-ai/sdk": "^0.11.0"
12
12
  },
13
13
  "devDependencies": {
14
- "robodev": "^0.35.0",
14
+ "robodev": "^0.37.0",
15
15
  "typescript": "^5.9.2"
16
16
  }
17
17
  }
@@ -11,7 +11,7 @@
11
11
  "@robodev-ai/sdk": "^0.11.0"
12
12
  },
13
13
  "devDependencies": {
14
- "robodev": "^0.35.0",
14
+ "robodev": "^0.37.0",
15
15
  "typescript": "^5.9.2"
16
16
  }
17
17
  }
@@ -12,7 +12,7 @@
12
12
  "@robodev-ai/sdk": "^0.11.0"
13
13
  },
14
14
  "devDependencies": {
15
- "robodev": "^0.35.0",
15
+ "robodev": "^0.37.0",
16
16
  "rulesync": "^8.18.0",
17
17
  "typescript": "^5.9.2"
18
18
  }
@@ -12,7 +12,7 @@
12
12
  "@robodev-ai/sdk": "^0.11.0"
13
13
  },
14
14
  "devDependencies": {
15
- "robodev": "^0.35.0",
15
+ "robodev": "^0.37.0",
16
16
  "rulesync": "^8.18.0",
17
17
  "typescript": "^5.9.2"
18
18
  }