mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 20:07:04 +01:00
feat(digital-wellbeing): time-boxed shutdown overrides + drift-proof i3blocks deploy
Add a self-expiring override system so specific late-night live events (2026 World Cup matches) can be watched without the auto-shutdown killing the PC, without weakening the permanent schedule ratchet: - shutdown-override-manager.sh (add/list/remove): add is friction-gated (typed confirmation phrase + 30s delay, max 12h, max 3 windows) and writes absolute-epoch windows to /etc/shutdown-schedule-overrides.conf. - day-specific-shutdown-check.sh now skips the shutdown decision inside an active window (pruning expired entries) and gained a DRY_RUN guard around rtcwake/poweroff/hibernate for safe testing. i3blocks shutdown_countdown.sh: show the exact next-shutdown clock time (not a relative countdown), show the overridden end time in green when an override is active or will rescue the upcoming shutdown, and make the skip-dates check fork-free (builtin read loop) so the hot path spawns no external processes. install.sh: deploy i3blocks by symlinking each repo file into ~/.config/i3blocks instead of cp -r (the source of silent drift), cd to the script dir so it runs from anywhere, and chmod +x only *.sh so the config file stays 0644. Repair the previously-dead i3blocks regression suite (stale path), update the shutdown assertion to the new absolute-time output, add hermetic override coverage, and keep the <=1-exec fork-count guarantee. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmwYH5RfThZGkVD2jE61Da
This commit is contained in:
parent
343d38eaf8
commit
7b4a08f482
@ -0,0 +1,16 @@
|
||||
{
|
||||
"title": "World Cup shutdown overrides + drift-proof i3blocks deploy",
|
||||
"objective": "Allow time-boxed, self-expiring exceptions to the digital-wellbeing auto-shutdown so specific late-night live events can be watched on the PC, show the exact next-shutdown (or overridden) clock time in the i3bar, and make the i3blocks deployment symlink-based so running scripts can never silently drift from the repo. Success = a friction-gated override suspends the shutdown only within its window and expires on its own, the i3bar reflects the real next-shutdown time, and the reconciled scripts are the repo's single source of truth with the regression suite green.",
|
||||
"acceptance_criteria": [
|
||||
"shutdown-override-manager.sh add requires a typed confirmation phrase plus a 30s delay, enforces max duration and window count, and writes an absolute-epoch window that day-specific-shutdown-check.sh honors (verified live via DRY_RUN and journal logs).",
|
||||
"shutdown_countdown.sh shows the exact clock time of the next shutdown, the overridden end time in green when an override is active or will rescue the upcoming shutdown, and spawns zero external processes in its hot path (strace: 1 execve).",
|
||||
"install.sh symlinks every i3blocks file into ~/.config/i3blocks (idempotent) and leaves the non-script config at 0644; all 18 tracked files verified as symlinks after the user's live run.",
|
||||
"The i3blocks regression suite (test_i3blocks_efficiency.sh + test_i3blocks_persist_common.sh) runs from its correct path and passes, including new absolute-time and override assertions and the fork-count guarantee."
|
||||
],
|
||||
"out_of_scope": [
|
||||
"Weakening the permanent shutdown ratchet or lowering MORNING_END_HOUR's hard floor (overrides are strictly time-boxed and expire).",
|
||||
"Reconciling the three other pre-existing drifted config trees (phone_focus_mode, meta, .pre-commit-config) unrelated to this change.",
|
||||
"Any change to the i3 (window manager) config beyond what install.sh already did."
|
||||
],
|
||||
"verifier": "bash linux_configuration/tests/test_i3blocks_efficiency.sh (exit 0), shellcheck on the changed shell scripts, and the ai-evidence / pre-commit gate"
|
||||
}
|
||||
@ -0,0 +1,54 @@
|
||||
{
|
||||
"intent": "Let the PC survive the normal digital-wellbeing auto-shutdown during specific live-event windows (2026 World Cup matches) via self-expiring, friction-gated overrides; surface the exact next-shutdown clock time (or the overridden time) in the i3bar; and make the i3blocks deploy drift-proof so the running scripts can never silently diverge from the repo.",
|
||||
"scope": [
|
||||
"linux_configuration/scripts/periodic_background/digital_wellbeing/setup_midnight_shutdown.sh",
|
||||
"linux_configuration/scripts/periodic_background/i3-configuration/i3blocks/shutdown_countdown.sh",
|
||||
"linux_configuration/scripts/periodic_background/i3-configuration/install.sh",
|
||||
"linux_configuration/tests/test_i3blocks_efficiency.sh",
|
||||
"linux_configuration/tests/test_i3blocks_persist_common.sh",
|
||||
"Non-goal: does not weaken the permanent shutdown ratchet or lower MORNING_END_HOUR's hard floor; overrides are time-boxed and expire automatically."
|
||||
],
|
||||
"changes": [
|
||||
"Added a time-boxed override system: shutdown-override-manager.sh (add/list/remove) writes absolute-epoch windows to /etc/shutdown-schedule-overrides.conf; adding requires an exact typed confirmation phrase plus a 30s delay. The generated day-specific-shutdown-check.sh now skips the shutdown decision inside an active window (pruning expired entries) and gained a DRY_RUN guard around rtcwake/poweroff/hibernate for safe testing.",
|
||||
"Rewrote the i3blocks shutdown_countdown.sh to show the exact clock time of the next shutdown (e.g. '21:00') instead of a relative countdown, to show the overridden end time in green when an override is active or will rescue the upcoming shutdown, and made the skip-dates check fork-free (bash builtin read loop replacing grep -qxF) so the hot path spawns zero external processes.",
|
||||
"Changed install.sh to deploy i3blocks by symlinking each repo file into ~/.config/i3blocks (was cp -r, the source of silent drift), added cd-to-script-dir so it runs from anywhere, and restricted chmod +x to *.sh so the non-executable config file is left at 0644. Reconciled all 18 live files (17 scripts + config) to symlinks.",
|
||||
"Repaired the previously-dead i3blocks regression suite: fixed the stale i3-configuration path in both test files, updated the shutdown assertion to the new absolute-time output, added hermetic override coverage, and re-verified the fork-count guarantee (<=1 exec)."
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "sudo DRY_RUN=1 /usr/local/bin/day-specific-shutdown-check.sh (with a short test override active)",
|
||||
"result": "pass",
|
||||
"evidence": "journalctl showed 'Active override in effect (reason: ...) - skipping shutdown check' and the script exited without evaluating the day-of-week branch; outside the window it logged the normal skip/shutdown decision unchanged."
|
||||
},
|
||||
{
|
||||
"command": "NOW_EPOCH=... bash ~/.config/i3blocks/shutdown_countdown.sh across normal/active-override/gap/skip-date cases",
|
||||
"result": "pass",
|
||||
"evidence": "Normal time -> '⏻ 21:00'; inside override -> '▶ 00:30 (override)' green; gap between windows -> '⏻ SHUTDOWN'; matching skip-date -> empty output exit 0. Matches expected enforcement."
|
||||
},
|
||||
{
|
||||
"command": "strace -f -e trace=execve bash ~/.config/i3blocks/shutdown_countdown.sh",
|
||||
"result": "pass",
|
||||
"evidence": "Exactly 1 execve (the bash interpreter); no grep/date forks in the hot path after the builtin-read rewrite."
|
||||
},
|
||||
{
|
||||
"command": "bash tests/test_i3blocks_efficiency.sh",
|
||||
"result": "pass",
|
||||
"evidence": "Exit code 0 - 'All i3blocks efficiency regression tests passed.' including the new absolute-time and override assertions and the fork-count check."
|
||||
},
|
||||
{
|
||||
"command": "shellcheck on shutdown_countdown.sh and install.sh (-s sh); bash -n on setup_midnight_shutdown.sh",
|
||||
"result": "pass",
|
||||
"evidence": "All lint-clean, zero warnings; setup script syntax OK. Live install.sh run by the user completed through 'i3-msg reload' -> [{\"success\":true}] and 18/18 files verified as symlinks."
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"Overrides intentionally suspend the wellbeing shutdown for their window; a mistakenly-long window keeps the PC up. Mitigated by the 12h max duration, 3-window cap, typed-confirmation + 30s delay on add, and automatic expiry.",
|
||||
"The override and schedule config files must stay world-readable (0644) because the i3blocks countdown runs as the regular user; a future 0600 regression would blank the block (the script now degrades on unreadable files rather than crashing).",
|
||||
"Symlink deploy assumes the repo stays at its current path; moving the repo breaks the ~/.config/i3blocks symlinks until install.sh is re-run."
|
||||
],
|
||||
"rollback": [
|
||||
"i3blocks: git checkout the four repo files, then re-run install.sh (or cp the previous scripts into ~/.config/i3blocks) to restore prior deploy; symlinks are replaced idempotently.",
|
||||
"Overrides: sudo shutdown-override-manager.sh remove <n> cancels a window immediately, or truncate /etc/shutdown-schedule-overrides.conf; the check script prunes expired entries on its own.",
|
||||
"After rollback, run bash tests/test_i3blocks_efficiency.sh and confirm the i3bar block still renders to validate the restored state."
|
||||
]
|
||||
}
|
||||
@ -324,6 +324,14 @@ show_current_status() {
|
||||
echo " - Canonical copy that auto-restores on modification"
|
||||
echo " - Path watcher service"
|
||||
echo ""
|
||||
|
||||
echo "Active Overrides:"
|
||||
if command -v /usr/local/bin/shutdown-override-manager.sh >/dev/null 2>&1; then
|
||||
/usr/local/bin/shutdown-override-manager.sh list | sed 's/^/ /'
|
||||
else
|
||||
echo " (override manager not installed)"
|
||||
fi
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Function to create/update shutdown schedule config file (shared with
|
||||
@ -819,6 +827,49 @@ create_shutdown_check_script() {
|
||||
# Reads shutdown windows from /etc/shutdown-schedule.conf
|
||||
|
||||
CONFIG_FILE="/etc/shutdown-schedule.conf"
|
||||
OVERRIDES_FILE="/etc/shutdown-schedule-overrides.conf"
|
||||
|
||||
# Time-boxed exceptions (e.g. watching a live event that runs past the normal
|
||||
# shutdown window) are registered via shutdown-override-manager.sh, which
|
||||
# appends "start_epoch|end_epoch|created_epoch|reason" lines to OVERRIDES_FILE.
|
||||
# Entries are absolute-epoch-bound so they expire on their own; this function
|
||||
# prunes stale lines and returns success (skip shutdown) if now falls inside
|
||||
# any remaining window.
|
||||
check_active_override() {
|
||||
[[ -f "$OVERRIDES_FILE" ]] || return 1
|
||||
|
||||
local now
|
||||
now=$(printf '%(%s)T' -1)
|
||||
|
||||
local kept=""
|
||||
local active=false
|
||||
local active_reason=""
|
||||
local start_epoch end_epoch _created reason
|
||||
|
||||
while IFS='|' read -r start_epoch end_epoch _created reason; do
|
||||
[[ -n "$start_epoch" ]] || continue
|
||||
if [[ $end_epoch -lt $now ]]; then
|
||||
continue # expired, drop it
|
||||
fi
|
||||
kept+="${start_epoch}|${end_epoch}|${_created}|${reason}"$'\n'
|
||||
if [[ $now -ge $start_epoch ]] && [[ $now -le $end_epoch ]]; then
|
||||
active=true
|
||||
active_reason="$reason"
|
||||
fi
|
||||
done <"$OVERRIDES_FILE"
|
||||
|
||||
printf '%s' "$kept" >"$OVERRIDES_FILE"
|
||||
|
||||
if [[ $active == true ]]; then
|
||||
logger -t day-specific-shutdown "Active override in effect (reason: ${active_reason}) - skipping shutdown check"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
if check_active_override; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Load config
|
||||
if [[ ! -f "$CONFIG_FILE" ]]; then
|
||||
@ -910,12 +961,20 @@ if [[ $should_shutdown == true ]]; then
|
||||
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 -i
|
||||
if [[ "${DRY_RUN:-}" == "1" ]]; then
|
||||
logger -t day-specific-shutdown "DRY_RUN: would run rtcwake -m no -t $wake_epoch then systemctl hibernate -i"
|
||||
else
|
||||
/usr/bin/sudo /usr/sbin/rtcwake -m no -t "$wake_epoch"
|
||||
/usr/bin/systemctl hibernate -i
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
logger -t day-specific-shutdown "Tomorrow is not an alarm day — powering off normally"
|
||||
/usr/bin/systemctl poweroff -i
|
||||
if [[ "${DRY_RUN:-}" == "1" ]]; then
|
||||
logger -t day-specific-shutdown "DRY_RUN: would run systemctl poweroff -i"
|
||||
else
|
||||
/usr/bin/systemctl poweroff -i
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
else
|
||||
@ -928,6 +987,243 @@ EOF
|
||||
echo "✓ Created smart shutdown check script: $check_script"
|
||||
}
|
||||
|
||||
# Function to create the time-boxed override manager (event exceptions like
|
||||
# watching a live match that runs past the normal shutdown window). Overrides
|
||||
# are absolute-epoch-bound so they expire on their own; the friction (typed
|
||||
# confirmation + delay) lives entirely in this CLI, matching the existing
|
||||
# unlock script's philosophy for the permanent schedule ratchet.
|
||||
create_override_manager_script() {
|
||||
echo ""
|
||||
echo "9. Creating Shutdown Override Manager..."
|
||||
echo "========================================"
|
||||
|
||||
local overrides_file="/etc/shutdown-schedule-overrides.conf"
|
||||
local log_file="/var/log/shutdown-schedule-overrides.log"
|
||||
local manager_script="/usr/local/bin/shutdown-override-manager.sh"
|
||||
|
||||
touch "$overrides_file"
|
||||
# World-readable (like shutdown-schedule.conf): the i3blocks countdown
|
||||
# script runs as the regular user and needs to read active overrides to
|
||||
# display them. Only root can write to it (via this manager script).
|
||||
chmod 644 "$overrides_file"
|
||||
touch "$log_file"
|
||||
chmod 600 "$log_file"
|
||||
|
||||
cat >"$manager_script" <<'EOF'
|
||||
#!/bin/bash
|
||||
# Shutdown Override Manager
|
||||
# Registers time-boxed exceptions to the day-specific-shutdown schedule, e.g.
|
||||
# to watch a live event that runs past the normal shutdown window. Entries are
|
||||
# absolute-epoch-bound and expire on their own - there is nothing to re-lock.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
OVERRIDES_FILE="/etc/shutdown-schedule-overrides.conf"
|
||||
LOG_FILE="/var/log/shutdown-schedule-overrides.log"
|
||||
MAX_OVERRIDE_HOURS=12
|
||||
MAX_ACTIVE_OVERRIDES=3
|
||||
MIN_REASON_LEN=10
|
||||
CONFIRM_PHRASE="I am deliberately bypassing my shutdown protection"
|
||||
DELAY_SECONDS=30
|
||||
|
||||
log() {
|
||||
printf '%s - %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" | tee -a "$LOG_FILE" >&2
|
||||
}
|
||||
|
||||
require_root() {
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
echo "This command requires sudo." >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
now_epoch() {
|
||||
printf '%(%s)T' -1
|
||||
}
|
||||
|
||||
prune_and_read() {
|
||||
# Prints kept (non-expired) lines; overwrites OVERRIDES_FILE with them.
|
||||
local now kept=""
|
||||
now=$(now_epoch)
|
||||
if [[ -f "$OVERRIDES_FILE" ]]; then
|
||||
local start_epoch end_epoch created reason
|
||||
while IFS='|' read -r start_epoch end_epoch created reason; do
|
||||
[[ -n "$start_epoch" ]] || continue
|
||||
[[ "$end_epoch" -lt "$now" ]] && continue
|
||||
kept+="${start_epoch}|${end_epoch}|${created}|${reason}"$'\n'
|
||||
done <"$OVERRIDES_FILE"
|
||||
fi
|
||||
printf '%s' "$kept" >"$OVERRIDES_FILE"
|
||||
printf '%s' "$kept"
|
||||
}
|
||||
|
||||
cmd_list() {
|
||||
local kept
|
||||
kept="$(prune_and_read)"
|
||||
if [[ -z "$kept" ]]; then
|
||||
echo "No active or upcoming overrides."
|
||||
return 0
|
||||
fi
|
||||
echo "Active/upcoming overrides:"
|
||||
local i=1
|
||||
local start_epoch end_epoch created reason
|
||||
while IFS='|' read -r start_epoch end_epoch created reason; do
|
||||
[[ -n "$start_epoch" ]] || continue
|
||||
printf ' [%d] %s -> %s (reason: %s)\n' \
|
||||
"$i" \
|
||||
"$(date -d "@$start_epoch" '+%Y-%m-%d %H:%M')" \
|
||||
"$(date -d "@$end_epoch" '+%Y-%m-%d %H:%M')" \
|
||||
"$reason"
|
||||
i=$((i + 1))
|
||||
done <<<"$kept"
|
||||
}
|
||||
|
||||
cmd_remove() {
|
||||
require_root
|
||||
local target="${1:-}"
|
||||
[[ -n "$target" ]] || { echo "Usage: $0 remove <line-number-from-list>" >&2; exit 1; }
|
||||
|
||||
local kept
|
||||
kept="$(prune_and_read)"
|
||||
if [[ -z "$kept" ]]; then
|
||||
echo "No overrides to remove." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local new_content="" i=1 start_epoch end_epoch created reason removed=false
|
||||
while IFS='|' read -r start_epoch end_epoch created reason; do
|
||||
[[ -n "$start_epoch" ]] || continue
|
||||
if [[ "$i" == "$target" ]]; then
|
||||
removed=true
|
||||
else
|
||||
new_content+="${start_epoch}|${end_epoch}|${created}|${reason}"$'\n'
|
||||
fi
|
||||
i=$((i + 1))
|
||||
done <<<"$kept"
|
||||
|
||||
if [[ "$removed" != true ]]; then
|
||||
echo "No override at index $target" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf '%s' "$new_content" >"$OVERRIDES_FILE"
|
||||
log "Removed override index $target (by $(logname 2>/dev/null || echo unknown))"
|
||||
echo "✓ Removed override $target"
|
||||
}
|
||||
|
||||
cmd_add() {
|
||||
require_root
|
||||
local start_str="${1:-}" end_str="${2:-}"
|
||||
local reason="${*:3}"
|
||||
|
||||
if [[ -z "$start_str" ]] || [[ -z "$end_str" ]] || [[ -z "$reason" ]]; then
|
||||
echo "Usage: $0 add '<start datetime>' '<end datetime>' <reason>" >&2
|
||||
echo "Example: $0 add '2026-07-10 21:30' '2026-07-11 00:30' 'World Cup match'" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "${#reason}" -lt "$MIN_REASON_LEN" ]]; then
|
||||
echo "Reason must be at least $MIN_REASON_LEN characters." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local start_epoch end_epoch
|
||||
start_epoch=$(date -d "$start_str" +%s) || { echo "Could not parse start datetime: $start_str" >&2; exit 1; }
|
||||
end_epoch=$(date -d "$end_str" +%s) || { echo "Could not parse end datetime: $end_str" >&2; exit 1; }
|
||||
|
||||
if [[ "$end_epoch" -le "$start_epoch" ]]; then
|
||||
echo "End must be after start." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local duration_hours=$(( (end_epoch - start_epoch) / 3600 ))
|
||||
if [[ "$duration_hours" -gt "$MAX_OVERRIDE_HOURS" ]]; then
|
||||
echo "Override duration (${duration_hours}h) exceeds the max of ${MAX_OVERRIDE_HOURS}h." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local kept active_count
|
||||
kept="$(prune_and_read)"
|
||||
active_count=$(grep -c . <<<"$kept" 2>/dev/null || true)
|
||||
active_count="${active_count:-0}"
|
||||
if [[ "$active_count" -ge "$MAX_ACTIVE_OVERRIDES" ]]; then
|
||||
echo "Already at the max of $MAX_ACTIVE_OVERRIDES active/upcoming overrides. Remove one first." >&2
|
||||
cmd_list
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "╔══════════════════════════════════════════════════════════════════╗"
|
||||
echo "║ SHUTDOWN SCHEDULE OVERRIDE REQUEST ║"
|
||||
echo "╚══════════════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
echo "This suspends the shutdown enforcement you built for yourself during:"
|
||||
echo " $(date -d "@$start_epoch" '+%Y-%m-%d %H:%M') -> $(date -d "@$end_epoch" '+%Y-%m-%d %H:%M') (${duration_hours}h)"
|
||||
echo " Reason: $reason"
|
||||
echo ""
|
||||
echo "Current overrides:"
|
||||
cmd_list
|
||||
echo ""
|
||||
echo "To confirm this is a deliberate, one-off exception (not just wanting to"
|
||||
echo "stay up), type the following phrase exactly:"
|
||||
echo ""
|
||||
echo " $CONFIRM_PHRASE"
|
||||
echo ""
|
||||
read -r -p "> " typed_phrase
|
||||
if [[ "$typed_phrase" != "$CONFIRM_PHRASE" ]]; then
|
||||
echo "Phrase did not match. Aborting." >&2
|
||||
log "ABORTED: confirmation phrase mismatch for window $start_str -> $end_str"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Waiting $DELAY_SECONDS seconds before committing (Ctrl+C to cancel)..."
|
||||
local i
|
||||
for ((i = DELAY_SECONDS; i > 0; i--)); do
|
||||
printf "\r Waiting: %2d seconds remaining..." "$i"
|
||||
sleep 1
|
||||
done
|
||||
echo ""
|
||||
|
||||
local created
|
||||
created=$(now_epoch)
|
||||
printf '%s|%s|%s|%s\n' "$start_epoch" "$end_epoch" "$created" "$reason" >>"$OVERRIDES_FILE"
|
||||
|
||||
log "ADDED override by $(logname 2>/dev/null || echo unknown) from TTY $(tty 2>/dev/null || echo unknown): $start_str -> $end_str (reason: $reason)"
|
||||
echo "✓ Override registered: $(date -d "@$start_epoch" '+%Y-%m-%d %H:%M') -> $(date -d "@$end_epoch" '+%Y-%m-%d %H:%M')"
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
add)
|
||||
shift
|
||||
cmd_add "$@"
|
||||
;;
|
||||
list)
|
||||
cmd_list
|
||||
;;
|
||||
remove)
|
||||
shift
|
||||
cmd_remove "$@"
|
||||
;;
|
||||
*)
|
||||
echo "Shutdown Override Manager"
|
||||
echo "Usage: $0 {add|list|remove}"
|
||||
echo ""
|
||||
echo " add '<start>' '<end>' <reason> Register a time-boxed exception"
|
||||
echo " list Show active/upcoming overrides"
|
||||
echo " remove <index> Cancel an override (no friction)"
|
||||
echo ""
|
||||
cmd_list
|
||||
;;
|
||||
esac
|
||||
EOF
|
||||
|
||||
chmod +x "$manager_script"
|
||||
echo "✓ Created override manager: $manager_script"
|
||||
echo "✓ Overrides file: $overrides_file (root-only)"
|
||||
echo "✓ Override log: $log_file"
|
||||
}
|
||||
|
||||
# Function to enable the timer
|
||||
enable_timer() {
|
||||
echo ""
|
||||
@ -1405,6 +1701,7 @@ enable_midnight_shutdown() {
|
||||
create_shutdown_timer
|
||||
create_management_script
|
||||
create_shutdown_check_script
|
||||
create_override_manager_script
|
||||
|
||||
# Enable and start timer
|
||||
enable_timer
|
||||
|
||||
@ -1,9 +1,13 @@
|
||||
#!/bin/bash
|
||||
# Shutdown countdown status script for i3blocks.
|
||||
# Shows the exact absolute time of the next enforced shutdown, or the
|
||||
# overridden time if a shutdown-override-manager.sh window rescues it.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SHUTDOWN_CONFIG=${SHUTDOWN_CONFIG:-/etc/shutdown-schedule.conf}
|
||||
SKIP_DATES_FILE=${SKIP_DATES_FILE:-/etc/shutdown-skip-dates}
|
||||
OVERRIDES_FILE=${OVERRIDES_FILE:-/etc/shutdown-schedule-overrides.conf}
|
||||
|
||||
# Function to show error state in i3blocks and exit
|
||||
show_error() {
|
||||
@ -57,12 +61,25 @@ get_now_epoch() {
|
||||
}
|
||||
|
||||
now_epoch=$(get_now_epoch)
|
||||
printf -v today_date '%(%Y-%m-%d)T' "$now_epoch"
|
||||
# Fork-free whole-line match against the skip-dates file (bash builtin read,
|
||||
# no `grep` process spawned every tick). Equivalent to `grep -qxF`.
|
||||
if [[ -r $SKIP_DATES_FILE ]]; then
|
||||
while IFS= read -r skip_date || [[ -n $skip_date ]]; do
|
||||
if [[ $skip_date == "$today_date" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
done < "$SKIP_DATES_FILE"
|
||||
fi
|
||||
|
||||
printf -v current_hour '%(%H)T' "$now_epoch"
|
||||
printf -v current_minute '%(%M)T' "$now_epoch"
|
||||
printf -v current_second '%(%S)T' "$now_epoch"
|
||||
printf -v day_of_week '%(%u)T' "$now_epoch"
|
||||
|
||||
current_time_minutes=$((10#$current_hour * 60 + 10#$current_minute))
|
||||
morning_end_minutes=$((10#$morning_end_hour * 60))
|
||||
midnight_epoch=$((now_epoch - (10#$current_hour * 3600 + 10#$current_minute * 60 + 10#$current_second)))
|
||||
|
||||
if [[ $day_of_week -ge 1 ]] && [[ $day_of_week -le 3 ]]; then
|
||||
shutdown_hour=$MON_WED_HOUR
|
||||
@ -71,7 +88,41 @@ else
|
||||
fi
|
||||
|
||||
shutdown_time_minutes=$((shutdown_hour * 60))
|
||||
shutdown_epoch_today=$((midnight_epoch + shutdown_hour * 3600))
|
||||
|
||||
# Prints "start_epoch|end_epoch|reason" for the first registered override
|
||||
# (shutdown-override-manager.sh) whose window covers the given epoch, empty
|
||||
# otherwise. Builtin `read` only - no forks in this hot path.
|
||||
find_override_covering() {
|
||||
local target_epoch=$1
|
||||
[[ -f $OVERRIDES_FILE ]] || return 1
|
||||
local start_epoch end_epoch _created reason
|
||||
while IFS='|' read -r start_epoch end_epoch _created reason; do
|
||||
[[ -n $start_epoch ]] || continue
|
||||
if [[ $target_epoch -ge $start_epoch ]] && [[ $target_epoch -le $end_epoch ]]; then
|
||||
printf '%s|%s|%s\n' "$start_epoch" "$end_epoch" "$reason"
|
||||
return 0
|
||||
fi
|
||||
done <"$OVERRIDES_FILE"
|
||||
return 1
|
||||
}
|
||||
|
||||
format_hhmm() {
|
||||
printf '%(%H:%M)T' "$1"
|
||||
}
|
||||
|
||||
# Case 1: an override is active right now - always takes priority, whether
|
||||
# or not we are inside the normal blocked window.
|
||||
if override_match=$(find_override_covering "$now_epoch"); then
|
||||
IFS='|' read -r _ override_end _ <<<"$override_match"
|
||||
echo "▶ $(format_hhmm "$override_end") (override)"
|
||||
echo "▶ $(format_hhmm "$override_end")"
|
||||
echo "#50FA7B"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Case 2: currently inside the normal blocked window with no rescuing
|
||||
# override registered - shutdown is due now.
|
||||
if [[ $current_time_minutes -ge $shutdown_time_minutes ]] || [[ $current_time_minutes -le $morning_end_minutes ]]; then
|
||||
echo "⏻ SHUTDOWN"
|
||||
echo "⏻"
|
||||
@ -79,30 +130,29 @@ if [[ $current_time_minutes -ge $shutdown_time_minutes ]] || [[ $current_time_mi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
minutes_until_shutdown=$((shutdown_time_minutes - current_time_minutes))
|
||||
hours=$((minutes_until_shutdown / 60))
|
||||
minutes=$((minutes_until_shutdown % 60))
|
||||
|
||||
if [[ $hours -gt 0 ]]; then
|
||||
time_str="${hours}h ${minutes}m"
|
||||
else
|
||||
time_str="${minutes}m"
|
||||
# Case 3: normal usable time. Show the exact hour of the next shutdown,
|
||||
# unless a registered override starts before it and extends past it - in
|
||||
# which case show the rescued (overridden) time instead, proactively.
|
||||
if override_match=$(find_override_covering "$shutdown_epoch_today"); then
|
||||
IFS='|' read -r _ override_end _ <<<"$override_match"
|
||||
echo "▶ $(format_hhmm "$override_end") (override)"
|
||||
echo "▶ $(format_hhmm "$override_end")"
|
||||
echo "#50FA7B"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
minutes_until_shutdown=$((shutdown_time_minutes - current_time_minutes))
|
||||
|
||||
if [[ $minutes_until_shutdown -le 30 ]]; then
|
||||
color="#FF5555"
|
||||
icon="⏻"
|
||||
elif [[ $minutes_until_shutdown -le 60 ]]; then
|
||||
color="#FFB86C"
|
||||
icon="⏻"
|
||||
elif [[ $minutes_until_shutdown -le 120 ]]; then
|
||||
color="#F1FA8C"
|
||||
icon="⏻"
|
||||
else
|
||||
color="#6272A4"
|
||||
icon="⏻"
|
||||
fi
|
||||
|
||||
echo "$icon $time_str"
|
||||
echo "$icon"
|
||||
echo "⏻ $(format_hhmm "$shutdown_epoch_today")"
|
||||
echo "⏻"
|
||||
echo "$color"
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Resolve the directory this script lives in and work from there, so every
|
||||
# relative path below (i3blocks/, i3/) resolves no matter where it is invoked.
|
||||
SCRIPT_DIR=$(unset CDPATH; cd -- "$(dirname -- "$0")" && pwd)
|
||||
cd "$SCRIPT_DIR" || exit 1
|
||||
|
||||
# Function to detect if the system is Ubuntu
|
||||
is_ubuntu() {
|
||||
[ -f /etc/os-release ] && grep -qi 'ubuntu' /etc/os-release
|
||||
@ -46,9 +51,23 @@ fi
|
||||
# Set font size based on screen resolution
|
||||
font_size=$(set_font_size)
|
||||
|
||||
# Make all scripts in i3blocks executable
|
||||
find i3blocks -type f -exec chmod +x {} \;
|
||||
cp -r i3blocks ~/.config/
|
||||
# Make the i3blocks shell scripts executable (only *.sh — the `config` file
|
||||
# and any runtime artifacts must stay non-executable).
|
||||
find i3blocks -type f -name '*.sh' -exec chmod +x {} \;
|
||||
|
||||
# Deploy i3blocks by SYMLINKING each repo file into ~/.config/i3blocks rather
|
||||
# than copying it. A plain `cp -r` here was the source of silent drift: edits
|
||||
# to a repo script never reached the running copy (and live tweaks never made
|
||||
# it back to the repo). Symlinks make the repo the single source of truth, so
|
||||
# editing a repo script IS editing the deployed one — drift becomes impossible.
|
||||
mkdir -p "$HOME/.config/i3blocks"
|
||||
for src in "$SCRIPT_DIR"/i3blocks/*; do
|
||||
[ -f "$src" ] || continue
|
||||
ln -sfn "$src" "$HOME/.config/i3blocks/$(basename "$src")"
|
||||
done
|
||||
|
||||
# i3 config is copied (not symlinked) because it needs a per-machine font-size
|
||||
# mutation applied below; symlinking would write that change back into the repo.
|
||||
cp -r i3 ~/.config/
|
||||
sed -i "s/font pango:System San Francisco Display, FontAwesome [0-9]*/font pango:System San Francisco Display, FontAwesome $font_size/" ~/.config/i3/config
|
||||
i3-msg reload
|
||||
|
||||
@ -5,7 +5,7 @@ set -euo pipefail
|
||||
|
||||
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
||||
REPO_DIR=$(cd -- "$SCRIPT_DIR/.." && pwd)
|
||||
I3BLOCKS_DIR="$REPO_DIR/i3-configuration/i3blocks"
|
||||
I3BLOCKS_DIR="$REPO_DIR/scripts/periodic_background/i3-configuration/i3blocks"
|
||||
CONFIG_FILE="$I3BLOCKS_DIR/config"
|
||||
|
||||
printf 'Running persist_common helper regression checks...\n'
|
||||
@ -310,18 +310,38 @@ MON_WED_HOUR=23
|
||||
THU_SUN_HOUR=21
|
||||
EOF
|
||||
|
||||
# Isolate from the machine's real override file so these assertions are hermetic.
|
||||
shutdown_overrides_empty="$TMP_DIR/shutdown-overrides-empty.conf"
|
||||
: > "$shutdown_overrides_empty"
|
||||
|
||||
# 2026-05-01 is a Friday -> THU_SUN_HOUR=21 applies; at 19:30 the next shutdown
|
||||
# is 21:00. The block shows the exact clock time of the shutdown, not a
|
||||
# relative countdown.
|
||||
shutdown_countdown_epoch=$(epoch_utc '2026-05-01 19:30:00')
|
||||
shutdown_countdown_output=$(TZ=UTC NOW_EPOCH="$shutdown_countdown_epoch" SHUTDOWN_CONFIG="$shutdown_config_file" PATH="$BIN_DIR:$PATH" bash "$I3BLOCKS_DIR/shutdown_countdown.sh")
|
||||
assert_equals '⏻ 1h 30m' "$(printf '%s\n' "$shutdown_countdown_output" | sed -n '1p')" \
|
||||
'shutdown countdown should show the time remaining until shutdown'
|
||||
shutdown_countdown_output=$(TZ=UTC NOW_EPOCH="$shutdown_countdown_epoch" SHUTDOWN_CONFIG="$shutdown_config_file" OVERRIDES_FILE="$shutdown_overrides_empty" PATH="$BIN_DIR:$PATH" bash "$I3BLOCKS_DIR/shutdown_countdown.sh")
|
||||
assert_equals '⏻ 21:00' "$(printf '%s\n' "$shutdown_countdown_output" | sed -n '1p')" \
|
||||
'shutdown countdown should show the exact clock time of the next shutdown'
|
||||
assert_equals '#F1FA8C' "$(printf '%s\n' "$shutdown_countdown_output" | sed -n '3p')" \
|
||||
'shutdown countdown should show yellow for two hours or less remaining'
|
||||
assert_le "$(count_execs "$I3BLOCKS_DIR/shutdown_countdown.sh")" 1 \
|
||||
'shutdown countdown should avoid date helpers in the hot path'
|
||||
|
||||
shutdown_window_epoch=$(epoch_utc '2026-05-01 21:15:00')
|
||||
shutdown_window_output=$(TZ=UTC NOW_EPOCH="$shutdown_window_epoch" SHUTDOWN_CONFIG="$shutdown_config_file" PATH="$BIN_DIR:$PATH" bash "$I3BLOCKS_DIR/shutdown_countdown.sh")
|
||||
shutdown_window_output=$(TZ=UTC NOW_EPOCH="$shutdown_window_epoch" SHUTDOWN_CONFIG="$shutdown_config_file" OVERRIDES_FILE="$shutdown_overrides_empty" PATH="$BIN_DIR:$PATH" bash "$I3BLOCKS_DIR/shutdown_countdown.sh")
|
||||
assert_equals '⏻ SHUTDOWN' "$(printf '%s\n' "$shutdown_window_output" | sed -n '1p')" \
|
||||
'shutdown countdown should show SHUTDOWN inside the blocked window'
|
||||
|
||||
# An active override (shutdown-override-manager.sh) rescues the shutdown: the
|
||||
# block should show the overridden end time in green instead of a countdown or
|
||||
# a SHUTDOWN warning. Window 19:00-23:00 covers the 19:30 sample time.
|
||||
shutdown_overrides_active="$TMP_DIR/shutdown-overrides-active.conf"
|
||||
override_start=$(epoch_utc '2026-05-01 19:00:00')
|
||||
override_end=$(epoch_utc '2026-05-01 23:00:00')
|
||||
printf '%s|%s|%s|%s\n' "$override_start" "$override_end" "$override_start" 'regression test override' > "$shutdown_overrides_active"
|
||||
shutdown_override_output=$(TZ=UTC NOW_EPOCH="$shutdown_countdown_epoch" SHUTDOWN_CONFIG="$shutdown_config_file" OVERRIDES_FILE="$shutdown_overrides_active" PATH="$BIN_DIR:$PATH" bash "$I3BLOCKS_DIR/shutdown_countdown.sh")
|
||||
assert_equals '▶ 23:00 (override)' "$(printf '%s\n' "$shutdown_override_output" | sed -n '1p')" \
|
||||
'shutdown countdown should show the overridden end time when an override is active'
|
||||
assert_equals '#50FA7B' "$(printf '%s\n' "$shutdown_override_output" | sed -n '3p')" \
|
||||
'shutdown countdown should show green while an override is active'
|
||||
|
||||
printf 'All i3blocks efficiency regression tests passed.\n'
|
||||
|
||||
@ -5,7 +5,7 @@ set -euo pipefail
|
||||
|
||||
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
||||
REPO_DIR=$(cd -- "$SCRIPT_DIR/.." && pwd)
|
||||
HELPER="$REPO_DIR/i3-configuration/i3blocks/persist_common.sh"
|
||||
HELPER="$REPO_DIR/scripts/periodic_background/i3-configuration/i3blocks/persist_common.sh"
|
||||
TMP_DIR=$(mktemp -d)
|
||||
|
||||
cleanup() {
|
||||
@ -13,7 +13,7 @@ cleanup() {
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
# shellcheck source=linux_configuration/i3-configuration/i3blocks/persist_common.sh
|
||||
# shellcheck source=linux_configuration/scripts/periodic_background/i3-configuration/i3blocks/persist_common.sh
|
||||
source "$HELPER"
|
||||
|
||||
fail() {
|
||||
@ -136,7 +136,7 @@ cat >"$fork_probe" <<EOF
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# shellcheck source=linux_configuration/i3-configuration/i3blocks/persist_common.sh
|
||||
# shellcheck source=linux_configuration/scripts/periodic_background/i3-configuration/i3blocks/persist_common.sh
|
||||
source "$HELPER"
|
||||
|
||||
I3BLOCKS_LAST_TS=()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user