From 23717ba6da0fadc0a02e122aea79bd816e04aa99 Mon Sep 17 00:00:00 2001 From: fy59 Date: Thu, 24 Sep 2026 07:54:29 +0200 Subject: [PATCH] fix(sleep): record wake taps as timed intervals --- CHANGELOG.md | 5 ++ .../trainlog/data/TrainlogRepository.kt | 60 ++++++++++++++++--- .../app/src/main/res/values-en/strings.xml | 2 +- android/app/src/main/res/values/strings.xml | 2 +- .../trainlog/data/SleepQuickCaptureTest.kt | 45 +++++++++++++- docs/android.md | 7 +++ docs/current_state.md | 3 +- docs/design/cardio_v1.md | 23 +++---- .../sleep_awake_selection_pdf_v1_evidence.md | 47 ++++++++++++++- docs/tests.md | 14 ++++- web/src/report/sleepDiaryPdf.test.ts | 2 +- web/src/report/sleepDiaryPdf.ts | 18 ++++-- web/src/theme/app.css | 4 +- 13 files changed, 194 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a3565f9..2bc75b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,6 +67,11 @@ also refined. sleep intervals or, only when none exist, uses the displayed Couché +45 min to Levé -10 min estimate minus structured awakenings; the estimate is never written to SQLite. +- Changed future Android one-tap Réveil capture from punctual `night_get_up` + facts to structured `long_awake` windows under the user-approved 30-minute + default. A repeated tap inside the window extends its end to 30 minutes after + the latest tap, while Levé clamps it to the factual final-get-up timestamp. + The policy is revisioned, undoable, and independent of BPM/RR inference. ## 0.1.5 — 2026-09-23 diff --git a/android/app/src/main/java/com/labfytools/trainlog/data/TrainlogRepository.kt b/android/app/src/main/java/com/labfytools/trainlog/data/TrainlogRepository.kt index bb7ae2d..51e89d8 100644 --- a/android/app/src/main/java/com/labfytools/trainlog/data/TrainlogRepository.kt +++ b/android/app/src/main/java/com/labfytools/trainlog/data/TrainlogRepository.kt @@ -80,6 +80,8 @@ import java.security.MessageDigest import java.time.Duration import java.time.LocalDate import java.time.OffsetDateTime +import java.time.format.DateTimeFormatterBuilder +import java.time.temporal.ChronoField import java.time.temporal.WeekFields import java.util.Locale import java.util.UUID @@ -88,6 +90,11 @@ private val PROFILE_REVISION_NAMESPACE = UUID.fromString("4f4c8ea6-18f6-5e48-9d6 private val PROFILE_REVISION_PATTERN = Regex("^pr2_[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$") private val PROTOTYPE_PROFILE_REVISION_PATTERN = Regex("^pr1\\|(sets|continuous)\\|(reps|duration)\\|([0-9]+)$") private const val PROFILE_HISTORY_MAX = 32 +private val SLEEP_QUICK_TIMESTAMP = DateTimeFormatterBuilder() + .appendPattern("uuuu-MM-dd'T'HH:mm:ss") + .appendFraction(ChronoField.NANO_OF_SECOND, 0, 9, true) + .appendOffsetId() + .toFormatter() /** Fixed-width UUIDv5 identity over the direct causal parent and resulting profile. */ internal fun exerciseProfileRevision(parent: String, recording: String, tracking: String, fields: Int): String { @@ -858,15 +865,37 @@ class TrainlogRepository( fun quickSleepWake(at: String = OffsetDateTime.now().toString()): SleepQuickActionResult = mutateActiveSleep(at) { current -> - current.quickDraft( - at, - events = current.events + SleepDiaryEvent( + val wake = OffsetDateTime.parse(at) + val inferredEnd = wake.plusMinutes(30).format(SLEEP_QUICK_TIMESTAMP) + val openIndex = current.events.indexOfLast { event -> + event.type == SleepEventType.LONG_AWAKE && + event.endAt != null && + !wake.isBefore(OffsetDateTime.parse(event.startAt)) && + !wake.isAfter(OffsetDateTime.parse(event.endAt)) + } + /* + * WHY: one-tap capture cannot require a second interaction from a + * partly awake user, but a point event cannot drive an awakening + * interval in agenda/PDF projections. + * CONTRACT: Réveil records a 30-minute LONG_AWAKE interval. A new + * tap inside that window resets its inferred end to 30 minutes + * after the latest tap; Levé may subsequently clamp it. + * INVARIANT: this is explicit product policy, never an inference + * from BPM/RR, and every persisted LONG_AWAKE has start < end. + */ + val events = if (openIndex >= 0) { + current.events.mapIndexed { index, event -> + if (index == openIndex) event.copy(endAt = inferredEnd) else event + } + } else { + current.events + SleepDiaryEvent( sleepId("sle"), - SleepEventType.NIGHT_GET_UP, + SleepEventType.LONG_AWAKE, at, - null, - ), - ) + inferredEnd, + ) + } + current.quickDraft(at, events = events) } fun quickSleepMedication( @@ -933,9 +962,24 @@ class TrainlogRepository( val activeBefore = activeSleepDiaryEntry() ?: return SleepQuickActionResult.NoActiveNight val result = mutateActiveSleep(at) { current -> + val finalGetUp = OffsetDateTime.parse(at) + val boundedEvents = current.events.map { event -> + if ( + event.type == SleepEventType.LONG_AWAKE && + event.endAt != null && + OffsetDateTime.parse(event.startAt).isBefore(finalGetUp) && + OffsetDateTime.parse(event.endAt).isAfter(finalGetUp) + ) { + // CONTRACT: final get-up is a factual upper bound; an + // inferred 30-minute awakening never extends past it. + event.copy(endAt = at) + } else { + event + } + } current.quickDraft( at, - events = current.events + SleepDiaryEvent( + events = boundedEvents + SleepDiaryEvent( sleepId("sle"), SleepEventType.FINAL_GET_UP, at, diff --git a/android/app/src/main/res/values-en/strings.xml b/android/app/src/main/res/values-en/strings.xml index 8dafeed..a17c9f5 100644 --- a/android/app/src/main/res/values-en/strings.xml +++ b/android/app/src/main/res/values-en/strings.xml @@ -544,7 +544,7 @@ 👁 WAKE ☀ GET UP Bed time recorded. - Wake recorded. + 30-minute awakening recorded. Get-up recorded. %1$s recorded. UNDO diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index edebd61..63cafee 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -554,7 +554,7 @@ 👁 RÉVEIL ☀ LEVÉ Couché enregistré. - Réveil enregistré. + Réveil de 30 minutes enregistré. Levé enregistré. %1$s enregistré. ANNULER diff --git a/android/app/src/test/java/com/labfytools/trainlog/data/SleepQuickCaptureTest.kt b/android/app/src/test/java/com/labfytools/trainlog/data/SleepQuickCaptureTest.kt index 58c36de..3b650bf 100644 --- a/android/app/src/test/java/com/labfytools/trainlog/data/SleepQuickCaptureTest.kt +++ b/android/app/src/test/java/com/labfytools/trainlog/data/SleepQuickCaptureTest.kt @@ -142,8 +142,18 @@ class SleepQuickCaptureTest { assertEquals(bed.receipt.entryId, beforeGetUp.entryId) assertEquals( 2, - beforeGetUp.events.count { it.type == SleepEventType.NIGHT_GET_UP }, + beforeGetUp.events.count { it.type == SleepEventType.LONG_AWAKE }, ) + assertEquals( + listOf( + "2026-09-24T02:17:00+02:00" to "2026-09-24T02:47:00+02:00", + "2026-09-24T04:42:00+02:00" to "2026-09-24T05:12:00+02:00", + ), + beforeGetUp.events + .filter { it.type == SleepEventType.LONG_AWAKE } + .map { it.startAt to it.endAt }, + ) + assertTrue(beforeGetUp.events.none { it.type == SleepEventType.NIGHT_GET_UP }) assertEquals(2, beforeGetUp.intakes.size) assertEquals(2, beforeGetUp.intakes.first().quantity) assertEquals(5.0, checkNotNull(beforeGetUp.intakes.first().doseValue), 0.0) @@ -217,7 +227,7 @@ class SleepQuickCaptureTest { assertEquals( listOf("2026-09-24T02:00:00+02:00"), night.events - .filter { it.type == SleepEventType.NIGHT_GET_UP } + .filter { it.type == SleepEventType.LONG_AWAKE } .map { it.startAt }, ) } finally { @@ -226,6 +236,37 @@ class SleepQuickCaptureTest { } } + @Test + fun repeatedWakeExtendsOneWindowAndFinalGetUpClampsIt() { + val name = "sleep-wake-window-" + UUID.randomUUID() + val repository = TrainlogRepository(context, name) + try { + repository.quickSleepBedTime("2026-09-23T22:05:00+02:00") + repository.quickSleepWake("2026-09-24T04:11:00+02:00") + repository.quickSleepWake("2026-09-24T04:14:00+02:00") + + val extended = repository.listSleepDiary().single() + assertEquals( + listOf("2026-09-24T04:11:00+02:00" to "2026-09-24T04:44:00+02:00"), + extended.events + .filter { it.type == SleepEventType.LONG_AWAKE } + .map { it.startAt to it.endAt }, + ) + + repository.quickSleepFinalGetUp("2026-09-24T04:14:30+02:00") + val completed = repository.listSleepDiary().single() + assertEquals( + listOf("2026-09-24T04:11:00+02:00" to "2026-09-24T04:14:30+02:00"), + completed.events + .filter { it.type == SleepEventType.LONG_AWAKE } + .map { it.startAt to it.endAt }, + ) + } finally { + repository.close() + context.deleteDatabase(name) + } + } + @Test fun bedtimeAndFinalGetUpWithoutMeasurementFabricateNoCapture() { val name = "sleep-no-sensor-" + UUID.randomUUID() diff --git a/docs/android.md b/docs/android.md index b6dd74e..f74e988 100644 --- a/docs/android.md +++ b/docs/android.md @@ -42,6 +42,13 @@ stale intake-free ancestor and a later current revision. This is the executable contract for lifecycle and synchronization retention; it does not infer facts that are absent from a device database. +The one-tap Réveil action records `long_awake` rather than a point +`night_get_up`: its factual start is the tap timestamp and its policy end is 30 +minutes later. Another tap inside that window extends the same interval to 30 +minutes after the latest tap; Levé clamps an overlapping interval to the exact +final-get-up timestamp. This explicit default requires no Rendormi interaction, +remains revisioned/undoable, and is not derived from heart-rate measurements. + Sleep-owned HR capture remains sample-driven. A pending medication alone opens no capture. After Couché, the first fresh real measurement creates or resumes one `context_kind=sleep` capture for that entry, with `started_at` equal to the diff --git a/docs/current_state.md b/docs/current_state.md index fdf7f14..0ee513d 100644 --- a/docs/current_state.md +++ b/docs/current_state.md @@ -161,7 +161,7 @@ parallel implementation of its rules. | Cardio BLE diagnostics | `TRAINLOG_CARDIO_BLE_DIAGNOSTICS_V1=PASS`: real CYCPLUS H2 validated on Android; `0x180D` + `0x2A37`, Battery Service, approximately 1 Hz notifications and one raw RR interval per observed notification; contact and energy fields absent in the observed payloads | | Cardio persistent acquisition | `TRAINLOG_CARDIO_PERSISTENT_ACQUISITION_V1=PASS`: selected sensor persists, dedicated connected-device foreground service reconnects, stale BPM is cleared, and the global Android `♥ BPM` indicator is grey/orange/green by connection state; real CYCPLUS restart and Bluetooth-off/on recovery validated on private vc40 | | Training timeline | `TRAINLOG_CARDIO_TRAINING_TIMELINE_V1=PASS`: real session bounds reuse canonical lifecycle timestamps; exercise start/end are explicit user actions; live BPM samples snapshot the active stable `entry_id`; session finalization refuses an unclosed exercise; timeline sync is Android→desktop idempotent | -| Sleep Android one-tap | `TRAINLOG_SLEEP_ANDROID_ONE_TAP_V1=PASS`: existing one-tap Couché / medication / Réveil / Levé, guarded 10 s undo, correction route, and sleep-entry-owned cardio capture remain intact. The validated V2 development delta additionally creates/reuses one 18:00-to-18:00 pending entry for pre-bed medication, reuses its stable `entry_id` at Couché, and exposes a compact quantity control. | +| Sleep Android one-tap | `TRAINLOG_SLEEP_ANDROID_ONE_TAP_V1=PASS`: one-tap Couché / medication / Réveil / Levé, guarded 10 s undo, correction route, and sleep-entry-owned cardio capture remain intact. Réveil now creates a structured 30-minute `long_awake` window, repeated taps extend it, and Levé clamps it. The validated V2 development delta additionally creates/reuses one 18:00-to-18:00 pending entry for pre-bed medication, reuses its stable `entry_id` at Couché, and exposes a compact quantity control. | | Cardio session type | `TRAINLOG_CARDIO_SESSION_TYPE_V1=PASS`: Android v29 + desktop v32 use canonical `session_kind=cardio`; frozen mobile/history and draft formats keep legacy training/max_test; dedicated `trainlog-cardio-sessions` V1 syncs completed cardio sessions Android→desktop idempotently | | Cardio calibration | `TRAINLOG_CARDIO_CALIBRATION_V1=PASS`: reserved system Calibration cardio exercise, real measured observed peak, ≥3 min recovery with bounded +1/+2/+3 min factual samples, immutable profile and Android→desktop calibration companion | | Cardio BPM guidance | `TRAINLOG_CARDIO_BPM_GUIDANCE_V1=PASS`: absolute/calibration-derived immutable BPM targets, 2 BPM hysteresis, 3 s confirmation, stale-signal suspension, durable phase/condition history, Android live guidance UI and strict Android→desktop `trainlog-cardio-guidance` V1 sync | @@ -171,6 +171,7 @@ parallel implementation of its rules. | Tomorrow session readiness | `TRAINLOG_TOMORROW_SESSION_CARDIO_READINESS_V1=PASS`: the private vc46 is clean on schema v31 with fresh CYCPLUS BPM and the next Program session is unambiguous; one combined regression proves two ordered occurrences loaded from the synchronized Program, Program provenance, factual timeline/HR ownership, finalization, full generation, desktop import, replay and correlated ACK | | Current operational cursor | 0.1.6 Sleep graph overlays, shared agenda estimate and denser responsive agenda axis validated and privately served from Web code commit `a60e1aa`; immutable stable v0.1.5 remains unchanged. | | Sleep awake/selection/PDF refinement | `TRAINLOG_SLEEP_AWAKE_SELECTION_PDF_V1=PASS`: one temporal projection drives agenda, selected HR detail and selected-only PDF; estimated sleep duration is consistent and remains non-persistent; Web 208/208 plus real read-only Firefox/PDF smoke passed. | +| Sleep Réveil interval policy | Android Réveil creates a revisioned 30-minute `long_awake` window; repeated taps extend it and Levé clamps it. The real 2026-09-23 night is causally repaired on Android/desktop and renders two orange intervals in agenda, HR and PDF; Android 307 passed plus five optional skips, and Web 208/208 passed. | | Complete synchronization gap contract | Frozen dependency contract; operational USB/Drive slices required by v0.1.2 are delivered | | Isolated synchronization test environment | `TRAINLOG_SYNC_TEST_ENV_V1=PASS/FROZEN` | | Synchronization characterization | `TRAINLOG_SYNC_CHARACTERIZATION_V1=PASS/FROZEN` | diff --git a/docs/design/cardio_v1.md b/docs/design/cardio_v1.md index 7c904cc..8cdd2c9 100644 --- a/docs/design/cardio_v1.md +++ b/docs/design/cardio_v1.md @@ -320,14 +320,15 @@ name/dose/unit snapshot. Full editing remains available later. ### Réveil -There is no `Rendormi` button. +There is no `Rendormi` button. One tap records a structured `long_awake` +interval beginning at the factual tap time and ending 30 minutes later under +the explicit capture policy. Nothing else is required before the user tries to +return to sleep. A second tap within that window resets its end to 30 minutes +after the latest tap. Levé truncates an overlapping interval to the factual +final-get-up timestamp. -One tap records one punctual night wake event. Nothing else is required before -the user tries to return to sleep. - -Trainlog does not infer a certain return-to-sleep time from BPM or RR data. -An approximate interval may be completed later only if the user chooses to -provide it. +The 30-minute end is user-approved product policy, not an inference from BPM or +RR data. Later correction remains available when the actual duration is known. ### Levé @@ -347,11 +348,11 @@ Undo is scoped to that exact action and must not roll back unrelated newer state. `Corriger la nuit` provides later editing of timestamps, medication -intakes, accidental wake points, forgotten events, and existing subjective +intakes, awakening intervals, forgotten events, and existing subjective Sleep Diary fields. -The night-time rule is: one tap, one timestamp, then no further required -interaction. +The night-time rule is: one tap, one 30-minute awakening window, then no further +required interaction. ## Sleep + cardio factual timeline @@ -361,7 +362,7 @@ After synchronization, Analyse may superpose factual events and measurements: - Couché; - medication intakes; -- wake points; +- structured 30-minute awakening windows; - Levé; - BPM; - RR when supplied; diff --git a/docs/reviews/sleep_awake_selection_pdf_v1_evidence.md b/docs/reviews/sleep_awake_selection_pdf_v1_evidence.md index ff0e2f4..3858e35 100644 --- a/docs/reviews/sleep_awake_selection_pdf_v1_evidence.md +++ b/docs/reviews/sleep_awake_selection_pdf_v1_evidence.md @@ -47,5 +47,48 @@ The rebuilt user Web service was exercised through real headless Firefox at - Raster inspection confirms the long-awake band remains visibly distinct inside the sleep timeline and point/medication markers retain their positions. -The change affects no database schema, synchronization artifact, frozen format, -or Android behavior. +The original Web change affected no database schema, synchronization artifact, +frozen format, or Android behavior; the follow-up below deliberately corrects +the Android capture semantics without changing those compatibility boundaries. + +## Android capture correction and real-night repair + +Follow-up visual review established that Android one-tap Réveil had produced +three punctual `night_get_up` facts for 2026-09-23 → 2026-09-24, so there was +no structured interval for the shared projection to color. The user selected +an explicit 30-minute no-further-action policy. Future taps now create a +`long_awake` interval from the factual tap through +30 minutes; another tap +inside the window extends that same interval, while Levé clamps it to the +factual final-get-up timestamp. This is product policy, not BPM/RR inference. + +Before repair, separate desktop and Android backups were retained under +`~/.local/share/trainlog/backups/sleep-wake-window-20260924T0733/`: + +- desktop SHA-256: `ac2246dc699a5e11e9d8b52c4cad0f096dfc08191f54b6a86c44ce72f623c39c`; +- Android SHA-256: `a1a9017875757f27264556ea62a9d27a98f2efad17ca0234fb10f4095170a98a`. + +The real night was advanced causally, retaining its stable `entry_id`, to: + +- `01:35:43.554104 → 02:05:43.554104`; +- `04:11:59.245980 → 04:14:28.358915` (bounded by Levé). + +Android and desktop current revision is +`slr_d6cac4c6-dab7-4327-bc06-5cc95f7a2dfa`. Its repaired linear ancestry +retains desktop revision `slr_2bbf573d-4262-49b0-896e-1bcabf3b002a`, including +the existing `TB` sleep and wake qualities. +Android database round-trip hashes matched, integrity/foreign-key checks passed, +and the non-destructive app update preserved first-install identity while +advancing the private build to versionCode 49. + +The first post-repair full-generation attempts correctly rejected an already +published Android archive whose embedded ancestry still named the pre-desktop +mobile parent. Both live databases are repaired and aligned, but that stale +transport archive remains rejected until the normal archive-capacity recovery +retires it; no successful synchronization receipt is claimed by this evidence. + +Post-correction validation passed 312 Android JVM tests (five optional skips), +`assembleDebug`, 208 Web tests, typecheck, and build. Real Firefox then observed +two orange agenda bands, two orange HR bands, `Réveils 2 · 32 min 29 s`, and two +orange PDF intervals. The selected-only PDF reports `Sommeil déclaré : 4 h 44 +min 16 s` after subtracting the repaired awakening intervals from the visual +estimate. diff --git a/docs/tests.md b/docs/tests.md index ccd7d0f..d7c8101 100644 --- a/docs/tests.md +++ b/docs/tests.md @@ -1823,21 +1823,29 @@ also succeeds. The five skips remain optional historical/external fixture gates. `TRAINLOG_SLEEP_ANDROID_ONE_TAP_V1=SOFTWARE_PASS / DEVICE_VISUAL_PENDING` reuses the existing Sleep Diary V1 revision model and schema. The quick-capture API creates/advances normal immutable Sleep Diary revisions for Couché, -medication intake, point Réveil and Levé actions. Undo is accepted only when +medication intake, 30-minute structured Réveil and Levé actions. Undo is accepted only when the action's exact applied revision is still current; older receipts conflict, and successful undo appends a compensating revision instead of rewriting the history. Targeted coverage executes Couché, a configured medication with dose snapshot, -two independent wake points, live sleep-owned BPM/RR capture, Levé and final +two independent awakening intervals, live sleep-owned BPM/RR capture, Levé and final heart-rate export against the same stable sleep `entry_id`. A separate test -proves stale undo rejection and exact restoration of the latest action. +proves stale undo rejection and exact restoration of the latest action. A +dedicated regression proves that a second tap inside 30 minutes extends one +interval and that final get-up clamps it without leaving overlapping windows. The complete Android JVM inventory before the final localization-only cleanup reported **274 tests: 269 passed, five skipped, zero failures/errors** and `assembleDebug` succeeded. After the localization cleanup the targeted SleepQuickCapture test and `assembleDebug` passed again. +The 30-minute awakening-policy follow-up passes the current complete inventory: +**312 tests: 307 passed, five skipped, zero failures/errors**, plus +`assembleDebug`. Real-data Firefox coverage additionally asserts two orange +agenda/HR bands and two orange vector-PDF intervals for the repaired +2026-09-23 night. + The private Android installation was advanced non-destructively to Trainlog 0.1.5 versionCode 41 with the same debug signing identity and no uninstall or data clear. The device locked before the final visual Sleep-screen inspection, diff --git a/web/src/report/sleepDiaryPdf.test.ts b/web/src/report/sleepDiaryPdf.test.ts index 9312552..3c75c56 100644 --- a/web/src/report/sleepDiaryPdf.test.ts +++ b/web/src/report/sleepDiaryPdf.test.ts @@ -185,7 +185,7 @@ describe("sleep diary PDF", () => { const source = await buildSleepDiaryPdf(snapshot(1), "fr").text(); const firstSleep = source.indexOf("0.75 g 216.17 509.00 83.33 18.00 re f"); const longAwakening = source.indexOf( - "0.5 g 299.50 509.00 15.63 18.00 re f", + "0.98 0.70 0.53 rg 299.50 509.00 15.63 18.00 re f", ); const secondSleep = source.indexOf("0.75 g 315.13 509.00 67.71 18.00 re f"); expect(firstSleep).toBeGreaterThan(-1); diff --git a/web/src/report/sleepDiaryPdf.ts b/web/src/report/sleepDiaryPdf.ts index 260fe25..081c44f 100644 --- a/web/src/report/sleepDiaryPdf.ts +++ b/web/src/report/sleepDiaryPdf.ts @@ -160,12 +160,18 @@ function row( ); const shade = range.kind === "sleep" ? range.estimated ? 0.88 : 0.75 - : range.kind === "awake" - ? 0.5 - : event?.type === "half_sleep" ? 0.87 : 0.93; - stream.push( - `${shade} g ${x.toFixed(2)} ${(y - 29).toFixed(2)} ${eventWidth.toFixed(2)} 18.00 re f 0 G ${x.toFixed(2)} ${(y - 29).toFixed(2)} ${eventWidth.toFixed(2)} 18.00 re S\n`, - ); + : event?.type === "half_sleep" ? 0.87 : 0.93; + if (range.kind === "awake") { + // CONTRACT: structured awakening intervals use the same orange semantic + // role in Web, HR, preview, and PDF; point markers remain separate. + stream.push( + `0.98 0.70 0.53 rg ${x.toFixed(2)} ${(y - 29).toFixed(2)} ${eventWidth.toFixed(2)} 18.00 re f 0.78 0.42 0.22 RG ${x.toFixed(2)} ${(y - 29).toFixed(2)} ${eventWidth.toFixed(2)} 18.00 re S 0 G 0 g\n`, + ); + } else { + stream.push( + `${shade} g ${x.toFixed(2)} ${(y - 29).toFixed(2)} ${eventWidth.toFixed(2)} 18.00 re f 0 G ${x.toFixed(2)} ${(y - 29).toFixed(2)} ${eventWidth.toFixed(2)} 18.00 re S\n`, + ); + } if (eventWidth > 24) { const label = event ? eventLabels[language][event.type] diff --git a/web/src/theme/app.css b/web/src/theme/app.css index d1a310e..f788767 100644 --- a/web/src/theme/app.css +++ b/web/src/theme/app.css @@ -3133,8 +3133,8 @@ h2 { stroke-dasharray: 5 3; } .heart-rate-awake-band { - fill: rgb(249 226 175 / 14%); - stroke: rgb(249 226 175 / 55%); + fill: rgb(250 179 135 / 18%); + stroke: rgb(250 179 135 / 72%); stroke-width: 0.7; vector-effect: non-scaling-stroke; }