<?php
/**
 * Plattr — Public status page (onplattr.com/status).
 *
 * Renders the colour-coded service catalogue + open/past incidents
 * managed from CC's Overview → Status admin. All data lives in the
 * shared `plattr` DB; landing connects across via the existing
 * connectDB() helper.
 *
 * Subscribe button is a placeholder until the SMTP/email-templates
 * piece lands in CC — the click handler just opens a small "soon"
 * tooltip rather than wiring a form to nothing.
 */

require_once __DIR__ . '/../config.php';

// Pull from CC's plattr DB rather than this env's own plattr_{dev,uat}.
// One source of truth for status; envs share it.
try {
    $db = connectDB('plattr');
} catch (Throwable $e) {
    http_response_code(503);
    echo '<!doctype html><meta charset="utf-8"><title>Status</title><body style="background:#08080D;color:#F0ECE2;font-family:sans-serif;padding:60px;text-align:center;">';
    echo '<h1>Status page temporarily unavailable</h1>';
    echo '<p>We could not reach the status database. Try again in a minute.</p>';
    exit;
}

/**
 * Every DATETIME in the status_* tables is UTC (DB server tz). This page's
 * PHP tz is Pacific/Auckland, so a bare strtotime() misreads DB values by
 * the NZ offset — always render through nzTime().
 */
function nzTime(?string $utc, string $fmt = 'M j, g:ia'): string {
    if (!$utc) return '';
    try {
        return (new DateTime($utc, new DateTimeZone('UTC')))
            ->setTimezone(new DateTimeZone('Pacific/Auckland'))->format($fmt);
    } catch (Throwable $e) {
        return (string)$utc;
    }
}

/** Epoch seconds for a UTC DB datetime (safe cousin of strtotime for these tables). */
function utcTs(?string $utc): int {
    if (!$utc) return 0;
    try {
        return (new DateTime($utc, new DateTimeZone('UTC')))->getTimestamp();
    } catch (Throwable $e) {
        return 0;
    }
}

/** "in 2 days" / "in 3 hours" for a future UTC datetime. */
function nzRel(?string $utc): string {
    $d = utcTs($utc) - time();
    if ($d <= 0)      return 'now';
    if ($d < 3600)    { $m = max(1, (int)floor($d / 60));  return "in $m minute"  . ($m === 1 ? '' : 's'); }
    if ($d < 86400)   { $h = (int)floor($d / 3600);        return "in $h hour"    . ($h === 1 ? '' : 's'); }
    $days = (int)floor($d / 86400);
    return "in $days day" . ($days === 1 ? '' : 's');
}

function statusDecodeAffected(?string $json): array {
    if ($json === null || $json === '') return [];
    $arr = json_decode($json, true);
    return is_array($arr) ? array_values(array_filter(array_map('strval', $arr))) : [];
}

/**
 * Operator-typed body text → tidy HTML. Trims stray indentation, collapses
 * runs of blank lines to a single paragraph gap, then converts newlines to
 * <br>. Pair with normal white-space (NOT pre-wrap — combining pre-wrap with
 * nl2br doubles every line break, which is how the page ended up with huge
 * gaps between sentences).
 */
function bodyHtml(?string $text): string {
    $text = trim((string)$text);
    $text = preg_replace('/[^\S\r\n]*\R[^\S\r\n]*/', "\n", $text); // strip per-line indent
    $text = preg_replace('/\n{3,}/', "\n\n", $text);               // max one blank line
    return nl2br(htmlspecialchars($text));
}

$nowUtc = gmdate('Y-m-d H:i:s');

$services = $db->query(
    "SELECT * FROM status_services ORDER BY sort_order, name"
)->fetchAll(PDO::FETCH_ASSOC);

// Page-level global banner, managed from CC → Status. Tolerates the
// status_page_settings migration not having landed yet.
$pageBanner = ['enabled' => false, 'style' => 'maintenance', 'title' => '', 'message' => ''];
try {
    $kv = [];
    foreach ($db->query("SELECT name, value FROM status_page_settings") as $r) {
        $kv[$r['name']] = (string)($r['value'] ?? '');
    }
    $pageBanner['style']   = in_array(($kv['banner_style'] ?? ''), ['info', 'maintenance', 'warning', 'critical'], true)
        ? $kv['banner_style'] : 'maintenance';
    $pageBanner['title']   = trim($kv['banner_title'] ?? '');
    $pageBanner['message'] = trim($kv['banner_message'] ?? '');
    $pageBanner['enabled'] = ($kv['banner_enabled'] ?? '0') === '1'
        && ($pageBanner['title'] !== '' || $pageBanner['message'] !== '');
} catch (Throwable $e) {
    // Pre-migration DB — no banner.
}

// Scheduled maintenance windows whose start hasn't arrived render under
// "Upcoming maintenance"; everything else unresolved is an active incident.
// Falls back to the legacy query while the scheduled_for migration is pending.
$hasSchedule = true;
$upcoming    = [];
try {
    $stmt = $db->prepare(
        "SELECT * FROM status_incidents
          WHERE published = 1 AND resolved_at IS NULL
            AND state = 'scheduled' AND scheduled_for IS NOT NULL
            AND scheduled_for > :now
          ORDER BY scheduled_for ASC"
    );
    $stmt->execute([':now' => $nowUtc]);
    $upcoming = $stmt->fetchAll(PDO::FETCH_ASSOC);

    $stmt = $db->prepare(
        "SELECT * FROM status_incidents
          WHERE published = 1 AND resolved_at IS NULL
            AND NOT (state = 'scheduled' AND scheduled_for IS NOT NULL AND scheduled_for > :now)
          ORDER BY started_at DESC"
    );
    $stmt->execute([':now' => $nowUtc]);
    $openIncidents = $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (Throwable $e) {
    $hasSchedule   = false;
    $openIncidents = $db->query(
        "SELECT * FROM status_incidents
          WHERE published = 1 AND resolved_at IS NULL
          ORDER BY started_at DESC"
    )->fetchAll(PDO::FETCH_ASSOC);
}

$pastIncidents = $db->query(
    "SELECT * FROM status_incidents
      WHERE published = 1 AND resolved_at IS NOT NULL
      ORDER BY resolved_at DESC
      LIMIT 20"
)->fetchAll(PDO::FETCH_ASSOC);

$updatesByIncident = [];
$allIds = array_map(fn($r) => (int)$r['id'], array_merge($openIncidents, $pastIncidents));
if ($allIds) {
    $place = implode(',', array_fill(0, count($allIds), '?'));
    $stmt = $db->prepare(
        "SELECT * FROM status_incident_updates
          WHERE incident_id IN ($place)
          ORDER BY created_at DESC"
    );
    $stmt->execute($allIds);
    foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $u) {
        $updatesByIncident[(int)$u['incident_id']][] = $u;
    }
}

