fleetlens 0.13.0 → 0.13.2

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 (211) 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.nft.json +1 -1
  27. package/app/apps/web/.next/server/app/api/jobs/route.js.nft.json +1 -1
  28. package/app/apps/web/.next/server/app/api/pdf/[key]/route.js.nft.json +1 -1
  29. package/app/apps/web/.next/server/app/api/runs/[runId]/route.js.nft.json +1 -1
  30. package/app/apps/web/.next/server/app/api/runs/[runId]/stream/route.js.nft.json +1 -1
  31. package/app/apps/web/.next/server/app/api/runs/route.js.nft.json +1 -1
  32. package/app/apps/web/.next/server/app/api/settings/route.js.nft.json +1 -1
  33. package/app/apps/web/.next/server/app/api/team-config/route.js.nft.json +1 -1
  34. package/app/apps/web/.next/server/app/api/workflow-agent/route.js.nft.json +1 -1
  35. package/app/apps/web/.next/server/app/changelog/page/build-manifest.json +5 -5
  36. package/app/apps/web/.next/server/app/changelog/page.js +1 -1
  37. package/app/apps/web/.next/server/app/changelog/page.js.nft.json +1 -1
  38. package/app/apps/web/.next/server/app/changelog/page_client-reference-manifest.js +1 -1
  39. package/app/apps/web/.next/server/app/day/[date]/page/build-manifest.json +5 -5
  40. package/app/apps/web/.next/server/app/day/[date]/page.js +1 -1
  41. package/app/apps/web/.next/server/app/day/[date]/page.js.nft.json +1 -1
  42. package/app/apps/web/.next/server/app/day/[date]/page_client-reference-manifest.js +1 -1
  43. package/app/apps/web/.next/server/app/day/page/build-manifest.json +5 -5
  44. package/app/apps/web/.next/server/app/day/page.js +1 -1
  45. package/app/apps/web/.next/server/app/day/page.js.nft.json +1 -1
  46. package/app/apps/web/.next/server/app/day/page_client-reference-manifest.js +1 -1
  47. package/app/apps/web/.next/server/app/digest/[date]/page/build-manifest.json +5 -5
  48. package/app/apps/web/.next/server/app/digest/[date]/page.js +1 -1
  49. package/app/apps/web/.next/server/app/digest/[date]/page.js.nft.json +1 -1
  50. package/app/apps/web/.next/server/app/digest/[date]/page_client-reference-manifest.js +1 -1
  51. package/app/apps/web/.next/server/app/digest/page/build-manifest.json +5 -5
  52. package/app/apps/web/.next/server/app/digest/page.js +1 -1
  53. package/app/apps/web/.next/server/app/digest/page.js.nft.json +1 -1
  54. package/app/apps/web/.next/server/app/digest/page_client-reference-manifest.js +1 -1
  55. package/app/apps/web/.next/server/app/insights/[key]/page/build-manifest.json +5 -5
  56. package/app/apps/web/.next/server/app/insights/[key]/page.js +1 -1
  57. package/app/apps/web/.next/server/app/insights/[key]/page.js.nft.json +1 -1
  58. package/app/apps/web/.next/server/app/insights/[key]/page_client-reference-manifest.js +1 -1
  59. package/app/apps/web/.next/server/app/insights/page/build-manifest.json +5 -5
  60. package/app/apps/web/.next/server/app/insights/page.js +3 -2
  61. package/app/apps/web/.next/server/app/insights/page.js.nft.json +1 -1
  62. package/app/apps/web/.next/server/app/insights/page_client-reference-manifest.js +1 -1
  63. package/app/apps/web/.next/server/app/page/build-manifest.json +5 -5
  64. package/app/apps/web/.next/server/app/page.js +1 -1
  65. package/app/apps/web/.next/server/app/page.js.nft.json +1 -1
  66. package/app/apps/web/.next/server/app/page_client-reference-manifest.js +1 -1
  67. package/app/apps/web/.next/server/app/parallelism/page/build-manifest.json +5 -5
  68. package/app/apps/web/.next/server/app/parallelism/page.js +1 -1
  69. package/app/apps/web/.next/server/app/parallelism/page.js.nft.json +1 -1
  70. package/app/apps/web/.next/server/app/parallelism/page_client-reference-manifest.js +1 -1
  71. package/app/apps/web/.next/server/app/print/[key]/page/build-manifest.json +5 -5
  72. package/app/apps/web/.next/server/app/print/[key]/page.js +1 -1
  73. package/app/apps/web/.next/server/app/print/[key]/page.js.nft.json +1 -1
  74. package/app/apps/web/.next/server/app/print/[key]/page_client-reference-manifest.js +1 -1
  75. package/app/apps/web/.next/server/app/projects/[slug]/page/build-manifest.json +5 -5
  76. package/app/apps/web/.next/server/app/projects/[slug]/page.js +1 -1
  77. package/app/apps/web/.next/server/app/projects/[slug]/page.js.nft.json +1 -1
  78. package/app/apps/web/.next/server/app/projects/[slug]/page_client-reference-manifest.js +1 -1
  79. package/app/apps/web/.next/server/app/projects/page/build-manifest.json +5 -5
  80. package/app/apps/web/.next/server/app/projects/page.js +1 -1
  81. package/app/apps/web/.next/server/app/projects/page.js.nft.json +1 -1
  82. package/app/apps/web/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  83. package/app/apps/web/.next/server/app/runs/page/build-manifest.json +5 -5
  84. package/app/apps/web/.next/server/app/runs/page.js +1 -1
  85. package/app/apps/web/.next/server/app/runs/page.js.nft.json +1 -1
  86. package/app/apps/web/.next/server/app/runs/page_client-reference-manifest.js +1 -1
  87. package/app/apps/web/.next/server/app/sessions/[id]/page/build-manifest.json +5 -5
  88. package/app/apps/web/.next/server/app/sessions/[id]/page.js +1 -1
  89. package/app/apps/web/.next/server/app/sessions/[id]/page.js.nft.json +1 -1
  90. package/app/apps/web/.next/server/app/sessions/[id]/page_client-reference-manifest.js +1 -1
  91. package/app/apps/web/.next/server/app/sessions/page/build-manifest.json +5 -5
  92. package/app/apps/web/.next/server/app/sessions/page.js +1 -1
  93. package/app/apps/web/.next/server/app/sessions/page.js.nft.json +1 -1
  94. package/app/apps/web/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
  95. package/app/apps/web/.next/server/app/settings/page/build-manifest.json +5 -5
  96. package/app/apps/web/.next/server/app/settings/page.js +1 -1
  97. package/app/apps/web/.next/server/app/settings/page.js.nft.json +1 -1
  98. package/app/apps/web/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  99. package/app/apps/web/.next/server/app/team/page/build-manifest.json +5 -5
  100. package/app/apps/web/.next/server/app/team/page.js +1 -1
  101. package/app/apps/web/.next/server/app/team/page.js.nft.json +1 -1
  102. package/app/apps/web/.next/server/app/team/page_client-reference-manifest.js +1 -1
  103. package/app/apps/web/.next/server/app/usage/page/build-manifest.json +5 -5
  104. package/app/apps/web/.next/server/app/usage/page.js +1 -1
  105. package/app/apps/web/.next/server/app/usage/page.js.nft.json +1 -1
  106. package/app/apps/web/.next/server/app/usage/page_client-reference-manifest.js +1 -1
  107. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0gril0u._.js +9 -0
  108. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0n79hpm._.js +9 -0
  109. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0~utv~v._.js +9 -0
  110. package/app/apps/web/.next/server/chunks/packages_parser_dist_06d9se1._.js +1 -1
  111. package/app/apps/web/.next/server/chunks/packages_parser_dist_10ihc3~._.js +1 -1
  112. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0exckd4._.js +1 -1
  113. 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
  114. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0lhwp-k._.js +8 -0
  115. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0n-ax-5._.js +1 -1
  116. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0~.f_3z._.js +3 -3
  117. package/app/apps/web/.next/server/chunks/ssr/_0kajk31._.js +1 -1
  118. package/app/apps/web/.next/server/chunks/ssr/_0s3n5mz._.js +8 -0
  119. package/app/apps/web/.next/server/chunks/ssr/_0xu2pvc._.js +1 -1
  120. package/app/apps/web/.next/server/chunks/ssr/{_0jw2sm7._.js → _12mbgs5._.js} +2 -2
  121. package/app/apps/web/.next/server/chunks/ssr/apps_web_app_day_[date]_day-view_tsx_0wjoe1u._.js +1 -1
  122. package/app/apps/web/.next/server/chunks/ssr/apps_web_app_insights_page_tsx_071kwyw._.js +3 -0
  123. package/app/apps/web/.next/server/chunks/ssr/apps_web_app_sessions_sessions-grid_tsx_0bri5x.._.js +3 -0
  124. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_0l6hsch._.js +6 -0
  125. package/app/apps/web/.next/server/chunks/ssr/apps_web_lib_changelog_ts_0n7_p1d._.js +1 -1
  126. package/app/apps/web/.next/server/chunks/ssr/apps_web_lib_changelog_ts_0nb5_-s._.js +1 -1
  127. package/app/apps/web/.next/server/chunks/ssr/node_modules_lucide-react_dist_esm_icons_0cepfi8._.js +3 -0
  128. package/app/apps/web/.next/server/chunks/ssr/packages_entries_dist_node_0_271yr.js +1 -1
  129. package/app/apps/web/.next/server/chunks/ssr/packages_parser_dist_0mx_509._.js +1 -1
  130. package/app/apps/web/.next/server/middleware-build-manifest.js +8 -8
  131. package/app/apps/web/.next/server/middleware-manifest.json +5 -5
  132. package/app/apps/web/.next/server/pages/500.html +1 -1
  133. package/app/apps/web/.next/server/server-reference-manifest.js +1 -1
  134. package/app/apps/web/.next/server/server-reference-manifest.json +1 -1
  135. package/app/apps/web/.next/static/chunks/02xxpexzk3rf-.js +1 -0
  136. package/app/apps/web/.next/static/chunks/{07lbm-sdh9c2j.js → 0389wb_f0j78j.js} +1 -1
  137. package/app/apps/web/.next/static/chunks/03gy1evbn94qu.js +6 -0
  138. package/app/apps/web/.next/static/chunks/03rd.3cc_t~yf.js +4 -0
  139. package/app/apps/web/.next/static/chunks/{0l4e3as18eexy.js → 0g223u9amib0q.js} +1 -1
  140. package/app/apps/web/.next/static/chunks/{0h3.-_s-k0mj3.js → 0i.hc~2bj0_3d.js} +1 -1
  141. package/app/apps/web/.next/static/chunks/0m7dd-2zd9vl2.js +4 -0
  142. package/app/apps/web/.next/static/chunks/0nqklmbs5s3ax.js +6 -0
  143. package/app/apps/web/.next/static/chunks/{073081qv4uyn8.js → 0p--hzrway8~3.js} +1 -1
  144. package/app/apps/web/.next/static/chunks/{0gblp_v.kiu-..js → 0qv-gw888_5nu.js} +1 -1
  145. package/app/apps/web/.next/static/chunks/{14bjditmmrn7l.js → 0tirv_vo6q8uc.js} +1 -1
  146. package/app/apps/web/.next/static/chunks/0uyctl-m8iydb.js +1 -0
  147. package/app/apps/web/.next/static/chunks/0~qdw1b2uz._5.js +10 -0
  148. package/app/apps/web/.next/static/chunks/{0n0n.0279ot6c.js → 14vx702c32kn6.js} +1 -1
  149. package/app/apps/web/.next/static/chunks/{turbopack-0lb9w090w5ezx.js → turbopack-0mxu3~9vk.brh.js} +1 -1
  150. package/app/apps/web/app/api/digest/_shared/digest-route-helpers.ts +178 -0
  151. package/app/apps/web/app/api/digest/day/[date]/route.ts +34 -128
  152. package/app/apps/web/app/api/digest/month/[yearMonth]/route.ts +36 -121
  153. package/app/apps/web/app/api/digest/week/[startDate]/route.ts +36 -123
  154. package/app/apps/web/app/insights/page.tsx +20 -5
  155. package/app/apps/web/app/parallelism/burst-detail-modal.tsx +637 -0
  156. package/app/apps/web/app/parallelism/concurrency-info-modal.tsx +228 -0
  157. package/app/apps/web/app/parallelism/gantt-chart-utils.ts +49 -0
  158. package/app/apps/web/app/parallelism/gantt-chart.tsx +24 -903
  159. package/app/apps/web/app/sessions/[id]/session-view/colors.ts +31 -0
  160. package/app/apps/web/app/sessions/[id]/session-view/debug-list.tsx +78 -0
  161. package/app/apps/web/app/sessions/[id]/session-view/drawer.tsx +414 -0
  162. package/app/apps/web/app/sessions/[id]/session-view/entry-day-strip.tsx +124 -0
  163. package/app/apps/web/app/sessions/[id]/session-view/header-stats.tsx +217 -0
  164. package/app/apps/web/app/sessions/[id]/session-view/helpers.ts +135 -0
  165. package/app/apps/web/app/sessions/[id]/session-view/minimap.tsx +1587 -0
  166. package/app/apps/web/app/sessions/[id]/session-view/subagent-drawer.tsx +339 -0
  167. package/app/apps/web/app/sessions/[id]/session-view/token-stats.tsx +111 -0
  168. package/app/apps/web/app/sessions/[id]/session-view/tooltip.tsx +63 -0
  169. package/app/apps/web/app/sessions/[id]/session-view/types.ts +46 -0
  170. package/app/apps/web/app/sessions/[id]/session-view/workflows-panel.tsx +944 -0
  171. package/app/apps/web/app/sessions/[id]/session-view/workflows-shared.tsx +104 -0
  172. package/app/apps/web/app/sessions/[id]/session-view.tsx +1513 -5018
  173. package/app/apps/web/app/sessions/[id]/team-tab/minimap-hover-card.tsx +201 -0
  174. package/app/apps/web/app/sessions/[id]/team-tab/minimap-idle-band.tsx +53 -0
  175. package/app/apps/web/app/sessions/[id]/team-tab/minimap-shared.ts +20 -0
  176. package/app/apps/web/app/sessions/[id]/team-tab/team-minimap.tsx +10 -265
  177. package/app/apps/web/app/sessions/sessions-grid.tsx +10 -10
  178. package/app/apps/web/components/digest-actions.tsx +103 -0
  179. package/app/apps/web/components/job-queue-widget.tsx +1 -1
  180. package/app/apps/web/components/month-digest-view.tsx +19 -64
  181. package/app/apps/web/components/searchable-select.tsx +215 -0
  182. package/app/apps/web/components/tail-mode.tsx +46 -9
  183. package/app/apps/web/components/week-digest-view.tsx +16 -71
  184. package/app/apps/web/lib/changelog-data.generated.ts +1 -1
  185. package/app/apps/web/lib/entries-index.ts +7 -8
  186. package/app/apps/web/lib/entries.ts +0 -9
  187. package/app/apps/web/lib/format.test.ts +221 -0
  188. package/app/apps/web/lib/format.ts +17 -0
  189. package/app/apps/web/lib/utilization-tone.test.ts +56 -0
  190. package/app/apps/web/package.json +1 -1
  191. package/app/apps/web/tsconfig.tsbuildinfo +1 -1
  192. package/dist/daemon-worker.js +35 -43
  193. package/dist/index.js +37 -49
  194. package/package.json +1 -1
  195. package/app/apps/web/.next/server/chunks/[root-of-the-server]__04j3tng._.js +0 -9
  196. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0cnh3k4._.js +0 -9
  197. package/app/apps/web/.next/server/chunks/[root-of-the-server]__132eosx._.js +0 -9
  198. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__136-0me._.js +0 -8
  199. package/app/apps/web/.next/server/chunks/ssr/_0w.cv64._.js +0 -8
  200. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_01d-iwx._.js +0 -6
  201. package/app/apps/web/.next/server/chunks/ssr/node_modules_lucide-react_dist_esm_icons_0-lmm4p._.js +0 -3
  202. package/app/apps/web/.next/static/chunks/05k84gif3ucxm.js +0 -6
  203. package/app/apps/web/.next/static/chunks/0nnjldasalwdu.js +0 -4
  204. package/app/apps/web/.next/static/chunks/0okg13phslr1..js +0 -4
  205. package/app/apps/web/.next/static/chunks/0pzt9vemtzct8.js +0 -10
  206. package/app/apps/web/.next/static/chunks/0qmh5da6gfm56.js +0 -1
  207. package/app/apps/web/.next/static/chunks/0rnay-a.-jr8-.js +0 -1
  208. package/app/apps/web/.next/static/chunks/0snv3-2ux2xvv.js +0 -6
  209. /package/app/apps/web/.next/static/{rbp9x3iPbzrz-wxS-DAU5 → bZs5UyzsYBqC6MFY5n-M_}/_buildManifest.js +0 -0
  210. /package/app/apps/web/.next/static/{rbp9x3iPbzrz-wxS-DAU5 → bZs5UyzsYBqC6MFY5n-M_}/_clientMiddlewareManifest.js +0 -0
  211. /package/app/apps/web/.next/static/{rbp9x3iPbzrz-wxS-DAU5 → bZs5UyzsYBqC6MFY5n-M_}/_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
  }
@@ -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 }}>