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%

statut : les 3 agents ·Ka (ka2.bot, ka4.bot, ka6.bot) surveillés sur /status

- src/lib/status.ts : liste AGENTS (hors ecosystem.json — les agents ne sont
  pas des plateformes) incluse dans checkAll() : les ticks aux 5 min couvrent
  maintenant 16 cibles
- /status : carte factorisée en StatusCard, section « Les agents ·Ka »
  (Guardian) avec les mêmes barres 24 h / 90 j et uptimes 24 h / 7 j / 30 j ;
  le compte des plateformes en ligne ne mélange plus agents et sites

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simon-Pierre Boucher committed 1 mo ago (Aug 23, 2026) parent cca17d0

2 changed files +143 −68

modified src/app/status/page.tsx +103 −67
@@ -5,6 +5,7 @@
5 5 import type { Metadata } from "next";
6 6 import eco from "@/ka/ecosystem.json";
7 7 import {
8 + AGENTS,
8 9 checkAll,
9 10 readHistory,
10 11 bucketize,
@@ -64,19 +65,94 @@ function Pct({ value }: { value: number | null }) {
64 65 return <span className={`font-bold ${cls}`}>{value.toFixed(value === 100 ? 0 : 2)} %</span>;
65 66 }
66 67
68 +const DAY = 24 * 3600 * 1000;
69 +
70 +function StatusCard({
71 + s,
72 + c,
73 + history,
74 + now,
75 +}: {
76 + s: { id: string; wordmark: string; domain: string; accent: string };
77 + c: SiteCheck | undefined;
78 + history: Tick[];
79 + now: number;
80 +}) {
81 + const up24 = uptimePct(history, s.id, DAY, now);
82 + const up7 = uptimePct(history, s.id, 7 * DAY, now);
83 + const up30 = uptimePct(history, s.id, 30 * DAY, now);
84 + return (
85 + <article className="gk-card p-5 sm:p-6">
86 + <div className="flex flex-wrap items-center justify-between gap-x-4 gap-y-2">
87 + <span className="flex min-w-0 items-center gap-2">
88 + <span
89 + aria-hidden="true"
90 + className="inline-block h-[10px] w-[10px] flex-none rounded-[3px] border border-ink"
91 + style={{ background: s.accent }}
92 + />
93 + <span className="gk-display truncate text-[17px] font-bold tracking-[-0.02em]">
94 + {s.wordmark}
95 + </span>
96 + <a
97 + href={`https://${s.domain}`}
98 + target="_blank"
99 + rel="noopener noreferrer"
100 + className="gk-mono hidden text-[11px] font-bold text-ink-3 underline-offset-4 hover:text-ink hover:underline sm:inline"
101 + >
102 + {s.domain}
103 + </a>
104 + </span>
105 + <span className="flex items-center gap-3">
106 + <span className="gk-mono text-[11px] text-ink-3">
107 + {c?.up ? `HTTP ${c.code} · ${c.ms} ms` : c?.code ? `HTTP ${c.code}` : "délai dépassé"}
108 + </span>
109 + {c?.up ? (
110 + <span className="gk-mono inline-flex items-center gap-[6px] rounded-full border-[1.5px] border-ink bg-lime px-3 py-[3px] text-[10px] font-bold tracking-[0.08em] text-ink uppercase">
111 + <span className="pulse-dot" aria-hidden="true" />
112 + En ligne
113 + </span>
114 + ) : (
115 + <span className="gk-mono inline-flex items-center rounded-full border-[1.5px] border-ink bg-[#fbe9e7] px-3 py-[3px] text-[10px] font-bold tracking-[0.08em] text-[#b3423a] uppercase">
116 + Hors ligne
117 + </span>
118 + )}
119 + </span>
120 + </div>
121 +
122 + <div className="mt-4">
123 + <p className="klabel">Dernières 24 heures · 1 barre = 15 min</p>
124 + <div className="mt-2">
125 + <Strip ticks={history} siteId={s.id} count={96} stepMs={15 * 60 * 1000} now={now} />
126 + </div>
127 + </div>
128 + <div className="mt-4">
129 + <p className="klabel">90 derniers jours · 1 barre = 1 jour</p>
130 + <div className="mt-2">
131 + <Strip ticks={history} siteId={s.id} count={90} stepMs={DAY} now={now} />
132 + </div>
133 + </div>
134 +
135 + <div className="gk-mono mt-4 flex flex-wrap gap-x-7 gap-y-2 border-t border-dashed border-[rgba(20,24,20,0.25)] pt-3 text-[11.5px] text-ink-2">
136 + <span>Uptime 24 h : <Pct value={up24} /></span>
137 + <span>7 jours : <Pct value={up7} /></span>
138 + <span>30 jours : <Pct value={up30} /></span>
139 + </div>
140 + </article>
141 + );
142 +}
143 +
67 144 export default async function StatusPage() {
68 145 const now = Date.now();
69 146 const [live, history] = await Promise.all([checkAll(), readHistory()]);
70 − const upCount = Object.values(live.checks).filter((c) => c.up).length;
147 + const upCount = eco.sites.filter((s) => live.checks[s.id]?.up).length;
71 148 const allUp = upCount === eco.sites.length;
149 + const agentsUp = AGENTS.filter((a) => live.checks[a.id]?.up).length;
72 150 const firstTick = history.length ? history[0].ts : null;
73 151 const when = new Date(now).toLocaleString("fr-CA", {
74 152 timeZone: "America/Toronto",
75 153 dateStyle: "long",
76 154 timeStyle: "medium",
77 155 });
78 − const DAY = 24 * 3600 * 1000;
79 −
80 156 return (
81 157 <main className="mx-auto max-w-6xl px-4 py-12 sm:px-6">
82 158 <p className="kicker">État des services · direct + historique</p>
@@ -117,72 +193,32 @@ export default async function StatusPage() {
117 193 </div>
118 194
119 195 <div className="mt-6 space-y-5">
120 − {eco.sites.map((s) => {
121 − const c: SiteCheck | undefined = live.checks[s.id];
122 − const up24 = uptimePct(history, s.id, DAY, now);
123 − const up7 = uptimePct(history, s.id, 7 * DAY, now);
124 − const up30 = uptimePct(history, s.id, 30 * DAY, now);
125 − return (
126 − <article key={s.id} className="gk-card p-5 sm:p-6">
127 − <div className="flex flex-wrap items-center justify-between gap-x-4 gap-y-2">
128 − <span className="flex min-w-0 items-center gap-2">
129 − <span
130 − aria-hidden="true"
131 − className="inline-block h-[10px] w-[10px] flex-none rounded-[3px] border border-ink"
132 − style={{ background: s.accent }}
133 − />
134 − <span className="gk-display truncate text-[17px] font-bold tracking-[-0.02em]">
135 − {s.wordmark}
136 − </span>
137 − <a
138 − href={`https://${s.domain}`}
139 − target="_blank"
140 − rel="noopener noreferrer"
141 − className="gk-mono hidden text-[11px] font-bold text-ink-3 underline-offset-4 hover:text-ink hover:underline sm:inline"
142 − >
143 − {s.domain}
144 − </a>
145 − </span>
146 − <span className="flex items-center gap-3">
147 − <span className="gk-mono text-[11px] text-ink-3">
148 − {c?.up ? `HTTP ${c.code} · ${c.ms} ms` : c?.code ? `HTTP ${c.code}` : "délai dépassé"}
149 − </span>
150 − {c?.up ? (
151 − <span className="gk-mono inline-flex items-center gap-[6px] rounded-full border-[1.5px] border-ink bg-lime px-3 py-[3px] text-[10px] font-bold tracking-[0.08em] text-ink uppercase">
152 − <span className="pulse-dot" aria-hidden="true" />
153 − En ligne
154 − </span>
155 − ) : (
156 − <span className="gk-mono inline-flex items-center rounded-full border-[1.5px] border-ink bg-[#fbe9e7] px-3 py-[3px] text-[10px] font-bold tracking-[0.08em] text-[#b3423a] uppercase">
157 − Hors ligne
158 − </span>
159 − )}
160 − </span>
161 − </div>
162 −
163 − <div className="mt-4">
164 − <p className="klabel">Dernières 24 heures · 1 barre = 15 min</p>
165 − <div className="mt-2">
166 − <Strip ticks={history} siteId={s.id} count={96} stepMs={15 * 60 * 1000} now={now} />
167 − </div>
168 − </div>
169 − <div className="mt-4">
170 − <p className="klabel">90 derniers jours · 1 barre = 1 jour</p>
171 − <div className="mt-2">
172 − <Strip ticks={history} siteId={s.id} count={90} stepMs={DAY} now={now} />
173 − </div>
174 − </div>
175 −
176 − <div className="gk-mono mt-4 flex flex-wrap gap-x-7 gap-y-2 border-t border-dashed border-[rgba(20,24,20,0.25)] pt-3 text-[11.5px] text-ink-2">
177 − <span>Uptime 24 h : <Pct value={up24} /></span>
178 − <span>7 jours : <Pct value={up7} /></span>
179 − <span>30 jours : <Pct value={up30} /></span>
180 − </div>
181 − </article>
182 − );
183 − })}
196 + {eco.sites.map((s) => (
197 + <StatusCard key={s.id} s={s} c={live.checks[s.id]} history={history} now={now} />
198 + ))}
184 199 </div>
185 200
201 + <section className="mt-14" id="agents">
202 + <p className="kicker">Les agents ·Ka · Guardian</p>
203 + <h2 className="gk-display mt-2 text-[clamp(22px,3vw,32px)] font-bold tracking-[-0.02em]">
204 + Les {AGENTS.length} agents,{" "}
205 + <span className="hl">
206 + {agentsUp === AGENTS.length ? "tous en ligne" : `${agentsUp}/${AGENTS.length} en ligne`}
207 + </span>
208 + </h2>
209 + <p className="mt-3 max-w-xl text-[13.5px] text-ink-2">
210 + Ka2, Ka4 et Ka6 sont les agents autonomes du groupe : ils
211 + cartographient le web québécois, construisent des connecteurs et
212 + réparent les plateformes en mission. Chacun expose son tableau de
213 + bord Guardian — surveillé ici aux 5 minutes, comme les plateformes.
214 + </p>
215 + <div className="mt-6 space-y-5">
216 + {AGENTS.map((a) => (
217 + <StatusCard key={a.id} s={a} c={live.checks[a.id]} history={history} now={now} />
218 + ))}
219 + </div>
220 + </section>
221 +
186 222 <p className="gk-mono mt-8 text-[11px] leading-relaxed text-ink-3">
187 223 Méthode : requête GET sur la page d&apos;accueil de chaque domaine
188 224 (délai maximal 9 s) toutes les 5 minutes depuis le serveur du hub, plus
modified src/lib/status.ts +40 −1
@@ -9,6 +9,41 @@ import eco from "@/ka/ecosystem.json";
9 9 export type SiteCheck = { up: boolean; code: number | null; ms: number | null };
10 10 export type Tick = { ts: number; checks: Record<string, SiteCheck> };
11 11
12 +// Les agents ·Ka (Guardian) — surveillés comme les plateformes, mais listés
13 +// à 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.
15 +export type Agent = {
16 + id: string;
17 + wordmark: string;
18 + domain: string;
19 + accent: string;
20 + tagline: string;
21 +};
22 +
23 +export 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 +];
46 +
12 47 const HISTORY_DIR = path.join(process.cwd(), "data", "status");
13 48 const HISTORY_FILE = path.join(HISTORY_DIR, "history.jsonl");
14 49 const RETENTION_MS = 90 * 24 * 3600 * 1000;
@@ -31,8 +66,12 @@ export async function checkSite(domain: string): Promise<SiteCheck> {
31 66 }
32 67
33 68 export 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 + ];
34 73 const entries = await Promise.all(
35 − eco.sites.map(async (s) => [s.id, await checkSite(s.domain)] as const),
74 + targets.map(async (t) => [t.id, await checkSite(t.domain)] as const),
36 75 );
37 76 return { ts: Date.now(), checks: Object.fromEntries(entries) };
38 77 }
39 78