diff --git a/backend/workflows/static/workflows/js/session_warning.js b/backend/workflows/static/workflows/js/session_warning.js index ae7eff5..6ea9ca5 100644 --- a/backend/workflows/static/workflows/js/session_warning.js +++ b/backend/workflows/static/workflows/js/session_warning.js @@ -11,6 +11,10 @@ const extendButton = document.getElementById("app-session-warning-extend"); if (!modal || !countdown || !extendButton || !status || !orb || !orbValue) return; + const storageKey = "workdock.session.lastConfirmedAt"; + const syncChannel = typeof BroadcastChannel !== "undefined" + ? new BroadcastChannel("workdock-session-warning") + : null; let lastConfirmedAt = Date.now(); let warningVisible = false; let keepaliveInFlight = false; @@ -40,6 +44,32 @@ status.textContent = ""; } + function readStoredConfirmedAt() { + try { + const raw = window.localStorage.getItem(storageKey); + const parsed = raw ? Number.parseInt(raw, 10) : NaN; + return Number.isFinite(parsed) ? parsed : null; + } catch (_error) { + return null; + } + } + + function syncConfirmedAt(timestamp, source) { + lastConfirmedAt = timestamp; + try { + window.localStorage.setItem(storageKey, String(timestamp)); + } catch (_error) { + // Ignore storage write failures. + } + if (syncChannel && source !== "broadcast") { + syncChannel.postMessage({ type: "confirmed-at", value: timestamp }); + } + if (source !== "self") { + hideWarning(); + hideStatus(); + } + } + function showWarning(secondsLeft) { countdown.textContent = `Noch etwa ${secondsLeft} Sekunden bis zur automatischen Abmeldung.`; orb.style.setProperty("--session-warning-progress", String(Math.max(0, Math.min(1, secondsLeft / warningLeadSeconds)))); @@ -69,7 +99,7 @@ window.location.href = config.loginUrl; return; } - lastConfirmedAt = Date.now(); + syncConfirmedAt(Date.now(), "self"); showStatus("Sitzung erfolgreich verlängert."); orb.style.setProperty("--session-warning-progress", "1"); orbValue.textContent = "OK"; @@ -84,6 +114,37 @@ } } + const storedConfirmedAt = readStoredConfirmedAt(); + if (storedConfirmedAt) { + lastConfirmedAt = storedConfirmedAt; + } else { + syncConfirmedAt(lastConfirmedAt, "self"); + } + + if (syncChannel) { + syncChannel.addEventListener("message", function (event) { + if (event.data && event.data.type === "confirmed-at" && Number.isFinite(event.data.value)) { + syncConfirmedAt(event.data.value, "broadcast"); + } + }); + } + + window.addEventListener("storage", function (event) { + if (event.key !== storageKey || !event.newValue) return; + const parsed = Number.parseInt(event.newValue, 10); + if (Number.isFinite(parsed)) { + syncConfirmedAt(parsed, "storage"); + } + }); + + document.addEventListener("visibilitychange", function () { + if (document.visibilityState !== "visible") return; + const latest = readStoredConfirmedAt(); + if (latest && latest > lastConfirmedAt) { + syncConfirmedAt(latest, "storage"); + } + }); + function tick() { const elapsedSeconds = Math.floor((Date.now() - lastConfirmedAt) / 1000); const secondsLeft = config.idleTimeoutSeconds - elapsedSeconds; diff --git a/backend/workflows/templates/workflows/developer_handbook.html b/backend/workflows/templates/workflows/developer_handbook.html index 04b1318..afc41b1 100644 --- a/backend/workflows/templates/workflows/developer_handbook.html +++ b/backend/workflows/templates/workflows/developer_handbook.html @@ -846,6 +846,7 @@ make backup-verify BACKUP_DIR=backups/backup_YYYYmmdd_HHMMSS
SESSION_IDLE_TIMEOUT_SECONDS controls the idle session window.SENSITIVE_ACTION_REAUTH_SECONDS controls when sensitive POST actions require fresh authentication./session/keepalive/ refreshes both session timestamps when the user chooses Angemeldet bleiben.BroadcastChannel, so extending the session in one tab updates the warning state in the others.This warning is meant to protect work in progress without silently relaxing the security middleware.