@carlonicora/nextjs-jsonapi 2.4.0 → 3.0.0

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 (128) hide show
  1. package/dist/{BlockNoteEditor-7WGCH2KB.js → BlockNoteEditor-CDTU6DRN.js} +19 -19
  2. package/dist/{BlockNoteEditor-7WGCH2KB.js.map → BlockNoteEditor-CDTU6DRN.js.map} +1 -1
  3. package/dist/{BlockNoteEditor-GWAV5ESO.mjs → BlockNoteEditor-VVAY73V2.mjs} +4 -4
  4. package/dist/billing/index.d.mts +127 -46
  5. package/dist/billing/index.d.ts +127 -46
  6. package/dist/billing/index.js +1243 -1084
  7. package/dist/billing/index.js.map +1 -1
  8. package/dist/billing/index.mjs +983 -824
  9. package/dist/billing/index.mjs.map +1 -1
  10. package/dist/{chunk-V66AZWPG.js → chunk-53B6NPGA.js} +278 -8
  11. package/dist/chunk-53B6NPGA.js.map +1 -0
  12. package/dist/{chunk-M2EGUO2F.mjs → chunk-J54546YC.mjs} +1608 -1419
  13. package/dist/chunk-J54546YC.mjs.map +1 -0
  14. package/dist/{chunk-TOKHHZSP.js → chunk-JKGEJGSD.js} +7 -7
  15. package/dist/{chunk-TOKHHZSP.js.map → chunk-JKGEJGSD.js.map} +1 -1
  16. package/dist/{chunk-3LVSA7IM.mjs → chunk-PHEFWL6L.mjs} +2 -2
  17. package/dist/{chunk-LCCRUWWY.mjs → chunk-TWXKAY34.mjs} +273 -3
  18. package/dist/chunk-TWXKAY34.mjs.map +1 -0
  19. package/dist/{chunk-GYGMEDVS.js → chunk-UGNLKDI6.js} +1012 -823
  20. package/dist/chunk-UGNLKDI6.js.map +1 -0
  21. package/dist/client/index.js +4 -4
  22. package/dist/client/index.mjs +3 -3
  23. package/dist/components/index.d.mts +16 -1
  24. package/dist/components/index.d.ts +16 -1
  25. package/dist/components/index.js +4 -4
  26. package/dist/components/index.mjs +3 -3
  27. package/dist/{tokenusage-admin.module-5wJ_tZTj.d.ts → config-BIjrg5wd.d.ts} +39 -1
  28. package/dist/{tokenusage-admin.module-DQVT4O4j.d.mts → config-BRUjtCd1.d.mts} +39 -1
  29. package/dist/contexts/index.js +4 -4
  30. package/dist/contexts/index.mjs +3 -3
  31. package/dist/core/index.d.mts +22 -3
  32. package/dist/core/index.d.ts +22 -3
  33. package/dist/core/index.js +12 -2
  34. package/dist/core/index.js.map +1 -1
  35. package/dist/core/index.mjs +11 -1
  36. package/dist/features/help/index.js +37 -37
  37. package/dist/features/help/index.mjs +3 -3
  38. package/dist/features/tokenusage/index.d.mts +413 -14
  39. package/dist/features/tokenusage/index.d.ts +413 -14
  40. package/dist/features/tokenusage/index.js +481 -120
  41. package/dist/features/tokenusage/index.js.map +1 -1
  42. package/dist/features/tokenusage/index.mjs +431 -70
  43. package/dist/features/tokenusage/index.mjs.map +1 -1
  44. package/dist/index.d.mts +3 -3
  45. package/dist/index.d.ts +3 -3
  46. package/dist/index.js +13 -3
  47. package/dist/index.js.map +1 -1
  48. package/dist/index.mjs +12 -2
  49. package/dist/server/index.js +3 -3
  50. package/dist/server/index.mjs +1 -1
  51. package/package.json +1 -1
  52. package/src/client/index.ts +4 -0
  53. package/src/core/index.ts +10 -0
  54. package/src/core/registry/ModuleRegistry.ts +4 -0
  55. package/src/features/administration/components/AdminIndexContainer.tsx +1 -1
  56. package/src/features/administration/components/__tests__/AdminIndexContainer.spec.tsx +1 -1
  57. package/src/features/billing/contexts/PriceContext.tsx +202 -0
  58. package/src/features/billing/contexts/ProductContext.tsx +185 -0
  59. package/src/features/billing/contexts/__tests__/PriceContext.spec.tsx +89 -0
  60. package/src/features/billing/contexts/__tests__/ProductContext.spec.tsx +81 -0
  61. package/src/features/billing/contexts/index.ts +2 -1
  62. package/src/features/billing/i18n-keys.ts +88 -0
  63. package/src/features/billing/stripe-price/components/containers/PriceContainer.tsx +21 -0
  64. package/src/features/billing/stripe-price/components/containers/index.ts +1 -0
  65. package/src/features/billing/stripe-price/components/details/PriceDetails.tsx +131 -0
  66. package/src/features/billing/stripe-price/components/details/index.ts +1 -0
  67. package/src/features/billing/stripe-price/components/forms/PriceArchiver.tsx +55 -0
  68. package/src/features/billing/stripe-price/components/forms/PriceEditor.tsx +307 -374
  69. package/src/features/billing/stripe-price/components/forms/index.ts +3 -1
  70. package/src/features/billing/stripe-price/components/index.ts +2 -0
  71. package/src/features/billing/stripe-price/components/lists/PricesList.tsx +49 -267
  72. package/src/features/billing/stripe-price/data/index.ts +1 -0
  73. package/src/features/billing/stripe-price/data/stripe-price.fields.ts +8 -0
  74. package/src/features/billing/stripe-price/hooks/useStripePriceTableStructure.tsx +119 -0
  75. package/src/features/billing/stripe-price/stripe-price.module.ts +5 -1
  76. package/src/features/billing/stripe-product/components/containers/ProductContainer.tsx +45 -0
  77. package/src/features/billing/stripe-product/components/containers/ProductsListContainer.tsx +27 -0
  78. package/src/features/billing/stripe-product/components/containers/index.ts +2 -2
  79. package/src/features/billing/stripe-product/components/details/ProductDetails.tsx +45 -0
  80. package/src/features/billing/stripe-product/components/details/index.ts +1 -0
  81. package/src/features/billing/stripe-product/components/forms/ProductArchiver.tsx +55 -0
  82. package/src/features/billing/stripe-product/components/forms/ProductEditor.tsx +92 -81
  83. package/src/features/billing/stripe-product/components/forms/index.ts +3 -1
  84. package/src/features/billing/stripe-product/components/index.ts +1 -0
  85. package/src/features/billing/stripe-product/components/lists/ProductsList.tsx +45 -187
  86. package/src/features/billing/stripe-product/data/index.ts +1 -0
  87. package/src/features/billing/stripe-product/data/stripe-product.fields.ts +6 -0
  88. package/src/features/billing/stripe-product/hooks/useStripeProductTableStructure.tsx +80 -0
  89. package/src/features/billing/stripe-product/stripe-product.module.ts +5 -1
  90. package/src/features/tokenusage/components/TokenUsageAdminContainer.tsx +20 -4
  91. package/src/features/tokenusage/components/TokenUsageAdminTiles.tsx +31 -18
  92. package/src/features/tokenusage/components/TokenUsageBreakdownTable.tsx +13 -13
  93. package/src/features/tokenusage/components/TokenUsageRankedBar.tsx +24 -7
  94. package/src/features/tokenusage/components/TokenUsageReportContainer.tsx +102 -0
  95. package/src/features/tokenusage/components/TokenUsageReportFilterBar.tsx +34 -0
  96. package/src/features/tokenusage/components/TokenUsageReportTiles.tsx +161 -0
  97. package/src/features/tokenusage/components/TokenUsageTimelineChart.tsx +15 -13
  98. package/src/features/tokenusage/components/__tests__/TokenUsageAdminTiles.spec.tsx +4 -2
  99. package/src/features/tokenusage/components/__tests__/TokenUsageRankedBar.spec.tsx +3 -1
  100. package/src/features/tokenusage/contexts/TokenUsageAdminContext.tsx +6 -3
  101. package/src/features/tokenusage/contexts/TokenUsageReportContext.tsx +199 -0
  102. package/src/features/tokenusage/data/TokenUsageReportService.ts +65 -0
  103. package/src/features/tokenusage/data/index.ts +9 -0
  104. package/src/features/tokenusage/data/tokenusage-report-breakdown.interface.ts +12 -0
  105. package/src/features/tokenusage/data/tokenusage-report-breakdown.ts +94 -0
  106. package/src/features/tokenusage/data/tokenusage-report-summary.interface.ts +12 -0
  107. package/src/features/tokenusage/data/tokenusage-report-summary.ts +87 -0
  108. package/src/features/tokenusage/data/tokenusage-report-timeline.interface.ts +13 -0
  109. package/src/features/tokenusage/data/tokenusage-report-timeline.ts +94 -0
  110. package/src/features/tokenusage/data/tokenusage-report.types.ts +51 -0
  111. package/src/features/tokenusage/i18n-keys.ts +28 -0
  112. package/src/features/tokenusage/index.ts +18 -0
  113. package/src/features/tokenusage/lib/config.ts +22 -0
  114. package/src/features/tokenusage/lib/formatters.ts +100 -0
  115. package/src/features/tokenusage/lib/metrics.ts +8 -26
  116. package/src/features/tokenusage/lib/palette.ts +41 -14
  117. package/src/features/tokenusage/tokenusage-admin.module.ts +4 -0
  118. package/src/features/tokenusage/tokenusage.modules.ts +40 -0
  119. package/src/shadcnui/ui/chart.tsx +30 -3
  120. package/dist/chunk-GYGMEDVS.js.map +0 -1
  121. package/dist/chunk-LCCRUWWY.mjs.map +0 -1
  122. package/dist/chunk-M2EGUO2F.mjs.map +0 -1
  123. package/dist/chunk-V66AZWPG.js.map +0 -1
  124. package/src/features/billing/contexts/BillingContext.tsx +0 -95
  125. package/src/features/billing/stripe-product/components/containers/ProductsAdminContainer.tsx +0 -98
  126. package/src/features/billing/stripe-product/components/containers/ProductsAdminPageContainer.tsx +0 -27
  127. /package/dist/{BlockNoteEditor-GWAV5ESO.mjs.map → BlockNoteEditor-VVAY73V2.mjs.map} +0 -0
  128. /package/dist/{chunk-3LVSA7IM.mjs.map → chunk-PHEFWL6L.mjs.map} +0 -0
