@hachej/boring-governance 0.1.83 → 0.1.85

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.
@@ -1,7 +1,8 @@
1
1
  import { LoadCompanyAdminStatus, RenderCompanyAdminContent, CompanyAdminLabels } from '@hachej/boring-core/front';
2
- import * as react from 'react';
3
- import { G as GovernanceUsageSummary } from '../usageContract-BNyGSg5T.js';
4
- export { a as GovernanceUsageEntry } from '../usageContract-BNyGSg5T.js';
2
+ import * as React from 'react';
3
+ import { ReactNode } from 'react';
4
+ import { G as GovernanceUsageSummary } from '../usageContract-DiJBjBko.js';
5
+ export { a as GovernanceCompanyContextAccess, b as GovernanceUsageEntry, c as GovernanceUsageRole } from '../usageContract-DiJBjBko.js';
5
6
 
6
7
  interface GovernancePolicyStatus {
7
8
  state: 'disabled' | 'active' | 'invalid';
@@ -43,7 +44,7 @@ interface GovernanceMeResponse {
43
44
  }
44
45
  declare function GovernanceAdminView({ status }: {
45
46
  status: GovernanceMeResponse;
46
- }): react.JSX.Element;
47
+ }): React.JSX.Element;
47
48
 
48
49
  interface GovernanceUsageMetersProps {
49
50
  /** Route to fetch the usage summary from. Defaults to the governance plugin route. */
@@ -64,7 +65,35 @@ interface GovernanceUsageMetersProps {
64
65
  * usage-summary route by default; renders nothing when governance is disabled so
65
66
  * a settings host can hide the section entirely.
66
67
  */
67
- declare function GovernanceUsageMeters({ endpoint, fetchImpl, fetchUsageSummary, title, description, className, }: GovernanceUsageMetersProps): react.JSX.Element | null;
68
+ declare function GovernanceUsageMeters({ endpoint, fetchImpl, fetchUsageSummary, title, description, className, }: GovernanceUsageMetersProps): React.JSX.Element | null;
69
+
70
+ interface GovernanceUsagePanelProps {
71
+ /**
72
+ * Label for the company-context box heading, its "(read/write)" line, and the
73
+ * "… context paths" heading. Tenants pass their own name (e.g. "Constellation
74
+ * context"). Defaults to "Company context".
75
+ */
76
+ contextLabel?: string;
77
+ /** Intro copy under the panel heading. Defaults to the credits-free budget line. */
78
+ description?: ReactNode;
79
+ /** Route to fetch the usage summary from. Defaults to the governance plugin route. */
80
+ endpoint?: string;
81
+ /** Override the fetch implementation (tests / non-browser hosts). */
82
+ fetchImpl?: typeof fetch;
83
+ /** Fully override data loading; takes precedence over `endpoint`/`fetchImpl`. */
84
+ fetchUsageSummary?: () => Promise<GovernanceUsageSummary>;
85
+ /** Section heading. */
86
+ title?: string;
87
+ className?: string;
88
+ }
89
+ /**
90
+ * Complete governed-usage settings panel: role, aggregate monthly cap,
91
+ * company-context access, the per-model usage meters (reusing
92
+ * `GovernanceUsageMeters`), and the company-context allow-rule chips. Self-fetches
93
+ * the summary once and hands the resolved data to the embedded meters so there is
94
+ * a single network round-trip. Renders nothing when governance is disabled.
95
+ */
96
+ declare function GovernanceUsagePanel({ contextLabel, description, endpoint, fetchImpl, fetchUsageSummary, title, className, }: GovernanceUsagePanelProps): React.JSX.Element | null;
68
97
 
69
98
  interface CreateGovernanceCompanyAdminOptions {
70
99
  fetchImpl?: typeof fetch;
@@ -81,4 +110,4 @@ type GovernanceFrontPlugin = ((api: unknown) => void) & {
81
110
  };
82
111
  declare const governanceFrontPlugin: GovernanceFrontPlugin;
83
112
 
84
- export { type CreateGovernanceCompanyAdminOptions, GovernanceAdminView, type GovernanceCompanyAdminProvider, type GovernanceMeResponse, type GovernancePolicyStatus, GovernanceUsageMeters, type GovernanceUsageMetersProps, GovernanceUsageSummary, createGovernanceCompanyAdmin, governanceFrontPlugin as default };
113
+ export { type CreateGovernanceCompanyAdminOptions, GovernanceAdminView, type GovernanceCompanyAdminProvider, type GovernanceMeResponse, type GovernancePolicyStatus, GovernanceUsageMeters, type GovernanceUsageMetersProps, GovernanceUsagePanel, type GovernanceUsagePanelProps, GovernanceUsageSummary, createGovernanceCompanyAdmin, governanceFrontPlugin as default };
@@ -203,8 +203,109 @@ async function fetchSummary(endpoint, fetchImpl, signal) {
203
203
  return await response.json();
204
204
  }
205
205
 
206
+ // src/front/GovernanceUsagePanel.tsx
207
+ import * as React2 from "react";
208
+ import { Button as Button3, Chip, ErrorState as ErrorState2, LoadingState as LoadingState2 } from "@hachej/boring-ui-kit";
209
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
210
+ var DEFAULT_ENDPOINT2 = "/api/v1/governance/usage-summary";
211
+ var DEFAULT_CONTEXT_LABEL = "Company context";
212
+ var DEFAULT_DESCRIPTION = "This deployment uses admin-managed monthly model budgets, not prepaid credits.";
213
+ var MICROS_PER_EUR2 = 1e6;
214
+ function formatEurMicros(micros) {
215
+ if (micros === null) return "No aggregate cap";
216
+ return new Intl.NumberFormat(void 0, { style: "currency", currency: "EUR", maximumFractionDigits: 2 }).format(micros / MICROS_PER_EUR2);
217
+ }
218
+ function contextAccessLabel(summary, contextLabel) {
219
+ if (summary.companyContextRules.length === 0) return "Not enabled";
220
+ return summary.companyContextAccess === "readwrite" ? `${contextLabel} (read/write)` : "Read-only allowed paths";
221
+ }
222
+ function GovernanceUsagePanel({
223
+ contextLabel = DEFAULT_CONTEXT_LABEL,
224
+ description = DEFAULT_DESCRIPTION,
225
+ endpoint = DEFAULT_ENDPOINT2,
226
+ fetchImpl,
227
+ fetchUsageSummary,
228
+ title = "Usage limits",
229
+ className
230
+ }) {
231
+ const [state, setState] = React2.useState({ status: "loading" });
232
+ const load = React2.useCallback(async (signal) => {
233
+ setState({ status: "loading" });
234
+ try {
235
+ const summary2 = fetchUsageSummary ? await fetchUsageSummary() : await fetchSummary2(endpoint, fetchImpl, signal);
236
+ if (signal?.aborted) return;
237
+ setState({ status: "ready", summary: summary2 });
238
+ } catch (error) {
239
+ if (signal?.aborted) return;
240
+ setState({ status: "error", message: error instanceof Error ? error.message : "Failed to load usage" });
241
+ }
242
+ }, [endpoint, fetchImpl, fetchUsageSummary]);
243
+ React2.useEffect(() => {
244
+ const controller = new AbortController();
245
+ void load(controller.signal);
246
+ return () => controller.abort();
247
+ }, [load]);
248
+ if (state.status === "loading") {
249
+ return /* @__PURE__ */ jsx3(PanelShell, { title, description, className, children: /* @__PURE__ */ jsx3(LoadingState2, { label: "Loading governed model limits\u2026" }) });
250
+ }
251
+ if (state.status === "error") {
252
+ return /* @__PURE__ */ jsx3(PanelShell, { title, description, className, children: /* @__PURE__ */ jsx3(
253
+ ErrorState2,
254
+ {
255
+ title: "Couldn\u2019t load usage limits",
256
+ description: state.message,
257
+ actions: /* @__PURE__ */ jsx3(Button3, { variant: "outline", size: "sm", onClick: () => void load(), children: "Retry" })
258
+ }
259
+ ) });
260
+ }
261
+ const { summary } = state;
262
+ if (!summary.enabled) return null;
263
+ const metersSummary = summary;
264
+ return /* @__PURE__ */ jsxs3(PanelShell, { title, description, className, children: [
265
+ /* @__PURE__ */ jsxs3("div", { className: "mt-5 grid gap-3 sm:grid-cols-3", children: [
266
+ /* @__PURE__ */ jsx3(StatCard, { label: "Role", value: summary.role ?? "Not governed" }),
267
+ /* @__PURE__ */ jsx3(StatCard, { label: "Aggregate monthly cap", value: formatEurMicros(summary.aggregateCapMicros) }),
268
+ /* @__PURE__ */ jsx3(StatCard, { label: contextLabel, value: contextAccessLabel(summary, contextLabel) })
269
+ ] }),
270
+ /* @__PURE__ */ jsx3("div", { className: "mt-6", children: /* @__PURE__ */ jsx3(GovernanceUsageMeters, { fetchUsageSummary: async () => metersSummary }) }),
271
+ summary.companyContextRules.length > 0 ? /* @__PURE__ */ jsxs3("div", { className: "mt-6", children: [
272
+ /* @__PURE__ */ jsxs3("h3", { className: "text-sm font-medium text-foreground", children: [
273
+ contextLabel,
274
+ " paths"
275
+ ] }),
276
+ /* @__PURE__ */ jsx3("div", { className: "mt-3 flex flex-wrap gap-2", children: summary.companyContextRules.map((rule) => /* @__PURE__ */ jsx3(Chip, { className: "font-mono text-muted-foreground", children: rule }, rule)) })
277
+ ] }) : null
278
+ ] });
279
+ }
280
+ function PanelShell({
281
+ title,
282
+ description,
283
+ className,
284
+ children
285
+ }) {
286
+ return /* @__PURE__ */ jsxs3("section", { "data-slot": "governance-usage-panel", className, children: [
287
+ /* @__PURE__ */ jsxs3("header", { children: [
288
+ /* @__PURE__ */ jsx3("h2", { className: "text-lg font-semibold text-foreground", children: title }),
289
+ /* @__PURE__ */ jsx3("p", { className: "mt-1 text-sm text-muted-foreground", children: description })
290
+ ] }),
291
+ children
292
+ ] });
293
+ }
294
+ function StatCard({ label, value }) {
295
+ return /* @__PURE__ */ jsxs3("div", { className: "rounded-xl border border-border bg-card p-4", children: [
296
+ /* @__PURE__ */ jsx3("div", { className: "text-xs uppercase tracking-[0.18em] text-muted-foreground", children: label }),
297
+ /* @__PURE__ */ jsx3("div", { className: "mt-2 text-base font-medium text-foreground", children: value })
298
+ ] });
299
+ }
300
+ async function fetchSummary2(endpoint, fetchImpl, signal) {
301
+ const doFetch = fetchImpl ?? fetch;
302
+ const response = await doFetch(endpoint, { credentials: "include", signal });
303
+ if (!response.ok) throw new Error(`Usage summary failed: HTTP ${response.status}`);
304
+ return await response.json();
305
+ }
306
+
206
307
  // src/front/index.tsx
207
- import { jsx as jsx3 } from "react/jsx-runtime";
308
+ import { jsx as jsx4 } from "react/jsx-runtime";
208
309
  function createGovernanceCompanyAdmin({ fetchImpl = fetch } = {}) {
209
310
  const loadStatus = async () => {
210
311
  const response = await fetchImpl("/api/v1/governance/me", { credentials: "include" });
@@ -221,7 +322,7 @@ function createGovernanceCompanyAdmin({ fetchImpl = fetch } = {}) {
221
322
  details: body
222
323
  };
223
324
  };
224
- const renderContent = (status) => /* @__PURE__ */ jsx3(GovernanceAdminView, { status: status.details });
325
+ const renderContent = (status) => /* @__PURE__ */ jsx4(GovernanceAdminView, { status: status.details });
225
326
  const labels = {
226
327
  menuLabel: "Company Admin",
227
328
  pageTitle: "Company Admin",
@@ -236,6 +337,7 @@ var front_default = governanceFrontPlugin;
236
337
  export {
237
338
  GovernanceAdminView,
238
339
  GovernanceUsageMeters,
340
+ GovernanceUsagePanel,
239
341
  createGovernanceCompanyAdmin,
240
342
  front_default as default
241
343
  };
@@ -2,8 +2,8 @@ import * as _hachej_boring_agent_server from '@hachej/boring-agent/server';
2
2
  import { AgentMeteringSink, RegisterAgentRoutesOptions } from '@hachej/boring-agent/server';
3
3
  import { CoreConfig } from '@hachej/boring-core/shared';
4
4
  import { CoreWorkspaceAgentServerPlugin } from '@hachej/boring-core/app/server';
5
- import { G as GovernanceUsageSummary } from '../usageContract-BNyGSg5T.js';
6
- export { a as GovernanceUsageEntry } from '../usageContract-BNyGSg5T.js';
5
+ import { G as GovernanceUsageSummary } from '../usageContract-DiJBjBko.js';
6
+ export { b as GovernanceUsageEntry } from '../usageContract-DiJBjBko.js';
7
7
  import { PostgresBudgetReservationStore } from '@hachej/boring-core/server';
8
8
  import { FastifyRequest } from 'fastify';
9
9
 
@@ -153,6 +153,19 @@ declare class GovernanceService {
153
153
  * period boundary (never hardcoded).
154
154
  */
155
155
  getUsageSummary(user: GovernanceUserLike | null | undefined, reader: GovernanceUsageSpendReader): Promise<GovernanceUsageSummary>;
156
+ /**
157
+ * Non per-model summary fields (role, aggregate cap, company-context access +
158
+ * rules) shared by every getUsageSummary return path and by the plugin route's
159
+ * empty fallback. Reuses the existing policy accessors so the panel and the
160
+ * admission path can never disagree.
161
+ */
162
+ usageSummaryMeta(user: GovernanceUserLike | null | undefined): {
163
+ enabled: boolean;
164
+ role: TenantRole | null;
165
+ aggregateCapMicros: number | null;
166
+ companyContextAccess: CompanyContextAccess;
167
+ companyContextRules: string[];
168
+ };
156
169
  me(user: GovernanceUserLike | null | undefined): GovernanceMeResponse;
157
170
  }
158
171
 
@@ -387,10 +387,10 @@ var GovernanceService = class {
387
387
  * period boundary (never hardcoded).
388
388
  */
389
389
  async getUsageSummary(user, reader) {
390
- if (!this.loaded.enabled) return { enabled: false, currency: "EUR", models: [], aggregate: null };
390
+ if (!this.loaded.enabled) return { ...this.usageSummaryMeta(user), currency: "EUR", models: [], aggregate: null };
391
391
  const userPolicy = this.userPolicy(user);
392
392
  const userId = user?.id;
393
- if (!userPolicy || !userId) return { enabled: true, currency: "EUR", models: [], aggregate: null };
393
+ if (!userPolicy || !userId) return { ...this.usageSummaryMeta(user), currency: "EUR", models: [], aggregate: null };
394
394
  const models = [];
395
395
  for (const grant of userPolicy.models) {
396
396
  const snapshot = await reader.getSpendSnapshot({ scope: "model", userId, provider: grant.provider, model: grant.id });
@@ -414,7 +414,22 @@ var GovernanceService = class {
414
414
  snapshot
415
415
  });
416
416
  }
417
- return { enabled: true, currency: "EUR", models, aggregate };
417
+ return { ...this.usageSummaryMeta(user), currency: "EUR", models, aggregate };
418
+ }
419
+ /**
420
+ * Non per-model summary fields (role, aggregate cap, company-context access +
421
+ * rules) shared by every getUsageSummary return path and by the plugin route's
422
+ * empty fallback. Reuses the existing policy accessors so the panel and the
423
+ * admission path can never disagree.
424
+ */
425
+ usageSummaryMeta(user) {
426
+ return {
427
+ enabled: this.loaded.enabled,
428
+ role: this.roleForUser(user),
429
+ aggregateCapMicros: user ? this.userMonthlyBudgetMicros(user) : null,
430
+ companyContextAccess: this.companyContextAccessForUser(user),
431
+ companyContextRules: user ? this.companyContextRules(user) : []
432
+ };
418
433
  }
419
434
  me(user) {
420
435
  const role = this.roleForUser(user);
@@ -484,7 +499,7 @@ function governanceRoutes(service, options = {}) {
484
499
  const user = request.user ?? null;
485
500
  const reader = options.getUsageReader?.();
486
501
  if (!user || !reader) {
487
- return { enabled: service.isEnabled(), currency: "EUR", models: [], aggregate: null };
502
+ return { ...service.usageSummaryMeta(user), currency: "EUR", models: [], aggregate: null };
488
503
  }
489
504
  return service.getUsageSummary({ id: user.id, email: user.email, emailVerified: user.emailVerified }, reader);
490
505
  });
@@ -18,6 +18,10 @@ interface GovernanceUsageEntry {
18
18
  /** ISO timestamp of the budget-period reset boundary. */
19
19
  resetsAt: string;
20
20
  }
21
+ /** Caller's governance role, or null when the caller is not governed. */
22
+ type GovernanceUsageRole = 'admin' | 'user' | null;
23
+ /** Caller's access to the company-context workspace. */
24
+ type GovernanceCompanyContextAccess = 'none' | 'readonly' | 'readwrite';
21
25
  interface GovernanceUsageSummary {
22
26
  /** Whether governance is enabled for this host. */
23
27
  enabled: boolean;
@@ -27,6 +31,14 @@ interface GovernanceUsageSummary {
27
31
  models: GovernanceUsageEntry[];
28
32
  /** Aggregate ("All models") row when an aggregate cap is configured, else null. */
29
33
  aggregate: GovernanceUsageEntry | null;
34
+ /** Caller's governance role, or null when not governed. */
35
+ role: GovernanceUsageRole;
36
+ /** Aggregate monthly cap in micros, or null when no aggregate cap is configured. */
37
+ aggregateCapMicros: number | null;
38
+ /** Caller's access to the company-context workspace. */
39
+ companyContextAccess: GovernanceCompanyContextAccess;
40
+ /** Company-context allow-rule patterns granted to the caller. */
41
+ companyContextRules: string[];
30
42
  }
31
43
 
32
- export type { GovernanceUsageSummary as G, GovernanceUsageEntry as a };
44
+ export type { GovernanceUsageSummary as G, GovernanceCompanyContextAccess as a, GovernanceUsageEntry as b, GovernanceUsageRole as c };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hachej/boring-governance",
3
- "version": "0.1.83",
3
+ "version": "0.1.85",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "license": "MIT",
@@ -42,10 +42,10 @@
42
42
  "peerDependencies": {
43
43
  "react": "^18.0.0 || ^19.0.0",
44
44
  "react-dom": "^18.0.0 || ^19.0.0",
45
- "@hachej/boring-agent": "0.1.83",
46
- "@hachej/boring-bash": "0.1.83",
47
- "@hachej/boring-core": "0.1.83",
48
- "@hachej/boring-ui-kit": "0.1.83"
45
+ "@hachej/boring-agent": "0.1.85",
46
+ "@hachej/boring-bash": "0.1.85",
47
+ "@hachej/boring-core": "0.1.85",
48
+ "@hachej/boring-ui-kit": "0.1.85"
49
49
  },
50
50
  "dependencies": {
51
51
  "proper-lockfile": "^4.1.2",
@@ -67,10 +67,10 @@
67
67
  "tsx": "^4.20.6",
68
68
  "typescript": "~6.0.3",
69
69
  "vitest": "^4.1.9",
70
- "@hachej/boring-agent": "0.1.83",
71
- "@hachej/boring-bash": "0.1.83",
72
- "@hachej/boring-core": "0.1.83",
73
- "@hachej/boring-ui-kit": "0.1.83"
70
+ "@hachej/boring-agent": "0.1.85",
71
+ "@hachej/boring-bash": "0.1.85",
72
+ "@hachej/boring-ui-kit": "0.1.85",
73
+ "@hachej/boring-core": "0.1.85"
74
74
  },
75
75
  "scripts": {
76
76
  "build": "tsup",