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>
52 lines
2.4 KiB
Bash
Executable File
52 lines
2.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# First run only: get a certificate for FIRMER_HOST, then hand off to deploy.sh.
|
|
#
|
|
# This uses the autotrader stack's certbot container and its Let's Encrypt
|
|
# account, so it agrees to the Subscriber Agreement the same way that stack's
|
|
# own bootstrap did (certbot --agree-tos):
|
|
# https://letsencrypt.org/repository/
|
|
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}"
|
|
|
|
# ACME_EMAIL is not duplicated here: it belongs to the account that already
|
|
# holds the other certificates on this box.
|
|
[ -f "$SHARED_STACK/.env" ] || fail "no .env under $SHARED_STACK"
|
|
ACME_EMAIL=$(grep -E '^ACME_EMAIL=' "$SHARED_STACK/.env" | cut -d= -f2-)
|
|
[ -n "$ACME_EMAIL" ] || fail "ACME_EMAIL is not set in $SHARED_STACK/.env"
|
|
|
|
# Let's Encrypt rate-limits failures, so check DNS before spending an attempt.
|
|
log "Checking DNS"
|
|
ip=$(curl -fsS --max-time 5 http://169.254.169.254/metadata/v1/interfaces/public/0/ipv4/address 2>/dev/null \
|
|
|| curl -fsS --max-time 5 https://api.ipify.org 2>/dev/null) \
|
|
|| fail "could not determine this droplet's public IP"
|
|
got=$(getent ahostsv4 "$FIRMER_HOST" 2>/dev/null | awk 'NR==1{print $1}') || true
|
|
echo " droplet: $ip"
|
|
echo " $FIRMER_HOST -> ${got:-<unresolved>}"
|
|
[ "$got" = "$ip" ] || fail "$FIRMER_HOST does not resolve to $ip — fix DNS and wait for it to propagate"
|
|
|
|
# The challenge is served over plain HTTP by the shared nginx, which needs a
|
|
# server block for this name before it will answer for it.
|
|
log "Rendering the HTTP-only config so the challenge can be served"
|
|
envsubst '${FIRMER_HOST}' < nginx/firmer-acme.conf.template > "$SHARED_STACK/nginx/conf.d/firmer.conf"
|
|
docker exec nginx nginx -t
|
|
docker exec nginx nginx -s reload
|
|
|
|
log "Requesting a certificate for $FIRMER_HOST"
|
|
docker compose --project-directory "$SHARED_STACK" -f "$SHARED_STACK/docker-compose.yml" \
|
|
run --rm --entrypoint certbot certbot \
|
|
certonly --webroot -w /var/www/certbot \
|
|
--email "$ACME_EMAIL" --agree-tos --no-eff-email \
|
|
--non-interactive -d "$FIRMER_HOST" \
|
|
|| fail "certbot failed — check DNS and that port 80 reaches this droplet"
|
|
|
|
log "Switching to the TLS config"
|
|
exec ./deploy.sh
|