// Service state → public-facing label + colour bucket.
$stateMap = [
    'operational'    => ['label' => 'Operational',         'class' => 'op',  'dot' => 'op'],
    'degraded'       => ['label' => 'Degraded performance','class' => 'wn',  'dot' => 'wn'],
    'partial_outage' => ['label' => 'Partial outage',      'class' => 'er',  'dot' => 'er'],
    'major_outage'   => ['label' => 'Major outage',        'class' => 'er',  'dot' => 'er'],
    'maintenance'    => ['label' => 'Maintenance',         'class' => 'mt',  'dot' => 'mt'],
];

$severityRank = [
    'operational'    => 0,
    'maintenance'    => 1,
    'degraded'       => 2,
    'partial_outage' => 3,
    'major_outage'   => 4,
];

// Mirror the CC's lazy schedule transitions display-side, so this page is
// truthful even when the CC hasn't been opened to persist them:
//   * window started but row still 'scheduled'  → show as in progress
//   * window ended but row not yet 'completed'  → show under history
// While a window is active, its affected services render as Maintenance
// below (unless their stored status is already worse) — and revert on
// their own the moment the window closes. No cron, no stuck colours.
$maintNowKeys = [];
if ($hasSchedule) {
    $stillOpen = [];
    foreach ($openIncidents as $inc) {
        $for   = $inc['scheduled_for']   ?? null;
        $until = $inc['scheduled_until'] ?? null;
        if ($for !== null && $until !== null && $until <= $nowUtc
            && in_array($inc['state'], ['scheduled', 'in_progress'], true)) {
            $inc['state']       = 'completed';
            $inc['started_at']  = $for;   // not creation time — real window start
            $inc['resolved_at'] = $until;
            array_unshift($pastIncidents, $inc);
            continue;
        }
        if ($for !== null && $for <= $nowUtc && $inc['state'] === 'scheduled') {
            $inc['state']      = 'in_progress';
            $inc['started_at'] = $for;
        }
        if ($for !== null && $for <= $nowUtc && ($until === null || $until > $nowUtc)
            && $inc['state'] === 'in_progress') {
            foreach (statusDecodeAffected($inc['affected_services'] ?? null) as $k) {
                $maintNowKeys[$k] = true;
            }
        }
        $stillOpen[] = $inc;
    }
    $openIncidents = $stillOpen;

    foreach ($services as &$svc) {
        if (isset($maintNowKeys[$svc['key_name']])
            && ($severityRank[$svc['status']] ?? 0) < $severityRank['maintenance']) {
            $svc['status'] = 'maintenance';
        }
    }
    unset($svc);
}

// Friendly names for affected-service chips on upcoming windows.
$svcNameByKey = [];
foreach ($services as $svc) {
    $svcNameByKey[$svc['key_name']] = $svc['name'];
}

/**
 * Build a 30-element map (oldest → today) of the worst state each service
 * was in on each of the last 30 days. Algorithm:
 *
 *   * Find the state immediately before the window started (so day 1 has
 *     a meaningful baseline even if no changes happened recently).
 *   * Walk every change in the window in order, advancing a "current" state
 *     pointer. For each day, the worst state seen during that day wins.
 *   * Days with no row for the service in history still inherit the current
 *     state — which is normally 'operational'.
 *
 * Days where the service didn't yet exist (created_at after that day) are
 * marked 'unknown' so the strip renders a muted segment rather than a
 * misleading green bar.
 */
function statusBuildHistoryStrip(PDO $db, int $serviceId, string $serviceCreatedAt, array $rank, int $days = 30): array {
    $tz       = new DateTimeZone('UTC');
    $today    = (new DateTime('today', $tz));
    $windowAt = (clone $today)->modify('-' . ($days - 1) . ' days')->setTime(0, 0);

    // Most recent change strictly before the window — establishes the
    // baseline state walking into day 1 of the strip.
    $prior = $db->prepare(
        "SELECT status FROM status_service_history
          WHERE service_id = :id AND changed_at < :cut
          ORDER BY changed_at DESC LIMIT 1"
    );
    $prior->execute([':id' => $serviceId, ':cut' => $windowAt->format('Y-m-d H:i:s')]);
    $current = (string)($prior->fetchColumn() ?: 'operational');

    // All changes during the window.
    $during = $db->prepare(
        "SELECT status, changed_at FROM status_service_history
          WHERE service_id = :id AND changed_at >= :cut
          ORDER BY changed_at"
    );
    $during->execute([':id' => $serviceId, ':cut' => $windowAt->format('Y-m-d H:i:s')]);
    $changes = $during->fetchAll(PDO::FETCH_ASSOC);

    $createdAt = $serviceCreatedAt ? new DateTime($serviceCreatedAt, $tz) : null;

    $strip = [];
    $i = 0;
    for ($d = 0; $d < $days; $d++) {
        $dayStart = (clone $windowAt)->modify("+$d days");
        $dayEnd   = (clone $dayStart)->modify('+1 day');

        // If the service didn't exist yet for the whole of this day,
        // render a muted segment rather than falsely green.
        if ($createdAt && $createdAt >= $dayEnd) {
            $strip[] = ['date' => $dayStart->format('Y-m-d'), 'state' => 'unknown'];
            continue;
        }

        $worst = $current;
        while ($i < count($changes)) {
            $rowAt = new DateTime($changes[$i]['changed_at'], $tz);
            if ($rowAt < $dayEnd) {
                if (($rank[$changes[$i]['status']] ?? 0) > ($rank[$worst] ?? 0)) {
                    $worst = $changes[$i]['status'];
                }
                $current = $changes[$i]['status'];
                $i++;
            } else {
                break;
            }
        }
        $strip[] = ['date' => $dayStart->format('Y-m-d'), 'state' => $worst];
    }
    return $strip;
}

