96 lines
6.0 KiB
TypeScript
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>
|
|
);
|
|
}
|