SPB Git

Repository Public

Commit

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 · Sep 8, 2026 parent 87d2922
2 changed files +16 −0 Browse files
modified src/lib/cost/ai/service.ts +3 −0
@@ -45,6 +45,7 @@ export async function startAnalysis(uid: string, opts: { force?: boolean } = {})
45 if (!ctx) throw new AnalysisError("Annonce introuvable.", 404, "not_found"); 45 if (!ctx) throw new AnalysisError("Annonce introuvable.", 404, "not_found");
46 if (!ctx.imageUrls.length) throw new AnalysisError("Cette annonce n'a aucune photo : l'analyse multimodale n'est pas possible.", 422, "no_images"); 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 if (!process.env.ANTHROPIC_API_KEY) throw new AnalysisError("Modèle indisponible (clé API absente côté serveur).", 503, "model_unavailable"); 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 const active = store.findActive(uid); 49 const active = store.findActive(uid);
49 if (active) return { analysisId: active.id, status: active.status, reused: true, version: active.version }; 50 if (active) return { analysisId: active.id, status: active.status, reused: true, version: active.version };
50 // empreinte provisoire (sans hashes d'images) pour la réutilisation rapide ; l'empreinte finale inclut les hashes 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 export function getLatest(uid: string): AnalysisView | null { 184 export function getLatest(uid: string): AnalysisView | null {
185+ store.failOrphans();
184 const row = store.latestForListing(uid); 186 const row = store.latestForListing(uid);
185 return row ? viewAnalysis(row) : null; 187 return row ? viewAnalysis(row) : null;
186 } 188 }
187 export function getById(id: string): AnalysisView | null { 189 export function getById(id: string): AnalysisView | null {
190+ store.failOrphans();
188 const row = store.getAnalysis(id); 191 const row = store.getAnalysis(id);
189 return row ? viewAnalysis(row) : null; 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 export function setFailed(id: string, error: string): void { 74 export function setFailed(id: string, error: string): void {
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); 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 export function saveOutput(id: string, o: { outputJson: string; imageCount: number; usageJson: string; issuesJson: string; imagesJson: string }): void { 90 export function saveOutput(id: string, o: { outputJson: string; imageCount: number; usageJson: string; issuesJson: string; imagesJson: string }): void {
78 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); 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
Menu

Theme