Files
skills/web-shot/shot.sh
T
mathiasandClaude Opus 4.8 1e29c6d1af
release / tag (push) Failing after 1s
feat(skills): import 17 skills from local-dev; fix stale host
Consolidating the divergence between this repo and local-dev's embedded
~/dev/.skills/ (the two had drifted; the 19 overlapping skills were byte-identical).

- Import the 17 skills that existed only in local-dev: discovery-framing,
  stage-gate-review, web-shot, and the 14 superpowers-* skills. This repo is now
  the superset / single source of truth.
- SKILLS_INDEX.md: add rows for the 17.
- install.sh + Taskfile.yml: git.d-ma.be host (was stale gitea.d-ma.be, which
  broke the one-line curl|bash installer post-rename).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 00:10:21 +02:00

182 lines
6.0 KiB
Bash
Executable File

#!/usr/bin/env bash
# web-shot/shot.sh — screenshot a running web UI via a k3s playwright Job.
#
# Reusable across any Tailscale-connected host whose kubeconfig points at the
# koala k3s cluster. See SKILL.md alongside this file for prereqs.
set -euo pipefail
# --- defaults --------------------------------------------------------------
PORT=""
PATH_=""
HOST=""
URL=""
SEARCH=""
OUT_DIR=""
JOB_PREFIX="web-shot"
PLAYWRIGHT_TAG="v1.49.0-jammy"
PLAYWRIGHT_NPM="1.49.0"
NAMESPACE="default"
usage() {
sed -n '1,/^# ---/p' "$0" | sed 's/^# \{0,1\}//;1,/^!/d' >&2
cat >&2 <<EOF
usage: shot.sh [--port N] [--path P] [--host H] [--url U]
[--search Q] [--out DIR] [--name PREFIX]
See SKILL.md for prerequisites and examples.
EOF
exit 2
}
# --- parse args ------------------------------------------------------------
while [ $# -gt 0 ]; do
case "$1" in
--port) PORT="$2"; shift 2 ;;
--path) PATH_="$2"; shift 2 ;;
--host) HOST="$2"; shift 2 ;;
--url) URL="$2"; shift 2 ;;
--search) SEARCH="$2"; shift 2 ;;
--out) OUT_DIR="$2"; shift 2 ;;
--name) JOB_PREFIX="$2"; shift 2 ;;
-h|--help) usage ;;
*) echo "unknown flag: $1" >&2; usage ;;
esac
done
# --- resolve URL -----------------------------------------------------------
if [ -z "$URL" ]; then
if [ -z "$PORT" ]; then
echo "✗ --port or --url is required" >&2
usage
fi
if [ -z "$HOST" ]; then
if command -v tailscale >/dev/null 2>&1; then
HOST="$(tailscale status --self --json 2>/dev/null | jq -r '.Self.HostName' 2>/dev/null || true)"
fi
if [ -z "$HOST" ] && [ -r /etc/hostname ]; then
HOST="$(tr -d '\n' </etc/hostname)"
fi
if [ -z "$HOST" ]; then
echo "✗ could not auto-detect Tailscale hostname — pass --host" >&2
exit 1
fi
fi
PATH_="${PATH_:-/}"
case "$PATH_" in /*) ;; *) PATH_="/$PATH_" ;; esac
URL="http://${HOST}:${PORT}${PATH_}"
fi
OUT_DIR="${OUT_DIR:-$(pwd)/screenshots}"
mkdir -p "$OUT_DIR"
JOB_NAME="${JOB_PREFIX}-$(date +%s)"
echo "→ host=${HOST:-} url=${URL} out=${OUT_DIR} job=${JOB_NAME}"
# --- preflight: dev server must be reachable -------------------------------
# (Optional but cheap. Run from the caller, not the pod.)
if ! curl -fsS --max-time 3 "${URL%/}/healthz" >/dev/null 2>&1 \
&& ! curl -fsS --max-time 3 "$URL" >/dev/null 2>&1; then
echo "⚠ ${URL} not reachable from this host. Pod will retry; if it fails, the dev server may be bound to 127.0.0.1 only." >&2
fi
# --- emit + apply Job ------------------------------------------------------
TAKE_SEARCH=""
[ -n "$SEARCH" ] && TAKE_SEARCH="yes"
cleanup() {
kubectl -n "$NAMESPACE" delete job "$JOB_NAME" --ignore-not-found=true >/dev/null 2>&1 || true
}
trap cleanup EXIT
kubectl -n "$NAMESPACE" apply -f - >/dev/null <<YAML
apiVersion: batch/v1
kind: Job
metadata:
name: ${JOB_NAME}
spec:
ttlSecondsAfterFinished: 120
backoffLimit: 0
template:
spec:
restartPolicy: Never
containers:
- name: shot
image: mcr.microsoft.com/playwright:${PLAYWRIGHT_TAG}
env:
- { name: URL, value: "${URL}" }
- { name: SEARCH, value: "${SEARCH}" }
- { name: DO_SEARCH, value: "${TAKE_SEARCH}" }
command: ["bash", "-lc"]
args:
- |
set -e
cd /tmp
npm init -y >/dev/null 2>&1
npm install playwright@${PLAYWRIGHT_NPM} --no-fund --no-audit --silent 2>&1 | tail -1
mkdir -p /shot
cat > /tmp/s.mjs <<'JS'
import { chromium } from 'playwright';
const url = process.env.URL;
const out = process.env.OUT;
const action = process.env.ACTION || '';
const b = await chromium.launch();
const ctx = await b.newContext({ viewport: { width: 1280, height: 900 }, deviceScaleFactor: 2 });
const p = await ctx.newPage();
await p.goto(url, { waitUntil: 'networkidle' });
if (action.startsWith('search:')) {
const q = action.slice('search:'.length);
await p.fill('input[name="q"]', q);
await p.waitForResponse(r => r.url().includes('/search'), { timeout: 5000 }).catch(() => {});
await p.waitForTimeout(500);
}
await p.screenshot({ path: out, fullPage: true });
await b.close();
console.log('wrote', out);
JS
URL="\$URL" OUT=/shot/home.png ACTION= node /tmp/s.mjs
if [ "\$DO_SEARCH" = "yes" ]; then
URL="\$URL" OUT=/shot/search.png ACTION="search:\$SEARCH" node /tmp/s.mjs
fi
chmod 644 /shot/*.png
echo "READY"
# Window for kubectl cp; deleted by trap after we've grabbed files.
sleep 90
YAML
# --- wait for pod + READY marker ------------------------------------------
echo "→ waiting for pod scheduling…"
for _ in $(seq 1 60); do
POD="$(kubectl -n "$NAMESPACE" get pods -l "batch.kubernetes.io/job-name=$JOB_NAME" \
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)"
[ -n "$POD" ] && break
sleep 1
done
if [ -z "$POD" ]; then
echo "✗ pod not scheduled" >&2; exit 1
fi
echo "→ pod $POD"
echo "→ waiting for READY (pulling image + rendering)…"
for _ in $(seq 1 60); do
if kubectl -n "$NAMESPACE" logs "$POD" 2>/dev/null | grep -q '^READY$'; then
break
fi
PHASE="$(kubectl -n "$NAMESPACE" get pod "$POD" -o jsonpath='{.status.phase}' 2>/dev/null || true)"
if [ "$PHASE" = "Failed" ]; then
echo "✗ pod Failed — logs:" >&2
kubectl -n "$NAMESPACE" logs "$POD" 2>&1 | tail -30 >&2
exit 1
fi
sleep 2
done
# --- copy files out --------------------------------------------------------
kubectl -n "$NAMESPACE" cp "${NAMESPACE}/${POD}:/shot/home.png" "$OUT_DIR/home.png" 2>/dev/null \
|| { echo "✗ kubectl cp home.png failed" >&2; exit 1; }
if [ "$TAKE_SEARCH" = "yes" ]; then
kubectl -n "$NAMESPACE" cp "${NAMESPACE}/${POD}:/shot/search.png" "$OUT_DIR/search.png" 2>/dev/null \
|| { echo "✗ kubectl cp search.png failed" >&2; exit 1; }
fi
ls -la "$OUT_DIR"/*.png
echo "✓ screenshots in $OUT_DIR"