From a4c025d2360cbec91b40cdca599bc70e32b5e099 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 13 Aug 2026 14:13:06 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20entertainment=20hardening=20=E2=80=94?= =?UTF-8?q?=20notify=5Fsend=20delivery,=20plugin=20lib,=20last-run=20state?= =?UTF-8?q?,=20config=20edit;=20shared=20user-timers=20lib?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENT_TODO.md | 2 + DOC/AGENT_Context_Project.md | 55 ++++++------ DOC/DEV.md | 30 ++++--- DOC/POS.md | 19 ++-- DOC/SCRIPTS.md | 13 ++- DOC/howto/entertainment.md | 30 ++++--- bin/pos | 2 +- bin/pos-entertainment-config | 52 ++++++++++- bin/pos-entertainment-enable | 2 +- bin/pos-entertainment-send | 22 ++--- bin/pos-entertainment-status | 27 ++++-- config/entertainment.env | 2 + entertainment/gold.sh | 16 ++-- entertainment/joke.sh | 17 ++-- entertainment/weather.sh | 97 +++++++++----------- install.sh | 2 +- lib/entertainment-lib.sh | 151 ++++++++++++-------------------- lib/entertainment-plugin-lib.sh | 67 ++++++++++++++ lib/scheduler-lib.sh | 124 ++++++-------------------- lib/user-timers-lib.sh | 112 +++++++++++++++++++++++ 20 files changed, 493 insertions(+), 349 deletions(-) create mode 100644 lib/entertainment-plugin-lib.sh create mode 100644 lib/user-timers-lib.sh diff --git a/AGENT_TODO.md b/AGENT_TODO.md index 5223f15..cd97fc0 100644 --- a/AGENT_TODO.md +++ b/AGENT_TODO.md @@ -16,6 +16,8 @@ summary (newest last). ## Done +- **2026-08-13** — Entertainment-module hardening (approved Tier 1 + Tier 2): delivery moved to `notify_send` (platform follows `NOTIFY_PLATFORM`, default Telegram) via `lib/notify.sh` sourced by `bin/pos-entertainment-send`; a **last-run state** is recorded per plugin (`~/.local/share/linux_post_install/entertainment/last/` — rc + timestamp) on every non-`--print` run and shown by `pos entertainment status`, which also lists installed-but-not-enabled plugins; a send that fails **while fired by a timer** (gated on `$INVOCATION_ID`) additionally notifies the configured platforms. New **message-safe plugin lib** `lib/entertainment-plugin-lib.sh` (defines only `plugin_*`, never writes stdout — the stdout contract stays "message only"): `plugin_load_config` (entertainment.env + env precedence), `plugin_have`, `plugin_require`, `plugin_err`, `plugin_http_json [--key ] [-H
]` (curl `--max-time 20 --retry 2`); `weather`/`joke`/`gold` refactored onto it. `pos entertainment config` gains `get|unset|ls|edit` (edit via the shared `pos config` UI — added to `INTERACTIVE_CMDS`). **Tier 2**: new shared lib `lib/user-timers-lib.sh` (only `ut_*`: `ut_interval_to_oncalendar`, `ut_interval_label`, `ut_unit_name`, `ut_write_unit_pair` incl. `TimeoutStopSec=5s` + `Persistent` + network-online deps, `ut_ensure_linger`, `USER_SYSTEMD_DIR`) dedupes the systemd user-timer machinery between `lib/entertainment-lib.sh` and `lib/scheduler-lib.sh` (the latter's `sched_*` duplicates deleted; both source it; collides-with-nothing). `install.sh` Phase 2 lib list += the two new libs; SCRIPTS.md/DEV.md/POS.md/howto/entertainment.md/AGENT_Context updated (hand-maintained lib rows: entertainment-lib 354→311, scheduler-lib 830→760, +112 user-timers-lib, +67 plugin lib). Verified: `bash -n` everywhere; smoke-tested in an isolated `HOME=/tmp/enttest` (status, config get/set/unset/ls, send path rc=0, failing plugin records rc=1, error-case message hygiene); `make gen && make check` green. + - **2026-08-13** — Fast pos-unit shutdown: every systemd unit a pos tool writes (or `systemd/` ships) now sets `TimeoutStopSec=5s` (+ `KillMode=control-group` on the daemons) so a stuck process can't stall a reboot for the 90s systemd default. Applied at all 7 template sites: `pos-communication-telegram-listener`, `pos-communication-matrix-listener` (also gained a `trap 'kill $(jobs -p) 2>/dev/null; exit 0' TERM INT` in `run_daemon` so stop returns sub-second), `pos-network-download` (aria2 + retry-healer units), `lib/scheduler-lib.sh` `sched_write_units`, `lib/entertainment-lib.sh` `write_units`, and `systemd/{ssh-agent,autostart,usb-automount}.service`. Legacy-unit cleanup: the repo no longer ships `pos-health.{service,timer}` / `pos-entertainment.service` (they were documented but postinstall never created them — found stale only on the live box, FAILED); removed their stale references from SYSTEMD.md (deleted the `pos-health.service` section + gating special-case, added a new **Stop behavior** section), POS.md, HOWTO.md, howto/system.md (now documents the `pos system schedule` job replacement + removal commands), AGENT_Context (tree, phase description, selfcontained table). DEV.md Best Practices gains a **Systemd units** convention (TimeoutStopSec=5s + TERM trap + regeneration caveat). Verified: `bash -n` on all edited scripts; `make gen && make check` green (filetable rows for the two listeners + network-download auto-regenerated, hand-maintained lib rows bumped 350→354 / 822→830). Live-box application is manual (this session was a Google Cloud Shell, not the real machine): regenerate units via `pos network download start`, `pos communication telegram listener --enable`, `pos system schedule enable `, `pos entertainment enable `, then `sudo systemctl disable --now pos-health.timer pos-health.service 2>/dev/null; sudo rm -f /etc/systemd/system/pos-health.{service,timer} && sudo systemctl daemon-reload`. - **2026-08-13** — Bootstrap output transparency (`install.sh` / `preinstall.sh` / `postinstall.sh`): removed the redundant `apt update` (preinstall.sh owns it — install.sh previously ran it twice, showing two identical `OK apt update` lines); Phase 2 now names what it installs — libs line (`libs -> /usr/local/bin (644): common.sh flags.sh …`), plugin names in the count line, x64_bin names, per-feature `feature installed/overwritten` + `feature flag set` logs with a `N features installed: …` summary — and the misleading `"47 scripts + libs"` label is fixed to `47 scripts + 6 libs` (the 6 libs were outside the counter); preinstall prints `Installing N packages (apt install -y):` with the 40-name list wrapped at 80 cols; postinstall now logs silent skips — `config/authorized_keys is empty — nothing to add` (empty file previously looped zero times with no message), `schedule.d already exists, keeping it` (restructured the condition so the message is accurate when the dest exists vs config/schedule.d absent), and a per-service `service enabled: ` line. No output-layer changes (no `--verbose`, no log file — decided scope). Verified: `bash -n` + `--dry-run` smokes of phases 1/2/3 showing every new line (learned: `install.sh:19` hardcodes `export DRY_RUN=0`, so an env `DRY_RUN=1` is ignored — the flag `--dry-run` is required), hand-maintained filetable count rows bumped (install.sh 206→223, preinstall.sh 73→75, postinstall.sh 163→168), `make gen && make check` green. usb-automount left live (user choice). diff --git a/DOC/AGENT_Context_Project.md b/DOC/AGENT_Context_Project.md index 71c58bb..2ef510d 100644 --- a/DOC/AGENT_Context_Project.md +++ b/DOC/AGENT_Context_Project.md @@ -10,19 +10,19 @@ | ## 1. Project Overview | 28–43 | -| ## 2. Directory Structure | 44–189 | -| ## 3. Installation Flow | 190–241 | -| ## 4. The `pos` CLI System | 242–311 | -| ## 5. Shared Library — `lib/common.sh` | 312–343 | -| ## 6. Docker Compose / ScaleTail | 344–386 | -| ## 7. Optional Apps (`apps/`) | 387–416 | -| ## 8. Entertainment Module | 417–430 | -| ## 9. Systemd Services | 431–442 | -| ## 10. Configuration Files | 443–469 | -| ## 11. Coding Conventions | 470–502 | -| ## 12. Development Workflow | 503–555 | -| ## 13. Key File Quick Reference | 556–613 | -| ## 14. Common Tasks for Agents | 614–643 | +| ## 2. Directory Structure | 44–192 | +| ## 3. Installation Flow | 193–244 | +| ## 4. The `pos` CLI System | 245–314 | +| ## 5. Shared Library — `lib/common.sh` | 315–346 | +| ## 6. Docker Compose / ScaleTail | 347–389 | +| ## 7. Optional Apps (`apps/`) | 390–419 | +| ## 8. Entertainment Module | 420–433 | +| ## 9. Systemd Services | 434–445 | +| ## 10. Configuration Files | 446–472 | +| ## 11. Coding Conventions | 473–505 | +| ## 12. Development Workflow | 506–558 | +| ## 13. Key File Quick Reference | 559–618 | +| ## 14. Common Tasks for Agents | 619–648 | ## 1. Project Overview @@ -53,7 +53,10 @@ Linux_post_install/ │ ├── common.sh # Shared library (colors, logging, spinner, timer, run, load_system_env) │ ├── flags.sh # Feature flag store (flag_set/clear/is_set/value/list/status) │ ├── notify.sh # Multi-platform alerting (notify_send) — sourced opt-in, silent-fails -│ └── entertainment-lib.sh # Entertainment module lib (ENABLED list, scheduler sync) +│ ├── entertainment-lib.sh # Entertainment module lib (ENABLED list, last-run state, scheduler sync) +│ ├── entertainment-plugin-lib.sh # Message-safe helpers for plugins (config load, require, fetch+retry) +│ ├── scheduler-lib.sh # System scheduler lib (job parsing, notify policies, per-job user timers) +│ └── user-timers-lib.sh # Shared systemd **user** timer machinery (interval→OnCalendar, unit pair, linger) │ ├── bin/ # CLI tools — installed to /usr/local/bin/ │ ├── pos # Main dispatcher — smart arg matching to pos-* scripts @@ -71,7 +74,7 @@ Linux_post_install/ │ ├── pos-entertainment-config # Show or edit the entertainment config (ENABLED auto-trigger list, weather location) │ ├── pos-entertainment-disable # Disable a plugin's auto-trigger (remove it from ENABLED) │ ├── pos-entertainment-enable # Enable an auto-trigger for a plugin on a schedule -│ ├── pos-entertainment-send # Run a public-API plugin and send its output via Telegram (default sender) +│ ├── pos-entertainment-send # Run a public-API plugin and send its output via the configured notify platforms │ ├── pos-entertainment-status # Show enabled plugins and scheduler state │ ├── pos-media-mp3 # Download audio as MP3 (yt-dlp) │ ├── pos-media-mp4 # Download video as MP4 (smart/interactive format select) @@ -271,7 +274,7 @@ All non-interactive `pos` commands log output to `~/.local/share/linux_post_inst | entertainment | config | `pos-entertainment-config` | Show or edit the entertainment config (ENABLED auto-trigger list, weather location) | | entertainment | disable | `pos-entertainment-disable` | Disable a plugin's auto-trigger (remove it from ENABLED) | | entertainment | enable | `pos-entertainment-enable` | Enable an auto-trigger for a plugin on a schedule | -| entertainment | send | `pos-entertainment-send` | Run a public-API plugin and send its output via Telegram (default sender) | +| entertainment | send | `pos-entertainment-send` | Run a public-API plugin and send its output via the configured notify platforms | | entertainment | status | `pos-entertainment-status` | Show enabled plugins and scheduler state | | media | mp3 | `pos-media-mp3` | Download audio as MP3 (yt-dlp) | | media | mp4 | `pos-media-mp4` | Download video as MP4 (smart/interactive format select) | @@ -416,13 +419,13 @@ ScaleTail provides 119+ Docker Compose templates with a Tailscale sidecar patter ## 8. Entertainment Module -Public-API "entertainment" plugins (weather, joke, gold) that can auto-send their output to Telegram on a schedule. +Public-API "entertainment" plugins (weather, joke, gold) that can auto-send their output to the configured notify platforms (default Telegram) on a schedule. - **CLI:** `pos entertainment {config|enable|disable|send|status}` — see the dispatch table in §4 and POS.md [entertainment](#entertainment). -- **Library:** `lib/entertainment-lib.sh` — config-file helpers, ENABLED-list parsing, plugin lookup, and scheduler sync (systemd user timers, crontab fallback). -- **Plugins:** `entertainment/*.sh` — standalone scripts that fetch a public API and **print the message to stdout** (what gets sent). Each declares its name with a `# POS_PLUGIN: ` header; a new plugin is auto-discovered. +- **Library:** `lib/entertainment-lib.sh` — config-file helpers, ENABLED-list parsing, plugin lookup, per-plugin last-run state, and scheduler sync. Timer machinery (interval→OnCalendar, unit pair writer, linger) is shared via `lib/user-timers-lib.sh` with the system scheduler. +- **Plugins:** `entertainment/*.sh` — standalone scripts that fetch a public API and **print the message to stdout** (what gets sent). Each declares its name with a `# POS_PLUGIN: ` header; a new plugin is auto-discovered. Plugins may source `lib/entertainment-plugin-lib.sh` (message-safe: no stdout chatter). - **Config:** `~/.config/linux_post_install/entertainment.env` (ENABLED auto-trigger list, weather location). Template: `config/entertainment.env`, auto-installed by postinstall. -- **Sending:** `pos entertainment send [--print] [--markdown]` runs the plugin and delivers via `pos communication telegram sender send`. +- **Sending:** `pos entertainment send [--print] [--markdown]` runs the plugin and delivers via `notify_send` (follows `NOTIFY_PLATFORM`, default Telegram). Last-run rc/timestamp is recorded per plugin and shown by `status`; a timer-context failure also notifies. - **Auto-trigger:** `pos entertainment enable ` writes the plugin into ENABLED and syncs a systemd user timer (allowed intervals: `5m 10m 15m 30m 45m hourly 2h 6h 12h daily weekly`, or `OnCalendar=…`); `disable` removes it. - **Docs:** DEV.md "Adding an Entertainment Plugin" (§1 step list) and POS.md [entertainment](#entertainment). @@ -563,8 +566,10 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:` | `lib/common.sh` | 144 | Shared library (log/warn/err/run/spawn, dry-run aware, `load_system_env`) | | `lib/flags.sh` | 60 | Feature flag store (set/clear/is_set/value/list/status) | | `lib/notify.sh` | 76 | Multi-platform alerting (`notify_send`) — opt-in source, silent-fails | -| `lib/entertainment-lib.sh` | 354 | Entertainment module lib (ENABLED parsing, scheduler sync) | -| `lib/scheduler-lib.sh` | 830 | Scheduler lib (job parsing, notify policies, per-job user timers, legacy migrate) | +| `lib/entertainment-lib.sh` | 311 | Entertainment module lib (ENABLED parsing, last-run state, scheduler sync via user-timers-lib) | +| `lib/entertainment-plugin-lib.sh` | 67 | Message-safe helpers for plugins (config load, require, fetch+retry) — plugins MAY source it | +| `lib/scheduler-lib.sh` | 760 | Scheduler lib (job parsing, notify policies, per-job user timers via user-timers-lib, legacy migrate) | +| `lib/user-timers-lib.sh` | 112 | Shared systemd **user** timer machinery (interval→OnCalendar, unit pair writer, linger) | | `bin/flag-reader` | 58 | Inspect flags (list/status/`--raw`) | | `bin/flag-set` | 21 | Set a flag (optionally with a value) | | `bin/flag-clear` | 21 | Unset a flag | @@ -582,11 +587,11 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:` | `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) | | `bin/pos-docker-vbox` | 158 | Disposable Docker-based VMs (create/enter/start/stop/rm/ls) | -| `bin/pos-entertainment-config` | 99 | Show or edit the entertainment config (ENABLED auto-trigger list, weather location) | +| `bin/pos-entertainment-config` | 143 | Show or edit the entertainment config (ENABLED auto-trigger list, weather location) | | `bin/pos-entertainment-disable` | 32 | Disable a plugin's auto-trigger (remove it from ENABLED) | | `bin/pos-entertainment-enable` | 49 | Enable an auto-trigger for a plugin on a schedule | -| `bin/pos-entertainment-send` | 93 | Run a public-API plugin and send its output via Telegram (default sender) | -| `bin/pos-entertainment-status` | 49 | Show enabled plugins and scheduler state | +| `bin/pos-entertainment-send` | 95 | Run a public-API plugin and send its output via the configured notify platforms | +| `bin/pos-entertainment-status` | 62 | Show enabled plugins and scheduler state | | `bin/pos-media-mp3` | 80 | Download audio as MP3 (yt-dlp) | | `bin/pos-media-mp4` | 126 | Download video as MP4 (smart/interactive format select) | | `bin/pos-network-checkport` | 496 | Check TCP/UDP port reachability (nmap, or bash/nc fallback) + local interface view | diff --git a/DOC/DEV.md b/DOC/DEV.md index d4831ea..f6df918 100644 --- a/DOC/DEV.md +++ b/DOC/DEV.md @@ -20,7 +20,7 @@ How this repo works, how to add features, and what to keep in mind when editing. | Phase | Script | Responsibility | |-------|--------|----------------| | Pre | `preinstall.sh` | System packages, apt repos, global binaries (yt-dlp) | -| Install | `install.sh` | Copies `bin/*` → `/usr/local/bin/` (chmod 755), `lib/common.sh` + `lib/flags.sh` + `lib/entertainment-lib.sh` → `/usr/local/bin/` (chmod 644) | +| Install | `install.sh` | Copies `bin/*` → `/usr/local/bin/` (chmod 755), all `lib/*.sh` → `/usr/local/bin/` (chmod 644) | | Post | `postinstall.sh` | User config (SSH keys, PATH, bash completion), systemd services | Each phase is independent and runs only if the corresponding script exists. @@ -31,7 +31,7 @@ Each phase is independent and runs only if the corresponding script exists. |-----------|---------|-------------| | `bin/` | Daily-use CLI tools and wrappers | `/usr/local/bin/` | | `apps//` | Optional desktop app installers | run on demand | -| `lib/` | Shared libraries: `common.sh` (helpers), `flags.sh` (feature flags), `notify.sh` (multi-platform alerting), `entertainment-lib.sh` (entertainment scheduling) | sourced at build time | +| `lib/` | Shared libraries: `common.sh` (helpers), `flags.sh` (feature flags), `notify.sh` (multi-platform alerting), `entertainment-lib.sh` (entertainment scheduling + last-run state), `entertainment-plugin-lib.sh` (message-safe plugin helpers), `scheduler-lib.sh` (system scheduler), `user-timers-lib.sh` (shared systemd user timer machinery), `config-ui.sh` (interactive config UI) | sourced at build time | | `config/` | Gitignored user config files | `~/.config//` (via postinstall) | | `entertainment/` | Public-API plugins for the entertainment module | `/usr/local/bin` (via install.sh Phase 2) | | `compose/` | ScaleTail templates (git submodule) | `/usr/local/share/linux_post_install/scale-tail` | @@ -203,11 +203,13 @@ Stub harnesses are **throwaway by design**: no `tests/` dir and no CI in this re ## Adding an Entertainment Plugin -The `entertainment` module routes public-API data to Telegram via the single runner `pos entertainment send ` (`bin/pos-entertainment-send`). Auto-triggering is config-driven: `ENABLED` in `entertainment.env` holds `plugin, interval` pairs; the tools `pos entertainment config|enable|disable|status` (`bin/pos-entertainment-*`) reconcile the schedule. All shared logic (ENABLED parsing, interval→schedule mapping, scheduler sync) lives in `lib/entertainment-lib.sh` — sourced by the `pos-entertainment-*` tools (never by plugins). The scheduler is **systemd user timers** — the only backend (requires a reachable user systemd manager). +The `entertainment` module routes public-API data to the configured notify platforms via the single runner `pos entertainment send ` (`bin/pos-entertainment-send`). Auto-triggering is config-driven: `ENABLED` in `entertainment.env` holds `plugin, interval` pairs; the tools `pos entertainment config|enable|disable|status` (`bin/pos-entertainment-*`) reconcile the schedule. Shared logic (ENABLED parsing, plugin lookup, last-run state, scheduler sync) lives in `lib/entertainment-lib.sh` — sourced by the `pos-entertainment-*` tools (never by plugins). The scheduler is **systemd user timers** — the only backend (requires a reachable user systemd manager); the timer machinery itself is shared with the system scheduler via `lib/user-timers-lib.sh`. ### 1. Create the plugin -Drop an executable script in `entertainment/.sh` with a `# POS_PLUGIN: ` marker on line 3 (this is what makes it a plugin — the installed runner lists plugins by this marker, not by `.sh` files, since `/usr/local/bin` is shared with other tooling). Declare every config key the plugin reads with `# POS_KEYS: (required|optional)` lines right after it — `pos entertainment config` prints these in its Keys section and uses them to warn/not-warn on `config set`: +Drop an executable script in `entertainment/.sh` with a `# POS_PLUGIN: ` marker (this is what makes it a plugin — the installed runner lists plugins by this marker, not by `.sh` files, since `/usr/local/bin` is shared with other tooling). Declare every config key the plugin reads with `# POS_KEYS: (required|optional)` lines right after it — `pos entertainment config` prints these in its Keys section and uses them to warn/not-warn on `config set`. + +Plugins may source `lib/entertainment-plugin-lib.sh` — message-safe helpers (config load, dep guards, JSON fetch with retry) that never write to stdout. Template: ```bash #!/usr/bin/env bash @@ -215,23 +217,27 @@ set -euo pipefail # POS_PLUGIN: myplugin # POS_KEYS: MYPLUGIN_URL (required) # POS_KEYS: MYPLUGIN_TAG (optional) -err() { echo "ERROR: $*" >&2; exit 1; } +source "$(dirname "${BASH_SOURCE[0]}")/../lib/entertainment-plugin-lib.sh" 2>/dev/null \ + || source "$(dirname "${BASH_SOURCE[0]}")/entertainment-plugin-lib.sh" 2>/dev/null \ + || source "$(dirname "$0")/../lib/entertainment-plugin-lib.sh" 2>/dev/null \ + || source "$(dirname "$0")/entertainment-plugin-lib.sh" -command -v curl &>/dev/null || err "curl not found" - -data="$(curl -fsS --max-time 20 https://api.example.com/foo)" -printf 'Title: %s\n' "$data" +plugin_load_config +plugin_require MYPLUGIN_URL +tag="${MYPLUGIN_TAG:-}" +json="$(plugin_http_json --key '.title' "${MYPLUGIN_URL}${tag:+/?tag=$tag}")" +printf 'Title: %s\n' "$json" ``` -**Contract:** plugins are **self-contained** — do **not** source `lib/common.sh`. Its `log`/`warn`/`ok` helpers print to **stdout**, and the runner captures stdout as the message to send (helper chatter would be sent to Telegram). All stdout is the message; errors go to stderr and exit nonzero. Plugins must be non-interactive (no prompts) — the module is designed for systemd user timers. +**Contract:** plugins are **self-contained** — do **not** source `lib/common.sh` or `lib/entertainment-lib.sh`. Their `log`/`warn`/`ok` helpers print to **stdout**, and the runner captures stdout as the message to send (helper chatter would be sent to the notify platforms). All stdout is the message; errors go to stderr and exit nonzero. `lib/entertainment-plugin-lib.sh` is the *only* lib plugins may source (it defines just `plugin_*` and writes nothing to stdout). Plugins must be non-interactive (no prompts) — the module is designed for systemd user timers. ### 2. Config (if needed) -Read runtime values from `~/.config/linux_post_install/entertainment.env` (chmod 600, env precedence) — same pattern as `telegram.env`. Example: `weather.sh` uses `WEATHER_LAT`/`WEATHER_LON`. Declare each key with a `# POS_KEYS:` header line (see step 1) so `pos entertainment config` lists it and `config set` recognizes it. +Read runtime values from `~/.config/linux_post_install/entertainment.env` (chmod 600, env precedence) via `plugin_load_config` — same pattern as `telegram.env`. Example: `weather.sh` uses `WEATHER_LAT`/`WEATHER_LON`. Declare each key with a `# POS_KEYS:` header line (see step 1) so `pos entertainment config` lists it and `config set` recognizes it. ### 3. Deps -`curl` and `jq` are already in `preinstall.sh` PACKAGES. Anything else: guard with `command -v … || err "…"` and, if apt-available, add to PACKAGES. +`curl` and `jq` are already in `preinstall.sh` PACKAGES (and are what `plugin_have curl`/`plugin_have jq` check). Anything else: guard with `plugin_have ` and, if apt-available, add to PACKAGES. ### 4. Done diff --git a/DOC/POS.md b/DOC/POS.md index 4880412..1186173 100644 --- a/DOC/POS.md +++ b/DOC/POS.md @@ -325,19 +325,23 @@ The daemon long-polls `/sync` (30s timeout, per-sync `since` token, compact filt ### entertainment **File:** `bin/pos-entertainment-send` -**Purpose:** run a public-API plugin and send its output to Telegram by default. Plugins are standalone scripts in `entertainment/` that fetch a public API and **print the message to stdout** — that stdout is what gets sent. +**Purpose:** run a public-API plugin and send its output via `notify_send` — the platform follows `NOTIFY_PLATFORM` (default Telegram, silent-fail when none configured). Plugins are standalone scripts in `entertainment/` that fetch a public API and **print the message to stdout** — that stdout is what gets sent. | Command | Behavior | |---------|----------| | `pos entertainment send` | List available plugins + usage | -| `pos entertainment send [--print] [--markdown] [args…]` | Run the plugin, send its output to Telegram (silent) | +| `pos entertainment send [--print] [--markdown] [args…]` | Run the plugin, send its output via `notify_send` (silent) | | `pos entertainment send --print` | Print the output locally; do not send | -| `pos entertainment send --markdown` | Send with `--parse-mode markdown` (via `pos communication telegram sender`) | +| `pos entertainment send --markdown` | Send with Markdown parse_mode (via the notify senders) | | `pos entertainment config` | Show the config file (`~/.config/linux_post_install/entertainment.env`) | +| `pos entertainment config get KEY` | Print one key's current value (`(not set)` if absent) | | `pos entertainment config set KEY=VALUE…` | Set keys (any UPPER_SNAKE key; warns if no installed plugin uses it) and re-sync the schedule | +| `pos entertainment config unset KEY` | Remove a key from the config file | +| `pos entertainment config ls` | Declared keys with their current values, aligned | +| `pos entertainment config edit` | Interactive editor for the scope (via `pos config` UI) | | `pos entertainment enable [interval]` | Add plugin to `ENABLED` + schedule it as a systemd user timer | | `pos entertainment disable ` | Remove plugin from `ENABLED` + remove its scheduled job | -| `pos entertainment status` | Enabled plugins + scheduler + schedule state | +| `pos entertainment status` | Enabled plugins (each with interval + last run), installed-but-not-enabled plugins, scheduler + timers | **Plugin lookup order:** `$ENTERTAINMENT_DIR` → repo `entertainment/` → `/usr/local/bin/` (installed by `install.sh` Phase 2, beside the runner). A plugin name matches the file name with or without the `.sh` suffix. @@ -351,7 +355,7 @@ Plugins: **Config auto-install:** `postinstall.sh` copies the repo's `config/entertainment.env` (a commented template showing each key's syntax) to `~/.config/linux_post_install/entertainment.env` on install — but only if you haven't already created your own (no clobber), and prints the template so you can fill in your location. Fill in `WEATHER_LAT`/`WEATHER_LON` (and optionally `WEATHER_CITY`) to enable the weather plugin. -**Adding a plugin:** drop an executable script in `entertainment/` (e.g. `myfeed.sh`) with a `# POS_PLUGIN: ` marker on line 3 — the runner lists and validates plugins by this marker, so non-plugin `.sh` files in the shared `/usr/local/bin` are ignored. The plugin must be non-interactive and print the message to stdout; errors go to stderr (exit nonzero). If it needs coordinates/tokens, read them from `~/.config/linux_post_install/entertainment.env` (chmod 600, env precedence). No registration needed. Dependencies beyond `curl`/`jq` (both in `preinstall.sh` PACKAGES) should be guarded with `command -v … || exit 1`. +**Adding a plugin:** drop an executable script in `entertainment/` (e.g. `myfeed.sh`) with a `# POS_PLUGIN: ` marker — the runner lists and validates plugins by this marker, so non-plugin `.sh` files in the shared `/usr/local/bin` are ignored. The plugin must be non-interactive and print the message to stdout; errors go to stderr (exit nonzero). Source `lib/entertainment-plugin-lib.sh` for the standard helpers — `plugin_load_config` (reads `entertainment.env`, env precedence), `plugin_have `, `plugin_require KEY `, `plugin_http_json [--key ] [-H
]` (curl, 2 retries, timeout) — it never writes to stdout, so the message stays clean. No registration needed. Dependencies beyond `curl`/`jq` (both in `preinstall.sh` PACKAGES) should be guarded with `plugin_have`. **Declaring config keys (pattern):** document every key the plugin reads with one `# POS_KEYS:` line right after `# POS_PLUGIN:` — `KEY`, a description, and `(required)`/`(optional)`: @@ -373,15 +377,16 @@ ENABLED="weather, 5m gold, 1h joke, daily" - One **user timer** per enabled plugin (`~/.config/systemd/user/pos-entertainment-.{service,timer}`), running `pos entertainment send ` as your user on that schedule (`OnCalendar` + `Persistent=true`). `pos entertainment enable` also tries `sudo loginctl enable-linger $USER` once so timers fire without login. -The job runs as you, so it reads your `$HOME` configs (weather location, Telegram token) natively — no `Environment=HOME=` hacks. +The job runs as you, so it reads your `$HOME` configs (weather location, notify platform) natively — no `Environment=HOME=` hacks. Intervals: `5m 10m 15m 30m 45m hourly 2h 6h 12h daily weekly`, or a raw `OnCalendar=…` spec. Default when omitted: `daily`. -`pos entertainment status` shows the enabled plugins, the scheduler, and each plugin's interval + next fire time (`systemctl --user list-timers`). +`pos entertainment status` shows the enabled plugins (each with interval + last run), the installed-but-not-enabled plugins, the scheduler, and each plugin's next fire time (`systemctl --user list-timers`). Last run is recorded by `pos entertainment send` on every non-`--print` run (`~/.local/share/linux_post_install/entertainment/last/`); a run that fails while fired by a timer also notifies the configured platforms. Notes: - The runner is headless/timer-friendly — no TTY prompts, exit 0 on success / 1 on failure. - Scheduling is per-user for the user who runs `enable`; if you manage a different machine's user (e.g. via `runuser`/`sudo -u`), run the `enable`/`disable` commands as that user. +- The unit template (`TimeoutStopSec=5s`, `Persistent=true`, network-online deps) is shared with the system scheduler via `lib/user-timers-lib.sh`. ### flags diff --git a/DOC/SCRIPTS.md b/DOC/SCRIPTS.md index d79a6eb..ce5bd8a 100644 --- a/DOC/SCRIPTS.md +++ b/DOC/SCRIPTS.md @@ -38,7 +38,7 @@ The phases: | # | Phase | Script/action | |---|-------|----------------| | 1 | preinstall | `preinstall.sh` — apt packages + yt-dlp | -| 2 | scripts | Copies `bin/*` → `/usr/local/bin/` (755), `lib/common.sh` + `lib/flags.sh` + `lib/notify.sh` + `lib/entertainment-lib.sh` → `/usr/local/bin/` (644). Copies precompiled arch binaries from `x64_bin/` (or `arm64_bin/`) → `/usr/local/bin/`. With `--feature`: also installs `features/*` (see below) | +| 2 | scripts | Copies `bin/*` → `/usr/local/bin/` (755), `lib/common.sh` + `lib/flags.sh` + `lib/notify.sh` + `lib/entertainment-lib.sh` + `lib/entertainment-plugin-lib.sh` + `lib/scheduler-lib.sh` + `lib/config-ui.sh` + `lib/user-timers-lib.sh` → `/usr/local/bin/` (644). Copies precompiled arch binaries from `x64_bin/` (or `arm64_bin/`) → `/usr/local/bin/`. With `--feature`: also installs `features/*` (see below) | | 3 | postinstall | `postinstall.sh` — PATH, completion, SSH keys, systemd | | 4 | scalepoint | Shallow-clones ScaleTail templates to `/usr/local/share/linux_post_install/scale-tail` | | 5 (opt) | apps | `apps/install.sh` when `--apps` (interactive) or `--full` (all, non-interactive) | @@ -194,9 +194,16 @@ Platform selection: `~/.config/linux_post_install/notify.env` (`NOTIFY_PLATFORM= ## lib/entertainment-lib.sh — entertainment module **File:** `lib/entertainment-lib.sh` (installed to `/usr/local/bin/entertainment-lib.sh`) -**Purpose:** shared logic for the `pos entertainment` tools — config (`entertainment.env`), `ENABLED` auto-trigger list parsing (`plugin, interval` pairs), plugin lookup by `# POS_PLUGIN:` marker, interval→schedule mapping, and scheduler reconciliation (systemd **user** timers — the only backend; requires a reachable user manager, `ensure_linger()` enables linger if needed). +**Purpose:** shared logic for the `pos entertainment` tools — config (`entertainment.env`), `ENABLED` auto-trigger list parsing (`plugin, interval` pairs), plugin lookup by `# POS_PLUGIN:` marker, per-plugin last-run state (`~/.local/share/linux_post_install/entertainment/last/`), and scheduler reconciliation. Timer machinery (interval→OnCalendar, unit pair writer, linger) comes from `lib/user-timers-lib.sh`, shared with the system scheduler. -Sourced by `bin/pos-entertainment-send|config|enable|disable|status` (after `lib/common.sh`). **Plugins must not source it** — their stdout is the sent message. +Sourced by `bin/pos-entertainment-send|config|enable|disable|status` (after `lib/common.sh`). **Plugins must not source it** — their stdout is the sent message; they may instead source `lib/entertainment-plugin-lib.sh` (message-safe helpers: config load, `plugin_have`/`plugin_require`, `plugin_http_json` with retry). + +--- + +## lib/user-timers-lib.sh — shared systemd user timers + +**File:** `lib/user-timers-lib.sh` (installed to `/usr/local/bin/user-timers-lib.sh`) +**Purpose:** the one copy of the systemd **user** timer machinery used by both the entertainment module and the system scheduler — `ut_interval_to_oncalendar` (interval→`OnCalendar`, incl. raw `OnCalendar=…` passthrough), `ut_interval_label`, `ut_unit_name`, `ut_write_unit_pair` (oneshot service + `Persistent=true` timer, `TimeoutStopSec=5s`, `network-online` deps), and `ut_ensure_linger`. Sourced by `lib/entertainment-lib.sh` and `lib/scheduler-lib.sh`; defines only `ut_*` so it never collides with either. --- diff --git a/DOC/howto/entertainment.md b/DOC/howto/entertainment.md index 062cc01..751ca04 100644 --- a/DOC/howto/entertainment.md +++ b/DOC/howto/entertainment.md @@ -8,8 +8,8 @@ Tools: `config`, `enable`, `disable`, `send`, `status`. | `pos entertainment send` | Fetch a plugin's message to stdout (dry-run) | | `pos entertainment enable [interval]` | Schedule auto-sends | | `pos entertainment disable ` | Remove the schedule | -| `pos entertainment status` | List plugins + their active schedules | -| `pos entertainment config set|get` | Plugin keys in `entertainment.env` | +| `pos entertainment status` | List plugins + active schedules + last run | +| `pos entertainment config set|get|unset|ls|edit` | Plugin keys in `entertainment.env` | ### Plugins @@ -19,8 +19,8 @@ Tools: `config`, `enable`, `disable`, `send`, `status`. | `joke` | icanhazdadjoke (no key) | — | | `gold` | goldprice.dev (no key) | — | -Interval (systemd time): `5m`, `10m`, `30m`, `1h`, `2h`, `6h`, `12h`, `daily`, -`weekly`. +Interval (systemd time): `5m`, `10m`, `15m`, `30m`, `45m`, `1h`, `2h`, `6h`, +`12h`, `daily`, `weekly`. --- @@ -28,6 +28,7 @@ Interval (systemd time): `5m`, `10m`, `30m`, `1h`, `2h`, `6h`, `12h`, `daily`, ```bash pos entertainment config set WEATHER_LAT=36.51 WEATHER_LON=40.75 WEATHER_CITY="Berlin" +pos entertainment config get WEATHER_CITY pos entertainment send weather # stdout test — message for Telegram pos entertainment send joke @@ -37,38 +38,47 @@ pos entertainment enable weather daily # once a day pos entertainment enable joke 2h # every 2 hours pos entertainment disable gold -pos entertainment status # plugins + active schedules +pos entertainment status # plugins + schedules + last run ``` ## How it works - **Scheduling** uses systemd **user** timers only (unit `pos-entertainment-.timer` in `~/.config/systemd/user/`). Interval is - resolved through the same systemd-time parser used by `.timer` units — - invalid values are rejected with a clear message. + resolved through the same `OnCalendar` mapper the system scheduler uses + (`lib/user-timers-lib.sh`) — invalid values are rejected with a clear message. - **Delivery** goes through `notify_send`, so the platform follows `NOTIFY_PLATFORM` (default Telegram). If no platform is configured, enable still works — the plugin fetches and tries to notify, silently no-ops when unconfigured. +- **Last run** — every send (except `--print`) records the plugin's exit code + and timestamp to `~/.local/share/linux_post_install/entertainment/last/`; + `pos entertainment status` shows it per plugin, and a failed timer run also + notifies the configured platforms. +- **Interactive editing** — `pos entertainment config edit` opens the same + numbered-menu editor as `pos config` (already registered for `pos + entertainment`). **Recipes:** - **Morning weather + joke:** `enable weather daily`, `enable joke daily`; the 08:00 health digest plus these make a nice wake-up. - **Add a plugin:** it's a `POS_PLUGIN` script in `entertainment/`; stdout is - the message, `POS_KEYS` lines declare config keys. See + the message, `POS_KEYS` lines declare config keys, and the message-safe + helpers in `lib/entertainment-plugin-lib.sh` remove the boilerplate. See [DOC/DEV.md → "Adding a plugin"](../DEV.md). **Troubleshooting:** - `send weather` prints an error about coordinates → set `WEATHER_LAT`/ `WEATHER_LON` (required keys) via `config set`. - `enable` fails on the interval → the value isn't valid systemd time; use one - of: `5m 10m 30m 1h 2h 6h 12h daily weekly`. + of: `5m 10m 15m 30m 45m 1h 2h 6h 12h daily weekly`. - Nothing arrives even though `status` shows the timer → check `notify.env` (`NOTIFY_PLATFORM`) and that `telegram.env` is configured (see [communication](communication.md)); confirm the timer fired: `systemctl --user list-timers pos-ent-*`. - Public-API flakiness → the plugin outputs a clear failure; the notify call is - silent-fail by design. + silent-fail by design. `pos entertainment status` shows the last failed run + (rc ≠ 0) so a missed timer is visible. --- diff --git a/bin/pos b/bin/pos index 4bb2ed1..acb1d50 100755 --- a/bin/pos +++ b/bin/pos @@ -256,7 +256,7 @@ MAIN_LOG="$LOG_DIR/pos.log" log_cmd() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $* → exit $2" >> "$MAIN_LOG"; } # Commands that read from stdin interactively — only log invocation -INTERACTIVE_CMDS="system-firewall media-mp4 system-backup share-usb-server share-smb-server share-smb-client communication-telegram-listener communication-matrix-listener ai-gemini system-schedule config" +INTERACTIVE_CMDS="system-firewall media-mp4 system-backup share-usb-server share-smb-server share-smb-client communication-telegram-listener communication-matrix-listener ai-gemini system-schedule entertainment-config config" for ((i=n-1; i>=0; i--)); do cmd="pos" diff --git a/bin/pos-entertainment-config b/bin/pos-entertainment-config index 7042558..b2e5001 100755 --- a/bin/pos-entertainment-config +++ b/bin/pos-entertainment-config @@ -27,10 +27,18 @@ keys_section() { usage() { cat <}" "$plugin" "$desc" "$req" + done <<< "$(config_keys "$(plugin_dir)")" + ;; + edit) + shift + source "$(dirname "$0")/../lib/config-ui.sh" 2>/dev/null || source "$(dirname "$0")/config-ui.sh" + cfg_ui entertainment + ;; set) shift [ $# -ge 1 ] || usage diff --git a/bin/pos-entertainment-enable b/bin/pos-entertainment-enable index cf87bab..3d6bbbb 100755 --- a/bin/pos-entertainment-enable +++ b/bin/pos-entertainment-enable @@ -40,7 +40,7 @@ interval="${2:-}" resolve_plugin "$(plugin_dir)" "$plugin" >/dev/null if [ -n "$interval" ]; then - interval_to_oncalendar "$interval" >/dev/null || \ + ut_interval_to_oncalendar "$interval" >/dev/null || \ err "Invalid interval '$interval' (allowed: 5m 10m 15m 30m 45m hourly 2h 6h 12h daily weekly, or OnCalendar=...)" fi diff --git a/bin/pos-entertainment-send b/bin/pos-entertainment-send index ee076bd..da6bd91 100755 --- a/bin/pos-entertainment-send +++ b/bin/pos-entertainment-send @@ -1,10 +1,11 @@ #!/usr/bin/env bash set -euo pipefail -# POS: entertainment send — Run a public-API plugin and send its output via Telegram (default sender) +# POS: entertainment send — Run a public-API plugin and send its output via the configured notify platforms # POS_FLAGS: --print --markdown source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh" source "$(dirname "$0")/../lib/entertainment-lib.sh" 2>/dev/null || source "$(dirname "$0")/entertainment-lib.sh" +source "$(dirname "$0")/../lib/notify.sh" 2>/dev/null || source "$(dirname "$0")/notify.sh" usage() { local dir plugins @@ -17,15 +18,16 @@ usage() { cat < [--print] [--markdown] [plugin args...] -Run an entertainment plugin, capture its output, and send it via Telegram. +Run an entertainment plugin, capture its output, and send it via the configured +notify platforms (NOTIFY_PLATFORM, default Telegram). Plugins are scripts that fetch a public API and print the message to stdout — that stdout is what gets sent. Modes: - (default) Send the plugin output to Telegram (silent) + (default) Send the plugin output via notify_send (silent) --print Print the output locally instead of sending - --markdown Send with Markdown parse_mode (via pos communication telegram sender) + --markdown Send with Markdown parse_mode (via the notify senders) Available plugins: $plugins @@ -71,10 +73,12 @@ script="$(resolve_plugin "$dir" "$plugin")" rc=0 output="$( "$script" "${plugin_args[@]}" )" || rc=$? if [ "$rc" -ne 0 ]; then + [ -n "${INVOCATION_ID:-}" ] && notify_send "⚠️ entertainment '$plugin' failed (exit $rc)" + save_last_run "$plugin" "$rc" "$(printf '%s' "$output" | head -1)" err "Plugin '$plugin' failed (exit $rc)" fi -[ -n "$output" ] || { warn "Plugin '$plugin' produced no output — nothing to send"; exit 0; } +[ -n "$output" ] || { warn "Plugin '$plugin' produced no output — nothing to send"; save_last_run "$plugin" 0 ""; exit 0; } # ── Route the output ──────────────────────────────────────────── if [ "$PRINT" -eq 1 ]; then @@ -82,12 +86,10 @@ if [ "$PRINT" -eq 1 ]; then exit 0 fi -telegram="$(dirname "$0")/pos-communication-telegram-sender" -[ -x "$telegram" ] || telegram="$(command -v pos-communication-telegram-sender 2>/dev/null || true)" -[ -n "$telegram" ] && [ -x "$telegram" ] || err "pos-communication-telegram-sender not found next to this script" +save_last_run "$plugin" 0 "$(printf '%s' "$output" | head -1)" if [ "$MARKDOWN" -eq 1 ]; then - exec "$telegram" send --parse-mode markdown -- "$output" + notify_send --markdown "$output" else - exec "$telegram" send -- "$output" + notify_send "$output" fi diff --git a/bin/pos-entertainment-status b/bin/pos-entertainment-status index ec7704a..56b827d 100755 --- a/bin/pos-entertainment-status +++ b/bin/pos-entertainment-status @@ -24,20 +24,33 @@ esac raw="$(config_value ENABLED)" echo "Config: $CONFIG_FILE" -if [ -z "$raw" ]; then - echo "No plugins enabled." - echo "Enable one with: pos entertainment enable [interval]" - exit 0 -fi - parse_enabled "$raw" echo "Enabled plugins:" +if [ ${#ENABLED_ENTRIES[@]} -eq 0 ]; then + echo " (none — enable one with: pos entertainment enable [interval])" +fi for entry in "${ENABLED_ENTRIES[@]}"; do plugin="${entry%%,*}"; interval="${entry##*,}" [ "$interval" = "$plugin" ] && interval="$DEFAULT_INTERVAL" - printf ' %-12s %s\n' "$plugin" "$(interval_label "$interval")" + printf ' %-12s %-16s last run: %s\n' "$plugin" "$(ut_interval_label "$interval")" "$(last_run_str "$plugin")" done +not_enabled=() +for name in $(list_plugins "$(plugin_dir)"); do + found=0 + for entry in "${ENABLED_ENTRIES[@]}"; do + [ "${entry%%,*}" = "$name" ] && found=1 && break + done + [ "$found" -eq 0 ] && not_enabled+=("$name") +done +if [ ${#not_enabled[@]} -gt 0 ]; then + echo + echo "Installed but not enabled:" + for name in "${not_enabled[@]}"; do + printf ' %s\n' "$name" + done +fi + echo if systemctl --user show-environment >/dev/null 2>&1; then echo "Scheduler: systemd user timers" diff --git a/config/entertainment.env b/config/entertainment.env index d19401c..f03c361 100644 --- a/config/entertainment.env +++ b/config/entertainment.env @@ -12,3 +12,5 @@ # WEATHER_CITY=