diff --git a/apps/api/src/modules/license/index.ts b/apps/api/src/modules/license/index.ts index b2d13c1..06a669a 100644 --- a/apps/api/src/modules/license/index.ts +++ b/apps/api/src/modules/license/index.ts @@ -19,7 +19,8 @@ export const licenseModule: KcModule = { requirePermission(req, 'license.read'); const ent = await getEntitlement(); const row = await one('SELECT instance_id, license_key_enc IS NOT NULL AS has_key, last_error, last_attempt_at FROM license_state WHERE id = 1'); - return { ...ent, instanceId: row?.instance_id ?? null, hasKey: !!row?.has_key, lastError: row?.last_error ?? null, lastAttemptAt: row?.last_attempt_at ?? null }; + const customers = await one("SELECT COUNT(*) AS n FROM organizations WHERE status IN ('active','suspended')"); + return { ...ent, instanceId: row?.instance_id ?? null, hasKey: !!row?.has_key, lastError: row?.last_error ?? null, lastAttemptAt: row?.last_attempt_at ?? null, customerCount: Number(customers?.n ?? 0) }; }); app.put('/admin/license/settings', async (req) => { diff --git a/apps/web/src/app/(app)/einstellungen/lizenz/page.tsx b/apps/web/src/app/(app)/einstellungen/lizenz/page.tsx index d6874c5..16e186d 100644 --- a/apps/web/src/app/(app)/einstellungen/lizenz/page.tsx +++ b/apps/web/src/app/(app)/einstellungen/lizenz/page.tsx @@ -8,6 +8,7 @@ interface Status { valid: boolean; plan: string | null; modules: string[]; customerLimit: number | null; userLimit: number | null; trial: boolean; expiresAt: string | null; configured: boolean; source: 'live' | 'offline-grace' | 'unchecked' | 'unconfigured'; checkedAt: string | null; message: string | null; instanceId: string | null; hasKey: boolean; lastError: string | null; lastAttemptAt: string | null; + customerCount: number; } const SOURCE_LABEL: Record = { live: 'aktuell geprüft', 'offline-grace': 'Offline-Gnadenzeit', unchecked: 'noch nicht geprüft', unconfigured: 'nicht eingerichtet' }; const MODULE_LABEL: Record = { billing: 'Rechnungen', domains: 'Domains', provisioning: 'Provisionierung', discord: 'Discord', automation: 'Automatisierung' }; @@ -40,13 +41,16 @@ export default function LizenzPage() { {!s.configured && Noch kein Lizenzschlüssel hinterlegt. Solange keiner hinterlegt ist, gilt keine Kunden-/Modulgrenze.} {s.configured && s.source === 'unchecked' && Lizenzschlüssel hinterlegt, erste Prüfung steht noch aus.} {s.configured && s.source !== 'unchecked' && !s.valid && Lizenz ungültig: {s.message ?? s.lastError ?? 'unbekannter Grund'}. Neue Kunden und kommerzielle Aktionen sind pausiert, bestehende Daten bleiben zugänglich.} + {s.valid && s.customerLimit !== null && s.customerCount >= s.customerLimit && Kundengrenze erreicht ({s.customerCount}/{s.customerLimit}). Neue Kunden können erst nach einem Upgrade angelegt werden.} + {s.valid && s.customerLimit !== null && s.customerCount < s.customerLimit && s.customerCount >= s.customerLimit * 0.9 && Kundengrenze bald erreicht ({s.customerCount}/{s.customerLimit}). Ein Upgrade lohnt sich bald.} + {s.valid && s.trial && s.expiresAt && new Date(s.expiresAt).getTime() - Date.now() < 7 * 86400000 && Testzeitraum endet bald ({fmt(s.expiresAt)}). Danach sind neue Kunden und kommerzielle Aktionen pausiert, bis ein Plan gewählt wird.}

Status

Plan
{s.plan ?? '–'}{s.trial ? ' (Test)' : ''}
Module
{s.modules.length ? s.modules.map((m) => MODULE_LABEL[m] ?? m).join(', ') : '–'}
-
Kundengrenze
{s.customerLimit === null ? 'unbegrenzt' : s.customerLimit}
+
Kunden
{s.customerCount}{s.customerLimit === null ? ' (unbegrenzt)' : ` / ${s.customerLimit}`}
Nutzer-/Slotlimit
{s.userLimit === null ? 'unbegrenzt' : s.userLimit}
Gültig bis
{s.expiresAt ? fmt(s.expiresAt) : 'unbegrenzt'}
Zuletzt geprüft
{s.checkedAt ? fmt(s.checkedAt) : '–'} ({SOURCE_LABEL[s.source]})