Something went wrong. Try again.
Backend environment for match hosting for lance.blue
Something went wrong. Try again.
Shell
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241#!/usr/bin/env bash# suramadu/web/report.js is the last screen of a match, and the ways it goes# wrong are all quiet ones:## - the page stops loading. Same-origin file under script-src 'self'.# - it asks somewhere else for the result. The fetch must stay same-origin:# that is what keeps the session cookie flowing and connect-src satisfied.# - it stops covering the client. Hiding .suramadu-element is the whole of# what keeps "your connection to the server is lost" off a finished# match; an overlay alone only covers what is already painted.# - it decides who won by counting. A forced victory leaves the loser's# units standing, so the verdict has to come from the result document's# own victoryTeam / victoryPlayerId and from nothing else.# - the flare button disappears under it. Two files, two z-indexes, and a# silent regression if they ever cross.## The test stage has no JavaScript engine, so "it parses" is as far as that# part can go: `node --check` when a laptop has node, otherwise the same# string-and-comment scanner test-flare-ui.sh uses.set -uo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"JS="$ROOT/suramadu/web/report.js"CSS="$ROOT/suramadu/web/report.css"FLARE_CSS="$ROOT/suramadu/web/flare.css"HTML="$ROOT/suramadu/web/index.html"
pass=0; fail=0check() { if "${@:2}"; then echo "ok $1"; pass=$((pass+1)); else echo "FAIL $1"; fail=$((fail+1)); fi; }
[ -s "$JS" ] || { echo "FAIL report.js is missing or empty"; exit 1; }[ -s "$CSS" ] || { echo "FAIL report.css is missing or empty"; exit 1; }[ -s "$HTML" ] || { echo "FAIL index.html is missing or empty"; exit 1; }
# --- it parses ---------------------------------------------------------------
check "no template literals, so the scanner's assumptions hold" \ bash -c "! grep -q '\`' '$JS'"
check "brackets balance" python3 - "$JS" <<'PY'import sys
src = open(sys.argv[1], encoding="utf-8").read()pairs = {")": "(", "]": "[", "}": "{"}stack = []i, n, line = 0, len(src), 1while i < n: c = src[i] if c == "\n": line += 1 i += 1 elif c == "/" and src[i + 1:i + 2] == "/": while i < n and src[i] != "\n": i += 1 elif c == "/" and src[i + 1:i + 2] == "*": i += 2 while i + 1 < n and not (src[i] == "*" and src[i + 1] == "/"): line += src[i] == "\n" i += 1 i += 2 elif c in "\"'`": quote, i = c, i + 1 while i < n and src[i] != quote: if src[i] == "\\": i += 2 continue line += src[i] == "\n" i += 1 i += 1 elif c in "([{": stack.append((c, line)) i += 1 elif c in ")]}": if not stack or stack[-1][0] != pairs[c]: sys.exit("unbalanced '%s' at line %d" % (c, line)) stack.pop() i += 1 else: i += 1if stack: sys.exit("unclosed '%s' opened at line %d" % (stack[-1][0], stack[-1][1]))PY
if command -v node >/dev/null; then check "node --check" node --check "$JS"else echo "skip node --check (no node in the test stage)"fi
# --- it asks us, and nobody else ---------------------------------------------
# Two, and only these two: the container's own copy of the result, resolved# relative to the page so it goes through the one proxied path this player is# allowed, and headquarters' route behind it. Both same-origin, which is what# keeps the session cookie flowing and connect-src satisfied.check "the only fetches are the local copy and the result route" bash -c ' calls="$(grep -o "fetch(\"[^\"]*\"" "$1" | sort -u | tr "\n" " ")" [ "$calls" = "fetch(\"/api/matches/\" fetch(\"decided.json\" " ]' _ "$JS"
# The one exception, and it is a constant rather than a literal so the check# above cannot see it: the face on a force's card, from Bluesky's public# appview. Nothing of ours goes with it - no cookie, no match id, no DID# beyond the account being asked about, which is public - and the CSP has to# name the host or the request never leaves. A third host appearing here is a# new thing to think about, which is what this fails for.check "the only cross-origin lookup is the public appview" bash -c ' hosts="$(grep -o "https://[a-z.]*" "$1" | sort -u \ | grep -v -E "^https://(bsky.app|lance.blue)$" | tr "\n" " ")" [ "$hosts" = "https://public.api.bsky.app " ]' _ "$JS"
check "the CSP allows the appview and its CDN" bash -c ' grep -q "connect-src[^;]*https://public.api.bsky.app" "$1" && grep -q "img-src[^;]*https://cdn.bsky.app" "$1"' _ "$HTML"
# --- the watchers are never a force ------------------------------------------
# ArenaObserver spools turn reports and ArenaSpectator is the seat a browser# takes in a match nobody is playing. Neither is a player, and the server's# own observer flag is not enough to keep them off this screen: the host sets# it once every human seat is filled, so a match decided before that records# it false and the report grew a third force with no units in it.check "the reserved seats are dropped by name as well as by flag" bash -c ' grep -q "ArenaObserver" "$1" && grep -q "ArenaSpectator" "$1" && grep -q "unitsStart" "$1"' _ "$JS"
# --- this screen holds no controls -------------------------------------------
# Sharing and the full report live on the match's own page, which is a page on# the site rather than a screen inside a container that is about to be# deleted. Nothing here writes to an account either: this page holds no token,# and a report that asked for one would be asking for a permission the whole# site does not have.# The profile links and the avatar lookup stay - those are the pilot cards,# and they read rather than write.check "nothing here posts, or offers to" bash -c ' ! grep -qE "intent/compose|com\.atproto\.repo|createRecord" "$1"' _ "$JS"
# The fast path is the point of it: a file in the match's own container, a# second away, against S3 and a control plane fifteen seconds away.check "the local copy is polled faster than the API" bash -c ' local_ms=$(grep -o "var LOCAL_POLL_MS = [0-9]*" "$1" | tr -dc "0-9") api_ms=$(grep -o "var POLL_MS = [0-9]*" "$1" | tr -dc "0-9") [ -n "$local_ms" ] && [ -n "$api_ms" ] && [ "$local_ms" -lt "$api_ms" ]' _ "$JS"
# --- it takes the screen and keeps it ----------------------------------------
# The overlay covers what is painted; this covers what Suramadu paints next.# Without it the disconnect card arrives on top when the container stops.check "the client is hidden, not just covered" bash -c ' grep -q "querySelector(\".suramadu-element\")" "$1" && grep -q "display = \"none\"" "$1"' _ "$JS"
check "nothing takes the report back off the page" \ bash -c "! grep -qE 'screen\.remove|removeChild' '$JS'"
# --- the verdict is recorded, never inferred ---------------------------------
check "the winner comes from the result document" bash -c ' grep -q "victoryTeam" "$1" && grep -q "victoryPlayerId" "$1"' _ "$JS"
# --- the page loads it -------------------------------------------------------
check "index.html loads report.js" grep -q '<script src="report.js"></script>' "$HTML"check "index.html links report.css" grep -q '"report.css"' "$HTML"
# --- the hand-off ------------------------------------------------------------
# The report is the verdict and the way onward; the machines, their armour and# their critical slots are on the match's own page. This screen takes the# player there on its own, with nothing to press: a container that is being# deleted is not a place to keep anybody.check "the report hands off to the match page" bash -c ' grep -q "lb-report-handoff" "$1" && grep -q "window.location.href = handoffTo" "$1"' _ "$JS"# Short, because it is a pause rather than a screen.check "the pause is seconds, not tens of them" bash -c ' ms=$(grep -o "var HANDOFF_MS = [0-9]*" "$1" | tr -dc "0-9") [ -n "$ms" ] && [ "$ms" -le 5000 ]' _ "$JS"# Only where there is somewhere to go: a redirect to nothing is worse than a# screen that waits.check "nothing is opened without a result and an address" \ grep -q "if (result && share && !stranded)" "$JS"
# The address is not enough: the API hands one out for any match it knows,# result uploaded or not, and that result is what makes the page exist. So the# pause and the departure are separate, and leaving takes both. Otherwise the# redirect races the upload, and losing is a 404 with no way back.check "the pause and the departure are separate" bash -c ' grep -q "handoffDue = true" "$1" && grep -q "if (handoffDue && landed && handoffTo)" "$1"' _ "$JS"check "the result is asked for, not assumed" bash -c ' grep -q "landed = landed || !!data.result" "$1"' _ "$JS"# A result that never arrives ends as a way home, not a line that never# resolves.check "a result that never lands strands the hand-off" bash -c ' grep -q "stranded = true" "$1" && grep -q "chasing > CHASE_TRIES" "$1"' _ "$JS"
# --- the layering holds ------------------------------------------------------
# One number each, and the order between them is the behaviour: the report is# the screen, and the feedback dock - the flare button and the survey link# beside it - is the one thing allowed on top of it.check "the feedback dock sits above the report screen" bash -c ' report=$(grep -A 8 "^:root .lb-report {" "$1" | grep -m1 "z-index:" | tr -dc "0-9") button=$(grep -A 8 "^:root .lb-flare-dock {" "$2" | grep -m1 "z-index:" | tr -dc "0-9") dialog=$(grep -A 8 "^:root .lb-flare-overlay {" "$2" | grep -m1 "z-index:" | tr -dc "0-9") [ -n "$report" ] && [ -n "$button" ] && [ -n "$dialog" ] && [ "$button" -gt "$report" ] && [ "$dialog" -gt "$button" ]' _ "$CSS" "$FLARE_CSS"
# --- the stylesheet holds theme.css's conventions -----------------------------
# At-rule preludes are skipped: `@media ... {` ends in a brace and would read# as a bare selector, but the selectors inside it are checked on their own# lines. Same allowance test-theme-css.sh makes.mapfile -t bare < <( awk ' /\/\*/ { inc = 1 } inc { if (/\*\//) inc = 0; next } /^[[:space:]]*$/ { next } /^[[:space:]]*@/ { next } /^[[:space:]]*:root[[:space:]]*\{/ { next } /[{,][[:space:]]*$/ { line = $0 sub(/^[[:space:]]+/, "", line) if (line !~ /^:root/) print line } ' "$CSS")check "every selector is prefixed with :root" test "${#bare[@]}" -eq 0if [ "${#bare[@]}" -ne 0 ]; then printf ' %s\n' "${bare[@]}"fi
NOCOMMENT="$(mktemp)"trap 'rm -f "$NOCOMMENT"' EXITawk '/\/\*/ { inc = 1 } inc { if (/\*\//) inc = 0; next } { print }' "$CSS" > "$NOCOMMENT"check "no !important in any declaration" bash -c "! grep -q '!important' '$NOCOMMENT'"
echoecho "$pass passed, $fail failed"[ "$fail" -eq 0 ]