1257 lines
45 KiB
HTML
1257 lines
45 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>CardSync — Admin</title>
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=DM+Mono:wght@400;500&family=DM+Sans:opsz,wght@9..40,300;9..40,400;9..40,500;9..40,600&display=swap" rel="stylesheet">
|
||
<style>
|
||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||
|
||
:root {
|
||
--bg: #0a0b0f;
|
||
--sidebar: #0e0f14;
|
||
--surface: #14161d;
|
||
--surface2: #1a1c25;
|
||
--surface3: #20232e;
|
||
--border: #252833;
|
||
--accent: #4f8ef7;
|
||
--accent2: #7c5cfc;
|
||
--success: #3ecf8e;
|
||
--error: #f76c6c;
|
||
--warning: #ffb900;
|
||
--text: #e4e6ef;
|
||
--muted: #6b7090;
|
||
--muted2: #4a4f66;
|
||
}
|
||
|
||
body {
|
||
background: var(--bg);
|
||
color: var(--text);
|
||
font-family: 'DM Sans', sans-serif;
|
||
font-size: 14px;
|
||
line-height: 1.6;
|
||
min-height: 100vh;
|
||
display: flex;
|
||
}
|
||
|
||
/* Sidebar */
|
||
.sidebar {
|
||
width: 240px;
|
||
background: var(--sidebar);
|
||
border-right: 1px solid var(--border);
|
||
display: flex;
|
||
flex-direction: column;
|
||
position: fixed;
|
||
top: 0; left: 0; bottom: 0;
|
||
z-index: 100;
|
||
}
|
||
|
||
.sidebar-logo {
|
||
padding: 24px 20px;
|
||
border-bottom: 1px solid var(--border);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
.logo-icon {
|
||
width: 32px; height: 32px;
|
||
background: linear-gradient(135deg, var(--accent), var(--accent2));
|
||
border-radius: 8px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 16px;
|
||
}
|
||
.logo-text {
|
||
font-family: 'DM Serif Display', serif;
|
||
font-size: 18px;
|
||
letter-spacing: -0.3px;
|
||
}
|
||
.logo-badge {
|
||
font-size: 9px;
|
||
background: rgba(79,142,247,0.15);
|
||
color: var(--accent);
|
||
padding: 2px 6px;
|
||
border-radius: 4px;
|
||
font-weight: 600;
|
||
letter-spacing: 0.5px;
|
||
text-transform: uppercase;
|
||
margin-left: auto;
|
||
}
|
||
|
||
.sidebar-nav {
|
||
flex: 1;
|
||
padding: 16px 0;
|
||
}
|
||
|
||
.nav-section {
|
||
padding: 0 12px;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.nav-label {
|
||
font-size: 10px;
|
||
font-weight: 600;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.8px;
|
||
color: var(--muted2);
|
||
padding: 8px 8px 4px;
|
||
}
|
||
|
||
.nav-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 8px 12px;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
color: var(--muted);
|
||
font-size: 14px;
|
||
transition: all 0.15s;
|
||
border: none;
|
||
background: none;
|
||
width: 100%;
|
||
text-align: left;
|
||
font-family: 'DM Sans', sans-serif;
|
||
}
|
||
.nav-item:hover { background: var(--surface2); color: var(--text); }
|
||
.nav-item.active { background: rgba(79,142,247,0.12); color: var(--accent); }
|
||
.nav-item .icon { font-size: 16px; width: 20px; text-align: center; flex-shrink: 0; }
|
||
|
||
.sidebar-footer {
|
||
padding: 16px 12px;
|
||
border-top: 1px solid var(--border);
|
||
}
|
||
|
||
.admin-badge {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 10px 12px;
|
||
background: var(--surface2);
|
||
border-radius: 8px;
|
||
margin-bottom: 8px;
|
||
}
|
||
.admin-avatar {
|
||
width: 28px; height: 28px;
|
||
background: linear-gradient(135deg, var(--accent), var(--accent2));
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: white;
|
||
}
|
||
.admin-info { flex: 1; min-width: 0; }
|
||
.admin-email { font-size: 12px; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||
.admin-role { font-size: 10px; color: var(--accent); font-weight: 500; }
|
||
|
||
.btn-logout {
|
||
width: 100%;
|
||
padding: 8px;
|
||
background: transparent;
|
||
border: 1px solid var(--border);
|
||
border-radius: 6px;
|
||
color: var(--muted);
|
||
font-family: 'DM Sans', sans-serif;
|
||
font-size: 12px;
|
||
cursor: pointer;
|
||
transition: all 0.15s;
|
||
}
|
||
.btn-logout:hover { border-color: var(--error); color: var(--error); }
|
||
|
||
/* Main Content */
|
||
.main {
|
||
flex: 1;
|
||
margin-left: 240px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
min-height: 100vh;
|
||
}
|
||
|
||
.topbar {
|
||
padding: 16px 32px;
|
||
border-bottom: 1px solid var(--border);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
background: var(--bg);
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 50;
|
||
}
|
||
|
||
.topbar-title {
|
||
font-family: 'DM Serif Display', serif;
|
||
font-size: 20px;
|
||
letter-spacing: -0.3px;
|
||
}
|
||
|
||
.topbar-right {
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: center;
|
||
}
|
||
|
||
/* Content area */
|
||
.content {
|
||
flex: 1;
|
||
padding: 32px;
|
||
}
|
||
|
||
/* Panels (tab views) */
|
||
.panel { display: none; }
|
||
.panel.active { display: block; }
|
||
|
||
/* Stats grid */
|
||
.stats-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||
gap: 16px;
|
||
margin-bottom: 28px;
|
||
}
|
||
|
||
.stat-card {
|
||
background: var(--surface);
|
||
border: 1px solid var(--border);
|
||
border-radius: 12px;
|
||
padding: 20px;
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
.stat-card::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 0; left: 0; right: 0;
|
||
height: 2px;
|
||
background: linear-gradient(90deg, var(--accent), var(--accent2));
|
||
opacity: 0.6;
|
||
}
|
||
.stat-label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.6px; color: var(--muted); margin-bottom: 8px; font-weight: 500; }
|
||
.stat-value { font-family: 'DM Serif Display', serif; font-size: 32px; line-height: 1; }
|
||
.stat-sub { font-size: 12px; color: var(--muted); margin-top: 6px; }
|
||
|
||
/* Card */
|
||
.card {
|
||
background: var(--surface);
|
||
border: 1px solid var(--border);
|
||
border-radius: 14px;
|
||
margin-bottom: 24px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.card-header {
|
||
padding: 18px 24px;
|
||
border-bottom: 1px solid var(--border);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
.card-title { font-size: 15px; font-weight: 600; }
|
||
.card-subtitle { font-size: 12px; color: var(--muted); margin-top: 2px; }
|
||
|
||
.card-body { padding: 24px; }
|
||
|
||
/* Form */
|
||
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
|
||
.form-group { margin-bottom: 16px; }
|
||
.form-group.full { grid-column: 1 / -1; }
|
||
.form-label { display: block; font-size: 12px; font-weight: 500; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 6px; }
|
||
|
||
.input, .select {
|
||
width: 100%;
|
||
padding: 10px 14px;
|
||
background: var(--surface2);
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
color: var(--text);
|
||
font-family: 'DM Sans', sans-serif;
|
||
font-size: 14px;
|
||
outline: none;
|
||
transition: border-color 0.15s;
|
||
}
|
||
.input:focus, .select:focus { border-color: var(--accent); }
|
||
.input[type="password"] { letter-spacing: 2px; }
|
||
.select option { background: var(--surface2); }
|
||
|
||
/* Buttons */
|
||
.btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 9px 18px;
|
||
border-radius: 8px;
|
||
font-family: 'DM Sans', sans-serif;
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
border: none;
|
||
transition: all 0.15s;
|
||
}
|
||
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
||
.btn-primary { background: linear-gradient(135deg, var(--accent), var(--accent2)); color: white; }
|
||
.btn-primary:hover:not(:disabled) { opacity: 0.9; }
|
||
.btn-secondary { background: var(--surface2); color: var(--text); border: 1px solid var(--border); }
|
||
.btn-secondary:hover:not(:disabled) { border-color: var(--accent); }
|
||
.btn-success { background: rgba(62,207,142,0.15); color: var(--success); border: 1px solid rgba(62,207,142,0.25); }
|
||
.btn-success:hover:not(:disabled) { background: rgba(62,207,142,0.25); }
|
||
.btn-danger { background: rgba(247,108,108,0.12); color: var(--error); border: 1px solid rgba(247,108,108,0.2); }
|
||
.btn-danger:hover:not(:disabled) { background: rgba(247,108,108,0.2); }
|
||
.btn-warning { background: rgba(255,185,0,0.12); color: var(--warning); border: 1px solid rgba(255,185,0,0.2); }
|
||
.btn-warning:hover:not(:disabled) { background: rgba(255,185,0,0.2); }
|
||
.btn-sm { padding: 5px 12px; font-size: 12px; border-radius: 6px; }
|
||
|
||
.btn-row { display: flex; gap: 8px; flex-wrap: wrap; }
|
||
|
||
/* Alert */
|
||
.alert {
|
||
padding: 12px 16px;
|
||
border-radius: 8px;
|
||
font-size: 13px;
|
||
margin-bottom: 16px;
|
||
display: none;
|
||
}
|
||
.alert.success { background: rgba(62,207,142,0.1); color: var(--success); border: 1px solid rgba(62,207,142,0.2); display: flex; }
|
||
.alert.error { background: rgba(247,108,108,0.1); color: var(--error); border: 1px solid rgba(247,108,108,0.2); display: flex; }
|
||
.alert.info { background: rgba(79,142,247,0.1); color: var(--accent); border: 1px solid rgba(79,142,247,0.2); display: flex; }
|
||
|
||
/* Table */
|
||
.table-wrap { overflow-x: auto; }
|
||
|
||
table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
}
|
||
|
||
th {
|
||
text-align: left;
|
||
padding: 10px 16px;
|
||
font-size: 11px;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.6px;
|
||
color: var(--muted);
|
||
font-weight: 500;
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
|
||
td {
|
||
padding: 12px 16px;
|
||
border-bottom: 1px solid var(--border);
|
||
vertical-align: middle;
|
||
font-size: 13px;
|
||
}
|
||
|
||
tr:last-child td { border-bottom: none; }
|
||
tr:hover td { background: rgba(255,255,255,0.02); }
|
||
|
||
.badge-status {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
padding: 3px 8px;
|
||
border-radius: 100px;
|
||
font-size: 11px;
|
||
font-weight: 500;
|
||
}
|
||
.badge-status.success { background: rgba(62,207,142,0.12); color: var(--success); }
|
||
.badge-status.error { background: rgba(247,108,108,0.12); color: var(--error); }
|
||
.badge-status.running { background: rgba(255,185,0,0.12); color: var(--warning); }
|
||
.badge-status.off { background: rgba(255,255,255,0.05); color: var(--muted); }
|
||
.dot-indicator { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
||
|
||
.mono { font-family: 'DM Mono', monospace; font-size: 12px; color: var(--muted); }
|
||
|
||
/* Loading */
|
||
.loading {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
color: var(--muted);
|
||
padding: 20px 0;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.spinner {
|
||
width: 16px; height: 16px;
|
||
border: 2px solid var(--border);
|
||
border-top-color: var(--accent);
|
||
border-radius: 50%;
|
||
animation: spin 0.8s linear infinite;
|
||
}
|
||
@keyframes spin { to { transform: rotate(360deg); } }
|
||
|
||
/* User detail modal */
|
||
.modal-overlay {
|
||
position: fixed;
|
||
inset: 0;
|
||
background: rgba(0,0,0,0.7);
|
||
backdrop-filter: blur(4px);
|
||
z-index: 500;
|
||
display: none;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
.modal-overlay.open { display: flex; }
|
||
|
||
.modal {
|
||
background: var(--surface);
|
||
border: 1px solid var(--border);
|
||
border-radius: 16px;
|
||
width: 100%;
|
||
max-width: 560px;
|
||
max-height: 90vh;
|
||
overflow-y: auto;
|
||
padding: 28px;
|
||
}
|
||
|
||
.modal-title { font-family: 'DM Serif Display', serif; font-size: 20px; margin-bottom: 4px; }
|
||
.modal-sub { font-size: 13px; color: var(--muted); margin-bottom: 24px; }
|
||
|
||
.modal-close {
|
||
position: absolute;
|
||
top: 16px; right: 16px;
|
||
background: var(--surface2);
|
||
border: 1px solid var(--border);
|
||
color: var(--muted);
|
||
border-radius: 6px;
|
||
padding: 4px 10px;
|
||
cursor: pointer;
|
||
font-family: 'DM Sans', sans-serif;
|
||
}
|
||
|
||
/* Log entries */
|
||
.log-entry {
|
||
padding: 10px 12px;
|
||
border-radius: 8px;
|
||
border: 1px solid var(--border);
|
||
margin-bottom: 8px;
|
||
background: var(--surface2);
|
||
font-size: 12px;
|
||
}
|
||
.log-entry.success { border-left: 3px solid var(--success); }
|
||
.log-entry.error { border-left: 3px solid var(--error); }
|
||
.log-meta { display: flex; justify-content: space-between; margin-bottom: 4px; }
|
||
.log-time { color: var(--muted); font-family: 'DM Mono', monospace; font-size: 11px; }
|
||
.log-stats { color: var(--muted); font-size: 11px; margin-top: 4px; }
|
||
.log-error { color: var(--error); font-size: 11px; margin-top: 4px; font-family: 'DM Mono', monospace; }
|
||
|
||
/* Access Denied */
|
||
#access-denied { display: none; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<!-- ACCESS DENIED -->
|
||
<div id="access-denied" style="flex:1;display:none;align-items:center;justify-content:center;flex-direction:column;gap:16px;min-height:100vh;text-align:center">
|
||
<div style="font-size:48px">🚫</div>
|
||
<h2 style="font-family:'DM Serif Display',serif;font-size:28px">Kein Zugriff</h2>
|
||
<p style="color:var(--muted);max-width:360px">Du bist nicht als Admin registriert. Bitte wende dich an den Systemadministrator.</p>
|
||
<a href="/" style="color:var(--accent);text-decoration:none;font-size:14px">← Zurück zur Startseite</a>
|
||
</div>
|
||
|
||
<!-- MAIN APP -->
|
||
<div id="app" style="display:none;flex:1;display:none">
|
||
|
||
<!-- Sidebar -->
|
||
<aside class="sidebar">
|
||
<div class="sidebar-logo">
|
||
<div class="logo-icon">⇄</div>
|
||
<span class="logo-text">CardSync</span>
|
||
<span class="logo-badge">Admin</span>
|
||
</div>
|
||
|
||
<nav class="sidebar-nav">
|
||
<div class="nav-section">
|
||
<div class="nav-label">Übersicht</div>
|
||
<button class="nav-item active" onclick="showPanel('dashboard', this)">
|
||
<span class="icon">📊</span> Dashboard
|
||
</button>
|
||
</div>
|
||
<div class="nav-section">
|
||
<div class="nav-label">Verwaltung</div>
|
||
<button class="nav-item" onclick="showPanel('users', this)">
|
||
<span class="icon">👥</span> Benutzer
|
||
</button>
|
||
<button class="nav-item" onclick="showPanel('azure-group', this)">
|
||
<span class="icon">🏢</span> Azure AD Gruppe
|
||
</button>
|
||
<button class="nav-item" onclick="showPanel('config', this)">
|
||
<span class="icon">⚙️</span> Synology Config
|
||
</button>
|
||
</div>
|
||
</nav>
|
||
|
||
<div class="sidebar-footer">
|
||
<div class="admin-badge">
|
||
<div class="admin-avatar" id="admin-avatar">A</div>
|
||
<div class="admin-info">
|
||
<div class="admin-email" id="admin-email">—</div>
|
||
<div class="admin-role">Administrator</div>
|
||
</div>
|
||
</div>
|
||
<button class="btn-logout" onclick="logout()">Abmelden</button>
|
||
</div>
|
||
</aside>
|
||
|
||
<!-- Main -->
|
||
<div class="main">
|
||
<div class="topbar">
|
||
<div class="topbar-title" id="topbar-title">Dashboard</div>
|
||
<div class="topbar-right">
|
||
<span id="topbar-status" style="font-size:12px;color:var(--muted)">Laden…</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="content">
|
||
|
||
<!-- Dashboard Panel -->
|
||
<div id="panel-dashboard" class="panel active">
|
||
<div class="stats-grid">
|
||
<div class="stat-card">
|
||
<div class="stat-label">Benutzer gesamt</div>
|
||
<div class="stat-value" id="stat-total">—</div>
|
||
<div class="stat-sub">registriert</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-label">Sync aktiv</div>
|
||
<div class="stat-value" id="stat-active">—</div>
|
||
<div class="stat-sub">Benutzer mit aktivem Sync</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-label">Letzte Syncs</div>
|
||
<div class="stat-value" id="stat-recent">—</div>
|
||
<div class="stat-sub">in den letzten 5 Einträgen</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<div>
|
||
<div class="card-title">Letzte Sync-Aktivitäten</div>
|
||
<div class="card-subtitle">Die 5 zuletzt gestarteten Sync-Vorgänge</div>
|
||
</div>
|
||
<button class="btn btn-secondary btn-sm" onclick="loadDashboard()">↺ Aktualisieren</button>
|
||
</div>
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Benutzer</th>
|
||
<th>Zeitpunkt</th>
|
||
<th>Status</th>
|
||
<th>Kontakte</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="recent-logs-tbody">
|
||
<tr><td colspan="4" style="color:var(--muted);text-align:center;padding:20px">Lade…</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Users Panel -->
|
||
<div id="panel-users" class="panel">
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<div>
|
||
<div class="card-title">Benutzer</div>
|
||
<div class="card-subtitle">Alle registrierten Benutzer und ihr Sync-Status</div>
|
||
</div>
|
||
<button class="btn btn-secondary btn-sm" onclick="loadUsers()">↺ Aktualisieren</button>
|
||
</div>
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>E-Mail</th>
|
||
<th>Sync</th>
|
||
<th>Adressbuch</th>
|
||
<th>Intervall</th>
|
||
<th>Letzter Sync</th>
|
||
<th>Aktionen</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="users-tbody">
|
||
<tr><td colspan="6" style="color:var(--muted);text-align:center;padding:20px">Lade…</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Azure AD Group Panel -->
|
||
<div id="panel-azure-group" class="panel">
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<div>
|
||
<div class="card-title">Azure AD Gruppen-Integration</div>
|
||
<div class="card-subtitle">Importiere User automatisch aus einer Azure AD Gruppe — kein OAuth-Login durch User nötig</div>
|
||
</div>
|
||
</div>
|
||
<div class="card-body">
|
||
<div id="azure-alert" class="alert"></div>
|
||
|
||
<div class="form-grid">
|
||
<div class="form-group full">
|
||
<label class="form-label">Azure AD Gruppen-ID (Object ID)</label>
|
||
<input type="text" id="az-group-id" class="input" placeholder="z.B. a1b2c3d4-e5f6-7890-abcd-ef1234567890" style="font-family:'DM Mono',monospace">
|
||
<div style="font-size:11px;color:var(--muted);margin-top:4px">In Azure Portal → Azure AD → Gruppen → deine Gruppe → "Object ID" kopieren</div>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">Mitglieder-Abgleich</label>
|
||
<select id="az-auto-sync" class="select">
|
||
<option value="true">Automatisch (empfohlen)</option>
|
||
<option value="false">Nur manuell</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">Abgleich-Intervall</label>
|
||
<select id="az-sync-interval" class="select">
|
||
<option value="1">Stündlich</option>
|
||
<option value="6" selected>Alle 6 Stunden</option>
|
||
<option value="12">Alle 12 Stunden</option>
|
||
<option value="24">Täglich</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">Default-Adressbuch für neue User</label>
|
||
<select id="az-default-folder" class="select">
|
||
<option value="">— Adressbuch wählen —</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">Default Sync-Intervall</label>
|
||
<select id="az-default-interval" class="select">
|
||
<option value="15">Alle 15 Minuten</option>
|
||
<option value="30">Alle 30 Minuten</option>
|
||
<option value="60" selected>Stündlich</option>
|
||
<option value="360">Alle 6 Stunden</option>
|
||
<option value="720">Alle 12 Stunden</option>
|
||
<option value="1440">Täglich</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="form-group full">
|
||
<label class="form-label">Neue User direkt aktiviert?</label>
|
||
<select id="az-default-enabled" class="select">
|
||
<option value="true">Ja — Sync läuft sofort nach Import</option>
|
||
<option value="false">Nein — Admin muss jeden User manuell aktivieren</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="btn-row">
|
||
<button class="btn btn-primary" onclick="saveAzureGroup()">💾 Speichern</button>
|
||
<button class="btn btn-secondary" onclick="testAzureGroup()">🔌 Gruppe testen</button>
|
||
<button class="btn btn-success" onclick="syncGroupNow()" id="az-sync-btn">⚡ Mitglieder jetzt importieren</button>
|
||
<button class="btn btn-danger" onclick="deleteAzureGroup()">🗑 Gruppen-Konfig entfernen</button>
|
||
</div>
|
||
|
||
<div id="az-status" style="margin-top:20px;padding:14px 16px;background:var(--surface2);border-radius:8px;font-size:13px;display:none">
|
||
<div style="font-weight:500;margin-bottom:6px" id="az-status-title">Status</div>
|
||
<div style="color:var(--muted)" id="az-status-message">—</div>
|
||
<div class="mono" style="font-size:11px;color:var(--muted2);margin-top:6px" id="az-status-time"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<div class="card-title">Wichtig: Azure App Permissions</div>
|
||
<div class="card-subtitle">Für Gruppen-Import sind Application Permissions nötig (nicht nur Delegated)</div>
|
||
</div>
|
||
<div class="card-body" style="color:var(--muted);font-size:13px;line-height:1.8">
|
||
In Azure Portal → App-Registrierungen → CardSync → <strong style="color:var(--text)">API-Berechtigungen</strong> → <strong style="color:var(--text)">Berechtigung hinzufügen</strong> → Microsoft Graph → <strong style="color:var(--text)">Anwendungsberechtigungen</strong>:
|
||
<ul style="padding-left:20px;margin-top:8px;display:flex;flex-direction:column;gap:4px">
|
||
<li><span class="mono">User.Read.All</span> — User-Daten lesen</li>
|
||
<li><span class="mono">GroupMember.Read.All</span> — Gruppenmitglieder lesen</li>
|
||
<li><span class="mono">Contacts.ReadWrite</span> (Application!) — Kontakte aller User schreiben</li>
|
||
</ul>
|
||
<div style="margin-top:14px;padding:10px 14px;background:rgba(255,185,0,0.1);border:1px solid rgba(255,185,0,0.2);border-radius:8px;color:var(--warning);font-size:12px">
|
||
⚠️ Nach dem Hinzufügen <strong>"Administratorzustimmung erteilen"</strong> klicken — sonst funktioniert es nicht.
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Config Panel -->
|
||
<div id="panel-config" class="panel">
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<div>
|
||
<div class="card-title">Synology CardDAV Konfiguration</div>
|
||
<div class="card-subtitle">Zentrale Servereinstellungen für alle Benutzer</div>
|
||
</div>
|
||
</div>
|
||
<div class="card-body">
|
||
<div id="config-alert" class="alert"></div>
|
||
|
||
<div class="form-grid">
|
||
<div class="form-group full">
|
||
<label class="form-label">Server URL</label>
|
||
<input type="url" id="cfg-url" class="input" placeholder="https://192.168.1.10:5006">
|
||
<div style="font-size:11px;color:var(--muted);margin-top:4px">Synology-IP oder Hostname mit Port (Standard: 5006 für CardDAV)</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Benutzername</label>
|
||
<input type="text" id="cfg-user" class="input" placeholder="admin">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Passwort</label>
|
||
<input type="password" id="cfg-pass" class="input" placeholder="••••••••">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="btn-row">
|
||
<button class="btn btn-primary" onclick="saveConfig()">💾 Speichern</button>
|
||
<button class="btn btn-secondary" onclick="testConfig()">🔌 Verbindung testen</button>
|
||
<button class="btn btn-secondary" onclick="clearCache()" title="Erzwingt frische CardDAV-Daten beim nächsten Sync">🔄 Cache leeren</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<div class="card-title">Azure App Registration</div>
|
||
<div class="card-subtitle">Einrichtungsanleitung für Microsoft OAuth</div>
|
||
</div>
|
||
<div class="card-body" style="color:var(--muted);font-size:13px;line-height:1.8">
|
||
<ol style="padding-left:20px;display:flex;flex-direction:column;gap:8px">
|
||
<li>Gehe zu <a href="https://portal.azure.com" target="_blank" style="color:var(--accent)">portal.azure.com</a> → Azure Active Directory → App-Registrierungen</li>
|
||
<li>Klicke auf <strong style="color:var(--text)">Neue Registrierung</strong></li>
|
||
<li>Name: <span class="mono">CardSync</span> | Kontotypen: <strong style="color:var(--text)">Nur eigene Organisation</strong> oder Multitenant je nach Bedarf</li>
|
||
<li>Redirect URI (Web): <span class="mono" id="redirect-uri" style="color:var(--accent)">http://deine-domain/auth/callback</span></li>
|
||
<li>Unter <strong style="color:var(--text)">Zertifikate & Geheimnisse</strong> ein neues Client-Secret erstellen</li>
|
||
<li>Unter <strong style="color:var(--text)">API-Berechtigungen</strong> hinzufügen: <span class="mono">Contacts.ReadWrite</span>, <span class="mono">offline_access</span>, <span class="mono">openid</span>, <span class="mono">profile</span>, <span class="mono">email</span></li>
|
||
<li>Client-ID, Secret und Tenant-ID in die <span class="mono">.env</span> Datei eintragen</li>
|
||
</ol>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- User Edit Modal -->
|
||
<div class="modal-overlay" id="user-modal">
|
||
<div class="modal" style="position:relative">
|
||
<button class="modal-close" onclick="closeModal()">✕ Schließen</button>
|
||
<div class="modal-title" id="modal-user-name">Benutzer</div>
|
||
<div class="modal-sub" id="modal-user-email"></div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">Adressbuch (CardDAV-Ordner URL)</label>
|
||
<select id="modal-folder" class="select">
|
||
<option value="">Wird geladen…</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Sync-Intervall</label>
|
||
<select id="modal-interval" class="select">
|
||
<option value="15">Alle 15 Minuten</option>
|
||
<option value="30">Alle 30 Minuten</option>
|
||
<option value="60">Stündlich</option>
|
||
<option value="360">Alle 6 Stunden</option>
|
||
<option value="720">Alle 12 Stunden</option>
|
||
<option value="1440">Täglich</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Sync aktiviert</label>
|
||
<select id="modal-sync-enabled" class="select">
|
||
<option value="true">Ja — Sync aktiv</option>
|
||
<option value="false">Nein — Sync pausiert</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="btn-row" style="margin-bottom:24px">
|
||
<button class="btn btn-primary" onclick="saveUserModal()">💾 Speichern</button>
|
||
<button class="btn btn-warning" onclick="syncUserNow()" id="modal-sync-btn">⚡ Jetzt synchronisieren</button>
|
||
<button class="btn btn-danger" onclick="deleteUser()">🗑 Löschen</button>
|
||
</div>
|
||
|
||
<div style="font-size:13px;font-weight:600;color:var(--text);margin-bottom:10px">Sync-Verlauf</div>
|
||
<div id="modal-logs">
|
||
<div class="loading"><div class="spinner"></div> Lade Logs…</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const API = '';
|
||
let currentUserId = null;
|
||
let usersCache = [];
|
||
let folderCache = [];
|
||
|
||
async function init() {
|
||
try {
|
||
const resp = await fetch(`${API}/auth/me`, { credentials: 'include' });
|
||
if (!resp.ok) throw new Error('not logged in');
|
||
const me = await resp.json();
|
||
|
||
if (!me.is_admin) {
|
||
document.getElementById('access-denied').style.display = 'flex';
|
||
document.getElementById('app').style.display = 'none';
|
||
return;
|
||
}
|
||
|
||
document.getElementById('access-denied').style.display = 'none';
|
||
document.getElementById('app').style.display = 'flex';
|
||
|
||
document.getElementById('admin-email').textContent = me.email;
|
||
document.getElementById('admin-avatar').textContent = me.email[0].toUpperCase();
|
||
|
||
// Redirect URI anzeigen
|
||
document.getElementById('redirect-uri').textContent = `${window.location.origin}/auth/callback`;
|
||
|
||
await Promise.all([loadDashboard(), loadConfig(), loadFolderCache()]);
|
||
|
||
} catch (e) {
|
||
window.location.href = '/auth/login?mode=admin';
|
||
}
|
||
}
|
||
|
||
function showPanel(name, el) {
|
||
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
|
||
document.querySelectorAll('.nav-item').forEach(i => i.classList.remove('active'));
|
||
document.getElementById(`panel-${name}`).classList.add('active');
|
||
el.classList.add('active');
|
||
|
||
const titles = { dashboard: 'Dashboard', users: 'Benutzer', 'azure-group': 'Azure AD Gruppe', config: 'Synology Config' };
|
||
document.getElementById('topbar-title').textContent = titles[name] || name;
|
||
|
||
if (name === 'users') loadUsers();
|
||
if (name === 'azure-group') loadAzureGroup();
|
||
}
|
||
|
||
async function loadDashboard() {
|
||
try {
|
||
const [statsResp] = await Promise.all([
|
||
fetch(`${API}/api/admin/stats`, { credentials: 'include' }),
|
||
]);
|
||
const stats = await statsResp.json();
|
||
|
||
document.getElementById('stat-total').textContent = stats.total_users;
|
||
document.getElementById('stat-active').textContent = stats.sync_enabled_users;
|
||
document.getElementById('stat-recent').textContent = stats.recent_logs.length;
|
||
document.getElementById('topbar-status').textContent = `${stats.sync_enabled_users}/${stats.total_users} Sync aktiv`;
|
||
|
||
// User-Map für Logs laden
|
||
const usersResp = await fetch(`${API}/api/admin/users`, { credentials: 'include' });
|
||
const usersData = await usersResp.json();
|
||
usersCache = usersData.users;
|
||
|
||
const tbody = document.getElementById('recent-logs-tbody');
|
||
if (!stats.recent_logs.length) {
|
||
tbody.innerHTML = '<tr><td colspan="4" style="color:var(--muted);text-align:center;padding:20px">Noch keine Sync-Vorgänge</td></tr>';
|
||
return;
|
||
}
|
||
|
||
tbody.innerHTML = stats.recent_logs.map(l => {
|
||
const user = usersCache.find(u => u.id === l.user_id);
|
||
return `<tr>
|
||
<td>${user ? user.email : `User #${l.user_id}`}</td>
|
||
<td class="mono">${l.started_at ? new Date(l.started_at).toLocaleString('de-DE') : '—'}</td>
|
||
<td><span class="badge-status ${l.status || 'off'}"><span class="dot-indicator"></span>${l.status || '—'}</span></td>
|
||
<td>${l.contacts_synced ?? '—'}</td>
|
||
</tr>`;
|
||
}).join('');
|
||
} catch (e) {
|
||
console.error('Dashboard load error:', e);
|
||
}
|
||
}
|
||
|
||
async function loadUsers() {
|
||
const tbody = document.getElementById('users-tbody');
|
||
tbody.innerHTML = '<tr><td colspan="6" style="color:var(--muted);text-align:center;padding:20px"><div class="loading" style="justify-content:center"><div class="spinner"></div> Lade…</div></td></tr>';
|
||
|
||
try {
|
||
const resp = await fetch(`${API}/api/admin/users`, { credentials: 'include' });
|
||
const data = await resp.json();
|
||
usersCache = data.users;
|
||
|
||
if (!data.users.length) {
|
||
tbody.innerHTML = '<tr><td colspan="6" style="color:var(--muted);text-align:center;padding:24px">Noch keine Benutzer registriert</td></tr>';
|
||
return;
|
||
}
|
||
|
||
tbody.innerHTML = data.users.map(u => {
|
||
const folderName = u.carddav_folder ? u.carddav_folder.split('/').filter(Boolean).pop() || u.carddav_folder : '—';
|
||
const lastSync = u.last_sync_at ? new Date(u.last_sync_at).toLocaleString('de-DE') : '—';
|
||
const statusClass = u.sync_enabled ? (u.last_sync_status || 'off') : 'off';
|
||
const statusLabel = u.sync_enabled ? (u.last_sync_status || 'Noch kein Sync') : 'Deaktiviert';
|
||
const sourceBadge = u.source === 'group_import'
|
||
? '<span class="badge-status" style="background:rgba(124,92,252,0.12);color:#7c5cfc;font-size:9px;padding:2px 6px;margin-left:6px">AD</span>'
|
||
: '<span class="badge-status" style="background:rgba(79,142,247,0.12);color:#4f8ef7;font-size:9px;padding:2px 6px;margin-left:6px">OAuth</span>';
|
||
|
||
return `<tr>
|
||
<td>
|
||
<div style="font-weight:500">${u.display_name || u.email}${sourceBadge}</div>
|
||
<div class="mono">${u.email}</div>
|
||
</td>
|
||
<td><span class="badge-status ${u.sync_enabled ? 'success' : 'off'}"><span class="dot-indicator"></span>${u.sync_enabled ? 'Aktiv' : 'Aus'}</span></td>
|
||
<td class="mono" title="${u.carddav_folder || ''}">${folderName}</td>
|
||
<td>${u.sync_interval_minutes ? `${u.sync_interval_minutes} Min.` : '—'}</td>
|
||
<td>
|
||
<div class="mono" style="font-size:11px">${lastSync}</div>
|
||
<span class="badge-status ${statusClass}" style="margin-top:4px"><span class="dot-indicator"></span>${statusLabel}</span>
|
||
</td>
|
||
<td>
|
||
<button class="btn btn-secondary btn-sm" onclick="openUserModal(${u.id})">✏️ Bearbeiten</button>
|
||
</td>
|
||
</tr>`;
|
||
}).join('');
|
||
} catch (e) {
|
||
tbody.innerHTML = '<tr><td colspan="6" style="color:var(--error);text-align:center;padding:20px">Fehler beim Laden</td></tr>';
|
||
}
|
||
}
|
||
|
||
async function loadFolderCache() {
|
||
try {
|
||
const resp = await fetch(`${API}/api/carddav/folders`, { credentials: 'include' });
|
||
if (resp.ok) {
|
||
const data = await resp.json();
|
||
folderCache = data.folders || [];
|
||
}
|
||
} catch {}
|
||
}
|
||
|
||
async function openUserModal(userId) {
|
||
currentUserId = userId;
|
||
const user = usersCache.find(u => u.id === userId);
|
||
if (!user) return;
|
||
|
||
document.getElementById('modal-user-name').textContent = user.display_name || user.email;
|
||
document.getElementById('modal-user-email').textContent = user.email;
|
||
document.getElementById('modal-sync-enabled').value = String(user.sync_enabled);
|
||
document.getElementById('modal-interval').value = user.sync_interval_minutes || 60;
|
||
|
||
// Folder Dropdown befüllen
|
||
const sel = document.getElementById('modal-folder');
|
||
sel.innerHTML = '<option value="">Kein Adressbuch ausgewählt</option>';
|
||
folderCache.forEach(f => {
|
||
const opt = document.createElement('option');
|
||
opt.value = f.url;
|
||
opt.textContent = f.name;
|
||
if (f.url === user.carddav_folder) opt.selected = true;
|
||
sel.appendChild(opt);
|
||
});
|
||
|
||
// Logs laden
|
||
document.getElementById('modal-logs').innerHTML = '<div class="loading"><div class="spinner"></div> Lade Logs…</div>';
|
||
|
||
document.getElementById('user-modal').classList.add('open');
|
||
|
||
try {
|
||
const resp = await fetch(`${API}/api/admin/users/${userId}/logs`, { credentials: 'include' });
|
||
const data = await resp.json();
|
||
renderModalLogs(data.logs);
|
||
} catch {
|
||
document.getElementById('modal-logs').innerHTML = '<div style="color:var(--error);font-size:12px">Fehler beim Laden der Logs</div>';
|
||
}
|
||
}
|
||
|
||
function renderModalLogs(logs) {
|
||
const container = document.getElementById('modal-logs');
|
||
if (!logs.length) {
|
||
container.innerHTML = '<div style="color:var(--muted);font-size:12px">Noch keine Sync-Einträge</div>';
|
||
return;
|
||
}
|
||
container.innerHTML = logs.map(l => `
|
||
<div class="log-entry ${l.status || ''}">
|
||
<div class="log-meta">
|
||
<span class="badge-status ${l.status}"><span class="dot-indicator"></span>${l.status}</span>
|
||
<span class="log-time">${l.started_at ? new Date(l.started_at).toLocaleString('de-DE') : '—'}</span>
|
||
</div>
|
||
${l.status === 'success' ? `<div class="log-stats">+${l.contacts_created} erstellt · ~${l.contacts_updated} aktualisiert · −${l.contacts_deleted} gelöscht · ${l.contacts_synced} gesamt</div>` : ''}
|
||
${l.error_message ? `<div class="log-error">${l.error_message}</div>` : ''}
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
function closeModal() {
|
||
document.getElementById('user-modal').classList.remove('open');
|
||
currentUserId = null;
|
||
}
|
||
|
||
async function saveUserModal() {
|
||
if (!currentUserId) return;
|
||
const payload = {
|
||
carddav_folder: document.getElementById('modal-folder').value,
|
||
sync_interval_minutes: parseInt(document.getElementById('modal-interval').value),
|
||
sync_enabled: document.getElementById('modal-sync-enabled').value === 'true',
|
||
};
|
||
await fetch(`${API}/api/admin/users/${currentUserId}`, {
|
||
method: 'PATCH',
|
||
credentials: 'include',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(payload),
|
||
});
|
||
showToast('Einstellungen gespeichert');
|
||
await loadUsers();
|
||
}
|
||
|
||
async function syncUserNow() {
|
||
if (!currentUserId) return;
|
||
const btn = document.getElementById('modal-sync-btn');
|
||
btn.disabled = true;
|
||
btn.textContent = '⟳ Läuft…';
|
||
|
||
const resp = await fetch(`${API}/api/admin/users/${currentUserId}/sync-now`, {
|
||
method: 'POST',
|
||
credentials: 'include',
|
||
});
|
||
const data = await resp.json();
|
||
showToast(data.status === 'success' ? `✓ ${data.message}` : `✗ ${data.message}`, data.status);
|
||
|
||
btn.disabled = false;
|
||
btn.textContent = '⚡ Jetzt synchronisieren';
|
||
|
||
// Logs neu laden
|
||
const logsResp = await fetch(`${API}/api/admin/users/${currentUserId}/logs`, { credentials: 'include' });
|
||
const logsData = await logsResp.json();
|
||
renderModalLogs(logsData.logs);
|
||
|
||
await loadUsers();
|
||
}
|
||
|
||
async function deleteUser() {
|
||
if (!currentUserId) return;
|
||
const user = usersCache.find(u => u.id === currentUserId);
|
||
if (!confirm(`Benutzer "${user?.email}" wirklich löschen? Alle Sync-Daten werden entfernt.`)) return;
|
||
|
||
await fetch(`${API}/api/admin/users/${currentUserId}`, {
|
||
method: 'DELETE',
|
||
credentials: 'include',
|
||
});
|
||
closeModal();
|
||
showToast('Benutzer gelöscht');
|
||
await loadUsers();
|
||
}
|
||
|
||
async function loadConfig() {
|
||
try {
|
||
const resp = await fetch(`${API}/api/admin/config`, { credentials: 'include' });
|
||
const data = await resp.json();
|
||
if (data.configured) {
|
||
document.getElementById('cfg-url').value = data.server_url;
|
||
document.getElementById('cfg-user').value = data.username;
|
||
}
|
||
} catch {}
|
||
}
|
||
|
||
async function saveConfig() {
|
||
const payload = {
|
||
server_url: document.getElementById('cfg-url').value.trim(),
|
||
username: document.getElementById('cfg-user').value.trim(),
|
||
password: document.getElementById('cfg-pass').value,
|
||
};
|
||
|
||
if (!payload.server_url || !payload.username || !payload.password) {
|
||
showAlert('config-alert', 'Bitte alle Felder ausfüllen.', 'error');
|
||
return;
|
||
}
|
||
|
||
const resp = await fetch(`${API}/api/admin/config`, {
|
||
method: 'POST',
|
||
credentials: 'include',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(payload),
|
||
});
|
||
|
||
if (resp.ok) {
|
||
showAlert('config-alert', '✓ Konfiguration gespeichert', 'success');
|
||
await loadFolderCache();
|
||
} else {
|
||
showAlert('config-alert', '✗ Fehler beim Speichern', 'error');
|
||
}
|
||
}
|
||
|
||
async function testConfig() {
|
||
const payload = {
|
||
server_url: document.getElementById('cfg-url').value.trim(),
|
||
username: document.getElementById('cfg-user').value.trim(),
|
||
password: document.getElementById('cfg-pass').value,
|
||
};
|
||
|
||
showAlert('config-alert', '⟳ Verbindung wird getestet…', 'info');
|
||
|
||
const resp = await fetch(`${API}/api/admin/config/test`, {
|
||
method: 'POST',
|
||
credentials: 'include',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(payload),
|
||
});
|
||
const data = await resp.json();
|
||
showAlert('config-alert', data.ok ? `✓ ${data.message}` : `✗ ${data.message}`, data.ok ? 'success' : 'error');
|
||
}
|
||
|
||
function showAlert(id, msg, type) {
|
||
const el = document.getElementById(id);
|
||
el.className = `alert ${type}`;
|
||
el.textContent = msg;
|
||
el.style.display = 'flex';
|
||
}
|
||
|
||
// ── Azure AD Gruppe ────────────────────────────────────────
|
||
|
||
async function loadAzureGroup() {
|
||
// Folder-Dropdown befüllen
|
||
const folderSel = document.getElementById('az-default-folder');
|
||
folderSel.innerHTML = '<option value="">— Adressbuch wählen —</option>';
|
||
folderCache.forEach(f => {
|
||
const opt = document.createElement('option');
|
||
opt.value = f.url;
|
||
opt.textContent = f.name;
|
||
folderSel.appendChild(opt);
|
||
});
|
||
|
||
try {
|
||
const resp = await fetch(`${API}/api/admin/azure-group`, { credentials: 'include' });
|
||
const data = await resp.json();
|
||
if (!data.configured) return;
|
||
|
||
document.getElementById('az-group-id').value = data.group_id || '';
|
||
document.getElementById('az-auto-sync').value = String(data.auto_sync_members);
|
||
document.getElementById('az-sync-interval').value = data.member_sync_interval_hours || 6;
|
||
document.getElementById('az-default-folder').value = data.default_carddav_folder || '';
|
||
document.getElementById('az-default-interval').value = data.default_sync_interval_minutes || 60;
|
||
document.getElementById('az-default-enabled').value = String(data.default_sync_enabled);
|
||
|
||
if (data.last_member_sync_at) {
|
||
const statusEl = document.getElementById('az-status');
|
||
statusEl.style.display = 'block';
|
||
document.getElementById('az-status-title').textContent =
|
||
`Letzter Member-Sync: ${data.last_member_sync_status === 'success' ? '✓ Erfolgreich' : '✗ Fehler'}` +
|
||
(data.group_name ? ` — Gruppe: ${data.group_name}` : '');
|
||
document.getElementById('az-status-message').textContent = data.last_member_sync_message || '—';
|
||
document.getElementById('az-status-time').textContent = new Date(data.last_member_sync_at).toLocaleString('de-DE');
|
||
}
|
||
} catch (e) {
|
||
console.error('loadAzureGroup error:', e);
|
||
}
|
||
}
|
||
|
||
function _azurePayload() {
|
||
return {
|
||
group_id: document.getElementById('az-group-id').value.trim(),
|
||
auto_sync_members: document.getElementById('az-auto-sync').value === 'true',
|
||
member_sync_interval_hours: parseInt(document.getElementById('az-sync-interval').value),
|
||
default_carddav_folder: document.getElementById('az-default-folder').value || null,
|
||
default_sync_interval_minutes: parseInt(document.getElementById('az-default-interval').value),
|
||
default_sync_enabled: document.getElementById('az-default-enabled').value === 'true',
|
||
};
|
||
}
|
||
|
||
async function saveAzureGroup() {
|
||
const payload = _azurePayload();
|
||
if (!payload.group_id) {
|
||
showAlert('azure-alert', 'Bitte Gruppen-ID eintragen', 'error');
|
||
return;
|
||
}
|
||
const resp = await fetch(`${API}/api/admin/azure-group`, {
|
||
method: 'POST',
|
||
credentials: 'include',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(payload),
|
||
});
|
||
if (resp.ok) {
|
||
showAlert('azure-alert', '✓ Konfiguration gespeichert', 'success');
|
||
} else {
|
||
showAlert('azure-alert', '✗ Fehler beim Speichern', 'error');
|
||
}
|
||
}
|
||
|
||
async function testAzureGroup() {
|
||
const payload = _azurePayload();
|
||
if (!payload.group_id) {
|
||
showAlert('azure-alert', 'Bitte Gruppen-ID eintragen', 'error');
|
||
return;
|
||
}
|
||
showAlert('azure-alert', '⟳ Teste Verbindung zu Azure AD…', 'info');
|
||
const resp = await fetch(`${API}/api/admin/azure-group/test`, {
|
||
method: 'POST',
|
||
credentials: 'include',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(payload),
|
||
});
|
||
const data = await resp.json();
|
||
showAlert('azure-alert', (data.ok ? '✓ ' : '✗ ') + data.message, data.ok ? 'success' : 'error');
|
||
}
|
||
|
||
async function syncGroupNow() {
|
||
const btn = document.getElementById('az-sync-btn');
|
||
btn.disabled = true;
|
||
btn.textContent = '⟳ Importiere…';
|
||
try {
|
||
const resp = await fetch(`${API}/api/admin/azure-group/sync-now`, {
|
||
method: 'POST',
|
||
credentials: 'include',
|
||
});
|
||
const data = await resp.json();
|
||
showAlert('azure-alert', (data.status === 'success' ? '✓ ' : '✗ ') + data.message, data.status === 'success' ? 'success' : 'error');
|
||
await loadAzureGroup();
|
||
await loadUsers();
|
||
} catch (e) {
|
||
showAlert('azure-alert', '✗ Fehler: ' + e.message, 'error');
|
||
}
|
||
btn.disabled = false;
|
||
btn.textContent = '⚡ Mitglieder jetzt importieren';
|
||
}
|
||
|
||
async function deleteAzureGroup() {
|
||
if (!confirm('Gruppen-Konfiguration entfernen? Die bereits importierten User bleiben bestehen, aber es findet kein automatischer Member-Abgleich mehr statt.')) return;
|
||
await fetch(`${API}/api/admin/azure-group`, {
|
||
method: 'DELETE',
|
||
credentials: 'include',
|
||
});
|
||
showAlert('azure-alert', '✓ Konfiguration entfernt', 'success');
|
||
document.getElementById('az-group-id').value = '';
|
||
document.getElementById('az-status').style.display = 'none';
|
||
}
|
||
|
||
async function clearCache() {
|
||
const resp = await fetch(`${API}/api/admin/cache/clear`, {
|
||
method: 'POST',
|
||
credentials: 'include',
|
||
});
|
||
if (resp.ok) {
|
||
showAlert('config-alert', '✓ Cache geleert — nächste Syncs holen frische Daten', 'success');
|
||
} else {
|
||
showAlert('config-alert', '✗ Fehler beim Cache leeren', 'error');
|
||
}
|
||
}
|
||
|
||
async function logout() {
|
||
await fetch(`${API}/auth/logout`, { method: 'POST', credentials: 'include' });
|
||
window.location.href = '/';
|
||
}
|
||
|
||
function showToast(msg, type = 'success') {
|
||
const t = document.createElement('div');
|
||
t.style.cssText = `
|
||
position:fixed;bottom:24px;right:24px;z-index:9999;
|
||
padding:12px 20px;border-radius:10px;font-size:13px;font-weight:500;
|
||
background:${type==='success'?'rgba(62,207,142,0.15)':type==='error'?'rgba(247,108,108,0.15)':'rgba(79,142,247,0.15)'};
|
||
border:1px solid ${type==='success'?'rgba(62,207,142,0.3)':type==='error'?'rgba(247,108,108,0.3)':'rgba(79,142,247,0.3)'};
|
||
color:${type==='success'?'#3ecf8e':type==='error'?'#f76c6c':'#4f8ef7'};
|
||
font-family:'DM Sans',sans-serif;
|
||
`;
|
||
t.textContent = msg;
|
||
document.body.appendChild(t);
|
||
setTimeout(() => t.remove(), 4000);
|
||
}
|
||
|
||
init();
|
||
</script>
|
||
</body>
|
||
</html>
|