// Quick Settings Panel - Centralized configuration for all UI features // Accessible via gear icon in header import { apiService } from '../services/api.service.js'; const API_TOKEN_STORAGE_KEY = 'ruview-api-token'; export class QuickSettings { constructor(app) { this.app = app; this.button = null; this.panel = null; this.isOpen = false; } init() { this.applyStoredApiToken(); this.createButton(); this.createPanel(); } // Apply a previously-saved bearer token to apiService as early as // possible, before any tab's REST calls fire. The server only ever // checks the `Authorization: Bearer` header (see bearer_auth.rs) — this // intentionally never puts the token in a URL query string. applyStoredApiToken() { let token = null; try { token = localStorage.getItem(API_TOKEN_STORAGE_KEY); } catch { /* noop */ } if (token) apiService.setAuthToken(token); } createButton() { this.button = document.createElement('button'); this.button.className = 'settings-gear'; this.button.setAttribute('aria-label', 'Settings'); this.button.setAttribute('title', 'Quick settings'); this.button.innerHTML = ``; this.button.addEventListener('click', () => this.toggle()); const headerInfo = document.querySelector('.header-info'); if (headerInfo) headerInfo.appendChild(this.button); } createPanel() { this.panel = document.createElement('div'); this.panel.className = 'quick-settings-panel'; this.panel.setAttribute('role', 'dialog'); this.panel.setAttribute('aria-label', 'Quick settings'); this.panel.innerHTML = `

Settings

Display
Monitoring
API Access
Bearer token (set only if the server enforces RUVIEW_API_TOKEN)
Data
Clear local data
Reset onboarding
`; // Bind events this.panel.querySelector('.qs-close').addEventListener('click', () => this.close()); this.panel.querySelector('#qs-reduced-motion').addEventListener('change', (e) => { document.body.classList.toggle('reduced-motion', e.target.checked); this.saveSetting('reduced-motion', e.target.checked); }); this.panel.querySelector('#qs-high-contrast').addEventListener('change', (e) => { document.body.classList.toggle('high-contrast', e.target.checked); this.saveSetting('high-contrast', e.target.checked); }); this.panel.querySelector('#qs-compact').addEventListener('change', (e) => { document.body.classList.toggle('compact-mode', e.target.checked); this.saveSetting('compact', e.target.checked); }); this.panel.querySelector('#qs-health-polling').addEventListener('change', (e) => { const healthService = this.app?.components?.dashboard?.healthSubscription; if (e.target.checked) { // Resume would need import - just dispatch event document.dispatchEvent(new CustomEvent('health-polling-toggle', { detail: true })); } else { document.dispatchEvent(new CustomEvent('health-polling-toggle', { detail: false })); } }); this.panel.querySelector('#qs-api-token-save').addEventListener('click', () => { const input = this.panel.querySelector('#qs-api-token'); const status = this.panel.querySelector('#qs-api-token-status'); const token = input.value.trim(); if (!token) { status.textContent = 'Enter a token first, or use Clear to remove one.'; return; } try { localStorage.setItem(API_TOKEN_STORAGE_KEY, token); } catch { /* noop */ } apiService.setAuthToken(token); status.textContent = 'Token saved and applied. Reloading...'; setTimeout(() => window.location.reload(), 600); }); this.panel.querySelector('#qs-api-token-clear').addEventListener('click', () => { const input = this.panel.querySelector('#qs-api-token'); const status = this.panel.querySelector('#qs-api-token-status'); try { localStorage.removeItem(API_TOKEN_STORAGE_KEY); } catch { /* noop */ } apiService.setAuthToken(null); input.value = ''; status.textContent = 'Token cleared. Reloading...'; setTimeout(() => window.location.reload(), 600); }); this.panel.querySelector('#qs-clear-data').addEventListener('click', () => { try { localStorage.clear(); sessionStorage.clear(); } catch { /* noop */ } this.close(); window.location.reload(); }); this.panel.querySelector('#qs-reset-tour').addEventListener('click', () => { try { localStorage.removeItem('ruview-onboarding-done'); } catch { /* noop */ } this.close(); document.dispatchEvent(new CustomEvent('start-onboarding')); }); document.body.appendChild(this.panel); // Close on outside click document.addEventListener('click', (e) => { if (this.isOpen && !this.panel.contains(e.target) && !this.button.contains(e.target)) { this.close(); } }); // Apply saved settings on init this.applySavedSettings(); } applySavedSettings() { if (this.getSetting('reduced-motion') || this.prefersReducedMotion()) { document.body.classList.add('reduced-motion'); const cb = this.panel.querySelector('#qs-reduced-motion'); if (cb) cb.checked = true; } if (this.getSetting('high-contrast')) { document.body.classList.add('high-contrast'); const cb = this.panel.querySelector('#qs-high-contrast'); if (cb) cb.checked = true; } if (this.getSetting('compact')) { document.body.classList.add('compact-mode'); } const status = this.panel.querySelector('#qs-api-token-status'); let hasToken = false; try { hasToken = !!localStorage.getItem(API_TOKEN_STORAGE_KEY); } catch { /* noop */ } if (status) status.textContent = hasToken ? 'A token is currently set.' : 'No token set (auth is off or unnecessary).'; } prefersReducedMotion() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; } toggle() { this.isOpen ? this.close() : this.open(); } open() { this.isOpen = true; this.panel.classList.add('open'); } close() { this.isOpen = false; this.panel.classList.remove('open'); } getSetting(key) { try { return JSON.parse(localStorage.getItem(`ruview-setting-${key}`)); } catch { return null; } } saveSetting(key, value) { try { localStorage.setItem(`ruview-setting-${key}`, JSON.stringify(value)); } catch { /* noop */ } } dispose() { this.button?.remove(); this.panel?.remove(); } }