// 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 = `
${escapeHtml(message)}
`;
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
};
})();