Fix 10 bugs found in general code review

Backend (app.py / database.py / ai_agent.py):
- [Critique] Autoclose loop: add pg_try_advisory_lock so only one Gunicorn
  worker runs the check per 60s cycle; add random startup jitter
- [Critique] admin_delete_idea: pass consultation_id to
  _update_synthesis_background so the right synthesis is regenerated
- [Majeur] admin_login: return HMAC-signed session token instead of raw
  ADMIN_SECRET; require_admin verifies the signature (TTL 8h)
- [Majeur] bulk_delete: replace str.isdigit() (Unicode-unsafe) with
  try/except int() to prevent crash on Unicode digit characters
- [Majeur] create_consultation: force UTC timezone on naive datetime from
  fromisoformat() to prevent TypeError when comparing with UTC-aware now()
- [Majeur] ai_agent.py: fix 'raw' in dir() -> 'raw' in locals() so the
  JSON parse error log actually shows the raw response
- [Mineur] export print: use datetime.now(UTC) instead of datetime.now()

Frontend (React):
- [Majeur] consultation.tsx: show startsAt (not endsAt) for upcoming
  consultations; add startsAt variable
- [Majeur] consultations-list.tsx: same fix for the list view
- [Mineur] home.tsx: guard new Date(idea.createdAt) against null
- [Mineur] admin.tsx: check HTTP status in exportCsv XHR before creating
  download link; show error toast on non-200

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 16:26:52 +02:00
parent 18bb0efc30
commit 9553115d8c
7 changed files with 92 additions and 19 deletions
@@ -658,6 +658,10 @@ export default function Admin() {
req.setRequestHeader("Authorization", `Bearer ${token}`);
req.responseType = "blob";
req.onload = () => {
if (req.status !== 200) {
toast({ title: "Erreur export", description: `Erreur ${req.status} lors de l'export CSV.`, variant: "destructive" });
return;
}
const a = document.createElement("a");
a.href = URL.createObjectURL(req.response);
a.setAttribute("download", "contributions.csv");
@@ -235,6 +235,7 @@ export default function ConsultationPage() {
const isOpen = consultation.isOpen;
const isClosed = !!consultation.closedAt;
const endsAt = consultation.endsAt ? new Date(consultation.endsAt) : null;
const startsAt = consultation.startsAt ? new Date(consultation.startsAt) : null;
const closedAt = consultation.closedAt ? new Date(consultation.closedAt) : null;
return (
@@ -299,8 +300,8 @@ export default function ConsultationPage() {
<span className="flex items-center gap-1.5">
<Clock className="h-3 w-3" />
{isOpen
? `Fermeture ${formatDistanceToNow(endsAt, { locale: fr, addSuffix: true })}`
: `Ouverture ${formatDistanceToNow(endsAt, { locale: fr, addSuffix: true })}`}
? `Fermeture ${formatDistanceToNow(endsAt!, { locale: fr, addSuffix: true })}`
: `Ouverture ${formatDistanceToNow(startsAt ?? endsAt!, { locale: fr, addSuffix: true })}`}
{" · "}
{format(endsAt, "d MMMM yyyy à HH:mm", { locale: fr })} UTC
</span>
@@ -115,7 +115,7 @@ export default function ConsultationsList() {
<Clock className="h-3 w-3" />
{c.isOpen
? `Ferme ${formatDistanceToNow(new Date(c.endsAt), { locale: fr, addSuffix: true })}`
: `Ouvre ${formatDistanceToNow(new Date(c.endsAt), { locale: fr, addSuffix: true })}`}
: `Ouvre ${formatDistanceToNow(new Date(c.startsAt ?? c.endsAt), { locale: fr, addSuffix: true })}`}
</span>
)}
{c.closedAt && (
+1 -1
View File
@@ -484,7 +484,7 @@ export default function Home() {
</span>
<span>&bull;</span>
<span>
{format(new Date(idea.createdAt), "d MMM, HH:mm", { locale: fr })}
{idea.createdAt ? format(new Date(idea.createdAt), "d MMM, HH:mm", { locale: fr }) : "—"}
</span>
<button
onClick={() => handleFlag(idea.id)}