feat: add multi-chat routing and bill-specific dashboard countdown
Some checks failed
Test blackout notifier / test (push) Failing after 4s
Hourly blackout check / check (push) Successful in 3s

This commit is contained in:
Meghdad
2026-07-22 02:14:45 +03:30
parent 6c3e146234
commit 42687ee651
12 changed files with 372 additions and 100 deletions

96
main.sh
View File

@@ -2,6 +2,31 @@
set -euo pipefail
# macOS ships Bash 3.2 and BSD date. Prefer Homebrew's compatible runtime when
# it is installed, even when this script was started explicitly with /bin/bash.
for coreutils_path in \
/opt/homebrew/opt/coreutils/libexec/gnubin \
/usr/local/opt/coreutils/libexec/gnubin; do
if [[ -d "$coreutils_path" ]]; then
PATH="$coreutils_path:$PATH"
export PATH
break
fi
done
unset coreutils_path
if ((BASH_VERSINFO[0] < 4 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] < 3))); then
for modern_bash in /opt/homebrew/bin/bash /usr/local/bin/bash; do
if [[ -x "$modern_bash" ]]; then
exec "$modern_bash" "$0" "$@"
fi
done
printf 'Error: Bash 4.3 or newer is required (found %s).\n' "$BASH_VERSION" >&2
printf 'On macOS, install it with: brew install bash coreutils\n' >&2
exit 2
fi
unset modern_bash
readonly STATE_URL="https://mahgit.ir/MeghdadFadaee/daily-blackout-check/raw/branch/main/state/outages.json"
readonly TEHRAN_TZ="Asia/Tehran"
@@ -36,20 +61,21 @@ trap cleanup EXIT
usage() {
cat <<'EOF'
Usage: ./main.sh [-i MINUTES]
Usage: ./main.sh [-i MINUTES] [-b BILL_ID]
Continuously display current and upcoming electricity outages from the public
repository state. The countdown updates every second.
Options:
-i, --interval MINUTES Refetch state at this interval (default: 10)
-b, --bill-id BILL_ID Use only this bill for the large countdown
-h, --help Show this help
Environment:
NO_COLOR Disable ANSI colors when set
COLUMNS Override the detected output width
Required commands: bash, curl, jq, and GNU date
Required commands: Bash 4.3+, curl, jq, and GNU date
EOF
}
@@ -299,6 +325,7 @@ record_fields() {
build_schedule() {
local path="$1"
local now_epoch="$2"
local countdown_bill_id="${3:-}"
local record start_epoch stop_epoch phase phase_order sort_key line hero_key=""
local -a fields sortable=()
@@ -324,7 +351,8 @@ build_schedule() {
phase="ONGOING"
phase_order=0
sort_key="$(printf '0:%020d:%s:%s' "$start_epoch" "${fields[0]}" "${fields[1]}")"
if [[ -z "$hero_key" || "$stop_epoch" -lt "$hero_key" ]]; then
if [[ (-z "$countdown_bill_id" || "${fields[0]}" == "$countdown_bill_id") \
&& (-z "$hero_key" || "$stop_epoch" -lt "$hero_key") ]]; then
hero_key="$stop_epoch"
HERO="$phase"$'\t'"$start_epoch"$'\t'"$stop_epoch"$'\t'"$record"
fi
@@ -332,9 +360,11 @@ build_schedule() {
phase="UPCOMING"
phase_order=1
sort_key="$(printf '1:%020d:%s:%s' "$start_epoch" "${fields[0]}" "${fields[1]}")"
if [[ -z "$hero_key" && -z "$HERO" ]]; then
if [[ (-z "$countdown_bill_id" || "${fields[0]}" == "$countdown_bill_id") \
&& -z "$hero_key" && -z "$HERO" ]]; then
HERO="$phase"$'\t'"$start_epoch"$'\t'"$stop_epoch"$'\t'"$record"
elif [[ "${HERO%%$'\t'*}" == "UPCOMING" ]]; then
elif [[ (-z "$countdown_bill_id" || "${fields[0]}" == "$countdown_bill_id") \
&& "${HERO%%$'\t'*}" == "UPCOMING" ]]; then
local current_hero_start
current_hero_start="${HERO#*$'\t'}"
current_hero_start="${current_hero_start%%$'\t'*}"
@@ -772,6 +802,7 @@ schedule_next_transition() {
render_dashboard() {
local now_epoch="$1"
local columns="${2:-}"
local countdown_bill_id="${3:-}"
local rule phase start_epoch stop_epoch record remaining banner
local schedule_line ignored
@@ -785,20 +816,30 @@ render_dashboard() {
return
fi
IFS=$'\t' read -r phase start_epoch stop_epoch record <<<"$HERO"
if [[ "$phase" == "ONGOING" ]]; then
remaining=$((stop_epoch - now_epoch))
else
remaining=$((start_epoch - now_epoch))
fi
banner="$(banner_duration "$remaining")"
rule="$(repeat_character '=' "$columns")"
printf '%s%s%s\n' "$CYAN" "$rule" "$RESET"
printf '\n'
printf '%s%s' "$BOLD" "$YELLOW"
print_large_countdown "$banner" "$columns"
printf '%s' "$RESET"
if [[ -z "$HERO" ]]; then
center_line "NO CURRENT OR UPCOMING OUTAGE" "$columns"
center_line "Countdown bill $countdown_bill_id" "$columns"
else
IFS=$'\t' read -r phase start_epoch stop_epoch record <<<"$HERO"
if [[ "$phase" == "ONGOING" ]]; then
remaining=$((stop_epoch - now_epoch))
else
remaining=$((start_epoch - now_epoch))
fi
banner="$(banner_duration "$remaining")"
if [[ -n "$countdown_bill_id" ]]; then
center_line "COUNTDOWN FOR BILL $countdown_bill_id" "$columns"
printf '\n'
fi
printf '%s%s' "$BOLD" "$YELLOW"
print_large_countdown "$banner" "$columns"
printf '%s' "$RESET"
fi
printf '\n%s%s%s\n' "$CYAN" "$rule" "$RESET"
printf '\n%sOUTAGE SCHEDULE (%d)%s\n\n' "$BOLD" "${#SCHEDULE[@]}" "$RESET"
@@ -812,7 +853,7 @@ render_dashboard() {
main() {
local now_epoch next_refresh=0 refresh_minutes=10 refresh_seconds state_ready=0
local last_rendered=-1 next_transition=0 rebuild_schedule=0 frame key input_fd=0
local display_columns
local display_columns countdown_bill_id=""
while (($#)); do
case "$1" in
@@ -833,6 +874,19 @@ main() {
refresh_minutes="${1#*=}"
shift
;;
-b|--bill-id)
if (($# < 2)); then
printf 'Error: %s requires a value\n' "$1" >&2
usage >&2
return 2
fi
countdown_bill_id="$2"
shift 2
;;
--bill-id=*)
countdown_bill_id="${1#*=}"
shift
;;
*)
printf 'Error: unknown option: %s\n' "$1" >&2
usage >&2
@@ -845,6 +899,10 @@ main() {
printf 'Error: interval must be a whole number from 1 to 1440 minutes\n' >&2
return 2
fi
if [[ -n "$countdown_bill_id" && ! "$countdown_bill_id" =~ ^[0-9]+$ ]]; then
printf 'Error: bill ID must contain only decimal digits\n' >&2
return 2
fi
refresh_seconds=$((10#$refresh_minutes * 60))
require_command curl
@@ -890,7 +948,7 @@ main() {
if ((state_ready)); then
if ((rebuild_schedule || (next_transition > 0 && now_epoch >= next_transition))); then
if build_schedule "$STATE_PATH" "$now_epoch"; then
if build_schedule "$STATE_PATH" "$now_epoch" "$countdown_bill_id"; then
next_transition="$(schedule_next_transition "$now_epoch")"
rebuild_schedule=0
else
@@ -903,7 +961,7 @@ main() {
display_columns="$(terminal_columns)"
frame="$(
if ((state_ready)); then
render_dashboard "$now_epoch" "$display_columns"
render_dashboard "$now_epoch" "$display_columns" "$countdown_bill_id"
else
render_unavailable "$display_columns"
fi