766 lines
21 KiB
HTML
766 lines
21 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 — Synology · Microsoft 365</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;1,9..40,300&display=swap" rel="stylesheet">
|
|
<style>
|
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
|
:root {
|
|
--bg: #0e0f14;
|
|
--surface: #16181f;
|
|
--surface2: #1e2029;
|
|
--border: #2a2d38;
|
|
--accent: #4f8ef7;
|
|
--accent2: #7c5cfc;
|
|
--success: #3ecf8e;
|
|
--error: #f76c6c;
|
|
--text: #e8eaf0;
|
|
--muted: #7a7f96;
|
|
--ms-blue: #0078d4;
|
|
}
|
|
|
|
html { height: 100%; }
|
|
|
|
body {
|
|
min-height: 100%;
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
font-family: 'DM Sans', sans-serif;
|
|
font-size: 15px;
|
|
line-height: 1.6;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
/* Background grid */
|
|
body::before {
|
|
content: '';
|
|
position: fixed;
|
|
inset: 0;
|
|
background-image:
|
|
linear-gradient(rgba(79,142,247,0.03) 1px, transparent 1px),
|
|
linear-gradient(90deg, rgba(79,142,247,0.03) 1px, transparent 1px);
|
|
background-size: 40px 40px;
|
|
pointer-events: none;
|
|
z-index: 0;
|
|
}
|
|
|
|
/* Glow blobs */
|
|
.blob {
|
|
position: fixed;
|
|
border-radius: 50%;
|
|
filter: blur(120px);
|
|
opacity: 0.12;
|
|
pointer-events: none;
|
|
z-index: 0;
|
|
}
|
|
.blob-1 {
|
|
width: 500px; height: 500px;
|
|
background: var(--accent);
|
|
top: -150px; left: -100px;
|
|
animation: drift 12s ease-in-out infinite alternate;
|
|
}
|
|
.blob-2 {
|
|
width: 400px; height: 400px;
|
|
background: var(--accent2);
|
|
bottom: -100px; right: -100px;
|
|
animation: drift 15s ease-in-out infinite alternate-reverse;
|
|
}
|
|
|
|
@keyframes drift {
|
|
from { transform: translate(0, 0); }
|
|
to { transform: translate(30px, 20px); }
|
|
}
|
|
|
|
/* Layout */
|
|
.page {
|
|
position: relative;
|
|
z-index: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-height: 100vh;
|
|
}
|
|
|
|
header {
|
|
padding: 24px 40px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
border-bottom: 1px solid var(--border);
|
|
backdrop-filter: blur(10px);
|
|
}
|
|
|
|
.logo {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
text-decoration: none;
|
|
}
|
|
.logo-icon {
|
|
width: 36px; height: 36px;
|
|
background: linear-gradient(135deg, var(--accent), var(--accent2));
|
|
border-radius: 10px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 18px;
|
|
}
|
|
.logo-text {
|
|
font-family: 'DM Serif Display', serif;
|
|
font-size: 20px;
|
|
color: var(--text);
|
|
letter-spacing: -0.3px;
|
|
}
|
|
|
|
.header-right {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
}
|
|
|
|
.btn-ghost {
|
|
padding: 8px 16px;
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
background: transparent;
|
|
color: var(--muted);
|
|
font-family: 'DM Sans', sans-serif;
|
|
font-size: 14px;
|
|
cursor: pointer;
|
|
text-decoration: none;
|
|
transition: all 0.2s;
|
|
}
|
|
.btn-ghost:hover {
|
|
border-color: var(--accent);
|
|
color: var(--text);
|
|
}
|
|
|
|
/* Hero */
|
|
main {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 60px 24px;
|
|
}
|
|
|
|
.badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 4px 12px;
|
|
background: rgba(79,142,247,0.08);
|
|
border: 1px solid rgba(79,142,247,0.2);
|
|
border-radius: 100px;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
color: var(--accent);
|
|
letter-spacing: 0.5px;
|
|
text-transform: uppercase;
|
|
margin-bottom: 32px;
|
|
}
|
|
|
|
h1 {
|
|
font-family: 'DM Serif Display', serif;
|
|
font-size: clamp(36px, 6vw, 64px);
|
|
line-height: 1.1;
|
|
text-align: center;
|
|
letter-spacing: -1px;
|
|
max-width: 800px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
h1 em {
|
|
font-style: italic;
|
|
background: linear-gradient(135deg, var(--accent), var(--accent2));
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
background-clip: text;
|
|
}
|
|
|
|
.subtitle {
|
|
text-align: center;
|
|
color: var(--muted);
|
|
font-size: 17px;
|
|
max-width: 520px;
|
|
margin-bottom: 48px;
|
|
font-weight: 300;
|
|
}
|
|
|
|
/* Connect Card */
|
|
.connect-card {
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: 20px;
|
|
padding: 40px;
|
|
width: 100%;
|
|
max-width: 460px;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.connect-card::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0; left: 0; right: 0;
|
|
height: 2px;
|
|
background: linear-gradient(90deg, var(--accent), var(--accent2));
|
|
}
|
|
|
|
.card-title {
|
|
font-family: 'DM Serif Display', serif;
|
|
font-size: 22px;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.card-desc {
|
|
color: var(--muted);
|
|
font-size: 14px;
|
|
margin-bottom: 28px;
|
|
}
|
|
|
|
.flow-steps {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
margin-bottom: 28px;
|
|
}
|
|
|
|
.flow-step {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 12px 14px;
|
|
background: var(--surface2);
|
|
border-radius: 10px;
|
|
border: 1px solid var(--border);
|
|
font-size: 13px;
|
|
color: var(--muted);
|
|
}
|
|
|
|
.step-num {
|
|
width: 22px; height: 22px;
|
|
background: linear-gradient(135deg, var(--accent), var(--accent2));
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
color: white;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.btn-ms {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 10px;
|
|
width: 100%;
|
|
padding: 14px 24px;
|
|
background: var(--ms-blue);
|
|
color: white;
|
|
border: none;
|
|
border-radius: 10px;
|
|
font-family: 'DM Sans', sans-serif;
|
|
font-size: 15px;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
text-decoration: none;
|
|
transition: all 0.2s;
|
|
box-shadow: 0 4px 20px rgba(0,120,212,0.3);
|
|
}
|
|
.btn-ms:hover {
|
|
background: #106ebe;
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 6px 24px rgba(0,120,212,0.4);
|
|
}
|
|
.btn-ms:active { transform: translateY(0); }
|
|
|
|
.ms-logo {
|
|
width: 20px; height: 20px;
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 2px;
|
|
}
|
|
.ms-logo span {
|
|
display: block;
|
|
border-radius: 1px;
|
|
}
|
|
.ms-logo span:nth-child(1) { background: #f25022; }
|
|
.ms-logo span:nth-child(2) { background: #7fba00; }
|
|
.ms-logo span:nth-child(3) { background: #00a4ef; }
|
|
.ms-logo span:nth-child(4) { background: #ffb900; }
|
|
|
|
.privacy-note {
|
|
text-align: center;
|
|
font-size: 12px;
|
|
color: var(--muted);
|
|
margin-top: 16px;
|
|
}
|
|
|
|
/* Status (logged in view) */
|
|
#status-view { display: none; }
|
|
|
|
.status-bar {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 10px 14px;
|
|
border-radius: 8px;
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
}
|
|
.status-bar.success { background: rgba(62,207,142,0.1); color: var(--success); border: 1px solid rgba(62,207,142,0.2); }
|
|
.status-bar.error { background: rgba(247,108,108,0.1); color: var(--error); border: 1px solid rgba(247,108,108,0.2); }
|
|
.status-bar.info { background: rgba(79,142,247,0.1); color: var(--accent); border: 1px solid rgba(79,142,247,0.2); }
|
|
.status-bar.running { background: rgba(255,185,0,0.1); color: #ffb900; border: 1px solid rgba(255,185,0,0.2); }
|
|
|
|
.select-field, .input-field {
|
|
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.2s;
|
|
}
|
|
.select-field:focus, .input-field:focus { border-color: var(--accent); }
|
|
.select-field option { background: var(--surface2); }
|
|
|
|
.field-label {
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
color: var(--muted);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
margin-bottom: 6px;
|
|
}
|
|
|
|
.field-group { margin-bottom: 16px; }
|
|
|
|
.btn-primary {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 10px 20px;
|
|
background: linear-gradient(135deg, var(--accent), var(--accent2));
|
|
color: white;
|
|
border: none;
|
|
border-radius: 8px;
|
|
font-family: 'DM Sans', sans-serif;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
}
|
|
.btn-primary:hover { opacity: 0.9; transform: translateY(-1px); }
|
|
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
|
|
|
|
.btn-secondary {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 10px 20px;
|
|
background: var(--surface2);
|
|
color: var(--text);
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
font-family: 'DM Sans', sans-serif;
|
|
font-size: 14px;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
}
|
|
.btn-secondary:hover { border-color: var(--accent); }
|
|
|
|
.btn-row {
|
|
display: flex;
|
|
gap: 10px;
|
|
flex-wrap: wrap;
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.toggle-wrap {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 12px 14px;
|
|
background: var(--surface2);
|
|
border-radius: 10px;
|
|
border: 1px solid var(--border);
|
|
margin-bottom: 16px;
|
|
}
|
|
.toggle-label { font-size: 14px; font-weight: 500; }
|
|
.toggle-sub { font-size: 12px; color: var(--muted); }
|
|
|
|
.toggle {
|
|
position: relative;
|
|
width: 42px; height: 24px;
|
|
flex-shrink: 0;
|
|
}
|
|
.toggle input { opacity: 0; width: 0; height: 0; }
|
|
.toggle-slider {
|
|
position: absolute;
|
|
inset: 0;
|
|
background: var(--border);
|
|
border-radius: 12px;
|
|
cursor: pointer;
|
|
transition: 0.2s;
|
|
}
|
|
.toggle-slider::before {
|
|
content: '';
|
|
position: absolute;
|
|
width: 18px; height: 18px;
|
|
left: 3px; top: 3px;
|
|
background: white;
|
|
border-radius: 50%;
|
|
transition: 0.2s;
|
|
}
|
|
.toggle input:checked + .toggle-slider { background: var(--accent); }
|
|
.toggle input:checked + .toggle-slider::before { transform: translateX(18px); }
|
|
|
|
/* Features strip */
|
|
.features {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
gap: 20px;
|
|
max-width: 700px;
|
|
width: 100%;
|
|
margin-top: 60px;
|
|
}
|
|
|
|
.feature {
|
|
text-align: center;
|
|
padding: 24px 16px;
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: 14px;
|
|
}
|
|
|
|
.feature-icon {
|
|
font-size: 28px;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.feature-title {
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.feature-desc {
|
|
font-size: 12px;
|
|
color: var(--muted);
|
|
line-height: 1.5;
|
|
}
|
|
|
|
/* Footer */
|
|
footer {
|
|
padding: 20px 40px;
|
|
border-top: 1px solid var(--border);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
font-size: 12px;
|
|
color: var(--muted);
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
|
|
.dot { width: 6px; height: 6px; border-radius: 50%; background: var(--success); display: inline-block; margin-right: 6px; animation: pulse 2s infinite; }
|
|
@keyframes pulse { 0%,100%{opacity:1}50%{opacity:0.3} }
|
|
|
|
@media (max-width: 600px) {
|
|
header { padding: 16px 20px; }
|
|
.features { grid-template-columns: 1fr; }
|
|
.connect-card { padding: 28px 20px; }
|
|
footer { flex-direction: column; gap: 8px; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="page">
|
|
<!-- Blobs -->
|
|
<div class="blob blob-1"></div>
|
|
<div class="blob blob-2"></div>
|
|
|
|
<header>
|
|
<a class="logo" href="/">
|
|
<div class="logo-icon">⇄</div>
|
|
<span class="logo-text">CardSync</span>
|
|
</a>
|
|
<div class="header-right">
|
|
<a id="admin-link" href="/auth/login?mode=admin" class="btn-ghost" style="display:none">Admin</a>
|
|
<button id="logout-btn" class="btn-ghost" style="display:none" onclick="logout()">Abmelden</button>
|
|
</div>
|
|
</header>
|
|
|
|
<main>
|
|
<div class="badge">🔄 Synology → Microsoft 365</div>
|
|
|
|
<h1>Kontakte <em>automatisch</em><br>synchronisieren</h1>
|
|
<p class="subtitle">Verbinde dein Synology Adressbuch mit Microsoft 365 — einmalig autorisieren, ab dann läuft alles automatisch.</p>
|
|
|
|
<!-- Login View -->
|
|
<div id="login-view" class="connect-card">
|
|
<h2 class="card-title">Jetzt verbinden</h2>
|
|
<p class="card-desc">Autorisiere den Zugriff auf deine Microsoft-Kontakte — einmalig, sicher per OAuth.</p>
|
|
|
|
<div class="flow-steps">
|
|
<div class="flow-step">
|
|
<div class="step-num">1</div>
|
|
<span>Mit Microsoft-Konto anmelden</span>
|
|
</div>
|
|
<div class="flow-step">
|
|
<div class="step-num">2</div>
|
|
<span>Kontakte-Zugriff erlauben</span>
|
|
</div>
|
|
<div class="flow-step">
|
|
<div class="step-num">3</div>
|
|
<span>Adressbuch auswählen & Sync startet</span>
|
|
</div>
|
|
</div>
|
|
|
|
<a href="/auth/login" class="btn-ms">
|
|
<div class="ms-logo">
|
|
<span></span><span></span><span></span><span></span>
|
|
</div>
|
|
Mit Microsoft anmelden
|
|
</a>
|
|
|
|
<p class="privacy-note">🔒 Nur Kontakte werden synchronisiert. Keine Daten werden gespeichert oder weitergegeben.</p>
|
|
</div>
|
|
|
|
<!-- Logged-in View -->
|
|
<div id="status-view" class="connect-card" style="max-width:500px">
|
|
<h2 class="card-title">Dein Sync</h2>
|
|
<p class="card-desc">Angemeldet als <strong id="user-email"></strong></p>
|
|
|
|
<div id="sync-status-bar" class="status-bar info" style="margin-bottom:20px">
|
|
<span id="sync-status-text">Lade Status…</span>
|
|
</div>
|
|
|
|
<div class="field-group">
|
|
<div class="field-label">Adressbuch auswählen</div>
|
|
<select id="folder-select" class="select-field">
|
|
<option value="">Wird geladen…</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="field-group">
|
|
<div class="field-label">Sync-Intervall</div>
|
|
<select id="interval-select" class="select-field">
|
|
<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="toggle-wrap">
|
|
<div>
|
|
<div class="toggle-label">Automatischer Sync</div>
|
|
<div class="toggle-sub">Kontakte werden regelmäßig synchronisiert</div>
|
|
</div>
|
|
<label class="toggle">
|
|
<input type="checkbox" id="sync-toggle" onchange="toggleSync()">
|
|
<span class="toggle-slider"></span>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="btn-row">
|
|
<button class="btn-primary" onclick="saveSettings()">💾 Einstellungen speichern</button>
|
|
<button class="btn-secondary" onclick="syncNow()" id="sync-btn">⚡ Jetzt synchronisieren</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="features">
|
|
<div class="feature">
|
|
<div class="feature-icon">🔒</div>
|
|
<div class="feature-title">Sicher</div>
|
|
<div class="feature-desc">OAuth 2.0 — keine Passwörter gespeichert</div>
|
|
</div>
|
|
<div class="feature">
|
|
<div class="feature-icon">⚡</div>
|
|
<div class="feature-title">Automatisch</div>
|
|
<div class="feature-desc">Konfigurierbare Intervalle ab 15 Minuten</div>
|
|
</div>
|
|
<div class="feature">
|
|
<div class="feature-icon">📋</div>
|
|
<div class="feature-title">Einseitig</div>
|
|
<div class="feature-desc">Nur Synology → MS365, kein Datenverlust</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
<footer>
|
|
<span><span class="dot"></span>System läuft</span>
|
|
<span>CardSync — Self-hosted</span>
|
|
</footer>
|
|
</div>
|
|
|
|
<script>
|
|
const API = '';
|
|
|
|
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();
|
|
|
|
document.getElementById('login-view').style.display = 'none';
|
|
document.getElementById('status-view').style.display = 'block';
|
|
document.getElementById('user-email').textContent = me.email;
|
|
document.getElementById('logout-btn').style.display = 'inline-flex';
|
|
|
|
if (me.is_admin) {
|
|
document.getElementById('admin-link').style.display = 'inline-flex';
|
|
}
|
|
|
|
// Sync-Status anzeigen
|
|
updateStatusBar(me.last_sync_status, me.last_sync_message, me.last_sync_at);
|
|
|
|
// Einstellungen laden
|
|
if (me.sync_interval_minutes) {
|
|
document.getElementById('interval-select').value = me.sync_interval_minutes;
|
|
}
|
|
document.getElementById('sync-toggle').checked = me.sync_enabled;
|
|
|
|
// Ordner laden
|
|
await loadFolders(me.carddav_folder);
|
|
|
|
} catch (e) {
|
|
document.getElementById('login-view').style.display = 'block';
|
|
document.getElementById('status-view').style.display = 'none';
|
|
}
|
|
}
|
|
|
|
function updateStatusBar(status, message, lastSyncAt) {
|
|
const bar = document.getElementById('sync-status-bar');
|
|
const text = document.getElementById('sync-status-text');
|
|
bar.className = 'status-bar';
|
|
|
|
if (!status) {
|
|
bar.classList.add('info');
|
|
text.textContent = 'Noch kein Sync durchgeführt';
|
|
return;
|
|
}
|
|
|
|
const timeStr = lastSyncAt ? ` · ${new Date(lastSyncAt).toLocaleString('de-DE')}` : '';
|
|
|
|
if (status === 'success') {
|
|
bar.classList.add('success');
|
|
text.textContent = `✓ ${message || 'Erfolgreich synchronisiert'}${timeStr}`;
|
|
} else if (status === 'error') {
|
|
bar.classList.add('error');
|
|
text.textContent = `✗ Fehler: ${message}${timeStr}`;
|
|
} else if (status === 'running') {
|
|
bar.classList.add('running');
|
|
text.textContent = `⟳ Sync läuft…`;
|
|
} else {
|
|
bar.classList.add('info');
|
|
text.textContent = message || status;
|
|
}
|
|
}
|
|
|
|
async function loadFolders(currentFolder) {
|
|
const sel = document.getElementById('folder-select');
|
|
try {
|
|
const resp = await fetch(`${API}/api/carddav/folders`, { credentials: 'include' });
|
|
if (!resp.ok) {
|
|
sel.innerHTML = '<option value="">Synology nicht konfiguriert</option>';
|
|
return;
|
|
}
|
|
const data = await resp.json();
|
|
sel.innerHTML = '<option value="">Adressbuch wählen…</option>';
|
|
data.folders.forEach(f => {
|
|
const opt = document.createElement('option');
|
|
opt.value = f.url;
|
|
opt.textContent = f.name;
|
|
if (f.url === currentFolder) opt.selected = true;
|
|
sel.appendChild(opt);
|
|
});
|
|
} catch {
|
|
sel.innerHTML = '<option value="">Fehler beim Laden</option>';
|
|
}
|
|
}
|
|
|
|
async function saveSettings() {
|
|
const folder = document.getElementById('folder-select').value;
|
|
const interval = parseInt(document.getElementById('interval-select').value);
|
|
const enabled = document.getElementById('sync-toggle').checked;
|
|
|
|
const resp = await fetch(`${API}/api/user/settings`, {
|
|
method: 'POST',
|
|
credentials: 'include',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ carddav_folder: folder, sync_interval_minutes: interval, sync_enabled: enabled }),
|
|
});
|
|
|
|
if (resp.ok) {
|
|
showToast('Einstellungen gespeichert ✓', 'success');
|
|
} else {
|
|
showToast('Fehler beim Speichern', 'error');
|
|
}
|
|
}
|
|
|
|
async function toggleSync() {
|
|
await saveSettings();
|
|
}
|
|
|
|
async function syncNow() {
|
|
const btn = document.getElementById('sync-btn');
|
|
btn.disabled = true;
|
|
btn.textContent = '⟳ Läuft…';
|
|
|
|
try {
|
|
const resp = await fetch(`${API}/api/user/sync-now`, {
|
|
method: 'POST',
|
|
credentials: 'include',
|
|
});
|
|
const data = await resp.json();
|
|
updateStatusBar(data.status, data.message, new Date().toISOString());
|
|
showToast(data.status === 'success' ? `Sync abgeschlossen: ${data.message}` : `Fehler: ${data.message}`, data.status);
|
|
} catch {
|
|
showToast('Verbindungsfehler', 'error');
|
|
}
|
|
|
|
btn.disabled = false;
|
|
btn.textContent = '⚡ Jetzt synchronisieren';
|
|
}
|
|
|
|
async function logout() {
|
|
await fetch(`${API}/auth/logout`, { method: 'POST', credentials: 'include' });
|
|
window.location.reload();
|
|
}
|
|
|
|
function showToast(msg, type = 'info') {
|
|
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: 14px; 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'};
|
|
animation: slideIn 0.3s ease; font-family: 'DM Sans', sans-serif;
|
|
`;
|
|
t.textContent = msg;
|
|
document.body.appendChild(t);
|
|
setTimeout(() => t.remove(), 4000);
|
|
}
|
|
|
|
init();
|
|
</script>
|
|
</body>
|
|
</html>
|