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;
}