Compare commits
33
Commits
bd7f6e4984
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2891be5898 | ||
|
|
3f6bc571d3 | ||
|
|
3c4dab88a5 | ||
|
|
aceb7e2c70 | ||
|
|
e2f7ea4546 | ||
|
|
e13a4b1bb5 | ||
|
|
d8ffa983b5 | ||
|
|
1c01cb20eb | ||
|
|
6b0ea8494b | ||
|
|
1d0b7ee8e7 | ||
|
|
9af6aa4be8 | ||
|
|
136e1df5e3 | ||
|
|
c8c72c24aa | ||
|
|
dd8c327162 | ||
|
|
84288c5c58 | ||
|
|
a589b99878 | ||
|
|
b86a5129f0 | ||
|
|
cc65225b3d | ||
|
|
2f5e51a1b4 | ||
|
|
613bfc0d49 | ||
|
|
1aa97f0af0 | ||
|
|
7967afff08 | ||
|
|
7984a7d2df | ||
|
|
f5158758b2 | ||
|
|
ede75765cd | ||
|
|
607124aa70 | ||
|
|
7b3d9bec4c | ||
|
|
30b0e44a45 | ||
|
|
d7256ffe0e | ||
|
|
b24c58b8a4 | ||
|
|
ef7e2312c6 | ||
|
|
c0632aefa8 | ||
|
|
d63a52ec50 |
@@ -92,3 +92,170 @@ container is redeployed: merge → VPS `git pull && docker compose up -d
|
|||||||
|
|
||||||
Commits: 4 (fix/clean/docs + this report). BREAKING: 1 (SEC-1, container
|
Commits: 4 (fix/clean/docs + this report). BREAKING: 1 (SEC-1, container
|
||||||
port — compose covered). Branch left UNMERGED — `gitflow finish` on GO.
|
port — compose covered). Branch left UNMERGED — `gitflow finish` on GO.
|
||||||
|
|
||||||
|
## Follow-up 2026-07-05 — residuals closed (chore/tour-residuals, user GO)
|
||||||
|
|
||||||
|
| ID | Resolution |
|
||||||
|
|----|-----------|
|
||||||
|
| CLN-3 | Card CSS deduplicated via grouped selectors (shared chrome/hover/head/title/tag blocks + per-class specifics), zero HTML change, cascade-order verified (no interfering same-specificity rules between shared and specific blocks), braces 195/195. Honest correction: the audited "~421 redundant lines" was overstated — real net dedup ≈ 60 lines. |
|
||||||
|
| CLN-4 | Norm aligned with reality: the 8 functional neutrals (inks, rule/tag, 2 green intermediates) are now DOCUMENTED as allowed in CLAUDE.md (+ README pointer). "Any color outside the two lists is a violation" keeps the norm enforceable. |
|
||||||
|
| SEC-7 | script-src hardened: `unsafe-inline` replaced by the sha256 hash of the single inline script (index has zero style/script attributes). style-src keeps `unsafe-inline` (CV carries 2 style attributes + single-file convention) — documented. NEW INVARIANT in CLAUDE.md: recompute the hash after any inline-JS edit (stale hash = JS silently blocked in prod). |
|
||||||
|
| INF-2 | CORRECTION: false positive in the 2026-07-05 report-only run — `.gitignore` exists (549B) and covers the expected classes. No action was ever needed. |
|
||||||
|
|
||||||
|
## Tour 2026-07-05-2 — AUTO — branch chore/tour-2026-07-05-2 — 2 iterations — CONVERGED
|
||||||
|
|
||||||
|
Re-run of /tour on develop (d7256ff) after the day's earlier tours merged. Goal:
|
||||||
|
verify no regression + catch anything new. Branch suffixed `-2` to keep this
|
||||||
|
header distinct from the earlier converged run. gstack OFF → optional It1 cso
|
||||||
|
posture add-on not run; the security floor (security-auditor + pinned semgrep)
|
||||||
|
ran BOTH iterations, not degraded. No package.json/Makefile → no automated
|
||||||
|
tests/lint/build; project checks = domain invariants (CSP-hash, PDF↔HTML).
|
||||||
|
|
||||||
|
| ID | Axis | File | Sev | Finding | Status |
|
||||||
|
|----|------|------|-----|---------|--------|
|
||||||
|
| SEC-1 | security | (full tree) | - | Fresh semgrep both iterations → VERDICT PASS, 0 blocking. Sole note: `style-src 'unsafe-inline'` (accepted single-file convention, It1==It2). No regression from the prior SEC fixes; script-src hash still matches inline script | pass |
|
||||||
|
| CLN-1 | clean | index.html:347 | - | dead `.reveal.d6` rule (markup uses reveal d1–d5 only) | fixed 30b0e44 |
|
||||||
|
| CLN-2 | clean | CV:408 | - | dead `position: running(siteFooter)` — no `element(siteFooter)` consumer; `.footer-bar` is `display:none` in @media print (@page auto-numbered footer replaces it); on screen running() is an invalid position value, ignored | fixed 30b0e44 |
|
||||||
|
| CLN-3 | clean | CV:438 | - | no-op `box-shadow: none` on `.page` (`.page` sets a shadow nowhere; weasyprint ignores box-shadow entirely — confirmed by its own warning) | fixed 30b0e44 |
|
||||||
|
| CLN-4 | clean | CV:315 | - | dead `.skills-grid { font-size: 8.4pt }` — every direct child is a `.skill-label`(9.5pt)/`.skill-values`(10pt) div; no bare text node, no em-dependency → never renders | fixed 30b0e44 |
|
||||||
|
| CLN-5 | clean | CV:46-48,54,316 | - | stray blank lines (triple blank before `.page`, blanks inside `.page`/`.skills-grid`) | fixed 30b0e44 |
|
||||||
|
| CLN-6 | a11y | index.html:1003,1007 | - | 2 decorative `.arrow` SVGs miss the `aria-hidden="true"` the sibling download arrow (1011) has. NOT auto-fixed: adding it changes the a11y tree → outside the clean phase's behavior-preserving scope (belongs to an a11y pass; cf. TODO "WCAG AA contrast") | open (suggested) |
|
||||||
|
| CLN-7 | norm | index.html:931 | - | `.footer` bg `#061008` is off-palette (darker than `--dark #0d1b12`, `--g900 #0e3320`). Design system documents footer = `#0d1b12`. Fix changes rendering (slightly lighter footer) → owner decision, not auto-fixed | open (suggested) |
|
||||||
|
| CLN-8 | norm | CV:252,135-136,434-435 | - | off-palette colors: `#a8d4bc` tag border (252), gradient stops `#edeadf`/`#f2efe6` (136/435), texture fill `rgba(26,71,48,0.05)` (135/434). All rendering-changing → owner decision, not auto-fixed | open (suggested) |
|
||||||
|
| CLN-9 | content | index.html vs CV:507-508 | - | profile-state wording drift: landing "Pays de la Loire / remote or 1–2j Paris" vs CV "région nantaise / hybride Nantes / 1–2j Paris" (not contradictory — Nantes ∈ PdL — but CV adds "hybride Nantes"). CLAUDE.md requires cross-file consistency → owner picks canonical wording, not auto-fixed | open (suggested) |
|
||||||
|
| REC-1 | reconcile | .claude/memory/decisions.md | - | **BDR-004 stale**: text says `nginx:1.27-alpine` / container port 80 / "HSTS omitted at container", but the real Dockerfile+compose (post 2026-07-05 SEC-1 fix) = `nginxinc/nginx-unprivileged:1.28-alpine` / port 8080 / uid 101. That tour never added a superseding decision (index stops at BDR-005). Append-only registry + tour-read-only → SUGGEST a superseding **BDR-006**. README deploy section is already correct | suggested |
|
||||||
|
| REC-2 | reconcile | .claude/memory/decisions.md | - | BDR-002 "Warnings connus: `box-shadow:none` ignoré par weasyprint" — that declaration was removed this tour (CLN-3), so the documented warning no longer fires. Minor note to add when BDR-002 is next touched | suggested |
|
||||||
|
| REC-3 | reconcile | TODO.md + registries | - | ZERO false-done. Oracles: 1369d27 exists ✓; `og:image` absent = TODO item genuinely open ✓; CV favicon-block not mirrored = open, matches BDR-005 note ✓; WCAG-contrast + real-mobile-QA open ✓; develop==origin/develop (d7256ff), branch +1 unmerged ✓; BLK-001 resolved, favicon assets present ✓ | consistent |
|
||||||
|
| DOC-1 | doc | README.md | - | doc-syncer automatic mode → `PATCHED_FILES: (none)`. Deploy section already reflects unprivileged image/port 8080 (prior tour sync); file table matches root inventory; cleanup touched nothing user-facing | no-op |
|
||||||
|
| INV-1 | invariant | index.html / CV pdf | - | CSP hash `sha256-Al1M34KxI6Ye5Viu6aO//7CYyaLzqtpG9GX95FFlSOY=` recomputed == pinned (inline `<script>` untouched) ✓; PDF regenerated byte-identical to the pre-edit baseline (text sha256 083055…96a8 + per-page PNG @150dpi render hash all match) → PDF=HTML invariant holds, PDF file unchanged | held |
|
||||||
|
|
||||||
|
### Iterations
|
||||||
|
1. **It1** — security-auditor fresh semgrep (94 rules / 25 files → VERDICT PASS,
|
||||||
|
1 accepted LOW) + read-only clean audit (13 findings: C1–C8, N1–N5). Applied
|
||||||
|
behavior-preserving fixes CLN-1..5 (commit 30b0e44); proven behavior-preserving
|
||||||
|
(PDF renders pixel-identical, CSP hash unchanged). Fresh `analyzer` re-verify:
|
||||||
|
RE-VERIFY PASS, braces balanced, zero new. Reconcile (report-only): BDR-004
|
||||||
|
drift + BDR-002 note + zero false-done. Doc: no drift.
|
||||||
|
2. **It2 (convergence)** — fresh full-tree semgrep: VERDICT PASS, identical to It1,
|
||||||
|
0 new blocking. Clean stability: 4 removed selectors GONE, braces balanced
|
||||||
|
(index 204/204, CV 68/68), known-open findings (CLN-6..9) persist = NOT new,
|
||||||
|
zero new introduced. Zero fixes → CONVERGED.
|
||||||
|
|
||||||
|
### Residuals (open — all require owner judgment, none auto-fixable behavior-preservingly)
|
||||||
|
CLN-6 (arrows aria-hidden — a11y pass), CLN-7/8 (off-palette colors — design
|
||||||
|
decision), CLN-9 (profile-state wording — copy canonicalization), REC-1
|
||||||
|
(superseding BDR-006 for the hardened container), REC-2 (BDR-002 warning note).
|
||||||
|
SEC accepted-LOW (`style-src 'unsafe-inline'`) unchanged from prior runs.
|
||||||
|
|
||||||
|
Checks: semgrep PASS (both it.), CSP-hash MATCH, PDF↔HTML byte-identical render,
|
||||||
|
CSS braces balanced. No automated tests/lint/build (static site).
|
||||||
|
Commits: 2 (clean 30b0e44 + this report). BREAKING: 0. Branch left UNMERGED.
|
||||||
|
Scratch reports (.tour-semgrep, .tour-clean, .tour-semgrep-it2) folded here then
|
||||||
|
deleted (STEP 3.2).
|
||||||
|
|
||||||
|
## Follow-up 2026-07-05-2 — all 5 residuals closed (chore/tour-2026-07-05-2, owner GO)
|
||||||
|
|
||||||
|
| ID | Resolution |
|
||||||
|
|----|-----------|
|
||||||
|
| CLN-6 | `aria-hidden="true"` added to the 2 decorative CTA arrows (match sibling download arrow). Visual identical, a11y-tree only. Commit `607124a`. |
|
||||||
|
| CLN-7 | `.footer` bg `#061008` → `var(--dark)` #0d1b12 (the design-system footer color). Commit `ede7576`. |
|
||||||
|
| CLN-8 | CV off-palette → tokens: `.tag` border `#a8d4bc` → `var(--g300)` (nearest visible green); body+print texture `rgba(26,71,48,.05)` → `rgba(27,94,59,.05)` (--g700); gradient stops `#edeadf`/`#f2efe6` → `var(--tag)`/`var(--page)`. PDF regenerated, render verified (2 pages, layout intact, page-1 eyeballed). Commit `ede7576`. |
|
||||||
|
| CLN-9 | Owner chose the CV wording as canonical (Option B): landing about-para + callout aligned to "installation région nantaise prévue" + "hybride Nantes"; `CLAUDE.md` geography note updated to match. CV unchanged. Commit `f515875` → BDR-007. |
|
||||||
|
| REC-1 | BDR-004 drift resolved by superseding entry **BDR-006** (nginx-unprivileged:1.28 / port 8080 / uid 101). |
|
||||||
|
| REC-2 | BDR-002 "box-shadow warning" note now historical (declaration removed in `30b0e44`) — left as-is (append-only registry), noted here. |
|
||||||
|
|
||||||
|
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,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"deployed_sha": "5fe8b4119b33e77c27b35eedc37b1ae7962a5070",
|
"deployed_sha": "b24c58b8a467811719ff197f4b008d2f991b80d0",
|
||||||
"deployed_at": "2026-07-05T15:24:00+02:00",
|
"deployed_at": "2026-07-05T16:55:00+02:00",
|
||||||
"outcome": "ok",
|
"outcome": "ok",
|
||||||
"tag": "deploy/2026-07-05"
|
"tag": "deploy/2026-07-05-2"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ rules:
|
|||||||
| BDR-003 | 2026-05-15 | Position pro: CDI prioritaire, freelance parallèle | accepted |
|
| BDR-003 | 2026-05-15 | Position pro: CDI prioritaire, freelance parallèle | accepted |
|
||||||
| BDR-004 | 2026-05-15 | Containerize site with nginx:alpine behind reverse proxy | accepted |
|
| BDR-004 | 2026-05-15 | Containerize site with nginx:alpine behind reverse proxy | accepted |
|
||||||
| BDR-005 | 2026-05-17 | Favicon: SVG primary + PIL raster fallback | accepted |
|
| BDR-005 | 2026-05-17 | Favicon: SVG primary + PIL raster fallback | accepted |
|
||||||
|
| BDR-006 | 2026-07-05 | Hardened container: nginx-unprivileged + port 8080 | accepted (supersedes BDR-004 infra detail) |
|
||||||
|
| BDR-007 | 2026-07-05 | Profile geo canonical: Nantes relocation | accepted (supersedes BDR-003 geo) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -95,3 +97,26 @@ rules:
|
|||||||
- Online favicon generator — external dep, opaque rendering, no source control.
|
- Online favicon generator — external dep, opaque rendering, no source control.
|
||||||
- **CV HTML**: not modified (user's WIP M state). Browser auto-fetches `/favicon.ico` from root → CV tab still shows icon. Link block mirror logged in `.claude/tasks/TODO.md` for later.
|
- **CV HTML**: not modified (user's WIP M state). Browser auto-fetches `/favicon.ico` from root → CV tab still shows icon. Link block mirror logged in `.claude/tasks/TODO.md` for later.
|
||||||
- **Reference**: `favicon.svg`, `favicon-32.png`, `favicon.ico`, `apple-touch-icon.png`, `index.html` head, commit `ef31fb3`.
|
- **Reference**: `favicon.svg`, `favicon-32.png`, `favicon.ico`, `apple-touch-icon.png`, `index.html` head, commit `ef31fb3`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## BDR-006 — Hardened container: nginx-unprivileged base + port 8080
|
||||||
|
|
||||||
|
- **Date**: 2026-07-05
|
||||||
|
- **Status**: accepted — supersedes the base-image/port detail of BDR-004
|
||||||
|
- **Decision**: Container base = `nginxinc/nginx-unprivileged:1.28-alpine` (digest-pinned), runs as uid 101, listens on **8080** (not 80). Compose maps `127.0.0.1:${PORT}:8080`; `USER root` scoped to the one build-time `rm` only; `cap_add` dropped; `server_tokens off`; `set_real_ip_from 127.0.0.1`; `nginx-security-headers.conf` re-included per `location`; CSP `script-src` hash-pinned.
|
||||||
|
- **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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## BDR-007 — Profile geo canonical: Nantes relocation
|
||||||
|
|
||||||
|
- **Date**: 2026-07-05
|
||||||
|
- **Status**: accepted — supersedes the geography detail of BDR-003
|
||||||
|
- **Decision**: Canonical profile geo = "Yerres (91) now; installation région nantaise prévue à moyen terme; full remote, hybride possible sur Nantes, ou 1–2 j/mois Paris." CV was the source of truth; `index.html` + `CLAUDE.md` aligned to it.
|
||||||
|
- **Why**: tour CLN-9 found the landing ("mobilité Pays de la Loire") drifting from the CV ("installation région nantaise" + "hybride Nantes"). Owner chose the CV wording as truth — more current/specific, and Nantes ∈ Pays de la Loire so not contradictory. Cross-file profile-state consistency is a CLAUDE.md content rule.
|
||||||
|
- **Alternatives rejected**: align CV down to the landing (would delete real, more-specific relocation info).
|
||||||
|
- **Reference**: `index.html` (about para + about-callout), `CV_Bastien_Chanot.html`, `CLAUDE.md` geography note. Commit `f515875`.
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|
||||||
|
|||||||
@@ -33,3 +33,17 @@ rules:
|
|||||||
- Favicon set added (commit `ef31fb3`): SVG primary + PIL-generated PNG/ICO/apple-touch. Brand pulse-dot translated to icon. BDR-005 + LRN-002 logged.
|
- Favicon set added (commit `ef31fb3`): SVG primary + PIL-generated PNG/ICO/apple-touch. Brand pulse-dot translated to icon. BDR-005 + LRN-002 logged.
|
||||||
- CV files (`CV_Bastien_Chanot.html`, `.pdf`) untouched — user's WIP M state, off-scope per brief.
|
- CV files (`CV_Bastien_Chanot.html`, `.pdf`) untouched — user's WIP M state, off-scope per brief.
|
||||||
- User pushed + reported favicon 404 in prod. Root cause: Dockerfile selective `COPY` whitelist never included favicon files. Fix shipped (commit `f1e4392`): COPY line appended + nginx long-cache rule for image assets. BLK-001 logged. VPS rebuild required.
|
- User pushed + reported favicon 404 in prod. Root cause: Dockerfile selective `COPY` whitelist never included favicon files. Fix shipped (commit `f1e4392`): COPY line appended + nginx long-cache rule for image assets. BLK-001 logged. VPS rebuild required.
|
||||||
|
|
||||||
|
## 2026-07-05
|
||||||
|
|
||||||
|
- Grouped tours (security+clean+reconcile+doc): container hardened (SEC-1..7 → nginx-unprivileged:1.28 / port 8080 / uid 101, per-location security headers, `server_tokens off`, CSP script-src hash-pinned), palette + dead-code clean, README synced. Merged via chore/tour + chore/tour-residuals. Detail in `.claude/audits/TOUR.md`.
|
||||||
|
- Re-run tour (chore/tour-2026-07-05-2) CONVERGED 2 it: fresh semgrep PASS, dead CSS removed (`30b0e44`, render-hash proven behavior-preserving → LRN-003), reconcile caught BDR-004 drift, doc no-drift.
|
||||||
|
- 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).
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ 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-004 | 2026-07-06 | Digest-pinned base image = frozen CVE exposure; SAST can't see it | any Dockerfile with pinned FROM |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -39,3 +41,21 @@ rules:
|
|||||||
- **Pattern**: Render icon at 8× target size via `ImageDraw.rounded_rectangle` + `ellipse` on RGBA canvas, then `Image.resize((target, target), Image.LANCZOS)`. Output rivals `rsvg-convert` / `inkscape` for simple geometric shapes. Crisp at 16×16 favicon scale, no visible jaggies.
|
- **Pattern**: Render icon at 8× target size via `ImageDraw.rounded_rectangle` + `ellipse` on RGBA canvas, then `Image.resize((target, target), Image.LANCZOS)`. Output rivals `rsvg-convert` / `inkscape` for simple geometric shapes. Crisp at 16×16 favicon scale, no visible jaggies.
|
||||||
- **Context**: Generated `favicon-32.png`, `apple-touch-icon.png` (180×180), `favicon.ico` (multi-size 16/24/32/48) for `bchanot.fr` from scratch — no `rsvg-convert` / `inkscape` / `ImageMagick` on host. Single PIL script, ~20 lines.
|
- **Context**: Generated `favicon-32.png`, `apple-touch-icon.png` (180×180), `favicon.ico` (multi-size 16/24/32/48) for `bchanot.fr` from scratch — no `rsvg-convert` / `inkscape` / `ImageMagick` on host. Single PIL script, ~20 lines.
|
||||||
- **Future application**: Any project needing a PNG/ICO icon set with a stdlib-only Python toolchain. Skip if shape is complex (text rendering, gradients, curves) — use `rsvg-convert` or commit a finalized PNG instead.
|
- **Future application**: Any project needing a PNG/ICO icon set with a stdlib-only Python toolchain. Skip if shape is complex (text rendering, gradients, curves) — use `rsvg-convert` or commit a finalized PNG instead.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## LRN-003 — Prove CSS cleanup is behavior-preserving via before/after PDF render-hash
|
||||||
|
|
||||||
|
- **Date**: 2026-07-05
|
||||||
|
- **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
|
.dockerignore
|
||||||
.env
|
.env
|
||||||
.env.example
|
.env.example
|
||||||
nginx.conf.bak
|
|
||||||
|
|
||||||
# Editor / OS noise
|
# Editor / OS noise
|
||||||
*.swp
|
*.swp
|
||||||
|
|||||||
@@ -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/
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project are documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||||
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [1.1.0] — 2026-07-23
|
||||||
|
|
||||||
|
Profile geography moved to the Loire-Atlantique · Vendée · Morbihan zone, and
|
||||||
|
the CV restructured.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Landing profile block: `Zone` (Loire-Atlantique · Vendée · Morbihan) replaces
|
||||||
|
the former `Localisation actuelle`; every mention of Yerres and of the planned
|
||||||
|
Nantes relocation removed, including the ZenQuality timeline entry.
|
||||||
|
- Presence stated as an explicit order of preference on both surfaces — full
|
||||||
|
remote, then hybrid or on-site inside the zone, Paris only as a fallback and
|
||||||
|
capped at 1–2 days per month.
|
||||||
|
- CV restructured: technical skills promoted above professional experience,
|
||||||
|
"Centres d'intérêt" dropped, markup trimmed 656 → 424 lines.
|
||||||
|
- `CLAUDE.md` content rules: geography note rewritten to the zone model, with an
|
||||||
|
explicit guard against reintroducing Yerres.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- `CV_Bastien_CHANOT_General.{html,pdf}` — general CV variant kept beside the
|
||||||
|
served one. Repo-only: not linked from the landing, not in the Dockerfile
|
||||||
|
COPY whitelist.
|
||||||
|
|
||||||
|
## [1.0.0] — 2026-07-06
|
||||||
|
|
||||||
|
First tagged release. Landing page + CV live at https://bchanot.fr, served by
|
||||||
|
a hardened nginx container behind the host reverse proxy.
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- Container runs unprivileged: `nginxinc/nginx-unprivileged` digest-pinned,
|
||||||
|
uid 101, internal port 8080; base at 1.30-alpine (nginx/1.30.3) covering
|
||||||
|
CVE-2026-42945. Compose hardening: `read_only`, `cap_drop: ALL`,
|
||||||
|
`no-new-privileges`, loopback-bound port.
|
||||||
|
- Security headers (CSP, X-Content-Type-Options, X-Frame-Options,
|
||||||
|
Referrer-Policy, Permissions-Policy) re-included in every nginx location
|
||||||
|
(add_header inheritance trap closed); `server_tokens off`; CSP `script-src`
|
||||||
|
pinned to the inline script's sha256 hash; dotfile requests 404 ahead of
|
||||||
|
the caching locations.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Palette strictly tokenized: 6 brand hexes + documented functional neutrals
|
||||||
|
+ white-on-dark family; off-palette colors mapped to tokens.
|
||||||
|
- CSS deduplicated via grouped selectors (landing cards, CV headers / date
|
||||||
|
chips / roles / tags); dead rules and no-op declarations removed.
|
||||||
|
- Unused Google Fonts faces trimmed from both pages (CV proven
|
||||||
|
render-identical; landing verified at 375 px and 1440 px).
|
||||||
|
- CV date chips in French with en-dashes; language/interest tags as true
|
||||||
|
pills; profile geography aligned landing ↔ CV (Nantes relocation).
|
||||||
|
- `prefers-reduced-motion` disables transitions as well as animations;
|
||||||
|
decorative CTA arrows removed from the accessibility tree.
|
||||||
|
- PDF served uncompressed (already flate-compressed); single container
|
||||||
|
healthcheck (image `HEALTHCHECK`, inherited by compose).
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- `version.txt` and this CHANGELOG — the release lineage starts here.
|
||||||
@@ -66,7 +66,7 @@ The PDF must match the latest HTML before pushing or sending.
|
|||||||
|
|
||||||
## Design system (non-negotiable)
|
## Design system (non-negotiable)
|
||||||
|
|
||||||
Palette — exact hex:
|
Palette — exact hex (brand colors):
|
||||||
- `#0d1b12` — dark forest (nav, dark sections, footer)
|
- `#0d1b12` — dark forest (nav, dark sections, footer)
|
||||||
- `#1b5e3b` — green primary (links, section titles on light bg)
|
- `#1b5e3b` — green primary (links, section titles on light bg)
|
||||||
- `#2d7a4f` — green accent (borders, dots, separators)
|
- `#2d7a4f` — green accent (borders, dots, separators)
|
||||||
@@ -74,11 +74,25 @@ Palette — exact hex:
|
|||||||
- `#dff0e7` — green tint (pill bg)
|
- `#dff0e7` — green tint (pill bg)
|
||||||
- `#f5f3ec` — parchment (page bg)
|
- `#f5f3ec` — parchment (page bg)
|
||||||
|
|
||||||
|
Functional neutrals (allowed, intentional — layering + text, NOT brand):
|
||||||
|
- `#183325` (`--dark-mid`), `#0e3320` (`--g900`), `#eef7f1` (`--g050`) —
|
||||||
|
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
|
||||||
|
- `#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
|
||||||
@@ -108,8 +122,10 @@ Forbidden:
|
|||||||
- Profile state, including job search context, must stay consistent across
|
- Profile state, including job search context, must stay consistent across
|
||||||
index.html and CV. Currently: looking for **CDI** in embedded / systems
|
index.html and CV. Currently: looking for **CDI** in embedded / systems
|
||||||
software first; freelance missions (ZenQuality) in parallel.
|
software first; freelance missions (ZenQuality) in parallel.
|
||||||
- Geography: Yerres (91) currently; targeting Pays de la Loire mid-term;
|
- Geography: zone **Loire-Atlantique · Vendée · Morbihan**. Never mention
|
||||||
full remote preferred or hybrid 1–2 days/month if Paris.
|
Yerres or the Essonne. Presence, in order of preference: full remote →
|
||||||
|
hybrid in the zone → on-site in the zone → Paris only as a fallback, and
|
||||||
|
only hybrid at 1–2 days per month maximum.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -124,6 +140,12 @@ None — global rules apply.
|
|||||||
- Edits to `index.html` or `CV_Bastien_Chanot.html` must preserve the
|
- Edits to `index.html` or `CV_Bastien_Chanot.html` must preserve the
|
||||||
palette + typography + structure unless explicitly asked to change them.
|
palette + typography + structure unless explicitly asked to change them.
|
||||||
- After editing `CV_Bastien_Chanot.html`, regenerate the PDF.
|
- After editing `CV_Bastien_Chanot.html`, regenerate the PDF.
|
||||||
|
- After editing index.html's inline `<script>`, recompute the CSP hash and
|
||||||
|
update `nginx-security-headers.conf` (script-src is hash-pinned — a stale
|
||||||
|
hash silently disables the JS in prod):
|
||||||
|
```bash
|
||||||
|
python3 -c "import hashlib,base64,re;h=base64.b64encode(hashlib.sha256(re.search(r'<script>(.*?)</script>',open('index.html',encoding='utf-8').read(),re.S).group(1).encode()).digest()).decode();print('sha256-'+h)"
|
||||||
|
```
|
||||||
- Never add external dependencies beyond Google Fonts.
|
- Never add external dependencies beyond Google Fonts.
|
||||||
- Never add tracking, analytics, cookie banners or third-party scripts.
|
- Never add tracking, analytics, cookie banners or third-party scripts.
|
||||||
- Always test in mobile width (375px) and desktop (1440px) before claiming done.
|
- Always test in mobile width (375px) and desktop (1440px) before claiming done.
|
||||||
|
|||||||
@@ -0,0 +1,424 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<!--
|
||||||
|
=====================================================================
|
||||||
|
CV MAÎTRE — Bastien Chanot
|
||||||
|
Personnalisation par offre : édite UNIQUEMENT les 3 zones marquées
|
||||||
|
▼ ZONE 1 : TITRE (la ligne sous le nom)
|
||||||
|
▼ ZONE 2 : ACCROCHE (2-3 lignes d'objectif, à adapter à l'entreprise)
|
||||||
|
▼ ZONE 3 : ORDRE (réordonne les <li> / blocs compétences selon l'offre)
|
||||||
|
Le reste (faits, dates, réalisations) ne bouge pas.
|
||||||
|
TODO une fois pour toutes : remplace les URLs LinkedIn / GitHub réelles
|
||||||
|
dans la ligne de contact (cherche "TODO-LIENS").
|
||||||
|
Palette reprise du CV v2.
|
||||||
|
=====================================================================
|
||||||
|
-->
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Bastien Chanot — CV</title>
|
||||||
|
<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=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:ital,wght@0,400;0,500;0,600;0,700;1,400&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
:root{
|
||||||
|
--ink:#14201a;
|
||||||
|
--paper:#eeebe0;
|
||||||
|
--band:#0d1b12;
|
||||||
|
--band2:#16301f;
|
||||||
|
--name:#ffffff;
|
||||||
|
--sage:#61ab7f;
|
||||||
|
--band-muted:#b7bbb8;
|
||||||
|
--green-deep:#1b5e3b;
|
||||||
|
--muted:#586b60;
|
||||||
|
--rule:#d6d2c3;
|
||||||
|
--chip-bg:#e6e2d4;
|
||||||
|
--chip-bd:#cfcabb;
|
||||||
|
--chip-ink:#1b5e3b;
|
||||||
|
}
|
||||||
|
|
||||||
|
*{ box-sizing:border-box; margin:0; padding:0; }
|
||||||
|
html{ background:#dcdfda; }
|
||||||
|
|
||||||
|
body{
|
||||||
|
font-family:"IBM Plex Sans", system-ui, "Segoe UI", Roboto, sans-serif;
|
||||||
|
color:var(--ink);
|
||||||
|
font-size:10.4pt;
|
||||||
|
line-height:1.42;
|
||||||
|
-webkit-font-smoothing:antialiased;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page{
|
||||||
|
background:var(--paper);
|
||||||
|
width:210mm;
|
||||||
|
min-height:297mm;
|
||||||
|
margin:24px auto;
|
||||||
|
padding:0;
|
||||||
|
box-shadow:0 8px 40px rgba(0,0,0,.18);
|
||||||
|
overflow:hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- HEADER BAND (full-bleed) ---------- */
|
||||||
|
.band{
|
||||||
|
background:linear-gradient(125deg, var(--band) 0%, var(--band2) 100%);
|
||||||
|
padding:11mm 14mm 8mm;
|
||||||
|
}
|
||||||
|
.head{
|
||||||
|
display:flex;
|
||||||
|
justify-content:space-between;
|
||||||
|
align-items:flex-start;
|
||||||
|
gap:18px;
|
||||||
|
}
|
||||||
|
.name{
|
||||||
|
font-size:26pt;
|
||||||
|
font-weight:700;
|
||||||
|
letter-spacing:-.02em;
|
||||||
|
line-height:1;
|
||||||
|
color:var(--name);
|
||||||
|
}
|
||||||
|
.name em{ font-style:normal; color:var(--sage); }
|
||||||
|
.title{
|
||||||
|
font-family:"IBM Plex Mono", ui-monospace, monospace;
|
||||||
|
font-size:9.2pt;
|
||||||
|
font-weight:500;
|
||||||
|
letter-spacing:.05em;
|
||||||
|
color:var(--sage);
|
||||||
|
text-transform:uppercase;
|
||||||
|
margin-top:7px;
|
||||||
|
}
|
||||||
|
.contact{
|
||||||
|
text-align:right;
|
||||||
|
font-family:"IBM Plex Mono", ui-monospace, monospace;
|
||||||
|
font-size:8.1pt;
|
||||||
|
line-height:1.75;
|
||||||
|
color:var(--band-muted);
|
||||||
|
white-space:nowrap;
|
||||||
|
}
|
||||||
|
.contact a{ color:var(--sage); text-decoration:none; }
|
||||||
|
.contact .strong{ color:var(--name); font-weight:500; }
|
||||||
|
|
||||||
|
/* ---------- CONTENT WRAPPER ---------- */
|
||||||
|
.content{ padding:13px 14mm 0; }
|
||||||
|
|
||||||
|
.status{
|
||||||
|
font-family:"IBM Plex Mono", ui-monospace, monospace;
|
||||||
|
font-size:8.2pt;
|
||||||
|
font-weight:500;
|
||||||
|
letter-spacing:.04em;
|
||||||
|
color:var(--sage);
|
||||||
|
margin-top:6px;
|
||||||
|
}
|
||||||
|
.avail{
|
||||||
|
font-family:"IBM Plex Mono", ui-monospace, monospace;
|
||||||
|
font-size:8.3pt;
|
||||||
|
color:var(--muted);
|
||||||
|
margin:2px 0 12px 0;
|
||||||
|
}
|
||||||
|
.avail b{ color:var(--green-deep); font-weight:600; }
|
||||||
|
|
||||||
|
/* ---------- PROFIL ---------- */
|
||||||
|
.profil{ margin-top:13px;
|
||||||
|
font-size:10.2pt;
|
||||||
|
line-height:1.5;
|
||||||
|
color:#22302a;
|
||||||
|
border-left:3px solid var(--green-deep);
|
||||||
|
padding-left:12px;
|
||||||
|
margin:0 0 15px 0;
|
||||||
|
max-width:172mm;
|
||||||
|
}
|
||||||
|
.profil strong{ color:var(--green-deep); font-weight:600; }
|
||||||
|
|
||||||
|
/* ---------- SECTION HEADERS ---------- */
|
||||||
|
.sec{
|
||||||
|
display:flex;
|
||||||
|
align-items:center;
|
||||||
|
gap:10px;
|
||||||
|
margin:0 0 9px 0;
|
||||||
|
break-after:avoid;
|
||||||
|
}
|
||||||
|
.sec h2{
|
||||||
|
font-size:10.5pt;
|
||||||
|
font-weight:600;
|
||||||
|
letter-spacing:.13em;
|
||||||
|
text-transform:uppercase;
|
||||||
|
color:var(--ink);
|
||||||
|
white-space:nowrap;
|
||||||
|
}
|
||||||
|
.sec .tok{
|
||||||
|
font-family:"IBM Plex Mono", ui-monospace, monospace;
|
||||||
|
font-size:8pt;
|
||||||
|
color:var(--green-deep);
|
||||||
|
font-weight:500;
|
||||||
|
}
|
||||||
|
.sec .line{ flex:1; height:1px; background:var(--rule); }
|
||||||
|
|
||||||
|
section{ margin-bottom:14px; }
|
||||||
|
|
||||||
|
/* ---------- COMPÉTENCES ---------- */
|
||||||
|
.skills{ margin-left:2px; }
|
||||||
|
.skill-row{
|
||||||
|
display:grid;
|
||||||
|
grid-template-columns:34mm 1fr;
|
||||||
|
gap:8px;
|
||||||
|
padding:2.5px 0;
|
||||||
|
align-items:baseline;
|
||||||
|
}
|
||||||
|
.skill-row + .skill-row{ border-top:1px solid var(--rule); }
|
||||||
|
.skill-k{
|
||||||
|
font-family:"IBM Plex Mono", ui-monospace, monospace;
|
||||||
|
font-size:8.2pt;
|
||||||
|
font-weight:500;
|
||||||
|
color:var(--green-deep);
|
||||||
|
letter-spacing:.01em;
|
||||||
|
}
|
||||||
|
.skill-v{ font-size:9.6pt; color:#22302a; }
|
||||||
|
.skill-v .sep{ color:var(--chip-bd); padding:0 1px; }
|
||||||
|
|
||||||
|
/* ---------- EXPÉRIENCE ---------- */
|
||||||
|
.entry{ margin-bottom:11px; }
|
||||||
|
.entry-top, .entry-role{ break-after:avoid; }
|
||||||
|
.entry-top{
|
||||||
|
display:flex;
|
||||||
|
justify-content:space-between;
|
||||||
|
align-items:baseline;
|
||||||
|
gap:12px;
|
||||||
|
}
|
||||||
|
.entry-co{ font-size:12pt; font-weight:600; color:var(--ink); }
|
||||||
|
.entry-date{
|
||||||
|
font-family:"IBM Plex Mono", ui-monospace, monospace;
|
||||||
|
font-size:8.4pt;
|
||||||
|
color:var(--muted);
|
||||||
|
white-space:nowrap;
|
||||||
|
}
|
||||||
|
.entry-role{
|
||||||
|
font-size:9.7pt;
|
||||||
|
color:var(--green-deep);
|
||||||
|
font-weight:500;
|
||||||
|
margin:1px 0 5px;
|
||||||
|
}
|
||||||
|
.entry-role .meta{ color:var(--muted); font-weight:400; }
|
||||||
|
ul.bul{ list-style:none; orphans:2; widows:2; }
|
||||||
|
ul.bul li{
|
||||||
|
list-style:"▸ ";
|
||||||
|
margin-left:13px;
|
||||||
|
margin-bottom:3.5px;
|
||||||
|
font-size:9.7pt;
|
||||||
|
line-height:1.4;
|
||||||
|
color:#212d27;
|
||||||
|
break-inside:avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.bul li b{ color:var(--ink); font-weight:600; }
|
||||||
|
code.k{
|
||||||
|
font-family:"IBM Plex Mono", ui-monospace, monospace;
|
||||||
|
font-size:8.4pt;
|
||||||
|
background:var(--chip-bg);
|
||||||
|
color:var(--chip-ink);
|
||||||
|
border:1px solid var(--chip-bd);
|
||||||
|
padding:.5px 4px;
|
||||||
|
border-radius:3px;
|
||||||
|
white-space:nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- PROJETS / FORMATION grid ---------- */
|
||||||
|
.two{ display:grid; grid-template-columns:1fr 1fr; gap:9px 22px; }
|
||||||
|
.blk-h{ font-size:10.2pt; font-weight:600; color:var(--ink); }
|
||||||
|
.blk-sub{
|
||||||
|
font-family:"IBM Plex Mono", ui-monospace, monospace;
|
||||||
|
font-size:8pt; color:var(--muted); margin:1px 0 4px;
|
||||||
|
}
|
||||||
|
.blk p{ font-size:9.4pt; line-height:1.4; color:#28332d; }
|
||||||
|
.blk a{ color:var(--green-deep); text-decoration:none; }
|
||||||
|
|
||||||
|
.langs{ font-size:9.8pt; color:#22302a; margin-left:2px; }
|
||||||
|
.langs b{ color:var(--ink); font-weight:600; }
|
||||||
|
.langs .sep{ color:var(--chip-bd); padding:0 6px; }
|
||||||
|
|
||||||
|
/* ---------- SCREEN FOOTER BAND ---------- */
|
||||||
|
.screen-foot{
|
||||||
|
background:var(--band);
|
||||||
|
color:#7e8b84;
|
||||||
|
font-family:"IBM Plex Mono", ui-monospace, monospace;
|
||||||
|
font-size:7.6pt;
|
||||||
|
padding:7px 14mm;
|
||||||
|
display:flex;
|
||||||
|
justify-content:space-between;
|
||||||
|
margin-top:18px;
|
||||||
|
}
|
||||||
|
.screen-foot a, .pdf-foot a{ color:inherit; text-decoration:none; }
|
||||||
|
|
||||||
|
/* ---------- PDF BACKDROP + FOOTER ---------- */
|
||||||
|
.bg{ display:none; }
|
||||||
|
.pdf-foot{ display:none; }
|
||||||
|
|
||||||
|
/* ---------- PRINT / PDF ---------- */
|
||||||
|
/* marges : 14mm haut (sauf p.1) · 0 L/R (bleed) · 13mm bas (réserve footer) */
|
||||||
|
@page{ size:A4; margin:14mm 0 13mm 0; }
|
||||||
|
@page :first{ margin-top:0; }
|
||||||
|
@media print{
|
||||||
|
html{ background:#fff; }
|
||||||
|
.two{ display:block; }
|
||||||
|
.two .blk{ margin-bottom:7px; break-inside:avoid; }
|
||||||
|
.page{ width:auto; min-height:0; margin:0; box-shadow:none; overflow:visible; background:transparent; }
|
||||||
|
.screen-foot{ display:none; }
|
||||||
|
/* fond crème pleine feuille, répété chaque page (déborde dans les marges) */
|
||||||
|
.bg{
|
||||||
|
display:block; position:fixed;
|
||||||
|
top:-14mm; bottom:-13mm; left:0; right:0;
|
||||||
|
background:var(--paper);
|
||||||
|
z-index:-1;
|
||||||
|
}
|
||||||
|
.pdf-foot{
|
||||||
|
display:flex;
|
||||||
|
position:fixed;
|
||||||
|
bottom:-13mm; left:0; right:0;
|
||||||
|
height:9mm;
|
||||||
|
align-items:center;
|
||||||
|
justify-content:space-between;
|
||||||
|
padding:0 14mm;
|
||||||
|
background:var(--band);
|
||||||
|
color:#8a968f;
|
||||||
|
font-family:"IBM Plex Mono", monospace;
|
||||||
|
font-size:7.4pt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="page">
|
||||||
|
<div class="bg"></div>
|
||||||
|
|
||||||
|
<!-- ===== HEADER BAND ===== -->
|
||||||
|
<div class="band">
|
||||||
|
<header class="head">
|
||||||
|
<div>
|
||||||
|
<div class="name">Bastien <em>Chanot</em></div>
|
||||||
|
<!-- ▼ ZONE 1 : TITRE — adapte au poste visé (format court, séparé par ·) -->
|
||||||
|
<div class="title">Développeur Systèmes & Backend · C · Rust · Linux</div>
|
||||||
|
<div class="status">CDI · disponible</div>
|
||||||
|
<!-- ▲ ZONE 1 -->
|
||||||
|
</div>
|
||||||
|
<div class="contact">
|
||||||
|
<a href="tel:+33778822297" class="strong">+33 7 78 82 22 97</a><br>
|
||||||
|
<a href="mailto:bastien@bchanot.fr">bastien@bchanot.fr</a><br>
|
||||||
|
<a href="https://bchanot.fr">bchanot.fr</a> · <a href="https://git.bchanot.fr/bchanot">git.bchanot.fr</a><br>
|
||||||
|
<!-- TODO-LIENS : remplace par tes URLs réelles -->
|
||||||
|
<a href="https://www.linkedin.com/in/bastien-chanot-4075a0a3/">linkedin.com/in/bastien-chanot</a><br><a href="https://github.com/bchanot">github.com/bchanot</a><br>
|
||||||
|
Loire-Atlantique · Vendée · Morbihan<br>full remote ou hybride<br>Permis B & A
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ===== CONTENT ===== -->
|
||||||
|
<div class="content">
|
||||||
|
|
||||||
|
<!-- ▼ ZONE 2 : ACCROCHE — réécris 2-3 lignes orientées vers l'entreprise / le poste -->
|
||||||
|
<p class="profil">
|
||||||
|
Développeur <strong>systèmes & backend</strong>, 7 ans en <strong>C et Rust sur Linux bare-metal</strong>. Du <strong>module kernel</strong> au backend temps réel : drivers, conteneurs (Docker / LXC / QEMU), exploitation d'une <strong>fleet GPU bare-metal</strong> en production, automatisation et CI/CD. Compréhension globale de la stack, du matériel au service.
|
||||||
|
</p>
|
||||||
|
<!-- ▲ ZONE 2 -->
|
||||||
|
|
||||||
|
<!-- ================= COMPÉTENCES ================= -->
|
||||||
|
<section>
|
||||||
|
<div class="sec"><span class="tok">~/</span><h2>Compétences techniques</h2><span class="line"></span></div>
|
||||||
|
<!-- ▼ ZONE 3a : réordonne les lignes selon l'offre -->
|
||||||
|
<div class="skills">
|
||||||
|
<div class="skill-row"><div class="skill-k">Langages</div><div class="skill-v">C <span class="sep">·</span> Rust <span class="sep">·</span> C++ <span class="sep">·</span> Bash <span class="sep">·</span> Python <span class="sep">·</span> Java (AOSP/Android)</div></div>
|
||||||
|
<div class="skill-row"><div class="skill-k">Systèmes & Embarqué</div><div class="skill-v">Linux kernel drivers <span class="sep">·</span> AOSP <span class="sep">·</span> ARM / x86 <span class="sep">·</span> GPIO <span class="sep">·</span> NFC <span class="sep">·</span> cross-compilation GCC <span class="sep">·</span> systemd <span class="sep">·</span> SELinux</div></div>
|
||||||
|
<div class="skill-row"><div class="skill-k">Conteneurs / Virtu.</div><div class="skill-v">Docker <span class="sep">·</span> LXC / LXD <span class="sep">·</span> QEMU <span class="sep">·</span> cgroups <span class="sep">·</span> namespaces</div></div>
|
||||||
|
<div class="skill-row"><div class="skill-k">Cloud & Infra</div><div class="skill-v">AWS (EC2, g4dn bare-metal, IAM, S3, CloudWatch) <span class="sep">·</span> Scaleway <span class="sep">·</span> OVH / Hetzner <span class="sep">·</span> Nginx</div></div>
|
||||||
|
<div class="skill-row"><div class="skill-k">DevOps & Outils</div><div class="skill-v">Git <span class="sep">·</span> GitHub Actions <span class="sep">·</span> GitLab CI <span class="sep">·</span> CI/CD <span class="sep">·</span> Claude Code (agents/skills custom) <span class="sep">·</span> N8N</div></div>
|
||||||
|
</div>
|
||||||
|
<!-- ▲ ZONE 3a -->
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ================= EXPÉRIENCE ================= -->
|
||||||
|
<section>
|
||||||
|
<div class="sec"><span class="tok">~/</span><h2>Expérience professionnelle</h2><span class="line"></span></div>
|
||||||
|
|
||||||
|
<div class="entry">
|
||||||
|
<div class="entry-top"><div class="entry-co">ZenQuality</div><div class="entry-date">2026 — présent</div></div>
|
||||||
|
<div class="entry-role">Développeur indépendant · Infra & dév web <span class="meta">· zenquality.fr</span></div>
|
||||||
|
<ul class="bul">
|
||||||
|
<li>Déployé et opéré en production l'<b>infrastructure auto-hébergée</b> (uptime continu) en conteneurisant une stack <code class="k">Astro</code> <code class="k">React</code> <code class="k">PHP 8</code> <code class="k">PostgreSQL</code> sur VPS <code class="k">Scaleway</code>, avec pipeline de déploiement automatisé.</li>
|
||||||
|
<li>Livré <b>5 projets clients de bout en bout</b> depuis avril 2026 — conception, intégration, déploiement, hébergement et support continu.</li>
|
||||||
|
<li>Réalisé des <b>audits techniques</b> (Core Web Vitals, Schema.org) et la <b>mise en conformité légale</b> (RGPD, CGV B2B/B2C, médiateur CM2C) pour des PME — plan d'action sur 12 sprints.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="entry">
|
||||||
|
<div class="entry-top"><div class="entry-co">CareGame</div><div class="entry-date">2019 — 2025</div></div>
|
||||||
|
<div class="entry-role">Développeur logiciel · Systèmes & Backend <span class="meta">· Paris · full remote dès 2020</span></div>
|
||||||
|
<!-- ▼ ZONE 3b : remonte les bullets les plus alignés avec l'offre -->
|
||||||
|
<ul class="bul">
|
||||||
|
<li>Conçu et maintenu les <b>modules kernel Linux en C</b> (x86 / ARM) assurant la communication bidirectionnelle hôte ↔ instances AOSP conteneurisées — drivers d'interface, brique critique du pipeline d'exécution serveur.</li>
|
||||||
|
<li>Co-développé le <b>backend Rust</b> orchestrant le cycle de vie des conteneurs AOSP (<code class="k">WebSocket</code> temps réel clients ↔ instances, scheduling sur la fleet GPU, intégration <code class="k">Docker</code> + <code class="k">LXC</code>) — support de <b>plusieurs centaines de joueurs simultanés</b>.</li>
|
||||||
|
<li>Porté la densité de production à <b>32 sessions de jeu AAA stables par serveur</b> en concevant l'isolation <b>CPU/GPU par session</b> — adaptation de modules GPU <code class="k">Nvidia</code>, partitionnement 2 cœurs/session (physiques et émulés), sérialisation des accès concurrents sur zones GPU partagées.</li>
|
||||||
|
<li>Architecturé et exploité une <b>fleet GPU bare-metal</b> <code class="k">AWS g4dn.metal</code> (8× T4, 64 vCPU, ~20 serveurs en pic) servant plusieurs centaines de joueurs en parallèle — isolation 2 cœurs CPU/session, ramdisk I/O (Asphalt 9 : 3 sessions / T4).</li>
|
||||||
|
<li>Industrialisé jusqu'en production un <b>PoC LXD + Docker</b> issu d'une R&D Nvidia — débogage kernel/conteneur, performance validée par les équipes Nvidia comme dépassant le scope initial du PoC.</li>
|
||||||
|
<li>Collaboré techniquement en <b>anglais</b> avec <b>Canonical</b> (Anbox, builds LXC/LXD non commerciaux, remontée bugs et feature requests) et <b>Ampere Computing</b> (benchmark de serveurs ARM pré-commerciaux pour évaluation de migration de fleet).</li>
|
||||||
|
<li>Automatisé l'installation des jeux AOSP et la persistance des sauvegardes (fusion Android Backup + scripts custom couvrant DRM et données externes) et développé un <b>outil d'orchestration Bash modulaire (1000+ lignes)</b> appelé par la CI et le backend Rust.</li>
|
||||||
|
<li>Atteint une <b>latence compatible gameplay AAA temps réel</b> en développant les virtual input devices AOSP en <code class="k">Java</code> (touchscreen, gamepad) sur le pipeline complet frontend → backend Rust → drivers hôtes → injection AOSP.</li>
|
||||||
|
</ul>
|
||||||
|
<!-- ▲ ZONE 3b -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="entry">
|
||||||
|
<div class="entry-top"><div class="entry-co">Deewee</div><div class="entry-date">2017</div></div>
|
||||||
|
<div class="entry-role">Développeur C · Système embarqué <span class="meta">· Ivry-sur-Seine · stage 42 (6 mois) puis CDD (4 mois)</span></div>
|
||||||
|
<ul class="bul">
|
||||||
|
<li>Développé en <b>C</b> le logiciel embarqué d'un boîtier connecté <code class="k">Orange Pi</code> (Debian ARM) interceptant le flux <b>ESC/POS</b> d'une imprimante thermique pour générer le PNG du ticket et le transférer en WiFi direct vers mobile.</li>
|
||||||
|
<li>Intégré le matériel : <b>GPIO</b> physique (bouton + timeout), hotspot WiFi embarqué avec appairage automatique par diffusion <b>NFC</b> des credentials.</li>
|
||||||
|
<li>Travaillé en cycle court sur un prototype fonctionnel — cross-compilation ARM, débogage sur cible, itérations rapides entre intégration matérielle et logiciel embarqué.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ================= PROJETS ================= -->
|
||||||
|
<section>
|
||||||
|
<div class="sec"><span class="tok">~/</span><h2>Projets & réalisations</h2><span class="line"></span></div>
|
||||||
|
<div class="two">
|
||||||
|
<div class="blk">
|
||||||
|
<div class="blk-h">Homelab — infrastructure personnelle</div>
|
||||||
|
<div class="blk-sub">en continu</div>
|
||||||
|
<p>Auto-hébergement Git / DNS / VPN / SMB — NAS Asustor, WireGuard site-to-site, Pi-hole, segmentation réseau, hardening fail2ban, gocryptfs sur dossiers sensibles.</p>
|
||||||
|
</div>
|
||||||
|
<div class="blk">
|
||||||
|
<div class="blk-h">Code source & projets persos</div>
|
||||||
|
<div class="blk-sub"><a href="https://git.bchanot.fr/bchanot">git.bchanot.fr/bchanot</a></div>
|
||||||
|
<p>Serveur Git auto-hébergé en production. Configuration Claude Code (agents/skills custom), dotfiles, projets bas-niveau C / Rust. Mirror automatique vers GitHub via push hook.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ================= FORMATION ================= -->
|
||||||
|
<section>
|
||||||
|
<div class="sec"><span class="tok">~/</span><h2>Formation</h2><span class="line"></span></div>
|
||||||
|
<div class="two">
|
||||||
|
<div class="blk">
|
||||||
|
<div class="blk-h">École 42 — programmation informatique</div>
|
||||||
|
<div class="blk-sub">2015 — 2019 · Clichy</div>
|
||||||
|
<p>Kernel / systèmes (ft_linux, kfs-1, drivers, malloc), bas niveau (nm, 42sh POSIX, ft_ls), sécurité & algorithmie (snow crash, ft_ssl_md5, lem-in).</p>
|
||||||
|
</div>
|
||||||
|
<div class="blk">
|
||||||
|
<div class="blk-h">TSRIT — Next Formation</div>
|
||||||
|
<div class="blk-sub">2013 — 2015 · Vincennes · Félicitations du jury</div>
|
||||||
|
<p>BTS Technicien Supérieur Réseaux & Télécoms. Socle réseau (OSI, TCP/IP), administration Linux / Windows Server, virtualisation.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ================= LANGUES ================= -->
|
||||||
|
<section>
|
||||||
|
<div class="sec"><span class="tok">~/</span><h2>Langues</h2><span class="line"></span></div>
|
||||||
|
<div class="langs"><b>Anglais</b> C2 <span class="sep">·</span> <b>Espagnol</b> B1 <span class="sep">·</span> <b>Français</b> natif</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</div><!-- /content -->
|
||||||
|
|
||||||
|
<div class="screen-foot"><a href="https://bchanot.fr">bchanot.fr</a><span>Bastien Chanot · CV 2026</span></div>
|
||||||
|
<div class="pdf-foot"><a href="https://bchanot.fr">bchanot.fr</a><span>Bastien Chanot · CV 2026</span></div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Binary file not shown.
+323
-602
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+1
-1
@@ -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
|
||||||
|
|||||||
@@ -73,6 +73,9 @@ Strict palette (non-negotiable):
|
|||||||
| `#dff0e7` | Green tint — pill background |
|
| `#dff0e7` | Green tint — pill background |
|
||||||
| `#f5f3ec` | Parchment — page background |
|
| `#f5f3ec` | Parchment — page background |
|
||||||
|
|
||||||
|
Plus a documented set of functional neutrals (text inks, rules/tags, two
|
||||||
|
green-scale intermediates) — the exhaustive list lives in `CLAUDE.md`.
|
||||||
|
|
||||||
Typography:
|
Typography:
|
||||||
- `Fraunces` — display (names, titles)
|
- `Fraunces` — display (names, titles)
|
||||||
- `JetBrains Mono` — technical labels, badges, pills, nav, contact
|
- `JetBrains Mono` — technical labels, badges, pills, nav, contact
|
||||||
@@ -85,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
@@ -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.
|
||||||
|
|||||||
+31
-91
@@ -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 */
|
||||||
@@ -344,7 +344,6 @@
|
|||||||
.reveal.d3 { animation-delay: .30s; }
|
.reveal.d3 { animation-delay: .30s; }
|
||||||
.reveal.d4 { animation-delay: .42s; }
|
.reveal.d4 { animation-delay: .42s; }
|
||||||
.reveal.d5 { animation-delay: .55s; }
|
.reveal.d5 { animation-delay: .55s; }
|
||||||
.reveal.d6 { animation-delay: .68s; }
|
|
||||||
@keyframes rise {
|
@keyframes rise {
|
||||||
to { opacity: 1; transform: translateY(0); }
|
to { opacity: 1; transform: translateY(0); }
|
||||||
}
|
}
|
||||||
@@ -419,39 +418,49 @@
|
|||||||
gap: 20px;
|
gap: 20px;
|
||||||
margin-top: 40px;
|
margin-top: 40px;
|
||||||
}
|
}
|
||||||
.stack-card {
|
/* ── Shared card chrome (stack / project / theme cards + méthode items).
|
||||||
|
Per-class blocks below keep only their specifics; the cascade resolves
|
||||||
|
identically to the previous duplicated declarations. ── */
|
||||||
|
.stack-card, .project-card, .theme-card, .methode-item {
|
||||||
background: var(--page);
|
background: var(--page);
|
||||||
border: 1px solid var(--rule);
|
border: 1px solid var(--rule);
|
||||||
border-radius: var(--r-md);
|
border-radius: var(--r-md);
|
||||||
padding: 24px;
|
|
||||||
transition: border-color .25s ease, transform .25s ease, box-shadow .25s ease;
|
transition: border-color .25s ease, transform .25s ease, box-shadow .25s ease;
|
||||||
}
|
}
|
||||||
.stack-card:hover {
|
.stack-card:hover, .project-card:hover, .theme-card:hover, .methode-item:hover {
|
||||||
border-color: var(--g300);
|
border-color: var(--g300);
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
box-shadow: var(--shadow-md);
|
box-shadow: var(--shadow-md);
|
||||||
}
|
}
|
||||||
.stack-card-head {
|
.stack-card-head, .project-card-head, .theme-card-head {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-bottom: 16px;
|
|
||||||
padding-bottom: 12px;
|
|
||||||
border-bottom: 1px dashed var(--rule);
|
border-bottom: 1px dashed var(--rule);
|
||||||
}
|
}
|
||||||
.stack-card h3 {
|
.stack-card h3, .project-card h3, .theme-card h4, .methode-body h3 {
|
||||||
font-family: var(--serif);
|
font-family: var(--serif);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 19px;
|
|
||||||
color: var(--ink-1);
|
color: var(--ink-1);
|
||||||
letter-spacing: -0.01em;
|
letter-spacing: -0.01em;
|
||||||
}
|
}
|
||||||
.stack-card-tag {
|
.stack-card-tag, .project-card-tag, .theme-card-tag {
|
||||||
font-family: var(--mono);
|
font-family: var(--mono);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: var(--g500);
|
color: var(--g500);
|
||||||
letter-spacing: 0.1em;
|
letter-spacing: 0.1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.stack-card {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
.stack-card-head {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
}
|
||||||
|
.stack-card h3 {
|
||||||
|
font-size: 19px;
|
||||||
|
}
|
||||||
.pills {
|
.pills {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -493,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); } }
|
||||||
@@ -641,40 +650,19 @@
|
|||||||
}
|
}
|
||||||
@media (min-width: 768px) { .projects-grid { grid-template-columns: repeat(2, 1fr); } }
|
@media (min-width: 768px) { .projects-grid { grid-template-columns: repeat(2, 1fr); } }
|
||||||
.project-card {
|
.project-card {
|
||||||
background: var(--page);
|
|
||||||
border: 1px solid var(--rule);
|
|
||||||
border-radius: var(--r-md);
|
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
transition: border-color .25s ease, transform .25s ease, box-shadow .25s ease;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
.project-card:hover {
|
|
||||||
border-color: var(--g300);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: var(--shadow-md);
|
|
||||||
}
|
|
||||||
.project-card-head {
|
.project-card-head {
|
||||||
display: flex;
|
|
||||||
align-items: baseline;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
border-bottom: 1px dashed var(--rule);
|
|
||||||
}
|
}
|
||||||
.project-card h3 {
|
.project-card h3 {
|
||||||
font-family: var(--serif);
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
color: var(--ink-1);
|
|
||||||
letter-spacing: -0.01em;
|
|
||||||
}
|
}
|
||||||
.project-card-tag {
|
.project-card-tag {
|
||||||
font-family: var(--mono);
|
|
||||||
font-size: 11px;
|
|
||||||
color: var(--g500);
|
|
||||||
letter-spacing: 0.1em;
|
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
@@ -709,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);
|
||||||
@@ -732,41 +718,20 @@
|
|||||||
@media (min-width: 1200px) { .formation-themes { grid-template-columns: repeat(3, 1fr); } }
|
@media (min-width: 1200px) { .formation-themes { grid-template-columns: repeat(3, 1fr); } }
|
||||||
|
|
||||||
.theme-card {
|
.theme-card {
|
||||||
background: var(--page);
|
|
||||||
border: 1px solid var(--rule);
|
|
||||||
border-radius: var(--r-md);
|
|
||||||
padding: 22px;
|
padding: 22px;
|
||||||
transition: border-color .25s ease, transform .25s ease, box-shadow .25s ease;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
.theme-card:hover {
|
|
||||||
border-color: var(--g300);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: var(--shadow-md);
|
|
||||||
}
|
|
||||||
.theme-card-head {
|
.theme-card-head {
|
||||||
display: flex;
|
|
||||||
align-items: baseline;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
border-bottom: 1px dashed var(--rule);
|
|
||||||
}
|
}
|
||||||
.theme-card h4 {
|
.theme-card h4 {
|
||||||
font-family: var(--serif);
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
color: var(--ink-1);
|
|
||||||
letter-spacing: -0.01em;
|
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
.theme-card-tag {
|
.theme-card-tag {
|
||||||
font-family: var(--mono);
|
|
||||||
font-size: 11px;
|
|
||||||
color: var(--g500);
|
|
||||||
letter-spacing: 0.1em;
|
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.theme-quote {
|
.theme-quote {
|
||||||
@@ -791,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;
|
||||||
@@ -861,16 +817,7 @@
|
|||||||
grid-template-columns: 56px 1fr;
|
grid-template-columns: 56px 1fr;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
background: var(--page);
|
|
||||||
border: 1px solid var(--rule);
|
|
||||||
border-radius: var(--r-md);
|
|
||||||
padding: 22px 24px;
|
padding: 22px 24px;
|
||||||
transition: border-color .25s ease, transform .25s ease, box-shadow .25s ease;
|
|
||||||
}
|
|
||||||
.methode-item:hover {
|
|
||||||
border-color: var(--g300);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: var(--shadow-md);
|
|
||||||
}
|
}
|
||||||
.methode-num {
|
.methode-num {
|
||||||
font-family: var(--mono);
|
font-family: var(--mono);
|
||||||
@@ -882,11 +829,7 @@
|
|||||||
padding-top: 4px;
|
padding-top: 4px;
|
||||||
}
|
}
|
||||||
.methode-body h3 {
|
.methode-body h3 {
|
||||||
font-family: var(--serif);
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 19px;
|
font-size: 19px;
|
||||||
color: var(--ink-1);
|
|
||||||
letter-spacing: -0.01em;
|
|
||||||
margin-bottom: 6px;
|
margin-bottom: 6px;
|
||||||
line-height: 1.25;
|
line-height: 1.25;
|
||||||
}
|
}
|
||||||
@@ -918,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;
|
||||||
}
|
}
|
||||||
@@ -973,7 +913,7 @@
|
|||||||
|
|
||||||
/* ── FOOTER ── */
|
/* ── FOOTER ── */
|
||||||
.footer {
|
.footer {
|
||||||
background: #061008;
|
background: var(--dark);
|
||||||
color: rgba(223, 240, 231, 0.55);
|
color: rgba(223, 240, 231, 0.55);
|
||||||
padding: 32px 24px;
|
padding: 32px 24px;
|
||||||
border-top: 1px solid rgba(106,185,138,0.1);
|
border-top: 1px solid rgba(106,185,138,0.1);
|
||||||
@@ -1045,11 +985,11 @@
|
|||||||
<div class="hero-cta reveal d4">
|
<div class="hero-cta reveal d4">
|
||||||
<a class="btn btn-primary" href="#contact">
|
<a class="btn btn-primary" href="#contact">
|
||||||
Me contacter
|
Me contacter
|
||||||
<svg class="arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="19" x2="19" y2="5"/><polyline points="8 5 19 5 19 16"/></svg>
|
<svg class="arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="5" y1="19" x2="19" y2="5"/><polyline points="8 5 19 5 19 16"/></svg>
|
||||||
</a>
|
</a>
|
||||||
<a class="btn btn-secondary" href="CV_Bastien_Chanot.html">
|
<a class="btn btn-secondary" href="CV_Bastien_Chanot.html">
|
||||||
Voir le CV
|
Voir le CV
|
||||||
<svg class="arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="19" x2="19" y2="5"/><polyline points="8 5 19 5 19 16"/></svg>
|
<svg class="arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="5" y1="19" x2="19" y2="5"/><polyline points="8 5 19 5 19 16"/></svg>
|
||||||
</a>
|
</a>
|
||||||
<a class="btn btn-secondary" href="CV_Bastien_Chanot.pdf" download>
|
<a class="btn btn-secondary" href="CV_Bastien_Chanot.pdf" download>
|
||||||
Télécharger PDF
|
Télécharger PDF
|
||||||
@@ -1076,17 +1016,17 @@
|
|||||||
<p>Ce qui m'intéresse, c'est descendre jusqu'à ce qu'il n'y ait plus de magie — <strong>kernel, hardware, drivers</strong>. Là, soit ça marche, soit ça ne marche pas.</p>
|
<p>Ce qui m'intéresse, c'est descendre jusqu'à ce qu'il n'y ait plus de magie — <strong>kernel, hardware, drivers</strong>. Là, soit ça marche, soit ça ne marche pas.</p>
|
||||||
<p>C'est ce confort-là que je cherche dans une équipe : <strong>systèmes, embarqué, backend bas niveau</strong>, sur une stack dont on peut lire le code source. Pas envie d'aller vers le buzzword-driven — microservices à tout prix, framework du mois, archi conçue pour le pitch deck.</p>
|
<p>C'est ce confort-là que je cherche dans une équipe : <strong>systèmes, embarqué, backend bas niveau</strong>, sur une stack dont on peut lire le code source. Pas envie d'aller vers le buzzword-driven — microservices à tout prix, framework du mois, archi conçue pour le pitch deck.</p>
|
||||||
<p>Aujourd'hui indépendant sous la marque <strong>ZenQuality</strong>, mais avant tout en recherche d'un <strong>CDI en systèmes embarqués ou logiciel</strong> — les missions freelance se font en parallèle.</p>
|
<p>Aujourd'hui indépendant sous la marque <strong>ZenQuality</strong>, mais avant tout en recherche d'un <strong>CDI en systèmes embarqués ou logiciel</strong> — les missions freelance se font en parallèle.</p>
|
||||||
<p>Côté présence : <strong>full remote</strong> idéalement, ou <strong>hybride 1 à 2 jours par mois</strong> si l'équipe est à Paris. Mobilité visée à moyen terme : <strong>Pays de la Loire</strong>.</p>
|
<p>Côté présence : <strong>full remote</strong> idéalement, sinon <strong>hybride ou présentiel</strong> en <strong>Loire-Atlantique · Vendée · Morbihan</strong>. Si l'équipe est à Paris, uniquement en <strong>hybride, 1 à 2 jours par mois maximum</strong>.</p>
|
||||||
</div>
|
</div>
|
||||||
<dl class="about-callout">
|
<dl class="about-callout">
|
||||||
<dt>Recherche prioritaire</dt>
|
<dt>Recherche prioritaire</dt>
|
||||||
<dd>CDI systèmes embarqués / logiciel</dd>
|
<dd>CDI systèmes embarqués / logiciel</dd>
|
||||||
<dt>En parallèle</dt>
|
<dt>En parallèle</dt>
|
||||||
<dd>Missions freelance · ZenQuality</dd>
|
<dd>Missions freelance · ZenQuality</dd>
|
||||||
<dt>Localisation actuelle</dt>
|
<dt>Zone</dt>
|
||||||
<dd>Yerres (91) · mobilité Pays de la Loire</dd>
|
<dd>Loire-Atlantique · Vendée · Morbihan</dd>
|
||||||
<dt>Présence</dt>
|
<dt>Présence</dt>
|
||||||
<dd>Full remote · ou 1–2 j/mois si Paris</dd>
|
<dd>Full remote · hybride ou présentiel dans la zone · Paris 1–2 j/mois max</dd>
|
||||||
<dt>Site pro</dt>
|
<dt>Site pro</dt>
|
||||||
<dd><a href="https://zenquality.fr" target="_blank" rel="noopener">zenquality.fr ↗</a></dd>
|
<dd><a href="https://zenquality.fr" target="_blank" rel="noopener">zenquality.fr ↗</a></dd>
|
||||||
</dl>
|
</dl>
|
||||||
@@ -1235,7 +1175,7 @@
|
|||||||
<div class="timeline-meta">
|
<div class="timeline-meta">
|
||||||
<span class="period">avr. 2026 — présent</span>
|
<span class="period">avr. 2026 — présent</span>
|
||||||
<span class="badge">En cours</span>
|
<span class="badge">En cours</span>
|
||||||
<span>Yerres · Full remote</span>
|
<span>Full remote</span>
|
||||||
</div>
|
</div>
|
||||||
<h3><a href="https://zenquality.fr" target="_blank" rel="noopener">ZenQuality</a></h3>
|
<h3><a href="https://zenquality.fr" target="_blank" rel="noopener">ZenQuality</a></h3>
|
||||||
<p class="timeline-role">Développeur indépendant · Systèmes & Backend</p>
|
<p class="timeline-role">Développeur indépendant · Systèmes & Backend</p>
|
||||||
|
|||||||
@@ -8,5 +8,8 @@ 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 + JS are allowed (project convention), fonts from Google.
|
# CSP: inline CSS allowed (single-file convention: inline <style> element);
|
||||||
add_header Content-Security-Policy "default-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; script-src 'self' 'unsafe-inline'; img-src 'self' data:; base-uri 'self'; form-action 'self'; frame-ancestors 'self'" always;
|
# 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.
|
||||||
|
add_header Content-Security-Policy "default-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; script-src 'self' 'sha256-Al1M34KxI6Ye5Viu6aO//7CYyaLzqtpG9GX95FFlSOY='; img-src 'self' data:; base-uri 'self'; form-action 'self'; frame-ancestors 'self'" always;
|
||||||
|
|||||||
+7
-7
@@ -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;
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
1.1.0
|
||||||
Reference in New Issue
Block a user