Files
nekonest-cloud/app/dashboard/page.tsx
T

139 lines
8.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 { getConnectionCopy } from "./connection-copy";
import { OpenPwaButton } from "./OpenPwaButton";
import { deriveControlPlaneContact } from "@/db/device-control-plane";
import { deriveBetaOnboarding } from "./onboarding";
export const dynamic = "force-dynamic";
export default async function DashboardPage() {
const viewer = await requireCloudViewer("/dashboard");
const account = await getOrCreateAccount(viewer);
const snapshot = await getDashboardSnapshot(account);
const activeHosts = snapshot.hosts.filter((host) => host.lifecycle === "active");
const entitlement = snapshot.entitlement;
const connection = getConnectionCopy(snapshot.connection.state);
const onboarding = deriveBetaOnboarding({
entitlement,
hasPendingRequest: snapshot.accessRequests.some((request) => request.status === "requested"),
});
const entitlementLabel = entitlement.mode === "public_beta"
? "公测免费"
: entitlement.mode === "grant"
? "闭测邀请"
: entitlement.publicBetaState === "gated"
? "等待安全门禁"
: "无有效权益";
const entitlementDetail = entitlement.mode === "none" && entitlement.publicBetaState === "gated"
? `公开接入仍有 ${entitlement.blockedP0} 项 P0 未通过;管理员邀请账户可继续闭测`
: entitlement.unlimited
? "费用全免;当前策略未按槽位限额"
: `可用 ${entitlement.availableSlots ?? 0} 个槽位`;
return (
<DashboardShell
viewer={viewer}
active="/dashboard"
serviceStatus={snapshot.serviceStatus}
>
<div className="cloud-page">
<PageHeading
eyebrow="OVERVIEW / 总览"
title={`晚上好,${viewer.displayName}`}
description="这里显示控制平面真实保存的公测接入状态;原生会话仍由你的主机和 NekoNest PWA 提供。"
actions={snapshot.connection.state === "ready"
? <OpenPwaButton />
: <Link className="button button-primary" href={onboarding.primaryHref}>{onboarding.primaryLabel}</Link>}
/>
<section className="metric-grid" aria-label="账户概览">
<article className="metric-card accent-card">
<span>当前权益</span>
<strong>{entitlementLabel}</strong>
<small>{entitlementDetail}</small>
</article>
<article className="metric-card">
<span>启用主机</span>
<strong>{entitlement.activeSlots}</strong>
<small>{entitlement.reservedSlots ? `${entitlement.reservedSlots} 个配对请求占位中` : "没有等待配对的占位"}</small>
</article>
<article className="metric-card">
<span>租户运行态</span>
<strong>{connection.label}</strong>
<small>{snapshot.connection.homeRegion
? `${snapshot.connection.homeRegion} · generation ${snapshot.connection.placementGeneration ?? "-"}`
: "等待 home region 分配"}</small>
</article>
<article className="metric-card">
<span>下次付款</span>
<strong></strong>
<small>公测不绑支付方式,也不会自动续费</small>
</article>
</section>
<div className="dashboard-columns">
<section className="panel">
<div className="panel-heading">
<div><span>主机</span><h2>接入状态</h2></div>
<Link href="/dashboard/hosts">查看全部 </Link>
</div>
{activeHosts.length ? (
<div className="host-list">
{activeHosts.slice(0, 4).map((host) => {
const contact = deriveControlPlaneContact(host.control_plane_last_seen_at);
return (
<article className="host-row" key={host.id}>
<span className={`host-icon host-${host.os}`}>{host.os === "windows" ? "W" : "L"}</span>
<div><strong>{host.name}</strong><small>{host.daemon_version || "daemon 版本待上报"}</small></div>
<div className="host-state">
<StatusPill tone={contact.tone}>{contact.label}</StatusPill>
<small>{host.control_plane_last_seen_at ? formatDate(host.control_plane_last_seen_at, true) : "等待首次 Relay 授权"}</small>
</div>
</article>
);
})}
</div>
) : (
<div className="empty-state compact-empty">
<span className="empty-symbol"></span>
<h3>还没有已认领的主机</h3>
<p>先创建一次性引导凭证。daemon 完成控制面认领后,会在同一个稳定服务地址等待共享 Relay 就绪。</p>
<Link className="button button-secondary" href="/dashboard/hosts/new">创建配对请求</Link>
</div>
)}
</section>
<section className="panel attention-panel">
<div className="panel-heading"><div><span>当前需要注意</span><h2>诚实状态</h2></div></div>
<div className="attention-list">
{onboarding.state !== "ready" && <article><StatusPill tone={onboarding.tone}>{onboarding.state === "request_pending" ? "审核中" : onboarding.state === "request_needed" ? "先申请" : "容量已满"}</StatusPill><div><strong>{onboarding.title}</strong><p>{onboarding.detail} <Link className="text-link" href={onboarding.primaryHref}>{onboarding.primaryLabel} </Link></p></div></article>}
<article><StatusPill tone={connection.tone}>{connection.label}</StatusPill><div><strong>{connection.detail}</strong><p>{connection.nextStep}</p></div></article>
{entitlement.publicBetaState === "gated" && <article><StatusPill tone="warn">公开接入冻结</StatusPill><div><strong>P0 门禁不能被免费政策绕过</strong><p>新公开配对暂不开放;管理员明确签发的免费闭测邀请仍可继续。</p></div></article>}
<article><StatusPill tone="info">公测</StatusPill><div><strong>报价与订单不开放</strong><p>当前没有任何付款流程;先把主机接入、恢复和稳定性做好。</p></div></article>
<article><StatusPill tone="info">规则</StatusPill><div><strong>公测结束不自动转付费</strong><p>收费方案以后再定;未主动确认前不会创建付费关系。</p></div></article>
</div>
</section>
</div>
<section className="path-panel">
<div><span className="eyebrow">CONNECTION PATH</span><h2>按当前状态完成下一步。</h2></div>
<ol>
<li className={entitlement.mode !== "none" ? "done" : "current"}><span>01</span><div><strong>获得免费测试资格</strong><p>{onboarding.title}{onboarding.detail}</p></div></li>
<li className={snapshot.pairings.length || activeHosts.length ? "done" : onboarding.state === "ready" ? "current" : undefined}><span>02</span><div><strong>创建短时配对请求</strong><p>有可用免费名额后,控制平面生成十分钟一次性码。</p></div></li>
<li className={activeHosts.length ? "done" : snapshot.pairings.length ? "current" : undefined}><span>03</span><div><strong>daemon 认领并证明主机身份</strong><p>控制面签发独立设备令牌,并推进租户 authorization revision</p></div></li>
<li className={snapshot.connection.state === "ready" ? "done" : activeHosts.length ? "current" : undefined}><span>04</span><div><strong>共享 sealed Relay 就绪</strong><p>{snapshot.connection.state === "ready" ? "daemon 保持稳定服务地址即可连接。" : "等待租户 placement 指向健康节点;不会向客户端释放后端节点 URL。"}</p></div></li>
</ol>
</section>
</div>
</DashboardShell>
);
}