diff --git a/webui/html/assets/js/tabs/containers.js b/webui/html/assets/js/tabs/containers.js
index 9a44007..4538226 100644
--- a/webui/html/assets/js/tabs/containers.js
+++ b/webui/html/assets/js/tabs/containers.js
@@ -420,6 +420,9 @@ let execTerminalState = {
outputText: '',
outputLineCount: 0,
outputTruncated: false,
+ sessionInfo: null,
+ infoPollTimer: null,
+ infoTickTimer: null,
};
const EXEC_OUTPUT_MAX_CHARS = 200000;
@@ -430,6 +433,8 @@ const EXEC_RECONNECT_BASE_MS = 350;
const EXEC_RECONNECT_MAX_MS = 5000;
const EXEC_RECONNECT_MAX_ATTEMPTS = 8;
const EXEC_PASTE_CHUNK_SIZE = 2048;
+const EXEC_SESSION_INFO_POLL_MS = 7000;
+const EXEC_IDLE_TTL_SECONDS = 60 * 60;
function _execEls() {
return {
@@ -526,7 +531,73 @@ function _execInitTerminal() {
function _execConnectedStatus() {
const sid = execTerminalState.sessionId || '';
- return sid ? `Connected (${sid})` : 'Connected';
+ if (!sid) return 'Connected';
+ const info = execTerminalState.sessionInfo;
+ if (!info) return `Connected (${sid})`;
+ if (info.closed) return `Session closed: ${info.close_reason || 'closed'}`;
+
+ const last = Number(info.last_activity || 0);
+ if (!Number.isFinite(last) || last <= 0) return `Connected (${sid})`;
+ const now = Math.floor(Date.now() / 1000);
+ const idleAge = Math.max(0, now - last);
+ const idleLeft = Math.max(0, EXEC_IDLE_TTL_SECONDS - idleAge);
+ const mm = String(Math.floor(idleLeft / 60)).padStart(2, '0');
+ const ss = String(idleLeft % 60).padStart(2, '0');
+ return `Connected (${sid}) - idle in ${mm}:${ss}`;
+}
+
+function _execStopSessionInfoTimers() {
+ if (execTerminalState.infoPollTimer) {
+ clearInterval(execTerminalState.infoPollTimer);
+ execTerminalState.infoPollTimer = null;
+ }
+ if (execTerminalState.infoTickTimer) {
+ clearInterval(execTerminalState.infoTickTimer);
+ execTerminalState.infoTickTimer = null;
+ }
+}
+
+function _execCanUpdateConnectedStatus() {
+ if (!execTerminalState.sessionId) return false;
+ if (
+ execTerminalState.reconnectEnabled &&
+ execTerminalState.reconnectAttempts > 0 &&
+ !execTerminalState.source
+ ) {
+ return false;
+ }
+ return true;
+}
+
+async function _execRefreshSessionInfo() {
+ const sid = execTerminalState.sessionId;
+ if (!sid) return;
+ try {
+ const info = await api(`/containers/exec/${encodeURIComponent(sid)}`, 'GET');
+ if (sid !== execTerminalState.sessionId) return;
+ execTerminalState.sessionInfo = info || null;
+ if (execTerminalState.sessionInfo?.closed) {
+ execTerminalState.reconnectEnabled = false;
+ _execCancelReconnect();
+ _execCloseEventSource();
+ _execSetStatus(`Session closed: ${execTerminalState.sessionInfo.close_reason || 'closed'}`);
+ _execStopSessionInfoTimers();
+ return;
+ }
+ if (_execCanUpdateConnectedStatus()) _execSetStatus(_execConnectedStatus());
+ } catch (_) {
+ // stream reconnect status has priority
+ }
+}
+
+function _execStartSessionInfoTimers() {
+ _execStopSessionInfoTimers();
+ _execRefreshSessionInfo();
+ execTerminalState.infoPollTimer = setInterval(_execRefreshSessionInfo, EXEC_SESSION_INFO_POLL_MS);
+ execTerminalState.infoTickTimer = setInterval(() => {
+ if (!_execCanUpdateConnectedStatus()) return;
+ _execSetStatus(_execConnectedStatus());
+ }, 1000);
}
function _execAppendOutput(txt) {
@@ -888,7 +959,9 @@ async function containerExecOpen(name) {
execTerminalState.sessionId = '';
execTerminalState.lastSeq = 0;
execTerminalState.reconnectEnabled = false;
+ execTerminalState.sessionInfo = null;
_execCancelReconnect();
+ _execStopSessionInfoTimers();
execTerminalState.rawMode = true;
_execInitTerminal();
_execUpdateInputModeUi();
@@ -907,8 +980,10 @@ async function containerExecOpen(name) {
execTerminalState.sessionId = sid;
execTerminalState.lastSeq = 0;
execTerminalState.reconnectEnabled = true;
+ execTerminalState.sessionInfo = null;
_execSetStatus(`Connected (${sid})`);
_execAttachStream(0, false);
+ _execStartSessionInfoTimers();
_execScheduleResize();
@@ -998,6 +1073,7 @@ async function containerExecClose() {
execTerminalState.reconnectEnabled = false;
_execCancelReconnect();
+ _execStopSessionInfoTimers();
_execCloseEventSource();
_execDestroyTerminal();
_execSetTerminalUiMode(false);
@@ -1013,6 +1089,7 @@ async function containerExecClose() {
execTerminalState.sessionId = '';
execTerminalState.lastSeq = 0;
+ execTerminalState.sessionInfo = null;
if (els.back) els.back.style.display = 'none';
if (sid) {