feat: entertainment hardening — notify_send delivery, plugin lib, last-run state, config edit; shared user-timers lib
This commit is contained in:
@@ -16,6 +16,8 @@ summary (newest last).
|
|||||||
|
|
||||||
## Done
|
## 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/<plugin>` — 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 <url> [--key <jq>] [-H <header>]` (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 <job>`, `pos entertainment enable <plugin>`, 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** — 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 <job>`, `pos entertainment enable <plugin>`, 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: <name>` 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).
|
- **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: <name>` 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).
|
||||||
|
|||||||
@@ -10,19 +10,19 @@
|
|||||||
|
|
||||||
<!-- GEN:START docmap -->
|
<!-- GEN:START docmap -->
|
||||||
| ## 1. Project Overview | 28–43 |
|
| ## 1. Project Overview | 28–43 |
|
||||||
| ## 2. Directory Structure | 44–189 |
|
| ## 2. Directory Structure | 44–192 |
|
||||||
| ## 3. Installation Flow | 190–241 |
|
| ## 3. Installation Flow | 193–244 |
|
||||||
| ## 4. The `pos` CLI System | 242–311 |
|
| ## 4. The `pos` CLI System | 245–314 |
|
||||||
| ## 5. Shared Library — `lib/common.sh` | 312–343 |
|
| ## 5. Shared Library — `lib/common.sh` | 315–346 |
|
||||||
| ## 6. Docker Compose / ScaleTail | 344–386 |
|
| ## 6. Docker Compose / ScaleTail | 347–389 |
|
||||||
| ## 7. Optional Apps (`apps/`) | 387–416 |
|
| ## 7. Optional Apps (`apps/`) | 390–419 |
|
||||||
| ## 8. Entertainment Module | 417–430 |
|
| ## 8. Entertainment Module | 420–433 |
|
||||||
| ## 9. Systemd Services | 431–442 |
|
| ## 9. Systemd Services | 434–445 |
|
||||||
| ## 10. Configuration Files | 443–469 |
|
| ## 10. Configuration Files | 446–472 |
|
||||||
| ## 11. Coding Conventions | 470–502 |
|
| ## 11. Coding Conventions | 473–505 |
|
||||||
| ## 12. Development Workflow | 503–555 |
|
| ## 12. Development Workflow | 506–558 |
|
||||||
| ## 13. Key File Quick Reference | 556–613 |
|
| ## 13. Key File Quick Reference | 559–618 |
|
||||||
| ## 14. Common Tasks for Agents | 614–643 |
|
| ## 14. Common Tasks for Agents | 619–648 |
|
||||||
<!-- GEN:END docmap -->
|
<!-- GEN:END docmap -->
|
||||||
|
|
||||||
## 1. Project Overview
|
## 1. Project Overview
|
||||||
@@ -53,7 +53,10 @@ Linux_post_install/
|
|||||||
│ ├── common.sh # Shared library (colors, logging, spinner, timer, run, load_system_env)
|
│ ├── 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)
|
│ ├── 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
|
│ ├── 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/
|
├── bin/ # CLI tools — installed to /usr/local/bin/
|
||||||
│ ├── pos # Main dispatcher — smart arg matching to pos-* scripts
|
│ ├── 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-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-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-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-entertainment-status # Show enabled plugins and scheduler state
|
||||||
│ ├── pos-media-mp3 # Download audio as MP3 (yt-dlp)
|
│ ├── pos-media-mp3 # Download audio as MP3 (yt-dlp)
|
||||||
│ ├── pos-media-mp4 # Download video as MP4 (smart/interactive format select)
|
│ ├── 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 | 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 | 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 | 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 |
|
| entertainment | status | `pos-entertainment-status` | Show enabled plugins and scheduler state |
|
||||||
| media | mp3 | `pos-media-mp3` | Download audio as MP3 (yt-dlp) |
|
| media | mp3 | `pos-media-mp3` | Download audio as MP3 (yt-dlp) |
|
||||||
| media | mp4 | `pos-media-mp4` | Download video as MP4 (smart/interactive format select) |
|
| 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
|
## 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).
|
- **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).
|
- **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: <name>` header; a new plugin is auto-discovered.
|
- **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: <name>` 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.
|
- **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 <plugin> [--print] [--markdown]` runs the plugin and delivers via `pos communication telegram sender send`.
|
- **Sending:** `pos entertainment send <plugin> [--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 <plugin> <interval>` 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.
|
- **Auto-trigger:** `pos entertainment enable <plugin> <interval>` 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).
|
- **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/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/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/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/entertainment-lib.sh` | 311 | Entertainment module lib (ENABLED parsing, last-run state, scheduler sync via user-timers-lib) |
|
||||||
| `lib/scheduler-lib.sh` | 830 | Scheduler lib (job parsing, notify policies, per-job user timers, legacy migrate) |
|
| `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-reader` | 58 | Inspect flags (list/status/`--raw`) |
|
||||||
| `bin/flag-set` | 21 | Set a flag (optionally with a value) |
|
| `bin/flag-set` | 21 | Set a flag (optionally with a value) |
|
||||||
| `bin/flag-clear` | 21 | Unset a flag |
|
| `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-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-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-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-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-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-send` | 95 | Run a public-API plugin and send its output via the configured notify platforms |
|
||||||
| `bin/pos-entertainment-status` | 49 | Show enabled plugins and scheduler state |
|
| `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-mp3` | 80 | Download audio as MP3 (yt-dlp) |
|
||||||
| `bin/pos-media-mp4` | 126 | Download video as MP4 (smart/interactive format select) |
|
| `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 |
|
| `bin/pos-network-checkport` | 496 | Check TCP/UDP port reachability (nmap, or bash/nc fallback) + local interface view |
|
||||||
|
|||||||
+18
-12
@@ -20,7 +20,7 @@ How this repo works, how to add features, and what to keep in mind when editing.
|
|||||||
| Phase | Script | Responsibility |
|
| Phase | Script | Responsibility |
|
||||||
|-------|--------|----------------|
|
|-------|--------|----------------|
|
||||||
| Pre | `preinstall.sh` | System packages, apt repos, global binaries (yt-dlp) |
|
| 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 |
|
| Post | `postinstall.sh` | User config (SSH keys, PATH, bash completion), systemd services |
|
||||||
|
|
||||||
Each phase is independent and runs only if the corresponding script exists.
|
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/` |
|
| `bin/` | Daily-use CLI tools and wrappers | `/usr/local/bin/` |
|
||||||
| `apps/<category>/` | Optional desktop app installers | run on demand |
|
| `apps/<category>/` | 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/<app>/` (via postinstall) |
|
| `config/` | Gitignored user config files | `~/.config/<app>/` (via postinstall) |
|
||||||
| `entertainment/` | Public-API plugins for the entertainment module | `/usr/local/bin` (via install.sh Phase 2) |
|
| `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` |
|
| `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
|
## 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 **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 <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. 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
|
### 1. Create the plugin
|
||||||
|
|
||||||
Drop an executable script in `entertainment/<name>.sh` with a `# POS_PLUGIN: <name>` 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: <KEY> <description> (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/<name>.sh` with a `# POS_PLUGIN: <name>` 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: <KEY> <description> (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
|
```bash
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
@@ -215,23 +217,27 @@ set -euo pipefail
|
|||||||
# POS_PLUGIN: myplugin
|
# POS_PLUGIN: myplugin
|
||||||
# POS_KEYS: MYPLUGIN_URL <feed url> (required)
|
# POS_KEYS: MYPLUGIN_URL <feed url> (required)
|
||||||
# POS_KEYS: MYPLUGIN_TAG <filter tag> (optional)
|
# POS_KEYS: MYPLUGIN_TAG <filter 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"
|
plugin_load_config
|
||||||
|
plugin_require MYPLUGIN_URL
|
||||||
data="$(curl -fsS --max-time 20 https://api.example.com/foo)"
|
tag="${MYPLUGIN_TAG:-}"
|
||||||
printf 'Title: %s\n' "$data"
|
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)
|
### 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
|
### 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 <cmd>` and, if apt-available, add to PACKAGES.
|
||||||
|
|
||||||
### 4. Done
|
### 4. Done
|
||||||
|
|
||||||
|
|||||||
+12
-7
@@ -325,19 +325,23 @@ The daemon long-polls `/sync` (30s timeout, per-sync `since` token, compact filt
|
|||||||
### entertainment
|
### entertainment
|
||||||
|
|
||||||
**File:** `bin/pos-entertainment-send`
|
**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 |
|
| Command | Behavior |
|
||||||
|---------|----------|
|
|---------|----------|
|
||||||
| `pos entertainment send` | List available plugins + usage |
|
| `pos entertainment send` | List available plugins + usage |
|
||||||
| `pos entertainment send <plugin> [--print] [--markdown] [args…]` | Run the plugin, send its output to Telegram (silent) |
|
| `pos entertainment send <plugin> [--print] [--markdown] [args…]` | Run the plugin, send its output via `notify_send` (silent) |
|
||||||
| `pos entertainment send <plugin> --print` | Print the output locally; do not send |
|
| `pos entertainment send <plugin> --print` | Print the output locally; do not send |
|
||||||
| `pos entertainment send <plugin> --markdown` | Send with `--parse-mode markdown` (via `pos communication telegram sender`) |
|
| `pos entertainment send <plugin> --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` | 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 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 <plugin> [interval]` | Add plugin to `ENABLED` + schedule it as a systemd user timer |
|
| `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 disable <plugin>` | 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.
|
**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.
|
**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: <name>` 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: <name>` 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 <cmd>`, `plugin_require KEY <desc>`, `plugin_http_json <url> [--key <jq>] [-H <header>]` (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)`:
|
**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-<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.
|
- 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.
|
||||||
|
|
||||||
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`.
|
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/<plugin>`); a run that fails while fired by a timer also notifies the configured platforms.
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
- The runner is headless/timer-friendly — no TTY prompts, exit 0 on success / 1 on failure.
|
- 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.
|
- 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
|
### flags
|
||||||
|
|
||||||
|
|||||||
+10
-3
@@ -38,7 +38,7 @@ The phases:
|
|||||||
| # | Phase | Script/action |
|
| # | Phase | Script/action |
|
||||||
|---|-------|----------------|
|
|---|-------|----------------|
|
||||||
| 1 | preinstall | `preinstall.sh` — apt packages + yt-dlp |
|
| 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 |
|
| 3 | postinstall | `postinstall.sh` — PATH, completion, SSH keys, systemd |
|
||||||
| 4 | scalepoint | Shallow-clones ScaleTail templates to `/usr/local/share/linux_post_install/scale-tail` |
|
| 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) |
|
| 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
|
## lib/entertainment-lib.sh — entertainment module
|
||||||
|
|
||||||
**File:** `lib/entertainment-lib.sh` (installed to `/usr/local/bin/entertainment-lib.sh`)
|
**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/<plugin>`), 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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+20
-10
@@ -8,8 +8,8 @@ Tools: `config`, `enable`, `disable`, `send`, `status`.
|
|||||||
| `pos entertainment send` | Fetch a plugin's message to stdout (dry-run) |
|
| `pos entertainment send` | Fetch a plugin's message to stdout (dry-run) |
|
||||||
| `pos entertainment enable <plugin> [interval]` | Schedule auto-sends |
|
| `pos entertainment enable <plugin> [interval]` | Schedule auto-sends |
|
||||||
| `pos entertainment disable <plugin>` | Remove the schedule |
|
| `pos entertainment disable <plugin>` | Remove the schedule |
|
||||||
| `pos entertainment status` | List plugins + their active schedules |
|
| `pos entertainment status` | List plugins + active schedules + last run |
|
||||||
| `pos entertainment config set|get` | Plugin keys in `entertainment.env` |
|
| `pos entertainment config set|get|unset|ls|edit` | Plugin keys in `entertainment.env` |
|
||||||
|
|
||||||
### Plugins
|
### Plugins
|
||||||
|
|
||||||
@@ -19,8 +19,8 @@ Tools: `config`, `enable`, `disable`, `send`, `status`.
|
|||||||
| `joke` | icanhazdadjoke (no key) | — |
|
| `joke` | icanhazdadjoke (no key) | — |
|
||||||
| `gold` | goldprice.dev (no key) | — |
|
| `gold` | goldprice.dev (no key) | — |
|
||||||
|
|
||||||
Interval (systemd time): `5m`, `10m`, `30m`, `1h`, `2h`, `6h`, `12h`, `daily`,
|
Interval (systemd time): `5m`, `10m`, `15m`, `30m`, `45m`, `1h`, `2h`, `6h`,
|
||||||
`weekly`.
|
`12h`, `daily`, `weekly`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -28,6 +28,7 @@ Interval (systemd time): `5m`, `10m`, `30m`, `1h`, `2h`, `6h`, `12h`, `daily`,
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
pos entertainment config set WEATHER_LAT=36.51 WEATHER_LON=40.75 WEATHER_CITY="Berlin"
|
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 weather # stdout test — message for Telegram
|
||||||
pos entertainment send joke
|
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 enable joke 2h # every 2 hours
|
||||||
pos entertainment disable gold
|
pos entertainment disable gold
|
||||||
|
|
||||||
pos entertainment status # plugins + active schedules
|
pos entertainment status # plugins + schedules + last run
|
||||||
```
|
```
|
||||||
|
|
||||||
## How it works
|
## How it works
|
||||||
|
|
||||||
- **Scheduling** uses systemd **user** timers only (unit
|
- **Scheduling** uses systemd **user** timers only (unit
|
||||||
`pos-entertainment-<plugin>.timer` in `~/.config/systemd/user/`). Interval is
|
`pos-entertainment-<plugin>.timer` in `~/.config/systemd/user/`). Interval is
|
||||||
resolved through the same systemd-time parser used by `.timer` units —
|
resolved through the same `OnCalendar` mapper the system scheduler uses
|
||||||
invalid values are rejected with a clear message.
|
(`lib/user-timers-lib.sh`) — invalid values are rejected with a clear message.
|
||||||
- **Delivery** goes through `notify_send`, so the platform follows
|
- **Delivery** goes through `notify_send`, so the platform follows
|
||||||
`NOTIFY_PLATFORM` (default Telegram). If no platform is configured, enable
|
`NOTIFY_PLATFORM` (default Telegram). If no platform is configured, enable
|
||||||
still works — the plugin fetches and tries to notify, silently no-ops when
|
still works — the plugin fetches and tries to notify, silently no-ops when
|
||||||
unconfigured.
|
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:**
|
**Recipes:**
|
||||||
- **Morning weather + joke:** `enable weather daily`, `enable joke daily`; the
|
- **Morning weather + joke:** `enable weather daily`, `enable joke daily`; the
|
||||||
08:00 health digest plus these make a nice wake-up.
|
08:00 health digest plus these make a nice wake-up.
|
||||||
- **Add a plugin:** it's a `POS_PLUGIN` script in `entertainment/`; stdout is
|
- **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).
|
[DOC/DEV.md → "Adding a plugin"](../DEV.md).
|
||||||
|
|
||||||
**Troubleshooting:**
|
**Troubleshooting:**
|
||||||
- `send weather` prints an error about coordinates → set `WEATHER_LAT`/
|
- `send weather` prints an error about coordinates → set `WEATHER_LAT`/
|
||||||
`WEATHER_LON` (required keys) via `config set`.
|
`WEATHER_LON` (required keys) via `config set`.
|
||||||
- `enable` fails on the interval → the value isn't valid systemd time; use one
|
- `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`
|
- Nothing arrives even though `status` shows the timer → check `notify.env`
|
||||||
(`NOTIFY_PLATFORM`) and that `telegram.env` is configured (see
|
(`NOTIFY_PLATFORM`) and that `telegram.env` is configured (see
|
||||||
[communication](communication.md)); confirm the timer fired:
|
[communication](communication.md)); confirm the timer fired:
|
||||||
`systemctl --user list-timers pos-ent-*`.
|
`systemctl --user list-timers pos-ent-*`.
|
||||||
- Public-API flakiness → the plugin outputs a clear failure; the notify call is
|
- 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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -256,7 +256,7 @@ MAIN_LOG="$LOG_DIR/pos.log"
|
|||||||
log_cmd() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $* → exit $2" >> "$MAIN_LOG"; }
|
log_cmd() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $* → exit $2" >> "$MAIN_LOG"; }
|
||||||
|
|
||||||
# Commands that read from stdin interactively — only log invocation
|
# 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
|
for ((i=n-1; i>=0; i--)); do
|
||||||
cmd="pos"
|
cmd="pos"
|
||||||
|
|||||||
@@ -27,10 +27,18 @@ keys_section() {
|
|||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
Usage: pos entertainment config [set KEY=VALUE ...]
|
Usage: pos entertainment config [get KEY | set KEY=VALUE ... | unset KEY | ls | edit]
|
||||||
|
|
||||||
Show the entertainment config file, or set one or more keys.
|
Manage the entertainment config file.
|
||||||
After 'set', auto-trigger timers are re-synced from ENABLED.
|
|
||||||
|
Subcommands:
|
||||||
|
(none) Show the whole config file + the declared keys reference
|
||||||
|
get KEY Print the current value of one key ('(not set)' if absent)
|
||||||
|
set KEY=V... Set one or more keys (validated first, all-or-nothing)
|
||||||
|
unset KEY Remove a key from the file
|
||||||
|
ls Declared keys with their current values, aligned
|
||||||
|
edit Interactive editor for the entertainment scope
|
||||||
|
After 'set'/'unset'/'edit', auto-trigger timers are re-synced from ENABLED.
|
||||||
|
|
||||||
Config file: $CONFIG_FILE
|
Config file: $CONFIG_FILE
|
||||||
|
|
||||||
@@ -41,9 +49,12 @@ Intervals: 5m 10m 15m 30m 45m hourly 2h 6h 12h daily weekly, or OnCalendar=...
|
|||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
pos entertainment config
|
pos entertainment config
|
||||||
pos entertainment config set ENABLED="weather, 5m gold, 1h"
|
pos entertainment config ls
|
||||||
|
pos entertainment config get WEATHER_LAT
|
||||||
pos entertainment config set WEATHER_LAT=36.51 WEATHER_LON=40.75
|
pos entertainment config set WEATHER_LAT=36.51 WEATHER_LON=40.75
|
||||||
pos entertainment config set ENABLED=""
|
pos entertainment config set ENABLED=""
|
||||||
|
pos entertainment config unset WEATHER_CITY
|
||||||
|
pos entertainment config edit
|
||||||
EOF
|
EOF
|
||||||
exit 0
|
exit 0
|
||||||
}
|
}
|
||||||
@@ -54,6 +65,39 @@ key_known() {
|
|||||||
|
|
||||||
case "${1:-}" in
|
case "${1:-}" in
|
||||||
-h|--help) usage ;;
|
-h|--help) usage ;;
|
||||||
|
get)
|
||||||
|
shift
|
||||||
|
[ $# -eq 1 ] || usage
|
||||||
|
v="$(config_value "$1")"
|
||||||
|
[ -n "$v" ] && printf '%s\n' "$v" || echo "(not set)"
|
||||||
|
;;
|
||||||
|
unset)
|
||||||
|
shift
|
||||||
|
[ $# -eq 1 ] || usage
|
||||||
|
write_config_key "$1" "-"
|
||||||
|
ok "$1 removed from $CONFIG_FILE"
|
||||||
|
sync_timers
|
||||||
|
;;
|
||||||
|
ls)
|
||||||
|
shift
|
||||||
|
maxw=8
|
||||||
|
while IFS= read -r line; do
|
||||||
|
IFS='|' read -r _plugin key _desc _req <<<"$line"
|
||||||
|
[ ${#key} -gt "$maxw" ] && maxw=${#key}
|
||||||
|
done <<< "$(config_keys "$(plugin_dir)")"
|
||||||
|
printf ' %-*s %s\n' "$maxw" "ENABLED" "\"$(config_value ENABLED)\""
|
||||||
|
while IFS= read -r line; do
|
||||||
|
[ -n "$line" ] || continue
|
||||||
|
IFS='|' read -r plugin key desc req <<<"$line"
|
||||||
|
v="$(config_value "$key")"
|
||||||
|
printf ' %-*s %s [%s] %s (%s)\n' "$maxw" "$key" "${v:-<not set>}" "$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)
|
set)
|
||||||
shift
|
shift
|
||||||
[ $# -ge 1 ] || usage
|
[ $# -ge 1 ] || usage
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ interval="${2:-}"
|
|||||||
|
|
||||||
resolve_plugin "$(plugin_dir)" "$plugin" >/dev/null
|
resolve_plugin "$(plugin_dir)" "$plugin" >/dev/null
|
||||||
if [ -n "$interval" ]; then
|
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=...)"
|
err "Invalid interval '$interval' (allowed: 5m 10m 15m 30m 45m hourly 2h 6h 12h daily weekly, or OnCalendar=...)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
+12
-10
@@ -1,10 +1,11 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
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
|
# POS_FLAGS: --print --markdown
|
||||||
|
|
||||||
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"
|
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/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() {
|
usage() {
|
||||||
local dir plugins
|
local dir plugins
|
||||||
@@ -17,15 +18,16 @@ usage() {
|
|||||||
cat <<EOF
|
cat <<EOF
|
||||||
Usage: pos entertainment send <plugin> [--print] [--markdown] [plugin args...]
|
Usage: pos entertainment send <plugin> [--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 —
|
Plugins are scripts that fetch a public API and print the message to stdout —
|
||||||
that stdout is what gets sent.
|
that stdout is what gets sent.
|
||||||
|
|
||||||
Modes:
|
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
|
--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:
|
Available plugins:
|
||||||
$plugins
|
$plugins
|
||||||
@@ -71,10 +73,12 @@ script="$(resolve_plugin "$dir" "$plugin")"
|
|||||||
rc=0
|
rc=0
|
||||||
output="$( "$script" "${plugin_args[@]}" )" || rc=$?
|
output="$( "$script" "${plugin_args[@]}" )" || rc=$?
|
||||||
if [ "$rc" -ne 0 ]; then
|
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)"
|
err "Plugin '$plugin' failed (exit $rc)"
|
||||||
fi
|
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 ────────────────────────────────────────────
|
# ── Route the output ────────────────────────────────────────────
|
||||||
if [ "$PRINT" -eq 1 ]; then
|
if [ "$PRINT" -eq 1 ]; then
|
||||||
@@ -82,12 +86,10 @@ if [ "$PRINT" -eq 1 ]; then
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
telegram="$(dirname "$0")/pos-communication-telegram-sender"
|
save_last_run "$plugin" 0 "$(printf '%s' "$output" | head -1)"
|
||||||
[ -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"
|
|
||||||
|
|
||||||
if [ "$MARKDOWN" -eq 1 ]; then
|
if [ "$MARKDOWN" -eq 1 ]; then
|
||||||
exec "$telegram" send --parse-mode markdown -- "$output"
|
notify_send --markdown "$output"
|
||||||
else
|
else
|
||||||
exec "$telegram" send -- "$output"
|
notify_send "$output"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -24,20 +24,33 @@ esac
|
|||||||
raw="$(config_value ENABLED)"
|
raw="$(config_value ENABLED)"
|
||||||
echo "Config: $CONFIG_FILE"
|
echo "Config: $CONFIG_FILE"
|
||||||
|
|
||||||
if [ -z "$raw" ]; then
|
|
||||||
echo "No plugins enabled."
|
|
||||||
echo "Enable one with: pos entertainment enable <plugin> [interval]"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
parse_enabled "$raw"
|
parse_enabled "$raw"
|
||||||
echo "Enabled plugins:"
|
echo "Enabled plugins:"
|
||||||
|
if [ ${#ENABLED_ENTRIES[@]} -eq 0 ]; then
|
||||||
|
echo " (none — enable one with: pos entertainment enable <plugin> [interval])"
|
||||||
|
fi
|
||||||
for entry in "${ENABLED_ENTRIES[@]}"; do
|
for entry in "${ENABLED_ENTRIES[@]}"; do
|
||||||
plugin="${entry%%,*}"; interval="${entry##*,}"
|
plugin="${entry%%,*}"; interval="${entry##*,}"
|
||||||
[ "$interval" = "$plugin" ] && interval="$DEFAULT_INTERVAL"
|
[ "$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
|
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
|
echo
|
||||||
if systemctl --user show-environment >/dev/null 2>&1; then
|
if systemctl --user show-environment >/dev/null 2>&1; then
|
||||||
echo "Scheduler: systemd user timers"
|
echo "Scheduler: systemd user timers"
|
||||||
|
|||||||
@@ -12,3 +12,5 @@
|
|||||||
# WEATHER_CITY=<label> # optional, shown in the message, e.g. Berlin
|
# WEATHER_CITY=<label> # optional, shown in the message, e.g. Berlin
|
||||||
#
|
#
|
||||||
# Manage auto-triggers with: pos entertainment enable|disable|status
|
# Manage auto-triggers with: pos entertainment enable|disable|status
|
||||||
|
# Inspect/edit keys with: pos entertainment config {get|set|unset|ls|edit}
|
||||||
|
# Last-run state per plugin: ~/.local/share/linux_post_install/entertainment/last/
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ set -euo pipefail
|
|||||||
# POS_PLUGIN: gold
|
# POS_PLUGIN: gold
|
||||||
# Contract: stdout is the message sent by 'pos entertainment send gold'.
|
# Contract: stdout is the message sent by 'pos entertainment send gold'.
|
||||||
|
|
||||||
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"
|
||||||
|
|
||||||
case "${1:-}" in
|
case "${1:-}" in
|
||||||
-h|--help)
|
-h|--help)
|
||||||
@@ -22,13 +25,10 @@ EOF
|
|||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
command -v curl &>/dev/null || err "curl not found"
|
plugin_have curl
|
||||||
command -v jq &>/dev/null || err "jq not found"
|
plugin_have jq
|
||||||
|
|
||||||
if ! json="$(curl -fsS --max-time 20 \
|
json="$(plugin_http_json "https://api.goldprice.dev/v1/prices?symbol=XAU-USD-SPOT")"
|
||||||
"https://api.goldprice.dev/v1/prices?symbol=XAU-USD-SPOT")"; then
|
|
||||||
err "Failed to fetch gold price from goldprice.dev"
|
|
||||||
fi
|
|
||||||
|
|
||||||
price="$(jq -r '.symbols[0].price' <<<"$json")"
|
price="$(jq -r '.symbols[0].price' <<<"$json")"
|
||||||
bid="$(jq -r '.symbols[0].bid' <<<"$json")"
|
bid="$(jq -r '.symbols[0].bid' <<<"$json")"
|
||||||
@@ -37,7 +37,7 @@ when="$(jq -r '.symbols[0].computed_at' <<<"$json")"
|
|||||||
when="${when:0:19}"
|
when="${when:0:19}"
|
||||||
when="${when/T/ }"
|
when="${when/T/ }"
|
||||||
|
|
||||||
[ -n "$price" ] || err "No price received"
|
[ -n "$price" ] || plugin_err "No price received"
|
||||||
|
|
||||||
# XAU spot is quoted per troy ounce (31.1034768 g) — convert to USD/gram.
|
# XAU spot is quoted per troy ounce (31.1034768 g) — convert to USD/gram.
|
||||||
per_g="$(awk -v p="$price" -v o="31.1034768" 'BEGIN{printf "%.2f", p/o}')"
|
per_g="$(awk -v p="$price" -v o="31.1034768" 'BEGIN{printf "%.2f", p/o}')"
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ set -euo pipefail
|
|||||||
# POS_PLUGIN: joke
|
# POS_PLUGIN: joke
|
||||||
# Contract: stdout is the message sent by 'pos entertainment send joke'.
|
# Contract: stdout is the message sent by 'pos entertainment send joke'.
|
||||||
|
|
||||||
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"
|
||||||
|
|
||||||
case "${1:-}" in
|
case "${1:-}" in
|
||||||
-h|--help)
|
-h|--help)
|
||||||
@@ -21,13 +24,9 @@ EOF
|
|||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
command -v curl &>/dev/null || err "curl not found"
|
plugin_have curl
|
||||||
command -v jq &>/dev/null || err "jq not found"
|
plugin_have jq
|
||||||
|
|
||||||
if ! joke="$(curl -fsS --max-time 20 -H 'Accept: application/json' \
|
joke="$(plugin_http_json --key '.joke' -H 'Accept: application/json' "https://icanhazdadjoke.com/")"
|
||||||
https://icanhazdadjoke.com/ | jq -r '.joke')"; then
|
[ -n "$joke" ] || plugin_err "No joke received"
|
||||||
err "Failed to fetch a joke from icanhazdadjoke.com"
|
|
||||||
fi
|
|
||||||
|
|
||||||
[ -n "$joke" ] || err "No joke received"
|
|
||||||
printf '%s\n' "$joke"
|
printf '%s\n' "$joke"
|
||||||
|
|||||||
+40
-57
@@ -9,24 +9,48 @@ set -euo pipefail
|
|||||||
# POS_KEYS: WEATHER_CITY <city label> (optional)
|
# POS_KEYS: WEATHER_CITY <city label> (optional)
|
||||||
# Contract: stdout is the message sent by 'pos entertainment send weather'.
|
# Contract: stdout is the message sent by 'pos entertainment send weather'.
|
||||||
|
|
||||||
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"
|
||||||
|
|
||||||
CONFIG_FILE="$HOME/.config/linux_post_install/entertainment.env"
|
case "${1:-}" in
|
||||||
|
-h|--help)
|
||||||
|
cat <<EOF
|
||||||
|
Usage: pos entertainment send weather
|
||||||
|
|
||||||
load_config() {
|
Current weather via Open-Meteo (no API key).
|
||||||
[ -f "$CONFIG_FILE" ] || return 0
|
|
||||||
local k v
|
Config: $plugin_config_file (chmod 600)
|
||||||
while IFS='=' read -r k v; do
|
WEATHER_LAT, WEATHER_LON Required — coordinates
|
||||||
[ -n "$k" ] || continue
|
WEATHER_CITY Optional label (e.g. "Berlin")
|
||||||
case "$k" in
|
|
||||||
\#*) continue ;;
|
Example:
|
||||||
esac
|
WEATHER_LAT=52.52 WEATHER_LON=13.41 pos entertainment send weather --print
|
||||||
v="${v%\"}"; v="${v#\"}"; v="${v%\'}"; v="${v#\'}"
|
EOF
|
||||||
if [ -z "${!k:-}" ]; then
|
exit 0
|
||||||
export "$k"="$v"
|
;;
|
||||||
fi
|
esac
|
||||||
done < <(grep -E '^[A-Z_]+=' "$CONFIG_FILE" || true)
|
|
||||||
}
|
plugin_have curl
|
||||||
|
plugin_have jq
|
||||||
|
|
||||||
|
plugin_load_config
|
||||||
|
plugin_require WEATHER_LAT
|
||||||
|
plugin_require WEATHER_LON
|
||||||
|
label="${WEATHER_CITY:-$WEATHER_LAT,$WEATHER_LON}"
|
||||||
|
|
||||||
|
json="$(plugin_http_json \
|
||||||
|
"https://api.open-meteo.com/v1/forecast?latitude=${WEATHER_LAT}&longitude=${WEATHER_LON}¤t=temperature_2m,relative_humidity_2m,apparent_temperature,weather_code,wind_speed_10m,is_day")"
|
||||||
|
|
||||||
|
temp="$(jq -r '.current.temperature_2m' <<<"$json")"
|
||||||
|
feels="$(jq -r '.current.apparent_temperature' <<<"$json")"
|
||||||
|
humidity="$(jq -r '.current.relative_humidity_2m' <<<"$json")"
|
||||||
|
code="$(jq -r '.current.weather_code' <<<"$json")"
|
||||||
|
wind="$(jq -r '.current.wind_speed_10m' <<<"$json")"
|
||||||
|
is_day="$(jq -r '.current.is_day' <<<"$json")"
|
||||||
|
unit_temp="$(jq -r '.current_units.temperature_2m' <<<"$json")"
|
||||||
|
unit_wind="$(jq -r '.current_units.wind_speed_10m' <<<"$json")"
|
||||||
|
|
||||||
# First arg: weather code; second arg: is_day (1=day). Prints the emoji.
|
# First arg: weather code; second arg: is_day (1=day). Prints the emoji.
|
||||||
wmo_emoji() {
|
wmo_emoji() {
|
||||||
@@ -61,47 +85,6 @@ wmo_desc() {
|
|||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
case "${1:-}" in
|
|
||||||
-h|--help)
|
|
||||||
cat <<EOF
|
|
||||||
Usage: pos entertainment send weather
|
|
||||||
|
|
||||||
Current weather via Open-Meteo (no API key).
|
|
||||||
|
|
||||||
Config: $CONFIG_FILE (chmod 600)
|
|
||||||
WEATHER_LAT, WEATHER_LON Required — coordinates
|
|
||||||
WEATHER_CITY Optional label (e.g. "Berlin")
|
|
||||||
|
|
||||||
Example:
|
|
||||||
WEATHER_LAT=52.52 WEATHER_LON=13.41 pos entertainment send weather --print
|
|
||||||
EOF
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
command -v curl &>/dev/null || err "curl not found"
|
|
||||||
command -v jq &>/dev/null || err "jq not found"
|
|
||||||
|
|
||||||
load_config
|
|
||||||
|
|
||||||
[ -n "${WEATHER_LAT:-}" ] || err "WEATHER_LAT not set — add it to $CONFIG_FILE"
|
|
||||||
[ -n "${WEATHER_LON:-}" ] || err "WEATHER_LON not set — add it to $CONFIG_FILE"
|
|
||||||
label="${WEATHER_CITY:-$WEATHER_LAT,$WEATHER_LON}"
|
|
||||||
|
|
||||||
if ! json="$(curl -fsS --max-time 20 \
|
|
||||||
"https://api.open-meteo.com/v1/forecast?latitude=${WEATHER_LAT}&longitude=${WEATHER_LON}¤t=temperature_2m,relative_humidity_2m,apparent_temperature,weather_code,wind_speed_10m,is_day")"; then
|
|
||||||
err "Failed to fetch weather from Open-Meteo"
|
|
||||||
fi
|
|
||||||
|
|
||||||
temp="$(jq -r '.current.temperature_2m' <<<"$json")"
|
|
||||||
feels="$(jq -r '.current.apparent_temperature' <<<"$json")"
|
|
||||||
humidity="$(jq -r '.current.relative_humidity_2m' <<<"$json")"
|
|
||||||
code="$(jq -r '.current.weather_code' <<<"$json")"
|
|
||||||
wind="$(jq -r '.current.wind_speed_10m' <<<"$json")"
|
|
||||||
is_day="$(jq -r '.current.is_day' <<<"$json")"
|
|
||||||
unit_temp="$(jq -r '.current_units.temperature_2m' <<<"$json")"
|
|
||||||
unit_wind="$(jq -r '.current_units.wind_speed_10m' <<<"$json")"
|
|
||||||
|
|
||||||
emoji="$(wmo_emoji "$code" "$is_day")"
|
emoji="$(wmo_emoji "$code" "$is_day")"
|
||||||
|
|
||||||
printf '%s %s · %s\n' "$emoji" "$(wmo_desc "$code")" "$label"
|
printf '%s %s · %s\n' "$emoji" "$(wmo_desc "$code")" "$label"
|
||||||
|
|||||||
+1
-1
@@ -115,7 +115,7 @@ if should_run 2 scripts; then
|
|||||||
done
|
done
|
||||||
lib_count=0
|
lib_count=0
|
||||||
lib_names=""
|
lib_names=""
|
||||||
for lf in common.sh flags.sh notify.sh entertainment-lib.sh scheduler-lib.sh config-ui.sh; do
|
for lf in common.sh flags.sh notify.sh entertainment-lib.sh scheduler-lib.sh config-ui.sh user-timers-lib.sh entertainment-plugin-lib.sh; do
|
||||||
run sudo install -m 644 "lib/$lf" "/usr/local/bin/$lf"
|
run sudo install -m 644 "lib/$lf" "/usr/local/bin/$lf"
|
||||||
lib_count=$((lib_count + 1))
|
lib_count=$((lib_count + 1))
|
||||||
lib_names+="$lf "
|
lib_names+="$lf "
|
||||||
|
|||||||
+54
-97
@@ -4,7 +4,6 @@
|
|||||||
|
|
||||||
CONFIG_DIR="$HOME/.config/linux_post_install"
|
CONFIG_DIR="$HOME/.config/linux_post_install"
|
||||||
CONFIG_FILE="$CONFIG_DIR/entertainment.env"
|
CONFIG_FILE="$CONFIG_DIR/entertainment.env"
|
||||||
USER_SYSTEMD_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
|
|
||||||
TIMER_PREFIX="pos-entertainment"
|
TIMER_PREFIX="pos-entertainment"
|
||||||
DEFAULT_INTERVAL="daily"
|
DEFAULT_INTERVAL="daily"
|
||||||
|
|
||||||
@@ -14,6 +13,17 @@ declare -F warn >/dev/null || warn() { echo "[!] $*"; }
|
|||||||
declare -F ok >/dev/null || ok() { echo " OK $*"; }
|
declare -F ok >/dev/null || ok() { echo " OK $*"; }
|
||||||
declare -F log >/dev/null || log() { echo "[+] $*"; }
|
declare -F log >/dev/null || log() { echo "[+] $*"; }
|
||||||
|
|
||||||
|
# Shared systemd **user** timer machinery (interval→OnCalendar mapping, unit
|
||||||
|
# pair writer, linger bootstrap) — the same lib the system scheduler uses, so
|
||||||
|
# the two unit templates never drift apart. Defines USER_SYSTEMD_DIR + ut_*.
|
||||||
|
source "$(dirname "${BASH_SOURCE[0]}")/../lib/user-timers-lib.sh" 2>/dev/null \
|
||||||
|
|| source "$(dirname "${BASH_SOURCE[0]}")/user-timers-lib.sh" 2>/dev/null \
|
||||||
|
|| source "$(dirname "$0")/../lib/user-timers-lib.sh" 2>/dev/null \
|
||||||
|
|| source "$(dirname "$0")/user-timers-lib.sh"
|
||||||
|
|
||||||
|
# Per-plugin last-run state (rc + timestamp + first output line).
|
||||||
|
LAST_RUN_DIR="${LAST_RUN_DIR:-$HOME/.local/share/linux_post_install/entertainment/last}"
|
||||||
|
|
||||||
# ── Config file helpers (file is the source of truth, never sourced) ──
|
# ── Config file helpers (file is the source of truth, never sourced) ──
|
||||||
config_value() {
|
config_value() {
|
||||||
local k="$1" v
|
local k="$1" v
|
||||||
@@ -28,6 +38,14 @@ write_config_key() {
|
|||||||
val="${val//$'\r'/}"
|
val="${val//$'\r'/}"
|
||||||
val="${val%%$'\n'*}"
|
val="${val%%$'\n'*}"
|
||||||
mkdir -p "$CONFIG_DIR"
|
mkdir -p "$CONFIG_DIR"
|
||||||
|
if [ "$val" = "-" ]; then
|
||||||
|
[ -f "$CONFIG_FILE" ] || return 0
|
||||||
|
tmp="$(mktemp)"
|
||||||
|
grep -v "^${key}=" "$CONFIG_FILE" >"$tmp" || true
|
||||||
|
mv "$tmp" "$CONFIG_FILE"
|
||||||
|
chmod 600 "$CONFIG_FILE"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
tmp="$(mktemp)"
|
tmp="$(mktemp)"
|
||||||
grep -v "^${key}=" "$CONFIG_FILE" 2>/dev/null >"$tmp" || true
|
grep -v "^${key}=" "$CONFIG_FILE" 2>/dev/null >"$tmp" || true
|
||||||
printf '%s="%s"\n' "$key" "$val" >>"$tmp"
|
printf '%s="%s"\n' "$key" "$val" >>"$tmp"
|
||||||
@@ -200,96 +218,10 @@ enabled_remove() {
|
|||||||
write_config_key ENABLED "$(render_enabled "${out[@]}")"
|
write_config_key ENABLED "$(render_enabled "${out[@]}")"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Interval → systemd OnCalendar ─────────────────────────────────
|
|
||||||
interval_to_oncalendar() {
|
|
||||||
local i="$1"
|
|
||||||
case "$i" in
|
|
||||||
OnCalendar=*) printf '%s' "${i#OnCalendar=}"; return 0 ;;
|
|
||||||
esac
|
|
||||||
if [[ "$i" =~ ^([0-9]+)m$ ]]; then
|
|
||||||
local n="${BASH_REMATCH[1]}"
|
|
||||||
[ "$n" -ge 1 ] && [ "$n" -le 59 ] || return 1
|
|
||||||
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 '*-*-* 00/%s:00:00' "$n"; return 0
|
|
||||||
fi
|
|
||||||
case "$i" in
|
|
||||||
hourly) printf '*-*-* *:00:00' ;;
|
|
||||||
daily) printf '*-*-* 08:00:00' ;;
|
|
||||||
weekly) printf 'Mon *-*-* 08:00:00' ;;
|
|
||||||
*) return 1 ;;
|
|
||||||
esac
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
interval_label() {
|
|
||||||
case "$1" in
|
|
||||||
daily) echo "daily (08:00)" ;;
|
|
||||||
weekly) echo "weekly (Mon 08:00)" ;;
|
|
||||||
hourly) echo "hourly" ;;
|
|
||||||
OnCalendar=*) echo "${1#OnCalendar=}" ;;
|
|
||||||
*m|*h|*d) echo "every $1" ;;
|
|
||||||
*) echo "$1" ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── systemd user timers ───────────────────────────────────────────
|
# ── systemd user timers ───────────────────────────────────────────
|
||||||
unit_name() { echo "${TIMER_PREFIX}-${1}"; }
|
# Interval → OnCalendar mapping, unit naming, the unit pair writer and linger
|
||||||
|
# bootstrap come from lib/user-timers-lib.sh (ut_*). Reconcile the auto-trigger
|
||||||
write_units() {
|
# schedule with the ENABLED list in the config.
|
||||||
local plugin="$1" oncal="$2"
|
|
||||||
local base="$USER_SYSTEMD_DIR/$(unit_name "$plugin")"
|
|
||||||
local runner
|
|
||||||
runner="$(command -v pos-entertainment-send 2>/dev/null || echo /usr/local/bin/pos-entertainment-send)"
|
|
||||||
mkdir -p "$USER_SYSTEMD_DIR"
|
|
||||||
cat >"$base.service" <<EOF
|
|
||||||
[Unit]
|
|
||||||
Description=pos entertainment send $plugin
|
|
||||||
After=network-online.target
|
|
||||||
Wants=network-online.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=oneshot
|
|
||||||
ExecStart=$runner $plugin
|
|
||||||
TimeoutStopSec=5s
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=timers.target
|
|
||||||
EOF
|
|
||||||
cat >"$base.timer" <<EOF
|
|
||||||
[Unit]
|
|
||||||
Description=Schedule: pos entertainment send $plugin
|
|
||||||
|
|
||||||
[Timer]
|
|
||||||
OnCalendar=$oncal
|
|
||||||
Persistent=true
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=timers.target
|
|
||||||
EOF
|
|
||||||
chmod 644 "$base.service" "$base.timer"
|
|
||||||
}
|
|
||||||
|
|
||||||
ensure_linger() {
|
|
||||||
local user
|
|
||||||
user="$(id -un)"
|
|
||||||
[ "$user" = "root" ] && { warn "running as root — enable linger for your real user: sudo loginctl enable-linger <user>"; return 0; }
|
|
||||||
command -v loginctl >/dev/null 2>&1 || return 0
|
|
||||||
if loginctl show-user "$user" 2>/dev/null | grep -q '^Linger=yes'; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
if sudo -n loginctl enable-linger "$user" 2>/dev/null; then
|
|
||||||
ok "enabled linger for $user (timers fire without login)"
|
|
||||||
else
|
|
||||||
warn "run once so timers fire without login: sudo loginctl enable-linger $user"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Reconcile the auto-trigger schedule with the ENABLED list in the config.
|
|
||||||
# Backend: systemd user timers (requires a reachable user systemd manager).
|
|
||||||
sync_timers() {
|
sync_timers() {
|
||||||
if systemctl --user show-environment >/dev/null 2>&1; then
|
if systemctl --user show-environment >/dev/null 2>&1; then
|
||||||
sync_systemd
|
sync_systemd
|
||||||
@@ -312,24 +244,26 @@ sync_systemd() {
|
|||||||
warn "plugin '$plugin' not installed — skipping"
|
warn "plugin '$plugin' not installed — skipping"
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
if ! oncal="$(interval_to_oncalendar "$interval")"; then
|
if ! oncal="$(ut_interval_to_oncalendar "$interval")"; then
|
||||||
warn "invalid interval '$interval' for '$plugin' — skipping"
|
warn "invalid interval '$interval' for '$plugin' — skipping"
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
wanted["$plugin"]="$oncal"
|
wanted["$plugin"]="$oncal"
|
||||||
done
|
done
|
||||||
|
|
||||||
local wrote=0 t n=0
|
local runner wrote=0 t n=0
|
||||||
|
runner="$(command -v pos-entertainment-send 2>/dev/null || echo /usr/local/bin/pos-entertainment-send)"
|
||||||
for plugin in "${!wanted[@]}"; do
|
for plugin in "${!wanted[@]}"; do
|
||||||
write_units "$plugin" "${wanted[$plugin]}"
|
local base="$USER_SYSTEMD_DIR/$(ut_unit_name "$TIMER_PREFIX" "$plugin")"
|
||||||
|
ut_write_unit_pair "$base.service" "$base.timer" "pos entertainment send $plugin" "$runner $plugin" "${wanted[$plugin]}"
|
||||||
wrote=1; n=$((n + 1))
|
wrote=1; n=$((n + 1))
|
||||||
done
|
done
|
||||||
[ "$wrote" -eq 1 ] && systemctl --user daemon-reload >/dev/null 2>&1 || true
|
[ "$wrote" -eq 1 ] && systemctl --user daemon-reload >/dev/null 2>&1 || true
|
||||||
|
|
||||||
[ "$n" -gt 0 ] && ensure_linger
|
[ "$n" -gt 0 ] && ut_ensure_linger
|
||||||
|
|
||||||
for plugin in "${!wanted[@]}"; do
|
for plugin in "${!wanted[@]}"; do
|
||||||
t="$(unit_name "$plugin").timer"
|
t="$(ut_unit_name "$TIMER_PREFIX" "$plugin").timer"
|
||||||
if systemctl --user is-enabled "$t" >/dev/null 2>&1; then
|
if systemctl --user is-enabled "$t" >/dev/null 2>&1; then
|
||||||
systemctl --user restart "$t" >/dev/null 2>&1 || true
|
systemctl --user restart "$t" >/dev/null 2>&1 || true
|
||||||
else
|
else
|
||||||
@@ -343,8 +277,8 @@ sync_systemd() {
|
|||||||
[ -f "$f" ] || continue
|
[ -f "$f" ] || continue
|
||||||
p="${f##*/}"; p="${p#${TIMER_PREFIX}-}"; p="${p%.timer}"
|
p="${f##*/}"; p="${p#${TIMER_PREFIX}-}"; p="${p%.timer}"
|
||||||
if [ -z "${wanted[$p]:-}" ]; then
|
if [ -z "${wanted[$p]:-}" ]; then
|
||||||
systemctl --user disable --now "$(unit_name "$p").timer" >/dev/null 2>&1 || true
|
systemctl --user disable --now "$(ut_unit_name "$TIMER_PREFIX" "$p").timer" >/dev/null 2>&1 || true
|
||||||
rm -f "$USER_SYSTEMD_DIR/$(unit_name "$p").timer" "$USER_SYSTEMD_DIR/$(unit_name "$p").service"
|
rm -f "$USER_SYSTEMD_DIR/$(ut_unit_name "$TIMER_PREFIX" "$p").timer" "$USER_SYSTEMD_DIR/$(ut_unit_name "$TIMER_PREFIX" "$p").service"
|
||||||
log "removed timer for '$p'"
|
log "removed timer for '$p'"
|
||||||
removed=1
|
removed=1
|
||||||
fi
|
fi
|
||||||
@@ -352,3 +286,26 @@ sync_systemd() {
|
|||||||
[ "$removed" -eq 1 ] && systemctl --user daemon-reload >/dev/null 2>&1 || true
|
[ "$removed" -eq 1 ] && systemctl --user daemon-reload >/dev/null 2>&1 || true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ── Per-plugin last-run state ────────────────────────────────────
|
||||||
|
# Written by 'pos entertainment send' on every non---print run so 'status' can
|
||||||
|
# show whether a scheduled run succeeded. State dir is per-user, not tracked.
|
||||||
|
save_last_run() { # $1 = plugin, $2 = rc, $3 = message (first line)
|
||||||
|
local f="$LAST_RUN_DIR/$1"
|
||||||
|
mkdir -p "$LAST_RUN_DIR"
|
||||||
|
{
|
||||||
|
printf 'rc=%s\n' "$2"
|
||||||
|
printf 'ts=%s\n' "$(date +%s)"
|
||||||
|
printf 'msg=%s\n' "$3"
|
||||||
|
} >"$f"
|
||||||
|
chmod 600 "$f"
|
||||||
|
}
|
||||||
|
|
||||||
|
last_run_str() { # $1 = plugin → "rc=N (MM-DD HH:MM)" or "never"
|
||||||
|
local f="$LAST_RUN_DIR/$1" rc ts
|
||||||
|
[ -f "$f" ] || { echo "never"; return 0; }
|
||||||
|
rc="$(sed -n 's/^rc=//p' "$f" | tail -1)"
|
||||||
|
ts="$(sed -n 's/^ts=//p' "$f" | tail -1)"
|
||||||
|
[ -n "$rc" ] || { echo "never"; return 0; }
|
||||||
|
printf 'rc=%s (%s)' "$rc" "$(date -d "@$ts" '+%m-%d %H:%M' 2>/dev/null || echo '?')"
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# lib/entertainment-plugin-lib.sh — message-safe helpers for entertainment
|
||||||
|
# plugins. Plugins MAY source this (unlike lib/common.sh, whose log/warn/ok
|
||||||
|
# chatter would leak into the sent message). Contract: this lib NEVER writes to
|
||||||
|
# stdout — helpers print errors to stderr and exit non-zero, so stdout stays the
|
||||||
|
# message. Defines only plugin_* names, so it can't collide with common.sh or a
|
||||||
|
# plugin's own helpers.
|
||||||
|
#
|
||||||
|
# Source pattern (works from the repo and from /usr/local/bin after install):
|
||||||
|
# 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"
|
||||||
|
|
||||||
|
# Config file is the source of truth — read it like the pos tools do (never source it).
|
||||||
|
plugin_config_file="${PLUGIN_CONFIG_FILE:-$HOME/.config/linux_post_install/entertainment.env}"
|
||||||
|
|
||||||
|
plugin_err() { echo "ERROR: $*" >&2; exit 1; }
|
||||||
|
|
||||||
|
# Load KEY=VALUE pairs from entertainment.env into the environment.
|
||||||
|
# Env vars already set win (env precedence); quoted values are stripped;
|
||||||
|
# comments and blank lines skipped.
|
||||||
|
plugin_load_config() {
|
||||||
|
[ -f "$plugin_config_file" ] || return 0
|
||||||
|
local k v
|
||||||
|
while IFS='=' read -r k v; do
|
||||||
|
[ -n "$k" ] || continue
|
||||||
|
case "$k" in
|
||||||
|
\#*) continue ;;
|
||||||
|
esac
|
||||||
|
v="${v%\"}"; v="${v#\"}"; v="${v%\'}"; v="${v#\'}"
|
||||||
|
if [ -z "${!k:-}" ]; then
|
||||||
|
export "$k"="$v"
|
||||||
|
fi
|
||||||
|
done < <(grep -E '^[A-Z_]+=' "$plugin_config_file" || true)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fail unless the command is available.
|
||||||
|
plugin_have() {
|
||||||
|
command -v "$1" >/dev/null 2>&1 || plugin_err "$1 not found — install it (see DOC/PREINSTALL.md or preinstall.sh PACKAGES)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fail unless the config key is set (after plugin_load_config).
|
||||||
|
plugin_require() {
|
||||||
|
[ -n "${!1:-}" ] || plugin_err "$1 not set — add it to $plugin_config_file"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fetch a URL as JSON with curl, retrying twice on transient failures, then
|
||||||
|
# extract the given jq key (optional). Everything is silent on stdout; errors
|
||||||
|
# go to stderr and exit non-zero.
|
||||||
|
plugin_http_json() {
|
||||||
|
local key="" url="" json
|
||||||
|
local -a headers=()
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--key) key="$2"; shift 2 ;;
|
||||||
|
--header|-H) headers+=(-H "$2"); shift 2 ;;
|
||||||
|
*) url="$1"; shift ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
[ -n "$url" ] || plugin_err "plugin_http_json: no URL given"
|
||||||
|
json="$(curl -fsS --max-time 20 --retry 2 --retry-delay 2 "${headers[@]}" "$url")" || \
|
||||||
|
plugin_err "Failed to fetch $url"
|
||||||
|
if [ -n "$key" ]; then
|
||||||
|
json="$(jq -r "$key" <<<"$json")" || plugin_err "Failed to parse '$key' from $url"
|
||||||
|
fi
|
||||||
|
printf '%s' "$json"
|
||||||
|
}
|
||||||
+27
-97
@@ -41,6 +41,14 @@ declare -F warn >/dev/null || warn() { echo "[!] $*"; }
|
|||||||
declare -F ok >/dev/null || ok() { echo " OK $*"; }
|
declare -F ok >/dev/null || ok() { echo " OK $*"; }
|
||||||
declare -F log >/dev/null || log() { echo "[+] $*"; }
|
declare -F log >/dev/null || log() { echo "[+] $*"; }
|
||||||
|
|
||||||
|
# Shared systemd **user** timer machinery (interval→OnCalendar mapping, unit
|
||||||
|
# pair writer, linger bootstrap) — the same lib the entertainment module uses,
|
||||||
|
# so the two unit templates never drift apart. Defines USER_SYSTEMD_DIR + ut_*.
|
||||||
|
source "$(dirname "${BASH_SOURCE[0]}")/../lib/user-timers-lib.sh" 2>/dev/null \
|
||||||
|
|| source "$(dirname "${BASH_SOURCE[0]}")/user-timers-lib.sh" 2>/dev/null \
|
||||||
|
|| source "$(dirname "$0")/../lib/user-timers-lib.sh" 2>/dev/null \
|
||||||
|
|| source "$(dirname "$0")/user-timers-lib.sh"
|
||||||
|
|
||||||
# ── Job discovery / naming ───────────────────────────────────────
|
# ── Job discovery / naming ───────────────────────────────────────
|
||||||
sched_valid_name() {
|
sched_valid_name() {
|
||||||
[[ "$1" =~ ^[a-z0-9][a-z0-9_-]*$ ]]
|
[[ "$1" =~ ^[a-z0-9][a-z0-9_-]*$ ]]
|
||||||
@@ -341,92 +349,9 @@ sched_run_job() { # $1 = job name
|
|||||||
[ "${DRY_RUN:-0}" -ne 1 ] && sched_save_run "$1" "$JOB_OUT" "$JOB_RC"
|
[ "${DRY_RUN:-0}" -ne 1 ] && sched_save_run "$1" "$JOB_OUT" "$JOB_RC"
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
# ── Interval → systemd OnCalendar (same list as entertainment) ──
|
|
||||||
sched_interval_to_oncalendar() {
|
|
||||||
local i="$1"
|
|
||||||
case "$i" in
|
|
||||||
OnCalendar=*) printf '%s' "${i#OnCalendar=}"; return 0 ;;
|
|
||||||
esac
|
|
||||||
if [[ "$i" =~ ^([0-9]+)m$ ]]; then
|
|
||||||
local n="${BASH_REMATCH[1]}"
|
|
||||||
[ "$n" -ge 1 ] && [ "$n" -le 59 ] || return 1
|
|
||||||
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 '*-*-* 00/%s:00:00' "$n"; return 0
|
|
||||||
fi
|
|
||||||
case "$i" in
|
|
||||||
hourly) printf '*-*-* *:00:00' ;;
|
|
||||||
daily) printf '*-*-* 08:00:00' ;;
|
|
||||||
weekly) printf 'Mon *-*-* 08:00:00' ;;
|
|
||||||
*) return 1 ;;
|
|
||||||
esac
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
sched_interval_label() {
|
|
||||||
case "$1" in
|
|
||||||
daily) echo "daily (08:00)" ;;
|
|
||||||
weekly) echo "weekly (Mon 08:00)" ;;
|
|
||||||
hourly) echo "hourly" ;;
|
|
||||||
OnCalendar=*) echo "${1#OnCalendar=}" ;;
|
|
||||||
*m|*h) echo "every $1" ;;
|
|
||||||
*) echo "$1" ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── systemd user timers (one pair per job) ──────────────────────
|
# ── systemd user timers (one pair per job) ──────────────────────
|
||||||
sched_unit() { printf '%s-%s\n' "$SCHED_PREFIX" "$1"; }
|
# Interval → OnCalendar mapping, unit naming, the unit pair writer and linger
|
||||||
|
# bootstrap come from lib/user-timers-lib.sh (ut_*).
|
||||||
sched_ensure_linger() {
|
|
||||||
local user
|
|
||||||
user="$(id -un)"
|
|
||||||
[ "$user" = "root" ] && { warn "running as root — enable linger for your real user: sudo loginctl enable-linger <user>"; return 0; }
|
|
||||||
command -v loginctl >/dev/null 2>&1 || return 0
|
|
||||||
if loginctl show-user "$user" 2>/dev/null | grep -q '^Linger=yes'; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
if sudo -n loginctl enable-linger "$user" 2>/dev/null; then
|
|
||||||
ok "enabled linger for $user (timers fire without login)"
|
|
||||||
else
|
|
||||||
warn "run once so timers fire without login: sudo loginctl enable-linger $user"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
sched_write_units() { # $1 = job name, $2 = OnCalendar
|
|
||||||
local name="$1" oncal="$2"
|
|
||||||
local base="$USER_SYSTEMD_DIR/$(sched_unit "$name")"
|
|
||||||
mkdir -p "$USER_SYSTEMD_DIR"
|
|
||||||
cat >"$base.service" <<EOF
|
|
||||||
[Unit]
|
|
||||||
Description=pos system schedule — run job $name
|
|
||||||
After=network-online.target
|
|
||||||
Wants=network-online.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=oneshot
|
|
||||||
ExecStart=$SCHED_RUNNER run $name
|
|
||||||
TimeoutStopSec=5s
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=timers.target
|
|
||||||
EOF
|
|
||||||
cat >"$base.timer" <<EOF
|
|
||||||
[Unit]
|
|
||||||
Description=Schedule: pos system schedule — $name
|
|
||||||
|
|
||||||
[Timer]
|
|
||||||
OnCalendar=$oncal
|
|
||||||
Persistent=true
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=timers.target
|
|
||||||
EOF
|
|
||||||
chmod 644 "$base.service" "$base.timer"
|
|
||||||
}
|
|
||||||
|
|
||||||
sched_job_active() { # $1 = job name — valid AND not disabled
|
sched_job_active() { # $1 = job name — valid AND not disabled
|
||||||
sched_read_job "$1" >/dev/null 2>&1 || return 1
|
sched_read_job "$1" >/dev/null 2>&1 || return 1
|
||||||
[ "$JOB_ENABLED" = "false" ] && return 1
|
[ "$JOB_ENABLED" = "false" ] && return 1
|
||||||
@@ -444,27 +369,32 @@ sched_sync() {
|
|||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
[ "$JOB_ENABLED" = "false" ] && continue # disabled: orphan cleanup drops its timer
|
[ "$JOB_ENABLED" = "false" ] && continue # disabled: orphan cleanup drops its timer
|
||||||
if ! oncal="$(sched_interval_to_oncalendar "$JOB_INTERVAL")"; then
|
if ! oncal="$(ut_interval_to_oncalendar "$JOB_INTERVAL")"; then
|
||||||
warn "invalid interval '$JOB_INTERVAL' for '$name' — skipping"
|
warn "invalid interval '$JOB_INTERVAL' for '$name' — skipping"
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
if [ "${DRY_RUN:-0}" -eq 1 ]; then
|
if [ "${DRY_RUN:-0}" -eq 1 ]; then
|
||||||
log "(dry-run) write $(sched_unit "$name").timer (OnCalendar=$oncal)"
|
log "(dry-run) write $(ut_unit_name "$SCHED_PREFIX" "$name").timer (OnCalendar=$oncal)"
|
||||||
else
|
else
|
||||||
sched_write_units "$name" "$oncal"
|
ut_write_unit_pair \
|
||||||
|
"$USER_SYSTEMD_DIR/$(ut_unit_name "$SCHED_PREFIX" "$name").service" \
|
||||||
|
"$USER_SYSTEMD_DIR/$(ut_unit_name "$SCHED_PREFIX" "$name").timer" \
|
||||||
|
"pos system schedule — run job $name" \
|
||||||
|
"$SCHED_RUNNER run $name" \
|
||||||
|
"$oncal"
|
||||||
fi
|
fi
|
||||||
wrote=1; n=$((n + 1))
|
wrote=1; n=$((n + 1))
|
||||||
done
|
done
|
||||||
[ "${DRY_RUN:-0}" -eq 1 ] && return 0
|
[ "${DRY_RUN:-0}" -eq 1 ] && return 0
|
||||||
[ "$wrote" -eq 1 ] && systemctl --user daemon-reload >/dev/null 2>&1 || true
|
[ "$wrote" -eq 1 ] && systemctl --user daemon-reload >/dev/null 2>&1 || true
|
||||||
[ "$n" -gt 0 ] && sched_ensure_linger
|
[ "$n" -gt 0 ] && ut_ensure_linger
|
||||||
for name in "${jobs[@]}"; do
|
for name in "${jobs[@]}"; do
|
||||||
if ! sched_read_job "$name" >/dev/null 2>&1; then
|
if ! sched_read_job "$name" >/dev/null 2>&1; then
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
[ "$JOB_ENABLED" = "false" ] && continue
|
[ "$JOB_ENABLED" = "false" ] && continue
|
||||||
sched_interval_to_oncalendar "$JOB_INTERVAL" >/dev/null 2>&1 || continue
|
ut_interval_to_oncalendar "$JOB_INTERVAL" >/dev/null 2>&1 || continue
|
||||||
local t="$(sched_unit "$name").timer"
|
local t="$(ut_unit_name "$SCHED_PREFIX" "$name").timer"
|
||||||
if systemctl --user is-enabled "$t" >/dev/null 2>&1; then
|
if systemctl --user is-enabled "$t" >/dev/null 2>&1; then
|
||||||
systemctl --user restart "$t" >/dev/null 2>&1 || true
|
systemctl --user restart "$t" >/dev/null 2>&1 || true
|
||||||
else
|
else
|
||||||
@@ -483,8 +413,8 @@ sched_remove_orphans() {
|
|||||||
[ -f "$f" ] || continue
|
[ -f "$f" ] || continue
|
||||||
p="${f##*/}"; p="${p#${SCHED_PREFIX}-}"; p="${p%.timer}"
|
p="${f##*/}"; p="${p#${SCHED_PREFIX}-}"; p="${p%.timer}"
|
||||||
if ! sched_job_active "$p"; then
|
if ! sched_job_active "$p"; then
|
||||||
systemctl --user disable --now "$(sched_unit "$p").timer" >/dev/null 2>&1 || true
|
systemctl --user disable --now "$(ut_unit_name "$SCHED_PREFIX" "$p").timer" >/dev/null 2>&1 || true
|
||||||
rm -f "$USER_SYSTEMD_DIR/$(sched_unit "$p").timer" "$USER_SYSTEMD_DIR/$(sched_unit "$p").service"
|
rm -f "$USER_SYSTEMD_DIR/$(ut_unit_name "$SCHED_PREFIX" "$p").timer" "$USER_SYSTEMD_DIR/$(ut_unit_name "$SCHED_PREFIX" "$p").service"
|
||||||
log "removed timer for '$p'"
|
log "removed timer for '$p'"
|
||||||
removed=1
|
removed=1
|
||||||
fi
|
fi
|
||||||
@@ -693,8 +623,8 @@ sched_status() {
|
|||||||
local st="enabled" oncal next=""
|
local st="enabled" oncal next=""
|
||||||
[ "$JOB_ENABLED" = "false" ] && st="disabled"
|
[ "$JOB_ENABLED" = "false" ] && st="disabled"
|
||||||
if [ "$st" = "enabled" ]; then
|
if [ "$st" = "enabled" ]; then
|
||||||
if oncal="$(sched_interval_to_oncalendar "$JOB_INTERVAL")"; then
|
if oncal="$(ut_interval_to_oncalendar "$JOB_INTERVAL")"; then
|
||||||
next="$(systemctl --user list-timers "$(sched_unit "$name").timer" --no-legend 2>/dev/null | awk '{print $1, $2}' | head -1)"
|
next="$(systemctl --user list-timers "$(ut_unit_name "$SCHED_PREFIX" "$name").timer" --no-legend 2>/dev/null | awk '{print $1, $2}' | head -1)"
|
||||||
else
|
else
|
||||||
oncal="(invalid interval: $JOB_INTERVAL)"
|
oncal="(invalid interval: $JOB_INTERVAL)"
|
||||||
fi
|
fi
|
||||||
@@ -743,7 +673,7 @@ sched_editor_add() {
|
|||||||
[ -f "$SCHEDULE_DIR/$name.env" ] && { warn "job '$name' already exists — use edit"; return; }
|
[ -f "$SCHEDULE_DIR/$name.env" ] && { warn "job '$name' already exists — use edit"; return; }
|
||||||
read -rp "interval [5m..59m | 1h..23h | hourly daily weekly | OnCalendar=…] (blank = $SCHED_DEFAULT_INTERVAL): " interval
|
read -rp "interval [5m..59m | 1h..23h | hourly daily weekly | OnCalendar=…] (blank = $SCHED_DEFAULT_INTERVAL): " interval
|
||||||
interval="${interval:-$SCHED_DEFAULT_INTERVAL}"
|
interval="${interval:-$SCHED_DEFAULT_INTERVAL}"
|
||||||
sched_interval_to_oncalendar "$interval" >/dev/null 2>&1 || warn "interval '$interval' not recognized — saved anyway, 'enable' will skip it"
|
ut_interval_to_oncalendar "$interval" >/dev/null 2>&1 || warn "interval '$interval' not recognized — saved anyway, 'enable' will skip it"
|
||||||
read -rp "notify [always|onchange|onerror|threshold|never] (blank = onchange): " notify
|
read -rp "notify [always|onchange|onerror|threshold|never] (blank = onchange): " notify
|
||||||
notify="${notify:-onchange}"
|
notify="${notify:-onchange}"
|
||||||
case "$notify" in always|onchange|onerror|threshold|never) ;; *) warn "unknown policy '$notify' — job will run silently" ;; esac
|
case "$notify" in always|onchange|onerror|threshold|never) ;; *) warn "unknown policy '$notify' — job will run silently" ;; esac
|
||||||
@@ -775,7 +705,7 @@ sched_editor_edit() {
|
|||||||
local interval notify msg rule cmd
|
local interval notify msg rule cmd
|
||||||
read -rp "interval (blank keeps, current: $JOB_INTERVAL): " interval
|
read -rp "interval (blank keeps, current: $JOB_INTERVAL): " interval
|
||||||
interval="${interval:-$JOB_INTERVAL}"
|
interval="${interval:-$JOB_INTERVAL}"
|
||||||
sched_interval_to_oncalendar "$interval" >/dev/null 2>&1 || warn "interval '$interval' not recognized — saved anyway, 'enable' will skip it"
|
ut_interval_to_oncalendar "$interval" >/dev/null 2>&1 || warn "interval '$interval' not recognized — saved anyway, 'enable' will skip it"
|
||||||
read -rp "notify (blank keeps, current: $JOB_NOTIFY): " notify
|
read -rp "notify (blank keeps, current: $JOB_NOTIFY): " notify
|
||||||
notify="${notify:-$JOB_NOTIFY}"
|
notify="${notify:-$JOB_NOTIFY}"
|
||||||
read -rp "message (blank keeps, current: ${JOB_MSG:-<none>}): " msg
|
read -rp "message (blank keeps, current: ${JOB_MSG:-<none>}): " msg
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
# lib/user-timers-lib.sh — shared systemd **user** timer machinery for the pos
|
||||||
|
# tools that schedule one-shot jobs: the entertainment module and the system
|
||||||
|
# scheduler. Sourced by lib/entertainment-lib.sh and lib/scheduler-lib.sh AFTER
|
||||||
|
# lib/common.sh. Defines only ut_* helpers so it never collides with either lib.
|
||||||
|
#
|
||||||
|
# Both libs previously re-implemented the same interval→OnCalendar mapping, unit
|
||||||
|
# pair writer and linger bootstrap (the two copies drifted — e.g. the
|
||||||
|
# TimeoutStopSec fix had to be applied to both). This is the single source for:
|
||||||
|
# ut_interval_to_oncalendar ut_interval_label ut_unit_name
|
||||||
|
# ut_write_unit_pair ut_ensure_linger
|
||||||
|
|
||||||
|
# common.sh helpers (guarded so the lib is safe if common.sh wasn't loaded)
|
||||||
|
declare -F err >/dev/null || err() { echo "ERROR: $*" >&2; exit 1; }
|
||||||
|
declare -F warn >/dev/null || warn() { echo "[!] $*"; }
|
||||||
|
declare -F ok >/dev/null || ok() { echo " OK $*"; }
|
||||||
|
declare -F log >/dev/null || log() { echo "[+] $*"; }
|
||||||
|
|
||||||
|
USER_SYSTEMD_DIR="${USER_SYSTEMD_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user}"
|
||||||
|
|
||||||
|
# ── Interval → systemd OnCalendar ─────────────────────────────────
|
||||||
|
# Named + numeric intervals shared by both schedulers, or a raw OnCalendar=…
|
||||||
|
# spec passed through unchanged. Returns non-zero for invalid values.
|
||||||
|
ut_interval_to_oncalendar() {
|
||||||
|
local i="$1"
|
||||||
|
case "$i" in
|
||||||
|
OnCalendar=*) printf '%s' "${i#OnCalendar=}"; return 0 ;;
|
||||||
|
esac
|
||||||
|
if [[ "$i" =~ ^([0-9]+)m$ ]]; then
|
||||||
|
local n="${BASH_REMATCH[1]}"
|
||||||
|
[ "$n" -ge 1 ] && [ "$n" -le 59 ] || return 1
|
||||||
|
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 '*-*-* 00/%s:00:00' "$n"; return 0
|
||||||
|
fi
|
||||||
|
case "$i" in
|
||||||
|
hourly) printf '*-*-* *:00:00' ;;
|
||||||
|
daily) printf '*-*-* 08:00:00' ;;
|
||||||
|
weekly) printf 'Mon *-*-* 08:00:00' ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
ut_interval_label() {
|
||||||
|
case "$1" in
|
||||||
|
daily) echo "daily (08:00)" ;;
|
||||||
|
weekly) echo "weekly (Mon 08:00)" ;;
|
||||||
|
hourly) echo "hourly" ;;
|
||||||
|
OnCalendar=*) echo "${1#OnCalendar=}" ;;
|
||||||
|
*m|*h|*d) echo "every $1" ;;
|
||||||
|
*) echo "$1" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Unit naming / writer ──────────────────────────────────────────
|
||||||
|
ut_unit_name() { # $1 = prefix (e.g. pos-entertainment), $2 = name → prefix-name
|
||||||
|
printf '%s-%s\n' "$1" "$2"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Write one service/timer pair (oneshot, network-online, Persistent).
|
||||||
|
# Usage: ut_write_unit_pair <service-file> <timer-file> <description> <execstart> <oncal>
|
||||||
|
ut_write_unit_pair() {
|
||||||
|
local svc="$1" timer="$2" desc="$3" execstart="$4" oncal="$5"
|
||||||
|
mkdir -p "$USER_SYSTEMD_DIR"
|
||||||
|
cat >"$svc" <<EOF
|
||||||
|
[Unit]
|
||||||
|
Description=$desc
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=$execstart
|
||||||
|
TimeoutStopSec=5s
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
|
EOF
|
||||||
|
cat >"$timer" <<EOF
|
||||||
|
[Unit]
|
||||||
|
Description=Schedule: $desc
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnCalendar=$oncal
|
||||||
|
Persistent=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
|
EOF
|
||||||
|
chmod 644 "$svc" "$timer"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Linger bootstrap ──────────────────────────────────────────────
|
||||||
|
# Timers only fire without a login when the user has linger enabled; try once,
|
||||||
|
# warn with the exact command otherwise (never fails the caller).
|
||||||
|
ut_ensure_linger() {
|
||||||
|
local user
|
||||||
|
user="$(id -un)"
|
||||||
|
[ "$user" = "root" ] && { warn "running as root — enable linger for your real user: sudo loginctl enable-linger <user>"; return 0; }
|
||||||
|
command -v loginctl >/dev/null 2>&1 || return 0
|
||||||
|
if loginctl show-user "$user" 2>/dev/null | grep -q '^Linger=yes'; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if sudo -n loginctl enable-linger "$user" 2>/dev/null; then
|
||||||
|
ok "enabled linger for $user (timers fire without login)"
|
||||||
|
else
|
||||||
|
warn "run once so timers fire without login: sudo loginctl enable-linger $user"
|
||||||
|
fi
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user