// status-components.jsx
// Status page UI. Loaded after React + Babel + status-data.jsx.

const { useState, useEffect, useMemo, useRef } = React;

// ─── helpers ────────────────────────────────────────────────────────────────

const STATUS_META = {
  operational: { label: 'Operational',         dot: 'var(--ok)',       fg: 'var(--ok)' },
  degraded:    { label: 'Degraded Performance', dot: 'var(--warn)',    fg: 'var(--warn)' },
  outage:      { label: 'Major Outage',         dot: 'var(--bad)',     fg: 'var(--bad)' },
};

const DAY_META = {
  '-1': { color: 'var(--bar-empty)',  label: 'No data',     status: 'no_data'     },
   '0': { color: 'var(--ok)',         label: 'Operational', status: 'operational' },
   '1': { color: 'var(--warn)',       label: 'Degraded',    status: 'degraded'    },
   '2': { color: 'var(--bad)',        label: 'Outage',      status: 'down'        },
};

// Active-incident severity → visual color. Drives the service dot and bar tint within the
// incident's time window. Uptime % and the operational counter stay probe-driven on purpose:
// an operator can declare an incident with no underlying probe failures.
const INCIDENT_META = {
  minor: { color: 'var(--warn)', label: 'Minor incident' },
  major: { color: 'var(--bad)',  label: 'Major incident' },
};

// Loss-of-signal styling. When `probesStale` is true, every service surface
// (dot, per-row uptime, headline) flips to this — distinct from any
// probe-derived state because "we don't know" is its own thing.
const STALE_META = { color: 'var(--stale)', label: 'Stale data' };

// Severity ranking shared by the incident-overlay logic for the per-service dot
// and history bars. Higher = worse. The overlay only repaints a bucket/dot when
// the incident's rank is STRICTLY greater than the probe-derived rank for that
// bucket — ties go to the probes, since they're observations and the incident
// severity is operator opinion. `no_data` ranks 0 (treated as no claim), so an
// incident overlay is allowed to fill an empty bucket. `outage` and `down` both
// rank 2 because the public payload uses `outage` for service.status while the
// per-bucket DAY_META uses `down` — same severity, two naming conventions.
//
// Not consolidated with the banner-local `rank` in OverallBanner: that table
// operates on post-merged overall states, after incident severity has already
// been mapped to a service-status word. Different layer, different inputs.
const SEVERITY_RANK = {
  operational: 0,
  no_data:     0,
  minor:       1,
  degraded:    1,
  major:       2,
  outage:      2,
  down:        2,
};

// Minimum interval covered by a single bucket of each history array — used to figure out
// which trailing buckets fall inside an active incident's [startedAt, now] window.
const RANGE_BUCKET_MS = {
  '1h':  60_000,
  '1d':  3_600_000,
  '7d':  86_400_000,
  '30d': 86_400_000,
  '90d': 86_400_000,
};

// Visuals for each overlay severity. Incident severities reuse INCIDENT_META;
// maintenance gets its own info-blue tint distinct from anything probe-derived.
const OVERLAY_META = {
  minor:       { color: 'var(--warn)', label: 'Minor incident' },
  major:       { color: 'var(--bad)',  label: 'Major incident' },
  maintenance: { color: 'var(--info)', label: 'Maintenance'    },
};
// How an overlay ranks against the probe-derived bucket state. Same rule as
// the active-incident overlay: an overlay only repaints a bucket when its
// rank is STRICTLY greater than the probe's, so a real outage is never
// visually downgraded by a maintenance announcement or a minor incident.
// Maintenance ranks the same as a minor incident (1): it overrides
// operational/no_data buckets but loses to a real outage probe.
const OVERLAY_RANK = {
  minor:       1,
  major:       2,
  maintenance: 1,
};

// Pick the worst overlay whose [startedAt, endedAt] intersects [bStart, bEnd].
// `overlays` items: { kind, severity, startedAt, endedAt|null }. Null endedAt
// is an ongoing window — treated as ending at `nowMs`.
function pickOverlayForBucket(overlays, bStart, bEnd, nowMs) {
  if (!overlays || overlays.length === 0) return null;
  let best = null;
  for (const o of overlays) {
    const oStart = new Date(o.startedAt).getTime();
    const oEnd   = o.endedAt ? new Date(o.endedAt).getTime() : nowMs;
    if (Number.isNaN(oStart) || Number.isNaN(oEnd)) continue;
    if (oStart < bEnd && oEnd > bStart) {
      const rank = OVERLAY_RANK[o.severity] ?? 0;
      if (!best || rank > best.rank) {
        best = { rank, meta: OVERLAY_META[o.severity] || OVERLAY_META.minor, kind: o.kind };
      }
    }
  }
  return best;
}

const TIME_ZONE = 'Europe/Athens';
const TZ_LABEL = 'EEST';

function relTime(iso, now) {
  const diff = (new Date(iso) - now) / 1000;
  const abs = Math.abs(diff);
  if (abs < 60)   return Math.round(abs) + 's ago';
  if (abs < 3600) return Math.round(abs / 60) + 'm ago';
  if (abs < 86400) return Math.round(abs / 3600) + 'h ago';
  return Math.round(abs / 86400) + 'd ago';
}

// App-wide date formatting. Canonical full format is `6 May, 15:30:22` for the
// current year, or `6 May 2026, 15:30:22` for other years. Date-only and
// time-only variants share the same building blocks so they stay consistent.
const _DATE_PARTS_FMT = new Intl.DateTimeFormat('en-GB', { day: 'numeric', month: 'short', year: 'numeric', timeZone: TIME_ZONE });
const _TIME_FMT       = new Intl.DateTimeFormat('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false, timeZone: TIME_ZONE });
const _YEAR_FMT       = new Intl.DateTimeFormat('en-GB', { year: 'numeric', timeZone: TIME_ZONE });
function _dateParts(iso) {
  const parts = _DATE_PARTS_FMT.formatToParts(new Date(iso));
  return {
    day:   parts.find(p => p.type === 'day').value,
    month: parts.find(p => p.type === 'month').value,
    year:  parts.find(p => p.type === 'year').value,
  };
}
function fmtDateOnly(iso) {
  if (!iso) return '—';
  const { day, month, year } = _dateParts(iso);
  return year === _YEAR_FMT.format(new Date()) ? `${day} ${month}` : `${day} ${month} ${year}`;
}
function fmtTimeOnly(iso) {
  if (!iso) return '—';
  return _TIME_FMT.format(new Date(iso));
}
function fmtDateTime(iso) {
  if (!iso) return '—';
  return `${fmtDateOnly(iso)}, ${fmtTimeOnly(iso)}`;
}
function fmtClock(now) {
  return now.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false, timeZone: TIME_ZONE });
}

// Maintenance window — JS counterpart of the PHP format_window helper so the
// /maintenance/?id=… page can format the same string the homepage cards show.
// Same-day: "16 May, 17:45 → 18:30". Cross-day: "16 May, 23:00 → 17 May, 02:00".
const _HHMM_FMT  = new Intl.DateTimeFormat('en-GB', { hour: '2-digit', minute: '2-digit', hour12: false, timeZone: TIME_ZONE });
const _DAYKEY_FMT = new Intl.DateTimeFormat('en-GB', { day: 'numeric', month: 'short', year: 'numeric', timeZone: TIME_ZONE });
function fmtMaintWindow(startsAt, endsAt) {
  if (!startsAt || !endsAt) return '—';
  const start = new Date(startsAt);
  const end   = new Date(endsAt);
  const sameDay = _DAYKEY_FMT.format(start) === _DAYKEY_FMT.format(end);
  if (sameDay) {
    return `${fmtDateOnly(startsAt)}, ${_HHMM_FMT.format(start)} → ${_HHMM_FMT.format(end)}`;
  }
  return `${fmtDateOnly(startsAt)}, ${_HHMM_FMT.format(start)} → ${fmtDateOnly(endsAt)}, ${_HHMM_FMT.format(end)}`;
}

// ─── DMU wordmark logo ──────────────────────────────────────────────────────

function Logo() {
  function onClick(e) {
    if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
    if (!window.publicNavigate) return;
    e.preventDefault();
    window.publicNavigate('home');
  }
  return (
    <a className="logo" href="/" onClick={onClick}>
      {/* The new SVG wordmarks already contain "DMU™ Status" — no
          sibling text needed. Two glyph variants render side by
          side; CSS in status.css hides whichever doesn't match the
          active data-theme on <html>. */}
      <img
        src="/assets/status-logo-trademarked-dark.svg"
        alt="DMU Status"
        className="logo-img logo-img--dark"
      />
      <img
        src="/assets/status-logo-trademarked-light.svg"
        alt="DMU Status"
        className="logo-img logo-img--light"
      />
    </a>
  );
}

// ─── Header ────────────────────────────────────────────────────────────────

const THEME_STORAGE_KEY = 'dmu_theme_status';

function ThemeToggle() {
  // Source of truth is the dataset attribute set by the inline bootstrap in index.html.
  const [theme, setTheme] = useState(() =>
    (typeof document !== 'undefined' && document.documentElement.dataset.theme === 'light') ? 'light' : 'dark'
  );
  function toggle() {
    const next = theme === 'light' ? 'dark' : 'light';
    // Brief class flip animates color/border/shadow swaps without bloating per-element CSS.
    const root = document.documentElement;
    root.classList.add('theme-transitioning');
    root.dataset.theme = next;
    try { localStorage.setItem(THEME_STORAGE_KEY, next); } catch (e) {}
    setTheme(next);
    window.setTimeout(() => root.classList.remove('theme-transitioning'), 260);
  }
  const isLight = theme === 'light';
  return (
    <button
      type="button"
      className="theme-toggle"
      title={isLight ? 'Switch to dark mode' : 'Switch to light mode'}
      aria-label={isLight ? 'Switch to dark mode' : 'Switch to light mode'}
      onClick={toggle}
    >
      {isLight ? (
        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
          <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79Z" />
        </svg>
      ) : (
        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
          <circle cx="12" cy="12" r="4" />
          <path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41" />
        </svg>
      )}
    </button>
  );
}

