<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TicVento Status</title>
<meta name="description" content="Live platform status for TicVento — website, buyer & seller apps, API, payments, search, and more.">
<link rel="canonical" href="https://status.ticvento.com/">
<meta name="robots" content="index,follow">
<link rel="icon" href="https://ticvento.com/icons/logo-32.webp" type="image/webp">
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700;1,9..40,400&family=Fraunces:opsz,wght@9..144,600;9..144,700&display=swap" rel="stylesheet">
<style>
  :root {
    --bg: #f4f7f6;
    --card: #ffffff;
    --ink: #14201c;
    --muted: #5c6b66;
    --line: #dce5e1;
    --ok: #0d9f6e;
    --ok-soft: #e6f7f0;
    --warn: #c97816;
    --warn-soft: #fff6e8;
    --bad: #d64545;
    --bad-soft: #fdeeee;
    --brand: #00b894;
    --banner-ok: #0d9f6e;
    --banner-warn: #c97816;
    --banner-bad: #c23b3b;
    --bar-ok: #0d9f6e;
    --bar-warn: #e8a23a;
    --bar-bad: #d64545;
    --bar-none: #e8eeeb;
  }
  * { box-sizing: border-box; margin: 0; padding: 0; }
  body {
    font-family: 'DM Sans', system-ui, sans-serif;
    background: var(--bg);
    color: var(--ink);
    min-height: 100vh;
    line-height: 1.5;
  }
  a { color: inherit; }
  .wrap { max-width: 820px; margin: 0 auto; padding: 1.5rem 1.25rem 4rem; }

  header {
    display: flex; align-items: center; justify-content: space-between;
    gap: 1rem; margin-bottom: 1.75rem; flex-wrap: wrap;
  }
  .brand {
    display: inline-flex; align-items: center; gap: .65rem;
    text-decoration: none; color: var(--ink);
  }
  .brand img { width: 34px; height: 34px; border-radius: 10px; }
  .brand-text { font-family: 'Fraunces', Georgia, serif; font-weight: 700; font-size: 1.35rem; letter-spacing: -.02em; }
  .brand-text span { color: var(--brand); }
  .header-actions { display: flex; gap: .5rem; align-items: center; }
  .btn {
    display: inline-flex; align-items: center; gap: .4rem;
    border: 1px solid var(--line); background: var(--card);
    color: var(--ink); font: inherit; font-size: .8rem; font-weight: 600;
    padding: .55rem 1rem; border-radius: 999px; cursor: pointer;
    text-decoration: none; transition: background .15s, border-color .15s;
  }
  .btn:hover { background: #eef5f2; border-color: #c5d4ce; }
  .btn-dark { background: var(--ink); color: #fff; border-color: var(--ink); }
  .btn-dark:hover { background: #24352f; }

  .banner {
    border-radius: 14px; padding: 1.35rem 1.25rem; text-align: center;
    color: #fff; font-weight: 700; font-size: clamp(1.15rem, 3vw, 1.55rem);
    letter-spacing: -.02em; margin-bottom: 1.75rem;
    box-shadow: 0 10px 30px rgba(13, 159, 110, .18);
    transition: background .3s;
  }
  .banner.ok { background: var(--banner-ok); }
  .banner.warn { background: var(--banner-warn); box-shadow: 0 10px 30px rgba(201, 120, 22, .2); }
  .banner.bad { background: var(--banner-bad); box-shadow: 0 10px 30px rgba(194, 59, 59, .2); }
  .banner.loading { background: #6b7f78; box-shadow: none; }
  .banner-sub { display: block; margin-top: .35rem; font-size: .78rem; font-weight: 500; opacity: .9; }

  .meta-row {
    display: flex; justify-content: space-between; align-items: baseline;
    gap: 1rem; flex-wrap: wrap; margin-bottom: .85rem;
    font-size: .78rem; color: var(--muted);
  }
  .meta-row a { color: var(--brand); font-weight: 600; text-decoration: none; }
  .meta-row a:hover { text-decoration: underline; }

  .services {
    background: var(--card); border: 1px solid var(--line);
    border-radius: 16px; overflow: hidden;
  }
  .svc {
    padding: 1.1rem 1.15rem 1rem;
    border-bottom: 1px solid var(--line);
  }
  .svc:last-child { border-bottom: none; }
  .svc-top {
    display: flex; justify-content: space-between; align-items: flex-start;
    gap: .75rem; margin-bottom: .65rem;
  }
  .svc-name { font-weight: 700; font-size: .95rem; }
  .svc-desc { font-size: .75rem; color: var(--muted); font-weight: 500; margin-top: .1rem; }
  .svc-status {
    font-size: .8rem; font-weight: 700; white-space: nowrap;
    display: inline-flex; align-items: center; gap: .35rem;
  }
  .svc-status.ok { color: var(--ok); }
  .svc-status.warn { color: var(--warn); }
  .svc-status.bad { color: var(--bad); }
  .dot {
    width: 8px; height: 8px; border-radius: 50%; background: currentColor;
    box-shadow: 0 0 0 3px color-mix(in srgb, currentColor 22%, transparent);
  }

  .bars {
    display: flex; gap: 1px; height: 34px; width: 100%;
    border-radius: 4px; overflow: hidden;
  }
  .bar {
    flex: 1; min-width: 0; background: var(--bar-ok);
    border-radius: 1px; transition: transform .12s;
  }
  .bar.warn { background: var(--bar-warn); }
  .bar.bad { background: var(--bar-bad); }
  .bar.none { background: var(--bar-none); }
  .bar:hover { transform: scaleY(1.12); }

  .bar-legend {
    display: flex; justify-content: space-between; align-items: center;
    margin-top: .45rem; font-size: .7rem; color: var(--muted); font-weight: 500;
  }
  .bar-legend strong { color: var(--ink); font-weight: 700; }

  .section-title {
    font-size: .72rem; font-weight: 700; text-transform: uppercase;
    letter-spacing: .08em; color: var(--muted); margin: 2rem 0 .75rem;
  }
  .incidents {
    background: var(--card); border: 1px solid var(--line);
    border-radius: 16px; padding: 1.15rem 1.25rem;
  }
  .incident-empty { color: var(--muted); font-size: .9rem; }
  .incident {
    padding: .85rem 0; border-bottom: 1px solid var(--line);
  }
  .incident:last-child { border-bottom: none; padding-bottom: 0; }
  .incident:first-child { padding-top: 0; }
  .incident-title { font-weight: 700; font-size: .92rem; }
  .incident-meta { font-size: .75rem; color: var(--muted); margin-top: .2rem; }
  .incident-body { font-size: .85rem; margin-top: .45rem; color: #33443e; }

  footer {
    margin-top: 2.5rem; padding-top: 1.25rem; border-top: 1px solid var(--line);
    display: flex; justify-content: space-between; gap: 1rem; flex-wrap: wrap;
    font-size: .75rem; color: var(--muted);
  }
  footer a { color: var(--brand); font-weight: 600; text-decoration: none; }
  footer a:hover { text-decoration: underline; }

  .skel { opacity: .55; }
  @media (max-width: 560px) {
    .wrap { padding: 1.1rem 1rem 3rem; }
    .bars { height: 28px; }
  }
</style>
</head>
<body>
  <div class="wrap">
    <header>
      <a class="brand" href="https://ticvento.com/">
        <img src="https://ticvento.com/icons/logo-32.webp" width="34" height="34" alt="">
        <div class="brand-text">Tic<span>Vento</span> Status</div>
      </a>
      <div class="header-actions">
        <button type="button" class="btn" id="btnRefresh" title="Re-check now">Refresh</button>
        <a class="btn btn-dark" href="mailto:support@ticvento.com?subject=Status%20updates">Subscribe to updates</a>
      </div>
    </header>

    <div class="banner loading" id="overallBanner" role="status" aria-live="polite">
      <span id="overallTitle">Checking systems…</span>
      <span class="banner-sub" id="overallSub">Live probes · usually under 5s</span>
    </div>

    <div class="meta-row">
      <span id="checkedAt">—</span>
      <span>Uptime over the past 90 days · <a href="#incidents">View incidents</a></span>
    </div>

    <div class="services" id="services" aria-busy="true">
      <!-- filled by JS -->
    </div>

    <h2 class="section-title" id="incidents">Past incidents</h2>
    <div class="incidents" id="incidentList">
      <p class="incident-empty">No incidents reported in the last 90 days.</p>
    </div>

    <footer>
      <span>Powered by live checks against TicVento services</span>
      <span>
        <a href="https://ticvento.com/">ticvento.com</a>
        ·
        <a href="https://status.ticvento.com/maintenance/">Maintenance page</a>
        ·
        <a href="mailto:support@ticvento.com">support@ticvento.com</a>
      </span>
    </footer>
  </div>

  <script>
  (function () {
    const API = (location.hostname === 'localhost' || location.hostname === '127.0.0.1')
      ? 'http://localhost:3000/api/public/status'
      : 'https://api.ticvento.com/api/public/status';

    const DAYS = 90;
    const LABEL = {
      operational: 'Operational',
      degraded: 'Degraded performance',
      major_outage: 'Major outage',
    };
    const CLASS = {
      operational: 'ok',
      degraded: 'warn',
      major_outage: 'bad',
    };

    /** Optional day markers: { "api": { "2026-07-01": "degraded" } } — edit incidents.json to paint history */
    let dayOverrides = {};
    /** Optional incident cards */
    let incidents = [];

    function dayKey(d) {
      return d.toISOString().slice(0, 10);
    }

    function buildBars(componentId, liveStatus) {
      const frag = document.createDocumentFragment();
      const today = new Date();
      let okDays = 0;
      for (let i = DAYS - 1; i >= 0; i--) {
        const d = new Date(today);
        d.setUTCDate(d.getUTCDate() - i);
        const key = dayKey(d);
        let st = (dayOverrides[componentId] && dayOverrides[componentId][key]) || 'operational';
        // Today's cell reflects the live probe
        if (i === 0) st = liveStatus === 'operational' ? 'operational' : liveStatus;
        if (st === 'operational') okDays++;
        const el = document.createElement('div');
        el.className = 'bar' + (st === 'degraded' ? ' warn' : st === 'major_outage' ? ' bad' : '');
        el.title = key + ' · ' + (LABEL[st] || st);
        frag.appendChild(el);
      }
      const pct = ((okDays / DAYS) * 100).toFixed(2);
      return { frag, pct };
    }

    function renderServices(components) {
      const root = document.getElementById('services');
      root.innerHTML = '';
      root.setAttribute('aria-busy', 'false');
      for (const c of components) {
        const { frag, pct } = buildBars(c.id, c.status);
        const row = document.createElement('div');
        row.className = 'svc';
        const cls = CLASS[c.status] || 'ok';
        const lat = c.latencyMs != null ? ` · ${c.latencyMs} ms` : '';
        row.innerHTML =
          '<div class="svc-top">' +
            '<div><div class="svc-name"></div><div class="svc-desc"></div></div>' +
            '<div class="svc-status ' + cls + '"><span class="dot" aria-hidden="true"></span><span class="st-label"></span></div>' +
          '</div>' +
          '<div class="bars" role="img" aria-label="90-day uptime"></div>' +
          '<div class="bar-legend"><span>90 days ago</span><strong class="uptime"></strong><span>Today</span></div>';
        row.querySelector('.svc-name').textContent = c.name;
        row.querySelector('.svc-desc').textContent = c.description + lat;
        row.querySelector('.st-label').textContent = LABEL[c.status] || c.status;
        row.querySelector('.bars').appendChild(frag);
        row.querySelector('.uptime').textContent = pct + '% uptime';
        root.appendChild(row);
      }
    }

    function renderIncidents() {
      const box = document.getElementById('incidentList');
      if (!incidents.length) {
        box.innerHTML = '<p class="incident-empty">No incidents reported in the last 90 days.</p>';
        return;
      }
      box.innerHTML = incidents.map(function (inc) {
        return '<div class="incident">' +
          '<div class="incident-title"></div>' +
          '<div class="incident-meta"></div>' +
          '<div class="incident-body"></div>' +
        '</div>';
      }).join('');
      const nodes = box.querySelectorAll('.incident');
      incidents.forEach(function (inc, i) {
        nodes[i].querySelector('.incident-title').textContent = inc.title;
        nodes[i].querySelector('.incident-meta').textContent =
          (inc.date || '') + (inc.status ? ' · ' + inc.status : '');
        nodes[i].querySelector('.incident-body').textContent = inc.body || '';
      });
    }

    function setBanner(overall, label, checkedAt) {
      const el = document.getElementById('overallBanner');
      const title = document.getElementById('overallTitle');
      const sub = document.getElementById('overallSub');
      el.className = 'banner ' + (CLASS[overall] || 'ok');
      title.textContent = label || LABEL[overall] || 'Status';
      const when = checkedAt ? new Date(checkedAt) : new Date();
      sub.textContent = 'Updated ' + when.toLocaleString(undefined, {
        dateStyle: 'medium', timeStyle: 'short',
      });
      document.getElementById('checkedAt').textContent = 'Last checked ' + when.toLocaleTimeString();
    }

    async function loadIncidents() {
      try {
        const res = await fetch('incidents.json', { cache: 'no-store' });
        if (!res.ok) return;
        const data = await res.json();
        incidents = Array.isArray(data.incidents) ? data.incidents : [];
        dayOverrides = data.days && typeof data.days === 'object' ? data.days : {};
      } catch (_) { /* optional file */ }
    }

    async function refresh() {
      const btn = document.getElementById('btnRefresh');
      btn.disabled = true;
      try {
        const res = await fetch(API, { cache: 'no-store' });
        if (!res.ok) throw new Error('HTTP ' + res.status);
        const data = await res.json();
        setBanner(data.overall, data.overallLabel, data.checkedAt);
        renderServices(data.components || []);
      } catch (e) {
        setBanner('major_outage', 'Unable to reach status API', new Date().toISOString());
        document.getElementById('services').innerHTML =
          '<div class="svc"><div class="svc-name">Status API</div>' +
          '<div class="svc-desc">Could not load live checks. Try Refresh, or check api.ticvento.com.</div>' +
          '<div class="svc-status bad"><span class="dot"></span>Unreachable</div></div>';
      } finally {
        btn.disabled = false;
      }
    }

    document.getElementById('btnRefresh').addEventListener('click', refresh);
    loadIncidents().then(function () {
      renderIncidents();
      return refresh();
    });
  })();
  </script>
</body>
</html>
