mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-14 02:38:07 +02:00
Historique : badge « Nouvel essai » et abandon des publications trop en retard
- Une publication en échec avec une nouvelle tentative programmée affiche « Nouvel essai » et compte dans « En cours », plus dans « Échecs ». - Le planificateur n'envoie plus une publication qui a plus de 24 h de retard sur son heure prévue : elle est marquée abandonnée (dernière erreur conservée), à reprogrammer si besoin. - L'historique ne se rafraîchit plus toutes les 3 s à cause d'un échec en attente de sa prochaine tentative. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018jcWbATGHxatn4V7qS3mr3
This commit is contained in:
3 files changed
+71
-6
No files matched your search
@@ -1,6 +1,6 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { ArrowRight, Clock, CheckCircle2, XCircle, Eye, Image as ImageIcon, Smartphone, Clapperboard } from "lucide-react";
|
||||
import { ArrowRight, Clock, CheckCircle2, XCircle, RotateCw, Eye, Image as ImageIcon, Smartphone, Clapperboard } from "lucide-react";
|
||||
import { Link } from "wouter";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { EmptyState } from "@/components/empty-state";
|
||||
@@ -197,7 +197,11 @@ export default function RecentPublications() {
|
||||
<p className="truncate text-sm font-medium" data-testid={`text-page-name-${scheduledPost.id}`}>
|
||||
{scheduledPost.page?.pageName || "Page inconnue"}
|
||||
</p>
|
||||
{scheduledPost.error ? (
|
||||
{scheduledPost.error && !scheduledPost.publishedAt && scheduledPost.nextAttemptAt ? (
|
||||
<Badge variant="warning" className="gap-1">
|
||||
<RotateCw className="h-3 w-3" /> Nouvel essai
|
||||
</Badge>
|
||||
) : scheduledPost.error ? (
|
||||
<Badge variant="danger" className="gap-1">
|
||||
<XCircle className="h-3 w-3" /> Échec
|
||||
</Badge>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState } from "react";
|
||||
import { Page } from "@/components/layout/app-shell";
|
||||
import { PageHeader } from "@/components/layout/page-header";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { CheckCircle, XCircle, Calendar, History as HistoryIcon, Eye, Image as ImageIcon, Smartphone, Loader2, Clapperboard, Search, Sparkles } from "lucide-react";
|
||||
import { CheckCircle, XCircle, RotateCw, Calendar, History as HistoryIcon, Eye, Image as ImageIcon, Smartphone, Loader2, Clapperboard, Search, Sparkles } from "lucide-react";
|
||||
import { Link } from "wouter";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
@@ -27,10 +27,17 @@ function isAwaitingPublication(scheduledPost: ScheduledPostWithRelations): boole
|
||||
return (
|
||||
!!scheduledPost.scheduledAt &&
|
||||
new Date(scheduledPost.scheduledAt) <= new Date() &&
|
||||
!scheduledPost.publishedAt
|
||||
!scheduledPost.publishedAt &&
|
||||
// Un échec ne bouge plus avant sa prochaine tentative (minutes ou heures)
|
||||
!scheduledPost.error
|
||||
);
|
||||
}
|
||||
|
||||
/** Échec dont le scheduler fera une nouvelle tentative automatique. */
|
||||
function isRetrying(scheduledPost: ScheduledPostWithRelations): boolean {
|
||||
return !!scheduledPost.error && !scheduledPost.publishedAt && !!scheduledPost.nextAttemptAt;
|
||||
}
|
||||
|
||||
type StatusFilter = "all" | "published" | "pending" | "failed";
|
||||
|
||||
const STATUS_TABS: { value: StatusFilter; label: string }[] = [
|
||||
@@ -115,8 +122,13 @@ export default function History() {
|
||||
return null;
|
||||
};
|
||||
|
||||
// Un échec avec un nouvel essai programmé est encore « en cours »
|
||||
const statusOf = (sp: ScheduledPostWithRelations): StatusFilter =>
|
||||
sp.error ? "failed" : !sp.publishedAt ? "pending" : "published";
|
||||
sp.publishedAt && !sp.error
|
||||
? "published"
|
||||
: sp.error && !isRetrying(sp)
|
||||
? "failed"
|
||||
: "pending";
|
||||
|
||||
const counts = publishedPosts.reduce(
|
||||
(acc, sp) => {
|
||||
@@ -200,6 +212,8 @@ export default function History() {
|
||||
<p className="truncate font-medium">{scheduledPost.page?.pageName || "Page inconnue"}</p>
|
||||
{status === "failed" ? (
|
||||
<Badge variant="danger" className="gap-1"><XCircle className="h-3 w-3" /> Échec</Badge>
|
||||
) : isRetrying(scheduledPost) ? (
|
||||
<Badge variant="warning" className="gap-1"><RotateCw className="h-3 w-3" /> Nouvel essai</Badge>
|
||||
) : status === "pending" ? (
|
||||
<Badge variant="info" className="gap-1"><Loader2 className="h-3 w-3 animate-spin" /> En cours</Badge>
|
||||
) : (
|
||||
@@ -238,7 +252,7 @@ export default function History() {
|
||||
{scheduledPost.nextAttemptAt
|
||||
? `Nouvelle tentative automatique le ${format(new Date(scheduledPost.nextAttemptAt), "d MMM yyyy 'à' HH:mm", { locale: fr })}`
|
||||
: scheduledPost.attempts > 0
|
||||
? `Abandon après ${scheduledPost.attempts} tentatives : reprogrammez la publication pour réessayer.`
|
||||
? "Plus de nouvelle tentative automatique : reprogrammez la publication pour réessayer."
|
||||
: null}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -37,6 +37,15 @@ pub fn start(state: AppState) {
|
||||
/// (il faut alors la reprogrammer).
|
||||
pub const MAX_PUBLISH_ATTEMPTS: i32 = 5;
|
||||
|
||||
/// Retard au-delà duquel une publication n'est plus envoyée : une promotion
|
||||
/// ou une story publiée un jour trop tard fait plus de mal que de bien.
|
||||
const MAX_PUBLISH_DELAY_HOURS: i64 = 24;
|
||||
|
||||
/// Publication trop en retard sur son heure prévue pour être encore envoyée.
|
||||
fn is_stale(scheduled_at: Ts, now: Ts) -> bool {
|
||||
scheduled_at.add(chrono::Duration::hours(MAX_PUBLISH_DELAY_HOURS)) < now
|
||||
}
|
||||
|
||||
/// Délai avant la tentative suivante, selon le nombre d'échecs déjà subis.
|
||||
/// Une limite de débit (quota d'appels, blocage anti-spam) appelle des délais
|
||||
/// plus longs : réessayer tout de suite prolonge le blocage.
|
||||
@@ -106,6 +115,36 @@ pub async fn process_pending_posts(state: &AppState) {
|
||||
let mut cooldowns: HashMap<Platform, Ts> = HashMap::new();
|
||||
|
||||
for scheduled_post in pending {
|
||||
if is_stale(scheduled_post.scheduled_at, Ts::now()) {
|
||||
tracing::warn!(
|
||||
"[Scheduler] Post {} abandonné : plus de {MAX_PUBLISH_DELAY_HOURS} h de retard",
|
||||
scheduled_post.id
|
||||
);
|
||||
// Le dernier message d'erreur est conservé s'il y en a un
|
||||
let reason = format!(
|
||||
"Publication abandonnée : plus de {MAX_PUBLISH_DELAY_HOURS} h de retard sur l'heure prévue."
|
||||
);
|
||||
let error = match scheduled_post.error.as_deref().filter(|e| !e.is_empty()) {
|
||||
Some(last) => format!("{reason} Dernière erreur : {last}"),
|
||||
None => reason,
|
||||
};
|
||||
let abandoned = state
|
||||
.db
|
||||
.update_scheduled_post(
|
||||
&scheduled_post.id,
|
||||
Patch::new()
|
||||
.set("error", error)
|
||||
.set("attempts", MAX_PUBLISH_ATTEMPTS)
|
||||
.set("next_attempt_at", None::<Ts>),
|
||||
)
|
||||
.await;
|
||||
if let Err(error) = abandoned {
|
||||
tracing::error!("Error processing pending posts: {error}");
|
||||
return;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if !cooldowns.is_empty() {
|
||||
let platform = page_platform(state, &scheduled_post.page_id).await;
|
||||
if let Some(until) = platform.and_then(|p| cooldowns.get(&p)).copied() {
|
||||
@@ -399,6 +438,14 @@ mod tests {
|
||||
assert!(!is_rate_limited("Post or page not found"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn posts_more_than_a_day_late_are_stale() {
|
||||
let now = Ts::parse("2026-10-12T19:00:00Z").unwrap();
|
||||
assert!(is_stale(Ts::parse("2026-10-11T16:30:00Z").unwrap(), now));
|
||||
assert!(!is_stale(Ts::parse("2026-10-12T16:30:00Z").unwrap(), now));
|
||||
assert!(!is_stale(Ts::parse("2026-10-11T19:00:00Z").unwrap(), now));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retry_delays_grow_and_stay_bounded() {
|
||||
assert_eq!(retry_delay(1, false), chrono::Duration::minutes(5));
|
||||
|
||||
Reference in new issue
Block a user