Files
dashcaddy/status/index.html
Sami 9a0abc02d1 Fix remaining frontend security issues (3 medium, 2 low)
- Escape user-input port number in app-selector innerHTML
- Replace inline onclick with addEventListener in backup history (HTML entity decode bypass)
- Add Content-Security-Policy meta tag with script hash
- Replace document.write with textContent for footer year
- Filter __proto__/constructor/prototype in Object.assign calls

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 02:06:55 -08:00

558 lines
28 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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-6JZtsKK/PZthh+stCmmCvC2QxCiyk6SwZCBjXE+kYr0='; 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">
</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>
</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="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>
<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="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>
</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">📋 Logs</button>
<button id="manage-notifications" aria-label="Manage notifications">🔔 Alerts</button>
<button id="audit-log-btn" aria-label="Audit log">📜 Audit</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="export-dashboard" aria-label="Export dashboard configuration">📤 Export</button>
<button id="import-dashboard" aria-label="Import dashboard configuration">📥 Import</button>
<button id="backup-restore-btn" aria-label="Backup and restore">💾 Backup</button>
<button id="license-btn" aria-label="License management" onclick="window.openLicenseModal && window.openLicenseModal()">🔑 License</button>
<button id="api-docs-btn" aria-label="API documentation" onclick="window.open('/api/docs', '_blank')">📖 API</button>
<button id="help-errors-btn" aria-label="Troubleshooting guide" onclick="window.open('/help-errors.html', '_blank')">❓ 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">
<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="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>
<!-- 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="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 Server (Technitium)
</label>
<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">
🔑 Technitium Admin Token
</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>
<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();
})();
</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">
</footer>
<!-- Bundled JS (built with: npm run build) -->
<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>
</body>
</html>