diff --git a/AGENT_TODO.md b/AGENT_TODO.md index 5be6c14..14e045c 100644 --- a/AGENT_TODO.md +++ b/AGENT_TODO.md @@ -47,6 +47,11 @@ summary (newest last). ## Done (summary, newest last) +- 2026-08-06: Fix entertainment timer `1h` not firing — `interval_to_oncalendar` + emitted invalid `OnCalendar=*-*-* */N:00:00` (systemd rejects `*/N` in the hour + field); now `*-*-* 00/N:00:00`. Dropped the cron fallback entirely: scheduling + is systemd user timers only (`sync_cron`/`interval_to_cron`/`cron_block` + removed), `status` simplified, `Nd` intervals rejected with a clear error. - 2026-08-06: Nested `pos` subcommands — `# POS_SUBCMDS:` header annotation (telegram, docker-compose, docker-vbox) + `make gen` emits a `_pos_subcmds` completion map; nested tools (`telegram listener`) auto-list under their parent instead of as a diff --git a/DOC/AGENT_Context_Project.md b/DOC/AGENT_Context_Project.md index b9a2d05..fe7974c 100644 --- a/DOC/AGENT_Context_Project.md +++ b/DOC/AGENT_Context_Project.md @@ -541,9 +541,9 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:` | `bin/pos-docker-vbox` | 158 | Disposable Docker-based VMs (create/enter/start/stop/rm/ls) | | `bin/pos-entertainment-config` | 98 | 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` | 50 | Enable an auto-trigger for a plugin on a schedule | +| `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` | 55 | Show enabled plugins and scheduler state | +| `bin/pos-entertainment-status` | 49 | Show enabled plugins and scheduler state | | `bin/pos-media-mp3` | 35 | Download audio as MP3 (yt-dlp) | | `bin/pos-media-mp4` | 38 | Download video as MP4 (interactive format select) | | `bin/pos-network-checkport` | 45 | Check TCP port connectivity | diff --git a/DOC/DEV.md b/DOC/DEV.md index be35872..ebe4b31 100644 --- a/DOC/DEV.md +++ b/DOC/DEV.md @@ -183,7 +183,7 @@ make check # full self-consistency gate (syntax, exec bits, d ## 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 auto-detected: systemd user timers when a user systemd manager exists, otherwise a managed user crontab block. +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). ### 1. Create the plugin @@ -203,7 +203,7 @@ data="$(curl -fsS --max-time 20 https://api.example.com/foo)" printf 'Title: %s\n' "$data" ``` -**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 cron/systemd timers. +**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. ### 2. Config (if needed) diff --git a/DOC/POS.md b/DOC/POS.md index b7c6fed..33a5468 100644 --- a/DOC/POS.md +++ b/DOC/POS.md @@ -242,7 +242,7 @@ The map file is re-read for every message — edits apply without a restart. The | `pos entertainment send --markdown` | Send with `--parse-mode markdown` (via `pos communication telegram`) | | `pos entertainment config` | Show the config file (`~/.config/linux_post_install/entertainment.env`) | | `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 enable [interval]` | Add plugin to `ENABLED` + schedule it (systemd user timer or cron, auto-detected) | +| `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 | @@ -276,16 +276,15 @@ The plugin itself still reads the keys as plain env vars (`${MYFEED_URL:-}`). Th ENABLED="weather, 5m gold, 1h joke, daily" ``` -`pos entertainment enable [interval]` appends/updates one entry and re-syncs; `pos entertainment disable ` removes it; `pos entertainment config set ENABLED="…"` replaces the whole list. The scheduler backend is auto-detected on each sync: +`pos entertainment enable [interval]` appends/updates one entry and re-syncs; `pos entertainment disable ` removes it; `pos entertainment config set ENABLED="…"` replaces the whole list. Scheduling uses **systemd user timers** (requires a reachable user systemd manager): -- **systemd** (when a user systemd manager is reachable): 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. -- **cron** (fallback when no systemd user manager, e.g. this dev box): a managed block in your user crontab (`# POS-ENTERTAINMENT-BEGIN`…`END`), one line per plugin. Cron runs as your user and fires without login. +- 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. -Either way 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, Telegram token) natively — no `Environment=HOME=` hacks. -Intervals: `5m 10m 15m 30m 45m hourly 2h 6h 12h daily weekly`, or a raw `OnCalendar=…` spec (systemd mode only; cron mode uses the named intervals). Default when omitted: `daily`. +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 detected scheduler, and each plugin's interval + next/next-ish fire time (`systemctl --user list-timers` or the crontab block). +`pos entertainment status` shows the enabled plugins, the scheduler, and each plugin's interval + next fire time (`systemctl --user list-timers`). Notes: - The runner is headless/timer-friendly — no TTY prompts, exit 0 on success / 1 on failure. diff --git a/DOC/howto/entertainment.md b/DOC/howto/entertainment.md index c71a3b8..062cc01 100644 --- a/DOC/howto/entertainment.md +++ b/DOC/howto/entertainment.md @@ -42,8 +42,8 @@ pos entertainment status # plugins + active schedules ## How it works -- **Scheduling** uses systemd **user** timers (unit `pos-ent-.timer`, - fallback to cron when systemd user units are unavailable). Interval is +- **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. - **Delivery** goes through `notify_send`, so the platform follows diff --git a/bin/pos-entertainment-disable b/bin/pos-entertainment-disable index 201e9ce..c65fe1c 100755 --- a/bin/pos-entertainment-disable +++ b/bin/pos-entertainment-disable @@ -11,7 +11,7 @@ Usage: pos entertainment disable Remove a plugin from the ENABLED list in $CONFIG_FILE -and remove its scheduled job (systemd user timer or cron line). +and remove its scheduled systemd user timer. Examples: pos entertainment disable weather diff --git a/bin/pos-entertainment-enable b/bin/pos-entertainment-enable index 71c3af1..cf87bab 100755 --- a/bin/pos-entertainment-enable +++ b/bin/pos-entertainment-enable @@ -11,9 +11,8 @@ Usage: pos entertainment enable [interval] Enable an auto-trigger: writes ', ' into the ENABLED list in $CONFIG_FILE -and schedules 'pos entertainment send ' to run (systemd user timer, or -cron when systemd is unavailable). Re-running without an interval keeps the -plugin's current one. +and schedules 'pos entertainment send ' to run as a systemd user +timer. Re-running without an interval keeps the plugin's current one. Available plugins: $(list_plugins "$(plugin_dir)" | sed 's/^/ /') diff --git a/bin/pos-entertainment-status b/bin/pos-entertainment-status index c7e7d30..ec7704a 100755 --- a/bin/pos-entertainment-status +++ b/bin/pos-entertainment-status @@ -39,17 +39,11 @@ for entry in "${ENABLED_ENTRIES[@]}"; do done echo -echo "Scheduler: $(if systemctl --user show-environment >/dev/null 2>&1; then echo 'systemd user timers'; elif command -v crontab >/dev/null 2>&1; then echo 'cron (user crontab)'; else echo 'none available'; fi)" if systemctl --user show-environment >/dev/null 2>&1; then + echo "Scheduler: systemd user timers" echo "Timers (systemd --user):" systemctl --user list-timers --no-pager 'pos-entertainment-*.timer' 2>/dev/null || \ echo " (no timers)" -elif command -v crontab >/dev/null 2>&1; then - echo "Cron jobs (user crontab):" - block="$(cron_block)" - if [ -n "$block" ]; then - printf '%s\n' "$block" | sed 's/^/ /' - else - echo " (none)" - fi +else + echo "Scheduler: none available (no systemd user manager)" fi diff --git a/lib/entertainment-lib.sh b/lib/entertainment-lib.sh index a0c3a1d..1377f6d 100644 --- a/lib/entertainment-lib.sh +++ b/lib/entertainment-lib.sh @@ -209,17 +209,12 @@ interval_to_oncalendar() { if [[ "$i" =~ ^([0-9]+)m$ ]]; then local n="${BASH_REMATCH[1]}" [ "$n" -ge 1 ] && [ "$n" -le 59 ] || return 1 - printf '*:0/%s:00' "$n"; return 0 + printf '*:00/%s:00' "$n"; return 0 fi if [[ "$i" =~ ^([0-9]+)h$ ]]; then local n="${BASH_REMATCH[1]}" [ "$n" -ge 1 ] && [ "$n" -le 23 ] || return 1 - printf '*-*-* */%s:00:00' "$n"; return 0 - fi - if [[ "$i" =~ ^([0-9]+)d$ ]]; then - local n="${BASH_REMATCH[1]}" - [ "$n" -ge 1 ] && [ "$n" -le 30 ] || return 1 - printf '*-*-*/%s 08:00:00' "$n"; return 0 + printf '*-*-* 00/%s:00:00' "$n"; return 0 fi case "$i" in hourly) printf '*-*-* *:00:00' ;; @@ -293,15 +288,12 @@ ensure_linger() { } # Reconcile the auto-trigger schedule with the ENABLED list in the config. -# Backend is auto-detected: systemd user timers if a user systemd manager is -# reachable, otherwise user crontab (cron fires without login, reads $HOME). +# Backend: systemd user timers (requires a reachable user systemd manager). sync_timers() { if systemctl --user show-environment >/dev/null 2>&1; then sync_systemd - elif command -v crontab >/dev/null 2>&1; then - sync_cron else - warn "no scheduler available (systemd user manager or cron) — run 'pos entertainment send ' manually" + warn "no systemd user manager available — run 'pos entertainment send ' manually" fi } @@ -357,72 +349,3 @@ sync_systemd() { done } -# ── Backend: user crontab (fallback when no systemd user manager) ── -CRON_BEGIN="# POS-ENTERTAINMENT-BEGIN" -CRON_END="# POS-ENTERTAINMENT-END" - -interval_to_cron() { - local i="$1" - case "$i" in - 1h|hourly) printf '0 * * * *'; return 0 ;; - 1d|daily) printf '0 8 * * *'; return 0 ;; - weekly) printf '0 8 * * 1'; return 0 ;; - OnCalendar=*) return 1 ;; - esac - if [[ "$i" =~ ^([0-9]+)m$ ]]; then - local n="${BASH_REMATCH[1]}" - [ "$n" -ge 1 ] && [ "$n" -le 59 ] || return 1 - printf '*/%s * * * *' "$n"; return 0 - fi - if [[ "$i" =~ ^([0-9]+)h$ ]]; then - local n="${BASH_REMATCH[1]}" - [ "$n" -ge 1 ] && [ "$n" -le 23 ] || return 1 - printf '0 */%s * * *' "$n"; return 0 - fi - if [[ "$i" =~ ^([0-9]+)d$ ]]; then - local n="${BASH_REMATCH[1]}" - [ "$n" -ge 1 ] && [ "$n" -le 30 ] || return 1 - printf '0 8 */%s * *' "$n"; return 0 - fi - return 1 -} - -sync_cron() { - local raw entry plugin interval cron runner - raw="$(config_value ENABLED)" - parse_enabled "$raw" - runner="$(command -v pos-entertainment-send 2>/dev/null || echo /usr/local/bin/pos-entertainment-send)" - - local -a lines=() - for entry in "${ENABLED_ENTRIES[@]}"; do - plugin="${entry%%,*}"; interval="${entry##*,}" - [ "$interval" = "$plugin" ] && interval="$DEFAULT_INTERVAL" - if ! plugin_exists "$(plugin_dir)" "$plugin"; then - warn "plugin '$plugin' not installed — skipping" - continue - fi - if ! cron="$(interval_to_cron "$interval")"; then - warn "interval '$interval' cannot be mapped to cron for '$plugin' — skipping (use a named interval)" - continue - fi - lines+=("$cron $runner $plugin") - done - - local head block="" - head="$(crontab -l 2>/dev/null | sed "/^${CRON_BEGIN}$/,/^${CRON_END}$/d" || true)" - [ "${#lines[@]}" -gt 0 ] && block="$CRON_BEGIN"$'\n'"$(printf '%s\n' "${lines[@]}")"$'\n'"$CRON_END" - if [ -n "$block" ]; then - [ -n "$head" ] && head+=$'\n' - head+="$block" - fi - printf '%s\n' "$head" | crontab - 2>/dev/null || \ - err "could not write crontab — check 'crontab -l' permissions" - local jobs - jobs="$(crontab -l 2>/dev/null | sed -n "/^${CRON_BEGIN}$/,/^${CRON_END}$/p" | grep -c '^\*/\|^0 ' || true)" - [ -z "$jobs" ] && jobs=0 - log "cron schedule synced ($jobs job(s))" -} - -cron_block() { - crontab -l 2>/dev/null | sed -n "/^${CRON_BEGIN}$/,/^${CRON_END}$/p" || true -}