fleetlens 0.13.0 → 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 (205) 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]__0~.f_3z._.js +3 -3
  116. package/app/apps/web/.next/server/chunks/ssr/_0kajk31._.js +1 -1
  117. package/app/apps/web/.next/server/chunks/ssr/_0s3n5mz._.js +8 -0
  118. package/app/apps/web/.next/server/chunks/ssr/_0xu2pvc._.js +1 -1
  119. package/app/apps/web/.next/server/chunks/ssr/{_0jw2sm7._.js → _12mbgs5._.js} +2 -2
  120. package/app/apps/web/.next/server/chunks/ssr/apps_web_app_day_[date]_day-view_tsx_0wjoe1u._.js +1 -1
  121. package/app/apps/web/.next/server/chunks/ssr/apps_web_app_insights_page_tsx_071kwyw._.js +3 -0
  122. package/app/apps/web/.next/server/chunks/ssr/apps_web_app_sessions_sessions-grid_tsx_0bri5x.._.js +3 -0
  123. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_0l6hsch._.js +6 -0
  124. package/app/apps/web/.next/server/chunks/ssr/apps_web_lib_changelog_ts_0n7_p1d._.js +1 -1
  125. package/app/apps/web/.next/server/chunks/ssr/apps_web_lib_changelog_ts_0nb5_-s._.js +1 -1
  126. package/app/apps/web/.next/server/chunks/ssr/node_modules_lucide-react_dist_esm_icons_0cepfi8._.js +3 -0
  127. package/app/apps/web/.next/server/chunks/ssr/packages_entries_dist_node_0_271yr.js +1 -1
  128. package/app/apps/web/.next/server/chunks/ssr/packages_parser_dist_0mx_509._.js +1 -1
  129. package/app/apps/web/.next/server/middleware-build-manifest.js +8 -8
  130. package/app/apps/web/.next/server/middleware-manifest.json +5 -5
  131. package/app/apps/web/.next/server/pages/500.html +1 -1
  132. package/app/apps/web/.next/server/server-reference-manifest.js +1 -1
  133. package/app/apps/web/.next/server/server-reference-manifest.json +1 -1
  134. package/app/apps/web/.next/static/chunks/{0pzt9vemtzct8.js → 02bv_ejzhhzvp.js} +3 -3
  135. package/app/apps/web/.next/static/chunks/{07lbm-sdh9c2j.js → 0389wb_f0j78j.js} +1 -1
  136. package/app/apps/web/.next/static/chunks/03gy1evbn94qu.js +6 -0
  137. package/app/apps/web/.next/static/chunks/03rd.3cc_t~yf.js +4 -0
  138. package/app/apps/web/.next/static/chunks/{0l4e3as18eexy.js → 0g223u9amib0q.js} +1 -1
  139. package/app/apps/web/.next/static/chunks/{0h3.-_s-k0mj3.js → 0i.hc~2bj0_3d.js} +1 -1
  140. package/app/apps/web/.next/static/chunks/0m7dd-2zd9vl2.js +4 -0
  141. package/app/apps/web/.next/static/chunks/0nqklmbs5s3ax.js +6 -0
  142. package/app/apps/web/.next/static/chunks/{073081qv4uyn8.js → 0p--hzrway8~3.js} +1 -1
  143. package/app/apps/web/.next/static/chunks/0q8dg4txlzno0.js +1 -0
  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/{0qmh5da6gfm56.js → 0vz4~a~~hq399.js} +1 -1
  147. package/app/apps/web/.next/static/chunks/{0n0n.0279ot6c.js → 14vx702c32kn6.js} +1 -1
  148. package/app/apps/web/.next/static/chunks/{turbopack-0lb9w090w5ezx.js → turbopack-0mxu3~9vk.brh.js} +1 -1
  149. package/app/apps/web/app/api/digest/_shared/digest-route-helpers.ts +178 -0
  150. package/app/apps/web/app/api/digest/day/[date]/route.ts +34 -128
  151. package/app/apps/web/app/api/digest/month/[yearMonth]/route.ts +36 -121
  152. package/app/apps/web/app/api/digest/week/[startDate]/route.ts +36 -123
  153. package/app/apps/web/app/insights/page.tsx +20 -5
  154. package/app/apps/web/app/parallelism/burst-detail-modal.tsx +637 -0
  155. package/app/apps/web/app/parallelism/concurrency-info-modal.tsx +228 -0
  156. package/app/apps/web/app/parallelism/gantt-chart-utils.ts +49 -0
  157. package/app/apps/web/app/parallelism/gantt-chart.tsx +16 -902
  158. package/app/apps/web/app/sessions/[id]/session-view/colors.ts +31 -0
  159. package/app/apps/web/app/sessions/[id]/session-view/debug-list.tsx +78 -0
  160. package/app/apps/web/app/sessions/[id]/session-view/drawer.tsx +414 -0
  161. package/app/apps/web/app/sessions/[id]/session-view/entry-day-strip.tsx +124 -0
  162. package/app/apps/web/app/sessions/[id]/session-view/header-stats.tsx +217 -0
  163. package/app/apps/web/app/sessions/[id]/session-view/helpers.ts +135 -0
  164. package/app/apps/web/app/sessions/[id]/session-view/subagent-drawer.tsx +339 -0
  165. package/app/apps/web/app/sessions/[id]/session-view/token-stats.tsx +111 -0
  166. package/app/apps/web/app/sessions/[id]/session-view/tooltip.tsx +63 -0
  167. package/app/apps/web/app/sessions/[id]/session-view/types.ts +46 -0
  168. package/app/apps/web/app/sessions/[id]/session-view/workflows-shared.tsx +104 -0
  169. package/app/apps/web/app/sessions/[id]/session-view.tsx +448 -1639
  170. package/app/apps/web/app/sessions/[id]/team-tab/minimap-hover-card.tsx +201 -0
  171. package/app/apps/web/app/sessions/[id]/team-tab/minimap-idle-band.tsx +53 -0
  172. package/app/apps/web/app/sessions/[id]/team-tab/minimap-shared.ts +20 -0
  173. package/app/apps/web/app/sessions/[id]/team-tab/team-minimap.tsx +10 -265
  174. package/app/apps/web/app/sessions/sessions-grid.tsx +10 -10
  175. package/app/apps/web/components/digest-actions.tsx +103 -0
  176. package/app/apps/web/components/job-queue-widget.tsx +1 -1
  177. package/app/apps/web/components/month-digest-view.tsx +19 -64
  178. package/app/apps/web/components/searchable-select.tsx +215 -0
  179. package/app/apps/web/components/tail-mode.tsx +39 -8
  180. package/app/apps/web/components/week-digest-view.tsx +16 -71
  181. package/app/apps/web/lib/changelog-data.generated.ts +1 -1
  182. package/app/apps/web/lib/entries-index.ts +7 -8
  183. package/app/apps/web/lib/entries.ts +0 -9
  184. package/app/apps/web/lib/format.test.ts +221 -0
  185. package/app/apps/web/lib/utilization-tone.test.ts +56 -0
  186. package/app/apps/web/package.json +1 -1
  187. package/app/apps/web/tsconfig.tsbuildinfo +1 -1
  188. package/dist/daemon-worker.js +35 -43
  189. package/dist/index.js +37 -49
  190. package/package.json +1 -1
  191. package/app/apps/web/.next/server/chunks/[root-of-the-server]__04j3tng._.js +0 -9
  192. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0cnh3k4._.js +0 -9
  193. package/app/apps/web/.next/server/chunks/[root-of-the-server]__132eosx._.js +0 -9
  194. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__136-0me._.js +0 -8
  195. package/app/apps/web/.next/server/chunks/ssr/_0w.cv64._.js +0 -8
  196. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_01d-iwx._.js +0 -6
  197. package/app/apps/web/.next/server/chunks/ssr/node_modules_lucide-react_dist_esm_icons_0-lmm4p._.js +0 -3
  198. package/app/apps/web/.next/static/chunks/05k84gif3ucxm.js +0 -6
  199. package/app/apps/web/.next/static/chunks/0nnjldasalwdu.js +0 -4
  200. package/app/apps/web/.next/static/chunks/0okg13phslr1..js +0 -4
  201. package/app/apps/web/.next/static/chunks/0rnay-a.-jr8-.js +0 -1
  202. package/app/apps/web/.next/static/chunks/0snv3-2ux2xvv.js +0 -6
  203. /package/app/apps/web/.next/static/{rbp9x3iPbzrz-wxS-DAU5 → VEfemGVAQ0xCHjj15orZU}/_buildManifest.js +0 -0
  204. /package/app/apps/web/.next/static/{rbp9x3iPbzrz-wxS-DAU5 → VEfemGVAQ0xCHjj15orZU}/_clientMiddlewareManifest.js +0 -0
  205. /package/app/apps/web/.next/static/{rbp9x3iPbzrz-wxS-DAU5 → VEfemGVAQ0xCHjj15orZU}/_ssgManifest.js +0 -0
