fleetlens 0.3.0 → 0.4.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.
Files changed (209) hide show
  1. package/app/apps/web/.next/BUILD_ID +1 -1
  2. package/app/apps/web/.next/app-path-routes-manifest.json +9 -0
  3. package/app/apps/web/.next/build-manifest.json +6 -6
  4. package/app/apps/web/.next/prerender-manifest.json +3 -3
  5. package/app/apps/web/.next/routes-manifest.json +62 -0
  6. package/app/apps/web/.next/server/app/_global-error/page/build-manifest.json +3 -3
  7. package/app/apps/web/.next/server/app/_global-error.html +1 -1
  8. package/app/apps/web/.next/server/app/_global-error.rsc +1 -1
  9. package/app/apps/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  10. package/app/apps/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  11. package/app/apps/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  12. package/app/apps/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  13. package/app/apps/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  14. package/app/apps/web/.next/server/app/_not-found/page/build-manifest.json +3 -3
  15. package/app/apps/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  16. package/app/apps/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  17. package/app/apps/web/.next/server/app/api/ask/route.js +3 -2
  18. package/app/apps/web/.next/server/app/api/ask/route.js.nft.json +1 -1
  19. package/app/apps/web/.next/server/app/api/events/route.js +1 -0
  20. package/app/apps/web/.next/server/app/api/events/route.js.nft.json +1 -1
  21. package/app/apps/web/.next/server/app/api/insights/months-index/route/app-paths-manifest.json +3 -0
  22. package/app/apps/web/.next/server/app/api/insights/months-index/route/build-manifest.json +9 -0
  23. package/app/apps/web/.next/server/app/api/insights/months-index/route/server-reference-manifest.json +4 -0
  24. package/app/apps/web/.next/server/app/api/insights/months-index/route.js +7 -0
  25. package/app/apps/web/.next/server/app/api/insights/months-index/route.js.map +5 -0
  26. package/app/apps/web/.next/server/app/api/insights/months-index/route.js.nft.json +1 -0
  27. package/app/apps/web/.next/server/app/api/insights/months-index/route_client-reference-manifest.js +3 -0
  28. package/app/apps/web/.next/server/app/api/insights/pdf/[key]/route/app-paths-manifest.json +3 -0
  29. package/app/apps/web/.next/server/app/api/insights/pdf/[key]/route/build-manifest.json +9 -0
  30. package/app/apps/web/.next/server/app/api/insights/pdf/[key]/route/server-reference-manifest.json +4 -0
  31. package/app/apps/web/.next/server/app/api/insights/pdf/[key]/route.js +6 -0
  32. package/app/apps/web/.next/server/app/api/insights/pdf/[key]/route.js.map +5 -0
  33. package/app/apps/web/.next/server/app/api/insights/pdf/[key]/route.js.nft.json +1 -0
  34. package/app/apps/web/.next/server/app/api/insights/pdf/[key]/route_client-reference-manifest.js +3 -0
  35. package/app/apps/web/.next/server/app/api/insights/route/app-paths-manifest.json +3 -0
  36. package/app/apps/web/.next/server/app/api/insights/route/build-manifest.json +9 -0
  37. package/app/apps/web/.next/server/app/api/insights/route/server-reference-manifest.json +4 -0
  38. package/app/apps/web/.next/server/app/api/insights/route.js +8 -0
  39. package/app/apps/web/.next/server/app/api/insights/route.js.map +5 -0
  40. package/app/apps/web/.next/server/app/api/insights/route.js.nft.json +1 -0
  41. package/app/apps/web/.next/server/app/api/insights/route_client-reference-manifest.js +3 -0
  42. package/app/apps/web/.next/server/app/api/insights/saved/[key]/route/app-paths-manifest.json +3 -0
  43. package/app/apps/web/.next/server/app/api/insights/saved/[key]/route/build-manifest.json +9 -0
  44. package/app/apps/web/.next/server/app/api/insights/saved/[key]/route/server-reference-manifest.json +4 -0
  45. package/app/apps/web/.next/server/app/api/insights/saved/[key]/route.js +6 -0
  46. package/app/apps/web/.next/server/app/api/insights/saved/[key]/route.js.map +5 -0
  47. package/app/apps/web/.next/server/app/api/insights/saved/[key]/route.js.nft.json +1 -0
  48. package/app/apps/web/.next/server/app/api/insights/saved/[key]/route_client-reference-manifest.js +3 -0
  49. package/app/apps/web/.next/server/app/api/insights/saved/route/app-paths-manifest.json +3 -0
  50. package/app/apps/web/.next/server/app/api/insights/saved/route/build-manifest.json +9 -0
  51. package/app/apps/web/.next/server/app/api/insights/saved/route/server-reference-manifest.json +4 -0
  52. package/app/apps/web/.next/server/app/api/insights/saved/route.js +6 -0
  53. package/app/apps/web/.next/server/app/api/insights/saved/route.js.map +5 -0
  54. package/app/apps/web/.next/server/app/api/insights/saved/route.js.nft.json +1 -0
  55. package/app/apps/web/.next/server/app/api/insights/saved/route_client-reference-manifest.js +3 -0
  56. package/app/apps/web/.next/server/app/api/insights/weeks-index/route/app-paths-manifest.json +3 -0
  57. package/app/apps/web/.next/server/app/api/insights/weeks-index/route/build-manifest.json +9 -0
  58. package/app/apps/web/.next/server/app/api/insights/weeks-index/route/server-reference-manifest.json +4 -0
  59. package/app/apps/web/.next/server/app/api/insights/weeks-index/route.js +7 -0
  60. package/app/apps/web/.next/server/app/api/insights/weeks-index/route.js.map +5 -0
  61. package/app/apps/web/.next/server/app/api/insights/weeks-index/route.js.nft.json +1 -0
  62. package/app/apps/web/.next/server/app/api/insights/weeks-index/route_client-reference-manifest.js +3 -0
  63. package/app/apps/web/.next/server/app/insights/[key]/page/app-paths-manifest.json +3 -0
  64. package/app/apps/web/.next/server/app/insights/[key]/page/build-manifest.json +17 -0
  65. package/app/apps/web/.next/server/app/insights/[key]/page/next-font-manifest.json +6 -0
  66. package/app/apps/web/.next/server/app/insights/[key]/page/react-loadable-manifest.json +1 -0
  67. package/app/apps/web/.next/server/app/insights/[key]/page/server-reference-manifest.json +4 -0
  68. package/app/apps/web/.next/server/app/insights/[key]/page.js +16 -0
  69. package/app/apps/web/.next/server/app/insights/[key]/page.js.map +5 -0
  70. package/app/apps/web/.next/server/app/insights/[key]/page.js.nft.json +1 -0
  71. package/app/apps/web/.next/server/app/insights/[key]/page_client-reference-manifest.js +3 -0
  72. package/app/apps/web/.next/server/app/insights/page/app-paths-manifest.json +3 -0
  73. package/app/apps/web/.next/server/app/insights/page/build-manifest.json +17 -0
  74. package/app/apps/web/.next/server/app/insights/page/next-font-manifest.json +6 -0
  75. package/app/apps/web/.next/server/app/insights/page/react-loadable-manifest.json +1 -0
  76. package/app/apps/web/.next/server/app/insights/page/server-reference-manifest.json +4 -0
  77. package/app/apps/web/.next/server/app/insights/page.js +16 -0
  78. package/app/apps/web/.next/server/app/insights/page.js.map +5 -0
  79. package/app/apps/web/.next/server/app/insights/page.js.nft.json +1 -0
  80. package/app/apps/web/.next/server/app/insights/page_client-reference-manifest.js +3 -0
  81. package/app/apps/web/.next/server/app/insights/print/[key]/page/app-paths-manifest.json +3 -0
  82. package/app/apps/web/.next/server/app/insights/print/[key]/page/build-manifest.json +17 -0
  83. package/app/apps/web/.next/server/app/insights/print/[key]/page/next-font-manifest.json +6 -0
  84. package/app/apps/web/.next/server/app/insights/print/[key]/page/react-loadable-manifest.json +1 -0
  85. package/app/apps/web/.next/server/app/insights/print/[key]/page/server-reference-manifest.json +4 -0
  86. package/app/apps/web/.next/server/app/insights/print/[key]/page.js +16 -0
  87. package/app/apps/web/.next/server/app/insights/print/[key]/page.js.map +5 -0
  88. package/app/apps/web/.next/server/app/insights/print/[key]/page.js.nft.json +1 -0
  89. package/app/apps/web/.next/server/app/insights/print/[key]/page_client-reference-manifest.js +3 -0
  90. package/app/apps/web/.next/server/app/page/build-manifest.json +3 -3
  91. package/app/apps/web/.next/server/app/page.js +1 -1
  92. package/app/apps/web/.next/server/app/page.js.nft.json +1 -1
  93. package/app/apps/web/.next/server/app/page_client-reference-manifest.js +1 -1
  94. package/app/apps/web/.next/server/app/parallelism/page/build-manifest.json +3 -3
  95. package/app/apps/web/.next/server/app/parallelism/page.js.nft.json +1 -1
  96. package/app/apps/web/.next/server/app/parallelism/page_client-reference-manifest.js +1 -1
  97. package/app/apps/web/.next/server/app/projects/[slug]/page/build-manifest.json +3 -3
  98. package/app/apps/web/.next/server/app/projects/[slug]/page.js +1 -2
  99. package/app/apps/web/.next/server/app/projects/[slug]/page.js.nft.json +1 -1
  100. package/app/apps/web/.next/server/app/projects/[slug]/page_client-reference-manifest.js +1 -1
  101. package/app/apps/web/.next/server/app/projects/page/build-manifest.json +3 -3
  102. package/app/apps/web/.next/server/app/projects/page.js.nft.json +1 -1
  103. package/app/apps/web/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  104. package/app/apps/web/.next/server/app/sessions/[id]/page/build-manifest.json +3 -3
  105. package/app/apps/web/.next/server/app/sessions/[id]/page.js +1 -2
  106. package/app/apps/web/.next/server/app/sessions/[id]/page.js.nft.json +1 -1
  107. package/app/apps/web/.next/server/app/sessions/[id]/page_client-reference-manifest.js +1 -1
  108. package/app/apps/web/.next/server/app/sessions/page/build-manifest.json +3 -3
  109. package/app/apps/web/.next/server/app/sessions/page.js.nft.json +1 -1
  110. package/app/apps/web/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
  111. package/app/apps/web/.next/server/app/usage/page/build-manifest.json +3 -3
  112. package/app/apps/web/.next/server/app/usage/page.js.nft.json +1 -1
  113. package/app/apps/web/.next/server/app/usage/page_client-reference-manifest.js +1 -1
  114. package/app/apps/web/.next/server/app-paths-manifest.json +9 -0
  115. package/app/apps/web/.next/server/chunks/02dz_web__next-internal_server_app_api_insights_months-index_route_actions_0~h~zci.js +3 -0
  116. package/app/apps/web/.next/server/chunks/02dz_web__next-internal_server_app_api_insights_saved_[key]_route_actions_0a5j9yw.js +3 -0
  117. package/app/apps/web/.next/server/chunks/02dz_web__next-internal_server_app_api_insights_weeks-index_route_actions_0hh-1c7.js +3 -0
  118. package/app/apps/web/.next/server/chunks/{[externals]__0rv9ct5._.js → [externals]__0_xztrf._.js} +2 -2
  119. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0.hmj0z._.js +3 -0
  120. package/app/apps/web/.next/server/chunks/[root-of-the-server]__01h_g-p._.js +3 -0
  121. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0nh-z10._.js +3 -0
  122. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0op9af~._.js +1 -1
  123. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0ri7q~r._.js +3 -0
  124. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0rr1~~x._.js +3 -0
  125. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0s0gv.0._.js +3 -0
  126. package/app/apps/web/.next/server/chunks/apps_web__next-internal_server_app_api_insights_pdf_[key]_route_actions_0lu-lnx.js +3 -0
  127. package/app/apps/web/.next/server/chunks/apps_web__next-internal_server_app_api_insights_route_actions_0h8l5lz.js +3 -0
  128. package/app/apps/web/.next/server/chunks/apps_web__next-internal_server_app_api_insights_saved_route_actions_13rlblw.js +3 -0
  129. package/app/apps/web/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_00t2xj6.js +68 -0
  130. package/app/apps/web/.next/server/chunks/{[root-of-the-server]__0sgyieh._.js → node_modules_next_dist_esm_build_templates_app-route_0ck.78s.js} +5 -5
  131. package/app/apps/web/.next/server/chunks/packages_parser_dist_fs_0bc7ptm.js +3 -0
  132. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__00z7zlm._.js +1 -1
  133. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__08.d5ia._.js +12 -0
  134. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0b323~k._.js +8 -0
  135. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0euu8sa._.js +3 -0
  136. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0gio_ck._.js +3 -0
  137. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0gx-4qa._.js +3 -0
  138. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0w52fbc._.js +2 -2
  139. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0z45d0g._.js +49 -0
  140. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__13itnvx._.js +3 -0
  141. package/app/apps/web/.next/server/chunks/ssr/_0r~__0v._.js +12 -0
  142. package/app/apps/web/.next/server/chunks/ssr/_0xies90._.js +1 -1
  143. package/app/apps/web/.next/server/chunks/ssr/apps_web__next-internal_server_app_insights_[key]_page_actions_0_z2~e_.js +3 -0
  144. package/app/apps/web/.next/server/chunks/ssr/apps_web__next-internal_server_app_insights_page_actions_0zjdm~n.js +3 -0
  145. package/app/apps/web/.next/server/chunks/ssr/apps_web__next-internal_server_app_insights_print_[key]_page_actions_0a853fw.js +3 -0
  146. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_06j3c.0._.js +1 -1
  147. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_insight-report_tsx_13q2a-c._.js +12 -0
  148. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_insights-view_tsx_11730ta._.js +6 -0
  149. package/app/apps/web/.next/server/chunks/ssr/node_modules_lucide-react_dist_esm_icons_0-lmm4p._.js +3 -0
  150. package/app/apps/web/.next/server/chunks/ssr/node_modules_lucide-react_dist_esm_icons_0nqz8dw._.js +3 -0
  151. package/app/apps/web/.next/server/chunks/ssr/node_modules_next_dist_0t-hj0x._.js +3 -0
  152. package/app/apps/web/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0vz7_px.js +4 -0
  153. package/app/apps/web/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_10cdrh2.js +4 -0
  154. package/app/apps/web/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_12n1lw_.js +4 -0
  155. package/app/apps/web/.next/server/chunks/ssr/packages_parser_dist_0-jvt~a._.js +1 -1
  156. package/app/apps/web/.next/server/functions-config-manifest.json +7 -1
  157. package/app/apps/web/.next/server/middleware-build-manifest.js +6 -6
  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/03_7xtpi9sv4g.js +4 -0
  162. package/app/apps/web/.next/static/chunks/03g5dw5eskotd.js +6 -0
  163. package/app/apps/web/.next/static/chunks/{0p1lpoh02zh8h.js → 063oimt30jeo9.js} +2 -2
  164. package/app/apps/web/.next/static/chunks/08c1pcrocqq5s.js +6 -0
  165. package/app/apps/web/.next/static/chunks/0e-wrhqh4h3r5.js +2 -0
  166. package/app/apps/web/.next/static/chunks/0i7er7_bfab36.js +10 -0
  167. package/app/apps/web/.next/static/chunks/0ipytyi01h0ta.js +1 -0
  168. package/app/apps/web/.next/static/chunks/137uw2-qbro07.js +31 -0
  169. package/app/apps/web/.next/static/chunks/{turbopack-17n9y8y0h4q9f.js → turbopack-169w-1._23ra_.js} +1 -1
  170. package/app/apps/web/app/api/insights/months-index/route.ts +80 -0
  171. package/app/apps/web/app/api/insights/pdf/[key]/route.ts +45 -0
  172. package/app/apps/web/app/api/insights/route.ts +508 -0
  173. package/app/apps/web/app/api/insights/saved/[key]/route.ts +11 -0
  174. package/app/apps/web/app/api/insights/saved/route.ts +9 -0
  175. package/app/apps/web/app/api/insights/weeks-index/route.ts +95 -0
  176. package/app/apps/web/app/insights/[key]/page.tsx +59 -0
  177. package/app/apps/web/app/insights/page.tsx +7 -0
  178. package/app/apps/web/app/insights/print/[key]/page.tsx +73 -0
  179. package/app/apps/web/components/insight-report.tsx +1031 -0
  180. package/app/apps/web/components/insights-view.tsx +647 -0
  181. package/app/apps/web/components/live-refresher.tsx +11 -2
  182. package/app/apps/web/components/sidebar.tsx +8 -0
  183. package/app/apps/web/eslint.config.mjs +2 -0
  184. package/app/apps/web/lib/ai/chrome-pdf.ts +64 -0
  185. package/app/apps/web/lib/ai/insights-prompt.ts +71 -0
  186. package/app/apps/web/lib/ai/saved-reports.ts +83 -0
  187. package/app/apps/web/lib/date-range.ts +11 -2
  188. package/app/apps/web/lib/use-live-events.ts +7 -2
  189. package/app/apps/web/package.json +3 -2
  190. package/app/apps/web/tsconfig.tsbuildinfo +1 -1
  191. package/dist/daemon-worker.js +2765 -11
  192. package/dist/index.js +2990 -254
  193. package/package.json +5 -3
  194. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__05lbtny._.js +0 -12
  195. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0ms7h0~._.js +0 -8
  196. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0p~g4m6._.js +0 -3
  197. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0wvzyl7._.js +0 -3
  198. package/app/apps/web/.next/server/chunks/ssr/_0ph19-k._.js +0 -12
  199. package/app/apps/web/.next/server/chunks/ssr/_0u29mdd._.js +0 -3
  200. package/app/apps/web/.next/server/chunks/ssr/node_modules_0i2xw~e._.js +0 -3
  201. package/app/apps/web/.next/server/chunks/ssr/node_modules_lucide-react_dist_esm_icons_0.knjn.._.js +0 -3
  202. package/app/apps/web/.next/static/chunks/0257pdz1-imal.js +0 -2
  203. package/app/apps/web/.next/static/chunks/08983lz4~hffx.js +0 -6
  204. package/app/apps/web/.next/static/chunks/0qmh881w772ef.js +0 -31
  205. package/app/apps/web/.next/static/chunks/0wdga7zbwvg70.js +0 -1
  206. package/app/apps/web/.next/static/chunks/14q3xsfpkvuef.js +0 -6
  207. /package/app/apps/web/.next/static/{0fPDRTdMQ-Y62h6fHNJXL → G_4ofKI9v9ARC_lb9JILZ}/_buildManifest.js +0 -0
  208. /package/app/apps/web/.next/static/{0fPDRTdMQ-Y62h6fHNJXL → G_4ofKI9v9ARC_lb9JILZ}/_clientMiddlewareManifest.js +0 -0
  209. /package/app/apps/web/.next/static/{0fPDRTdMQ-Y62h6fHNJXL → G_4ofKI9v9ARC_lb9JILZ}/_ssgManifest.js +0 -0
