fleetlens 0.16.6 → 0.16.8

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 (118) hide show
  1. package/app/apps/web/.next/BUILD_ID +1 -1
  2. package/app/apps/web/.next/build-manifest.json +3 -3
  3. package/app/apps/web/.next/prerender-manifest.json +3 -3
  4. package/app/apps/web/.next/server/app/_global-error.html +1 -1
  5. package/app/apps/web/.next/server/app/_global-error.rsc +1 -1
  6. package/app/apps/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  7. package/app/apps/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  8. package/app/apps/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  9. package/app/apps/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  10. package/app/apps/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  11. package/app/apps/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  12. package/app/apps/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  13. package/app/apps/web/.next/server/app/agent/page.js.nft.json +1 -1
  14. package/app/apps/web/.next/server/app/agent/page_client-reference-manifest.js +1 -1
  15. package/app/apps/web/.next/server/app/api/agent/chats/[id]/messages/route.js.nft.json +1 -1
  16. package/app/apps/web/.next/server/app/api/agent/index/route.js.nft.json +1 -1
  17. package/app/apps/web/.next/server/app/api/agent/mcp/route.js.nft.json +1 -1
  18. package/app/apps/web/.next/server/app/api/agent/suggestions/route.js.nft.json +1 -1
  19. package/app/apps/web/.next/server/app/api/ask/route.js.nft.json +1 -1
  20. package/app/apps/web/.next/server/app/api/digest/day/[date]/route.js.nft.json +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.nft.json +1 -1
  24. package/app/apps/web/.next/server/app/api/digest/week-index/route.js.nft.json +1 -1
  25. package/app/apps/web/.next/server/app/api/pdf/[key]/route.js.nft.json +1 -1
  26. package/app/apps/web/.next/server/app/api/projects/open-folder/route.js.nft.json +1 -1
  27. package/app/apps/web/.next/server/app/api/runs/[runId]/route.js.nft.json +1 -1
  28. package/app/apps/web/.next/server/app/api/runs/[runId]/stream/route.js.nft.json +1 -1
  29. package/app/apps/web/.next/server/app/api/settings/credentials/route.js.nft.json +1 -1
  30. package/app/apps/web/.next/server/app/api/settings/route.js.nft.json +1 -1
  31. package/app/apps/web/.next/server/app/changelog/page.js.nft.json +1 -1
  32. package/app/apps/web/.next/server/app/changelog/page_client-reference-manifest.js +1 -1
  33. package/app/apps/web/.next/server/app/day/[date]/page.js.nft.json +1 -1
  34. package/app/apps/web/.next/server/app/day/[date]/page_client-reference-manifest.js +1 -1
  35. package/app/apps/web/.next/server/app/day/page.js.nft.json +1 -1
  36. package/app/apps/web/.next/server/app/day/page_client-reference-manifest.js +1 -1
  37. package/app/apps/web/.next/server/app/digest/[date]/page.js.nft.json +1 -1
  38. package/app/apps/web/.next/server/app/digest/[date]/page_client-reference-manifest.js +1 -1
  39. package/app/apps/web/.next/server/app/digest/page.js.nft.json +1 -1
  40. package/app/apps/web/.next/server/app/digest/page_client-reference-manifest.js +1 -1
  41. package/app/apps/web/.next/server/app/insights/[key]/page.js.nft.json +1 -1
  42. package/app/apps/web/.next/server/app/insights/[key]/page_client-reference-manifest.js +1 -1
  43. package/app/apps/web/.next/server/app/insights/page.js.nft.json +1 -1
  44. package/app/apps/web/.next/server/app/insights/page_client-reference-manifest.js +1 -1
  45. package/app/apps/web/.next/server/app/page.js.nft.json +1 -1
  46. package/app/apps/web/.next/server/app/page_client-reference-manifest.js +1 -1
  47. package/app/apps/web/.next/server/app/parallelism/page.js.nft.json +1 -1
  48. package/app/apps/web/.next/server/app/parallelism/page_client-reference-manifest.js +1 -1
  49. package/app/apps/web/.next/server/app/print/[key]/page.js.nft.json +1 -1
  50. package/app/apps/web/.next/server/app/print/[key]/page_client-reference-manifest.js +1 -1
  51. package/app/apps/web/.next/server/app/projects/[slug]/page.js.nft.json +1 -1
  52. package/app/apps/web/.next/server/app/projects/[slug]/page_client-reference-manifest.js +1 -1
  53. package/app/apps/web/.next/server/app/projects/page.js.nft.json +1 -1
  54. package/app/apps/web/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  55. package/app/apps/web/.next/server/app/runs/page.js.nft.json +1 -1
  56. package/app/apps/web/.next/server/app/runs/page_client-reference-manifest.js +1 -1
  57. package/app/apps/web/.next/server/app/sessions/[id]/page.js.nft.json +1 -1
  58. package/app/apps/web/.next/server/app/sessions/[id]/page_client-reference-manifest.js +1 -1
  59. package/app/apps/web/.next/server/app/sessions/page.js.nft.json +1 -1
  60. package/app/apps/web/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
  61. package/app/apps/web/.next/server/app/settings/page.js.nft.json +1 -1
  62. package/app/apps/web/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  63. package/app/apps/web/.next/server/app/team/onboarding/page.js.nft.json +1 -1
  64. package/app/apps/web/.next/server/app/team/onboarding/page_client-reference-manifest.js +1 -1
  65. package/app/apps/web/.next/server/app/team/page.js.nft.json +1 -1
  66. package/app/apps/web/.next/server/app/team/page_client-reference-manifest.js +1 -1
  67. package/app/apps/web/.next/server/app/usage/page.js.nft.json +1 -1
  68. package/app/apps/web/.next/server/app/usage/page_client-reference-manifest.js +1 -1
  69. package/app/apps/web/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_0sr4wr_.js +1 -1
  70. package/app/apps/web/.next/server/chunks/packages_parser_dist_06d9se1._.js +1 -1
  71. package/app/apps/web/.next/server/chunks/packages_parser_dist_10ihc3~._.js +1 -1
  72. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0-zxvif._.js +1 -1
  73. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__01wj6p0._.js +1 -1
  74. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0f3q8gt._.js +1 -1
  75. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0ygwqfa._.js +1 -1
  76. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0z0wa91._.js +1 -1
  77. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__12s3gv9._.js +1 -1
  78. package/app/apps/web/.next/server/chunks/ssr/_0k8yioi._.js +1 -1
  79. package/app/apps/web/.next/server/chunks/ssr/apps_web_0epqfet._.js +1 -1
  80. package/app/apps/web/.next/server/chunks/ssr/apps_web_0~aj0~u._.js +1 -1
  81. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_0x014hn._.js +1 -1
  82. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_usage-charts-dashboard_tsx_0bgr_uv._.js +1 -1
  83. package/app/apps/web/.next/server/chunks/ssr/apps_web_lib_changelog_ts_0n7_p1d._.js +1 -1
  84. package/app/apps/web/.next/server/chunks/ssr/apps_web_lib_changelog_ts_0nb5_-s._.js +1 -1
  85. package/app/apps/web/.next/server/chunks/ssr/packages_parser_dist_0mx_509._.js +1 -1
  86. package/app/apps/web/.next/server/middleware-build-manifest.js +3 -3
  87. package/app/apps/web/.next/server/middleware-manifest.json +5 -5
  88. package/app/apps/web/.next/server/pages/500.html +1 -1
  89. package/app/apps/web/.next/server/server-reference-manifest.js +1 -1
  90. package/app/apps/web/.next/server/server-reference-manifest.json +1 -1
  91. package/app/apps/web/.next/static/chunks/01ky0t_.38u4u.js +14 -0
  92. package/app/apps/web/.next/static/chunks/0d~6j~aj2vr1t.js +1 -0
  93. package/app/apps/web/.next/static/chunks/{0-z49mtd-zwgv.js → 0~opjw4gew2-w.js} +1 -1
  94. package/app/apps/web/app/settings/page.tsx +1 -1
  95. package/app/apps/web/app/usage/page.tsx +178 -13
  96. package/app/apps/web/components/menubar-install-banner.tsx +1 -1
  97. package/app/apps/web/components/menubar-install.tsx +1 -1
  98. package/app/apps/web/components/usage-chart-range.tsx +9 -3
  99. package/app/apps/web/components/usage-chart.tsx +9 -3
  100. package/app/apps/web/components/usage-charts-dashboard.tsx +10 -3
  101. package/app/apps/web/lib/changelog-data.generated.ts +1 -1
  102. package/app/apps/web/lib/format.test.ts +10 -0
  103. package/app/apps/web/lib/format.ts +6 -4
  104. package/app/apps/web/lib/usage-data.ts +11 -0
  105. package/app/apps/web/lib/usage-display.test.ts +79 -0
  106. package/app/apps/web/lib/usage-display.ts +59 -0
  107. package/app/apps/web/package.json +1 -1
  108. package/app/apps/web/tsconfig.tsbuildinfo +1 -1
  109. package/dist/daemon-worker.js +994 -328
  110. package/dist/index.js +1085 -413
  111. package/menubar/FleetlensMenubar.app/Contents/Info.plist +2 -2
  112. package/menubar/FleetlensMenubar.app/Contents/MacOS/FleetlensMenubar +0 -0
  113. package/package.json +1 -1
  114. package/app/apps/web/.next/static/chunks/0rrcj195v1nd..js +0 -1
  115. package/app/apps/web/.next/static/chunks/1350ox0.qybjo.js +0 -14
  116. /package/app/apps/web/.next/static/{8slBxbEObd72azKwDvtmy → yxbgwGc3dXx7HY0GSHJeG}/_buildManifest.js +0 -0
  117. /package/app/apps/web/.next/static/{8slBxbEObd72azKwDvtmy → yxbgwGc3dXx7HY0GSHJeG}/_clientMiddlewareManifest.js +0 -0
  118. /package/app/apps/web/.next/static/{8slBxbEObd72azKwDvtmy → yxbgwGc3dXx7HY0GSHJeG}/_ssgManifest.js +0 -0
