Files
artcraft/script/bootstrap/common.sh
Tanish Manku 335015d9e2 Dev infra: fully-local generation and gallery for the dev stack
Closes the two gaps found smoke-testing the local stack: an empty gallery
(zero media_files rows, media URLs hardcoded to the remote r2.dev bucket)
and generation flows that could not complete (omni_gen calls providers
synchronously in the handler, so dummy keys meant a 500 after billing;
the webapp blocked submits earlier anyway because signup grants no wallet).

Backend (all development-only):
- CDN_BASE_URL env override in cdn_link.rs (host-only, validated loudly;
  precedence over the compiled-in hosts for every domain/env) and a
  dev-gated actix-files mount serving LOCAL_MEDIA_ROOT/media at /media,
  mirroring the public bucket layout - media URLs in API responses resolve
  against the backend itself
- DEV_FAKE_GENERATION flag on ServerState (easyenv bool, forced off outside
  Development): the omni_gen image/video pipelines skip the external
  provider call and return a synthetic Fal enqueue (external id fake_<uuid>),
  so billing, prompt insert, and the pending generic_inference_jobs row all
  stay real
- dev_fake_generation_resolver_thread: polls pending fake_ jobs (new
  runtime-query list_pending_fake_generation_jobs in mysql_queries) and
  after DEV_FAKE_GENERATION_RESOLVE_SECS (default 6s) marks them
  complete_success with a media_files row backed by a test_data asset
  copied under LOCAL_MEDIA_ROOT (mp4 results get -thumb.jpg/-thumb.gif
  siblings since the thumbnail job never runs locally), or complete_failure
  via the existing simulate_artcraft_failure prompt triggers (category word
  supported, e.g. "simulate_artcraft_failure face_not_detected")

Bootstrap/seeds (PS + bash kept in lockstep):
- seed_demo_user seeds an artcraft wallet (DEMO_CREDITS, default 100k
  banked + ledger rows; idempotent top-up) - signup creates no wallet and
  the webapp gates generate on balance, which is why submits went nowhere
- seed_demo_user also seeds 6 gallery images (test_data jpgs) with objects
  under .devstack/media in the exact bucket fan-out layout
- fixed the already-exists detection in seed_demo_user: create_account
  returns error_type, not error_code, so re-runs died on UsernameTaken
- generated secrets env gains DEV_FAKE_GENERATION/CDN_BASE_URL/
  LOCAL_MEDIA_ROOT (appended to existing files on bootstrap re-runs);
  stale "providers are only contacted by workers" comments corrected
- doctor checks wallet balance, media rows, and the new secrets keys

Docs: dev_setup_local_stack.md documents the new flags, corrects the
sync-provider-call and upload-500 claims, updates the seams table and
verification checklist; storyteller_web CLAUDE.md video model examples
fixed (seedance_2p0_fast_g -> seedance_2p0_fast).

Verified E2E on this machine: gallery lists 8 items on localhost URLs;
image and video submits go pending ~6s then complete with locally-served
results (mp4 + gif preview 200); failure trigger yields complete_failure
with category face_not_detected; credits genuinely deducted (100000 ->
99932); doctor 0 failures / 0 warnings.
2026-07-29 20:48:31 -07:00

122 lines
4.1 KiB
Bash
Executable File