// Probes /api/me once on mount; renders an "Admin" link only if the response
// is 200 and {admin: true}. Failure modes (401, network error, slow) silently
// render nothing — the public page must keep working regardless of auth state.
// The fetch is fire-and-forget; we never block the header on it.
function AdminButton() {
  const [isAdmin, setIsAdmin] = useState(false);
  useEffect(() => {
    let cancelled = false;
    (async () => {
      try {
        const res = await fetch('/api/me', { credentials: 'same-origin' });
        if (!res.ok || cancelled) return;
        const data = await res.json();
        if (!cancelled && data && data.admin === true) setIsAdmin(true);
      } catch (_) { /* silent — public page must render regardless */ }
    })();
    return () => { cancelled = true; };
  }, []);
  if (!isAdmin) return null;
  return <a className="hdr-admin-link" href="/admin/">Admin</a>;
}

function Header({ now, monitorTick }) {
  // Pulse the clock dot once whenever any monitor records a fresh check.
  // Compare against a ref so we only fire on real ticks — not on mount, and
  // not on unrelated re-renders from the seconds clock.
  const [pulse, setPulse] = useState(false);
  const prevTick = useRef(undefined);
  useEffect(() => {
    if (monitorTick == null) return;
    const isFirst = prevTick.current === undefined;
    if (!isFirst && prevTick.current === monitorTick) return;
    prevTick.current = monitorTick;
    if (isFirst) return; // first value is just "data loaded", not "fresh check"
    setPulse(true);
    const t = setTimeout(() => setPulse(false), 1700);
    return () => clearTimeout(t);
  }, [monitorTick]);

  return (
    <header className="hdr">
      <Logo />
      <div className="hdr-right">
        <AdminButton />
        <ThemeToggle />
        <div className="hdr-clock" title="Current time · Athens, Greece">
          <span className={`dot ${pulse ? 'dot-pulse' : ''}`} />
          <span>{fmtClock(now)} {TZ_LABEL}</span>
        </div>
      </div>
    </header>
  );
}

// ─── Overall banner ─────────────────────────────────────────────────────────

function InfoTip({ children }) {
  return (
    <span className="info-tip" tabIndex={0} aria-label="More info">
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
        <circle cx="12" cy="12" r="10" />
        <path d="M12 16v-4M12 8h.01" />
      </svg>
      <span className="info-tip-bubble">{children}</span>
    </span>
  );
}

// Rendered ABOVE OverallBanner when probesStale=true. Loss-of-signal is
// warning-yellow, not red — it's "we don't know", not "outage observed".
function StaleBanner({ proberLastSuccessAt, now }) {
  const ago = proberLastSuccessAt ? relTime(proberLastSuccessAt, now) : null;
  return (
    <section className="stale-banner" role="alert">
      <div className="stale-banner-icon" aria-hidden="true">
        <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
          <path d="M12 9v4M12 17h.01M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0Z" />
        </svg>
      </div>
      <div>
        <div className="stale-banner-title">Probe data is stale{ago ? ` — last successful check ${ago}` : ''}.</div>
        <div className="stale-banner-sub">Service status below may not reflect current reality.</div>
      </div>
    </section>
  );
}

function OverallBanner({ services, uptime, proberLastSuccessAt, probesStale, activeMaintenance, now }) {
  const anyOutage    = services.some(s => s.status === 'outage');
  const anyDegraded  = services.some(s => s.status === 'degraded');
  const probeOverall = anyOutage ? 'outage' : anyDegraded ? 'degraded' : 'operational';

  // Escalate the banner based on operator-declared incidents too — even if probes are clean,
  // an active incident should be visible at the top. Counter and uptime % stay probe-driven.
  const anyIncidentBad   = services.some(s => s.incidentSeverity === 'major');
  const anyIncidentMinor = services.some(s => s.incidentSeverity === 'minor');
  const incidentOverall  = anyIncidentBad ? 'outage' : anyIncidentMinor ? 'degraded' : 'operational';

  // Worse-of-two so probe failures and incidents can each push the banner up but neither pulls it down.
  const rank = { operational: 0, maintenance: 1, degraded: 2, outage: 3 };
  let probeOrIncidentOverall = rank[incidentOverall] > rank[probeOverall] ? incidentOverall : probeOverall;

  // Maintenance is an announcement layer: it doesn't downgrade a degraded /
  // outage banner, but it DOES escalate "operational" so users see at the top
  // that planned work is happening. Same one-way-up rule as incidents.
  const hasMaintenance = Array.isArray(activeMaintenance) && activeMaintenance.length > 0;
  if (hasMaintenance && rank.maintenance > rank[probeOrIncidentOverall]) {
    probeOrIncidentOverall = 'maintenance';
  }

  // Stale wins everything else — once the prober has stopped reporting, no
  // probe-derived or incident-derived headline is meaningful. Spec precedence:
  // probesStale > incidentRank > probeRank.
  const overall = probesStale ? 'stale' : probeOrIncidentOverall;
  const meta = probesStale
    ? { label: STALE_META.label, dot: STALE_META.color, fg: STALE_META.color }
    : overall === 'maintenance'
      ? { label: 'Maintenance', dot: 'var(--info)', fg: 'var(--info)' }
      : STATUS_META[overall];

  const maintCount = activeMaintenance?.length ?? 0;
  const heads = {
    operational: 'All systems operational',
    maintenance: 'Maintenance in progress',
    degraded:    'Some systems are experiencing issues',
    outage:      'Major outage in progress',
    stale:       'Status unknown — probes stale',
  };
  const subs = {
    operational: 'Everything is running smoothly across every service.',
    maintenance: maintCount === 1
      ? 'Planned maintenance is underway. Brief impact on the affected services is possible.'
      : `${maintCount} maintenance windows are underway. Brief impact on the affected services is possible.`,
    degraded:    'We\u2019re actively working on the issue. Most services remain unaffected.',
    outage:      'We\u2019re engaging the on-call team. Updates will follow as we learn more.',
    stale:       `No probe data received for ${proberLastSuccessAt && now ? relTime(proberLastSuccessAt, now) : 'an extended period'}. Service status shown reflects the last successful check, not current reality.`,
  };

  const eyebrowFreshness = proberLastSuccessAt
    ? `Last updated ${relTime(proberLastSuccessAt, now ?? new Date())}`
    : 'No probe data yet';

  return (
    <section className={`banner banner-${overall}`}>
      <div className="banner-glow" aria-hidden="true" />
      <div className="banner-inner">
        <div className="banner-icon">
          <span className="dot dot-lg dot-pulse" style={{ background: meta.dot }} />
        </div>
        <div className="banner-text">
          <div className="banner-eyebrow">
            <span style={{ color: meta.fg }}>{meta.label}</span>
            <span className="banner-sep">·</span>
            <span>{eyebrowFreshness}</span>
          </div>
          <h1 className="banner-title">{heads[overall]}</h1>
          <p className="banner-sub">{subs[overall]}</p>
        </div>
        <div className="banner-stats">
          <div className="stat">
            {probesStale ? (
              <div className="stat-num srv-uptime-stale">—</div>
            ) : (
              <div className="stat-num">{services.filter(s => s.status === 'operational').length}<span>/{services.length}</span></div>
            )}
            <div className="stat-lbl">Operational</div>
          </div>
          <div className="stat">
            <div className={`stat-num${probesStale ? ' srv-uptime-stale' : ''}`}>
              {probesStale || !uptime || uptime.percent == null ? '—' : uptime.percent.toFixed(3)}
              {!probesStale && uptime && uptime.percent != null && <span>%</span>}
            </div>
            <div className="stat-lbl">
              {(uptime?.windowDays ?? 30)}-day uptime
              <InfoTip>
                Aggregate uptime across {uptime?.monitorCount ?? 0} monitor{(uptime?.monitorCount ?? 0) === 1 ? '' : 's'} over the last {(uptime?.windowDays ?? 30)} days.
                Some monitors are configured not to contribute to this number.
              </InfoTip>
            </div>
          </div>
        </div>
      </div>
    </section>
  );
}

// ─── Active incident with timeline ──────────────────────────────────────────

