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. 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, Commits: 3 fixes (`607124a`/`ede7576`/`f515875`) + capitalize (BDR-006/007, LRN-003,
journal) + this follow-up. Branch finished → develop + pushed on owner GO. 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. - **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. - **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). - **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 | | 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: <!-- 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`). - 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). - 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. - 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-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-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-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. - **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. - **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. - **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 .dockerignore
.env .env
.env.example .env.example
nginx.conf.bak
# Editor / OS noise # Editor / OS noise
*.swp *.swp
+1
View File
@@ -32,3 +32,4 @@ graphify-out/
.claude/gstack/ .claude/gstack/
.claude/deploy/PENDING.json .claude/deploy/PENDING.json
.claude/deploy/NEXT.sh .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 green-scale intermediates for dark layering and light block bg
- `#111111` / `#1e1e1e` / `#636363` (`--ink-1/2/3`) — text hierarchy - `#111111` / `#1e1e1e` / `#636363` (`--ink-1/2/3`) — text hierarchy
- `#d8d4c8` (`--rule`), `#e6e2d8` (`--tag`) — separators, generic tags - `#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: Typography:
- `Fraunces` (serif) — display: hero name, section titles, role headings - `Fraunces` (serif) — display: hero name, section titles, role headings
- `JetBrains Mono` (mono) — eyebrows, badges, tech pills, nav, contact rows - `JetBrains Mono` (mono) — eyebrows, badges, tech pills, nav, contact rows
- `DM Sans` (sans) — body text - `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: Forbidden:
- Pure white background (`#ffffff`) - Pure white background (`#ffffff`)
- `border-radius` > 6px except pills - `border-radius` > 6px except pills
+45 -86
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bastien Chanot — CV</title> <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> <style>
:root { :root {
/* Zones sombres (header, footer) */ /* Zones sombres (header, footer) */
@@ -40,8 +40,6 @@
background: var(--page); background: var(--page);
font-family: var(--sans); font-family: var(--sans);
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
margin: 0;
padding: 0;
} }
.page { .page {
@@ -174,15 +172,46 @@
background: var(--rule); background: var(--rule);
} }
/* ── XP ── */ /* ── SHARED (xp/project/edu headers, date chips, roles, tags, links) ── */
.xp-block { margin-bottom: 6px; } .xp-header, .project-header, .edu-header {
.xp-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: baseline; 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 { .xp-company {
font-family: var(--mono); font-family: var(--mono);
@@ -191,22 +220,7 @@
color: var(--ink-1); color: var(--ink-1);
} }
.xp-dates { .xp-dates { font-size: 8.5pt; white-space: nowrap; }
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-loc { .xp-loc {
font-size: 7.5pt; font-size: 7.5pt;
@@ -259,13 +273,6 @@
/* ── PROJECTS ── */ /* ── PROJECTS ── */
.project-block { margin-bottom: 5px; } .project-block { margin-bottom: 5px; }
.project-header {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 1px;
}
.project-name { .project-name {
font-family: var(--mono); font-family: var(--mono);
font-size: 10.5pt; font-size: 10.5pt;
@@ -280,15 +287,7 @@
font-style: italic; font-style: italic;
} }
.project-dates { .project-dates { font-size: 8pt; white-space: nowrap; }
font-family: var(--mono);
font-size: 8pt;
color: var(--ink-3);
white-space: nowrap;
background: var(--tag);
padding: 1px 6px;
border-radius: 2px;
}
.project-desc { .project-desc {
font-size: 10pt; font-size: 10pt;
@@ -328,12 +327,6 @@
/* ── EDU ── */ /* ── EDU ── */
.edu-block { margin-bottom: 5px; } .edu-block { margin-bottom: 5px; }
.edu-header {
display: flex;
justify-content: space-between;
align-items: baseline;
}
.edu-school { .edu-school {
font-family: var(--mono); font-family: var(--mono);
font-size: 11.5pt; font-size: 11.5pt;
@@ -341,21 +334,7 @@
color: var(--ink-1); color: var(--ink-1);
} }
.edu-dates { .edu-dates { font-size: 7.2pt; }
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-detail { .edu-detail {
font-size: 10pt; font-size: 10pt;
@@ -373,15 +352,6 @@
.lang-row { display: flex; gap: 4px; flex-wrap: wrap; } .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 { .lang-item .level {
color: var(--ink-3); color: var(--ink-3);
font-size: 7.5pt; font-size: 7.5pt;
@@ -389,15 +359,6 @@
.interests { display: flex; flex-wrap: wrap; gap: 4px; } .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 ── */
.footer-bar { .footer-bar {
background: var(--dark); background: var(--dark);
@@ -427,7 +388,6 @@
background: 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"), 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%); linear-gradient(160deg, var(--page) 0%, var(--tag) 55%, var(--page) 100%);
padding: 0;
} }
.page { background: transparent; } .page { background: transparent; }
.cv-header { padding: 18px 14mm 14px; } .cv-header { padding: 18px 14mm 14px; }
@@ -488,7 +448,6 @@
</head> </head>
<body> <body>
<div class="page"> <div class="page">
<div class="cv-header"> <div class="cv-header">
@@ -523,9 +482,9 @@
<div class="xp-block"> <div class="xp-block">
<div class="xp-header"> <div class="xp-header">
<span class="xp-company">ZenQuality</span> <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>
<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"> <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>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> <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-block">
<div class="xp-header"> <div class="xp-header">
<span class="xp-company">CareGame</span> <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>
<div class="xp-role">Développeur logiciel — Systèmes &amp; Backend <span class="xp-loc">· Paris · Full remote dès 2020</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"> <ul class="bullets">
@@ -555,7 +514,7 @@
<div class="xp-block break-before-page"> <div class="xp-block break-before-page">
<div class="xp-header"> <div class="xp-header">
<span class="xp-company">Deewee</span> <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>
<div class="xp-role">Développeur C — Système embarqué <span class="xp-loc">· Ivry-sur-Seine</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> <div class="xp-contract">Stage 42 (6 mois) puis CDD (4 mois)</div>
@@ -582,7 +541,7 @@
<div class="project-desc"> <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. Configuration Claude Code, dotfiles, projets bas-niveau (42, expérimentations C/Rust) — accessibles publiquement. Mirror automatique vers GitHub via push hook.
</div> </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>
<div class="project-block"> <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 — # nginx-unprivileged serves index.html + CV (HTML + PDF) as uid 101 —
# no root master process in the container (tag + digest pinned). # 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). # Custom nginx config (gzip, cache, security headers).
COPY nginx.conf /etc/nginx/conf.d/default.conf 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 Production currently serves the static files directly from the VPS's native
nginx (which also terminates TLS). The repo additionally maintains a hardened 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 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 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. `127.0.0.1`, so all traffic goes through the front proxy.
+2 -6
View File
@@ -19,12 +19,8 @@ services:
restart: unless-stopped restart: unless-stopped
ports: ports:
- "127.0.0.1:${PORT:-8080}:8080" - "127.0.0.1:${PORT:-8080}:8080"
healthcheck: # Healthcheck inherited from the image HEALTHCHECK (Dockerfile) — do not
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8080/"] # redeclare here, one definition only.
interval: 30s
timeout: 3s
retries: 3
start_period: 5s
read_only: true read_only: true
tmpfs: tmpfs:
# nginx-unprivileged writes pid + temp files under /tmp only. # 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="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <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> <style>
:root { :root {
/* Palette — non négociable */ /* Palette — non négociable */
@@ -502,16 +502,16 @@
gap: 8px; gap: 8px;
flex-wrap: wrap; flex-wrap: wrap;
} }
.stack-note code { .stack-note code, .theme-list code {
font-family: var(--mono); font-family: var(--mono);
font-size: 12px; font-size: 12px;
font-weight: 500; font-weight: 500;
color: var(--g700); color: var(--g700);
background: var(--g050); background: var(--g050);
border: 1px solid var(--g100); border: 1px solid var(--g100);
padding: 2px 8px;
border-radius: var(--r-sm); 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: 768px) { .stack-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1200px) { .stack-grid { grid-template-columns: repeat(3, 1fr); } } @media (min-width: 1200px) { .stack-grid { grid-template-columns: repeat(3, 1fr); } }
@@ -697,8 +697,6 @@
/* ── FORMATION ── */ /* ── FORMATION ── */
.formation { background: var(--g050); } .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 { .formation-school-desc {
font-family: var(--serif); font-family: var(--serif);
@@ -758,16 +756,7 @@
line-height: 1.55; line-height: 1.55;
color: var(--ink-2); color: var(--ink-2);
} }
.theme-list code { .theme-list code { padding: 1px 7px; }
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);
}
.formation-tsrit-list { .formation-tsrit-list {
list-style: none; list-style: none;
@@ -872,9 +861,6 @@
pointer-events: none; pointer-events: none;
} }
.contact-grid { .contact-grid {
display: grid;
grid-template-columns: 1fr;
gap: 32px;
position: relative; position: relative;
z-index: 1; 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 X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=()" 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 # 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 # 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. # 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/javascript
application/json application/json
application/xml application/xml
application/pdf
image/svg+xml; 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). # Long cache for the PDF (regenerated rarely, content-hash not used).
location ~* \.pdf$ { location ~* \.pdf$ {
add_header Cache-Control "public, max-age=604800"; add_header Cache-Control "public, max-age=604800";
@@ -62,12 +68,6 @@ server {
access_log /var/log/nginx/access.log; access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log warn; 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. # Default: serve files, fall back to 404.
location / { location / {
try_files $uri $uri/ =404; try_files $uri $uri/ =404;