// Build the per-service strips in a single pass.
$historyByService = [];
foreach ($services as $svc) {
    $historyByService[(int)$svc['id']] = statusBuildHistoryStrip(
        $db, (int)$svc['id'], (string)($svc['created_at'] ?? ''), $severityRank
    );
}

// An active maintenance window only exists in status_incidents — the strip is
// built from status_service_history, which never hears about it. Paint today's
// segment blue for affected services so the badge and the strip agree (and the
// uptime %, which excludes maintenance days, stays honest).
if ($maintNowKeys) {
    foreach ($services as $svc) {
        if (!isset($maintNowKeys[$svc['key_name']])) continue;
        $sid = (int)$svc['id'];
        if (empty($historyByService[$sid])) continue;
        $lastIdx = count($historyByService[$sid]) - 1;
        $seg = $historyByService[$sid][$lastIdx];
        if (($severityRank[$seg['state']] ?? 0) < $severityRank['maintenance']) {
            $historyByService[$sid][$lastIdx]['state'] = 'maintenance';
        }
    }
}

$overall = 'operational';
foreach ($services as $s) {
    if (($severityRank[$s['status']] ?? 0) > ($severityRank[$overall] ?? 0)) {
        $overall = $s['status'];
    }
}

// Header banner copy + colour
$bannerCopy = [
    'operational'    => ['title' => 'All systems operational',    'sub' => 'Everything is running normally.'],
    'maintenance'    => ['title' => 'Scheduled maintenance',      'sub' => 'A planned maintenance window is in effect.'],
    'degraded'       => ['title' => 'Some services degraded',     'sub' => 'We\'ve seen reduced performance on one or more services.'],
    'partial_outage' => ['title' => 'Partial outage in progress', 'sub' => 'A subset of services is currently unavailable.'],
    'major_outage'   => ['title' => 'Major outage in progress',   'sub' => 'We\'re working on a critical incident right now.'],
];
$banner = $bannerCopy[$overall];

// Service icon → boxicons class
$iconMap = [
    'globe'        => 'bx-globe',
    'layout'       => 'bx-layout',
    'database'     => 'bx-data',
    'shopping-bag' => 'bxs-shopping-bag',
    'mail'         => 'bx-envelope',
    'image'        => 'bx-image-alt',
    'sparkles'     => 'bx-bot',
    'shield'       => 'bx-shield-quarter',
    'server'       => 'bx-server',
];

