feat: pos docker stack — containers grouped by compose stack (project)
gates / consistency-and-conventions (push) Failing after 12s

New bin/pos-docker-stack: docker ps output grouped by Docker Compose
project (stack). Each stack is a sorted section (name, status, ports);
containers without a compose project land in a Standalone group at the
end. Running only by default, -a|--all includes stopped/exited. Status
colored on a terminal; summary line 'Stacks: N containers: N
standalone: N'; exit 0 when empty.

Data via docker ps --format with \x1f delimiters (project label
com.docker.compose.project from compose v2); parsed with awk -F'\x1f'
+ IFS=$'\x1f' read — tab/pipe delimiters are IFS whitespace or appear
in values (DEV.md:213). Dash padding via sed, not tr (multi-byte).
Deps guard (docker) before --help; no stdin.

Docs: POS.md docker row + detail, howto/docker.md table + section,
bin/pos usage EXAMPLES, AGENT_Context Common Tasks row. Verified:
stub suite 23/23, live daemon runs, dispatch, make gen && make check,
make lint 0 FAIL / 0 WARN.
This commit is contained in:
Your Name
2026-08-15 13:01:03 -04:00
parent a09f9cfafa
commit 364c5c3687
7 changed files with 154 additions and 16 deletions
+1
View File
@@ -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).
+19 -15
View File
@@ -10,19 +10,19 @@
<!-- GEN:START docmap -->
| ## 1. Project Overview | 2843 |
| ## 2. Directory Structure | 44194 |
| ## 3. Installation Flow | 195246 |
| ## 4. The `pos` CLI System | 247318 |
| ## 5. Shared Library — `lib/common.sh` | 319350 |
| ## 6. Docker Compose / ScaleTail | 351393 |
| ## 7. Optional Apps (`apps/`) | 394423 |
| ## 8. Entertainment Module | 424437 |
| ## 9. Systemd Services | 438449 |
| ## 10. Configuration Files | 450476 |
| ## 11. Coding Conventions | 477509 |
| ## 12. Development Workflow | 510562 |
| ## 13. Key File Quick Reference | 563625 |
| ## 14. Common Tasks for Agents | 626657 |
| ## 2. Directory Structure | 44195 |
| ## 3. Installation Flow | 196247 |
| ## 4. The `pos` CLI System | 248320 |
| ## 5. Shared Library — `lib/common.sh` | 321352 |
| ## 6. Docker Compose / ScaleTail | 353395 |
| ## 7. Optional Apps (`apps/`) | 396425 |
| ## 8. Entertainment Module | 426439 |
| ## 9. Systemd Services | 440451 |
| ## 10. Configuration Files | 452478 |
| ## 11. Coding Conventions | 479511 |
| ## 12. Development Workflow | 512564 |
| ## 13. Key File Quick Reference | 565628 |
| ## 14. Common Tasks for Agents | 629661 |
<!-- GEN:END docmap -->
## 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) |
<!-- GEN:START filetable -->
| `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 |
<!-- GEN:END filetable -->
| `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` |
+3
View File
@@ -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.
+27 -1
View File
@@ -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 <svc>`, confirm it joined the right
stack:
```bash
pos docker stack | grep -A5 <svc>
```
**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
+2
View File
@@ -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 <url> Download audio as MP3
pos media mp4 <url> Download video as MP4
+101
View File
@@ -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 <<EOF
Usage: pos docker stack [-a|--all]
Show running containers grouped by their Docker Compose stack (project).
Non-compose containers (no stack) are listed under a "Standalone" group
at the end.
Flags:
-a, --all Include stopped/exited containers too (like docker ps -a)
-h, --help Show this help
Exit codes:
0 Success (also when no containers are found)
Examples:
pos docker stack Grouped view of running containers
pos docker stack -a Include stopped/exited containers
EOF
exit 0
}
command -v docker &>/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"
+1
View File
@@ -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"