Files

951 lines
47 KiB
HTML
Raw Permalink Normal View History

2026-05-10 02:24:34 +02:00
<!DOCTYPE html>
<html lang="nl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Home Dashboard</title>
<style>
:root {
--bg: #0a0e14;
--surface: #131820;
--surface2: #1a202c;
--border: #262d38;
--text: #cdd6e0;
--text-dim: #6b7280;
--accent: #5c9eff;
--accent2: #7c3aed;
--danger: #f85149;
--success: #3fb950;
--warning: #d29922;
--orange: #f0883e;
--radius: 10px;
--radius-sm: 6px;
--transition: 0.15s ease;
}
* { margin:0; padding:0; box-sizing:border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
background: var(--bg); color: var(--text);
min-height: 100vh; display: flex;
}
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: var(--bg); }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
/* ── Sidebar ──────────────────────────── */
.sidebar {
width: 220px; background: var(--surface);
border-right: 1px solid var(--border);
display: flex; flex-direction: column;
padding: 20px 0; flex-shrink: 0;
}
.sidebar-logo {
padding: 0 20px 20px; font-size: 1.1rem;
font-weight: 700; letter-spacing: -0.02em;
border-bottom: 1px solid var(--border); margin-bottom: 12px;
}
.sidebar-logo span { color: var(--accent); }
.nav-item {
display: flex; align-items: center; gap: 10px;
padding: 10px 20px; cursor: pointer;
color: var(--text-dim); transition: var(--transition);
font-size: 0.9rem; border-left: 3px solid transparent;
user-select: none;
}
.nav-item:hover { color: var(--text); background: var(--surface2); }
.nav-item.active { color: var(--accent); border-left-color: var(--accent); background: rgba(92,158,255,0.06); }
.nav-item .icon { font-size: 1.1rem; width: 22px; text-align: center; }
.nav-item .badge {
margin-left: auto; background: var(--accent);
color: #fff; font-size: 0.7rem; padding: 1px 7px;
border-radius: 10px; font-weight: 600;
}
/* ── Main content ─────────────────────── */
.main {
flex: 1; display: flex; flex-direction: column;
overflow: hidden;
}
.topbar {
display: flex; align-items: center; justify-content: space-between;
padding: 14px 28px; background: var(--surface);
border-bottom: 1px solid var(--border);
}
.topbar h1 { font-size: 1.15rem; font-weight: 600; }
.topbar-right { display: flex; gap: 16px; align-items: center; font-size: 0.85rem; color: var(--text-dim); }
.status-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--success); }
.content {
flex: 1; overflow-y: auto; padding: 24px 28px;
}
.tab-content { display: none; }
.tab-content.active { display: block; }
/* ── Cards / Widgets ──────────────────── */
.card {
background: var(--surface); border: 1px solid var(--border);
border-radius: var(--radius); padding: 18px 20px;
margin-bottom: 16px;
}
.card-header {
display: flex; align-items: center; justify-content: space-between;
margin-bottom: 14px;
}
.card-header h3 { font-size: 0.95rem; font-weight: 600; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.05em; }
.card-header .count { font-size: 0.8rem; color: var(--text-dim); }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 16px; }
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.grid-3 { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 12px; }
/* ── Quick link tiles ─────────────────── */
.quick-link {
display: flex; align-items: center; gap: 12px;
padding: 14px 16px; background: var(--surface2);
border: 1px solid var(--border); border-radius: var(--radius);
cursor: pointer; transition: var(--transition);
text-decoration: none; color: var(--text);
}
.quick-link:hover { border-color: var(--accent); background: rgba(92,158,255,0.06); transform: translateY(-1px); }
.quick-link .ql-icon { font-size: 1.8rem; }
.quick-link .ql-info { flex: 1; min-width: 0; }
.quick-link .ql-title { font-size: 0.9rem; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.quick-link .ql-url { font-size: 0.75rem; color: var(--text-dim); }
/* ── Buttons ──────────────────────────── */
.btn {
padding: 7px 16px; border-radius: var(--radius-sm);
border: 1px solid var(--border); background: var(--surface2);
color: var(--text); cursor: pointer; font-size: 0.85rem;
transition: var(--transition);
}
.btn:hover { border-color: var(--accent); }
.btn-primary { background: var(--accent); border-color: var(--accent); color: #fff; }
.btn-danger { border-color: var(--danger); color: var(--danger); }
.btn-sm { padding: 4px 10px; font-size: 0.78rem; }
/* ── Forms ────────────────────────────── */
input, textarea, select {
padding: 8px 12px; border-radius: var(--radius-sm);
border: 1px solid var(--border); background: var(--surface2);
color: var(--text); font-size: 0.85rem; font-family: inherit;
width: 100%;
}
input:focus, textarea:focus, select:focus {
outline: none; border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(92,158,255,0.1);
}
label { display: block; font-size: 0.8rem; color: var(--text-dim); margin-bottom: 4px; }
.form-group { margin-bottom: 14px; }
.form-row { display: flex; gap: 12px; }
.form-row > * { flex: 1; }
/* ── Network graph placeholder ────────── */
.network-node {
display: inline-flex; align-items: center; gap: 8px;
padding: 10px 14px; background: var(--surface2);
border: 1px solid var(--border); border-radius: var(--radius);
font-size: 0.85rem; cursor: pointer; transition: var(--transition);
margin: 4px;
}
.network-node:hover { border-color: var(--accent); }
.network-node .status { width: 8px; height: 8px; border-radius: 50%; }
.network-node .status.online { background: var(--success); }
.network-node .status.offline { background: var(--text-dim); }
.node-ports { font-size: 0.7rem; color: var(--text-dim); margin-top: 2px; }
/* ── Calendar ─────────────────────────── */
.calendar-grid {
display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px;
}
.cal-day-header {
text-align: center; font-size: 0.75rem; color: var(--text-dim);
padding: 8px 0; font-weight: 600;
}
.cal-day {
aspect-ratio: 1; display: flex; align-items: center; justify-content: center;
border-radius: var(--radius-sm); cursor: pointer;
font-size: 0.85rem; transition: var(--transition);
position: relative;
}
.cal-day:hover { background: var(--surface2); }
.cal-day.today { border: 2px solid var(--accent); font-weight: 700; }
.cal-day.has-event::after {
content: ''; position: absolute; bottom: 4px;
width: 5px; height: 5px; border-radius: 50%; background: var(--accent);
}
.cal-day.other-month { color: var(--border); }
.event-item {
display: flex; align-items: center; gap: 10px;
padding: 10px 14px; border-left: 3px solid var(--accent);
background: var(--surface2); border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
margin-bottom: 6px;
}
.event-time { font-size: 0.78rem; color: var(--text-dim); min-width: 50px; }
.event-title { font-size: 0.88rem; }
/* ── Password list ────────────────────── */
.pw-item {
display: flex; align-items: center; gap: 12px;
padding: 10px 14px; border: 1px solid var(--border);
border-radius: var(--radius-sm); margin-bottom: 6px;
transition: var(--transition);
}
.pw-item:hover { border-color: var(--accent); }
.pw-icon { font-size: 1.3rem; }
.pw-info { flex: 1; }
.pw-title { font-size: 0.88rem; font-weight: 500; }
.pw-user { font-size: 0.75rem; color: var(--text-dim); }
.pw-actions { display: flex; gap: 6px; }
/* ── Files table ──────────────────────── */
.file-table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
.file-table th {
text-align: left; padding: 10px 12px;
border-bottom: 1px solid var(--border);
color: var(--text-dim); font-weight: 600; font-size: 0.78rem;
text-transform: uppercase;
}
.file-table td { padding: 8px 12px; border-bottom: 1px solid var(--border); }
.file-table tr:hover td { background: var(--surface2); }
/* ── Photo grid ───────────────────────── */
.photo-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 10px; }
.photo-thumb {
aspect-ratio: 1; background: var(--surface2);
border-radius: var(--radius-sm); overflow: hidden;
cursor: pointer; transition: var(--transition); position: relative;
}
.photo-thumb:hover { transform: scale(1.03); }
.photo-thumb img { width: 100%; height: 100%; object-fit: cover; }
.photo-face-badge {
position: absolute; top: 6px; right: 6px;
background: var(--accent2); color: #fff;
font-size: 0.7rem; padding: 2px 8px; border-radius: 10px;
}
/* ── Modal ────────────────────────────── */
.modal-overlay {
display: none; position: fixed; inset: 0;
background: rgba(0,0,0,0.7); z-index: 100;
align-items: center; justify-content: center;
}
.modal-overlay.show { display: flex; }
.modal {
background: var(--surface); border: 1px solid var(--border);
border-radius: var(--radius); padding: 24px; max-width: 500px;
width: 90%; max-height: 80vh; overflow-y: auto;
}
.modal h2 { margin-bottom: 16px; font-size: 1.1rem; }
.modal-actions { display: flex; gap: 10px; justify-content: flex-end; margin-top: 20px; }
/* ── Responsive ───────────────────────── */
@media (max-width: 768px) {
.sidebar { width: 56px; }
.sidebar .nav-item span:not(.icon) { display: none; }
.sidebar .badge { display: none; }
.sidebar-logo { font-size: 1.3rem; text-align: center; padding: 0 0 16px; }
.sidebar-logo span:last-child { display: none; }
.grid-2 { grid-template-columns: 1fr; }
.content { padding: 16px; }
}
</style>
</head>
<body>
<!-- ═══════════════ SIDEBAR ═══════════════ -->
<nav class="sidebar">
<div class="sidebar-logo"><span></span> Home<span> Dashboard</span></div>
<div class="nav-item active" data-tab="home"><span class="icon">🏠</span><span>Overzicht</span></div>
<div class="nav-item" data-tab="network"><span class="icon">🌐</span><span>Netwerk</span><span class="badge" id="net-count">--</span></div>
<div class="nav-item" data-tab="systems"><span class="icon">🖥️</span><span>Systemen</span></div>
<div class="nav-item" data-tab="config"><span class="icon">⚙️</span><span>Config</span></div>
<div class="nav-item" data-tab="control"><span class="icon">🎮</span><span>Control Center</span></div>
<div class="nav-item" data-tab="calendar"><span class="icon">📅</span><span>Agenda</span></div>
<div class="nav-item" data-tab="files"><span class="icon">📁</span><span>Bestanden</span></div>
<div class="nav-item" data-tab="photos"><span class="icon">🖼️</span><span>Foto's</span></div>
<div class="nav-item" data-tab="passwords"><span class="icon">🔑</span><span>Wachtwoorden</span></div>
<div class="nav-item" data-tab="links"><span class="icon"></span><span>Quick Links</span></div>
</nav>
<!-- ═══════════════ MAIN ═══════════════ -->
<div class="main">
<div class="topbar">
<h1 id="page-title">Overzicht</h1>
<div class="topbar-right">
<div class="status-dot" id="status-dot"></div>
<span id="clock">--:--:--</span>
<span id="date-display"></span>
</div>
</div>
<div class="content" id="content">
<!-- TAB: Home/Overzicht -->
<div class="tab-content active" id="tab-home">
<div class="grid-2">
<div class="card">
<div class="card-header"><h3>📊 Stats</h3></div>
<div class="grid-3" id="home-stats"></div>
</div>
<div class="card">
<div class="card-header"><h3>📅 Vandaag</h3></div>
<div id="home-today-events"><span style="color:var(--text-dim)">Laden...</span></div>
</div>
</div>
<div class="card">
<div class="card-header"><h3>⚡ Quick Links</h3><button class="btn btn-sm" onclick="switchTab('links')">Alle</button></div>
<div class="grid-3" id="home-links"></div>
</div>
</div>
<!-- TAB: Netwerk -->
<div class="tab-content" id="tab-network">
<div class="card">
<div class="card-header"><h3>🌐 Thuisnetwerk 192.168.1.0/24</h3><span id="net-summary" style="color:var(--text-dim);font-size:0.85rem">Laden...</span></div>
<div style="display:flex; flex-wrap:wrap; gap:6px; margin-bottom:16px" id="network-nodes"></div>
<div class="card" style="background:var(--surface2);margin-top:8px">
<div class="card-header"><h4>📊 Poort-statistieken</h4></div>
<div id="port-stats" style="display:flex; flex-wrap:wrap; gap:8px"></div>
</div>
<div class="card" style="background:var(--surface2);margin-top:8px">
<div class="card-header"><h4>📋 Laatste scans</h4></div>
<div id="scan-history" style="color:var(--text-dim);font-size:0.85rem"></div>
</div>
</div>
</div>
<!-- TAB: Systemen -->
<div class="tab-content" id="tab-systems">
<div class="card">
<div class="card-header"><h3>🖥️ Alle Systemen</h3>
<div style="display:flex;gap:8px">
<select id="sys-filter" onchange="loadSystems()" style="width:auto">
<option value="">Alle systemen</option>
<option value="Windows">Windows</option>
<option value="Linux">Linux</option>
<option value="ESP32">ESP32 IoT</option>
<option value="UniFi">UniFi</option>
<option value="Router">Router/Gateway</option>
</select>
</div>
</div>
<div id="systems-list"></div>
</div>
</div>
<!-- TAB: Config -->
<div class="tab-content" id="tab-config">
<div class="card">
<div class="card-header"><h3>⚙️ Configuratie & Status</h3></div>
<div id="config-content"><span style="color:var(--text-dim)">Laden...</span></div>
</div>
</div>
<!-- TAB: Control Center -->
<div class="tab-content" id="tab-control">
<div class="card">
<div class="card-header"><h3>💡 Lampen</h3></div>
<div class="grid-3" id="lights-grid"><span style="color:var(--text-dim)">Laden...</span></div>
</div>
</div>
<!-- TAB: Calendar -->
<div class="tab-content" id="tab-calendar">
<div class="grid-2">
<div class="card">
<div class="card-header"><h3 id="cal-month-label"></h3>
<div style="display:flex; gap:6px">
<button class="btn btn-sm" onclick="calShift(-1)">&lt;</button>
<button class="btn btn-sm" onclick="calShift(0)">Vandaag</button>
<button class="btn btn-sm" onclick="calShift(1)">&gt;</button>
</div>
</div>
<div class="calendar-grid" id="calendar-grid"></div>
</div>
<div class="card">
<div class="card-header"><h3 id="cal-date-label">Events</h3>
<button class="btn btn-sm btn-primary" onclick="showEventModal()">+ Nieuw</button>
</div>
<div id="event-list"><span style="color:var(--text-dim)">Selecteer een dag</span></div>
</div>
</div>
</div>
<!-- TAB: Files -->
<div class="tab-content" id="tab-files">
<div class="card">
<div class="card-header"><h3>📁 Bestanden</h3>
<input type="text" placeholder="Zoeken..." style="width:200px" oninput="loadFiles(this.value)">
</div>
<div style="overflow-x:auto"><table class="file-table" id="file-table"></table></div>
<div style="margin-top:12px; color:var(--text-dim); font-size:0.82rem" id="file-count"></div>
</div>
</div>
<!-- TAB: Photos -->
<div class="tab-content" id="tab-photos">
<div class="card">
<div class="card-header"><h3>🖼️ Foto's</h3>
<select id="photo-filter" onchange="loadPhotos()" style="width:auto">
<option value="">Alle foto's</option>
</select>
</div>
<div class="photo-grid" id="photo-grid"><span style="color:var(--text-dim)">Nog geen foto's geindexeerd</span></div>
</div>
</div>
<!-- TAB: Passwords -->
<div class="tab-content" id="tab-passwords">
<div class="card">
<div class="card-header"><h3>🔑 Opgeslagen wachtwoorden</h3>
<div style="display:flex; gap:8px">
<input type="text" placeholder="Zoeken..." style="width:180px" oninput="loadPasswords(this.value)">
<button class="btn btn-primary btn-sm" onclick="showPasswordModal()">+ Nieuw</button>
</div>
</div>
<div id="password-list"></div>
</div>
</div>
<!-- TAB: Quick Links -->
<div class="tab-content" id="tab-links">
<div class="card">
<div class="card-header"><h3>⚡ Favorieten & Quick Links</h3>
<button class="btn btn-primary btn-sm" onclick="showLinkModal()">+ Nieuw</button>
</div>
<div class="grid-3" id="all-links"></div>
</div>
</div>
</div>
</div>
<!-- ═══════════════ MODALS ═══════════════ -->
<div class="modal-overlay" id="modal-overlay">
<div class="modal" id="modal-content"></div>
</div>
<script>
// ═══════════ STATE ═══════════
const API = '/api/dashboard';
let activeTab = 'home';
let calYear, calMonth, calSelectedDate;
// ═══════════ NAVIGATION ═══════════
document.querySelectorAll('.nav-item').forEach(item => {
item.addEventListener('click', () => switchTab(item.dataset.tab));
});
function switchTab(tab) {
activeTab = tab;
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
document.querySelector(`.nav-item[data-tab="${tab}"]`)?.classList.add('active');
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
document.getElementById(`tab-${tab}`)?.classList.add('active');
document.getElementById('page-title').textContent =
document.querySelector(`.nav-item[data-tab="${tab}"] span:last-child`)?.textContent || tab;
loadTab(tab);
}
function loadTab(tab) {
switch(tab) {
case 'home': loadOverview(); break;
case 'network': loadNetwork(); break;
case 'systems': loadSystems(); break;
case 'config': loadConfig(); break;
case 'control': loadLights(); break;
case 'calendar': loadCalendar(); break;
case 'files': loadFiles(); break;
case 'photos': loadPhotos(); break;
case 'passwords': loadPasswords(); break;
case 'links': loadLinks(); break;
}
}
// ═══════════ CLOCK ═══════════
function updateClock() {
const now = new Date();
document.getElementById('clock').textContent = now.toLocaleTimeString('nl-NL');
document.getElementById('date-display').textContent = now.toLocaleDateString('nl-NL', {weekday:'long', day:'numeric', month:'long'});
}
setInterval(updateClock, 1000); updateClock();
// ═══════════ OVERVIEW ═══════════
async function loadOverview() {
try {
const r = await fetch(`${API}/overview`); const d = await r.json();
document.getElementById('home-stats').innerHTML = `
<div class="card" style="text-align:center"><div style="font-size:2rem">⚡</div><div style="font-size:1.5rem;font-weight:700">${d.favorites_count||0}</div><div style="font-size:0.8rem;color:var(--text-dim)">Favorieten</div></div>
<div class="card" style="text-align:center"><div style="font-size:2rem">🌐</div><div style="font-size:1.5rem;font-weight:700">${d.network_devices||0}</div><div style="font-size:0.8rem;color:var(--text-dim)">Netwerk Devices</div></div>
<div class="card" style="text-align:center"><div style="font-size:2rem">🔑</div><div style="font-size:1.5rem;font-weight:700">${d.password_count||0}</div><div style="font-size:0.8rem;color:var(--text-dim)">Wachtwoorden</div></div>
<div class="card" style="text-align:center"><div style="font-size:2rem">🖼️</div><div style="font-size:1.5rem;font-weight:700">${d.photo_count||0}</div><div style="font-size:0.8rem;color:var(--text-dim)">Foto's</div></div>
`;
// Update sidebar network count
document.getElementById('net-count').textContent = d.network_devices || '--';
const todayEv = (d.today_events||[]).map(e => `<div class="event-item"><span class="event-time">${e.event_start?.split('T')[1]?.substring(0,5)||'--:--'}</span><span class="event-title">${esc(e.title)}</span></div>`).join('') || '<span style="color:var(--text-dim)">Geen events vandaag</span>';
document.getElementById('home-today-events').innerHTML = todayEv;
const links = await fetch(`${API}/favorites`).then(r=>r.json());
document.getElementById('home-links').innerHTML = (links||[]).slice(0,6).map(l => `<a class="quick-link" href="${esc(l.url)}" target="_blank"><span class="ql-icon">${l.icon||'⭐'}</span><div class="ql-info"><div class="ql-title">${esc(l.title)}</div><div class="ql-url">${esc(l.url)}</div></div></a>`).join('');
} catch(e) { console.error(e); }
}
// ═══════════ NETWORK (live uit Neo4j) ═══════════
async function loadNetwork() {
try {
const r = await fetch(`${API}/network`);
if (!r.ok) throw new Error('API niet beschikbaar');
const d = await r.json();
document.getElementById('net-summary').textContent =
`${d.summary.total_devices} devices • ${d.summary.total_ports} unieke poorten`;
// Device nodes
document.getElementById('network-nodes').innerHTML = (d.devices||[]).map(dev => {
const osIcon = dev.os_guess.includes('Windows') ? '🪟' :
dev.os_guess.includes('ESP32') ? '📡' :
dev.os_guess.includes('Router') ? '📶' :
dev.os_guess.includes('UniFi') ? '📡' :
dev.os_guess.includes('Home Assistant') ? '🏠' :
dev.os_guess.includes('Synology') ? '🗄️' :
dev.os_guess.includes('Linux') ? '🐧' : '💻';
return `
<div class="network-node" onclick="showSystemDetail('${dev.ip}')" title="Klik voor details">
<div class="status online"></div>
<div><strong>${osIcon} ${esc(dev.hostname||dev.ip)}</strong><br>
<span class="node-ports">${dev.ip}${(dev.open_ports||[]).join(', ')||'geen open poorten'}</span></div>
</div>`;
}).join('');
// Poort stats
document.getElementById('port-stats').innerHTML = (d.summary.top_ports||[]).map(p => `
<div class="network-node" style="cursor:default">
<span style="font-weight:600">:${p.port}</span>
<span style="font-size:0.75rem;color:var(--text-dim)">${p.count}x</span>
</div>
`).join('');
// Scan history
document.getElementById('scan-history').innerHTML = (d.scan_history||[]).map(s => `
<div style="margin-bottom:4px">📅 ${(s.timestamp||'').substring(0,16).replace('T',' ')}${s.id}${s.hosts_active} hosts</div>
`).join('') || 'Geen scan historie';
// Update sidebar badge
document.getElementById('net-count').textContent = d.summary.total_devices;
} catch(e) {
document.getElementById('network-nodes').innerHTML =
'<span style="color:var(--danger)">Kan netwerkdata niet laden. Draait de web server met Neo4j?</span>';
console.error(e);
}
}
// ═══════════ SYSTEMS ═══════════
async function loadSystems() {
try {
const filter = document.getElementById('sys-filter')?.value || '';
const url = filter ? `${API}/systems?os=${encodeURIComponent(filter)}` : `${API}/systems`;
const systems = await fetch(url).then(r => r.json());
document.getElementById('systems-list').innerHTML = systems.map(s => {
const ports = (s.open_ports||[]).map(p => {
let svc = '';
const common = {22:'SSH',53:'DNS',80:'HTTP',443:'HTTPS',445:'SMB',3000:'Grafana',3306:'MySQL',3389:'RDP',5000:'DSM',5432:'PostgreSQL',5433:'PostgreSQL',8000:'HTTP',8080:'HTTP',8123:'HA API',8443:'HTTPS',9000:'Portainer',9443:'UniFi',49153:'Neo4j',49154:'Neo4j Browser'};
if (common[p]) svc = common[p];
return `<span style="background:var(--surface2);padding:2px 8px;border-radius:4px;font-size:0.78rem;margin:2px">:${p}${svc?' '+svc:''}</span>`;
}).join('');
const osIcon = s.os_guess?.includes('Windows') ? '🪟' :
s.os_guess?.includes('ESP32') ? '📡' :
s.os_guess?.includes('Router') ? '📶' :
s.os_guess?.includes('UniFi') ? '📡' :
s.os_guess?.includes('Home Assistant') ? '🏠' :
s.os_guess?.includes('Synology') ? '🗄️' :
s.os_guess?.includes('Stofzuiger') ? '🧹' :
s.os_guess?.includes('Linux') ? '🐧' : '💻';
return `
<div class="pw-item" style="cursor:pointer" onclick="showSystemDetail('${s.ip}')">
<span style="font-size:1.5rem">${osIcon}</span>
<div class="pw-info">
<div class="pw-title">${esc(s.hostname||s.ip)} <span style="font-size:0.75rem;color:var(--text-dim)">${s.ip}</span></div>
<div style="font-size:0.78rem;color:var(--text-dim)">${esc(s.os_guess||'Onbekend')}${s.mac?' • MAC: '+s.mac:''}</div>
<div style="margin-top:4px">${ports||'<span style="color:var(--text-dim)">geen open poorten</span>'}</div>
</div>
<span style="color:var(--text-dim);font-size:0.75rem">${s.port_count} poorten</span>
</div>`;
}).join('') || '<span style="color:var(--text-dim)">Geen systemen gevonden</span>';
} catch(e) {
document.getElementById('systems-list').innerHTML =
'<span style="color:var(--danger)">Kan systemen niet laden. Is de Neo4j database bereikbaar?</span>';
console.error(e);
}
}
// ═══════════ SYSTEM DETAIL ═══════════
async function showSystemDetail(ip) {
try {
const sys = await fetch(`${API}/systems/${ip}`).then(r => r.json());
const portsHtml = (sys.ports||[]).map(p => `
<tr><td>:${p.port}</td><td>${p.service||'?'}</td><td style="color:var(--text-dim);font-size:0.8rem">${esc(p.banner||'')}</td></tr>
`).join('');
document.getElementById('modal-content').innerHTML = `
<h2>🖥️ ${esc(sys.hostname||sys.ip)}</h2>
<div style="display:grid;grid-template-columns:120px 1fr;gap:8px;margin:16px 0;font-size:0.9rem">
<div style="color:var(--text-dim)">IP</div><div><strong>${sys.ip}</strong></div>
<div style="color:var(--text-dim)">MAC</div><div>${sys.mac||'onbekend'}</div>
<div style="color:var(--text-dim)">OS</div><div>${esc(sys.os_guess||'Onbekend')}</div>
<div style="color:var(--text-dim)">Eerste scan</div><div>${(sys.first_seen||'').substring(0,16).replace('T',' ')}</div>
<div style="color:var(--text-dim)">Laatste scan</div><div>${(sys.last_seen||'').substring(0,16).replace('T',' ')}</div>
</div>
<h4 style="margin-bottom:8px">Open poorten (${sys.ports?.length||0})</h4>
<table class="file-table" style="width:100%">
<tr><th>Poort</th><th>Service</th><th>Banner</th></tr>
${portsHtml||'<tr><td colspan="3">Geen open poorten</td></tr>'}
</table>
<div class="modal-actions">
<button class="btn" onclick="closeModal()">Sluiten</button>
</div>`;
document.getElementById('modal-overlay').classList.add('show');
} catch(e) {
alert('Kan systeem-details niet laden: ' + e.message);
}
}
// ═══════════ CONFIG ═══════════
async function loadConfig() {
try {
const cfg = await fetch(`${API}/config`).then(r => r.json());
const connStatus = (connected) => connected
? '<span style="color:var(--success)">● verbonden</span>'
: '<span style="color:var(--warning)">○ niet geconfigureerd</span>';
document.getElementById('config-content').innerHTML = `
<div class="grid-2">
<div class="card" style="background:var(--surface2)">
<h4>🔌 Connecties</h4>
<div style="font-size:0.85rem;margin-top:8px">
<div style="margin-bottom:6px"><strong>Home Assistant</strong><br>${cfg.connections.home_assistant.url} ${connStatus(cfg.connections.home_assistant.connected)}</div>
<div style="margin-bottom:6px"><strong>PostgreSQL</strong><br>${cfg.connections.postgresql.host}:${cfg.connections.postgresql.port}/${cfg.connections.postgresql.database} (${cfg.connections.postgresql.user})</div>
<div style="margin-bottom:6px"><strong>Neo4j</strong><br>${cfg.connections.neo4j.uri} ${cfg.connections.neo4j.available ? '<span style="color:var(--success)">● bereikbaar</span>' : '<span style="color:var(--danger)">✗ niet bereikbaar</span>'}<br>
<span style="color:var(--text-dim)">${cfg.connections.neo4j.devices} devices • laatste scan: ${(cfg.connections.neo4j.last_scan||'nooit').substring(0,16).replace('T',' ')}</span></div>
</div>
</div>
<div class="card" style="background:var(--surface2)">
<h4>📊 Database Status</h4>
<div style="font-size:0.85rem;margin-top:8px;display:grid;grid-template-columns:1fr 1fr;gap:4px">
<div>Favorieten:</div><div><strong>${cfg.counts.favorites}</strong></div>
<div>Wachtwoorden:</div><div><strong>${cfg.counts.passwords}</strong></div>
<div>Agenda events:</div><div><strong>${cfg.counts.calendar_events}</strong></div>
<div>Bestanden:</div><div><strong>${cfg.counts.files_indexed}</strong></div>
<div>Foto's:</div><div><strong>${cfg.counts.photos_indexed}</strong></div>
<div>Netwerk devices:</div><div><strong>${cfg.counts.network_devices}</strong></div>
</div>
</div>
</div>
<div class="card" style="background:var(--surface2);margin-top:16px">
<h4>⚙️ Settings</h4>
<div style="font-size:0.85rem;margin-top:8px">
${Object.entries(cfg.settings||{}).map(([k,v]) => `<div style="margin-bottom:4px"><span style="color:var(--text-dim)">${k}:</span> <strong>${esc(v)}</strong></div>`).join('')}
</div>
</div>
<div class="card" style="background:var(--surface2);margin-top:16px">
<h4>🖥 Server</h4>
<div style="font-size:0.85rem;margin-top:8px">
<div>Host: ${cfg.server.host}:${cfg.server.port}</div>
<div>Whisper: ${cfg.server.whisper_mode} (${cfg.server.whisper_model}) op ${cfg.server.whisper_device}</div>
</div>
</div>`;
} catch(e) {
document.getElementById('config-content').innerHTML =
'<span style="color:var(--danger)">Kan config niet laden.</span>';
console.error(e);
}
}
// ═══════════ LIGHTS ═══════════
async function loadLights() {
try {
const r = await fetch('/api/lights'); const lights = await r.json();
document.getElementById('lights-grid').innerHTML = lights.map(l => `
<div class="card" style="cursor:pointer" onclick="toggleLight('${l.entity_id}','${l.state==='on'?'turn_off':'turn_on'}')">
<div style="display:flex;align-items:center;gap:12px">
<span style="font-size:2rem">${l.state==='on'?'💡':'🌑'}</span>
<div style="flex:1">
<div style="font-weight:500">${esc(l.friendly_name||l.entity_id)}</div>
<div style="font-size:0.8rem;color:var(--text-dim)">${l.state==='on'?'Aan'+(l.brightness?` ${Math.round(l.brightness/2.55)}%`:'') : 'Uit'}</div>
</div>
</div>
</div>
`).join('') || '<span style="color:var(--text-dim)">Geen lampen</span>';
} catch(e) { document.getElementById('lights-grid').innerHTML = '<span style="color:var(--danger)">Fout bij laden</span>'; }
}
async function toggleLight(entityId, action) {
await fetch('/api/light/control', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({entity_id:entityId, action})
});
setTimeout(loadLights, 300);
}
// ═══════════ CALENDAR ═══════════
async function loadCalendar() {
const now = new Date();
if (!calYear) { calYear = now.getFullYear(); calMonth = now.getMonth(); }
const months = ['Jan','Feb','Mrt','Apr','Mei','Jun','Jul','Aug','Sep','Okt','Nov','Dec'];
document.getElementById('cal-month-label').textContent = `${months[calMonth]} ${calYear}`;
const firstDay = new Date(calYear, calMonth, 1).getDay(); // 0=Sun
const daysInMonth = new Date(calYear, calMonth+1, 0).getDate();
const prevDays = new Date(calYear, calMonth, 0).getDate();
// Haal events
let events = [];
try {
events = await fetch(`${API}/calendar?days=90`).then(r=>r.json());
} catch(e) {}
const eventDays = new Set(events.map(e => e.event_start?.substring(0,10)));
let html = '<div class="cal-day-header">Zo</div><div class="cal-day-header">Ma</div><div class="cal-day-header">Di</div><div class="cal-day-header">Wo</div><div class="cal-day-header">Do</div><div class="cal-day-header">Vr</div><div class="cal-day-header">Za</div>';
const today = new Date().toISOString().substring(0,10);
for (let i = firstDay-1; i >= 0; i--) {
html += `<div class="cal-day other-month">${prevDays - i}</div>`;
}
for (let d = 1; d <= daysInMonth; d++) {
const dateStr = `${calYear}-${String(calMonth+1).padStart(2,'0')}-${String(d).padStart(2,'0')}`;
const classes = ['cal-day'];
if (dateStr === calSelectedDate) classes.push('selected');
if (dateStr === today) classes.push('today');
if (eventDays.has(dateStr)) classes.push('has-event');
html += `<div class="${classes.join(' ')}" onclick="selectDate('${dateStr}')">${d}</div>`;
}
const remaining = 42 - (firstDay + daysInMonth);
for (let d = 1; d <= remaining; d++) {
html += `<div class="cal-day other-month">${d}</div>`;
}
document.getElementById('calendar-grid').innerHTML = html;
if (calSelectedDate) selectDate(calSelectedDate);
}
function calShift(dir) {
if (dir === 0) { const n=new Date(); calYear=n.getFullYear(); calMonth=n.getMonth(); }
else { calMonth += dir; if(calMonth<0){calMonth=11;calYear--;} if(calMonth>11){calMonth=0;calYear++;} }
loadCalendar();
}
async function selectDate(dateStr) {
calSelectedDate = dateStr;
document.getElementById('cal-date-label').textContent = `Events — ${dateStr}`;
try {
const events = await fetch(`${API}/calendar?days=90`).then(r=>r.json());
const dayEvents = events.filter(e => e.event_start?.substring(0,10) === dateStr);
document.getElementById('event-list').innerHTML = dayEvents.map(e => `
<div class="event-item" style="border-left-color:${e.color||'var(--accent)'}">
<span class="event-time">${e.event_start?.split('T')[1]?.substring(0,5)||'--:--'}</span>
<span class="event-title">${esc(e.title)}</span>
<button class="btn btn-sm btn-danger" style="margin-left:auto" onclick="deleteEvent(${e.id})">x</button>
</div>
`).join('') || '<span style="color:var(--text-dim)">Geen events op deze dag</span>';
} catch(e) {}
loadCalendar();
}
function showEventModal() {
document.getElementById('modal-content').innerHTML = `
<h2>Nieuw event</h2>
<div class="form-group"><label>Titel</label><input id="ev-title"></div>
<div class="form-row">
<div class="form-group"><label>Datum</label><input type="date" id="ev-date" value="${calSelectedDate||new Date().toISOString().substring(0,10)}"></div>
<div class="form-group"><label>Tijd</label><input type="time" id="ev-time" value="09:00"></div>
</div>
<div class="modal-actions">
<button class="btn" onclick="closeModal()">Annuleren</button>
<button class="btn btn-primary" onclick="saveEvent()">Opslaan</button>
</div>`;
document.getElementById('modal-overlay').classList.add('show');
}
async function saveEvent() {
const title = document.getElementById('ev-title').value;
const date = document.getElementById('ev-date').value;
const time = document.getElementById('ev-time').value;
if(!title){alert('Titel is verplicht');return;}
await fetch(`${API}/calendar`, {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({title, event_start: `${date}T${time}:00`, all_day: false})
});
closeModal(); loadCalendar();
}
async function deleteEvent(id) {
await fetch(`${API}/calendar/${id}`, {method:'DELETE'});
loadCalendar(); selectDate(calSelectedDate);
}
// ═══════════ FILES ═══════════
async function loadFiles(search='') {
try {
const r = await fetch(`${API}/files?search=${encodeURIComponent(search)}&limit=100`);
const d = await r.json();
document.getElementById('file-count').textContent = `${d.total||0} bestanden`;
document.getElementById('file-table').innerHTML = `
<tr><th>Naam</th><th>Type</th><th>Grootte</th><th>Locatie</th><th>Datum</th></tr>
${(d.files||[]).map(f => `
<tr>
<td>${esc(f.file_name)}</td>
<td>${f.file_type||'-'}</td>
<td>${formatSize(f.file_size)}</td>
<td>${esc(f.source_host||'')}</td>
<td>${f.last_modified?.substring(0,10)||'-'}</td>
</tr>`).join('')}`;
} catch(e) { document.getElementById('file-table').innerHTML = '<tr><td colspan="5">Geen bestanden geindexeerd. Run de file scanner.</td></tr>'; }
}
function formatSize(bytes) {
if (!bytes) return '-';
const units = ['B','KB','MB','GB','TB'];
let i = 0; while (bytes >= 1024 && i < 4) { bytes /= 1024; i++; }
return `${bytes.toFixed(1)} ${units[i]}`;
}
// ═══════════ PHOTOS ═══════════
async function loadPhotos() {
try {
// Laad persons voor filter
const persons = await fetch(`${API}/photos/persons`).then(r=>r.json());
const sel = document.getElementById('photo-filter');
sel.innerHTML = '<option value="">Alle foto\'s</option>' + persons.map(p => `<option value="${p.id}">${esc(p.name)} (${p.photo_count})</option>`).join('');
const personId = sel.value;
const url = personId ? `${API}/photos?person_id=${personId}&limit=50` : `${API}/photos?limit=50`;
const photos = await fetch(url).then(r=>r.json());
document.getElementById('photo-grid').innerHTML = photos.map(p => `
<div class="photo-thumb">
<div style="display:flex;align-items:center;justify-content:center;height:100%;color:var(--text-dim);font-size:0.8rem">
📷 ${esc(p.file_name||'')}
${p.faces_detected ? `<div class="photo-face-badge">${p.faces_detected} 👤</div>` : ''}
</div>
</div>
`).join('') || '<span style="color:var(--text-dim);grid-column:1/-1">Nog geen foto\'s geindexeerd. Gebruik de file scanner met face recognition.</span>';
} catch(e) { console.error(e); }
}
// ═══════════ PASSWORDS ═══════════
async function loadPasswords(search='') {
try {
const url = search ? `${API}/passwords?search=${encodeURIComponent(search)}` : `${API}/passwords`;
const pws = await fetch(url).then(r=>r.json());
document.getElementById('password-list').innerHTML = pws.map(p => `
<div class="pw-item">
<span class="pw-icon">🔐</span>
<div class="pw-info">
<div class="pw-title">${esc(p.title)}</div>
<div class="pw-user">${esc(p.username||'')} ${p.url ? '• '+esc(p.url) : ''}</div>
</div>
<div class="pw-actions">
<button class="btn btn-sm" onclick="copyPassword(${p.id})">📋</button>
<button class="btn btn-sm btn-danger" onclick="deletePassword(${p.id})">x</button>
</div>
</div>
`).join('') || '<span style="color:var(--text-dim)">Geen wachtwoorden</span>';
} catch(e) { console.error(e); }
}
async function copyPassword(id) {
const r = await fetch(`${API}/passwords/${id}`); const d = await r.json();
await navigator.clipboard.writeText(d.password);
alert('Wachtwoord gekopieerd!');
}
function showPasswordModal() {
document.getElementById('modal-content').innerHTML = `
<h2>Nieuw wachtwoord</h2>
<div class="form-group"><label>Titel</label><input id="pw-title"></div>
<div class="form-row">
<div class="form-group"><label>Gebruikersnaam</label><input id="pw-user"></div>
<div class="form-group"><label>Wachtwoord</label><input type="password" id="pw-pass"></div>
</div>
<div class="form-group"><label>URL</label><input id="pw-url"></div>
<div class="modal-actions">
<button class="btn" onclick="closeModal()">Annuleren</button>
<button class="btn btn-primary" onclick="savePassword()">Opslaan</button>
</div>`;
document.getElementById('modal-overlay').classList.add('show');
}
async function savePassword() {
const title=document.getElementById('pw-title').value, username=document.getElementById('pw-user').value;
const password=document.getElementById('pw-pass').value, url=document.getElementById('pw-url').value;
if(!title||!password){alert('Titel en wachtwoord zijn verplicht');return;}
await fetch(`${API}/passwords`, {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({title,username,password,url})
});
closeModal(); loadPasswords();
}
async function deletePassword(id) {
if(!confirm('Verwijderen?'))return;
await fetch(`${API}/passwords/${id}`, {method:'DELETE'});
loadPasswords();
}
// ═══════════ QUICK LINKS ═══════════
async function loadLinks() {
try {
const links = await fetch(`${API}/favorites`).then(r=>r.json());
const grouped = {}; links.forEach(l => { if(!grouped[l.category])grouped[l.category]=[]; grouped[l.category].push(l); });
let html = '';
for(const [cat, items] of Object.entries(grouped)) {
html += `<div style="grid-column:1/-1;margin-top:12px;color:var(--text-dim);font-size:0.8rem;font-weight:600;text-transform:uppercase">${esc(cat)}</div>`;
html += items.map(l => `<a class="quick-link" href="${esc(l.url)}" target="_blank"><span class="ql-icon">${l.icon||'⭐'}</span><div class="ql-info"><div class="ql-title">${esc(l.title)}</div><div class="ql-url">${esc(l.url)}</div></div></a>`).join('');
}
document.getElementById('all-links').innerHTML = html || '<span style="color:var(--text-dim)">Geen links</span>';
} catch(e) { console.error(e); }
}
function showLinkModal() {
document.getElementById('modal-content').innerHTML = `
<h2>Nieuwe Quick Link</h2>
<div class="form-group"><label>Titel</label><input id="ql-title"></div>
<div class="form-group"><label>URL</label><input id="ql-url"></div>
<div class="form-row">
<div class="form-group"><label>Icoon</label><input id="ql-icon" value="⭐"></div>
<div class="form-group"><label>Categorie</label><input id="ql-cat" value="Algemeen"></div>
</div>
<div class="modal-actions">
<button class="btn" onclick="closeModal()">Annuleren</button>
<button class="btn btn-primary" onclick="saveLink()">Opslaan</button>
</div>`;
document.getElementById('modal-overlay').classList.add('show');
}
async function saveLink() {
const title=document.getElementById('ql-title').value, url=document.getElementById('ql-url').value;
const icon=document.getElementById('ql-icon').value, category=document.getElementById('ql-cat').value;
if(!title||!url){alert('Titel en URL zijn verplicht');return;}
await fetch(`${API}/favorites`, {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({title,url,icon,category})
});
closeModal(); loadLinks();
}
// ═══════════ MODAL ═══════════
function closeModal() { document.getElementById('modal-overlay').classList.remove('show'); }
document.getElementById('modal-overlay').addEventListener('click', function(e) { if(e.target===this) closeModal(); });
// ═══════════ HELPERS ═══════════
function esc(s) { if(!s) return ''; const d=document.createElement('div'); d.textContent=s; return d.innerHTML; }
// ═══════════ INIT ═══════════
loadOverview();
</script>
</body>
</html>