fleetlens 0.11.2 → 0.12.1

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 (214) hide show
  1. package/app/apps/web/.next/BUILD_ID +1 -1
  2. package/app/apps/web/.next/app-path-routes-manifest.json +3 -0
  3. package/app/apps/web/.next/build-manifest.json +3 -3
  4. package/app/apps/web/.next/prerender-manifest.json +3 -3
  5. package/app/apps/web/.next/routes-manifest.json +22 -0
  6. package/app/apps/web/.next/server/app/_global-error.html +1 -1
  7. package/app/apps/web/.next/server/app/_global-error.rsc +1 -1
  8. package/app/apps/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  9. package/app/apps/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  10. package/app/apps/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  11. package/app/apps/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  12. package/app/apps/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/app/apps/web/.next/server/app/_not-found/page.js +2 -1
  14. package/app/apps/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  15. package/app/apps/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  16. package/app/apps/web/.next/server/app/api/ask/route.js +1 -1
  17. package/app/apps/web/.next/server/app/api/ask/route.js.nft.json +1 -1
  18. package/app/apps/web/.next/server/app/api/digest/day/[date]/route.js +2 -2
  19. package/app/apps/web/.next/server/app/api/digest/day/[date]/route.js.nft.json +1 -1
  20. package/app/apps/web/.next/server/app/api/digest/month/[yearMonth]/route.js +2 -2
  21. package/app/apps/web/.next/server/app/api/digest/month/[yearMonth]/route.js.nft.json +1 -1
  22. package/app/apps/web/.next/server/app/api/digest/month-index/route.js +1 -1
  23. package/app/apps/web/.next/server/app/api/digest/month-index/route.js.nft.json +1 -1
  24. package/app/apps/web/.next/server/app/api/digest/week/[startDate]/route.js +2 -2
  25. package/app/apps/web/.next/server/app/api/digest/week/[startDate]/route.js.nft.json +1 -1
  26. package/app/apps/web/.next/server/app/api/digest/week-index/route.js +1 -1
  27. package/app/apps/web/.next/server/app/api/digest/week-index/route.js.nft.json +1 -1
  28. package/app/apps/web/.next/server/app/api/events/route.js +1 -1
  29. package/app/apps/web/.next/server/app/api/events/route.js.nft.json +1 -1
  30. package/app/apps/web/.next/server/app/api/jobs/route.js +1 -1
  31. package/app/apps/web/.next/server/app/api/jobs/route.js.nft.json +1 -1
  32. package/app/apps/web/.next/server/app/api/pdf/[key]/route/app-paths-manifest.json +3 -0
  33. package/app/apps/web/.next/server/app/api/pdf/[key]/route/build-manifest.json +9 -0
  34. package/app/apps/web/.next/server/app/api/pdf/[key]/route/server-reference-manifest.json +4 -0
  35. package/app/apps/web/.next/server/app/api/pdf/[key]/route.js +10 -0
  36. package/app/apps/web/.next/server/app/api/pdf/[key]/route.js.map +5 -0
  37. package/app/apps/web/.next/server/app/api/pdf/[key]/route.js.nft.json +1 -0
  38. package/app/apps/web/.next/server/app/api/pdf/[key]/route_client-reference-manifest.js +3 -0
  39. package/app/apps/web/.next/server/app/api/runs/[runId]/route.js +1 -1
  40. package/app/apps/web/.next/server/app/api/runs/[runId]/route.js.nft.json +1 -1
  41. package/app/apps/web/.next/server/app/api/runs/[runId]/stream/route.js +1 -1
  42. package/app/apps/web/.next/server/app/api/runs/[runId]/stream/route.js.nft.json +1 -1
  43. package/app/apps/web/.next/server/app/api/runs/route.js +1 -1
  44. package/app/apps/web/.next/server/app/api/runs/route.js.nft.json +1 -1
  45. package/app/apps/web/.next/server/app/api/settings/route.js +1 -1
  46. package/app/apps/web/.next/server/app/api/settings/route.js.nft.json +1 -1
  47. package/app/apps/web/.next/server/app/api/team-config/route.js +1 -1
  48. package/app/apps/web/.next/server/app/api/team-config/route.js.nft.json +1 -1
  49. package/app/apps/web/.next/server/app/api/workflow-agent/route/app-paths-manifest.json +3 -0
  50. package/app/apps/web/.next/server/app/api/workflow-agent/route/build-manifest.json +9 -0
  51. package/app/apps/web/.next/server/app/api/workflow-agent/route/server-reference-manifest.json +4 -0
  52. package/app/apps/web/.next/server/app/api/workflow-agent/route.js +7 -0
  53. package/app/apps/web/.next/server/app/api/workflow-agent/route.js.map +5 -0
  54. package/app/apps/web/.next/server/app/api/workflow-agent/route.js.nft.json +1 -0
  55. package/app/apps/web/.next/server/app/api/workflow-agent/route_client-reference-manifest.js +3 -0
  56. package/app/apps/web/.next/server/app/changelog/page.js +2 -1
  57. package/app/apps/web/.next/server/app/changelog/page.js.nft.json +1 -1
  58. package/app/apps/web/.next/server/app/changelog/page_client-reference-manifest.js +1 -1
  59. package/app/apps/web/.next/server/app/day/[date]/page.js +2 -1
  60. package/app/apps/web/.next/server/app/day/[date]/page.js.nft.json +1 -1
  61. package/app/apps/web/.next/server/app/day/[date]/page_client-reference-manifest.js +1 -1
  62. package/app/apps/web/.next/server/app/day/page.js +2 -1
  63. package/app/apps/web/.next/server/app/day/page.js.nft.json +1 -1
  64. package/app/apps/web/.next/server/app/day/page_client-reference-manifest.js +1 -1
  65. package/app/apps/web/.next/server/app/digest/[date]/page.js +2 -1
  66. package/app/apps/web/.next/server/app/digest/[date]/page.js.nft.json +1 -1
  67. package/app/apps/web/.next/server/app/digest/[date]/page_client-reference-manifest.js +1 -1
  68. package/app/apps/web/.next/server/app/digest/page.js +2 -1
  69. package/app/apps/web/.next/server/app/digest/page.js.nft.json +1 -1
  70. package/app/apps/web/.next/server/app/digest/page_client-reference-manifest.js +1 -1
  71. package/app/apps/web/.next/server/app/insights/[key]/page.js +2 -1
  72. package/app/apps/web/.next/server/app/insights/[key]/page.js.nft.json +1 -1
  73. package/app/apps/web/.next/server/app/insights/[key]/page_client-reference-manifest.js +1 -1
  74. package/app/apps/web/.next/server/app/insights/page.js +2 -1
  75. package/app/apps/web/.next/server/app/insights/page.js.nft.json +1 -1
  76. package/app/apps/web/.next/server/app/insights/page_client-reference-manifest.js +1 -1
  77. package/app/apps/web/.next/server/app/page.js +2 -1
  78. package/app/apps/web/.next/server/app/page.js.nft.json +1 -1
  79. package/app/apps/web/.next/server/app/page_client-reference-manifest.js +1 -1
  80. package/app/apps/web/.next/server/app/parallelism/page.js +2 -1
  81. package/app/apps/web/.next/server/app/parallelism/page.js.nft.json +1 -1
  82. package/app/apps/web/.next/server/app/parallelism/page_client-reference-manifest.js +1 -1
  83. package/app/apps/web/.next/server/app/print/[key]/page/app-paths-manifest.json +3 -0
  84. package/app/apps/web/.next/server/app/print/[key]/page/build-manifest.json +17 -0
  85. package/app/apps/web/.next/server/app/print/[key]/page/next-font-manifest.json +6 -0
  86. package/app/apps/web/.next/server/app/print/[key]/page/react-loadable-manifest.json +1 -0
  87. package/app/apps/web/.next/server/app/print/[key]/page/server-reference-manifest.json +4 -0
  88. package/app/apps/web/.next/server/app/print/[key]/page.js +19 -0
  89. package/app/apps/web/.next/server/app/print/[key]/page.js.map +5 -0
  90. package/app/apps/web/.next/server/app/print/[key]/page.js.nft.json +1 -0
  91. package/app/apps/web/.next/server/app/print/[key]/page_client-reference-manifest.js +3 -0
  92. package/app/apps/web/.next/server/app/projects/[slug]/page.js +2 -1
  93. package/app/apps/web/.next/server/app/projects/[slug]/page.js.nft.json +1 -1
  94. package/app/apps/web/.next/server/app/projects/[slug]/page_client-reference-manifest.js +1 -1
  95. package/app/apps/web/.next/server/app/projects/page.js +2 -1
  96. package/app/apps/web/.next/server/app/projects/page.js.nft.json +1 -1
  97. package/app/apps/web/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  98. package/app/apps/web/.next/server/app/runs/page.js +2 -1
  99. package/app/apps/web/.next/server/app/runs/page.js.nft.json +1 -1
  100. package/app/apps/web/.next/server/app/runs/page_client-reference-manifest.js +1 -1
  101. package/app/apps/web/.next/server/app/sessions/[id]/page.js +2 -1
  102. package/app/apps/web/.next/server/app/sessions/[id]/page.js.nft.json +1 -1
  103. package/app/apps/web/.next/server/app/sessions/[id]/page_client-reference-manifest.js +1 -1
  104. package/app/apps/web/.next/server/app/sessions/page.js +2 -1
  105. package/app/apps/web/.next/server/app/sessions/page.js.nft.json +1 -1
  106. package/app/apps/web/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
  107. package/app/apps/web/.next/server/app/settings/page.js +2 -1
  108. package/app/apps/web/.next/server/app/settings/page.js.nft.json +1 -1
  109. package/app/apps/web/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  110. package/app/apps/web/.next/server/app/team/page.js +2 -1
  111. package/app/apps/web/.next/server/app/team/page.js.nft.json +1 -1
  112. package/app/apps/web/.next/server/app/team/page_client-reference-manifest.js +1 -1
  113. package/app/apps/web/.next/server/app/usage/page.js +2 -1
  114. package/app/apps/web/.next/server/app/usage/page.js.nft.json +1 -1
  115. package/app/apps/web/.next/server/app/usage/page_client-reference-manifest.js +1 -1
  116. package/app/apps/web/.next/server/app-paths-manifest.json +3 -0
  117. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0455vu3._.js +2 -2
  118. package/app/apps/web/.next/server/chunks/{[root-of-the-server]__0_8zh-i._.js → [root-of-the-server]__04j3tng._.js} +2 -2
  119. package/app/apps/web/.next/server/chunks/{[root-of-the-server]__0jprdks._.js → [root-of-the-server]__0cnh3k4._.js} +2 -2
  120. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0e.f-t.._.js +3 -0
  121. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0kc9e1n._.js +3 -0
  122. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0y5p73y._.js +3 -0
  123. package/app/apps/web/.next/server/chunks/{[root-of-the-server]__0ej_p3s._.js → [root-of-the-server]__132eosx._.js} +2 -2
  124. package/app/apps/web/.next/server/chunks/apps_web__next-internal_server_app_api_pdf_[key]_route_actions_0intcw9.js +3 -0
  125. package/app/apps/web/.next/server/chunks/apps_web__next-internal_server_app_api_workflow-agent_route_actions_0gxw9tv.js +3 -0
  126. package/app/apps/web/.next/server/chunks/packages_entries_dist_node_0bckr7-.js +1 -1
  127. package/app/apps/web/.next/server/chunks/packages_parser_dist_06d9se1._.js +3 -0
  128. package/app/apps/web/.next/server/chunks/packages_parser_dist_10ihc3~._.js +3 -0
  129. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__04eq6z2._.js +1 -1
  130. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__04il6m2._.js +11 -0
  131. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0exckd4._.js +1 -1
  132. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0n-ax-5._.js +3 -0
  133. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0o03ymy._.js +2 -2
  134. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0rvzdnx._.js +1 -1
  135. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0z7mg0b._.js +1 -1
  136. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0~.f_3z._.js +12 -0
  137. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__136-0me._.js +8 -0
  138. package/app/apps/web/.next/server/chunks/ssr/_0cgg5zi._.js +3 -0
  139. package/app/apps/web/.next/server/chunks/ssr/_0jw2sm7._.js +1 -1
  140. package/app/apps/web/.next/server/chunks/ssr/_0kajk31._.js +3 -0
  141. package/app/apps/web/.next/server/chunks/ssr/_0w.cv64._.js +8 -0
  142. package/app/apps/web/.next/server/chunks/ssr/_0xu2pvc._.js +1 -1
  143. package/app/apps/web/.next/server/chunks/ssr/apps_web_0.mu7c7._.js +1 -1
  144. package/app/apps/web/.next/server/chunks/ssr/apps_web__next-internal_server_app_print_[key]_page_actions_0_ygul-.js +3 -0
  145. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_01d-iwx._.js +6 -0
  146. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_week-digest_tsx_0c9ha31._.js +3 -0
  147. package/app/apps/web/.next/server/chunks/ssr/apps_web_lib_0k0tl52._.js +3 -0
  148. package/app/apps/web/.next/server/chunks/ssr/apps_web_lib_changelog_ts_0n7_p1d._.js +3 -0
  149. package/app/apps/web/.next/server/chunks/ssr/apps_web_lib_changelog_ts_0nb5_-s._.js +3 -0
  150. package/app/apps/web/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_07orc4m.js +4 -0
  151. package/app/apps/web/.next/server/chunks/ssr/packages_parser_dist_12.endt._.js +1 -1
  152. package/app/apps/web/.next/server/edge/chunks/[root-of-the-server]__0.sx~id._.js +11 -0
  153. package/app/apps/web/.next/server/edge/chunks/node_modules_next_dist_esm_build_templates_edge-wrapper_0p1t1z8.js +3 -0
  154. package/app/apps/web/.next/server/edge/chunks/turbopack-node_modules_next_dist_esm_build_templates_edge-wrapper_0mw-sqe.js +3 -0
  155. package/app/apps/web/.next/server/functions-config-manifest.json +4 -1
  156. package/app/apps/web/.next/server/middleware-build-manifest.js +3 -3
  157. package/app/apps/web/.next/server/middleware-manifest.json +30 -2
  158. package/app/apps/web/.next/server/pages/500.html +1 -1
  159. package/app/apps/web/.next/server/server-reference-manifest.js +1 -1
  160. package/app/apps/web/.next/server/server-reference-manifest.json +1 -1
  161. package/app/apps/web/.next/static/chunks/00xkqnvk4-d1-.js +10 -0
  162. package/app/apps/web/.next/static/chunks/03_7wmpodz36t.js +1 -0
  163. package/app/apps/web/.next/static/chunks/0716x58e1_2c5.js +1 -0
  164. package/app/apps/web/.next/static/chunks/0nnjldasalwdu.js +4 -0
  165. package/app/apps/web/.next/static/chunks/0okg13phslr1..js +4 -0
  166. package/app/apps/web/.next/static/chunks/0vf-3rauafbek.js +6 -0
  167. package/app/apps/web/.next/static/chunks/0vl27-vjob2_9.js +6 -0
  168. package/app/apps/web/.next/static/chunks/0yo458-7577lr.js +1 -0
  169. package/app/apps/web/.next/static/chunks/14bjditmmrn7l.js +1 -0
  170. package/app/apps/web/.next/static/rrY-ephAArv8tjJOXD622/_clientMiddlewareManifest.js +6 -0
  171. package/app/apps/web/app/api/events/route.ts +50 -2
  172. package/app/apps/web/app/api/pdf/[key]/route.ts +199 -0
  173. package/app/apps/web/app/api/workflow-agent/route.ts +27 -0
  174. package/app/apps/web/app/insights/[key]/page.tsx +4 -1
  175. package/app/apps/web/app/layout.tsx +23 -1
  176. package/app/apps/web/app/page.tsx +24 -1
  177. package/app/apps/web/app/print/[key]/page.tsx +91 -0
  178. package/app/apps/web/app/projects/[slug]/page.tsx +6 -7
  179. package/app/apps/web/app/projects/page.tsx +2 -3
  180. package/app/apps/web/app/sessions/[id]/session-view.tsx +1297 -19
  181. package/app/apps/web/app/sessions/sessions-grid.tsx +10 -1
  182. package/app/apps/web/components/dashboard-view.tsx +2 -1
  183. package/app/apps/web/components/export-pdf-button.tsx +76 -0
  184. package/app/apps/web/components/month-digest-view.tsx +10 -0
  185. package/app/apps/web/components/week-digest-view.tsx +10 -0
  186. package/app/apps/web/lib/changelog-data.generated.ts +1 -1
  187. package/app/apps/web/lib/entries-index.ts +5 -4
  188. package/app/apps/web/lib/find-chrome.ts +71 -0
  189. package/app/apps/web/middleware.ts +22 -0
  190. package/app/apps/web/package.json +1 -1
  191. package/app/apps/web/tsconfig.tsbuildinfo +1 -1
  192. package/dist/daemon-worker.js +1716 -281
  193. package/dist/index.js +1617 -195
  194. package/package.json +1 -1
  195. package/app/apps/web/.next/server/chunks/packages_parser_dist_04rh1zd._.js +0 -3
  196. package/app/apps/web/.next/server/chunks/packages_parser_dist_0ndsxe6._.js +0 -3
  197. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0_xpuh7._.js +0 -3
  198. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0d~2~le._.js +0 -8
  199. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0w52fbc._.js +0 -12
  200. package/app/apps/web/.next/server/chunks/ssr/_02s1n-x._.js +0 -8
  201. package/app/apps/web/.next/server/chunks/ssr/_0fjbf--._.js +0 -3
  202. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_0ly1xes._.js +0 -6
  203. package/app/apps/web/.next/server/chunks/ssr/apps_web_lib_0af94gl._.js +0 -3
  204. package/app/apps/web/.next/static/chunks/0.3mqbj24kwtc.js +0 -6
  205. package/app/apps/web/.next/static/chunks/0byz8fqw.exef.js +0 -1
  206. package/app/apps/web/.next/static/chunks/0l3ck.awv~4t7.js +0 -1
  207. package/app/apps/web/.next/static/chunks/0lxuac2v0_gdl.js +0 -6
  208. package/app/apps/web/.next/static/chunks/0m62vx92~0_0d.js +0 -1
  209. package/app/apps/web/.next/static/chunks/0rtba0saz74e1.js +0 -10
  210. package/app/apps/web/.next/static/chunks/0s4eyofh28l~u.js +0 -1
  211. package/app/apps/web/.next/static/chunks/0uev~g~qvbjv9.js +0 -4
  212. package/app/apps/web/.next/static/m6ibRSdAu8jD10VBA2OFH/_clientMiddlewareManifest.js +0 -1
  213. /package/app/apps/web/.next/static/{m6ibRSdAu8jD10VBA2OFH → rrY-ephAArv8tjJOXD622}/_buildManifest.js +0 -0
  214. /package/app/apps/web/.next/static/{m6ibRSdAu8jD10VBA2OFH → rrY-ephAArv8tjJOXD622}/_ssgManifest.js +0 -0
@@ -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 sessions={sessions} daySummaries={daySummaries} />
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
- canonicalProjectName,
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
- // `%2FUsers%2Ffoo%2FRepo%2Fbar`). Match any session whose own
43
- // canonicalProjectName resolves to the same path — that includes the
44
- // parent repo AND any `/.worktrees/<name>` subdirs, so a project detail
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) => canonicalProjectName(s.projectName) === decodedCanonical,
48
+ (s) => projectRepoName(s.projectName) === decodedCanonical,
50
49
  );
51
50
  if (projectSessions.length === 0) return notFound();
52
51
 
53
- // Use the canonical name for display (no `.worktrees/<name>` suffix).
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 { canonicalProjectName, groupByProject } from "@claude-lens/parser";
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 canonical = canonicalProjectName(p.projectName);
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);