- TanStack Start (React 19, Tailwind 4) frontend recreated from Stitch design - PocketBase backend with schema migration + idempotent content seed - Single Docker image running both services (non-root, healthchecked) - deploy.sh to build & push to registry.tanshu.com - Ansible playbook (roles: app, caddy) deploying to www.greatbear.in with Caddy TLS entries for the site and the PocketBase admin
123 lines
5.1 KiB
Bash
Executable File
123 lines
5.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# One-off helper used during development to create the Great Bear collections
|
|
# on a running PocketBase instance via the admin API.
|
|
# Usage: PB_URL=http://127.0.0.1:8099 PB_EMAIL=... PB_PASSWORD=... ./scripts/create-collections.sh
|
|
set -euo pipefail
|
|
|
|
PB_URL="${PB_URL:-http://127.0.0.1:8099}"
|
|
PB_EMAIL="${PB_EMAIL:?PB_EMAIL required}"
|
|
PB_PASSWORD="${PB_PASSWORD:?PB_PASSWORD required}"
|
|
|
|
TOKEN=$(curl -sS -X POST "$PB_URL/api/collections/_superusers/auth-with-password" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{\"identity\":\"$PB_EMAIL\",\"password\":\"$PB_PASSWORD\"}" \
|
|
| python3 -c "import sys,json;print(json.load(sys.stdin)['token'])")
|
|
echo "authenticated: ${TOKEN:0:12}..."
|
|
|
|
WORKDIR=$(mktemp -d)
|
|
trap 'rm -rf "$WORKDIR"' EXIT
|
|
cd "$WORKDIR"
|
|
|
|
content_rules='"listRule":"","viewRule":"","createRule":null,"updateRule":null,"deleteRule":null'
|
|
# Reservations: visitors may create (forms) but only admins may read/update.
|
|
reservation_rules='"listRule":null,"viewRule":null,"createRule":"","updateRule":null,"deleteRule":null'
|
|
mk() { NAME=$1; FIELDS=$2; IDX=$3; RULES=${4:-$content_rules}; cat > "$NAME.json" <<EOF
|
|
{"name":"$NAME","type":"base",$RULES,"fields":$FIELDS,"indexes":$IDX}
|
|
EOF
|
|
}
|
|
|
|
mk brews '[
|
|
{"name":"uid","type":"text","required":true},
|
|
{"name":"name","type":"text","required":true},
|
|
{"name":"style","type":"text","required":true},
|
|
{"name":"short_name","type":"text"},
|
|
{"name":"abv","type":"number"},
|
|
{"name":"ibu","type":"number"},
|
|
{"name":"description","type":"text"},
|
|
{"name":"tasting_notes","type":"text"},
|
|
{"name":"pairing","type":"text"},
|
|
{"name":"image","type":"text"},
|
|
{"name":"featured","type":"bool"},
|
|
{"name":"flagship","type":"bool"},
|
|
{"name":"on_tap","type":"bool"},
|
|
{"name":"seasonal","type":"bool"},
|
|
{"name":"seasonal_icon","type":"text"},
|
|
{"name":"flight","type":"bool"},
|
|
{"name":"sort","type":"number"}]' '["CREATE UNIQUE INDEX idx_brews_uid ON brews (uid)"]'
|
|
|
|
mk stats '[
|
|
{"name":"uid","type":"text","required":true},
|
|
{"name":"value","type":"text","required":true},
|
|
{"name":"label","type":"text","required":true},
|
|
{"name":"sort","type":"number"}]' '["CREATE UNIQUE INDEX idx_stats_uid ON stats (uid)"]'
|
|
|
|
mk experiences '[
|
|
{"name":"uid","type":"text","required":true},
|
|
{"name":"title","type":"text","required":true},
|
|
{"name":"description","type":"text"},
|
|
{"name":"icon","type":"text"},
|
|
{"name":"image","type":"text"},
|
|
{"name":"sort","type":"number"}]' '["CREATE UNIQUE INDEX idx_experiences_uid ON experiences (uid)"]'
|
|
|
|
mk testimonials '[
|
|
{"name":"uid","type":"text","required":true},
|
|
{"name":"name","type":"text","required":true},
|
|
{"name":"role","type":"text"},
|
|
{"name":"quote","type":"text","required":true},
|
|
{"name":"rating","type":"number"},
|
|
{"name":"sort","type":"number"}]' '["CREATE UNIQUE INDEX idx_testimonials_uid ON testimonials (uid)"]'
|
|
|
|
mk menu_categories '[
|
|
{"name":"uid","type":"text","required":true},
|
|
{"name":"slug","type":"text","required":true},
|
|
{"name":"name","type":"text","required":true},
|
|
{"name":"sort","type":"number"}]' '["CREATE UNIQUE INDEX idx_menu_categories_uid ON menu_categories (uid)","CREATE UNIQUE INDEX idx_menu_categories_slug ON menu_categories (slug)"]'
|
|
|
|
mk menu_items '[
|
|
{"name":"uid","type":"text","required":true},
|
|
{"name":"category","type":"text","required":true},
|
|
{"name":"name","type":"text","required":true},
|
|
{"name":"description","type":"text"},
|
|
{"name":"price","type":"number"},
|
|
{"name":"pairing","type":"text"},
|
|
{"name":"tag","type":"text"},
|
|
{"name":"image","type":"text"},
|
|
{"name":"sort","type":"number"}]' '["CREATE UNIQUE INDEX idx_menu_items_uid ON menu_items (uid)"]'
|
|
|
|
mk pairings '[
|
|
{"name":"uid","type":"text","required":true},
|
|
{"name":"title","type":"text","required":true},
|
|
{"name":"description","type":"text"},
|
|
{"name":"sort","type":"number"}]' '["CREATE UNIQUE INDEX idx_pairings_uid ON pairings (uid)"]'
|
|
|
|
mk settings '[
|
|
{"name":"uid","type":"text","required":true},
|
|
{"name":"brand","type":"text","required":true},
|
|
{"name":"footer_about","type":"text"},
|
|
{"name":"address_line","type":"text"},
|
|
{"name":"city_line","type":"text"},
|
|
{"name":"hours_line","type":"text"},
|
|
{"name":"hours_multiline","type":"text"},
|
|
{"name":"phone_primary","type":"text"},
|
|
{"name":"phone_secondary","type":"text"},
|
|
{"name":"instagram_url","type":"text"},
|
|
{"name":"instagram_handle","type":"text"},
|
|
{"name":"maps_query","type":"text"}]' '["CREATE UNIQUE INDEX idx_settings_uid ON settings (uid)"]'
|
|
|
|
mk reservations '[
|
|
{"name":"name","type":"text","required":true},
|
|
{"name":"phone","type":"text","required":true},
|
|
{"name":"source","type":"text","required":true},
|
|
{"name":"date","type":"text"},
|
|
{"name":"time","type":"text"},
|
|
{"name":"guests","type":"text"},
|
|
{"name":"requests","type":"text"},
|
|
{"name":"status","type":"select","maxSelect":1,"values":["new","confirmed","cancelled"]}]' '[]' "$reservation_rules"
|
|
|
|
for NAME in brews stats experiences testimonials menu_categories menu_items pairings settings reservations; do
|
|
CODE=$(curl -sS -o "resp_$NAME.json" -w "%{http_code}" -X POST "$PB_URL/api/collections" \
|
|
-H "Authorization: $TOKEN" -H "Content-Type: application/json" -d @"$NAME.json")
|
|
echo "$NAME -> HTTP $CODE"
|
|
if [ "$CODE" != "200" ]; then cat "resp_$NAME.json"; echo; fi
|
|
done
|