diff --git a/AGENT_TODO.md b/AGENT_TODO.md index b10f2e8..a397c80 100644 --- a/AGENT_TODO.md +++ b/AGENT_TODO.md @@ -49,6 +49,10 @@ summary (newest last). ## Done (summary, newest last) +- 2026-08-06: Multi-platform alerting — `lib/notify.sh` routes via `NOTIFY_PLATFORM` + (`notify.env`, default telegram; sender contract for Matrix/Synapse later), + `system.env` shared config for health/backup, dynamic effective values in + `--help`, telegram `--markdown` alias. - 2026-08-06: Tier 1 — `pos system health` (dashboard + `--send`), `lib/notify.sh` (wired into backup + firewall), daily digest timer via postinstall. - 2026-08-06: Document Map index + Entertainment section in AGENT_Context (cf36780). diff --git a/DOC/AGENT_Context_Project.md b/DOC/AGENT_Context_Project.md index 010c0b4..cfe33c9 100644 --- a/DOC/AGENT_Context_Project.md +++ b/DOC/AGENT_Context_Project.md @@ -527,7 +527,7 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:` | `features/autostart.sh` | 14 | Boot-time feature (moved from `bin/`, flag-gated service) | | `bin/pos` | 213 | CLI dispatcher with smart arg matching + logging + category help | -| `bin/pos-communication-telegram` | 270 | Send Telegram messages/files/links/stickers via Bot API (send, test, config set) | +| `bin/pos-communication-telegram` | 274 | Send Telegram messages/files/links/stickers via Bot API (send, test, config set) | | `bin/pos-docker-compose` | 364 | Docker Compose service manager (ls/up/down/restart/logs/update/config) | | `bin/pos-docker-health` | 110 | One-glance container health dashboard (exits 1 if unhealthy) | | `bin/pos-docker-ps` | 128 | Enhanced container overview (health, IPs, ports, uptime) | @@ -544,9 +544,9 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:` | `bin/pos-network-ip` | 69 | Show interfaces, routes, public IP + location | | `bin/pos-network-scan` | 271 | Parallel ping sweep of CIDR | | `bin/pos-ssh-load-keys` | 31 | Load all SSH keys into the agent | -| `bin/pos-system-backup` | 121 | Encrypted (AES-256) folder snapshots (tar + gpg) | +| `bin/pos-system-backup` | 125 | Encrypted (AES-256) folder snapshots (tar + gpg) | | `bin/pos-system-firewall` | 291 | Interactive UFW management | -| `bin/pos-system-health` | 230 | Host health dashboard (disk, RAM, services, backup age, fail2ban, docker); exit 1 if any FAIL | +| `bin/pos-system-health` | 243 | Host health dashboard (disk, RAM, services, backup age, fail2ban, docker); exit 1 if any FAIL | | `bin/pos-usb-server` | 218 | USB Redirector server control (--ls, --share; prompts when args omitted) | | `completions/pos.bash` | 189 | Dynamic bash completion | diff --git a/DOC/DEV.md b/DOC/DEV.md index 0f25058..cc798be 100644 --- a/DOC/DEV.md +++ b/DOC/DEV.md @@ -148,7 +148,9 @@ PACKAGES=( Two kinds of config, don't mix them up: - **Machine defaults shipped by the installer:** place the file in `config/` and add copy logic to `postinstall.sh`. If it contains secrets, add to `.gitignore` and document in `DOC/`. -- **Runtime tool config set by the user:** `~/.config/linux_post_install/.env` with `chmod 600`. Load it with env-var precedence (flags > environment > file). Patterns: `pos-docker-compose` (`compose.env`) and `pos-communication-telegram` (`telegram.env`, token masked in `config` output). Never store tokens in the repo. +- **Runtime tool config set by the user:** `~/.config/linux_post_install/.env` with `chmod 600`. Load it with env-var precedence (flags > environment > file). Patterns: `pos-docker-compose` (`compose.env`), `pos-communication-telegram` (`telegram.env`, token masked in `config` output), and the shared ones below. Never store tokens in the repo. + - `system.env` — shared "system" settings loaded by `pos-system-*` tools via `load_system_env()` in `lib/common.sh` (currently `BACKUP_SERVICE_ROOTS`, `HEALTH_BACKUP_MAX_AGE_DAYS`). Env already exported wins over the file. + - `notify.env` — alerting platform selection (`NOTIFY_PLATFORM=telegram,matrix`), read by `lib/notify.sh`. ### 5. Add SSH keys (if needed) @@ -277,7 +279,7 @@ Place it in `apps//.sh`. It auto-appears in the picker — no re ### Alerting -To notify on events (Telegram), source the shared helper instead of calling the telegram tool directly: +To notify on events, source the shared helper instead of calling a platform tool directly: ```bash source "$(dirname "$0")/../lib/notify.sh" 2>/dev/null || source "$(dirname "$0")/notify.sh" @@ -285,7 +287,15 @@ notify_send "Backup completed" notify_send "**disk full**" --markdown ``` -`notify_send` is deliberately dependency-free (defines only itself, so it never clobbers a tool's own `log`/`warn`/`err`) and **silent-fails**: if Telegram is missing or not configured it warns and returns 0, never breaking the caller's flow or exit code. Source it opt-in in any tool that should alert; for failure alerts use `trap 'notify_send "..." ERR'`. +`notify_send` is deliberately dependency-free (defines only itself, so it never clobbers a tool's own `log`/`warn`/`err`) and **silent-fails**: if no platform is configured it warns and returns 0, never breaking the caller's flow or exit code. Source it opt-in in any tool that should alert; for failure alerts use `trap 'notify_send "..." ERR'`. + +**Multi-platform routing:** `notify_send` delivers to every platform listed in `NOTIFY_PLATFORM` (env or `~/.config/linux_post_install/notify.env`, default `telegram`, comma-separated to send to all). Adding a new platform (e.g. Matrix/Synapse) means creating a `bin/pos-communication-` tool that implements the **sender contract**: + +```bash +pos-communication- send [--markdown] # exit 0 on delivery +``` + +then listing it in `NOTIFY_PLATFORM`. `pos-communication-telegram` already follows this (`--markdown` is an alias for `--parse-mode markdown`). No changes to `lib/notify.sh` are needed for a new platform. ### Idempotency diff --git a/DOC/POS.md b/DOC/POS.md index 6ae12b4..53a2c56 100644 --- a/DOC/POS.md +++ b/DOC/POS.md @@ -153,10 +153,10 @@ The standalone `vbox` command still works and forwards to `pos docker vbox` (see |---------|------|---------|---------------| | `sudo pos system firewall` | `bin/pos-system-firewall` | Interactive UFW ("UFW POWER") menu: add/delete rules, status, enable/disable/reset, default policies | Must run as root. Every command is previewed and confirmed before execution; supports `--dry-run`; keeps a history of executed commands. Executed mutating changes are announced via `lib/notify.sh` | | `pos system backup ` | `bin/pos-system-backup` | Create a gpg-encrypted (AES-256) `tar.gz` snapshot of a folder and verify it | Prompts twice for a password (never stored). Uses `sudo tar`; needs `gnupg` (in `preinstall.sh` PACKAGES). Artifact `_.tar.gz.gpg` in the current directory, `chmod 600`. Success/failure are announced via `lib/notify.sh` | -| `pos system backup --service` | `bin/pos-system-backup` | Lists folders under `/srv` and `~/srv`, lets you pick one, then runs the same backup | Roots via `BACKUP_SERVICE_ROOTS` (space-separated, default `/srv $HOME/srv`) | -| `pos system health [--send] [--markdown]` | `bin/pos-system-health` | Host health dashboard: disk per mount, RAM/swap, failed systemd units, backup age, fail2ban, docker containers. Exits 1 if any check FAILs | `--send`/`--markdown` send the summary via Telegram (`lib/notify.sh`). Backup age threshold via `HEALTH_BACKUP_MAX_AGE_DAYS` (default 2); backup search roots via `BACKUP_SERVICE_ROOTS` | +| `pos system backup --service` | `bin/pos-system-backup` | Lists folders under `/srv` and `~/srv`, lets you pick one, then runs the same backup | Roots via `BACKUP_SERVICE_ROOTS` (space-separated, default `/srv $HOME/srv`) or `~/.config/linux_post_install/system.env` | +| `pos system health [--send] [--markdown]` | `bin/pos-system-health` | Host health dashboard: disk per mount, RAM/swap, failed systemd units, backup age, fail2ban, docker containers. Exits 1 if any check FAILs | `--send`/`--markdown` send the summary via `lib/notify.sh` to every platform in `NOTIFY_PLATFORM`. `HEALTH_BACKUP_MAX_AGE_DAYS` (default 2) and `BACKUP_SERVICE_ROOTS` come from `~/.config/linux_post_install/system.env`; `--help` shows the effective values. Platform list from `~/.config/linux_post_install/notify.env` | -`systemd/pos-health.service` + `systemd/pos-health.timer` run `pos system health --send --markdown` daily at 08:00 as the installing user. `postinstall.sh` enables the timer automatically once `~/.config/linux_post_install/telegram.env` exists — re-run postinstall after configuring Telegram to pick it up. +`systemd/pos-health.service` + `systemd/pos-health.timer` run `pos system health --send --markdown` daily at 08:00 as the installing user. `postinstall.sh` enables the timer automatically once `~/.config/linux_post_install/telegram.env` exists — re-run postinstall after configuring a notify platform to pick it up. The service also loads `system.env` + `notify.env` via `EnvironmentFile=`. ### ssh diff --git a/DOC/SYSTEMD.md b/DOC/SYSTEMD.md index b0749d6..a40564d 100644 --- a/DOC/SYSTEMD.md +++ b/DOC/SYSTEMD.md @@ -62,17 +62,19 @@ WantedBy=multi-user.target ### pos-health.service -**Purpose:** daily "health digest" — runs `pos system health --send --markdown` at 08:00 and sends the report to Telegram. +**Purpose:** daily "health digest" — runs `pos system health --send --markdown` at 08:00 and sends the report to the configured notify platform(s). ```ini [Unit] -Description=POS Health digest (daily report via Telegram) +Description=POS Health digest (daily report via configured notify platforms) After=network-online.target Wants=network-online.target [Service] Type=oneshot User=__POS_USER__ +EnvironmentFile=-%h/.config/linux_post_install/system.env +EnvironmentFile=-%h/.config/linux_post_install/notify.env ExecStart=/usr/local/bin/pos system health --send --markdown [Timer] @@ -82,7 +84,7 @@ Persistent=true The service is `Type=oneshot` and is driven **only** by its companion `pos-health.timer` (`WantedBy=timers.target`); the service itself is never enabled directly. -**Configuration:** `postinstall.sh` substitutes `__POS_USER__` with the installing user (`${SUDO_USER:-$USER}`) so the digest uses that user's real Telegram config. The timer is enabled only when `~/.config/linux_post_install/telegram.env` already exists — otherwise postinstall warns and skips; re-run postinstall after configuring Telegram (`pos communication telegram config set TELEGRAM_*`) to install it. +**Configuration:** `postinstall.sh` substitutes `__POS_USER__` with the installing user (`${SUDO_USER:-$USER}`) so the digest uses that user's real notify config. The `EnvironmentFile=` lines load `system.env` (health/backup settings) and `notify.env` (`NOTIFY_PLATFORM`). The timer is enabled only when a Telegram config (`~/.config/linux_post_install/telegram.env`) already exists — otherwise postinstall warns and skips; re-run postinstall after configuring a notify platform to install it. --- diff --git a/bin/pos-communication-telegram b/bin/pos-communication-telegram index 611c8ba..56da6c1 100755 --- a/bin/pos-communication-telegram +++ b/bin/pos-communication-telegram @@ -34,6 +34,7 @@ Options: --type Force a type: message|file|link|sticker|photo|video|audio|voice|animation --caption Caption for file/photo/video/audio/voice/animation --parse-mode Format mode: plain (default), markdown, html (message/link/caption) + --markdown Alias for --parse-mode markdown (uniform notify_send contract) --no-preview Disable the link's web page preview (message/link only) --token Override token for one send --chat-id Override chat id for one send @@ -143,6 +144,9 @@ cmd_send() { --caption) [ $# -ge 2 ] || err "--caption needs a value" caption="$2"; shift 2 ;; + --markdown) + # Alias for --parse-mode markdown (uniform notify_send contract) + parse_mode="markdown"; shift ;; --parse-mode) [ $# -ge 2 ] || err "--parse-mode needs a value" case "$2" in diff --git a/bin/pos-system-backup b/bin/pos-system-backup index d3fd949..d3ef07e 100755 --- a/bin/pos-system-backup +++ b/bin/pos-system-backup @@ -6,6 +6,9 @@ set -euo pipefail source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh" source "$(dirname "$0")/../lib/notify.sh" 2>/dev/null || source "$(dirname "$0")/notify.sh" +load_system_env +EFF_ROOTS="${BACKUP_SERVICE_ROOTS:-/srv $HOME/srv}" + trap 'notify_send "Backup FAILED: ${FOLDER:-unknown}"' ERR usage() { @@ -24,7 +27,8 @@ The final artifact _.tar.gz.gpg is written to the current directory. Environment: BACKUP_SERVICE_ROOTS Space-separated roots for --service - (default: /srv \$HOME/srv) + (effective: ${EFF_ROOTS}) + (loaded from ~/.config/linux_post_install/system.env unless exported) EOF exit 0 } diff --git a/bin/pos-system-health b/bin/pos-system-health index 2e8a1f7..4504069 100755 --- a/bin/pos-system-health +++ b/bin/pos-system-health @@ -5,18 +5,14 @@ set -euo pipefail source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh" source "$(dirname "$0")/../lib/notify.sh" 2>/dev/null || source "$(dirname "$0")/notify.sh" -SEND=0 -MARKDOWN=0 -for arg in "$@"; do - case "$arg" in - -h|--help) usage_placeholder=1 ;; - --send) SEND=1 ;; - --markdown) MARKDOWN=1 ;; - *) err "Unknown option '$arg' (see --help)" ;; - esac -done +load_system_env -if [ "${usage_placeholder:-0}" -eq 1 ]; then +# Effective dynamic values (environment > system.env > defaults) shown in --help +EFF_PLATFORMS="$(notify_platforms)" +EFF_MAX_AGE="${HEALTH_BACKUP_MAX_AGE_DAYS:-2}" +EFF_ROOTS="${BACKUP_SERVICE_ROOTS:-/srv $HOME/srv}" + +usage() { cat < tool that +# implements the sender contract: +# pos-communication- send [--markdown] +# Default: telegram +#NOTIFY_PLATFORM=telegram,matrix diff --git a/config/system.env b/config/system.env new file mode 100644 index 0000000..8276c95 --- /dev/null +++ b/config/system.env @@ -0,0 +1,12 @@ +# ~/.config/linux_post_install/system.env — shared "system" tool config +# +# Loaded by `pos system health` and `pos system backup` via load_system_env() +# with precedence: already-exported environment > this file > defaults. +# Commented lines are defaults — uncomment to override. + +# Roots scanned by `pos system backup --service` and by the backup-age check +# in `pos system health`. Default: /srv $HOME/srv +#BACKUP_SERVICE_ROOTS=/srv /home/you/srv + +# Max backup age in days before `pos system health` raises a WARN. Default: 2 +#HEALTH_BACKUP_MAX_AGE_DAYS=3 diff --git a/lib/common.sh b/lib/common.sh index 0dc149e..bbcdc33 100644 --- a/lib/common.sh +++ b/lib/common.sh @@ -117,5 +117,24 @@ confirm() { fi } +# ── system.env loader ────────────────────────────────────────── +# Shared "system" tool config (~/.config/linux_post_install/system.env). +# Fills only variables that are not already exported — an explicitly-set +# environment variable always wins (flags > environment > file). +load_system_env() { + local f="$HOME/.config/linux_post_install/system.env" k v + [ -f "$f" ] || return 0 + while IFS='=' read -r k v; do + [ -n "$k" ] || continue + case "$k" in + \#*) continue ;; + esac + v="${v%\"}"; v="${v#\"}"; v="${v%\'}"; v="${v#\'}" + if [ -z "${!k:-}" ]; then + export "$k"="$v" + fi + done < <(grep -E '^[A-Z_]+=' "$f" || true) +} + # ── Source guard ─────────────────────────────────────────────── return 0 2>/dev/null || true diff --git a/lib/notify.sh b/lib/notify.sh index 082e03d..68b666e 100644 --- a/lib/notify.sh +++ b/lib/notify.sh @@ -1,16 +1,39 @@ #!/usr/bin/env bash -# lib/notify.sh — optional alerting helper. Self-contained by design: -# defines ONLY notify_send() so it can be sourced by tools that define -# their own log/warn/err (e.g. pos-system-firewall) without clobbering. +# lib/notify.sh — optional MULTI-PLATFORM alerting helper. Self-contained by +# design: defines ONLY notify_send() + notify_platforms() (plus internal +# helpers) so it can be sourced by tools that define their own log/warn/err +# (e.g. pos-system-firewall) without clobbering. # # Usage (opt-in — source it, do NOT auto-load from common.sh): # source "$(dirname "$0")/../lib/notify.sh" 2>/dev/null || source "$(dirname "$0")/notify.sh" # notify_send "Backup completed: $ARCHIVE" -# notify_send "⚠ disk full" --markdown +# notify_send "**disk full**" --markdown # -# Delegates to `pos communication telegram send`; silent-fail if the -# telegram sender is missing or not configured (warns, never breaks the -# caller and never changes its exit code). +# Platform routing — ~/.config/linux_post_install/notify.env: +# NOTIFY_PLATFORM=telegram,matrix +# Comma-separated = send to every listed platform (default: telegram). +# +# Sender contract — each platform is a bin/pos-communication- tool +# that MUST implement: +# pos-communication- send [--markdown] +# (exit 0 on delivery; non-zero on failure) +# To add a platform (e.g. Matrix/Synapse), add `bin/pos-communication-matrix` +# implementing that interface and put `matrix` in NOTIFY_PLATFORM. +# +# Silent-fails per platform: a missing sender or a failed send only warns and +# never changes the caller's exit code. + +CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/linux_post_install" + +# Effective platform list (env > notify.env > "telegram"). +notify_platforms() { + local p="${NOTIFY_PLATFORM:-}" + if [ -z "$p" ] && [ -f "$CONFIG_DIR/notify.env" ]; then + p="$(grep -E '^NOTIFY_PLATFORM=' "$CONFIG_DIR/notify.env" | tail -1 | cut -d= -f2-)" + p="${p%\"}"; p="${p#\"}"; p="${p%\'}"; p="${p#\'}" + fi + printf '%s' "${p:-telegram}" +} notify_send() { local msg="" markdown=0 @@ -26,22 +49,29 @@ notify_send() { return 0 fi - local tg - tg="$(command -v pos-communication-telegram 2>/dev/null)" || \ - tg="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/../bin/pos-communication-telegram" + local -a plist + IFS=',' read -r -a plist <<< "$(notify_platforms)" - if [ ! -x "$tg" ]; then - warn "notify_send: pos-communication-telegram not found, notification skipped" 2>/dev/null || true - return 0 - fi + local p sender + for p in "${plist[@]}"; do + p="${p// /}" + [ -n "$p" ] || continue + sender="$(command -v "pos-communication-${p}" 2>/dev/null)" || \ + sender="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/../bin/pos-communication-${p}" - if [ "$markdown" -eq 1 ]; then - "$tg" send "$msg" --parse-mode markdown >/dev/null 2>&1 || { - warn "notify_send: telegram send failed, notification skipped" 2>/dev/null || true - } - else - "$tg" send "$msg" >/dev/null 2>&1 || { - warn "notify_send: telegram send failed, notification skipped" 2>/dev/null || true - } - fi + if [ ! -x "$sender" ]; then + warn "notify_send: pos-communication-${p} not found, notification skipped" 2>/dev/null || true + continue + fi + + if [ "$markdown" -eq 1 ]; then + "$sender" send "$msg" --markdown >/dev/null 2>&1 || { + warn "notify_send: ${p} send failed, notification skipped" 2>/dev/null || true + } + else + "$sender" send "$msg" >/dev/null 2>&1 || { + warn "notify_send: ${p} send failed, notification skipped" 2>/dev/null || true + } + fi + done } diff --git a/postinstall.sh b/postinstall.sh index 3bd9de4..1069711 100755 --- a/postinstall.sh +++ b/postinstall.sh @@ -34,6 +34,21 @@ else warn "config/entertainment.env not found, skipping" fi +# ── system + notify config templates ─────────────────────────── +# Copied only if the user has not already created their own (no clobber). +mkdir -p "$ENT_DIR" +for tpl in system.env notify.env; do + if [ -f "config/$tpl" ]; then + if [ -f "$ENT_DIR/$tpl" ]; then + log "$tpl already exists, keeping it" + else + cp "config/$tpl" "$ENT_DIR/$tpl" + chmod 600 "$ENT_DIR/$tpl" + log "Installed $tpl — edit $ENT_DIR/$tpl" + fi + fi +done + # ── Ensure all bin dirs are in PATH ──────────────────────────── PATH_LINE='export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$HOME/.local/bin:$PATH"' BASHRC="$HOME/.bashrc" diff --git a/systemd/pos-health.service b/systemd/pos-health.service index 3121270..570a300 100644 --- a/systemd/pos-health.service +++ b/systemd/pos-health.service @@ -1,9 +1,11 @@ [Unit] -Description=POS Health digest (daily report via Telegram) +Description=POS Health digest (daily report via configured notify platforms) After=network-online.target Wants=network-online.target [Service] Type=oneshot User=__POS_USER__ +EnvironmentFile=-%h/.config/linux_post_install/system.env +EnvironmentFile=-%h/.config/linux_post_install/notify.env ExecStart=/usr/local/bin/pos system health --send --markdown