@@ -27,10 +27,15 @@ import { UsageChartsDashboard } from "@/components/usage-charts-dashboard";
27
27
  import { PreviousCyclesTrend } from "@/components/previous-cycles-trend";
28
28
  import {
29
29
  type AgentKind,
30
- agentMetadata,
31
30
  getAgentMetadata,
32
31
  isAgentKind,
33
32
  } from "@claude-lens/parser";
33
+ import {
34
+ copilotQuotaPresentation,
35
+ copilotUnitLabel,
36
+ type CopilotMonthlyQuota,
37
+ visibleUsageAgents,
38
+ } from "@/lib/usage-display";
34
39
 
35
40
  export const dynamic = "force-dynamic";
36
41
 
@@ -49,17 +54,9 @@ export default async function UsagePage({
49
54
  }) {
50
55
  const params = await searchParams;
51
56
  const allSnapshots = readUsageSnapshots();
52
- const agentsWithData = new Set<AgentKind>();
53
- for (const s of allSnapshots) {
54
- const k = (s.agent ?? "claude-code") as AgentKind;
55
- agentsWithData.add(k);
56
- }
57
- // Always show Claude in the tab strip even if empty — it's the canonical
58
- // source. Codex appears only once it has at least one snapshot.
59
- agentsWithData.add("claude-code");
60
- // Always list Grok once registered — weekly pool snapshots appear after
61
- // the daemon's first poll; the tab still works empty.
62
- if (getAgentMetadata("grok")) agentsWithData.add("grok");
57
+ // Tabs represent usage sources found on this machine. Claude stays as the
58
+ // canonical empty state; every other provider needs a real saved sample.
59
+ const agentsWithData = new Set(visibleUsageAgents(allSnapshots));
63
60
 
64
61
  const requested: AgentKind | undefined = isAgentKind(params.agent) ? params.agent : undefined;
65
62
  const selected: AgentKind =
@@ -78,6 +75,7 @@ export default async function UsagePage({
78
75
  // longer history just narrows them rather than overflowing.
79
76
  const cycles7d = calibration ? previousCyclesTrend(calibration, "7d", 12) : [];
80
77
  const isGrok = selected === "grok";
78
+ const isCopilot = selected === "copilot";
81
79
  const codexWeeklyOnly =
82
80
  selected === "codex" && latest?.five_hour?.utilization == null;
83
81
 
@@ -157,6 +155,26 @@ export default async function UsagePage({
157
155
  </span>
158
156
  </span>
159
157
  )}
158
+ {isCopilot && latest?.plan_type && (
159
+ <span
160
+ style={{
161
+ marginLeft: "auto",
162
+ display: "inline-flex",
163
+ alignItems: "center",
164
+ gap: 8,
165
+ fontSize: 12,
166
+ padding: "4px 10px",
167
+ border: "1px solid var(--af-border-subtle)",
168
+ borderRadius: 999,
169
+ fontFamily: "var(--font-mono)",
170
+ }}
171
+ >
172
+ <span style={{ color: "var(--af-text-tertiary)" }}>allowance</span>
173
+ <span style={{ color: "var(--af-text)", fontWeight: 600 }}>
174
+ GitHub Copilot · {latest.plan_type}
175
+ </span>
176
+ </span>
177
+ )}
160
178
  {selected === "zai" && latest?.plan_type && (
161
179
  <span
162
180
  style={{
@@ -207,6 +225,12 @@ export default async function UsagePage({
207
225
  planType={latest.plan_type}
208
226
  />
209
227
  )}
228
+ {isCopilot && latest.monthly && (
229
+ <CopilotMonthlyCard
230
+ window={latest.monthly}
231
+ quota={latest.monthly_quota}
232
+ />
233
+ )}
210
234
  {cycles7d.length > 0 && (
211
235
  <PreviousCyclesTrend windowLabel="7d" cycles={cycles7d} />
212
236
  )}
@@ -224,6 +248,17 @@ export default async function UsagePage({
224
248
  colorVar: "var(--af-accent)",
225
249
  },
226
250
  ]
251
+ : isCopilot
252
+ ? [
253
+ {
254
+ key: "monthly",
255
+ label: "Monthly AI-credit utilization",
256
+ // Monthly charts derive their exact start from the
257
+ // reset date; this value is only a nominal fallback.
258
+ windowMs: 30 * 24 * 60 * 60 * 1000,
259
+ colorVar: agentAccent("copilot"),
260
+ },
261
+ ]
227
262
  : codexWeeklyOnly
228
263
  ? [
229
264
  {
@@ -235,7 +270,7 @@ export default async function UsagePage({
235
270
  ]
236
271
  : undefined
237
272
  }
238
- hideSonnet={isGrok || selected === "codex"}
273
+ hideSonnet={isGrok || isCopilot || selected === "codex"}
239
274
  />
240
275
  {selected === "zai" && latest?.web_search_quota && (
241
276
  <WebSearchQuotaCard quota={latest.web_search_quota} />
@@ -251,6 +286,7 @@ export default async function UsagePage({
251
286
  Last {agentLabel(selected)} poll: {new Date(latest.captured_at).toLocaleString()} ·{" "}
252
287
  {snapshots.length} snapshot{snapshots.length === 1 ? "" : "s"} on disk
253
288
  {isGrok ? " · weekly shared-pool % (no 5h window)" : ""}
289
+ {isCopilot ? " · monthly AI-credit allowance (no 5h/7d windows)" : ""}
254
290
  {codexWeeklyOnly ? " · weekly-only Codex limit" : ""}
255
291
  </div>
256
292
  </>
@@ -259,6 +295,99 @@ export default async function UsagePage({
259
295
  );
260
296
  }
261
297
 
298
+ function CopilotMonthlyCard({
299
+ window,
300
+ quota,
301
+ }: {
302
+ window: { utilization: number | null; resets_at: string | null };
303
+ quota?: CopilotMonthlyQuota | null;
304
+ }) {
305
+ const pct = window.utilization;
306
+ const unit = copilotUnitLabel(quota?.unit);
307
+ const presentation = copilotQuotaPresentation(pct, quota);
308
+ return (
309
+ <div className="af-card" style={{ padding: "16px 18px" }}>
310
+ <div
311
+ style={{
312
+ fontSize: 11,
313
+ color: "var(--af-text-tertiary)",
314
+ textTransform: "uppercase",
315
+ letterSpacing: "0.05em",
316
+ fontWeight: 600,
317
+ }}
318
+ >
319
+ Monthly {unit}
320
+ </div>
321
+ <div
322
+ style={{
323
+ fontSize: 28,
324
+ fontWeight: 700,
325
+ marginTop: 8,
326
+ letterSpacing: "-0.02em",
327
+ lineHeight: 1.1,
328
+ fontVariantNumeric: "tabular-nums",
329
+ }}
330
+ >
331
+ {presentation.headline}
332
+ </div>
333
+ {presentation.detail && (
334
+ <div style={{ marginTop: 6, fontSize: 12, color: "var(--af-text-secondary)" }}>
335
+ {presentation.detail}
336
+ </div>
337
+ )}
338
+ {pct !== null && (
339
+ <div
340
+ style={{
341
+ marginTop: 10,
342
+ height: 8,
343
+ borderRadius: 999,
344
+ background: "var(--af-border-subtle)",
345
+ overflow: "hidden",
346
+ }}
347
+ >
348
+ <div
349
+ style={{
350
+ width: `${Math.min(100, Math.max(0, pct))}%`,
351
+ height: "100%",
352
+ borderRadius: 999,
353
+ background: agentAccent("copilot"),
354
+ }}
355
+ />
356
+ </div>
357
+ )}
358
+ {presentation.limitNotReported ? (
359
+ <p style={{ fontSize: 12, color: "var(--af-text-tertiary)", marginTop: 10, marginBottom: 0, lineHeight: 1.5 }}>
360
+ Copilot did not expose a personal credit ceiling for this account. Organization-managed plans can
361
+ draw from a shared pool, so this does not mean individual use is unlimited.{" "}
362
+ <a
363
+ href="https://docs.github.com/en/copilot/concepts/billing/usage-based-billing-for-organizations-and-enterprises"
364
+ target="_blank"
365
+ rel="noreferrer"
366
+ style={{ color: "var(--af-accent)" }}
367
+ >
368
+ How GitHub pools AI credits ↗
369
+ </a>
370
+ {" · "}
371
+ <a
372
+ href="https://github.com/settings/billing"
373
+ target="_blank"
374
+ rel="noreferrer"
375
+ style={{ color: "var(--af-accent)" }}
376
+ >
377
+ Verify in GitHub billing ↗
378
+ </a>
379
+ {window.resets_at ? ` · Reported reset ${new Date(window.resets_at).toLocaleString()}.` : ""}
380
+ </p>
381
+ ) : (
382
+ <p style={{ fontSize: 12, color: "var(--af-text-tertiary)", marginTop: 10, marginBottom: 0 }}>
383
+ Copilot reports a monthly allowance; it does not expose Codex-style 5-hour or 7-day windows.
384
+ {window.resets_at ? ` Resets ${new Date(window.resets_at).toLocaleString()}.` : ""}
385
+ </p>
386
+ )}
387
+ </div>
388
+ );
389
+ }
390
+
262
391
  function WebSearchQuotaCard({
263
392
  quota,
264
393
  }: {
@@ -450,6 +579,42 @@ function GrokWeeklyCard({
450
579
  }
451
580
 
452
581
  function EmptyState({ agent }: { agent: AgentKind }) {
582
+ if (agent === "copilot") {
583
+ return (
584
+ <div className="af-card" style={{ padding: "48px 32px", textAlign: "center" }}>
585
+ <div style={{ fontSize: 15, fontWeight: 600, color: "var(--af-text)" }}>
586
+ No GitHub Copilot usage samples yet
587
+ </div>
588
+ <p
589
+ style={{
590
+ fontSize: 12,
591
+ color: "var(--af-text-tertiary)",
592
+ margin: "8px auto 0",
593
+ maxWidth: 520,
594
+ lineHeight: 1.5,
595
+ }}
596
+ >
597
+ Run <code style={{ fontSize: 11 }}>copilot login</code>, then start the Fleetlens daemon.
598
+ Fleetlens reads the monthly AI-credit quota through Copilot CLI&apos;s authenticated local SDK server.
599
+ </p>
600
+ <pre
601
+ style={{
602
+ display: "inline-block",
603
+ marginTop: 14,
604
+ background: "var(--background)",
605
+ border: "1px solid var(--af-border-subtle)",
606
+ padding: "8px 16px",
607
+ borderRadius: 6,
608
+ fontSize: 13,
609
+ fontFamily: "var(--font-mono)",
610
+ color: "var(--af-text)",
611
+ }}
612
+ >
613
+ fleetlens daemon start
614
+ </pre>
615
+ </div>
616
+ );
617
+ }
453
618
  if (agent === "grok") {
454
619
  return (
455
620
  <div
@@ -83,7 +83,7 @@ export function MenubarInstallBanner() {
83
83
  }}
84
84
  >
85
85
  <span style={{ flex: 1 }}>
86
- Get live Claude Code / Codex / Z.ai plan usage in your menu bar (Grok
86
+ Get live Claude Code / Codex / Copilot / Z.ai plan usage in your menu bar (Grok
87
87
  sessions live on the dashboard) —{" "}
88
88
  <strong>install the Fleetlens widget</strong>.
89
89
  </span>
@@ -57,7 +57,7 @@ export function MenubarInstall({ initial }: { initial: MenubarState }) {
57
57
  ? state.running
58
58
  ? "Installed and running — check your menu bar."
59
59
  : "Installed but not running."
60
- : "Adds a live gauge to your menu bar showing Claude Code / Codex plan utilization, updated as the daemon polls."}
60
+ : "Adds live Claude Code, Codex, Copilot, Z.ai, and Grok quota gauges to your menu bar, updated as the daemon polls."}
61
61
  </p>
62
62
 
63
63
  <div className="flex flex-wrap gap-2">
@@ -1,9 +1,15 @@
1
1
  "use client";
2
2
 
3
3
  import { useMemo, useState } from "react";
4
- import type { UsageSnapshot, UsageWindow } from "@/lib/usage-data";
4
+ import type { UsageSnapshot } from "@/lib/usage-data";
5
5
 
6
- type SeriesKey = "five_hour" | "seven_day" | "seven_day_sonnet";
6
+ type SeriesKey = "five_hour" | "seven_day" | "seven_day_sonnet" | "monthly";
7
+
8
+ function cycleStart(seriesKey: SeriesKey, resetsAt: number, windowMs: number): number {
9
+ if (seriesKey !== "monthly") return resetsAt - windowMs;
10
+ const reset = new Date(resetsAt);
11
+ return Date.UTC(reset.getUTCFullYear(), reset.getUTCMonth() - 1, 1);
12
+ }
7
13
 
8
14
  /**
9
15
  * Gap threshold: consecutive snapshots separated by more than this are
@@ -126,7 +132,7 @@ export function UsageChartRange({
126
132
 
127
133
  const cycles: CycleData[] = [];
128
134
  for (const [resetsAtMs, points] of byReset) {
129
- const cycleStartMs = resetsAtMs - windowMs;
135
+ const cycleStartMs = cycleStart(seriesKey, resetsAtMs, windowMs);
130
136
  // Sort points by time
131
137
  points.sort((a, b) => a.t - b.t);
132
138
  // Convert utilization → remaining budget
@@ -5,7 +5,13 @@ import { Maximize2 } from "lucide-react";
5
5
  import type { UsageSnapshot, UsageWindow } from "@/lib/usage-data";
6
6
  import { paceLabel, toneVar } from "@/lib/utilization-tone";
7
7
 
8
- type SeriesKey = "five_hour" | "seven_day" | "seven_day_sonnet";
8
+ type SeriesKey = "five_hour" | "seven_day" | "seven_day_sonnet" | "monthly";
9
+
10
+ function cycleStart(seriesKey: SeriesKey, resetsAt: number, windowMs: number): number {
11
+ if (seriesKey !== "monthly") return resetsAt - windowMs;
12
+ const reset = new Date(resetsAt);
13
+ return Date.UTC(reset.getUTCFullYear(), reset.getUTCMonth() - 1, 1);
14
+ }
9
15
 
10
16
  /**
11
17
  * Burndown-style chart for a single usage window.
@@ -52,7 +58,7 @@ export function UsageChart({
52
58
  .map((s) => ({ capturedAt: new Date(s.captured_at).getTime(), window: s[seriesKey] }))
53
59
  .filter(
54
60
  (x): x is { capturedAt: number; window: UsageWindow } =>
55
- x.window !== null && x.window.utilization !== null,
61
+ x.window != null && x.window.utilization !== null,
56
62
  );
57
63
 
58
64
  if (valid.length === 0) return null;
@@ -61,7 +67,7 @@ export function UsageChart({
61
67
  const resetsAt = latest.window.resets_at ? new Date(latest.window.resets_at).getTime() : null;
62
68
  if (!resetsAt) return null;
63
69
 
64
- const windowStart = resetsAt - windowMs;
70
+ const windowStart = cycleStart(seriesKey, resetsAt, windowMs);
65
71
  const now = Date.now();
66
72
  const currentRemaining = 100 - (latest.window.utilization ?? 0);
67
73
 
@@ -13,7 +13,7 @@ import {
13
13
  type DateRange,
14
14
  } from "@/components/date-range-picker";
15
15
 
16
- type SeriesKey = "five_hour" | "seven_day" | "seven_day_sonnet";
16
+ type SeriesKey = "five_hour" | "seven_day" | "seven_day_sonnet" | "monthly";
17
17
 
18
18
  const HOUR = 60 * 60 * 1000;
19
19
  const DAY = 24 * HOUR;
@@ -99,7 +99,7 @@ export function UsageChartsDashboard({
99
99
  seriesKey={w.key}
100
100
  windowMs={w.windowMs}
101
101
  colorVar={w.colorVar}
102
- predictedSeries={pred[w.key]}
102
+ predictedSeries={w.key === "monthly" ? undefined : pred[w.key]}
103
103
  onExpand={() => setExpanded(w)}
104
104
  />
105
105
  </section>
@@ -125,7 +125,7 @@ export function UsageChartsDashboard({
125
125
  <ExpandedModal
126
126
  config={expanded}
127
127
  snapshots={snapshots}
128
- predictedSeries={pred[expanded.key]}
128
+ predictedSeries={expanded.key === "monthly" ? undefined : pred[expanded.key]}
129
129
  onClose={() => setExpanded(null)}
130
130
  />
131
131
  )}
@@ -154,6 +154,13 @@ function ExpandedModal({
154
154
  const w = snapshots[i]![config.key];
155
155
  if (w && w.resets_at) {
156
156
  const endMs = new Date(w.resets_at).getTime();
157
+ if (config.key === "monthly") {
158
+ const end = new Date(endMs);
159
+ return {
160
+ startMs: Date.UTC(end.getUTCFullYear(), end.getUTCMonth() - 1, 1),
161
+ endMs,
162
+ };
163
+ }
157
164
  return { startMs: endMs - config.windowMs, endMs };
158
165
  }
159
166
  }