Files
dashcaddy/status/index.html
T

991 lines
51 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>DashCaddy</title>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'sha256-dtGmAPWjcgykNC2GM60HzlzMwiAqHarVnnPf8NO0XrA='; style-src 'self' 'unsafe-inline'; img-src 'self' https://cdn.jsdelivr.net data:; connect-src 'self' https://api.open-meteo.com https://geocoding-api.open-meteo.com; font-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'">
<link rel="icon" href="/assets/dashcaddy-favicon.ico" sizes="any">
<link rel="icon" type="image/png" sizes="192x192" href="/assets/icon-192.png">
<link rel="icon" type="image/png" sizes="512x512" href="/assets/icon-512.png">
<link rel="apple-touch-icon" href="/assets/apple-touch-icon.png">
<link rel="manifest" href="/assets/site.webmanifest">
<link rel="stylesheet" href="/assets/fonts.css">
<link rel="preload" href="/assets/icons.svg" as="image" type="image/svg+xml">
<link rel="preload" href="/assets/fonts/sami-grotesk/SamiGrotesk-Regular.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/assets/fonts/sami-grotesk/SamiGrotesk-Medium.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/assets/fonts/sami-grotesk/SamiGrotesk-Bold.woff2" as="font" type="font/woff2" crossorigin>
<meta name="theme-color" content="#0e1116">
<link rel="stylesheet" href="/css/themes.css">
<link rel="stylesheet" href="/css/dashboard.css">
<link rel="stylesheet" href="/css/xterm.css">
</head>
<body>
<!-- TOTP Login Overlay -->
<div id="totp-overlay">
<div class="totp-card">
<img class="totp-logo totp-logo-dark" src="/assets/dashcaddy-logo-dark.png" alt="DashCaddy" onerror="this.style.display='none'">
<img class="totp-logo totp-logo-light" src="/assets/dashcaddy-logo-light.png" alt="DashCaddy" onerror="this.style.display='none'">
<p class="subtitle">Enter your 6-digit authentication code</p>
<div class="totp-digits" id="totp-digits">
<input type="text" maxlength="1" inputmode="numeric" pattern="[0-9]" autocomplete="one-time-code">
<input type="text" maxlength="1" inputmode="numeric" pattern="[0-9]">
<input type="text" maxlength="1" inputmode="numeric" pattern="[0-9]">
<input type="text" maxlength="1" inputmode="numeric" pattern="[0-9]">
<input type="text" maxlength="1" inputmode="numeric" pattern="[0-9]">
<input type="text" maxlength="1" inputmode="numeric" pattern="[0-9]">
</div>
<div class="totp-error" id="totp-error"></div>
<div class="totp-recovery-link" id="totp-recovery-link" style="display: none;">
<a href="#" id="totp-show-recovery">Lost access? Recover with saved Base32 key →</a>
</div>
</div>
</div>
<!-- TOTP Recovery Panel (hidden by default, shown via "Lost access?" link on overlay) -->
<div id="totp-recovery-panel" class="weather-modal" style="display: none;">
<div class="weather-modal-content" style="min-width: 420px; max-width: 540px;">
<h3 style="margin: 0 0 12px; font-size: 1.1rem;">Recover TOTP Access</h3>
<div id="totp-recovery-status" style="margin-bottom: 12px; padding: 10px 14px; border-radius: 6px; border: 1px solid var(--border); font-size: 0.85rem; line-height: 1.4;"></div>
<!-- Path A: Paste saved Base32 secret -->
<div id="totp-recovery-import">
<p style="font-size: 0.85rem; color: var(--muted); margin: 0 0 8px;">
Paste the Base32 secret you saved when you first set up TOTP (e.g. <code>JBSWY3DPEHPK3PXP</code>).
If you don't have it, you'll need to SSH into the server to rotate the encryption key.
</p>
<div style="display: flex; gap: 8px; margin-top: 8px;">
<input type="text" id="totp-recovery-secret" placeholder="Paste your Base32 key"
autocomplete="off" spellcheck="false"
style="flex: 1; padding: 10px; background: var(--bg); color: var(--fg); border: 1px solid var(--border); border-radius: 6px; font-size: 0.9rem; font-family: monospace; letter-spacing: 1px; text-transform: uppercase;" />
<button id="totp-recovery-submit"
style="padding: 10px 16px; background: var(--card-base); color: var(--fg); border: 1px solid var(--border); border-radius: 6px; cursor: pointer; font-weight: 600; font-size: 0.85rem; white-space: nowrap;">
Restore
</button>
</div>
<div id="totp-recovery-error" style="color: var(--bad-fg, #ff9aa3); font-size: 0.8rem; min-height: 1.2em; margin-top: 6px;"></div>
</div>
<!-- Path B: After successful import, ask user to verify with code -->
<div id="totp-recovery-verify" style="display: none;">
<p style="font-size: 0.85rem; color: var(--muted); margin: 0 0 8px;">
Secret accepted. Add it to your authenticator app and enter a 6-digit code to confirm.
</p>
<div style="display: flex; gap: 8px; margin-top: 8px;">
<input type="text" id="totp-recovery-code" maxlength="6" inputmode="numeric" pattern="[0-9]{6}"
placeholder="000000" autocomplete="one-time-code"
style="flex: 1; padding: 10px; text-align: center; font-size: 1.2rem; font-family: 'Sami Grotesk', monospace; letter-spacing: 4px; background: var(--bg); color: var(--fg); border: 1px solid var(--border); border-radius: 6px;" />
<button id="totp-recovery-confirm"
style="padding: 10px 16px; background: var(--card-base); color: var(--fg); border: 1px solid var(--border); border-radius: 6px; cursor: pointer; font-weight: 600; font-size: 0.85rem; white-space: nowrap;">
Confirm
</button>
</div>
<div id="totp-recovery-confirm-error" style="color: var(--bad-fg, #ff9aa3); font-size: 0.8rem; min-height: 1.2em; margin-top: 6px;"></div>
</div>
<div style="margin-top: 14px; text-align: right;">
<button id="totp-recovery-close"
style="padding: 8px 18px; background: transparent; color: var(--muted); border: 1px solid var(--border); border-radius: 6px; cursor: pointer; font-size: 0.85rem;">
Cancel
</button>
</div>
</div>
</div>
<!-- Top bar with logo and weather on same line, tools below -->
<div class="bar">
<div class="top-row">
<!-- Logo and Weather grouped together -->
<div class="brand-weather-group">
<div id="brand" title="Click to customize logo and position">
<img class="brand-logo-top brand-logo-dark" src="/assets/dashcaddy-logo-dark.png" alt="DashCaddy" loading="eager" decoding="sync" />
<img class="brand-logo-top brand-logo-light" src="/assets/dashcaddy-logo-light.png" alt="DashCaddy" loading="eager" decoding="sync" />
</div>
<!-- Weather widget directly next to logo -->
<div class="weather-widget-container">
<div id="weather-widget" class="weather-widget">
<div class="weather-content">
<div class="weather-icon">🌤️</div>
<div class="weather-info">
<div class="weather-location">--</div>
<div class="weather-temp">--°</div>
<div class="weather-condition">--</div>
<div class="weather-wind">--</div>
<button id="weather-settings" class="weather-settings-btn-bottom" aria-label="Weather settings">⚙️</button>
</div>
</div>
</div>
</div>
<!-- Digital Clock widget -->
<div class="clock-widget-container">
<div id="clock-widget" class="clock-widget">
<div id="clock-render"></div>
<button id="clock-settings" class="clock-settings-btn" aria-label="Clock settings">⚙️</button>
</div>
</div>
</div>
<!-- License status + Reload Caddy Button - top right corner -->
<div class="reload-caddy-container">
<div class="reload-caddy-main">
<div class="theme-toggle-group">
<button id="theme" class="theme-toggle-btn" aria-label="Cycle theme" title="Click to cycle themes">
<span id="theme-icon">🎨</span> <span id="theme-label">Dark</span>
</button>
<button id="theme-customize-btn" class="theme-customize-link" title="Customize theme colors">Customize Theme</button>
</div>
<button id="reload-caddy-top" aria-label="Reload Caddy configuration" style="padding: 10px 20px; font-size: 0.95rem; font-weight: 600; background: linear-gradient(135deg, #3498db 0%, #2980b9 100%); border: none; border-radius: 6px; color: white; cursor: pointer; box-shadow: 0 2px 6px rgba(52, 152, 219, 0.3); transition: all 0.2s ease;">
🔄 Reload Caddy
</button>
<div class="license-version-row">
<div id="license-status-topbar" class="license-status-topbar free" title="Click to manage license">
<span id="license-topbar-icon">&#9734;</span>
<span id="license-topbar-text">FREE TIER</span>
<span id="license-topbar-time"></span>
</div>
<button id="license-topbar-version" class="dashboard-version dashboard-version-inline" type="button" title="View DashCaddy verification info" aria-label="View DashCaddy verification info"><span class="dashboard-version-bullet" id="license-topbar-version-bullet">·</span> <span id="license-topbar-version-text">Loading…</span></button>
</div>
</div>
</div>
</div>
<div id="version-info-modal" class="weather-modal">
<div class="weather-modal-content version-info-modal-content">
<h3>DashCaddy Verification Info</h3>
<p class="version-info-subtitle">Current version details and updater verification status.</p>
<div id="version-info-status" class="version-info-status">Loading…</div>
<div id="version-info-grid" class="version-info-grid" style="display:none;"></div>
<div id="version-info-history" class="version-info-history" style="display:none;"></div>
<div id="version-info-actions" class="weather-modal-buttons" style="margin-top: 16px; display:none; justify-content: space-between; gap: 10px;">
<button id="version-info-update">Update Now</button>
<button id="version-info-open-updates">Open Updates</button>
<button id="version-info-close">Close</button>
</div>
</div>
</div>
<!-- Tools row below logo and weather -->
<div class="tools-row">
<!-- Primary actions (always visible) -->
<div class="tools tools-primary">
<div class="chip muted" id="stamp">last check: —</div>
<button id="refresh"><span class="brand-spinner" aria-hidden="true"></span>Refresh</button>
</div>
<!-- Collapsible sections -->
<div class="tools-sections">
<div class="tools-section" data-section="status">
<button class="tools-section-header" aria-expanded="false">
<span class="tools-section-arrow">&#9656;</span>
<span class="tools-section-label">Status</span>
</button>
<div class="tools-section-items">
<button id="container-stats-btn" aria-label="Container resource monitor">📊 Monitor</button>
<button id="health-check-btn" aria-label="Service health dashboard">🏥 Health</button>
<button id="updates-btn" aria-label="Container updates">⬆️ Updates</button>
</div>
</div>
<div class="tools-section" data-section="tools">
<button class="tools-section-header" aria-expanded="false">
<span class="tools-section-arrow">&#9656;</span>
<span class="tools-section-label">Tools</span>
</button>
<div class="tools-section-items">
<button id="view-error-logs" aria-label="View error logs">📋 Error Logs</button>
<button id="view-container-logs" aria-label="View container logs">📜 Container Logs</button>
<button id="manage-notifications" aria-label="Manage notifications">🔔 Alerts</button>
<button id="audit-log-btn" aria-label="Audit log">📜 Audit</button>
<button id="security-center-btn" aria-label="Security Center">🛡️ Security</button>
<button id="log-insights-btn" aria-label="Log Insights">🔍 Insights</button>
<button onclick="openDiskSettings()" aria-label="Disk Safety">💾 Disk</button>
<button id="docker-resources-btn" aria-label="Docker resources">🐳 Docker</button>
</div>
</div>
<div class="tools-section" data-section="admin">
<button class="tools-section-header" aria-expanded="false">
<span class="tools-section-arrow">&#9656;</span>
<span class="tools-section-label">Admin</span>
</button>
<div class="tools-section-items">
<button id="manage-tokens" aria-label="Manage API tokens">🔑 Tokens</button>
<button id="backup-restore-btn" aria-label="Backup and restore">💾 Backup</button>
<button id="snapshot-btn" aria-label="Container snapshots">💾 Snapshots</button>
<button id="license-btn" aria-label="License management">🔑 License</button>
<button id="api-docs-btn" aria-label="API documentation">📖 API</button>
<button id="help-errors-btn" aria-label="Troubleshooting guide">❓ Help</button>
</div>
</div>
</div>
</div>
</div>
<!-- Top Anchor Row -->
<div class="top">
<!-- DNS cards rendered dynamically from config by renderDnsCards() -->
<div class="card" data-app="internet" data-status="off">
<span id="internet-dot" class="dot bad at-bl"></span>
<div class="row">
<div class="logo-wrap">
<svg viewBox="0 0 24 24" class="service-icon">
<circle cx="12" cy="12" r="10" fill="#27ae60"/>
<path d="M12 2c-2.5 4-2.5 14 0 18M12 2c2.5 4 2.5 14 0 18M2 12h20" stroke="white" stroke-width="1.5"/>
<path d="M4.5 7.5c3-1.5 6-1.5 9 0M4.5 16.5c3 1.5 6 1.5 9 0" stroke="white" stroke-width="1"/>
<path d="M10.5 7.5c3-1.5 6-1.5 9 0M10.5 16.5c3 1.5 6 1.5 9 0" stroke="white" stroke-width="1"/>
</svg>
</div>
<span class="name">Internet</span>
<span class="spacer"></span>
<span id="internet-pill" class="badge off">OFF</span>
</div>
<div class="response-row">
<span id="internet-time" class="response-time">--</span>
</div>
<div class="btn-row"><!-- No button for Internet --></div>
</div>
<div class="card" data-app="auth" data-status="off" id="auth-card"
title="Two-factor authentication (TOTP). On first setup, save the Base32 secret — it's the only way to recover if you ever lose your authenticator.">
<span id="auth-dot" class="dot bad at-bl"></span>
<div class="row">
<div class="logo-wrap">
<svg viewBox="0 0 24 24" class="service-icon">
<rect x="6" y="10" width="12" height="10" rx="2" fill="#8FD6FF"/>
<path d="M8 10V7a4 4 0 0 1 8 0v3" stroke="#e8ecf5" stroke-width="2" fill="none"/>
<circle cx="12" cy="15" r="1.5" fill="#0b0f1a"/>
<line x1="12" y1="16.5" x2="12" y2="18" stroke="#0b0f1a" stroke-width="1.5" stroke-linecap="round"/>
</svg>
</div>
<span class="name">Auth</span>
<span class="spacer"></span>
<span id="auth-pill" class="badge off">NO</span>
</div>
<div class="response-row">
<span id="auth-status-text" class="response-time" style="font-size: 0.7rem;">Not configured</span>
</div>
<div class="btn-row">
<button id="auth-settings-btn">Settings</button>
</div>
</div>
<div class="card" data-app="tailscale" data-status="off" id="tailscale-card"
title="Devices on your Tailscale network. Online = connected right now. Offline = known to the network but not currently connected.">
<span id="tailscale-dot" class="dot bad at-bl"></span>
<div class="row">
<div class="logo-wrap">
<svg viewBox="0 0 24 24" class="service-icon" aria-hidden="true">
<path d="M12 2L4 7v6c0 5 3.5 8.5 8 9 4.5-.5 8-4 8-9V7l-8-5z" fill="none" stroke="#7D8FE3" stroke-width="2" stroke-linejoin="round"/>
<circle cx="12" cy="10" r="2.5" fill="#7D8FE3"/>
<path d="M12 13v4M9 19h6" stroke="#7D8FE3" stroke-width="2" stroke-linecap="round"/>
</svg>
</div>
<span class="name">Tailscale</span>
<span class="spacer"></span>
<span id="tailscale-pill" class="badge off"></span>
</div>
<div class="response-row">
<span id="tailscale-summary" class="response-time" style="font-size: 0.7rem;">Loading…</span>
</div>
<div class="btn-row">
<button id="tailscale-refresh-btn" title="Refresh device list"></button>
</div>
</div>
<div class="card" data-app="ca" data-status="off">
<span id="dot-ca-grid" class="dot bad at-bl"></span>
<div class="row">
<div class="logo-wrap">
<span style="font-size: 28px; display: flex; align-items: center; justify-content: center; width: 100%; height: 100%;">🔐</span>
</div>
<span class="name">DashCA</span>
<span class="spacer"></span>
<span id="badge-ca" class="badge off">OFF</span>
</div>
<div class="response-row">
<span id="time-ca" class="response-time">--</span>
</div>
<div class="health-row" id="health-ca">
<span id="uptime-ca" class="uptime-chip">--</span>
<div class="uptime-mini-bar"><div class="fill" id="uptime-bar-ca" style="width: 0%"></div></div>
</div>
<div class="btn-row">
<button class="creds-btn" id="creds-btn-ca" title="Auto-login credentials">🔑</button>
<button class="options-btn" id="options-btn-ca" title="Edit service settings">⚙️</button>
<button class="delete-btn" id="delete-btn-ca" title="Delete this service">🗑️</button>
<button id="ca-open">Open</button>
</div>
</div>
</div>
<!-- Tailscale Device Panel (collapsible, populated by tailscale-devices.js) -->
<div id="tailscale-panel" style="display: none; margin-bottom: 16px; padding: 16px; background: var(--card-base); border: 1px solid var(--border); border-radius: var(--radius);">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px;">
<div style="display: flex; align-items: center; gap: 10px;">
<span style="font-size: 1.1rem; font-weight: 600;">Devices on Tailscale</span>
<span id="tailscale-panel-count" style="color: var(--muted); font-size: 0.85rem;"></span>
</div>
<div style="display: flex; gap: 8px;">
<span id="tailscale-tailnet" style="color: var(--muted); font-size: 0.8rem; align-self: center;"></span>
<button id="tailscale-panel-close" class="btn-sm" title="Close panel" style="padding: 4px 10px;"></button>
</div>
</div>
<div id="tailscale-device-list" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 10px;"></div>
</div>
<!-- Service Filter Bar -->
<div id="service-filter-bar" style="display: flex; gap: 12px; align-items: center; margin-bottom: 16px; padding: 12px 16px; background: var(--card-base); border: 1px solid var(--border); border-radius: var(--radius); flex-wrap: wrap;">
<input type="text" id="service-filter-search" placeholder="🔍 Filter services..." style="flex: 1; min-width: 180px; padding: 8px 12px; background: var(--bg); border: 1px solid var(--border); border-radius: 6px; color: var(--fg); font-size: 0.9rem;" />
<select id="service-filter-status" style="padding: 8px 12px; background: var(--bg); border: 1px solid var(--border); border-radius: 6px; color: var(--fg); font-size: 0.9rem;">
<option value="all">All Status</option>
<option value="on">🟢 Online</option>
<option value="off">🔴 Offline</option>
</select>
<select id="service-filter-category" style="padding: 8px 12px; background: var(--bg); border: 1px solid var(--border); border-radius: 6px; color: var(--fg); font-size: 0.9rem;">
<option value="all">All Categories</option>
</select>
<button id="batch-operations-btn" class="btn-sm" style="padding: 8px 12px;">☰ Batch Operations</button>
<span id="service-filter-count" style="color: var(--muted); font-size: 0.85rem; white-space: nowrap;"></span>
</div>
<!-- Batch Operations Bar (hidden by default) -->
<div id="batch-action-bar" style="display: none; margin-bottom: 16px; padding: 12px 16px; background: var(--accent-bg, #1a3a5c); border: 1px solid var(--accent, #3498db); border-radius: var(--radius);">
<div style="display: flex; align-items: center; gap: 12px; flex-wrap: wrap;">
<span id="batch-selected-count" style="font-weight: 500;">0 selected</span>
<button id="batch-start-btn" class="btn-sm" style="background: var(--ok-fg, #27ae60);">▶ Start All</button>
<button id="batch-stop-btn" class="btn-sm" style="background: var(--bad-fg, #e74c3c);">⬛ Stop All</button>
<button id="batch-restart-btn" class="btn-sm" style="background: #f39c12;">🔄 Restart All</button>
<button id="batch-cancel-btn" class="btn-sm">Cancel</button>
</div>
</div>
<!-- App/service grid -->
<div id="cards" class="grid"></div>
<!-- App Selector Button -->
<div style="text-align: center; margin: 32px 0; display: flex; justify-content: center; gap: 16px; flex-wrap: wrap;">
<button id="add-service-btn" style="padding: 12px 32px; font-size: 1.1rem; font-weight: 600;">📱 App Selector</button>
<button id="add-service" style="padding: 12px 32px; font-size: 1.1rem; font-weight: 600;" aria-label="Add new app manually">+ Add App Manually</button>
<button id="compose-import-btn" style="padding: 12px 32px; font-size: 1.1rem; font-weight: 600;">📦 Import Compose</button>
<button id="arr-setup-btn" style="padding: 12px 32px; font-size: 1.1rem; font-weight: 600; background: linear-gradient(135deg, #e74c3c 0%, #9b59b6 100%); border: none;">🎬 Smart Arr Connect</button>
</div>
<!-- Initial Setup Wizard -->
<div id="setup-wizard" class="setup-wizard" style="display: none;">
<div class="setup-wizard-content">
<!-- Step 1: Welcome -->
<div class="setup-step" id="setup-step-1" style="display: block;">
<h2 style="margin: 0 0 16px; text-align: center;">Welcome to DashCaddy! 🎉</h2>
<p style="text-align: center; color: var(--muted); margin-bottom: 32px;">
Let's set up your self-hosted app dashboard. This will only take a few minutes.
</p>
<!-- Timezone (universal, applies to all config types) -->
<div style="margin-bottom: 24px; padding: 16px; background: var(--card-bg); border-radius: 8px; border: 1px solid var(--border);">
<label class="form-label-accent" for="setup-timezone">Your Timezone</label>
<select id="setup-timezone" class="form-input-lg" style="width: 100%;">
<!-- Populated by setup-wizard.js with IANA timezones, auto-detected -->
</select>
<div class="form-hint">
Auto-detected from your browser. All deployed apps will use this timezone.
</div>
</div>
<h3 class="heading-accent-lg">Choose your configuration:</h3>
<div class="setup-options">
<label class="setup-option" data-preset="simple">
<input type="radio" name="config-type" value="simple" />
<div class="setup-option-content">
<div class="setup-option-icon">🚀</div>
<div class="setup-option-title">Simple</div>
<div class="setup-option-desc">Access apps via IP:Port only. No DNS or SSL setup needed.</div>
<div class="setup-option-example">Example: http://&lt;your-ip&gt;:8080</div>
</div>
</label>
<label class="setup-option" data-preset="homelab">
<input type="radio" name="config-type" value="homelab" checked />
<div class="setup-option-content">
<div class="setup-option-icon">🏠</div>
<div class="setup-option-title">Professional Home Lab</div>
<div class="setup-option-desc">Custom TLD + Private DNS + Internal CA. Full HTTPS with your own certificate authority.</div>
<div class="setup-option-example">Example: https://uptime.home</div>
</div>
</label>
<label class="setup-option" data-preset="public">
<input type="radio" name="config-type" value="public" />
<div class="setup-option-content">
<div class="setup-option-icon">🌍</div>
<div class="setup-option-title">Public Server</div>
<div class="setup-option-desc">Real domain + Public DNS + Let's Encrypt. Internet-accessible with trusted SSL.</div>
<div class="setup-option-example">Example: https://cloud.example.com</div>
</div>
</label>
<label class="setup-option" data-preset="custom">
<input type="radio" name="config-type" value="custom" />
<div class="setup-option-content">
<div class="setup-option-icon">⚙️</div>
<div class="setup-option-title">Custom</div>
<div class="setup-option-desc">I'll configure everything myself. Show me all options.</div>
<div class="setup-option-example">Full control over every setting</div>
</div>
</label>
</div>
<div class="setup-wizard-buttons">
<button id="setup-skip" style="margin-right: auto; background: transparent; border: 1px solid var(--muted); color: var(--muted);">Skip Setup</button>
<button id="setup-step-1-next" class="setup-btn-primary">Continue →</button>
</div>
</div>
<!-- Step 2: Home Lab Configuration -->
<div class="setup-step" id="setup-step-homelab" style="display: none;">
<h2 style="margin: 0 0 8px;">Professional Home Lab Setup</h2>
<p class="setup-desc">Configure your custom TLD and certificate authority</p>
<div style="display: grid; gap: 24px;">
<!-- Custom TLD -->
<div>
<label class="form-label-accent">
🌐 Custom Top-Level Domain (TLD)
</label>
<input type="text" id="setup-tld" value=".home" placeholder=".home"
class="form-input-lg" />
<div class="form-hint">
Your apps will use this TLD. Examples: .sami, .home, .lab, .local, .internal
</div>
<div style="font-size: 0.85rem; color: var(--accent); margin-top: 8px; font-weight: 500;">
📱 Preview: uptime<span id="tld-preview">.home</span>, nextcloud<span id="tld-preview-2">.home</span>
</div>
</div>
<!-- Certificate Authority Name -->
<div>
<label class="form-label-accent">
🔒 Certificate Authority Name
</label>
<input type="text" id="setup-ca-name" value="" placeholder="My Home Lab Root CA"
class="form-input-lg" />
<div class="form-hint">
This name appears in browser certificate details. Make it memorable!
</div>
</div>
<!-- DNS Server Configuration -->
<div>
<label class="form-label-accent">
🗂️ DNS Provider
</label>
<select id="setup-dns-provider" class="form-input-lg" style="margin-bottom: 12px;">
<option value="technitium">Technitium DNS (recommended)</option>
<option value="cloudflare">Cloudflare DNS</option>
<option value="rfc2136">RFC 2136 (BIND / PowerDNS / other)</option>
<option value="manual">Manual / External DNS</option>
</select>
<div style="display: grid; grid-template-columns: 1fr auto; gap: 8px;">
<input type="text" id="setup-dns-ip" value="" placeholder="DNS server IP"
style="padding: 12px; background: var(--card-bg); color: var(--fg); border: 1px solid var(--border); border-radius: 6px; font-size: 1rem;" />
<input type="number" id="setup-dns-port" value="5380" placeholder="5380"
style="width: 100px; padding: 12px; background: var(--card-bg); color: var(--fg); border: 1px solid var(--border); border-radius: 6px; font-size: 1rem;" />
</div>
<div class="form-hint">
DashCaddy will automatically create DNS records for your apps
</div>
</div>
<!-- DNS Admin Token -->
<div>
<label class="form-label-accent">
🔑 DNS Admin Token / API Key
</label>
<input type="password" id="setup-dns-token" placeholder="Paste your admin token here"
class="form-input-lg" />
<details style="margin-top: 8px;">
<summary style="cursor: pointer; color: var(--muted); font-size: 0.85rem;">️ How to get your token</summary>
<div style="margin-top: 8px; padding: 12px; background: color-mix(in srgb, var(--accent) 5%, transparent); border-radius: 6px; font-size: 0.85rem;">
<ol style="margin: 0; padding-left: 20px; color: var(--muted);">
<li>Open Technitium DNS web panel</li>
<li>Go to <strong>Settings → API</strong></li>
<li>Create a new token with <strong>"Administration"</strong> permission</li>
<li>Copy the token and paste it here</li>
</ol>
</div>
</details>
</div>
</div>
<div class="setup-wizard-buttons">
<button id="setup-homelab-back">← Back</button>
<button id="setup-homelab-next" class="setup-btn-primary">Continue →</button>
</div>
</div>
<!-- Step 3: Simple Configuration -->
<div class="setup-step" id="setup-step-simple" style="display: none;">
<h2 style="margin: 0 0 8px;">Simple Setup</h2>
<p class="setup-desc">Access your apps directly via IP addresses and ports</p>
<div style="padding: 24px; background: color-mix(in srgb, var(--accent) 5%, transparent); border-radius: 12px; border: 1px solid var(--border);">
<h3 style="margin: 0 0 16px; color: var(--accent);">✓ What you'll have:</h3>
<ul style="margin: 0; padding-left: 20px; line-height: 1.8;">
<li>Direct access via IP:Port (e.g., http://&lt;your-ip&gt;:8080)</li>
<li>No DNS configuration needed</li>
<li>No SSL/certificate setup</li>
<li>Apps work immediately after deployment</li>
</ul>
<h3 class="heading-accent-lg">⚠️ Limitations:</h3>
<ul style="margin: 0; padding-left: 20px; line-height: 1.8; color: var(--muted);">
<li>Must remember IP:Port for each app</li>
<li>No pretty URLs</li>
<li>HTTP only (no HTTPS encryption)</li>
<li>Not recommended for sensitive data</li>
</ul>
</div>
<div style="margin-top: 24px; padding: 16px; background: var(--card-bg); border-radius: 8px; border: 1px solid var(--border);">
<label style="display: block; margin-bottom: 8px; font-weight: 500;">
Default Server IP Address
</label>
<input type="text" id="setup-simple-ip" value="localhost" placeholder="Your LAN IP"
style="width: 100%; padding: 10px; background: var(--bg); color: var(--fg); border: 1px solid var(--border); border-radius: 6px;" />
<div class="form-hint">
Use "localhost" for same-host deployments, or your server's IP
</div>
</div>
<div class="setup-wizard-buttons">
<button id="setup-simple-back">← Back</button>
<button id="setup-simple-next" class="setup-btn-primary">Continue →</button>
</div>
</div>
<!-- Step 4: Public Server Configuration -->
<div class="setup-step" id="setup-step-public" style="display: none;">
<h2 style="margin: 0 0 8px;">Public Server Setup</h2>
<p class="setup-desc">Configure for internet-accessible apps with Let's Encrypt SSL</p>
<div style="display: grid; gap: 20px;">
<div>
<label class="form-label-accent">
🌍 Your Domain
</label>
<input type="text" id="setup-public-domain" placeholder="example.com"
class="form-input-lg" />
<div class="form-hint">
Your registered domain name (must point to this server)
</div>
</div>
<div>
<label class="form-label-accent">
📧 Email for Let's Encrypt
</label>
<input type="email" id="setup-public-email" placeholder="admin@example.com"
class="form-input-lg" />
<div class="form-hint">
Required for Let's Encrypt certificate renewal notifications
</div>
</div>
<div>
<label class="form-label-accent">
🔀 App Routing Mode
</label>
<div style="display: grid; gap: 10px; margin-top: 8px;">
<label style="display: flex; align-items: start; gap: 10px; padding: 12px; background: var(--card-bg); border: 1px solid var(--border); border-radius: 8px; cursor: pointer;">
<input type="radio" name="routing-mode" value="subdirectory" checked style="margin-top: 3px;" />
<div>
<strong>Subdirectory</strong> <span style="color: var(--muted);">(example.com/app)</span>
<div style="font-size: 0.8rem; color: var(--muted); margin-top: 2px;">
Only needs a single DNS record. Recommended for most setups.
</div>
</div>
</label>
<label style="display: flex; align-items: start; gap: 10px; padding: 12px; background: var(--card-bg); border: 1px solid var(--border); border-radius: 8px; cursor: pointer;">
<input type="radio" name="routing-mode" value="subdomain" style="margin-top: 3px;" />
<div>
<strong>Subdomain</strong> <span style="color: var(--muted);">(app.example.com)</span>
<div style="font-size: 0.8rem; color: var(--muted); margin-top: 2px;">
Requires wildcard DNS (*.example.com) or per-app DNS records
</div>
</div>
</label>
</div>
</div>
<div style="padding: 16px; background: color-mix(in srgb, var(--accent) 5%, transparent); border-radius: 8px; border: 1px solid var(--accent);">
<h4 class="heading-accent-md">⚠️ Requirements:</h4>
<ul style="margin: 0; padding-left: 20px; font-size: 0.9rem; line-height: 1.6;">
<li>Port 80 and 443 must be open to the internet</li>
<li>Domain DNS must point to this server's public IP</li>
<li>Server must be reachable from the internet</li>
<li id="dns-requirement-note">Only one DNS record needed (for the main domain)</li>
</ul>
</div>
</div>
<div class="setup-wizard-buttons">
<button id="setup-public-back">← Back</button>
<button id="setup-public-next" class="setup-btn-primary">Continue →</button>
</div>
</div>
<!-- Step 5: Summary & Confirmation -->
<div class="setup-step" id="setup-step-summary" style="display: none;">
<h2 style="margin: 0 0 8px;">Review Your Configuration</h2>
<p class="setup-desc">Make sure everything looks correct before finishing</p>
<div id="setup-summary-content" style="padding: 24px; background: var(--card-bg); border-radius: 12px; border: 1px solid var(--border);">
<!-- Will be filled dynamically -->
</div>
<!-- Disk safety info: health checks accumulate data over time -->
<div style="margin-top: 16px; padding: 14px 16px; background: color-mix(in srgb, var(--warn-fg, #f39c12) 10%, transparent); border-radius: 8px; border: 1px solid var(--warn-fg, #f39c12);">
<div style="display: flex; gap: 10px; align-items: flex-start;">
<span style="font-size: 1.2rem; line-height: 1;">💾</span>
<div>
<strong style="color: var(--warn-fg, #f39c12);">Disk Usage &amp; Health-Check Data</strong>
<div style="font-size: 0.85rem; margin-top: 4px; color: var(--muted); line-height: 1.45;">
DashCaddy's health checks log response times, uptime history, and incidents for every monitored service.
Over time this data accumulates and can consume significant disk space — especially on small VPS or
SD-card installs. After setup, open <strong>Health → Configure → Global Settings</strong> to set a
<strong>data retention period</strong> (default 30 days), adjust the <strong>polling interval</strong>,
and configure a <strong>disk-usage warning threshold</strong> so you're alerted before storage runs low.
</div>
</div>
</div>
</div>
<div style="margin-top: 24px; padding: 16px; background: color-mix(in srgb, var(--ok-fg) 10%, transparent); border-radius: 8px; border: 1px solid var(--ok-fg);">
<strong style="color: var(--ok-fg);">✓ You can change these settings later</strong>
<div style="font-size: 0.85rem; margin-top: 4px; color: var(--muted);">
Go to Settings → System Configuration to edit your setup anytime
</div>
</div>
<div class="setup-wizard-buttons">
<button id="setup-summary-back">← Back</button>
<button id="setup-finish" class="setup-btn-primary" style="background: var(--ok-bg); border-color: var(--ok-fg); color: var(--ok-fg);">✓ Finish Setup</button>
</div>
</div>
</div>
</div>
<!-- Dashboard Widget Visibility (load before paint) -->
<script>
(function() {
var widgets = [
{ id: 'weather', selector: '.weather-widget-container' },
{ id: 'digital-clock', selector: '.clock-widget-container' }
];
for (var i = 0; i < widgets.length; i++) {
var key = 'widget-' + widgets[i].id + '-enabled';
var val = localStorage.getItem(key);
// Default to enabled if never set
if (val === 'false') {
var el = document.querySelector(widgets[i].selector);
if (el) el.style.display = 'none';
}
}
var yr = document.getElementById('footer-year');
if (yr) yr.textContent = new Date().getFullYear();
var versionEl = document.getElementById('license-topbar-version');
var versionTextEl = document.getElementById('license-topbar-version-text');
var versionBulletEl = document.getElementById('license-topbar-version-bullet');
var versionInfoModal = document.getElementById('version-info-modal');
var versionInfoStatus = document.getElementById('version-info-status');
var versionInfoGrid = document.getElementById('version-info-grid');
var versionInfoHistory = document.getElementById('version-info-history');
var versionInfoActions = document.getElementById('version-info-actions');
var versionInfoUpdate = document.getElementById('version-info-update');
var versionInfoOpenUpdates = document.getElementById('version-info-open-updates');
var versionInfoClose = document.getElementById('version-info-close');
var latestUpdateCheck = null;
function formatValue(value) {
if (value == null || value === '') return '—';
if (typeof value === 'object') return JSON.stringify(value);
return String(value);
}
function renderInfoRow(label, value) {
return '<div class="version-info-row"><span class="version-info-label">' + label + '</span><span class="version-info-value">' + formatValue(value) + '</span></div>';
}
function renderHistory(history) {
if (!Array.isArray(history) || !history.length) {
versionInfoHistory.style.display = 'none';
versionInfoHistory.innerHTML = '';
return;
}
versionInfoHistory.style.display = '';
versionInfoHistory.innerHTML = '<h4>Recent Update History</h4>' + history.slice(0, 5).map(function(entry) {
return '<div class="version-history-entry">'
+ '<div><strong>' + formatValue(entry.version) + '</strong> <span class="version-history-status">' + formatValue(entry.status) + '</span></div>'
+ '<div class="version-history-meta">From ' + formatValue(entry.fromVersion) + ' · ' + formatValue(entry.timestamp) + '</div>'
+ '</div>';
}).join('');
}
function setVersionLabel(text) {
if (versionTextEl) versionTextEl.textContent = text;
}
function setVersionUpdateState(hasUpdate) {
if (!versionEl || !versionBulletEl) return;
versionEl.classList.toggle('has-update', !!hasUpdate);
versionEl.title = hasUpdate
? 'Update available — click for details or to update'
: 'View DashCaddy verification info';
versionEl.setAttribute('aria-label', hasUpdate
? 'DashCaddy update available — click for details or to update'
: 'View DashCaddy verification info');
versionBulletEl.textContent = hasUpdate ? '⬆' : '·';
}
function checkVersionUpdateAvailability() {
return fetch('/api/v1/system/update-check', { cache: 'no-store', credentials: 'same-origin' })
.then(function(response) {
if (response.status === 401) throw new Error('AUTH_PENDING');
if (!response.ok) throw new Error('HTTP ' + response.status);
return response.json();
})
.then(function(data) {
latestUpdateCheck = data;
setVersionUpdateState(!!(data && data.success && data.available && data.remote));
return data;
})
.catch(function(error) {
if (!(error && error.message === 'AUTH_PENDING')) {
setVersionUpdateState(false);
}
throw error;
});
}
function loadVersionLabel(retryCount) {
if (!versionEl) return;
fetch('/api/v1/system/version', { cache: 'no-store', credentials: 'same-origin' })
.then(function(response) {
if (response.status === 401) {
throw new Error('AUTH_PENDING');
}
if (!response.ok) throw new Error('HTTP ' + response.status);
return response.json();
})
.then(function(data) {
if (data && data.success && data.version) {
setVersionLabel('v' + data.version);
checkVersionUpdateAvailability().catch(function() {});
return;
}
setVersionLabel('version unknown');
setVersionUpdateState(false);
})
.catch(function(error) {
if (error && error.message === 'AUTH_PENDING') {
setVersionLabel('signing in…');
if ((retryCount || 0) < 12) {
setTimeout(function() {
loadVersionLabel((retryCount || 0) + 1);
}, 1500);
}
return;
}
setVersionLabel('version unavailable');
setVersionUpdateState(false);
});
}
function openUpdatesPanel() {
var updatesBtn = document.getElementById('updates-btn');
var updatesDashcaddyTab = document.getElementById('updates-dashcaddy-tab');
if (updatesBtn) updatesBtn.click();
setTimeout(function() {
if (updatesDashcaddyTab) updatesDashcaddyTab.click();
}, 50);
}
function applyVersionUpdate() {
if (window.dcApplyUpdate) {
return window.dcApplyUpdate();
}
openUpdatesPanel();
}
function openVersionInfo() {
if (!versionInfoModal) return;
versionInfoModal.classList.add('show');
versionInfoStatus.textContent = 'Loading…';
versionInfoGrid.style.display = 'none';
versionInfoHistory.style.display = 'none';
if (versionInfoActions) versionInfoActions.style.display = 'none';
if (versionInfoUpdate) {
versionInfoUpdate.disabled = true;
versionInfoUpdate.textContent = 'Update Now';
}
versionInfoGrid.innerHTML = '';
versionInfoHistory.innerHTML = '';
Promise.all([
fetch('/api/v1/system/version', { cache: 'no-store', credentials: 'same-origin' }).then(function(response) {
if (!response.ok) throw new Error('Version check failed');
return response.json();
}),
fetch('/api/v1/system/update-status', { cache: 'no-store', credentials: 'same-origin' }).then(function(response) {
if (!response.ok) throw new Error('Update status failed');
return response.json();
}),
fetch('/api/v1/system/update-history', { cache: 'no-store', credentials: 'same-origin' }).then(function(response) {
if (!response.ok) throw new Error('Update history failed');
return response.json();
}),
checkVersionUpdateAvailability().catch(function() { return latestUpdateCheck || {}; })
]).then(function(results) {
var versionData = results[0] || {};
var statusData = results[1] || {};
var historyData = results[2] || {};
var updateCheckData = results[3] || latestUpdateCheck || {};
var lastResult = statusData.lastResult || updateCheckData || {};
var lastPolicy = lastResult.policy || {};
var hasUpdate = !!(updateCheckData && updateCheckData.success && updateCheckData.available && updateCheckData.remote);
versionInfoStatus.textContent = hasUpdate
? 'Update available — you can install it from here.'
: 'Verification info loaded.';
versionInfoGrid.style.display = 'grid';
versionInfoGrid.innerHTML = [
renderInfoRow('Version', versionData.version),
renderInfoRow('Commit', versionData.commit),
renderInfoRow('Updater Status', statusData.status),
renderInfoRow('Last Check', statusData.lastCheck ? new Date(statusData.lastCheck).toLocaleString() : 'Never'),
renderInfoRow('Update Available', hasUpdate),
renderInfoRow('Available Version', updateCheckData.remote && updateCheckData.remote.version),
renderInfoRow('Eligible', lastPolicy.eligible),
renderInfoRow('Policy Reason', lastPolicy.reason),
renderInfoRow('Channel', lastPolicy.releaseChannel || (lastResult.instance && lastResult.instance.channel)),
renderInfoRow('Instance ID', lastResult.instance && lastResult.instance.instanceId)
].join('');
if (versionInfoActions) versionInfoActions.style.display = 'flex';
if (versionInfoUpdate) versionInfoUpdate.disabled = !hasUpdate;
renderHistory(historyData.history);
}).catch(function(error) {
versionInfoStatus.textContent = 'Could not load verification info: ' + error.message;
if (versionInfoActions) versionInfoActions.style.display = 'flex';
});
}
if (versionEl) {
loadVersionLabel(0);
versionEl.addEventListener('click', function(event) {
event.stopPropagation();
openVersionInfo();
});
}
var licenseBtn = document.getElementById('license-btn');
var apiDocsBtn = document.getElementById('api-docs-btn');
var helpErrorsBtn = document.getElementById('help-errors-btn');
if (licenseBtn) {
licenseBtn.addEventListener('click', function() {
if (window.openLicenseModal) window.openLicenseModal();
});
}
if (apiDocsBtn) {
apiDocsBtn.addEventListener('click', function() {
window.open('/api/v1/docs', '_blank');
});
}
if (helpErrorsBtn) {
helpErrorsBtn.addEventListener('click', function() {
window.open('/help-errors.html', '_blank');
});
}
if (versionInfoUpdate) {
versionInfoUpdate.addEventListener('click', function() {
applyVersionUpdate();
});
}
if (versionInfoOpenUpdates) {
versionInfoOpenUpdates.addEventListener('click', function() {
versionInfoModal.classList.remove('show');
openUpdatesPanel();
});
}
if (versionInfoClose && versionInfoModal) {
versionInfoClose.addEventListener('click', function() {
versionInfoModal.classList.remove('show');
});
versionInfoModal.addEventListener('click', function(event) {
if (event.target === versionInfoModal) {
versionInfoModal.classList.remove('show');
}
});
}
window.openVersionInfo = openVersionInfo;
window.applyVersionUpdate = applyVersionUpdate;
})();
</script>
<!-- Clock rendering is handled by the bundled clock.js module -->
<footer class="dashcaddy-footer">
<span class="footer-copy">&copy; <span id="footer-year"></span></span>
<img src="/assets/sami7777-logo.png" alt="samiahmed7777" class="footer-logo">
<nav class="footer-legal" aria-label="Legal">
<a href="/legal/terms">Terms of Service</a>
<a href="/legal/privacy">Privacy Policy</a>
</nav>
</footer>
<!-- xterm.js for container exec/shell -->
<script src="/js/xterm.min.js" defer></script>
<script src="/js/xterm-fit.min.js" defer></script>
<!-- Tailscale device list panel (self-contained, polls /api/v1/tailscale/devices) -->
<script src="/js/log-insights.js" defer></script>
<script src="/js/disk-settings.js" defer></script>
<script src="/js/ai-chat.js" defer></script>
<script src="/js/tailscale-devices.js" defer></script>
<script src="/js/language-selector.js" defer></script>
<!-- Bundled JS (built with: npm run build) -->
<!-- i18n (language selector + translation system) -->
<script src="/js/i18n.js" defer></script>
<script src="/dist/core.js" defer></script>
<script src="/dist/features.js" defer></script>
<script src="/dist/onboarding.js" defer></script>
<script src="/dist/init.js" defer></script>
<!-- AI Chat Floating Button -->
<button onclick="toggleAIChat()" style="position:fixed;bottom:20px;right:20px;width:56px;height:56px;border-radius:50%;background:linear-gradient(135deg,#6366f1,#8b5cf6);border:none;box-shadow:0 4px 20px rgba(99,102,241,0.4);cursor:pointer;z-index:9998;font-size:1.5rem;display:flex;align-items:center;justify-content:center;transition:transform 0.2s;" onmouseover="this.style.transform='scale(1.1)'" onmouseout="this.style.transform='scale(1)'" title="AI Assistant">🤖</button>
</body>
</html>