feat(n7web): sync N7 scene cards and optimize runtime

This commit is contained in:
Miayui
2026-09-17 11:04:05 +08:00
parent 75ce391312
commit 7d196db02b
31 changed files with 2924 additions and 126 deletions
+324 -28
View File
@@ -60,6 +60,8 @@ const state = {
vehicleStateTurn: -1,
seatTwin: null,
seatActuatorState: {},
seatRenderFingerprint: "",
domainRenderFingerprint: "",
seatFeedbackSource: "simulated",
seatFeedbackSources: { passenger: "simulated", driver: "unavailable" },
availableSeatAxes: null,
@@ -76,6 +78,14 @@ const state = {
modeRunTimer: null,
modeHighlightTimer: null,
latestModeRefreshKey: "",
modeProfileSync: {
initialized: false,
inFlight: false,
poller: null,
catalogRevision: "",
importedCount: 0,
rejectedCount: 0,
},
modeEditor: null,
directCanCommandPending: false,
};
@@ -90,6 +100,7 @@ const apiRequest = (input, options) => window.CognitiveAgentNetwork.request(inpu
const HARDWARE_REQUEST_TIMEOUT_MS = 15 * 60 * 1000;
const REQUEST_TIMEOUT_MS = 45 * 1000;
const DEFAULT_PHYSICAL_STATE_POLL_INTERVAL_MS = 5000;
const MODE_PROFILE_SYNC_POLL_INTERVAL_MS = 1000;
const AUTHORITATIVE_CH1_SOURCES = new Set(["physical_can_ch1", "wireless_mqtt_ch1"]);
async function apiResponseError(response) {
@@ -2025,6 +2036,7 @@ function applyTwinSceneTheme(twin) {
canvas.dataset.themeRole = role;
canvas.dataset.themeAccent = `#${palette.accent.toString(16).padStart(6, "0")}`;
}
twin.requestRender?.({ hotspots: false });
}
function updateTwinVehicleVisual(systems, timestamp) {
@@ -2574,6 +2586,10 @@ function configureFixedTwinSeat(THREE, model, position, role) {
return model;
}
function requestSeatTwinRender(options = {}) {
state.seatTwin?.requestRender?.(options);
}
function setTwinSelection(selected) {
const twin = state.seatTwin;
if (!twin) return;
@@ -2609,6 +2625,7 @@ function setTwinSelection(selected) {
twin.view.targetX = 0;
twin.view.yaw = -0.62;
renderTwinReadout();
requestSeatTwinRender();
}
function twinDeviceForSeat(seat, actuatorState = state.seatTwin?.actuatorState || {}) {
@@ -2734,6 +2751,7 @@ function updateSeatTwin(actuatorState) {
});
});
renderTwinReadout();
requestSeatTwinRender();
}
const REFERENCE_VEHICLE_ROOT = "/assets/models/byd-tang/";
@@ -4080,7 +4098,7 @@ function initSeatTwin() {
return;
}
renderer.autoClear = false;
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 1.5));
if ("outputColorSpace" in renderer) renderer.outputColorSpace = THREE.SRGBColorSpace;
else renderer.outputEncoding = THREE.sRGBEncoding;
renderer.shadowMap.enabled = true;
@@ -4218,6 +4236,15 @@ function initSeatTwin() {
view: { yaw: -0.62, pitch: 0.31, distance: 10.2, targetX: 0 },
dragging: false,
pointer: { x: 0, y: 0 },
animationFrame: null,
renderRequested: false,
settleFrames: 0,
inViewport: true,
hotspotsDirty: true,
hotspotLastLayoutAt: Number.NEGATIVE_INFINITY,
hotspotMetrics: new Map(),
hotspotElements: new Map(),
requestRender,
};
applyTwinSceneTheme(state.seatTwin);
window.addEventListener("cognitive-agent:module-change", () => {
@@ -4284,6 +4311,7 @@ function initSeatTwin() {
distance: 10.2,
targetX: 0,
});
requestRender();
}
let stageWasVisible = false;
@@ -4294,6 +4322,8 @@ function initSeatTwin() {
camera.aspect = width / height;
camera.updateProjectionMatrix();
state.seatTwin.narrowViewport = width < 600;
state.seatTwin.hotspotMetrics.clear();
requestRender({ settleFrames: 2 });
}
function synchronizeTwinViewport({ reset = false } = {}) {
@@ -4317,6 +4347,31 @@ function initSeatTwin() {
stageObserver.observe(stage);
state.seatTwin.resizeObserver = stageObserver;
}
if (window.IntersectionObserver) {
const visibilityObserver = new IntersectionObserver(([entry]) => {
const twin = state.seatTwin;
if (!twin) return;
twin.inViewport = Boolean(entry?.isIntersecting);
if (!twin.inViewport && twin.animationFrame !== null) {
window.cancelAnimationFrame(twin.animationFrame);
twin.animationFrame = null;
} else if (twin.inViewport) {
requestRender({ settleFrames: 2 });
}
}, { root: null, rootMargin: "100px" });
visibilityObserver.observe(stage);
state.seatTwin.visibilityObserver = visibilityObserver;
}
document.addEventListener("visibilitychange", () => {
const twin = state.seatTwin;
if (!twin) return;
if (document.visibilityState !== "visible" && twin.animationFrame !== null) {
window.cancelAnimationFrame(twin.animationFrame);
twin.animationFrame = null;
return;
}
requestRender({ settleFrames: 2 });
});
window.addEventListener("resize", scheduleViewportSync);
window.addEventListener("cognitive-agent:module-change", scheduleViewportSync);
canvas.addEventListener("webglcontextlost", (event) => {
@@ -4342,6 +4397,7 @@ function initSeatTwin() {
refreshIcons();
window.setTimeout(() => { fallback.hidden = true; }, 3200);
}
requestRender({ settleFrames: 8 });
});
synchronizeTwinViewport({ reset: true });
stage.addEventListener("pointerdown", (event) => {
@@ -4349,6 +4405,7 @@ function initSeatTwin() {
state.seatTwin.pointer = { x: event.clientX, y: event.clientY };
stage.setPointerCapture(event.pointerId);
stage.classList.add("dragging");
requestRender({ settleFrames: 2 });
});
stage.addEventListener("pointermove", (event) => {
const twin = state.seatTwin;
@@ -4358,11 +4415,13 @@ function initSeatTwin() {
twin.pointer = { x: event.clientX, y: event.clientY };
twin.view.yaw -= dx * 0.006;
twin.view.pitch = Math.max(0.12, Math.min(0.68, twin.view.pitch + dy * 0.004));
requestRender({ settleFrames: 2 });
});
const endDrag = () => {
if (!state.seatTwin) return;
state.seatTwin.dragging = false;
stage.classList.remove("dragging");
requestRender({ settleFrames: 2 });
};
stage.addEventListener("pointerup", endDrag);
stage.addEventListener("pointercancel", endDrag);
@@ -4370,6 +4429,7 @@ function initSeatTwin() {
event.preventDefault();
const twin = state.seatTwin;
twin.view.distance = Math.max(9.4, Math.min(15.2, twin.view.distance + event.deltaY * 0.006));
requestRender({ settleFrames: 2 });
}, { passive: false });
$("#twinReset").addEventListener("click", resetView);
$$(".twin-seat-button").forEach((button) => button.addEventListener("click", () => setTwinSelection(button.dataset.twinSeat)));
@@ -4392,7 +4452,11 @@ function initSeatTwin() {
scene.updateMatrixWorld(true);
camera.updateMatrixWorld();
Object.entries(TWIN_DEVICE_HOTSPOT_LAYOUT).forEach(([deviceId, layout]) => {
const button = $(`[data-twin-device="${deviceId}"]`, hotspotLayer);
let button = twin.hotspotElements.get(deviceId);
if (!button?.isConnected) {
button = $(`[data-twin-device="${deviceId}"]`, hotspotLayer);
if (button) twin.hotspotElements.set(deviceId, button);
}
if (!button || !twinDeviceWorldAnchor(THREE, twin, deviceId, hotspotWorldAnchor)) return;
projectedHotspot.copy(hotspotWorldAnchor).project(camera);
const anchorX = (projectedHotspot.x * 0.5 + 0.5) * width;
@@ -4412,8 +4476,18 @@ function initSeatTwin() {
button.dataset.anchorSource = "projected-3d";
const label = $(".v2-twin-hotspot-label", button);
const halfWidth = Math.max(44, (label?.offsetWidth || 94) / 2);
const halfHeight = Math.max(20, (label?.offsetHeight || 42) / 2);
let metrics = twin.hotspotMetrics.get(deviceId);
if (!metrics || metrics.label !== label) {
const measuredWidth = label?.offsetWidth || 94;
const measuredHeight = label?.offsetHeight || 42;
metrics = {
label,
halfWidth: Math.max(44, measuredWidth / 2),
halfHeight: Math.max(20, measuredHeight / 2),
};
twin.hotspotMetrics.set(deviceId, metrics);
}
const { halfWidth, halfHeight } = metrics;
const preferredX = anchorX + layout.labelOffset[0] * offsetScale;
const preferredY = anchorY + layout.labelOffset[1] * offsetScale;
const labelX = clampLabelCoordinate(
@@ -4448,22 +4522,84 @@ function initSeatTwin() {
window.CognitiveAgentConsoleV2?.positionTwinDevicePopover?.();
}
function queueTwinFrame() {
const twin = state.seatTwin;
if (!twin || twin.animationFrame !== null) return;
if (document.visibilityState !== "visible" || twin.inViewport === false) return;
twin.animationFrame = window.requestAnimationFrame(animate);
}
function requestRender({ hotspots = true, settleFrames = 36 } = {}) {
const twin = state.seatTwin;
if (!twin) return;
twin.renderRequested = true;
twin.settleFrames = Math.max(twin.settleFrames || 0, Math.max(0, Number(settleFrames) || 0));
if (hotspots) twin.hotspotsDirty = true;
queueTwinFrame();
}
function twinNeedsAnimation(twin) {
if (twin.dragging || twin.renderRequested || twin.hotspotsDirty || twin.settleFrames > 0) {
return true;
}
const movingSeat = Object.values(twin.models).some((model) => (
Object.keys(model.positions).some(
(axis) => Math.abs(model.targets[axis] - model.positions[axis]) >= 0.002,
)
|| Object.keys(model.angles).some(
(axis) => Math.abs(model.angleTargets[axis] - model.angles[axis]) >= 0.002,
)
|| ["ventilation", "heating"].some((feature) => (
Math.abs(model.thermalTargets[feature] - model.thermalLevels[feature]) >= 0.002
|| model.thermalTargets[feature] > 0.015
|| model.thermalLevels[feature] > 0.015
))
));
if (movingSeat) return true;
const systems = twin.vehicleSystems;
if (!systems) return false;
const desiredAmbient = systems.targets.ambientPower ? systems.targets.ambientBrightness : 0;
const movingSystem = [
[systems.current.ambientBrightness, desiredAmbient],
[systems.current.climateLevel, systems.targets.climateLevel],
[systems.current.roofTransmittance, systems.targets.roofTransmittance],
[systems.current.roofShadePosition, systems.targets.roofShadePosition],
[systems.current.lowBeam, systems.targets.lowBeam],
[systems.current.highBeam, systems.targets.highBeam],
[systems.current.fogLight, systems.targets.fogLight],
[systems.current.turnSignal, systems.targets.turnSignal],
...Object.keys(systems.current.windowPositions).map((key) => [
systems.current.windowPositions[key],
systems.targets.windowPositions[key],
]),
].some(([current, target]) => Math.abs(Number(target) - Number(current)) >= 0.002);
return movingSystem
|| systems.targets.turnSignal > 0.015
|| systems.targets.climateLevel > 0.015
|| systems.airflow.level > 0.015;
}
function animate(timestamp = 0) {
const twin = state.seatTwin;
if (!twin) return;
twin.animationFrame = null;
if (document.visibilityState !== "visible" || twin.inViewport === false) return;
if (!stageHasLayout() || canvas.dataset.contextLost === "true") {
stageWasVisible = false;
window.requestAnimationFrame(animate);
twin.renderRequested = true;
return;
}
if (!stageWasVisible) synchronizeTwinViewport({ reset: true });
let geometryChanged = false;
Object.values(twin.models).forEach((model) => {
Object.keys(model.positions).forEach((axis) => {
const delta = model.targets[axis] - model.positions[axis];
if (Math.abs(delta) >= 0.002) geometryChanged = true;
model.positions[axis] += Math.abs(delta) < 0.03 ? delta : delta * 0.085;
});
Object.keys(model.angles).forEach((axis) => {
const delta = model.angleTargets[axis] - model.angles[axis];
if (Math.abs(delta) >= 0.002) geometryChanged = true;
model.angles[axis] += Math.abs(delta) < 0.03 ? delta : delta * 0.085;
});
model.slide.position.z = (model.positions.longitudinal - 50) * 0.012;
@@ -4499,6 +4635,7 @@ function initSeatTwin() {
);
updateTwinThermalVisual(model, timestamp);
});
if (geometryChanged) twin.hotspotsDirty = true;
updateTwinVehicleVisual(twin.vehicleSystems, timestamp);
const { view } = twin;
// Fit the whole reference vehicle against the limiting horizontal field
@@ -4519,7 +4656,11 @@ function initSeatTwin() {
);
camera.lookAt(cameraTargetX, 1.14, -0.08);
updateReferenceWindowOutlineLayers(twin.vehicleSystems, camera);
positionTwinDeviceHotspots();
if (twin.hotspotsDirty && timestamp - twin.hotspotLastLayoutAt >= 100) {
positionTwinDeviceHotspots();
twin.hotspotsDirty = false;
twin.hotspotLastLayoutAt = timestamp;
}
canvas.dataset.framing = "whole-vehicle-centered";
renderer.clear();
camera.layers.set(0);
@@ -4532,10 +4673,12 @@ function initSeatTwin() {
scene.background = sceneBackground;
camera.layers.set(0);
if (!canvas.dataset.rendered) canvas.dataset.rendered = "true";
window.requestAnimationFrame(animate);
twin.renderRequested = false;
twin.settleFrames = Math.max(0, twin.settleFrames - 1);
if (twinNeedsAnimation(twin)) queueTwinFrame();
}
setTwinSelection("passenger");
animate();
requestRender({ settleFrames: 48 });
}
function showToast(message) {
@@ -5114,6 +5257,10 @@ function isUserModeProfile(mode) {
return mode?.origin === "user" || mode?.deletable === true;
}
function isN7SyncedModeProfile(mode) {
return mode?.origin === "n7_mqtt" || mode?.source_topic === "from_n7/scenes";
}
function isDynamicModeProfile(mode) {
const behavior = String(mode?.behavior || "").trim().toLowerCase();
if (["cyclic", "dynamic"].includes(behavior)) return true;
@@ -5179,10 +5326,14 @@ function createModeGalleryCard(mode) {
heading.className = "mode-gallery-card-heading";
const name = document.createElement("strong");
name.textContent = mode.name || mode.id;
const sourceBadge = document.createElement("span");
sourceBadge.className = "mode-gallery-source-badge";
sourceBadge.textContent = "N7 同步";
sourceBadge.hidden = !isN7SyncedModeProfile(mode);
const stateLabel = document.createElement("small");
stateLabel.className = "mode-gallery-state";
stateLabel.textContent = "点击执行";
heading.append(name, stateLabel);
heading.append(name, sourceBadge, stateLabel);
const description = document.createElement("span");
description.className = "mode-gallery-card-description";
description.textContent = mode.description || "整车场景联动配置";
@@ -5382,6 +5533,93 @@ function refreshModeProfileFromResult(result) {
.catch(() => {});
}
function modeProfileSyncCount(value) {
const count = Number(value);
return Number.isFinite(count) && count >= 0 ? Math.trunc(count) : 0;
}
function primeModeProfileSyncFromRuntime(runtime) {
const sync = state.modeProfileSync;
const catalogRevision = String(
runtime?.mode_scene_catalog?.catalog_revision || "",
);
if (catalogRevision) sync.catalogRevision = catalogRevision;
if (sync.initialized) return;
const ingress = runtime?.vehicle_mqtt?.mode_scene_import;
if (!ingress || typeof ingress !== "object") return;
sync.importedCount = modeProfileSyncCount(ingress.imported_count);
sync.rejectedCount = modeProfileSyncCount(ingress.rejected_count);
sync.initialized = true;
}
async function checkModeProfileSync() {
const sync = state.modeProfileSync;
if (document.visibilityState === "hidden" || sync.inFlight) return;
sync.inFlight = true;
try {
const response = await apiRequest("/api/mode-profiles/status", {
cache: "no-store",
});
if (response.status === 401) {
window.location.replace("/login");
return;
}
if (!response.ok) throw await apiResponseError(response);
const payload = await response.json();
const ingress = payload?.mode_scene_ingress || {};
const catalogRevision = String(payload?.catalog_revision || "");
const importedCount = modeProfileSyncCount(ingress.imported_count);
const rejectedCount = modeProfileSyncCount(ingress.rejected_count);
if (!sync.initialized) {
sync.catalogRevision = catalogRevision;
sync.importedCount = importedCount;
sync.rejectedCount = rejectedCount;
sync.initialized = true;
return;
}
const catalogChanged = Boolean(
catalogRevision && catalogRevision !== sync.catalogRevision,
);
const imported = importedCount > sync.importedCount;
const rejected = rejectedCount > sync.rejectedCount;
sync.catalogRevision = catalogRevision || sync.catalogRevision;
sync.importedCount = importedCount;
sync.rejectedCount = rejectedCount;
const sceneId = String(ingress.last_scene_id || "").trim();
const sceneAlreadyVisible = sceneId
? modeProfiles().some((mode) => String(mode?.id || "") === sceneId)
: false;
if (catalogChanged || (imported && !sceneAlreadyVisible)) {
await loadRuntime();
}
if (imported) {
if (sceneId) revealModeProfile(sceneId);
const sceneName = String(
ingress.last_result?.scene_name || sceneId || "新模式",
);
showToast(`已从 N7 同步模式卡片:${sceneName}`);
} else if (rejected) {
const reason = String(ingress.last_error || "消息格式不符合要求");
showToast(`N7 模式卡片导入失败:${reason}`);
}
} catch (error) {
console.warn("N7 mode profile synchronization check failed", error);
} finally {
sync.inFlight = false;
}
}
function startModeProfileSyncPolling() {
const sync = state.modeProfileSync;
window.clearInterval(sync.poller);
sync.poller = window.setInterval(
() => void checkModeProfileSync(),
MODE_PROFILE_SYNC_POLL_INTERVAL_MS,
);
}
function openModeGallery() {
const backdrop = $("#modeGalleryBackdrop");
if (!backdrop) return;
@@ -6639,8 +6877,16 @@ function setModeChipsDisabled(disabled) {
});
}
function hasExecutableModeCommandFlow(mode) {
const flow = mode?.command_flow;
if (!mode?.id || !flow || typeof flow !== "object") return false;
const plans = flow.plans;
return Boolean(flow.source_request_id)
|| (plans && typeof plans === "object" && Object.keys(plans).length > 0);
}
async function executeSavedModeProfile(mode, { command = "" } = {}) {
if (!mode?.id || !mode?.command_flow?.source_request_id) return false;
if (!hasExecutableModeCommandFlow(mode)) return false;
const modeCommand = String(command || `进入${mode.name || mode.id}`).trim();
const assistant = window.CognitiveAgentAssistant;
const driverOnlyCh1 = isDriverOnlyCh1Runtime();
@@ -6747,7 +6993,7 @@ function applyModeCommand(command, modeProfileId = null) {
setModeRailSelection(mode);
closeModeGallery({ restoreFocus: false });
assistant?.open?.();
if (mode?.command_flow?.source_request_id) {
if (hasExecutableModeCommandFlow(mode)) {
void executeSavedModeProfile(mode, { command: modeCommand });
return true;
}
@@ -6809,6 +7055,7 @@ async function loadRuntime() {
const response = await apiRequest("/api/runtime", { cache: "no-store" });
if (!response.ok) throw await apiResponseError(response);
state.runtime = await response.json();
primeModeProfileSyncFromRuntime(state.runtime);
renderControlTransport();
applyRuntimeTargetPolicy(state.runtime);
populateVehicleSelector();
@@ -7157,6 +7404,8 @@ function resetSeatState() {
function resetVehicleStatusDisplay() {
state.seatActuatorState = {};
state.seatRenderFingerprint = "";
state.domainRenderFingerprint = "";
resetSeatState();
const setText = (selector, value) => {
const element = $(selector);
@@ -7227,6 +7476,28 @@ function resetVehicleStatusDisplay() {
summarySystem.textContent = "等待状态";
summarySystem.dataset.state = "waiting";
}
window.dispatchEvent(new CustomEvent("cognitive-agent:vehicle-state-rendered", {
detail: { scope: "reset", fingerprint: "" },
}));
}
const VEHICLE_RENDER_VOLATILE_KEYS = new Set([
"age_ms",
"capture_sequence",
"captured_at",
"frame_timestamp",
"last_seen_at",
"observed_at",
"received_at",
"request_id",
"timestamp",
"updated_at",
]);
function vehicleRenderFingerprint(value) {
return JSON.stringify(value, (key, item) => (
VEHICLE_RENDER_VOLATILE_KEYS.has(key) ? undefined : item
));
}
function mergeSeatActuatorState(previousState, patchState) {
@@ -7246,20 +7517,6 @@ function mergeSeatActuatorState(previousState, patchState) {
mergedSeat.position = mergedPosition;
return;
}
if (feature === "dynamic_mode") {
const item = $(`.seat-card[data-seat="${seat}"] [data-dynamic-mode]`);
if (!item) return;
const running = value.state === "running";
item.classList.toggle("running", running);
const runtimeMinutes = Number(value.max_runtime_ms || 600000) / 60000;
const runtimeLabel = Number.isInteger(runtimeMinutes)
? String(runtimeMinutes)
: runtimeMinutes.toFixed(1);
$("b", item).textContent = running
? `${value.name} · 循环中 · 最长${runtimeLabel}分钟`
: "未运行";
return;
}
mergedSeat[feature] = value && typeof value === "object" && !Array.isArray(value)
? { ...(previousSeat[feature] || {}), ...value }
: value;
@@ -7270,13 +7527,15 @@ function mergeSeatActuatorState(previousState, patchState) {
}
function renderSeatState(actuatorState) {
if (!actuatorState) return;
if (!actuatorState) return false;
const mergedActuatorState = mergeSeatActuatorState(
state.seatActuatorState,
actuatorState,
);
const renderFingerprint = vehicleRenderFingerprint(mergedActuatorState);
state.seatActuatorState = mergedActuatorState;
resetSeatState();
if (renderFingerprint === state.seatRenderFingerprint) return false;
state.seatRenderFingerprint = renderFingerprint;
Object.entries(mergedActuatorState).forEach(([seat, functions]) => {
const displayFunctions = seat === "driver"
? twinDeviceForSeat("driver", { driver: functions })
@@ -7302,6 +7561,20 @@ function renderSeatState(actuatorState) {
: "CH1 状态已同步";
return;
}
if (feature === "dynamic_mode") {
const item = $(`.seat-card[data-seat="${seat}"] [data-dynamic-mode]`);
if (!item) return;
const running = value.state === "running";
item.classList.toggle("running", running);
const runtimeMinutes = Number(value.max_runtime_ms || 600000) / 60000;
const runtimeLabel = Number.isInteger(runtimeMinutes)
? String(runtimeMinutes)
: runtimeMinutes.toFixed(1);
$("b", item).textContent = running
? `${value.name} · 循环中 · 最长${runtimeLabel}分钟`
: "未运行";
return;
}
if (feature === "position") {
Object.entries(value).forEach(([axis, motion]) => {
const item = $(`.seat-card[data-seat="${seat}"] [data-axis="${axis}"]`);
@@ -7335,10 +7608,17 @@ function renderSeatState(actuatorState) {
});
});
updateSeatTwin(mergedActuatorState);
window.dispatchEvent(new CustomEvent("cognitive-agent:vehicle-state-rendered", {
detail: { scope: "seat", fingerprint: renderFingerprint },
}));
return true;
}
function renderDomainState(domainState) {
if (!domainState) return;
if (!domainState) return false;
const renderFingerprint = vehicleRenderFingerprint(domainState);
if (renderFingerprint === state.domainRenderFingerprint) return false;
state.domainRenderFingerprint = renderFingerprint;
const outletModeLabel = (value) => {
const normalized = String(value || "").toLowerCase().replaceAll("-", "_");
const matrixLabels = {
@@ -7723,6 +8003,11 @@ function renderDomainState(domainState) {
});
}
}
requestSeatTwinRender();
window.dispatchEvent(new CustomEvent("cognitive-agent:vehicle-state-rendered", {
detail: { scope: "domain", fingerprint: renderFingerprint },
}));
return true;
}
function renderedConfig(result) {
@@ -9041,6 +9326,15 @@ window.addEventListener("cognitive-agent:request-complete", (event) => {
setModeExecutionState(modeProfileId, succeeded ? "succeeded" : "failed");
});
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible") void checkModeProfileSync();
});
window.addEventListener("pagehide", () => {
window.clearInterval(state.modeProfileSync.poller);
state.modeProfileSync.poller = null;
});
document.addEventListener("DOMContentLoaded", async () => {
initGlassThemeToggle();
initJointTwinLayout();
@@ -9147,6 +9441,8 @@ document.addEventListener("DOMContentLoaded", async () => {
$$('[data-download]').forEach((button) => button.addEventListener("click", () => downloadArtifact(button.dataset.download)));
await loadAuthStatus();
await loadRuntime();
startModeProfileSyncPolling();
void checkModeProfileSync();
if (!isDriverOnlyCh1Runtime()) {
await loadHardwareAccessStatus({ silent: true });
state.hardwareAccessPoller = window.setInterval(