Merge remote-tracking branch 'origin/main' into rega/research-gallery-embed
|
|
@ -209,8 +209,8 @@ iframe at it.
|
|||
<summary><b>More about method 2 — mobile app</b></summary>
|
||||
|
||||
[Capacitor](https://capacitorjs.com) wraps the built maps into an Android app — the same files,
|
||||
no new code. Shown here: the UPG-100 prayer lists and posters, one poster per row, with print
|
||||
options that fit a phone.
|
||||
no new code. Shown here: the UPG-100 prayer lists and posters at phone size, each poster with its
|
||||
own print options.
|
||||
|
||||
```bash
|
||||
bun run check:mobile # checks the app is wired up correctly — needs no Android tools
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 665 KiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 892 KiB After Width: | Height: | Size: 828 KiB |
|
|
@ -1,35 +1,35 @@
|
|||
{
|
||||
"cap1-single-element": {
|
||||
"bundle": "all-people-groups",
|
||||
"sha256": "db9e293144928477bc7fc1ef50f469db073d81575b29ba394b50d33273c6e1a5"
|
||||
"sha256": "312b3a0f64b0842b498ae8c0c88491a9225c353a60c0420e05ac18fa0333a0a5"
|
||||
},
|
||||
"cap2-adoption": {
|
||||
"bundle": "doxa-simple-map",
|
||||
"sha256": "27f32e7a2168391286d190ecfdb9445fb1a610700f1250d0769c50cbd422d778"
|
||||
"sha256": "95ab02edf30ab19b1637238bb50f580286daa2808fbcb43478dbb0c3d0115d95"
|
||||
},
|
||||
"cap2-engagement": {
|
||||
"bundle": "doxa-simple-map",
|
||||
"sha256": "27f32e7a2168391286d190ecfdb9445fb1a610700f1250d0769c50cbd422d778"
|
||||
"sha256": "95ab02edf30ab19b1637238bb50f580286daa2808fbcb43478dbb0c3d0115d95"
|
||||
},
|
||||
"cap2-prayer": {
|
||||
"bundle": "doxa-simple-map",
|
||||
"sha256": "27f32e7a2168391286d190ecfdb9445fb1a610700f1250d0769c50cbd422d778"
|
||||
"sha256": "95ab02edf30ab19b1637238bb50f580286daa2808fbcb43478dbb0c3d0115d95"
|
||||
},
|
||||
"cap3-monolithic-research": {
|
||||
"bundle": "doxa-research-map",
|
||||
"sha256": "56845dc5bd33198a7dd8fb42895700f21b4bc1c3b7f98f57832e8373c7782a83"
|
||||
"sha256": "7e3637a635faf8223488a4bc21c1f308c5b3ea94fa6c80d5f1560a04ef7ec9cb"
|
||||
},
|
||||
"cap4-upg100-list": {
|
||||
"bundle": "my-upg-100-list",
|
||||
"sha256": "efc16067d66481e80d6242beb792ad83c06b6ac71ef978176074fea855ca63c7"
|
||||
"sha256": "57dc358c76f736f5c1b01afe57b3f722e08a1620143ca7f72b2d83133b43f86d"
|
||||
},
|
||||
"cap4-upg100-mfe": {
|
||||
"bundle": "my-upg-100-list",
|
||||
"sha256": "efc16067d66481e80d6242beb792ad83c06b6ac71ef978176074fea855ca63c7"
|
||||
"sha256": "57dc358c76f736f5c1b01afe57b3f722e08a1620143ca7f72b2d83133b43f86d"
|
||||
},
|
||||
"cap4-upg100-poster-wall": {
|
||||
"bundle": "my-upg-100-list",
|
||||
"sha256": "efc16067d66481e80d6242beb792ad83c06b6ac71ef978176074fea855ca63c7"
|
||||
"sha256": "57dc358c76f736f5c1b01afe57b3f722e08a1620143ca7f72b2d83133b43f86d"
|
||||
},
|
||||
"cap5-doxa-maps-spa": {
|
||||
"bundle": "doxa-maps-spa",
|
||||
|
|
@ -41,6 +41,6 @@
|
|||
},
|
||||
"deploy2-mobile-upg100": {
|
||||
"bundle": "my-upg-100-list",
|
||||
"sha256": "efc16067d66481e80d6242beb792ad83c06b6ac71ef978176074fea855ca63c7"
|
||||
"sha256": "57dc358c76f736f5c1b01afe57b3f722e08a1620143ca7f72b2d83133b43f86d"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
67
internal/browser-slot.sh
Executable file
|
|
@ -0,0 +1,67 @@
|
|||
#!/usr/bin/env bash
|
||||
# browser-slot.sh — run ONE browser command inside a memory-floored, N-slot rendezvous.
|
||||
#
|
||||
# WHY A SEMAPHORE AND NOT A LOCK. A single lock made every agent's browser proof wait behind one
|
||||
# chrome: measured 2026-09-25, the queue sat 10-19 deep for hours. The real constraint is MEMORY,
|
||||
# not exclusivity — a headless chrome costs ~300-500MB — so the right shape is "as many browsers as
|
||||
# the box can hold", which is a counting semaphore with a memory floor, not a mutex.
|
||||
#
|
||||
# HOW: N slot files. Try each with flock -n (no wait). If all are busy, WAIT on slot 1. Before
|
||||
# taking any slot, require MemAvailable above the floor — a slot that is free while the box is out
|
||||
# of memory is an invitation to be OOM-killed mid-measurement, which reads as a flaky gate.
|
||||
#
|
||||
# ★ EXIT 75 MEANS "NOT MEASURED", NEVER "FAILED". flock returns 1 on timeout, the same code a
|
||||
# failing gate uses, and that once turned a busy queue into five red product gates. 75 is
|
||||
# EX_TEMPFAIL: distinct from 0 and from any gate's own verdict, so a caller can report it as the
|
||||
# precondition it is.
|
||||
#
|
||||
# Usage: internal/browser-slot.sh <command> [args...]
|
||||
# Env: BROWSER_SLOTS (default 2), BROWSER_SLOT_DIR (default /tmp),
|
||||
# BROWSER_MEM_FLOOR_MB (default 2500), BROWSER_LOCK_WAIT (default 900),
|
||||
# BROWSER_MEM_WAIT (default 300)
|
||||
set -u
|
||||
|
||||
SLOTS="${BROWSER_SLOTS:-2}"
|
||||
DIR="${BROWSER_SLOT_DIR:-/tmp}"
|
||||
BASE="$DIR/doxa-maps-browser-gate.lock"
|
||||
FLOOR_MB="${BROWSER_MEM_FLOOR_MB:-2500}"
|
||||
WAIT_S="${BROWSER_LOCK_WAIT:-900}"
|
||||
MEM_WAIT_S="${BROWSER_MEM_WAIT:-300}"
|
||||
BUSY=75
|
||||
|
||||
slot_path() { [ "$1" = "1" ] && echo "$BASE" || echo "$BASE$1"; }
|
||||
|
||||
mem_available_mb() {
|
||||
awk '/^MemAvailable:/ { printf "%d", $2 / 1024 }' /proc/meminfo 2>/dev/null || echo 999999
|
||||
}
|
||||
|
||||
# Hold off while the box is below the floor. Proceeding anyway is not an option here the way it is
|
||||
# for a load average: load makes a measurement slow, memory makes it get killed halfway through.
|
||||
waited=0
|
||||
while [ "$(mem_available_mb)" -lt "$FLOOR_MB" ]; do
|
||||
if [ "$waited" -ge "$MEM_WAIT_S" ]; then
|
||||
echo "browser-slot: MemAvailable $(mem_available_mb)MB below floor ${FLOOR_MB}MB after ${waited}s — not measuring" >&2
|
||||
exit "$BUSY"
|
||||
fi
|
||||
sleep 10
|
||||
waited=$((waited + 10))
|
||||
done
|
||||
|
||||
# Fast path: any free slot, in a FIXED order so two processes can never take them in opposite
|
||||
# orders. -n means "do not wait": if this slot is busy we move on rather than queue behind it.
|
||||
i=1
|
||||
while [ "$i" -le "$SLOTS" ]; do
|
||||
exec 9>"$(slot_path "$i")" 2>/dev/null || true
|
||||
if flock -n 9; then
|
||||
exec "$@"
|
||||
fi
|
||||
i=$((i + 1))
|
||||
done
|
||||
|
||||
# Everything busy: queue on slot 1, and report a timeout as UNMEASURED rather than as a verdict.
|
||||
exec 9>"$(slot_path 1)"
|
||||
if flock -w "$WAIT_S" 9; then
|
||||
exec "$@"
|
||||
fi
|
||||
echo "browser-slot: no slot within ${WAIT_S}s (${SLOTS} slots, floor ${FLOOR_MB}MB) — not measuring" >&2
|
||||
exit "$BUSY"
|
||||
|
|
@ -84,10 +84,10 @@ const CONCURRENCY = Math.max(
|
|||
// processes can never take them in opposite orders and deadlock. Either file alone still
|
||||
// serialises everything that uses it; taking both is what makes gates and hand-run probes queue
|
||||
// against each other without the repo having to spell a name it must not ship.
|
||||
const BROWSER_LOCK = '/tmp/doxa-maps-browser-gate.lock'
|
||||
const EXTRA_LOCK = process.env.BROWSER_GATE_LOCK && process.env.BROWSER_GATE_LOCK !== BROWSER_LOCK
|
||||
? process.env.BROWSER_GATE_LOCK
|
||||
: ''
|
||||
// The slot files live in browser-slot.sh, which owns the rendezvous now. BROWSER_GATE_LOCK from a
|
||||
// caller's environment is deliberately NOT consulted here any more: a single legacy lock taken
|
||||
// alongside the slots would collapse two slots back into one, which is the queue this replaced.
|
||||
// Agents' own wrappers should call internal/browser-slot.sh so everyone shares the same counter.
|
||||
// ★ THE LOAD WAIT WAS COSTING MORE THAN IT SAVED. It was written at LOAD_MAX 12 when the risk was
|
||||
// seven agents starting chrome at once; the FLOCK below is what actually prevents that — one
|
||||
// browser gate at a time, across every agent on the box. The load check is belt-and-braces on top
|
||||
|
|
@ -177,18 +177,13 @@ function buildSteps() {
|
|||
// wait-for-quiet.sh takes the same lock), so this queues rather than dogpiles. -w 1200
|
||||
// caps the wait so a crashed holder cannot wedge a wave forever, and static gates —
|
||||
// parse, sri, hygiene, the twenty that are node-startup — are left completely free.
|
||||
// flock can exec flock: nesting is how one command waits on both rendezvous points.
|
||||
// ★ -E 75: A LOCK TIMEOUT IS NOT A GATE FAILURE, AND flock CANNOT SAY SO BY DEFAULT.
|
||||
// Without it flock exits 1 on timeout — the same code a failing gate uses — and a wave that
|
||||
// simply could not get the browser lock prints ✗ against five product gates. MEASURED: five
|
||||
// browser gates each "failed" at exactly 1200064ms, which is the timeout to the millisecond
|
||||
// and not a verdict about the product at all. 75 (EX_TEMPFAIL) is distinct, so the runner can
|
||||
// report "could not take the lock" as the precondition it is.
|
||||
const lockArgs = ['-w', String(LOCK_WAIT_S), '-E', String(LOCK_BUSY_CODE)]
|
||||
const args = EXTRA_LOCK
|
||||
? [...lockArgs, BROWSER_LOCK, 'flock', ...lockArgs, EXTRA_LOCK, 'bun', 'run', name]
|
||||
: [...lockArgs, BROWSER_LOCK, 'bun', 'run', name]
|
||||
return { ...step, browser: true, cmd: 'flock', args }
|
||||
// ★ A TWO-SLOT SEMAPHORE, NOT A MUTEX. One lock made every agent's browser proof wait behind
|
||||
// a single chrome — measured 2026-09-25, the queue sat 10-19 deep for hours. The constraint
|
||||
// is MEMORY (~300-500MB per headless chrome), not exclusivity, so browser-slot.sh runs as
|
||||
// many as the box can hold and refuses to start one when MemAvailable is below the floor.
|
||||
// It keeps the same exit-code contract: 75 means "not measured", never "failed".
|
||||
const args = [resolve(ROOT, 'internal/browser-slot.sh'), 'bun', 'run', name]
|
||||
return { ...step, browser: true, cmd: 'sh', args }
|
||||
})
|
||||
}
|
||||
// A bundle is a plugin-designs/<name>/ directory — the same selector build:unlocked used
|
||||
|
|
|
|||