function ActiveIncident({ incident, now, collapsible = true }) {
  const [open, setOpen] = useState(true);
  if (!incident) return null;
  // On the dedicated /incident/?id=… page we render the card statically — no
  // toggle, no chevron, body always visible.
  const isOpen = collapsible ? open : true;
  const HeadTag = collapsible ? 'button' : 'div';
  const sevClass = incident.severity === 'major' ? 'bad' : 'warn';
  // Resolved → neutral outline (no severity/status tint; the pills inside the
  // card already carry the resolved color). Monitoring → blue ("we're watching"
  // rather than "on fire"). Otherwise the severity drives the tint.
  const cardClass = incident.status === 'resolved'   ? 'neutral'
                  : incident.status === 'monitoring' ? 'info'
                  :                                    sevClass;

  // Current stage shown as a secondary pill alongside severity.
  const statusClass = incident.status === 'monitoring'  ? 'info'
                    : incident.status === 'identified'  ? 'warn'
                    : incident.status === 'resolved'    ? 'ok'
                    :                                     'bad';
  const statusLabel = incident.status[0].toUpperCase() + incident.status.slice(1);

  return (
    <section className={`card incident incident-${cardClass}`}>
      <HeadTag
        className={`incident-head ${collapsible ? '' : 'incident-head-static'} ${collapsible && !isOpen ? 'incident-head-collapsed' : ''}`}
        {...(collapsible ? { onClick: () => setOpen(o => !o) } : {})}
      >
        <div className="incident-head-top">
          <div className="incident-head-pills">
            <span className={`pill pill-${sevClass}`}>
              <span className="dot" />
              {incident.severity === 'major' ? 'Major Incident' : 'Minor Incident'}
            </span>
            <span className={`pill pill-${statusClass}`}>
              <span className="dot" />
              {statusLabel}
            </span>
          </div>
          <div className="incident-head-r">
            <span className="muted">{relTime(incident.started, now)}</span>
            {collapsible && (
              <svg className={`chev ${isOpen ? 'open' : ''}`} width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M6 9l6 6 6-6"/></svg>
            )}
          </div>
        </div>
        <div className="incident-title">{incident.title}</div>
      </HeadTag>

      <div className={`incident-body ${isOpen ? 'open' : ''}`}>
        <div className="incident-body-inner">
        {incident.description && <p className="incident-desc">{incident.description}</p>}
        <div className="incident-meta">
          <div><span className="muted">Status</span><b>{incident.status[0].toUpperCase() + incident.status.slice(1)}</b></div>
          <div><span className="muted">Started</span><b>{fmtDateTime(incident.started)}</b></div>
          <div><span className="muted">Affects</span><b>{incident.components.join(', ')}</b></div>
        </div>

        <ol className="timeline">
          {(() => {
            const steps = ['Investigating', 'Identified', 'Monitoring', 'Resolved'];
            // A phase is "past" if it appears earlier in the canonical sequence
            // than the incident's current status — not just when an update was
            // posted at that label. That way Identified/Monitoring still go green
            // when an incident jumps straight from Investigating to Resolved.
            const currentIdx = steps.findIndex(s => s.toLowerCase() === incident.status);
            return steps.map((step, idx) => {
              const isPast    = currentIdx > -1 && idx < currentIdx;
              const isCurrent = idx === currentIdx;
              // Per-phase class (tl-investigating / -identified / -monitoring / -resolved)
              // supplies the --tl-color custom property consumed by .current styling.
              return (
                <li key={step} className={`tl-step tl-${step.toLowerCase()} ${isPast ? 'on' : ''} ${isCurrent ? 'current' : ''}`}>
                  <span className="tl-mark"><span /></span>
                  <span className="tl-name">{step}</span>
                </li>
              );
            });
          })()}
        </ol>

        <ul className="updates">
          {incident.updates.map((u, i) => (
            <li key={i} className="update">
              <div className="update-head">
                <span className={`update-tag tag-${u.label.toLowerCase()}`}>{u.label}</span>
                <span className="muted">{fmtDateTime(u.at)} · {relTime(u.at, now)}</span>
              </div>
              <p>{u.text}</p>
            </li>
          ))}
        </ul>
        </div>
      </div>
    </section>
  );
}

// ─── Uptime bar grid ────────────────────────────────────────────────────────

// Pick the right bucket array for a given range key.
function pickHistory(historyObj, range) {
  if (!historyObj) return [];
  if (range === '1h') return historyObj.hour || [];
  if (range === '1d') return historyObj.day  || [];
  const days = historyObj['90d'] || [];
  if (range === '7d')  return days.slice(-7);
  if (range === '30d') return days.slice(-30);
  return days; // '90d'
}

function UptimeRow({ service, viz, range, rowIndex = 0, probesStale = false }) {
  const [hovered, setHovered] = useState(null);
  const history = pickHistory(service.history, range);
  const total = history.length;
  // Per-row uptime is the server-derived raw-probe value for the currently
  // selected range — an object keyed by '1h'|'1d'|'7d'|'30d'|'90d', each value
  // a number or null. Bars smooth short bad runs to operational for visual
  // denoising; the percentage does not. Both surfaces are intentionally
  // separate — see how-status-works.jsx §4 for the documented tradeoff.
  //
  // The server emits null for any range with zero probes in window (a fresh
  // monitor with no probes ever gets null in every slot), so we don't need a
  // separate hasNoProbes check — the per-range null subsumes it.
  const uptimeForRange = service.uptime?.[range];
  const hasUptimeNumber = !probesStale && typeof uptimeForRange === 'number';
  const meta = STATUS_META[service.status];

  // Active-incident overlay. Both the dot and the trailing bars only get repainted
  // with the incident severity when that severity is STRICTLY worse than the
  // probe-derived state — so a minor incident can never visually downgrade a
  // probe-observed outage, and a "no data" bucket inside the window can still
  // be filled in by any incident severity (no_data ranks 0).
  const incMeta = service.incidentSeverity ? INCIDENT_META[service.incidentSeverity] : null;
  const incidentRank   = incMeta ? (SEVERITY_RANK[service.incidentSeverity] ?? 0) : -1;
  const probeStatusRank = SEVERITY_RANK[service.status] ?? 0;
  const incidentWinsDot = incMeta !== null && incidentRank > probeStatusRank;
  // Precedence: probesStale > incidentRank > probeRank. Once the prober has
  // gone silent, no operator overlay or probe state is meaningful for THIS dot.
  const dotColor = probesStale ? STALE_META.color : (incidentWinsDot ? incMeta.color : meta.dot);
  const dotLabel = probesStale ? STALE_META.label : (incidentWinsDot ? incMeta.label : meta.label);
  const bucketMs = RANGE_BUCKET_MS[range] ?? 60_000;
  // Per-bar overlay lookup uses the full per-service overlays array (resolved
  // incidents + maintenance windows, last 90 days). The active-incident
  // `incMeta` above remains the only signal for the dot; bars now consult the
  // overlay list directly so historical events tint their span too.
  const overlays = service.overlays || [];
  const nowMs    = Date.now();

  // Pulse the status dot once whenever the monitor records a fresh check.
  // We compare the incoming lastCheckedAt against a ref so we only fire on real changes
  // (not on mount, not on unrelated re-renders).
  const [pulse, setPulse] = useState(false);
  const prevCheckedAt = useRef(service.lastCheckedAt);
  useEffect(() => {
    if (!service.lastCheckedAt) return;
    if (prevCheckedAt.current === service.lastCheckedAt) return;
    prevCheckedAt.current = service.lastCheckedAt;
    setPulse(true);
    const t = setTimeout(() => setPulse(false), 1600);
    return () => clearTimeout(t);
  }, [service.lastCheckedAt]);

  // One-shot reveal for the newest bar when it transitions out of the fetching
  // shimmer (latest bucket was -1, now has a real value). We track the previous
  // value of that bucket with a ref so this only fires on the real handoff,
  // not on initial mount or when the range/total changes.
  const [landed, setLanded] = useState(false);
  const prevLatest = useRef(total > 0 ? history[total - 1] : null);
  useEffect(() => {
    if (total === 0) return;
    const cur = history[total - 1];
    const prev = prevLatest.current;
    prevLatest.current = cur;
    if (prev === -1 && cur !== -1 && cur != null) {
      setLanded(true);
      const t = setTimeout(() => setLanded(false), 600);
      return () => clearTimeout(t);
    }
  }, [history, total]);

  // Tooltip label depends on the unit of each bucket.
  function bucketLabel(i) {
    const now = new Date();
    if (range === '1h') {
      const d = new Date(now.getTime() - (total - 1 - i) * 60_000);
      return d.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false });
    }
    if (range === '1d') {
      const d = new Date(now.getTime() - (total - 1 - i) * 3_600_000);
      return d.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false });
    }
    const d = new Date(now);
    d.setDate(d.getDate() - (total - 1 - i));
    return fmtDateOnly(d);
  }

  // Whole row is a link to the public detail page. Modifier-click and middle-click
  // fall through to a real navigation so "open in new tab" still works; bare left-clicks
  // hand off to the SPA router so we don't blow away the rest of the page state.
  function onRowClick(e) {
    if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
    if (!window.publicNavigate) return;
    e.preventDefault();
    window.publicNavigate('monitor', { id: service.id });
  }

  return (
    // --row-i is read by the row's entry animation and by each bar's animation
    // so the bar sweep is offset by the row's stagger.
    <a className="srv-row" href={`/monitor/?id=${service.id}`} onClick={onRowClick} style={{ '--row-i': rowIndex }}>
      <div className="srv-meta">
        <div className="srv-name">
          <span
            className="info-tip srv-dot-tip"
            tabIndex={0}
            aria-label={`${dotLabel}, ${service.lastCheckedAt ? 'updated ' + relTime(service.lastCheckedAt, new Date()) : 'never checked'}`}
          >
            <span
              className={`srv-dot ${pulse ? 'srv-dot-pulsing' : ''}`}
              style={{ background: dotColor, color: dotColor }}
            />
            <span className="info-tip-bubble srv-dot-bubble">
              <div className="srv-dot-tip-row">
                <span className="srv-dot" style={{ background: dotColor, color: dotColor }} />
                <div>
                  <div className="srv-dot-tip-title">{dotLabel}</div>
                  <div className="srv-dot-tip-meta">
                    {service.lastCheckedAt ? 'Updated ' + relTime(service.lastCheckedAt, new Date()) : 'No checks yet'}
                  </div>
                </div>
              </div>
            </span>
          </span>
          {service.name}
          {service.countsForUptime === false && (
            <InfoTip>This service is excluded from the total uptime calculation.</InfoTip>
          )}
        </div>
        <div className="srv-desc">{service.desc}</div>
      </div>

      <div className="srv-viz" data-viz={viz}>
        {viz === 'bars' && (
          <div className="bars" onMouseLeave={() => setHovered(null)}>
            {history.map((d, i) => {
              // Incident tint only wins when its severity is strictly worse than what
              // the probes saw in this bucket. `no_data` ranks 0, so an overlay can
              // fill an empty bucket; a `down` bucket (rank 2) is never repainted
              // by a `minor` incident (rank 1) or by maintenance (rank 1) either.
              // The overlay list covers both incidents (active + resolved last
              // 90d) and maintenance windows for this service, so historical
              // events tint their span — not just the currently active one.
              const bEnd    = nowMs - (total - 1 - i) * bucketMs;
              const bStart  = bEnd - bucketMs;
              const overlay = pickOverlayForBucket(overlays, bStart, bEnd, nowMs);
              const probeRank   = SEVERITY_RANK[DAY_META[d].status] ?? 0;
              const overlayWins = overlay !== null && overlay.rank > probeRank;
              const color = overlayWins ? overlay.meta.color : DAY_META[d].color;
              // The newest bucket is briefly empty between the moment its window
              // opens and the moment the prober writes a result into it. Treat
              // that specific bar as a "fetching" placeholder so the user sees a
              // shimmer instead of a mute "no data" gap that turns green a second
              // later. Skip when a winning overlay is already repainting it.
              const isFetching = i === total - 1 && d === -1 && !probesStale && !overlayWins;
              const isJustLanded = i === total - 1 && landed;
              return (
                <div
                  key={i}
                  className={`bar bar-${d}${overlayWins ? ' bar-inc' : ''}${isFetching ? ' bar-fetching' : ''}${isJustLanded ? ' bar-just-landed' : ''}`}
                  style={{ '--bar-color': color, '--bar-i': i }}
                  onMouseEnter={() => setHovered(i)}
                />
              );
            })}
            {hovered != null && (() => {
              const bEnd    = nowMs - (total - 1 - hovered) * bucketMs;
              const bStart  = bEnd - bucketMs;
              const overlay = pickOverlayForBucket(overlays, bStart, bEnd, nowMs);
              const probeRank   = SEVERITY_RANK[DAY_META[history[hovered]].status] ?? 0;
              const overlayWins = overlay !== null && overlay.rank > probeRank;
              const tipColor = overlayWins ? overlay.meta.color : DAY_META[history[hovered]].color;
              const tipLabel = overlayWins ? overlay.meta.label : DAY_META[history[hovered]].label;
              const isFetching = hovered === total - 1 && history[hovered] === -1 && !probesStale && !overlayWins;
              return (
                <div className="bar-tip" style={{ left: `calc(${(hovered + 0.5) / total * 100}% )` }}>
                  <div className="bar-tip-d">{bucketLabel(hovered)}</div>
                  <div className="bar-tip-s">
                    {isFetching
                      ? <><span className="bar-tip-spinner" />Fetching status</>
                      : <><span className="dot" style={{ background: tipColor }} />{tipLabel}</>}
                  </div>
                </div>
              );
            })()}
          </div>
        )}
        {viz === 'dots' && (
          <div className="dots-grid">
            {history.map((d, i) => (
              <div key={i} className="dot-cell" style={{ background: DAY_META[d].color }} />
            ))}
          </div>
        )}
        {viz === 'line' && (
          <UptimeLine history={history} />
        )}
        {viz === 'pill' && (
          <UptimePill history={history} />
        )}
      </div>

      <div className="srv-stat">
        {hasUptimeNumber ? (
          <div className="srv-uptime">{uptimeForRange.toFixed(3)}<span>%</span></div>
        ) : (
          <div className="srv-uptime srv-uptime-stale">—</div>
        )}
        <div className="srv-uptime-lbl">{RANGE_UPTIME_LABEL[range] ?? RANGE_UPTIME_LABEL['30d']}</div>
      </div>
    </a>
  );
}

