> ## Documentation Index
> Fetch the complete documentation index at: https://docs.teamsoclo.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Stav systému

> Dostupnost, latence a úspěšnost podle rodin modelů, aktualizováno každou minutu

export const StatusBoard = ({lang = "vi"}) => {
  const BASE = "https://status.teamsoclo.site/status/api";
  const T = {
    vi: {
      loading: "Đang tải số liệu...",
      error: "Chưa lấy được số liệu, thử tải lại trang sau ít phút.",
      allOk: "Tất cả dịch vụ đang hoạt động bình thường",
      someDeg: "Một số dịch vụ đang chậm hoặc chập chờn",
      someDown: "Một số dịch vụ đang gián đoạn",
      ok: "Hoạt động",
      degraded: "Chập chờn",
      down: "Gián đoạn",
      nodata: "Chưa có dữ liệu",
      idle: "Đang chờ request mới",
      live: "Trực tiếp",
      secAgo: "giây trước",
      justNow: "vừa cập nhật",
      ago: "trước",
      now: "bây giờ",
      min: "phút",
      liveLatency: "Độ trễ trực tiếp",
      liveSuccess: "Thành công · 60 giây",
      avgResponse: "Độ trễ trung bình · 24 giờ",
      uptime24: "Thành công · 24 giờ",
      uptime7: "Thành công · 7 ngày",
      uptime30: "Thành công · 30 ngày",
      chartTitle: "Độ trễ phản hồi",
      rLive: "Trực tiếp",
      r6: "6 giờ",
      r24: "24 giờ",
      viewTable: "Xem dạng bảng",
      viewChart: "Xem biểu đồ",
      time: "Thời gian",
      models: "Từng model",
      model: "Model",
      status: "Trạng thái",
      latency: "Độ trễ",
      avail7: "7 ngày",
      avail30: "30 ngày",
      recent: "200 phút gần nhất",
      beatsEvery: "Mỗi cột là 5 phút",
      current: "Hiện tại · 15 phút",
      note: "Số liệu tính từ request thật của người dùng và được giữ 31 ngày. Độ trễ là thời gian tới chữ đầu tiên của câu trả lời. Phần trực tiếp tính 60 giây gần nhất, làm mới mỗi 5 giây. Lỗi do request sai (sai key, sai tham số) không tính."
    },
    en: {
      loading: "Loading...",
      error: "Could not load status data. Try again in a few minutes.",
      allOk: "All services are operating normally",
      someDeg: "Some services are slow or unstable",
      someDown: "Some services are disrupted",
      ok: "Operational",
      degraded: "Degraded",
      down: "Down",
      nodata: "No data",
      idle: "Waiting for new requests",
      live: "Live",
      secAgo: "s ago",
      justNow: "just updated",
      ago: "ago",
      now: "now",
      min: "min",
      liveLatency: "Live latency",
      liveSuccess: "Success · 60s",
      avgResponse: "Avg latency · 24h",
      uptime24: "Uptime · 24h",
      uptime7: "Uptime · 7 days",
      uptime30: "Uptime · 30 days",
      chartTitle: "Response latency",
      rLive: "Live",
      r6: "6 hours",
      r24: "24 hours",
      viewTable: "View as table",
      viewChart: "View chart",
      time: "Time",
      models: "Models",
      model: "Model",
      status: "Status",
      latency: "Latency",
      avail7: "7 days",
      avail30: "30 days",
      recent: "Last 200 minutes",
      beatsEvery: "Each bar is 5 minutes",
      current: "Now · 15 min",
      note: "Measured from real user requests and kept for 31 days. Latency is the time to the first token of the reply. Live figures cover the last 60 seconds and refresh every 5 seconds. Errors caused by bad requests (wrong key, invalid parameters) are not counted."
    },
    zh: {
      loading: "加载中...",
      error: "暂时无法获取数据，请稍后刷新。",
      allOk: "所有服务运行正常",
      someDeg: "部分服务较慢或不稳定",
      someDown: "部分服务中断",
      ok: "正常",
      degraded: "降级",
      down: "中断",
      nodata: "无数据",
      idle: "等待新请求",
      live: "实时",
      secAgo: "秒前",
      justNow: "刚刚更新",
      ago: "前",
      now: "现在",
      min: "分钟",
      liveLatency: "实时延迟",
      liveSuccess: "成功率 · 60 秒",
      avgResponse: "平均延迟 · 24 小时",
      uptime24: "可用率 · 24 小时",
      uptime7: "可用率 · 7 天",
      uptime30: "可用率 · 30 天",
      chartTitle: "响应延迟",
      rLive: "实时",
      r6: "6 小时",
      r24: "24 小时",
      viewTable: "表格视图",
      viewChart: "图表视图",
      time: "时间",
      models: "各模型",
      model: "模型",
      status: "状态",
      latency: "延迟",
      avail7: "7 天",
      avail30: "30 天",
      recent: "最近 200 分钟",
      beatsEvery: "每格 5 分钟",
      current: "当前 · 15 分钟",
      note: "数据来自真实用户请求，保留 31 天。延迟为首个 token 返回时间。实时数据统计最近 60 秒，每 5 秒刷新。错误请求（错误密钥、参数）不计入。"
    }
  };
  const t = T[lang] || T[(lang || "").split("-")[0]] || T.en;
  const META = {
    gpt: {
      name: "GPT",
      icon: "openai",
      mono: true
    },
    astra: {
      name: "GPT Astra",
      icon: "openai",
      mono: true
    },
    claude: {
      name: "Claude",
      icon: "claude",
      mono: false
    },
    grok: {
      name: "Grok",
      icon: "grok",
      mono: true
    },
    deepseek: {
      name: "DeepSeek",
      icon: "deepseek",
      mono: false
    }
  };
  const STATUS = {
    ok: "var(--tsl-good)",
    degraded: "var(--tsl-warn)",
    down: "var(--tsl-crit)",
    nodata: "var(--tsl-none)"
  };
  const GLYPH = {
    ok: "✓",
    degraded: "!",
    down: "×",
    nodata: "–"
  };
  const [data, setData] = useState(null);
  const [live, setLive] = useState(null);
  const [err, setErr] = useState(false);
  const [sel, setSel] = useState("");
  const [range, setRange] = useState("live");
  const [asTable, setAsTable] = useState(false);
  const [hover, setHover] = useState(null);
  const [beatTip, setBeatTip] = useState(null);
  const [clock, setClock] = useState(Date.now());
  useEffect(() => {
    let alive = true;
    const loadSummary = () => fetch(`${BASE}/summary`, {
      cache: "no-store"
    }).then(r => r.json()).then(j => {
      if (alive) {
        setData(j);
        setErr(false);
      }
    }).catch(() => alive && setErr(true));
    const loadLive = () => fetch(`${BASE}/live`, {
      cache: "no-store"
    }).then(r => r.json()).then(j => {
      if (alive) setLive(j);
    }).catch(() => {});
    loadSummary();
    loadLive();
    const a = setInterval(loadSummary, 30000);
    const b = setInterval(loadLive, 5000);
    const c = setInterval(() => setClock(Date.now()), 1000);
    return () => {
      alive = false;
      clearInterval(a);
      clearInterval(b);
      clearInterval(c);
    };
  }, []);
  const locale = lang === "vi" ? "vi-VN" : lang === "zh" ? "zh-CN" : "en-US";
  const nf1 = new Intl.NumberFormat(locale, {
    minimumFractionDigits: 1,
    maximumFractionDigits: 1
  });
  const nf0 = new Intl.NumberFormat(locale, {
    maximumFractionDigits: 0
  });
  const hhmm = s => new Date(s * 1000).toLocaleTimeString(locale, {
    hour: "2-digit",
    minute: "2-digit"
  });
  const hhmmss = s => new Date(s * 1000).toLocaleTimeString(locale, {
    hour: "2-digit",
    minute: "2-digit",
    second: "2-digit"
  });
  const pct = v => v == null ? "–" : `${new Intl.NumberFormat(locale, {
    minimumFractionDigits: 2,
    maximumFractionDigits: 2
  }).format(v)}%`;
  const dur = ms => ms == null ? ["–", ""] : ms < 1000 ? [nf0.format(ms), "ms"] : [nf1.format(ms / 1000), "s"];
  const durText = ms => {
    const d = dur(ms);
    return d[1] ? `${d[0]} ${d[1]}` : d[0];
  };
  const label = s => t[s] || s;
  const css = `
  .tslst { --tsl-surface: #ffffff; --tsl-plane: #f6f7f9; --tsl-fg: #0b0b0b; --tsl-fg2: #52514e; --tsl-muted: #898781;
    --tsl-grid: #e8e7e1; --tsl-axis: #c3c2b7; --tsl-ring: rgba(11,11,11,.10); --tsl-none: #e4e3dd;
    --tsl-series: #2a78d6; --tsl-good: #0ca30c; --tsl-warn: #fab219; --tsl-crit: #d03b3b;
    color: var(--tsl-fg); display: grid; gap: 14px; margin-top: 8px; font-family: system-ui, -apple-system, "Segoe UI", sans-serif; }
  .dark .tslst { --tsl-surface: #161618; --tsl-plane: #1d1d20; --tsl-fg: #ffffff; --tsl-fg2: #c3c2b7; --tsl-muted: #898781;
    --tsl-grid: #2c2c2a; --tsl-axis: #383835; --tsl-ring: rgba(255,255,255,.10); --tsl-none: #34343a; --tsl-series: #3987e5; }
  .tslst img { margin: 0 !important; display: inline-block !important; vertical-align: middle; border-radius: 0 !important; box-shadow: none !important; }
  .tslst button { height: auto; min-height: 0; line-height: 1.2; font-family: inherit; }
  .tslst button:focus-visible { outline: 2px solid var(--tsl-series); outline-offset: 2px; }
  .dark .tslst-mono { filter: invert(1); }
  .tslst-card { background: var(--tsl-surface); border: 1px solid var(--tsl-ring); border-radius: 14px; padding: 18px 20px; }
  .tslst-cap { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-bottom: 12px; }
  .tslst-cap b { font-size: 15px; font-weight: 600; }
  .tslst-cap span { color: var(--tsl-muted); font-size: 12.5px; }
  .tslst-head { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
  .tslst-head .grow { flex: 1 1 260px; min-width: 0; }
  .tslst-head h3 { margin: 0 !important; font-size: 16px; font-weight: 600; }
  .tslst-sub { color: var(--tsl-muted); font-size: 12.5px; }
  .tslst-glyph { width: 20px; height: 20px; border-radius: 50%; display: inline-grid; place-items: center; flex: none;
    color: #fff; font-size: 12px; font-weight: 800; line-height: 1; }
  .tslst-glyph[data-s="degraded"] { color: #3a2a00; }
  .tslst-glyph[data-s="nodata"] { color: var(--tsl-muted); }
  .tslst-live { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 600; color: var(--tsl-fg2);
    border: 1px solid var(--tsl-ring); border-radius: 999px; padding: 3px 10px; }
  .tslst-live i { width: 7px; height: 7px; border-radius: 50%; background: var(--tsl-crit); animation: tslst-blink 1.6s ease-in-out infinite; }
  @keyframes tslst-blink { 0%, 100% { opacity: 1; } 50% { opacity: .3; } }
  .tslst-chip { display: inline-flex; align-items: center; gap: 7px; font-size: 13px; font-weight: 600; white-space: nowrap; color: var(--tsl-fg);
    padding: 3px 10px 3px 4px; border-radius: 999px; background: color-mix(in srgb, var(--st) 12%, transparent); }
  .tslst-chip .tslst-glyph { width: 18px; height: 18px; font-size: 11px; }
  .tslst-list { padding: 4px 20px; }
  .tslst-row { display: grid; grid-template-columns: minmax(120px, 150px) minmax(0, 1fr) 96px 138px; align-items: center; gap: 16px; padding: 12px 0; border-top: 1px solid var(--tsl-grid); cursor: pointer; }
  .tslst-row:first-child { border-top: 0; }
  .tslst-name { display: flex; align-items: center; gap: 10px; font-weight: 600; font-size: 15px; }
  .tslst-row[data-active="1"] .tslst-name span { box-shadow: inset 0 -2px 0 var(--tsl-series); }
  .tslst-beats { position: relative; display: flex; gap: 2px; height: 24px; align-items: stretch; min-width: 0; }
  .tslst-beats i { flex: 1 1 0; min-width: 2px; border-radius: 3px; transition: opacity .15s; }
  .tslst-beats i:hover { opacity: .7; }
  .tslst-beats.big { height: 36px; }
  .tslst-btip { position: absolute; bottom: calc(100% + 6px); transform: translateX(-50%); pointer-events: none; z-index: 2;
    background: var(--tsl-fg); color: var(--tsl-surface); font-size: 12px; padding: 4px 8px; border-radius: 6px; white-space: nowrap; }
  .tslst-livecell { text-align: right; font-size: 12px; line-height: 1.35; color: var(--tsl-muted); }
  .tslst-livecell b { display: block; font-size: 14px; font-weight: 600; color: var(--tsl-fg); font-variant-numeric: tabular-nums; }
  .tslst-legend { display: flex; flex-wrap: wrap; gap: 6px 16px; padding: 10px 0 8px; border-top: 1px solid var(--tsl-grid); font-size: 12.5px; color: var(--tsl-fg2); }
  .tslst-legend span { display: inline-flex; align-items: center; gap: 6px; }
  .tslst-legend i { width: 10px; height: 10px; border-radius: 3px; }
  .tslst-filters { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 10px; }
  .tslst-tabs { display: flex; flex-wrap: wrap; gap: 6px; }
  .tslst-tabs button { border: 1px solid var(--tsl-ring); background: var(--tsl-surface); color: var(--tsl-fg); border-radius: 999px; padding: 6px 13px; height: 34px;
    font-weight: 600; font-size: 13.5px; cursor: pointer; display: inline-flex; align-items: center; gap: 7px; }
  .tslst-tabs button:hover { background: var(--tsl-plane); }
  .tslst-tabs button[data-on="1"] { background: var(--tsl-fg); color: var(--tsl-surface); border-color: var(--tsl-fg); }
  .tslst-tabs button[data-on="1"] .tslst-mono { filter: invert(1); }
  .dark .tslst-tabs button[data-on="1"] .tslst-mono { filter: none; }
  .tslst-seg { display: inline-flex; padding: 3px; gap: 2px; background: var(--tsl-plane); border: 1px solid var(--tsl-ring); border-radius: 10px; }
  .tslst-seg button { border: 0; background: transparent; color: var(--tsl-fg2); border-radius: 7px; padding: 5px 11px; font-size: 13px; font-weight: 600; cursor: pointer; display: inline-flex; align-items: center; gap: 6px; }
  .tslst-seg button[data-on="1"] { background: var(--tsl-surface); color: var(--tsl-fg); box-shadow: 0 1px 2px rgba(0,0,0,.12); }
  .tslst-seg button i { width: 6px; height: 6px; border-radius: 50%; background: var(--tsl-crit); }
  .tslst-detail { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 24px; align-items: end; }
  .tslst-axis { display: flex; justify-content: space-between; gap: 8px; font-size: 12px; color: var(--tsl-muted); margin-top: 6px; }
  .tslst-now { display: grid; gap: 6px; justify-items: end; }
  .tslst-now span { font-size: 12px; color: var(--tsl-muted); }
  .tslst-now .tslst-chip { font-size: 15px; padding: 5px 14px 5px 5px; }
  .tslst-now .tslst-chip .tslst-glyph { width: 24px; height: 24px; font-size: 13px; }
  .tslst-stats { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; }
  .tslst-stat { background: var(--tsl-surface); border: 1px solid var(--tsl-ring); border-radius: 12px; padding: 12px 14px; }
  .tslst-stat span { display: flex; align-items: center; gap: 6px; color: var(--tsl-fg2); font-size: 12.5px; }
  .tslst-stat span i { width: 6px; height: 6px; border-radius: 50%; background: var(--tsl-crit); }
  .tslst-stat b { display: block; font-size: 24px; font-weight: 600; margin-top: 4px; letter-spacing: -.01em; }
  .tslst-stat b small { font-size: 13px; color: var(--tsl-muted); font-weight: 500; margin-left: 4px; }
  .tslst-link { border: 0; background: transparent; color: var(--tsl-series); font-size: 12.5px; font-weight: 600; cursor: pointer; padding: 0; }
  .tslst-tip { position: absolute; pointer-events: none; transform: translate(-50%, -100%); margin-top: -10px; background: var(--tsl-surface);
    border: 1px solid var(--tsl-ring); box-shadow: 0 6px 18px rgba(0,0,0,.12); border-radius: 8px; padding: 6px 10px; white-space: nowrap; }
  .tslst-tip b { display: flex; align-items: center; gap: 6px; font-size: 14px; }
  .tslst-tip b::before { content: ""; width: 12px; height: 2px; background: var(--tsl-series); border-radius: 1px; }
  .tslst-tip span { display: block; font-size: 12px; color: var(--tsl-muted); }
  .tslst-scroll { max-height: 260px; overflow: auto; }
  .tslst-table { display: table !important; width: 100% !important; border-collapse: collapse; font-size: 13.5px; margin: 0 !important; }
  .tslst-table th { text-align: left; color: var(--tsl-muted); font-weight: 500; padding: 8px 10px; border-bottom: 1px solid var(--tsl-grid); font-size: 12px; position: sticky; top: 0; background: var(--tsl-surface); }
  .tslst-table td { padding: 9px 10px; border-bottom: 1px solid var(--tsl-grid); font-variant-numeric: tabular-nums; }
  .tslst-table tr:last-child td { border-bottom: 0; }
  .tslst-table .r { text-align: right; }
  @media (prefers-reduced-motion: reduce) { .tslst-live i { animation: none; } .tslst-beats i { transition: none; } }
  @media (max-width: 720px) {
    .tslst-row { grid-template-columns: 1fr auto; }
    .tslst-row .tslst-beats { grid-column: 1 / -1; order: 3; }
    .tslst-row .tslst-livecell { display: none; }
    .tslst-detail { grid-template-columns: 1fr; }
    .tslst-now { justify-items: start; }
    .tslst-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  }
  `;
  if (!data) {
    return <div className="tslst"><style>{css}</style><div className="tslst-card">{err ? t.error : t.loading}</div></div>;
  }
  const products = data.products || [];
  if (!products.length) {
    return <div className="tslst"><style>{css}</style><div className="tslst-card">{t.nodata}</div></div>;
  }
  const cur = products.find(p => p.id === sel) || products[0];
  const points = live && live.points || [];
  const lastLive = points.length ? points[points.length - 1] : null;
  const liveOf = id => lastLive && lastLive.p && lastLive.p[id] || null;
  const secAgo = live ? Math.max(0, Math.round(clock / 1000 - (lastLive ? lastLive.ts : live.ts))) : null;
  const overall = data.overall || "ok";
  const icon = (id, size) => {
    const m = META[id] || ({});
    return m.icon ? <img src={`/images/status/${m.icon}.svg`} alt="" width={size} height={size} className={m.mono ? "tslst-mono" : ""} /> : null;
  };
  const glyph = s => <span className="tslst-glyph" data-s={s} style={{
    background: STATUS[s] || STATUS.nodata
  }} aria-hidden="true">{GLYPH[s] || "–"}</span>;
  const chip = s => <span className="tslst-chip" style={{
    "--st": STATUS[s] || STATUS.nodata
  }}>{glyph(s)}{label(s)}</span>;
  const beats = (key, items, big) => <div className={`tslst-beats${big ? " big" : ""}`} onMouseLeave={() => setBeatTip(null)}>
      {items.map((b, i) => <i key={i} style={{
    background: STATUS[b.state] || STATUS.nodata
  }} aria-label={`${hhmm(b.ts)} · ${label(b.state)}`} onMouseEnter={() => setBeatTip({
    key,
    i,
    left: (i + 0.5) / items.length * 100,
    text: `${hhmm(b.ts)} · ${label(b.state)}`
  })} />)}
      {beatTip && beatTip.key === key && <div className="tslst-btip" style={{
    left: `${beatTip.left}%`
  }}>{beatTip.text}</div>}
    </div>;
  const series = range === "live" ? points.map(pt => [pt.ts, pt.p && pt.p[cur.id] ? pt.p[cur.id].latency_ms : null]) : (cur.series_24h || []).slice(-(Number(range) * 12)).map(s => [s[0], s[1]]);
  const vals = series.map(s => s[1]).filter(v => v != null);
  const niceStep = raw => {
    const p = Math.pow(10, Math.floor(Math.log10(raw || 1)));
    const f = raw / p;
    return (f <= 1 ? 1 : f <= 2 ? 2 : f <= 2.5 ? 2.5 : f <= 5 ? 5 : 10) * p;
  };
  const vMin = vals.length ? Math.min(...vals) : 0, vMax = vals.length ? Math.max(...vals) : 1000;
  const step = niceStep(Math.max(vMax - vMin, 200) / 4);
  const lo = Math.max(0, Math.floor(vMin / step) * step - (vMin - Math.floor(vMin / step) * step < step * 0.2 ? step : 0));
  const hi = Math.ceil(vMax / step) * step + (Math.ceil(vMax / step) * step - vMax < step * 0.2 ? step : 0);
  const yTicks = [];
  for (let v = lo; v <= hi + 1e-6; v += step) yTicks.push(v);
  const inSeconds = hi >= 1000;
  const tickDigits = inSeconds ? step < 100 ? 2 : step < 1000 ? 1 : 0 : 0;
  const tickText = v => new Intl.NumberFormat(locale, {
    minimumFractionDigits: tickDigits,
    maximumFractionDigits: tickDigits
  }).format(inSeconds ? v / 1000 : v);
  const W = 800, H = 240, PL = 46, PR = 14, PT = 10, PB = 26;
  const x = i => PL + (series.length <= 1 ? 0 : i / (series.length - 1) * (W - PL - PR));
  const y = v => PT + (1 - (v - lo) / (hi - lo || 1)) * (H - PT - PB);
  let line = "", area = "", seg = [], gap = 0;
  const flush = () => {
    if (seg.length) {
      line += seg.map((p, k) => `${k ? "L" : "M"}${p[0].toFixed(1)},${p[1].toFixed(1)}`).join(" ") + " ";
      area += `M${seg[0][0].toFixed(1)},${H - PB} ` + seg.map(p => `L${p[0].toFixed(1)},${p[1].toFixed(1)}`).join(" ") + ` L${seg[seg.length - 1][0].toFixed(1)},${H - PB} Z `;
    }
    seg = [];
  };
  series.forEach((s, i) => {
    if (s[1] == null) {
      gap += 1;
      if (gap > 6) flush();
      return;
    }
    gap = 0;
    seg.push([x(i), y(s[1])]);
  });
  flush();
  let lastIdx = -1;
  for (let i = series.length - 1; i >= 0; i--) {
    if (series[i][1] != null) {
      lastIdx = i;
      break;
    }
  }
  const xTicks = series.length ? [0, 1, 2, 3, 4].map(k => Math.round((series.length - 1) * k / 4)) : [];
  const tickLabel = ts => range === "live" ? hhmmss(ts) : hhmm(ts);
  const onMove = e => {
    const box = e.currentTarget.getBoundingClientRect();
    const px = (e.clientX - box.left) / box.width * W;
    let i = Math.max(0, Math.min(series.length - 1, Math.round((px - PL) / (W - PL - PR) * (series.length - 1))));
    for (let d = 0; d < series.length; d++) {
      if (series[i - d] && series[i - d][1] != null) {
        i -= d;
        break;
      }
      if (series[i + d] && series[i + d][1] != null) {
        i += d;
        break;
      }
    }
    setHover(series[i] && series[i][1] != null ? {
      i,
      left: x(i) / W * 100,
      top: y(series[i][1]) / H * 100
    } : null);
  };
  const recentBeats = (cur.history || []).slice(-40);
  const curLive = liveOf(cur.id);
  const liveLat = dur(curLive ? curLive.latency_ms : null);
  const avgLat = dur(cur.latency_24h_ms);
  const setProduct = id => {
    setSel(id);
    setHover(null);
    setBeatTip(null);
  };
  return <div className="tslst">
      <style>{css}</style>

      <div className="tslst-card tslst-head">
        {glyph(overall)}
        <div className="grow">
          <h3>{overall === "ok" ? t.allOk : overall === "degraded" ? t.someDeg : t.someDown}</h3>
          <div className="tslst-sub">{secAgo == null ? "…" : secAgo < 2 ? t.justNow : `${secAgo} ${t.secAgo}`}</div>
        </div>
        <span className="tslst-live"><i />{t.live}</span>
      </div>

      <div className="tslst-card tslst-list">
        {products.map(p => {
    const lv = liveOf(p.id);
    return <div key={p.id} className="tslst-row" data-active={p.id === cur.id ? "1" : "0"} onClick={() => setProduct(p.id)}>
              <div className="tslst-name">{icon(p.id, 20)}<span>{(META[p.id] || ({})).name || p.id}</span></div>
              {beats(`row-${p.id}`, (p.history || []).slice(-40))}
              <div className="tslst-livecell"><b>{durText(lv ? lv.latency_ms : null)}</b>{pct(p.uptime_24h)} · 24h</div>
              <div style={{
      textAlign: "right"
    }}>{chip(p.status)}</div>
            </div>;
  })}
        <div className="tslst-legend">
          {["ok", "degraded", "down", "nodata"].map(s => <span key={s}><i style={{
    background: STATUS[s]
  }} />{label(s)}</span>)}
          <span style={{
    marginLeft: "auto",
    color: "var(--tsl-muted)"
  }}>{t.beatsEvery}</span>
        </div>
      </div>

      <div className="tslst-filters">
        <div className="tslst-tabs">
          {products.map(p => <button key={p.id} type="button" data-on={p.id === cur.id ? "1" : "0"} onClick={() => setProduct(p.id)}>{icon(p.id, 16)}{(META[p.id] || ({})).name || p.id}</button>)}
        </div>
        <div className="tslst-seg" role="group" aria-label={t.chartTitle}>
          <button type="button" data-on={range === "live" ? "1" : "0"} onClick={() => {
    setRange("live");
    setHover(null);
  }}><i />{t.rLive}</button>
          <button type="button" data-on={range === "6" ? "1" : "0"} onClick={() => {
    setRange("6");
    setHover(null);
  }}>{t.r6}</button>
          <button type="button" data-on={range === "24" ? "1" : "0"} onClick={() => {
    setRange("24");
    setHover(null);
  }}>{t.r24}</button>
        </div>
      </div>

      <div className="tslst-card tslst-detail">
        <div style={{
    minWidth: 0
  }}>
          <div className="tslst-cap"><b>{t.recent}</b><span>{t.beatsEvery}</span></div>
          {beats(`detail-${cur.id}`, recentBeats, true)}
          <div className="tslst-axis"><span>{recentBeats.length * 5} {t.min} {t.ago}</span><span>{t.now}</span></div>
        </div>
        <div className="tslst-now"><span>{t.current}</span>{chip(cur.status)}</div>
      </div>

      <div className="tslst-stats">
        <div className="tslst-stat"><span><i />{t.liveLatency}</span><b>{liveLat[0]}<small>{liveLat[1]}</small></b></div>
        <div className="tslst-stat"><span>{t.liveSuccess}</span><b>{curLive && curLive.success != null ? `${nf1.format(curLive.success)}%` : "–"}</b></div>
        <div className="tslst-stat"><span>{t.avgResponse}</span><b>{avgLat[0]}<small>{avgLat[1]}</small></b></div>
        <div className="tslst-stat"><span>{t.uptime24}</span><b>{pct(cur.uptime_24h)}</b></div>
        <div className="tslst-stat"><span>{t.uptime7}</span><b>{pct(cur.availability && cur.availability["7"])}</b></div>
        <div className="tslst-stat"><span>{t.uptime30}</span><b>{pct(cur.availability && cur.availability["30"])}</b></div>
      </div>

      <div className="tslst-card">
        <div className="tslst-cap">
          <b>{t.chartTitle} ({inSeconds ? "s" : "ms"}) · {(META[cur.id] || ({})).name || cur.id}</b>
          <button type="button" className="tslst-link" onClick={() => setAsTable(!asTable)}>{asTable ? t.viewChart : t.viewTable}</button>
        </div>
        {asTable ? <div className="tslst-scroll">
            <table className="tslst-table">
              <thead><tr><th>{t.time}</th><th className="r">{t.latency}</th></tr></thead>
              <tbody>
                {series.filter(s => s[1] != null).slice().reverse().map(s => <tr key={s[0]}><td>{tickLabel(s[0])}</td><td className="r">{durText(s[1])}</td></tr>)}
              </tbody>
            </table>
            {!vals.length && <p style={{
    color: "var(--tsl-muted)",
    margin: "8px 0 0"
  }}>{range === "live" ? t.idle : t.nodata}</p>}
          </div> : <div style={{
    position: "relative"
  }}>
            <svg viewBox={`0 0 ${W} ${H}`} style={{
    width: "100%",
    height: "auto",
    display: "block"
  }} onMouseMove={onMove} onMouseLeave={() => setHover(null)} role="img" aria-label={t.chartTitle}>
              {yTicks.map((v, k) => <g key={`y${k}`}>
                  <line x1={PL} x2={W - PR} y1={y(v)} y2={y(v)} stroke={k === 0 ? "var(--tsl-axis)" : "var(--tsl-grid)"} strokeWidth="1" />
                  <text x={PL - 8} y={y(v) + 4} textAnchor="end" fontSize="11" fill="var(--tsl-muted)" style={{
    fontVariantNumeric: "tabular-nums"
  }}>{tickText(v)}</text>
                </g>)}
              {xTicks.map((i, k) => <text key={`x${k}`} x={x(i)} y={H - 6} textAnchor={k === 0 ? "start" : k === xTicks.length - 1 ? "end" : "middle"} fontSize="11" fill="var(--tsl-muted)" style={{
    fontVariantNumeric: "tabular-nums"
  }}>{tickLabel(series[i][0])}</text>)}
              <path d={area} fill="var(--tsl-series)" fillOpacity="0.1" />
              <path d={line} fill="none" stroke="var(--tsl-series)" strokeWidth="2" strokeLinejoin="round" strokeLinecap="round" />
              {hover && <line x1={x(hover.i)} x2={x(hover.i)} y1={PT} y2={H - PB} stroke="var(--tsl-axis)" strokeWidth="1" />}
              {lastIdx >= 0 && !hover && <circle cx={x(lastIdx)} cy={y(series[lastIdx][1])} r="4" fill="var(--tsl-series)" stroke="var(--tsl-surface)" strokeWidth="2" />}
              {hover && <circle cx={x(hover.i)} cy={y(series[hover.i][1])} r="4" fill="var(--tsl-series)" stroke="var(--tsl-surface)" strokeWidth="2" />}
            </svg>
            {hover && <div className="tslst-tip" style={{
    left: `${Math.min(88, Math.max(12, hover.left))}%`,
    top: `${hover.top}%`
  }}>
                <b>{durText(series[hover.i][1])}</b>
                <span>{tickLabel(series[hover.i][0])}</span>
              </div>}
            {!vals.length && <div style={{
    position: "absolute",
    inset: 0,
    display: "grid",
    placeItems: "center",
    color: "var(--tsl-muted)"
  }}>{range === "live" ? t.idle : t.nodata}</div>}
          </div>}
      </div>

      {cur.models && cur.models.length > 0 && <div className="tslst-card" style={{
    overflowX: "auto"
  }}>
          <div className="tslst-cap"><b>{t.models}</b></div>
          <table className="tslst-table">
            <thead><tr><th>{t.model}</th><th>{t.status}</th><th className="r">{t.latency}</th><th className="r">{t.avail7}</th><th className="r">{t.avail30}</th></tr></thead>
            <tbody>
              {cur.models.map(m => <tr key={m.name}>
                  <td style={{
    fontFamily: "ui-monospace, Menlo, monospace",
    fontSize: 13
  }}>{m.name}</td>
                  <td>{chip(m.status)}</td>
                  <td className="r">{durText(m.latency_ms)}</td>
                  <td className="r">{pct(m.availability && m.availability["7"])}</td>
                  <td className="r">{pct(m.availability && m.availability["30"])}</td>
                </tr>)}
            </tbody>
          </table>
        </div>}

      <p style={{
    color: "var(--tsl-muted)",
    fontSize: 12.5,
    margin: 0
  }}>{t.note}</p>
    </div>;
};

<StatusBoard lang="cs" />


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.