wow
This commit is contained in:
parent
e5c54597c5
commit
ddb20f2fc8
1 changed files with 22 additions and 21 deletions
43
src/index.ts
43
src/index.ts
|
|
@ -103,7 +103,7 @@ const initialSnapshot: StatusSnapshot = {
|
||||||
ok: false,
|
ok: false,
|
||||||
fetchedAt: null,
|
fetchedAt: null,
|
||||||
pageUpdatedAt: null,
|
pageUpdatedAt: null,
|
||||||
status: { indicator: "unknown", description: "Cloudflare 상태를 확인하는 중입니다" },
|
status: { indicator: "unknown", description: "Checking Cloudflare status" },
|
||||||
streak: { days: null, since: null, activeIncident: false },
|
streak: { days: null, since: null, activeIncident: false },
|
||||||
lastIncident: null,
|
lastIncident: null,
|
||||||
incidents: [],
|
incidents: [],
|
||||||
|
|
@ -228,7 +228,7 @@ async function refreshStatus(source: "startup" | "cron") {
|
||||||
pageUpdatedAt: summary.page?.updated_at ?? null,
|
pageUpdatedAt: summary.page?.updated_at ?? null,
|
||||||
status: {
|
status: {
|
||||||
indicator: indicator(summary.status?.indicator),
|
indicator: indicator(summary.status?.indicator),
|
||||||
description: summary.status?.description ?? "상태 정보가 없습니다",
|
description: summary.status?.description ?? "Status information is unavailable",
|
||||||
},
|
},
|
||||||
streak: {
|
streak: {
|
||||||
days: daysSince(latestIncident?.started_at ?? latestIncident?.created_at ?? null),
|
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>
|
const INDEX_HTML = String.raw`<!doctype html>
|
||||||
<html lang="ko">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="theme-color" content="#07111f" />
|
<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>
|
<title>Cloudflare · Days Without Incident</title>
|
||||||
<style>
|
<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; }
|
: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>
|
<body>
|
||||||
<main class="shell">
|
<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>
|
<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">
|
<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 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">서비스 구성요소</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 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">최근 인시던트</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 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">예정된 유지보수</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 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>
|
</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>
|
</main>
|
||||||
<script>
|
<script>
|
||||||
const labels = { operational: "정상", degraded_performance: "성능 저하", partial_outage: "부분 장애", major_outage: "주요 장애", unknown: "확인 중" };
|
const labels = { operational: "Operational", degraded_performance: "Degraded performance", partial_outage: "Partial outage", major_outage: "Major outage", unknown: "Checking" };
|
||||||
const statusLabels = { none: "모든 시스템 정상", minor: "일부 성능 저하", major: "부분 시스템 장애", critical: "주요 시스템 장애", unknown: "상태 확인 중" };
|
const statusLabels = { none: "All systems operational", minor: "Minor performance issues", major: "Partial system outage", critical: "Major system outage", unknown: "Checking status" };
|
||||||
const impactLabels = { none: "영향 없음", minor: "경미한 영향", major: "주요 영향", critical: "치명적 영향" };
|
const impactLabels = { none: "No impact", minor: "Minor impact", major: "Major impact", critical: "Critical impact" };
|
||||||
const incidentStatusLabels = { investigating: "조사 중", identified: "원인 확인", monitoring: "모니터링", resolved: "해결됨", postmortem: "사후 분석" };
|
const incidentStatusLabels = { investigating: "Investigating", identified: "Identified", monitoring: "Monitoring", resolved: "Resolved", postmortem: "Postmortem" };
|
||||||
const dateFormatter = new Intl.DateTimeFormat("ko-KR", { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
|
const maintenanceStatusLabels = { scheduled: "Scheduled", in_progress: "In progress", verifying: "Verifying", completed: "Completed" };
|
||||||
const fullDateFormatter = new Intl.DateTimeFormat("ko-KR", { year: "numeric", month: "long", day: "numeric", hour: "2-digit", minute: "2-digit" });
|
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 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 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) => ({ "&": "&", "<": "<", ">": ">", "\"": """, "'": "'" }[character]));
|
const escapeHtml = (value) => String(value || "").replace(/[&<>\"']/g, (character) => ({ "&": "&", "<": "<", ">": ">", "\"": """, "'": "'" }[character]));
|
||||||
const setText = (id, value) => { byId(id).textContent = value; };
|
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 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">기록된 인시던트가 없습니다.</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 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">예정된 유지보수가 없습니다.</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 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("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); }
|
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", "상태 데이터를 불러오지 못했습니다. 잠시 후 다시 시도합니다."); } }
|
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);
|
load(); setInterval(load, 60 * 1000);
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue