Merge chore/tour-2026-07-05-3 into develop

This commit is contained in:
Bastien Chanot
2026-07-06 01:07:37 +02:00
16 changed files with 188 additions and 122 deletions
+92
View File
@@ -167,3 +167,95 @@ deleted (STEP 3.2).
Checks: CSP-hash MATCH, braces balanced (index 204/204, CV 68/68), PDF 2 pages.
Commits: 3 fixes (`607124a`/`ede7576`/`f515875`) + capitalize (BDR-006/007, LRN-003,
journal) + this follow-up. Branch finished → develop + pushed on owner GO.
## Tour 2026-07-05-3 — AUTO — branch chore/tour-2026-07-05-3 — 3 iterations — CONVERGED
Third run of the day, on develop 7967aff (all prior tour residuals merged).
gstack ON → cso posture add-on ran it1 (it was OFF for run -2) — and caught the
run's only HIGH. Session-limit pause mid-it3 (2026-07-05→06); both it3 agents
resumed from transcript, tree unchanged, no audit gap.
| ID | Axis | File | Sev | Finding | Status |
|----|------|------|-----|---------|--------|
| SEC-1 | security | Dockerfile:5 | high | base `nginx-unprivileged:1.28-alpine` (correct this morning) now on a RETIRED stable branch — 2026-05-13 nginx batch (CVE-2026-42945, rewrite-module buffer overflow, RCE/DoS-class) fixed only in 1.30.1+/1.31.1+, never backported to 1.28.x; digest pin froze the vulnerable build | fixed 1aa97f0 — `1.30-alpine@fd3314e3…` (nginx/1.30.3). Verified: build, `nginx -t`, uid 101, hardened run 5/5 headers + HTTP 200 on /, .html, .pdf, favicon. Not BREAKING (same port/contract); prod needs rebuild+redeploy after merge |
| SEC-2 | security | (full tree) | - | semgrep floor: fresh scan ALL 3 iterations → VERDICT PASS, 0 findings (94 rules; 23→28 files as scratch reports accrued). cso it1: all same-day fixes hold; secrets sweep tree + 36-commit history clean | pass |
| CLN-1 | clean | CV:490 | - | leftover double blank after `<body>` (run -2's CLN-5 went triple→double) | fixed 613bfc0 |
| CLN-2 | clean | nginx.conf:67 | - | dead `deny all;` — `return 404` fires at rewrite phase, access phase never reached | fixed 613bfc0 — dotfile-404 oracle PASS |
| CLN-3 | clean | .dockerignore:14 | - | phantom `nginx.conf.bak` (never existed in tree or history) | fixed 613bfc0 |
| CLN-4 | clean | CV | - | duplicated rules: `.xp/.project/.edu-header`, 3 date chips (5/7 shared), `.xp-role`≡`.edu-degree`, `.lang-item`≡`.interest-tag` → shared block + per-class overrides | fixed 613bfc0 — PDF text-hash + per-page render-hash + full byte-identity vs committed PDF (LRN-003) |
| CLN-5 | clean | CV:528,585 | - | 2 byte-identical inline `style=` attrs → `.inline-link` class; snippet comment ("style attributes in the CV") synced | fixed 613bfc0 — CV now zero style attrs |
| CLN-6 | clean | index:505/761 | - | `.stack-note code`≡`.theme-list code` minus padding → grouped | fixed 613bfc0 |
| CLN-7 | clean | CV:43-44,430 | - | no-op body `margin/padding` (universal reset covers) | fixed 613bfc0 |
| CLN-8 | clean | index:700-701 | - | 2 no-op `.formation .timeline*` overrides restating base values (also removed a latent same-specificity override of the `.current` ring) | fixed 613bfc0 |
| J1 | norm | index+CV (12 sites) | - | `#fff`/rgba-white family (text-on-dark + 4% overlay) outside BOTH documented palette lists — de-facto accepted, undocumented | open — document as 3rd allowed family in CLAUDE.md, or map to tokens (visible change) |
| J2 | norm | CV headings | - | CV section titles/roles mono/sans vs CLAUDE.md "Fraunces = section titles, role headings" — deliberate compact-CV style, unflagged by all prior passes | open — document CV exception (recommended) or restyle |
| J3 | norm | CV:204 | - | `border-radius: 10px` on lang/interest tags — >6px unless counted as pills | open — owner call |
| J4 | config | nginx.conf | - | regex-location order lets hypothetical `/.foo.html` hit the caching block before the dotfile block (both still 404 — file absent; defense-in-depth only) | open — optional reorder |
| J5 | config | Dockerfile+compose | - | healthcheck duplicated (compose fully overrides image HEALTHCHECK, identical params — one always inert) | open — owner call (image stays self-checking without compose) |
| N1 | clean | both font URLs | info | unused Google Fonts faces (index: Fraunces 0,300/0,500/0,700 + DM Sans 300; CV: Fraunces 0,300/0,600 + DM Sans 300) | open — CV half provable via render-hash; index half needs a visual oracle (browser) → owner GO |
| N2 | content | CV:485/498/517 | - | date chips in English (`Apr 2026 - present`…) vs French-copy rule + hyphen/en-dash inconsistency | open — content change, owner call |
| N3 | clean | index:863-869 | info | `.contact-grid` grid declarations no-op around single child — removing `display:grid` can alter margin-collapsing, no render oracle | open |
| N4 | config | nginx.conf:39 | info | `application/pdf` in gzip_types — compressing already-flate-compressed format; removal changes observable response header | open — owner call |
| REC-1 | reconcile | TODO + registries | - | ZERO pre-existing drift. Oracles: CSP hash pinned==computed ✓, PDF↔HTML same commit ede7576 + byte-identical render ✓, BDR-006 (unprivileged/8080) + BDR-007 (Nantes wording ×2 index, ×1 CV) match tree ✓, BLK-001 COPY line holds ✓, og:image + CV-favicon TODO items genuinely open ✓, develop==origin ✓ | consistent |
| REC-2 | reconcile | decisions.md BDR-006 | - | SEC-1 bump makes BDR-006's "1.28-alpine" version detail stale (decision itself — unprivileged base + 8080 — unchanged). Registry read-only for tour | suggested — annotate via /reconcile or /capitalize |
| DOC-1 | doc | README.md:91 | - | stale `1.28-alpine` ref after SEC-1 | fixed 2f5e51a (doc-syncer automatic, single-line) |
| INV-1 | invariant | CSP + PDF | - | CSP hash MATCH all iterations (script byte-untouched); post-clean PDF byte-identical to committed (text sha256 083055…96a8 + both page render-hashes) → PDF file unchanged, nothing to regen | held |
### Iterations
1. **It1** — parallel: security-auditor (semgrep PASS 0 findings) + cso posture
(gstack ON, it1-only: 0c/1h/0m/0l/6i — the HIGH = SEC-1, sourced
endoflife.date + nginx advisories) + clean audit (8 fixable / 5 judgment).
Fixes: 1aa97f0 (security, oracle-verified) + 613bfc0 (clean F1–F8, 5 files,
net −54 lines, render-hash proof). Re-verify (fresh analyzer): PASS — cascade
safety, zero dead selectors, commits scoped. Reconcile: zero drift + REC-2.
Doc-syncer automatic: README 1.28→1.30 (2f5e51a via scoped fallback commit).
2. **It2** — fresh semgrep PASS; clean stability: it1 fixes hold (braces
203/203, 67/67), but 4 NEW info/judgment findings (N1–N4). Fix policy: none
provably behavior-preserving with available oracles (N1-index/N3 need a
browser render; N2/N4 owner calls) → 0 applied, all catalogued open. New
findings appeared → iteration 3 required.
3. **It3 (convergence, at bound)** — fresh semgrep PASS (0 findings); fresh
clean sweep against the full catalogue: stability PASS, borderline items
considered and rejected below threshold, ZERO new. Zero fixes + zero new →
CONVERGED.
### Residuals (open — all owner-judgment, none auto-fixable with available oracles)
J1 (document white family — recommended), J2 (document CV typography
exception — recommended), J3 (10px radius), J4 (dotfile regex order), J5
(healthcheck dup), N1 (font trim — CV provable, index needs eyeball), N2
(English date chips), N3 (.contact-grid), N4 (gzip pdf), REC-2 (BDR-006
version note). Standing accepted/TODO: SEC-7 CSP style-src, og:image, CV
favicon block, WCAG contrast, real-mobile QA.
### Prod follow-up
SEC-1 lands in prod only after merge: VPS `git pull && docker compose up -d
--build` → verify `curl -sI https://bchanot.fr/ | grep -i server` + container
`nginx -v` = 1.30.3.
Checks: semgrep PASS ×3, docker build + nginx -t + hardened-run 4-location
header oracle PASS, CSP-hash MATCH, PDF byte-identical, braces 203/203 + 67/67.
No automated tests/lint/build (static site). Commits: 4 (fix/clean/docs + this
report). BREAKING: 0. Branch left UNMERGED — `gitflow finish` on owner GO.
Scratch reports (.tour-semgrep ×3, .tour-cso, .tour-clean ×3) folded here then
deleted (STEP 3.2).
## Follow-up 2026-07-06 — all 10 residuals closed (chore/tour-2026-07-05-3, owner GO)
| ID | Resolution |
|----|-----------|
| N1 | Google Fonts trimmed: index drops Fraunces 0,300/0,500/0,700 + DM Sans 300 (keeps 0,600 + 1,400 / 400;500;600); CV drops Fraunces 0,300/0,600 + DM Sans 300 (keeps 0,700 + 1,300 / 400;500). CV PROVEN render-identical (per-page hash == baseline). index: font-matching analysis (zero strong/em inside serif elements beyond handled cases: hero-name em → 1,400; about/tsrit strong = sans with explicit weights) + headless-browser check 375px & 1440px — real Fraunces italic renders, zero console errors. |
| N2 | CV date chips → French + en-dash: `avr. 2026 – présent`, `mars 2019 – mars 2025`, `fév. 2017 – nov. 2017` (mirrors landing wording; edu chips already en-dash). |
| J3 | `.lang-item`/`.interest-tag` radius 10px → 999px (true pill treatment, matches landing `--r-pill`). |
| N3 | `.contact-grid` no-op grid declarations dropped (single child + universal reset ⇒ no margin-collapse delta); `position`/`z-index` kept. Browser-verified both widths. |
| J4 | dotfile `location ~ /\.` moved ABOVE the caching regex locations (first regex match wins). Oracle: `/.hidden` + `/.foo.html` → 404, pages 200, headers 5/5. |
| N4 | `application/pdf` dropped from `gzip_types`. Oracle: PDF response carries no Content-Encoding under `Accept-Encoding: gzip`; HTML still gzipped. |
| J5 | compose `healthcheck:` block removed — image HEALTHCHECK is the single definition, inherited by compose. Oracle: compose-less hardened run → `docker inspect` Health = `healthy`. |
| J1 | White family documented in CLAUDE.md allowed lists (text/hover on dark + ≤5% overlays; never a background). |
| J2 | CV typography exception documented in CLAUDE.md (mono section titles/company names, sans roles; Fraunces = header name + accroche; main mapping = landing). |
| REC-2 | BDR-006 annotated: 1.30-alpine bump (CVE-2026-42945), decision itself unchanged. |
CV PDF regenerated (weasyprint, 2 pages, both eyeballed: chips one line, layout
intact). Checks: docker build + nginx -t PASS, header/dotfile/gzip/healthcheck
oracles PASS, CSP hash MATCH (inline script untouched), index verified headless
at 375px + 1440px. Capitalize: LRN-004, EVAL-001, BDR-006 note, journal
2026-07-06. Branch → develop on owner GO (this session).
+1
View File
@@ -108,6 +108,7 @@ rules:
- **Why**: SEC-1 tour finding — stock `nginx:*-alpine` runs its master as root inside the container. Unprivileged image + port 8080 removes the root master; the rest shrinks blast radius. BDR-004's "port 80 / nginx:1.27-alpine / HSTS omitted at container" no longer matched the tree.
- **Supersedes**: BDR-004 — topology unchanged (native front proxy → container on loopback); only the base image, internal port, and uid change.
- **Reference**: `Dockerfile`, `docker-compose.yml`, `nginx.conf`, `nginx-security-headers.conf`. Fix commit `ba13d69`; drift caught by tour REC-1 (`.claude/audits/TOUR.md`, run 2026-07-05-2).
- **Update 2026-07-06**: base bumped `1.28-alpine` → `1.30-alpine` digest-pinned (nginx/1.30.3) — 1.28 branch retired, CVE-2026-42945 fixed 1.30.1+ only, no backport. Decision unchanged (unprivileged base, 8080, uid 101). Commit `1aa97f0`, tour 2026-07-05-3 REC-2.
---
+9
View File
@@ -21,6 +21,15 @@ rules:
| ID | Date | Output | Action |
|----|------|--------|--------|
| EVAL-001 | 2026-07-06 | /tour run 2026-07-05-3 (3 it., converged) + residual closure | keep |
## EVAL-001 — /tour run 2026-07-05-3 + residual closure pass
- **Date**: 2026-07-06
- **Output**: 3-iteration tour (security/clean/reconcile/doc, converged at bound) + closure of all 10 residuals on owner GO. Commits `1aa97f0`/`613bfc0`/`2f5e51a` + follow-up.
- **Method**: oracle-based — semgrep ×3 (deterministic PASS), PDF render-hash (LRN-003) for behavior-preserving proofs, docker oracles (build, nginx -t, header/dotfile/gzip/healthcheck curls), headless-browser screenshots 375+1440 (index font trim), brace counts, CSP-hash pinned==computed.
- **Anomalies**: (1) cso add-on caught a HIGH (base-image CVE) two same-day semgrep-only tours missed — gstack was OFF then → LRN-004. (2) Fresh clean sweeps surfaced new info-tier nits each iteration (N1–N4 at it2) — convergence needed explicit reporting threshold in it3 prompt; bound of 3 did its job. (3) Session limit killed both it3 agents mid-flight — SendMessage transcript-resume recovered both, zero re-audit gap.
- **Action**: keep
<!-- Append entries below. Template:
+6
View File
@@ -41,3 +41,9 @@ rules:
- Closed all 5 residuals on owner GO: CLN-6 aria-hidden CTA arrows (`607124a`), CLN-7/8 palette conformance (5 off-palette colors → tokens, PDF regen render-verified, `ede7576`), CLN-9 geo aligned landing→CV = Nantes relocation (`f515875`).
- Decided: BDR-006 (hardened container, supersedes BDR-004 infra), BDR-007 (geo canonical = Nantes, supersedes BDR-003 geo).
- Branch chore/tour-2026-07-05-2 finished → develop + pushed.
## 2026-07-06
- Tour 2026-07-05-3 finished (session-limit pause mid-it3, agents transcript-resumed): CONVERGED 3 it. SEC-1 HIGH fixed — base 1.28→1.30-alpine, CVE-2026-42945 (`1aa97f0`); clean F1-F8 (`613bfc0`, −54 lines, render-hash proven); README synced (`2f5e51a`).
- All 10 residuals closed on owner GO: Google Fonts trimmed both files (CV render-hash identical, index browser-verified 375+1440), CV date chips French + en-dash, CV tags → 999px pills, contact-grid no-ops dropped, nginx dotfile block reordered first, PDF gzip dropped, compose healthcheck deduped (image healthcheck verified healthy), CLAUDE.md white-family + CV-typography exception documented, BDR-006 annotated (1.30 bump).
- LRN-004 (pinned base = frozen CVE exposure) + EVAL-001 (tour verdict) logged. Branch chore/tour-2026-07-05-3 → develop on owner GO (this session).
+10
View File
@@ -22,6 +22,7 @@ rules:
| LRN-001 | 2026-05-15 | certbot --nginx matches `server_name`, not filename | nginx + certbot on multi-site VPS |
| LRN-002 | 2026-05-17 | PIL supersample ×8 + Lanczos = clean icon antialiasing | Python stdlib icon generation |
| LRN-003 | 2026-07-05 | Prove CSS cleanup behavior-preserving via before/after PDF render-hash | weasyprint / paged-media PDF projects |
| LRN-004 | 2026-07-06 | Digest-pinned base image = frozen CVE exposure; SAST can't see it | any Dockerfile with pinned FROM |
---
@@ -49,3 +50,12 @@ rules:
- **Pattern**: To confirm a CSS/HTML edit is truly behavior-preserving on a project whose deliverable is a weasyprint PDF: render a baseline PDF from the pre-edit HTML, apply the edit, regenerate, then compare (a) `pdftotext | sha256` and (b) per-page `pdftoppm -r 150 -png | sha256`. Text-hash alone misses `font-size`/color changes — the render-hash catches them. Identical render-hash = provably no visual change; and since weasyprint output is deterministic, an unchanged render yields a byte-identical PDF → nothing new to commit.
- **Context**: tour clean phase on `bchanot-cv` removed dead CSS (`.reveal.d6`, `position:running()`, no-op `box-shadow`, dead `.skills-grid font-size`). Render-hash matched on both pages → proven before commit `30b0e44`. The same tooling later confirmed the intentional palette edit DID change the render (expected), distinguishing dead-code removal from real visual change.
- **Future application**: Any weasyprint / paged-media project where you must tell "dead code removal" (must render identically) apart from "intended visual change". General trick: verify a refactor by hashing the rendered artifact, not the source.
---
## LRN-004 — Digest-pinned base image = frozen CVE exposure; SAST can't see it
- **Date**: 2026-07-06
- **Pattern**: Digest pin freezes image bytes → also freezes vulnerabilities. Pin correct at audit time can be HIGH same day: upstream retires stable branch, security batch lands only on newer branches, no backport. semgrep/SAST floor scans code, blind to base-image CVE freshness. Complementary posture pass required: base branch EOL status (endoflife.date) + vendor security advisories, every audit.
- **Context**: bchanot-cv tour 2026-07-05-3. `nginx-unprivileged:1.28-alpine` digest-pinned as SEC fix in morning run; same evening cso posture add-on flagged HIGH — 1.28 branch retired, CVE-2026-42945 (rewrite-module overflow) fixed 1.30.1+/1.31.1+ only. Two intervening semgrep-only tours saw nothing (gstack OFF → no cso). Bump commit `1aa97f0`.
- **Future application**: Any Dockerfile `FROM x@sha256:…` → security audit must include EOL + advisory check on the pinned branch, not just SAST. gstack ON → cso add-on covers it; OFF → manual endoflife.date + vendor advisory check.
-1
View File
@@ -11,7 +11,6 @@ docker-compose.yml
.dockerignore
.env
.env.example
nginx.conf.bak
# Editor / OS noise
*.swp
+1
View File
@@ -32,3 +32,4 @@ graphify-out/
.claude/gstack/
.claude/deploy/PENDING.json
.claude/deploy/NEXT.sh
.gstack/
+8 -1
View File
@@ -79,13 +79,20 @@ Functional neutrals (allowed, intentional — layering + text, NOT brand):
green-scale intermediates for dark layering and light block bg
- `#111111` / `#1e1e1e` / `#636363` (`--ink-1/2/3`) — text hierarchy
- `#d8d4c8` (`--rule`), `#e6e2d8` (`--tag`) — separators, generic tags
Any color outside these two lists is a violation.
- `#ffffff` text + `rgba(255,255,255,…)` alphas — text/hover on dark bg and
low-alpha (≤5%) overlays only; never as a background color
Any color outside these lists is a violation.
Typography:
- `Fraunces` (serif) — display: hero name, section titles, role headings
- `JetBrains Mono` (mono) — eyebrows, badges, tech pills, nav, contact rows
- `DM Sans` (sans) — body text
CV exception (`CV_Bastien_Chanot.html`, compact print style): section titles
and company/school names are mono, roles/degrees are sans; Fraunces is
reserved for the header name and the accroche. The mapping above applies to
the landing.
Forbidden:
- Pure white background (`#ffffff`)
- `border-radius` > 6px except pills
+45 -86
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bastien Chanot — CV</title>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700&family=Fraunces:ital,wght@0,300;0,600;0,700;1,300&family=DM+Sans:wght@300;400;500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700&family=Fraunces:ital,wght@0,700;1,300&family=DM+Sans:wght@400;500&display=swap" rel="stylesheet">
<style>
:root {
/* Zones sombres (header, footer) */
@@ -40,8 +40,6 @@
background: var(--page);
font-family: var(--sans);
-webkit-font-smoothing: antialiased;
margin: 0;
padding: 0;
}
.page {
@@ -174,15 +172,46 @@
background: var(--rule);
}
/* ── XP ── */
.xp-block { margin-bottom: 6px; }
.xp-header {
/* ── SHARED (xp/project/edu headers, date chips, roles, tags, links) ── */
.xp-header, .project-header, .edu-header {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 1px;
}
.xp-header, .project-header { margin-bottom: 1px; }
.xp-dates, .project-dates, .edu-dates {
font-family: var(--mono);
color: var(--ink-3);
background: var(--tag);
padding: 1px 6px;
border-radius: 2px;
}
.xp-role, .edu-degree {
font-size: 10pt;
font-weight: 500;
color: var(--g700);
margin-bottom: 3px;
}
.lang-item, .interest-tag {
font-family: var(--mono);
font-size: 8.5pt;
color: var(--g900);
background: var(--g100);
padding: 2px 8px;
border-radius: 999px;
}
.inline-link {
color: var(--g500);
text-decoration: none;
border-bottom: 1px solid var(--g300);
}
/* ── XP ── */
.xp-block { margin-bottom: 6px; }
.xp-company {
font-family: var(--mono);
@@ -191,22 +220,7 @@
color: var(--ink-1);
}
.xp-dates {
font-family: var(--mono);
font-size: 8.5pt;
color: var(--ink-3);
white-space: nowrap;
background: var(--tag);
padding: 1px 6px;
border-radius: 2px;
}
.xp-role {
font-size: 10pt;
font-weight: 500;
color: var(--g700);
margin-bottom: 3px;
}
.xp-dates { font-size: 8.5pt; white-space: nowrap; }
.xp-loc {
font-size: 7.5pt;
@@ -259,13 +273,6 @@
/* ── PROJECTS ── */
.project-block { margin-bottom: 5px; }
.project-header {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 1px;
}
.project-name {
font-family: var(--mono);
font-size: 10.5pt;
@@ -280,15 +287,7 @@
font-style: italic;
}
.project-dates {
font-family: var(--mono);
font-size: 8pt;
color: var(--ink-3);
white-space: nowrap;
background: var(--tag);
padding: 1px 6px;
border-radius: 2px;
}
.project-dates { font-size: 8pt; white-space: nowrap; }
.project-desc {
font-size: 10pt;
@@ -328,12 +327,6 @@
/* ── EDU ── */
.edu-block { margin-bottom: 5px; }
.edu-header {
display: flex;
justify-content: space-between;
align-items: baseline;
}
.edu-school {
font-family: var(--mono);
font-size: 11.5pt;
@@ -341,21 +334,7 @@
color: var(--ink-1);
}
.edu-dates {
font-family: var(--mono);
font-size: 7.2pt;
color: var(--ink-3);
background: var(--tag);
padding: 1px 6px;
border-radius: 2px;
}
.edu-degree {
font-size: 10pt;
font-weight: 500;
color: var(--g700);
margin-bottom: 3px;
}
.edu-dates { font-size: 7.2pt; }
.edu-detail {
font-size: 10pt;
@@ -373,15 +352,6 @@
.lang-row { display: flex; gap: 4px; flex-wrap: wrap; }
.lang-item {
font-family: var(--mono);
font-size: 8.5pt;
color: var(--g900);
background: var(--g100);
padding: 2px 8px;
border-radius: 10px;
}
.lang-item .level {
color: var(--ink-3);
font-size: 7.5pt;
@@ -389,15 +359,6 @@
.interests { display: flex; flex-wrap: wrap; gap: 4px; }
.interest-tag {
font-family: var(--mono);
font-size: 8.5pt;
color: var(--g900);
background: var(--g100);
padding: 2px 8px;
border-radius: 10px;
}
/* ── FOOTER ── */
.footer-bar {
background: var(--dark);
@@ -427,7 +388,6 @@
background:
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='6' height='6'%3E%3Ccircle cx='1' cy='1' r='0.6' fill='rgba(27,94,59,0.05)'/%3E%3C/svg%3E"),
linear-gradient(160deg, var(--page) 0%, var(--tag) 55%, var(--page) 100%);
padding: 0;
}
.page { background: transparent; }
.cv-header { padding: 18px 14mm 14px; }
@@ -488,7 +448,6 @@
</head>
<body>
<div class="page">
<div class="cv-header">
@@ -523,9 +482,9 @@
<div class="xp-block">
<div class="xp-header">
<span class="xp-company">ZenQuality</span>
<span class="xp-dates">Apr 2026 - present</span>
<span class="xp-dates">avr. 2026 – présent</span>
</div>
<div class="xp-role">Développeur indépendant <span class="xp-loc">· Yerres · <a href="https://zenquality.fr" style="color:var(--g500);text-decoration:none;border-bottom:1px solid var(--g300);">zenquality.fr</a></span></div>
<div class="xp-role">Développeur indépendant <span class="xp-loc">· Yerres · <a href="https://zenquality.fr" class="inline-link">zenquality.fr</a></span></div>
<ul class="bullets">
<li>Mission SEO et conformité légale RGPD pour PME service (Île-de-France) — audit technique Core Web Vitals + Schema.org + NAP, refonte CGV B2B/B2C, RGPD, mentions légales, mise en conformité médiateur CM2C. Plan d'action 12 sprints.</li>
<li>Site vitrine WordPress (<span class="tag">Gutenverse</span>) pour PME esthétique — conception, intégration, déploiement et support continu (hébergement client).</li>
@@ -536,7 +495,7 @@
<div class="xp-block">
<div class="xp-header">
<span class="xp-company">CareGame</span>
<span class="xp-dates">Mar 2019 - Mar 2025</span>
<span class="xp-dates">mars 2019 – mars 2025</span>
</div>
<div class="xp-role">Développeur logiciel — Systèmes &amp; Backend <span class="xp-loc">· Paris · Full remote dès 2020</span></div>
<ul class="bullets">
@@ -555,7 +514,7 @@
<div class="xp-block break-before-page">
<div class="xp-header">
<span class="xp-company">Deewee</span>
<span class="xp-dates">Feb 2017 - Nov 2017</span>
<span class="xp-dates">fév. 2017 – nov. 2017</span>
</div>
<div class="xp-role">Développeur C — Système embarqué <span class="xp-loc">· Ivry-sur-Seine</span></div>
<div class="xp-contract">Stage 42 (6 mois) puis CDD (4 mois)</div>
@@ -582,7 +541,7 @@
<div class="project-desc">
Configuration Claude Code, dotfiles, projets bas-niveau (42, expérimentations C/Rust) — accessibles publiquement. Mirror automatique vers GitHub via push hook.
</div>
<div class="project-link"><a href="https://git.bchanot.fr/bchanot" style="color:var(--g500);text-decoration:none;border-bottom:1px solid var(--g300);">git.bchanot.fr/bchanot</a></div>
<div class="project-link"><a href="https://git.bchanot.fr/bchanot" class="inline-link">git.bchanot.fr/bchanot</a></div>
</div>
<div class="project-block">
Binary file not shown.
+1 -1
View File
@@ -2,7 +2,7 @@
# nginx-unprivileged serves index.html + CV (HTML + PDF) as uid 101 —
# no root master process in the container (tag + digest pinned).
FROM nginxinc/nginx-unprivileged:1.28-alpine@sha256:209331cfcaec00da781f5b8a38e0d1c0abd00cb2b51e6ad385a30abbbdb04e15
FROM nginxinc/nginx-unprivileged:1.30-alpine@sha256:fd3314e343bad2de4e1127ef58be122abbfa7e09572fa46ae62fcddb6b3f21c5
# Custom nginx config (gzip, cache, security headers).
COPY nginx.conf /etc/nginx/conf.d/default.conf
+1 -1
View File
@@ -88,7 +88,7 @@ WCAG AA contrast. Focus visible. Semantic HTML.
Production currently serves the static files directly from the VPS's native
nginx (which also terminates TLS). The repo additionally maintains a hardened
container path (`bchanot-web`, `nginxinc/nginx-unprivileged:1.28-alpine`,
container path (`bchanot-web`, `nginxinc/nginx-unprivileged:1.30-alpine`,
digest-pinned, runs as uid 101 on port 8080) for when a containerized deploy
is preferred: the host port is set via `PORT` (default 8080) and bound to
`127.0.0.1`, so all traffic goes through the front proxy.
+2 -6
View File
@@ -19,12 +19,8 @@ services:
restart: unless-stopped
ports:
- "127.0.0.1:${PORT:-8080}:8080"
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8080/"]
interval: 30s
timeout: 3s
retries: 3
start_period: 5s
# Healthcheck inherited from the image HEALTHCHECK (Dockerfile) — do not
# redeclare here, one definition only.
read_only: true
tmpfs:
# nginx-unprivileged writes pid + temp files under /tmp only.
+4 -18
View File
@@ -13,7 +13,7 @@
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&family=Fraunces:ital,wght@0,300;0,500;0,600;0,700;1,400&family=DM+Sans:wght@300;400;500;600&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&family=Fraunces:ital,wght@0,600;1,400&family=DM+Sans:wght@400;500;600&display=swap" rel="stylesheet">
<style>
:root {
/* Palette — non négociable */
@@ -502,16 +502,16 @@
gap: 8px;
flex-wrap: wrap;
}
.stack-note code {
.stack-note code, .theme-list code {
font-family: var(--mono);
font-size: 12px;
font-weight: 500;
color: var(--g700);
background: var(--g050);
border: 1px solid var(--g100);
padding: 2px 8px;
border-radius: var(--r-sm);
}
.stack-note code { padding: 2px 8px; }
@media (min-width: 768px) { .stack-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1200px) { .stack-grid { grid-template-columns: repeat(3, 1fr); } }
@@ -697,8 +697,6 @@
/* ── FORMATION ── */
.formation { background: var(--g050); }
.formation .timeline { border-left-color: var(--g100); }
.formation .timeline-item::before { box-shadow: 0 0 0 4px var(--g050); }
.formation-school-desc {
font-family: var(--serif);
@@ -758,16 +756,7 @@
line-height: 1.55;
color: var(--ink-2);
}
.theme-list code {
font-family: var(--mono);
font-size: 12px;
font-weight: 500;
color: var(--g700);
background: var(--g050);
border: 1px solid var(--g100);
padding: 1px 7px;
border-radius: var(--r-sm);
}
.theme-list code { padding: 1px 7px; }
.formation-tsrit-list {
list-style: none;
@@ -872,9 +861,6 @@
pointer-events: none;
}
.contact-grid {
display: grid;
grid-template-columns: 1fr;
gap: 32px;
position: relative;
z-index: 1;
}
+1 -1
View File
@@ -8,7 +8,7 @@ add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=()" always;
# CSP: inline CSS allowed (style attributes in the CV + single-file convention);
# CSP: inline CSS allowed (single-file convention: inline <style> element);
# the inline script is HASH-pinned (no script unsafe-inline). INVARIANT: after
# ANY edit to index.html's inline <script>, recompute the hash (command in
# CLAUDE.md) and update it here — a stale hash silently disables the JS.
+7 -7
View File
@@ -36,9 +36,15 @@ server {
application/javascript
application/json
application/xml
application/pdf
image/svg+xml;
# Block access to dotfiles (defense-in-depth — none are shipped anyway).
# First regex location wins: keep this above the caching regex blocks so
# a hypothetical /.foo.html can't be served by them.
location ~ /\. {
return 404;
}
# Long cache for the PDF (regenerated rarely, content-hash not used).
location ~* \.pdf$ {
add_header Cache-Control "public, max-age=604800";
@@ -62,12 +68,6 @@ server {
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log warn;
# Block access to dotfiles (defense-in-depth — none are shipped anyway).
location ~ /\. {
deny all;
return 404;
}
# Default: serve files, fall back to 404.
location / {
try_files $uri $uri/ =404;