Files
wol-dashboard/templates/index.html
T
2026-06-07 15:46:18 +02:00

387 lines
15 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">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WoL Dashboard</title>
<style>
:root {
--bg: #080d14;
--surface: #111827;
--surface2: #1a2235;
--border: #1e2d45;
--accent: #3b82f6;
--accent2: #6366f1;
--success: #10b981;
--danger: #ef4444;
--warn: #f59e0b;
--text: #f1f5f9;
--muted: #64748b;
--radius: 16px;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Segoe UI', system-ui, sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
padding: 1.5rem 1rem 4rem;
}
.header { text-align: center; padding: 2rem 0 2rem; }
.header .logo { font-size: 2.8rem; margin-bottom: 0.5rem; }
.header h1 {
font-size: 1.6rem; font-weight: 700;
background: linear-gradient(135deg, #3b82f6, #6366f1);
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
background-clip: text; margin-bottom: 0.3rem;
}
.header p { color: var(--muted); font-size: 0.9rem; }
.stats {
display: flex; justify-content: center; gap: 0.8rem;
margin-bottom: 2rem; flex-wrap: wrap;
}
.stat-chip {
background: var(--surface); border: 1px solid var(--border);
border-radius: 999px; padding: 0.4rem 1rem;
font-size: 0.82rem; color: var(--muted);
}
.stat-chip span { font-weight: 700; }
.online-count { color: var(--success) !important; }
.offline-count { color: var(--danger) !important; }
.total-count { color: var(--accent) !important; }
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(270px, 1fr));
gap: 1rem; max-width: 1000px; margin: 0 auto 2rem;
}
.card {
background: var(--surface); border: 1px solid var(--border);
border-radius: var(--radius); padding: 1.3rem;
display: flex; flex-direction: column; gap: 0.8rem;
transition: all 0.25s; position: relative; overflow: hidden;
}
.card::before {
content: ''; position: absolute; top: 0; left: 0; right: 0;
height: 2px; background: linear-gradient(90deg, var(--accent), var(--accent2));
opacity: 0; transition: opacity 0.25s;
}
.card:hover { border-color: var(--accent); transform: translateY(-2px); box-shadow: 0 8px 30px rgba(59,130,246,0.1); }
.card:hover::before { opacity: 1; }
.card.is-online { border-color: rgba(16,185,129,0.35); }
.card.is-online::before { background: linear-gradient(90deg, var(--success), #059669); opacity: 1; }
.card-top { display: flex; justify-content: space-between; align-items: flex-start; }
.card-icon {
width: 42px; height: 42px;
background: linear-gradient(135deg, var(--accent), var(--accent2));
border-radius: 10px; display: flex; align-items: center;
justify-content: center; font-size: 1.2rem; flex-shrink: 0;
}
.card-icon.online { background: linear-gradient(135deg, var(--success), #059669); }
.card-actions { display: flex; align-items: center; gap: 0.4rem; }
.status-dot {
width: 10px; height: 10px; border-radius: 50%;
background: var(--muted); flex-shrink: 0; transition: background 0.3s;
}
.status-dot.online { background: var(--success); box-shadow: 0 0 6px var(--success); animation: pulse 2s infinite; }
.status-dot.offline { background: var(--danger); }
@keyframes pulse {
0%, 100% { box-shadow: 0 0 4px var(--success); }
50% { box-shadow: 0 0 10px var(--success); }
}
.btn-del {
background: none; border: none; color: var(--muted);
cursor: pointer; font-size: 1rem; padding: 4px;
border-radius: 6px; transition: all 0.2s; line-height: 1;
}
.btn-del:hover { color: var(--danger); background: rgba(239,68,68,0.1); }
.card-name { font-size: 1rem; font-weight: 700; }
.card-mac {
font-family: 'Courier New', monospace; font-size: 0.78rem;
color: var(--muted); background: var(--surface2);
padding: 0.35rem 0.65rem; border-radius: 8px;
}
.card-ip { font-size: 0.78rem; color: var(--muted); font-family: monospace; }
.status-label { font-size: 0.78rem; font-weight: 600; }
.status-label.online { color: var(--success); }
.status-label.offline { color: var(--danger); }
.status-label.unknown { color: var(--muted); }
.btn-wake {
width: 100%; padding: 0.75rem;
background: linear-gradient(135deg, var(--accent), var(--accent2));
color: white; border: none; border-radius: 10px;
font-size: 0.95rem; font-weight: 600; cursor: pointer;
transition: all 0.2s; display: flex; align-items: center;
justify-content: center; gap: 0.5rem;
}
.btn-wake:hover { opacity: 0.85; transform: scale(0.98); }
.btn-wake:active { transform: scale(0.95); }
.feedback-msg {
font-size: 0.78rem; text-align: center;
min-height: 1rem; padding: 0.2rem;
border-radius: 6px; transition: all 0.3s;
}
.success { color: var(--success); background: rgba(16,185,129,0.08); padding: 0.3rem 0.5rem; }
.error { color: var(--danger); background: rgba(239,68,68,0.08); padding: 0.3rem 0.5rem; }
.empty-state { grid-column: 1/-1; text-align: center; padding: 3rem 1rem; color: var(--muted); }
.empty-state .empty-icon { font-size: 3rem; margin-bottom: 0.8rem; }
.panel {
max-width: 480px; margin: 0 auto 1.2rem;
background: var(--surface); border: 1px solid var(--border);
border-radius: var(--radius); overflow: hidden;
}
.panel-header {
padding: 1rem 1.3rem; display: flex; align-items: center;
gap: 0.6rem; cursor: pointer; user-select: none; transition: background 0.2s;
}
.panel-header:hover { background: var(--surface2); }
.panel-header h3 { font-size: 0.95rem; font-weight: 600; flex: 1; }
.panel-header .chevron { color: var(--muted); transition: transform 0.3s; }
.panel-header.open .chevron { transform: rotate(180deg); }
.panel-body { padding: 0 1.3rem 1.3rem; display: none; }
.panel-body.open { display: block; }
.panel-body input {
width: 100%; padding: 0.7rem 0.9rem;
background: var(--bg); border: 1px solid var(--border);
border-radius: 10px; color: var(--text); font-size: 0.9rem;
margin-bottom: 0.65rem; outline: none; transition: border-color 0.2s;
}
.panel-body input:focus { border-color: var(--accent); }
.panel-body input::placeholder { color: var(--muted); }
.btn-primary {
width: 100%; padding: 0.75rem;
background: linear-gradient(135deg, var(--accent), var(--accent2));
color: white; border: none; border-radius: 10px;
font-size: 0.95rem; font-weight: 600; cursor: pointer;
transition: opacity 0.2s;
}
.btn-primary:hover { opacity: 0.85; }
.btn-primary:disabled { opacity: 0.4; cursor: not-allowed; }
.scan-hint { font-size: 0.8rem; color: var(--muted); margin-bottom: 1rem; }
.scan-status { font-size: 0.83rem; margin: 0.8rem 0 0.3rem; }
.scan-item {
background: var(--surface2); border: 1px solid var(--border);
border-radius: 10px; padding: 0.75rem 1rem; margin-top: 0.6rem;
display: flex; justify-content: space-between; align-items: center; gap: 0.5rem;
}
.scan-item-name { font-weight: 600; font-size: 0.9rem; }
.scan-item-mac { font-size: 0.75rem; color: var(--muted); font-family: monospace; }
.btn-add-small {
padding: 0.4rem 0.9rem; background: var(--accent);
color: white; border: none; border-radius: 8px;
font-size: 0.8rem; font-weight: 600; cursor: pointer;
white-space: nowrap; flex-shrink: 0; transition: opacity 0.2s;
}
.btn-add-small:hover { opacity: 0.8; }
.refresh-btn {
background: none; border: 1px solid var(--border); color: var(--muted);
padding: 0.35rem 0.8rem; border-radius: 999px; font-size: 0.78rem;
cursor: pointer; transition: all 0.2s; margin-left: 0.5rem;
}
.refresh-btn:hover { border-color: var(--accent); color: var(--accent); }
@media (max-width: 500px) {
.header h1 { font-size: 1.3rem; }
.card-grid { grid-template-columns: 1fr; }
body { padding: 1rem 0.75rem 4rem; }
}
</style>
</head>
<body>
<div class="header">
<div class="logo"></div>
<h1>Wake-on-LAN</h1>
<p>Wake up your devices remotely via Magic Packet</p>
</div>
<div class="stats">
<div class="stat-chip">Total: <span class="total-count" id="totalCount">0</span></div>
<div class="stat-chip">Online: <span class="online-count" id="onlineCount">0</span></div>
<div class="stat-chip">Offline: <span class="offline-count" id="offlineCount">0</span></div>
<button class="refresh-btn" onclick="refreshStatus()">↻ Refresh Status</button>
</div>
<div class="card-grid" id="deviceGrid"></div>
<!-- Scan Panel -->
<div class="panel">
<div class="panel-header" onclick="togglePanel(this)">
<span>🔍</span><h3>Scan Network</h3><span class="chevron"></span>
</div>
<div class="panel-body">
<p class="scan-hint">Finds all active devices on your local network. Takes 1030 seconds.</p>
<button class="btn-primary" id="scanBtn" onclick="scanNetwork()">🔍 Start Scan</button>
<div class="scan-status" id="scanStatus"></div>
<div id="scanResults"></div>
</div>
</div>
<!-- Add Device Panel -->
<div class="panel">
<div class="panel-header" onclick="togglePanel(this)">
<span></span><h3>Add Device</h3><span class="chevron"></span>
</div>
<div class="panel-body">
<input type="text" id="newName" placeholder="PC Name (e.g. Gaming-PC)" />
<input type="text" id="newMac" placeholder="MAC Address (AA:BB:CC:DD:EE:FF)" />
<input type="text" id="newIp" placeholder="IP Address (optional, for online status)" />
<button class="btn-primary" onclick="addDevice()">💾 Save Device</button>
</div>
</div>
<script>
let devices = [];
let statusData = [];
function togglePanel(header) {
header.classList.toggle('open');
header.nextElementSibling.classList.toggle('open');
}
async function loadDevices() {
const res = await fetch('/devices');
devices = await res.json();
renderDevices();
refreshStatus();
}
function renderDevices() {
document.getElementById('totalCount').textContent = devices.length;
const grid = document.getElementById('deviceGrid');
grid.innerHTML = '';
if (devices.length === 0) {
grid.innerHTML = `
<div class="empty-state">
<div class="empty-icon">🖥️</div>
<p>No devices yet.<br>Scan your network or add one manually.</p>
</div>`;
return;
}
devices.forEach((d, i) => {
const s = statusData[i];
const isOnline = s === true;
const hasIp = !!d.ip;
const dotClass = hasIp ? (isOnline ? 'online' : 'offline') : '';
const labelText = hasIp ? (isOnline ? '● Online' : '● Offline') : '○ No IP set';
const labelClass = hasIp ? (isOnline ? 'online' : 'offline') : 'unknown';
grid.innerHTML += `
<div class="card ${isOnline ? 'is-online' : ''}" id="card-${i}">
<div class="card-top">
<div class="card-icon ${isOnline ? 'online' : ''}">🖥️</div>
<div class="card-actions">
<div class="status-dot ${dotClass}" title="${labelText}"></div>
<button class="btn-del" onclick="deleteDevice(${i})" title="Remove">✕</button>
</div>
</div>
<div class="card-name">${d.name}</div>
<div class="card-mac">${d.mac}</div>
${d.ip ? `<div class="card-ip">IP: ${d.ip}</div>` : ''}
<div class="status-label ${labelClass}">${labelText}</div>
<button class="btn-wake" onclick="wakeDevice('${d.mac}', ${i})">⚡ Wake Up</button>
<div class="feedback-msg" id="status-${i}"></div>
</div>`;
});
}
async function refreshStatus() {
if (devices.length === 0) return;
const res = await fetch('/status');
statusData = await res.json();
const online = statusData.filter(s => s === true).length;
const offline = statusData.filter(s => s === false).length;
document.getElementById('onlineCount').textContent = online;
document.getElementById('offlineCount').textContent = offline;
renderDevices();
}
async function wakeDevice(mac, idx) {
const el = document.getElementById(`status-${idx}`);
el.textContent = 'Sending Magic Packet...'; el.className = 'feedback-msg';
const res = await fetch(`/wake/${mac}`, { method: 'POST' });
const data = await res.json();
el.textContent = data.message;
el.className = `feedback-msg ${data.status === 'success' ? 'success' : 'error'}`;
setTimeout(() => { el.textContent = ''; el.className = 'feedback-msg'; }, 4000);
}
async function deleteDevice(idx) {
if (!confirm('Remove this device?')) return;
await fetch(`/devices/${idx}`, { method: 'DELETE' });
await loadDevices();
}
async function addDevice() {
const name = document.getElementById('newName').value.trim();
const mac = document.getElementById('newMac').value.trim();
const ip = document.getElementById('newIp').value.trim();
if (!name || !mac) return alert('Please enter a name and MAC address!');
await fetch('/devices', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, mac, ip })
});
document.getElementById('newName').value = '';
document.getElementById('newMac').value = '';
document.getElementById('newIp').value = '';
await loadDevices();
}
async function scanNetwork() {
const btn = document.getElementById('scanBtn');
const status = document.getElementById('scanStatus');
const results = document.getElementById('scanResults');
btn.disabled = true; btn.textContent = '⏳ Scanning...';
status.innerHTML = '<span style="color:var(--warn)">nmap running, please wait...</span>';
results.innerHTML = '';
const res = await fetch('/scan', { method: 'POST' });
const data = await res.json();
btn.disabled = false; btn.textContent = '🔍 Start Scan';
if (data.status === 'error') {
status.innerHTML = `<span style="color:var(--danger)">Error: ${data.message}</span>`;
return;
}
if (data.devices.length === 0) {
status.innerHTML = '<span style="color:var(--danger)">No devices with MAC found.</span>';
return;
}
status.innerHTML = `<span style="color:var(--success)">${data.devices.length} device(s) found</span>`;
results.innerHTML = data.devices.map(d => `
<div class="scan-item">
<div>
<div class="scan-item-name">${d.name}</div>
<div class="scan-item-mac">${d.mac} · ${d.ip}</div>
</div>
<button class="btn-add-small" onclick="addFromScan('${d.name}','${d.mac}','${d.ip}')">+ Add</button>
</div>`).join('');
}
async function addFromScan(name, mac, ip) {
await fetch('/devices', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, mac, ip })
});
await loadDevices();
}
loadDevices();
setInterval(refreshStatus, 30000);
</script>
</body>
</html>