diff --git a/client/public/__manus__/version.json b/client/public/__manus__/version.json index 327c46f..cf9b662 100644 --- a/client/public/__manus__/version.json +++ b/client/public/__manus__/version.json @@ -1,4 +1,4 @@ { - "version": "2bfe4358", - "timestamp": 1783252262810 + "version": "aa6b358c", + "timestamp": 1783341062192 } \ No newline at end of file diff --git a/server/routers.ts b/server/routers.ts index 1d79d7f..ef5b861 100644 --- a/server/routers.ts +++ b/server/routers.ts @@ -37,7 +37,7 @@ import { scheduleDailyImport } from "./_core/index"; import { loginLocalUser, hashPassword, ensureAdminExists } from "./localAuth"; import { classifyArticle } from "./aiClassifier"; import { getDb } from "./db"; -import { veilleItems, aapItems, articleReads } from "../drizzle/schema"; +import { veilleItems, aapItems, articleReads, processedDedupKeys } from "../drizzle/schema"; import { isNull, or, eq as eqDrizzle, and, inArray, count } from "drizzle-orm"; // ─── Middleware admin ───────────────────────────────────────────────────────── @@ -116,14 +116,15 @@ export const appRouter = router({ const db = await getDb(); if (!db) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "DB indisponible" }); - // Récupérer les articles non encore classés par l'IA + // Récupérer les articles non encore classés par l'IA OU classés par le fallback rules const rows = await db .select() .from(veilleItems) - .where(or(isNull(veilleItems.iaClassifiedBy), isNull(veilleItems.iaRelevant))) + .where(or(isNull(veilleItems.iaClassifiedBy), isNull(veilleItems.iaRelevant), eqDrizzle(veilleItems.iaClassifiedBy, "rules"))) .limit(input.limit); let processed = 0; + let deleted = 0; let errors = 0; for (const row of rows) { @@ -133,22 +134,34 @@ export const appRouter = router({ row.resume || "", () => ({ typeVeille: (row.typeVeille || "informationnelle") as "reglementaire" | "concurrentielle" | "technologique" | "informationnelle" }) ); - await db - .update(veilleItems) - .set({ - typeVeille: (aiResult.typeVeille ?? row.typeVeille ?? "informationnelle") as any, - iaRelevant: aiResult.relevant, - iaClassifiedBy: aiResult.classifiedBy, - iaReason: aiResult.reason, - }) - .where(eqDrizzle(veilleItems.id, row.id)); - processed++; + if (!aiResult.relevant) { + // Supprimer l'article non pertinent + await db.delete(veilleItems).where(eqDrizzle(veilleItems.id, row.id)); + // Conserver le tombstone pour éviter la réinsertion + if (row.dedupKey) { + await db.insert(processedDedupKeys) + .values({ dedupKey: row.dedupKey, feedType: "veille" }) + .onDuplicateKeyUpdate({ set: { dedupKey: row.dedupKey } }); + } + deleted++; + } else { + await db + .update(veilleItems) + .set({ + typeVeille: (aiResult.typeVeille ?? row.typeVeille ?? "informationnelle") as any, + iaRelevant: aiResult.relevant, + iaClassifiedBy: aiResult.classifiedBy, + iaReason: aiResult.reason, + }) + .where(eqDrizzle(veilleItems.id, row.id)); + processed++; + } } catch { errors++; } } - return { processed, errors, total: rows.length }; + return { processed, deleted, errors, total: rows.length }; }), // ─── Marquage lu/non lu ────────────────────────────────────────────────────────────────────── @@ -240,34 +253,45 @@ export const appRouter = router({ const rows = await db .select() .from(aapItems) - .where(or(isNull(aapItems.iaClassifiedBy), isNull(aapItems.iaRelevant))) + .where(or(isNull(aapItems.iaClassifiedBy), isNull(aapItems.iaRelevant), eqDrizzle(aapItems.iaClassifiedBy, "rules"))) .limit(input.limit); let processed = 0; + let deleted = 0; let errors = 0; for (const row of rows) { try { const aiResult = await classifyArticle( row.titre || "", - "", + row.resume || "", () => ({ typeVeille: "informationnelle" as const }) ); - await db - .update(aapItems) - .set({ - iaRelevant: aiResult.relevant, - iaClassifiedBy: aiResult.classifiedBy, - iaReason: aiResult.reason, - }) - .where(eqDrizzle(aapItems.id, row.id)); - processed++; + if (!aiResult.relevant) { + await db.delete(aapItems).where(eqDrizzle(aapItems.id, row.id)); + if (row.dedupKey) { + await db.insert(processedDedupKeys) + .values({ dedupKey: row.dedupKey, feedType: "aap" }) + .onDuplicateKeyUpdate({ set: { dedupKey: row.dedupKey } }); + } + deleted++; + } else { + await db + .update(aapItems) + .set({ + iaRelevant: aiResult.relevant, + iaClassifiedBy: aiResult.classifiedBy, + iaReason: aiResult.reason, + }) + .where(eqDrizzle(aapItems.id, row.id)); + processed++; + } } catch { errors++; } } - return { processed, errors, total: rows.length }; + return { processed, deleted, errors, total: rows.length }; }), // ─── Marquage lu/non lu AAP ────────────────────────────────────────────────────────────────────── diff --git a/todo.md b/todo.md index 277077b..a96c56a 100644 --- a/todo.md +++ b/todo.md @@ -187,5 +187,5 @@ - [x] Afficher la prochaine date d'exécution dynamique dans les Paramètres (selon mode/heure/jour choisi) - [x] Ajouter la règle de rétention configurable dans les Paramètres (supprimer articles > N mois) - [x] Implémenter la purge automatique au démarrage du serveur selon la règle de rétention -- [ ] Déployer en recette -- [ ] Répliquer BDD recette vers production et déployer le code en production +- [x] Déployer en recette +- [x] Répliquer BDD recette vers production et déployer le code en production