28 Commits
Author SHA1 Message Date
Bastien Chanot 2891be5898 Merge release/1.1.0 into main 2026-07-23 15:22:56 +02:00
Bastien Chanot 3f6bc571d3 chore(release): 1.1.0 — zone Loire-Atlantique · Vendée · Morbihan 2026-07-23 15:22:56 +02:00
Bastien Chanot 3c4dab88a5 Merge chore/cv-update into develop 2026-07-23 15:22:20 +02:00
Bastien Chanot aceb7e2c70 chore(cv): restructured CV + general variant, zone aligned
Owner-authored CV rework: technical skills promoted above experience,
'Centres d'intérêt' dropped, markup trimmed 656 -> 424 lines. Header
carries the Loire-Atlantique · Vendée · Morbihan zone and the presence
hierarchy (full remote, hybrid or on-site in the zone, Paris capped at
1-2 days/month).

Adds CV_Bastien_CHANOT_General.{html,pdf} as the general variant kept
alongside the served CV. Not linked from the landing and not in the
Dockerfile COPY whitelist, so it ships in the repo only, not to prod.

Both HTML/PDF pairs verified in sync via weasyprint re-render (text
identical, 2 pages each). New files normalized to mode 644.
2026-07-23 15:22:13 +02:00
Bastien Chanot e2f7ea4546 Merge chore/zone-loire-atlantique into develop 2026-07-23 14:56:24 +02:00
Bastien Chanot e13a4b1bb5 chore(profile): align geography to Loire-Atlantique · Vendée · Morbihan
Landing was still advertising Yerres (91) + a planned Nantes relocation,
and a presence rule that omitted on-site work in the target zone.

- about paragraph + callout: zone replaces 'Localisation actuelle';
  presence stated in order of preference — full remote, then hybrid or
  on-site in the zone, Paris only as a fallback at 1-2 days/month max
- ZenQuality timeline entry: drop the 'Yerres' location, keep 'Full remote'
- CLAUDE.md content rules: geography note rewritten to match, with an
  explicit 'never mention Yerres' guard

