Compare commits

...

2 Commits

Author SHA1 Message Date
Manus
d283675230 Checkpoint: Purge automatique des tombstones (processed_dedup_keys) de plus de 6 mois :
- purgeOldArticles dans db.ts : suppression des entrées processedDedupKeys.processedAt <= cutoff 6 mois
- Logs cron et init mis à jour pour afficher le nombre de tombstones supprimés
- 0 erreur TypeScript
2026-07-10 21:14:55 -04:00
Manus
8782883bea Checkpoint: Persistance "marquer comme lu" entre sessions :
- Ajout de veille.getReadIds et aap.getReadIds dans routers.ts (procédures protectedProcedure)
- VeilleDashboard.tsx : chargement des IDs lus depuis la DB au montage (useEffect sur readIdsQuery), invalidation getReadIds dans markAsRead et markAllAsRead
- AAPDashboard.tsx : même pattern
- 0 erreur TypeScript
2026-07-10 21:08:26 -04:00
6 changed files with 53 additions and 11 deletions

View File

@@ -1,4 +1,4 @@
{ {
"version": "f8f8affc", "version": "509dd73b",
"timestamp": 1783434543359 "timestamp": 1783732495856
} }

View File

@@ -1,4 +1,4 @@
import { useState, useMemo } from "react"; import { useState, useMemo, useEffect } from "react";
import { useLocalAuth } from "@/contexts/LocalAuthContext"; import { useLocalAuth } from "@/contexts/LocalAuthContext";
import { toast } from "sonner"; import { toast } from "sonner";
import { trpc } from "@/lib/trpc"; import { trpc } from "@/lib/trpc";
@@ -117,13 +117,23 @@ export default function AAPDashboard() {
// Marquage lu/non lu // Marquage lu/non lu
const [readIds, setReadIds] = useState<Set<number>>(new Set()); const [readIds, setReadIds] = useState<Set<number>>(new Set());
const readIdsQuery = trpc.aap.getReadIds.useQuery(undefined, { enabled: !!user });
useEffect(() => {
if (readIdsQuery.data?.ids) {
setReadIds(new Set(readIdsQuery.data.ids));
}
}, [readIdsQuery.data]);
const markAsReadMutation = trpc.aap.markAsRead.useMutation({ const markAsReadMutation = trpc.aap.markAsRead.useMutation({
onSuccess: (_, vars) => { onSuccess: (_, vars) => {
setReadIds((prev) => { const next = new Set(prev); next.add(vars.articleId); return next; }); setReadIds((prev) => { const next = new Set(prev); next.add(vars.articleId); return next; });
utils.aap.getReadIds.invalidate();
}, },
}); });
const markAllAsReadMutation = trpc.aap.markAllAsRead.useMutation({ const markAllAsReadMutation = trpc.aap.markAllAsRead.useMutation({
onSuccess: () => { utils.aap.unreadCount.invalidate(); }, onSuccess: () => {
utils.aap.unreadCount.invalidate();
utils.aap.getReadIds.invalidate();
},
}); });
const unreadCountQuery = trpc.aap.unreadCount.useQuery(); const unreadCountQuery = trpc.aap.unreadCount.useQuery();
const unreadCount = unreadCountQuery.data?.count ?? 0; const unreadCount = unreadCountQuery.data?.count ?? 0;

View File

@@ -1,4 +1,4 @@
import { useState, useMemo } from "react"; import { useState, useMemo, useEffect } from "react";
import { useLocalAuth } from "@/contexts/LocalAuthContext"; import { useLocalAuth } from "@/contexts/LocalAuthContext";
import { toast } from "sonner"; import { toast } from "sonner";
import { trpc } from "@/lib/trpc"; import { trpc } from "@/lib/trpc";
@@ -234,14 +234,22 @@ export default function VeilleDashboard() {
// Marquage lu/non lu // Marquage lu/non lu
const [readIds, setReadIds] = useState<Set<number>>(new Set()); const [readIds, setReadIds] = useState<Set<number>>(new Set());
const readIdsQuery = trpc.veille.getReadIds.useQuery(undefined, { enabled: !!user });
useEffect(() => {
if (readIdsQuery.data?.ids) {
setReadIds(new Set(readIdsQuery.data.ids));
}
}, [readIdsQuery.data]);
const markAsReadMutation = trpc.veille.markAsRead.useMutation({ const markAsReadMutation = trpc.veille.markAsRead.useMutation({
onSuccess: (_, vars) => { onSuccess: (_, vars) => {
setReadIds((prev) => { const next = new Set(prev); next.add(vars.articleId); return next; }); setReadIds((prev) => { const next = new Set(prev); next.add(vars.articleId); return next; });
utils.veille.getReadIds.invalidate();
}, },
}); });
const markAllAsReadMutation = trpc.veille.markAllAsRead.useMutation({ const markAllAsReadMutation = trpc.veille.markAllAsRead.useMutation({
onSuccess: () => { onSuccess: () => {
utils.veille.unreadCount.invalidate(); utils.veille.unreadCount.invalidate();
utils.veille.getReadIds.invalidate();
}, },
}); });
const unreadCountQuery = trpc.veille.unreadCount.useQuery(); const unreadCountQuery = trpc.veille.unreadCount.useQuery();

View File

@@ -98,8 +98,8 @@ export async function scheduleDailyImport() {
const retentionMonths = retentionStr ? parseInt(retentionStr, 10) : 0; const retentionMonths = retentionStr ? parseInt(retentionStr, 10) : 0;
if (retentionMonths > 0) { if (retentionMonths > 0) {
const purged = await purgeOldArticles(retentionMonths); const purged = await purgeOldArticles(retentionMonths);
if (purged.veille > 0 || purged.aap > 0) { if (purged.veille > 0 || purged.aap > 0 || purged.tombstones > 0) {
console.log(`[Cron] Purge rétention (${retentionMonths} mois) — Veille: -${purged.veille} | AAP: -${purged.aap}`); console.log(`[Cron] Purge rétention (${retentionMonths} mois) — Veille: -${purged.veille} | AAP: -${purged.aap} | Tombstones: -${purged.tombstones}`);
} }
} }
} catch (e) { } catch (e) {
@@ -236,8 +236,8 @@ async function startServer() {
const retentionMonths = retentionStr ? parseInt(retentionStr, 10) : 0; const retentionMonths = retentionStr ? parseInt(retentionStr, 10) : 0;
if (retentionMonths > 0) { if (retentionMonths > 0) {
const purged = await purgeOldArticles(retentionMonths); const purged = await purgeOldArticles(retentionMonths);
if (purged.veille > 0 || purged.aap > 0) { if (purged.veille > 0 || purged.aap > 0 || purged.tombstones > 0) {
console.log(`[Init] Purge rétention (${retentionMonths} mois) — Veille: -${purged.veille} | AAP: -${purged.aap}`); console.log(`[Init] Purge rétention (${retentionMonths} mois) — Veille: -${purged.veille} | AAP: -${purged.aap} | Tombstones: -${purged.tombstones}`);
} }
} }
} catch (e) { } catch (e) {

View File

@@ -14,6 +14,7 @@ import {
InsertIdea, InsertIdea,
rssFeeds, rssFeeds,
rssSettings, rssSettings,
processedDedupKeys,
type InsertRssFeed, type InsertRssFeed,
type InsertRssSettings, type InsertRssSettings,
type ImportLog, type ImportLog,
@@ -489,16 +490,21 @@ export async function saveRssSettings(data: Partial<Omit<InsertRssSettings, "id"
} }
// ─── Purge ─────────────────────────────────────────────────────────────────── // ─── Purge ───────────────────────────────────────────────────────────────────
export async function purgeOldArticles(retentionMonths: number): Promise<{ veille: number; aap: number }> { export async function purgeOldArticles(retentionMonths: number): Promise<{ veille: number; aap: number; tombstones: number }> {
const db = await getDb(); const db = await getDb();
if (!db) throw new Error("Database not available"); if (!db) throw new Error("Database not available");
const cutoff = new Date(); const cutoff = new Date();
cutoff.setMonth(cutoff.getMonth() - retentionMonths); cutoff.setMonth(cutoff.getMonth() - retentionMonths);
const veilleResult = await db.delete(veilleItems).where(lte(veilleItems.importedAt, cutoff)); const veilleResult = await db.delete(veilleItems).where(lte(veilleItems.importedAt, cutoff));
const aapResult = await db.delete(aapItems).where(lte(aapItems.importedAt, cutoff)); const aapResult = await db.delete(aapItems).where(lte(aapItems.importedAt, cutoff));
// Purge des tombstones (processed_dedup_keys) de plus de 6 mois
const tombstoneCutoff = new Date();
tombstoneCutoff.setMonth(tombstoneCutoff.getMonth() - 6);
const tombstoneResult = await db.delete(processedDedupKeys).where(lte(processedDedupKeys.processedAt, tombstoneCutoff));
return { return {
veille: (veilleResult as any).affectedRows ?? 0, veille: (veilleResult as any).affectedRows ?? 0,
aap: (aapResult as any).affectedRows ?? 0, aap: (aapResult as any).affectedRows ?? 0,
tombstones: (tombstoneResult as any).affectedRows ?? 0,
}; };
} }

View File

@@ -226,6 +226,15 @@ export const appRouter = router({
const read = readRows[0]?.cnt ?? 0; const read = readRows[0]?.cnt ?? 0;
return { count: Math.max(0, total - read) }; return { count: Math.max(0, total - read) };
}), }),
getReadIds: protectedProcedure.query(async ({ ctx }) => {
const db = await getDb();
if (!db) return { ids: [] as number[] };
const rows = await db
.select({ articleId: articleReads.articleId })
.from(articleReads)
.where(and(eqDrizzle(articleReads.userId, ctx.user.id), eqDrizzle(articleReads.articleType, "veille")));
return { ids: rows.map((r: { articleId: number }) => r.articleId) };
}),
}), }),
// ─── AAPP ──────────────────────────────────────────────────────────────────── // ─── AAPP ────────────────────────────────────────────────────────────────────
aap: router({ aap: router({
@@ -353,8 +362,17 @@ export const appRouter = router({
const read = readRows[0]?.cnt ?? 0; const read = readRows[0]?.cnt ?? 0;
return { count: Math.max(0, total - read) }; return { count: Math.max(0, total - read) };
}), }),
getReadIds: protectedProcedure.query(async ({ ctx }) => {
const db = await getDb();
if (!db) return { ids: [] as number[] };
const rows = await db
.select({ articleId: articleReads.articleId })
.from(articleReads)
.where(and(eqDrizzle(articleReads.userId, ctx.user.id), eqDrizzle(articleReads.articleType, "aap")));
return { ids: rows.map((r: { articleId: number }) => r.articleId) };
}), }),
// ─── Importt ───────────────────────────────────────────────────────────────── }),
// ─── Import ─────────────────────────────────────────────────────────────────
import: router({ import: router({
run: adminProcedure run: adminProcedure
.input(z.object({ type: z.enum(["veille", "aap", "all"]).default("all") })) .input(z.object({ type: z.enum(["veille", "aap", "all"]).default("all") }))