From 21e6af1a6b73c3755790f6a3f198e475afa0b101 Mon Sep 17 00:00:00 2001 From: jhfnetboy Date: Sat, 1 Aug 2026 23:17:17 +0700 Subject: [PATCH 1/2] chore(ops): track deploy-frontend.sh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This has been sitting untracked in the working tree while being the actual production deploy path for the frontend, so the knowledge it encodes lived on one machine only. What it knows that nothing else in the repo does: the frontend runs under the launchd agent `io.aastar.yaa-frontend` (KeepAlive + RunAtLoad, `next start -p 5173`) behind the cloudflared tunnel, so `npm run build` alone leaves the *running* process serving a stale chunk manifest — /_next/static/chunks/* return 500, hydration fails, white screen. The script builds, kickstarts the agent, waits for :5173, then verifies every chunk the homepage references returns 200 (`--public` runs the same check through Cloudflare end-to-end). It also refuses to restart if the build fails, so a broken build can't take the running site down. Sits at the repo root next to the existing dev helpers (backend.sh / dev.sh / frontend.sh) — frontend.sh starts dev, this deploys prod. Mode 100755, no secrets (public hostname, port and agent label only). Gap noted, not addressed here: the referenced `io.aastar.yaa-frontend.plist` lives in ~/Library/LaunchAgents and is still untracked, so the script's precondition can't be reproduced from the repo. `scripts/ops/` already tracks io.aastar.yaa-monitor.plist and would be its natural home — left out because that file is machine-specific and its contents should be reviewed before committing. Claude-Session: https://claude.ai/code/session_01BxmyQj2A82DfFXu97kKACk --- deploy-frontend.sh | 87 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100755 deploy-frontend.sh diff --git a/deploy-frontend.sh b/deploy-frontend.sh new file mode 100755 index 0000000..1361cad --- /dev/null +++ b/deploy-frontend.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +# Deploy the cos72/yaa frontend: clean build -> launchd kickstart -> verify chunks 200. +# +# Why this exists: the frontend runs under launchd agent `io.aastar.yaa-frontend` +# (KeepAlive + RunAtLoad) serving `next start -p 5173` behind the cloudflared tunnel. +# If you `npm run build` but forget to restart, the *running* process keeps serving a +# stale chunk manifest -> /_next/static/chunks/*.{js,css} 500 -> browser can't hydrate +# -> WHITE SCREEN. This script builds AND restarts AND verifies, so that can't happen. +# +# Usage: +# ./deploy-frontend.sh # clean build + kickstart + verify localhost +# ./deploy-frontend.sh --public # also verify https://cos72.aastar.io end-to-end +# ./deploy-frontend.sh --no-clean # skip `rm -rf .next` (faster, incremental) +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +FRONTEND_DIR="$SCRIPT_DIR/aastar-frontend" +AGENT="io.aastar.yaa-frontend" +PORT=5173 +LOCAL="http://localhost:$PORT" +PUBLIC_HOST="cos72.aastar.io" + +CLEAN=1 +CHECK_PUBLIC=0 +for a in "$@"; do + case "$a" in + --no-clean) CLEAN=0 ;; + --public) CHECK_PUBLIC=1 ;; + *) echo "unknown arg: $a"; echo "usage: $0 [--public] [--no-clean]"; exit 2 ;; + esac +done + +say() { printf '\033[1;34m==>\033[0m %s\n' "$*"; } +die() { printf '\033[1;31mFAIL:\033[0m %s\n' "$*" >&2; exit 1; } + +# 1) build ------------------------------------------------------------------ +say "Building frontend (aastar-frontend)..." +[ "$CLEAN" = "1" ] && { say "clean: rm -rf .next"; rm -rf "$FRONTEND_DIR/.next"; } +if ! npm run build -w aastar-frontend; then + die "next build failed — NOT restarting; site stays on the current (working) build." +fi + +# 2) restart via launchd (canonical) --------------------------------------- +if launchctl print "gui/$(id -u)/$AGENT" >/dev/null 2>&1; then + say "Restarting launchd agent $AGENT (kickstart -k)..." + launchctl kickstart -k "gui/$(id -u)/$AGENT" +else + die "launchd agent $AGENT not loaded. Load it first (see ~/Library/LaunchAgents/$AGENT.plist), don't nohup npm start (it fights KeepAlive for :$PORT)." +fi + +# 3) wait for the port to come back ---------------------------------------- +say "Waiting for $LOCAL to respond 200..." +up=0 +for i in $(seq 1 60); do + if [ "$(curl -s -o /dev/null -w '%{http_code}' "$LOCAL/" || true)" = "200" ]; then + up=1; say "up after ${i}s"; break + fi + sleep 1 +done +[ "$up" = "1" ] || die "server did not return 200 on $LOCAL within 60s (check ~/Library/Logs/yaa-frontend.log)" + +# 4) verify every referenced JS/CSS chunk is 200 (this is the white-screen guard) +verify_chunks() { + local base="$1" label="$2" total=0 bad=0 code + local home; home="$(curl -s "$base/")" || die "$label: could not fetch homepage" + # sanity: rendered content present + echo "$home" | grep -qi "cos72" || printf '\033[1;33mwarn:\033[0m %s homepage has no "cos72" text\n' "$label" + while IFS= read -r u; do + [ -z "$u" ] && continue + total=$((total+1)) + code="$(curl -s -o /dev/null -w '%{http_code}' "$base$u" || echo 000)" + [ "$code" = "200" ] || { printf ' \033[1;31mBAD %s\033[0m %s\n' "$code" "$u"; bad=$((bad+1)); } + done < <(printf '%s' "$home" | grep -oE '/_next/static/[^"]+\.(js|css)' | sort -u) + echo "$label: $total chunks checked, $bad non-200" + [ "$bad" = "0" ] || die "$label: $bad chunk(s) not 200 — this is exactly the white-screen condition." +} + +say "Verifying chunks on $LOCAL..." +verify_chunks "$LOCAL" "localhost" + +if [ "$CHECK_PUBLIC" = "1" ]; then + say "Verifying chunks on https://$PUBLIC_HOST (end-to-end through Cloudflare)..." + verify_chunks "https://$PUBLIC_HOST" "public" +fi + +printf '\033[1;32m✔ deploy OK\033[0m — %s serving fresh build, all chunks 200.\n' "$LOCAL" +[ "$CHECK_PUBLIC" = "1" ] && printf ' https://%s verified end-to-end.\n' "$PUBLIC_HOST" From 047db21dedff39f5dbcef1a41134dd7f078c0b92 Mon Sep 17 00:00:00 2001 From: jhfnetboy Date: Sat, 1 Aug 2026 23:26:12 +0700 Subject: [PATCH 2/2] chore(ops): track the launchd plist deploy-frontend.sh depends on MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Without this, the script's own precondition can't be reproduced from the repo — it just dies with "launchd agent io.aastar.yaa-frontend not loaded" and the reader has no way to find out what that agent should contain. Copied verbatim from the running ~/Library/LaunchAgents/ definition; verified with `plutil -lint` and by diffing both files through `plutil -convert xml1`, which match apart from the added comment header. No secrets (paths, a label and a node path only). Follows the convention io.aastar.yaa-monitor.plist already set in this directory: committed as-is with absolute /Users/jason paths rather than templated, plus an install-command header. The header additionally flags the pinned nvm node path (v22.22.2), because a node upgrade makes the agent fail to start silently — the only trace is ~/Library/Logs/yaa-frontend.log. Claude-Session: https://claude.ai/code/session_01BxmyQj2A82DfFXu97kKACk --- scripts/ops/io.aastar.yaa-frontend.plist | 44 ++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 scripts/ops/io.aastar.yaa-frontend.plist diff --git a/scripts/ops/io.aastar.yaa-frontend.plist b/scripts/ops/io.aastar.yaa-frontend.plist new file mode 100644 index 0000000..fdcc599 --- /dev/null +++ b/scripts/ops/io.aastar.yaa-frontend.plist @@ -0,0 +1,44 @@ + + + + + + Label + io.aastar.yaa-frontend + ProgramArguments + + /bin/bash + -c + exec /Users/jason/.nvm/versions/node/v22.22.2/bin/npm run start -w aastar-frontend + + WorkingDirectory + /Users/jason/Dev/aastar/YetAnotherAA + EnvironmentVariables + + PATH + /Users/jason/.nvm/versions/node/v22.22.2/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin + HOME + /Users/jason + + RunAtLoad + + KeepAlive + + ThrottleInterval + 10 + StandardOutPath + /Users/jason/Library/Logs/yaa-frontend.log + StandardErrorPath + /Users/jason/Library/Logs/yaa-frontend.log + +