fix: health --send is notification-only; listener @quiet prefix kills double reply
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
@@ -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 ]
|
||||||
|
|||||||
Reference in New Issue
Block a user