From 7cac8636eb0949a6d3e039d357a60b279b251730 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?BITARD=20Micha=C3=ABl?= <michael.bitard@beta.gouv.fr>
Date: Wed, 26 Feb 2025 13:40:31 +0000
Subject: [PATCH] =?UTF-8?q?chore(api):=20am=C3=A9liore=20la=20remont=C3=A9?=
 =?UTF-8?q?e=20du=20daily=20(pub/pnm-public/camino!1656)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 packages/api/src/business/_logs-update.ts     | 179 -------------
 packages/api/src/business/daily.ts            | 248 +++++++++++++++++-
 .../titres-etapes-heritage-contenu-update.ts  |   9 +-
 .../processes/titres-slugs-update.test.ts     |  11 +-
 .../business/processes/titres-slugs-update.ts |  19 +-
 .../api/src/business/titre-demarche-update.ts |  33 +--
 .../api/src/business/titre-etape-update.ts    |  48 +---
 packages/api/src/business/titre-update.ts     |  14 +-
 packages/api/src/database/init.ts             |   4 +-
 packages/api/src/scripts/daily.ts             |  89 +------
 10 files changed, 302 insertions(+), 352 deletions(-)
 delete mode 100644 packages/api/src/business/_logs-update.ts

diff --git a/packages/api/src/business/_logs-update.ts b/packages/api/src/business/_logs-update.ts
deleted file mode 100644
index bf49ca629..000000000
--- a/packages/api/src/business/_logs-update.ts
+++ /dev/null
@@ -1,179 +0,0 @@
-import { isNotNullNorUndefined, isNotNullNorUndefinedNorEmpty } from 'camino-common/src/typescript-tools'
-import { Index, IEntrepriseEtablissement, IEntreprise } from '../types'
-export const dailySummaryMarker = 'tâches exécutées:' as const
-export const logsUpdate = ({
-  heritageWithUnknownEtapes,
-  etapesCompletesErreurs,
-  tdeErreurs,
-  demarcheDefinitionsErreurs,
-  fondamentaleIdUpdated,
-  consentementUpdated,
-  misesEnConcurrenceUpdated,
-  titresEtapesStatusUpdated,
-  titresEtapesOrdreUpdated,
-  titresEtapesHeritagePropsUpdated,
-  titresEtapesHeritageContenuUpdated,
-  titresDemarchesStatutUpdated,
-  titresDemarchesPublicUpdated,
-  titresDemarchesOrdreUpdated,
-  titresStatutIdUpdated,
-  titresPublicUpdated,
-  titresDemarchesDatesUpdated,
-  titresEtapesAdministrationsLocalesUpdated,
-  titresPropsEtapesIdsUpdated,
-  titresActivitesCreated,
-  titresActivitesRelanceSent,
-  titresActivitesStatutIdsUpdated,
-  titresActivitesPropsUpdated,
-  titresUpdatedIndex,
-  entreprisesUpdated,
-  etablissementsUpdated,
-  etablissementsDeleted,
-}: {
-  heritageWithUnknownEtapes?: unknown[]
-  etapesCompletesErreurs?: { surTitreValide: string[]; autre: string[]; etapesRecentes: string[] }
-  tdeErreurs?: number
-  demarcheDefinitionsErreurs?: number
-  fondamentaleIdUpdated?: number
-  consentementUpdated?: unknown[]
-  misesEnConcurrenceUpdated?: unknown[]
-  titresEtapesStatusUpdated?: string[]
-  titresEtapesOrdreUpdated?: string[]
-  titresEtapesHeritagePropsUpdated?: string[]
-  titresEtapesHeritageContenuUpdated?: string[]
-  titresDemarchesStatutUpdated?: string[]
-  titresDemarchesPublicUpdated?: string[]
-  titresDemarchesOrdreUpdated?: string[]
-  titresStatutIdUpdated?: string[]
-  titresPublicUpdated?: string[]
-  titresDemarchesDatesUpdated?: string[]
-  titresEtapesAdministrationsLocalesUpdated?: string[]
-  titresPropsEtapesIdsUpdated?: string[]
-  titresActivitesCreated?: string[]
-  titresActivitesRelanceSent?: string[]
-  titresActivitesStatutIdsUpdated?: string[]
-  titresActivitesPropsUpdated?: string[]
-  titresUpdatedIndex?: Index<string>
-  entreprisesUpdated?: IEntreprise[]
-  etablissementsUpdated?: IEntrepriseEtablissement[]
-  etablissementsDeleted?: string[]
-}): void => {
-  console.info()
-  console.info('-')
-  console.info(dailySummaryMarker)
-
-  if (isNotNullNorUndefined(etapesCompletesErreurs)) {
-    if (etapesCompletesErreurs.etapesRecentes.length > 0) {
-      console.warn(`${etapesCompletesErreurs.etapesRecentes.length} étapes récentes ne sont pas complètes`)
-    }
-    if (etapesCompletesErreurs.surTitreValide.length > 0) {
-      console.warn(`${etapesCompletesErreurs.surTitreValide.length} étapes sur des titres valides ne sont pas complètes`)
-    }
-    if (etapesCompletesErreurs.autre.length > 0) {
-      console.info(`${etapesCompletesErreurs.autre.length} étapes sur des titres non valides ne sont pas complètes`)
-    }
-  }
-
-  if (isNotNullNorUndefinedNorEmpty(heritageWithUnknownEtapes)) {
-    console.error(`${heritageWithUnknownEtapes.length} heritage de contenu qui pointe sur des étapes non existantes`)
-  }
-  if (isNotNullNorUndefined(tdeErreurs) && tdeErreurs > 0) {
-    console.warn(`${tdeErreurs} erreurs TDE`)
-  }
-
-  if (isNotNullNorUndefined(demarcheDefinitionsErreurs) && demarcheDefinitionsErreurs > 0) {
-    console.warn(`${demarcheDefinitionsErreurs} erreurs sur les définitions des démarches`)
-  }
-
-  if (isNotNullNorUndefined(fondamentaleIdUpdated) && fondamentaleIdUpdated > 0) {
-    console.info(`mise à jour: ${fondamentaleIdUpdated} étapes(s) (fondamentale)`)
-  }
-
-  if (isNotNullNorUndefinedNorEmpty(consentementUpdated)) {
-    console.info(`mise à jour: ${consentementUpdated.length} étapes(s) (consentement)`)
-  }
-
-  if (isNotNullNorUndefinedNorEmpty(misesEnConcurrenceUpdated)) {
-    console.info(`mise à jour: ${misesEnConcurrenceUpdated.length} étapes(s) (concurrence)`)
-  }
-
-  if (isNotNullNorUndefinedNorEmpty(titresEtapesStatusUpdated)) {
-    console.info(`mise à jour: ${titresEtapesStatusUpdated.length} étape(s) (statut)`)
-  }
-
-  if (isNotNullNorUndefinedNorEmpty(titresEtapesOrdreUpdated)) {
-    console.info(`mise à jour: ${titresEtapesOrdreUpdated.length} étape(s) (ordre)`)
-  }
-
-  if (isNotNullNorUndefinedNorEmpty(titresEtapesHeritagePropsUpdated)) {
-    console.info(`mise à jour: ${titresEtapesHeritagePropsUpdated.length} étape(s) (héritage des propriétés)`)
-  }
-
-  if (isNotNullNorUndefinedNorEmpty(titresEtapesHeritageContenuUpdated)) {
-    console.info(`mise à jour: ${titresEtapesHeritageContenuUpdated.length} étape(s) (héritage du contenu)`)
-  }
-
-  if (isNotNullNorUndefinedNorEmpty(titresDemarchesStatutUpdated)) {
-    console.info(`mise à jour: ${titresDemarchesStatutUpdated.length} démarche(s) (statut)`)
-  }
-
-  if (isNotNullNorUndefinedNorEmpty(titresDemarchesPublicUpdated)) {
-    console.info(`mise à jour: ${titresDemarchesPublicUpdated.length} démarche(s) (publicité)`)
-  }
-
-  if (isNotNullNorUndefinedNorEmpty(titresDemarchesOrdreUpdated)) {
-    console.info(`mise à jour: ${titresDemarchesOrdreUpdated.length} démarche(s) (ordre)`)
-  }
-
-  if (isNotNullNorUndefinedNorEmpty(titresStatutIdUpdated)) {
-    console.info(`mise à jour: ${titresStatutIdUpdated.length} titre(s) (statuts)`)
-  }
-
-  if (isNotNullNorUndefinedNorEmpty(titresPublicUpdated)) {
-    console.info(`mise à jour: ${titresPublicUpdated.length} titre(s) (publicité)`)
-  }
-
-  if (isNotNullNorUndefinedNorEmpty(titresDemarchesDatesUpdated)) {
-    console.info(`mise à jour: ${titresDemarchesDatesUpdated.length} titre(s) (phases mises à jour)`)
-  }
-
-  if (isNotNullNorUndefinedNorEmpty(titresEtapesAdministrationsLocalesUpdated)) {
-    console.info(`mise à jour: ${titresEtapesAdministrationsLocalesUpdated.length} administration(s) locale(s) modifiée(s) dans des étapes`)
-  }
-
-  if (isNotNullNorUndefinedNorEmpty(titresPropsEtapesIdsUpdated)) {
-    console.info(`mise à jour: ${titresPropsEtapesIdsUpdated.length} titres(s) (propriétés-étapes)`)
-  }
-
-  if (isNotNullNorUndefinedNorEmpty(titresActivitesCreated)) {
-    console.info(`mise à jour: ${titresActivitesCreated.length} activité(s) créée(s)`)
-  }
-
-  if (isNotNullNorUndefinedNorEmpty(titresActivitesRelanceSent)) {
-    console.info(`mise à jour: ${titresActivitesRelanceSent.length} activité(s) relancée(s)`)
-  }
-
-  if (isNotNullNorUndefinedNorEmpty(titresActivitesStatutIdsUpdated)) {
-    console.info(`mise à jour: ${titresActivitesStatutIdsUpdated.length} activité(s) fermée(s)`)
-  }
-
-  if (isNotNullNorUndefinedNorEmpty(titresActivitesPropsUpdated)) {
-    console.info(`mise à jour: ${titresActivitesPropsUpdated.length} activité(s) (propriété suppression)`)
-  }
-
-  if (isNotNullNorUndefined(titresUpdatedIndex) && Object.keys(titresUpdatedIndex).length) {
-    console.info(`mise à jour: ${Object.keys(titresUpdatedIndex).length} titre(s) (slugs)`)
-  }
-
-  if (isNotNullNorUndefinedNorEmpty(entreprisesUpdated)) {
-    console.info(`mise à jour: ${entreprisesUpdated.length} adresse(s) d'entreprise(s)`)
-  }
-
-  if (isNotNullNorUndefinedNorEmpty(etablissementsUpdated)) {
-    console.info(`mise à jour: ${etablissementsUpdated.length} établissement(s) d'entreprise(s)`)
-  }
-
-  if (isNotNullNorUndefinedNorEmpty(etablissementsDeleted)) {
-    console.info(`suppression: ${etablissementsDeleted.length} établissement(s) d'entreprise(s)`)
-  }
-}
diff --git a/packages/api/src/business/daily.ts b/packages/api/src/business/daily.ts
index 6ca341289..5c523ecde 100644
--- a/packages/api/src/business/daily.ts
+++ b/packages/api/src/business/daily.ts
@@ -12,8 +12,7 @@ import { titresStatutIdsUpdate } from './processes/titres-statut-ids-update'
 import { titresEtapesHeritagePropsUpdate } from './processes/titres-etapes-heritage-props-update'
 import { checkEtapeInContenuHeritage, titresEtapesHeritageContenuUpdate } from './processes/titres-etapes-heritage-contenu-update'
 import { titresActivitesPropsUpdate } from './processes/titres-activites-props-update'