@@ -0,0 +1,178 @@
1
+ import "server-only";
2
+ import type {
3
+ DayDigest, MonthDigest, WeekDigest,
4
+ } from "@claude-lens/entries";
5
+ import type { PipelineEvent } from "@claude-lens/entries/node";
6
+ import { InflightCoalescer } from "@/lib/inflight-coalesce";
7
+ import {
8
+ type JobKind,
9
+ registerJob, updateJob, completeJob, failJob,
10
+ } from "@/lib/jobs";
11
+
12
+ type AnyDigest = DayDigest | WeekDigest | MonthDigest;
13
+
14
+ // Shared coalescer instance for all digest periods. Per-period keys are
15
+ // already prefixed (day|/week|/month|) so a single instance is equivalent
16
+ // to the previous one-per-route setup.
17
+ const coalescer = new InflightCoalescer<string, void>();
18
+
19
+ export 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
+ function jsonResponse(body: unknown, status: number): Response {
30
+ return new Response(JSON.stringify(body), {
31
+ status, headers: { "content-type": "application/json" },
32
+ });
33
+ }
34
+
35
+ // 400 without content-type header — preserves byte-equality for the routes
36
+ // whose POST validation paths historically omitted it.
37
+ function jsonResponseBare(body: unknown, status: number): Response {
38
+ return new Response(JSON.stringify(body), { status });
39
+ }
40
+
41
+ export type DigestGetSpec = {
42
+ // Parsed/validated key for cache reads. null = invalid input.
43
+ key: string | null;
44
+ // Validation error response when key is null.
45
+ invalidResponse: Response;
46
+ // True if the key represents a future date/period.
47
+ isFuture: boolean;
48
+ futureResponse: Response;
49
+ // True if the key represents the current (in-progress) period.
50
+ isCurrent: boolean;
51
+ // Lookup for the in-memory current-period cache (only consulted when isCurrent).
52
+ readCurrentCache: () => AnyDigest | null | undefined;
53
+ // Persisted-disk lookup (consulted for past periods, and as a fallback for
54
+ // current periods that have already been persisted — week uses this fallback,
55
+ // day and month do not).
56
+ readPersisted: () => AnyDigest | null | undefined;
57
+ // Payload shape for the "no cache yet" current-period response. Day uses
58
+ // {pending:true, today:true}; week and month use {pending:true, current:true}.
59
+ pendingCurrentPayload: Record<string, unknown>;
60
+ // True if the current-period branch should also fall back to readPersisted
61
+ // when the in-memory cache is empty. Week does this; day and month do not.
62
+ currentFallsBackToPersisted: boolean;
63
+ };
64
+
65
+ export function handleDigestGet(spec: DigestGetSpec): Response {
66
+ if (spec.key === null) return spec.invalidResponse;
67
+ if (spec.isFuture) return spec.futureResponse;
68
+
69
+ if (spec.isCurrent) {
70
+ const cached = spec.currentFallsBackToPersisted
71
+ ? (spec.readCurrentCache() ?? spec.readPersisted())
72
+ : spec.readCurrentCache();
73
+ if (cached) return jsonResponse(cached, 200);
74
+ return jsonResponse(spec.pendingCurrentPayload, 200);
75
+ }
76
+
77
+ const cached = spec.readPersisted();
78
+ if (cached) return jsonResponse(cached, 200);
79
+ return jsonResponse({ pending: true }, 200);
80
+ }
81
+
82
+ export type DigestPostSpec = {
83
+ // Parsed/validated key — feeds cache key, job target, resultUrl. null = invalid.
84
+ key: string | null;
85
+ invalidResponse: Response;
86
+ isFuture: boolean;
87
+ futureResponse: Response;
88
+ // Coalescer key prefix: "day" / "week" / "month".
89
+ coalesceScope: string;
90
+ // ?force=1
91
+ force: boolean;
92
+ // Job-tracking fields.
93
+ jobKind: JobKind;
94
+ jobLabel: string;
95
+ // resultUrl for completeJob.
96
+ resultUrl: string;
97
+ // Pre-bound pipeline factory — the helper never sees the per-period option shape.
98
+ runPipeline: () => AsyncIterable<PipelineEvent>;
99
+ // Fallback to deliver a final digest to a request that coalesced onto an
100
+ // in-flight run. Returns the digest if available, else null/undefined.
101
+ readDigestForCoalescedRequest: () => AnyDigest | null | undefined;
102
+ };
103
+
104
+ export function handleDigestPost(spec: DigestPostSpec): Response {
105
+ if (spec.key === null) return spec.invalidResponse;
106
+ if (spec.isFuture) return spec.futureResponse;
107
+
108
+ const coalesceKey = `${spec.coalesceScope}|${spec.key}|${spec.force ? 1 : 0}`;
109
+ const encoder = new TextEncoder();
110
+
111
+ const stream = new ReadableStream({
112
+ async start(controller) {
113
+ let closed = false;
114
+ function send(event: PipelineEvent) {
115
+ if (closed) return;
116
+ try {
117
+ controller.enqueue(
118
+ encoder.encode(`event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`),
119
+ );
120
+ } catch {
121
+ closed = true;
122
+ }
123
+ }
124
+ function finish() {
125
+ if (!closed) {
126
+ try { controller.close(); } catch { /* already */ }
127
+ closed = true;
128
+ }
129
+ }
130
+
131
+ const alreadyInflight = coalescer.inflight(coalesceKey);
132
+ let jobId: string | null = null;
133
+
134
+ try {
135
+ await coalescer.run(coalesceKey, async () => {
136
+ if (alreadyInflight) return;
137
+ jobId = registerJob({
138
+ kind: spec.jobKind,
139
+ label: spec.jobLabel,
140
+ target: spec.key as string,
141
+ caller: "user",
142
+ });
143
+ updateJob(jobId, { status: "running" });
144
+ for await (const ev of spec.runPipeline()) {
145
+ send(ev);
146
+ if (jobId) updateJobFromEvent(jobId, ev);
147
+ }
148
+ if (jobId) completeJob(jobId, spec.resultUrl);
149
+ });
150
+
151
+ if (alreadyInflight) {
152
+ const d = spec.readDigestForCoalescedRequest();
153
+ if (d) send({ type: "digest", digest: d });
154
+ send({ type: "status", phase: "persist", text: "coalesced with in-flight request" });
155
+ }
156
+ } catch (err) {
157
+ send({ type: "error", message: (err as Error).message });
158
+ if (jobId) failJob(jobId, (err as Error).message);
159
+ } finally {
160
+ try { controller.enqueue(encoder.encode(`event: done\ndata: {}\n\n`)); } catch { /* ignore */ }
161
+ finish();
162
+ }
163
+ },
164
+ });
165
+
166
+ return new Response(stream, {
167
+ headers: {
168
+ "Content-Type": "text/event-stream",
169
+ "Cache-Control": "no-cache, no-transform",
170
+ Connection: "keep-alive",
171
+ "X-Accel-Buffering": "no",
172
+ },
173
+ });
174
+ }
175
+
176
+ // Re-export so route files can build their own validation responses without
177
+ // duplicating the JSON-body Response idiom.
178
+ export { jsonResponse, jsonResponseBare };
@@ -1,152 +1,58 @@
1
1
  import {
2
2
  runDayDigestPipeline, readSettings, getTodayDigestFromCache,
3
3
  } from "@claude-lens/entries/node";
