diff --git a/js/app.js b/js/app.js
index c71b1a6..2dcd694 100644
--- a/js/app.js
+++ b/js/app.js
@@ -17,7 +17,7 @@ import {
import {
scheduleStatusPoll, startPolling, pausePollingForUpload,
resumePollingAfterUpload, pollStatus, pollLogs, setStatusPollIntervalMs
-} from './common/polling.js';
+} from './common/polling.js?v=20260916d';
import {
OtaSessionState, otaSessionState, otaSessionTarget, otaFailReason,
@@ -32,7 +32,7 @@ import {
} from './ota/progress.js';
import {
updateStagesFromRelayState, updateSystemStatusUi, clearLiveMetricsOnDisconnect
-} from './ota/stages.js';
+} from './ota/stages.js?v=20260916d';
import { startEcatLoadPoller } from './ota/ecat_load.js';
import { deviceLink } from './common/device-link.js';
import {
@@ -293,6 +293,18 @@ setupDropZone('uploadZoneGd32', 'gd32');
// 否则 polling.js 的 scheduleStatusPoll 会因 window.OtaSessionState 未定义而抛 TypeError)
startPolling();
+/* DeviceLink 快照判 ESP32 离线时也清指标:
+ * 本页 pollStatus 可能 AbortError 静默返回,或 iframe 里旧模块未清——
+ * 外壳 Owner 是更权威的「设备在线」来源,离线布尔一翻就清。 */
+deviceLink.subscribe(function (s) {
+ try {
+ if (!s || !s.ts || !s.links || !s.links.esp32) return;
+ if ((Date.now() - s.ts) >= 5000) return;
+ if (s.links.esp32.online) return;
+ clearLiveMetricsOnDisconnect();
+ } catch (_) {}
+});
+
// GD32 CPU 负载率(EtherCAT 通道,独立于 /system/status 轮询;页面上无该指标则自动空转)
startEcatLoadPoller();
diff --git a/pages/index.html b/pages/index.html
index a841fb5..686ab9e 100644
--- a/pages/index.html
+++ b/pages/index.html
@@ -569,8 +569,8 @@
必须持有租约,否则服务端在租约归零宽限期后会把循环又停掉。
与 tabs.html / motor.html 并存时连接数>1,互不影响。 -->
-
-
+
+