@a5c-ai/babysitter-observer-dashboard 1.0.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/LICENSE +21 -0
- package/README.md +490 -0
- package/next.config.mjs +25 -0
- package/package.json +104 -0
- package/postcss.config.mjs +8 -0
- package/src/app/actions/__tests__/approve-breakpoint.test.ts +246 -0
- package/src/app/actions/approve-breakpoint.ts +145 -0
- package/src/app/api/config/route.ts +137 -0
- package/src/app/api/digest/route.ts +45 -0
- package/src/app/api/runs/[runId]/events/route.ts +56 -0
- package/src/app/api/runs/[runId]/route.ts +84 -0
- package/src/app/api/runs/[runId]/tasks/[effectId]/route.ts +44 -0
- package/src/app/api/runs/route.ts +48 -0
- package/src/app/api/stream/route.ts +136 -0
- package/src/app/api/test/route.ts +1 -0
- package/src/app/api/version/route.ts +57 -0
- package/src/app/globals.css +555 -0
- package/src/app/icon.svg +20 -0
- package/src/app/layout.tsx +39 -0
- package/src/app/not-found.tsx +16 -0
- package/src/app/page.tsx +120 -0
- package/src/app/runs/[runId]/page.tsx +279 -0
- package/src/cli.ts +271 -0
- package/src/components/breakpoint/__tests__/breakpoint-approval.test.tsx +212 -0
- package/src/components/breakpoint/__tests__/breakpoint-panel.test.tsx +130 -0
- package/src/components/breakpoint/__tests__/file-preview.test.tsx +313 -0
- package/src/components/breakpoint/breakpoint-approval.tsx +138 -0
- package/src/components/breakpoint/breakpoint-panel.tsx +95 -0
- package/src/components/breakpoint/file-preview.tsx +215 -0
- package/src/components/dashboard/.gitkeep +0 -0
- package/src/components/dashboard/__tests__/breakpoint-banner.test.tsx +177 -0
- package/src/components/dashboard/__tests__/catch-up-banner.test.tsx +141 -0
- package/src/components/dashboard/__tests__/executive-summary-banner.test.tsx +164 -0
- package/src/components/dashboard/__tests__/kpi-grid.test.tsx +101 -0
- package/src/components/dashboard/__tests__/pagination-controls.test.tsx +125 -0
- package/src/components/dashboard/__tests__/project-accordion.test.tsx +97 -0
- package/src/components/dashboard/__tests__/project-list-view.test.tsx +174 -0
- package/src/components/dashboard/__tests__/project-search-input.test.tsx +110 -0
- package/src/components/dashboard/__tests__/project-section-header.test.tsx +91 -0
- package/src/components/dashboard/__tests__/project-section.test.tsx +151 -0
- package/src/components/dashboard/__tests__/run-card.test.tsx +164 -0
- package/src/components/dashboard/__tests__/run-filter-bar.test.tsx +109 -0
- package/src/components/dashboard/__tests__/run-list.test.tsx +123 -0
- package/src/components/dashboard/__tests__/search-filter.test.tsx +150 -0
- package/src/components/dashboard/__tests__/virtualized-run-list.test.tsx +179 -0
- package/src/components/dashboard/breakpoint-banner.tsx +301 -0
- package/src/components/dashboard/catch-up-banner.tsx +88 -0
- package/src/components/dashboard/executive-summary-banner.tsx +174 -0
- package/src/components/dashboard/global-search.tsx +323 -0
- package/src/components/dashboard/kpi-grid.tsx +140 -0
- package/src/components/dashboard/pagination-controls.tsx +100 -0
- package/src/components/dashboard/project-accordion.tsx +72 -0
- package/src/components/dashboard/project-health-card.tsx +536 -0
- package/src/components/dashboard/project-list-view.tsx +246 -0
- package/src/components/dashboard/project-search-input.tsx +41 -0
- package/src/components/dashboard/project-section-header.tsx +73 -0
- package/src/components/dashboard/project-section.tsx +89 -0
- package/src/components/dashboard/run-card.tsx +218 -0
- package/src/components/dashboard/run-filter-bar.tsx +100 -0
- package/src/components/dashboard/run-list.tsx +77 -0
- package/src/components/dashboard/search-filter.tsx +69 -0
- package/src/components/dashboard/virtualized-run-list.tsx +130 -0
- package/src/components/details/.gitkeep +0 -0
- package/src/components/details/__tests__/agent-panel.test.tsx +236 -0
- package/src/components/details/__tests__/json-tree.test.tsx +347 -0
- package/src/components/details/__tests__/log-viewer.test.tsx +168 -0
- package/src/components/details/__tests__/task-detail.test.tsx +212 -0
- package/src/components/details/__tests__/timing-panel.test.tsx +271 -0
- package/src/components/details/agent-panel.tsx +234 -0
- package/src/components/details/json-tree/categorize.ts +131 -0
- package/src/components/details/json-tree/index.tsx +120 -0
- package/src/components/details/json-tree/json-node.tsx +223 -0
- package/src/components/details/json-tree/smart-summary.tsx +596 -0
- package/src/components/details/json-tree/tree-controls.tsx +47 -0
- package/src/components/details/json-tree.tsx +9 -0
- package/src/components/details/log-viewer.tsx +140 -0
- package/src/components/details/task-detail.tsx +114 -0
- package/src/components/details/timing-panel.tsx +247 -0
- package/src/components/events/.gitkeep +0 -0
- package/src/components/events/__tests__/event-item.test.tsx +211 -0
- package/src/components/events/__tests__/event-stream.test.tsx +225 -0
- package/src/components/events/event-item.tsx +121 -0
- package/src/components/events/event-stream.tsx +260 -0
- package/src/components/notifications/.gitkeep +0 -0
- package/src/components/notifications/__tests__/notification-panel.test.tsx +287 -0
- package/src/components/notifications/__tests__/notification-provider.test.tsx +585 -0
- package/src/components/notifications/__tests__/toast-stack.test.tsx +217 -0
- package/src/components/notifications/notification-panel.tsx +124 -0
- package/src/components/notifications/notification-provider.tsx +175 -0
- package/src/components/notifications/toast-stack.tsx +75 -0
- package/src/components/pipeline/.gitkeep +0 -0
- package/src/components/pipeline/__tests__/parallel-group.test.tsx +88 -0
- package/src/components/pipeline/__tests__/pipeline-view.test.tsx +345 -0
- package/src/components/pipeline/__tests__/step-card.test.tsx +330 -0
- package/src/components/pipeline/parallel-group.tsx +39 -0
- package/src/components/pipeline/pipeline-view.tsx +197 -0
- package/src/components/pipeline/step-card.tsx +166 -0
- package/src/components/providers/event-stream-provider.tsx +29 -0
- package/src/components/providers.tsx +24 -0
- package/src/components/shared/.gitkeep +0 -0
- package/src/components/shared/__tests__/empty-state.test.tsx +49 -0
- package/src/components/shared/__tests__/friendly-id.test.tsx +47 -0
- package/src/components/shared/__tests__/kbd.test.tsx +45 -0
- package/src/components/shared/__tests__/kind-badge.test.tsx +71 -0
- package/src/components/shared/__tests__/metrics-row.test.tsx +74 -0
- package/src/components/shared/__tests__/outcome-banner.test.tsx +71 -0
- package/src/components/shared/__tests__/progress-bar.test.tsx +89 -0
- package/src/components/shared/__tests__/session-pill.test.tsx +62 -0
- package/src/components/shared/__tests__/settings-modal.test.tsx +201 -0
- package/src/components/shared/__tests__/shortcuts-help.test.tsx +103 -0
- package/src/components/shared/__tests__/status-badge.test.tsx +98 -0
- package/src/components/shared/__tests__/theme-provider.test.tsx +100 -0
- package/src/components/shared/__tests__/truncated-id.test.tsx +53 -0
- package/src/components/shared/app-footer.tsx +80 -0
- package/src/components/shared/app-header.tsx +160 -0
- package/src/components/shared/empty-state.tsx +18 -0
- package/src/components/shared/error-boundary.tsx +81 -0
- package/src/components/shared/friendly-id.tsx +48 -0
- package/src/components/shared/kbd.tsx +15 -0
- package/src/components/shared/kind-badge.tsx +51 -0
- package/src/components/shared/metrics-row.tsx +106 -0
- package/src/components/shared/outcome-banner.tsx +56 -0
- package/src/components/shared/progress-bar.tsx +42 -0
- package/src/components/shared/session-pill.tsx +69 -0
- package/src/components/shared/settings-modal.tsx +509 -0
- package/src/components/shared/shortcuts-help.tsx +113 -0
- package/src/components/shared/status-badge.tsx +110 -0
- package/src/components/shared/theme-provider.tsx +46 -0
- package/src/components/shared/truncated-id.tsx +51 -0
- package/src/components/ui/.gitkeep +0 -0
- package/src/components/ui/__tests__/accordion.test.tsx +96 -0
- package/src/components/ui/__tests__/badge.test.tsx +69 -0
- package/src/components/ui/__tests__/button.test.tsx +113 -0
- package/src/components/ui/__tests__/tabs.test.tsx +75 -0
- package/src/components/ui/__tests__/tooltip.test.tsx +90 -0
- package/src/components/ui/accordion.tsx +61 -0
- package/src/components/ui/badge.tsx +25 -0
- package/src/components/ui/button.tsx +40 -0
- package/src/components/ui/card.tsx +21 -0
- package/src/components/ui/scroll-area.tsx +35 -0
- package/src/components/ui/separator.tsx +24 -0
- package/src/components/ui/tabs.tsx +64 -0
- package/src/components/ui/tooltip.tsx +37 -0
- package/src/hooks/.gitkeep +0 -0
- package/src/hooks/__tests__/use-animated-number.test.ts +184 -0
- package/src/hooks/__tests__/use-batched-updates.test.ts +315 -0
- package/src/hooks/__tests__/use-event-stream.test.ts +243 -0
- package/src/hooks/__tests__/use-keyboard.test.ts +217 -0
- package/src/hooks/__tests__/use-notifications.test.ts +230 -0
- package/src/hooks/__tests__/use-polling.test.ts +274 -0
- package/src/hooks/__tests__/use-project-runs.test.ts +163 -0
- package/src/hooks/__tests__/use-projects.test.ts +248 -0
- package/src/hooks/__tests__/use-run-dashboard.test.ts +168 -0
- package/src/hooks/__tests__/use-run-detail.test.ts +273 -0
- package/src/hooks/__tests__/use-smart-polling.test.ts +305 -0
- package/src/hooks/use-animated-number.ts +87 -0
- package/src/hooks/use-batched-updates.ts +150 -0
- package/src/hooks/use-event-stream.ts +150 -0
- package/src/hooks/use-keyboard.ts +45 -0
- package/src/hooks/use-notifications.ts +82 -0
- package/src/hooks/use-persisted-state.ts +60 -0
- package/src/hooks/use-polling.ts +60 -0
- package/src/hooks/use-project-runs.ts +51 -0
- package/src/hooks/use-projects.ts +26 -0
- package/src/hooks/use-run-dashboard.ts +207 -0
- package/src/hooks/use-run-detail.ts +77 -0
- package/src/hooks/use-smart-polling.ts +144 -0
- package/src/lib/.gitkeep +0 -0
- package/src/lib/__tests__/cn.test.ts +69 -0
- package/src/lib/__tests__/config-loader.test.ts +210 -0
- package/src/lib/__tests__/config.test.ts +561 -0
- package/src/lib/__tests__/error-handler.test.ts +143 -0
- package/src/lib/__tests__/fetcher.test.ts +517 -0
- package/src/lib/__tests__/global-registry.test.ts +214 -0
- package/src/lib/__tests__/parser.test.ts +1532 -0
- package/src/lib/__tests__/path-resolver.test.ts +112 -0
- package/src/lib/__tests__/run-cache.test.ts +591 -0
- package/src/lib/__tests__/server-init.test.ts +512 -0
- package/src/lib/__tests__/source-discovery.test.ts +246 -0
- package/src/lib/__tests__/utils.test.ts +160 -0
- package/src/lib/__tests__/watcher.test.ts +227 -0
- package/src/lib/cn.ts +6 -0
- package/src/lib/config-loader.ts +195 -0
- package/src/lib/config.ts +20 -0
- package/src/lib/error-handler.ts +76 -0
- package/src/lib/fetcher.ts +394 -0
- package/src/lib/global-registry.ts +117 -0
- package/src/lib/parser.ts +794 -0
- package/src/lib/path-resolver.ts +16 -0
- package/src/lib/run-cache.ts +404 -0
- package/src/lib/server-init.ts +226 -0
- package/src/lib/services/__tests__/run-query-service.test.ts +819 -0
- package/src/lib/services/run-query-service.ts +286 -0
- package/src/lib/source-discovery.ts +216 -0
- package/src/lib/utils.ts +103 -0
- package/src/lib/watcher.ts +265 -0
- package/src/test/fixtures.ts +269 -0
- package/src/test/mocks/handlers.ts +110 -0
- package/src/test/mocks/server.ts +17 -0
- package/src/test/setup.ts +200 -0
- package/src/test/test-utils.tsx +36 -0
- package/src/types/.gitkeep +0 -0
- package/src/types/breakpoint.ts +17 -0
- package/src/types/index.ts +214 -0
- package/tsconfig.json +50 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { findRunDir } from "@/lib/path-resolver";
|
|
3
|
+
import { ensureInitialized } from "@/lib/server-init";
|
|
4
|
+
import { getRunCached } from "@/lib/run-cache";
|
|
5
|
+
import { normalizeError } from "@/lib/error-handler";
|
|
6
|
+
import { createHash } from "crypto";
|
|
7
|
+
|
|
8
|
+
export const dynamic = "force-dynamic";
|
|
9
|
+
|
|
10
|
+
function isValidId(id: string): boolean {
|
|
11
|
+
return /^[a-zA-Z0-9_\-]+$/.test(id);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const DEFAULT_MAX_EVENTS = 50;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Generate a lightweight ETag from run state.
|
|
18
|
+
* Uses status, task count, event count, and updatedAt as a fingerprint
|
|
19
|
+
* to avoid re-serializing unchanged data to the client.
|
|
20
|
+
*/
|
|
21
|
+
function generateETag(run: { status: string; updatedAt: string; tasks: unknown[]; events: unknown[] }): string {
|
|
22
|
+
const fingerprint = `${run.status}:${run.tasks.length}:${run.events.length}:${run.updatedAt}`;
|
|
23
|
+
const hash = createHash("md5").update(fingerprint).digest("hex").slice(0, 16);
|
|
24
|
+
return `"${hash}"`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function GET(
|
|
28
|
+
request: Request,
|
|
29
|
+
{ params }: { params: { runId: string } }
|
|
30
|
+
) {
|
|
31
|
+
try {
|
|
32
|
+
// Ensure watcher and cache are initialized
|
|
33
|
+
await ensureInitialized();
|
|
34
|
+
|
|
35
|
+
const { runId } = params;
|
|
36
|
+
if (!isValidId(runId)) {
|
|
37
|
+
return NextResponse.json({ error: "Invalid run ID" }, { status: 400 });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const found = await findRunDir(runId);
|
|
41
|
+
if (!found) {
|
|
42
|
+
return NextResponse.json({ error: "Run not found" }, { status: 404 });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Use cached run for better performance
|
|
46
|
+
const run = await getRunCached(found.runDir, found.source, found.projectName);
|
|
47
|
+
|
|
48
|
+
// Limit events returned (keep most recent) to reduce payload size
|
|
49
|
+
const { searchParams } = new URL(request.url);
|
|
50
|
+
const maxEvents = parseInt(searchParams.get("maxEvents") || String(DEFAULT_MAX_EVENTS));
|
|
51
|
+
const totalEvents = run.events.length;
|
|
52
|
+
const limitedRun = totalEvents > maxEvents
|
|
53
|
+
? { ...run, events: run.events.slice(-maxEvents), totalEvents }
|
|
54
|
+
: { ...run, totalEvents };
|
|
55
|
+
|
|
56
|
+
// ETag support: if client sends If-None-Match and data hasn't changed,
|
|
57
|
+
// return 304 Not Modified to save bandwidth and serialization cost.
|
|
58
|
+
const etag = generateETag(limitedRun);
|
|
59
|
+
const ifNoneMatch = request.headers.get("If-None-Match");
|
|
60
|
+
if (ifNoneMatch && ifNoneMatch === etag) {
|
|
61
|
+
return new NextResponse(null, {
|
|
62
|
+
status: 304,
|
|
63
|
+
headers: {
|
|
64
|
+
ETag: etag,
|
|
65
|
+
"Cache-Control": "no-cache",
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return NextResponse.json({ run: limitedRun }, {
|
|
71
|
+
headers: {
|
|
72
|
+
"Cache-Control": "no-cache",
|
|
73
|
+
ETag: etag,
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.error("Failed to read run:", error);
|
|
78
|
+
const normalized = normalizeError(error);
|
|
79
|
+
return NextResponse.json(
|
|
80
|
+
{ error: normalized.message, code: normalized.code },
|
|
81
|
+
{ status: normalized.status }
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { findRunDir } from "@/lib/path-resolver";
|
|
3
|
+
import { parseTaskDetail } from "@/lib/parser";
|
|
4
|
+
import { normalizeError } from "@/lib/error-handler";
|
|
5
|
+
|
|
6
|
+
export const dynamic = "force-dynamic";
|
|
7
|
+
|
|
8
|
+
function isValidId(id: string): boolean {
|
|
9
|
+
return /^[a-zA-Z0-9_\-]+$/.test(id);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function GET(
|
|
13
|
+
_request: Request,
|
|
14
|
+
{ params }: { params: { runId: string; effectId: string } }
|
|
15
|
+
) {
|
|
16
|
+
try {
|
|
17
|
+
const { runId, effectId } = params;
|
|
18
|
+
if (!isValidId(runId) || !isValidId(effectId)) {
|
|
19
|
+
return NextResponse.json({ error: "Invalid ID" }, { status: 400 });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const found = await findRunDir(runId);
|
|
23
|
+
if (!found) {
|
|
24
|
+
return NextResponse.json({ error: "Run not found" }, { status: 404 });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const task = await parseTaskDetail(found.runDir, effectId);
|
|
28
|
+
|
|
29
|
+
if (!task) {
|
|
30
|
+
return NextResponse.json({ error: "Task not found" }, { status: 404 });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return NextResponse.json({ task }, {
|
|
34
|
+
headers: { "Cache-Control": "no-cache, no-store" },
|
|
35
|
+
});
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.error("Failed to read task:", error);
|
|
38
|
+
const normalized = normalizeError(error);
|
|
39
|
+
return NextResponse.json(
|
|
40
|
+
{ error: normalized.message, code: normalized.code },
|
|
41
|
+
{ status: normalized.status }
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { ensureInitialized } from "@/lib/server-init";
|
|
3
|
+
import { normalizeError } from "@/lib/error-handler";
|
|
4
|
+
import { RunQueryService, type SortMode } from "@/lib/services/run-query-service";
|
|
5
|
+
|
|
6
|
+
export const dynamic = "force-dynamic";
|
|
7
|
+
|
|
8
|
+
const NO_CACHE_HEADERS = { "Cache-Control": "no-cache, no-store" };
|
|
9
|
+
|
|
10
|
+
const service = new RunQueryService();
|
|
11
|
+
|
|
12
|
+
export async function GET(request: Request) {
|
|
13
|
+
try {
|
|
14
|
+
await ensureInitialized();
|
|
15
|
+
|
|
16
|
+
const { searchParams } = new URL(request.url);
|
|
17
|
+
const mode = searchParams.get("mode");
|
|
18
|
+
const project = searchParams.get("project");
|
|
19
|
+
const limit = parseInt(searchParams.get("limit") || "0");
|
|
20
|
+
const offset = parseInt(searchParams.get("offset") || "0");
|
|
21
|
+
const search = searchParams.get("search") || "";
|
|
22
|
+
const status = searchParams.get("status") || "";
|
|
23
|
+
const sort = (searchParams.get("sort") || "status") as SortMode;
|
|
24
|
+
|
|
25
|
+
// Mode: projects - return lightweight project summaries
|
|
26
|
+
if (mode === "projects") {
|
|
27
|
+
const data = await service.listProjects();
|
|
28
|
+
return NextResponse.json(data, { headers: NO_CACHE_HEADERS });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Mode: project - return paginated runs for a specific project
|
|
32
|
+
if (project) {
|
|
33
|
+
const data = await service.listProjectRuns({ project, limit, offset, search, status, sort });
|
|
34
|
+
return NextResponse.json(data, { headers: NO_CACHE_HEADERS });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Default: return all runs with totalCount
|
|
38
|
+
const data = await service.listAllRuns({ limit, offset, search, status, sort });
|
|
39
|
+
return NextResponse.json(data, { headers: NO_CACHE_HEADERS });
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.error("Failed to read runs:", error);
|
|
42
|
+
const normalized = normalizeError(error);
|
|
43
|
+
return NextResponse.json(
|
|
44
|
+
{ error: normalized.message, code: normalized.code },
|
|
45
|
+
{ status: normalized.status }
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { ensureInitialized, serverEvents, type BatchedRunChangedEvent } from "@/lib/server-init";
|
|
3
|
+
|
|
4
|
+
export const dynamic = "force-dynamic";
|
|
5
|
+
|
|
6
|
+
// Extract runId from a runDir path (last segment of the directory)
|
|
7
|
+
function extractRunId(runDir: string): string {
|
|
8
|
+
return path.basename(runDir);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function GET() {
|
|
12
|
+
try {
|
|
13
|
+
// Ensure watcher and cache are initialized
|
|
14
|
+
await ensureInitialized();
|
|
15
|
+
|
|
16
|
+
// Track cleanup via closure so cancel() can access it
|
|
17
|
+
let cleanup: (() => void) | null = null;
|
|
18
|
+
|
|
19
|
+
const stream = new ReadableStream({
|
|
20
|
+
start(controller) {
|
|
21
|
+
const encoder = new TextEncoder();
|
|
22
|
+
|
|
23
|
+
// Send initial connection message
|
|
24
|
+
controller.enqueue(
|
|
25
|
+
encoder.encode(
|
|
26
|
+
`data: ${JSON.stringify({ type: "connected", timestamp: new Date().toISOString() })}\n\n`
|
|
27
|
+
)
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
// Listen for run-changed events (batched by leading-edge debounce).
|
|
31
|
+
// Each event contains runIds[] and runDirs[] for targeted client refresh.
|
|
32
|
+
const runChangedListener = (event: BatchedRunChangedEvent) => {
|
|
33
|
+
try {
|
|
34
|
+
const message = {
|
|
35
|
+
type: "update",
|
|
36
|
+
runIds: event.runIds,
|
|
37
|
+
// Keep singular runId for backward compatibility (first in batch)
|
|
38
|
+
runId: event.runIds[0],
|
|
39
|
+
timestamp: new Date().toISOString(),
|
|
40
|
+
};
|
|
41
|
+
controller.enqueue(
|
|
42
|
+
encoder.encode(`data: ${JSON.stringify(message)}\n\n`)
|
|
43
|
+
);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
console.error("Failed to send run-changed event:", err);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Listen for new-run events
|
|
50
|
+
const newRunListener = (event: {
|
|
51
|
+
type: string;
|
|
52
|
+
runDir: string;
|
|
53
|
+
error?: Error;
|
|
54
|
+
}) => {
|
|
55
|
+
try {
|
|
56
|
+
const runId = extractRunId(event.runDir);
|
|
57
|
+
const message = {
|
|
58
|
+
type: "new-run",
|
|
59
|
+
runId,
|
|
60
|
+
runDir: event.runDir,
|
|
61
|
+
timestamp: new Date().toISOString(),
|
|
62
|
+
};
|
|
63
|
+
controller.enqueue(
|
|
64
|
+
encoder.encode(`data: ${JSON.stringify(message)}\n\n`)
|
|
65
|
+
);
|
|
66
|
+
} catch (err) {
|
|
67
|
+
console.error("Failed to send new-run event:", err);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Listen for watcher-error events (deduplicated in server-init)
|
|
72
|
+
// These are logged server-side but NOT forwarded as SSE data events
|
|
73
|
+
// to prevent transient filesystem errors from triggering client-side
|
|
74
|
+
// status flashes. The client will self-heal via normal polling.
|
|
75
|
+
const errorListener = (event: {
|
|
76
|
+
type: string;
|
|
77
|
+
runDir: string;
|
|
78
|
+
error?: Error;
|
|
79
|
+
}) => {
|
|
80
|
+
// Log server-side only; do not push to client SSE stream
|
|
81
|
+
console.warn(
|
|
82
|
+
"Watcher error (suppressed from SSE):",
|
|
83
|
+
event.error?.message ?? "unknown",
|
|
84
|
+
event.runDir
|
|
85
|
+
);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
serverEvents.on("run-changed", runChangedListener);
|
|
89
|
+
serverEvents.on("new-run", newRunListener);
|
|
90
|
+
serverEvents.on("watcher-error", errorListener);
|
|
91
|
+
|
|
92
|
+
// Keep-alive ping every 15 seconds
|
|
93
|
+
const pingInterval = setInterval(() => {
|
|
94
|
+
try {
|
|
95
|
+
controller.enqueue(encoder.encode(": ping\n\n"));
|
|
96
|
+
} catch (err) {
|
|
97
|
+
console.error("Failed to send ping:", err);
|
|
98
|
+
clearInterval(pingInterval);
|
|
99
|
+
}
|
|
100
|
+
}, 15000);
|
|
101
|
+
|
|
102
|
+
// Store cleanup via closure (accessible from cancel)
|
|
103
|
+
cleanup = () => {
|
|
104
|
+
clearInterval(pingInterval);
|
|
105
|
+
serverEvents.off("run-changed", runChangedListener);
|
|
106
|
+
serverEvents.off("new-run", newRunListener);
|
|
107
|
+
serverEvents.off("watcher-error", errorListener);
|
|
108
|
+
};
|
|
109
|
+
},
|
|
110
|
+
cancel() {
|
|
111
|
+
if (cleanup) {
|
|
112
|
+
cleanup();
|
|
113
|
+
cleanup = null;
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
return new Response(stream, {
|
|
119
|
+
headers: {
|
|
120
|
+
"Content-Type": "text/event-stream",
|
|
121
|
+
"Cache-Control": "no-cache, no-store",
|
|
122
|
+
Connection: "keep-alive",
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
} catch (error) {
|
|
126
|
+
console.error("Failed to initialize SSE stream:", error);
|
|
127
|
+
return new Response(
|
|
128
|
+
JSON.stringify({ error: "Failed to initialize stream" }),
|
|
129
|
+
{
|
|
130
|
+
status: 500,
|
|
131
|
+
headers: { "Content-Type": "application/json" },
|
|
132
|
+
}
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export async function GET() { return new Response("test", { status: 200 }); }
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import { readFileSync } from "fs";
|
|
4
|
+
import { resolve } from "path";
|
|
5
|
+
|
|
6
|
+
export const dynamic = "force-dynamic";
|
|
7
|
+
|
|
8
|
+
function detectVersion(command: string): string {
|
|
9
|
+
try {
|
|
10
|
+
const raw = execSync(command, { encoding: "utf-8", timeout: 3000 }).trim();
|
|
11
|
+
const match = raw.match(/(\d+\.\d+\.\d+)/);
|
|
12
|
+
return match ? match[1] : raw || "N/A";
|
|
13
|
+
} catch {
|
|
14
|
+
return "N/A";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getAppVersion(): string {
|
|
19
|
+
try {
|
|
20
|
+
const pkgPath = resolve(__dirname, "..", "..", "..", "..", "package.json");
|
|
21
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
22
|
+
return pkg.version || process.env.NEXT_PUBLIC_APP_VERSION || "unknown";
|
|
23
|
+
} catch {
|
|
24
|
+
return process.env.NEXT_PUBLIC_APP_VERSION || "unknown";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes for normal polling
|
|
29
|
+
const IDLE_THRESHOLD_MS = 60 * 1000; // 1 minute gap = user was away
|
|
30
|
+
|
|
31
|
+
let cached: { app: string; babysitter: string } | null = null;
|
|
32
|
+
let cachedAt = 0;
|
|
33
|
+
let lastRequestAt = 0;
|
|
34
|
+
|
|
35
|
+
function isCacheStale(): boolean {
|
|
36
|
+
const now = Date.now();
|
|
37
|
+
const idleGap = now - lastRequestAt;
|
|
38
|
+
const cacheAge = now - cachedAt;
|
|
39
|
+
|
|
40
|
+
// User returned after being away — likely upgraded something
|
|
41
|
+
if (lastRequestAt > 0 && idleGap > IDLE_THRESHOLD_MS) return true;
|
|
42
|
+
|
|
43
|
+
// Normal TTL expiry
|
|
44
|
+
return cacheAge > CACHE_TTL_MS;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function GET() {
|
|
48
|
+
if (!cached || isCacheStale()) {
|
|
49
|
+
cached = {
|
|
50
|
+
app: getAppVersion(),
|
|
51
|
+
babysitter: detectVersion("babysitter --version"),
|
|
52
|
+
};
|
|
53
|
+
cachedAt = Date.now();
|
|
54
|
+
}
|
|
55
|
+
lastRequestAt = Date.now();
|
|
56
|
+
return NextResponse.json(cached);
|
|
57
|
+
}
|