feat(awooop): expose apply gate closure readiness
This commit is contained in:
@@ -4079,6 +4079,7 @@ def _status_chain_ansible_apply_gate_handoff(
|
||||
*,
|
||||
ansible_dry_run_only: bool,
|
||||
execution_section: dict[str, Any],
|
||||
facts: dict[str, Any],
|
||||
incident_ids: list[str],
|
||||
source_id: str | None,
|
||||
verification: str,
|
||||
@@ -4121,6 +4122,64 @@ def _status_chain_ansible_apply_gate_handoff(
|
||||
latest_returncode = str(ansible.get("latest_returncode") or "")
|
||||
dry_run_passed = latest_status == "success" and latest_returncode in {"", "0"}
|
||||
verifier_ready = str(verification).lower() in {"verified", "success", "healthy"}
|
||||
mcp_evidence_ready = _safe_int(facts.get("mcp_gateway_total")) > 0
|
||||
closure_gates = [
|
||||
{
|
||||
"key": "mcp_evidence",
|
||||
"status": "passed" if mcp_evidence_ready else "warning",
|
||||
"detail": f"mcp={_safe_int(facts.get('mcp_gateway_total'))}",
|
||||
"asset_id": f"mcp-evidence:{safe_source_ref}",
|
||||
},
|
||||
{
|
||||
"key": "dry_run",
|
||||
"status": "passed" if dry_run_passed else "warning",
|
||||
"detail": (
|
||||
f"check={_safe_int(ansible.get('check_mode_total'))}; "
|
||||
f"rc={ansible.get('latest_returncode') if ansible.get('latest_returncode') is not None else '--'}"
|
||||
),
|
||||
"asset_id": f"ansible-check-mode:{catalog_id}",
|
||||
},
|
||||
{
|
||||
"key": "apply_candidate",
|
||||
"status": "passed",
|
||||
"detail": f"catalog={catalog_id}",
|
||||
"asset_id": f"ansible-apply-candidate:{catalog_id}",
|
||||
},
|
||||
{
|
||||
"key": "owner_release",
|
||||
"status": "blocked",
|
||||
"detail": "owner_release_receipt=0",
|
||||
"asset_id": f"owner-release-approval:{safe_source_ref}",
|
||||
},
|
||||
{
|
||||
"key": "controlled_execution",
|
||||
"status": "blocked",
|
||||
"detail": "runtime_gate=closed",
|
||||
"asset_id": f"controlled-execution:{safe_source_ref}",
|
||||
},
|
||||
{
|
||||
"key": "post_apply_verifier",
|
||||
"status": "blocked",
|
||||
"detail": f"verification={verification or 'missing'}",
|
||||
"asset_id": f"verifier-plan:{safe_source_ref}",
|
||||
},
|
||||
{
|
||||
"key": "km_writeback",
|
||||
"status": "blocked",
|
||||
"detail": f"km={_safe_int(facts.get('knowledge_entries'))}",
|
||||
"asset_id": f"km-writeback-candidate:{safe_source_ref}",
|
||||
},
|
||||
{
|
||||
"key": "playbook_trust",
|
||||
"status": "blocked",
|
||||
"detail": "trust_writeback=0",
|
||||
"asset_id": f"playbook-trust-update-candidate:{catalog_id}",
|
||||
},
|
||||
]
|
||||
closure_ready_count = sum(1 for gate in closure_gates if gate["status"] == "passed")
|
||||
closure_total_count = len(closure_gates)
|
||||
closure_blocked_count = sum(1 for gate in closure_gates if gate["status"] == "blocked")
|
||||
closure_completion_percent = int(round((closure_ready_count / closure_total_count) * 100))
|
||||
|
||||
return {
|
||||
"schema_version": "awooop_automation_handoff_v1",
|
||||
@@ -4138,6 +4197,54 @@ def _status_chain_ansible_apply_gate_handoff(
|
||||
"apply_candidate": f"ansible-apply-candidate:{catalog_id}",
|
||||
"verifier": f"verifier-plan:{safe_source_ref}",
|
||||
},
|
||||
"closure_readiness": {
|
||||
"schema_version": "awooop_apply_gate_closure_readiness_v1",
|
||||
"status": "blocked_before_owner_release",
|
||||
"completion_percent": closure_completion_percent,
|
||||
"ready_count": closure_ready_count,
|
||||
"total_count": closure_total_count,
|
||||
"blocked_count": closure_blocked_count,
|
||||
"runtime_execution_authorized": False,
|
||||
"writes_runtime_state": False,
|
||||
"next_action": "review_owner_release_packet_before_apply",
|
||||
"blocked_reason": (
|
||||
"owner_release_controlled_execution_verifier_km_playbook_trust_missing"
|
||||
),
|
||||
"gates": closure_gates,
|
||||
"required_owner_fields": [
|
||||
"owner_approval_receipt",
|
||||
"maintenance_window",
|
||||
"rollback_owner",
|
||||
"blast_radius",
|
||||
"post_apply_verifier_plan",
|
||||
"km_writeback_owner",
|
||||
"playbook_trust_writeback_owner",
|
||||
"evidence_refs",
|
||||
],
|
||||
"readback_assets": [
|
||||
{
|
||||
"key": "owner_execution_rehearsal",
|
||||
"asset_id": (
|
||||
"agent-result-capture-owner-approved-execution-rehearsal:P2-126"
|
||||
),
|
||||
"status": "no_write_rehearsal",
|
||||
},
|
||||
{
|
||||
"key": "final_candidate_readback",
|
||||
"asset_id": (
|
||||
"agent-result-capture-final-release-candidate-readback:P2-133"
|
||||
),
|
||||
"status": "read_only",
|
||||
},
|
||||
{
|
||||
"key": "release_verifier_preflight",
|
||||
"asset_id": (
|
||||
"agent-result-capture-release-verifier-preflight-gate:P2-136"
|
||||
),
|
||||
"status": "read_only",
|
||||
},
|
||||
],
|
||||
},
|
||||
"candidate": {
|
||||
"catalog_id": catalog_id,
|
||||
"check_mode_playbook_path": check_mode_playbook,
|
||||
@@ -5026,6 +5133,7 @@ def _build_awooop_status_chain(
|
||||
automation_handoff = _status_chain_ansible_apply_gate_handoff(
|
||||
ansible_dry_run_only=ansible_dry_run_only,
|
||||
execution_section=execution_section,
|
||||
facts=facts,
|
||||
incident_ids=incident_ids,
|
||||
source_id=source_id,
|
||||
verification=str(verification),
|
||||
|
||||
@@ -1826,6 +1826,42 @@ def test_awooop_status_chain_does_not_treat_ansible_check_mode_as_repair() -> No
|
||||
"blocked",
|
||||
"blocked",
|
||||
]
|
||||
closure = chain["automation_handoff"]["closure_readiness"]
|
||||
assert closure["schema_version"] == "awooop_apply_gate_closure_readiness_v1"
|
||||
assert closure["status"] == "blocked_before_owner_release"
|
||||
assert closure["runtime_execution_authorized"] is False
|
||||
assert closure["writes_runtime_state"] is False
|
||||
assert closure["ready_count"] == 3
|
||||
assert closure["total_count"] == 8
|
||||
assert closure["blocked_count"] == 5
|
||||
assert closure["completion_percent"] == 38
|
||||
assert closure["next_action"] == "review_owner_release_packet_before_apply"
|
||||
assert [gate["key"] for gate in closure["gates"]] == [
|
||||
"mcp_evidence",
|
||||
"dry_run",
|
||||
"apply_candidate",
|
||||
"owner_release",
|
||||
"controlled_execution",
|
||||
"post_apply_verifier",
|
||||
"km_writeback",
|
||||
"playbook_trust",
|
||||
]
|
||||
assert [gate["status"] for gate in closure["gates"]] == [
|
||||
"passed",
|
||||
"passed",
|
||||
"passed",
|
||||
"blocked",
|
||||
"blocked",
|
||||
"blocked",
|
||||
"blocked",
|
||||
"blocked",
|
||||
]
|
||||
assert "owner_approval_receipt" in closure["required_owner_fields"]
|
||||
assert "post_apply_verifier_plan" in closure["required_owner_fields"]
|
||||
assert "km_writeback_owner" in closure["required_owner_fields"]
|
||||
assert closure["readback_assets"][0]["asset_id"] == (
|
||||
"agent-result-capture-owner-approved-execution-rehearsal:P2-126"
|
||||
)
|
||||
assert chain["execution"]["ansible"]["check_mode_total"] == 1
|
||||
assert chain["execution"]["ansible"]["apply_total"] == 0
|
||||
assert chain["execution"]["ansible"]["applied"] is False
|
||||
|
||||
@@ -10099,6 +10099,7 @@
|
||||
},
|
||||
"nextActions": {
|
||||
"openApplyGateWorkItem": "開啟 apply gate 工作項,審查 Verifier 與 KM 回寫",
|
||||
"reviewOwnerReleasePacket": "審查 owner 放行包、執行窗口、rollback 與回寫責任",
|
||||
"manualReviewNoActionDecision": "人工判斷是否接手或關閉事件",
|
||||
"ownerReviewRepairCandidateDraft": "審查修復候選草案、rollback 與 verifier",
|
||||
"collectRepairEvidence": "補齊修復證據或建立專屬 PlayBook",
|
||||
@@ -10137,6 +10138,11 @@
|
||||
"dryRunAsset": "乾跑資產",
|
||||
"applyAsset": "套用候選資產",
|
||||
"verifierAsset": "Verifier 資產",
|
||||
"closureTitle": "批准後自動化閉環準備度",
|
||||
"closureProgress": "{ready}/{total} ready · {percent}%",
|
||||
"closureBlockedReason": "阻擋:{reason}",
|
||||
"closureOwnerFields": "Owner 放行欄位",
|
||||
"closureReadbackAssets": "只讀回查資產",
|
||||
"checklistTitle": "Owner 審查清單",
|
||||
"forbiddenTitle": "禁止動作",
|
||||
"gates": {
|
||||
@@ -10144,6 +10150,26 @@
|
||||
"applyGate": "套用審查",
|
||||
"verifier": "驗證回寫"
|
||||
},
|
||||
"closureGates": {
|
||||
"mcpEvidence": "MCP 證據",
|
||||
"dryRun": "乾跑",
|
||||
"applyCandidate": "Apply 候選",
|
||||
"ownerRelease": "Owner 放行",
|
||||
"controlledExecution": "受控執行",
|
||||
"postApplyVerifier": "套用後驗證",
|
||||
"kmWriteback": "KM 回寫",
|
||||
"playbookTrust": "PlayBook 信任"
|
||||
},
|
||||
"closureAssets": {
|
||||
"ownerExecutionRehearsal": "Owner 批准執行演練",
|
||||
"finalCandidateReadback": "最終候選回查",
|
||||
"releaseVerifierPreflight": "Verifier 放行前檢查"
|
||||
},
|
||||
"closureStatuses": {
|
||||
"blockedBeforeOwnerRelease": "Owner 放行前受阻",
|
||||
"noWriteRehearsal": "無寫入演練",
|
||||
"readOnly": "只讀"
|
||||
},
|
||||
"statuses": {
|
||||
"passed": "已通過",
|
||||
"blocked": "阻擋",
|
||||
|
||||
@@ -10099,6 +10099,7 @@
|
||||
},
|
||||
"nextActions": {
|
||||
"openApplyGateWorkItem": "開啟 apply gate 工作項,審查 Verifier 與 KM 回寫",
|
||||
"reviewOwnerReleasePacket": "審查 owner 放行包、執行窗口、rollback 與回寫責任",
|
||||
"manualReviewNoActionDecision": "人工判斷是否接手或關閉事件",
|
||||
"ownerReviewRepairCandidateDraft": "審查修復候選草案、rollback 與 verifier",
|
||||
"collectRepairEvidence": "補齊修復證據或建立專屬 PlayBook",
|
||||
@@ -10137,6 +10138,11 @@
|
||||
"dryRunAsset": "乾跑資產",
|
||||
"applyAsset": "套用候選資產",
|
||||
"verifierAsset": "Verifier 資產",
|
||||
"closureTitle": "批准後自動化閉環準備度",
|
||||
"closureProgress": "{ready}/{total} ready · {percent}%",
|
||||
"closureBlockedReason": "阻擋:{reason}",
|
||||
"closureOwnerFields": "Owner 放行欄位",
|
||||
"closureReadbackAssets": "只讀回查資產",
|
||||
"checklistTitle": "Owner 審查清單",
|
||||
"forbiddenTitle": "禁止動作",
|
||||
"gates": {
|
||||
@@ -10144,6 +10150,26 @@
|
||||
"applyGate": "套用審查",
|
||||
"verifier": "驗證回寫"
|
||||
},
|
||||
"closureGates": {
|
||||
"mcpEvidence": "MCP 證據",
|
||||
"dryRun": "乾跑",
|
||||
"applyCandidate": "Apply 候選",
|
||||
"ownerRelease": "Owner 放行",
|
||||
"controlledExecution": "受控執行",
|
||||
"postApplyVerifier": "套用後驗證",
|
||||
"kmWriteback": "KM 回寫",
|
||||
"playbookTrust": "PlayBook 信任"
|
||||
},
|
||||
"closureAssets": {
|
||||
"ownerExecutionRehearsal": "Owner 批准執行演練",
|
||||
"finalCandidateReadback": "最終候選回查",
|
||||
"releaseVerifierPreflight": "Verifier 放行前檢查"
|
||||
},
|
||||
"closureStatuses": {
|
||||
"blockedBeforeOwnerRelease": "Owner 放行前受阻",
|
||||
"noWriteRehearsal": "無寫入演練",
|
||||
"readOnly": "只讀"
|
||||
},
|
||||
"statuses": {
|
||||
"passed": "已通過",
|
||||
"blocked": "阻擋",
|
||||
|
||||
@@ -143,6 +143,30 @@ export interface AwoooPStatusChain {
|
||||
apply_candidate?: string | null;
|
||||
verifier?: string | null;
|
||||
};
|
||||
closure_readiness?: {
|
||||
schema_version?: string;
|
||||
status?: string | null;
|
||||
completion_percent?: number | null;
|
||||
ready_count?: number | null;
|
||||
total_count?: number | null;
|
||||
blocked_count?: number | null;
|
||||
runtime_execution_authorized?: boolean | null;
|
||||
writes_runtime_state?: boolean | null;
|
||||
next_action?: string | null;
|
||||
blocked_reason?: string | null;
|
||||
gates?: Array<{
|
||||
key?: string | null;
|
||||
status?: string | null;
|
||||
detail?: string | null;
|
||||
asset_id?: string | null;
|
||||
}>;
|
||||
required_owner_fields?: string[];
|
||||
readback_assets?: Array<{
|
||||
key?: string | null;
|
||||
asset_id?: string | null;
|
||||
status?: string | null;
|
||||
}>;
|
||||
};
|
||||
candidate?: {
|
||||
catalog_id?: string | null;
|
||||
check_mode_playbook_path?: string | null;
|
||||
@@ -272,6 +296,7 @@ export function AwoooPStatusChainPanel({
|
||||
const outcome = chain?.operator_outcome;
|
||||
const outcomeExecution = outcome?.execution_result;
|
||||
const automationHandoff = chain?.automation_handoff;
|
||||
const closureReadiness = automationHandoff?.closure_readiness;
|
||||
const blockers = chain?.blockers ?? [];
|
||||
const sourceCorrelation = chain?.source_refs?.correlation;
|
||||
const nextActionLabel = (value: unknown) => {
|
||||
@@ -281,6 +306,7 @@ export function AwoooPStatusChainPanel({
|
||||
owner_review_apply_gate_or_create_verifier_plan: t("nextActions.openApplyGateWorkItem"),
|
||||
manual_review_no_action_decision: t("nextActions.manualReviewNoActionDecision"),
|
||||
owner_review_repair_candidate_draft: t("nextActions.ownerReviewRepairCandidateDraft"),
|
||||
review_owner_release_packet_before_apply: t("nextActions.reviewOwnerReleasePacket"),
|
||||
manual_review_or_collect_repair_evidence: t("nextActions.collectRepairEvidence"),
|
||||
manual_investigation: t("nextActions.manualInvestigation"),
|
||||
run_or_review_post_execution_verification: t("nextActions.reviewVerifier"),
|
||||
@@ -437,6 +463,9 @@ export function AwoooPStatusChainPanel({
|
||||
const executionTotal = execution.operation_total ?? 0;
|
||||
const handoffCandidate = automationHandoff?.candidate;
|
||||
const handoffAssets = automationHandoff?.asset_ids;
|
||||
const closureGates = closureReadiness?.gates ?? [];
|
||||
const closureReadbackAssets = closureReadiness?.readback_assets ?? [];
|
||||
const closureOwnerFields = closureReadiness?.required_owner_fields ?? [];
|
||||
const ownerReviewChecklist = automationHandoff?.owner_review_checklist ?? [];
|
||||
const forbiddenActions = automationHandoff?.forbidden_actions ?? [];
|
||||
const sourceToolchainTone: SourceFlowTone = sourceCorrelation
|
||||
@@ -548,6 +577,35 @@ export function AwoooPStatusChainPanel({
|
||||
{ key: "applyAsset", label: t("applyGate.applyAsset"), value: handoffAssets?.apply_candidate },
|
||||
{ key: "verifierAsset", label: t("applyGate.verifierAsset"), value: handoffAssets?.verifier },
|
||||
];
|
||||
const closureGateLabel = (key: string | null | undefined) => {
|
||||
const labels: Record<string, string> = {
|
||||
mcp_evidence: t("applyGate.closureGates.mcpEvidence"),
|
||||
dry_run: t("applyGate.closureGates.dryRun"),
|
||||
apply_candidate: t("applyGate.closureGates.applyCandidate"),
|
||||
owner_release: t("applyGate.closureGates.ownerRelease"),
|
||||
controlled_execution: t("applyGate.closureGates.controlledExecution"),
|
||||
post_apply_verifier: t("applyGate.closureGates.postApplyVerifier"),
|
||||
km_writeback: t("applyGate.closureGates.kmWriteback"),
|
||||
playbook_trust: t("applyGate.closureGates.playbookTrust"),
|
||||
};
|
||||
return labels[String(key ?? "")] ?? valueOrEmpty(key, emptyLabel);
|
||||
};
|
||||
const closureAssetLabel = (key: string | null | undefined) => {
|
||||
const labels: Record<string, string> = {
|
||||
owner_execution_rehearsal: t("applyGate.closureAssets.ownerExecutionRehearsal"),
|
||||
final_candidate_readback: t("applyGate.closureAssets.finalCandidateReadback"),
|
||||
release_verifier_preflight: t("applyGate.closureAssets.releaseVerifierPreflight"),
|
||||
};
|
||||
return labels[String(key ?? "")] ?? valueOrEmpty(key, emptyLabel);
|
||||
};
|
||||
const closureStatusLabel = (status: string | null | undefined) => {
|
||||
const labels: Record<string, string> = {
|
||||
blocked_before_owner_release: t("applyGate.closureStatuses.blockedBeforeOwnerRelease"),
|
||||
no_write_rehearsal: t("applyGate.closureStatuses.noWriteRehearsal"),
|
||||
read_only: t("applyGate.closureStatuses.readOnly"),
|
||||
};
|
||||
return labels[String(status ?? "")] ?? handoffStatusLabel(status);
|
||||
};
|
||||
const handoffWorkItemHref = automationHandoff?.work_item_id
|
||||
? `/awooop/work-items?project_id=awoooi&work_item_id=${encodeURIComponent(automationHandoff.work_item_id)}${automationHandoff.source_id ? `&incident_id=${encodeURIComponent(automationHandoff.source_id)}` : ""}`
|
||||
: null;
|
||||
@@ -807,6 +865,100 @@ export function AwoooPStatusChainPanel({
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{closureReadiness && (
|
||||
<div>
|
||||
<div className="border-t border-[#e0ddd4] bg-[#f5f8f2] px-4 py-2">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs font-semibold text-[#141413]">{t("applyGate.closureTitle")}</p>
|
||||
<p className="mt-1 truncate text-xs text-[#5f5b52]" title={valueOrEmpty(closureReadiness.blocked_reason, emptyLabel)}>
|
||||
{t("applyGate.closureBlockedReason", {
|
||||
reason: valueOrEmpty(closureReadiness.blocked_reason, emptyLabel),
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="border border-[#9fcbab] bg-[#f1fbf2] px-2 py-0.5 font-mono text-[11px] font-semibold text-[#17602a]">
|
||||
{t("applyGate.closureProgress", {
|
||||
ready: closureReadiness.ready_count ?? 0,
|
||||
total: closureReadiness.total_count ?? closureGates.length,
|
||||
percent: closureReadiness.completion_percent ?? 0,
|
||||
})}
|
||||
</span>
|
||||
<span className="border border-[#e2a29b] bg-[#fff0ef] px-2 py-0.5 font-mono text-[11px] font-semibold text-[#9f2f25]">
|
||||
{closureStatusLabel(closureReadiness.status)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-px bg-[#e0ddd4] md:grid-cols-4">
|
||||
{closureGates.map((gate) => (
|
||||
<div key={`${gate.key}-${gate.status}`} className="min-w-0 bg-white px-4 py-3">
|
||||
<div className="flex min-w-0 items-start gap-3">
|
||||
<span className={cn(
|
||||
"flex h-8 w-8 shrink-0 items-center justify-center border",
|
||||
sourceFlowToneClass(handoffTone(gate.status))
|
||||
)}>
|
||||
<CheckCircle2 className="h-4 w-4" aria-hidden="true" />
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs font-semibold text-[#77736a]">{closureGateLabel(gate.key)}</p>
|
||||
<p className="mt-1 truncate font-mono text-sm font-semibold text-[#141413]" title={closureStatusLabel(gate.status)}>
|
||||
{closureStatusLabel(gate.status)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-2 truncate font-mono text-xs text-[#5f5b52]" title={valueOrEmpty(gate.detail, emptyLabel)}>
|
||||
{valueOrEmpty(gate.detail, emptyLabel)}
|
||||
</p>
|
||||
<p className="mt-1 truncate font-mono text-[11px] text-[#77736a]" title={valueOrEmpty(gate.asset_id, emptyLabel)}>
|
||||
{valueOrEmpty(gate.asset_id, emptyLabel)}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="grid gap-px bg-[#e0ddd4] lg:grid-cols-2">
|
||||
<div className="min-w-0 bg-white px-4 py-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<p className="text-xs font-semibold text-[#77736a]">{t("applyGate.closureOwnerFields")}</p>
|
||||
<span className="border border-[#d8d3c7] bg-[#faf9f3] px-2 py-0.5 font-mono text-[11px] text-[#5f5b52]">
|
||||
{closureOwnerFields.length}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-3 grid gap-2 sm:grid-cols-2">
|
||||
{(closureOwnerFields.length ? closureOwnerFields : [emptyLabel]).map((item, index) => (
|
||||
<span key={`${item}-${index}`} className="min-w-0 border border-[#ece8dd] bg-[#faf9f3] px-2 py-1 font-mono text-[11px] text-[#141413]">
|
||||
{item}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-w-0 bg-white px-4 py-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<p className="text-xs font-semibold text-[#77736a]">{t("applyGate.closureReadbackAssets")}</p>
|
||||
<span className="border border-[#d8d3c7] bg-[#faf9f3] px-2 py-0.5 font-mono text-[11px] text-[#5f5b52]">
|
||||
{closureReadbackAssets.length}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-3 space-y-2">
|
||||
{(closureReadbackAssets.length ? closureReadbackAssets : [{ key: emptyLabel, asset_id: emptyLabel, status: emptyLabel }]).map((item, index) => (
|
||||
<div key={`${item.key}-${index}`} className="min-w-0 border border-[#ece8dd] bg-[#faf9f3] px-2 py-2">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<p className="text-xs font-semibold text-[#141413]">{closureAssetLabel(item.key)}</p>
|
||||
<span className="border border-[#d8d3c7] bg-white px-2 py-0.5 font-mono text-[11px] text-[#5f5b52]">
|
||||
{closureStatusLabel(item.status)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 break-all font-mono text-[11px] text-[#5f5b52]" title={valueOrEmpty(item.asset_id, emptyLabel)}>
|
||||
{valueOrEmpty(item.asset_id, emptyLabel)}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="grid gap-px bg-[#e0ddd4] md:grid-cols-3">
|
||||
<div className="min-w-0 bg-white px-4 py-3">
|
||||
<p className="text-xs font-semibold text-[#77736a]">{t("applyGate.workItem")}</p>
|
||||
|
||||
Reference in New Issue
Block a user