Merge chore/tour-2026-07-05-3 into develop
This commit is contained in:
@@ -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).
|
||||
|
||||
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -11,7 +11,6 @@ docker-compose.yml
|
||||
.dockerignore
|
||||
.env
|
||||
.env.example
|
||||
nginx.conf.bak
|
||||
|
||||
# Editor / OS noise
|
||||
*.swp
|
||||
|
||||
@@ -32,3 +32,4 @@ graphify-out/
|
||||
.claude/gstack/
|
||||
.claude/deploy/PENDING.json
|
||||
.claude/deploy/NEXT.sh
|
||||
.gstack/
|
||||
|
||||
@@ -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
@@ -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 & 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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user