From 9c0667d1875d8dffca8456fd9a98f8143d169646 Mon Sep 17 00:00:00 2001 From: bsgdigital Date: Fri, 1 May 2026 16:22:05 +0000 Subject: [PATCH 1/3] fix(auth): extend session TTL to 30 days + redirect back after login --- api/auth.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/api/auth.py b/api/auth.py index fc397760..4746d642 100644 --- a/api/auth.py +++ b/api/auth.py @@ -24,7 +24,7 @@ PUBLIC_PATHS = frozenset({ }) COOKIE_NAME = 'hermes_session' -SESSION_TTL = 86400 # 24 hours +SESSION_TTL = 86400 * 30 # 30 days _SESSIONS_FILE = STATE_DIR / '.sessions.json' @@ -229,7 +229,12 @@ def check_auth(handler, parsed) -> bool: handler.wfile.write(b'{"error":"Authentication required"}') else: handler.send_response(302) - handler.send_header('Location', '/login') + # Pass the original path as ?next= so login.js redirects back after auth. + import urllib.parse as _urlparse + _next = _urlparse.quote(parsed.path or '/', safe='/:@!$&\'()*+,;=') + if parsed.query: + _next += '?' + parsed.query + handler.send_header('Location', '/login?next=' + _urlparse.quote(_next, safe='/:@!$&\'()*+,;=?')) handler.end_headers() return False From af3d26f14135baf7a5aa89873fb25737be1eeb1b Mon Sep 17 00:00:00 2001 From: bsgdigital Date: Fri, 1 May 2026 16:30:01 +0000 Subject: [PATCH 2/3] fix(login): probe /health on load, show VPN error if unreachable --- static/login.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/static/login.js b/static/login.js index bba29b50..1d6bddbe 100644 --- a/static/login.js +++ b/static/login.js @@ -66,4 +66,22 @@ document.addEventListener('DOMContentLoaded', function () { doLogin(e); } }); + + // On page load, probe the server so we can distinguish "can't reach server" + // (Tailscale off, wrong network) from "session expired / need to log in". + // Uses /health — a public endpoint, no auth required. + (function checkConnectivity() { + fetch('health', { method: 'GET', credentials: 'omit' }) + .then(function (r) { + if (!r.ok) showErr(connFailed + ' (server error ' + r.status + ')'); + }) + .catch(function () { + showErr('Cannot reach server — check your VPN / Tailscale connection.'); + // Disable the form so the user doesn't waste time trying a password + // that will never reach the server. + if (input) input.disabled = true; + var btn = form.querySelector('button'); + if (btn) btn.disabled = true; + }); + })(); }); From fa0ac9f3e74b45644defab6a78d27ea43bfa2cfc Mon Sep 17 00:00:00 2001 From: bsgdigital Date: Fri, 1 May 2026 19:54:28 +0000 Subject: [PATCH 3/3] fix(login): retry connectivity probe every 3s, auto-reload when server recovers When the server is unreachable (VPN/Tailscale off), the login page now polls /health every 3 seconds instead of failing silently. Once the server becomes reachable, the page reloads automatically so the user doesn't have to manually refresh. --- static/login.js | 47 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/static/login.js b/static/login.js index 1d6bddbe..72c47a5b 100644 --- a/static/login.js +++ b/static/login.js @@ -70,18 +70,41 @@ document.addEventListener('DOMContentLoaded', function () { // On page load, probe the server so we can distinguish "can't reach server" // (Tailscale off, wrong network) from "session expired / need to log in". // Uses /health — a public endpoint, no auth required. + // If unreachable, retries every 3 s and auto-reloads once the server is back. (function checkConnectivity() { - fetch('health', { method: 'GET', credentials: 'omit' }) - .then(function (r) { - if (!r.ok) showErr(connFailed + ' (server error ' + r.status + ')'); - }) - .catch(function () { - showErr('Cannot reach server — check your VPN / Tailscale connection.'); - // Disable the form so the user doesn't waste time trying a password - // that will never reach the server. - if (input) input.disabled = true; - var btn = form.querySelector('button'); - if (btn) btn.disabled = true; - }); + var retryTimer = null; + + function setFormDisabled(disabled) { + if (input) input.disabled = disabled; + var btn = form.querySelector('button'); + if (btn) btn.disabled = disabled; + } + + function probe() { + fetch('health', { method: 'GET', credentials: 'omit' }) + .then(function (r) { + if (r.ok) { + // Server is reachable — if we were in retry mode, reload so the + // page reflects the correct auth state (expired session, etc.). + if (retryTimer !== null) { + clearTimeout(retryTimer); + retryTimer = null; + window.location.reload(); + } + } else { + showErr(connFailed + ' (server error ' + r.status + ')'); + } + }) + .catch(function () { + showErr('Cannot reach server — check your VPN / Tailscale connection.'); + setFormDisabled(true); + // Keep retrying so the page auto-recovers once the network is back. + if (retryTimer === null) { + retryTimer = setInterval(probe, 3000); + } + }); + } + + probe(); })(); });