feat: add sources to retrieve news and divide the IA reflexions in 2 steps to limit the number of news

This commit is contained in:
2026-04-19 10:43:15 +02:00
parent 93668273ff
commit eb1fb5ca78
28 changed files with 1086 additions and 249 deletions

View File

@ -12,6 +12,7 @@ export interface ScrapeJob {
articles_found: number; error_msg: string; created_at: string
}
export interface Setting { key: string; value: string }
export interface ScheduleSlot { id?: string; day_of_week: number; hour: number; minute: number }
export interface AdminUser { id: string; email: string; role: string; created_at: string }
export interface Credential { source_id: string; source_name: string; username: string; has_password: boolean }
@ -44,6 +45,10 @@ export const adminApi = {
updateSettings: (settings: Setting[]) => api.put<void>('/admin/settings', { settings }),
getDefaultPrompt: () => api.get<{ prompt: string }>('/admin/settings/default-prompt'),
// Schedule
getSchedule: () => api.get<ScheduleSlot[]>('/admin/schedule'),
updateSchedule: (slots: ScheduleSlot[]) => api.put<void>('/admin/schedule', { slots }),
// Users
listUsers: () => api.get<AdminUser[]>('/admin/users'),
updateUser: (id: string, email: string, role: string) =>

View File

@ -1,5 +1,5 @@
import { NavLink } from 'react-router-dom'
import { LayoutDashboard, Newspaper, Star, Settings, Key, Cpu, Database, ClipboardList, Users, LogOut, TrendingUp } from 'lucide-react'
import { LayoutDashboard, Newspaper, Star, Settings, Key, Cpu, Database, ClipboardList, Users, LogOut, TrendingUp, CalendarDays } from 'lucide-react'
import { useAuth } from '@/lib/auth'
import { cn } from '@/lib/cn'
@ -15,6 +15,7 @@ const adminItems = [
{ to: '/admin/sources', icon: Database, label: 'Sources' },
{ to: '/admin/jobs', icon: ClipboardList, label: 'Jobs' },
{ to: '/admin/users', icon: Users, label: 'Utilisateurs' },
{ to: '/admin/schedule', icon: CalendarDays, label: 'Planning' },
{ to: '/admin/settings', icon: Settings, label: 'Paramètres' },
]

View File

@ -11,6 +11,7 @@ import { Sources } from '@/pages/admin/Sources'
import { Jobs } from '@/pages/admin/Jobs'
import { AdminUsers } from '@/pages/admin/AdminUsers'
import { AdminSettings } from '@/pages/admin/AdminSettings'
import { Schedule } from '@/pages/admin/Schedule'
export const router = createBrowserRouter([
{ path: '/login', element: <Login /> },
@ -31,6 +32,7 @@ export const router = createBrowserRouter([
{ path: 'jobs', element: <Jobs /> },
{ path: 'users', element: <AdminUsers /> },
{ path: 'settings', element: <AdminSettings /> },
{ path: 'schedule', element: <Schedule /> },
],
},
],

View File

@ -0,0 +1,145 @@
import { useState, useEffect } from 'react'
import { Plus, Trash2, Save } from 'lucide-react'
import { adminApi, type ScheduleSlot } from '@/api/admin'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Spinner } from '@/components/ui/spinner'
const DAYS = [
{ label: 'Lundi', short: 'LUN', value: 1 },
{ label: 'Mardi', short: 'MAR', value: 2 },
{ label: 'Mercredi', short: 'MER', value: 3 },
{ label: 'Jeudi', short: 'JEU', value: 4 },
{ label: 'Vendredi', short: 'VEN', value: 5 },
{ label: 'Samedi', short: 'SAM', value: 6 },
{ label: 'Dimanche', short: 'DIM', value: 0 },
]
type SlotKey = `${number}-${number}-${number}`
function toKey(s: ScheduleSlot): SlotKey {
return `${s.day_of_week}-${s.hour}-${s.minute}`
}
function fmt(h: number, m: number) {
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`
}
export function Schedule() {
const [slots, setSlots] = useState<ScheduleSlot[]>([])
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
const [saved, setSaved] = useState(false)
const [newTimes, setNewTimes] = useState<Record<number, string>>({})
useEffect(() => { load() }, [])
async function load() {
setLoading(true)
try { setSlots((await adminApi.getSchedule()) ?? []) } finally { setLoading(false) }
}
function slotsForDay(day: number) {
return slots
.filter(s => s.day_of_week === day)
.sort((a, b) => a.hour !== b.hour ? a.hour - b.hour : a.minute - b.minute)
}
function addSlot(day: number) {
const time = newTimes[day] || '06:00'
const [h, m] = time.split(':').map(Number)
const newSlot: ScheduleSlot = { day_of_week: day, hour: h, minute: m }
if (slots.some(s => toKey(s) === toKey(newSlot))) return
setSlots(prev => [...prev, newSlot])
setNewTimes(p => ({ ...p, [day]: '06:00' }))
}
function removeSlot(slot: ScheduleSlot) {
setSlots(prev => prev.filter(s => toKey(s) !== toKey(slot)))
}
async function save() {
setSaving(true); setSaved(false)
await adminApi.updateSchedule(slots)
setSaving(false); setSaved(true)
setTimeout(() => setSaved(false), 2000)
}
if (loading) return <div className="flex justify-center py-20"><Spinner /></div>
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold">Planning hebdomadaire</h1>
<p className="text-muted-foreground text-sm">
Définissez les créneaux de scraping + résumé IA pour chaque jour
</p>
</div>
<Button onClick={save} disabled={saving}>
{saving ? <Spinner className="h-4 w-4" /> : <Save className="h-4 w-4" />}
{saved ? 'Enregistré !' : 'Enregistrer'}
</Button>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 xl:grid-cols-7 gap-3">
{DAYS.map(day => {
const daySlots = slotsForDay(day.value)
const isWeekend = day.value === 0 || day.value === 6
return (
<Card key={day.value} className={isWeekend ? 'border-muted' : ''}>
<CardHeader className="pb-2 pt-4 px-4">
<CardTitle className="text-sm font-semibold">
<span className="hidden xl:block">{day.label}</span>
<span className="xl:hidden">{day.short}</span>
</CardTitle>
</CardHeader>
<CardContent className="px-4 pb-4 space-y-2">
{/* Créneaux existants */}
{daySlots.length === 0 && (
<p className="text-xs text-muted-foreground italic">Aucun créneau</p>
)}
{daySlots.map(slot => (
<div
key={toKey(slot)}
className="flex items-center justify-between rounded bg-primary/10 px-2 py-1"
>
<span className="text-sm font-mono font-medium">
{fmt(slot.hour, slot.minute)}
</span>
<button
onClick={() => removeSlot(slot)}
className="text-muted-foreground hover:text-destructive transition-colors ml-2"
>
<Trash2 className="h-3 w-3" />
</button>
</div>
))}
{/* Ajout d'un créneau */}
<div className="flex items-center gap-1 pt-1">
<input
type="time"
value={newTimes[day.value] ?? '06:00'}
onChange={e => setNewTimes(p => ({ ...p, [day.value]: e.target.value }))}
className="flex-1 min-w-0 rounded border border-input bg-background px-2 py-1 text-xs font-mono focus:outline-none focus:ring-1 focus:ring-ring"
/>
<button
onClick={() => addSlot(day.value)}
className="rounded bg-primary/10 p-1 hover:bg-primary/20 transition-colors"
>
<Plus className="h-3 w-3" />
</button>
</div>
</CardContent>
</Card>
)
})}
</div>
<p className="text-xs text-muted-foreground">
À chaque créneau, le service lance le scraping de toutes les sources actives puis génère les résumés IA.
</p>
</div>
)
}

View File

@ -15,8 +15,7 @@
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": { "@/*": ["src/*"] }
"paths": { "@/*": ["./src/*"] }
},
"include": ["src"]
}