@@ -13,6 +13,7 @@ import {
13
13
  Search,
14
14
  Activity,
15
15
  Gauge,
16
+ Lightbulb,
16
17
  } from "lucide-react";
17
18
  import { formatRelative, prettyProjectName } from "@/lib/format";
18
19
  import { ThemeToggle } from "@/components/theme-toggle";
@@ -193,6 +194,13 @@ export function Sidebar({
193
194
  >
194
195
  Usage
195
196
  </NavLink>
197
+ <NavLink
198
+ href="/insights"
199
+ active={pathname === "/insights"}
200
+ icon={<Lightbulb size={15} />}
201
+ >
202
+ Insights
203
+ </NavLink>
196
204
  </nav>
197
205
 
198
206
  {/* Projects search + list */}
@@ -0,0 +1,2 @@
1
+ import root from "../../eslint.config.mjs";
2
+ export default root;
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Thin wrapper around system Chrome/Chromium headless for PDF rendering.
3
+ *
4
+ * We detect the browser binary at known paths (macOS / Linux / Windows);
5
+ * override via CHROME_BIN env var. No node dependency on Puppeteer —
6
+ * Fleetlens is a local-only tool so we assume the user has a browser
7
+ * installed anyway.
8
+ */
9
+ import { existsSync } from "node:fs";
10
+ import { readFile, unlink } from "node:fs/promises";
11
+ import { spawn } from "node:child_process";
12
+ import { tmpdir } from "node:os";
13
+ import { join } from "node:path";
14
+
15
+ const CANDIDATES: string[] = [
16
+ process.env.CHROME_BIN ?? "",
17
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
18
+ "/Applications/Chromium.app/Contents/MacOS/Chromium",
19
+ "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
20
+ "/usr/bin/google-chrome",
21
+ "/usr/bin/google-chrome-stable",
22
+ "/usr/bin/chromium-browser",
23
+ "/usr/bin/chromium",
24
+ "/snap/bin/chromium",
25
+ "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
26
+ "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
27
+ ].filter(Boolean);
28
+
29
+ let cached: string | null | undefined;
30
+
31
+ export function findChrome(): string | null {
32
+ if (cached !== undefined) return cached;
33
+ for (const p of CANDIDATES) {
34
+ if (existsSync(p)) { cached = p; return p; }
35
+ }
36
+ cached = null;
37
+ return null;
38
+ }
39
+
40
+ export async function renderPdf(url: string, outName: string): Promise<Buffer> {
41
+ const chrome = findChrome();
42
+ if (!chrome) {
43
+ throw new Error("Chrome/Chromium binary not found. Install Chrome or set CHROME_BIN.");
44
+ }
45
+ const safeName = outName.replace(/[^a-z0-9-]/gi, "_");
46
+ const outPath = join(tmpdir(), `fleetlens-${safeName}-${Date.now()}.pdf`);
47
+ await new Promise<void>((resolve, reject) => {
48
+ const proc = spawn(chrome, [
49
+ "--headless=new",
50
+ "--disable-gpu",
51
+ "--no-sandbox",
52
+ "--hide-scrollbars",
53
+ "--no-pdf-header-footer",
54
+ "--virtual-time-budget=5000",
55
+ `--print-to-pdf=${outPath}`,
56
+ url,
57
+ ], { stdio: "ignore" });
58
+ proc.on("close", (code) => code === 0 ? resolve() : reject(new Error(`chrome exited ${code}`)));
59
+ proc.on("error", reject);
60
+ });
61
+ const bytes = await readFile(outPath);
62
+ void unlink(outPath).catch(() => {});
63
+ return bytes;
64
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * System prompt for the Insights analyst.
3
+ *
4
+ * Input it receives:
5
+ * - period: { start, end, label, range_type }
6
+ * - aggregates: PeriodBundle from @claude-lens/parser (all deterministic numbers)
7
+ * - capsules: SessionCapsule[] — intent material + behavioural flags
8
+ * - prior: PriorWeek[] — last 4 reports (archetype + stats). Empty if this
9
+ * is the first run. When non-empty, populate archetype.vs_usual.
10
+ *
11
+ * Output MUST be a single JSON object inside a ```json fence, matching
12
+ * the schema below. No prose outside the fence. No markdown outside.
13
+ */
14
+ export const INSIGHTS_SYSTEM_PROMPT = `You are the Insights analyst for Fleetlens, a dashboard for Claude Code sessions.
15
+
16
+ You receive a JSON payload summarising one calendar period (week or 4-weeks). The numbers are already computed — you do NOT do arithmetic, you interpret and name patterns.
17
+
18
+ ## Your output
19
+
20
+ Return EXACTLY ONE JSON object inside a \`\`\`json fenced code block. No prose before or after. Schema:
21
+
22
+ {
23
+ "archetype": {
24
+ "label": "Orchestration Conductor" | "Deep-dive Conversationalist" | "Fire-and-go Operator" | "Solo Builder" | "Context Switcher" | "Research Explorer",
25
+ "icon": "Network" | "BrainCircuit" | "Zap" | "Compass" | "Layers3" | "Sparkles",
26
+ "tagline": "… short phrase, ≤ 70 chars, describing working style …",
27
+ "why": "… 1-2 sentences grounded in aggregate numbers (e.g. 'You ran X subagent dispatches across Y turns…') …",
28
+ "vs_usual": "… OPTIONAL: only populate if \`prior\` is non-empty. 1 sentence contrasting this week's archetype + behaviour with the baseline (e.g. 'More orchestrated than the last 4 weeks (usual: Solo Builder)'). Omit the key entirely otherwise."
29
+ },
30
+ "theme_headline": "3-7 words capturing the period's dominant theme",
31
+ "shipped_summaries": {
32
+ "<session_id>": "1-sentence characterisation of the shipped work"
33
+ },
34
+ "patterns": [
35
+ { "icon": "Repeat" | "ClipboardList" | "GitCommit" | "Network" | "BrainCircuit" | "Zap" | "TrendingUp" | "TrendingDown",
36
+ "title": "…",
37
+ "stat": "…",
38
+ "note": "1 short sentence" }
39
+ // 2-4 patterns total
40
+ ],
41
+ "concurrency_insight": "1-2 sentences about parallelism shape. Empty string if no concurrency data.",
42
+ "concurrency_suggestion": "1 sentence, actionable. Empty string if nothing useful.",
43
+ "outlier_notes": {
44
+ "longest_run": "1 line takeaway",
45
+ "fastest_ship": "…",
46
+ "most_errors": "…",
47
+ "wandered": "…"
48
+ },
49
+ "suggestion_headline": "bold, imperative, ≤ 70 chars",
50
+ "suggestion_body": "2-3 sentences. Grounded in what you saw. Actionable."
51
+ }
52
+
53
+ ## Archetype guide
54
+
55
+ - **Orchestration Conductor** — heavy subagent_calls, long unsupervised turns, planning unfolds in-session.
56
+ - **Deep-dive Conversationalist** — high turn_count per session, rapid user↔agent ping-pong, low subagent use.
57
+ - **Fire-and-go Operator** — few user turns per session, long autonomous runs, minimal interruption.
58
+ - **Solo Builder** — mid-length sessions, steady cadence, low subagent use, high direct edit/bash ratio.
59
+ - **Context Switcher** — many short sessions across many projects, high cross-project count.
60
+ - **Research Explorer** — Grep/Read/WebFetch heavy, low commits, exploratory outcomes.
61
+
62
+ Pick exactly one. If borderline, pick the closer fit and say so in the \`why\`.
63
+
64
+ ## Rules
65
+
66
+ 1. **Ground every claim** — every sentence you write must reference a number or fact in the data. Never invent. Never include unreferenced general advice.
67
+ 2. **No vanity metrics** — don't quote total tokens, total cost, total sessions as headline claims. Use behavioural signals (subagent_calls, session length distribution, plan mode use, interrupt rate, concurrency peak).
68
+ 3. **Short and concrete** — no filler words. "You ran X this week" not "This past week saw the user running X."
69
+ 4. **Valid JSON** — strict. No trailing commas. No comments in the output. No text outside the fence.
70
+ 5. **Shipped summaries** — if capsules have pr_titles, write one summary per session_id that shipped. Use capsule first_user / final_agent / flags for texture.
71
+ 6. **Pattern selection** — pick 2-4 patterns where the signal is strongest. Don't pad. If plan_mode usage is zero and it's a busy week, say so. If loop_suspected flags correlate with shipping, call that out.`;
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Persistence for generated insight reports.
3
+ *
4
+ * Reports are keyed by their window start date + range_type:
5
+ * week-2026-04-13.json — week of Apr 13 – Apr 19
6
+ * 4weeks-2026-03-23.json — 4-week rollup Mar 23 – Apr 19
7
+ *
8
+ * Pruning is lazy — we don't delete; this is ~2 KB/report so a year of
9
+ * weekly reports is ~100 KB.
10
+ */
11
+ import { promises as fs } from "node:fs";
12
+ import { join } from "node:path";
13
+ import { homedir } from "node:os";
14
+ import type { ReportData } from "@/components/insight-report";
15
+
16
+ const DIR = join(homedir(), ".cclens", "insights");
17
+
18
+ export type SavedReportMeta = {
19
+ key: string;
20
+ period_label: string;
21
+ period_sublabel: string;
22
+ range_type: ReportData["range_type"];
23
+ archetype_label: string;
24
+ archetype_icon: string;
25
+ saved_at: string;
26
+ sessions_used: number;
27
+ prs: number;
28
+ };
29
+
30
+ type SavedFile = { saved_at: string; report: ReportData };
31
+
32
+ export function keyForRange(range_type: ReportData["range_type"], startIso: string): string {
33
+ const yyyymmdd = startIso.slice(0, 10);
34
+ return `${range_type}-${yyyymmdd}`;
35
+ }
36
+
37
+ export async function saveReport(key: string, report: ReportData): Promise<void> {
38
+ await fs.mkdir(DIR, { recursive: true });
39
+ const payload: SavedFile = { saved_at: new Date().toISOString(), report };
40
+ await fs.writeFile(join(DIR, `${key}.json`), JSON.stringify(payload, null, 2));
41
+ }
42
+
43
+ export async function listSavedReports(): Promise<SavedReportMeta[]> {
44
+ try {
45
+ const files = await fs.readdir(DIR);
46
+ const out: SavedReportMeta[] = [];
47
+ for (const f of files) {
48
+ if (!f.endsWith(".json")) continue;
49
+ try {
50
+ const raw = await fs.readFile(join(DIR, f), "utf8");
51
+ const parsed = JSON.parse(raw) as SavedFile;
52
+ const r = parsed.report;
53
+ out.push({
54
+ key: f.replace(/\.json$/, ""),
55
+ period_label: r.period_label,
56
+ period_sublabel: r.period_sublabel,
57
+ range_type: r.range_type,
58
+ archetype_label: r.archetype.label,
59
+ archetype_icon: r.archetype.icon,
60
+ saved_at: parsed.saved_at,
61
+ sessions_used: r.meta.sessions_used,
62
+ prs: r.shipped.length,
63
+ });
64
+ } catch { /* skip corrupt */ }
65
+ }
66
+ // Newest first (keys sort lexicographically by start date since YYYY-MM-DD)
67
+ out.sort((a, b) => b.key.localeCompare(a.key));
68
+ return out;
69
+ } catch {
70
+ return [];
71
+ }
72
+ }
73
+
74
+ export async function getSavedReport(key: string): Promise<ReportData | null> {
75
+ if (!/^[a-z0-9-]+$/i.test(key)) return null; // no path traversal
76
+ try {
77
+ const raw = await fs.readFile(join(DIR, `${key}.json`), "utf8");
78
+ const parsed = JSON.parse(raw) as SavedFile;
79
+ return parsed.report;
80
+ } catch {
81
+ return null;
82
+ }
83
+ }
@@ -14,9 +14,18 @@ export function parseRange(value: string | string[] | undefined): RangeKey {
14
14
  return "all";
15
15
  }
16
16
 
17
- /** Return the cutoff ms for a given range, or undefined for "all". */
17
+ /**
18
+ * Return the cutoff ms for a given range, or undefined for "all".
19
+ * Calendar-day semantics: "30d" means the last 30 local calendar days
20
+ * (today + 29 prior), not a rolling 30×24h window. Keeps the daily-bucket
21
+ * bars and the filter boundary aligned, and keeps the answer stable
22
+ * across page refreshes within the same day.
23
+ */
18
24
  export function cutoffMs(range: RangeKey, now = Date.now()): number | undefined {
19
25
  if (range === "all") return undefined;
20
26
  const days = range === "7d" ? 7 : range === "30d" ? 30 : 90;
21
- return now - days * 24 * 60 * 60 * 1000;
27
+ const d = new Date(now);
28
+ d.setHours(0, 0, 0, 0);
29
+ d.setDate(d.getDate() - (days - 1));
30
+ return d.getTime();
22
31
  }
@@ -34,7 +34,11 @@ export type LiveUpdate = LiveSessionUpdate | LiveUsageUpdate;
34
34
 
35
35
  type LiveEvent = LiveUpdate | { type: "heartbeat"; tsMs: number } | { type: "ready" };
36
36
 
37
- export function useLiveEvents(onUpdate: (update: LiveUpdate) => void): void {
37
+ export function useLiveEvents(
38
+ onUpdate: (update: LiveUpdate) => void,
39
+ options: { enabled?: boolean } = {},
40
+ ): void {
41
+ const { enabled = true } = options;
38
42
  // Keep the latest handler in a ref so the EventSource effect doesn't
39
43
  // re-open every time the parent re-renders with a new closure.
40
44
  const handlerRef = useRef(onUpdate);
@@ -43,6 +47,7 @@ export function useLiveEvents(onUpdate: (update: LiveUpdate) => void): void {
43
47
  }, [onUpdate]);
44
48
 
45
49
  useEffect(() => {
50
+ if (!enabled) return;
46
51
  const es = new EventSource("/api/events");
47
52
  es.onmessage = (e) => {
48
53
  try {
@@ -61,5 +66,5 @@ export function useLiveEvents(onUpdate: (update: LiveUpdate) => void): void {
61
66
  return () => {
62
67
  es.close();
63
68
  };
64
- }, []);
69
+ }, [enabled]);
65
70
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-lens/web",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "private": true,
5
5
  "description": "Local-only dashboard for Claude Code sessions. Reads ~/.claude/projects/*.jsonl and visualizes agent activity.",
6
6
  "license": "MIT",
@@ -11,7 +11,8 @@
11
11
  "lint": "eslint",
12
12
  "typecheck": "tsc --noEmit",
13
13
  "test": "vitest run --passWithNoTests",
14
- "clean": "rm -rf .next .turbo dist *.tsbuildinfo"
14
+ "clean": "rm -rf .next .turbo dist *.tsbuildinfo",
15
+ "test:coverage": "vitest run --passWithNoTests --coverage"
15
16
  },
16
17
  "dependencies": {
17
18
  "@claude-lens/parser": "workspace:*",