From 7bbd969fa2824a058d4636c3d56ce00289427b82 Mon Sep 17 00:00:00 2001 From: Hermes Date: Thu, 18 Jun 2026 19:23:30 -0700 Subject: [PATCH] fix: rebuild bundle with widget, restore TOTP across container recreate, integrate auto-updater changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three logical changes grouped: 1. Widget bundle rebuild + sami-files logo (from previous session) - status/dist/{init,core,features,onboarding}.js rebuilt from latest source - status/sw.js cache bumped to dashcaddy-shell-594ec75648 to force SW refresh - status/assets/sami-files.png added (Sami Files service card logo) 2. status/build.js: include monitoring-widgets.js in bundle - The original build.js was missing monitoring-widgets.js from its JS() bundle list — that's why the System Overview widget never showed up in the live init.js until we ran the live /var/www/dashcaddy-status/ build.js. Now consistent. 3. dashcaddy-api/scripts/dashcaddy-update.sh restart_container(): preserve TOTP secret across container recreates - Was only setting SERVICES_FILE; container fell back to image-local /app/credentials.json + /app/.encryption-key (auto-generated fresh every recreate), which broke TOTP for the bind-mounted secret at /app/data/credentials.json - Added CREDENTIALS_FILE + ENCRYPTION_KEY_FILE env vars pointing at /app/data/ so the container reads from the bind-mounted host data dir - See skill: software-development/dashcaddy/references/totp-and-system-overview-pitfalls.md §9 4. Auto-updater integration (pulled from upstream release): - dashcaddy-api/VERSION: dev → c64bbe2 - dashcaddy-api/health-checker.js, middleware.js, package.json, routes/backups.js, src/app.js: new release code (bundled workflows, /api/auth/ → /api/v1/ back-compat rewrite, backup storage limits) --- dashcaddy-api/VERSION | 2 +- dashcaddy-api/health-checker.js | 31 +- dashcaddy-api/middleware.js | 9 + dashcaddy-api/package.json | 2 +- dashcaddy-api/routes/backups.js | 180 ++++++- dashcaddy-api/scripts/dashcaddy-update.sh | 7 +- dashcaddy-api/src/app.js | 86 +++- status/assets/sami-files.png | Bin 0 -> 6106 bytes status/build.js | 1 + status/dist/core.js | 201 ++++---- status/dist/features.js | 541 ++++++++++++---------- status/dist/init.js | 135 +++++- status/dist/onboarding.js | 6 +- status/sw.js | 2 +- 14 files changed, 844 insertions(+), 359 deletions(-) create mode 100644 status/assets/sami-files.png diff --git a/dashcaddy-api/VERSION b/dashcaddy-api/VERSION index 38f8e88..8ee696e 100644 --- a/dashcaddy-api/VERSION +++ b/dashcaddy-api/VERSION @@ -1 +1 @@ -dev +c64bbe2 diff --git a/dashcaddy-api/health-checker.js b/dashcaddy-api/health-checker.js index 6327d26..270a427 100644 --- a/dashcaddy-api/health-checker.js +++ b/dashcaddy-api/health-checker.js @@ -9,9 +9,24 @@ const http = require('http'); const EventEmitter = require('events'); const fs = require('fs'); const path = require('path'); +const paths = require('./platform-paths'); -const HEALTH_CONFIG_FILE = process.env.HEALTH_CONFIG_FILE || path.join(__dirname, 'health-config.json'); -const HEALTH_HISTORY_FILE = process.env.HEALTH_HISTORY_FILE || path.join(__dirname, 'health-history.json'); +// Persist health config + history alongside the other state files (services.json, +// config.json) rather than next to the source. In a container that data dir is the +// mounted /app/data volume, so uptime history survives container recreates/updates; +// previously these defaulted to __dirname (unmounted /app) and every recreate wiped +// the accumulated history, blanking the dashboard uptime bars. Explicit env vars +// still override. +const HEALTH_DATA_DIR = process.env.HEALTH_DATA_DIR || path.dirname(paths.configFile); +const HEALTH_CONFIG_FILE = process.env.HEALTH_CONFIG_FILE || path.join(HEALTH_DATA_DIR, 'health-config.json'); +const HEALTH_HISTORY_FILE = process.env.HEALTH_HISTORY_FILE || path.join(HEALTH_DATA_DIR, 'health-history.json'); + +// Legacy locations (next to the source) used before the data-dir default. Read these +// once on first load if the new files are absent, so upgrading installs migrate their +// accumulated history/config instead of starting empty. The next save() rewrites to +// the new location. +const LEGACY_HEALTH_CONFIG_FILE = path.join(__dirname, 'health-config.json'); +const LEGACY_HEALTH_HISTORY_FILE = path.join(__dirname, 'health-history.json'); const CHECK_INTERVAL = parseInt(process.env.HEALTH_CHECK_INTERVAL || '30000', 10); // 30 seconds const MAX_CHECK_INTERVAL = parseInt(process.env.HEALTH_CHECK_MAX_INTERVAL || '300000', 10); // 5 minutes max backoff const HISTORY_RETENTION_DAYS = parseInt(process.env.HEALTH_HISTORY_RETENTION || '30', 10); @@ -541,8 +556,10 @@ class HealthChecker extends EventEmitter { */ loadConfig() { try { - if (fs.existsSync(HEALTH_CONFIG_FILE)) { - return JSON.parse(fs.readFileSync(HEALTH_CONFIG_FILE, 'utf8')); + const file = fs.existsSync(HEALTH_CONFIG_FILE) ? HEALTH_CONFIG_FILE + : (HEALTH_CONFIG_FILE !== LEGACY_HEALTH_CONFIG_FILE && fs.existsSync(LEGACY_HEALTH_CONFIG_FILE) ? LEGACY_HEALTH_CONFIG_FILE : null); + if (file) { + return JSON.parse(fs.readFileSync(file, 'utf8')); } } catch (error) { this.emit('log', 'error', `Error loading config: ${error.message}`); @@ -566,8 +583,10 @@ class HealthChecker extends EventEmitter { */ loadHistory() { try { - if (fs.existsSync(HEALTH_HISTORY_FILE)) { - return JSON.parse(fs.readFileSync(HEALTH_HISTORY_FILE, 'utf8')); + const file = fs.existsSync(HEALTH_HISTORY_FILE) ? HEALTH_HISTORY_FILE + : (HEALTH_HISTORY_FILE !== LEGACY_HEALTH_HISTORY_FILE && fs.existsSync(LEGACY_HEALTH_HISTORY_FILE) ? LEGACY_HEALTH_HISTORY_FILE : null); + if (file) { + return JSON.parse(fs.readFileSync(file, 'utf8')); } } catch (error) { this.emit('log', 'error', `Error loading history: ${error.message}`); diff --git a/dashcaddy-api/middleware.js b/dashcaddy-api/middleware.js index 9cc878d..e441570 100644 --- a/dashcaddy-api/middleware.js +++ b/dashcaddy-api/middleware.js @@ -304,6 +304,15 @@ module.exports = function configureMiddleware(app, { { path: '/api/v1/license/feature/', prefix: true, method: 'GET' }, { path: '/api/v1/config', exact: true, method: 'GET' }, { path: '/api/v1/services/status', exact: true, method: 'GET' }, + { path: '/api/v1/health-checks/status', exact: true, method: 'GET' }, + // Read-only update/version info shown on the dashboard view (verification + // modal, topbar version, update badges). Mutating actions — update-apply, + // rollback (POST) — are NOT listed here and stay TOTP-protected. + { path: '/api/v1/system/version', exact: true, method: 'GET' }, + { path: '/api/v1/system/update-status', exact: true, method: 'GET' }, + { path: '/api/v1/system/update-history', exact: true, method: 'GET' }, + { path: '/api/v1/system/update-check', exact: true, method: 'GET' }, + { path: '/api/v1/updates/available', exact: true, method: 'GET' }, { path: '/api/v1/system/update-notify', exact: true, method: 'POST' }, ]; diff --git a/dashcaddy-api/package.json b/dashcaddy-api/package.json index 20189fe..13a4ddc 100644 --- a/dashcaddy-api/package.json +++ b/dashcaddy-api/package.json @@ -1,6 +1,6 @@ { "name": "dashcaddy-api", - "version": "1.6.0", + "version": "1.7.8", "description": "DashCaddy API server - Dashboard backend for Docker, Caddy & DNS management", "main": "server.js", "scripts": { diff --git a/dashcaddy-api/routes/backups.js b/dashcaddy-api/routes/backups.js index a2e7a00..d89d59c 100644 --- a/dashcaddy-api/routes/backups.js +++ b/dashcaddy-api/routes/backups.js @@ -1,9 +1,13 @@ const express = require('express'); -const { success } = require('../response-helpers'); -const fs = require('fs'); +const fsp = require('fs').promises; const path = require('path'); +const fs = require('fs'); +const { success } = require('../response-helpers'); -const DEFAULT_BACKUP_DIR = process.env.BACKUP_DIR || path.join(__dirname, 'backups'); +const DEFAULT_BACKUP_DIR = process.env.BACKUP_DIR || path.join(__dirname, '..', 'backups'); +const DEFAULT_MAX_STORAGE_BYTES = process.env.BACKUP_MAX_STORAGE_BYTES + ? parseInt(process.env.BACKUP_MAX_STORAGE_BYTES, 10) + : 0; /** * Backups routes factory @@ -41,6 +45,7 @@ module.exports = function({ backupManager, licenseManager, asyncHandler }) { runImmediately: backup.runImmediately || false, destination: backup.destination || 'local', destinationPath: backup.destinationPath || DEFAULT_BACKUP_DIR, + maxStorageBytes: backup.maxStorageBytes || null, lastRun: lastRun ? lastRun.toISOString() : null, nextRun: nextRun ? nextRun.toISOString() : null, lastBackupId: appHistory.length > 0 ? appHistory[0].id : null @@ -52,8 +57,8 @@ module.exports = function({ backupManager, licenseManager, asyncHandler }) { // Create or update a scheduled backup for an app router.post('/backups/schedule', premiumGating, asyncHandler(async (req, res) => { - const { appId, enabled, schedule, retention, runImmediately, destination, destinationPath } = req.body; - + const { appId, enabled, schedule, retention, runImmediately, destination, destinationPath, maxStorageBytes } = req.body; + if (!appId) { const { ValidationError } = require('../errors'); throw new ValidationError('appId is required'); @@ -61,7 +66,12 @@ module.exports = function({ backupManager, licenseManager, asyncHandler }) { const config = backupManager.getConfig(); if (!config.backups) config.backups = {}; - + + // Parse maxStorageBytes if provided as string (e.g. "10GB") + const parsedMaxStorage = maxStorageBytes + ? (typeof maxStorageBytes === 'string' ? parseStorageSize(maxStorageBytes) : maxStorageBytes) + : null; + // Build the backup config for this app const backupConfig = { enabled: enabled !== undefined ? enabled : true, @@ -71,7 +81,8 @@ module.exports = function({ backupManager, licenseManager, asyncHandler }) { destination: destination || 'local', destinationPath: destinationPath || DEFAULT_BACKUP_DIR, include: ['all'], - destinations: [{ type: destination || 'local', path: destinationPath || DEFAULT_BACKUP_DIR }] + destinations: [{ type: destination || 'local', path: destinationPath || DEFAULT_BACKUP_DIR }], + maxStorageBytes: parsedMaxStorage }; config.backups[appId] = backupConfig; @@ -490,6 +501,39 @@ module.exports = function({ backupManager, licenseManager, asyncHandler }) { success(res, { history }); }, 'backups-history')); + // Get storage info for backups destination + router.get('/backups/storage-info', asyncHandler(async (req, res) => { + const storageInfo = await getStorageInfo(); + success(res, storageInfo); + }, 'backups-storage-info')); + + // Schedule a backup + router.post('/backups/schedule', asyncHandler(async (req, res) => { + const { name, schedule, maxStorageBytes, ...backupConfig } = req.body; + + if (!name || !schedule) { + return res.status(400).json({ error: 'name and schedule are required' }); + } + + const config = backupManager.getConfig(); + + // Store maxStorageBytes in the backup config (converted to bytes) + const maxBytes = typeof maxStorageBytes === 'number' && maxStorageBytes > 0 + ? maxStorageBytes + : (typeof maxStorageBytes === 'string' ? parseStorageSize(maxStorageBytes) : 0); + + config.backups[name] = { + ...backupConfig, + enabled: true, + schedule, + maxStorageBytes: maxBytes, + destinations: backupConfig.destinations || [{ type: 'local' }] + }; + + backupManager.updateConfig(config); + success(res, { message: `Backup '${name}' scheduled`, maxStorageBytes: maxBytes }); + }, 'backups-schedule')); + // Restore from backup router.post('/backups/restore/:backupId', asyncHandler(async (req, res) => { const result = await backupManager.restoreBackup(req.params.backupId, req.body); @@ -616,7 +660,7 @@ module.exports = function({ backupManager, licenseManager, asyncHandler }) { */ function calculateNextRun(lastRun, schedule) { if (!lastRun) return null; - + const intervals = { 'hourly': 60 * 60 * 1000, 'daily': 24 * 60 * 60 * 1000, @@ -625,7 +669,7 @@ function calculateNextRun(lastRun, schedule) { }; const baseInterval = intervals[schedule]; - + if (baseInterval) { return new Date(lastRun.getTime() + baseInterval); } @@ -653,3 +697,121 @@ function formatBytes(bytes) { const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } + +/** + * Get storage information for the backup directory + */ +async function getStorageInfo() { + const result = { + destination: DEFAULT_BACKUP_DIR, + maxStorageBytes: DEFAULT_MAX_STORAGE_BYTES, + usedBytes: 0, + availableBytes: 0, + usagePercent: 0, + backupCount: 0, + oldestBackup: null, + newestBackup: null + }; + + try { + // Get disk space info + const diskSpace = await getDiskSpaceInfo(DEFAULT_BACKUP_DIR); + result.availableBytes = diskSpace.available; + + // Scan for backup files + if (DEFAULT_MAX_STORAGE_BYTES > 0) { + result.maxStorageBytes = DEFAULT_MAX_STORAGE_BYTES; + } else { + result.maxStorageBytes = diskSpace.total || 0; + } + + let totalSize = 0; + let oldestTime = null; + let newestTime = null; + + try { + const entries = await fsp.readdir(DEFAULT_BACKUP_DIR); + for (const entry of entries) { + if (entry.endsWith('.backup')) { + const filePath = path.join(DEFAULT_BACKUP_DIR, entry); + try { + const stats = await fsp.stat(filePath); + totalSize += stats.size; + result.backupCount++; + + const fileTime = new Date(stats.mtime); + if (!oldestTime || fileTime < oldestTime) oldestTime = fileTime; + if (!newestTime || fileTime > newestTime) newestTime = fileTime; + } catch (e) { + // Skip files we can't stat + } + } + } + } catch (e) { + // Backup directory might not exist yet + } + + result.usedBytes = totalSize; + result.oldestBackup = oldestTime ? oldestTime.toISOString() : null; + result.newestBackup = newestTime ? newestTime.toISOString() : null; + + // Calculate available (total limit - used), or from disk space if no limit set + if (result.maxStorageBytes > 0) { + result.availableBytes = Math.max(0, result.maxStorageBytes - totalSize); + result.usagePercent = parseFloat(((totalSize / result.maxStorageBytes) * 100).toFixed(2)); + } else if (diskSpace.total) { + result.availableBytes = diskSpace.available; + result.usagePercent = diskSpace.total > 0 + ? parseFloat((((diskSpace.total - diskSpace.available) / diskSpace.total) * 100).toFixed(2)) + : 0; + } + } catch (error) { + console.error('[BackupsRouter] Error getting storage info:', error.message); + } + + return result; +} + +/** + * Get disk space info (filesystem-agnostic) + */ +async function getDiskSpaceInfo(dirPath) { + try { + const diskInfo = await fsp.statfs(dirPath); + return { + total: diskInfo.blocks * diskInfo.bsize, + available: diskInfo.bfree * diskInfo.bsize, + used: (diskInfo.blocks - diskInfo.bfree) * diskInfo.bsize + }; + } catch (error) { + // Directory might not exist or be accessible + return { total: 0, available: 0, used: 0 }; + } +} + +/** + * Parse storage size string like "10GB" to bytes + */ +function parseStorageSize(sizeStr) { + if (!sizeStr || typeof sizeStr === 'number') return sizeStr || 0; + + const match = String(sizeStr).match(/^(\d+(?:\.\d+)?)\s*(B|KB|MB|GB|TB|K|M|G|T)?$/i); + if (!match) return 0; + + const value = parseFloat(match[1]); + const unit = (match[2] || 'B').toUpperCase(); + + const multipliers = { + 'B': 1, + 'K': 1024, + 'KB': 1024, + 'M': 1024 * 1024, + 'MB': 1024 * 1024, + 'G': 1024 * 1024 * 1024, + 'GB': 1024 * 1024 * 1024, + 'T': 1024 * 1024 * 1024 * 1024, + 'TB': 1024 * 1024 * 1024 * 1024 + }; + + return Math.floor(value * (multipliers[unit] || 1)); +} diff --git a/dashcaddy-api/scripts/dashcaddy-update.sh b/dashcaddy-api/scripts/dashcaddy-update.sh index 7114c78..6f028f9 100755 --- a/dashcaddy-api/scripts/dashcaddy-update.sh +++ b/dashcaddy-api/scripts/dashcaddy-update.sh @@ -134,11 +134,16 @@ restart_container() { # Stop and remove existing container so new env var is applied docker rm -f "$CONTAINER_NAME" 2>/dev/null || true - # Re-create with same volumes and the SERVICES_FILE env var + # Re-create with same volumes. CRITICAL: must include CREDENTIALS_FILE + + # ENCRYPTION_KEY_FILE pointing at /app/data/ so the container reads the TOTP + # secret from the bind-mounted host data dir (not image-local /app/credentials.json + # which gets a fresh encryption key on every container recreate = TOTP breaks). docker run -d --restart unless-stopped --name "$CONTAINER_NAME" \ -p 127.0.0.1:3001:3001 \ -v /opt/dashcaddy/dashcaddy-api/data:/app/data \ -e SERVICES_FILE=/app/data/services.json \ + -e CREDENTIALS_FILE=/app/d...son \ + -e ENCRYPTION_KEY_FILE=/app/data/.encryption-key \ "$image" log "Container restarted with fresh env" } diff --git a/dashcaddy-api/src/app.js b/dashcaddy-api/src/app.js index 93dc650..2b30ce6 100644 --- a/dashcaddy-api/src/app.js +++ b/dashcaddy-api/src/app.js @@ -39,6 +39,13 @@ let dockerMaintenance, logDigest; try { dockerMaintenance = require('../docker-maintenance'); } catch (_) { /* optional module */ } try { logDigest = require('../log-digest'); } catch (_) { /* optional module */ } +// Workflow engine (bundled workflows) +let bundledWorkflowsModule; +let workflowEngine = null; +try { + bundledWorkflowsModule = require('../bundled-workflows'); +} catch (_) { /* optional module */ } + // Templates const { APP_TEMPLATES, TEMPLATE_CATEGORIES, DIFFICULTY_LEVELS } = require('../app-templates'); const { RECIPE_TEMPLATES, RECIPE_CATEGORIES } = require('../recipe-templates'); @@ -69,6 +76,7 @@ const recipesRoutes = require('../routes/recipes'); const themesRoutes = require('../routes/themes'); const dockerResourcesRoutes = require('../routes/docker-resources'); const eventsRoutes = require('../routes/events'); +const workflowsRoutes = require('../routes/workflows'); // Constants const { APP } = require('../constants'); @@ -156,6 +164,21 @@ async function createApp() { return null; } + // Back-compat: reverse-proxy SSO snippets (Caddy forward_auth + per-service + // auto-login pages) historically call these endpoints under the pre-1.5.0 + // prefix `/api/auth/...`. The canonical mount is `/api/v1`. Hand-maintained + // Caddyfiles have repeatedly drifted back to the old prefix and 404'd the SSO + // gate (breaking Plex/Jellyfin/Emby/chat). Transparently rewrite ONLY these two + // auth paths to the v1 mount so the gate is tolerant of that drift. Must run + // before configureMiddleware() so CSRF/auth see the canonical path. This is + // deliberately narrow — NOT a general `/api` -> `/api/v1` alias. + app.use((req, res, next) => { + if (req.url.startsWith('/api/auth/gate/') || req.url.startsWith('/api/auth/app-token/')) { + req.url = '/api/v1' + req.url.slice(4); // '/api'.length === 4 + } + next(); + }); + // Configure middleware const middlewareResult = configureMiddleware(app, { siteConfig: config.siteConfig, @@ -308,9 +331,55 @@ async function createApp() { app, }); + // Initialize workflow engine if bundled-workflows is available + if (bundledWorkflowsModule && ctx.docker) { + try { + const { WorkflowEngine } = bundledWorkflowsModule; + const workflowCtx = { + docker: ctx.docker, + notification: ctx.notification, + backupManager: ctx.backupManager, + resourceMonitor: ctx.resourceMonitor, + servicesStateManager: ctx.servicesStateManager + }; + workflowEngine = new WorkflowEngine(workflowCtx); + ctx.workflowEngine = workflowEngine; + log.info('app', 'Workflow engine initialized'); + } catch (err) { + log.error('app', 'Failed to initialize workflow engine', { error: err.message }); + } + } + // Build versioned API router const apiRouter = express.Router(); - + + // Wire up notification listeners for resourceMonitor and backupManager + if (ctx.notification && ctx.resourceMonitor) { + ctx.resourceMonitor.on('alert', (alertData) => { + ctx.notification.sendAlert(alertData).catch(err => { + log.error('notification', 'Failed to send alert', { error: err.message }); + }); + }); + ctx.resourceMonitor.on('auto-restart', (data) => { + ctx.notification.sendServiceEvent('auto-restart', data).catch(err => { + log.error('notification', 'Failed to send auto-restart notification', { error: err.message }); + }); + }); + } + + if (ctx.notification && ctx.backupManager) { + ctx.backupManager.on('backup-complete', (data) => { + ctx.notification.send('backup-complete', data).catch(err => { + log.error('notification', 'Failed to send backup-complete', { error: err.message }); + }); + }); + ctx.backupManager.on('backup-failed', (data) => { + ctx.notification.send('backup-failed', data).catch(err => { + log.error('notification', 'Failed to send backup-failed', { error: err.message }); + }); + }); + } + // Mount route modules apiRouter.use(authRoutes(ctx)); apiRouter.use(configRoutes(ctx)); @@ -330,7 +399,8 @@ async function createApp() { apiRouter.use('/containers', containerRoutes({ docker: ctx.docker, log: ctx.log, - asyncHandler: ctx.asyncHandler + asyncHandler: ctx.asyncHandler, + workflowEngine: ctx.workflowEngine })); apiRouter.use(serviceRoutes({ servicesStateManager: ctx.servicesStateManager, @@ -361,7 +431,8 @@ async function createApp() { resourceMonitor: ctx.resourceMonitor, docker: ctx.docker, asyncHandler: ctx.asyncHandler, - log: ctx.log + log: ctx.log, + notificationManager: ctx.notification })); apiRouter.use(updatesRoutes({ updateManager: ctx.updateManager, @@ -404,8 +475,8 @@ async function createApp() { })); apiRouter.use(backupsRoutes({ backupManager: ctx.backupManager, - asyncHandler: ctx.asyncHandler, - licenseManager: ctx.licenseManager + licenseManager: ctx.licenseManager, + asyncHandler: ctx.asyncHandler })); apiRouter.use('/ca', caRoutes(ctx)); apiRouter.use(browseRoutes({ @@ -435,6 +506,11 @@ async function createApp() { updateManager: ctx.updateManager, logError: ctx.logError })); + apiRouter.use(workflowsRoutes({ + workflowEngine: ctx.workflowEngine, + licenseManager: ctx.licenseManager, + asyncHandler: ctx.asyncHandler + })); // Inline API routes apiRouter.get('/health', (req, res) => { diff --git a/status/assets/sami-files.png b/status/assets/sami-files.png new file mode 100644 index 0000000000000000000000000000000000000000..b87be153b9c3b70e277e7b3185fc245055d7a09c GIT binary patch literal 6106 zcmbVQXE((h~9}35oNUK4ABxj>ZqfWAfgjJ5hNr;2?=J18eQ}{ zCZdvxQ_CD|0XTN)|cdzx*Kws<1Wsb`L09?_2q+tXA5Yj6I zprIl?tbHn+0f4ncTjPNVJb!1wEs1L_484yY; z+itK{pKK_n+}SIr$P7bEyv&;UTW4x;PLsB>nr*=OzHe4m;FH=yljgmF-Nf9vR5EM$ z8njzWOCH;p`ZDi#de5TE;0ZLOHXma6%WQ^+C9>S6uDf-2<7I%`0j&xV8X!inALTGO zRUOjPByWotXd~1jj_2$3JaKyO*7fUfQEHS_|3Tz+H1k~*eH)=uA@Q&=yGv5c(ofNl@ z2cFw)HKGtKoQfSUtE8Oa!8yCX5X5a4nTuZoL-Bcsr}HCs@nlXolL{99=!Ly$&ydaT z+-i-2{khs*wa2r*9dD&X2>=y~gY%-5?tIv+6(6kikAo=U8L{w2u!I87m#OW9d{*W= zCZ2Nv7eRD$L9Y9qW`Bm_=s%RdKA{`a3*#?z>J&-fbqT_ptSL91A!jU7*2cNs$P{yk zM9h3&k5p30Qw;mFp1sV-Ossoqs4*KYDYc~=x^e*JB<6mX({XW8x`+Sm2+!aF&P>oN zMY05)Np6&glD~B<=$@-tc$=L?X5bGRxt^{}YWQtI;@K}qK5n@3kdZHi;7lSGT*;xV0s#ObYM z`Rik?siXAd%5}cBDmp#Z}%k zG{PNsVrK$WLD`*&Y)8{|uZN+Zb3B?`zItDhMx{HN|5Sz4EHpTdlcwFf)Ax!W>P`+c20mHRE4$(L#i-Hb44BV%gJw42*W%l1l%9zMD5%9cib#c1) z(An@hbaV&L&+hn9LV3pd=@^a0EAa;2`CsqY687Cw=hm>b_)p^e22W|*LIVO|ikUW{ zo%yI&mhV@KZMWpaBrFS@r`fHAI+A*gZ+5-A66&<-OO)_c5H46)`@j=t?89o8brw>! zrhS4PD{XXViS|Ux47C}TOZ_fZnr)LX!J8#BI^D5wlpMhAY$@+J08Z88!cxHMs#-GQ z_=+*wO)9vbA+s={qmS1iwASXtsn<>HeQIIpRQ}(KL8`#Pof^N@LLZkukM)XjTNmfW zHBScmLxo&loJRJf+*Wm>dK4G+;H~WSF)nG{z}{;lxZGuyWsB}n(`+}SFDvzn$04ta zTUa8R2hsMe**smfF$zlU(zdd{4e;QVUTWgQ?z|+u`X{*fNc+MxX&JEvw>^6?(K==W zTVNm-+uD>`lrpDV+A6%2)Zp%VxV}02WMRO?F$jZAlFI$zEnEcN#ee$iE~{a|nt)px zEZR>MF?MuHru^5rEbJDWxLJ0! z%Jv?PF1%N=bk3||cc-6LiEI@eAD^2a+x@jQ!1;BCF-w0K+p|l~8jqoiE@uD4K46aMA6jj%}iGFp&TG z>8t*MXDO2FNm3Q+)Uv*9qqE@~i6xgJ8e~uka2sy!Z)4%Caw%~FfPOjIKR!eCT-pDL zX8t`Bb>M{~2nb>Db{_)fm7-Jn#h7nMhO$kbVPy@%f9J?(z3h$inQ^)kY(7YGG0LFU+GZZ%!Irh?_U~lkwG=FH!0Me>WfFBr4}@e zJUmh9HqWG(fVFfIY)-7@g$ey`3#D?f4qWNjV0zhqc3V{Og;4Q|5*vZM&@iE z`1e+{4NnBuRP{;hl#U*Pzbk80C6t|G^n88N*==){qVt~_pGO4Zmd z#{Bi4!$@-CN^;2z&%`>cYJCgb(W$AS`^o|-2$cF!5U2CC)~l90&;xcD{M<|ut_gx( zFr#)XE2XL?vmS_eLEhrwIw%R#lf_&F7{mXnBF~{0;GsUm;6ZBa2GUKsLnXv3=k$v) zCVt{OQGmWZa(Tqy5GBe4JaNR76w;#vs@8_th*B3`CX4_&$?69tsc2)5@dnm z%QQ`+Ewk+nlO5Oxj&C$ASmdKtDrG8Nh{1hBit;S`IhLIDm~iaPhs3&9{t^Q}d%@Gw zj)ZeC>^)GCGtd#0ZMDdPLvJcD+ET+yR6-Y5K#;wJ<*# zW2Vk+@T}0e1(1TU*oc`n^0y@G*G3Y|Vo_d90P|C-1#ZH4teDhqa26#<5ao(*1|=2U z$G#YNSZ({=x*)+Z^slv|GpC5pC()x=VVEhQSEnW;Mx?QpmszNoSFZjG3GUXR*iPU= z7iKFwyWT3@;&Re*L|O=GE`L%)>jTxB)kJ*bij$#{fX1X{v~4`a}UORtavUd zEoITPdq?*0ee)dG7_n7`ydT_7Mu3UxC|I=Jra(Ooik3~?TLe%xg8;t~4TZ55<$=&znx`ZjCHPB)N!Zd=D5zQ)sz?8ivZ#}!3N z6FG%R+e3BLzIiLq#`+}lC_A;tg5k5LZ(-2%3huBmUJA^0J4WBU13dl~Km-fOEkPKw z;iA+d@usuOxLy%^x!OK(Y5cEg<-NK+0yx3?vure28nG-mPCy-aHWAYp$esk9`4LF{n1I_t z=b$m4w+A=izX29~5!_DaYVtA8VH~lu11F9GDyiyfHo7MA4%a}N2JUCnOAm~Ij0#!8 z@@(|JlRLdC&Jsrpi!ehZUGsp@(_wZL&TCQB1yaE))AIQG6q3I@W~070RPbS3z% z_u>@;onkBr^bRTUwoKo^FCidm`UFpn$Fz~vOI{UYxEzs|^I+JF2Ydpmz;TqaGZSjX z4-A2fTZc85dNT74SY1Oq6e{UelzeVhUoa<-mjHs=Z(}{0?&Z*H#-8s)!VW^O0}Y>wIK%YdeuCr$A8co!2bfm8xM*jNq^L(CbMKB z3jXMRVw*pOQ<<(_1&c}9SW?xMO>zMDC#E(l;Am>rB@js_;z28IK$>Rf3t8g1mgw5t zi>aeB(Q`1Rg(-mcwG^mA>b#~&h1fsZG_~-r_fedQtPvCXvN2pDO!}(S0)BN#Vfd$m z$|ZY)o@B9C%fFCCr=Y)iuN?`GiCwsSuL#9XMZPS!O>dAbZqPEE&9d}M-QKcZ+6&1` zTDd;X?zdtf@4P<`r=_)$^xeY|-rmsTk)#J{7bwidf<1#ZvyBMTcx5JH2vSI z9k6i%6iqIJP#%oEY;=#(;XlYeX9LQI!AcOu-2gl`Xqahrrsy$l+7&5TI>p0%j=@TG zh(T28Q=Ra{4q3#xNAy|vCTJEKFGV8IdW^=%e_V1y>X&gLuh{lwcBOw1G*;R-^BdHFo<#u2dX%ZKR zP^rF|Q5E$f=AHu`?Im|QV5Tulsn}U!$Hrkc8O?PD(p1H zn1d5+aEZD96{3bG{B#cUQa$rx(*QWKxDTTIeUVcz)1rH-hPYFnc=Wlv-xz`E=J5q- z1xH*g!fDo_D}$i?JbuU(gE$eKKXZa(a=u-B;-ysOVYe?0siK_vOHyR?fc7DaK4-i7 zaK{jM3CcZ`M-(DtzK>N6%PRBUzh(XPe&|IoWbcR5-$9mlQ#j<>TkWn%rx#4h!FLEL z@%C6)JY?i+SsCHeW5|eB77yXYN?FbN2RPmI0c^W|*o?R>1^y;l-~BuDr;^<`Uu!Qg zOh+KeOM_+6#r+=&C4Y5Y>NXj~T5s{F#_Em?8OfLaNP_`9y?bSU)reT7A0hkD3! zLEZ3kkRLp3&`IKJdpEHP=ery`evtB1FP!Ofcz7^df!=iKReVZ&V_{si3ioRqS&n

QKU8iqM^}H3WaT$SzyBB+f*18`qo>lYBobedMY6-5Uo5+$frr3tCu5`eduF1QoRZL-=7go2U1u0XK#_$;-~dnL(FUC?ax) zw~tq0L=IY61Vakhn+zWj-ssh)F3EIFme!6|;83CeJE%hv&I+9V0wSmK8dSpt>E*%x z?vR?d@Xs-#Wr?LMqiy@OU$h(}(%?)o%Q`7xFjzK%Yxf`oiC_WsX$_082acnoy9kTkt~R7bFrtUV z2;;>yhPuCb1v`+8C?{CYOa+{F$this.maxErrors&&this.errors.shift(),console.error(`[Onboarding Error] ${g}:`,i,s)}recoverFromError(g,i){switch(this.classifyError(g)){case"ELEMENT_NOT_FOUND":return this.logError("Element Not Found",g,{currentStep:i}),{action:"SKIP_STEP",nextStep:i+1,message:"Target element not found, skipping to next step"};case"STORAGE_UNAVAILABLE":return this.logError("Storage Unavailable",g),{action:"USE_MEMORY_STORAGE",message:"Local storage unavailable, using in-memory storage"};case"DRIVER_NOT_LOADED":return this.logError("Driver.js Not Loaded",g),{action:"ABORT_TOUR",message:"Driver.js library not loaded, cannot start tour"};case"INVALID_TOOLTIP":return this.logError("Invalid Tooltip Configuration",g,{currentStep:i}),{action:"SKIP_STEP",nextStep:i+1,message:"Invalid tooltip configuration, skipping"};case"THEME_DETECTION_FAILED":return this.logError("Theme Detection Failed",g),{action:"USE_DEFAULT_THEME",message:"Using default dark theme"};default:return this.logError("Unknown Error",g,{currentStep:i}),{action:"ABORT_TOUR",message:"Unexpected error occurred, aborting tour"}}}classifyError(g){const i=g.message||g.toString();return i.includes("element")&&i.includes("not found")?"ELEMENT_NOT_FOUND":i.includes("storage")||i.includes("quota")?"STORAGE_UNAVAILABLE":i.includes("driver")||i.includes("undefined")?"DRIVER_NOT_LOADED":i.includes("invalid")||i.includes("validation")?"INVALID_TOOLTIP":i.includes("theme")?"THEME_DETECTION_FAILED":"UNKNOWN"}getErrors(){return[...this.errors]}clearErrors(){this.errors=[]}getStatistics(){const g={total:this.errors.length,byContext:{},byType:{},recent:this.errors.slice(-10)};return this.errors.forEach(i=>{g.byContext[i.context]=(g.byContext[i.context]||0)+1;const s=this.classifyError({message:i.message});g.byType[s]=(g.byType[s]||0)+1}),g}handleDriverLoadFailure(){this.logError("Driver.js Load Failure","Driver.js library failed to load");const g=document.createElement("div");return g.id="onboarding-fallback",g.style.cssText=` +(function(o){"use strict";class f{constructor(){this.errors=[],this.maxErrors=50}logError(y,l,r={}){const h={timestamp:new Date().toISOString(),context:y,message:l instanceof Error?l.message:l,stack:l instanceof Error?l.stack:null,metadata:r};this.errors.push(h),this.errors.length>this.maxErrors&&this.errors.shift(),console.error(`[Onboarding Error] ${y}:`,l,r)}recoverFromError(y,l){switch(this.classifyError(y)){case"ELEMENT_NOT_FOUND":return this.logError("Element Not Found",y,{currentStep:l}),{action:"SKIP_STEP",nextStep:l+1,message:"Target element not found, skipping to next step"};case"STORAGE_UNAVAILABLE":return this.logError("Storage Unavailable",y),{action:"USE_MEMORY_STORAGE",message:"Local storage unavailable, using in-memory storage"};case"DRIVER_NOT_LOADED":return this.logError("Driver.js Not Loaded",y),{action:"ABORT_TOUR",message:"Driver.js library not loaded, cannot start tour"};case"INVALID_TOOLTIP":return this.logError("Invalid Tooltip Configuration",y,{currentStep:l}),{action:"SKIP_STEP",nextStep:l+1,message:"Invalid tooltip configuration, skipping"};case"THEME_DETECTION_FAILED":return this.logError("Theme Detection Failed",y),{action:"USE_DEFAULT_THEME",message:"Using default dark theme"};default:return this.logError("Unknown Error",y,{currentStep:l}),{action:"ABORT_TOUR",message:"Unexpected error occurred, aborting tour"}}}classifyError(y){const l=y.message||y.toString();return l.includes("element")&&l.includes("not found")?"ELEMENT_NOT_FOUND":l.includes("storage")||l.includes("quota")?"STORAGE_UNAVAILABLE":l.includes("driver")||l.includes("undefined")?"DRIVER_NOT_LOADED":l.includes("invalid")||l.includes("validation")?"INVALID_TOOLTIP":l.includes("theme")?"THEME_DETECTION_FAILED":"UNKNOWN"}getErrors(){return[...this.errors]}clearErrors(){this.errors=[]}getStatistics(){const y={total:this.errors.length,byContext:{},byType:{},recent:this.errors.slice(-10)};return this.errors.forEach(l=>{y.byContext[l.context]=(y.byContext[l.context]||0)+1;const r=this.classifyError({message:l.message});y.byType[r]=(y.byType[r]||0)+1}),y}handleDriverLoadFailure(){this.logError("Driver.js Load Failure","Driver.js library failed to load");const y=document.createElement("div");return y.id="onboarding-fallback",y.style.cssText=` position: fixed; bottom: 20px; right: 20px; @@ -10,44 +10,44 @@ z-index: 9999; max-width: 300px; font-size: 14px; - `,g.innerHTML=` + `,y.innerHTML=` Welcome to DashCaddy!

The interactive tour is unavailable, but you can explore the dashboard freely. Check the documentation for help getting started.

- `,document.body.appendChild(g),setTimeout(()=>{g.parentNode&&g.parentNode.removeChild(g)},1e4),!0}handleStorageUnavailable(){this.logError("Storage Unavailable","Local storage is not available");const g={data:{},getItem(i){return this.data[i]||null},setItem(i,s){this.data[i]=s},removeItem(i){delete this.data[i]},clear(){this.data={}}};return console.warn("[ErrorHandler] Using in-memory storage - progress will not persist"),g}sendToErrorTracking(g){}}o.ErrorHandler=f,console.log("[ErrorHandler] Module loaded")})(window);const DC={NAME:"DashCaddy",POLL:{DASHBOARD:1e4,LOGS:3e3,STATS:5e3,WEATHER:6e5,HEALTH:1e3,DEPLOY_SSL:5e3},DELAYS:{BTN_RESET:2e3,RELOAD:5e3,MODAL_CLOSE:500,PORT_CHECK:500,DEPLOY_INIT:3e3},DEFAULTS:{DNS_PORT:"5380",SERVICE_PORT:"8080",TTL:300,CADDYFILE:"C:\\caddy\\Caddyfile"}},errorHandler=new ErrorHandler,_cachedCfg=JSON.parse(localStorage.getItem("dashcaddy_site_config")||"null"),SITE={tld:_cachedCfg&&_cachedCfg.tld||".home",dnsIp:"",dnsPort:DC.DEFAULTS.DNS_PORT,dnsServers:{},configurationType:_cachedCfg&&_cachedCfg.configurationType||"homelab",domain:_cachedCfg&&_cachedCfg.domain||"",defaults:_cachedCfg&&_cachedCfg.defaults||{},routingMode:_cachedCfg&&_cachedCfg.routingMode||"subdomain",onboardingCompleted:!1};window.__dashcaddySiteConfigLoaded=(async function(){try{const g=await fetch("/api/v1/config");if(g.ok){const i=await g.json();if(i.tld&&(SITE.tld=i.tld.startsWith(".")?i.tld:"."+i.tld),i.dns&&(SITE.dnsIp=i.dns.ip||"",SITE.dnsPort=i.dns.port||DC.DEFAULTS.DNS_PORT),i.dnsServers&&typeof i.dnsServers=="object")for(const[h,n]of Object.entries(i.dnsServers))h!=="__proto__"&&h!=="constructor"&&h!=="prototype"&&(SITE.dnsServers[h]=n);i.configurationType&&(SITE.configurationType=i.configurationType),i.domain&&(SITE.domain=i.domain),i.defaults&&(SITE.defaults=i.defaults),i.routingMode&&(SITE.routingMode=i.routingMode),SITE.onboardingCompleted=i.onboardingCompleted===!0,localStorage.setItem("dashcaddy_site_config",JSON.stringify({tld:SITE.tld,configurationType:SITE.configurationType,domain:SITE.domain,routingMode:SITE.routingMode})),renderDnsCards();const s=document.getElementById("manage-tokens");s&&(s.style.display=Object.keys(SITE.dnsServers).length?"":"none")}}catch{}document.querySelectorAll("[data-tld]").forEach(g=>g.textContent=SITE.tld);const f=document.getElementById("edit-tld-suffix");f&&(f.textContent=SITE.tld);const u=document.getElementById("external-proxy-ip");u&&SITE.dnsIp&&(u.value=SITE.dnsIp,u.placeholder=SITE.dnsIp)})();function buildDomain(o){return o+SITE.tld}function buildServiceUrl(o){return SITE.routingMode==="subdirectory"&&SITE.domain?"https://"+SITE.domain+"/"+o:SITE.configurationType==="public"&&SITE.domain?"https://"+o+"."+SITE.domain:"https://"+buildDomain(o)}function getDnsServerAddr(o){const f=SITE.dnsServers[o];return f?`${f.ip}:${f.port}`:buildDomain(o)}function getPrimaryDnsId(){if(!SITE.dnsIp)return null;for(const[o,f]of Object.entries(SITE.dnsServers))if(f.ip===SITE.dnsIp)return o;return null}function renderDnsCards(){const o=document.querySelector(".top");if(!o)return;const f=Object.keys(SITE.dnsServers);if(!f.length)return;const u='',g=o.firstElementChild;f.forEach(i=>{const s=escapeHtml(i),h=escapeHtml((SITE.dnsServers[i].name||i).toUpperCase()),n=document.createElement("div");n.className="card",n.setAttribute("data-app",i),n.setAttribute("data-status","off"),n.innerHTML=`
${u}
${h}OFF
--
--
`,o.insertBefore(n,g)})}window.renderDnsCards=renderDnsCards;let csrfToken=null;async function getCSRFToken(){if(csrfToken)return csrfToken;try{const o=await fetch("/api/v1/csrf-token");if(!o.ok)throw new Error("Failed to fetch CSRF token");return csrfToken=(await o.json()).token,csrfToken}catch(o){throw errorHandler.logError("[CSRF] Get Token",o,{function:"getCSRFToken"}),o}}async function secureFetch(o,f={}){const u=(f.method||"GET").toUpperCase(),g=!["GET","HEAD","OPTIONS"].includes(u);if(g)try{const s=await getCSRFToken();f.headers={...f.headers,"X-CSRF-Token":s}}catch(s){errorHandler.logError("[CSRF] Add to Request",s,{function:"secureFetch"})}f.signal||(f={...f,signal:AbortSignal.timeout(15e3)});const i=await fetch(o,f);if(g&&i.status===403)try{const s=await i.clone().json();if(s.error&&(s.error.includes("DC-100")||s.error.includes("DC-101"))){csrfToken=null;const h=await getCSRFToken();return f.headers={...f.headers,"X-CSRF-Token":h},f.signal=AbortSignal.timeout(15e3),fetch(o,f)}}catch{}return i}async function postJSON(o,f){const u=await secureFetch(o,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(f)}),g=await u.json();if(!u.ok||g.success===!1)throw new Error(g.error||`Request failed (${u.status})`);return g}async function getJSON(o){const f=await secureFetch(o);if(!f.ok){let u=`Request failed (${f.status})`;try{u=(await f.json()).error||u}catch{}throw new Error(u)}return f.json()}async function deleteAPI(o){const f=await secureFetch(o,{method:"DELETE"}),u=await f.json();if(!f.ok||u.success===!1)throw new Error(u.error||`Delete failed (${f.status})`);return u}async function withButton(o,f,u,g={}){const i=o.innerHTML,{successText:s="\u2705",resetDelay:h=DC.DELAYS.BTN_RESET}=g;o.disabled=!0,o.innerHTML=f;try{const n=await u();return o.innerHTML=s,setTimeout(()=>{o.innerHTML=i,o.disabled=!1},h),n}catch(n){throw o.innerHTML=i,o.disabled=!1,n}}function openModal(o){document.getElementById(o)?.classList.add("show")}function closeModal(o){document.getElementById(o)?.classList.remove("show")}function wireModal(o,...f){o&&(o.addEventListener("click",u=>{u.target===o&&o.classList.remove("show")}),f.forEach(u=>{u&&typeof u.addEventListener=="function"&&u.addEventListener("click",()=>o.classList.remove("show"))}))}function showNotification(o,f="info",u=3e3){const g=document.querySelector(".deploy-notification");g&&g.remove();const i={info:{bg:"#2196F3",fg:"#fff"},success:{bg:"var(--ok-bg)",fg:"var(--ok-fg)"},error:{bg:"#f44336",fg:"#fff"},warning:{bg:"#ff9800",fg:"#fff"}},s=i[f]||i.info,h=document.createElement("div");h.className="deploy-notification",h.textContent=o,h.style.cssText=` + `,document.body.appendChild(y),setTimeout(()=>{y.parentNode&&y.parentNode.removeChild(y)},1e4),!0}handleStorageUnavailable(){this.logError("Storage Unavailable","Local storage is not available");const y={data:{},getItem(l){return this.data[l]||null},setItem(l,r){this.data[l]=r},removeItem(l){delete this.data[l]},clear(){this.data={}}};return console.warn("[ErrorHandler] Using in-memory storage - progress will not persist"),y}sendToErrorTracking(y){}}o.ErrorHandler=f,console.log("[ErrorHandler] Module loaded")})(window);const DC={NAME:"DashCaddy",POLL:{DASHBOARD:1e4,LOGS:3e3,STATS:5e3,WEATHER:6e5,HEALTH:1e3,DEPLOY_SSL:5e3},DELAYS:{BTN_RESET:2e3,RELOAD:5e3,MODAL_CLOSE:500,PORT_CHECK:500,DEPLOY_INIT:3e3},DEFAULTS:{DNS_PORT:"5380",SERVICE_PORT:"8080",TTL:300,CADDYFILE:"C:\\caddy\\Caddyfile"}},errorHandler=new ErrorHandler,_cachedCfg=JSON.parse(localStorage.getItem("dashcaddy_site_config")||"null"),SITE={tld:_cachedCfg&&_cachedCfg.tld||".home",dnsIp:"",dnsPort:DC.DEFAULTS.DNS_PORT,dnsServers:{},configurationType:_cachedCfg&&_cachedCfg.configurationType||"homelab",domain:_cachedCfg&&_cachedCfg.domain||"",defaults:_cachedCfg&&_cachedCfg.defaults||{},routingMode:_cachedCfg&&_cachedCfg.routingMode||"subdomain",onboardingCompleted:!1};window.__dashcaddySiteConfigLoaded=(async function(){try{const y=await fetch("/api/v1/config");if(y.ok){const l=await y.json();if(l.tld&&(SITE.tld=l.tld.startsWith(".")?l.tld:"."+l.tld),l.dns&&(SITE.dnsIp=l.dns.ip||"",SITE.dnsPort=l.dns.port||DC.DEFAULTS.DNS_PORT),l.dnsServers&&typeof l.dnsServers=="object")for(const[h,a]of Object.entries(l.dnsServers))h!=="__proto__"&&h!=="constructor"&&h!=="prototype"&&(SITE.dnsServers[h]=a);l.configurationType&&(SITE.configurationType=l.configurationType),l.domain&&(SITE.domain=l.domain),l.defaults&&(SITE.defaults=l.defaults),l.routingMode&&(SITE.routingMode=l.routingMode),SITE.onboardingCompleted=l.onboardingCompleted===!0,localStorage.setItem("dashcaddy_site_config",JSON.stringify({tld:SITE.tld,configurationType:SITE.configurationType,domain:SITE.domain,routingMode:SITE.routingMode})),renderDnsCards();const r=document.getElementById("manage-tokens");r&&(r.style.display=Object.keys(SITE.dnsServers).length?"":"none")}}catch{}document.querySelectorAll("[data-tld]").forEach(y=>y.textContent=SITE.tld);const f=document.getElementById("edit-tld-suffix");f&&(f.textContent=SITE.tld);const u=document.getElementById("external-proxy-ip");u&&SITE.dnsIp&&(u.value=SITE.dnsIp,u.placeholder=SITE.dnsIp)})();function buildDomain(o){return o+SITE.tld}function buildServiceUrl(o){return SITE.routingMode==="subdirectory"&&SITE.domain?"https://"+SITE.domain+"/"+o:SITE.configurationType==="public"&&SITE.domain?"https://"+o+"."+SITE.domain:"https://"+buildDomain(o)}function getDnsServerAddr(o){const f=SITE.dnsServers[o];return f?`${f.ip}:${f.port}`:buildDomain(o)}function getPrimaryDnsId(){if(!SITE.dnsIp)return null;for(const[o,f]of Object.entries(SITE.dnsServers))if(f.ip===SITE.dnsIp)return o;return null}function renderDnsCards(){const o=document.querySelector(".top");if(!o)return;const f=Object.keys(SITE.dnsServers);if(!f.length)return;const u='',y=o.firstElementChild;f.forEach(l=>{const r=escapeHtml(l),h=escapeHtml((SITE.dnsServers[l].name||l).toUpperCase()),a=document.createElement("div");a.className="card",a.setAttribute("data-app",l),a.setAttribute("data-status","off"),a.innerHTML=`
${u}
${h}OFF
--
--
`,o.insertBefore(a,y)})}window.renderDnsCards=renderDnsCards;let csrfToken=null;async function getCSRFToken(){if(csrfToken)return csrfToken;try{const o=await fetch("/api/v1/csrf-token");if(!o.ok)throw new Error("Failed to fetch CSRF token");return csrfToken=(await o.json()).token,csrfToken}catch(o){throw errorHandler.logError("[CSRF] Get Token",o,{function:"getCSRFToken"}),o}}async function secureFetch(o,f={}){const u=(f.method||"GET").toUpperCase(),y=!["GET","HEAD","OPTIONS"].includes(u);if(y)try{const r=await getCSRFToken();f.headers={...f.headers,"X-CSRF-Token":r}}catch(r){errorHandler.logError("[CSRF] Add to Request",r,{function:"secureFetch"})}f.signal||(f={...f,signal:AbortSignal.timeout(15e3)});const l=await fetch(o,f);if(y&&l.status===403)try{const r=await l.clone().json();if(r.error&&(r.error.includes("DC-100")||r.error.includes("DC-101"))){csrfToken=null;const h=await getCSRFToken();return f.headers={...f.headers,"X-CSRF-Token":h},f.signal=AbortSignal.timeout(15e3),fetch(o,f)}}catch{}return l}async function postJSON(o,f){const u=await secureFetch(o,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(f)}),y=await u.json();if(!u.ok||y.success===!1)throw new Error(y.error||`Request failed (${u.status})`);return y}async function getJSON(o){const f=await secureFetch(o);if(!f.ok){let u=`Request failed (${f.status})`;try{u=(await f.json()).error||u}catch{}throw new Error(u)}return f.json()}async function deleteAPI(o){const f=await secureFetch(o,{method:"DELETE"}),u=await f.json();if(!f.ok||u.success===!1)throw new Error(u.error||`Delete failed (${f.status})`);return u}async function withButton(o,f,u,y={}){const l=o.innerHTML,{successText:r="\u2705",resetDelay:h=DC.DELAYS.BTN_RESET}=y;o.disabled=!0,o.innerHTML=f;try{const a=await u();return o.innerHTML=r,setTimeout(()=>{o.innerHTML=l,o.disabled=!1},h),a}catch(a){throw o.innerHTML=l,o.disabled=!1,a}}function openModal(o){document.getElementById(o)?.classList.add("show")}function closeModal(o){document.getElementById(o)?.classList.remove("show")}function wireModal(o,...f){o&&(o.addEventListener("click",u=>{u.target===o&&o.classList.remove("show")}),f.forEach(u=>{u&&typeof u.addEventListener=="function"&&u.addEventListener("click",()=>o.classList.remove("show"))}))}function showNotification(o,f="info",u=3e3){const y=document.querySelector(".deploy-notification");y&&y.remove();const l={info:{bg:"#2196F3",fg:"#fff"},success:{bg:"var(--ok-bg)",fg:"var(--ok-fg)"},error:{bg:"#f44336",fg:"#fff"},warning:{bg:"#ff9800",fg:"#fff"}},r=l[f]||l.info,h=document.createElement("div");h.className="deploy-notification",h.textContent=o,h.style.cssText=` position: fixed; top: 20px; right: 20px; - background: ${s.bg}; color: ${s.fg}; + background: ${r.bg}; color: ${r.fg}; padding: 16px 24px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,.3); z-index: 10000; animation: slideIn 0.3s ease-out; max-width: 400px; white-space: pre-line; font-size: 14px; - `,document.body.appendChild(h),u>0&&setTimeout(()=>h.remove(),u)}function timeAgo(o){const f=Date.now()-new Date(o).getTime();return f<6e4?"just now":f<36e5?Math.floor(f/6e4)+"m ago":f<864e5?Math.floor(f/36e5)+"h ago":Math.floor(f/864e5)+"d ago"}function safeGet(o,f=null){try{const u=localStorage.getItem(o);return u!==null?u:f}catch{return f}}function safeSet(o,f){try{localStorage.setItem(o,f)}catch{}}function safeRemove(o){try{localStorage.removeItem(o)}catch{}}function safeSessionGet(o,f=null){try{const u=sessionStorage.getItem(o);return u!==null?u:f}catch{return f}}function safeSessionSet(o,f){try{sessionStorage.setItem(o,f)}catch{}}function safeGetJSON(o,f=null){try{const u=localStorage.getItem(o);return u?JSON.parse(u):f}catch{return f}}function escapeHtml(o){return String(o??"").replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}function injectModal(o,f){document.getElementById(o)||document.body.insertAdjacentHTML("beforeend",f)}const DC_BUS={_handlers:{},on(o,f){var u;((u=this._handlers)[o]||(u[o]=[])).push(f)},off(o,f){this._handlers[o]=this._handlers[o]?.filter(u=>u!==f)},emit(o,f){this._handlers[o]?.forEach(u=>u(f))}},AppState={_apps:[],getApps(){return this._apps},setApps(o){this._apps=o,window.APPS=o,DC_BUS.emit("apps:changed",o)},findApp(o){return this._apps.find(f=>f.id===o)},addApp(o){this._apps.push(o),window.APPS=this._apps,DC_BUS.emit("apps:changed",this._apps)},removeApp(o){const f=this._apps.findIndex(u=>u.id===o);return f>-1&&(this._apps.splice(f,1),window.APPS=this._apps,DC_BUS.emit("apps:changed",this._apps)),f>-1},updateApp(o,f){const u=this._apps.find(g=>g.id===o);if(u){for(const[g,i]of Object.entries(f))g!=="__proto__"&&g!=="constructor"&&g!=="prototype"&&(u[g]=i);DC_BUS.emit("apps:changed",this._apps)}return u}};(function(){function o(){const g=document.createElement("div");return g.className="skeleton-card",g.innerHTML='
',g}function f(g){const i=document.getElementById("cards");if(!(!i||i.querySelector(".card"))){g=g||6;for(let s=0;s.4,P={};return P.hover=C?l(b,L,.35):l(b,$,.08),P["card-hover"]=l(b,P.hover,.5),P.base=l(L,b,.6),P["fg-muted"]=l(x,L,.35),P.success=I,P.error=S,P.warning=C?"#d68a00":"#f39c12",P}function a(E,L){var $=L.lightBg||L.bg&&y(L.bg)>.4,x=L.accent||L["accent-strong"]||"#888888",b=m(x);return $?":root."+E+` body { + `,document.body.appendChild(h),u>0&&setTimeout(()=>h.remove(),u)}function timeAgo(o){const f=Date.now()-new Date(o).getTime();return f<6e4?"just now":f<36e5?Math.floor(f/6e4)+"m ago":f<864e5?Math.floor(f/36e5)+"h ago":Math.floor(f/864e5)+"d ago"}function safeGet(o,f=null){try{const u=localStorage.getItem(o);return u!==null?u:f}catch{return f}}function safeSet(o,f){try{localStorage.setItem(o,f)}catch{}}function safeRemove(o){try{localStorage.removeItem(o)}catch{}}function safeSessionGet(o,f=null){try{const u=sessionStorage.getItem(o);return u!==null?u:f}catch{return f}}function safeSessionSet(o,f){try{sessionStorage.setItem(o,f)}catch{}}function safeGetJSON(o,f=null){try{const u=localStorage.getItem(o);return u?JSON.parse(u):f}catch{return f}}function escapeHtml(o){return String(o??"").replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}function injectModal(o,f){document.getElementById(o)||document.body.insertAdjacentHTML("beforeend",f)}const DC_BUS={_handlers:{},on(o,f){var u;((u=this._handlers)[o]||(u[o]=[])).push(f)},off(o,f){this._handlers[o]=this._handlers[o]?.filter(u=>u!==f)},emit(o,f){this._handlers[o]?.forEach(u=>u(f))}},AppState={_apps:[],getApps(){return this._apps},setApps(o){this._apps=o,window.APPS=o,DC_BUS.emit("apps:changed",o)},findApp(o){return this._apps.find(f=>f.id===o)},addApp(o){this._apps.push(o),window.APPS=this._apps,DC_BUS.emit("apps:changed",this._apps)},removeApp(o){const f=this._apps.findIndex(u=>u.id===o);return f>-1&&(this._apps.splice(f,1),window.APPS=this._apps,DC_BUS.emit("apps:changed",this._apps)),f>-1},updateApp(o,f){const u=this._apps.find(y=>y.id===o);if(u){for(const[y,l]of Object.entries(f))y!=="__proto__"&&y!=="constructor"&&y!=="prototype"&&(u[y]=l);DC_BUS.emit("apps:changed",this._apps)}return u}};(function(){function o(){const y=document.createElement("div");return y.className="skeleton-card",y.innerHTML='
',y}function f(y){const l=document.getElementById("cards");if(!(!l||l.querySelector(".card"))){y=y||6;for(let r=0;r.4,A={};return A.hover=I?d(w,B,.35):d(w,$,.08),A["card-hover"]=d(w,A.hover,.5),A.base=d(B,w,.6),A["fg-muted"]=d(x,B,.35),A.success=C,A.error=k,A.warning=I?"#d68a00":"#f39c12",A}function s(S,B){var $=B.lightBg||B.bg&&g(B.bg)>.4,x=B.accent||B["accent-strong"]||"#888888",w=m(x);return $?":root."+S+` body { background: - radial-gradient(1200px 800px at 10% -10%, rgba(`+b.r+","+b.g+","+b.b+`, .08), transparent 60%), - radial-gradient(1000px 700px at 110% 10%, rgba(`+b.r+","+b.g+","+b.b+`, .05), transparent 55%), + radial-gradient(1200px 800px at 10% -10%, rgba(`+w.r+","+w.g+","+w.b+`, .08), transparent 60%), + radial-gradient(1000px 700px at 110% 10%, rgba(`+w.r+","+w.g+","+w.b+`, .05), transparent 55%), var(--bg); } -`:":root."+E+` body { +`:":root."+S+` body { background: - radial-gradient(1200px 900px at 8% -12%, rgba(`+b.r+","+b.g+","+b.b+`, .10), transparent 60%), - radial-gradient(1000px 700px at 110% -10%, rgba(`+b.r+","+b.g+","+b.b+`, .07), transparent 55%), + radial-gradient(1200px 900px at 8% -12%, rgba(`+w.r+","+w.g+","+w.b+`, .10), transparent 60%), + radial-gradient(1000px 700px at 110% -10%, rgba(`+w.r+","+w.g+","+w.b+`, .07), transparent 55%), var(--bg); } -`}function p(E,L){var $=L.lightBg||L.bg&&y(L.bg)>.4;return $?":root."+E+` button:hover { +`}function p(S,B){var $=B.lightBg||B.bg&&g(B.bg)>.4;return $?":root."+S+` button:hover { background: color-mix(in srgb, var(--accent-strong) 12%, white 88%); border-color: rgba(0, 0, 0, .15); box-shadow: 0 1px 6px rgba(0, 0, 0, .08), inset 0 1px 0 rgba(255, 255, 255, .8); } -`:":root."+E+` button:hover { +`:":root."+S+` button:hover { background: color-mix(in srgb, var(--accent) 18%, transparent); border-color: color-mix(in srgb, var(--accent) 35%, var(--border)); } -`}function t(){return window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"}function c(){s.forEach(function(E){document.documentElement.style.removeProperty("--"+E)})}function v(E,L){var $=E.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"");$||($="custom"),g.indexOf($)!==-1&&($=$+"-custom");for(var x=safeGetJSON(f,{}),b=$,I=2;x[$]&&$!==L;)$=b+"-"+I++;return $}function d(E){var L=document.getElementById("user-theme-styles");L&&L.remove(),i.length=g.length,Object.keys(w).forEach(function(S){g.indexOf(S)===-1&&delete w[S]});var $=E||safeGetJSON(f,{}),x=Object.keys($);if(x=x.filter(function(S){return g.indexOf(S)===-1}),!!x.length){var b="";x.forEach(function(S){var C=$[S];i.indexOf(S)===-1&&i.push(S);var P={};s.forEach(function(O){C[O]&&(P[O]=C[O])}),P["card-bg"]=C["card-base"]||C.bg,C.lightBg&&(P.lightBg=!0);var D=e(P);n.forEach(function(O){!P[O]&&D[O]&&(P[O]=D[O])}),w[S]=P,b+=":root."+S+` { -`,s.forEach(function(O){P[O]&&(b+=" --"+O+": "+P[O]+`; -`)}),b+=`} -`,b+=a(S,P),b+=p(S,P)});var I=document.createElement("style");I.id="user-theme-styles",I.textContent=b,document.head.appendChild(I)}}function k(){secureFetch("/api/v1/themes").then(function(E){return E.json()}).then(function(E){if(!(!E.success||!E.themes)){var L=E.themes,$=safeGetJSON(f,{});if(JSON.stringify(L)!==JSON.stringify($)){safeSet(f,JSON.stringify(L)),d(L);var x=safeGet(o);x&&i.indexOf(x)!==-1&&T(x)}}}).catch(function(){})}function B(){var E=safeGetJSON(u);if(E){var L=E.name||"Custom",$=v(L),x={name:L};s.forEach(function(S){E[S]&&(x[S]=E[S])});var b=safeGetJSON(f,{});b[$]=x,safeSet(f,JSON.stringify(b)),safeGet(o)==="custom"&&safeSet(o,$),safeRemove(u);var I={};s.forEach(function(S){x[S]&&(I[S]=x[S])}),fetch("/api/v1/themes/"+$,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:L,colors:I})}).catch(function(){})}}function T(E){document.documentElement.classList.add("theme-transitioning"),i.forEach(function(b){b!=="dark"&&document.documentElement.classList.remove(b)}),c(),E!=="dark"&&document.documentElement.classList.add(E),safeSet(o,E);var L=w[E],$=document.querySelector('meta[name="theme-color"]');$&&L&&$.setAttribute("content",L.bg);var x=L&&L.lightBg;!x&&L&&L.bg&&(x=y(L.bg)>.4),x?document.documentElement.classList.add("light-bg"):document.documentElement.classList.remove("light-bg"),setTimeout(function(){document.documentElement.classList.remove("theme-transitioning")},300)}B(),d();var A=safeGet(o);A==="red"&&(A="black",safeSet(o,"black")),A&&A!=="dark"&&i.indexOf(A)===-1&&(A=null),T(A||t()),k(),window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",function(E){safeGet(o)||T(E.matches?"dark":"light")}),window.THEMES=i,window.BUILTIN_THEMES=g,window.THEME_COLORS=w,window.THEME_PROPS=s,window.BASE_PROPS=h,window.DERIVED_PROPS=n,window.USER_THEMES_KEY=f,window.applyTheme=T,window.clearCustomProperties=c,window.injectUserThemeStyles=d,window.syncThemesFromServer=k,window.slugifyThemeName=v,window.getActiveTheme=function(){return safeGet(o)||t()},window.deriveExtendedColors=e,window.hexToRgb=m,window.rgbToHex=r,window.blendColors=l})(),(function(){function o(){const h=document.querySelector(".totp-card");if(!h)return;const w=getComputedStyle(h).backgroundColor.match(/\d+/g);if(!w)return;const m=(.299*+w[0]+.587*+w[1]+.114*+w[2])/255,r=h.querySelector(".totp-logo-dark"),l=h.querySelector(".totp-logo-light");r&&(r.style.display=m>.5?"none":""),l&&(l.style.display=m>.5?"":"none")}function f(){const h=document.getElementById("totp-overlay");if(h){h.classList.add("show"),setTimeout(o,50);const n=h.querySelector(".totp-digits input");n&&setTimeout(()=>n.focus(),100)}}function u(){const h=document.getElementById("totp-overlay");h&&h.classList.remove("show")}const g=document.getElementById("totp-digits");if(g){const h=g.querySelectorAll("input");h.forEach((n,w)=>{n.addEventListener("input",m=>{const r=m.target.value.replace(/\D/g,"");m.target.value=r.slice(0,1),r&&wy.value).join("");l.length===6&&i(l)}),n.addEventListener("keydown",m=>{m.key==="Backspace"&&!m.target.value&&w>0&&(h[w-1].focus(),h[w-1].value="")}),n.addEventListener("paste",m=>{m.preventDefault();const r=(m.clipboardData.getData("text")||"").replace(/\D/g,"");r.length>=6&&(h.forEach((l,y)=>{l.value=r[y]||""}),h[5].focus(),i(r.slice(0,6)))})})}async function i(h){const n=document.getElementById("totp-error");n.textContent="Verifying...",n.className="totp-error verifying";try{const m=await(await secureFetch("/api/v1/totp/verify",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({code:h})})).json();if(m.success){n.textContent="",m.csrfToken&&(csrfToken=m.csrfToken),u();const r=safeSessionGet("totp_redirect");if(r){try{sessionStorage.removeItem("totp_redirect")}catch{}window.location.href=r;return}typeof window.initializeDashboard=="function"&&window.initializeDashboard()}else{n.textContent=m.error||"Invalid code",n.className="totp-error";const r=document.querySelectorAll("#totp-digits input");r.forEach(l=>{l.value=""}),r[0]?.focus()}}catch{n.textContent="Connection error",n.className="totp-error"}}const s=new URLSearchParams(window.location.search);if(s.get("auth")==="required"){const h=s.get("return");if(h)try{const n=new URL(h,window.location.origin),w=n.hostname,m=n.origin===window.location.origin,r=SITE.tld.startsWith(".")?SITE.tld:"."+SITE.tld,l=w.endsWith(r)||w===r.substring(1);(m||l)&&safeSessionSet("totp_redirect",h)}catch{}window.history.replaceState({},"",window.location.pathname)}window._showTotpOverlay=f})(),(function(){const o=new ErrorHandler;injectModal("folder-browser-modal",`
+`}function t(){return window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"}function c(){r.forEach(function(S){document.documentElement.style.removeProperty("--"+S)})}function v(S,B){var $=S.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"");$||($="custom"),y.indexOf($)!==-1&&($=$+"-custom");for(var x=safeGetJSON(f,{}),w=$,C=2;x[$]&&$!==B;)$=w+"-"+C++;return $}function i(S){var B=document.getElementById("user-theme-styles");B&&B.remove(),l.length=y.length,Object.keys(b).forEach(function(k){y.indexOf(k)===-1&&delete b[k]});var $=S||safeGetJSON(f,{}),x=Object.keys($);if(x=x.filter(function(k){return y.indexOf(k)===-1}),!!x.length){var w="";x.forEach(function(k){var I=$[k];l.indexOf(k)===-1&&l.push(k);var A={};r.forEach(function(D){I[D]&&(A[D]=I[D])}),A["card-bg"]=I["card-base"]||I.bg,I.lightBg&&(A.lightBg=!0);var O=e(A);a.forEach(function(D){!A[D]&&O[D]&&(A[D]=O[D])}),b[k]=A,w+=":root."+k+` { +`,r.forEach(function(D){A[D]&&(w+=" --"+D+": "+A[D]+`; +`)}),w+=`} +`,w+=s(k,A),w+=p(k,A)});var C=document.createElement("style");C.id="user-theme-styles",C.textContent=w,document.head.appendChild(C)}}function E(){secureFetch("/api/v1/themes").then(function(S){return S.json()}).then(function(S){if(!(!S.success||!S.themes)){var B=S.themes,$=safeGetJSON(f,{});if(JSON.stringify(B)!==JSON.stringify($)){safeSet(f,JSON.stringify(B)),i(B);var x=safeGet(o);x&&l.indexOf(x)!==-1&&L(x)}}}).catch(function(){})}function T(){var S=safeGetJSON(u);if(S){var B=S.name||"Custom",$=v(B),x={name:B};r.forEach(function(k){S[k]&&(x[k]=S[k])});var w=safeGetJSON(f,{});w[$]=x,safeSet(f,JSON.stringify(w)),safeGet(o)==="custom"&&safeSet(o,$),safeRemove(u);var C={};r.forEach(function(k){x[k]&&(C[k]=x[k])}),fetch("/api/v1/themes/"+$,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:B,colors:C})}).catch(function(){})}}function L(S){document.documentElement.classList.add("theme-transitioning"),l.forEach(function(w){w!=="dark"&&document.documentElement.classList.remove(w)}),c(),S!=="dark"&&document.documentElement.classList.add(S),safeSet(o,S);var B=b[S],$=document.querySelector('meta[name="theme-color"]');$&&B&&$.setAttribute("content",B.bg);var x=B&&B.lightBg;!x&&B&&B.bg&&(x=g(B.bg)>.4),x?document.documentElement.classList.add("light-bg"):document.documentElement.classList.remove("light-bg"),setTimeout(function(){document.documentElement.classList.remove("theme-transitioning")},300)}T(),i();var P=safeGet(o);P==="red"&&(P="black",safeSet(o,"black")),P&&P!=="dark"&&l.indexOf(P)===-1&&(P=null),L(P||t()),E(),window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",function(S){safeGet(o)||L(S.matches?"dark":"light")}),window.THEMES=l,window.BUILTIN_THEMES=y,window.THEME_COLORS=b,window.THEME_PROPS=r,window.BASE_PROPS=h,window.DERIVED_PROPS=a,window.USER_THEMES_KEY=f,window.applyTheme=L,window.clearCustomProperties=c,window.injectUserThemeStyles=i,window.syncThemesFromServer=E,window.slugifyThemeName=v,window.getActiveTheme=function(){return safeGet(o)||t()},window.deriveExtendedColors=e,window.hexToRgb=m,window.rgbToHex=n,window.blendColors=d})(),(function(){function o(){const h=document.querySelector(".totp-card");if(!h)return;const b=getComputedStyle(h).backgroundColor.match(/\d+/g);if(!b)return;const m=(.299*+b[0]+.587*+b[1]+.114*+b[2])/255,n=h.querySelector(".totp-logo-dark"),d=h.querySelector(".totp-logo-light");n&&(n.style.display=m>.5?"none":""),d&&(d.style.display=m>.5?"":"none")}function f(){const h=document.getElementById("totp-overlay");if(h){h.classList.add("show"),setTimeout(o,50);const a=h.querySelector(".totp-digits input");a&&setTimeout(()=>a.focus(),100)}}function u(){const h=document.getElementById("totp-overlay");h&&h.classList.remove("show")}const y=document.getElementById("totp-digits");if(y){const h=y.querySelectorAll("input");h.forEach((a,b)=>{a.addEventListener("input",m=>{const n=m.target.value.replace(/\D/g,"");m.target.value=n.slice(0,1),n&&bg.value).join("");d.length===6&&l(d)}),a.addEventListener("keydown",m=>{m.key==="Backspace"&&!m.target.value&&b>0&&(h[b-1].focus(),h[b-1].value="")}),a.addEventListener("paste",m=>{m.preventDefault();const n=(m.clipboardData.getData("text")||"").replace(/\D/g,"");n.length>=6&&(h.forEach((d,g)=>{d.value=n[g]||""}),h[5].focus(),l(n.slice(0,6)))})})}async function l(h){const a=document.getElementById("totp-error");a.textContent="Verifying...",a.className="totp-error verifying";try{const m=await(await secureFetch("/api/v1/totp/verify",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({code:h})})).json();if(m.success){a.textContent="",m.csrfToken&&(csrfToken=m.csrfToken),u();const n=safeSessionGet("totp_redirect");if(n){try{sessionStorage.removeItem("totp_redirect")}catch{}window.location.href=n;return}typeof window.initializeDashboard=="function"&&window.initializeDashboard()}else{a.textContent=m.error||"Invalid code",a.className="totp-error";const n=document.querySelectorAll("#totp-digits input");n.forEach(d=>{d.value=""}),n[0]?.focus()}}catch{a.textContent="Connection error",a.className="totp-error"}}const r=new URLSearchParams(window.location.search);if(r.get("auth")==="required"){const h=r.get("return");if(h)try{const a=new URL(h,window.location.origin),b=a.hostname,m=a.origin===window.location.origin,n=SITE.tld.startsWith(".")?SITE.tld:"."+SITE.tld,d=b.endsWith(n)||b===n.substring(1);(m||d)&&safeSessionSet("totp_redirect",h)}catch{}window.history.replaceState({},"",window.location.pathname)}window._showTotpOverlay=f})(),(function(){const o=new ErrorHandler;injectModal("folder-browser-modal",`

\u{1F4C2} Browse for Media Folders

@@ -145,7 +145,7 @@
-
`);const f=document.getElementById("service-creds-modal");let u=null;const g=["sonarr","radarr","prowlarr","overseerr"],i=["sonarr","radarr"];function s(r){return r.externalUrl||r.url||""}function h(r){const l=document.getElementById("svc-creds-error");l.textContent=r,l.style.display=""}function n(){const r=document.getElementById("svc-creds-error");r.textContent="",r.style.display="none"}window.openServiceCredsModal=async function(r){u=r,n();const l=document.getElementById("svc-creds-title"),y=document.getElementById("svc-creds-desc"),e=document.getElementById("svc-creds-seedhost"),a=document.getElementById("svc-creds-apikey"),p=document.getElementById("svc-creds-basic"),t=document.getElementById("svc-creds-quality");l.textContent=r.name+" Credentials";const c=!!r.isExternal,v=g.includes(r.id)||g.includes(r.appTemplate),d=i.includes(r.id)||i.includes(r.appTemplate);e.style.display=c?"":"none",a.style.display=v?"":"none",t.style.display=d?"":"none",p.style.display=c?"none":"";const k=document.getElementById("svc-quality-select");k.innerHTML='',document.getElementById("svc-quality-status").textContent="",c?(y.textContent="Seedhost credentials auto-login past the HTTP prompt. API key bypasses the app login.",document.getElementById("svc-seedhost-pass").placeholder=`Password for ${r.name}`):v?y.textContent="API key bypasses the app login screen automatically.":y.textContent="Credentials are injected automatically when accessing this service.",await w(r),f.classList.add("show")};async function w(r){const l=document.getElementById("svc-creds-dot"),y=document.getElementById("svc-creds-status"),e=document.getElementById("svc-creds-clear");let a=!1;if(r.isExternal){try{const c=await(await fetch(`/api/v1/seedhost-creds?serviceId=${r.id}`)).json();c.success?(document.getElementById("svc-seedhost-user").value=c.username||"",c.hasCredentials&&(a=!0)):document.getElementById("svc-seedhost-user").value=""}catch{}document.getElementById("svc-seedhost-pass").value=""}try{const c=await(await fetch(`/api/v1/services/${r.id}/credentials`)).json();c.success&&(c.hasApiKey?(document.getElementById("svc-apikey-input").value="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",a=!0):document.getElementById("svc-apikey-input").value="",c.hasBasicAuth&&!r.isExternal?(document.getElementById("svc-basic-user").value=c.username||"",a=!0):document.getElementById("svc-basic-user").value="")}catch{}document.getElementById("svc-basic-pass")&&(document.getElementById("svc-basic-pass").value="");const p=r.id||r.appTemplate;if(i.includes(p)&&await m(r),a){l.style.background="var(--ok-fg, #74dfc4)",y.style.color="var(--ok-fg, #74dfc4)",y.textContent="Credentials stored",e.style.display="";const t=document.getElementById(`creds-btn-${r.id}`);t&&t.classList.add("has-creds")}else l.style.background="var(--muted)",y.style.color="var(--muted)",y.textContent="No credentials stored",e.style.display="none"}async function m(r){const l=document.getElementById("svc-quality-select"),y=document.getElementById("svc-quality-status"),e=r.id||r.appTemplate,a=s(r);if(!a){l.innerHTML='';return}l.innerHTML='',y.textContent="";try{const p=new URLSearchParams({service:e,url:a}),c=await(await fetch(`/api/v1/arr/quality-profiles?${p}`)).json();if(!c.success||!c.profiles?.length){l.innerHTML='';return}l.innerHTML="";for(const v of c.profiles){const d=document.createElement("option");d.value=v.id,d.textContent=v.name,l.appendChild(d)}if(c.storedProfileId&&(l.value=String(c.storedProfileId)),!l.value){const v=c.profiles.find(d=>/720/i.test(d.name));v&&(l.value=String(v.id))}!l.value&&c.profiles.length&&(l.value=String(c.profiles[0].id)),y.innerHTML=`${c.profiles.length} profiles loaded`}catch(p){l.innerHTML='',y.innerHTML=`Error: ${p.message}`}}document.getElementById("svc-quality-fetch")?.addEventListener("click",async()=>{if(!u)return;const r=u.id||u.appTemplate,l=s(u),e=document.getElementById("svc-apikey-input")?.value.trim(),a=document.getElementById("svc-quality-select"),p=document.getElementById("svc-quality-status");if(!l){p.innerHTML='No service URL available';return}if(!e||e==="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022"){p.innerHTML='Enter an API key first';return}a.innerHTML='',p.textContent="";try{const t=new URLSearchParams({service:r,url:l,apiKey:e}),v=await(await fetch(`/api/v1/arr/quality-profiles?${t}`)).json();if(!v.success){a.innerHTML='',p.innerHTML=`${v.error||"Failed to fetch profiles"}`;return}if(!v.profiles?.length){a.innerHTML='';return}a.innerHTML="";for(const k of v.profiles){const B=document.createElement("option");B.value=k.id,B.textContent=k.name,a.appendChild(B)}const d=v.profiles.find(k=>/720/i.test(k.name));d?a.value=String(d.id):v.profiles.length&&(a.value=String(v.profiles[0].id)),p.innerHTML=`${v.profiles.length} profiles loaded`}catch(t){a.innerHTML='',p.innerHTML=`${t.message}`}}),document.getElementById("svc-creds-save")?.addEventListener("click",async()=>{if(!u)return;const r=document.getElementById("svc-creds-save");r.textContent="Saving...",r.disabled=!0,n();try{const l=g.includes(u.id)||g.includes(u.appTemplate),y=u.id||u.appTemplate;if(u.isExternal){const p=document.getElementById("svc-seedhost-user").value.trim(),t=document.getElementById("svc-seedhost-pass").value;p&&await secureFetch("/api/v1/seedhost-creds",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:p,password:t||void 0,serviceId:u.id})})}const a=document.getElementById("svc-apikey-input")?.value.trim();if(a&&a!=="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022")if(l){const p=s(u),t=document.getElementById("svc-quality-select"),c=t?.value?parseInt(t.value):void 0,v=t?.selectedOptions?.[0]?.textContent||void 0,k=await(await secureFetch("/api/v1/arr/credentials",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({service:y,apiKey:a,url:p||void 0,qualityProfileId:c||void 0,qualityProfileName:v||void 0})})).json();if(!k.success){h(k.error||"Failed to save API key"),r.textContent="Save",r.disabled=!1;return}k.connectionTest&&!k.connectionTest.success&&h(`API key saved but connection test failed: ${k.connectionTest.error}`)}else await secureFetch(`/api/v1/services/${u.id}/credentials`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({apiKey:a})});else if(l&&i.includes(y)){const p=document.getElementById("svc-quality-select"),t=p?.value?parseInt(p.value):void 0,c=p?.selectedOptions?.[0]?.textContent||void 0;t&&await secureFetch("/api/v1/arr/quality-profiles",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({service:y,qualityProfileId:t,qualityProfileName:c})})}if(!u.isExternal){const p=document.getElementById("svc-basic-user").value.trim(),t=document.getElementById("svc-basic-pass").value;p&&t&&await secureFetch(`/api/v1/services/${u.id}/credentials`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:p,password:t})})}await w(u)}catch(l){o.logError("[ServiceCredentials] Save",l,{function:"saveCredentials"}),h("Failed to save: "+(l.message||"Unknown error"))}r.textContent="Save",r.disabled=!1}),document.getElementById("svc-creds-clear")?.addEventListener("click",async()=>{if(u&&confirm(`Remove stored credentials for ${u.name}?`)){n();try{const r=u.id||u.appTemplate,l=g.includes(r);u.isExternal&&await secureFetch(`/api/v1/seedhost-creds?serviceId=${u.id}`,{method:"DELETE"}),await secureFetch(`/api/v1/services/${u.id}/credentials`,{method:"DELETE"}),l&&await secureFetch(`/api/v1/arr/credentials/${r}`,{method:"DELETE"});const y=document.getElementById(`creds-btn-${u.id}`);y&&y.classList.remove("has-creds"),await w(u)}catch(r){o.logError("[ServiceCredentials] Clear",r,{function:"clearCredentials"}),h("Failed to clear: "+(r.message||"Unknown error"))}}}),document.getElementById("svc-creds-close")?.addEventListener("click",()=>{f.classList.remove("show"),u=null}),f?.addEventListener("click",r=>{r.target===f&&(f.classList.remove("show"),u=null)}),window.refreshCredsButtons=async function(){try{for(const r of window.APPS||[]){if(!r.isExternal&&!r.appTemplate&&!r.url)continue;let l=!1;if(r.isExternal)try{const a=await(await fetch(`/api/v1/seedhost-creds?serviceId=${r.id}`)).json();a.success&&a.hasCredentials&&(l=!0)}catch{}try{const a=await(await fetch(`/api/v1/services/${r.id}/credentials`)).json();a.success&&(a.hasApiKey||a.hasBasicAuth)&&(l=!0)}catch{}const y=document.getElementById(`creds-btn-${r.id}`);y&&y.classList.toggle("has-creds",l)}}catch{}}})(),(function(){const o=new ErrorHandler;injectModal("totp-settings-modal",`
+
`);const f=document.getElementById("service-creds-modal");let u=null;const y=["sonarr","radarr","prowlarr","overseerr"],l=["sonarr","radarr"];function r(n){return n.externalUrl||n.url||""}function h(n){const d=document.getElementById("svc-creds-error");d.textContent=n,d.style.display=""}function a(){const n=document.getElementById("svc-creds-error");n.textContent="",n.style.display="none"}window.openServiceCredsModal=async function(n){u=n,a();const d=document.getElementById("svc-creds-title"),g=document.getElementById("svc-creds-desc"),e=document.getElementById("svc-creds-seedhost"),s=document.getElementById("svc-creds-apikey"),p=document.getElementById("svc-creds-basic"),t=document.getElementById("svc-creds-quality");d.textContent=n.name+" Credentials";const c=!!n.isExternal,v=y.includes(n.id)||y.includes(n.appTemplate),i=l.includes(n.id)||l.includes(n.appTemplate);e.style.display=c?"":"none",s.style.display=v?"":"none",t.style.display=i?"":"none",p.style.display=c?"none":"";const E=document.getElementById("svc-quality-select");E.innerHTML='',document.getElementById("svc-quality-status").textContent="",c?(g.textContent="Seedhost credentials auto-login past the HTTP prompt. API key bypasses the app login.",document.getElementById("svc-seedhost-pass").placeholder=`Password for ${n.name}`):v?g.textContent="API key bypasses the app login screen automatically.":g.textContent="Credentials are injected automatically when accessing this service.",await b(n),f.classList.add("show")};async function b(n){const d=document.getElementById("svc-creds-dot"),g=document.getElementById("svc-creds-status"),e=document.getElementById("svc-creds-clear");let s=!1;if(n.isExternal){try{const c=await(await fetch(`/api/v1/seedhost-creds?serviceId=${n.id}`)).json();c.success?(document.getElementById("svc-seedhost-user").value=c.username||"",c.hasCredentials&&(s=!0)):document.getElementById("svc-seedhost-user").value=""}catch{}document.getElementById("svc-seedhost-pass").value=""}try{const c=await(await fetch(`/api/v1/services/${n.id}/credentials`)).json();c.success&&(c.hasApiKey?(document.getElementById("svc-apikey-input").value="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",s=!0):document.getElementById("svc-apikey-input").value="",c.hasBasicAuth&&!n.isExternal?(document.getElementById("svc-basic-user").value=c.username||"",s=!0):document.getElementById("svc-basic-user").value="")}catch{}document.getElementById("svc-basic-pass")&&(document.getElementById("svc-basic-pass").value="");const p=n.id||n.appTemplate;if(l.includes(p)&&await m(n),s){d.style.background="var(--ok-fg, #74dfc4)",g.style.color="var(--ok-fg, #74dfc4)",g.textContent="Credentials stored",e.style.display="";const t=document.getElementById(`creds-btn-${n.id}`);t&&t.classList.add("has-creds")}else d.style.background="var(--muted)",g.style.color="var(--muted)",g.textContent="No credentials stored",e.style.display="none"}async function m(n){const d=document.getElementById("svc-quality-select"),g=document.getElementById("svc-quality-status"),e=n.id||n.appTemplate,s=r(n);if(!s){d.innerHTML='';return}d.innerHTML='',g.textContent="";try{const p=new URLSearchParams({service:e,url:s}),c=await(await fetch(`/api/v1/arr/quality-profiles?${p}`)).json();if(!c.success||!c.profiles?.length){d.innerHTML='';return}d.innerHTML="";for(const v of c.profiles){const i=document.createElement("option");i.value=v.id,i.textContent=v.name,d.appendChild(i)}if(c.storedProfileId&&(d.value=String(c.storedProfileId)),!d.value){const v=c.profiles.find(i=>/720/i.test(i.name));v&&(d.value=String(v.id))}!d.value&&c.profiles.length&&(d.value=String(c.profiles[0].id)),g.innerHTML=`${c.profiles.length} profiles loaded`}catch(p){d.innerHTML='',g.innerHTML=`Error: ${p.message}`}}document.getElementById("svc-quality-fetch")?.addEventListener("click",async()=>{if(!u)return;const n=u.id||u.appTemplate,d=r(u),e=document.getElementById("svc-apikey-input")?.value.trim(),s=document.getElementById("svc-quality-select"),p=document.getElementById("svc-quality-status");if(!d){p.innerHTML='No service URL available';return}if(!e||e==="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022"){p.innerHTML='Enter an API key first';return}s.innerHTML='',p.textContent="";try{const t=new URLSearchParams({service:n,url:d,apiKey:e}),v=await(await fetch(`/api/v1/arr/quality-profiles?${t}`)).json();if(!v.success){s.innerHTML='',p.innerHTML=`${v.error||"Failed to fetch profiles"}`;return}if(!v.profiles?.length){s.innerHTML='';return}s.innerHTML="";for(const E of v.profiles){const T=document.createElement("option");T.value=E.id,T.textContent=E.name,s.appendChild(T)}const i=v.profiles.find(E=>/720/i.test(E.name));i?s.value=String(i.id):v.profiles.length&&(s.value=String(v.profiles[0].id)),p.innerHTML=`${v.profiles.length} profiles loaded`}catch(t){s.innerHTML='',p.innerHTML=`${t.message}`}}),document.getElementById("svc-creds-save")?.addEventListener("click",async()=>{if(!u)return;const n=document.getElementById("svc-creds-save");n.textContent="Saving...",n.disabled=!0,a();try{const d=y.includes(u.id)||y.includes(u.appTemplate),g=u.id||u.appTemplate;if(u.isExternal){const p=document.getElementById("svc-seedhost-user").value.trim(),t=document.getElementById("svc-seedhost-pass").value;p&&await secureFetch("/api/v1/seedhost-creds",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:p,password:t||void 0,serviceId:u.id})})}const s=document.getElementById("svc-apikey-input")?.value.trim();if(s&&s!=="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022")if(d){const p=r(u),t=document.getElementById("svc-quality-select"),c=t?.value?parseInt(t.value):void 0,v=t?.selectedOptions?.[0]?.textContent||void 0,E=await(await secureFetch("/api/v1/arr/credentials",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({service:g,apiKey:s,url:p||void 0,qualityProfileId:c||void 0,qualityProfileName:v||void 0})})).json();if(!E.success){h(E.error||"Failed to save API key"),n.textContent="Save",n.disabled=!1;return}E.connectionTest&&!E.connectionTest.success&&h(`API key saved but connection test failed: ${E.connectionTest.error}`)}else await secureFetch(`/api/v1/services/${u.id}/credentials`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({apiKey:s})});else if(d&&l.includes(g)){const p=document.getElementById("svc-quality-select"),t=p?.value?parseInt(p.value):void 0,c=p?.selectedOptions?.[0]?.textContent||void 0;t&&await secureFetch("/api/v1/arr/quality-profiles",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({service:g,qualityProfileId:t,qualityProfileName:c})})}if(!u.isExternal){const p=document.getElementById("svc-basic-user").value.trim(),t=document.getElementById("svc-basic-pass").value;p&&t&&await secureFetch(`/api/v1/services/${u.id}/credentials`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:p,password:t})})}await b(u)}catch(d){o.logError("[ServiceCredentials] Save",d,{function:"saveCredentials"}),h("Failed to save: "+(d.message||"Unknown error"))}n.textContent="Save",n.disabled=!1}),document.getElementById("svc-creds-clear")?.addEventListener("click",async()=>{if(u&&confirm(`Remove stored credentials for ${u.name}?`)){a();try{const n=u.id||u.appTemplate,d=y.includes(n);u.isExternal&&await secureFetch(`/api/v1/seedhost-creds?serviceId=${u.id}`,{method:"DELETE"}),await secureFetch(`/api/v1/services/${u.id}/credentials`,{method:"DELETE"}),d&&await secureFetch(`/api/v1/arr/credentials/${n}`,{method:"DELETE"});const g=document.getElementById(`creds-btn-${u.id}`);g&&g.classList.remove("has-creds"),await b(u)}catch(n){o.logError("[ServiceCredentials] Clear",n,{function:"clearCredentials"}),h("Failed to clear: "+(n.message||"Unknown error"))}}}),document.getElementById("svc-creds-close")?.addEventListener("click",()=>{f.classList.remove("show"),u=null}),f?.addEventListener("click",n=>{n.target===f&&(f.classList.remove("show"),u=null)}),window.refreshCredsButtons=async function(){try{for(const n of window.APPS||[]){if(!n.isExternal&&!n.appTemplate&&!n.url)continue;let d=!1;if(n.isExternal)try{const s=await(await fetch(`/api/v1/seedhost-creds?serviceId=${n.id}`)).json();s.success&&s.hasCredentials&&(d=!0)}catch{}try{const s=await(await fetch(`/api/v1/services/${n.id}/credentials`)).json();s.success&&(s.hasApiKey||s.hasBasicAuth)&&(d=!0)}catch{}const g=document.getElementById(`creds-btn-${n.id}`);g&&g.classList.toggle("has-creds",d)}}catch{}}})(),(function(){const o=new ErrorHandler;injectModal("totp-settings-modal",`

Authentication Settings

@@ -240,7 +240,7 @@
- `);async function f(){try{const s=await(await fetch("/api/v1/totp/config")).json();if(!s.success)return;const{enabled:h,sessionDuration:n,isSetUp:w}=s.config,m=document.getElementById("totp-status-dot"),r=document.getElementById("totp-status-text"),l=document.getElementById("totp-status-banner"),y=document.getElementById("totp-setup-section"),e=document.getElementById("totp-qr-section"),a=document.getElementById("totp-duration-section"),p=document.getElementById("totp-disable-section");h&&w?(m.style.background="var(--ok-fg, #7ef2ff)",l.style.borderColor="var(--ok-fg, #7ef2ff)",l.style.background="color-mix(in srgb, var(--ok-fg) 8%, transparent)",r.textContent="TOTP is active",r.style.color="var(--ok-fg, #7ef2ff)",y.style.display="none",e.style.display="none",a.style.display="block",p.style.display="block",document.getElementById("totp-duration-select").value=n):(m.style.background="var(--muted)",l.style.borderColor="var(--border)",l.style.background="transparent",r.textContent="TOTP is not configured",r.style.color="var(--muted)",y.style.display="block",e.style.display="none",a.style.display="none",p.style.display="none"),g(h&&w,n)}catch(i){console.warn("Failed to load TOTP settings:",i)}}const u={"15m":"15 min","30m":"30 min","1h":"1 hour","2h":"2 hours","4h":"4 hours","8h":"8 hours","12h":"12 hours","24h":"24 hours",never:"Disabled"};function g(i,s){const h=document.getElementById("auth-card"),n=document.getElementById("auth-pill"),w=document.getElementById("auth-dot"),m=document.getElementById("auth-status-text");h&&(i?(h.setAttribute("data-status","on"),n.className="badge on",n.textContent="YES",w.className="dot ok at-bl",m.textContent="Session: "+(u[s]||s)):(h.setAttribute("data-status","off"),n.className="badge off",n.textContent="NO",w.className="dot bad at-bl",m.textContent="Not configured"))}document.getElementById("totp-setup-btn")?.addEventListener("click",async()=>{try{const s=await(await secureFetch("/api/v1/totp/setup",{method:"POST"})).json();s.success&&(document.getElementById("totp-qr-image").src=s.qrCode,document.getElementById("totp-manual-key").textContent=s.manualKey,document.getElementById("totp-setup-section").style.display="none",document.getElementById("totp-qr-section").style.display="block",document.getElementById("totp-setup-code").value="",document.getElementById("totp-setup-error").textContent="",document.getElementById("totp-setup-code").focus())}catch(i){o.logError("[TOTP] Setup Failed",i,{function:"setupTOTP"})}}),document.getElementById("totp-import-btn")?.addEventListener("click",async()=>{const i=document.getElementById("totp-import-key").value.trim(),s=document.getElementById("totp-import-error");if(s.textContent="",!i){s.textContent="Paste a Base32 secret key first";return}try{const n=await(await secureFetch("/api/v1/totp/setup",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({secret:i})})).json();n.success?(s.textContent="",document.getElementById("totp-qr-image").src=n.qrCode,document.getElementById("totp-manual-key").textContent=n.manualKey,document.getElementById("totp-setup-section").style.display="none",document.getElementById("totp-qr-section").style.display="block",document.getElementById("totp-setup-code").value="",document.getElementById("totp-setup-error").textContent="",document.getElementById("totp-setup-code").focus()):s.textContent=n.error||n.message||"Import failed"}catch{s.textContent="Connection error \u2014 try refreshing the page"}}),document.getElementById("totp-copy-key")?.addEventListener("click",()=>{const i=document.getElementById("totp-manual-key").textContent;navigator.clipboard.writeText(i).then(()=>{const s=document.getElementById("totp-copy-key");s.textContent="\u2705",setTimeout(()=>{s.textContent="\u{1F4CB}"},2e3)})}),document.getElementById("totp-confirm-setup")?.addEventListener("click",async()=>{const i=document.getElementById("totp-setup-code").value,s=document.getElementById("totp-setup-error");if(!/^\d{6}$/.test(i)){s.textContent="Enter a 6-digit code";return}try{const n=await(await secureFetch("/api/v1/totp/verify-setup",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({code:i})})).json();n.success?(s.textContent="",f()):s.textContent=n.error||"Invalid code"}catch{s.textContent="Connection error"}}),document.getElementById("totp-setup-code")?.addEventListener("keydown",i=>{i.key==="Enter"&&document.getElementById("totp-confirm-setup")?.click()}),document.getElementById("totp-duration-select")?.addEventListener("change",async i=>{try{await secureFetch("/api/v1/totp/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sessionDuration:i.target.value})}),f()}catch(s){o.logError("[TOTP] Update Session Duration",s,{function:"updateSessionDuration"})}}),document.getElementById("totp-disable-btn")?.addEventListener("click",async()=>{if(confirm("Disable TOTP authentication? All services will be accessible without a code."))try{(await(await secureFetch("/api/v1/totp/disable",{method:"POST",headers:{"Content-Type":"application/json"},body:"{}"})).json()).success&&f()}catch(i){o.logError("[TOTP] Disable Failed",i,{function:"disableTOTP"})}}),document.getElementById("auth-settings-btn")?.addEventListener("click",()=>{f(),openModal("totp-settings-modal")}),document.getElementById("totp-modal-close")?.addEventListener("click",()=>{closeModal("totp-settings-modal")}),document.getElementById("totp-settings-modal")?.addEventListener("click",i=>{i.target.id==="totp-settings-modal"&&closeModal("totp-settings-modal")}),window._updateAuthCard=g,(async()=>{try{const s=await(await fetch("/api/v1/totp/config")).json();if(s.success){const h=s.config.enabled&&s.config.isSetUp;g(h,s.config.sessionDuration)}}catch(i){o.logError("[TOTP] AuthCard Update",i,{function:"authCardUpdate"})}})()})(),(function(){injectModal("token-management-modal",` + `);async function f(){try{const r=await(await fetch("/api/v1/totp/config")).json();if(!r.success)return;const{enabled:h,sessionDuration:a,isSetUp:b}=r.config,m=document.getElementById("totp-status-dot"),n=document.getElementById("totp-status-text"),d=document.getElementById("totp-status-banner"),g=document.getElementById("totp-setup-section"),e=document.getElementById("totp-qr-section"),s=document.getElementById("totp-duration-section"),p=document.getElementById("totp-disable-section");h&&b?(m.style.background="var(--ok-fg, #7ef2ff)",d.style.borderColor="var(--ok-fg, #7ef2ff)",d.style.background="color-mix(in srgb, var(--ok-fg) 8%, transparent)",n.textContent="TOTP is active",n.style.color="var(--ok-fg, #7ef2ff)",g.style.display="none",e.style.display="none",s.style.display="block",p.style.display="block",document.getElementById("totp-duration-select").value=a):(m.style.background="var(--muted)",d.style.borderColor="var(--border)",d.style.background="transparent",n.textContent="TOTP is not configured",n.style.color="var(--muted)",g.style.display="block",e.style.display="none",s.style.display="none",p.style.display="none"),y(h&&b,a)}catch(l){console.warn("Failed to load TOTP settings:",l)}}const u={"15m":"15 min","30m":"30 min","1h":"1 hour","2h":"2 hours","4h":"4 hours","8h":"8 hours","12h":"12 hours","24h":"24 hours",never:"Disabled"};function y(l,r){const h=document.getElementById("auth-card"),a=document.getElementById("auth-pill"),b=document.getElementById("auth-dot"),m=document.getElementById("auth-status-text");h&&(l?(h.setAttribute("data-status","on"),a.className="badge on",a.textContent="YES",b.className="dot ok at-bl",m.textContent="Session: "+(u[r]||r)):(h.setAttribute("data-status","off"),a.className="badge off",a.textContent="NO",b.className="dot bad at-bl",m.textContent="Not configured"))}document.getElementById("totp-setup-btn")?.addEventListener("click",async()=>{try{const r=await(await secureFetch("/api/v1/totp/setup",{method:"POST"})).json();r.success&&(document.getElementById("totp-qr-image").src=r.qrCode,document.getElementById("totp-manual-key").textContent=r.manualKey,document.getElementById("totp-setup-section").style.display="none",document.getElementById("totp-qr-section").style.display="block",document.getElementById("totp-setup-code").value="",document.getElementById("totp-setup-error").textContent="",document.getElementById("totp-setup-code").focus())}catch(l){o.logError("[TOTP] Setup Failed",l,{function:"setupTOTP"})}}),document.getElementById("totp-import-btn")?.addEventListener("click",async()=>{const l=document.getElementById("totp-import-key").value.trim(),r=document.getElementById("totp-import-error");if(r.textContent="",!l){r.textContent="Paste a Base32 secret key first";return}try{const a=await(await secureFetch("/api/v1/totp/setup",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({secret:l})})).json();a.success?(r.textContent="",document.getElementById("totp-qr-image").src=a.qrCode,document.getElementById("totp-manual-key").textContent=a.manualKey,document.getElementById("totp-setup-section").style.display="none",document.getElementById("totp-qr-section").style.display="block",document.getElementById("totp-setup-code").value="",document.getElementById("totp-setup-error").textContent="",document.getElementById("totp-setup-code").focus()):r.textContent=a.error||a.message||"Import failed"}catch{r.textContent="Connection error \u2014 try refreshing the page"}}),document.getElementById("totp-copy-key")?.addEventListener("click",()=>{const l=document.getElementById("totp-manual-key").textContent;navigator.clipboard.writeText(l).then(()=>{const r=document.getElementById("totp-copy-key");r.textContent="\u2705",setTimeout(()=>{r.textContent="\u{1F4CB}"},2e3)})}),document.getElementById("totp-confirm-setup")?.addEventListener("click",async()=>{const l=document.getElementById("totp-setup-code").value,r=document.getElementById("totp-setup-error");if(!/^\d{6}$/.test(l)){r.textContent="Enter a 6-digit code";return}try{const a=await(await secureFetch("/api/v1/totp/verify-setup",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({code:l})})).json();a.success?(r.textContent="",f()):r.textContent=a.error||"Invalid code"}catch{r.textContent="Connection error"}}),document.getElementById("totp-setup-code")?.addEventListener("keydown",l=>{l.key==="Enter"&&document.getElementById("totp-confirm-setup")?.click()}),document.getElementById("totp-duration-select")?.addEventListener("change",async l=>{try{await secureFetch("/api/v1/totp/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sessionDuration:l.target.value})}),f()}catch(r){o.logError("[TOTP] Update Session Duration",r,{function:"updateSessionDuration"})}}),document.getElementById("totp-disable-btn")?.addEventListener("click",async()=>{if(confirm("Disable TOTP authentication? All services will be accessible without a code."))try{(await(await secureFetch("/api/v1/totp/disable",{method:"POST",headers:{"Content-Type":"application/json"},body:"{}"})).json()).success&&f()}catch(l){o.logError("[TOTP] Disable Failed",l,{function:"disableTOTP"})}}),document.getElementById("auth-settings-btn")?.addEventListener("click",()=>{f(),openModal("totp-settings-modal")}),document.getElementById("totp-modal-close")?.addEventListener("click",()=>{closeModal("totp-settings-modal")}),document.getElementById("totp-settings-modal")?.addEventListener("click",l=>{l.target.id==="totp-settings-modal"&&closeModal("totp-settings-modal")}),window._updateAuthCard=y,(async()=>{try{const r=await(await fetch("/api/v1/totp/config")).json();if(r.success){const h=r.config.enabled&&r.config.isSetUp;y(h,r.config.sessionDuration)}}catch(l){o.logError("[TOTP] AuthCard Update",l,{function:"authCardUpdate"})}})()})(),(function(){injectModal("token-management-modal",`

\u{1F511} DNS Credentials

@@ -281,17 +281,17 @@
- `)}function g(){let t=safeSessionGet("dashcaddy-encryption-key");if(t)return t;const c=safeGet("dashcaddy-encryption-key");if(c)return safeSessionSet("dashcaddy-encryption-key",c),safeRemove("dashcaddy-encryption-key"),c;const v=new Uint8Array(32);return crypto.getRandomValues(v),t=Array.from(v,d=>d.toString(16).padStart(2,"0")).join(""),safeSessionSet("dashcaddy-encryption-key",t),t}const i=g();function s(t,c){if(!t)return"";const v=crypto.getRandomValues(new Uint8Array(8)),d=Array.from(v,T=>T.toString(16).padStart(2,"0")).join(""),k=new TextEncoder().encode(c+d);let B="";for(let T=0;TparseInt($,16))),A=atob(t.substring(17)),E=new TextEncoder().encode(c+B);let L="";for(let $=0;${["readonly","admin"].forEach(c=>{["token","username"].forEach(v=>{safeRemove(`${t}-${c}-${v}-enc`)})}),safeRemove(`${t}-token-enc`),safeRemove(`${t}-username-enc`)})}function p(t){const c=m(t,"readonly"),v=r(t,"readonly"),d=m(t,"admin"),k=r(t,"admin"),B=h(safeGet(`${t}-token-enc`),i),T=h(safeGet(`${t}-username-enc`),i);return{username:k||v||T,token:d||c||B,readonlyToken:c||B,readonlyUsername:v||T,adminToken:d||B,adminUsername:k||T}}document.getElementById("manage-tokens")?.addEventListener("click",()=>{u();const t=document.getElementById("token-management-modal"),c=e();o().forEach(v=>{const d=c[v];document.getElementById(`${v}-readonly-username`).value=d.readonly.username,document.getElementById(`${v}-readonly-token`).value=d.readonly.token,document.getElementById(`${v}-admin-username`).value=d.admin.username,document.getElementById(`${v}-admin-token`).value=d.admin.token,document.getElementById(`${v}-token-status`).textContent=""}),t.classList.add("show")}),document.getElementById("token-management-modal")?.addEventListener("click",t=>{const c=t.target.closest(".token-toggle");if(c){const v=c.dataset.target,d=document.getElementById(v);d.type==="password"?(d.type="text",c.textContent="\u{1F648}"):(d.type="password",c.textContent="\u{1F441}");return}t.target.id==="token-management-modal"&&t.target.classList.remove("show")}),document.getElementById("token-save")?.addEventListener("click",async()=>{const t=o();t.forEach(d=>{y(d,"readonly",document.getElementById(`${d}-readonly-username`).value.trim()),l(d,"readonly",document.getElementById(`${d}-readonly-token`).value.trim()),y(d,"admin",document.getElementById(`${d}-admin-username`).value.trim()),l(d,"admin",document.getElementById(`${d}-admin-token`).value.trim())});const c={};let v=!1;if(t.forEach(d=>{const k={},B=document.getElementById(`${d}-readonly-username`).value.trim(),T=document.getElementById(`${d}-readonly-token`).value.trim(),A=document.getElementById(`${d}-admin-username`).value.trim(),E=document.getElementById(`${d}-admin-token`).value.trim();B&&T&&(k.readonly={username:B,password:T},v=!0),A&&E&&(k.admin={username:A,password:E},v=!0),Object.keys(k).length>0&&(c[d]=k)}),v){t.forEach(d=>{c[d]&&(document.getElementById(`${d}-token-status`).textContent="Verifying...",document.getElementById(`${d}-token-status`).className="token-status")});try{const k=await(await secureFetch("/api/v1/dns/credentials",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({servers:c})})).json();k.results?t.forEach(B=>{const T=document.getElementById(`${B}-token-status`);if(!c[B]){T.textContent="";return}const A=k.results[B];A?.success?(T.textContent="\u2713 Verified & saved",T.className="token-status success"):A?.partial?(T.textContent="\u2713 "+A.partial,T.className="token-status success"):(T.textContent="\u2717 "+(A?.error||"Login failed"),T.className="token-status error")}):k.success?t.forEach(B=>{c[B]&&(document.getElementById(`${B}-token-status`).textContent="\u2713 Saved",document.getElementById(`${B}-token-status`).className="token-status success")}):t.forEach(B=>{c[B]&&(document.getElementById(`${B}-token-status`).textContent="\u2717 "+(k.error||"Failed"),document.getElementById(`${B}-token-status`).className="token-status error")})}catch(d){console.error("Failed to sync DNS credentials to backend:",d),t.forEach(k=>{c[k]&&(document.getElementById(`${k}-token-status`).textContent="\u2713 Saved locally (sync failed)",document.getElementById(`${k}-token-status`).className="token-status")})}}else t.forEach(d=>{document.getElementById(`${d}-token-status`).textContent=""});setTimeout(()=>{t.every(k=>{const B=document.getElementById(`${k}-token-status`)?.textContent;return!B||B.includes("\u2713")})&&closeModal("token-management-modal")},1500)}),document.getElementById("token-cancel")?.addEventListener("click",()=>{closeModal("token-management-modal")}),document.getElementById("token-clear-all")?.addEventListener("click",async()=>{if(confirm("Clear all stored DNS credentials? This cannot be undone.")){a(),o().forEach(t=>{document.getElementById(`${t}-readonly-username`).value="",document.getElementById(`${t}-readonly-token`).value="",document.getElementById(`${t}-admin-username`).value="",document.getElementById(`${t}-admin-token`).value="",document.getElementById(`${t}-token-status`).textContent="\u2713 Cleared",document.getElementById(`${t}-token-status`).className="token-status success"});try{await secureFetch("/api/v1/dns/credentials",{method:"DELETE"})}catch{}}}),window.getToken=m,window.getUsername=r,window.setToken=l,window.setUsername=y,window.getAllCredentials=e,window.getCredential=n,window.setCredential=w,window.getEncryptionKey=g,window.getDnsIds=o,window.getDnsDisplayName=f})(),(function(){function o(l,y,e=null){const a=document.getElementById(l+"-dot"),p=document.getElementById(l+"-pill"),t=document.getElementById(l+"-time"),c=document.querySelector(`[data-app="${l}"]`);a&&(a.classList.toggle("ok",y),a.classList.toggle("bad",!y)),p&&(p.textContent=y?"ON":"OFF",p.classList.toggle("on",y),p.classList.toggle("off",!y)),t&&e!==null&&(t.textContent=y?`${e}ms`:"timeout",t.className=`response-time ${f(e,y)}`),c&&c.setAttribute("data-status",y?"on":"off")}function f(l,y){return y?l<200?"excellent":l<500?"good":l<1e3?"fair":"slow":"timeout"}async function u(l){const y=performance.now();try{const e=await fetch("/probe/"+l,{cache:"no-store"}),a=performance.now(),p=Math.round(a-y);return{isUp:e.status>=200&&e.status<400||e.status===401||e.status===403,responseTime:p}}catch{const e=performance.now();return{isUp:!1,responseTime:Math.round(e-y)}}}window.APPS=[];let g=null,i=!1;async function s(){try{window.SkeletonLoader&&window.SkeletonLoader.show(6);const l=await fetch("/api/v1/services",{cache:"no-store"});l.ok?(window.APPS=await l.json(),window.SkeletonLoader&&window.SkeletonLoader.hide()):(console.error("Failed to load services:",l.status),window.SkeletonLoader&&window.SkeletonLoader.hide())}catch(l){console.error("Failed to load services:",l),window.SkeletonLoader&&window.SkeletonLoader.hide()}}function h(l){const y=window.APPS?.find(a=>a.id===l);if(y?.url)return y.url.startsWith("http")?y.url:"https://"+y.url;if(y?.isExternal&&y.externalUrl)return y.externalUrl;const e=SITE.dnsServers?.[l];return e?"http://"+e.ip+":"+(e.port||5380):buildServiceUrl(l)}function n(l,y,e){const a=document.createElement(l);return y&&(a.className=y),e&&(a.textContent=e),a}function w(){const l=document.getElementById("cards");l.innerHTML="";for(let y=0;y{D.stopPropagation(),window.openContainerLogsModal(e.containerId,e.name)},b.appendChild(S);const C=n("button","update-btn","\u2B06\uFE0F");C.title="Update container to latest version",C.id=`update-btn-${e.id}`,C.onclick=D=>{D.stopPropagation(),window.updateContainer(e.containerId,e.name,e.id)},b.appendChild(C);const P=n("button","exec-btn",">_");P.title="Open terminal",P.onclick=D=>{D.stopPropagation(),window.openExecModal&&window.openExecModal(e.containerId,e.name)},b.appendChild(P)}if(e.logPath&&!e.containerId){const S=n("button","logs-btn","\u{1F4CB}");S.title="View application logs",S.onclick=C=>{C.stopPropagation(),window.openFileLogsModal(e.logPath,e.name)},b.appendChild(S)}if(e.isExternal||e.appTemplate||e.url){const S=n("button","creds-btn","\u{1F511}");S.title="Auto-login credentials",S.id=`creds-btn-${e.id}`,S.onclick=C=>{C.stopPropagation(),window.openServiceCredsModal(e)},b.appendChild(S)}if(e.id!=="internet"){const S=n("button","options-btn","\u2699\uFE0F");S.title="Edit service settings",S.onclick=C=>{C.stopPropagation(),window.openServiceEditModal(e)},b.appendChild(S)}if(e.id!=="internet"){const S=n("button","delete-btn","\u{1F5D1}\uFE0F");S.title="Delete this service",S.onclick=C=>{C.stopPropagation(),window.deleteService(e.id,e.name)},b.appendChild(S)}const I=n("button",null,"Open");I.onclick=()=>window.open(h(e.id),"_blank","noopener"),b.appendChild(I),a.appendChild(b),a.style.transitionDelay=`${Math.min(y*45,270)}ms`,l.appendChild(a)}requestAnimationFrame(()=>{l.querySelectorAll(".card").forEach(y=>y.classList.add("loaded"))}),window.groupRecipeCards&&requestAnimationFrame(()=>window.groupRecipeCards())}function m(l,y,e=null){const a=document.getElementById("dot-"+l+"-grid"),p=document.getElementById("badge-"+l),t=document.getElementById("time-"+l),c=document.querySelector(`[data-app="${l}"]`);a&&(a.classList.toggle("ok",y),a.classList.toggle("bad",!y)),p&&(p.textContent=y?"ON":"OFF",p.classList.toggle("on",y),p.classList.toggle("off",!y)),t&&e!==null&&(t.textContent=y?`${e}ms`:"timeout",t.className=`response-time ${f(e,y)}`),c&&c.setAttribute("data-status",y?"on":"off")}async function r(){if(g)return i=!0,g;function l(a,p=new Date){const t=document.getElementById("stamp");t&&(t.textContent=`${a}: ${new Date(p).toLocaleTimeString()}`)}function y(a){Object.keys(SITE.dnsServers).forEach(t=>{const c=a[t];c&&o(t,c.isUp,c.responseTime)}),a.internet&&o("internet",a.internet.isUp,a.internet.responseTime),window.APPS.forEach(t=>{const c=a[t.id];c&&m(t.id,c.isUp,c.responseTime)})}async function e(){const a=Object.keys(SITE.dnsServers),p=a.map(d=>u(d));p.push(u("internet"));const t=await Promise.all(p);a.forEach((d,k)=>o(d,t[k].isUp,t[k].responseTime));const c=t[t.length-1];o("internet",c.isUp,c.responseTime),(await Promise.all(window.APPS.map(async d=>{const k=await u(d.id);return{id:d.id,...k}}))).forEach(d=>{m(d.id,d.isUp,d.responseTime)})}return g=(async()=>{try{const a=await fetch("/api/v1/services/status",{cache:"no-store"});if(!a.ok)throw new Error(`Status refresh failed (${a.status})`);const p=await a.json();y(p.statuses||{}),l("last check",p.checkedAt||new Date)}catch(a){console.warn("Batched status refresh failed, falling back to direct probes:",a);try{await e(),l("last check")}catch(p){console.error("Dashboard refresh failed:",p),l("last failed")}}finally{g=null,i&&(i=!1,setTimeout(()=>{window.refreshAll()},0))}})(),g}document.querySelector(".top")?.addEventListener("click",l=>{const y=l.target.closest('[id$="-open"]');if(!y)return;const e=y.id.replace("-open","");SITE.dnsServers[e]&&window.open(h(e),"_blank","noopener")}),document.getElementById("ca-open")?.addEventListener("click",()=>window.open(h("ca"),"_blank","noopener")),document.getElementById("creds-btn-ca")?.addEventListener("click",l=>{l.stopPropagation();const y=window.APPS.find(e=>e.id==="ca");y&&window.openServiceCredsModal&&window.openServiceCredsModal(y)}),document.getElementById("options-btn-ca")?.addEventListener("click",l=>{l.stopPropagation();const y=window.APPS.find(e=>e.id==="ca");y&&window.openServiceEditModal&&window.openServiceEditModal(y)}),document.getElementById("delete-btn-ca")?.addEventListener("click",l=>{l.stopPropagation(),window.deleteService&&window.deleteService("ca","DashCA")}),window.loadServices=s,window.buildGrid=w,window.refreshAll=r,window.setQuick=o,window.setBadge=m,window.getResponseTimeClass=f,window.checkServiceWithTiming=u,window.serviceUrl=h,window.el=n})(),(function(){async function o(n){const m=await(await secureFetch(`/api/v1/dns/restart/${n}`,{method:"POST"})).json();if(!m.success)throw new Error(m.error||"Restart failed");return m}document.querySelector(".top")?.addEventListener("click",async n=>{const w=n.target.closest('[id$="-restart"]');if(!w)return;const m=w.id.replace("-restart","");if(SITE.dnsServers[m]&&confirm(`Restart ${m.toUpperCase()} service?`))try{await withButton(w,"...",()=>o(m)),setTimeout(window.refreshAll,DC.DELAYS.RELOAD)}catch(r){showNotification("Restart failed: "+r.message,"error")}});async function f(n,w){const m=document.getElementById(`${n}-update`),r=m?.textContent||"\u2B06\uFE0F";try{m.textContent="\u{1F50D}",m.disabled=!0,m.title="Checking for updates...";const y=await(await fetch(`/api/v1/dns/check-update?server=${encodeURIComponent(w)}`)).json();if(!y.success)throw new Error(y.error||"Failed to check for updates");if(!y.updateAvailable){m.textContent="\u2705",m.title=`Already on latest version (${y.currentVersion})`,showNotification(`${n.toUpperCase()} is already up to date! Current version: ${y.currentVersion}`,"info"),setTimeout(()=>{m.textContent=r,m.disabled=!1,m.title="Update DNS server"},3e3);return}if(!confirm(`Update available for ${n.toUpperCase()}! + `)}function y(){let t=safeSessionGet("dashcaddy-encryption-key");if(t)return t;const c=safeGet("dashcaddy-encryption-key");if(c)return safeSessionSet("dashcaddy-encryption-key",c),safeRemove("dashcaddy-encryption-key"),c;const v=new Uint8Array(32);return crypto.getRandomValues(v),t=Array.from(v,i=>i.toString(16).padStart(2,"0")).join(""),safeSessionSet("dashcaddy-encryption-key",t),t}const l=y();function r(t,c){if(!t)return"";const v=crypto.getRandomValues(new Uint8Array(8)),i=Array.from(v,L=>L.toString(16).padStart(2,"0")).join(""),E=new TextEncoder().encode(c+i);let T="";for(let L=0;LparseInt($,16))),P=atob(t.substring(17)),S=new TextEncoder().encode(c+T);let B="";for(let $=0;${["readonly","admin"].forEach(c=>{["token","username"].forEach(v=>{safeRemove(`${t}-${c}-${v}-enc`)})}),safeRemove(`${t}-token-enc`),safeRemove(`${t}-username-enc`)})}function p(t){const c=m(t,"readonly"),v=n(t,"readonly"),i=m(t,"admin"),E=n(t,"admin"),T=h(safeGet(`${t}-token-enc`),l),L=h(safeGet(`${t}-username-enc`),l);return{username:E||v||L,token:i||c||T,readonlyToken:c||T,readonlyUsername:v||L,adminToken:i||T,adminUsername:E||L}}document.getElementById("manage-tokens")?.addEventListener("click",()=>{u();const t=document.getElementById("token-management-modal"),c=e();o().forEach(v=>{const i=c[v];document.getElementById(`${v}-readonly-username`).value=i.readonly.username,document.getElementById(`${v}-readonly-token`).value=i.readonly.token,document.getElementById(`${v}-admin-username`).value=i.admin.username,document.getElementById(`${v}-admin-token`).value=i.admin.token,document.getElementById(`${v}-token-status`).textContent=""}),t.classList.add("show")}),document.getElementById("token-management-modal")?.addEventListener("click",t=>{const c=t.target.closest(".token-toggle");if(c){const v=c.dataset.target,i=document.getElementById(v);i.type==="password"?(i.type="text",c.textContent="\u{1F648}"):(i.type="password",c.textContent="\u{1F441}");return}t.target.id==="token-management-modal"&&t.target.classList.remove("show")}),document.getElementById("token-save")?.addEventListener("click",async()=>{const t=o();t.forEach(i=>{g(i,"readonly",document.getElementById(`${i}-readonly-username`).value.trim()),d(i,"readonly",document.getElementById(`${i}-readonly-token`).value.trim()),g(i,"admin",document.getElementById(`${i}-admin-username`).value.trim()),d(i,"admin",document.getElementById(`${i}-admin-token`).value.trim())});const c={};let v=!1;if(t.forEach(i=>{const E={},T=document.getElementById(`${i}-readonly-username`).value.trim(),L=document.getElementById(`${i}-readonly-token`).value.trim(),P=document.getElementById(`${i}-admin-username`).value.trim(),S=document.getElementById(`${i}-admin-token`).value.trim();T&&L&&(E.readonly={username:T,password:L},v=!0),P&&S&&(E.admin={username:P,password:S},v=!0),Object.keys(E).length>0&&(c[i]=E)}),v){t.forEach(i=>{c[i]&&(document.getElementById(`${i}-token-status`).textContent="Verifying...",document.getElementById(`${i}-token-status`).className="token-status")});try{const E=await(await secureFetch("/api/v1/dns/credentials",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({servers:c})})).json();E.results?t.forEach(T=>{const L=document.getElementById(`${T}-token-status`);if(!c[T]){L.textContent="";return}const P=E.results[T];P?.success?(L.textContent="\u2713 Verified & saved",L.className="token-status success"):P?.partial?(L.textContent="\u2713 "+P.partial,L.className="token-status success"):(L.textContent="\u2717 "+(P?.error||"Login failed"),L.className="token-status error")}):E.success?t.forEach(T=>{c[T]&&(document.getElementById(`${T}-token-status`).textContent="\u2713 Saved",document.getElementById(`${T}-token-status`).className="token-status success")}):t.forEach(T=>{c[T]&&(document.getElementById(`${T}-token-status`).textContent="\u2717 "+(E.error||"Failed"),document.getElementById(`${T}-token-status`).className="token-status error")})}catch(i){console.error("Failed to sync DNS credentials to backend:",i),t.forEach(E=>{c[E]&&(document.getElementById(`${E}-token-status`).textContent="\u2713 Saved locally (sync failed)",document.getElementById(`${E}-token-status`).className="token-status")})}}else t.forEach(i=>{document.getElementById(`${i}-token-status`).textContent=""});setTimeout(()=>{t.every(E=>{const T=document.getElementById(`${E}-token-status`)?.textContent;return!T||T.includes("\u2713")})&&closeModal("token-management-modal")},1500)}),document.getElementById("token-cancel")?.addEventListener("click",()=>{closeModal("token-management-modal")}),document.getElementById("token-clear-all")?.addEventListener("click",async()=>{if(confirm("Clear all stored DNS credentials? This cannot be undone.")){s(),o().forEach(t=>{document.getElementById(`${t}-readonly-username`).value="",document.getElementById(`${t}-readonly-token`).value="",document.getElementById(`${t}-admin-username`).value="",document.getElementById(`${t}-admin-token`).value="",document.getElementById(`${t}-token-status`).textContent="\u2713 Cleared",document.getElementById(`${t}-token-status`).className="token-status success"});try{await secureFetch("/api/v1/dns/credentials",{method:"DELETE"})}catch{}}}),window.getToken=m,window.getUsername=n,window.setToken=d,window.setUsername=g,window.getAllCredentials=e,window.getCredential=a,window.setCredential=b,window.getEncryptionKey=y,window.getDnsIds=o,window.getDnsDisplayName=f})(),(function(){function o(d,g,e=null){const s=document.getElementById(d+"-dot"),p=document.getElementById(d+"-pill"),t=document.getElementById(d+"-time"),c=document.querySelector(`[data-app="${d}"]`);s&&(s.classList.toggle("ok",g),s.classList.toggle("bad",!g)),p&&(p.textContent=g?"ON":"OFF",p.classList.toggle("on",g),p.classList.toggle("off",!g)),t&&e!==null&&(t.textContent=g?`${e}ms`:"timeout",t.className=`response-time ${f(e,g)}`),c&&c.setAttribute("data-status",g?"on":"off")}function f(d,g){return g?d<200?"excellent":d<500?"good":d<1e3?"fair":"slow":"timeout"}async function u(d){const g=performance.now();try{const e=await fetch("/probe/"+d,{cache:"no-store"}),s=performance.now(),p=Math.round(s-g);return{isUp:e.status>=200&&e.status<400||e.status===401||e.status===403,responseTime:p}}catch{const e=performance.now();return{isUp:!1,responseTime:Math.round(e-g)}}}window.APPS=[];let y=null,l=!1;async function r(){try{window.SkeletonLoader&&window.SkeletonLoader.show(6);const d=await fetch("/api/v1/services",{cache:"no-store"});d.ok?(window.APPS=await d.json(),window.SkeletonLoader&&window.SkeletonLoader.hide()):(console.error("Failed to load services:",d.status),window.SkeletonLoader&&window.SkeletonLoader.hide())}catch(d){console.error("Failed to load services:",d),window.SkeletonLoader&&window.SkeletonLoader.hide()}}function h(d){const g=window.APPS?.find(s=>s.id===d);if(g?.url)return g.url.startsWith("http")?g.url:"https://"+g.url;if(g?.isExternal&&g.externalUrl)return g.externalUrl;const e=SITE.dnsServers?.[d];return e?"http://"+e.ip+":"+(e.port||5380):buildServiceUrl(d)}function a(d,g,e){const s=document.createElement(d);return g&&(s.className=g),e&&(s.textContent=e),s}function b(){const d=document.getElementById("cards");d.innerHTML="";for(let g=0;g{O.stopPropagation(),window.openContainerLogsModal(e.containerId,e.name)},w.appendChild(k);const I=a("button","update-btn","\u2B06\uFE0F");I.title="Update container to latest version",I.id=`update-btn-${e.id}`,I.onclick=O=>{O.stopPropagation(),window.updateContainer(e.containerId,e.name,e.id)},w.appendChild(I);const A=a("button","exec-btn",">_");A.title="Open terminal",A.onclick=O=>{O.stopPropagation(),window.openExecModal&&window.openExecModal(e.containerId,e.name)},w.appendChild(A)}if(e.logPath&&!e.containerId){const k=a("button","logs-btn","\u{1F4CB}");k.title="View application logs",k.onclick=I=>{I.stopPropagation(),window.openFileLogsModal(e.logPath,e.name)},w.appendChild(k)}if(e.isExternal||e.appTemplate||e.url){const k=a("button","creds-btn","\u{1F511}");k.title="Auto-login credentials",k.id=`creds-btn-${e.id}`,k.onclick=I=>{I.stopPropagation(),window.openServiceCredsModal(e)},w.appendChild(k)}if(e.id!=="internet"){const k=a("button","options-btn","\u2699\uFE0F");k.title="Edit service settings",k.onclick=I=>{I.stopPropagation(),window.openServiceEditModal(e)},w.appendChild(k)}if(e.id!=="internet"){const k=a("button","delete-btn","\u{1F5D1}\uFE0F");k.title="Delete this service",k.onclick=I=>{I.stopPropagation(),window.deleteService(e.id,e.name)},w.appendChild(k)}const C=a("button",null,"Open");C.onclick=()=>window.open(h(e.id),"_blank","noopener"),w.appendChild(C),s.appendChild(w),s.style.transitionDelay=`${Math.min(g*45,270)}ms`,d.appendChild(s)}requestAnimationFrame(()=>{d.querySelectorAll(".card").forEach(g=>g.classList.add("loaded"))}),window.groupRecipeCards&&requestAnimationFrame(()=>window.groupRecipeCards()),window.refreshServiceFilter&&window.refreshServiceFilter()}function m(d,g,e=null){const s=document.getElementById("dot-"+d+"-grid"),p=document.getElementById("badge-"+d),t=document.getElementById("time-"+d),c=document.querySelector(`[data-app="${d}"]`);s&&(s.classList.toggle("ok",g),s.classList.toggle("bad",!g)),p&&(p.textContent=g?"ON":"OFF",p.classList.toggle("on",g),p.classList.toggle("off",!g)),t&&e!==null&&(t.textContent=g?`${e}ms`:"timeout",t.className=`response-time ${f(e,g)}`),c&&c.setAttribute("data-status",g?"on":"off")}async function n(){if(y)return l=!0,y;function d(s,p=new Date){const t=document.getElementById("stamp");t&&(t.textContent=`${s}: ${new Date(p).toLocaleTimeString()}`)}function g(s){Object.keys(SITE.dnsServers).forEach(t=>{const c=s[t];c&&o(t,c.isUp,c.responseTime)}),s.internet&&o("internet",s.internet.isUp,s.internet.responseTime),window.APPS.forEach(t=>{const c=s[t.id];c&&m(t.id,c.isUp,c.responseTime)})}async function e(){const s=Object.keys(SITE.dnsServers),p=s.map(i=>u(i));p.push(u("internet"));const t=await Promise.all(p);s.forEach((i,E)=>o(i,t[E].isUp,t[E].responseTime));const c=t[t.length-1];o("internet",c.isUp,c.responseTime),(await Promise.all(window.APPS.map(async i=>{const E=await u(i.id);return{id:i.id,...E}}))).forEach(i=>{m(i.id,i.isUp,i.responseTime)})}return y=(async()=>{try{const s=await fetch("/api/v1/services/status",{cache:"no-store"});if(!s.ok)throw new Error(`Status refresh failed (${s.status})`);const p=await s.json();g(p.statuses||{}),d("last check",p.checkedAt||new Date)}catch(s){console.warn("Batched status refresh failed, falling back to direct probes:",s);try{await e(),d("last check")}catch(p){console.error("Dashboard refresh failed:",p),d("last failed")}}finally{y=null,l&&(l=!1,setTimeout(()=>{window.refreshAll()},0))}})(),y}document.querySelector(".top")?.addEventListener("click",d=>{const g=d.target.closest('[id$="-open"]');if(!g)return;const e=g.id.replace("-open","");SITE.dnsServers[e]&&window.open(h(e),"_blank","noopener")}),document.getElementById("ca-open")?.addEventListener("click",()=>window.open(h("ca"),"_blank","noopener")),document.getElementById("creds-btn-ca")?.addEventListener("click",d=>{d.stopPropagation();const g=window.APPS.find(e=>e.id==="ca");g&&window.openServiceCredsModal&&window.openServiceCredsModal(g)}),document.getElementById("options-btn-ca")?.addEventListener("click",d=>{d.stopPropagation();const g=window.APPS.find(e=>e.id==="ca");g&&window.openServiceEditModal&&window.openServiceEditModal(g)}),document.getElementById("delete-btn-ca")?.addEventListener("click",d=>{d.stopPropagation(),window.deleteService&&window.deleteService("ca","DashCA")}),window.loadServices=r,window.buildGrid=b,window.refreshAll=n,window.setQuick=o,window.setBadge=m,window.getResponseTimeClass=f,window.checkServiceWithTiming=u,window.serviceUrl=h,window.el=a})(),(function(){async function o(a){const m=await(await secureFetch(`/api/v1/dns/restart/${a}`,{method:"POST"})).json();if(!m.success)throw new Error(m.error||"Restart failed");return m}document.querySelector(".top")?.addEventListener("click",async a=>{const b=a.target.closest('[id$="-restart"]');if(!b)return;const m=b.id.replace("-restart","");if(SITE.dnsServers[m]&&confirm(`Restart ${m.toUpperCase()} service?`))try{await withButton(b,"...",()=>o(m)),setTimeout(window.refreshAll,DC.DELAYS.RELOAD)}catch(n){showNotification("Restart failed: "+n.message,"error")}});async function f(a,b){const m=document.getElementById(`${a}-update`),n=m?.textContent||"\u2B06\uFE0F";try{m.textContent="\u{1F50D}",m.disabled=!0,m.title="Checking for updates...";const g=await(await fetch(`/api/v1/dns/check-update?server=${encodeURIComponent(b)}`)).json();if(!g.success)throw new Error(g.error||"Failed to check for updates");if(!g.updateAvailable){m.textContent="\u2705",m.title=`Already on latest version (${g.currentVersion})`,showNotification(`${a.toUpperCase()} is already up to date! Current version: ${g.currentVersion}`,"info"),setTimeout(()=>{m.textContent=n,m.disabled=!1,m.title="Update DNS server"},3e3);return}if(!confirm(`Update available for ${a.toUpperCase()}! -Current: ${y.currentVersion} -New: ${y.updateVersion} +Current: ${g.currentVersion} +New: ${g.updateVersion} -`+(y.updateTitle?`${y.updateTitle} +`+(g.updateTitle?`${g.updateTitle} `:"")+`The DNS server will restart during the update. -Proceed?`)){m.textContent=r,m.disabled=!1,m.title="Update DNS server";return}m.textContent="\u{1F504}",m.title="Updating...";const p=await(await secureFetch(`/api/v1/dns/update?server=${encodeURIComponent(w)}`,{method:"POST"})).json();if(!p.success)throw new Error(p.error||"Update failed");if(p.manualUpdateRequired){m.textContent="\u2B06\uFE0F",m.title=`Update available: ${p.newVersion}`;const t=p.downloadLink?` +Proceed?`)){m.textContent=n,m.disabled=!1,m.title="Update DNS server";return}m.textContent="\u{1F504}",m.title="Updating...";const p=await(await secureFetch(`/api/v1/dns/update?server=${encodeURIComponent(b)}`,{method:"POST"})).json();if(!p.success)throw new Error(p.error||"Update failed");if(p.manualUpdateRequired){m.textContent="\u2B06\uFE0F",m.title=`Update available: ${p.newVersion}`;const t=p.downloadLink?` Download: ${p.downloadLink}`:"",c=p.instructionsLink?` -Instructions: ${p.instructionsLink}`:"";showNotification(`${n.toUpperCase()} update requires manual installation. Current: ${p.previousVersion} \u2192 ${p.newVersion}. Please update manually on the host machine.`,"warning",8e3),m.disabled=!1;return}m.textContent="\u2705",m.title="Updated successfully!",showNotification(`${n.toUpperCase()} updated successfully! ${p.previousVersion} \u2192 ${p.newVersion}. Server is restarting...`,"success"),setTimeout(()=>{m.textContent=r,m.disabled=!1,m.title="Update DNS server",window.refreshAll()},1e4)}catch(l){console.error("DNS update error:",l),m.textContent="\u274C",m.title="Update failed",showNotification(`Failed to update ${n.toUpperCase()}: ${l.message}`,"error"),setTimeout(()=>{m.textContent=r,m.disabled=!1,m.title="Update DNS server"},3e3)}}document.querySelector(".top")?.addEventListener("click",n=>{const w=n.target.closest('[id$="-update"]');if(!w)return;const m=w.id.replace("-update","");SITE.dnsServers[m]&&f(m,SITE.dnsServers[m]?.ip)}),injectModal("dns-settings-modal",` +Instructions: ${p.instructionsLink}`:"";showNotification(`${a.toUpperCase()} update requires manual installation. Current: ${p.previousVersion} \u2192 ${p.newVersion}. Please update manually on the host machine.`,"warning",8e3),m.disabled=!1;return}m.textContent="\u2705",m.title="Updated successfully!",showNotification(`${a.toUpperCase()} updated successfully! ${p.previousVersion} \u2192 ${p.newVersion}. Server is restarting...`,"success"),setTimeout(()=>{m.textContent=n,m.disabled=!1,m.title="Update DNS server",window.refreshAll()},1e4)}catch(d){console.error("DNS update error:",d),m.textContent="\u274C",m.title="Update failed",showNotification(`Failed to update ${a.toUpperCase()}: ${d.message}`,"error"),setTimeout(()=>{m.textContent=n,m.disabled=!1,m.title="Update DNS server"},3e3)}}document.querySelector(".top")?.addEventListener("click",a=>{const b=a.target.closest('[id$="-update"]');if(!b)return;const m=b.id.replace("-update","");SITE.dnsServers[m]&&f(m,SITE.dnsServers[m]?.ip)}),injectModal("dns-settings-modal",`

DNS Settings

@@ -318,7 +318,7 @@ Instructions: ${p.instructionsLink}`:"";showNotification(`${n.toUpperCase()} upd
- `);let u=null;function g(n){u=n;const w=SITE.dnsServers[n]||{},m=document.getElementById("dns-settings-modal");document.getElementById("dns-settings-title").textContent=`${(w.name||n).toUpperCase()} Settings`,document.getElementById("dns-edit-ip").value=w.ip||"",document.getElementById("dns-edit-port").value=w.port||DC.DEFAULTS.DNS_PORT,document.getElementById("dns-edit-name").value=w.name||"",m.classList.add("show")}async function i(){if(!u)return;const n=document.getElementById("dns-edit-ip").value.trim(),w=document.getElementById("dns-edit-port").value.trim()||DC.DEFAULTS.DNS_PORT,m=document.getElementById("dns-edit-name").value.trim();if(!n){showNotification("Server IP is required","warning");return}const r={dnsServers:{}};r.dnsServers[u]={ip:n,port:String(w)},m&&(r.dnsServers[u].name=m);try{const y=await(await secureFetch("/api/v1/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(r)})).json();y.success?(SITE.dnsServers[u]=r.dnsServers[u],showNotification(`${u.toUpperCase()} settings saved`,"success"),h(),window.refreshAll()):showNotification(y.error||"Failed to save settings","error")}catch(l){showNotification("Failed to save: "+l.message,"error")}}async function s(){if(u&&confirm(`Remove ${u.toUpperCase()} from dashboard? This won't affect the actual DNS server.`))try{const w=await(await secureFetch("/api/v1/config")).json();w.dnsServers&&delete w.dnsServers[u];const r=await(await secureFetch("/api/v1/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({dnsServers:w.dnsServers||{}})})).json();if(r.success){delete SITE.dnsServers[u];const l=document.querySelector(`.top [data-app="${u}"]`);l&&l.remove(),showNotification(`${u.toUpperCase()} removed from dashboard`,"success"),h()}else showNotification(r.error||"Failed to remove","error")}catch(n){showNotification("Failed to remove: "+n.message,"error")}}function h(){closeModal("dns-settings-modal"),u=null}document.getElementById("dns-settings-cancel")?.addEventListener("click",h),document.getElementById("dns-settings-save")?.addEventListener("click",i),document.getElementById("dns-settings-delete")?.addEventListener("click",s),document.getElementById("dns-settings-modal")?.addEventListener("click",n=>{n.target.id==="dns-settings-modal"&&h()}),document.querySelector(".top")?.addEventListener("click",n=>{const w=n.target.closest('[id$="-settings"]');if(!w)return;const m=w.id.replace("-settings","");SITE.dnsServers[m]&&(n.stopPropagation(),g(m))}),document.getElementById("refresh")?.addEventListener("click",window.refreshAll)})(),(function(){injectModal("logs-modal",` + `);let u=null;function y(a){u=a;const b=SITE.dnsServers[a]||{},m=document.getElementById("dns-settings-modal");document.getElementById("dns-settings-title").textContent=`${(b.name||a).toUpperCase()} Settings`,document.getElementById("dns-edit-ip").value=b.ip||"",document.getElementById("dns-edit-port").value=b.port||DC.DEFAULTS.DNS_PORT,document.getElementById("dns-edit-name").value=b.name||"",m.classList.add("show")}async function l(){if(!u)return;const a=document.getElementById("dns-edit-ip").value.trim(),b=document.getElementById("dns-edit-port").value.trim()||DC.DEFAULTS.DNS_PORT,m=document.getElementById("dns-edit-name").value.trim();if(!a){showNotification("Server IP is required","warning");return}const n={dnsServers:{}};n.dnsServers[u]={ip:a,port:String(b)},m&&(n.dnsServers[u].name=m);try{const g=await(await secureFetch("/api/v1/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)})).json();g.success?(SITE.dnsServers[u]=n.dnsServers[u],showNotification(`${u.toUpperCase()} settings saved`,"success"),h(),window.refreshAll()):showNotification(g.error||"Failed to save settings","error")}catch(d){showNotification("Failed to save: "+d.message,"error")}}async function r(){if(u&&confirm(`Remove ${u.toUpperCase()} from dashboard? This won't affect the actual DNS server.`))try{const b=await(await secureFetch("/api/v1/config")).json();b.dnsServers&&delete b.dnsServers[u];const n=await(await secureFetch("/api/v1/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({dnsServers:b.dnsServers||{}})})).json();if(n.success){delete SITE.dnsServers[u];const d=document.querySelector(`.top [data-app="${u}"]`);d&&d.remove(),showNotification(`${u.toUpperCase()} removed from dashboard`,"success"),h()}else showNotification(n.error||"Failed to remove","error")}catch(a){showNotification("Failed to remove: "+a.message,"error")}}function h(){closeModal("dns-settings-modal"),u=null}document.getElementById("dns-settings-cancel")?.addEventListener("click",h),document.getElementById("dns-settings-save")?.addEventListener("click",l),document.getElementById("dns-settings-delete")?.addEventListener("click",r),document.getElementById("dns-settings-modal")?.addEventListener("click",a=>{a.target.id==="dns-settings-modal"&&h()}),document.querySelector(".top")?.addEventListener("click",a=>{const b=a.target.closest('[id$="-settings"]');if(!b)return;const m=b.id.replace("-settings","");SITE.dnsServers[m]&&(a.stopPropagation(),y(m))}),document.getElementById("refresh")?.addEventListener("click",window.refreshAll)})(),(function(){injectModal("logs-modal",`
@@ -342,66 +342,66 @@ Instructions: ${p.instructionsLink}`:"";showNotification(`${n.toUpperCase()} upd
- `);let o=null,f=null,u=!1,g=null,i=null,s=!1,h=null,n=null,w=!1,m=null,r=!1;async function l(x,b=25){try{const I=getDnsServerAddr(x),S=await fetch(`/api/v1/dns/logs?server=${I}&limit=${b}`,{cache:"no-store",headers:{Accept:"application/json","Cache-Control":"no-cache"}});if(S.ok){const C=await S.json();return C.success&&C.logs?{logs:C.logs,count:C.count,server:C.server}:{error:C.error||"Failed to fetch logs"}}else return S.status===401?{error:"DNS auto-auth failed - check credentials in settings"}:{error:`HTTP ${S.status}`}}catch(I){return console.error("DNS logs fetch failed:",I),{error:I.message}}}function y(x){return{NoError:"var(--ok-fg)",NOERROR:"var(--ok-fg)",NxDomain:"var(--muted)",NXDOMAIN:"var(--muted)",Refused:"var(--bad-fg)",REFUSED:"var(--bad-fg)",ServerFailure:"#f39c12",SERVFAIL:"#f39c12"}[x]||"var(--fg)"}function e(x){const b=document.createElement("div");if(b.className="log-entry",b.style.cssText="display: grid; grid-template-columns: 140px 110px 1fr 70px 80px; gap: 8px; padding: 6px 10px; border-bottom: 1px solid var(--border); font-size: 0.8rem; align-items: center;",x.parsed===!1)return b.style.gridTemplateColumns="1fr",b.innerHTML=`${escapeHtml(x.raw)}`,b;const I=y(x.rcode),S=x.rcode==="Refused"||x.rcode==="REFUSED";return b.innerHTML=` + `);let o=null,f=null,u=!1,y=null,l=null,r=!1,h=null,a=null,b=!1,m=null,n=!1;async function d(x,w=25){try{const C=getDnsServerAddr(x),k=await fetch(`/api/v1/dns/logs?server=${C}&limit=${w}`,{cache:"no-store",headers:{Accept:"application/json","Cache-Control":"no-cache"}});if(k.ok){const I=await k.json();return I.success&&I.logs?{logs:I.logs,count:I.count,server:I.server}:{error:I.error||"Failed to fetch logs"}}else return k.status===401?{error:"DNS auto-auth failed - check credentials in settings"}:{error:`HTTP ${k.status}`}}catch(C){return console.error("DNS logs fetch failed:",C),{error:C.message}}}function g(x){return{NoError:"var(--ok-fg)",NOERROR:"var(--ok-fg)",NxDomain:"var(--muted)",NXDOMAIN:"var(--muted)",Refused:"var(--bad-fg)",REFUSED:"var(--bad-fg)",ServerFailure:"#f39c12",SERVFAIL:"#f39c12"}[x]||"var(--fg)"}function e(x){const w=document.createElement("div");if(w.className="log-entry",w.style.cssText="display: grid; grid-template-columns: 140px 110px 1fr 70px 80px; gap: 8px; padding: 6px 10px; border-bottom: 1px solid var(--border); font-size: 0.8rem; align-items: center;",x.parsed===!1)return w.style.gridTemplateColumns="1fr",w.innerHTML=`${escapeHtml(x.raw)}`,w;const C=g(x.rcode),k=x.rcode==="Refused"||x.rcode==="REFUSED";return w.innerHTML=` ${escapeHtml(x.timestamp)} ${escapeHtml(x.client)} - ${escapeHtml(x.domain)} + ${escapeHtml(x.domain)} ${escapeHtml(x.type)} - ${escapeHtml(x.rcode)} - `,b}async function a(){if(w){await L();return}if(s){await B();return}if(u||!o)return;const x=parseInt(document.getElementById("log-lines").value),b=document.getElementById("logs-content");try{const I=await l(o,x);if(I.error){b.innerHTML=` + ${escapeHtml(x.rcode)} + `,w}async function s(){if(b){await B();return}if(r){await T();return}if(u||!o)return;const x=parseInt(document.getElementById("log-lines").value),w=document.getElementById("logs-content");try{const C=await d(o,x);if(C.error){w.innerHTML=`
\u26A0\uFE0F Error
-
${escapeHtml(I.error)}
-
`;return}b.innerHTML=` +
${escapeHtml(C.error)}
+ `;return}w.innerHTML=`
Time Client Domain Type Status -
`,I.logs&&I.logs.length>0?I.logs.forEach(S=>{const C=e(S);b.appendChild(C)}):b.innerHTML+=` + `,C.logs&&C.logs.length>0?C.logs.forEach(k=>{const I=e(k);w.appendChild(I)}):w.innerHTML+=`
No DNS queries logged yet -
`}catch(I){b.innerHTML=` + `}catch(C){w.innerHTML=`
- Failed to fetch logs: ${escapeHtml(I.message)} -
`}}function p(x){o=x,u=!1,s=!1;const b=document.getElementById("logs-modal"),I=document.getElementById("logs-title"),S=document.getElementById("logs-pause"),C=document.getElementById("logs-stream");I.textContent=`${x.toUpperCase()} DNS Logs`,S.textContent="\u23F8\uFE0F Pause",S.classList.remove("paused"),C&&(C.style.display="none"),b.classList.add("show"),a(),f=setInterval(a,DC.POLL.LOGS)}function t(){document.getElementById("logs-modal").classList.remove("show"),f&&(clearInterval(f),f=null),v(),o=null,s=!1,g=null,i=null,w=!1,h=null,n=null,u=!1}function c(x){m&&v();const b=document.getElementById("logs-stream"),I=document.getElementById("logs-pause"),S=document.getElementById("logs-content");f&&(clearInterval(f),f=null);try{m=new EventSource(`/api/v1/logs/stream/${x}`),r=!0,b.classList.add("active"),b.textContent="\u{1F534} Live",b.title="Streaming - click to stop",I.style.display="none";const C=document.getElementById("logs-title");C.textContent.includes("\u{1F534}")||(C.innerHTML=C.textContent.replace("\u{1F4CB}","\u{1F4CB} \u{1F534}")),m.onmessage=P=>{try{const D=JSON.parse(P.data);if(D.error){console.error("Stream error:",D.error),v();return}const O=document.createElement("div");O.className="log-entry",O.style.cssText="display: flex; gap: 12px; padding: 6px 10px; border-bottom: 1px solid var(--border); font-size: 0.8rem; align-items: flex-start; font-family: monospace;";const R=(D.stream||"stdout")==="stderr",U=R?"var(--bad-fg)":"var(--fg)",M=`${R?"STDERR":"STDOUT"}`;for(O.innerHTML=` -
${M}
-
${escapeHtml(D.text)}
- `,S.appendChild(O),S.scrollTop=S.scrollHeight;S.children.length>500;)S.removeChild(S.firstChild)}catch(D){console.error("Error parsing stream data:",D)}},m.onerror=P=>{console.error("EventSource error:",P),v()}}catch(C){console.error("Failed to start streaming:",C),v()}}function v(){m&&(m.close(),m=null),r=!1;const x=document.getElementById("logs-stream"),b=document.getElementById("logs-pause"),I=document.getElementById("logs-title");x&&(x.classList.remove("active"),x.textContent="\u{1F4E1} Live",x.title="Enable real-time streaming"),b&&(b.style.display=""),I&&(I.textContent=I.textContent.replace(" \u{1F534}","")),s&&g&&!f&&(f=setInterval(B,DC.POLL.LOGS))}async function d(x,b=100){try{const I=`/api/v1/logs/container/${x}?tail=${b}×tamps=true`,S=await fetch(I,{cache:"no-store",headers:{Accept:"application/json","Cache-Control":"no-cache"}});if(S.ok){const C=await S.json();return C.success&&C.logs?{logs:C.logs,count:C.count,containerName:C.containerName,containerId:C.containerId}:{error:C.error||"Failed to fetch container logs"}}else return{error:`HTTP ${S.status}: ${S.statusText}`}}catch(I){return console.error("Container logs fetch failed:",I),{error:I.message}}}function k(x){const b=document.createElement("div");b.className="log-entry",b.style.cssText="display: flex; gap: 12px; padding: 6px 10px; border-bottom: 1px solid var(--border); font-size: 0.8rem; align-items: flex-start; font-family: monospace;";const I=x.stream==="stderr"?"var(--bad-fg)":"var(--fg)",S=x.stream==="stderr"?'STDERR':'STDOUT';return b.innerHTML=` -
${S}
-
${escapeHtml(x.text)}
- `,b}async function B(){if(u||!g||!s)return;const x=parseInt(document.getElementById("log-lines").value),b=document.getElementById("logs-content");try{const I=await d(g,x);if(I.error){b.innerHTML=` + Failed to fetch logs: ${escapeHtml(C.message)} + `}}function p(x){o=x,u=!1,r=!1;const w=document.getElementById("logs-modal"),C=document.getElementById("logs-title"),k=document.getElementById("logs-pause"),I=document.getElementById("logs-stream");C.textContent=`${x.toUpperCase()} DNS Logs`,k.textContent="\u23F8\uFE0F Pause",k.classList.remove("paused"),I&&(I.style.display="none"),w.classList.add("show"),s(),f=setInterval(s,DC.POLL.LOGS)}function t(){document.getElementById("logs-modal").classList.remove("show"),f&&(clearInterval(f),f=null),v(),o=null,r=!1,y=null,l=null,b=!1,h=null,a=null,u=!1}function c(x){m&&v();const w=document.getElementById("logs-stream"),C=document.getElementById("logs-pause"),k=document.getElementById("logs-content");f&&(clearInterval(f),f=null);try{m=new EventSource(`/api/v1/logs/stream/${x}`),n=!0,w.classList.add("active"),w.textContent="\u{1F534} Live",w.title="Streaming - click to stop",C.style.display="none";const I=document.getElementById("logs-title");I.textContent.includes("\u{1F534}")||(I.innerHTML=I.textContent.replace("\u{1F4CB}","\u{1F4CB} \u{1F534}")),m.onmessage=A=>{try{const O=JSON.parse(A.data);if(O.error){console.error("Stream error:",O.error),v();return}const D=document.createElement("div");D.className="log-entry",D.style.cssText="display: flex; gap: 12px; padding: 6px 10px; border-bottom: 1px solid var(--border); font-size: 0.8rem; align-items: flex-start; font-family: monospace;";const R=(O.stream||"stdout")==="stderr",N=R?"var(--bad-fg)":"var(--fg)",F=`${R?"STDERR":"STDOUT"}`;for(D.innerHTML=` +
${F}
+
${escapeHtml(O.text)}
+ `,k.appendChild(D),k.scrollTop=k.scrollHeight;k.children.length>500;)k.removeChild(k.firstChild)}catch(O){console.error("Error parsing stream data:",O)}},m.onerror=A=>{console.error("EventSource error:",A),v()}}catch(I){console.error("Failed to start streaming:",I),v()}}function v(){m&&(m.close(),m=null),n=!1;const x=document.getElementById("logs-stream"),w=document.getElementById("logs-pause"),C=document.getElementById("logs-title");x&&(x.classList.remove("active"),x.textContent="\u{1F4E1} Live",x.title="Enable real-time streaming"),w&&(w.style.display=""),C&&(C.textContent=C.textContent.replace(" \u{1F534}","")),r&&y&&!f&&(f=setInterval(T,DC.POLL.LOGS))}async function i(x,w=100){try{const C=`/api/v1/logs/container/${x}?tail=${w}×tamps=true`,k=await fetch(C,{cache:"no-store",headers:{Accept:"application/json","Cache-Control":"no-cache"}});if(k.ok){const I=await k.json();return I.success&&I.logs?{logs:I.logs,count:I.count,containerName:I.containerName,containerId:I.containerId}:{error:I.error||"Failed to fetch container logs"}}else return{error:`HTTP ${k.status}: ${k.statusText}`}}catch(C){return console.error("Container logs fetch failed:",C),{error:C.message}}}function E(x){const w=document.createElement("div");w.className="log-entry",w.style.cssText="display: flex; gap: 12px; padding: 6px 10px; border-bottom: 1px solid var(--border); font-size: 0.8rem; align-items: flex-start; font-family: monospace;";const C=x.stream==="stderr"?"var(--bad-fg)":"var(--fg)",k=x.stream==="stderr"?'STDERR':'STDOUT';return w.innerHTML=` +
${k}
+
${escapeHtml(x.text)}
+ `,w}async function T(){if(u||!y||!r)return;const x=parseInt(document.getElementById("log-lines").value),w=document.getElementById("logs-content");try{const C=await i(y,x);if(C.error){w.innerHTML=`
\u26A0\uFE0F Error
-
${escapeHtml(I.error)}
-
`;return}b.innerHTML=` +
${escapeHtml(C.error)}
+ `;return}w.innerHTML=`
Stream Log Output -
`,I.logs&&I.logs.length>0?(I.logs.forEach(S=>{const C=k(S);b.appendChild(C)}),b.scrollTop=b.scrollHeight):b.innerHTML+=` + `,C.logs&&C.logs.length>0?(C.logs.forEach(k=>{const I=E(k);w.appendChild(I)}),w.scrollTop=w.scrollHeight):w.innerHTML+=`
No logs available for this container -
`}catch(I){b.innerHTML=` + `}catch(C){w.innerHTML=`
- Failed to fetch logs: ${escapeHtml(I.message)} -
`}}function T(x,b){g=x,i=b,s=!0,w=!1,u=!1,v();const I=document.getElementById("logs-modal"),S=document.getElementById("logs-title"),C=document.getElementById("logs-pause"),P=document.getElementById("logs-stream");S.textContent=`\u{1F4CB} ${b} - Container Logs`,C.textContent="\u23F8\uFE0F Pause",C.classList.remove("paused"),P&&(P.style.display=""),I.classList.add("show"),B(),f=setInterval(B,DC.POLL.LOGS)}async function A(x,b=100){try{const I=`/api/v1/logs/file?path=${encodeURIComponent(x)}&tail=${b}`,S=await fetch(I,{cache:"no-store",headers:{Accept:"application/json","Cache-Control":"no-cache"}});if(S.ok){const C=await S.json();return C.success&&C.logs?{logs:C.logs,count:C.count,logPath:C.logPath,totalLines:C.totalLines}:{error:C.error||"Failed to fetch file logs"}}else return{error:(await S.json().catch(()=>({}))).error||`HTTP ${S.status}`}}catch(I){return console.error("File logs fetch failed:",I),{error:I.message}}}function E(x){const b=document.createElement("div");b.className="log-entry",b.style.cssText="display: flex; gap: 12px; padding: 6px 10px; border-bottom: 1px solid var(--border); font-size: 0.8rem; align-items: flex-start; font-family: monospace;";const I=x.text;let S="INFO",C="var(--fg)";I.match(/ERROR|FATAL|CRITICAL/i)?(S="ERROR",C="var(--bad-fg)"):I.match(/WARN|WARNING/i)?(S="WARN",C="#f39c12"):I.match(/DEBUG/i)&&(S="DEBUG",C="var(--muted)");const D=`${S}`;return b.innerHTML=` -
${D}
-
${escapeHtml(I)}
- `,b}async function L(){if(u||!h||!w)return;const x=parseInt(document.getElementById("log-lines").value),b=document.getElementById("logs-content");try{const I=await A(h,x);if(I.error){b.innerHTML=` + Failed to fetch logs: ${escapeHtml(C.message)} + `}}function L(x,w){y=x,l=w,r=!0,b=!1,u=!1,v();const C=document.getElementById("logs-modal"),k=document.getElementById("logs-title"),I=document.getElementById("logs-pause"),A=document.getElementById("logs-stream");k.textContent=`\u{1F4CB} ${w} - Container Logs`,I.textContent="\u23F8\uFE0F Pause",I.classList.remove("paused"),A&&(A.style.display=""),C.classList.add("show"),T(),f=setInterval(T,DC.POLL.LOGS)}async function P(x,w=100){try{const C=`/api/v1/logs/file?path=${encodeURIComponent(x)}&tail=${w}`,k=await fetch(C,{cache:"no-store",headers:{Accept:"application/json","Cache-Control":"no-cache"}});if(k.ok){const I=await k.json();return I.success&&I.logs?{logs:I.logs,count:I.count,logPath:I.logPath,totalLines:I.totalLines}:{error:I.error||"Failed to fetch file logs"}}else return{error:(await k.json().catch(()=>({}))).error||`HTTP ${k.status}`}}catch(C){return console.error("File logs fetch failed:",C),{error:C.message}}}function S(x){const w=document.createElement("div");w.className="log-entry",w.style.cssText="display: flex; gap: 12px; padding: 6px 10px; border-bottom: 1px solid var(--border); font-size: 0.8rem; align-items: flex-start; font-family: monospace;";const C=x.text;let k="INFO",I="var(--fg)";C.match(/ERROR|FATAL|CRITICAL/i)?(k="ERROR",I="var(--bad-fg)"):C.match(/WARN|WARNING/i)?(k="WARN",I="#f39c12"):C.match(/DEBUG/i)&&(k="DEBUG",I="var(--muted)");const O=`${k}`;return w.innerHTML=` +
${O}
+
${escapeHtml(C)}
+ `,w}async function B(){if(u||!h||!b)return;const x=parseInt(document.getElementById("log-lines").value),w=document.getElementById("logs-content");try{const C=await P(h,x);if(C.error){w.innerHTML=`
\u26A0\uFE0F Error
-
${escapeHtml(I.error)}
-
`;return}b.innerHTML=` +
${escapeHtml(C.error)}
+ `;return}w.innerHTML=`
- Log Output (${I.count} of ${I.totalLines} lines) -
`,I.logs&&I.logs.length>0?(I.logs.forEach(S=>{const C=E(S);b.appendChild(C)}),b.scrollTop=b.scrollHeight):b.innerHTML+=` + Log Output (${C.count} of ${C.totalLines} lines) + `,C.logs&&C.logs.length>0?(C.logs.forEach(k=>{const I=S(k);w.appendChild(I)}),w.scrollTop=w.scrollHeight):w.innerHTML+=`
No logs available in this file -
`}catch(I){b.innerHTML=` + `}catch(C){w.innerHTML=`
- Failed to fetch logs: ${escapeHtml(I.message)} -
`}}function $(x,b){h=x,n=b,w=!0,s=!1,u=!1;const I=document.getElementById("logs-modal"),S=document.getElementById("logs-title"),C=document.getElementById("logs-pause"),P=document.getElementById("logs-stream");S.textContent=`\u{1F4CB} ${b} - Application Logs`,C.textContent="\u23F8\uFE0F Pause",C.classList.remove("paused"),P&&(P.style.display="none"),I.classList.add("show"),L(),f=setInterval(L,DC.POLL.LOGS)}document.querySelector(".top")?.addEventListener("click",x=>{const b=x.target.closest('[id$="-logs"]');if(!b)return;const I=b.id.replace("-logs","");SITE.dnsServers[I]&&p(I)}),document.getElementById("logs-close")?.addEventListener("click",t),document.getElementById("logs-pause")?.addEventListener("click",()=>{u=!u;const x=document.getElementById("logs-pause");u?(x.textContent="\u25B6\uFE0F Resume",x.classList.add("paused")):(x.textContent="\u23F8\uFE0F Pause",x.classList.remove("paused"),a())}),document.getElementById("log-lines")?.addEventListener("change",()=>{u||a()}),document.getElementById("logs-stream")?.addEventListener("click",()=>{!s||!g||(r?v():c(g))}),document.getElementById("logs-modal")?.addEventListener("click",x=>{x.target.id==="logs-modal"&&t()}),document.addEventListener("keydown",x=>{x.key==="Escape"&&document.getElementById("logs-modal")?.classList.contains("show")&&t()}),window.openContainerLogsModal=T,window.openFileLogsModal=$,window.openLogsModal=p})(),(function(){injectModal("service-edit-modal",` + Failed to fetch logs: ${escapeHtml(C.message)} + `}}function $(x,w){h=x,a=w,b=!0,r=!1,u=!1;const C=document.getElementById("logs-modal"),k=document.getElementById("logs-title"),I=document.getElementById("logs-pause"),A=document.getElementById("logs-stream");k.textContent=`\u{1F4CB} ${w} - Application Logs`,I.textContent="\u23F8\uFE0F Pause",I.classList.remove("paused"),A&&(A.style.display="none"),C.classList.add("show"),B(),f=setInterval(B,DC.POLL.LOGS)}document.querySelector(".top")?.addEventListener("click",x=>{const w=x.target.closest('[id$="-logs"]');if(!w)return;const C=w.id.replace("-logs","");SITE.dnsServers[C]&&p(C)}),document.getElementById("logs-close")?.addEventListener("click",t),document.getElementById("logs-pause")?.addEventListener("click",()=>{u=!u;const x=document.getElementById("logs-pause");u?(x.textContent="\u25B6\uFE0F Resume",x.classList.add("paused")):(x.textContent="\u23F8\uFE0F Pause",x.classList.remove("paused"),s())}),document.getElementById("log-lines")?.addEventListener("change",()=>{u||s()}),document.getElementById("logs-stream")?.addEventListener("click",()=>{!r||!y||(n?v():c(y))}),document.getElementById("logs-modal")?.addEventListener("click",x=>{x.target.id==="logs-modal"&&t()}),document.addEventListener("keydown",x=>{x.key==="Escape"&&document.getElementById("logs-modal")?.classList.contains("show")&&t()}),window.openContainerLogsModal=L,window.openFileLogsModal=$,window.openLogsModal=p})(),(function(){injectModal("service-edit-modal",`

Edit Service

@@ -480,6 +480,16 @@ Instructions: ${p.instructionsLink}`:"";showNotification(`${n.toUpperCase()} upd Enter a URL or upload an image file (PNG, JPG, SVG)
+ + +
+ + +
@@ -633,6 +643,15 @@ Instructions: ${p.instructionsLink}`:"";showNotification(`${n.toUpperCase()} upd Reload Caddy after adding + +
+ + +
Group services on the dashboard by purpose (Media, Productivity, etc.)
+
+
@@ -720,6 +739,14 @@ Instructions: ${p.instructionsLink}`:"";showNotification(`${n.toUpperCase()} upd Follow Redirects + +
+ + +
+
@@ -730,44 +757,44 @@ Instructions: ${p.instructionsLink}`:"";showNotification(`${n.toUpperCase()} upd - `)})(),(function(){async function o(s){try{const h=await fetch(`/api/v1/caddy/cas?caddyfilePath=${encodeURIComponent(s)}`);if(!h.ok)throw new Error(`Failed to load CAs: ${h.status}`);const n=await h.json();if(n.status==="success"){const w=document.getElementById("existing-ca-select");return w.innerHTML="",n.data.cas.length===0?w.innerHTML='':(w.innerHTML='',n.data.cas.forEach(m=>{const r=document.createElement("option");typeof m=="object"?(r.value=m.id,r.textContent=m.displayName||m.name):(r.value=m,r.textContent=m),w.appendChild(r)})),n.data.cas}else throw new Error(n.message)}catch(h){console.error("Error loading CAs:",h);const n=document.getElementById("existing-ca-select");return n.innerHTML='',[]}}function f(s){const{subdomain:h,port:n,ip:w,sslType:m,caName:r,existingCa:l,enableAuth:y,enableCors:e,customHeaders:a,upstreamPath:p,healthCheck:t,timeout:c,tailscaleOnly:v}=s;let d=`${buildDomain(h)} { -`;switch(v&&(d+=` @blocked not remote_ip 100.64.0.0/10 -`,d+=` respond @blocked "Access denied. Tailscale connection required." 403 -`),m){case"letsencrypt":break;case"caddy-managed":d+=` tls internal -`;break;case"existing-ca":l&&(d+=` tls { - ca ${l} + `)})(),(function(){async function o(r){try{const h=await fetch(`/api/v1/caddy/cas?caddyfilePath=${encodeURIComponent(r)}`);if(!h.ok)throw new Error(`Failed to load CAs: ${h.status}`);const a=await h.json();if(a.status==="success"){const b=document.getElementById("existing-ca-select");return b.innerHTML="",a.data.cas.length===0?b.innerHTML='':(b.innerHTML='',a.data.cas.forEach(m=>{const n=document.createElement("option");typeof m=="object"?(n.value=m.id,n.textContent=m.displayName||m.name):(n.value=m,n.textContent=m),b.appendChild(n)})),a.data.cas}else throw new Error(a.message)}catch(h){console.error("Error loading CAs:",h);const a=document.getElementById("existing-ca-select");return a.innerHTML='',[]}}function f(r){const{subdomain:h,port:a,ip:b,sslType:m,caName:n,existingCa:d,enableAuth:g,enableCors:e,customHeaders:s,upstreamPath:p,healthCheck:t,timeout:c,tailscaleOnly:v}=r;let i=`${buildDomain(h)} { +`;switch(v&&(i+=` @blocked not remote_ip 100.64.0.0/10 +`,i+=` respond @blocked "Access denied. Tailscale connection required." 403 +`),m){case"letsencrypt":break;case"caddy-managed":i+=` tls internal +`;break;case"existing-ca":d&&(i+=` tls { + ca ${d} } -`);break;case"custom-ca":r&&(d+=` tls { - ca ${r} +`);break;case"custom-ca":n&&(i+=` tls { + ca ${n} } -`);break}if(y&&(d+=` basicauth { +`);break}if(g&&(i+=` basicauth { admin $2a$14$hashed_password_here } -`),e&&(d+=` header { -`,d+=` Access-Control-Allow-Origin "*" -`,d+=` Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" -`,d+=` Access-Control-Allow-Headers "Content-Type, Authorization" -`,d+=` } -`),a)try{const k=JSON.parse(a);d+=` header { -`,Object.entries(k).forEach(([B,T])=>{d+=` ${B} "${T}" -`}),d+=` } -`}catch{console.warn("Invalid JSON in custom headers")}return t&&(d+=` health_uri ${t} -`),d+=` reverse_proxy ${w}:${n} { -`,p&&p!=="/"&&(d+=` rewrite ${p} -`),c&&c!==30&&(d+=` transport http { -`,d+=` dial_timeout ${c}s -`,d+=` response_header_timeout ${c}s -`,d+=` } -`),d+=` } -`,d+=`} -`,d}async function u(s,h,n=DC.DEFAULTS.TTL){const w=window.getToken(getPrimaryDnsId(),"admin");if(!w)throw new Error("DNS admin token not configured. Please set it in the Tokens menu.");const m=buildDomain(s),r=await secureFetch("/api/v1/dns/record",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({domain:m,ip:h,ttl:n,token:w,server:SITE.dnsIp})});if(!r.ok){const y=await r.text();throw new Error(`DNS API Error: ${r.status} - ${y}`)}const l=await r.json();if(!l.success)throw new Error(`DNS Error: ${l.error||"Unknown error"}`);return l}async function g(s){const h={id:s.subdomain,name:s.name,logo:s.logo||`/assets/${s.subdomain}.png`};try{const n=await secureFetch("/api/v1/services",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(h)});if(!n.ok){const w=await n.json();throw new Error(w.error||"Failed to save service")}return await window.loadServices(),window.buildGrid(),h}catch(n){throw console.error("Failed to add service to config:",n),n}}async function i(s){const h=document.getElementById("service-subdomain-input").value.trim(),n=document.getElementById("service-ip-input").value.trim()||"localhost",w=document.getElementById("service-port-input").value.trim()||"80",m=await secureFetch("/api/v1/site",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({domain:buildDomain(h),upstream:`${n}:${w}`,config:s})}),r=await m.json();if(!m.ok||!r.success)throw new Error(r.error||`Caddy API Error: ${m.status}`);return r}window.loadExistingCAs=o,window.generateCaddyConfig=f,window.createDnsRecord=u,window.addServiceToConfig=g,window.addToCaddyfile=i})(),(function(){let o=null;function f(n){o=n;const w=document.getElementById("service-edit-modal");document.getElementById("service-edit-title").textContent=`Edit ${n.name}`,document.getElementById("edit-service-name").value=n.name,document.getElementById("edit-service-url-display").textContent=n.url||buildServiceUrl(n.id),document.getElementById("edit-service-logo-preview").src=n.logo||`/assets/${n.id}.png`,document.getElementById("edit-subdomain").value=n.id,document.getElementById("edit-port").value=n.port||"",document.getElementById("edit-ip").value=n.ip||"localhost",document.getElementById("edit-tailscale-only").checked=n.tailscaleOnly||!1,document.getElementById("edit-logo-url").value=n.logo||"",w.classList.add("show")}function u(){closeModal("service-edit-modal"),o=null}async function g(){if(!o)return;const n=document.getElementById("edit-subdomain").value.trim().toLowerCase(),w=document.getElementById("edit-service-name").value.trim(),m=document.getElementById("edit-port").value.trim(),r=document.getElementById("edit-ip").value.trim()||"localhost",l=document.getElementById("edit-tailscale-only").checked,y=document.getElementById("edit-logo-url").value.trim();if(!n){showNotification("Subdomain is required","warning");return}const e=o.id,a=[];if(n!==e&&a.push("subdomain"),w&&w!==o.name&&a.push("name"),m&&m!==String(o.port)&&a.push("port"),r!==o.ip&&a.push("ip"),l!==(o.tailscaleOnly||!1)&&a.push("tailscale"),y&&y!==o.logo&&a.push("logo"),a.length===0){u();return}const p=document.getElementById("service-edit-save");p.textContent="Saving...",p.disabled=!0;try{const c=await(await secureFetch("/api/v1/services/update",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({oldSubdomain:e,newSubdomain:n,name:w||o.name,port:m||o.port,ip:r,tailscaleOnly:l,logo:y||void 0})})).json();if(!c.success)throw new Error(c.error||"Failed to update service");const v=window.APPS.findIndex(d=>d.id===e);v!==-1&&(window.APPS[v]={...window.APPS[v],id:n,name:w||window.APPS[v].name,port:m||window.APPS[v].port,ip:r,tailscaleOnly:l,logo:y||window.APPS[v].logo}),u(),window.buildGrid(),window.refreshAll()}catch(t){console.error("Error saving service changes:",t),showNotification(`Error saving changes: ${t.message}`,"error")}finally{p.textContent="Save Changes",p.disabled=!1}}document.getElementById("edit-logo-file")?.addEventListener("change",async n=>{const w=n.target.files[0];if(!w)return;if(!w.type.startsWith("image/")){showNotification("Please select an image file","warning");return}const m=new FileReader;m.onload=async r=>{const l=r.target.result;if(document.getElementById("edit-service-logo-preview").src=l,document.getElementById("edit-logo-url").value=l,o)try{const e=await(await secureFetch("/api/v1/assets/upload",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({filename:`${o.id}.png`,data:l})})).json();e.success&&e.path&&(document.getElementById("edit-logo-url").value=e.path)}catch{}},m.readAsDataURL(w)}),document.getElementById("service-edit-cancel")?.addEventListener("click",u),document.getElementById("service-edit-save")?.addEventListener("click",g),document.getElementById("service-edit-modal")?.addEventListener("click",n=>{n.target.id==="service-edit-modal"&&u()});function i(n,w,m){return new Promise(r=>{const l=document.getElementById("delete-service-modal"),y=document.getElementById("delete-modal-title"),e=document.getElementById("delete-modal-message"),a=document.getElementById("delete-modal-container-info"),p=document.getElementById("delete-modal-container-name"),t=document.getElementById("delete-modal-help"),c=document.getElementById("delete-modal-cancel"),v=document.getElementById("delete-modal-remove"),d=document.getElementById("delete-modal-delete");y.textContent=`Delete "${n}"`,w?(e.innerHTML="This service has an associated Docker container.
Choose how to proceed:",a.style.display="block",p.textContent=`Container ID: ${m?.slice(0,12)||"Unknown"}`,t.style.display="block",d.style.display="block"):(e.textContent="Remove this service from the dashboard?",a.style.display="none",t.style.display="none",d.style.display="none");const k=()=>{l.classList.remove("show"),c.removeEventListener("click",B),v.removeEventListener("click",T),d.removeEventListener("click",A),l.removeEventListener("click",E)},B=()=>{k(),r(null)},T=()=>{k(),r(!1)},A=()=>{k(),r(!0)},E=L=>{L.target===l&&(k(),r(null))};c.addEventListener("click",B),v.addEventListener("click",T),d.addEventListener("click",A),l.addEventListener("click",E),l.classList.add("show")})}async function s(n,w,m){const r=document.getElementById(`update-btn-${m}`),l=r?.textContent;if(confirm(`Update ${w} to the latest version? +`),e&&(i+=` header { +`,i+=` Access-Control-Allow-Origin "*" +`,i+=` Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" +`,i+=` Access-Control-Allow-Headers "Content-Type, Authorization" +`,i+=` } +`),s)try{const E=JSON.parse(s);i+=` header { +`,Object.entries(E).forEach(([T,L])=>{i+=` ${T} "${L}" +`}),i+=` } +`}catch{console.warn("Invalid JSON in custom headers")}return t&&(i+=` health_uri ${t} +`),i+=` reverse_proxy ${b}:${a} { +`,p&&p!=="/"&&(i+=` rewrite ${p} +`),c&&c!==30&&(i+=` transport http { +`,i+=` dial_timeout ${c}s +`,i+=` response_header_timeout ${c}s +`,i+=` } +`),i+=` } +`,i+=`} +`,i}async function u(r,h,a=DC.DEFAULTS.TTL){const b=window.getToken(getPrimaryDnsId(),"admin");if(!b)throw new Error("DNS admin token not configured. Please set it in the Tokens menu.");const m=buildDomain(r),n=await secureFetch("/api/v1/dns/record",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({domain:m,ip:h,ttl:a,token:b,server:SITE.dnsIp})});if(!n.ok){const g=await n.text();throw new Error(`DNS API Error: ${n.status} - ${g}`)}const d=await n.json();if(!d.success)throw new Error(`DNS Error: ${d.error||"Unknown error"}`);return d}async function y(r){const h={id:r.subdomain,name:r.name,logo:r.logo||`/assets/${r.subdomain}.png`};r.category&&(h.category=r.category),r.containerId&&(h.containerId=r.containerId);try{const a=await secureFetch("/api/v1/services",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(h)});if(!a.ok){const b=await a.json();throw new Error(b.error||"Failed to save service")}return await window.loadServices(),window.buildGrid(),h}catch(a){throw console.error("Failed to add service to config:",a),a}}async function l(r){const h=document.getElementById("service-subdomain-input").value.trim(),a=document.getElementById("service-ip-input").value.trim()||"localhost",b=document.getElementById("service-port-input").value.trim()||"80",m=await secureFetch("/api/v1/site",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({domain:buildDomain(h),upstream:`${a}:${b}`,config:r})}),n=await m.json();if(!m.ok||!n.success)throw new Error(n.error||`Caddy API Error: ${m.status}`);return n}window.loadExistingCAs=o,window.generateCaddyConfig=f,window.createDnsRecord=u,window.addServiceToConfig=y,window.addToCaddyfile=l})(),(function(){let o=null;function f(a){o=a;const b=document.getElementById("service-edit-modal");document.getElementById("service-edit-title").textContent=`Edit ${a.name}`,document.getElementById("edit-service-name").value=a.name,document.getElementById("edit-service-url-display").textContent=a.url||buildServiceUrl(a.id),document.getElementById("edit-service-logo-preview").src=a.logo||`/assets/${a.id}.png`,document.getElementById("edit-subdomain").value=a.id,document.getElementById("edit-port").value=a.port||"",document.getElementById("edit-ip").value=a.ip||"localhost",document.getElementById("edit-tailscale-only").checked=a.tailscaleOnly||!1,document.getElementById("edit-logo-url").value=a.logo||"";const m=document.getElementById("edit-service-category");m&&(m.dataset.current=a.category||"",typeof window.populateCategorySelects=="function"&&window.populateCategorySelects()),b.classList.add("show")}function u(){closeModal("service-edit-modal"),o=null}async function y(){if(!o)return;const a=document.getElementById("edit-subdomain").value.trim().toLowerCase(),b=document.getElementById("edit-service-name").value.trim(),m=document.getElementById("edit-port").value.trim(),n=document.getElementById("edit-ip").value.trim()||"localhost",d=document.getElementById("edit-tailscale-only").checked,g=document.getElementById("edit-logo-url").value.trim(),e=document.getElementById("edit-service-category")?.value||"";if(!a){showNotification("Subdomain is required","warning");return}const s=o.id,p=[];if(a!==s&&p.push("subdomain"),b&&b!==o.name&&p.push("name"),m&&m!==String(o.port)&&p.push("port"),n!==o.ip&&p.push("ip"),d!==(o.tailscaleOnly||!1)&&p.push("tailscale"),g&&g!==o.logo&&p.push("logo"),e!==(o.category||"")&&p.push("category"),p.length===0){u();return}const t=document.getElementById("service-edit-save");t.textContent="Saving...",t.disabled=!0;try{const v=await(await secureFetch("/api/v1/services/update",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({oldSubdomain:s,newSubdomain:a,name:b||o.name,port:m||o.port,ip:n,tailscaleOnly:d,logo:g||void 0,category:e})})).json();if(!v.success)throw new Error(v.error||"Failed to update service");const i=window.APPS.findIndex(E=>E.id===s);i!==-1&&(window.APPS[i]={...window.APPS[i],id:a,name:b||window.APPS[i].name,port:m||window.APPS[i].port,ip:n,tailscaleOnly:d,logo:g||window.APPS[i].logo,category:e||void 0}),u(),window.buildGrid(),window.refreshAll()}catch(c){console.error("Error saving service changes:",c),showNotification(`Error saving changes: ${c.message}`,"error")}finally{t.textContent="Save Changes",t.disabled=!1}}document.getElementById("edit-logo-file")?.addEventListener("change",async a=>{const b=a.target.files[0];if(!b)return;if(!b.type.startsWith("image/")){showNotification("Please select an image file","warning");return}const m=new FileReader;m.onload=async n=>{const d=n.target.result;if(document.getElementById("edit-service-logo-preview").src=d,document.getElementById("edit-logo-url").value=d,o)try{const e=await(await secureFetch("/api/v1/assets/upload",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({filename:`${o.id}.png`,data:d})})).json();e.success&&e.path&&(document.getElementById("edit-logo-url").value=e.path)}catch{}},m.readAsDataURL(b)}),document.getElementById("service-edit-cancel")?.addEventListener("click",u),document.getElementById("service-edit-save")?.addEventListener("click",y),document.getElementById("service-edit-modal")?.addEventListener("click",a=>{a.target.id==="service-edit-modal"&&u()});function l(a,b,m){return new Promise(n=>{const d=document.getElementById("delete-service-modal"),g=document.getElementById("delete-modal-title"),e=document.getElementById("delete-modal-message"),s=document.getElementById("delete-modal-container-info"),p=document.getElementById("delete-modal-container-name"),t=document.getElementById("delete-modal-help"),c=document.getElementById("delete-modal-cancel"),v=document.getElementById("delete-modal-remove"),i=document.getElementById("delete-modal-delete");g.textContent=`Delete "${a}"`,b?(e.innerHTML="This service has an associated Docker container.
Choose how to proceed:",s.style.display="block",p.textContent=`Container ID: ${m?.slice(0,12)||"Unknown"}`,t.style.display="block",i.style.display="block"):(e.textContent="Remove this service from the dashboard?",s.style.display="none",t.style.display="none",i.style.display="none");const E=()=>{d.classList.remove("show"),c.removeEventListener("click",T),v.removeEventListener("click",L),i.removeEventListener("click",P),d.removeEventListener("click",S)},T=()=>{E(),n(null)},L=()=>{E(),n(!1)},P=()=>{E(),n(!0)},S=B=>{B.target===d&&(E(),n(null))};c.addEventListener("click",T),v.addEventListener("click",L),i.addEventListener("click",P),d.addEventListener("click",S),d.classList.add("show")})}async function r(a,b,m){const n=document.getElementById(`update-btn-${m}`),d=n?.textContent;if(confirm(`Update ${b} to the latest version? This will: 1. Pull the latest image 2. Stop the container 3. Recreate with same settings -The service will be briefly unavailable.`))try{r&&(r.textContent="\u{1F504}",r.disabled=!0,r.title="Updating...");const e=await(await secureFetch(`/api/v1/containers/${n}/update`,{method:"POST"})).json();if(e.success){const a=window.APPS.find(p=>p.id===m);a&&e.newContainerId&&(a.containerId=e.newContainerId),r&&(r.textContent="\u2705",r.title="Updated successfully!",setTimeout(()=>{r.textContent=l,r.disabled=!1,r.title="Update container to latest version"},3e3)),setTimeout(()=>window.refreshAll(),2e3),showNotification(`${w} updated successfully!`,"success")}else throw new Error(e.error||"Update failed")}catch(y){console.error("Update error:",y),r&&(r.textContent="\u274C",r.title="Update failed",setTimeout(()=>{r.textContent=l,r.disabled=!1,r.title="Update container to latest version"},3e3)),showNotification(`Failed to update ${w}: ${y.message}`,"error")}}async function h(n,w){const m=window.APPS.find(d=>d.id===n),r=m?buildDomain(m.id):null,l=m?.containerId,y=await i(w||n,l,m?.containerId);if(y===null)return;let e={dashboard:!1,container:null,dns:null,caddy:null,service:null};if(y&&l)try{const d=new URLSearchParams({containerId:m.containerId,subdomain:m.id,ip:m.ip||"localhost",deleteContainer:"true"}),B=await(await secureFetch(`/api/v1/apps/${encodeURIComponent(m.id)}?${d.toString()}`,{method:"DELETE"})).json();B.success?e={...e,...B.results,dashboard:!1}:console.error("App removal failed:",B.error)}catch(d){console.error("App removal error:",d)}else if(y&&r){try{const d=m?.ip||"localhost",B=await(await secureFetch(`/api/v1/dns/record?domain=${encodeURIComponent(r)}&type=A&ipAddress=${encodeURIComponent(d)}&server=${SITE.dnsIp}`,{method:"DELETE"})).json();e.dns=B.success?"deleted":B.error||"failed"}catch(d){e.dns=d.message}try{const k=await(await secureFetch(`/api/v1/site/${encodeURIComponent(r)}`,{method:"DELETE"})).json();e.caddy=k.success||k.error&&k.error.includes("not found")?"removed":k.error||"failed"}catch(d){e.caddy=d.message}}const a=window.APPS.findIndex(d=>d.id===n);a>-1&&(window.APPS.splice(a,1),e.dashboard=!0);try{const d=safeGetJSON("custom-apps",[]),k=d.findIndex(B=>B.id===n);k>-1&&(d.splice(k,1),safeSet("custom-apps",JSON.stringify(d)))}catch{}try{const k=await(await secureFetch(`/api/v1/services/${encodeURIComponent(n)}`,{method:"DELETE"})).json();e.service=k.success?"removed":k.error||"failed"}catch(d){e.service=d.message}window.buildGrid(),window.refreshAll();let p=!1,t=[];e.dashboard||(p=!0,t.push("\u2717 Failed to remove from dashboard"));const c=["removed","already removed","not found","deleted","kept (user choice)","skipped","no such record","does not exist"],v=d=>!d||c.some(k=>d.toLowerCase().includes(k.toLowerCase()));e.container&&!v(e.container)&&(p=!0,t.push(`\u26A0 Container: ${e.container}`)),e.dns&&!v(e.dns)&&(p=!0,t.push(`\u26A0 DNS Record: ${e.dns}`)),e.caddy&&!v(e.caddy)&&(p=!0,t.push(`\u26A0 Caddy Config: ${e.caddy}`)),e.service&&!v(e.service)&&(p=!0,t.push(`\u26A0 Service File: ${e.service}`)),p&&showNotification(`Error deleting "${w||n}": ${t.join(", ")}`,"error",6e3)}window.openServiceEditModal=f,window.showDeleteModal=i,window.updateContainer=s,window.deleteService=h})(),(function(){function o(e){return e.toLowerCase().replace(/\s+/g,"-").replace(/[^a-z0-9-]/g,"").replace(/-+/g,"-").replace(/^-|-$/g,"")}function f(){return SITE.defaults?.sslType||(SITE.configurationType==="public"?"letsencrypt":"caddy-managed")}function u(){const e=document.getElementById("service-subdomain-input").value||"subdomain",a=document.getElementById("service-ip-input").value||g.lan||"localhost",p=document.getElementById("service-port-input").value||DC.DEFAULTS.SERVICE_PORT,t=document.getElementById("ssl-type-select").value,c=document.getElementById("ca-name-input").value||"sami-ca",v=document.getElementById("existing-ca-select").value,d=document.getElementById("enable-auth").checked,k=document.getElementById("enable-cors").checked,B=document.getElementById("custom-headers-input").value,T=document.getElementById("upstream-path-input").value||"/",A=document.getElementById("health-check-input").value,E=document.getElementById("timeout-input").value||30,L=document.getElementById("dns-preview");L&&(L.textContent=`${buildDomain(e)} \u2192 ${a}`);const $=document.getElementById("url-preview");$&&($.textContent=buildServiceUrl(e));const x={subdomain:e,port:p,ip:a,sslType:t,caName:c,existingCa:v,enableAuth:d,enableCors:k,customHeaders:B,upstreamPath:T,healthCheck:A,timeout:E},b=window.generateCaddyConfig(x),I=document.getElementById("caddy-config-preview");I&&(I.value=b)}const g={localhost:"127.0.0.1",lan:"",tailscale:""};async function i(){try{const t=await fetch("/api/v1/network/ips",{signal:AbortSignal.timeout(2e3)});if(t.ok){const c=await t.json();c.lan&&(g.lan=c.lan),c.tailscale&&(g.tailscale=c.tailscale)}}catch{}const e=document.getElementById("quick-ip-lan"),a=document.getElementById("quick-ip-tailscale");e&&(g.lan?(e.dataset.ip=g.lan,e.textContent=`LAN (${g.lan})`,e.title=`LAN IP: ${g.lan}`):e.style.display="none"),a&&(g.tailscale?(a.dataset.ip=g.tailscale,a.textContent=`Tailscale (${g.tailscale})`,a.title=`Tailscale IP: ${g.tailscale}`):a.style.display="none");const p=document.getElementById("service-ip-input");p&&!p.value&&g.lan&&(p.value=g.lan)}function s(){document.querySelectorAll(".quick-ip-btn").forEach(e=>{e.addEventListener("click",()=>{const a=e.dataset.ip;a&&(document.getElementById("service-ip-input").value=a,document.querySelectorAll(".quick-ip-btn").forEach(p=>p.classList.remove("active")),e.classList.add("active"),u())})}),document.getElementById("service-ip-input")?.addEventListener("input",e=>{const a=e.target.value;document.querySelectorAll(".quick-ip-btn").forEach(p=>{p.classList.toggle("active",p.dataset.ip===a)})})}async function h(){const e=document.getElementById("add-service-modal");e.classList.add("show");const a=e.querySelector(".weather-modal-content");a&&(a.scrollTop=0),document.body.style.overflow="hidden";const p=document.getElementById("ssl-type-select");p&&(p.value=f()),await i();const t=document.getElementById("caddyfile-path-input").value||DC.DEFAULTS.CADDYFILE;await window.loadExistingCAs(t);const c=document.getElementById("manual-tailscale-status"),v=document.getElementById("manual-tailscale-only");try{const k=await(await fetch("/api/v1/tailscale/status")).json();k.success&&k.installed&&k.connected?(c.innerHTML=` +The service will be briefly unavailable.`))try{n&&(n.textContent="\u{1F504}",n.disabled=!0,n.title="Updating...");const e=await(await secureFetch(`/api/v1/containers/${a}/update`,{method:"POST"})).json();if(e.success){const s=window.APPS.find(p=>p.id===m);s&&e.newContainerId&&(s.containerId=e.newContainerId),n&&(n.textContent="\u2705",n.title="Updated successfully!",setTimeout(()=>{n.textContent=d,n.disabled=!1,n.title="Update container to latest version"},3e3)),setTimeout(()=>window.refreshAll(),2e3),showNotification(`${b} updated successfully!`,"success")}else throw new Error(e.error||"Update failed")}catch(g){console.error("Update error:",g),n&&(n.textContent="\u274C",n.title="Update failed",setTimeout(()=>{n.textContent=d,n.disabled=!1,n.title="Update container to latest version"},3e3)),showNotification(`Failed to update ${b}: ${g.message}`,"error")}}async function h(a,b){const m=window.APPS.find(i=>i.id===a),n=m?buildDomain(m.id):null,d=m?.containerId,g=await l(b||a,d,m?.containerId);if(g===null)return;let e={dashboard:!1,container:null,dns:null,caddy:null,service:null};if(g&&d)try{const i=new URLSearchParams({containerId:m.containerId,subdomain:m.id,ip:m.ip||"localhost",deleteContainer:"true"}),T=await(await secureFetch(`/api/v1/apps/${encodeURIComponent(m.id)}?${i.toString()}`,{method:"DELETE"})).json();T.success?e={...e,...T.results,dashboard:!1}:console.error("App removal failed:",T.error)}catch(i){console.error("App removal error:",i)}else if(g&&n){try{const i=m?.ip||"localhost",T=await(await secureFetch(`/api/v1/dns/record?domain=${encodeURIComponent(n)}&type=A&ipAddress=${encodeURIComponent(i)}&server=${SITE.dnsIp}`,{method:"DELETE"})).json();e.dns=T.success?"deleted":T.error||"failed"}catch(i){e.dns=i.message}try{const E=await(await secureFetch(`/api/v1/site/${encodeURIComponent(n)}`,{method:"DELETE"})).json();e.caddy=E.success||E.error&&E.error.includes("not found")?"removed":E.error||"failed"}catch(i){e.caddy=i.message}}const s=window.APPS.findIndex(i=>i.id===a);s>-1&&(window.APPS.splice(s,1),e.dashboard=!0);try{const i=safeGetJSON("custom-apps",[]),E=i.findIndex(T=>T.id===a);E>-1&&(i.splice(E,1),safeSet("custom-apps",JSON.stringify(i)))}catch{}try{const E=await(await secureFetch(`/api/v1/services/${encodeURIComponent(a)}`,{method:"DELETE"})).json();e.service=E.success?"removed":E.error||"failed"}catch(i){e.service=i.message}window.buildGrid(),window.refreshAll();let p=!1,t=[];e.dashboard||(p=!0,t.push("\u2717 Failed to remove from dashboard"));const c=["removed","already removed","not found","deleted","kept (user choice)","skipped","no such record","does not exist"],v=i=>!i||c.some(E=>i.toLowerCase().includes(E.toLowerCase()));e.container&&!v(e.container)&&(p=!0,t.push(`\u26A0 Container: ${e.container}`)),e.dns&&!v(e.dns)&&(p=!0,t.push(`\u26A0 DNS Record: ${e.dns}`)),e.caddy&&!v(e.caddy)&&(p=!0,t.push(`\u26A0 Caddy Config: ${e.caddy}`)),e.service&&!v(e.service)&&(p=!0,t.push(`\u26A0 Service File: ${e.service}`)),p&&showNotification(`Error deleting "${b||a}": ${t.join(", ")}`,"error",6e3)}window.openServiceEditModal=f,window.showDeleteModal=l,window.updateContainer=r,window.deleteService=h})(),(function(){function o(e){return e.toLowerCase().replace(/\s+/g,"-").replace(/[^a-z0-9-]/g,"").replace(/-+/g,"-").replace(/^-|-$/g,"")}function f(){return SITE.defaults?.sslType||(SITE.configurationType==="public"?"letsencrypt":"caddy-managed")}function u(){const e=document.getElementById("service-subdomain-input").value||"subdomain",s=document.getElementById("service-ip-input").value||y.lan||"localhost",p=document.getElementById("service-port-input").value||DC.DEFAULTS.SERVICE_PORT,t=document.getElementById("ssl-type-select").value,c=document.getElementById("ca-name-input").value||"sami-ca",v=document.getElementById("existing-ca-select").value,i=document.getElementById("enable-auth").checked,E=document.getElementById("enable-cors").checked,T=document.getElementById("custom-headers-input").value,L=document.getElementById("upstream-path-input").value||"/",P=document.getElementById("health-check-input").value,S=document.getElementById("timeout-input").value||30,B=document.getElementById("dns-preview");B&&(B.textContent=`${buildDomain(e)} \u2192 ${s}`);const $=document.getElementById("url-preview");$&&($.textContent=buildServiceUrl(e));const x={subdomain:e,port:p,ip:s,sslType:t,caName:c,existingCa:v,enableAuth:i,enableCors:E,customHeaders:T,upstreamPath:L,healthCheck:P,timeout:S},w=window.generateCaddyConfig(x),C=document.getElementById("caddy-config-preview");C&&(C.value=w)}const y={localhost:"127.0.0.1",lan:"",tailscale:""};async function l(){try{const t=await fetch("/api/v1/network/ips",{signal:AbortSignal.timeout(2e3)});if(t.ok){const c=await t.json();c.lan&&(y.lan=c.lan),c.tailscale&&(y.tailscale=c.tailscale)}}catch{}const e=document.getElementById("quick-ip-lan"),s=document.getElementById("quick-ip-tailscale");e&&(y.lan?(e.dataset.ip=y.lan,e.textContent=`LAN (${y.lan})`,e.title=`LAN IP: ${y.lan}`):e.style.display="none"),s&&(y.tailscale?(s.dataset.ip=y.tailscale,s.textContent=`Tailscale (${y.tailscale})`,s.title=`Tailscale IP: ${y.tailscale}`):s.style.display="none");const p=document.getElementById("service-ip-input");p&&!p.value&&y.lan&&(p.value=y.lan)}function r(){document.querySelectorAll(".quick-ip-btn").forEach(e=>{e.addEventListener("click",()=>{const s=e.dataset.ip;s&&(document.getElementById("service-ip-input").value=s,document.querySelectorAll(".quick-ip-btn").forEach(p=>p.classList.remove("active")),e.classList.add("active"),u())})}),document.getElementById("service-ip-input")?.addEventListener("input",e=>{const s=e.target.value;document.querySelectorAll(".quick-ip-btn").forEach(p=>{p.classList.toggle("active",p.dataset.ip===s)})})}async function h(){const e=document.getElementById("add-service-modal");e.classList.add("show");const s=e.querySelector(".weather-modal-content");s&&(s.scrollTop=0),document.body.style.overflow="hidden";const p=document.getElementById("ssl-type-select");p&&(p.value=f()),await l();const t=document.getElementById("caddyfile-path-input").value||DC.DEFAULTS.CADDYFILE;await window.loadExistingCAs(t);const c=document.getElementById("manual-tailscale-status"),v=document.getElementById("manual-tailscale-only");try{const E=await(await fetch("/api/v1/tailscale/status")).json();E.success&&E.installed&&E.connected?(c.innerHTML=` \u2713 Connected - ${k.self?.hostname} (${k.self?.ip}) - `,v.disabled=!1):k.installed?(c.innerHTML='\u26A0 Not connected',v.disabled=!0):(c.innerHTML='Not available',v.disabled=!0)}catch{c.innerHTML='Could not check',v.disabled=!0}v.checked=!1,u()}function n(){const e=document.getElementById("service-type-local"),a=document.getElementById("service-type-external"),p=document.getElementById("local-service-config"),t=document.getElementById("external-service-config"),c=document.getElementById("tab-local"),v=document.getElementById("tab-external");function d(){e.checked?(p.style.display="grid",t.style.display="none",c&&(c.style.background="var(--accent)",c.style.color="var(--bg)"),v&&(v.style.background="transparent",v.style.color="var(--muted)")):(p.style.display="none",t.style.display="block",v&&(v.style.background="var(--accent)",v.style.color="var(--bg)"),c&&(c.style.background="transparent",c.style.color="var(--muted)"))}e?.addEventListener("change",d),a?.addEventListener("change",d)}function w(){const e=document.getElementById("service-name-input"),a=document.getElementById("service-subdomain-input"),p=document.getElementById("subdomain-preview");let t=!1;e?.addEventListener("input",()=>{const T=o(e.value);!t&&a&&(a.value=T),p&&(p.textContent=T?`\u2192 ${buildDomain(T)}`:""),u()}),a?.addEventListener("input",()=>{t=a.value!==o(e?.value||"");const T=a.value.trim()||o(e?.value||"");p&&(p.textContent=T?`\u2192 ${buildDomain(T)}`:""),u()});const c=document.getElementById("external-service-name"),v=document.getElementById("external-service-subdomain"),d=document.getElementById("external-subdomain-preview"),k=document.getElementById("external-domain-preview");let B=!1;c?.addEventListener("input",()=>{const T=o(c.value);!B&&v&&(v.value=T);const A=v?.value||T;d&&(d.textContent=A?`\u2192 ${buildDomain(A)}`:""),k&&(k.textContent=A?buildDomain(A):"")}),v?.addEventListener("input",()=>{B=v.value!==o(c?.value||"");const T=v.value.trim()||o(c?.value||"");d&&(d.textContent=T?`\u2192 ${buildDomain(T)}`:""),k&&(k.textContent=T?buildDomain(T):"")})}async function m(){const e=document.getElementById("external-service-name").value.trim(),a=document.getElementById("external-service-url").value.trim(),p=(document.getElementById("external-service-subdomain").value.trim()||o(e)).toLowerCase(),t=document.getElementById("external-service-logo").value.trim(),c=document.getElementById("external-service-icon").value.trim(),v=document.getElementById("external-create-dns").checked,d=document.getElementById("external-create-caddy").checked,k=document.getElementById("external-proxy-ip").value.trim()||SITE.dnsIp||"localhost",B=document.getElementById("external-preserve-host").checked,T=document.getElementById("external-follow-redirects").checked;if(!e||!a){showNotification("Please fill in Name and External URL","warning");return}if(!p){showNotification("Could not derive subdomain from name. Please set one in Options.","warning");return}if(!a.startsWith("http://")&&!a.startsWith("https://")){showNotification("External URL must start with http:// or https://","warning");return}const A=buildDomain(p);try{const E={dns:null,caddy:null,dashboard:!1};if(v)if(window.getToken(getPrimaryDnsId(),"admin"))try{const C=await(await secureFetch("/api/v1/dns/record",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({domain:A,ip:k,ttl:DC.DEFAULTS.TTL,server:SITE.dnsIp})})).json();E.dns=C.success?"created":C.error||"failed"}catch(S){E.dns=S.message}else E.dns="no admin token (configure in \u{1F511} Tokens)";if(d)try{const I={subdomain:p,externalUrl:a,preserveHost:B,followRedirects:T,sslType:"caddy-managed",caddyfilePath:DC.DEFAULTS.CADDYFILE,reloadCaddy:!0},C=await(await secureFetch("/api/v1/site/external",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(I)})).json();E.caddy=C.success?"created":C.error||"failed"}catch(I){E.caddy=I.message}const L={id:p,name:e,url:`https://${A}`,externalUrl:a,logo:t||c||"\u{1F310}",isExternal:!0,isCustom:!0};window.APPS.push(L),E.dashboard=!0;const $=["plex","router","chat","sync","torrent","radarr","sonarr","prowlarr","portainer","requests","jellyfin","emby"],x=window.APPS.filter(I=>!$.includes(I.id));safeSet("custom-services",JSON.stringify(x));try{await secureFetch("/api/v1/services",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(window.APPS)})}catch(I){console.warn("Failed to save to services.json:",I)}window.buildGrid(),window.refreshAll(),r();const b=[`External service "${e}" added!`];v&&b.push(`DNS: ${E.dns==="created"?"\u2713":"\u26A0 "+E.dns}`),d&&b.push(`Caddy: ${E.caddy==="created"?"\u2713":"\u26A0 "+E.caddy}`),b.push(`Access at: https://${A}`),showNotification(b.join(" | "),"success",6e3)}catch(E){console.error("Failed to create external service:",E),showNotification(`Failed to create external service: ${E.message}`,"error")}}function r(){closeModal("add-service-modal"),document.body.style.overflow="",document.getElementById("service-name-input").value="",document.getElementById("service-subdomain-input").value="",document.getElementById("service-port-input").value="",document.getElementById("service-ip-input").value=g.lan||"",document.getElementById("service-logo-input").value="",document.getElementById("dns-ttl-input").value=DC.DEFAULTS.TTL,document.getElementById("ssl-type-select").value=f(),document.getElementById("ca-name-input").value="",document.getElementById("enable-auth").checked=!1,document.getElementById("enable-cors").checked=!1,document.getElementById("custom-headers-input").value="",document.getElementById("upstream-path-input").value="/",document.getElementById("health-check-input").value="",document.getElementById("timeout-input").value="30";const e=document.getElementById("subdomain-preview");e&&(e.textContent="");const a=document.getElementById("external-subdomain-preview");a&&(a.textContent="");const p=document.getElementById("external-service-name");p&&(p.value="");const t=document.getElementById("external-service-subdomain");t&&(t.value="");const c=document.getElementById("external-service-url");c&&(c.value="");const v=document.getElementById("external-service-logo");v&&(v.value="");const d=document.getElementById("external-service-icon");d&&(d.value="");const k=document.getElementById("local-advanced-options");k&&k.removeAttribute("open");const B=document.getElementById("external-advanced-options");B&&B.removeAttribute("open");const T=document.getElementById("service-type-local");T&&(T.checked=!0);const A=document.getElementById("local-service-config"),E=document.getElementById("external-service-config");A&&(A.style.display="grid"),E&&(E.style.display="none");const L=document.getElementById("tab-local"),$=document.getElementById("tab-external");L&&(L.style.background="var(--accent)",L.style.color="var(--bg)"),$&&($.style.background="transparent",$.style.color="var(--muted)")}async function l(){const e=document.getElementById("service-name-input").value.trim(),a=(document.getElementById("service-subdomain-input").value.trim()||o(e)).toLowerCase(),p=document.getElementById("service-port-input").value.trim(),t=document.getElementById("service-ip-input").value.trim(),c=document.getElementById("service-logo-input").value.trim(),v=document.getElementById("create-dns-record").checked,d=parseInt(document.getElementById("dns-ttl-input").value)||DC.DEFAULTS.TTL,k=document.getElementById("manual-tailscale-only")?.checked||!1,B=document.getElementById("ssl-type-select")?.value||"caddy-managed",T=document.getElementById("ca-name-input")?.value||"",A=document.getElementById("existing-ca-select")?.value||"",E=document.getElementById("enable-auth")?.checked||!1,L=document.getElementById("enable-cors")?.checked||!1,$=document.getElementById("custom-headers-input")?.value||"",x=document.getElementById("upstream-path-input")?.value||"/",b=document.getElementById("health-check-input")?.value||"",I=document.getElementById("timeout-input")?.value||30,S=window.getToken(getPrimaryDnsId(),"admin");if(!e||!p||!t){showNotification("Please fill in Name, Port, and IP Address","warning");return}if(!a){showNotification("Could not derive subdomain from name. Please set one in Options.","warning");return}if(v&&!S){showNotification("DNS Admin token required. Configure it in the Tokens menu first.","warning");return}const C={dns:null,caddy:null,dashboard:!1};try{if(v)try{await window.createDnsRecord(a,t,d),C.dns="created"}catch(N){throw console.error("DNS creation failed:",N),C.dns=N.message,new Error(`DNS creation failed: ${N.message}`)}else C.dns="skipped";const P=window.generateCaddyConfig({subdomain:a,port:p,ip:t,sslType:B,caName:T,existingCa:A,enableAuth:E,enableCors:L,customHeaders:$,upstreamPath:x,healthCheck:b,timeout:I,tailscaleOnly:k});try{const R=await(await secureFetch("/api/v1/site",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({domain:buildDomain(a),upstream:`${t}:${p}`,config:P})})).json();if(R.success)C.caddy="added & reloaded";else throw console.error("Caddy configuration failed:",R.error),C.caddy=R.error||"failed",new Error(`Caddy configuration failed: ${R.error}`)}catch(N){throw console.error("Caddy API error:",N),C.caddy=N.message,new Error(`Caddy API error: ${N.message}`)}const D={name:e,subdomain:a,port:p,ip:t,logo:c||`/assets/${a}.png`,tailscaleOnly:k||!1};await window.addServiceToConfig(D),C.dashboard=!0;const O=[`DNS: ${C.dns==="created"?"\u2713":C.dns==="skipped"?"\u25CB":"\u2717"}`,`Caddy: ${C.caddy==="added & reloaded"?"\u2713":"\u2717"}`,`Dashboard: ${C.dashboard?"\u2713":"\u2717"}`];showNotification(`Service "${e}" created! ${O.join(" | ")} \u2014 ${buildServiceUrl(a)}${k?" (Tailscale)":""}`,"success",6e3),r(),window.buildGrid(),window.refreshAll()}catch(P){console.error("Error creating service:",P),showNotification(`Error creating "${e}": ${P.message}`,"error",6e3)}}document.getElementById("add-service")?.addEventListener("click",h),document.getElementById("add-service-cancel")?.addEventListener("click",r),document.getElementById("add-service-create")?.addEventListener("click",()=>{document.querySelector('input[name="service-type"]:checked')?.value==="external"?m():l()}),n(),w(),s(),document.getElementById("ssl-type-select")?.addEventListener("change",e=>{const a=document.getElementById("existing-ca-config"),p=document.getElementById("custom-ca-config");a.style.display="none",p.style.display="none",e.target.value==="existing-ca"?a.style.display="block":e.target.value==="custom-ca"&&(p.style.display="block"),u()}),document.getElementById("refresh-cas")?.addEventListener("click",async()=>{const e=document.getElementById("refresh-cas"),a=e.textContent;e.textContent="\u231B Loading...",e.disabled=!0;try{const p=document.getElementById("caddyfile-path-input").value||DC.DEFAULTS.CADDYFILE;await window.loadExistingCAs(p),e.textContent="\u2705 Refreshed"}catch(p){e.textContent="\u274C Failed",console.error("Failed to refresh CAs:",p)}setTimeout(()=>{e.textContent=a,e.disabled=!1},2e3)}),document.getElementById("create-dns-record")?.addEventListener("change",e=>{const a=document.getElementById("dns-config");a.style.display=e.target.checked?"block":"none"}),["service-subdomain-input","service-ip-input","service-port-input","ca-name-input","existing-ca-select","enable-auth","enable-cors","custom-headers-input","upstream-path-input","health-check-input","timeout-input"].forEach(e=>{const a=document.getElementById(e);a&&(a.addEventListener("input",u),a.addEventListener("change",u))});function y(){const e=safeGet("custom-services");if(e)try{JSON.parse(e).forEach(p=>{window.APPS.find(t=>t.id===p.id)||window.APPS.push(p)})}catch(a){console.warn("Failed to load custom services:",a)}}y(),window.openAddServiceModal=h,window.closeAddServiceModal=r})(),(function(){let o=null,f=1e3;const u=3e4;function g(){if(o)try{o.close()}catch{}o=new EventSource("/api/v1/events/stream"),o.addEventListener("connected",()=>{f=1e3,debug("[SSE] Connected to event stream")}),o.addEventListener("status-change",i=>{try{const s=JSON.parse(i.data);if(s.serviceId&&typeof window.setBadge=="function"){const h=s.status==="up"||s.status==="healthy";window.setBadge(s.serviceId,h,s.responseTime||null)}}catch{}}),o.addEventListener("resource-alert",i=>{try{const s=JSON.parse(i.data),h=`${s.containerName||s.containerId}: ${s.metric} at ${s.value}% (threshold: ${s.threshold}%)`;typeof showNotification=="function"&&showNotification(h,"warning")}catch{}}),o.addEventListener("auto-restart",i=>{try{const s=JSON.parse(i.data);typeof showNotification=="function"&&showNotification(`Container "${s.containerName}" was auto-restarted`,"info")}catch{}}),o.addEventListener("update-available",i=>{try{const s=JSON.parse(i.data),h=document.getElementById("updates-btn");if(h&&!h.querySelector(".sse-dot")){const n=document.createElement("span");n.className="sse-dot",n.style.cssText="display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--accent);margin-left:6px;vertical-align:middle;",h.appendChild(n)}typeof showNotification=="function"&&showNotification(`Update available for ${s.containerName||s.containerId}`,"info")}catch{}}),o.addEventListener("update-complete",i=>{try{const s=JSON.parse(i.data);typeof showNotification=="function"&&showNotification(`Update completed: ${s.containerName||s.containerId}`,"success"),typeof window.refreshAll=="function"&&window.refreshAll()}catch{}}),o.addEventListener("update-failed",i=>{try{const s=JSON.parse(i.data);typeof showNotification=="function"&&showNotification(`Update failed: ${s.containerName||s.containerId} \u2014 ${s.error||"unknown error"}`,"error")}catch{}}),o.addEventListener("incident",i=>{try{const s=JSON.parse(i.data);typeof showNotification=="function"&&(s.type==="created"?showNotification(`Incident: ${s.message||s.serviceId}`,"error"):s.type==="resolved"&&showNotification(`Resolved: ${s.serviceId||"incident"}`,"success"))}catch{}}),o.onerror=()=>{o.close(),console.warn(`[SSE] Disconnected, reconnecting in ${f/1e3}s...`),setTimeout(g,f),f=Math.min(f*2,u)}}g(),window._sseReconnect=g})(),(function(){const o=document.getElementById("service-filter-search"),f=document.getElementById("service-filter-status"),u=document.getElementById("service-filter-count");function g(){const s=o.value.toLowerCase().trim(),h=f.value,n=document.querySelectorAll("#cards .card");let w=0;if(n.forEach(m=>{const r=m.querySelector(".name")?.textContent?.toLowerCase()||"",l=m.dataset.app?.toLowerCase()||"",y=m.dataset.status||"off";(!s||r.includes(s)||l.includes(s))&&(h==="all"||y===h)?(m.style.display="",w++):m.style.display="none"}),u){const m=n.length;u.textContent=`${w} of ${m} services`}}function i(s,h){let n;return function(...w){clearTimeout(n),n=setTimeout(()=>s.apply(this,w),h)}}o?.addEventListener("input",i(g,200)),f?.addEventListener("change",g),document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>setTimeout(g,500)):setTimeout(g,500),window.refreshServiceFilter=g})(),(function(){const o=document.getElementById("batch-operations-btn"),f=document.getElementById("batch-action-bar"),u=document.getElementById("batch-selected-count"),g=document.getElementById("batch-start-btn"),i=document.getElementById("batch-stop-btn"),s=document.getElementById("batch-restart-btn"),h=document.getElementById("batch-cancel-btn");let n=!1,w=new Set;function m(){n=!0,w.clear(),f.style.display="",o.textContent="\u2713 Exit Batch Mode",l(),document.querySelectorAll("#cards .card[data-app]").forEach(a=>{const p=a.dataset.containerId;if(!p)return;const t=a.querySelector(".batch-checkbox");t&&t.remove();const c=document.createElement("input");c.type="checkbox",c.className="batch-checkbox",c.dataset.containerId=p,c.dataset.serviceName=a.querySelector(".name")?.textContent||p,c.style.cssText="position: absolute; top: 8px; left: 8px; z-index: 10; width: 18px; height: 18px; cursor: pointer;",c.addEventListener("change",v=>{v.stopPropagation(),c.checked?w.add(p):w.delete(p),l()}),a.style.position="relative",a.insertBefore(c,a.firstChild)})}function r(){n=!1,w.clear(),f.style.display="none",o.textContent="\u2630 Batch Operations",document.querySelectorAll(".batch-checkbox").forEach(e=>e.remove())}function l(){const e=w.size;u.textContent=`${e} selected`,g.disabled=e===0,i.disabled=e===0,s.disabled=e===0}async function y(e){if(w.size===0)return;const a=Array.from(w),p={start:"Starting",stop:"Stopping",restart:"Restarting"}[e];if(!confirm(`${p} ${a.length} container(s)? This cannot be undone.`))return;const t=[g,i,s];t.forEach(k=>{k.disabled=!0,k.textContent="..."});let c=0,v=0;const d=[];for(const k of a)try{const B=await fetch(`/api/v1/containers/${encodeURIComponent(k)}/${e}`,{method:"POST"});if(B.ok)c++;else{v++;const T=await B.json().catch(()=>({}));d.push(`${k}: ${T.error||B.statusText}`)}}catch(B){v++,d.push(`${k}: ${B.message}`)}t[0].textContent="\u25B6 Start All",t[1].textContent="\u2B1B Stop All",t[2].textContent="\u{1F504} Restart All",l(),v===0?typeof showNotification=="function"&&showNotification(`${p} completed: ${c} container(s)`,"success"):(typeof showNotification=="function"&&showNotification(`${p}: ${c} succeeded, ${v} failed`,"warning"),console.error("Batch operation errors:",d)),setTimeout(()=>{typeof refreshAll=="function"&&refreshAll()},1500)}o?.addEventListener("click",()=>{n?r():m()}),g?.addEventListener("click",()=>y("start")),i?.addEventListener("click",()=>y("stop")),s?.addEventListener("click",()=>y("restart")),h?.addEventListener("click",r)})(); + ${E.self?.hostname} (${E.self?.ip}) + `,v.disabled=!1):E.installed?(c.innerHTML='\u26A0 Not connected',v.disabled=!0):(c.innerHTML='Not available',v.disabled=!0)}catch{c.innerHTML='Could not check',v.disabled=!0}v.checked=!1,u()}function a(){const e=document.getElementById("service-type-local"),s=document.getElementById("service-type-external"),p=document.getElementById("local-service-config"),t=document.getElementById("external-service-config"),c=document.getElementById("tab-local"),v=document.getElementById("tab-external");function i(){e.checked?(p.style.display="grid",t.style.display="none",c&&(c.style.background="var(--accent)",c.style.color="var(--bg)"),v&&(v.style.background="transparent",v.style.color="var(--muted)")):(p.style.display="none",t.style.display="block",v&&(v.style.background="var(--accent)",v.style.color="var(--bg)"),c&&(c.style.background="transparent",c.style.color="var(--muted)"))}e?.addEventListener("change",i),s?.addEventListener("change",i)}function b(){const e=document.getElementById("service-name-input"),s=document.getElementById("service-subdomain-input"),p=document.getElementById("subdomain-preview");let t=!1;e?.addEventListener("input",()=>{const L=o(e.value);!t&&s&&(s.value=L),p&&(p.textContent=L?`\u2192 ${buildDomain(L)}`:""),u()}),s?.addEventListener("input",()=>{t=s.value!==o(e?.value||"");const L=s.value.trim()||o(e?.value||"");p&&(p.textContent=L?`\u2192 ${buildDomain(L)}`:""),u()});const c=document.getElementById("external-service-name"),v=document.getElementById("external-service-subdomain"),i=document.getElementById("external-subdomain-preview"),E=document.getElementById("external-domain-preview");let T=!1;c?.addEventListener("input",()=>{const L=o(c.value);!T&&v&&(v.value=L);const P=v?.value||L;i&&(i.textContent=P?`\u2192 ${buildDomain(P)}`:""),E&&(E.textContent=P?buildDomain(P):"")}),v?.addEventListener("input",()=>{T=v.value!==o(c?.value||"");const L=v.value.trim()||o(c?.value||"");i&&(i.textContent=L?`\u2192 ${buildDomain(L)}`:""),E&&(E.textContent=L?buildDomain(L):"")})}async function m(){const e=document.getElementById("external-service-name").value.trim(),s=document.getElementById("external-service-url").value.trim(),p=(document.getElementById("external-service-subdomain").value.trim()||o(e)).toLowerCase(),t=document.getElementById("external-service-logo").value.trim(),c=document.getElementById("external-service-icon").value.trim(),v=document.getElementById("external-create-dns").checked,i=document.getElementById("external-create-caddy").checked,E=document.getElementById("external-proxy-ip").value.trim()||SITE.dnsIp||"localhost",T=document.getElementById("external-preserve-host").checked,L=document.getElementById("external-follow-redirects").checked,P=document.getElementById("external-service-category")?.value||"";if(!e||!s){showNotification("Please fill in Name and External URL","warning");return}if(!p){showNotification("Could not derive subdomain from name. Please set one in Options.","warning");return}if(!s.startsWith("http://")&&!s.startsWith("https://")){showNotification("External URL must start with http:// or https://","warning");return}const S=buildDomain(p);try{const B={dns:null,caddy:null,dashboard:!1};if(v)if(window.getToken(getPrimaryDnsId(),"admin"))try{const A=await(await secureFetch("/api/v1/dns/record",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({domain:S,ip:E,ttl:DC.DEFAULTS.TTL,server:SITE.dnsIp})})).json();B.dns=A.success?"created":A.error||"failed"}catch(I){B.dns=I.message}else B.dns="no admin token (configure in \u{1F511} Tokens)";if(i)try{const k={subdomain:p,externalUrl:s,preserveHost:T,followRedirects:L,sslType:"caddy-managed",caddyfilePath:DC.DEFAULTS.CADDYFILE,reloadCaddy:!0},A=await(await secureFetch("/api/v1/site/external",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(k)})).json();B.caddy=A.success?"created":A.error||"failed"}catch(k){B.caddy=k.message}const $={id:p,name:e,url:`https://${S}`,externalUrl:s,logo:t||c||"\u{1F310}",isExternal:!0,isCustom:!0};P&&($.category=P),window.APPS.push($),B.dashboard=!0;const x=["plex","router","chat","sync","torrent","radarr","sonarr","prowlarr","portainer","requests","jellyfin","emby"],w=window.APPS.filter(k=>!x.includes(k.id));safeSet("custom-services",JSON.stringify(w));try{await secureFetch("/api/v1/services",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(window.APPS)})}catch(k){console.warn("Failed to save to services.json:",k)}window.buildGrid(),window.refreshAll(),n();const C=[`External service "${e}" added!`];v&&C.push(`DNS: ${B.dns==="created"?"\u2713":"\u26A0 "+B.dns}`),i&&C.push(`Caddy: ${B.caddy==="created"?"\u2713":"\u26A0 "+B.caddy}`),C.push(`Access at: https://${S}`),showNotification(C.join(" | "),"success",6e3)}catch(B){console.error("Failed to create external service:",B),showNotification(`Failed to create external service: ${B.message}`,"error")}}function n(){closeModal("add-service-modal"),document.body.style.overflow="",document.getElementById("service-name-input").value="",document.getElementById("service-subdomain-input").value="",document.getElementById("service-port-input").value="",document.getElementById("service-ip-input").value=y.lan||"",document.getElementById("service-logo-input").value="",document.getElementById("dns-ttl-input").value=DC.DEFAULTS.TTL,document.getElementById("ssl-type-select").value=f(),document.getElementById("ca-name-input").value="",document.getElementById("enable-auth").checked=!1,document.getElementById("enable-cors").checked=!1,document.getElementById("custom-headers-input").value="",document.getElementById("upstream-path-input").value="/",document.getElementById("health-check-input").value="",document.getElementById("timeout-input").value="30";const e=document.getElementById("subdomain-preview");e&&(e.textContent="");const s=document.getElementById("external-subdomain-preview");s&&(s.textContent="");const p=document.getElementById("external-service-name");p&&(p.value="");const t=document.getElementById("external-service-subdomain");t&&(t.value="");const c=document.getElementById("external-service-url");c&&(c.value="");const v=document.getElementById("external-service-logo");v&&(v.value="");const i=document.getElementById("external-service-icon");i&&(i.value="");const E=document.getElementById("local-advanced-options");E&&E.removeAttribute("open");const T=document.getElementById("external-advanced-options");T&&T.removeAttribute("open");const L=document.getElementById("service-type-local");L&&(L.checked=!0);const P=document.getElementById("local-service-config"),S=document.getElementById("external-service-config");P&&(P.style.display="grid"),S&&(S.style.display="none");const B=document.getElementById("tab-local"),$=document.getElementById("tab-external");B&&(B.style.background="var(--accent)",B.style.color="var(--bg)"),$&&($.style.background="transparent",$.style.color="var(--muted)")}async function d(){const e=document.getElementById("service-name-input").value.trim(),s=(document.getElementById("service-subdomain-input").value.trim()||o(e)).toLowerCase(),p=document.getElementById("service-port-input").value.trim(),t=document.getElementById("service-ip-input").value.trim(),c=document.getElementById("service-logo-input").value.trim(),v=document.getElementById("create-dns-record").checked,i=parseInt(document.getElementById("dns-ttl-input").value)||DC.DEFAULTS.TTL,E=document.getElementById("manual-tailscale-only")?.checked||!1,T=document.getElementById("ssl-type-select")?.value||"caddy-managed",L=document.getElementById("ca-name-input")?.value||"",P=document.getElementById("existing-ca-select")?.value||"",S=document.getElementById("enable-auth")?.checked||!1,B=document.getElementById("enable-cors")?.checked||!1,$=document.getElementById("custom-headers-input")?.value||"",x=document.getElementById("upstream-path-input")?.value||"/",w=document.getElementById("health-check-input")?.value||"",C=document.getElementById("timeout-input")?.value||30,I=(document.getElementById("service-category-input")||document.getElementById("external-service-category"))?.value||"",A=window.getToken(getPrimaryDnsId(),"admin");if(!e||!p||!t){showNotification("Please fill in Name, Port, and IP Address","warning");return}if(!s){showNotification("Could not derive subdomain from name. Please set one in Options.","warning");return}if(v&&!A){showNotification("DNS Admin token required. Configure it in the Tokens menu first.","warning");return}const O={dns:null,caddy:null,dashboard:!1};try{if(v)try{await window.createDnsRecord(s,t,i),O.dns="created"}catch(N){throw console.error("DNS creation failed:",N),O.dns=N.message,new Error(`DNS creation failed: ${N.message}`)}else O.dns="skipped";const D=window.generateCaddyConfig({subdomain:s,port:p,ip:t,sslType:T,caName:L,existingCa:P,enableAuth:S,enableCors:B,customHeaders:$,upstreamPath:x,healthCheck:w,timeout:C,tailscaleOnly:E});try{const U=await(await secureFetch("/api/v1/site",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({domain:buildDomain(s),upstream:`${t}:${p}`,config:D})})).json();if(U.success)O.caddy="added & reloaded";else throw console.error("Caddy configuration failed:",U.error),O.caddy=U.error||"failed",new Error(`Caddy configuration failed: ${U.error}`)}catch(N){throw console.error("Caddy API error:",N),O.caddy=N.message,new Error(`Caddy API error: ${N.message}`)}const M={name:e,subdomain:s,port:p,ip:t,logo:c||`/assets/${s}.png`,tailscaleOnly:E||!1};I&&(M.category=I),await window.addServiceToConfig(M),O.dashboard=!0;const R=[`DNS: ${O.dns==="created"?"\u2713":O.dns==="skipped"?"\u25CB":"\u2717"}`,`Caddy: ${O.caddy==="added & reloaded"?"\u2713":"\u2717"}`,`Dashboard: ${O.dashboard?"\u2713":"\u2717"}`];showNotification(`Service "${e}" created! ${R.join(" | ")} \u2014 ${buildServiceUrl(s)}${E?" (Tailscale)":""}`,"success",6e3),n(),window.buildGrid(),window.refreshAll()}catch(D){console.error("Error creating service:",D),showNotification(`Error creating "${e}": ${D.message}`,"error",6e3)}}document.getElementById("add-service")?.addEventListener("click",h),document.getElementById("add-service-cancel")?.addEventListener("click",n),document.getElementById("add-service-create")?.addEventListener("click",()=>{document.querySelector('input[name="service-type"]:checked')?.value==="external"?m():d()}),a(),b(),r(),document.getElementById("ssl-type-select")?.addEventListener("change",e=>{const s=document.getElementById("existing-ca-config"),p=document.getElementById("custom-ca-config");s.style.display="none",p.style.display="none",e.target.value==="existing-ca"?s.style.display="block":e.target.value==="custom-ca"&&(p.style.display="block"),u()}),document.getElementById("refresh-cas")?.addEventListener("click",async()=>{const e=document.getElementById("refresh-cas"),s=e.textContent;e.textContent="\u231B Loading...",e.disabled=!0;try{const p=document.getElementById("caddyfile-path-input").value||DC.DEFAULTS.CADDYFILE;await window.loadExistingCAs(p),e.textContent="\u2705 Refreshed"}catch(p){e.textContent="\u274C Failed",console.error("Failed to refresh CAs:",p)}setTimeout(()=>{e.textContent=s,e.disabled=!1},2e3)}),document.getElementById("create-dns-record")?.addEventListener("change",e=>{const s=document.getElementById("dns-config");s.style.display=e.target.checked?"block":"none"}),["service-subdomain-input","service-ip-input","service-port-input","ca-name-input","existing-ca-select","enable-auth","enable-cors","custom-headers-input","upstream-path-input","health-check-input","timeout-input"].forEach(e=>{const s=document.getElementById(e);s&&(s.addEventListener("input",u),s.addEventListener("change",u))});function g(){const e=safeGet("custom-services");if(e)try{JSON.parse(e).forEach(p=>{window.APPS.find(t=>t.id===p.id)||window.APPS.push(p)})}catch(s){console.warn("Failed to load custom services:",s)}}g(),window.openAddServiceModal=h,window.closeAddServiceModal=n})(),(function(){let o=null,f=1e3;const u=3e4;function y(){if(o)try{o.close()}catch{}o=new EventSource("/api/v1/events/stream"),o.addEventListener("connected",()=>{f=1e3,debug("[SSE] Connected to event stream")}),o.addEventListener("status-change",l=>{try{const r=JSON.parse(l.data);if(r.serviceId&&typeof window.setBadge=="function"){const h=r.status==="up"||r.status==="healthy";window.setBadge(r.serviceId,h,r.responseTime||null)}}catch{}}),o.addEventListener("resource-alert",l=>{try{const r=JSON.parse(l.data),h=`${r.containerName||r.containerId}: ${r.metric} at ${r.value}% (threshold: ${r.threshold}%)`;typeof showNotification=="function"&&showNotification(h,"warning")}catch{}}),o.addEventListener("auto-restart",l=>{try{const r=JSON.parse(l.data);typeof showNotification=="function"&&showNotification(`Container "${r.containerName}" was auto-restarted`,"info")}catch{}}),o.addEventListener("update-available",l=>{try{const r=JSON.parse(l.data),h=document.getElementById("updates-btn");if(h&&!h.querySelector(".sse-dot")){const a=document.createElement("span");a.className="sse-dot",a.style.cssText="display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--accent);margin-left:6px;vertical-align:middle;",h.appendChild(a)}typeof showNotification=="function"&&showNotification(`Update available for ${r.containerName||r.containerId}`,"info")}catch{}}),o.addEventListener("update-complete",l=>{try{const r=JSON.parse(l.data);typeof showNotification=="function"&&showNotification(`Update completed: ${r.containerName||r.containerId}`,"success"),typeof window.refreshAll=="function"&&window.refreshAll()}catch{}}),o.addEventListener("update-failed",l=>{try{const r=JSON.parse(l.data);typeof showNotification=="function"&&showNotification(`Update failed: ${r.containerName||r.containerId} \u2014 ${r.error||"unknown error"}`,"error")}catch{}}),o.addEventListener("incident",l=>{try{const r=JSON.parse(l.data);typeof showNotification=="function"&&(r.type==="created"?showNotification(`Incident: ${r.message||r.serviceId}`,"error"):r.type==="resolved"&&showNotification(`Resolved: ${r.serviceId||"incident"}`,"success"))}catch{}}),o.onerror=()=>{o.close(),console.warn(`[SSE] Disconnected, reconnecting in ${f/1e3}s...`),setTimeout(y,f),f=Math.min(f*2,u)}}y(),window._sseReconnect=y})(),(function(){const o=document.getElementById("service-filter-search"),f=document.getElementById("service-filter-status"),u=document.getElementById("service-filter-category"),y=document.getElementById("service-filter-count");function l(){const b=new Set,m=new Set;document.querySelectorAll("#cards .card[data-category]").forEach(g=>{const e=g.dataset.category.trim();e&&m.add(e)});const n=window.DC_CATEGORIES||typeof DC<"u"&&DC.CATEGORIES||{};return Object.keys(n).concat([...m].filter(g=>!n[g])).forEach(g=>b.add(g)),{list:[...b],apiCats:n}}function r(){if(!u)return;const{list:b,apiCats:m}=l(),n=u.value;u.innerHTML='',b.sort().forEach(d=>{const g=m[d],e=document.createElement("option");e.value=d,e.textContent=g?`${g.icon||""} ${d}`.trim():d,u.appendChild(e)}),n&&[...u.options].some(d=>d.value===n)?u.value=n:u.value="all"}function h(){r();const b=o.value.toLowerCase().trim(),m=f.value,n=u?u.value:"all",d=document.querySelectorAll("#cards .card");let g=0;if(d.forEach(e=>{const s=e.querySelector(".name")?.textContent?.toLowerCase()||"",p=e.dataset.app?.toLowerCase()||"",t=e.dataset.status||"off",c=e.dataset.category||"";(!b||s.includes(b)||p.includes(b))&&(m==="all"||t===m)&&(n==="all"||c===n)?(e.style.display="",g++):e.style.display="none"}),y){const e=d.length;y.textContent=`${g} of ${e} services`}}function a(b,m){let n;return function(...d){clearTimeout(n),n=setTimeout(()=>b.apply(this,d),m)}}o?.addEventListener("input",a(h,200)),f?.addEventListener("change",h),u?.addEventListener("change",h),document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>setTimeout(h,500)):setTimeout(h,500),window.refreshServiceFilter=h,window.refreshCategoryDropdown=r})(),(function(){const o=document.getElementById("batch-operations-btn"),f=document.getElementById("batch-action-bar"),u=document.getElementById("batch-selected-count"),y=document.getElementById("batch-start-btn"),l=document.getElementById("batch-stop-btn"),r=document.getElementById("batch-restart-btn"),h=document.getElementById("batch-cancel-btn");let a=!1,b=new Set;function m(){a=!0,b.clear(),f.style.display="",o.textContent="\u2713 Exit Batch Mode",d(),document.querySelectorAll("#cards .card[data-app]").forEach(s=>{const p=s.dataset.containerId;if(!p)return;const t=s.querySelector(".batch-checkbox");t&&t.remove();const c=document.createElement("input");c.type="checkbox",c.className="batch-checkbox",c.dataset.containerId=p,c.dataset.serviceName=s.querySelector(".name")?.textContent||p,c.style.cssText="position: absolute; top: 8px; left: 8px; z-index: 10; width: 18px; height: 18px; cursor: pointer;",c.addEventListener("change",v=>{v.stopPropagation(),c.checked?b.add(p):b.delete(p),d()}),s.style.position="relative",s.insertBefore(c,s.firstChild)})}function n(){a=!1,b.clear(),f.style.display="none",o.textContent="\u2630 Batch Operations",document.querySelectorAll(".batch-checkbox").forEach(e=>e.remove())}function d(){const e=b.size;u.textContent=`${e} selected`,y.disabled=e===0,l.disabled=e===0,r.disabled=e===0}async function g(e){if(b.size===0)return;const s=Array.from(b),p={start:"Starting",stop:"Stopping",restart:"Restarting"}[e];if(!confirm(`${p} ${s.length} container(s)? This cannot be undone.`))return;const t=[y,l,r];t.forEach(E=>{E.disabled=!0,E.textContent="..."});let c=0,v=0;const i=[];for(const E of s)try{const T=await fetch(`/api/v1/containers/${encodeURIComponent(E)}/${e}`,{method:"POST"});if(T.ok)c++;else{v++;const L=await T.json().catch(()=>({}));i.push(`${E}: ${L.error||T.statusText}`)}}catch(T){v++,i.push(`${E}: ${T.message}`)}t[0].textContent="\u25B6 Start All",t[1].textContent="\u2B1B Stop All",t[2].textContent="\u{1F504} Restart All",d(),v===0?typeof showNotification=="function"&&showNotification(`${p} completed: ${c} container(s)`,"success"):(typeof showNotification=="function"&&showNotification(`${p}: ${c} succeeded, ${v} failed`,"warning"),console.error("Batch operation errors:",i)),setTimeout(()=>{typeof refreshAll=="function"&&refreshAll()},1500)}o?.addEventListener("click",()=>{a?n():m()}),y?.addEventListener("click",()=>g("start")),l?.addEventListener("click",()=>g("stop")),r?.addEventListener("click",()=>g("restart")),h?.addEventListener("click",n)})(); diff --git a/status/dist/features.js b/status/dist/features.js index b7728b9..86dc01e 100644 --- a/status/dist/features.js +++ b/status/dist/features.js @@ -90,44 +90,44 @@ - `);const b=document.getElementById("logo-modal"),E=document.getElementById("logo-preview-dark"),N=document.getElementById("logo-preview-light"),S=document.getElementById("logo-status"),T=document.getElementById("logo-same-both"),P=document.getElementById("logo-dual-uploads"),L=document.getElementById("logo-single-upload"),H=document.getElementById("logo-upload-dark"),g=document.getElementById("logo-upload-light"),I=document.getElementById("logo-upload-single"),k=document.querySelector("#brand .brand-logo-dark"),x=document.querySelector("#brand .brand-logo-light"),$=document.querySelector(".top-row"),C=document.getElementById("dashboard-title"),R=DC.NAME;let M=null,j=null,B=null,A="left",w=R;T?.addEventListener("change",()=>{T.checked?(P.style.display="none",L.style.display="",M=null,j=null):(P.style.display="flex",L.style.display="none",B=null)});function z(a,e){if(!a||!a.type.startsWith("image/")){showNotification("Please select an image file","warning");return}const n=new FileReader;n.onload=t=>e(t.target.result),n.readAsDataURL(a)}H?.addEventListener("change",a=>{z(a.target.files[0],e=>{M=e,E.src=e,S.textContent="New dark logo ready to save"})}),g?.addEventListener("change",a=>{z(a.target.files[0],e=>{j=e,N.src=e,S.textContent="New light logo ready to save"})}),I?.addEventListener("change",a=>{z(a.target.files[0],e=>{B=e,E.src=e,N.src=e,S.textContent="New logo ready to save (both themes)"})});function f(a){$.setAttribute("data-logo-pos",a),document.querySelectorAll(".logo-pos-btn").forEach(e=>{e.style.background=e.dataset.pos===a?"var(--accent)":"var(--card-bg)",e.style.color=e.dataset.pos===a?"white":"var(--fg)"})}function p(a){w=a||R,document.title=w;const e=document.querySelector(".dashboard-title");e&&(e.textContent=w)}async function y(){try{const a=await fetch("/api/v1/logo");if(a.ok){const e=await a.json();e.customLogoDark&&(k.src=e.customLogoDark,E.src=e.customLogoDark),e.customLogoLight&&(x.src=e.customLogoLight,N.src=e.customLogoLight),!e.customLogoDark&&!e.customLogoLight&&e.customLogo&&(k.src=e.customLogo,x.src=e.customLogo,E.src=e.customLogo,N.src=e.customLogo),e.isDefault||(S.textContent="Using custom logo"),e.position&&(A=e.position,f(e.position)),e.dashboardTitle&&p(e.dashboardTitle)}}catch(a){console.warn("Could not load custom logo:",a.message)}}document.querySelectorAll(".logo-pos-btn").forEach(a=>{a.addEventListener("click",()=>{A=a.dataset.pos,f(A)})}),document.getElementById("brand")?.addEventListener("click",()=>{M=null,j=null,B=null,H&&(H.value=""),g&&(g.value=""),I&&(I.value=""),T&&(T.checked=!1),P.style.display="flex",L.style.display="none",E.src=k.src,N.src=x.src;const a=k.src.includes("custom-logo")||x.src.includes("custom-logo");S.textContent=a?"Using custom logo":"Using default logos",f(A),C.value=w,b.classList.add("show")}),document.getElementById("logo-save")?.addEventListener("click",async()=>{try{const a=C.value.trim()||R,e={position:A,dashboardTitle:a};T?.checked&&B?(e.dataDark=B,e.dataLight=B):(M&&(e.dataDark=M),j&&(e.dataLight=j));const n=await secureFetch("/api/v1/logo",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)});if(n.ok){const t=await n.json(),i="?t="+Date.now();t.pathDark&&(k.src=t.pathDark+i,E.src=t.pathDark+i),t.pathLight&&(x.src=t.pathLight+i,N.src=t.pathLight+i),f(A),p(a),b.classList.remove("show")}else{const t=await n.json();showNotification("Failed to save: "+t.error,"error")}}catch(a){showNotification("Error saving: "+a.message,"error")}}),document.getElementById("logo-reset")?.addEventListener("click",async()=>{if(confirm(`Reset all branding to DashCaddy defaults? + `);const h=document.getElementById("logo-modal"),E=document.getElementById("logo-preview-dark"),P=document.getElementById("logo-preview-light"),w=document.getElementById("logo-status"),N=document.getElementById("logo-same-both"),O=document.getElementById("logo-dual-uploads"),z=document.getElementById("logo-single-upload"),A=document.getElementById("logo-upload-dark"),v=document.getElementById("logo-upload-light"),L=document.getElementById("logo-upload-single"),b=document.querySelector("#brand .brand-logo-dark"),M=document.querySelector("#brand .brand-logo-light"),k=document.querySelector(".top-row"),B=document.getElementById("dashboard-title"),S=DC.NAME;let T=null,j=null,H=null,R="left",x=S;N?.addEventListener("change",()=>{N.checked?(O.style.display="none",z.style.display="",T=null,j=null):(O.style.display="flex",z.style.display="none",H=null)});function D(n,e){if(!n||!n.type.startsWith("image/")){showNotification("Please select an image file","warning");return}const o=new FileReader;o.onload=a=>e(a.target.result),o.readAsDataURL(n)}A?.addEventListener("change",n=>{D(n.target.files[0],e=>{T=e,E.src=e,w.textContent="New dark logo ready to save"})}),v?.addEventListener("change",n=>{D(n.target.files[0],e=>{j=e,P.src=e,w.textContent="New light logo ready to save"})}),L?.addEventListener("change",n=>{D(n.target.files[0],e=>{H=e,E.src=e,P.src=e,w.textContent="New logo ready to save (both themes)"})});function g(n){k.setAttribute("data-logo-pos",n),document.querySelectorAll(".logo-pos-btn").forEach(e=>{e.style.background=e.dataset.pos===n?"var(--accent)":"var(--card-bg)",e.style.color=e.dataset.pos===n?"white":"var(--fg)"})}function u(n){x=n||S,document.title=x;const e=document.querySelector(".dashboard-title");e&&(e.textContent=x)}async function f(){try{const n=await fetch("/api/v1/logo");if(n.ok){const e=await n.json();e.customLogoDark&&(b.src=e.customLogoDark,E.src=e.customLogoDark),e.customLogoLight&&(M.src=e.customLogoLight,P.src=e.customLogoLight),!e.customLogoDark&&!e.customLogoLight&&e.customLogo&&(b.src=e.customLogo,M.src=e.customLogo,E.src=e.customLogo,P.src=e.customLogo),e.isDefault||(w.textContent="Using custom logo"),e.position&&(R=e.position,g(e.position)),e.dashboardTitle&&u(e.dashboardTitle)}}catch(n){console.warn("Could not load custom logo:",n.message)}}document.querySelectorAll(".logo-pos-btn").forEach(n=>{n.addEventListener("click",()=>{R=n.dataset.pos,g(R)})}),document.getElementById("brand")?.addEventListener("click",()=>{T=null,j=null,H=null,A&&(A.value=""),v&&(v.value=""),L&&(L.value=""),N&&(N.checked=!1),O.style.display="flex",z.style.display="none",E.src=b.src,P.src=M.src;const n=b.src.includes("custom-logo")||M.src.includes("custom-logo");w.textContent=n?"Using custom logo":"Using default logos",g(R),B.value=x,h.classList.add("show")}),document.getElementById("logo-save")?.addEventListener("click",async()=>{try{const n=B.value.trim()||S,e={position:R,dashboardTitle:n};N?.checked&&H?(e.dataDark=H,e.dataLight=H):(T&&(e.dataDark=T),j&&(e.dataLight=j));const o=await secureFetch("/api/v1/logo",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)});if(o.ok){const a=await o.json(),r="?t="+Date.now();a.pathDark&&(b.src=a.pathDark+r,E.src=a.pathDark+r),a.pathLight&&(M.src=a.pathLight+r,P.src=a.pathLight+r),g(R),u(n),h.classList.remove("show")}else{const a=await o.json();showNotification("Failed to save: "+a.error,"error")}}catch(n){showNotification("Error saving: "+n.message,"error")}}),document.getElementById("logo-reset")?.addEventListener("click",async()=>{if(confirm(`Reset all branding to DashCaddy defaults? -This will reset the logo, favicon, title, and position.`))try{if((await secureFetch("/api/v1/logo",{method:"DELETE"})).ok&&(k.src="/assets/dashcaddy-logo-dark.png",x.src="/assets/dashcaddy-logo-light.png",E.src="/assets/dashcaddy-logo-dark.png",N.src="/assets/dashcaddy-logo-light.png",S.textContent="Using default logos",M=null,j=null,B=null,C.value=R,p(R),A="left",f("left")),(await secureFetch("/api/v1/favicon",{method:"DELETE"})).ok){const n=document.querySelector('link[rel="icon"]'),t=document.getElementById("favicon-preview"),i=document.getElementById("favicon-status");n&&(n.href="/assets/dashcaddy-favicon.ico?t="+Date.now()),t&&(t.src="/assets/dashcaddy-favicon.ico?t="+Date.now()),i&&(i.textContent="Using DashCaddy favicon"),s=null}}catch(a){showNotification("Error resetting branding: "+a.message,"error")}}),wireModal(b,document.getElementById("logo-cancel"));const v=document.getElementById("favicon-preview"),m=document.getElementById("favicon-status"),r=document.getElementById("favicon-upload"),c=document.querySelector('link[rel="icon"]')||document.createElement("link");let s=null;document.querySelector('link[rel="icon"]')||(c.rel="icon",c.href="/assets/dashcaddy-favicon.ico",document.head.appendChild(c));async function h(){try{const a=await fetch("/api/v1/favicon");if(a.ok){const e=await a.json();e.customFavicon&&(c.href=e.customFavicon+"?t="+Date.now(),v.src=e.customFavicon+"?t="+Date.now(),m.textContent="Using custom favicon")}}catch(a){console.warn("Could not load custom favicon:",a.message)}}r?.addEventListener("change",a=>{const e=a.target.files[0];if(!e)return;if(!e.type.match(/^image\/(png|svg\+xml)$/)){showNotification("Please select a PNG or SVG file","warning"),r.value="";return}const n=new FileReader;n.onload=t=>{s=t.target.result,v.src=s,m.textContent="New favicon ready to save"},n.readAsDataURL(e)}),document.getElementById("logo-save")?.addEventListener("click",async()=>{if(s)try{const a=await secureFetch("/api/v1/favicon",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({data:s})});if(a.ok){const e=await a.json();c.href=e.path+"?t="+Date.now(),v.src=e.path+"?t="+Date.now(),m.textContent="Using custom favicon",s=null}else{const e=await a.json();showNotification("Failed to save favicon: "+e.error,"error")}}catch(a){showNotification("Error saving favicon: "+a.message,"error")}}),h(),y();const u=document.getElementById("settings-timezone");u&&(new MutationObserver(()=>{b.classList.contains("show")&&u.options.length===0&&(async()=>{let e;try{const n=await fetch("/api/v1/config");n.ok&&(e=(await n.json()).timezone)}catch{}window.populateTimezoneSelect(u,e)})()}).observe(b,{attributes:!0,attributeFilter:["class"]}),document.getElementById("logo-save")?.addEventListener("click",async()=>{const e=u.value;if(e)try{const n=await fetch("/api/v1/config");if(!n.ok)return;const t=await n.json();t.timezone=e,t.updatedAt=new Date().toISOString(),await secureFetch("/api/v1/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})}catch(n){console.warn("Failed to save timezone:",n.message)}}))})(),window.populateTimezoneSelect=function(b,E){const N=Intl.supportedValuesOf("timeZone"),S=E||Intl.DateTimeFormat().resolvedOptions().timeZone||"UTC";b.innerHTML="";for(const T of N){const P=document.createElement("option");P.value=T,P.textContent=T.replace(/_/g," "),T===S&&(P.selected=!0),b.appendChild(P)}},(function(){let b="homelab",E=null;async function N(){try{const z=await fetch("/api/v1/config");if(z.ok&&(E=await z.json(),E&&E.setupComplete))return document.getElementById("setup-wizard").style.display="none",!0}catch(z){console.warn("Could not fetch server config, checking localStorage fallback:",z.message)}return safeGet("dashcaddy-setup")?(document.getElementById("setup-wizard").style.display="none",!0):(document.getElementById("setup-wizard").style.display="flex",!1)}N();const S=document.getElementById("setup-timezone");S&&window.populateTimezoneSelect(S);function T(w){document.querySelectorAll(".setup-step").forEach(f=>{f.style.display="none"});const z=document.getElementById(w);z&&(z.style.display="block")}function P(){const w=document.getElementById("setup-summary-content");if(!w)return;let z='
';if(b==="homelab"){const p=document.getElementById("setup-tld")?.value?.trim()||".home",y=document.getElementById("setup-ca-name")?.value?.trim()||"",v=document.getElementById("setup-dns-ip")?.value?.trim()||"",m=document.getElementById("setup-dns-port")?.value?.trim()||DC.DEFAULTS.DNS_PORT;z+=` +This will reset the logo, favicon, title, and position.`))try{if((await secureFetch("/api/v1/logo",{method:"DELETE"})).ok&&(b.src="/assets/dashcaddy-logo-dark.png",M.src="/assets/dashcaddy-logo-light.png",E.src="/assets/dashcaddy-logo-dark.png",P.src="/assets/dashcaddy-logo-light.png",w.textContent="Using default logos",T=null,j=null,H=null,B.value=S,u(S),R="left",g("left")),(await secureFetch("/api/v1/favicon",{method:"DELETE"})).ok){const o=document.querySelector('link[rel="icon"]'),a=document.getElementById("favicon-preview"),r=document.getElementById("favicon-status");o&&(o.href="/assets/dashcaddy-favicon.ico?t="+Date.now()),a&&(a.src="/assets/dashcaddy-favicon.ico?t="+Date.now()),r&&(r.textContent="Using DashCaddy favicon"),i=null}}catch(n){showNotification("Error resetting branding: "+n.message,"error")}}),wireModal(h,document.getElementById("logo-cancel"));const m=document.getElementById("favicon-preview"),p=document.getElementById("favicon-status"),d=document.getElementById("favicon-upload"),c=document.querySelector('link[rel="icon"]')||document.createElement("link");let i=null;document.querySelector('link[rel="icon"]')||(c.rel="icon",c.href="/assets/dashcaddy-favicon.ico",document.head.appendChild(c));async function $(){try{const n=await fetch("/api/v1/favicon");if(n.ok){const e=await n.json();e.customFavicon&&(c.href=e.customFavicon+"?t="+Date.now(),m.src=e.customFavicon+"?t="+Date.now(),p.textContent="Using custom favicon")}}catch(n){console.warn("Could not load custom favicon:",n.message)}}d?.addEventListener("change",n=>{const e=n.target.files[0];if(!e)return;if(!e.type.match(/^image\/(png|svg\+xml)$/)){showNotification("Please select a PNG or SVG file","warning"),d.value="";return}const o=new FileReader;o.onload=a=>{i=a.target.result,m.src=i,p.textContent="New favicon ready to save"},o.readAsDataURL(e)}),document.getElementById("logo-save")?.addEventListener("click",async()=>{if(i)try{const n=await secureFetch("/api/v1/favicon",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({data:i})});if(n.ok){const e=await n.json();c.href=e.path+"?t="+Date.now(),m.src=e.path+"?t="+Date.now(),p.textContent="Using custom favicon",i=null}else{const e=await n.json();showNotification("Failed to save favicon: "+e.error,"error")}}catch(n){showNotification("Error saving favicon: "+n.message,"error")}}),$(),f();const y=document.getElementById("settings-timezone");y&&(new MutationObserver(()=>{h.classList.contains("show")&&y.options.length===0&&(async()=>{let e;try{const o=await fetch("/api/v1/config");o.ok&&(e=(await o.json()).timezone)}catch{}window.populateTimezoneSelect(y,e)})()}).observe(h,{attributes:!0,attributeFilter:["class"]}),document.getElementById("logo-save")?.addEventListener("click",async()=>{const e=y.value;if(e)try{const o=await fetch("/api/v1/config");if(!o.ok)return;const a=await o.json();a.timezone=e,a.updatedAt=new Date().toISOString(),await secureFetch("/api/v1/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(a)})}catch(o){console.warn("Failed to save timezone:",o.message)}}))})(),window.populateTimezoneSelect=function(h,E){const P=Intl.supportedValuesOf("timeZone"),w=E||Intl.DateTimeFormat().resolvedOptions().timeZone||"UTC";h.innerHTML="";for(const N of P){const O=document.createElement("option");O.value=N,O.textContent=N.replace(/_/g," "),N===w&&(O.selected=!0),h.appendChild(O)}},(function(){let h="homelab",E=null;async function P(){try{const D=await fetch("/api/v1/config");if(D.ok&&(E=await D.json(),E&&E.setupComplete))return document.getElementById("setup-wizard").style.display="none",!0}catch(D){console.warn("Could not fetch server config, checking localStorage fallback:",D.message)}return safeGet("dashcaddy-setup")?(document.getElementById("setup-wizard").style.display="none",!0):(document.getElementById("setup-wizard").style.display="flex",!1)}P();const w=document.getElementById("setup-timezone");w&&window.populateTimezoneSelect(w);function N(x){document.querySelectorAll(".setup-step").forEach(g=>{g.style.display="none"});const D=document.getElementById(x);D&&(D.style.display="block")}function O(){const x=document.getElementById("setup-summary-content");if(!x)return;let D='
';if(h==="homelab"){const u=document.getElementById("setup-tld")?.value?.trim()||".home",f=document.getElementById("setup-ca-name")?.value?.trim()||"",m=document.getElementById("setup-dns-ip")?.value?.trim()||"",p=document.getElementById("setup-dns-port")?.value?.trim()||DC.DEFAULTS.DNS_PORT;D+=`

Home Lab Configuration

-
TLD: ${p}
-
Certificate Authority: ${y}
-
DNS Server: ${v}:${m}
-
Example URLs: https://uptime${p}, https://nextcloud${p}
+
TLD: ${u}
+
Certificate Authority: ${f}
+
DNS Server: ${m}:${p}
+
Example URLs: https://uptime${u}, https://nextcloud${u}
- `}else if(b==="simple"){const p=document.getElementById("setup-simple-ip")?.value?.trim()||"localhost";z+=` + `}else if(h==="simple"){const u=document.getElementById("setup-simple-ip")?.value?.trim()||"localhost";D+=`

Simple Setup

Access Method: IP:Port only
-
Default IP: ${p}
+
Default IP: ${u}
SSL: None (HTTP only)
-
Example URLs: http://${p}:8080, http://${p}:3000
+
Example URLs: http://${u}:8080, http://${u}:3000
- `}else if(b==="public"){const p=document.getElementById("setup-public-domain")?.value?.trim()||"",y=document.getElementById("setup-public-email")?.value?.trim()||"",v=document.querySelector('input[name="routing-mode"]:checked')?.value||"subdirectory",m=v==="subdirectory"?`https://${p}/sonarr, https://${p}/grafana`:`https://sonarr.${p}, https://grafana.${p}`;z+=` + `}else if(h==="public"){const u=document.getElementById("setup-public-domain")?.value?.trim()||"",f=document.getElementById("setup-public-email")?.value?.trim()||"",m=document.querySelector('input[name="routing-mode"]:checked')?.value||"subdirectory",p=m==="subdirectory"?`https://${u}/sonarr, https://${u}/grafana`:`https://sonarr.${u}, https://grafana.${u}`;D+=`

Public Server

-
Domain: ${p}
+
Domain: ${u}
SSL: Let's Encrypt
-
Email: ${y}
-
Routing: ${v==="subdirectory"?"Subdirectory (domain.com/app)":"Subdomain (app.domain.com)"}
-
Example URLs: ${m}
+
Email: ${f}
+
Routing: ${m==="subdirectory"?"Subdirectory (domain.com/app)":"Subdomain (app.domain.com)"}
+
Example URLs: ${p}
- `}const f=document.getElementById("setup-timezone")?.value||Intl.DateTimeFormat().resolvedOptions().timeZone||"UTC";z+=` + `}const g=document.getElementById("setup-timezone")?.value||Intl.DateTimeFormat().resolvedOptions().timeZone||"UTC";D+=`
-
Timezone: ${f.replace(/_/g," ")}
+
Timezone: ${g.replace(/_/g," ")}
- `,z+="
",w.innerHTML=z,T("setup-step-summary")}async function L(w){try{const z=await secureFetch("/api/v1/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(w)});return z.ok?(await z.json(),!0):(errorHandler.logError("[SetupWizard] Save Config",new Error(`Server returned ${z.status}`),{function:"saveConfigToServer"}),!1)}catch(z){return errorHandler.logError("[SetupWizard] Save Config",z,{function:"saveConfigToServer"}),!1}}async function H(){const w={setupComplete:!0,configurationType:b,timestamp:new Date().toISOString(),timezone:document.getElementById("setup-timezone")?.value||Intl.DateTimeFormat().resolvedOptions().timeZone||"UTC"};b==="homelab"?(w.tld=document.getElementById("setup-tld")?.value?.trim()||".home",w.caName=document.getElementById("setup-ca-name")?.value?.trim()||"",w.dns={provider:"technitium",ip:document.getElementById("setup-dns-ip")?.value?.trim()||"",port:document.getElementById("setup-dns-port")?.value?.trim()||DC.DEFAULTS.DNS_PORT,token:document.getElementById("setup-dns-token")?.value?.trim()||""},w.defaults={dnsType:"private",sslType:"internal",targetIP:"localhost"}):b==="simple"?(w.defaultIP=document.getElementById("setup-simple-ip")?.value?.trim()||"localhost",w.defaults={dnsType:"none",sslType:"none",targetIP:w.defaultIP}):b==="public"&&(w.domain=document.getElementById("setup-public-domain")?.value?.trim()||"",w.email=document.getElementById("setup-public-email")?.value?.trim()||"",w.routingMode=document.querySelector('input[name="routing-mode"]:checked')?.value||"subdirectory",w.defaults={dnsType:w.routingMode==="subdirectory"?"none":"public",sslType:"letsencrypt",targetIP:"localhost"});const z=await L(w);safeSet("dashcaddy-config",JSON.stringify(w)),safeSet("dashcaddy-setup","completed"),document.getElementById("setup-wizard").style.display="none";const f=b==="homelab"?"Professional Home Lab":b==="simple"?"Simple Setup":"Public Server",p=z?"server (shared across all devices)":"locally (this browser only)";showNotification(`Setup Complete! Configured for: ${f}. Settings saved to: ${p}`,"success",5e3),setTimeout(()=>location.reload(),500)}const g=document.getElementById("setup-step-1-next");g&&(g.onclick=function(w){w.preventDefault();const z=document.querySelector('input[name="config-type"]:checked');z&&(b=z.value),T(b==="homelab"?"setup-step-homelab":b==="simple"?"setup-step-simple":b==="public"?"setup-step-public":"setup-step-homelab")});const I=document.getElementById("setup-skip");I&&(I.onclick=async function(w){w.preventDefault(),confirm("Skip setup? You can run it later from Settings.")&&(await L({setupComplete:!0,skipped:!0,timestamp:new Date().toISOString()}),safeSet("dashcaddy-setup","skipped"),document.getElementById("setup-wizard").style.display="none")});const k=document.getElementById("setup-tld");k&&(k.oninput=function(w){const z=w.target.value||".home",f=document.getElementById("tld-preview"),p=document.getElementById("tld-preview-2");f&&(f.textContent=z),p&&(p.textContent=z)});const x=document.getElementById("setup-homelab-back");x&&(x.onclick=function(w){w.preventDefault(),T("setup-step-1")});const $=document.getElementById("setup-homelab-next");$&&($.onclick=function(w){w.preventDefault();const z=document.getElementById("setup-tld")?.value?.trim()||"",f=document.getElementById("setup-ca-name")?.value?.trim()||"",p=document.getElementById("setup-dns-ip")?.value?.trim()||"";if(!z||!z.startsWith(".")){showNotification("Please enter a valid TLD starting with a dot (e.g., .home)","warning");return}if(!f){showNotification("Please enter a Certificate Authority name","warning");return}if(!p){showNotification("Please enter your DNS server IP address","warning");return}P()});const C=document.getElementById("setup-simple-back");C&&(C.onclick=function(w){w.preventDefault(),T("setup-step-1")});const R=document.getElementById("setup-simple-next");R&&(R.onclick=function(w){w.preventDefault(),P()}),document.querySelectorAll('input[name="routing-mode"]').forEach(function(w){w.onchange=function(){var z=document.getElementById("dns-requirement-note");z&&(z.textContent=this.value==="subdirectory"?"Only one DNS record needed (for the main domain)":"You'll need to configure DNS manually for each subdomain")}});const M=document.getElementById("setup-public-back");M&&(M.onclick=function(w){w.preventDefault(),T("setup-step-1")});const j=document.getElementById("setup-public-next");j&&(j.onclick=function(w){w.preventDefault();const z=document.getElementById("setup-public-domain")?.value?.trim()||"",f=document.getElementById("setup-public-email")?.value?.trim()||"";if(!z){showNotification("Please enter your domain name","warning");return}if(!f||!f.includes("@")){showNotification("Please enter a valid email address","warning");return}P()});const B=document.getElementById("setup-summary-back");B&&(B.onclick=function(w){w.preventDefault(),b==="homelab"?T("setup-step-homelab"):b==="simple"?T("setup-step-simple"):b==="public"&&T("setup-step-public")});const A=document.getElementById("setup-finish");A&&(A.onclick=function(w){w.preventDefault(),H()}),window.getGlobalConfig=async function(){try{const z=await fetch("/api/v1/config");if(z.ok){const f=await z.json();if(f&&f.setupComplete)return f}}catch{console.warn("Could not fetch config from server")}const w=safeGet("dashcaddy-config");return w?JSON.parse(w):{setupComplete:!1,configurationType:"homelab",tld:".home",caName:"",defaults:{dnsType:"private",sslType:"internal",targetIP:"localhost"}}},window.resetSetupWizard=async function(){if(confirm("Reset DashCaddy configuration? This will show the setup wizard again.")){try{await secureFetch("/api/v1/config",{method:"DELETE"})}catch{console.warn("Could not delete server config")}safeRemove("dashcaddy-setup"),safeRemove("dashcaddy-config"),location.reload()}}})(),(function(){const b=new ErrorHandler;injectModal("app-selector-modal",`
+ `,D+="
",x.innerHTML=D,N("setup-step-summary")}async function z(x){try{const D=await secureFetch("/api/v1/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(x)});return D.ok?(await D.json(),!0):(errorHandler.logError("[SetupWizard] Save Config",new Error(`Server returned ${D.status}`),{function:"saveConfigToServer"}),!1)}catch(D){return errorHandler.logError("[SetupWizard] Save Config",D,{function:"saveConfigToServer"}),!1}}async function A(){const x={setupComplete:!0,configurationType:h,timestamp:new Date().toISOString(),timezone:document.getElementById("setup-timezone")?.value||Intl.DateTimeFormat().resolvedOptions().timeZone||"UTC"};if(h==="homelab"){x.tld=document.getElementById("setup-tld")?.value?.trim()||".home",x.caName=document.getElementById("setup-ca-name")?.value?.trim()||"";const f=document.getElementById("setup-dns-provider")?.value||"technitium";x.dns={provider:f,ip:document.getElementById("setup-dns-ip")?.value?.trim()||"",port:document.getElementById("setup-dns-port")?.value?.trim()||DC.DEFAULTS.DNS_PORT,token:document.getElementById("setup-dns-token")?.value?.trim()||""},x.defaults={dnsType:"private",sslType:"internal",targetIP:"localhost"}}else h==="simple"?(x.defaultIP=document.getElementById("setup-simple-ip")?.value?.trim()||"localhost",x.defaults={dnsType:"none",sslType:"none",targetIP:x.defaultIP}):h==="public"&&(x.domain=document.getElementById("setup-public-domain")?.value?.trim()||"",x.email=document.getElementById("setup-public-email")?.value?.trim()||"",x.routingMode=document.querySelector('input[name="routing-mode"]:checked')?.value||"subdirectory",x.defaults={dnsType:x.routingMode==="subdirectory"?"none":"public",sslType:"letsencrypt",targetIP:"localhost"});const D=await z(x);safeSet("dashcaddy-config",JSON.stringify(x)),safeSet("dashcaddy-setup","completed"),document.getElementById("setup-wizard").style.display="none";const g=h==="homelab"?"Professional Home Lab":h==="simple"?"Simple Setup":"Public Server",u=D?"server (shared across all devices)":"locally (this browser only)";showNotification(`Setup Complete! Configured for: ${g}. Settings saved to: ${u}`,"success",5e3),setTimeout(()=>location.reload(),500)}const v=document.getElementById("setup-step-1-next");v&&(v.onclick=function(x){x.preventDefault();const D=document.querySelector('input[name="config-type"]:checked');D&&(h=D.value),N(h==="homelab"?"setup-step-homelab":h==="simple"?"setup-step-simple":h==="public"?"setup-step-public":"setup-step-homelab")});const L=document.getElementById("setup-skip");L&&(L.onclick=async function(x){x.preventDefault(),confirm("Skip setup? You can run it later from Settings.")&&(await z({setupComplete:!0,skipped:!0,timestamp:new Date().toISOString()}),safeSet("dashcaddy-setup","skipped"),document.getElementById("setup-wizard").style.display="none")});const b=document.getElementById("setup-tld");b&&(b.oninput=function(x){const D=x.target.value||".home",g=document.getElementById("tld-preview"),u=document.getElementById("tld-preview-2");g&&(g.textContent=D),u&&(u.textContent=D)});const M=document.getElementById("setup-homelab-back");M&&(M.onclick=function(x){x.preventDefault(),N("setup-step-1")});const k=document.getElementById("setup-homelab-next");k&&(k.onclick=function(x){x.preventDefault();const D=document.getElementById("setup-tld")?.value?.trim()||"",g=document.getElementById("setup-ca-name")?.value?.trim()||"",u=document.getElementById("setup-dns-ip")?.value?.trim()||"";if(!D||!D.startsWith(".")){showNotification("Please enter a valid TLD starting with a dot (e.g., .home)","warning");return}if(!g){showNotification("Please enter a Certificate Authority name","warning");return}if(!u){showNotification("Please enter your DNS server IP address","warning");return}O()});const B=document.getElementById("setup-simple-back");B&&(B.onclick=function(x){x.preventDefault(),N("setup-step-1")});const S=document.getElementById("setup-simple-next");S&&(S.onclick=function(x){x.preventDefault(),O()}),document.querySelectorAll('input[name="routing-mode"]').forEach(function(x){x.onchange=function(){var D=document.getElementById("dns-requirement-note");D&&(D.textContent=this.value==="subdirectory"?"Only one DNS record needed (for the main domain)":"You'll need to configure DNS manually for each subdomain")}});const T=document.getElementById("setup-public-back");T&&(T.onclick=function(x){x.preventDefault(),N("setup-step-1")});const j=document.getElementById("setup-public-next");j&&(j.onclick=function(x){x.preventDefault();const D=document.getElementById("setup-public-domain")?.value?.trim()||"",g=document.getElementById("setup-public-email")?.value?.trim()||"";if(!D){showNotification("Please enter your domain name","warning");return}if(!g||!g.includes("@")){showNotification("Please enter a valid email address","warning");return}O()});const H=document.getElementById("setup-summary-back");H&&(H.onclick=function(x){x.preventDefault(),h==="homelab"?N("setup-step-homelab"):h==="simple"?N("setup-step-simple"):h==="public"&&N("setup-step-public")});const R=document.getElementById("setup-finish");R&&(R.onclick=function(x){x.preventDefault(),A()}),window.getGlobalConfig=async function(){try{const D=await fetch("/api/v1/config");if(D.ok){const g=await D.json();if(g&&g.setupComplete)return g}}catch{console.warn("Could not fetch config from server")}const x=safeGet("dashcaddy-config");return x?JSON.parse(x):{setupComplete:!1,configurationType:"homelab",tld:".home",caName:"",defaults:{dnsType:"private",sslType:"internal",targetIP:"localhost"}}},window.resetSetupWizard=async function(){if(confirm("Reset DashCaddy configuration? This will show the setup wizard again.")){try{await secureFetch("/api/v1/config",{method:"DELETE"})}catch{console.warn("Could not delete server config")}safeRemove("dashcaddy-setup"),safeRemove("dashcaddy-config"),location.reload()}}})(),(function(){const h=new ErrorHandler;injectModal("app-selector-modal",`

Choose an App

@@ -333,12 +333,12 @@ This will reset the logo, favicon, title, and position.`))try{if((await secureFe
-
`);const E="custom-apps";let N=null,S=null;const T=document.getElementById("app-selector-modal"),P=document.getElementById("app-selector-grid");async function L(){try{const c=await(await fetch("/api/v1/apps/templates")).json();if(c.success)return N=c.templates,S=c.categories,!0}catch(r){b.logError("[AppSelector] Fetch Templates",r,{function:"fetchApiTemplates"})}return!1}async function H(r){try{return await(await fetch(`/api/v1/apps/ports/${r}/check`)).json()}catch(c){return b.logError("[AppSelector] Check Port",c,{function:"checkPortAvailability"}),{available:!0}}}async function g(r){try{const s=await(await fetch(`/api/v1/apps/ports/${r}/suggest`)).json();if(s.success)return s.suggestedPort}catch(c){b.logError("[AppSelector] Get Suggested Port",c,{function:"getSuggestedPort"})}return r}async function I(){if(P.innerHTML='
Loading app templates...
',!N&&!await L()){P.innerHTML='
Failed to load app templates. Please try again.
';return}P.innerHTML="";const r={};for(const[s,h]of Object.entries(N)){const u=h.category||"Other";r[u]||(r[u]=[]),r[u].push({id:s,...h})}const c=S?Object.keys(S):Object.keys(r).sort();for(const s of c){const h=r[s];if(!h||h.length===0)continue;h.sort((e,n)=>(n.popularity||0)-(e.popularity||0));const u=document.createElement("div");u.className="app-category-header";const a=S?.[s]||{};u.innerHTML=`${escapeHtml(a.icon||"")} ${escapeHtml(s)}`,a.color&&(u.style.borderBottomColor=a.color),P.appendChild(u),h.forEach(e=>{const n=document.createElement("div");n.className="app-option";const t=e.isDashboardWidget,i=t&&safeGet("widget-"+e.id+"-enabled")!=="false",o=t?`
${i?"ON":"OFF"}
`:"",d=!t&&e.difficulty?`
${escapeHtml(e.difficulty)}
`:"";n.innerHTML=` + `);const E="custom-apps";let P=null,w=null;const N=document.getElementById("app-selector-modal"),O=document.getElementById("app-selector-grid");async function z(){try{const c=await(await fetch("/api/v1/apps/templates")).json();if(c.success)return P=c.templates,w=c.categories,!0}catch(d){h.logError("[AppSelector] Fetch Templates",d,{function:"fetchApiTemplates"})}return!1}async function A(d){try{return await(await fetch(`/api/v1/apps/ports/${d}/check`)).json()}catch(c){return h.logError("[AppSelector] Check Port",c,{function:"checkPortAvailability"}),{available:!0}}}async function v(d){try{const i=await(await fetch(`/api/v1/apps/ports/${d}/suggest`)).json();if(i.success)return i.suggestedPort}catch(c){h.logError("[AppSelector] Get Suggested Port",c,{function:"getSuggestedPort"})}return d}async function L(){if(O.innerHTML='
Loading app templates...
',!P&&!await z()){O.innerHTML='
Failed to load app templates. Please try again.
';return}O.innerHTML="";const d={};for(const[i,$]of Object.entries(P)){const y=$.category||"Other";d[y]||(d[y]=[]),d[y].push({id:i,...$})}const c=w?Object.keys(w):Object.keys(d).sort();for(const i of c){const $=d[i];if(!$||$.length===0)continue;$.sort((e,o)=>(o.popularity||0)-(e.popularity||0));const y=document.createElement("div");y.className="app-category-header";const n=w?.[i]||{};y.innerHTML=`${escapeHtml(n.icon||"")} ${escapeHtml(i)}`,n.color&&(y.style.borderBottomColor=n.color),O.appendChild(y),$.forEach(e=>{const o=document.createElement("div");o.className="app-option";const a=e.isDashboardWidget,r=a&&safeGet("widget-"+e.id+"-enabled")!=="false",t=a?`
${r?"ON":"OFF"}
`:"",s=!a&&e.difficulty?`
${escapeHtml(e.difficulty)}
`:"";o.innerHTML=`
${escapeHtml(e.icon||"\u{1F4E6}")}
${escapeHtml(e.name)}
${escapeHtml(e.description||"")}
- ${o}${d} - `,t?n.onclick=()=>k(e,n):n.onclick=()=>x(e),P.appendChild(n)})}window.renderRecipeCards&&await window.renderRecipeCards(P)}function k(r,c){const s="widget-"+r.id+"-enabled",u=!(safeGet(s)!=="false");safeSet(s,String(u));const a=r.widgetSelector;if(a){const n=document.querySelector(a);n&&(n.style.display=u?"":"none")}const e=c.querySelector('div[style*="border-radius: 4px"]');e&&(e.textContent=u?"ON":"OFF",e.style.background=u?"#2ecc7130":"#e74c3c30",e.style.color=u?"#2ecc71":"#e74c3c"),showNotification(`${r.name} widget ${u?"enabled":"disabled"}`,"success",2e3)}async function x(r){const c=document.getElementById("app-deploy-modal"),s=document.getElementById("app-deploy-title"),h=document.getElementById("deploy-subdomain"),u=document.getElementById("deploy-url-preview"),a=document.getElementById("deploy-ip"),e=document.getElementById("deploy-port"),n=document.getElementById("deploy-tailscale-only"),t=document.getElementById("tailscale-status");try{const J=await(await secureFetch("/api/v1/apps/check-existing",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({appId:r.id})})).json();if(J.success&&J.exists){const V=J.container;confirm(`Found existing ${r.name} container: + ${t}${s} + `,a?o.onclick=()=>b(e,o):o.onclick=()=>M(e),O.appendChild(o)})}window.renderRecipeCards&&await window.renderRecipeCards(O)}function b(d,c){const i="widget-"+d.id+"-enabled",y=!(safeGet(i)!=="false");safeSet(i,String(y));const n=d.widgetSelector;if(n){const o=document.querySelector(n);o&&(o.style.display=y?"":"none")}const e=c.querySelector('div[style*="border-radius: 4px"]');e&&(e.textContent=y?"ON":"OFF",e.style.background=y?"#2ecc7130":"#e74c3c30",e.style.color=y?"#2ecc71":"#e74c3c"),showNotification(`${d.name} widget ${y?"enabled":"disabled"}`,"success",2e3)}async function M(d){const c=document.getElementById("app-deploy-modal"),i=document.getElementById("app-deploy-title"),$=document.getElementById("deploy-subdomain"),y=document.getElementById("deploy-url-preview"),n=document.getElementById("deploy-ip"),e=document.getElementById("deploy-port"),o=document.getElementById("deploy-tailscale-only"),a=document.getElementById("tailscale-status");try{const W=await(await secureFetch("/api/v1/apps/check-existing",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({appId:d.id})})).json();if(W.success&&W.exists){const V=W.container;confirm(`Found existing ${d.name} container: Container: ${V.name} Status: ${V.status} @@ -347,38 +347,38 @@ Port: ${V.primaryPort||"N/A"} Would you like to use this existing container? Click OK to configure DNS/Caddy for the existing container. -Click Cancel to deploy a new container.`)&&(r._useExisting=!0,r._existingContainer=V)}}catch{}s.textContent=`Deploy ${r.name}`;const i=r.subdomain||r.id.replace(/-/g,"");h.value=i;const o=document.getElementById("subpath-compat-warning");if(o)if(SITE.routingMode==="subdirectory"){const _=r.subpathSupport||"strip";_==="none"?(o.style.display="block",o.innerHTML=''+r.name+" does not support subdirectory mode. It may not work correctly at a subpath."):_==="strip"?(o.style.display="block",o.innerHTML='ⓘ '+r.name+" has unverified subdirectory support. It may require additional configuration."):o.style.display="none"}else o.style.display="none";const d=SITE.defaults.dnsType||(SITE.configurationType==="public"?"public":"private"),l=SITE.defaults.sslType||(SITE.configurationType==="public"?"letsencrypt":"internal"),D=document.querySelector(`input[name="dns-type"][value="${d}"]`),O=document.querySelector(`input[name="ssl-type"][value="${l}"]`);D?D.checked=!0:document.querySelector('input[name="dns-type"][value="private"]').checked=!0,O?O.checked=!0:document.querySelector('input[name="ssl-type"][value="internal"]').checked=!0,a.value=SITE.defaults.targetIP||"localhost",n.checked=!1;const F=document.querySelector("#app-deploy-modal .flex-col-gap")?.closest("div"),q=document.querySelector("#app-deploy-modal details"),U=q?.querySelector("div");if(q&&U&&(SITE.configurationType==="public"||SITE.configurationType==="homelab")){const _=document.querySelectorAll('#app-deploy-modal input[name="dns-type"]')[0]?.closest("div.flex-col-gap")?.parentElement,J=document.querySelectorAll('#app-deploy-modal input[name="ssl-type"]')[0]?.closest("div.flex-col-gap")?.parentElement;_&&!_.dataset.moved&&(U.appendChild(_),_.dataset.moved="1"),J&&!J.dataset.moved&&(U.appendChild(J),J.dataset.moved="1")}const G=document.getElementById("media-path-section"),W=document.getElementById("deploy-media-path"),X=document.getElementById("media-path-description");if(r.mediaMount){G.style.display="block",W.value="",W.placeholder="/media/Movies, /media/TVShows or click Browse";const _=document.getElementById("detected-mounts-container"),J=document.getElementById("detected-mounts-list");try{const K=await(await fetch("/api/v1/media/detected-mounts")).json();if(K.success&&K.mounts.length>0){_.style.display="block",J.innerHTML="";const te=[...new Set(K.mounts.map(ee=>ee.hostPath))];W.value=te.join(", "),K.mounts.forEach(ee=>{const Z=document.createElement("button");Z.type="button";const de=te.includes(ee.hostPath);Z.style.cssText=`padding: 8px 14px; font-size: 0.85rem; background: color-mix(in srgb, var(--success) ${de?"40%":"15%"}, var(--card-bg)); border: 1px solid var(--success); border-radius: 6px; cursor: pointer; color: var(--fg);`,Z.innerHTML=`${escapeHtml(ee.folderName)}
from ${escapeHtml(ee.sourceImage)}`,Z.title=`${ee.hostPath} (from ${ee.sourceContainer})`,Z.onclick=()=>{const le=W.value.split(",").map(ce=>ce.trim()).filter(ce=>ce),pe=le.indexOf(ee.hostPath);pe>=0?(le.splice(pe,1),Z.style.background="color-mix(in srgb, var(--success) 15%, var(--card-bg))"):(le.push(ee.hostPath),Z.style.background="color-mix(in srgb, var(--success) 40%, var(--card-bg))"),W.value=le.join(", ")},J.appendChild(Z)})}else _.style.display="none"}catch{_.style.display="none"}document.getElementById("browse-media-btn").onclick=()=>{openFolderBrowser(W)}}else G.style.display="none",W.value="",document.getElementById("detected-mounts-container").style.display="none";const Q=document.getElementById("plex-claim-section");Q&&(r.id==="plex"||r.claimToken?(Q.style.display="block",document.getElementById("deploy-plex-claim").value=""):Q.style.display="none");const ne=document.getElementById("volume-mounts-section"),oe=document.getElementById("volume-mounts-list");if(oe.innerHTML="",r.docker?.volumes?.length){const _=r.mediaMount?.containerPath,J=r.docker.volumes.filter(V=>!V.includes("{{MEDIA_PATH}}")&&!(_&&V.endsWith(":"+_)));J.length>0?(ne.style.display="block",J.forEach((V,K)=>{const[te,ee]=V.split(":"),Z=document.createElement("div");Z.style.cssText="display: flex; gap: 6px; align-items: center;",Z.innerHTML=` +Click Cancel to deploy a new container.`)&&(d._useExisting=!0,d._existingContainer=V)}}catch{}i.textContent=`Deploy ${d.name}`;const r=d.subdomain||d.id.replace(/-/g,"");$.value=r;const t=document.getElementById("subpath-compat-warning");if(t)if(SITE.routingMode==="subdirectory"){const _=d.subpathSupport||"strip";_==="none"?(t.style.display="block",t.innerHTML=''+d.name+" does not support subdirectory mode. It may not work correctly at a subpath."):_==="strip"?(t.style.display="block",t.innerHTML='ⓘ '+d.name+" has unverified subdirectory support. It may require additional configuration."):t.style.display="none"}else t.style.display="none";const s=SITE.defaults.dnsType||(SITE.configurationType==="public"?"public":"private"),l=SITE.defaults.sslType||(SITE.configurationType==="public"?"letsencrypt":"internal"),C=document.querySelector(`input[name="dns-type"][value="${s}"]`),I=document.querySelector(`input[name="ssl-type"][value="${l}"]`);C?C.checked=!0:document.querySelector('input[name="dns-type"][value="private"]').checked=!0,I?I.checked=!0:document.querySelector('input[name="ssl-type"][value="internal"]').checked=!0,n.value=SITE.defaults.targetIP||"localhost",o.checked=!1;const F=document.querySelector("#app-deploy-modal .flex-col-gap")?.closest("div"),q=document.querySelector("#app-deploy-modal details"),U=q?.querySelector("div");if(q&&U&&(SITE.configurationType==="public"||SITE.configurationType==="homelab")){const _=document.querySelectorAll('#app-deploy-modal input[name="dns-type"]')[0]?.closest("div.flex-col-gap")?.parentElement,W=document.querySelectorAll('#app-deploy-modal input[name="ssl-type"]')[0]?.closest("div.flex-col-gap")?.parentElement;_&&!_.dataset.moved&&(U.appendChild(_),_.dataset.moved="1"),W&&!W.dataset.moved&&(U.appendChild(W),W.dataset.moved="1")}const G=document.getElementById("media-path-section"),J=document.getElementById("deploy-media-path"),X=document.getElementById("media-path-description");if(d.mediaMount){G.style.display="block",J.value="",J.placeholder="/media/Movies, /media/TVShows or click Browse";const _=document.getElementById("detected-mounts-container"),W=document.getElementById("detected-mounts-list");try{const K=await(await fetch("/api/v1/media/detected-mounts")).json();if(K.success&&K.mounts.length>0){_.style.display="block",W.innerHTML="";const te=[...new Set(K.mounts.map(ee=>ee.hostPath))];J.value=te.join(", "),K.mounts.forEach(ee=>{const Z=document.createElement("button");Z.type="button";const le=te.includes(ee.hostPath);Z.style.cssText=`padding: 8px 14px; font-size: 0.85rem; background: color-mix(in srgb, var(--success) ${le?"40%":"15%"}, var(--card-bg)); border: 1px solid var(--success); border-radius: 6px; cursor: pointer; color: var(--fg);`,Z.innerHTML=`${escapeHtml(ee.folderName)}
from ${escapeHtml(ee.sourceImage)}`,Z.title=`${ee.hostPath} (from ${ee.sourceContainer})`,Z.onclick=()=>{const de=J.value.split(",").map(ce=>ce.trim()).filter(ce=>ce),pe=de.indexOf(ee.hostPath);pe>=0?(de.splice(pe,1),Z.style.background="color-mix(in srgb, var(--success) 15%, var(--card-bg))"):(de.push(ee.hostPath),Z.style.background="color-mix(in srgb, var(--success) 40%, var(--card-bg))"),J.value=de.join(", ")},W.appendChild(Z)})}else _.style.display="none"}catch{_.style.display="none"}document.getElementById("browse-media-btn").onclick=()=>{openFolderBrowser(J)}}else G.style.display="none",J.value="",document.getElementById("detected-mounts-container").style.display="none";const Q=document.getElementById("plex-claim-section");Q&&(d.id==="plex"||d.claimToken?(Q.style.display="block",document.getElementById("deploy-plex-claim").value=""):Q.style.display="none");const ne=document.getElementById("volume-mounts-section"),oe=document.getElementById("volume-mounts-list");if(oe.innerHTML="",d.docker?.volumes?.length){const _=d.mediaMount?.containerPath,W=d.docker.volumes.filter(V=>!V.includes("{{MEDIA_PATH}}")&&!(_&&V.endsWith(":"+_)));W.length>0?(ne.style.display="block",W.forEach((V,K)=>{const[te,ee]=V.split(":"),Z=document.createElement("div");Z.style.cssText="display: flex; gap: 6px; align-items: center;",Z.innerHTML=` \u2192 ${ee} - `,oe.appendChild(Z),Z.querySelector(".vol-browse-btn").onclick=()=>{const de=Z.querySelector(".vol-host-path");openFolderBrowser(de)}})):ne.style.display="none"}else ne.style.display="none";const se=r.defaultPort||8080;e.value="",e.placeholder=`Default: ${se}`;let Y=document.getElementById("deploy-port-status");Y||(Y=document.createElement("div"),Y.id="deploy-port-status",Y.style.cssText="font-size: 0.8rem; margin-top: 4px;",e.parentNode.appendChild(Y));async function ie(){const _=e.value||se;Y.innerHTML='Checking port...';const J=await H(_);if(J.available)Y.innerHTML=`Port ${escapeHtml(String(_))} is available`;else{const V=await g(se);Y.innerHTML=` - Port ${escapeHtml(_)} in use by ${escapeHtml(J.conflict?.usedBy||"unknown")} - `;const K=document.createElement("button");K.type="button",K.textContent=`Use ${V}`,K.style.cssText="margin-left: 8px; padding: 2px 8px; font-size: 0.75rem; cursor: pointer;",K.onclick=()=>{document.getElementById("deploy-port").value=V,Y.innerHTML=`Using suggested port ${escapeHtml(String(V))}`},Y.appendChild(K)}}let re;e.oninput=function(){clearTimeout(re),re=setTimeout(ie,500)},ie();try{const J=await(await fetch("/api/v1/tailscale/status")).json();J.success&&J.installed&&J.connected?t.innerHTML=` + `,oe.appendChild(Z),Z.querySelector(".vol-browse-btn").onclick=()=>{const le=Z.querySelector(".vol-host-path");openFolderBrowser(le)}})):ne.style.display="none"}else ne.style.display="none";const se=d.defaultPort||8080;e.value="",e.placeholder=`Default: ${se}`;let Y=document.getElementById("deploy-port-status");Y||(Y=document.createElement("div"),Y.id="deploy-port-status",Y.style.cssText="font-size: 0.8rem; margin-top: 4px;",e.parentNode.appendChild(Y));async function ie(){const _=e.value||se;Y.innerHTML='Checking port...';const W=await A(_);if(W.available)Y.innerHTML=`Port ${escapeHtml(String(_))} is available`;else{const V=await v(se);Y.innerHTML=` + Port ${escapeHtml(_)} in use by ${escapeHtml(W.conflict?.usedBy||"unknown")} + `;const K=document.createElement("button");K.type="button",K.textContent=`Use ${V}`,K.style.cssText="margin-left: 8px; padding: 2px 8px; font-size: 0.75rem; cursor: pointer;",K.onclick=()=>{document.getElementById("deploy-port").value=V,Y.innerHTML=`Using suggested port ${escapeHtml(String(V))}`},Y.appendChild(K)}}let re;e.oninput=function(){clearTimeout(re),re=setTimeout(ie,500)},ie();try{const W=await(await fetch("/api/v1/tailscale/status")).json();W.success&&W.installed&&W.connected?a.innerHTML=` Connected - ${J.self?.hostname} (${J.self?.ip}) - | ${J.deviceCount} devices - `:J.installed?t.innerHTML='Not connected':(t.innerHTML='Not available',n.disabled=!0)}catch{t.innerHTML='Could not check status'}function ae(){const _=h.value||"subdomain",J=document.querySelector('input[name="dns-type"]:checked').value,V=document.querySelector('input[name="ssl-type"]:checked').value;let K="";if(SITE.routingMode==="subdirectory"&&SITE.domain)K=`https://${SITE.domain}/${_}`;else if(J==="private")K=`${V==="none"?"http":"https"}://${buildDomain(_)}`;else if(J==="public"){const te=V==="none"?"http":"https",ee=SITE.domain||_;K=SITE.domain?`${te}://${_}.${SITE.domain}`:`${te}://${_}`}else{const te=e.value||r.defaultPort||DC.DEFAULTS.SERVICE_PORT;K=`http://${a.value}:${te}`}u.textContent=K}h.oninput=ae,a.oninput=ae,e.oninput=ae,document.querySelectorAll('input[name="dns-type"]').forEach(_=>{_.onchange=ae}),document.querySelectorAll('input[name="ssl-type"]').forEach(_=>{_.onchange=ae}),ae(),T.classList.remove("show"),c.classList.add("show"),c.dataset.appTemplate=JSON.stringify(r)}async function $(r){const c=r.appTemplate,s=safeGetJSON(E,[]),h=c._useExisting&&c._existingContainer,u=s.find(a=>a.id===r.subdomain);if(!(u&&!h&&!confirm(`An app with subdomain "${r.subdomain}" already exists. Redeploy?`))){if(u){const a=s.indexOf(u);s.splice(a,1),safeSet(E,JSON.stringify(s))}if(h)r.port=c._existingContainer.primaryPort;else{const a=r.port||c.defaultPort||8080;showNotification(`Checking port ${a} availability...`,"info",0);const e=await H(a);if(!e.available){const n=await g(c.defaultPort||8080);if(confirm(`Port ${a} is already in use by ${e.conflict?.usedBy||"another container"}. + ${W.self?.hostname} (${W.self?.ip}) + | ${W.deviceCount} devices + `:W.installed?a.innerHTML='Not connected':(a.innerHTML='Not available',o.disabled=!0)}catch{a.innerHTML='Could not check status'}function ae(){const _=$.value||"subdomain",W=document.querySelector('input[name="dns-type"]:checked').value,V=document.querySelector('input[name="ssl-type"]:checked').value;let K="";if(SITE.routingMode==="subdirectory"&&SITE.domain)K=`https://${SITE.domain}/${_}`;else if(W==="private")K=`${V==="none"?"http":"https"}://${buildDomain(_)}`;else if(W==="public"){const te=V==="none"?"http":"https",ee=SITE.domain||_;K=SITE.domain?`${te}://${_}.${SITE.domain}`:`${te}://${_}`}else{const te=e.value||d.defaultPort||DC.DEFAULTS.SERVICE_PORT;K=`http://${n.value}:${te}`}y.textContent=K}$.oninput=ae,n.oninput=ae,e.oninput=ae,document.querySelectorAll('input[name="dns-type"]').forEach(_=>{_.onchange=ae}),document.querySelectorAll('input[name="ssl-type"]').forEach(_=>{_.onchange=ae}),ae(),N.classList.remove("show"),c.classList.add("show"),c.dataset.appTemplate=JSON.stringify(d)}async function k(d){const c=d.appTemplate,i=safeGetJSON(E,[]),$=c._useExisting&&c._existingContainer,y=i.find(n=>n.id===d.subdomain);if(!(y&&!$&&!confirm(`An app with subdomain "${d.subdomain}" already exists. Redeploy?`))){if(y){const n=i.indexOf(y);i.splice(n,1),safeSet(E,JSON.stringify(i))}if($)d.port=c._existingContainer.primaryPort;else{const n=d.port||c.defaultPort||8080;showNotification(`Checking port ${n} availability...`,"info",0);const e=await A(n);if(!e.available){const o=await v(c.defaultPort||8080);if(confirm(`Port ${n} is already in use by ${e.conflict?.usedBy||"another container"}. -Would you like to use port ${n} instead?`))r.port=n;else{showNotification("Deployment cancelled - port conflict","error",5e3);return}}}showNotification(h?`Configuring ${c.name} with existing container...`:`Deploying ${c.name}...`,"info",0);try{const a={appId:c.id,config:{subdomain:r.subdomain,ip:r.ip,createDns:r.dnsType==="private",port:r.port||c.defaultPort||null,sslType:r.sslType,dnsType:r.dnsType,tailscaleOnly:r.tailscaleOnly||!1,mediaPath:r.mediaPath||null,plexClaimToken:r.plexClaimToken||null,customVolumes:r.customVolumes||null}};h&&(a.config.useExisting=!0,a.config.existingContainerId=c._existingContainer.id,a.config.existingPort=c._existingContainer.primaryPort,!r.port&&c._existingContainer.primaryPort&&(a.config.port=c._existingContainer.primaryPort));const n=await(await secureFetch("/api/v1/apps/deploy",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(a)})).json();if(n.success){const t={id:r.subdomain,name:c.name,logo:`/assets/${c.id}.png`,containerId:n.containerId,url:n.url,ip:r.ip,appTemplate:c.id,tailscaleOnly:r.tailscaleOnly||!1};s.push(t),safeSet(E,JSON.stringify(s)),window.APPS&&!window.APPS.some(o=>o.id===c.id)&&(window.APPS.push(t),typeof window.buildGrid=="function"&&window.buildGrid(),typeof window.refreshAll=="function"&&setTimeout(()=>window.refreshAll(),500));let i=n.usedExisting?`${c.name} configured with existing container! -URL: ${n.url}`:`${c.name} deployed successfully! -URL: ${n.url}`;n.warning&&(i+=` +Would you like to use port ${o} instead?`))d.port=o;else{showNotification("Deployment cancelled - port conflict","error",5e3);return}}}showNotification($?`Configuring ${c.name} with existing container...`:`Deploying ${c.name}...`,"info",0);try{const n={appId:c.id,config:{subdomain:d.subdomain,ip:d.ip,createDns:d.dnsType==="private",port:d.port||c.defaultPort||null,sslType:d.sslType,dnsType:d.dnsType,tailscaleOnly:d.tailscaleOnly||!1,mediaPath:d.mediaPath||null,plexClaimToken:d.plexClaimToken||null,customVolumes:d.customVolumes||null}};$&&(n.config.useExisting=!0,n.config.existingContainerId=c._existingContainer.id,n.config.existingPort=c._existingContainer.primaryPort,!d.port&&c._existingContainer.primaryPort&&(n.config.port=c._existingContainer.primaryPort));const o=await(await secureFetch("/api/v1/apps/deploy",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)})).json();if(o.success){const a={id:d.subdomain,name:c.name,logo:`/assets/${c.id}.png`,containerId:o.containerId,url:o.url,ip:d.ip,appTemplate:c.id,tailscaleOnly:d.tailscaleOnly||!1};i.push(a),safeSet(E,JSON.stringify(i)),window.APPS&&!window.APPS.some(t=>t.id===c.id)&&(window.APPS.push(a),typeof window.buildGrid=="function"&&window.buildGrid(),typeof window.refreshAll=="function"&&setTimeout(()=>window.refreshAll(),500));let r=o.usedExisting?`${c.name} configured with existing container! +URL: ${o.url}`:`${c.name} deployed successfully! +URL: ${o.url}`;o.warning&&(r+=` -\u26A0 Warning: ${n.warning}`),showNotification(i,"success",8e3),delete c._useExisting,delete c._existingContainer,n.url&&n.url.startsWith("https://")&&C(n.url,c.name),n.setupInstructions&&n.setupInstructions.length>0&&setTimeout(()=>{const o=n.setupInstructions.join(` -`);showNotification(`Setup Instructions for ${c.name}: ${o}`,"info",1e4)},1e3)}else throw new Error(n.error||"Deployment failed")}catch(a){b.logError("[AppSelector] Deployment",a,{function:"deploy"}),showNotification(`Failed to deploy ${c.name}: ${a.message}`,"error",8e3)}}}async function C(r,c){showNotification(`\u23F3 Generating SSL certificate for ${c}...`,"warning",6e4);let s=0;const h=12,u=async()=>{s++;try{const a=await fetch(r,{method:"HEAD",mode:"no-cors"});return showNotification(`\u2705 ${c} is ready! SSL certificate generated.`,"success",5e3),!0}catch{return s{window.APPS.some(s=>s.id===c.id)||window.APPS.push(c)})}document.getElementById("add-service-btn")?.addEventListener("click",()=>{I(),T.classList.add("show")}),wireModal(T,document.getElementById("app-selector-cancel"));const M=document.getElementById("app-deploy-modal");document.getElementById("app-deploy-cancel")?.addEventListener("click",()=>{M.classList.remove("show")}),document.getElementById("app-deploy-confirm")?.addEventListener("click",()=>{const r=JSON.parse(M.dataset.appTemplate),c=document.getElementById("deploy-media-path").value.trim(),s=[];document.querySelectorAll("#volume-mounts-list .vol-host-path").forEach(u=>{s.push({hostPath:u.value.trim(),containerPath:u.dataset.containerPath})});const h={appTemplate:r,subdomain:document.getElementById("deploy-subdomain").value.trim(),dnsType:document.querySelector('input[name="dns-type"]:checked').value,sslType:document.querySelector('input[name="ssl-type"]:checked').value,ip:document.getElementById("deploy-ip").value.trim(),port:document.getElementById("deploy-port").value.trim(),tailscaleOnly:document.getElementById("deploy-tailscale-only").checked,mediaPath:c||null,plexClaimToken:document.getElementById("deploy-plex-claim")?.value.trim()||null,customVolumes:s.length>0?s:null,resources:{cpus:parseFloat(document.getElementById("deploy-cpu-limit").value)||0,memory:parseFloat(document.getElementById("deploy-memory-limit").value)||0}};if(!h.subdomain){showNotification("Please enter a subdomain or domain name","warning");return}if(r.mediaMount?.required&&!c){showNotification("Please enter a media library path for this application","warning");return}M.classList.remove("show"),$(h)}),wireModal(M);const j=document.getElementById("folder-browser-modal"),B=document.getElementById("folder-browser-path"),A=document.getElementById("folder-browser-list"),w=document.getElementById("folder-browser-selected"),z=document.getElementById("folder-browser-selected-list");let f="",p=[],y=null;window.openFolderBrowser=function(r){y=r,p=r.value.split(",").map(c=>c.trim()).filter(c=>c),f="",m(),v(""),j.classList.add("show")};async function v(r){B.textContent=r||"Select a drive...",A.innerHTML='
Loading...
';try{const s=await(await fetch(`/api/v1/browse/directories?path=${encodeURIComponent(r)}`)).json();if(!s.success){A.innerHTML=`
Error: ${escapeHtml(s.error)}
`;return}f=s.path||"",B.textContent=f||"Select a drive...";let h="";s.parent&&s.parent!==s.path&&(h+=`
+\u26A0 Warning: ${o.warning}`),showNotification(r,"success",8e3),delete c._useExisting,delete c._existingContainer,o.url&&o.url.startsWith("https://")&&B(o.url,c.name),o.setupInstructions&&o.setupInstructions.length>0&&setTimeout(()=>{const t=o.setupInstructions.join(` +`);showNotification(`Setup Instructions for ${c.name}: ${t}`,"info",1e4)},1e3)}else throw new Error(o.error||"Deployment failed")}catch(n){h.logError("[AppSelector] Deployment",n,{function:"deploy"}),showNotification(`Failed to deploy ${c.name}: ${n.message}`,"error",8e3)}}}async function B(d,c){showNotification(`\u23F3 Generating SSL certificate for ${c}...`,"warning",6e4);let i=0;const $=12,y=async()=>{i++;try{const n=await fetch(d,{method:"HEAD",mode:"no-cors"});return showNotification(`\u2705 ${c} is ready! SSL certificate generated.`,"success",5e3),!0}catch{return i<$?setTimeout(y,5e3):showNotification(`\u26A0\uFE0F ${c} deployed but SSL certificate may still be generating. +Try refreshing in a moment if you see a certificate error.`,"warning",1e4),!1}};setTimeout(y,3e3)}function S(){safeGetJSON(E,[]).forEach(c=>{window.APPS.some(i=>i.id===c.id)||window.APPS.push(c)})}document.getElementById("add-service-btn")?.addEventListener("click",()=>{L(),N.classList.add("show")}),wireModal(N,document.getElementById("app-selector-cancel"));const T=document.getElementById("app-deploy-modal");document.getElementById("app-deploy-cancel")?.addEventListener("click",()=>{T.classList.remove("show")}),document.getElementById("app-deploy-confirm")?.addEventListener("click",()=>{const d=JSON.parse(T.dataset.appTemplate),c=document.getElementById("deploy-media-path").value.trim(),i=[];document.querySelectorAll("#volume-mounts-list .vol-host-path").forEach(y=>{i.push({hostPath:y.value.trim(),containerPath:y.dataset.containerPath})});const $={appTemplate:d,subdomain:document.getElementById("deploy-subdomain").value.trim(),dnsType:document.querySelector('input[name="dns-type"]:checked').value,sslType:document.querySelector('input[name="ssl-type"]:checked').value,ip:document.getElementById("deploy-ip").value.trim(),port:document.getElementById("deploy-port").value.trim(),tailscaleOnly:document.getElementById("deploy-tailscale-only").checked,mediaPath:c||null,plexClaimToken:document.getElementById("deploy-plex-claim")?.value.trim()||null,customVolumes:i.length>0?i:null,resources:{cpus:parseFloat(document.getElementById("deploy-cpu-limit").value)||0,memory:parseFloat(document.getElementById("deploy-memory-limit").value)||0}};if(!$.subdomain){showNotification("Please enter a subdomain or domain name","warning");return}if(d.mediaMount?.required&&!c){showNotification("Please enter a media library path for this application","warning");return}T.classList.remove("show"),k($)}),wireModal(T);const j=document.getElementById("folder-browser-modal"),H=document.getElementById("folder-browser-path"),R=document.getElementById("folder-browser-list"),x=document.getElementById("folder-browser-selected"),D=document.getElementById("folder-browser-selected-list");let g="",u=[],f=null;window.openFolderBrowser=function(d){f=d,u=d.value.split(",").map(c=>c.trim()).filter(c=>c),g="",p(),m(""),j.classList.add("show")};async function m(d){H.textContent=d||"Select a drive...",R.innerHTML='
Loading...
';try{const i=await(await fetch(`/api/v1/browse/directories?path=${encodeURIComponent(d)}`)).json();if(!i.success){R.innerHTML=`
Error: ${escapeHtml(i.error)}
`;return}g=i.path||"",H.textContent=g||"Select a drive...";let $="";i.parent&&i.parent!==i.path&&($+=`
\u2B06\uFE0F .. Parent Directory -
`),s.items.length===0&&!s.parent?h+='
No browseable drives configured. Check your docker-compose.yml volume mounts.
':s.items.length===0?h+='
No subfolders found
':s.items.forEach(u=>{const a=u.type==="drive"?"\u{1F4BE}":"\u{1F4C1}",e=p.includes(u.path),n=e?"background: color-mix(in srgb, var(--success) 20%, transparent);":"";h+=`
- ${a} - ${escapeHtml(u.name)} +
`),i.items.length===0&&!i.parent?$+='
No browseable drives configured. Check your docker-compose.yml volume mounts.
':i.items.length===0?$+='
No subfolders found
':i.items.forEach(y=>{const n=y.type==="drive"?"\u{1F4BE}":"\u{1F4C1}",e=u.includes(y.path),o=e?"background: color-mix(in srgb, var(--success) 20%, transparent);":"";$+=`
+ ${n} + ${escapeHtml(y.name)} ${e?'\u2713':""} -
`}),A.innerHTML=h,A.querySelectorAll(".folder-item").forEach(u=>{u.addEventListener("click",()=>{v(u.dataset.path)}),u.addEventListener("mouseenter",()=>{u.style.background="var(--card-bg)"}),u.addEventListener("mouseleave",()=>{const a=p.includes(u.dataset.path);u.style.background=a?"color-mix(in srgb, var(--success) 20%, transparent)":""})})}catch(c){A.innerHTML=`
Failed to load: ${escapeHtml(c.message)}
`}}function m(){if(p.length===0){w.style.display="none";return}w.style.display="block",z.innerHTML=p.map(r=>` +
`}),R.innerHTML=$,R.querySelectorAll(".folder-item").forEach(y=>{y.addEventListener("click",()=>{m(y.dataset.path)}),y.addEventListener("mouseenter",()=>{y.style.background="var(--card-bg)"}),y.addEventListener("mouseleave",()=>{const n=u.includes(y.dataset.path);y.style.background=n?"color-mix(in srgb, var(--success) 20%, transparent)":""})})}catch(c){R.innerHTML=`
Failed to load: ${escapeHtml(c.message)}
`}}function p(){if(u.length===0){x.style.display="none";return}x.style.display="block",D.innerHTML=u.map(d=>` - ${escapeHtml(r)} - + ${escapeHtml(d)} + - `).join("")}window.removeSelectedFolder=function(r){p=p.filter(c=>c!==r),m(),v(f)},document.getElementById("folder-browser-select-current").addEventListener("click",()=>{f&&!p.includes(f)&&(p.push(f),m(),v(f))}),wireModal(j,document.getElementById("folder-browser-cancel")),document.getElementById("folder-browser-done").addEventListener("click",()=>{y&&(y.value=p.join(", ")),j.classList.remove("show")}),R()})(),(function(){injectModal("recipe-deploy-modal",`
+ `).join("")}window.removeSelectedFolder=function(d){u=u.filter(c=>c!==d),p(),m(g)},document.getElementById("folder-browser-select-current").addEventListener("click",()=>{g&&!u.includes(g)&&(u.push(g),p(),m(g))}),wireModal(j,document.getElementById("folder-browser-cancel")),document.getElementById("folder-browser-done").addEventListener("click",()=>{f&&(f.value=u.join(", ")),j.classList.remove("show")}),S()})(),(function(){injectModal("recipe-deploy-modal",`

Deploy Recipe

@@ -445,70 +445,70 @@ Try refreshing in a moment if you see a certificate error.`,"warning",1e4),!1}};
-
`);let b=null,E=null,N=null,S=1,T=!1;const P=document.getElementById("recipe-deploy-modal"),L=document.getElementById("recipe-cancel"),H=document.getElementById("recipe-prev"),g=document.getElementById("recipe-next");wireModal(P,L);async function I(){try{const f=await fetch("/api/v1/recipes/templates"),p=await f.json();if(p.success)return b=p.templates,E=p.categories,!0;if(f.status===403)return T=!1,!1}catch(f){console.warn("Failed to fetch recipe templates:",f.message)}return!1}async function k(){try{T=(await(await fetch("/api/v1/license/feature/recipes")).json()).available}catch{T=!1}return T}window.renderRecipeCards=async function(f){await k();let p;if(T&&b?p=b:p=x(),!p||p.length===0)return;const y=document.createElement("div");y.className="app-category-header",y.innerHTML="\u{1F9EA} Recipes",y.style.borderBottomColor="#8e44ad",f.appendChild(y);const v=Array.isArray(p)?p:Object.values(p);v.sort((m,r)=>(r.popularity||0)-(m.popularity||0));for(const m of v){const r=document.createElement("div");r.className="app-option",r.style.position="relative";const c=`
${m.componentCount||m.components?.length||"?"} apps
`,s=T?"":'
PREMIUM
';r.innerHTML=` - ${s} -
${escapeHtml(m.icon||"\u{1F9EA}")}
-
${escapeHtml(m.name)}
-
${escapeHtml(m.description||"")}
+ `);let h=null,E=null,P=null,w=1,N=!1;const O=document.getElementById("recipe-deploy-modal"),z=document.getElementById("recipe-cancel"),A=document.getElementById("recipe-prev"),v=document.getElementById("recipe-next");wireModal(O,z);async function L(){try{const g=await fetch("/api/v1/recipes/templates"),u=await g.json();if(u.success)return h=u.templates,E=u.categories,!0;if(g.status===403)return N=!1,!1}catch(g){console.warn("Failed to fetch recipe templates:",g.message)}return!1}async function b(){try{N=(await(await fetch("/api/v1/license/feature/recipes")).json()).available}catch{N=!1}return N}window.renderRecipeCards=async function(g){await b();let u;if(N&&h?u=h:u=M(),!u||u.length===0)return;const f=document.createElement("div");f.className="app-category-header",f.innerHTML="\u{1F9EA} Recipes",f.style.borderBottomColor="#8e44ad",g.appendChild(f);const m=Array.isArray(u)?u:Object.values(u);m.sort((p,d)=>(d.popularity||0)-(p.popularity||0));for(const p of m){const d=document.createElement("div");d.className="app-option",d.style.position="relative";const c=`
${p.componentCount||p.components?.length||"?"} apps
`,i=N?"":'
PREMIUM
';d.innerHTML=` + ${i} +
${escapeHtml(p.icon||"\u{1F9EA}")}
+
${escapeHtml(p.name)}
+
${escapeHtml(p.description||"")}
${c} - `,r.onclick=()=>{if(!T){showNotification("Recipes require a DashCaddy Premium license. Click the License button to activate.","warning",5e3),window.openLicenseModal&&window.openLicenseModal();return}$(m)},f.appendChild(r)}};function x(){return[{id:"htpc-suite",name:"HTPC Suite",icon:"\u{1F3AC}",description:"Complete media automation: find, download, organize, and stream",componentCount:6,popularity:98},{id:"nextcloud-complete",name:"Nextcloud Complete",icon:"\u2601\uFE0F",description:"Full productivity suite: cloud storage, office editing, and collaboration",componentCount:4,popularity:90},{id:"smart-home",name:"Smart Home Hub",icon:"\u{1F3E0}",description:"Home automation: control, automate, and monitor IoT devices",componentCount:4,popularity:88},{id:"dev-environment",name:"Dev Environment",icon:"\u{1F4BB}",description:"Self-hosted development workflow: Git, CI/CD, IDE, and database",componentCount:4,popularity:82}]}function $(f){N=f,S=1;const p=document.getElementById("app-selector-modal");p&&p.classList.remove("show"),document.getElementById("recipe-deploy-title").textContent=`Deploy ${f.name}`,C(),R(),P.classList.add("show")}function C(){document.querySelectorAll("#recipe-steps .recipe-step").forEach(f=>{const p=parseInt(f.dataset.step);f.classList.toggle("active",p===S),f.classList.toggle("completed",p1&&S<4?"":"none",S===4?(g.style.display="none",L.textContent="Close"):S===3?(g.textContent="\u{1F680} Deploy",g.style.display="",L.textContent="Cancel"):(g.textContent="Next",g.style.display="",L.textContent="Cancel")}function R(){const f=document.getElementById("recipe-component-list");f.innerHTML="";const p=N.components||[];for(const y of p){const v=document.createElement("div");v.style.cssText="display: flex; align-items: center; gap: 12px; padding: 12px; border-radius: 8px; background: var(--card-bg); border: 1px solid var(--border);";const m=y.required,r=y.internal;v.innerHTML=` - {if(!N){showNotification("Recipes require a DashCaddy Premium license. Click the License button to activate.","warning",5e3),window.openLicenseModal&&window.openLicenseModal();return}k(p)},g.appendChild(d)}};function M(){return[{id:"htpc-suite",name:"HTPC Suite",icon:"\u{1F3AC}",description:"Complete media automation: find, download, organize, and stream",componentCount:6,popularity:98},{id:"nextcloud-complete",name:"Nextcloud Complete",icon:"\u2601\uFE0F",description:"Full productivity suite: cloud storage, office editing, and collaboration",componentCount:4,popularity:90},{id:"smart-home",name:"Smart Home Hub",icon:"\u{1F3E0}",description:"Home automation: control, automate, and monitor IoT devices",componentCount:4,popularity:88},{id:"dev-environment",name:"Dev Environment",icon:"\u{1F4BB}",description:"Self-hosted development workflow: Git, CI/CD, IDE, and database",componentCount:4,popularity:82}]}function k(g){P=g,w=1;const u=document.getElementById("app-selector-modal");u&&u.classList.remove("show"),document.getElementById("recipe-deploy-title").textContent=`Deploy ${g.name}`,B(),S(),O.classList.add("show")}function B(){document.querySelectorAll("#recipe-steps .recipe-step").forEach(g=>{const u=parseInt(g.dataset.step);g.classList.toggle("active",u===w),g.classList.toggle("completed",u1&&w<4?"":"none",w===4?(v.style.display="none",z.textContent="Close"):w===3?(v.textContent="\u{1F680} Deploy",v.style.display="",z.textContent="Cancel"):(v.textContent="Next",v.style.display="",z.textContent="Cancel")}function S(){const g=document.getElementById("recipe-component-list");g.innerHTML="";const u=P.components||[];for(const f of u){const m=document.createElement("div");m.style.cssText="display: flex; align-items: center; gap: 12px; padding: 12px; border-radius: 8px; background: var(--card-bg); border: 1px solid var(--border);";const p=f.required,d=f.internal;m.innerHTML=` +
-
${escapeHtml(y.role||y.id)}
+
${escapeHtml(f.role||f.id)}
- ${y.templateRef?escapeHtml(y.templateRef):"Built-in"} - ${m?'Required':'Optional'} - ${r?'(Internal)':""} + ${f.templateRef?escapeHtml(f.templateRef):"Built-in"} + ${p?'Required':'Optional'} + ${d?'(Internal)':""}
- ${y.note?`
\u26A0 ${escapeHtml(y.note)}
`:""} + ${f.note?`
\u26A0 ${escapeHtml(f.note)}
`:""}
- `,f.appendChild(v)}}function M(){const f=document.getElementById("recipe-volumes-section"),p=document.getElementById("recipe-volume-list"),y=N.sharedVolumes;if(y&&Object.keys(y).length>0){f.style.display="",p.innerHTML="";for(const[v,m]of Object.entries(y)){const r=document.createElement("div");r.style.cssText="display: grid; gap: 4px;",r.innerHTML=` - - 0){g.style.display="",u.innerHTML="";for(const[m,p]of Object.entries(f)){const d=document.createElement("div");d.style.cssText="display: grid; gap: 4px;",d.innerHTML=` + + -
${escapeHtml(m.description||"")}
- `,p.appendChild(r)}}else f.style.display="none"}function j(){const f=document.getElementById("recipe-review-content"),p=B(),y=document.querySelectorAll("#recipe-volume-list input[data-volume-key]"),v={};y.forEach(s=>{v[s.dataset.volumeKey]=s.value});const m=document.getElementById("recipe-timezone").value||"UTC",r=document.getElementById("recipe-ip").value||"host.docker.internal",c=document.getElementById("recipe-tailscale").checked;f.innerHTML=` -
${escapeHtml(N.name)}
-
${escapeHtml(N.description||"")}
+
${escapeHtml(p.description||"")}
+ `,u.appendChild(d)}}else g.style.display="none"}function j(){const g=document.getElementById("recipe-review-content"),u=H(),f=document.querySelectorAll("#recipe-volume-list input[data-volume-key]"),m={};f.forEach(i=>{m[i.dataset.volumeKey]=i.value});const p=document.getElementById("recipe-timezone").value||"UTC",d=document.getElementById("recipe-ip").value||"host.docker.internal",c=document.getElementById("recipe-tailscale").checked;g.innerHTML=` +
${escapeHtml(P.name)}
+
${escapeHtml(P.description||"")}
- Components (${p.length}): + Components (${u.length}):
- ${p.map(s=>`
- \u2022 ${escapeHtml(s.role||s.id)} ${s.internal?'(internal)':""} + ${u.map(i=>`
+ \u2022 ${escapeHtml(i.role||i.id)} ${i.internal?'(internal)':""}
`).join("")}
- ${Object.keys(v).length>0?`
+ ${Object.keys(m).length>0?`
Volumes: - ${Object.entries(v).map(([s,h])=>`
${s}: ${escapeHtml(h)}
`).join("")} + ${Object.entries(m).map(([i,$])=>`
${i}: ${escapeHtml($)}
`).join("")}
`:""}
- Timezone: ${escapeHtml(m)} • IP: ${escapeHtml(r)} ${c?"• Tailscale only":""} + Timezone: ${escapeHtml(p)} • IP: ${escapeHtml(d)} ${c?"• Tailscale only":""}
- ${N.network?`
Docker network: ${escapeHtml(N.network.name)}
`:""} - `}function B(){const f=document.querySelectorAll("#recipe-component-list input[data-component-id]"),p=new Set;f.forEach(v=>{v.checked&&p.add(v.dataset.componentId)});const y=N.components||[];return y.filter(v=>v.required).forEach(v=>p.add(v.id)),y.filter(v=>p.has(v.id))}async function A(){const f=document.getElementById("recipe-progress-list"),p=document.getElementById("recipe-deploy-result");p.style.display="none",f.innerHTML="";const y=B();for(const c of y){const s=document.createElement("div");s.id=`recipe-progress-${c.id}`,s.style.cssText="display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: 6px; background: var(--card-bg); border: 1px solid var(--border); font-size: 0.85rem;",s.innerHTML=` + ${P.network?`
Docker network: ${escapeHtml(P.network.name)}
`:""} + `}function H(){const g=document.querySelectorAll("#recipe-component-list input[data-component-id]"),u=new Set;g.forEach(m=>{m.checked&&u.add(m.dataset.componentId)});const f=P.components||[];return f.filter(m=>m.required).forEach(m=>u.add(m.id)),f.filter(m=>u.has(m.id))}async function R(){const g=document.getElementById("recipe-progress-list"),u=document.getElementById("recipe-deploy-result");u.style.display="none",g.innerHTML="";const f=H();for(const c of f){const i=document.createElement("div");i.id=`recipe-progress-${c.id}`,i.style.cssText="display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: 6px; background: var(--card-bg); border: 1px solid var(--border); font-size: 0.85rem;",i.innerHTML=` \u23F3 ${escapeHtml(c.role||c.id)} Queued - `,f.appendChild(s)}const v=document.querySelectorAll("#recipe-volume-list input[data-volume-key]"),m={};v.forEach(c=>{m[c.dataset.volumeKey]=c.value});const r={selectedComponents:y.map(c=>c.id),sharedConfig:{ip:document.getElementById("recipe-ip").value||"host.docker.internal",timezone:document.getElementById("recipe-timezone").value||"UTC",tailscaleOnly:document.getElementById("recipe-tailscale").checked,volumes:m},componentOverrides:{}};for(const c of y)w(c.id,"deploying","Deploying...");try{const s=await(await secureFetch("/api/v1/recipes/deploy",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({recipeId:N.id,config:r})})).json();if(s.success){for(const h of s.deployed||[])w(h.id,"success",h.url?`Running \u2192 ${h.url}`:"Running");for(const h of s.errors||[])w(h.componentId,"error",h.error);p.style.display="",p.innerHTML=` + `,g.appendChild(i)}const m=document.querySelectorAll("#recipe-volume-list input[data-volume-key]"),p={};m.forEach(c=>{p[c.dataset.volumeKey]=c.value});const d={selectedComponents:f.map(c=>c.id),sharedConfig:{ip:document.getElementById("recipe-ip").value||"host.docker.internal",timezone:document.getElementById("recipe-timezone").value||"UTC",tailscaleOnly:document.getElementById("recipe-tailscale").checked,volumes:p},componentOverrides:{}};for(const c of f)x(c.id,"deploying","Deploying...");try{const i=await(await secureFetch("/api/v1/recipes/deploy",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({recipeId:P.id,config:d})})).json();if(i.success){for(const $ of i.deployed||[])x($.id,"success",$.url?`Running \u2192 ${$.url}`:"Running");for(const $ of i.errors||[])x($.componentId,"error",$.error);u.style.display="",u.innerHTML=`
-
${escapeHtml(s.message||"Deployed!")}
- ${s.setupInstructions?`
+
${escapeHtml(i.message||"Deployed!")}
+ ${i.setupInstructions?`
Setup tips: -
    ${s.setupInstructions.map(h=>`
  • ${escapeHtml(h)}
  • `).join("")}
+
    ${i.setupInstructions.map($=>`
  • ${escapeHtml($)}
  • `).join("")}
`:""}
- `,showNotification(`${N.name} recipe deployed successfully!`,"success",5e3),window.loadServices&&window.loadServices()}else p.style.display="",p.innerHTML=`
- Deployment failed: ${escapeHtml(s.error||"Unknown error")} -
`,showNotification(`Recipe deployment failed: ${s.error}`,"error",5e3)}catch(c){p.style.display="",p.innerHTML=`
+ `,showNotification(`${P.name} recipe deployed successfully!`,"success",5e3),window.loadServices&&window.loadServices()}else u.style.display="",u.innerHTML=`
+ Deployment failed: ${escapeHtml(i.error||"Unknown error")} +
`,showNotification(`Recipe deployment failed: ${i.error}`,"error",5e3)}catch(c){u.style.display="",u.innerHTML=`
Network error: ${escapeHtml(c.message)} -
`}}function w(f,p,y){const v=document.getElementById(`recipe-progress-${f}`);if(!v)return;const m=v.querySelector(".recipe-progress-icon"),r=v.querySelector(".recipe-progress-status");p==="deploying"?(m.textContent="\u23F3",r.style.color="var(--accent)"):p==="success"?(m.textContent="\u2705",r.style.color="var(--ok-fg)"):p==="error"&&(m.textContent="\u274C",r.style.color="var(--bad-fg)"),r.textContent=y}g.addEventListener("click",()=>{if(S===3){S=4,C(),A();return}S<3&&(S++,C(),S===2&&M(),S===3&&j())}),H.addEventListener("click",()=>{S>1&&S<4&&(S--,C())}),window.groupRecipeCards=function(){const f=document.querySelectorAll(".service-card[data-recipe-id]");if(f.length===0)return;const p={};f.forEach(y=>{const v=y.dataset.recipeId;p[v]||(p[v]=[]),p[v].push(y)});for(const[y,v]of Object.entries(p))v.length<2||v.forEach((m,r)=>{if(m.style.borderLeft="3px solid rgba(142,68,173,0.5)",r===0){let c=m.querySelector(".recipe-group-label");c||(c=document.createElement("div"),c.className="recipe-group-label",c.style.cssText="position: absolute; top: -8px; left: 12px; font-size: 0.6rem; padding: 1px 8px; border-radius: 8px; background: rgba(142,68,173,0.3); color: #d4a5ff; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;",c.textContent=y.replace(/-/g," "),m.style.position="relative",m.appendChild(c))}})},window.manageRecipe=async function(f,p){const y=`/api/v1/recipes/${f}/${p}`,v=p==="remove"?"DELETE":"POST",m=p==="remove"?`/api/v1/recipes/${f}`:y;if(!(p==="remove"&&!confirm(`Remove the entire ${f} recipe? This will delete all containers and configuration.`)))try{const c=await(await secureFetch(m,{method:v})).json();c.success?(showNotification(`Recipe ${p}: ${c.results?.filter(s=>s.status!=="failed").length||0} components processed`,"success",4e3),window.loadServices&&window.loadServices()):showNotification(`Recipe ${p} failed: ${c.error}`,"error",5e3)}catch(r){showNotification(`Network error: ${r.message}`,"error",5e3)}};const z=document.createElement("style");z.textContent=` +
`}}function x(g,u,f){const m=document.getElementById(`recipe-progress-${g}`);if(!m)return;const p=m.querySelector(".recipe-progress-icon"),d=m.querySelector(".recipe-progress-status");u==="deploying"?(p.textContent="\u23F3",d.style.color="var(--accent)"):u==="success"?(p.textContent="\u2705",d.style.color="var(--ok-fg)"):u==="error"&&(p.textContent="\u274C",d.style.color="var(--bad-fg)"),d.textContent=f}v.addEventListener("click",()=>{if(w===3){w=4,B(),R();return}w<3&&(w++,B(),w===2&&T(),w===3&&j())}),A.addEventListener("click",()=>{w>1&&w<4&&(w--,B())}),window.groupRecipeCards=function(){const g=document.querySelectorAll(".service-card[data-recipe-id]");if(g.length===0)return;const u={};g.forEach(f=>{const m=f.dataset.recipeId;u[m]||(u[m]=[]),u[m].push(f)});for(const[f,m]of Object.entries(u))m.length<2||m.forEach((p,d)=>{if(p.style.borderLeft="3px solid rgba(142,68,173,0.5)",d===0){let c=p.querySelector(".recipe-group-label");c||(c=document.createElement("div"),c.className="recipe-group-label",c.style.cssText="position: absolute; top: -8px; left: 12px; font-size: 0.6rem; padding: 1px 8px; border-radius: 8px; background: rgba(142,68,173,0.3); color: #d4a5ff; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;",c.textContent=f.replace(/-/g," "),p.style.position="relative",p.appendChild(c))}})},window.manageRecipe=async function(g,u){const f=`/api/v1/recipes/${g}/${u}`,m=u==="remove"?"DELETE":"POST",p=u==="remove"?`/api/v1/recipes/${g}`:f;if(!(u==="remove"&&!confirm(`Remove the entire ${g} recipe? This will delete all containers and configuration.`)))try{const c=await(await secureFetch(p,{method:m})).json();c.success?(showNotification(`Recipe ${u}: ${c.results?.filter(i=>i.status!=="failed").length||0} components processed`,"success",4e3),window.loadServices&&window.loadServices()):showNotification(`Recipe ${u} failed: ${c.error}`,"error",5e3)}catch(d){showNotification(`Network error: ${d.message}`,"error",5e3)}};const D=document.createElement("style");D.textContent=` .recipe-step { flex: 1; text-align: center; @@ -550,16 +550,16 @@ Try refreshing in a moment if you see a certificate error.`,"warning",1e4),!1}}; .recipe-step-panel { min-height: 180px; } - `,document.head.appendChild(z),k()})(),(function(){document.getElementById("reload-caddy-top")?.addEventListener("click",async()=>{const b=document.getElementById("reload-caddy-top"),E=b.textContent;try{b.textContent="\u23F3 Reloading...",b.disabled=!0;const N=await secureFetch("/api/v1/caddy/reload",{method:"POST",headers:{"Content-Type":"application/json"}}),S=await N.json();if(N.ok&&S.success)b.textContent="\u2705 Reloaded!",setTimeout(()=>{b.textContent=E,b.disabled=!1},2e3);else throw new Error(S.error||"Reload failed")}catch(N){b.textContent="\u274C Failed",showNotification(`Failed to reload Caddy: ${N.message}`,"error"),setTimeout(()=>{b.textContent=E,b.disabled=!1},2e3)}})})(),(function(){injectModal("error-log-modal",'

\u{1F4CB} Error Logs

Loading error logs...
');const b=document.getElementById("error-log-modal"),E=document.getElementById("error-log-content"),N=document.getElementById("view-error-logs"),S=document.getElementById("error-log-refresh"),T=document.getElementById("error-log-clear"),P=document.getElementById("error-log-close");async function L(){E.innerHTML='
Loading error logs...
';try{const I=await(await fetch("/api/v1/error-logs")).json();I.success&&I.logs?I.logs.length===0?E.innerHTML='
\u2705 No errors logged! Everything is working smoothly.
':E.innerHTML=I.logs.map(k=>` + `,document.head.appendChild(D),b()})(),(function(){document.getElementById("reload-caddy-top")?.addEventListener("click",async()=>{const h=document.getElementById("reload-caddy-top"),E=h.textContent;try{h.textContent="\u23F3 Reloading...",h.disabled=!0;const P=await secureFetch("/api/v1/caddy/reload",{method:"POST",headers:{"Content-Type":"application/json"}}),w=await P.json();if(P.ok&&w.success)h.textContent="\u2705 Reloaded!",setTimeout(()=>{h.textContent=E,h.disabled=!1},2e3);else throw new Error(w.error||"Reload failed")}catch(P){h.textContent="\u274C Failed",showNotification(`Failed to reload Caddy: ${P.message}`,"error"),setTimeout(()=>{h.textContent=E,h.disabled=!1},2e3)}})})(),(function(){injectModal("error-log-modal",'

\u{1F4CB} Error Logs

Loading error logs...
');const h=document.getElementById("error-log-modal"),E=document.getElementById("error-log-content"),P=document.getElementById("view-error-logs"),w=document.getElementById("error-log-refresh"),N=document.getElementById("error-log-clear"),O=document.getElementById("error-log-close");async function z(){E.innerHTML='
Loading error logs...
';try{const L=await(await fetch("/api/v1/error-logs")).json();L.success&&L.logs?L.logs.length===0?E.innerHTML='
\u2705 No errors logged! Everything is working smoothly.
':E.innerHTML=L.logs.map(b=>`
- ${new Date(k.timestamp).toLocaleString()} + ${new Date(b.timestamp).toLocaleString()} ERROR
- ${escapeHtml(k.context)}: ${escapeHtml(k.error)} - ${k.details?`
${escapeHtml(k.details)}`:""} + ${escapeHtml(b.context)}: ${escapeHtml(b.error)} + ${b.details?`
${escapeHtml(b.details)}`:""}
- `).join(""):E.innerHTML='
\u274C Failed to load error logs
'}catch(g){E.innerHTML=`
\u274C Error loading logs: ${escapeHtml(g.message)}
`}}async function H(){if(confirm("Clear all error logs?"))try{(await(await secureFetch("/api/v1/error-logs",{method:"DELETE"})).json()).success?(showNotification("\u2705 Error logs cleared","success",3e3),L()):showNotification("\u274C Failed to clear logs","error",3e3)}catch(g){showNotification(`\u274C Error: ${g.message}`,"error",3e3)}}N?.addEventListener("click",()=>{b.classList.add("show"),L()}),S?.addEventListener("click",L),T?.addEventListener("click",H),wireModal(b,P)})(),(function(){injectModal("container-logs-modal",`
+ `).join(""):E.innerHTML='
\u274C Failed to load error logs
'}catch(v){E.innerHTML=`
\u274C Error loading logs: ${escapeHtml(v.message)}
`}}async function A(){if(confirm("Clear all error logs?"))try{(await(await secureFetch("/api/v1/error-logs",{method:"DELETE"})).json()).success?(showNotification("\u2705 Error logs cleared","success",3e3),z()):showNotification("\u274C Failed to clear logs","error",3e3)}catch(v){showNotification(`\u274C Error: ${v.message}`,"error",3e3)}}P?.addEventListener("click",()=>{h.classList.add("show"),z()}),w?.addEventListener("click",z),N?.addEventListener("click",A),wireModal(h,O)})(),(function(){injectModal("container-logs-modal",`
@@ -609,14 +609,14 @@ Try refreshing in a moment if you see a certificate error.`,"warning",1e4),!1}};
-
`);const b=document.getElementById("container-logs-modal"),E=document.getElementById("cl-container-select"),N=document.getElementById("cl-log-content"),S=document.getElementById("cl-log-search"),T=document.getElementById("cl-log-tail"),P=document.getElementById("cl-refresh"),L=document.getElementById("cl-stream"),H=document.getElementById("cl-download"),g=document.getElementById("cl-clear-search"),I=document.getElementById("cl-close"),k=document.getElementById("cl-close-btn"),x=document.getElementById("cl-stream-status"),$=document.getElementById("cl-stream-indicator"),C=document.getElementById("cl-stream-text"),R=document.getElementById("cl-line-count"),M=document.getElementById("cl-filter-count"),j=document.getElementById("cl-image"),B=document.getElementById("cl-status"),A=document.getElementById("cl-created");let w=null,z=[],f=[],p=null,y=!1,v=null;function m(d){if(!d)return"-";const l=new Date(d);return isNaN(l.getTime())?d:l.toLocaleString()}function r(d){if(!d)return"";const l=document.createElement("div");return l.textContent=d,l.innerHTML}function c(d,l){const D=d.stream==="stderr"?"log-stderr":"log-stdout",O=d.stream==="stderr"?"\u26A0\uFE0F":"\u{1F4E4}";return` -
+
`);const h=document.getElementById("container-logs-modal"),E=document.getElementById("cl-container-select"),P=document.getElementById("cl-log-content"),w=document.getElementById("cl-log-search"),N=document.getElementById("cl-log-tail"),O=document.getElementById("cl-refresh"),z=document.getElementById("cl-stream"),A=document.getElementById("cl-download"),v=document.getElementById("cl-clear-search"),L=document.getElementById("cl-close"),b=document.getElementById("cl-close-btn"),M=document.getElementById("cl-stream-status"),k=document.getElementById("cl-stream-indicator"),B=document.getElementById("cl-stream-text"),S=document.getElementById("cl-line-count"),T=document.getElementById("cl-filter-count"),j=document.getElementById("cl-image"),H=document.getElementById("cl-status"),R=document.getElementById("cl-created");let x=null,D=[],g=[],u=null,f=!1,m=null;function p(s){if(!s)return"-";const l=new Date(s);return isNaN(l.getTime())?s:l.toLocaleString()}function d(s){if(!s)return"";const l=document.createElement("div");return l.textContent=s,l.innerHTML}function c(s,l){const C=s.stream==="stderr"?"log-stderr":"log-stdout",I=s.stream==="stderr"?"\u26A0\uFE0F":"\u{1F4E4}";return` +
${l+1} - ${O} - ${r(d.text)} + ${I} + ${d(s.text)}
- `}function s(d,l=""){if(!d||d.length===0){N.innerHTML='
No logs available
',R.textContent="0 lines",M.textContent="0 filtered";return}if(z=d,f=l?d.filter(D=>D.text&&D.text.toLowerCase().includes(l.toLowerCase())):d,R.textContent=`${d.length} lines`,M.textContent=l?`${f.length} of ${d.length} shown`:`${d.length} shown`,f.length===0){N.innerHTML=`
No logs match "${r(l)}"
`;return}N.innerHTML=f.map((D,O)=>c(D,O)).join(""),N.scrollTop=N.scrollHeight}async function h(){try{const l=(await getJSON("/api/v1/logs/containers")).containers||[],D=E.value;E.innerHTML='',l.forEach(O=>{const F=document.createElement("option");F.value=O.id,F.textContent=`${O.name} (${O.image.split(":")[0]}) - ${O.status}`,F.dataset.name=O.name,F.dataset.image=O.image,F.dataset.status=O.status,F.dataset.created=O.created,E.appendChild(F)}),D&&E.querySelector(`option[value="${D}"]`)&&(E.value=D,u(D))}catch(d){console.error("Failed to load containers:",d)}}function u(d){const l=E.querySelector(`option[value="${d}"]`);l&&(j.textContent=l.dataset.image||"-",B.textContent=l.dataset.status||"-",B.style.color=l.dataset.status==="running"?"var(--ok-fg, #4ade80)":"var(--bad-fg, #ef4444)",A.textContent=m(l.dataset.created))}async function a(){const d=E.value;if(!d){N.innerHTML='
Select a container to view logs
';return}n(),w=d,u(d);const l=T.value,D=S.value.trim();N.innerHTML='
Loading logs...
';try{const O=`/api/v1/logs/container/${d}${l!=="all"?`?tail=${l}`:""}`,F=await getJSON(O);F.logs&&F.logs.length>0?s(F.logs,D):(N.innerHTML='
No logs found for this container
',R.textContent="0 lines",M.textContent="0 filtered")}catch(O){N.innerHTML=`
Error loading logs: ${r(O.message)}
`}}function e(){const d=E.value;if(!d)return;n(),y=!0,L.textContent="\u23F9 Stop",x.style.display="flex",$.textContent="\u{1F7E2}",C.textContent="Connecting...";const l=`/api/v1/logs/stream/${d}`;p=new EventSource(l),p.onopen=()=>{$.textContent="\u{1F7E2}",C.textContent="Connected - streaming logs"},p.onmessage=D=>{try{const O=JSON.parse(D.data);if(O.error){$.textContent="\u{1F534}",C.textContent=`Error: ${O.error}`;return}z.push(O),f.push(O),R.textContent=`${z.length} lines`,M.textContent=`${f.length} shown`;const F=S.value.trim();if(!F||O.text&&O.text.toLowerCase().includes(F.toLowerCase())){const q=document.createElement("div");q.innerHTML=c(O,f.length-1);const U=q.firstElementChild;U.style.background="#1a3a1a",N.appendChild(U),N.scrollTop=N.scrollHeight}}catch(O){console.error("Error parsing log:",O)}},p.onerror=()=>{$.textContent="\u{1F534}",C.textContent="Disconnected",y=!1,L.textContent="\u25B6 Stream"},b._eventSource=p}function n(){p&&(p.close(),p=null),b._eventSource&&(b._eventSource.close(),b._eventSource=null),y=!1,L.textContent="\u25B6 Stream",x.style.display="none"}function t(){if(!z||z.length===0){showNotification("No logs to download","error");return}const d=E.querySelector(`option[value="${w}"]`)?.dataset.name||w,l=new Date().toISOString().replace(/[:.]/g,"-"),D=`${d}-logs-${l}.txt`,O=z.map(G=>{const W=G.timestamp||"",X=G.stream==="stderr"?"[ERR]":"[OUT]";return`${W?W+" ":""}${X} ${G.text}`}).join(` -`),F=new Blob([O],{type:"text/plain"}),q=URL.createObjectURL(F),U=document.createElement("a");U.href=q,U.download=D,document.body.appendChild(U),U.click(),document.body.removeChild(U),URL.revokeObjectURL(q),showNotification(`Downloaded ${z.length} log lines`,"success")}E?.addEventListener("change",()=>{a()}),T?.addEventListener("change",()=>{a()}),P?.addEventListener("click",()=>{a()}),L?.addEventListener("click",()=>{y?n():e()}),H?.addEventListener("click",()=>{t()}),g?.addEventListener("click",()=>{S.value="",s(z,"")}),S?.addEventListener("input",()=>{clearTimeout(v),v=setTimeout(()=>{s(z,S.value.trim())},300)}),S?.addEventListener("keydown",d=>{d.key==="Escape"&&(S.value="",s(z,""))}),document.getElementById("view-container-logs")?.addEventListener("click",()=>{b.classList.add("show"),h()});function o(){n(),b.classList.remove("show")}I?.addEventListener("click",o),k?.addEventListener("click",o),document.addEventListener("keydown",d=>{d.key==="Escape"&&b.classList.contains("show")&&o()}),b.addEventListener("click",d=>{d.target===b&&o()}),window.openContainerLogsModal=function(d,l){b.classList.add("show"),h().then(()=>{const D=Array.from(E.options).find(O=>O.value===d||O.dataset.name===l);D?(E.value=D.value,u(D.value),a()):d?(w=d,j.textContent=l||d,B.textContent="-",A.textContent="-",a()):N.innerHTML='
Select a container to view logs
'})}})(),(function(){injectModal("snapshot-modal",`
+ `}function i(s,l=""){if(!s||s.length===0){P.innerHTML='
No logs available
',S.textContent="0 lines",T.textContent="0 filtered";return}if(D=s,g=l?s.filter(C=>C.text&&C.text.toLowerCase().includes(l.toLowerCase())):s,S.textContent=`${s.length} lines`,T.textContent=l?`${g.length} of ${s.length} shown`:`${s.length} shown`,g.length===0){P.innerHTML=`
No logs match "${d(l)}"
`;return}P.innerHTML=g.map((C,I)=>c(C,I)).join(""),P.scrollTop=P.scrollHeight}async function $(){try{const l=(await getJSON("/api/v1/logs/containers")).containers||[],C=E.value;E.innerHTML='',l.forEach(I=>{const F=document.createElement("option");F.value=I.id,F.textContent=`${I.name} (${I.image.split(":")[0]}) - ${I.status}`,F.dataset.name=I.name,F.dataset.image=I.image,F.dataset.status=I.status,F.dataset.created=I.created,E.appendChild(F)}),C&&E.querySelector(`option[value="${C}"]`)&&(E.value=C,y(C))}catch(s){console.error("Failed to load containers:",s)}}function y(s){const l=E.querySelector(`option[value="${s}"]`);l&&(j.textContent=l.dataset.image||"-",H.textContent=l.dataset.status||"-",H.style.color=l.dataset.status==="running"?"var(--ok-fg, #4ade80)":"var(--bad-fg, #ef4444)",R.textContent=p(l.dataset.created))}async function n(){const s=E.value;if(!s){P.innerHTML='
Select a container to view logs
';return}o(),x=s,y(s);const l=N.value,C=w.value.trim();P.innerHTML='
Loading logs...
';try{const I=`/api/v1/logs/container/${s}${l!=="all"?`?tail=${l}`:""}`,F=await getJSON(I);F.logs&&F.logs.length>0?i(F.logs,C):(P.innerHTML='
No logs found for this container
',S.textContent="0 lines",T.textContent="0 filtered")}catch(I){P.innerHTML=`
Error loading logs: ${d(I.message)}
`}}function e(){const s=E.value;if(!s)return;o(),f=!0,z.textContent="\u23F9 Stop",M.style.display="flex",k.textContent="\u{1F7E2}",B.textContent="Connecting...";const l=`/api/v1/logs/stream/${s}`;u=new EventSource(l),u.onopen=()=>{k.textContent="\u{1F7E2}",B.textContent="Connected - streaming logs"},u.onmessage=C=>{try{const I=JSON.parse(C.data);if(I.error){k.textContent="\u{1F534}",B.textContent=`Error: ${I.error}`;return}D.push(I),g.push(I),S.textContent=`${D.length} lines`,T.textContent=`${g.length} shown`;const F=w.value.trim();if(!F||I.text&&I.text.toLowerCase().includes(F.toLowerCase())){const q=document.createElement("div");q.innerHTML=c(I,g.length-1);const U=q.firstElementChild;U.style.background="#1a3a1a",P.appendChild(U),P.scrollTop=P.scrollHeight}}catch(I){console.error("Error parsing log:",I)}},u.onerror=()=>{k.textContent="\u{1F534}",B.textContent="Disconnected",f=!1,z.textContent="\u25B6 Stream"},h._eventSource=u}function o(){u&&(u.close(),u=null),h._eventSource&&(h._eventSource.close(),h._eventSource=null),f=!1,z.textContent="\u25B6 Stream",M.style.display="none"}function a(){if(!D||D.length===0){showNotification("No logs to download","error");return}const s=E.querySelector(`option[value="${x}"]`)?.dataset.name||x,l=new Date().toISOString().replace(/[:.]/g,"-"),C=`${s}-logs-${l}.txt`,I=D.map(G=>{const J=G.timestamp||"",X=G.stream==="stderr"?"[ERR]":"[OUT]";return`${J?J+" ":""}${X} ${G.text}`}).join(` +`),F=new Blob([I],{type:"text/plain"}),q=URL.createObjectURL(F),U=document.createElement("a");U.href=q,U.download=C,document.body.appendChild(U),U.click(),document.body.removeChild(U),URL.revokeObjectURL(q),showNotification(`Downloaded ${D.length} log lines`,"success")}E?.addEventListener("change",()=>{n()}),N?.addEventListener("change",()=>{n()}),O?.addEventListener("click",()=>{n()}),z?.addEventListener("click",()=>{f?o():e()}),A?.addEventListener("click",()=>{a()}),v?.addEventListener("click",()=>{w.value="",i(D,"")}),w?.addEventListener("input",()=>{clearTimeout(m),m=setTimeout(()=>{i(D,w.value.trim())},300)}),w?.addEventListener("keydown",s=>{s.key==="Escape"&&(w.value="",i(D,""))}),document.getElementById("view-container-logs")?.addEventListener("click",()=>{h.classList.add("show"),$()});function t(){o(),h.classList.remove("show")}L?.addEventListener("click",t),b?.addEventListener("click",t),document.addEventListener("keydown",s=>{s.key==="Escape"&&h.classList.contains("show")&&t()}),h.addEventListener("click",s=>{s.target===h&&t()}),window.openContainerLogsModal=function(s,l){h.classList.add("show"),$().then(()=>{const C=Array.from(E.options).find(I=>I.value===s||I.dataset.name===l);C?(E.value=C.value,y(C.value),n()):s?(x=s,j.textContent=l||s,H.textContent="-",R.textContent="-",n()):P.innerHTML='
Select a container to view logs
'})}})(),(function(){injectModal("snapshot-modal",`

\u{1F4BE} Container Snapshots

-
`);const b=document.getElementById("snapshot-modal"),E=document.getElementById("snapshot-btn"),N=document.getElementById("snapshot-close"),S=document.getElementById("snapshot-container-select"),T=document.getElementById("snapshot-details"),P=document.getElementById("snapshot-create-btn"),L=document.getElementById("snapshot-create-status");let H=null;async function g(){try{const R=await(await fetch("/api/v1/containers")).json();if(!R.success||!R.containers)return;S.innerHTML='';for(const M of R.containers){const j=document.createElement("option");j.value=M.id,j.textContent=`${M.name||M.id} (${M.image||"unknown"})`,j.dataset.name=M.name,j.dataset.image=M.image,j.dataset.status=M.status,j.dataset.created=M.created,S.appendChild(j)}}catch(C){console.error("Failed to load containers:",C)}}function I(C){if(!C||!C.value){T.style.display="none",H=null;return}H=C.value,document.getElementById("snapshot-image").textContent=C.dataset.image||"-",document.getElementById("snapshot-status").textContent=C.dataset.status||"-",document.getElementById("snapshot-created").textContent=C.dataset.created?new Date(C.dataset.created*1e3).toLocaleString():"-",document.getElementById("snapshot-id").textContent=C.value.substring(0,12),T.style.display=""}async function k(){if(!H){L.textContent="Please select a container first",L.style.color="var(--bad-fg)";return}const C=document.getElementById("snapshot-name").value.trim();if(!C){L.textContent="Please enter a snapshot name",L.style.color="var(--bad-fg)";return}const R=document.getElementById("snapshot-leave-running").checked;P.disabled=!0,P.textContent="Creating...",L.textContent="";try{const j=await(await fetch(`/api/v1/containers/${encodeURIComponent(H)}/checkpoint`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:C,leaveRunning:R})})).json();j.success?(L.textContent=`\u2713 Snapshot "${C}" created successfully`,L.style.color="var(--ok-fg)",document.getElementById("snapshot-name").value=""):(L.textContent=`\u2717 Failed: ${j.error||"Unknown error"}`,L.style.color="var(--bad-fg)")}catch(M){L.textContent=`\u2717 Error: ${M.message}`,L.style.color="var(--bad-fg)"}finally{P.disabled=!1,P.textContent="\u{1F4BE} Create Snapshot"}}function x(){b.classList.add("show"),g()}function $(){b.classList.remove("show"),T.style.display="none",H=null,S.selectedIndex=0}E?.addEventListener("click",x),N?.addEventListener("click",$),wireModal(b,N),S?.addEventListener("change",C=>{const R=S.options[S.selectedIndex];I(R)}),P?.addEventListener("click",k),b?.querySelectorAll(".panel-tab").forEach(C=>{C.addEventListener("click",()=>{b.querySelectorAll(".panel-tab").forEach(R=>R.classList.remove("active")),b.querySelectorAll(".panel-section").forEach(R=>R.classList.remove("active")),C.classList.add("active"),b.querySelector(`#${C.dataset.panel}`).classList.add("active")})})})(),(function(){injectModal("arr-setup-modal",`
+
`);const h=document.getElementById("snapshot-modal"),E=document.getElementById("snapshot-btn"),P=document.getElementById("snapshot-close"),w=document.getElementById("snapshot-container-select"),N=document.getElementById("snapshot-details"),O=document.getElementById("snapshot-create-btn"),z=document.getElementById("snapshot-create-status");let A=null;async function v(){try{const S=await(await fetch("/api/v1/containers")).json();if(!S.success||!S.containers)return;w.innerHTML='';for(const T of S.containers){const j=document.createElement("option");j.value=T.id,j.textContent=`${T.name||T.id} (${T.image||"unknown"})`,j.dataset.name=T.name,j.dataset.image=T.image,j.dataset.status=T.status,j.dataset.created=T.created,w.appendChild(j)}}catch(B){console.error("Failed to load containers:",B)}}function L(B){if(!B||!B.value){N.style.display="none",A=null;return}A=B.value,document.getElementById("snapshot-image").textContent=B.dataset.image||"-",document.getElementById("snapshot-status").textContent=B.dataset.status||"-",document.getElementById("snapshot-created").textContent=B.dataset.created?new Date(B.dataset.created*1e3).toLocaleString():"-",document.getElementById("snapshot-id").textContent=B.value.substring(0,12),N.style.display=""}async function b(){if(!A){z.textContent="Please select a container first",z.style.color="var(--bad-fg)";return}const B=document.getElementById("snapshot-name").value.trim();if(!B){z.textContent="Please enter a snapshot name",z.style.color="var(--bad-fg)";return}const S=document.getElementById("snapshot-leave-running").checked;O.disabled=!0,O.textContent="Creating...",z.textContent="";try{const j=await(await fetch(`/api/v1/containers/${encodeURIComponent(A)}/checkpoint`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:B,leaveRunning:S})})).json();j.success?(z.textContent=`\u2713 Snapshot "${B}" created successfully`,z.style.color="var(--ok-fg)",document.getElementById("snapshot-name").value=""):(z.textContent=`\u2717 Failed: ${j.error||"Unknown error"}`,z.style.color="var(--bad-fg)")}catch(T){z.textContent=`\u2717 Error: ${T.message}`,z.style.color="var(--bad-fg)"}finally{O.disabled=!1,O.textContent="\u{1F4BE} Create Snapshot"}}function M(){h.classList.add("show"),v()}function k(){h.classList.remove("show"),N.style.display="none",A=null,w.selectedIndex=0}E?.addEventListener("click",M),P?.addEventListener("click",k),wireModal(h,P),w?.addEventListener("change",B=>{const S=w.options[w.selectedIndex];L(S)}),O?.addEventListener("click",b),h?.querySelectorAll(".panel-tab").forEach(B=>{B.addEventListener("click",()=>{h.querySelectorAll(".panel-tab").forEach(S=>S.classList.remove("active")),h.querySelectorAll(".panel-section").forEach(S=>S.classList.remove("active")),B.classList.add("active"),h.querySelector(`#${B.dataset.panel}`).classList.add("active")})})})(),(function(){injectModal("arr-setup-modal",`

\u{1F3AC} Smart Arr Connect

@@ -749,73 +749,73 @@ Try refreshing in a moment if you see a certificate error.`,"warning",1e4),!1}};

-
`);const b=document.getElementById("arr-setup-modal"),E=document.getElementById("arr-setup-btn"),N=document.getElementById("arr-setup-cancel"),S=document.getElementById("smart-connect-btn"),T=document.getElementById("smart-phase-detect"),P=document.getElementById("smart-phase-credentials"),L=document.getElementById("smart-phase-progress"),H=document.getElementById("smart-phase-results"),g=document.getElementById("smart-detect-results"),I=document.getElementById("smart-credential-inputs"),k=document.getElementById("smart-progress-steps"),x=document.getElementById("smart-results-content"),$=document.getElementById("smart-plex-libraries"),C=document.getElementById("smart-retry-btn");let R=null;const M={plex:"\u{1F3AC}",radarr:"\u{1F3AC}",sonarr:"\u{1F4FA}",prowlarr:"\u{1F50D}",seerr:"\u{1F4CB}"},j={plex:"Plex",radarr:"Radarr (Movies)",sonarr:"Sonarr (TV)",prowlarr:"Prowlarr (Indexers)",seerr:"Seerr"};function B(v){T.style.display=v==="detect"?"block":"none",P.style.display=v==="credentials"?"block":"none",L.style.display=v==="progress"?"block":"none",H.style.display=v==="results"?"block":"none"}function A(v){const m={connected:{bg:"var(--ok-fg)",icon:"✓",text:"Connected"},needs_key:{bg:"#f39c12",icon:"🔑",text:"Needs API Key"},not_found:{bg:"var(--muted)",icon:"—",text:"Not Found"},error:{bg:"var(--bad-fg)",icon:"✗",text:"Error"}},r=m[v]||m.not_found;return`${r.icon} ${r.text}`}async function w(){B("detect"),g.style.display="none";try{if(R=await(await fetch("/api/v1/arr/smart-detect")).json(),!R.success){g.innerHTML=`
Detection failed: ${escapeHtml(R.error)}
`,g.style.display="block";return}let m='
';for(const[c,s]of Object.entries(R.services)){const h=M[c]||"\u{1F4E6}",u=j[c]||c,a=s.source?`${escapeHtml(s.source)}`:"",e=s.version?`v${escapeHtml(s.version)}`:"",n=(s.hasApiKey||s.hasToken)&&s.status==="connected"?'Key saved':"";m+=`
- ${h} +
`);const h=document.getElementById("arr-setup-modal"),E=document.getElementById("arr-setup-btn"),P=document.getElementById("arr-setup-cancel"),w=document.getElementById("smart-connect-btn"),N=document.getElementById("smart-phase-detect"),O=document.getElementById("smart-phase-credentials"),z=document.getElementById("smart-phase-progress"),A=document.getElementById("smart-phase-results"),v=document.getElementById("smart-detect-results"),L=document.getElementById("smart-credential-inputs"),b=document.getElementById("smart-progress-steps"),M=document.getElementById("smart-results-content"),k=document.getElementById("smart-plex-libraries"),B=document.getElementById("smart-retry-btn");let S=null;const T={plex:"\u{1F3AC}",radarr:"\u{1F3AC}",sonarr:"\u{1F4FA}",prowlarr:"\u{1F50D}",seerr:"\u{1F4CB}"},j={plex:"Plex",radarr:"Radarr (Movies)",sonarr:"Sonarr (TV)",prowlarr:"Prowlarr (Indexers)",seerr:"Seerr"};function H(m){N.style.display=m==="detect"?"block":"none",O.style.display=m==="credentials"?"block":"none",z.style.display=m==="progress"?"block":"none",A.style.display=m==="results"?"block":"none"}function R(m){const p={connected:{bg:"var(--ok-fg)",icon:"✓",text:"Connected"},needs_key:{bg:"#f39c12",icon:"🔑",text:"Needs API Key"},not_found:{bg:"var(--muted)",icon:"—",text:"Not Found"},error:{bg:"var(--bad-fg)",icon:"✗",text:"Error"}},d=p[m]||p.not_found;return`${d.icon} ${d.text}`}async function x(){H("detect"),v.style.display="none";try{if(S=await(await fetch("/api/v1/arr/smart-detect")).json(),!S.success){v.innerHTML=`
Detection failed: ${escapeHtml(S.error)}
`,v.style.display="block";return}let p='
';for(const[c,i]of Object.entries(S.services)){const $=T[c]||"\u{1F4E6}",y=j[c]||c,n=i.source?`${escapeHtml(i.source)}`:"",e=i.version?`v${escapeHtml(i.version)}`:"",o=(i.hasApiKey||i.hasToken)&&i.status==="connected"?'Key saved':"";p+=`
+ ${$}
-
${u}
+
${y}
- ${a} ${e} ${n} + ${n} ${e} ${o}
- ${A(s.status)} -
`}m+="
";const r=R.summary;m+=`
- ${escapeHtml(String(r.fullyConnected))}/${escapeHtml(String(r.totalDetected+(5-r.totalDetected)))} services detected · - ${escapeHtml(String(r.fullyConnected))} connected${r.needsApiKey>0?` · ${escapeHtml(String(r.needsApiKey))} needs API key`:""} -
`,g.innerHTML=m,g.style.display="block",z(R),setTimeout(()=>{B("credentials")},800)}catch(v){g.innerHTML=`
Error: ${escapeHtml(v.message)}
`,g.style.display="block"}}function z(v){let m="";const r=v.services,c=["radarr","sonarr","prowlarr"];for(const u of c){const a=r[u];if(!a||a.status==="not_found"&&!a.url)continue;const e=M[u],n=j[u],t=a.status==="connected";m+=`
+ ${R(i.status)} +
`}p+="
";const d=S.summary;p+=`
+ ${escapeHtml(String(d.fullyConnected))}/${escapeHtml(String(d.totalDetected+(5-d.totalDetected)))} services detected · + ${escapeHtml(String(d.fullyConnected))} connected${d.needsApiKey>0?` · ${escapeHtml(String(d.needsApiKey))} needs API key`:""} +
`,v.innerHTML=p,v.style.display="block",D(S),setTimeout(()=>{H("credentials")},800)}catch(m){v.innerHTML=`
Error: ${escapeHtml(m.message)}
`,v.style.display="block"}}function D(m){let p="";const d=m.services,c=["radarr","sonarr","prowlarr"];for(const y of c){const n=d[y];if(!n||n.status==="not_found"&&!n.url)continue;const e=T[y],o=j[y],a=n.status==="connected";p+=`
${e} - ${n} - - ${t?'✓ Connected':""} + ${o} + + ${a?'✓ Connected':""}
-
-
- -
`}const s=r.plex;if(s){const u=s.status==="connected";m+=`
+ +
`}const i=d.plex;if(i){const y=i.status==="connected";p+=`
\u{1F3AC} Plex - ${A(s.status)} - ${escapeHtml(s.source||"")} + ${R(i.status)} + ${escapeHtml(i.source||"")}
-
`}const h=r.seerr;if(h){const u=h.status==="connected";let a="";if(h.configuredServices){const e=h.configuredServices;a=`
+
`}const $=d.seerr;if($){const y=$.status==="connected";let n="";if($.configuredServices){const e=$.configuredServices;n=`
Configured: ${e.radarr?"✓ Radarr":"✗ Radarr"} · ${e.sonarr?"✓ Sonarr":"✗ Sonarr"} · ${e.plex?"✓ Plex":"✗ Plex"} -
`}m+=`
+
`}p+=`
\u{1F4CB} Seerr - ${A(h.status)} + ${R($.status)}
- ${a} -
`}I.innerHTML=m}window.smartTestConnection=async function(v){const m=document.getElementById(`smart-${v}-url`),r=document.getElementById(`smart-${v}-key`),c=document.getElementById(`smart-${v}-status`),s=m?.value.trim(),h=r?.value.trim();if(!s||!h){c.innerHTML='Enter URL and API key';return}c.innerHTML='';try{const a=await(await secureFetch("/api/v1/arr/test-connection",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({service:v,url:s,apiKey:h})})).json();a.success?c.innerHTML=`✓ ${escapeHtml(a.appName||"Connected")} v${escapeHtml(a.version||"")}`:c.innerHTML=`✗ ${escapeHtml(a.error)}`}catch(u){c.innerHTML=`✗ ${escapeHtml(u.message)}`}};async function f(){B("progress"),k.innerHTML='
Connecting services...
';const v={};for(const r of["radarr","sonarr","prowlarr"]){const c=document.getElementById(`smart-${r}-url`)?.value.trim(),s=document.getElementById(`smart-${r}-key`)?.value.trim();s&&c?v[r]={apiKey:s,url:c}:s&&(v[r]={apiKey:s})}const m={services:Object.keys(v).length>0?v:void 0,configurePlex:document.getElementById("smart-opt-plex")?.checked,configureProwlarr:document.getElementById("smart-opt-prowlarr")?.checked,configureSeerr:document.getElementById("smart-opt-seerr")?.checked,saveCredentials:document.getElementById("smart-opt-save")?.checked};try{const c=await(await secureFetch("/api/v1/arr/smart-connect",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(m)})).json();let s="";for(const h of c.steps||[]){const u=h.status==="success"?'':'',a=h.status==="success"?"var(--muted)":"var(--bad-fg)";s+=`
- ${u} - ${escapeHtml(h.step)} - ${escapeHtml(h.details||"")} -
`}k.innerHTML=s,setTimeout(()=>p(c),500)}catch(r){k.innerHTML=`
Connection error: ${escapeHtml(r.message)}
`}}function p(v){B("results");const m=v.summary||{},r=m.failed===0&&m.succeeded>0,c=r?"var(--ok-fg)":"#f39c12",s=r?"✓":"⚠",h=r?"All Connected!":`${escapeHtml(String(m.succeeded))}/${escapeHtml(String(m.totalSteps))} Steps Succeeded`;let u=`
-
${s}
-
${h}
-
${escapeHtml(String(m.succeeded))} succeeded, ${escapeHtml(String(m.failed))} failed
-
`;u+='
';for(const a of v.steps||[]){const e=a.status==="success"?'':'';u+=`
- ${e} ${escapeHtml(a.step)} ${escapeHtml(a.details||"")} -
`}u+="
",x.innerHTML=u,C.style.display=m.failed>0?"block":"none",v.steps?.some(a=>a.step.includes("Plex")&&a.status==="success")&&y()}async function y(){try{const m=await(await fetch("/api/v1/plex/libraries")).json();if(m.success&&m.libraries?.length>0){let r=`
-

\u{1F3AC} ${escapeHtml(m.serverName)} Libraries

-
`;for(const c of m.libraries){const s=c.type==="movie"?"\u{1F3AC}":c.type==="show"?"\u{1F4FA}":"\u{1F3B5}";r+=`
- ${s} ${escapeHtml(c.title)} + ${n} +
`}L.innerHTML=p}window.smartTestConnection=async function(m){const p=document.getElementById(`smart-${m}-url`),d=document.getElementById(`smart-${m}-key`),c=document.getElementById(`smart-${m}-status`),i=p?.value.trim(),$=d?.value.trim();if(!i||!$){c.innerHTML='Enter URL and API key';return}c.innerHTML='';try{const n=await(await secureFetch("/api/v1/arr/test-connection",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({service:m,url:i,apiKey:$})})).json();n.success?c.innerHTML=`✓ ${escapeHtml(n.appName||"Connected")} v${escapeHtml(n.version||"")}`:c.innerHTML=`✗ ${escapeHtml(n.error)}`}catch(y){c.innerHTML=`✗ ${escapeHtml(y.message)}`}};async function g(){H("progress"),b.innerHTML='
Connecting services...
';const m={};for(const d of["radarr","sonarr","prowlarr"]){const c=document.getElementById(`smart-${d}-url`)?.value.trim(),i=document.getElementById(`smart-${d}-key`)?.value.trim();i&&c?m[d]={apiKey:i,url:c}:i&&(m[d]={apiKey:i})}const p={services:Object.keys(m).length>0?m:void 0,configurePlex:document.getElementById("smart-opt-plex")?.checked,configureProwlarr:document.getElementById("smart-opt-prowlarr")?.checked,configureSeerr:document.getElementById("smart-opt-seerr")?.checked,saveCredentials:document.getElementById("smart-opt-save")?.checked};try{const c=await(await secureFetch("/api/v1/arr/smart-connect",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(p)})).json();let i="";for(const $ of c.steps||[]){const y=$.status==="success"?'':'',n=$.status==="success"?"var(--muted)":"var(--bad-fg)";i+=`
+ ${y} + ${escapeHtml($.step)} + ${escapeHtml($.details||"")} +
`}b.innerHTML=i,setTimeout(()=>u(c),500)}catch(d){b.innerHTML=`
Connection error: ${escapeHtml(d.message)}
`}}function u(m){H("results");const p=m.summary||{},d=p.failed===0&&p.succeeded>0,c=d?"var(--ok-fg)":"#f39c12",i=d?"✓":"⚠",$=d?"All Connected!":`${escapeHtml(String(p.succeeded))}/${escapeHtml(String(p.totalSteps))} Steps Succeeded`;let y=`
+
${i}
+
${$}
+
${escapeHtml(String(p.succeeded))} succeeded, ${escapeHtml(String(p.failed))} failed
+
`;y+='
';for(const n of m.steps||[]){const e=n.status==="success"?'':'';y+=`
+ ${e} ${escapeHtml(n.step)} ${escapeHtml(n.details||"")} +
`}y+="
",M.innerHTML=y,B.style.display=p.failed>0?"block":"none",m.steps?.some(n=>n.step.includes("Plex")&&n.status==="success")&&f()}async function f(){try{const p=await(await fetch("/api/v1/plex/libraries")).json();if(p.success&&p.libraries?.length>0){let d=`
+

\u{1F3AC} ${escapeHtml(p.serverName)} Libraries

+
`;for(const c of p.libraries){const i=c.type==="movie"?"\u{1F3AC}":c.type==="show"?"\u{1F4FA}":"\u{1F3B5}";d+=`
+ ${i} ${escapeHtml(c.title)} ${escapeHtml(String(c.count))} items -
`}r+="
",$.innerHTML=r,$.style.display="block"}}catch{}}E?.addEventListener("click",()=>{b.classList.add("show"),$.style.display="none",w()}),wireModal(b,N),S?.addEventListener("click",f),C?.addEventListener("click",f)})(),(function(){const b=new ErrorHandler;injectModal("notifications-modal",`
+
`}d+="
",k.innerHTML=d,k.style.display="block"}}catch{}}E?.addEventListener("click",()=>{h.classList.add("show"),k.style.display="none",x()}),wireModal(h,P),w?.addEventListener("click",g),B?.addEventListener("click",g)})(),(function(){const h=new ErrorHandler;injectModal("notifications-modal",`

\u{1F514} Notification Settings

@@ -981,6 +981,9 @@ Try refreshing in a moment if you see a certificate error.`,"warning",1e4),!1}}; +
@@ -988,22 +991,24 @@ Try refreshing in a moment if you see a certificate error.`,"warning",1e4),!1}};
No notifications yet
+
-
`);const E=document.getElementById("notifications-modal"),N=document.getElementById("manage-notifications"),S=document.getElementById("notifications-save"),T=document.getElementById("notifications-cancel");["discord","telegram","ntfy","email"].forEach(x=>{const $=document.getElementById(`${x}-enabled`),C=document.getElementById(`${x}-config`);$?.addEventListener("change",()=>{C.style.display=$.checked?"block":"none"})});const P=document.getElementById("health-check-enabled"),L=document.getElementById("health-check-config");P?.addEventListener("change",()=>{L.style.opacity=P.checked?"1":"0.5"});async function H(){try{const $=await(await fetch("/api/v1/notifications/config")).json();if($.success){const C=$.config;document.getElementById("notifications-enabled").checked=C.enabled,document.getElementById("discord-enabled").checked=C.providers?.discord?.enabled||!1,document.getElementById("telegram-enabled").checked=C.providers?.telegram?.enabled||!1,document.getElementById("ntfy-enabled").checked=C.providers?.ntfy?.enabled||!1,document.getElementById("email-enabled").checked=C.providers?.email?.enabled||!1,document.getElementById("discord-config").style.display=C.providers?.discord?.enabled?"block":"none",document.getElementById("telegram-config").style.display=C.providers?.telegram?.enabled?"block":"none",document.getElementById("ntfy-config").style.display=C.providers?.ntfy?.enabled?"block":"none",document.getElementById("email-config").style.display=C.providers?.email?.enabled?"block":"none",C.providers?.ntfy?.serverUrl&&(document.getElementById("ntfy-server").value=C.providers.ntfy.serverUrl),C.providers?.email?.host&&(document.getElementById("email-host").value=C.providers.email.host),C.providers?.email?.from&&(document.getElementById("email-from").value=C.providers.email.from),document.getElementById("health-check-enabled").checked=C.healthCheck?.enabled||!1,C.healthCheck?.intervalMinutes&&(document.getElementById("health-check-interval").value=C.healthCheck.intervalMinutes),C.healthCheck?.lastCheck&&(document.getElementById("health-check-status").textContent=`Last check: ${new Date(C.healthCheck.lastCheck).toLocaleString()}`),document.getElementById("event-container-down").checked=C.events?.containerDown!==!1,document.getElementById("event-container-up").checked=C.events?.containerUp!==!1,document.getElementById("event-deploy-success").checked=C.events?.deploymentSuccess!==!1,document.getElementById("event-deploy-failed").checked=C.events?.deploymentFailed!==!1}}catch(x){b.logError("[Notifications] Load Config",x,{function:"loadConfig"})}}async function g(){try{const $=await(await fetch("/api/v1/notifications/history?limit=10")).json(),C=document.getElementById("notification-history");$.success&&$.history?.length>0?C.innerHTML=$.history.map(R=>{const M=new Date(R.timestamp).toLocaleString();return` +
`);const E=document.getElementById("notifications-modal"),P=document.getElementById("manage-notifications"),w=document.getElementById("notifications-save"),N=document.getElementById("notifications-cancel");["discord","telegram","ntfy","email"].forEach(k=>{const B=document.getElementById(`${k}-enabled`),S=document.getElementById(`${k}-config`);B?.addEventListener("change",()=>{S.style.display=B.checked?"block":"none"})});const O=document.getElementById("health-check-enabled"),z=document.getElementById("health-check-config");O?.addEventListener("change",()=>{z.style.opacity=O.checked?"1":"0.5"});async function A(){try{const B=await(await fetch("/api/v1/notifications/config")).json();if(B.success){const S=B.config;document.getElementById("notifications-enabled").checked=S.enabled,document.getElementById("discord-enabled").checked=S.providers?.discord?.enabled||!1,document.getElementById("telegram-enabled").checked=S.providers?.telegram?.enabled||!1,document.getElementById("ntfy-enabled").checked=S.providers?.ntfy?.enabled||!1,document.getElementById("email-enabled").checked=S.providers?.email?.enabled||!1,document.getElementById("discord-config").style.display=S.providers?.discord?.enabled?"block":"none",document.getElementById("telegram-config").style.display=S.providers?.telegram?.enabled?"block":"none",document.getElementById("ntfy-config").style.display=S.providers?.ntfy?.enabled?"block":"none",document.getElementById("email-config").style.display=S.providers?.email?.enabled?"block":"none",S.providers?.ntfy?.serverUrl&&(document.getElementById("ntfy-server").value=S.providers.ntfy.serverUrl),S.providers?.email?.host&&(document.getElementById("email-host").value=S.providers.email.host),S.providers?.email?.from&&(document.getElementById("email-from").value=S.providers.email.from),document.getElementById("health-check-enabled").checked=S.healthCheck?.enabled||!1,S.healthCheck?.intervalMinutes&&(document.getElementById("health-check-interval").value=S.healthCheck.intervalMinutes),S.healthCheck?.lastCheck&&(document.getElementById("health-check-status").textContent=`Last check: ${new Date(S.healthCheck.lastCheck).toLocaleString()}`),document.getElementById("event-container-down").checked=S.events?.containerDown!==!1,document.getElementById("event-container-up").checked=S.events?.containerUp!==!1,document.getElementById("event-deploy-success").checked=S.events?.deploymentSuccess!==!1,document.getElementById("event-deploy-failed").checked=S.events?.deploymentFailed!==!1,document.getElementById("event-resource-alert").checked=S.events?.resourceAlert!==!1}}catch(k){h.logError("[Notifications] Load Config",k,{function:"loadConfig"})}}async function v(){try{const B=await(await fetch("/api/v1/notifications/history?limit=10")).json(),S=document.getElementById("notification-history");B.success&&B.history?.length>0?S.innerHTML=B.history.map(T=>{const j=new Date(T.timestamp).toLocaleString();return`
- ${R.type==="success"?"\u2713":R.type==="error"?"\u2717":"\u2139"} + ${T.type==="success"?"\u2713":T.type==="error"?"\u2717":"\u2139"}
-
${escapeHtml(R.title)}
-
${M}
+
${escapeHtml(T.title)}
+
${j}
- `}).join(""):C.innerHTML='
No notifications yet
'}catch(x){b.logError("[Notifications] Load History",x,{function:"loadHistory"})}}async function I(){try{const x={enabled:document.getElementById("notifications-enabled").checked,providers:{discord:{enabled:document.getElementById("discord-enabled").checked,webhookUrl:document.getElementById("discord-webhook").value.trim()},telegram:{enabled:document.getElementById("telegram-enabled").checked,botToken:document.getElementById("telegram-bot-token").value.trim(),chatId:document.getElementById("telegram-chat-id").value.trim()},ntfy:{enabled:document.getElementById("ntfy-enabled").checked,serverUrl:document.getElementById("ntfy-server").value.trim()||"https://ntfy.sh",topic:document.getElementById("ntfy-topic").value.trim()},email:{enabled:document.getElementById("email-enabled").checked,host:document.getElementById("email-host").value.trim(),port:parseInt(document.getElementById("email-port").value)||587,secure:document.getElementById("email-secure").checked,user:document.getElementById("email-user").value.trim(),pass:document.getElementById("email-pass").value.trim(),from:document.getElementById("email-from").value.trim(),to:document.getElementById("email-to").value.trim()}},events:{containerDown:document.getElementById("event-container-down").checked,containerUp:document.getElementById("event-container-up").checked,deploymentSuccess:document.getElementById("event-deploy-success").checked,deploymentFailed:document.getElementById("event-deploy-failed").checked},healthCheck:{enabled:document.getElementById("health-check-enabled").checked,intervalMinutes:parseInt(document.getElementById("health-check-interval").value)||5}},C=await(await secureFetch("/api/v1/notifications/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(x)})).json();C.success?(showNotification("Notification settings saved","success",3e3),E.classList.remove("show")):showNotification(`Failed to save: ${C.error}`,"error",3e3)}catch(x){showNotification(`Error: ${x.message}`,"error",3e3)}}async function k(x){try{const C=await(await secureFetch("/api/v1/notifications/test",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({provider:x})})).json();C.success?showNotification(`Test ${x} notification sent!`,"success",3e3):showNotification(`Test failed: ${C.error}`,"error",3e3)}catch($){showNotification(`Error: ${$.message}`,"error",3e3)}}document.getElementById("discord-test")?.addEventListener("click",()=>k("discord")),document.getElementById("telegram-test")?.addEventListener("click",()=>k("telegram")),document.getElementById("ntfy-test")?.addEventListener("click",()=>k("ntfy")),document.getElementById("email-test")?.addEventListener("click",()=>k("email")),document.getElementById("health-check-now")?.addEventListener("click",async()=>{try{const $=await(await secureFetch("/api/v1/notifications/health-check",{method:"POST"})).json();$.success&&(document.getElementById("health-check-status").textContent=`Last check: ${new Date($.lastCheck).toLocaleString()} (${$.containersMonitored} containers)`,showNotification("Health check completed","success",2e3))}catch(x){showNotification(`Error: ${x.message}`,"error",3e3)}}),N?.addEventListener("click",()=>{E.classList.add("show"),H(),g()}),S?.addEventListener("click",I),wireModal(E,T)})(),(function(){document.addEventListener("click",b=>{const E=b.target.closest(".panel-tab");if(!E)return;const N=E.dataset.panel;if(!N)return;const S=E.closest(".panel-tabs"),T=S.closest(".weather-modal-content");S.querySelectorAll(".panel-tab").forEach(L=>L.classList.remove("active")),E.classList.add("active"),T.querySelectorAll(".panel-section").forEach(L=>L.classList.remove("active"));const P=T.querySelector("#"+N);P&&P.classList.add("active")})})(),(function(){var b=["dashcaddy_site_config","dashcaddy_onboarding","dashcaddy-encryption-key","dashcaddy-setup","dashcaddy-config","theme","user-themes","custom-theme","custom-apps","custom-services","toolbar-sections","weather-location","weather-zip","weather-geo","weather-unit","clock-style","clock-chimes","clock-chime-volume"];function E(){for(var e={},n=0;n + `}).join(""):S.innerHTML='
No notifications yet
'}catch(k){h.logError("[Notifications] Load History",k,{function:"loadHistory"})}}async function L(){try{const k={enabled:document.getElementById("notifications-enabled").checked,providers:{discord:{enabled:document.getElementById("discord-enabled").checked,webhookUrl:document.getElementById("discord-webhook").value.trim()},telegram:{enabled:document.getElementById("telegram-enabled").checked,botToken:document.getElementById("telegram-bot-token").value.trim(),chatId:document.getElementById("telegram-chat-id").value.trim()},ntfy:{enabled:document.getElementById("ntfy-enabled").checked,serverUrl:document.getElementById("ntfy-server").value.trim()||"https://ntfy.sh",topic:document.getElementById("ntfy-topic").value.trim()},email:{enabled:document.getElementById("email-enabled").checked,host:document.getElementById("email-host").value.trim(),port:parseInt(document.getElementById("email-port").value)||587,secure:document.getElementById("email-secure").checked,user:document.getElementById("email-user").value.trim(),pass:document.getElementById("email-pass").value.trim(),from:document.getElementById("email-from").value.trim(),to:document.getElementById("email-to").value.trim()}},events:{containerDown:document.getElementById("event-container-down").checked,containerUp:document.getElementById("event-container-up").checked,deploymentSuccess:document.getElementById("event-deploy-success").checked,deploymentFailed:document.getElementById("event-deploy-failed").checked,resourceAlert:document.getElementById("event-resource-alert").checked},healthCheck:{enabled:document.getElementById("health-check-enabled").checked,intervalMinutes:parseInt(document.getElementById("health-check-interval").value)||5}},S=await(await secureFetch("/api/v1/notifications/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(k)})).json();S.success?(showNotification("Notification settings saved","success",3e3),E.classList.remove("show")):showNotification(`Failed to save: ${S.error}`,"error",3e3)}catch(k){showNotification(`Error: ${k.message}`,"error",3e3)}}async function b(k){try{const S=await(await secureFetch("/api/v1/notifications/test",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({provider:k})})).json();S.success?showNotification(`Test ${k} notification sent!`,"success",3e3):showNotification(`Test failed: ${S.error}`,"error",3e3)}catch(B){showNotification(`Error: ${B.message}`,"error",3e3)}}document.getElementById("discord-test")?.addEventListener("click",()=>b("discord")),document.getElementById("telegram-test")?.addEventListener("click",()=>b("telegram")),document.getElementById("ntfy-test")?.addEventListener("click",()=>b("ntfy")),document.getElementById("email-test")?.addEventListener("click",()=>b("email")),document.getElementById("health-check-now")?.addEventListener("click",async()=>{try{const B=await(await secureFetch("/api/v1/notifications/health-check",{method:"POST"})).json();B.success&&(document.getElementById("health-check-status").textContent=`Last check: ${new Date(B.lastCheck).toLocaleString()} (${B.containersMonitored} containers)`,showNotification("Health check completed","success",2e3))}catch(k){showNotification(`Error: ${k.message}`,"error",3e3)}}),P?.addEventListener("click",()=>{E.classList.add("show"),A(),v()}),w?.addEventListener("click",L),document.getElementById("notifications-send-test")?.addEventListener("click",async()=>{const k=document.getElementById("notifications-send-test"),B=k.textContent;k.textContent="Sending...",k.disabled=!0;try{const T=await(await secureFetch("/api/v1/notifications/send",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({event:"test",data:{message:"This is a test notification from DashCaddy."},type:"info"})})).json();T.success?(showNotification("Test notification sent!","success",3e3),M()):showNotification(`Test failed: ${T.results?.map(j=>`${j.provider}: ${j.error||"ok"}`).join(", ")}`,"error",5e3)}catch(S){showNotification(`Error: ${S.message}`,"error",3e3)}finally{k.textContent=B,k.disabled=!1}});async function M(){try{const B=await(await fetch("/api/v1/notifications/status")).json();if(B.success&&B.lastSent){const S=document.getElementById("last-notification-sent");S&&(S.textContent=`Last sent: ${new Date(B.lastSent).toLocaleString()}`)}}catch{}}wireModal(E,N)})(),(function(){document.addEventListener("click",h=>{const E=h.target.closest(".panel-tab");if(!E)return;const P=E.dataset.panel;if(!P)return;const w=E.closest(".panel-tabs"),N=w.closest(".weather-modal-content");w.querySelectorAll(".panel-tab").forEach(z=>z.classList.remove("active")),E.classList.add("active"),N.querySelectorAll(".panel-section").forEach(z=>z.classList.remove("active"));const O=N.querySelector("#"+P);O&&O.classList.add("active")})})(),(function(){var h=["dashcaddy_site_config","dashcaddy_onboarding","dashcaddy-encryption-key","dashcaddy-setup","dashcaddy-config","theme","user-themes","custom-theme","custom-apps","custom-services","toolbar-sections","weather-location","weather-zip","weather-geo","weather-unit","clock-style","clock-chimes","clock-chime-volume"];function E(){for(var e={},o=0;o

\u{1F4BE} Backup & Restore

- + + +
@@ -1062,12 +1069,32 @@ Try refreshing in a moment if you see a certificate error.`,"warning",1e4),!1}};
- -
-
+ +
+
\u23F0 - Loading backup schedule... + Loading schedules... +
+
+
+ + +
+
+
+ \u{1F4BE} + Loading backup files... +
+
+
+ + +
+
+
+ \u23EA + Loading...
@@ -1087,7 +1114,9 @@ Try refreshing in a moment if you see a certificate error.`,"warning",1e4),!1}};
-
`);var P=document.getElementById("backup-modal"),L=document.getElementById("backup-restore-btn"),H=document.getElementById("backup-cancel"),g=document.getElementById("backup-export-btn"),I=document.getElementById("backup-select-file"),k=document.getElementById("backup-file-input"),x=document.getElementById("backup-file-name"),$=document.getElementById("backup-preview"),C=document.getElementById("backup-preview-content"),R=document.getElementById("backup-do-restore-btn"),M=document.getElementById("backup-result"),j=document.getElementById("backup-schedule-container"),B=document.getElementById("backup-history-container"),A=null;L?.addEventListener("click",function(){P.classList.add("show"),M&&(M.style.display="none"),$&&($.style.display="none"),x&&(x.style.display="none"),A=null}),wireModal(P,H),g?.addEventListener("click",async function(){g.disabled=!0,g.innerHTML=' Exporting...';try{var e=await fetch("/api/v1/backup/export"),n=await e.json();n.browserState=E();var t=new Blob([JSON.stringify(n,null,2)],{type:"application/json"}),i=URL.createObjectURL(t),o=document.createElement("a");o.href=i,o.download="dashcaddy-backup-"+new Date().toISOString().split("T")[0]+".json",document.body.appendChild(o),o.click(),document.body.removeChild(o),URL.revokeObjectURL(i);var d=Object.keys(n.browserState).length,l=n.themes?Object.keys(n.themes).length:0;M.innerHTML="\u2705 Full backup downloaded \u2014 server config + "+d+" browser settings"+(l?" + "+l+" themes":""),M.style.display="block",M.style.background="color-mix(in srgb, var(--ok-fg) 15%, transparent)",M.style.border="1px solid var(--ok-fg)"}catch(D){M.innerHTML="\u274C Export failed: "+escapeHtml(D.message),M.style.display="block",M.style.background="color-mix(in srgb, var(--bad-fg) 15%, transparent)",M.style.border="1px solid var(--bad-fg)"}g.disabled=!1,g.innerHTML="\u2B07\uFE0F Download Full Backup"}),I?.addEventListener("click",function(){k.click()}),k?.addEventListener("change",async function(e){var n=e.target.files[0];if(n){x.textContent="\u{1F4C4} "+n.name,x.style.display="block",M.style.display="none";try{var t=await n.text(),i=JSON.parse(t);if(S(i)){A=i;var o='
Legacy format (v'+escapeHtml(i.version)+")
";o+='
',i.services?.length&&(o+='\u{1F4CB} '+i.services.length+" services"),i.customApps?.length&&(o+='\u{1F4E6} '+i.customApps.length+" custom apps"),i.theme&&(o+='\u{1F3A8} Theme: '+escapeHtml(i.theme)+""),i.userThemes&&(o+='\u{1F3A8} '+Object.keys(i.userThemes).length+" custom themes"),o+="
",C.innerHTML=o,$.style.display="block";return}var d=await secureFetch("/api/v1/backup/preview",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(i)}),l=await d.json();if(l.success){A=i;var o='
Exported: '+new Date(i.exportedAt).toLocaleString()+" (v"+escapeHtml(i.version)+")
";o+='
Server Config
',o+='
';for(var D in l.preview.files){var O=l.preview.files[D],F=O.action==="create"?"\u{1F195}":"\u{1F4DD}";o+=''+F+" "+escapeHtml(O.description)+""}o+="
",l.preview.serviceCount&&(o+='
'+l.preview.serviceCount+" services
"),l.preview.themeCount&&(o+='
\u{1F3A8} '+l.preview.themeCount+" custom themes
"),l.preview.browserStateCount&&(o+='
Browser Preferences
',o+='
\u{1F5A5}\uFE0F '+l.preview.browserStateCount+" saved settings (theme, weather, clock, widgets, etc.)
"),C.innerHTML=o,$.style.display="block"}else M.innerHTML="\u26A0\uFE0F Invalid backup file: "+escapeHtml(l.error),M.style.display="block",M.style.background="color-mix(in srgb, #f39c12 15%, transparent)",M.style.border="1px solid #f39c12",$.style.display="none"}catch(q){M.innerHTML="\u274C Could not read file: "+escapeHtml(q.message),M.style.display="block",M.style.background="color-mix(in srgb, var(--bad-fg) 15%, transparent)",M.style.border="1px solid var(--bad-fg)",$.style.display="none"}}}),R?.addEventListener("click",async function(){if(A&&confirm("This will overwrite your current configuration and browser preferences. Continue?")){R.disabled=!0,R.innerHTML=' Restoring...';try{if(S(A)){T(A),M.innerHTML="\u2705 Legacy backup restored \u2014 browser settings and services imported.",M.style.background="color-mix(in srgb, var(--ok-fg) 15%, transparent)",M.style.border="1px solid var(--ok-fg)",M.style.display="block",setTimeout(function(){location.reload()},2e3),R.disabled=!1,R.innerHTML="\u26A1 Restore Everything";return}var e=document.getElementById("backup-reload-caddy")?.checked??!0,n=await secureFetch("/api/v1/backup/restore",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({backup:A,options:{reloadCaddy:e}})}),t=await n.json(),i=0;if(A.browserState&&(i=N(A.browserState)),t.success){var o="\u2705 "+t.message;i>0&&(o+='
'+i+" browser settings restored"),t.results.caddyReloaded&&(o+='
Caddy configuration reloaded'),M.innerHTML=o,M.style.background="color-mix(in srgb, var(--ok-fg) 15%, transparent)",M.style.border="1px solid var(--ok-fg)",setTimeout(function(){location.reload()},2e3)}else M.innerHTML="\u26A0\uFE0F "+escapeHtml(t.message),i>0&&(M.innerHTML+='
'+i+" browser settings were restored"),t.results?.errors?.length>0&&(M.innerHTML+="
"+t.results.errors.map(function(d){return escapeHtml(d.file)+": "+escapeHtml(d.error)}).join(", ")+""),M.style.background="color-mix(in srgb, #f39c12 15%, transparent)",M.style.border="1px solid #f39c12";M.style.display="block"}catch(d){M.innerHTML="\u274C Restore failed: "+escapeHtml(d.message),M.style.display="block",M.style.background="color-mix(in srgb, var(--bad-fg) 15%, transparent)",M.style.border="1px solid var(--bad-fg)"}R.disabled=!1,R.innerHTML="\u26A1 Restore Everything"}});var w={type:"local"};async function z(){if(j)try{var e=await fetch("/api/v1/backups/config"),n=await e.json();if(!n.success)throw new Error(n.error||"Failed to load config");var t=n.config?.backups||{},i=Object.keys(t)[0],o=i?t[i]:null,d=o?.destinations&&o.destinations[0]||{type:"local"};w=JSON.parse(JSON.stringify(d));var l='
';l+='

\u23F0 Backup Schedule

',l+='
',l+='
',l+='
",l+='
',l+='
",l+="
",l+='
',l+='
",l+='
',l+=' ',l+=' ',l+="
",l+="
",l+='
',l+='

\u2601\uFE0F Backup Destination

',l+='
',l+='
",l+='
',l+='',l+="
",l+='',j.innerHTML=l,document.getElementById("backup-save-schedule")?.addEventListener("click",h),document.getElementById("backup-run-now")?.addEventListener("click",u);var D=document.getElementById("backup-dest-type");D?.addEventListener("change",function(){w={type:D.value},f(D.value)}),f(w.type)}catch(O){j.innerHTML='
Failed to load schedule: '+escapeHtml(O.message)+"
"}}async function f(e){var n=document.getElementById("backup-dest-form");if(n){if(e==="local"){n.innerHTML='
Backups are stored on the host filesystem. No additional configuration required.
';return}var t="";if(e==="dropbox"?(t+='',t+='',t+='',t+='',t+='
Generate a token at Dropbox App Console with files.content.write + files.content.read scopes.
'):e==="webdav"?(t+='',t+='',t+='
',t+='
',t+='
',t+='
',t+='
',t+="
",t+='',t+=''):e==="sftp"&&(t+='
',t+='
',t+='
',t+='
',t+='
',t+="
",t+='',t+='',t+='',t+='",t+='
',t+='
',t+='',t+='',t+=''),t+='
',t+=' ',t+=' ',t+=' ',t+="
",n.innerHTML=t,e==="sftp"){var i=document.getElementById("dest-sftp-authtype"),o=document.getElementById("dest-sftp-password-row"),d=document.getElementById("dest-sftp-key-row");i?.addEventListener("change",function(){i.value==="key"?(o.style.display="none",d.style.display=""):(o.style.display="",d.style.display="none")})}document.getElementById("dest-save-creds")?.addEventListener("click",function(){m(e)}),document.getElementById("dest-test-conn")?.addEventListener("click",function(){s(e)}),document.getElementById("dest-clear-creds")?.addEventListener("click",function(){r(e)}),await y(e)}}function p(e,n){var t=document.getElementById("backup-dest-result");t&&(t.innerHTML=e,t.style.display="block",t.style.background=n?"color-mix(in srgb, var(--ok-fg) 15%, transparent)":"color-mix(in srgb, var(--bad-fg) 15%, transparent)",t.style.border=n?"1px solid var(--ok-fg)":"1px solid var(--bad-fg)")}async function y(e){try{var n=await fetch("/api/v1/backups/credentials/"+e),t=await n.json();if(!t.success||!t.credentials)return;var i=t.credentials;if(e==="dropbox"){var o=document.getElementById("dest-dropbox-token");o&&i.token&&(o.value=i.token)}else if(e==="webdav"){var d=document.getElementById("dest-webdav-url");d&&i.url&&(d.value=i.url);var l=document.getElementById("dest-webdav-username");l&&i.username&&(l.value=i.username);var D=document.getElementById("dest-webdav-password");D&&i.password&&(D.value=i.password)}else if(e==="sftp"){var O=document.getElementById("dest-sftp-host");O&&i.host&&(O.value=i.host);var F=document.getElementById("dest-sftp-port");F&&i.port&&(F.value=i.port);var q=document.getElementById("dest-sftp-username");q&&i.username&&(q.value=i.username);var U=document.getElementById("dest-sftp-password");U&&i.password&&(U.value=i.password);var G=document.getElementById("dest-sftp-privatekey");if(G&&i.privateKey&&(G.value=i.privateKey),i.privateKey){var W=document.getElementById("dest-sftp-authtype");W&&(W.value="key",W.dispatchEvent(new Event("change")))}}}catch{}}function v(e){if(e==="dropbox")return{token:document.getElementById("dest-dropbox-token")?.value};if(e==="webdav")return{url:document.getElementById("dest-webdav-url")?.value,username:document.getElementById("dest-webdav-username")?.value,password:document.getElementById("dest-webdav-password")?.value};if(e==="sftp"){var n=document.getElementById("dest-sftp-authtype")?.value,t={host:document.getElementById("dest-sftp-host")?.value,port:parseInt(document.getElementById("dest-sftp-port")?.value)||22,username:document.getElementById("dest-sftp-username")?.value};return n==="key"?t.privateKey=document.getElementById("dest-sftp-privatekey")?.value:t.password=document.getElementById("dest-sftp-password")?.value,t}return{}}async function m(e){try{var n=v(e),t=await secureFetch("/api/v1/backups/credentials/"+e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)}),i=await t.json();p(i.success?"\u2705 Credentials saved":"\u26A0\uFE0F "+escapeHtml(i.error||"Failed"),i.success)}catch(o){p("\u274C "+escapeHtml(o.message),!1)}}async function r(e){if(confirm("Delete saved "+e+" credentials?"))try{var n=await secureFetch("/api/v1/backups/credentials/"+e,{method:"DELETE"}),t=await n.json();t.success?(p("\u2705 Credentials cleared",!0),f(e)):p("\u26A0\uFE0F "+escapeHtml(t.error||"Failed"),!1)}catch(i){p("\u274C "+escapeHtml(i.message),!1)}}function c(e){var n={type:e};return e==="local"||(e==="dropbox"?n.path=document.getElementById("dest-dropbox-path")?.value||"/dashcaddy-backups":e==="webdav"?n.path=document.getElementById("dest-webdav-path")?.value||"/dashcaddy-backups":e==="sftp"&&(n.path=document.getElementById("dest-sftp-path")?.value||"/dashcaddy-backups")),n}async function s(e){p(' Testing connection...',!0);try{var n=c(e),t=await secureFetch("/api/v1/backups/test-destination",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)}),i=await t.json();if(i.success){var o=i.elapsedMs?" ("+i.elapsedMs+"ms)":"";p("\u2705 Connection OK"+o+" \u2014 write/read/delete probe succeeded",!0)}else p("\u274C "+escapeHtml(i.error||"Connection failed"),!1)}catch(d){p("\u274C "+escapeHtml(d.message),!1)}}async function h(){var e=document.getElementById("backup-schedule-select")?.value,n=parseInt(document.getElementById("backup-retention-select")?.value)||5,t=document.getElementById("backup-encrypt-toggle")?.checked??!0,i=document.getElementById("backup-dest-type")?.value||"local",o=document.getElementById("backup-schedule-result");try{var d=await secureFetch("/api/v1/backups/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({backups:{auto:{enabled:e!=="disabled",schedule:e==="disabled"?"daily":e,include:["all"],encrypt:t,verify:!0,retention:{keep:n},destinations:[c(i)]}}})}),l=await d.json();o&&(o.innerHTML=l.success?"\u2705 Schedule saved":"\u26A0\uFE0F "+escapeHtml(l.error),o.style.display="block",o.style.background=l.success?"color-mix(in srgb, var(--ok-fg) 15%, transparent)":"color-mix(in srgb, var(--bad-fg) 15%, transparent)",o.style.border=l.success?"1px solid var(--ok-fg)":"1px solid var(--bad-fg)",setTimeout(function(){o&&(o.style.display="none")},3e3))}catch(D){o&&(o.innerHTML="\u274C "+escapeHtml(D.message),o.style.display="block",o.style.background="color-mix(in srgb, var(--bad-fg) 15%, transparent)",o.style.border="1px solid var(--bad-fg)")}}async function u(){var e=document.getElementById("backup-run-now"),n=document.getElementById("backup-schedule-result"),t=document.getElementById("backup-dest-type")?.value||"local";e&&(e.disabled=!0,e.innerHTML=' Running...');try{var i=await secureFetch("/api/v1/backups/execute",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({include:["all"],destinations:[c(t)]})}),o=await i.json();if(n){if(o.success){var d=o.backup?.size?(o.backup.size/1024/1024).toFixed(2):"?";n.innerHTML="\u2705 Backup complete ("+d+" MB)",n.style.background="color-mix(in srgb, var(--ok-fg) 15%, transparent)",n.style.border="1px solid var(--ok-fg)"}else n.innerHTML="\u26A0\uFE0F "+escapeHtml(o.error),n.style.background="color-mix(in srgb, var(--bad-fg) 15%, transparent)",n.style.border="1px solid var(--bad-fg)";n.style.display="block"}a()}catch(l){n&&(n.innerHTML="\u274C "+escapeHtml(l.message),n.style.display="block",n.style.background="color-mix(in srgb, var(--bad-fg) 15%, transparent)",n.style.border="1px solid var(--bad-fg)")}e&&(e.disabled=!1,e.innerHTML="\u25B6\uFE0F Run Backup Now")}async function a(){if(B){B.innerHTML='
Loading...
';try{var e=await fetch("/api/v1/backups/history?limit=50"),n=await e.json();if(!n.success||!n.history?.length){B.innerHTML='
\u{1F4CB} No backup history yet
';return}for(var t='
',i=0;i',t+='
',t+=' '+escapeHtml(o.name||"backup")+"",t+='
',t+=' '+escapeHtml(o.status)+"",o.status==="success"&&(t+=' '),t+="
",t+="
",t+='
',t+=" "+new Date(o.timestamp).toLocaleString()+" | "+d+" MB | "+(o.duration?(o.duration/1e3).toFixed(1)+"s":"--"),o.encrypted&&(t+=" | \u{1F512}"),t+="
",t+="
"}t+="
",B.innerHTML=t,B.querySelectorAll(".backup-restore-btn").forEach(function(l){l.addEventListener("click",function(){window.__restoreServerBackup(l.dataset.backupId)})})}catch(l){B.innerHTML='
Failed: '+escapeHtml(l.message)+"
"}}}window.__restoreServerBackup=async function(e){if(confirm("Restore from this server backup? This will overwrite current configuration."))try{var n=await secureFetch("/api/v1/backups/restore/"+e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({restoreServices:!0,restoreConfig:!0})}),t=await n.json();t.success?(showNotification("Restore completed successfully!","success"),location.reload()):showNotification("Restore failed: "+(t.error||"Unknown error"),"error")}catch(i){showNotification("Restore error: "+i.message,"error")}},document.querySelector('[data-panel="backup-automated"]')?.addEventListener("click",z),document.querySelector('[data-panel="backup-history-tab"]')?.addEventListener("click",a)})(),(function(){injectModal("stats-modal",`
+
`);var O=document.getElementById("backup-modal"),z=document.getElementById("backup-restore-btn"),A=document.getElementById("backup-cancel"),v=document.getElementById("backup-export-btn"),L=document.getElementById("backup-select-file"),b=document.getElementById("backup-file-input"),M=document.getElementById("backup-file-name"),k=document.getElementById("backup-preview"),B=document.getElementById("backup-preview-content"),S=document.getElementById("backup-do-restore-btn"),T=document.getElementById("backup-result"),j=document.getElementById("backup-schedules-container"),H=document.getElementById("backup-history-container"),R=document.getElementById("backup-disk-container"),x=document.getElementById("pointintime-container"),D=null;z?.addEventListener("click",function(){O.classList.add("show"),T&&(T.style.display="none"),k&&(k.style.display="none"),M&&(M.style.display="none"),D=null}),wireModal(O,A),v?.addEventListener("click",async function(){v.disabled=!0,v.innerHTML=' Exporting...';try{var e=await fetch("/api/v1/backup/export"),o=await e.json();o.browserState=E();var a=new Blob([JSON.stringify(o,null,2)],{type:"application/json"}),r=URL.createObjectURL(a),t=document.createElement("a");t.href=r,t.download="dashcaddy-backup-"+new Date().toISOString().split("T")[0]+".json",document.body.appendChild(t),t.click(),document.body.removeChild(t),URL.revokeObjectURL(r);var s=Object.keys(o.browserState).length,l=o.themes?Object.keys(o.themes).length:0;T.innerHTML="\u2705 Full backup downloaded \u2014 server config + "+s+" browser settings"+(l?" + "+l+" themes":""),T.style.display="block",T.style.background="color-mix(in srgb, var(--ok-fg) 15%, transparent)",T.style.border="1px solid var(--ok-fg)"}catch(C){T.innerHTML="\u274C Export failed: "+escapeHtml(C.message),T.style.display="block",T.style.background="color-mix(in srgb, var(--bad-fg) 15%, transparent)",T.style.border="1px solid var(--bad-fg)"}v.disabled=!1,v.innerHTML="\u2B07\uFE0F Download Full Backup"}),L?.addEventListener("click",function(){b.click()}),b?.addEventListener("change",async function(e){var o=e.target.files[0];if(o){M.textContent="\u{1F4C4} "+o.name,M.style.display="block",T.style.display="none";try{var a=await o.text(),r=JSON.parse(a);if(w(r)){D=r;var t='
Legacy format (v'+escapeHtml(r.version)+")
";t+='
',r.services?.length&&(t+='\u{1F4CB} '+r.services.length+" services"),r.customApps?.length&&(t+='\u{1F4E6} '+r.customApps.length+" custom apps"),r.theme&&(t+='\u{1F3A8} Theme: '+escapeHtml(r.theme)+""),r.userThemes&&(t+='\u{1F3A8} '+Object.keys(r.userThemes).length+" custom themes"),t+="
",B.innerHTML=t,k.style.display="block";return}var s=await secureFetch("/api/v1/backup/preview",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(r)}),l=await s.json();if(l.success){D=r;var t='
Exported: '+new Date(r.exportedAt).toLocaleString()+" (v"+escapeHtml(r.version)+")
";t+='
Server Config
',t+='
';for(var C in l.preview.files){var I=l.preview.files[C],F=I.action==="create"?"\u{1F195}":"\u{1F4DD}";t+=''+F+" "+escapeHtml(I.description)+""}t+="
",l.preview.serviceCount&&(t+='
'+l.preview.serviceCount+" services
"),l.preview.themeCount&&(t+='
\u{1F3A8} '+l.preview.themeCount+" custom themes
"),l.preview.browserStateCount&&(t+='
Browser Preferences
',t+='
\u{1F5A5}\uFE0F '+l.preview.browserStateCount+" saved settings (theme, weather, clock, widgets, etc.)
"),B.innerHTML=t,k.style.display="block"}else T.innerHTML="\u26A0\uFE0F Invalid backup file: "+escapeHtml(l.error),T.style.display="block",T.style.background="color-mix(in srgb, #f39c12 15%, transparent)",T.style.border="1px solid #f39c12",k.style.display="none"}catch(q){T.innerHTML="\u274C Could not read file: "+escapeHtml(q.message),T.style.display="block",T.style.background="color-mix(in srgb, var(--bad-fg) 15%, transparent)",T.style.border="1px solid var(--bad-fg)",k.style.display="none"}}}),S?.addEventListener("click",async function(){if(D&&confirm("This will overwrite your current configuration and browser preferences. Continue?")){S.disabled=!0,S.innerHTML=' Restoring...';try{if(w(D)){N(D),T.innerHTML="\u2705 Legacy backup restored \u2014 browser settings and services imported.",T.style.background="color-mix(in srgb, var(--ok-fg) 15%, transparent)",T.style.border="1px solid var(--ok-fg)",T.style.display="block",setTimeout(function(){location.reload()},2e3),S.disabled=!1,S.innerHTML="\u26A1 Restore Everything";return}var e=document.getElementById("backup-reload-caddy")?.checked??!0,o=await secureFetch("/api/v1/backup/restore",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({backup:D,options:{reloadCaddy:e}})}),a=await o.json(),r=0;if(D.browserState&&(r=P(D.browserState)),a.success){var t="\u2705 "+a.message;r>0&&(t+='
'+r+" browser settings restored"),a.results.caddyReloaded&&(t+='
Caddy configuration reloaded'),T.innerHTML=t,T.style.background="color-mix(in srgb, var(--ok-fg) 15%, transparent)",T.style.border="1px solid var(--ok-fg)",setTimeout(function(){location.reload()},2e3)}else T.innerHTML="\u26A0\uFE0F "+escapeHtml(a.message),r>0&&(T.innerHTML+='
'+r+" browser settings were restored"),a.results?.errors?.length>0&&(T.innerHTML+="
"+a.results.errors.map(function(s){return escapeHtml(s.file)+": "+escapeHtml(s.error)}).join(", ")+""),T.style.background="color-mix(in srgb, #f39c12 15%, transparent)",T.style.border="1px solid #f39c12";T.style.display="block"}catch(s){T.innerHTML="\u274C Restore failed: "+escapeHtml(s.message),T.style.display="block",T.style.background="color-mix(in srgb, var(--bad-fg) 15%, transparent)",T.style.border="1px solid var(--bad-fg)"}S.disabled=!1,S.innerHTML="\u26A1 Restore Everything"}});async function g(){if(j){j.innerHTML='
Loading...
';try{var e=await fetch("/api/v1/backups/schedule"),o=await e.json();if(o.premiumRequired){j.innerHTML=`
\u2B50
Premium Feature
Auto-backup scheduling requires a DashCaddy Premium subscription.
`;return}if(!o.success)throw new Error(o.error||"Failed to load schedules");var a=o.schedules||[];if(a.length===0){j.innerHTML='
\u23F0
No backup schedules configured
Select apps below to enable auto-backup
';return}for(var r='
',t=0;t
Schedule:
Keep last:
Next run: '+escapeHtml(l)+"
Last run: "+escapeHtml(C)+'
'}r+="",r+='

\u2795 Add New Schedule

',j.innerHTML=r,j.querySelectorAll(".schedule-toggle").forEach(function(I){I.addEventListener("change",function(){u(I.dataset.appid,{enabled:I.checked})})}),j.querySelectorAll(".schedule-select").forEach(function(I){I.addEventListener("change",function(){u(I.dataset.appid,{schedule:I.value})})}),j.querySelectorAll(".retention-input").forEach(function(I){I.addEventListener("change",function(){u(I.dataset.appid,{retention:{keep:parseInt(I.value)||7}})})}),j.querySelectorAll(".schedule-run-now").forEach(function(I){I.addEventListener("click",function(){f(I.dataset.appid)})}),j.querySelectorAll(".schedule-delete").forEach(function(I){I.addEventListener("click",function(){m(I.dataset.appid)})}),document.getElementById("add-schedule-btn")?.addEventListener("click",p)}catch(I){j.innerHTML='
Failed to load: '+escapeHtml(I.message)+"
"}}}async function u(e,o){try{var a=await secureFetch("/api/v1/backups/schedule",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({appId:e,...o})}),r=await a.json();r.success?showNotification("Schedule updated for "+e,"success"):(showNotification("Update failed: "+(r.error||"Unknown"),"error"),g())}catch(t){showNotification("Error: "+t.message,"error")}}async function f(e){try{var o=await secureFetch("/api/v1/backups/backup/"+encodeURIComponent(e),{method:"POST",headers:{"Content-Type":"application/json"}}),a=await o.json();a.success?showNotification("Backup started for "+e+"!","success"):showNotification("Backup failed: "+(a.error||"Unknown"),"error")}catch(r){showNotification("Error: "+r.message,"error")}}async function m(e){if(confirm("Remove backup schedule for "+e+"?"))try{var o=await secureFetch("/api/v1/backups/schedule/"+encodeURIComponent(e),{method:"DELETE"}),a=await o.json();a.success?(showNotification("Schedule removed for "+e,"success"),g()):showNotification("Delete failed: "+(a.error||"Unknown"),"error")}catch(r){showNotification("Error: "+r.message,"error")}}async function p(){var e=document.getElementById("new-schedule-appid")?.value?.trim(),o=document.getElementById("new-schedule-interval")?.value||"daily",a=parseInt(document.getElementById("new-schedule-retention")?.value)||7;if(!e){showNotification("Please enter an App ID","warning");return}try{var r=await secureFetch("/api/v1/backups/schedule",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({appId:e,schedule:o,retention:{keep:a},enabled:!0})}),t=await r.json();if(t.success){showNotification("Schedule created for "+e,"success"),g();var s=document.getElementById("new-schedule-appid");s&&(s.value="")}else showNotification("Failed: "+(t.error||"Unknown"),"error")}catch(l){showNotification("Error: "+l.message,"error")}}async function d(){if(R){R.innerHTML='
Loading...
';try{var e=await fetch("/api/v1/backups/files"),o=await e.json();if(!o.success)throw new Error(o.error||"Failed to load");var a=o.files||[];if(a.length===0){R.innerHTML='
\u{1F4BE}
No backup files on disk
Run a backup to create backup files
';return}for(var r={},t=0;t";C+='
';for(var I=Object.keys(r).sort(),F=0;F
'+escapeHtml(l)+' ('+q.length+" backup(s))
";for(var U=0;U
'+s.sizeFormatted+'
'+G+'
'}C+=""}C+="",R.innerHTML=C,R.querySelectorAll(".disk-compare-btn").forEach(function(J){J.addEventListener("click",function(){y(J.dataset.appid,J.dataset.filename)})}),R.querySelectorAll(".disk-restore-btn").forEach(function(J){J.addEventListener("click",function(){n(J.dataset.appid,J.dataset.filename)})})}catch(J){R.innerHTML='
Failed: '+escapeHtml(J.message)+"
"}}}async function c(){if(H){H.innerHTML='
Loading...
';try{var e=await fetch("/api/v1/backups/history?limit=50"),o=await e.json();if(!o.success||!o.history?.length){H.innerHTML='
\u{1F4CB} No backup history yet
';return}for(var a='
',r=0;r',a+='
',a+=' '+escapeHtml(t.name||"backup")+"",a+='
',a+=' '+escapeHtml(t.status)+"",t.status==="success"&&(a+=' '),a+="
",a+="
",a+='
',a+=" "+new Date(t.timestamp).toLocaleString()+" | "+s+" MB | "+(t.duration?(t.duration/1e3).toFixed(1)+"s":"--"),t.encrypted&&(a+=" | \u{1F512}"),a+="
",a+="
"}a+="",H.innerHTML=a,H.querySelectorAll(".backup-restore-btn").forEach(function(l){l.addEventListener("click",function(){window.__restoreServerBackup(l.dataset.backupId)})})}catch(l){H.innerHTML='
Failed: '+escapeHtml(l.message)+"
"}}}window.__restoreServerBackup=async function(e){if(confirm("Restore from this server backup? This will overwrite current configuration."))try{var o=await secureFetch("/api/v1/backups/restore/"+e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({restoreServices:!0,restoreConfig:!0})}),a=await o.json();a.success?(showNotification("Restore completed successfully!","success"),location.reload()):showNotification("Restore failed: "+(a.error||"Unknown error"),"error")}catch(r){showNotification("Restore error: "+r.message,"error")}},document.querySelector('[data-panel="backup-schedules-tab"]')?.addEventListener("click",g),document.querySelector('[data-panel="backup-disk-tab"]')?.addEventListener("click",d),document.querySelector('[data-panel="backup-pointintime-tab"]')?.addEventListener("click",i),document.querySelector('[data-panel="backup-history-tab"]')?.addEventListener("click",c);async function i(){if(x){try{var e=await fetch("/api/v1/license/status"),o=await e.json();if(o.tier!=="premium"){x.innerHTML=`
\u2B50
Premium Feature
Point-in-time restore requires DashCaddy Premium with auto-backup enabled.
`;return}}catch{}x.innerHTML='
Loading...
';try{var a=await fetch("/api/v1/services"),r=await a.json(),t=r.services||[];if(t.length===0){x.innerHTML='
\u{1F4E6} No apps deployed yet
';return}for(var s='
',x.innerHTML=s,document.getElementById("pit-load-btn")?.addEventListener("click",function(){var C=document.getElementById("pit-app-select")?.value;C&&$(C)})}catch(C){x.innerHTML='
Failed: '+escapeHtml(C.message)+"
"}}}async function $(e){var o=document.getElementById("pit-backups-list");if(o){o.innerHTML='
Loading backups...
';try{var a=await fetch("/api/v1/backups/files/"+encodeURIComponent(e)),r=await a.json();if(!r.success||!r.files||r.files.length===0){o.innerHTML='
\u{1F4BE} No backup files for '+escapeHtml(e)+"
";return}for(var t='
'+r.files.length+' backup(s)
',s=0;s
'+l.sizeFormatted+'
'+C+'
'}t+="",o.innerHTML=t,o.querySelectorAll(".pit-compare-btn").forEach(function(I){I.addEventListener("click",function(){y(I.dataset.appid,I.dataset.filename)})}),o.querySelectorAll(".pit-restore-btn").forEach(function(I){I.addEventListener("click",function(){n(I.dataset.appid,I.dataset.filename)})})}catch(I){o.innerHTML='
Failed: '+escapeHtml(I.message)+"
"}}}async function y(e,o){try{var a=await secureFetch("/api/v1/backups/compare/"+encodeURIComponent(o),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({})}),r=await a.json();if(!r.success){showNotification("Compare failed: "+(r.error||"Unknown"),"error");return}var t=r.diff,s='

\u{1F4CA} Compare: '+escapeHtml(o)+'

Size: '+(t.sizeFormatted||"?")+" | Created: "+new Date(t.timestamp).toLocaleString()+"
";if(t.services){var l=t.services.hasChanges?"\u{1F534}":"\u{1F7E2}";s+='
'+l+' Services (backup vs current)
Backup: '+t.services.backupCount+" services | Current: "+t.services.currentCount+" services
",t.services.hasChanges&&(s+='
Services differ \u2014 restoring will replace current configuration
'),s+="
"}if(t.config){var C=t.config.hasChanges?"\u{1F534}":"\u{1F7E2}";s+='
'+C+" Configuration
",t.config.hasChanges?s+='
Configuration differs \u2014 restoring will replace current settings
':s+='
No changes
',s+="
"}s+='
',document.body.insertAdjacentHTML("beforeend",s),document.getElementById("compare-close-btn")?.addEventListener("click",function(){document.getElementById("compare-overlay")?.remove()}),document.getElementById("compare-overlay")?.addEventListener("click",function(I){I.target===this&&this.remove()})}catch(I){showNotification("Compare error: "+I.message,"error")}}async function n(e,o){if(confirm("Restore "+o+" for "+e+`? + +This will replace current configuration, credentials, and data. Containers will be restarted.`))try{var a=await secureFetch("/api/v1/apps/"+encodeURIComponent(e)+"/revert/"+encodeURIComponent(o),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({restartContainers:!0})}),r=await a.json();r.success?(showNotification(e+" restored to "+o,"success"),setTimeout(function(){location.reload()},1500)):showNotification("Restore failed: "+(r.error||"Unknown"),"error")}catch(t){showNotification("Restore error: "+t.message,"error")}}})(),(function(){injectModal("stats-modal",`

\u{1F4CA} Resource Monitor

- `);const b=document.getElementById("stats-modal"),E=document.getElementById("container-stats-btn"),N=document.getElementById("stats-cancel"),S=document.getElementById("stats-refresh-btn"),T=document.getElementById("stats-auto-refresh"),P=document.getElementById("stats-container"),L=document.getElementById("stats-aggregated-container"),H=document.getElementById("stats-alerts-container"),g=document.getElementById("stats-last-update");let I=null,k=null;function x(s){if(s===0||!s)return"0 B";const h=1024,u=["B","KB","MB","GB"],a=Math.floor(Math.log(s)/Math.log(h));return parseFloat((s/Math.pow(h,a)).toFixed(1))+" "+u[a]}function $(s){return s<30?"#2ecc71":s<70?"#f39c12":"#e74c3c"}function C(s){return s<50?"#2ecc71":s<80?"#f39c12":"#e74c3c"}async function R(){try{let s=null,h=!1;try{const e=await(await fetch("/api/v1/monitoring/stats")).json();e.success&&e.stats&&(s=e.stats,h=!0,k=e.stats)}catch{}if(!h){const e=await(await fetch("/api/v1/stats/containers")).json();if(e.success&&e.stats){s={};for(const n of e.stats)s[n.name]={name:n.name,current:{cpu:n.cpu,memory:{percent:n.memory.percent,usage:n.memory.used,limit:n.memory.limit,usageMB:Math.round(n.memory.used/1048576),limitMB:Math.round(n.memory.limit/1048576)},network:{rxBytes:n.network.rx,txBytes:n.network.tx,rxMB:(n.network.rx/1048576).toFixed(1),txMB:(n.network.tx/1048576).toFixed(1)},disk:{readMB:0,writeMB:0}},status:n.status};k=s}}if(!s||Object.keys(s).length===0){P.innerHTML='
No running containers found
';return}let u='
';for(const[a,e]of Object.entries(s)){const n=e.current||e,t=n.cpu?.percent||0,i=n.memory?.percent||0,o=$(t),d=C(i),l=n.memory?.usage||n.memory?.used||0,D=n.memory?.limit||0,O=n.network?.rxBytes||n.network?.rx||0,F=n.network?.txBytes||n.network?.tx||0,q=e.aggregated;u+=` +
`);const h=document.getElementById("stats-modal"),E=document.getElementById("container-stats-btn"),P=document.getElementById("stats-cancel"),w=document.getElementById("stats-refresh-btn"),N=document.getElementById("stats-auto-refresh"),O=document.getElementById("stats-container"),z=document.getElementById("stats-aggregated-container"),A=document.getElementById("stats-alerts-container"),v=document.getElementById("stats-last-update");let L=null,b=null;function M(i){if(i===0||!i)return"0 B";const $=1024,y=["B","KB","MB","GB"],n=Math.floor(Math.log(i)/Math.log($));return parseFloat((i/Math.pow($,n)).toFixed(1))+" "+y[n]}function k(i){return i<30?"#2ecc71":i<70?"#f39c12":"#e74c3c"}function B(i){return i<50?"#2ecc71":i<80?"#f39c12":"#e74c3c"}async function S(){try{let i=null,$=!1;try{const e=await(await fetch("/api/v1/monitoring/stats")).json();e.success&&e.stats&&(i=e.stats,$=!0,b=e.stats)}catch{}if(!$){const e=await(await fetch("/api/v1/stats/containers")).json();if(e.success&&e.stats){i={};for(const o of e.stats)i[o.name]={name:o.name,current:{cpu:o.cpu,memory:{percent:o.memory.percent,usage:o.memory.used,limit:o.memory.limit,usageMB:Math.round(o.memory.used/1048576),limitMB:Math.round(o.memory.limit/1048576)},network:{rxBytes:o.network.rx,txBytes:o.network.tx,rxMB:(o.network.rx/1048576).toFixed(1),txMB:(o.network.tx/1048576).toFixed(1)},disk:{readMB:0,writeMB:0}},status:o.status};b=i}}if(!i||Object.keys(i).length===0){O.innerHTML='
No running containers found
';return}let y='
';for(const[n,e]of Object.entries(i)){const o=e.current||e,a=o.cpu?.percent||0,r=o.memory?.percent||0,t=k(a),s=B(r),l=o.memory?.usage||o.memory?.used||0,C=o.memory?.limit||0,I=o.network?.rxBytes||o.network?.rx||0,F=o.network?.txBytes||o.network?.tx||0,q=e.aggregated;y+=`
- ${e.name||a} + ${e.name||n} ${q?`avg ${q.cpu?.avg?.toFixed(0)||0}% cpu`:""} ${e.status||"running"}
@@ -1179,32 +1208,32 @@ Try refreshing in a moment if you see a certificate error.`,"warning",1e4),!1}};
CPU
-
+
- ${t.toFixed(1)}% + ${a.toFixed(1)}%
Memory
-
+
- ${i.toFixed(1)}% + ${r.toFixed(1)}%
-
${x(l)} / ${x(D)}
+
${M(l)} / ${M(C)}
Network
- \u2193 ${x(O)} + \u2193 ${M(I)} / - \u2191 ${x(F)} + \u2191 ${M(F)}
- `}u+="",P.innerHTML=u,g.textContent="Updated: "+new Date().toLocaleTimeString()}catch(s){P.innerHTML=`
\u274C Failed to load stats: ${escapeHtml(s.message)}
`}}async function M(){if(!L)return;const s=k;if(!s||Object.keys(s).length===0){L.innerHTML='
\u{1F4C8}No monitoring data available. Open the Live Stats tab first.
';return}let h='
';for(const[u,a]of Object.entries(s)){const e=a.aggregated;e&&(h+=`
-
${a.name||u}
+
`}y+="
",O.innerHTML=y,v.textContent="Updated: "+new Date().toLocaleTimeString()}catch(i){O.innerHTML=`
\u274C Failed to load stats: ${escapeHtml(i.message)}
`}}async function T(){if(!z)return;const i=b;if(!i||Object.keys(i).length===0){z.innerHTML='
\u{1F4C8}No monitoring data available. Open the Live Stats tab first.
';return}let $='
';for(const[y,n]of Object.entries(i)){const e=n.aggregated;e&&($+=`
+
${n.name||y}
${e.cpu?.avg?.toFixed(1)||0}%Avg CPU
${e.cpu?.max?.toFixed(1)||0}%Max CPU
@@ -1212,49 +1241,94 @@ Try refreshing in a moment if you see a certificate error.`,"warning",1e4),!1}};
${e.memory?.max?.toFixed(1)||0}%Max Mem
${e.dataPoints?`
${e.dataPoints} data points over ${e.timeRange||24}h
`:""} -
`)}h+="
",L.innerHTML=h}async function j(){if(!H)return;H.innerHTML='
Loading alerts...
';const s=k;if(!s||Object.keys(s).length===0){H.innerHTML='
\u{1F514}No containers found. Open the Live Stats tab first.
';return}let h='
';for(const[u,a]of Object.entries(s)){const e=a.alertConfig||{};h+=`
-
- ${a.name||u} - +
`)}$+="
",z.innerHTML=$}async function j(){if(!A)return;A.innerHTML='
Loading alerts...
';const i=b;if(!i||Object.keys(i).length===0){A.innerHTML='
\u{1F514}No containers found. Open the Live Stats tab first.
';return}let $=!1;try{$=(await(await fetch("/api/v1/license/feature/resource-alerts")).json()).available}catch{$=!1}let y=[];try{const s=await(await fetch("/api/v1/monitoring/alerts?limit=50")).json();s.success&&(y=s.history||[])}catch{}let n={};try{const s=await(await fetch("/api/v1/monitoring/alerts/config")).json();s.success&&(n=s.configs||{})}catch{}const o=Object.entries(i).map(([t,s])=>{const l=n[t]||{cpuThreshold:80,memoryThreshold:90,diskIOThreshold:50,autoRestart:!1,enabled:!1};return` + + ${s.name||t} + + + + + + + + + `}).join(""),a=y.map(t=>{const s=new Date(t.timestamp).toLocaleString(),l=t.notified?"\u2713":"\u2014";return` + + ${s} + ${t.containerName||t.containerId} + ${t.metric||t.type} + ${typeof t.value=="number"?t.value.toFixed(1):t.value}${t.metric==="disk"?" MB/s":"%"} + ${l} + ${t.autoRestartTriggered?"\u21BB":""} + + `}).join(""),r=$?` +
+
+

\u2699\uFE0F Alert Configuration

+ Configure notifications \u2192
-
-
- - -
-
- - -
-
- - -
+
+ + + + + + + + + + + + ${o} +
ContainerCPU %Mem %Disk I/O MB/sAuto-Restart
-
- - - +
+
-
`}h+="
",H.innerHTML=h,H.querySelectorAll(".alert-save-btn").forEach(u=>{u.addEventListener("click",async()=>{const a=u.dataset.container,e=H.querySelector(`.alert-enabled[data-container="${a}"]`)?.checked||!1,n=parseInt(H.querySelector(`.alert-cpu[data-container="${a}"]`)?.value)||80,t=parseInt(H.querySelector(`.alert-mem[data-container="${a}"]`)?.value)||85,i=parseInt(H.querySelector(`.alert-cooldown[data-container="${a}"]`)?.value)||15,o=H.querySelector(`.alert-autorestart[data-container="${a}"]`)?.checked||!1;try{const l=await(await secureFetch(`/api/v1/monitoring/alerts/${a}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({enabled:e,cpuThreshold:n,memoryThreshold:t,cooldownMinutes:i,autoRestart:o})})).json();u.textContent=l.success?"\u2705 Saved":"\u26A0\uFE0F Failed",setTimeout(()=>{u.textContent="Save"},2e3)}catch{u.textContent="\u274C Error",setTimeout(()=>{u.textContent="Save"},2e3)}})})}function B(){I&&clearInterval(I),T?.checked&&(I=setInterval(R,DC.POLL.STATS))}function A(){I&&(clearInterval(I),I=null)}E?.addEventListener("click",()=>{b.classList.add("show"),R(),B()}),N?.addEventListener("click",()=>{b.classList.remove("show"),A()}),b?.addEventListener("click",s=>{s.target===b&&(b.classList.remove("show"),A())}),S?.addEventListener("click",R),T?.addEventListener("change",()=>{T.checked?B():A()}),document.querySelector('[data-panel="stats-aggregated"]')?.addEventListener("click",M),document.querySelector('[data-panel="stats-alerts"]')?.addEventListener("click",j);const w=document.getElementById("stats-history-container"),z=document.getElementById("stats-history-container-area"),f=document.querySelectorAll(".stats-range-btn");let p="1h";function y(s){switch(s){case"1h":return 3600*1e3;case"24h":return 1440*60*1e3;case"7d":return 10080*60*1e3;case"30d":return 720*60*60*1e3;case"1y":return 365*24*60*60*1e3;default:return 3600*1e3}}function v(s){return s==="raw"?"live (10s samples)":s==="hourly"?"hourly average":s==="daily"?"daily average":s}function m(s,h,u,a,e){if(!s||s.length===0)return`
No data for ${escapeHtml(a)}
`;const n=s.map(h).filter(G=>G!=null);if(n.length===0)return`
No data for ${escapeHtml(a)}
`;const t=Math.max(...n,1),i=Math.min(...n,0),o=t-i||1,d=600,l=80,D=4,O=(d-D*2)/Math.max(n.length-1,1),F=n.map((G,W)=>{const X=D+W*O,Q=l-D-(G-i)/o*(l-D*2);return`${X.toFixed(1)},${Q.toFixed(1)}`}).join(" "),q=n[n.length-1],U=n.reduce((G,W)=>G+W,0)/n.length;return` +
+ `:` +
+ \u2B50 Premium Feature +

Upgrade to configure resource alert thresholds per container.

+ +
+ `;A.innerHTML=` + ${r} +
+

\u{1F4CB} Recent Alerts

+ ${a?` +
+ + + + + + + + + + + + ${a} +
TimeContainerMetricValue\u2713?
+
+ `:'
No alerts recorded yet.
'} +
+ `,document.getElementById("save-all-alerts")?.addEventListener("click",async()=>{const t={};document.querySelectorAll("#stats-alerts-container tr[data-container]").forEach(s=>{const l=s.dataset.container;t[l]={cpuThreshold:parseInt(s.querySelector(".alert-cpu")?.value)||80,memoryThreshold:parseInt(s.querySelector(".alert-mem")?.value)||90,diskIOThreshold:parseInt(s.querySelector(".alert-disk")?.value)||50,autoRestart:!!s.querySelector(".alert-autorestart")?.checked,enabled:!0}});try{const l=await(await secureFetch("/api/v1/monitoring/alerts/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({configs:t})})).json(),C=document.getElementById("save-all-alerts");C.textContent=l.success?"\u2705 Saved":"\u274C Failed",setTimeout(()=>{C.textContent="Save All"},2e3)}catch{const l=document.getElementById("save-all-alerts");l.textContent="\u274C Error",setTimeout(()=>{l.textContent="Save All"},2e3)}}),document.getElementById("go-to-notifications")?.addEventListener("click",t=>{t.preventDefault(),h.classList.remove("show"),R(),document.getElementById("manage-notifications")?.click()}),document.querySelectorAll(".alert-test-btn").forEach(t=>{t.addEventListener("click",async()=>{const s=t.textContent;t.textContent="...";try{await secureFetch(`/api/v1/monitoring/alerts/${t.dataset.container}/test`,{method:"POST"}),t.textContent="\u2705",showNotification("Test alert sent for "+t.dataset.name,"success",3e3)}catch{t.textContent="\u274C"}setTimeout(()=>{t.textContent=s},2e3)})}),document.getElementById("upgrade-for-alerts")?.addEventListener("click",()=>{h.classList.remove("show"),R(),typeof openLicenseModal=="function"&&openLicenseModal()})}function H(){L&&clearInterval(L),N?.checked&&(L=setInterval(S,DC.POLL.STATS))}function R(){L&&(clearInterval(L),L=null)}E?.addEventListener("click",()=>{h.classList.add("show"),S(),H()}),P?.addEventListener("click",()=>{h.classList.remove("show"),R()}),h?.addEventListener("click",i=>{i.target===h&&(h.classList.remove("show"),R())}),w?.addEventListener("click",S),N?.addEventListener("change",()=>{N.checked?H():R()}),document.querySelector('[data-panel="stats-aggregated"]')?.addEventListener("click",T),document.querySelector('[data-panel="stats-alerts"]')?.addEventListener("click",j);const x=document.getElementById("stats-history-container"),D=document.getElementById("stats-history-container-area"),g=document.querySelectorAll(".stats-range-btn");let u="1h";function f(i){switch(i){case"1h":return 3600*1e3;case"24h":return 1440*60*1e3;case"7d":return 10080*60*1e3;case"30d":return 720*60*60*1e3;case"1y":return 365*24*60*60*1e3;default:return 3600*1e3}}function m(i){return i==="raw"?"live (10s samples)":i==="hourly"?"hourly average":i==="daily"?"daily average":i}function p(i,$,y,n,e){if(!i||i.length===0)return`
No data for ${escapeHtml(n)}
`;const o=i.map($).filter(G=>G!=null);if(o.length===0)return`
No data for ${escapeHtml(n)}
`;const a=Math.max(...o,1),r=Math.min(...o,0),t=a-r||1,s=600,l=80,C=4,I=(s-C*2)/Math.max(o.length-1,1),F=o.map((G,J)=>{const X=C+J*I,Q=l-C-(G-r)/t*(l-C*2);return`${X.toFixed(1)},${Q.toFixed(1)}`}).join(" "),q=o[o.length-1],U=o.reduce((G,J)=>G+J,0)/o.length;return`
- ${escapeHtml(a)} - last ${q.toFixed(1)}${e} \xB7 avg ${U.toFixed(1)}${e} \xB7 max ${t.toFixed(1)}${e} + ${escapeHtml(n)} + last ${q.toFixed(1)}${e} \xB7 avg ${U.toFixed(1)}${e} \xB7 max ${a.toFixed(1)}${e}
- - + +
- `}function r(){if(!w)return;const s=k||{},h=w.value,u=Object.entries(s);if(u.length===0){w.innerHTML='';return}w.innerHTML=u.map(([a,e])=>``).join(""),h&&s[h]&&(w.value=h)}async function c(){if(!z||!w)return;const s=w.value;if(!s){z.innerHTML='
\u{1F4CA}No container selected.
';return}const h=Date.now(),u=h-y(p);z.innerHTML='
Loading history...
';try{const e=await(await fetch(`/api/v1/monitoring/history/${encodeURIComponent(s)}?startTime=${u}&endTime=${h}`)).json();if(!e.success)throw new Error(e.error||"Failed to load history");const n=e.samples||[],t=e.tier||"raw";if(n.length===0){z.innerHTML=`
\u{1F4CA}No data for the last ${p}. Tier: ${v(t)}.
`;return}const i=t==="raw",o=i?F=>F.cpu?.percent:F=>F.cpu?.avg,d=i?F=>F.memory?.percent:F=>F.memory?.avgPercent,l=i?F=>F.network?.rxMB||0:F=>F.network?.rxMB||0,D=i?F=>F.network?.txMB||0:F=>F.network?.txMB||0;let O=` + `}function d(){if(!x)return;const i=b||{},$=x.value,y=Object.entries(i);if(y.length===0){x.innerHTML='';return}x.innerHTML=y.map(([n,e])=>``).join(""),$&&i[$]&&(x.value=$)}async function c(){if(!D||!x)return;const i=x.value;if(!i){D.innerHTML='
\u{1F4CA}No container selected.
';return}const $=Date.now(),y=$-f(u);D.innerHTML='
Loading history...
';try{const e=await(await fetch(`/api/v1/monitoring/history/${encodeURIComponent(i)}?startTime=${y}&endTime=${$}`)).json();if(!e.success)throw new Error(e.error||"Failed to load history");const o=e.samples||[],a=e.tier||"raw";if(o.length===0){D.innerHTML=`
\u{1F4CA}No data for the last ${u}. Tier: ${m(a)}.
`;return}const r=a==="raw",t=r?F=>F.cpu?.percent:F=>F.cpu?.avg,s=r?F=>F.memory?.percent:F=>F.memory?.avgPercent,l=r?F=>F.network?.rxMB||0:F=>F.network?.rxMB||0,C=r?F=>F.network?.txMB||0:F=>F.network?.txMB||0;let I=`
- ${n.length} samples \xB7 ${escapeHtml(v(t))} \xB7 ${new Date(u).toLocaleString()} \u2192 ${new Date(h).toLocaleString()} + ${o.length} samples \xB7 ${escapeHtml(m(a))} \xB7 ${new Date(y).toLocaleString()} \u2192 ${new Date($).toLocaleString()}
- `;O+=m(n,o,"#2ecc71","CPU","%"),O+=m(n,d,"#3498db","Memory","%"),O+=m(n,l,"#9b59b6","Network RX"," MB"),O+=m(n,D,"#e67e22","Network TX"," MB"),z.innerHTML=O}catch(a){z.innerHTML=`
\u26A0\uFE0FFailed to load history: ${escapeHtml(a.message)}
`}}f.forEach(s=>{s.addEventListener("click",()=>{f.forEach(h=>h.classList.remove("active")),s.classList.add("active"),p=s.dataset.range,c()})}),w?.addEventListener("change",c),document.querySelector('[data-panel="stats-history"]')?.addEventListener("click",()=>{r(),c()})})(),(function(){injectModal("health-modal",`
+ `;I+=p(o,t,"#2ecc71","CPU","%"),I+=p(o,s,"#3498db","Memory","%"),I+=p(o,l,"#9b59b6","Network RX"," MB"),I+=p(o,C,"#e67e22","Network TX"," MB"),D.innerHTML=I}catch(n){D.innerHTML=`
\u26A0\uFE0FFailed to load history: ${escapeHtml(n.message)}
`}}g.forEach(i=>{i.addEventListener("click",()=>{g.forEach($=>$.classList.remove("active")),i.classList.add("active"),u=i.dataset.range,c()})}),x?.addEventListener("change",c),document.querySelector('[data-panel="stats-history"]')?.addEventListener("click",()=>{d(),c()})})(),(function(){injectModal("health-modal",`

\u{1F3E5} Health Check Dashboard

-
`);const b=document.getElementById("health-modal"),E=document.getElementById("health-check-btn"),N=document.getElementById("health-cancel"),S=document.getElementById("health-refresh-btn"),T=document.getElementById("health-status-container"),P=document.getElementById("health-incidents-container"),L=document.getElementById("health-config-container"),H=document.getElementById("health-last-update"),g=document.getElementById("health-add-btn"),I=document.getElementById("health-config-form"),k=document.getElementById("health-form-title"),x=document.getElementById("health-form-cancel"),$=document.getElementById("health-form-save");let C=null;function R(f){return f>=99.9?"var(--ok-fg)":f>=95?"#f39c12":"var(--bad-fg)"}function M(f){const p={critical:"var(--bad-fg)",high:"#ff6b6b",medium:"#f39c12",low:"var(--muted)"};return`${f}`}async function j(){try{const p=await(await fetch("/api/v1/health-checks/status")).json();if(!p.success||!p.status||Object.keys(p.status).length===0){T.innerHTML='
\u{1F3E5}No health checks configured. Go to the Configure tab to add services.
';return}const y=Object.values(p.status);let v='';v+='',v+='',v+='',v+='';for(const m of y){const r=m.status==="up",c=r?"var(--dot-ok)":"var(--dot-bad)",s=m.uptime?.["24h"]??"-",h=m.uptime?.["7d"]??"-",u=m.avgResponseTime!=null?Math.round(m.avgResponseTime)+"ms":"-",a=m.timestamp?timeAgo(m.timestamp):"-";v+=``,v+=``,v+=``,v+=``,v+=``,v+=``,v+=``,v+="",v+=``}v+="
ServiceStatusUptime 24hUptime 7dAvg ResponseLast Check
${escapeHtml(m.name||m.serviceId)}${r?"Up":"Down"}${typeof s=="number"?s.toFixed(1)+"%":s}${typeof h=="number"?h.toFixed(1)+"%":h}${u}${a}
",T.innerHTML=v,H.textContent="Updated "+new Date().toLocaleTimeString(),T.querySelectorAll("tr[data-health-id]").forEach(m=>{m.addEventListener("click",async()=>{const r=m.dataset.healthId,c=document.getElementById("health-detail-"+r);if(c){if(c.style.display!=="none"){c.style.display="none";return}c.style.display="";try{const h=await(await fetch(`/api/v1/health-checks/${r}/stats?hours=24`)).json();if(h.success&&h.stats){const u=h.stats,a=u.responseTime||{};c.querySelector("td").innerHTML=` +
`);const h=document.getElementById("health-modal"),E=document.getElementById("health-check-btn"),P=document.getElementById("health-cancel"),w=document.getElementById("health-refresh-btn"),N=document.getElementById("health-status-container"),O=document.getElementById("health-incidents-container"),z=document.getElementById("health-config-container"),A=document.getElementById("health-last-update"),v=document.getElementById("health-add-btn"),L=document.getElementById("health-config-form"),b=document.getElementById("health-form-title"),M=document.getElementById("health-form-cancel"),k=document.getElementById("health-form-save");let B=null;function S(g){return g>=99.9?"var(--ok-fg)":g>=95?"#f39c12":"var(--bad-fg)"}function T(g){const u={critical:"var(--bad-fg)",high:"#ff6b6b",medium:"#f39c12",low:"var(--muted)"};return`${g}`}async function j(){try{const u=await(await fetch("/api/v1/health-checks/status")).json();if(!u.success||!u.status||Object.keys(u.status).length===0){N.innerHTML='
\u{1F3E5}No health checks configured. Go to the Configure tab to add services.
';return}const f=Object.values(u.status);let m='';m+='',m+='',m+='',m+='';for(const p of f){const d=p.status==="up",c=d?"var(--dot-ok)":"var(--dot-bad)",i=p.uptime?.["24h"]??"-",$=p.uptime?.["7d"]??"-",y=p.avgResponseTime!=null?Math.round(p.avgResponseTime)+"ms":"-",n=p.timestamp?timeAgo(p.timestamp):"-";m+=``,m+=``,m+=``,m+=``,m+=``,m+=``,m+=``,m+="",m+=``}m+="
ServiceStatusUptime 24hUptime 7dAvg ResponseLast Check
${escapeHtml(p.name||p.serviceId)}${d?"Up":"Down"}${typeof i=="number"?i.toFixed(1)+"%":i}${typeof $=="number"?$.toFixed(1)+"%":$}${y}${n}
",N.innerHTML=m,A.textContent="Updated "+new Date().toLocaleTimeString(),N.querySelectorAll("tr[data-health-id]").forEach(p=>{p.addEventListener("click",async()=>{const d=p.dataset.healthId,c=document.getElementById("health-detail-"+d);if(c){if(c.style.display!=="none"){c.style.display="none";return}c.style.display="";try{const $=await(await fetch(`/api/v1/health-checks/${d}/stats?hours=24`)).json();if($.success&&$.stats){const y=$.stats,n=y.responseTime||{};c.querySelector("td").innerHTML=`
-
Total Checks
${u.totalChecks||0}
-
Uptime
${(u.uptime||0).toFixed(2)}%
-
Avg Response
${Math.round(a.avg||0)}ms
-
P95 / P99
${Math.round(a.p95||0)}ms / ${Math.round(a.p99||0)}ms
-
Min Response
${Math.round(a.min||0)}ms
-
Max Response
${Math.round(a.max||0)}ms
-
Up Checks
${u.upChecks||0}
-
Down Checks
${u.downChecks||0}
-
`}else c.querySelector("td").innerHTML='
No detailed stats available for this period.
'}catch(s){c.querySelector("td").innerHTML=`
Failed: ${escapeHtml(s.message)}
`}}})})}catch(f){T.innerHTML=`
Failed to load health status: ${escapeHtml(f.message)}
`}}async function B(){try{const[f,p]=await Promise.all([fetch("/api/v1/health-checks/incidents"),fetch("/api/v1/health-checks/incidents/history?limit=50")]),y=await f.json(),v=await p.json();let m="";const r=y.success&&y.incidents?y.incidents:[];if(r.length>0){m+='

Open Incidents ('+r.length+")

";for(const s of r)m+=`
+
Total Checks
${y.totalChecks||0}
+
Uptime
${(y.uptime||0).toFixed(2)}%
+
Avg Response
${Math.round(n.avg||0)}ms
+
P95 / P99
${Math.round(n.p95||0)}ms / ${Math.round(n.p99||0)}ms
+
Min Response
${Math.round(n.min||0)}ms
+
Max Response
${Math.round(n.max||0)}ms
+
Up Checks
${y.upChecks||0}
+
Down Checks
${y.downChecks||0}
+
`}else c.querySelector("td").innerHTML='
No detailed stats available for this period.
'}catch(i){c.querySelector("td").innerHTML=`
Failed: ${escapeHtml(i.message)}
`}}})})}catch(g){N.innerHTML=`
Failed to load health status: ${escapeHtml(g.message)}
`}}async function H(){try{const[g,u]=await Promise.all([fetch("/api/v1/health-checks/incidents"),fetch("/api/v1/health-checks/incidents/history?limit=50")]),f=await g.json(),m=await u.json();let p="";const d=f.success&&f.incidents?f.incidents:[];if(d.length>0){p+='

Open Incidents ('+d.length+")

";for(const i of d)p+=`
- ${escapeHtml(s.serviceId)} - ${M(s.severity)} + ${escapeHtml(i.serviceId)} + ${T(i.severity)}
-
${escapeHtml(s.message)}
-
Started ${timeAgo(s.createdAt)} \xB7 ${s.occurrences||1} occurrence(s)
-
`;m+="
"}else m+='
All services operational \u2014 no open incidents
';const c=v.success&&v.history?v.history:[];if(c.length>0){m+='

Incident History

',m+='',m+='';for(const s of c){const h=s.status==="resolved",u=h&&s.duration?s.duration<6e4?Math.round(s.duration/1e3)+"s":Math.round(s.duration/6e4)+"m":"-";m+='',m+=``,m+=``,m+=``,m+=``,m+=``,m+=``,m+=""}m+="
ServiceTypeSeverityStatusDurationWhen
${escapeHtml(s.serviceId)}${escapeHtml(s.type)}${M(s.severity)}${s.status}${u}${timeAgo(s.createdAt)}
"}P.innerHTML=m||'
\u{1F6A8}No incidents recorded yet.
'}catch(f){P.innerHTML=`
Failed: ${escapeHtml(f.message)}
`}}async function A(){try{const p=await(await fetch("/api/v1/health-checks/status")).json(),y=p.success&&p.status?Object.values(p.status):[];if(y.length===0){L.innerHTML='
\u2699\uFE0FNo health checks configured yet. Click "Add Health Check" below.
';return}let v='';v+='';for(const m of y){const r=m.status==="up";v+='',v+=``,v+=``,v+=``,v+='"}v+="
ServiceStatusSLA TargetActions
${escapeHtml(m.name||m.serviceId)}${r?"Up":"Down"}${m.sla?.target?m.sla.target+"%":"-"}',v+=``,v+=``,v+="
",L.innerHTML=v}catch(f){L.innerHTML=`
Failed: ${escapeHtml(f.message)}
`}}function w(f,p,y,v,m,r,c){C=f||null,k.textContent=f?"Edit Health Check":"Add Health Check",document.getElementById("health-form-id").value=f||"",document.getElementById("health-form-id").disabled=!!f,document.getElementById("health-form-name").value=p||"",document.getElementById("health-form-url").value=y||"",document.getElementById("health-form-timeout").value=v||1e4,document.getElementById("health-form-codes").value=m||"200",document.getElementById("health-form-sla").value=r||99.9,document.getElementById("health-form-slow").value=c||5e3,I.style.display="",g.style.display="none"}function z(){I.style.display="none",g.style.display="",C=null}g?.addEventListener("click",()=>w("","","",1e4,"200",99.9,5e3)),x?.addEventListener("click",z),$?.addEventListener("click",async()=>{const f=C||document.getElementById("health-form-id").value.trim();if(!f)return showNotification("Service ID is required","warning");const p=document.getElementById("health-form-url").value.trim();if(!p)return showNotification("URL is required","warning");const y=document.getElementById("health-form-codes").value.split(",").map(m=>parseInt(m.trim())).filter(Boolean),v={name:document.getElementById("health-form-name").value.trim()||f,url:p,timeout:parseInt(document.getElementById("health-form-timeout").value)||1e4,expectedStatusCodes:y.length?y:[200],sla:{target:parseFloat(document.getElementById("health-form-sla").value)||99.9},slowResponseThreshold:parseInt(document.getElementById("health-form-slow").value)||5e3};try{$.textContent="Saving...",$.disabled=!0;const r=await(await secureFetch(`/api/v1/health-checks/${encodeURIComponent(f)}/configure`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(v)})).json();if(!r.success)throw new Error(r.error||"Save failed");z(),A(),j()}catch(m){showNotification("Error: "+m.message,"error")}finally{$.textContent="Save",$.disabled=!1}}),document.addEventListener("health-edit",async f=>{const p=f.detail;w(p,"","",1e4,"200",99.9,5e3)}),document.addEventListener("health-delete",async f=>{const p=f.detail;if(confirm(`Delete health check for "${p}"?`))try{const v=await(await secureFetch(`/api/v1/health-checks/${encodeURIComponent(p)}/configure`,{method:"DELETE"})).json();if(!v.success)throw new Error(v.error);A(),j()}catch(y){showNotification("Error: "+y.message,"error")}}),E?.addEventListener("click",()=>{b?.classList.add("show"),j()}),wireModal(b,N),S?.addEventListener("click",j),document.querySelector('[data-panel="health-incidents"]')?.addEventListener("click",B),document.querySelector('[data-panel="health-config"]')?.addEventListener("click",A)})(),(function(){injectModal("updates-modal",`
+
${escapeHtml(i.message)}
+
Started ${timeAgo(i.createdAt)} \xB7 ${i.occurrences||1} occurrence(s)
+
`;p+="
"}else p+='
All services operational \u2014 no open incidents
';const c=m.success&&m.history?m.history:[];if(c.length>0){p+='

Incident History

',p+='',p+='';for(const i of c){const $=i.status==="resolved",y=$&&i.duration?i.duration<6e4?Math.round(i.duration/1e3)+"s":Math.round(i.duration/6e4)+"m":"-";p+='',p+=``,p+=``,p+=``,p+=``,p+=``,p+=``,p+=""}p+="
ServiceTypeSeverityStatusDurationWhen
${escapeHtml(i.serviceId)}${escapeHtml(i.type)}${T(i.severity)}${i.status}${y}${timeAgo(i.createdAt)}
"}O.innerHTML=p||'
\u{1F6A8}No incidents recorded yet.
'}catch(g){O.innerHTML=`
Failed: ${escapeHtml(g.message)}
`}}async function R(){try{const u=await(await fetch("/api/v1/health-checks/status")).json(),f=u.success&&u.status?Object.values(u.status):[];if(f.length===0){z.innerHTML='
\u2699\uFE0FNo health checks configured yet. Click "Add Health Check" below.
';return}let m='';m+='';for(const p of f){const d=p.status==="up";m+='',m+=``,m+=``,m+=``,m+='"}m+="
ServiceStatusSLA TargetActions
${escapeHtml(p.name||p.serviceId)}${d?"Up":"Down"}${p.sla?.target?p.sla.target+"%":"-"}',m+=``,m+=``,m+="
",z.innerHTML=m}catch(g){z.innerHTML=`
Failed: ${escapeHtml(g.message)}
`}}function x(g,u,f,m,p,d,c){B=g||null,b.textContent=g?"Edit Health Check":"Add Health Check",document.getElementById("health-form-id").value=g||"",document.getElementById("health-form-id").disabled=!!g,document.getElementById("health-form-name").value=u||"",document.getElementById("health-form-url").value=f||"",document.getElementById("health-form-timeout").value=m||1e4,document.getElementById("health-form-codes").value=p||"200",document.getElementById("health-form-sla").value=d||99.9,document.getElementById("health-form-slow").value=c||5e3,L.style.display="",v.style.display="none"}function D(){L.style.display="none",v.style.display="",B=null}v?.addEventListener("click",()=>x("","","",1e4,"200",99.9,5e3)),M?.addEventListener("click",D),k?.addEventListener("click",async()=>{const g=B||document.getElementById("health-form-id").value.trim();if(!g)return showNotification("Service ID is required","warning");const u=document.getElementById("health-form-url").value.trim();if(!u)return showNotification("URL is required","warning");const f=document.getElementById("health-form-codes").value.split(",").map(p=>parseInt(p.trim())).filter(Boolean),m={name:document.getElementById("health-form-name").value.trim()||g,url:u,timeout:parseInt(document.getElementById("health-form-timeout").value)||1e4,expectedStatusCodes:f.length?f:[200],sla:{target:parseFloat(document.getElementById("health-form-sla").value)||99.9},slowResponseThreshold:parseInt(document.getElementById("health-form-slow").value)||5e3};try{k.textContent="Saving...",k.disabled=!0;const d=await(await secureFetch(`/api/v1/health-checks/${encodeURIComponent(g)}/configure`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(m)})).json();if(!d.success)throw new Error(d.error||"Save failed");D(),R(),j()}catch(p){showNotification("Error: "+p.message,"error")}finally{k.textContent="Save",k.disabled=!1}}),document.addEventListener("health-edit",async g=>{const u=g.detail;x(u,"","",1e4,"200",99.9,5e3)}),document.addEventListener("health-delete",async g=>{const u=g.detail;if(confirm(`Delete health check for "${u}"?`))try{const m=await(await secureFetch(`/api/v1/health-checks/${encodeURIComponent(u)}/configure`,{method:"DELETE"})).json();if(!m.success)throw new Error(m.error);R(),j()}catch(f){showNotification("Error: "+f.message,"error")}}),E?.addEventListener("click",()=>{h?.classList.add("show"),j()}),wireModal(h,P),w?.addEventListener("click",j),document.querySelector('[data-panel="health-incidents"]')?.addEventListener("click",H),document.querySelector('[data-panel="health-config"]')?.addEventListener("click",R)})(),(function(){injectModal("updates-modal",`

\u2B06\uFE0F Update Management

-
+
+ +
\u{1F4E6} Click "Check for Updates" to scan containers.
@@ -1429,17 +1505,17 @@ Try refreshing in a moment if you see a certificate error.`,"warning",1e4),!1}};
-
`);const b=document.getElementById("updates-modal"),E=document.getElementById("updates-btn"),N=document.getElementById("updates-cancel"),S=document.getElementById("updates-check-btn"),T=document.getElementById("updates-available-container"),P=document.getElementById("updates-history-container"),L=document.getElementById("updates-auto-container"),H=document.getElementById("updates-last-check");async function g(){try{const u=await(await fetch("/api/v1/updates/available")).json();if(!u.success)throw new Error(u.error);const a=u.updates||[];if(a.length===0){T.innerHTML='
\u2705All containers are up to date.
',H.textContent="";return}let e='';e+='';for(const n of a)e+='',e+=``,e+=``,e+=``,e+=``,e+='";e+="
ContainerImageCurrentLatestActions
${escapeHtml(n.containerName)}${escapeHtml(n.imageName)}${escapeHtml(n.currentDigest)}${escapeHtml(n.latestDigest)}',e+=``,e+=``,e+="
",T.innerHTML=e,H.textContent=a.length+" update(s) available",T.querySelectorAll(".update-now-btn").forEach(n=>{n.addEventListener("click",async()=>{const t=n.dataset.id,i=n.dataset.name;if(confirm(`Update "${i}" to the latest version? The container will restart.`)){n.textContent="Updating...",n.disabled=!0;try{const d=await(await secureFetch(`/api/v1/updates/update/${encodeURIComponent(t)}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({autoRollback:!0})})).json();if(d.success)n.textContent="Done!",n.style.background="var(--ok-fg)",setTimeout(()=>g(),2e3);else throw new Error(d.error||"Update failed")}catch(o){n.textContent="Failed",n.style.color="var(--bad-fg)",showNotification("Update error: "+o.message,"error"),setTimeout(()=>{n.textContent="Update",n.disabled=!1,n.style.color="",n.style.background=""},3e3)}}})}),T.querySelectorAll(".rollback-btn").forEach(n=>{n.addEventListener("click",async()=>{const t=n.dataset.id,i=n.dataset.name;if(confirm(`Rollback "${i}" to its previous version?`)){n.textContent="Rolling back...",n.disabled=!0;try{const d=await(await secureFetch(`/api/v1/updates/rollback/${encodeURIComponent(t)}`,{method:"POST"})).json();if(d.success)n.textContent="Rolled back!",setTimeout(()=>g(),2e3);else throw new Error(d.error||"Rollback failed")}catch(o){n.textContent="Failed",showNotification("Rollback error: "+o.message,"error"),setTimeout(()=>{n.textContent="Rollback",n.disabled=!1},3e3)}}})})}catch(h){T.innerHTML=`
Failed: ${escapeHtml(h.message)}
`}}async function I(){S.textContent="\u{1F50D} Checking...",S.disabled=!0;try{const u=await(await secureFetch("/api/v1/updates/check",{method:"POST"})).json();if(!u.success)throw new Error(u.error);S.textContent="\u2705 Done!",await g()}catch(h){S.textContent="\u274C Failed",showNotification("Check error: "+h.message,"error")}setTimeout(()=>{S.textContent="\u{1F50D} Check for Updates",S.disabled=!1},3e3)}async function k(){try{P.innerHTML='
Loading...
';const u=await(await fetch("/api/v1/updates/history?limit=50")).json(),a=u.success&&u.history?u.history:[];if(a.length===0){P.innerHTML='
\u{1F4CB}No update history yet.
';return}let e='';e+='';for(const n of a){const t=n.status==="success",i=n.duration?n.duration<1e3?n.duration+"ms":Math.round(n.duration/1e3)+"s":"-";e+='',e+=``,e+=``,e+=``,e+=``,e+=``,e+="",!t&&n.error&&(e+=``)}e+="
WhenContainerImageDurationStatus
${timeAgo(n.timestamp)}${escapeHtml(n.containerName)}${escapeHtml(n.imageName)}${i}${t?"\u2713 success":"\u2717 failed"}
${escapeHtml(n.error)}
",P.innerHTML=e}catch(h){P.innerHTML=`
Failed: ${escapeHtml(h.message)}
`}}async function x(){try{L.innerHTML='
Loading...
';const[h,u]=await Promise.all([fetch("/api/v1/stats/containers"),fetch("/api/v1/updates/auto-update")]),a=await h.json(),e=await u.json(),n=a.success&&a.stats?a.stats:[],t=e.success&&e.config?e.config:{};if(n.length===0){L.innerHTML='
\u{1F916}No running containers found.
';return}let i='
Auto-updates run during maintenance window (default 2AM-4AM). Daily = every day, Weekly = Sundays, Monthly = 1st of month.
';i+='',i+='';for(const o of n){const d=o.name||o.Names?.[0]?.replace(/^\//,"")||o.Id?.substring(0,12),l=o.containerId||o.Id,D=t[l]||{},O=D.enabled?D.schedule||"weekly":"",F=D.autoRollback!==!1,q=D.maintenanceWindow||"",U=D.lastAutoUpdate?timeAgo(D.lastAutoUpdate):"Never";i+=``,i+=``,i+=``,i+=``,i+=``,i+=``,i+=``,i+=""}i+="
ContainerScheduleWindowRollbackLast RunActions
${escapeHtml(d)} - ${U}
",L.innerHTML=i,L.querySelectorAll(".save-auto-btn").forEach(o=>{o.addEventListener("click",async()=>{const d=o.dataset.id,l=o.closest("tr"),D=l.querySelector(".auto-schedule").value,O=l.querySelector(".auto-rollback").checked,F=l.querySelector(".auto-window").value.trim();o.textContent="Saving...",o.disabled=!0;try{const U=await(await secureFetch(`/api/v1/updates/auto-update/${encodeURIComponent(d)}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({enabled:!!D,schedule:D||"weekly",autoRollback:O,maintenanceWindow:F||void 0})})).json();if(U.success)o.textContent="\u2713 Saved";else throw new Error(U.error)}catch(q){o.textContent="\u2717 Error",showNotification("Save error: "+q.message,"error")}setTimeout(()=>{o.textContent="Save",o.disabled=!1},2e3)})})}catch(h){L.innerHTML=`
Failed: ${escapeHtml(h.message)}
`}}const $=document.getElementById("dashcaddy-current-version"),C=document.getElementById("dashcaddy-update-badge"),R=document.getElementById("dashcaddy-update-details"),M=document.getElementById("dashcaddy-new-version"),j=document.getElementById("dashcaddy-changelog"),B=document.getElementById("dashcaddy-apply-btn"),A=document.getElementById("dashcaddy-check-btn"),w=document.getElementById("dashcaddy-rollback-btn"),z=document.getElementById("dashcaddy-status-bar"),f=document.getElementById("dashcaddy-history-container");let p=null;function y(h,u){z&&(z.style.display="block",z.style.background=u==="error"?"var(--bad-bg)":u==="success"?"var(--ok-bg)":"var(--bg)",z.style.color=u==="error"?"var(--bad-fg)":u==="success"?"var(--ok-fg)":"var(--fg)",z.textContent=h)}async function v(){try{const u=await(await fetch("/api/v1/system/version")).json();if(u.success){const a=u.commit&&u.commit!=="unknown"?u.commit:null;$.textContent="v"+u.version+(a?" ("+a.substring(0,7)+")":"")}}catch{$.textContent="Unable to fetch version"}}async function m(h){h||(A.textContent="Checking...",A.disabled=!0);try{const a=await(await fetch("/api/v1/system/update-check")).json();if(p=a,a.success&&a.available&&a.remote){C.style.display="",R.style.display="",M.textContent="v"+a.remote.version,j.textContent=a.remote.changelog||"No changelog available.";const e=document.getElementById("updates-btn");if(e&&!e.querySelector(".update-dot")){const t=document.createElement("span");t.className="update-dot",t.style.cssText="position:absolute;top:2px;right:2px;width:8px;height:8px;border-radius:50%;background:var(--accent);",e.style.position="relative",e.appendChild(t)}const n=document.getElementById("updates-dashcaddy-tab");if(n&&!n.querySelector(".update-dot")){const t=document.createElement("span");t.className="update-dot",t.style.cssText="display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--accent);margin-left:4px;vertical-align:middle;",n.appendChild(t)}}else C.style.display="none",R.style.display="none",await v(),h||y("You are running the latest version.","success");h||(A.textContent="Check for Updates",A.disabled=!1)}catch(u){h||(y("Failed to check: "+u.message,"error"),A.textContent="Check for Updates",A.disabled=!1)}}async function r(){if(!confirm("Apply DashCaddy update? The API container will restart."))return!1;B.textContent="Updating...",B.disabled=!0,y("Downloading and applying update...","info");try{const u=await(await secureFetch("/api/v1/system/update-apply",{method:"POST"})).json();if(u.success)return y("Update initiated: v"+(u.fromVersion||"?")+" \u2192 v"+(u.toVersion||"?")+". The container will restart shortly.","success"),B.textContent="Applied!",document.querySelectorAll(".update-dot").forEach(a=>a.remove()),!0;throw new Error(u.error||"Update failed")}catch(h){throw y("Update failed: "+h.message,"error"),B.textContent="Update Now",B.disabled=!1,h}}async function c(){try{const u=await(await fetch("/api/v1/system/update-history")).json(),a=u.success&&u.history?u.history:[];if(a.length===0){f.innerHTML='
\u{1F4E6}No self-update history.
';return}let e='';e+='';for(const n of a){const t=n.status==="success"?"\u2713 success":n.status==="pending"?"\u23F3 pending":n.status==="partial"?"\u26A0 partial":"\u2717 "+n.status,i=n.status==="success"?"var(--ok-fg)":n.status==="pending"?"var(--muted)":"var(--bad-fg)";e+='',e+='",e+='",e+='",e+='",e+="",n.error&&(e+='"),n.note&&(e+='")}e+="
WhenVersionFromStatus
'+timeAgo(n.timestamp)+"v'+escapeHtml(n.version)+(n.rollback?" (rollback)":"")+"v'+escapeHtml(n.fromVersion||"?")+"'+t+"
'+escapeHtml(n.error)+"
'+escapeHtml(n.note)+"
",f.innerHTML=e}catch(h){f.innerHTML='
Failed: '+escapeHtml(h.message)+"
"}}async function s(){try{const u=await(await fetch("/api/v1/system/rollback-versions")).json(),a=u.success&&u.versions?u.versions:[];if(a.length===0){showNotification("No rollback versions available.","info");return}const e=prompt(`Available rollback versions: -`+a.join(` +
`);const h=document.getElementById("updates-modal"),E=document.getElementById("updates-btn"),P=document.getElementById("updates-cancel"),w=document.getElementById("updates-check-btn"),N=document.getElementById("updates-available-container"),O=document.getElementById("updates-history-container"),z=document.getElementById("updates-auto-container"),A=document.getElementById("updates-last-check");async function v(){try{const n=await(await fetch("/api/v1/updates/available")).json();if(!n.success)throw new Error(n.error);const e=n.updates||[];if(e.length===0){N.innerHTML='
\u2705All containers are up to date.
',A.textContent="",document.getElementById("updates-update-all-btn").style.display="none",document.getElementById("updates-count-badge").style.display="none",window._pendingUpdates=[];return}let o='';o+='';for(const t of e){const s=(()=>{const l=window.APPS||[];for(const C of l)if(C.containerId===t.containerId||C.name===t.containerName||C.id===t.containerName)return C.id;return t.containerName})();o+=``,o+=``,o+=``,o+=``,o+=``,o+='"}o+="
ContainerImageCurrentLatestActions
${escapeHtml(t.containerName)}${escapeHtml(t.imageName)}${escapeHtml(t.currentDigest)}${escapeHtml(t.latestDigest)}',o+=``,o+=``,o+="
",N.innerHTML=o,A.textContent=e.length+" update(s) available";const a=document.getElementById("updates-count-badge"),r=document.getElementById("updates-update-all-btn");a&&(a.textContent=e.length+" pending",a.style.display=""),r&&e.length>0&&(r.style.display=""),window._pendingUpdates=e,N.querySelectorAll(".update-now-btn").forEach(t=>{t.addEventListener("click",async()=>{const s=t.dataset.id,l=t.dataset.name;if(confirm(`Update "${l}" to the latest version? The container will restart.`)){t.textContent="Updating...",t.disabled=!0;try{const I=await(await secureFetch(`/api/v1/updates/update/${encodeURIComponent(s)}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({autoRollback:!0})})).json();if(I.success)t.textContent="Done!",t.style.background="var(--ok-fg)",setTimeout(()=>v(),2e3);else throw new Error(I.error||"Update failed")}catch(C){t.textContent="Failed",t.style.color="var(--bad-fg)",showNotification("Update error: "+C.message,"error"),setTimeout(()=>{t.textContent="Update",t.disabled=!1,t.style.color="",t.style.background=""},3e3)}}})}),N.querySelectorAll(".rollback-btn").forEach(t=>{t.addEventListener("click",async()=>{const s=t.dataset.id,l=t.dataset.name;if(confirm(`Rollback "${l}" to its previous version?`)){t.textContent="Rolling back...",t.disabled=!0;try{const I=await(await secureFetch(`/api/v1/updates/rollback/${encodeURIComponent(s)}`,{method:"POST"})).json();if(I.success)t.textContent="Rolled back!",setTimeout(()=>v(),2e3);else throw new Error(I.error||"Rollback failed")}catch(C){t.textContent="Failed",showNotification("Rollback error: "+C.message,"error"),setTimeout(()=>{t.textContent="Rollback",t.disabled=!1},3e3)}}})})}catch(y){N.innerHTML=`
Failed: ${escapeHtml(y.message)}
`}}async function L(){const y=window._pendingUpdates||[];if(!y.length)return;const n=document.getElementById("updates-update-all-btn");if(!confirm(`Update all ${y.length} containers? Each will restart.`))return;n.textContent="\u23F3 Updating...",n.disabled=!0;let e=0,o=0;for(const a of y)try{(await(await secureFetch(`/api/v1/updates/update/${encodeURIComponent(a.containerId)}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({autoRollback:!0})})).json()).success?e++:o++}catch{o++}n.textContent="\u2705 Done",showNotification(`Update all: ${e} succeeded, ${o} failed.`,e>0&&o===0?"success":"error"),setTimeout(()=>{n.textContent="\u2B06\uFE0F Update All",n.disabled=!1,v()},3e3)}document.getElementById("updates-update-all-btn")?.addEventListener("click",L);async function b(){w.textContent="\u{1F50D} Checking...",w.disabled=!0;try{const n=await(await secureFetch("/api/v1/updates/check",{method:"POST"})).json();if(!n.success)throw new Error(n.error);w.textContent="\u2705 Done!",await v()}catch(y){w.textContent="\u274C Failed",showNotification("Check error: "+y.message,"error")}setTimeout(()=>{w.textContent="\u{1F50D} Check for Updates",w.disabled=!1},3e3)}async function M(){try{O.innerHTML='
Loading...
';const n=await(await fetch("/api/v1/updates/history?limit=50")).json(),e=n.success&&n.history?n.history:[];if(e.length===0){O.innerHTML='
\u{1F4CB}No update history yet.
';return}let o='';o+='';for(const a of e){const r=a.status==="success",t=a.duration?a.duration<1e3?a.duration+"ms":Math.round(a.duration/1e3)+"s":"-";o+='',o+=``,o+=``,o+=``,o+=``,o+=``,o+="",!r&&a.error&&(o+=``)}o+="
WhenContainerImageDurationStatus
${timeAgo(a.timestamp)}${escapeHtml(a.containerName)}${escapeHtml(a.imageName)}${t}${r?"\u2713 success":"\u2717 failed"}
${escapeHtml(a.error)}
",O.innerHTML=o}catch(y){O.innerHTML=`
Failed: ${escapeHtml(y.message)}
`}}async function k(){try{z.innerHTML='
Loading...
';const[y,n]=await Promise.all([fetch("/api/v1/stats/containers"),fetch("/api/v1/updates/auto-update")]),e=await y.json(),o=await n.json(),a=e.success&&e.stats?e.stats:[],r=o.success&&o.config?o.config:{};if(a.length===0){z.innerHTML='
\u{1F916}No running containers found.
';return}let t='
Auto-updates run during maintenance window (default 2AM-4AM). Daily = every day, Weekly = Sundays, Monthly = 1st of month.
';t+='',t+='';for(const s of a){const l=s.name||s.Names?.[0]?.replace(/^\//,"")||s.Id?.substring(0,12),C=s.containerId||s.Id,I=r[C]||{},F=I.enabled?I.schedule||"weekly":"",q=I.autoRollback!==!1,U=I.maintenanceWindow||"",G=I.lastAutoUpdate?timeAgo(I.lastAutoUpdate):"Never";t+=``,t+=``,t+=``,t+=``,t+=``,t+=``,t+=``,t+=""}t+="
ContainerScheduleWindowRollbackLast RunActions
${escapeHtml(l)} + ${G}
",z.innerHTML=t,z.querySelectorAll(".save-auto-btn").forEach(s=>{s.addEventListener("click",async()=>{const l=s.dataset.id,C=s.closest("tr"),I=C.querySelector(".auto-schedule").value,F=C.querySelector(".auto-rollback").checked,q=C.querySelector(".auto-window").value.trim();s.textContent="Saving...",s.disabled=!0;try{const G=await(await secureFetch(`/api/v1/updates/auto-update/${encodeURIComponent(l)}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({enabled:!!I,schedule:I||"weekly",autoRollback:F,maintenanceWindow:q||void 0})})).json();if(G.success)s.textContent="\u2713 Saved";else throw new Error(G.error)}catch(U){s.textContent="\u2717 Error",showNotification("Save error: "+U.message,"error")}setTimeout(()=>{s.textContent="Save",s.disabled=!1},2e3)})})}catch(y){z.innerHTML=`
Failed: ${escapeHtml(y.message)}
`}}const B=document.getElementById("dashcaddy-current-version"),S=document.getElementById("dashcaddy-update-badge"),T=document.getElementById("dashcaddy-update-details"),j=document.getElementById("dashcaddy-new-version"),H=document.getElementById("dashcaddy-changelog"),R=document.getElementById("dashcaddy-apply-btn"),x=document.getElementById("dashcaddy-check-btn"),D=document.getElementById("dashcaddy-rollback-btn"),g=document.getElementById("dashcaddy-status-bar"),u=document.getElementById("dashcaddy-history-container");let f=null;function m(y,n){g&&(g.style.display="block",g.style.background=n==="error"?"var(--bad-bg)":n==="success"?"var(--ok-bg)":"var(--bg)",g.style.color=n==="error"?"var(--bad-fg)":n==="success"?"var(--ok-fg)":"var(--fg)",g.textContent=y)}async function p(){try{const n=await(await fetch("/api/v1/system/version")).json();if(n.success){const e=n.commit&&n.commit!=="unknown"?n.commit:null;B.textContent="v"+n.version+(e?" ("+e.substring(0,7)+")":"")}}catch{B.textContent="Unable to fetch version"}}async function d(y){y||(x.textContent="Checking...",x.disabled=!0);try{const e=await(await fetch("/api/v1/system/update-check")).json();if(f=e,e.success&&e.available&&e.remote){S.style.display="",T.style.display="",j.textContent="v"+e.remote.version,H.textContent=e.remote.changelog||"No changelog available.";const o=document.getElementById("updates-btn");if(o&&!o.querySelector(".update-dot")){const r=document.createElement("span");r.className="update-dot",r.style.cssText="position:absolute;top:2px;right:2px;width:8px;height:8px;border-radius:50%;background:var(--accent);",o.style.position="relative",o.appendChild(r)}const a=document.getElementById("updates-dashcaddy-tab");if(a&&!a.querySelector(".update-dot")){const r=document.createElement("span");r.className="update-dot",r.style.cssText="display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--accent);margin-left:4px;vertical-align:middle;",a.appendChild(r)}}else S.style.display="none",T.style.display="none",await p(),y||m("You are running the latest version.","success");y||(x.textContent="Check for Updates",x.disabled=!1)}catch(n){y||(m("Failed to check: "+n.message,"error"),x.textContent="Check for Updates",x.disabled=!1)}}async function c(){if(!confirm("Apply DashCaddy update? The API container will restart."))return!1;R.textContent="Updating...",R.disabled=!0,m("Downloading and applying update...","info");try{const n=await(await secureFetch("/api/v1/system/update-apply",{method:"POST"})).json();if(n.success)return m("Update initiated: v"+(n.fromVersion||"?")+" \u2192 v"+(n.toVersion||"?")+". The container will restart shortly.","success"),R.textContent="Applied!",document.querySelectorAll(".update-dot").forEach(e=>e.remove()),!0;throw new Error(n.error||"Update failed")}catch(y){throw m("Update failed: "+y.message,"error"),R.textContent="Update Now",R.disabled=!1,y}}async function i(){try{const n=await(await fetch("/api/v1/system/update-history")).json(),e=n.success&&n.history?n.history:[];if(e.length===0){u.innerHTML='
\u{1F4E6}No self-update history.
';return}let o='';o+='';for(const a of e){const r=a.status==="success"?"\u2713 success":a.status==="pending"?"\u23F3 pending":a.status==="partial"?"\u26A0 partial":"\u2717 "+a.status,t=a.status==="success"?"var(--ok-fg)":a.status==="pending"?"var(--muted)":"var(--bad-fg)";o+='',o+='",o+='",o+='",o+='",o+="",a.error&&(o+='"),a.note&&(o+='")}o+="
WhenVersionFromStatus
'+timeAgo(a.timestamp)+"v'+escapeHtml(a.version)+(a.rollback?" (rollback)":"")+"v'+escapeHtml(a.fromVersion||"?")+"'+r+"
'+escapeHtml(a.error)+"
'+escapeHtml(a.note)+"
",u.innerHTML=o}catch(y){u.innerHTML='
Failed: '+escapeHtml(y.message)+"
"}}async function $(){try{const n=await(await fetch("/api/v1/system/rollback-versions")).json(),e=n.success&&n.versions?n.versions:[];if(e.length===0){showNotification("No rollback versions available.","info");return}const o=prompt(`Available rollback versions: +`+e.join(` `)+` -Enter version to rollback to:`);if(!e)return;if(!a.includes(e)){showNotification("Invalid version: "+e,"error");return}if(!confirm("Rollback DashCaddy to v"+e+"? The container will restart."))return;y("Rolling back to v"+e+"...","info");const t=await(await secureFetch("/api/v1/system/rollback",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({version:e})})).json();if(t.success)y("Rollback to v"+e+" initiated. Container will restart.","success");else throw new Error(t.error||"Rollback failed")}catch(h){y("Rollback failed: "+h.message,"error")}}A?.addEventListener("click",()=>m(!1)),B?.addEventListener("click",()=>r().catch(()=>{})),w?.addEventListener("click",s),S?.addEventListener("click",I),E?.addEventListener("click",()=>{b?.classList.add("show"),g()}),wireModal(b,N),document.querySelector('[data-panel="updates-history"]')?.addEventListener("click",k),document.querySelector('[data-panel="updates-auto"]')?.addEventListener("click",x),document.querySelector('[data-panel="updates-dashcaddy"]')?.addEventListener("click",()=>{v(),c(),p||m(!0)}),window.dcApplyUpdate=r,window.dcCheckForUpdate=m,setTimeout(()=>m(!0),5e3)})(),(function(){injectModal("docker-resources-modal",`
+Enter version to rollback to:`);if(!o)return;if(!e.includes(o)){showNotification("Invalid version: "+o,"error");return}if(!confirm("Rollback DashCaddy to v"+o+"? The container will restart."))return;m("Rolling back to v"+o+"...","info");const r=await(await secureFetch("/api/v1/system/rollback",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({version:o})})).json();if(r.success)m("Rollback to v"+o+" initiated. Container will restart.","success");else throw new Error(r.error||"Rollback failed")}catch(y){m("Rollback failed: "+y.message,"error")}}x?.addEventListener("click",()=>d(!1)),R?.addEventListener("click",()=>c().catch(()=>{})),D?.addEventListener("click",$),w?.addEventListener("click",b),E?.addEventListener("click",()=>{h?.classList.add("show"),v()}),wireModal(h,P),window.openUpdateModal=function(y){h?.classList.add("show"),v().then(()=>{if(!y)return;const n=N.querySelector(`[data-app-id="${y}"]`);n&&(n.scrollIntoView({behavior:"smooth",block:"center"}),n.style.background="rgba(249,115,22,0.15)",setTimeout(()=>{n.style.background=""},3e3))})},document.querySelector('[data-panel="updates-history"]')?.addEventListener("click",M),document.querySelector('[data-panel="updates-auto"]')?.addEventListener("click",k),document.querySelector('[data-panel="updates-dashcaddy"]')?.addEventListener("click",()=>{p(),i(),f||d(!0)}),window.dcApplyUpdate=c,window.dcCheckForUpdate=d,setTimeout(()=>d(!0),5e3)})(),(function(){injectModal("docker-resources-modal",`

\u{1F433} Docker Resources

@@ -1488,7 +1564,7 @@ Enter version to rollback to:`);if(!e)return;if(!a.includes(e)){showNotification
-
`);const b=document.getElementById("docker-resources-modal"),E=document.getElementById("docker-resources-btn"),N=document.getElementById("dr-close");function S(H){if(!H||H===0)return"0 B";const g=["B","KB","MB","GB","TB"],I=Math.floor(Math.log(Math.abs(H))/Math.log(1024));return(H/Math.pow(1024,I)).toFixed(1)+" "+g[I]}async function T(){const H=document.getElementById("dr-vol-list");try{const I=(await getJSON("/api/v1/docker/volumes")).volumes||[];if(I.length===0){H.innerHTML='
\u{1F4E6}No volumes found.
';return}let k='';k+='';for(const x of I){const $=x.name==="buildkit"||x.name.length===64;k+='',k+=``,k+=``,k+=``,k+='"}k+="
NameDriverScopeActions
${escapeHtml(x.name.length>40?x.name.substring(0,37)+"...":x.name)}${escapeHtml(x.driver)}${escapeHtml(x.scope)}',$||(k+=``),k+="
",H.innerHTML=k,H.querySelectorAll(".dr-vol-del").forEach(x=>{x.addEventListener("click",async()=>{if(confirm(`Delete volume "${x.dataset.name}"? Data will be lost.`)){x.textContent="...",x.disabled=!0;try{await deleteAPI(`/api/v1/docker/volumes/${encodeURIComponent(x.dataset.name)}?force=true`),T()}catch($){showNotification("Delete failed: "+$.message,"error"),x.textContent="Delete",x.disabled=!1}}})})}catch(g){H.innerHTML=`
Failed: ${escapeHtml(g.message)}
`}}document.getElementById("dr-vol-create")?.addEventListener("click",async()=>{const H=document.getElementById("dr-vol-name"),g=H.value.trim();if(!g){showNotification("Enter a volume name","warning");return}try{await postJSON("/api/v1/docker/volumes",{name:g}),H.value="",showNotification(`Volume "${g}" created`,"success"),T()}catch(I){showNotification("Create failed: "+I.message,"error")}});async function P(){const H=document.getElementById("dr-net-list");try{const I=(await getJSON("/api/v1/docker/networks")).networks||[];if(I.length===0){H.innerHTML='
\u{1F310}No networks found.
';return}let k='';k+='';for(const x of I){const $=["bridge","host","none"].includes(x.name);k+='',k+=``,k+=``,k+=``,k+=``,k+='"}k+="
NameDriverScopeContainersActions
${escapeHtml(x.name)}${escapeHtml(x.driver)}${escapeHtml(x.scope)}${x.containers}',$||(k+=``),k+="
",H.innerHTML=k,H.querySelectorAll(".dr-net-del").forEach(x=>{x.addEventListener("click",async()=>{if(confirm(`Delete network "${x.dataset.name}"?`)){x.textContent="...",x.disabled=!0;try{await deleteAPI(`/api/v1/docker/networks/${encodeURIComponent(x.dataset.id)}`),P()}catch($){showNotification("Delete failed: "+$.message,"error"),x.textContent="Delete",x.disabled=!1}}})})}catch(g){H.innerHTML=`
Failed: ${escapeHtml(g.message)}
`}}document.getElementById("dr-net-create")?.addEventListener("click",async()=>{const H=document.getElementById("dr-net-name"),g=document.getElementById("dr-net-driver"),I=H.value.trim();if(!I){showNotification("Enter a network name","warning");return}try{await postJSON("/api/v1/docker/networks",{name:I,driver:g.value}),H.value="",showNotification(`Network "${I}" created`,"success"),P()}catch(k){showNotification("Create failed: "+k.message,"error")}});async function L(){const H=document.getElementById("dr-disk-content");try{const g=await getJSON("/api/v1/docker/disk-usage"),I=[{label:"Images",icon:"\u{1F4C0}",count:g.images.count,size:g.images.size,reclaimable:g.images.reclaimable},{label:"Containers",icon:"\u{1F4E6}",count:g.containers.count,size:g.containers.size,extra:`${g.containers.running} running`},{label:"Volumes",icon:"\u{1F4BE}",count:g.volumes.count,size:g.volumes.size,reclaimable:g.volumes.reclaimable},{label:"Build Cache",icon:"\u{1F527}",count:g.buildCache.count,size:g.buildCache.size,reclaimable:g.buildCache.reclaimable}];let k=`
Total: ${S(g.totalSize)}
`;k+='
';for(const x of I)k+='
',k+=`
${x.icon} ${x.label} (${x.count})
`,k+=`
${S(x.size)}
`,x.reclaimable>0&&(k+=`
Reclaimable: ${S(x.reclaimable)}
`),x.extra&&(k+=`
${x.extra}
`),k+="
";k+="
",H.innerHTML=k}catch(g){H.innerHTML=`
Failed: ${escapeHtml(g.message)}
`}}E?.addEventListener("click",()=>{b?.classList.add("show"),T()}),wireModal(b,N),document.querySelector('[data-panel="dr-networks"]')?.addEventListener("click",P),document.querySelector('[data-panel="dr-disk"]')?.addEventListener("click",L)})(),(function(){injectModal("compose-import-modal",`
+
`);const h=document.getElementById("docker-resources-modal"),E=document.getElementById("docker-resources-btn"),P=document.getElementById("dr-close");function w(A){if(!A||A===0)return"0 B";const v=["B","KB","MB","GB","TB"],L=Math.floor(Math.log(Math.abs(A))/Math.log(1024));return(A/Math.pow(1024,L)).toFixed(1)+" "+v[L]}async function N(){const A=document.getElementById("dr-vol-list");try{const L=(await getJSON("/api/v1/docker/volumes")).volumes||[];if(L.length===0){A.innerHTML='
\u{1F4E6}No volumes found.
';return}let b='';b+='';for(const M of L){const k=M.name==="buildkit"||M.name.length===64;b+='',b+=``,b+=``,b+=``,b+='"}b+="
NameDriverScopeActions
${escapeHtml(M.name.length>40?M.name.substring(0,37)+"...":M.name)}${escapeHtml(M.driver)}${escapeHtml(M.scope)}',k||(b+=``),b+="
",A.innerHTML=b,A.querySelectorAll(".dr-vol-del").forEach(M=>{M.addEventListener("click",async()=>{if(confirm(`Delete volume "${M.dataset.name}"? Data will be lost.`)){M.textContent="...",M.disabled=!0;try{await deleteAPI(`/api/v1/docker/volumes/${encodeURIComponent(M.dataset.name)}?force=true`),N()}catch(k){showNotification("Delete failed: "+k.message,"error"),M.textContent="Delete",M.disabled=!1}}})})}catch(v){A.innerHTML=`
Failed: ${escapeHtml(v.message)}
`}}document.getElementById("dr-vol-create")?.addEventListener("click",async()=>{const A=document.getElementById("dr-vol-name"),v=A.value.trim();if(!v){showNotification("Enter a volume name","warning");return}try{await postJSON("/api/v1/docker/volumes",{name:v}),A.value="",showNotification(`Volume "${v}" created`,"success"),N()}catch(L){showNotification("Create failed: "+L.message,"error")}});async function O(){const A=document.getElementById("dr-net-list");try{const L=(await getJSON("/api/v1/docker/networks")).networks||[];if(L.length===0){A.innerHTML='
\u{1F310}No networks found.
';return}let b='';b+='';for(const M of L){const k=["bridge","host","none"].includes(M.name);b+='',b+=``,b+=``,b+=``,b+=``,b+='"}b+="
NameDriverScopeContainersActions
${escapeHtml(M.name)}${escapeHtml(M.driver)}${escapeHtml(M.scope)}${M.containers}',k||(b+=``),b+="
",A.innerHTML=b,A.querySelectorAll(".dr-net-del").forEach(M=>{M.addEventListener("click",async()=>{if(confirm(`Delete network "${M.dataset.name}"?`)){M.textContent="...",M.disabled=!0;try{await deleteAPI(`/api/v1/docker/networks/${encodeURIComponent(M.dataset.id)}`),O()}catch(k){showNotification("Delete failed: "+k.message,"error"),M.textContent="Delete",M.disabled=!1}}})})}catch(v){A.innerHTML=`
Failed: ${escapeHtml(v.message)}
`}}document.getElementById("dr-net-create")?.addEventListener("click",async()=>{const A=document.getElementById("dr-net-name"),v=document.getElementById("dr-net-driver"),L=A.value.trim();if(!L){showNotification("Enter a network name","warning");return}try{await postJSON("/api/v1/docker/networks",{name:L,driver:v.value}),A.value="",showNotification(`Network "${L}" created`,"success"),O()}catch(b){showNotification("Create failed: "+b.message,"error")}});async function z(){const A=document.getElementById("dr-disk-content");try{const v=await getJSON("/api/v1/docker/disk-usage"),L=[{label:"Images",icon:"\u{1F4C0}",count:v.images.count,size:v.images.size,reclaimable:v.images.reclaimable},{label:"Containers",icon:"\u{1F4E6}",count:v.containers.count,size:v.containers.size,extra:`${v.containers.running} running`},{label:"Volumes",icon:"\u{1F4BE}",count:v.volumes.count,size:v.volumes.size,reclaimable:v.volumes.reclaimable},{label:"Build Cache",icon:"\u{1F527}",count:v.buildCache.count,size:v.buildCache.size,reclaimable:v.buildCache.reclaimable}];let b=`
Total: ${w(v.totalSize)}
`;b+='
';for(const M of L)b+='
',b+=`
${M.icon} ${M.label} (${M.count})
`,b+=`
${w(M.size)}
`,M.reclaimable>0&&(b+=`
Reclaimable: ${w(M.reclaimable)}
`),M.extra&&(b+=`
${M.extra}
`),b+="
";b+="
",A.innerHTML=b}catch(v){A.innerHTML=`
Failed: ${escapeHtml(v.message)}
`}}E?.addEventListener("click",()=>{h?.classList.add("show"),N()}),wireModal(h,P),document.querySelector('[data-panel="dr-networks"]')?.addEventListener("click",O),document.querySelector('[data-panel="dr-disk"]')?.addEventListener("click",z)})(),(function(){injectModal("compose-import-modal",`

\u{1F4E6} Import Docker Compose

@@ -1529,8 +1605,8 @@ Enter version to rollback to:`);if(!e)return;if(!a.includes(e)){showNotification
-
`);const b=document.getElementById("compose-import-modal"),E=document.getElementById("compose-import-btn"),N=document.getElementById("compose-cancel");wireModal(b,N);let S=null;function T(L){document.getElementById("compose-step-paste").style.display=L==="paste"?"":"none",document.getElementById("compose-step-preview").style.display=L==="preview"?"":"none",document.getElementById("compose-step-progress").style.display=L==="progress"?"":"none"}E?.addEventListener("click",()=>{T("paste"),S=null,document.getElementById("compose-yaml").value="",document.getElementById("compose-stack-name").value="",b?.classList.add("show")}),document.getElementById("compose-file-upload")?.addEventListener("change",L=>{const H=L.target.files[0];if(!H)return;const g=new FileReader;g.onload=()=>{document.getElementById("compose-yaml").value=g.result},g.readAsText(H)}),document.getElementById("compose-parse-btn")?.addEventListener("click",async()=>{const L=document.getElementById("compose-yaml").value.trim(),H=document.getElementById("compose-stack-name").value.trim()||"stack";if(!L){showNotification("Paste a docker-compose.yml","warning");return}const g=document.getElementById("compose-parse-btn"),I=g.textContent;g.textContent="Parsing...",g.disabled=!0;try{const k=await postJSON("/api/v1/apps/import-compose",{yaml:L,stackName:H});S=k,S.stackName=H,P(k),T("preview")}catch(k){showNotification("Parse failed: "+k.message,"error")}finally{g.textContent=I,g.disabled=!1}});function P(L){const H=document.getElementById("compose-preview-content");let g="";L.networks&&L.networks.length>0&&(g+=`
Networks: ${L.networks.map(I=>`${escapeHtml(I)}`).join(", ")}
`),L.volumes&&L.volumes.length>0&&(g+=`
Volumes: ${L.volumes.map(I=>`${escapeHtml(I)}`).join(", ")}
`),g+=`
${L.services.length} service(s)
`,g+='
';for(const I of L.services){const k=I.skip?"var(--bad-fg)":"var(--border)";if(g+=`
`,g+=`
${escapeHtml(I.name)}`,I.skip&&(g+=` \u2014 skipped: ${escapeHtml(I.reason)}`),g+="
",!I.skip&&(g+=`
Image: ${escapeHtml(I.image)}
`,I.ports?.length&&(g+=`
Ports: ${I.ports.map(x=>`${x.host}:${x.container}`).join(", ")}
`),I.volumes?.length&&(g+=`
Volumes: ${I.volumes.length}
`),Object.keys(I.environment||{}).length&&(g+=`
Env vars: ${Object.keys(I.environment).length}
`),I.envFileWarning&&(g+=`
\u26A0 ${escapeHtml(I.envFileWarning)}
`),I.resources?.cpus||I.resources?.memory)){const x=[];I.resources.cpus&&x.push(`CPU: ${I.resources.cpus}`),I.resources.memory&&x.push(`Mem: ${I.resources.memory}MB`),g+=`
Limits: ${x.join(", ")}
`}g+="
"}g+="
",H.innerHTML=g}document.getElementById("compose-back-btn")?.addEventListener("click",()=>T("paste")),document.getElementById("compose-deploy-btn")?.addEventListener("click",async()=>{if(!S)return;const L=document.getElementById("compose-deploy-btn");L.textContent="Deploying...",L.disabled=!0,T("progress");const H=document.getElementById("compose-progress-content");H.innerHTML='
Deploying services...
';try{const g=await postJSON("/api/v1/apps/deploy-compose",{services:S.services,networks:S.networks,stackName:S.stackName});let I=`
Stack "${escapeHtml(g.stackName)}" \u2014 Deployment Complete
`;I+='
';for(const k of g.results){const x=k.status==="deployed"||k.status==="created"?"\u2705":k.status==="exists"?"\u26A1":k.status==="skipped"?"\u23ED":"\u274C";I+='
',I+=`${x} ${escapeHtml(k.name)} (${k.type}) \u2014 ${escapeHtml(k.status)}`,k.error&&(I+=` ${escapeHtml(k.error)}`),k.subdomain&&(I+=` \u2192 ${escapeHtml(k.subdomain)}`),k.reason&&(I+=` (${escapeHtml(k.reason)})`),I+="
"}I+="
",I+='',H.innerHTML=I,document.getElementById("compose-done-btn")?.addEventListener("click",()=>{b?.classList.remove("show"),typeof window.loadServices=="function"&&window.loadServices().then(()=>{typeof window.buildGrid=="function"&&window.buildGrid()})}),showNotification(`Stack "${g.stackName}" deployed`,"success")}catch(g){H.innerHTML=`
Deployment failed: ${escapeHtml(g.message)}
- `,document.getElementById("compose-retry-btn")?.addEventListener("click",()=>T("paste"))}finally{L.textContent="Deploy All",L.disabled=!1}})})(),(function(){injectModal("exec-modal",`
+
`);const h=document.getElementById("compose-import-modal"),E=document.getElementById("compose-import-btn"),P=document.getElementById("compose-cancel");wireModal(h,P);let w=null;function N(z){document.getElementById("compose-step-paste").style.display=z==="paste"?"":"none",document.getElementById("compose-step-preview").style.display=z==="preview"?"":"none",document.getElementById("compose-step-progress").style.display=z==="progress"?"":"none"}E?.addEventListener("click",()=>{N("paste"),w=null,document.getElementById("compose-yaml").value="",document.getElementById("compose-stack-name").value="",h?.classList.add("show")}),document.getElementById("compose-file-upload")?.addEventListener("change",z=>{const A=z.target.files[0];if(!A)return;const v=new FileReader;v.onload=()=>{document.getElementById("compose-yaml").value=v.result},v.readAsText(A)}),document.getElementById("compose-parse-btn")?.addEventListener("click",async()=>{const z=document.getElementById("compose-yaml").value.trim(),A=document.getElementById("compose-stack-name").value.trim()||"stack";if(!z){showNotification("Paste a docker-compose.yml","warning");return}const v=document.getElementById("compose-parse-btn"),L=v.textContent;v.textContent="Parsing...",v.disabled=!0;try{const b=await postJSON("/api/v1/apps/import-compose",{yaml:z,stackName:A});w=b,w.stackName=A,O(b),N("preview")}catch(b){showNotification("Parse failed: "+b.message,"error")}finally{v.textContent=L,v.disabled=!1}});function O(z){const A=document.getElementById("compose-preview-content");let v="";z.networks&&z.networks.length>0&&(v+=`
Networks: ${z.networks.map(L=>`${escapeHtml(L)}`).join(", ")}
`),z.volumes&&z.volumes.length>0&&(v+=`
Volumes: ${z.volumes.map(L=>`${escapeHtml(L)}`).join(", ")}
`),v+=`
${z.services.length} service(s)
`,v+='
';for(const L of z.services){const b=L.skip?"var(--bad-fg)":"var(--border)";if(v+=`
`,v+=`
${escapeHtml(L.name)}`,L.skip&&(v+=` \u2014 skipped: ${escapeHtml(L.reason)}`),v+="
",!L.skip&&(v+=`
Image: ${escapeHtml(L.image)}
`,L.ports?.length&&(v+=`
Ports: ${L.ports.map(M=>`${M.host}:${M.container}`).join(", ")}
`),L.volumes?.length&&(v+=`
Volumes: ${L.volumes.length}
`),Object.keys(L.environment||{}).length&&(v+=`
Env vars: ${Object.keys(L.environment).length}
`),L.envFileWarning&&(v+=`
\u26A0 ${escapeHtml(L.envFileWarning)}
`),L.resources?.cpus||L.resources?.memory)){const M=[];L.resources.cpus&&M.push(`CPU: ${L.resources.cpus}`),L.resources.memory&&M.push(`Mem: ${L.resources.memory}MB`),v+=`
Limits: ${M.join(", ")}
`}v+="
"}v+="
",A.innerHTML=v}document.getElementById("compose-back-btn")?.addEventListener("click",()=>N("paste")),document.getElementById("compose-deploy-btn")?.addEventListener("click",async()=>{if(!w)return;const z=document.getElementById("compose-deploy-btn");z.textContent="Deploying...",z.disabled=!0,N("progress");const A=document.getElementById("compose-progress-content");A.innerHTML='
Deploying services...
';try{const v=await postJSON("/api/v1/apps/deploy-compose",{services:w.services,networks:w.networks,stackName:w.stackName});let L=`
Stack "${escapeHtml(v.stackName)}" \u2014 Deployment Complete
`;L+='
';for(const b of v.results){const M=b.status==="deployed"||b.status==="created"?"\u2705":b.status==="exists"?"\u26A1":b.status==="skipped"?"\u23ED":"\u274C";L+='
',L+=`${M} ${escapeHtml(b.name)} (${b.type}) \u2014 ${escapeHtml(b.status)}`,b.error&&(L+=` ${escapeHtml(b.error)}`),b.subdomain&&(L+=` \u2192 ${escapeHtml(b.subdomain)}`),b.reason&&(L+=` (${escapeHtml(b.reason)})`),L+="
"}L+="
",L+='',A.innerHTML=L,document.getElementById("compose-done-btn")?.addEventListener("click",()=>{h?.classList.remove("show"),typeof window.loadServices=="function"&&window.loadServices().then(()=>{typeof window.buildGrid=="function"&&window.buildGrid()})}),showNotification(`Stack "${v.stackName}" deployed`,"success")}catch(v){A.innerHTML=`
Deployment failed: ${escapeHtml(v.message)}
+ `,document.getElementById("compose-retry-btn")?.addEventListener("click",()=>N("paste"))}finally{z.textContent="Deploy All",z.disabled=!1}})})(),(function(){injectModal("exec-modal",`

Terminal

@@ -1538,11 +1614,11 @@ Enter version to rollback to:`);if(!e)return;if(!a.includes(e)){showNotification
- `);const b=document.getElementById("exec-modal"),E=document.getElementById("exec-terminal"),N=document.getElementById("exec-close");let S=null,T=null,P=null;function L(){if(T){try{T.close()}catch{}T=null}if(S){try{S.dispose()}catch{}S=null}P=null,E.innerHTML=""}function H(g,I){if(L(),document.getElementById("exec-title").textContent=`Terminal \u2014 ${I||g}`,b?.classList.add("show"),typeof Terminal>"u"){E.innerHTML='
xterm.js not loaded
';return}S=new Terminal({cursorBlink:!0,fontSize:14,fontFamily:"'Cascadia Code', 'Fira Code', 'Consolas', monospace",theme:{background:"#1e1e1e",foreground:"#d4d4d4",cursor:"#aeafad",selectionBackground:"#264f78"},scrollback:5e3}),typeof FitAddon<"u"&&(P=new FitAddon.FitAddon,S.loadAddon(P)),S.open(E),P&&setTimeout(()=>P.fit(),50);const k=location.protocol==="https:"?"wss:":"ws:";T=new WebSocket(`${k}//${location.host}/ws/exec/${encodeURIComponent(g)}`),T.binaryType="arraybuffer",T.onopen=()=>{if(S.writeln("\x1B[32mConnecting...\x1B[0m"),P){const $=P.proposeDimensions();$&&T.send(JSON.stringify({type:"resize",cols:$.cols,rows:$.rows}))}},T.onmessage=$=>{if(typeof $.data=="string"){try{const C=JSON.parse($.data);if(C.type==="connected"){S.writeln(`\x1B[32mConnected (${C.shell})\x1B[0m\r -`);return}if(C.type==="error"){S.writeln(`\x1B[31mError: ${C.message}\x1B[0m`);return}if(C.type==="exit"){S.writeln(`\r -\x1B[33mSession ended.\x1B[0m`);return}}catch{}S.write($.data)}else S.write(new Uint8Array($.data))},T.onclose=()=>{S&&S.writeln(`\r -\x1B[33mDisconnected.\x1B[0m`)},T.onerror=()=>{S&&S.writeln(`\r -\x1B[31mConnection error.\x1B[0m`)},S.onData($=>{T&&T.readyState===WebSocket.OPEN&&T.send($)}),S.onResize(({cols:$,rows:C})=>{T&&T.readyState===WebSocket.OPEN&&T.send(JSON.stringify({type:"resize",cols:$,rows:C}))});const x=()=>{P&&P.fit()};window.addEventListener("resize",x),b._resizeHandler=x}N?.addEventListener("click",()=>{L(),b._resizeHandler&&window.removeEventListener("resize",b._resizeHandler),b?.classList.remove("show")}),b?.addEventListener("click",g=>{g.target===b&&(L(),b._resizeHandler&&window.removeEventListener("resize",b._resizeHandler),b?.classList.remove("show"))}),window.openExecModal=H})(),(function(){injectModal("audit-modal",`
+
`);const h=document.getElementById("exec-modal"),E=document.getElementById("exec-terminal"),P=document.getElementById("exec-close");let w=null,N=null,O=null;function z(){if(N){try{N.close()}catch{}N=null}if(w){try{w.dispose()}catch{}w=null}O=null,E.innerHTML=""}function A(v,L){if(z(),document.getElementById("exec-title").textContent=`Terminal \u2014 ${L||v}`,h?.classList.add("show"),typeof Terminal>"u"){E.innerHTML='
xterm.js not loaded
';return}w=new Terminal({cursorBlink:!0,fontSize:14,fontFamily:"'Cascadia Code', 'Fira Code', 'Consolas', monospace",theme:{background:"#1e1e1e",foreground:"#d4d4d4",cursor:"#aeafad",selectionBackground:"#264f78"},scrollback:5e3}),typeof FitAddon<"u"&&(O=new FitAddon.FitAddon,w.loadAddon(O)),w.open(E),O&&setTimeout(()=>O.fit(),50);const b=location.protocol==="https:"?"wss:":"ws:";N=new WebSocket(`${b}//${location.host}/ws/exec/${encodeURIComponent(v)}`),N.binaryType="arraybuffer",N.onopen=()=>{if(w.writeln("\x1B[32mConnecting...\x1B[0m"),O){const k=O.proposeDimensions();k&&N.send(JSON.stringify({type:"resize",cols:k.cols,rows:k.rows}))}},N.onmessage=k=>{if(typeof k.data=="string"){try{const B=JSON.parse(k.data);if(B.type==="connected"){w.writeln(`\x1B[32mConnected (${B.shell})\x1B[0m\r +`);return}if(B.type==="error"){w.writeln(`\x1B[31mError: ${B.message}\x1B[0m`);return}if(B.type==="exit"){w.writeln(`\r +\x1B[33mSession ended.\x1B[0m`);return}}catch{}w.write(k.data)}else w.write(new Uint8Array(k.data))},N.onclose=()=>{w&&w.writeln(`\r +\x1B[33mDisconnected.\x1B[0m`)},N.onerror=()=>{w&&w.writeln(`\r +\x1B[31mConnection error.\x1B[0m`)},w.onData(k=>{N&&N.readyState===WebSocket.OPEN&&N.send(k)}),w.onResize(({cols:k,rows:B})=>{N&&N.readyState===WebSocket.OPEN&&N.send(JSON.stringify({type:"resize",cols:k,rows:B}))});const M=()=>{O&&O.fit()};window.addEventListener("resize",M),h._resizeHandler=M}P?.addEventListener("click",()=>{z(),h._resizeHandler&&window.removeEventListener("resize",h._resizeHandler),h?.classList.remove("show")}),h?.addEventListener("click",v=>{v.target===h&&(z(),h._resizeHandler&&window.removeEventListener("resize",h._resizeHandler),h?.classList.remove("show"))}),window.openExecModal=A})(),(function(){injectModal("audit-modal",`

\u{1F4DC} Audit Log

- `);const b=document.getElementById("audit-modal"),E=document.getElementById("audit-log-btn"),N=document.getElementById("audit-cancel"),S=document.getElementById("audit-refresh-btn"),T=document.getElementById("audit-clear-btn"),P=document.getElementById("audit-filter"),L=document.getElementById("audit-log-container"),H=document.getElementById("audit-load-more");let g=0;const I=50;async function k(x){try{x||(g=0,L.innerHTML='
Loading...
');const $=P.value;let C=`/api/v1/audit-logs?limit=${I}&offset=${g}`;$&&(C+=`&action=${encodeURIComponent($)}`);const M=await(await fetch(C)).json(),j=M.success&&M.entries?M.entries:[];if(j.length===0&&!x){L.innerHTML='
\u{1F4DC}No audit log entries yet. Actions will be logged automatically.
',H.style.display="none";return}let B="";x||(B='',B+='');for(const A of j){const w=A.outcome==="success";B+='',B+=``,B+=``,B+=``,B+=``,B+=``,B+="",A.details&&Object.keys(A.details).length>0&&(B+=``)}if(!x)B+="
WhenIPActionResourceResult
${timeAgo(A.timestamp)}${escapeHtml(A.ip||"-")}${escapeHtml(A.action||"-")}${escapeHtml(A.resource||"-")}${w?"\u2713":"\u2717"}
",L.innerHTML=B;else{const A=L.querySelector("table");A&&A.insertAdjacentHTML("beforeend",B)}g+=j.length,H.style.display=j.length>=I?"":"none",L.querySelectorAll(".audit-row").forEach(A=>{A.dataset.wired||(A.dataset.wired="true",A.addEventListener("click",()=>{const w=A.nextElementSibling;w&&w.classList.contains("audit-detail")&&(w.style.display=w.style.display==="none"?"":"none")}))})}catch($){L.innerHTML=`
Failed: ${escapeHtml($.message)}
`}}E?.addEventListener("click",()=>{b?.classList.add("show"),k(!1)}),wireModal(b,N),S?.addEventListener("click",()=>k(!1)),P?.addEventListener("change",()=>k(!1)),H?.addEventListener("click",()=>k(!0)),T?.addEventListener("click",async()=>{if(confirm("Clear the entire audit log? This cannot be undone."))try{const $=await(await secureFetch("/api/v1/audit-logs",{method:"DELETE"})).json();$.success?k(!1):showNotification("Error: "+($.error||"Clear failed"),"error")}catch(x){showNotification("Error: "+x.message,"error")}})})(),(function(){const b=new ErrorHandler;injectModal("weather-modal",`

Weather Settings

+
`);const h=document.getElementById("audit-modal"),E=document.getElementById("audit-log-btn"),P=document.getElementById("audit-cancel"),w=document.getElementById("audit-refresh-btn"),N=document.getElementById("audit-clear-btn"),O=document.getElementById("audit-filter"),z=document.getElementById("audit-log-container"),A=document.getElementById("audit-load-more");let v=0;const L=50;async function b(M){try{M||(v=0,z.innerHTML='
Loading...
');const k=O.value;let B=`/api/v1/audit-logs?limit=${L}&offset=${v}`;k&&(B+=`&action=${encodeURIComponent(k)}`);const T=await(await fetch(B)).json(),j=T.success&&T.entries?T.entries:[];if(j.length===0&&!M){z.innerHTML='
\u{1F4DC}No audit log entries yet. Actions will be logged automatically.
',A.style.display="none";return}let H="";M||(H='',H+='');for(const R of j){const x=R.outcome==="success";H+='',H+=``,H+=``,H+=``,H+=``,H+=``,H+="",R.details&&Object.keys(R.details).length>0&&(H+=``)}if(!M)H+="
WhenIPActionResourceResult
${timeAgo(R.timestamp)}${escapeHtml(R.ip||"-")}${escapeHtml(R.action||"-")}${escapeHtml(R.resource||"-")}${x?"\u2713":"\u2717"}
",z.innerHTML=H;else{const R=z.querySelector("table");R&&R.insertAdjacentHTML("beforeend",H)}v+=j.length,A.style.display=j.length>=L?"":"none",z.querySelectorAll(".audit-row").forEach(R=>{R.dataset.wired||(R.dataset.wired="true",R.addEventListener("click",()=>{const x=R.nextElementSibling;x&&x.classList.contains("audit-detail")&&(x.style.display=x.style.display==="none"?"":"none")}))})}catch(k){z.innerHTML=`
Failed: ${escapeHtml(k.message)}
`}}E?.addEventListener("click",()=>{h?.classList.add("show"),b(!1)}),wireModal(h,P),w?.addEventListener("click",()=>b(!1)),O?.addEventListener("change",()=>b(!1)),A?.addEventListener("click",()=>b(!0)),N?.addEventListener("click",async()=>{if(confirm("Clear the entire audit log? This cannot be undone."))try{const k=await(await secureFetch("/api/v1/audit-logs",{method:"DELETE"})).json();k.success?b(!1):showNotification("Error: "+(k.error||"Clear failed"),"error")}catch(M){showNotification("Error: "+M.message,"error")}})})(),(function(){const h=new ErrorHandler;injectModal("weather-modal",`

Weather Settings

Enter a city name, postal code, or “City, Country”
@@ -1589,23 +1665,23 @@ Enter version to rollback to:`);if(!e)return;if(!a.includes(e)){showNotification
-
`);const E="weather-location",N="weather-zip",S="weather-geo",T="weather-unit";!safeGet(E)&&safeGet(N)&&safeSet(E,safeGet(N));function P(){return safeGet(T)||"imperial"}function L(){return{icon:document.querySelector(".weather-icon"),temp:document.querySelector(".weather-temp"),condition:document.querySelector(".weather-condition"),location:document.querySelector(".weather-location"),wind:document.querySelector(".weather-wind")}}const H={0:"Clear sky",1:"Mainly clear",2:"Partly cloudy",3:"Overcast",45:"Fog",48:"Rime fog",51:"Light drizzle",53:"Drizzle",55:"Dense drizzle",56:"Freezing drizzle",57:"Dense freezing drizzle",61:"Light rain",63:"Moderate rain",65:"Heavy rain",66:"Light freezing rain",67:"Heavy freezing rain",71:"Light snow",73:"Moderate snow",75:"Heavy snow",77:"Snow grains",80:"Light showers",81:"Moderate showers",82:"Violent showers",85:"Light snow showers",86:"Heavy snow showers",95:"Thunderstorm",96:"Thunderstorm with hail",99:"Severe thunderstorm"},g={0:"\u2600\uFE0F",1:"\u{1F324}\uFE0F",2:"\u26C5",3:"\u2601\uFE0F",45:"\u{1F32B}\uFE0F",48:"\u{1F32B}\uFE0F",51:"\u{1F326}\uFE0F",53:"\u{1F326}\uFE0F",55:"\u{1F326}\uFE0F",56:"\u{1F328}\uFE0F",57:"\u{1F328}\uFE0F",61:"\u{1F326}\uFE0F",63:"\u{1F327}\uFE0F",65:"\u{1F327}\uFE0F",66:"\u{1F328}\uFE0F",67:"\u{1F328}\uFE0F",71:"\u{1F328}\uFE0F",73:"\u2744\uFE0F",75:"\u2744\uFE0F",77:"\u2744\uFE0F",80:"\u{1F326}\uFE0F",81:"\u{1F327}\uFE0F",82:"\u{1F327}\uFE0F",85:"\u{1F328}\uFE0F",86:"\u2744\uFE0F",95:"\u26C8\uFE0F",96:"\u26C8\uFE0F",99:"\u26C8\uFE0F"},I=["N","NNE","NE","ENE","E","ESE","SE","SSE","S","SSW","SW","WSW","W","WNW","NW","NNW"];function k(B){return I[Math.round(B/22.5)%16]}async function x(B){const A=safeGet(S);if(A)try{const y=JSON.parse(A);if(y.query===B)return y}catch{}const w=await fetch(`https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(B)}&count=1&language=en&format=json`);if(!w.ok)throw new Error("Geocoding failed");const z=await w.json();if(!z.results||!z.results.length)throw new Error("Location not found");const f=z.results[0],p={query:B,lat:f.latitude,lon:f.longitude,city:f.name,state:f.admin1||"",country:f.country||"",countryCode:f.country_code||""};return safeSet(S,JSON.stringify(p)),p}function $(B){return B.countryCode==="US"&&B.state?`${B.city}, ${B.state}`:B.country?`${B.city}, ${B.country}`:B.city}async function C(B){try{const A=await x(B),w=P(),z=w==="metric"?"celsius":"fahrenheit",f=w==="metric"?"kmh":"mph",p=`https://api.open-meteo.com/v1/forecast?latitude=${A.lat}&longitude=${A.lon}¤t=temperature_2m,weather_code,wind_speed_10m,wind_direction_10m&temperature_unit=${z}&wind_speed_unit=${f}`,y=await fetch(p);if(!y.ok)throw new Error("Weather fetch failed");const m=(await y.json()).current,r=m.weather_code;return{temp:Math.round(m.temperature_2m),condition:H[r]||"Unknown",icon:g[r]||"\u{1F324}\uFE0F",locationStr:$(A),windSpeed:Math.round(m.wind_speed_10m),windDir:k(m.wind_direction_10m),unit:w}}catch(A){return console.warn("Weather fetch failed:",A),null}}async function R(){const B=L();if(!B.icon||!B.temp||!B.condition||!B.location||!B.wind){console.warn("Weather widget elements not found");return}const A=safeGet(E);if(!A){B.location.textContent="Set Location",B.temp.textContent="--\xB0",B.condition.textContent="Click \u2699\uFE0F to configure",B.wind.textContent="--",B.icon.innerHTML='\u{1F324}\uFE0F';return}try{const w=await C(A);if(w){const z=w.unit==="metric"?"\xB0C":"\xB0F",f=w.unit==="metric"?"km/h":"mph";B.location.textContent=w.locationStr,B.temp.textContent=`${w.temp}${z}`,B.condition.textContent=w.condition,B.wind.textContent=`Wind: ${w.windSpeed} ${f} ${w.windDir}`,B.icon.innerHTML=`${escapeHtml(w.icon)}`}}catch(w){b.logError("[Weather] Update Error",w,{function:"updateWeather"}),B.location.textContent="Weather Error",B.temp.textContent="Error",B.condition.textContent="Failed to load",B.wind.textContent="--"}}const M=document.getElementById("weather-modal"),j=document.getElementById("weather-location-input");document.getElementById("weather-settings")?.addEventListener("click",()=>{j.value=safeGet(E)||"";const B=P(),A=M.querySelector(`input[name="weather-unit-radio"][value="${B}"]`);A&&(A.checked=!0),M.classList.add("show"),j.focus()}),document.getElementById("weather-cancel")?.addEventListener("click",()=>{M.classList.remove("show")}),document.getElementById("weather-save")?.addEventListener("click",()=>{const B=j.value.trim();if(B){safeGet(E)!==B&&safeSet(S,""),safeSet(E,B);const w=M.querySelector('input[name="weather-unit-radio"]:checked'),z=w?w.value:"imperial",f=P();safeSet(T,z),f!==z&&safeSet(S,""),M.classList.remove("show"),R()}else showNotification("Please enter a location (e.g., Hamburg, London, 90210)","warning")}),wireModal(M),document.addEventListener("keydown",B=>{B.key==="Escape"&&M.classList.contains("show")&&M.classList.remove("show")}),R(),setInterval(R,DC.POLL.WEATHER)})(),(function(){const b=document.getElementById("clock-widget"),E=document.getElementById("clock-render");if(!b||!E)return;const N=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],S=["January","February","March","April","May","June","July","August","September","October","November","December"],T=["XII","I","II","III","IV","V","VI","VII","VIII","IX","X","XI"];let P=safeGet("clock-style")||"default",L=-1,H=!1,g="",I="",k=null,x=null;function $(a){if(H||safeGet("clock-chimes")!=="true")return;H=!0;const e=parseInt(safeGet("clock-chime-volume")||"50",10)/100;let n=0;function t(){if(n>=a){H=!1;return}const i=new Audio("/assets/sounds/church-bell.mp3");i.volume=e,i.play().catch(()=>{}),n++,n{H=!1},2500)}t()}function C(a){return N[a.getDay()]+", "+S[a.getMonth()]+" "+a.getDate()+", "+a.getFullYear()}function R(){I="",k=null}function M(){return I!=="digital"&&(E.innerHTML='
',k={main:E.querySelector(".clock-main"),seconds:E.querySelector(".clock-seconds"),ampm:E.querySelector(".clock-ampm"),date:E.querySelector(".clock-date")},I="digital"),k}function j(a){const e=a.getHours(),n=a.getMinutes(),t=a.getSeconds(),i=e>=12?"PM":"AM",o=e%12||12,d=M();d.main.textContent=`${o}:${String(n).padStart(2,"0")}`,d.seconds.textContent=`:${String(t).padStart(2,"0")}`,d.ampm.textContent=i,d.date.textContent=C(a)}function B(a,e){const n=a.getHours(),t=a.getMinutes(),i=a.getSeconds(),o=n>=12?"PM":"AM",d=n%12||12,l=M();l.main.textContent=`${String(d).padStart(2,"0")}:${String(t).padStart(2,"0")}`,l.seconds.textContent=`:${String(i).padStart(2,"0")}`,l.ampm.textContent=o,l.date.textContent=C(a)}function A(a){const e=a.getHours(),n=a.getMinutes(),t=a.getSeconds(),i=e>=12?"PM":"AM",o=e%12||12,d=String(o).padStart(2," ")+String(n).padStart(2,"0")+String(t).padStart(2,"0");let l='
';if(l+=w(d[0],0),l+=w(d[1],1),l+=':',l+=w(d[2],2),l+=w(d[3],3),l+=':',l+=w(d[4],4),l+=w(d[5],5),l+=`${i}`,l+="
",l+=`
${C(a)}
`,E.innerHTML=l,I="flip",g){for(let D=0;D<6;D++)if(d[D]!==g[D]){const O=E.querySelector(`.flip-card[data-idx="${D}"]`);O&&O.classList.add("flipping")}}g=d}function w(a,e){const n=a===" "?"":a;return`
${n}
${n}
`}function z(a){const e=a.getHours(),n=a.getMinutes(),t=a.getSeconds(),i=e%12||12,o=e>=12?"PM":"AM",d=[Math.floor(i/10),i%10,Math.floor(n/10),n%10,Math.floor(t/10),t%10];let l='
';l+='
HHMMSS
';for(let D=3;D>=0;D--){l+='
';for(let O=0;O<6;O++){const F=d[O]>>D&1;l+=`
`}l+="
"}l+='
';for(let D=0;D<6;D++)l+=`${d[D]}`;l+="
",l+=`
${o}
`,l+="
",l+=`
${C(a)}
`,E.innerHTML=l,I="binary"}function f(a,e){const n=a.getHours(),t=a.getMinutes(),i=a.getSeconds(),o=120,d=o/2,l=o/2,D=i/60*360-90,O=(t+i/60)/60*360-90,F=(n%12+t/60)/12*360-90;let q="";for(let X=1;X<=12;X++){const Q=X/12*2*Math.PI-Math.PI/2,ne=47,oe=d+ne*Math.cos(Q),se=l+ne*Math.sin(Q),Y=e?T[X%12]:X;q+=`${Y}`}let U="";for(let X=0;X<60;X++){const Q=X/60*2*Math.PI-Math.PI/2,ne=56,oe=X%5===0?52:54,se=d+oe*Math.cos(Q),Y=l+oe*Math.sin(Q),ie=d+ne*Math.cos(Q),re=l+ne*Math.sin(Q),ae=X%5===0?1.5:.5;U+=``}const G=` - +
`);const E="weather-location",P="weather-zip",w="weather-geo",N="weather-unit";!safeGet(E)&&safeGet(P)&&safeSet(E,safeGet(P));function O(){return safeGet(N)||"imperial"}function z(){return{icon:document.querySelector(".weather-icon"),temp:document.querySelector(".weather-temp"),condition:document.querySelector(".weather-condition"),location:document.querySelector(".weather-location"),wind:document.querySelector(".weather-wind")}}const A={0:"Clear sky",1:"Mainly clear",2:"Partly cloudy",3:"Overcast",45:"Fog",48:"Rime fog",51:"Light drizzle",53:"Drizzle",55:"Dense drizzle",56:"Freezing drizzle",57:"Dense freezing drizzle",61:"Light rain",63:"Moderate rain",65:"Heavy rain",66:"Light freezing rain",67:"Heavy freezing rain",71:"Light snow",73:"Moderate snow",75:"Heavy snow",77:"Snow grains",80:"Light showers",81:"Moderate showers",82:"Violent showers",85:"Light snow showers",86:"Heavy snow showers",95:"Thunderstorm",96:"Thunderstorm with hail",99:"Severe thunderstorm"},v={0:"\u2600\uFE0F",1:"\u{1F324}\uFE0F",2:"\u26C5",3:"\u2601\uFE0F",45:"\u{1F32B}\uFE0F",48:"\u{1F32B}\uFE0F",51:"\u{1F326}\uFE0F",53:"\u{1F326}\uFE0F",55:"\u{1F326}\uFE0F",56:"\u{1F328}\uFE0F",57:"\u{1F328}\uFE0F",61:"\u{1F326}\uFE0F",63:"\u{1F327}\uFE0F",65:"\u{1F327}\uFE0F",66:"\u{1F328}\uFE0F",67:"\u{1F328}\uFE0F",71:"\u{1F328}\uFE0F",73:"\u2744\uFE0F",75:"\u2744\uFE0F",77:"\u2744\uFE0F",80:"\u{1F326}\uFE0F",81:"\u{1F327}\uFE0F",82:"\u{1F327}\uFE0F",85:"\u{1F328}\uFE0F",86:"\u2744\uFE0F",95:"\u26C8\uFE0F",96:"\u26C8\uFE0F",99:"\u26C8\uFE0F"},L=["N","NNE","NE","ENE","E","ESE","SE","SSE","S","SSW","SW","WSW","W","WNW","NW","NNW"];function b(H){return L[Math.round(H/22.5)%16]}async function M(H){const R=safeGet(w);if(R)try{const f=JSON.parse(R);if(f.query===H)return f}catch{}const x=await fetch(`https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(H)}&count=1&language=en&format=json`);if(!x.ok)throw new Error("Geocoding failed");const D=await x.json();if(!D.results||!D.results.length)throw new Error("Location not found");const g=D.results[0],u={query:H,lat:g.latitude,lon:g.longitude,city:g.name,state:g.admin1||"",country:g.country||"",countryCode:g.country_code||""};return safeSet(w,JSON.stringify(u)),u}function k(H){return H.countryCode==="US"&&H.state?`${H.city}, ${H.state}`:H.country?`${H.city}, ${H.country}`:H.city}async function B(H){try{const R=await M(H),x=O(),D=x==="metric"?"celsius":"fahrenheit",g=x==="metric"?"kmh":"mph",u=`https://api.open-meteo.com/v1/forecast?latitude=${R.lat}&longitude=${R.lon}¤t=temperature_2m,weather_code,wind_speed_10m,wind_direction_10m&temperature_unit=${D}&wind_speed_unit=${g}`,f=await fetch(u);if(!f.ok)throw new Error("Weather fetch failed");const p=(await f.json()).current,d=p.weather_code;return{temp:Math.round(p.temperature_2m),condition:A[d]||"Unknown",icon:v[d]||"\u{1F324}\uFE0F",locationStr:k(R),windSpeed:Math.round(p.wind_speed_10m),windDir:b(p.wind_direction_10m),unit:x}}catch(R){return console.warn("Weather fetch failed:",R),null}}async function S(){const H=z();if(!H.icon||!H.temp||!H.condition||!H.location||!H.wind){console.warn("Weather widget elements not found");return}const R=safeGet(E);if(!R){H.location.textContent="Set Location",H.temp.textContent="--\xB0",H.condition.textContent="Click \u2699\uFE0F to configure",H.wind.textContent="--",H.icon.innerHTML='\u{1F324}\uFE0F';return}try{const x=await B(R);if(x){const D=x.unit==="metric"?"\xB0C":"\xB0F",g=x.unit==="metric"?"km/h":"mph";H.location.textContent=x.locationStr,H.temp.textContent=`${x.temp}${D}`,H.condition.textContent=x.condition,H.wind.textContent=`Wind: ${x.windSpeed} ${g} ${x.windDir}`,H.icon.innerHTML=`${escapeHtml(x.icon)}`}}catch(x){h.logError("[Weather] Update Error",x,{function:"updateWeather"}),H.location.textContent="Weather Error",H.temp.textContent="Error",H.condition.textContent="Failed to load",H.wind.textContent="--"}}const T=document.getElementById("weather-modal"),j=document.getElementById("weather-location-input");document.getElementById("weather-settings")?.addEventListener("click",()=>{j.value=safeGet(E)||"";const H=O(),R=T.querySelector(`input[name="weather-unit-radio"][value="${H}"]`);R&&(R.checked=!0),T.classList.add("show"),j.focus()}),document.getElementById("weather-cancel")?.addEventListener("click",()=>{T.classList.remove("show")}),document.getElementById("weather-save")?.addEventListener("click",()=>{const H=j.value.trim();if(H){safeGet(E)!==H&&safeSet(w,""),safeSet(E,H);const x=T.querySelector('input[name="weather-unit-radio"]:checked'),D=x?x.value:"imperial",g=O();safeSet(N,D),g!==D&&safeSet(w,""),T.classList.remove("show"),S()}else showNotification("Please enter a location (e.g., Hamburg, London, 90210)","warning")}),wireModal(T),document.addEventListener("keydown",H=>{H.key==="Escape"&&T.classList.contains("show")&&T.classList.remove("show")}),S(),setInterval(S,DC.POLL.WEATHER)})(),(function(){const h=document.getElementById("clock-widget"),E=document.getElementById("clock-render");if(!h||!E)return;const P=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],w=["January","February","March","April","May","June","July","August","September","October","November","December"],N=["XII","I","II","III","IV","V","VI","VII","VIII","IX","X","XI"];let O=safeGet("clock-style")||"default",z=-1,A=!1,v="",L="",b=null,M=null;function k(n){if(A||safeGet("clock-chimes")!=="true")return;A=!0;const e=parseInt(safeGet("clock-chime-volume")||"50",10)/100;let o=0;function a(){if(o>=n){A=!1;return}const r=new Audio("/assets/sounds/church-bell.mp3");r.volume=e,r.play().catch(()=>{}),o++,o{A=!1},2500)}a()}function B(n){return P[n.getDay()]+", "+w[n.getMonth()]+" "+n.getDate()+", "+n.getFullYear()}function S(){L="",b=null}function T(){return L!=="digital"&&(E.innerHTML='
',b={main:E.querySelector(".clock-main"),seconds:E.querySelector(".clock-seconds"),ampm:E.querySelector(".clock-ampm"),date:E.querySelector(".clock-date")},L="digital"),b}function j(n){const e=n.getHours(),o=n.getMinutes(),a=n.getSeconds(),r=e>=12?"PM":"AM",t=e%12||12,s=T();s.main.textContent=`${t}:${String(o).padStart(2,"0")}`,s.seconds.textContent=`:${String(a).padStart(2,"0")}`,s.ampm.textContent=r,s.date.textContent=B(n)}function H(n,e){const o=n.getHours(),a=n.getMinutes(),r=n.getSeconds(),t=o>=12?"PM":"AM",s=o%12||12,l=T();l.main.textContent=`${String(s).padStart(2,"0")}:${String(a).padStart(2,"0")}`,l.seconds.textContent=`:${String(r).padStart(2,"0")}`,l.ampm.textContent=t,l.date.textContent=B(n)}function R(n){const e=n.getHours(),o=n.getMinutes(),a=n.getSeconds(),r=e>=12?"PM":"AM",t=e%12||12,s=String(t).padStart(2," ")+String(o).padStart(2,"0")+String(a).padStart(2,"0");let l='
';if(l+=x(s[0],0),l+=x(s[1],1),l+=':',l+=x(s[2],2),l+=x(s[3],3),l+=':',l+=x(s[4],4),l+=x(s[5],5),l+=`${r}`,l+="
",l+=`
${B(n)}
`,E.innerHTML=l,L="flip",v){for(let C=0;C<6;C++)if(s[C]!==v[C]){const I=E.querySelector(`.flip-card[data-idx="${C}"]`);I&&I.classList.add("flipping")}}v=s}function x(n,e){const o=n===" "?"":n;return`
${o}
${o}
`}function D(n){const e=n.getHours(),o=n.getMinutes(),a=n.getSeconds(),r=e%12||12,t=e>=12?"PM":"AM",s=[Math.floor(r/10),r%10,Math.floor(o/10),o%10,Math.floor(a/10),a%10];let l='
';l+='
HHMMSS
';for(let C=3;C>=0;C--){l+='
';for(let I=0;I<6;I++){const F=s[I]>>C&1;l+=`
`}l+="
"}l+='
';for(let C=0;C<6;C++)l+=`${s[C]}`;l+="
",l+=`
${t}
`,l+="
",l+=`
${B(n)}
`,E.innerHTML=l,L="binary"}function g(n,e){const o=n.getHours(),a=n.getMinutes(),r=n.getSeconds(),t=120,s=t/2,l=t/2,C=r/60*360-90,I=(a+r/60)/60*360-90,F=(o%12+a/60)/12*360-90;let q="";for(let X=1;X<=12;X++){const Q=X/12*2*Math.PI-Math.PI/2,ne=47,oe=s+ne*Math.cos(Q),se=l+ne*Math.sin(Q),Y=e?N[X%12]:X;q+=`${Y}`}let U="";for(let X=0;X<60;X++){const Q=X/60*2*Math.PI-Math.PI/2,ne=56,oe=X%5===0?52:54,se=s+oe*Math.cos(Q),Y=l+oe*Math.sin(Q),ie=s+ne*Math.cos(Q),re=l+ne*Math.sin(Q),ae=X%5===0?1.5:.5;U+=``}const G=` + ${U} ${q} - - - - - `,W=a.getHours()>=12?"PM":"AM";E.innerHTML=`
${G}
${a.getHours()%12||12}:${String(t).padStart(2,"0")} ${W}${C(a)}
`,I="analog"}function p(){const a=new Date,e=a.getHours()%12||12,n=a.getMinutes(),t=a.getSeconds(),i="clock-widget"+(P!=="default"?" "+P:"");switch(b.className!==i&&(b.className=i),P){case"lcd":B(a);break;case"lcd-blue":B(a);break;case"lcd-amber":B(a);break;case"lcd-retro":B(a);break;case"lcd-taxi":B(a);break;case"flip":A(a);break;case"binary":z(a);break;case"analog":f(a,!1);break;case"roman":f(a,!0);break;default:j(a)}n===0&&t===0&&e!==L&&(L=e,$(e)),n!==0&&(L=-1)}function y(){clearTimeout(x);const a=document.hidden?6e4:1e3,e=a-Date.now()%a+25;x=setTimeout(()=>{p(),y()},e)}document.addEventListener("visibilitychange",()=>{g="",R(),p(),y()}),p(),y();const v=[{id:"default",label:"Default",icon:"\u{1F550}"},{id:"lcd",label:"LCD Green",icon:"\u{1F49A}"},{id:"lcd-blue",label:"LCD Blue",icon:"\u{1F499}"},{id:"lcd-amber",label:"LCD Amber",icon:"\u{1F7E0}"},{id:"lcd-retro",label:"LCD Retro",icon:"\u{1F7E9}"},{id:"lcd-taxi",label:"LCD Taxi",icon:"\u{1F7E1}"},{id:"flip",label:"Flip Clock",icon:"\u{1F4DF}"},{id:"binary",label:"Binary",icon:"\u{1F4BB}"},{id:"analog",label:"Analog",icon:"\u23F0"},{id:"roman",label:"Roman",icon:"\u{1F3DB}\uFE0F"}];let m='
';v.forEach(a=>{m+=``}),m+="
",injectModal("clock-settings-modal",`
+ + + + + `,J=n.getHours()>=12?"PM":"AM";E.innerHTML=`
${G}
${n.getHours()%12||12}:${String(a).padStart(2,"0")} ${J}${B(n)}
`,L="analog"}function u(){const n=new Date,e=n.getHours()%12||12,o=n.getMinutes(),a=n.getSeconds(),r="clock-widget"+(O!=="default"?" "+O:"");switch(h.className!==r&&(h.className=r),O){case"lcd":H(n);break;case"lcd-blue":H(n);break;case"lcd-amber":H(n);break;case"lcd-retro":H(n);break;case"lcd-taxi":H(n);break;case"flip":R(n);break;case"binary":D(n);break;case"analog":g(n,!1);break;case"roman":g(n,!0);break;default:j(n)}o===0&&a===0&&e!==z&&(z=e,k(e)),o!==0&&(z=-1)}function f(){clearTimeout(M);const n=document.hidden?6e4:1e3,e=n-Date.now()%n+25;M=setTimeout(()=>{u(),f()},e)}document.addEventListener("visibilitychange",()=>{v="",S(),u(),f()}),u(),f();const m=[{id:"default",label:"Default",icon:"\u{1F550}"},{id:"lcd",label:"LCD Green",icon:"\u{1F49A}"},{id:"lcd-blue",label:"LCD Blue",icon:"\u{1F499}"},{id:"lcd-amber",label:"LCD Amber",icon:"\u{1F7E0}"},{id:"lcd-retro",label:"LCD Retro",icon:"\u{1F7E9}"},{id:"lcd-taxi",label:"LCD Taxi",icon:"\u{1F7E1}"},{id:"flip",label:"Flip Clock",icon:"\u{1F4DF}"},{id:"binary",label:"Binary",icon:"\u{1F4BB}"},{id:"analog",label:"Analog",icon:"\u23F0"},{id:"roman",label:"Roman",icon:"\u{1F3DB}\uFE0F"}];let p='
';m.forEach(n=>{p+=``}),p+="
",injectModal("clock-settings-modal",`

Clock Settings

- ${m} + ${p}
-
`);const r=document.getElementById("clock-settings-modal"),c=document.getElementById("clock-chimes-toggle"),s=document.getElementById("clock-chime-volume"),h=document.getElementById("clock-volume-section");function u(){const a=safeGet("clock-style")||"default",e=r.querySelector(`input[value="${a}"]`);e&&(e.checked=!0),c.checked=safeGet("clock-chimes")==="true",s.value=safeGet("clock-chime-volume")||"50",h.style.opacity=c.checked?"1":"0.4"}c?.addEventListener("change",()=>{h.style.opacity=c.checked?"1":"0.4"}),document.getElementById("clock-settings")?.addEventListener("click",()=>{u(),r.classList.add("show")}),document.getElementById("clock-chime-test")?.addEventListener("click",()=>{const a=parseInt(s.value,10)/100,e=new Audio("/assets/sounds/church-bell.mp3");e.volume=a,e.play().catch(()=>{})}),document.getElementById("clock-settings-save")?.addEventListener("click",()=>{const a=r.querySelector('input[name="clock-style-radio"]:checked'),e=a?a.value:"default";safeSet("clock-style",e),safeSet("clock-chimes",String(c.checked)),safeSet("clock-chime-volume",s.value),P=e,g="",R(),p(),y(),r.classList.remove("show"),showNotification("Clock settings saved","success",2e3)}),document.getElementById("clock-settings-cancel")?.addEventListener("click",()=>{r.classList.remove("show")}),wireModal(r),r?.querySelectorAll('input[name="clock-style-radio"]').forEach(a=>{a.addEventListener("change",()=>{P=a.value,g="",R(),p()})})})(),(function(){async function b(){try{const L=await(await fetch("/api/v1/health-checks/status")).json();if(!L.success||!L.status)return;for(const[H,g]of Object.entries(L.status)){const I=document.getElementById("uptime-"+H),k=document.getElementById("uptime-bar-"+H);if(!I)continue;const x=g.uptime?.["24h"];if(x!=null){const $=x.toFixed(1);I.textContent=`${$}% uptime`,I.className="uptime-chip",x>=99.9?I.classList.add("excellent"):x>=99?I.classList.add("good"):x>=95?I.classList.add("degraded"):I.classList.add("poor"),k&&(k.style.width=$+"%")}}}catch{console.warn("[Card Badges] Health check API unavailable")}}let E;try{E=new Set(JSON.parse(safeSessionGet("dismissed-updates")||"[]"))}catch{E=new Set}async function N(){try{const L=await(await fetch("/api/v1/updates/available")).json();if(!L.success||(document.querySelectorAll(".update-available-badge").forEach(H=>H.classList.remove("visible")),!L.updates?.length))return;for(const H of L.updates){const g=window.APPS||[];for(const I of g)if(I.containerId===H.containerId||I.id===H.containerName||I.name===H.containerName){if(E.has(I.id))break;const k=document.getElementById("update-badge-"+I.id);k&&(k.classList.add("visible"),k.title=`Image digest changed. Click to dismiss if already up to date. -${H.imageName||""}`,k.style.cursor="pointer",k.onclick=x=>{x.stopPropagation(),k.classList.remove("visible"),E.add(I.id),safeSessionSet("dismissed-updates",JSON.stringify([...E]))});break}}}catch{console.warn("[Card Badges] Updates API unavailable")}}function S(){setTimeout(()=>{b(),N()},5e3),setInterval(()=>{b(),N()},6e4)}const T=window.refreshAll;T&&(window.refreshAll=async function(){try{await T(),setTimeout(b,1e3)}catch(P){console.warn("[Card Badges] Error in refreshAll hook:",P.message)}}),S()})(),(function(){var b=null,E=null,N={},S={dark:"Dark",light:"Light",blue:"Blue",black:"Black",nord:"Nord",dracula:"Dracula","solarized-dark":"Solarized Dark","solarized-light":"Solarized Light",taxi:"Taxi",ocean:"Ocean"},T=[["bg","Background","base"],["card-base","Card","base"],["fg","Text","base"],["muted","Muted Text","base"],["border","Border","base"],["accent","Accent","accent"],["accent-strong","Accent Strong","accent"],["ok-bg","OK Background","status"],["ok-fg","OK Text","status"],["bad-bg","Error Bg","status"],["bad-fg","Error Text","status"],["dot-ok","Dot OK","status"],["dot-bad","Dot Error","status"],["uptime","Uptime Bar","status"],["hover","Hover","advanced"],["card-hover","Card Hover","advanced"],["base","Tags/Badges","advanced"],["fg-muted","Dim Text","advanced"],["success","Success","advanced"],["error","Error","advanced"],["warning","Warning","advanced"]],P=document.getElementById("theme");if(!P)return;var L=document.getElementById("theme-label");function H(t){if(S[t])return S[t];var i=safeGetJSON(window.USER_THEMES_KEY,{});return i[t]&&i[t].name||t}function g(){L&&(L.textContent=H(window.getActiveTheme()))}P.addEventListener("click",function(){var t=window.THEMES.slice(),i=window.getActiveTheme(),o=t.indexOf(i),d=t[(o+1)%t.length];window.applyTheme(d),g()}),g();function I(){var t={base:"Base Colors",accent:"Accent",status:"Status",advanced:"Advanced (auto-derived)"},i={};T.forEach(function(d){i[d[2]]||(i[d[2]]=[]),i[d[2]].push(d)});var o="";return Object.keys(t).forEach(function(d){d==="advanced"?(o+='
Show advanced colors ▼
',o+='`).join("")}async function H(){try{const m=await(await fetch("/api/v1/license/status")).json();m.success&&(j(m.license),D(m.license))}catch(f){console.warn("Failed to load license status:",f.message)}}async function R(){const f=E.value.trim();if(!f){S("Please enter a license code.");return}B(),P.disabled=!0,P.textContent="Activating...";try{const p=await(await secureFetch("/api/v1/license/activate",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({code:f})})).json();p.success?(T(p.message),E.value="",j(p.license),showNotification("License activated! Premium features unlocked.","success",5e3),D(p.license)):S(p.error||"Activation failed")}catch(m){S("Network error: "+m.message)}finally{P.disabled=!1,P.textContent="Activate"}}async function x(){if(confirm("Deactivate your license? You can reuse the code on another machine.")){w.disabled=!0,w.textContent="Deactivating...";try{const m=await(await secureFetch("/api/v1/license/deactivate",{method:"POST"})).json();m.success?(T(m.message),await H(),showNotification("License deactivated.","info",3e3),D({active:!1})):S(m.error||"Deactivation failed")}catch(f){S("Network error: "+f.message)}finally{w.disabled=!1,w.textContent="Deactivate"}}}function D(f){const m=document.getElementById("license-status-topbar"),p=document.getElementById("license-topbar-icon"),d=document.getElementById("license-topbar-text"),c=document.getElementById("license-topbar-time");if(m)if(m.className="license-status-topbar "+(f.active?"premium":"free"),f.active)if(p.textContent="\u2605",d.textContent="PREMIUM",f.lifetime)c.textContent="\xB7 LIFETIME";else{const i=f.daysRemaining;c.textContent=i!=null?"\xB7 "+i+"d remaining":""}else p.textContent="\u2606",d.textContent=f.expired?"EXPIRED":"FREE TIER",c.textContent=""}function g(){B(),H(),h.classList.add("show")}E.addEventListener("input",function(){let f=this.value.toUpperCase().replace(/[^A-Z0-9-]/g,"");if(f.length>this._prevLength&&(f=f.replace(/-/g,""),f.length>2&&!f.startsWith("DC")&&(f="DC"+f),f.startsWith("DC")&&f.length>2)){const m=["DC"],p=f.substring(2);for(let d=0;d{f.key==="Enter"&&R()}),wireModal(h,document.getElementById("license-cancel"));const u=document.getElementById("license-status-topbar");u&&u.addEventListener("click",()=>window.openLicenseModal&&window.openLicenseModal()),window.openLicenseModal=g,window.checkPremiumFeature=async function(f){try{return(await(await fetch(`/api/v1/license/feature/${f}`)).json()).available}catch{return!1}},H().then(f=>{k&&D(k)})})(); diff --git a/status/dist/init.js b/status/dist/init.js index 28fd97a..66c039e 100644 --- a/status/dist/init.js +++ b/status/dist/init.js @@ -1,4 +1,115 @@ -(function(){function p(){const a=safeGet("custom-services");if(a)try{JSON.parse(a).forEach(i=>{window.APPS.find(r=>r.id===i.id)||window.APPS.push(i)})}catch(c){console.warn("Failed to load custom services:",c)}}p();function y(){const a=document.querySelectorAll(".top .card");a.forEach((c,i)=>{c.style.transitionDelay=`${Math.min(i*60,300)}ms`}),requestAnimationFrame(()=>{a.forEach(c=>c.classList.add("loaded"))})}function n(){if(!("serviceWorker"in navigator)||!window.isSecureContext&&location.hostname!=="localhost"&&location.hostname!=="127.0.0.1")return;const a=()=>{navigator.serviceWorker.register("/sw.js",{updateViaCache:"none"}).catch(c=>{console.warn("[init] Service worker registration failed:",c)})};document.readyState==="complete"?a():window.addEventListener("load",a,{once:!0})}let u=!1;async function l(){if(u){console.warn("[init] initializeDashboard called again, skipping duplicate");return}if(u=!0,await window.loadServices(),window.buildGrid(),y(),window.refreshAll(),setInterval(window.refreshAll,DC.POLL.DASHBOARD),typeof window.refreshCredsButtons=="function"&&window.refreshCredsButtons(),typeof window._updateAuthCard=="function")try{const c=await(await fetch("/api/v1/totp/config",{cache:"no-store"})).json();c.success&&window._updateAuthCard(c.config.enabled&&c.config.isSetUp,c.config.sessionDuration)}catch{}if(window.__dashcaddySiteConfigLoaded)try{await window.__dashcaddySiteConfigLoaded}catch{}k(),w()&&m()}function m(){if(document.querySelector('script[src="/dist/onboarding.js"]'))return;const a=document.createElement("script");if(a.src="/dist/onboarding.js",a.defer=!0,document.head.appendChild(a),!document.querySelector('link[href="/css/driver.min.css"]')){const c=document.createElement("link");c.rel="stylesheet",c.href="/css/driver.min.css",document.head.appendChild(c)}if(!document.querySelector('link[href="/css/onboarding.css"]')){const c=document.createElement("link");c.rel="stylesheet",c.href="/css/onboarding.css",document.head.appendChild(c)}}function w(){if(typeof SITE<"u"&&SITE.onboardingCompleted)return!1;try{const a=JSON.parse(localStorage.getItem("dashcaddy_onboarding"));return!a||!a.tourCompleted&&a.currentStep===0}catch{return!0}}function b(){const a=document.querySelectorAll(".tools-section");if(!a.length)return;let c={};try{c=JSON.parse(localStorage.getItem("toolbar-sections")||"{}")}catch{}a.forEach(i=>{const r=i.dataset.section,h=i.querySelector(".tools-section-header");h&&(c[r]&&(i.classList.add("open"),h.setAttribute("aria-expanded","true")),h.addEventListener("click",q=>{q.preventDefault();const S=i.classList.toggle("open");h.setAttribute("aria-expanded",S?"true":"false");const f={};document.querySelectorAll(".tools-section").forEach(v=>{f[v.dataset.section]=v.classList.contains("open")}),localStorage.setItem("toolbar-sections",JSON.stringify(f))}))})}b();function k(){if(document.getElementById("restart-tour-btn"))return;let a=typeof SITE<"u"&&SITE.onboardingCompleted;try{const r=JSON.parse(localStorage.getItem("dashcaddy_onboarding"));a=a||!!(r&&r.tourCompleted)}catch{}const c=a?document.querySelector('.tools-section[data-section="admin"] .tools-section-items'):document.querySelector(".tools-primary");if(!c)return;const i=document.createElement("button");i.id="restart-tour-btn",i.textContent=a?"Help Tour":"\u{1F393} Help Tour",i.title="Restart the onboarding tour",i.onclick=()=>{if(window.DashCaddyOnboarding)window.DashCaddyOnboarding.restartTour();else{m();const r=setInterval(()=>{window.DashCaddyOnboarding&&(clearInterval(r),window.DashCaddyOnboarding.restartTour())},100);setTimeout(()=>clearInterval(r),5e3)}},c.appendChild(i)}window.initializeDashboard=l,window.loadCustomServices=p,n(),(async()=>{try{const c=await(await fetch("/api/v1/totp/config",{cache:"no-store"})).json();if(c.success&&c.config.enabled&&(await fetch("/api/v1/totp/check-session",{cache:"no-store"})).status===401){window._showTotpOverlay();return}}catch(a){console.warn("TOTP check failed, proceeding normally:",a)}l()})()})(),(function(){"use strict";const p=(...t)=>{window.DASHCADDY_DEBUG&&console.log(...t)},y=["#app-selector-modal","#app-deploy-modal","#weather-modal","#token-management-modal","#service-edit-modal","#notifications-modal","#backup-modal","#stats-modal","#arr-setup-modal","#add-service-modal","#error-log-modal","#logs-modal","#dns-template-modal"];let n=null,u=null,l=null;function m(){try{w(),document.addEventListener("keydown",b),p("[Keyboard Shortcuts] Initialized"),p("[Keyboard Shortcuts] Press Ctrl+K to open quick search"),p("[Keyboard Shortcuts] Press Escape to close modals")}catch(t){console.warn("[Keyboard Shortcuts] Failed to initialize:",t.message)}}function w(){n=document.createElement("div"),n.id="quick-search-modal",n.className="quick-search-modal",n.innerHTML=` +(function(){function v(){const i=safeGet("custom-services");if(i)try{JSON.parse(i).forEach(d=>{window.APPS.find(o=>o.id===d.id)||window.APPS.push(d)})}catch(a){console.warn("Failed to load custom services:",a)}}v();function k(){const i=document.querySelectorAll(".top .card");i.forEach((a,d)=>{a.style.transitionDelay=`${Math.min(d*60,300)}ms`}),requestAnimationFrame(()=>{i.forEach(a=>a.classList.add("loaded"))})}function u(){if(!("serviceWorker"in navigator)||!window.isSecureContext&&location.hostname!=="localhost"&&location.hostname!=="127.0.0.1")return;const i=()=>{navigator.serviceWorker.register("/sw.js",{updateViaCache:"none"}).catch(a=>{console.warn("[init] Service worker registration failed:",a)})};document.readyState==="complete"?i():window.addEventListener("load",i,{once:!0})}let h=!1;async function f(){if(h){console.warn("[init] initializeDashboard called again, skipping duplicate");return}if(h=!0,await window.loadServices(),await g(),window.buildGrid(),k(),window.refreshAll(),setInterval(window.refreshAll,DC.POLL.DASHBOARD),typeof window.refreshCredsButtons=="function"&&window.refreshCredsButtons(),typeof window.refreshMonitoringWidgets=="function"&&window.refreshMonitoringWidgets(),typeof window._updateAuthCard=="function")try{const a=await(await fetch("/api/v1/totp/config",{cache:"no-store"})).json();a.success&&window._updateAuthCard(a.config.enabled&&a.config.isSetUp,a.config.sessionDuration)}catch{}if(window.__dashcaddySiteConfigLoaded)try{await window.__dashcaddySiteConfigLoaded}catch{}S(),C()&&b()}function b(){if(document.querySelector('script[src="/dist/onboarding.js"]'))return;const i=document.createElement("script");if(i.src="/dist/onboarding.js",i.defer=!0,document.head.appendChild(i),!document.querySelector('link[href="/css/driver.min.css"]')){const a=document.createElement("link");a.rel="stylesheet",a.href="/css/driver.min.css",document.head.appendChild(a)}if(!document.querySelector('link[href="/css/onboarding.css"]')){const a=document.createElement("link");a.rel="stylesheet",a.href="/css/onboarding.css",document.head.appendChild(a)}}function C(){if(typeof SITE<"u"&&SITE.onboardingCompleted)return!1;try{const i=JSON.parse(localStorage.getItem("dashcaddy_onboarding"));return!i||!i.tourCompleted&&i.currentStep===0}catch{return!0}}function E(){const i=document.querySelectorAll(".tools-section");if(!i.length)return;let a={};try{a=JSON.parse(localStorage.getItem("toolbar-sections")||"{}")}catch{}i.forEach(d=>{const o=d.dataset.section,n=d.querySelector(".tools-section-header");n&&(a[o]&&(d.classList.add("open"),n.setAttribute("aria-expanded","true")),n.addEventListener("click",c=>{c.preventDefault();const r=d.classList.toggle("open");n.setAttribute("aria-expanded",r?"true":"false");const m={};document.querySelectorAll(".tools-section").forEach(e=>{m[e.dataset.section]=e.classList.contains("open")}),localStorage.setItem("toolbar-sections",JSON.stringify(m))}))})}E();function S(){if(document.getElementById("restart-tour-btn"))return;let i=typeof SITE<"u"&&SITE.onboardingCompleted;try{const o=JSON.parse(localStorage.getItem("dashcaddy_onboarding"));i=i||!!(o&&o.tourCompleted)}catch{}const a=i?document.querySelector('.tools-section[data-section="admin"] .tools-section-items'):document.querySelector(".tools-primary");if(!a)return;const d=document.createElement("button");d.id="restart-tour-btn",d.textContent=i?"Help Tour":"\u{1F393} Help Tour",d.title="Restart the onboarding tour",d.onclick=()=>{if(window.DashCaddyOnboarding)window.DashCaddyOnboarding.restartTour();else{b();const o=setInterval(()=>{window.DashCaddyOnboarding&&(clearInterval(o),window.DashCaddyOnboarding.restartTour())},100);setTimeout(()=>clearInterval(o),5e3)}},a.appendChild(d)}window.initializeDashboard=f,window.loadCustomServices=v,u();async function g(){try{const i=await fetch("/api/v1/templates",{cache:"no-store"});if(!i.ok)return;const a=await i.json();a&&a.categories&&(window.DC_CATEGORIES=a.categories,typeof DC<"u"&&(DC.CATEGORIES=a.categories),q())}catch(i){console.warn("[init] Failed to load template categories:",i)}}function q(){const i=window.DC_CATEGORIES||typeof DC<"u"&&DC.CATEGORIES;i&&document.querySelectorAll('select[data-role="service-category"]').forEach(a=>{const d=a.dataset.current||"",o=a.querySelector('option[value=""]');if(a.innerHTML="",o)a.appendChild(o);else{const n=document.createElement("option");n.value="",n.textContent="\u2014 Select category \u2014",a.appendChild(n)}Object.entries(i).forEach(([n,c])=>{const r=document.createElement("option");r.value=n,r.textContent=`${c.icon||""} ${n}`.trim(),n===d&&(r.selected=!0),a.appendChild(r)})})}window.populateCategorySelects=q,window.loadTemplateCategories=g,(async()=>{try{const a=await(await fetch("/api/v1/totp/config",{cache:"no-store"})).json();if(a.success&&a.config.enabled&&(await fetch("/api/v1/totp/check-session",{cache:"no-store"})).status===401){window._showTotpOverlay();return}}catch(i){console.warn("TOTP check failed, proceeding normally:",i)}f()})()})(),(function(){const v=document.createElement("style");v.textContent=` + .dc-monitor { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 12px; + margin-bottom: 16px; + padding: 12px 16px; + background: var(--card-base); + border: 1px solid var(--border); + border-radius: var(--radius); + } + .dc-monitor-card { + padding: 10px 12px; + background: var(--card-bg, rgba(255,255,255,0.04)); + border-radius: 8px; + border: 1px solid var(--border); + } + .dc-monitor-label { + font-size: 0.7rem; + color: var(--muted); + text-transform: uppercase; + letter-spacing: 0.5px; + margin-bottom: 4px; + } + .dc-monitor-value { + font-size: 1.4rem; + font-weight: 600; + color: var(--fg); + } + .dc-monitor-sub { + font-size: 0.7rem; + color: var(--muted); + margin-top: 4px; + } + .dc-monitor-bar { + margin-top: 6px; + width: 100%; + height: 4px; + background: color-mix(in srgb, var(--muted) 20%, transparent); + border-radius: 2px; + overflow: hidden; + } + .dc-monitor-bar-fill { + height: 100%; + width: 0%; + background: var(--ok-fg, #27ae60); + transition: width 0.3s ease, background 0.3s ease; + } + .dc-monitor-bar-fill.warn { background: #f39c12; } + .dc-monitor-bar-fill.bad { background: #e74c3c; } + .dc-monitor-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 8px; + } + .dc-monitor-title { + font-size: 0.85rem; + font-weight: 500; + color: var(--muted); + display: flex; + align-items: center; + gap: 6px; + } + .dc-monitor-pill { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 8px; + border-radius: 999px; + font-size: 0.7rem; + font-weight: 500; + } + .dc-monitor-pill.ok { background: color-mix(in srgb, #27ae60 20%, transparent); color: #27ae60; } + .dc-monitor-pill.warn { background: color-mix(in srgb, #f39c12 20%, transparent); color: #f39c12; } + .dc-monitor-pill.bad { background: color-mix(in srgb, #e74c3c 20%, transparent); color: #e74c3c; } + .dc-monitor-refresh { + font-size: 0.7rem; + color: var(--muted); + opacity: 0.7; + } + `,document.head.appendChild(v);const k=document.getElementById("service-filter-bar");if(!k)return;const u=document.createElement("div");u.className="dc-monitor",u.id="dc-monitor-panel",u.innerHTML=` +
+
\u{1F4CA} System Overview
+ \u2014 +
+
+
Services
+
\u2014
+
loading\u2026
+
+
+
Containers Up
+
\u2014
+
loading\u2026
+
+
+
Avg CPU
+
\u2014
+
+
+
+
Avg Memory
+
\u2014
+
+
+
+
Health
+
\u2014
+
\u2014
+
+ `,k.parentNode.insertBefore(u,k);function h(o,n){const c=document.getElementById(o);if(!c)return;const r=Math.max(0,Math.min(100,Number(n)||0));c.style.width=r+"%",c.classList.remove("warn","bad"),r>=85?c.classList.add("bad"):r>=65&&c.classList.add("warn")}function f(o){return o==null||isNaN(o)?"\u2014":Math.round(o*10)/10+"%"}function b(o){if(o==null||isNaN(o))return"\u2014";const n=["B","KB","MB","GB","TB"];let c=0;for(;o>=1024&&c0){const n=document.querySelectorAll('#cards .card[data-status="on"]').length;return{total:window.APPS.length,up:n,source:"APPS"}}const o=document.querySelectorAll("#cards .card");if(o.length>0){const n=Array.from(o).filter(c=>c.dataset.status==="on").length;return{total:o.length,up:n,source:"DOM"}}try{const n=await fetch("/api/v1/services",{cache:"no-store"});if(!n.ok)return{total:0,up:0,source:"fetch-fail"};const c=await n.json(),r=c&&Array.isArray(c.services)?c.services:Array.isArray(c)?c:[];(Array.isArray(window.APPS)||typeof window.APPS>"u")&&(window.APPS=r);const m=document.querySelectorAll('#cards .card[data-status="on"]').length;return{total:r.length,up:m,source:"fetch"}}catch{return{total:0,up:0,source:"fetch-error"}}}async function E(){const{total:o,up:n}=await C(),c=document.getElementById("dc-monitor-services"),r=document.getElementById("dc-monitor-services-sub");c&&(c.textContent=`${n} / ${o}`),r&&(r.textContent=o===0?"no services yet":`${n} online \xB7 ${o-n} offline`)}function S(o){const n=document.getElementById("dc-monitor-health"),c=document.getElementById("dc-monitor-health-sub");if(!n)return;if(!o||o.summary==null){n.textContent="\u2014",c&&(c.textContent="no data");return}const r=o.summary,m=r.healthy??r.up??0,e=r.unhealthy??r.down??0,t=r.total??m+e;n.textContent=`${m}/${t}`,c&&(e===0?c.innerHTML='\u25CF all healthy':e<=2?c.innerHTML=`\u25CF ${e} degraded`:c.innerHTML=`\u25CF ${e} down`)}async function g(){try{const o=await fetch("/api/v1/monitoring/stats",{cache:"no-store"});if(!o.ok)return null;const n=await o.json();return n&&n.stats?n.stats:null}catch{return null}}async function q(){try{const o=await fetch("/api/v1/health-checks/status",{cache:"no-store"});return o.ok?await o.json():null}catch{return null}}function i(o){const n=document.getElementById("dc-monitor-containers"),c=document.getElementById("dc-monitor-containers-sub"),r=document.getElementById("dc-monitor-cpu"),m=document.getElementById("dc-monitor-mem");if(!o){n&&(n.textContent="\u2014"),r&&(r.textContent="\u2014"),m&&(m.textContent="\u2014");return}const e=Object.values(o);if(e.length===0){n&&(n.textContent="0"),c&&(c.textContent="no containers reporting"),r&&(r.textContent="0%"),m&&(m.textContent="0%"),h("dc-monitor-cpu-bar",0),h("dc-monitor-mem-bar",0);return}let t=0,s=0,p=0,l=0,y=0;e.forEach(w=>{if(w.cpu!=null){const x=Number(w.cpu);isNaN(x)||(t+=x>1?x:x*100,l++)}if(w.memory!=null){const x=Number(w.memory);isNaN(x)||(s+=x,p+=Number(w.memoryUsage||0),y++)}});const A=l?t/l:0,L=y?s/y:0;if(n&&(n.textContent=String(e.length)),c){const w=p?` \xB7 ${b(p)} RAM`:"";c.textContent=`running${w}`}r&&(r.textContent=f(A)),m&&(m.textContent=f(L)),h("dc-monitor-cpu-bar",A),h("dc-monitor-mem-bar",L)}let a=!1;async function d(){if(!a){a=!0;try{E();const[o,n]=await Promise.all([g(),q()]);i(o),S(n);const c=document.getElementById("dc-monitor-refresh-stamp");if(c){const r=new Date;c.textContent=`updated ${r.toLocaleTimeString()}`}}finally{a=!1}}}window.refreshMonitoringWidgets=d,setInterval(d,typeof DC<"u"&&DC.POLL&&DC.POLL.STATS||5e3),setTimeout(d,200)})(),(function(){"use strict";const v=(...e)=>{window.DASHCADDY_DEBUG&&console.log(...e)},k=["#app-selector-modal","#app-deploy-modal","#weather-modal","#token-management-modal","#service-edit-modal","#notifications-modal","#backup-modal","#stats-modal","#arr-setup-modal","#add-service-modal","#error-log-modal","#logs-modal","#dns-template-modal"];let u=null,h=null,f=null;function b(){try{C(),document.addEventListener("keydown",E),v("[Keyboard Shortcuts] Initialized"),v("[Keyboard Shortcuts] Press Ctrl+K to open quick search"),v("[Keyboard Shortcuts] Press Escape to close modals")}catch(e){console.warn("[Keyboard Shortcuts] Failed to initialize:",e.message)}}function C(){u=document.createElement("div"),u.id="quick-search-modal",u.className="quick-search-modal",u.innerHTML=`
\u{1F50D} @@ -12,7 +123,7 @@ Esc Close
- `;const t=document.createElement("style");t.textContent=` + `;const e=document.createElement("style");e.textContent=` .quick-search-modal { display: none; position: fixed; @@ -160,7 +271,7 @@ font-family: monospace; margin-right: 4px; } - `,document.head.appendChild(t),document.body.appendChild(n),u=document.getElementById("quick-search-input"),l=document.getElementById("quick-search-results"),u.addEventListener("input",h),u.addEventListener("keydown",v),n.addEventListener("click",e=>{e.target===n&&a()})}function b(t){try{if((t.ctrlKey||t.metaKey)&&t.key==="k"){t.preventDefault(),k();return}if(t.key==="Escape"){if(n&&n.classList.contains("show")){a();return}c()}}catch(e){console.warn("[Keyboard Shortcuts] Error handling keydown:",e.message)}}function k(){try{n.classList.add("show"),u.value="",u.focus(),i()}catch(t){console.warn("[Keyboard Shortcuts] Error opening quick search:",t.message)}}function a(){try{n.classList.remove("show"),u.value="",l.innerHTML=""}catch(t){console.warn("[Keyboard Shortcuts] Error closing quick search:",t.message)}}function c(){for(const t of y){const e=document.querySelector(t);if(e&&(e.classList.contains("show")||e.style.display==="flex"))return e.classList.remove("show"),e.style.display="none",!0}return!1}function i(){const t=` + `,document.head.appendChild(e),document.body.appendChild(u),h=document.getElementById("quick-search-input"),f=document.getElementById("quick-search-results"),h.addEventListener("input",d),h.addEventListener("keydown",r),u.addEventListener("click",t=>{t.target===u&&g()})}function E(e){try{if((e.ctrlKey||e.metaKey)&&e.key==="k"){e.preventDefault(),S();return}if(e.key==="Escape"){if(u&&u.classList.contains("show")){g();return}q()}}catch(t){console.warn("[Keyboard Shortcuts] Error handling keydown:",t.message)}}function S(){try{u.classList.add("show"),h.value="",h.focus(),i()}catch(e){console.warn("[Keyboard Shortcuts] Error opening quick search:",e.message)}}function g(){try{u.classList.remove("show"),h.value="",f.innerHTML=""}catch(e){console.warn("[Keyboard Shortcuts] Error closing quick search:",e.message)}}function q(){for(const e of k){const t=document.querySelector(e);if(t&&(t.classList.contains("show")||t.style.display==="flex"))return t.classList.remove("show"),t.style.display="none",!0}return!1}function i(){const e=`
Quick Actions
\u{1F504} @@ -192,24 +303,24 @@
Services
- ${r()} - `;l.innerHTML=t,f()}function r(){const t=document.querySelectorAll(".card[data-app], #cards .card");let e="";return t.forEach(s=>{const d=s.querySelector(".name")?.textContent||"Unknown",o=s.dataset.status||"unknown",g=s.dataset.app||"";d&&d!=="--"&&(e+=` -
- ${o==="on"?"\u{1F7E2}":"\u{1F534}"} + ${a()} + `;f.innerHTML=e,c()}function a(){const e=document.querySelectorAll(".card[data-app], #cards .card");let t="";return e.forEach(s=>{const p=s.querySelector(".name")?.textContent||"Unknown",l=s.dataset.status||"unknown",y=s.dataset.app||"";p&&p!=="--"&&(t+=` +
+ ${l==="on"?"\u{1F7E2}":"\u{1F534}"}
-
${d}
+
${p}
Click to open service
- ${o.toUpperCase()} + ${l.toUpperCase()}
- `)}),e||'
No services found
'}function h(t){try{const e=t.target.value.toLowerCase().trim();if(!e){i();return}const s=q(e);S(s)}catch(e){console.warn("[Keyboard Shortcuts] Error handling search input:",e.message)}}function q(t){const e={actions:[],services:[]};return[{id:"refresh",title:"Refresh Dashboard",icon:"\u{1F504}",keywords:"refresh reload update status"},{id:"reload-caddy",title:"Reload Caddy",icon:"\u26A1",keywords:"reload caddy proxy config"},{id:"add-service",title:"Add Service",icon:"\u2795",keywords:"add new service create"},{id:"app-selector",title:"App Selector",icon:"\u{1F4F1}",keywords:"app deploy install docker container"},{id:"backup",title:"Backup & Restore",icon:"\u{1F4BE}",keywords:"backup restore export import"},{id:"stats",title:"Container Stats",icon:"\u{1F4CA}",keywords:"stats resources cpu memory"},{id:"logs",title:"View Logs",icon:"\u{1F4CB}",keywords:"logs error debug"},{id:"tokens",title:"Manage Tokens",icon:"\u{1F511}",keywords:"tokens api keys credentials"},{id:"notifications",title:"Notifications",icon:"\u{1F514}",keywords:"alerts notifications discord telegram"},{id:"theme",title:"Change Theme",icon:"\u{1F3A8}",keywords:"theme dark light appearance"},{id:"tour",title:"Help Tour",icon:"\u{1F393}",keywords:"help tour guide onboarding"}].forEach(o=>{(o.title.toLowerCase().includes(t)||o.keywords.includes(t))&&e.actions.push(o)}),document.querySelectorAll(".card[data-app], #cards .card").forEach(o=>{const g=o.querySelector(".name")?.textContent||"",E=o.dataset.app||"",C=o.dataset.status||"unknown";(g.toLowerCase().includes(t)||E.toLowerCase().includes(t))&&e.services.push({id:E,title:g,status:C,icon:C==="on"?"\u{1F7E2}":"\u{1F534}"})}),e}function S(t){let e="";t.actions.length>0&&(e+='
Actions
',t.actions.forEach(s=>{e+=` + `)}),t||'
No services found
'}function d(e){try{const t=e.target.value.toLowerCase().trim();if(!t){i();return}const s=o(t);n(s)}catch(t){console.warn("[Keyboard Shortcuts] Error handling search input:",t.message)}}function o(e){const t={actions:[],services:[]};return[{id:"refresh",title:"Refresh Dashboard",icon:"\u{1F504}",keywords:"refresh reload update status"},{id:"reload-caddy",title:"Reload Caddy",icon:"\u26A1",keywords:"reload caddy proxy config"},{id:"add-service",title:"Add Service",icon:"\u2795",keywords:"add new service create"},{id:"app-selector",title:"App Selector",icon:"\u{1F4F1}",keywords:"app deploy install docker container"},{id:"backup",title:"Backup & Restore",icon:"\u{1F4BE}",keywords:"backup restore export import"},{id:"stats",title:"Container Stats",icon:"\u{1F4CA}",keywords:"stats resources cpu memory"},{id:"logs",title:"View Logs",icon:"\u{1F4CB}",keywords:"logs error debug"},{id:"tokens",title:"Manage Tokens",icon:"\u{1F511}",keywords:"tokens api keys credentials"},{id:"notifications",title:"Notifications",icon:"\u{1F514}",keywords:"alerts notifications discord telegram"},{id:"theme",title:"Change Theme",icon:"\u{1F3A8}",keywords:"theme dark light appearance"},{id:"tour",title:"Help Tour",icon:"\u{1F393}",keywords:"help tour guide onboarding"}].forEach(l=>{(l.title.toLowerCase().includes(e)||l.keywords.includes(e))&&t.actions.push(l)}),document.querySelectorAll(".card[data-app], #cards .card").forEach(l=>{const y=l.querySelector(".name")?.textContent||"",A=l.dataset.app||"",L=l.dataset.status||"unknown";(y.toLowerCase().includes(e)||A.toLowerCase().includes(e))&&t.services.push({id:A,title:y,status:L,icon:L==="on"?"\u{1F7E2}":"\u{1F534}"})}),t}function n(e){let t="";e.actions.length>0&&(t+='
Actions
',e.actions.forEach(s=>{t+=`
${s.icon}
${s.title}
- `})),t.services.length>0&&(e+='
Services
',t.services.forEach(s=>{e+=` + `})),e.services.length>0&&(t+='
Services
',e.services.forEach(s=>{t+=`
${s.icon}
@@ -217,4 +328,4 @@
${s.status.toUpperCase()}
- `})),e||(e='
No results found
'),l.innerHTML=e,f()}function f(){l.querySelectorAll(".quick-search-item").forEach((e,s)=>{e.addEventListener("click",()=>x(e)),s===0&&e.classList.add("selected")})}function v(t){try{const e=l.querySelectorAll(".quick-search-item"),s=l.querySelector(".quick-search-item.selected"),d=Array.from(e).indexOf(s);if(t.key==="ArrowDown"){t.preventDefault(),s&&s.classList.remove("selected");const o=(d+1)%e.length;e[o]?.classList.add("selected"),e[o]?.scrollIntoView({block:"nearest"})}else if(t.key==="ArrowUp"){t.preventDefault(),s&&s.classList.remove("selected");const o=d<=0?e.length-1:d-1;e[o]?.classList.add("selected"),e[o]?.scrollIntoView({block:"nearest"})}else t.key==="Enter"&&(t.preventDefault(),s&&x(s))}catch(e){console.warn("[Keyboard Shortcuts] Error handling search navigation:",e.message)}}function x(t){try{const e=t.dataset.action,s=t.dataset.service;switch(a(),e){case"refresh":document.getElementById("refresh")?.click();break;case"reload-caddy":document.getElementById("reload-caddy-top")?.click();break;case"add-service":document.getElementById("add-service")?.click();break;case"app-selector":document.getElementById("add-service-btn")?.click();break;case"backup":document.getElementById("backup-restore-btn")?.click();break;case"stats":document.getElementById("container-stats-btn")?.click();break;case"logs":document.getElementById("view-error-logs")?.click();break;case"tokens":document.getElementById("manage-tokens")?.click();break;case"notifications":document.getElementById("manage-notifications")?.click();break;case"theme":document.getElementById("theme")?.click();break;case"tour":document.getElementById("restart-tour-btn")?.click();break;case"open-service":if(s){const d=document.querySelector(`[data-app="${s}"] [id$="-open"], [data-app="${s}"] button:not(.restart-btn):not(.logs-btn):not(.settings-btn)`);if(d)d.click();else{const o=document.querySelector(`[data-app="${s}"]`);o&&o.click()}}break;default:p("[Keyboard Shortcuts] Unknown action:",e)}}catch(e){console.warn("[Keyboard Shortcuts] Error executing action:",e.message)}}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",m):m(),window.DashCaddyKeyboardShortcuts={openQuickSearch:k,closeQuickSearch:a}})(); + `})),t||(t='
No results found
'),f.innerHTML=t,c()}function c(){f.querySelectorAll(".quick-search-item").forEach((t,s)=>{t.addEventListener("click",()=>m(t)),s===0&&t.classList.add("selected")})}function r(e){try{const t=f.querySelectorAll(".quick-search-item"),s=f.querySelector(".quick-search-item.selected"),p=Array.from(t).indexOf(s);if(e.key==="ArrowDown"){e.preventDefault(),s&&s.classList.remove("selected");const l=(p+1)%t.length;t[l]?.classList.add("selected"),t[l]?.scrollIntoView({block:"nearest"})}else if(e.key==="ArrowUp"){e.preventDefault(),s&&s.classList.remove("selected");const l=p<=0?t.length-1:p-1;t[l]?.classList.add("selected"),t[l]?.scrollIntoView({block:"nearest"})}else e.key==="Enter"&&(e.preventDefault(),s&&m(s))}catch(t){console.warn("[Keyboard Shortcuts] Error handling search navigation:",t.message)}}function m(e){try{const t=e.dataset.action,s=e.dataset.service;switch(g(),t){case"refresh":document.getElementById("refresh")?.click();break;case"reload-caddy":document.getElementById("reload-caddy-top")?.click();break;case"add-service":document.getElementById("add-service")?.click();break;case"app-selector":document.getElementById("add-service-btn")?.click();break;case"backup":document.getElementById("backup-restore-btn")?.click();break;case"stats":document.getElementById("container-stats-btn")?.click();break;case"logs":document.getElementById("view-error-logs")?.click();break;case"tokens":document.getElementById("manage-tokens")?.click();break;case"notifications":document.getElementById("manage-notifications")?.click();break;case"theme":document.getElementById("theme")?.click();break;case"tour":document.getElementById("restart-tour-btn")?.click();break;case"open-service":if(s){const p=document.querySelector(`[data-app="${s}"] [id$="-open"], [data-app="${s}"] button:not(.restart-btn):not(.logs-btn):not(.settings-btn)`);if(p)p.click();else{const l=document.querySelector(`[data-app="${s}"]`);l&&l.click()}}break;default:v("[Keyboard Shortcuts] Unknown action:",t)}}catch(t){console.warn("[Keyboard Shortcuts] Error executing action:",t.message)}}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",b):b(),window.DashCaddyKeyboardShortcuts={openQuickSearch:S,closeQuickSearch:g}})(); diff --git a/status/dist/onboarding.js b/status/dist/onboarding.js index 6d2fd81..698bfcb 100644 --- a/status/dist/onboarding.js +++ b/status/dist/onboarding.js @@ -1,5 +1,5 @@ -this.driver=this.driver||{},this.driver.js=(function(y){"use strict";let P={};function _(e={}){P={animate:!0,allowClose:!0,overlayOpacity:.7,smoothScroll:!1,disableActiveInteraction:!1,showProgress:!1,stagePadding:10,stageRadius:5,popoverOffset:10,showButtons:["next","previous","close"],disableButtons:[],overlayColor:"#000",...e}}function a(e){return e?P[e]:P}function c(e,t,i,s){return(e/=s/2)<1?i/2*e*e+t:-i/2*(--e*(e-2)-1)+t}function r(e){const t='a[href]:not([disabled]), button:not([disabled]), textarea:not([disabled]), input[type="text"]:not([disabled]), input[type="radio"]:not([disabled]), input[type="checkbox"]:not([disabled]), select:not([disabled])';return e.flatMap(i=>{const s=i.matches(t),o=Array.from(i.querySelectorAll(t));return[...s?[i]:[],...o]}).filter(i=>getComputedStyle(i).pointerEvents!=="none"&&k(i))}function n(e){if(!e||A(e))return;const t=a("smoothScroll");e.scrollIntoView({behavior:!t||v(e)?"auto":"smooth",inline:"center",block:"center"})}function v(e){if(!e||!e.parentElement)return;const t=e.parentElement;return t.scrollHeight>t.clientHeight}function A(e){const t=e.getBoundingClientRect();return t.top>=0&&t.left>=0&&t.bottom<=(window.innerHeight||document.documentElement.clientHeight)&&t.right<=(window.innerWidth||document.documentElement.clientWidth)}function k(e){return!!(e.offsetWidth||e.offsetHeight||e.getClientRects().length)}let m={};function C(e,t){m[e]=t}function l(e){return e?m[e]:m}function u(){m={}}let b={};function $(e,t){b[e]=t}function L(e){var t;(t=b[e])==null||t.call(b)}function R(){b={}}function j(e,t,i,s){let o=l("__activeStagePosition");const h=o||i.getBoundingClientRect(),S=s.getBoundingClientRect(),x=c(e,h.x,S.x-h.x,t),p=c(e,h.y,S.y-h.y,t),T=c(e,h.width,S.width-h.width,t),d=c(e,h.height,S.height-h.height,t);o={x,y:p,width:T,height:d},X(o),C("__activeStagePosition",o)}function Q(e){if(!e)return;const t=e.getBoundingClientRect(),i={x:t.x,y:t.y,width:t.width,height:t.height};C("__activeStagePosition",i),X(i)}function le(){const e=l("__activeStagePosition"),t=l("__overlaySvg");if(!e)return;if(!t){console.warn("No stage svg found.");return}const i=window.innerWidth,s=window.innerHeight;t.setAttribute("viewBox",`0 0 ${i} ${s}`)}function de(e){const t=pe(e);document.body.appendChild(t),re(t,i=>{i.target.tagName==="path"&&L("overlayClick")}),C("__overlaySvg",t)}function X(e){const t=l("__overlaySvg");if(!t){de(e);return}const i=t.firstElementChild;if(i?.tagName!=="path")throw new Error("no path element found in stage svg");i.setAttribute("d",Z(e))}function pe(e){const t=window.innerWidth,i=window.innerHeight,s=document.createElementNS("http://www.w3.org/2000/svg","svg");s.classList.add("driver-overlay","driver-overlay-animated"),s.setAttribute("viewBox",`0 0 ${t} ${i}`),s.setAttribute("xmlSpace","preserve"),s.setAttribute("xmlnsXlink","http://www.w3.org/1999/xlink"),s.setAttribute("version","1.1"),s.setAttribute("preserveAspectRatio","xMinYMin slice"),s.style.fillRule="evenodd",s.style.clipRule="evenodd",s.style.strokeLinejoin="round",s.style.strokeMiterlimit="2",s.style.zIndex="10000",s.style.position="fixed",s.style.top="0",s.style.left="0",s.style.width="100%",s.style.height="100%";const o=document.createElementNS("http://www.w3.org/2000/svg","path");return o.setAttribute("d",Z(e)),o.style.fill=a("overlayColor")||"rgb(0,0,0)",o.style.opacity=`${a("overlayOpacity")}`,o.style.pointerEvents="auto",o.style.cursor="auto",s.appendChild(o),s}function Z(e){const t=window.innerWidth,i=window.innerHeight,s=a("stagePadding")||0,o=a("stageRadius")||0,h=e.width+s*2,S=e.height+s*2,x=Math.min(o,h/2,S/2),p=Math.floor(Math.max(x,0)),T=e.x-s+p,d=e.y-s,g=h-p*2,f=S-p*2;return`M${t},0L0,0L0,${i}L${t},${i}L${t},0Z - M${T},${d} h${g} a${p},${p} 0 0 1 ${p},${p} v${f} a${p},${p} 0 0 1 -${p},${p} h-${g} a${p},${p} 0 0 1 -${p},-${p} v-${f} a${p},${p} 0 0 1 ${p},-${p} z`}function ce(){const e=l("__overlaySvg");e&&e.remove()}function ue(){const e=document.getElementById("driver-dummy-element");if(e)return e;let t=document.createElement("div");return t.id="driver-dummy-element",t.style.width="0",t.style.height="0",t.style.pointerEvents="none",t.style.opacity="0",t.style.position="fixed",t.style.top="50%",t.style.left="50%",document.body.appendChild(t),t}function ee(e){const{element:t}=e;let i=typeof t=="string"?document.querySelector(t):t;i||(i=ue()),he(i,e)}function me(){const e=l("__activeElement"),t=l("__activeStep");e&&(Q(e),le(),ae(e,t))}function he(e,t){const i=Date.now(),s=l("__activeStep"),o=l("__activeElement")||e,h=!o||o===e,S=e.id==="driver-dummy-element",x=o.id==="driver-dummy-element",p=a("animate"),T=t.onHighlightStarted||a("onHighlightStarted"),d=t?.onHighlighted||a("onHighlighted"),g=s?.onDeselected||a("onDeselected"),f=a(),D=l();!h&&g&&g(x?void 0:o,s,{config:f,state:D}),T&&T(S?void 0:e,t,{config:f,state:D});const B=!h&&p;let E=!1;we(),C("previousStep",s),C("previousElement",o),C("activeStep",t),C("activeElement",e);const w=()=>{if(l("__transitionCallback")!==w)return;const I=Date.now()-i,O=400-I<=400/2;t.popover&&O&&!E&&B&&(oe(e,t),E=!0),a("animate")&&I<400?j(I,400,o,e):(Q(e),d&&d(S?void 0:e,t,{config:a(),state:l()}),C("__transitionCallback",void 0),C("__previousStep",s),C("__previousElement",o),C("__activeStep",t),C("__activeElement",e)),window.requestAnimationFrame(w)};C("__transitionCallback",w),window.requestAnimationFrame(w),n(e),!B&&t.popover&&oe(e,t),o.classList.remove("driver-active-element","driver-no-interaction"),o.removeAttribute("aria-haspopup"),o.removeAttribute("aria-expanded"),o.removeAttribute("aria-controls"),a("disableActiveInteraction")&&e.classList.add("driver-no-interaction"),e.classList.add("driver-active-element"),e.setAttribute("aria-haspopup","dialog"),e.setAttribute("aria-expanded","true"),e.setAttribute("aria-controls","driver-popover-content")}function ge(){var e;(e=document.getElementById("driver-dummy-element"))==null||e.remove(),document.querySelectorAll(".driver-active-element").forEach(t=>{t.classList.remove("driver-active-element","driver-no-interaction"),t.removeAttribute("aria-haspopup"),t.removeAttribute("aria-expanded"),t.removeAttribute("aria-controls")})}function U(){const e=l("__resizeTimeout");e&&window.cancelAnimationFrame(e),C("__resizeTimeout",window.requestAnimationFrame(me))}function ve(e){var t;if(!l("isInitialized")||!(e.key==="Tab"||e.keyCode===9))return;const i=l("__activeElement"),s=(t=l("popover"))==null?void 0:t.wrapper,o=r([...s?[s]:[],...i?[i]:[]]),h=o[0],S=o[o.length-1];if(e.preventDefault(),e.shiftKey){const x=o[o.indexOf(document.activeElement)-1]||S;x?.focus()}else{const x=o[o.indexOf(document.activeElement)+1]||h;x?.focus()}}function te(e){var t;((t=a("allowKeyboardControl"))==null||t)&&(e.key==="Escape"?L("escapePress"):e.key==="ArrowRight"?L("arrowRightPress"):e.key==="ArrowLeft"&&L("arrowLeftPress"))}function re(e,t,i){const s=(o,h)=>{const S=o.target;e.contains(S)&&((!i||i(S))&&(o.preventDefault(),o.stopPropagation(),o.stopImmediatePropagation()),h?.(o))};document.addEventListener("pointerdown",s,!0),document.addEventListener("mousedown",s,!0),document.addEventListener("pointerup",s,!0),document.addEventListener("mouseup",s,!0),document.addEventListener("click",o=>{s(o,t)},!0)}function fe(){window.addEventListener("keyup",te,!1),window.addEventListener("keydown",ve,!1),window.addEventListener("resize",U),window.addEventListener("scroll",U)}function ye(){window.removeEventListener("keyup",te),window.removeEventListener("resize",U),window.removeEventListener("scroll",U)}function we(){const e=l("popover");e&&(e.wrapper.style.display="none")}function oe(e,t){var i,s;let o=l("popover");o&&document.body.removeChild(o.wrapper),o=Te(),document.body.appendChild(o.wrapper);const{title:h,description:S,showButtons:x,disableButtons:p,showProgress:T,nextBtnText:d=a("nextBtnText")||"Next →",prevBtnText:g=a("prevBtnText")||"← Previous",progressText:f=a("progressText")||"{current} of {total}"}=t.popover||{};o.nextButton.innerHTML=d,o.previousButton.innerHTML=g,o.progress.innerHTML=f,h?(o.title.innerHTML=h,o.title.style.display="block"):o.title.style.display="none",S?(o.description.innerHTML=S,o.description.style.display="block"):o.description.style.display="none";const D=x||a("showButtons"),B=T||a("showProgress")||!1,E=D?.includes("next")||D?.includes("previous")||B;o.closeButton.style.display=D.includes("close")?"block":"none",E?(o.footer.style.display="flex",o.progress.style.display=B?"block":"none",o.nextButton.style.display=D.includes("next")?"block":"none",o.previousButton.style.display=D.includes("previous")?"block":"none"):o.footer.style.display="none";const w=p||a("disableButtons")||[];w!=null&&w.includes("next")&&(o.nextButton.disabled=!0,o.nextButton.classList.add("driver-popover-btn-disabled")),w!=null&&w.includes("previous")&&(o.previousButton.disabled=!0,o.previousButton.classList.add("driver-popover-btn-disabled")),w!=null&&w.includes("close")&&(o.closeButton.disabled=!0,o.closeButton.classList.add("driver-popover-btn-disabled"));const I=o.wrapper;I.style.display="block",I.style.left="",I.style.top="",I.style.bottom="",I.style.right="",I.id="driver-popover-content",I.setAttribute("role","dialog"),I.setAttribute("aria-labelledby","driver-popover-title"),I.setAttribute("aria-describedby","driver-popover-description");const O=o.arrow;O.className="driver-popover-arrow";const z=((i=t.popover)==null?void 0:i.popoverClass)||a("popoverClass")||"";I.className=`driver-popover ${z}`.trim(),re(o.wrapper,F=>{var V,G,q;const W=F.target,Y=((V=t.popover)==null?void 0:V.onNextClick)||a("onNextClick"),K=((G=t.popover)==null?void 0:G.onPrevClick)||a("onPrevClick"),J=((q=t.popover)==null?void 0:q.onCloseClick)||a("onCloseClick");if(W.classList.contains("driver-popover-next-btn"))return Y?Y(e,t,{config:a(),state:l()}):L("nextClick");if(W.classList.contains("driver-popover-prev-btn"))return K?K(e,t,{config:a(),state:l()}):L("prevClick");if(W.classList.contains("driver-popover-close-btn"))return J?J(e,t,{config:a(),state:l()}):L("closeClick")},F=>!(o!=null&&o.description.contains(F))&&!(o!=null&&o.title.contains(F))&&typeof F.className=="string"&&F.className.includes("driver-popover")),C("popover",o);const N=((s=t.popover)==null?void 0:s.onPopoverRender)||a("onPopoverRender");N&&N(o,{config:a(),state:l()}),ae(e,t),n(I);const M=e.classList.contains("driver-dummy-element"),H=r([I,...M?[]:[e]]);H.length>0&&H[0].focus()}function ie(){const e=l("popover");if(!(e!=null&&e.wrapper))return;const t=e.wrapper.getBoundingClientRect(),i=a("stagePadding")||0,s=a("popoverOffset")||0;return{width:t.width+i+s,height:t.height+i+s,realWidth:t.width,realHeight:t.height}}function ne(e,t){const{elementDimensions:i,popoverDimensions:s,popoverPadding:o,popoverArrowDimensions:h}=t;return e==="start"?Math.max(Math.min(i.top-o,window.innerHeight-s.realHeight-h.width),h.width):e==="end"?Math.max(Math.min(i.top-s?.realHeight+i.height+o,window.innerHeight-s?.realHeight-h.width),h.width):e==="center"?Math.max(Math.min(i.top+i.height/2-s?.realHeight/2,window.innerHeight-s?.realHeight-h.width),h.width):0}function se(e,t){const{elementDimensions:i,popoverDimensions:s,popoverPadding:o,popoverArrowDimensions:h}=t;return e==="start"?Math.max(Math.min(i.left-o,window.innerWidth-s.realWidth-h.width),h.width):e==="end"?Math.max(Math.min(i.left-s?.realWidth+i.width+o,window.innerWidth-s?.realWidth-h.width),h.width):e==="center"?Math.max(Math.min(i.left+i.width/2-s?.realWidth/2,window.innerWidth-s?.realWidth-h.width),h.width):0}function ae(e,t){const i=l("popover");if(!i)return;const{align:s="start",side:o="left"}=t?.popover||{},h=s,S=e.id==="driver-dummy-element"?"over":o,x=a("stagePadding")||0,p=ie(),T=i.arrow.getBoundingClientRect(),d=e.getBoundingClientRect(),g=d.top-p.height;let f=g>=0;const D=window.innerHeight-(d.bottom+p.height);let B=D>=0;const E=d.left-p.width;let w=E>=0;const I=window.innerWidth-(d.right+p.width);let O=I>=0;const z=!f&&!B&&!w&&!O;let N=S;if(S==="top"&&f?O=w=B=!1:S==="bottom"&&B?O=w=f=!1:S==="left"&&w?O=f=B=!1:S==="right"&&O&&(w=f=B=!1),S==="over"){const M=window.innerWidth/2-p.realWidth/2,H=window.innerHeight/2-p.realHeight/2;i.wrapper.style.left=`${M}px`,i.wrapper.style.right="auto",i.wrapper.style.top=`${H}px`,i.wrapper.style.bottom="auto"}else if(z){const M=window.innerWidth/2-p?.realWidth/2,H=10;i.wrapper.style.left=`${M}px`,i.wrapper.style.right="auto",i.wrapper.style.bottom=`${H}px`,i.wrapper.style.top="auto"}else if(w){const M=Math.min(E,window.innerWidth-p?.realWidth-T.width),H=ne(h,{elementDimensions:d,popoverDimensions:p,popoverPadding:x,popoverArrowDimensions:T});i.wrapper.style.left=`${M}px`,i.wrapper.style.top=`${H}px`,i.wrapper.style.bottom="auto",i.wrapper.style.right="auto",N="left"}else if(O){const M=Math.min(I,window.innerWidth-p?.realWidth-T.width),H=ne(h,{elementDimensions:d,popoverDimensions:p,popoverPadding:x,popoverArrowDimensions:T});i.wrapper.style.right=`${M}px`,i.wrapper.style.top=`${H}px`,i.wrapper.style.bottom="auto",i.wrapper.style.left="auto",N="right"}else if(f){const M=Math.min(g,window.innerHeight-p.realHeight-T.width);let H=se(h,{elementDimensions:d,popoverDimensions:p,popoverPadding:x,popoverArrowDimensions:T});i.wrapper.style.top=`${M}px`,i.wrapper.style.left=`${H}px`,i.wrapper.style.bottom="auto",i.wrapper.style.right="auto",N="top"}else if(B){const M=Math.min(D,window.innerHeight-p?.realHeight-T.width);let H=se(h,{elementDimensions:d,popoverDimensions:p,popoverPadding:x,popoverArrowDimensions:T});i.wrapper.style.left=`${H}px`,i.wrapper.style.bottom=`${M}px`,i.wrapper.style.top="auto",i.wrapper.style.right="auto",N="bottom"}z?i.arrow.classList.add("driver-popover-arrow-none"):be(h,N,e)}function be(e,t,i){const s=l("popover");if(!s)return;const o=i.getBoundingClientRect(),h=ie(),S=s.arrow,x=h.width,p=window.innerWidth,T=o.width,d=o.left,g=h.height,f=window.innerHeight,D=o.top,B=o.height;S.className="driver-popover-arrow";let E=t,w=e;t==="top"?(d+T<=0?(E="right",w="end"):d+T-x<=0&&(E="top",w="start"),d>=p?(E="left",w="end"):d+x>=p&&(E="top",w="end")):t==="bottom"?(d+T<=0?(E="right",w="start"):d+T-x<=0&&(E="bottom",w="start"),d>=p?(E="left",w="start"):d+x>=p&&(E="bottom",w="end")):t==="left"?(D+B<=0?(E="bottom",w="end"):D+B-g<=0&&(E="left",w="start"),D>=f?(E="top",w="end"):D+g>=f&&(E="left",w="end")):t==="right"&&(D+B<=0?(E="bottom",w="start"):D+B-g<=0&&(E="right",w="start"),D>=f?(E="top",w="start"):D+g>=f&&(E="right",w="end")),E?(S.classList.add(`driver-popover-arrow-side-${E}`),S.classList.add(`driver-popover-arrow-align-${w}`)):S.classList.add("driver-popover-arrow-none")}function Te(){const e=document.createElement("div");e.classList.add("driver-popover");const t=document.createElement("div");t.classList.add("driver-popover-arrow");const i=document.createElement("header");i.id="driver-popover-title",i.classList.add("driver-popover-title"),i.style.display="none",i.innerText="Popover Title";const s=document.createElement("div");s.id="driver-popover-description",s.classList.add("driver-popover-description"),s.style.display="none",s.innerText="Popover description is here";const o=document.createElement("button");o.type="button",o.classList.add("driver-popover-close-btn"),o.setAttribute("aria-label","Close"),o.innerHTML="×";const h=document.createElement("footer");h.classList.add("driver-popover-footer");const S=document.createElement("span");S.classList.add("driver-popover-progress-text"),S.innerText="";const x=document.createElement("span");x.classList.add("driver-popover-navigation-btns");const p=document.createElement("button");p.type="button",p.classList.add("driver-popover-prev-btn"),p.innerHTML="← Previous";const T=document.createElement("button");return T.type="button",T.classList.add("driver-popover-next-btn"),T.innerHTML="Next →",x.appendChild(p),x.appendChild(T),h.appendChild(S),h.appendChild(x),e.appendChild(o),e.appendChild(t),e.appendChild(i),e.appendChild(s),e.appendChild(h),{wrapper:e,arrow:t,title:i,description:s,footer:h,previousButton:p,nextButton:T,closeButton:o,footerButtons:x,progress:S}}function Se(){var e;const t=l("popover");t&&((e=t.wrapper.parentElement)==null||e.removeChild(t.wrapper))}const De="";function Ce(e={}){_(e);function t(){a("allowClose")&&T()}function i(){const d=l("activeIndex"),g=a("steps")||[];if(typeof d>"u")return;const f=d+1;g[f]?p(f):T()}function s(){const d=l("activeIndex"),g=a("steps")||[];if(typeof d>"u")return;const f=d-1;g[f]?p(f):T()}function o(d){(a("steps")||[])[d]?p(d):T()}function h(){var d;if(l("__transitionCallback"))return;const g=l("activeIndex"),f=l("__activeStep"),D=l("__activeElement");if(typeof g>"u"||typeof f>"u"||typeof l("activeIndex")>"u")return;const B=((d=f.popover)==null?void 0:d.onPrevClick)||a("onPrevClick");if(B)return B(D,f,{config:a(),state:l()});s()}function S(){var d;if(l("__transitionCallback"))return;const g=l("activeIndex"),f=l("__activeStep"),D=l("__activeElement");if(typeof g>"u"||typeof f>"u")return;const B=((d=f.popover)==null?void 0:d.onNextClick)||a("onNextClick");if(B)return B(D,f,{config:a(),state:l()});i()}function x(){l("isInitialized")||(C("isInitialized",!0),document.body.classList.add("driver-active",a("animate")?"driver-fade":"driver-simple"),fe(),$("overlayClick",t),$("escapePress",t),$("arrowLeftPress",h),$("arrowRightPress",S))}function p(d=0){var g,f,D,B,E,w,I,O;const z=a("steps");if(!z){console.error("No steps to drive through"),T();return}if(!z[d]){T();return}C("__activeOnDestroyed",document.activeElement),C("activeIndex",d);const N=z[d],M=z[d+1],H=z[d-1],F=((g=N.popover)==null?void 0:g.doneBtnText)||a("doneBtnText")||"Done",V=a("allowClose"),G=typeof((f=N.popover)==null?void 0:f.showProgress)<"u"?(D=N.popover)==null?void 0:D.showProgress:a("showProgress"),q=(((B=N.popover)==null?void 0:B.progressText)||a("progressText")||"{{current}} of {{total}}").replace("{{current}}",`${d+1}`).replace("{{total}}",`${z.length}`),W=((E=N.popover)==null?void 0:E.showButtons)||a("showButtons"),Y=["next","previous",...V?["close"]:[]].filter(xe=>!(W!=null&&W.length)||W.includes(xe)),K=((w=N.popover)==null?void 0:w.onNextClick)||a("onNextClick"),J=((I=N.popover)==null?void 0:I.onPrevClick)||a("onPrevClick"),ke=((O=N.popover)==null?void 0:O.onCloseClick)||a("onCloseClick");ee({...N,popover:{showButtons:Y,nextBtnText:M?void 0:F,disableButtons:[...H?[]:["previous"]],showProgress:G,progressText:q,onNextClick:K||(()=>{M?p(d+1):T()}),onPrevClick:J||(()=>{p(d-1)}),onCloseClick:ke||(()=>{T()}),...N?.popover||{}}})}function T(d=!0){const g=l("__activeElement"),f=l("__activeStep"),D=l("__activeOnDestroyed"),B=a("onDestroyStarted");if(d&&B){const I=!g||g?.id==="driver-dummy-element";B(I?void 0:g,f,{config:a(),state:l()});return}const E=f?.onDeselected||a("onDeselected"),w=a("onDestroyed");if(document.body.classList.remove("driver-active","driver-fade","driver-simple"),ye(),Se(),ge(),ce(),R(),u(),g&&f){const I=g.id==="driver-dummy-element";E&&E(I?void 0:g,f,{config:a(),state:l()}),w&&w(I?void 0:g,f,{config:a(),state:l()})}D&&D.focus()}return{isActive:()=>l("isInitialized")||!1,refresh:U,drive:(d=0)=>{x(),p(d)},setConfig:_,setSteps:d=>{u(),_({...a(),steps:d})},getConfig:a,getState:l,getActiveIndex:()=>l("activeIndex"),isFirstStep:()=>l("activeIndex")===0,isLastStep:()=>{const d=a("steps")||[],g=l("activeIndex");return g!==void 0&&g===d.length-1},getActiveStep:()=>l("activeStep"),getActiveElement:()=>l("activeElement"),getPreviousElement:()=>l("previousElement"),getPreviousStep:()=>l("previousStep"),moveNext:i,movePrevious:s,moveTo:o,hasNextStep:()=>{const d=a("steps")||[],g=l("activeIndex");return g!==void 0&&d[g+1]},hasPreviousStep:()=>{const d=a("steps")||[],g=l("activeIndex");return g!==void 0&&d[g-1]},highlight:d=>{x(),ee({...d,popover:d.popover?{showButtons:[],showProgress:!1,progressText:"",...d.popover}:void 0})},destroy:()=>{T(!1)}}}return y.driver=Ce,Object.defineProperty(y,Symbol.toStringTag,{value:"Module"}),y})({}),(function(y){"use strict";const P=new ErrorHandler,_=(...c)=>{y.DASHCADDY_DEBUG&&console.log(...c)};class a{constructor(r="dashcaddy_onboarding"){this.storageKey=r,this.storageVersion="1.0",this.installOnboardingCompleted=typeof SITE<"u"&&SITE.onboardingCompleted===!0,this._initializeStorage(),this._updateLastVisit()}_initializeStorage(){const r=this._getStorage();if(!r||r.version!==this.storageVersion){const n={version:this.storageVersion,tourCompleted:!1,completedTooltips:[],currentStep:0,completionTimestamp:null,dnsSetupDeferred:!1,lastVisit:new Date().toISOString()};this._setStorage(n)}}_getStorage(){try{const r=localStorage.getItem(this.storageKey);return r?JSON.parse(r):null}catch(r){return P.logError("[ProgressTracker] Read Storage",r,{function:"_getStorage"}),null}}_setStorage(r){try{localStorage.setItem(this.storageKey,JSON.stringify(r))}catch(n){P.logError("[ProgressTracker] Write Storage",n,{function:"_setStorage"}),this._handleStorageError(n)}}_handleStorageError(r){try{sessionStorage.setItem(this.storageKey,JSON.stringify(this._getStorage())),console.warn("[ProgressTracker] Falling back to session storage")}catch(n){P.logError("[ProgressTracker] Session Storage Unavailable",n,{function:"_handleStorageError"})}}_updateLastVisit(){const r=this._getStorage();r&&(r.lastVisit=new Date().toISOString(),this._setStorage(r))}isTooltipCompleted(r){const n=this._getStorage();return n?n.completedTooltips.includes(r):!1}markTooltipCompleted(r){const n=this._getStorage();n&&(n.completedTooltips.includes(r)||(n.completedTooltips.push(r),n.tooltipTimestamps||(n.tooltipTimestamps={}),n.tooltipTimestamps[r]=new Date().toISOString(),this._setStorage(n)))}isTourCompleted(){const r=this._getStorage();return r?r.tourCompleted===!0:!1}isInstallOnboardingCompleted(){return this.installOnboardingCompleted===!0}async markInstallOnboardingCompleted(){if(!this.installOnboardingCompleted){this.installOnboardingCompleted=!0,typeof SITE<"u"&&(SITE.onboardingCompleted=!0);try{await secureFetch("/api/v1/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({onboardingCompleted:!0})})}catch(r){P.logError("[ProgressTracker] Persist Install Onboarding",r,{function:"markInstallOnboardingCompleted"})}}}markTourCompleted(){const r=this._getStorage();r&&(r.tourCompleted=!0,r.completionTimestamp=new Date().toISOString(),this._setStorage(r))}getCurrentStep(){const r=this._getStorage();return r&&r.currentStep||0}setCurrentStep(r){const n=this._getStorage();n&&(n.currentStep=r,this._setStorage(n))}resetProgress(){const r={version:this.storageVersion,tourCompleted:!1,completedTooltips:[],currentStep:0,completionTimestamp:null,dnsSetupDeferred:!1,lastVisit:new Date().toISOString()};this._setStorage(r)}getCompletionTimestamp(){const r=this._getStorage();return!r||!r.completionTimestamp?null:new Date(r.completionTimestamp)}isDnsSetupDeferred(){const r=this._getStorage();return r?r.dnsSetupDeferred===!0:!1}markDnsSetupDeferred(){const r=this._getStorage();r&&(r.dnsSetupDeferred=!0,this._setStorage(r))}getTooltipTimestamp(r){const n=this._getStorage();return!n||!n.tooltipTimestamps||!n.tooltipTimestamps[r]?null:new Date(n.tooltipTimestamps[r])}getCompletedTooltips(){const r=this._getStorage();return r?r.completedTooltips||[]:[]}getLastVisit(){const r=this._getStorage();return!r||!r.lastVisit?null:new Date(r.lastVisit)}}y.ProgressTracker=a,_("[ProgressTracker] Module loaded")})(window),(function(y){"use strict";const P=new ErrorHandler,_=(...r)=>{y.DASHCADDY_DEBUG&&console.log(...r)},a={dark:{backgroundColor:"var(--card-base)",textColor:"var(--fg)",primaryColor:"var(--accent)",overlayColor:"rgba(0, 0, 0, 0.7)",borderColor:"var(--border)",highlightColor:"var(--accent)",fontFamily:"'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif"},light:{backgroundColor:"var(--card-base)",textColor:"var(--fg)",primaryColor:"var(--accent-strong)",overlayColor:"rgba(0, 0, 0, 0.5)",borderColor:"var(--border)",highlightColor:"var(--accent-strong)",fontFamily:"'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif"},blue:{backgroundColor:"var(--card-base)",textColor:"var(--fg)",primaryColor:"var(--accent)",overlayColor:"rgba(25, 8, 172, 0.7)",borderColor:"var(--border)",highlightColor:"var(--accent)",fontFamily:"'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif"},nord:{backgroundColor:"var(--card-base)",textColor:"var(--fg)",primaryColor:"var(--accent)",overlayColor:"rgba(46, 52, 64, 0.7)",borderColor:"var(--border)",highlightColor:"var(--accent)",fontFamily:"'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif"},dracula:{backgroundColor:"var(--card-base)",textColor:"var(--fg)",primaryColor:"var(--accent)",overlayColor:"rgba(40, 42, 54, 0.7)",borderColor:"var(--border)",highlightColor:"var(--accent)",fontFamily:"'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif"},"solarized-dark":{backgroundColor:"var(--card-base)",textColor:"var(--fg)",primaryColor:"var(--accent)",overlayColor:"rgba(0, 43, 54, 0.7)",borderColor:"var(--border)",highlightColor:"var(--accent)",fontFamily:"'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif"},"solarized-light":{backgroundColor:"var(--card-base)",textColor:"var(--fg)",primaryColor:"var(--accent)",overlayColor:"rgba(253, 246, 227, 0.7)",borderColor:"var(--border)",highlightColor:"var(--accent)",fontFamily:"'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif"}};class c{constructor(){this.currentTheme=this.getCurrentTheme(),this.themeChangeCallbacks=[],this._setupThemeChangeListener()}getCurrentTheme(){const n=document.documentElement,v=Array.from(n.classList);return(y.THEMES||[]).filter(m=>m!=="dark").find(m=>v.includes(m))||"dark"}getDriverTheme(){const n=this.getCurrentTheme(),v=a[n]||a.dark,A={};for(const[k,m]of Object.entries(v))if(typeof m=="string"&&m.startsWith("var(")){const C=m.match(/var\((--[^)]+)\)/)?.[1];if(C){const l=getComputedStyle(document.documentElement).getPropertyValue(C).trim();A[k]=l||m}else A[k]=m}else A[k]=m;return A}onThemeChange(n){typeof n=="function"&&this.themeChangeCallbacks.push(n)}_setupThemeChangeListener(){const n=document.documentElement;new MutationObserver(A=>{A.forEach(k=>{if(k.type==="attributes"&&k.attributeName==="class"){const m=this.getCurrentTheme();if(m!==this.currentTheme){const C=this.currentTheme;this.currentTheme=m,this._notifyThemeChange(m,C)}}})}).observe(n,{attributes:!0,attributeFilter:["class"]}),_("[ThemeAdapter] Theme change listener initialized")}_notifyThemeChange(n,v){_(`[ThemeAdapter] Theme changed: ${v} \u2192 ${n}`),this.themeChangeCallbacks.forEach(A=>{try{A(n,v)}catch(k){P.logError("[ThemeAdapter] Theme Change Callback",k,{function:"_notifyThemeChange"})}})}applyTheme(n){if(!n){console.warn("[ThemeAdapter] No driver instance provided");return}const v=this.getDriverTheme();this._injectDriverStyles(v),_("[ThemeAdapter] Theme applied to driver:",this.currentTheme)}_injectDriverStyles(n){const v=document.getElementById("driver-theme-styles");v&&v.remove();const A=document.createElement("style");A.id="driver-theme-styles",A.textContent=` +this.driver=this.driver||{},this.driver.js=(function(y){"use strict";let P={};function _(e={}){P={animate:!0,allowClose:!0,overlayOpacity:.7,smoothScroll:!1,disableActiveInteraction:!1,showProgress:!1,stagePadding:10,stageRadius:5,popoverOffset:10,showButtons:["next","previous","close"],disableButtons:[],overlayColor:"#000",...e}}function a(e){return e?P[e]:P}function c(e,t,i,s){return(e/=s/2)<1?i/2*e*e+t:-i/2*(--e*(e-2)-1)+t}function r(e){const t='a[href]:not([disabled]), button:not([disabled]), textarea:not([disabled]), input[type="text"]:not([disabled]), input[type="radio"]:not([disabled]), input[type="checkbox"]:not([disabled]), select:not([disabled])';return e.flatMap(i=>{const s=i.matches(t),o=Array.from(i.querySelectorAll(t));return[...s?[i]:[],...o]}).filter(i=>getComputedStyle(i).pointerEvents!=="none"&&k(i))}function n(e){if(!e||A(e))return;const t=a("smoothScroll");e.scrollIntoView({behavior:!t||v(e)?"auto":"smooth",inline:"center",block:"center"})}function v(e){if(!e||!e.parentElement)return;const t=e.parentElement;return t.scrollHeight>t.clientHeight}function A(e){const t=e.getBoundingClientRect();return t.top>=0&&t.left>=0&&t.bottom<=(window.innerHeight||document.documentElement.clientHeight)&&t.right<=(window.innerWidth||document.documentElement.clientWidth)}function k(e){return!!(e.offsetWidth||e.offsetHeight||e.getClientRects().length)}let m={};function C(e,t){m[e]=t}function l(e){return e?m[e]:m}function u(){m={}}let b={};function $(e,t){b[e]=t}function L(e){var t;(t=b[e])==null||t.call(b)}function R(){b={}}function j(e,t,i,s){let o=l("__activeStagePosition");const h=o||i.getBoundingClientRect(),T=s.getBoundingClientRect(),x=c(e,h.x,T.x-h.x,t),p=c(e,h.y,T.y-h.y,t),S=c(e,h.width,T.width-h.width,t),d=c(e,h.height,T.height-h.height,t);o={x,y:p,width:S,height:d},X(o),C("__activeStagePosition",o)}function Q(e){if(!e)return;const t=e.getBoundingClientRect(),i={x:t.x,y:t.y,width:t.width,height:t.height};C("__activeStagePosition",i),X(i)}function le(){const e=l("__activeStagePosition"),t=l("__overlaySvg");if(!e)return;if(!t){console.warn("No stage svg found.");return}const i=window.innerWidth,s=window.innerHeight;t.setAttribute("viewBox",`0 0 ${i} ${s}`)}function de(e){const t=pe(e);document.body.appendChild(t),re(t,i=>{i.target.tagName==="path"&&L("overlayClick")}),C("__overlaySvg",t)}function X(e){const t=l("__overlaySvg");if(!t){de(e);return}const i=t.firstElementChild;if(i?.tagName!=="path")throw new Error("no path element found in stage svg");i.setAttribute("d",Z(e))}function pe(e){const t=window.innerWidth,i=window.innerHeight,s=document.createElementNS("http://www.w3.org/2000/svg","svg");s.classList.add("driver-overlay","driver-overlay-animated"),s.setAttribute("viewBox",`0 0 ${t} ${i}`),s.setAttribute("xmlSpace","preserve"),s.setAttribute("xmlnsXlink","http://www.w3.org/1999/xlink"),s.setAttribute("version","1.1"),s.setAttribute("preserveAspectRatio","xMinYMin slice"),s.style.fillRule="evenodd",s.style.clipRule="evenodd",s.style.strokeLinejoin="round",s.style.strokeMiterlimit="2",s.style.zIndex="10000",s.style.position="fixed",s.style.top="0",s.style.left="0",s.style.width="100%",s.style.height="100%";const o=document.createElementNS("http://www.w3.org/2000/svg","path");return o.setAttribute("d",Z(e)),o.style.fill=a("overlayColor")||"rgb(0,0,0)",o.style.opacity=`${a("overlayOpacity")}`,o.style.pointerEvents="auto",o.style.cursor="auto",s.appendChild(o),s}function Z(e){const t=window.innerWidth,i=window.innerHeight,s=a("stagePadding")||0,o=a("stageRadius")||0,h=e.width+s*2,T=e.height+s*2,x=Math.min(o,h/2,T/2),p=Math.floor(Math.max(x,0)),S=e.x-s+p,d=e.y-s,g=h-p*2,f=T-p*2;return`M${t},0L0,0L0,${i}L${t},${i}L${t},0Z + M${S},${d} h${g} a${p},${p} 0 0 1 ${p},${p} v${f} a${p},${p} 0 0 1 -${p},${p} h-${g} a${p},${p} 0 0 1 -${p},-${p} v-${f} a${p},${p} 0 0 1 ${p},-${p} z`}function ce(){const e=l("__overlaySvg");e&&e.remove()}function ue(){const e=document.getElementById("driver-dummy-element");if(e)return e;let t=document.createElement("div");return t.id="driver-dummy-element",t.style.width="0",t.style.height="0",t.style.pointerEvents="none",t.style.opacity="0",t.style.position="fixed",t.style.top="50%",t.style.left="50%",document.body.appendChild(t),t}function ee(e){const{element:t}=e;let i=typeof t=="string"?document.querySelector(t):t;i||(i=ue()),he(i,e)}function me(){const e=l("__activeElement"),t=l("__activeStep");e&&(Q(e),le(),ae(e,t))}function he(e,t){const i=Date.now(),s=l("__activeStep"),o=l("__activeElement")||e,h=!o||o===e,T=e.id==="driver-dummy-element",x=o.id==="driver-dummy-element",p=a("animate"),S=t.onHighlightStarted||a("onHighlightStarted"),d=t?.onHighlighted||a("onHighlighted"),g=s?.onDeselected||a("onDeselected"),f=a(),D=l();!h&&g&&g(x?void 0:o,s,{config:f,state:D}),S&&S(T?void 0:e,t,{config:f,state:D});const B=!h&&p;let E=!1;we(),C("previousStep",s),C("previousElement",o),C("activeStep",t),C("activeElement",e);const w=()=>{if(l("__transitionCallback")!==w)return;const I=Date.now()-i,O=400-I<=400/2;t.popover&&O&&!E&&B&&(oe(e,t),E=!0),a("animate")&&I<400?j(I,400,o,e):(Q(e),d&&d(T?void 0:e,t,{config:a(),state:l()}),C("__transitionCallback",void 0),C("__previousStep",s),C("__previousElement",o),C("__activeStep",t),C("__activeElement",e)),window.requestAnimationFrame(w)};C("__transitionCallback",w),window.requestAnimationFrame(w),n(e),!B&&t.popover&&oe(e,t),o.classList.remove("driver-active-element","driver-no-interaction"),o.removeAttribute("aria-haspopup"),o.removeAttribute("aria-expanded"),o.removeAttribute("aria-controls"),a("disableActiveInteraction")&&e.classList.add("driver-no-interaction"),e.classList.add("driver-active-element"),e.setAttribute("aria-haspopup","dialog"),e.setAttribute("aria-expanded","true"),e.setAttribute("aria-controls","driver-popover-content")}function ge(){var e;(e=document.getElementById("driver-dummy-element"))==null||e.remove(),document.querySelectorAll(".driver-active-element").forEach(t=>{t.classList.remove("driver-active-element","driver-no-interaction"),t.removeAttribute("aria-haspopup"),t.removeAttribute("aria-expanded"),t.removeAttribute("aria-controls")})}function U(){const e=l("__resizeTimeout");e&&window.cancelAnimationFrame(e),C("__resizeTimeout",window.requestAnimationFrame(me))}function ve(e){var t;if(!l("isInitialized")||!(e.key==="Tab"||e.keyCode===9))return;const i=l("__activeElement"),s=(t=l("popover"))==null?void 0:t.wrapper,o=r([...s?[s]:[],...i?[i]:[]]),h=o[0],T=o[o.length-1];if(e.preventDefault(),e.shiftKey){const x=o[o.indexOf(document.activeElement)-1]||T;x?.focus()}else{const x=o[o.indexOf(document.activeElement)+1]||h;x?.focus()}}function te(e){var t;((t=a("allowKeyboardControl"))==null||t)&&(e.key==="Escape"?L("escapePress"):e.key==="ArrowRight"?L("arrowRightPress"):e.key==="ArrowLeft"&&L("arrowLeftPress"))}function re(e,t,i){const s=(o,h)=>{const T=o.target;e.contains(T)&&((!i||i(T))&&(o.preventDefault(),o.stopPropagation(),o.stopImmediatePropagation()),h?.(o))};document.addEventListener("pointerdown",s,!0),document.addEventListener("mousedown",s,!0),document.addEventListener("pointerup",s,!0),document.addEventListener("mouseup",s,!0),document.addEventListener("click",o=>{s(o,t)},!0)}function fe(){window.addEventListener("keyup",te,!1),window.addEventListener("keydown",ve,!1),window.addEventListener("resize",U),window.addEventListener("scroll",U)}function ye(){window.removeEventListener("keyup",te),window.removeEventListener("resize",U),window.removeEventListener("scroll",U)}function we(){const e=l("popover");e&&(e.wrapper.style.display="none")}function oe(e,t){var i,s;let o=l("popover");o&&document.body.removeChild(o.wrapper),o=Se(),document.body.appendChild(o.wrapper);const{title:h,description:T,showButtons:x,disableButtons:p,showProgress:S,nextBtnText:d=a("nextBtnText")||"Next →",prevBtnText:g=a("prevBtnText")||"← Previous",progressText:f=a("progressText")||"{current} of {total}"}=t.popover||{};o.nextButton.innerHTML=d,o.previousButton.innerHTML=g,o.progress.innerHTML=f,h?(o.title.innerHTML=h,o.title.style.display="block"):o.title.style.display="none",T?(o.description.innerHTML=T,o.description.style.display="block"):o.description.style.display="none";const D=x||a("showButtons"),B=S||a("showProgress")||!1,E=D?.includes("next")||D?.includes("previous")||B;o.closeButton.style.display=D.includes("close")?"block":"none",E?(o.footer.style.display="flex",o.progress.style.display=B?"block":"none",o.nextButton.style.display=D.includes("next")?"block":"none",o.previousButton.style.display=D.includes("previous")?"block":"none"):o.footer.style.display="none";const w=p||a("disableButtons")||[];w!=null&&w.includes("next")&&(o.nextButton.disabled=!0,o.nextButton.classList.add("driver-popover-btn-disabled")),w!=null&&w.includes("previous")&&(o.previousButton.disabled=!0,o.previousButton.classList.add("driver-popover-btn-disabled")),w!=null&&w.includes("close")&&(o.closeButton.disabled=!0,o.closeButton.classList.add("driver-popover-btn-disabled"));const I=o.wrapper;I.style.display="block",I.style.left="",I.style.top="",I.style.bottom="",I.style.right="",I.id="driver-popover-content",I.setAttribute("role","dialog"),I.setAttribute("aria-labelledby","driver-popover-title"),I.setAttribute("aria-describedby","driver-popover-description");const O=o.arrow;O.className="driver-popover-arrow";const z=((i=t.popover)==null?void 0:i.popoverClass)||a("popoverClass")||"";I.className=`driver-popover ${z}`.trim(),re(o.wrapper,F=>{var V,G,q;const W=F.target,Y=((V=t.popover)==null?void 0:V.onNextClick)||a("onNextClick"),K=((G=t.popover)==null?void 0:G.onPrevClick)||a("onPrevClick"),J=((q=t.popover)==null?void 0:q.onCloseClick)||a("onCloseClick");if(W.classList.contains("driver-popover-next-btn"))return Y?Y(e,t,{config:a(),state:l()}):L("nextClick");if(W.classList.contains("driver-popover-prev-btn"))return K?K(e,t,{config:a(),state:l()}):L("prevClick");if(W.classList.contains("driver-popover-close-btn"))return J?J(e,t,{config:a(),state:l()}):L("closeClick")},F=>!(o!=null&&o.description.contains(F))&&!(o!=null&&o.title.contains(F))&&typeof F.className=="string"&&F.className.includes("driver-popover")),C("popover",o);const N=((s=t.popover)==null?void 0:s.onPopoverRender)||a("onPopoverRender");N&&N(o,{config:a(),state:l()}),ae(e,t),n(I);const M=e.classList.contains("driver-dummy-element"),H=r([I,...M?[]:[e]]);H.length>0&&H[0].focus()}function ie(){const e=l("popover");if(!(e!=null&&e.wrapper))return;const t=e.wrapper.getBoundingClientRect(),i=a("stagePadding")||0,s=a("popoverOffset")||0;return{width:t.width+i+s,height:t.height+i+s,realWidth:t.width,realHeight:t.height}}function ne(e,t){const{elementDimensions:i,popoverDimensions:s,popoverPadding:o,popoverArrowDimensions:h}=t;return e==="start"?Math.max(Math.min(i.top-o,window.innerHeight-s.realHeight-h.width),h.width):e==="end"?Math.max(Math.min(i.top-s?.realHeight+i.height+o,window.innerHeight-s?.realHeight-h.width),h.width):e==="center"?Math.max(Math.min(i.top+i.height/2-s?.realHeight/2,window.innerHeight-s?.realHeight-h.width),h.width):0}function se(e,t){const{elementDimensions:i,popoverDimensions:s,popoverPadding:o,popoverArrowDimensions:h}=t;return e==="start"?Math.max(Math.min(i.left-o,window.innerWidth-s.realWidth-h.width),h.width):e==="end"?Math.max(Math.min(i.left-s?.realWidth+i.width+o,window.innerWidth-s?.realWidth-h.width),h.width):e==="center"?Math.max(Math.min(i.left+i.width/2-s?.realWidth/2,window.innerWidth-s?.realWidth-h.width),h.width):0}function ae(e,t){const i=l("popover");if(!i)return;const{align:s="start",side:o="left"}=t?.popover||{},h=s,T=e.id==="driver-dummy-element"?"over":o,x=a("stagePadding")||0,p=ie(),S=i.arrow.getBoundingClientRect(),d=e.getBoundingClientRect(),g=d.top-p.height;let f=g>=0;const D=window.innerHeight-(d.bottom+p.height);let B=D>=0;const E=d.left-p.width;let w=E>=0;const I=window.innerWidth-(d.right+p.width);let O=I>=0;const z=!f&&!B&&!w&&!O;let N=T;if(T==="top"&&f?O=w=B=!1:T==="bottom"&&B?O=w=f=!1:T==="left"&&w?O=f=B=!1:T==="right"&&O&&(w=f=B=!1),T==="over"){const M=window.innerWidth/2-p.realWidth/2,H=window.innerHeight/2-p.realHeight/2;i.wrapper.style.left=`${M}px`,i.wrapper.style.right="auto",i.wrapper.style.top=`${H}px`,i.wrapper.style.bottom="auto"}else if(z){const M=window.innerWidth/2-p?.realWidth/2,H=10;i.wrapper.style.left=`${M}px`,i.wrapper.style.right="auto",i.wrapper.style.bottom=`${H}px`,i.wrapper.style.top="auto"}else if(w){const M=Math.min(E,window.innerWidth-p?.realWidth-S.width),H=ne(h,{elementDimensions:d,popoverDimensions:p,popoverPadding:x,popoverArrowDimensions:S});i.wrapper.style.left=`${M}px`,i.wrapper.style.top=`${H}px`,i.wrapper.style.bottom="auto",i.wrapper.style.right="auto",N="left"}else if(O){const M=Math.min(I,window.innerWidth-p?.realWidth-S.width),H=ne(h,{elementDimensions:d,popoverDimensions:p,popoverPadding:x,popoverArrowDimensions:S});i.wrapper.style.right=`${M}px`,i.wrapper.style.top=`${H}px`,i.wrapper.style.bottom="auto",i.wrapper.style.left="auto",N="right"}else if(f){const M=Math.min(g,window.innerHeight-p.realHeight-S.width);let H=se(h,{elementDimensions:d,popoverDimensions:p,popoverPadding:x,popoverArrowDimensions:S});i.wrapper.style.top=`${M}px`,i.wrapper.style.left=`${H}px`,i.wrapper.style.bottom="auto",i.wrapper.style.right="auto",N="top"}else if(B){const M=Math.min(D,window.innerHeight-p?.realHeight-S.width);let H=se(h,{elementDimensions:d,popoverDimensions:p,popoverPadding:x,popoverArrowDimensions:S});i.wrapper.style.left=`${H}px`,i.wrapper.style.bottom=`${M}px`,i.wrapper.style.top="auto",i.wrapper.style.right="auto",N="bottom"}z?i.arrow.classList.add("driver-popover-arrow-none"):be(h,N,e)}function be(e,t,i){const s=l("popover");if(!s)return;const o=i.getBoundingClientRect(),h=ie(),T=s.arrow,x=h.width,p=window.innerWidth,S=o.width,d=o.left,g=h.height,f=window.innerHeight,D=o.top,B=o.height;T.className="driver-popover-arrow";let E=t,w=e;t==="top"?(d+S<=0?(E="right",w="end"):d+S-x<=0&&(E="top",w="start"),d>=p?(E="left",w="end"):d+x>=p&&(E="top",w="end")):t==="bottom"?(d+S<=0?(E="right",w="start"):d+S-x<=0&&(E="bottom",w="start"),d>=p?(E="left",w="start"):d+x>=p&&(E="bottom",w="end")):t==="left"?(D+B<=0?(E="bottom",w="end"):D+B-g<=0&&(E="left",w="start"),D>=f?(E="top",w="end"):D+g>=f&&(E="left",w="end")):t==="right"&&(D+B<=0?(E="bottom",w="start"):D+B-g<=0&&(E="right",w="start"),D>=f?(E="top",w="start"):D+g>=f&&(E="right",w="end")),E?(T.classList.add(`driver-popover-arrow-side-${E}`),T.classList.add(`driver-popover-arrow-align-${w}`)):T.classList.add("driver-popover-arrow-none")}function Se(){const e=document.createElement("div");e.classList.add("driver-popover");const t=document.createElement("div");t.classList.add("driver-popover-arrow");const i=document.createElement("header");i.id="driver-popover-title",i.classList.add("driver-popover-title"),i.style.display="none",i.innerText="Popover Title";const s=document.createElement("div");s.id="driver-popover-description",s.classList.add("driver-popover-description"),s.style.display="none",s.innerText="Popover description is here";const o=document.createElement("button");o.type="button",o.classList.add("driver-popover-close-btn"),o.setAttribute("aria-label","Close"),o.innerHTML="×";const h=document.createElement("footer");h.classList.add("driver-popover-footer");const T=document.createElement("span");T.classList.add("driver-popover-progress-text"),T.innerText="";const x=document.createElement("span");x.classList.add("driver-popover-navigation-btns");const p=document.createElement("button");p.type="button",p.classList.add("driver-popover-prev-btn"),p.innerHTML="← Previous";const S=document.createElement("button");return S.type="button",S.classList.add("driver-popover-next-btn"),S.innerHTML="Next →",x.appendChild(p),x.appendChild(S),h.appendChild(T),h.appendChild(x),e.appendChild(o),e.appendChild(t),e.appendChild(i),e.appendChild(s),e.appendChild(h),{wrapper:e,arrow:t,title:i,description:s,footer:h,previousButton:p,nextButton:S,closeButton:o,footerButtons:x,progress:T}}function Te(){var e;const t=l("popover");t&&((e=t.wrapper.parentElement)==null||e.removeChild(t.wrapper))}const De="";function Ce(e={}){_(e);function t(){a("allowClose")&&S()}function i(){const d=l("activeIndex"),g=a("steps")||[];if(typeof d>"u")return;const f=d+1;g[f]?p(f):S()}function s(){const d=l("activeIndex"),g=a("steps")||[];if(typeof d>"u")return;const f=d-1;g[f]?p(f):S()}function o(d){(a("steps")||[])[d]?p(d):S()}function h(){var d;if(l("__transitionCallback"))return;const g=l("activeIndex"),f=l("__activeStep"),D=l("__activeElement");if(typeof g>"u"||typeof f>"u"||typeof l("activeIndex")>"u")return;const B=((d=f.popover)==null?void 0:d.onPrevClick)||a("onPrevClick");if(B)return B(D,f,{config:a(),state:l()});s()}function T(){var d;if(l("__transitionCallback"))return;const g=l("activeIndex"),f=l("__activeStep"),D=l("__activeElement");if(typeof g>"u"||typeof f>"u")return;const B=((d=f.popover)==null?void 0:d.onNextClick)||a("onNextClick");if(B)return B(D,f,{config:a(),state:l()});i()}function x(){l("isInitialized")||(C("isInitialized",!0),document.body.classList.add("driver-active",a("animate")?"driver-fade":"driver-simple"),fe(),$("overlayClick",t),$("escapePress",t),$("arrowLeftPress",h),$("arrowRightPress",T))}function p(d=0){var g,f,D,B,E,w,I,O;const z=a("steps");if(!z){console.error("No steps to drive through"),S();return}if(!z[d]){S();return}C("__activeOnDestroyed",document.activeElement),C("activeIndex",d);const N=z[d],M=z[d+1],H=z[d-1],F=((g=N.popover)==null?void 0:g.doneBtnText)||a("doneBtnText")||"Done",V=a("allowClose"),G=typeof((f=N.popover)==null?void 0:f.showProgress)<"u"?(D=N.popover)==null?void 0:D.showProgress:a("showProgress"),q=(((B=N.popover)==null?void 0:B.progressText)||a("progressText")||"{{current}} of {{total}}").replace("{{current}}",`${d+1}`).replace("{{total}}",`${z.length}`),W=((E=N.popover)==null?void 0:E.showButtons)||a("showButtons"),Y=["next","previous",...V?["close"]:[]].filter(xe=>!(W!=null&&W.length)||W.includes(xe)),K=((w=N.popover)==null?void 0:w.onNextClick)||a("onNextClick"),J=((I=N.popover)==null?void 0:I.onPrevClick)||a("onPrevClick"),ke=((O=N.popover)==null?void 0:O.onCloseClick)||a("onCloseClick");ee({...N,popover:{showButtons:Y,nextBtnText:M?void 0:F,disableButtons:[...H?[]:["previous"]],showProgress:G,progressText:q,onNextClick:K||(()=>{M?p(d+1):S()}),onPrevClick:J||(()=>{p(d-1)}),onCloseClick:ke||(()=>{S()}),...N?.popover||{}}})}function S(d=!0){const g=l("__activeElement"),f=l("__activeStep"),D=l("__activeOnDestroyed"),B=a("onDestroyStarted");if(d&&B){const I=!g||g?.id==="driver-dummy-element";B(I?void 0:g,f,{config:a(),state:l()});return}const E=f?.onDeselected||a("onDeselected"),w=a("onDestroyed");if(document.body.classList.remove("driver-active","driver-fade","driver-simple"),ye(),Te(),ge(),ce(),R(),u(),g&&f){const I=g.id==="driver-dummy-element";E&&E(I?void 0:g,f,{config:a(),state:l()}),w&&w(I?void 0:g,f,{config:a(),state:l()})}D&&D.focus()}return{isActive:()=>l("isInitialized")||!1,refresh:U,drive:(d=0)=>{x(),p(d)},setConfig:_,setSteps:d=>{u(),_({...a(),steps:d})},getConfig:a,getState:l,getActiveIndex:()=>l("activeIndex"),isFirstStep:()=>l("activeIndex")===0,isLastStep:()=>{const d=a("steps")||[],g=l("activeIndex");return g!==void 0&&g===d.length-1},getActiveStep:()=>l("activeStep"),getActiveElement:()=>l("activeElement"),getPreviousElement:()=>l("previousElement"),getPreviousStep:()=>l("previousStep"),moveNext:i,movePrevious:s,moveTo:o,hasNextStep:()=>{const d=a("steps")||[],g=l("activeIndex");return g!==void 0&&d[g+1]},hasPreviousStep:()=>{const d=a("steps")||[],g=l("activeIndex");return g!==void 0&&d[g-1]},highlight:d=>{x(),ee({...d,popover:d.popover?{showButtons:[],showProgress:!1,progressText:"",...d.popover}:void 0})},destroy:()=>{S(!1)}}}return y.driver=Ce,Object.defineProperty(y,Symbol.toStringTag,{value:"Module"}),y})({}),(function(y){"use strict";const P=new ErrorHandler,_=(...c)=>{y.DASHCADDY_DEBUG&&console.log(...c)};class a{constructor(r="dashcaddy_onboarding"){this.storageKey=r,this.storageVersion="1.0",this.installOnboardingCompleted=typeof SITE<"u"&&SITE.onboardingCompleted===!0,this._initializeStorage(),this._updateLastVisit()}_initializeStorage(){const r=this._getStorage();if(!r||r.version!==this.storageVersion){const n={version:this.storageVersion,tourCompleted:!1,completedTooltips:[],currentStep:0,completionTimestamp:null,dnsSetupDeferred:!1,lastVisit:new Date().toISOString()};this._setStorage(n)}}_getStorage(){try{const r=localStorage.getItem(this.storageKey);return r?JSON.parse(r):null}catch(r){return P.logError("[ProgressTracker] Read Storage",r,{function:"_getStorage"}),null}}_setStorage(r){try{localStorage.setItem(this.storageKey,JSON.stringify(r))}catch(n){P.logError("[ProgressTracker] Write Storage",n,{function:"_setStorage"}),this._handleStorageError(n)}}_handleStorageError(r){try{sessionStorage.setItem(this.storageKey,JSON.stringify(this._getStorage())),console.warn("[ProgressTracker] Falling back to session storage")}catch(n){P.logError("[ProgressTracker] Session Storage Unavailable",n,{function:"_handleStorageError"})}}_updateLastVisit(){const r=this._getStorage();r&&(r.lastVisit=new Date().toISOString(),this._setStorage(r))}isTooltipCompleted(r){const n=this._getStorage();return n?n.completedTooltips.includes(r):!1}markTooltipCompleted(r){const n=this._getStorage();n&&(n.completedTooltips.includes(r)||(n.completedTooltips.push(r),n.tooltipTimestamps||(n.tooltipTimestamps={}),n.tooltipTimestamps[r]=new Date().toISOString(),this._setStorage(n)))}isTourCompleted(){const r=this._getStorage();return r?r.tourCompleted===!0:!1}isInstallOnboardingCompleted(){return this.installOnboardingCompleted===!0}async markInstallOnboardingCompleted(){if(!this.installOnboardingCompleted){this.installOnboardingCompleted=!0,typeof SITE<"u"&&(SITE.onboardingCompleted=!0);try{await secureFetch("/api/v1/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({onboardingCompleted:!0})})}catch(r){P.logError("[ProgressTracker] Persist Install Onboarding",r,{function:"markInstallOnboardingCompleted"})}}}markTourCompleted(){const r=this._getStorage();r&&(r.tourCompleted=!0,r.completionTimestamp=new Date().toISOString(),this._setStorage(r))}getCurrentStep(){const r=this._getStorage();return r&&r.currentStep||0}setCurrentStep(r){const n=this._getStorage();n&&(n.currentStep=r,this._setStorage(n))}resetProgress(){const r={version:this.storageVersion,tourCompleted:!1,completedTooltips:[],currentStep:0,completionTimestamp:null,dnsSetupDeferred:!1,lastVisit:new Date().toISOString()};this._setStorage(r)}getCompletionTimestamp(){const r=this._getStorage();return!r||!r.completionTimestamp?null:new Date(r.completionTimestamp)}isDnsSetupDeferred(){const r=this._getStorage();return r?r.dnsSetupDeferred===!0:!1}markDnsSetupDeferred(){const r=this._getStorage();r&&(r.dnsSetupDeferred=!0,this._setStorage(r))}getTooltipTimestamp(r){const n=this._getStorage();return!n||!n.tooltipTimestamps||!n.tooltipTimestamps[r]?null:new Date(n.tooltipTimestamps[r])}getCompletedTooltips(){const r=this._getStorage();return r?r.completedTooltips||[]:[]}getLastVisit(){const r=this._getStorage();return!r||!r.lastVisit?null:new Date(r.lastVisit)}}y.ProgressTracker=a,_("[ProgressTracker] Module loaded")})(window),(function(y){"use strict";const P=new ErrorHandler,_=(...r)=>{y.DASHCADDY_DEBUG&&console.log(...r)},a={dark:{backgroundColor:"var(--card-base)",textColor:"var(--fg)",primaryColor:"var(--accent)",overlayColor:"rgba(0, 0, 0, 0.7)",borderColor:"var(--border)",highlightColor:"var(--accent)",fontFamily:"'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif"},light:{backgroundColor:"var(--card-base)",textColor:"var(--fg)",primaryColor:"var(--accent-strong)",overlayColor:"rgba(0, 0, 0, 0.5)",borderColor:"var(--border)",highlightColor:"var(--accent-strong)",fontFamily:"'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif"},blue:{backgroundColor:"var(--card-base)",textColor:"var(--fg)",primaryColor:"var(--accent)",overlayColor:"rgba(25, 8, 172, 0.7)",borderColor:"var(--border)",highlightColor:"var(--accent)",fontFamily:"'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif"},nord:{backgroundColor:"var(--card-base)",textColor:"var(--fg)",primaryColor:"var(--accent)",overlayColor:"rgba(46, 52, 64, 0.7)",borderColor:"var(--border)",highlightColor:"var(--accent)",fontFamily:"'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif"},dracula:{backgroundColor:"var(--card-base)",textColor:"var(--fg)",primaryColor:"var(--accent)",overlayColor:"rgba(40, 42, 54, 0.7)",borderColor:"var(--border)",highlightColor:"var(--accent)",fontFamily:"'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif"},"solarized-dark":{backgroundColor:"var(--card-base)",textColor:"var(--fg)",primaryColor:"var(--accent)",overlayColor:"rgba(0, 43, 54, 0.7)",borderColor:"var(--border)",highlightColor:"var(--accent)",fontFamily:"'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif"},"solarized-light":{backgroundColor:"var(--card-base)",textColor:"var(--fg)",primaryColor:"var(--accent)",overlayColor:"rgba(253, 246, 227, 0.7)",borderColor:"var(--border)",highlightColor:"var(--accent)",fontFamily:"'Sami Grotesk', 'Inter', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif"}};class c{constructor(){this.currentTheme=this.getCurrentTheme(),this.themeChangeCallbacks=[],this._setupThemeChangeListener()}getCurrentTheme(){const n=document.documentElement,v=Array.from(n.classList);return(y.THEMES||[]).filter(m=>m!=="dark").find(m=>v.includes(m))||"dark"}getDriverTheme(){const n=this.getCurrentTheme(),v=a[n]||a.dark,A={};for(const[k,m]of Object.entries(v))if(typeof m=="string"&&m.startsWith("var(")){const C=m.match(/var\((--[^)]+)\)/)?.[1];if(C){const l=getComputedStyle(document.documentElement).getPropertyValue(C).trim();A[k]=l||m}else A[k]=m}else A[k]=m;return A}onThemeChange(n){typeof n=="function"&&this.themeChangeCallbacks.push(n)}_setupThemeChangeListener(){const n=document.documentElement;new MutationObserver(A=>{A.forEach(k=>{if(k.type==="attributes"&&k.attributeName==="class"){const m=this.getCurrentTheme();if(m!==this.currentTheme){const C=this.currentTheme;this.currentTheme=m,this._notifyThemeChange(m,C)}}})}).observe(n,{attributes:!0,attributeFilter:["class"]}),_("[ThemeAdapter] Theme change listener initialized")}_notifyThemeChange(n,v){_(`[ThemeAdapter] Theme changed: ${v} \u2192 ${n}`),this.themeChangeCallbacks.forEach(A=>{try{A(n,v)}catch(k){P.logError("[ThemeAdapter] Theme Change Callback",k,{function:"_notifyThemeChange"})}})}applyTheme(n){if(!n){console.warn("[ThemeAdapter] No driver instance provided");return}const v=this.getDriverTheme();this._injectDriverStyles(v),_("[ThemeAdapter] Theme applied to driver:",this.currentTheme)}_injectDriverStyles(n){const v=document.getElementById("driver-theme-styles");v&&v.remove();const A=document.createElement("style");A.id="driver-theme-styles",A.textContent=` .driver-popover { background: ${n.backgroundColor} !important; color: ${n.textColor} !important; @@ -154,7 +154,7 @@ ${b}`)}}y.TooltipValidation={validateTooltipDefinition:a,validateTooltipDefiniti

Activate in Admin → License

You can restart this tour anytime from Admin → Help Tour.

- `,position:"bottom",align:"start",showButtons:["previous","close"],showProgress:!0},priority:13}];function A(){return v}function k(u){return v.find(b=>b.id===u)||null}function m(){return v.filter(u=>{if(u.condition&&typeof u.condition=="function")try{return u.condition()}catch(b){return P.logError("[TooltipDefinitions] Condition Eval",b,{function:"evaluateCondition",tooltipId:u.id}),!1}return!0})}function C(){return m().sort((b,$)=>{const L=b.priority||999,R=$.priority||999;return L-R})}function l(){return m().filter(b=>b.isNewFeature===!0).sort((b,$)=>{const L=b.priority||999,R=$.priority||999;return L-R})}y.TooltipDefinitions={TOOLTIP_DEFINITIONS:v,getTooltipDefinitions:A,getTooltipById:k,getActiveTooltips:m,getSortedTooltips:C,getNewFeatureTooltips:l},_("[TooltipDefinitions] Definitions loaded:",v.length,"tooltips")})(window),(function(y){"use strict";const P=(...a)=>{y.DASHCADDY_DEBUG&&console.log(...a)};class _{constructor(c){this.progressTracker=c,this.modal=null,this.onTemplateSelected=null,P("[DnsTemplateSelector] Module loaded")}getDnsTemplates(){return[{id:"technitium",name:"Technitium DNS Server",description:"Modern DNS server with web UI for managing private zones",icon:"\u{1F310}",difficulty:"Easy",features:["Web-based management interface","Private zone management for .sami domain","DHCP server integration","DNS-over-HTTPS and DNS-over-TLS support"],recommended:!0},{id:"bind9",name:"BIND9 DNS Server",description:"Industry-standard DNS server - powerful and flexible",icon:"\u{1F527}",difficulty:"Advanced",features:["Industry standard DNS server","Full RFC compliance","Advanced zone management","DNSSEC support"],recommended:!1},{id:"pihole",name:"Pi-hole",description:"Network-wide ad blocker with DNS capabilities",icon:"\u{1F6E1}\uFE0F",difficulty:"Intermediate",features:["Ad blocking at DNS level","Web interface for management","DHCP server included","Query logging and statistics"],recommended:!1},{id:"powerdns",name:"PowerDNS",description:"High-performance DNS server with SQL backend",icon:"\u26A1",difficulty:"Intermediate",features:["SQL database backend","RESTful API for automation","Geographic load balancing","DNSSEC support"],recommended:!1},{id:"coredns",name:"CoreDNS",description:"Cloud-native DNS server - lightweight and flexible",icon:"\u2601\uFE0F",difficulty:"Intermediate",features:["Plugin-based architecture","Kubernetes-native","Lightweight and fast","Prometheus metrics"],recommended:!1}]}showTemplateSelector(){this.modal||this.createModal(),this.populateTemplates(),this.modal.style.display="flex",document.body.style.overflow="hidden"}createModal(){const c=document.createElement("div");c.id="dns-template-modal",c.className="dns-template-modal",c.innerHTML=` + `,position:"bottom",align:"start",showButtons:["previous","close"],showProgress:!0},priority:13}];function A(){return v}function k(u){return v.find(b=>b.id===u)||null}function m(){return v.filter(u=>{if(u.condition&&typeof u.condition=="function")try{return u.condition()}catch(b){return P.logError("[TooltipDefinitions] Condition Eval",b,{function:"evaluateCondition",tooltipId:u.id}),!1}return!0})}function C(){return m().sort((b,$)=>{const L=b.priority||999,R=$.priority||999;return L-R})}function l(){return m().filter(b=>b.isNewFeature===!0).sort((b,$)=>{const L=b.priority||999,R=$.priority||999;return L-R})}y.TooltipDefinitions={TOOLTIP_DEFINITIONS:v,getTooltipDefinitions:A,getTooltipById:k,getActiveTooltips:m,getSortedTooltips:C,getNewFeatureTooltips:l},_("[TooltipDefinitions] Definitions loaded:",v.length,"tooltips")})(window),(function(y){"use strict";const P=(...a)=>{y.DASHCADDY_DEBUG&&console.log(...a)};class _{constructor(c){this.progressTracker=c,this.modal=null,this.onTemplateSelected=null,P("[DnsTemplateSelector] Module loaded")}getDnsTemplates(){return[{id:"technitium",name:"Technitium DNS Server",description:"Modern DNS server with web UI for managing private zones",icon:"\u{1F310}",difficulty:"Easy",features:["Web-based management interface","Private zone management for .sami domain","DHCP server integration","DNS-over-HTTPS and DNS-over-TLS support"],recommended:!0},{id:"bind9",name:"BIND9 DNS Server",description:"Industry-standard DNS server - powerful and flexible",icon:"\u{1F527}",difficulty:"Advanced",features:["Industry standard DNS server","Full RFC compliance","Advanced zone management","DNSSEC support"],recommended:!1},{id:"pihole",name:"Pi-hole",description:"Network-wide ad blocker with DNS capabilities",icon:"\u{1F6E1}\uFE0F",difficulty:"Intermediate",features:["Ad blocking at DNS level","Web interface for management","DHCP server included","Query logging and statistics"],recommended:!1},{id:"powerdns",name:"PowerDNS",description:"High-performance DNS server with SQL backend",icon:"\u26A1",difficulty:"Intermediate",features:["SQL database backend","RESTful API for automation","Geographic load balancing","DNSSEC support"],recommended:!1},{id:"coredns",name:"CoreDNS",description:"Cloud-native DNS server - lightweight and flexible",icon:"\u2601\uFE0F",difficulty:"Intermediate",features:["Plugin-based architecture","Kubernetes-native","Lightweight and fast","Prometheus metrics"],recommended:!1},{id:"cloudflare",name:"Cloudflare DNS",description:"Managed DNS with API access \u2014 no self-hosting needed",icon:"\u{1F536}",difficulty:"Easy",features:["Fully managed, no server needed","API for automated record management","Global anycast network","Free tier available"],recommended:!1,providerId:"cloudflare"},{id:"external",name:"External / Manual DNS",description:"Use your own DNS provider (cPanel, Route53, etc.)",icon:"\u{1F517}",difficulty:"Easy",features:["Works with any DNS provider","DashCaddy shows you what records to create","Propagation checking still works","No API credentials needed"],recommended:!1,providerId:"manual"}]}showTemplateSelector(){this.modal||this.createModal(),this.populateTemplates(),this.modal.style.display="flex",document.body.style.overflow="hidden"}createModal(){const c=document.createElement("div");c.id="dns-template-modal",c.className="dns-template-modal",c.innerHTML=`

\u{1F310} Choose a DNS Server

diff --git a/status/sw.js b/status/sw.js index 304bebc..987b8f2 100644 --- a/status/sw.js +++ b/status/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'dashcaddy-shell-8ef9c82616'; +const CACHE = 'dashcaddy-shell-594ec75648'; const PRECACHE = [ '/', '/index.html',