Files
infra/portfolio/index.html
T
qpismont ede28f6d12 Extend monitoring and update portfolio
Add hostmetrics receiver and OTel Collector telemetry, and move
Alertmanager and Blackbox scraping to the collector. Raise collector
memory limits, restrict Blackbox to HTTP 200, and add collector and
Caddy down alerts with inhibition. Update portfolio title and copy.
2026-08-05 19:49:18 +00:00

585 lines
21 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Portfolio of Quentin Pismont, senior/lead platform engineer: distributed systems, observability, security and self-hosted CI/CD, tested in production.">
<title>Quentin Pismont — Senior/Lead Platform Engineer</title>
<!-- Open Graph -->
<meta property="og:type" content="website">
<meta property="og:title" content="Quentin Pismont — Senior/Lead Platform Engineer">
<meta property="og:description" content="Self-hosted infrastructure running in production, a security-first AI agent in Rust, and a distributed media pipeline. Source code available.">
<style>
:root {
--bg: #0d1117;
--bg-soft: #161b22;
--bg-card: #161b22;
--border: #21262d;
--border-hover: #30363d;
--text: #e6edf3;
--text-dim: #8b949e;
--text-faint: #6e7681;
--accent: #58a6ff;
--accent-soft: rgba(88, 166, 255, 0.1);
--green: #3fb950;
--mono: 'SF Mono', 'JetBrains Mono', 'Fira Code', ui-monospace, monospace;
--sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
background: var(--bg);
color: var(--text);
font-family: var(--sans);
line-height: 1.65;
font-size: 16px;
-webkit-font-smoothing: antialiased;
}
.wrap {
max-width: 860px;
margin: 0 auto;
padding: 0 24px;
}
/* ---------- NAV ---------- */
nav {
position: sticky;
top: 0;
z-index: 100;
background: rgba(13, 17, 23, 0.8);
backdrop-filter: blur(12px);
border-bottom: 1px solid var(--border);
}
nav .wrap {
display: flex;
justify-content: space-between;
align-items: center;
height: 60px;
}
nav .logo {
font-family: var(--mono);
font-weight: 600;
font-size: 15px;
color: var(--text);
}
nav .logo span { color: var(--accent); }
nav .links { display: flex; gap: 28px; }
nav a {
color: var(--text-dim);
text-decoration: none;
font-size: 14px;
transition: color 0.2s;
}
nav a:hover { color: var(--text); }
@media (max-width: 620px) { nav .links { display: none; } }
/* ---------- HERO ---------- */
header {
padding: 100px 0 80px;
border-bottom: 1px solid var(--border);
}
.badge {
display: inline-flex;
align-items: center;
gap: 8px;
font-family: var(--mono);
font-size: 13px;
color: var(--green);
background: rgba(63, 185, 80, 0.1);
border: 1px solid rgba(63, 185, 80, 0.2);
padding: 5px 12px;
border-radius: 20px;
margin-bottom: 28px;
}
.badge .dot {
width: 7px; height: 7px;
border-radius: 50%;
background: var(--green);
box-shadow: 0 0 8px var(--green);
}
header h1 {
font-size: clamp(2rem, 5vw, 3rem);
line-height: 1.15;
letter-spacing: -0.02em;
margin-bottom: 20px;
font-weight: 700;
}
header h1 .grad {
background: linear-gradient(120deg, #58a6ff, #a371f7);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
header p.lead {
font-size: 1.15rem;
color: var(--text-dim);
max-width: 640px;
margin-bottom: 32px;
}
.cta {
display: flex;
gap: 14px;
flex-wrap: wrap;
}
.btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 20px;
border-radius: 8px;
text-decoration: none;
font-size: 14px;
font-weight: 500;
transition: all 0.2s;
border: 1px solid transparent;
}
.btn-primary {
background: var(--accent);
color: #0d1117;
}
.btn-primary:hover { background: #79b8ff; transform: translateY(-1px); }
.btn-ghost {
background: transparent;
color: var(--text);
border-color: var(--border-hover);
}
.btn-ghost:hover { border-color: var(--accent); color: var(--accent); }
/* ---------- SECTIONS ---------- */
section { padding: 80px 0; border-bottom: 1px solid var(--border); }
.section-head {
display: flex;
align-items: baseline;
gap: 12px;
margin-bottom: 40px;
}
.section-head .num {
font-family: var(--mono);
font-size: 14px;
color: var(--accent);
}
.section-head h2 {
font-size: 1.6rem;
letter-spacing: -0.01em;
font-weight: 600;
}
.section-intro {
color: var(--text-dim);
max-width: 640px;
margin: -24px 0 40px;
}
/* ---------- ABOUT ---------- */
.about-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 40px;
}
@media (max-width: 620px) { .about-grid { grid-template-columns: 1fr; gap: 28px; } }
.about-grid p { color: var(--text-dim); margin-bottom: 16px; }
.about-grid p strong { color: var(--text); font-weight: 500; }
.principles { list-style: none; }
.principles li {
padding: 10px 0 10px 24px;
position: relative;
color: var(--text-dim);
border-bottom: 1px solid var(--border);
}
.principles li:last-child { border-bottom: none; }
.principles li::before {
content: "→";
position: absolute;
left: 0;
color: var(--accent);
}
/* ---------- PROJECTS ---------- */
.project {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 14px;
padding: 32px;
margin-bottom: 24px;
transition: border-color 0.25s, transform 0.25s;
}
.project:hover {
border-color: var(--border-hover);
transform: translateY(-2px);
}
.project-top {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 16px;
margin-bottom: 8px;
flex-wrap: wrap;
}
.project h3 {
font-size: 1.3rem;
font-weight: 600;
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.project .tag {
font-family: var(--mono);
font-size: 11px;
padding: 3px 10px;
border-radius: 20px;
background: var(--accent-soft);
color: var(--accent);
border: 1px solid rgba(88,166,255,0.2);
white-space: nowrap;
}
.tag.wip { background: rgba(210,153,34,0.1); color: #d29922; border-color: rgba(210,153,34,0.2); }
.tag.done { background: rgba(63,185,80,0.1); color: var(--green); border-color: rgba(63,185,80,0.2); }
.project .subtitle {
color: var(--text-dim);
font-size: 0.98rem;
margin-bottom: 24px;
}
.block { margin-bottom: 20px; }
.block-label {
font-family: var(--mono);
font-size: 12px;
color: var(--text-faint);
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 8px;
}
.block p, .block ul { color: var(--text-dim); font-size: 0.95rem; }
.block ul { list-style: none; }
.block ul li {
padding: 4px 0 4px 20px;
position: relative;
}
.block ul li::before {
content: "▹";
position: absolute;
left: 0;
color: var(--accent);
font-size: 0.85em;
}
.block code {
font-family: var(--mono);
font-size: 0.85em;
background: var(--bg-soft);
padding: 2px 6px;
border-radius: 4px;
color: var(--accent);
border: 1px solid var(--border);
}
.stack {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 24px;
padding-top: 20px;
border-top: 1px solid var(--border);
}
.stack span {
font-family: var(--mono);
font-size: 12px;
color: var(--text-dim);
background: var(--bg-soft);
padding: 4px 10px;
border-radius: 6px;
border: 1px solid var(--border);
}
/* ---------- SKILLS ---------- */
.skills-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 20px;
}
.skill-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 12px;
padding: 22px;
}
.skill-card h4 {
font-size: 0.95rem;
margin-bottom: 12px;
color: var(--text);
display: flex;
align-items: center;
gap: 8px;
}
.skill-card h4 .ic { color: var(--accent); }
.skill-card p { color: var(--text-dim); font-size: 0.9rem; }
/* ---------- CONTACT ---------- */
#contact { text-align: center; border-bottom: none; }
#contact h2 { justify-content: center; }
#contact .section-head { justify-content: center; }
#contact p { color: var(--text-dim); max-width: 500px; margin: 0 auto 32px; }
.contact-links { display: flex; gap: 14px; justify-content: center; flex-wrap: wrap; }
/* ---------- FOOTER ---------- */
footer {
padding: 40px 0;
text-align: center;
color: var(--text-faint);
font-size: 13px;
font-family: var(--mono);
border-top: 1px solid var(--border);
}
/* Reveal animation */
.reveal {
opacity: 0;
transform: translateY(20px);
transition: opacity 0.6s ease, transform 0.6s ease;
}
.reveal.visible { opacity: 1; transform: translateY(0); }
/* Accessibility: no animation if the user prefers reduced motion */
@media (prefers-reduced-motion: reduce) {
html { scroll-behavior: auto; }
.reveal { opacity: 1; transform: none; transition: none; }
.project:hover, .btn-primary:hover { transform: none; }
}
</style>
</head>
<body>
<nav>
<div class="wrap">
<div class="logo">~/<span>quentin_pismont</span></div>
<div class="links">
<a href="#about">About</a>
<a href="#projects">Projects</a>
<a href="#skills">Skills</a>
<a href="#contact">Contact</a>
</div>
</div>
</nav>
<header>
<div class="wrap">
<div class="badge"><span class="dot"></span> Open to opportunities</div>
<h1>I design systems<br><span class="grad">end to end</span>, built to hold up in production.</h1>
<p class="lead">
Platform / infrastructure engineer. From code to production, through CI/CD,
observability and security — an integrated ecosystem designed to be operated
over the long run.
</p>
<div class="cta">
<a href="#projects" class="btn btn-primary">View projects →</a>
<a href="https://gitea.qpismont.fr/explore/repos" target="_blank" class="btn btn-ghost">Gitea / Source code</a>
</div>
</div>
</header>
<!-- ABOUT -->
<section id="about">
<div class="wrap">
<div class="section-head reveal">
<span class="num">01</span>
<h2>About</h2>
</div>
<div class="about-grid reveal">
<div>
<p>
Working as an engineer for <strong>10 years</strong>. My specialty: designing coherent
distributed systems and keeping them operable over time.
</p>
<p>
What I show here is infrastructure
<strong>actually running in production</strong> for my daily use, <strong>tested under failure</strong>,
with full observability and an end-to-end CI/CD chain.
</p>
<p>
On the people side, I <strong>mentored two interns from the ground up</strong> on our stack —
both were hired at the end of their internship. Helping others level up
is part of how I approach the craft.
</p>
<p>
My areas of focus: <strong>Rust</strong>, container orchestration,
security / isolation, networking, and self-hosted observability.
</p>
</div>
<div>
<div class="block-label">Principles</div>
<ul class="principles">
<li>If it hasn't been tested under failure, it isn't resilient.</li>
<li>A technical choice is justified by its tradeoffs, not by trends.</li>
<li>Migrate for value, never for dogma.</li>
<li>Security is designed starting from the threat model.</li>
<li>A system must be observable before it's scaled.</li>
</ul>
</div>
</div>
</div>
</section>
<!-- PROJECTS -->
<section id="projects">
<div class="wrap">
<div class="section-head reveal">
<span class="num">02</span>
<h2>Selected projects</h2>
</div>
<p class="section-intro reveal">
Three pieces that, together, cover the essentials of a platform profile:
operating a system, designing a secure architecture, and thinking about scale.
</p>
<!-- PROJECT 1 : INFRA -->
<div class="project reveal">
<div class="project-top">
<h3>🏗️ Self-hosted infrastructure <span class="tag done">Complete &amp; tested</span></h3>
</div>
<p class="subtitle">A complete ecosystem — code → CI → production → observability — on bare metal, validated by real failure tests.</p>
<div class="block">
<div class="block-label">The problem</div>
<p>Having a reliable personal platform to host and operate my own services, with the standards of real production: resilience, alerting, zero data loss.</p>
</div>
<div class="block">
<div class="block-label">The architecture</div>
<ul>
<li><strong>Networking</strong> — WireGuard (the only entry point), Docker overlay, and <code>CoreDNS</code> for <code>*.internal</code> resolution with fallback to Docker's DNS.</li>
<li><strong>CI/CD</strong> — Gitea + Woodpecker: lint → tests → build → automatic push registry on tag/release → deploy on prod.</li>
<li><strong>Observability</strong> — OtelCollector (central hub), Prometheus (storage + alerting), Loki (logs) + Tempo (traces), Perses dashboards, Alertmanager → ntfy.</li>
<li><strong>Safety net</strong> — Blackbox scraped directly by Prometheus: monitoring survives a Collector failure.</li>
</ul>
</div>
<div class="block">
<div class="block-label">What sets it apart — validated resilience</div>
<ul>
<li>Gitea outage → ntfy alert received ✓</li>
<li>OtelCollector outage → alert received, monitoring maintained ✓</li>
<li>Multiple redeployments → zero data loss (persistent volumes) ✓</li>
</ul>
</div>
<div class="stack">
<span>Docker Swarm</span><span>WireGuard</span><span>CoreDNS</span><span>Gitea</span><span>Woodpecker CI</span><span>OpenTelemetry</span><span>Prometheus</span><span>Loki</span><span>Tempo</span><span>Perses</span><span>Alertmanager</span>
</div>
</div>
<!-- PROJECT 2 : HERALD -->
<div class="project reveal">
<div class="project-top">
<h3>🤖 Herald — AI agent for Gitea <span class="tag wip">In active development</span></h3>
</div>
<p class="subtitle">A code-review and AI-assistance bot for Gitea, written in Rust, with a <strong>security-first</strong> agentic architecture.</p>
<div class="block">
<div class="block-label">The problem</div>
<p>No AI assistance solution exists for Gitea. A real need: solo code review, implementing tests/features, usable day to day — including from a phone.</p>
</div>
<div class="block">
<div class="block-label">The security model</div>
<p>The challenge of an agent that executes code isn't making it run, but making it run <strong>without risk of exfiltration or compromise</strong>. The decisions:</p>
<ul>
<li>Agent loop on the host, <strong>side effects isolated in a container</strong></li>
<li><strong>Git handled exclusively by the host</strong> (Gitea token): no secret ever touches the sandbox.</li>
<li><code>.git</code> <strong>"disarmed"</strong> for reading (remotes removed, credentials cleared, hooks emptied) — on a copy, with the armed original staying on the host.</li>
<li><strong>Network cut during the AI phase</strong> (disconnect via bollard): exfiltration is impossible. Network active only for <code>postCreate</code>.</li>
<li>Persistence from the start (resume after stop).</li>
</ul>
</div>
<div class="block">
<div class="block-label">Roadmap (intentional semver)</div>
<ul>
<li><strong>1.0</strong> — Review + comments posted to the right line on the MR</li>
<li><strong>1.1</strong> — The 3 observability signals</li>
<li><strong>1.2</strong> — Sandboxing begins (read-only first)</li>
<li><strong>2.0</strong> — Feature creation (write / execute)</li>
<li><strong>2.x</strong> — Advanced persistence + web config dashboard</li>
</ul>
</div>
<div class="stack">
<span>Rust</span><span>Tokio</span><span>bollard</span><span>DevContainers</span><span>OpenRouter</span><span>OpenTelemetry</span>
</div>
</div>
<!-- PROJECT 3 : PIPELINE -->
<div class="project reveal">
<div class="project-top">
<h3>⚡ Distributed media pipeline <span class="tag wip">Design / upcoming</span></h3>
</div>
<p class="subtitle">An extended imgproxy-style media processing pipeline, designed for horizontal scaling and streaming.</p>
<div class="block">
<div class="block-label">The problem</div>
<p>Process media (images, streaming) compositionally, with an architecture able to scale out horizontally — and serve as a real load-testing playground.</p>
</div>
<div class="block">
<div class="block-label">The architecture</div>
<ul>
<li>Compositional API: a <strong>core</strong> + <strong>runners</strong> communicating over <code>NATS</code>.</li>
<li>Multi-level cache, horizontal scaling of runners.</li>
<li><strong>Claim-check pattern</strong>: files transit through an object store (S3/Minio); NATS carries only commands and metadata.</li>
</ul>
</div>
<div class="block">
<div class="block-label">The technical goal</div>
<p>An ideal candidate for <strong>load tests (k6) instrumented by my observability stack</strong> — validating system behavior at scale and documenting the breaking points.</p>
</div>
<div class="stack">
<span>Rust</span><span>Tokio</span><span>NATS</span><span>S3 / Minio</span><span>k6</span><span>OpenTelemetry</span><span>Docker Swarm</span>
</div>
</div>
</div>
</section>
<!-- SKILLS -->
<section id="skills">
<div class="wrap">
<div class="section-head reveal">
<span class="num">03</span>
<h2>Skills</h2>
</div>
<div class="skills-grid">
<div class="skill-card reveal">
<h4><span class="ic"></span> Languages</h4>
<p>Rust (async / Tokio), TypeScript / Node. Looking for the right tool for the right problem.</p>
</div>
<div class="skill-card reveal">
<h4><span class="ic"></span> Infrastructure &amp; Ops</h4>
<p>Docker Swarm, bare metal, Fedora Bootc (immutable OS), CI/CD (Gitea, Woodpecker).</p>
</div>
<div class="skill-card reveal">
<h4><span class="ic"></span> Observability</h4>
<p>OpenTelemetry, Prometheus, Loki, Tempo, Perses, Alertmanager. Full stack, tested under failure.</p>
</div>
<div class="skill-card reveal">
<h4><span class="ic"></span> Networking</h4>
<p>WireGuard, CoreDNS, overlay networks, Caddy. Designing self-hosted private networks.</p>
</div>
<div class="skill-card reveal">
<h4><span class="ic"></span> Security &amp; Isolation</h4>
<p>Container sandboxing, privilege separation (DBus / Polkit), threat modeling.</p>
</div>
<div class="skill-card reveal">
<h4><span class="ic"></span> Distributed &amp; Messaging</h4>
<p>NATS, distributed architectures, claim-check patterns, horizontal scaling.</p>
</div>
</div>
</div>
</section>
<!-- CONTACT -->
<section id="contact">
<div class="wrap">
<div class="section-head reveal">
<span class="num">04</span>
<h2>Contact</h2>
</div>
<p class="reveal">
Interested in platform / infrastructure / staff roles where technical quality
and autonomy matter. The code for everything shown here is available to review.
</p>
<div class="contact-links reveal">
<a href="mailto:pismont.quentin@protonmail.com" class="btn btn-primary">Get in touch</a>
<a href="https://gitea.qpismont.fr/explore/repos" class="btn btn-ghost">Gitea</a>
<a href="#" class="btn btn-ghost">LinkedIn</a>
</div>
</div>
</section>
<footer>
<div class="wrap">
© 2026
</div>
</footer>
<script>
// Reveal on scroll
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
observer.unobserve(entry.target);
}
});
}, { threshold: 0.12 });
document.querySelectorAll('.reveal').forEach((el, i) => {
// slight cascade offset
el.style.transitionDelay = (i % 4 * 60) + 'ms';
observer.observe(el);
});
</script>
</body>
</html>