fleetlens 0.2.9 → 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 (237) 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 +1 -1
  16. package/app/apps/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  17. package/app/apps/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  18. package/app/apps/web/.next/server/app/api/ask/route.js +3 -2
  19. package/app/apps/web/.next/server/app/api/ask/route.js.nft.json +1 -1
  20. package/app/apps/web/.next/server/app/api/events/route.js +1 -0
  21. package/app/apps/web/.next/server/app/api/events/route.js.nft.json +1 -1
  22. package/app/apps/web/.next/server/app/api/insights/months-index/route/app-paths-manifest.json +3 -0
  23. package/app/apps/web/.next/server/app/api/insights/months-index/route/build-manifest.json +9 -0
  24. package/app/apps/web/.next/server/app/api/insights/months-index/route/server-reference-manifest.json +4 -0
  25. package/app/apps/web/.next/server/app/api/insights/months-index/route.js +7 -0
  26. package/app/apps/web/.next/server/app/api/insights/months-index/route.js.map +5 -0
  27. package/app/apps/web/.next/server/app/api/insights/months-index/route.js.nft.json +1 -0
  28. package/app/apps/web/.next/server/app/api/insights/months-index/route_client-reference-manifest.js +3 -0
  29. package/app/apps/web/.next/server/app/api/insights/pdf/[key]/route/app-paths-manifest.json +3 -0
  30. package/app/apps/web/.next/server/app/api/insights/pdf/[key]/route/build-manifest.json +9 -0
  31. package/app/apps/web/.next/server/app/api/insights/pdf/[key]/route/server-reference-manifest.json +4 -0
  32. package/app/apps/web/.next/server/app/api/insights/pdf/[key]/route.js +6 -0
  33. package/app/apps/web/.next/server/app/api/insights/pdf/[key]/route.js.map +5 -0
  34. package/app/apps/web/.next/server/app/api/insights/pdf/[key]/route.js.nft.json +1 -0
  35. package/app/apps/web/.next/server/app/api/insights/pdf/[key]/route_client-reference-manifest.js +3 -0
  36. package/app/apps/web/.next/server/app/api/insights/route/app-paths-manifest.json +3 -0
  37. package/app/apps/web/.next/server/app/api/insights/route/build-manifest.json +9 -0
  38. package/app/apps/web/.next/server/app/api/insights/route/server-reference-manifest.json +4 -0
  39. package/app/apps/web/.next/server/app/api/insights/route.js +8 -0
  40. package/app/apps/web/.next/server/app/api/insights/route.js.map +5 -0
  41. package/app/apps/web/.next/server/app/api/insights/route.js.nft.json +1 -0
  42. package/app/apps/web/.next/server/app/api/insights/route_client-reference-manifest.js +3 -0
  43. package/app/apps/web/.next/server/app/api/insights/saved/[key]/route/app-paths-manifest.json +3 -0
  44. package/app/apps/web/.next/server/app/api/insights/saved/[key]/route/build-manifest.json +9 -0
  45. package/app/apps/web/.next/server/app/api/insights/saved/[key]/route/server-reference-manifest.json +4 -0
  46. package/app/apps/web/.next/server/app/api/insights/saved/[key]/route.js +6 -0
  47. package/app/apps/web/.next/server/app/api/insights/saved/[key]/route.js.map +5 -0
  48. package/app/apps/web/.next/server/app/api/insights/saved/[key]/route.js.nft.json +1 -0
  49. package/app/apps/web/.next/server/app/api/insights/saved/[key]/route_client-reference-manifest.js +3 -0
  50. package/app/apps/web/.next/server/app/api/insights/saved/route/app-paths-manifest.json +3 -0
  51. package/app/apps/web/.next/server/app/api/insights/saved/route/build-manifest.json +9 -0
  52. package/app/apps/web/.next/server/app/api/insights/saved/route/server-reference-manifest.json +4 -0
  53. package/app/apps/web/.next/server/app/api/insights/saved/route.js +6 -0
  54. package/app/apps/web/.next/server/app/api/insights/saved/route.js.map +5 -0
  55. package/app/apps/web/.next/server/app/api/insights/saved/route.js.nft.json +1 -0
  56. package/app/apps/web/.next/server/app/api/insights/saved/route_client-reference-manifest.js +3 -0
  57. package/app/apps/web/.next/server/app/api/insights/weeks-index/route/app-paths-manifest.json +3 -0
  58. package/app/apps/web/.next/server/app/api/insights/weeks-index/route/build-manifest.json +9 -0
  59. package/app/apps/web/.next/server/app/api/insights/weeks-index/route/server-reference-manifest.json +4 -0
  60. package/app/apps/web/.next/server/app/api/insights/weeks-index/route.js +7 -0
  61. package/app/apps/web/.next/server/app/api/insights/weeks-index/route.js.map +5 -0
  62. package/app/apps/web/.next/server/app/api/insights/weeks-index/route.js.nft.json +1 -0
  63. package/app/apps/web/.next/server/app/api/insights/weeks-index/route_client-reference-manifest.js +3 -0
  64. package/app/apps/web/.next/server/app/insights/[key]/page/app-paths-manifest.json +3 -0
  65. package/app/apps/web/.next/server/app/insights/[key]/page/build-manifest.json +17 -0
  66. package/app/apps/web/.next/server/app/insights/[key]/page/next-font-manifest.json +6 -0
  67. package/app/apps/web/.next/server/app/insights/[key]/page/react-loadable-manifest.json +1 -0
  68. package/app/apps/web/.next/server/app/insights/[key]/page/server-reference-manifest.json +4 -0
  69. package/app/apps/web/.next/server/app/insights/[key]/page.js +16 -0
  70. package/app/apps/web/.next/server/app/insights/[key]/page.js.map +5 -0
  71. package/app/apps/web/.next/server/app/insights/[key]/page.js.nft.json +1 -0
  72. package/app/apps/web/.next/server/app/insights/[key]/page_client-reference-manifest.js +3 -0
  73. package/app/apps/web/.next/server/app/insights/page/app-paths-manifest.json +3 -0
  74. package/app/apps/web/.next/server/app/insights/page/build-manifest.json +17 -0
  75. package/app/apps/web/.next/server/app/insights/page/next-font-manifest.json +6 -0
  76. package/app/apps/web/.next/server/app/insights/page/react-loadable-manifest.json +1 -0
  77. package/app/apps/web/.next/server/app/insights/page/server-reference-manifest.json +4 -0
  78. package/app/apps/web/.next/server/app/insights/page.js +16 -0
  79. package/app/apps/web/.next/server/app/insights/page.js.map +5 -0
  80. package/app/apps/web/.next/server/app/insights/page.js.nft.json +1 -0
  81. package/app/apps/web/.next/server/app/insights/page_client-reference-manifest.js +3 -0
  82. package/app/apps/web/.next/server/app/insights/print/[key]/page/app-paths-manifest.json +3 -0
  83. package/app/apps/web/.next/server/app/insights/print/[key]/page/build-manifest.json +17 -0
  84. package/app/apps/web/.next/server/app/insights/print/[key]/page/next-font-manifest.json +6 -0
  85. package/app/apps/web/.next/server/app/insights/print/[key]/page/react-loadable-manifest.json +1 -0
  86. package/app/apps/web/.next/server/app/insights/print/[key]/page/server-reference-manifest.json +4 -0
  87. package/app/apps/web/.next/server/app/insights/print/[key]/page.js +16 -0
  88. package/app/apps/web/.next/server/app/insights/print/[key]/page.js.map +5 -0
  89. package/app/apps/web/.next/server/app/insights/print/[key]/page.js.nft.json +1 -0
  90. package/app/apps/web/.next/server/app/insights/print/[key]/page_client-reference-manifest.js +3 -0
  91. package/app/apps/web/.next/server/app/page/build-manifest.json +3 -3
  92. package/app/apps/web/.next/server/app/page.js +3 -3
  93. package/app/apps/web/.next/server/app/page.js.nft.json +1 -1
  94. package/app/apps/web/.next/server/app/page_client-reference-manifest.js +1 -1
  95. package/app/apps/web/.next/server/app/parallelism/page/build-manifest.json +3 -3
  96. package/app/apps/web/.next/server/app/parallelism/page.js +1 -1
  97. package/app/apps/web/.next/server/app/parallelism/page.js.nft.json +1 -1
  98. package/app/apps/web/.next/server/app/parallelism/page_client-reference-manifest.js +1 -1
  99. package/app/apps/web/.next/server/app/projects/[slug]/page/build-manifest.json +3 -3
  100. package/app/apps/web/.next/server/app/projects/[slug]/page.js +2 -3
  101. package/app/apps/web/.next/server/app/projects/[slug]/page.js.nft.json +1 -1
  102. package/app/apps/web/.next/server/app/projects/[slug]/page_client-reference-manifest.js +1 -1
  103. package/app/apps/web/.next/server/app/projects/page/build-manifest.json +3 -3
  104. package/app/apps/web/.next/server/app/projects/page.js +1 -1
  105. package/app/apps/web/.next/server/app/projects/page.js.nft.json +1 -1
  106. package/app/apps/web/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  107. package/app/apps/web/.next/server/app/sessions/[id]/page/build-manifest.json +3 -3
  108. package/app/apps/web/.next/server/app/sessions/[id]/page.js +2 -2
  109. package/app/apps/web/.next/server/app/sessions/[id]/page.js.nft.json +1 -1
  110. package/app/apps/web/.next/server/app/sessions/[id]/page_client-reference-manifest.js +1 -1
  111. package/app/apps/web/.next/server/app/sessions/page/build-manifest.json +3 -3
  112. package/app/apps/web/.next/server/app/sessions/page.js +1 -1
  113. package/app/apps/web/.next/server/app/sessions/page.js.nft.json +1 -1
  114. package/app/apps/web/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
  115. package/app/apps/web/.next/server/app/usage/page/build-manifest.json +3 -3
  116. package/app/apps/web/.next/server/app/usage/page.js +1 -1
  117. package/app/apps/web/.next/server/app/usage/page.js.nft.json +1 -1
  118. package/app/apps/web/.next/server/app/usage/page_client-reference-manifest.js +1 -1
  119. package/app/apps/web/.next/server/app-paths-manifest.json +9 -0
  120. 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
  121. package/app/apps/web/.next/server/chunks/02dz_web__next-internal_server_app_api_insights_saved_[key]_route_actions_0a5j9yw.js +3 -0
  122. package/app/apps/web/.next/server/chunks/02dz_web__next-internal_server_app_api_insights_weeks-index_route_actions_0hh-1c7.js +3 -0
  123. package/app/apps/web/.next/server/chunks/{[externals]__0rv9ct5._.js → [externals]__0_xztrf._.js} +2 -2
  124. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0.hmj0z._.js +3 -0
  125. package/app/apps/web/.next/server/chunks/[root-of-the-server]__01h_g-p._.js +3 -0
  126. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0nh-z10._.js +3 -0
  127. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0op9af~._.js +1 -1
  128. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0ri7q~r._.js +3 -0
  129. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0rr1~~x._.js +3 -0
  130. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0s0gv.0._.js +3 -0
  131. package/app/apps/web/.next/server/chunks/apps_web__next-internal_server_app_api_insights_pdf_[key]_route_actions_0lu-lnx.js +3 -0
  132. package/app/apps/web/.next/server/chunks/apps_web__next-internal_server_app_api_insights_route_actions_0h8l5lz.js +3 -0
  133. package/app/apps/web/.next/server/chunks/apps_web__next-internal_server_app_api_insights_saved_route_actions_13rlblw.js +3 -0
  134. package/app/apps/web/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_00t2xj6.js +68 -0
  135. 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
  136. package/app/apps/web/.next/server/chunks/packages_parser_dist_fs_0bc7ptm.js +3 -0
  137. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__00z7zlm._.js +3 -0
  138. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__08.d5ia._.js +12 -0
  139. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0b323~k._.js +8 -0
  140. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0euu8sa._.js +3 -0
  141. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0gio_ck._.js +3 -0
  142. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0gx-4qa._.js +3 -0
  143. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0w52fbc._.js +3 -3
  144. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0z45d0g._.js +49 -0
  145. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__13itnvx._.js +3 -0
  146. package/app/apps/web/.next/server/chunks/ssr/_09n75_5._.js +1 -1
  147. package/app/apps/web/.next/server/chunks/ssr/_0r~__0v._.js +12 -0
  148. package/app/apps/web/.next/server/chunks/ssr/_0xies90._.js +1 -1
  149. package/app/apps/web/.next/server/chunks/ssr/apps_web__next-internal_server_app_insights_[key]_page_actions_0_z2~e_.js +3 -0
  150. package/app/apps/web/.next/server/chunks/ssr/apps_web__next-internal_server_app_insights_page_actions_0zjdm~n.js +3 -0
  151. package/app/apps/web/.next/server/chunks/ssr/apps_web__next-internal_server_app_insights_print_[key]_page_actions_0a853fw.js +3 -0
  152. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_06j3c.0._.js +1 -1
  153. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_insight-report_tsx_13q2a-c._.js +12 -0
  154. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_insights-view_tsx_11730ta._.js +6 -0
  155. package/app/apps/web/.next/server/chunks/ssr/node_modules_lucide-react_dist_esm_icons_0-lmm4p._.js +3 -0
  156. package/app/apps/web/.next/server/chunks/ssr/node_modules_lucide-react_dist_esm_icons_0nqz8dw._.js +3 -0
  157. package/app/apps/web/.next/server/chunks/ssr/node_modules_next_dist_0t-hj0x._.js +3 -0
  158. package/app/apps/web/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0vz7_px.js +4 -0
  159. package/app/apps/web/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_10cdrh2.js +4 -0
  160. package/app/apps/web/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_12n1lw_.js +4 -0
  161. package/app/apps/web/.next/server/chunks/ssr/packages_parser_dist_0-jvt~a._.js +1 -1
  162. package/app/apps/web/.next/server/chunks/ssr/packages_parser_dist_analytics_0k4dgst.js +1 -1
  163. package/app/apps/web/.next/server/functions-config-manifest.json +7 -1
  164. package/app/apps/web/.next/server/middleware-build-manifest.js +6 -6
  165. package/app/apps/web/.next/server/pages/500.html +1 -1
  166. package/app/apps/web/.next/server/server-reference-manifest.js +1 -1
  167. package/app/apps/web/.next/server/server-reference-manifest.json +1 -1
  168. package/app/apps/web/.next/static/chunks/03.mv4y4.w-vb.js +6 -0
  169. package/app/apps/web/.next/static/chunks/03_7xtpi9sv4g.js +4 -0
  170. package/app/apps/web/.next/static/chunks/03g5dw5eskotd.js +6 -0
  171. package/app/apps/web/.next/static/chunks/063oimt30jeo9.js +10 -0
  172. package/app/apps/web/.next/static/chunks/08c1pcrocqq5s.js +6 -0
  173. package/app/apps/web/.next/static/chunks/0e-wrhqh4h3r5.js +2 -0
  174. package/app/apps/web/.next/static/chunks/0fc6dk2qtokwh.js +1 -0
  175. package/app/apps/web/.next/static/chunks/0i7er7_bfab36.js +10 -0
  176. package/app/apps/web/.next/static/chunks/0ipytyi01h0ta.js +1 -0
  177. package/app/apps/web/.next/static/chunks/137uw2-qbro07.js +31 -0
  178. package/app/apps/web/.next/static/chunks/{turbopack-17n9y8y0h4q9f.js → turbopack-169w-1._23ra_.js} +1 -1
  179. package/app/apps/web/app/api/insights/months-index/route.ts +80 -0
  180. package/app/apps/web/app/api/insights/pdf/[key]/route.ts +45 -0
  181. package/app/apps/web/app/api/insights/route.ts +508 -0
  182. package/app/apps/web/app/api/insights/saved/[key]/route.ts +11 -0
  183. package/app/apps/web/app/api/insights/saved/route.ts +9 -0
  184. package/app/apps/web/app/api/insights/weeks-index/route.ts +95 -0
  185. package/app/apps/web/app/insights/[key]/page.tsx +59 -0
  186. package/app/apps/web/app/insights/page.tsx +7 -0
  187. package/app/apps/web/app/insights/print/[key]/page.tsx +73 -0
  188. package/app/apps/web/app/layout.tsx +2 -0
  189. package/app/apps/web/app/projects/[slug]/page.tsx +2 -0
  190. package/app/apps/web/app/sessions/[id]/page.tsx +27 -1
  191. package/app/apps/web/app/sessions/[id]/session-view.tsx +372 -793
  192. package/app/apps/web/app/sessions/[id]/team-tab/adapter.ts +633 -0
  193. package/app/apps/web/app/sessions/[id]/team-tab/format.ts +20 -0
  194. package/app/apps/web/app/sessions/[id]/team-tab/team-minimap.tsx +682 -0
  195. package/app/apps/web/app/sessions/[id]/team-tab/team-tab-client.tsx +78 -0
  196. package/app/apps/web/app/sessions/[id]/team-tab/team-table.tsx +796 -0
  197. package/app/apps/web/app/sessions/[id]/team-tab/turn-drawer.tsx +805 -0
  198. package/app/apps/web/app/sessions/[id]/tool-cards.tsx +461 -0
  199. package/app/apps/web/app/sessions/[id]/turn-steps.tsx +306 -0
  200. package/app/apps/web/app/sessions/sessions-grid.tsx +36 -5
  201. package/app/apps/web/components/insight-report.tsx +1031 -0
  202. package/app/apps/web/components/insights-view.tsx +647 -0
  203. package/app/apps/web/components/live-refresher.tsx +11 -2
  204. package/app/apps/web/components/live-sessions-widget.tsx +20 -6
  205. package/app/apps/web/components/sidebar.tsx +8 -0
  206. package/app/apps/web/components/team-badge.tsx +90 -0
  207. package/app/apps/web/eslint.config.mjs +2 -0
  208. package/app/apps/web/lib/ai/chrome-pdf.ts +64 -0
  209. package/app/apps/web/lib/ai/insights-prompt.ts +71 -0
  210. package/app/apps/web/lib/ai/saved-reports.ts +83 -0
  211. package/app/apps/web/lib/date-range.ts +11 -2
  212. package/app/apps/web/lib/use-live-events.ts +7 -2
  213. package/app/apps/web/package.json +3 -2
  214. package/app/apps/web/tsconfig.tsbuildinfo +1 -1
  215. package/dist/daemon-worker.js +2765 -11
  216. package/dist/index.js +3047 -253
  217. package/package.json +5 -3
  218. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__08cr2u9._.js +0 -3
  219. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0b..h_8._.js +0 -3
  220. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0cdd~8n._.js +0 -3
  221. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0ms7h0~._.js +0 -8
  222. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0~a~v00._.js +0 -3
  223. package/app/apps/web/.next/server/chunks/ssr/_0zv~jv-._.js +0 -12
  224. package/app/apps/web/.next/server/chunks/ssr/_12dtryh._.js +0 -12
  225. package/app/apps/web/.next/server/chunks/ssr/node_modules_0i2xw~e._.js +0 -3
  226. package/app/apps/web/.next/server/chunks/ssr/node_modules_lucide-react_dist_esm_icons_0.knjn.._.js +0 -3
  227. package/app/apps/web/.next/static/chunks/0257pdz1-imal.js +0 -2
  228. package/app/apps/web/.next/static/chunks/03fw7f2obcvq~.js +0 -6
  229. package/app/apps/web/.next/static/chunks/07oyxajlp3dvb.js +0 -6
  230. package/app/apps/web/.next/static/chunks/0a24elrqoqqep.js +0 -10
  231. package/app/apps/web/.next/static/chunks/0i4zodv86~~~i.js +0 -1
  232. package/app/apps/web/.next/static/chunks/0qmh881w772ef.js +0 -31
  233. package/app/apps/web/.next/static/chunks/0rbm60-qyshdm.js +0 -6
  234. package/app/apps/web/.next/static/chunks/0wdga7zbwvg70.js +0 -1
  235. /package/app/apps/web/.next/static/{2rEsiJssG30QsuNT0Lp9S → G_4ofKI9v9ARC_lb9JILZ}/_buildManifest.js +0 -0
  236. /package/app/apps/web/.next/static/{2rEsiJssG30QsuNT0Lp9S → G_4ofKI9v9ARC_lb9JILZ}/_clientMiddlewareManifest.js +0 -0
  237. /package/app/apps/web/.next/static/{2rEsiJssG30QsuNT0Lp9S → G_4ofKI9v9ARC_lb9JILZ}/_ssgManifest.js +0 -0
