Files
icloud-contacts-sync/src/api/templates/dashboard.html
T

387 lines
11 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de) -->
<!-- Licensed under the MIT License. See LICENSE file in project root for details. -->
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
<title>Dashboard {{ account_name }}</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: #f5f5f5;
color: #333;
line-height: 1.6;
overflow-y: scroll;
}
.container {
max-width: 800px;
margin: 0 auto;
padding: 2rem 1rem;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
h1 {
font-size: 1.5rem;
color: #222;
}
.nav-links {
display: flex;
gap: 0.75rem;
}
.nav-links a {
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.5rem 1rem;
background: #0066cc;
color: #fff;
text-decoration: none;
border-radius: 6px;
font-size: 0.875rem;
transition: background 0.15s;
}
.nav-links a:hover {
background: #0055aa;
}
.meta {
color: #666;
font-size: 0.875rem;
margin-bottom: 1.5rem;
}
.dashboard-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
margin-bottom: 1.5rem;
}
@media (max-width: 600px) {
.dashboard-grid {
grid-template-columns: 1fr;
}
}
.box {
background: #fff;
border-radius: 8px;
padding: 1.25rem;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.box-title {
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: #888;
margin-bottom: 1rem;
}
.stat-value {
font-size: 2rem;
font-weight: 700;
color: #222;
}
.stat-label {
font-size: 0.875rem;
color: #666;
margin-top: 0.25rem;
}
.sync-status {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.status-dot {
width: 10px;
height: 10px;
border-radius: 50%;
flex-shrink: 0;
}
.status-dot.success {
background: #28a745;
}
.status-dot.failed {
background: #dc3545;
}
.status-dot.running {
background: #ffc107;
}
.sync-detail {
font-size: 0.875rem;
color: #666;
}
.sync-detail strong {
color: #333;
}
.birthday-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.birthday-item {
display: block;
background: #fff;
border-radius: 8px;
padding: 1rem 1.25rem;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
text-decoration: none;
color: inherit;
transition: box-shadow 0.15s, transform 0.15s;
}
.birthday-item:hover {
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
transform: translateY(-1px);
}
.birthday-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 1rem;
}
.birthday-name {
font-size: 1.125rem;
font-weight: 600;
color: #222;
}
.birthday-organization {
font-size: 0.875rem;
color: #666;
margin-top: 0.25rem;
}
.birthday-date {
font-size: 0.875rem;
color: #888;
margin-top: 0.25rem;
}
.birthday-date::before {
content: "\1F382 ";
}
.contact-photo {
width: 80px;
height: 80px;
border-radius: 50%;
object-fit: cover;
flex-shrink: 0;
}
.empty-state {
color: #888;
font-size: 0.875rem;
font-style: italic;
}
.search-form {
display: flex;
gap: 0.5rem;
margin-bottom: 1.5rem;
}
.search-form input[type="text"] {
flex: 1;
padding: 0.75rem 1rem;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 1rem;
background: #fff;
}
.search-form input[type="text"]:focus {
outline: none;
border-color: #0066cc;
}
.search-form button {
padding: 0.75rem 1.5rem;
background: #0066cc;
color: #fff;
border: none;
border-radius: 6px;
font-size: 1rem;
cursor: pointer;
}
.search-form button:hover {
background: #0055aa;
}
.contact-stats {
display: flex;
gap: 1.5rem;
}
.contact-stats-left {
flex: 1;
}
.contact-stats-right {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.5rem;
justify-content: center;
}
.special-search {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.4rem 0.6rem;
background: #f8f9fa;
border-radius: 4px;
text-decoration: none;
color: inherit;
font-size: 0.8125rem;
transition: background 0.15s;
}
.special-search:hover {
background: #e9ecef;
}
.special-search-label {
color: #555;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1><a href="/" style="text-decoration:none;color:inherit;display:inline-flex;align-items:center;gap:0.4rem"><img src="/static/favicon.svg" alt="" style="height:1.5em;width:1.5em;vertical-align:middle">Kontakte</a></h1>
</div>
<div class="meta">
Angemeldet als <strong>{{ current_user }}</strong>
{% if is_admin %}(<a href="/admin" style="color:inherit">Admin</a>{% if show_all %}, sieht alle Accounts{% endif %}){% else %}(Account: {{ account_name }}){% endif %}
</div>
<form class="search-form" method="get" action="/search">
<input type="text" name="search" placeholder="Suche nach Name oder Organisation...">
<button type="submit">Suchen</button>
</form>
<div class="dashboard-grid">
<div class="box">
<div class="box-title">Kontakte</div>
<div class="contact-stats">
<div class="contact-stats-left">
<div class="stat-value">{{ contact_count }}</div>
<div class="stat-label">gespeicherte Kontakte</div>
</div>
<div class="contact-stats-right">
<a href="/search/special?type=no_photo" class="special-search">
<span class="special-search-label">ohne Bild</span>
</a>
<a href="/search/special?type=no_city" class="special-search">
<span class="special-search-label">ohne City</span>
</a>
<a href="/search/special?type=no_social" class="special-search">
<span class="special-search-label">ohne Social</span>
</a>
</div>
</div>
</div>
<div class="box">
<div class="box-title">Letzter Sync</div>
{% if last_sync %}
<div class="sync-status">
<div class="status-dot {{ last_sync.status }}"></div>
<div class="sync-detail">
<strong>{{ last_sync.status }}</strong> · {{ last_sync.sync_type }}
</div>
</div>
<div class="sync-detail">
{{ last_sync.started_at }}
{% if last_sync.contacts_upserted is not none %}
· {{ last_sync.contacts_upserted }} aktualisiert
{% endif %}
{% if last_sync.contacts_deleted is not none and last_sync.contacts_deleted > 0 %}
· {{ last_sync.contacts_deleted }} gelöscht
{% endif %}
</div>
{% if last_sync_with_changes %}
<div class="sync-detail">
{{ last_sync_with_changes.started_at }} · {{ last_sync_with_changes.contacts_upserted }} aktualisiert
</div>
{% endif %}
{% if last_sync.error_message %}
<div class="sync-detail" style="color: #dc3545; margin-top: 0.5rem;">
{{ last_sync.error_message }}
</div>
{% endif %}
{% else %}
<div class="empty-state">Noch kein Sync durchgeführt.</div>
{% endif %}
</div>
</div>
<div class="box">
<div class="box-title">Geburtstage nächste 7 Tage</div>
{% if upcoming_birthdays %}
<div class="birthday-list">
{% for b in upcoming_birthdays %}
<a href="/contacts/{{ b.id }}" class="birthday-item"{% if b.birthday.month == today.month and b.birthday.day == today.day %} style="background: #fff3cd;"{% endif %}>
<div class="birthday-header">
<div>
<div class="birthday-name">{{ b.full_name or '(Kein Name)' }}</div>
{% if b.organization %}
<div class="birthday-organization">{{ b.organization }}</div>
{% endif %}
<div class="birthday-date">
{{ b.birthday.strftime('%d.%m.') }}{% if b.birthday.year and b.birthday.year < current_year %} · {{ current_year - b.birthday.year }} Jahre{% endif %}
</div>
</div>
{% if b.photo_url %}
<img src="{{ b.photo_url }}" alt="" class="contact-photo" onerror="this.style.display='none'">
{% endif %}
</div>
</a>
{% endfor %}
</div>
{% else %}
<div class="empty-state">Keine Geburtstage in den nächsten 7 Tagen.</div>
{% endif %}
</div>
</div>
</body>
</html>