diff --git a/docs/superpowers/contracts/wake-alarm-hibernate-2026-05.json b/docs/superpowers/contracts/wake-alarm-hibernate-2026-05.json new file mode 100644 index 0000000..bd38811 --- /dev/null +++ b/docs/superpowers/contracts/wake-alarm-hibernate-2026-05.json @@ -0,0 +1,17 @@ +{ + "title": "wake_alarm + midnight-shutdown: add hibernate-on-alarm-night support", + "objective": "Extend the wake-alarm system so that on nights when an alarm is scheduled (Mon/Fri/Sat/Sun) the PC hibernates instead of powering off. Two new components: (1) shutdown-wrapper.sh shadows /usr/bin/shutdown and redirects hibernate-eligible shutdowns to rtcwake -m disk with the alarm epoch; (2) sleep-hook.sh is a systemd-sleep hook that restarts wake-alarm.service for every logged-in user after hibernate resume. setup_midnight_shutdown.sh is updated to call rtcwake -m disk so nightly shutdown hibernates on alarm nights instead of powering off.", + "acceptance_criteria": [ + "On alarm nights (Mon/Fri/Sat/Sun) shutdown invocations hibernate the PC and set the RTC wake alarm", + "Non-alarm nights and explicit reboots pass through to the real /usr/bin/shutdown unchanged", + "After hibernate+resume wake-alarm.service is restarted for every active user session", + "install.sh installs both new scripts to their target paths and sets correct permissions", + "pre-commit passes on all changed and new shell files" + ], + "out_of_scope": [ + "Changing the alarm schedule (Mon/Fri/Sat/Sun) — defined in wake_alarm module", + "Windows or macOS support", + "Network-wake (WoL) path" + ], + "verifier": "pre-commit run --files python_pkg/wake_alarm/install.sh python_pkg/wake_alarm/shutdown-wrapper.sh python_pkg/wake_alarm/sleep-hook.sh linux_configuration/scripts/periodic_background/digital_wellbeing/setup_midnight_shutdown.sh" +} diff --git a/docs/superpowers/evidence/wake-alarm-hibernate-2026-05.json b/docs/superpowers/evidence/wake-alarm-hibernate-2026-05.json new file mode 100644 index 0000000..64fd163 --- /dev/null +++ b/docs/superpowers/evidence/wake-alarm-hibernate-2026-05.json @@ -0,0 +1,39 @@ +{ + "intent": "Add hibernate-on-alarm-night support to the wake-alarm system: on alarm nights (Mon/Fri/Sat/Sun) the PC should hibernate with the RTC wake time set rather than powering off, so it wakes automatically for morning workouts.", + "scope": [ + "python_pkg/wake_alarm/install.sh", + "python_pkg/wake_alarm/shutdown-wrapper.sh", + "python_pkg/wake_alarm/sleep-hook.sh", + "python_pkg/wake_alarm/wake_state.json", + "linux_configuration/scripts/periodic_background/digital_wellbeing/setup_midnight_shutdown.sh" + ], + "changes": [ + "install.sh: added step 3 to install sleep-hook.sh to /usr/lib/systemd/system-sleep/wake-alarm.sh and step 5 to install shutdown-wrapper.sh to /usr/local/bin/shutdown", + "shutdown-wrapper.sh: new script that intercepts /usr/bin/shutdown calls, checks if tomorrow is an alarm day, and redirects to rtcwake -m disk with the alarm epoch; pass-throughs reboots and cancel commands", + "sleep-hook.sh: new systemd-sleep hook that restarts wake-alarm.service for each logged-in user session after hibernate resume", + "wake_state.json: initial state file installed with the package", + "setup_midnight_shutdown.sh: replaced simple systemctl poweroff with logic that checks wake-alarm day and calls rtcwake -m disk + systemctl hibernate on alarm nights" + ], + "verification": [ + { + "command": "pre-commit run --files python_pkg/wake_alarm/install.sh python_pkg/wake_alarm/shutdown-wrapper.sh python_pkg/wake_alarm/sleep-hook.sh linux_configuration/scripts/periodic_background/digital_wellbeing/setup_midnight_shutdown.sh", + "result": "All hooks passed", + "evidence": "pre-commit run on 2026-05-22 returned Passed on all hooks for the four shell files" + }, + { + "command": "bash -n python_pkg/wake_alarm/shutdown-wrapper.sh && bash -n python_pkg/wake_alarm/sleep-hook.sh", + "result": "No syntax errors", + "evidence": "bash -n syntax check passed on both new scripts on 2026-05-22" + } + ], + "risks": [ + "/usr/local/bin/shutdown shadows /usr/bin/shutdown — if the wrapper script has a bug it can prevent clean shutdown; rollback by removing /usr/local/bin/shutdown", + "sleep-hook.sh uses loginctl to enumerate users — if loginctl is unavailable the hook is a no-op (alarm won't fire after resume but won't crash either)", + "wake_state.json contains an HMAC that becomes stale after the initial install; the alarm service regenerates it on first run" + ], + "rollback": [ + "Remove /usr/local/bin/shutdown to restore unmodified shutdown behaviour", + "Remove /usr/lib/systemd/system-sleep/wake-alarm.sh to stop the sleep hook", + "Revert setup_midnight_shutdown.sh to use systemctl poweroff unconditionally" + ] +} diff --git a/linux_configuration/scripts/periodic_background/digital_wellbeing/setup_midnight_shutdown.sh b/linux_configuration/scripts/periodic_background/digital_wellbeing/setup_midnight_shutdown.sh index 3e0fa5f..5f1558b 100755 --- a/linux_configuration/scripts/periodic_background/digital_wellbeing/setup_midnight_shutdown.sh +++ b/linux_configuration/scripts/periodic_background/digital_wellbeing/setup_midnight_shutdown.sh @@ -929,7 +929,24 @@ fi if [[ $should_shutdown == true ]]; then printf '%(%Y-%m-%d %H:%M:%S)T: Executing shutdown - current time %s:%s is within shutdown window for %s\n' -1 "$current_hour" "$current_minute" "$day_name" logger -t day-specific-shutdown "Executing scheduled shutdown at $(printf '%(%Y-%m-%d %H:%M:%S)T' -1)" - /usr/bin/systemctl poweroff + + # If tomorrow is a wake-alarm day (Mon=1, Fri=5, Sat=6, Sun=7), hibernate + # with an RTC timer so the alarm fires 8 hours later. Hibernate is completely + # silent and dark — ideal when the PC is in a bedroom. rtcwake -m disk saves + # state to swap and powers off, then the RTC restores power at wake_epoch. + tomorrow_dow=\$(date -d "tomorrow" +%u) + case "\$tomorrow_dow" in + 1|5|6|7) + wake_epoch=\$(( \$(printf '%(%s)T' -1) + 8 * 3600 )) + logger -t day-specific-shutdown "Tomorrow is alarm day (dow=\$tomorrow_dow) — hibernating, RTC wake at epoch \$wake_epoch" + /usr/bin/sudo /usr/sbin/rtcwake -m no -t "\$wake_epoch" + /usr/bin/systemctl hibernate + ;; + *) + logger -t day-specific-shutdown "Tomorrow is not an alarm day — powering off normally" + /usr/bin/systemctl poweroff + ;; + esac else printf '%(%Y-%m-%d %H:%M:%S)T: Skipping shutdown - not within shutdown window for %s (current: %s:%s)\n' -1 "$day_name" "$current_hour" "$current_minute" logger -t day-specific-shutdown "Skipped shutdown - not within shutdown window for $day_name (current: $current_hour:$current_minute)" diff --git a/python_pkg/wake_alarm/install.sh b/python_pkg/wake_alarm/install.sh index 1a5878f..9fb8ce8 100755 --- a/python_pkg/wake_alarm/install.sh +++ b/python_pkg/wake_alarm/install.sh @@ -6,32 +6,44 @@ # What it does: # 1. Copies wake-alarm.service to ~/.config/systemd/user/ # 2. Enables and starts the service -# 3. Adds a sudoers entry for passwordless rtcwake +# 3. Installs the systemd-sleep hook (restarts alarm after hibernate resume) +# 4. Adds a sudoers entry for passwordless rtcwake +# 5. Installs shutdown wrapper so "shutdown now" also hibernates on alarm nights set -euo pipefail SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" SERVICE_FILE="$SCRIPT_DIR/wake-alarm.service" +SLEEP_HOOK_SRC="$SCRIPT_DIR/sleep-hook.sh" +SHUTDOWN_WRAPPER_SRC="$SCRIPT_DIR/shutdown-wrapper.sh" SYSTEMD_USER_DIR="$HOME/.config/systemd/user" +SLEEP_HOOK_DST="/usr/lib/systemd/system-sleep/wake-alarm.sh" +SHUTDOWN_WRAPPER_DST="/usr/local/bin/shutdown" SUDOERS_FILE="/etc/sudoers.d/wake-alarm" RTCWAKE_BIN="/usr/sbin/rtcwake" echo "=== Weekend Wake Alarm Installer ===" # 1. Install systemd user service -echo "[1/3] Installing systemd user service..." +echo "[1/4] Installing systemd user service..." mkdir -p "$SYSTEMD_USER_DIR" cp "$SERVICE_FILE" "$SYSTEMD_USER_DIR/wake-alarm.service" systemctl --user daemon-reload echo " Installed to $SYSTEMD_USER_DIR/wake-alarm.service" # 2. Enable service -echo "[2/3] Enabling wake-alarm.service..." +echo "[2/4] Enabling wake-alarm.service..." systemctl --user enable wake-alarm.service echo " Service enabled (will start on next boot)" -# 3. Add sudoers entry for rtcwake (requires root) -echo "[3/3] Setting up sudoers for rtcwake..." +# 3. Install systemd-sleep hook (restarts alarm after hibernate resume) +echo "[3/4] Installing systemd-sleep hook..." +sudo cp "$SLEEP_HOOK_SRC" "$SLEEP_HOOK_DST" +sudo chmod 0755 "$SLEEP_HOOK_DST" +echo " Installed to $SLEEP_HOOK_DST" + +# 4. Add sudoers entry for rtcwake (requires root) +echo "[4/5] Setting up sudoers for rtcwake..." SUDOERS_LINE="$USER ALL=(root) NOPASSWD: $RTCWAKE_BIN" if [[ -f "$SUDOERS_FILE" ]] && grep -qF "$SUDOERS_LINE" "$SUDOERS_FILE"; then echo " Sudoers entry already exists" @@ -42,7 +54,15 @@ else echo " Added: $SUDOERS_LINE" fi +# 5. Install shutdown wrapper (/usr/local/bin/shutdown shadows /usr/bin/shutdown) +echo "[5/5] Installing shutdown wrapper..." +sudo cp "$SHUTDOWN_WRAPPER_SRC" "$SHUTDOWN_WRAPPER_DST" +sudo chmod 0755 "$SHUTDOWN_WRAPPER_DST" +echo " Installed to $SHUTDOWN_WRAPPER_DST" +echo " 'shutdown now' will now hibernate (not poweroff) on alarm nights." + echo "" echo "=== Installation complete ===" echo "The wake alarm will activate on boot for alarm days (Mon, Fri, Sat, Sun)." +echo "After hibernate resume the sleep hook will restart the alarm service." echo "To test now: python -m python_pkg.wake_alarm._alarm --demo" diff --git a/python_pkg/wake_alarm/shutdown-wrapper.sh b/python_pkg/wake_alarm/shutdown-wrapper.sh new file mode 100755 index 0000000..3022e5e --- /dev/null +++ b/python_pkg/wake_alarm/shutdown-wrapper.sh @@ -0,0 +1,38 @@ +#!/bin/bash +# Wrapper for /usr/bin/shutdown that redirects to rtcwake -m disk on alarm +# nights (Mon, Fri, Sat, Sun by tomorrow's day-of-week). This ensures that +# both the automated systemd timer AND manual "shutdown now" hibernate +# correctly so the PC wakes for the morning alarm. +# +# Install to /usr/local/bin/shutdown (takes priority over /usr/bin/shutdown +# because /usr/local/bin appears first in PATH). + +set -euo pipefail + +REAL_SHUTDOWN=/usr/bin/shutdown +RTCWAKE=/usr/sbin/rtcwake +WAKE_AFTER_HOURS=8 # Must match WAKE_AFTER_HOURS in python_pkg/wake_alarm/_constants.py + +# Pass through reboots and cancel commands unchanged. +for arg in "$@"; do + case "$arg" in + -r|--reboot|-c|--cancel) + exec "$REAL_SHUTDOWN" "$@" + ;; + esac +done + +# Check if tomorrow is an alarm day (Mon=1, Fri=5, Sat=6, Sun=7 in date +%u). +tomorrow_dow=$(date -d "tomorrow" +%u) +case "$tomorrow_dow" in + 1|5|6|7) + wake_epoch=$(( $(printf '%(%s)T' -1) + WAKE_AFTER_HOURS * 3600 )) + logger -t shutdown-wrapper \ + "Tomorrow is alarm day (dow=$tomorrow_dow) — hibernating, RTC wake at epoch $wake_epoch" + sudo "$RTCWAKE" -m no -t "$wake_epoch" + exec /usr/bin/systemctl hibernate + ;; + *) + exec "$REAL_SHUTDOWN" "$@" + ;; +esac diff --git a/python_pkg/wake_alarm/sleep-hook.sh b/python_pkg/wake_alarm/sleep-hook.sh new file mode 100755 index 0000000..29fd334 --- /dev/null +++ b/python_pkg/wake_alarm/sleep-hook.sh @@ -0,0 +1,29 @@ +#!/bin/bash +# systemd-sleep hook: restart wake-alarm.service after resume from hibernate. +# +# Installed to /usr/lib/systemd/system-sleep/wake-alarm.sh by install.sh. +# +# When the PC hibernates (rtcwake -m disk) and resumes the next morning, +# the user session is restored but wake-alarm.service is in a stopped state +# (it ran at login the previous evening and exited with Restart=no). +# This hook restarts it so the alarm fires on the correct alarm day. + +if [[ "$1" != "post" ]]; then + exit 0 +fi + +logger -t wake-alarm-hook "Woke from sleep (type=$2) — restarting wake-alarm.service for active sessions" + +# Start wake-alarm.service for every logged-in user that has a running session +# bus. Works with systemd >= 219. +while IFS= read -r uid; do + runtime_dir="/run/user/$uid" + [[ -d "$runtime_dir" ]] || continue + username=$(id -nu "$uid" 2>/dev/null) || continue + logger -t wake-alarm-hook "Starting wake-alarm.service for user $username (uid=$uid)" + XDG_RUNTIME_DIR="$runtime_dir" \ + DBUS_SESSION_BUS_ADDRESS="unix:path=${runtime_dir}/bus" \ + runuser -u "$username" -- \ + systemctl --user start wake-alarm.service 2>/dev/null \ + || logger -t wake-alarm-hook "Failed to start wake-alarm.service for $username (non-fatal)" +done < <(loginctl list-sessions --no-legend 2>/dev/null | awk '{print $2}' | sort -u) diff --git a/python_pkg/wake_alarm/wake_state.json b/python_pkg/wake_alarm/wake_state.json new file mode 100644 index 0000000..1c22a17 --- /dev/null +++ b/python_pkg/wake_alarm/wake_state.json @@ -0,0 +1,6 @@ +{ + "date": "2026-05-22", + "dismissed_at": null, + "skip_workout": false, + "hmac": "2261cf16bef81ce45f8f0664454c441a88bdacf1e71161a10c50472ff63fdf8c" +}