Files

284 lines
25 KiB
TypeScript
Raw Permalink 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 { requireAdminViewer } from "../cloud-auth";
import { DashboardShell, PageHeading, StatusPill, formatDate } from "../components/Shells";
import { getAdminSnapshot, type AccountDeletionRequestRecord, type FeedbackCategory } from "@/db/repository";
import type { ControlPlaneContactState } from "@/db/device-control-plane";
import { deriveInvitationDisplayState } from "@/db/invitations";
import {
AccessRequestAction,
BetaAction,
ExemptionAction,
FeedbackAction,
GateAction,
IncidentAction,
IncidentResolveAction,
InvitationRevokeAction,
PurgeAction,
RetentionAction,
} from "./AdminActions";
export const dynamic = "force-dynamic";
const feedbackCategoryLabels: Record<FeedbackCategory, string> = {
connection_issue: "连接或接入",
bug: "功能异常",
suggestion: "使用建议",
other: "其他",
};
const daemonContactCopy: Record<
ControlPlaneContactState,
{ label: string; tone: "good" | "warn" | "danger" | "neutral" }
> = {
fresh: { label: "控制面正常", tone: "good" },
delayed: { label: "联系延迟", tone: "warn" },
stale: { label: "长时间未联系", tone: "danger" },
never: { label: "尚未联系", tone: "neutral" },
invalid: { label: "时间异常", tone: "danger" },
};
const deletionStatusCopy: Record<
AccountDeletionRequestRecord["status"],
{ label: string; tone: "good" | "warn" | "danger" | "neutral" }
> = {
requested: { label: "待核对", tone: "warn" },
cancelled: { label: "已撤回", tone: "neutral" },
processing: { label: "删除中", tone: "danger" },
relay_purged: { label: "Relay 已逻辑删除", tone: "good" },
};
const retentionStatusCopy = {
never_run: {
label: "尚未自动运行",
tone: "warn",
detail: "定时入口尚未留下首次执行证据;管理员仍可使用下方手工回退。",
},
running: {
label: "正在清理",
tone: "info",
detail: "自动任务已经取得单实例运行权,正在处理已到期技术记录。",
},
healthy: {
label: "自动清理正常",
tone: "good",
detail: "最近一次自动清理在预期时间窗内成功完成。",
},
overdue: {
label: "自动清理逾期",
tone: "warn",
detail: "最近成功已经超过 36 小时,请核对托管环境的定时触发器。",
},
failed: {
label: "自动清理失败",
tone: "danger",
detail: "最近一次自动清理失败;错误码已最小化保存,运行时错误会继续进入平台观测。",
},
stalled: {
label: "自动清理卡住",
tone: "danger",
detail: "任务运行超过 30 分钟;下一次触发可以安全接管,但应先核对 D1 与 Worker 状态。",
},
invalid: {
label: "清理记录异常",
tone: "danger",
detail: "运行状态包含无效时间、计数或触发来源,不能把它当作成功证据。",
},
} as const;
function formatPercent(value: number | null) {
return value === null ? "暂无样本" : `${value}%`;
}
function formatDuration(seconds: number | null) {
if (seconds === null) return "暂无样本";
if (seconds < 60) return `${seconds} 秒`;
if (seconds < 3600) return `${Math.round(seconds / 60)} 分钟`;
if (seconds < 86400) return `${Math.round((seconds / 3600) * 10) / 10} 小时`;
return `${Math.round((seconds / 86400) * 10) / 10} 天`;
}
export default async function AdminPage() {
const viewer = await requireAdminViewer();
const snapshot = await getAdminSnapshot();
const deferredPaidGates = snapshot.gates.filter((gate) => gate.priority === "PAID").length;
const openFeedback = snapshot.feedback.filter((item) => item.status === "open");
const pendingAccessRequests = snapshot.accessRequests.filter((item) => item.status === "requested");
const pendingAccessAccountIds = new Set(pendingAccessRequests.map((item) => item.account_id));
const proactiveInvitationAccounts = snapshot.accounts.filter((account) => !pendingAccessAccountIds.has(account.id));
const invitations = snapshot.grants.filter((grant) => grant.source === "admin_exemption");
const activeIncidents = snapshot.incidents.filter((item) => item.status === "active");
const pendingDeletionRequests = snapshot.deletionRequests.filter((item) => item.status === "requested");
const daemonContactAttention = snapshot.hostContacts.delayed
+ snapshot.hostContacts.stale
+ snapshot.hostContacts.never
+ snapshot.hostContacts.invalid;
const daemonContactTone = snapshot.hostContacts.invalid || snapshot.hostContacts.stale
? "danger"
: daemonContactAttention
? "warn"
: snapshot.hostContacts.totalActive
? "good"
: "neutral";
const retentionStatus = retentionStatusCopy[snapshot.retention.state];
const retentionLastSuccess = snapshot.retention.state === "invalid"
? "记录异常"
: formatDate(snapshot.retention.lastSuccessAt, true);
return (
<DashboardShell viewer={viewer} active="/admin">
<div className="cloud-page admin-page">
<PageHeading eyebrow="PUBLIC BETA ADMIN / 公测后台" title="免费政策、接入资格与上线门禁。" description="当前只运营免费公测。每次管理变更仍需理由、幂等键和追加式审计;报价、订单和价格发布均保持关闭。" />
<section className="admin-metrics"><article><span>公测 P0 未通过</span><strong>{snapshot.blockedP0}</strong><small>非零时生产公测开通必须关闭</small></article><article><span>公测政策</span><strong>{snapshot.beta?.state === "active" ? "全部免费" : "已结束"}</strong><small>{snapshot.beta?.capacity_slots === null ? "每账户不按主机槽位限额" : `每账户最多 ${snapshot.beta?.capacity_slots ?? 0} 台主机`}</small></article><article><span>服务状态</span><strong>{activeIncidents.length ? `${activeIncidents.length} 个事件` : "正常"}</strong><small>公开状态与控制台提醒同步</small></article><article><span>待审闭测申请</span><strong>{pendingAccessRequests.length}</strong><small>另有 {openFeedback.length} 条待处理反馈;收费门禁 {deferredPaidGates} 项暂缓</small></article></section>
<section className="panel full-panel operations-panel">
<div className="panel-heading">
<div><span>DAEMON CONTROL PLANE / 即时</span><h2>主机控制面签到</h2></div>
<StatusPill tone={daemonContactTone}>{daemonContactAttention ? `${daemonContactAttention} 台需留意` : snapshot.hostContacts.totalActive ? "全部正常" : "暂无主机"}</StatusPill>
</div>
<p className="operations-intro">从设备凭据最近一次成功 Relay 授权汇总,只证明 daemon 已通过控制面鉴权;不单独证明长连接、重连或 sealed 会话质量。</p>
<div className="operations-grid daemon-contact-grid">
<article><span>启用主机</span><strong>{snapshot.hostContacts.totalActive}</strong><small>{snapshot.hostContacts.versionUnknown} 台尚未上报 daemon 版本</small></article>
<article><span>十分钟内联系</span><strong>{snapshot.hostContacts.fresh}</strong><small>最近成功使用有效设备令牌完成 Relay 授权</small></article>
<article><span>联系延迟</span><strong>{snapshot.hostContacts.delayed}</strong><small>十至三十分钟没有再次查询,建议先观察</small></article>
<article><span>长时间未联系</span><strong>{snapshot.hostContacts.stale}</strong><small>超过三十分钟,优先核对 daemon 进程、网络和配置</small></article>
<article><span>从未联系</span><strong>{snapshot.hostContacts.never}</strong><small>认领后还没有成功完成首次 Relay 授权</small></article>
<article><span>时间异常</span><strong>{snapshot.hostContacts.invalid}</strong><small>记录不可解析或明显超前,不能作为活性证据</small></article>
</div>
<div className="daemon-contact-attention">
<h3>需要留意的主机</h3>
{snapshot.hostContacts.attentionHosts.length ? (
<div className="admin-gate-table">
{snapshot.hostContacts.attentionHosts.map((host) => {
const state = daemonContactCopy[host.contact_state];
return (
<article key={host.id}>
<span className="gate-priority">{host.os === "windows" ? "W" : "L"}</span>
<div><strong>{host.name}</strong><small>{host.email} · {host.id} · {host.daemon_version || "版本待上报"}</small></div>
<StatusPill tone={state.tone}>{state.label}</StatusPill>
<span>{host.control_plane_last_seen_at ? formatDate(host.control_plane_last_seen_at, true) : "没有成功查询"}</span>
</article>
);
})}
</div>
) : <div className="empty-inline">当前没有需要留意的启用主机。</div>}
</div>
<p className="operations-footnote">最多列出 25 台非正常主机;汇总包含全部启用主机。最近核对:{formatDate(snapshot.hostContacts.generatedAt, true)}</p>
</section>
<section className="panel full-panel operations-panel">
<div className="panel-heading">
<div><span>ROLLING 30 DAYS / 30 </span><h2>免费公测运营漏斗</h2></div>
<StatusPill tone="info">控制平面实数</StatusPill>
</div>
<p className="operations-intro">只汇总账户、配对、开通任务和站内反馈,不采集原生会话、项目内容或提示词。数字来自当前数据库记录,不代表真实 relay 稳定性。</p>
<div className="operations-grid">
<article>
<span>新增账户</span>
<strong>{snapshot.operations.accounts.newAccounts}</strong>
<small>累计 {snapshot.operations.accounts.total} 个账户;{snapshot.operations.accounts.withActiveHost} 个已有启用主机</small>
</article>
<article>
<span>闭测申请</span>
<strong>{snapshot.operations.accessRequests.submitted}</strong>
<small>{snapshot.operations.accessRequests.pending} 条仍待审;{snapshot.operations.accessRequests.cancelled} 条由用户撤回</small>
</article>
<article>
<span>申请批准率</span>
<strong>{formatPercent(snapshot.operations.accessRequests.approvalRatePercent)}</strong>
<small>{snapshot.operations.accessRequests.approved}/{snapshot.operations.accessRequests.decided} 条已决申请获批;平均审核 {formatDuration(snapshot.operations.accessRequests.averageReviewSeconds)}</small>
</article>
<article>
<span>申请设备需求</span>
<strong>{snapshot.operations.accessRequests.requestedSlotDemand} </strong>
<small>平均 {snapshot.operations.accessRequests.averageRequestedSlots ?? "暂无样本"} 台;Windows {snapshot.operations.accessRequests.windows}Linux {snapshot.operations.accessRequests.linux}、双系统 {snapshot.operations.accessRequests.both}</small>
</article>
<article>
<span>获批后认领率</span>
<strong>{formatPercent(snapshot.operations.accessRequests.postApprovalClaimRatePercent)}</strong>
<small>{snapshot.operations.accessRequests.approvedWithPostApprovalClaim}/{snapshot.operations.accessRequests.approved} 条获批申请在获批后至少认领过一台主机;不代表 relay 可用</small>
</article>
<article>
<span>配对认领率</span>
<strong>{formatPercent(snapshot.operations.pairings.claimRatePercent)}</strong>
<small>{snapshot.operations.pairings.claimed}/{snapshot.operations.pairings.created} 个新请求已认领;平均 {formatDuration(snapshot.operations.pairings.averageClaimSeconds)}</small>
</article>
<article>
<span>配对异常信号</span>
<strong>{snapshot.operations.pairings.attentionRequired}</strong>
<small>{snapshot.operations.pairings.waiting} 个仍可认领;{snapshot.operations.pairings.expired} 个过期、{snapshot.operations.pairings.locked} 个锁定;另有 {snapshot.operations.pairings.rejectedAttempts} 次拒绝、{snapshot.operations.pairings.rateLimitedAttempts} 次限速</small>
</article>
<article>
<span>Relay placement 就绪率</span>
<strong>{formatPercent(snapshot.operations.provisioning.successRatePercent)}</strong>
<small>{snapshot.operations.provisioning.succeeded}/{snapshot.operations.provisioning.created} placement 已进入 active;平均 {formatDuration(snapshot.operations.provisioning.averageCompletionSeconds)}</small>
</article>
<article>
<span>反馈解决率</span>
<strong>{formatPercent(snapshot.operations.support.resolutionRatePercent)}</strong>
<small>{snapshot.operations.support.resolved}/{snapshot.operations.support.created} 条已回复;{snapshot.operations.support.connectionIssues} 条属于连接问题,平均周转 {formatDuration(snapshot.operations.support.averageResolutionSeconds)}</small>
</article>
<article className="operations-unavailable">
<span>尚不可测</span>
<strong>真实中继</strong>
<small>重连成功率、relay 延迟、运行时资源成本和实际支持工时,等真实租户接通后再采集</small>
</article>
</div>
<p className="operations-footnote">口径:账户总数和“已有启用主机”是当前累计值,其余均按记录创建时间滚动统计近 {snapshot.operations.windowDays} 天;处理中记录不会被算作成功。最近核对:{formatDate(snapshot.operations.generatedAt, true)}</p>
</section>
<div className="admin-grid">
<section className="panel admin-module"><div className="panel-heading"><div><span>版本化政策</span><h2>全局公测免费</h2></div><StatusPill tone={snapshot.beta?.state === "active" && snapshot.blockedP0 === 0 ? "good" : snapshot.beta?.state === "active" ? "warn" : "neutral"}>{snapshot.beta?.state === "active" && snapshot.blockedP0 > 0 ? "政策已设 / 接入冻结" : snapshot.beta?.state ?? "未设置"}</StatusPill></div><BetaAction beta={snapshot.beta} blockedP0={snapshot.blockedP0} /></section>
<section className="panel admin-module"><div className="panel-heading"><div><span>暂缓施工</span><h2>收费功能以后再做</h2></div><StatusPill tone="neutral">未开放</StatusPill></div><p>当前不发布价格、不生成报价、不创建订单。等用户规模、资源成本和支持负担有真实数据后,再重新做收费决策。</p></section>
<section className="panel admin-module"><div className="panel-heading"><div><span>主动邀请 / 非货币权益</span><h2>签发闭测邀请</h2></div></div><p>这里只给没有待审申请的账户主动发邀请。已有申请必须在下方队列批准或拒绝,避免用户同时看到“已有资格”和“仍在审核”。邀请不创建金额、余额或未来付费关系。</p><ExemptionAction accounts={proactiveInvitationAccounts} /></section>
<section className="panel admin-module">
<div className="panel-heading">
<div><span>DATA MINIMIZATION</span><h2>到期技术数据清理</h2></div>
<StatusPill tone={retentionStatus.tone}>{retentionStatus.label}</StatusPill>
</div>
<p>每天北京时间 02:17 自动处理已经到期的技术记录;手工入口仅作恢复回退,不扩大删除范围。</p>
<div className="maintenance-status" aria-label="到期数据自动清理状态">
<div><span>最近自动成功</span><strong>{retentionLastSuccess}</strong></div>
<div><span>自动运行次数</span><strong>{snapshot.retention.runCount}</strong></div>
<div><span>连续失败</span><strong>{snapshot.retention.consecutiveFailures}</strong></div>
</div>
<p className="maintenance-status-detail">
{retentionStatus.detail}
{snapshot.retention.ageSeconds !== null ? ` 距相关状态约 ${formatDuration(snapshot.retention.ageSeconds)}。` : ""}
{snapshot.retention.errorCode ? ` 错误码:${snapshot.retention.errorCode}。` : ""}
</p>
<RetentionAction />
</section>
<section className="panel admin-module"><div className="panel-heading"><div><span>证据驱动</span><h2>更新上线门禁</h2></div></div><GateAction gates={snapshot.gates} /></section>
</div>
<section className="panel full-panel"><div className="panel-heading"><div><span>CLOSED BETA REQUESTS</span><h2>免费闭测申请队列</h2></div><StatusPill tone={pendingAccessRequests.length ? "warn" : "good"}>{pendingAccessRequests.length ? `${pendingAccessRequests.length} 条待处理` : "已清空"}</StatusPill></div><p>批准申请会在同一数据库动作中签发 13 台、有期限的非货币邀请;拒绝不会创建权益。申请顺序不代表承诺或优先级。</p><div className="admin-feedback-list">{snapshot.accessRequests.length ? snapshot.accessRequests.map((request) => { const account = snapshot.accounts.find((item) => item.id === request.account_id); return <article key={request.id}><div className="feedback-meta"><StatusPill tone={request.status === "approved" ? "good" : request.status === "requested" ? "warn" : "neutral"}>{request.status === "approved" ? "已批准" : request.status === "requested" ? "待处理" : request.status === "declined" ? "未批准" : "用户已撤回"}</StatusPill><span>{account?.email ?? request.account_id}</span><span>{request.requested_slots} · {request.preferred_os}</span><span>{formatDate(request.requested_at, true)}</span></div><p>{request.use_case}</p>{request.admin_response && <div className="feedback-response"><strong>给用户的说明</strong><p>{request.admin_response}</p></div>}<code>{request.id}</code>{request.status === "requested" && <AccessRequestAction request={request} />}</article>; }) : <div className="empty-inline">还没有闭测申请。</div>}</div></section>
<section className="panel full-panel"><div className="panel-heading"><div><span>CLOSED BETA INVITATIONS</span><h2>闭测邀请记录</h2></div><StatusPill tone={invitations.some((grant) => deriveInvitationDisplayState(grant) === "active") ? "good" : "neutral"}>{invitations.filter((grant) => deriveInvitationDisplayState(grant) === "active").length} 个有效</StatusPill></div><p>撤销只阻止该邀请继续创建或认领新配对;既有主机的处置走独立撤销流程。</p><div className="admin-feedback-list">{invitations.length ? invitations.map((invitation) => { const state = deriveInvitationDisplayState(invitation); const account = snapshot.accounts.find((item) => item.id === invitation.account_id); return <article key={invitation.id}><div className="feedback-meta"><StatusPill tone={state === "active" ? "good" : "neutral"}>{state === "active" ? "有效" : state === "expired" ? "已到期" : "已撤销"}</StatusPill><span>{account?.email ?? invitation.account_id}</span><span>{invitation.capacity_slots === null ? "不按槽位限额" : `${invitation.capacity_slots} 个槽位`}</span><span> {formatDate(invitation.ends_at, true)}</span></div><p>{invitation.reason}</p><code>{invitation.id}</code>{state === "active" && <InvitationRevokeAction invitation={invitation} />}</article>; }) : <div className="empty-inline">还没有闭测邀请。</div>}</div></section>
<section className="panel full-panel">
<div className="panel-heading">
<div><span>ACCOUNT LIFECYCLE</span><h2>注销申请队列</h2></div>
<StatusPill tone={pendingDeletionRequests.length ? "warn" : "good"}>
{pendingDeletionRequests.length ? `${pendingDeletionRequests.length} 条待核对` : "无待核对"}
</StatusPill>
</div>
<p>只有已确认的申请才能启动永久逻辑删除。Relay 数据删除完成不等于账户身份、法定保留记录和云存储物理块已经全部清除。</p>
<div className="admin-deletion-list">
{snapshot.deletionRequests.length ? snapshot.deletionRequests.map((request) => {
const copy = deletionStatusCopy[request.status];
return (
<article key={request.id}>
<div>
<StatusPill tone={copy.tone}>{copy.label}</StatusPill>
<strong>{snapshot.accounts.find((account) => account.id === request.account_id)?.email ?? request.account_id}</strong>
<span>{formatDate(request.requested_at, true)}</span>
</div>
<p>{request.reason || "用户未填写补充说明。"}</p>
<code>{request.id}</code>
{request.status === "requested" && <PurgeAction request={request} />}
</article>
);
}) : <div className="empty-inline">还没有注销申请。</div>}
</div>
</section>
<section className="panel full-panel"><div className="panel-heading"><div><span>STATUS & INCIDENTS</span><h2>服务状态与故障公告</h2></div><StatusPill tone={activeIncidents.length ? "danger" : "good"}>{activeIncidents.length ? `${activeIncidents.length} 个处理中` : "服务正常"}</StatusPill></div><div className="admin-incident-layout"><div><h3>发布新公告</h3><p>只发布会影响用户操作的信息;公开说明不会替代内部日志和异常告警。</p><IncidentAction /></div><div className="admin-incident-list"><h3>事件记录</h3>{snapshot.incidents.length ? snapshot.incidents.map((incident) => <article key={incident.id}><div className="incident-card-heading"><StatusPill tone={incident.status === "resolved" ? "good" : incident.severity === "outage" ? "danger" : incident.severity === "degraded" ? "warn" : "info"}>{incident.status === "resolved" ? "已恢复" : incident.severity === "outage" ? "服务中断" : incident.severity === "degraded" ? "服务降级" : "计划维护"}</StatusPill><span>{formatDate(incident.started_at, true)}</span></div><h4>{incident.title}</h4><p>{incident.message}</p>{incident.status === "active" ? <IncidentResolveAction incident={incident} /> : <div className="feedback-response"><strong>恢复说明</strong><p>{incident.resolution}</p></div>}</article>) : <div className="empty-inline">还没有服务事件。</div>}</div></div></section>
<section className="panel full-panel"><div className="panel-heading"><div><span>FREE BETA SUPPORT</span><h2>公测反馈处理</h2></div><StatusPill tone={openFeedback.length ? "warn" : "good"}>{openFeedback.length ? `${openFeedback.length} 条待处理` : "已清空"}</StatusPill></div>{snapshot.feedback.length ? <div className="admin-feedback-list">{snapshot.feedback.map((item) => <article key={item.id}><div className="feedback-meta"><StatusPill tone={item.status === "resolved" ? "good" : "warn"}>{item.status === "resolved" ? "已回复" : "待处理"}</StatusPill><span>{feedbackCategoryLabels[item.category]}</span><span>{formatDate(item.created_at, true)}</span><span>{snapshot.accounts.find((account) => account.id === item.account_id)?.email ?? item.account_id}</span></div><p>{item.message}</p>{item.admin_response && <div className="feedback-response"><strong>已回复</strong><p>{item.admin_response}</p></div>}{item.status === "open" && <FeedbackAction feedback={item} />}</article>)}</div> : <div className="empty-inline">还没有用户反馈。</div>}</section>
<section className="panel full-panel"><div className="panel-heading"><div><span>P0 / P1 / PAID</span><h2>门禁总表</h2></div></div><div className="admin-gate-table">{snapshot.gates.map((gate) => <article key={gate.key}><span className="gate-priority">{gate.priority}</span><div><strong>{gate.title}</strong><small>{gate.category} · {gate.key}</small></div><StatusPill tone={gate.priority === "PAID" ? "neutral" : gate.status === "passed" ? "good" : gate.status === "in_progress" ? "warn" : "danger"}>{gate.priority === "PAID" ? "以后处理" : gate.status}</StatusPill><span>{gate.owner || "待指定"}</span></article>)}</div></section>
<section className="panel full-panel"><div className="panel-heading"><div><span>APPEND ONLY</span><h2>最近管理审计</h2></div></div><div className="audit-list">{snapshot.audits.length ? snapshot.audits.map((audit) => <article key={audit.id}><span>{formatDate(audit.created_at, true)}</span><strong>{audit.action}</strong><span>{audit.target_type} / {audit.target_id}</span><p>{audit.reason}</p><code>{audit.actor_id}</code></article>) : <div className="empty-inline">还没有管理审计事件。</div>}</div></section>
</div>
</DashboardShell>
);
}