<!DOCTYPE html>
<?php
/* Owner-only licence server console.
   Vars: $baseUrl, $assetsUrl, $user, $hasKey, $keyId, $publicB64, $keyPerms,
         $keyMade, $stamped, $stampMatches, $licenses, $events, $serverUrl,
         $mirrors, $requireHttps, $thisOrigin, $isPrimaryHost, $ownerName,
         $ownerPwSet, $ownerPwWhen, $ownerPwMode, $deriveGen */
if (!isset($baseUrl)) $baseUrl = '';
if (!isset($assetsUrl)) $assetsUrl = rtrim($baseUrl, '/') . '/assets';
$b = rtrim($baseUrl, '/');
$token = \PpPhp\Core\Security::CSRF()->Token();

/** A small circular “?” with a hover tooltip. Purely presentational. */
$hint = static fn (string $t): string =>
    '<span class="pls-hint" data-tip="' . htmlspecialchars($t, ENT_QUOTES) . '" tabindex="0">?</span>';

$totalSeats = 0; $usedSeats = 0; $activeCount = 0;
foreach ($licenses as $l) {
    $totalSeats += (int) $l['seat_limit'];
    $usedSeats  += (int) $l['seats_used'];
    if ($l['status'] === 'active') $activeCount++;
}

$mode = $ownerPwMode ?? 'unified';
$perInstance = $mode === 'per-instance';
$tokenOnly   = $mode === 'token';
$modeLabel = $perInstance ? 'Per-install passwords' : ($tokenOnly ? 'Access tokens only' : 'One shared password');
$estate        = isset($estate) && is_array($estate) ? $estate : [];
$estateSummary = isset($estateSummary) && is_array($estateSummary) ? $estateSummary
    : ['total' => 0, 'active' => 0, 'flagged' => 0, 'tampered' => 0, 'stale' => 0, 'domains' => 0];

