951 lines
47 KiB
HTML
951 lines
47 KiB
HTML
|
|
<!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)"><</button>
|
|||
|
|
<button class="btn btn-sm" onclick="calShift(0)">Vandaag</button>
|
|||
|
|
<button class="btn btn-sm" onclick="calShift(1)">></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>
|