Initial commit

This commit is contained in:
2026-06-05 23:32:06 +02:00
parent 735ea3964f
commit a0e7e3f7a8
4 changed files with 1024 additions and 0 deletions
+386
View File
@@ -0,0 +1,386 @@
<!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>