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
+95
View File
@@ -0,0 +1,95 @@
import Link from "next/link";
import { requireCloudViewer } from "../../cloud-auth";
import { DashboardShell, PageHeading, StatusPill, formatDate } from "../../components/Shells";
import { getDashboardSnapshot, getOrCreateAccount } from "@/db/repository";
import { HostRevokeButton } from "./HostRevokeButton";
import { PairingCancelButton } from "./PairingCancelButton";
import { getConnectionCopy } from "../connection-copy";
import { deriveControlPlaneContact } from "@/db/device-control-plane";
export const dynamic = "force-dynamic";
function lifecycleLabel(value: string) {
return value === "active" ? "已启用" : value === "deactivated" ? "已撤销" : value;
}
function slotLabel(value: string) {
return value === "active" ? "占用中" : value === "released" ? "已释放" : value;
}
export default async function HostsPage() {
const viewer = await requireCloudViewer("/dashboard/hosts");
const account = await getOrCreateAccount(viewer);
const snapshot = await getDashboardSnapshot(account);
const connection = getConnectionCopy(snapshot.connection.state);
return (
<DashboardShell viewer={viewer} active="/dashboard/hosts">
<div className="cloud-page">
<PageHeading
eyebrow="HOSTS / 主机"
title="主机与槽位"
description="只有启用的持久主机记录占槽位。离线不释放;明确停用或撤销后才释放。"
actions={<Link className="button button-primary" href="/dashboard/hosts/new"></Link>}
/>
<div className="entitlement-bar">
<div><span></span><strong>{snapshot.entitlement.mode === "public_beta" ? "公开公测" : snapshot.entitlement.mode === "grant" ? "闭测邀请" : "无"}</strong></div>
<div><span></span><strong>{snapshot.entitlement.activeSlots}</strong></div>
<div><span></span><strong>{snapshot.entitlement.reservedSlots}</strong></div>
<div><span></span><strong>{snapshot.entitlement.unlimited ? "不按槽位限额" : snapshot.entitlement.availableSlots}</strong></div>
</div>
<section className="panel full-panel">
<div className="panel-heading"><div><span></span><h2></h2></div><StatusPill tone={connection.tone}>{connection.label}</StatusPill></div>
<p>{connection.detail} {connection.nextStep}</p>
<p className="host-contact-note"> Cloud 线 Relay </p>
{snapshot.hosts.length ? (
<div className="host-list">
{snapshot.hosts.map((host) => {
const contact = deriveControlPlaneContact(host.control_plane_last_seen_at);
return (
<article className="host-row detailed-host" key={host.id}>
<span className={`host-icon host-${host.os}`}>{host.os === "windows" ? "W" : "L"}</span>
<div><strong>{host.name}</strong><small>{host.id}</small></div>
<div><span className="row-label"></span><strong>{slotLabel(host.slot_state)}</strong></div>
<div>
<span className="row-label">Daemon / </span>
<strong>{host.daemon_version || "版本待上报"}</strong>
<small>{host.control_plane_last_seen_at ? formatDate(host.control_plane_last_seen_at, true) : "等待首次 Relay 授权"}</small>
</div>
<StatusPill tone={host.lifecycle === "active" ? contact.tone : "neutral"}>{host.lifecycle === "active" ? contact.label : lifecycleLabel(host.lifecycle)}</StatusPill>
{host.lifecycle === "active" && <HostRevokeButton hostId={host.id} />}
{host.lifecycle === "deactivated" && <Link className="button button-secondary" href="/dashboard/hosts/new"></Link>}
</article>
);
})}
</div>
) : (
<div className="empty-state"><span className="empty-symbol"></span><h3></h3><p>线</p><Link className="button button-primary" href="/dashboard/hosts/new"></Link></div>
)}
</section>
<section className="panel full-panel recovery-guide">
<div className="panel-heading"><div><span></span><h2></h2></div></div>
<div className="recovery-options">
<article><StatusPill tone="good">identity.json </StatusPill><h3></h3><p> identity.json config.json使 daemon daemon Cloud ID </p></article>
<article><StatusPill tone="warn"></StatusPill><h3></h3><p> ID</p></article>
</div>
</section>
<section className="panel full-panel">
<div className="panel-heading"><div><span></span><h2></h2></div></div>
{snapshot.pairings.length ? (
<div className="pairing-list">
{snapshot.pairings.map((pairing) => (
<article key={pairing.id}><div><strong>{pairing.requested_name}</strong><small>{pairing.os.toUpperCase()} · {pairing.id}</small></div><StatusPill tone="warn"> daemon</StatusPill><span>{formatDate(pairing.expires_at, true)}</span><PairingCancelButton pairingId={pairing.id} /></article>
))}
</div>
) : (
<div className="empty-inline"></div>
)}
</section>
</div>
</DashboardShell>
);
}