Verified: no 'Yerres' left outside the negative rule, CSP script hash
unchanged, headless render checked at 375px and 1440px.
2026-07-23 14:39:43 +02:00
Bastien Chanot d8ffa983b5 Merge release/1.0.0 into main 2026-07-06 01:14:47 +02:00
Bastien Chanot 1c01cb20eb Merge release/1.0.0 into develop 2026-07-06 01:14:47 +02:00
Bastien Chanot 6b0ea8494b chore(release): 1.0.0 — version.txt + CHANGELOG (lineage starts) 2026-07-06 01:12:53 +02:00
Bastien Chanot 1d0b7ee8e7 Merge chore/tour-2026-07-05-3 into develop 2026-07-06 01:07:37 +02:00
Bastien Chanot 9af6aa4be8 chore(gitignore): ignore .gstack/ (browse daemon session state, contains tokens) 2026-07-06 01:07:26 +02:00
Bastien Chanot 136e1df5e3 docs(tour): follow-up — 10 residuals closed 2026-07-06 01:06:54 +02:00
Bastien Chanot c8c72c24aa chore(memory): capitalize — LRN-004, EVAL-001, BDR-006 update note, journal 2026-07-06 2026-07-06 01:06:54 +02:00
Bastien Chanot dd8c327162 docs(claude): document white family + CV typography exception (tour J1/J2) 2026-07-06 01:06:54 +02:00
Bastien Chanot 84288c5c58 fix(nginx+compose): dotfile block first, no pdf gzip, single healthcheck (tour J4/N4/J5)
Dotfile location moved above caching regex locations (first match wins).
application/pdf out of gzip_types (already flate-compressed). Compose
healthcheck block removed — image HEALTHCHECK is the single source,
inherited. Oracles: nginx -t, dotfiles 404, PDF no Content-Encoding,
HTML still gzipped, headers 5/5, inherited health = healthy.
2026-07-06 01:06:54 +02:00
Bastien Chanot a589b99878 perf(index): trim unused Google Fonts faces, drop contact-grid no-ops (tour N1/N3)
Fraunces 0,300/0,500/0,700 + DM Sans 300 unused (all serif-300 usages
are italic -> served by 1,400; no strong/em inside serif elements).
.contact-grid grid props no-op around single child. Verified headless
Chromium 375px + 1440px: real italic renders, layout intact, zero
console errors. Inline script untouched, CSP hash unchanged.
2026-07-06 01:06:54 +02:00
Bastien Chanot b86a5129f0 style(cv): french date chips, pill radius, font trim (tour N1/N2/J3)
Chips: avr./mars/fév + en-dash, mirrors landing wording. Tags radius
10px -> 999px (true pills). Fonts URL drops Fraunces 0,300/0,600 +
DM Sans 300 — trim proven render-identical (per-page hash == baseline)
BEFORE the intended chip/radius changes; PDF regenerated, 2 pages
eyeballed.
2026-07-06 01:06:54 +02:00
Bastien Chanot cc65225b3d docs(tour): report — run 2026-07-05-3 (converged, 3 iterations) 2026-07-06 00:51:17 +02:00
Bastien Chanot 2f5e51a1b4 docs: sync README base-image reference (1.28 -> 1.30-alpine) 2026-07-05 22:57:48 +02:00
Bastien Chanot 613bfc0d49 chore(clean): dedup CV/index CSS, drop dead directives (tour F1-F8)
CV: shared block for xp/project/edu headers + date chips + roles + tags,
2 identical inline style attrs -> .inline-link class, no-op body margin/
padding removed, stray blank collapsed. Proven behavior-preserving: PDF
text-hash + per-page render-hash + full byte-identity vs committed PDF.
index: .stack-note/.theme-list code grouped, 2 no-op .formation overrides
removed. nginx.conf: dead 'deny all' after return 404 removed (nginx -t +
dotfile-404 oracle PASS). .dockerignore: phantom nginx.conf.bak entry.
Snippet comment: CV style attrs no longer exist. CSP hash unchanged.
2026-07-05 22:52:39 +02:00
Bastien Chanot 1aa97f0af0 fix(security): bump base to nginx-unprivileged 1.30-alpine — CVE-2026-42945 (tour SEC-1)
1.28 stable branch retired; 2026-05-13 nginx security batch (rewrite-module
buffer overflow, fixed 1.30.1+) never backported to 1.28.x. New digest pin
carries nginx/1.30.3. Verified: build, nginx -t, uid 101, hardened run,
5/5 security headers + HTTP 200 on /, .html, .pdf, favicon.
2026-07-05 22:41:58 +02:00
Bastien Chanot 7967afff08 Merge chore/tour-2026-07-05-2 into develop 2026-07-05 22:20:32 +02:00
Bastien Chanot 7984a7d2df docs(memory): capitalize tour residuals — BDR-006/007, LRN-003, journal, TOUR follow-up
BDR-006 supersedes BDR-004 infra detail (hardened container: nginx-unprivileged
:1.28 / port 8080 / uid 101) — closes reconcile REC-1.
BDR-007 supersedes BDR-003 geo (canonical = Nantes relocation) — records the
CLN-9 owner decision.
LRN-003: prove CSS cleanup behavior-preserving via before/after PDF render-hash.
journal 2026-07-05; TOUR.md follow-up documenting all 5 residuals closed.
2026-07-05 21:28:17 +02:00
Bastien Chanot f5158758b2 content: align landing geo to CV — Nantes relocation (tour CLN-9)
CLAUDE.md requires the profile/job-search state to stay consistent across
index.html and the CV. The CV stated a concrete Nantes relocation + a
hybride-Nantes option the landing lacked. Per owner decision, the CV is
canonical: propagate those facts into the landing (about paragraph +
callout) and update CLAUDE.md's geography note to match. No invented claims
— mirrors what the CV already states. CV unchanged (no PDF regen).
2026-07-05 21:25:16 +02:00
Bastien Chanot ede75765cd style(palette): map 5 off-palette colors to palette tokens (tour CLN-7/8)
Brings both files back inside the CLAUDE.md palette (any color outside the
6 brand hex + documented neutrals is a violation). Nearest-allowed mappings,
minimal visual delta (verified by rendering the CV):
- index .footer bg #061008 -> var(--dark) #0d1b12 (the documented footer color).
- CV .tag border #a8d4bc -> var(--g300) (nearest visible green; keeps the pill outline).
- CV body+print texture rgba(26,71,48,.05) -> rgba(27,94,59,.05) (--g700 green primary).
- CV body+print gradient stops #edeadf/#f2efe6 -> var(--tag)/var(--page).
PDF regenerated (renders 2 pages, layout intact).
2026-07-05 21:24:08 +02:00
Bastien Chanot 607124aa70 fix(a11y): aria-hidden on 2 decorative CTA arrows (tour CLN-6)
The 'Me contacter' and 'Voir le CV' arrow SVGs were missing the
aria-hidden the sibling download arrow already carries; they are purely
decorative, so screen readers should skip them. Visual output unchanged.
2026-07-05 21:22:28 +02:00
Bastien Chanot 7b3d9bec4c docs(tour): report chore/tour-2026-07-05-2 — CONVERGED (2 it., 1 clean fix commit, 5 suggestions) 2026-07-05 21:05:43 +02:00
Bastien Chanot 30b0e44a45 chore(clean): remove dead CSS + normalize whitespace (tour CLN)
index.html: drop unused .reveal.d6 rule (markup uses d1-d5 only).

