fix: entertainment 1h timer never fired (invalid OnCalendar); drop cron backend, systemd-only

This commit is contained in:
Your Name
2026-08-07 13:43:45 -04:00
parent f99acc36a2
commit 81ac553590
9 changed files with 27 additions and 107 deletions
+5
View File
@@ -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
+2 -2
View File
@@ -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 |
+2 -2
View File
@@ -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 <plugin>` (`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 <plugin>` (`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)
+6 -7
View File
@@ -242,7 +242,7 @@ The map file is re-read for every message — edits apply without a restart. The
| `pos entertainment send <plugin> --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 <plugin> [interval]` | Add plugin to `ENABLED` + schedule it (systemd user timer or cron, auto-detected) |
| `pos entertainment enable <plugin> [interval]` | Add plugin to `ENABLED` + schedule it as a systemd user timer |
| `pos entertainment disable <plugin>` | 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 <plugin> [interval]` appends/updates one entry and re-syncs; `pos entertainment disable <plugin>` removes it; `pos entertainment config set ENABLED="…"` replaces the whole list. The scheduler backend is auto-detected on each sync:
`pos entertainment enable <plugin> [interval]` appends/updates one entry and re-syncs; `pos entertainment disable <plugin>` 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-<plugin>.{service,timer}`), running `pos entertainment send <plugin>` 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-<plugin>.{service,timer}`), running `pos entertainment send <plugin>` 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.
+2 -2
View File
@@ -42,8 +42,8 @@ pos entertainment status # plugins + active schedules
## How it works
- **Scheduling** uses systemd **user** timers (unit `pos-ent-<plugin>.timer`,
fallback to cron when systemd user units are unavailable). Interval is
- **Scheduling** uses systemd **user** timers only (unit
`pos-entertainment-<plugin>.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
+1 -1
View File
@@ -11,7 +11,7 @@ Usage: pos entertainment disable <plugin>
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
+2 -3
View File
@@ -11,9 +11,8 @@ Usage: pos entertainment enable <plugin> [interval]
Enable an auto-trigger: writes '<plugin>, <interval>' into the ENABLED list in
$CONFIG_FILE
and schedules 'pos entertainment send <plugin>' 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 <plugin>' 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/^/ /')
+3 -9
View File
@@ -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
+4 -81
View File
@@ -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 <plugin>' manually"
warn "no systemd user manager available — run 'pos entertainment send <plugin>' 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
}