function UptimeLine({ history }) {
  // Map: rolling 7-day "incident density" sparkline
  const w = 100, h = 28;
  const pts = history.map((d, i) => {
    const window = history.slice(Math.max(0, i - 6), i + 1);
    const score = window.reduce((a, b) => a + (b === 0 ? 1 : b === 1 ? 0.4 : 0), 0) / window.length;
    return [i / (history.length - 1) * w, h - score * h * 0.9 - 2];
  });
  const path = pts.map((p, i) => (i ? 'L' : 'M') + p[0].toFixed(2) + ' ' + p[1].toFixed(2)).join(' ');
  const fillPath = path + ` L ${w} ${h} L 0 ${h} Z`;
  return (
    <svg className="spark" viewBox={`0 0 ${w} ${h}`} preserveAspectRatio="none">
      <defs>
        <linearGradient id="sparkfill" x1="0" y1="0" x2="0" y2="1">
          <stop offset="0%"  stopColor="var(--ok)" stopOpacity="0.35"/>
          <stop offset="100%" stopColor="var(--ok)" stopOpacity="0"/>
        </linearGradient>
      </defs>
      <path d={fillPath} fill="url(#sparkfill)" />
      <path d={path} fill="none" stroke="var(--ok)" strokeWidth="1.2" vectorEffect="non-scaling-stroke" />
    </svg>
  );
}

function UptimePill({ history }) {
  const okCount = history.filter(d => d === 0).length;
  const pct = okCount / history.length * 100;
  return (
    <div className="pill-bar">
      <div className="pill-bar-track" />
      <div className="pill-bar-fill" style={{ width: pct + '%' }} />
      {history.map((d, i) => d !== 0 && (
        <div key={i} className={`pill-bar-incident bar-${d}`}
             style={{ left: (i / history.length * 100) + '%', background: DAY_META[d].color }} />
      ))}
    </div>
  );
}

// ─── Services section ───────────────────────────────────────────────────────

const RANGES = [
  { key: '1h',  label: '1h'  },
  { key: '1d',  label: '1d'  },
  { key: '7d',  label: '7d'  },
  { key: '30d', label: '30d' },
  { key: '90d', label: '90d' },
];
// Per-row uptime label that tracks the selected range. The 30-day headline
// at the top of the page is independent — it stays anchored at "30-day uptime"
// regardless of this selection.
const RANGE_UPTIME_LABEL = {
  '1h':  '1-hour uptime',
  '1d':  '24-hour uptime',
  '7d':  '7-day uptime',
  '30d': '30-day uptime',
  '90d': '90-day uptime',
};
const RANGE_WINDOW_LABEL = {
  '1h':  { left: '1 hour ago',    right: 'Now',   sub: '60-minute' },
  '1d':  { left: '24 hours ago',  right: 'Now',   sub: '24-hour'   },
  '7d':  { left: '7 days ago',    right: 'Today', sub: '7-day'     },
  '30d': { left: '30 days ago',   right: 'Today', sub: '30-day'    },
  '90d': { left: '90 days ago',   right: 'Today', sub: '90-day'    },
};

function ServicesSection({ services, viz, setViz, range, setRange, probesStale = false }) {
  const groups = useMemo(() => {
    const m = new Map();
    services.forEach(s => { if (!m.has(s.group)) m.set(s.group, []); m.get(s.group).push(s); });
    return [...m.entries()];
  }, [services]);
  const win = RANGE_WINDOW_LABEL[range] || RANGE_WINDOW_LABEL['90d'];

  // A range is "available" if it contains ANY real probe data for any
  // service. Stricter "oldest bucket must have data" rules are too fragile —
  // a single missed probe at the cutoff hides the whole range, even when
  // there's plenty of usable data in the rest of the window. The dashboard
  // is more useful showing sparse data than hiding it.
  function rangeCovered(key) {
    return services.some(s => {
      const h = pickHistory(s.history, key);
      return h.some(v => v >= 0);
    });
  }
  const visibleRanges = useMemo(() => RANGES.filter(r => {
    if (r.key === range) return true;     // never hide the active button under the user
    return rangeCovered(r.key);
  }), [services, range]);

  // If the active range is no longer covered, fall back to the longest one that is.
  useEffect(() => {
    if (!services.length) return;
    if (rangeCovered(range)) return;
    // Prefer broader ranges first when stepping back.
    const fallback = [...RANGES].reverse().find(r => r.key !== range && rangeCovered(r.key));
    if (fallback) setRange(fallback.key);
  }, [services, range, setRange]);

  return (
    <section className="card services">
      <div className="card-head">
        <div>
          <h2>Services</h2>
          <p className="muted">Live status and {win.sub} history across every service we operate.</p>
        </div>
        <div className="seg-row">
          <div className="seg">
            {visibleRanges.map(r => (
              <button key={r.key} className={range === r.key ? 'on' : ''} onClick={() => setRange(r.key)}>{r.label}</button>
            ))}
          </div>
        </div>
      </div>

      <div className="srv-legend">
        <div className="legend-l">
          <span><span className="dot" style={{ background: 'var(--ok)' }} /> Operational</span>
          <span><span className="dot" style={{ background: 'var(--warn)' }} /> Degraded</span>
          <span><span className="dot" style={{ background: 'var(--bad)' }} /> Outage</span>
          {probesStale && <span><span className="dot" style={{ background: 'var(--stale)' }} /> Stale data</span>}
        </div>
        <div className="legend-r">
          <span className="muted">{win.left}</span>
          <span className="legend-line" />
          <span className="muted">{win.right}</span>
        </div>
      </div>

      <div className="srv-list">
        {(() => {
          // Compute a flat index across groups so each row's CSS-driven entry
          // animation gets a continuous stagger rather than restarting per group.
          let rowIdx = 0;
          return groups.map(([groupName, list]) => (
            <div key={groupName} className="srv-group">
              <div className="srv-group-head">{groupName}</div>
              {list.map(s => <UptimeRow key={s.id} service={s} viz={viz} range={range} rowIndex={rowIdx++} probesStale={probesStale} />)}
            </div>
          ));
        })()}
      </div>
    </section>
  );
}

// ─── Metrics charts ─────────────────────────────────────────────────────────

// Bucket size per range key (used to label the hover tooltip on metric charts).
const METRIC_STEP_MS = {
  '1h':  60_000,
  '1d':  3_600_000,
  '7d':  86_400_000,
  '30d': 86_400_000,
  '90d': 86_400_000,
};

// Build a human-readable label for bucket index i in a window.
// Buckets are oldest-first: i=0 is (count-1) intervals ago, i=count-1 is "now".
function metricBucketLabel(windowKey, total, i) {
  const stepsAgo = total - 1 - i;
  const stepMs = METRIC_STEP_MS[windowKey] ?? 86_400_000;
  const d = new Date(Date.now() - stepsAgo * stepMs);
  if (windowKey === '1h' || windowKey === '1d') {
    return d.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false });
  }
  return fmtDateOnly(d);
}

function fmtMetricValue(v, unit) {
  if (v === null || v === undefined) return '—';
  if (unit === '%')  return v.toFixed(2) + '%';
  if (unit === 'ms') return Math.round(v) + ' ms';
  return v.toLocaleString();
}

