<!DOCTYPE html>
<?php
/* Owner-only build-usage analytics — interactive dashboard.
   Vars: $baseUrl, $assetsUrl, $user, $estate, $totals, $globalLimits, $byPlan, $analytics
   Charts are hand-rolled (Canvas + SVG + CSS) — NO external library, NO CDN. */
if (!isset($baseUrl)) $baseUrl = '';
if (!isset($assetsUrl)) $assetsUrl = rtrim($baseUrl, '/') . '/assets';
$b = rtrim($baseUrl, '/');
$estate    = $estate ?? [];
$totals    = $totals ?? ['hour' => 0, 'day' => 0, 'week' => 0, 'month' => 0, 'total' => 0, 'installs' => 0];
$byPlan    = $byPlan ?? [];
$analytics = $analytics ?? ['range' => '30d', 'series' => [], 'rates' => [], 'prev' => [], 'heatmap' => ['grid' => [], 'max' => 0]];
$e = static fn ($s) => htmlspecialchars((string) $s, ENT_QUOTES);
$meter = static function (int $used, int $cap): string {
    if ($cap <= 0) return '<div class="bu-meter"><div class="bu-bar"><span style="width:0"></span></div><div class="bu-mtxt">' . $used . ' <span class="text-muted">/ ∞</span></div></div>';
    $pct  = max(0, min(100, (int) round($used / $cap * 100)));
    $tone = $pct >= 100 ? 'over' : ($pct >= 80 ? 'warn' : 'ok');
    return '<div class="bu-meter"><div class="bu-bar ' . $tone . '"><span style="width:' . $pct . '%"></span></div><div class="bu-mtxt">' . $used . ' <span class="text-muted">/ ' . $cap . '</span> · ' . max(0, $cap - $used) . ' left</div></div>';
};
?>
<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()) ?> · Build usage</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'; ?>
    <style>
        body { background: var(--pp-app-bg, #f5f6fa); color: var(--pp-text, #172033); }
        .bu-topbar { background: var(--pp-surface, #fff); border-bottom: 1px solid var(--pp-border, #e6e9f0);
            padding: .7rem 1rem; display: flex; align-items: center; gap: .8rem; position: sticky; top: 0; z-index: 1030; }
        .bu-topbar .brand { font-weight: 800; display: flex; align-items: center; gap: .4rem; color: var(--pp-text); text-decoration: none; }
        .bu-topbar .brand .bi { color: var(--pp-accent, #4f46e5); }
        .bu-wrap { max-width: 1280px; margin: 1.1rem auto 3rem; padding: 0 1.1rem; }
        .bu-hero { display: flex; align-items: flex-end; justify-content: space-between; gap: 1rem; flex-wrap: wrap; margin-bottom: 1rem; }
        .bu-hero h1 { font-size: 1.25rem; font-weight: 800; margin: 0 0 .12rem; display: flex; align-items: center; gap: .5rem; }
        .bu-hero p { margin: 0; color: var(--pp-text-muted, #64748b); font-size: .85rem; }
        /* Range + live controls */
        .bu-controls { display: flex; align-items: center; gap: .6rem; }
        .bu-seg { display: inline-flex; background: var(--pp-surface-muted, #eef1f6); border: 1px solid var(--pp-border, #e6e9f0); border-radius: 10px; padding: 2px; }
        .bu-seg button { border: 0; background: transparent; color: var(--pp-text-muted, #64748b); font-size: .8rem; font-weight: 700;
            padding: .32rem .7rem; border-radius: 8px; cursor: pointer; transition: all .12s; }
        .bu-seg button.on { background: var(--pp-surface, #fff); color: var(--pp-accent, #4f46e5); box-shadow: 0 1px 3px rgba(0,0,0,.12); }
        .bu-live { display: inline-flex; align-items: center; gap: .4rem; font-size: .78rem; color: var(--pp-text-muted, #64748b);
            border: 1px solid var(--pp-border, #e6e9f0); border-radius: 10px; padding: .3rem .6rem; cursor: pointer; background: var(--pp-surface, #fff); }
        .bu-live .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--pp-border-strong, #cbd5e1); }
        .bu-live.on .dot { background: #10b981; box-shadow: 0 0 0 3px color-mix(in srgb, #10b981 25%, transparent); animation: bu-pulse 1.8s ease-in-out infinite; }
        @keyframes bu-pulse { 50% { opacity: .5; } }
        .bu-card { background: var(--pp-surface, #fff); border: 1px solid var(--pp-border, #e6e9f0); border-radius: 14px; padding: 1.05rem 1.1rem; margin-bottom: 1rem; }
        .bu-card h2 { font-size: .96rem; font-weight: 800; margin: 0 0 .8rem; display: flex; align-items: center; gap: .45rem; }
        .bu-card h2 .sub { font-weight: 600; font-size: .76rem; color: var(--pp-text-muted, #94a3b8); margin-left: auto; }
        /* KPI tiles */
        .bu-kpis { display: grid; grid-template-columns: repeat(6, 1fr); gap: .8rem; margin-bottom: 1rem; }
        @media (max-width: 1000px) { .bu-kpis { grid-template-columns: repeat(3, 1fr); } }
        @media (max-width: 560px)  { .bu-kpis { grid-template-columns: repeat(2, 1fr); } }
        .bu-kpi { background: var(--pp-surface, #fff); border: 1px solid var(--pp-border, #e6e9f0); border-radius: 13px; padding: .8rem .9rem; position: relative; overflow: hidden; }
        .bu-kpi .l { font-size: .68rem; font-weight: 800; letter-spacing: .05em; text-transform: uppercase; color: var(--pp-text-muted, #94a3b8); }
        .bu-kpi .n { font-size: 1.55rem; font-weight: 800; line-height: 1.15; font-variant-numeric: tabular-nums; margin-top: .1rem; }
        .bu-kpi .d { font-size: .72rem; font-weight: 700; margin-top: .1rem; display: inline-flex; align-items: center; gap: .2rem; }
        .bu-kpi .d.up { color: #059669; } .bu-kpi .d.down { color: #dc2626; } .bu-kpi .d.flat { color: var(--pp-text-muted, #94a3b8); }
        .bu-kpi canvas { position: absolute; right: 8px; bottom: 8px; width: 66px; height: 26px; opacity: .8; }
        .bu-kpi.accent .n { color: var(--pp-accent, #4f46e5); }
        /* Charts */
        .bu-chartbox { position: relative; }
        #buSeries { width: 100%; height: 260px; display: block; }
        .bu-tt { position: absolute; pointer-events: none; z-index: 5; background: var(--pp-surface, #fff); border: 1px solid var(--pp-border, #e6e9f0);
            border-radius: 9px; padding: .4rem .6rem; font-size: .76rem; box-shadow: 0 8px 24px rgba(0,0,0,.16); opacity: 0; transition: opacity .1s; white-space: nowrap; }
        .bu-tt b { font-size: .82rem; }
        .bu-grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
        @media (max-width: 820px) { .bu-grid2 { grid-template-columns: 1fr; } }
        /* 3D donut */
        .bu-donut { display: flex; align-items: center; gap: 1.2rem; flex-wrap: wrap; }
        .bu-donut svg { flex: 0 0 auto; overflow: visible; }
        .bu-donut .dseg { cursor: pointer; transition: opacity .14s ease; }
        .bu-legend { display: flex; flex-direction: column; gap: .15rem; font-size: .82rem; flex: 1; min-width: 150px; }
        .bu-legend .it { display: flex; align-items: center; gap: .5rem; padding: .28rem .4rem; border-radius: 8px; transition: background .12s ease; }
        .bu-legend .it.hit { cursor: pointer; }
        .bu-legend .it.on { background: color-mix(in srgb, var(--pp-accent, #4f46e5) 12%, transparent); }
        .bu-legend .it.pinned { background: color-mix(in srgb, var(--pp-accent, #4f46e5) 18%, transparent);
            box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--pp-accent, #4f46e5) 40%, transparent); }
        .bu-legend .sw { width: 11px; height: 11px; border-radius: 4px; flex: none; box-shadow: inset 0 -2px 3px rgba(0,0,0,.18); }
        .bu-legend .v { margin-left: auto; font-weight: 700; font-variant-numeric: tabular-nums; white-space: nowrap; padding-left: .5rem; }
        .bu-ddetail { width: 100%; margin-top: .7rem; padding: .55rem .65rem; border-radius: 10px; animation: bu-fadein .2s ease;
            background: var(--pp-surface-soft, #f8fafc); border: 1px solid var(--pp-border, #e6e9f0); }
        .bu-ddetail[hidden] { display: none; }
        .bu-ddetail .dd-top { display: flex; align-items: baseline; gap: .4rem; }
        .bu-ddetail .dd-top .sw { width: 11px; height: 11px; border-radius: 4px; flex: none; align-self: center; }
        .bu-ddetail .dd-val { font-size: 1.05rem; font-weight: 800; font-variant-numeric: tabular-nums; }
        .bu-ddetail .dd-lab { font-size: .84rem; font-weight: 600; }
        .bu-ddetail .dd-pct { margin-left: auto; font-size: .8rem; font-weight: 700; color: var(--pp-accent, #4f46e5); }
        .bu-ddetail .dd-sub { font-size: .72rem; color: var(--pp-text-muted, #64748b); margin-top: .15rem; }
        .bu-ddetail .dd-bar { height: 6px; border-radius: 99px; margin-top: .45rem; overflow: hidden; background: var(--pp-surface-muted, #eef1f6); }
        .bu-ddetail .dd-bar span { display: block; height: 100%; border-radius: 99px; background: var(--pp-accent, #4f46e5); transition: width .3s ease; }
        @keyframes bu-fadein { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
        /* Interactive 3D plan bars */
        .bu-pbar { display: grid; grid-template-columns: 96px 1fr 54px; align-items: center; gap: .6rem; margin-bottom: .25rem;
            font-size: .82rem; padding: .32rem .45rem; border-radius: 10px; cursor: pointer; transition: background .12s ease; }
        .bu-pbar:hover { background: var(--pp-surface-soft, #f8fafc); }
        .bu-pbar.active { background: color-mix(in srgb, var(--pp-accent, #4f46e5) 12%, transparent);
            box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--pp-accent, #4f46e5) 32%, transparent); }
        .bu-pbar .track { height: 12px; border-radius: 99px; overflow: hidden; position: relative;
            background: var(--pp-surface-muted, #eef1f6); box-shadow: inset 0 1px 2px rgba(0,0,0,.14); }
        .bu-pbar .track > span { display: block; height: 100%; width: 0; border-radius: 99px; transition: width .6s cubic-bezier(.4,0,.2,1);
            background: linear-gradient(180deg, color-mix(in srgb, var(--pp-accent, #4f46e5) 78%, #fff) 0%, var(--pp-accent, #4f46e5) 55%,
                color-mix(in srgb, var(--pp-accent, #4f46e5) 82%, #000) 100%);
            box-shadow: 0 1px 2px rgba(0,0,0,.22), inset 0 1px 0 rgba(255,255,255,.35); }
        .bu-pbar .val { text-align: right; font-weight: 700; font-variant-numeric: tabular-nums; }
        .bu-pbar .val .pc { display: block; font-size: .64rem; font-weight: 700; color: var(--pp-text-muted, #94a3b8); }
        .bu-plan { display: inline-block; font-size: .72rem; font-weight: 700; padding: .1rem .5rem; border-radius: 99px;
            background: color-mix(in srgb, var(--pp-accent,#4f46e5) 12%, var(--pp-surface)); color: var(--pp-accent, #4f46e5); }
        .bu-planhint { font-size: .72rem; color: var(--pp-text-muted, #94a3b8); margin-top: .5rem; display: flex; align-items: center; gap: .4rem; }
        .bu-planhint button { border: 0; background: color-mix(in srgb, var(--pp-accent, #4f46e5) 12%, transparent); color: var(--pp-accent, #4f46e5);
            font-size: .68rem; font-weight: 700; padding: .12rem .5rem; border-radius: 99px; cursor: pointer; }
        /* Heatmap */
        .bu-heat { display: grid; grid-template-columns: 30px repeat(24, 1fr); gap: 2px; font-size: .6rem; }
        .bu-heat .hd { color: var(--pp-text-muted, #94a3b8); text-align: center; }
        .bu-heat .rl { color: var(--pp-text-muted, #94a3b8); display: flex; align-items: center; }
        .bu-heat .cell { aspect-ratio: 1/1; border-radius: 3px; background: var(--pp-surface-muted, #eef1f6); min-height: 13px; }
        /* ── Installations: summary chips ── */
        .bu-instsum { display: flex; flex-wrap: wrap; gap: .6rem; margin-bottom: .9rem; }
        .bu-chip { display: flex; align-items: center; gap: .55rem; padding: .5rem .8rem; border-radius: 12px; border: 1px solid var(--pp-border, #e6e9f0);
            background: var(--pp-surface-soft, #f8fafc); min-width: 118px; transition: border-color .12s, transform .12s; }
        .bu-chip:hover { transform: translateY(-1px); }
        .bu-chip .ic { width: 30px; height: 30px; border-radius: 9px; display: grid; place-items: center; font-size: .95rem; flex: none;
            background: color-mix(in srgb, var(--pp-accent, #4f46e5) 14%, transparent); color: var(--pp-accent, #4f46e5); }
        .bu-chip .tx { display: flex; flex-direction: column; line-height: 1.1; }
        .bu-chip .tx b { font-size: 1.15rem; font-weight: 800; font-variant-numeric: tabular-nums; }
        .bu-chip .tx span { font-size: .68rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; color: var(--pp-text-muted, #94a3b8); }
        .bu-chip.warn .ic { background: color-mix(in srgb, #f59e0b 18%, transparent); color: #d97706; }
        .bu-chip.over .ic { background: color-mix(in srgb, #dc2626 18%, transparent); color: #dc2626; }
        .bu-chip.ok   .ic { background: color-mix(in srgb, #16a34a 18%, transparent); color: #16a34a; }
        /* ── Tables ── */
        .bu-tablewrap { border: 1px solid var(--pp-border, #e6e9f0); border-radius: 12px; overflow: auto; max-height: 480px; }
        .bu-t { width: 100%; border-collapse: collapse; font-size: .84rem; }
        .bu-t th { text-align: left; font-size: .68rem; text-transform: uppercase; letter-spacing: .05em; color: var(--pp-text-muted, #94a3b8);
            font-weight: 800; padding: .55rem .6rem; border-bottom: 1px solid var(--pp-border, #e6e9f0);
            position: sticky; top: 0; background: var(--pp-surface-soft, #f8fafc); z-index: 2; white-space: nowrap; }
        .bu-t th:first-child { border-top-left-radius: 12px; } .bu-t th:last-child { border-top-right-radius: 12px; }
        .bu-t th.srt { cursor: pointer; user-select: none; transition: color .12s ease; }
        .bu-t th.srt:hover { color: var(--pp-accent, #4f46e5); }
        .bu-t th.srt .ar { opacity: 0; margin-left: .2rem; font-size: .8em; }
        .bu-t th.srt.asc .ar, .bu-t th.srt.desc .ar { opacity: 1; }
        .bu-t th.srt.asc .ar::before { content: '▲'; } .bu-t th.srt.desc .ar::before { content: '▼'; }
        .bu-t td { padding: .55rem .6rem; border-bottom: 1px solid var(--pp-border, #eef1f6); vertical-align: middle; }
        .bu-t tbody tr { transition: background .12s ease; }
        .bu-t tbody tr:hover { background: var(--pp-surface-soft, #f8fafc); }
        .bu-t tbody tr:last-child td { border-bottom: 0; }
        .bu-t code { font-size: .74rem; color: var(--pp-text-muted, #64748b); }
        /* Installation name cell: status dot + domain + holder + key */
        .bu-inst { display: flex; align-items: flex-start; gap: .55rem; }
        .bu-inst .st { width: 9px; height: 9px; border-radius: 50%; margin-top: .42rem; flex: none; background: var(--pp-border-strong, #cbd5e1); }
        .bu-inst .st.ok { background: #16a34a; box-shadow: 0 0 0 3px color-mix(in srgb, #16a34a 18%, transparent); }
        .bu-inst .st.warn { background: #f59e0b; box-shadow: 0 0 0 3px color-mix(in srgb, #f59e0b 18%, transparent); }
        .bu-inst .st.over { background: #dc2626; box-shadow: 0 0 0 3px color-mix(in srgb, #dc2626 20%, transparent); }
        .bu-inst .nm { min-width: 0; }
        .bu-inst .dom { font-weight: 700; word-break: break-word; }
        .bu-tnum { font-variant-numeric: tabular-nums; }
        .bu-tnum .lim { color: var(--pp-text-muted, #94a3b8); font-size: .82em; }
        .bu-norows { text-align: center; color: var(--pp-text-muted, #94a3b8); padding: 1.4rem; font-size: .84rem; }
        .bu-meter { min-width: 150px; }
        .bu-bar { height: 8px; border-radius: 99px; background: var(--pp-surface-muted, #eef1f6); overflow: hidden; box-shadow: inset 0 1px 2px rgba(0,0,0,.14); }
        .bu-bar > span { display: block; height: 100%; border-radius: 99px;
            background: linear-gradient(180deg, color-mix(in srgb, var(--pp-accent, #4f46e5) 78%, #fff), var(--pp-accent, #4f46e5) 60%, color-mix(in srgb, var(--pp-accent, #4f46e5) 82%, #000));
            box-shadow: inset 0 1px 0 rgba(255,255,255,.35); }
        .bu-bar.warn > span { background: linear-gradient(180deg, #fbbf24, #f59e0b 60%, #d97706); }
        .bu-bar.over > span { background: linear-gradient(180deg, #f87171, #dc2626 60%, #b91c1c); }
        .bu-mtxt { font-size: .72rem; margin-top: .2rem; font-variant-numeric: tabular-nums; }
        .bu-empty { text-align: center; color: var(--pp-text-muted, #94a3b8); padding: 1.6rem 1rem; }
        .bu-search { max-width: 420px; }

        /* ── Phone: the 24-hour heatmap can't shrink (cells are aspect-ratio 1/1
           with a min-height), so it overflows. Give it fixed small columns and
           let it scroll horizontally inside the card, like a contribution graph. */
        @media (max-width: 767.98px) {
            html, body { overflow-x: hidden; }
            .bu-wrap { padding: 0 .85rem; }
            .bu-heat { grid-template-columns: 26px repeat(24, 15px); overflow-x: auto; -webkit-overflow-scrolling: touch; padding-bottom: 4px; }
            .bu-search { max-width: 100%; }
        }
    </style>
</head>
<body>
<div class="bu-topbar">
    <a href="<?= $b ?>/licensing/server" class="brand"><i class="bi bi-arrow-left-short"></i> Licence server</a>
    <span style="color:var(--pp-text-muted);font-size:.85rem">/ Build usage</span>
    <div class="ms-auto d-flex align-items-center gap-2" style="margin-left:auto">
        <a href="<?= $b ?>/licensing/server#tab-installs" class="btn btn-sm btn-outline-secondary"><i class="bi bi-hdd-stack"></i> Installations</a>
        <?php include constant('BASE_PATH') . '/core/views/theme-toggle.php'; ?>
    </div>
</div>

<div class="bu-wrap">
    <div class="bu-hero">
        <div>
            <h1><i class="bi bi-graph-up-arrow"></i> Build usage &amp; analytics</h1>
            <p>Live build throughput, cache efficiency and load patterns across your licensed estate. <span id="buUpdated" class="text-muted"></span></p>
        </div>
        <div class="bu-controls">
            <div class="bu-seg" id="buRange">
                <button data-range="24h">24h</button>
                <button data-range="7d">7d</button>
                <button data-range="30d" class="on">30d</button>
                <button data-range="90d">90d</button>
            </div>
            <button class="bu-live" id="buLive" title="Auto-refresh every 20s"><span class="dot"></span> Live</button>
        </div>
    </div>

    <!-- KPI tiles (JS-populated from the range payload) -->
    <div class="bu-kpis" id="buKpis"></div>

    <!-- Primary time-series -->
    <div class="bu-card">
        <h2><i class="bi bi-activity"></i> Build volume <span class="sub" id="buSeriesSub"></span></h2>
        <div class="bu-chartbox">
            <canvas id="buSeries"></canvas>
            <div class="bu-tt" id="buTip"></div>
        </div>
    </div>

    <div class="bu-grid2">
        <!-- Full vs delta / cache donut -->
        <div class="bu-card">
            <h2><i class="bi bi-pie-chart"></i> Build mix</h2>
            <div class="bu-donut">
                <svg id="buDonut" width="130" height="130" viewBox="0 0 130 130" role="img" aria-label="Build mix"></svg>
                <div class="bu-legend" id="buDonutLegend"></div>
            </div>
            <div class="bu-ddetail" id="buDonutDetail" hidden></div>
        </div>
        <!-- By plan -->
        <div class="bu-card">
            <h2><i class="bi bi-boxes"></i> By plan <span class="sub">all-time builds</span></h2>
            <?php if (!$byPlan): ?><div class="bu-empty">No builds yet.</div><?php else:
                $planMax = 1; $planSum = 0;
                foreach ($byPlan as $p) { $planMax = max($planMax, (int) $p['total']); $planSum += (int) $p['total']; }
                $planSum = max(1, $planSum); ?>
                <div id="buPlanBars">
                <?php foreach ($byPlan as $p): $tot = (int) $p['total'];
                    $w = (int) round($tot / $planMax * 100); $pc = (int) round($tot / $planSum * 100); ?>
                <div class="bu-pbar" data-plan="<?= $e($p['plan']) ?>" data-tip="Click to filter installations by <?= $e($p['plan']) ?>" title="<?= $e($p['plan']) ?> — <?= $tot ?> builds · <?= $pc ?>% of all">
                    <span class="bu-plan"><?= $e($p['plan']) ?></span>
                    <span class="track"><span data-w="<?= $w ?>"></span></span>
                    <span class="val"><?= $tot ?><span class="pc"><?= $pc ?>%</span></span>
                </div>
                <?php endforeach; ?>
                </div>
                <div class="bu-planhint">
                    <span><?= count($byPlan) ?> plan(s) · <?= (int) $totals['installs'] ?> install(s) have built</span>
                    <button type="button" id="buPlanClear" hidden>✕ Clear plan filter</button>
                </div>
            <?php endif; ?>
        </div>
    </div>

    <!-- Load heatmap -->
    <div class="bu-card">
        <h2><i class="bi bi-calendar3-week"></i> Load by hour <span class="sub">last 30 days · builds per weekday × hour</span></h2>
        <div class="bu-heat" id="buHeat"></div>
    </div>

    <!-- Per-install detail -->
    <div class="bu-card">
        <h2><i class="bi bi-hdd-stack"></i> Installations <span class="sub">standing usage vs limit</span></h2>
        <?php if ($estate):
            // Roll up quick health counts across the estate for the summary chips.
            $sActive = $sNear = $sOver = 0;
            foreach ($estate as $i) {
                $m   = (int) ($i['build_usage']['month'] ?? 0);
                $cap = (int) ($i['build_limits']['month'] ?? 0);
                if ((int) ($i['build_usage']['day'] ?? 0) > 0) $sActive++;
                if ($cap > 0) { $pct = $m / $cap * 100; if ($pct >= 100) $sOver++; elseif ($pct >= 80) $sNear++; }
            }
        ?>
        <div class="bu-instsum">
            <div class="bu-chip"><span class="ic"><i class="bi bi-pc-display"></i></span><span class="tx"><b><?= count($estate) ?></b><span>installs</span></span></div>
            <div class="bu-chip ok"><span class="ic"><i class="bi bi-activity"></i></span><span class="tx"><b><?= $sActive ?></b><span>active today</span></span></div>
            <div class="bu-chip warn"><span class="ic"><i class="bi bi-exclamation-triangle"></i></span><span class="tx"><b><?= $sNear ?></b><span>near limit</span></span></div>
            <div class="bu-chip over"><span class="ic"><i class="bi bi-shield-exclamation"></i></span><span class="tx"><b><?= $sOver ?></b><span>over limit</span></span></div>
        </div>
        <?php endif; ?>
        <div class="position-relative mb-3 bu-search">
            <i class="bi bi-search" style="position:absolute;left:.7rem;top:50%;transform:translateY(-50%);color:var(--pp-text-muted)"></i>
            <input type="search" id="buSearch" class="form-control" style="padding-left:2.1rem" placeholder="Search domain, holder, plan or key…" autocomplete="off">
        </div>
        <?php if (!$estate): ?>
            <div class="bu-empty"><i class="bi bi-pc-display"></i><br>No installations have built yet.</div>
        <?php else: ?>
        <div class="bu-tablewrap">
        <table class="bu-t" id="buTable">
            <thead><tr>
                <th class="srt" data-key="dom" data-type="text">Installation <span class="ar"></span></th>
                <th class="srt" data-key="plan" data-type="text">Plan <span class="ar"></span></th>
                <th class="srt" data-key="hour" data-type="num">Hour <span class="ar"></span></th>
                <th class="srt" data-key="day" data-type="num">Today <span class="ar"></span></th>
                <th class="srt" data-key="week" data-type="num">Week <span class="ar"></span></th>
                <th class="srt" data-key="month" data-type="num">This month (used / limit) <span class="ar"></span></th>
                <th class="srt" data-key="total" data-type="num">Total <span class="ar"></span></th>
                <th class="srt" data-key="last" data-type="num">Last build <span class="ar"></span></th>
            </tr></thead>
            <tbody>
                <?php foreach ($estate as $i):
                    $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];
                    $domain = (string) ($i['domain'] ?? '') ?: (string) ($i['host'] ?? '') ?: '—';
                    $who = (string) ($i['holder_name'] ?? '') ?: (string) ($i['owner_email'] ?? '');
                    $lastTs = strtotime((string) ($bu['last_at'] ?? '')) ?: 0;
                    $plan = (string) ($i['plan'] ?? '—');
                    $mCap = (int) $bl['month']; $mUse = (int) $bu['month'];
                    $st = 'idle';
                    if ($mCap > 0) { $p = $mUse / $mCap * 100; $st = $p >= 100 ? 'over' : ($p >= 80 ? 'warn' : 'ok'); }
                    elseif ($mUse > 0) $st = 'ok';
                    $stTip = ['over'=>'Over the monthly limit','warn'=>'Near the monthly limit','ok'=>'Within limits','idle'=>'No builds this month'][$st];
                    $hay = strtolower($domain . ' ' . $who . ' ' . $plan . ' ' . ($i['license_key'] ?? ''));
                ?>
                <tr data-hay="<?= $e($hay) ?>" data-plan="<?= $e($plan) ?>">
                    <td data-sort="<?= $e(strtolower($domain)) ?>">
                        <div class="bu-inst">
                            <span class="st <?= $st ?>" title="<?= $e($stTip) ?>"></span>
                            <div class="nm">
                                <div class="dom"><?= $e($domain) ?></div>
                                <?php if ($who !== ''): ?><div class="small text-muted"><?= $e($who) ?></div><?php endif; ?>
                                <div><code><?= $e(substr((string) ($i['license_key'] ?? ''), 0, 14)) ?>…</code></div>
                            </div>
                        </div>
                    </td>
                    <td data-sort="<?= $e(strtolower($plan)) ?>"><span class="bu-plan"><?= $e($plan) ?></span></td>
                    <td class="bu-tnum" data-sort="<?= (int) $bu['hour'] ?>"><?= (int) $bu['hour'] ?><?php if (((int) $bl['hour']) > 0): ?> <span class="lim">/ <?= (int) $bl['hour'] ?></span><?php endif; ?></td>
                    <td class="bu-tnum" data-sort="<?= (int) $bu['day'] ?>"><?= (int) $bu['day'] ?><?php if (((int) $bl['day']) > 0): ?> <span class="lim">/ <?= (int) $bl['day'] ?></span><?php endif; ?></td>
                    <td class="bu-tnum" data-sort="<?= (int) $bu['week'] ?>"><?= (int) $bu['week'] ?><?php if (((int) $bl['week']) > 0): ?> <span class="lim">/ <?= (int) $bl['week'] ?></span><?php endif; ?></td>
                    <td data-sort="<?= (int) $bu['month'] ?>"><?= $meter($mUse, $mCap) ?></td>
                    <td class="bu-tnum" data-sort="<?= (int) $bu['total'] ?>"><strong><?= (int) $bu['total'] ?></strong></td>
                    <td class="small text-muted bu-tnum" data-sort="<?= $lastTs ?>"><?= $lastTs ? $e(date('j M Y, H:i', $lastTs)) : 'never' ?></td>
                </tr>
                <?php endforeach; ?>
                <tr id="buNoRows" style="display:none"><td colspan="8"><div class="bu-norows"><i class="bi bi-search"></i> No installations match your search or filter.</div></td></tr>
            </tbody>
        </table>
        </div>
        <?php endif; ?>
    </div>
</div>

<script>
(function () {
    var B = <?= json_encode($b) ?>;
    var BOOT = <?= json_encode($analytics, JSON_UNESCAPED_SLASHES) ?>;
    var WD = ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'];

    // Theme colours pulled live from the app's tokens (theme-aware, re-read on redraw).
    function cssVar(name, fb) { var v = getComputedStyle(document.body).getPropertyValue(name); return (v && v.trim()) || fb; }
    // Parse a hex/rgb() colour to [r,g,b]; falls back to indigo. (No color-mix() in
    // canvas/SVG — older Safari doesn't support it there, so we build rgba() ourselves.)
    function toRGB(col) {
        col = String(col || '').trim();
        var m = col.match(/^#([0-9a-f]{3})$/i);
        if (m) { var h = m[1]; return [parseInt(h[0] + h[0], 16), parseInt(h[1] + h[1], 16), parseInt(h[2] + h[2], 16)]; }
        m = col.match(/^#([0-9a-f]{6})$/i);
        if (m) { return [parseInt(m[1].slice(0, 2), 16), parseInt(m[1].slice(2, 4), 16), parseInt(m[1].slice(4, 6), 16)]; }
        m = col.match(/rgba?\(\s*(\d+)[,\s]+(\d+)[,\s]+(\d+)/i);
        if (m) { return [+m[1], +m[2], +m[3]]; }
        return [79, 70, 229];
    }
    function rgba(rgb, a) { return 'rgba(' + rgb[0] + ',' + rgb[1] + ',' + rgb[2] + ',' + a + ')'; }
    function theme() {
        var accent = cssVar('--pp-accent', '#4f46e5');
        return {
            accent: accent,
            accentRGB: toRGB(accent),
            text:   cssVar('--pp-text', '#172033'),
            muted:  cssVar('--pp-text-muted', '#94a3b8'),
            grid:   cssVar('--pp-border', '#e6e9f0'),
            surface:cssVar('--pp-surface', '#ffffff')
        };
    }
    function fmtBytes(n) { n = +n || 0; if (n < 1024) return n + ' B'; var u = ['KB','MB','GB','TB']; var i = -1; do { n /= 1024; i++; } while (n >= 1024 && i < 3); return n.toFixed(n < 10 ? 1 : 0) + ' ' + u[i]; }
    function fmtMs(n) { if (n === null || n === undefined) return '—'; n = +n; return n < 1000 ? n + ' ms' : (n / 1000).toFixed(n < 10000 ? 1 : 0) + ' s'; }
    function esc(s) { return String(s == null ? '' : s).replace(/[&<>"]/g, function (c) { return ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' })[c]; }); }
    function delta(cur, prev) {
        if (!prev) return cur > 0 ? { cls: 'up', txt: 'new' } : { cls: 'flat', txt: '—' };
        var p = Math.round((cur - prev) / prev * 100);
        if (p === 0) return { cls: 'flat', txt: '0%' };
        return { cls: p > 0 ? 'up' : 'down', txt: (p > 0 ? '▲ ' : '▼ ') + Math.abs(p) + '%' };
    }

    // ── KPI tiles ────────────────────────────────────────────────────────
    function renderKpis(a) {
        var r = a.rates || {}, pv = a.prev || {};
        var db = delta(r.builds || 0, pv.builds || 0);
        var tiles = [
            { l: 'Builds', n: (r.builds || 0).toLocaleString(), d: db, accent: true, spark: (a.series || []).map(function (p) { return p.builds; }) },
            { l: 'Cache hit', n: (r.cacheHitPct || 0) + '%', d: delta(r.cacheHitPct || 0, pv.cacheHitPct || 0) },
            { l: 'Avg build time', n: fmtMs(r.durAvg), d: null },
            { l: 'Avg size', n: fmtBytes(r.bytesAvg || 0), d: null },
            { l: 'Full / delta', n: (r.full || 0) + ' / ' + (r.delta || 0), d: null },
            { l: 'Active installs', n: (r.installs || 0), d: delta(r.installs || 0, pv.installs || 0) }
        ];
        var box = document.getElementById('buKpis');
        box.innerHTML = tiles.map(function (t, idx) {
            var d = t.d ? '<div class="d ' + t.d.cls + '">' + t.d.txt + ' <span class="text-muted" style="font-weight:600">vs prev</span></div>' : '<div class="d flat">&nbsp;</div>';
            return '<div class="bu-kpi' + (t.accent ? ' accent' : '') + '"><div class="l">' + esc(t.l) + '</div><div class="n">' + esc(t.n) + '</div>' + d
                + (t.spark ? '<canvas data-spark="' + idx + '"></canvas>' : '') + '</div>';
        }).join('');
        // Sparkline in the Builds tile.
        var sc = box.querySelector('canvas[data-spark]');
        if (sc) drawSpark(sc, tiles[0].spark);
    }
    function drawSpark(cv, data) {
        var t = theme(), dpr = window.devicePixelRatio || 1, w = 66, h = 26;
        cv.width = w * dpr; cv.height = h * dpr; var x = cv.getContext('2d'); x.scale(dpr, dpr);
        var max = Math.max.apply(null, data.concat([1])), n = data.length;
        if (!n) return;
        x.beginPath();
        data.forEach(function (v, i) { var px = i / (n - 1 || 1) * w, py = h - (v / max) * (h - 2) - 1; i ? x.lineTo(px, py) : x.moveTo(px, py); });
        x.strokeStyle = t.accent; x.lineWidth = 1.5; x.lineJoin = 'round'; x.stroke();
    }

    // ── Time-series (area + line, crosshair tooltip) ─────────────────────
    var seriesData = [];
    function drawSeries() {
        var cv = document.getElementById('buSeries'), t = theme();
        var dpr = window.devicePixelRatio || 1, W = cv.clientWidth, H = 260;
        cv.width = W * dpr; cv.height = H * dpr; var c = cv.getContext('2d'); c.setTransform(dpr, 0, 0, dpr, 0, 0);
        c.clearRect(0, 0, W, H);
        var padL = 34, padR = 8, padT = 10, padB = 22, iw = W - padL - padR, ih = H - padT - padB;
        var n = seriesData.length; if (!n) return;
        var max = Math.max.apply(null, seriesData.map(function (p) { return p.builds; }).concat([1]));
        // nice y ceil
        var step = Math.pow(10, Math.floor(Math.log10(max))); var yMax = Math.ceil(max / step) * step || 1;
        var xAt = function (i) { return padL + (n === 1 ? iw / 2 : i / (n - 1) * iw); };
        var yAt = function (v) { return padT + ih - (v / yMax) * ih; };
        // grid + y labels
        c.strokeStyle = t.grid; c.fillStyle = t.muted; c.font = '10px system-ui,sans-serif'; c.lineWidth = 1;
        for (var g = 0; g <= 4; g++) {
            var yv = yMax / 4 * g, y = yAt(yv);
            c.globalAlpha = .5; c.beginPath(); c.moveTo(padL, y); c.lineTo(W - padR, y); c.stroke(); c.globalAlpha = 1;
            c.fillText(String(Math.round(yv)), 4, y + 3);
        }
        // x labels (thinned)
        var everyX = Math.ceil(n / 8);
        seriesData.forEach(function (p, i) { if (i % everyX === 0 || i === n - 1) { c.fillText(p.label, xAt(i) - 8, H - 6); } });
        // area
        var grad = c.createLinearGradient(0, padT, 0, padT + ih);
        grad.addColorStop(0, rgba(t.accentRGB, .34));
        grad.addColorStop(1, rgba(t.accentRGB, .02));
        c.beginPath(); c.moveTo(xAt(0), yAt(0));
        seriesData.forEach(function (p, i) { c.lineTo(xAt(i), yAt(p.builds)); });
        c.lineTo(xAt(n - 1), yAt(0)); c.closePath(); c.fillStyle = grad; c.fill();
        // line
        c.beginPath(); seriesData.forEach(function (p, i) { i ? c.lineTo(xAt(i), yAt(p.builds)) : c.moveTo(xAt(i), yAt(p.builds)); });
        c.strokeStyle = t.accent; c.lineWidth = 2; c.lineJoin = 'round'; c.stroke();
        cv._geo = { xAt: xAt, yAt: yAt, padL: padL, padR: padR, W: W, H: H, ih: ih, padT: padT };
    }
    (function bindTip() {
        var cv = document.getElementById('buSeries'), tip = document.getElementById('buTip');
        cv.addEventListener('mousemove', function (ev) {
            var n = seriesData.length, g = cv._geo; if (!n || !g) return;
            var rect = cv.getBoundingClientRect(), mx = ev.clientX - rect.left;
            var i = Math.round((mx - g.padL) / ((g.W - g.padL - g.padR) || 1) * (n - 1));
            i = Math.max(0, Math.min(n - 1, i)); var p = seriesData[i];
            drawSeries();
            var c = cv.getContext('2d'), x = g.xAt(i), y = g.yAt(p.builds), t = theme();
            c.strokeStyle = t.muted; c.globalAlpha = .5; c.beginPath(); c.moveTo(x, g.padT); c.lineTo(x, g.padT + g.ih); c.stroke(); c.globalAlpha = 1;
            c.beginPath(); c.arc(x, y, 4, 0, 7); c.fillStyle = t.accent; c.fill(); c.strokeStyle = t.surface; c.lineWidth = 2; c.stroke();
            tip.innerHTML = '<b>' + p.builds + '</b> build' + (p.builds === 1 ? '' : 's') + '<br><span style="color:var(--pp-text-muted)">' + esc(p.label) + '</span>';
            tip.style.opacity = 1;
            var tx = Math.min(x + 12, g.W - 120), ty = Math.max(6, y - 40);
            tip.style.left = tx + 'px'; tip.style.top = ty + 'px';
        });
        cv.addEventListener('mouseleave', function () { tip.style.opacity = 0; drawSeries(); });
    })();

    // ── Donut (full vs delta) ────────────────────────────────────────────
    // ── 3D interactive donut (arc paths + drop-shadow depth + gloss; hover
    //    explodes a slice, dims the rest, cross-highlights the legend, swaps the
    //    centre; click pins a slice and opens a details card). No library/CDN. ──
    function renderDonut(a) {
        var r = a.rates || {}, full = r.full || 0, delta = r.delta || 0, tot = full + delta, t = theme();
        var rows = [
            { label: 'Delta', v: delta, col: rgba(t.accentRGB, 1) },
            { label: 'Full',  v: full,  col: rgba(t.accentRGB, .42) }
        ];
        var svg = document.getElementById('buDonut'), leg = document.getElementById('buDonutLegend'), det = document.getElementById('buDonutDetail');
        var cx = 65, cy = 65, ri = 38, ro = 57, TAU = Math.PI * 2;
        function pt(rr, ang) { return [cx + rr * Math.sin(ang), cy - rr * Math.cos(ang)]; }
        function arc(a0, a1, r0, r1, off) {
            if (a1 - a0 >= TAU - 1e-4) {   // full circle = degenerate arc → draw an annulus
                return 'M' + cx + ',' + (cy - r1) + 'A' + r1 + ',' + r1 + ' 0 1 1 ' + cx + ',' + (cy + r1) + 'A' + r1 + ',' + r1 + ' 0 1 1 ' + cx + ',' + (cy - r1) + 'Z'
                     + 'M' + cx + ',' + (cy - r0) + 'A' + r0 + ',' + r0 + ' 0 1 0 ' + cx + ',' + (cy + r0) + 'A' + r0 + ',' + r0 + ' 0 1 0 ' + cx + ',' + (cy - r0) + 'Z';
            }
            var mid = (a0 + a1) / 2, dx = Math.sin(mid) * off, dy = -Math.cos(mid) * off, lg = (a1 - a0) > Math.PI ? 1 : 0;
            var A = pt(r1, a0), B = pt(r1, a1), Cc = pt(r0, a1), D = pt(r0, a0);
            return 'M' + (A[0] + dx) + ',' + (A[1] + dy) + 'A' + r1 + ',' + r1 + ' 0 ' + lg + ' 1 ' + (B[0] + dx) + ',' + (B[1] + dy)
                 + 'L' + (Cc[0] + dx) + ',' + (Cc[1] + dy) + 'A' + r0 + ',' + r0 + ' 0 ' + lg + ' 0 ' + (D[0] + dx) + ',' + (D[1] + dy) + 'Z';
        }
        var defs = '<defs><filter id="buDonut-sh" x="-40%" y="-40%" width="180%" height="180%">'
            + '<feDropShadow dx="0" dy="2.5" stdDeviation="2.6" flood-color="rgba(15,23,42,0.30)"/></filter>'
            + '<radialGradient id="buDonut-gl" cx="40%" cy="32%" r="72%">'
            + '<stop offset="0%" stop-color="rgba(255,255,255,0.55)"/><stop offset="42%" stop-color="rgba(255,255,255,0.12)"/>'
            + '<stop offset="100%" stop-color="rgba(255,255,255,0)"/></radialGradient></defs>';
        var seg = [], ang = 0, paths = '';
        if (tot <= 0) {
            paths = '<circle cx="65" cy="65" r="' + ((ri + ro) / 2) + '" fill="none" stroke="' + rgba(t.accentRGB, .12) + '" stroke-width="' + (ro - ri) + '"/>';
        } else rows.forEach(function (s, i) {
            var a0 = ang, a1 = ang + s.v / tot * TAU; ang = a1; seg.push([a0, a1]);
            paths += '<path class="dseg" data-i="' + i + '" fill-rule="evenodd" d="' + arc(a0, a1, ri, ro, 0) + '" fill="' + s.col + '">'
                + '<title>' + s.label + ': ' + s.v + ' · ' + Math.round(s.v / tot * 100) + '%</title></path>';
        });
        var gloss = '<circle cx="65" cy="65" r="' + ((ri + ro) / 2) + '" fill="none" stroke="url(#buDonut-gl)" stroke-width="' + (ro - ri) + '" pointer-events="none"/>';
        var center = '<text class="dtot" x="65" y="61" text-anchor="middle" font-size="21" font-weight="800" fill="' + t.text + '">' + tot + '</text>'
            + '<text class="dsub" x="65" y="78" text-anchor="middle" font-size="9" fill="' + t.muted + '">builds</text>';
        svg.innerHTML = defs + '<g filter="url(#buDonut-sh)">' + paths + '</g>' + gloss + center;
        leg.innerHTML = rows.map(function (s, i) { var pct = tot > 0 ? Math.round(s.v / tot * 100) : 0;
            return '<div class="it hit" data-i="' + i + '"><span class="sw" style="background:' + s.col + '"></span>' + s.label
                + '<span class="v">' + s.v + ' · ' + pct + '%</span></div>'; }).join('')
            + '<div class="it" style="color:var(--pp-text-muted)"><span class="sw" style="background:transparent;box-shadow:none"></span>Cache hit<span class="v">' + (r.cacheHitPct || 0) + '%</span></div>';
        // Interactivity
        var segEls = svg.querySelectorAll('.dseg'), itEls = leg.querySelectorAll('.it.hit');
        var totEl = svg.querySelector('.dtot'), subEl = svg.querySelector('.dsub');
        if (det) det.hidden = true;
        var pinned = null;
        function paint(i) {
            segEls.forEach(function (el) { var k = +el.getAttribute('data-i');
                el.setAttribute('d', arc(seg[k][0], seg[k][1], ri, ro, i === null ? 0 : (k === i ? 5 : 0)));
                el.style.opacity = (i === null || k === i) ? 1 : .38; });
            itEls.forEach(function (el) { var k = +el.getAttribute('data-i');
                el.classList.toggle('on', k === i); el.classList.toggle('pinned', k === pinned); });
            if (i === null || tot <= 0) { totEl.textContent = tot; subEl.textContent = 'builds'; }
            else { totEl.textContent = rows[i].v; subEl.textContent = Math.round(rows[i].v / tot * 100) + '%'; }
        }
        function detail(i) {
            if (!det) return;
            if (i === null || tot <= 0) { det.hidden = true; return; }
            var pct = Math.round(rows[i].v / tot * 100);
            det.innerHTML = '<div class="dd-top"><span class="sw" style="background:' + rows[i].col + '"></span>'
                + '<span class="dd-val">' + rows[i].v + '</span><span class="dd-lab">' + rows[i].label + ' builds</span>'
                + '<span class="dd-pct">' + pct + '%</span></div>'
                + '<div class="dd-sub">' + rows[i].v + ' of ' + tot + ' builds' + (rows[i].label === 'Delta' ? ' · incremental (cache-assisted)' : ' · full rebuild') + (pinned === i ? ' · pinned' : '') + '</div>'
                + '<div class="dd-bar"><span style="width:' + pct + '%"></span></div>';
            det.hidden = false;
        }
        function show(i) { var a2 = (i === null ? pinned : i); paint(a2); detail(a2); }
        function pick(k) { pinned = (pinned === k) ? null : k; show(null); }
        segEls.forEach(function (el) { var k = +el.getAttribute('data-i');
            el.addEventListener('mouseenter', function () { show(k); });
            el.addEventListener('mouseleave', function () { show(null); });
            el.addEventListener('click', function () { pick(k); }); });
        itEls.forEach(function (el) { var k = +el.getAttribute('data-i'); el.style.cursor = 'pointer';
            el.addEventListener('mouseenter', function () { show(k); });
            el.addEventListener('mouseleave', function () { show(null); });
            el.addEventListener('click', function () { pick(k); }); });
    }

    // ── Heatmap ──────────────────────────────────────────────────────────
    function renderHeat(a) {
        var hm = a.heatmap || { grid: [], max: 0 }, max = hm.max || 0, t = theme();
        var el = document.getElementById('buHeat'), html = '<div class="hd"></div>';
        for (var h = 0; h < 24; h++) html += '<div class="hd">' + (h % 3 === 0 ? h : '') + '</div>';
        for (var d = 0; d < 7; d++) {
            html += '<div class="rl">' + WD[d] + '</div>';
            for (var hr = 0; hr < 24; hr++) {
                var v = (hm.grid[d] && hm.grid[d][hr]) || 0;
                var alpha = max > 0 && v > 0 ? (0.15 + 0.85 * (v / max)) : 0;
                var bg = alpha > 0 ? rgba(t.accentRGB, alpha.toFixed(2)) : '';
                html += '<div class="cell" style="' + (bg ? 'background:' + bg : '') + '" title="' + WD[d] + ' ' + hr + ':00 — ' + v + ' build(s)"></div>';
            }
        }
        el.innerHTML = html;
    }

    // ── Wire it up ───────────────────────────────────────────────────────
    var lastPayload = BOOT;
    function apply(a) {
        lastPayload = a;
        seriesData = a.series || [];
        document.getElementById('buSeriesSub').textContent = ({ '24h': 'last 24 hours', '7d': 'last 7 days', '30d': 'last 30 days', '90d': 'last 90 days' })[a.range] || '';
        renderKpis(a); drawSeries(); renderDonut(a); renderHeat(a);
        var now = new Date();
        document.getElementById('buUpdated').textContent = 'Updated ' + now.toLocaleTimeString();
    }
    // Re-render the JS-coloured charts when the theme changes (canvas/SVG read tokens).
    (function () {
        var obs = new MutationObserver(function () { if (lastPayload) apply(lastPayload); });
        obs.observe(document.documentElement, { attributes: true, attributeFilter: ['data-pp-theme', 'class'] });
        obs.observe(document.body, { attributes: true, attributeFilter: ['data-pp-theme', 'class'] });
    })();
    var current = BOOT.range || '30d';
    function load(range) {
        current = range;
        fetch(B + '/licensing/build-usage/data?range=' + encodeURIComponent(range), { credentials: 'same-origin' })
            .then(function (r) { return r.json(); })
            .then(function (a) { if (a && a.ok) apply(a); })
            .catch(function () {});
    }
    document.getElementById('buRange').addEventListener('click', function (e) {
        var btn = e.target.closest('button[data-range]'); if (!btn) return;
        this.querySelectorAll('button').forEach(function (b) { b.classList.remove('on'); });
        btn.classList.add('on'); load(btn.dataset.range);
    });
    // Live auto-refresh.
    var liveTimer = null, liveBtn = document.getElementById('buLive');
    liveBtn.addEventListener('click', function () {
        liveBtn.classList.toggle('on');
        if (liveBtn.classList.contains('on')) { liveTimer = setInterval(function () { load(current); }, 20000); }
        else { clearInterval(liveTimer); liveTimer = null; }
    });
    window.addEventListener('resize', function () { drawSeries(); });

    // ── Installations table: search + plan filter + column sort ──────────────
    var s = document.getElementById('buSearch'), tbl = document.getElementById('buTable');
    var planFilter = '';
    // Data rows only — never the "no matches" placeholder row.
    function dataRows() { return tbl ? Array.prototype.slice.call(tbl.querySelectorAll('tbody tr[data-hay]')) : []; }
    function applyFilter() {
        if (!tbl) return;
        var q = (s ? s.value : '').toLowerCase().trim(), shown = 0;
        dataRows().forEach(function (r) {
            var okQ = !q || (r.getAttribute('data-hay') || '').indexOf(q) !== -1;
            var okP = !planFilter || (r.getAttribute('data-plan') || '') === planFilter;
            var vis = okQ && okP; r.style.display = vis ? '' : 'none'; if (vis) shown++;
        });
        var nr = document.getElementById('buNoRows'); if (nr) nr.style.display = shown ? 'none' : '';
    }
    if (s) s.addEventListener('input', applyFilter);

    // Sortable columns (the placeholder row is excluded and always kept last).
    if (tbl) {
        var tbody = tbl.querySelector('tbody');
        tbl.querySelectorAll('th.srt').forEach(function (th) {
            th.addEventListener('click', function () {
                var idx = Array.prototype.indexOf.call(th.parentNode.children, th);
                var numeric = th.getAttribute('data-type') === 'num';
                var asc = !th.classList.contains('asc');   // toggle; default first click = asc (num→ still asc unless already asc)
                tbl.querySelectorAll('th.srt').forEach(function (o) { o.classList.remove('asc', 'desc'); });
                th.classList.add(asc ? 'asc' : 'desc');
                var rows = dataRows();
                rows.sort(function (a, b) {
                    var va = a.children[idx].getAttribute('data-sort') || '', vb = b.children[idx].getAttribute('data-sort') || '';
                    var c = numeric ? ((+va || 0) - (+vb || 0)) : va.localeCompare(vb);
                    return asc ? c : -c;
                });
                var nr = document.getElementById('buNoRows');
                rows.forEach(function (r) { tbody.insertBefore(r, nr || null); });   // keep placeholder at the end
            });
        });
    }

    // ── "By plan" bars: animate widths in + click to filter the table ─────────
    var planBars = document.getElementById('buPlanBars'), planClear = document.getElementById('buPlanClear');
    if (planBars) {
        // Animate the fill widths on load (CSS transitions from width:0).
        requestAnimationFrame(function () {
            planBars.querySelectorAll('.bu-pbar .track > span').forEach(function (sp) { sp.style.width = (sp.getAttribute('data-w') || 0) + '%'; });
        });
        function setPlan(p) {
            planFilter = p;
            planBars.querySelectorAll('.bu-pbar').forEach(function (b) { b.classList.toggle('active', !!p && b.getAttribute('data-plan') === p); });
            if (planClear) { planClear.hidden = !p; planClear.textContent = p ? ('✕ Clear filter: ' + p) : ''; }
            applyFilter();
            if (p && tbl) tbl.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
        }
        planBars.querySelectorAll('.bu-pbar').forEach(function (b) {
            b.addEventListener('click', function () {
                var p = b.getAttribute('data-plan');
                setPlan(planFilter === p ? '' : p);
            });
        });
        if (planClear) planClear.addEventListener('click', function () { setPlan(''); });
    }

    apply(BOOT);
})();
</script>
</body>
</html>