CV_Bastien_Chanot.html:
- remove dead `position: running(siteFooter)` — no `element()` consumer,
  and .footer-bar is `display:none` in @media print (the @page
  auto-numbered footer replaces it); on screen running() is an invalid
  position value, ignored.
- remove no-op `box-shadow: none` on .page (weasyprint ignores box-shadow;
  .page sets a shadow nowhere).
- remove dead `.skills-grid { font-size: 8.4pt }` (every direct child is a
  .skill-label/.skill-values div that sets its own size; no bare text).
- normalize stray blank lines.

Behavior-preserving: PDF regenerated from the edited HTML is byte-identical
to the pre-edit baseline (text sha256 + per-page PNG render hash match),
so CV_Bastien_Chanot.pdf is unchanged and the PDF=HTML invariant holds.
2026-07-05 20:47:51 +02:00
20 changed files with 1118 additions and 693 deletions
+158
View File
@@ -101,3 +101,161 @@ port — compose covered). Branch left UNMERGED — `gitflow finish` on GO.
| 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).
+25
View File
@@ -27,6 +27,8 @@ rules:
| 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-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.
- **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`.
---
## 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`.
+9
View File
@@ -21,6 +21,15 @@ rules:
| ID | Date | Output | Action |
|----|------|--------|--------|
| EVAL-001 | 2026-07-06 | /tour run 2026-07-05-3 (3 it., converged) + residual closure | keep |
## EVAL-001 — /tour run 2026-07-05-3 + residual closure pass
- **Date**: 2026-07-06
- **Output**: 3-iteration tour (security/clean/reconcile/doc, converged at bound) + closure of all 10 residuals on owner GO. Commits `1aa97f0`/`613bfc0`/`2f5e51a` + follow-up.
- **Method**: oracle-based — semgrep ×3 (deterministic PASS), PDF render-hash (LRN-003) for behavior-preserving proofs, docker oracles (build, nginx -t, header/dotfile/gzip/healthcheck curls), headless-browser screenshots 375+1440 (index font trim), brace counts, CSP-hash pinned==computed.
- **Anomalies**: (1) cso add-on caught a HIGH (base-image CVE) two same-day semgrep-only tours missed — gstack was OFF then → LRN-004. (2) Fresh clean sweeps surfaced new info-tier nits each iteration (N1–N4 at it2) — convergence needed explicit reporting threshold in it3 prompt; bound of 3 did its job. (3) Session limit killed both it3 agents mid-flight — SendMessage transcript-resume recovered both, zero re-audit gap.
- **Action**: keep
<!-- Append entries below. Template:
+14
View File
@@ -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.
- 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.
## 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).
+20
View File
@@ -21,6 +21,8 @@ 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 |
---
@@ -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.
- **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.
---
## 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.
-1
View File
@@ -11,7 +11,6 @@ docker-compose.yml
.dockerignore
.env
.env.example
nginx.conf.bak
# Editor / OS noise
*.swp
+1
View File
@@ -32,3 +32,4 @@ graphify-out/
.claude/gstack/
.claude/deploy/PENDING.json
.claude/deploy/NEXT.sh
.gstack/
+63
View File
@@ -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.
+12 -3
View File
@@ -79,13 +79,20 @@ Functional neutrals (allowed, intentional — layering + text, NOT brand):
green-scale intermediates for dark layering and light block bg
- `#111111` / `#1e1e1e` / `#636363` (`--ink-1/2/3`) — text hierarchy
- `#d8d4c8` (`--rule`), `#e6e2d8` (`--tag`) — separators, generic tags
Any color outside these two lists is a violation.
- `#ffffff` text + `rgba(255,255,255,…)` alphas — text/hover on dark bg and
low-alpha (≤5%) overlays only; never as a background color
Any color outside these lists is a violation.
Typography:
- `Fraunces` (serif) — display: hero name, section titles, role headings
- `JetBrains Mono` (mono) — eyebrows, badges, tech pills, nav, contact rows
- `DM Sans` (sans) — body text
CV exception (`CV_Bastien_Chanot.html`, compact print style): section titles
and company/school names are mono, roles/degrees are sans; Fraunces is
reserved for the header name and the accroche. The mapping above applies to
the landing.
Forbidden:
- Pure white background (`#ffffff`)
- `border-radius` > 6px except pills
@@ -115,8 +122,10 @@ Forbidden:
- Profile state, including job search context, must stay consistent across
index.html and CV. Currently: looking for **CDI** in embedded / systems
software first; freelance missions (ZenQuality) in parallel.
- Geography: Yerres (91) currently; targeting Pays de la Loire mid-term;
full remote preferred or hybrid 1–2 days/month if Paris.
- Geography: zone **Loire-Atlantique · Vendée · Morbihan**. Never mention
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.
---
+424
View File
@@ -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 &amp; Backend · C · Rust · Linux</div>
<div class="status">CDI · disponible</div>
<!-- ▲ ZONE 1 -->
</div>
<div class="contact">
<a href="tel:+33778822297" class="strong">+33&nbsp;7&nbsp;78&nbsp;82&nbsp;22&nbsp;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&nbsp;B&nbsp;&amp;&nbsp;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 &amp; 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 &amp; 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 &amp; 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 &amp; 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 &amp; 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 &amp; 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&amp;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 &amp; 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 &amp; 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é &amp; 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 &amp; 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
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.
+1 -1
View File
@@ -2,7 +2,7 @@
# nginx-unprivileged serves index.html + CV (HTML + PDF) as uid 101 —
# no root master process in the container (tag + digest pinned).
FROM nginxinc/nginx-unprivileged:1.28-alpine@sha256:209331cfcaec00da781f5b8a38e0d1c0abd00cb2b51e6ad385a30abbbdb04e15
FROM nginxinc/nginx-unprivileged:1.30-alpine@sha256:fd3314e343bad2de4e1127ef58be122abbfa7e09572fa46ae62fcddb6b3f21c5
# Custom nginx config (gzip, cache, security headers).
COPY nginx.conf /etc/nginx/conf.d/default.conf
+1 -1
View File
@@ -88,7 +88,7 @@ WCAG AA contrast. Focus visible. Semantic HTML.
Production currently serves the static files directly from the VPS's native
nginx (which also terminates TLS). The repo additionally maintains a hardened
container path (`bchanot-web`, `nginxinc/nginx-unprivileged:1.28-alpine`,
container path (`bchanot-web`, `nginxinc/nginx-unprivileged:1.30-alpine`,
digest-pinned, runs as uid 101 on port 8080) for when a containerized deploy
is preferred: the host port is set via `PORT` (default 8080) and bound to
`127.0.0.1`, so all traffic goes through the front proxy.
+2 -6
View File
@@ -19,12 +19,8 @@ services:
restart: unless-stopped
ports:
- "127.0.0.1:${PORT:-8080}:8080"
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8080/"]
interval: 30s
timeout: 3s
retries: 3
start_period: 5s
# Healthcheck inherited from the image HEALTHCHECK (Dockerfile) — do not
# redeclare here, one definition only.
read_only: true
tmpfs:
# nginx-unprivileged writes pid + temp files under /tmp only.
+12 -27
View File
@@ -13,7 +13,7 @@
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&family=Fraunces:ital,wght@0,300;0,500;0,600;0,700;1,400&family=DM+Sans:wght@300;400;500;600&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&family=Fraunces:ital,wght@0,600;1,400&family=DM+Sans:wght@400;500;600&display=swap" rel="stylesheet">
<style>
:root {
/* Palette — non négociable */
@@ -344,7 +344,6 @@
.reveal.d3 { animation-delay: .30s; }
.reveal.d4 { animation-delay: .42s; }
.reveal.d5 { animation-delay: .55s; }
.reveal.d6 { animation-delay: .68s; }
@keyframes rise {
to { opacity: 1; transform: translateY(0); }
}
@@ -503,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); } }
@@ -698,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);
@@ -759,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;
@@ -873,9 +861,6 @@
pointer-events: none;
}
.contact-grid {
display: grid;
grid-template-columns: 1fr;
gap: 32px;
position: relative;
z-index: 1;
}
@@ -928,7 +913,7 @@
/* ── FOOTER ── */
.footer {
background: #061008;
background: var(--dark);
color: rgba(223, 240, 231, 0.55);
padding: 32px 24px;
border-top: 1px solid rgba(106,185,138,0.1);
@@ -1000,11 +985,11 @@
<div class="hero-cta reveal d4">
<a class="btn btn-primary" href="#contact">
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 class="btn btn-secondary" href="CV_Bastien_Chanot.html">
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 class="btn btn-secondary" href="CV_Bastien_Chanot.pdf" download>
Télécharger PDF
@@ -1031,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>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>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>
<dl class="about-callout">
<dt>Recherche prioritaire</dt>
<dd>CDI systèmes embarqués / logiciel</dd>
<dt>En parallèle</dt>
<dd>Missions freelance · ZenQuality</dd>
<dt>Localisation actuelle</dt>
<dd>Yerres (91) · mobilité Pays de la Loire</dd>
<dt>Zone</dt>
<dd>Loire-Atlantique · Vendée · Morbihan</dd>
<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>
<dd><a href="https://zenquality.fr" target="_blank" rel="noopener">zenquality.fr&nbsp;↗</a></dd>
</dl>
@@ -1190,7 +1175,7 @@
<div class="timeline-meta">
<span class="period">avr.&nbsp;2026 — présent</span>
<span class="badge">En cours</span>
<span>Yerres · Full remote</span>
<span>Full remote</span>
</div>
<h3><a href="https://zenquality.fr" target="_blank" rel="noopener">ZenQuality</a></h3>
<p class="timeline-role">Développeur indépendant · Systèmes &amp; Backend</p>
+1 -1
View File
@@ -8,7 +8,7 @@ add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=()" always;
# CSP: inline CSS allowed (style attributes in the CV + single-file convention);
# CSP: inline CSS allowed (single-file convention: inline <style> element);
# the inline script is HASH-pinned (no script unsafe-inline). INVARIANT: after
# ANY edit to index.html's inline <script>, recompute the hash (command in
# CLAUDE.md) and update it here — a stale hash silently disables the JS.
+7 -7
View File
@@ -36,9 +36,15 @@ server {
application/javascript
application/json
application/xml
application/pdf
image/svg+xml;
# Block access to dotfiles (defense-in-depth — none are shipped anyway).
# First regex location wins: keep this above the caching regex blocks so
# a hypothetical /.foo.html can't be served by them.
location ~ /\. {
return 404;
}
# Long cache for the PDF (regenerated rarely, content-hash not used).
location ~* \.pdf$ {
add_header Cache-Control "public, max-age=604800";
@@ -62,12 +68,6 @@ server {
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log warn;
# Block access to dotfiles (defense-in-depth — none are shipped anyway).
location ~ /\. {
deny all;
return 404;
}
# Default: serve files, fall back to 404.
location / {
try_files $uri $uri/ =404;
+1
View File
@@ -0,0 +1 @@
1.1.0