-import { titresSlugsUpdate } from './processes/titres-slugs-update'
-import { logsUpdate } from './_logs-update'
+import { TitreSlugUpdate, titresSlugsUpdate } from './processes/titres-slugs-update'
 import { userSuper } from '../database/user-super'
 import { titresActivitesRelanceSend } from './processes/titres-activites-relance-send'
 import type { Pool } from 'pg'
@@ -25,8 +24,208 @@ import { allEtapesMiseEnConcurrenceUpdate } from './processes/titres-etapes-mise
 import { etapesFondamentaleIdUpdateForAll } from './processes/titres-etapes-fondamentale-id-update'
 import { allEtapesConsentementUpdate } from './processes/titres-etapes-consentement'
 import { etapesCompletesCheck } from '../tools/etapes/etapes-complete-check'
+import { isNotNullNorUndefined, isNotNullNorUndefinedNorEmpty, NonEmptyArray } from 'camino-common/src/typescript-tools'
+import { config } from '../config'
+import { createReadStream, createWriteStream, readFileSync, writeFileSync } from 'node:fs'
+import { createInterface } from 'node:readline'
+import { fetch } from 'undici'
+import { mailjetSend } from '../tools/api-mailjet/emails'
+import { EtapeId } from 'camino-common/src/etape'
 
