Skip to content

Commit 88c56e4

Browse files
ferr079claude
andcommitted
feat: dynamic stats via DynNum component + kv-push.sh
Replace hardcoded infrastructure numbers across 12 pages (EN+FR) with live data from Cloudflare KV, pushed every 5min by kv-push.sh on CT 192. - New DynNum.astro component: inline <span data-stat="..."> with fallback - Base.astro hydration script: single fetch('/api/stats') per page - StatsBar extended with optional stat/suffix props - kv-push.sh: 3 new metrics (lxc_count, https_services, ansible_playbooks) from Proxmox API and Semaphore API - Fix stale values: 12→13 playbooks, 21→33 monitored services, 35+→30+ in meta descriptions Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 5f0368a commit 88c56e4

17 files changed

Lines changed: 98 additions & 44 deletions

src/components/DynNum.astro

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
/**
3+
* DynNum — inline dynamic number from /api/stats
4+
*
5+
* Renders fallback in static HTML (SSG/SEO).
6+
* Client-side script in Base.astro fetches /api/stats once
7+
* and updates all [data-stat] elements.
8+
*
9+
* Usage:
10+
* <DynNum stat="lxc_count" fallback="30+" suffix="+" />
11+
* → static: "30+", dynamic: "35+"
12+
*/
13+
interface Props {
14+
stat: string;
15+
fallback: string;
16+
suffix?: string;
17+
}
18+
19+
const { stat, fallback, suffix = '' } = Astro.props;
20+
---
21+
<span data-stat={stat} data-stat-suffix={suffix}>{fallback}</span>

