feat: add RSS feeds management page with automation rules

This commit is contained in:
Manus Deploy
2026-04-25 22:51:35 +02:00
parent 8b9a1ace2f
commit ea40cdfd4f
18 changed files with 1184 additions and 912 deletions

View File

@@ -11,6 +11,12 @@ import {
InsertLocalUser,
ideas,
InsertIdea,
rssFeeds,
rssSettings,
type InsertRssFeed,
type InsertRssSettings,
type RssFeed,
type RssSettings,
} from "../drizzle/schema";
import { ENV } from "./_core/env";
@@ -360,3 +366,70 @@ export async function updateIdeaStatut(id: number, statut: "ouvert" | "en_cours"
if (!db) throw new Error("Database not available");
await db.update(ideas).set({ statut }).where(eq(ideas.id, id));
}
// ─── Flux RSS ────────────────────────────────────────────────────────────────────────────────────
export async function getRssFeeds(): Promise<RssFeed[]> {
const db = await getDb();
if (!db) return [];
return db.select().from(rssFeeds).orderBy(rssFeeds.name);
}
export async function getRssFeedById(id: number): Promise<RssFeed | null> {
const db = await getDb();
if (!db) return null;
const rows = await db.select().from(rssFeeds).where(eq(rssFeeds.id, id)).limit(1);
return rows[0] ?? null;
}
export async function createRssFeed(data: Omit<InsertRssFeed, "id" | "createdAt" | "updatedAt">): Promise<number> {
const db = await getDb();
if (!db) throw new Error("Database not available");
const result = await db.insert(rssFeeds).values(data);
return (result[0] as any).insertId as number;
}
export async function updateRssFeed(id: number, data: Partial<Omit<InsertRssFeed, "id" | "createdAt" | "updatedAt">>): Promise<void> {
const db = await getDb();
if (!db) throw new Error("Database not available");
await db.update(rssFeeds).set(data).where(eq(rssFeeds.id, id));
}
export async function deleteRssFeed(id: number): Promise<void> {
const db = await getDb();
if (!db) throw new Error("Database not available");
await db.delete(rssFeeds).where(eq(rssFeeds.id, id));
}
export async function getRssSettings(): Promise<RssSettings | null> {
const db = await getDb();
if (!db) return null;
const rows = await db.select().from(rssSettings).limit(1);
if (rows.length > 0) return rows[0];
// Créer les paramètres par défaut si inexistants
await db.insert(rssSettings).values({
fetchIntervalMinutes: 360,
scheduledTime: "06:00",
fetchMode: "scheduled",
autoFetchEnabled: true,
});
const newRows = await db.select().from(rssSettings).limit(1);
return newRows[0] ?? null;
}
export async function saveRssSettings(data: Partial<Omit<InsertRssSettings, "id" | "updatedAt">>): Promise<void> {
const db = await getDb();
if (!db) throw new Error("Database not available");
const existing = await db.select().from(rssSettings).limit(1);
if (existing.length > 0) {
await db.update(rssSettings).set(data).where(eq(rssSettings.id, existing[0].id));
} else {
await db.insert(rssSettings).values({
fetchIntervalMinutes: 360,
scheduledTime: "06:00",
fetchMode: "scheduled",
autoFetchEnabled: true,
...data,
});
}
}

View File

@@ -22,6 +22,13 @@ import {
getIdeasByUser,
repondreIdea,
updateIdeaStatut,
getRssFeeds,
getRssFeedById,
createRssFeed,
updateRssFeed,
deleteRssFeed,
getRssSettings,
saveRssSettings,
} from "./db";
import { importVeille, importAAP, runFullImport, getImportConfig } from "./importer";
import { loginLocalUser, hashPassword, ensureAdminExists } from "./localAuth";
@@ -50,9 +57,9 @@ export const appRouter = router({
}),
// Connexion locale
localLogin: publicProcedure
.input(z.object({ email: z.string().min(1), password: z.string().min(1) }))
.input(z.object({ identifier: z.string().min(1), password: z.string().min(1) }))
.mutation(async ({ input, ctx }) => {
const result = await loginLocalUser(input.email, input.password);
const result = await loginLocalUser(input.identifier, input.password);
// Stocker le token dans un cookie
const cookieOptions = getSessionCookieOptions(ctx.req);
ctx.res.cookie("veille_local_auth", result.token, {
@@ -307,6 +314,94 @@ export const appRouter = router({
return { success: true };
}),
}),
});
// ─── Flux RSS ───────────────────────────────────────────────────────────────────────────────────
rss: router({
// Lister tous les flux
list: protectedProcedure.query(async () => {
return getRssFeeds();
}),
// Créer un flux
create: adminProcedure
.input(z.object({
url: z.string().url("URL invalide"),
name: z.string().min(1, "Nom requis").max(255),
feedType: z.enum(["veille", "aap"]),
defaultTypeVeille: z.enum(["reglementaire", "concurrentielle", "technologique", "generale"]).optional(),
defaultCategorieAap: z.enum(["Handicap", "PA", "Enfance", "Précarité", "Sanitaire", "Autre"]).optional(),
autoRules: z.array(z.object({
keyword: z.string(),
value: z.string(),
})).optional(),
isActive: z.boolean().optional().default(true),
}))
.mutation(async ({ input }) => {
const id = await createRssFeed({
url: input.url,
name: input.name,
feedType: input.feedType,
defaultTypeVeille: input.defaultTypeVeille ?? null,
defaultCategorieAap: input.defaultCategorieAap ?? null,
autoRules: input.autoRules ?? null,
isActive: input.isActive ?? true,
});
return { id };
}),
// Modifier un flux
update: adminProcedure
.input(z.object({
id: z.number().int().positive(),
url: z.string().url("URL invalide").optional(),
name: z.string().min(1).max(255).optional(),
feedType: z.enum(["veille", "aap"]).optional(),
defaultTypeVeille: z.enum(["reglementaire", "concurrentielle", "technologique", "generale"]).nullable().optional(),
defaultCategorieAap: z.enum(["Handicap", "PA", "Enfance", "Précarité", "Sanitaire", "Autre"]).nullable().optional(),
autoRules: z.array(z.object({
keyword: z.string(),
value: z.string(),
})).nullable().optional(),
isActive: z.boolean().optional(),
}))
.mutation(async ({ input }) => {
const { id, ...data } = input;
await updateRssFeed(id, data);
return { success: true };
}),
// Supprimer un flux
delete: adminProcedure
.input(z.object({ id: z.number().int().positive() }))
.mutation(async ({ input }) => {
await deleteRssFeed(input.id);
return { success: true };
}),
// Activer / désactiver un flux
toggleActive: adminProcedure
.input(z.object({ id: z.number().int().positive(), isActive: z.boolean() }))
.mutation(async ({ input }) => {
await updateRssFeed(input.id, { isActive: input.isActive });
return { success: true };
}),
// Lire les paramètres globaux RSS
getSettings: protectedProcedure.query(async () => {
return getRssSettings();
}),
// Sauvegarder les paramètres globaux RSS
saveSettings: adminProcedure
.input(z.object({
fetchIntervalMinutes: z.number().int().min(5).max(10080).optional(),
scheduledTime: z.string().regex(/^\d{2}:\d{2}$/, "Format HH:MM requis").optional(),
fetchMode: z.enum(["interval", "scheduled"]).optional(),
autoFetchEnabled: z.boolean().optional(),
}))
.mutation(async ({ input }) => {
await saveRssSettings(input);
return { success: true };
}),
}),
});
export type AppRouter = typeof appRouter;