This commit is contained in:
Akiyama Mizuki 2026-08-18 10:23:04 +09:00
commit ddb20f2fc8

View file

@ -103,7 +103,7 @@ const initialSnapshot: StatusSnapshot = {
ok: false,
fetchedAt: null,
pageUpdatedAt: null,
status: { indicator: "unknown", description: "Cloudflare 상태를 확인하는 중입니다" },
status: { indicator: "unknown", description: "Checking Cloudflare status" },
streak: { days: null, since: null, activeIncident: false },
lastIncident: null,
incidents: [],
@ -228,7 +228,7 @@ async function refreshStatus(source: "startup" | "cron") {
pageUpdatedAt: summary.page?.updated_at ?? null,
status: {
indicator: indicator(summary.status?.indicator),
description: summary.status?.description ?? "상태 정보가 없습니다",
description: summary.status?.description ?? "Status information is unavailable",
},
streak: {
days: daysSince(latestIncident?.started_at ?? latestIncident?.created_at ?? null),
@ -265,12 +265,12 @@ async function refreshStatus(source: "startup" | "cron") {
}
const INDEX_HTML = String.raw`<!doctype html>
<html lang="ko">
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#07111f" />
<meta name="description" content="Cloudflare 장애 없는 날을 확인하는 상태 대시보드" />
<meta name="description" content="A Cloudflare reliability dashboard tracking days without an incident" />
<title>Cloudflare · Days Without Incident</title>
<style>
:root { color-scheme: dark; --bg: #06101e; --card: rgba(13, 31, 52, .7); --line: rgba(141, 196, 255, .16); --text: #eef7ff; --muted: #8da8c0; --blue: #69b7ff; --cyan: #59e3f4; --green: #58e39a; --amber: #ffc76b; --red: #ff7d9b; }
@ -304,31 +304,32 @@ const INDEX_HTML = String.raw`<!doctype html>
<body>
<main class="shell">
<nav class="topbar"><a class="brand" href="/"><span class="brand-mark" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M12 3.5 19 6v5.4c0 4.3-2.9 7.9-7 9.1-4.1-1.2-7-4.8-7-9.1V6l7-2.5Z"/><path d="m8.7 12.2 2.1 2.1 4.6-4.6"/></svg></span><span class="brand-copy"><strong>CF STATUS D-DAY</strong><span>Cloudflare reliability</span></span></a><a class="source-link" href="https://www.cloudflarestatus.com" target="_blank" rel="noreferrer">cloudflarestatus.com </a></nav>
<section class="hero"><span class="eyebrow">a quiet day on the edge</span><h1>Cloudflare <br /><em id="hero-days"> </em> </h1><p class="hero-subtitle"> Cloudflare Status API를 <br /> .</p><div class="counter-wrap"><div id="days" class="counter" aria-live="polite"></div><div class="counter-label">days without incident</div></div><div id="status-pill" class="status-pill" data-indicator="unknown"> </div><p id="hero-note" class="hero-note">Cloudflare status .</p><div id="error-banner" class="error-banner" data-visible="false"></div></section>
<section class="hero"><span class="eyebrow">a quiet day on the edge</span><h1>Cloudflare has gone<br /><em id="hero-days"></em> days without an incident</h1><p class="hero-subtitle">We check the official Cloudflare Status API on a regular schedule<br />to track time since the last incident.</p><div class="counter-wrap"><div id="days" class="counter" aria-live="polite"></div><div class="counter-label">days without incident</div></div><div id="status-pill" class="status-pill" data-indicator="unknown">Checking status</div><p id="hero-note" class="hero-note">Loading Cloudflare status data.</p><div id="error-banner" class="error-banner" data-visible="false"></div></section>
<section class="grid" aria-label="Cloudflare status overview">
<article class="card health-card"><div class="card-pad"><div class="card-heading"><div><h2 class="card-title"> </h2><p class="card-kicker">Cloudflare </p></div><span class="live-dot">LIVE</span></div><div class="health-summary"><div class="health-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M4 12h3l2-6 3 12 2-6h6"/><path d="M4 19h16" opacity=".45"/></svg></div><div><strong id="status-description"> </strong><span id="last-updated"> : </span></div></div><div class="metric-row"><div class="metric"><strong id="total-components"></strong><span> </span></div><div class="metric"><strong id="operational-components"></strong><span> </span></div><div class="metric"><strong id="affected-components"></strong><span> </span></div></div></div></article>
<article class="card components-card"><div class="card-pad"><div class="card-heading"><div><h2 class="card-title"> </h2><p class="card-kicker"> </p></div><span id="component-count" class="component-count"></span></div><div id="component-list" class="component-list"><div class="empty"> </div></div></div></article>
<article class="card incidents-card"><div class="card-pad"><div class="card-heading"><div><h2 class="card-title"> </h2><p class="card-kicker">Cloudflare Status </p></div></div><div id="incident-list" class="incident-list"><div class="empty"> </div></div></div></article>
<article class="card maintenance-card"><div class="card-pad"><div class="card-heading"><div><h2 class="card-title"> </h2><p class="card-kicker"> </p></div></div><div id="maintenance-list" class="maintenance-list"><div class="empty"> .</div></div></div></article>
<article class="card health-card"><div class="card-pad"><div class="card-heading"><div><h2 class="card-title">Current overall status</h2><p class="card-kicker">Cloudflare's official page rollup</p></div><span class="live-dot">LIVE</span></div><div class="health-summary"><div class="health-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M4 12h3l2-6 3 12 2-6h6"/><path d="M4 19h16" opacity=".45"/></svg></div><div><strong id="status-description">Checking status</strong><span id="last-updated">Last synced: </span></div></div><div class="metric-row"><div class="metric"><strong id="total-components"></strong><span>Total services</span></div><div class="metric"><strong id="operational-components"></strong><span>Operational</span></div><div class="metric"><strong id="affected-components"></strong><span>Needs attention</span></div></div></div></article>
<article class="card components-card"><div class="card-pad"><div class="card-heading"><div><h2 class="card-title">Service components</h2><p class="card-kicker">Affected items are shown first</p></div><span id="component-count" class="component-count"></span></div><div id="component-list" class="component-list"><div class="empty">Loading</div></div></div></article>
<article class="card incidents-card"><div class="card-pad"><div class="card-heading"><div><h2 class="card-title">Recent incidents</h2><p class="card-kicker">Recent incidents recorded by Cloudflare Status</p></div></div><div id="incident-list" class="incident-list"><div class="empty">Loading</div></div></div></article>
<article class="card maintenance-card"><div class="card-pad"><div class="card-heading"><div><h2 class="card-title">Scheduled maintenance</h2><p class="card-kicker">Scheduled or in-progress work</p></div></div><div id="maintenance-list" class="maintenance-list"><div class="empty">No scheduled maintenance.</div></div></div></article>
</section>
<footer class="footer"><span id="footer-status">5 .</span><span>Data from <a href="https://www.cloudflarestatus.com/api" target="_blank" rel="noreferrer">Cloudflare Status API</a></span></footer>
<footer class="footer"><span id="footer-status">Updates automatically every 5 minutes.</span><span>Data from <a href="https://www.cloudflarestatus.com/api" target="_blank" rel="noreferrer">Cloudflare Status API</a></span></footer>
</main>
<script>
const labels = { operational: "정상", degraded_performance: "성능 저하", partial_outage: "부분 장애", major_outage: "주요 장애", unknown: "확인 중" };
const statusLabels = { none: "모든 시스템 정상", minor: "일부 성능 저하", major: "부분 시스템 장애", critical: "주요 시스템 장애", unknown: "상태 확인 중" };
const impactLabels = { none: "영향 없음", minor: "경미한 영향", major: "주요 영향", critical: "치명적 영향" };
const incidentStatusLabels = { investigating: "조사 중", identified: "원인 확인", monitoring: "모니터링", resolved: "해결됨", postmortem: "사후 분석" };
const dateFormatter = new Intl.DateTimeFormat("ko-KR", { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
const fullDateFormatter = new Intl.DateTimeFormat("ko-KR", { year: "numeric", month: "long", day: "numeric", hour: "2-digit", minute: "2-digit" });
const labels = { operational: "Operational", degraded_performance: "Degraded performance", partial_outage: "Partial outage", major_outage: "Major outage", unknown: "Checking" };
const statusLabels = { none: "All systems operational", minor: "Minor performance issues", major: "Partial system outage", critical: "Major system outage", unknown: "Checking status" };
const impactLabels = { none: "No impact", minor: "Minor impact", major: "Major impact", critical: "Critical impact" };
const incidentStatusLabels = { investigating: "Investigating", identified: "Identified", monitoring: "Monitoring", resolved: "Resolved", postmortem: "Postmortem" };
const maintenanceStatusLabels = { scheduled: "Scheduled", in_progress: "In progress", verifying: "Verifying", completed: "Completed" };
const dateFormatter = new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
const fullDateFormatter = new Intl.DateTimeFormat("en-US", { year: "numeric", month: "long", day: "numeric", hour: "2-digit", minute: "2-digit" });
const byId = (id) => document.getElementById(id);
const formatDate = (value, full) => { if (!value) return "—"; const date = new Date(value); return Number.isNaN(date.getTime()) ? "—" : (full ? fullDateFormatter : dateFormatter).format(date); };
const escapeHtml = (value) => String(value || "").replace(/[&<>\"']/g, (character) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", "\"": "&quot;", "'": "&#039;" }[character]));
const setText = (id, value) => { byId(id).textContent = value; };
function renderComponents(data) { const list = byId("component-list"); setText("component-count", data.componentCounts.total + ""); if (!data.components.length) { list.innerHTML = '<div class="empty">구성요소 정보가 없습니다.</div>'; return; } list.innerHTML = data.components.slice(0, 14).map((component) => '<div class="component" data-status="' + component.status + '"><span class="dot"></span><span class="component-name" title="' + escapeHtml(component.description || component.name) + '">' + escapeHtml(component.name) + '</span><span class="component-state">' + (labels[component.status] || labels.unknown) + '</span></div>').join(""); if (data.components.length > 14) list.insertAdjacentHTML("beforeend", '<div class="empty">' + (data.components.length - 14) + '개 구성요소</div>'); }
function renderIncidents(data) { const list = byId("incident-list"); if (!data.incidents.length) { list.innerHTML = '<div class="empty">기록된 인시던트가 없습니다.</div>'; return; } list.innerHTML = data.incidents.map((incident) => { const active = incident.status !== "resolved" && incident.status !== "postmortem"; const content = '<span class="incident-status"></span><span class="incident-copy"><strong>' + escapeHtml(incident.name) + '</strong><span>' + (incidentStatusLabels[incident.status] || incident.status) + ' · ' + formatDate(incident.startedAt) + '</span></span><span class="incident-impact">' + (impactLabels[incident.impact] || incident.impact) + '</span>'; return incident.shortlink ? '<a class="incident" data-active="' + active + '" href="' + escapeHtml(incident.shortlink) + '" target="_blank" rel="noreferrer">' + content + '</a>' : '<div class="incident" data-active="' + active + '">' + content + '</div>'; }).join(""); }
function renderMaintenances(data) { const list = byId("maintenance-list"); if (!data.maintenances.length) { list.innerHTML = '<div class="empty">예정된 유지보수가 없습니다.</div>'; return; } list.innerHTML = data.maintenances.map((maintenance) => { const content = '<span class="incident-status"></span><span class="maintenance-copy"><strong>' + escapeHtml(maintenance.name) + '</strong><span>' + escapeHtml(maintenance.status) + ' · ' + formatDate(maintenance.startsAt) + '</span></span><span class="incident-impact">' + (impactLabels[maintenance.impact] || maintenance.impact) + '</span>'; return maintenance.shortlink ? '<a class="maintenance" href="' + escapeHtml(maintenance.shortlink) + '" target="_blank" rel="noreferrer">' + content + '</a>' : '<div class="maintenance">' + content + '</div>'; }).join(""); }
function render(data) { const indicator = data.status.indicator; const active = data.streak.activeIncident; byId("status-pill").dataset.indicator = indicator; setText("status-pill", statusLabels[indicator] || statusLabels.unknown); setText("status-description", data.status.description); setText("hero-days", data.streak.days === null ? "아직" : data.streak.days.toLocaleString("ko-KR")); setText("days", data.streak.days === null ? "—" : data.streak.days.toLocaleString("ko-KR")); setText("hero-note", active ? "현재 진행 중인 인시던트가 있어 카운터가 멈춰 있습니다." : (data.lastIncident ? "마지막 인시던트: " + data.lastIncident.name + " · " + formatDate(data.lastIncident.startedAt, true) : "최근 인시던트 데이터를 기다리는 중입니다.")); setText("last-updated", "마지막 동기화: " + formatDate(data.fetchedAt, true)); setText("total-components", data.componentCounts.total.toLocaleString("ko-KR")); setText("operational-components", data.componentCounts.operational.toLocaleString("ko-KR")); setText("affected-components", data.componentCounts.affected.toLocaleString("ko-KR")); setText("footer-status", data.error ? "마지막 정상 데이터 이후 업데이트에 실패했습니다." : "5분마다 자동으로 업데이트됩니다."); byId("error-banner").dataset.visible = data.error ? "true" : "false"; setText("error-banner", data.error ? "업데이트 오류: " + data.error : ""); renderComponents(data); renderIncidents(data); renderMaintenances(data); }
async function load() { try { const response = await fetch("/api/status", { cache: "no-store" }); render(await response.json()); } catch (error) { setText("hero-note", "상태 데이터를 불러오지 못했습니다. 잠시 후 다시 시도합니다."); } }
function renderComponents(data) { const list = byId("component-list"); setText("component-count", data.componentCounts.total + " components"); if (!data.components.length) { list.innerHTML = '<div class="empty">No component data available.</div>'; return; } list.innerHTML = data.components.slice(0, 14).map((component) => '<div class="component" data-status="' + component.status + '"><span class="dot"></span><span class="component-name" title="' + escapeHtml(component.description || component.name) + '">' + escapeHtml(component.name) + '</span><span class="component-state">' + (labels[component.status] || labels.unknown) + '</span></div>').join(""); if (data.components.length > 14) list.insertAdjacentHTML("beforeend", '<div class="empty">' + (data.components.length - 14) + ' more components</div>'); }
function renderIncidents(data) { const list = byId("incident-list"); if (!data.incidents.length) { list.innerHTML = '<div class="empty">No recorded incidents.</div>'; return; } list.innerHTML = data.incidents.map((incident) => { const active = incident.status !== "resolved" && incident.status !== "postmortem"; const content = '<span class="incident-status"></span><span class="incident-copy"><strong>' + escapeHtml(incident.name) + '</strong><span>' + (incidentStatusLabels[incident.status] || incident.status) + ' · ' + formatDate(incident.startedAt) + '</span></span><span class="incident-impact">' + (impactLabels[incident.impact] || incident.impact) + '</span>'; return incident.shortlink ? '<a class="incident" data-active="' + active + '" href="' + escapeHtml(incident.shortlink) + '" target="_blank" rel="noreferrer">' + content + '</a>' : '<div class="incident" data-active="' + active + '">' + content + '</div>'; }).join(""); }
function renderMaintenances(data) { const list = byId("maintenance-list"); if (!data.maintenances.length) { list.innerHTML = '<div class="empty">No scheduled maintenance.</div>'; return; } list.innerHTML = data.maintenances.map((maintenance) => { const content = '<span class="incident-status"></span><span class="maintenance-copy"><strong>' + escapeHtml(maintenance.name) + '</strong><span>' + (maintenanceStatusLabels[maintenance.status] || maintenance.status) + ' · ' + formatDate(maintenance.startsAt) + '</span></span><span class="incident-impact">' + (impactLabels[maintenance.impact] || maintenance.impact) + '</span>'; return maintenance.shortlink ? '<a class="maintenance" href="' + escapeHtml(maintenance.shortlink) + '" target="_blank" rel="noreferrer">' + content + '</a>' : '<div class="maintenance">' + content + '</div>'; }).join(""); }
function render(data) { const indicator = data.status.indicator; const active = data.streak.activeIncident; byId("status-pill").dataset.indicator = indicator; setText("status-pill", statusLabels[indicator] || statusLabels.unknown); setText("status-description", data.status.description); setText("hero-days", data.streak.days === null ? "" : data.streak.days.toLocaleString("en-US")); setText("days", data.streak.days === null ? "—" : data.streak.days.toLocaleString("en-US")); setText("hero-note", active ? "The counter is paused while an incident is in progress." : (data.lastIncident ? "Last incident: " + data.lastIncident.name + " · " + formatDate(data.lastIncident.startedAt, true) : "Waiting for recent incident data.")); setText("last-updated", "Last synced: " + formatDate(data.fetchedAt, true)); setText("total-components", data.componentCounts.total.toLocaleString("en-US")); setText("operational-components", data.componentCounts.operational.toLocaleString("en-US")); setText("affected-components", data.componentCounts.affected.toLocaleString("en-US")); setText("footer-status", data.error ? "Update failed after the last successful refresh." : "Updates automatically every 5 minutes."); byId("error-banner").dataset.visible = data.error ? "true" : "false"; setText("error-banner", data.error ? "Update error: " + data.error : ""); renderComponents(data); renderIncidents(data); renderMaintenances(data); }
async function load() { try { const response = await fetch("/api/status", { cache: "no-store" }); render(await response.json()); } catch (error) { setText("hero-note", "Unable to load status data. Retrying shortly."); } }
load(); setInterval(load, 60 * 1000);
</script>
</body>