+
+ {label}
+ = 100 ? { color: 'var(--error)', fontWeight: 600 } : undefined}>
+ {used.toLocaleString()}{' / '}{unlimited ? '\u221e' : limit.toLocaleString()}
+
{!unlimited && (
-
diff --git a/ui/src/pages/tenant/TenantLicensePage.tsx b/ui/src/pages/tenant/TenantLicensePage.tsx
index 03dac71..92bb3de 100644
--- a/ui/src/pages/tenant/TenantLicensePage.tsx
+++ b/ui/src/pages/tenant/TenantLicensePage.tsx
@@ -1,14 +1,15 @@
import { useState } from 'react';
import {
- Alert,
Badge,
Button,
Card,
Spinner,
+ Alert,
useToast,
} from '@cameleer/design-system';
import { Copy, Eye, EyeOff } from 'lucide-react';
import { useTenantLicense } from '../../api/tenant-hooks';
+import { UsageIndicator } from '../../components/UsageIndicator';
import { tierColor } from '../../utils/tier';
import styles from '../../styles/platform.module.css';
@@ -28,6 +29,13 @@ const LIMIT_LABELS: Record
= {
max_jar_retention_count: 'JAR Retention Count',
};
+const METERED_LIMITS: Record = {
+ max_agents: 'agents',
+ max_environments: 'environments',
+ max_apps: 'apps',
+ max_users: 'users',
+};
+
function daysColor(days: number): 'success' | 'warning' | 'error' | 'auto' {
if (days <= 0) return 'error';
if (days <= 30) return 'warning';
@@ -57,6 +65,22 @@ export function TenantLicensePage() {
);
}
+ const usage = data.usage ?? {};
+ const meteredEntries = Object.entries(METERED_LIMITS)
+ .filter(([limitKey]) => data.limits[limitKey] != null)
+ .map(([limitKey, usageKey]) => ({
+ label: LIMIT_LABELS[limitKey] ?? limitKey,
+ used: usage[usageKey] ?? 0,
+ limit: data.limits[limitKey],
+ }));
+
+ const configEntries = Object.entries(data.limits)
+ .filter(([key]) => !(key in METERED_LIMITS))
+ .map(([key, value]) => ({
+ label: LIMIT_LABELS[key] ?? key,
+ value,
+ }));
+
async function handleCopy() {
try {
await navigator.clipboard.writeText(data!.token);
@@ -68,14 +92,12 @@ export function TenantLicensePage() {
return (
- {/* Header */}
License
- {/* Validity card */}
@@ -108,25 +130,37 @@ export function TenantLicensePage() {
- {/* Limits card */}
-
-
- {Object.entries(data.limits).map(([key, value]) => (
-
- {LIMIT_LABELS[key] ?? key}
-
- {value.toLocaleString()}
-
-
- ))}
- {Object.keys(data.limits).length === 0 && (
-
No limits configured.
- )}
-
-
+ {meteredEntries.length > 0 && (
+
+
+ {meteredEntries.map((entry) => (
+
+ ))}
+
+
+ )}
+
+ {configEntries.length > 0 && (
+
+
+ {configEntries.map((entry) => (
+
+ {entry.label}
+
+ {entry.value.toLocaleString()}
+
+
+ ))}
+
+
+ )}
- {/* License token card — full width */}
@@ -134,7 +168,7 @@ export function TenantLicensePage() {
- {showToken ? data.token : '••••••••••••••••••••••••••••••••'}
+ {showToken ? data.token : '\u2022'.repeat(32)}
diff --git a/ui/src/pages/vendor/TenantDetailPage.tsx b/ui/src/pages/vendor/TenantDetailPage.tsx
index 8e3bfa1..ca2057e 100644
--- a/ui/src/pages/vendor/TenantDetailPage.tsx
+++ b/ui/src/pages/vendor/TenantDetailPage.tsx
@@ -22,6 +22,7 @@ import {
} from '../../api/vendor-hooks';
import { errorMessage } from '../../api/client';
import { ServerStatusBadge } from '../../components/ServerStatusBadge';
+import { UsageIndicator } from '../../components/UsageIndicator';
import { tierColor } from '../../utils/tier';
import styles from '../../styles/platform.module.css';
import type { LicenseBundleResponse, MintLicenseRequest } from '../../types/api';
@@ -44,6 +45,13 @@ const LIMIT_LABELS: Record
= {
const LIMIT_KEYS = Object.keys(LIMIT_LABELS);
+const METERED_LIMITS: Record = {
+ max_agents: 'agents',
+ max_environments: 'environments',
+ max_apps: 'apps',
+ max_users: 'users',
+};
+
function licenseDaysRemaining(expiresAt: string | null | undefined): number {
if (!expiresAt) return 0;
return Math.ceil((new Date(expiresAt).getTime() - Date.now()) / 86_400_000);
@@ -340,6 +348,24 @@ export function TenantDetailPage() {
)}
+ {/* Resource Usage */}
+ {data.usage && Object.keys(data.usage).length > 0 && license && (
+
+
+ {Object.entries(METERED_LIMITS)
+ .filter(([limitKey]) => license.limits[limitKey] != null)
+ .map(([limitKey, usageKey]) => (
+
+ ))}
+
+
+ )}
+
{/* Tenant Info */}
diff --git a/ui/src/styles/platform.module.css b/ui/src/styles/platform.module.css
index 76f8746..52be069 100644
--- a/ui/src/styles/platform.module.css
+++ b/ui/src/styles/platform.module.css
@@ -61,3 +61,42 @@
word-break: break-all;
white-space: pre-wrap;
}
+
+/* Usage progress bar */
+.usageRow {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ padding: 6px 0;
+}
+
+.usageHeader {
+ display: flex;
+ justify-content: space-between;
+ align-items: baseline;
+}
+
+.usageLabel {
+ font-size: 0.8125rem;
+ font-weight: 500;
+ color: var(--text-primary);
+}
+
+.usageValue {
+ font-size: 0.75rem;
+ font-variant-numeric: tabular-nums;
+ color: var(--text-secondary);
+}
+
+.usageBarTrack {
+ height: 8px;
+ border-radius: 4px;
+ background: var(--bg-inset);
+ overflow: hidden;
+}
+
+.usageBarFill {
+ height: 100%;
+ border-radius: 4px;
+ transition: width 0.4s ease, background-color 0.3s ease;
+}
diff --git a/ui/src/types/api.ts b/ui/src/types/api.ts
index b375e09..5f7d8ef 100644
--- a/ui/src/types/api.ts
+++ b/ui/src/types/api.ts
@@ -89,6 +89,7 @@ export interface VendorTenantDetail {
serverHealthy: boolean;
serverStatus: string;
license: LicenseResponse | null;
+ usage: Record;
}
export interface CreateTenantRequest {
@@ -125,6 +126,7 @@ export interface TenantLicenseData {
expiresAt: string;
token: string;
daysRemaining: number;
+ usage: Record;
}
export interface TenantSettings {