$startTab = $hasKey ? 'licences' : 'servers';
// A flagged installation is the one thing that should not wait to be noticed —
// open straight onto it rather than making the owner go looking.
if ($hasKey && !empty($estateSummary['flagged'])) $startTab = 'installs';
?>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title><?= htmlspecialchars(\PpPhp\Core\Brand::name()) ?> · Licence server</title>
    <?php include constant('BASE_PATH') . '/core/views/favicon.php'; ?>
    <link href="<?= $assetsUrl ?>/bootstrap/css/bootstrap.min.css" rel="stylesheet">
    <link href="<?= $assetsUrl ?>/icons/bootstrap-icons.css" rel="stylesheet">
    <?php include constant('BASE_PATH') . '/core/views/theme-head.php'; ?>
    <link href="<?= $assetsUrl ?>/sweetalert2/sweetalert2.min.css" rel="stylesheet">
    <script src="<?= $assetsUrl ?>/sweetalert2/sweetalert2.min.js"></script>
    <link href="<?= $assetsUrl ?>/vendor/flatpickr/flatpickr.min.css" rel="stylesheet">
    <script src="<?= $assetsUrl ?>/vendor/flatpickr/flatpickr.min.js"></script>
    <style>
        /* Make flatpickr theme-aware so the date picker matches the console. */
        .flatpickr-calendar { background: var(--pp-surface, #fff); border: 1px solid var(--pp-border, #e6e9f0);
            box-shadow: 0 18px 44px rgba(2, 6, 23, .22); border-radius: 12px; }
        .flatpickr-months, .flatpickr-weekdays, .flatpickr-weekday { background: transparent; color: var(--pp-text-muted, #64748b); }
        .flatpickr-month, .flatpickr-current-month, .flatpickr-monthDropdown-months, .numInputWrapper { color: var(--pp-text, #172033); fill: var(--pp-text, #172033); }
        .flatpickr-day { color: var(--pp-text, #172033); border-radius: 8px; }
        .flatpickr-day:hover { background: var(--pp-surface-soft, #f1f5f9); border-color: transparent; }
        .flatpickr-day.selected, .flatpickr-day.selected:hover { background: var(--pp-accent, #4f46e5); border-color: var(--pp-accent, #4f46e5); color: #fff; }
        .flatpickr-day.today { border-color: var(--pp-accent, #4f46e5); }
        .flatpickr-day.flatpickr-disabled, .flatpickr-day.prevMonthDay, .flatpickr-day.nextMonthDay { color: var(--pp-text-muted, #94a3b8); opacity: .55; }
        .flatpickr-prev-month svg, .flatpickr-next-month svg { fill: var(--pp-text-muted, #64748b); }
    </style>
    <style>
        body { background: var(--pp-app-bg, #f5f6fa); color: var(--pp-text, #172033); padding-top: 56px; }
        .pls-topbar { background: var(--pp-surface, #fff); border-bottom: 1px solid var(--pp-border, #e6e9f0);
            position: sticky; top: 0; z-index: 1030; }
        .pls-wrap { max-width: 1280px; margin: 1.4rem auto 3rem; padding: 0 1.25rem; }
        .pls-card { background: var(--pp-surface, #fff); border: 1px solid var(--pp-border, #e6e9f0);
                    border-radius: 14px; padding: 1.25rem 1.35rem; margin-bottom: 1.1rem; }
        /* Keep every field label tight to its input and on a single line, so a
           row of inputs stays on one clean baseline (no "floating down"). */
        .pls-card .form-label.small { margin-bottom: .3rem; white-space: nowrap; line-height: 1.2; }
        /* The label-less action column (Issue button) aligns its control to the
           bottom, level with the inputs beside it. */
        .pls-card .row.g-2 .align-self-end { align-self: flex-end; }

        /* ── Card header with search + filter (scales as licences grow) ── */
        .pls-card-head { display: flex; align-items: center; justify-content: space-between; gap: 1rem;
            flex-wrap: wrap; margin-bottom: 1rem; }
        .pls-count { display: inline-flex; align-items: center; justify-content: center; min-width: 1.5rem; height: 1.5rem;
            padding: 0 .45rem; margin-left: .1rem; border-radius: 999px; font-size: .74rem; font-weight: 800;
            background: color-mix(in srgb, var(--pp-accent, #4f46e5) 14%, var(--pp-surface, #fff)); color: var(--pp-accent, #4f46e5); }
        .pls-lic-tools { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; }
        .pls-search { display: flex; align-items: center; gap: .45rem; background: var(--pp-surface-soft, #f8fafc);
            border: 1px solid var(--pp-border, #e2e8f0); border-radius: 10px; padding: .3rem .6rem; margin: 0; }
        .pls-search i { color: var(--pp-text-muted, #94a3b8); font-size: .9rem; }
        .pls-search input { border: 0; background: transparent; outline: none; font-size: .84rem; color: var(--pp-text); width: 210px; max-width: 44vw; }
        .pls-search:focus-within { border-color: var(--pp-accent, #4f46e5); box-shadow: 0 0 0 3px color-mix(in srgb, var(--pp-accent, #4f46e5) 14%, transparent); }
        .pls-filter { display: inline-flex; background: var(--pp-surface-soft, #f8fafc); border: 1px solid var(--pp-border, #e2e8f0);
            border-radius: 10px; padding: 2px; gap: 2px; }
        .pls-filter button { border: 0; background: transparent; border-radius: 8px; padding: .28rem .7rem; font-size: .78rem;
            font-weight: 700; color: var(--pp-text-muted, #64748b); cursor: pointer; transition: all .12s; }
        .pls-filter button:hover { color: var(--pp-text); }
        .pls-filter button.active { background: var(--pp-surface, #fff); color: var(--pp-accent, #4f46e5);
            box-shadow: 0 1px 3px rgba(2, 6, 23, .12); }

        /* ── Foldable card (collapsible section) ── */
        .pls-fold .pls-fold-head { width: 100%; display: flex; align-items: center; gap: .6rem; background: transparent;
            border: 0; padding: 0; cursor: pointer; color: var(--pp-text); text-align: left; }
        .pls-fold .pls-fold-head .h2 { font-size: 1rem; font-weight: 800; display: flex; align-items: center; gap: .5rem; margin: 0; }
        .pls-fold .pls-fold-head .h2 .bi { color: var(--pp-accent, #4f46e5); font-size: 1.05rem; }
        .pls-fold .pls-fold-meta { margin-left: auto; font-size: .78rem; color: var(--pp-text-muted, #94a3b8); font-weight: 600; }
        .pls-fold .pls-chev { color: var(--pp-text-muted, #94a3b8); transition: transform .18s; }
        .pls-fold.open .pls-chev { transform: rotate(180deg); }
        .pls-fold .pls-fold-body { display: grid; grid-template-rows: 0fr; transition: grid-template-rows .22s ease; }
        .pls-fold.open .pls-fold-body { grid-template-rows: 1fr; }
        .pls-fold .pls-fold-inner { overflow: hidden; min-height: 0; }
        .pls-fold.open .pls-fold-inner { padding-top: .9rem; }
        @media (prefers-reduced-motion: reduce) { .pls-fold .pls-fold-body { transition: none; } .pls-fold .pls-chev { transition: none; } }

        /* Plan capability chips in the licence detail. */
        .pls-plan-caps { display: flex; flex-wrap: wrap; gap: .35rem; margin: -.2rem 0 .3rem; }
        .pls-cap-chip { font-size: .72rem; font-weight: 700; padding: .12rem .55rem; border-radius: 999px;
            display: inline-flex; align-items: center; gap: .3rem;
            background: color-mix(in srgb, #10b981 14%, var(--pp-surface, #fff)); color: #047857; }

        /* Per-server display-name editor (primary + each backup). */
        .pls-srv-labels { display: flex; flex-direction: column; gap: .4rem; }
        .pls-srv-row { display: flex; align-items: center; gap: .6rem; }
        .pls-srv-row > code { flex: 1 1 46%; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
        .pls-srv-row > code .bi { color: var(--pp-accent, #4f46e5); }
        .pls-srv-row .srv-label { flex: 0 0 44%; }
        @media (max-width: 560px) { .pls-srv-row { flex-wrap: wrap; } .pls-srv-row .srv-label { flex: 1 1 100%; } }
        .pls-card > h2 { font-size: 1rem; font-weight: 800; margin: 0 0 .2rem; display: flex; align-items: center; gap: .5rem; }
        .pls-card > h2 .bi { color: var(--pp-accent, #4f46e5); font-size: 1.05rem; }
        .pls-sub { color: var(--pp-text-muted, #64748b); font-size: .86rem; margin: 0 0 1rem; max-width: 70ch; }

        /* ── Hero ─────────────────────────────────────────── */
        .pls-hero { background: linear-gradient(135deg,
                        color-mix(in srgb, var(--pp-accent, #4f46e5) 12%, var(--pp-surface, #fff)),
                        var(--pp-surface, #fff));
                    border: 1px solid var(--pp-border, #e6e9f0); border-radius: 16px;
                    padding: 1.3rem 1.45rem; margin-bottom: 1.1rem; }
        .pls-hero h1 { font-size: 1.28rem; font-weight: 800; margin: 0; display: flex; align-items: center; gap: .55rem; }
        .pls-hero p { color: var(--pp-text-muted, #64748b); font-size: .88rem; margin: .3rem 0 0; }
        .pls-chips { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: 1rem; }
        .pls-chip { display: inline-flex; align-items: center; gap: .4rem; font-size: .78rem; font-weight: 600;
                    padding: .32rem .7rem; border-radius: 999px; background: var(--pp-surface, #fff);
                    border: 1px solid var(--pp-border, #e6e9f0); }
        .pls-chip .bi { font-size: .9rem; }
        .pls-chip.ok   { color: #15803d; border-color: color-mix(in srgb, #16a34a 40%, transparent); }
        .pls-chip.warn { color: #b45309; border-color: color-mix(in srgb, #f59e0b 45%, transparent); }
        .pls-chip.bad  { color: #b91c1c; border-color: color-mix(in srgb, #dc2626 40%, transparent); }
        .pls-chip.mut  { color: var(--pp-text-muted, #64748b); }
        .pls-chip code { font-size: .74rem; }

        /* ── Sidebar layout + tabs ────────────────────────── */
        .pls-layout { display: flex; gap: 1.25rem; align-items: flex-start; }
        .pls-side { flex: 0 0 220px; position: sticky; top: 1.4rem; }
        .pls-nav-title { font-size: .72rem; font-weight: 700; text-transform: uppercase; letter-spacing: .06em;
                    color: var(--pp-muted, #94a3b8); padding: .1rem .7rem .4rem; }
        .pls-main { flex: 1 1 auto; min-width: 0; }
        .pls-tabs { flex-direction: column; gap: .25rem; border: 0; margin: 0; flex-wrap: nowrap; }
        .pls-tabs .nav-item { width: 100%; }
        .pls-tabs .nav-link { width: 100%; border: 1px solid transparent; border-radius: 11px; font-weight: 600;
                    font-size: .9rem; color: var(--pp-text, #475569); padding: .6rem .85rem;
                    display: flex; align-items: center; gap: .6rem; }
        .pls-tabs .nav-link i { font-size: 1.05rem; opacity: .85; width: 20px; text-align: center; }
        .pls-tabs .nav-link:hover { background: color-mix(in srgb, var(--pp-accent, #4f46e5) 7%, transparent);
                    color: var(--pp-accent, #4f46e5); }
        .pls-tabs .nav-link.active { background: var(--pp-surface, #fff); color: var(--pp-accent, #4f46e5);
                    border-color: var(--pp-border, #e6e9f0); box-shadow: 0 6px 18px color-mix(in srgb, var(--pp-accent,#4f46e5) 12%, transparent); }
        .pls-tabs .nav-link .badge { font-size: .66rem; margin-left: auto; }
        /* Anchor items (Plans / Push / Guide) navigate away — same look as the tab
           buttons, plus a small section divider so they read as a separate group. */
        .pls-tabs .nav-link[href] { text-decoration: none; }
        .pls-tabs .pls-nav-sep { list-style: none; display: flex; align-items: center; gap: .5rem;
            margin: .55rem .35rem .3rem; font-size: .66rem; font-weight: 800; letter-spacing: .06em;
            text-transform: uppercase; color: var(--pp-text-muted, #94a3b8); }
        .pls-tabs .pls-nav-sep::after { content: ''; flex: 1; height: 1px; background: var(--pp-border, #e6e9f0); }
        .pls-tabs .nav-link .pls-ext { font-size: .72rem; opacity: .5; margin-left: auto; }
        @media (max-width: 900px) {
            .pls-layout { flex-direction: column; gap: .9rem; }
            .pls-side { position: static; flex: none; width: 100%; }
            .pls-nav-title { display: none; }
            .pls-tabs { flex-direction: row; overflow-x: auto; flex-wrap: nowrap; padding-bottom: .3rem; -webkit-overflow-scrolling: touch; }
            .pls-tabs .nav-item { width: auto; flex: 0 0 auto; }
            .pls-tabs .nav-link { white-space: nowrap; }
            .pls-tabs .nav-link .badge { margin-left: .35rem; }
            /* In the horizontal row, drop the section divider and the trailing
               external-link glyph so the nav links sit flush with the tabs. */
            .pls-tabs .pls-nav-sep { display: none; }
            .pls-tabs .nav-link .pls-ext { display: none; }
        }

        /* ── Stats ────────────────────────────────────────── */
        .pls-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: .9rem; }
        .pls-stat { border: 1px solid var(--pp-border, #e6e9f0); border-radius: 12px; padding: .9rem 1rem;
                    display: flex; align-items: center; gap: .8rem; }
        .pls-stat .ic { width: 40px; height: 40px; border-radius: 10px; display: grid; place-items: center;
                    font-size: 1.2rem; flex: none;
                    background: color-mix(in srgb, var(--pp-accent, #4f46e5) 12%, transparent); color: var(--pp-accent, #4f46e5); }
        .pls-stat b { display: block; font-size: 1.4rem; font-weight: 800; line-height: 1.05; font-variant-numeric: tabular-nums; }
        .pls-stat span { font-size: .74rem; text-transform: uppercase; letter-spacing: .04em;
                    color: var(--pp-text-muted, #94a3b8); font-weight: 700; }

        /* ── Table ────────────────────────────────────────── */
        table.pls-t { width: 100%; font-size: .88rem; }
        table.pls-t th { font-size: .72rem; text-transform: uppercase; letter-spacing: .04em;
                    color: var(--pp-text-muted, #94a3b8); font-weight: 700;
                    border-bottom: 1px solid var(--pp-border, #e6e9f0); padding: .45rem .55rem; text-align: left; }
        table.pls-t td { padding: .6rem .55rem; border-bottom: 1px solid var(--pp-border, #f1f4f9); vertical-align: middle; }
        table.pls-t tbody tr:hover { background: color-mix(in srgb, var(--pp-accent, #4f46e5) 4%, transparent); }
        .pls-key { font: 600 .82rem ui-monospace, Menlo, monospace; }
        .pls-pill { font-size: .7rem; font-weight: 700; padding: .16rem .5rem; border-radius: 999px;
                    text-transform: uppercase; letter-spacing: .03em; }
        .pls-pill.active    { background: color-mix(in srgb, #16a34a 15%, transparent); color: #15803d; }
        .pls-pill.suspended { background: color-mix(in srgb, #f59e0b 18%, transparent); color: #b45309; }
        .pls-pill.revoked   { background: color-mix(in srgb, #dc2626 14%, transparent); color: #b91c1c; }
        .pls-pill.released  { background: color-mix(in srgb, #64748b 15%, transparent); color: #475569; }
        .pls-pill.risk      { background: color-mix(in srgb, #dc2626 14%, transparent); color: #b91c1c; }

        .pls-note { background: color-mix(in srgb, var(--pp-accent, #4f46e5) 6%, transparent);
                    border-radius: 10px; padding: .8rem 1rem; font-size: .85rem; }
        .pls-note.warn { background: color-mix(in srgb, #f59e0b 12%, transparent); }
        .pls-note.bad  { background: color-mix(in srgb, #dc2626 10%, transparent); }
        textarea.pls-code, input.pls-code { font: 500 .8rem/1.45 ui-monospace, Menlo, monospace; }
        .pls-ev { font-size: .82rem; padding: .4rem 0; border-bottom: 1px solid var(--pp-border, #f1f4f9);
                  display: flex; gap: .6rem; align-items: baseline; }
        .pls-ev time { color: var(--pp-text-muted, #94a3b8); font-size: .74rem; white-space: nowrap; }
        .pls-ev .k { font-weight: 700; font-size: .74rem; text-transform: uppercase; letter-spacing: .03em; min-width: 118px; }
        .pls-empty { text-align: center; padding: 2.2rem 1rem; color: var(--pp-text-muted, #94a3b8); font-size: .9rem; }
        .pls-empty .bi { font-size: 1.6rem; display: block; margin-bottom: .5rem; opacity: .5; }

        .pls-mode { display: flex; gap: .6rem; align-items: flex-start; cursor: pointer; height: 100%;
                    border: 1px solid var(--pp-border, #e6e9f0); border-radius: 11px; padding: .8rem .9rem;
                    transition: border-color .15s, background .15s; }
        .pls-mode:hover { border-color: var(--pp-accent, #4f46e5); }
        .pls-mode input { margin-top: .2rem; flex: none; }
        .pls-mode.on, .pls-mode:has(input:checked) { border-color: var(--pp-accent, #4f46e5);
                    background: color-mix(in srgb, var(--pp-accent, #4f46e5) 5%, transparent); }
        .pls-reveal { font: 600 1.1rem/1.3 ui-monospace, Menlo, monospace; letter-spacing: .5px;
                      user-select: all; padding: .6rem .8rem; border-radius: 8px;
                      background: color-mix(in srgb, var(--pp-accent, #4f46e5) 8%, transparent); }

        /* ── Help hint ────────────────────────────────────── */
        .pls-hint { display: inline-flex; align-items: center; justify-content: center; width: 15px; height: 15px;
                    border-radius: 50%; background: var(--pp-border, #e6e9f0); color: var(--pp-text-muted, #64748b);
                    font-size: .66rem; font-weight: 800; font-style: normal; cursor: help; position: relative;
                    vertical-align: middle; margin-left: .3rem; }
        .pls-hint:hover, .pls-hint:focus { background: var(--pp-accent, #4f46e5); color: #fff; outline: none; }
        .pls-hint::after { content: attr(data-tip); position: absolute; bottom: calc(100% + 9px); left: 50%;
                    transform: translateX(-50%); width: max-content; max-width: 250px; white-space: normal;
                    background: #0f172a; color: #e2e8f0; font-size: .74rem; font-weight: 500; line-height: 1.45;
                    text-align: left; padding: .55rem .7rem; border-radius: 8px; box-shadow: 0 8px 24px rgba(0,0,0,.28);
                    opacity: 0; visibility: hidden; transition: opacity .12s; z-index: 60; pointer-events: none; }
        .pls-hint::before { content: ''; position: absolute; bottom: calc(100% + 4px); left: 50%; transform: translateX(-50%);
                    border: 5px solid transparent; border-top-color: #0f172a; opacity: 0; visibility: hidden;
                    transition: opacity .12s; z-index: 60; }
        .pls-hint:hover::after, .pls-hint:hover::before,
        .pls-hint:focus::after, .pls-hint:focus::before { opacity: 1; visibility: visible; }

        /* ── Phone (native-app feel): stop the header + grids overflowing ── */
        @media (max-width: 767.98px) {
            html, body { overflow-x: hidden; }
            .pls-topbar { flex-wrap: wrap; row-gap: .45rem; padding: .5rem .75rem; }
            .pls-topbar .btn { font-size: .82rem; }
            .pls-topbar > .badge { order: 3; }
            /* Put the two nav buttons on their own full row, side by side. */
            .pls-topbar > a[href$="/licensing"],
            .pls-topbar > a[href$="/app-bundle"] { flex: 1 1 calc(50% - .3rem); justify-content: center; margin: 0; }
            .pls-topbar > a[href$="/licensing"] { order: 4; }
            .pls-topbar > a[href$="/app-bundle"] { order: 5; }
            .pls-topbar .ms-auto { order: 2; }
            .pls-wrap { padding-left: .85rem; padding-right: .85rem; overflow-x: hidden; }
            /* Keep the stacked content column from being pushed wider than the screen
               by any min-content descendant (tables, nowrap rows). */
            .pls-layout { flex-direction: column; }
            .pls-side { width: 100%; }
            .pls-main { width: 100% !important; min-width: 0 !important; max-width: 100% !important; }
            .pls-main .tab-content, .pls-main .tab-pane, .pls-card { min-width: 0; max-width: 100%; }
            .pls-card { overflow-x: hidden; }
            .pls-hero { padding: 1.1rem; }
            .pls-hero h1 { font-size: 1.15rem; }
            .pls-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .6rem; }
            .pls-stat { padding: .75rem; }
            .pls-stat b { font-size: 1.25rem; }
            .pls-card { padding: 1rem; }
            .pls-card-head { flex-wrap: wrap; }
            /* iOS: 16px inputs prevent focus-zoom. */
            .pls-wrap input:not([type=checkbox]):not([type=radio]),
            .pls-wrap select, .pls-wrap textarea { font-size: 16px; }
        }
    </style>
</head>
<body>
<?php
    $navBrandLabel = 'Licence server';
    $navBrandIcon  = 'key-fill';
    $navBack = ['url' => $b . '/projects', 'label' => 'Projects', 'icon' => 'arrow-left'];
    $navExtra = '<a href="' . htmlspecialchars($b) . '/licensing" class="btn btn-sm btn-outline-light" data-tip="This install\'s own licence &amp; activation"><i class="bi bi-shield-check"></i><span class="d-none d-xl-inline ms-1">This install\'s licence</span></a>'
        . '<a href="' . htmlspecialchars($b) . '/platform/app-bundle" class="btn btn-sm btn-outline-light" data-tip="Generate installers + OTA updates, publish releases — signed with this same key"><i class="bi bi-box-seam"></i><span class="d-none d-xl-inline ms-1">App Bundle</span></a>';
    include constant('BASE_PATH') . '/core/views/console-navbar.php';
?>

<div class="pls-wrap">

    <!-- ── Hero / status at a glance ─────────────────────────────── -->
    <div class="pls-hero">
        <h1><i class="bi bi-key-fill" style="color:var(--pp-accent,#4f46e5)"></i> Licence server</h1>
        <p>Issue licences, watch installations, and control how you reach the owner account on every install.</p>
        <div class="pls-chips">
            <?php if ($hasKey): ?>
                <span class="pls-chip ok"><i class="bi bi-key"></i> Signing key <code><?= htmlspecialchars($keyId) ?></code></span>
            <?php else: ?>
                <span class="pls-chip warn"><i class="bi bi-key"></i> No signing key</span>
            <?php endif; ?>

            <?php if ($stampMatches): ?>
                <span class="pls-chip ok"><i class="bi bi-shield-check"></i> Enforcement on</span>
            <?php elseif ($stamped): ?>
                <span class="pls-chip bad"><i class="bi bi-shield-exclamation"></i> Stamped key mismatch</span>
            <?php else: ?>
                <span class="pls-chip warn"><i class="bi bi-shield-slash"></i> Not stamped</span>
            <?php endif; ?>

            <?php if (($isBackup ?? false)): ?>
                <span class="pls-chip warn"><i class="bi bi-diagram-2"></i> Backup server<?= !empty($mirrorStatus['paired']) ? ' · paired' : ' · not paired' ?></span>
            <?php else: ?>
                <span class="pls-chip mut"><i class="bi bi-diagram-3"></i> Main server<?= !empty($peers) ? ' · ' . count($peers) . ' backup' . (count($peers) === 1 ? '' : 's') : '' ?></span>
            <?php endif; ?>
            <span class="pls-chip mut"><i class="bi bi-person-lock"></i> <?= htmlspecialchars($modeLabel) ?></span>
            <?php if ($hasKey): ?>
                <span class="pls-chip mut"><i class="bi bi-hdd-network"></i> <?= count($licenses) ?> licence<?= count($licenses) === 1 ? '' : 's' ?> · <?= $usedSeats ?>/<?= $totalSeats ?> seats</span>
            <?php endif; ?>
            <?php if (!$isPrimaryHost): ?>
                <span class="pls-chip warn"><i class="bi bi-diagram-2"></i> Viewing a backup host</span>
            <?php endif; ?>
        </div>
    </div>

    <!-- ── Sidebar nav + content ─────────────────────────────────── -->
    <div class="pls-layout">
    <aside class="pls-side">
    <div class="pls-nav-title">Licence server</div>
    <ul class="nav pls-tabs" role="tablist">
        <li class="nav-item" role="presentation">
            <button class="nav-link <?= $startTab === 'licences' ? 'active' : '' ?>" data-bs-toggle="tab"
                    data-bs-target="#tab-licences" type="button" role="tab">
                <i class="bi bi-card-list"></i> Licences
                <?php if ($hasKey && $licenses): ?><span class="badge bg-secondary-subtle text-secondary-emphasis"><?= count($licenses) ?></span><?php endif; ?>
            </button>
        </li>
        <li class="nav-item" role="presentation">
            <button class="nav-link <?= $startTab === 'servers' ? 'active' : '' ?>" data-bs-toggle="tab"
                    data-bs-target="#tab-servers" type="button" role="tab">
                <i class="bi bi-hdd-stack"></i> Servers &amp; keys
                <?php if (!$hasKey): ?><span class="badge bg-warning text-dark">setup</span><?php endif; ?>
            </button>
        </li>
        <li class="nav-item" role="presentation">
            <button class="nav-link <?= $startTab === 'installs' ? 'active' : '' ?>" data-bs-toggle="tab"
                    data-bs-target="#tab-installs" type="button" role="tab">
                <i class="bi bi-pc-display"></i> Installations
                <?php if (!empty($estateSummary['flagged'])): ?>
                    <span class="badge bg-danger"><?= (int) $estateSummary['flagged'] ?></span>
                <?php elseif (!empty($estateSummary['total'])): ?>
                    <span class="badge bg-secondary-subtle text-secondary-emphasis"><?= (int) $estateSummary['total'] ?></span>
                <?php endif; ?>
            </button>
        </li>
        <li class="nav-item" role="presentation">
            <button class="nav-link" data-bs-toggle="tab" data-bs-target="#tab-owner" type="button" role="tab">
                <i class="bi bi-person-badge"></i> Owner access
            </button>
        </li>
        <li class="nav-item" role="presentation">
            <button class="nav-link" data-bs-toggle="tab" data-bs-target="#tab-activity" type="button" role="tab">
                <i class="bi bi-activity"></i> Activity
            </button>
        </li>

        <li class="nav-item" role="presentation">
            <a class="nav-link" href="<?= $b ?>/licensing/replication"
               data-tip="Replication &amp; failover — every sync between this server and its backups, with timestamps and health">
                <i class="bi bi-hdd-network"></i> Replication
            </a>
        </li>
        <?php if ($hasKey): ?>
        <li class="nav-item" role="presentation">
            <a class="nav-link" href="<?= $b ?>/licensing/build-usage"
               data-tip="Build usage &amp; analytics — how much each licence is building and how much is left">
                <i class="bi bi-graph-up-arrow"></i> Build usage
            </a>
        </li>
        <li class="pls-nav-sep"><span>Distribute</span></li>
        <li class="nav-item" role="presentation">
            <a class="nav-link" href="<?= $b ?>/licensing/plans"
               data-tip="Define the plans you sell (Standard, Pro, Premium) and what each unlocks">
                <i class="bi bi-box-seam"></i> Plans &amp; packages
            </a>
        </li>
        <li class="nav-item" role="presentation">
            <a class="nav-link" href="<?= $b ?>/licensing/advisories"
               data-tip="Send notices and critical security patches to your licensed clients">
                <i class="bi bi-broadcast"></i> Push advisories
            </a>
        </li>
        <?php endif; ?>
        <li class="nav-item" role="presentation">
            <a class="nav-link" href="<?= $b ?>/help/licensing" target="_blank" rel="noopener"
               data-tip="The licence-server guide — leases, activation, plans and pushing to clients">
                <i class="bi bi-question-circle"></i> Guide <i class="bi bi-box-arrow-up-right pls-ext"></i>
            </a>
        </li>
    </ul>
    </aside>

    <div class="pls-main">
    <div class="tab-content">

    <!-- ═══ TAB: Licences ════════════════════════════════════════════ -->
    <div class="tab-pane fade <?= $startTab === 'licences' ? 'show active' : '' ?>" id="tab-licences" role="tabpanel">
        <?php if (!$hasKey): ?>
            <div class="pls-card">
                <div class="pls-empty">
                    <i class="bi bi-key"></i>
                    Create a signing key first — open <strong>Servers &amp; keys</strong> to get started.
                </div>
            </div>
        <?php else: ?>
            <div class="pls-card">
                <div class="pls-stats">
                    <div class="pls-stat"><div class="ic"><i class="bi bi-card-checklist"></i></div>
                        <div><b><?= count($licenses) ?></b><span>Licences</span></div></div>
                    <div class="pls-stat"><div class="ic"><i class="bi bi-check-circle"></i></div>
                        <div><b><?= $activeCount ?></b><span>Active</span></div></div>
                    <div class="pls-stat"><div class="ic"><i class="bi bi-pc-display"></i></div>
                        <div><b><?= $usedSeats ?> / <?= $totalSeats ?></b><span>Seats in use</span></div></div>
                </div>
            </div>

            <!-- Issue -->
            <?php
                // Build-server choices the owner can pin a licence to (Auto = mesh).
                $buildServerOptions = [['value' => '', 'label' => 'Auto — balance across all servers']];
                $__primary = \PpPhp\Core\Licensing\LicenseConfig::primary();
                if ($__primary !== '') $buildServerOptions[] = ['value' => 'primary', 'label' => 'Primary — ' . $__primary];
                foreach (\PpPhp\Core\Licensing\LicenseConfig::distributableBackups() as $__b) {
                    $buildServerOptions[] = ['value' => $__b, 'label' => 'Backup — ' . $__b];
                }
            ?>
            <div class="pls-card pls-fold" data-fold="issue" id="issueCard">
                <button type="button" class="pls-fold-head">
                    <span class="h2"><i class="bi bi-plus-circle"></i> Issue a licence</span>
                    <span class="pls-fold-meta">New entitlement</span>
                    <i class="bi bi-chevron-down pls-chev"></i>
                </button>
                <div class="pls-fold-body"><div class="pls-fold-inner">
                <p class="pls-sub">
                    A licence is an entitlement, not a machine binding: give it as many seats as the customer bought
                    and they can activate that many installations, moving them freely between servers.
                </p>
                <div class="row g-2">
                    <div class="col-md-4">
                        <label class="form-label small text-muted mb-1">Holder</label>
                        <input class="form-control form-control-sm" id="f_name" placeholder="Acme Ltd">
                    </div>
                    <div class="col-md-4">
                        <label class="form-label small text-muted mb-1">Email</label>
                        <input class="form-control form-control-sm" id="f_email" type="email" placeholder="ops@acme.com">
                    </div>
                    <div class="col-md-2">
                        <label class="form-label small text-muted mb-1">Plan <?= $hint('The package this licence is sold on. Its capabilities (e.g. server AI/Email/SMS on Premium) travel to the client in the signed lease. Define plans under “Plans & packages”.') ?></label>
                        <?php $__plans = \PpPhp\Core\Licensing\PlanRegistry::all(); ?>
                        <select class="form-select form-select-sm" id="f_plan">
                            <?php foreach ($__plans as $__p): ?>
                                <option value="<?= htmlspecialchars($__p['slug']) ?>"<?= $__p['slug'] === 'standard' ? ' selected' : '' ?>><?= htmlspecialchars($__p['name']) ?></option>
                            <?php endforeach; ?>
                        </select>
                    </div>
                    <div class="col-md-2">
                        <label class="form-label small text-muted mb-1">Seats <?= $hint('How many installations may be activated at once. Customers can move a seat between servers by deactivating one and activating another.') ?></label>
                        <input class="form-control form-control-sm" id="f_seats" type="number" min="1" value="1">
                    </div>
                    <div class="col-md-3">
                        <label class="form-label small text-muted mb-1">Ends on <?= $hint('After this date the licence stops renewing. Blank = perpetual. Grace covers an unreachable server, not an expired term, so access ends when the date passes.') ?></label>
                        <input class="form-control form-control-sm" id="f_expires" type="text" placeholder="blank = perpetual">
                    </div>
                    <div class="col-md-4">
                        <label class="form-label small text-muted mb-1">Build server <?= $hint('Which of your servers builds this customer\'s apps. Auto balances the load across your whole mesh; pin the Primary or a specific backup to steer it. The client still falls back to the mesh if the pinned server is unreachable, so a build never fails just because one server is down.') ?></label>
                        <select class="form-select form-select-sm" id="f_build">
                            <?php foreach ($buildServerOptions as $opt): ?>
                                <option value="<?= htmlspecialchars($opt['value']) ?>"><?= htmlspecialchars($opt['label']) ?></option>
                            <?php endforeach; ?>
                        </select>
                    </div>
                    <div class="col-md-3">
                        <label class="form-label small text-muted mb-1">Notes</label>
                        <input class="form-control form-control-sm" id="f_notes" placeholder="Order #1042">
                    </div>
                    <div class="col-md-2 d-flex align-items-end">
                        <button class="btn btn-primary btn-sm w-100" id="btnIssue"><i class="bi bi-plus-lg"></i> Issue</button>
                    </div>
                    <div class="col-12">
                        <div class="small text-muted" id="f_plan_limits"><i class="bi bi-hammer"></i> This plan's build limits load here.</div>
                    </div>
                </div>
                </div></div>
            </div>

            <!-- Licences table -->
            <div class="pls-card">
                <div class="pls-card-head">
                    <h2 class="mb-0"><i class="bi bi-card-list"></i> Licences
                        <span class="pls-count" id="licCount"><?= count($licenses) ?></span></h2>
                    <?php if ($licenses): ?>
                    <div class="pls-lic-tools">
                        <label class="pls-search">
                            <i class="bi bi-search"></i>
                            <input type="search" id="licSearch" placeholder="Search key, holder, email…" autocomplete="off" aria-label="Search licences">
                        </label>
                        <div class="pls-filter" id="licFilter" role="group" aria-label="Filter by status">
                            <button type="button" data-st="" class="active">All</button>
                            <button type="button" data-st="active">Active</button>
                            <button type="button" data-st="suspended">Suspended</button>
                            <button type="button" data-st="revoked">Revoked</button>
                        </div>
                    </div>
                    <?php endif; ?>
                </div>
                <?php if (!$licenses): ?>
                    <div class="pls-empty">
                        <i class="bi bi-inboxes"></i>
                        No licences yet. Issue one above and hand the customer its activation code.
                    </div>
                <?php else: ?>
                <div class="table-responsive">
                    <table class="pls-t" id="licTable">
                        <thead><tr>
                            <th>Key</th><th>Holder</th><th>Plan</th>
                            <th>Seats <?= $hint('In use / total. “In use” counts installations currently activated.') ?></th>
                            <th>Status</th><th>Ends</th><th></th>
                        </tr></thead>
                        <tbody>
                        <?php foreach ($licenses as $l): ?>
                            <tr data-status="<?= htmlspecialchars($l['status']) ?>"
                                data-search="<?= htmlspecialchars(strtolower(trim(($l['license_key'] ?? '') . ' ' . ($l['holder_name'] ?? '') . ' ' . ($l['holder_email'] ?? '') . ' ' . ($l['plan'] ?? '')))) ?>">
                                <td class="pls-key"><?= htmlspecialchars($l['license_key']) ?></td>
                                <td>
                                    <div><?= htmlspecialchars($l['holder_name'] ?: '—') ?></div>
                                    <div class="small text-muted"><?= htmlspecialchars($l['holder_email'] ?: '') ?></div>
                                </td>
                                <td><?= htmlspecialchars($l['plan']) ?></td>
                                <td><?= (int) $l['seats_used'] ?> / <?= (int) $l['seat_limit'] ?></td>
                                <td><span class="pls-pill <?= htmlspecialchars($l['status']) ?>"><?= htmlspecialchars($l['status']) ?></span></td>
                                <td><?= $l['expires_at'] ? htmlspecialchars(date('j M Y', strtotime((string) $l['expires_at']))) : '—' ?></td>
                                <td class="text-end">
                                    <button class="btn btn-sm btn-outline-secondary js-detail" data-id="<?= (int) $l['id'] ?>">
                                        <i class="bi bi-list-ul"></i> Manage
                                    </button>
                                </td>
                            </tr>
                        <?php endforeach; ?>
                        </tbody>
                    </table>
                </div>
                <div class="pls-empty" id="licNoMatch" style="display:none">
                    <i class="bi bi-search"></i> No licences match your search.
                </div>
                <?php endif; ?>
            </div>
        <?php endif; ?>
    </div>

    <!-- ═══ TAB: Servers & keys ══════════════════════════════════════ -->
    <div class="tab-pane fade <?= $startTab === 'servers' ? 'show active' : '' ?>" id="tab-servers" role="tabpanel">

        <!-- Signing key -->
        <div class="pls-card">
            <h2><i class="bi bi-key"></i> Signing key
                <?= $hint('The one secret that matters. Every licence and lease is signed with it; every install verifies against its public half. Kept in storage/license/, never in the database and never in a bundle.') ?></h2>
            <p class="pls-sub">
                Every licence you issue is signed with this key, and every installation verifies against it
                offline. It is the one secret that matters here.
            </p>

            <?php if (!$hasKey): ?>
                <div class="pls-note warn mb-3">
                    <strong>No signing key yet.</strong> Create one to start issuing licences. It is written to
                    <code>storage/license/server-key.json</code> — outside the database, and never included in a
                    release bundle.
                </div>
                <button class="btn btn-primary" id="btnKeys"><i class="bi bi-key"></i> Create signing key</button>
            <?php else: ?>
                <div class="row g-3">
                    <div class="col-md-6">
                        <div class="small text-muted mb-1">Key ID</div>
                        <div class="pls-key mb-3"><?= htmlspecialchars($keyId) ?></div>
                        <div class="small text-muted mb-1">Created</div>
                        <div class="mb-3"><?= htmlspecialchars($keyMade ? date('j M Y, H:i', strtotime($keyMade)) : '—') ?></div>
                    </div>
                    <div class="col-md-6">
                        <div class="small text-muted mb-1">Public key <span class="text-muted">(safe to share)</span></div>
                        <textarea class="form-control pls-code" rows="2" readonly onclick="this.select()"><?= htmlspecialchars($publicB64) ?></textarea>
                    </div>
                </div>

                <?php if (!$keyPerms): ?>
                    <div class="pls-note warn mt-3">
                        <i class="bi bi-exclamation-triangle"></i>
                        The key file is readable by other users on this server. Run
                        <code>chmod 600 storage/license/server-key.json</code> if your host allows it.
                    </div>
                <?php endif; ?>

                <?php if (($isBackup ?? false)): ?>
                    <div class="pls-note mt-3">
                        <i class="bi bi-diagram-2"></i> This key was <strong>synced from the main</strong> during pairing.
                        Stamping is a build-machine step for the customer bundles you ship — <strong>a backup does
                        not build those</strong>, so there is nothing to do here.
                    </div>
                <?php else: ?>
                <div class="pls-note <?= $stampMatches ? '' : 'warn' ?> mt-3">
                    <?php if ($stampMatches): ?>
                        <strong><i class="bi bi-check-circle"></i> Stamped into the source.</strong>
                        Bundles you build from here refuse activation codes signed by anyone else.
                    <?php elseif ($stamped): ?>
                        <strong>A different key is stamped in the source.</strong>
                        Installs from your bundles will not accept licences signed by the key above. Re-stamp to fix it.
                        <div class="mt-2"><button class="btn btn-warning btn-sm" id="btnStamp">Re-stamp with this key</button></div>
                    <?php else: ?>
                        <strong>Not stamped yet.</strong> <?= $hint('Stamping writes the public key into core/Licensing/LicenseTrust.php, which ships inside every bundle. Until you stamp, released copies accept any code and enforcement stays off.') ?>
                        Until you stamp it, released copies accept any activation code they are given, and
                        enforcement stays off. Stamping writes the public key above into
                        <code>core/Licensing/LicenseTrust.php</code> so it travels inside every bundle.
                        <div class="mt-2"><button class="btn btn-primary btn-sm" id="btnStamp">
                            <i class="bi bi-stamp"></i> Stamp into source</button></div>
                    <?php endif; ?>
                </div>
                <?php endif; /* isBackup vs stamp note */ ?>
            <?php endif; /* !hasKey vs has key */ ?>
        </div>

        <?php if ($hasKey): ?>
        <!-- Service addresses -->
        <div class="pls-card">
            <h2><i class="bi bi-hdd-network"></i> Service addresses
                <?= $hint('Where installations go to renew. This list ships inside every activation code and every signed lease, so changing it reaches the whole estate within a day — no customer re-enters anything.') ?></h2>
            <p class="pls-sub">
                Where installations go to renew. This list is baked into every activation code
                <em>and</em> into every signed lease — so changing it here reaches the whole estate within a
                day, without anyone re-entering anything.
            </p>

            <div class="pls-note mt-2 mb-3" style="display:flex; gap:.6rem; align-items:flex-start;">
                <i class="bi bi-shield-check" style="margin-top:.15rem;"></i>
                <div>
                    <strong>Give every activation code a fallback.</strong>
                    Clients try the <strong>Primary</strong> first, then each backup below <em>in order</em>.
                    If the primary is ever wrong or unreachable, a client with a backup in its code
                    <strong>automatically moves on to the next address</strong> instead of failing — so
                    always keep at least one working backup here (paired backups add themselves).
                    <div class="mt-2">
                        <i class="bi bi-exclamation-triangle text-warning"></i>
                        <strong>The address must be where the licence API actually answers.</strong>
                        Enter the full public <code>https://</code> URL and open
                        <code>&lt;that-url&gt;/api/license/activate</code> in a browser to confirm — a
                        PPPHP licence server replies with JSON (<em>“No pairing code…”</em>), <strong>not</strong>
                        a 404 page. If you get a 404, this server's web <strong>document root is not its
                        <code>public/</code> folder</strong>: fix the document root (recommended — it also
                        keeps your source and signing key private), or, as a temporary measure, append
                        <code>/public</code> to the address here. Re-issue activation codes after any change.
                    </div>
                </div>
            </div>

            <div class="mb-3">
                <label class="form-label small text-muted mb-1">Primary
                    <?= $hint('The permanent public HTTPS address of this licence service — where /api/license/* actually answers. Clients always try it first and it is baked into every activation code. If this server\'s document root is not its public/ folder, the API only answers under /public, so include it (better: fix the document root).') ?></label>
                <input class="form-control form-control-sm pls-code" id="serverUrl" value="<?= htmlspecialchars($serverUrl) ?>">
                <div class="form-text">The permanent public address of the licence service. Tried first, always.</div>
            </div>

            <div class="mb-3">
                <label class="form-label small text-muted mb-1">Extra backup servers <span class="text-muted">(one per line, optional)</span>
                    <?= $hint('Paired backups auto-join this list — you normally leave this empty. Only add a URL by hand for a backup you cannot pair.') ?></label>
                <textarea class="form-control form-control-sm pls-code" id="mirrors" rows="2"
                          placeholder="https://ppphp-backup.example.com"><?= htmlspecialchars(implode("\n", $mirrors)) ?></textarea>
                <div class="form-text">
                    <i class="bi bi-magic"></i> <strong>Backups you pair are added here automatically</strong> and handed to
                    every client — nothing to type. This box is only for a manual override.
                </div>
            </div>

            <div class="mb-3">
                <label class="form-label small text-muted mb-1">Server display names
                    <?= $hint('A short, friendly name each licensed client shows for its licence server instead of the raw address — e.g. SEC-SRV-NP. Carried inside the signed lease, so it reaches every install within a day and cannot be spoofed. Leave blank and the client shows an auto-generated code.') ?></label>
                <?php
                    $__eps  = \PpPhp\Core\Licensing\LicenseConfig::endpoints();
                    $__lbls = \PpPhp\Core\Licensing\LicenseConfig::labels();
                ?>
                <div id="srvLabels" class="pls-srv-labels">
                    <?php foreach ($__eps as $__i => $__ep): if ($__ep === '') continue; ?>
                        <div class="pls-srv-row">
                            <code class="pls-code" title="<?= htmlspecialchars($__ep) ?>">
                                <?php if ($__i === 0): ?><i class="bi bi-star-fill" title="Primary"></i> <?php endif; ?><?= htmlspecialchars($__ep) ?>
                            </code>
                            <input class="form-control form-control-sm srv-label" data-url="<?= htmlspecialchars($__ep) ?>"
                                   maxlength="40" value="<?= htmlspecialchars((string) ($__lbls[$__ep] ?? '')) ?>" placeholder="e.g. SEC-SRV-NP">
                        </div>
                    <?php endforeach; ?>
                </div>
                <button class="btn btn-outline-secondary btn-sm mt-2" id="btnSrvLabels"><i class="bi bi-tag"></i> Save display names</button>
                <div class="form-text">Optional. Blank shows an auto-generated code on the client. The <i class="bi bi-star-fill"></i> is your primary.</div>
            </div>

            <div class="mb-3">
                <label class="form-label small text-muted mb-1">Owner recovery email
                    <?= $hint('When a distributed install has no local mail, an owner password-reset link is emailed HERE (never to an address the install supplies). This is your secret way back into any deployment.') ?></label>
                <input class="form-control form-control-sm" type="email" id="ownerRecoveryEmail"
                       value="<?= htmlspecialchars(\PpPhp\Core\Licensing\OwnerRecovery::recoveryEmail()) ?>"
                       placeholder="you@example.com">
                <div class="form-text">Where owner-access recovery links are sent for installs with no mail of their own.</div>
            </div>

            <div class="form-check form-switch mb-3">
                <input class="form-check-input" type="checkbox" id="requireHttps" <?= $requireHttps ? 'checked' : '' ?>>
                <label class="form-check-label small" for="requireHttps">
                    Refuse activation over plain HTTP
                    <span class="text-muted">— activation is the one request that carries the licence key itself</span>
                    <?= $hint('Leases are signed so plain HTTP cannot forge one, but activation sends the licence key in the request body. Keep this on in production; turn it off only for a local http:// dev server.') ?>
                </label>
            </div>

            <button class="btn btn-primary btn-sm" id="btnUrl"><i class="bi bi-check2"></i> Save addresses</button>

            <?php if (!$isPrimaryHost): ?>
                <div class="pls-note warn mt-3">
                    <i class="bi bi-diagram-2"></i>
                    You are viewing this on <code><?= htmlspecialchars($thisOrigin) ?></code>, which is not the
                    configured primary. That is normal on a backup server — but remember that licences you issue
                    here live in <em>this</em> database, so issue from the primary unless you are deliberately
                    failing over.
                </div>
            <?php endif; ?>

            <div class="pls-note mt-3">
                <strong>Clients will try, in order:</strong>
                <ol class="mb-0 mt-1">
                    <li><code><?= htmlspecialchars($serverUrl) ?></code></li>
                    <?php foreach ($mirrors as $m): ?>
                        <li><code><?= htmlspecialchars($m) ?></code></li>
                    <?php endforeach; ?>
                </ol>
            </div>
        </div>
        <?php endif; ?>

        <?php $ms = $mirrorStatus ?? []; $paired = !empty($ms['paired']); ?>

        <?php if (($isBackup ?? false) && $paired): ?>
        <!-- BACKUP: already paired to a main -->
        <div class="pls-card">
            <h2><i class="bi bi-diagram-2"></i> This is a backup server
                <?= $hint('A backup holds a full copy of the main so nothing is lost if the main goes down. Two channels keep it warm, BOTH fully automatic — no server cron to set up: the CONTROL PLANE (signing key, licences, user accounts, plans, and the virtual-database + project catalogues) pulls every 15 minutes; the DATA channel (server databases, virtual-database .sqlite files, and every project\'s files) replicates on a slower schedule (default 6h). The backup drives both itself whenever it receives traffic. It answers renewals when the main is down but never issues — the main is the single writer.') ?></h2>
            <div class="pls-note mb-3">
                <i class="bi bi-check-circle"></i> Paired to <code><?= htmlspecialchars((string) $ms['mainUrl']) ?></code>.
                Signing key <code><?= htmlspecialchars((string) $ms['keyId']) ?></code>.
                Last sync <?= !empty($ms['lastSync']) ? htmlspecialchars(date('j M Y, H:i', (int) $ms['lastSync'])) : 'never' ?>
                · pulls every <?= (int) (($ms['interval'] ?? 900) / 60) ?> minutes.
            </div>
            <?php if (!empty($ms['lastError'])): ?>
                <div class="pls-note warn mb-3"><strong>Last sync didn't complete:</strong> <?= htmlspecialchars((string) $ms['lastError']) ?></div>
            <?php endif; ?>
            <div class="pls-note mb-3" style="background:var(--pp-surface-muted,#f1f5f9)">
                <i class="bi bi-hdd-stack"></i> <strong>What this backup holds:</strong> user accounts, licences &amp; plans,
                the managed &amp; virtual-database catalogues and every project record (control-plane, 15&nbsp;min) — plus the
                actual server databases, virtual-database files and project design trees (data clone, default 6h). If the main is
                ever lost, promote this server and nothing is missing.
                <br><i class="bi bi-magic"></i> <strong>Fully automatic:</strong> this server drives both syncs on its own
                whenever it gets a request — there is no cron to configure on the host. The button below only forces the data
                clone to run right now.
            </div>
            <button class="btn btn-primary btn-sm" id="btnSyncNow"><i class="bi bi-arrow-repeat"></i> Sync now</button>
            <button class="btn btn-outline-primary btn-sm" id="btnCloneNow"
                    data-tip="Pull the actual databases, virtual-database files and project files now (normally on the slower schedule).">
                <i class="bi bi-hdd-network"></i> Replicate data &amp; files now</button>
            <button class="btn btn-outline-danger btn-sm" id="btnUnpair"><i class="bi bi-x-circle"></i> Unpair</button>
        </div>
        <?php else: ?>

        <?php if ($hasKey && !($isBackup ?? false)): ?>
        <!-- MAIN: manage backup servers (needs a signing key) -->
        <div class="pls-card">
            <h2><i class="bi bi-diagram-3"></i> Backup servers
                <?= $hint('A backup is a hot standby that pulls the signing key + licence data from here and answers renewals when this server is down. Generate a pairing code, then run it on the backup during setup.') ?></h2>
            <p class="pls-sub">
                A backup server pairs to this one, pulls the signing key and licence data automatically, then
                re-syncs every 15 minutes. Add each backup to the <strong>Backup servers</strong> list above so
                clients fail over to it.
            </p>
            <div class="row g-2 mb-3" style="max-width:640px">
                <div class="col-sm-8">
                    <label class="form-label small text-muted mb-1">Label for the new backup (optional)</label>
                    <input class="form-control form-control-sm" id="peerLabel" placeholder="e.g. DR — Nepal">
                </div>
                <div class="col-sm-4 d-flex align-items-end">
                    <button class="btn btn-primary btn-sm w-100" id="btnPairing"><i class="bi bi-key"></i> Generate pairing code</button>
                </div>
            </div>

            <?php if (!empty($peers)): ?>
                <div class="table-responsive">
                    <table class="pls-t">
                        <thead><tr><th>Backup</th><th>Status</th><th>Last sync</th><th></th></tr></thead>
                        <tbody>
                        <?php foreach ($peers as $pr): ?>
                            <?php
                                $pill = match ($pr['status']) {
                                    'active'    => 'active',
                                    'suspended' => 'suspended',
                                    'pending'   => 'suspended',
                                    default     => 'revoked',
                                };
                            ?>
                            <tr>
                                <td><?= htmlspecialchars($pr['label'] ?: ($pr['host'] ?: 'unnamed')) ?>
                                    <?php if (!empty($pr['last_ip'])): ?><div class="small text-muted"><?= htmlspecialchars((string) $pr['last_ip']) ?></div><?php endif; ?></td>
                                <td><span class="pls-pill <?= $pill ?>"><?= htmlspecialchars($pr['status']) ?></span></td>
                                <td class="small"><?= !empty($pr['last_sync_at']) ? htmlspecialchars(date('j M H:i', strtotime((string) $pr['last_sync_at']))) : '—' ?></td>
                                <td class="text-end" style="white-space:nowrap">
                                    <?php if ($pr['status'] === 'active'): ?>
                                        <button class="btn btn-sm btn-outline-warning js-peer" data-id="<?= (int) $pr['id'] ?>" data-action="suspend">Suspend</button>
                                        <button class="btn btn-sm btn-outline-danger js-peer" data-id="<?= (int) $pr['id'] ?>" data-action="revoke">Revoke</button>
                                    <?php elseif ($pr['status'] === 'suspended'): ?>
                                        <button class="btn btn-sm btn-outline-success js-peer" data-id="<?= (int) $pr['id'] ?>" data-action="resume">Resume</button>
                                    <?php endif; ?>
                                    <button class="btn btn-sm btn-outline-secondary js-peer ms-1" data-id="<?= (int) $pr['id'] ?>" data-action="delete" title="Remove this entry"><i class="bi bi-trash"></i></button>
                                </td>
                            </tr>
                        <?php endforeach; ?>
                        </tbody>
                    </table>
                </div>
            <?php else: ?>
                <div class="text-muted small">No backup servers paired yet.</div>
            <?php endif; ?>
        </div>
        <?php endif; /* MAIN manage card */ ?>

        <!-- Assign THIS server AS a backup / secondary / fallback of a main.
             Reachable in BOTH states:
               • no signing key  → pair straight away (fresh install);
               • has a signing key → this server is currently a MAIN, so first
                 offer to convert it to a backup (archives its key, reversibly),
                 then the pair form appears. This closes the old trap where a
                 secondary keyed by accident had no way back to pairing. -->
        <div class="pls-card" id="backupSetupCard">
            <h2><i class="bi bi-diagram-2"></i> Make this a backup / secondary server
                <?= $hint('A backup (a.k.a. secondary or fallback) pairs to your main with a one-time code, pulls the main\'s signing key + licence data, and answers renewals when the main is down. It never issues — the main is the single writer.') ?></h2>

            <?php if ($hasKey): ?>
                <!-- Currently a MAIN (it has a signing key). Convert first. -->
                <?php $liveMain = (!empty($licenses) || !empty($peers)); ?>
                <div class="pls-note <?= $liveMain ? 'warn' : '' ?> mb-3">
                    <i class="bi bi-info-circle"></i>
                    <strong>This server is currently a main.</strong>
                    It holds a signing key<?= $liveMain ? ' and looks in use (' . count($licenses) . ' licence' . (count($licenses) === 1 ? '' : 's') . ' issued, ' . count($peers) . ' backup' . (count($peers) === 1 ? '' : 's') . ' paired)' : '' ?>.
                    To make it a <strong>backup</strong> instead, convert it below — its signing key is
                    <em>archived</em> (kept in <code>storage/license/</code>, not deleted), then you pair it to the real main.
                    <?php if ($liveMain): ?><br><strong>Only do this if this server was set up as a main by mistake.</strong><?php endif; ?>
                </div>
                <button class="btn <?= $liveMain ? 'btn-outline-danger' : 'btn-outline-primary' ?> btn-sm"
                        id="btnBecomeBackup" data-live="<?= $liveMain ? '1' : '0' ?>">
                    <i class="bi bi-arrow-left-right"></i> Convert this server to a backup…
                </button>
            <?php else: ?>
                <div class="pls-note mb-2"><i class="bi bi-info-circle"></i>
                    Enter your <strong>main</strong> server's address and a one-time <strong>pairing code</strong>
                    (generate it on the main under <em>Backup servers → Generate pairing code</em>).
                    This server then pulls the signing key, configuration and a copy of the licence data —
                    no manual copying, no credential mismatch.</div>
                <div class="row g-2" style="max-width:640px">
                    <div class="col-12">
                        <label class="form-label small text-muted mb-1">Main server address
                            <?= $hint('Just the domain is enough — e.g. ppphp.ppradhan.com. It auto-detects http/https and the /public folder.') ?></label>
                        <input class="form-control form-control-sm pls-code" id="pairMain"
                               value="<?= htmlspecialchars((string) ($roleMainUrl ?? '')) ?>" placeholder="ppphp.ppradhan.com">
                        <div class="ue-hint small text-muted mt-1">Domain is enough — we try https/http and the /public folder for you.</div>
                    </div>
                    <div class="col-12">
                        <button class="btn btn-outline-secondary btn-sm" id="btnTestMain"><i class="bi bi-wifi"></i> Test connection to main</button>
                        <span class="small text-muted ms-1">Check the address works before entering a code.</span>
                    </div>
                    <div class="col-sm-8">
                        <label class="form-label small text-muted mb-1">Pairing code
                            <?= $hint('Generated on the MAIN server (Backup servers → Generate pairing code). One-time and short-lived.') ?></label>
                        <input class="form-control form-control-sm pls-code" id="pairCode" placeholder="PAIR-XXXXX-XXXXX-XXXXX-XXXXX">
                    </div>
                    <div class="col-sm-4 d-flex align-items-end">
                        <button class="btn btn-primary btn-sm w-100" id="btnPair"><i class="bi bi-link-45deg"></i> Pair</button>
                    </div>
                </div>
            <?php endif; ?>
        </div>

        <?php endif; /* not a paired backup */ ?>
    </div>

    <!-- ═══ TAB: Installations ═══════════════════════════════════════ -->
    <div class="tab-pane fade <?= $startTab === 'installs' ? 'show active' : '' ?>" id="tab-installs" role="tabpanel">
        <?php if (($isBackup ?? false)): ?>
            <div class="pls-card"><p class="pls-sub mb-0">
                This is a backup server. It mirrors the main server's records; manage installations on the main.
            </p></div>
        <?php elseif (!$hasKey): ?>
            <div class="pls-card"><p class="pls-sub mb-0">
                Create a signing key on <strong>Servers &amp; keys</strong> before you can issue licences and see installations.
            </p></div>
        <?php else: ?>
        <div class="pls-card">
            <h2><i class="bi bi-pc-display"></i> Installations
                <?= $hint('Every copy of PPPHP running against this licence server: where it is, who administers it, and whether its program files still match the release you signed.') ?></h2>
            <p class="pls-sub">
                Each install reports in every hour. Anything flagged is listed first — a flag never blocks the
                customer by itself, it is here so you can decide whether to revoke.
            </p>

            <div class="pls-stats mb-3">
                <div class="pls-stat"><b><?= (int) ($estateSummary['total'] ?? 0) ?></b><span>installations</span></div>
                <div class="pls-stat"><b><?= (int) ($estateSummary['active'] ?? 0) ?></b><span>active seats</span></div>
                <div class="pls-stat"><b><?= (int) ($estateSummary['domains'] ?? 0) ?></b><span>distinct domains</span></div>
                <div class="pls-stat"><b class="<?= !empty($estateSummary['flagged']) ? 'text-danger' : '' ?>"><?= (int) ($estateSummary['flagged'] ?? 0) ?></b><span>flagged</span></div>
                <div class="pls-stat"><b class="<?= !empty($estateSummary['tampered']) ? 'text-danger' : '' ?>"><?= (int) ($estateSummary['tampered'] ?? 0) ?></b><span>tamper reports</span></div>
                <div class="pls-stat"><b><?= (int) ($estateSummary['stale'] ?? 0) ?></b><span>silent &gt; 48h</span></div>
            </div>

            <div class="position-relative mb-3" style="max-width:460px">
                <i class="bi bi-search" style="position:absolute;left:.75rem;top:50%;transform:translateY(-50%);color:var(--pp-text-muted,#94a3b8)"></i>
                <input type="search" id="instSearch" class="form-control" style="padding-left:2.2rem"
                       placeholder="Search by domain, IP, email, holder or licence key…" autocomplete="off">
            </div>

            <?php if (!$estate): ?>
                <p class="pls-sub mb-0">No installations have activated yet.</p>
            <?php else: ?>
                <div class="table-responsive">
                <table class="pls-t" id="instTable">
                    <thead><tr>
                        <th>Where</th><th>Administrator</th><th>Licence</th>
                        <th>Builds</th>
                        <th>Files</th><th>Last seen</th><th></th>
                    </tr></thead>
                    <tbody>
                    <?php foreach ($estate as $i):
                        $risk      = trim((string) ($i['risk'] ?? ''));
                        $integrity = (string) ($i['integrity'] ?? '');
                        $bad       = in_array($integrity, ['modified', 'missing'], true);
                        $seenTs    = strtotime((string) ($i['last_seen_at'] ?? '')) ?: 0;
                        $stale     = $seenTs > 0 && (time() - $seenTs) > 172800;
                        $domain    = (string) ($i['domain'] ?? '') ?: (string) ($i['host'] ?? '');
                        $keyHint   = substr((string) ($i['license_key'] ?? ''), 0, 12);
                    ?>
                        <tr data-inst>
                            <td>
                                <strong><?= htmlspecialchars($domain ?: '—') ?></strong>
                                <div class="small text-muted">
                                    <?php if (!empty($i['server_ip'])): ?>server <code><?= htmlspecialchars((string) $i['server_ip']) ?></code><?php endif; ?>
                                    <?php if (!empty($i['last_ip'])): ?> · seen from <code><?= htmlspecialchars((string) $i['last_ip']) ?></code><?php endif; ?>
                                </div>
                                <?php if (!empty($i['label'])): ?><div class="small text-muted"><?= htmlspecialchars((string) $i['label']) ?></div><?php endif; ?>
                            </td>
                            <td>
                                <?php if (!empty($i['owner_email'])): ?>
                                    <a href="mailto:<?= htmlspecialchars((string) $i['owner_email']) ?>"><?= htmlspecialchars((string) $i['owner_email']) ?></a>
                                <?php else: ?><span class="text-muted">—</span><?php endif; ?>
                                <div class="small text-muted"><?= htmlspecialchars((string) ($i['holder_name'] ?? '')) ?></div>
                                <?php if (!empty($i['holder_email'])): ?>
                                    <div class="small text-muted">buyer: <?= htmlspecialchars((string) $i['holder_email']) ?></div>
                                <?php endif; ?>
                            </td>
                            <td>
                                <code><?= htmlspecialchars($keyHint) ?>…</code>
                                <div class="small text-muted">
                                    <?= htmlspecialchars((string) ($i['plan'] ?? '')) ?> ·
                                    seat <?= (int) ($i['seat_limit'] ?? 0) ?> ·
                                    <?= htmlspecialchars((string) ($i['license_status'] ?? '')) ?>
                                </div>
                                <div class="small text-muted">v<?= htmlspecialchars((string) ($i['app_version'] ?? '?')) ?> · PHP <?= htmlspecialchars((string) ($i['php_version'] ?? '?')) ?></div>
                            </td>
                            <td>
                                <?php
                                    $bu = $i['build_usage']  ?? ['hour' => 0, 'day' => 0, 'week' => 0, 'month' => 0, 'total' => 0, 'last_at' => null];
                                    $bl = $i['build_limits']  ?? ['hour' => 0, 'day' => 0, 'week' => 0, 'month' => 0];
                                    $fmtq = static fn ($used, $cap) => ((int) $cap) > 0 ? ($used . ' / ' . (int) $cap) : (string) $used;
                                    $dCap = (int) ($bl['day'] ?? 0); $dUsed = (int) ($bu['day'] ?? 0);
                                    $hCap = (int) ($bl['hour'] ?? 0); $hUsed = (int) ($bu['hour'] ?? 0);
                                    $atCap  = ($dCap > 0 && $dUsed >= $dCap) || ($hCap > 0 && $hUsed >= $hCap);
                                    $nearCap = !$atCap && (($dCap > 0 && $dUsed >= (int) ceil($dCap * 0.8)) || ($hCap > 0 && $hUsed >= (int) ceil($hCap * 0.8)));
                                ?>
                                <div class="small<?= $atCap ? ' text-danger fw-semibold' : ($nearCap ? ' text-warning' : '') ?>" title="Builds this hour">
                                    <i class="bi bi-hammer"></i> <?= $fmtq($hUsed, $hCap) ?> <span class="text-muted">/hr</span>
                                </div>
                                <div class="small text-muted">
                                    today <?= $fmtq($dUsed, $dCap) ?> · wk <?= $fmtq((int) ($bu['week'] ?? 0), $bl['week'] ?? 0) ?> · mo <?= $fmtq((int) ($bu['month'] ?? 0), $bl['month'] ?? 0) ?>
                                </div>
                                <div class="small text-muted">
                                    total <?= (int) ($bu['total'] ?? 0) ?><?php if (!empty($bu['last_at']) && ($__lb = strtotime((string) $bu['last_at']))): ?> · last <?= htmlspecialchars(date('j M H:i', $__lb)) ?><?php endif; ?>
                                </div>
                            </td>
                            <td>
                                <?php if ($bad): ?>
                                    <span class="badge bg-danger">modified</span>
                                <?php elseif ($integrity === 'ok'): ?>
                                    <span class="badge bg-success-subtle text-success-emphasis">verified</span>
                                <?php else: ?>
                                    <span class="badge bg-secondary-subtle text-secondary-emphasis">unknown</span>
                                <?php endif; ?>
                                <?php if ($risk !== ''): ?>
                                    <div class="small text-danger mt-1"><i class="bi bi-flag-fill"></i> <?= htmlspecialchars($risk) ?></div>
                                <?php endif; ?>
                            </td>
                            <td>
                                <?= $seenTs ? htmlspecialchars(date('j M Y, H:i', $seenTs)) : '<span class="text-muted">never</span>' ?>
                                <?php if ($stale): ?><div class="small text-warning">silent</div><?php endif; ?>
                                <div class="small text-muted"><?= htmlspecialchars((string) ($i['status'] ?? '')) ?></div>
                            </td>
                            <td class="text-end">
                                <?php if ((string) ($i['status'] ?? '') === 'active'): ?>
                                    <button class="btn btn-sm btn-outline-danger js-revoke"
                                            data-iid="<?= htmlspecialchars((string) $i['instance_id']) ?>">Revoke</button>
                                <?php endif; ?>
                            </td>
                        </tr>
                    <?php endforeach; ?>
                    <tr id="instNoHit" style="display:none"><td colspan="6" class="text-center text-muted py-4">No installations match your search.</td></tr>
                    </tbody>
                </table>
                </div>
                <p class="pls-sub mt-3 mb-0">
                    <i class="bi bi-shield-lock"></i> <strong>Files</strong> is what the install reports about its own
                    program files. <em>modified</em> means the copy has already locked itself and told us; it cannot be
                    cleared by the customer, only by restoring the original release.
                </p>
            <?php endif; ?>
        </div>
        <?php endif; ?>
    </div>

    <!-- ═══ TAB: Owner access ════════════════════════════════════════ -->
    <div class="tab-pane fade" id="tab-owner" role="tabpanel">
        <div class="pls-card">
            <h2><i class="bi bi-person-badge"></i> Owner account access
                <?= $hint('How you reach the hidden owner account (' . htmlspecialchars($ownerName) . ') on every install, including your customers\'. Only ever handled as a hash or a signed token — the plaintext never leaves this box.') ?></h2>
            <p class="pls-sub">
                Controls how you sign in as <code><?= htmlspecialchars($ownerName) ?></code> — the vendor account that
                exists on every installation. Set it once here; changes reach new bundles immediately and
                already-running installs on their next daily licence check.
            </p>

            <div class="pls-note <?= $ownerPwSet ? '' : 'warn' ?> mb-3">
                <?php if ($tokenOnly): ?>
                    <i class="bi bi-shield-lock"></i> <strong>Access-token mode.</strong> Installs carry no owner
                    password at all — you mint a signed token to sign in.
                <?php elseif ($ownerPwSet): ?>
                    <i class="bi bi-check-circle"></i> A password is configured<?php if ($ownerPwWhen): ?>,
                    last set <?= htmlspecialchars(date('j M Y, H:i', strtotime($ownerPwWhen))) ?><?php endif; ?>.
                <?php else: ?>
                    <i class="bi bi-exclamation-triangle"></i> No owner password configured yet. Until you set
                    one, fresh installs seed a random password written to
                    <code>storage/owner-initial-password.txt</code>.
                <?php endif; ?>
            </div>

            <div class="mb-3">
                <label class="form-label small text-muted mb-2 d-block">How owner access works on installs
                    <?= $hint('Shared: one password you remember. Per-install: a unique password derived from your key, revealed on demand. Token-only: no password anywhere — you mint a short-lived signed token.') ?></label>
                <div class="row g-2">
                    <div class="col-md-4">
                        <label class="pls-mode<?= $mode === 'unified' ? ' on' : '' ?>">
                            <input type="radio" name="ownerMode" value="unified" <?= $mode === 'unified' ? 'checked' : '' ?>>
                            <div>
                                <strong>One shared password</strong>
                                <div class="small text-muted">You set it and remember it; works on every install.</div>
                            </div>
                        </label>
                    </div>
                    <div class="col-md-4">
                        <label class="pls-mode<?= $perInstance ? ' on' : '' ?>">
                            <input type="radio" name="ownerMode" value="per-instance" <?= $perInstance ? 'checked' : '' ?>>
                            <div>
                                <strong>Unique per install <span class="badge bg-primary-subtle text-primary-emphasis">derived</span></strong>
                                <div class="small text-muted">A different password per install, derived from your key. Reveal any on demand.</div>
                            </div>
                        </label>
                    </div>
                    <div class="col-md-4">
                        <label class="pls-mode<?= $tokenOnly ? ' on' : '' ?>">
                            <input type="radio" name="ownerMode" value="token" <?= $tokenOnly ? 'checked' : '' ?>>
                            <div>
                                <strong>Access tokens only <span class="badge bg-success-subtle text-success-emphasis">no secret on client</span></strong>
                                <div class="small text-muted">No password anywhere on the install. You mint a short-lived signed
                                    token to sign in — verified offline, works even on a locked box.</div>
                            </div>
                        </label>
                    </div>
                </div>
            </div>

            <?php if ($tokenOnly): ?>
            <div id="tokenFields">
                <div class="pls-note mb-2">
                    <i class="bi bi-shield-lock"></i> Installs carry <strong>no owner password at all</strong>. To sign in as
                    <code><?= htmlspecialchars($ownerName) ?></code> on any install, mint a token from its <strong>Manage</strong> view
                    (or a bootstrap token below for one that hasn't activated yet), then paste it in place of the password. It is
                    checked against your signing key on the box itself — no network needed.
                </div>
                <button class="btn btn-outline-primary btn-sm" id="btnBootstrapToken">
                    <i class="bi bi-key"></i> Mint a bootstrap token
                    <?= $hint('For an install that has not activated yet (no instance id). It opens a fresh install so you can finish setup before it is licensed.') ?>
                </button>
            </div>
            <?php endif; ?>

            <div id="unifiedFields" class="<?= $mode === 'unified' ? '' : 'd-none' ?>">
                <div class="row g-2" style="max-width:640px">
                    <div class="col-sm-6">
                        <label class="form-label small text-muted mb-1">New password
                            <?= $hint('At least 12 characters. Its hash ships to every install, and a bundle can be opened offline, so a weak password would be crackable — make it strong.') ?></label>
                        <input type="password" class="form-control form-control-sm" id="ownerPw" autocomplete="new-password"
                               placeholder="at least 12 characters">
                    </div>
                    <div class="col-sm-6">
                        <label class="form-label small text-muted mb-1">Confirm</label>
                        <input type="password" class="form-control form-control-sm" id="ownerPw2" autocomplete="new-password">
                    </div>
                </div>
                <button class="btn btn-primary btn-sm mt-2" id="btnOwnerPw"><i class="bi bi-shield-lock"></i> Set owner password</button>
            </div>

            <div id="perInstanceFields" class="<?= $perInstance ? '' : 'd-none' ?>">
                <div class="pls-note mb-2">
                    <i class="bi bi-magic"></i> Each install has its own password, generation <strong><?= (int) ($deriveGen ?? 1) ?></strong>.
                    Open a licence's <strong>Manage</strong> view and use <em>Reveal password</em> on any installation — it is
                    re-derived from your signing key on the spot, never stored.
                </div>
                <button class="btn btn-outline-danger btn-sm" id="btnRotateOwner">
                    <i class="bi bi-arrow-repeat"></i> Rotate all per-install passwords
                    <?= $hint('Bumps the derivation generation. Every current per-install password stops working, and each install adopts a fresh one on its next daily lease.') ?>
                </button>
            </div>

            <div class="pls-note bad mt-3">
                <strong><i class="bi bi-exclamation-octagon"></i> Two things to know.</strong>
                <ul class="mb-0 mt-1">
                    <li>This is a vendor account present on customer machines and hidden from their own
                        admins. Disclosing break-glass access in your terms is the honest default.</li>
                    <li>Only a hash (or, in token mode, nothing) ever lives on a client — the plaintext never
                        leaves this box. In shared mode, choose a strong password; per-install and token modes
                        are the stronger choices for a real customer estate.</li>
                </ul>
            </div>
        </div>
    </div>

    <!-- ═══ TAB: Activity ════════════════════════════════════════════ -->
    <div class="tab-pane fade" id="tab-activity" role="tabpanel">
        <div class="pls-card">
            <h2><i class="bi bi-activity"></i> Recent activity
                <?= $hint('Activations, daily renewals, refusals, revocations, password reveals and token mints — anything the licence server did or refused, newest first.') ?></h2>
            <p class="pls-sub">Activations, renewals, refusals and anything that looked unusual.</p>
            <?php if (empty($events)): ?>
                <div class="pls-empty"><i class="bi bi-clock-history"></i> Nothing has happened yet.</div>
            <?php else: foreach ($events as $ev): ?>
                <div class="pls-ev">
                    <time><?= htmlspecialchars(date('j M H:i', strtotime((string) $ev['created_at']))) ?></time>
                    <span class="k"><?= htmlspecialchars(str_replace('_', ' ', (string) $ev['kind'])) ?></span>
                    <span class="flex-grow-1"><?= htmlspecialchars((string) $ev['detail']) ?></span>
                    <?php if (!empty($ev['ip'])): ?><span class="text-muted small"><?= htmlspecialchars((string) $ev['ip']) ?></span><?php endif; ?>
                </div>
            <?php endforeach; endif; ?>
        </div>
    </div>

    </div><!-- /tab-content -->
    </div><!-- /pls-main -->
    </div><!-- /pls-layout -->
</div>

<!-- Manage-licence modal -->
<div class="modal fade" id="detailModal" tabindex="-1">
    <div class="modal-dialog modal-lg modal-dialog-scrollable">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title"><i class="bi bi-card-list me-1"></i> Licence</h5>
                <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
            </div>
            <div class="modal-body" id="detailBody"></div>
        </div>
    </div>
</div>

<script src="<?= $assetsUrl ?>/bootstrap/js/bootstrap.bundle.min.js"></script>
<script>
(function () {
    const B = '<?= $b ?>', TOKEN = '<?= htmlspecialchars($token) ?>';
    const esc = (s) => String(s ?? '').replace(/[&<>"]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));

    // Build-server choices (Auto + Primary + backups) for the licence detail editor.
    const BUILD_SERVERS = <?= json_encode($buildServerOptions ?? [['value' => '', 'label' => 'Auto — balance across all servers']], JSON_UNESCAPED_SLASHES) ?>;
    const buildServerOptions = (current) => BUILD_SERVERS.map(o =>
        `<option value="${esc(o.value)}"${String(current || '') === o.value ? ' selected' : ''}>${esc(o.label)}</option>`).join('');

    // One window's value out of a stored build-limit override (a JSON string, an
    // object, or empty) — '' means "inherit the plan / global default".
    const blimVal = (raw, w) => {
        try {
            const o = typeof raw === 'string' ? (raw ? JSON.parse(raw) : {}) : (raw || {});
            const v = o ? o[w] : null;
            return (v === null || v === undefined) ? '' : v;
        } catch (e) { return ''; }
    };

    const PLANS = <?= json_encode(array_map(static fn ($p) => ['slug' => $p['slug'], 'name' => $p['name'], 'build_limits' => $p['build_limits'] ?? null], \PpPhp\Core\Licensing\PlanRegistry::all()), JSON_UNESCAPED_SLASHES) ?>;
    // Global default build limits + the four windows — for showing what a licence
    // INHERITS from its plan (and then the global default) in the Manage modal.
    const GLOBAL_BUILD_LIMITS = <?= json_encode(class_exists(\PpPhp\Core\Build\BuildMeter::class) ? \PpPhp\Core\Build\BuildMeter::globalLimits() : ['hour' => 0, 'day' => 0, 'week' => 0, 'month' => 0]) ?>;
    const BUILD_WINDOWS = ['hour', 'day', 'week', 'month'];
    // The value a licence on `planSlug` INHERITS for window `w` (plan value if set,
    // else the global default). 0 = unlimited.
    const inheritedLimit = (planSlug, w) => {
        const p = PLANS.find(x => x.slug === planSlug);
        const pv = p && p.build_limits ? p.build_limits[w] : null;
        if (pv !== null && pv !== undefined) return pv;
        const g = GLOBAL_BUILD_LIMITS ? GLOBAL_BUILD_LIMITS[w] : 0;
        return (g === null || g === undefined) ? 0 : g;
    };
    const limitLabel = (v) => (v === 0 || v === null || v === undefined) ? '∞' : String(v);
    // A one-line summary of what a plan grants across all windows.
    const inheritedSummary = (planSlug) =>
        BUILD_WINDOWS.map(w => limitLabel(inheritedLimit(planSlug, w)) + '/' + w[0]).join(' · ');
    const planOptions = (current) => PLANS.map(p =>
        `<option value="${esc(p.slug)}"${String(current || '') === p.slug ? ' selected' : ''}>${esc(p.name)}</option>`).join('')
        // Keep an unknown legacy plan visible rather than silently switching it.
        + (PLANS.some(p => p.slug === String(current || '')) ? '' : (current ? `<option value="${esc(current)}" selected>${esc(current)} (legacy)</option>` : ''));

    // What each plan grants — for the little chip row under the Plan picker.
    const PLAN_CAPS = <?php
        $__pc = [];
        foreach (\PpPhp\Core\Licensing\PlanRegistry::all() as $__pl) {
            $__row = [];
            foreach (\PpPhp\Core\Licensing\Capability::CATALOG as $__ck => $__cm) {
                $__row[] = ['label' => $__cm['label'], 'on' => !empty($__pl['capabilities'][$__ck])];
            }
            $__pc[$__pl['slug']] = $__row;
        }
        echo json_encode($__pc, JSON_UNESCAPED_SLASHES);
    ?>;
    const planCapChips = (slug) => {
        const caps = PLAN_CAPS[String(slug || '')];
        if (!caps) return '<span class="text-muted" style="font-size:.78rem">No server capabilities.</span>';
        const on = caps.filter(c => c.on);
        if (!on.length) return '<span class="text-muted" style="font-size:.78rem">No server capabilities on this plan.</span>';
        return on.map(c => `<span class="pls-cap-chip"><i class="bi bi-check-lg"></i> ${esc(c.label)}</span>`).join(' ');
    };

    // Remember the open tab across reloads (mode/save actions reload the page).
    document.querySelectorAll('[data-bs-toggle="tab"]').forEach(t => t.addEventListener('shown.bs.tab', e => {
        history.replaceState(null, '', e.target.dataset.bsTarget);
    }));
    if (location.hash && document.querySelector('[data-bs-target="' + location.hash + '"]')) {
        new bootstrap.Tab(document.querySelector('[data-bs-target="' + location.hash + '"]')).show();
    }

    async function post(path, body, btn) {
        const label = btn ? btn.innerHTML : '';
        if (btn) { btn.disabled = true; btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span>'; }
        try {
            const r = await fetch(B + path, {
                method: 'POST', credentials: 'same-origin',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(Object.assign({ _token: TOKEN }, body))
            });
            return await r.json();
        } catch (e) {
            return { success: false, message: 'Request failed: ' + e.message };
        } finally {
            if (btn) { btn.disabled = false; btn.innerHTML = label; }
        }
    }
    const done = (res, reload = true) => Swal.fire({
        icon: res.success ? 'success' : 'error',
        title: res.success ? 'Done' : 'Not completed',
        text: res.message || ''
    }).then(() => { if (res.success && reload) location.reload(); });

    document.getElementById('btnKeys')?.addEventListener('click', async (e) => {
        const ask = await Swal.fire({
            icon: 'question', title: 'Create the signing key?',
            html: 'Back it up as soon as it exists. If it is lost, <b>no existing licence can be renewed</b> and every customer would need re-activating.',
            showCancelButton: true, confirmButtonText: 'Create it'
        });
        if (ask.isConfirmed) done(await post('/licensing/server/keys', {}, e.currentTarget));
    });

    document.getElementById('btnStamp')?.addEventListener('click', async (e) =>
        done(await post('/licensing/server/stamp', {}, e.currentTarget)));

    document.getElementById('btnOwnerPw')?.addEventListener('click', async (e) => {
        const pw = document.getElementById('ownerPw').value;
        const pw2 = document.getElementById('ownerPw2').value;
        if (!pw) { Swal.fire({ icon: 'info', title: 'Enter a new password' }); return; }
        if (pw !== pw2) { Swal.fire({ icon: 'error', title: 'Passwords do not match' }); return; }
        const res = await post('/licensing/server/owner-password', { password: pw, confirm: pw2 }, e.currentTarget);
        if (res.success) { document.getElementById('ownerPw').value = ''; document.getElementById('ownerPw2').value = ''; }
        done(res, false);
    });

    // Owner-access mode: save, then reload so the right fields render.
    document.querySelectorAll('input[name="ownerMode"]').forEach(r => r.addEventListener('change', async (e) => {
        const res = await post('/licensing/server/owner-mode', { mode: e.currentTarget.value });
        if (!res.success) { done(res); return; }
        await Swal.fire({ icon: 'success', title: 'Mode changed', text: res.message });
        location.hash = '#tab-owner';
        location.reload();
    }));

    // Show a minted access token to the owner (copy + expiry).
    function showToken(res) {
        Swal.fire({
            icon: 'success', title: 'Owner access token',
            html: '<div style="text-align:left">'
                + '<p style="font-size:.85rem;margin-bottom:.4rem">Sign in as <code>' + esc(res.username) + '</code> on that install, '
                + 'pasting this in place of the password. Valid for <strong>' + esc(res.ttlHuman) + '</strong>.</p>'
                + '<textarea readonly rows="3" class="pls-reveal" style="width:100%" onclick="this.select()">' + esc(res.token) + '</textarea>'
                + '<p style="font-size:.78rem;color:#64748b;margin-top:.5rem">Checked against your signing key on the box itself — '
                + 'no network needed, works even if its licence has lapsed.</p></div>',
            width: 560, confirmButtonText: 'Done'
        });
    }

    document.getElementById('btnBootstrapToken')?.addEventListener('click', async (e) => {
        const res = await post('/licensing/server/access-token', { instanceId: 'bootstrap' }, e.currentTarget);
        if (res.success) showToken(res); else done(res);
    });

    // ── Backup replication ──────────────────────────────────────
    // MAIN: generate a pairing code for a new backup.
    document.getElementById('btnPairing')?.addEventListener('click', async (e) => {
        const res = await post('/licensing/server/pairing', { label: document.getElementById('peerLabel').value }, e.currentTarget);
        if (!res.success) { done(res); return; }
        await Swal.fire({
            icon: 'success', title: 'Pairing code',
            html: '<div style="text-align:left">'
                + '<p style="font-size:.85rem">On the <b>backup</b> server, open Licence server → Servers &amp; keys → '
                + '“<b>Make this a backup / secondary server</b>”, enter this main address and code, and click Pair. '
                + 'The code is one-time and valid for <b>' + esc(res.ttlHuman) + '</b>.</p>'
                + '<p style="font-size:.8rem;margin-bottom:.2rem">Main address</p>'
                + '<div class="pls-reveal" style="font-size:.9rem" onclick="getSelection().selectAllChildren(this)">' + esc(res.mainUrl) + '</div>'
                + '<p style="font-size:.8rem;margin:.6rem 0 .2rem">Pairing code</p>'
                + '<div class="pls-reveal" onclick="getSelection().selectAllChildren(this)">' + esc(res.code) + '</div></div>',
            width: 560, confirmButtonText: 'Done'
        });
        location.hash = '#tab-servers'; location.reload();
    });

    document.querySelectorAll('.js-peer').forEach(btn => btn.addEventListener('click', async () => {
        const action = btn.dataset.action;
        const confirmText = {
            suspend: 'Suspend this backup? It stops syncing until you resume it. The data it already has stays valid.',
            revoke:  'Revoke this backup? It can no longer sync, and would have to re-pair. Its data stays until replaced.',
            delete:  'Delete this backup entry from the list? If it is still paired, revoke it first.',
        }[action];
        if (confirmText) {
            const ask = await Swal.fire({ icon:'warning', title:'Are you sure?', text: confirmText,
                showCancelButton:true, confirmButtonText:'Yes', confirmButtonColor: action === 'suspend' ? '#b45309' : '#dc2626' });
            if (!ask.isConfirmed) return;
        }
        location.hash = '#tab-servers';
        done(await post('/licensing/server/peer-manage', { id: btn.dataset.id, action }, btn));
    }));

    // BACKUP: test reachability before pairing.
    document.getElementById('btnTestMain')?.addEventListener('click', async (e) => {
        const res = await post('/licensing/server/test-main', { mainUrl: document.getElementById('pairMain').value }, e.currentTarget);
        Swal.fire({ icon: res.success ? 'success' : 'error',
                    title: res.success ? 'Connection OK' : 'Cannot reach the main',
                    text: res.message });
    });

    // Convert a keyed MAIN into a backup: archive its signing key, then reload so
    // the pair form appears. A server that looks in use (licences/backups) needs an
    // explicit typed confirmation first.
    document.getElementById('btnBecomeBackup')?.addEventListener('click', async (e) => {
        const btn = e.currentTarget;
        const live = btn.dataset.live === '1';
        if (live) {
            const ask = await Swal.fire({
                icon: 'warning', title: 'Convert this main to a backup?',
                html: 'This server has issued licences and/or has paired backups. Converting it <b>archives its signing key</b> '
                    + 'and it will <b>stop issuing licences</b>. The key file is kept in <code>storage/license/</code> so this is '
                    + 'reversible, but every client that renews against <em>this</em> address will fail over to the main.<br><br>'
                    + 'Type <b>CONVERT</b> to proceed.',
                input: 'text', inputPlaceholder: 'CONVERT',
                showCancelButton: true, confirmButtonText: 'Convert to backup', confirmButtonColor: '#dc2626',
                preConfirm: (v) => (String(v || '').trim().toUpperCase() === 'CONVERT') ? true
                    : Swal.showValidationMessage('Type CONVERT to confirm')
            });
            if (!ask.isConfirmed) return;
        } else {
            const ask = await Swal.fire({
                icon: 'question', title: 'Make this a backup?',
                html: 'This server was set up with a signing key (so it is a main). Converting it <b>archives that key</b> '
                    + '(kept in <code>storage/license/</code>, not deleted) so you can pair it to your real main as a backup.',
                showCancelButton: true, confirmButtonText: 'Convert to backup'
            });
            if (!ask.isConfirmed) return;
        }
        location.hash = '#tab-servers';
        const res = await post('/licensing/server/become-backup', { confirm: true }, btn);
        done(res); // on success the page reloads and the pair form is shown
    });

    // BACKUP: pair / sync / unpair.
    document.getElementById('btnPair')?.addEventListener('click', async (e) => {
        location.hash = '#tab-servers';
        done(await post('/licensing/server/pair', {
            mainUrl: document.getElementById('pairMain').value,
            code:    document.getElementById('pairCode').value
        }, e.currentTarget));
    });
    document.getElementById('btnSyncNow')?.addEventListener('click', async (e) => {
        location.hash = '#tab-servers'; done(await post('/licensing/server/sync-now', {}, e.currentTarget));
    });
    document.getElementById('btnCloneNow')?.addEventListener('click', async (e) => {
        const btn = e.currentTarget;
        const ask = await Swal.fire({ icon:'question', title:'Replicate data & files now?',
            html:'Pulls the actual server databases, virtual-database <code>.sqlite</code> files and every project’s files from the main. '
               + 'This can take a while for large data — it normally runs on the schedule.',
            showCancelButton:true, confirmButtonText:'Replicate now' });
        if (!ask.isConfirmed) return;
        location.hash = '#tab-servers';
        done(await post('/licensing/server/clone-now', {}, btn));
    });
    document.getElementById('btnUnpair')?.addEventListener('click', async (e) => {
        const ask = await Swal.fire({ icon:'warning', title:'Unpair from the main?',
            text:'This server stops syncing. The licence data it already has stays until you re-pair.',
            showCancelButton:true, confirmButtonText:'Unpair', confirmButtonColor:'#dc2626' });
        if (ask.isConfirmed) { location.hash = '#tab-servers'; done(await post('/licensing/server/unpair', {}, e.currentTarget)); }
    });

    document.getElementById('btnRotateOwner')?.addEventListener('click', async (e) => {
        const ask = await Swal.fire({
            icon: 'warning', title: 'Rotate every per-install password?',
            text: 'All current per-install passwords stop working. Each install adopts its new one within a day.',
            showCancelButton: true, confirmButtonText: 'Rotate all', confirmButtonColor: '#dc2626'
        });
        if (ask.isConfirmed) done(await post('/licensing/server/rotate-owner', {}, e.currentTarget));
    });

    document.getElementById('btnUrl')?.addEventListener('click', async (e) => {
        const res = await post('/licensing/server/url', {
            url:                document.getElementById('serverUrl').value,
            mirrors:            document.getElementById('mirrors').value,
            requireHttps:       document.getElementById('requireHttps').checked,
            ownerRecoveryEmail: document.getElementById('ownerRecoveryEmail')?.value || ''
        }, e.currentTarget);
        if (res.success) location.hash = '#tab-servers';
        done(res);      // reload so the "clients will try" list reflects what was saved
    });

    // Save the friendly per-server display names (carried to clients in the lease).
    document.getElementById('btnSrvLabels')?.addEventListener('click', async (e) => {
        const labels = {};
        document.querySelectorAll('.srv-label').forEach(function (i) { labels[i.dataset.url] = i.value.trim(); });
        const res = await post('/licensing/server/labels', { labels: labels }, e.currentTarget);
        done(res);
    });

    // Replace the native date box on the Issue form with the nice calendar.
    if (window.flatpickr) flatpickr('#f_expires', { dateFormat: 'Y-m-d', allowInput: true, minDate: 'today' });

    // ── Foldable cards — remember open/closed per card in localStorage ──
    document.querySelectorAll('.pls-fold').forEach(function (card) {
        var key = 'plsFold:' + (card.dataset.fold || card.id || '');
        var head = card.querySelector('.pls-fold-head');
        var saved = null; try { saved = localStorage.getItem(key); } catch (e) {}
        // Default: "Issue a licence" starts collapsed once there are several licences
        // (you mostly manage, not issue); otherwise open. An explicit choice wins.
        var many = (document.querySelectorAll('#licTable tbody tr').length || 0) > 6;
        var open = saved === null ? !(card.dataset.fold === 'issue' && many) : saved === '1';
        card.classList.toggle('open', open);
        if (head) head.setAttribute('aria-expanded', open ? 'true' : 'false');
        head && head.addEventListener('click', function () {
            var now = !card.classList.contains('open');
            card.classList.toggle('open', now);
            head.setAttribute('aria-expanded', now ? 'true' : 'false');
            try { localStorage.setItem(key, now ? '1' : '0'); } catch (e) {}
        });
    });

    // ── Licence search + status filter ──
    (function () {
        var search = document.getElementById('licSearch');
        var table = document.getElementById('licTable');
        if (!table) return;
        var filter = document.getElementById('licFilter');
        var count = document.getElementById('licCount');
        var noMatch = document.getElementById('licNoMatch');
        var rows = Array.prototype.slice.call(table.querySelectorAll('tbody tr'));
        var total = rows.length;
        var st = '';
        function apply() {
            var q = (search ? search.value : '').trim().toLowerCase();
            var shown = 0;
            rows.forEach(function (tr) {
                var okQ = q === '' || (tr.dataset.search || tr.textContent).toLowerCase().indexOf(q) >= 0;
                var okS = st === '' || tr.dataset.status === st;
                var vis = okQ && okS;
                tr.style.display = vis ? '' : 'none';
                if (vis) shown++;
            });
            if (count) count.textContent = (shown === total) ? total : (shown + ' / ' + total);
            if (noMatch) noMatch.style.display = shown === 0 ? '' : 'none';
            if (table.parentElement) table.parentElement.style.display = shown === 0 ? 'none' : '';
        }
        if (search) search.addEventListener('input', apply);
        if (filter) filter.addEventListener('click', function (e) {
            var btn = e.target.closest('button[data-st]'); if (!btn) return;
            st = btn.dataset.st;
            filter.querySelectorAll('button').forEach(function (b) { b.classList.toggle('active', b === btn); });
            apply();
        });
    }());

    // Live hint: the build limits a new licence will inherit from the chosen plan.
    (function () {
        const box = document.getElementById('f_plan_limits'), sel = document.getElementById('f_plan');
        if (!box || !sel) return;
        const refresh = () => {
            const slug = sel.value, name = (PLANS.find(x => x.slug === slug) || {}).name || slug;
            box.innerHTML = '<i class="bi bi-hammer"></i> <strong>' + esc(name) + '</strong> plan build limits: '
                + '<strong>' + esc(inheritedSummary(slug)) + '</strong> '
                + '<span class="text-muted">(per hour · day · week · month; ∞ = unlimited — the licence inherits these unless you override them under Manage).</span>';
        };
        sel.addEventListener('change', refresh);
        refresh();
    })();

    document.getElementById('btnIssue')?.addEventListener('click', async (e) => {
        const res = await post('/licensing/server/issue', {
            holder_name:  document.getElementById('f_name').value,
            holder_email: document.getElementById('f_email').value,
            plan:         document.getElementById('f_plan').value,
            seat_limit:   document.getElementById('f_seats').value,
            expires_at:   document.getElementById('f_expires').value,
            build_server: document.getElementById('f_build').value,
            notes:        document.getElementById('f_notes').value
        }, e.currentTarget);

        if (!res.success) { done(res); return; }
        await Swal.fire({
            icon: 'success', title: 'Licence issued',
            html: '<div style="text-align:left">'
                + '<p style="font-size:.9rem">Send the customer this activation code — it is all they need to paste.</p>'
                + '<textarea readonly rows="4" style="width:100%;font:500 .72rem/1.4 ui-monospace,Menlo,monospace" '
                + 'onclick="this.select()">' + esc(res.code) + '</textarea>'
                + '<p style="font-size:.8rem;color:#64748b;margin-top:.5rem">Licence key: <code>' + esc(res.license.license_key) + '</code></p>'
                + '</div>',
            width: 640, confirmButtonText: 'Done'
        });
        location.hash = '#tab-licences';
        location.reload();
    });

    // ── Manage one licence ──────────────────────────────────────
    const modal = new bootstrap.Modal(document.getElementById('detailModal'));

    document.querySelectorAll('.js-detail').forEach(btn => btn.addEventListener('click', async () => {
        const r = await fetch(B + '/licensing/server/license/' + btn.dataset.id, { credentials: 'same-origin' });
        const d = await r.json();
        if (!d.success) { Swal.fire({ icon: 'error', text: d.message }); return; }
        document.getElementById('detailBody').innerHTML = renderDetail(d);
        modal.show();
        bindDetail(d);
    }));

    function renderDetail(d) {
        const l = d.license;
        const instances = d.instances.length ? d.instances.map(i => `
            <tr>
                <td>
                    <div>${esc(i.label || i.host || 'unnamed')}</div>
                    <div class="small text-muted">${esc(i.host)} · <code>${esc(String(i.instance_id).slice(0, 12))}…</code></div>
                </td>
                <td class="small">${esc(i.last_ip || '—')}</td>
                <td class="small">${i.last_seen_at ? esc(i.last_seen_at.slice(0, 16)) : '—'}</td>
                <td><span class="pls-pill ${esc(i.status)}">${esc(i.status)}</span>
                    ${i.risk ? `<span class="pls-pill risk ms-1" title="Machine traits changed under these credentials">${esc(i.risk)}</span>` : ''}</td>
                <td class="text-end">
                    ${d.perInstance ? `<button class="btn btn-sm btn-outline-secondary js-reveal me-1" data-iid="${esc(i.instance_id)}" title="Show this install's owner password"><i class="bi bi-eye"></i></button>` : ''}
                    <button class="btn btn-sm btn-outline-primary js-token me-1" data-iid="${esc(i.instance_id)}" title="Mint a one-time owner login token for this install"><i class="bi bi-key"></i></button>
                    ${i.status === 'active'
                    ? `<button class="btn btn-sm btn-outline-danger js-revoke" data-iid="${esc(i.instance_id)}">Revoke</button>` : ''}</td>
            </tr>`).join('') : '<tr><td colspan="5" class="pls-empty"><i class="bi bi-pc-display"></i> No installations have activated yet.</td></tr>';

        const events = d.events.map(e => `
            <div class="pls-ev"><time>${esc(String(e.created_at).slice(5, 16))}</time>
            <span class="k">${esc(String(e.kind).replace(/_/g, ' '))}</span>
            <span class="flex-grow-1">${esc(e.detail)}</span></div>`).join('');

        return `
        <div class="mb-3">
            <div class="pls-key mb-1">${esc(l.license_key)}</div>
        </div>

        <div class="row g-2 mb-3">
            <div class="col-6"><label class="form-label small text-muted mb-1">Vendor / holder name</label>
                <input class="form-control form-control-sm" id="d_name" value="${esc(l.holder_name || '')}" placeholder="Company or person"></div>
            <div class="col-6"><label class="form-label small text-muted mb-1">Email</label>
                <input class="form-control form-control-sm" id="d_email" type="email" value="${esc(l.holder_email || '')}" placeholder="name@example.com"></div>
            <div class="col-4"><label class="form-label small text-muted mb-1">Seats</label>
                <input class="form-control form-control-sm" id="d_seats" type="number" min="1" value="${Number(l.seat_limit)}"></div>
            <div class="col-4"><label class="form-label small text-muted mb-1">Status</label>
                <select class="form-select form-select-sm" id="d_status">
                    ${['active','suspended','revoked'].map(s =>
                        `<option value="${s}"${l.status === s ? ' selected' : ''}>${s}</option>`).join('')}
                </select></div>
            <div class="col-4"><label class="form-label small text-muted mb-1">Ends on</label>
                <input class="form-control form-control-sm" id="d_expires" type="text" placeholder="blank = perpetual"
                       value="${l.expires_at ? esc(String(l.expires_at).slice(0, 10)) : ''}"></div>
            <div class="col-6"><label class="form-label small text-muted mb-1">Plan</label>
                <select class="form-select form-select-sm" id="d_plan">${planOptions(l.plan)}</select></div>
            <div class="col-6"><label class="form-label small text-muted mb-1">Build server</label>
                <select class="form-select form-select-sm" id="d_build">${buildServerOptions(l.build_server)}</select></div>
            <div class="col-12">
                <label class="form-label small text-muted mb-1">Build limit override
                    <span class="text-muted">(blank = inherit the plan · 0 = unlimited)</span></label>
                <div class="row g-2">
                    ${BUILD_WINDOWS.map(w =>
                        `<div class="col-3"><label class="form-label small text-muted mb-0">Per ${w}</label>`
                        + `<input class="form-control form-control-sm d_blim" data-window="${w}" type="number" min="0" `
                        + `value="${blimVal(l.build_limits_override, w)}" placeholder="${limitLabel(inheritedLimit(l.plan, w))}"></div>`).join('')}
                </div>
                <div class="small text-muted mt-1">Inherits the <strong id="d_effplan">${esc((PLANS.find(x => x.slug === l.plan) || {}).name || l.plan)}</strong> plan:
                    <span id="d_effsum">${inheritedSummary(l.plan)}</span> <span class="text-muted">(a blank field uses these)</span></div>
            </div>
            <div class="col-12"><div class="pls-plan-caps" id="d_caps">${planCapChips(l.plan)}</div></div>
            <div class="col-12 d-flex align-items-center">
                <button class="btn btn-sm btn-outline-danger" id="d_delete" data-id="${Number(l.id)}" data-key="${esc(l.license_key)}"
                        title="Permanently delete this licence and all its data">
                    <i class="bi bi-trash"></i> Delete licence</button>
                <button class="btn btn-sm btn-primary ms-auto" id="d_save" data-id="${Number(l.id)}">Save changes</button></div>
        </div>

        <h6 class="mt-4">Activation code</h6>
        <p class="small text-muted mb-1">Same code for every seat on this licence.</p>
        <textarea readonly rows="3" class="form-control pls-code mb-4" onclick="this.select()">${esc(d.code)}</textarea>

        <h6>Installations</h6>
        <div class="table-responsive mb-4"><table class="pls-t">
            <thead><tr><th>Installation</th><th>Last IP</th><th>Last seen</th><th>Status</th><th></th></tr></thead>
            <tbody>${instances}</tbody></table></div>

        <h6>History</h6>
        <div>${events || '<div class="pls-empty">Nothing recorded.</div>'}</div>`;
    }

    function bindDetail(d) {
        // A nice calendar in place of the browser's native date box.
        if (window.flatpickr) flatpickr('#d_expires', { dateFormat: 'Y-m-d', allowInput: true });

        // Reflect the selected plan's server capabilities AND build limits live, so
        // the owner sees exactly what a licence carries the moment they pick a plan.
        document.getElementById('d_plan')?.addEventListener('change', (e) => {
            const slug = e.target.value;
            const box = document.getElementById('d_caps');
            if (box) box.innerHTML = planCapChips(slug);
            // Update the inherited placeholders + the summary line.
            document.querySelectorAll('.d_blim').forEach((c) => {
                c.placeholder = limitLabel(inheritedLimit(slug, c.dataset.window));
            });
            const sum = document.getElementById('d_effsum');
            if (sum) sum.textContent = inheritedSummary(slug);
            const pn = document.getElementById('d_effplan');
            if (pn) pn.textContent = (PLANS.find(x => x.slug === slug) || {}).name || slug;
        });

        document.getElementById('d_save')?.addEventListener('click', async (e) =>
            done(await post('/licensing/server/update', {
                id:           e.currentTarget.dataset.id,
                holder_name:  document.getElementById('d_name').value,
                holder_email: document.getElementById('d_email').value,
                plan:         document.getElementById('d_plan').value,
                seat_limit:   document.getElementById('d_seats').value,
                status:       document.getElementById('d_status').value,
                expires_at:   document.getElementById('d_expires').value,
                build_server: document.getElementById('d_build').value,
                build_limits_override: (function () {
                    var o = {};
                    document.querySelectorAll('.d_blim').forEach(function (c) {
                        o[c.dataset.window] = c.value === '' ? null : (parseInt(c.value, 10) || 0);
                    });
                    return o;
                })()
            }, e.currentTarget)));

        // Permanent delete (owner-only — the whole console is owner-gated). Two-step
        // confirm since this destroys the licence, its installs and its build history.
        document.getElementById('d_delete')?.addEventListener('click', async (e) => {
            const id = e.currentTarget.dataset.id, key = e.currentTarget.dataset.key || 'this licence';
            const ask = await Swal.fire({
                icon: 'warning', title: 'Delete this licence permanently?',
                html: '<div style="text-align:left;font-size:.9rem">'
                    + '<p>This removes <code>' + esc(key) + '</code> and <strong>everything tied to it</strong> — '
                    + 'its installations, audit history and build counters. This cannot be undone.</p>'
                    + '<p class="text-muted" style="font-size:.85rem">To disable a licence but keep the record, use <em>Revoke</em> on its installation instead.</p></div>',
                showCancelButton: true, confirmButtonText: 'Delete permanently',
                confirmButtonColor: '#dc2626', focusCancel: true
            });
            if (!ask.isConfirmed) return;
            const res = await post('/licensing/server/license-delete', { id }, e.currentTarget);
            if (res && res.success) {
                modal.hide();
                await Swal.fire({ icon: 'success', title: 'Licence deleted', timer: 1200, showConfirmButton: false });
                location.reload();
            } else { done(res); }
        });

        document.querySelectorAll('.js-revoke').forEach(btn => btn.addEventListener('click', async () => {
            const ask = await Swal.fire({
                icon: 'warning', title: 'Revoke this installation?',
                text: 'Its seat is freed immediately. The installation itself stops working at its next daily check — apps it has already built keep running.',
                showCancelButton: true, confirmButtonText: 'Revoke', confirmButtonColor: '#dc2626'
            });
            if (ask.isConfirmed) done(await post('/licensing/server/revoke-instance', { instanceId: btn.dataset.iid }, btn));
        }));

        // Installations tab: instant client-side filter across every visible field.
        const instSearch = document.getElementById('instSearch');
        if (instSearch) instSearch.addEventListener('input', () => {
            const q = instSearch.value.trim().toLowerCase();
            let any = false;
            document.querySelectorAll('#instTable tr[data-inst]').forEach(tr => {
                const hit = q === '' || tr.textContent.toLowerCase().includes(q);
                tr.style.display = hit ? '' : 'none';
                if (hit) any = true;
            });
            const none = document.getElementById('instNoHit');
            if (none) none.style.display = any ? 'none' : '';
        });

        document.querySelectorAll('.js-token').forEach(btn => btn.addEventListener('click', async () => {
            const res = await post('/licensing/server/access-token', { instanceId: btn.dataset.iid }, btn);
            if (res.success) showToken(res); else Swal.fire({ icon: 'error', title: 'Could not mint', text: res.message });
        }));

        document.querySelectorAll('.js-reveal').forEach(btn => btn.addEventListener('click', async () => {
            const res = await post('/licensing/server/reveal-password', { instanceId: btn.dataset.iid }, btn);
            if (!res.success) { Swal.fire({ icon: 'error', title: 'Could not reveal', text: res.message }); return; }
            Swal.fire({
                icon: 'info', title: 'Owner login for this install',
                html: '<div style="text-align:left">'
                    + '<p style="font-size:.85rem;margin-bottom:.4rem">Username <code>' + esc(res.username) + '</code></p>'
                    + '<div class="pls-reveal" onclick="getSelection().selectAllChildren(this)">' + esc(res.password) + '</div>'
                    + '<p style="font-size:.78rem;color:#64748b;margin-top:.6rem">Re-derived from your signing key just now — nothing was stored. '
                    + 'Click to select, then copy.</p></div>',
                width: 480, confirmButtonText: 'Done'
            });
        }));
    }
})();
</script>
</body>
</html>