function Chart({ data, label, unit, color, windowKey, windowLabel, description, yMin, yMax, variant = 'line' }) {
  const [hover, setHover] = useState(null);
  const w = 600, h = 140;

  // Find points where we have real values (null = no data in that bucket).
  const valid = data.map((v, i) => v == null ? null : { i, v });
  const validValues = valid.filter(p => p !== null).map(p => p.v);
  const hasData = validValues.length > 0;
  const dataMax = hasData ? Math.max(...validValues) : 1;
  const dataMin = hasData ? Math.min(...validValues) : 0;
  // Honour fixed bounds when supplied; otherwise auto-scale to the data + add headroom.
  const min = (yMin != null) ? yMin : dataMin;
  const max = (yMax != null) ? yMax : dataMax;
  const rangeSize  = Math.max(0.0001, max - min);
  const topPad    = (yMax != null) ? 0 : rangeSize * 0.075;
  const bottomPad = (yMin != null) ? 0 : rangeSize * 0.075;
  const span   = rangeSize + topPad + bottomPad;
  const offset = min - bottomPad;
  function pointFor(v, i) {
    return [
      data.length > 1 ? (i / (data.length - 1)) * w : w / 2,
      h - ((v - offset) / span) * (h - 24) - 12,
    ];
  }
  const pts = data.map((v, i) => v == null ? null : pointFor(v, i));

  // Build line segments and fill segments, splitting around null buckets so we
  // don't draw straight lines across gaps in the data.
  const segments = [];
  let cur = [];
  for (const p of pts) {
    if (p == null) {
      if (cur.length) { segments.push(cur); cur = []; }
    } else {
      cur.push(p);
    }
  }
  if (cur.length) segments.push(cur);

  const linePath = segments.map(seg =>
    seg.map((p, i) => (i === 0 ? 'M' : 'L') + p[0].toFixed(2) + ' ' + p[1].toFixed(2)).join(' ')
  ).join(' ');
  const fillPath = segments.filter(seg => seg.length >= 2).map(seg => {
    const start = seg[0], end = seg[seg.length - 1];
    let s = 'M' + start[0].toFixed(2) + ' ' + h;
    seg.forEach(p => { s += ' L ' + p[0].toFixed(2) + ' ' + p[1].toFixed(2); });
    s += ' L ' + end[0].toFixed(2) + ' ' + h + ' Z';
    return s;
  }).join(' ');

  // Latest non-null value for the header readout.
  const cur_idx = [...valid].reverse().find(p => p !== null);
  const headValue = hover != null ? data[hover] : (cur_idx ? cur_idx.v : null);

  function onMove(e) {
    if (!hasData) return;
    const r = e.currentTarget.getBoundingClientRect();
    const x = (e.clientX - r.left) / r.width;
    const i = Math.round(x * (data.length - 1));
    setHover(Math.max(0, Math.min(data.length - 1, i)));
  }

  const gid = 'g-' + label.replace(/\W/g, '') + '-' + windowKey;
  const hoverPt = (hover != null && pts[hover]) ? pts[hover] : null;

  return (
    <div className="chart">
      <div className="chart-head">
        <div>
          <div className="chart-lbl">
            {label}
            {description && <InfoTip>{description}</InfoTip>}
          </div>
          <div className="chart-val" style={{ color }}>
            {fmtMetricValue(headValue, unit)}
          </div>
        </div>
        <div className="chart-meta">
          <span className="muted">{windowLabel}</span>
        </div>
      </div>
      <div className="chart-wrap" onMouseMove={onMove} onMouseLeave={() => setHover(null)}>
        <svg viewBox={`0 0 ${w} ${h}`} preserveAspectRatio="none" className="chart-svg">
          <defs>
            <linearGradient id={gid} x1="0" y1="0" x2="0" y2="1">
              <stop offset="0%"  stopColor={color} stopOpacity="0.28"/>
              <stop offset="100%" stopColor={color} stopOpacity="0"/>
            </linearGradient>
          </defs>
          {[0.25, 0.5, 0.75].map(g => (
            <line key={g} x1="0" x2={w} y1={h * g} y2={h * g} stroke="var(--grid)" strokeDasharray="2 4" vectorEffect="non-scaling-stroke" />
          ))}
          {hasData && variant === 'line' && <path d={fillPath} fill={`url(#${gid})`} />}
          {hasData && variant === 'line' && <path d={linePath} fill="none" stroke={color} strokeWidth="1.5" vectorEffect="non-scaling-stroke" />}
          {hasData && variant === 'bar' && data.map((v, i) => {
            if (v == null || v <= 0) return null;
            const [cx, cy] = pointFor(v, i);
            const barW = (data.length > 1 ? (w / (data.length - 1)) : w) * 0.6;
            return (
              <rect key={i}
                x={cx - barW / 2}
                y={cy}
                width={barW}
                height={Math.max(1, h - cy)}
                fill={color}
                rx="1"
                ry="1"
              />
            );
          })}
          {hoverPt && (
            <g>
              <line x1={hoverPt[0]} x2={hoverPt[0]} y1="0" y2={h} stroke="var(--grid-strong)" vectorEffect="non-scaling-stroke" />
              <circle cx={hoverPt[0]} cy={hoverPt[1]} r="3.5" fill={color} stroke="var(--bg)" strokeWidth="1.5" vectorEffect="non-scaling-stroke" />
            </g>
          )}
        </svg>
        {hover != null && (
          <div className="chart-tip" style={{ left: `${(hover / (data.length - 1)) * 100}%` }}>
            <div className="chart-tip-lbl">{metricBucketLabel(windowKey, data.length, hover)}</div>
            <div className="chart-tip-val">{fmtMetricValue(data[hover], unit)}</div>
          </div>
        )}
        {!hasData && (
          <div className="chart-empty">No data yet for this window.</div>
        )}
      </div>
    </div>
  );
}

function MetricsSection({ metrics, range }) {
  if (!metrics) return null;
  const data = metrics[range];
  if (!data) return null;

  return (
    <section className="card">
      <div className="card-head">
        <div>
          <h2>Metrics</h2>
          <p className="muted">Aggregate probe performance across public monitors. Matches the range you've picked above.</p>
        </div>
      </div>
      <div className="charts">
        <Chart
          data={data.avgResponse}
          label="Avg response time"
          unit="ms"
          color="var(--accent)"
          windowKey={range}
          windowLabel={range}
          yMin={0}
          description="How quickly our services respond, on average. Lower is better. Brief spikes usually mean a service got slower for a moment, not that it broke."
        />
        <Chart
          data={data.successRate}
          label="Probe success rate"
          unit="%"
          color="var(--ok)"
          windowKey={range}
          windowLabel={range}
          yMin={0}
          yMax={100}
          description="The share of our health checks that came back successful. 100% means everything passed. Lower values mean some checks failed."
        />
        <Chart
          data={data.failures}
          label="Failed probes"
          unit=""
          color="var(--bad)"
          windowKey={range}
          windowLabel={range}
          yMin={0}
          variant="bar"
          description="How many individual health checks failed in each time bucket. A few isolated failures are usually transient network glitches; larger numbers suggest a real issue."
        />
        <Chart
          data={data.affectedMonitors}
          label="Affected monitors"
          unit=""
          color="var(--warn)"
          windowKey={range}
          windowLabel={range}
          yMin={0}
          variant="bar"
          description="How many distinct services had at least one failed check in each bucket. Helps tell whether an issue was widespread or limited to a single service."
        />
      </div>
    </section>
  );
}

// ─── Scheduled maintenance ──────────────────────────────────────────────────

// Surfaces maintenance windows the operator has marked as currently underway.
// Renders one ActiveIncident-style card per window so the visual treatment
// matches the rest of the page (collapsible head, meta grid, timeline,
// updates list). One <section> per row keeps the page's stagger animation
// working — each card is a direct child of .shell with its own delay slot.
const MNT_TIMELINE_STEPS = ['Scheduled', 'In Progress', 'Verifying', 'Completed'];
const _mntSlug = (label) => label.toLowerCase().replace(/\s+/g, '-');

function ActiveMaintenance({ maintenance = [], now }) {
  if (maintenance.length === 0) return null;
  return (
    <React.Fragment>
      {maintenance.map(m => <MaintenanceCard key={m.id} m={m} now={now} />)}
    </React.Fragment>
  );
}

function MaintenanceCard({ m, now, collapsible = true }) {
  const [open, setOpen] = useState(true);
  // Same toggle pattern as ActiveIncident: head becomes a static <div> on the
  // dedicated /maintenance/?id=… page so there's no expand/collapse there.
  const isOpen = collapsible ? open : true;
  const HeadTag = collapsible ? 'button' : 'div';

  const statusLabel = m.status === 'inprogress' ? 'In Progress'
                    : m.status === 'verifying'  ? 'Verifying'
                    : m.status[0].toUpperCase() + m.status.slice(1);
  // Map the DB status to its label inside MNT_TIMELINE_STEPS — used to decide
  // which step is "current" and which are "past" (green).
  const currentStep = m.status === 'completed'  ? 'Completed'
                    : m.status === 'verifying'  ? 'Verifying'
                    : m.status === 'inprogress' ? 'In Progress'
                    :                              'Scheduled';
  const currentIdx = MNT_TIMELINE_STEPS.indexOf(currentStep);

  // Completed → neutral outline (same treatment as resolved incidents).
  // Otherwise info-blue, signalling "active maintenance".
  const cardClass = m.status === 'completed' ? 'neutral' : 'info';
  // Second pill (status) tracks the timeline current-step color so the head
  // visually mirrors where the maintenance is in its lifecycle.
  const statusPillColor = m.status === 'completed'  ? 'ok'
                        : m.status === 'verifying'  ? 'info'
                        : m.status === 'inprogress' ? 'warn'
                        :                              'info';

  return (
    <section className={`card incident incident-${cardClass}`}>
      <HeadTag
        className={`incident-head ${collapsible ? '' : 'incident-head-static'} ${collapsible && !isOpen ? 'incident-head-collapsed' : ''}`}
        {...(collapsible ? { onClick: () => setOpen(o => !o) } : {})}
      >
        <div className="incident-head-top">
          <div className="incident-head-pills">
            <span className="pill pill-info"><span className="dot" />Maintenance</span>
            <span className={`pill pill-${statusPillColor}`}><span className="dot" />{statusLabel}</span>
          </div>
          <div className="incident-head-r">
            <span className="muted">{relTime(m.startsAt, now)}</span>
            {collapsible && (
              <svg className={`chev ${isOpen ? 'open' : ''}`} width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M6 9l6 6 6-6"/></svg>
            )}
          </div>
        </div>
        <div className="incident-title">{m.title}</div>
      </HeadTag>

      <div className={`incident-body ${isOpen ? 'open' : ''}`}>
        <div className="incident-body-inner">
          {m.description && <p className="incident-desc">{m.description}</p>}
          <div className="incident-meta">
            <div><span className="muted">Status</span><b>{statusLabel}</b></div>
            <div><span className="muted">Window</span><b>{m.window}</b></div>
            <div><span className="muted">Affects</span><b>{(m.affects || []).join(', ') || '—'}</b></div>
          </div>

          <ol className="timeline">
            {MNT_TIMELINE_STEPS.map((step, idx) => {
              const isPast    = currentIdx > -1 && idx < currentIdx;
              const isCurrent = idx === currentIdx;
              return (
                <li key={step} className={`tl-step tl-${_mntSlug(step)} ${isPast ? 'on' : ''} ${isCurrent ? 'current' : ''}`}>
                  <span className="tl-mark"><span /></span>
                  <span className="tl-name">{step}</span>
                </li>
              );
            })}
          </ol>

          {m.updates && m.updates.length > 0 && (
            <ul className="updates">
              {m.updates.map((u, i) => (
                <li key={i} className="update">
                  <div className="update-head">
                    <span className={`update-tag tag-${_mntSlug(u.label)}`}>{u.label}</span>
                    <span className="muted">{fmtDateTime(u.at)} · {relTime(u.at, now)}</span>
                  </div>
                  <p>{u.text}</p>
                </li>
              ))}
            </ul>
          )}
        </div>
      </div>
    </section>
  );
}