4
- import type { PipelineEvent } from "@claude-lens/entries/node";
5
4
  import { readDayDigest } from "@claude-lens/entries/fs";
6
- import { InflightCoalescer } from "@/lib/inflight-coalesce";
7
5
  import { isValidDate, 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<{ date: string }> };
14
14
 
15
- // Keyed by `${date}|${force ? 1 : 0}` — force=1 requests never coalesce
16
- // with force=0 (different key), matching spec §S2.
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, {
24
- progress: { phase: "enrich", index: ev.index, total: ev.total },
25
- });
26
- } else if (ev.type === "progress") {
27
- updateJob(jobId, {
28
- progress: { phase: ev.phase, bytes: ev.bytes },
29
- });
30
- }
31
- }
32
-
33
15
  export async function GET(_req: Request, ctx: Params) {
34
16
  const { date } = await ctx.params;
35
- if (!isValidDate(date)) {
36
- return new Response(JSON.stringify({ error: "invalid date" }), {
37
- status: 400, headers: { "content-type": "application/json" },
38
- });
39
- }
40
- if (date > todayLocal()) {
41
- return new Response(JSON.stringify({ error: "future date" }), {
42
- status: 400, headers: { "content-type": "application/json" },
43
- });
44
- }
45
-
46
- if (date === todayLocal()) {
47
- const cached = getTodayDigestFromCache(date, Date.now());
48
- if (cached) {
49
- return new Response(JSON.stringify(cached), {
50
- status: 200, headers: { "content-type": "application/json" },
51
- });
52
- }
53
- return new Response(JSON.stringify({ pending: true, today: true }), {
54
- status: 200, headers: { "content-type": "application/json" },
55
- });
56
- }
57
-
58
- const cached = readDayDigest(date);
59
- if (cached) {
60
- return new Response(JSON.stringify(cached), {
61
- status: 200, headers: { "content-type": "application/json" },
62
- });
63
- }
64
- return new Response(JSON.stringify({ pending: true }), {
65
- status: 200, headers: { "content-type": "application/json" },
17
+ const valid = isValidDate(date);
18
+ const isFuture = valid && date > todayLocal();
19
+ return handleDigestGet({
20
+ key: valid ? date : null,
21
+ invalidResponse: jsonResponse({ error: "invalid date" }, 400),
22
+ isFuture,
23
+ futureResponse: jsonResponse({ error: "future date" }, 400),
24
+ isCurrent: valid && date === todayLocal(),
25
+ readCurrentCache: () => getTodayDigestFromCache(date, Date.now()),
26
+ readPersisted: () => readDayDigest(date),
27
+ pendingCurrentPayload: { pending: true, today: true },
28
+ currentFallsBackToPersisted: false,
66
29
  });
67
30
  }
68
31
 
69
32
  export async function POST(req: Request, ctx: Params) {
70
33
  const { date } = await ctx.params;
71
- if (!isValidDate(date)) {
72
- return new Response(JSON.stringify({ error: "invalid date" }), { status: 400 });
73
- }
74
- if (date > todayLocal()) {
75
- return new Response(JSON.stringify({ error: "future date" }), { status: 400 });
76
- }
77
-
34
+ const valid = isValidDate(date);
35
+ const isFuture = valid && date > todayLocal();
78
36
  const url = new URL(req.url);
79
37
  const force = url.searchParams.get("force") === "1";
80
- const key = `day|${date}|${force ? 1 : 0}`;
81
-
82
- const encoder = new TextEncoder();
83
38
  const settings = readSettings();
84
39
 
85
- const stream = new ReadableStream({
86
- async start(controller) {
87
- let closed = false;
88
- function send(event: PipelineEvent) {
89
- if (closed) return;
90
- try {
91
- controller.enqueue(
92
- encoder.encode(`event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`),
93
- );
94
- } catch {
95
- closed = true;
96
- }
97
- }
98
- function finish() {
99
- if (!closed) {
100
- try { controller.close(); } catch { /* already */ }
101
- closed = true;
102
- }
103
- }
104
-
105
- const alreadyInflight = coalescer.inflight(key);
106
- let jobId: string | null = null;
107
-
108
- try {
109
- await coalescer.run(key, async () => {
110
- if (alreadyInflight) return;
111
- jobId = registerJob({
112
- kind: "digest.day",
113
- label: `Day digest · ${date}`,
114
- target: date,
115
- caller: "user",
116
- });
117
- updateJob(jobId, { status: "running" });
118
- for await (const ev of runDayDigestPipeline(date, {
119
- settings: settings.ai_features,
120
- force,
121
- todayLocalDay: todayLocal(),
122
- })) {
123
- send(ev);
124
- if (jobId) updateJobFromEvent(jobId, ev);
125
- }
126
- if (jobId) completeJob(jobId, `/day/${date}`);
127
- });
128
-
129
- if (alreadyInflight) {
130
- const d = readDayDigest(date) ?? getTodayDigestFromCache(date, Date.now());
131
- if (d) send({ type: "digest", digest: d });
132
- send({ type: "status", phase: "persist", text: "coalesced with in-flight request" });
133
- }
134
- } catch (err) {
135
- send({ type: "error", message: (err as Error).message });
136
- if (jobId) failJob(jobId, (err as Error).message);
137
- } finally {
138
- try { controller.enqueue(encoder.encode(`event: done\ndata: {}\n\n`)); } catch { /* ignore */ }
139
- finish();
140
- }
141
- },
142
- });
143
-
144
- return new Response(stream, {
145
- headers: {
146
- "Content-Type": "text/event-stream",
147
- "Cache-Control": "no-cache, no-transform",
148
- Connection: "keep-alive",
149
- "X-Accel-Buffering": "no",
150
- },
40
+ return handleDigestPost({
41
+ key: valid ? date : null,
42
+ invalidResponse: jsonResponseBare({ error: "invalid date" }, 400),
43
+ isFuture,
44
+ futureResponse: jsonResponseBare({ error: "future date" }, 400),
45
+ coalesceScope: "day",
46
+ force,
47
+ jobKind: "digest.day",
48
+ jobLabel: `Day digest · ${date}`,
49
+ resultUrl: `/day/${date}`,
50
+ runPipeline: () => runDayDigestPipeline(date, {
51
+ settings: settings.ai_features,
52
+ force,
53
+ todayLocalDay: todayLocal(),
54
+ }),
55
+ readDigestForCoalescedRequest: () =>
56
+ readDayDigest(date) ?? getTodayDigestFromCache(date, Date.now()),
151
57
  });
152
58
  }
@@ -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
  }