#!/usr/bin/env bash # ============================================================================= # Kitchen — Migrate from Production # ============================================================================= # Copies the live production kitchen_gp database and invoice files into the # new stack (kitchen_db on LXC 100 + invoice_data volume on LXC 110). # # Run this from the Proxmox host AFTER kitchen and kds are deployed: # bash /opt/kitchen/migrate-from-production.sh # # It is safe to re-run: the final step truncates and re-imports, so a second # run simply refreshes the data. Stop kitchen + kds before running if live. # # FILL IN SECTION BELOW before first run (D10: confirm production host + creds). # ============================================================================= set -euo pipefail # ── Production source (fill in) ────────────────────────────────────────────── PROD_HOST="" # e.g. 192.168.1.50 or laptop hostname PROD_PG_PORT="5432" PROD_PG_USER="postgres" PROD_DB="kitchen_gp" # production DB name PROD_DATA_DIR="" # path to invoice data on production, e.g. /home/user/data/invoices # ── Stack target ───────────────────────────────────────────────────────────── STACK_PG_HOST="10.10.10.100" STACK_PG_PORT="5432" STACK_PG_SUPER="postgres" # postgres superuser in hotel-manage-postgres container STACK_DB="kitchen_db" STACK_DB_USER="kitchen" # created by deploy_kitchen() KITCHEN_LXC=110 # ── Load KITCHEN_DB_PASS from credentials file ──────────────────────────────── CREDS_FILE=/root/hotel-manage-credentials.txt if [[ -f "$CREDS_FILE" ]]; then KITCHEN_DB_PASS=$(grep '^KITCHEN_DB_PASS=' "$CREDS_FILE" | cut -d= -f2- | head -1) fi if [[ -z "${KITCHEN_DB_PASS:-}" ]]; then echo "ERROR: KITCHEN_DB_PASS not found in ${CREDS_FILE}" >&2 exit 1 fi # ============================================================================= RD="\033[01;31m"; GN="\033[1;92m"; YW="\033[33m"; CL="\033[m" die() { printf "${RD}✗ %s${CL}\n" "$*" >&2; exit 1; } ok() { printf "${GN}✓ %s${CL}\n" "$*"; } inf() { printf "${YW}◌ %s${CL}\n" "$*"; } [[ -z "$PROD_HOST" ]] && die "PROD_HOST not set — edit this script first (see D10)" [[ -z "$PROD_DATA_DIR" ]] && die "PROD_DATA_DIR not set — edit this script first" DUMP_FILE="/tmp/kitchen_migration_$(date +%Y%m%d_%H%M%S).dump" # ============================================================================= # 1. Dump production DB # ============================================================================= inf "Dumping ${PROD_DB} from ${PROD_HOST}..." PGPASSWORD="${PROD_PG_PASS:-}" pg_dump \ -h "$PROD_HOST" -p "$PROD_PG_PORT" -U "$PROD_PG_USER" \ -Fc --no-owner --no-privileges \ "$PROD_DB" > "$DUMP_FILE" ok "Dump written to ${DUMP_FILE} ($(du -sh "$DUMP_FILE" | cut -f1))" # ============================================================================= # 2. Stop kitchen + kds so no writes arrive during restore # ============================================================================= inf "Stopping kitchen and kds containers..." pct exec 110 -- bash -c "cd /opt/kitchen && docker compose stop backend" 2>/dev/null || true pct exec 125 -- bash -c "cd /opt/kds && docker compose stop backend" 2>/dev/null || true ok "Services stopped" # ============================================================================= # 3. Drop + recreate kitchen_db (clean slate for restore) # ============================================================================= inf "Recreating ${STACK_DB} on ${STACK_PG_HOST}..." # Push dump file into LXC 100 for restore pct push 100 "$DUMP_FILE" /tmp/kitchen_restore.dump pct exec 100 -- bash -c " docker exec hotel-manage-postgres psql -U postgres -c \ \"SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '${STACK_DB}' AND pid <> pg_backend_pid();\" docker exec hotel-manage-postgres psql -U postgres -c \ \"DROP DATABASE IF EXISTS ${STACK_DB};\" docker exec hotel-manage-postgres psql -U postgres -c \ \"CREATE DATABASE ${STACK_DB} OWNER ${STACK_DB_USER};\" docker exec hotel-manage-postgres psql -U postgres -d ${STACK_DB} -c \ \"GRANT ALL ON SCHEMA public TO ${STACK_DB_USER};\" " ok "${STACK_DB} recreated" # ============================================================================= # 4. Restore dump into kitchen_db # ============================================================================= inf "Restoring dump into ${STACK_DB}..." pct exec 100 -- bash -c " docker cp /tmp/kitchen_restore.dump hotel-manage-postgres:/tmp/ docker exec hotel-manage-postgres pg_restore \ -U postgres -d ${STACK_DB} \ --no-owner --no-privileges \ --exit-on-error \ /tmp/kitchen_restore.dump docker exec hotel-manage-postgres rm -f /tmp/kitchen_restore.dump rm -f /tmp/kitchen_restore.dump " ok "Dump restored to ${STACK_DB}" # ============================================================================= # 5. Re-run KDS migrations (add KDS columns not in production dump) # ============================================================================= inf "Applying KDS schema additions to ${STACK_DB}..." pct exec 125 -- bash -c " cd /opt/kds docker compose run --rm -e DATABASE_URL=postgresql://${STACK_DB_USER}:${KITCHEN_DB_PASS}@${STACK_PG_HOST}:${STACK_PG_PORT}/${STACK_DB} \ backend python -c \" import asyncio from database import engine, Base from migrations.add_kds_tables import run_migration as m1 from migrations.add_kds_course_flow import run_migration as m2 from migrations.add_kds_order_tracking import run_migration as m3 from migrations.add_kds_bookings_refresh import run_migration as m4 async def run(): async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) for m in [m1, m2, m3, m4]: try: await m() except Exception as e: print(f'migration warning (may be ok): {e}') asyncio.run(run()) \"" || true ok "KDS schema additions applied" # ============================================================================= # 6. Rsync invoice data files # ============================================================================= inf "Syncing invoice data files from ${PROD_HOST}:${PROD_DATA_DIR} ..." # Find the docker volume mount on LXC 110 VOLUME_PATH=$(pct exec 110 -- bash -c " docker inspect kitchen-backend-1 2>/dev/null | \ python3 -c \"import sys,json; mounts=json.load(sys.stdin)[0]['Mounts']; \ print(next(m['Source'] for m in mounts if 'invoice_data' in m.get('Name','') or '/app/data' in m.get('Destination','')))\"\ " 2>/dev/null || echo "") if [[ -z "$VOLUME_PATH" ]]; then inf "Could not auto-detect invoice volume path — skipping file sync" inf "Manually rsync: ${PROD_HOST}:${PROD_DATA_DIR}/ → LXC 110's invoice_data volume" else rsync -avz --progress "${PROD_HOST}:${PROD_DATA_DIR}/" \ "root@${STACK_PG_HOST}:${VOLUME_PATH}/" || true ok "Invoice files synced to ${VOLUME_PATH}" fi # ============================================================================= # 7. Restart services # ============================================================================= inf "Restarting kitchen and kds..." pct exec 110 -- bash -c "cd /opt/kitchen && docker compose start backend" pct exec 125 -- bash -c "cd /opt/kds && docker compose start backend" ok "Services restarted" # ============================================================================= # 8. Verification report # ============================================================================= inf "Running verification report..." REPORT=$(pct exec 100 -- bash -c " docker exec hotel-manage-postgres psql -U postgres -d ${STACK_DB} -t -A -F'|' -c \" SELECT 'invoices' , COUNT(*) FROM invoices; SELECT 'line_items' , COUNT(*) FROM invoice_line_items; SELECT 'suppliers' , COUNT(*) FROM suppliers; SELECT 'ingredients' , COUNT(*) FROM ingredients; SELECT 'recipes' , COUNT(*) FROM recipes; SELECT 'resos_bookings', COUNT(*) FROM resos_bookings; \" " 2>/dev/null || echo "verification query failed") printf "\n${GN}══ Migration verification ══════════════════════════════════${CL}\n" while IFS='|' read -r table count; do printf " %-20s %s rows\n" "$table" "$count" done <<< "$REPORT" printf "\n" # Monthly invoice total sanity check (compare with production before cutover) MONTHLY=$(pct exec 100 -- bash -c " docker exec hotel-manage-postgres psql -U postgres -d ${STACK_DB} -t -A -F'|' -c \" SELECT TO_CHAR(invoice_date, 'YYYY-MM'), COUNT(*), ROUND(SUM(total_exc_vat)::numeric, 2) FROM invoices WHERE invoice_date >= NOW() - INTERVAL '6 months' GROUP BY 1 ORDER BY 1 DESC LIMIT 6; \" " 2>/dev/null || echo "") if [[ -n "$MONTHLY" ]]; then printf "${GN} Monthly invoice counts (last 6 months)${CL}\n" while IFS='|' read -r month cnt total; do printf " %s %4s invoices £%s\n" "$month" "$cnt" "$total" done <<< "$MONTHLY" fi printf "\n${GN}Done. Clean up dump file:${CL}\n" printf " rm -f %s\n\n" "$DUMP_FILE"