function MaintenanceSection({ maintenance = [] }) {
  if (maintenance.length === 0) return null;
  return (
    <section className="card">
      <div className="card-head">
        <div>
          <h2>Scheduled maintenance</h2>
          <p className="muted">Upcoming planned work. We post these at least 72 hours in advance.</p>
        </div>
      </div>
      <ul className="maint">
        {maintenance.map(m => (
          <li key={m.id} className="maint-row">
            <div className="maint-icon" aria-hidden="true">
              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg>
            </div>
            <div className="maint-body">
              <div className="maint-title">{m.title}</div>
              <div className="maint-window">{m.window}</div>
              <div className="maint-impact muted">{m.impact}</div>
              <div className="maint-affects">
                {m.affects.map(a => <span key={a} className="chip">{a}</span>)}
              </div>
            </div>
          </li>
        ))}
      </ul>
    </section>
  );
}

// ─── Past incidents ─────────────────────────────────────────────────────────

function PastIncidents({ incidents = [] }) {
  // Group by calendar month (Y-m-d strings parsed deterministically to avoid
  // any local-vs-UTC day shift). Incidents arrive newest-first from the API,
  // so the resulting month order is also newest-first.
  const groups = useMemo(() => {
    const map = new Map();
    incidents.forEach(inc => {
      const [year, month, day] = inc.date.split('-').map(s => parseInt(s, 10));
      const key       = `${year}-${String(month).padStart(2, '0')}`;
      const monthDate = new Date(year, month - 1, 1);
      const label      = monthDate.toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
      const monthShort = monthDate.toLocaleDateString('en-US', { month: 'short' });
      if (!map.has(key)) map.set(key, { label, monthShort, items: [] });
      map.get(key).items.push({ ...inc, _day: day });
    });
    return [...map.values()];
  }, [incidents]);

  if (incidents.length === 0) return null;

  return (
    <section className="card">
      <div className="card-head">
        <div>
          <h2>Incident history</h2>
          <p className="muted">Past 90 days.</p>
        </div>
        <span className="muted past-total">
          {incidents.length} {incidents.length === 1 ? 'incident' : 'incidents'}
        </span>
      </div>

      <div className="past-groups">
        {groups.map(g => (
          <div key={g.label} className="past-month">
            <div className="past-month-head">
              <span className="past-month-label">{g.label}</span>
              <span className="past-month-count">{g.items.length}</span>
            </div>
            <ul className="past-list">
              {g.items.map(p => {
                const sevClass = p.severity === 'major' ? 'bad'
                              : p.severity === 'maintenance' ? 'info'
                              : 'warn';
                // The public API prefixes past-incident ids with 'p' so they
                // don't collide with active-incident ids in this same payload.
                // Strip it before hitting /api/incidents.php?id=N on the detail page.
                const numericId = typeof p.id === 'string' && p.id.startsWith('p') ? p.id.slice(1) : p.id;
                const onClick = e => {
                  if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
                  if (!window.publicNavigate) return;
                  e.preventDefault();
                  window.publicNavigate('incident', { id: numericId });
                };
                return (
                  <li key={p.id}>
                    <a className="past-item" href={`/incident/?id=${numericId}`} onClick={onClick}>
                      <div className="past-item-body">
                        <div className="past-item-title">{p.title}</div>
                        <div className="past-item-sub">
                          {fmtDateOnly(p.date)} · {p.duration}
                        </div>
                      </div>
                      <span className={`pill pill-sm pill-${sevClass}`}>
                        <span className="dot" />
                        {p.severity[0].toUpperCase() + p.severity.slice(1)}
                      </span>
                      <span className="pill pill-sm pill-ok">
                        <span className="dot" />
                        Resolved
                      </span>
                    </a>
                  </li>
                );
              })}
            </ul>
          </div>
        ))}
      </div>
    </section>
  );
}

// ─── Maintenance history ────────────────────────────────────────────────────

// Same structure as PastIncidents — grouped by month, each row a link to the
// dedicated detail page. The row gets a "Maintenance" + "Completed" pill pair
// so it visually distinguishes itself from past incidents above.
function MaintenanceHistory({ maintenance = [] }) {
  const groups = useMemo(() => {
    const map = new Map();
    maintenance.forEach(m => {
      const [year, month, day] = m.date.split('-').map(s => parseInt(s, 10));
      const key       = `${year}-${String(month).padStart(2, '0')}`;
      const monthDate = new Date(year, month - 1, 1);
      const label     = monthDate.toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
      const monthShort = monthDate.toLocaleDateString('en-US', { month: 'short' });
      if (!map.has(key)) map.set(key, { label, monthShort, items: [] });
      map.get(key).items.push({ ...m, _day: day });
    });
    return [...map.values()];
  }, [maintenance]);

  if (maintenance.length === 0) return null;

  return (
    <section className="card">
      <div className="card-head">
        <div>
          <h2>Maintenance history</h2>
          <p className="muted">Completed planned work, past 90 days.</p>
        </div>
        <span className="muted past-total">
          {maintenance.length} {maintenance.length === 1 ? 'window' : 'windows'}
        </span>
      </div>

      <div className="past-groups">
        {groups.map(g => (
          <div key={g.label} className="past-month">
            <div className="past-month-head">
              <span className="past-month-label">{g.label}</span>
              <span className="past-month-count">{g.items.length}</span>
            </div>
            <ul className="past-list">
              {g.items.map(p => {
                // API prefixes maintenance ids with 'm' to avoid colliding with
                // incidents; strip before hitting /api/maintenance.php?id=N.
                const numericId = typeof p.id === 'string' && p.id.startsWith('m') ? p.id.slice(1) : p.id;
                const onClick = e => {
                  if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
                  if (!window.publicNavigate) return;
                  e.preventDefault();
                  window.publicNavigate('maintenance', { id: numericId });
                };
                return (
                  <li key={p.id}>
                    <a className="past-item" href={`/maintenance/?id=${numericId}`} onClick={onClick}>
                      <div className="past-item-body">
                        <div className="past-item-title">{p.title}</div>
                        <div className="past-item-sub">
                          {fmtDateOnly(p.date)} · {p.duration}
                        </div>
                      </div>
                      <span className="pill pill-sm pill-info">
                        <span className="dot" />
                        Maintenance
                      </span>
                      <span className="pill pill-sm pill-ok">
                        <span className="dot" />
                        Completed
                      </span>
                    </a>
                  </li>
                );
              })}
            </ul>
          </div>
        ))}
      </div>
    </section>
  );
}

// ─── Footer ─────────────────────────────────────────────────────────────────

function Footer() {
  const year = new Date().getFullYear();
  return (
    <footer className="ftr">
      <div className="ftr-l">
        <p className="muted">© {year} <a href="https://dmu.gr">DMU</a></p>
        <p className="ftr-contact">
          <a href="mailto:customers@dmu.gr">customers@dmu.gr</a>
          <span className="ftr-sep">·</span>
          <a href="tel:+302108043621">+30 210 80 43 621</a>
        </p>
      </div>
      <div className="ftr-links">
        <a href="/how-status-works/">How Status Works</a>
        <a href="https://dmu.gr/sla" target="_blank" rel="noopener">SLA</a>
        <a href="https://dmu.gr/terms" target="_blank" rel="noopener">Terms of Service</a>
        <a href="https://dmu.gr/privacy" target="_blank" rel="noopener">Privacy Policy</a>
      </div>
    </footer>
  );
}

// ─── Standalone page views (SPA routes) ─────────────────────────────────────

