all working

This commit is contained in:
HotSwapp
2025-08-10 21:34:11 -05:00
parent 14ee479edc
commit 1512b2d12a
22 changed files with 1453 additions and 489 deletions

View File

@@ -6,7 +6,7 @@
// ... add the JS content ...
// Modify modal showing/hiding to use classList.add/remove('hidden') instead of Bootstrap modal
// Modify modal showing/hiding to use classList.add/remove('hidden')
// For example:
function showQuickTimeModal() {

View File

@@ -5,6 +5,7 @@
// Global application state
const app = {
token: localStorage.getItem('auth_token'),
refreshToken: localStorage.getItem('refresh_token'),
user: null,
initialized: false,
refreshTimerId: null,
@@ -13,9 +14,98 @@ const app = {
// Initialize application
document.addEventListener('DOMContentLoaded', function() {
try { setupGlobalErrorHandlers(); } catch (_) {}
initializeApp();
});
// Theme Management (centralized)
function applyTheme(theme) {
const html = document.documentElement;
const isDark = theme === 'dark';
html.classList.toggle('dark', isDark);
html.setAttribute('data-theme', isDark ? 'dark' : 'light');
}
function toggleTheme() {
const html = document.documentElement;
const nextTheme = html.classList.contains('dark') ? 'light' : 'dark';
applyTheme(nextTheme);
try { localStorage.setItem('theme-preference', nextTheme); } catch (_) {}
saveThemePreference(nextTheme);
}
function initializeTheme() {
// Check for saved theme preference
let savedTheme = null;
try { savedTheme = localStorage.getItem('theme-preference'); } catch (_) {}
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
const theme = savedTheme || (prefersDark ? 'dark' : 'light');
applyTheme(theme);
// Load from server if available
loadUserThemePreference();
// Listen for OS theme changes if no explicit preference is set
attachSystemThemeListener();
}
async function saveThemePreference(theme) {
const token = localStorage.getItem('auth_token');
if (!token || isLoginPage()) return;
try {
await fetch('/api/auth/theme-preference', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ theme_preference: theme })
});
} catch (error) {
console.log('Could not save theme preference to server:', error.message);
}
}
function attachSystemThemeListener() {
if (!('matchMedia' in window)) return;
const media = window.matchMedia('(prefers-color-scheme: dark)');
const handleChange = (e) => {
let savedTheme = null;
try { savedTheme = localStorage.getItem('theme-preference'); } catch (_) {}
if (!savedTheme || savedTheme === 'system') {
applyTheme(e.matches ? 'dark' : 'light');
}
};
if (typeof media.addEventListener === 'function') {
media.addEventListener('change', handleChange);
} else if (typeof media.addListener === 'function') {
media.addListener(handleChange); // Safari fallback
}
}
async function loadUserThemePreference() {
const token = localStorage.getItem('auth_token');
if (!token || isLoginPage()) return;
try {
const response = await fetch('/api/auth/me', {
headers: { 'Authorization': `Bearer ${token}` }
});
if (response.ok) {
const user = await response.json();
if (user.theme_preference) {
applyTheme(user.theme_preference);
try { localStorage.setItem('theme-preference', user.theme_preference); } catch (_) {}
}
}
} catch (error) {
console.log('Could not load theme preference from server:', error.message);
}
}
// Apply theme immediately on script load
try { initializeTheme(); } catch (_) {}
async function initializeApp() {
// Initialize keyboard shortcuts
if (window.keyboardShortcuts) {
@@ -29,6 +119,11 @@ async function initializeApp() {
// Initialize API helpers
setupAPIHelpers();
// Initialize authentication manager (centralized)
if (typeof initializeAuthManager === 'function') {
initializeAuthManager();
}
app.initialized = true;
console.log('Delphi Database System initialized');
@@ -103,6 +198,14 @@ async function apiCall(url, options = {}) {
try {
let response = await fetch(url, config);
const updateCorrelationFromResponse = (resp) => {
try {
const cid = resp && resp.headers ? resp.headers.get('X-Correlation-ID') : null;
if (cid) { window.app.lastCorrelationId = cid; }
return cid;
} catch (_) { return null; }
};
let lastCorrelationId = updateCorrelationFromResponse(response);
if (response.status === 401 && app.token) {
// Attempt one refresh then retry once
@@ -113,6 +216,7 @@ async function apiCall(url, options = {}) {
...options
};
response = await fetch(url, retryConfig);
lastCorrelationId = updateCorrelationFromResponse(response);
} catch (_) {
// fall through to logout below
}
@@ -124,7 +228,10 @@ async function apiCall(url, options = {}) {
if (!response.ok) {
const errorData = await response.json().catch(() => ({ detail: 'Request failed' }));
throw new Error(errorData.detail || `HTTP ${response.status}`);
const err = new Error(errorData.detail || `HTTP ${response.status}`);
err.status = response.status;
err.correlationId = lastCorrelationId || null;
throw err;
}
return await response.json();
@@ -158,16 +265,263 @@ async function apiDelete(url) {
}
// Authentication functions
function setAuthToken(token) {
app.token = token;
localStorage.setItem('auth_token', token);
window.apiHeaders['Authorization'] = `Bearer ${token}`;
function setAuthTokens(accessToken, newRefreshToken = null) {
if (accessToken) {
app.token = accessToken;
localStorage.setItem('auth_token', accessToken);
window.apiHeaders['Authorization'] = `Bearer ${accessToken}`;
}
if (newRefreshToken) {
app.refreshToken = newRefreshToken;
localStorage.setItem('refresh_token', newRefreshToken);
}
// Reschedule refresh on token update
scheduleTokenRefresh();
if (accessToken) {
scheduleTokenRefresh();
}
}
function logout() {
function setAuthToken(token) {
// Backwards compatibility
setAuthTokens(token, null);
}
// Page helpers
function isLoginPage() {
const path = window.location.pathname;
return path === '/login' || path === '/';
}
// Verify the current access token by hitting /api/auth/me
async function checkTokenValidity() {
const token = localStorage.getItem('auth_token');
if (!token) return false;
try {
const response = await fetch('/api/auth/me', {
headers: { 'Authorization': `Bearer ${token}` }
});
if (!response.ok) {
// Invalid token
return false;
}
// Cache user for later UI updates
try { app.user = await response.json(); } catch (_) {}
return true;
} catch (error) {
console.error('Error checking token validity:', error);
return false;
}
}
// Try to refresh token if refresh token is present; fallback to validity check+logout
async function refreshTokenIfNeeded() {
const refreshTokenValue = localStorage.getItem('refresh_token');
if (!refreshTokenValue) return;
app.refreshToken = refreshTokenValue;
try {
await refreshToken();
console.log('Token refreshed successfully');
} catch (error) {
const stillValid = await checkTokenValidity();
if (!stillValid) {
await logout('Session expired or invalid token');
}
}
}
// Update UI elements that are permission/user dependent
async function checkUserPermissions() {
const token = localStorage.getItem('auth_token');
if (!token || isLoginPage()) return;
try {
const response = await fetch('/api/auth/me', {
headers: { 'Authorization': `Bearer ${token}` }
});
if (response.ok) {
const user = await response.json();
app.user = user;
if (user.is_admin) {
const adminItem = document.getElementById('admin-menu-item');
const adminDivider = document.getElementById('admin-menu-divider');
if (adminItem) adminItem.classList.remove('hidden');
if (adminDivider) adminDivider.classList.remove('hidden');
}
const userDropdownName = document.querySelector('#userDropdown button span');
if (user.full_name && userDropdownName) {
userDropdownName.textContent = user.full_name;
}
}
} catch (error) {
console.error('Error checking user permissions:', error);
}
}
// Inactivity monitoring & session extension UI
async function getInactivityWarningMinutes() {
const token = localStorage.getItem('auth_token');
if (!token) return 240;
try {
const resp = await fetch('/api/settings/inactivity_warning_minutes', {
headers: { 'Authorization': `Bearer ${token}` }
});
if (!resp.ok) return 240;
const data = await resp.json();
if (typeof data.minutes === 'number') return data.minutes;
const parsed = parseInt(data.setting_value || data.minutes, 10);
return Number.isFinite(parsed) ? parsed : 240;
} catch (_) {
return 240;
}
}
function showSessionExtendedNotification() {
if (window.alerts && typeof window.alerts.success === 'function') {
window.alerts.success('Your session has been refreshed successfully.', {
title: 'Session Extended',
duration: 3000
});
return;
}
// Fallback
const notification = document.createElement('div');
notification.className = 'fixed top-4 right-4 bg-green-100 border-l-4 border-green-500 text-green-700 p-4 rounded-lg shadow-lg z-50 max-w-sm';
notification.innerHTML = `
<div class="flex items-center">
<div class="flex-shrink-0">
<i class="fas fa-check-circle text-green-500"></i>
</div>
<div class="ml-3">
<p class="text-sm font-medium">Session Extended</p>
<p class="text-xs mt-1">Your session has been refreshed successfully.</p>
</div>
</div>
`;
document.body.appendChild(notification);
setTimeout(() => notification.remove(), 3000);
}
function setupActivityMonitoring() {
let lastActivity = Date.now();
let warningShown = false;
let inactivityWarningMinutes = 240; // default 4 hours
const inactivityGraceMinutes = 5; // auto-logout after warning + 5 minutes
// Fetch setting (best effort)
getInactivityWarningMinutes().then(minutes => {
if (Number.isFinite(minutes) && minutes > 0) {
inactivityWarningMinutes = minutes;
}
}).catch(() => {});
function hideInactivityWarning() {
const el = document.getElementById('inactivity-warning');
if (el && el.remove) el.remove();
}
function extendSession() {
refreshTokenIfNeeded();
hideInactivityWarning();
showSessionExtendedNotification();
}
function showInactivityWarning() {
hideInactivityWarning();
const msg = `You've been inactive. Your session may expire due to inactivity.`;
if (window.alerts && typeof window.alerts.show === 'function') {
window.alerts.show(msg, 'warning', {
title: 'Session Warning',
html: false,
duration: 0,
dismissible: true,
id: 'inactivity-warning',
actions: [
{
label: 'Stay Logged In',
classes: 'bg-warning-600 hover:bg-warning-700 text-white text-xs px-3 py-1 rounded',
onClick: () => extendSession(),
autoClose: true
},
{
label: 'Dismiss',
classes: 'bg-neutral-200 hover:bg-neutral-300 text-neutral-800 dark:bg-neutral-700 dark:hover:bg-neutral-600 dark:text-neutral-200 text-xs px-3 py-1 rounded',
onClick: () => hideInactivityWarning(),
autoClose: true
}
]
});
} else {
alert('Session Warning: ' + msg);
}
// Auto-hide after 2 minutes
setTimeout(() => hideInactivityWarning(), 2 * 60 * 1000);
}
// Track user activity
const activityEvents = ['mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart'];
activityEvents.forEach(event => {
document.addEventListener(event, () => {
lastActivity = Date.now();
warningShown = false;
const el = document.getElementById('inactivity-warning');
if (el && el.remove) el.remove();
});
});
// Check every 5 minutes for inactivity
setInterval(() => {
const now = Date.now();
const warningMs = inactivityWarningMinutes * 60 * 1000;
const logoutMs = (inactivityWarningMinutes + inactivityGraceMinutes) * 60 * 1000;
const timeSinceActivity = now - lastActivity;
if (timeSinceActivity > warningMs && !warningShown) {
showInactivityWarning();
warningShown = true;
}
if (timeSinceActivity > logoutMs) {
logout('Session expired due to inactivity');
}
}, 5 * 60 * 1000);
}
// Central initializer for auth
async function initializeAuthManager() {
const token = localStorage.getItem('auth_token');
// If on the login page, do nothing
if (isLoginPage()) return;
if (token) {
// Align in-memory/app state with stored tokens
app.token = token;
const storedRefresh = localStorage.getItem('refresh_token');
if (storedRefresh) app.refreshToken = storedRefresh;
// Verify token and schedule refresh
checkTokenValidity();
scheduleTokenRefresh();
// Start inactivity monitoring
setupActivityMonitoring();
// Update UI according to user permissions
checkUserPermissions();
} else {
// No token and not on login page - redirect to login
window.location.href = '/login';
}
}
async function logout(reason = null) {
// Best-effort revoke refresh token server-side
const rtoken = localStorage.getItem('refresh_token');
try {
if (rtoken) {
await fetch('/api/auth/logout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refresh_token: rtoken })
});
}
} catch (_) {
// ignore
}
if (app.refreshTimerId) {
clearTimeout(app.refreshTimerId);
app.refreshTimerId = null;
@@ -176,7 +530,12 @@ function logout() {
app.token = null;
app.user = null;
localStorage.removeItem('auth_token');
localStorage.removeItem('refresh_token');
delete window.apiHeaders['Authorization'];
if (reason) {
try { sessionStorage.setItem('logout_reason', reason); } catch (_) {}
}
window.location.href = '/login';
}
@@ -404,6 +763,70 @@ window.apiPut = apiPut;
window.apiDelete = apiDelete;
window.formatCurrency = formatCurrency;
window.formatDate = formatDate;
window.toggleTheme = toggleTheme;
window.initializeTheme = initializeTheme;
window.saveThemePreference = saveThemePreference;
window.loadUserThemePreference = loadUserThemePreference;
// Global error handling
function setupGlobalErrorHandlers() {
// Handle unexpected runtime errors
window.addEventListener('error', function(event) {
try {
const payload = {
message: event && event.message ? String(event.message) : 'Unhandled error',
action: 'window.onerror',
stack: event && event.error && event.error.stack ? String(event.error.stack) : null,
url: (event && event.filename) ? String(event.filename) : String(window.location.href),
line: event && typeof event.lineno === 'number' ? event.lineno : null,
column: event && typeof event.colno === 'number' ? event.colno : null,
user_agent: navigator.userAgent,
extra: {
page: window.location.pathname,
lastCorrelationId: (window.app && window.app.lastCorrelationId) || null
}
};
postClientError(payload);
} catch (_) {}
});
// Handle unhandled promise rejections
window.addEventListener('unhandledrejection', function(event) {
try {
const reason = event && event.reason ? event.reason : null;
const payload = {
message: reason && reason.message ? String(reason.message) : 'Unhandled promise rejection',
action: 'window.unhandledrejection',
stack: reason && reason.stack ? String(reason.stack) : null,
url: String(window.location.href),
user_agent: navigator.userAgent,
extra: {
page: window.location.pathname,
reasonType: reason ? (reason.name || typeof reason) : null,
status: reason && typeof reason.status === 'number' ? reason.status : null,
correlationId: reason && reason.correlationId ? reason.correlationId : ((window.app && window.app.lastCorrelationId) || null)
}
};
postClientError(payload);
} catch (_) {}
});
}
async function postClientError(payload) {
try {
const headers = { 'Content-Type': 'application/json', 'Accept': 'application/json' };
const token = (window.app && window.app.token) || localStorage.getItem('auth_token');
if (token) headers['Authorization'] = `Bearer ${token}`;
// Fire-and-forget; do not block UI
fetch('/api/documents/client-error', {
method: 'POST',
headers,
body: JSON.stringify(payload)
}).catch(() => {});
} catch (_) {
// no-op
}
}
// JWT utilities and refresh handling
function decodeJwt(token) {
@@ -448,13 +871,14 @@ function scheduleTokenRefresh() {
}
async function refreshToken() {
if (!app.token) throw new Error('No token to refresh');
if (!app.refreshToken) throw new Error('No refresh token available');
if (app.refreshInProgress) return; // Avoid parallel refreshes
app.refreshInProgress = true;
try {
const response = await fetch('/api/auth/refresh', {
method: 'POST',
headers: { ...window.apiHeaders }
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
body: JSON.stringify({ refresh_token: app.refreshToken })
});
if (!response.ok) {
throw new Error('Refresh failed');
@@ -463,7 +887,8 @@ async function refreshToken() {
if (!data || !data.access_token) {
throw new Error('Invalid refresh response');
}
setAuthToken(data.access_token);
// Handle refresh token rotation if provided
setAuthTokens(data.access_token, data.refresh_token || null);
} finally {
app.refreshInProgress = false;
}