feat: sync session warning across tabs
Some checks failed
CI / python-validation (push) Has been cancelled
CI / docker-release-gate (push) Has been cancelled
i18n / compile-translations (push) Has been cancelled

This commit is contained in:
Md Bayazid Bostame
2026-04-01 22:24:18 +02:00
parent 6d8c727b29
commit 7312dc0514
2 changed files with 63 additions and 1 deletions

View File

@@ -11,6 +11,10 @@
const extendButton = document.getElementById("app-session-warning-extend"); const extendButton = document.getElementById("app-session-warning-extend");
if (!modal || !countdown || !extendButton || !status || !orb || !orbValue) return; 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 lastConfirmedAt = Date.now();
let warningVisible = false; let warningVisible = false;
let keepaliveInFlight = false; let keepaliveInFlight = false;
@@ -40,6 +44,32 @@
status.textContent = ""; 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) { function showWarning(secondsLeft) {
countdown.textContent = `Noch etwa ${secondsLeft} Sekunden bis zur automatischen Abmeldung.`; 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)))); orb.style.setProperty("--session-warning-progress", String(Math.max(0, Math.min(1, secondsLeft / warningLeadSeconds))));
@@ -69,7 +99,7 @@
window.location.href = config.loginUrl; window.location.href = config.loginUrl;
return; return;
} }
lastConfirmedAt = Date.now(); syncConfirmedAt(Date.now(), "self");
showStatus("Sitzung erfolgreich verlängert."); showStatus("Sitzung erfolgreich verlängert.");
orb.style.setProperty("--session-warning-progress", "1"); orb.style.setProperty("--session-warning-progress", "1");
orbValue.textContent = "OK"; 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() { function tick() {
const elapsedSeconds = Math.floor((Date.now() - lastConfirmedAt) / 1000); const elapsedSeconds = Math.floor((Date.now() - lastConfirmedAt) / 1000);
const secondsLeft = config.idleTimeoutSeconds - elapsedSeconds; const secondsLeft = config.idleTimeoutSeconds - elapsedSeconds;

View File

@@ -846,6 +846,7 @@ make backup-verify BACKUP_DIR=backups/backup_YYYYmmdd_HHMMSS</code></pre>
<li><code>SESSION_IDLE_TIMEOUT_SECONDS</code> controls the idle session window.</li> <li><code>SESSION_IDLE_TIMEOUT_SECONDS</code> controls the idle session window.</li>
<li><code>SENSITIVE_ACTION_REAUTH_SECONDS</code> controls when sensitive POST actions require fresh authentication.</li> <li><code>SENSITIVE_ACTION_REAUTH_SECONDS</code> controls when sensitive POST actions require fresh authentication.</li>
<li><code>/session/keepalive/</code> refreshes both session timestamps when the user chooses <code>Angemeldet bleiben</code>.</li> <li><code>/session/keepalive/</code> refreshes both session timestamps when the user chooses <code>Angemeldet bleiben</code>.</li>
<li>Open tabs now sync the confirmed session timestamp through browser storage and <code>BroadcastChannel</code>, so extending the session in one tab updates the warning state in the others.</li>
</ul> </ul>
<p>This warning is meant to protect work in progress without silently relaxing the security middleware.</p> <p>This warning is meant to protect work in progress without silently relaxing the security middleware.</p>
</div> </div>