// Renders the dedicated incident detail route (/incident/?id=N) inside the
// public shell. Accepts `id` as a prop so the SPA router can pass it directly;
// falls back to the URL on direct loads.
function IncidentPage({ id: idProp }) {
  const [incident, setIncident] = useState(null);
  const [error,    setError]    = useState(null);
  const [now,      setNow]      = useState(new Date());

  const id = idProp != null
    ? idProp
    : new URLSearchParams(window.location.search).get('id');

  useEffect(() => {
    const i = setInterval(() => setNow(new Date()), 1000);
    return () => clearInterval(i);
  }, []);

  useEffect(() => {
    if (!id) { setError('Missing id'); return; }
    setIncident(null); setError(null);
    apiFetch('/api/incidents.php?id=' + encodeURIComponent(id))
      .then(setIncident)
      .catch(err => setError(err.message));
  }, [id]);

  // Reflect the loaded incident in the browser tab title (and in
  // bookmarks / shared links). The static <title> in incident/index.html
  // stays as "Incident Detail — DMU Status" so crawlers and the
  // first-paint state have something sensible; this overrides it
  // once the live data lands.
  useEffect(() => {
    if (incident && incident.title) {
      document.title = incident.title + ' — DMU Status';
    } else if (error) {
      document.title = 'Incident not found — DMU Status';
    }
  }, [incident, error]);

  function onBack(e) {
    if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
    e.preventDefault();
    window.publicNavigate && window.publicNavigate('home');
  }

  return (
    <React.Fragment>
      <Header now={now} />
      <main className="shell">
        <div className="incident-page-nav">
          <a href="/" onClick={onBack}>← Back to status</a>
        </div>

        {error && (
          <section className="card">
            <h2>Incident not found</h2>
            <p className="muted" style={{ marginTop: 6 }}>{error}</p>
          </section>
        )}

        {!error && !incident && (
          <p className="muted" style={{ padding: '24px 0' }}>Loading…</p>
        )}

        {incident && <ActiveIncident incident={incident} now={now} collapsible={false} />}

        <Footer />
      </main>
    </React.Fragment>
  );
}

// Same shape as IncidentPage but for the maintenance detail route. The admin
// endpoint is publicly readable; we reshape its `components` field into
// `affects` and pre-format the window string client-side so we don't need a
// separate public endpoint.
function MaintenancePage({ id: idProp }) {
  const [m,     setM]     = useState(null);
  const [error, setError] = useState(null);
  const [now,   setNow]   = useState(new Date());

  const id = idProp != null
    ? idProp
    : new URLSearchParams(window.location.search).get('id');

  useEffect(() => {
    const i = setInterval(() => setNow(new Date()), 1000);
    return () => clearInterval(i);
  }, []);

  useEffect(() => {
    if (!id) { setError('Missing id'); return; }
    setM(null); setError(null);
    apiFetch('/api/maintenance.php?id=' + encodeURIComponent(id))
      .then(raw => {
        setM({
          id:       'm' + raw.id,
          title:    raw.title,
          description: raw.description,
          status:   raw.status,
          startsAt: raw.startsAt,
          endsAt:   raw.endsAt,
          window:   fmtMaintWindow(raw.startsAt, raw.endsAt),
          affects:  raw.components,
          updates:  raw.updates,
        });
      })
      .catch(err => setError(err.message));
  }, [id]);

  function onBack(e) {
    if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
    e.preventDefault();
    window.publicNavigate && window.publicNavigate('home');
  }

  return (
    <React.Fragment>
      <Header now={now} />
      <main className="shell">
        <div className="incident-page-nav">
          <a href="/" onClick={onBack}>← Back to status</a>
        </div>

        {error && (
          <section className="card">
            <h2>Maintenance not found</h2>
            <p className="muted" style={{ marginTop: 6 }}>{error}</p>
          </section>
        )}

        {!error && !m && (
          <p className="muted" style={{ padding: '24px 0' }}>Loading…</p>
        )}

        {m && <MaintenanceCard m={m} now={now} collapsible={false} />}

        <Footer />
      </main>
    </React.Fragment>
  );
}

// ─── Monitor detail page (SPA route /monitor/?id=N) ─────────────────────────

// Public-safe monitor detail view. Backs /monitor/?id=N. Mirrors the admin
// MonitorDetailView layout (hero + stats grid + history bars + details + recent
// checks table) but is driven by /api/monitor_public.php, which intentionally
// withholds anything not already implied by the homepage (target, type, region,
// owner, threshold, per-check response_ms). Component itself doesn't enforce
// the redaction — it just renders whatever the API hands back.
const MONITOR_RANGE_LABELS = {
  '1h':  '60-minute',
  '1d':  '24-hour',
  '7d':  '7-day',
  '30d': '30-day',
  '90d': '90-day',
};

function fmtMonInterval(sec) {
  if (sec == null) return '—';
  if (sec < 60) return `every ${sec}s`;
  if (sec % 60 === 0 && sec < 3600) return `every ${sec / 60}m`;
  if (sec % 3600 === 0) return `every ${sec / 3600}h`;
  return `every ${Math.round(sec / 60)}m`;
}

function MonStatCard({ label, value, unit, sub, color }) {
  return (
    <div className="mon-stat">
      <div className="mon-stat-lbl">{label}</div>
      <div className="mon-stat-val" style={color ? { color } : undefined}>
        {value}
        {unit && <span className="mon-stat-unit">{unit}</span>}
      </div>
      {sub && <div className="mon-stat-sub">{sub}</div>}
    </div>
  );
}

function MonitorHistory({ history, overlays = [], range, setRange }) {
  const [hovered, setHovered] = useState(null);
  const buckets = pickHistory(history, range);
  const total   = buckets.length;

  // Mirror the UptimeRow handoff: while the newest bucket is -1 we're waiting
  // on a probe, so show the shimmer + spinner tooltip. The moment it flips to
  // a real value, play a one-shot reveal so it doesn't pop into place.
  const [landed, setLanded] = useState(false);
  const prevLatest = useRef(total > 0 ? buckets[total - 1] : null);
  useEffect(() => {
    if (total === 0) return;
    const cur = buckets[total - 1];
    const prev = prevLatest.current;
    prevLatest.current = cur;
    if (prev === -1 && cur !== -1 && cur != null) {
      setLanded(true);
      const t = setTimeout(() => setLanded(false), 600);
      return () => clearTimeout(t);
    }
  }, [buckets, total]);

  // Same overlay rule as UptimeRow: per-bar lookup against the full overlays
  // array (resolved incidents + maintenance windows, last 90d). An overlay
  // only repaints a bucket when its rank is STRICTLY greater than the probe
  // state — real outages are never visually downgraded.
  const bucketMs = RANGE_BUCKET_MS[range] ?? 60_000;
  const nowMs    = Date.now();

  function bucketLabel(i) {
    const now = new Date();
    if (range === '1h') {
      const d = new Date(now.getTime() - (total - 1 - i) * 60_000);
      return d.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false });
    }
    if (range === '1d') {
      const d = new Date(now.getTime() - (total - 1 - i) * 3_600_000);
      return d.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false });
    }
    const d = new Date(now);
    d.setDate(d.getDate() - (total - 1 - i));
    return fmtDateOnly(d);
  }

  // Show the range whenever it contains any real probe data — see the note
  // on the homepage's rangeCovered. Showing sparse bars beats hiding the
  // whole range over a missed probe at the cutoff.
  function rangeCovered(key) {
    const h = pickHistory(history, key);
    return h.some(v => v >= 0);
  }
  const visibleRanges = RANGES.filter(r => r.key === range || rangeCovered(r.key));
  const win = RANGE_WINDOW_LABEL[range] || RANGE_WINDOW_LABEL['90d'];

  return (
    <section className="card mon-history-card">
      <div className="card-head">
        <div>
          <h2>Uptime history</h2>
          <p className="muted">{MONITOR_RANGE_LABELS[range] || range} window, smoothed bucket view.</p>
        </div>
        <div className="seg-row">
          <div className="seg">
            {visibleRanges.map(r => (
              <button key={r.key} className={range === r.key ? 'on' : ''} onClick={() => setRange(r.key)}>{r.label}</button>
            ))}
          </div>
        </div>
      </div>

      <div className="srv-legend">
        <div className="legend-l">
          <span><span className="dot" style={{ background: 'var(--ok)' }} /> Operational</span>
          <span><span className="dot" style={{ background: 'var(--warn)' }} /> Degraded</span>
          <span><span className="dot" style={{ background: 'var(--bad)' }} /> Outage</span>
        </div>
        <div className="legend-r">
          <span className="muted">{win.left}</span>
          <span className="legend-line" />
          <span className="muted">{win.right}</span>
        </div>
      </div>

      <div className="mon-bars-wrap">
        <div className="bars mon-bars" onMouseLeave={() => setHovered(null)}>
          {buckets.map((d, i) => {
            const bEnd    = nowMs - (total - 1 - i) * bucketMs;
            const bStart  = bEnd - bucketMs;
            const overlay = pickOverlayForBucket(overlays, bStart, bEnd, nowMs);
            const probeRank   = SEVERITY_RANK[DAY_META[d].status] ?? 0;
            const overlayWins = overlay !== null && overlay.rank > probeRank;
            const color = overlayWins ? overlay.meta.color : DAY_META[d].color;
            const isFetching   = i === total - 1 && d === -1 && !overlayWins;
            const isJustLanded = i === total - 1 && landed;
            return (
              <div
                key={i}
                className={`bar bar-${d}${overlayWins ? ' bar-inc' : ''}${isFetching ? ' bar-fetching' : ''}${isJustLanded ? ' bar-just-landed' : ''}`}
                style={{ '--bar-color': color, '--bar-i': i }}
                onMouseEnter={() => setHovered(i)}
              />
            );
          })}
          {hovered != null && (() => {
            const bEnd    = nowMs - (total - 1 - hovered) * bucketMs;
            const bStart  = bEnd - bucketMs;
            const overlay = pickOverlayForBucket(overlays, bStart, bEnd, nowMs);
            const probeRank   = SEVERITY_RANK[DAY_META[buckets[hovered]].status] ?? 0;
            const overlayWins = overlay !== null && overlay.rank > probeRank;
            const tipColor = overlayWins ? overlay.meta.color : DAY_META[buckets[hovered]].color;
            const tipLabel = overlayWins ? overlay.meta.label : DAY_META[buckets[hovered]].label;
            const isFetching = hovered === total - 1 && buckets[hovered] === -1 && !overlayWins;
            return (
              <div className="bar-tip" style={{ left: `calc(${(hovered + 0.5) / total * 100}% )` }}>
                <div className="bar-tip-d">{bucketLabel(hovered)}</div>
                <div className="bar-tip-s">
                  {isFetching
                    ? <><span className="bar-tip-spinner" />Fetching status</>
                    : <><span className="dot" style={{ background: tipColor }} />{tipLabel}</>}
                </div>
              </div>
            );
          })()}
        </div>
      </div>
    </section>
  );
}

