Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions api/src/static/chart-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,22 @@ export function fmtHours(seconds) {
}

// Accessibility: versteckte Datentabelle je Diagramm (WCAG 1.1.1).
// Das aria-label des Canvas fasst nur zusammen — diese sr-only-<table> macht
// jeden Einzel-Messpunkt fuer Screenreader zugaenglich. Canvas verweist via
// aria-describedby auf die Tabelle. Idempotent: ersetzt bei Re-Render.
// Das aria-label des Canvas fasst nur zusammen — diese Tabelle macht jeden
// Einzel-Messpunkt fuer Screenreader zugaenglich. Ein sr-only-Wrapper begrenzt
// ihre intrinsische Breite. Canvas verweist via aria-describedby auf die
// Tabelle. Idempotent: ersetzt bei Re-Render.
// createElement statt innerHTML (kein XSS mit dynamischen Werten).
export function buildChartDataTable(canvas, labels, datasets, captionText = '') {
if (!canvas?.id) return;
const tableId = `${canvas.id}-table`;
document.getElementById(tableId)?.remove();
const wrapperId = `${tableId}-wrapper`;
document.getElementById(wrapperId)?.remove();

const wrapper = document.createElement('div');
wrapper.id = wrapperId;
wrapper.className = 'sr-only';
const table = document.createElement('table');
table.id = tableId;
table.className = 'sr-only';

if (captionText) {
const caption = document.createElement('caption');
Expand Down Expand Up @@ -84,6 +88,7 @@ export function buildChartDataTable(canvas, labels, datasets, captionText = '')
});
table.appendChild(tbody);

canvas.insertAdjacentElement('afterend', table);
wrapper.appendChild(table);
canvas.insertAdjacentElement('afterend', wrapper);
canvas.setAttribute('aria-describedby', tableId);
}
50 changes: 44 additions & 6 deletions api/src/static/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,34 @@ html:not(.dark) {
border: 1px solid var(--card-border);
box-shadow: var(--card-shadow);
}
.card-hero {
border-color: color-mix(in srgb, var(--accent) 18%, var(--card-border));
}
.card-metric {
box-shadow: 0 1px 3px rgba(0,0,0,.18), 0 3px 12px rgba(0,0,0,.1);
}
.card-collection,
.card-status,
.card-insight {
box-shadow: none;
}
.card-collection {
background: var(--surface);
}
.card-form {
box-shadow: 0 1px 3px rgba(0,0,0,.14);
}
.card-insight {
line-height: 1.6;
}
.prose-layout {
display: flex;
flex-direction: column;
gap: var(--sp-6);
color: var(--muted);
font-size: var(--text-sm);
line-height: 1.625;
}
.card h2 {
font-size: var(--text-xs);
color: var(--muted);
Expand Down Expand Up @@ -187,6 +215,16 @@ html:not(.dark) .tab-btn.active { color: #047857; border-bottom-color: #047857;
.topbar-btn:hover { background: var(--surface-2); color: var(--text); }
.topbar-btn-danger:hover { color: var(--red); background: color-mix(in srgb, var(--red) 12%, transparent); }
.topbar-divider { width: 1px; height: 20px; background: var(--card-border); margin: 0 var(--sp-2); }
@media (max-width: 640px) {
.dash-topbar .dash-shell { flex-wrap: wrap; }
.dash-topbar nav[aria-label="Hauptnavigation"] {
flex: 1 0 100%;
min-width: 0;
flex-wrap: wrap;
}
.dash-topbar .topbar-btn { padding: 5px 8px; }
.dash-topbar .topbar-divider { display: none; }
}

/* ── View Toolbar + Segmented Time-Range ───────────────────────────────── */
.view-toolbar { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); flex-wrap: wrap; }
Expand Down Expand Up @@ -861,12 +899,12 @@ html:not(.dark) .recommendation-block { background: rgba(16,185,129,.07); }
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: none; }
}
.hero-card-wrap > .card,
.tab-panel > .card { animation: card-in var(--dur-base) var(--ease-out) backwards; }
.tab-panel > .card:nth-child(1) { animation-delay: 0ms; }
.tab-panel > .card:nth-child(2) { animation-delay: 40ms; }
.tab-panel > .card:nth-child(3) { animation-delay: 80ms; }
.tab-panel > .card:nth-child(4) { animation-delay: 120ms; }
.hero-card-wrap > .card-hero,
.tab-panel > .card-metric { animation: card-in var(--dur-base) var(--ease-out) backwards; }
.tab-panel > .card-metric:nth-child(1) { animation-delay: 0ms; }
.tab-panel > .card-metric:nth-child(2) { animation-delay: 40ms; }
.tab-panel > .card-metric:nth-child(3) { animation-delay: 80ms; }
.tab-panel > .card-metric:nth-child(4) { animation-delay: 120ms; }