#!/usr/bin/env bash
# Shared helpers for the dev-stack bootstrap scripts.
#
# Usage (from a script that has set ${root_dir}):
#
# source "${root_dir}/script/bootstrap/common.sh"
#
# See _docs/dev_setup_local_stack.md for the full picture.
# Canonical local-dev database identity. These match _docs/dev_setup_server.md,
# the diesel DATABASE_URL in the repo-root .env, and the server's MYSQL_URL
# default (crates/schema/config/shared_env_var_config/src/mysql.rs). Override
# via environment only if your local MySQL differs — most tooling assumes them.
DEV_MYSQL_DB="${DEV_MYSQL_DB:-storyteller}"
DEV_MYSQL_USER="${DEV_MYSQL_USER:-storyteller}"
DEV_MYSQL_PASSWORD="${DEV_MYSQL_PASSWORD:-password}"
DEV_MYSQL_HOST="${DEV_MYSQL_HOST:-localhost}"
# Where the backend listens (BIND_ADDRESS default in
# storyteller_web/src/startup/build_dependencies.rs).
DEV_BACKEND_URL="${DEV_BACKEND_URL:-http://localhost:12345}"
# Default demo account. The username must NOT be on the reserved-usernames list
# (includes/binary_includes/usernames/reserved_usernames.txt — "demo", "admin",
# "test", and "dev" are all reserved) and must match ^[A-Za-z0-9_\-]{3,16}$.
DEMO_USERNAME="${DEMO_USERNAME:-localdev1}"
DEMO_PASSWORD="${DEMO_PASSWORD:-localdev1pass}"
DEMO_EMAIL="${DEMO_EMAIL:-localdev1@example.com}"
# Banked credits the demo user's artcraft wallet is seeded/topped-up to.
# Generation submits are gated on wallet balance, so 0 credits means the
# generate button silently refuses in the webapp.
DEMO_CREDITS="${DEMO_CREDITS:-100000}"
# Local media served by the backend at /media when LOCAL_MEDIA_ROOT points
# here (fully-local gallery + fake generation results). Mirrors the public
# bucket layout: object path /media/{...} lives at .devstack/media/media/{...}.
# NB: callers must have set ${root_dir} before sourcing this file.
DEV_MEDIA_ROOT="${DEV_MEDIA_ROOT:-${root_dir}/.devstack/media}"
log() { echo "[bootstrap] $*"; }
warn() { echo "[bootstrap] WARNING: $*" >&2; }
die() { echo "[bootstrap] ERROR: $*" >&2; exit 1; }
step() {
echo ""
echo "=== $* ==="
}
# sudo unless we already are root (CI containers often run as root without sudo).
maybe_sudo() {
if [ "$(id -u)" -eq 0 ]; then
"$@"
else
sudo "$@"
fi
}
confirm() {
local prompt="$1"
if [ "${ASSUME_YES:-false}" = "true" ]; then
return 0
fi
local reply
read -r -p "${prompt} [y/N] " reply
[[ "${reply}" =~ ^[Yy] ]]
}
# Generate a token in the backend's format: {prefix}{crockford-lower entropy}
# padded to the same TOTAL length the Rust generators use (tokens crate,
# impl_crockford_generator!). Alphabet excludes i/l/o/u like Crockford base32.
new_dev_token() {
local prefix="$1" total_length="$2"
local charset="0123456789abcdefghjkmnpqrstvwxyz"
local count=$((total_length - ${#prefix}))
local entropy=""
local i
for ((i = 0; i < count; i++)); do
entropy+="${charset:RANDOM%32:1}"
done
echo "${prefix}${entropy}"
}
# Run a statement (or stdin) as the storyteller app user against the dev DB.
# MYSQL_PWD keeps the password off the process list.
mysql_app() {
MYSQL_PWD="${DEV_MYSQL_PASSWORD}" mysql \
-u "${DEV_MYSQL_USER}" -h "${DEV_MYSQL_HOST}" -D "${DEV_MYSQL_DB}" "$@"
}
# Run a statement as an administrator, for provisioning the DB and user.
# On a fresh Ubuntu/WSL install, root uses auth_socket, so `sudo mysql` works.
# On GitHub Actions runners the preinstalled MySQL root password is "root" —
# export MYSQL_ROOT_PASSWORD to use password auth instead.
mysql_admin() {
if [ -n "${MYSQL_ROOT_PASSWORD:-}" ]; then
MYSQL_PWD="${MYSQL_ROOT_PASSWORD}" mysql -u root -h "${DEV_MYSQL_HOST}" "$@"
else
maybe_sudo mysql "$@"
fi
}
mysql_app_reachable() {
mysql_app -e "SELECT 1" >/dev/null 2>&1
}
redis_reachable() {
redis-cli ping 2>/dev/null | grep -q PONG
}
backend_reachable() {
curl -sf -o /dev/null --max-time 3 "${DEV_BACKEND_URL}/_status"
}
# Make ~/.cargo/bin visible when rustup was installed but the shell profile
# hasn't been re-sourced (fresh installs, CI).
ensure_cargo_on_path() {
if ! command -v cargo >/dev/null 2>&1 && [ -x "${HOME}/.cargo/bin/cargo" ]; then
export PATH="${HOME}/.cargo/bin:${PATH}"
fi
}