#!/bin/zsh -l # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # scripts/immoka-sync.sh — rafraîchissement nocturne des propriétés à vendre. # # 1. instantané cohérent de la base Immo-Ka (sqlite3 .backup) sur le nœud # hôte d'Immo-Ka (M4M64b, LAN — SSH inter-nœuds via Tailscale bloqué) ; # 2. rapatriement rsync (delta contre la copie précédente) → data/immoka.db.new ; # 3. ré-évaluation de toutes les annonces vivantes par le moteur Vrai-Prix # (scripts/run-eval-shards.sh, N shards parallèles) → table vp_eval ; # 4. agrégats marché → data/marche-stats.json (lu à chaud par /stats et # /stats/marche via src/lib/marche-stats.ts — pas de rebuild) ; # 5. index + FTS5 pré-construits (src/lib/immoka.ts) ; # 6. bascule atomique data/immoka.db.new → data/immoka.db, puis pm2 restart # (le handle better-sqlite3 du serveur est un singleton : sans restart il # continuerait de lire l'ancien inode). # # Usage (racine du dépôt, sur le nœud) : # scripts/immoka-sync.sh # complet # SKIP_FETCH=1 scripts/immoka-sync.sh # réutilise data/immoka.db.new déjà téléchargé # SHARDS=24 scripts/immoka-sync.sh # nombre de processus d'évaluation (défaut : cœurs − 4) # NO_RESTART=1 … # ne redémarre pas PM2 (tests) # # Variables : IMMOKA_HOST (défaut MacStudio-de-simon-pierre-4.local = M4M64b), # IMMOKA_REMOTE_DB (défaut ~/apps/immo-ka/data/immoka.db), # IMMOKA_SSH_KEY (défaut ~/.ssh/ka_guardian_ed25519, autorisée sur M4M64b), # PM2_APP (défaut vrai-prix). set -u set -o pipefail cd "$(dirname "$0")/.." export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:$PATH" IMMOKA_HOST=${IMMOKA_HOST:-MacStudio-de-simon-pierre-4.local} IMMOKA_REMOTE_DB=${IMMOKA_REMOTE_DB:-/Users/simon-pierreboucher/apps/immo-ka/data/immoka.db} IMMOKA_SSH_KEY=${IMMOKA_SSH_KEY:-$HOME/.ssh/ka_guardian_ed25519} PM2_APP=${PM2_APP:-vrai-prix} NCPU=$(sysctl -n hw.ncpu 2>/dev/null || echo 8) SHARDS=${SHARDS:-$(( NCPU > 8 ? NCPU - 4 : 4 ))} LIVE=data/immoka.db NEW=data/immoka.db.new SNAP=/tmp/immoka-vp-snap.db LOG=tmp/immoka-sync.log mkdir -p data tmp T0=$(date +%s) log() { local m="[$(date '+%Y-%m-%d %H:%M:%S')] $*"; echo "$m"; echo "$m" >> "$LOG"; } die() { log "ÉCHEC : $*"; exit 1; } SSH_OPTS=(-i "$IMMOKA_SSH_KEY" -o IdentitiesOnly=yes -o BatchMode=yes -o ConnectTimeout=15 -o StrictHostKeyChecking=accept-new -o ControlMaster=no -o ControlPath=none) REMOTE="simon-pierreboucher@$IMMOKA_HOST" log "=== immoka-sync — hôte $IMMOKA_HOST · shards $SHARDS ===" if [[ "${SKIP_FETCH:-0}" != "1" ]]; then # 1. instantané cohérent côté Immo-Ka (la base est chaude : WAL en écriture continue) log "1/6 instantané distant (.backup)…" ssh "${SSH_OPTS[@]}" "$REMOTE" "rm -f $SNAP && /usr/bin/sqlite3 '$IMMOKA_REMOTE_DB' '.backup $SNAP' && ls -l $SNAP" >> "$LOG" 2>&1 \ || die "instantané distant impossible (SSH LAN vers $IMMOKA_HOST ?)" # 2. rapatriement (delta contre la copie précédente si elle existe) log "2/6 rsync → $NEW…" [[ -f "$LIVE" && ! -f "$NEW" ]] && cp "$LIVE" "$NEW" rsync -a --inplace --no-whole-file -e "ssh ${SSH_OPTS[*]}" "$REMOTE:$SNAP" "$NEW" >> "$LOG" 2>&1 \ || die "rsync de l'instantané" ssh "${SSH_OPTS[@]}" "$REMOTE" "rm -f $SNAP" >> "$LOG" 2>&1 rm -f "$NEW-wal" "$NEW-shm" fi [[ -f "$NEW" ]] || die "$NEW absent" N_LIVE=$(/usr/bin/sqlite3 "$NEW" "select count(*) from listings where status='a-vendre' and published=1 and active=1 and dup_hidden=0 and price>0" 2>/dev/null || echo 0) log " annonces vivantes dans l'instantané : $N_LIVE" (( N_LIVE > 10000 )) || die "instantané suspect ($N_LIVE annonces vivantes) — bascule annulée" # 3. ré-évaluation par le moteur (shards parallèles) → tmp/eval-shards/*.jsonl log "3/6 évaluation de $N_LIVE annonces ($SHARDS shards)…" IMMOKA_DB="$PWD/$NEW" SHARDS=$SHARDS scripts/run-eval-shards.sh >> "$LOG" 2>&1 || die "run-eval-shards" N_EVAL=$(cat tmp/eval-shards/*.jsonl 2>/dev/null | wc -l | tr -d ' ') log " $N_EVAL évaluations produites" (( N_EVAL > 10000 )) || die "trop peu d'évaluations ($N_EVAL) — bascule annulée" # 4. table vp_eval + agrégats marché (fichier lu à chaud par le site) log "4/6 table vp_eval + data/marche-stats.json…" IMMOKA_DB="$PWD/$NEW" MARCHE_STATS_OUT="$PWD/data/marche-stats.json.new" node scripts/build-marche-stats.mjs >> "$LOG" 2>&1 || die "build-marche-stats" # 5. index + FTS5 (sinon la première requête du site les construirait) log "5/6 index + FTS5…" IMMOKA_DB="$PWD/$NEW" npx tsx -e "import('./src/lib/immoka.ts').then((m) => { m.getImmoDb(); console.log('index ok'); })" >> "$LOG" 2>&1 || die "index/FTS" # 6. bascule atomique + restart log "6/6 bascule → $LIVE + restart $PM2_APP…" mv -f data/marche-stats.json.new data/marche-stats.json rm -f "$LIVE-wal" "$LIVE-shm" mv -f "$NEW" "$LIVE" rm -f "$NEW-wal" "$NEW-shm" if [[ "${NO_RESTART:-0}" != "1" ]] && command -v pm2 >/dev/null 2>&1; then pm2 restart "$PM2_APP" --update-env >> "$LOG" 2>&1 || log "avertissement : pm2 restart $PM2_APP a échoué" fi rm -f tmp/eval-shards/*.jsonl log "=== terminé en $(( $(date +%s) - T0 )) s — $N_LIVE annonces, $N_EVAL évaluées ==="