header('Cache-Control: no-cache, no-store, must-revalidate, max-age=0');
$_host = $_SERVER['HTTP_HOST'] ?? 'onplattr.com';
$_base = ((!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http') . '://' . $_host;
?><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Plattr — Status</title>
<meta name="description" content="Real-time status of Plattr's hosting, dashboards, ordering, email, and AI services.">
<meta name="theme-color" content="#08080D">
<meta property="og:type"  content="website">
<meta property="og:title" content="Plattr — Status">
<meta property="og:description" content="Real-time status of Plattr's hosting, dashboards, ordering, email, and AI services.">
<meta property="og:image" content="<?= $_base ?>/plattr-og.jpg">
<meta property="og:url"   content="<?= $_base ?>/status">
<meta name="robots" content="index, follow">

<link rel="icon" type="image/png" href="assets/favicon/favicon-96x96.png?v=4" sizes="96x96">
<link rel="icon" type="image/svg+xml" href="assets/favicon/favicon.svg?v=4">
<link rel="shortcut icon" href="assets/favicon/favicon.ico?v=4">

<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<link href="https://unpkg.com/boxicons@2.1.4/css/boxicons.min.css" rel="stylesheet">

<style>
:root{
  --gold:#C9A96E;
  --gold-light:#DFC08A;
  --gold-bright:#E8D5A8;
  --dark:#08080D;
  --dark-surface:#0F0F16;
  --dark-card:#141420;
  --dark-card-hover:#1A1A28;
  --dark-border:#1C1C2B;
  --dark-border-strong:#2A2A40;
  --text-primary:#F0ECE2;
  --text-secondary:#8A8698;
  --text-muted:#55526A;
  --op:#34D399;          /* green — operational */
  --op-soft:rgba(52,211,153,0.12);
  --op-glow:rgba(52,211,153,0.35);
  --wn:#FBBF24;          /* yellow — warning */
  --wn-soft:rgba(251,191,36,0.13);
  --wn-glow:rgba(251,191,36,0.35);
  --er:#F87171;          /* red — error */
  --er-soft:rgba(248,113,113,0.13);
  --er-glow:rgba(248,113,113,0.35);
  --mt:#60A5FA;          /* blue — maintenance */
  --mt-soft:rgba(96,165,250,0.13);
  --mt-glow:rgba(96,165,250,0.35);
  --ease:cubic-bezier(0.16,1,0.3,1);
}

*,*::before,*::after{margin:0;padding:0;box-sizing:border-box}

html{scroll-behavior:smooth}
body{
  font-family:'Inter',-apple-system,BlinkMacSystemFont,sans-serif;
  background:var(--dark);
  color:var(--text-primary);
  -webkit-font-smoothing:antialiased;
  -moz-osx-font-smoothing:grayscale;
  min-height:100vh;
  overflow-x:hidden;
}

/* Film grain — matches the rest of the marketing site */
body::after{
  content:'';position:fixed;inset:0;
  background-image:url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.03'/%3E%3C/svg%3E");
  pointer-events:none;z-index:9999;opacity:.35;
}

/* Aurora background — subtle radial glows */
.aurora{
  position:fixed;inset:0;pointer-events:none;z-index:0;
  background:
    radial-gradient(60% 40% at 12% 0%, rgba(201,169,110,0.07) 0%, transparent 60%),
    radial-gradient(50% 35% at 90% 5%, rgba(96,165,250,0.05) 0%, transparent 60%);
}

main{position:relative;z-index:1}
.container{max-width:980px;margin:0 auto;padding:0 24px}

/* ── NAV ───────────────────────────────────────────────────────────────── */
.status-nav{
  position:sticky;top:0;z-index:30;
  backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);
  background:rgba(8,8,13,0.75);
  border-bottom:1px solid var(--dark-border);
}
.status-nav-inner{
  max-width:980px;margin:0 auto;padding:16px 24px;
  display:flex;align-items:center;justify-content:space-between;gap:16px;
}
.status-nav-logo{
  display:inline-flex;align-items:center;gap:12px;
  color:var(--text-primary);text-decoration:none;font-weight:700;letter-spacing:-0.01em;
}
.status-nav-logo img{height:28px;width:auto;display:block}
.status-nav-logo .sub{
  font-size:12px;color:var(--text-secondary);font-weight:500;
  padding-left:12px;border-left:1px solid var(--dark-border-strong);
}
.status-nav-right{display:flex;gap:8px;align-items:center}
.btn-subscribe{
  display:inline-flex;align-items:center;gap:6px;padding:9px 16px;border-radius:999px;
  background:linear-gradient(135deg,var(--gold),var(--gold-light));
  color:#1a1400;font-weight:600;font-size:13px;text-decoration:none;
  border:none;cursor:pointer;transition:transform 0.2s var(--ease),box-shadow 0.2s var(--ease);
  box-shadow:0 6px 20px rgba(201,169,110,0.25);
}
.btn-subscribe:hover{transform:translateY(-1px);box-shadow:0 8px 24px rgba(201,169,110,0.35)}
.btn-subscribe i{font-size:16px}

/* Soon-popover (placeholder for the eventual subscribe modal) */
.soon-pop{
  position:absolute;top:calc(100% + 8px);right:0;
  background:var(--dark-card);border:1px solid var(--dark-border-strong);
  border-radius:10px;padding:14px 16px;width:280px;font-size:13px;
  color:var(--text-secondary);box-shadow:0 12px 40px rgba(0,0,0,0.5);
  opacity:0;visibility:hidden;transform:translateY(-4px);transition:all 0.2s var(--ease);
}
.soon-pop.show{opacity:1;visibility:visible;transform:translateY(0)}
.soon-pop strong{color:var(--text-primary);display:block;margin-bottom:4px}
.soon-pop-wrap{position:relative}

/* ── GLOBAL BANNER (operator-authored, CC → Status → Page banner) ─────── */
.global-banner{position:relative;z-index:2;border-bottom:1px solid;padding:18px 0}
.global-banner-inner{
  max-width:980px;margin:0 auto;padding:0 24px;
  display:flex;gap:14px;align-items:flex-start;
}
.global-banner i{font-size:24px;line-height:1.25;flex-shrink:0}
.global-banner strong{display:block;font-size:17px;letter-spacing:-0.01em;margin-bottom:2px}
.global-banner p{font-size:14px;opacity:.92;line-height:1.5}
.global-banner.info{background:linear-gradient(180deg,rgba(201,169,110,0.18),rgba(201,169,110,0.06));color:var(--gold-bright);border-color:rgba(201,169,110,0.4)}
.global-banner.maintenance{background:linear-gradient(180deg,rgba(96,165,250,0.18),rgba(96,165,250,0.05));color:var(--mt);border-color:rgba(96,165,250,0.4)}
.global-banner.warning{background:linear-gradient(180deg,rgba(251,191,36,0.18),rgba(251,191,36,0.05));color:var(--wn);border-color:rgba(251,191,36,0.4)}
.global-banner.critical{background:linear-gradient(180deg,rgba(248,113,113,0.20),rgba(248,113,113,0.06));color:var(--er);border-color:rgba(248,113,113,0.45)}

/* ── UPCOMING MAINTENANCE ──────────────────────────────────────────────── */
.upcoming-window{
  display:inline-flex;align-items:center;gap:6px;
  font-family:'JetBrains Mono',monospace;font-size:12px;
  color:var(--mt);background:var(--mt-soft);
  padding:4px 10px;border-radius:6px;white-space:nowrap;
}
.aff-chips{display:flex;flex-wrap:wrap;gap:6px;margin-top:10px}
.aff-chip{
  font-size:11px;font-weight:500;color:var(--text-secondary);
  border:1px solid var(--dark-border-strong);border-radius:999px;padding:3px 10px;
}
.upcoming-body{margin-top:12px;font-size:14px;color:var(--text-secondary);line-height:1.55}

/* ── HERO BANNER ───────────────────────────────────────────────────────── */
.status-hero{padding:64px 0 36px;text-align:center}
.status-hero-eyebrow{
  display:inline-block;font-size:11px;letter-spacing:0.22em;text-transform:uppercase;
  color:var(--gold);font-weight:600;margin-bottom:18px;
}
.status-hero-title{
  font-size:clamp(36px,5vw,52px);font-weight:800;line-height:1.05;letter-spacing:-0.025em;
  margin-bottom:14px;
}
.status-hero-sub{font-size:16px;color:var(--text-secondary);max-width:520px;margin:0 auto}

/* Big status pill in hero */
.status-overall{
  margin:32px auto 0;display:inline-flex;align-items:center;gap:14px;
  padding:16px 28px;border-radius:14px;font-size:15px;font-weight:600;
  border:1px solid;background:var(--dark-card);
}
.status-overall .pulse-dot{
  width:14px;height:14px;border-radius:50%;flex-shrink:0;
  position:relative;
}
.status-overall .pulse-dot::after{
  content:'';position:absolute;inset:-6px;border-radius:50%;
  animation:pulse 2.4s ease-out infinite;
}
@keyframes pulse{
  0%  {transform:scale(0.85);opacity:0.6}
  70% {transform:scale(1.6);opacity:0}
  100%{transform:scale(0.85);opacity:0}
}
.status-overall.op{border-color:var(--op);color:var(--op);background:linear-gradient(135deg,var(--op-soft),var(--dark-card))}
.status-overall.op .pulse-dot{background:var(--op);box-shadow:0 0 0 4px var(--op-soft)}
.status-overall.op .pulse-dot::after{background:var(--op-glow)}
.status-overall.wn{border-color:var(--wn);color:var(--wn);background:linear-gradient(135deg,var(--wn-soft),var(--dark-card))}
.status-overall.wn .pulse-dot{background:var(--wn);box-shadow:0 0 0 4px var(--wn-soft)}
.status-overall.wn .pulse-dot::after{background:var(--wn-glow)}
.status-overall.er{border-color:var(--er);color:var(--er);background:linear-gradient(135deg,var(--er-soft),var(--dark-card))}
.status-overall.er .pulse-dot{background:var(--er);box-shadow:0 0 0 4px var(--er-soft)}
.status-overall.er .pulse-dot::after{background:var(--er-glow)}
.status-overall.mt{border-color:var(--mt);color:var(--mt);background:linear-gradient(135deg,var(--mt-soft),var(--dark-card))}
.status-overall.mt .pulse-dot{background:var(--mt);box-shadow:0 0 0 4px var(--mt-soft)}
.status-overall.mt .pulse-dot::after{background:var(--mt-glow)}
.status-overall-text{display:flex;flex-direction:column;text-align:left}
.status-overall-text small{color:var(--text-secondary);font-weight:400;font-size:12px;margin-top:1px}

.last-checked{margin-top:18px;font-size:12px;color:var(--text-muted)}

/* ── SECTION ───────────────────────────────────────────────────────────── */
.section{padding:32px 0}
.section-title{
  display:flex;align-items:center;justify-content:space-between;gap:12px;
  margin-bottom:18px;
}
.section-title h2{
  font-size:14px;text-transform:uppercase;letter-spacing:0.16em;
  color:var(--text-secondary);font-weight:600;
}
.section-title .meta{font-size:12px;color:var(--text-muted)}

/* ── SERVICE LIST ──────────────────────────────────────────────────────── */
.svc-list{display:flex;flex-direction:column;gap:8px}
.svc-item{
  display:grid;grid-template-columns:auto 1fr auto;gap:16px;align-items:center;
  padding:18px 22px;background:var(--dark-card);
  border:1px solid var(--dark-border);border-radius:12px;
  transition:border-color 0.2s var(--ease),background 0.2s var(--ease);
}
.svc-item:hover{border-color:var(--dark-border-strong);background:var(--dark-card-hover)}
.svc-icon{
  width:40px;height:40px;border-radius:10px;display:grid;place-items:center;
  background:rgba(201,169,110,0.06);border:1px solid rgba(201,169,110,0.18);
  color:var(--gold);font-size:20px;
}
.svc-body .svc-name{font-weight:600;font-size:15px;letter-spacing:-0.01em}
.svc-body .svc-desc{font-size:13px;color:var(--text-secondary);margin-top:2px}
.svc-body .svc-msg{font-size:12px;color:var(--gold-light);margin-top:4px;font-style:italic}
.svc-state{
  display:inline-flex;align-items:center;gap:8px;
  font-size:12px;font-weight:600;padding:6px 12px;border-radius:999px;
  border:1px solid currentColor;background:rgba(0,0,0,0.2);
  white-space:nowrap;
}
.svc-state .dot{width:8px;height:8px;border-radius:50%;background:currentColor}
.svc-state.op{color:var(--op);background:var(--op-soft)}
.svc-state.wn{color:var(--wn);background:var(--wn-soft)}
.svc-state.er{color:var(--er);background:var(--er-soft)}
.svc-state.mt{color:var(--mt);background:var(--mt-soft)}

/* 30-day history strip — one tall segment per day, leftmost = 30 days ago. */
.svc-strip{
  grid-column:1 / -1;
  margin-top:14px;padding-top:14px;border-top:1px dashed var(--dark-border);
}
.svc-strip-bar{
  display:flex;gap:3px;align-items:stretch;width:100%;height:28px;
}
.svc-strip-seg{
  flex:1 1 0;min-width:0;border-radius:3px;
  background:var(--op);position:relative;cursor:default;
  transition:transform 0.15s var(--ease),filter 0.15s var(--ease);
}
.svc-strip-seg:hover{transform:scaleY(1.18);filter:brightness(1.2)}
.svc-strip-seg.op{background:var(--op)}
.svc-strip-seg.wn{background:var(--wn)}
.svc-strip-seg.er{background:var(--er)}
.svc-strip-seg.mt{background:var(--mt)}
.svc-strip-seg.unknown{background:rgba(255,255,255,0.06)}
/* Tooltip on hover — pure CSS, lightweight. */
.svc-strip-seg::after{
  content:attr(data-tip);
  position:absolute;bottom:calc(100% + 8px);left:50%;transform:translateX(-50%);
  background:var(--dark-card);border:1px solid var(--dark-border-strong);
  color:var(--text-primary);font-size:11px;font-weight:500;
  padding:5px 9px;border-radius:6px;white-space:nowrap;
  opacity:0;pointer-events:none;transition:opacity 0.15s var(--ease);
  font-family:'JetBrains Mono',monospace;
  box-shadow:0 6px 20px rgba(0,0,0,0.45);z-index:10;
}
.svc-strip-seg:hover::after{opacity:1}
.svc-strip-labels{
  display:flex;justify-content:space-between;
  margin-top:6px;font-size:11px;color:var(--text-muted);
  font-family:'JetBrains Mono',monospace;letter-spacing:0.04em;
}
.svc-strip-labels .uptime-pct{color:var(--text-secondary);font-weight:500}

/* ── INCIDENTS ─────────────────────────────────────────────────────────── */
.incident{
  background:var(--dark-card);border:1px solid var(--dark-border);
  border-radius:14px;padding:22px 24px;margin-bottom:14px;
}
.incident.is-open{border-color:var(--wn);background:linear-gradient(180deg,var(--wn-soft),var(--dark-card) 60%)}
.incident.is-critical{border-color:var(--er);background:linear-gradient(180deg,var(--er-soft),var(--dark-card) 60%)}
.incident.is-maintenance{border-color:var(--mt);background:linear-gradient(180deg,var(--mt-soft),var(--dark-card) 60%)}

.inc-head{display:flex;justify-content:space-between;gap:16px;align-items:flex-start;flex-wrap:wrap}
.inc-title{font-weight:700;font-size:18px;letter-spacing:-0.01em}
.inc-meta{display:flex;flex-wrap:wrap;gap:8px;font-size:12px;color:var(--text-secondary);margin-top:8px}
.inc-pill{
  display:inline-flex;align-items:center;gap:4px;padding:3px 9px;
  border-radius:999px;font-size:11px;font-weight:600;text-transform:capitalize;
  background:rgba(255,255,255,0.04);border:1px solid var(--dark-border-strong);
}
.inc-pill.critical{color:var(--er);border-color:var(--er)}
.inc-pill.major   {color:var(--wn);border-color:var(--wn)}
.inc-pill.minor   {color:var(--text-secondary)}
.inc-pill.maintenance{color:var(--mt);border-color:var(--mt)}
.inc-pill.state-resolved   {color:var(--op);border-color:var(--op)}
.inc-pill.state-completed  {color:var(--op);border-color:var(--op)}
.inc-pill.state-monitoring {color:var(--mt);border-color:var(--mt)}
.inc-pill.state-identified {color:var(--wn);border-color:var(--wn)}
.inc-pill.state-investigating{color:var(--er);border-color:var(--er)}
.inc-pill.state-scheduled  {color:var(--mt);border-color:var(--mt)}
.inc-pill.state-in_progress{color:var(--wn);border-color:var(--wn)}

.inc-timeline{margin-top:18px;border-top:1px solid var(--dark-border);padding-top:14px}
.tl-item{
  display:grid;grid-template-columns:100px 1fr;gap:18px;
  padding:10px 0;border-top:1px dashed var(--dark-border);
  font-size:14px;
}
.tl-item:first-child{border-top:none}
.tl-time{font-size:12px;color:var(--text-muted);font-family:'JetBrains Mono',monospace}
.tl-body{color:var(--text-primary);line-height:1.55}
.tl-body .state{font-weight:700;color:var(--gold);text-transform:capitalize;display:block;margin-bottom:4px;font-size:12px;letter-spacing:0.08em}

.empty-card{
  text-align:center;padding:48px 24px;border:1px dashed var(--dark-border);
  border-radius:12px;color:var(--text-secondary);
}
.empty-card .bx{font-size:32px;color:var(--op);margin-bottom:10px;display:block}
.empty-card strong{display:block;color:var(--text-primary);font-size:16px;margin-bottom:4px}

/* ── FOOTER ────────────────────────────────────────────────────────────── */
footer.status-foot{
  margin-top:64px;border-top:1px solid var(--dark-border);
  padding:28px 0 48px;color:var(--text-muted);font-size:13px;
  text-align:center;
}
footer.status-foot a{color:var(--text-secondary);text-decoration:none;margin:0 12px;transition:color 0.15s}
footer.status-foot a:hover{color:var(--gold)}

@media (max-width:720px){
  .status-overall{padding:14px 20px;gap:12px;font-size:14px}
  .status-overall-text small{display:none}
  .svc-item{grid-template-columns:auto 1fr;padding:14px 16px}
  .svc-state{grid-column:1/-1;justify-self:start}
  .tl-item{grid-template-columns:1fr}
  .tl-time{font-size:11px}
}
</style>
</head>
<body>

<div class="aurora"></div>

<header class="status-nav">
  <div class="status-nav-inner">
    <a class="status-nav-logo" href="/" aria-label="Back to Plattr">
      <img src="/plattr-logo.webp?v=4" alt="Plattr" onerror="this.style.display='none'">
      <span class="sub">Status</span>
    </a>
    <div class="status-nav-right">
      <div class="soon-pop-wrap">
        <button type="button" class="btn-subscribe" id="subscribeBtn"
                onclick="toggleSoon(event)" aria-haspopup="dialog" aria-expanded="false">
          <i class="bx bx-bell"></i>
          Subscribe to updates
        </button>
        <div class="soon-pop" id="soonPop" role="dialog" aria-label="Email subscription">
          <strong>Email updates coming soon</strong>
          We're wiring up email-template management in the operator panel first. Check back in a few days — you'll be able to subscribe with just your email here.
        </div>
      </div>
    </div>
  </div>
</header>

<?php if ($pageBanner['enabled']):
  $bannerIcon = [
      'info'        => 'bx-info-circle',
      'maintenance' => 'bx-wrench',
      'warning'     => 'bx-error',
      'critical'    => 'bx-error-circle',
  ][$pageBanner['style']];
?>
<div class="global-banner <?= htmlspecialchars($pageBanner['style']) ?>" role="status" aria-live="polite">
  <div class="global-banner-inner">
    <i class="bx <?= $bannerIcon ?>"></i>
    <div>
      <?php if ($pageBanner['title'] !== ''): ?>
        <strong><?= htmlspecialchars($pageBanner['title']) ?></strong>
      <?php endif; ?>
      <?php if ($pageBanner['message'] !== ''): ?>
        <p><?= bodyHtml($pageBanner['message']) ?></p>
      <?php endif; ?>
    </div>
  </div>
</div>
<?php endif; ?>

<main>
  <section class="status-hero container">
    <div class="status-hero-eyebrow">Service status</div>
    <h1 class="status-hero-title"><?= htmlspecialchars($banner['title']) ?></h1>
    <p class="status-hero-sub"><?= htmlspecialchars($banner['sub']) ?></p>

    <div class="status-overall <?= htmlspecialchars($stateMap[$overall]['class']) ?>">
      <span class="pulse-dot"></span>
      <span class="status-overall-text">
        <span><?= htmlspecialchars($stateMap[$overall]['label']) ?></span>
        <small>Updated <?= htmlspecialchars(date('M j, g:ia T')) ?></small>
      </span>
    </div>
    <div class="last-checked">Auto-refresh every 60 seconds</div>
  </section>

  <!-- ── CURRENT INCIDENTS (only if any) ──────────────────────────────── -->
  <?php if (!empty($openIncidents)): ?>
  <section class="section container">
    <div class="section-title">
      <h2>Active incidents</h2>
      <span class="meta"><?= count($openIncidents) ?> open</span>
    </div>
    <?php foreach ($openIncidents as $inc):
      $updates  = $updatesByIncident[(int)$inc['id']] ?? [];
      $cls = 'is-open';
      if ($inc['severity'] === 'critical') $cls = 'is-critical';
      elseif ($inc['severity'] === 'maintenance') $cls = 'is-maintenance';
    ?>
      <article class="incident <?= $cls ?>">
        <div class="inc-head">
          <div>
            <div class="inc-title"><?= htmlspecialchars($inc['title']) ?></div>
            <div class="inc-meta">
              <span class="inc-pill <?= htmlspecialchars($inc['severity']) ?>"><?= htmlspecialchars(ucfirst($inc['severity'])) ?></span>
              <span class="inc-pill state-<?= htmlspecialchars($inc['state']) ?>"><?= htmlspecialchars(str_replace('_',' ', $inc['state'])) ?></span>
              <span>Started <?= htmlspecialchars(nzTime($inc['started_at'])) ?> NZT</span>
              <?php if (!empty($inc['scheduled_until']) && $inc['scheduled_until'] > $nowUtc): ?>
                <span>Expected until <?= htmlspecialchars(nzTime($inc['scheduled_until'])) ?> NZT</span>
              <?php endif; ?>
            </div>
          </div>
        </div>

        <?php if ($updates): ?>
          <div class="inc-timeline">
            <?php foreach ($updates as $u): ?>
              <div class="tl-item">
                <div class="tl-time"><?= htmlspecialchars(nzTime($u['created_at'], 'M j g:ia')) ?></div>
                <div class="tl-body">
                  <span class="state"><?= htmlspecialchars(str_replace('_',' ', $u['state'])) ?></span>
                  <?= bodyHtml($u['body']) ?>
                </div>
              </div>
            <?php endforeach; ?>
          </div>
        <?php elseif (!empty($inc['body'])): ?>
          <div class="inc-timeline">
            <div class="tl-item">
              <div class="tl-time"><?= htmlspecialchars(nzTime($inc['started_at'], 'M j g:ia')) ?></div>
              <div class="tl-body"><?= bodyHtml($inc['body']) ?></div>
            </div>
          </div>
        <?php endif; ?>
      </article>
    <?php endforeach; ?>
  </section>
  <?php endif; ?>

  <!-- ── UPCOMING MAINTENANCE (only if any) ───────────────────────────── -->
  <?php if (!empty($upcoming)): ?>
  <section class="section container">
    <div class="section-title">
      <h2>Upcoming maintenance</h2>
      <span class="meta"><?= count($upcoming) ?> scheduled</span>
    </div>
    <?php foreach ($upcoming as $inc):
      $sameDay  = nzTime($inc['scheduled_for'], 'Y-m-d') === nzTime($inc['scheduled_until'], 'Y-m-d');
      $affected = statusDecodeAffected($inc['affected_services'] ?? null);
    ?>
      <article class="incident is-maintenance">
        <div class="inc-head">
          <div>
            <div class="inc-title"><?= htmlspecialchars($inc['title']) ?></div>
            <div class="inc-meta">
              <span class="inc-pill maintenance">Maintenance</span>
              <span class="upcoming-window">
                <i class='bx bx-calendar-event'></i>
                <?= htmlspecialchars(nzTime($inc['scheduled_for'], 'D j M, g:ia')) ?>
                → <?= htmlspecialchars(nzTime($inc['scheduled_until'], $sameDay ? 'g:ia' : 'D j M, g:ia')) ?> NZT
              </span>
              <span>Starts <?= htmlspecialchars(nzRel($inc['scheduled_for'])) ?>
                · about <?= htmlspecialchars(formatDuration(utcTs($inc['scheduled_until']) - utcTs($inc['scheduled_for']))) ?></span>
            </div>
            <?php if ($affected): ?>
              <div class="aff-chips">
                <?php foreach ($affected as $key): ?>
                  <span class="aff-chip"><?= htmlspecialchars($svcNameByKey[$key] ?? $key) ?></span>
                <?php endforeach; ?>
              </div>
            <?php endif; ?>
            <?php if (!empty($inc['body'])): ?>
              <div class="upcoming-body"><?= bodyHtml($inc['body']) ?></div>
            <?php endif; ?>
          </div>
        </div>
      </article>
    <?php endforeach; ?>
  </section>
  <?php endif; ?>

  <!-- ── SERVICE LIST ─────────────────────────────────────────────────── -->
  <section class="section container">
    <div class="section-title">
      <h2>Services</h2>
      <span class="meta">All times NZT (Pacific/Auckland)</span>
    </div>
    <div class="svc-list">
      <?php foreach ($services as $svc):
        $sm = $stateMap[$svc['status']] ?? $stateMap['operational'];
        $icon = $iconMap[$svc['icon']] ?? 'bx-server';
        $strip = $historyByService[(int)$svc['id']] ?? [];

        // Tiny stat for the strip footer: "100% uptime" when every day in the
        // window was operational (treats maintenance + unknown as neutral —
        // they don't count for or against uptime).
        $ratedDays = 0; $okDays = 0;
        foreach ($strip as $seg) {
            if ($seg['state'] === 'unknown' || $seg['state'] === 'maintenance') continue;
            $ratedDays++;
            if ($seg['state'] === 'operational') $okDays++;
        }
        $uptimePct = $ratedDays > 0 ? round(100 * $okDays / $ratedDays, $okDays === $ratedDays ? 0 : 2) : null;
      ?>
        <div class="svc-item">
          <div class="svc-icon"><i class="bx <?= htmlspecialchars($icon) ?>"></i></div>
          <div class="svc-body">
            <div class="svc-name"><?= htmlspecialchars($svc['name']) ?></div>
            <?php if (!empty($svc['description'])): ?>
              <div class="svc-desc"><?= htmlspecialchars($svc['description']) ?></div>
            <?php endif; ?>
            <?php if (!empty($svc['status_message'])): ?>
              <div class="svc-msg">&ldquo;<?= htmlspecialchars($svc['status_message']) ?>&rdquo;</div>
            <?php endif; ?>
          </div>
          <div class="svc-state <?= htmlspecialchars($sm['class']) ?>">
            <span class="dot"></span>
            <?= htmlspecialchars($sm['label']) ?>
          </div>
          <div class="svc-strip">
            <div class="svc-strip-bar" aria-label="Status for the last 30 days">
              <?php foreach ($strip as $seg):
                $segClass = $stateMap[$seg['state']]['class'] ?? 'unknown';
                $tipState = $seg['state'] === 'unknown' ? 'no data' : ($stateMap[$seg['state']]['label'] ?? $seg['state']);
                $tipDate  = date('M j', strtotime($seg['date']));
              ?>
                <div class="svc-strip-seg <?= htmlspecialchars($segClass) ?>"
                     data-tip="<?= htmlspecialchars($tipDate . ' — ' . $tipState) ?>"
                     role="img"
                     aria-label="<?= htmlspecialchars($tipDate . ': ' . $tipState) ?>"></div>
              <?php endforeach; ?>
            </div>
            <div class="svc-strip-labels">
              <span>30 days ago</span>
              <?php if ($uptimePct !== null): ?>
                <span class="uptime-pct"><?= htmlspecialchars((string)$uptimePct) ?>% uptime</span>
              <?php endif; ?>
              <span>Today</span>
            </div>
          </div>
        </div>
      <?php endforeach; ?>
    </div>
  </section>

  <!-- ── PAST INCIDENTS ───────────────────────────────────────────────── -->
  <section class="section container">
    <div class="section-title">
      <h2>Incident history</h2>
      <span class="meta">Last <?= count($pastIncidents) ?: 'few' ?></span>
    </div>
    <?php if (empty($pastIncidents) && empty($openIncidents)): ?>
      <div class="empty-card">
        <i class='bx bx-check-circle'></i>
        <strong>No incidents to report</strong>
        Smooth sailing on every service. We'll post here the moment something needs attention.
      </div>
    <?php elseif (empty($pastIncidents)): ?>
      <div class="empty-card">
        <strong>No past incidents in the recent window</strong>
        Resolved incidents appear here for transparency.
      </div>
    <?php else: ?>
      <?php foreach ($pastIncidents as $inc):
        $updates = $updatesByIncident[(int)$inc['id']] ?? [];
      ?>
        <article class="incident">
          <div class="inc-head">
            <div>
              <div class="inc-title"><?= htmlspecialchars($inc['title']) ?></div>
              <div class="inc-meta">
                <span class="inc-pill <?= htmlspecialchars($inc['severity']) ?>"><?= htmlspecialchars(ucfirst($inc['severity'])) ?></span>
                <span class="inc-pill state-<?= htmlspecialchars($inc['state']) ?>"><?= htmlspecialchars(str_replace('_',' ', $inc['state'])) ?></span>
                <span><?= htmlspecialchars(nzTime($inc['started_at'], 'M j, Y')) ?>
                  · resolved in <?= htmlspecialchars(formatDuration(max(0, utcTs($inc['resolved_at']) - utcTs($inc['started_at'])))) ?></span>
              </div>
            </div>
          </div>
          <?php if ($updates): ?>
            <div class="inc-timeline">
              <?php foreach ($updates as $u): ?>
                <div class="tl-item">
                  <div class="tl-time"><?= htmlspecialchars(nzTime($u['created_at'], 'M j g:ia')) ?></div>
                  <div class="tl-body">
                    <span class="state"><?= htmlspecialchars(str_replace('_',' ', $u['state'])) ?></span>
                    <?= bodyHtml($u['body']) ?>
                  </div>
                </div>
              <?php endforeach; ?>
            </div>
          <?php endif; ?>
        </article>
      <?php endforeach; ?>
    <?php endif; ?>
  </section>
</main>

<footer class="status-foot">
  <div class="container">
    <div>
      <a href="/">Home</a>
      <a href="/about">About</a>
      <a href="/contact">Contact</a>
      <a href="https://onplattr.com/status">Status</a>
    </div>
    <div style="margin-top:14px;">
      © <?= date('Y') ?> Plattr · Aotearoa New Zealand
    </div>
  </div>
</footer>

<script>
  function toggleSoon(ev){
    ev.preventDefault();
    const pop = document.getElementById('soonPop');
    const btn = document.getElementById('subscribeBtn');
    const show = !pop.classList.contains('show');
    pop.classList.toggle('show', show);
    btn.setAttribute('aria-expanded', String(show));
  }
  // Dismiss when clicking outside.
  document.addEventListener('click', function(e){
    if (!e.target.closest('.soon-pop-wrap')) {
      document.getElementById('soonPop').classList.remove('show');
      document.getElementById('subscribeBtn').setAttribute('aria-expanded','false');
    }
  });
  // Auto-refresh every 60s so the page reflects current state without a
  // manual reload. Skip when the tab is hidden so we don't ping forever.
  setInterval(function(){
    if (document.visibilityState === 'visible') location.reload();
  }, 60000);
</script>

</body>
</html>
<?php
/** Quick humanizer for incident durations on the past-incidents list. */
function formatDuration(int $secs): string {
    if ($secs < 60)   return $secs . 's';
    if ($secs < 3600) return floor($secs / 60) . 'm';
    if ($secs < 86400) {
        $h = floor($secs / 3600); $m = floor(($secs % 3600) / 60);
        return $m ? "{$h}h {$m}m" : "{$h}h";
    }
    $d = floor($secs / 86400); $h = floor(($secs % 86400) / 3600);
    return $h ? "{$d}d {$h}h" : "{$d}d";
}
