Files

96 lines
6.0 KiB
TypeScript

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>
);
}