2026-05-02 07:09:31 +00:00

798 lines
34 KiB
PHP

<?php
require_once __DIR__ . '/bootstrap.php';
function clm_local_datetime_value($value): string
{
if (empty($value)) {
return '';
}
$time = strtotime((string)$value);
return $time ? date('Y-m-d\TH:i', $time) : '';
}
$schemaError = '';
try {
clm_ensure_schema();
} catch (Throwable $e) {
$schemaError = $e->getMessage();
}
if (isset($_GET['logout'])) {
unset($_SESSION['clm_admin_auth']);
clm_redirect('manage.php');
}
$error = $schemaError;
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'login') {
if ($schemaError !== '') {
$error = $schemaError;
} elseif (hash_equals((string)clm_cfg('admin_password'), (string)($_POST['password'] ?? ''))) {
$_SESSION['clm_admin_auth'] = true;
clm_redirect('manage.php');
} else {
$error = 'Wrong admin password.';
}
}
$isAuthenticated = !empty($_SESSION['clm_admin_auth']);
if ($isAuthenticated && $schemaError === '' && $_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') !== 'login') {
try {
$action = (string)($_POST['action'] ?? '');
$tables = clm_tables();
$activationsTable = $tables['activations'];
switch ($action) {
case 'create_app':
$name = trim((string)($_POST['name'] ?? ''));
$slug = trim((string)($_POST['slug'] ?? ''));
if ($name === '' && $slug === '') {
throw new RuntimeException('App name is required.');
}
$app = clm_resolve_app($slug, $name, true);
if (!$app) {
throw new RuntimeException('App could not be created.');
}
clm_set_flash('success', 'App saved: ' . $app['name']);
break;
case 'save_app':
$app = clm_app_by_id((int)($_POST['id'] ?? 0));
if (!$app) {
throw new RuntimeException('App not found.');
}
$app = clm_update_app_record($app, [
'name' => $_POST['name'] ?? '',
'slug' => $_POST['slug'] ?? '',
'status' => $_POST['status'] ?? 'active',
]);
clm_set_flash('success', 'App updated: ' . $app['name']);
break;
case 'issue_license':
$license = clm_issue_license_record([
'app_slug' => $_POST['app_slug'] ?? '',
'app_name' => $_POST['app_name'] ?? '',
'prefix' => $_POST['prefix'] ?? 'FLAT',
'max_activations' => $_POST['max_activations'] ?? 1,
'max_counters' => $_POST['max_counters'] ?? 1,
'owner' => $_POST['owner'] ?? '',
'customer_name' => $_POST['customer_name'] ?? '',
'customer_email' => $_POST['customer_email'] ?? '',
'address' => $_POST['address'] ?? '',
'expires_at' => $_POST['expires_at'] ?? '',
'notes' => $_POST['notes'] ?? '',
]);
clm_set_flash('success', 'License issued: ' . $license['license_key']);
break;
case 'save_license':
$license = clm_license_with_app_by_id((int)($_POST['id'] ?? 0));
if (!$license) {
throw new RuntimeException('License not found.');
}
clm_update_license_record($license, [
'owner' => $_POST['owner'] ?? '',
'customer_name' => $_POST['customer_name'] ?? '',
'customer_email' => $_POST['customer_email'] ?? '',
'app_slug' => $_POST['app_slug'] ?? '',
'max_activations' => $_POST['max_activations'] ?? 1,
'max_counters' => $_POST['max_counters'] ?? 1,
'expires_at' => $_POST['expires_at'] ?? '',
'status' => $_POST['status'] ?? 'active',
'notes' => $_POST['notes'] ?? '',
]);
clm_set_flash('success', 'License updated.');
break;
case 'toggle_license':
$license = clm_license_with_app_by_id((int)($_POST['id'] ?? 0));
if (!$license) {
throw new RuntimeException('License not found.');
}
$nextStatus = ($license['status'] ?? 'active') === 'active' ? 'suspended' : 'active';
clm_update_license_record($license, ['status' => $nextStatus]);
clm_set_flash('success', 'License status changed to ' . $nextStatus . '.');
break;
case 'deactivate_activation':
$id = (int)($_POST['id'] ?? 0);
if ($id <= 0) {
throw new RuntimeException('Activation not found.');
}
$stmt = clm_db()->prepare("UPDATE `{$activationsTable}` SET status = 'deactivated', deactivated_at = NOW(), last_seen_at = NOW() WHERE id = ?");
$stmt->execute([$id]);
clm_set_flash('success', 'Activation deactivated.');
break;
}
} catch (Throwable $e) {
clm_set_flash('error', $e->getMessage());
}
clm_redirect('manage.php');
}
$flash = clm_get_flash();
if (!$isAuthenticated):
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Central License Manager Login</title>
<meta name="description" content="Admin login for the Central License Manager.">
<meta name="robots" content="noindex, nofollow">
<style>
:root {
--bg: #f6f8fb;
--card: rgba(255,255,255,0.94);
--text: #0f172a;
--muted: #64748b;
--line: #d9e2ec;
--accent: #0ea5e9;
--danger: #b91c1c;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
padding: 20px;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: radial-gradient(circle at top left, rgba(14,165,233,0.14), transparent 24%), var(--bg);
color: var(--text);
}
.card {
width: min(100%, 460px);
background: var(--card);
border: 1px solid var(--line);
border-radius: 28px;
padding: 28px;
box-shadow: 0 24px 54px rgba(15, 23, 42, 0.09);
}
.badge {
display: inline-flex;
padding: 8px 12px;
border-radius: 999px;
background: rgba(14,165,233,0.09);
color: #0284c7;
font-size: 12px;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.04em;
}
h1 { margin: 14px 0 10px; font-size: 2rem; }
p { color: var(--muted); line-height: 1.65; }
label { display: block; font-size: 0.9rem; font-weight: 700; margin: 14px 0 8px; }
input {
width: 100%;
border: 1px solid var(--line);
border-radius: 16px;
padding: 13px 14px;
font: inherit;
}
button, a.button {
width: 100%;
display: inline-flex;
align-items: center;
justify-content: center;
margin-top: 16px;
padding: 13px 16px;
border-radius: 16px;
border: none;
background: linear-gradient(135deg, #0ea5e9, #0284c7);
color: white;
font: inherit;
font-weight: 800;
text-decoration: none;
cursor: pointer;
}
.alt {
background: white;
color: var(--text);
border: 1px solid var(--line);
}
.error {
margin-top: 14px;
padding: 12px 14px;
border-radius: 16px;
background: rgba(185, 28, 28, 0.08);
color: var(--danger);
border: 1px solid rgba(185, 28, 28, 0.15);
}
code {
display: block;
margin-top: 12px;
padding: 12px 14px;
border-radius: 16px;
background: #0f172a;
color: #e2e8f0;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 13px;
overflow-x: auto;
}
</style>
</head>
<body>
<main class="card">
<span class="badge">Admin access</span>
<h1>Central License Manager</h1>
<p>Use the configured admin password to manage apps, licenses, and activations from one place.</p>
<code>Mode: <?= clm_html(clm_manager_mode_label()) ?></code>
<?php if ($error !== ''): ?>
<div class="error"><?= clm_html($error) ?></div>
<?php endif; ?>
<form method="post">
<input type="hidden" name="action" value="login">
<label for="password">Admin password</label>
<input id="password" type="password" name="password" required autofocus>
<button type="submit">Login</button>
</form>
<a class="button alt" href="index.php">Back to module home</a>
</main>
</body>
</html>
<?php
exit;
endif;
$tables = clm_tables();
$appsTable = $tables['apps'];
$licensesTable = $tables['licenses'];
$activationsTable = $tables['activations'];
$stats = [
'apps' => 0,
'licenses' => 0,
'activations' => 0,
];
$apps = [];
$licenses = [];
$activations = [];
try {
$pdo = clm_db();
$stats['apps'] = (int)$pdo->query("SELECT COUNT(*) FROM `{$appsTable}`")->fetchColumn();
$stats['licenses'] = (int)$pdo->query("SELECT COUNT(*) FROM `{$licensesTable}`")->fetchColumn();
$stats['activations'] = (int)$pdo->query("SELECT COUNT(*) FROM `{$activationsTable}` WHERE status = 'active'")->fetchColumn();
$apps = $pdo->query("SELECT a.*,
(SELECT COUNT(*) FROM `{$licensesTable}` l WHERE l.app_id = a.id) AS licenses_count,
(SELECT COUNT(*)
FROM `{$activationsTable}` act
INNER JOIN `{$licensesTable}` l2 ON l2.id = act.license_id
WHERE l2.app_id = a.id AND act.status = 'active') AS active_activations
FROM `{$appsTable}` a
ORDER BY a.name ASC")->fetchAll();
$licenses = $pdo->query("SELECT l.*, a.slug AS app_slug, a.name AS app_name,
(SELECT COUNT(*) FROM `{$activationsTable}` act WHERE act.license_id = l.id AND act.status = 'active') AS active_activations
FROM `{$licensesTable}` l
INNER JOIN `{$appsTable}` a ON a.id = l.app_id
ORDER BY l.created_at DESC
LIMIT 200")->fetchAll();
$activations = $pdo->query("SELECT act.*, l.license_key, a.slug AS app_slug, a.name AS app_name
FROM `{$activationsTable}` act
INNER JOIN `{$licensesTable}` l ON l.id = act.license_id
INNER JOIN `{$appsTable}` a ON a.id = l.app_id
ORDER BY COALESCE(act.last_seen_at, act.activated_at) DESC
LIMIT 200")->fetchAll();
} catch (Throwable $e) {
$flash = ['type' => 'error', 'message' => $e->getMessage()];
}
$baseUrl = clm_base_url();
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Central License Manager Dashboard</title>
<meta name="description" content="Manage apps, licenses, and activations from a single central dashboard.">
<meta name="robots" content="noindex, nofollow">
<style>
:root {
--bg: #f6f8fb;
--card: rgba(255,255,255,0.93);
--text: #0f172a;
--muted: #64748b;
--line: #d9e2ec;
--accent: #0ea5e9;
--accent-strong: #0284c7;
--success: #15803d;
--danger: #b91c1c;
--warning: #b45309;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background:
radial-gradient(circle at top left, rgba(14,165,233,0.14), transparent 24%),
radial-gradient(circle at top right, rgba(34,197,94,0.08), transparent 20%),
var(--bg);
color: var(--text);
}
.shell { max-width: 1320px; margin: 0 auto; padding: 28px 20px 48px; }
.card {
background: var(--card);
border: 1px solid rgba(217,226,236,0.95);
border-radius: 24px;
padding: 22px;
box-shadow: 0 22px 50px rgba(15, 23, 42, 0.07);
backdrop-filter: blur(10px);
}
.hero {
display: grid;
grid-template-columns: minmax(0, 1.3fr) minmax(320px, 0.8fr);
gap: 20px;
margin-bottom: 20px;
}
.badge {
display: inline-flex;
padding: 8px 12px;
border-radius: 999px;
background: rgba(14,165,233,0.09);
color: var(--accent-strong);
font-size: 12px;
font-weight: 800;
letter-spacing: 0.04em;
text-transform: uppercase;
}
h1, h2, h3 { margin: 0 0 12px; }
h1 { font-size: clamp(2rem, 3.4vw, 3.1rem); line-height: 1.02; }
p { margin: 0 0 12px; color: var(--muted); line-height: 1.65; }
.top-actions, .row-actions { display: flex; flex-wrap: wrap; gap: 10px; }
.button, button {
display: inline-flex;
align-items: center;
justify-content: center;
border: none;
border-radius: 16px;
padding: 12px 16px;
font: inherit;
font-weight: 800;
cursor: pointer;
text-decoration: none;
background: linear-gradient(135deg, #0ea5e9, #0284c7);
color: white;
}
.button.alt, button.alt {
background: white;
color: var(--text);
border: 1px solid var(--line);
}
.stats, .forms {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 16px;
margin-bottom: 20px;
}
.forms { grid-template-columns: 1fr 1.2fr 1fr; }
.stat strong { display: block; font-size: 1.8rem; margin-bottom: 6px; }
.code {
display: block;
margin-top: 12px;
padding: 14px 16px;
border-radius: 18px;
background: #0f172a;
color: #e2e8f0;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 13px;
overflow-x: auto;
}
.notice {
margin-bottom: 18px;
padding: 14px 16px;
border-radius: 18px;
border: 1px solid transparent;
}
.notice.success { background: rgba(21,128,61,0.09); color: var(--success); border-color: rgba(21,128,61,0.16); }
.notice.error { background: rgba(185,28,28,0.08); color: var(--danger); border-color: rgba(185,28,28,0.16); }
.field-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
label {
display: flex;
flex-direction: column;
gap: 8px;
font-size: 0.88rem;
font-weight: 700;
color: var(--text);
}
input, select, textarea {
width: 100%;
border: 1px solid var(--line);
border-radius: 14px;
padding: 12px 13px;
font: inherit;
background: white;
color: var(--text);
}
textarea { min-height: 108px; resize: vertical; }
.section { margin-top: 20px; }
.table-wrap { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; min-width: 760px; }
.table-input, .table-select {
min-width: 180px;
padding: 10px 12px;
}
.table-actions { white-space: nowrap; }
th, td {
padding: 13px 12px;
border-bottom: 1px solid var(--line);
text-align: left;
vertical-align: top;
font-size: 0.95rem;
}
th { color: var(--muted); font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.05em; }
.licenses {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
}
.license-card {
background: rgba(255,255,255,0.98);
border: 1px solid var(--line);
border-radius: 22px;
padding: 18px;
}
.license-head {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
gap: 12px;
margin-bottom: 14px;
}
.pill {
display: inline-flex;
align-items: center;
padding: 7px 11px;
border-radius: 999px;
font-size: 12px;
font-weight: 800;
}
.pill.active { background: rgba(21,128,61,0.1); color: var(--success); }
.pill.suspended { background: rgba(180,83,9,0.1); color: var(--warning); }
.pill.expired, .pill.deactivated { background: rgba(185,28,28,0.1); color: var(--danger); }
.meta { color: var(--muted); font-size: 0.9rem; }
.split { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 14px; }
.split > * { flex: 1 1 180px; }
.small-form { display: inline; }
.small-btn {
border: 1px solid var(--line);
background: white;
color: var(--text);
padding: 8px 12px;
border-radius: 12px;
font-weight: 700;
}
@media (max-width: 1120px) {
.hero, .forms, .stats, .licenses { grid-template-columns: 1fr; }
}
@media (max-width: 720px) {
.field-grid { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<main class="shell">
<section class="hero">
<article class="card">
<span class="badge">Multi-app dashboard</span>
<h1><?= clm_html((string)clm_cfg('manager_name')) ?></h1>
<p>Manage licenses and installations for multiple products from one isolated module. This dashboard is intentionally separate from the old in-app license manager so you can move it later.</p>
<div class="top-actions">
<a class="button" href="index.php">Module home</a>
<a class="button alt" href="install.php">Installer</a>
<a class="button alt" href="manage.php?logout=1">Logout</a>
</div>
</article>
<aside class="card">
<h2>Integration</h2>
<p><?= clm_html(clm_manager_mode_label()) ?></p>
<span class="code">LICENSE_API_URL=<?= clm_html($baseUrl) ?></span>
<p style="margin-top: 12px;">Change <code>CLM_API_SECRET</code> and <code>CLM_ADMIN_PASSWORD</code> before exposing this module publicly.</p>
</aside>
</section>
<?php if ($flash): ?>
<div class="notice <?= $flash['type'] === 'success' ? 'success' : 'error' ?>">
<?= clm_html($flash['message']) ?>
</div>
<?php endif; ?>
<section class="stats">
<article class="card stat">
<strong><?= (int)$stats['apps'] ?></strong>
<span>Apps</span>
</article>
<article class="card stat">
<strong><?= (int)$stats['licenses'] ?></strong>
<span>Licenses</span>
</article>
<article class="card stat">
<strong><?= (int)$stats['activations'] ?></strong>
<span>Active installs</span>
</article>
<article class="card stat">
<strong><?= clm_html(date('Y-m-d')) ?></strong>
<span>Manager date</span>
</article>
</section>
<section class="forms">
<article class="card">
<h2>Create app</h2>
<form method="post">
<input type="hidden" name="action" value="create_app">
<label>App name
<input type="text" name="name" placeholder="Bilingual Accounting">
</label>
<label>Slug
<input type="text" name="slug" placeholder="bilingual-accounting">
</label>
<p class="meta">You can rename the app or adjust its slug later in the Apps table.</p>
<button type="submit">Save app</button>
</form>
</article>
<article class="card">
<h2>Issue license</h2>
<form method="post">
<input type="hidden" name="action" value="issue_license">
<div class="field-grid">
<label>App
<select name="app_slug">
<?php foreach ($apps as $app): ?>
<option value="<?= clm_html($app['slug']) ?>"><?= clm_html($app['name']) ?> (<?= clm_html($app['slug']) ?>)</option>
<?php endforeach; ?>
</select>
</label>
<label>Key prefix
<input type="text" name="prefix" value="FLAT">
</label>
<label>Owner
<input type="text" name="owner" placeholder="Company owner">
</label>
<label>Customer name
<input type="text" name="customer_name" placeholder="Client name">
</label>
<label>Customer email
<input type="email" name="customer_email" placeholder="client@example.com">
</label>
<label>Max activations
<input type="number" name="max_activations" value="1" min="1">
</label>
<label>Max counters
<input type="number" name="max_counters" value="1" min="1">
</label>
<label>Expires at
<input type="datetime-local" name="expires_at">
</label>
</div>
<label>Notes
<textarea name="notes" placeholder="Optional internal note"></textarea>
</label>
<button type="submit">Issue license</button>
</form>
</article>
<article class="card">
<h2>What to point clients to</h2>
<p>Each app should call this module, not keep its own separate activation database.</p>
<span class="code"><?= clm_html($baseUrl) ?></span>
<p style="margin-top: 14px;">Current API stays backward compatible with older clients by returning both <code>max_activations</code> and <code>allowed_activations</code>.</p>
</article>
</section>
<section class="section card">
<h2>Apps</h2>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Name</th>
<th>Slug</th>
<th>Status</th>
<th>Licenses</th>
<th>Active installs</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<?php if (!$apps): ?>
<tr><td colspan="6">No apps yet.</td></tr>
<?php else: ?>
<?php foreach ($apps as $app): ?>
<?php $appFormId = 'app-form-' . (int)$app['id']; ?>
<tr>
<td>
<form id="<?= clm_html($appFormId) ?>" method="post">
<input type="hidden" name="action" value="save_app">
<input type="hidden" name="id" value="<?= (int)$app['id'] ?>">
</form>
<input form="<?= clm_html($appFormId) ?>" class="table-input" type="text" name="name" value="<?= clm_html($app['name']) ?>">
</td>
<td>
<input form="<?= clm_html($appFormId) ?>" class="table-input" type="text" name="slug" value="<?= clm_html($app['slug']) ?>">
<div class="meta">Update client settings too if you rename this slug.</div>
</td>
<td>
<select form="<?= clm_html($appFormId) ?>" class="table-select" name="status">
<?php foreach (['active', 'inactive'] as $status): ?>
<option value="<?= $status ?>" <?= $app['status'] === $status ? 'selected' : '' ?>><?= ucfirst($status) ?></option>
<?php endforeach; ?>
</select>
</td>
<td><?= (int)$app['licenses_count'] ?></td>
<td><?= (int)$app['active_activations'] ?></td>
<td class="table-actions">
<button form="<?= clm_html($appFormId) ?>" class="small-btn" type="submit">Save</button>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</section>
<section class="section">
<h2>Licenses</h2>
<div class="licenses">
<?php if (!$licenses): ?>
<article class="card"><p>No licenses yet.</p></article>
<?php else: ?>
<?php foreach ($licenses as $license): ?>
<article class="license-card">
<div class="license-head">
<div>
<strong><code><?= clm_html($license['license_key']) ?></code></strong>
<div class="meta"><?= clm_html($license['app_name']) ?> · <?= clm_html($license['app_slug']) ?></div>
</div>
<span class="pill <?= clm_html($license['status']) ?>"><?= clm_html($license['status']) ?></span>
</div>
<form method="post">
<input type="hidden" name="action" value="save_license">
<input type="hidden" name="id" value="<?= (int)$license['id'] ?>">
<div class="field-grid">
<label>Owner
<input type="text" name="owner" value="<?= clm_html($license['owner']) ?>">
</label>
<label>Customer name
<input type="text" name="customer_name" value="<?= clm_html($license['customer_name']) ?>">
</label>
<label>Customer email
<input type="email" name="customer_email" value="<?= clm_html($license['customer_email']) ?>">
</label>
<label>App
<select name="app_slug">
<?php foreach ($apps as $appOption): ?>
<option value="<?= clm_html($appOption['slug']) ?>" <?= $license['app_slug'] === $appOption['slug'] ? 'selected' : '' ?>><?= clm_html($appOption['name']) ?> (<?= clm_html($appOption['slug']) ?>)</option>
<?php endforeach; ?>
</select>
</label>
<label>Max activations
<input type="number" name="max_activations" min="1" value="<?= (int)$license['max_activations'] ?>">
</label>
<label>Max counters
<input type="number" name="max_counters" min="1" value="<?= (int)$license['max_counters'] ?>">
</label>
<label>Expires at
<input type="datetime-local" name="expires_at" value="<?= clm_html(clm_local_datetime_value($license['expires_at'])) ?>">
</label>
<label>Status
<select name="status">
<?php foreach (['active', 'suspended', 'expired'] as $status): ?>
<option value="<?= $status ?>" <?= $license['status'] === $status ? 'selected' : '' ?>><?= ucfirst($status) ?></option>
<?php endforeach; ?>
</select>
</label>
<label>Notes
<textarea name="notes"><?= clm_html($license['notes']) ?></textarea>
</label>
</div>
<div class="split">
<div class="meta">Created: <?= clm_html((string)$license['created_at']) ?></div>
<div class="meta">Active installs: <?= (int)$license['active_activations'] ?></div>
</div>
<div class="row-actions" style="margin-top: 14px;">
<button type="submit">Save license</button>
</div>
</form>
<form method="post" class="row-actions" style="margin-top: 10px;">
<input type="hidden" name="action" value="toggle_license">
<input type="hidden" name="id" value="<?= (int)$license['id'] ?>">
<button class="alt" type="submit"><?= $license['status'] === 'active' ? 'Suspend' : 'Activate' ?></button>
</form>
</article>
<?php endforeach; ?>
<?php endif; ?>
</div>
</section>
<section class="section card">
<h2>Recent activations</h2>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>License</th>
<th>App</th>
<th>Domain</th>
<th>Fingerprint</th>
<th>Product</th>
<th>Status</th>
<th>Activated</th>
<th>Last seen</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<?php if (!$activations): ?>
<tr><td colspan="9">No activations yet.</td></tr>
<?php else: ?>
<?php foreach ($activations as $activation): ?>
<tr>
<td><code><?= clm_html($activation['license_key']) ?></code></td>
<td><?= clm_html($activation['app_name']) ?></td>
<td><?= clm_html($activation['domain']) ?></td>
<td><code><?= clm_html(substr((string)$activation['fingerprint'], 0, 16)) ?>…</code></td>
<td><?= clm_html($activation['product']) ?></td>
<td><span class="pill <?= clm_html($activation['status']) ?>"><?= clm_html($activation['status']) ?></span></td>
<td><?= clm_html((string)$activation['activated_at']) ?></td>
<td><?= clm_html((string)($activation['last_seen_at'] ?: '—')) ?></td>
<td>
<?php if (($activation['status'] ?? 'active') === 'active'): ?>
<form method="post" class="small-form">
<input type="hidden" name="action" value="deactivate_activation">
<input type="hidden" name="id" value="<?= (int)$activation['id'] ?>">
<button class="small-btn" type="submit">Deactivate</button>
</form>
<?php else: ?>
<span class="meta">Inactive</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</section>
</main>
</body>
</html>