Vrai-Prix — l'évaluation du vrai prix des propriétés résidentielles au Québec.
TypeScript 90.2%
JavaScript 3.5%
Python 3.4%
CSS 1.9%
HTML 0.6%
1#!/bin/zsh -l2# Auteur : Simon-Pierre Boucher — contact@spboucher.ai3# scripts/immoka-sync.sh — rafraîchissement nocturne des propriétés à vendre.4#5# 1. instantané cohérent de la base Immo-Ka (sqlite3 .backup) sur le nœud6# hôte d'Immo-Ka (M4M64b, LAN — SSH inter-nœuds via Tailscale bloqué) ;7# 2. rapatriement rsync (delta contre la copie précédente) → data/immoka.db.new ;8# 3. ré-évaluation de toutes les annonces vivantes par le moteur Vrai-Prix9# (scripts/run-eval-shards.sh, N shards parallèles) → table vp_eval ;10# 4. agrégats marché → data/marche-stats.json (lu à chaud par /stats et11# /stats/marche via src/lib/marche-stats.ts — pas de rebuild) ;12# 5. index + FTS5 pré-construits (src/lib/immoka.ts) ;13# 6. bascule atomique data/immoka.db.new → data/immoka.db, puis pm2 restart14# (le handle better-sqlite3 du serveur est un singleton : sans restart il15# continuerait de lire l'ancien inode).16#17# Usage (racine du dépôt, sur le nœud) :18# scripts/immoka-sync.sh # complet19# SKIP_FETCH=1 scripts/immoka-sync.sh # réutilise data/immoka.db.new déjà téléchargé20# SHARDS=24 scripts/immoka-sync.sh # nombre de processus d'évaluation (défaut : cœurs − 4)21# NO_RESTART=1 … # ne redémarre pas PM2 (tests)22#23# Variables : IMMOKA_HOST (défaut MacStudio-de-simon-pierre-4.local = M4M64b),24# IMMOKA_REMOTE_DB (défaut ~/apps/immo-ka/data/immoka.db),25# IMMOKA_SSH_KEY (défaut ~/.ssh/ka_guardian_ed25519, autorisée sur M4M64b),26# PM2_APP (défaut vrai-prix).27set -u28set -o pipefail29cd "$(dirname "$0")/.."30export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:$PATH"3132IMMOKA_HOST=${IMMOKA_HOST:-MacStudio-de-simon-pierre-4.local}33IMMOKA_REMOTE_DB=${IMMOKA_REMOTE_DB:-/Users/simon-pierreboucher/apps/immo-ka/data/immoka.db}34IMMOKA_SSH_KEY=${IMMOKA_SSH_KEY:-$HOME/.ssh/ka_guardian_ed25519}35PM2_APP=${PM2_APP:-vrai-prix}36NCPU=$(sysctl -n hw.ncpu 2>/dev/null || echo 8)37SHARDS=${SHARDS:-$(( NCPU > 8 ? NCPU - 4 : 4 ))}38LIVE=data/immoka.db39NEW=data/immoka.db.new40SNAP=/tmp/immoka-vp-snap.db41LOG=tmp/immoka-sync.log42mkdir -p data tmp43T0=$(date +%s)44log() { local m="[$(date '+%Y-%m-%d %H:%M:%S')] $*"; echo "$m"; echo "$m" >> "$LOG"; }45die() { log "ÉCHEC : $*"; exit 1; }4647SSH_OPTS=(-i "$IMMOKA_SSH_KEY" -o IdentitiesOnly=yes -o BatchMode=yes -o ConnectTimeout=1548 -o StrictHostKeyChecking=accept-new -o ControlMaster=no -o ControlPath=none)49REMOTE="simon-pierreboucher@$IMMOKA_HOST"5051log "=== immoka-sync — hôte $IMMOKA_HOST · shards $SHARDS ==="5253if [[ "${SKIP_FETCH:-0}" != "1" ]]; then54 # 1. instantané cohérent côté Immo-Ka (la base est chaude : WAL en écriture continue)55 log "1/6 instantané distant (.backup)…"56 ssh "${SSH_OPTS[@]}" "$REMOTE" "rm -f $SNAP && /usr/bin/sqlite3 '$IMMOKA_REMOTE_DB' '.backup $SNAP' && ls -l $SNAP" >> "$LOG" 2>&1 \57 || die "instantané distant impossible (SSH LAN vers $IMMOKA_HOST ?)"5859 # 2. rapatriement (delta contre la copie précédente si elle existe)60 log "2/6 rsync → $NEW…"61 [[ -f "$LIVE" && ! -f "$NEW" ]] && cp "$LIVE" "$NEW"62 rsync -a --inplace --no-whole-file -e "ssh ${SSH_OPTS[*]}" "$REMOTE:$SNAP" "$NEW" >> "$LOG" 2>&1 \63 || die "rsync de l'instantané"64 ssh "${SSH_OPTS[@]}" "$REMOTE" "rm -f $SNAP" >> "$LOG" 2>&165 rm -f "$NEW-wal" "$NEW-shm"66fi67[[ -f "$NEW" ]] || die "$NEW absent"6869N_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)70log " annonces vivantes dans l'instantané : $N_LIVE"71(( N_LIVE > 10000 )) || die "instantané suspect ($N_LIVE annonces vivantes) — bascule annulée"7273# 3. ré-évaluation par le moteur (shards parallèles) → tmp/eval-shards/*.jsonl74log "3/6 évaluation de $N_LIVE annonces ($SHARDS shards)…"75IMMOKA_DB="$PWD/$NEW" SHARDS=$SHARDS scripts/run-eval-shards.sh >> "$LOG" 2>&1 || die "run-eval-shards"76N_EVAL=$(cat tmp/eval-shards/*.jsonl 2>/dev/null | wc -l | tr -d ' ')77log " $N_EVAL évaluations produites"78(( N_EVAL > 10000 )) || die "trop peu d'évaluations ($N_EVAL) — bascule annulée"7980# 4. table vp_eval + agrégats marché (fichier lu à chaud par le site)81log "4/6 table vp_eval + data/marche-stats.json…"82IMMOKA_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"8384# 5. index + FTS5 (sinon la première requête du site les construirait)85log "5/6 index + FTS5…"86IMMOKA_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"8788# 6. bascule atomique + restart89log "6/6 bascule → $LIVE + restart $PM2_APP…"90mv -f data/marche-stats.json.new data/marche-stats.json91rm -f "$LIVE-wal" "$LIVE-shm"92mv -f "$NEW" "$LIVE"93rm -f "$NEW-wal" "$NEW-shm"94if [[ "${NO_RESTART:-0}" != "1" ]] && command -v pm2 >/dev/null 2>&1; then95 pm2 restart "$PM2_APP" --update-env >> "$LOG" 2>&1 || log "avertissement : pm2 restart $PM2_APP a échoué"96fi97rm -f tmp/eval-shards/*.jsonl98log "=== terminé en $(( $(date +%s) - T0 )) s — $N_LIVE annonces, $N_EVAL évaluées ==="99