fleetlens 0.12.3 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. package/app/apps/web/.next/BUILD_ID +1 -1
  2. package/app/apps/web/.next/build-manifest.json +8 -8
  3. package/app/apps/web/.next/prerender-manifest.json +3 -3
  4. package/app/apps/web/.next/server/app/_global-error/page/build-manifest.json +5 -5
  5. package/app/apps/web/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  6. package/app/apps/web/.next/server/app/_global-error.html +1 -1
  7. package/app/apps/web/.next/server/app/_global-error.rsc +7 -7
  8. package/app/apps/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
  9. package/app/apps/web/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  10. package/app/apps/web/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
  11. package/app/apps/web/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
  12. package/app/apps/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/app/apps/web/.next/server/app/_not-found/page/build-manifest.json +5 -5
  14. package/app/apps/web/.next/server/app/_not-found/page.js +1 -1
  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.nft.json +1 -1
  18. package/app/apps/web/.next/server/app/api/digest/day/[date]/route.js +1 -1
  19. package/app/apps/web/.next/server/app/api/digest/day/[date]/route.js.nft.json +1 -1
  20. package/app/apps/web/.next/server/app/api/digest/month/[yearMonth]/route.js +1 -1
  21. package/app/apps/web/.next/server/app/api/digest/month/[yearMonth]/route.js.nft.json +1 -1
  22. package/app/apps/web/.next/server/app/api/digest/month-index/route.js.nft.json +1 -1
  23. package/app/apps/web/.next/server/app/api/digest/week/[startDate]/route.js +1 -1
  24. package/app/apps/web/.next/server/app/api/digest/week/[startDate]/route.js.nft.json +1 -1
  25. package/app/apps/web/.next/server/app/api/digest/week-index/route.js.nft.json +1 -1
  26. package/app/apps/web/.next/server/app/api/events/route.js +2 -1
  27. package/app/apps/web/.next/server/app/api/events/route.js.nft.json +1 -1
  28. package/app/apps/web/.next/server/app/api/jobs/route.js.nft.json +1 -1
  29. package/app/apps/web/.next/server/app/api/pdf/[key]/route.js.nft.json +1 -1
  30. package/app/apps/web/.next/server/app/api/runs/[runId]/route.js.nft.json +1 -1
  31. package/app/apps/web/.next/server/app/api/runs/[runId]/stream/route.js.nft.json +1 -1
  32. package/app/apps/web/.next/server/app/api/runs/route.js.nft.json +1 -1
  33. package/app/apps/web/.next/server/app/api/settings/route.js.nft.json +1 -1
  34. package/app/apps/web/.next/server/app/api/team-config/route.js.nft.json +1 -1
  35. package/app/apps/web/.next/server/app/api/workflow-agent/route.js.nft.json +1 -1
  36. package/app/apps/web/.next/server/app/changelog/page/build-manifest.json +5 -5
  37. package/app/apps/web/.next/server/app/changelog/page.js +1 -1
  38. package/app/apps/web/.next/server/app/changelog/page.js.nft.json +1 -1
  39. package/app/apps/web/.next/server/app/changelog/page_client-reference-manifest.js +1 -1
  40. package/app/apps/web/.next/server/app/day/[date]/page/build-manifest.json +5 -5
  41. package/app/apps/web/.next/server/app/day/[date]/page.js +1 -1
  42. package/app/apps/web/.next/server/app/day/[date]/page.js.nft.json +1 -1
  43. package/app/apps/web/.next/server/app/day/[date]/page_client-reference-manifest.js +1 -1
  44. package/app/apps/web/.next/server/app/day/page/build-manifest.json +5 -5
  45. package/app/apps/web/.next/server/app/day/page.js +1 -1
  46. package/app/apps/web/.next/server/app/day/page.js.nft.json +1 -1
  47. package/app/apps/web/.next/server/app/day/page_client-reference-manifest.js +1 -1
  48. package/app/apps/web/.next/server/app/digest/[date]/page/build-manifest.json +5 -5
  49. package/app/apps/web/.next/server/app/digest/[date]/page.js +1 -1
  50. package/app/apps/web/.next/server/app/digest/[date]/page.js.nft.json +1 -1
  51. package/app/apps/web/.next/server/app/digest/[date]/page_client-reference-manifest.js +1 -1
  52. package/app/apps/web/.next/server/app/digest/page/build-manifest.json +5 -5
  53. package/app/apps/web/.next/server/app/digest/page.js +1 -1
  54. package/app/apps/web/.next/server/app/digest/page.js.nft.json +1 -1
  55. package/app/apps/web/.next/server/app/digest/page_client-reference-manifest.js +1 -1
  56. package/app/apps/web/.next/server/app/insights/[key]/page/build-manifest.json +5 -5
  57. package/app/apps/web/.next/server/app/insights/[key]/page.js +1 -1
  58. package/app/apps/web/.next/server/app/insights/[key]/page.js.nft.json +1 -1
  59. package/app/apps/web/.next/server/app/insights/[key]/page_client-reference-manifest.js +1 -1
  60. package/app/apps/web/.next/server/app/insights/page/build-manifest.json +5 -5
  61. package/app/apps/web/.next/server/app/insights/page.js +3 -2
  62. package/app/apps/web/.next/server/app/insights/page.js.nft.json +1 -1
  63. package/app/apps/web/.next/server/app/insights/page_client-reference-manifest.js +1 -1
  64. package/app/apps/web/.next/server/app/page/build-manifest.json +5 -5
  65. package/app/apps/web/.next/server/app/page.js +1 -1
  66. package/app/apps/web/.next/server/app/page.js.nft.json +1 -1
  67. package/app/apps/web/.next/server/app/page_client-reference-manifest.js +1 -1
  68. package/app/apps/web/.next/server/app/parallelism/page/build-manifest.json +5 -5
  69. package/app/apps/web/.next/server/app/parallelism/page.js +1 -1
  70. package/app/apps/web/.next/server/app/parallelism/page.js.nft.json +1 -1
  71. package/app/apps/web/.next/server/app/parallelism/page_client-reference-manifest.js +1 -1
  72. package/app/apps/web/.next/server/app/print/[key]/page/build-manifest.json +5 -5
  73. package/app/apps/web/.next/server/app/print/[key]/page.js +1 -1
  74. package/app/apps/web/.next/server/app/print/[key]/page.js.nft.json +1 -1
  75. package/app/apps/web/.next/server/app/print/[key]/page_client-reference-manifest.js +1 -1
  76. package/app/apps/web/.next/server/app/projects/[slug]/page/build-manifest.json +5 -5
  77. package/app/apps/web/.next/server/app/projects/[slug]/page.js +1 -1
  78. package/app/apps/web/.next/server/app/projects/[slug]/page.js.nft.json +1 -1
  79. package/app/apps/web/.next/server/app/projects/[slug]/page_client-reference-manifest.js +1 -1
  80. package/app/apps/web/.next/server/app/projects/page/build-manifest.json +5 -5
  81. package/app/apps/web/.next/server/app/projects/page.js +1 -1
  82. package/app/apps/web/.next/server/app/projects/page.js.nft.json +1 -1
  83. package/app/apps/web/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  84. package/app/apps/web/.next/server/app/runs/page/build-manifest.json +5 -5
  85. package/app/apps/web/.next/server/app/runs/page.js +1 -1
  86. package/app/apps/web/.next/server/app/runs/page.js.nft.json +1 -1
  87. package/app/apps/web/.next/server/app/runs/page_client-reference-manifest.js +1 -1
  88. package/app/apps/web/.next/server/app/sessions/[id]/page/build-manifest.json +5 -5
  89. package/app/apps/web/.next/server/app/sessions/[id]/page.js +1 -1
  90. package/app/apps/web/.next/server/app/sessions/[id]/page.js.nft.json +1 -1
  91. package/app/apps/web/.next/server/app/sessions/[id]/page_client-reference-manifest.js +1 -1
  92. package/app/apps/web/.next/server/app/sessions/page/build-manifest.json +5 -5
  93. package/app/apps/web/.next/server/app/sessions/page.js +1 -1
  94. package/app/apps/web/.next/server/app/sessions/page.js.nft.json +1 -1
  95. package/app/apps/web/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
  96. package/app/apps/web/.next/server/app/settings/page/build-manifest.json +5 -5
  97. package/app/apps/web/.next/server/app/settings/page.js +1 -1
  98. package/app/apps/web/.next/server/app/settings/page.js.nft.json +1 -1
  99. package/app/apps/web/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  100. package/app/apps/web/.next/server/app/team/page/build-manifest.json +5 -5
  101. package/app/apps/web/.next/server/app/team/page.js +1 -1
  102. package/app/apps/web/.next/server/app/team/page.js.nft.json +1 -1
  103. package/app/apps/web/.next/server/app/team/page_client-reference-manifest.js +1 -1
  104. package/app/apps/web/.next/server/app/usage/page/build-manifest.json +5 -5
  105. package/app/apps/web/.next/server/app/usage/page.js +1 -1
  106. package/app/apps/web/.next/server/app/usage/page.js.nft.json +1 -1
  107. package/app/apps/web/.next/server/app/usage/page_client-reference-manifest.js +1 -1
  108. package/app/apps/web/.next/server/chunks/[externals]__0rv9ct5._.js +3 -0
  109. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0gril0u._.js +9 -0
  110. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0n79hpm._.js +9 -0
  111. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0~utv~v._.js +9 -0
  112. package/app/apps/web/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_02bw8.n.js +5 -0
  113. package/app/apps/web/.next/server/chunks/packages_parser_dist_06d9se1._.js +1 -1
  114. package/app/apps/web/.next/server/chunks/packages_parser_dist_10ihc3~._.js +1 -1
  115. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0exckd4._.js +1 -1
  116. package/app/apps/web/.next/server/chunks/ssr/{[root-of-the-server]__0-1_njm._.js → [root-of-the-server]__0k1x-c9._.js} +2 -2
  117. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0lhwp-k._.js +8 -0
  118. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0n-ax-5._.js +1 -1
  119. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0o03ymy._.js +1 -1
  120. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0~.f_3z._.js +3 -3
  121. package/app/apps/web/.next/server/chunks/ssr/_0kajk31._.js +1 -1
  122. package/app/apps/web/.next/server/chunks/ssr/_0s3n5mz._.js +8 -0
  123. package/app/apps/web/.next/server/chunks/ssr/_0xu2pvc._.js +1 -1
  124. package/app/apps/web/.next/server/chunks/ssr/{_0jw2sm7._.js → _12mbgs5._.js} +2 -2
  125. package/app/apps/web/.next/server/chunks/ssr/apps_web_0.mu7c7._.js +1 -1
  126. package/app/apps/web/.next/server/chunks/ssr/apps_web_app_day_[date]_day-view_tsx_0wjoe1u._.js +1 -1
  127. package/app/apps/web/.next/server/chunks/ssr/apps_web_app_insights_page_tsx_071kwyw._.js +3 -0
  128. package/app/apps/web/.next/server/chunks/ssr/apps_web_app_sessions_sessions-grid_tsx_0bri5x.._.js +3 -0
  129. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_0b-4-4j._.js +1 -1
  130. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_0c-tpft._.js +1 -1
  131. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_0l6hsch._.js +6 -0
  132. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_0txtsv5._.js +1 -1
  133. package/app/apps/web/.next/server/chunks/ssr/apps_web_lib_changelog_ts_0n7_p1d._.js +1 -1
  134. package/app/apps/web/.next/server/chunks/ssr/apps_web_lib_changelog_ts_0nb5_-s._.js +1 -1
  135. package/app/apps/web/.next/server/chunks/ssr/node_modules_lucide-react_dist_esm_icons_0cepfi8._.js +3 -0
  136. package/app/apps/web/.next/server/chunks/ssr/packages_entries_dist_node_0_271yr.js +1 -1
  137. package/app/apps/web/.next/server/chunks/ssr/packages_parser_dist_0mx_509._.js +1 -1
  138. package/app/apps/web/.next/server/middleware-build-manifest.js +8 -8
  139. package/app/apps/web/.next/server/middleware-manifest.json +5 -5
  140. package/app/apps/web/.next/server/pages/500.html +1 -1
  141. package/app/apps/web/.next/server/server-reference-manifest.js +1 -1
  142. package/app/apps/web/.next/server/server-reference-manifest.json +1 -1
  143. package/app/apps/web/.next/static/chunks/02bv_ejzhhzvp.js +10 -0
  144. package/app/apps/web/.next/static/chunks/{07lbm-sdh9c2j.js → 0389wb_f0j78j.js} +1 -1
  145. package/app/apps/web/.next/static/chunks/03gy1evbn94qu.js +6 -0
  146. package/app/apps/web/.next/static/chunks/03rd.3cc_t~yf.js +4 -0
  147. package/app/apps/web/.next/static/chunks/{12jvvc_w.cb45.css → 083~hzkv577.5.css} +1 -1
  148. package/app/apps/web/.next/static/chunks/{0osdgl2.9cl1q.js → 0d2x-wfu.m0-z.js} +1 -1
  149. package/app/apps/web/.next/static/chunks/{0lziv8y_6yp5w.js → 0e_fzy0n8ij-o.js} +1 -1
  150. package/app/apps/web/.next/static/chunks/{0l4e3as18eexy.js → 0g223u9amib0q.js} +1 -1
  151. package/app/apps/web/.next/static/chunks/{0h3.-_s-k0mj3.js → 0i.hc~2bj0_3d.js} +1 -1
  152. package/app/apps/web/.next/static/chunks/0m7dd-2zd9vl2.js +4 -0
  153. package/app/apps/web/.next/static/chunks/0nqklmbs5s3ax.js +6 -0
  154. package/app/apps/web/.next/static/chunks/{073081qv4uyn8.js → 0p--hzrway8~3.js} +1 -1
  155. package/app/apps/web/.next/static/chunks/0q8dg4txlzno0.js +1 -0
  156. package/app/apps/web/.next/static/chunks/{0gblp_v.kiu-..js → 0qv-gw888_5nu.js} +1 -1
  157. package/app/apps/web/.next/static/chunks/{14bjditmmrn7l.js → 0tirv_vo6q8uc.js} +1 -1
  158. package/app/apps/web/.next/static/chunks/{0qmh5da6gfm56.js → 0vz4~a~~hq399.js} +1 -1
  159. package/app/apps/web/.next/static/chunks/{13po66tt.68f3.js → 0wwcx42ng8i2e.js} +3 -3
  160. package/app/apps/web/.next/static/chunks/{0n0n.0279ot6c.js → 14vx702c32kn6.js} +1 -1
  161. package/app/apps/web/.next/static/chunks/{turbopack-0lb9w090w5ezx.js → turbopack-0mxu3~9vk.brh.js} +1 -1
  162. package/app/apps/web/app/api/digest/_shared/digest-route-helpers.ts +178 -0
  163. package/app/apps/web/app/api/digest/day/[date]/route.ts +34 -128
  164. package/app/apps/web/app/api/digest/month/[yearMonth]/route.ts +36 -121
  165. package/app/apps/web/app/api/digest/week/[startDate]/route.ts +36 -123
  166. package/app/apps/web/app/api/events/route.ts +47 -4
  167. package/app/apps/web/app/globals.css +23 -0
  168. package/app/apps/web/app/insights/page.tsx +20 -5
  169. package/app/apps/web/app/layout.tsx +1 -0
  170. package/app/apps/web/app/page.tsx +1 -1
  171. package/app/apps/web/app/parallelism/burst-detail-modal.tsx +637 -0
  172. package/app/apps/web/app/parallelism/concurrency-info-modal.tsx +228 -0
  173. package/app/apps/web/app/parallelism/gantt-chart-utils.ts +49 -0
  174. package/app/apps/web/app/parallelism/gantt-chart.tsx +16 -902
  175. package/app/apps/web/app/projects/[slug]/page.tsx +1 -1
  176. package/app/apps/web/app/sessions/[id]/session-view/colors.ts +31 -0
  177. package/app/apps/web/app/sessions/[id]/session-view/debug-list.tsx +78 -0
  178. package/app/apps/web/app/sessions/[id]/session-view/drawer.tsx +414 -0
  179. package/app/apps/web/app/sessions/[id]/session-view/entry-day-strip.tsx +124 -0
  180. package/app/apps/web/app/sessions/[id]/session-view/header-stats.tsx +217 -0
  181. package/app/apps/web/app/sessions/[id]/session-view/helpers.ts +135 -0
  182. package/app/apps/web/app/sessions/[id]/session-view/subagent-drawer.tsx +339 -0
  183. package/app/apps/web/app/sessions/[id]/session-view/token-stats.tsx +111 -0
  184. package/app/apps/web/app/sessions/[id]/session-view/tooltip.tsx +63 -0
  185. package/app/apps/web/app/sessions/[id]/session-view/types.ts +46 -0
  186. package/app/apps/web/app/sessions/[id]/session-view/workflows-shared.tsx +104 -0
  187. package/app/apps/web/app/sessions/[id]/session-view.tsx +701 -1577
  188. package/app/apps/web/app/sessions/[id]/team-tab/minimap-hover-card.tsx +201 -0
  189. package/app/apps/web/app/sessions/[id]/team-tab/minimap-idle-band.tsx +53 -0
  190. package/app/apps/web/app/sessions/[id]/team-tab/minimap-shared.ts +20 -0
  191. package/app/apps/web/app/sessions/[id]/team-tab/team-minimap.tsx +10 -265
  192. package/app/apps/web/app/sessions/sessions-grid.tsx +43 -19
  193. package/app/apps/web/components/digest-actions.tsx +103 -0
  194. package/app/apps/web/components/job-queue-widget.tsx +1 -1
  195. package/app/apps/web/components/live-badge.tsx +11 -3
  196. package/app/apps/web/components/live-sessions-widget.tsx +9 -3
  197. package/app/apps/web/components/month-digest-view.tsx +19 -64
  198. package/app/apps/web/components/outcome-pill.tsx +24 -4
  199. package/app/apps/web/components/searchable-select.tsx +215 -0
  200. package/app/apps/web/components/tail-mode.tsx +39 -8
  201. package/app/apps/web/components/week-digest-view.tsx +16 -71
  202. package/app/apps/web/lib/changelog-data.generated.ts +1 -1
  203. package/app/apps/web/lib/entries-index.ts +7 -8
  204. package/app/apps/web/lib/entries.ts +0 -9
  205. package/app/apps/web/lib/format.test.ts +221 -0
  206. package/app/apps/web/lib/utilization-tone.test.ts +56 -0
  207. package/app/apps/web/package.json +1 -1
  208. package/app/apps/web/tsconfig.tsbuildinfo +1 -1
  209. package/dist/daemon-worker.js +83 -45
  210. package/dist/index.js +452 -198
  211. package/package.json +1 -1
  212. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0455vu3._.js +0 -5
  213. package/app/apps/web/.next/server/chunks/[root-of-the-server]__04j3tng._.js +0 -9
  214. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0cnh3k4._.js +0 -9
  215. package/app/apps/web/.next/server/chunks/[root-of-the-server]__132eosx._.js +0 -9
  216. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__136-0me._.js +0 -8
  217. package/app/apps/web/.next/server/chunks/ssr/_0w.cv64._.js +0 -8
  218. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_01d-iwx._.js +0 -6
  219. package/app/apps/web/.next/server/chunks/ssr/node_modules_lucide-react_dist_esm_icons_0-lmm4p._.js +0 -3
  220. package/app/apps/web/.next/static/chunks/00xkqnvk4-d1-.js +0 -10
  221. package/app/apps/web/.next/static/chunks/0htgc_hr1bw67.js +0 -1
  222. package/app/apps/web/.next/static/chunks/0nnjldasalwdu.js +0 -4
  223. package/app/apps/web/.next/static/chunks/0okg13phslr1..js +0 -4
  224. package/app/apps/web/.next/static/chunks/0vf-3rauafbek.js +0 -6
  225. package/app/apps/web/.next/static/chunks/0vl27-vjob2_9.js +0 -6
  226. /package/app/apps/web/.next/static/{7kQ9mNICnVHhf4-LmSGcy → VEfemGVAQ0xCHjj15orZU}/_buildManifest.js +0 -0
  227. /package/app/apps/web/.next/static/{7kQ9mNICnVHhf4-LmSGcy → VEfemGVAQ0xCHjj15orZU}/_clientMiddlewareManifest.js +0 -0
  228. /package/app/apps/web/.next/static/{7kQ9mNICnVHhf4-LmSGcy → VEfemGVAQ0xCHjj15orZU}/_ssgManifest.js +0 -0
