SPB Git forge

spb/vrai-prix

Public

Vrai-Prix — l'évaluation du vrai prix des propriétés résidentielles au Québec.

60commits 1branches 0releases
12.3 MBsize
maindefault branch
17 days agolast push
TypeScript 90.2% JavaScript 3.5% Python 3.4% CSS 1.9% HTML 0.6%

fix(analyse IA): analyses orphelines — une analyse interrompue par un redémarrage du serveur (pm2 restart, déploiement) est marquée échouée après 20 min (store.failOrphans, appelé au démarrage d'une analyse et à chaque lecture) au lieu de rester « en cours » indéfiniment

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Simon-Pierre Boucher committed 17 days ago (Sep 8, 2026) parent 87d2922

2 changed files +16 −0

modified src/lib/cost/ai/service.ts +3 −0
@@ -45,6 +45,7 @@ export async function startAnalysis(uid: string, opts: { force?: boolean } = {})
45 45 if (!ctx) throw new AnalysisError("Annonce introuvable.", 404, "not_found");
46 46 if (!ctx.imageUrls.length) throw new AnalysisError("Cette annonce n'a aucune photo : l'analyse multimodale n'est pas possible.", 422, "no_images");
47 47 if (!process.env.ANTHROPIC_API_KEY) throw new AnalysisError("Modèle indisponible (clé API absente côté serveur).", 503, "model_unavailable");
48 + store.failOrphans(); // une analyse figée par un redémarrage ne doit jamais bloquer une nouvelle demande
48 49 const active = store.findActive(uid);
49 50 if (active) return { analysisId: active.id, status: active.status, reused: true, version: active.version };
50 51 // empreinte provisoire (sans hashes d'images) pour la réutilisation rapide ; l'empreinte finale inclut les hashes
@@ -181,10 +182,12 @@ export function viewAnalysis(row: store.AnalysisRow): AnalysisView {
181 182 }
182 183
183 184 export function getLatest(uid: string): AnalysisView | null {
185 + store.failOrphans();
184 186 const row = store.latestForListing(uid);
185 187 return row ? viewAnalysis(row) : null;
186 188 }
187 189 export function getById(id: string): AnalysisView | null {
190 + store.failOrphans();
188 191 const row = store.getAnalysis(id);
189 192 return row ? viewAnalysis(row) : null;
190 193 }
modified src/lib/cost/ai/store.ts +13 −0
@@ -74,6 +74,19 @@ export function setStage(id: string, status: AnalysisStatus): void {
74 74 export function setFailed(id: string, error: string): void {
75 75 db().prepare("UPDATE listing_ai_analyses SET status='failed', stage='failed', error=?, completed_at=datetime('now') WHERE id=?").run(error.slice(0, 2000), id);
76 76 }
77 +/**
78 + * Analyses orphelines : une analyse tourne dans le processus du serveur ; un
79 + * redémarrage (pm2 restart, déploiement) l'interrompt sans transition vers
80 + * `failed`, et l'interface resterait sur « analyse en cours » indéfiniment.
81 + * Toute analyse non terminale plus vieille que `maxAgeMin` est marquée échouée
82 + * (les plus longues observées : ≈ 2 min pour 27-36 photos). Renvoie le nombre corrigé.
83 + */
84 +export function failOrphans(maxAgeMin = 20): number {
85 + const r = db().prepare(
86 + "UPDATE listing_ai_analyses SET status='failed', stage='failed', error=?, completed_at=datetime('now') WHERE status NOT IN ('completed','failed') AND COALESCE(started_at, created_at) < datetime('now', ?)",
87 + ).run("Analyse interrompue (redémarrage du serveur pendant le traitement) — relancez l'analyse.", `-${Math.max(1, Math.round(maxAgeMin))} minutes`);
88 + return r.changes;
89 +}
77 90 export function saveOutput(id: string, o: { outputJson: string; imageCount: number; usageJson: string; issuesJson: string; imagesJson: string }): void {
78 91 db().prepare("UPDATE listing_ai_analyses SET output_json=?, image_count=?, usage_json=?, issues_json=?, images_json=? WHERE id=?").run(o.outputJson, o.imageCount, o.usageJson, o.issuesJson, o.imagesJson, id);
79 92 }
80 93