798 lines
44 KiB
JavaScript
798 lines
44 KiB
JavaScript
(function () {
|
||
"use strict";
|
||
|
||
const API_ROOT = "/api/self-repair";
|
||
const VIEW_KEY = "cognitive-agent.self-repair-view";
|
||
const MODE_KEY = "cognitive-agent.self-repair-execution";
|
||
const STATE_TEXT = {
|
||
OK: "正常",
|
||
WARN: "告警",
|
||
FAULT: "故障",
|
||
UNKNOWN: "未检测",
|
||
UNMONITORED: "关联待测",
|
||
NOT_USED: "未启用",
|
||
IDLE: "检测中",
|
||
};
|
||
const EVENT_META = {
|
||
BASELINE: { kind: "system", label: "运行" },
|
||
INJECTED: { kind: "fault", label: "注入" },
|
||
DIAGNOSED: { kind: "diagnosis", label: "诊断" },
|
||
REPAIRING: { kind: "repair", label: "修复" },
|
||
VERIFYING: { kind: "repair", label: "复测" },
|
||
PASSED: { kind: "success", label: "恢复" },
|
||
FAILED: { kind: "fault", label: "失败" },
|
||
};
|
||
const FALLBACK_SCENARIOS = [
|
||
["FI-FIELD-01", "位置标定丢失", "座椅移动 / 位置记忆"],
|
||
["FI-FIELD-02", "位置记录CRC回退", "四轴位置 / SMV8持久化"],
|
||
["FI-FIELD-03", "瞬态过载误判", "电机移动 / 防夹保护"],
|
||
["FI-FIELD-04", "热管理唤醒失败", "通风 / 加热"],
|
||
["FI-FIELD-05", "风扇关闭异常", "座椅通风"],
|
||
["FI-FIELD-06", "WDT复位后失能", "座椅控制器 / 多功能协同"],
|
||
].map(([id, shortTitle, system]) => ({
|
||
id,
|
||
short_title: shortTitle,
|
||
system,
|
||
normal_task: "等待连接后加载正常任务",
|
||
fault_reaction: "等待连接后加载故障停止状态",
|
||
diagnosis: "等待连接后加载 AI 诊断",
|
||
repair: "等待连接后加载恢复策略",
|
||
recovery_reaction: "等待连接后加载恢复结果",
|
||
field_basis: "自修复服务连接后显示实时用例说明",
|
||
}));
|
||
|
||
function fallbackState(message = "正在连接自修复服务") {
|
||
return {
|
||
scenarios: FALLBACK_SCENARIOS,
|
||
healing: {
|
||
phase: "IDLE",
|
||
phase_text: "等待服务连接",
|
||
elapsed_s: 0,
|
||
case_id: null,
|
||
telemetry: {},
|
||
events: [],
|
||
},
|
||
batch: { active: false, completed: 0, total: 6, passed: 0, failed: 0, results: [] },
|
||
runtime: {
|
||
active: false,
|
||
execution_mode: "shadow",
|
||
started_epoch: null,
|
||
stopped_epoch: null,
|
||
elapsed_s: 0,
|
||
healthy_elapsed_s: 0,
|
||
motion_profile: "四电机协同自由运动",
|
||
cycle_count: 0,
|
||
injected_count: 0,
|
||
repaired_count: 0,
|
||
events: [],
|
||
},
|
||
bridge: { online: false, protocol_online: false, bus_traffic: false },
|
||
bridge_manager: { state: "STARTING", message },
|
||
integration: { can_runtime_configured: false, firmware_tools_configured: false },
|
||
hardware: { groups: [], connectors: {} },
|
||
hardware_session: { active: false, events: [] },
|
||
bench_twin: {},
|
||
};
|
||
}
|
||
|
||
const $ = (selector, root = document) => root.querySelector(selector);
|
||
const $$ = (selector, root = document) => [...root.querySelectorAll(selector)];
|
||
const esc = (value) => String(value == null ? "" : value)
|
||
.replaceAll("&", "&")
|
||
.replaceAll("<", "<")
|
||
.replaceAll(">", ">")
|
||
.replaceAll('"', """);
|
||
|
||
let latest = fallbackState();
|
||
let selectedCaseId = null;
|
||
let selectedPinId = "JC1-1";
|
||
let activeView = sessionStorage.getItem(VIEW_KEY) === "hardware"
|
||
? "hardware"
|
||
: "software";
|
||
sessionStorage.removeItem(MODE_KEY);
|
||
let executionMode = "shadow";
|
||
let polling = false;
|
||
let bootstrapRuntimeReset = false;
|
||
let toastTimer = 0;
|
||
let confirmationResolve = null;
|
||
let eventFilter = "all";
|
||
|
||
function isAdmin() {
|
||
return window.CognitiveAgentConsoleV2?.getRole() === "admin"
|
||
|| document.body.dataset.demoRole === "admin";
|
||
}
|
||
|
||
function isActive() {
|
||
return isAdmin() && document.body.dataset.module === "self-repair";
|
||
}
|
||
|
||
function toast(message, error = false) {
|
||
const node = $("#toast");
|
||
if (!node) return;
|
||
node.textContent = message;
|
||
node.className = error ? "toast show error" : "toast show";
|
||
window.clearTimeout(toastTimer);
|
||
toastTimer = window.setTimeout(() => { node.className = "toast"; }, 3600);
|
||
}
|
||
|
||
function buildShell() {
|
||
const sidebar = $(".v2-sidebar");
|
||
const mainPanel = $(".main-panel");
|
||
const moduleSwitcher = $("#v2ModuleSwitcher");
|
||
if (!sidebar || !mainPanel || !moduleSwitcher || $("#v2SelfRepair")) return false;
|
||
|
||
const repairSidebar = document.createElement("section");
|
||
repairSidebar.className = "v2-sidebar-section v2-repair-sidebar";
|
||
repairSidebar.innerHTML = `
|
||
<div class="v2-section-heading"><span>SELF-REPAIR DESK</span><h2>控制台</h2></div>
|
||
<span class="v2-field-caption">修复对象</span>
|
||
<div class="v2-repair-view-switch" role="group" aria-label="修复对象">
|
||
<button type="button" data-repair-view="software"><i data-lucide="shield-check"></i><span><strong>软件修复</strong><small>注入 · 判断 · 修复 · 验证</small></span></button>
|
||
<button type="button" data-repair-view="hardware"><i data-lucide="cable"></i><span><strong>硬件诊断</strong><small>CAN · 支路 · 连接器针脚</small></span></button>
|
||
</div>
|
||
<div class="v2-divider"></div>
|
||
<span class="v2-field-caption">配置信息</span>
|
||
<div class="v2-repair-config-stack">
|
||
<div class="v2-repair-config-card"><i data-lucide="car-front"></i><div><small>当前车型</small><strong>腾势 N7</strong></div><span>N7</span></div>
|
||
<div class="v2-repair-config-card"><i data-lucide="sparkles"></i><div><small>推理模型</small><strong>GPT-5.6-sol</strong></div><span>READY</span></div>
|
||
<div class="v2-repair-config-card"><i data-lucide="cpu"></i><div><small>座椅控制器</small><strong>T2G 4BF</strong></div><span>4BF</span></div>
|
||
<div class="v2-repair-config-card v2-config-select-card repair-execution-card">
|
||
<i data-lucide="workflow"></i>
|
||
<div class="v2-config-card-copy"><small>验证方式</small><div class="v2-config-card-select">
|
||
<select id="repairExecutionMode" aria-label="选择自修复验证方式">
|
||
<option value="shadow">影子仿真</option>
|
||
<option value="bench">实物台架</option>
|
||
</select>
|
||
</div></div><span id="repairExecutionState">SAFE</span>
|
||
</div>
|
||
</div>
|
||
<p class="v2-repair-config-note" id="repairConfigNote">影子仿真不会写入 ECU 或驱动实体设备</p>`;
|
||
sidebar.insertBefore(repairSidebar, $(".v2-sidebar-footer"));
|
||
|
||
const repairFooter = document.createElement("footer");
|
||
repairFooter.className = "v2-sidebar-footer v2-repair-footer offline";
|
||
repairFooter.innerHTML = `
|
||
<span class="v2-live-dot"></span>
|
||
<div><strong>自修复安全链路</strong><small id="repairConnectionText">CAN 桥接离线</small></div>
|
||
<button type="button" id="repairEmergencyStop" title="STOP ALL + 热管理关闭" aria-label="自修复紧急停止"><i data-lucide="octagon"></i></button>`;
|
||
sidebar.append(repairFooter);
|
||
|
||
const root = document.createElement("section");
|
||
root.className = "v2-self-repair";
|
||
root.id = "v2SelfRepair";
|
||
root.setAttribute("aria-label", "座椅 AI 自修复验证台");
|
||
root.innerHTML = `
|
||
<header class="v2-repair-heading">
|
||
<div class="v2-module-heading"><span class="section-kicker">SELF-REPAIR</span><h1>座椅 AI 自修复验证</h1><p>座椅持续自由运行 · 故障独立注入 · 全过程事件留痕</p></div>
|
||
<div class="v2-repair-status-grid">
|
||
<div><i id="repairCanDot"></i><span><small>CAN桥接</small><strong id="repairCanStatus">离线</strong></span></div>
|
||
<div><i id="repairProtocolDot"></i><span><small>座椅ECU</small><strong id="repairProtocolStatus">未检测</strong></span></div>
|
||
<div><i id="repairAiDot"></i><span><small>AI状态</small><strong id="repairAiStatus">等待验证</strong></span></div>
|
||
</div>
|
||
</header>
|
||
|
||
<section class="v2-repair-view" data-repair-page="software">
|
||
<header class="v2-repair-toolbar">
|
||
<div><span>01</span><div><h2>连续运行与自修复验证</h2><p>运行任务与故障注入相互独立;选择用例不会启动或重置座椅</p></div></div>
|
||
<div>
|
||
<button type="button" id="repairSoftwareRecords"><i data-lucide="history"></i>验证记录</button>
|
||
<button type="button" id="repairFirmwareFlash"><i data-lucide="microchip"></i>构建/烧录</button>
|
||
<button type="button" id="repairReset"><i data-lucide="rotate-ccw"></i>重置统计</button>
|
||
<button class="primary" type="button" id="repairRuntimeToggle"><i data-lucide="play"></i>启动座椅运行</button>
|
||
</div>
|
||
</header>
|
||
<div class="v2-repair-software-grid">
|
||
<section class="v2-repair-panel v2-repair-twin-panel">
|
||
<header><div><span class="section-kicker">DIGITAL TWIN</span><h2>座椅3D孪生</h2></div><b id="repairTwinState">正常基线</b></header>
|
||
<div class="v2-repair-seat-viewer" id="repairSeatViewer">
|
||
<canvas id="repairSeatCanvas" aria-label="自修复座椅三维孪生,可拖动旋转并滚轮缩放"></canvas>
|
||
<div class="v2-repair-model-loading" id="repairModelLoading"><span></span><strong>正在构建座椅孪生</strong><small>坐垫 · 靠背 · 腿托 · 滑轨</small></div>
|
||
<div class="v2-repair-orbit-hint">拖动旋转 · 滚轮缩放 · 双击复位</div>
|
||
<div class="v2-repair-twin-live"><div><small id="repairTwinSource">影子动作仿真</small><strong id="repairTwinTask">等待启动座椅任务</strong><b id="repairTwinStage">待机</b></div><div id="repairAxisStrip"></div><p id="repairTwinReaction">实际动作将同步显示在座椅模型</p></div>
|
||
</div>
|
||
</section>
|
||
<section class="v2-repair-panel v2-repair-cases-panel">
|
||
<header><div><span class="section-kicker">FAULT INJECTION</span><h2>量产故障注入</h2></div><b id="repairCaseCount">6 个用例</b></header>
|
||
<p class="v2-repair-injection-note">先启动座椅持续运行,再按需选择故障。注入事件会明确标记为管理员操作。</p>
|
||
<div class="v2-repair-case-list" id="repairCaseList"></div>
|
||
<footer class="v2-repair-injection-footer">
|
||
<small id="repairSelectedCase">当前选择:—</small>
|
||
<button class="primary" type="button" id="repairInject"><i data-lucide="syringe"></i>注入所选故障</button>
|
||
<span id="repairInjectionCount">本次尚未注入故障</span>
|
||
</footer>
|
||
</section>
|
||
<section class="v2-repair-panel v2-repair-log-panel">
|
||
<header><div><span class="section-kicker">EVENT STREAM</span><h2>运行与自修复日志</h2></div><b id="repairLogState" data-state="idle">等待启动</b></header>
|
||
<div class="v2-repair-log-summary">
|
||
<div><small>本次连续运行</small><strong id="repairRuntimeElapsed">00:00:00</strong></div>
|
||
<div><small>累计健康运行</small><strong id="repairHealthyElapsed">00:00:00</strong></div>
|
||
<div><small>故障 / 已修复</small><strong id="repairFaultSummary">0 / 0</strong></div>
|
||
</div>
|
||
<div class="v2-repair-log-toolbar">
|
||
<div role="group" aria-label="日志筛选">
|
||
<button class="active" type="button" data-repair-log-filter="all">全部事件</button>
|
||
<button type="button" data-repair-log-filter="fault">仅故障与修复</button>
|
||
</div>
|
||
<span><i></i>自动跟随最新日志</span>
|
||
</div>
|
||
<div class="v2-repair-event-stream" id="repairEventStream" aria-live="polite"></div>
|
||
</section>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="v2-repair-view" data-repair-page="hardware" hidden>
|
||
<header class="v2-repair-toolbar">
|
||
<div><span>02</span><div><h2>硬件自诊断</h2><p>固件资格检查 → CAN 分层诊断 → 21 个启用针脚主动检测</p></div></div>
|
||
<div>
|
||
<button type="button" id="repairHardwareSelfTest"><i data-lucide="list-checks"></i>规则自检</button>
|
||
<button type="button" id="repairHardwareRecords"><i data-lucide="history"></i>诊断记录</button>
|
||
<button class="primary" type="button" id="repairHardwareToggle"><i data-lucide="activity"></i>启动硬件诊断</button>
|
||
</div>
|
||
</header>
|
||
<section class="v2-repair-panel v2-repair-link-panel">
|
||
<header><div><span class="section-kicker">CAN LAYERS</span><h2>CAN分层诊断</h2></div><p id="repairBridgeMessage">等待桥接状态</p></header>
|
||
<div class="v2-repair-link-layers" id="repairLinkLayers"></div>
|
||
<p id="repairLinkDetail">尚未收到实时 CAN 帧,针脚保持未检测状态。</p>
|
||
</section>
|
||
<div class="v2-repair-hardware-grid">
|
||
<section class="v2-repair-panel v2-repair-groups-panel"><header><div><span class="section-kicker">FUNCTION BRANCHES</span><h2>功能支路诊断</h2></div><b id="repairHardwareOverall">等待诊断</b></header><div id="repairHardwareGroups"></div></section>
|
||
<section class="v2-repair-panel v2-repair-connectors-panel"><header><div><span class="section-kicker">CONNECTOR PINOUT</span><h2>JA1 / JB1 / JC1 全针脚视图</h2></div><div class="v2-repair-pin-legend"><span data-state="OK"></span>正常<span data-state="UNKNOWN"></span>未检测<span data-state="IDLE"></span>检测中<span data-state="WARN"></span>告警<span data-state="FAULT"></span>故障</div></header><div id="repairConnectors"></div></section>
|
||
<section class="v2-repair-panel v2-repair-pin-panel"><header><div><span class="section-kicker">PIN EVIDENCE</span><h2 id="repairPinTitle">JC1-1</h2></div><b id="repairPinState">未检测</b></header><div id="repairPinDetail"></div></section>
|
||
</div>
|
||
</section>
|
||
|
||
<dialog class="v2-repair-dialog" id="repairSoftwareDialog"><header><div><strong>软件自修复验证记录</strong><small>成功用例自动保存诊断快照</small></div><button type="button" data-repair-close="repairSoftwareDialog">×</button></header><div id="repairSoftwareRecordsBody"></div></dialog>
|
||
<dialog class="v2-repair-dialog" id="repairHardwareDialog"><header><div><strong>硬件自诊断记录</strong><small>支路状态变化与最终报告</small></div><button type="button" data-repair-close="repairHardwareDialog">×</button></header><div id="repairHardwareRecordsBody"></div></dialog>
|
||
<dialog class="v2-repair-dialog v2-repair-confirm-dialog" id="repairConfirmDialog" aria-labelledby="repairConfirmTitle">
|
||
<header><div><strong id="repairConfirmTitle">确认台架安全条件</strong><small>确认后才会向实物链路提交请求</small></div></header>
|
||
<div><p id="repairConfirmMessage"></p><div class="v2-repair-confirm-actions"><button type="button" id="repairConfirmCancel">取消</button><button class="primary" type="button" id="repairConfirmAccept">确认执行</button></div></div>
|
||
</dialog>`;
|
||
moduleSwitcher.insertAdjacentElement("afterend", root);
|
||
|
||
const select = $("#repairExecutionMode");
|
||
select.value = executionMode;
|
||
window.CognitiveAgentConsoleV2?.enhanceSelect(select);
|
||
return true;
|
||
}
|
||
|
||
async function apiPost(path, body = {}) {
|
||
const response = await fetch(`${API_ROOT}${path}`, {
|
||
method: "POST",
|
||
credentials: "same-origin",
|
||
headers: {
|
||
Accept: "application/json",
|
||
"Content-Type": "application/json",
|
||
"X-Cognitive-Agent-Role": "admin",
|
||
},
|
||
body: JSON.stringify(body),
|
||
});
|
||
const value = await response.json().catch(() => ({ message: `HTTP ${response.status}` }));
|
||
if (!response.ok || value.ok === false) {
|
||
throw new Error(value.message || value.error || `HTTP ${response.status}`);
|
||
}
|
||
return value;
|
||
}
|
||
|
||
function showView(view) {
|
||
activeView = view === "hardware" ? "hardware" : "software";
|
||
sessionStorage.setItem(VIEW_KEY, activeView);
|
||
$$('[data-repair-view]').forEach((button) => {
|
||
const active = button.dataset.repairView === activeView;
|
||
button.classList.toggle("active", active);
|
||
button.setAttribute("aria-pressed", String(active));
|
||
});
|
||
$$(".v2-repair-view").forEach((page) => {
|
||
const active = page.dataset.repairPage === activeView;
|
||
page.hidden = !active;
|
||
page.classList.toggle("active", active);
|
||
});
|
||
window.setTimeout(() => {
|
||
window.dispatchEvent(new Event("resize"));
|
||
if (activeView === "software") {
|
||
window.dispatchEvent(new Event("self-repair-seat-reset"));
|
||
}
|
||
}, 80);
|
||
}
|
||
|
||
function scenarioForDisplay() {
|
||
if (!latest?.scenarios?.length) return null;
|
||
return latest.scenarios.find((item) => item.id === selectedCaseId)
|
||
|| latest.scenarios[0];
|
||
}
|
||
|
||
function formatDuration(seconds) {
|
||
const whole = Math.max(0, Math.floor(Number(seconds || 0)));
|
||
const hours = Math.floor(whole / 3600);
|
||
const minutes = Math.floor((whole % 3600) / 60);
|
||
const remainder = whole % 60;
|
||
return [hours, minutes, remainder]
|
||
.map((value) => String(value).padStart(2, "0"))
|
||
.join(":");
|
||
}
|
||
|
||
function formatEventTime(epoch) {
|
||
if (!epoch) return "--:--:--.---";
|
||
const value = new Date(Number(epoch) * 1000);
|
||
const base = [value.getHours(), value.getMinutes(), value.getSeconds()]
|
||
.map((item) => String(item).padStart(2, "0"))
|
||
.join(":");
|
||
return `${base}.${String(value.getMilliseconds()).padStart(3, "0")}`;
|
||
}
|
||
|
||
function runtimeState() {
|
||
return latest.runtime || fallbackState().runtime;
|
||
}
|
||
|
||
function renderEventStream() {
|
||
const runtime = runtimeState();
|
||
const healing = latest.healing || {};
|
||
const scenario = (latest.scenarios || []).find((item) => item.id === healing.case_id)
|
||
|| scenarioForDisplay();
|
||
const runtimeEvents = (runtime.events || []).map((event) => ({
|
||
...event,
|
||
kind: event.kind || "system",
|
||
label: event.label || "运行",
|
||
}));
|
||
const healingEvents = (healing.events || []).map((event) => {
|
||
const meta = EVENT_META[event.phase] || EVENT_META.BASELINE;
|
||
const injected = event.phase === "INJECTED";
|
||
return {
|
||
...event,
|
||
kind: meta.kind,
|
||
label: meta.label,
|
||
title: injected
|
||
? `管理员注入「${scenario?.short_title || "故障用例"}」`
|
||
: event.title,
|
||
tags: [
|
||
injected ? "来源:admin" : null,
|
||
healing.run_id ? `事件:${healing.run_id}` : null,
|
||
healing.diagnosed_code && ["DIAGNOSED", "REPAIRING", "VERIFYING", "PASSED", "FAILED"].includes(event.phase)
|
||
? `诊断码:${healing.diagnosed_code}` : null,
|
||
].filter(Boolean),
|
||
};
|
||
});
|
||
const activeRepair = ["INJECTED", "DIAGNOSED", "REPAIRING", "VERIFYING"].includes(healing.phase);
|
||
const twin = latest.bench_twin || {};
|
||
const naturalFault = runtime.active && !activeRepair && ["IDLE", "PASSED"].includes(healing.phase)
|
||
&& Number(twin.system?.faulted_mask || 0) !== 0;
|
||
const liveEvents = runtime.active && !activeRepair ? [{
|
||
epoch: latest.generated_epoch,
|
||
time: formatEventTime(latest.generated_epoch),
|
||
kind: naturalFault ? "fault" : "heartbeat",
|
||
label: naturalFault ? "自然故障" : "运行心跳",
|
||
title: naturalFault
|
||
? "运行中检测到座椅异常"
|
||
: `座椅已正常运行 ${formatDuration(runtime.healthy_elapsed_s)}`,
|
||
detail: naturalFault
|
||
? `ECU FaultedMask=0x${Number(twin.system.faulted_mask).toString(16).toUpperCase()},AI正在持续监测。`
|
||
: `${runtime.motion_profile} · 已完成 ${runtime.cycle_count || 0} 个动作周期。`,
|
||
tags: (twin.axes?.length ? twin.axes : [35, 42, 48, 20].map((position_pct, motor) => ({ motor, position_pct })))
|
||
.map((axis) => `M${axis.motor} ${Number(axis.position_pct || 0).toFixed(0)}%`),
|
||
}] : [];
|
||
const events = [...runtimeEvents, ...healingEvents, ...liveEvents]
|
||
.filter((event) => eventFilter === "all" || ["fault", "diagnosis", "repair", "success"].includes(event.kind))
|
||
.sort((left, right) => Number(left.epoch || 0) - Number(right.epoch || 0));
|
||
|
||
$("#repairEventStream").innerHTML = events.length
|
||
? events.map((event) => `<article data-kind="${esc(event.kind)}"><time>${esc(event.time || "--:--:--.---")}</time><i></i><div><header><span>${esc(event.label)}</span><strong>${esc(event.title)}</strong></header><p>${esc(event.detail || "状态已更新")}</p>${event.tags?.length ? `<footer>${event.tags.map((tag) => `<span>${esc(tag)}</span>`).join("")}</footer>` : ""}</div></article>`).join("")
|
||
: `<div class="v2-repair-log-empty"><i data-lucide="scroll-text"></i><strong>${runtime.active ? "座椅正在正常运行" : "等待启动座椅运行"}</strong><span>${runtime.active ? "运行心跳、故障、诊断和修复事件将在这里持续追加。" : "启动后将记录运行时长、四轴状态和后续故障注入。"}</span></div>`;
|
||
$("#repairEventStream").scrollTop = $("#repairEventStream").scrollHeight;
|
||
|
||
$("#repairRuntimeElapsed").textContent = formatDuration(runtime.elapsed_s);
|
||
$("#repairHealthyElapsed").textContent = formatDuration(runtime.healthy_elapsed_s);
|
||
$("#repairFaultSummary").textContent = `${Number(runtime.injected_count || 0)} / ${Number(runtime.repaired_count || 0)}`;
|
||
const failed = healing.phase === "FAILED";
|
||
const repairing = activeRepair;
|
||
const logState = $("#repairLogState");
|
||
logState.textContent = failed ? "需要处理" : repairing ? "自修复中" : healing.phase === "PASSED" && runtime.active ? "已恢复运行" : runtime.active ? "健康运行" : "等待启动";
|
||
logState.dataset.state = failed ? "fault" : repairing ? "repairing" : runtime.active ? "healthy" : "idle";
|
||
}
|
||
|
||
function renderCases() {
|
||
if (!selectedCaseId && latest.scenarios?.length) selectedCaseId = latest.scenarios[0].id;
|
||
const results = new Map((latest.batch?.results || []).map((item) => [item.case_id, item]));
|
||
$("#repairCaseList").innerHTML = (latest.scenarios || []).map((scenario, index) => {
|
||
const result = results.get(scenario.id);
|
||
const running = latest.batch?.current_case_id === scenario.id
|
||
|| (latest.healing?.case_id === scenario.id
|
||
&& !["IDLE", "PASSED", "FAILED"].includes(latest.healing.phase));
|
||
const state = result?.result === "PASS" ? "complete"
|
||
: result?.result === "FAIL" ? "failed"
|
||
: running ? "active" : "pending";
|
||
return `<button type="button" data-repair-case="${esc(scenario.id)}" data-state="${state}" class="${selectedCaseId === scenario.id ? "selected" : ""}"><span>${String(index + 1).padStart(2, "0")}</span><div><strong>${esc(scenario.short_title)}</strong><small>${esc(scenario.system)}</small></div><i></i></button>`;
|
||
}).join("");
|
||
$$('[data-repair-case]').forEach((button) => {
|
||
button.addEventListener("click", () => {
|
||
if (latest.batch?.active) return;
|
||
selectedCaseId = button.dataset.repairCase;
|
||
renderCases();
|
||
renderTwin();
|
||
});
|
||
});
|
||
const selected = scenarioForDisplay();
|
||
const runtime = runtimeState();
|
||
$("#repairCaseCount").textContent = `${(latest.scenarios || []).length} 个用例`;
|
||
$("#repairSelectedCase").textContent = selected
|
||
? `当前选择:${selected.short_title}` : "当前选择:—";
|
||
$("#repairInjectionCount").textContent = Number(runtime.injected_count || 0)
|
||
? `本次已注入 ${runtime.injected_count} 次 · 已修复 ${runtime.repaired_count || 0} 次`
|
||
: "本次尚未注入故障";
|
||
const activeRepair = !["IDLE", "PASSED", "FAILED"].includes(latest.healing?.phase);
|
||
$("#repairInject").disabled = !runtime.active || activeRepair || latest.batch?.active;
|
||
$("#repairInject").innerHTML = `<i data-lucide="syringe"></i>${activeRepair ? "自修复处理中" : runtime.active ? "注入所选故障" : "请先启动座椅运行"}`;
|
||
}
|
||
|
||
function renderTwin() {
|
||
const healing = latest.healing;
|
||
const scenario = scenarioForDisplay();
|
||
if (!scenario) return;
|
||
const runtime = runtimeState();
|
||
const telemetry = healing.telemetry || {};
|
||
const live = latest.bench_twin || {};
|
||
const bench = executionMode === "bench";
|
||
const positions = Array.isArray(telemetry.axis_positions)
|
||
? telemetry.axis_positions : [35, 42, 48, 20];
|
||
const axes = bench && live.axes?.length === 4
|
||
? live.axes
|
||
: positions.map((position, motor) => ({
|
||
motor,
|
||
label: ["水平滑轨", "坐盆升降", "靠背角度", "腿托角度"][motor],
|
||
position_pct: Number(position),
|
||
active: (telemetry.active_axes || []).includes(motor),
|
||
position_valid: telemetry.position_valid !== false,
|
||
}));
|
||
const stage = {
|
||
NORMAL_TASK: "正常任务",
|
||
FAULT_REACTION: "故障停止",
|
||
REPAIR: "执行修复",
|
||
VERIFYING: "安全复测",
|
||
RESUMING: "继续原任务",
|
||
COMPLETE: "任务已恢复",
|
||
SAFE_STOP: "安全停止",
|
||
}[telemetry.operation_stage] || healing.phase_text;
|
||
$("#repairTwinSource").textContent = bench
|
||
? (live.online ? "实物 CAN 同步" : "实物 CAN 暂离线")
|
||
: "影子动作仿真";
|
||
$("#repairTwinTask").textContent = healing.phase === "IDLE"
|
||
? (runtime.active ? runtime.motion_profile : "等待启动座椅运行")
|
||
: scenario.normal_task;
|
||
$("#repairTwinStage").textContent = healing.phase === "IDLE" && runtime.active
|
||
? "自由运动" : stage || "待机";
|
||
$("#repairTwinState").textContent = healing.phase === "IDLE"
|
||
? (runtime.active ? "持续运行中" : "等待启动")
|
||
: healing.phase === "PASSED" && runtime.active ? "已恢复运行" : healing.phase_text;
|
||
$("#repairTwinReaction").textContent = telemetry.bench_reaction
|
||
|| (healing.phase === "IDLE"
|
||
? (runtime.active
|
||
? `四电机按设定轨迹协同运动 · 已完成 ${runtime.cycle_count || 0} 个周期`
|
||
: "实际动作将同步显示在座椅模型")
|
||
: scenario.fault_reaction);
|
||
$("#repairAxisStrip").innerHTML = axes.map((axis) => {
|
||
const position = Math.max(0, Math.min(100, Number(axis.position_pct || 0)));
|
||
return `<span class="${axis.active ? "active" : ""}"><b>M${axis.motor}</b><i style="--position:${position}%"></i><em>${position.toFixed(0)}%</em></span>`;
|
||
}).join("");
|
||
window.dispatchEvent(new CustomEvent("seat-healing-state", { detail: {
|
||
state: healing.phase,
|
||
axes: axes.map((axis) => ({
|
||
motor: axis.motor,
|
||
position_pct: Number(axis.position_pct || 0),
|
||
active: Boolean(axis.active),
|
||
valid: axis.position_valid !== false,
|
||
})),
|
||
ventilation: Boolean((bench ? live.thermal : telemetry).fan_running),
|
||
heatingLevel: Number((bench ? live.thermal : telemetry).heater_level || 0),
|
||
} }));
|
||
}
|
||
|
||
function connectorRows(name, pins) {
|
||
if (name === "JA1") return Array.from({ length: 7 }, (_, index) => [index + 1, index + 8]);
|
||
if (name === "JB1") return Array.from({ length: 9 }, (_, index) => [(index + 1) * 2, index * 2 + 1]);
|
||
return Array.from({ length: 20 }, (_, index) => [index + 1, index + 21]);
|
||
}
|
||
|
||
function pinButton(pin) {
|
||
if (!pin) return "<span></span>";
|
||
const state = pin.enabled === false ? "NOT_USED" : pin.state;
|
||
return `<button type="button" data-repair-pin="${esc(pin.id)}" data-state="${esc(state)}" class="${selectedPinId === pin.id ? "selected" : ""}" aria-label="${esc(`${pin.id} ${pin.net} ${STATE_TEXT[state] || state}`)}"><b>${String(pin.number).padStart(2, "0")}</b><small>${esc(pin.net)}</small></button>`;
|
||
}
|
||
|
||
function renderConnectors(hardware) {
|
||
const connectors = hardware.connectors || {};
|
||
const names = ["JA1", "JB1", "JC1"];
|
||
$("#repairConnectors").innerHTML = names.map((name) => {
|
||
const pins = connectors[name] || [];
|
||
const byNumber = new Map(pins.map((pin) => [Number(pin.number), pin]));
|
||
const ok = pins.filter((pin) => pin.state === "OK").length;
|
||
const abnormal = pins.filter((pin) => ["WARN", "FAULT"].includes(pin.state)).length;
|
||
return `<section class="v2-repair-connector" data-connector="${name}"><header><strong>${name}</strong><span>已验 ${ok} · 异常 ${abnormal}</span></header><div class="v2-repair-connector-body">${name === "JC1" ? "<i class=\"keying k1\"></i><i class=\"keying k2\"></i><i class=\"keying k3\"></i><i class=\"keying k4\"></i>" : ""}<div>${connectorRows(name, pins).map((row) => `<span>${pinButton(byNumber.get(row[0]))}${pinButton(byNumber.get(row[1]))}</span>`).join("")}</div></div></section>`;
|
||
}).join("");
|
||
$$('[data-repair-pin]').forEach((button) => button.addEventListener("click", () => {
|
||
selectedPinId = button.dataset.repairPin;
|
||
renderHardware();
|
||
}));
|
||
}
|
||
|
||
function renderPinDetail(hardware) {
|
||
const allPins = Object.values(hardware.connectors || {}).flat();
|
||
const pin = allPins.find((item) => item.id === selectedPinId) || allPins[0];
|
||
if (!pin) return;
|
||
selectedPinId = pin.id;
|
||
const state = pin.enabled === false ? "NOT_USED" : pin.state;
|
||
$("#repairPinTitle").textContent = pin.id;
|
||
$("#repairPinState").textContent = STATE_TEXT[state] || state;
|
||
$("#repairPinState").dataset.state = state;
|
||
$("#repairPinDetail").innerHTML = `
|
||
<dl><div><dt>原理图网络</dt><dd>${esc(pin.net)}</dd></div><div><dt>座椅侧功能</dt><dd>${esc(pin.function)}</dd></div><div><dt>板内二级溯源</dt><dd>${esc(pin.mcu || "—")}</dd></div><div><dt>本工程启用</dt><dd>${pin.enabled === false ? "否" : "是"}</dd></div></dl>
|
||
<section><h3>判定依据</h3><ul>${(pin.evidence || ["等待诊断证据"]).map((item) => `<li>${esc(item)}</li>`).join("")}</ul></section>
|
||
<section><h3>建议检查顺序</h3><ol>${(pin.actions || ["启动硬件诊断后生成建议"]).map((item) => `<li>${esc(item)}</li>`).join("")}</ol></section>`;
|
||
}
|
||
|
||
function renderHardware() {
|
||
const hardware = latest.hardware || {};
|
||
const session = latest.hardware_session || {};
|
||
const bridge = latest.bridge || {};
|
||
const manager = latest.bridge_manager || {};
|
||
const adapterOnline = hardware.link?.adapter === "OK"
|
||
|| Boolean(bridge.bus?.opened)
|
||
|| (bridge.online && bridge.bus_traffic);
|
||
const layers = [
|
||
["1", "CAN Runtime", bridge.online ? "正常" : "离线", bridge.online ? "OK" : "UNKNOWN"],
|
||
["2", "CAN设备 / 通道", adapterOnline ? "设备与通道已打开" : "未连接", adapterOnline ? "OK" : "UNKNOWN"],
|
||
["3", "总线实时流量", bridge.bus_traffic ? "正常" : "未检测", bridge.bus_traffic ? "OK" : "WARN"],
|
||
["4", "座椅ECU协议", bridge.protocol_online ? "0x301~0x305在线" : "未检测", bridge.protocol_online ? "OK" : "UNKNOWN"],
|
||
];
|
||
$("#repairLinkLayers").innerHTML = layers.map((item) => `<div data-state="${item[3]}"><small>${item[0]} · ${item[1]}</small><strong>${item[2]}</strong></div>`).join("");
|
||
$("#repairBridgeMessage").textContent = manager.message || "等待桥接状态";
|
||
$("#repairLinkDetail").textContent = bridge.protocol_online
|
||
? `协议在线 · 最近帧 ${(bridge.recent_frame_ids || []).join(" / ")}`
|
||
: bridge.bus_traffic
|
||
? "CAN 有实时流量,但尚未识别本工程 0x301~0x305 协议。"
|
||
: "尚未收到实时 CAN 帧,启用针脚保持未检测状态,不表示针脚故障。";
|
||
$("#repairHardwareGroups").innerHTML = (hardware.groups || []).map((group) => `<button type="button" data-state="${esc(group.state)}"><i></i><div><strong>${esc(group.name)}</strong><small>${esc(group.summary)}</small></div><span>${esc(STATE_TEXT[group.state] || group.state)}</span></button>`).join("");
|
||
$("#repairHardwareOverall").textContent = `已验证 ${hardware.connector_ok_count || 0}/21 · 异常 ${(hardware.connector_fault_count || 0) + (hardware.connector_warn_count || 0)}`;
|
||
const toggle = $("#repairHardwareToggle");
|
||
toggle.innerHTML = session.active
|
||
? '<i data-lucide="square"></i>停止硬件诊断'
|
||
: '<i data-lucide="activity"></i>启动硬件诊断';
|
||
renderConnectors(hardware);
|
||
renderPinDetail(hardware);
|
||
const events = session.events || [];
|
||
$("#repairHardwareRecordsBody").innerHTML = `<p class="v2-repair-report-path">${esc(session.report_path || "停止诊断后自动保存硬件报告")}</p>${events.length ? `<table><thead><tr><th>时间</th><th>支路</th><th>状态</th><th>摘要</th></tr></thead><tbody>${[...events].reverse().map((event) => `<tr><td>${esc(event.time)}</td><td>${esc(event.name)}</td><td>${esc(STATE_TEXT[event.state] || event.state)}</td><td>${esc(event.summary)}</td></tr>`).join("")}</tbody></table>` : "<p>尚无硬件诊断事件。</p>"}`;
|
||
}
|
||
|
||
function renderGlobal() {
|
||
const bridge = latest.bridge || {};
|
||
const manager = latest.bridge_manager || {};
|
||
const healing = latest.healing || {};
|
||
const hardwarePhase = latest.hardware_session?.progress?.phase;
|
||
const integration = latest.integration || {};
|
||
const runtime = runtimeState();
|
||
$("#repairCanStatus").textContent = bridge.online ? "在线" : "离线";
|
||
$("#repairCanDot").dataset.state = bridge.online ? "online" : manager.state === "STARTING" ? "working" : "offline";
|
||
$("#repairProtocolStatus").textContent = bridge.protocol_online ? "协议在线" : "未检测";
|
||
$("#repairProtocolDot").dataset.state = bridge.protocol_online ? "online" : "offline";
|
||
const hardwareState = {
|
||
BASELINE: ["硬件检测中", "working"],
|
||
RUNNING: ["硬件检测中", "working"],
|
||
PASSED: ["21针通过", "online"],
|
||
FAILED: ["硬件异常", "offline"],
|
||
}[hardwarePhase];
|
||
const aiState = activeView === "hardware" && hardwareState
|
||
? hardwareState
|
||
: runtime.active && healing.phase === "IDLE"
|
||
? ["持续监控", "online"]
|
||
: runtime.active && healing.phase === "PASSED"
|
||
? ["已恢复监控", "online"]
|
||
: [healing.phase_text || "等待验证", healing.phase === "PASSED"
|
||
? "online"
|
||
: healing.phase === "IDLE" ? "offline" : "working"];
|
||
$("#repairAiStatus").textContent = aiState[0];
|
||
$("#repairAiDot").dataset.state = aiState[1];
|
||
$("#repairConnectionText").textContent = bridge.online ? "CAN 桥接在线" : manager.message || "CAN 桥接离线";
|
||
$(".v2-repair-footer").classList.toggle("offline", !bridge.online);
|
||
const benchOption = $('#repairExecutionMode option[value="bench"]');
|
||
if (benchOption) benchOption.disabled = !integration.can_runtime_configured;
|
||
if (executionMode === "bench" && !integration.can_runtime_configured) {
|
||
executionMode = "shadow";
|
||
$("#repairExecutionMode").value = "shadow";
|
||
sessionStorage.setItem(MODE_KEY, executionMode);
|
||
}
|
||
$("#repairExecutionState").textContent = executionMode === "bench" ? "HIL" : "SAFE";
|
||
$("#repairConfigNote").textContent = executionMode === "bench"
|
||
? "真实台架动作受本机 admin、安全确认与桥接状态约束"
|
||
: "影子仿真不会写入 ECU 或驱动实体设备";
|
||
$("#repairFirmwareFlash").disabled = !integration.firmware_tools_configured;
|
||
const runtimeToggle = $("#repairRuntimeToggle");
|
||
const repairActive = ["INJECTED", "DIAGNOSED", "REPAIRING", "VERIFYING"].includes(healing.phase);
|
||
runtimeToggle.innerHTML = runtime.active
|
||
? '<i data-lucide="square"></i>停止座椅运行'
|
||
: '<i data-lucide="play"></i>启动座椅运行';
|
||
runtimeToggle.classList.toggle("danger", runtime.active);
|
||
runtimeToggle.classList.toggle("primary", !runtime.active);
|
||
runtimeToggle.disabled = repairActive;
|
||
window.CognitiveAgentConsoleV2?.enhanceSelect($("#repairExecutionMode"));
|
||
}
|
||
|
||
function renderRecords() {
|
||
const results = latest.batch?.results || [];
|
||
const events = latest.healing?.events || [];
|
||
$("#repairSoftwareRecordsBody").innerHTML = `<p class="v2-repair-report-path">${esc(latest.latest_report_path || "成功用例将自动保存诊断快照")}</p>${results.length ? `<table><thead><tr><th>用例</th><th>结果</th><th>耗时</th><th>诊断码</th></tr></thead><tbody>${results.map((item) => `<tr><td>${esc(item.title)}</td><td>${esc(item.result)}</td><td>${Number(item.duration_s || 0).toFixed(2)} s</td><td>${esc(item.diagnosed_code || "—")}</td></tr>`).join("")}</tbody></table>` : "<p>尚无批量验证记录。</p>"}<div class="v2-repair-event-list">${events.map((event) => `<article><strong>${esc(event.title)}</strong><span>${esc(event.detail)}</span></article>`).join("")}</div>`;
|
||
}
|
||
|
||
function renderAll() {
|
||
renderGlobal();
|
||
renderCases();
|
||
renderEventStream();
|
||
renderTwin();
|
||
renderHardware();
|
||
renderRecords();
|
||
// Polling refreshes data only. Reapplying the active view here would also
|
||
// reset the user's current 3D orbit every 600 ms.
|
||
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
|
||
}
|
||
|
||
async function poll() {
|
||
if (!isActive() || polling) return;
|
||
polling = true;
|
||
try {
|
||
const response = await fetch(`${API_ROOT}/state`, {
|
||
cache: "no-store",
|
||
credentials: "same-origin",
|
||
headers: { Accept: "application/json", "X-Cognitive-Agent-Role": "admin" },
|
||
});
|
||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||
latest = await response.json();
|
||
if (!bootstrapRuntimeReset) {
|
||
bootstrapRuntimeReset = true;
|
||
const backendMode = latest.runtime?.execution_mode === "bench" ? "bench" : "shadow";
|
||
if (backendMode === "bench") {
|
||
// A physical session is authoritative: retain it and render bench feedback.
|
||
executionMode = "bench";
|
||
sessionStorage.setItem(MODE_KEY, executionMode);
|
||
if ($("#repairExecutionMode")) $("#repairExecutionMode").value = executionMode;
|
||
} else {
|
||
// Shadow state is one-run-only. A full web runtime restart always returns
|
||
// counters, injected faults, and the twin to the initial safe state.
|
||
if (latest.runtime?.active) {
|
||
await apiPost("/runtime", { active: false, execution_mode: "shadow" });
|
||
}
|
||
await apiPost("/runtime/reset-statistics");
|
||
const resetResponse = await fetch(`${API_ROOT}/state`, {
|
||
cache: "no-store",
|
||
credentials: "same-origin",
|
||
headers: { Accept: "application/json", "X-Cognitive-Agent-Role": "admin" },
|
||
});
|
||
if (resetResponse.ok) latest = await resetResponse.json();
|
||
}
|
||
}
|
||
if (!["IDLE"].includes(latest.healing?.phase)) selectedCaseId = latest.healing.case_id;
|
||
renderAll();
|
||
} catch (error) {
|
||
$("#repairConnectionText").textContent = `自修复服务连接失败:${error.message}`;
|
||
$(".v2-repair-footer").classList.add("offline");
|
||
} finally {
|
||
polling = false;
|
||
}
|
||
}
|
||
|
||
async function runAction(action, successMessage) {
|
||
try {
|
||
await action();
|
||
toast(successMessage);
|
||
await poll();
|
||
} catch (error) {
|
||
toast(error.message, true);
|
||
}
|
||
}
|
||
|
||
function finishConfirmation(accepted) {
|
||
const dialog = $("#repairConfirmDialog");
|
||
const resolve = confirmationResolve;
|
||
confirmationResolve = null;
|
||
if (dialog?.open) dialog.close(accepted ? "confirmed" : "cancelled");
|
||
if (resolve) resolve(accepted);
|
||
}
|
||
|
||
function confirmAction(message, confirmLabel = "确认执行") {
|
||
const dialog = $("#repairConfirmDialog");
|
||
if (!dialog || confirmationResolve) return Promise.resolve(false);
|
||
$("#repairConfirmMessage").textContent = message;
|
||
$("#repairConfirmAccept").textContent = confirmLabel;
|
||
dialog.showModal();
|
||
return new Promise((resolve) => {
|
||
confirmationResolve = resolve;
|
||
});
|
||
}
|
||
|
||
function bindEvents() {
|
||
$$('[data-repair-view]').forEach((button) => button.addEventListener("click", () => showView(button.dataset.repairView)));
|
||
$("#repairExecutionMode").addEventListener("change", (event) => {
|
||
executionMode = event.target.value === "bench" ? "bench" : "shadow";
|
||
sessionStorage.setItem(MODE_KEY, executionMode);
|
||
if (latest) renderAll();
|
||
});
|
||
$("#repairRuntimeToggle").addEventListener("click", async () => {
|
||
const active = Boolean(runtimeState().active);
|
||
if (executionMode === "bench" && !(await confirmAction(
|
||
active
|
||
? "即将停止实物座椅连续运行。请确认可安全停止。"
|
||
: "即将启动实物座椅四电机协同自由运动。请确认运动区域无人、无遮挡且急停可用。",
|
||
active ? "确认停止" : "确认启动",
|
||
))) return;
|
||
runAction(
|
||
() => apiPost("/runtime", { active: !active, execution_mode: executionMode }),
|
||
active ? "座椅连续运行已停止" : "座椅连续运行已启动",
|
||
);
|
||
});
|
||
$("#repairInject").addEventListener("click", async () => {
|
||
const scenario = scenarioForDisplay();
|
||
if (!runtimeState().active || !scenario) return;
|
||
if (executionMode === "bench" && !(await confirmAction(
|
||
`即将向实物 ECU 注入「${scenario.short_title}」并驱动座椅自修复。请确认运动区域无人、无遮挡且急停可用。`,
|
||
"确认注入",
|
||
))) return;
|
||
runAction(
|
||
() => apiPost("/healing/start", { case_id: selectedCaseId, execution_mode: executionMode }),
|
||
executionMode === "bench" ? "实物故障已注入" : "影子故障已注入,开始自修复",
|
||
);
|
||
});
|
||
$("#repairReset").addEventListener("click", async () => {
|
||
runAction(() => apiPost("/runtime/reset-statistics"), "本次运行统计已重置");
|
||
});
|
||
$("#repairFirmwareFlash").addEventListener("click", async () => {
|
||
if (!(await confirmAction("即将构建并烧录台架测试固件。请确认座椅运动区域无人、急停可用且 CMSIS-DAP 已连接。", "确认构建/烧录"))) return;
|
||
runAction(() => apiPost("/firmware/build-flash", { confirmed_bench_clear: true }), "固件构建/烧录流程已完成");
|
||
});
|
||
$("#repairHardwareToggle").addEventListener("click", async () => {
|
||
if (!latest) return;
|
||
if (latest.hardware_session?.active) {
|
||
runAction(() => apiPost("/hardware/stop"), "硬件诊断已停止并保存报告");
|
||
return;
|
||
}
|
||
if (!(await confirmAction("硬件诊断可能自动烧录测试固件,并短时驱动四路电机、风扇与低档加热。请确认台架区域安全且急停可用。", "确认启动诊断"))) return;
|
||
runAction(() => apiPost("/hardware/start", { confirmed_bench_clear: true }), "21 针主动硬件诊断已启动");
|
||
});
|
||
$("#repairHardwareSelfTest").addEventListener("click", () => runAction(() => apiPost("/hardware/self-test"), "72 针映射和诊断规则自检完成"));
|
||
$("#repairEmergencyStop").addEventListener("click", async () => {
|
||
if (!(await confirmAction("确认提交 STOP ALL 和热管理关闭请求?", "确认紧急停止"))) return;
|
||
runAction(() => apiPost("/emergency-stop"), "紧急停止请求已提交");
|
||
});
|
||
$("#repairConfirmCancel").addEventListener("click", () => finishConfirmation(false));
|
||
$("#repairConfirmAccept").addEventListener("click", () => finishConfirmation(true));
|
||
$("#repairConfirmDialog").addEventListener("cancel", (event) => {
|
||
event.preventDefault();
|
||
finishConfirmation(false);
|
||
});
|
||
$("#repairConfirmDialog").addEventListener("close", () => {
|
||
if (confirmationResolve) finishConfirmation(false);
|
||
});
|
||
$("#repairSoftwareRecords").addEventListener("click", () => $("#repairSoftwareDialog").showModal());
|
||
$("#repairHardwareRecords").addEventListener("click", () => $("#repairHardwareDialog").showModal());
|
||
$$('[data-repair-log-filter]').forEach((button) => button.addEventListener("click", () => {
|
||
eventFilter = button.dataset.repairLogFilter === "fault" ? "fault" : "all";
|
||
$$('[data-repair-log-filter]').forEach((item) => item.classList.toggle("active", item === button));
|
||
renderEventStream();
|
||
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
|
||
}));
|
||
$$('[data-repair-close]').forEach((button) => button.addEventListener("click", () => $(`#${button.dataset.repairClose}`).close()));
|
||
window.addEventListener("cognitive-agent:module-change", (event) => {
|
||
if (event.detail?.module === "self-repair") poll();
|
||
});
|
||
window.addEventListener("cognitive-agent:demo-role-change", () => {
|
||
if (isActive()) poll();
|
||
});
|
||
}
|
||
|
||
function init() {
|
||
if (!buildShell()) return;
|
||
bindEvents();
|
||
showView(activeView);
|
||
renderAll();
|
||
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
|
||
if (isActive()) poll();
|
||
window.setInterval(poll, 600);
|
||
}
|
||
|
||
document.addEventListener("DOMContentLoaded", init);
|
||
}());
|