fix(awooop): make approvals mobile queue scannable
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 57s
CD Pipeline / build-and-deploy (push) Successful in 6m36s
CD Pipeline / post-deploy-checks (push) Has been cancelled

This commit is contained in:
Your Name
2026-07-03 10:08:55 +08:00
parent b91c6e8a40
commit 48042cbdde
2 changed files with 190 additions and 13 deletions

View File

@@ -633,6 +633,52 @@ function LogAutomationMainlinePanel({
);
}
function legacyRiskClass(risk: string) {
return risk === "critical"
? "border-[#e2a29b] bg-[#fff0ef] text-[#9f2f25]"
: "border-[#d9b36f] bg-[#fff7e8] text-[#8a5a08]";
}
function LegacyApprovalMobileCard({ approval }: { approval: LegacyApproval }) {
const tLegacy = useTranslations("awooop.approvals.legacyHitl");
const risk = approval.risk_level?.toLowerCase() ?? "unknown";
return (
<article className="border border-[#ead9b4] bg-white p-4">
<div className="flex min-w-0 items-start justify-between gap-3">
<div className="min-w-0">
<span className={cn("inline-flex border px-2 py-0.5 text-xs font-semibold", legacyRiskClass(risk))}>
{risk}
</span>
<p className="mt-2 break-words text-sm font-semibold text-[#141413]">{approval.action}</p>
</div>
<span className="shrink-0 font-mono text-xs text-[#77736a]">{approval.id.slice(0, 8)}</span>
</div>
<p className="mt-2 line-clamp-3 break-words text-xs leading-5 text-[#5f5b52]">
{approval.description || "--"}
</p>
<div className="mt-4 grid gap-2 border-t border-[#eee9dd] pt-3 text-xs leading-5 text-[#5f5b52]">
<div className="grid grid-cols-[5rem_minmax(0,1fr)] gap-3">
<span className="font-semibold text-[#77736a]">{tLegacy("columns.incident")}</span>
<span className="min-w-0 break-all font-mono">{approval.incident_id || "--"}</span>
</div>
<div className="grid grid-cols-[5rem_minmax(0,1fr)] gap-3">
<span className="font-semibold text-[#77736a]">{tLegacy("columns.source")}</span>
<span className="min-w-0 break-words">
{approval.telegram_message_id
? tLegacy("telegramRef", { id: approval.telegram_message_id })
: tLegacy("noTelegram")}
</span>
</div>
<div className="grid grid-cols-[5rem_minmax(0,1fr)] gap-3">
<span className="font-semibold text-[#77736a]">{tLegacy("columns.created")}</span>
<span>{formatLocalTime(approval.created_at)}</span>
</div>
</div>
</article>
);
}
function LegacyHitlBacklogPanel({
approvals,
loading,
@@ -694,7 +740,32 @@ function LegacyHitlBacklogPanel({
)}
{(loading || approvals.length > 0) && (
<div className="overflow-x-auto bg-white">
<div className="grid gap-3 bg-white p-3 md:hidden" data-testid="legacy-hitl-mobile-list">
{loading ? (
Array.from({ length: 3 }).map((_, row) => (
<div key={row} className="border border-[#ead9b4] bg-white p-4">
<div className="h-5 w-24 animate-pulse rounded bg-[#f0ede5]" />
<div className="mt-4 grid gap-3">
<div className="h-4 w-full animate-pulse rounded bg-[#f0ede5]" />
<div className="h-4 w-3/4 animate-pulse rounded bg-[#f0ede5]" />
</div>
</div>
))
) : (
approvals.slice(0, 8).map((approval) => (
<LegacyApprovalMobileCard key={approval.id} approval={approval} />
))
)}
{!loading && approvals.length > 8 && (
<div className="border border-[#e0ddd4] bg-[#faf9f3] px-4 py-3 text-xs text-[#5f5b52]">
{tLegacy("moreRows", { count: approvals.length - 8 })}
</div>
)}
</div>
)}
{(loading || approvals.length > 0) && (
<div className="hidden overflow-x-auto bg-white md:block">
<table className="w-full" aria-label={tLegacy("tableLabel")}>
<thead>
<tr className="border-b border-[#e0ddd4] bg-[#faf9f3]">
@@ -719,14 +790,10 @@ function LegacyHitlBacklogPanel({
) : (
approvals.slice(0, 8).map((approval) => {
const risk = approval.risk_level?.toLowerCase() ?? "unknown";
const riskClass =
risk === "critical"
? "border-[#e2a29b] bg-[#fff0ef] text-[#9f2f25]"
: "border-[#d9b36f] bg-[#fff7e8] text-[#8a5a08]";
return (
<tr key={approval.id} className="border-b border-[#f0ede5]">
<td className="px-4 py-3 align-top">
<span className={cn("inline-flex border px-2 py-0.5 text-xs font-semibold", riskClass)}>
<span className={cn("inline-flex border px-2 py-0.5 text-xs font-semibold", legacyRiskClass(risk))}>
{risk}
</span>
</td>
@@ -1354,6 +1421,95 @@ function ApprovalRow({ approval }: { approval: Approval }) {
);
}
function ApprovalMobileCard({ approval }: { approval: Approval }) {
const t = useTranslations("awooop.approvals");
const tEvidence = useTranslations("awooop.listEvidence");
const tStatusChain = useTranslations("awooop.statusChain");
const formattedDate = approval.created_at
? new Date(approval.created_at).toLocaleDateString("zh-TW", {
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
})
: "--";
const remainingMs = getRemainingMs(approval.timeout_at);
const isCritical = remainingMs !== null && remainingMs <= 5 * 60 * 1000;
const isGate5Projection = approval.trigger_type === "adr100_runtime_replay_gate5";
return (
<article
className={cn(
"border border-[#e0ddd4] bg-white p-4 shadow-[0_1px_4px_rgba(0,0,0,0.05)]",
isCritical && "border-[#e2a29b] bg-[#fff7f6]"
)}
>
<div className="flex min-w-0 items-start justify-between gap-3">
<div className="min-w-0">
<p className="text-xs font-semibold text-[#77736a]">{t("columns.runId")}</p>
<Link
href={`/awooop/approvals/${approval.run_id}`}
className="mt-1 inline-flex max-w-full items-center gap-1.5 border border-brand-accent/20 bg-brand-accent/10 px-2 py-1 font-mono text-xs font-semibold text-brand-accent transition-colors hover:bg-brand-accent/20"
aria-label={`開啟審批 ${approval.run_id}`}
>
<span className="truncate">{approval.run_id.slice(0, 12)}</span>
<ArrowRight className="h-3 w-3 shrink-0" aria-hidden="true" />
</Link>
</div>
<DecisionPostureBadge />
</div>
<div className="mt-4 grid gap-3 border-t border-[#eee9dd] pt-3 text-xs leading-5 text-[#5f5b52]">
<div className="grid grid-cols-[6rem_minmax(0,1fr)] gap-3">
<span className="font-semibold text-[#77736a]">{t("columns.projectId")}</span>
<span className="min-w-0 break-words font-semibold text-[#141413]">
{publicProjectText(approval.project_id)}
</span>
</div>
<div className="grid grid-cols-[6rem_minmax(0,1fr)] gap-3">
<span className="font-semibold text-[#77736a]">{t("columns.agent")}</span>
<div className="min-w-0">
<span className="break-words font-semibold text-[#141413]">
{publicAgentText(approval.agent_id)}
</span>
{isGate5Projection && <Gate5ProjectionBadge />}
</div>
</div>
<div className="grid grid-cols-[6rem_minmax(0,1fr)] gap-3">
<span className="font-semibold text-[#77736a]">{t("columns.created")}</span>
<span className="font-mono text-[#141413]">{formattedDate}</span>
</div>
<div className="grid grid-cols-[6rem_minmax(0,1fr)] gap-3">
<span className="font-semibold text-[#77736a]">{t("columns.remaining")}</span>
<TimeoutCell timeoutAt={approval.timeout_at} />
</div>
</div>
<div className="mt-4 grid gap-3 border-t border-[#eee9dd] pt-3">
<div className="min-w-0 overflow-hidden">
<p className="mb-2 text-xs font-semibold text-[#77736a]">{tEvidence("column")}</p>
<RemediationEvidenceCell summary={approval.remediation_summary} />
</div>
<div className="min-w-0 overflow-hidden">
<p className="mb-2 text-xs font-semibold text-[#77736a]">{tEvidence("sourceFlow.column")}</p>
<ApprovalSourceFlowCell chain={approval.awooop_status_chain} />
</div>
<div className="min-w-0 overflow-hidden">
<p className="mb-2 text-xs font-semibold text-[#77736a]">{t("assetLedger.column")}</p>
<AwoooPAutomationAssetLedger
chain={approval.awooop_status_chain}
remediationSummary={approval.remediation_summary}
/>
</div>
<div className="min-w-0 overflow-hidden">
<p className="mb-2 text-xs font-semibold text-[#77736a]">{tStatusChain("title")}</p>
<AwoooPStatusChainPanel chain={approval.awooop_status_chain} compact />
</div>
</div>
</article>
);
}
function SecurityOwnerResponseGatePanel() {
const t = useTranslations("awooop.approvals.securityOwnerResponseGate");
const metrics = [
@@ -2293,7 +2449,28 @@ export default function ApprovalsPage() {
{/* Table */}
{(loading || approvals.length > 0) && (
<div className="overflow-hidden border border-[#e0ddd4] bg-white shadow-[0_1px_4px_rgba(0,0,0,0.05)]">
<div className="grid gap-3 md:hidden" data-testid="approvals-mobile-list">
{loading ? (
Array.from({ length: 4 }).map((_, i) => (
<div key={i} className="border border-[#e0ddd4] bg-white p-4">
<div className="h-5 w-32 animate-pulse rounded bg-muted" />
<div className="mt-4 grid gap-3">
<div className="h-4 w-full animate-pulse rounded bg-muted" />
<div className="h-4 w-3/4 animate-pulse rounded bg-muted" />
<div className="h-16 w-full animate-pulse rounded bg-muted" />
</div>
</div>
))
) : (
approvals.map((approval) => (
<ApprovalMobileCard key={approval.run_id} approval={approval} />
))
)}
</div>
)}
{(loading || approvals.length > 0) && (
<div className="hidden overflow-hidden border border-[#e0ddd4] bg-white shadow-[0_1px_4px_rgba(0,0,0,0.05)] md:block">
<div className="overflow-x-auto">
<table className="w-full" role="table" aria-label={t("page.title")}>
<thead>

View File

@@ -1550,7 +1550,7 @@ export function AutonomousRuntimeReceiptPanel({
<Icon className="h-4 w-4" aria-hidden="true" />
</span>
</div>
<p className="mt-1 line-clamp-2 text-xs leading-5 text-[#5f5b52]">
<p className="mt-1 line-clamp-2 break-words text-xs leading-5 text-[#5f5b52]">
{card.detail}
</p>
</div>
@@ -1563,7 +1563,7 @@ export function AutonomousRuntimeReceiptPanel({
className="border-t border-[#e0ddd4] bg-white"
>
<div className="flex flex-wrap items-start justify-between gap-3 px-4 py-3">
<div className="min-w-0">
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<Bell className="h-4 w-4 text-[#87867f]" aria-hidden="true" />
<h4 className="text-sm font-semibold text-[#141413]">{t("monitoringCoverage.title")}</h4>
@@ -1572,7 +1572,7 @@ export function AutonomousRuntimeReceiptPanel({
{t("monitoringCoverage.subtitle")}
</p>
</div>
<span className={cn("inline-flex border px-2 py-0.5 text-xs font-semibold", toneClass(
<span className={cn("inline-flex max-w-full min-w-0 break-all border px-2 py-0.5 text-left text-xs font-semibold", toneClass(
!hasTelegramCoverageReadback
? "neutral"
: telegramMonitoringCoverageReady ? "ok" : "warn"
@@ -1598,7 +1598,7 @@ export function AutonomousRuntimeReceiptPanel({
<Icon className="h-4 w-4" aria-hidden="true" />
</span>
</div>
<p className="mt-1 line-clamp-2 text-xs leading-5 text-[#5f5b52]">
<p className="mt-1 line-clamp-2 break-words text-xs leading-5 text-[#5f5b52]">
{card.detail}
</p>
</div>
@@ -1624,7 +1624,7 @@ export function AutonomousRuntimeReceiptPanel({
{telegramCoveragePipeline.slice(0, 7).map((stage) => (
<div
key={stage.stage_id ?? stage.next_action ?? "stage"}
className="flex items-center justify-between gap-3 border border-[#eee9dd] px-2 py-1.5"
className="flex min-w-0 items-center justify-between gap-3 border border-[#eee9dd] px-2 py-1.5"
>
<div className="min-w-0 truncate text-xs font-semibold text-[#141413]">
{stage.stage_id ?? "--"}
@@ -1656,7 +1656,7 @@ export function AutonomousRuntimeReceiptPanel({
className="grid gap-1 border border-[#eee9dd] px-2 py-1.5"
>
<div className="flex min-w-0 items-center justify-between gap-2">
<span className="truncate text-xs font-semibold text-[#141413]">
<span className="min-w-0 truncate text-xs font-semibold text-[#141413]">
{item.controlled_next_action ?? item.blocker ?? "--"}
</span>
<span className="font-mono text-xs text-[#77736a]">