Optimize terminal detail loading and Redis replay

This commit is contained in:
2569718930@qq.com
2026-05-31 04:32:48 +08:00
parent 2a6d8748f4
commit 46effcd45f
16 changed files with 948 additions and 80 deletions
@@ -27,8 +27,12 @@ export function runTests() {
systemPage.includes("sourceHealth") &&
systemPage.includes("MGM、KNMI、IMS") &&
systemPage.includes("断线") &&
systemPage.includes("延迟"),
"ops system page must show source latency/disconnect status for operational city sources",
systemPage.includes("延迟") &&
systemPage.includes("sourceReasonLabel") &&
systemPage.includes("观测时间缺失") &&
systemPage.includes("formatOpsValue") &&
systemPage.includes("强制刷新"),
"ops system page must show readable source reasons, object values, and cache force-refresh metrics",
);
assert(
nextRoute.includes("requireOpsProxyAuth") &&
@@ -50,6 +50,25 @@ function compactDate(value?: string) {
return value.slice(0, 19).replace("T", " ");
}
function paymentReasonLabel(reason?: string) {
const key = String(reason || "").trim().toLowerCase();
if (key === "receiver_mismatch") return "收款地址不匹配";
if (key === "amount_mismatch") return "金额不匹配";
if (key === "chain_mismatch") return "支付网络不匹配";
if (key === "tx_not_found") return "链上交易未找到";
if (key === "tx_reverted") return "链上交易失败";
if (key === "expired") return "订单已过期";
if (key === "unknown") return "未知原因";
return key || "未知原因";
}
function compactMono(value?: string, head = 10, tail = 6) {
const text = String(value || "").trim();
if (!text) return "—";
if (text.length <= head + tail + 3) return text;
return `${text.slice(0, head)}...${text.slice(-tail)}`;
}
function RiskStat({
label,
value,
@@ -148,7 +167,7 @@ export function PaymentsPageClient() {
<div className="grid grid-cols-2 gap-3 md:grid-cols-4 xl:grid-cols-7">
<RiskStat label="总异常" value={Number(riskSummary.issues ?? 0)} sub="需要人工关注" tone={hasRisk ? "text-red-600" : "text-emerald-600"} />
<RiskStat label="Intent 卡住" value={Number(riskSummary.stuck_intents ?? 0)} sub="submitted/过期 created" />
<RiskStat label="试用漏开" value={Number(riskSummary.trial_gaps ?? 0)} sub="signup 无 trial_created" />
<RiskStat label="试用漏开" value={Number(riskSummary.trial_gaps ?? 0)} sub="后端试用/订阅证据缺失" />
<RiskStat label="支付异常" value={Number(riskSummary.payment_incidents ?? incidents.length)} sub="未标记处理" />
<RiskStat label="积分异常" value={Number(riskSummary.points_discount_issues ?? 0)} sub="确认后未扣/少扣" />
<RiskStat label="推荐异常" value={Number(riskSummary.referral_settlement_issues ?? 0)} sub="转化无奖励记录" />
@@ -287,7 +306,8 @@ export function PaymentsPageClient() {
<thead>
<tr className="border-b border-white/10 text-left text-slate-400">
<th className="py-2 pr-4 font-medium">ID</th>
<th className="py-2 pr-4 font-medium"></th>
<th className="py-2 pr-4 font-medium"> / </th>
<th className="py-2 pr-4 font-medium"> / Intent</th>
<th className="py-2 pr-4 font-medium"></th>
<th className="py-2 font-medium"></th>
</tr>
@@ -296,7 +316,16 @@ export function PaymentsPageClient() {
{incidents.map((inc) => (
<tr key={inc.id} className="border-b border-white/5">
<td className="py-2 pr-4 text-slate-500 font-mono">{inc.id}</td>
<td className="py-2 pr-4 text-amber-300">{inc.reason ?? "—"}</td>
<td className="py-2 pr-4">
<div className="font-bold text-amber-600">{paymentReasonLabel(inc.reason)}</div>
<div className="mt-0.5 max-w-xl truncate text-xs text-slate-500" title={inc.detail || inc.reason || ""}>
{inc.detail || inc.reason || "—"}
</div>
</td>
<td className="py-2 pr-4 text-xs text-slate-500">
<div className="font-mono" title={inc.user_id || ""}>{compactMono(inc.user_id)}</div>
<div className="mt-0.5 font-mono text-blue-700" title={inc.intent_id || ""}>{compactMono(inc.intent_id, 12, 6)}</div>
</td>
<td className="py-2 pr-4 text-slate-400 text-xs">{inc.created_at?.slice(0, 19) ?? "—"}</td>
<td className="py-2">
<Button
@@ -31,6 +31,35 @@ function formatAge(ageMin?: number | null) {
return `${(ageMin / 60).toFixed(1)}h`;
}
function sourceReasonLabel(reason?: string | null) {
const key = String(reason || "").trim().toLowerCase();
if (key === "observation_time_missing") return "观测时间缺失";
if (key === "expected_source_not_present_in_cached_detail") return "缓存详情未包含预期来源";
if (key === "past_expected_cadence") return "超过预期更新节奏";
if (key === "within_expected_cadence") return "仍在预期更新窗口";
if (key === "missing_observation") return "缺少观测值";
if (key === "no_cached_detail") return "缺少城市详情缓存";
return key || "—";
}
function formatOpsValue(value: unknown) {
if (typeof value === "boolean") {
return { label: value ? "TRUE" : "FALSE", active: value };
}
if (typeof value === "string" || typeof value === "number") {
return { label: String(value), active: Boolean(value) };
}
if (Array.isArray(value)) {
return { label: `${value.length}`, active: value.length > 0 };
}
if (value && typeof value === "object") {
const entries = Object.values(value as Record<string, unknown>);
const enabled = entries.filter(Boolean).length;
return { label: `${enabled}/${entries.length} 已配置`, active: enabled > 0 };
}
return { label: "—", active: false };
}
export function SystemPageClient() {
const [loading, setLoading] = useState(true);
const [health, setHealth] = useState<HealthPayload | null>(null);
@@ -137,12 +166,15 @@ export function SystemPageClient() {
<CardContent>
<dl className="space-y-2 text-sm">
{status?.features
? Object.entries(status.features).map(([k, v]) => (
<div key={k} className="flex justify-between">
<span className="text-slate-400">{k}</span>
<Badge variant={v ? "default" : "secondary"}>{String(v)}</Badge>
</div>
))
? Object.entries(status.features).map(([k, v]) => {
const formatted = formatOpsValue(v);
return (
<div key={k} className="flex justify-between">
<span className="text-slate-400">{k}</span>
<Badge variant={formatted.active ? "default" : "secondary"}>{formatted.label}</Badge>
</div>
);
})
: <span className="text-slate-500"></span>}
</dl>
</CardContent>
@@ -155,12 +187,15 @@ export function SystemPageClient() {
<CardContent>
<dl className="space-y-2 text-sm">
{status?.integrations
? Object.entries(status.integrations).map(([k, v]) => (
<div key={k} className="flex justify-between">
<span className="text-slate-400">{k}</span>
<Badge variant={v ? "default" : "secondary"}>{String(v)}</Badge>
</div>
))
? Object.entries(status.integrations).map(([k, v]) => {
const formatted = formatOpsValue(v);
return (
<div key={k} className="flex justify-between">
<span className="text-slate-400">{k}</span>
<Badge variant={formatted.active ? "default" : "secondary"}>{formatted.label}</Badge>
</div>
);
})
: <span className="text-slate-500"></span>}
</dl>
</CardContent>
@@ -174,7 +209,7 @@ export function SystemPageClient() {
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4 text-sm">
<div className="grid grid-cols-2 gap-4 text-sm sm:grid-cols-5">
<div>
<div className="text-slate-500"></div>
<div className="text-lg font-bold text-white">{cacheAnalysis.total_requests ?? 0}</div>
@@ -187,6 +222,10 @@ export function SystemPageClient() {
<div className="text-slate-500"></div>
<div className="text-lg font-bold text-amber-400">{cacheAnalysis.cache_misses ?? 0}</div>
</div>
<div>
<div className="text-slate-500"></div>
<div className="text-lg font-bold text-blue-500">{cacheAnalysis.force_refresh_requests ?? 0}</div>
</div>
<div>
<div className="text-slate-500"></div>
<div className="text-lg font-bold text-cyan-400">
@@ -194,6 +233,9 @@ export function SystemPageClient() {
</div>
</div>
</div>
<div className="mt-3 text-xs text-slate-500">
</div>
</CardContent>
</Card>
) : null}
@@ -243,7 +285,9 @@ export function SystemPageClient() {
</td>
<td className="px-3 py-2 font-mono text-slate-600">{formatAge(source.age_min)}</td>
<td className="px-3 py-2 font-mono text-slate-600">{source.observed_at || "—"}</td>
<td className="px-3 py-2 text-slate-500">{source.reason || ""}</td>
<td className="px-3 py-2 text-slate-500" title={source.reason || ""}>
{sourceReasonLabel(source.reason)}
</td>
</tr>
))}
</tbody>