fleetlens 0.6.4 → 0.7.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.
- package/app/apps/web/.next/BUILD_ID +1 -1
- package/app/apps/web/.next/app-path-routes-manifest.json +1 -0
- package/app/apps/web/.next/build-manifest.json +5 -5
- package/app/apps/web/.next/prerender-manifest.json +3 -3
- package/app/apps/web/.next/routes-manifest.json +6 -0
- package/app/apps/web/.next/server/app/_global-error/page/build-manifest.json +2 -2
- package/app/apps/web/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/_global-error.html +1 -1
- package/app/apps/web/.next/server/app/_global-error.rsc +1 -1
- package/app/apps/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/app/apps/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/app/apps/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/app/apps/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/app/apps/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/app/apps/web/.next/server/app/_not-found/page/build-manifest.json +2 -2
- package/app/apps/web/.next/server/app/_not-found/page.js +3 -3
- package/app/apps/web/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/api/ask/route.js +2 -2
- package/app/apps/web/.next/server/app/api/ask/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/digest/day/[date]/route.js +3 -3
- package/app/apps/web/.next/server/app/api/digest/day/[date]/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/digest/month/[yearMonth]/route.js +3 -3
- package/app/apps/web/.next/server/app/api/digest/month/[yearMonth]/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/digest/month-index/route.js +1 -1
- package/app/apps/web/.next/server/app/api/digest/month-index/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/digest/week/[startDate]/route.js +3 -3
- package/app/apps/web/.next/server/app/api/digest/week/[startDate]/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/digest/week-index/route.js +1 -1
- package/app/apps/web/.next/server/app/api/digest/week-index/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/events/route.js +2 -2
- package/app/apps/web/.next/server/app/api/events/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/jobs/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/runs/[runId]/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/runs/[runId]/stream/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/settings/route.js +3 -3
- package/app/apps/web/.next/server/app/api/settings/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/changelog/page/app-paths-manifest.json +3 -0
- package/app/apps/web/.next/server/app/changelog/page/build-manifest.json +17 -0
- package/app/apps/web/.next/server/app/changelog/page/next-font-manifest.json +6 -0
- package/app/apps/web/.next/server/app/changelog/page/react-loadable-manifest.json +1 -0
- package/app/apps/web/.next/server/app/changelog/page/server-reference-manifest.json +4 -0
- package/app/apps/web/.next/server/app/changelog/page.js +17 -0
- package/app/apps/web/.next/server/app/changelog/page.js.map +5 -0
- package/app/apps/web/.next/server/app/changelog/page.js.nft.json +1 -0
- package/app/apps/web/.next/server/app/changelog/page_client-reference-manifest.js +3 -0
- package/app/apps/web/.next/server/app/day/[date]/page/build-manifest.json +2 -2
- package/app/apps/web/.next/server/app/day/[date]/page.js +4 -4
- package/app/apps/web/.next/server/app/day/[date]/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/day/[date]/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/day/page/build-manifest.json +2 -2
- package/app/apps/web/.next/server/app/day/page.js +3 -3
- package/app/apps/web/.next/server/app/day/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/day/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/digest/[date]/page/build-manifest.json +2 -2
- package/app/apps/web/.next/server/app/digest/[date]/page.js +3 -3
- package/app/apps/web/.next/server/app/digest/[date]/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/digest/[date]/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/digest/page/build-manifest.json +2 -2
- package/app/apps/web/.next/server/app/digest/page.js +3 -3
- package/app/apps/web/.next/server/app/digest/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/digest/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/insights/[key]/page/build-manifest.json +2 -2
- package/app/apps/web/.next/server/app/insights/[key]/page.js +3 -3
- package/app/apps/web/.next/server/app/insights/[key]/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/insights/[key]/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/insights/page/build-manifest.json +2 -2
- package/app/apps/web/.next/server/app/insights/page.js +3 -3
- package/app/apps/web/.next/server/app/insights/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/insights/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/page/build-manifest.json +2 -2
- package/app/apps/web/.next/server/app/page.js +4 -4
- package/app/apps/web/.next/server/app/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/parallelism/page/build-manifest.json +2 -2
- package/app/apps/web/.next/server/app/parallelism/page.js +3 -3
- package/app/apps/web/.next/server/app/parallelism/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/parallelism/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/projects/[slug]/page/build-manifest.json +2 -2
- package/app/apps/web/.next/server/app/projects/[slug]/page.js +4 -4
- package/app/apps/web/.next/server/app/projects/[slug]/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/projects/[slug]/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/projects/page/build-manifest.json +2 -2
- package/app/apps/web/.next/server/app/projects/page.js +4 -4
- package/app/apps/web/.next/server/app/projects/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/runs/page/build-manifest.json +2 -2
- package/app/apps/web/.next/server/app/runs/page.js +3 -3
- package/app/apps/web/.next/server/app/runs/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/runs/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/sessions/[id]/page/build-manifest.json +2 -2
- package/app/apps/web/.next/server/app/sessions/[id]/page.js +4 -4
- package/app/apps/web/.next/server/app/sessions/[id]/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/sessions/[id]/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/sessions/page/build-manifest.json +2 -2
- package/app/apps/web/.next/server/app/sessions/page.js +4 -4
- package/app/apps/web/.next/server/app/sessions/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/settings/page/build-manifest.json +2 -2
- package/app/apps/web/.next/server/app/settings/page.js +3 -3
- package/app/apps/web/.next/server/app/settings/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/usage/page/build-manifest.json +2 -2
- package/app/apps/web/.next/server/app/usage/page.js +3 -3
- package/app/apps/web/.next/server/app/usage/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/usage/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app-paths-manifest.json +1 -0
- package/app/apps/web/.next/server/chunks/{[externals]__0o43ejh._.js → [externals]__0fpbr8-._.js} +2 -2
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0-vgrh1._.js +1 -1
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0.z48ue._.js +3 -0
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0dctov1._.js +5 -0
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0e8n_x4._.js +1 -1
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0jprdks._.js +2 -2
- package/app/apps/web/.next/server/chunks/node_modules_next_036my~r._.js +1 -1
- package/app/apps/web/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_0ck.78s.js +1 -1
- package/app/apps/web/.next/server/chunks/packages_09hmrqx._.js +3 -0
- package/app/apps/web/.next/server/chunks/packages_118y1lp._.js +3 -0
- package/app/apps/web/.next/server/chunks/packages_entries_dist_node_0bckr7-.js +26 -17
- package/app/apps/web/.next/server/chunks/packages_parser_dist_fs_122oe0-.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/{[root-of-the-server]__0vvlth5._.js → [root-of-the-server]__02~v50d._.js} +2 -2
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__04eq6z2._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__09rjz7_._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0e.gxk3._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0p1x6v2._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0r4tks8._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0rvzdnx._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0sp6.jt._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0tejjy_._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0w52fbc._.js +3 -3
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0wwa6gh._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0ytdqxt._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0yv-a8z._.js +12 -0
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__10~b4bf._.js +12 -0
- package/app/apps/web/.next/server/chunks/ssr/_02s1n-x._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/_0woap-3._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/_0xu2pvc._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/apps_web__next-internal_server_app_changelog_page_actions_12_04gy.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/apps_web_app_day_[date]_day-view_tsx_0wjoe1u._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/apps_web_app_settings_ai-features-form_tsx_01p699p._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/apps_web_components_0ly1xes._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/apps_web_components_0txtsv5._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/apps_web_components_changelog-mark-read_tsx_0hm41sv._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/apps_web_lib_0m67-xi._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/{node_modules_0i2xw~e._.js → node_modules_0fx6-.x._.js} +2 -2
- package/app/apps/web/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0uwo3v3.js +4 -0
- package/app/apps/web/.next/server/chunks/ssr/packages_entries_dist_node_0_271yr.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/packages_parser_dist_10_oegc._.js +3 -0
- package/app/apps/web/.next/server/middleware-build-manifest.js +5 -5
- package/app/apps/web/.next/server/pages/500.html +1 -1
- package/app/apps/web/.next/server/server-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/server-reference-manifest.json +1 -1
- package/app/apps/web/.next/static/chunks/0.3mqbj24kwtc.js +6 -0
- package/app/apps/web/.next/static/chunks/05b689hq3x49w.js +1 -0
- package/app/apps/web/.next/static/chunks/07-fq4im3pmjp.js +4 -0
- package/app/apps/web/.next/static/chunks/{0mykrrv810ra-.js → 0kjot8367asxe.js} +1 -1
- package/app/apps/web/.next/static/chunks/0n3-prt61k7pr.js +1 -0
- package/app/apps/web/.next/static/chunks/{0_7303jr0sf0w.js → 0q2vd~aj.1hpz.js} +3 -3
- package/app/apps/web/.next/static/chunks/0qmh5da6gfm56.js +1 -0
- package/app/apps/web/.next/static/chunks/{0k9pksi_i.odf.js → 111-bgx08j2nq.js} +1 -1
- package/app/apps/web/.next/static/chunks/179_t_igg8ghh.js +14 -0
- package/app/apps/web/.next/static/chunks/{turbopack-0_p36yduv.waj.js → turbopack-00w.w1u3c~d-6.js} +1 -1
- package/app/apps/web/app/api/ask/route.ts +1 -1
- package/app/apps/web/app/api/digest/week/[startDate]/route.ts +2 -1
- package/app/apps/web/app/api/settings/route.ts +1 -0
- package/app/apps/web/app/changelog/page.tsx +238 -0
- package/app/apps/web/app/insights/[key]/page.tsx +8 -2
- package/app/apps/web/app/layout.tsx +12 -7
- package/app/apps/web/app/page.tsx +19 -15
- package/app/apps/web/app/parallelism/gantt-chart.tsx +7 -18
- package/app/apps/web/app/projects/projects-view.tsx +3 -0
- package/app/apps/web/app/sessions/[id]/session-view.tsx +168 -41
- package/app/apps/web/app/sessions/page.tsx +22 -12
- package/app/apps/web/app/sessions/sessions-grid.tsx +82 -9
- package/app/apps/web/app/settings/ai-features-form.tsx +20 -1
- package/app/apps/web/app/settings/page.tsx +1 -0
- package/app/apps/web/app/usage/page.tsx +161 -52
- package/app/apps/web/components/agent-badge.tsx +43 -0
- package/app/apps/web/components/agent-icon.tsx +38 -0
- package/app/apps/web/components/agent-mix-chip.tsx +42 -0
- package/app/apps/web/components/{ask-claude.tsx → ask.tsx} +13 -12
- package/app/apps/web/components/changelog-icon.tsx +62 -0
- package/app/apps/web/components/changelog-mark-read.tsx +12 -0
- package/app/apps/web/components/day-digest.tsx +1 -1
- package/app/apps/web/components/live-sessions-widget.tsx +5 -1
- package/app/apps/web/components/outcome-pill.tsx +7 -2
- package/app/apps/web/components/sidebar.tsx +8 -1
- package/app/apps/web/components/week-digest.tsx +2 -0
- package/app/apps/web/lib/ai/session-summary.ts +1 -1
- package/app/apps/web/lib/calibration-data.ts +4 -64
- package/app/apps/web/lib/changelog-data.generated.ts +3 -0
- package/app/apps/web/lib/changelog.ts +67 -0
- package/app/apps/web/lib/cycle-peaks.test.ts +185 -0
- package/app/apps/web/lib/cycle-peaks.ts +104 -0
- package/app/apps/web/lib/data.ts +7 -8
- package/app/apps/web/lib/date-range.ts +9 -2
- package/app/apps/web/lib/usage-data.ts +24 -1
- package/app/apps/web/lib/validate-settings.ts +1 -0
- package/app/apps/web/package.json +1 -1
- package/app/apps/web/tsconfig.tsbuildinfo +1 -1
- package/dist/daemon-worker.js +3162 -2354
- package/dist/index.js +4419 -3742
- package/package.json +1 -1
- package/app/apps/web/.next/server/chunks/[externals]__0_xztrf._.js +0 -3
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0455vu3._.js +0 -5
- package/app/apps/web/.next/server/chunks/packages_0e7q9u_._.js +0 -3
- package/app/apps/web/.next/server/chunks/packages_0n6p2ot._.js +0 -3
- package/app/apps/web/.next/server/chunks/packages_parser_dist_fs_0bc7ptm.js +0 -3
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0-94ji~._.js +0 -3
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0_6.inc._.js +0 -3
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0gio_ck._.js +0 -3
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0jdyefm._.js +0 -12
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0ln9jsw._.js +0 -3
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0u5d7de._.js +0 -12
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0~_8vff._.js +0 -3
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__11v.eqw._.js +0 -3
- package/app/apps/web/.next/server/chunks/ssr/apps_web_lib_usage-data_ts_0xtv99n._.js +0 -3
- package/app/apps/web/.next/server/chunks/ssr/packages_parser_dist_0-jvt~a._.js +0 -3
- package/app/apps/web/.next/static/chunks/04m3slg_5ph02.js +0 -1
- package/app/apps/web/.next/static/chunks/0coa1800xqgfj.js +0 -1
- package/app/apps/web/.next/static/chunks/0k1psab.bybh9.js +0 -14
- package/app/apps/web/.next/static/chunks/0xfu09fn_358k.js +0 -4
- package/app/apps/web/.next/static/chunks/17u2ww8ya5dv1.js +0 -6
- /package/app/apps/web/.next/static/{2jcMG-92q2XKei8dN7BMd → tiK9aMHXkgnKLNmxj9J1P}/_buildManifest.js +0 -0
- /package/app/apps/web/.next/static/{2jcMG-92q2XKei8dN7BMd → tiK9aMHXkgnKLNmxj9J1P}/_clientMiddlewareManifest.js +0 -0
- /package/app/apps/web/.next/static/{2jcMG-92q2XKei8dN7BMd → tiK9aMHXkgnKLNmxj9J1P}/_ssgManifest.js +0 -0
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
import { formatRelative, prettyProjectName } from "@/lib/format";
|
|
22
22
|
import { ThemeToggle } from "@/components/theme-toggle";
|
|
23
23
|
import { UsageSidebar } from "@/components/usage-sidebar";
|
|
24
|
+
import { ChangelogIcon } from "@/components/changelog-icon";
|
|
24
25
|
import type { UsageSnapshot } from "@/lib/usage-data";
|
|
25
26
|
|
|
26
27
|
export type ProjectRef = {
|
|
@@ -59,11 +60,13 @@ export function Sidebar({
|
|
|
59
60
|
totalSessions,
|
|
60
61
|
currentUsage,
|
|
61
62
|
version,
|
|
63
|
+
latestChangelogVersion,
|
|
62
64
|
}: {
|
|
63
65
|
projects: ProjectRef[];
|
|
64
66
|
totalSessions: number;
|
|
65
67
|
currentUsage: UsageSnapshot | null;
|
|
66
68
|
version: string;
|
|
69
|
+
latestChangelogVersion: string | null;
|
|
67
70
|
}) {
|
|
68
71
|
const pathname = usePathname();
|
|
69
72
|
const [pins, setPins] = useState<Set<string>>(new Set());
|
|
@@ -156,7 +159,7 @@ export function Sidebar({
|
|
|
156
159
|
</span>
|
|
157
160
|
</div>
|
|
158
161
|
<div style={{ fontSize: 11, color: "var(--af-text-tertiary)" }}>
|
|
159
|
-
|
|
162
|
+
Multi-agent fleet analytics
|
|
160
163
|
</div>
|
|
161
164
|
</div>
|
|
162
165
|
</div>
|
|
@@ -283,6 +286,10 @@ export function Sidebar({
|
|
|
283
286
|
<span style={{ flex: 1, minWidth: 0 }}>
|
|
284
287
|
{totalSessions} sessions · {projects.length} projects
|
|
285
288
|
</span>
|
|
289
|
+
<ChangelogIcon
|
|
290
|
+
latestVersion={latestChangelogVersion}
|
|
291
|
+
active={pathname === "/changelog"}
|
|
292
|
+
/>
|
|
286
293
|
<Link
|
|
287
294
|
href="/settings"
|
|
288
295
|
aria-label="Settings"
|
|
@@ -6,6 +6,7 @@ import { Check, Copy } from "lucide-react";
|
|
|
6
6
|
import type { WeekDigest as WeekDigestType, DayHelpfulness, WeekTopSession, SessionPin } from "@claude-lens/entries";
|
|
7
7
|
import { renderWithFlagChips } from "./flag-chip";
|
|
8
8
|
import { GoalBar } from "./goal-bar";
|
|
9
|
+
import { AgentBadge } from "./agent-badge";
|
|
9
10
|
|
|
10
11
|
const HELP_COLORS: Record<NonNullable<DayHelpfulness>, string> = {
|
|
11
12
|
essential: "#48bb78",
|
|
@@ -1742,6 +1743,7 @@ function TopSessionCard({ session }: { session: WeekTopSession }) {
|
|
|
1742
1743
|
>
|
|
1743
1744
|
{session.project_display}
|
|
1744
1745
|
</Link>
|
|
1746
|
+
<AgentBadge agent={session.agent} />
|
|
1745
1747
|
<Link
|
|
1746
1748
|
href={`/digest/${session.date}`}
|
|
1747
1749
|
style={{
|
|
@@ -47,70 +47,10 @@ export type PredictedSeriesByKey = {
|
|
|
47
47
|
seven_day_sonnet: { capturedAt: number; util: number }[];
|
|
48
48
|
};
|
|
49
49
|
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
// back to the JSONL-derived prediction (cold-start path).
|
|
55
|
-
export type CyclePeak = {
|
|
56
|
-
endsAt: string;
|
|
57
|
-
peakPct: number;
|
|
58
|
-
source: "real" | "predicted";
|
|
59
|
-
current: boolean;
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
// Per-cycle peak history for a single window (5h or 7d). Used by the
|
|
63
|
-
// dashboard's "previous cycles" trend strip — humans understand "last 4
|
|
64
|
-
// weeks: 86%, 92%, 44%, 34% (current)" much better than a single number.
|
|
65
|
-
// Limited to the last `maxCycles` (most recent) so the visual stays compact.
|
|
66
|
-
export function previousCyclesTrend(
|
|
67
|
-
dump: CalibrationDump | null,
|
|
68
|
-
window: "5h" | "7d",
|
|
69
|
-
maxCycles = 6,
|
|
70
|
-
): CyclePeak[] {
|
|
71
|
-
if (!dump || dump.curve.length < 2) return [];
|
|
72
|
-
const HOUR = 3_600_000;
|
|
73
|
-
const cycleKey: "cycle_end_5h" | "cycle_end_7d" = window === "5h" ? "cycle_end_5h" : "cycle_end_7d";
|
|
74
|
-
const predKey: "pred_5h" | "pred_7d" = window === "5h" ? "pred_5h" : "pred_7d";
|
|
75
|
-
const realKey: "real_5h" | "real_7d" = window === "5h" ? "real_5h" : "real_7d";
|
|
76
|
-
|
|
77
|
-
// Hour-round the cycle key so millisecond-jittered resets collapse.
|
|
78
|
-
const byCycle = new Map<number, typeof dump.curve>();
|
|
79
|
-
for (const p of dump.curve) {
|
|
80
|
-
const k = p[cycleKey];
|
|
81
|
-
if (!k) continue;
|
|
82
|
-
const ms = Date.parse(k);
|
|
83
|
-
if (Number.isNaN(ms)) continue;
|
|
84
|
-
const bucket = Math.round(ms / HOUR) * HOUR;
|
|
85
|
-
const arr = byCycle.get(bucket) ?? [];
|
|
86
|
-
arr.push(p);
|
|
87
|
-
byCycle.set(bucket, arr);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const nowMs = Date.now();
|
|
91
|
-
const cycles: CyclePeak[] = [];
|
|
92
|
-
for (const [endMs, points] of Array.from(byCycle.entries()).sort((a, b) => a[0] - b[0])) {
|
|
93
|
-
// Take the max across BOTH real and predicted — when the daemon goes
|
|
94
|
-
// dark before cycle close, predicted forward-extrapolation often
|
|
95
|
-
// exceeds the last observed value, and the cycle's true peak is the
|
|
96
|
-
// predicted close, not the last poll. `source` reflects which side won.
|
|
97
|
-
let peak = 0;
|
|
98
|
-
let source: "real" | "predicted" = "predicted";
|
|
99
|
-
for (const p of points) {
|
|
100
|
-
const r = p[realKey];
|
|
101
|
-
if (typeof r === "number" && r > peak) { peak = r; source = "real"; }
|
|
102
|
-
const v = p[predKey] ?? 0;
|
|
103
|
-
if (v > peak) { peak = v; source = "predicted"; }
|
|
104
|
-
}
|
|
105
|
-
cycles.push({
|
|
106
|
-
endsAt: new Date(endMs).toISOString(),
|
|
107
|
-
peakPct: Math.round(peak * 10) / 10,
|
|
108
|
-
source,
|
|
109
|
-
current: endMs > nowMs,
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
return cycles.slice(-maxCycles);
|
|
113
|
-
}
|
|
50
|
+
// Re-export the pure cycle-peak helpers so existing call sites keep
|
|
51
|
+
// working. New tests import directly from ./cycle-peaks (no server-only).
|
|
52
|
+
export { previousCyclesTrend } from "./cycle-peaks";
|
|
53
|
+
export type { CyclePeak } from "./cycle-peaks";
|
|
114
54
|
|
|
115
55
|
// Converts the calibration dump into the same shape UsageChart expects to
|
|
116
56
|
// overlay on each burndown plot. seven_day_sonnet has no predictor today
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
// AUTOGENERATED by scripts/build-changelog.mjs from CHANGELOG.md.
|
|
2
|
+
// Do not edit by hand — run `pnpm changelog:sync`.
|
|
3
|
+
export const RAW_CHANGELOG = "# Changelog\n\nAll notable user-facing changes to the Fleetlens CLI (`fleetlens` on npm) are\nrecorded here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).\nThe team-server has its own log at `packages/team-server/CHANGELOG.md`.\n\n## [0.7.1] — 2026-05-07\n\n### Fixed\n- Cycle-peaks trend no longer renders two bars labeled the same day. When Anthropic's rolling 7-day window slid its anchor between consecutive polls, the daemon's bucketer (hour-rounded) recorded both reset boundaries as distinct cycles. They're now merged within a 12 h tolerance — same fix in `apps/web/lib/cycle-peaks.ts` (used by `/usage`) and in `packages/cli/src/team/sync.ts` (the daemon's team-server push), so both the personal `/usage` chart and the team-server's plan-utilization view stay consistent. 5-hour cycles keep no merge tolerance (their anchor is stable; merging would collapse legitimate distinct cycles).\n\n## [0.6.4] — 2026-05-04\n\n### Fixed\n- Cached calibration events per-file so `/usage` warm renders complete in well under a second on large session histories.\n\n## [0.6.3] — 2026-05-04\n\n### Fixed\n- Dropped poisoned auto-week locks left behind by interrupted runs; backfill now gates on the live pipeline lock instead.\n\n## [0.6.2] — 2026-05-04\n\n### Added\n- Last week's digest auto-generates on launch when AI features are on. Disable in Settings → AI features.\n\n## [0.6.1] — 2026-05-04\n\n### Changed\n- Smaller install size — published bundle no longer drags in unused workspace metadata.\n\n## [0.6.0] — 2026-04-30\n\n### Added\n- V2 perception layer. Daily, weekly, and monthly digests synthesized from per-session entries via a strict hierarchical pipeline (entry → day → week → month). Past-period digests are immutable on disk; current-period digests use a 10-min in-memory TTL.\n- `/insights` route with on-demand week and month digest generation, picker history, and force-regen.\n- `fleetlens digest day | week | month [--json]` — CLI surface that reproduces the same digests served at `/insights`.\n- `fleetlens entries [--day | --session | --all] [--json]` — inspect the per-session entry primitive.\n\n### Changed\n- Renamed \"active time\" to \"agent time\" across the dashboard and CLI for clarity.\n\n## [0.4.1] — 2026-04-23\n\n### Added\n- Concurrency view on `/parallelism` page. Bursts colored teal for same-project and purple for cross-project (the genuinely interesting fleet-work signal).\n- Plan utilization sparkline on the sidebar, fed by the usage daemon's 5-min snapshots.\n\n### Fixed\n- Auto-update no longer leaves a zombie web server on the old binary; both server and daemon are torn down before re-exec.\n\n## [0.4.0] — 2026-04-18\n\n### Added\n- Usage daemon (`fleetlens daemon …`) polls `/api/oauth/usage` every 5 min and appends snapshots to `~/.cclens/usage.jsonl`. `fleetlens start` and `fleetlens stop` manage server + daemon together.\n- `fleetlens usage --history` for inspecting past plan utilization.\n\n## [0.3.0] — 2026-04-10\n\n### Added\n- Project rollups with worktree aggregation. Sessions inside `<project>/.worktrees/<name>` roll up under their parent canonical project with a `+N wt` badge.\n- Pinned projects in the sidebar (per-browser via localStorage).\n\n## [0.2.0] — 2026-04-01\n\n### Changed\n- Active-segment computation now uses all timestamped events (system + summary + sidechain + conversational) instead of conversational-only. Earlier filter undercounted by up to 100x on tool-heavy sessions.\n\n## [0.1.0] — 2026-03-15\n\n### Added\n- Initial release. Local-only dashboard for Claude Code sessions: timeline, projects, daily activity bucketing, parallelism detection, per-model cost estimation. Reads JSONL transcripts from `~/.claude/projects/`.\n";
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { RAW_CHANGELOG } from "./changelog-data.generated";
|
|
2
|
+
|
|
3
|
+
export type ChangelogSection = {
|
|
4
|
+
/** "Added" | "Changed" | "Fixed" | etc. — verbatim from `### <kind>` heading. */
|
|
5
|
+
kind: string;
|
|
6
|
+
bullets: string[];
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type ChangelogEntry = {
|
|
10
|
+
version: string;
|
|
11
|
+
date: string | null;
|
|
12
|
+
sections: ChangelogSection[];
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const STORAGE_KEY = "cclens:changelog-last-seen";
|
|
16
|
+
|
|
17
|
+
const VERSION_HEADING = /^##\s+\[([^\]]+)\](?:\s+[—\-–]\s+(\S+))?\s*$/;
|
|
18
|
+
const SECTION_HEADING = /^###\s+(.+?)\s*$/;
|
|
19
|
+
const BULLET = /^[-*]\s+(.+?)\s*$/;
|
|
20
|
+
|
|
21
|
+
export function parseChangelog(raw: string): ChangelogEntry[] {
|
|
22
|
+
const lines = raw.split(/\r?\n/);
|
|
23
|
+
const entries: ChangelogEntry[] = [];
|
|
24
|
+
let current: ChangelogEntry | null = null;
|
|
25
|
+
let section: ChangelogSection | null = null;
|
|
26
|
+
let bulletBuf: string[] = [];
|
|
27
|
+
|
|
28
|
+
function flushBullet(): void {
|
|
29
|
+
if (section && bulletBuf.length > 0) {
|
|
30
|
+
section.bullets.push(bulletBuf.join(" ").trim());
|
|
31
|
+
bulletBuf = [];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
for (const line of lines) {
|
|
36
|
+
const v = VERSION_HEADING.exec(line);
|
|
37
|
+
if (v) {
|
|
38
|
+
flushBullet();
|
|
39
|
+
section = null;
|
|
40
|
+
current = { version: v[1].trim(), date: v[2]?.trim() ?? null, sections: [] };
|
|
41
|
+
entries.push(current);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (!current) continue;
|
|
45
|
+
const s = SECTION_HEADING.exec(line);
|
|
46
|
+
if (s) {
|
|
47
|
+
flushBullet();
|
|
48
|
+
section = { kind: s[1].trim(), bullets: [] };
|
|
49
|
+
current.sections.push(section);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const b = BULLET.exec(line);
|
|
53
|
+
if (b) {
|
|
54
|
+
flushBullet();
|
|
55
|
+
bulletBuf = [b[1]];
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (bulletBuf.length > 0 && line.trim().length > 0) {
|
|
59
|
+
bulletBuf.push(line.trim());
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
flushBullet();
|
|
63
|
+
return entries;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const ENTRIES: ChangelogEntry[] = parseChangelog(RAW_CHANGELOG);
|
|
67
|
+
export const LATEST_VERSION: string | null = ENTRIES[0]?.version ?? null;
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
previousCyclesTrend,
|
|
4
|
+
type CalibrationCurvePoint,
|
|
5
|
+
type CalibrationLike,
|
|
6
|
+
} from "./cycle-peaks";
|
|
7
|
+
|
|
8
|
+
function dump(curve: CalibrationCurvePoint[]): CalibrationLike {
|
|
9
|
+
return { curve };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function point(overrides: Partial<CalibrationCurvePoint>): CalibrationCurvePoint {
|
|
13
|
+
return {
|
|
14
|
+
ts: "2026-05-01T00:00:00.000Z",
|
|
15
|
+
real_5h: null,
|
|
16
|
+
pred_5h: 0,
|
|
17
|
+
real_7d: null,
|
|
18
|
+
pred_7d: 0,
|
|
19
|
+
cycle_end_5h: null,
|
|
20
|
+
cycle_end_7d: null,
|
|
21
|
+
...overrides,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const NOW = Date.parse("2026-05-08T00:00:00.000Z");
|
|
26
|
+
|
|
27
|
+
describe("previousCyclesTrend (7d)", () => {
|
|
28
|
+
it("merges two cycle ends within 12 h into a single cycle, latest endsAt + max peak", () => {
|
|
29
|
+
// Regression: Anthropic's rolling 7-day window slid its anchor between
|
|
30
|
+
// two polls on the same day, producing two distinct hour-buckets.
|
|
31
|
+
// Without the merge, /usage and the team-server both rendered two
|
|
32
|
+
// bars labeled the same day.
|
|
33
|
+
const result = previousCyclesTrend(
|
|
34
|
+
dump([
|
|
35
|
+
point({ cycle_end_7d: "2026-05-07T07:00:00.000Z", real_7d: 6 }),
|
|
36
|
+
point({ cycle_end_7d: "2026-05-07T09:00:00.000Z", real_7d: 91 }),
|
|
37
|
+
]),
|
|
38
|
+
"7d",
|
|
39
|
+
6,
|
|
40
|
+
NOW,
|
|
41
|
+
);
|
|
42
|
+
expect(result).toHaveLength(1);
|
|
43
|
+
expect(result[0].endsAt).toBe("2026-05-07T09:00:00.000Z");
|
|
44
|
+
expect(result[0].peakPct).toBe(91);
|
|
45
|
+
expect(result[0].source).toBe("real");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("keeps cycles 7 d apart as distinct entries", () => {
|
|
49
|
+
const result = previousCyclesTrend(
|
|
50
|
+
dump([
|
|
51
|
+
point({ cycle_end_7d: "2026-04-30T12:00:00.000Z", real_7d: 50 }),
|
|
52
|
+
point({ cycle_end_7d: "2026-05-07T12:00:00.000Z", real_7d: 80 }),
|
|
53
|
+
]),
|
|
54
|
+
"7d",
|
|
55
|
+
6,
|
|
56
|
+
NOW,
|
|
57
|
+
);
|
|
58
|
+
expect(result).toHaveLength(2);
|
|
59
|
+
expect(result.map((r) => r.endsAt)).toEqual([
|
|
60
|
+
"2026-04-30T12:00:00.000Z",
|
|
61
|
+
"2026-05-07T12:00:00.000Z",
|
|
62
|
+
]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("collapses millisecond-jitter on the same reset boundary", () => {
|
|
66
|
+
const result = previousCyclesTrend(
|
|
67
|
+
dump([
|
|
68
|
+
point({ cycle_end_7d: "2026-05-07T12:00:00.123Z", real_7d: 40 }),
|
|
69
|
+
point({ cycle_end_7d: "2026-05-07T12:00:00.999Z", real_7d: 60 }),
|
|
70
|
+
]),
|
|
71
|
+
"7d",
|
|
72
|
+
6,
|
|
73
|
+
NOW,
|
|
74
|
+
);
|
|
75
|
+
expect(result).toHaveLength(1);
|
|
76
|
+
expect(result[0].peakPct).toBe(60);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("takes max across real AND predicted, preferring real when it wins", () => {
|
|
80
|
+
const result = previousCyclesTrend(
|
|
81
|
+
dump([
|
|
82
|
+
point({ cycle_end_7d: "2026-05-07T12:00:00.000Z", real_7d: 30, pred_7d: 20 }),
|
|
83
|
+
point({ cycle_end_7d: "2026-05-07T12:00:00.000Z", real_7d: 50, pred_7d: 40 }),
|
|
84
|
+
]),
|
|
85
|
+
"7d",
|
|
86
|
+
6,
|
|
87
|
+
NOW,
|
|
88
|
+
);
|
|
89
|
+
expect(result[0].peakPct).toBe(50);
|
|
90
|
+
expect(result[0].source).toBe("real");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("falls back to predicted when no real value is recorded", () => {
|
|
94
|
+
const result = previousCyclesTrend(
|
|
95
|
+
dump([
|
|
96
|
+
point({ cycle_end_7d: "2026-05-07T12:00:00.000Z", pred_7d: 70 }),
|
|
97
|
+
point({ cycle_end_7d: "2026-05-07T12:00:00.000Z", pred_7d: 40 }),
|
|
98
|
+
]),
|
|
99
|
+
"7d",
|
|
100
|
+
6,
|
|
101
|
+
NOW,
|
|
102
|
+
);
|
|
103
|
+
expect(result[0].peakPct).toBe(70);
|
|
104
|
+
expect(result[0].source).toBe("predicted");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("respects maxCycles by returning the most recent N", () => {
|
|
108
|
+
const result = previousCyclesTrend(
|
|
109
|
+
dump([
|
|
110
|
+
point({ cycle_end_7d: "2026-04-02T00:00:00.000Z", real_7d: 10 }),
|
|
111
|
+
point({ cycle_end_7d: "2026-04-09T00:00:00.000Z", real_7d: 20 }),
|
|
112
|
+
point({ cycle_end_7d: "2026-04-16T00:00:00.000Z", real_7d: 0.3 }),
|
|
113
|
+
point({ cycle_end_7d: "2026-04-23T00:00:00.000Z", real_7d: 40 }),
|
|
114
|
+
]),
|
|
115
|
+
"7d",
|
|
116
|
+
2,
|
|
117
|
+
NOW,
|
|
118
|
+
);
|
|
119
|
+
expect(result).toHaveLength(2);
|
|
120
|
+
expect(result.map((r) => r.endsAt)).toEqual([
|
|
121
|
+
"2026-04-16T00:00:00.000Z",
|
|
122
|
+
"2026-04-23T00:00:00.000Z",
|
|
123
|
+
]);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("flags the future-ending cycle as current", () => {
|
|
127
|
+
const result = previousCyclesTrend(
|
|
128
|
+
dump([
|
|
129
|
+
point({ cycle_end_7d: "2026-05-01T00:00:00.000Z", real_7d: 50 }),
|
|
130
|
+
point({ cycle_end_7d: "2026-05-09T00:00:00.000Z", real_7d: 0.7 }),
|
|
131
|
+
]),
|
|
132
|
+
"7d",
|
|
133
|
+
6,
|
|
134
|
+
NOW,
|
|
135
|
+
);
|
|
136
|
+
expect(result.find((r) => r.endsAt.startsWith("2026-05-01"))?.current).toBe(false);
|
|
137
|
+
expect(result.find((r) => r.endsAt.startsWith("2026-05-09"))?.current).toBe(true);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("returns empty when the dump is null or has fewer than two points", () => {
|
|
141
|
+
expect(previousCyclesTrend(null, "7d", 6, NOW)).toEqual([]);
|
|
142
|
+
expect(
|
|
143
|
+
previousCyclesTrend(
|
|
144
|
+
dump([point({ cycle_end_7d: "2026-05-07T12:00:00.000Z" })]),
|
|
145
|
+
"7d",
|
|
146
|
+
6,
|
|
147
|
+
NOW,
|
|
148
|
+
),
|
|
149
|
+
).toEqual([]);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("skips points missing the cycle key", () => {
|
|
153
|
+
const result = previousCyclesTrend(
|
|
154
|
+
dump([
|
|
155
|
+
point({ cycle_end_7d: null, real_7d: 50 }),
|
|
156
|
+
point({ cycle_end_7d: "2026-05-07T12:00:00.000Z", real_7d: 40 }),
|
|
157
|
+
]),
|
|
158
|
+
"7d",
|
|
159
|
+
6,
|
|
160
|
+
NOW,
|
|
161
|
+
);
|
|
162
|
+
expect(result).toHaveLength(1);
|
|
163
|
+
expect(result[0].peakPct).toBe(40);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe("previousCyclesTrend (5h)", () => {
|
|
168
|
+
it("keeps two 5h cycle ends as distinct entries (no merge tolerance for 5h)", () => {
|
|
169
|
+
const result = previousCyclesTrend(
|
|
170
|
+
dump([
|
|
171
|
+
point({ cycle_end_5h: "2026-05-07T10:00:00.000Z", real_5h: 30 }),
|
|
172
|
+
point({ cycle_end_5h: "2026-05-07T15:00:00.000Z", real_5h: 70 }),
|
|
173
|
+
]),
|
|
174
|
+
"5h",
|
|
175
|
+
6,
|
|
176
|
+
NOW,
|
|
177
|
+
);
|
|
178
|
+
expect(result).toHaveLength(2);
|
|
179
|
+
expect(result.map((r) => r.endsAt)).toEqual([
|
|
180
|
+
"2026-05-07T10:00:00.000Z",
|
|
181
|
+
"2026-05-07T15:00:00.000Z",
|
|
182
|
+
]);
|
|
183
|
+
expect(result.map((r) => r.peakPct)).toEqual([30, 70]);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// Pure helpers for deriving per-cycle peak utilization from the daemon's
|
|
2
|
+
// calibration curve. Lives separately from calibration-data.ts so it can
|
|
3
|
+
// be imported in unit tests (calibration-data.ts pulls in `server-only`
|
|
4
|
+
// for its fs-touching helpers).
|
|
5
|
+
|
|
6
|
+
export type CalibrationCurvePoint = {
|
|
7
|
+
ts: string;
|
|
8
|
+
real_5h: number | null;
|
|
9
|
+
pred_5h: number;
|
|
10
|
+
real_7d: number | null;
|
|
11
|
+
pred_7d: number;
|
|
12
|
+
cycle_end_5h?: string | null;
|
|
13
|
+
cycle_end_7d?: string | null;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type CalibrationLike = {
|
|
17
|
+
curve: CalibrationCurvePoint[];
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type CyclePeak = {
|
|
21
|
+
endsAt: string;
|
|
22
|
+
peakPct: number;
|
|
23
|
+
source: "real" | "predicted";
|
|
24
|
+
current: boolean;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const HOUR = 3_600_000;
|
|
28
|
+
|
|
29
|
+
/** History of recent cycles for trend visuals. Each entry is one full
|
|
30
|
+
* cycle — `current: true` for the in-progress cycle, completed otherwise.
|
|
31
|
+
* Limited to the last `maxCycles` so the visual stays compact. */
|
|
32
|
+
export function previousCyclesTrend(
|
|
33
|
+
dump: CalibrationLike | null,
|
|
34
|
+
window: "5h" | "7d",
|
|
35
|
+
maxCycles = 6,
|
|
36
|
+
nowMs: number = Date.now(),
|
|
37
|
+
): CyclePeak[] {
|
|
38
|
+
if (!dump || dump.curve.length < 2) return [];
|
|
39
|
+
const cycleKey: "cycle_end_5h" | "cycle_end_7d" = window === "5h" ? "cycle_end_5h" : "cycle_end_7d";
|
|
40
|
+
const predKey: "pred_5h" | "pred_7d" = window === "5h" ? "pred_5h" : "pred_7d";
|
|
41
|
+
const realKey: "real_5h" | "real_7d" = window === "5h" ? "real_5h" : "real_7d";
|
|
42
|
+
|
|
43
|
+
// Hour-round the cycle key so millisecond-jittered resets collapse, then
|
|
44
|
+
// merge adjacent buckets within a window-specific tolerance. Anthropic's
|
|
45
|
+
// rolling 7-day window can slide its anchor by a few hours within the
|
|
46
|
+
// SAME cycle (different polls report different `seven_day.resets_at`
|
|
47
|
+
// values); without this merge, a cycle that slid would render as two
|
|
48
|
+
// bars labeled the same day. The 5-hour cycle's anchor is stable, so we
|
|
49
|
+
// don't merge there beyond hour-rounding — distinct 5h cycles would
|
|
50
|
+
// otherwise collapse.
|
|
51
|
+
const TOLERANCE_MS = window === "7d" ? 12 * HOUR : 0;
|
|
52
|
+
const byCycle = new Map<number, CalibrationCurvePoint[]>();
|
|
53
|
+
for (const p of dump.curve) {
|
|
54
|
+
const k = p[cycleKey];
|
|
55
|
+
if (!k) continue;
|
|
56
|
+
const ms = Date.parse(k);
|
|
57
|
+
if (Number.isNaN(ms)) continue;
|
|
58
|
+
const bucket = Math.round(ms / HOUR) * HOUR;
|
|
59
|
+
const arr = byCycle.get(bucket) ?? [];
|
|
60
|
+
arr.push(p);
|
|
61
|
+
byCycle.set(bucket, arr);
|
|
62
|
+
}
|
|
63
|
+
const merged = mergeAdjacentBuckets(byCycle, TOLERANCE_MS);
|
|
64
|
+
|
|
65
|
+
const cycles: CyclePeak[] = [];
|
|
66
|
+
for (const { endMs, points } of merged) {
|
|
67
|
+
let peak = 0;
|
|
68
|
+
let source: "real" | "predicted" = "predicted";
|
|
69
|
+
for (const p of points) {
|
|
70
|
+
const r = p[realKey];
|
|
71
|
+
if (typeof r === "number" && r > peak) { peak = r; source = "real"; }
|
|
72
|
+
const v = p[predKey] ?? 0;
|
|
73
|
+
if (v > peak) { peak = v; source = "predicted"; }
|
|
74
|
+
}
|
|
75
|
+
cycles.push({
|
|
76
|
+
endsAt: new Date(endMs).toISOString(),
|
|
77
|
+
peakPct: Math.round(peak * 10) / 10,
|
|
78
|
+
source,
|
|
79
|
+
current: endMs > nowMs,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return cycles.slice(-maxCycles);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Merge buckets whose end-times are within `toleranceMs` of each other.
|
|
86
|
+
* Output sorted ascending by `endMs`. Each merged bucket adopts the
|
|
87
|
+
* LATEST endMs (so the rendered label reflects the most recent reset
|
|
88
|
+
* boundary) and the union of points from all merged buckets. */
|
|
89
|
+
export function mergeAdjacentBuckets<P>(
|
|
90
|
+
byCycle: Map<number, P[]>,
|
|
91
|
+
toleranceMs: number,
|
|
92
|
+
): Array<{ endMs: number; points: P[] }> {
|
|
93
|
+
const merged: Array<{ endMs: number; points: P[] }> = [];
|
|
94
|
+
for (const [endMs, points] of Array.from(byCycle.entries()).sort((a, b) => a[0] - b[0])) {
|
|
95
|
+
const last = merged[merged.length - 1];
|
|
96
|
+
if (last && endMs - last.endMs <= toleranceMs) {
|
|
97
|
+
last.endMs = endMs;
|
|
98
|
+
last.points.push(...points);
|
|
99
|
+
} else {
|
|
100
|
+
merged.push({ endMs, points: [...points] });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return merged;
|
|
104
|
+
}
|
package/app/apps/web/lib/data.ts
CHANGED
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Server-only data access layer.
|
|
3
3
|
*
|
|
4
|
-
* Wraps @claude-lens/parser/fs with a per-request cache so
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Wraps @claude-lens/parser/fs with a per-request cache so a single RSC
|
|
5
|
+
* render doesn't re-scan the agent disk roots for every page that needs
|
|
6
|
+
* the session list. Multi-agent: iterates the parser's AgentSource
|
|
7
|
+
* registry, so adding a new agent means adding one source — no edits here.
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
import "server-only";
|
|
10
11
|
import { cache } from "react";
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
const dataRoot = process.env.CCLENS_DATA_DIR || undefined;
|
|
12
|
+
import { listAllSessions, getAnySession } from "@claude-lens/parser/fs";
|
|
14
13
|
|
|
15
14
|
export const listSessions = cache(async () => {
|
|
16
|
-
return
|
|
15
|
+
return listAllSessions({ limit: 1000 });
|
|
17
16
|
});
|
|
18
17
|
|
|
19
18
|
export const getSession = cache(async (id: string) => {
|
|
20
|
-
return
|
|
19
|
+
return getAnySession(id);
|
|
21
20
|
});
|
|
@@ -7,11 +7,18 @@
|
|
|
7
7
|
|
|
8
8
|
export type RangeKey = "7d" | "30d" | "90d" | "all";
|
|
9
9
|
|
|
10
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* Parse a search-param value into a RangeKey (default: "90d").
|
|
12
|
+
*
|
|
13
|
+
* 90d is chosen as a sensible recent window for the heatmap and activity
|
|
14
|
+
* chart — large enough to show trends, small enough that one stale agent
|
|
15
|
+
* (Codex sessions back to last September, say) doesn't stretch the x-axis
|
|
16
|
+
* across months of empty days. Users can still pick "all" explicitly.
|
|
17
|
+
*/
|
|
11
18
|
export function parseRange(value: string | string[] | undefined): RangeKey {
|
|
12
19
|
const v = Array.isArray(value) ? value[0] : value;
|
|
13
20
|
if (v === "7d" || v === "30d" || v === "90d" || v === "all") return v;
|
|
14
|
-
return "
|
|
21
|
+
return "90d";
|
|
15
22
|
}
|
|
16
23
|
|
|
17
24
|
/**
|
|
@@ -9,6 +9,7 @@ import { cache } from "react";
|
|
|
9
9
|
import { existsSync, readFileSync } from "node:fs";
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
import { homedir } from "node:os";
|
|
12
|
+
import type { AgentKind } from "@claude-lens/parser";
|
|
12
13
|
|
|
13
14
|
export type UsageWindow = {
|
|
14
15
|
utilization: number | null;
|
|
@@ -17,6 +18,9 @@ export type UsageWindow = {
|
|
|
17
18
|
|
|
18
19
|
export type UsageSnapshot = {
|
|
19
20
|
captured_at: string;
|
|
21
|
+
/** Source agent. Absent on legacy snapshots written before multi-agent
|
|
22
|
+
* support — readers MUST treat undefined as "claude-code". */
|
|
23
|
+
agent?: AgentKind;
|
|
20
24
|
five_hour: UsageWindow;
|
|
21
25
|
seven_day: UsageWindow;
|
|
22
26
|
seven_day_opus: UsageWindow | null;
|
|
@@ -29,6 +33,7 @@ export type UsageSnapshot = {
|
|
|
29
33
|
used_credits: number | null;
|
|
30
34
|
utilization: number | null;
|
|
31
35
|
} | null;
|
|
36
|
+
plan_type?: string | null;
|
|
32
37
|
};
|
|
33
38
|
|
|
34
39
|
function usageLogPath(): string {
|
|
@@ -53,9 +58,27 @@ export const readUsageSnapshots = cache((): UsageSnapshot[] => {
|
|
|
53
58
|
|
|
54
59
|
export const latestUsageSnapshot = cache((): UsageSnapshot | null => {
|
|
55
60
|
const all = readUsageSnapshots();
|
|
56
|
-
|
|
61
|
+
// Sidebar's "current usage" widget is Claude-tier specific (extra_usage,
|
|
62
|
+
// per-model 7d windows). Filter to Claude only so a Codex snapshot doesn't
|
|
63
|
+
// accidentally end up as the latest source.
|
|
64
|
+
const claude = all.filter((s) => (s.agent ?? "claude-code") === "claude-code");
|
|
65
|
+
return claude.length > 0 ? claude[claude.length - 1]! : null;
|
|
57
66
|
});
|
|
58
67
|
|
|
68
|
+
export const latestUsageSnapshotByAgent = cache(
|
|
69
|
+
(agent: AgentKind): UsageSnapshot | null => {
|
|
70
|
+
const all = readUsageSnapshots();
|
|
71
|
+
const filtered = all.filter((s) => (s.agent ?? "claude-code") === agent);
|
|
72
|
+
return filtered.length > 0 ? filtered[filtered.length - 1]! : null;
|
|
73
|
+
},
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
export const readUsageSnapshotsByAgent = cache(
|
|
77
|
+
(agent: AgentKind): UsageSnapshot[] => {
|
|
78
|
+
return readUsageSnapshots().filter((s) => (s.agent ?? "claude-code") === agent);
|
|
79
|
+
},
|
|
80
|
+
);
|
|
81
|
+
|
|
59
82
|
// The CLI's profile cache mirrors what we report to a paired team-server.
|
|
60
83
|
// Reading it here keeps personal and team editions consistent — both show
|
|
61
84
|
// the same "what tier are we?" answer.
|
|
@@ -4,6 +4,7 @@ export const SettingsUpdateSchema = z.object({
|
|
|
4
4
|
ai_features: z.object({
|
|
5
5
|
enabled: z.boolean().optional(),
|
|
6
6
|
autoBackfillLastWeek: z.boolean().optional(),
|
|
7
|
+
autoBackfillYesterday: z.boolean().optional(),
|
|
7
8
|
}).refine((v) => Object.keys(v).length > 0, {
|
|
8
9
|
message: "ai_features must include at least one field",
|
|
9
10
|
}),
|