// Logo Customization System (function() { // Inject modal HTML injectModal('logo-modal', `

Dashboard Settings

Customize your dashboard's appearance and system preferences.

Shown in browser tab and header (max 50 characters)


Separate logos for dark and light themes, or use the same for both.

Dark theme logo

Dark themes

Light theme logo

Light themes

Using default logos


Current favicon Using DashCaddy favicon

Upload PNG or SVG - automatically converted to ICO


Used by all deployed containers. Changes apply to new deployments.

`); const logoModal = document.getElementById('logo-modal'); const previewDark = document.getElementById('logo-preview-dark'); const previewLight = document.getElementById('logo-preview-light'); const logoStatus = document.getElementById('logo-status'); const sameBothCheckbox = document.getElementById('logo-same-both'); const dualUploads = document.getElementById('logo-dual-uploads'); const singleUpload = document.getElementById('logo-single-upload'); const uploadDark = document.getElementById('logo-upload-dark'); const uploadLight = document.getElementById('logo-upload-light'); const uploadSingle = document.getElementById('logo-upload-single'); const brandLogoDark = document.querySelector('#brand .brand-logo-dark'); const brandLogoLight = document.querySelector('#brand .brand-logo-light'); const topRow = document.querySelector('.top-row'); const dashboardTitleInput = document.getElementById('dashboard-title'); const DEFAULT_TITLE = DC.NAME; let pendingDarkData = null; let pendingLightData = null; let pendingSingleData = null; let currentPosition = 'left'; let currentTitle = DEFAULT_TITLE; // Toggle between dual and single upload mode sameBothCheckbox?.addEventListener('change', () => { if (sameBothCheckbox.checked) { dualUploads.style.display = 'none'; singleUpload.style.display = ''; // Clear individual pending data pendingDarkData = null; pendingLightData = null; } else { dualUploads.style.display = 'flex'; singleUpload.style.display = 'none'; pendingSingleData = null; } }); // Read file as data URL helper function readFileAsDataURL(file, callback) { if (!file || !file.type.startsWith('image/')) { showNotification('Please select an image file', 'warning'); return; } const reader = new FileReader(); reader.onload = (e) => callback(e.target.result); reader.readAsDataURL(file); } // Upload handlers uploadDark?.addEventListener('change', (e) => { readFileAsDataURL(e.target.files[0], (data) => { pendingDarkData = data; previewDark.src = data; logoStatus.textContent = 'New dark logo ready to save'; }); }); uploadLight?.addEventListener('change', (e) => { readFileAsDataURL(e.target.files[0], (data) => { pendingLightData = data; previewLight.src = data; logoStatus.textContent = 'New light logo ready to save'; }); }); uploadSingle?.addEventListener('change', (e) => { readFileAsDataURL(e.target.files[0], (data) => { pendingSingleData = data; previewDark.src = data; previewLight.src = data; logoStatus.textContent = 'New logo ready to save (both themes)'; }); }); // Apply logo position function applyLogoPosition(pos) { topRow.setAttribute('data-logo-pos', pos); document.querySelectorAll('.logo-pos-btn').forEach(btn => { btn.style.background = btn.dataset.pos === pos ? 'var(--accent)' : 'var(--card-bg)'; btn.style.color = btn.dataset.pos === pos ? 'white' : 'var(--fg)'; }); } // Apply dashboard title function applyDashboardTitle(title) { currentTitle = title || DEFAULT_TITLE; document.title = currentTitle; const headerTitle = document.querySelector('.dashboard-title'); if (headerTitle) headerTitle.textContent = currentTitle; } // Load custom logo, position, and title on startup async function loadCustomLogo() { try { const response = await fetch('/api/v1/logo'); if (response.ok) { const data = await response.json(); // Apply dark/light variants if (data.customLogoDark) { brandLogoDark.src = data.customLogoDark; previewDark.src = data.customLogoDark; } if (data.customLogoLight) { brandLogoLight.src = data.customLogoLight; previewLight.src = data.customLogoLight; } // Legacy single-logo fallback if (!data.customLogoDark && !data.customLogoLight && data.customLogo) { brandLogoDark.src = data.customLogo; brandLogoLight.src = data.customLogo; previewDark.src = data.customLogo; previewLight.src = data.customLogo; } if (!data.isDefault) { logoStatus.textContent = 'Using custom logo'; } if (data.position) { currentPosition = data.position; applyLogoPosition(data.position); } if (data.dashboardTitle) { applyDashboardTitle(data.dashboardTitle); } } } catch (error) { console.warn('Could not load custom logo:', error.message); } } // Position button handlers document.querySelectorAll('.logo-pos-btn').forEach(btn => { btn.addEventListener('click', () => { currentPosition = btn.dataset.pos; applyLogoPosition(currentPosition); }); }); // Open logo modal on brand click document.getElementById('brand')?.addEventListener('click', () => { pendingDarkData = null; pendingLightData = null; pendingSingleData = null; if (uploadDark) uploadDark.value = ''; if (uploadLight) uploadLight.value = ''; if (uploadSingle) uploadSingle.value = ''; // Reset checkbox to dual mode if (sameBothCheckbox) sameBothCheckbox.checked = false; dualUploads.style.display = 'flex'; singleUpload.style.display = 'none'; // Reset previews to current header logos previewDark.src = brandLogoDark.src; previewLight.src = brandLogoLight.src; const isCustom = brandLogoDark.src.includes('custom-logo') || brandLogoLight.src.includes('custom-logo'); logoStatus.textContent = isCustom ? 'Using custom logo' : 'Using default logos'; applyLogoPosition(currentPosition); dashboardTitleInput.value = currentTitle; logoModal.classList.add('show'); }); // Save logo, position, and title document.getElementById('logo-save')?.addEventListener('click', async () => { try { const newTitle = dashboardTitleInput.value.trim() || DEFAULT_TITLE; const payload = { position: currentPosition, dashboardTitle: newTitle }; // Determine which logo data to send if (sameBothCheckbox?.checked && pendingSingleData) { // Single logo for both variants payload.dataDark = pendingSingleData; payload.dataLight = pendingSingleData; } else { if (pendingDarkData) payload.dataDark = pendingDarkData; if (pendingLightData) payload.dataLight = pendingLightData; } const response = await secureFetch('/api/v1/logo', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (response.ok) { const data = await response.json(); const t = '?t=' + Date.now(); if (data.pathDark) { brandLogoDark.src = data.pathDark + t; previewDark.src = data.pathDark + t; } if (data.pathLight) { brandLogoLight.src = data.pathLight + t; previewLight.src = data.pathLight + t; } applyLogoPosition(currentPosition); applyDashboardTitle(newTitle); logoModal.classList.remove('show'); } else { const error = await response.json(); showNotification('Failed to save: ' + error.error, 'error'); } } catch (error) { showNotification('Error saving: ' + error.message, 'error'); } }); // Reset all branding to defaults document.getElementById('logo-reset')?.addEventListener('click', async () => { if (!confirm('Reset all branding to DashCaddy defaults?\n\nThis will reset the logo, favicon, title, and position.')) return; try { const logoResponse = await secureFetch('/api/v1/logo', { method: 'DELETE' }); if (logoResponse.ok) { brandLogoDark.src = '/assets/dashcaddy-logo-dark.png'; brandLogoLight.src = '/assets/dashcaddy-logo-light.png'; previewDark.src = '/assets/dashcaddy-logo-dark.png'; previewLight.src = '/assets/dashcaddy-logo-light.png'; logoStatus.textContent = 'Using default logos'; pendingDarkData = null; pendingLightData = null; pendingSingleData = null; dashboardTitleInput.value = DEFAULT_TITLE; applyDashboardTitle(DEFAULT_TITLE); currentPosition = 'left'; applyLogoPosition('left'); } const faviconResponse = await secureFetch('/api/v1/favicon', { method: 'DELETE' }); if (faviconResponse.ok) { const faviconLink = document.querySelector('link[rel="icon"]'); const faviconPreview = document.getElementById('favicon-preview'); const faviconStatus = document.getElementById('favicon-status'); if (faviconLink) faviconLink.href = '/assets/dashcaddy-favicon.ico?t=' + Date.now(); if (faviconPreview) faviconPreview.src = '/assets/dashcaddy-favicon.ico?t=' + Date.now(); if (faviconStatus) faviconStatus.textContent = 'Using DashCaddy favicon'; pendingFaviconData = null; } } catch (error) { showNotification('Error resetting branding: ' + error.message, 'error'); } }); wireModal(logoModal, document.getElementById('logo-cancel')); // ===== FAVICON HANDLING ===== const faviconPreview = document.getElementById('favicon-preview'); const faviconStatus = document.getElementById('favicon-status'); const faviconUpload = document.getElementById('favicon-upload'); const faviconLink = document.querySelector('link[rel="icon"]') || document.createElement('link'); let pendingFaviconData = null; if (!document.querySelector('link[rel="icon"]')) { faviconLink.rel = 'icon'; faviconLink.href = '/assets/dashcaddy-favicon.ico'; document.head.appendChild(faviconLink); } async function loadCustomFavicon() { try { const response = await fetch('/api/v1/favicon'); if (response.ok) { const data = await response.json(); if (data.customFavicon) { faviconLink.href = data.customFavicon + '?t=' + Date.now(); faviconPreview.src = data.customFavicon + '?t=' + Date.now(); faviconStatus.textContent = 'Using custom favicon'; } } } catch (error) { console.warn('Could not load custom favicon:', error.message); } } faviconUpload?.addEventListener('change', (e) => { const file = e.target.files[0]; if (!file) return; if (!file.type.match(/^image\/(png|svg\+xml)$/)) { showNotification('Please select a PNG or SVG file', 'warning'); faviconUpload.value = ''; return; } const reader = new FileReader(); reader.onload = (event) => { pendingFaviconData = event.target.result; faviconPreview.src = pendingFaviconData; faviconStatus.textContent = 'New favicon ready to save'; }; reader.readAsDataURL(file); }); // Save favicon alongside logo save document.getElementById('logo-save')?.addEventListener('click', async () => { if (pendingFaviconData) { try { const response = await secureFetch('/api/v1/favicon', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ data: pendingFaviconData }) }); if (response.ok) { const data = await response.json(); faviconLink.href = data.path + '?t=' + Date.now(); faviconPreview.src = data.path + '?t=' + Date.now(); faviconStatus.textContent = 'Using custom favicon'; pendingFaviconData = null; } else { const error = await response.json(); showNotification('Failed to save favicon: ' + error.error, 'error'); } } catch (error) { showNotification('Error saving favicon: ' + error.message, 'error'); } } }); loadCustomFavicon(); loadCustomLogo(); // ===== TIMEZONE SETTING ===== const settingsTzSelect = document.getElementById('settings-timezone'); if (settingsTzSelect) { const observer = new MutationObserver(() => { if (logoModal.classList.contains('show') && settingsTzSelect.options.length === 0) { (async () => { let currentTz; try { const res = await fetch('/api/v1/config'); if (res.ok) { const cfg = await res.json(); currentTz = cfg.timezone; } } catch (e) { /* ignore */ } window.populateTimezoneSelect(settingsTzSelect, currentTz); })(); } }); observer.observe(logoModal, { attributes: true, attributeFilter: ['class'] }); document.getElementById('logo-save')?.addEventListener('click', async () => { const tz = settingsTzSelect.value; if (!tz) return; try { const res = await fetch('/api/v1/config'); if (!res.ok) return; const cfg = await res.json(); cfg.timezone = tz; cfg.updatedAt = new Date().toISOString(); await secureFetch('/api/v1/config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(cfg) }); } catch (e) { console.warn('Failed to save timezone:', e.message); } }); } })();