mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-13 01:50:59 +01:00
Some checks failed
dependency freshness / dependencies on newest stable (push) Has been cancelled
file length / file-length (push) Has been cancelled
md-naming / markdown naming convention (push) Has been cancelled
omniroute_offload pre-commit / pre-commit (push) Has been cancelled
palette drift / palette (push) Has been cancelled
repo-contract / repo contract (push) Has been cancelled
shared gate tests / gate unit tests (push) Has been cancelled
omniroute-gateway start|health|update|backup|combo keeps the local OmniRoute gateway up. systemd --user timers: health every 10 min (real free-best chat; restart <=1/30 min, reactivate providers whose cooldown ended, notify once per outage), update weekly Sun 12:00 (backup, newest stable -web tag, rollback of image + volume on a failed health check). Failure modes: health/update fail loudly (exit 1 + desktop + Signal); all providers cooling = degraded (exit 0, one notice/day); a missing signal-bot .env logs and exits non-zero on alert paths; health skips while update holds the lock. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WzxdWZSdJmNjGUvLnHJ4WG
74 lines
2.5 KiB
Python
74 lines
2.5 KiB
Python
"""Tell kuhy: one replaceable desktop popup plus a Signal DM from the bot account."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import subprocess
|
|
import sys
|
|
from typing import TYPE_CHECKING
|
|
|
|
from omniroute_offload import _gw_api as api
|
|
|
|
if TYPE_CHECKING:
|
|
from pathlib import Path
|
|
|
|
from omniroute_offload._gw_config import Config
|
|
|
|
|
|
class NotifyError(RuntimeError):
|
|
"""A channel could not be used."""
|
|
|
|
|
|
def _read_env(path: Path) -> dict[str, str]:
|
|
pairs = (
|
|
line.split("=", 1)
|
|
for line in path.read_text().splitlines()
|
|
if "=" in line and not line.lstrip().startswith("#")
|
|
)
|
|
return {k.strip(): v.strip() for k, v in pairs}
|
|
|
|
|
|
def signal(cfg: Config, text: str) -> None:
|
|
"""DM the first TRUSTED_INVITERS number from the signal-bot account."""
|
|
try:
|
|
env = _read_env(cfg.signal_env)
|
|
owner = env["TRUSTED_INVITERS"].split(",")[0].strip()
|
|
body = {"number": env["PHONE_NUMBER"], "recipients": [owner], "message": text}
|
|
api.call(f"{env['API_BASE_URL']}/v2/send", body, timeout=20)
|
|
except (OSError, KeyError, api.ApiError) as err:
|
|
msg = f"signal: {err!r}"
|
|
raise NotifyError(msg) from err
|
|
|
|
|
|
def desktop(cfg: Config, text: str, urgency: str) -> None:
|
|
"""notify-send, replacing this instance's previous popup instead of stacking."""
|
|
id_file = cfg.state_dir / "notify_id"
|
|
previous = id_file.read_text().strip() if id_file.exists() else "0"
|
|
proc = subprocess.run(
|
|
[
|
|
"notify-send", "--app-name=omniroute", "--print-id",
|
|
f"--replace-id={previous}", f"--urgency={urgency}",
|
|
f"OmniRoute ({cfg.name})", text,
|
|
],
|
|
capture_output=True, text=True, check=False, timeout=10,
|
|
) # fmt: skip
|
|
if proc.returncode:
|
|
msg = f"notify-send: {proc.stderr.strip()}"
|
|
raise NotifyError(msg)
|
|
id_file.write_text(proc.stdout.strip())
|
|
|
|
|
|
def notify(cfg: Config, text: str, *, urgency: str = "normal") -> bool:
|
|
"""Send on both channels, log each failure; True only when Signal got it."""
|
|
print(f"notify: {text}", file=sys.stderr)
|
|
cfg.state_dir.mkdir(parents=True, exist_ok=True)
|
|
try:
|
|
desktop(cfg, text, urgency)
|
|
except (NotifyError, OSError, subprocess.SubprocessError) as err:
|
|
print(f"desktop notify failed: {err}", file=sys.stderr)
|
|
try:
|
|
signal(cfg, f"OmniRoute ({cfg.name}): {text}")
|
|
except NotifyError as err:
|
|
print(f"signal notify failed: {err}", file=sys.stderr)
|
|
return False
|
|
return True
|