const MONITOR_CHECKS_PAGE_SIZE = 20;

function MonitorPage({ id: idProp }) {
  const [data,  setData]  = useState(null);
  const [error, setError] = useState(null);
  const [now,   setNow]   = useState(new Date());
  const [range, setRange] = useState('1h');
  // Persisted across the 30s refresh — new checks land at index 0, shifting
  // later rows down a slot but leaving the user's chosen page in place.
  const [checksPage, setChecksPage] = useState(0);

  const id = idProp != null
    ? idProp
    : new URLSearchParams(window.location.search).get('id');

  useEffect(() => {
    const i = setInterval(() => setNow(new Date()), 1000);
    return () => clearInterval(i);
  }, []);

  // Same polling cadence as the home page so the bars + recent checks stay live.
  useEffect(() => {
    if (!id) { setError('Missing id'); return; }
    let alive = true;
    function load() {
      apiFetch('/api/monitor_public.php?id=' + encodeURIComponent(id))
        .then(d => { if (alive) { setData(d); setError(null); } })
        .catch(err => { if (alive) setError(err.message); });
    }
    load();
    const t = setInterval(load, 30000);
    return () => { alive = false; clearInterval(t); };
  }, [id]);

  // Reflect the loaded monitor in the browser tab title (and in
  // bookmarks / shared links). The static <title> in monitor/index.html
  // stays as "Service Monitor — DMU Status" for crawlers and the
  // first-paint state; this overrides it once the live data lands.
  useEffect(() => {
    const name = data && data.monitor && data.monitor.name;
    if (name) {
      document.title = name + ' — DMU Status';
    } else if (error) {
      document.title = 'Monitor not found — DMU Status';
    }
  }, [data, error]);

  function onBack(e) {
    if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
    e.preventDefault();
    window.publicNavigate && window.publicNavigate('home');
  }

  const m     = data?.monitor;
  const stats = data?.stats;
  const meta  = m ? STATUS_META[m.status] : null;

  return (
    <React.Fragment>
      <Header now={now} />
      <main className="shell">
        <div className="incident-page-nav">
          <a href="/" onClick={onBack}>← Back to status</a>
        </div>

        {error && (
          <section className="card">
            <h2>Monitor not found</h2>
            <p className="muted" style={{ marginTop: 6 }}>{error}</p>
          </section>
        )}

        {!error && !data && (
          <p className="muted" style={{ padding: '24px 0' }}>Loading…</p>
        )}

        {m && (
          <React.Fragment>
            <section className="card mon-hero">
              <div className="mon-hero-row">
                <span
                  className="info-tip srv-dot-tip mon-hero-dot-tip"
                  tabIndex={0}
                  aria-label={`${meta.label}, ${m.lastCheckedAt ? 'updated ' + relTime(m.lastCheckedAt, now) : 'never checked'}`}
                >
                  <span
                    className="srv-dot mon-hero-dot"
                    style={{ background: meta.dot, color: meta.dot }}
                  />
                  <span className="info-tip-bubble srv-dot-bubble">
                    <div className="srv-dot-tip-row">
                      <span className="srv-dot" style={{ background: meta.dot, color: meta.dot }} />
                      <div>
                        <div className="srv-dot-tip-title">{meta.label}</div>
                        <div className="srv-dot-tip-meta">
                          {m.lastCheckedAt ? 'Updated ' + relTime(m.lastCheckedAt, now) : 'No checks yet'}
                        </div>
                      </div>
                    </div>
                  </span>
                </span>
                <h1 className="mon-hero-title">{m.name}</h1>
              </div>
              {m.description && <p className="mon-hero-desc">{m.description}</p>}
              <div className="mon-hero-meta">
                {m.group && (
                  <React.Fragment>
                    <span className="muted">{m.group}</span>
                    <span className="mon-hero-sep">·</span>
                  </React.Fragment>
                )}
                <span className="muted">
                  {m.lastCheckedAt ? `Last checked ${relTime(m.lastCheckedAt, now)}` : 'No checks yet'}
                </span>
                <span className="mon-hero-sep">·</span>
                <span className="muted">Probed {fmtMonInterval(m.interval)}</span>
                {m.countsForUptime === false && (
                  <React.Fragment>
                    <span className="mon-hero-sep">·</span>
                    <span className="muted">Excluded from overall uptime</span>
                  </React.Fragment>
                )}
              </div>
            </section>

            <section className="card">
              <div className="mon-stats">
                <MonStatCard
                  label="Uptime · 24h"
                  value={stats.uptime24h != null ? stats.uptime24h.toFixed(2) : '—'}
                  unit={stats.uptime24h != null ? '%' : ''}
                  sub={`${stats.totalChecks24h} ${stats.totalChecks24h === 1 ? 'check' : 'checks'}`}
                  color="var(--ok)"
                />
                <MonStatCard
                  label="Uptime · 7d"
                  value={stats.uptime7d != null ? stats.uptime7d.toFixed(2) : '—'}
                  unit={stats.uptime7d != null ? '%' : ''}
                  sub="rolling window"
                  color="var(--ok)"
                />
                <MonStatCard
                  label="Uptime · 30d"
                  value={stats.uptime30d != null ? stats.uptime30d.toFixed(2) : '—'}
                  unit={stats.uptime30d != null ? '%' : ''}
                  sub="rolling window"
                  color="var(--ok)"
                />
                <MonStatCard
                  label="Avg response · 24h"
                  value={stats.avgResponseMs24h != null ? Math.round(stats.avgResponseMs24h) : '—'}
                  unit={stats.avgResponseMs24h != null ? 'ms' : ''}
                  sub="across all probes"
                  color="var(--accent)"
                />
                <MonStatCard
                  label="Failures · 24h"
                  value={stats.failures24h}
                  sub={stats.failures24h === 0 ? 'all clear' : 'down events'}
                  color={stats.failures24h > 0 ? 'var(--bad)' : 'var(--text)'}
                />
                <MonStatCard
                  label="Last check"
                  value={m.lastCheckedAt ? relTime(m.lastCheckedAt, now) : '—'}
                  sub={m.lastCheckedAt ? fmtTimeOnly(m.lastCheckedAt) : 'no probe yet'}
                  color="var(--info)"
                />
              </div>
            </section>

            <MonitorHistory
              history={m.history}
              overlays={m.overlays}
              range={range}
              setRange={setRange}
            />

            {(() => {
              const all   = data.recentChecks;
              const total = all.length;
              const totalPages = Math.max(1, Math.ceil(total / MONITOR_CHECKS_PAGE_SIZE));
              // Clamp in case the dataset shrank — e.g. a row was removed in admin
              // and the user is sitting on what used to be the last page.
              const activePage = Math.min(checksPage, totalPages - 1);
              const start  = activePage * MONITOR_CHECKS_PAGE_SIZE;
              const slice  = all.slice(start, start + MONITOR_CHECKS_PAGE_SIZE);
              const showPager = total > MONITOR_CHECKS_PAGE_SIZE;
              return (
                <section className="card mon-checks-card">
                  <div className="card-head">
                    <div>
                      <h2>Recent checks</h2>
                      <p className="muted">
                        {total === 0
                          ? 'No probes recorded yet.'
                          : showPager
                            ? `Showing ${start + 1}–${start + slice.length} of ${total}.`
                            : `Latest ${total} probe${total === 1 ? '' : 's'}.`}
                      </p>
                    </div>
                  </div>
                  {total === 0 ? (
                    <p className="muted">The prober runs {fmtMonInterval(m.interval)} — check back shortly.</p>
                  ) : (
                    <React.Fragment>
                      <table className="tbl mon-checks-tbl">
                        <thead>
                          <tr>
                            <th>Time</th>
                            <th style={{ width: 140 }}>Status</th>
                            <th style={{ width: 90 }}>HTTP</th>
                            <th>Error</th>
                          </tr>
                        </thead>
                        <tbody>
                          {slice.map((c, i) => {
                            const pillClass = c.status === 'up'       ? 'ok'
                                            : c.status === 'degraded' ? 'warn'
                                            : c.status === 'down'     ? 'bad'
                                            :                           'info';
                            const statusLabel = c.status === 'up' ? 'Operational'
                                             : c.status[0].toUpperCase() + c.status.slice(1);
                            return (
                              <tr key={start + i}>
                                <td className="cell-mono">{c.checkedAt ? fmtDateTime(c.checkedAt) : '—'}</td>
                                <td>
                                  <span className={`pill pill-sm pill-${pillClass}`}>
                                    <span className="dot" />{statusLabel}
                                  </span>
                                </td>
                                <td className="cell-mono">{c.httpCode ?? <span className="muted">—</span>}</td>
                                <td className="muted">{c.error || '—'}</td>
                              </tr>
                            );
                          })}
                        </tbody>
                      </table>
                      {showPager && (
                        <div className="pager">
                          <button
                            className="btn btn-ghost btn-sm"
                            disabled={activePage === 0}
                            onClick={() => setChecksPage(p => Math.max(0, p - 1))}
                          >
                            ← Previous
                          </button>
                          <span className="muted pager-status">Page {activePage + 1} of {totalPages}</span>
                          <button
                            className="btn btn-ghost btn-sm"
                            disabled={activePage >= totalPages - 1}
                            onClick={() => setChecksPage(p => Math.min(totalPages - 1, p + 1))}
                          >
                            Next →
                          </button>
                        </div>
                      )}
                    </React.Fragment>
                  )}
                </section>
              );
            })()}
          </React.Fragment>
        )}

        <Footer />
      </main>
    </React.Fragment>
  );
}

Object.assign(window, {
  Header, OverallBanner, ActiveIncident, ActiveMaintenance, MaintenanceCard,
  ServicesSection,
  MetricsSection, MaintenanceSection, PastIncidents, MaintenanceHistory,
  Footer,
  IncidentPage, MaintenancePage, MonitorPage,
  fmtMaintWindow,
});
