284 lines
25 KiB
TypeScript
284 lines
25 KiB
TypeScript
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>批准申请会在同一数据库动作中签发 1–3 台、有期限的非货币邀请;拒绝不会创建权益。申请顺序不代表承诺或优先级。</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>
|
||
);
|
||
}
|