SPB Git forge

spb/groupe-ka

Public

Groupe KA — site du holding + KA ID (compte unique & SSO des 7 plateformes). Next.js 16, SQLite, Google & Apple login.

81commits 1branches 0releases
89.2 MBsize
maindefault branch
22 days agolast push
TypeScript 70.4% HTML 18.4% JavaScript 4% Python 3.8% CSS 3.4%
4.1 KB · 156 lines typescript
Raw Blame History
1// Auteur : Simon-Pierre Boucher — contact@spboucher.ai2// Lecture des rapports quotidiens générés par scripts/rapport-quotidien.mjs3// (data/rapports/YYYY-MM-DD.json + .pdf). Aucune génération ici : le job PM24// groupe-ka-rapport-quotidien écrit, le site lit.5import { promises as fs } from "node:fs";6import path from "node:path";78export const RAPPORTS_DIR = path.join(process.cwd(), "data", "rapports");9const RE_DATE = /^\d{4}-\d{2}-\d{2}$/;1011export type MeteoEtat = "beau" | "variable" | "orage";1213export type ServiceSnap = {14  service: string;15  nom: string;16  description: string;17  total: number | null;18  date_jour: string | null;19  jour: number | null;20  veille: number | null;21  delta_pct: number | null;22  statut: string;23  serie_14j: { d: string; n: number }[];24};2526export type Snapshot = {27  date_donnees: string;28  services: ServiceSnap[];29  totaux: { records_jour: number; records_total: number };30  connecteurs: {31    ok: number;32    degraded: number;33    broken: number;34    stale: number;35  } | null;36  api_7j: {37    appels: number | null;38    latence_ms: number | null;39    erreurs_pct: number | null;40  };41  runs_du_jour: {42    service: string;43    statut: string;44    records: number | null;45    duree_s: number | null;46  }[];47};4849export type RapportContenu = {50  titre: string;51  sous_titre?: string;52  meteo?: { etat: MeteoEtat; resume: string };53  chiffre_du_jour?: { valeur: string; label: string; contexte?: string };54  edito: string[];55  faits_saillants: { plateforme: string; texte: string }[];56  analyse: { titre: string; contenu: string[] }[];57  vigilance?: string[];58  mot_de_la_fin?: string;59};6061export type Rapport = {62  date: string;63  genere_le: string;64  modele: string;65  contenu: RapportContenu;66  snapshot: Snapshot;67};6869export type RapportResume = {70  date: string;71  titre: string;72  sous_titre?: string;73  meteo?: { etat: MeteoEtat; resume: string };74  chiffre?: { valeur: string; label: string };75  records_jour: number;76};7778export function isDateValide(date: string): boolean {79  return RE_DATE.test(date);80}8182export async function getRapport(date: string): Promise<Rapport | null> {83  if (!isDateValide(date)) return null;84  try {85    const brut = await fs.readFile(86      path.join(RAPPORTS_DIR, `${date}.json`),87      "utf8",88    );89    return JSON.parse(brut) as Rapport;90  } catch {91    return null;92  }93}9495export async function hasPdf(date: string): Promise<boolean> {96  if (!isDateValide(date)) return false;97  try {98    await fs.access(path.join(RAPPORTS_DIR, `${date}.pdf`));99    return true;100  } catch {101    return false;102  }103}104105/** Liste des rapports disponibles, du plus récent au plus ancien. */106export async function listRapports(): Promise<RapportResume[]> {107  let fichiers: string[] = [];108  try {109    fichiers = await fs.readdir(RAPPORTS_DIR);110  } catch {111    return [];112  }113  const dates = fichiers114    .filter((f) => /^\d{4}-\d{2}-\d{2}\.json$/.test(f))115    .map((f) => f.slice(0, 10))116    .sort()117    .reverse();118  const resumes = await Promise.all(119    dates.map(async (date): Promise<RapportResume | null> => {120      const r = await getRapport(date);121      if (!r) return null;122      return {123        date,124        titre: r.contenu.titre,125        sous_titre: r.contenu.sous_titre,126        meteo: r.contenu.meteo,127        chiffre: r.contenu.chiffre_du_jour,128        records_jour: r.snapshot?.totaux?.records_jour ?? 0,129      };130    }),131  );132  return resumes.filter((r): r is RapportResume => r !== null);133}134135export function fmtDateLongue(date: string): string {136  const [y, m, d] = date.split("-").map(Number);137  return new Intl.DateTimeFormat("fr-CA", {138    weekday: "long",139    day: "numeric",140    month: "long",141    year: "numeric",142    timeZone: "America/Toronto",143  }).format(new Date(Date.UTC(y, m - 1, d, 12)));144}145146export function fmtNombre(n: number | null): string {147  if (n === null || n === undefined) return "—";148  return new Intl.NumberFormat("fr-CA").format(n);149}150151export const METEO_INFO: Record<MeteoEtat, { icone: string; label: string }> = {152  beau: { icone: "☀️", label: "Beau fixe" },153  variable: { icone: "⛅", label: "Variable" },154  orage: { icone: "⛈️", label: "Orageux" },155};156