botserver/ui/suite/js/htmx-app.js

286 lines
9.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Minimal HTMX Application Initialization
// Pure HTMX-based with no external dependencies except HTMX itself
(function() {
'use strict';
// Configuration
const config = {
sessionRefreshInterval: 15 * 60 * 1000, // 15 minutes
tokenKey: 'auth_token',
themeKey: 'app_theme'
};
// Initialize HTMX settings
function initHTMX() {
// Configure HTMX
htmx.config.defaultSwapStyle = 'innerHTML';
htmx.config.defaultSettleDelay = 100;
htmx.config.timeout = 10000;
htmx.config.scrollBehavior = 'smooth';
// Add authentication token to all requests
document.body.addEventListener('htmx:configRequest', (event) => {
// Get token from cookie (httpOnly cookies are automatically sent)
// For additional security, we can also check localStorage
const token = localStorage.getItem(config.tokenKey);
if (token) {
event.detail.headers['Authorization'] = `Bearer ${token}`;
}
});
// Handle authentication errors
document.body.addEventListener('htmx:responseError', (event) => {
if (event.detail.xhr.status === 401) {
// Unauthorized - redirect to login
window.location.href = '/login?redirect=' + encodeURIComponent(window.location.pathname);
} else if (event.detail.xhr.status === 403) {
// Forbidden - show error
showNotification('Access denied', 'error');
}
});
// Handle successful responses
document.body.addEventListener('htmx:afterSwap', (event) => {
// Auto-initialize any new HTMX elements
htmx.process(event.detail.target);
// Trigger any custom events
if (event.detail.target.dataset.afterSwap) {
htmx.trigger(event.detail.target, event.detail.target.dataset.afterSwap);
}
});
// Handle redirects
document.body.addEventListener('htmx:beforeSwap', (event) => {
if (event.detail.xhr.getResponseHeader('HX-Redirect')) {
event.detail.shouldSwap = false;
window.location.href = event.detail.xhr.getResponseHeader('HX-Redirect');
}
});
}
// Theme management
function initTheme() {
// Get saved theme or default to system preference
const savedTheme = localStorage.getItem(config.themeKey) ||
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme', savedTheme);
// Listen for theme changes
document.body.addEventListener('theme-changed', (event) => {
const newTheme = event.detail.theme ||
(document.documentElement.getAttribute('data-theme') === 'light' ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme', newTheme);
localStorage.setItem(config.themeKey, newTheme);
// Update theme icons
document.querySelectorAll('[data-theme-icon]').forEach(icon => {
icon.textContent = newTheme === 'light' ? '🌙' : '☀️';
});
});
}
// Session management
function initSession() {
// Check session validity on page load
checkSession();
// Periodically refresh token
setInterval(refreshToken, config.sessionRefreshInterval);
// Check session before page unload
window.addEventListener('beforeunload', () => {
// Save any pending data
htmx.trigger(document.body, 'save-pending');
});
}
// Check if user session is valid
async function checkSession() {
try {
const response = await fetch('/api/auth/check');
const data = await response.json();
if (!data.authenticated && !isPublicPath()) {
window.location.href = '/login?redirect=' + encodeURIComponent(window.location.pathname);
}
} catch (err) {
console.error('Session check failed:', err);
}
}
// Refresh authentication token
async function refreshToken() {
if (!isPublicPath()) {
try {
const response = await fetch('/api/auth/refresh', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
if (response.ok) {
const data = await response.json();
if (data.refreshed && data.token) {
localStorage.setItem(config.tokenKey, data.token);
}
} else if (response.status === 401) {
// Token expired, redirect to login
window.location.href = '/login?redirect=' + encodeURIComponent(window.location.pathname);
}
} catch (err) {
console.error('Token refresh failed:', err);
}
}
}
// Check if current path is public (doesn't require auth)
function isPublicPath() {
const publicPaths = ['/login', '/logout', '/auth/callback', '/health', '/register', '/forgot-password'];
const currentPath = window.location.pathname;
return publicPaths.some(path => currentPath.startsWith(path));
}
// Show notification
function showNotification(message, type = 'info') {
const container = document.getElementById('notifications') || createNotificationContainer();
const notification = document.createElement('div');
notification.className = `notification ${type}`;
notification.innerHTML = `
<span class="notification-message">${escapeHtml(message)}</span>
<button class="notification-close" onclick="this.parentElement.remove()">×</button>
`;
container.appendChild(notification);
// Auto-dismiss after 5 seconds
setTimeout(() => {
notification.classList.add('fade-out');
setTimeout(() => notification.remove(), 300);
}, 5000);
}
// Create notification container if it doesn't exist
function createNotificationContainer() {
const container = document.createElement('div');
container.id = 'notifications';
container.className = 'notifications-container';
document.body.appendChild(container);
return container;
}
// Escape HTML to prevent XSS
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Handle keyboard shortcuts
function initKeyboardShortcuts() {
document.addEventListener('keydown', (e) => {
// Ctrl/Cmd + K - Quick search
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault();
const searchInput = document.querySelector('[data-search-input]');
if (searchInput) {
searchInput.focus();
}
}
// Escape - Close modals
if (e.key === 'Escape') {
const modal = document.querySelector('.modal.active');
if (modal) {
htmx.trigger(modal, 'close-modal');
}
}
});
}
// Handle form validation
function initFormValidation() {
document.addEventListener('htmx:validateUrl', (event) => {
// Custom URL validation if needed
return true;
});
document.addEventListener('htmx:beforeRequest', (event) => {
// Add loading state to forms
const form = event.target.closest('form');
if (form) {
form.classList.add('loading');
// Disable submit buttons
form.querySelectorAll('[type="submit"]').forEach(btn => {
btn.disabled = true;
});
}
});
document.addEventListener('htmx:afterRequest', (event) => {
// Remove loading state from forms
const form = event.target.closest('form');
if (form) {
form.classList.remove('loading');
// Re-enable submit buttons
form.querySelectorAll('[type="submit"]').forEach(btn => {
btn.disabled = false;
});
}
});
}
// Initialize offline detection
function initOfflineDetection() {
window.addEventListener('online', () => {
document.body.classList.remove('offline');
showNotification('Connection restored', 'success');
// Retry any pending requests
htmx.trigger(document.body, 'retry-pending');
});
window.addEventListener('offline', () => {
document.body.classList.add('offline');
showNotification('No internet connection', 'warning');
});
}
// Main initialization
function init() {
console.log('Initializing HTMX application...');
// Initialize core features
initHTMX();
initTheme();
initSession();
initKeyboardShortcuts();
initFormValidation();
initOfflineDetection();
// Mark app as initialized
document.body.classList.add('app-initialized');
console.log('Application initialized successfully');
}
// Wait for DOM to be ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
// DOM is already ready
init();
}
// Expose public API for other scripts if needed
window.BotServerApp = {
showNotification,
checkSession,
refreshToken,
config
};
})();