Files

951 lines
47 KiB
HTML
Raw Permalink 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.
<!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>