#!/bin/bash # actions.sh APP — one real action per heavy app, run INSIDE the guest, through the app's own front # door (its container address on the Docker network; the setup gate sits in traefik, in front of it). # Content is in /root/sizing/content (made by make_content.py: generated or CC0, no personal data). # PW (env) is a password generated for this run; it is never printed and never stored. # Every line it prints is evidence; it prints no secret. set -u APP=${1:?app}; C=/root/sizing/content; PW=${PW:?pw} ip() { docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}} {{end}}' "$1" | awk '{print $1}'; } src() { docker inspect -f "{{range .Mounts}}{{if eq .Destination \"$2\"}}{{.Source}}{{end}}{{end}}" "$1"; } waitup() { for i in $(seq 1 120); do c=$(curl -s -o /dev/null -w '%{http_code}' "$1"); case $c in 200|302|401) echo "up after $((i*5))s ($c)"; return 0;; esac; sleep 5; done; echo "never up ($c)"; return 1; } ts() { echo "@$(date +%s) $*"; } case $APP in immich) U=http://$(ip immich-server):2283; waitup $U/api/server/ping curl -s -o /dev/null -w 'admin-sign-up %{http_code}\n' -X POST $U/api/auth/admin-sign-up -H 'Content-Type: application/json' \ -d "{\"email\":\"bench@example.invalid\",\"password\":\"$PW\",\"name\":\"Bench\"}" T=$(curl -s -X POST $U/api/auth/login -H 'Content-Type: application/json' -d "{\"email\":\"bench@example.invalid\",\"password\":\"$PW\"}" | grep -o '"accessToken":"[^"]*"' | cut -d'"' -f4) echo "token length ${#T}" ts action_start upload ok=0; bad=0 for f in $C/photos/*.jpg; do c=$(curl -s -o /tmp/immich-up.json -w '%{http_code}' -H "Authorization: Bearer $T" -F "deviceAssetId=$(basename $f)" -F deviceId=bench \ -F fileCreatedAt=2026-01-01T00:00:00.000Z -F fileModifiedAt=2026-01-01T00:00:00.000Z -F "assetData=@$f" $U/api/assets) case $c in 200|201) ok=$((ok+1));; *) bad=$((bad+1)); [ $bad -le 3 ] && echo "upload $(basename $f) -> $c $(head -c 200 /tmp/immich-up.json)";; esac done ts upload_done ok=$ok bad=$bad # wait until every job queue is empty (thumbnails, metadata, smart search, faces) for i in $(seq 1 720); do J=$(curl -s -H "Authorization: Bearer $T" $U/api/jobs); [ -z "$J" ] || echo "$J" | grep -q '"statusCode":404' && J=$(curl -s -H "Authorization: Bearer $T" $U/api/queues) n=$(echo "$J" | grep -oE '"(active|waiting|delayed)":[0-9]+' | awk -F: '{s+=$2} END{print s+0}') [ $((i % 12)) -eq 1 ] && echo " t=$((i*5))s jobs pending=$n" if [ "$n" = 0 ] && [ $i -gt 6 ]; then break; fi sleep 5 done ts action_end jobs-idle curl -s -H "Authorization: Bearer $T" $U/api/server/statistics | head -c 300; echo curl -s -H "Authorization: Bearer $T" "$U/api/people" | grep -oE '"total":[0-9]+' | head -1 ;; jellyfin) U=http://$(ip jellyfin):8096; waitup $U/health M=$(src jellyfin /media); echo "media source: $M"; mkdir -p "$M/movies"; cp -r "$C/video/Bench Films/." "$M/movies/"; ls "$M/movies" H='Content-Type: application/json' curl -s -o /dev/null -w 'startup-config %{http_code}\n' -X POST $U/Startup/Configuration -H "$H" -d '{"UICulture":"en-US","MetadataCountryCode":"US","PreferredMetadataLanguage":"en"}' curl -s -o /dev/null $U/Startup/User curl -s -o /dev/null -w 'startup-user %{http_code}\n' -X POST $U/Startup/User -H "$H" -d "{\"Name\":\"bench\",\"Password\":\"$PW\"}" curl -s -o /dev/null -w 'startup-complete %{http_code}\n' -X POST $U/Startup/Complete A='Authorization: MediaBrowser Client="bench", Device="bench", DeviceId="sizing-bench", Version="1.0"' T=$(curl -s -X POST $U/Users/AuthenticateByName -H "$H" -H "$A" -d "{\"Username\":\"bench\",\"Pw\":\"$PW\"}" | grep -o '"AccessToken":"[^"]*"' | cut -d'"' -f4) echo "token length ${#T}" ts action_start scan curl -s -o /dev/null -w 'add-library %{http_code}\n' -X POST "$U/Library/VirtualFolders?name=Films&collectionType=movies&refreshLibrary=true" \ -H "$H" -H "X-Emby-Token: $T" -d '{"LibraryOptions":{"PathInfos":[{"Path":"/media/movies"}]}}' for i in $(seq 1 120); do n=$(curl -s -H "X-Emby-Token: $T" "$U/Items?Recursive=true&IncludeItemTypes=Movie" | grep -oE '"TotalRecordCount":[0-9]+' | cut -d: -f2) run=$(curl -s -H "X-Emby-Token: $T" "$U/ScheduledTasks?IsHidden=false" | grep -o '"State":"Running"' | wc -l) [ "${n:-0}" -ge 3 ] && [ "$run" = 0 ] && break; sleep 5 done ts scan_done movies=${n:-0} ID=$(curl -s -H "X-Emby-Token: $T" "$U/Items?Recursive=true&IncludeItemTypes=Movie&Limit=1" | grep -oE '"Id":"[0-9a-f]{32}"' | head -1 | cut -d'"' -f4) ts transcode_start curl -s -o /tmp/jf.mp4 -w 'transcode %{http_code} %{size_download} bytes %{time_total}s\n' \ "$U/Videos/$ID/stream.mp4?static=false&mediaSourceId=$ID&videoCodec=h264&audioCodec=aac&maxWidth=1920&videoBitRate=3000000&audioBitRate=128000&api_key=$T" rm -f /tmp/jf.mp4 ts action_end transcode_done ;; paperless-ngx) U=http://$(ip paperless-webserver):8000; waitup $U/api/ for i in $(seq 1 60); do c=$(curl -s -o /dev/null -w '%{http_code}' -u "admin:$PW" $U/api/documents/); [ "$c" = 200 ] && break; sleep 5; done; echo "api auth $c" ts action_start ocr for f in $C/pdfs/*.pdf; do curl -s -o /dev/null -w "%{http_code} " -u "admin:$PW" -F "document=@$f" $U/api/documents/post_document/; done; echo for i in $(seq 1 360); do n=$(curl -s -u "admin:$PW" "$U/api/documents/?page_size=1" | grep -oE '"count":[0-9]+' | cut -d: -f2) p=$(curl -s -u "admin:$PW" "$U/api/tasks/" | grep -oE '"status":"(PENDING|STARTED)"' | wc -l) [ $((i % 12)) -eq 1 ] && echo " t=$((i*5))s documents=${n:-0} pending=$p" [ "${n:-0}" -ge 20 ] && [ "$p" = 0 ] && break; sleep 5 done ts action_end documents=${n:-0} ;; nextcloud) NIP=$(ip nextcloud); TD=$(docker inspect -f '{{range .Config.Env}}{{println .}}{{end}}' nextcloud | sed -n 's/^NEXTCLOUD_TRUSTED_DOMAINS=//p' | awk '{print $1}') echo "trusted domain: $TD" for i in $(seq 1 120); do s=$(curl -s -H "Host: $TD" http://$NIP/status.php); echo "$s" | grep -q '"installed":true' && break; sleep 5; done echo "status: $(echo $s | head -c 160)" D="http://$NIP/remote.php/dav/files/admin" ts action_start upload curl -s -o /dev/null -w 'mkcol %{http_code}\n' -X MKCOL -H "Host: $TD" -u "admin:$PW" $D/bench ok=0; for f in $C/upload/*.bin; do c=$(curl -s -o /dev/null -w '%{http_code}' -H "Host: $TD" -u "admin:$PW" -T "$f" "$D/bench/$(basename $f)"); case $c in 201|204) ok=$((ok+1));; *) echo "put $(basename $f) -> $c";; esac; done ts action_end uploaded=$ok ;; home-assistant) U=http://$(ip home-assistant):8123; waitup $U/ ts action_start onboarding curl -s -o /dev/null -w 'onboarding-user %{http_code}\n' -X POST $U/api/onboarding/users -H 'Content-Type: application/json' \ -d "{\"client_id\":\"$U/\",\"name\":\"Bench\",\"username\":\"bench\",\"password\":\"$PW\",\"language\":\"en\"}" ts action_end onboarding ;; calibre-web) I=$(src calibre-web /cwa-book-ingest); L=$(src calibre-web /calibre-library); echo "ingest: $I library: $L" waitup http://$(ip calibre-web):8083/ ts action_start ingest cp $C/books/*.epub "$I/" for i in $(seq 1 240); do n=$(ls "$I" 2>/dev/null | grep -c epub); [ "$n" = 0 ] && break; sleep 5; done sleep 20 ts action_end ingest-folder-left=$n library-epubs=$(find "$L" -name '*.epub' 2>/dev/null | wc -l) ;; romm) R=$(src romm /romm/library); echo "library: $R"; waitup http://$(ip romm):8080/ ts action_start copy-library mkdir -p "$R/roms"; cp -r $C/roms/. "$R/roms/"; find "$R" -type f | wc -l ts action_end copied-only-scan-not-triggered ;; *) echo "no action for $APP";; esac