C-grade round-1 blockers fixed:
- [HIGH] retention default 14d → 30d to match engine (health-checker.js:34)
- [MEDIUM] phantom 'Settings → Disk Safety' path removed
- [MEDIUM] dangling 'stats polling interval' bullet (no such control in modal)
- [LOW] exaggerated 'hundreds of MB' → 'tens of MB'
- [LOW] button label mismatch (real button is '💾 Disk')
GLM round 2 verified all 5 fixes landed; no new regressions; HTML balanced.
Pre-existing follow-up parked: disk-settings.json saved values not reloaded by engine on container restart (out of scope for this commit).
1042 lines
55 KiB
HTML
1042 lines
55 KiB
HTML
<!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-LqwtGSCiBtjq9Bnb5wA3kQFrFRgZWtDWcswiK656gEk='; 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">☆</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">▸</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">▸</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">▸</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" data-i18n="card.internet">Internet</span>
|
||
<span class="spacer"></span>
|
||
<span id="internet-pill" class="badge off" data-i18n-live-status="binary">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" data-i18n="card.auth">Auth</span>
|
||
<span class="spacer"></span>
|
||
<span id="auth-pill" class="badge off" data-i18n-live-status="yesno">NO</span>
|
||
</div>
|
||
<div class="response-row">
|
||
<span id="auth-status-text" class="response-time" style="font-size: 0.7rem;" data-i18n="card.auth.not_configured">Not configured</span>
|
||
</div>
|
||
<div class="btn-row">
|
||
<button id="auth-settings-btn" data-i18n="action.settings">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" data-i18n="card.tailscale">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" data-i18n="card.dashca">DashCA</span>
|
||
<span class="spacer"></span>
|
||
<span id="badge-ca" class="badge off" data-i18n-live-status="binary">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" data-i18n="action.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..." data-i18n-placeholder="filter.services_placeholder" data-i18n-prefix="🔍 " 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" data-i18n="filter.all_status">All Status</option>
|
||
<option value="on" data-i18n="filter.online" data-i18n-prefix="🟢 ">🟢 Online</option>
|
||
<option value="off" data-i18n="filter.offline" data-i18n-prefix="🔴 ">🔴 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" data-i18n="filter.all_categories">All Categories</option>
|
||
</select>
|
||
<button id="batch-operations-btn" class="btn-sm" style="padding: 8px 12px;" data-i18n="filter.batch_operations" data-i18n-prefix="☰ ">☰ 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://<your-ip>: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://<your-ip>: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 & 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-summary-next" class="setup-btn-primary">Continue →</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Disk Safety Warning step (shown after the configuration summary) -->
|
||
<div class="setup-step" id="setup-step-disk-safety" style="display: none;">
|
||
<h2 style="margin: 0 0 8px;">⚠️ Disk Usage Note</h2>
|
||
<p class="setup-desc">Important information about storage before you finish</p>
|
||
|
||
<div style="margin-top: 8px; padding: 18px 20px; background: color-mix(in srgb, var(--warn-fg, #f39c12) 12%, transparent); border-radius: 10px; border: 1px solid var(--warn-fg, #f39c12);">
|
||
<div style="display: flex; gap: 12px; align-items: flex-start;">
|
||
<span style="font-size: 1.5rem; line-height: 1.2;">⚠️</span>
|
||
<div style="font-size: 0.92rem; line-height: 1.55; color: var(--text);">
|
||
<strong style="color: var(--warn-fg, #f39c12);">Disk Usage Note:</strong>
|
||
DashCaddy stores up to <strong>500 health check entries per service</strong> (plus container statistics and event logs).
|
||
At high check frequencies this may consume significant disk space — on a host with many services the history file can grow to tens of megabytes.
|
||
Adjust the <strong>health check interval</strong>, <strong>max entries per service</strong>, and <strong>health retention period</strong> in <strong>Disk Safety</strong> (the 💾 Disk button in the top bar) to control disk usage.
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="margin-top: 16px; padding: 14px 16px; background: var(--card-bg); border-radius: 8px; border: 1px solid var(--border);">
|
||
<strong style="font-size: 0.9rem;">📋 Recommended after setup</strong>
|
||
<ul style="margin: 8px 0 0; padding-left: 20px; font-size: 0.85rem; color: var(--muted); line-height: 1.6;">
|
||
<li>Open the <strong>💾 Disk</strong> button in the top bar (opens the Disk Safety modal)</li>
|
||
<li>Set a <strong>health check polling interval</strong> (default: 30s)</li>
|
||
<li>Set a <strong>health retention period</strong> (default: 30 days)</li>
|
||
<li>Cap <strong>max health entries per service</strong> (default: 500)</li>
|
||
<li>Cap <strong>max stats entries</strong> (default: 500)</li>
|
||
<li>Click <strong>Clean Up Now</strong> to purge old data immediately</li>
|
||
</ul>
|
||
</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-disk-safety-back">← Back</button>
|
||
<button id="setup-disk-safety-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 escapeHtml(value) {
|
||
return String(value).replace(/[&<>"']/g, function(character) {
|
||
return {
|
||
'&': '&', '<': '<', '>': '>',
|
||
'"': '"', "'": '''
|
||
}[character];
|
||
});
|
||
}
|
||
|
||
function formatValue(value) {
|
||
if (value == null || value === '') return '—';
|
||
if (typeof value === 'object') return escapeHtml(JSON.stringify(value));
|
||
return escapeHtml(value);
|
||
}
|
||
|
||
function renderInfoRow(label, value) {
|
||
return '<div class="version-info-row"><span class="version-info-label">' + escapeHtml(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">© <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>
|