fleetlens 0.11.2 → 0.12.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/app/apps/web/.next/BUILD_ID +1 -1
- package/app/apps/web/.next/app-path-routes-manifest.json +3 -0
- package/app/apps/web/.next/build-manifest.json +3 -3
- package/app/apps/web/.next/prerender-manifest.json +3 -3
- package/app/apps/web/.next/routes-manifest.json +22 -0
- package/app/apps/web/.next/server/app/_global-error.html +1 -1
- package/app/apps/web/.next/server/app/_global-error.rsc +1 -1
- package/app/apps/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/app/apps/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/app/apps/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/app/apps/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/app/apps/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/app/apps/web/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/api/ask/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/digest/day/[date]/route.js +1 -1
- package/app/apps/web/.next/server/app/api/digest/day/[date]/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/digest/month/[yearMonth]/route.js +1 -1
- package/app/apps/web/.next/server/app/api/digest/month/[yearMonth]/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/digest/month-index/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/digest/week/[startDate]/route.js +1 -1
- package/app/apps/web/.next/server/app/api/digest/week/[startDate]/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/digest/week-index/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/events/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/jobs/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/pdf/[key]/route/app-paths-manifest.json +3 -0
- package/app/apps/web/.next/server/app/api/pdf/[key]/route/build-manifest.json +9 -0
- package/app/apps/web/.next/server/app/api/pdf/[key]/route/server-reference-manifest.json +4 -0
- package/app/apps/web/.next/server/app/api/pdf/[key]/route.js +10 -0
- package/app/apps/web/.next/server/app/api/pdf/[key]/route.js.map +5 -0
- package/app/apps/web/.next/server/app/api/pdf/[key]/route.js.nft.json +1 -0
- package/app/apps/web/.next/server/app/api/pdf/[key]/route_client-reference-manifest.js +3 -0
- package/app/apps/web/.next/server/app/api/runs/[runId]/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/runs/[runId]/stream/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/runs/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/settings/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/team-config/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/workflow-agent/route/app-paths-manifest.json +3 -0
- package/app/apps/web/.next/server/app/api/workflow-agent/route/build-manifest.json +9 -0
- package/app/apps/web/.next/server/app/api/workflow-agent/route/server-reference-manifest.json +4 -0
- package/app/apps/web/.next/server/app/api/workflow-agent/route.js +7 -0
- package/app/apps/web/.next/server/app/api/workflow-agent/route.js.map +5 -0
- package/app/apps/web/.next/server/app/api/workflow-agent/route.js.nft.json +1 -0
- package/app/apps/web/.next/server/app/api/workflow-agent/route_client-reference-manifest.js +3 -0
- package/app/apps/web/.next/server/app/changelog/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/changelog/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/day/[date]/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/day/[date]/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/day/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/day/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/digest/[date]/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/digest/[date]/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/digest/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/digest/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/insights/[key]/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/insights/[key]/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/insights/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/insights/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/parallelism/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/parallelism/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/print/[key]/page/app-paths-manifest.json +3 -0
- package/app/apps/web/.next/server/app/print/[key]/page/build-manifest.json +17 -0
- package/app/apps/web/.next/server/app/print/[key]/page/next-font-manifest.json +6 -0
- package/app/apps/web/.next/server/app/print/[key]/page/react-loadable-manifest.json +1 -0
- package/app/apps/web/.next/server/app/print/[key]/page/server-reference-manifest.json +4 -0
- package/app/apps/web/.next/server/app/print/[key]/page.js +18 -0
- package/app/apps/web/.next/server/app/print/[key]/page.js.map +5 -0
- package/app/apps/web/.next/server/app/print/[key]/page.js.nft.json +1 -0
- package/app/apps/web/.next/server/app/print/[key]/page_client-reference-manifest.js +3 -0
- package/app/apps/web/.next/server/app/projects/[slug]/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/projects/[slug]/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/projects/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/runs/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/runs/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/sessions/[id]/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/sessions/[id]/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/sessions/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/settings/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/team/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/team/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/usage/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/usage/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app-paths-manifest.json +3 -0
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0455vu3._.js +2 -2
- package/app/apps/web/.next/server/chunks/{[root-of-the-server]__0_8zh-i._.js → [root-of-the-server]__04j3tng._.js} +2 -2
- package/app/apps/web/.next/server/chunks/{[root-of-the-server]__0jprdks._.js → [root-of-the-server]__0cnh3k4._.js} +2 -2
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0e.f-t.._.js +3 -0
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0kc9e1n._.js +3 -0
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0y5p73y._.js +3 -0
- package/app/apps/web/.next/server/chunks/{[root-of-the-server]__0ej_p3s._.js → [root-of-the-server]__132eosx._.js} +2 -2
- package/app/apps/web/.next/server/chunks/apps_web__next-internal_server_app_api_pdf_[key]_route_actions_0intcw9.js +3 -0
- package/app/apps/web/.next/server/chunks/apps_web__next-internal_server_app_api_workflow-agent_route_actions_0gxw9tv.js +3 -0
- package/app/apps/web/.next/server/chunks/packages_entries_dist_node_0bckr7-.js +1 -1
- package/app/apps/web/.next/server/chunks/packages_parser_dist_04rh1zd._.js +1 -1
- package/app/apps/web/.next/server/chunks/packages_parser_dist_0ndsxe6._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/{[root-of-the-server]__0_xpuh7._.js → [root-of-the-server]__03y~03u._.js} +2 -2
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__04eq6z2._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__04il6m2._.js +11 -0
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0exckd4._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0o03ymy._.js +2 -2
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0rvzdnx._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0z7mg0b._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0~.f_3z._.js +12 -0
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__136-0me._.js +8 -0
- package/app/apps/web/.next/server/chunks/ssr/_0cgg5zi._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/_0jw2sm7._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/_0kajk31._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/_0w.cv64._.js +8 -0
- package/app/apps/web/.next/server/chunks/ssr/_0xu2pvc._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/apps_web_0.mu7c7._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/apps_web__next-internal_server_app_print_[key]_page_actions_0_ygul-.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/apps_web_components_01d-iwx._.js +6 -0
- package/app/apps/web/.next/server/chunks/ssr/apps_web_components_week-digest_tsx_0c9ha31._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/apps_web_lib_0af94gl._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_07orc4m.js +4 -0
- package/app/apps/web/.next/server/chunks/ssr/packages_parser_dist_12.endt._.js +1 -1
- package/app/apps/web/.next/server/edge/chunks/[root-of-the-server]__0.sx~id._.js +11 -0
- package/app/apps/web/.next/server/edge/chunks/node_modules_next_dist_esm_build_templates_edge-wrapper_0p1t1z8.js +3 -0
- package/app/apps/web/.next/server/edge/chunks/turbopack-node_modules_next_dist_esm_build_templates_edge-wrapper_0mw-sqe.js +3 -0
- package/app/apps/web/.next/server/functions-config-manifest.json +4 -1
- package/app/apps/web/.next/server/middleware-build-manifest.js +3 -3
- package/app/apps/web/.next/server/middleware-manifest.json +30 -2
- package/app/apps/web/.next/server/pages/500.html +1 -1
- package/app/apps/web/.next/server/server-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/server-reference-manifest.json +1 -1
- package/app/apps/web/.next/static/WH20Ct_NdpZTKsqI_yquG/_clientMiddlewareManifest.js +6 -0
- package/app/apps/web/.next/static/chunks/00xkqnvk4-d1-.js +10 -0
- package/app/apps/web/.next/static/chunks/0716x58e1_2c5.js +1 -0
- package/app/apps/web/.next/static/chunks/{0s4eyofh28l~u.js → 0ck03i0l4zeun.js} +1 -1
- package/app/apps/web/.next/static/chunks/0nnjldasalwdu.js +4 -0
- package/app/apps/web/.next/static/chunks/0okg13phslr1..js +4 -0
- package/app/apps/web/.next/static/chunks/0vf-3rauafbek.js +6 -0
- package/app/apps/web/.next/static/chunks/0vl27-vjob2_9.js +6 -0
- package/app/apps/web/.next/static/chunks/0yo458-7577lr.js +1 -0
- package/app/apps/web/.next/static/chunks/14bjditmmrn7l.js +1 -0
- package/app/apps/web/app/api/events/route.ts +50 -2
- package/app/apps/web/app/api/pdf/[key]/route.ts +199 -0
- package/app/apps/web/app/api/workflow-agent/route.ts +27 -0
- package/app/apps/web/app/insights/[key]/page.tsx +4 -1
- package/app/apps/web/app/layout.tsx +23 -1
- package/app/apps/web/app/page.tsx +24 -1
- package/app/apps/web/app/print/[key]/page.tsx +91 -0
- package/app/apps/web/app/projects/[slug]/page.tsx +6 -7
- package/app/apps/web/app/projects/page.tsx +2 -3
- package/app/apps/web/app/sessions/[id]/session-view.tsx +1297 -19
- package/app/apps/web/app/sessions/sessions-grid.tsx +10 -1
- package/app/apps/web/components/dashboard-view.tsx +2 -1
- package/app/apps/web/components/export-pdf-button.tsx +76 -0
- package/app/apps/web/components/month-digest-view.tsx +10 -0
- package/app/apps/web/components/week-digest-view.tsx +10 -0
- package/app/apps/web/lib/changelog-data.generated.ts +1 -1
- package/app/apps/web/lib/entries-index.ts +5 -4
- package/app/apps/web/lib/find-chrome.ts +71 -0
- package/app/apps/web/middleware.ts +22 -0
- package/app/apps/web/package.json +1 -1
- package/app/apps/web/tsconfig.tsbuildinfo +1 -1
- package/dist/daemon-worker.js +1592 -252
- package/dist/index.js +1495 -168
- package/package.json +1 -1
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0d~2~le._.js +0 -8
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0w52fbc._.js +0 -12
- package/app/apps/web/.next/server/chunks/ssr/_02s1n-x._.js +0 -8
- package/app/apps/web/.next/server/chunks/ssr/_0fjbf--._.js +0 -3
- package/app/apps/web/.next/server/chunks/ssr/apps_web_components_0ly1xes._.js +0 -6
- package/app/apps/web/.next/static/chunks/0.3mqbj24kwtc.js +0 -6
- package/app/apps/web/.next/static/chunks/0byz8fqw.exef.js +0 -1
- package/app/apps/web/.next/static/chunks/0l3ck.awv~4t7.js +0 -1
- package/app/apps/web/.next/static/chunks/0lxuac2v0_gdl.js +0 -6
- package/app/apps/web/.next/static/chunks/0m62vx92~0_0d.js +0 -1
- package/app/apps/web/.next/static/chunks/0rtba0saz74e1.js +0 -10
- package/app/apps/web/.next/static/chunks/0uev~g~qvbjv9.js +0 -4
- package/app/apps/web/.next/static/m6ibRSdAu8jD10VBA2OFH/_clientMiddlewareManifest.js +0 -1
- /package/app/apps/web/.next/static/{m6ibRSdAu8jD10VBA2OFH → WH20Ct_NdpZTKsqI_yquG}/_buildManifest.js +0 -0
- /package/app/apps/web/.next/static/{m6ibRSdAu8jD10VBA2OFH → WH20Ct_NdpZTKsqI_yquG}/_ssgManifest.js +0 -0
|
@@ -110,6 +110,44 @@ export async function GET(request: Request) {
|
|
|
110
110
|
);
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
// Workflow journals (`<session>/workflows/wf_*.json`) aren't `.jsonl`
|
|
114
|
+
// and live a level down, so the main emit() path skips them. Map the
|
|
115
|
+
// journal back to its parent session, drop the parent's cached meta /
|
|
116
|
+
// detail (so the list re-reads the spawned-agent aggregate), and emit a
|
|
117
|
+
// session-updated for the parent — the run itself re-reads journals
|
|
118
|
+
// fresh, so the detail page reflects new fan-out without a parent write.
|
|
119
|
+
function emitWorkflow(fullPath: string) {
|
|
120
|
+
const rel = path.relative(DEFAULT_ROOT, fullPath);
|
|
121
|
+
const parts = rel.split(path.sep);
|
|
122
|
+
const wfIdx = parts.lastIndexOf("workflows");
|
|
123
|
+
if (wfIdx < 2) return;
|
|
124
|
+
const projectDir = parts[0]!;
|
|
125
|
+
const sessionId = parts[wfIdx - 1]!;
|
|
126
|
+
const parentJsonl = path.join(DEFAULT_ROOT, projectDir, `${sessionId}.jsonl`);
|
|
127
|
+
const prev = pending.get(parentJsonl);
|
|
128
|
+
if (prev) clearTimeout(prev);
|
|
129
|
+
pending.set(
|
|
130
|
+
parentJsonl,
|
|
131
|
+
setTimeout(async () => {
|
|
132
|
+
pending.delete(parentJsonl);
|
|
133
|
+
if (closed) return;
|
|
134
|
+
invalidateFile(parentJsonl);
|
|
135
|
+
// Report the JOURNAL's mtime, not the parent .jsonl's. A
|
|
136
|
+
// journal-only write leaves the parent .jsonl untouched, so its
|
|
137
|
+
// (stale) mtime would lose LiveRefresher's monotonic-per-source
|
|
138
|
+
// dedup and the refresh would be silently dropped. The journal we
|
|
139
|
+
// just saw change always carries a fresh mtime.
|
|
140
|
+
let mtimeMs = Date.now();
|
|
141
|
+
try {
|
|
142
|
+
mtimeMs = (await fs.stat(fullPath)).mtimeMs;
|
|
143
|
+
} catch {
|
|
144
|
+
// journal might be gone already — fall back to now.
|
|
145
|
+
}
|
|
146
|
+
send({ type: "session-updated", sessionId, projectDir, mtimeMs });
|
|
147
|
+
}, DEBOUNCE_MS),
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
113
151
|
let watcher: ReturnType<typeof watch> | null = null;
|
|
114
152
|
try {
|
|
115
153
|
watcher = watch(
|
|
@@ -118,8 +156,18 @@ export async function GET(request: Request) {
|
|
|
118
156
|
(_eventType, filename) => {
|
|
119
157
|
if (!filename) return;
|
|
120
158
|
const fullPath = path.join(DEFAULT_ROOT, filename.toString());
|
|
121
|
-
if (
|
|
122
|
-
|
|
159
|
+
if (fullPath.endsWith(".jsonl")) {
|
|
160
|
+
emit(fullPath);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const base = path.basename(fullPath);
|
|
164
|
+
if (
|
|
165
|
+
base.startsWith("wf_") &&
|
|
166
|
+
base.endsWith(".json") &&
|
|
167
|
+
fullPath.includes(`${path.sep}workflows${path.sep}`)
|
|
168
|
+
) {
|
|
169
|
+
emitWorkflow(fullPath);
|
|
170
|
+
}
|
|
123
171
|
},
|
|
124
172
|
);
|
|
125
173
|
} catch (e) {
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { promises as fs } from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
6
|
+
import {
|
|
7
|
+
readWeekDigest, readMonthDigest,
|
|
8
|
+
getCurrentWeekDigestFromCache, getCurrentMonthDigestFromCache,
|
|
9
|
+
} from "@claude-lens/entries/node";
|
|
10
|
+
import { currentWeekMonday, currentYearMonth } from "@/lib/entries";
|
|
11
|
+
import { findChrome } from "@/lib/find-chrome";
|
|
12
|
+
|
|
13
|
+
export const runtime = "nodejs";
|
|
14
|
+
export const dynamic = "force-dynamic";
|
|
15
|
+
|
|
16
|
+
const KEY = /^(week-(\d{4}-\d{2}-\d{2})|month-(\d{4}-\d{2}))$/;
|
|
17
|
+
|
|
18
|
+
const RENDER_TIMEOUT_MS = 25_000;
|
|
19
|
+
const STABILITY_MS = 600;
|
|
20
|
+
|
|
21
|
+
export async function POST(
|
|
22
|
+
req: Request,
|
|
23
|
+
{ params }: { params: Promise<{ key: string }> },
|
|
24
|
+
) {
|
|
25
|
+
const { key } = await params;
|
|
26
|
+
const m = KEY.exec(key);
|
|
27
|
+
if (!m) {
|
|
28
|
+
return jsonError(400, `Invalid key: ${key}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Pre-check the digest exists in whichever source the /print page would
|
|
32
|
+
// read from. Without this, Chrome happily captures Next.js's 404 page as
|
|
33
|
+
// a "successful" PDF, and current-period digests that have aged out of
|
|
34
|
+
// the in-memory TTL would yield a broken download instead of a clear
|
|
35
|
+
// error.
|
|
36
|
+
if (!digestExists(m)) {
|
|
37
|
+
return jsonError(
|
|
38
|
+
404,
|
|
39
|
+
"Digest not found. If this is the current week or month, the in-memory cache may have expired — reload the page and re-generate.",
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const chrome = await findChrome();
|
|
44
|
+
if (!chrome) {
|
|
45
|
+
return jsonError(
|
|
46
|
+
500,
|
|
47
|
+
"Could not find Chrome / Chromium. Install Google Chrome, or set FLEETLENS_CHROME_PATH.",
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const origin = new URL(req.url).origin;
|
|
52
|
+
// Headless Chrome won't carry the user's session cookies, so forward the
|
|
53
|
+
// current theme via query param. Middleware turns it into a header that
|
|
54
|
+
// the root layout applies to <html data-theme>.
|
|
55
|
+
const theme = parseThemeCookie(req.headers.get("cookie"));
|
|
56
|
+
const targetUrl = theme
|
|
57
|
+
? `${origin}/print/${key}?theme=${theme}`
|
|
58
|
+
: `${origin}/print/${key}`;
|
|
59
|
+
|
|
60
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "fleetlens-pdf-"));
|
|
61
|
+
const tmpPath = path.join(tmpDir, "out.pdf");
|
|
62
|
+
const profileDir = path.join(tmpDir, "profile");
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
await renderToPdf(chrome, targetUrl, tmpPath, profileDir);
|
|
66
|
+
const bytes = await fs.readFile(tmpPath);
|
|
67
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
68
|
+
return new Response(new Uint8Array(bytes), {
|
|
69
|
+
status: 200,
|
|
70
|
+
headers: {
|
|
71
|
+
"Content-Type": "application/pdf",
|
|
72
|
+
"Content-Disposition": `attachment; filename="fleetlens-${key}-${today}.pdf"`,
|
|
73
|
+
"Cache-Control": "no-store",
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
} catch (err) {
|
|
77
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
78
|
+
return jsonError(500, `PDF render failed: ${message}`);
|
|
79
|
+
} finally {
|
|
80
|
+
fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function digestExists(match: RegExpExecArray): boolean {
|
|
85
|
+
const monday = match[2];
|
|
86
|
+
const yearMonth = match[3];
|
|
87
|
+
if (monday) {
|
|
88
|
+
if (monday === currentWeekMonday()) {
|
|
89
|
+
return !!(getCurrentWeekDigestFromCache(monday, Date.now()) ?? readWeekDigest(monday));
|
|
90
|
+
}
|
|
91
|
+
return !!readWeekDigest(monday);
|
|
92
|
+
}
|
|
93
|
+
if (yearMonth) {
|
|
94
|
+
if (yearMonth === currentYearMonth()) {
|
|
95
|
+
return !!(getCurrentMonthDigestFromCache(yearMonth, Date.now()) ?? readMonthDigest(yearMonth));
|
|
96
|
+
}
|
|
97
|
+
return !!readMonthDigest(yearMonth);
|
|
98
|
+
}
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Chrome on macOS produces the PDF in 2–3s but the parent process keeps
|
|
103
|
+
// running (GoogleUpdater helpers prevent a clean exit). So instead of waiting
|
|
104
|
+
// for the process to close, we poll the output file for stability and kill
|
|
105
|
+
// Chrome once it's stable.
|
|
106
|
+
async function renderToPdf(
|
|
107
|
+
chrome: string,
|
|
108
|
+
url: string,
|
|
109
|
+
out: string,
|
|
110
|
+
profileDir: string,
|
|
111
|
+
): Promise<void> {
|
|
112
|
+
// --no-sandbox is required on Linux containers / CI without user namespaces
|
|
113
|
+
// (Chrome can't drop privileges otherwise), but it's a security weakening
|
|
114
|
+
// that's not needed on macOS or Windows desktop Chrome. Opt-in via env on
|
|
115
|
+
// the systems that need it.
|
|
116
|
+
const noSandbox = process.env.FLEETLENS_CHROME_NO_SANDBOX === "1"
|
|
117
|
+
|| process.platform === "linux";
|
|
118
|
+
const args = [
|
|
119
|
+
"--headless=new",
|
|
120
|
+
"--disable-gpu",
|
|
121
|
+
...(noSandbox ? ["--no-sandbox"] : []),
|
|
122
|
+
"--no-pdf-header-footer",
|
|
123
|
+
"--hide-scrollbars",
|
|
124
|
+
"--no-first-run",
|
|
125
|
+
"--no-default-browser-check",
|
|
126
|
+
"--disable-background-networking",
|
|
127
|
+
"--disable-component-update",
|
|
128
|
+
`--user-data-dir=${profileDir}`,
|
|
129
|
+
`--print-to-pdf=${out}`,
|
|
130
|
+
url,
|
|
131
|
+
];
|
|
132
|
+
|
|
133
|
+
let earlyExit: number | null = null;
|
|
134
|
+
const proc = spawn(chrome, args, {
|
|
135
|
+
stdio: "ignore",
|
|
136
|
+
detached: true,
|
|
137
|
+
});
|
|
138
|
+
proc.on("exit", (code) => { earlyExit = code; });
|
|
139
|
+
proc.unref();
|
|
140
|
+
|
|
141
|
+
const deadline = Date.now() + RENDER_TIMEOUT_MS;
|
|
142
|
+
let lastSize = -1;
|
|
143
|
+
let stableSince = 0;
|
|
144
|
+
let lastErr: NodeJS.ErrnoException | null = null;
|
|
145
|
+
|
|
146
|
+
while (Date.now() < deadline) {
|
|
147
|
+
await delay(150);
|
|
148
|
+
if (earlyExit !== null && earlyExit !== 0) {
|
|
149
|
+
throw new Error(`Chrome exited early with code ${earlyExit}`);
|
|
150
|
+
}
|
|
151
|
+
let size: number;
|
|
152
|
+
try {
|
|
153
|
+
size = (await fs.stat(out)).size;
|
|
154
|
+
} catch (e) {
|
|
155
|
+
lastErr = e as NodeJS.ErrnoException;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (size <= 0) continue;
|
|
159
|
+
if (size === lastSize) {
|
|
160
|
+
if (!stableSince) stableSince = Date.now();
|
|
161
|
+
if (Date.now() - stableSince >= STABILITY_MS) {
|
|
162
|
+
killGroup(proc.pid);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
} else {
|
|
166
|
+
lastSize = size;
|
|
167
|
+
stableSince = 0;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
killGroup(proc.pid);
|
|
172
|
+
throw new Error(
|
|
173
|
+
`Chrome render timed out after ${RENDER_TIMEOUT_MS / 1000}s` +
|
|
174
|
+
(lastErr ? ` (no PDF file produced: ${lastErr.code})` : ""),
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// On Unix the negative PID kills the whole process group (Chrome spawns
|
|
179
|
+
// helper processes for GPU, network, renderer). On Windows process.kill(-pid)
|
|
180
|
+
// throws ESRCH — the swallowed exception is intentional, and the fallback
|
|
181
|
+
// kill below at least reaps the root Chrome process.
|
|
182
|
+
function killGroup(pid: number | undefined): void {
|
|
183
|
+
if (!pid) return;
|
|
184
|
+
try { process.kill(-pid, "SIGKILL"); } catch { /* group already gone, or Windows */ }
|
|
185
|
+
try { process.kill(pid, "SIGKILL"); } catch { /* already gone */ }
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function parseThemeCookie(header: string | null): "light" | "dark" | null {
|
|
189
|
+
if (!header) return null;
|
|
190
|
+
const m = /(?:^|;\s*)claude-lens-theme=(light|dark)/.exec(header);
|
|
191
|
+
return m ? (m[1] as "light" | "dark") : null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function jsonError(status: number, message: string): Response {
|
|
195
|
+
return new Response(JSON.stringify({ error: message }), {
|
|
196
|
+
status,
|
|
197
|
+
headers: { "Content-Type": "application/json" },
|
|
198
|
+
});
|
|
199
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* On-demand loader for a single workflow-spawned agent's full transcript —
|
|
3
|
+
* the ordered step list, full prompt, and final result. Lazy-fetched by the
|
|
4
|
+
* Workflows tab when an agent row is expanded, so a 200-agent session's detail
|
|
5
|
+
* page never parses 200 transcripts up front.
|
|
6
|
+
*
|
|
7
|
+
* GET /api/workflow-agent?session=<id>&run=<runId>&agent=<agentId>
|
|
8
|
+
*/
|
|
9
|
+
import { loadWorkflowAgentDetail } from "@claude-lens/parser/fs";
|
|
10
|
+
|
|
11
|
+
export const runtime = "nodejs";
|
|
12
|
+
export const dynamic = "force-dynamic";
|
|
13
|
+
|
|
14
|
+
export async function GET(req: Request): Promise<Response> {
|
|
15
|
+
const url = new URL(req.url);
|
|
16
|
+
const session = url.searchParams.get("session");
|
|
17
|
+
const run = url.searchParams.get("run");
|
|
18
|
+
const agent = url.searchParams.get("agent");
|
|
19
|
+
if (!session || !run || !agent) {
|
|
20
|
+
return Response.json({ error: "session, run and agent are required" }, { status: 400 });
|
|
21
|
+
}
|
|
22
|
+
const detail = await loadWorkflowAgentDetail(session, run, agent);
|
|
23
|
+
if (!detail) {
|
|
24
|
+
return Response.json({ error: "not found" }, { status: 404 });
|
|
25
|
+
}
|
|
26
|
+
return Response.json(detail);
|
|
27
|
+
}
|
|
@@ -58,8 +58,11 @@ export default async function SavedInsightPage({
|
|
|
58
58
|
const monthMatch = MONTH_KEY.exec(key);
|
|
59
59
|
if (monthMatch) {
|
|
60
60
|
const yearMonth = monthMatch[1]!;
|
|
61
|
+
// Same in-memory-then-disk fallback as the week branch above:
|
|
62
|
+
// force-generated current-month digests get persisted by
|
|
63
|
+
// digest-month-pipeline, so this read survives the 10-min TTL.
|
|
61
64
|
const cached = yearMonth === currentYearMonth()
|
|
62
|
-
? getCurrentMonthDigestFromCache(yearMonth, Date.now())
|
|
65
|
+
? (getCurrentMonthDigestFromCache(yearMonth, Date.now()) ?? readMonthDigest(yearMonth))
|
|
63
66
|
: readMonthDigest(yearMonth);
|
|
64
67
|
|
|
65
68
|
const prev = shiftYearMonth(yearMonth, -1);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Metadata } from "next";
|
|
2
|
-
import { cookies } from "next/headers";
|
|
2
|
+
import { cookies, headers } from "next/headers";
|
|
3
3
|
import { Sidebar } from "@/components/sidebar";
|
|
4
4
|
import { LiveRefresher } from "@/components/live-refresher";
|
|
5
5
|
import {
|
|
@@ -25,6 +25,28 @@ export const metadata: Metadata = {
|
|
|
25
25
|
export const dynamic = "force-dynamic";
|
|
26
26
|
|
|
27
27
|
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
|
28
|
+
// /print/* is a stripped layout used as the headless-render target for PDF
|
|
29
|
+
// export. Skip the sidebar, live-sessions widget, and job-queue widget so
|
|
30
|
+
// the captured PDF contains only the digest content.
|
|
31
|
+
const reqHeaders = await headers();
|
|
32
|
+
const pathname = reqHeaders.get("x-fleetlens-pathname") ?? "";
|
|
33
|
+
if (pathname.startsWith("/print/")) {
|
|
34
|
+
// Theme priority: header set by middleware from ?theme= (forwarded by
|
|
35
|
+
// the PDF API), then cookie (for direct browser visits), then dark.
|
|
36
|
+
const headerTheme = reqHeaders.get("x-fleetlens-theme");
|
|
37
|
+
const cookieStore = await cookies();
|
|
38
|
+
const cookieTheme = cookieStore.get("claude-lens-theme")?.value;
|
|
39
|
+
const theme =
|
|
40
|
+
headerTheme === "light" || headerTheme === "dark" ? headerTheme :
|
|
41
|
+
cookieTheme === "light" || cookieTheme === "dark" ? cookieTheme :
|
|
42
|
+
"dark";
|
|
43
|
+
return (
|
|
44
|
+
<html lang="en" data-theme={theme} suppressHydrationWarning>
|
|
45
|
+
<body>{children}</body>
|
|
46
|
+
</html>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
28
50
|
const [projects, allFiles, allSessions, entriesIndex] = await Promise.all([
|
|
29
51
|
listProjects(),
|
|
30
52
|
walkJsonlFiles(),
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
9
|
groupByProject,
|
|
10
|
+
dailyActivity,
|
|
11
|
+
toLocalDay,
|
|
10
12
|
type AgentKind,
|
|
11
13
|
agentMetadata,
|
|
12
14
|
type SessionMeta,
|
|
@@ -53,6 +55,23 @@ export default async function DashboardHome({
|
|
|
53
55
|
const sessions = filterByRange(allSessions, cutoff);
|
|
54
56
|
const conn = readTeamConnection();
|
|
55
57
|
|
|
58
|
+
// Headline "Agent time" answers "agent time spent within the selected
|
|
59
|
+
// window". Summing whole-session airTimeMs over start-day-filtered sessions
|
|
60
|
+
// diverged from the team rollups at the window edges — it dropped pre-window
|
|
61
|
+
// sessions that bled into the window and kept the out-of-window tail of
|
|
62
|
+
// in-window sessions. Compute it the way the team daily_rollups / weekly
|
|
63
|
+
// digest do: dailyActivity's per-calendar-day clipped agent time over ALL
|
|
64
|
+
// sessions, kept to the window's days. For "all" this equals the whole-set
|
|
65
|
+
// total, so it's a no-op there.
|
|
66
|
+
const windowBuckets = (() => {
|
|
67
|
+
const buckets = dailyActivity(allSessions);
|
|
68
|
+
if (cutoff === undefined) return buckets;
|
|
69
|
+
const cutoffDay = toLocalDay(cutoff);
|
|
70
|
+
return buckets.filter((b) => b.date >= cutoffDay);
|
|
71
|
+
})();
|
|
72
|
+
const windowAgentMs = windowBuckets.reduce((sum, b) => sum + b.airTimeMs, 0);
|
|
73
|
+
const windowActiveDays = windowBuckets.filter((b) => b.airTimeMs > 0).length;
|
|
74
|
+
|
|
56
75
|
// Heatmap day summaries — outcome from index, headline + helpfulness from cached digests
|
|
57
76
|
const daySummaries = new Map<string, DaySummary>();
|
|
58
77
|
for (const [date, outcome] of entriesIndex.dayOutcome) {
|
|
@@ -143,7 +162,11 @@ export default async function DashboardHome({
|
|
|
143
162
|
is closed and the digest is final. */}
|
|
144
163
|
<YesterdayHero />
|
|
145
164
|
|
|
146
|
-
<DashboardView
|
|
165
|
+
<DashboardView
|
|
166
|
+
sessions={sessions}
|
|
167
|
+
daySummaries={daySummaries}
|
|
168
|
+
override={{ activeTimeMs: windowAgentMs, activeDayCount: windowActiveDays }}
|
|
169
|
+
/>
|
|
147
170
|
|
|
148
171
|
{/* Top projects + Recent sessions + Recent days */}
|
|
149
172
|
<section
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { notFound } from "next/navigation";
|
|
2
|
+
import {
|
|
3
|
+
readWeekDigest, readMonthDigest,
|
|
4
|
+
getCurrentWeekDigestFromCache, getCurrentMonthDigestFromCache,
|
|
5
|
+
} from "@claude-lens/entries/node";
|
|
6
|
+
import { WeekDigest } from "@/components/week-digest";
|
|
7
|
+
import { MonthDigest } from "@/components/month-digest";
|
|
8
|
+
import { currentWeekMonday, currentYearMonth } from "@/lib/entries";
|
|
9
|
+
|
|
10
|
+
export const dynamic = "force-dynamic";
|
|
11
|
+
export const runtime = "nodejs";
|
|
12
|
+
|
|
13
|
+
const WEEK_KEY = /^week-(\d{4}-\d{2}-\d{2})$/;
|
|
14
|
+
const MONTH_KEY = /^month-(\d{4}-\d{2})$/;
|
|
15
|
+
|
|
16
|
+
// Headless-render target for the PDF export. Sits on top of the root layout
|
|
17
|
+
// via a position:fixed overlay (z-index above the floating LiveSessions /
|
|
18
|
+
// JobQueue widgets, which use 99/100). No actions, no auto-fire — the digest
|
|
19
|
+
// must already exist on disk (the API route fails fast if it doesn't).
|
|
20
|
+
export default async function PrintPage({
|
|
21
|
+
params,
|
|
22
|
+
}: { params: Promise<{ key: string }> }) {
|
|
23
|
+
const { key } = await params;
|
|
24
|
+
|
|
25
|
+
const weekMatch = WEEK_KEY.exec(key);
|
|
26
|
+
if (weekMatch) {
|
|
27
|
+
const monday = weekMatch[1]!;
|
|
28
|
+
const digest = monday === currentWeekMonday()
|
|
29
|
+
? (getCurrentWeekDigestFromCache(monday, Date.now()) ?? readWeekDigest(monday))
|
|
30
|
+
: readWeekDigest(monday);
|
|
31
|
+
if (!digest) notFound();
|
|
32
|
+
const prior = readWeekDigest(shiftMonday(monday, -7));
|
|
33
|
+
return (
|
|
34
|
+
<PrintShell>
|
|
35
|
+
<WeekDigest digest={digest} aiEnabled={true} priorDigest={prior} />
|
|
36
|
+
</PrintShell>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const monthMatch = MONTH_KEY.exec(key);
|
|
41
|
+
if (monthMatch) {
|
|
42
|
+
const yearMonth = monthMatch[1]!;
|
|
43
|
+
// Match the week branch: try in-memory cache for the current month, then
|
|
44
|
+
// fall back to disk. Force-generated current-month digests get persisted
|
|
45
|
+
// by digest-month-pipeline so PDF export survives the 10-min TTL.
|
|
46
|
+
const digest = yearMonth === currentYearMonth()
|
|
47
|
+
? (getCurrentMonthDigestFromCache(yearMonth, Date.now()) ?? readMonthDigest(yearMonth))
|
|
48
|
+
: readMonthDigest(yearMonth);
|
|
49
|
+
if (!digest) notFound();
|
|
50
|
+
return (
|
|
51
|
+
<PrintShell>
|
|
52
|
+
<MonthDigest digest={digest} aiEnabled={true} />
|
|
53
|
+
</PrintShell>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
notFound();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function PrintShell({ children }: { children: React.ReactNode }) {
|
|
61
|
+
return (
|
|
62
|
+
<>
|
|
63
|
+
<style dangerouslySetInnerHTML={{ __html: PRINT_CSS }} />
|
|
64
|
+
{children}
|
|
65
|
+
</>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Reset html/body to the digest's own background so the PDF page bleeds
|
|
70
|
+
// edge-to-edge in the same color the digest renders against. @page disables
|
|
71
|
+
// the printer header/margin defaults; Chrome's --print-to-pdf paginates A4.
|
|
72
|
+
// --background is theme-aware (dark / light tokens in globals.css); never
|
|
73
|
+
// hardcode a fallback that would break the light theme.
|
|
74
|
+
const PRINT_CSS = `
|
|
75
|
+
@page { size: A4; margin: 8mm; }
|
|
76
|
+
html, body {
|
|
77
|
+
background: var(--background) !important;
|
|
78
|
+
color: var(--af-text);
|
|
79
|
+
margin: 0 !important;
|
|
80
|
+
padding: 0 !important;
|
|
81
|
+
}
|
|
82
|
+
`;
|
|
83
|
+
|
|
84
|
+
function shiftMonday(monday: string, days: number): string {
|
|
85
|
+
const d = new Date(`${monday}T00:00:00`);
|
|
86
|
+
d.setDate(d.getDate() + days);
|
|
87
|
+
const y = d.getFullYear();
|
|
88
|
+
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
89
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
90
|
+
return `${y}-${m}-${day}`;
|
|
91
|
+
}
|
|
@@ -2,7 +2,7 @@ import { notFound } from "next/navigation";
|
|
|
2
2
|
import Link from "next/link";
|
|
3
3
|
import { listSessions, getSession } from "@/lib/data";
|
|
4
4
|
import {
|
|
5
|
-
|
|
5
|
+
projectRepoName,
|
|
6
6
|
detectPrMarkers,
|
|
7
7
|
sessionAirTimeMs,
|
|
8
8
|
} from "@claude-lens/parser";
|
|
@@ -39,18 +39,17 @@ export const dynamic = "force-dynamic";
|
|
|
39
39
|
export default async function ProjectDetail({ params }: { params: Promise<{ slug: string }> }) {
|
|
40
40
|
const { slug } = await params;
|
|
41
41
|
// Slugs are URL-encoded canonical project names (e.g.
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
// page naturally shows all parallel worktree activity in one view.
|
|
42
|
+
// Slugs are repo names (e.g. `claude-lens`). Match any session in that repo,
|
|
43
|
+
// which folds the parent repo, every `/.worktrees/<name>` subdir, and the
|
|
44
|
+
// same repo reached via Conductor / Superset into one view.
|
|
46
45
|
const decodedCanonical = decodeURIComponent(slug);
|
|
47
46
|
const all = await listSessions();
|
|
48
47
|
const projectSessions = all.filter(
|
|
49
|
-
(s) =>
|
|
48
|
+
(s) => projectRepoName(s.projectName) === decodedCanonical,
|
|
50
49
|
);
|
|
51
50
|
if (projectSessions.length === 0) return notFound();
|
|
52
51
|
|
|
53
|
-
//
|
|
52
|
+
// Display the repo name.
|
|
54
53
|
const projectName = decodedCanonical;
|
|
55
54
|
|
|
56
55
|
// PR detection is per-session — it needs the event stream, so load the
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { listSessions } from "@/lib/data";
|
|
2
|
-
import {
|
|
2
|
+
import { projectRepoName, groupByProject } from "@claude-lens/parser";
|
|
3
3
|
import type { DayOutcome } from "@claude-lens/entries";
|
|
4
4
|
import { ProjectsView, type ProjectRow } from "./projects-view";
|
|
5
5
|
import { buildEntriesIndex } from "@/lib/entries-index";
|
|
@@ -26,8 +26,7 @@ export default async function ProjectsPage() {
|
|
|
26
26
|
const days = lastNDays(7);
|
|
27
27
|
|
|
28
28
|
const rows: ProjectRow[] = projects.map((p) => {
|
|
29
|
-
const
|
|
30
|
-
const projectEntries = index.byProject.get(canonical) ?? [];
|
|
29
|
+
const projectEntries = index.byProject.get(projectRepoName(p.projectName)) ?? [];
|
|
31
30
|
const byDay = new Map<string, typeof projectEntries>();
|
|
32
31
|
for (const e of projectEntries) {
|
|
33
32
|
const arr = byDay.get(e.local_day);
|