Checkpoint: Implémentation du résumé automatique généré par l'IA pour chaque article RSS (veille et AAP), marquage lu/non lu avec point bleu et fond teinté, bouton "Tout marquer comme lu", compteurs non lus dans la sidebar et dans les titres de page. Migration BDD : table article_reads + colonne iaResume dans veille_items et aap_items.
This commit is contained in:
@@ -37,8 +37,8 @@ import { scheduleRssFetch } from "./_core/index";
|
||||
import { loginLocalUser, hashPassword, ensureAdminExists } from "./localAuth";
|
||||
import { classifyArticle } from "./aiClassifier";
|
||||
import { getDb } from "./db";
|
||||
import { veilleItems, aapItems } from "../drizzle/schema";
|
||||
import { isNull, or, eq as eqDrizzle } from "drizzle-orm";
|
||||
import { veilleItems, aapItems, articleReads } from "../drizzle/schema";
|
||||
import { isNull, or, eq as eqDrizzle, and, inArray, count } from "drizzle-orm";
|
||||
|
||||
// ─── Middleware admin ─────────────────────────────────────────────────────────
|
||||
|
||||
@@ -152,6 +152,57 @@ export const appRouter = router({
|
||||
|
||||
return { processed, errors, total: rows.length };
|
||||
}),
|
||||
|
||||
// ─── Marquage lu/non lu ──────────────────────────────────────────────────────────────────────
|
||||
markAsRead: protectedProcedure
|
||||
.input(z.object({ articleId: z.number().int().positive() }))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const db = await getDb();
|
||||
if (!db) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "DB indisponible" });
|
||||
// Insérer seulement si pas déjà lu (ignore le doublon)
|
||||
try {
|
||||
await db.insert(articleReads).values({
|
||||
userId: ctx.user.id,
|
||||
articleType: "veille",
|
||||
articleId: input.articleId,
|
||||
});
|
||||
} catch { /* doublon = déjà lu, on ignore */ }
|
||||
return { success: true };
|
||||
}),
|
||||
|
||||
markAllAsRead: protectedProcedure.mutation(async ({ ctx }) => {
|
||||
const db = await getDb();
|
||||
if (!db) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "DB indisponible" });
|
||||
// Récupérer tous les IDs veille
|
||||
const allItems = await db.select({ id: veilleItems.id }).from(veilleItems);
|
||||
const allIds = allItems.map((r: { id: number }) => r.id);
|
||||
// Trouver ceux déjà lus
|
||||
const alreadyRead = await db
|
||||
.select({ articleId: articleReads.articleId })
|
||||
.from(articleReads)
|
||||
.where(and(eqDrizzle(articleReads.userId, ctx.user.id), eqDrizzle(articleReads.articleType, "veille")));
|
||||
const alreadyReadIds = new Set(alreadyRead.map((r: { articleId: number }) => r.articleId));
|
||||
const toInsert = allIds.filter((id: number) => !alreadyReadIds.has(id)).map((id: number) => ({
|
||||
userId: ctx.user.id, articleType: "veille" as const, articleId: id,
|
||||
}));
|
||||
if (toInsert.length > 0) {
|
||||
await db.insert(articleReads).values(toInsert);
|
||||
}
|
||||
return { success: true, marked: toInsert.length };
|
||||
}),
|
||||
|
||||
unreadCount: protectedProcedure.query(async ({ ctx }) => {
|
||||
const db = await getDb();
|
||||
if (!db) return { count: 0 };
|
||||
const totalRows = await db.select({ cnt: count() }).from(veilleItems);
|
||||
const total = totalRows[0]?.cnt ?? 0;
|
||||
const readRows = await db
|
||||
.select({ cnt: count() })
|
||||
.from(articleReads)
|
||||
.where(and(eqDrizzle(articleReads.userId, ctx.user.id), eqDrizzle(articleReads.articleType, "veille")));
|
||||
const read = readRows[0]?.cnt ?? 0;
|
||||
return { count: Math.max(0, total - read) };
|
||||
}),
|
||||
}),
|
||||
// ─── AAPP ────────────────────────────────────────────────────────────────────
|
||||
aap: router({
|
||||
@@ -222,6 +273,54 @@ export const appRouter = router({
|
||||
|
||||
return { processed, errors, total: rows.length };
|
||||
}),
|
||||
|
||||
// ─── Marquage lu/non lu AAP ──────────────────────────────────────────────────────────────────────
|
||||
markAsRead: protectedProcedure
|
||||
.input(z.object({ articleId: z.number().int().positive() }))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const db = await getDb();
|
||||
if (!db) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "DB indisponible" });
|
||||
try {
|
||||
await db.insert(articleReads).values({
|
||||
userId: ctx.user.id,
|
||||
articleType: "aap",
|
||||
articleId: input.articleId,
|
||||
});
|
||||
} catch { /* doublon = déjà lu, on ignore */ }
|
||||
return { success: true };
|
||||
}),
|
||||
|
||||
markAllAsRead: protectedProcedure.mutation(async ({ ctx }) => {
|
||||
const db = await getDb();
|
||||
if (!db) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "DB indisponible" });
|
||||
const allItems = await db.select({ id: aapItems.id }).from(aapItems);
|
||||
const allIds = allItems.map((r: { id: number }) => r.id);
|
||||
const alreadyRead = await db
|
||||
.select({ articleId: articleReads.articleId })
|
||||
.from(articleReads)
|
||||
.where(and(eqDrizzle(articleReads.userId, ctx.user.id), eqDrizzle(articleReads.articleType, "aap")));
|
||||
const alreadyReadIds = new Set(alreadyRead.map((r: { articleId: number }) => r.articleId));
|
||||
const toInsert = allIds.filter((id: number) => !alreadyReadIds.has(id)).map((id: number) => ({
|
||||
userId: ctx.user.id, articleType: "aap" as const, articleId: id,
|
||||
}));
|
||||
if (toInsert.length > 0) {
|
||||
await db.insert(articleReads).values(toInsert);
|
||||
}
|
||||
return { success: true, marked: toInsert.length };
|
||||
}),
|
||||
|
||||
unreadCount: protectedProcedure.query(async ({ ctx }) => {
|
||||
const db = await getDb();
|
||||
if (!db) return { count: 0 };
|
||||
const totalRows = await db.select({ cnt: count() }).from(aapItems);
|
||||
const total = totalRows[0]?.cnt ?? 0;
|
||||
const readRows = await db
|
||||
.select({ cnt: count() })
|
||||
.from(articleReads)
|
||||
.where(and(eqDrizzle(articleReads.userId, ctx.user.id), eqDrizzle(articleReads.articleType, "aap")));
|
||||
const read = readRows[0]?.cnt ?? 0;
|
||||
return { count: Math.max(0, total - read) };
|
||||
}),
|
||||
}),
|
||||
// ─── Importt ─────────────────────────────────────────────────────────────────
|
||||
import: router({
|
||||
|
||||
Reference in New Issue
Block a user