diff --git a/README.md b/README.md index 1d60067..de83056 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ numeric metrics, and keeps recent history in SQLite. The backend is a single Go binary, the frontend is a Preact + Vite page, and everything is configured with one YAML file. -![statuspage](assets/screenshots/status.png) +![statuspage](assets/screenshots/status-collage.png) ## Features @@ -120,16 +120,26 @@ README image, but is handy for previewing the UI quickly: ### Screenshot `scripts/generate-screenshot` builds the frontend and server, starts the app in -mock mode, captures the top of the page with headless Chromium, and frames it -with a transparent background, border, and shadow. It needs `chromium` and -`python3-PIL` on PATH: +mock mode, captures the top of the page with headless Chromium in both light and +dark, and frames each with a transparent background, border, and shadow. It +needs `chromium` and `python3-PIL` on PATH: ```sh ./scripts/generate-screenshot ``` -The PNG is written to `assets/screenshots/status.png` — the image at the top of -this README. +This writes three PNGs to `assets/screenshots/`: + +| file | contents | +| --------------------------- | ------------------------------------------------- | +| `status.png` | light capture | +| `status-dark.png` | dark capture | +| `status-collage.png` | dark behind, offset top-left, light on top | + +Runs are incremental: a capture is skipped when no frontend/server source is +newer than the existing PNG, and the collage is rebuilt only when the mtime of a +light/dark variant is newer than the collage. `status-collage.png` is the image +at the top of this README. ### Flags diff --git a/assets/screenshots/status-collage.png b/assets/screenshots/status-collage.png new file mode 100644 index 0000000..0c1bf92 Binary files /dev/null and b/assets/screenshots/status-collage.png differ diff --git a/assets/screenshots/status-dark.png b/assets/screenshots/status-dark.png new file mode 100644 index 0000000..8e6b807 Binary files /dev/null and b/assets/screenshots/status-dark.png differ diff --git a/assets/screenshots/status.png b/assets/screenshots/status.png index 435f3f8..3be75fd 100644 Binary files a/assets/screenshots/status.png and b/assets/screenshots/status.png differ diff --git a/scripts/generate-screenshot b/scripts/generate-screenshot index f41d4b2..6e3469c 100755 --- a/scripts/generate-screenshot +++ b/scripts/generate-screenshot @@ -3,7 +3,10 @@ set -euo pipefail cd "$(dirname "$0")/.." -OUT="assets/screenshots/status.png" +ASSETS="assets/screenshots" +LIGHT="$ASSETS/status.png" +DARK="$ASSETS/status-dark.png" +COLLAGE="$ASSETS/status-collage.png" PORT="${PORT:-17645}" URL="http://127.0.0.1:$PORT" @@ -13,6 +16,13 @@ if [ -z "$CHROMIUM" ]; then exit 1 fi +if python3 -c 'import PIL' 2>/dev/null; then + HAVE_PIL=1 +else + HAVE_PIL=0 + echo "note: python3-PIL not found, skipping framing and collage" >&2 +fi + echo "building frontend..." bun run build @@ -41,23 +51,29 @@ if ! curl -sf "$URL/api/status" >/dev/null 2>&1; then exit 1 fi -mkdir -p "$(dirname "$OUT")" +mkdir -p "$ASSETS" -"$CHROMIUM" --headless=new --disable-gpu --hide-scrollbars \ - --window-size=1280,720 \ - --force-device-scale-factor=2 \ - --screenshot="$OUT" \ - --virtual-time-budget=8000 \ - "$URL/" 2>/dev/null +# $1=theme $2=out $3=shadow $4=border +capture() { + local theme="$1" out="$2" shadow="$3" border="$4" -# frame the capture: rounded corners, 2px border, hard offset shadow (2x for device scale) -if ! python3 -c 'import PIL' 2>/dev/null; then - echo "note: python3-PIL not found, skipping frame" >&2 - echo "screenshot written to $OUT" - exit 0 -fi + if [ -f "$out" ] && + ! find src index.html public server go.mod go.sum \ + -type f -newer "$out" -print -quit 2>/dev/null | grep -q .; then + echo "up to date, skipping ($out)" + return + fi -python3 - "$OUT" <<'PY' + "$CHROMIUM" --headless=new --disable-gpu --hide-scrollbars \ + --window-size=1280,720 \ + --force-device-scale-factor=2 \ + --screenshot="$out" \ + --virtual-time-budget=8000 \ + "$URL/?theme=$theme" 2>/dev/null + + # frame the capture: rounded corners, 2px border, hard offset shadow (2x for device scale) + if [ "$HAVE_PIL" = 1 ]; then + python3 - "$out" "$shadow" "$border" <<'PY' import sys from PIL import Image, ImageDraw @@ -68,8 +84,8 @@ r = 16 # 0.5rem radius b = 4 # 2px border s = 12 # 0.35rem shadow offset m = 24 # margin around the frame -BORDER = (17, 17, 17) -SHADOW = (136, 136, 136) +BORDER = tuple(int(c) for c in sys.argv[3].split(",")) +SHADOW = tuple(int(c) for c in sys.argv[2].split(",")) mask = Image.new("L", (W, H), 0) ImageDraw.Draw(mask).rounded_rectangle([0, 0, W - 1, H - 1], radius=r, fill=255) @@ -89,5 +105,32 @@ canvas.alpha_composite(img_r, (m + b, m + b)) canvas.save(path) print(f"framed {path}") PY + fi + echo "wrote $out" +} -echo "screenshot written to $OUT" +capture light "$LIGHT" "136,136,136" "17,17,17" +capture dark "$DARK" "0,0,0" "58,64,58" + +# collage: rebuild only when a variant is newer than the existing collage +if [ "$HAVE_PIL" = 1 ]; then + if [ -f "$COLLAGE" ] && [ "$COLLAGE" -nt "$LIGHT" ] && [ "$COLLAGE" -nt "$DARK" ]; then + echo "collage up to date, skipping ($COLLAGE)" + else + python3 - "$LIGHT" "$DARK" "$COLLAGE" <<'PY' +import sys +from PIL import Image + +light = Image.open(sys.argv[1]).convert("RGBA") +dark = Image.open(sys.argv[2]).convert("RGBA") +W, H = light.size +ox, oy = 480, 160 +canvas = Image.new("RGBA", (W + ox, H + oy), (242, 242, 242, 255)) +canvas.alpha_composite(dark, (0, 0)) +canvas.alpha_composite(light, (ox, oy)) +canvas.thumbnail((1920, 1080), Image.LANCZOS) +canvas.save(sys.argv[3]) +print(f"wrote {sys.argv[3]}") +PY + fi +fi