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:
@ -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) =>
|
||||
|
||||
@ -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' },
|
||||
]
|
||||
|
||||
|
||||
@ -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 /> },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
145
frontend/src/pages/admin/Schedule.tsx
Normal file
145
frontend/src/pages/admin/Schedule.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@ -15,8 +15,7 @@
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"baseUrl": ".",
|
||||
"paths": { "@/*": ["src/*"] }
|
||||
"paths": { "@/*": ["./src/*"] }
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user