@@ -14,6 +14,7 @@ import {
14
14
  } from "../../../shadcnui";
15
15
  import { TokenUsageAdminTimelineInterface } from "../data/tokenusage-admin-timeline.interface";
16
16
  import { Metric, StackBy } from "../data/tokenusage-admin.types";
17
+ import { useUsageFormatters } from "../lib/formatters";
17
18
  import { operationLabel } from "../lib/operation-label";
18
19
  import { CATEGORICAL_CEILING, ChartMode, OTHER_COLOR, seriesColor } from "../lib/palette";
19
20
 
@@ -71,13 +72,6 @@ const inferGranularity = (buckets: string[]): "day" | "week" | "month" => {
71
72
  return "day";
72
73
  };
73
74
 
74
- const bucketFormatter = (granularity: "day" | "week" | "month"): Intl.DateTimeFormat =>
75
- granularity === "month"
76
- ? new Intl.DateTimeFormat("it-IT", { month: "short", year: "numeric", timeZone: "UTC" })
77
- : new Intl.DateTimeFormat("it-IT", { day: "numeric", month: "short", timeZone: "UTC" });
78
-
79
- const compactNumber = new Intl.NumberFormat("it-IT", { notation: "compact", maximumFractionDigits: 1 });
80
-
81
75
  /**
82
76
  * Usage over time as a stacked bar chart.
83
77
  *
@@ -93,6 +87,7 @@ const compactNumber = new Intl.NumberFormat("it-IT", { notation: "compact", maxi
93
87
  */