/* ── Skeleton-Screens — perceived performance, 300-ms-Regel ─────────────────────
Wrapper bleibt 300 ms unsichtbar; kommt der echte Inhalt vorher, wird das Markup
Expand Down
4 changes: 2 additions & 2 deletions api/src/templates/accessibility.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@
{% block title %}PulseBase — Barrierefreiheitserklärung{% endblock %}
{% block content %}
<div class="max-w-2xl mx-auto px-4 py-10 pb-16">
<div class="flex items-center gap-4 mb-6">
<div class="flex flex-wrap items-center gap-4 mb-6">
<a href="#" data-history-back
class="inline-flex items-center gap-1.5 px-3.5 py-2 rounded-xl bg-black/[0.06] dark:bg-white/[0.06] border border-black/10 dark:border-white/10 text-slate-600 dark:text-slate-300 text-sm font-medium hover:bg-black/10 dark:hover:bg-white/10 transition-colors">
← Zurück
</a>
<h1 class="text-2xl font-bold text-slate-900 dark:text-slate-100">Barrierefreiheitserklärung</h1>
</div>

<div class="card flex flex-col gap-6 text-sm text-slate-700 dark:text-slate-300 leading-relaxed">
<div class="prose-layout text-slate-700 dark:text-slate-300">

<section>
<h2 class="text-base font-semibold text-slate-900 dark:text-slate-100 mb-2">1. Geltungsbereich</h2>
Expand Down
2 changes: 1 addition & 1 deletion api/src/templates/account_delete_confirm.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ <h1 class="text-2xl font-bold tracking-tight text-slate-100 mb-1">PulseBase</h1>
<p class="text-slate-400 text-sm mb-8">Kontolöschung bestätigen</p>

<div class="w-full max-w-sm">
<div class="card">
<div class="card card-form">
{% if error %}
<div class="mb-4 px-4 py-3 rounded-xl bg-red-500/15 border border-red-500/25 text-red-300 text-sm">{{ error }}</div>
<p class="mt-3 text-slate-400 text-sm">
Expand Down
2 changes: 1 addition & 1 deletion api/src/templates/account_delete_pending.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ <h1 class="text-2xl font-bold tracking-tight text-slate-100 mb-1">PulseBase</h1>
<p class="text-slate-400 text-sm mb-8">Kontolöschung</p>

<div class="w-full max-w-sm">
<div class="card">
<div class="card card-status">
{% if error %}
<div class="mb-4 px-4 py-3 rounded-xl bg-red-500/15 border border-red-500/25 text-red-300 text-sm">{{ error }}</div>
{% else %}
Expand Down
18 changes: 9 additions & 9 deletions api/src/templates/activity.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
{% endblock %}
{% block content %}

<div class="card mx-4 mt-4 flex flex-col gap-2">
<div class="card card-status mx-4 mt-4 flex flex-col gap-2">
<a class="inline-flex items-center gap-1 text-sm text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-100 transition-colors no-underline w-fit" href="/dashboard">← Dashboard</a>
<h1 class="text-2xl font-bold text-slate-900 dark:text-slate-100 m-0" id="act-title">—</h1>
<div class="text-sm text-slate-600 dark:text-slate-400" id="act-meta">—</div>
Expand All @@ -18,7 +18,7 @@ <h1 class="text-2xl font-bold text-slate-900 dark:text-slate-100 m-0" id="act-ti
<div class="px-4 pb-10 flex flex-col gap-4">

<!-- Training Effect -->
<div class="card hidden" id="effect-card">
<div class="card card-metric hidden" id="effect-card">
<h2>Trainingswirkung</h2>
<p class="card-desc">Garmin Firstbeat · Skala 0–5 · 1–2 Erhalt · 2–3 Verbesserung · 3–4 Stark verbessernd · 4–5 Überbelastung</p>
<div id="effect-aerobic" class="effect-row">
Expand All @@ -40,7 +40,7 @@ <h2>Trainingswirkung</h2>
</div>

<!-- RPE Rating -->
<div class="card" id="rpe-card">
<div class="card card-form" id="rpe-card">
<h2>Anstrengung (RPE)</h2>
<p class="card-desc">Rating of Perceived Exertion — deine subjektive Einschätzung der Belastung · Skala 1 (sehr leicht) bis 10 (maximale Anstrengung) · Foster et al. (2001)</p>
<div class="rpe-chips" id="rpe-chips"></div>
Expand All @@ -64,14 +64,14 @@ <h2>Anstrengung (RPE)</h2>
</div>

<!-- Laufökonomie -->
<div class="card hidden" id="economy-card">
<div class="card card-metric hidden" id="economy-card">
<h2>Laufökonomie</h2>
<p class="card-desc">Biomechanische Effizienz-Kennzahlen · Garmin Running Dynamics</p>
<div id="economy-rows"></div>
</div>

<!-- GPS Map -->
<div class="card hidden" id="map-card">
<div class="card card-metric hidden" id="map-card">
<div class="flex items-center justify-between gap-2 mb-2">
<h2 class="mb-0">GPS-Track</h2>
<div id="map-layer-control" class="hidden items-center gap-1.5">
Expand All @@ -93,19 +93,19 @@ <h2 class="mb-0">GPS-Track</h2>

<!-- Charts -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-6">
<div class="card hidden" id="hr-card">
<div class="card card-metric hidden" id="hr-card">
<h2>Herzfrequenz</h2>
<div class="chart-wrap"><canvas id="hr-chart"></canvas></div>
</div>
<div class="card hidden" id="pace-card">
<div class="card card-metric hidden" id="pace-card">
<h2 id="pace-title">Pace</h2>
<div class="chart-wrap"><canvas id="pace-chart"></canvas></div>
</div>
<div class="card hidden" id="elev-card">
<div class="card card-metric hidden" id="elev-card">
<h2>Höhenprofil</h2>
<div class="chart-wrap"><canvas id="elev-chart"></canvas></div>
</div>
<div class="card hidden" id="cadence-card">
<div class="card card-metric hidden" id="cadence-card">
<h2>Kadenz</h2>
<div class="chart-wrap"><canvas id="cadence-chart"></canvas></div>
</div>
Expand Down
34 changes: 17 additions & 17 deletions api/src/templates/dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<span class="text-lg font-bold tracking-tight text-slate-900 dark:text-slate-100">PulseBase</span>
<span class="text-sm text-slate-600 dark:text-slate-400">{{ user.name }}</span>
</a>
<nav class="flex items-center gap-1" aria-label="Konto">
<nav class="flex items-center gap-1" aria-label="Hauptnavigation">
{% if user.epilepsy_mode %}
<a href="/epilepsy" class="topbar-btn">Anfallstagebuch</a>
{% endif %}
Expand Down Expand Up @@ -41,7 +41,7 @@
<!-- ── Onboarding-/Datenlatenz-Hinweis (einmalig, dismissbar) ── -->
<div id="onboarding-hint" class="px-4 sm:px-6 pt-5 is-hidden">
<div role="status" class="flex items-start gap-3 px-4 py-3 rounded-xl bg-sky-500/10 border border-sky-500/25 text-sky-800 dark:text-sky-200 text-sm">
<span class="shrink-0">⏳</span>
<span class="shrink-0" aria-hidden="true">⏳</span>
<span class="flex-1">
Deine Daten füllen sich nach und nach: erste Trends nach ~7 Tagen,
die ML-Prognose nach ~30 Tagen Daten. Solange einzelne Kacheln noch leer bleiben, ist das normal.
Expand Down Expand Up @@ -71,7 +71,7 @@
<!-- ── Hero Card (immer sichtbar) ───────────────────────────── -->
<div class="px-4 sm:px-6 pt-5">
<div class="hero-card-wrap">
<div class="card" id="bento-hero">
<div class="card card-hero" id="bento-hero">
<div class="skeleton" aria-hidden="true">
<div class="hero-grid">
<div class="hero-ring-panel">
Expand Down Expand Up @@ -123,13 +123,13 @@ <h2 id="curated-metrics-heading" class="metrics-section-heading">Deine Metriken<
<!-- ── Tab: Training ─────────────────────────────────────────── -->
<div id="tab-training" role="tabpanel" aria-labelledby="tab-btn-training" tabindex="0" class="tab-panel grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-6 px-4 sm:px-6 pt-6 pb-12">

<div class="card col-span-full" id="weekly-card">
<div class="card card-metric col-span-full" id="weekly-card">
<h2>Trainingsübersicht</h2>
<div class="chart-wrap"><canvas id="weekly-chart"></canvas></div>
<p class="empty hidden" id="weekly-empty">Noch keine Daten</p>
</div>

<div class="card col-span-full" data-href="/metrics/physical">
<div class="card card-metric col-span-full" data-href="/metrics/physical">
<h2><a class="card-title-link" href="/metrics/physical">Training Load</a> <span class="text-sm font-normal text-slate-600 dark:text-slate-400 ml-1">Ermüdung · Fitness · Form</span></h2>
<div class="flex gap-6 mb-3 text-sm">
<span>Ermüdung&nbsp;<strong id="tl-atl">—</strong></span>
Expand All @@ -140,13 +140,13 @@ <h2><a class="card-title-link" href="/metrics/physical">Training Load</a> <span
<p class="empty hidden" id="training-load-empty">Noch keine Aktivitätsdaten</p>
</div>

<div class="card col-span-full" data-href="/metrics/intensity-minutes">
<div class="card card-metric col-span-full" data-href="/metrics/intensity-minutes">
<h2><a class="card-title-link" href="/metrics/intensity-minutes">Intensitätsminuten</a></h2>
<div class="chart-wrap"><canvas id="intensity-chart"></canvas></div>
<p class="empty hidden" id="intensity-empty">Noch keine Daten</p>
</div>

<div class="card col-span-full">
<div class="card card-collection col-span-full">
<h2>Letzte Aktivitäten</h2>
<div id="activities-container">
<div class="skeleton" aria-hidden="true">
Expand All @@ -164,37 +164,37 @@ <h2>Letzte Aktivitäten</h2>
<!-- ── Tab: Verlauf ──────────────────────────────────────────── -->
<div id="tab-verlauf" role="tabpanel" aria-labelledby="tab-btn-verlauf" tabindex="0" class="tab-panel grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-6 px-4 sm:px-6 pt-6 pb-12 hidden">

<div class="card" data-href="/metrics/body-battery-custom">
<div class="card card-metric" data-href="/metrics/body-battery-custom">
<h2><a class="card-title-link" href="/metrics/body-battery-custom">Body Battery</a></h2>
<div class="chart-wrap"><canvas id="battery-chart"></canvas></div>
<p class="empty hidden" id="battery-empty">Noch keine Daten</p>
</div>

<div class="card" data-href="/metrics/hr-zscore">
<div class="card card-metric" data-href="/metrics/hr-zscore">
<h2><a class="card-title-link" href="/metrics/hr-zscore">Ruhepuls</a></h2>
<div class="chart-wrap"><canvas id="hr-chart"></canvas></div>
<p class="empty hidden" id="hr-empty">Noch keine Daten</p>
</div>

<div class="card" data-href="/metrics/steps">
<div class="card card-metric" data-href="/metrics/steps">
<h2><a class="card-title-link" href="/metrics/steps">Schritte</a></h2>
<div class="chart-wrap"><canvas id="steps-chart"></canvas></div>
<p class="empty hidden" id="steps-empty">Noch keine Daten</p>
</div>

<div class="card" data-href="/metrics/stress-score-custom">
<div class="card card-metric" data-href="/metrics/stress-score-custom">
<h2><a class="card-title-link" href="/metrics/stress-score-custom">Stress</a></h2>
<div class="chart-wrap"><canvas id="stress-chart"></canvas></div>
<p class="empty hidden" id="stress-empty">Noch keine Daten</p>
</div>

<div class="card col-span-full">
<div class="card card-metric col-span-full">
<h2>Kalorien</h2>
<div class="chart-wrap"><canvas id="calories-chart"></canvas></div>
<p class="empty hidden" id="calories-empty">Noch keine Daten</p>
</div>

<div class="card col-span-full hidden" id="ml-verlauf-card">
<div class="card card-insight col-span-full hidden" id="ml-verlauf-card">
<h2>KI-Einblicke <span class="text-xs font-normal text-slate-500 dark:text-slate-400">— automatisierte Analyse, kein medizinischer Befund</span></h2>
<div id="ml-verlauf-content"></div>
</div>
Expand All @@ -204,25 +204,25 @@ <h2>KI-Einblicke <span class="text-xs font-normal text-slate-500 dark:text-slate
<!-- ── Tab: Erholung ─────────────────────────────────────────── -->
<div id="tab-erholung" role="tabpanel" aria-labelledby="tab-btn-erholung" tabindex="0" class="tab-panel grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-6 px-4 sm:px-6 pt-6 pb-12 hidden">

<div class="card" data-href="/metrics/sleep-score-custom">
<div class="card card-metric" data-href="/metrics/sleep-score-custom">
<h2><a class="card-title-link" href="/metrics/sleep-score-custom">Schlaf-Score</a></h2>
<div class="chart-wrap"><canvas id="sleep-chart"></canvas></div>
<p class="empty hidden" id="sleep-empty">Noch keine Daten</p>
</div>

<div class="card" data-href="/metrics/hrv-status-custom">
<div class="card card-metric" data-href="/metrics/hrv-status-custom">
<h2><a class="card-title-link" href="/metrics/hrv-status-custom">HRV-Verlauf</a></h2>
<div class="chart-wrap"><canvas id="hrv-trend-chart"></canvas></div>
<p class="empty hidden" id="hrv-trend-empty">Noch keine Daten</p>
</div>

<div class="card col-span-full" data-href="/metrics/sleep-score-custom">
<div class="card card-metric col-span-full" data-href="/metrics/sleep-score-custom">
<h2><a class="card-title-link" href="/metrics/sleep-score-custom">Schlafphasen</a></h2>
<div class="chart-wrap"><canvas id="sleep-stages-chart"></canvas></div>
<p class="empty hidden" id="sleep-stages-empty">Noch keine Daten</p>
</div>

<div class="card col-span-full hidden" id="ml-erholung-card">
<div class="card card-insight col-span-full hidden" id="ml-erholung-card">
<h2>KI-Einblicke <span class="text-xs font-normal text-slate-500 dark:text-slate-400">— automatisierte Analyse, kein medizinischer Befund</span></h2>
<div id="ml-erholung-content"></div>
</div>
Expand Down
6 changes: 3 additions & 3 deletions api/src/templates/epilepsy.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ <h1 class="text-2xl font-bold text-slate-900 dark:text-slate-100">Anfallstagebuc
</div>

<!-- Risiko-Indikator -->
<div class="card" id="risk-card">
<div class="card card-status" id="risk-card">
<h2>Tagesaktuelles Risiko</h2>
<div class="flex items-center gap-3 mt-3" id="risk-content">
<div class="w-3 h-3 rounded-full bg-slate-300 shrink-0" id="risk-dot"></div>
Expand Down Expand Up @@ -103,7 +103,7 @@ <h2>Tagesaktuelles Risiko</h2>
</div>

<!-- Anfall loggen -->
<div class="card">
<div class="card card-form">
<h2 id="log-card-title">Anfall erfassen</h2>
<div class="flex flex-col gap-4 mt-2">
<div class="flex flex-col gap-1.5">
Expand Down Expand Up @@ -164,7 +164,7 @@ <h2 id="log-card-title">Anfall erfassen</h2>
</div>

<!-- Ereignisliste -->
<div class="card">
<div class="card card-collection">
<h2>Verlauf</h2>
<div id="event-list" class="flex flex-col gap-3 mt-3">
<p class="text-sm text-slate-600 dark:text-slate-400">Lade…</p>
Expand Down
Loading