Groupe KA — site du holding + KA ID (compte unique & SSO des 7 plateformes). Next.js 16, SQLite, Google & Apple login.
TypeScript 70.4%
HTML 18.4%
JavaScript 4%
Python 3.8%
CSS 3.4%
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