diff --git a/assets/css/app.css b/assets/css/app.css index cc196f8..ed79229 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -886,15 +886,39 @@ body.page-dashboard .content { } .sleep-phase-bar { + position: relative; display: flex; min-height: 2.35rem; - margin-top: 0.75rem; + margin-top: 1.55rem; overflow: hidden; border-radius: 999px; border: 1px solid rgba(255, 255, 255, 0.14); background: rgba(255, 255, 255, 0.08); } +.sleep-phase-bar__target { + position: absolute; + left: var(--sleep-optimal-left, 100%); + top: -1.28rem; + bottom: 0; + width: 0; + border-left: 2px solid rgba(255, 255, 255, 0.92); + filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.45)); + pointer-events: none; +} + +.sleep-phase-bar__target span { + position: absolute; + top: 0; + left: 0.28rem; + padding: 0.08rem 0.34rem; + border-radius: 999px; + background: rgba(6, 16, 28, 0.72); + color: rgba(255, 255, 255, 0.95); + font-size: 0.68rem; + white-space: nowrap; +} + .sleep-phase-bar__segment { display: inline-flex; align-items: center; diff --git a/assets/js/app.js b/assets/js/app.js index ea3cf29..82760f6 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -1658,6 +1658,10 @@ return; } + if (lightbox.parentElement !== document.body) { + document.body.appendChild(lightbox); + } + const close = () => { lightbox.setAttribute("hidden", "hidden"); content.replaceChildren(); diff --git a/src/App.php b/src/App.php index ea720d2..9140505 100644 --- a/src/App.php +++ b/src/App.php @@ -366,7 +366,7 @@ final class App $metrics = $this->healthMetricsFromPayload($payload); $workouts = $this->healthWorkoutsFromPayload($payload); - $metricImport = $this->healthEventsFromMetrics($metrics); + $metricImport = $this->healthEventsFromMetrics($metrics, (float) ($settings['sleep']['optimal_hours'] ?? 7.0)); $workoutImport = $this->healthEventsFromWorkouts($workouts, $settings); $entries = $this->entries->all($username); @@ -717,7 +717,7 @@ final class App ]; } - private function healthEventsFromMetrics(array $metrics): array + private function healthEventsFromMetrics(array $metrics, float $optimalSleepHours = 7.0): array { $steps = []; $sleepBuckets = []; @@ -786,7 +786,7 @@ final class App $sleep = []; foreach ($sleepBuckets as $date => $bucket) { - $signals = $this->healthSleepSignals($bucket); + $signals = $this->healthSleepSignals($bucket, $optimalSleepHours); $sleep[$date][] = [ 'id' => 'health-sleep-' . $date, @@ -815,17 +815,19 @@ final class App ]; } - private function healthSleepSignals(array $bucket): array + private function healthSleepSignals(array $bucket, float $optimalSleepHours = 7.0): array { $hours = (float) ($bucket['hours'] ?? 0); $deep = (float) ($bucket['deep'] ?? 0); $rem = (float) ($bucket['rem'] ?? 0); $core = (float) ($bucket['core'] ?? 0); + $optimalSleepHours = max(1.0, min(16.0, $optimalSleepHours)); $quality = 0; + $deviation = abs($hours - $optimalSleepHours); - if ($hours >= 7 && $hours <= 9) { + if ($deviation <= 0.75) { $quality++; - } elseif ($hours < 5 || $hours > 10) { + } elseif ($deviation >= 2.0) { $quality--; } @@ -843,8 +845,8 @@ final class App return [ 'mood' => max(-2, min(2, $quality - 1)), - 'energy' => max(-2, min(2, (int) round(($deep + $rem) - 2))), - 'stress' => max(-2, min(2, $core >= 3.5 && $hours >= 6 ? -1 : ($hours < 5 ? 1 : 0))), + 'energy' => max(-2, min(2, (int) round(($deep + $rem) - 2 - max(0, $deviation - 1)))), + 'stress' => max(-2, min(2, $core >= 3.5 && $deviation <= 1.0 ? -1 : ($deviation >= 2.0 ? 1 : 0))), ]; } @@ -1202,7 +1204,7 @@ final class App 'dayEntry' => $selectedEntry, 'dashboardEventTypes' => day_event_type_options(), 'dashboardSignals' => signal_scale_options(), - 'dashboardTimeline' => $this->buildDashboardTimeline($selectedEntry), + 'dashboardTimeline' => $this->buildDashboardTimeline($selectedEntry, $settings), 'dashboardCompareDays' => $this->buildDashboardCompareDays($dashboardDate, $entryMap, $settings), 'dashboardWeek' => $this->buildDashboardWeekView($dashboardDate, $entryMap), 'dashboardMonth' => $this->buildDashboardMonthView($dashboardDate, $entryMap), @@ -1392,7 +1394,7 @@ final class App ]); } - private function buildDashboardTimeline(array $entry): array + private function buildDashboardTimeline(array $entry, array $settings): array { $timeline = []; @@ -1401,10 +1403,26 @@ final class App continue; } + $type = (string) ($event['type'] ?? 'event'); + $mood = normalize_signal_value($event['mood'] ?? 0); + $energy = normalize_signal_value($event['energy'] ?? 0); + $stress = normalize_signal_value($event['stress'] ?? 0); + if ($type === 'sleep' && (string) ($event['source'] ?? '') === 'health_auto_export') { + $signals = $this->healthSleepSignals([ + 'hours' => (float) ($event['value'] ?? 0), + 'deep' => (float) ($event['sleep_deep'] ?? 0), + 'rem' => (float) ($event['sleep_rem'] ?? 0), + 'core' => (float) ($event['sleep_core'] ?? 0), + ], (float) ($settings['sleep']['optimal_hours'] ?? 7.0)); + $mood = $signals['mood']; + $energy = $signals['energy']; + $stress = $signals['stress']; + } + $timeline[] = [ 'kind' => 'event', 'id' => (string) ($event['id'] ?? ''), - 'type' => (string) ($event['type'] ?? 'event'), + 'type' => $type, 'time' => (string) ($event['time'] ?? ''), 'comment' => (string) ($event['comment'] ?? ''), 'value' => (float) ($event['value'] ?? 0), @@ -1413,9 +1431,9 @@ final class App 'consumed' => !empty($event['consumed']), 'image' => (string) ($event['image'] ?? ''), 'image_url' => is_string($event['image_url'] ?? null) ? (string) $event['image_url'] : null, - 'mood' => normalize_signal_value($event['mood'] ?? 0), - 'energy' => normalize_signal_value($event['energy'] ?? 0), - 'stress' => normalize_signal_value($event['stress'] ?? 0), + 'mood' => $mood, + 'energy' => $energy, + 'stress' => $stress, 'source' => (string) ($event['source'] ?? ''), 'import_id' => (string) ($event['import_id'] ?? ''), 'duration_label' => (string) ($event['duration_label'] ?? ''), @@ -2894,6 +2912,9 @@ final class App $settings['walk'] = [ 'mode' => ($input['walk']['mode'] ?? ($settings['walk']['mode'] ?? 'time')) === 'steps' ? 'steps' : 'time', ]; + $settings['sleep'] = [ + 'optimal_hours' => max(1.0, min(16.0, round((float) ($input['sleep']['optimal_hours'] ?? ($settings['sleep']['optimal_hours'] ?? 7.0)), 1))), + ]; $settings['scoring']['mood_multiplier'] = max(0, min(10, (int) ($input['scoring']['mood_multiplier'] ?? 3))); $settings['scoring']['energy_multiplier'] = max(0, min(10, (int) ($input['scoring']['energy_multiplier'] ?? 2))); @@ -2976,6 +2997,11 @@ final class App Defaults::settings()['walk'], is_array($settings['walk'] ?? null) ? $settings['walk'] : [] ); + $settings['sleep'] = array_replace( + Defaults::settings()['sleep'], + is_array($settings['sleep'] ?? null) ? $settings['sleep'] : [] + ); + $settings['sleep']['optimal_hours'] = max(1.0, min(16.0, round((float) ($settings['sleep']['optimal_hours'] ?? 7.0), 1))); $settings['tracking'] = array_replace( Defaults::settings()['tracking'], is_array($settings['tracking'] ?? null) ? $settings['tracking'] : [] diff --git a/src/Support/Defaults.php b/src/Support/Defaults.php index da8833e..fac2169 100644 --- a/src/Support/Defaults.php +++ b/src/Support/Defaults.php @@ -19,6 +19,9 @@ final class Defaults 'walk' => [ 'mode' => 'time', ], + 'sleep' => [ + 'optimal_hours' => 7.0, + ], 'tracking' => [ 'pain_enabled' => false, ], diff --git a/templates/layout.php b/templates/layout.php index 5d3f5c6..b4a7e89 100644 --- a/templates/layout.php +++ b/templates/layout.php @@ -19,7 +19,7 @@ $jsVersion = is_file(base_path('assets/js/app.js')) ? (string) filemtime(base_pa - + diff --git a/templates/pages/dashboard.php b/templates/pages/dashboard.php index b5f3cb5..2ddcb81 100644 --- a/templates/pages/dashboard.php +++ b/templates/pages/dashboard.php @@ -10,6 +10,7 @@ $summaryAlcohol = !empty($dayEntry['summary']['alcohol'] ?? $dayEntry['summary_a $dayHealth = is_array($dayEntry['health'] ?? null) ? $dayEntry['health'] : []; $daySteps = (int) ($dayHealth['steps'] ?? 0); $dayStepBonus = (float) ($dayEntry['evaluation']['components']['step_bonus'] ?? 0); +$optimalSleepHours = max(1.0, min(16.0, (float) ($settings['sleep']['optimal_hours'] ?? 7.0))); ?>
@@ -113,6 +114,8 @@ $dayStepBonus = (float) ($dayEntry['evaluation']['components']['step_bonus'] ?? } } $sleepPhaseTotal = max(0.0, array_sum($sleepPhases)); + $sleepBarTotal = $eventType === 'sleep' ? max((float) ($item['value'] ?? 0), $sleepPhaseTotal, $optimalSleepHours) : 0.0; + $sleepOptimalPercent = $sleepBarTotal > 0 ? max(0, min(100, ($optimalSleepHours / $sleepBarTotal) * 100)) : 0; ?> (string) ($item['id'] ?? ''), @@ -182,7 +185,7 @@ $dayStepBonus = (float) ($dayEntry['evaluation']['components']['step_bonus'] ?? 0): ?> -
+
['Tief', 'deep'], 'rem' => ['REM', 'rem'], 'core' => ['Kern', 'core']] as $phase => [$label, $class]): ?> @@ -190,6 +193,7 @@ $dayStepBonus = (float) ($dayEntry['evaluation']['components']['step_bonus'] ?? h + h
@@ -626,6 +630,7 @@ $dayStepBonus = (float) ($dayEntry['evaluation']['components']['step_bonus'] ??
Sportarten anpassenEigene Sportarten und Bonuspunkte Spaziergang anpassenZeit oder Schritte auswerten + Schlaf anpassenOptimale Schlafmenge Health ImportApple Health automatisch übernehmen Erinnerungen setzenPush und tägliche Erinnerung Bewertungsskala ändernLabels und Schutzregeln diff --git a/templates/pages/options.php b/templates/pages/options.php index 21bbd04..fcb8b18 100644 --- a/templates/pages/options.php +++ b/templates/pages/options.php @@ -18,6 +18,7 @@
+ @@ -113,6 +114,17 @@
+ +