Deux comportements demandés :
1. Une promotion n'apparaît sur le site que pendant sa période de validité.
Les lectures publiques passent par `VISIBLE_PROMO` (`src/lib/queries.ts`) :
statut `live` **et** fenêtre `starts_on` / `ends_on`, journée calculée en
Europe/Paris pour qu'un conteneur en UTC ne retire pas une offre deux
heures trop tôt. Le statut n'est pas touché : hors période, le backoffice
explique pourquoi l'offre n'est pas visible (`visibilityNote`) et elle
revient d'elle-même si les dates changent.
2. La publication peut être programmée. `promotions.publish_at` : l'adhérent
propose une date, le modérateur la garde, la déplace ou la vide dans le
formulaire « Valider ». Avec une échéance future la promotion passe au
statut `scheduled` — rien n'est visible, rien n'est diffusé — et
`releaseDuePromotions()` fait la bascule à l'heure dite, site et réseaux
ensemble. Un bouton « Publier maintenant » court-circuite l'attente.
Mécanique :
- `src/lib/promo-schedule.ts` (pur, testé) : conversion `datetime-local` ⇄
heure de l'association, changements d'heure compris, et refus d'une saisie
illisible plutôt qu'une publication immédiate involontaire.
- `src/lib/promo-publish.ts` reprend `publishPromoShares()` : deux appelants
désormais, une action serveur et la boucle de libération, qui n'a pas de
requête et ne peut donc pas appeler `revalidatePath()`. Idem pour
`logActivity()` dans `src/lib/activity-log.ts`.
- La bascule `scheduled → live` est un `UPDATE … RETURNING` filtré sur le
statut : atomique, donc pas de double publication même à plusieurs
instances, en plus de la garde existante sur `social_posts`.
- Déclencheur : `src/instrumentation.ts`, boucle d'une minute avec rattrapage
au démarrage (`PROMO_SCHEDULER=off` la désactive). Le travail vit dans
`instrumentation-node.ts`, écarté du bundle edge par `next.config.mjs` :
Next compile aussi l'instrumentation pour le middleware, où `pg` ne peut
pas être embarqué.
Migration 0016 : `promo_status += 'scheduled'`, `promotions.publish_at`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QXNXRC4j5VLfKvpyyisrnb