src/components/StatsBar.astro

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
interface Props {
3-
stats: Array<{ number: string; label: string }>;
3+
stats: Array<{ number: string; label: string; stat?: string; suffix?: string }>;
44
}
55
66
const { stats } = Astro.props;
@@ -9,12 +9,14 @@ const { stats } = Astro.props;
99
<section class="stats">
1010
<div class="stats-container">
1111
<div class="stats-grid">
12-
{stats.map((stat, i) => (
12+
{stats.map((s, i) => (
1313
<>
1414
{i > 0 && <div class="stat-sep"></div>}
1515
<div class="stat reveal">
16-
<span class="stat-number" data-target={stat.number}>{stat.number}</span>
17-
<span class="stat-label">{stat.label}</span>
16+
<span class="stat-number" data-target={s.number}
17+
{...(s.stat ? { 'data-stat': s.stat, 'data-stat-suffix': s.suffix || '' } : {})}
18+
>{s.number}</span>
19+
<span class="stat-label">{s.label}</span>
1820
</div>
1921
</>
2022
))}

src/layouts/Base.astro

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,27 @@ const siteUrl = 'https://pixelium.win';
8787
</body>
8888
</html>
8989

90+
<!-- Dynamic stats hydration — updates all [data-stat] elements from /api/stats -->
91+
<script>
92+
(async function hydrateStats() {
93+
const els = document.querySelectorAll('[data-stat]');
94+
if (!els.length) return;
95+
try {
96+
const res = await fetch('/api/stats');
97+
if (!res.ok) return;
98+
const data = await res.json();
99+
if (!data.stats) return;
100+
els.forEach(el => {
101+
const key = (el as HTMLElement).dataset.stat!;
102+
const val = data.stats[key];
103+
if (val == null) return;
104+
const suffix = (el as HTMLElement).dataset.statSuffix || '';
105+
(el as HTMLElement).textContent = val + suffix;
106+
});
107+
} catch (_) { /* graceful — keep fallbacks */ }
108+
})();
109+
</script>
110+
90111
<!-- Scroll reveal - global -->
91112
<script>
92113
const observer = new IntersectionObserver(

src/pages/about.astro

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
---
22
import Base from '../layouts/Base.astro';
3+
import DynNum from '../components/DynNum.astro';
34
45
// Forgejo — fetch commit counts at build time
56
const forgejoRepos = ['homelab-infra', 'homelab-configs', 'ansible-homelab', 'pixelium-site'];
@@ -50,7 +51,7 @@ const skills = [
5051
];
5152
---
5253

53-
<Base title="Stéphane Ferreira — pixelium.win" description="Stéphane Ferreira — DevSecOps engineer, self-hosted infrastructure specialist, pentester (HTB Hacker rank). 35+ services, Proxmox, Ansible, AI agents. Presented by Claude.">
54+
<Base title="Stéphane Ferreira — pixelium.win" description="Stéphane Ferreira — DevSecOps engineer, self-hosted infrastructure specialist, pentester (HTB Hacker rank). 30+ services, Proxmox, Ansible, AI agents. Presented by Claude.">
5455
<main>
5556
<section class="hero-mini">
5657
<div class="container">
@@ -96,19 +97,19 @@ const skills = [
9697
<span class="track-label">ops journal entries</span>
9798
</div>
9899
<div class="track-stat">
99-
<span class="track-number">30+</span>
100+
<span class="track-number"><DynNum stat="lxc_count" fallback="30+" suffix="+" /></span>
100101
<span class="track-label">LXC containers in production</span>
101102
</div>
102103
<div class="track-stat">
103-
<span class="track-number">25+</span>
104+
<span class="track-number"><DynNum stat="https_services" fallback="25+" suffix="+" /></span>
104105
<span class="track-label">services behind HTTPS</span>
105106
</div>
106107
<div class="track-stat">
107108
<span class="track-number">35</span>
108109
<span class="track-label">hosts managed by Ansible</span>
109110
</div>
110111
<div class="track-stat">
111-
<span class="track-number">12</span>
112+
<span class="track-number"><DynNum stat="ansible_playbooks" fallback="14" /></span>
112113
<span class="track-label">Ansible playbooks</span>
113114
</div>
114115
<div class="track-stat">
@@ -201,7 +202,7 @@ const skills = [
201202
</div>
202203
<div class="detail">
203204
<h4>AI agents in production</h4>
204-
<p>OpenFang monitors 21 services autonomously, wakes servers remotely (WOL), and alerts via Telegram — for ~$1.50/month. PentAGI runs autonomous pentests powered by local LLM inference — its first scan found a real misconfiguration. These are real agents in production, not demos.</p>
205+
<p>OpenFang monitors <DynNum stat="services_total" fallback="33" /> services autonomously, wakes servers remotely (WOL), and alerts via Telegram — for ~$1.50/month. PentAGI runs autonomous pentests powered by local LLM inference — its first scan found a real misconfiguration. These are real agents in production, not demos.</p>
205206
</div>
206207
<div class="detail">
207208
<h4>Sovereign local inference</h4>

src/pages/fr/about.astro

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
---
22
import Base from '../../layouts/Base.astro';
3+
import DynNum from '../../components/DynNum.astro';
34
45
// Forgejo — fetch commit counts at build time
56
const forgejoRepos = ['homelab-infra', 'homelab-configs', 'ansible-homelab', 'pixelium-site'];
@@ -48,7 +49,7 @@ const skills = [
4849
];
4950
---
5051

51-
<Base title="Stéphane Ferreira — pixelium.win" description="Stéphane Ferreira — ingénieur DevSecOps, spécialiste infrastructure self-hosted, pentester (HTB Hacker rank). 35+ services, Proxmox, Ansible, agents IA. Présenté par Claude.">
52+
<Base title="Stéphane Ferreira — pixelium.win" description="Stéphane Ferreira — ingénieur DevSecOps, spécialiste infrastructure self-hosted, pentester (HTB Hacker rank). 30+ services, Proxmox, Ansible, agents IA. Présenté par Claude.">
5253
<main>
5354
<section class="hero-mini">
5455
<div class="container">
@@ -94,19 +95,19 @@ const skills = [
9495
<span class="track-label">entrées journal ops</span>
9596
</div>
9697
<div class="track-stat">
97-
<span class="track-number">30+</span>
98+
<span class="track-number"><DynNum stat="lxc_count" fallback="30+" suffix="+" /></span>
9899
<span class="track-label">conteneurs LXC en production</span>
99100
</div>
100101
<div class="track-stat">
101-
<span class="track-number">25+</span>
102+
<span class="track-number"><DynNum stat="https_services" fallback="25+" suffix="+" /></span>
102103
<span class="track-label">services derrière HTTPS</span>
103104
</div>
104105
<div class="track-stat">
105106
<span class="track-number">35</span>
106107
<span class="track-label">hôtes gérés par Ansible</span>
107108
</div>
108109
<div class="track-stat">
109-
<span class="track-number">12</span>
110+
<span class="track-number"><DynNum stat="ansible_playbooks" fallback="14" /></span>
110111
<span class="track-label">playbooks Ansible</span>
111112
</div>
112113
<div class="track-stat">
@@ -199,7 +200,7 @@ const skills = [
199200
</div>
200201
<div class="detail">
201202
<h4>Agents IA en production</h4>
202-
<p>OpenFang surveille 21 services en autonomie, réveille des serveurs à distance (WOL), alerte via Telegram — pour ~$1.50/mois. PentAGI exécute des pentests autonomes propulsés par l'inférence LLM locale — son premier scan a trouvé une vraie faille de config. Ce sont de vrais agents en production, pas des démos.</p>
203+
<p>OpenFang surveille <DynNum stat="services_total" fallback="33" /> services en autonomie, réveille des serveurs à distance (WOL), alerte via Telegram — pour ~$1.50/mois. PentAGI exécute des pentests autonomes propulsés par l'inférence LLM locale — son premier scan a trouvé une vraie faille de config. Ce sont de vrais agents en production, pas des démos.</p>
203204
</div>
204205
<div class="detail">
205206
<h4>Inférence locale souveraine</h4>

src/pages/fr/ia.astro

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
---
22
import Base from '../../layouts/Base.astro';
33
import StatsBar from '../../components/StatsBar.astro';
4+
import DynNum from '../../components/DynNum.astro';
45
---
56

67
<Base title="IA — pixelium.win" description="L'écosystème IA de Stéphane — Ollama local (RTX 3090), MiniMax en production, open source surveillé, vision fine-tuning. Présenté par Claude.">
@@ -109,7 +110,7 @@ import StatsBar from '../../components/StatsBar.astro';
109110
<span class="api-name">MiniMax M2.7</span>
110111
<span class="api-role">Agents AIOps</span>
111112
</div>
112-
<p class="api-desc">Modèle principal d'OpenFang (Guardian AIOps) et fallback disponible pour PentAGI. Excellent ratio perf/coût pour des agents qui tournent en continu. ~$1.50/mois pour la surveillance de 30+ services.</p>
113+
<p class="api-desc">Modèle principal d'OpenFang (Guardian AIOps) et fallback disponible pour PentAGI. Excellent ratio perf/coût pour des agents qui tournent en continu. ~$1.50/mois pour la surveillance de <DynNum stat="services_total" fallback="33" /> services.</p>
113114
<div class="api-tags">
114115
<span>OpenFang</span>
115116
<span>PentAGI fallback</span>

src/pages/fr/index.astro

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ const cards = [
4343
{
4444
icon: '&#9881;',
4545
title: 'IaC, CI/CD & Automatisation',
46-
description: 'Ansible via Semaphore pour le déploiement — 12 playbooks couvrent du hardening SSH au déploiement d\'agents. CI/CD sur Forgejo Runner (Podman). Configs versionnées. Unattended upgrades partout.',
46+
description: 'Ansible via Semaphore pour le déploiement — 14 playbooks couvrent du hardening SSH au déploiement d\'agents. CI/CD sur Forgejo Runner (Podman). Configs versionnées. Unattended upgrades partout.',
4747
tags: ['Ansible', 'Semaphore', 'Forgejo Runner', 'CI/CD'],
4848
},
4949
{
@@ -73,7 +73,7 @@ const cards = [
7373
];
7474
---
7575

76-
<Base title="pixelium.win — Portfolio DevSecOps Homelab" description="Infrastructure de production self-hosted : 35+ services sur Proxmox, automatisation Ansible, agents IA, sécurité en profondeur. Conçu par Stéphane Ferreira, raconté par Claude.">
76+
<Base title="pixelium.win — Portfolio DevSecOps Homelab" description="Infrastructure de production self-hosted : 30+ services sur Proxmox, automatisation Ansible, agents IA, sécurité en profondeur. Conçu par Stéphane Ferreira, raconté par Claude.">
7777
<main>
7878
<!-- Hero -->
7979
<section class="hero">
@@ -97,8 +97,8 @@ const cards = [
9797
<!-- Stats -->
9898
<StatsBar stats={[
9999
{ number: '3', label: 'nœuds Proxmox' },
100-
{ number: '30+', label: 'conteneurs LXC' },
101-
{ number: '20+', label: 'services HTTPS' },
100+
{ number: '30+', label: 'conteneurs LXC', stat: 'lxc_count', suffix: '+' },
101+
{ number: '20+', label: 'services HTTPS', stat: 'https_services', suffix: '+' },
102102
{ number: '0€', label: 'cloud externe' },
103103
]} />
104104

src/pages/fr/infrastructure.astro

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ import Screenshot from '../../components/Screenshot.astro';
44
import Carousel from '../../components/Carousel.astro';
55
import StatsBar from '../../components/StatsBar.astro';
66
import SectionHeading from '../../components/SectionHeading.astro';
7+
import DynNum from '../../components/DynNum.astro';
78
89
const serviceSlides = [
910
{ src: '/images/services/traefik.webp', alt: 'Dashboard Traefik — routers, services, middlewares', title: 'Traefik — Reverse Proxy' },
1011
{ src: '/images/services/authentik.webp', alt: 'Admin Authentik — dashboard SSO avec stats connexion', title: 'Authentik — SSO / IdP' },
1112
{ src: '/images/services/technitium.webp', alt: 'TechnitiumDNS — stats requêtes et top clients', title: 'TechnitiumDNS — Serveur DNS' },
12-
{ src: '/images/services/semaphore.webp', alt: 'Semaphore — 12 templates de tâches Ansible', title: 'Semaphore — UI Ansible' },
13+
{ src: '/images/services/semaphore.webp', alt: 'Semaphore — 14 templates de tâches Ansible', title: 'Semaphore — UI Ansible' },
1314
{ src: '/images/services/netbox.webp', alt: 'NetBox — inventaire IPAM et changelog', title: 'NetBox — IPAM / DCIM' },
1415
{ src: '/images/services/immich.webp', alt: 'Immich — galerie photo avec classification IA', title: 'Immich — Photothèque' },
1516
{ src: '/images/services/bytestash.webp', alt: 'ByteStash — gestionnaire de snippets', title: 'ByteStash — Snippets' },
@@ -34,13 +35,13 @@ const monitoringSlides = [
3435
<div class="container">
3536
<p class="terminal-prompt">$ cat infrastructure.md</p>
3637
<h1>Infrastructure<span class="dot">.</span></h1>
37-
<p class="subtitle">3 noeuds Proxmox, 30+ conteneurs LXC, tout self-hosted. Pas un seul service cloud payant.</p>
38+
<p class="subtitle">3 noeuds Proxmox, <DynNum stat="lxc_count" fallback="30+" suffix="+" /> conteneurs LXC, tout self-hosted. Pas un seul service cloud payant.</p>
3839
</div>
3940
</section>
4041

4142
<StatsBar stats={[
4243
{ number: "3", label: "noeuds Proxmox" },
43-
{ number: "30+", label: "conteneurs LXC" },
44+
{ number: "30+", label: "conteneurs LXC", stat: "lxc_count", suffix: "+" },
4445
{ number: "13", label: "widgets Homepage" },
4546
{ number: "0", label: "euro cloud externe" },
4647
]} />
@@ -160,7 +161,7 @@ const monitoringSlides = [
160161
<div class="tech-card-body">
161162
<p class="tech-why"><strong>Pourquoi :</strong> Hyperviseur libre avec LXC natif — les conteneurs demarrent en 2 secondes et consomment 50 Mo de RAM. PBS integre pour les backups. API complete.</p>
162163
<p class="tech-alt"><span class="alt-label">Ecartes :</span> ESXi (payant depuis 2024), Hyper-V (Windows only), XCP-ng (moins de communaute)</p>
163-
<p class="tech-result"><span class="result-label">Resultat :</span> 3 noeuds heterogenes, 30+ CTs, backups incrementaux via PBS</p>
164+
<p class="tech-result"><span class="result-label">Resultat :</span> 3 noeuds heterogenes, <DynNum stat="lxc_count" fallback="30+" suffix="+" /> CTs, backups incrementaux via PBS</p>
164165
</div>
165166
</div>
166167

@@ -172,7 +173,7 @@ const monitoringSlides = [
172173
<div class="tech-card-body">
173174
<p class="tech-why"><strong>Pourquoi :</strong> Config dynamique en YAML rechargee a chaud — j'ajoute un service HTTPS en deposant un fichier dans <code>conf.d/</code>, sans restart. ACME natif avec step-ca.</p>
174175
<p class="tech-alt"><span class="alt-label">Ecartes :</span> Nginx Proxy Manager (UI-only, pas IaC), Caddy (moins d'integrations reverse proxy)</p>
175-
<p class="tech-result"><span class="result-label">Resultat :</span> 25+ services HTTPS, certificats auto-renouveles, zero intervention manuelle</p>
176+
<p class="tech-result"><span class="result-label">Resultat :</span> <DynNum stat="https_services" fallback="25+" suffix="+" /> services HTTPS, certificats auto-renouveles, zero intervention manuelle</p>
176177
</div>
177178
</div>
178179

@@ -220,7 +221,7 @@ const monitoringSlides = [
220221
<div class="tech-card-body">
221222
<p class="tech-why"><strong>Pourquoi :</strong> Agentless — SSH suffit, pas de daemon a installer sur 30+ CTs. Idempotent — je relance un playbook sans risque. Semaphore ajoute une UI web pour les lancements en 1 clic.</p>
222223
<p class="tech-alt"><span class="alt-label">Ecartes :</span> Puppet/Chef (agents sur chaque hote), Terraform (provisioning, pas config management)</p>
223-
<p class="tech-result"><span class="result-label">Resultat :</span> 12 playbooks operationnels, deploiement d'agent Wazuh/Beszel en 1 commande</p>
224+
<p class="tech-result"><span class="result-label">Resultat :</span> <DynNum stat="ansible_playbooks" fallback="14" /> playbooks operationnels, deploiement d'agent Wazuh/Beszel en 1 commande</p>
224225
</div>
225226
</div>
226227

src/pages/fr/projets.astro

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
---
22
import Base from '../../layouts/Base.astro';
3+
import DynNum from '../../components/DynNum.astro';
34
---
45

56
<Base title="Projets — pixelium.win" description="12 projets en production : SSO Authentik, monitoring IA (OpenFang), pentest autonome (PentAGI), veille RSS, observabilité, sécurité en profondeur, CI/CD. Vrais ops, vrai debugging.">
@@ -69,7 +70,7 @@ import Base from '../../layouts/Base.astro';
6970
</div>
7071
<div class="detail-block">
7172
<h4>Le contournement shell_exec</h4>
72-
<p>Le sanitizer d'OpenFang bloque les accolades <code>{}</code>, les pipes et les semicolons dans les commandes shell. Impossible d'exécuter du LogQL ou du PromQL directement. Solution : 3 wrappers CLI dédiés — <code>http-check</code> (21 services), <code>vm-query</code> (VictoriaMetrics), <code>pve-status</code> (Proxmox) — qui encapsulent la complexité et exposent une interface propre.</p>
73+
<p>Le sanitizer d'OpenFang bloque les accolades <code>{}</code>, les pipes et les semicolons dans les commandes shell. Impossible d'exécuter du LogQL ou du PromQL directement. Solution : 3 wrappers CLI dédiés — <code>http-check</code> (<DynNum stat="services_total" fallback="33" /> services), <code>vm-query</code> (VictoriaMetrics), <code>pve-status</code> (Proxmox) — qui encapsulent la complexité et exposent une interface propre.</p>
7374
</div>
7475
<div class="detail-block">
7576
<h4>WOL pve3 — l'agent qui allume les machines</h4>
@@ -85,7 +86,7 @@ import Base from '../../layouts/Base.astro';
8586
<li>VictoriaMetrics</li>
8687
<li>Loki</li>
8788
</ul>
88-
<span class="project-services">~0,05€/jour — 4 cron jobs, 2 agents, 21 services monitorés, alertes Telegram</span>
89+
<span class="project-services">~0,05€/jour — 4 cron jobs, 2 agents, <DynNum stat="services_total" fallback="33" /> services monitorés, alertes Telegram</span>
8990
</div>
9091
</div>
9192
</div>

src/pages/fr/securite.astro

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
---
22
import Base from '../../layouts/Base.astro';
33
import StatsBar from '../../components/StatsBar.astro';
4+
import DynNum from '../../components/DynNum.astro';
45
---
56

67
<Base title="Sécurité — pixelium.win" description="Defense-in-depth sur 30+ services self-hosted : PKI interne, SSO, IPS, SIEM, VPN mesh, YubiKey FIDO2 — par Claude, l'IA qui a déployé chaque couche.">
@@ -91,7 +92,7 @@ import StatsBar from '../../components/StatsBar.astro';
9192
</div>
9293
<div class="spec">
9394
<span class="spec-label">Résultat</span>
94-
<span class="spec-value">25+ services en HTTPS valide, cadenas vert, zéro avertissement navigateur</span>
95+
<span class="spec-value"><DynNum stat="https_services" fallback="25+" suffix="+" /> services en HTTPS valide, cadenas vert, zéro avertissement navigateur</span>
9596
</div>
9697
</div>
9798
</div>

0 commit comments

Comments
 (0)