-export const daily = async (pool: Pool): Promise<void> => {
+interface Daily {
+  heritageWithUnknownEtapes: unknown[]
+  etapesCompletesErreurs: { surTitreValide: string[]; autre: string[]; etapesRecentes: string[] }
+  tdeErreurs: number
+  demarcheDefinitionsErreurs: number
+  fondamentaleIdUpdated: number
+  consentementUpdated: unknown[]
+  misesEnConcurrenceUpdated: unknown[]
+  titresEtapesStatusUpdated: string[]
+  titresEtapesOrdreUpdated: string[]
+  titresEtapesHeritagePropsUpdated: string[]
+  titresEtapesHeritageContenuUpdated: {
+    updated: EtapeId[]
+    errors: EtapeId[]
+  }
+  titresDemarchesStatutUpdated: string[]
+  titresDemarchesPublicUpdated: string[]
+  titresDemarchesOrdreUpdated: string[]
+  titresStatutIdUpdated: string[]
+  titresPublicUpdated: string[]
+  titresDemarchesDatesUpdated: string[]
+  titresEtapesAdministrationsLocalesUpdated: string[]
+  titresPropsEtapesIdsUpdated: string[]
+  titresActivitesCreated: string[]
+  titresActivitesRelanceSent: string[]
+  titresActivitesStatutIdsUpdated: string[]
+  titresActivitesPropsUpdated: string[]
+  titresUpdatedIndex: TitreSlugUpdate[]
+}
+
+// Mis à jour le 2025-02-26
+const NOMBRE_ERREURS_ETAPES = 10870 as const
+
+type DailyResult = { changes: false } | { changes: true; logs: NonEmptyArray<string> }
+const dailyResult = (daily: Daily | null): DailyResult => {
+  if (daily === null) {
+    return { changes: true, logs: ['Une erreur est survenue durant le daily'] }
+  }
+  const logs: string[] = []
+  if (isNotNullNorUndefined(daily.etapesCompletesErreurs)) {
+    if (daily.etapesCompletesErreurs.etapesRecentes.length + daily.etapesCompletesErreurs.surTitreValide.length + daily.etapesCompletesErreurs.surTitreValide.length > NOMBRE_ERREURS_ETAPES) {
+      logs.push("ATTENTION IL Y'A DE NOUVELLES ÉTAPES INCOMPLÈTES")
+      if (daily.etapesCompletesErreurs.etapesRecentes.length > 0) {
+        logs.push(`${daily.etapesCompletesErreurs.etapesRecentes.length} étapes récentes ne sont pas complètes`)
+      }
+      if (daily.etapesCompletesErreurs.surTitreValide.length > 0) {
+        logs.push(`${daily.etapesCompletesErreurs.surTitreValide.length} étapes sur des titres valides ne sont pas complètes`)
+      }
+      if (daily.etapesCompletesErreurs.surTitreValide.length > 0) {
+        logs.push(`${daily.etapesCompletesErreurs.autre.length} étapes sur des titres non valides ne sont pas complètes`)
+      }
+    }
+  }
+
+  if (isNotNullNorUndefinedNorEmpty(daily.heritageWithUnknownEtapes)) {
+    logs.push(`${daily.heritageWithUnknownEtapes.length} heritage de contenu qui pointe sur des étapes non existantes`)
+  }
+  if (isNotNullNorUndefined(daily.tdeErreurs) && daily.tdeErreurs > 0) {
+    logs.push(`${daily.tdeErreurs} erreurs TDE`)
+  }
+
+  if (isNotNullNorUndefined(daily.demarcheDefinitionsErreurs) && daily.demarcheDefinitionsErreurs > 0) {
+    logs.push(`${daily.demarcheDefinitionsErreurs} erreurs sur les définitions des démarches`)
+  }
+
+  if (isNotNullNorUndefined(daily.fondamentaleIdUpdated) && daily.fondamentaleIdUpdated > 0) {
+    logs.push(`mise à jour: ${daily.fondamentaleIdUpdated} étapes(s) (fondamentale)`)
+  }
+
+  if (isNotNullNorUndefinedNorEmpty(daily.consentementUpdated)) {
+    logs.push(`mise à jour: ${daily.consentementUpdated.length} étapes(s) (consentement)`)
+  }
+
+  if (isNotNullNorUndefinedNorEmpty(daily.misesEnConcurrenceUpdated)) {
+    logs.push(`mise à jour: ${daily.misesEnConcurrenceUpdated.length} étapes(s) (concurrence)`)
+  }
+
+  if (isNotNullNorUndefinedNorEmpty(daily.titresEtapesStatusUpdated)) {
+    logs.push(`mise à jour: ${daily.titresEtapesStatusUpdated.length} étape(s) (statut)`)
+  }
+
+  if (isNotNullNorUndefinedNorEmpty(daily.titresEtapesOrdreUpdated)) {
+    logs.push(`mise à jour: ${daily.titresEtapesOrdreUpdated.length} étape(s) (ordre)`)
+  }
+
+  if (isNotNullNorUndefinedNorEmpty(daily.titresEtapesHeritagePropsUpdated)) {
+    logs.push(`mise à jour: ${daily.titresEtapesHeritagePropsUpdated.length} étape(s) (héritage des propriétés)`)
+  }
+
+  if (isNotNullNorUndefinedNorEmpty(daily.titresEtapesHeritageContenuUpdated.updated)) {
+    logs.push(`mise à jour: ${daily.titresEtapesHeritageContenuUpdated.updated.length} étape(s) (héritage du contenu)`)
+  }
+
+  if (isNotNullNorUndefinedNorEmpty(daily.titresEtapesHeritageContenuUpdated.errors)) {
+    logs.push(`erreurs: ${daily.titresEtapesHeritageContenuUpdated.errors.length} étape(s) (héritage du contenu) en erreur`)
+  }
+
+  if (isNotNullNorUndefinedNorEmpty(daily.titresDemarchesStatutUpdated)) {
+    logs.push(`mise à jour: ${daily.titresDemarchesStatutUpdated.length} démarche(s) (statut)`)
+  }
+
+  if (isNotNullNorUndefinedNorEmpty(daily.titresDemarchesPublicUpdated)) {
+    logs.push(`mise à jour: ${daily.titresDemarchesPublicUpdated.length} démarche(s) (publicité)`)
+  }
+
+  if (isNotNullNorUndefinedNorEmpty(daily.titresDemarchesOrdreUpdated)) {
+    logs.push(`mise à jour: ${daily.titresDemarchesOrdreUpdated.length} démarche(s) (ordre)`)
+  }
+
+  if (isNotNullNorUndefinedNorEmpty(daily.titresStatutIdUpdated)) {
+    logs.push(`mise à jour: ${daily.titresStatutIdUpdated.length} titre(s) (statuts)`)
+  }
+
+  if (isNotNullNorUndefinedNorEmpty(daily.titresPublicUpdated)) {
+    logs.push(`mise à jour: ${daily.titresPublicUpdated.length} titre(s) (publicité)`)
+  }
+
+  if (isNotNullNorUndefinedNorEmpty(daily.titresDemarchesDatesUpdated)) {
+    logs.push(`mise à jour: ${daily.titresDemarchesDatesUpdated.length} titre(s) (phases mises à jour)`)
+  }
+
+  if (isNotNullNorUndefinedNorEmpty(daily.titresEtapesAdministrationsLocalesUpdated)) {
+    logs.push(`mise à jour: ${daily.titresEtapesAdministrationsLocalesUpdated.length} administration(s) locale(s) modifiée(s) dans des étapes`)
+  }
+
+  if (isNotNullNorUndefinedNorEmpty(daily.titresPropsEtapesIdsUpdated)) {
+    logs.push(`mise à jour: ${daily.titresPropsEtapesIdsUpdated.length} titres(s) (propriétés-étapes)`)
+  }
+
+  if (isNotNullNorUndefinedNorEmpty(daily.titresActivitesCreated)) {
+    logs.push(`mise à jour: ${daily.titresActivitesCreated.length} activité(s) créée(s)`)
+  }
+
+  if (isNotNullNorUndefinedNorEmpty(daily.titresActivitesRelanceSent)) {
+    logs.push(`mise à jour: ${daily.titresActivitesRelanceSent.length} activité(s) relancée(s)`)
+  }
+
+  if (isNotNullNorUndefinedNorEmpty(daily.titresActivitesStatutIdsUpdated)) {
+    logs.push(`mise à jour: ${daily.titresActivitesStatutIdsUpdated.length} activité(s) fermée(s)`)
+  }
+
+  if (isNotNullNorUndefinedNorEmpty(daily.titresActivitesPropsUpdated)) {
+    logs.push(`mise à jour: ${daily.titresActivitesPropsUpdated.length} activité(s) (propriété suppression)`)
+  }
+
+  if (isNotNullNorUndefinedNorEmpty(daily.titresUpdatedIndex)) {
+    logs.push(`mise à jour: ${daily.titresUpdatedIndex.length} titre(s) (slugs)`)
+  }
+
+  if (isNotNullNorUndefinedNorEmpty(logs)) {
+    return { changes: true, logs }
+  }
+  return { changes: false }
+}
+
+export const daily = async (pool: Pool, logFile: string): Promise<void> => {
+  const output = createWriteStream(logFile, { flush: true, autoClose: true })
+
+  // Réinitialise les logs qui seront envoyés par email
+  writeFileSync(logFile, '')
+  let resume: DailyResult = { changes: true, logs: ["Le daily ne s'est pas lancé"] }
+  try {
+    resume = await rawDaily(pool)
+  } catch (e) {
+    console.error('Erreur durant le daily', e)
+  }
+
+  if (isNotNullNorUndefined(config().CAMINO_STAGE)) {
+    await new Promise<void>(resolve => {
+      output.end(() => resolve())
+    })
+    const emailBody = `Résultats de ${config().ENV} \n${resume.changes ? resume.logs.join('\n') : 'Pas de changement\n'}\n${readFileSync(logFile).toString()}`
+    try {
+      const tchapHook = config().TCHAP_HOOK
+      if (isNotNullNorUndefined(tchapHook)) {
+        const markdown = await transformIntoMarkDown(resume, logFile)
+        await tchapSend(markdown, tchapHook)
+      }
+    } catch (e: unknown) {
+      let errorMessage = "Une erreur s'est produite pendant l'envoi du daily sur tchap"
+      if (e instanceof Error) {
+        errorMessage += `-> ${e.message}`
+      }
+      console.error(errorMessage)
+    }
+
+    // TODO 2024-12-05 enlever le daily par email si il s'est bien envoyé via tchap
+    await mailjetSend([config().ADMIN_EMAIL], {
+      Subject: `[Camino][${config().ENV}] Résultats du daily`,
+      TextPart: emailBody,
+    })
+  }
+}
+const rawDaily = async (pool: Pool): Promise<DailyResult> => {
   try {
     console.info()
     console.info('- - -')
@@ -62,7 +261,7 @@ export const daily = async (pool: Pool): Promise<void> => {
     const tdeErreurs = await titreTypeDemarcheTypeEtapeTypeCheck()
     const etapesCompletesErreurs = await etapesCompletesCheck(pool)
 
-    logsUpdate({
+    return dailyResult({
       heritageWithUnknownEtapes,
       etapesCompletesErreurs: etapesCompletesErreurs,
       tdeErreurs,
@@ -94,3 +293,44 @@ export const daily = async (pool: Pool): Promise<void> => {
     throw e
   }
 }
+
+const transformIntoMarkDown = async (resume: DailyResult, logFile: string): Promise<string> => {
+  const fileStream = createReadStream(logFile)
+
+  const readLine = createInterface(fileStream)
+  let detail = ''
+  const summary = resume.changes ? resume.logs.map(line => `\n* ${line}`).join('') : ''
+
+  for await (const line of readLine) {
+    detail += `\n${line}`
+  }
+  return `### Résultat du daily de ${config().ENV}
+${
+  summary !== ''
+    ? `<details>
+    <summary>Résumé du daily</summary>
+${summary}
+</details>`
+    : '**Pas de changement**'
+}
+<details>
+    <summary>Détail du daily</summary>
+
+\`\`\`bash
+${detail}
+\`\`\`
+
+</details>`
+  fileStream.close()
+}
+
+const tchapSend = async (markdown: string, url: string): Promise<void> => {
+  await fetch(url, {
+    method: 'POST',
+    body: JSON.stringify({ message: markdown, message_format: 'markdown' }),
+    headers: {
+      Accept: 'application/json',
+      'Content-Type': 'application/json',
+    },
+  })
+}
diff --git a/packages/api/src/business/processes/titres-etapes-heritage-contenu-update.ts b/packages/api/src/business/processes/titres-etapes-heritage-contenu-update.ts
index cd5b05c9c..24dca5acc 100644
--- a/packages/api/src/business/processes/titres-etapes-heritage-contenu-update.ts
+++ b/packages/api/src/business/processes/titres-etapes-heritage-contenu-update.ts
@@ -55,7 +55,7 @@ export const checkEtapeInContenuHeritage = (pool: Pool): Effect.Effect<ErrorInCo
     )
   )
 }
-export const titresEtapesHeritageContenuUpdate = async (pool: Pool, user: UserNotNull, demarcheId?: DemarcheId): Promise<string[]> => {
+export const titresEtapesHeritageContenuUpdate = async (pool: Pool, user: UserNotNull, demarcheId?: DemarcheId): Promise<{ updated: EtapeId[]; errors: EtapeId[] }> => {
   console.info()
   console.info('héritage des contenus des étapes…')
 
@@ -65,7 +65,8 @@ export const titresEtapesHeritageContenuUpdate = async (pool: Pool, user: UserNo
   // l'objet heritageContenu reçu ne contient pas d'id d'étape
   // l'étape est donc toujours mise à jour
 
-  const titresEtapesIdsUpdated = [] as string[]
+  const titresEtapesIdsUpdated: EtapeId[] = []
+  const titresEtapesIdsErrors: EtapeId[] = []
 
   for (const titreDemarche of Object.values(titresDemarches)) {
     if (isNotNullNorUndefinedNorEmpty(titreDemarche.etapes)) {
@@ -86,7 +87,7 @@ export const titresEtapesHeritageContenuUpdate = async (pool: Pool, user: UserNo
           // TODO 2025-02-24 : à décommenter et à lancer en prod, puis à supprimer
           // prettier-ignore
           // await titreEtapeUpdate(etapePerdantLesSections.id, { contenu: null, heritageContenu: null, }, user, titreDemarche.titreId)
-          titresEtapesIdsUpdated.push(etapePerdantLesSections.id)
+          titresEtapesIdsErrors.push(etapePerdantLesSections.id)
         }
       }
       // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
@@ -122,5 +123,5 @@ export const titresEtapesHeritageContenuUpdate = async (pool: Pool, user: UserNo
     }
   }
 
-  return titresEtapesIdsUpdated
+  return { updated: titresEtapesIdsUpdated, errors: titresEtapesIdsErrors }
 }
diff --git a/packages/api/src/business/processes/titres-slugs-update.test.ts b/packages/api/src/business/processes/titres-slugs-update.test.ts
index 5969e2181..7fc778fa0 100644
--- a/packages/api/src/business/processes/titres-slugs-update.test.ts
+++ b/packages/api/src/business/processes/titres-slugs-update.test.ts
@@ -5,7 +5,6 @@ import { titreSlugAndRelationsUpdate } from '../utils/titre-slug-and-relations-u
 import { titresGet } from '../../database/queries/titres'
 import { vi, describe, expect, test } from 'vitest'
 import { titreSlugValidator } from 'camino-common/src/validators/titres'
-import { isNotNullNorUndefined } from 'camino-common/src/typescript-tools'
 vi.mock('../utils/titre-slug-and-relations-update', () => ({
   __esModule: true,
   titreSlugAndRelationsUpdate: vi.fn(),
@@ -37,9 +36,15 @@ describe("mise à jour du slug d'un titre", () => {
     })
 
     const titresUpdatedIndex = await titresSlugsUpdate()
-    const titreSlug = isNotNullNorUndefined(titresUpdatedIndex) && Object.keys(titresUpdatedIndex)[0]
 
-    expect(titreSlug).toEqual(slug)
+    expect(titresUpdatedIndex).toMatchInlineSnapshot(`
+      [
+        {
+          "newSlug": "slug-new",
+          "oldSlug": "slug-old",
+        },
+      ]
+    `)
 
     expect(titreSlugAndRelationsUpdate).toHaveBeenCalled()
   })
diff --git a/packages/api/src/business/processes/titres-slugs-update.ts b/packages/api/src/business/processes/titres-slugs-update.ts
index 1b32c2779..f1c59154e 100644
--- a/packages/api/src/business/processes/titres-slugs-update.ts
+++ b/packages/api/src/business/processes/titres-slugs-update.ts
@@ -4,9 +4,15 @@ import { titresGet } from '../../database/queries/titres'
 import { userSuper } from '../../database/user-super'
 
 import { titreSlugAndRelationsUpdate } from '../utils/titre-slug-and-relations-update'
+import { TitreSlug } from 'camino-common/src/validators/titres'
+import { isNotNullNorUndefined } from 'camino-common/src/typescript-tools'
 
+export interface TitreSlugUpdate {
+  newSlug: TitreSlug
+  oldSlug: TitreSlug | undefined
+}
 // met à jour les slugs de titre
-const titreSlugsUpdate = async (titre: ITitre) => {
+const titreSlugsUpdate = async (titre: ITitre): Promise<TitreSlugUpdate | null> => {
   const titreOldSlug = titre.slug
 
   try {
@@ -16,7 +22,7 @@ const titreSlugsUpdate = async (titre: ITitre) => {
 
     console.info('titre : slug (mise à jour) ->', slug)
 
-    return { [slug]: titreOldSlug }
+    return { newSlug: slug, oldSlug: titreOldSlug }
   } catch (e) {
     console.error(`erreur: titreSlugsUpdate ${titreOldSlug}`, e)
 
@@ -24,7 +30,7 @@ const titreSlugsUpdate = async (titre: ITitre) => {
   }
 }
 
-export const titresSlugsUpdate = async (titresIds?: string[]): Promise<Record<string, string>> => {
+export const titresSlugsUpdate = async (titresIds?: string[]): Promise<TitreSlugUpdate[]> => {
   console.info()
   console.info('slugs de titres, démarches, étapes et activités')
 
@@ -43,13 +49,12 @@ export const titresSlugsUpdate = async (titresIds?: string[]): Promise<Record<st
     userSuper
   )
 
-  const titresUpdatedIndex: Record<string, string> = {}
+  const titresUpdatedIndex: TitreSlugUpdate[] = []
 
   for (const titre of titres) {
     const titreUpdatedIndex = await titreSlugsUpdate(titre)
-
-    if (titreUpdatedIndex) {
-      Object.assign(titresUpdatedIndex, titreUpdatedIndex)
+    if (isNotNullNorUndefined(titreUpdatedIndex)) {
+      titresUpdatedIndex.push(titreUpdatedIndex)
     }
   }
 
diff --git a/packages/api/src/business/titre-demarche-update.ts b/packages/api/src/business/titre-demarche-update.ts
index 44a093718..d7fc2489a 100644
--- a/packages/api/src/business/titre-demarche-update.ts
+++ b/packages/api/src/business/titre-demarche-update.ts
@@ -8,7 +8,6 @@ import { titresDemarchesDatesUpdate } from './processes/titres-phases-update'
 import { titresDemarchesOrdreUpdate } from './processes/titres-demarches-ordre-update'
 import { titresPublicUpdate } from './processes/titres-public-update'
 import { titresSlugsUpdate } from './processes/titres-slugs-update'
-import { logsUpdate } from './_logs-update'
 import { titresActivitesPropsUpdate } from './processes/titres-activites-props-update'
 import { userSuper } from '../database/user-super'
 import type { Pool } from 'pg'
@@ -27,35 +26,21 @@ export const titreDemarcheUpdateTask = async (pool: Pool, titreDemarcheId: Demar
       throw new Error(`warning: le titre ${titreId} n'existe pas`)
     }
 
-    let titresDemarchesPublicUpdated
-
-    const titresDemarchesOrdreUpdated = await titresDemarchesOrdreUpdate([titreId])
-    const titresDemarchesDatesUpdated = await titresDemarchesDatesUpdate(pool, [titreId])
+    await titresDemarchesOrdreUpdate([titreId])
+    await titresDemarchesDatesUpdate(pool, [titreId])
 
     // si c'est une création ou modification
     // pas une suppression
     if (titreDemarcheId) {
-      titresDemarchesPublicUpdated = await titresDemarchesPublicUpdate([titreId])
+      await titresDemarchesPublicUpdate([titreId])
     }
-    const titresStatutIdUpdated = await titresStatutIdsUpdate([titreId])
-    const titresPublicUpdated = await titresPublicUpdate(pool, [titreId])
-    const titresPropsEtapesIdsUpdated = await titresPropsEtapesIdsUpdate([titreId])
-    const titresActivitesCreated = await titresActivitesUpdate(pool, [titreId])
-    const titresActivitesPropsUpdated = await titresActivitesPropsUpdate([titreId])
-
-    const titresUpdatedIndex = await titresSlugsUpdate([titreId])
+    await titresStatutIdsUpdate([titreId])
+    await titresPublicUpdate(pool, [titreId])
+    await titresPropsEtapesIdsUpdate([titreId])
+    await titresActivitesUpdate(pool, [titreId])
+    await titresActivitesPropsUpdate([titreId])
 
-    logsUpdate({
-      titresDemarchesPublicUpdated,
-      titresDemarchesOrdreUpdated,
-      titresStatutIdUpdated,
-      titresPublicUpdated,
-      titresDemarchesDatesUpdated,
-      titresPropsEtapesIdsUpdated,
-      titresActivitesCreated,
-      titresActivitesPropsUpdated,
-      titresUpdatedIndex,
-    })
+    await titresSlugsUpdate([titreId])
   } catch (e) {
     console.error(`erreur: titreDemarcheUpdate ${titreId}`)
     console.error(e)
diff --git a/packages/api/src/business/titre-etape-update.ts b/packages/api/src/business/titre-etape-update.ts
index e413a4b16..a669b992c 100644
--- a/packages/api/src/business/titre-etape-update.ts
+++ b/packages/api/src/business/titre-etape-update.ts
@@ -16,7 +16,6 @@ import { titresEtapesAdministrationsLocalesUpdate } from './processes/titres-eta
 import { titresPropsEtapesIdsUpdate } from './processes/titres-props-etapes-ids-update'
 import { titresSlugsUpdate } from './processes/titres-slugs-update'
 import { titresPublicUpdate } from './processes/titres-public-update'
-import { logsUpdate } from './_logs-update'
 import { titresActivitesPropsUpdate } from './processes/titres-activites-props-update'
 import { userSuper } from '../database/user-super'
 import type { UserNotNull } from 'camino-common/src/roles'
@@ -49,50 +48,31 @@ export const titreEtapeUpdateTask = async (pool: Pool, titreEtapeId: EtapeId | n
       await callAndExit(etapeConsentementUpdate(pool, titreEtapeId))
     }
 
-    const titresEtapesOrdreUpdated = await titresEtapesOrdreUpdate(pool, user, titreDemarcheId)
+    await titresEtapesOrdreUpdate(pool, user, titreDemarcheId)
     await callAndExit(etapesFondamentaleIdUpdate(pool, titreDemarcheId))
 
-    const titresEtapesHeritagePropsUpdated = await titresEtapesHeritagePropsUpdate(user, [titreDemarcheId])
-    const titresEtapesHeritageContenuUpdated = await titresEtapesHeritageContenuUpdate(pool, user, titreDemarcheId)
+    await titresEtapesHeritagePropsUpdate(user, [titreDemarcheId])
+    await titresEtapesHeritageContenuUpdate(pool, user, titreDemarcheId)
 
     const titreId = titreDemarche.titreId
-    const titresDemarchesStatutUpdated = await titresDemarchesStatutIdUpdate(pool, titreId)
-    const titresDemarchesOrdreUpdated = await titresDemarchesOrdreUpdate([titreId])
-    const titresDemarchesDatesUpdated = await titresDemarchesDatesUpdate(pool, [titreId])
-    const titresDemarchesPublicUpdated = await titresDemarchesPublicUpdate([titreId])
-    const titresStatutIdUpdated = await titresStatutIdsUpdate([titreId])
-    const titresPublicUpdated = await titresPublicUpdate(pool, [titreId])
+    await titresDemarchesStatutIdUpdate(pool, titreId)
+    await titresDemarchesOrdreUpdate([titreId])
+    await titresDemarchesDatesUpdate(pool, [titreId])
+    await titresDemarchesPublicUpdate([titreId])
+    await titresStatutIdsUpdate([titreId])
+    await titresPublicUpdate(pool, [titreId])
 
     // si l'étape est supprimée, pas de mise à jour
     if (titreEtapeId) {
       await titresEtapesAreasUpdate(pool, [titreEtapeId])
     }
 
-    const titresEtapesAdministrationsLocalesUpdated = await titresEtapesAdministrationsLocalesUpdate(titreEtapeId ? [titreEtapeId] : undefined)
+    await titresEtapesAdministrationsLocalesUpdate(titreEtapeId ? [titreEtapeId] : undefined)
 
-    const titresPropsEtapesIdsUpdated = await titresPropsEtapesIdsUpdate([titreId])
-
-    const titresActivitesCreated = await titresActivitesUpdate(pool, [titreId])
-    const titresActivitesPropsUpdated = await titresActivitesPropsUpdate([titreId])
-
-    const titresUpdatedIndex = await titresSlugsUpdate([titreId])
-
-    logsUpdate({
-      titresEtapesOrdreUpdated,
-      titresEtapesHeritagePropsUpdated,
-      titresEtapesHeritageContenuUpdated,
-      titresDemarchesStatutUpdated,
-      titresDemarchesPublicUpdated,
-      titresDemarchesOrdreUpdated,
-      titresStatutIdUpdated,
-      titresPublicUpdated,
-      titresDemarchesDatesUpdated,
-      titresEtapesAdministrationsLocalesUpdated: titresEtapesAdministrationsLocalesUpdated.map(({ titreEtapeId }) => titreEtapeId),
-      titresPropsEtapesIdsUpdated,
-      titresActivitesCreated,
-      titresActivitesPropsUpdated,
-      titresUpdatedIndex,
-    })
+    await titresPropsEtapesIdsUpdate([titreId])
+    await titresActivitesUpdate(pool, [titreId])
+    await titresActivitesPropsUpdate([titreId])
+    await titresSlugsUpdate([titreId])
   } catch (e) {
     console.error(`erreur: titreEtapeUpdate ${titreEtapeId}`)
     console.error(e)
diff --git a/packages/api/src/business/titre-update.ts b/packages/api/src/business/titre-update.ts
index cb2a1add3..51a707b4c 100644
--- a/packages/api/src/business/titre-update.ts
+++ b/packages/api/src/business/titre-update.ts
@@ -1,7 +1,6 @@
 import { titresActivitesUpdate } from './processes/titres-activites-update'
 import { titresPublicUpdate } from './processes/titres-public-update'
 import { titresSlugsUpdate } from './processes/titres-slugs-update'
-import { logsUpdate } from './_logs-update'
 import { TitreId } from 'camino-common/src/validators/titres'
 import { Pool } from 'pg'
 
@@ -11,16 +10,9 @@ export const titreUpdateTask = async (pool: Pool, titreId: TitreId): Promise<voi
     console.info('- - -')
     console.info(`mise à jour d'un titre : ${titreId}`)
 
-    const titresPublicUpdated = await titresPublicUpdate(pool, [titreId])
-
-    const titresActivitesCreated = await titresActivitesUpdate(pool, [titreId])
-    const titresUpdatedIndex = await titresSlugsUpdate([titreId])
-
-    logsUpdate({
-      titresPublicUpdated,
-      titresActivitesCreated,
-      titresUpdatedIndex,
-    })
+    await titresPublicUpdate(pool, [titreId])
+    await titresActivitesUpdate(pool, [titreId])
+    await titresSlugsUpdate([titreId])
   } catch (e) {
     console.error(`erreur: titreUpdate ${titreId}`)
     console.error(e)
diff --git a/packages/api/src/database/init.ts b/packages/api/src/database/init.ts
index 44b5e8407..7a65fd750 100644
--- a/packages/api/src/database/init.ts
+++ b/packages/api/src/database/init.ts
@@ -1,13 +1,13 @@
 import { knex } from '../knex'
 import { daily } from '../business/daily'
 import type { Pool } from 'pg'
-import { config } from '../config/index'
 import { isNotNullNorUndefined } from 'camino-common/src/typescript-tools'
+import { config } from '../config'
 
 export const databaseInit = async (pool: Pool): Promise<void> => {
   await knex.migrate.latest()
   if (isNotNullNorUndefined(config().CAMINO_STAGE)) {
     // pas de await pour ne pas bloquer le démarrage de l’appli
-    daily(pool) // eslint-disable-line @typescript-eslint/no-floating-promises
+    daily(pool, '/tmp/unused') // eslint-disable-line @typescript-eslint/no-floating-promises
   }
 }
diff --git a/packages/api/src/scripts/daily.ts b/packages/api/src/scripts/daily.ts
index ab1e82594..117f54a1c 100644
--- a/packages/api/src/scripts/daily.ts
+++ b/packages/api/src/scripts/daily.ts
@@ -1,21 +1,17 @@
 import '../init'
 import { daily } from '../business/daily'
 import { consoleOverride } from '../config/logger'
-import { mailjetSend } from '../tools/api-mailjet/emails'
-import { readFileSync, writeFileSync, createWriteStream, createReadStream } from 'node:fs'
+import { writeFileSync, createWriteStream } from 'node:fs'
 import * as Console from 'console'
 import pg from 'pg'
 import { config } from '../config/index'
 import { isNotNullNorUndefined } from 'camino-common/src/typescript-tools'
 import { setGlobalDispatcher, EnvHttpProxyAgent } from 'undici'
-import { createInterface } from 'node:readline'
-import { dailySummaryMarker } from '../business/_logs-update'
-import { fetch } from 'undici'
 
 const envHttpProxyAgent = new EnvHttpProxyAgent()
 setGlobalDispatcher(envHttpProxyAgent)
 
-const logFile = '/tmp/cron.log'
+const logFile = '/tmp/cron.log' as const
 const output = createWriteStream(logFile, { flush: true, autoClose: true })
 // eslint-disable-next-line
 const oldConsole = console.log
@@ -34,92 +30,17 @@ const pool = new pg.Pool({
   database: config().PGDATABASE,
 })
 
-const transformIntoMarkDown = async (): Promise<string> => {
-  const fileStream = createReadStream(logFile)
-
-  const readLine = createInterface(fileStream)
-  let detail = ''
-  let summary = ''
-
-  let summaryBegin = false
-  for await (const line of readLine) {
-    if (line.includes(dailySummaryMarker)) {
-      summaryBegin = true
-    } else {
-      if (summaryBegin) {
-        summary += `\n* ${line}`
-      } else {
-        detail += `\n${line}`
-      }
-    }
-  }
-  return `### Résultat du daily de ${config().ENV}
-${
-  summary !== ''
-    ? `<details>
-    <summary>Résumé du daily</summary>
-${summary}
-</details>`
-    : '**Pas de changement**'
-}
-<details>
-    <summary>Détail du daily</summary>
-
-\`\`\`bash
-${detail}
-\`\`\`
-
-</details>`
-}
-
-const tchapSend = async (markdown: string, url: string): Promise<void> => {
-  await fetch(url, {
-    method: 'POST',
-    body: JSON.stringify({ message: markdown, message_format: 'markdown' }),
-    headers: {
-      Accept: 'application/json',
-      'Content-Type': 'application/json',
-    },
-  })
-}
-
 const tasks = async () => {
   console.info('Tâches quotidiennes : démarrage')
   // Réinitialise les logs qui seront envoyés par email
   writeFileSync(logFile, '')
   try {
-    await daily(pool)
+    await daily(pool, logFile)
   } catch (e) {
     console.error('Erreur durant le daily', e)
   }
-
-  if (isNotNullNorUndefined(config().CAMINO_STAGE)) {
-    await new Promise<void>(resolve => {
-      output.end(() => resolve())
-    })
-    // eslint-disable-next-line
-    console.log = oldConsole
-    const emailBody = `Résultats de ${config().ENV} \n${readFileSync(logFile).toString()}`
-    try {
-      const tchapHook = config().TCHAP_HOOK
-      if (isNotNullNorUndefined(tchapHook)) {
-        const markdown = await transformIntoMarkDown()
-        await tchapSend(markdown, tchapHook)
-      }
-    } catch (e: unknown) {
-      let errorMessage = "Une erreur s'est produite pendant l'envoi du daily sur tchap"
-      if (e instanceof Error) {
-        errorMessage += `-> ${e.message}`
-      }
-      console.error(errorMessage)
-    }
-
-    // TODO 2024-12-05 enlever le daily par email si il s'est bien envoyé via tchap
-    await mailjetSend([config().ADMIN_EMAIL], {
-      Subject: `[Camino][${config().ENV}] Résultats du daily`,
-      TextPart: emailBody,
-    })
-  }
+  // eslint-disable-next-line no-console
+  console.log = oldConsole
   console.info('Tâches quotidiennes : terminé')
 }
 
-- 
GitLab