Add tempo + portfolio
This commit is contained in:
@@ -7,5 +7,5 @@
|
|||||||
"build": {
|
"build": {
|
||||||
"dockerfile": "Dockerfile"
|
"dockerfile": "Dockerfile"
|
||||||
},
|
},
|
||||||
"postAttachCommand": "docker context create prod --docker \"host=ssh://user@62.210.212.10\""
|
"postCreateCommand": "docker context create prod --docker \"host=ssh://user@62.210.212.10\""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ qpismont.fr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
www.qpismont.fr {
|
www.qpismont.fr {
|
||||||
reverse_proxy my_cv:80
|
root * /www/html
|
||||||
|
file_server
|
||||||
}
|
}
|
||||||
|
|
||||||
gitea.qpismont.fr {
|
gitea.qpismont.fr {
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ services:
|
|||||||
configs:
|
configs:
|
||||||
- source: caddy_config
|
- source: caddy_config
|
||||||
target: /etc/caddy/Caddyfile
|
target: /etc/caddy/Caddyfile
|
||||||
|
- source: caddy_portfolio_html
|
||||||
|
target: /www/html/index.html
|
||||||
volumes:
|
volumes:
|
||||||
- reversecaddydata:/data
|
- reversecaddydata:/data
|
||||||
- reversecaddyconfig:/config
|
- reversecaddyconfig:/config
|
||||||
@@ -152,6 +154,9 @@ configs:
|
|||||||
wireguard_coredns_config:
|
wireguard_coredns_config:
|
||||||
name: wireguard_coredns_config_${DATETIME}
|
name: wireguard_coredns_config_${DATETIME}
|
||||||
file: ./wireguard/CoreDns
|
file: ./wireguard/CoreDns
|
||||||
|
caddy_portfolio_html:
|
||||||
|
name: caddy_portfolio_html_${DATETIME}
|
||||||
|
file: ./portfolio/index.html
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
giteadata:
|
giteadata:
|
||||||
|
|||||||
@@ -1,6 +1,19 @@
|
|||||||
version: "3"
|
version: "3"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
|
tempo:
|
||||||
|
image: grafana/tempo:2.10.7
|
||||||
|
command:
|
||||||
|
- -config.file=/etc/tempo/tempo.yml
|
||||||
|
configs:
|
||||||
|
- source: tempo_config
|
||||||
|
target: /etc/tempo/tempo.yml
|
||||||
|
volumes:
|
||||||
|
- tempodata:/var/tempo
|
||||||
|
networks:
|
||||||
|
- prometheus_network
|
||||||
|
- wireguard_network
|
||||||
|
|
||||||
perses:
|
perses:
|
||||||
image: persesdev/perses:latest
|
image: persesdev/perses:latest
|
||||||
volumes:
|
volumes:
|
||||||
@@ -94,12 +107,16 @@ configs:
|
|||||||
otel_collector_config:
|
otel_collector_config:
|
||||||
name: otel_collector_config_${DATETIME}
|
name: otel_collector_config_${DATETIME}
|
||||||
file: ./otel-collector/config.yaml
|
file: ./otel-collector/config.yaml
|
||||||
|
tempo_config:
|
||||||
|
name: tempo_config_${DATETIME}
|
||||||
|
file: ./tempo/tempo.yml
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
persesdata:
|
persesdata:
|
||||||
prometheusdata:
|
prometheusdata:
|
||||||
ntfydata:
|
ntfydata:
|
||||||
alertmanagerdata:
|
alertmanagerdata:
|
||||||
|
tempodata:
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
reverse_network:
|
reverse_network:
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ processors:
|
|||||||
exporters:
|
exporters:
|
||||||
prometheusremotewrite:
|
prometheusremotewrite:
|
||||||
endpoint: http://prometheus:9090/api/v1/write
|
endpoint: http://prometheus:9090/api/v1/write
|
||||||
|
otlphttp/tempo:
|
||||||
|
endpoint: http://tempo:4318
|
||||||
|
|
||||||
service:
|
service:
|
||||||
pipelines:
|
pipelines:
|
||||||
@@ -34,3 +36,7 @@ service:
|
|||||||
receivers: [otlp, prometheus]
|
receivers: [otlp, prometheus]
|
||||||
processors: [memory_limiter, batch]
|
processors: [memory_limiter, batch]
|
||||||
exporters: [prometheusremotewrite]
|
exporters: [prometheusremotewrite]
|
||||||
|
traces:
|
||||||
|
receivers: [otlp]
|
||||||
|
processors: [memory_limiter, batch]
|
||||||
|
exporters: [otlphttp/tempo]
|
||||||
@@ -0,0 +1,586 @@
|
|||||||
|
<!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 a platform / infrastructure engineer: distributed systems, observability, security and self-hosted CI/CD, tested in production.">
|
||||||
|
<title>Portfolio — Platform Engineer</title>
|
||||||
|
<!-- Open Graph -->
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
<meta property="og:title" content="Portfolio — 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>
|
||||||
|
Developer for <strong>over 20 years</strong> (first lines of C at age 10),
|
||||||
|
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 & 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 & 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 & Isolation</h4>
|
||||||
|
<p>Container sandboxing, privilege separation (DBus / Polkit), threat modeling.</p>
|
||||||
|
</div>
|
||||||
|
<div class="skill-card reveal">
|
||||||
|
<h4><span class="ic">◆</span> Distributed & 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>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
server:
|
||||||
|
http_listen_port: 3200
|
||||||
|
|
||||||
|
distributor:
|
||||||
|
receivers:
|
||||||
|
otlp:
|
||||||
|
protocols:
|
||||||
|
http:
|
||||||
|
endpoint: 0.0.0.0:4318
|
||||||
|
|
||||||
|
ingester:
|
||||||
|
max_block_duration: 5m
|
||||||
|
|
||||||
|
compactor:
|
||||||
|
compaction:
|
||||||
|
block_retention: 48h
|
||||||
|
|
||||||
|
storage:
|
||||||
|
trace:
|
||||||
|
backend: local
|
||||||
|
local:
|
||||||
|
path: /var/tempo/traces
|
||||||
|
wal:
|
||||||
|
path: /var/tempo/wal
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
.:53 {
|
.:53 {
|
||||||
rewrite name perses.internal perses
|
rewrite name perses.internal perses
|
||||||
rewrite name prometheus.internal prometheus
|
rewrite name prometheus.internal prometheus
|
||||||
|
rewrite name tempo.internal tempo
|
||||||
forward . 127.0.0.11
|
forward . 127.0.0.11
|
||||||
errors
|
errors
|
||||||
cache 10
|
cache 10
|
||||||
|
|||||||
Reference in New Issue
Block a user