commit 2067bc6c8365c854240e4d78ef5c00fe5fc639ec Author: electron-rare <108685187+electron-rare@users.noreply.github.com> Date: Wed Jun 10 20:51:18 2026 +0200 docs: reader + 3D atom hub design spec diff --git a/docs/superpowers/specs/2026-06-10-formations-reader-hub3d-design.md b/docs/superpowers/specs/2026-06-10-formations-reader-hub3d-design.md new file mode 100644 index 0000000..0af0604 --- /dev/null +++ b/docs/superpowers/specs/2026-06-10-formations-reader-hub3d-design.md @@ -0,0 +1,129 @@ +# Design: formations reader + 3D atom hub (sub-project 1 of 4) + +Date: 2026-06-10 +Status: approved (brainstorm with owner, visual companion session) + +## Context + +moodle.saillant.cc (Moodle 4.5 on Tower, restored 2026-06-10) hosts 6 +formations, each = 4 books (text-only HTML chapters) + 4 quizzes + forum + +customcert certificate. 3 formations are published (esp32-ia, kicad-makers, +freertos), 3 hidden (llm-locaux, iot-az, docker-selfhosting). The owner wants +to replace the learner-facing Moodle UI with a web app matching the new +electron 3D identity of lelectronrare.fr (dark + copper, canvas electron +hero shipped 2026-06-10). + +## Program decisions (brainstormed, apply to all 4 sub-projects) + +1. **Full replacement**: the app becomes the entire learner experience; + Moodle stays as backend (accounts, grades, completion, certificates) + and as the author/admin tool. +2. **Certificates**: generated by Moodle (customcert); the app celebrates + and links to the PDF. +3. **Access model**: published course content is readable WITHOUT an + account (replaces "preview gratuite", gives SEO); a Moodle account is + required for quizzes, saved progression, certificates. +4. **Domain**: the app will ultimately TAKE moodle.saillant.cc; the Moodle + UI moves to an admin subdomain (sub-project 4). Until then the app + lives at formations.saillant.cc. +5. **three.js role**: option B "the atom is the table of contents" — each + course is a three.js atom: 4 orbits = 4 modules (books), electrons = + chapters, click to read. Reading happens in a clean light panel. +6. **Quizzes** (sub-project 3): rendered natively in the app; attempts and + grades pushed to Moodle via its quiz API. + +### Sub-projects + +1. **Reader + 3D hub** (this spec) — public reading experience. +2. Accounts + progression — login (Moodle token), progression lights up + the atom's electrons. +3. Native quizzes — in-app MCQ/true-false, graded via Moodle API. +4. Certificates + domain swap — celebration screen, customcert PDF link, + Moodle UI → lms-admin.saillant.cc, app takes moodle.saillant.cc. + +## Sub-project 1 scope + +### Stack & deployment + +- **Astro 6 SSR (node adapter) + Tailwind 4** — same stack as + lelectronrare.fr; one Docker container (node:22-alpine multi-stage, same + Dockerfile pattern) on **Tower** next to Moodle, port **8096**. +- Routing: traefik on electron-server (dynamic file `formations.yml`) → + `http://100.78.6.122:8096` (Tailscale — the LAN 192.168.0.x is shadowed + on ES by a docker bridge) + CF tunnel ingress `formations.saillant.cc` + (API recipe, same as moodle/coder restores). +- **three.js pinned** (exact version in package.json, lockfile committed; + vendor/fork per the supply-chain HITL policy). It is loaded ONLY on + course hub pages, lazily. +- Repo: `electron-rare/formations-app` on Gitea (this repo). + +### Data flow (BFF) + +- The app's server (Astro API routes / loaders) is the only thing that + talks to Moodle. Browser → app only. +- Moodle **REST web services** with a dedicated read-only service token: + `core_course_get_courses_by_field` (catalogue), `core_course_get_contents` + (course structure: books, chapters), chapter HTML files fetched + server-side with the token. +- Admin prerequisite (done via Moodle CLI during implementation): enable + web services + REST protocol, create service user + token with the + minimal read functions. +- **Cache**: in-memory per-process with TTL (content changes rarely) + + stale-while-revalidate: if Moodle is down, serve the last good copy and + log; only error pages if the cache is cold. +- Slug mapping fixed in app config: `esp32-ia`, `kicad-makers`, `freertos` + (+ hidden ones listed as "en préparation", never fetched publicly). + +### Pages + +- `/` — catalogue: 6 cards (3 open, 3 "en préparation"), dark + copper, + consistent with lelectronrare.fr (reuse of the visual language, not the + code). +- `/cours/` — **the atom hub** (three.js): nucleus = the course; 4 + tilted orbits = the 4 modules (books); electrons on each orbit = that + module's chapters. Hover an electron → chapter title tooltip; click → + navigate to the chapter. Gentle orbital camera (drag to rotate, scroll + zoom within limits). Module labels anchored to orbits. Progression + rendering exists but all-grey in v1 (lights up in sub-project 2). +- `/cours//module-/chapitre-` — reading panel: light + background, max-width prose column, book chapter HTML rendered + **server-side** (free reading = indexable), breadcrumb, prev/next + chapter, a small static atom position indicator in the margin (current + electron highlighted), table of contents drawer. +- Legacy URLs `/course/view.php?id=N` answered with 301 → `/cours/` + (active from day one; becomes load-bearing at sub-project 4). + +### Progressive enhancement & guard rails + +- The hub page ALWAYS renders a complete HTML table of contents + (modules + chapters list) server-side; the three.js atom replaces it + visually when JS+WebGL are available. noscript, `prefers-reduced-motion`, + WebGL-less, and low-end mobile users get the HTML sommaire. The 3D is an + enhancement, never a wall. +- Perf budget: three.js chunk lazy-loaded only on `/cours/`; + reading pages ship no 3D; LCP < 2.5 s on reading pages; the atom scene + pauses when the tab is hidden (same discipline as the lelectronrare + hero: DPR cap, rAF pause, reduced-motion static). +- Chapter HTML is sanitized server-side before render (defense in depth — + authors are trusted today, but the pipe shouldn't assume it). + +### Error handling + +- Moodle WS unreachable: stale cache if warm; otherwise a friendly error + page on course routes; `/` still renders (catalogue is app-config). +- Unknown slug/module/chapter → 404 page (same dark identity). +- three.js init failure → automatic fallback to the HTML sommaire + (try/catch, no blank page). + +### Testing + +- `npm run build` green in Docker (node:22-alpine, --network=host on ES). +- E2E curl: `/` 200, hub 200 with sommaire HTML present, chapter page 200 + with real book content, legacy `/course/view.php?id=2` → 301. +- Manual: atom navigation on desktop + mobile, reduced-motion fallback, + Lighthouse on a reading page. + +## Out of scope (later sub-projects) + +- Login, progression state, quiz rendering/grading, certificates, + domain swap, Moodle admin relocation, payment.