| 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 |
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 |
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'accueil de chaque domaine |
| 188 |
224 |
(délai maximal 9 s) toutes les 5 minutes depuis le serveur du hub, plus |