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
@@ -0,0 +1,1031 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import {
5
+ ArrowRight,
6
+ BrainCircuit,
7
+ Check,
8
+ ClipboardList,
9
+ Compass,
10
+ Download,
11
+ FileText,
12
+ GitCommit,
13
+ Layers3,
14
+ Lightbulb,
15
+ Network,
16
+ Repeat,
17
+ Rocket,
18
+ Share2,
19
+ Sparkles,
20
+ TrendingDown,
21
+ TrendingUp,
22
+ Users,
23
+ Zap,
24
+ } from "lucide-react";
25
+
26
+ const ICON_MAP = {
27
+ BrainCircuit, ClipboardList, Compass, GitCommit, Layers3, Network, Repeat,
28
+ Rocket, Sparkles, TrendingDown, TrendingUp, Users, Zap,
29
+ } as const;
30
+ export type IconKey = keyof typeof ICON_MAP;
31
+
32
+ // ──────────────────────────────────────────────────────────────────
33
+ // Types
34
+ // ──────────────────────────────────────────────────────────────────
35
+
36
+ export type ReportData = {
37
+ period_label: string; // "Week of Apr 14 — Apr 20"
38
+ period_sublabel: string; // "Calendar week · in progress"
39
+ range_type: "week" | "4weeks" | "month" | "custom";
40
+
41
+ archetype: {
42
+ label: string; // "Orchestration conductor"
43
+ icon: IconKey;
44
+ tagline: string; // "deep-dive + heavy subagent usage"
45
+ why: string; // 1 supporting sentence
46
+ vs_usual?: string; // "More orchestrated than your usual baseline"
47
+ };
48
+
49
+ top_skills: { name: string; count: number; vs_prior?: number }[];
50
+
51
+ // Daily day-by-day strip (Mon → Sun when range=week)
52
+ days: {
53
+ day_name: string; // "Mon"
54
+ date_label: string; // "Apr 14"
55
+ agent_minutes: number;
56
+ sessions: number;
57
+ concurrency_peak: number; // ×N parallel
58
+ has_cross_project: boolean;
59
+ plan_util_pct: number; // 0..100
60
+ is_partial?: boolean; // future / in-progress day
61
+ }[];
62
+
63
+ theme_headline: string; // "Fleetlens Team Edition sprint"
64
+
65
+ projects: {
66
+ name: string;
67
+ display_name: string;
68
+ agent_minutes: number;
69
+ share_pct: number;
70
+ prs: number;
71
+ commits: number;
72
+ }[];
73
+
74
+ shipped: {
75
+ title: string;
76
+ project: string;
77
+ duration_label: string;
78
+ commits: number;
79
+ subagents: number;
80
+ flags: string[];
81
+ summary: string;
82
+ }[];
83
+
84
+ patterns: {
85
+ icon: IconKey;
86
+ title: string;
87
+ stat: string;
88
+ note: string;
89
+ }[];
90
+
91
+ concurrency: {
92
+ multi_agent_days: number;
93
+ peak: number;
94
+ peak_day: string;
95
+ cross_project_days: number;
96
+ insight: string;
97
+ suggestion: string;
98
+ };
99
+
100
+ outliers: { label: string; detail: string; note: string }[];
101
+
102
+ suggestion_headline: string;
103
+ suggestion_body: string;
104
+
105
+ prior_weeks: {
106
+ period_label: string;
107
+ archetype: string;
108
+ sessions: number;
109
+ prs: number;
110
+ subagents: number;
111
+ agent_minutes: number;
112
+ }[];
113
+
114
+ saved_reports: { id: string; period_label: string; note?: string; current?: boolean }[];
115
+
116
+ meta: {
117
+ generated_at: string;
118
+ sessions_total: number;
119
+ sessions_used: number;
120
+ trivial_dropped: number;
121
+ model: string;
122
+ pipeline_ms: number;
123
+ context_kb: number;
124
+ };
125
+ };
126
+
127
+ const FLAG_TONE: Record<string, { label: string; tone: "good" | "warn" | "danger" | "neutral" }> = {
128
+ fast_ship: { label: "fast ship", tone: "good" },
129
+ plan_used: { label: "plan used", tone: "good" },
130
+ orchestrated: { label: "orchestrated", tone: "good" },
131
+ long_autonomous: { label: "long autonomous", tone: "neutral" },
132
+ loop_suspected: { label: "loop suspected", tone: "warn" },
133
+ high_errors: { label: "high errors", tone: "warn" },
134
+ interrupt_heavy: { label: "interrupt-heavy", tone: "danger" },
135
+ };
136
+
137
+ // ──────────────────────────────────────────────────────────────────
138
+ // Top-level component
139
+ // ──────────────────────────────────────────────────────────────────
140
+
141
+ export function InsightReport({ data, savedKey }: { data: ReportData; savedKey?: string | null }) {
142
+ return (
143
+ <>
144
+ <PrintStyles />
145
+ <div className="report-root" style={rootStyle}>
146
+ <Toolbar data={data} savedKey={savedKey ?? null} />
147
+
148
+ {/* Section 1 · ARCHETYPE */}
149
+ <Section index={1} title="Who you were" kicker="Your working style, distilled" anchor="archetype">
150
+ <ArchetypeHero data={data} />
151
+ <SkillsStrip skills={data.top_skills} />
152
+ </Section>
153
+
154
+ {/* Section 2 · THE WEEK AT A GLANCE */}
155
+ <Section index={2} title="The week at a glance" kicker={`${data.period_label} · ${data.theme_headline}`} anchor="glance">
156
+ <DayStrip days={data.days} />
157
+ <PlanUtilRow days={data.days} />
158
+ </Section>
159
+
160
+ {/* Section 3 · WHERE YOUR TIME WENT */}
161
+ <Section index={3} title="Where your time went" kicker="Projects and shipped work" anchor="time">
162
+ <ProjectBars projects={data.projects} />
163
+ <ShippedList items={data.shipped} />
164
+ </Section>
165
+
166
+ {/* Section 4 · HOW YOU WORKED */}
167
+ <Section index={4} title="How you worked" kicker="Patterns, concurrency, outliers" anchor="how">
168
+ <PatternsGrid patterns={data.patterns} />
169
+ <ConcurrencyBlock c={data.concurrency} />
170
+ <OutliersRow outliers={data.outliers} />
171
+ </Section>
172
+
173
+ {/* Section 5 · WHAT TO TRY */}
174
+ <Section index={5} title="What to try next" kicker="One experiment, grounded in what we saw" anchor="next">
175
+ <SuggestionBlock headline={data.suggestion_headline} body={data.suggestion_body} />
176
+ <PriorWeeksStrip weeks={data.prior_weeks} />
177
+ </Section>
178
+
179
+ <MetaFooter meta={data.meta} />
180
+ </div>
181
+ </>
182
+ );
183
+ }
184
+
185
+ // ──────────────────────────────────────────────────────────────────
186
+ // Sub-components
187
+ // ──────────────────────────────────────────────────────────────────
188
+
189
+ function Toolbar({ data, savedKey }: { data: ReportData; savedKey: string | null }) {
190
+ const [copied, setCopied] = useState(false);
191
+ const [pdfState, setPdfState] = useState<"idle" | "rendering" | "error">("idle");
192
+
193
+ const onCopyMd = async () => {
194
+ try {
195
+ await navigator.clipboard.writeText(renderReportAsMarkdown(data));
196
+ setCopied(true);
197
+ window.setTimeout(() => setCopied(false), 1500);
198
+ } catch { /* clipboard may be unavailable in insecure contexts */ }
199
+ };
200
+
201
+ const onSavePdf = async () => {
202
+ // For unsaved (in-progress) reports, fall back to window.print() —
203
+ // the backend can only render reports that exist on disk.
204
+ if (!savedKey) { window.print(); return; }
205
+ setPdfState("rendering");
206
+ try {
207
+ const res = await fetch(`/api/insights/pdf/${savedKey}`);
208
+ if (!res.ok) { setPdfState("error"); window.setTimeout(() => setPdfState("idle"), 2500); return; }
209
+ const blob = await res.blob();
210
+ const url = URL.createObjectURL(blob);
211
+ const a = document.createElement("a");
212
+ a.href = url;
213
+ a.download = `${savedKey}.pdf`;
214
+ document.body.appendChild(a);
215
+ a.click();
216
+ a.remove();
217
+ URL.revokeObjectURL(url);
218
+ setPdfState("idle");
219
+ } catch {
220
+ setPdfState("error");
221
+ window.setTimeout(() => setPdfState("idle"), 2500);
222
+ }
223
+ };
224
+
225
+ return (
226
+ <div className="no-print" style={toolbarStyle}>
227
+ <div style={{ display: "flex", flexDirection: "column", gap: 2, marginRight: "auto" }}>
228
+ <div style={{ fontSize: 11, letterSpacing: "0.08em", textTransform: "uppercase", color: "var(--af-text-tertiary)", fontWeight: 600 }}>
229
+ Insight report
230
+ </div>
231
+ <div style={{ fontSize: 20, fontWeight: 700, letterSpacing: "-0.02em", color: "var(--af-text)" }}>
232
+ {data.period_label}
233
+ </div>
234
+ <div style={{ fontSize: 12, color: "var(--af-text-secondary)" }}>{data.period_sublabel}</div>
235
+ </div>
236
+ <button type="button" style={secondaryBtn} onClick={onCopyMd}>
237
+ {copied ? <Check size={12} /> : <Share2 size={12} />} {copied ? "Copied" : "Copy MD"}
238
+ </button>
239
+ <button type="button" style={primaryBtn} onClick={onSavePdf} disabled={pdfState === "rendering"}>
240
+ <Download size={12} />{" "}
241
+ {pdfState === "rendering" ? "Rendering…" : pdfState === "error" ? "Retry" : "Save as PDF"}
242
+ </button>
243
+ </div>
244
+ );
245
+ }
246
+
247
+ function renderReportAsMarkdown(d: ReportData): string {
248
+ const lines: string[] = [];
249
+ lines.push(`# Insight report — ${d.period_label}`);
250
+ lines.push(`*${d.period_sublabel}*`);
251
+ lines.push("");
252
+ lines.push(`## ${d.archetype.label}`);
253
+ lines.push(`*${d.archetype.tagline}*`);
254
+ lines.push("");
255
+ lines.push(d.archetype.why);
256
+ if (d.archetype.vs_usual) lines.push(`\n*vs your usual:* ${d.archetype.vs_usual}`);
257
+ lines.push("");
258
+ if (d.top_skills.length) {
259
+ lines.push("**Skills leaned on:** " + d.top_skills.map((s) => `${s.name} (${s.count}×)`).join(", "));
260
+ lines.push("");
261
+ }
262
+ lines.push(`## The week at a glance`);
263
+ lines.push(`*${d.theme_headline}*`);
264
+ lines.push("");
265
+ for (const day of d.days) {
266
+ const conc = day.concurrency_peak > 1 ? ` · ×${day.concurrency_peak}${day.has_cross_project ? " cross-project" : ""}` : "";
267
+ const util = day.plan_util_pct > 0 ? ` · ${day.plan_util_pct}% plan` : "";
268
+ lines.push(`- **${day.day_name} ${day.date_label}** — ${day.agent_minutes}m${conc}${util}`);
269
+ }
270
+ lines.push("");
271
+ lines.push(`## Projects`);
272
+ for (const p of d.projects) lines.push(`- ${p.display_name} — ${p.share_pct}% (${p.prs} PR, ${p.commits} commits)`);
273
+ lines.push("");
274
+ if (d.shipped.length) {
275
+ lines.push(`## Shipped (${d.shipped.length})`);
276
+ for (const s of d.shipped) {
277
+ lines.push(`### ${s.title}`);
278
+ lines.push(`${s.project} · ${s.duration_label} · ${s.commits} commits${s.subagents ? ` · ${s.subagents} subagents` : ""}`);
279
+ if (s.flags.length) lines.push(`Flags: ${s.flags.join(", ")}`);
280
+ lines.push("");
281
+ if (s.summary) { lines.push(s.summary); lines.push(""); }
282
+ }
283
+ }
284
+ if (d.patterns.length) {
285
+ lines.push(`## Patterns`);
286
+ for (const p of d.patterns) lines.push(`- **${p.title}** (${p.stat}) — ${p.note}`);
287
+ lines.push("");
288
+ }
289
+ lines.push(`## Concurrency`);
290
+ lines.push(`Peak ×${d.concurrency.peak} on ${d.concurrency.peak_day} · ${d.concurrency.multi_agent_days} multi-agent days · ${d.concurrency.cross_project_days} cross-project days`);
291
+ if (d.concurrency.insight) { lines.push(""); lines.push(d.concurrency.insight); }
292
+ if (d.concurrency.suggestion) { lines.push(""); lines.push(`→ ${d.concurrency.suggestion}`); }
293
+ lines.push("");
294
+ if (d.outliers.length) {
295
+ lines.push(`## Outliers`);
296
+ for (const o of d.outliers) lines.push(`- **${o.label}** — ${o.detail} · ${o.note}`);
297
+ lines.push("");
298
+ }
299
+ lines.push(`## What to try next`);
300
+ lines.push(`**${d.suggestion_headline}**`);
301
+ lines.push("");
302
+ lines.push(d.suggestion_body);
303
+ lines.push("");
304
+ lines.push(`---`);
305
+ lines.push(`*${d.meta.sessions_used} substantive sessions · ${d.meta.model} · generated ${d.meta.generated_at}*`);
306
+ return lines.join("\n");
307
+ }
308
+
309
+ function Section({
310
+ index, title, kicker, anchor, children,
311
+ }: { index: number; title: string; kicker: string; anchor: string; children: React.ReactNode }) {
312
+ return (
313
+ <section id={anchor} style={sectionStyle}>
314
+ <div style={sectionHeaderStyle}>
315
+ <div style={sectionIndex}>{String(index).padStart(2, "0")}</div>
316
+ <div style={{ display: "flex", flexDirection: "column" }}>
317
+ <div style={sectionTitle}>{title}</div>
318
+ <div style={sectionKicker}>{kicker}</div>
319
+ </div>
320
+ </div>
321
+ <div style={{ display: "flex", flexDirection: "column", gap: 18 }}>
322
+ {children}
323
+ </div>
324
+ </section>
325
+ );
326
+ }
327
+
328
+ function ArchetypeHero({ data }: { data: ReportData }) {
329
+ const Icon = ICON_MAP[data.archetype.icon] ?? Sparkles;
330
+ return (
331
+ <div style={archetypeHeroStyle}>
332
+ <div style={{ display: "flex", alignItems: "center", gap: 14 }}>
333
+ <div style={archetypeIconWrap}><Icon size={22} color="var(--af-accent)" /></div>
334
+ <div>
335
+ <div style={archetypeEyebrow}>You were a</div>
336
+ <h2 style={archetypeLabel}>{data.archetype.label}</h2>
337
+ <div style={archetypeTagline}>{data.archetype.tagline}</div>
338
+ </div>
339
+ </div>
340
+ <p style={archetypeWhy}>{data.archetype.why}</p>
341
+ {data.archetype.vs_usual && (
342
+ <div style={archetypeVsUsual}>
343
+ <Compass size={12} style={{ verticalAlign: -2 }} /> <span style={{ opacity: 0.75, marginLeft: 2 }}>vs your usual:</span>{" "}
344
+ <strong style={{ color: "var(--af-text)" }}>{data.archetype.vs_usual}</strong>
345
+ </div>
346
+ )}
347
+ </div>
348
+ );
349
+ }
350
+
351
+ function SkillsStrip({ skills }: { skills: ReportData["top_skills"] }) {
352
+ const max = Math.max(1, ...skills.map((s) => s.count));
353
+ return (
354
+ <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
355
+ <div style={miniSectionTitle}><Sparkles size={12} color="var(--af-accent)" style={{ marginRight: 6, verticalAlign: -1 }} />Skills you leaned on</div>
356
+ <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
357
+ {skills.map((s) => (
358
+ <div key={s.name} style={skillRow}>
359
+ <div style={skillName}>{s.name}</div>
360
+ <div style={skillBarWrap}>
361
+ <div style={{ ...skillBar, width: `${(s.count / max) * 100}%` }} />
362
+ </div>
363
+ <div style={skillCount}>
364
+ {s.count}×
365
+ {typeof s.vs_prior === "number" && s.vs_prior !== 0 && (
366
+ <span style={{ color: s.vs_prior > 0 ? "var(--af-accent)" : "var(--af-text-tertiary)", marginLeft: 6, fontSize: 10 }}>
367
+ {s.vs_prior > 0 ? "+" : ""}{s.vs_prior}
368
+ </span>
369
+ )}
370
+ </div>
371
+ </div>
372
+ ))}
373
+ </div>
374
+ </div>
375
+ );
376
+ }
377
+
378
+ function DayStrip({ days }: { days: ReportData["days"] }) {
379
+ const max = Math.max(1, ...days.map((d) => d.agent_minutes));
380
+ return (
381
+ <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
382
+ <div style={miniSectionTitle}><Layers3 size={12} color="var(--af-accent)" style={{ marginRight: 6, verticalAlign: -1 }} />Daily activity</div>
383
+ <div style={dayStripStyle}>
384
+ {days.map((d) => {
385
+ const heightPct = max === 0 ? 0 : (d.agent_minutes / max) * 100;
386
+ const empty = d.agent_minutes === 0;
387
+ return (
388
+ <div key={d.day_name} style={dayCol}>
389
+ <div style={dayBarWrap}>
390
+ <div
391
+ style={{
392
+ ...dayBar,
393
+ height: `${heightPct}%`,
394
+ opacity: empty ? 0 : 1,
395
+ background: d.has_cross_project
396
+ ? "linear-gradient(180deg, #a78bfa 0%, var(--af-accent) 100%)"
397
+ : "var(--af-accent)",
398
+ }}
399
+ />
400
+ </div>
401
+ <div style={dayConcurrency}>
402
+ {empty ? (
403
+ <span style={{ opacity: 0.3 }}>—</span>
404
+ ) : d.concurrency_peak > 1 ? (
405
+ <span style={{ color: d.has_cross_project ? "#a78bfa" : "var(--af-accent)", fontWeight: 600 }}>
406
+ ×{d.concurrency_peak}
407
+ </span>
408
+ ) : (
409
+ <span style={{ opacity: 0.45 }}>×1</span>
410
+ )}
411
+ </div>
412
+ <div style={dayMinutes}>{empty ? "—" : `${d.agent_minutes}m`}</div>
413
+ <div style={dayName}>{d.day_name}</div>
414
+ <div style={dayDate}>{d.date_label}</div>
415
+ </div>
416
+ );
417
+ })}
418
+ </div>
419
+ <div style={dayLegend}>
420
+ <span><span style={{ ...legendDot, background: "var(--af-accent)" }} /> same-project</span>
421
+ <span><span style={{ ...legendDot, background: "#a78bfa" }} /> cross-project</span>
422
+ <span style={{ opacity: 0.6 }}>× = concurrency peak</span>
423
+ </div>
424
+ </div>
425
+ );
426
+ }
427
+
428
+ function PlanUtilRow({ days }: { days: ReportData["days"] }) {
429
+ return (
430
+ <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
431
+ <div style={miniSectionTitle}>License utilization (by day)</div>
432
+ <div style={planRow}>
433
+ {days.map((d) => {
434
+ const empty = d.plan_util_pct === 0;
435
+ const tone = d.plan_util_pct >= 80 ? "#c08a1f" : d.plan_util_pct >= 50 ? "var(--af-accent)" : "var(--af-text-tertiary)";
436
+ return (
437
+ <div key={d.day_name} style={planCell}>
438
+ <div style={{ ...planBar, background: "var(--af-border-subtle)" }}>
439
+ <div style={{ width: `${d.plan_util_pct}%`, height: "100%", background: tone, borderRadius: 3 }} />
440
+ </div>
441
+ <div style={{ fontSize: 10, color: empty ? "var(--af-text-tertiary)" : tone, fontWeight: empty ? 400 : 600, fontFamily: "var(--font-mono)" }}>
442
+ {empty ? "—" : `${d.plan_util_pct}%`}
443
+ </div>
444
+ </div>
445
+ );
446
+ })}
447
+ </div>
448
+ </div>
449
+ );
450
+ }
451
+
452
+ function ProjectBars({ projects }: { projects: ReportData["projects"] }) {
453
+ return (
454
+ <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
455
+ <div style={miniSectionTitle}><Users size={12} color="var(--af-accent)" style={{ marginRight: 6, verticalAlign: -1 }} />Project time share</div>
456
+ <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
457
+ {projects.map((p) => (
458
+ <div key={p.name} style={projectRow}>
459
+ <div style={projectName}>{p.display_name}</div>
460
+ <div style={projectBarWrap}>
461
+ <div style={{ ...projectBar, width: `${p.share_pct}%` }} />
462
+ </div>
463
+ <div style={projectMeta}>
464
+ <span style={{ color: "var(--af-text)", fontWeight: 600, fontVariantNumeric: "tabular-nums" }}>{p.share_pct}%</span>
465
+ <span style={{ opacity: 0.6, marginLeft: 8 }}>
466
+ {Math.round(p.agent_minutes / 60 * 10) / 10}h · {p.prs} PR · {p.commits} commits
467
+ </span>
468
+ </div>
469
+ </div>
470
+ ))}
471
+ </div>
472
+ </div>
473
+ );
474
+ }
475
+
476
+ function ShippedList({ items }: { items: ReportData["shipped"] }) {
477
+ return (
478
+ <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
479
+ <div style={miniSectionTitle}><Rocket size={12} color="var(--af-accent)" style={{ marginRight: 6, verticalAlign: -1 }} />What shipped ({items.length})</div>
480
+ <div style={shippedList}>
481
+ {items.map((s) => (
482
+ <div key={s.title} style={shippedRow}>
483
+ <div style={shippedLeft}>
484
+ <div style={shippedTitleStyle}>{s.title}</div>
485
+ <div style={shippedSummary}>{s.summary}</div>
486
+ </div>
487
+ <div style={shippedRight}>
488
+ <div style={shippedMetaLine}>
489
+ <span style={metaChip}>{s.project}</span>
490
+ <span style={metaChip}>{s.duration_label}</span>
491
+ <span style={metaChip}>{s.commits} commits</span>
492
+ {s.subagents > 0 && <span style={metaChip}>{s.subagents} subagents</span>}
493
+ </div>
494
+ <div style={shippedFlags}>
495
+ {s.flags.map((f) => <FlagPill key={f} flag={f} />)}
496
+ </div>
497
+ </div>
498
+ </div>
499
+ ))}
500
+ </div>
501
+ </div>
502
+ );
503
+ }
504
+
505
+ function PatternsGrid({ patterns }: { patterns: ReportData["patterns"] }) {
506
+ return (
507
+ <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
508
+ <div style={miniSectionTitle}><BrainCircuit size={12} color="var(--af-accent)" style={{ marginRight: 6, verticalAlign: -1 }} />Patterns worth naming</div>
509
+ <div style={patternsGridStyle}>
510
+ {patterns.map((p) => {
511
+ const Icon = ICON_MAP[p.icon] ?? Zap;
512
+ return (
513
+ <div key={p.title} style={patternCardStyle}>
514
+ <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 6 }}>
515
+ <div style={patternIconWrap}><Icon size={14} color="var(--af-accent)" /></div>
516
+ <div style={patternTitle}>{p.title}</div>
517
+ </div>
518
+ <div style={patternStat}>{p.stat}</div>
519
+ <div style={patternNote}>{p.note}</div>
520
+ </div>
521
+ );
522
+ })}
523
+ </div>
524
+ </div>
525
+ );
526
+ }
527
+
528
+ function ConcurrencyBlock({ c }: { c: ReportData["concurrency"] }) {
529
+ return (
530
+ <div style={concurrencyCard}>
531
+ <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 4 }}>
532
+ <div style={{ ...patternIconWrap, background: "color-mix(in srgb, #a78bfa 18%, transparent)" }}>
533
+ <Network size={14} color="#a78bfa" />
534
+ </div>
535
+ <div style={{ ...miniSectionTitle, margin: 0, color: "#a78bfa" }}>Concurrency</div>
536
+ </div>
537
+ <div style={concurrencyStatRow}>
538
+ <div style={concurrencyStat}><span style={concurrencyStatValue}>{c.peak}</span><span style={concurrencyStatLabel}>peak agents · {c.peak_day}</span></div>
539
+ <div style={concurrencyStat}><span style={concurrencyStatValue}>{c.multi_agent_days}</span><span style={concurrencyStatLabel}>days ≥3 parallel</span></div>
540
+ <div style={concurrencyStat}><span style={concurrencyStatValue}>{c.cross_project_days}</span><span style={concurrencyStatLabel}>cross-project days</span></div>
541
+ </div>
542
+ <p style={{ ...patternNote, margin: "12px 0 0", fontSize: 13 }}>{c.insight}</p>
543
+ <div style={concurrencySuggestion}>
544
+ <ArrowRight size={13} color="var(--af-accent)" style={{ flexShrink: 0, marginTop: 2 }} />
545
+ <span>{c.suggestion}</span>
546
+ </div>
547
+ </div>
548
+ );
549
+ }
550
+
551
+ function OutliersRow({ outliers }: { outliers: ReportData["outliers"] }) {
552
+ return (
553
+ <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
554
+ <div style={miniSectionTitle}>Outliers</div>
555
+ <div style={outliersGridStyle}>
556
+ {outliers.map((o) => (
557
+ <div key={o.label} style={outlierCard}>
558
+ <div style={outlierLabel}>{o.label}</div>
559
+ <div style={outlierDetail}>{o.detail}</div>
560
+ <div style={outlierNote}>{o.note}</div>
561
+ </div>
562
+ ))}
563
+ </div>
564
+ </div>
565
+ );
566
+ }
567
+
568
+ function SuggestionBlock({ headline, body }: { headline: string; body: string }) {
569
+ return (
570
+ <div style={suggestionCard}>
571
+ <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 10 }}>
572
+ <Lightbulb size={16} color="var(--af-accent)" />
573
+ <div style={suggestionEyebrow}>Next experiment</div>
574
+ </div>
575
+ <div style={suggestionHeadline}>{headline}</div>
576
+ <p style={suggestionBody}>{body}</p>
577
+ </div>
578
+ );
579
+ }
580
+
581
+ function PriorWeeksStrip({ weeks }: { weeks: ReportData["prior_weeks"] }) {
582
+ if (!weeks.length) return null;
583
+ return (
584
+ <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
585
+ <div style={miniSectionTitle}>You, the last 4 weeks</div>
586
+ <div style={priorWeeksGrid}>
587
+ {weeks.map((w) => (
588
+ <div key={w.period_label} style={priorWeekCard}>
589
+ <div style={priorWeekPeriod}>{w.period_label}</div>
590
+ <div style={priorWeekArchetype}>{w.archetype}</div>
591
+ <div style={priorWeekStats}>
592
+ <span>{w.sessions} sess</span>
593
+ <span>{w.prs} PR</span>
594
+ <span>{w.subagents} sub</span>
595
+ <span>{(w.agent_minutes / 60).toFixed(1)}h</span>
596
+ </div>
597
+ </div>
598
+ ))}
599
+ </div>
600
+ </div>
601
+ );
602
+ }
603
+
604
+ function MetaFooter({ meta }: { meta: ReportData["meta"] }) {
605
+ return (
606
+ <footer style={footerStyle}>
607
+ <div style={{ display: "flex", gap: 18, flexWrap: "wrap" }}>
608
+ <span>
609
+ <FileText size={11} style={inlineIcon} /> {meta.sessions_used} substantive
610
+ <span style={{ opacity: 0.6 }}> · {meta.trivial_dropped} trivial dropped from {meta.sessions_total} total</span>
611
+ </span>
612
+ <span>Model: {meta.model}</span>
613
+ <span>Context: {meta.context_kb} KB</span>
614
+ <span>Pipeline: {(meta.pipeline_ms / 1000).toFixed(1)}s</span>
615
+ </div>
616
+ <div style={{ opacity: 0.7 }}>{meta.generated_at}</div>
617
+ </footer>
618
+ );
619
+ }
620
+
621
+ function FlagPill({ flag }: { flag: string }) {
622
+ const meta = FLAG_TONE[flag] ?? { label: flag, tone: "neutral" as const };
623
+ const colors = {
624
+ good: { bg: "color-mix(in srgb, var(--af-accent) 14%, transparent)", fg: "var(--af-accent)" },
625
+ warn: { bg: "color-mix(in srgb, #f5b445 18%, transparent)", fg: "#c08a1f" },
626
+ danger: { bg: "color-mix(in srgb, #ef6a5e 18%, transparent)", fg: "#c13f33" },
627
+ neutral: { bg: "var(--af-surface-raised)", fg: "var(--af-text-secondary)" },
628
+ }[meta.tone];
629
+ return (
630
+ <span style={{
631
+ fontSize: 10.5, fontWeight: 500, padding: "3px 8px", borderRadius: 999,
632
+ background: colors.bg, color: colors.fg, letterSpacing: "0.01em", whiteSpace: "nowrap",
633
+ }}>
634
+ {meta.label}
635
+ </span>
636
+ );
637
+ }
638
+
639
+ // ──────────────────────────────────────────────────────────────────
640
+ // Styles
641
+ // ──────────────────────────────────────────────────────────────────
642
+
643
+ const rootStyle: React.CSSProperties = {
644
+ display: "flex", flexDirection: "column", gap: 24,
645
+ maxWidth: 980, padding: "28px 44px 64px", margin: "0 auto",
646
+ };
647
+
648
+ const toolbarStyle: React.CSSProperties = {
649
+ display: "flex", alignItems: "flex-end", justifyContent: "space-between",
650
+ flexWrap: "wrap", gap: 12, paddingBottom: 14,
651
+ borderBottom: "1px solid var(--af-border-subtle)",
652
+ };
653
+
654
+ const primaryBtn: React.CSSProperties = {
655
+ display: "inline-flex", alignItems: "center", gap: 5, padding: "7px 12px",
656
+ border: "1px solid var(--af-accent)", borderRadius: 8,
657
+ background: "var(--af-accent)", color: "white",
658
+ fontSize: 11.5, fontWeight: 600, cursor: "pointer",
659
+ };
660
+
661
+ const secondaryBtn: React.CSSProperties = {
662
+ display: "inline-flex", alignItems: "center", gap: 5, padding: "6px 11px",
663
+ border: "1px solid var(--af-border)", borderRadius: 8,
664
+ background: "var(--af-surface)", color: "var(--af-text)",
665
+ fontSize: 11.5, fontWeight: 500, cursor: "pointer",
666
+ };
667
+
668
+ const sectionStyle: React.CSSProperties = {
669
+ display: "flex", flexDirection: "column", gap: 18,
670
+ scrollMarginTop: 20,
671
+ };
672
+
673
+ const sectionHeaderStyle: React.CSSProperties = {
674
+ display: "flex", alignItems: "center", gap: 14,
675
+ paddingBottom: 4,
676
+ };
677
+
678
+ const sectionIndex: React.CSSProperties = {
679
+ fontSize: 11, fontWeight: 700, letterSpacing: "0.08em",
680
+ color: "var(--af-accent)", fontFamily: "var(--font-mono)",
681
+ padding: "2px 9px", borderRadius: 6,
682
+ background: "color-mix(in srgb, var(--af-accent) 14%, transparent)",
683
+ };
684
+
685
+ const sectionTitle: React.CSSProperties = {
686
+ fontSize: 20, fontWeight: 700, letterSpacing: "-0.02em", color: "var(--af-text)",
687
+ };
688
+
689
+ const sectionKicker: React.CSSProperties = {
690
+ fontSize: 12, color: "var(--af-text-secondary)", marginTop: 1,
691
+ };
692
+
693
+ const miniSectionTitle: React.CSSProperties = {
694
+ fontSize: 11, fontWeight: 600, letterSpacing: "0.06em",
695
+ textTransform: "uppercase", color: "var(--af-text-tertiary)",
696
+ };
697
+
698
+ // Archetype hero
699
+ const archetypeHeroStyle: React.CSSProperties = {
700
+ padding: "26px 28px", borderRadius: 16,
701
+ background: "linear-gradient(135deg, color-mix(in srgb, var(--af-accent) 10%, var(--af-surface)) 0%, var(--af-surface) 70%)",
702
+ border: "1px solid color-mix(in srgb, var(--af-accent) 20%, var(--af-border))",
703
+ display: "flex", flexDirection: "column", gap: 10,
704
+ };
705
+
706
+ const archetypeIconWrap: React.CSSProperties = {
707
+ width: 48, height: 48, borderRadius: 12,
708
+ background: "color-mix(in srgb, var(--af-accent) 16%, transparent)",
709
+ display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0,
710
+ };
711
+
712
+ const archetypeEyebrow: React.CSSProperties = {
713
+ fontSize: 11, color: "var(--af-text-tertiary)", letterSpacing: "0.06em", textTransform: "uppercase", fontWeight: 600,
714
+ };
715
+
716
+ const archetypeLabel: React.CSSProperties = {
717
+ fontSize: 28, fontWeight: 700, letterSpacing: "-0.025em", color: "var(--af-text)", margin: "2px 0", lineHeight: 1.15,
718
+ };
719
+
720
+ const archetypeTagline: React.CSSProperties = {
721
+ fontSize: 13, color: "var(--af-text-secondary)",
722
+ };
723
+
724
+ const archetypeWhy: React.CSSProperties = {
725
+ fontSize: 14, lineHeight: 1.55, color: "var(--af-text)", margin: 0, maxWidth: 720,
726
+ };
727
+
728
+ const archetypeVsUsual: React.CSSProperties = {
729
+ fontSize: 12, color: "var(--af-text-secondary)",
730
+ };
731
+
732
+ // Skills strip
733
+ const skillRow: React.CSSProperties = {
734
+ display: "grid", gridTemplateColumns: "minmax(200px, 40%) 1fr 80px",
735
+ alignItems: "center", gap: 14,
736
+ };
737
+
738
+ const skillName: React.CSSProperties = {
739
+ fontSize: 12.5, color: "var(--af-text)", fontFamily: "var(--font-mono)",
740
+ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap",
741
+ };
742
+
743
+ const skillBarWrap: React.CSSProperties = {
744
+ height: 6, background: "var(--af-border-subtle)", borderRadius: 3,
745
+ };
746
+
747
+ const skillBar: React.CSSProperties = {
748
+ height: "100%", background: "var(--af-accent)", borderRadius: 3,
749
+ };
750
+
751
+ const skillCount: React.CSSProperties = {
752
+ fontSize: 12, color: "var(--af-text-secondary)", fontFamily: "var(--font-mono)",
753
+ fontVariantNumeric: "tabular-nums", textAlign: "right",
754
+ };
755
+
756
+ // Day strip
757
+ const dayStripStyle: React.CSSProperties = {
758
+ display: "grid", gridTemplateColumns: "repeat(7, 1fr)", gap: 6,
759
+ padding: "14px 10px 10px",
760
+ border: "1px solid var(--af-border-subtle)", borderRadius: 12,
761
+ background: "var(--af-surface)",
762
+ };
763
+
764
+ const dayCol: React.CSSProperties = {
765
+ display: "flex", flexDirection: "column", alignItems: "center", gap: 4,
766
+ };
767
+
768
+ const dayBarWrap: React.CSSProperties = {
769
+ width: "100%", height: 70, display: "flex", alignItems: "flex-end", justifyContent: "center",
770
+ };
771
+
772
+ const dayBar: React.CSSProperties = {
773
+ width: "60%", borderRadius: 4, minHeight: 3,
774
+ };
775
+
776
+ const dayConcurrency: React.CSSProperties = {
777
+ fontSize: 11, fontFamily: "var(--font-mono)",
778
+ };
779
+
780
+ const dayMinutes: React.CSSProperties = {
781
+ fontSize: 11, color: "var(--af-text)", fontFamily: "var(--font-mono)",
782
+ };
783
+
784
+ const dayName: React.CSSProperties = {
785
+ fontSize: 11, fontWeight: 600, color: "var(--af-text)", marginTop: 2,
786
+ };
787
+
788
+ const dayDate: React.CSSProperties = {
789
+ fontSize: 10, color: "var(--af-text-tertiary)",
790
+ };
791
+
792
+ const dayLegend: React.CSSProperties = {
793
+ display: "flex", gap: 18, fontSize: 10.5, color: "var(--af-text-secondary)",
794
+ paddingLeft: 4,
795
+ };
796
+
797
+ const legendDot: React.CSSProperties = {
798
+ display: "inline-block", width: 8, height: 8, borderRadius: 2, marginRight: 6, verticalAlign: 0,
799
+ };
800
+
801
+ // Plan utilization
802
+ const planRow: React.CSSProperties = {
803
+ display: "grid", gridTemplateColumns: "repeat(7, 1fr)", gap: 6,
804
+ };
805
+
806
+ const planCell: React.CSSProperties = {
807
+ display: "flex", flexDirection: "column", alignItems: "center", gap: 4, padding: "6px 0",
808
+ };
809
+
810
+ const planBar: React.CSSProperties = {
811
+ width: "70%", height: 6, borderRadius: 3, overflow: "hidden",
812
+ };
813
+
814
+ // Projects
815
+ const projectRow: React.CSSProperties = {
816
+ display: "grid", gridTemplateColumns: "minmax(180px, 30%) 1fr minmax(170px, auto)",
817
+ alignItems: "center", gap: 14,
818
+ };
819
+
820
+ const projectName: React.CSSProperties = {
821
+ fontSize: 13, color: "var(--af-text)", fontFamily: "var(--font-mono)",
822
+ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap",
823
+ };
824
+
825
+ const projectBarWrap: React.CSSProperties = {
826
+ height: 14, background: "var(--af-border-subtle)", borderRadius: 4,
827
+ };
828
+
829
+ const projectBar: React.CSSProperties = {
830
+ height: "100%",
831
+ background: "linear-gradient(90deg, var(--af-accent) 0%, color-mix(in srgb, var(--af-accent) 65%, transparent) 100%)",
832
+ borderRadius: 4,
833
+ };
834
+
835
+ const projectMeta: React.CSSProperties = {
836
+ fontSize: 12, color: "var(--af-text-secondary)", fontFamily: "var(--font-mono)", textAlign: "right",
837
+ };
838
+
839
+ // Shipped list
840
+ const shippedList: React.CSSProperties = {
841
+ display: "flex", flexDirection: "column", gap: 8,
842
+ };
843
+
844
+ const shippedRow: React.CSSProperties = {
845
+ display: "grid", gridTemplateColumns: "1fr auto", gap: 20,
846
+ padding: "14px 16px",
847
+ border: "1px solid var(--af-border-subtle)", borderRadius: 10,
848
+ background: "var(--af-surface)",
849
+ };
850
+
851
+ const shippedLeft: React.CSSProperties = {
852
+ display: "flex", flexDirection: "column", gap: 4, minWidth: 0,
853
+ };
854
+
855
+ const shippedTitleStyle: React.CSSProperties = {
856
+ fontSize: 14, fontWeight: 600, color: "var(--af-text)", letterSpacing: "-0.01em",
857
+ };
858
+
859
+ const shippedSummary: React.CSSProperties = {
860
+ fontSize: 12.5, color: "var(--af-text-secondary)", lineHeight: 1.5,
861
+ };
862
+
863
+ const shippedRight: React.CSSProperties = {
864
+ display: "flex", flexDirection: "column", alignItems: "flex-end", gap: 5,
865
+ minWidth: 0,
866
+ };
867
+
868
+ const shippedMetaLine: React.CSSProperties = {
869
+ display: "flex", flexWrap: "wrap", gap: 5, justifyContent: "flex-end",
870
+ };
871
+
872
+ const metaChip: React.CSSProperties = {
873
+ fontSize: 10.5, padding: "2px 7px", background: "var(--af-surface-raised)",
874
+ borderRadius: 5, color: "var(--af-text-secondary)", fontFamily: "var(--font-mono)",
875
+ };
876
+
877
+ const shippedFlags: React.CSSProperties = {
878
+ display: "flex", flexWrap: "wrap", gap: 5, justifyContent: "flex-end",
879
+ };
880
+
881
+ // Patterns
882
+ const patternsGridStyle: React.CSSProperties = {
883
+ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(260px, 1fr))", gap: 10,
884
+ };
885
+
886
+ const patternCardStyle: React.CSSProperties = {
887
+ padding: "14px 16px", background: "var(--af-surface)",
888
+ border: "1px solid var(--af-border-subtle)", borderRadius: 10,
889
+ display: "flex", flexDirection: "column", gap: 4,
890
+ };
891
+
892
+ const patternIconWrap: React.CSSProperties = {
893
+ width: 24, height: 24, borderRadius: 6,
894
+ background: "color-mix(in srgb, var(--af-accent) 14%, transparent)",
895
+ display: "flex", alignItems: "center", justifyContent: "center",
896
+ };
897
+
898
+ const patternTitle: React.CSSProperties = { fontSize: 13, fontWeight: 600, color: "var(--af-text)" };
899
+ const patternStat: React.CSSProperties = {
900
+ fontSize: 11.5, fontWeight: 600, color: "var(--af-accent)",
901
+ fontFamily: "var(--font-mono)", marginBottom: 2,
902
+ };
903
+ const patternNote: React.CSSProperties = {
904
+ fontSize: 12, lineHeight: 1.5, color: "var(--af-text-secondary)",
905
+ };
906
+
907
+ // Concurrency block
908
+ const concurrencyCard: React.CSSProperties = {
909
+ padding: "18px 22px", borderRadius: 12,
910
+ background: "color-mix(in srgb, #a78bfa 6%, var(--af-surface))",
911
+ border: "1px solid color-mix(in srgb, #a78bfa 28%, var(--af-border))",
912
+ display: "flex", flexDirection: "column", gap: 4,
913
+ };
914
+
915
+ const concurrencyStatRow: React.CSSProperties = {
916
+ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 10, marginTop: 8,
917
+ };
918
+
919
+ const concurrencyStat: React.CSSProperties = {
920
+ display: "flex", flexDirection: "column", gap: 2,
921
+ };
922
+
923
+ const concurrencyStatValue: React.CSSProperties = {
924
+ fontSize: 24, fontWeight: 700, color: "#a78bfa",
925
+ fontVariantNumeric: "tabular-nums", letterSpacing: "-0.02em",
926
+ };
927
+
928
+ const concurrencyStatLabel: React.CSSProperties = {
929
+ fontSize: 11, color: "var(--af-text-secondary)",
930
+ };
931
+
932
+ const concurrencySuggestion: React.CSSProperties = {
933
+ display: "flex", gap: 8, alignItems: "flex-start",
934
+ padding: "10px 12px", marginTop: 10,
935
+ background: "var(--af-surface-raised)", borderRadius: 8,
936
+ fontSize: 12.5, lineHeight: 1.5, color: "var(--af-text)",
937
+ };
938
+
939
+ // Outliers
940
+ const outliersGridStyle: React.CSSProperties = {
941
+ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(160px, 1fr))", gap: 8,
942
+ };
943
+
944
+ const outlierCard: React.CSSProperties = {
945
+ padding: "10px 12px", background: "var(--af-surface)",
946
+ border: "1px solid var(--af-border-subtle)", borderRadius: 8,
947
+ display: "flex", flexDirection: "column", gap: 1,
948
+ };
949
+
950
+ const outlierLabel: React.CSSProperties = {
951
+ fontSize: 10, fontWeight: 600, letterSpacing: "0.05em", textTransform: "uppercase",
952
+ color: "var(--af-text-tertiary)",
953
+ };
954
+
955
+ const outlierDetail: React.CSSProperties = {
956
+ fontSize: 16, fontWeight: 700, color: "var(--af-text)",
957
+ fontVariantNumeric: "tabular-nums", letterSpacing: "-0.01em",
958
+ };
959
+
960
+ const outlierNote: React.CSSProperties = {
961
+ fontSize: 11, color: "var(--af-text-secondary)", lineHeight: 1.35,
962
+ };
963
+
964
+ // Suggestion
965
+ const suggestionCard: React.CSSProperties = {
966
+ padding: "22px 26px", borderRadius: 14,
967
+ background: "color-mix(in srgb, var(--af-accent) 10%, var(--af-surface))",
968
+ border: "1px solid color-mix(in srgb, var(--af-accent) 30%, transparent)",
969
+ };
970
+
971
+ const suggestionEyebrow: React.CSSProperties = {
972
+ fontSize: 11, fontWeight: 700, letterSpacing: "0.09em",
973
+ textTransform: "uppercase", color: "var(--af-accent)",
974
+ };
975
+
976
+ const suggestionHeadline: React.CSSProperties = {
977
+ fontSize: 18, fontWeight: 600, color: "var(--af-text)", letterSpacing: "-0.01em",
978
+ marginBottom: 6, lineHeight: 1.3,
979
+ };
980
+
981
+ const suggestionBody: React.CSSProperties = {
982
+ fontSize: 13.5, lineHeight: 1.55, color: "var(--af-text-secondary)", margin: 0, maxWidth: 720,
983
+ };
984
+
985
+ // Prior weeks
986
+ const priorWeeksGrid: React.CSSProperties = {
987
+ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(200px, 1fr))", gap: 8,
988
+ };
989
+
990
+ const priorWeekCard: React.CSSProperties = {
991
+ padding: "12px 14px", background: "var(--af-surface)",
992
+ border: "1px solid var(--af-border-subtle)", borderRadius: 10,
993
+ display: "flex", flexDirection: "column", gap: 4,
994
+ };
995
+
996
+ const priorWeekPeriod: React.CSSProperties = {
997
+ fontSize: 11, color: "var(--af-text-tertiary)", fontFamily: "var(--font-mono)",
998
+ };
999
+
1000
+ const priorWeekArchetype: React.CSSProperties = {
1001
+ fontSize: 13, fontWeight: 600, color: "var(--af-text)",
1002
+ };
1003
+
1004
+ const priorWeekStats: React.CSSProperties = {
1005
+ display: "flex", gap: 10, fontSize: 11, color: "var(--af-text-secondary)",
1006
+ fontFamily: "var(--font-mono)", marginTop: 2,
1007
+ };
1008
+
1009
+ // Footer
1010
+ const footerStyle: React.CSSProperties = {
1011
+ display: "flex", alignItems: "center", justifyContent: "space-between",
1012
+ flexWrap: "wrap", gap: 10, fontSize: 11, color: "var(--af-text-tertiary)",
1013
+ fontFamily: "var(--font-mono)", paddingTop: 18, marginTop: 8,
1014
+ borderTop: "1px solid var(--af-border-subtle)",
1015
+ };
1016
+
1017
+ const inlineIcon: React.CSSProperties = { display: "inline", verticalAlign: -1, marginRight: 4 };
1018
+
1019
+ function PrintStyles() {
1020
+ return (
1021
+ <style>{`
1022
+ @media print {
1023
+ body, html { background: white !important; }
1024
+ aside, .af-sidebar, nav[data-sidebar], header[data-app-header] { display: none !important; }
1025
+ .no-print { display: none !important; }
1026
+ .report-root { max-width: none !important; padding: 24px !important; margin: 0 !important; color: #111 !important; }
1027
+ .report-root section { page-break-inside: avoid; }
1028
+ }
1029
+ `}</style>
1030
+ );
1031
+ }