Compare commits
11 Commits
42687ee651
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14f54dd09f | ||
|
|
b75f44f0a1 | ||
| bf09d4d99c | |||
| 0b24d544d3 | |||
|
|
b44d6dd3cb | ||
|
|
cfd19fe4a8 | ||
|
|
5f5329530c | ||
|
|
88e11f6594 | ||
|
|
ec54c552c2 | ||
|
|
e9947a14cb | ||
|
|
ea03d84901 |
19
README.md
19
README.md
@@ -215,7 +215,7 @@ minute interval with `-i`/`--interval`:
|
||||
./main.sh --interval 5
|
||||
```
|
||||
|
||||
Select one bill for the large countdown with `-b`/`--bill-id`:
|
||||
Select one bill for the countdown and outage schedule with `-b`/`--bill-id`:
|
||||
|
||||
```sh
|
||||
./main.sh --bill-id 7356609804429
|
||||
@@ -223,20 +223,25 @@ Select one bill for the large countdown with `-b`/`--bill-id`:
|
||||
|
||||
The dashboard uses Tehran time to show a large `HH : mm : ss` countdown for the
|
||||
selected bill's ongoing or nearest upcoming outage. The compact schedule below
|
||||
continues to list active current and future events for every bill, including
|
||||
emergency outages. If the selected bill has no qualifying outage, the countdown
|
||||
area says so while the full schedule remains visible. Without `--bill-id`, the
|
||||
countdown retains the original behavior and selects across all bills.
|
||||
is also limited to that bill's active current and future events, including
|
||||
emergency outages. If the selected bill has no qualifying outage, the dashboard
|
||||
shows `ALL CLEAR`. Without `--bill-id`, the countdown and schedule include all
|
||||
bills.
|
||||
|
||||
The terminal output uses English-only labels and omits the stored address and
|
||||
reason text. When there are no qualifying events for any bill it prints an `ALL
|
||||
CLEAR` screen and exits successfully.
|
||||
|
||||
Schedule entries use one compact row each. When every outage cannot fit below
|
||||
the countdown, the dashboard keeps the nearest entries visible and reports how
|
||||
many additional outages were omitted. Resizing the terminal reveals more rows.
|
||||
|
||||
Colors are enabled only when output is attached to a terminal. Set `NO_COLOR` to
|
||||
disable them explicitly, or set `COLUMNS` to control the rendered width:
|
||||
disable them explicitly, or set `COLUMNS` and `LINES` to control the rendered
|
||||
size:
|
||||
|
||||
```sh
|
||||
NO_COLOR=1 COLUMNS=80 ./main.sh
|
||||
NO_COLOR=1 COLUMNS=80 LINES=24 ./main.sh
|
||||
```
|
||||
|
||||
When `pyfiglet` or `figlet` provides the `univers` font, the countdown uses it
|
||||
|
||||
160
main.sh
160
main.sh
@@ -68,12 +68,13 @@ 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
|
||||
-b, --bill-id BILL_ID Show the countdown and schedule for this bill only
|
||||
-h, --help Show this help
|
||||
|
||||
Environment:
|
||||
NO_COLOR Disable ANSI colors when set
|
||||
COLUMNS Override the detected output width
|
||||
LINES Override the detected output height
|
||||
|
||||
Required commands: Bash 4.3+, curl, jq, and GNU date
|
||||
EOF
|
||||
@@ -334,6 +335,9 @@ build_schedule() {
|
||||
|
||||
while IFS= read -r record; do
|
||||
record_fields "$record" fields
|
||||
if [[ -n "$countdown_bill_id" && "${fields[0]}" != "$countdown_bill_id" ]]; then
|
||||
continue
|
||||
fi
|
||||
if [[ "${fields[2]}" != "active" ]]; then
|
||||
continue
|
||||
fi
|
||||
@@ -412,6 +416,38 @@ terminal_columns() {
|
||||
printf '%s\n' "$columns"
|
||||
}
|
||||
|
||||
terminal_lines() {
|
||||
local lines="${LINES:-}"
|
||||
local terminal_size="" detected_lines=""
|
||||
|
||||
if [[ -z "$lines" ]] && command -v stty >/dev/null 2>&1; then
|
||||
if [[ -r /dev/tty ]]; then
|
||||
terminal_size="$(stty size 2>/dev/null </dev/tty || true)"
|
||||
fi
|
||||
if [[ -z "$terminal_size" && -t 2 ]]; then
|
||||
terminal_size="$(stty size <&2 2>/dev/null || true)"
|
||||
fi
|
||||
if [[ -z "$terminal_size" && -t 0 ]]; then
|
||||
terminal_size="$(stty size <&0 2>/dev/null || true)"
|
||||
fi
|
||||
read -r detected_lines _ <<<"$terminal_size"
|
||||
if [[ "$detected_lines" =~ ^[0-9]+$ ]]; then
|
||||
lines="$detected_lines"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "$lines" ]] && command -v tput >/dev/null 2>&1; then
|
||||
lines="$(tput lines 2>/dev/null || true)"
|
||||
fi
|
||||
|
||||
if [[ ! "$lines" =~ ^[0-9]+$ ]]; then
|
||||
lines=24
|
||||
elif ((lines < 10)); then
|
||||
lines=10
|
||||
fi
|
||||
printf '%s\n' "$lines"
|
||||
}
|
||||
|
||||
color_setup() {
|
||||
RESET=""
|
||||
BOLD=""
|
||||
@@ -711,10 +747,29 @@ human_duration() {
|
||||
printf '%s\n' "$output"
|
||||
}
|
||||
|
||||
compact_reference() {
|
||||
local bill_id="$1"
|
||||
local outage_number="$2"
|
||||
local width="$3"
|
||||
local reference="$bill_id/$outage_number"
|
||||
local bill_width
|
||||
|
||||
if ((${#reference} <= width)); then
|
||||
printf '%s' "$reference"
|
||||
elif ((width > ${#outage_number} + 2)); then
|
||||
bill_width=$((width - ${#outage_number} - 2))
|
||||
printf '~%s/%s' "${bill_id: -bill_width}" "$outage_number"
|
||||
elif ((width > 1)); then
|
||||
printf '%s~' "${reference:0:width-1}"
|
||||
else
|
||||
printf '~'
|
||||
fi
|
||||
}
|
||||
|
||||
render_event() {
|
||||
local line="$1"
|
||||
local now_epoch="$2"
|
||||
local ignored phase start_epoch stop_epoch record type_label timing
|
||||
local columns="$2"
|
||||
local ignored phase start_epoch stop_epoch record type_label reference reference_width
|
||||
local -a fields
|
||||
|
||||
IFS=$'\t' read -r ignored phase start_epoch stop_epoch record <<<"$line"
|
||||
@@ -722,18 +777,24 @@ render_event() {
|
||||
type_label="EMERGENCY"
|
||||
[[ "${fields[8]}" == "true" ]] && type_label="PLANNED"
|
||||
|
||||
if [[ "$phase" == "ONGOING" ]]; then
|
||||
timing="ends in $(human_duration "$((stop_epoch - now_epoch))")"
|
||||
if ((columns >= 60)); then
|
||||
reference_width=$((columns - 47))
|
||||
reference="$(compact_reference "${fields[0]}" "${fields[1]}" "$reference_width")"
|
||||
printf '%s%-9s%s %s%-9s%s %-10s %-11s %s\n' \
|
||||
"$([[ "$phase" == "ONGOING" ]] && printf '%s' "$RED" || printf '%s' "$CYAN")" \
|
||||
"$phase" "$RESET" \
|
||||
"$([[ "$type_label" == "PLANNED" ]] && printf '%s' "$GREEN" || printf '%s' "$YELLOW")" \
|
||||
"$type_label" "$RESET" "${fields[3]}" "${fields[4]}-${fields[5]}" "$reference"
|
||||
else
|
||||
timing="starts in $(human_duration "$((start_epoch - now_epoch))")"
|
||||
reference_width=$((columns - 34))
|
||||
((reference_width < 1)) && reference_width=1
|
||||
reference="$(compact_reference "${fields[0]}" "${fields[1]}" "$reference_width")"
|
||||
printf '%s%-4s%s %-5s %-10s %-11s %s\n' \
|
||||
"$([[ "$phase" == "ONGOING" ]] && printf '%s' "$RED" || printf '%s' "$CYAN")" \
|
||||
"$([[ "$phase" == "ONGOING" ]] && printf 'ON' || printf 'UP')" "$RESET" \
|
||||
"$([[ "$type_label" == "PLANNED" ]] && printf 'PLAN' || printf 'EMERG')" \
|
||||
"${fields[3]}" "${fields[4]}-${fields[5]}" "$reference"
|
||||
fi
|
||||
|
||||
printf '%s%s%-10s%s %s%-9s%s %s\n' \
|
||||
"$BOLD" "$([[ "$phase" == "ONGOING" ]] && printf '%s' "$RED" || printf '%s' "$CYAN")" \
|
||||
"$phase" "$RESET" "$([[ "$type_label" == "PLANNED" ]] && printf '%s' "$GREEN" || printf '%s' "$YELLOW")" \
|
||||
"$type_label" "$RESET" "$timing"
|
||||
printf ' Date %s %s - %s\n' "${fields[3]}" "${fields[4]}" "${fields[5]}"
|
||||
printf ' Reference bill %s / outage %s\n' "${fields[0]}" "${fields[1]}"
|
||||
}
|
||||
|
||||
render_empty() {
|
||||
@@ -777,7 +838,7 @@ draw_frame() {
|
||||
|
||||
if ((TERMINAL_ACTIVE)); then
|
||||
cleared_frame="${frame//$'\n'/$'\033[K\n'}"
|
||||
printf '\033[H%s\033[K\n\033[J' "$cleared_frame"
|
||||
printf '\033[H%s\033[K\033[J' "$cleared_frame"
|
||||
else
|
||||
printf '%s\n' "$frame"
|
||||
fi
|
||||
@@ -803,13 +864,17 @@ 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
|
||||
local lines="${4:-}"
|
||||
local rule phase start_epoch stop_epoch record remaining banner banner_art
|
||||
local banner_rows fixed_rows available_rows visible_count hidden_count index show_bill_heading=0
|
||||
|
||||
color_setup
|
||||
if [[ -z "$columns" ]]; then
|
||||
columns="$(terminal_columns)"
|
||||
fi
|
||||
if [[ -z "$lines" ]]; then
|
||||
lines="$(terminal_lines)"
|
||||
fi
|
||||
|
||||
if ((${#SCHEDULE[@]} == 0)); then
|
||||
render_empty "$columns"
|
||||
@@ -831,29 +896,69 @@ render_dashboard() {
|
||||
remaining=$((start_epoch - now_epoch))
|
||||
fi
|
||||
banner="$(banner_duration "$remaining")"
|
||||
if [[ -n "$countdown_bill_id" ]]; then
|
||||
banner_art="$(print_large_countdown "$banner" "$columns")"
|
||||
banner_rows="$(awk 'END { print NR }' <<<"$banner_art")"
|
||||
[[ -n "$countdown_bill_id" ]] && show_bill_heading=1
|
||||
fixed_rows=$((8 + banner_rows))
|
||||
((show_bill_heading)) && fixed_rows=$((fixed_rows + 2))
|
||||
if ((fixed_rows + 2 > lines)); then
|
||||
banner_art="$banner"
|
||||
banner_rows=1
|
||||
fixed_rows=$((8 + banner_rows))
|
||||
((show_bill_heading)) && fixed_rows=$((fixed_rows + 2))
|
||||
if ((fixed_rows + 1 > lines)); then
|
||||
show_bill_heading=0
|
||||
fi
|
||||
fi
|
||||
if ((show_bill_heading)); 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\n' "$banner_art"
|
||||
printf '%s' "$RESET"
|
||||
fi
|
||||
|
||||
printf '\n%s%s%s\n' "$CYAN" "$rule" "$RESET"
|
||||
printf '\n%sOUTAGE SCHEDULE (%d)%s\n\n' "$BOLD" "${#SCHEDULE[@]}" "$RESET"
|
||||
printf '\n%sOUTAGE SCHEDULE (%d)%s\n' "$BOLD" "${#SCHEDULE[@]}" "$RESET"
|
||||
if ((columns >= 60)); then
|
||||
printf '%s%-9s %-9s %-10s %-11s %s%s\n' \
|
||||
"$BOLD" "STATUS" "TYPE" "DATE" "TIME" "BILL/OUTAGE" "$RESET"
|
||||
else
|
||||
printf '%s%-4s %-5s %-10s %-11s %s%s\n' \
|
||||
"$BOLD" "ST" "TYPE" "DATE" "TIME" "REF" "$RESET"
|
||||
fi
|
||||
printf '%s%s%s\n' "$DIM" "$(repeat_character '-' "$columns")" "$RESET"
|
||||
|
||||
for schedule_line in "${SCHEDULE[@]}"; do
|
||||
IFS=$'\t' read -r ignored phase start_epoch stop_epoch record <<<"$schedule_line"
|
||||
render_event "$schedule_line" "$now_epoch"
|
||||
printf '%s\n' "$DIM$(repeat_character '-' "$columns")$RESET"
|
||||
fixed_rows=$((8 + ${banner_rows:-1}))
|
||||
((show_bill_heading)) && fixed_rows=$((fixed_rows + 2))
|
||||
available_rows=$((lines - fixed_rows - 1))
|
||||
((available_rows < 0)) && available_rows=0
|
||||
visible_count="${#SCHEDULE[@]}"
|
||||
hidden_count=0
|
||||
if ((visible_count > available_rows)); then
|
||||
if ((available_rows > 0)); then
|
||||
visible_count=$((available_rows - 1))
|
||||
else
|
||||
visible_count=0
|
||||
fi
|
||||
hidden_count=$((${#SCHEDULE[@]} - visible_count))
|
||||
fi
|
||||
|
||||
for ((index = 0; index < visible_count; index++)); do
|
||||
render_event "${SCHEDULE[index]}" "$columns"
|
||||
done
|
||||
if ((hidden_count && available_rows > 0)); then
|
||||
printf '%s... %d more outage%s%s\n' \
|
||||
"$DIM" "$hidden_count" "$([[ "$hidden_count" == 1 ]] || printf 's')" "$RESET"
|
||||
fi
|
||||
printf '%s%s%s\n' "$CYAN" "$rule" "$RESET"
|
||||
}
|
||||
|
||||
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 countdown_bill_id=""
|
||||
local display_columns display_lines render_lines countdown_bill_id=""
|
||||
|
||||
while (($#)); do
|
||||
case "$1" in
|
||||
@@ -959,9 +1064,14 @@ main() {
|
||||
fi
|
||||
|
||||
display_columns="$(terminal_columns)"
|
||||
display_lines="$(terminal_lines)"
|
||||
render_lines="$display_lines"
|
||||
if [[ -n "$REFRESH_ERROR" ]] && ((render_lines > 12)); then
|
||||
render_lines=$((render_lines - 2))
|
||||
fi
|
||||
frame="$(
|
||||
if ((state_ready)); then
|
||||
render_dashboard "$now_epoch" "$display_columns" "$countdown_bill_id"
|
||||
render_dashboard "$now_epoch" "$display_columns" "$countdown_bill_id" "$render_lines"
|
||||
else
|
||||
render_unavailable "$display_columns"
|
||||
fi
|
||||
|
||||
@@ -114,14 +114,11 @@ class EitaaClient:
|
||||
try:
|
||||
body = response.json()
|
||||
except requests.JSONDecodeError as error:
|
||||
raise ExternalServiceError(
|
||||
f"Eitaa returned invalid JSON for chat {chat_id}"
|
||||
) from error
|
||||
raise ExternalServiceError(f"Eitaa returned invalid JSON for chat {chat_id}") from error
|
||||
if not isinstance(body, dict) or body.get("ok") is not True:
|
||||
description = body.get("description") if isinstance(body, dict) else None
|
||||
raise ExternalServiceError(
|
||||
f"Eitaa rejected the message for chat {chat_id}: "
|
||||
f"{description or 'unknown error'}"
|
||||
f"Eitaa rejected the message for chat {chat_id}: {description or 'unknown error'}"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -88,16 +88,13 @@ def _bill_chat_map(raw_routes: str) -> dict[str, tuple[str, ...]]:
|
||||
malformed.append(route)
|
||||
continue
|
||||
if not bill_id.isdecimal():
|
||||
raise ConfigurationError(
|
||||
f"BILL_CHAT_MAP bill IDs must be decimal: {bill_id}"
|
||||
)
|
||||
raise ConfigurationError(f"BILL_CHAT_MAP bill IDs must be decimal: {bill_id}")
|
||||
chat_ids = routes.setdefault(bill_id, [])
|
||||
if chat_id not in chat_ids:
|
||||
chat_ids.append(chat_id)
|
||||
if malformed:
|
||||
raise ConfigurationError(
|
||||
"BILL_CHAT_MAP entries must use BILL_ID:CHAT_ID format: "
|
||||
+ ", ".join(malformed)
|
||||
"BILL_CHAT_MAP entries must use BILL_ID:CHAT_ID format: " + ", ".join(malformed)
|
||||
)
|
||||
if not routes:
|
||||
raise ConfigurationError("BILL_CHAT_MAP must contain at least one route")
|
||||
|
||||
@@ -91,9 +91,7 @@ class OutageService:
|
||||
|
||||
previous = Outage.from_dict(record["snapshot"])
|
||||
if record["status"] == "cancelled":
|
||||
if self._notify(
|
||||
chat_ids, reactivated_outage_message(bill_id, outage, now), result
|
||||
):
|
||||
if self._notify(chat_ids, reactivated_outage_message(bill_id, outage, now), result):
|
||||
records[event_id] = _record(outage, now)
|
||||
result.state_changed = True
|
||||
continue
|
||||
@@ -126,9 +124,7 @@ class OutageService:
|
||||
if record["missing_checks"] == 0:
|
||||
record["missing_checks"] = 1
|
||||
result.state_changed = True
|
||||
elif self._notify(
|
||||
chat_ids, cancelled_outage_message(bill_id, previous, now), result
|
||||
):
|
||||
elif self._notify(chat_ids, cancelled_outage_message(bill_id, previous, now), result):
|
||||
record["status"] = "cancelled"
|
||||
record["missing_checks"] = 2
|
||||
record["notified_at"] = now.isoformat()
|
||||
|
||||
@@ -52,8 +52,8 @@
|
||||
},
|
||||
"159719": {
|
||||
"last_seen_at": "2026-07-21T18:30:24.266070+03:30",
|
||||
"missing_checks": 0,
|
||||
"notified_at": "2026-07-21T18:30:24.266070+03:30",
|
||||
"missing_checks": 2,
|
||||
"notified_at": "2026-07-22T17:30:23.869053+03:30",
|
||||
"notified_hash": "40590e173639e2c8ecdb3682c092adfe5a0a9897535df835e4e1ce44cc6afa01",
|
||||
"snapshot": {
|
||||
"address": "باغ قلعه 8متری سوم ج ترانسفورماتور",
|
||||
@@ -64,11 +64,191 @@
|
||||
"start_time": "18:00",
|
||||
"stop_time": "20:00"
|
||||
},
|
||||
"status": "cancelled"
|
||||
},
|
||||
"159977": {
|
||||
"last_seen_at": "2026-07-22T16:30:24.078355+03:30",
|
||||
"missing_checks": 0,
|
||||
"notified_at": "2026-07-22T16:30:24.078355+03:30",
|
||||
"notified_hash": "9e082e2d029e51980d1178903a346f6d7a0ff9c5036bbc62fd9c256912865e40",
|
||||
"snapshot": {
|
||||
"address": "باغ قلعه 8متری سوم ج ترانسفورماتور",
|
||||
"is_planned": false,
|
||||
"outage_date": "1405/04/31",
|
||||
"outage_number": "159977",
|
||||
"reason": "مدیریت اضطراری بار",
|
||||
"start_time": "18:15",
|
||||
"stop_time": "20:00"
|
||||
},
|
||||
"status": "active"
|
||||
},
|
||||
"160375": {
|
||||
"last_seen_at": "2026-07-22T16:30:24.078355+03:30",
|
||||
"missing_checks": 0,
|
||||
"notified_at": "2026-07-22T16:30:24.078355+03:30",
|
||||
"notified_hash": "1be423abd3e35086d9956597041568a284a8f21ef9a0c67d17cd0e2d83f73cf2",
|
||||
"snapshot": {
|
||||
"address": "باغ قلعه 8متری سوم ج ترانسفورماتور",
|
||||
"is_planned": false,
|
||||
"outage_date": "1405/05/03",
|
||||
"outage_number": "160375",
|
||||
"reason": "مدیریت اضطراری بار",
|
||||
"start_time": "20:00",
|
||||
"stop_time": "22:00"
|
||||
},
|
||||
"status": "active"
|
||||
},
|
||||
"160621": {
|
||||
"last_seen_at": "2026-07-26T16:30:57.897178+03:30",
|
||||
"missing_checks": 0,
|
||||
"notified_at": "2026-07-26T16:30:57.897178+03:30",
|
||||
"notified_hash": "a1089904ac9932cd248aba8187fa44adabebf926be04e1ae382ef3f93d2a4f4b",
|
||||
"snapshot": {
|
||||
"address": "باغ قلعه 8متری سوم ج ترانسفورماتور",
|
||||
"is_planned": false,
|
||||
"outage_date": "1405/05/05",
|
||||
"outage_number": "160621",
|
||||
"reason": "مدیریت اضطراری بار",
|
||||
"start_time": "12:00",
|
||||
"stop_time": "14:00"
|
||||
},
|
||||
"status": "active"
|
||||
},
|
||||
"160869": {
|
||||
"last_seen_at": "2026-07-27T15:30:59.279795+03:30",
|
||||
"missing_checks": 0,
|
||||
"notified_at": "2026-07-27T15:30:59.279795+03:30",
|
||||
"notified_hash": "b78a1c2bde240ab2a99bf92e2f51678394c40557d2cf6feb86290cf4b416bf81",
|
||||
"snapshot": {
|
||||
"address": "باغ قلعه 8متری سوم ج ترانسفورماتور",
|
||||
"is_planned": false,
|
||||
"outage_date": "1405/05/06",
|
||||
"outage_number": "160869",
|
||||
"reason": "مدیریت اضطراری بار",
|
||||
"start_time": "16:00",
|
||||
"stop_time": "18:00"
|
||||
},
|
||||
"status": "active"
|
||||
},
|
||||
"161111": {
|
||||
"last_seen_at": "2026-07-29T17:31:00.393243+03:30",
|
||||
"missing_checks": 0,
|
||||
"notified_at": "2026-07-29T17:31:00.393243+03:30",
|
||||
"notified_hash": "4a19b3095a786c5ce1da5cb41e820ac91bdf59f124c1aad0bc76edc8cfff9b74",
|
||||
"snapshot": {
|
||||
"address": "باغ قلعه 8متری سوم ج ترانسفورماتور",
|
||||
"is_planned": false,
|
||||
"outage_date": "1405/05/08",
|
||||
"outage_number": "161111",
|
||||
"reason": "مدیریت اضطراری بار",
|
||||
"start_time": "10:00",
|
||||
"stop_time": "12:00"
|
||||
},
|
||||
"status": "active"
|
||||
},
|
||||
"161389": {
|
||||
"last_seen_at": "2026-07-29T19:30:59.469939+03:30",
|
||||
"missing_checks": 0,
|
||||
"notified_at": "2026-07-29T19:30:59.469939+03:30",
|
||||
"notified_hash": "698ad7a1d28d9f1fd42a350926da771a11abd9d5c14d63dbf156f8ca789b80da",
|
||||
"snapshot": {
|
||||
"address": "باغ قلعه 8متری سوم ج ترانسفورماتور",
|
||||
"is_planned": false,
|
||||
"outage_date": "1405/05/10",
|
||||
"outage_number": "161389",
|
||||
"reason": "مدیریت اضطراری بار",
|
||||
"start_time": "20:00",
|
||||
"stop_time": "22:00"
|
||||
},
|
||||
"status": "active"
|
||||
}
|
||||
}
|
||||
},
|
||||
"7359439104426": {
|
||||
"events": {
|
||||
"160167": {
|
||||
"last_seen_at": "2026-07-22T16:30:24.078355+03:30",
|
||||
"missing_checks": 0,
|
||||
"notified_at": "2026-07-22T16:30:24.078355+03:30",
|
||||
"notified_hash": "b156c680bc077693453f33be3ea11bead797f4793503d151434501b38a552d2d",
|
||||
"snapshot": {
|
||||
"address": "بلوار 15 خرداد ک 27 بن بست گلستان پ 38",
|
||||
"is_planned": false,
|
||||
"outage_date": "1405/05/01",
|
||||
"outage_number": "160167",
|
||||
"reason": "مدیریت اضطراری بار",
|
||||
"start_time": "16:00",
|
||||
"stop_time": "18:00"
|
||||
},
|
||||
"status": "active"
|
||||
},
|
||||
"160499": {
|
||||
"last_seen_at": "2026-07-25T16:30:57.756664+03:30",
|
||||
"missing_checks": 0,
|
||||
"notified_at": "2026-07-25T16:30:57.756664+03:30",
|
||||
"notified_hash": "ade1146f40d4ed0bbe8a182afe6f40f0f256a4accd84155fc4ae097e66309734",
|
||||
"snapshot": {
|
||||
"address": "بلوار 15 خرداد ک 27 بن بست گلستان پ 38",
|
||||
"is_planned": false,
|
||||
"outage_date": "1405/05/04",
|
||||
"outage_number": "160499",
|
||||
"reason": "مدیریت اضطراری بار",
|
||||
"start_time": "16:00",
|
||||
"stop_time": "18:00"
|
||||
},
|
||||
"status": "active"
|
||||
},
|
||||
"160727": {
|
||||
"last_seen_at": "2026-07-26T16:30:57.897178+03:30",
|
||||
"missing_checks": 0,
|
||||
"notified_at": "2026-07-26T16:30:57.897178+03:30",
|
||||
"notified_hash": "36501deedcf55f8a43dfc42b3ce265097a3a2202a20183f69ff6bc0aaf8f8b58",
|
||||
"snapshot": {
|
||||
"address": "بلوار 15 خرداد ک 27 بن بست گلستان پ 38",
|
||||
"is_planned": false,
|
||||
"outage_date": "1405/05/05",
|
||||
"outage_number": "160727",
|
||||
"reason": "مدیریت اضطراری بار",
|
||||
"start_time": "22:00",
|
||||
"stop_time": "00:00"
|
||||
},
|
||||
"status": "active"
|
||||
},
|
||||
"160785": {
|
||||
"last_seen_at": "2026-07-27T14:34:00.319029+03:30",
|
||||
"missing_checks": 0,
|
||||
"notified_at": "2026-07-27T14:34:00.319029+03:30",
|
||||
"notified_hash": "7dbebe0f429ee9591442b43bd91066da407e106f5863d57bfe7e385aaa4ae73e",
|
||||
"snapshot": {
|
||||
"address": "بلوار 15 خرداد ک 27 بن بست گلستان پ 38",
|
||||
"is_planned": false,
|
||||
"outage_date": "1405/05/06",
|
||||
"outage_number": "160785",
|
||||
"reason": "مدیریت اضطراری بار",
|
||||
"start_time": "10:00",
|
||||
"stop_time": "12:00"
|
||||
},
|
||||
"status": "active"
|
||||
},
|
||||
"161230": {
|
||||
"last_seen_at": "2026-07-29T17:31:00.393243+03:30",
|
||||
"missing_checks": 0,
|
||||
"notified_at": "2026-07-29T17:31:00.393243+03:30",
|
||||
"notified_hash": "5951e4742eb57490e879f9b7b924460929e750a39dffc9c8fbb00f08fb81a0ae",
|
||||
"snapshot": {
|
||||
"address": "بلوار 15 خرداد ک 27 بن بست گلستان پ 38",
|
||||
"is_planned": false,
|
||||
"outage_date": "1405/05/08",
|
||||
"outage_number": "161230",
|
||||
"reason": "مدیریت اضطراری بار",
|
||||
"start_time": "20:00",
|
||||
"stop_time": "22:00"
|
||||
},
|
||||
"status": "active"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema_version": 1,
|
||||
"updated_at": "2026-07-21T18:30:24.266070+03:30"
|
||||
"updated_at": "2026-07-29T19:30:59.469939+03:30"
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ def test_config_rejects_malformed_routes(
|
||||
with pytest.raises(ConfigurationError, match="BILL_ID:CHAT_ID"):
|
||||
Config.from_env()
|
||||
|
||||
|
||||
def test_legacy_single_chat_configuration_is_still_supported(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
|
||||
@@ -120,6 +120,8 @@ assert_equal "01 : 01 : 01" "$(banner_duration 3661)" \
|
||||
"countdowns should include hours, minutes, and seconds"
|
||||
assert_equal "49 : 30 : 00" "$(banner_duration 178200)" \
|
||||
"countdowns should use total hours beyond one day"
|
||||
assert_equal "17" "$(LINES=17 terminal_lines)" \
|
||||
"the terminal height override should be respected"
|
||||
FONT_OUTPUT="$(print_large_countdown "00 : 00 : 00" 150)"
|
||||
assert_contains "$FONT_OUTPUT" ',a8888a,' \
|
||||
"the countdown should consistently use the univers-style font"
|
||||
@@ -143,8 +145,8 @@ assert_contains "${SCHEDULE[2]}" '"outage_number":"5"' \
|
||||
"later events should remain in the schedule"
|
||||
|
||||
build_schedule "$FIXTURE" "$NOW_EPOCH" "222"
|
||||
assert_equal "3" "${#SCHEDULE[@]}" \
|
||||
"selecting a countdown bill should not filter the schedule"
|
||||
assert_equal "1" "${#SCHEDULE[@]}" \
|
||||
"selecting a bill should filter the schedule"
|
||||
assert_equal "UPCOMING" "${HERO%%$'\t'*}" \
|
||||
"the countdown should ignore another bill's ongoing outage"
|
||||
assert_contains "$HERO" '"bill_id":"222"' \
|
||||
@@ -152,17 +154,21 @@ assert_contains "$HERO" '"bill_id":"222"' \
|
||||
SELECTED_OUTPUT="$(NO_COLOR=1 COLUMNS=80 render_dashboard "$NOW_EPOCH" 80 "222")"
|
||||
assert_contains "$SELECTED_OUTPUT" "COUNTDOWN FOR BILL 222" \
|
||||
"the selected countdown bill should be labeled"
|
||||
assert_contains "$SELECTED_OUTPUT" "OUTAGE SCHEDULE (3)" \
|
||||
"the selected countdown bill should not hide other bills from the list"
|
||||
assert_contains "$SELECTED_OUTPUT" "OUTAGE SCHEDULE (1)" \
|
||||
"the schedule should contain only the selected bill"
|
||||
if [[ "$SELECTED_OUTPUT" == *"Bill 111"* ]]; then
|
||||
printf 'FAIL: selected bill output contains another bill\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
build_schedule "$FIXTURE" "$NOW_EPOCH" "333"
|
||||
assert_equal "" "$HERO" \
|
||||
"a bill without upcoming outages should not borrow another bill's countdown"
|
||||
assert_equal "0" "${#SCHEDULE[@]}" \
|
||||
"a bill without upcoming outages should have an empty schedule"
|
||||
NO_HERO_OUTPUT="$(NO_COLOR=1 COLUMNS=80 render_dashboard "$NOW_EPOCH" 80 "333")"
|
||||
assert_contains "$NO_HERO_OUTPUT" "NO CURRENT OR UPCOMING OUTAGE" \
|
||||
"a selected bill without outages should have a clear countdown status"
|
||||
assert_contains "$NO_HERO_OUTPUT" "OUTAGE SCHEDULE (3)" \
|
||||
"all bills should remain listed when the selected bill has no outage"
|
||||
assert_contains "$NO_HERO_OUTPUT" "ALL CLEAR" \
|
||||
"a selected bill without outages should render the all-clear screen"
|
||||
|
||||
build_schedule "$FIXTURE" "$NOW_EPOCH"
|
||||
OUTPUT="$(NO_COLOR=1 COLUMNS=80 render_dashboard "$NOW_EPOCH")"
|
||||
@@ -185,6 +191,17 @@ if [[ "$OUTPUT" == *$'\033['* ]]; then
|
||||
printf 'FAIL: NO_COLOR output contains ANSI escapes\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
SHORT_OUTPUT="$(NO_COLOR=1 COLUMNS=80 LINES=12 render_dashboard "$NOW_EPOCH" 80 "" 12)"
|
||||
SHORT_LINE_COUNT="$(awk 'END { print NR }' <<<"$SHORT_OUTPUT")"
|
||||
assert_equal "12" "$SHORT_LINE_COUNT" \
|
||||
"a dashboard with extra outages should fit the terminal height"
|
||||
assert_contains "$SHORT_OUTPUT" '01 : 00 : 00' \
|
||||
"the countdown should remain visible when schedule rows are limited"
|
||||
assert_contains "$SHORT_OUTPUT" "... 2 more outages" \
|
||||
"the dashboard should report schedule rows that do not fit"
|
||||
SHORT_BOTTOM_LINE="${SHORT_OUTPUT##*$'\n'}"
|
||||
assert_equal "$(repeat_character '=' 80)" "$SHORT_BOTTOM_LINE" \
|
||||
"the compact schedule should end with a full-width rule"
|
||||
WIDE_OUTPUT="$(NO_COLOR=1 COLUMNS=180 render_dashboard "$NOW_EPOCH")"
|
||||
WIDE_TOP_LINE="${WIDE_OUTPUT%%$'\n'*}"
|
||||
assert_equal "180" "${#WIDE_TOP_LINE}" \
|
||||
@@ -192,6 +209,8 @@ assert_equal "180" "${#WIDE_TOP_LINE}" \
|
||||
WIDE_BOTTOM_LINE="${WIDE_OUTPUT##*$'\n'}"
|
||||
assert_equal "180" "${#WIDE_BOTTOM_LINE}" \
|
||||
"the dashboard bottom divider should span the full terminal width"
|
||||
assert_equal "$(repeat_character '=' 180)" "$WIDE_BOTTOM_LINE" \
|
||||
"the dashboard should end with the same rule as the countdown section"
|
||||
|
||||
FUTURE_NOW="$(TZ=Asia/Tehran date -d '2026-07-18 15:00:00' +%s)"
|
||||
build_schedule "$FIXTURE" "$FUTURE_NOW"
|
||||
|
||||
Reference in New Issue
Block a user