feat(awooop): 顯示修復候選升級合約
This commit is contained in:
@@ -1102,6 +1102,25 @@ function laneToneFromCounts(ready: number, total: number, blocked: number): Work
|
||||
return "watching";
|
||||
}
|
||||
|
||||
function promotionValueText(value: unknown): string {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => String(item)).filter(Boolean).slice(0, 3).join(" / ") || "--";
|
||||
}
|
||||
if (value && typeof value === "object") {
|
||||
return Object.values(value as Record<string, unknown>)
|
||||
.map((item) => String(item))
|
||||
.filter(Boolean)
|
||||
.slice(0, 3)
|
||||
.join(" / ") || "--";
|
||||
}
|
||||
return String(value ?? "--");
|
||||
}
|
||||
|
||||
function promotionStatusTone(status: string | null | undefined): WorkStatus {
|
||||
if (String(status ?? "").toLowerCase() === "ready") return "live";
|
||||
return "blocked";
|
||||
}
|
||||
|
||||
function buildAutomationBlockerLanes(
|
||||
telemetry: Telemetry,
|
||||
focusedDraft: RepairCandidateDraftFocus | null
|
||||
@@ -1444,6 +1463,13 @@ const REPAIR_CANDIDATE_DRAFT_BLOCKED_OPERATIONS = [
|
||||
"generic_fallback_repair",
|
||||
] as const;
|
||||
|
||||
const REPAIR_CANDIDATE_PROMOTION_FOCUS_FIELDS = [
|
||||
"route_id",
|
||||
"repair_command_template",
|
||||
"rollback_command_template",
|
||||
"verifier_plan",
|
||||
] as const;
|
||||
|
||||
const statusConfig: Record<WorkStatus, { className: string; icon: typeof Activity }> = {
|
||||
live: {
|
||||
className: "border-[#9bc7a4] bg-[#f0faf2] text-[#17602a]",
|
||||
@@ -4122,6 +4148,53 @@ function RepairCandidateDraftPanel({
|
||||
}) {
|
||||
const t = useTranslations("awooop.workItems.repairCandidateDraft");
|
||||
if (!draft) return null;
|
||||
const promotion = chain?.repair_candidate_promotion;
|
||||
const promotionContract = promotion?.contract ?? null;
|
||||
const promotionAvailable = Boolean(promotion?.available && promotionContract);
|
||||
const promotionReady = toCount(promotionContract?.ready_count);
|
||||
const promotionTotal = toCount(promotionContract?.total_count);
|
||||
const promotionBlocked = toCount(promotionContract?.blocked_count);
|
||||
const promotionPercent = promotionTotal > 0
|
||||
? Math.min(100, Math.round((promotionReady / promotionTotal) * 100))
|
||||
: 0;
|
||||
const promotionFields = promotionContract?.fields ?? [];
|
||||
const promotionFieldMap = new Map(
|
||||
promotionFields
|
||||
.filter((field) => field.field)
|
||||
.map((field) => [String(field.field), field])
|
||||
);
|
||||
const promotionLabels: Record<string, string> = {
|
||||
target_selector: t("promotion.fields.target_selector"),
|
||||
mcp_evidence_refs: t("promotion.fields.mcp_evidence_refs"),
|
||||
route_id: t("promotion.fields.route_id"),
|
||||
repair_command_template: t("promotion.fields.repair_command_template"),
|
||||
rollback_command_template: t("promotion.fields.rollback_command_template"),
|
||||
verifier_plan: t("promotion.fields.verifier_plan"),
|
||||
owner_review: t("promotion.fields.owner_review"),
|
||||
maintenance_window: t("promotion.fields.maintenance_window"),
|
||||
blast_radius: t("promotion.fields.blast_radius"),
|
||||
km_writeback_owner: t("promotion.fields.km_writeback_owner"),
|
||||
playbook_trust_owner: t("promotion.fields.playbook_trust_owner"),
|
||||
};
|
||||
const promotionFocusRows = REPAIR_CANDIDATE_PROMOTION_FOCUS_FIELDS.map((field) => {
|
||||
const row = promotionFieldMap.get(field);
|
||||
return {
|
||||
field,
|
||||
label: promotionLabels[field] ?? row?.label ?? field,
|
||||
status: row?.status ?? "blocked",
|
||||
value: row?.value,
|
||||
source: row?.source ?? "--",
|
||||
};
|
||||
});
|
||||
const promotionBlockedFields = (
|
||||
promotionContract?.blocked_fields?.length
|
||||
? promotionContract.blocked_fields
|
||||
: promotionFields
|
||||
.filter((field) => String(field.status ?? "").toLowerCase() !== "ready")
|
||||
.map((field) => field.field)
|
||||
)
|
||||
.filter((field): field is string => Boolean(field))
|
||||
.slice(0, 8);
|
||||
|
||||
const runsHref = draft.incidentId
|
||||
? `/awooop/runs?project_id=${encodeURIComponent(draft.projectId)}&incident_id=${encodeURIComponent(draft.incidentId)}`
|
||||
@@ -4195,6 +4268,116 @@ function RepairCandidateDraftPanel({
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 border border-[#e0ddd4] bg-[#faf9f3]">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-[#e0ddd4] bg-white px-3 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<ShieldCheck className="h-4 w-4 text-brand-accent" aria-hidden="true" />
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-[#141413]">{t("promotion.title")}</h4>
|
||||
<p className="mt-1 text-[11px] leading-4 text-[#77736a]">
|
||||
{t("promotion.subtitle")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className="border border-[#d9b36f] bg-[#fff7e8] px-2 py-1 font-mono text-xs font-semibold text-[#8a5a08]">
|
||||
{promotionAvailable
|
||||
? t("promotion.progress", {
|
||||
ready: promotionReady,
|
||||
total: promotionTotal,
|
||||
blocked: promotionBlocked,
|
||||
})
|
||||
: t("promotion.missingBadge")}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{promotionAvailable ? (
|
||||
<div className="p-3">
|
||||
<div className="grid gap-px bg-[#e0ddd4] md:grid-cols-4">
|
||||
<div className="min-w-0 bg-white px-3 py-2">
|
||||
<p className="text-xs font-semibold text-[#77736a]">{t("promotion.metrics.readiness")}</p>
|
||||
<p className="mt-1 font-mono text-2xl font-semibold text-[#141413]">{promotionPercent}%</p>
|
||||
</div>
|
||||
<div className="min-w-0 bg-white px-3 py-2">
|
||||
<p className="text-xs font-semibold text-[#77736a]">{t("promotion.metrics.route")}</p>
|
||||
<p className="mt-1 break-all font-mono text-xs font-semibold text-[#141413]">
|
||||
{promotionContract?.route_id ?? "--"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="min-w-0 bg-white px-3 py-2">
|
||||
<p className="text-xs font-semibold text-[#77736a]">{t("promotion.metrics.status")}</p>
|
||||
<p className="mt-1 break-words font-mono text-xs font-semibold text-[#141413]">
|
||||
{promotion?.status ?? promotionContract?.status ?? "--"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="min-w-0 bg-white px-3 py-2">
|
||||
<p className="text-xs font-semibold text-[#77736a]">{t("promotion.metrics.runtime")}</p>
|
||||
<p className="mt-1 font-mono text-xs font-semibold text-[#9f2f25]">
|
||||
{t("promotion.runtimeClosed")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 h-2 border border-[#d8d3c7] bg-white">
|
||||
<div
|
||||
className="h-full bg-[#d97757]"
|
||||
style={{ width: `${promotionPercent}%` }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 grid gap-2 md:grid-cols-4">
|
||||
{promotionFocusRows.map((row) => {
|
||||
const tone = promotionStatusTone(row.status);
|
||||
const Icon = statusConfig[tone].icon;
|
||||
return (
|
||||
<div key={row.field} className="min-w-0 border border-[#e0ddd4] bg-white px-3 py-2">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs font-semibold text-[#141413]">{row.label}</p>
|
||||
<p className="mt-1 break-all font-mono text-[11px] leading-4 text-[#5f5b52]">
|
||||
{promotionValueText(row.value)}
|
||||
</p>
|
||||
</div>
|
||||
<span className={cn("inline-flex shrink-0 items-center gap-1 border px-1.5 py-0.5 font-mono text-[10px] font-semibold", statusConfig[tone].className)}>
|
||||
<Icon className="h-3 w-3" aria-hidden="true" />
|
||||
{row.status}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="mt-3 border border-[#e2a29b] bg-[#fff0ef] px-3 py-2">
|
||||
<p className="text-xs font-semibold text-[#9f2f25]">{t("promotion.blockedTitle")}</p>
|
||||
<div className="mt-2 flex flex-wrap gap-1.5">
|
||||
{promotionBlockedFields.map((field) => (
|
||||
<span key={field} className="border border-[#e2a29b] bg-white px-2 py-0.5 font-mono text-[10px] font-semibold text-[#9f2f25]">
|
||||
{promotionLabels[field] ?? field}
|
||||
</span>
|
||||
))}
|
||||
{promotionBlockedFields.length === 0 ? (
|
||||
<span className="border border-[#d8d3c7] bg-white px-2 py-0.5 font-mono text-[10px] font-semibold text-[#5f5b52]">
|
||||
{t("promotion.noBlockedFields")}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-3">
|
||||
<div className="border border-[#d9b36f] bg-[#fff7e8] px-3 py-2">
|
||||
<p className="text-xs font-semibold text-[#8a5a08]">{t("promotion.missingTitle")}</p>
|
||||
<p className="mt-1 text-[11px] leading-5 text-[#5f5b52]">
|
||||
{t("promotion.missingDetail", {
|
||||
reason: promotion?.reason ?? "repair_candidate_promotion_contract_not_found",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 border border-[#e0ddd4] bg-[#faf9f3] p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Database className="h-4 w-4 text-brand-accent" aria-hidden="true" />
|
||||
|
||||
@@ -314,6 +314,52 @@ export interface AwoooPStatusChain {
|
||||
owner_review_checklist?: string[];
|
||||
forbidden_actions?: string[];
|
||||
} | null;
|
||||
repair_candidate_promotion?: {
|
||||
schema_version?: string | null;
|
||||
status?: string | null;
|
||||
source?: string | null;
|
||||
available?: boolean | null;
|
||||
reason?: string | null;
|
||||
approval_id?: string | null;
|
||||
work_item_id?: string | null;
|
||||
work_item_url?: string | null;
|
||||
summary?: string | null;
|
||||
runtime_execution_authorized?: boolean | null;
|
||||
runtime_write_allowed?: boolean | null;
|
||||
contract?: {
|
||||
schema_version?: string | null;
|
||||
status?: string | null;
|
||||
lane?: string | null;
|
||||
incident_id?: string | null;
|
||||
project_id?: string | null;
|
||||
source_work_item_id?: string | null;
|
||||
source_work_item_url?: string | null;
|
||||
route_id?: string | null;
|
||||
repair_command_template?: string | null;
|
||||
rollback_command_template?: string | null;
|
||||
verifier_plan_template?: string[];
|
||||
ready_count?: number | null;
|
||||
total_count?: number | null;
|
||||
blocked_count?: number | null;
|
||||
ready_fields?: string[];
|
||||
blocked_fields?: string[];
|
||||
fields?: Array<{
|
||||
field?: string | null;
|
||||
label?: string | null;
|
||||
status?: string | null;
|
||||
source?: string | null;
|
||||
value?: unknown;
|
||||
runtime_execution_authorized?: boolean | null;
|
||||
}>;
|
||||
blockers?: string[];
|
||||
runtime_write_allowed?: boolean | null;
|
||||
runtime_execution_authorized?: boolean | null;
|
||||
approval_required_before_execution?: boolean | null;
|
||||
owner_review_required?: boolean | null;
|
||||
forbidden_until_promoted?: string[];
|
||||
next_steps?: string[];
|
||||
} | null;
|
||||
} | null;
|
||||
source_refs?: {
|
||||
inbound_total?: number | null;
|
||||
outbound_total?: number | null;
|
||||
|
||||
Reference in New Issue
Block a user