Let the extension update itself, and host the dashboard

Chrome has no update channel for an unpacked extension, so every pull left one
manual step: click reload on its card. The dashboard now publishes the version
its own checkout ships at /api/autobuyer/status, and the extension reloads
itself when it is behind — chrome.runtime.reload() re-reads the folder from
disk, which the update check has already pulled into. It only ever runs code
that is on disk, so a reload that changes nothing is reported and waited out
rather than repeated; a dashboard that is behind is ignored.

Also adds the deploy stack for running the dashboard on the droplet, next to
the autotrader one: it joins that nginx's network and renders a server block
into its conf.d rather than standing up a second ingress. AUTOFIRMER_DB moves
the SQLite file onto a volume, since the working directory in a container is
replaced on every deploy.

And waitFor gained `hidden`: an absent gate matched on DOM presence only, so a
modal that closes by going display:none could never satisfy it. The extension
grew a visibility probe for this — locate cannot answer it, because absent and
hidden are both errors there and a wait gate has to tell them apart.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-08-31 12:30:59 -05:00
co-authored by Claude Opus 5
parent 1f4459985a
commit d1e8668440
18 changed files with 637 additions and 28 deletions
+56
View File
@@ -0,0 +1,56 @@
#!/usr/bin/env bash
# Rebuild the dashboard image, restart the container, point the shared nginx at
# it. Safe to re-run any time — this is the normal "ship a change" command.
set -euo pipefail
cd "$(dirname "$0")"
log() { printf '\n\033[1;34m==> %s\033[0m\n' "$1"; }
fail() { printf '\n\033[1;31mERROR: %s\033[0m\n' "$1" >&2; exit 1; }
[ -f .env ] || fail "no .env — copy .env.example to .env and fill it in"
set -a; . ./.env; set +a
: "${FIRMER_HOST:?set FIRMER_HOST in .env}"
SHARED_STACK="${SHARED_STACK:-/opt/autotrader/deploy}"
command -v envsubst >/dev/null || fail "envsubst missing — apt-get install -y gettext-base"
[ -d "$SHARED_STACK/nginx/conf.d" ] || fail "no nginx/conf.d under $SHARED_STACK"
log "Building and starting"
docker compose up -d --build
log "Waiting for the container to report healthy"
for _ in $(seq 1 30); do
state=$(docker inspect autofirmer --format '{{.State.Health.Status}}' 2>/dev/null || echo missing)
[ "$state" = healthy ] && break
sleep 2
done
[ "${state:-}" = healthy ] || fail "autofirmer is $state — check: docker logs autofirmer"
# Rendered into the *other* stack's conf.d, because that is the directory its
# nginx has mounted. The file is untracked there, and survives that stack's
# deploys and pulls; only its bootstrap-tls.sh wipes conf.d, and re-running this
# script puts it back.
#
# Held back until the certificate exists: nginx refuses to load a server block
# whose ssl_certificate is missing, and a broken file in that directory would
# also abort the autotrader deploys that reload the same nginx.
if docker run --rm -v autotrader-vps_letsencrypt:/le alpine \
test -f "/le/live/$FIRMER_HOST/fullchain.pem" 2>/dev/null; then
template=nginx/firmer.conf.template
else
log "No certificate for $FIRMER_HOST yet — rendering the HTTP-only config"
log "Run ./bootstrap-tls.sh once DNS points here, then this script again"
template=nginx/firmer-acme.conf.template
fi
log "Rendering $template"
# Named explicitly so envsubst leaves nginx's own $host, $remote_addr and the
# rest alone.
envsubst '${FIRMER_HOST}' < "$template" > "$SHARED_STACK/nginx/conf.d/firmer.conf"
log "Reloading the shared nginx"
docker exec nginx nginx -t
docker exec nginx nginx -s reload
log "Status"
docker compose ps