94
88
  export function TokenUsageTimelineChart({ rows, metric, stackBy, className }: TokenUsageTimelineChartProps) {
95
89
  const t = useTranslations();
90
+ const { compact, bucketDate, metricValue: formatValue } = useUsageFormatters();
96
91
  const { resolvedTheme } = useTheme();
97
92
  const mode: ChartMode = resolvedTheme === "dark" ? "dark" : "light";
98
93
 
@@ -134,7 +129,7 @@ export function TokenUsageTimelineChart({ rows, metric, stackBy, className }: To
134
129
  const seriesLabel = (series: string): string => {
135
130
  if (series === OTHER_SERIES) {
136
131
  const key = "token_usage.series.other";
137
- return t.has(key) ? t(key) : "Altro";
132
+ return t.has(key) ? t(key) : "Other";
138
133
  }
139
134
  // Stacking by company makes the series a company NAME, which is data, not
140
135
  // vocabulary — it is never looked up. Scope and type keys are vocabulary the
@@ -156,13 +151,12 @@ export function TokenUsageTimelineChart({ rows, metric, stackBy, className }: To
156
151
  const key = "token_usage.timeline.empty";
157
152
  return (
158
153
  <p className={className ? `text-muted-foreground text-sm ${className}` : "text-muted-foreground text-sm"}>
159
- {t.has(key) ? t(key) : "Nessun dato nel periodo selezionato"}
154
+ {t.has(key) ? t(key) : "No data in the selected period"}
160
155
  </p>
161
156
  );
162
157
  }
163
158
 
164
159
  const granularity = inferGranularity(chartData.map((entry) => entry.bucket));
165
- const formatBucket = bucketFormatter(granularity);
166
160
  const seriesColorFor = (series: string, index: number) =>
167
161
  series === OTHER_SERIES ? OTHER_COLOR : seriesColor(index, mode);
168
162
 
@@ -186,19 +180,27 @@ export function TokenUsageTimelineChart({ rows, metric, stackBy, className }: To
186
180
  axisLine={false}
187
181
  tickMargin={8}
188
182
  minTickGap={16}
189
- tickFormatter={(value: string) => formatBucket.format(new Date(`${value}T00:00:00.000Z`))}
183
+ tickFormatter={(value: string) => bucketDate(value, granularity)}
190
184
  />
191
185
  <YAxis
192
186
  tickLine={false}
193
187
  axisLine={false}
194
188
  tickMargin={8}
195
189
  width={48}
196
- tickFormatter={(value: number) => compactNumber.format(value)}
190
+ tickFormatter={(value: number) => compact(value)}
197
191
  />
198
192
  <ChartTooltip
199
193
  content={
200
194
  <ChartTooltipContent
201
- labelFormatter={(value) => formatBucket.format(new Date(`${String(value)}T00:00:00.000Z`))}
195
+ labelFormatter={(value) => bucketDate(String(value), granularity)}
196
+ // Without this the tooltip falls back to toLocaleString() and
197
+ // prints the raw stored precision (credits carry 4 decimals),
198
+ // which disagrees with every other number on the page.
199
+ valueFormatter={(value) => formatValue(Number(value), metric)}
200
+ // Every series appears in every bucket's payload, so a day with
201
+ // one active operation would otherwise list the whole vocabulary
202
+ // as zeros and bury the one number that matters.
203
+ hideZeroValues
202
204
  />
203
205
  }
204
206
  />
@@ -18,8 +18,10 @@ describe("TokenUsageAdminTiles", () => {
18
18
  it("shows both cost centres with their deltas against the previous window", () => {
19
19
  render(<TokenUsageAdminTiles summary={summary} metric="cost" singleCustomerMode={false} />);
20
20
 
21
- expect(screen.getByTestId("tile-customer")).toHaveTextContent("9,46");
22
- expect(screen.getByTestId("tile-platform")).toHaveTextContent("9,47");
21
+ // The suite mocks useLocale() as "en", and formatting now honours the
22
+ // locale instead of a hard-coded it-IT, so the separator is a point.
23
+ expect(screen.getByTestId("tile-customer")).toHaveTextContent("9.46");
24
+ expect(screen.getByTestId("tile-platform")).toHaveTextContent("9.47");
23
25
  // customer 9.46 vs 8.00 => +18%
24
26
  expect(screen.getByTestId("tile-customer-delta")).toHaveTextContent("18");
25
27
  // platform 9.47 vs 10.00 => -5%
@@ -15,7 +15,9 @@ describe("TokenUsageRankedBar", () => {
15
15
  />,
16
16
  );
17
17
 
18
- expect(screen.getByTestId("ranked-value-a")).toHaveTextContent("6,00");
18
+ // The suite mocks useLocale() as "en", and formatting now honours the
19
+ // locale instead of a hard-coded it-IT, so the separator is a point.
20
+ expect(screen.getByTestId("ranked-value-a")).toHaveTextContent("6.00");
19
21
  expect(screen.getByTestId("ranked-share-a")).toHaveTextContent("60");
20
22
  expect(screen.getByTestId("ranked-share-b")).toHaveTextContent("40");
21
23
  });
@@ -13,8 +13,8 @@ import type { Granularity, Metric, StackBy } from "../data/tokenusage-admin.type
13
13
  import { TokenUsageAdminService } from "../data/TokenUsageAdminService";
14
14
 
15
15
  /**
16
- * Route the breadcrumb links back to. The page itself lives in the consuming
17
- * app (a360ai mounts it at `/administration/token-usage`); the constant is here
16
+ * Default route the breadcrumb links back to. A host app that mounts the page
17
+ * elsewhere overrides it with the `pageUrl` prop — the constant stays here
18
18
  * because the breadcrumb is built by this provider, not by the app.
19
19
  */
20
20
  const TOKEN_USAGE_ADMIN_PAGE_URL = "/administration/token-usage";
@@ -72,6 +72,8 @@ type TokenUsageAdminProviderProps = {
72
72
  initialTo?: string;
73
73
  /** Rows per ranked panel. Defaults to 10. */
74
74
  topN?: number;
75
+ /** Route the breadcrumb links back to. */
76
+ pageUrl?: string;
75
77
  };
76
78
 
77
79
  /**
@@ -89,6 +91,7 @@ export const TokenUsageAdminProvider = ({
89
91
  initialFrom,
90
92
  initialTo,
91
93
  topN = DEFAULT_TOP_N,
94
+ pageUrl = TOKEN_USAGE_ADMIN_PAGE_URL,
92
95
  }: TokenUsageAdminProviderProps) => {
93
96
  const t = useTranslations();
94
97
  const generateUrl = usePageUrlGenerator();
@@ -185,7 +188,7 @@ export const TokenUsageAdminProvider = ({
185
188
  const breadcrumb = (): BreadcrumbItemData[] => [
186
189
  {
187
190
  name: t("token_usage.admin.title"),
188
- href: generateUrl({ page: TOKEN_USAGE_ADMIN_PAGE_URL }),
191
+ href: generateUrl({ page: pageUrl }),
189
192
  },
190
193
  ];
191
194
 
@@ -0,0 +1,199 @@
1
+ "use client";
2
+
3
+ import { useTranslations } from "next-intl";
4
+ import { createContext, ReactNode, useCallback, useContext, useEffect, useMemo, useState } from "react";
5
+ import { SharedProvider } from "../../../contexts";
6
+ import { usePageUrlGenerator } from "../../../hooks";
7
+ import { BreadcrumbItemData } from "../../../interfaces";
8
+ import { TokenUsageReportFilterBar } from "../components/TokenUsageReportFilterBar";
9
+ import type { TokenUsageReportBreakdownInterface } from "../data/tokenusage-report-breakdown.interface";
10
+ import type { TokenUsageReportSummaryInterface } from "../data/tokenusage-report-summary.interface";
11
+ import type { TokenUsageReportTimelineInterface } from "../data/tokenusage-report-timeline.interface";
12
+ import type { ReportMetric } from "../data/tokenusage-report.types";
13
+ import { TokenUsageReportService } from "../data/TokenUsageReportService";
14
+
15
+ /** Default page the breadcrumb links back to; overridable per host app. */
16
+ const TOKEN_USAGE_REPORT_PAGE_URL = "/tokenusage";
17
+
18
+ /** Rows kept per ranked panel before the backend folds the tail into "other". */
19
+ const DEFAULT_TOP_N = 10;
20
+
21
+ /**
22
+ * The unit this surface reports in, always. A game master is billed in credits,
23
+ * so credits are the only number that means anything to them: cost would leak
24
+ * platform margin and a raw token count is an implementation detail nobody is
25
+ * charged for. Pinned rather than exposed as a filter — there is no metric
26
+ * selector on the page.
27
+ */
28
+ const REPORT_METRIC: ReportMetric = "credits";
29
+
30
+ export type TokenUsageReportFilterState = {
31
+ /** ISO 8601 instant. */
32
+ from: string;
33
+ /** ISO 8601 instant. */
34
+ to: string;
35
+ };
36
+
37
+ export interface TokenUsageReportContextType {
38
+ summary: TokenUsageReportSummaryInterface[];
39
+ timeline: TokenUsageReportTimelineInterface[];
40
+ byOperation: TokenUsageReportBreakdownInterface[];
41
+ /** Empty unless the host app declared a targetLabel. */
42
+ byTarget: TokenUsageReportBreakdownInterface[];
43
+ /** i18n key for the target panel's title; undefined when the host set no targetLabel. */
44
+ targetPanelTitleKey?: string;
45
+ filters: TokenUsageReportFilterState;
46
+ /** Merges a partial patch into the current filters; every key is optional. */
47
+ setFilters: (next: Partial<TokenUsageReportFilterState>) => void;
48
+ isLoading: boolean;
49
+ error: string | null;
50
+ }
51
+
52
+ const TokenUsageReportContext = createContext<TokenUsageReportContextType | undefined>(undefined);
53
+
54
+ /** Current calendar month to now, which is the window the page opens on. */
55
+ function defaultRange(): { from: string; to: string } {
56
+ const now = new Date();
57
+ const start = new Date(now.getFullYear(), now.getMonth(), 1, 0, 0, 0, 0);
58
+ return { from: start.toISOString(), to: now.toISOString() };
59
+ }
60
+
61
+ type TokenUsageReportProviderProps = {
62
+ children: ReactNode;
63
+ /**
64
+ * The Neo4j label the "by target" panel groups by, e.g. "Campaign". The set of
65
+ * things usage can be attributed to is application-specific, so the package
66
+ * cannot pick one — omit it and the panel is skipped rather than rendered
67
+ * empty, and no request is issued.
68
+ */
69
+ targetLabel?: string;
70
+ /** i18n key for the target panel's title. Required when targetLabel is set. */
71
+ targetPanelTitleKey?: string;
72
+ /** ISO 8601 instant. Defaults to the start of the current month. */
73
+ initialFrom?: string;
74
+ /** ISO 8601 instant. Defaults to now. */
75
+ initialTo?: string;
76
+ /** Rows per ranked panel. Defaults to 10. */
77
+ topN?: number;
78
+ /** Route the breadcrumb links back to. */
79
+ pageUrl?: string;
80
+ };
81
+
82
+ /**
83
+ * Owns the date range of the self-service token-usage page, fetches the three
84
+ * panels behind it, and publishes the filter bar into the page title bar.
85
+ *
86
+ * The filter bar is rendered into `title.functions` here — NOT in the container —
87
+ * because `RoundPageContainer`'s title bar reads `title.functions` from
88
+ * `SharedContext`, and a descendant cannot inject nodes into an ancestor's
89
+ * provider value. That is why the filter state lives at this level.
90
+ */
91
+ export const TokenUsageReportProvider = ({
92
+ children,
93
+ targetLabel,
94
+ targetPanelTitleKey,
95
+ initialFrom,
96
+ initialTo,
97
+ topN = DEFAULT_TOP_N,
98
+ pageUrl = TOKEN_USAGE_REPORT_PAGE_URL,
99
+ }: TokenUsageReportProviderProps) => {
100
+ const t = useTranslations();
101
+ const generateUrl = usePageUrlGenerator();
102
+
103
+ const [filters, setFilterState] = useState<TokenUsageReportFilterState>(() => {
104
+ const range = defaultRange();
105
+ return { from: initialFrom ?? range.from, to: initialTo ?? range.to };
106
+ });
107
+
108
+ const [summary, setSummary] = useState<TokenUsageReportSummaryInterface[]>([]);
109
+ const [timeline, setTimeline] = useState<TokenUsageReportTimelineInterface[]>([]);
110
+ const [byOperation, setByOperation] = useState<TokenUsageReportBreakdownInterface[]>([]);
111
+ const [byTarget, setByTarget] = useState<TokenUsageReportBreakdownInterface[]>([]);
112
+ const [isLoading, setIsLoading] = useState(true);
113
+ const [error, setError] = useState<string | null>(null);
114
+
115
+ const { from, to } = filters;
116
+
117
+ useEffect(() => {
118
+ // `cancelled` is the out-of-order guard: a filter change fires a new request
119
+ // while the previous one is still in flight, and without this flag the slower
120
+ // (older) response would land last and overwrite the fresher state.
121
+ let cancelled = false;
122
+
123
+ setIsLoading(true);
124
+ setError(null);
125
+
126
+ const base = { from, to };
127
+
128
+ Promise.all([
129
+ TokenUsageReportService.getSummary(base),
130
+ // granularity is pinned to "day": week/month bucketing buys nothing on the
131
+ // ranges a single tenant browses.
132
+ TokenUsageReportService.getTimeline({ ...base, granularity: "day" }),
133
+ TokenUsageReportService.getBreakdown({ ...base, dimension: "operation", metric: REPORT_METRIC, limit: topN }),
134
+ // No targetLabel means the host app never opted in, so the request is
135
+ // skipped entirely rather than issued and discarded.
136
+ targetLabel
137
+ ? TokenUsageReportService.getBreakdown({
138
+ ...base,
139
+ dimension: "target",
140
+ targetLabel,
141
+ metric: REPORT_METRIC,
142
+ limit: topN,
143
+ })
144
+ : Promise.resolve<TokenUsageReportBreakdownInterface[]>([]),
145
+ ])
146
+ .then(([nextSummary, nextTimeline, nextByOperation, nextByTarget]) => {
147
+ if (cancelled) return;
148
+ setSummary(nextSummary ?? []);
149
+ setTimeline(nextTimeline ?? []);
150
+ setByOperation(nextByOperation ?? []);
151
+ setByTarget(nextByTarget ?? []);
152
+ })
153
+ .catch((err) => {
154
+ if (cancelled) return;
155
+ console.error("Failed to load token usage:", err);
156
+ setError(err instanceof Error ? err.message : String(err));
157
+ })
158
+ .finally(() => {
159
+ if (cancelled) return;
160
+ setIsLoading(false);
161
+ });
162
+
163
+ return () => {
164
+ cancelled = true;
165
+ };
166
+ }, [from, to, targetLabel, topN]);
167
+
168
+ const setFilters = useCallback((next: Partial<TokenUsageReportFilterState>) => {
169
+ setFilterState((prev) => ({ ...prev, ...next }));
170
+ }, []);
171
+
172
+ const breadcrumb = (): BreadcrumbItemData[] => [
173
+ { name: t("token_usage.report.title"), href: generateUrl({ page: pageUrl }) },
174
+ ];
175
+
176
+ const title = () => ({
177
+ type: t("token_usage.report.title"),
178
+ functions: <TokenUsageReportFilterBar key="tokenUsageReportFilterBar" onChange={setFilters} />,
179
+ });
180
+
181
+ const contextValue = useMemo<TokenUsageReportContextType>(
182
+ () => ({ summary, timeline, byOperation, byTarget, targetPanelTitleKey, filters, setFilters, isLoading, error }),
183
+ [summary, timeline, byOperation, byTarget, targetPanelTitleKey, filters, setFilters, isLoading, error],
184
+ );
185
+
186
+ return (
187
+ <SharedProvider value={{ breadcrumbs: breadcrumb(), title: title() }}>
188
+ <TokenUsageReportContext.Provider value={contextValue}>{children}</TokenUsageReportContext.Provider>
189
+ </SharedProvider>
190
+ );
191
+ };
192
+
193
+ export const useTokenUsageReport = (): TokenUsageReportContextType => {
194
+ const ctx = useContext(TokenUsageReportContext);
195
+ if (!ctx) {
196
+ throw new Error("useTokenUsageReport() called outside <TokenUsageReportProvider>.");
197
+ }
198
+ return ctx;
199
+ };
@@ -0,0 +1,65 @@
1
+ import { AbstractService, EndpointCreator, HttpMethod, Modules } from "../../../core";
2
+ import { TokenUsageReportBreakdownInterface } from "./tokenusage-report-breakdown.interface";
3
+ import { TokenUsageReportSummaryInterface } from "./tokenusage-report-summary.interface";
4
+ import { TokenUsageReportTimelineInterface } from "./tokenusage-report-timeline.interface";
5
+ import { ReportDimension, ReportMetric, TokenUsageReportFilters } from "./tokenusage-report.types";
6
+
7
+ function withFilters(endpoint: EndpointCreator, filters: TokenUsageReportFilters): EndpointCreator {
8
+ endpoint.addAdditionalParam("from", filters.from);
9
+ endpoint.addAdditionalParam("to", filters.to);
10
+ return endpoint;
11
+ }
12
+
13
+ /**
14
+ * The self-service token-usage endpoints.
15
+ *
16
+ * No companyId parameter anywhere: the backend scopes every query to the
17
+ * caller's own company through the CLS preamble, so there is nothing for the
18
+ * client to name. No `metric: "cost"` either — the controller rejects it.
19
+ */
20
+ export class TokenUsageReportService extends AbstractService {
21
+ /** Two rows: window "current" and "previous". */
22
+ static async getSummary(filters: TokenUsageReportFilters): Promise<TokenUsageReportSummaryInterface[]> {
23
+ const endpoint = withFilters(new EndpointCreator({ endpoint: Modules.TokenUsageReportSummary }), filters);
24
+
25
+ return this.callApi<TokenUsageReportSummaryInterface[]>({
26
+ type: Modules.TokenUsageReportSummary,
27
+ method: HttpMethod.GET,
28
+ endpoint: endpoint.generate(),
29
+ });
30
+ }
31
+
32
+ static async getTimeline(
33
+ params: TokenUsageReportFilters & { granularity: "day" },
34
+ ): Promise<TokenUsageReportTimelineInterface[]> {
35
+ const endpoint = withFilters(new EndpointCreator({ endpoint: Modules.TokenUsageReportTimeline }), params);
36
+ endpoint.addAdditionalParam("granularity", params.granularity);
37
+
38
+ return this.callApi<TokenUsageReportTimelineInterface[]>({
39
+ type: Modules.TokenUsageReportTimeline,
40
+ method: HttpMethod.GET,
41
+ endpoint: endpoint.generate(),
42
+ });
43
+ }
44
+
45
+ static async getBreakdown(
46
+ params: TokenUsageReportFilters & {
47
+ dimension: ReportDimension;
48
+ targetLabel?: string;
49
+ metric: ReportMetric;
50
+ limit?: number;
51
+ },
52
+ ): Promise<TokenUsageReportBreakdownInterface[]> {
53
+ const endpoint = withFilters(new EndpointCreator({ endpoint: Modules.TokenUsageReportBreakdown }), params);
54
+ endpoint.addAdditionalParam("dimension", params.dimension);
55
+ if (params.targetLabel) endpoint.addAdditionalParam("targetLabel", params.targetLabel);
56
+ endpoint.addAdditionalParam("metric", params.metric);
57
+ if (params.limit !== undefined) endpoint.addAdditionalParam("limit", String(params.limit));
58
+
59
+ return this.callApi<TokenUsageReportBreakdownInterface[]>({
60
+ type: Modules.TokenUsageReportBreakdown,
61
+ method: HttpMethod.GET,
62
+ endpoint: endpoint.generate(),
63
+ });
64
+ }
65
+ }
@@ -6,3 +6,12 @@ export type { TokenUsageAdminTimelineInterface } from "./tokenusage-admin-timeli
6
6
  export { TokenUsageAdminBreakdown } from "./tokenusage-admin-breakdown";
7
7
  export type { TokenUsageAdminBreakdownInterface } from "./tokenusage-admin-breakdown.interface";
8
8
  export { TokenUsageAdminService } from "./TokenUsageAdminService";
9
+
10
+ export * from "./tokenusage-report.types";
11
+ export { TokenUsageReportSummary } from "./tokenusage-report-summary";
12
+ export type { TokenUsageReportSummaryInterface } from "./tokenusage-report-summary.interface";
13
+ export { TokenUsageReportTimeline } from "./tokenusage-report-timeline";
14
+ export type { TokenUsageReportTimelineInterface } from "./tokenusage-report-timeline.interface";
15
+ export { TokenUsageReportBreakdown } from "./tokenusage-report-breakdown";
16
+ export type { TokenUsageReportBreakdownInterface } from "./tokenusage-report-breakdown.interface";
17
+ export { TokenUsageReportService } from "./TokenUsageReportService";
@@ -0,0 +1,12 @@
1
+ import { ApiDataInterface } from "../../../core";
2
+
3
+ export interface TokenUsageReportBreakdownInterface extends ApiDataInterface {
4
+ get label(): string;
5
+ get sublabel(): string | undefined;
6
+ get cost(): number;
7
+ get credits(): number;
8
+ get tokensIn(): number;
9
+ get tokensOut(): number;
10
+ get cached(): number;
11
+ get calls(): number;
12
+ }
@@ -0,0 +1,94 @@
1
+ import { AbstractApiData, JsonApiHydratedDataInterface, Modules } from "../../../core";
2
+ import { TokenUsageReportBreakdownInterface } from "./tokenusage-report-breakdown.interface";
3
+ import { TokenUsageReportBreakdownInput } from "./tokenusage-report.types";
4
+
5
+ /**
6
+ * One ranked row of a self-service breakdown. The same shape serves both
7
+ * dimensions — operation and target — which is why the two "by-X" panels
8
+ * collapse into a single `breakdown?dimension=` call.
9
+ *
10
+ * Deliberately narrower than the administrative row: no `activeUsers`,
11
+ * `monthlyCredits` or `availableMonthlyCredits`, which are company-fleet
12
+ * figures a single company has no business seeing.
13
+ */
14
+ export class TokenUsageReportBreakdown extends AbstractApiData implements TokenUsageReportBreakdownInterface {
15
+ private _label: string = "";
16
+ private _sublabel?: string;
17
+ private _cost: number = 0;
18
+ private _credits: number = 0;
19
+ private _tokensIn: number = 0;
20
+ private _tokensOut: number = 0;
21
+ private _cached: number = 0;
22
+ private _calls: number = 0;
23
+
24
+ get label(): string {
25
+ return this._label;
26
+ }
27
+
28
+ get sublabel(): string | undefined {
29
+ return this._sublabel;
30
+ }
31
+
32
+ get cost(): number {
33
+ return this._cost;
34
+ }
35
+
36
+ get credits(): number {
37
+ return this._credits;
38
+ }
39
+
40
+ get tokensIn(): number {
41
+ return this._tokensIn;
42
+ }
43
+
44
+ get tokensOut(): number {
45
+ return this._tokensOut;
46
+ }
47
+
48
+ get cached(): number {
49
+ return this._cached;
50
+ }
51
+
52
+ get calls(): number {
53
+ return this._calls;
54
+ }
55
+
56
+ rehydrate(data: JsonApiHydratedDataInterface): this {
57
+ super.rehydrate(data);
58
+
59
+ const attrs = data.jsonApi.attributes;
60
+ this._label = attrs.label ?? "";
61
+ this._sublabel = attrs.sublabel ?? undefined;
62
+ this._cost = attrs.cost ?? 0;
63
+ this._credits = attrs.credits ?? 0;
64
+ this._tokensIn = attrs.tokensIn ?? 0;
65
+ this._tokensOut = attrs.tokensOut ?? 0;
66
+ this._cached = attrs.cached ?? 0;
67
+ this._calls = attrs.calls ?? 0;
68
+
69
+ return this;
70
+ }
71
+
72
+ createJsonApi(data: TokenUsageReportBreakdownInput) {
73
+ const response: any = {
74
+ data: {
75
+ type: Modules.TokenUsageReportBreakdown.name,
76
+ id: data.id,
77
+ attributes: {},
78
+ relationships: {},
79
+ },
80
+ included: [],
81
+ };
82
+
83
+ if (data.label !== undefined) response.data.attributes.label = data.label;
84
+ if (data.sublabel !== undefined) response.data.attributes.sublabel = data.sublabel;
85
+ if (data.cost !== undefined) response.data.attributes.cost = data.cost;
86
+ if (data.credits !== undefined) response.data.attributes.credits = data.credits;
87
+ if (data.tokensIn !== undefined) response.data.attributes.tokensIn = data.tokensIn;
88
+ if (data.tokensOut !== undefined) response.data.attributes.tokensOut = data.tokensOut;
89
+ if (data.cached !== undefined) response.data.attributes.cached = data.cached;
90
+ if (data.calls !== undefined) response.data.attributes.calls = data.calls;
91
+
92
+ return response;
93
+ }
94
+ }
@@ -0,0 +1,12 @@
1
+ import { ApiDataInterface } from "../../../core";
2
+
3
+ export interface TokenUsageReportSummaryInterface extends ApiDataInterface {
4
+ /** "current" or "previous" — the equal-length preceding span. */
5
+ get window(): string;
6
+ get cost(): number;
7
+ get credits(): number;
8
+ get tokensIn(): number;
9
+ get tokensOut(): number;
10
+ get cached(): number;
11
+ get calls(): number;
12
+ }
@@ -0,0 +1,87 @@
1
+ import { AbstractApiData, JsonApiHydratedDataInterface, Modules } from "../../../core";
2
+ import { TokenUsageReportSummaryInterface } from "./tokenusage-report-summary.interface";
3
+ import { TokenUsageReportSummaryInput } from "./tokenusage-report.types";
4
+
5
+ /**
6
+ * One row of the self-service summary: the caller's own company observed over
7
+ * one time window. Two rows are returned per request — "current" and the
8
+ * equal-length "previous" span — so the KPI tiles can render a value and its
9
+ * delta without a second call.
10
+ *
11
+ * There is no `scope` here: a company only ever sees itself, so the
12
+ * customer/platform split that the administrative summary carries is absent.
13
+ */
14
+ export class TokenUsageReportSummary extends AbstractApiData implements TokenUsageReportSummaryInterface {
15
+ private _window: string = "";
16
+ private _cost: number = 0;
17
+ private _credits: number = 0;
18
+ private _tokensIn: number = 0;
19
+ private _tokensOut: number = 0;
20
+ private _cached: number = 0;
21
+ private _calls: number = 0;
22
+
23
+ get window(): string {
24
+ return this._window;
25
+ }
26
+
27
+ get cost(): number {
28
+ return this._cost;
29
+ }
30
+
31
+ get credits(): number {
32
+ return this._credits;
33
+ }
34
+
35
+ get tokensIn(): number {
36
+ return this._tokensIn;
37
+ }
38
+
39
+ get tokensOut(): number {
40
+ return this._tokensOut;
41
+ }
42
+
43
+ get cached(): number {
44
+ return this._cached;
45
+ }
46
+
47
+ get calls(): number {
48
+ return this._calls;
49
+ }
50
+
51
+ rehydrate(data: JsonApiHydratedDataInterface): this {
52
+ super.rehydrate(data);
53
+
54
+ const attrs = data.jsonApi.attributes;
55
+ this._window = attrs.window ?? "";
56
+ this._cost = attrs.cost ?? 0;
57
+ this._credits = attrs.credits ?? 0;
58
+ this._tokensIn = attrs.tokensIn ?? 0;
59
+ this._tokensOut = attrs.tokensOut ?? 0;
60
+ this._cached = attrs.cached ?? 0;
61
+ this._calls = attrs.calls ?? 0;
62
+
63
+ return this;
64
+ }
65
+
66
+ createJsonApi(data: TokenUsageReportSummaryInput) {
67
+ const response: any = {
68
+ data: {
69
+ type: Modules.TokenUsageReportSummary.name,
70
+ id: data.id,
71
+ attributes: {},
72
+ relationships: {},
73
+ },
74
+ included: [],
75
+ };
76
+
77
+ if (data.window !== undefined) response.data.attributes.window = data.window;
78
+ if (data.cost !== undefined) response.data.attributes.cost = data.cost;
79
+ if (data.credits !== undefined) response.data.attributes.credits = data.credits;
80
+ if (data.tokensIn !== undefined) response.data.attributes.tokensIn = data.tokensIn;
81
+ if (data.tokensOut !== undefined) response.data.attributes.tokensOut = data.tokensOut;
82
+ if (data.cached !== undefined) response.data.attributes.cached = data.cached;
83
+ if (data.calls !== undefined) response.data.attributes.calls = data.calls;
84
+
85
+ return response;
86
+ }
87
+ }
@@ -0,0 +1,13 @@
1
+ import { ApiDataInterface } from "../../../core";
2
+
3
+ export interface TokenUsageReportTimelineInterface extends ApiDataInterface {
4
+ /** Backend declares this `type: "date"`, so it is a Date in memory. */
5
+ get bucket(): Date;
6
+ get series(): string;
7
+ get cost(): number;
8
+ get credits(): number;
9
+ get tokensIn(): number;
10
+ get tokensOut(): number;
11
+ get cached(): number;
12
+ get calls(): number;
13
+ }