feat: establish NekoNest Cloud control and relay

This commit is contained in:
2026-08-12 23:25:43 +08:00
commit f27606b709
222 changed files with 71456 additions and 0 deletions
+14
View File
@@ -0,0 +1,14 @@
"use client";
import { RouteErrorState } from "../components/RouteStates";
export default function StatusError({ reset }: { reset: () => void }) {
return (
<RouteErrorState
area="服务状态"
title="状态页暂时不可用"
description="这次没有读取到可信的服务公告,因此不会显示推测的“服务正常”。"
reset={reset}
/>
);
}
+10
View File
@@ -0,0 +1,10 @@
import { RouteLoadingState } from "../components/RouteStates";
export default function StatusLoading() {
return (
<RouteLoadingState
area="服务状态"
description="正在核对免费公测服务的最新公告和恢复记录。"
/>
);
}
+97
View File
@@ -0,0 +1,97 @@
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>
);
}