add health import
This commit is contained in:
+293
-2
@@ -625,9 +625,35 @@ body.page-dashboard .content {
|
||||
|
||||
.day-summary-card__title {
|
||||
display: block;
|
||||
font-size: clamp(1.05rem, 2.5vw, 1.5rem);
|
||||
font-weight: 400;
|
||||
font-size: clamp(1.35rem, 4vw, 2.35rem);
|
||||
font-weight: 650;
|
||||
line-height: 1.2;
|
||||
color: #fff;
|
||||
text-shadow: 0 8px 24px rgba(0, 0, 0, 0.38);
|
||||
}
|
||||
|
||||
.day-summary-card__chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.45rem;
|
||||
margin-top: 0.85rem;
|
||||
}
|
||||
|
||||
.day-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 2rem;
|
||||
padding: 0.35rem 0.72rem;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
border: 1px solid rgba(255, 255, 255, 0.16);
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
.day-chip--bonus {
|
||||
background: rgba(139, 255, 207, 0.16);
|
||||
border-color: rgba(139, 255, 207, 0.32);
|
||||
}
|
||||
|
||||
.day-summary-card__head {
|
||||
@@ -778,6 +804,72 @@ body.page-dashboard .content {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.timeline-card__comment {
|
||||
margin: 0.28rem 0 0;
|
||||
color: rgba(239, 247, 255, 0.9);
|
||||
font-size: 0.98rem;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.timeline-card__stats {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.42rem;
|
||||
margin-top: 0.72rem;
|
||||
}
|
||||
|
||||
.timeline-card__stats span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 1.9rem;
|
||||
padding: 0.32rem 0.66rem;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.14);
|
||||
color: rgba(239, 247, 255, 0.88);
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.timeline-route-map {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 24rem;
|
||||
height: 10.5rem;
|
||||
margin-top: 0.8rem;
|
||||
overflow: hidden;
|
||||
border-radius: 1.1rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.timeline-route-map svg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.timeline-route-map polyline {
|
||||
fill: none;
|
||||
stroke: #1494de;
|
||||
stroke-width: 5;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
filter: drop-shadow(0 1px 2px rgba(255, 255, 255, 0.9));
|
||||
}
|
||||
|
||||
.timeline-route-map a {
|
||||
position: absolute;
|
||||
right: 0.35rem;
|
||||
bottom: 0.28rem;
|
||||
padding: 0.12rem 0.35rem;
|
||||
border-radius: 0.45rem;
|
||||
background: rgba(255, 255, 255, 0.82);
|
||||
color: rgba(10, 22, 35, 0.82);
|
||||
font-size: 0.66rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.timeline-card__meta strong {
|
||||
font-size: 0.92rem;
|
||||
color: rgba(239, 247, 255, 0.68);
|
||||
@@ -1620,6 +1712,13 @@ body.page-dashboard .content {
|
||||
border-color: rgba(69, 201, 141, 0.34);
|
||||
}
|
||||
|
||||
.week-insight-card {
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.78), rgba(248, 253, 255, 0.62)),
|
||||
radial-gradient(circle at top left, rgba(90, 188, 242, 0.14), transparent 42%);
|
||||
border-color: rgba(92, 129, 160, 0.2);
|
||||
}
|
||||
|
||||
.range-moment-list__item,
|
||||
.timeline-card__body h3,
|
||||
.day-summary-card__title,
|
||||
@@ -1655,6 +1754,27 @@ body.page-dashboard .content {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.week-insight-card {
|
||||
display: grid;
|
||||
gap: 0.55rem;
|
||||
margin-bottom: 1rem;
|
||||
padding: 1rem 1.1rem;
|
||||
border-radius: 1.55rem;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(25, 36, 56, 0.72), rgba(16, 25, 40, 0.58)),
|
||||
radial-gradient(circle at top left, rgba(139, 228, 255, 0.16), transparent 44%);
|
||||
}
|
||||
|
||||
.week-insight-card p {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.week-insight-card strong {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.range-card {
|
||||
padding: 1rem 1.1rem;
|
||||
border-radius: 1.45rem;
|
||||
@@ -1746,6 +1866,38 @@ body.page-dashboard .content {
|
||||
border-color: rgba(255, 143, 143, 0.18);
|
||||
}
|
||||
|
||||
.health-import-progress {
|
||||
display: grid;
|
||||
gap: 0.45rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.health-import-progress__bar {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 0.72rem;
|
||||
overflow: hidden;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
.health-import-progress__bar::-webkit-progress-bar {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.health-import-progress__bar::-webkit-progress-value {
|
||||
border-radius: inherit;
|
||||
background: linear-gradient(90deg, var(--primary), var(--accent));
|
||||
}
|
||||
|
||||
.health-import-progress__bar::-moz-progress-bar {
|
||||
border-radius: inherit;
|
||||
background: linear-gradient(90deg, var(--primary), var(--accent));
|
||||
}
|
||||
|
||||
.options-logout-form {
|
||||
margin: 0;
|
||||
}
|
||||
@@ -2116,6 +2268,145 @@ body.page-dashboard .content {
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
.dashboard-overlay__backdrop,
|
||||
.options-overlay__backdrop {
|
||||
background: rgba(224, 235, 245, 0.64);
|
||||
}
|
||||
|
||||
.dashboard-modal,
|
||||
.dashboard-modal--settings,
|
||||
.options-modal {
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.97), rgba(242, 248, 253, 0.93)),
|
||||
radial-gradient(circle at 50% 0%, rgba(90, 188, 242, 0.16), transparent 46%);
|
||||
border-color: rgba(92, 129, 160, 0.2);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.dashboard-modal__controls,
|
||||
.options-modal__controls {
|
||||
background: linear-gradient(180deg, rgba(247, 252, 255, 0.96), rgba(247, 252, 255, 0.76), transparent);
|
||||
}
|
||||
|
||||
.dashboard-modal__round {
|
||||
background: rgba(255, 255, 255, 0.74);
|
||||
border-color: rgba(92, 129, 160, 0.22);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.dashboard-modal__round--confirm {
|
||||
background: linear-gradient(180deg, rgba(90, 188, 242, 0.24), rgba(99, 217, 180, 0.2));
|
||||
color: #0e2b45;
|
||||
}
|
||||
|
||||
.dashboard-modal__subtitle,
|
||||
.overlay-signal-card p {
|
||||
color: rgba(18, 48, 75, 0.62);
|
||||
}
|
||||
|
||||
.dashboard-modal__textarea textarea,
|
||||
.options-modal input[type="text"],
|
||||
.options-modal input[type="password"],
|
||||
.options-modal input[type="number"],
|
||||
.options-modal input[type="date"],
|
||||
.options-modal select,
|
||||
.options-modal textarea {
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
border-color: rgba(92, 129, 160, 0.22);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.overlay-signal-card {
|
||||
border-top-color: rgba(92, 129, 160, 0.16);
|
||||
}
|
||||
|
||||
.overlay-signal-card__buttons {
|
||||
background: rgba(18, 48, 75, 0.08);
|
||||
}
|
||||
|
||||
.overlay-signal-card__buttons button + button {
|
||||
border-left-color: rgba(18, 48, 75, 0.12);
|
||||
}
|
||||
|
||||
.dashboard-fab-menu {
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.88), rgba(245, 251, 255, 0.76)),
|
||||
radial-gradient(circle at top left, rgba(90, 188, 242, 0.16), transparent 48%);
|
||||
border-color: rgba(92, 129, 160, 0.2);
|
||||
}
|
||||
|
||||
.dashboard-fab-menu button,
|
||||
.moment-type-card,
|
||||
.moment-choice-pill span,
|
||||
.options-menu-card,
|
||||
.options-modal .settings-section,
|
||||
.options-modal .band-card,
|
||||
.options-modal .sport-type-card,
|
||||
.options-modal .checkbox-row--panel,
|
||||
.options-modal .push-panel,
|
||||
.options-modal .detail-card--overlay {
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.78), rgba(247, 252, 255, 0.62)),
|
||||
radial-gradient(circle at top left, rgba(90, 188, 242, 0.12), transparent 48%);
|
||||
border-color: rgba(92, 129, 160, 0.2);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.options-menu-card--danger {
|
||||
background: rgba(219, 107, 107, 0.1);
|
||||
border-color: rgba(219, 107, 107, 0.22);
|
||||
}
|
||||
|
||||
.health-import-progress__bar {
|
||||
background: rgba(18, 48, 75, 0.08);
|
||||
border-color: rgba(92, 129, 160, 0.16);
|
||||
}
|
||||
|
||||
.week-insight-card {
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.78), rgba(248, 253, 255, 0.62)),
|
||||
radial-gradient(circle at top left, rgba(90, 188, 242, 0.14), transparent 42%);
|
||||
border-color: rgba(92, 129, 160, 0.2);
|
||||
}
|
||||
|
||||
.moment-type-card.is-selected,
|
||||
.moment-choice-pill input:checked + span {
|
||||
background: rgba(90, 188, 242, 0.18);
|
||||
border-color: rgba(20, 148, 222, 0.34);
|
||||
}
|
||||
|
||||
.day-summary-card__title {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.day-chip {
|
||||
background: rgba(255, 255, 255, 0.62);
|
||||
border-color: rgba(92, 129, 160, 0.2);
|
||||
color: rgba(18, 48, 75, 0.9);
|
||||
}
|
||||
|
||||
.day-chip--bonus {
|
||||
background: rgba(99, 217, 180, 0.18);
|
||||
border-color: rgba(69, 201, 141, 0.3);
|
||||
}
|
||||
|
||||
.timeline-card__comment {
|
||||
color: rgba(18, 48, 75, 0.82);
|
||||
}
|
||||
|
||||
.timeline-card__stats span {
|
||||
background: rgba(255, 255, 255, 0.58);
|
||||
border-color: rgba(92, 129, 160, 0.18);
|
||||
color: rgba(18, 48, 75, 0.82);
|
||||
}
|
||||
|
||||
.timeline-route-map {
|
||||
border-color: rgba(92, 129, 160, 0.2);
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.site-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
@@ -1557,6 +1557,100 @@
|
||||
}
|
||||
}
|
||||
|
||||
function initHealthImportStatus() {
|
||||
const panel = document.querySelector("[data-health-import-status]");
|
||||
if (!panel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const progressWrap = panel.querySelector("[data-health-progress-wrap]");
|
||||
const progress = panel.querySelector("[data-health-progress-bar]");
|
||||
const progressText = panel.querySelector("[data-health-progress-text]");
|
||||
const lastImport = panel.querySelector("[data-health-last-import]");
|
||||
const lastMessage = panel.querySelector("[data-health-last-message]");
|
||||
let timer = null;
|
||||
|
||||
const formatDuration = seconds => {
|
||||
const rounded = Math.max(0, Math.round(Number(seconds) || 0));
|
||||
if (rounded < 60) {
|
||||
return `${rounded} s`;
|
||||
}
|
||||
|
||||
return `${Math.ceil(rounded / 60)} min`;
|
||||
};
|
||||
|
||||
const render = status => {
|
||||
const done = Math.max(0, Number(status.progress_done || 0));
|
||||
const total = Math.max(0, Number(status.progress_total || 0));
|
||||
const percent = total > 0 ? Math.min(100, Math.round((done / total) * 100)) : 0;
|
||||
|
||||
if (progress) {
|
||||
progress.value = String(percent);
|
||||
progress.textContent = `${percent}%`;
|
||||
}
|
||||
|
||||
if (progressWrap) {
|
||||
progressWrap.dataset.progressDone = String(done);
|
||||
progressWrap.dataset.progressTotal = String(total);
|
||||
}
|
||||
|
||||
if (lastImport) {
|
||||
lastImport.textContent = status.last_import_at ? new Date(status.last_import_at).toLocaleString("de-DE") : "-";
|
||||
}
|
||||
|
||||
if (lastMessage) {
|
||||
lastMessage.textContent = status.last_message || "-";
|
||||
}
|
||||
|
||||
if (progressText) {
|
||||
if (status.last_status === "running") {
|
||||
let eta = "wird berechnet";
|
||||
const started = Date.parse(status.started_at || "");
|
||||
if (started && done > 0 && total > done) {
|
||||
const elapsed = (Date.now() - started) / 1000;
|
||||
eta = formatDuration((elapsed / done) * (total - done));
|
||||
}
|
||||
progressText.textContent = `Import läuft: ${done} von ${total} verarbeitet. Restzeit ca. ${eta}.`;
|
||||
return;
|
||||
}
|
||||
|
||||
if (status.last_status === "error") {
|
||||
progressText.textContent = `${status.last_message || "Import fehlgeschlagen."} Derselbe Export kann erneut gesendet werden und wird idempotent übernommen.`;
|
||||
return;
|
||||
}
|
||||
|
||||
progressText.textContent = status.last_message || "Noch kein Import gelaufen.";
|
||||
}
|
||||
};
|
||||
|
||||
const initialDone = progressWrap ? Number(progressWrap.dataset.progressDone || 0) : 0;
|
||||
const initialTotal = progressWrap ? Number(progressWrap.dataset.progressTotal || 0) : 0;
|
||||
render({ progress_done: initialDone, progress_total: initialTotal });
|
||||
|
||||
const refresh = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/health/status", { credentials: "same-origin" });
|
||||
if (!response.ok) {
|
||||
return;
|
||||
}
|
||||
const data = await response.json();
|
||||
if (!data || !data.ok || !data.status) {
|
||||
return;
|
||||
}
|
||||
render(data.status);
|
||||
if (data.status.last_status !== "running" && timer !== null) {
|
||||
window.clearInterval(timer);
|
||||
timer = null;
|
||||
}
|
||||
} catch (error) {
|
||||
// Status polling is best-effort; the import itself is server-side.
|
||||
}
|
||||
};
|
||||
|
||||
refresh();
|
||||
timer = window.setInterval(refresh, 3500);
|
||||
}
|
||||
|
||||
function csrfToken() {
|
||||
return document.querySelector('meta[name="csrf-token"]')?.getAttribute("content") || "";
|
||||
}
|
||||
@@ -1888,6 +1982,7 @@
|
||||
initDashboardCharts();
|
||||
initDashboardExperience();
|
||||
initOptionsPanels();
|
||||
initHealthImportStatus();
|
||||
initSportTypeManager();
|
||||
initPwaShell();
|
||||
initPullToRefresh();
|
||||
|
||||
Reference in New Issue
Block a user