@@ -2,146 +2,61 @@ import {
2
2
  runMonthDigestPipeline, readSettings,
3
3
  readMonthDigest, getCurrentMonthDigestFromCache,
4
4
  } from "@claude-lens/entries/node";
5
- import type { PipelineEvent } from "@claude-lens/entries/node";
6
- import { InflightCoalescer } from "@/lib/inflight-coalesce";
7
5
  import {
8
6
  isValidYearMonth, currentYearMonth, currentWeekMonday, todayLocal,
9
7
  } from "@/lib/entries";
10
- import { registerJob, updateJob, completeJob, failJob } from "@/lib/jobs";
8
+ import {
9
+ handleDigestGet, handleDigestPost, jsonResponse, jsonResponseBare,
10
+ } from "../../_shared/digest-route-helpers";
11
11
 
12
12
  export const runtime = "nodejs";
13
13
  export const dynamic = "force-dynamic";
14
14
 
15
15
  type Params = { params: Promise<{ yearMonth: string }> };
16
16
 
17
- const coalescer = new InflightCoalescer<string, void>();
18
-
19
- function updateJobFromEvent(jobId: string, ev: PipelineEvent): void {
20
- if (ev.type === "status") {
21
- updateJob(jobId, { progress: { phase: ev.phase, text: ev.text } });
22
- } else if (ev.type === "entry") {
23
- updateJob(jobId, { progress: { phase: "enrich", index: ev.index, total: ev.total } });
24
- } else if (ev.type === "progress") {
25
- updateJob(jobId, { progress: { phase: ev.phase, bytes: ev.bytes } });
26
- }
27
- }
28
-
29
17
  export async function GET(_req: Request, ctx: Params) {
30
18
  const { yearMonth } = await ctx.params;
31
- if (!isValidYearMonth(yearMonth)) {
32
- return new Response(JSON.stringify({ error: "invalid year-month" }), {
33
- status: 400, headers: { "content-type": "application/json" },
34
- });
35
- }
36
- if (yearMonth > currentYearMonth()) {
37
- return new Response(JSON.stringify({ error: "future month" }), { status: 400 });
38
- }
39
-
40
- const isCurrent = yearMonth === currentYearMonth();
41
- if (isCurrent) {
42
- const cached = getCurrentMonthDigestFromCache(yearMonth, Date.now());
43
- if (cached) {
44
- return new Response(JSON.stringify(cached), {
45
- status: 200, headers: { "content-type": "application/json" },
46
- });
47
- }
48
- return new Response(JSON.stringify({ pending: true, current: true }), {
49
- status: 200, headers: { "content-type": "application/json" },
50
- });
51
- }
52
-
53
- const cached = readMonthDigest(yearMonth);
54
- if (cached) {
55
- return new Response(JSON.stringify(cached), {
56
- status: 200, headers: { "content-type": "application/json" },
57
- });
58
- }
59
- return new Response(JSON.stringify({ pending: true }), {
60
- status: 200, headers: { "content-type": "application/json" },
19
+ const valid = isValidYearMonth(yearMonth);
20
+ const isFuture = valid && yearMonth > currentYearMonth();
21
+ return handleDigestGet({
22
+ key: valid ? yearMonth : null,
23
+ invalidResponse: jsonResponse({ error: "invalid year-month" }, 400),
24
+ isFuture,
25
+ futureResponse: jsonResponseBare({ error: "future month" }, 400),
26
+ isCurrent: valid && yearMonth === currentYearMonth(),
27
+ readCurrentCache: () => getCurrentMonthDigestFromCache(yearMonth, Date.now()),
28
+ readPersisted: () => readMonthDigest(yearMonth),
29
+ pendingCurrentPayload: { pending: true, current: true },
30
+ currentFallsBackToPersisted: false,
61
31
  });
62
32
  }
63
33
 
64
34
  export async function POST(req: Request, ctx: Params) {
65
35
  const { yearMonth } = await ctx.params;
66
- if (!isValidYearMonth(yearMonth)) {
67
- return new Response(JSON.stringify({ error: "invalid year-month" }), { status: 400 });
68
- }
69
- if (yearMonth > currentYearMonth()) {
70
- return new Response(JSON.stringify({ error: "future month" }), { status: 400 });
71
- }
72
-
36
+ const valid = isValidYearMonth(yearMonth);
37
+ const isFuture = valid && yearMonth > currentYearMonth();
73
38
  const url = new URL(req.url);
74
39
  const force = url.searchParams.get("force") === "1";
75
- const key = `month|${yearMonth}|${force ? 1 : 0}`;
76
-
77
- const encoder = new TextEncoder();
78
40
  const settings = readSettings();
79
41
 
80
- const stream = new ReadableStream({
81
- async start(controller) {
82
- let closed = false;
83
- function send(event: PipelineEvent) {
84
- if (closed) return;
85
- try {
86
- controller.enqueue(
87
- encoder.encode(`event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`),
88
- );
89
- } catch { closed = true; }
90
- }
91
- function finish() {
92
- if (!closed) {
93
- try { controller.close(); } catch { /* ignore */ }
94
- closed = true;
95
- }
96
- }
97
-
98
- const alreadyInflight = coalescer.inflight(key);
99
- let jobId: string | null = null;
100
-
101
- try {
102
- await coalescer.run(key, async () => {
103
- if (alreadyInflight) return;
104
- jobId = registerJob({
105
- kind: "monthly.synth",
106
- label: `Month digest · ${yearMonth}`,
107
- target: yearMonth,
108
- caller: "user",
109
- });
110
- updateJob(jobId, { status: "running" });
111
- for await (const ev of runMonthDigestPipeline(yearMonth, {
112
- settings: settings.ai_features,
113
- force,
114
- currentYearMonth: currentYearMonth(),
115
- currentWeekMonday: currentWeekMonday(),
116
- todayLocalDay: todayLocal(),
117
- })) {
118
- send(ev);
119
- if (jobId) updateJobFromEvent(jobId, ev);
120
- }
121
- if (jobId) completeJob(jobId, `/insights/month-${yearMonth}`);
122
- });
123
-
124
- if (alreadyInflight) {
125
- const d = readMonthDigest(yearMonth) ?? getCurrentMonthDigestFromCache(yearMonth, Date.now());
126
- if (d) send({ type: "digest", digest: d });
127
- send({ type: "status", phase: "persist", text: "coalesced with in-flight request" });
128
- }
129
- } catch (err) {
130
- send({ type: "error", message: (err as Error).message });
131
- if (jobId) failJob(jobId, (err as Error).message);
132
- } finally {
133
- try { controller.enqueue(encoder.encode(`event: done\ndata: {}\n\n`)); } catch { /* ignore */ }
134
- finish();
135
- }
136
- },
137
- });
138
-
139
- return new Response(stream, {
140
- headers: {
141
- "Content-Type": "text/event-stream",
142
- "Cache-Control": "no-cache, no-transform",
143
- Connection: "keep-alive",
144
- "X-Accel-Buffering": "no",
145
- },
42
+ return handleDigestPost({
43
+ key: valid ? yearMonth : null,
44
+ invalidResponse: jsonResponseBare({ error: "invalid year-month" }, 400),
45
+ isFuture,
46
+ futureResponse: jsonResponseBare({ error: "future month" }, 400),
47
+ coalesceScope: "month",
48
+ force,
49
+ jobKind: "monthly.synth",
50
+ jobLabel: `Month digest · ${yearMonth}`,
51
+ resultUrl: `/insights/month-${yearMonth}`,
52
+ runPipeline: () => runMonthDigestPipeline(yearMonth, {
53
+ settings: settings.ai_features,
54
+ force,
55
+ currentYearMonth: currentYearMonth(),
56
+ currentWeekMonday: currentWeekMonday(),
57
+ todayLocalDay: todayLocal(),
58
+ }),
59
+ readDigestForCoalescedRequest: () =>
60
+ readMonthDigest(yearMonth) ?? getCurrentMonthDigestFromCache(yearMonth, Date.now()),
146
61
  });
147
62
  }
@@ -2,149 +2,62 @@ import {
2
2
  runWeekDigestPipeline, readSettings,
3
3
  readWeekDigest, getCurrentWeekDigestFromCache,
4
4
  } from "@claude-lens/entries/node";
5
- import type { PipelineEvent } from "@claude-lens/entries/node";
6
- import { InflightCoalescer } from "@/lib/inflight-coalesce";
7
5
  import { asMonday, currentWeekMonday, todayLocal } from "@/lib/entries";
8
- import { registerJob, updateJob, completeJob, failJob } from "@/lib/jobs";
6
+ import {
7
+ handleDigestGet, handleDigestPost, jsonResponse, jsonResponseBare,
8
+ } from "../../_shared/digest-route-helpers";
9
9
 
10
10
  export const runtime = "nodejs";
11
11
  export const dynamic = "force-dynamic";
12
12
 
13
13
  type Params = { params: Promise<{ startDate: string }> };
14
14
 
15
- const coalescer = new InflightCoalescer<string, void>();
16
-
17
- function updateJobFromEvent(jobId: string, ev: PipelineEvent): void {
18
- if (ev.type === "status") {
19
- updateJob(jobId, { progress: { phase: ev.phase, text: ev.text } });
20
- } else if (ev.type === "entry") {
21
- updateJob(jobId, { progress: { phase: "enrich", index: ev.index, total: ev.total } });
22
- } else if (ev.type === "progress") {
23
- updateJob(jobId, { progress: { phase: ev.phase, bytes: ev.bytes } });
24
- }
25
- }
26
-
27
15
  export async function GET(_req: Request, ctx: Params) {
28
16
  const { startDate } = await ctx.params;
29
17
  const monday = asMonday(startDate);
30
- if (!monday) {
31
- return new Response(JSON.stringify({ error: "invalid Monday date" }), {
32
- status: 400, headers: { "content-type": "application/json" },
33
- });
34
- }
35
18
  const today = todayLocal();
36
- if (monday > today) {
37
- return new Response(JSON.stringify({ error: "future date" }), { status: 400 });
38
- }
39
-
40
- const isCurrent = monday === currentWeekMonday();
41
- if (isCurrent) {
42
- const cached =
43
- getCurrentWeekDigestFromCache(monday, Date.now()) ?? readWeekDigest(monday);
44
- if (cached) {
45
- return new Response(JSON.stringify(cached), {
46
- status: 200, headers: { "content-type": "application/json" },
47
- });
48
- }
49
- return new Response(JSON.stringify({ pending: true, current: true }), {
50
- status: 200, headers: { "content-type": "application/json" },
51
- });
52
- }
53
-
54
- const cached = readWeekDigest(monday);
55
- if (cached) {
56
- return new Response(JSON.stringify(cached), {
57
- status: 200, headers: { "content-type": "application/json" },
58
- });
59
- }
60
- return new Response(JSON.stringify({ pending: true }), {
61
- status: 200, headers: { "content-type": "application/json" },
19
+ const isFuture = monday !== null && monday > today;
20
+ return handleDigestGet({
21
+ key: monday,
22
+ invalidResponse: jsonResponse({ error: "invalid Monday date" }, 400),
23
+ isFuture,
24
+ futureResponse: jsonResponseBare({ error: "future date" }, 400),
25
+ isCurrent: monday !== null && monday === currentWeekMonday(),
26
+ readCurrentCache: () =>
27
+ monday !== null ? getCurrentWeekDigestFromCache(monday, Date.now()) : null,
28
+ readPersisted: () => (monday !== null ? readWeekDigest(monday) : null),
29
+ pendingCurrentPayload: { pending: true, current: true },
30
+ currentFallsBackToPersisted: true,
62
31
  });
63
32
  }
64
33
 
65
34
  export async function POST(req: Request, ctx: Params) {
66
35
  const { startDate } = await ctx.params;
67
36
  const monday = asMonday(startDate);
68
- if (!monday) {
69
- return new Response(JSON.stringify({ error: "invalid Monday date" }), { status: 400 });
70
- }
71
- if (monday > todayLocal()) {
72
- return new Response(JSON.stringify({ error: "future date" }), { status: 400 });
73
- }
74
-
37
+ const isFuture = monday !== null && monday > todayLocal();
75
38
  const url = new URL(req.url);
76
39
  const force = url.searchParams.get("force") === "1";
77
- const key = `week|${monday}|${force ? 1 : 0}`;
78
-
79
- const encoder = new TextEncoder();
80
40
  const settings = readSettings();
81
41
 
82
- const stream = new ReadableStream({
83
- async start(controller) {
84
- let closed = false;
85
- function send(event: PipelineEvent) {
86
- if (closed) return;
87
- try {
88
- controller.enqueue(
89
- encoder.encode(`event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`),
90
- );
91
- } catch {
92
- closed = true;
93
- }
94
- }
95
- function finish() {
96
- if (!closed) {
97
- try { controller.close(); } catch { /* ignore */ }
98
- closed = true;
99
- }
100
- }
101
-
102
- const alreadyInflight = coalescer.inflight(key);
103
- let jobId: string | null = null;
104
-
105
- try {
106
- await coalescer.run(key, async () => {
107
- if (alreadyInflight) return;
108
- jobId = registerJob({
109
- kind: "weekly.synth",
110
- label: `Week digest · ${monday}`,
111
- target: monday,
112
- caller: "user",
113
- });
114
- updateJob(jobId, { status: "running" });
115
- for await (const ev of runWeekDigestPipeline(monday, {
116
- settings: settings.ai_features,
117
- force,
118
- currentWeekMonday: currentWeekMonday(),
119
- todayLocalDay: todayLocal(),
120
- })) {
121
- send(ev);
122
- if (jobId) updateJobFromEvent(jobId, ev);
123
- }
124
- if (jobId) completeJob(jobId, `/insights/week-${monday}`);
125
- });
126
-
127
- if (alreadyInflight) {
128
- const d = readWeekDigest(monday) ?? getCurrentWeekDigestFromCache(monday, Date.now());
129
- if (d) send({ type: "digest", digest: d });
130
- send({ type: "status", phase: "persist", text: "coalesced with in-flight request" });
131
- }
132
- } catch (err) {
133
- send({ type: "error", message: (err as Error).message });
134
- if (jobId) failJob(jobId, (err as Error).message);
135
- } finally {
136
- try { controller.enqueue(encoder.encode(`event: done\ndata: {}\n\n`)); } catch { /* ignore */ }
137
- finish();
138
- }
139
- },
140
- });
141
-
142
- return new Response(stream, {
143
- headers: {
144
- "Content-Type": "text/event-stream",
145
- "Cache-Control": "no-cache, no-transform",
146
- Connection: "keep-alive",
147
- "X-Accel-Buffering": "no",
148
- },
42
+ return handleDigestPost({
43
+ key: monday,
44
+ invalidResponse: jsonResponseBare({ error: "invalid Monday date" }, 400),
45
+ isFuture,
46
+ futureResponse: jsonResponseBare({ error: "future date" }, 400),
47
+ coalesceScope: "week",
48
+ force,
49
+ jobKind: "weekly.synth",
50
+ jobLabel: `Week digest · ${monday}`,
51
+ resultUrl: `/insights/week-${monday}`,
52
+ runPipeline: () => runWeekDigestPipeline(monday as string, {
53
+ settings: settings.ai_features,
54
+ force,
55
+ currentWeekMonday: currentWeekMonday(),
56
+ todayLocalDay: todayLocal(),
57
+ }),
58
+ readDigestForCoalescedRequest: () =>
59
+ monday !== null
60
+ ? (readWeekDigest(monday) ?? getCurrentWeekDigestFromCache(monday, Date.now()))
61
+ : null,
149
62
  });
150
63
  }
@@ -50,6 +50,11 @@ const HEARTBEAT_MS = 15_000;
50
50
  /** Per-file debounce so a burst of writes doesn't flood subscribers. */
51
51
  const DEBOUNCE_MS = 150;
52
52
 
53
+ /** Subagent writes coalesce per PARENT session over a much longer window — a
54
+ * 16-agent workflow fan-out writes constantly, so this caps that to at most
55
+ * one parent refresh every 2s instead of a storm. */
56
+ const SUBAGENT_DEBOUNCE_MS = 2_000;
57
+
53
58
  export async function GET(request: Request) {
54
59
  const encoder = new TextEncoder();
55
60
 
@@ -87,9 +92,10 @@ export async function GET(request: Request) {
87
92
  const projectDir = parts[0]!;
88
93
  const fileName = parts[parts.length - 1]!;
89
94
  if (!fileName.endsWith(".jsonl")) return;
90
- // Skip subagent child files under <session>/subagents/ —
91
- // they're noise; the parent session file mtime changes
92
- // whenever a subagent writes (Claude Code touches both).
95
+ // Subagent child files under <session>/subagents/ are routed to
96
+ // emitSubagent (throttled, mapped to the parent) by the dispatch
97
+ // below — this early-return is a safety net so they never go
98
+ // through the per-file fast path here.
93
99
  if (parts.includes("subagents")) return;
94
100
  const sessionId = fileName.replace(/\.jsonl$/, "");
95
101
 
@@ -148,6 +154,42 @@ export async function GET(request: Request) {
148
154
  );
149
155
  }
150
156
 
157
+ // Background-agent transcripts (`<session>/subagents/agent-*.jsonl`, and
158
+ // workflow agents at `<session>/subagents/workflows/<runId>/agent-*.jsonl`)
159
+ // are written while the parent session sits idle. Map them back to the
160
+ // parent and emit a session-updated so the LIVE indicator (now nested-
161
+ // aware via lastActivityMs) refreshes. Coalesced per-parent over a long
162
+ // window so a big fan-out can't flood subscribers.
163
+ function emitSubagent(fullPath: string) {
164
+ const rel = path.relative(DEFAULT_ROOT, fullPath);
165
+ const parts = rel.split(path.sep);
166
+ const subIdx = parts.lastIndexOf("subagents");
167
+ if (subIdx < 2) return;
168
+ const projectDir = parts[0]!;
169
+ const sessionId = parts[subIdx - 1]!;
170
+ const parentJsonl = path.join(DEFAULT_ROOT, projectDir, `${sessionId}.jsonl`);
171
+ const key = `sub:${parentJsonl}`;
172
+ const prev = pending.get(key);
173
+ if (prev) clearTimeout(prev);
174
+ pending.set(
175
+ key,
176
+ setTimeout(async () => {
177
+ pending.delete(key);
178
+ if (closed) return;
179
+ invalidateFile(parentJsonl);
180
+ // Report the child's fresh mtime — the parent .jsonl may be stale,
181
+ // and LiveRefresher dedups per source by a monotonic mtime.
182
+ let mtimeMs = Date.now();
183
+ try {
184
+ mtimeMs = (await fs.stat(fullPath)).mtimeMs;
185
+ } catch {
186
+ // child gone already — fall back to now.
187
+ }
188
+ send({ type: "session-updated", sessionId, projectDir, mtimeMs });
189
+ }, SUBAGENT_DEBOUNCE_MS),
190
+ );
191
+ }
192
+
151
193
  let watcher: ReturnType<typeof watch> | null = null;
152
194
  try {
153
195
  watcher = watch(
@@ -157,7 +199,8 @@ export async function GET(request: Request) {
157
199
  if (!filename) return;
158
200
  const fullPath = path.join(DEFAULT_ROOT, filename.toString());
159
201
  if (fullPath.endsWith(".jsonl")) {
160
- emit(fullPath);
202
+ if (fullPath.includes(`${path.sep}subagents${path.sep}`)) emitSubagent(fullPath);
203
+ else emit(fullPath);
161
204
  return;
162
205
  }
163
206
  const base = path.basename(fullPath);
@@ -141,6 +141,29 @@ input::placeholder {
141
141
  border-radius: 6px !important;
142
142
  }
143
143
 
144
+ /* Day-navigator step buttons on the session timeline minimap. */
145
+ .sl-day-nav-btn {
146
+ display: inline-flex;
147
+ align-items: center;
148
+ justify-content: center;
149
+ width: 26px;
150
+ height: 26px;
151
+ border: 1px solid var(--af-border-subtle);
152
+ border-radius: 6px;
153
+ background: var(--af-surface-hover);
154
+ color: var(--af-text-secondary);
155
+ cursor: pointer;
156
+ transition: background 0.15s ease, opacity 0.15s ease;
157
+ }
158
+ .sl-day-nav-btn:hover:not(:disabled) {
159
+ background: var(--af-surface-elevated);
160
+ color: var(--af-text-primary);
161
+ }
162
+ .sl-day-nav-btn:disabled {
163
+ opacity: 0.35;
164
+ cursor: default;
165
+ }
166
+
144
167
  /* Scrollbar */
145
168
  ::-webkit-scrollbar {
146
169
  width: 8px;
@@ -1,6 +1,8 @@
1
1
  import Link from "next/link";
2
2
  import { readSettings, readWeekDigest, interactiveLockFresh } from "@claude-lens/entries/node";
3
- import { lastCompletedWeekMonday, currentWeekMonday, listEntriesForDay } from "@/lib/entries";
3
+ import { listEntryKeys } from "@claude-lens/entries/fs";
4
+ import { parseEntryKey } from "@claude-lens/entries";
5
+ import { lastCompletedWeekMonday, currentWeekMonday } from "@/lib/entries";
4
6
  import { WeekDigestView } from "@/components/week-digest-view";
5
7
  import { InsightsTopBar } from "@/components/insights-top-bar";
6
8
 
@@ -24,9 +26,21 @@ function weekDates(monday: string): string[] {
24
26
  return out;
25
27
  }
26
28
 
27
- function hasAnyEntries(monday: string): boolean {
29
+ // Build once from the entries dir, then test the 7 weekdays against it.
30
+ // Earlier this called listEntriesForDay per day per render, which opened
31
+ // and parsed every matching entry file 14× before deciding the week is empty.
32
+ function presentDays(): Set<string> {
33
+ const out = new Set<string>();
34
+ for (const key of listEntryKeys()) {
35
+ const parsed = parseEntryKey(key);
36
+ if (parsed) out.add(parsed.local_day);
37
+ }
38
+ return out;
39
+ }
40
+
41
+ function hasAnyEntries(monday: string, present: Set<string>): boolean {
28
42
  for (const d of weekDates(monday)) {
29
- if (listEntriesForDay(d).length > 0) return true;
43
+ if (present.has(d)) return true;
30
44
  }
31
45
  return false;
32
46
  }
@@ -45,7 +59,8 @@ export default async function InsightsPage() {
45
59
  const lastWeek = lastCompletedWeekMonday();
46
60
  const cached = readWeekDigest(lastWeek);
47
61
  const prior = readWeekDigest(priorMonday(lastWeek));
48
- const hasData = !!cached || hasAnyEntries(lastWeek);
62
+ const present = presentDays();
63
+ const hasData = !!cached || hasAnyEntries(lastWeek, present);
49
64
 
50
65
  // Auto-fire when AI is on and the digest isn't already cached. The interactive
51
66
  // pipeline lock (heartbeat-refreshed) suppresses concurrent fires from the
@@ -58,7 +73,7 @@ export default async function InsightsPage() {
58
73
  const prevMonday = priorMonday(lastWeek);
59
74
  const currentMonday = currentWeekMonday();
60
75
  const nextMonday = currentMonday > lastWeek ? currentMonday : null;
61
- const nextHasData = nextMonday ? hasAnyEntries(nextMonday) : false;
76
+ const nextHasData = nextMonday ? hasAnyEntries(nextMonday, present) : false;
62
77
 
63
78
  return (
64
79
  <div style={{ paddingBottom: 64 }}>
@@ -78,6 +78,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
78
78
  lastAgentPreview: s.lastAgentPreview,
79
79
  firstTimestamp: s.firstTimestamp,
80
80
  lastTimestamp: s.lastTimestamp,
81
+ lastActivityMs: s.lastActivityMs,
81
82
  teamName: s.teamName,
82
83
  agentName: s.agentName,
83
84
  agent: s.agent,
@@ -307,7 +307,7 @@ export default async function DashboardHome({
307
307
  gap: 6,
308
308
  }}
309
309
  >
310
- <LiveBadge mtimeIso={s.lastTimestamp} />
310
+ <LiveBadge mtimeIso={s.lastTimestamp} activityMs={s.lastActivityMs} />
311
311
  <span
312
312
  style={{
313
313
  overflow: "hidden",