98 lines
3.6 KiB
TypeScript
98 lines
3.6 KiB
TypeScript
import { PublicShell, StatusPill, formatDate } from "../components/Shells";
|
|
import { getServiceStatusSnapshot } from "@/db/repository";
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
const statusCopy = {
|
|
operational: {
|
|
label: "服务正常",
|
|
tone: "good" as const,
|
|
summary: "目前没有正在处理的服务故障或维护公告。",
|
|
},
|
|
maintenance: {
|
|
label: "计划维护",
|
|
tone: "info" as const,
|
|
summary: "部分能力正在维护,请按公告中的建议操作。",
|
|
},
|
|
degraded: {
|
|
label: "服务降级",
|
|
tone: "warn" as const,
|
|
summary: "部分用户可能遇到延迟、重连或接入异常。",
|
|
},
|
|
outage: {
|
|
label: "服务中断",
|
|
tone: "danger" as const,
|
|
summary: "当前存在影响使用的服务中断,我们正在处理。",
|
|
},
|
|
};
|
|
|
|
export default async function ServiceStatusPage() {
|
|
const snapshot = await getServiceStatusSnapshot();
|
|
const copy = statusCopy[snapshot.status];
|
|
const resolved = snapshot.recentIncidents.filter(
|
|
(incident) => incident.status === "resolved",
|
|
);
|
|
|
|
return (
|
|
<PublicShell serviceStatus={snapshot}>
|
|
<div className="public-page status-page">
|
|
<header className={`status-hero status-hero-${snapshot.status}`}>
|
|
<StatusPill tone={copy.tone}>{copy.label}</StatusPill>
|
|
<h1>{copy.summary}</h1>
|
|
<p>
|
|
这里发布 NekoNest Cloud 免费公测的维护、降级和中断信息。主机自身离线但这里显示正常时,请先查看控制台接入状态。
|
|
</p>
|
|
<small>最近核对:{formatDate(snapshot.checkedAt, true)}</small>
|
|
</header>
|
|
|
|
<section className="status-section">
|
|
<div className="section-heading compact-heading">
|
|
<span className="eyebrow">ACTIVE / 处理中</span>
|
|
<h2>当前事件</h2>
|
|
</div>
|
|
{snapshot.activeIncidents.length ? (
|
|
<div className="incident-list">
|
|
{snapshot.activeIncidents.map((incident) => (
|
|
<article className={`incident-card incident-${incident.severity}`} key={incident.id}>
|
|
<div className="incident-card-heading">
|
|
<StatusPill tone={statusCopy[incident.severity].tone}>
|
|
{statusCopy[incident.severity].label}
|
|
</StatusPill>
|
|
<span>{formatDate(incident.started_at, true)}</span>
|
|
</div>
|
|
<h3>{incident.title}</h3>
|
|
<p>{incident.message}</p>
|
|
</article>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="status-empty">
|
|
<span aria-hidden="true">✓</span>
|
|
<div><strong>没有正在处理的事件</strong><p>如果你仍然无法连接主机,请在控制台提交问题反馈。</p></div>
|
|
</div>
|
|
)}
|
|
</section>
|
|
|
|
<section className="status-section status-history">
|
|
<div className="section-heading compact-heading">
|
|
<span className="eyebrow">HISTORY / 最近恢复</span>
|
|
<h2>事件记录</h2>
|
|
</div>
|
|
{resolved.length ? (
|
|
<div className="incident-history-list">
|
|
{resolved.map((incident) => (
|
|
<article key={incident.id}>
|
|
<div><strong>{incident.title}</strong><span>{formatDate(incident.resolved_at, true)}</span></div>
|
|
<p>{incident.resolution}</p>
|
|
</article>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<p className="status-history-empty">还没有已恢复的公开事件。</p>
|
|
)}
|
|
</section>
|
|
</div>
|
|
</PublicShell>
|
|
);
|
|
}
|