<?php
/**
 * AI Monitor - Customer Dashboard
 * Shows AI monitoring subscriptions, issues, on-demand reviews, and notification settings.
 * Accessible to all authenticated users (not just root).
 */

if (!defined('AUTH_USER')) {
    header('Location: login.php');
    exit();
}

// The Traffic panel JS has been iterated on a few times; without cache
// hints, browsers were serving stale HTML against fresh static assets.
// no-cache (revalidate every load) is the cheap right answer for an
// authenticated-only dashboard page.
header('Cache-Control: no-cache, must-revalidate');
header('Pragma: no-cache');

$username = AUTH_USER;
$is_root = (AUTH_USER === 'root');

// Load subscriptions server-side for initial state
$subscriptions = [];
$has_subscriptions = false;

try {
    require_once('/docker/whp/web/libs/mysqlmgmt.php');
    $mysql = new mysqlmgmt();
    $db = $mysql->getMySQLConnection();
    if (!$db) {
        throw new Exception('Database connection failed');
    }
    $db->exec("USE whp");

    // sub.* includes the new health cache columns (http/ssl/dns/whois status)
    // added by migration 005_subscription_status_cache.sql
    $query = "
        SELECT sub.*, s.site_name, sub.username AS owner,
               COALESCE(sd.fqdn, d.domain_name) AS primary_domain,
               (SELECT COUNT(*) FROM whp.ai_monitor_issues i WHERE i.subscription_id = sub.id AND i.status = 'open') AS open_issues,
               (SELECT COUNT(*) FROM whp.ai_monitor_issues i WHERE i.subscription_id = sub.id AND i.status = 'open' AND i.severity = 'critical') AS critical_count,
               (SELECT COUNT(*) FROM whp.ai_monitor_issues i WHERE i.subscription_id = sub.id AND i.status = 'open' AND i.severity = 'warning') AS warning_count,
               (SELECT COUNT(*) FROM whp.ai_monitor_issues i WHERE i.subscription_id = sub.id AND i.status = 'open' AND i.severity = 'info') AS info_count
        FROM whp.ai_monitor_subscriptions sub
        JOIN whp.sites s ON sub.site_id = s.id
        JOIN whp.domains d ON s.primary_domain_id = d.id
        LEFT JOIN whp.site_domains sd ON s.id = sd.site_id AND sd.is_primary = 1
        WHERE sub.status = 'active'
    ";

    if ($is_root) {
        // Root sees all subscriptions
        $query .= " ORDER BY sub.username ASC, s.site_name ASC";
        $stmt = $db->query($query);
    } else {
        $query .= " AND sub.username = ? ORDER BY s.site_name ASC";
        $stmt = $db->prepare($query);
        $stmt->execute([$username]);
    }
    $subscriptions = $stmt->fetchAll(PDO::FETCH_ASSOC);
    $has_subscriptions = count($subscriptions) > 0;
} catch (Exception $e) {
    error_log('AI Monitor Customer: Database error: ' . $e->getMessage());
}

$csrf_token = $_SESSION['csrf_token'] ?? '';

/**
 * Render one row of the Site Health strip. The right side shows whatever
 * is most useful for the check:
 *   SSL / Domain — the days-or-date until expiry (e.g. "89d" / "Mar 14, 2026")
 *   HTTP / DNS  — a one-word status (OK / Fail / CDN / Mismatch ...)
 * Color comes from the status; the value uses a darker, larger font so
 * dates stay legible.
 */
function whp_health_indicator(string $label, string $status, string $value, string $kind = ''): string {
    // Color values are stable between themes. The previous run used
    // *-text-emphasis variables, but the dark-mode "ok" rendered too dark
    // in our theme — the original #198754 reads better in both modes per
    // user feedback. Label colors are theme-switched via the
    // .whp-health-label CSS rules added once at the top of the page.
    $palette = [
        'ok'          => '#198754',
        'expiring'    => '#fd7e14',
        'expired'     => '#dc3545',
        'fail'        => '#dc3545',
        'mismatch'    => '#dc3545',
        'nxdomain'    => '#dc3545',
        'unreachable' => '#dc3545',
        'cdn'         => '#0d6efd',
        'unparsed'    => '#6c757d',
        'skipped'     => '#6c757d',
        'unknown'     => '#6c757d',
    ];
    $color = $palette[$status] ?? '#6c757d';
    if ($value === '') $value = ucfirst($status);

    // The data-health-kind attribute (http|ssl|dns|whois) lets the
    // dashboard JS find and update each row in place when an async
    // recheck returns. Empty kind keeps backwards-compat for any
    // unrelated caller.
    $kindAttr = $kind !== '' ? ' data-health-kind="' . htmlspecialchars($kind) . '"' : '';
    return '<div class="d-flex align-items-center gap-2 mb-1 whp-health-row" style="line-height: 1.3; font-size: 0.85rem;"' . $kindAttr . '>'
        . '<span class="whp-health-dot" style="display:inline-block;width:9px;height:9px;border-radius:50%;background:' . $color . ';flex-shrink:0;" aria-hidden="true"></span>'
        . '<span class="whp-health-label" style="flex-shrink:0;">' . htmlspecialchars($label) . '</span>'
        . '<span class="ms-auto text-truncate whp-health-value" title="' . htmlspecialchars($value) . '" '
        . 'style="color: ' . $color . '; font-weight: 600;">'
        . htmlspecialchars($value)
        . '</span>'
        . '</div>';
}

/**
 * Format a "days remaining" / "Mar 14, 2026" string from an expiry
 * timestamp. Returns the raw status word ("Expired", "Pending", ...) when
 * we have no usable date.
 */
function whp_health_expiry_value(?string $expiresAt, string $status): string {
    static $statusLabels = [
        'unknown'  => 'Pending',
        'skipped'  => 'Skipped',
        'unparsed' => 'Unparsed',
        'expired'  => 'Expired',
    ];
    if (!$expiresAt) {
        return $statusLabels[$status] ?? ucfirst($status);
    }
    $ts = strtotime($expiresAt);
    if ($ts === false) {
        return $statusLabels[$status] ?? ucfirst($status);
    }
    $days = (int) floor(($ts - time()) / 86400);
    if ($days < 0)  return abs($days) . 'd ago';
    if ($days === 0) return 'today';
    if ($days <= 90) return $days . ' days';
    return date('M j, Y', $ts);
}
?>

<style>
    /* Site Health labels (HTTP/SSL/DNS/Domain): pure black in light mode,
       pure white in dark mode. Bootstrap's --bs-body-color was rendering
       too muted in our custom theme overlay, so we set the colors
       explicitly here. */
    .whp-health-label { color: #1a1a1a; font-weight: 500; }
    [data-theme="dark"] .whp-health-label { color: #ffffff; }
    .whp-health-section-title { color: #1a1a1a; opacity: 0.75; }
    [data-theme="dark"] .whp-health-section-title { color: #ffffff; opacity: 0.85; }

    /* Bootstrap's .row uses `display: flex`, which beats the [hidden]
       HTML attribute. Force-hide section wrappers via !important. */
    .whp-domain-section[hidden] { display: none !important; }

    /* The Issues + Review History tables use plain `.table` markup, which
       inherits Bootstrap's --bs-table-bg=white default. The project's
       theme system uses --whp-* tokens (see bootstrap5-whp-theme.css),
       NOT --bs-* tokens — so var(--bs-body-color) etc don't actually
       theme-flip here. Override --bs-table-* via the project's --whp-*
       tokens, which ARE set per-theme. */
    [data-theme="dark"] #section-issues .table,
    [data-theme="dark"] #section-log-review .table,
    [data-theme="dark"] #section-suppressions .table {
        --bs-table-bg: var(--whp-card-bg);
        --bs-table-color: var(--whp-content-text);
        --bs-table-border-color: var(--whp-table-border);
        --bs-table-striped-bg: var(--whp-table-stripe);
        --bs-table-hover-bg: rgba(255,255,255,0.07);
        color: var(--whp-content-text);
    }
    [data-theme="dark"] #section-issues .table thead th,
    [data-theme="dark"] #section-log-review .table thead th,
    [data-theme="dark"] #section-suppressions .table thead th {
        background-color: var(--whp-card-header-bg);
        color: var(--whp-content-text);
        border-color: var(--whp-table-border);
    }
    [data-theme="dark"] #section-issues .table tbody td,
    [data-theme="dark"] #section-log-review .table tbody td,
    [data-theme="dark"] #section-suppressions .table tbody td {
        background-color: var(--whp-card-bg);
        color: var(--whp-content-text);
        border-color: var(--whp-table-border);
    }
    [data-theme="dark"] #section-issues .table tbody tr:hover td,
    [data-theme="dark"] #section-log-review .table tbody tr:hover td,
    [data-theme="dark"] #section-suppressions .table tbody tr:hover td {
        background-color: rgba(255,255,255,0.07);
    }
    /* The empty/loading rows use `.text-center py-4` which Bootstrap
       paints with --bs-secondary-color (light gray) — keep that, but
       make sure the cell background is the dark surface, not white. */
    [data-theme="dark"] #section-issues .table .text-muted,
    [data-theme="dark"] #section-log-review .table .text-muted,
    [data-theme="dark"] #section-suppressions .table .text-muted {
        color: var(--whp-text-muted) !important;
    }

    /* ============================================================
       Domain picker (custom searchable dropdown).
       Bootstrap 5 has no native combobox; this is a self-contained
       implementation using --bs-* tokens so the existing data-theme
       overlay handles light/dark for free.
       ============================================================ */
    .whp-domain-picker {
        position: relative;
        max-width: 640px;
    }
    .whp-domain-picker .whp-picker-input-wrap {
        position: relative;
    }
    .whp-domain-picker .whp-picker-input-wrap .fa {
        position: absolute;
        left: 12px;
        top: 50%;
        transform: translateY(-50%);
        color: var(--bs-secondary-color, #6c757d);
        pointer-events: none;
    }
    .whp-domain-picker .whp-picker-input {
        padding-left: 36px;
        padding-right: 36px;
    }
    .whp-domain-picker .whp-picker-clear {
        position: absolute;
        right: 8px;
        top: 50%;
        transform: translateY(-50%);
        background: transparent;
        border: 0;
        color: var(--bs-secondary-color, #6c757d);
        cursor: pointer;
        padding: 4px 8px;
        line-height: 1;
        font-size: 0.95rem;
    }
    .whp-domain-picker .whp-picker-clear:hover {
        color: var(--bs-body-color, inherit);
    }
    .whp-domain-picker .whp-picker-listbox {
        position: absolute;
        top: calc(100% + 4px);
        left: 0;
        right: 0;
        z-index: 1050;
        background: var(--bs-body-bg, #fff);
        color: var(--bs-body-color, inherit);
        border: 1px solid var(--bs-border-color, rgba(0,0,0,0.15));
        border-radius: var(--bs-border-radius, 0.375rem);
        box-shadow: 0 4px 12px rgba(0,0,0,0.12);
        max-height: 320px;
        overflow-y: auto;
        padding: 4px 0;
        margin: 0;
        list-style: none;
    }
    .whp-domain-picker .whp-picker-listbox[hidden] { display: none; }
    .whp-domain-picker .whp-picker-option {
        padding: 8px 12px;
        cursor: pointer;
        display: flex;
        flex-direction: column;
        gap: 2px;
        line-height: 1.3;
    }
    .whp-domain-picker .whp-picker-option:hover,
    .whp-domain-picker .whp-picker-option.is-active {
        background: var(--bs-tertiary-bg, rgba(13, 110, 253, 0.08));
    }
    .whp-domain-picker .whp-picker-option.is-selected {
        background: var(--bs-primary-bg-subtle, rgba(13, 110, 253, 0.15));
    }
    .whp-domain-picker .whp-picker-option-domain {
        font-weight: 600;
        color: var(--bs-body-color, inherit);
    }
    .whp-domain-picker .whp-picker-option-meta {
        font-size: 0.8rem;
        color: var(--bs-secondary-color, #6c757d);
    }
    .whp-domain-picker .whp-picker-empty {
        padding: 10px 12px;
        color: var(--bs-secondary-color, #6c757d);
        font-style: italic;
    }
    /* Theme-aware listbox background — Bootstrap 5.3 sets --bs-body-bg
       per-theme via [data-bs-theme]; our overlay uses [data-theme="dark"]
       so we map both. */
    [data-theme="dark"] .whp-domain-picker .whp-picker-listbox {
        background: var(--bs-body-bg, #1f2937);
        border-color: var(--bs-border-color, rgba(255,255,255,0.15));
    }
</style>
<?php if (!$has_subscriptions): ?>
<!-- No Subscriptions Message -->
<div class="row">
    <div class="col-12">
        <div class="card">
            <div class="card-header">
                <h5 class="card-title mb-0"><i class="fa fa-shield"></i> AI Monitoring</h5>
            </div>
            <div class="card-body">
                <div class="text-center py-5">
                    <i class="fa fa-shield fa-3x text-muted mb-3 d-block"></i>
                    <?php if ($is_root): ?>
                    <h5 class="text-muted">No active AI monitoring subscriptions.</h5>
                    <p class="text-muted">Enable monitoring for sites from the <a href="index.php?page=ai-monitor">AI Monitor admin page</a>.</p>
                    <?php else: ?>
                    <h5 class="text-muted">AI monitoring is not enabled for any of your sites.</h5>
                    <p class="text-muted">Contact your hosting provider to enable this feature.</p>
                    <?php endif; ?>
                </div>
            </div>
        </div>
    </div>
</div>
<?php return; endif; ?>

<!-- Domain Picker (single-domain dashboard mode) -->
<div class="row g-4 mb-3">
    <div class="col-12">
        <div class="card">
            <div class="card-body">
                <label for="whp-domain-picker-input" class="form-label fw-semibold mb-2">
                    <i class="fa fa-globe"></i> Select a domain
                </label>
                <div class="whp-domain-picker" id="whp-domain-picker">
                    <div class="whp-picker-input-wrap">
                        <i class="fa fa-search" aria-hidden="true"></i>
                        <input type="text"
                               id="whp-domain-picker-input"
                               class="form-control whp-picker-input"
                               role="combobox"
                               autocomplete="off"
                               spellcheck="false"
                               placeholder="Type to search by domain<?php echo $is_root ? ', site name, or owner' : ' or site name'; ?>..."
                               aria-controls="whp-domain-picker-listbox"
                               aria-autocomplete="list"
                               aria-expanded="false"
                               aria-haspopup="listbox">
                        <button type="button"
                                class="whp-picker-clear"
                                id="whp-domain-picker-clear"
                                aria-label="Clear selection"
                                hidden>&times;</button>
                    </div>
                    <ul class="whp-picker-listbox"
                        id="whp-domain-picker-listbox"
                        role="listbox"
                        aria-label="Domain subscriptions"
                        hidden>
                        <?php foreach ($subscriptions as $idx => $sub):
                            $optionId = 'whp-domain-option-' . intval($sub['id']);
                            $domain   = (string) ($sub['primary_domain'] ?? '');
                            $siteName = (string) ($sub['site_name'] ?? '');
                            $owner    = (string) ($sub['owner'] ?? '');
                            // Search haystack mirrors what the JS filter checks against.
                            $haystackParts = [strtolower($domain), strtolower($siteName)];
                            if ($is_root && $owner !== '') $haystackParts[] = strtolower($owner);
                            $haystack = trim(implode(' ', array_filter($haystackParts)));
                        ?>
                        <li class="whp-picker-option"
                            id="<?php echo $optionId; ?>"
                            role="option"
                            aria-selected="false"
                            data-subscription-id="<?php echo intval($sub['id']); ?>"
                            data-site-id="<?php echo intval($sub['site_id']); ?>"
                            data-domain="<?php echo htmlspecialchars($domain); ?>"
                            data-site-name="<?php echo htmlspecialchars($siteName); ?>"
                            data-owner="<?php echo htmlspecialchars($owner); ?>"
                            data-search="<?php echo htmlspecialchars($haystack); ?>">
                            <span class="whp-picker-option-domain"><?php echo htmlspecialchars($domain); ?></span>
                            <span class="whp-picker-option-meta">
                                <?php echo htmlspecialchars($siteName); ?>
                                <?php if ($is_root && $owner !== ''): ?>
                                &middot; <?php echo htmlspecialchars($owner); ?>
                                <?php endif; ?>
                            </span>
                        </li>
                        <?php endforeach; ?>
                        <li class="whp-picker-empty" id="whp-domain-picker-empty" hidden>
                            No matching domains.
                        </li>
                    </ul>
                </div>
                <p class="text-muted small mt-2 mb-0" id="whp-domain-picker-hint">
                    Select a domain to view its monitoring data.
                </p>
            </div>
        </div>
    </div>
</div>

<!-- Section 1: Subscription Overview -->
<div class="row g-4 mb-4 whp-domain-section" id="section-overview" data-section="overview" hidden>
    <div class="col-12">
        <div class="card">
            <div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
                <h5 class="card-title mb-0"><i class="fa fa-shield"></i> AI Monitoring Overview</h5>
                <div class="d-flex align-items-center gap-2 flex-wrap">
                    <input id="overview-card-search"
                           type="search"
                           class="form-control form-control-sm"
                           placeholder="Search domain or site name..."
                           style="width: 240px;"
                           autocomplete="off">
                    <span id="overview-card-count" class="text-muted small"></span>
                </div>
            </div>
            <div class="card-body">
                <div class="row g-3" id="overview-cards-container" data-page-size="5">
                    <?php foreach ($subscriptions as $sub):
                        // Lowercased haystack for the search filter — domain and
                        // site name are the only fields the customer recognizes.
                        $cardSearchText = strtolower(($sub['primary_domain'] ?? '') . ' ' . ($sub['site_name'] ?? '') . ' ' . ($sub['owner'] ?? ''));
                    ?>
                    <div class="col-md-6 col-lg-4 col-xl-3 overview-card-wrapper"
                         data-search-text="<?php echo htmlspecialchars($cardSearchText, ENT_QUOTES); ?>">
                        <div class="card h-100 border subscription-card" data-site-id="<?php echo intval($sub['site_id']); ?>" data-subscription-id="<?php echo intval($sub['id']); ?>">
                            <div class="card-body">
                                <div class="d-flex justify-content-between align-items-start mb-2">
                                    <h6 class="card-title mb-0 text-truncate" title="<?php echo htmlspecialchars($sub['primary_domain']); ?>">
                                        <?php echo htmlspecialchars($sub['primary_domain']); ?>
                                    </h6>
                                    <span class="badge bg-<?php echo $sub['status'] === 'active' ? 'success' : 'warning'; ?> ms-2">
                                        <?php echo ucfirst(htmlspecialchars($sub['status'])); ?>
                                    </span>
                                </div>
                                <p class="text-muted small mb-2">
                                    <?php echo htmlspecialchars($sub['site_name']); ?>
                                    <?php if ($is_root && !empty($sub['owner'])): ?>
                                    <span class="badge bg-secondary ms-1" title="Owner"><?php echo htmlspecialchars($sub['owner']); ?></span>
                                    <?php endif; ?>
                                </p>
                                <div class="d-flex gap-2 flex-wrap">
                                    <?php if ($sub['critical_count'] > 0): ?>
                                    <span class="badge bg-danger" title="Critical issues">
                                        <i class="fa fa-exclamation-circle"></i> <?php echo intval($sub['critical_count']); ?>
                                    </span>
                                    <?php endif; ?>
                                    <?php if ($sub['warning_count'] > 0): ?>
                                    <span class="badge bg-warning text-dark" title="Warnings">
                                        <i class="fa fa-exclamation-triangle"></i> <?php echo intval($sub['warning_count']); ?>
                                    </span>
                                    <?php endif; ?>
                                    <?php if ($sub['info_count'] > 0): ?>
                                    <span class="badge bg-info" title="Info">
                                        <i class="fa fa-info-circle"></i> <?php echo intval($sub['info_count']); ?>
                                    </span>
                                    <?php endif; ?>
                                    <?php if ($sub['open_issues'] == 0): ?>
                                    <span class="badge bg-secondary">No open issues</span>
                                    <?php endif; ?>
                                </div>

                                <?php
                                // Site health strip — reads cache columns populated by
                                // scripts/scheduled-site-checks.php (--daily / --initial).
                                $http_state = (string) ($sub['http_status'] ?? 'unknown');
                                $http_value_map = [
                                    'ok'          => 'OK',
                                    'fail'        => 'Failing',
                                    'unreachable' => 'Unreachable',
                                    'unknown'     => 'Pending',
                                ];
                                $http_value = $http_value_map[$http_state] ?? ucfirst($http_state);

                                $ssl_state = (string) ($sub['ssl_status'] ?? 'unknown');
                                $ssl_value = whp_health_expiry_value($sub['ssl_expires_at'] ?? null, $ssl_state);

                                $dns_state = (string) ($sub['dns_status'] ?? 'unknown');
                                $dns_value_map = [
                                    'ok'       => 'OK',
                                    'cdn'      => 'Behind CDN',
                                    'mismatch' => 'Mismatch',
                                    'nxdomain' => 'No DNS',
                                    'skipped'  => 'Skipped',
                                    'unknown'  => 'Pending',
                                ];
                                $dns_value = $dns_value_map[$dns_state] ?? ucfirst($dns_state);

                                $whois_state = (string) ($sub['whois_status'] ?? 'unknown');
                                $whois_value = whp_health_expiry_value($sub['whois_expires_at'] ?? null, $whois_state);
                                ?>
                                <div class="mt-2 pt-2" style="border-top: 1px solid var(--bs-border-color, rgba(128,128,128,0.2));">
                                    <div class="d-flex align-items-center justify-content-between mb-2">
                                        <div class="whp-health-section-title text-uppercase fw-semibold" style="font-size: 0.7rem; letter-spacing: 0.5px;">Site Health</div>
                                        <span class="whp-health-refresh-indicator" data-state="idle" style="font-size: 0.7rem; color: #6c757d;" aria-live="polite"></span>
                                    </div>
                                    <?php echo whp_health_indicator('HTTP',   $http_state,  $http_value,  'http'); ?>
                                    <?php echo whp_health_indicator('SSL',    $ssl_state,   $ssl_value,   'ssl'); ?>
                                    <?php echo whp_health_indicator('DNS',    $dns_state,   $dns_value,   'dns'); ?>
                                    <?php echo whp_health_indicator('Domain', $whois_state, $whois_value, 'whois'); ?>
                                </div>
                            </div>
                        </div>
                    </div>
                    <?php endforeach; ?>
                </div>
                <nav id="overview-pagination" class="mt-3 d-none" aria-label="Subscription cards pagination">
                    <ul class="pagination pagination-sm mb-0 justify-content-center"></ul>
                </nav>
                <div id="overview-cards-empty" class="text-muted text-center py-3 d-none">
                    No subscriptions match that search.
                </div>
            </div>
        </div>
    </div>
</div>

<!-- Section 2: Issues List -->
<div class="row g-4 mb-4 whp-domain-section" id="section-issues" data-section="issues" hidden>
    <div class="col-12">
        <div class="card">
            <div class="card-header d-flex justify-content-between align-items-center flex-wrap">
                <h5 class="card-title mb-0"><i class="fa fa-list"></i> Issues</h5>
                <div class="d-flex gap-2 flex-wrap mt-2 mt-md-0">
                    <select id="filter-severity" class="form-select form-select-sm" style="width: auto;">
                        <option value="">All Severities</option>
                        <option value="critical">Critical</option>
                        <option value="warning">Warning</option>
                        <option value="info">Info</option>
                    </select>
                    <select id="filter-site" class="form-select form-select-sm" style="width: auto;">
                        <option value="">All Sites</option>
                        <?php foreach ($subscriptions as $sub): ?>
                        <option value="<?php echo intval($sub['site_id']); ?>"><?php echo htmlspecialchars($sub['primary_domain']); ?></option>
                        <?php endforeach; ?>
                    </select>
                    <select id="filter-status" class="form-select form-select-sm" style="width: auto;">
                        <option value="unread">Unread</option>
                        <option value="all">All</option>
                        <option value="resolved">Resolved</option>
                    </select>
                </div>
            </div>
            <div class="card-body p-0">
                <!-- Bulk action bar (hidden until at least one row is selected) -->
                <div id="issues-bulk-bar" class="px-3 pt-3" style="display: none;">
                    <div class="alert alert-light border d-flex flex-wrap align-items-center mb-2">
                        <span class="me-2"><strong id="issues-selected-count">0</strong> issue(s) selected</span>
                        <button type="button" class="btn btn-sm btn-outline-success ms-2" id="bulk-fix-issues">
                            <i class="fa fa-check-circle"></i> Mark Fixed
                        </button>
                        <button type="button" class="btn btn-sm btn-outline-warning ms-2" id="bulk-ignore-issues">
                            <i class="fa fa-ban"></i> Ignore
                        </button>
                        <button type="button" class="btn btn-sm btn-outline-danger ms-2" id="bulk-delete-issues">
                            <i class="fa fa-trash"></i> Delete
                        </button>
                        <button type="button" class="btn btn-sm btn-outline-secondary ms-2" id="bulk-clear-issues">
                            Clear
                        </button>
                    </div>
                </div>

                <div class="table-responsive">
                    <table class="table table-hover mb-0" id="issues-table">
                        <thead>
                            <tr>
                                <th style="width: 36px;">
                                    <input type="checkbox" id="issues-select-all"
                                           title="Select all visible" aria-label="Select all visible issues">
                                </th>
                                <th style="width: 100px;">Severity</th>
                                <th>Title</th>
                                <th>Site</th>
                                <th>Container</th>
                                <th>First Seen</th>
                                <th>Last Seen</th>
                                <th style="width: 100px;">Status</th>
                            </tr>
                        </thead>
                        <tbody id="issues-tbody">
                            <tr>
                                <td colspan="8" class="text-center py-4">
                                    <i class="fa fa-spinner fa-spin"></i> Loading issues...
                                </td>
                            </tr>
                        </tbody>
                    </table>
                </div>
                <!-- Pagination -->
                <div class="d-flex justify-content-between align-items-center p-3" id="issues-pagination-wrap">
                    <div class="text-muted small" id="issues-count-label"></div>
                    <nav>
                        <ul class="pagination pagination-sm mb-0" id="issues-pagination"></ul>
                    </nav>
                </div>
            </div>
        </div>
    </div>
</div>

<!-- Section 3: On-Demand Log Review -->
<div class="row g-4 mb-4 whp-domain-section" id="section-log-review" data-section="log-review" hidden>
    <div class="col-12">
        <div class="card">
            <div class="card-header">
                <h5 class="card-title mb-0"><i class="fa fa-search"></i> On-Demand Log Review</h5>
            </div>
            <div class="card-body">
                <!-- Per-site review request -->
                <div class="row g-3 mb-4">
                    <?php foreach ($subscriptions as $sub): ?>
                    <div class="col-md-4 col-lg-3">
                        <div class="card border h-100">
                            <div class="card-body text-center">
                                <h6 class="text-truncate" title="<?php echo htmlspecialchars($sub['primary_domain']); ?>">
                                    <?php echo htmlspecialchars($sub['primary_domain']); ?>
                                </h6>
                                <div class="mb-2">
                                    <span class="text-muted small review-remaining" data-subscription-id="<?php echo intval($sub['id']); ?>" data-site-id="<?php echo intval($sub['site_id']); ?>" data-daily-limit="<?php echo intval($sub['daily_review_limit']); ?>">
                                        Loading...
                                    </span>
                                </div>
                                <button type="button" class="btn btn-sm btn-primary btn-request-review"
                                        data-site-id="<?php echo intval($sub['site_id']); ?>"
                                        data-subscription-id="<?php echo intval($sub['id']); ?>">
                                    <i class="fa fa-search"></i> Request Log Review
                                </button>
                                <!-- In-progress spinner (hidden by default) -->
                                <div class="review-progress mt-2 d-none" data-site-id="<?php echo intval($sub['site_id']); ?>">
                                    <i class="fa fa-spinner fa-spin text-primary"></i>
                                    <span class="text-muted small">Review in progress...</span>
                                </div>
                            </div>
                        </div>
                    </div>
                    <?php endforeach; ?>
                </div>

                <!-- Per-subscription Traffic panels. Sparklines are inline SVG
                     rendered server-side — no chart library, no JS render race. -->

                <style>
                    /* Full-row clickable summaries for the top-URLs / top-bandwidth blocks. */
                    details.traffic-details > summary {
                        list-style: none;
                        cursor: pointer;
                        user-select: none;
                        padding: 0.5rem 0.75rem;
                        background: rgba(13, 110, 253, 0.06);
                        border: 1px solid rgba(13, 110, 253, 0.18);
                        border-radius: 0.375rem;
                        display: flex;
                        align-items: center;
                        transition: background 0.15s;
                    }
                    details.traffic-details > summary::-webkit-details-marker { display: none; }
                    details.traffic-details > summary:hover { background: rgba(13, 110, 253, 0.12); }
                    details.traffic-details > summary .traffic-details-marker::before {
                        content: "\25B8"; /* ▸ */
                        display: inline-block;
                        width: 1em;
                        margin-right: 0.4em;
                        transition: transform 0.15s ease;
                    }
                    details.traffic-details[open] > summary .traffic-details-marker::before {
                        transform: rotate(90deg);
                    }
                </style>

                <div id="traffic-panels-section" class="mb-4">
                    <div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-2">
                        <div class="position-relative flex-grow-1" style="max-width: 320px;">
                            <i class="fa fa-search position-absolute" style="left: 10px; top: 50%; transform: translateY(-50%); color: #6c757d; pointer-events: none;"></i>
                            <input id="traffic-panels-search" type="search" class="form-control form-control-sm" placeholder="Search sites..." style="padding-left: 30px;">
                        </div>
                        <div class="d-flex align-items-center gap-2">
                            <button type="button" class="btn btn-sm btn-outline-secondary" id="traffic-panels-prev"><i class="fa fa-chevron-left"></i></button>
                            <span class="text-muted small" id="traffic-panels-pagelabel">&nbsp;</span>
                            <button type="button" class="btn btn-sm btn-outline-secondary" id="traffic-panels-next"><i class="fa fa-chevron-right"></i></button>
                        </div>
                    </div>
                    <div id="traffic-panels-container">
                <?php foreach ($subscriptions as $sub):
                    $site_id         = (int) $sub['site_id'];
                    $username        = (string) ($sub['owner'] ?? $sub['username'] ?? AUTH_USER);
                    $primary_domain  = (string) $sub['primary_domain'];
                    $subscription_id = (int) $sub['id'];
                    include __DIR__ . '/../includes/ai-monitor-traffic-panel.php';
                endforeach; ?>
                    </div>
                    <div id="traffic-panels-empty" class="text-muted text-center py-3 d-none">No sites match your search.</div>
                </div>
                <script>
                (function() {
                    var pageSize = 5;
                    var page = 1;
                    var container = document.getElementById('traffic-panels-container');
                    if (!container) return;
                    var allPanels = Array.from(container.querySelectorAll(':scope > .traffic-panel'));
                    var search    = document.getElementById('traffic-panels-search');
                    var prevBtn   = document.getElementById('traffic-panels-prev');
                    var nextBtn   = document.getElementById('traffic-panels-next');
                    var label     = document.getElementById('traffic-panels-pagelabel');
                    var emptyMsg  = document.getElementById('traffic-panels-empty');

                    function render() {
                        var q = (search.value || '').trim().toLowerCase();
                        var visible = allPanels.filter(function(p) {
                            if (!q) return true;
                            var d = (p.getAttribute('data-domain') || '').toLowerCase();
                            return d.indexOf(q) !== -1;
                        });
                        var totalPages = Math.max(1, Math.ceil(visible.length / pageSize));
                        if (page > totalPages) page = totalPages;
                        if (page < 1) page = 1;

                        allPanels.forEach(function(p) { p.style.display = 'none'; });
                        var start = (page - 1) * pageSize;
                        visible.slice(start, start + pageSize).forEach(function(p) { p.style.display = ''; });

                        emptyMsg.classList.toggle('d-none', visible.length > 0);
                        prevBtn.disabled = page <= 1;
                        nextBtn.disabled = page >= totalPages;
                        if (visible.length === 0) {
                            label.textContent = '0 sites';
                        } else {
                            label.textContent = 'Page ' + page + ' of ' + totalPages + ' · ' + visible.length + ' site' + (visible.length === 1 ? '' : 's');
                        }
                    }

                    prevBtn.addEventListener('click', function() { page--; render(); });
                    nextBtn.addEventListener('click', function() { page++; render(); });
                    search.addEventListener('input', function() { page = 1; render(); });
                    render();
                })();
                </script>

                <!-- Review History -->
                <h6 class="mb-3"><i class="fa fa-history"></i> Review History</h6>
                <div class="row mb-3">
                    <div class="col-auto">
                        <select id="review-history-site" class="form-select form-select-sm">
                            <option value="">All Sites</option>
                            <?php foreach ($subscriptions as $sub): ?>
                            <option value="<?php echo intval($sub['site_id']); ?>"><?php echo htmlspecialchars($sub['primary_domain']); ?></option>
                            <?php endforeach; ?>
                        </select>
                    </div>
                </div>
                <div class="table-responsive">
                    <table class="table table-hover mb-0" id="review-history-table">
                        <thead>
                            <tr>
                                <th>Date</th>
                                <th>Site</th>
                                <th>Type</th>
                                <th>Status</th>
                                <th>Summary</th>
                                <th style="width: 80px;"></th>
                            </tr>
                        </thead>
                        <tbody id="review-history-tbody">
                            <tr>
                                <td colspan="6" class="text-center py-4">
                                    <i class="fa fa-spinner fa-spin"></i> Loading review history...
                                </td>
                            </tr>
                        </tbody>
                    </table>
                </div>
            </div>
        </div>
    </div>
</div>

<!-- Section 4: Notification Settings -->
<div class="row g-4 mb-4 whp-domain-section" id="section-notifications" data-section="notifications" hidden>
    <div class="col-12">
        <div class="card">
            <div class="card-header">
                <h5 class="card-title mb-0"><i class="fa fa-bell"></i> Notification Settings</h5>
            </div>
            <div class="card-body">
                <p class="text-muted mb-3">Set an email address per site to receive issue notifications. Leave blank to disable email alerts.</p>
                <?php foreach ($subscriptions as $sub): ?>
                <div class="row align-items-center mb-3 notification-row" data-subscription-id="<?php echo intval($sub['id']); ?>">
                    <div class="col-md-3">
                        <strong><?php echo htmlspecialchars($sub['primary_domain']); ?></strong>
                    </div>
                    <div class="col-md-5">
                        <div class="input-group input-group-sm">
                            <input type="email" class="form-control notification-email"
                                   value="<?php echo htmlspecialchars($sub['notification_email'] ?? ''); ?>"
                                   placeholder="email@example.com"
                                   data-subscription-id="<?php echo intval($sub['id']); ?>">
                            <button type="button" class="btn btn-outline-primary btn-save-notification"
                                    data-subscription-id="<?php echo intval($sub['id']); ?>">
                                <i class="fa fa-save"></i> Save
                            </button>
                        </div>
                    </div>
                    <div class="col-md-4">
                        <span class="notification-save-status text-muted small" data-subscription-id="<?php echo intval($sub['id']); ?>"></span>
                    </div>
                </div>
                <?php endforeach; ?>
            </div>
        </div>
    </div>
</div>

<!-- Suppressed Alerts Panel -->
<div class="card mt-3 whp-domain-section" id="section-suppressions" data-section="suppressions" hidden>
    <button type="button"
            class="card-header d-flex justify-content-between align-items-center w-100 border-0 bg-transparent text-start"
            data-bs-toggle="collapse" data-bs-target="#suppressionsBody"
            aria-expanded="false" aria-controls="suppressionsBody">
        <strong><i class="fa fa-ban"></i> Suppressed Alerts</strong>
        <i class="fa fa-caret-down"></i>
    </button>
    <div id="suppressionsBody" class="collapse">
        <div class="card-body">
            <table class="table table-sm">
                <thead>
                    <tr>
                        <th>Site</th>
                        <th>Issue</th>
                        <th>Reason</th>
                        <th>Suppressed at</th>
                        <th>Count</th>
                        <th></th>
                    </tr>
                </thead>
                <tbody id="suppressionsTableBody">
                    <tr><td colspan="6" class="text-muted">Click to load…</td></tr>
                </tbody>
            </table>
        </div>
    </div>
</div>

<!-- Issue Detail Modal -->
<div class="modal fade" id="issueDetailModal" tabindex="-1" aria-hidden="true">
    <div class="modal-dialog modal-lg">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title" id="issueDetailTitle">Issue Details</h5>
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
            </div>
            <div class="modal-body">
                <div class="mb-3">
                    <span class="badge" id="issueDetailSeverity"></span>
                    <span class="badge bg-secondary" id="issueDetailCategory"></span>
                    <span class="text-muted small ms-2" id="issueDetailMeta"></span>
                </div>
                <h6>Diagnosis</h6>
                <p id="issueDetailDiagnosis"></p>
                <h6>Recommendation</h6>
                <p id="issueDetailRecommendation"></p>
                <div id="issueDetailAutoFixWrap" class="d-none mt-3">
                    <h6><i class="fa fa-magic"></i> Suggested Fix</h6>
                    <div class="card border-info">
                        <div class="card-body py-2 px-3">
                            <div id="issueDetailAutoFixDescription" class="small mb-2"></div>
                            <pre class="bg-dark text-light p-2 rounded mb-2" style="font-size: 0.85em; white-space: pre-wrap;"><code id="issueDetailAutoFixCommand"></code></pre>
                            <div id="issueDetailAutoFixActions" class="d-flex align-items-center gap-2 flex-wrap">
                                <button type="button" class="btn btn-sm btn-info btn-apply-fix-modal" data-issue-id="">
                                    <i class="fa fa-play"></i> Apply this fix
                                </button>
                                <span id="issueDetailAutoFixStatus" class="small text-muted"></span>
                            </div>
                            <div id="issueDetailAutoFixOutputWrap" class="mt-2 d-none">
                                <details>
                                    <summary class="small text-muted" style="cursor: pointer;">Command output</summary>
                                    <pre class="bg-dark text-light p-2 rounded mt-1" style="max-height: 200px; overflow-y: auto; font-size: 0.8em; white-space: pre-wrap;" id="issueDetailAutoFixOutput"></pre>
                                </details>
                            </div>
                        </div>
                    </div>
                </div>
                <div id="issueDetailEvidenceWrap" class="d-none mt-3">
                    <h6><i class="fa fa-quote-left"></i> Evidence (verbatim log line)</h6>
                    <pre class="bg-dark text-light p-3 rounded" style="max-height: 200px; overflow-y: auto; font-size: 0.85em; white-space: pre-wrap;" id="issueDetailEvidence"></pre>
                    <div id="issueDetailEvidenceMeta" class="small text-muted mt-1"></div>
                    <div id="issueDetailStaleWarning" class="alert alert-warning small mt-2 d-none">
                        <i class="fa fa-clock-o"></i>
                        <strong>This issue may no longer be relevant.</strong>
                        <span id="issueDetailStaleReason"></span>
                    </div>
                </div>
                <div id="issueDetailLogWrap" class="d-none mt-3">
                    <h6>Surrounding Log Context</h6>
                    <pre class="bg-dark text-light p-3 rounded" style="max-height: 300px; overflow-y: auto; font-size: 0.85em;" id="issueDetailLog"></pre>
                </div>
                <div id="issueDetailExplainWrap" class="d-none mt-3">
                    <h6><i class="fa fa-comments-o"></i> Explanation</h6>
                    <div id="issueDetailExplain" class="p-3 rounded border" style="white-space: pre-wrap; line-height: 1.5; background: var(--bs-tertiary-bg, rgba(128,128,128,0.08)); color: var(--bs-body-color);"></div>
                    <small class="text-muted d-block mt-1" id="issueDetailExplainMeta"></small>
                </div>
            </div>
            <div class="modal-footer justify-content-between">
                <button type="button" class="btn btn-sm btn-outline-info btn-explain-modal" data-issue-id="">
                    <i class="fa fa-comments-o"></i> Explain this to me
                </button>
                <div>
                    <button type="button" class="btn btn-sm btn-outline-success btn-mark-fixed-modal" data-issue-id="">
                        <i class="fa fa-check-circle"></i> Mark Fixed
                    </button>
                    <button type="button" class="btn btn-sm btn-outline-warning btn-mark-ignored-modal" data-issue-id="">
                        <i class="fa fa-ban"></i> Ignore
                    </button>
                    <button type="button" class="btn btn-sm btn-outline-danger btn-ignore-similar-modal" data-issue-id="" title="Create a rule that ignores future findings with this category/title">
                        <i class="fa fa-filter"></i> Ignore similar issues
                    </button>
                    <button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">Close</button>
                </div>
            </div>
        </div>
    </div>
</div>

<!-- Review Detail Modal -->
<div class="modal fade" id="reviewDetailModal" tabindex="-1" aria-hidden="true">
    <div class="modal-dialog modal-lg">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title">Log Review Details</h5>
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
            </div>
            <div class="modal-body" id="reviewDetailBody">
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">Close</button>
            </div>
        </div>
    </div>
</div>

<!-- Chart.js v4 is loaded near the top of the page (above the traffic
     panels) so the panel sparklines find the global Chart constructor
     synchronously instead of polling. -->

<script>
(function() {
    'use strict';

    var csrfToken = <?php echo json_encode($csrf_token); ?>;
    var isRoot = <?php echo $is_root ? 'true' : 'false'; ?>;
    var apiBase = 'api/ai-monitor-customer.php';
    var currentIssuesPage = 1;
    var perPage = 20;
    var pollTimers = {};
    var selectedIssues = new Set();

    // ========================
    // Utility Functions
    // ========================

    function byId(id) { return document.getElementById(id); }
    function qa(sel) { return document.querySelectorAll(sel); }
    function q(sel) { return document.querySelector(sel); }

    function apiGet(action, params) {
        params = params || {};
        params.action = action;
        var qs = Object.keys(params).map(function(k) { return encodeURIComponent(k) + '=' + encodeURIComponent(params[k]); }).join('&');
        return fetch(apiBase + '?' + qs).then(function(r) { return r.json(); });
    }

    function apiPost(action, data) {
        data = data || {};
        data.action = action;
        return fetch(apiBase, {
            method: 'POST',
            headers: {'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken},
            body: JSON.stringify(data)
        }).then(function(r) { return r.json(); });
    }

    function severityBadge(severity) {
        var map = { critical: 'bg-danger', warning: 'bg-warning text-dark', info: 'bg-info' };
        return '<span class="badge ' + (map[severity] || 'bg-secondary') + '">' + escapeHtml(severity) + '</span>';
    }

    function statusBadge(status) {
        var map = { pending: 'bg-warning text-dark', processing: 'bg-info', completed: 'bg-success', failed: 'bg-danger' };
        return '<span class="badge ' + (map[status] || 'bg-secondary') + '">' + escapeHtml(status) + '</span>';
    }

    function escapeHtml(text) {
        if (!text) return '';
        var div = document.createElement('div');
        div.appendChild(document.createTextNode(text));
        return div.innerHTML;
    }

    function formatDate(dateStr) {
        if (!dateStr) return '-';
        var d = new Date(dateStr);
        if (isNaN(d.getTime())) return dateStr;
        return d.toLocaleDateString() + ' ' + d.toLocaleTimeString([], {hour: '2-digit', minute: '2-digit'});
    }

    // Note: innerHTML is used throughout this file to render trusted, server-derived
    // content (severity badges, pagination, table rows). All user-visible strings are
    // passed through escapeHtml() before insertion. No raw user input is rendered.

    // ========================
    // Issues
    // ========================

    function loadIssues(page) {
        page = page || 1;
        currentIssuesPage = page;
        var params = { page: page, per_page: perPage };
        var severity = byId('filter-severity') ? byId('filter-severity').value : '';
        var siteId = byId('filter-site') ? byId('filter-site').value : '';
        var status = byId('filter-status') ? byId('filter-status').value : '';
        if (severity) params.severity = severity;
        if (siteId) params.site_id = siteId;
        if (status) params.status = status;

        var tbody = byId('issues-tbody');
        if (tbody) tbody.innerHTML = '<tr><td colspan="8" class="text-center py-4"><i class="fa fa-spinner fa-spin"></i> Loading...</td></tr>';

        apiGet('issues', params).then(function(resp) {
            if (!resp.success || !resp.data) {
                if (tbody) tbody.innerHTML = '<tr><td colspan="8" class="text-center py-4 text-danger">Failed to load issues.</td></tr>';
                return;
            }
            var issues = resp.data.issues || [];
            var total = resp.data.total || 0;
            var totalPages = resp.data.total_pages || 1;
            // A fresh page of results invalidates any prior selection.
            selectedIssues.clear();
            updateBulkBar();
            if (issues.length === 0) {
                if (tbody) tbody.innerHTML = '<tr><td colspan="8" class="text-center py-4 text-muted">No issues found.</td></tr>';
                var cl = byId('issues-count-label'); if (cl) cl.textContent = '';
                var pg = byId('issues-pagination'); if (pg) pg.innerHTML = '';
                return;
            }
            var html = '';
            for (var i = 0; i < issues.length; i++) {
                var issue = issues[i];
                var rowClass = (issue.status === 'fixed' || issue.status === 'ignored') ? 'text-muted' : 'fw-bold';
                html += '<tr class="issue-row ' + rowClass + '" data-issue-id="' + issue.id + '" style="cursor:pointer;">';
                html += '<td><input type="checkbox" class="issue-select" value="' + issue.id + '" aria-label="Select issue ' + issue.id + '"></td>';
                html += '<td>' + severityBadge(issue.severity) + '</td>';
                html += '<td>' + escapeHtml(issue.title);
                if (issue.occurrence_count > 1) html += ' <span class="badge bg-secondary">' + issue.occurrence_count + 'x</span>';
                html += '</td>';
                html += '<td>' + escapeHtml(issue.primary_domain || issue.site_name || '') + '</td>';
                html += '<td>' + escapeHtml(issue.container_name || '-') + '</td>';
                html += '<td class="small">' + formatDate(issue.first_seen) + '</td>';
                html += '<td class="small">' + formatDate(issue.last_seen) + '</td>';
                html += '<td>';
                if (issue.status === 'fixed') html += '<span class="badge bg-success">Fixed</span>';
                else if (issue.status === 'ignored') html += '<span class="badge bg-warning">Ignored</span>';
                else if (issue.auto_resolved_at) html += '<span class="badge bg-info" title="System observed recovery">auto-resolved</span>';
                else html += '<span class="badge bg-primary">Open</span>';
                html += '</td></tr>';
            }
            if (tbody) tbody.innerHTML = html;
            var selAll = byId('issues-select-all');
            if (selAll) { selAll.checked = false; selAll.indeterminate = false; }
            var start = (page - 1) * perPage + 1;
            var end = Math.min(page * perPage, total);
            var countLabel = byId('issues-count-label');
            if (countLabel) countLabel.textContent = 'Showing ' + start + '-' + end + ' of ' + total + ' issues';
            renderPagination(totalPages, page);
        }).catch(function() {
            if (tbody) tbody.innerHTML = '<tr><td colspan="8" class="text-center py-4 text-danger">Failed to load issues.</td></tr>';
        });
    }

    function renderPagination(totalPages, currentPage) {
        var container = byId('issues-pagination');
        if (!container) return;
        if (totalPages <= 1) { container.innerHTML = ''; return; }
        var html = '';
        html += '<li class="page-item ' + (currentPage <= 1 ? 'disabled' : '') + '"><a class="page-link" href="#" data-page="' + (currentPage - 1) + '">&laquo;</a></li>';
        var startPage = Math.max(1, currentPage - 2), endPage = Math.min(totalPages, currentPage + 2);
        if (startPage > 1) {
            html += '<li class="page-item"><a class="page-link" href="#" data-page="1">1</a></li>';
            if (startPage > 2) html += '<li class="page-item disabled"><span class="page-link">...</span></li>';
        }
        for (var p = startPage; p <= endPage; p++) {
            html += '<li class="page-item ' + (p === currentPage ? 'active' : '') + '"><a class="page-link" href="#" data-page="' + p + '">' + p + '</a></li>';
        }
        if (endPage < totalPages) {
            if (endPage < totalPages - 1) html += '<li class="page-item disabled"><span class="page-link">...</span></li>';
            html += '<li class="page-item"><a class="page-link" href="#" data-page="' + totalPages + '">' + totalPages + '</a></li>';
        }
        html += '<li class="page-item ' + (currentPage >= totalPages ? 'disabled' : '') + '"><a class="page-link" href="#" data-page="' + (currentPage + 1) + '">&raquo;</a></li>';
        container.innerHTML = html;
    }

    function showIssueDetail(issueId) {
        apiGet('issues', {issue_id: issueId}).then(function(resp) {
            if (!resp.success || !resp.data || !resp.data.issue) return;
            var issue = resp.data.issue;
            byId('issueDetailTitle').textContent = issue.title;
            var sevBadge = byId('issueDetailSeverity');
            sevBadge.className = 'badge ' + (issue.severity === 'critical' ? 'bg-danger' : issue.severity === 'warning' ? 'bg-warning text-dark' : 'bg-info');
            sevBadge.textContent = issue.severity;
            byId('issueDetailCategory').textContent = issue.category || '';
            byId('issueDetailMeta').textContent =
                (issue.primary_domain || issue.site_name || '') +
                (issue.container_name ? ' / ' + issue.container_name : '') +
                ' | Occurrences: ' + (issue.occurrence_count || 1) +
                ' | First: ' + formatDate(issue.first_seen) + ' | Last: ' + formatDate(issue.last_seen);
            byId('issueDetailDiagnosis').textContent = issue.diagnosis || '';
            byId('issueDetailRecommendation').textContent = issue.recommendation || '';

            // Auto-fix panel: shows the AI-suggested wp-cli command, an
            // "Apply this fix" button when status is 'available', a
            // running spinner during execution, or the captured output
            // once applied/failed.
            renderAutoFixPanel(issue);

            // Evidence panel: shows the verbatim log line the AI cited as
            // proof, plus the timestamp parsed from that line. If the
            // observed timestamp is older than 7 days, warn the user.
            var evidenceWrap = byId('issueDetailEvidenceWrap');
            if (issue.evidence) {
                byId('issueDetailEvidence').textContent = issue.evidence;
                var metaParts = [];
                if (issue.evidence_observed_at) {
                    metaParts.push('Log line timestamp: ' + formatDate(issue.evidence_observed_at));
                } else {
                    metaParts.push('Log line timestamp not available');
                }
                if (issue.last_seen) {
                    metaParts.push('Last detected: ' + formatDate(issue.last_seen));
                }
                byId('issueDetailEvidenceMeta').textContent = metaParts.join('  ·  ');

                var staleEl  = byId('issueDetailStaleWarning');
                var reasonEl = byId('issueDetailStaleReason');
                var observedTs = issue.evidence_observed_at ? Date.parse(issue.evidence_observed_at.replace(' ', 'T')) : NaN;
                var ageDays = isNaN(observedTs) ? null : Math.floor((Date.now() - observedTs) / 86400000);
                if (ageDays !== null && ageDays >= 7) {
                    reasonEl.textContent = ' The log line evidencing this issue is from '
                        + ageDays + ' day(s) ago. Verify the issue is still happening before acting.';
                    staleEl.classList.remove('d-none');
                } else {
                    staleEl.classList.add('d-none');
                }
                evidenceWrap.classList.remove('d-none');
            } else {
                evidenceWrap.classList.add('d-none');
            }

            var logWrap = byId('issueDetailLogWrap');
            if (issue.raw_log_excerpt) { byId('issueDetailLog').textContent = issue.raw_log_excerpt; logWrap.classList.remove('d-none'); }
            else { logWrap.classList.add('d-none'); }
            qa('.btn-mark-fixed-modal').forEach(function(b) { b.setAttribute('data-issue-id', issue.id); });
            qa('.btn-mark-ignored-modal').forEach(function(b) { b.setAttribute('data-issue-id', issue.id); });
            qa('.btn-ignore-similar-modal').forEach(function(b) { b.setAttribute('data-issue-id', issue.id); });
            qa('.btn-explain-modal').forEach(function(b) {
                b.setAttribute('data-issue-id', issue.id);
                b.disabled = false;
                b.innerHTML = '<i class="fa fa-comments-o"></i> Explain this to me';
            });
            // Show cached explanation if present; otherwise hide the wrap until user clicks.
            var explainWrap = byId('issueDetailExplainWrap');
            var explainBody = byId('issueDetailExplain');
            var explainMeta = byId('issueDetailExplainMeta');
            if (issue.user_explanation) {
                explainBody.textContent = issue.user_explanation;
                explainMeta.textContent = 'Generated ' + (issue.user_explanation_at || '')
                    + (issue.user_explanation_model ? ' · ' + issue.user_explanation_model : '');
                explainWrap.classList.remove('d-none');
            } else {
                explainBody.textContent = '';
                explainMeta.textContent = '';
                explainWrap.classList.add('d-none');
            }
            var isClosed = (issue.status === 'fixed' || issue.status === 'ignored');
            qa('.btn-mark-fixed-modal').forEach(function(b) { b.classList.toggle('d-none', isClosed); });
            qa('.btn-mark-ignored-modal').forEach(function(b) { b.classList.toggle('d-none', isClosed); });
            qa('.btn-ignore-similar-modal').forEach(function(b) { b.classList.toggle('d-none', isClosed); });
            var modal = new bootstrap.Modal(byId('issueDetailModal'));
            modal.show();
        });
    }

    function updateBulkBar() {
        var bar = byId('issues-bulk-bar');
        var countEl = byId('issues-selected-count');
        var n = selectedIssues.size;
        if (countEl) countEl.textContent = n;
        if (bar) bar.style.display = n > 0 ? 'block' : 'none';
        // Sync the header checkbox's tri-state.
        var selAll = byId('issues-select-all');
        if (selAll) {
            var rowChecks = qa('.issue-select');
            var visible = rowChecks.length;
            var checkedVisible = 0;
            rowChecks.forEach(function(cb) { if (cb.checked) checkedVisible++; });
            selAll.checked = (visible > 0 && checkedVisible === visible);
            selAll.indeterminate = (checkedVisible > 0 && checkedVisible < visible);
        }
    }

    function clearIssueSelection() {
        selectedIssues.clear();
        qa('.issue-select').forEach(function(cb) { cb.checked = false; });
        updateBulkBar();
    }

    function bulkIssueAction(action, confirmMsg) {
        var n = selectedIssues.size;
        if (n === 0) return;
        if (confirmMsg && !confirm(confirmMsg.replace('{n}', n))) return;
        apiPost(action, { issue_ids: Array.from(selectedIssues) }).then(function(resp) {
            if (!resp.success) {
                alert('Action failed: ' + (resp.error || 'unknown'));
                return;
            }
            var affected = (resp.data && resp.data.affected) || 0;
            var requested = (resp.data && resp.data.requested) || n;
            if (affected < requested) {
                // Some rows were skipped (already in target state, or another
                // user's row that root didn't touch).
                console.info('bulk ' + action + ': ' + affected + '/' + requested + ' rows changed');
            }
            clearIssueSelection();
            loadIssues(currentIssuesPage);
        }).catch(function(err) {
            alert('Request failed: ' + err);
        });
    }

    function markIssue(action, issueId) {
        apiPost(action, {issue_id: issueId}).then(function(resp) {
            if (resp.success) {
                var modalInst = bootstrap.Modal.getInstance(byId('issueDetailModal'));
                if (modalInst) modalInst.hide();
                loadIssues(currentIssuesPage);
            } else { alert(resp.error || 'Failed to update issue.'); }
        }).catch(function() { alert('Failed to update issue.'); });
    }

    // Render the AI-suggested auto-fix card inside the issue detail modal.
    // Visibility and controls vary by issue.auto_fix_status:
    //   available -> show "Apply this fix" button
    //   running   -> show "Applying..." with disabled button
    //   applied   -> show success badge + collapsed command output
    //   failed    -> show failure badge + collapsed command output
    //   none / no command -> hide the panel entirely
    //
    // All values inserted into the DOM here are either trusted static
    // markup (badge templates) or routed through textContent. No raw
    // server strings are passed to innerHTML.
    function renderAutoFixPanel(issue) {
        var wrap         = byId('issueDetailAutoFixWrap');
        var cmdEl        = byId('issueDetailAutoFixCommand');
        var descEl       = byId('issueDetailAutoFixDescription');
        var statusEl     = byId('issueDetailAutoFixStatus');
        var outputWrap   = byId('issueDetailAutoFixOutputWrap');
        var outputEl     = byId('issueDetailAutoFixOutput');
        var btn          = wrap ? wrap.querySelector('.btn-apply-fix-modal') : null;

        if (!wrap || !btn) return;

        var hasFix = issue.auto_fix_command && issue.auto_fix_status && issue.auto_fix_status !== 'none';
        if (!hasFix) {
            wrap.classList.add('d-none');
            return;
        }

        cmdEl.textContent  = issue.auto_fix_command;
        descEl.textContent = issue.auto_fix_description || '';
        btn.setAttribute('data-issue-id', issue.id);

        // Reset state.
        btn.disabled = false;
        btn.classList.remove('d-none');
        // Static template — no untrusted content.
        setTrustedHtml(btn, '<i class="fa fa-play"></i> Apply this fix');
        statusEl.textContent = '';
        statusEl.className = 'small text-muted';
        outputWrap.classList.add('d-none');
        outputEl.textContent = '';

        switch (issue.auto_fix_status) {
            case 'available':
                // Default reset state already shows the button.
                break;
            case 'running':
                btn.disabled = true;
                setTrustedHtml(btn, '<span class="spinner-border spinner-border-sm me-1"></span>Applying…');
                statusEl.textContent = 'Running command…';
                break;
            case 'applied':
                btn.classList.add('d-none');
                // Build success badge with safe DOM ops.
                statusEl.textContent = '';
                appendBadge(statusEl, 'bg-success', 'fa-check', 'Applied');
                if (issue.auto_fix_applied_at) {
                    var when = document.createElement('span');
                    when.className = 'text-muted ms-1';
                    when.textContent = 'at ' + issue.auto_fix_applied_at;
                    statusEl.appendChild(when);
                }
                if (issue.auto_fix_output) {
                    outputEl.textContent = issue.auto_fix_output;
                    outputWrap.classList.remove('d-none');
                }
                break;
            case 'failed':
                btn.classList.add('d-none');
                statusEl.textContent = '';
                appendBadge(statusEl, 'bg-danger', 'fa-times', 'Failed');
                if (issue.auto_fix_output) {
                    outputEl.textContent = issue.auto_fix_output;
                    outputWrap.classList.remove('d-none');
                }
                break;
        }
        wrap.classList.remove('d-none');
    }

    // Helper: assign a fixed, source-controlled HTML template. Centralised
    // so the security review can audit every callsite easily — no untrusted
    // content ever flows in here.
    function setTrustedHtml(el, html) {
        el.innerHTML = html;
    }

    // Helper: append a Bootstrap badge with an icon and a text label.
    // The label is set via textContent so server / AI strings are safe.
    function appendBadge(parent, bgClass, iconClass, label) {
        var span = document.createElement('span');
        span.className = 'badge ' + bgClass;
        var icon = document.createElement('i');
        icon.className = 'fa ' + iconClass;
        span.appendChild(icon);
        span.appendChild(document.createTextNode(' ' + label));
        parent.appendChild(span);
    }

    // Apply an AI-suggested wp-cli fix. Disables the button while in flight,
    // surfaces the captured output on completion, and refreshes the issue
    // list (the issue should flip to 'fixed' on success).
    function applyAutoFix(issueId) {
        var wrap     = byId('issueDetailAutoFixWrap');
        var btn      = wrap ? wrap.querySelector('.btn-apply-fix-modal') : null;
        var statusEl = byId('issueDetailAutoFixStatus');
        var outputWrap = byId('issueDetailAutoFixOutputWrap');
        var outputEl   = byId('issueDetailAutoFixOutput');
        if (!btn) return;

        btn.disabled = true;
        setTrustedHtml(btn, '<span class="spinner-border spinner-border-sm me-1"></span>Applying…');
        if (statusEl) {
            statusEl.textContent = 'Running command…';
            statusEl.className = 'small text-muted';
        }

        apiPost('apply_auto_fix', {issue_id: parseInt(issueId, 10)}).then(function(resp) {
            if (resp.success) {
                if (statusEl) {
                    statusEl.textContent = '';
                    appendBadge(statusEl, 'bg-success', 'fa-check', 'Applied');
                }
                btn.classList.add('d-none');
                if (resp.data && resp.data.output) {
                    outputEl.textContent = resp.data.output;
                    outputWrap.classList.remove('d-none');
                }
                // Refresh list so the issue flips to fixed.
                loadIssues(currentIssuesPage);
                // Close modal after a short pause so the user sees the success state.
                setTimeout(function() {
                    var modalInst = bootstrap.Modal.getInstance(byId('issueDetailModal'));
                    if (modalInst) modalInst.hide();
                }, 1500);
            } else {
                btn.disabled = false;
                setTrustedHtml(btn, '<i class="fa fa-play"></i> Apply this fix');
                if (statusEl) {
                    statusEl.textContent = '';
                    appendBadge(statusEl, 'bg-danger', 'fa-times', 'Failed');
                    var err = document.createElement('span');
                    err.className = 'text-muted ms-1';
                    err.textContent = resp.error || 'unknown error';
                    statusEl.appendChild(err);
                }
                if (resp.data && resp.data.output) {
                    outputEl.textContent = resp.data.output;
                    outputWrap.classList.remove('d-none');
                }
            }
        }).catch(function() {
            btn.disabled = false;
            setTrustedHtml(btn, '<i class="fa fa-play"></i> Apply this fix');
            if (statusEl) {
                statusEl.textContent = '';
                appendBadge(statusEl, 'bg-danger', 'fa-times', 'Request failed');
            }
        });
    }

    function markIssueIgnored(issueId, suppress, reason) {
        apiPost('mark_ignored', {
            issue_id: parseInt(issueId, 10),
            suppress: suppress ? 1 : 0,
            reason: reason || ''
        }).then(function(data) {
            if (!data.success) {
                alert('Failed to ignore: ' + (data.error || 'unknown'));
                return;
            }
            var modalInst = bootstrap.Modal.getInstance(byId('issueDetailModal'));
            if (modalInst) modalInst.hide();
            loadIssues(currentIssuesPage);
        }).catch(function(err) {
            alert('Request failed: ' + err);
        });
    }

    function explainIssue(issueId, btn) {
        var wrap = byId('issueDetailExplainWrap');
        var body = byId('issueDetailExplain');
        var meta = byId('issueDetailExplainMeta');
        if (btn) {
            btn.disabled = true;
            btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Thinking…';
        }
        apiPost('explain_issue', { issue_id: parseInt(issueId, 10) })
            .then(function(data) {
                if (!data.success) {
                    alert('Failed to generate explanation: ' + (data.error || 'unknown'));
                    return;
                }
                var payload = data.data || data;
                body.textContent = payload.explanation || '';
                meta.textContent = (payload.cached ? 'Cached explanation' : 'Newly generated')
                    + (payload.generated_at ? ' · ' + payload.generated_at : '')
                    + (payload.model ? ' · ' + payload.model : '');
                wrap.classList.remove('d-none');
            })
            .catch(function(err) { alert('Request failed: ' + err); })
            .finally(function() {
                if (btn) {
                    btn.disabled = false;
                    btn.innerHTML = '<i class="fa fa-comments-o"></i> Explain this to me';
                }
            });
    }

    // ========================
    // Log Reviews
    // ========================

    function loadReviewRemaining() {
        qa('.review-remaining').forEach(function(elem) {
            var siteId = elem.getAttribute('data-site-id');
            var dailyLimit = parseInt(elem.getAttribute('data-daily-limit'), 10) || 3;

            // Root has no limits - just check for in-progress reviews
            if (isRoot) {
                elem.textContent = '';
                apiGet('review_history', {site_id: siteId}).then(function(resp) {
                    if (!resp.success) return;
                    var reviews = resp.data.reviews || [];
                    for (var j = 0; j < reviews.length; j++) {
                        if (reviews[j].status === 'pending' || reviews[j].status === 'processing') {
                            showReviewProgress(siteId, reviews[j].id);
                            break;
                        }
                    }
                });
                return;
            }

            apiGet('review_history', {site_id: siteId}).then(function(resp) {
                if (!resp.success) return;
                var reviews = resp.data.reviews || [];
                var today = new Date().toISOString().slice(0, 10);
                var todayCount = 0;
                for (var i = 0; i < reviews.length; i++) {
                    // Only count the user's own reviews against the limit (not root's)
                    if (reviews[i].requested_at && reviews[i].requested_at.slice(0, 10) === today
                        && reviews[i].username !== 'root') {
                        todayCount++;
                    }
                }
                var remaining = Math.max(0, dailyLimit - todayCount);
                // Only surface the limit when it's actually reached — the
                // running count was noise users didn't care about.
                if (remaining <= 0) {
                    elem.textContent = 'Daily review limit reached';
                    elem.className = 'review-remaining text-warning fw-semibold small';
                } else {
                    elem.textContent = '';
                    elem.className = 'review-remaining';
                }
                var cardBody = elem.closest('.card-body');
                var btn = cardBody ? cardBody.querySelector('.btn-request-review') : null;
                if (btn) { btn.disabled = remaining <= 0; btn.title = remaining <= 0 ? 'Daily limit reached' : ''; }
                for (var j = 0; j < reviews.length; j++) {
                    if (reviews[j].status === 'pending' || reviews[j].status === 'processing') {
                        showReviewProgress(siteId, reviews[j].id);
                        break;
                    }
                }
            });
        });
    }

    function requestReview(siteId) {
        var btn = q('.btn-request-review[data-site-id="' + siteId + '"]');
        if (btn) { btn.disabled = true; btn.innerHTML = '<i class="fa fa-spinner fa-spin"></i> Requesting...'; }
        apiPost('request_review', {site_id: siteId, request_type: 'full'}).then(function(resp) {
            if (resp.success) { showReviewProgress(siteId, resp.data.review_id); loadReviewRemaining(); loadReviewHistory(); }
            else { alert(resp.error || 'Failed to request review.'); if (btn) { btn.disabled = false; btn.innerHTML = '<i class="fa fa-search"></i> Request Log Review'; } }
        }).catch(function() { alert('Failed to request review.'); if (btn) { btn.disabled = false; btn.innerHTML = '<i class="fa fa-search"></i> Request Log Review'; } });
    }

    function showReviewProgress(siteId, reviewId) {
        var progressEl = q('.review-progress[data-site-id="' + siteId + '"]');
        if (progressEl) progressEl.classList.remove('d-none');
        if (pollTimers['review_' + reviewId]) clearInterval(pollTimers['review_' + reviewId]);
        pollTimers['review_' + reviewId] = setInterval(function() {
            apiGet('review_status', {id: reviewId}).then(function(resp) {
                if (!resp.success) return;
                var review = resp.data.review;
                if (review.status === 'completed' || review.status === 'failed') {
                    clearInterval(pollTimers['review_' + reviewId]);
                    delete pollTimers['review_' + reviewId];
                    if (progressEl) progressEl.classList.add('d-none');
                    var btn = q('.btn-request-review[data-site-id="' + siteId + '"]');
                    if (btn) { btn.disabled = false; btn.innerHTML = '<i class="fa fa-search"></i> Request Log Review'; }
                    loadReviewRemaining();
                    loadReviewHistory();
                }
            });
        }, 5000);
    }

    function loadReviewHistory() {
        var siteSelect = byId('review-history-site');
        var siteId = siteSelect ? siteSelect.value : '';
        var params = {};
        if (siteId) params.site_id = siteId;
        var tbody = byId('review-history-tbody');
        if (tbody) tbody.innerHTML = '<tr><td colspan="6" class="text-center py-4"><i class="fa fa-spinner fa-spin"></i> Loading...</td></tr>';
        apiGet('review_history', params).then(function(resp) {
            if (!resp.success || !resp.data) { if (tbody) tbody.innerHTML = '<tr><td colspan="6" class="text-center py-4 text-danger">Failed to load review history.</td></tr>'; return; }
            var reviews = resp.data.reviews || [];
            if (reviews.length === 0) { if (tbody) tbody.innerHTML = '<tr><td colspan="6" class="text-center py-4 text-muted">No reviews yet.</td></tr>'; return; }
            var html = '';
            for (var i = 0; i < reviews.length; i++) {
                var r = reviews[i];
                html += '<tr>';
                html += '<td class="small">' + formatDate(r.requested_at) + '</td>';
                html += '<td>' + escapeHtml(r.primary_domain || '') + '</td>';
                var typeLabel;
                if (r.review_type === 'traffic') {
                    typeLabel = 'Traffic';
                } else {
                    var rt = r.request_type || 'full';
                    typeLabel = rt.charAt(0).toUpperCase() + rt.slice(1).replace(/_/g, ' ');
                }
                html += '<td>' + escapeHtml(typeLabel) + '</td>';
                html += '<td>' + statusBadge(r.status) + '</td>';
                html += '<td class="text-truncate" style="max-width: 300px;">' + escapeHtml(r.result_summary || (r.status === 'pending' || r.status === 'processing' ? 'In progress...' : '-')) + '</td>';
                html += '<td>';
                if (r.status === 'completed') html += '<button type="button" class="btn btn-sm btn-outline-primary btn-view-review me-1" data-review-id="' + r.id + '" title="View details"><i class="fa fa-eye"></i></button>';
                else if (r.status === 'pending' || r.status === 'processing') html += '<i class="fa fa-spinner fa-spin text-muted me-1"></i>';
                if (r.status === 'completed' || r.status === 'failed') {
                    html += '<button type="button" class="btn btn-sm btn-outline-danger btn-delete-review" data-review-id="' + r.id + '" title="Delete review"><i class="fa fa-trash-o"></i></button>';
                }
                html += '</td></tr>';
            }
            if (tbody) tbody.innerHTML = html;
        }).catch(function() { if (tbody) tbody.innerHTML = '<tr><td colspan="6" class="text-center py-4 text-danger">Failed to load review history.</td></tr>'; });
    }

    function showReviewDetail(reviewId) {
        apiGet('review_status', {id: reviewId}).then(function(resp) {
            if (!resp.success || !resp.data || !resp.data.review) return;
            var review = resp.data.review;
            var html = '';

            // Status and meta header
            html += '<div class="d-flex align-items-center gap-2 mb-3">';
            html += statusBadge(review.status);
            html += ' <span class="text-muted small">Requested: ' + formatDate(review.requested_at) + '</span>';
            if (review.completed_at) html += ' <span class="text-muted small">Completed: ' + formatDate(review.completed_at) + '</span>';
            html += '</div>';

            // Parse result_details
            var details = review.result_details;
            if (typeof details === 'string') { try { details = JSON.parse(details); } catch (e) { details = null; } }

            // Traffic analysis short-circuits the log-review renderer below — it has
            // no findings/site_status/pages_checked, just a free-text AI write-up.
            if (review.review_type === 'traffic' || (details && details.traffic_analysis)) {
                var analysisText = (details && details.traffic_analysis) ? details.traffic_analysis : (review.result_summary || '');
                html += '<div class="mb-3"><strong><i class="fa fa-line-chart me-1"></i>Traffic Analysis</strong>';
                if (details && (details.date_range || details.days_analyzed)) {
                    html += ' <span class="text-muted small">';
                    if (details.date_range) html += escapeHtml(details.date_range);
                    if (details.days_analyzed) html += ' &middot; ' + escapeHtml(String(details.days_analyzed)) + ' day(s)';
                    html += '</span>';
                }
                html += '<div class="mt-2 p-3 border rounded" style="white-space:pre-wrap; font-family:inherit;">' + escapeHtml(analysisText) + '</div></div>';
                byId('reviewDetailBody').innerHTML = html;
                var modal = new bootstrap.Modal(byId('reviewDetailModal'));
                modal.show();
                return;
            }

            // Site status banner (from the new AI prompt structure)
            if (details && details.site_status) {
                var ss = details.site_status;
                var statusClass = ss.is_loading === true ? 'alert-success' : ss.is_loading === false ? 'alert-danger' : 'alert-warning';
                var statusIcon = ss.is_loading === true ? 'fa-check-circle' : ss.is_loading === false ? 'fa-times-circle' : 'fa-question-circle';
                var statusLabel = ss.is_loading === true ? 'Site is Loading' : ss.is_loading === false ? 'Site May Not Be Loading' : 'Unable to Determine';
                html += '<div class="alert ' + statusClass + ' d-flex align-items-start">';
                html += '<i class="fa ' + statusIcon + ' me-3 mt-1" style="font-size:1.4em;"></i>';
                html += '<div><strong>' + statusLabel + '</strong>';
                if (ss.confidence) html += ' <span class="badge bg-secondary">' + escapeHtml(ss.confidence) + ' confidence</span>';
                if (ss.explanation) html += '<br><span class="text-muted small">' + escapeHtml(ss.explanation) + '</span>';
                html += '</div></div>';
            }

            // Live site check results
            if (details && details.initial_site_check) {
                var sc = details.initial_site_check;
                var scClass = (sc.http_code >= 200 && sc.http_code < 400) ? 'text-success' : sc.http_code >= 400 ? 'text-danger' : 'text-warning';
                html += '<div class="mb-3 small"><strong><i class="fa fa-globe me-1"></i>Live Site Check</strong>';
                html += '<table class="table table-sm table-borderless mb-0 mt-1" style="max-width:400px;">';
                html += '<tr><td class="text-muted py-0">URL:</td><td class="py-0">' + escapeHtml(sc.url) + '</td></tr>';
                html += '<tr><td class="text-muted py-0">Status:</td><td class="py-0 ' + scClass + '"><strong>' + sc.http_code + '</strong></td></tr>';
                html += '<tr><td class="text-muted py-0">Response Time:</td><td class="py-0">' + sc.time_total + 's</td></tr>';
                if (sc.error) html += '<tr><td class="text-muted py-0">Error:</td><td class="py-0 text-danger">' + escapeHtml(sc.error) + '</td></tr>';
                html += '</table></div>';
            }

            // Additional page check results
            if (details && details.pages_checked && details.pages_checked.length > 0) {
                html += '<div class="mb-3 small"><strong><i class="fa fa-files-o me-1"></i>Additional Page Checks</strong>';
                html += '<table class="table table-sm table-bordered mt-1">';
                html += '<thead><tr><th>Path</th><th>Status</th><th>Time</th><th>Size</th></tr></thead><tbody>';
                for (var p = 0; p < details.pages_checked.length; p++) {
                    var pc = details.pages_checked[p];
                    var pcClass = (pc.http_code >= 200 && pc.http_code < 400) ? 'text-success' : pc.http_code >= 400 ? 'text-danger' : 'text-warning';
                    html += '<tr>';
                    html += '<td>' + escapeHtml(pc.path) + '</td>';
                    html += '<td class="' + pcClass + '"><strong>' + (pc.http_code || 'failed') + '</strong></td>';
                    html += '<td>' + pc.time_total + 's</td>';
                    html += '<td>' + (pc.size_download ? pc.size_download + ' bytes' : '-') + '</td>';
                    html += '</tr>';
                }
                html += '</tbody></table></div>';
            }

            // Summary
            if (review.result_summary) {
                html += '<div class="mb-3"><strong>Summary</strong><p class="mb-0">' + escapeHtml(review.result_summary) + '</p></div>';
            }

            // Overall health badge
            if (details && details.overall_health) {
                var healthMap = { healthy: 'bg-success', degraded: 'bg-warning text-dark', critical: 'bg-danger' };
                html += '<div class="mb-3"><strong>Overall Health:</strong> ';
                html += '<span class="badge ' + (healthMap[details.overall_health] || 'bg-secondary') + '">';
                html += escapeHtml(details.overall_health.charAt(0).toUpperCase() + details.overall_health.slice(1)) + '</span></div>';
            }

            // Findings
            if (details && details.findings && details.findings.length > 0) {
                html += '<strong>Findings (' + details.findings.length + ')</strong>';
                html += '<div class="mt-2">';
                for (var i = 0; i < details.findings.length; i++) {
                    var f = details.findings[i];
                    var borderClass = f.severity === 'critical' ? 'border-danger' : f.severity === 'warning' ? 'border-warning' : 'border-info';
                    var impactLabel = '';
                    if (f.impact) {
                        var impactMap = { site_down: 'Site Down', partial_outage: 'Partial Outage', degraded: 'Degraded', cosmetic: 'Cosmetic' };
                        var impactBg = { site_down: 'bg-danger', partial_outage: 'bg-warning text-dark', degraded: 'bg-info', cosmetic: 'bg-secondary' };
                        impactLabel = ' <span class="badge ' + (impactBg[f.impact] || 'bg-secondary') + '">' + (impactMap[f.impact] || escapeHtml(f.impact)) + '</span>';
                    }
                    html += '<div class="card mb-2 border-start border-4 ' + borderClass + '">';
                    html += '<div class="card-body py-2 px-3">';
                    html += '<div class="d-flex align-items-center gap-2 mb-1">';
                    html += severityBadge(f.severity);
                    html += impactLabel;
                    if (f.affects_loading) html += ' <span class="badge bg-dark">Affects Loading</span>';
                    html += ' <strong>' + escapeHtml(f.title) + '</strong>';
                    html += '</div>';
                    if (f.description) html += '<p class="mb-1 small">' + escapeHtml(f.description) + '</p>';
                    if (f.recommendation) html += '<p class="mb-0 small text-primary"><i class="fa fa-lightbulb-o me-1"></i>' + escapeHtml(f.recommendation) + '</p>';
                    html += '</div></div>';
                }
                html += '</div>';
            } else if (details) {
                html += '<p class="text-success"><i class="fa fa-check-circle me-1"></i>No issues found - your site looks healthy.</p>';
            }

            // Model/token info - only shown if present (root only, API strips these for non-root)
            if (review.ai_model_used) {
                html += '<hr><p class="text-muted small mb-0">Model: ' + escapeHtml(review.ai_model_used);
                if (review.ai_tokens_used) html += ' | Tokens: ' + review.ai_tokens_used;
                html += '</p>';
            }

            // Error message if failed
            if (review.error_message) {
                html += '<div class="alert alert-danger mt-2">' + escapeHtml(review.error_message) + '</div>';
            }

            byId('reviewDetailBody').innerHTML = html;
            var modal = new bootstrap.Modal(byId('reviewDetailModal'));
            modal.show();
        });
    }

    function deleteReview(reviewId) {
        apiPost('delete_review', {review_id: reviewId}).then(function(resp) {
            if (resp.success) {
                loadReviewHistory();
                loadReviewRemaining();
            } else {
                alert(resp.error || 'Failed to delete review.');
            }
        }).catch(function() { alert('Failed to delete review.'); });
    }

    // ========================
    // Notification Settings
    // ========================

    function saveNotificationEmail(subscriptionId) {
        var input = q('.notification-email[data-subscription-id="' + subscriptionId + '"]');
        var statusEl = q('.notification-save-status[data-subscription-id="' + subscriptionId + '"]');
        var email = input ? input.value.trim() : '';
        if (statusEl) statusEl.innerHTML = '<i class="fa fa-spinner fa-spin"></i> Saving...';
        apiPost('update_notification_email', { subscription_id: subscriptionId, notification_email: email }).then(function(resp) {
            if (resp.success) {
                if (statusEl) statusEl.innerHTML = '<span class="text-success"><i class="fa fa-check"></i> Saved</span>';
                setTimeout(function() { if (statusEl) statusEl.textContent = ''; }, 3000);
            } else {
                if (statusEl) statusEl.innerHTML = '<span class="text-danger"><i class="fa fa-times"></i> ' + escapeHtml(resp.error || 'Failed') + '</span>';
            }
        }).catch(function() {
            if (statusEl) statusEl.innerHTML = '<span class="text-danger"><i class="fa fa-times"></i> Request failed</span>';
        });
    }

    // ========================
    // On-Demand Traffic Analysis
    // ========================

    // Enqueue a traffic-type review via the dedicated endpoint. The button
    // sits inside the per-subscription Traffic panel; on success we disable
    // it and show a confirmation. The existing review-history table picks
    // up the result on its next load.
    //
    // Button label HTML is built from a fixed template (no untrusted data);
    // status text is set via textContent.
    function requestTrafficAnalysis(btn) {
        if (!btn || btn.disabled) return;
        var subscriptionId = parseInt(btn.getAttribute('data-subscription-id'), 10) || 0;
        if (!subscriptionId) return;
        var statusEl = q('.analyze-traffic-status[data-subscription-id="' + subscriptionId + '"]');
        var origHtml = btn.innerHTML;
        btn.disabled = true;
        setTrustedHtml(btn, '<span class="spinner-border spinner-border-sm me-1"></span>Queuing…');
        if (statusEl) { statusEl.textContent = ''; statusEl.className = 'analyze-traffic-status'; }

        fetch('api/ai-monitor-request-traffic-analysis.php', {
            method: 'POST',
            headers: {'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken},
            body: JSON.stringify({ subscription_id: subscriptionId })
        }).then(function(r) { return r.json(); }).then(function(resp) {
            if (resp && resp.ok) {
                // Keep the button busy until the worker is done so the user
                // doesn't queue duplicates and gets clear "still working" feedback.
                setTrustedHtml(btn, '<i class="fa fa-spinner fa-spin"></i> Analyzing…');
                if (statusEl) {
                    statusEl.textContent = 'Analysis queued — running now. This can take a minute or two.';
                    statusEl.className = 'analyze-traffic-status badge bg-info text-white';
                }
                loadReviewHistory();
                pollTrafficAnalysis(btn, resp.queued_review_id, origHtml);
            } else {
                btn.disabled = false;
                setTrustedHtml(btn, origHtml);
                if (statusEl) {
                    statusEl.textContent = (resp && resp.error) ? String(resp.error) : 'Failed to queue traffic analysis.';
                    statusEl.className = 'analyze-traffic-status badge bg-danger text-white';
                }
            }
        }).catch(function() {
            btn.disabled = false;
            setTrustedHtml(btn, origHtml);
            if (statusEl) {
                statusEl.textContent = 'Request failed. Please try again.';
                statusEl.className = 'analyze-traffic-status badge bg-danger text-white';
            }
        });
    }

    function pollTrafficAnalysis(btn, reviewId, origBtnHtml) {
        if (!reviewId) return;
        var key = 'traffic_' + reviewId;
        if (pollTimers[key]) clearInterval(pollTimers[key]);
        var subscriptionId = btn.getAttribute('data-subscription-id');
        var statusEl = q('.analyze-traffic-status[data-subscription-id="' + subscriptionId + '"]');
        pollTimers[key] = setInterval(function() {
            apiGet('review_status', {id: reviewId}).then(function(resp) {
                if (!resp.success || !resp.data) return;
                var review = resp.data.review;
                if (!review) return;
                if (review.status === 'completed' || review.status === 'failed') {
                    clearInterval(pollTimers[key]);
                    delete pollTimers[key];
                    btn.disabled = false;
                    setTrustedHtml(btn, origBtnHtml);
                    if (statusEl) {
                        if (review.status === 'completed') {
                            statusEl.textContent = 'Analysis ready — open it from Review History below.';
                            statusEl.className = 'analyze-traffic-status badge bg-success text-white';
                        } else {
                            statusEl.textContent = 'Analysis failed — see Review History for details.';
                            statusEl.className = 'analyze-traffic-status badge bg-danger text-white';
                        }
                    }
                    loadReviewHistory();
                }
            });
        }, 5000);
    }

    // ========================
    // Event Bindings
    // ========================

    document.addEventListener('DOMContentLoaded', function() {
        loadIssues(1);
        loadReviewRemaining();
        loadReviewHistory();

        ['filter-severity', 'filter-site', 'filter-status'].forEach(function(id) {
            var elem = byId(id);
            if (elem) elem.addEventListener('change', function() { loadIssues(1); });
        });

        // Deep-link from email: ?review_id=N opens that review's modal;
        // ?site_id=N pre-filters the issues list to that site.
        var qs = new URLSearchParams(window.location.search);
        var deepReviewId = parseInt(qs.get('review_id') || '0', 10);
        var deepSiteId = parseInt(qs.get('site_id') || '0', 10);
        if (deepReviewId > 0) {
            setTimeout(function() { showReviewDetail(deepReviewId); }, 250);
        }
        if (deepSiteId > 0) {
            var siteFilter = byId('filter-site');
            if (siteFilter) {
                siteFilter.value = String(deepSiteId);
                loadIssues(1);
            }
        }

        document.addEventListener('click', function(e) {
            var pageLink = e.target.closest('#issues-pagination .page-link');
            if (pageLink) { e.preventDefault(); var page = parseInt(pageLink.getAttribute('data-page'), 10); if (page >= 1) loadIssues(page); return; }
            // Checkbox clicks must not bubble into the row-click handler
            // that opens the detail modal.
            if (e.target.classList.contains('issue-select')) {
                var id = parseInt(e.target.value, 10);
                if (e.target.checked) selectedIssues.add(id);
                else selectedIssues.delete(id);
                updateBulkBar();
                e.stopPropagation();
                return;
            }
            if (e.target.id === 'issues-select-all') {
                var on = e.target.checked;
                qa('.issue-select').forEach(function(cb) {
                    cb.checked = on;
                    var id = parseInt(cb.value, 10);
                    if (on) selectedIssues.add(id);
                    else selectedIssues.delete(id);
                });
                updateBulkBar();
                return;
            }
            if (e.target.closest('#bulk-fix-issues')) {
                bulkIssueAction('bulk_mark_fixed', 'Mark {n} issue(s) as fixed?');
                return;
            }
            if (e.target.closest('#bulk-ignore-issues')) {
                bulkIssueAction('bulk_mark_ignored', 'Ignore {n} issue(s) and suppress future matches?');
                return;
            }
            if (e.target.closest('#bulk-delete-issues')) {
                bulkIssueAction('bulk_delete', 'Permanently delete {n} issue(s)? This cannot be undone.');
                return;
            }
            if (e.target.closest('#bulk-clear-issues')) {
                clearIssueSelection();
                return;
            }
            var issueRow = e.target.closest('.issue-row');
            if (issueRow) {
                // Ignore clicks on the checkbox cell (handled above).
                if (e.target.closest('.issue-select')) return;
                showIssueDetail(issueRow.getAttribute('data-issue-id'));
                return;
            }
            var applyFixBtn = e.target.closest('.btn-apply-fix-modal');
            if (applyFixBtn) {
                var afid = applyFixBtn.getAttribute('data-issue-id');
                if (afid) applyAutoFix(afid);
                return;
            }
            var markFixedBtn = e.target.closest('.btn-mark-fixed-modal');
            if (markFixedBtn) {
                var fid = markFixedBtn.getAttribute('data-issue-id');
                if (fid) markIssue('mark_fixed', fid);
                return;
            }
            var markIgnoredBtn = e.target.closest('.btn-mark-ignored-modal');
            if (markIgnoredBtn) {
                var iid = markIgnoredBtn.getAttribute('data-issue-id');
                if (!iid) return;
                // Silent ignore: always suppress future matches, no prompt.
                // User can un-suppress from the Suppressed Alerts panel.
                markIssueIgnored(iid, true, '');
                return;
            }
            var ignoreSimilarBtn = e.target.closest('.btn-ignore-similar-modal');
            if (ignoreSimilarBtn) {
                var simId = ignoreSimilarBtn.getAttribute('data-issue-id');
                if (!simId) return;
                var why = window.prompt(
                    'Create an ignore rule for issues with the same title/category for your account.\n\n' +
                    'Reason (required) - why are you ignoring these?');
                if (why === null) return;
                why = why.trim();
                if (!why) {
                    alert('A reason is required.');
                    return;
                }
                apiPost('create_user_ignore_rule', {issue_id: simId, reason: why}).then(function(resp) {
                    if (resp && resp.success) {
                        alert('Ignore rule created (id ' + resp.data.rule_id + '). Future matching findings will be suppressed.');
                        var m = bootstrap.Modal.getInstance(byId('issueDetailModal'));
                        if (m) m.hide();
                        loadIssues(currentIssuesPage);
                    } else {
                        alert('Failed: ' + (resp && resp.error ? resp.error : 'unknown error'));
                    }
                });
                return;
            }
            var explainBtn = e.target.closest('.btn-explain-modal');
            if (explainBtn) {
                var eid = explainBtn.getAttribute('data-issue-id');
                if (!eid) return;
                explainIssue(eid, explainBtn);
                return;
            }
            var reviewBtn = e.target.closest('.btn-request-review');
            if (reviewBtn) { requestReview(reviewBtn.getAttribute('data-site-id')); return; }
            var viewReviewBtn = e.target.closest('.btn-view-review');
            if (viewReviewBtn) { showReviewDetail(viewReviewBtn.getAttribute('data-review-id')); return; }
            var deleteReviewBtn = e.target.closest('.btn-delete-review');
            if (deleteReviewBtn) {
                if (confirm('Delete this review? This cannot be undone.')) {
                    deleteReview(deleteReviewBtn.getAttribute('data-review-id'));
                }
                return;
            }
            var saveNotiBtn = e.target.closest('.btn-save-notification');
            if (saveNotiBtn) { saveNotificationEmail(saveNotiBtn.getAttribute('data-subscription-id')); return; }
            var analyzeBtn = e.target.closest('.btn-analyze-traffic');
            if (analyzeBtn) {
                requestTrafficAnalysis(analyzeBtn);
                return;
            }
            var subCard = e.target.closest('.subscription-card');
            if (subCard) {
                var filterSite = byId('filter-site');
                if (filterSite) filterSite.value = subCard.getAttribute('data-site-id');
                loadIssues(1);
                var issuesTable = byId('issues-table');
                if (issuesTable) issuesTable.scrollIntoView({behavior: 'smooth', block: 'start'});
                return;
            }
        });

        document.addEventListener('keydown', function(e) {
            if (e.key === 'Enter' && e.target.closest('.notification-email')) {
                e.preventDefault();
                saveNotificationEmail(e.target.getAttribute('data-subscription-id'));
            }
        });

        var reviewSiteSelect = byId('review-history-site');
        if (reviewSiteSelect) reviewSiteSelect.addEventListener('change', function() { loadReviewHistory(); });
    });

    window.addEventListener('beforeunload', function() {
        for (var key in pollTimers) clearInterval(pollTimers[key]);
    });

    // Cross-IIFE handles for the single-domain orchestrator: it needs to
    // re-trigger the issues table and review history when the active
    // domain changes. The two log-review helpers are no-ops outside the
    // main IIFE without these.
    window.WHPDashboard = {
        loadIssues: loadIssues,
        loadReviewHistory: loadReviewHistory,
        loadReviewRemaining: loadReviewRemaining
    };
})();

// =====================================================================
// Overview cards: search, pagination (5 per page), and on-screen async
// recheck. Cached site-health columns can be hours/days stale; this IIFE
// patches them in place as soon as a card scrolls into view.
// =====================================================================
(function() {
    var container = document.getElementById('overview-cards-container');
    if (!container) return;

    var searchInput  = document.getElementById('overview-card-search');
    var countLabel   = document.getElementById('overview-card-count');
    var paginationEl = document.getElementById('overview-pagination');
    var emptyEl      = document.getElementById('overview-cards-empty');
    var paginationUl = paginationEl ? paginationEl.querySelector('ul') : null;
    var csrfToken    = <?php echo json_encode($csrf_token); ?>;

    var pageSize = parseInt(container.getAttribute('data-page-size'), 10) || 5;
    var allCards = [].slice.call(container.querySelectorAll('.overview-card-wrapper'));
    var filteredCards = allCards.slice();
    var currentPage = 1;
    var rechecked = Object.create(null);  // subscription_id -> true after recheck completes

    var palette = {
        ok: '#198754', expiring: '#fd7e14', expired: '#dc3545',
        fail: '#dc3545', mismatch: '#dc3545', nxdomain: '#dc3545',
        unreachable: '#dc3545', cdn: '#0d6efd',
        unparsed: '#6c757d', skipped: '#6c757d', unknown: '#6c757d'
    };
    var httpLabels  = { ok: 'OK', fail: 'Failing', unreachable: 'Unreachable', unknown: 'Pending' };
    var dnsLabels   = { ok: 'OK', cdn: 'Behind CDN', mismatch: 'Mismatch', nxdomain: 'No DNS', skipped: 'Skipped', unknown: 'Pending' };
    var staticLabels = { unknown: 'Pending', skipped: 'Skipped', unparsed: 'Unparsed', expired: 'Expired' };

    function expiryValue(expiresAt, status) {
        if (!expiresAt) return staticLabels[status] || (status ? status.charAt(0).toUpperCase() + status.slice(1) : '');
        var ts = Date.parse(expiresAt.replace(' ', 'T') + 'Z');
        if (isNaN(ts)) return staticLabels[status] || status;
        var days = Math.floor((ts - Date.now()) / 86400000);
        if (days < 0)   return Math.abs(days) + 'd ago';
        if (days === 0) return 'today';
        if (days <= 90) return days + ' days';
        var d = new Date(ts);
        return d.toLocaleString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
    }

    function colorFor(status) { return palette[status] || '#6c757d'; }

    function updateIndicator(card, kind, status, value) {
        var row = card.querySelector('.whp-health-row[data-health-kind="' + kind + '"]');
        if (!row) return;
        var dot = row.querySelector('.whp-health-dot');
        var val = row.querySelector('.whp-health-value');
        var color = colorFor(status);
        if (dot) dot.style.background = color;
        if (val) {
            val.textContent = value;
            val.title = value;
            val.style.color = color;
        }
    }

    function setRefreshState(card, state) {
        var ind = card.querySelector('.whp-health-refresh-indicator');
        if (!ind) return;
        ind.setAttribute('data-state', state);
        if (state === 'loading')      ind.textContent = '⟳ checking...';
        else if (state === 'updated') ind.textContent = '✓ updated';
        else if (state === 'failed')  ind.textContent = '× failed';
        else                          ind.textContent = '';
        if (state === 'updated') {
            setTimeout(function() {
                if (ind.getAttribute('data-state') === 'updated') {
                    ind.setAttribute('data-state', 'idle');
                    ind.textContent = '';
                }
            }, 2500);
        }
    }

    function applyHealthPayload(card, data) {
        if (!data) return;
        var http = data.http_status || 'unknown';
        updateIndicator(card, 'http', http, httpLabels[http] || (http.charAt(0).toUpperCase() + http.slice(1)));

        var ssl = data.ssl_status || 'unknown';
        updateIndicator(card, 'ssl', ssl, expiryValue(data.ssl_expires_at, ssl));

        var dns = data.dns_status || 'unknown';
        updateIndicator(card, 'dns', dns, dnsLabels[dns] || (dns.charAt(0).toUpperCase() + dns.slice(1)));

        var whois = data.whois_status || 'unknown';
        updateIndicator(card, 'whois', whois, expiryValue(data.whois_expires_at, whois));
    }

    function rechecksub(card) {
        var subCard = card.querySelector('.subscription-card');
        if (!subCard) return;
        var subId = subCard.getAttribute('data-subscription-id');
        if (!subId || rechecked[subId]) return;
        rechecked[subId] = true;  // mark in-flight; prevents duplicate fires per pageload

        setRefreshState(subCard, 'loading');
        fetch('api/ai-monitor-customer.php?action=recheck_health', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken },
            body: JSON.stringify({ subscription_id: parseInt(subId, 10) })
        }).then(function(r) { return r.json(); }).then(function(resp) {
            if (!resp || !resp.success) {
                setRefreshState(subCard, 'failed');
                return;
            }
            applyHealthPayload(subCard, resp.data);
            setRefreshState(subCard, resp.cached ? 'idle' : 'updated');
        }).catch(function() {
            setRefreshState(subCard, 'failed');
        });
    }

    var observer = null;
    function ensureObserver() {
        if (observer || !('IntersectionObserver' in window)) return;
        observer = new IntersectionObserver(function(entries) {
            entries.forEach(function(entry) {
                if (entry.isIntersecting) {
                    rechecksub(entry.target);
                    observer.unobserve(entry.target);
                }
            });
        }, { root: null, threshold: 0.25 });
    }

    function observeVisibleCards() {
        ensureObserver();
        if (!observer) {
            // No IntersectionObserver support — just trigger every visible card.
            allCards.forEach(function(card) {
                if (!card.classList.contains('d-none')) rechecksub(card);
            });
            return;
        }
        allCards.forEach(function(card) {
            if (!card.classList.contains('d-none')) {
                var subCard = card.querySelector('.subscription-card');
                var subId = subCard && subCard.getAttribute('data-subscription-id');
                if (subId && !rechecked[subId]) observer.observe(card);
            }
        });
    }

    function renderPage() {
        var totalPages = Math.max(1, Math.ceil(filteredCards.length / pageSize));
        if (currentPage > totalPages) currentPage = totalPages;
        if (currentPage < 1) currentPage = 1;
        var start = (currentPage - 1) * pageSize;
        var end   = start + pageSize;

        // Hide all, then reveal current-page slice of the filtered set.
        allCards.forEach(function(card) { card.classList.add('d-none'); });
        var slice = filteredCards.slice(start, end);
        slice.forEach(function(card) { card.classList.remove('d-none'); });

        if (countLabel) {
            if (filteredCards.length === 0) {
                countLabel.textContent = '';
            } else if (filteredCards.length <= pageSize) {
                countLabel.textContent = filteredCards.length + ' site' + (filteredCards.length === 1 ? '' : 's');
            } else {
                countLabel.textContent = (start + 1) + '–' + Math.min(end, filteredCards.length) + ' of ' + filteredCards.length;
            }
        }
        if (emptyEl) emptyEl.classList.toggle('d-none', filteredCards.length !== 0);
        renderPagination(totalPages);
        observeVisibleCards();
    }

    function makePageItem(label, page, opts) {
        opts = opts || {};
        var li = document.createElement('li');
        li.className = 'page-item' + (opts.disabled ? ' disabled' : '') + (opts.active ? ' active' : '');
        var a  = document.createElement('a');
        a.className = 'page-link';
        a.href = '#';
        a.textContent = label;
        if (opts.ariaLabel) a.setAttribute('aria-label', opts.ariaLabel);
        a.addEventListener('click', function(e) {
            e.preventDefault();
            if (opts.disabled || opts.active) return;
            currentPage = page;
            renderPage();
        });
        li.appendChild(a);
        return li;
    }

    function renderPagination(totalPages) {
        if (!paginationEl || !paginationUl) return;
        paginationUl.textContent = '';
        if (totalPages <= 1) {
            paginationEl.classList.add('d-none');
            return;
        }
        paginationEl.classList.remove('d-none');
        paginationUl.appendChild(makePageItem('‹', currentPage - 1, { disabled: currentPage === 1, ariaLabel: 'Previous' }));
        // Compact window: always show 1, current ±1, last; ellipsis between gaps.
        var pages = [];
        for (var i = 1; i <= totalPages; i++) {
            if (i === 1 || i === totalPages || Math.abs(i - currentPage) <= 1) pages.push(i);
            else if (pages[pages.length - 1] !== '…') pages.push('…');
        }
        pages.forEach(function(p) {
            if (p === '…') {
                var li = document.createElement('li');
                li.className = 'page-item disabled';
                var span = document.createElement('span');
                span.className = 'page-link';
                span.textContent = '…';
                li.appendChild(span);
                paginationUl.appendChild(li);
            } else {
                paginationUl.appendChild(makePageItem(String(p), p, { active: p === currentPage }));
            }
        });
        paginationUl.appendChild(makePageItem('›', currentPage + 1, { disabled: currentPage === totalPages, ariaLabel: 'Next' }));
    }

    function applyFilter() {
        var q = (searchInput && searchInput.value || '').trim().toLowerCase();
        if (q === '') {
            filteredCards = allCards.slice();
        } else {
            filteredCards = allCards.filter(function(card) {
                var hay = (card.getAttribute('data-search-text') || '');
                return hay.indexOf(q) !== -1;
            });
        }
        currentPage = 1;
        renderPage();
    }

    if (searchInput) {
        var debounceTimer = null;
        searchInput.addEventListener('input', function() {
            clearTimeout(debounceTimer);
            debounceTimer = setTimeout(applyFilter, 120);
        });
    }

    renderPage();

    // Single-domain orchestrator hook: when the picker reveals a card,
    // we want the IntersectionObserver path to fire for it (or, if it's
    // already visible by virtue of all-but-one being hidden, just trigger
    // recheck directly). observeVisibleCards() handles both cases by
    // skipping cards already-rechecked.
    window.WHPOverview = {
        observeVisibleCards: observeVisibleCards,
        rechecksub: rechecksub
    };
})();

(function() {
    var suppBody = document.getElementById('suppressionsBody');
    if (!suppBody) return;

    var csrfToken = <?php echo json_encode($csrf_token); ?>;

    function makeCell(text) {
        var td = document.createElement('td');
        td.textContent = text != null ? String(text) : '';
        return td;
    }

    function loadSuppressions() {
        // Single-domain mode: only load when a domain is active. Without a
        // selection, the panel is hidden entirely; bail before issuing the
        // (now necessarily site-scoped) query.
        var activeSiteId = (window.WHPDomainPicker && window.WHPDomainPicker.getActiveSiteId)
            ? window.WHPDomainPicker.getActiveSiteId() : 0;
        if (!activeSiteId) return;
        var url = 'api/ai-monitor-customer.php?action=list_suppressions&site_id=' + encodeURIComponent(activeSiteId);
        fetch(url)
            .then(function(r) { return r.json(); })
            .then(function(data) {
                var tbody = document.getElementById('suppressionsTableBody');
                tbody.textContent = '';
                if (!data.success) {
                    var errRow = document.createElement('tr');
                    var errCell = document.createElement('td');
                    errCell.colSpan = 6;
                    errCell.className = 'text-danger';
                    errCell.textContent = 'Failed to load';
                    errRow.appendChild(errCell);
                    tbody.appendChild(errRow);
                    return;
                }
                var rows = (data.data && data.data.suppressions) || [];
                if (rows.length === 0) {
                    var noneRow = document.createElement('tr');
                    var noneCell = document.createElement('td');
                    noneCell.colSpan = 6;
                    noneCell.className = 'text-muted';
                    noneCell.textContent = 'None.';
                    noneRow.appendChild(noneCell);
                    tbody.appendChild(noneRow);
                    return;
                }
                rows.forEach(function(s) {
                    var tr = document.createElement('tr');
                    tr.appendChild(makeCell(s.primary_domain || 'Server-wide'));
                    var issueCell = document.createElement('td');
                    if (s.issue_title) {
                        issueCell.textContent = s.issue_title;
                        if (s.issue_category) {
                            var cat = document.createElement('small');
                            cat.className = 'text-muted d-block';
                            cat.textContent = s.issue_category;
                            issueCell.appendChild(cat);
                        }
                    } else {
                        var code = document.createElement('code');
                        code.title = 'signature';
                        code.textContent = (s.signature || '').slice(0, 12) + '…';
                        issueCell.appendChild(code);
                    }
                    tr.appendChild(issueCell);
                    tr.appendChild(makeCell(s.reason || ''));
                    tr.appendChild(makeCell(s.suppressed_at));
                    tr.appendChild(makeCell(String(s.suppressed_occurrence_count || 0)));
                    var actionCell = document.createElement('td');
                    var unsuppBtn = document.createElement('button');
                    unsuppBtn.className = 'btn btn-xs btn-outline-danger btn-unsuppress';
                    unsuppBtn.setAttribute('data-id', parseInt(s.id, 10));
                    unsuppBtn.textContent = 'Un-suppress';
                    actionCell.appendChild(unsuppBtn);
                    tr.appendChild(actionCell);
                    tbody.appendChild(tr);
                });
            });
    }

    suppBody.addEventListener('shown.bs.collapse', loadSuppressions);

    // Expose for the domain picker orchestrator: when domain switches and
    // the suppressions panel is currently open, refresh in-place.
    window.WHPSuppressions = {
        reload: loadSuppressions,
        isOpen: function() { return suppBody.classList.contains('show'); }
    };

    document.addEventListener('click', function(e) {
        var btn = e.target.closest('.btn-unsuppress');
        if (!btn) return;
        var sid = parseInt(btn.getAttribute('data-id'), 10);
        if (!sid) return;
        if (!confirm('Un-suppress this signature? Future matching events will alert you again.')) return;
        fetch('api/ai-monitor-customer.php?action=un_suppress', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken },
            body: JSON.stringify({ suppression_id: sid })
        }).then(function(r) { return r.json(); }).then(function(data) {
            if (!data.success) { alert('Failed: ' + (data.error || 'unknown')); return; }
            loadSuppressions();
        });
    });
})();

// =====================================================================
// Single-Domain Picker + Section Orchestrator
// ---------------------------------------------------------------------
// The page renders all subscriptions' cards/rows server-side; this IIFE
// hides everything until the user picks a domain, then reveals each
// section filtered to that one subscription. URL param `?sub=N` is
// the source of truth (read on load, updated via replaceState on change).
//
// The picker itself is a self-contained combobox: input + listbox with
// arrow-key navigation, Enter-to-select, Esc-to-close, ARIA wired up.
// =====================================================================
(function() {
    var picker        = document.getElementById('whp-domain-picker');
    if (!picker) return;
    var input         = document.getElementById('whp-domain-picker-input');
    var listbox       = document.getElementById('whp-domain-picker-listbox');
    var emptyOpt      = document.getElementById('whp-domain-picker-empty');
    var clearBtn      = document.getElementById('whp-domain-picker-clear');
    var hint          = document.getElementById('whp-domain-picker-hint');
    var allOptions    = Array.prototype.slice.call(listbox.querySelectorAll('.whp-picker-option'));
    var visibleOpts   = allOptions.slice();
    var activeIndex   = -1;   // index into visibleOpts (keyboard highlight)
    var selectedId    = 0;    // subscription id currently selected, 0 = none
    var selectedSiteId = 0;
    var isOpen        = false;

    // ------------------------------------------------------------------
    // Section visibility helpers
    // ------------------------------------------------------------------

    // Show/hide entire section blocks based on whether a domain is selected.
    function setSectionsVisible(visible) {
        document.querySelectorAll('.whp-domain-section').forEach(function(sec) {
            if (visible) sec.removeAttribute('hidden');
            else sec.setAttribute('hidden', '');
        });
    }

    // Apply single-domain filter to each section in place. No reload.
    function applyDomainFilter(option) {
        var subscriptionId = parseInt(option.getAttribute('data-subscription-id'), 10) || 0;
        var siteId         = parseInt(option.getAttribute('data-site-id'), 10) || 0;
        if (!subscriptionId || !siteId) return;

        selectedId     = subscriptionId;
        selectedSiteId = siteId;

        setSectionsVisible(true);

        // --- Overview section: hide search/pagination/count, show only the
        //     matching card.
        var overviewSearch = document.getElementById('overview-card-search');
        var overviewPag    = document.getElementById('overview-pagination');
        var overviewCount  = document.getElementById('overview-card-count');
        var overviewEmpty  = document.getElementById('overview-cards-empty');
        if (overviewSearch) overviewSearch.classList.add('d-none');
        if (overviewPag)    overviewPag.classList.add('d-none');
        if (overviewCount)  overviewCount.classList.add('d-none');
        if (overviewEmpty)  overviewEmpty.classList.add('d-none');
        document.querySelectorAll('.overview-card-wrapper').forEach(function(wrap) {
            var inner = wrap.querySelector('.subscription-card');
            var match = inner && parseInt(inner.getAttribute('data-subscription-id'), 10) === subscriptionId;
            wrap.classList.toggle('d-none', !match);
        });
        // Re-trigger recheck-on-visible for the now-visible card.
        if (window.WHPOverview && window.WHPOverview.observeVisibleCards) {
            window.WHPOverview.observeVisibleCards();
        }

        // --- Issues section: pre-select the site filter, hide the select,
        //     load issues for that site.
        var filterSite = document.getElementById('filter-site');
        if (filterSite) {
            filterSite.value = String(siteId);
            filterSite.classList.add('d-none');
        }
        if (window.WHPDashboard && window.WHPDashboard.loadIssues) {
            window.WHPDashboard.loadIssues(1);
        }

        // --- Log Review section: show only matching review-request card,
        //     hide the traffic-panels search/prev/next header row, show
        //     only the matching .traffic-panel, hide review-history-site.
        document.querySelectorAll('.btn-request-review').forEach(function(btn) {
            // Walk up to the per-site review request card column wrapper.
            // The markup uses Bootstrap col-md-4 col-lg-3 — match either.
            var card = btn.closest('[class*="col-"]');
            if (!card) return;
            var match = parseInt(btn.getAttribute('data-subscription-id'), 10) === subscriptionId;
            card.classList.toggle('d-none', !match);
        });
        // Hide the entire traffic-panels paginator/search header row.
        var trafficSearch = document.getElementById('traffic-panels-search');
        var trafficHeader = trafficSearch
            ? trafficSearch.closest('.d-flex.justify-content-between')
            : null;
        if (trafficHeader) trafficHeader.classList.add('d-none');
        var trafficEmpty  = document.getElementById('traffic-panels-empty');
        if (trafficEmpty) trafficEmpty.classList.add('d-none');
        document.querySelectorAll('.traffic-panel').forEach(function(panel) {
            // Panels carry data-subscription-id and/or data-site-id; prefer
            // the more specific match.
            var psid = parseInt(panel.getAttribute('data-subscription-id'), 10);
            var psite = parseInt(panel.getAttribute('data-site-id'), 10);
            var match = (psid && psid === subscriptionId) || (psite && psite === siteId);
            panel.style.display = match ? '' : 'none';
        });
        var reviewSiteSel = document.getElementById('review-history-site');
        if (reviewSiteSel) {
            reviewSiteSel.value = String(siteId);
            // Hide the select's containing column to drop the empty row.
            var col = reviewSiteSel.closest('.col-auto') || reviewSiteSel;
            col.classList.add('d-none');
        }
        if (window.WHPDashboard && window.WHPDashboard.loadReviewHistory) {
            window.WHPDashboard.loadReviewHistory();
        }
        if (window.WHPDashboard && window.WHPDashboard.loadReviewRemaining) {
            window.WHPDashboard.loadReviewRemaining();
        }

        // --- Notifications: show only the matching notification-row.
        document.querySelectorAll('.notification-row').forEach(function(row) {
            var match = parseInt(row.getAttribute('data-subscription-id'), 10) === subscriptionId;
            row.classList.toggle('d-none', !match);
        });

        // --- Suppressions: panel becomes visible (still collapsed); when
        //     it's already open, re-load filtered to the active site.
        if (window.WHPSuppressions && window.WHPSuppressions.isOpen && window.WHPSuppressions.isOpen()) {
            window.WHPSuppressions.reload();
        }

        // Hide the "select a domain" hint once a selection exists.
        if (hint) hint.hidden = true;

        // URL persistence: ?sub=<id>, no reload.
        try {
            var url = new URL(window.location.href);
            url.searchParams.set('sub', String(subscriptionId));
            window.history.replaceState({}, '', url.toString());
        } catch (err) { /* IE/old-Safari URL ctor — silently ignore */ }
    }

    // ------------------------------------------------------------------
    // Picker UI
    // ------------------------------------------------------------------

    function open() {
        if (isOpen) return;
        listbox.hidden = false;
        input.setAttribute('aria-expanded', 'true');
        isOpen = true;
        if (visibleOpts.length > 0 && activeIndex < 0) {
            // Highlight the selected option if visible, else first.
            activeIndex = 0;
            for (var i = 0; i < visibleOpts.length; i++) {
                if (parseInt(visibleOpts[i].getAttribute('data-subscription-id'), 10) === selectedId) {
                    activeIndex = i; break;
                }
            }
            updateActiveOption();
        }
    }

    function close() {
        if (!isOpen) return;
        listbox.hidden = true;
        input.setAttribute('aria-expanded', 'false');
        input.removeAttribute('aria-activedescendant');
        isOpen = false;
        clearActiveHighlight();
        activeIndex = -1;
    }

    function clearActiveHighlight() {
        allOptions.forEach(function(o) { o.classList.remove('is-active'); });
    }

    function updateActiveOption() {
        clearActiveHighlight();
        if (activeIndex < 0 || activeIndex >= visibleOpts.length) {
            input.removeAttribute('aria-activedescendant');
            return;
        }
        var opt = visibleOpts[activeIndex];
        opt.classList.add('is-active');
        input.setAttribute('aria-activedescendant', opt.id);
        // Keep the active option in view inside the scrollable listbox.
        var optTop    = opt.offsetTop;
        var optBottom = optTop + opt.offsetHeight;
        if (optTop < listbox.scrollTop) {
            listbox.scrollTop = optTop;
        } else if (optBottom > listbox.scrollTop + listbox.clientHeight) {
            listbox.scrollTop = optBottom - listbox.clientHeight;
        }
    }

    function applyFilterText(q) {
        q = (q || '').trim().toLowerCase();
        visibleOpts = [];
        allOptions.forEach(function(opt) {
            var hay = opt.getAttribute('data-search') || '';
            var match = q === '' || hay.indexOf(q) !== -1;
            opt.hidden = !match;
            if (match) visibleOpts.push(opt);
        });
        if (visibleOpts.length === 0) {
            emptyOpt.hidden = false;
            activeIndex = -1;
        } else {
            emptyOpt.hidden = true;
            activeIndex = 0;
        }
        updateActiveOption();
    }

    function selectOption(opt) {
        if (!opt) return;
        // Mark selected state across all options.
        allOptions.forEach(function(o) {
            var sel = (o === opt);
            o.classList.toggle('is-selected', sel);
            o.setAttribute('aria-selected', sel ? 'true' : 'false');
        });
        // Display the chosen domain in the input.
        var domain   = opt.getAttribute('data-domain') || '';
        input.value = domain;
        if (clearBtn) clearBtn.hidden = false;
        applyDomainFilter(opt);
        close();
    }

    function clearSelection() {
        selectedId     = 0;
        selectedSiteId = 0;
        input.value    = '';
        if (clearBtn) clearBtn.hidden = true;
        allOptions.forEach(function(o) {
            o.classList.remove('is-selected');
            o.setAttribute('aria-selected', 'false');
        });
        applyFilterText('');
        setSectionsVisible(false);
        if (hint) hint.hidden = false;
        // Drop the URL param.
        try {
            var url = new URL(window.location.href);
            url.searchParams.delete('sub');
            window.history.replaceState({}, '', url.toString());
        } catch (err) {}
        input.focus();
    }

    // ------------------------------------------------------------------
    // Wiring
    // ------------------------------------------------------------------

    input.addEventListener('focus', function() {
        applyFilterText(input.value);
        open();
    });

    input.addEventListener('input', function() {
        applyFilterText(input.value);
        open();
    });

    input.addEventListener('keydown', function(e) {
        if (e.key === 'ArrowDown') {
            e.preventDefault();
            if (!isOpen) { open(); return; }
            if (visibleOpts.length === 0) return;
            activeIndex = (activeIndex + 1) % visibleOpts.length;
            updateActiveOption();
        } else if (e.key === 'ArrowUp') {
            e.preventDefault();
            if (!isOpen) { open(); return; }
            if (visibleOpts.length === 0) return;
            activeIndex = (activeIndex - 1 + visibleOpts.length) % visibleOpts.length;
            updateActiveOption();
        } else if (e.key === 'Enter') {
            if (isOpen && activeIndex >= 0 && activeIndex < visibleOpts.length) {
                e.preventDefault();
                selectOption(visibleOpts[activeIndex]);
            }
        } else if (e.key === 'Escape') {
            if (isOpen) {
                e.preventDefault();
                close();
            }
        } else if (e.key === 'Home') {
            if (isOpen && visibleOpts.length > 0) {
                e.preventDefault();
                activeIndex = 0;
                updateActiveOption();
            }
        } else if (e.key === 'End') {
            if (isOpen && visibleOpts.length > 0) {
                e.preventDefault();
                activeIndex = visibleOpts.length - 1;
                updateActiveOption();
            }
        }
    });

    listbox.addEventListener('mousedown', function(e) {
        // mousedown (not click) so the input doesn't blur first.
        var opt = e.target.closest('.whp-picker-option');
        if (!opt) return;
        e.preventDefault();
        selectOption(opt);
    });

    listbox.addEventListener('mouseover', function(e) {
        var opt = e.target.closest('.whp-picker-option');
        if (!opt) return;
        var idx = visibleOpts.indexOf(opt);
        if (idx >= 0) {
            activeIndex = idx;
            updateActiveOption();
        }
    });

    if (clearBtn) {
        clearBtn.addEventListener('click', function(e) {
            e.preventDefault();
            clearSelection();
        });
    }

    document.addEventListener('mousedown', function(e) {
        if (!picker.contains(e.target)) close();
    });

    input.addEventListener('blur', function() {
        // Let click handlers inside the listbox fire first.
        setTimeout(close, 100);
    });

    // ------------------------------------------------------------------
    // Public API for cross-IIFE callers (suppressions filter etc.)
    // ------------------------------------------------------------------
    window.WHPDomainPicker = {
        getActiveSubscriptionId: function() { return selectedId; },
        getActiveSiteId:         function() { return selectedSiteId; }
    };

    // ------------------------------------------------------------------
    // Initial state: read ?sub=<id> from URL, apply if valid.
    // ------------------------------------------------------------------
    function init() {
        setSectionsVisible(false);
        try {
            var qs    = new URLSearchParams(window.location.search);
            var subId = parseInt(qs.get('sub') || '0', 10);
            if (subId > 0) {
                var match = null;
                for (var i = 0; i < allOptions.length; i++) {
                    if (parseInt(allOptions[i].getAttribute('data-subscription-id'), 10) === subId) {
                        match = allOptions[i]; break;
                    }
                }
                if (match) selectOption(match);
            }
        } catch (err) { /* legacy browsers — leave unselected */ }
    }

    // Run after the other IIFEs have wired up their window.WHP* objects so
    // applyDomainFilter can call into them on initial load.
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        // DOMContentLoaded already fired (the main IIFE binds inside it,
        // and we're inline at the bottom). Defer one tick so the main
        // IIFE's listener finishes wiring window.WHPDashboard first.
        setTimeout(init, 0);
    }
})();
</script>
