fix: health --send is notification-only; listener @quiet prefix kills double reply

This commit is contained in:
Your Name
2026-08-07 14:15:33 -04:00
parent 81ac553590
commit 7dff27206b
6 changed files with 52 additions and 8 deletions
+4
View File
@@ -14,6 +14,10 @@ summary (newest last).
- When a task is completed: move it from Now/Next into **Done** (dated one-line) - When a task is completed: move it from Now/Next into **Done** (dated one-line)
in the same commit that finishes the work. 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 ## Now
- (none — Tier 1 shipped: `pos system health`, `lib/notify.sh`, digest timer) - (none — Tier 1 shipped: `pos system health`, `lib/notify.sh`, digest timer)
+2 -2
View File
@@ -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) | | `features/autostart.sh` | 14 | Boot-time feature (moved from `bin/`, flag-gated service) |
<!-- GEN:START filetable --> <!-- GEN:START filetable -->
| `bin/pos` | 267 | CLI dispatcher with smart arg matching + logging + category help | | `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-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-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) | | `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-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-backup` | 125 | Encrypted (AES-256) folder snapshots (tar + gpg) |
| `bin/pos-system-firewall` | 291 | Interactive UFW management | | `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-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-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) | | `bin/pos-usb-server` | 218 | USB Redirector server control (--ls, --share; prompts when args omitted) |
+2 -2
View File
@@ -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` | | `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 <folder-path>` | `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 `<name>_<date>.tar.gz.gpg` in the current directory, `chmod 600`. Success/failure are announced via `lib/notify.sh` | | `pos system backup <folder-path>` | `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 `<name>_<date>.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 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 <cmd>` | `bin/pos-system-nfs-server` | Manage the NFS kernel server: `status`, `share <path> [client]`, `unshare <path>`, `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-server <cmd>` | `bin/pos-system-nfs-server` | Manage the NFS kernel server: `status`, `share <path> [client]`, `unshare <path>`, `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 <cmd>` | `bin/pos-system-nfs-client` | Mount and manage NFS shares: `mount <server:export> <local-dir>`, `unmount <local-dir>`, `list`, `persist <server:export> <local-dir>`, `unpersist <local-dir>` | 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` | | `pos system nfs-client <cmd>` | `bin/pos-system-nfs-client` | Mount and manage NFS shares: `mount <server:export> <local-dir>`, `unmount <local-dir>`, `list`, `persist <server:export> <local-dir>`, `unpersist <local-dir>` | 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 --disable` | Stops, disables, and removes the service |
| `pos communication telegram listener --run` | Run the polling loop in the foreground (what the service executes) | | `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 ### entertainment
+5 -1
View File
@@ -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 one `/cmd=bash command` per line — re-read on every message, so edits apply
instantly. Example: 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' /temp=sensors | grep -i 'Tctl\|package id 0'
/update=cd /path/to/repo && git pull /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, - **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`). stdout + stderr are replied to the chat (truncated ~3800 chars; empty → `OK`).
`sudo` inside a command needs a NOPASSWD rule. `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 - **Daemon lifecycle:** the service is a systemd **user** unit; it stops at
logout unless you enable linger: `sudo loginctl enable-linger $(whoami)`. logout unless you enable linger: `sudo loginctl enable-linger $(whoami)`.
`--enable` prints this warning if linger is off. `--enable` prints this warning if linger is off.
+27 -2
View File
@@ -30,6 +30,10 @@ Commands:
Config: $CONFIG_FILE (TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID) Config: $CONFIG_FILE (TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID)
Map: $MAP_FILE — one '/cmd=bash command' per line 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 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 as your user, so 'sudo' inside them needs a NOPASSWD rule. The interactive
editor runs 'bash -n' to syntax-check commands before saving. editor runs 'bash -n' to syntax-check commands before saving.
@@ -125,7 +129,7 @@ map_cmds_list() {
} }
check_syntax() { check_syntax() {
bash -n -c "$1" 2>&1 bash -n -c "$(strip_quiet "$1")" 2>&1
} }
# ── interactive editor ────────────────────────────────────────── # ── interactive editor ──────────────────────────────────────────
@@ -149,8 +153,24 @@ ui_pick() {
echo "$idx" 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() { ui_run_command() {
local value="$1" output rc local value="$1" output rc
value="$(strip_quiet "$value")"
echo echo
echo "--- running: $value" echo "--- running: $value"
if output="$(timeout 60 bash -c "$value" 2>&1)"; then if output="$(timeout 60 bash -c "$value" 2>&1)"; then
@@ -334,7 +354,7 @@ reply() {
} }
handle_message() { 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 case "$text" in
/help|/start) /help|/start)
reply "Mapped commands: $(map_cmds_list)" "$msg_id" reply "Mapped commands: $(map_cmds_list)" "$msg_id"
@@ -345,12 +365,17 @@ handle_message() {
reply "Unknown command: $text (send /help)" "$msg_id" reply "Unknown command: $text (send /help)" "$msg_id"
return return
fi fi
if [ "${value#"$QUIET_PREFIX "}" != "$value" ]; then
quiet=1
value="${value#"$QUIET_PREFIX "}"
fi
log "exec: $text" log "exec: $text"
if output="$(timeout 60 bash -c "$value" 2>&1)"; then if output="$(timeout 60 bash -c "$value" 2>&1)"; then
rc=0 rc=0
else else
rc=$? rc=$?
fi fi
[ "$quiet" -eq 1 ] && return
if [ -z "$output" ]; then if [ -z "$output" ]; then
output="OK" output="OK"
fi fi
+12 -1
View File
@@ -20,7 +20,9 @@ Host health dashboard: disk, RAM/swap, failed systemd units, backup age,
fail2ban, docker containers. Exits 1 if any check FAILs. fail2ban, docker containers. Exits 1 if any check FAILs.
Flags: 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) (uses lib/notify.sh; see NOTIFY_PLATFORM below)
--markdown Same as --send, with markdown parse mode --markdown Same as --send, with markdown parse mode
-h, --help Show this help -h, --help Show this help
@@ -56,6 +58,13 @@ if [ "$MARKDOWN" -eq 1 ]; then
SEND=1 SEND=1
fi 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 FAILURES=0
WARNINGS=0 WARNINGS=0
REPORT=() REPORT=()
@@ -227,6 +236,7 @@ else
fi fi
if [ "$SEND" -eq 1 ]; then if [ "$SEND" -eq 1 ]; then
exec 1>&3 3>&-
msg="POS Health — ${host} msg="POS Health — ${host}
Verdict: ${verdict}" Verdict: ${verdict}"
for line in "${REPORT[@]}"; do for line in "${REPORT[@]}"; do
@@ -238,6 +248,7 @@ ${line}"
else else
notify_send "$msg" notify_send "$msg"
fi fi
echo "sent: POS Health — ${host} (${verdict})"
fi fi
[ "$FAILURES" -eq 0 ] [ "$FAILURES" -eq 0 ]