@@ -0,0 +1,508 @@
1
+ /**
2
+ * Insights agent — structured ReportData pipeline with persistence.
3
+ *
4
+ * POST /api/insights
5
+ * { range_type: "prior_week" | "week" | "4weeks_completed" | "4weeks" | "custom",
6
+ * since?: "YYYY-MM-DD", until?: "YYYY-MM-DD" }
7
+ *
8
+ * Streams SSE:
9
+ * { type: "status", phase: "data"|"analyst"|"compose", text }
10
+ * { type: "report", report }
11
+ * { type: "saved", key, saved_at }
12
+ * { type: "done" }
13
+ * { type: "error", message }
14
+ */
15
+ import { listSessions, getSession, loadUsageByDay } from "@claude-lens/parser/fs";
16
+ import {
17
+ buildCapsule,
18
+ buildPeriodBundle,
19
+ aggregateConcurrency,
20
+ calendarWeek,
21
+ priorCalendarWeek,
22
+ last4CompletedWeeks,
23
+ calendarMonth,
24
+ priorCalendarMonth,
25
+ computeBurstsFromSessions,
26
+ type SessionCapsule,
27
+ type PeriodBundle,
28
+ } from "@claude-lens/parser";
29
+ import { INSIGHTS_SYSTEM_PROMPT } from "@/lib/ai/insights-prompt";
30
+ import { saveReport, keyForRange, listSavedReports, getSavedReport } from "@/lib/ai/saved-reports";
31
+ import { spawn } from "node:child_process";
32
+ import type { ReportData, IconKey } from "@/components/insight-report";
33
+
34
+ export const dynamic = "force-dynamic";
35
+ export const runtime = "nodejs";
36
+
37
+ type Narrative = {
38
+ archetype: { label: string; icon: string; tagline: string; why: string; vs_usual?: string };
39
+ theme_headline: string;
40
+ shipped_summaries: Record<string, string>;
41
+ patterns: { icon: string; title: string; stat: string; note: string }[];
42
+ concurrency_insight: string;
43
+ concurrency_suggestion: string;
44
+ outlier_notes: {
45
+ longest_run?: string;
46
+ fastest_ship?: string;
47
+ most_errors?: string;
48
+ wandered?: string;
49
+ };
50
+ suggestion_headline: string;
51
+ suggestion_body: string;
52
+ };
53
+
54
+ const VALID_ICONS: IconKey[] = [
55
+ "BrainCircuit", "ClipboardList", "Compass", "GitCommit", "Layers3",
56
+ "Network", "Repeat", "Rocket", "Sparkles", "TrendingDown", "TrendingUp",
57
+ "Users", "Zap",
58
+ ];
59
+
60
+ function safeIcon(v: string): IconKey {
61
+ return VALID_ICONS.includes(v as IconKey) ? (v as IconKey) : "Sparkles";
62
+ }
63
+
64
+ function resolveRange(body: { range_type?: string; since?: string; until?: string }): {
65
+ start: Date; end: Date; range_type: ReportData["range_type"]; in_progress: boolean;
66
+ } {
67
+ switch (body.range_type) {
68
+ case "prior_week": {
69
+ const r = priorCalendarWeek();
70
+ return { ...r, range_type: "week", in_progress: false };
71
+ }
72
+ case "4weeks_completed": {
73
+ const r = last4CompletedWeeks();
74
+ return { ...r, range_type: "4weeks", in_progress: false };
75
+ }
76
+ case "4weeks": {
77
+ const end = calendarWeek().end;
78
+ const start = new Date(end);
79
+ start.setDate(end.getDate() - 27);
80
+ return { start, end, range_type: "4weeks", in_progress: true };
81
+ }
82
+ case "prior_month": {
83
+ const r = priorCalendarMonth();
84
+ return { ...r, range_type: "month", in_progress: false };
85
+ }
86
+ case "month": {
87
+ const r = calendarMonth();
88
+ return { ...r, range_type: "month", in_progress: true };
89
+ }
90
+ case "custom": {
91
+ if (!body.since || !body.until) throw new Error("custom range requires since + until");
92
+ const start = new Date(body.since);
93
+ const end = new Date(body.until);
94
+ // Normalise: pin Mon-Sun 7-day windows to `week`, and full-calendar-month
95
+ // windows to `month`, so they participate in baselines and pickers with
96
+ // the right key instead of living off in custom-* land.
97
+ const days = Math.round((end.getTime() - start.getTime()) / 86_400_000) + 1;
98
+ const isCalendarWeek = days === 7 && start.getDay() === 1;
99
+ const monthEnd = new Date(start.getFullYear(), start.getMonth() + 1, 0);
100
+ const isCalendarMonth = start.getDate() === 1
101
+ && end.getFullYear() === monthEnd.getFullYear()
102
+ && end.getMonth() === monthEnd.getMonth()
103
+ && end.getDate() === monthEnd.getDate();
104
+ const normalised: "week" | "month" | "custom" =
105
+ isCalendarWeek ? "week" : isCalendarMonth ? "month" : "custom";
106
+ return {
107
+ start, end,
108
+ range_type: normalised,
109
+ in_progress: false,
110
+ };
111
+ }
112
+ case "week":
113
+ default: {
114
+ const r = calendarWeek();
115
+ return { ...r, range_type: "week", in_progress: true };
116
+ }
117
+ }
118
+ }
119
+
120
+ export async function POST(request: Request) {
121
+ let body: { range_type?: string; since?: string; until?: string };
122
+ try {
123
+ body = await request.json();
124
+ } catch {
125
+ return Response.json({ error: "invalid JSON" }, { status: 400 });
126
+ }
127
+
128
+ const encoder = new TextEncoder();
129
+ const stream = new ReadableStream({
130
+ async start(controller) {
131
+ let closed = false;
132
+ function send(data: Record<string, unknown>) {
133
+ if (closed) return;
134
+ try { controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`)); }
135
+ catch { closed = true; }
136
+ }
137
+ const status = (phase: "data" | "analyst" | "compose", text: string) =>
138
+ send({ type: "status", phase, text });
139
+ function finish() {
140
+ if (!closed) {
141
+ try { controller.close(); } catch { /* already closed */ }
142
+ closed = true;
143
+ }
144
+ }
145
+
146
+ const t0 = Date.now();
147
+
148
+ try {
149
+ const range = resolveRange(body);
150
+ const rangeLabel = range.range_type === "week"
151
+ ? (range.in_progress ? "this week (in progress)" : "last week")
152
+ : range.range_type === "4weeks"
153
+ ? (range.in_progress ? "last 4 weeks (incl. current)" : "last 4 completed weeks")
154
+ : range.range_type === "month"
155
+ ? (range.in_progress ? "this month (in progress)" : "last month")
156
+ : "custom range";
157
+ status("data", `Window: ${rangeLabel}`);
158
+
159
+ // Phase 1 · Data
160
+ const metas = await listSessions({ limit: 10000 });
161
+ const inRange = metas.filter((m) => {
162
+ if (!m.firstTimestamp) return false;
163
+ const t = new Date(m.firstTimestamp).getTime();
164
+ return t >= range.start.getTime() && t <= range.end.getTime();
165
+ });
166
+ status("data", `${inRange.length} sessions found in range`);
167
+
168
+ const caps: SessionCapsule[] = [];
169
+ let trivial = 0;
170
+ const CONCURRENCY = 8;
171
+ for (let i = 0; i < inRange.length; i += CONCURRENCY) {
172
+ const slice = inRange.slice(i, i + CONCURRENCY);
173
+ const results = await Promise.all(slice.map(async (m) => {
174
+ try {
175
+ const d = await getSession(m.id);
176
+ if (!d) return null;
177
+ return buildCapsule(d, { compact: true });
178
+ } catch { return null; }
179
+ }));
180
+ for (const cap of results) {
181
+ if (!cap) continue;
182
+ if (cap.outcome === "trivial") trivial++;
183
+ else caps.push(cap);
184
+ }
185
+ status("data", `Built ${caps.length}/${inRange.length} capsules (${trivial} trivial)`);
186
+ }
187
+
188
+ status("data", `Aggregating ${caps.length} sessions over ${Math.round((range.end.getTime() - range.start.getTime()) / 86400000) + 1} days`);
189
+ const bundle: PeriodBundle = buildPeriodBundle(caps, {
190
+ period: range,
191
+ trivial_dropped: trivial,
192
+ sessions_total: inRange.length,
193
+ });
194
+
195
+ status("data", "Computing concurrency bursts");
196
+ bundle.concurrency = aggregateConcurrency(computeBurstsFromSessions(inRange), range);
197
+
198
+ status("data", "Loading license utilization");
199
+ try { bundle.usage = await loadUsageByDay(range.start, range.end); }
200
+ catch { /* optional */ }
201
+
202
+ status("data", "Loading prior reports for baseline comparison");
203
+ const priorBaseline = await loadPriorWeeks(4);
204
+
205
+ // Phase 2 · Analyst — stringify the payload once; reuse for status, prompt, and context_kb.
206
+ const payloadJson = JSON.stringify({
207
+ period: bundle.period,
208
+ aggregates: bundle,
209
+ capsules: caps,
210
+ prior: priorBaseline,
211
+ });
212
+ const payloadKb = Math.round(payloadJson.length / 1024);
213
+ status("analyst", `Sending ${payloadKb} KB to Claude (sonnet)`);
214
+
215
+ const narrative = await callAnalyst(payloadJson, status);
216
+ if (!narrative) {
217
+ send({ type: "error", message: "Analyst returned no parseable JSON." });
218
+ finish();
219
+ return;
220
+ }
221
+
222
+ // Phase 3 · Compose
223
+ status("compose", "Merging aggregates + narrative");
224
+ const report = mergeReport(bundle, narrative, range, payloadKb, priorBaseline);
225
+ report.meta.pipeline_ms = Date.now() - t0;
226
+
227
+ send({ type: "report", report });
228
+
229
+ // Persistence
230
+ if (!range.in_progress) {
231
+ status("compose", "Saving report to disk");
232
+ const key = keyForRange(range.range_type, bundle.period.start);
233
+ try {
234
+ await saveReport(key, report);
235
+ send({ type: "saved", key, saved_at: new Date().toISOString() });
236
+ } catch (e) {
237
+ send({ type: "status", phase: "compose", text: `(save failed: ${(e as Error).message})` });
238
+ }
239
+ }
240
+
241
+ send({ type: "done" });
242
+ finish();
243
+ } catch (err) {
244
+ send({ type: "error", message: (err as Error).message });
245
+ finish();
246
+ }
247
+ },
248
+ });
249
+
250
+ return new Response(stream, {
251
+ headers: {
252
+ "Content-Type": "text/event-stream",
253
+ "Cache-Control": "no-cache, no-transform",
254
+ Connection: "keep-alive",
255
+ "X-Accel-Buffering": "no",
256
+ },
257
+ });
258
+ }
259
+
260
+ // ──────────────────────────────────────────────────────────────────
261
+ // Spawn claude -p, parse JSON
262
+ // ──────────────────────────────────────────────────────────────────
263
+
264
+ async function callAnalyst(
265
+ payloadJson: string,
266
+ status: (phase: "data" | "analyst" | "compose", text: string) => void,
267
+ ): Promise<Narrative | null> {
268
+ const userPrompt = `Period payload:\n\n${payloadJson}\n\n---\n\nReturn the JSON per your system instructions.`;
269
+
270
+ return new Promise((resolve) => {
271
+ const args = [
272
+ "-p",
273
+ "--output-format", "stream-json",
274
+ "--verbose",
275
+ "--model", "sonnet",
276
+ "--tools", "",
277
+ "--disable-slash-commands",
278
+ "--no-session-persistence",
279
+ "--setting-sources", "",
280
+ "--append-system-prompt", INSIGHTS_SYSTEM_PROMPT,
281
+ ];
282
+ const proc = spawn("claude", args, { stdio: ["pipe", "pipe", "pipe"], env: { ...process.env } });
283
+ proc.stdin.write(userPrompt);
284
+ proc.stdin.end();
285
+
286
+ let buffer = "";
287
+ let lastReportedKb = -1;
288
+ const phaseStart = Date.now();
289
+ proc.stdout.on("data", (chunk: Buffer) => {
290
+ const text = chunk.toString("utf8");
291
+ for (const line of text.split("\n")) {
292
+ const trimmed = line.trim();
293
+ if (!trimmed) continue;
294
+ try {
295
+ const obj = JSON.parse(trimmed) as Record<string, unknown>;
296
+ if (obj.type === "assistant") {
297
+ const msg = obj.message as Record<string, unknown> | undefined;
298
+ const content = msg?.content as Array<Record<string, unknown>> | undefined;
299
+ if (Array.isArray(content)) {
300
+ for (const block of content) {
301
+ if (block.type === "text" && typeof block.text === "string") {
302
+ buffer += block.text;
303
+ const kb = Math.floor(buffer.length / 1024);
304
+ if (kb > lastReportedKb) {
305
+ lastReportedKb = kb;
306
+ const secs = Math.round((Date.now() - phaseStart) / 1000);
307
+ status("analyst", `Claude composing… ${buffer.length.toLocaleString()} chars (${secs}s)`);
308
+ }
309
+ }
310
+ }
311
+ }
312
+ }
313
+ } catch { /* skip non-JSON framing */ }
314
+ }
315
+ });
316
+ proc.on("close", () => {
317
+ const json = extractJsonBlock(buffer);
318
+ if (!json) { resolve(null); return; }
319
+ try { resolve(JSON.parse(json) as Narrative); }
320
+ catch { resolve(null); }
321
+ });
322
+ proc.on("error", () => resolve(null));
323
+ });
324
+ }
325
+
326
+ function extractJsonBlock(s: string): string | null {
327
+ const fence = /```json\s*([\s\S]*?)```/m.exec(s);
328
+ if (fence) return fence[1]!.trim();
329
+ const first = s.indexOf("{");
330
+ const last = s.lastIndexOf("}");
331
+ if (first !== -1 && last > first) return s.slice(first, last + 1);
332
+ return null;
333
+ }
334
+
335
+ // ──────────────────────────────────────────────────────────────────
336
+ // Merge → ReportData
337
+ // ──────────────────────────────────────────────────────────────────
338
+
339
+ type PriorWeek = {
340
+ period_label: string;
341
+ archetype: string;
342
+ sessions: number;
343
+ prs: number;
344
+ subagents: number;
345
+ agent_minutes: number;
346
+ };
347
+
348
+ async function loadPriorWeeks(limit: number): Promise<PriorWeek[]> {
349
+ const all = await listSavedReports();
350
+ const weeks = all.filter((r) => r.key.startsWith("week-")).slice(0, limit);
351
+ const out: PriorWeek[] = [];
352
+ for (const meta of weeks) {
353
+ const r = await getSavedReport(meta.key);
354
+ if (!r) continue;
355
+ out.push({
356
+ period_label: r.period_label,
357
+ archetype: r.archetype.label,
358
+ sessions: r.meta.sessions_used,
359
+ prs: r.shipped.length,
360
+ subagents: r.concurrency.peak, // rough proxy for ambition
361
+ agent_minutes: r.days.reduce((a, d) => a + d.agent_minutes, 0),
362
+ });
363
+ }
364
+ return out;
365
+ }
366
+
367
+ function mergeReport(
368
+ bundle: PeriodBundle,
369
+ n: Narrative,
370
+ range: { in_progress: boolean },
371
+ contextKb: number,
372
+ priorWeeks: PriorWeek[],
373
+ ): ReportData {
374
+ const skillEntries = Object.entries(bundle.skills_total).sort((a, b) => b[1] - a[1]);
375
+ const top_skills = skillEntries.slice(0, 6).map(([name, count]) => ({ name, count }));
376
+
377
+ const days = bundle.by_day.map((d, i) => {
378
+ const concOfDay = bundle.concurrency?.by_day[i];
379
+ const useOfDay = bundle.usage?.by_day.find((u) => u.date === d.date);
380
+ return {
381
+ day_name: d.day_name,
382
+ date_label: new Date(d.date).toLocaleDateString("en-US", { month: "short", day: "numeric" }),
383
+ agent_minutes: Math.round(d.agent_min),
384
+ sessions: d.sessions,
385
+ concurrency_peak: concOfDay?.peak ?? 0,
386
+ has_cross_project: Boolean(concOfDay?.has_cross_project),
387
+ plan_util_pct: useOfDay?.peak_util_pct ?? 0,
388
+ is_partial: new Date(d.date).getTime() > Date.now(),
389
+ };
390
+ });
391
+
392
+ const projects = bundle.project_shares.map((p) => ({
393
+ name: p.name,
394
+ display_name: p.display_name,
395
+ agent_minutes: Math.round(p.agent_min),
396
+ share_pct: p.share_pct,
397
+ prs: p.prs,
398
+ commits: p.commits,
399
+ }));
400
+
401
+ const shipped = bundle.shipped_prs.map((s) => ({
402
+ title: s.title,
403
+ project: s.project,
404
+ duration_label: fmtDuration(s.active_min),
405
+ commits: s.commits,
406
+ subagents: s.subagents,
407
+ flags: s.flags,
408
+ summary: n.shipped_summaries[s.session_id] ?? "",
409
+ }));
410
+
411
+ const outliers: ReportData["outliers"] = [];
412
+ if (bundle.outliers.longest_run) {
413
+ outliers.push({
414
+ label: "Longest run",
415
+ detail: fmtDuration(bundle.outliers.longest_run.active_min),
416
+ note: n.outlier_notes.longest_run ?? bundle.outliers.longest_run.project,
417
+ });
418
+ }
419
+ if (bundle.outliers.fastest_ship) {
420
+ outliers.push({
421
+ label: "Fastest ship",
422
+ detail: fmtDuration(bundle.outliers.fastest_ship.active_min),
423
+ note: n.outlier_notes.fastest_ship ?? bundle.outliers.fastest_ship.pr_title,
424
+ });
425
+ }
426
+ if (bundle.outliers.most_errors) {
427
+ outliers.push({
428
+ label: "Most errors",
429
+ detail: String(bundle.outliers.most_errors.tool_errors),
430
+ note: n.outlier_notes.most_errors ?? bundle.outliers.most_errors.project,
431
+ });
432
+ }
433
+ if (bundle.outliers.wandered) {
434
+ outliers.push({
435
+ label: "Wandered",
436
+ detail: fmtDuration(bundle.outliers.wandered.active_min),
437
+ note: n.outlier_notes.wandered ?? bundle.outliers.wandered.project,
438
+ });
439
+ }
440
+
441
+ const in_progress_suffix = range.in_progress ? " · in progress" : "";
442
+ const periodPrefix =
443
+ bundle.period.range_type === "4weeks" ? "Last 4 weeks ·"
444
+ : bundle.period.range_type === "month" ? "Month of"
445
+ : "Week of";
446
+ const sublabel =
447
+ bundle.period.range_type === "4weeks" ? "Calendar 4-week rollup"
448
+ : bundle.period.range_type === "month" ? "Calendar month"
449
+ : "Calendar week · Mon–Sun";
450
+
451
+ return {
452
+ period_label: `${periodPrefix} ${bundle.period.label}`,
453
+ period_sublabel: `${sublabel}${in_progress_suffix}`,
454
+ range_type: bundle.period.range_type,
455
+ archetype: {
456
+ label: n.archetype.label,
457
+ icon: safeIcon(n.archetype.icon),
458
+ tagline: n.archetype.tagline,
459
+ why: n.archetype.why,
460
+ vs_usual: n.archetype.vs_usual,
461
+ },
462
+ top_skills,
463
+ days,
464
+ theme_headline: n.theme_headline,
465
+ projects,
466
+ shipped,
467
+ patterns: n.patterns.map((p) => ({
468
+ icon: safeIcon(p.icon), title: p.title, stat: p.stat, note: p.note,
469
+ })),
470
+ concurrency: {
471
+ multi_agent_days: bundle.concurrency?.multi_agent_days ?? 0,
472
+ peak: bundle.concurrency?.peak ?? 0,
473
+ peak_day: bundle.concurrency?.peak_day
474
+ ? new Date(bundle.concurrency.peak_day).toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric" })
475
+ : "—",
476
+ cross_project_days: bundle.concurrency?.cross_project_days ?? 0,
477
+ insight: n.concurrency_insight,
478
+ suggestion: n.concurrency_suggestion,
479
+ },
480
+ outliers,
481
+ suggestion_headline: n.suggestion_headline,
482
+ suggestion_body: n.suggestion_body,
483
+ prior_weeks: priorWeeks,
484
+ saved_reports: [],
485
+ meta: {
486
+ generated_at: new Date().toISOString().replace("T", " ").slice(0, 16),
487
+ sessions_total: bundle.counts.sessions_total,
488
+ sessions_used: bundle.counts.substantive,
489
+ trivial_dropped: bundle.counts.trivial_dropped,
490
+ model: "claude-sonnet-4-6",
491
+ pipeline_ms: 0,
492
+ context_kb: contextKb,
493
+ },
494
+ };
495
+ }
496
+
497
+ function fmtDuration(minutes: number): string {
498
+ if (minutes < 60) {
499
+ const whole = Math.floor(minutes);
500
+ const secs = Math.round((minutes - whole) * 60);
501
+ if (whole === 0 && secs > 0) return `${secs}s`;
502
+ if (secs === 0) return `${whole}m`;
503
+ return `${whole}m ${secs}s`;
504
+ }
505
+ const h = Math.floor(minutes / 60);
506
+ const m = Math.round(minutes - h * 60);
507
+ return `${h}h ${m}m`;
508
+ }
@@ -0,0 +1,11 @@
1
+ import { getSavedReport } from "@/lib/ai/saved-reports";
2
+
3
+ export const dynamic = "force-dynamic";
4
+ export const runtime = "nodejs";
5
+
6
+ export async function GET(_req: Request, ctx: { params: Promise<{ key: string }> }) {
7
+ const { key } = await ctx.params;
8
+ const report = await getSavedReport(key);
9
+ if (!report) return Response.json({ error: "not found" }, { status: 404 });
10
+ return Response.json({ report });
11
+ }
@@ -0,0 +1,9 @@
1
+ import { listSavedReports } from "@/lib/ai/saved-reports";
2
+
3
+ export const dynamic = "force-dynamic";
4
+ export const runtime = "nodejs";
5
+
6
+ export async function GET() {
7
+ const reports = await listSavedReports();
8
+ return Response.json({ reports });
9
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Weeks index — drives the week picker in /insights.
3
+ *
4
+ * Returns the last N calendar weeks (Mon-Sun, newest first) with:
5
+ * - isoweek + period label
6
+ * - session count observed in that window (raw SessionMeta filter)
7
+ * - in_progress flag for this week
8
+ * - saved_key when a report exists on disk
9
+ * - archetype_label + sessions_used from the saved report, for display
10
+ */
11
+ import { listSessions } from "@claude-lens/parser/fs";
12
+ import { calendarWeek, priorCalendarWeek } from "@claude-lens/parser";
13
+ import { listSavedReports, type SavedReportMeta } from "@/lib/ai/saved-reports";
14
+
15
+ export const dynamic = "force-dynamic";
16
+ export const runtime = "nodejs";
17
+
18
+ type WeekRow = {
19
+ iso_week: number;
20
+ start: string; // "2026-04-13"
21
+ end: string;
22
+ label: string; // "Apr 13 – Apr 19"
23
+ sessions: number;
24
+ in_progress: boolean;
25
+ saved_key: string | null;
26
+ archetype_label?: string;
27
+ sessions_used?: number;
28
+ prs?: number;
29
+ };
30
+
31
+ function isoDay(d: Date): string {
32
+ const y = d.getFullYear();
33
+ const m = String(d.getMonth() + 1).padStart(2, "0");
34
+ const day = String(d.getDate()).padStart(2, "0");
35
+ return `${y}-${m}-${day}`;
36
+ }
37
+
38
+ function isoWeekNo(d: Date): number {
39
+ const t = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
40
+ t.setUTCDate(t.getUTCDate() + 4 - (t.getUTCDay() || 7));
41
+ const yearStart = new Date(Date.UTC(t.getUTCFullYear(), 0, 1));
42
+ return Math.ceil((((t.getTime() - yearStart.getTime()) / 86_400_000) + 1) / 7);
43
+ }
44
+
45
+ function label(start: Date, end: Date): string {
46
+ const fmt = (d: Date) => d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
47
+ return `${fmt(start)} – ${fmt(end)}`;
48
+ }
49
+
50
+ export async function GET(req: Request) {
51
+ const url = new URL(req.url);
52
+ const count = Math.min(26, Math.max(1, parseInt(url.searchParams.get("count") ?? "12", 10)));
53
+
54
+ const [metas, saved] = await Promise.all([listSessions({ limit: 10000 }), listSavedReports()]);
55
+ const savedByKey = new Map<string, SavedReportMeta>(saved.map((s) => [s.key, s]));
56
+
57
+ const current = calendarWeek();
58
+ const prior = priorCalendarWeek();
59
+
60
+ const weeks: WeekRow[] = [];
61
+ for (let i = 0; i < count; i++) {
62
+ let start: Date, end: Date, in_progress: boolean;
63
+ if (i === 0) {
64
+ start = current.start; end = current.end; in_progress = true;
65
+ } else {
66
+ const offset = (i - 1) * 7;
67
+ start = new Date(prior.start); start.setDate(prior.start.getDate() - offset);
68
+ end = new Date(prior.end); end.setDate(prior.end.getDate() - offset);
69
+ in_progress = false;
70
+ }
71
+ const startMs = start.getTime();
72
+ const endMs = new Date(end).setHours(23, 59, 59, 999);
73
+ const sessions = metas.filter((m) => {
74
+ if (!m.firstTimestamp) return false;
75
+ const t = Date.parse(m.firstTimestamp);
76
+ return !Number.isNaN(t) && t >= startMs && t <= endMs;
77
+ }).length;
78
+ const startKey = isoDay(start);
79
+ const savedMeta = savedByKey.get(`week-${startKey}`);
80
+ weeks.push({
81
+ iso_week: isoWeekNo(start),
82
+ start: startKey,
83
+ end: isoDay(end),
84
+ label: label(start, end),
85
+ sessions,
86
+ in_progress,
87
+ saved_key: savedMeta ? savedMeta.key : null,
88
+ archetype_label: savedMeta?.archetype_label,
89
+ sessions_used: savedMeta?.sessions_used,
90
+ prs: savedMeta?.prs,
91
+ });
92
+ }
93
+
94
+ return Response.json({ weeks });
95
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Saved-report viewer. Deep-linkable, bookmarkable, server-rendered.
3
+ *
4
+ * /insights → history + picker (client-state machine)
5
+ * /insights/[key] → this page — reads the saved JSON off disk
6
+ * /insights/print/[key] → headless-Chrome target for PDF export
7
+ */
8
+ import Link from "next/link";
9
+ import { notFound } from "next/navigation";
10
+ import { ArrowLeft } from "lucide-react";
11
+ import { InsightReport } from "@/components/insight-report";
12
+ import { getSavedReport } from "@/lib/ai/saved-reports";
13
+
14
+ export const dynamic = "force-dynamic";
15
+
16
+ export default async function SavedInsightPage({
17
+ params,
18
+ }: { params: Promise<{ key: string }> }) {
19
+ const { key } = await params;
20
+ const report = await getSavedReport(key);
21
+ if (!report) notFound();
22
+
23
+ return (
24
+ <div>
25
+ <div className="no-print" style={topNavStyle}>
26
+ <Link href="/insights" style={backBtnStyle}>
27
+ <ArrowLeft size={12} /> Reports
28
+ </Link>
29
+ <span style={{ fontSize: 11, color: "var(--af-text-tertiary)", fontFamily: "var(--font-mono)" }}>
30
+ saved · key: {key}
31
+ </span>
32
+ </div>
33
+ <InsightReport data={report} savedKey={key} />
34
+ </div>
35
+ );
36
+ }
37
+
38
+ const topNavStyle: React.CSSProperties = {
39
+ maxWidth: 980,
40
+ margin: "0 auto",
41
+ padding: "16px 44px 0",
42
+ display: "flex",
43
+ alignItems: "center",
44
+ gap: 12,
45
+ };
46
+
47
+ const backBtnStyle: React.CSSProperties = {
48
+ display: "inline-flex",
49
+ alignItems: "center",
50
+ gap: 6,
51
+ padding: "7px 12px",
52
+ border: "1px solid var(--af-border)",
53
+ borderRadius: 8,
54
+ background: "var(--af-surface)",
55
+ color: "var(--af-text)",
56
+ fontSize: 12,
57
+ fontWeight: 500,
58
+ textDecoration: "none",
59
+ };
@@ -0,0 +1,7 @@
1
+ import { InsightsView } from "@/components/insights-view";
2
+
3
+ export const dynamic = "force-dynamic";
4
+
5
+ export default function InsightsPage() {
6
+ return <InsightsView />;
7
+ }