diff --git a/public/index.html b/public/index.html
index e1104db..7db03c8 100644
--- a/public/index.html
+++ b/public/index.html
@@ -429,6 +429,7 @@
+
diff --git a/public/js/authFetch.js b/public/js/authFetch.js
new file mode 100644
index 0000000..2d957e7
--- /dev/null
+++ b/public/js/authFetch.js
@@ -0,0 +1,85 @@
+// ============================================================
+// Global fetch interceptor for auth failures
+// ============================================================
+// When the server responds 401 to any /api/ request, the user's
+// session has been revoked or expired. Previously this would leave
+// the app UI on screen silently because each fetch handled its own
+// errors. Now we detect it once, clear local state, and bounce to
+// the login screen.
+// ============================================================
+
+(function() {
+ if (window.__fetchAuthIntercepted) return;
+ window.__fetchAuthIntercepted = true;
+
+ var _fetch = window.fetch.bind(window);
+ var handlingLogout = false;
+
+ function handleAuthFailure(status, reason) {
+ if (handlingLogout) return;
+ handlingLogout = true;
+ try {
+ // Clear any cached token + user info
+ window.AUTH_TOKEN = null;
+ window.CURRENT_USER = null;
+ if (window.SecureStorage) {
+ try { window.SecureStorage.remove('ped_scribe_token'); } catch(e) {}
+ try { window.SecureStorage.remove('ped_scribe_user'); } catch(e) {}
+ try { window.SecureStorage.remove('ped_session_id'); } catch(e) {}
+ }
+ try {
+ localStorage.removeItem('ped_scribe_token');
+ localStorage.removeItem('ped_scribe_user');
+ localStorage.removeItem('ped_session_id');
+ } catch(e) {}
+
+ // Brief user-visible notice before the reload swap
+ try {
+ if (typeof window.showToast === 'function') {
+ window.showToast(reason || 'Your session has ended. Please sign in again.', 'info');
+ }
+ } catch(e) {}
+
+ // Reload — the boot flow in auth.js will call /api/auth/me, get a
+ // fresh 401 (no cookie / no token), and show the login screen.
+ setTimeout(function() { window.location.reload(); }, 800);
+ } catch (e) {
+ // Last resort
+ window.location.reload();
+ }
+ }
+
+ window.fetch = function(input, init) {
+ return _fetch(input, init).then(function(resp) {
+ // Only act on same-origin /api/ calls; don't interfere with login
+ // or logout endpoints themselves (they're allowed to return 401).
+ try {
+ var url = typeof input === 'string' ? input : (input && input.url) || '';
+ var isApiCall = url.indexOf('/api/') !== -1;
+ var isAuthEndpoint = url.indexOf('/api/auth/login') !== -1
+ || url.indexOf('/api/auth/register') !== -1
+ || url.indexOf('/api/auth/logout') !== -1
+ || url.indexOf('/api/auth/forgot-password') !== -1
+ || url.indexOf('/api/auth/reset-password') !== -1
+ || url.indexOf('/api/auth/verify-email') !== -1
+ || url.indexOf('/api/auth/resend-verification') !== -1
+ || url.indexOf('/api/auth/oidc') !== -1
+ || url.indexOf('/api/auth/me') !== -1
+ || url.indexOf('/api/auth/2fa') !== -1;
+ if (resp.status === 401 && isApiCall && !isAuthEndpoint) {
+ // Only treat as global session failure if the app thinks the user
+ // was logged in (AUTH_TOKEN set OR main-app visible). Avoid
+ // flashing the login screen on public-endpoint 401s during the
+ // pre-login phase.
+ var authed = !!window.AUTH_TOKEN
+ || document.documentElement.classList.contains('has-session')
+ || (document.getElementById('main-app') && document.getElementById('main-app').style.display === 'block');
+ if (authed) {
+ handleAuthFailure(resp.status, 'Your session was ended by another device. Please sign in again.');
+ }
+ }
+ } catch(e) {}
+ return resp;
+ });
+ };
+})();