feat(ui): add license minting form, verify tool, and update all pages
Some checks failed
CI / build (push) Successful in 2m42s
CI / docker (push) Failing after 51s

Vendor UI:
- TenantDetailPage: full minting form with tier presets, 13 configurable
  limits, expiry/grace period, label. Mint & Push or Mint & Copy actions.
  License bundle display with all three env vars for standalone deployment.
- LicenseVerifyPage: paste token to decode + validate signature, shows
  envelope details and state badge. Public key viewer with copy button.
- Layout: added "License Tools" nav item under Vendor section.
- vendor-hooks: useMintLicense, useLicensePresets, useVerifyLicense, usePublicKey

Tenant UI:
- TenantLicensePage: replaced features card with full 13-key limits display,
  added grace period and label fields
- TenantDashboardPage: fixed limit keys (agents→max_agents, environments→max_environments)

Common:
- Updated types (dropped features, added label/gracePeriodDays/bundle types)
- Updated tier colors for STARTER/TEAM/BUSINESS/ENTERPRISE
- Updated CreateTenantPage tier dropdown

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-26 17:41:40 +02:00
parent 4dea1c6764
commit ffb7ef0839
11 changed files with 552 additions and 63 deletions

View File

@@ -53,8 +53,8 @@ export function TenantDashboardPage() {
const serverDown = !data.serverHealthy;
const daysRemaining = data.licenseDaysRemaining ?? 0;
const agentLimit = data.limits?.['agents'] ?? -1;
const envLimit = data.limits?.['environments'] ?? -1;
const agentLimit = data.limits?.['max_agents'] ?? -1;
const envLimit = data.limits?.['max_environments'] ?? -1;
const agentUsed = data.agentCount ?? 0;
const envUsed = data.environmentCount ?? 0;

View File

@@ -9,10 +9,25 @@ import {
} 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';
const LIMIT_LABELS: Record<string, string> = {
max_environments: 'Environments',
max_apps: 'Apps',
max_agents: 'Agents',
max_users: 'Users',
max_outbound_connections: 'Outbound Connections',
max_alert_rules: 'Alert Rules',
max_total_cpu_millis: 'CPU (millicores)',
max_total_memory_mb: 'Memory (MB)',
max_total_replicas: 'Replicas',
max_execution_retention_days: 'Execution Retention (days)',
max_log_retention_days: 'Log Retention (days)',
max_metric_retention_days: 'Metric Retention (days)',
max_jar_retention_count: 'JAR Retention Count',
};
function daysColor(days: number): 'success' | 'warning' | 'error' | 'auto' {
if (days <= 0) return 'error';
if (days <= 30) return 'warning';
@@ -51,10 +66,6 @@ export function TenantLicensePage() {
}
}
const agentLimit = data.limits?.['agents'] ?? -1;
const envLimit = data.limits?.['environments'] ?? -1;
const retentionDays = data.limits?.['retentionDays'] ?? -1;
return (
<div style={{ padding: 24, display: 'flex', flexDirection: 'column', gap: 20 }}>
{/* Header */}
@@ -82,37 +93,34 @@ export function TenantLicensePage() {
color={daysColor(data.daysRemaining)}
/>
</div>
</div>
</Card>
{/* Features card */}
<Card title="Features">
<div className={styles.dividerList}>
{Object.entries(data.features).map(([key, enabled]) => (
<div key={key} className={styles.kvRow}>
<span className={styles.kvLabel}>{key}</span>
<Badge
label={enabled ? 'Enabled' : 'Disabled'}
color={enabled ? 'success' : 'auto'}
/>
{data.gracePeriodDays > 0 && (
<div className={styles.kvRow}>
<span className={styles.kvLabel}>Grace period</span>
<span className={styles.kvValue}>{data.gracePeriodDays}d</span>
</div>
)}
{data.label && (
<div className={styles.kvRow}>
<span className={styles.kvLabel}>Label</span>
<span className={styles.kvValue}>{data.label}</span>
</div>
))}
{Object.keys(data.features).length === 0 && (
<p className={styles.description}>No feature flags configured.</p>
)}
</div>
</Card>
{/* Limits & Usage card */}
<Card title="Limits & Usage">
{/* Limits card */}
<Card title="Limits">
<div className={styles.dividerList}>
<UsageIndicator used={0} limit={agentLimit} label="Agents" />
<UsageIndicator used={0} limit={envLimit} label="Environments" />
{retentionDays >= 0 && (
<div className={styles.kvRow}>
<span className={styles.kvLabel}>Data retention</span>
<span className={styles.kvValue}>{retentionDays}d</span>
{Object.entries(data.limits).map(([key, value]) => (
<div key={key} className={styles.kvRow}>
<span className={styles.kvLabel}>{LIMIT_LABELS[key] ?? key}</span>
<span className={styles.kvValue} style={{ fontVariantNumeric: 'tabular-nums' }}>
{value.toLocaleString()}
</span>
</div>
))}
{Object.keys(data.limits).length === 0 && (
<p className={styles.description}>No limits configured.</p>
)}
</div>
</Card>