From 7dff27206b4f5aea009715e2075edb1b98edbe76 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 7 Aug 2026 14:15:33 -0400 Subject: [PATCH] fix: health --send is notification-only; listener @quiet prefix kills double reply --- AGENT_TODO.md | 4 ++++ DOC/AGENT_Context_Project.md | 4 ++-- DOC/POS.md | 4 ++-- DOC/howto/communication.md | 6 ++++- bin/pos-communication-telegram-listener | 29 +++++++++++++++++++++++-- bin/pos-system-health | 13 ++++++++++- 6 files changed, 52 insertions(+), 8 deletions(-) diff --git a/AGENT_TODO.md b/AGENT_TODO.md index 14e045c..1cf7ae5 100644 --- a/AGENT_TODO.md +++ b/AGENT_TODO.md @@ -14,6 +14,10 @@ summary (newest last). - When a task is completed: move it from Now/Next into **Done** (dated one-line) in the same commit that finishes the work. +## Done + +- **2026-08-07** — `pos system health --send` notification-only; listener `@quiet` prefix (run mapped command without replying, for commands that self-notify). `/status=@quiet pos system health --send` = exactly one digest. + ## Now - (none — Tier 1 shipped: `pos system health`, `lib/notify.sh`, digest timer) diff --git a/DOC/AGENT_Context_Project.md b/DOC/AGENT_Context_Project.md index fe7974c..d77dd58 100644 --- a/DOC/AGENT_Context_Project.md +++ b/DOC/AGENT_Context_Project.md @@ -533,7 +533,7 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:` | `features/autostart.sh` | 14 | Boot-time feature (moved from `bin/`, flag-gated service) | | `bin/pos` | 267 | CLI dispatcher with smart arg matching + logging + category help | -| `bin/pos-communication-telegram-listener` | 408 | Telegram bot listener: map /command → bash, run them on chat messages | +| `bin/pos-communication-telegram-listener` | 433 | Telegram bot listener: map /command → bash, run them on chat messages | | `bin/pos-communication-telegram` | 275 | Send Telegram messages/files/links/stickers via Bot API (send, test, config set) | | `bin/pos-docker-compose` | 365 | 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) | @@ -553,7 +553,7 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:` | `bin/pos-ssh-load-keys` | 31 | Load all SSH keys into the agent | | `bin/pos-system-backup` | 125 | Encrypted (AES-256) folder snapshots (tar + gpg) | | `bin/pos-system-firewall` | 291 | Interactive UFW management | -| `bin/pos-system-health` | 243 | Host health dashboard (disk, RAM, services, backup age, fail2ban, docker); exit 1 if any FAIL | +| `bin/pos-system-health` | 254 | Host health dashboard (disk, RAM, services, backup age, fail2ban, docker); exit 1 if any FAIL | | `bin/pos-system-nfs-client` | 138 | Mount NFS shares (ephemeral or persistent systemd mount units) | | `bin/pos-system-nfs-server` | 134 | Manage the NFS kernel server (status, share/unshare exports, enable/disable) | | `bin/pos-usb-server` | 218 | USB Redirector server control (--ls, --share; prompts when args omitted) | diff --git a/DOC/POS.md b/DOC/POS.md index 33a5468..9abfcb3 100644 --- a/DOC/POS.md +++ b/DOC/POS.md @@ -154,7 +154,7 @@ 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`) 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` | +| `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` are notification-only: they send the summary via `lib/notify.sh` to every platform in `NOTIFY_PLATFORM` and do NOT print the dashboard (so wrappers like the Telegram listener don't echo it back — pair with the listener's `@quiet` marker). `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` | | `pos system nfs-server ` | `bin/pos-system-nfs-server` | Manage the NFS kernel server: `status`, `share [client]`, `unshare `, `list`, `reload`, `enable`, `disable` | Requires `nfs-kernel-server` (added to `preinstall.sh` PACKAGES). Exports live in `/etc/exports`; `share` is idempotent (replaces any existing line for the path) and runs `exportfs -ra`. Default client `*(rw,sync,no_subtree_check)` — the tool warns you to restrict it; help prints Tailscale CGNAT (`100.64.0.0/10`), WireGuard (`10.10.0.0/24`) and LAN examples. Mutating commands announce via `lib/notify.sh` | | `pos system nfs-client ` | `bin/pos-system-nfs-client` | Mount and manage NFS shares: `mount `, `unmount `, `list`, `persist `, `unpersist ` | Requires `nfs-common` (added to `preinstall.sh` PACKAGES). `persist` writes a systemd `.mount` unit (`systemd-escape --path --suffix=mount`) with `After=network-online.target` / `Wants=network-online.target` — mounts only once all interfaces are up, no fstab edits to break boot — then `daemon-reload` + `enable --now`. `unpersist` stops/disables/removes the unit. `mount`/`persist` announce via `lib/notify.sh` | @@ -227,7 +227,7 @@ The bot token is a secret — it is stored only in `~/.config/linux_post_install | `pos communication telegram listener --disable` | Stops, disables, and removes the service | | `pos communication telegram listener --run` | Run the polling loop in the foreground (what the service executes) | -The map file is re-read for every message — edits apply without a restart. The listener only reacts to the owner chat (`TELEGRAM_CHAT_ID`); anyone else's message is ignored. `/help` lists mapped commands; an unmapped command replies "Unknown command". Commands run as your user via `timeout 60 bash -c "…"` (stdout + stderr are replied, truncated to ~3800 chars; empty output → `OK`), so `sudo` inside them needs a NOPASSWD rule. `--enable` warns if linger is off — the service stops when you log out unless you run `sudo loginctl enable-linger $(whoami)`. +The map file is re-read for every message — edits apply without a restart. The listener only reacts to the owner chat (`TELEGRAM_CHAT_ID`); anyone else's message is ignored. `/help` lists mapped commands; an unmapped command replies "Unknown command". Commands run as your user via `timeout 60 bash -c "…"` (stdout + stderr are replied, truncated to ~3800 chars; empty output → `OK`), so `sudo` inside them needs a NOPASSWD rule. A map value prefixed with `@quiet ` runs the command but does NOT reply — for commands that already send their own notification (e.g. `/status=@quiet pos system health --send`), avoiding a double message. `--enable` warns if linger is off — the service stops when you log out unless you run `sudo loginctl enable-linger $(whoami)`. ### entertainment diff --git a/DOC/howto/communication.md b/DOC/howto/communication.md index 83d82cc..a1baeaa 100644 --- a/DOC/howto/communication.md +++ b/DOC/howto/communication.md @@ -88,7 +88,7 @@ pos communication telegram listener --disable # remove it one `/cmd=bash command` per line — re-read on every message, so edits apply instantly. Example: ``` - /status=systemctl --user is-active pos-telegram-listener.service + /status=@quiet pos system health --send /temp=sensors | grep -i 'Tctl\|package id 0' /update=cd /path/to/repo && git pull ``` @@ -97,6 +97,10 @@ pos communication telegram listener --disable # remove it - **Runs as you:** mapped commands execute as your user with a 60s timeout, stdout + stderr are replied to the chat (truncated ~3800 chars; empty → `OK`). `sudo` inside a command needs a NOPASSWD rule. +- **`@quiet` prefix:** a map value starting with `@quiet ` runs the command but + does NOT reply — for commands that already send their own notification, so + you don't get it twice. `/status=@quiet pos system health --send` delivers + one digest via the notify system and nothing else. - **Daemon lifecycle:** the service is a systemd **user** unit; it stops at logout unless you enable linger: `sudo loginctl enable-linger $(whoami)`. `--enable` prints this warning if linger is off. diff --git a/bin/pos-communication-telegram-listener b/bin/pos-communication-telegram-listener index 66700f1..48badaa 100755 --- a/bin/pos-communication-telegram-listener +++ b/bin/pos-communication-telegram-listener @@ -30,6 +30,10 @@ Commands: Config: $CONFIG_FILE (TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID) Map: $MAP_FILE — one '/cmd=bash command' per line +Prefix a map value with '@quiet ' to run the command without replying — +for commands that already send their own notification, so you don't get it +twice (e.g. '/status=@quiet pos system health --send'). + The listener only reacts to the owner chat (TELEGRAM_CHAT_ID). Commands run as your user, so 'sudo' inside them needs a NOPASSWD rule. The interactive editor runs 'bash -n' to syntax-check commands before saving. @@ -125,7 +129,7 @@ map_cmds_list() { } check_syntax() { - bash -n -c "$1" 2>&1 + bash -n -c "$(strip_quiet "$1")" 2>&1 } # ── interactive editor ────────────────────────────────────────── @@ -149,8 +153,24 @@ ui_pick() { echo "$idx" } +# Map entries may be prefixed with '@quiet ' to run the command without +# replying — for commands that deliver their own notification (e.g. the +# health check's --send). The marker is stripped before running and before +# the editor's bash -n syntax check. +QUIET_PREFIX="@quiet" + +strip_quiet() { + local value="$1" + if [ "${value#"$QUIET_PREFIX "}" != "$value" ]; then + printf '%s' "${value#"$QUIET_PREFIX "}" + else + printf '%s' "$value" + fi +} + ui_run_command() { local value="$1" output rc + value="$(strip_quiet "$value")" echo echo "--- running: $value" if output="$(timeout 60 bash -c "$value" 2>&1)"; then @@ -334,7 +354,7 @@ reply() { } handle_message() { - local text="$1" msg_id="$2" value output rc + local text="$1" msg_id="$2" value output rc quiet=0 case "$text" in /help|/start) reply "Mapped commands: $(map_cmds_list)" "$msg_id" @@ -345,12 +365,17 @@ handle_message() { reply "Unknown command: $text (send /help)" "$msg_id" return fi + if [ "${value#"$QUIET_PREFIX "}" != "$value" ]; then + quiet=1 + value="${value#"$QUIET_PREFIX "}" + fi log "exec: $text" if output="$(timeout 60 bash -c "$value" 2>&1)"; then rc=0 else rc=$? fi + [ "$quiet" -eq 1 ] && return if [ -z "$output" ]; then output="OK" fi diff --git a/bin/pos-system-health b/bin/pos-system-health index 4504069..0051ea5 100755 --- a/bin/pos-system-health +++ b/bin/pos-system-health @@ -20,7 +20,9 @@ Host health dashboard: disk, RAM/swap, failed systemd units, backup age, fail2ban, docker containers. Exits 1 if any check FAILs. Flags: - --send Send the summary via the configured notify platform(s) + --send Send the summary via the configured notify platform(s) and do + NOT print the dashboard (notification-only — for cron/timers or + wrappers like the Telegram listener that reply with output) (uses lib/notify.sh; see NOTIFY_PLATFORM below) --markdown Same as --send, with markdown parse mode -h, --help Show this help @@ -56,6 +58,13 @@ if [ "$MARKDOWN" -eq 1 ]; then SEND=1 fi +# --send is notification-only: suppress the dashboard output (so a wrapper +# such as the Telegram listener doesn't echo it back as a duplicate reply). +if [ "$SEND" -eq 1 ]; then + exec 3>&1 + exec 1>/dev/null +fi + FAILURES=0 WARNINGS=0 REPORT=() @@ -227,6 +236,7 @@ else fi if [ "$SEND" -eq 1 ]; then + exec 1>&3 3>&- msg="POS Health — ${host} Verdict: ${verdict}" for line in "${REPORT[@]}"; do @@ -238,6 +248,7 @@ ${line}" else notify_send "$msg" fi + echo "sent: POS Health — ${host} (${verdict})" fi [ "$FAILURES" -eq 0 ]