diff --git a/AGENT_TODO.md b/AGENT_TODO.md index 82d9200..712e926 100644 --- a/AGENT_TODO.md +++ b/AGENT_TODO.md @@ -42,6 +42,7 @@ summary (newest last). ## Done +- **2026-08-15** — `pos docker stack` (`bin/pos-docker-stack`) — containers grouped by their Docker Compose project. Each stack is a section (project name, sorted) with lines `container-name status ports`; containers with no compose project land in a `Standalone` section at the end; ends with `Stacks: N containers: N standalone: N`. Running only by default, `-a|--all` includes stopped/exited (like `docker ps -a`). Status colored on a terminal (`Up*` green, `Exited*`/`Dead*`/`Created*` red, `Paused*`/`Restarting*` yellow); exit 0 also when no containers. Data via `docker ps` with `--format '{{.Names}}{{"\u001f"}}{{.Label "com.docker.compose.project"}}{{"\u001f"}}{{.Status}}{{"\u001f"}}{{.Ports}}'` (compose v2 sets the project label; `{{"\u001f"}}` escapes in the Go template), parsed with `awk -F'\x1f'` + `IFS=$'\x1f' read` everywhere — tab/pipe delimiters are IFS whitespace or inside values, so `\x1f` (DEV.md:213 gotcha); dash padding via `sed` not `tr` (tr corrupts multi-byte `─`). Deps guard (`docker`) before `--help`; no stdin → not in `INTERACTIVE_CMDS`; `# POS_FLAGS: -a --all`. Docs: POS.md docker row + detail, howto/docker.md table + section, `bin/pos` usage EXAMPLES, AGENT_Context §14 row. Verified: stub-PATH suite `/tmp/opencode/docker-stack-test/run-tests.sh` 23/23 (grouping, sorted stacks, `-a` shows exited, standalone, empty daemon rc=0, colored status, missing docker rc=1, `--help` after deps guard); live runs against the real daemon (affine/audiobookshelf/convertx/gitea stacks, `affine_migration_job Exited (0)` + `lab1 Exited (137)` under `-a`); dispatch via `pos docker stack`; `make gen && make check`, `make lint` 0 FAIL / 0 WARN. - **2026-08-15** — Fix `pos media sync` offering a Ventoy stick's **EFI partition** as the sync target: with the data partition unmounted, the 32 MB `VTOYEFI` ESP was the only mounted USB partition, `usb_detect` offered it with no context, and `cp` died mid-copy with `No space left on device` (live-box report). `usb_detect` now fetches `FSTYPE`/`PARTTYPENAME` and excludes EFI system partitions (Ventoy `VTOYEFI`, `/boot/efi`) from **both** the mounted list and the mount-offer list; `USB_MOUNTED` entries carry `mp|label|size|model|fs` and `usb_pick_root` shows that in the single-stick confirm and the multi-stick/partition picker (`1) /media/Ventoy (1.1T, Ventoy, exfat)`), while `USB_ROOT` stays a bare mountpoint (`${root%|*}`) so `pos system backup` (`${root%/}/backups`) is unaffected. `pos-media-sync` gained a pre-flight space check (measures exactly what `needs_copy` would copy vs `df -Pk`, `err`/`warn` before any copy) — no more mid-copy ENOSPC. Docs: howto/media.md target-picking note, SCRIPTS.md usb-lib paragraph, AGENT_Context hand-maintained lib row (194→205). Verified: new stub harness `/tmp/opencode/vtoyefi-run.sh` (ESP filtered from mounted + mount-offer, multi-pick shows only the data partition, space fit/too-small/dry-run-warn) green; `/tmp/opencode/backup-test` still green; live check `printf 'n\ns\n' | bash bin/pos-media-sync --mp3` no longer offers VTOYEFI (offers unmounted `sda1` Ventoy instead); `make gen && make check`, `make lint` 0 FAIL / 0 WARN. - **2026-08-15** — Fix `pos media sync` reporting success with 0 files when the source is a symlink: it enumerated with plain `find "$SRC"`, and GNU find (default `-P`) does not descend a command-line symlink to a directory — `~/Music -> /mnt/hdd/…/music` therefore yielded zero matches, the loop never ran, and the tool printed `0 added, 0 updated, 0 unchanged` without creating the target dir (live-box report). Switched to `find -H "$SRC"` (follows only command-line symlinks; inner-symlink semantics unchanged). howto/media.md sync section notes symlinked sources are followed. Caught live, not by the 46-case stub suite (which used a real temp dir source — lesson: add a symlink-root fixture). Verified: `printf 'y\n' | bash bin/pos-media-sync --mp3 --dry-run` now lists all 31 mp3s as "would copy"; `make gen && make check` green. - **2026-08-14** — `pos system backup` — smart USB detection: lsblk TRAN (lsusb/by-id cross-check), mount offer for plugged-in-but-unmounted sticks, sha256-verified copy (stub-suite 54/54). diff --git a/DOC/AGENT_Context_Project.md b/DOC/AGENT_Context_Project.md index 62d9e1d..754c72b 100644 --- a/DOC/AGENT_Context_Project.md +++ b/DOC/AGENT_Context_Project.md @@ -10,19 +10,19 @@ | ## 1. Project Overview | 28–43 | -| ## 2. Directory Structure | 44–194 | -| ## 3. Installation Flow | 195–246 | -| ## 4. The `pos` CLI System | 247–318 | -| ## 5. Shared Library — `lib/common.sh` | 319–350 | -| ## 6. Docker Compose / ScaleTail | 351–393 | -| ## 7. Optional Apps (`apps/`) | 394–423 | -| ## 8. Entertainment Module | 424–437 | -| ## 9. Systemd Services | 438–449 | -| ## 10. Configuration Files | 450–476 | -| ## 11. Coding Conventions | 477–509 | -| ## 12. Development Workflow | 510–562 | -| ## 13. Key File Quick Reference | 563–625 | -| ## 14. Common Tasks for Agents | 626–657 | +| ## 2. Directory Structure | 44–195 | +| ## 3. Installation Flow | 196–247 | +| ## 4. The `pos` CLI System | 248–320 | +| ## 5. Shared Library — `lib/common.sh` | 321–352 | +| ## 6. Docker Compose / ScaleTail | 353–395 | +| ## 7. Optional Apps (`apps/`) | 396–425 | +| ## 8. Entertainment Module | 426–439 | +| ## 9. Systemd Services | 440–451 | +| ## 10. Configuration Files | 452–478 | +| ## 11. Coding Conventions | 479–511 | +| ## 12. Development Workflow | 512–564 | +| ## 13. Key File Quick Reference | 565–628 | +| ## 14. Common Tasks for Agents | 629–661 | ## 1. Project Overview @@ -70,6 +70,7 @@ Linux_post_install/ │ ├── pos-docker-compose # Docker Compose service manager (ls/up/down/restart/logs/update/config) │ ├── pos-docker-health # One-glance container health dashboard (exits 1 if unhealthy) │ ├── pos-docker-ps # Enhanced container overview (health, IPs, ports, uptime) +│ ├── pos-docker-stack # Containers grouped by compose stack (project); standalone group; -a/--all includes stopped │ ├── pos-docker-vbox # Disposable Docker-based VMs (create/enter/start/stop/rm/ls) │ ├── 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) @@ -272,6 +273,7 @@ All non-interactive `pos` commands log output to `~/.local/share/linux_post_inst | docker | compose | `pos-docker-compose` | Docker Compose service manager (ls/up/down/restart/logs/update/config) | | docker | health | `pos-docker-health` | One-glance container health dashboard (exits 1 if unhealthy) | | docker | ps | `pos-docker-ps` | Enhanced container overview (health, IPs, ports, uptime) | +| docker | stack | `pos-docker-stack` | Containers grouped by compose stack (project); standalone group; -a/--all includes stopped | | docker | vbox | `pos-docker-vbox` | Disposable Docker-based VMs (create/enter/start/stop/rm/ls) | | 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) | @@ -581,7 +583,7 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:` | `features/autostart.sh` | 50 | Boot-time feature (moved from `bin/`, flag-gated service) | | `features/usb-automount.sh` | 138 | USB automount feature (udev rule + flag-gated service) | -| `bin/pos` | 292 | CLI dispatcher with smart arg matching + logging + category help | +| `bin/pos` | 294 | CLI dispatcher with smart arg matching + logging + category help | | `bin/pos-ai-gemini` | 311 | Chat with Google Gemini (ask, chat, models, sessions) | | `bin/pos-communication-matrix-listener` | 568 | Matrix listener: map /command → bash, run them on room messages | | `bin/pos-communication-matrix-sender` | 224 | Send messages to a Matrix room via the client-server API (send, test, login) | @@ -591,6 +593,7 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:` | `bin/pos-docker-compose` | 366 | Docker Compose service manager (ls/up/down/restart/logs/update/config) | | `bin/pos-docker-health` | 107 | One-glance container health dashboard (exits 1 if unhealthy) | | `bin/pos-docker-ps` | 125 | Enhanced container overview (health, IPs, ports, uptime) | +| `bin/pos-docker-stack` | 101 | Containers grouped by compose stack (project); standalone group; -a/--all includes stopped | | `bin/pos-docker-vbox` | 158 | Disposable Docker-based VMs (create/enter/start/stop/rm/ls) | | `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) | @@ -617,7 +620,7 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:` | `bin/pos-system-schedule` | 81 | Scheduled jobs: run a command on a timer; notify on threshold/change/error/always or silently | | `bin/pos-config` | 80 | Interactive editor for the tools' runtime config (reads # POS_CONFIG: registry) | | `bin/pos-tree` | 112 | Show the pos CLI command tree: categories, commands, and subcommands | -| `completions/pos.bash` | 293 | Dynamic bash completion | +| `completions/pos.bash` | 294 | Dynamic bash completion | | `apps/install.sh` | 171 | App install/uninstall picker/orchestrator | @@ -641,6 +644,7 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:` | Add bash completion | Edit `completions/pos.bash` | | Modify Docker Compose logic | Edit `bin/pos-docker-compose` | | Modify Docker health check | Edit `bin/pos-docker-health` | +| Modify Docker stack (grouped container) view | Edit `bin/pos-docker-stack` | | Modify vbox (Docker VM) logic | Edit `bin/pos-docker-vbox` | | Modify USB forwarding logic | Edit `bin/pos-share-usb-server` | | Modify aria2 download daemon / queue logic | Edit `bin/pos-network-download` | diff --git a/DOC/POS.md b/DOC/POS.md index 83add30..9cd53e9 100644 --- a/DOC/POS.md +++ b/DOC/POS.md @@ -143,8 +143,11 @@ Runs a persistent `aria2c` JSON-RPC daemon (`localhost:6800`) as a **systemd use |---------|------|---------|---------------| | `pos docker ps` | `bin/pos-docker-ps` | Enhanced container list: name, image, health, uptime, IPs, ports, ID, plus a healthy/unhealthy summary | None. Requires Docker + Python 3 | | `pos docker health` | `bin/pos-docker-health` | One-glance health dashboard; **exits 1** if any container is unhealthy | None. Checks all containers including stopped ones | +| `pos docker stack [-a]` | `bin/pos-docker-stack` | Containers grouped by compose stack (project); `-a` includes stopped; non-compose containers under `Standalone` | None. Requires Docker | | `pos docker compose …` | `bin/pos-docker-compose` | ScaleTail service manager | See [Docker Compose / ScaleTail](#docker-compose--scaletail) below | +**`pos docker stack [-a|--all]`** — containers grouped by their Docker Compose project (stack). Each stack is a section (project name, sorted) whose lines show container name, status, and port mappings (`-` when none — e.g. ScaleTail sidecar containers). Containers with no compose project land in a `Standalone` section at the end. Default shows running containers; `-a|--all` includes stopped/exited (like `docker ps -a`). Status is colored when output goes to a terminal: `Up*` green, `Exited*`/`Dead*`/`Created*` red, `Paused*`/`Restarting*` yellow. Ends with a summary line (`Stacks: N containers: N standalone: N`); exit 0 even when no containers exist. + #### Docker Compose / ScaleTail **`pos docker compose ls`** — list available ScaleTail service templates. diff --git a/DOC/howto/docker.md b/DOC/howto/docker.md index 94ea0d5..87cba91 100644 --- a/DOC/howto/docker.md +++ b/DOC/howto/docker.md @@ -1,13 +1,14 @@ # How-To: `pos docker` Manage self-hosted services, watch containers, and spin up disposable VMs. -Tools: `compose`, `ps`, `health`, `vbox`. +Tools: `compose`, `ps`, `health`, `stack`, `vbox`. | Tool | What it does | |------|--------------| | `pos docker compose` | ScaleTail service manager (deploy/stop/logs/update) | | `pos docker ps` | Enhanced container overview (health, IPs, ports, uptime) | | `pos docker health` | One-glance health dashboard (exits 1 if unhealthy) | +| `pos docker stack` | Containers grouped by compose stack (`-a` includes stopped) | | `pos docker vbox` | Disposable Docker containers as lightweight VMs | --- @@ -100,6 +101,31 @@ not unhealthy. --- +## `pos docker stack` — containers by compose stack + +```bash +pos docker stack # running containers grouped by compose project +pos docker stack -a # include stopped/exited containers +``` + +Grouped view of your compose deployments: every stack is a section with its +services (container name, status, ports); containers started outside compose +land in a `Standalone` group at the end. `-a` behaves like `docker ps -a` and +also shows exited services (e.g. one-shot migration jobs that `docker ps` +hides). + +**Recipe:** after `pos docker compose up `, confirm it joined the right +stack: + +```bash +pos docker stack | grep -A5 +``` + +**Recipe:** spot what's restarting across all stacks at a glance (yellow +`Restarting` / red `Exited` statuses stand out on a terminal). + +--- + ## `pos docker vbox` — disposable VMs Docker containers with a bind-mounted host dir so files persist even after the diff --git a/bin/pos b/bin/pos index e283497..9a41370 100755 --- a/bin/pos +++ b/bin/pos @@ -148,6 +148,8 @@ EXAMPLES pos docker compose ls List available ScaleTail services pos docker compose up jellyfin Deploy a service with Tailscale pos docker health One-glance health dashboard + pos docker stack Containers grouped by compose stack + pos docker stack -a Include stopped/exited containers pos media mp3 Download audio as MP3 pos media mp4 Download video as MP4 diff --git a/bin/pos-docker-stack b/bin/pos-docker-stack new file mode 100755 index 0000000..1f86f38 --- /dev/null +++ b/bin/pos-docker-stack @@ -0,0 +1,101 @@ +#!/usr/bin/env bash +set -euo pipefail +# POS: docker stack — Containers grouped by compose stack (project); standalone group; -a/--all includes stopped +# POS_FLAGS: -a --all +source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh" + +usage() { + cat </dev/null || err "docker not found — run 'install.sh --apps' and install Docker first" + +ALL=0 +case "${1:-}" in + -h|--help) usage ;; + -a|--all) ALL=1 ;; +esac + +ps_args=() +[ "$ALL" -eq 1 ] && ps_args+=(-a) +ps_args+=(--format '{{.Names}}{{"\u001f"}}{{.Label "com.docker.compose.project"}}{{"\u001f"}}{{.Status}}{{"\u001f"}}{{.Ports}}') + +if ! data="$(docker ps "${ps_args[@]}" 2>/dev/null)"; then + err "docker daemon not reachable — is it running?" +fi + +if [ -z "$data" ]; then + echo "No containers." + exit 0 +fi + +# Compose projects (stack names), sorted; empty project label = standalone. +projects="$(printf '%s\n' "$data" | awk -F'\x1f' 'NF>=2 && $2!="" {print $2}' | LC_ALL=C sort -u)" +standalone_rows="$(printf '%s\n' "$data" | awk -F'\x1f' 'NF>=2 && $2==""')" + +# Column width for aligned output (global so all stacks line up). +width=0 +while IFS=$'\x1f' read -r name _proj _status _ports; do + [ "${#name}" -gt "$width" ] && width="${#name}" +done <<< "$data" + +show_rows() { + local rows="$1" + [ -z "$rows" ] && return 0 + while IFS=$'\x1f' read -r name _proj status ports; do + [ -z "$name" ] && continue + case "$status" in + Up*) sdisp="${GREEN}${status}${RESET}" ;; + Exited*|Dead*|Created*) sdisp="${RED}${status}${RESET}" ;; + Paused*|Restarting*) sdisp="${YELLOW}${status}${RESET}" ;; + *) sdisp="$status" ;; + esac + printf " %-*s %b %s\n" "$width" "$name" "$sdisp" "${ports:--}" + done <<< "$rows" +} + +stack_header() { + local label="$1" + local pad=$((46 - ${#label})) + [ "$pad" -lt 1 ] && pad=1 + printf -- "── %s%s%s %s\n" "$CYAN" "$label" "$RESET" "$(printf '%*s' "$pad" '' | sed 's/ /─/g')" +} + +stacks=0 +while IFS= read -r proj; do + [ -z "$proj" ] && continue + stacks=$((stacks + 1)) + rows="$(printf '%s\n' "$data" | awk -F'\x1f' -v p="$proj" '$2==p')" + stack_header "$proj" + show_rows "$rows" + echo +done <<< "$projects" + +if [ -n "$standalone_rows" ]; then + stack_header "Standalone" + show_rows "$standalone_rows" + echo +fi + +containers="$(printf '%s\n' "$data" | grep -c . || true)" +standalone="$(printf '%s\n' "$standalone_rows" | grep -c . || true)" +printf '%*s\n' 60 '' | sed 's/ /─/g' +echo "Stacks: $stacks containers: $containers standalone: $standalone" diff --git a/completions/pos.bash b/completions/pos.bash index be88ae9..fee6e61 100644 --- a/completions/pos.bash +++ b/completions/pos.bash @@ -7,6 +7,7 @@ _pos_flags[ai-gemini]="--model --session --system" _pos_flags[communication-matrix-listener]="--enable --disable --status --run" _pos_flags[communication-telegram-listener]="--enable --disable --status --sync-commands --run" _pos_flags[communication-telegram-sender]="--type --caption --parse-mode --no-preview --token --chat-id --markdown" +_pos_flags[docker-stack]="-a --all" _pos_flags[entertainment-send]="--print --markdown" _pos_flags[media-mp3]="--output --no-playlist --cookies --by-artist --dry-run" _pos_flags[media-mp4]="--format --best --worst --output --no-playlist --cookies --dry-run"