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%
5.1 KB · 162 lines typescript
Raw Blame History
1// Auteur : Simon-Pierre Boucher — contact@spboucher.ai2// src/lib/status.ts — vérification d'état des plateformes + historique3// persistant (data/status/history.jsonl, alimenté par /api/status/tick via4// pm2 cron toutes les 5 min). Sert la page /status (barres d'uptime).5import { promises as fs } from "fs";6import path from "path";7import eco from "@/ka/ecosystem.json";89export type SiteCheck = { up: boolean; code: number | null; ms: number | null };10export type Tick = { ts: number; checks: Record<string, SiteCheck> };1112// Les agents ·Ka (Guardian) — surveillés comme les plateformes, mais listés13// à part : ce ne sont pas des sites d'ecosystem.json (source canonique ka-ui),14// ce sont les agents autonomes qui construisent et réparent l'écosystème.15export type Agent = {16  id: string;17  wordmark: string;18  domain: string;19  accent: string;20  tagline: string;21};2223export const AGENTS: Agent[] = [24  {25    id: "ka2",26    wordmark: "Ka2",27    domain: "www.ka2.bot",28    accent: "#1c5c41",29    tagline: "Agent autonome — Guardian · cartographie du web québécois",30  },31  {32    id: "ka4",33    wordmark: "Ka4",34    domain: "www.ka4.bot",35    accent: "#1c5c41",36    tagline: "Agent autonome — Guardian · cartographie du web québécois",37  },38  {39    id: "ka6",40    wordmark: "Ka6",41    domain: "www.ka6.bot",42    accent: "#1c5c41",43    tagline: "Agent autonome — Guardian · missions connecteurs",44  },45];4647const HISTORY_DIR = path.join(process.cwd(), "data", "status");48const HISTORY_FILE = path.join(HISTORY_DIR, "history.jsonl");49const RETENTION_MS = 90 * 24 * 3600 * 1000;5051export async function checkSite(domain: string): Promise<SiteCheck> {52  const started = Date.now();53  try {54    const res = await fetch(`https://${domain}/`, {55      cache: "no-store",56      redirect: "follow",57      signal: AbortSignal.timeout(9000),58      headers: {59        "user-agent": "GroupeKA-Status/1.0 (+https://www.groupe-ka.com/status)",60      },61    });62    return { up: res.ok, code: res.status, ms: Date.now() - started };63  } catch {64    return { up: false, code: null, ms: null };65  }66}6768export async function checkAll(): Promise<Tick> {69  const targets = [70    ...eco.sites.map((s) => ({ id: s.id, domain: s.domain })),71    ...AGENTS.map((a) => ({ id: a.id, domain: a.domain })),72  ];73  const entries = await Promise.all(74    targets.map(async (t) => [t.id, await checkSite(t.domain)] as const),75  );76  return { ts: Date.now(), checks: Object.fromEntries(entries) };77}7879/** Ajoute un tick à l'historique et purge au-delà de 90 jours. */80export async function appendTick(tick: Tick): Promise<void> {81  await fs.mkdir(HISTORY_DIR, { recursive: true });82  await fs.appendFile(HISTORY_FILE, JSON.stringify(tick) + "\n", "utf8");83  // purge occasionnelle (1 fois sur ~50) pour rester O(1) en régime normal84  if (Math.random() < 0.02) {85    const cutoff = Date.now() - RETENTION_MS;86    const ticks = await readHistory();87    const kept = ticks.filter((t) => t.ts >= cutoff);88    if (kept.length < ticks.length) {89      await fs.writeFile(90        HISTORY_FILE,91        kept.map((t) => JSON.stringify(t)).join("\n") + "\n",92        "utf8",93      );94    }95  }96}9798export async function readHistory(): Promise<Tick[]> {99  try {100    const raw = await fs.readFile(HISTORY_FILE, "utf8");101    return raw102      .split("\n")103      .filter(Boolean)104      .map((l) => {105        try {106          return JSON.parse(l) as Tick;107        } catch {108          return null;109        }110      })111      .filter((t): t is Tick => t !== null);112  } catch {113    return [];114  }115}116117export type Bucket = { state: "up" | "degraded" | "down" | "empty"; label: string };118119/** Découpe l'historique d'un site en `count` intervalles de `stepMs` (le plus récent à droite). */120export function bucketize(121  ticks: Tick[],122  siteId: string,123  count: number,124  stepMs: number,125  now: number,126): Bucket[] {127  const buckets: Bucket[] = [];128  for (let i = count - 1; i >= 0; i--) {129    const end = now - i * stepMs;130    const start = end - stepMs;131    const inRange = ticks.filter((t) => t.ts > start && t.ts <= end);132    const d = new Date(end);133    const label = d.toLocaleString("fr-CA", {134      timeZone: "America/Toronto",135      month: "short",136      day: "numeric",137      hour: "2-digit",138      minute: "2-digit",139    });140    if (!inRange.length) {141      buckets.push({ state: "empty", label: `${label} — aucune mesure` });142      continue;143    }144    const ups = inRange.filter((t) => t.checks[siteId]?.up).length;145    const state = ups === inRange.length ? "up" : ups === 0 ? "down" : "degraded";146    buckets.push({147      state,148      label: `${label} — ${ups}/${inRange.length} vérifications réussies`,149    });150  }151  return buckets;152}153154/** % de disponibilité d'un site sur une fenêtre donnée (null si aucune mesure). */155export function uptimePct(ticks: Tick[], siteId: string, windowMs: number, now: number): number | null {156  const inRange = ticks.filter((t) => t.ts > now - windowMs);157  const measured = inRange.filter((t) => t.checks[siteId] !== undefined);158  if (!measured.length) return null;159  const ups = measured.filter((t) => t.checks[siteId].up).length;160  return (ups / measured.length) * 100;161}162