First Commit with complete Setup coded by Claude 4.7 Opus
This commit is contained in:
commit
bf1d37de77
19 changed files with 4544 additions and 0 deletions
1257
frontend/admin.html
Normal file
1257
frontend/admin.html
Normal file
File diff suppressed because it is too large
Load diff
766
frontend/index.html
Normal file
766
frontend/index.html
Normal file
|
|
@ -0,0 +1,766 @@
|
|||
<!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>
|
||||
29
frontend/success.html
Normal file
29
frontend/success.html
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>CardSync — Verbunden!</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=DM+Sans:opsz,wght@9..40,300;9..40,400;9..40,500&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root { --bg:#0e0f14;--accent:#4f8ef7;--accent2:#7c5cfc;--success:#3ecf8e;--text:#e8eaf0;--muted:#7a7f96; }
|
||||
body { min-height:100vh;background:var(--bg);color:var(--text);font-family:'DM Sans',sans-serif;display:flex;align-items:center;justify-content:center; }
|
||||
.card { text-align:center;max-width:440px;padding:48px 40px; }
|
||||
.icon { font-size:64px;margin-bottom:24px;animation:pop 0.5s cubic-bezier(0.34,1.56,0.64,1); }
|
||||
@keyframes pop { from{transform:scale(0);opacity:0} to{transform:scale(1);opacity:1} }
|
||||
h1 { font-family:'DM Serif Display',serif;font-size:32px;margin-bottom:12px;background:linear-gradient(135deg,var(--accent),var(--accent2));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text; }
|
||||
p { color:var(--muted);margin-bottom:32px;line-height:1.7;font-size:15px; }
|
||||
.btn { display:inline-flex;align-items:center;gap:8px;padding:12px 28px;background:linear-gradient(135deg,var(--accent),var(--accent2));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:opacity 0.2s; }
|
||||
.btn:hover { opacity:0.9; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<div class="icon">✅</div>
|
||||
<h1>Erfolgreich verbunden!</h1>
|
||||
<p>Dein Microsoft-365-Konto ist jetzt mit CardSync verknüpft. Wähle auf der Startseite dein Synology-Adressbuch aus und aktiviere den Sync.</p>
|
||||
<a href="/" class="btn">Zum Dashboard →</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Add table
Add a link
Reference in a new issue