From b7258be8cc253aad77e419554a4243e070bb048a Mon Sep 17 00:00:00 2001 From: electron-rare <108685187+electron-rare@users.noreply.github.com> Date: Thu, 11 Jun 2026 01:21:10 +0200 Subject: [PATCH] docs: sp4 spec + plan (certs, domain swap) --- .../plans/2026-06-11-sp4-certs-domain.md | 59 ++++++++++++ .../2026-06-11-sp4-certs-domain-design.md | 90 +++++++++++++++++++ 2 files changed, 149 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-11-sp4-certs-domain.md create mode 100644 docs/superpowers/specs/2026-06-11-sp4-certs-domain-design.md diff --git a/docs/superpowers/plans/2026-06-11-sp4-certs-domain.md b/docs/superpowers/plans/2026-06-11-sp4-certs-domain.md new file mode 100644 index 0000000..42268de --- /dev/null +++ b/docs/superpowers/plans/2026-06-11-sp4-certs-domain.md @@ -0,0 +1,59 @@ +# SP4 — Certificates + Domain Swap Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Moodle-generated certificates emailed automatically on course completion, celebrated in the app; then the app takes moodle.saillant.cc and Moodle moves to lms-admin.saillant.cc. + +**Stack/Repo:** unchanged. Branch `feat/sp4-certs-domain` off main. Runner as before. Secrets only in env files (Tower `/home/clems/formations-app.env`; SMTP creds read from ES `/home/clems/electron-rare-site/mail-api.env` at provisioning time, stored in Moodle config — never in git). + +--- + +### Task 1: Moodle certificates provisioning + +**Files:** Create `ops/provision-certs.php`; update `ops/README.md`; host crontab on Tower. + +- [ ] **1.1** `ops/provision-certs.php` (CLI, idempotent, style of provision-ws.php). For each course id>1: + - find the customcert cm + the 4 quiz cmids (modules join, like export script); + - set `course_modules.availability` on the customcert cm to `{"op":"&","c":[{"type":"completion","cm":,"e":1},...×4],"showc":[true,true,true,true]}` (e:1 = COMPLETE). Use `$DB->set_field` + `rebuild_course_cache($courseid, true)`. + - set customcert.emailstudents=1 (`$DB->set_field('customcert','emailstudents',1,...)`). + Then SMTP config from env vars (script reads SMTP_HOST/SMTP_PORT/SMTP_USER/SMTP_PASS/SMTP_FROM passed via docker exec -e): `set_config('smtphosts', host.':'.port); set_config('smtpsecure','tls'); set_config('smtpuser',...); set_config('smtppass',...); set_config('noreplyaddress', SMTP_FROM); set_config('allowedemaildomains','');`. Print summary. +- [ ] **1.2** Run on Tower (SMTP values from ES `sudo grep -E 'SMTP_|ICLOUD' /home/clems/electron-rare-site/mail-api.env` — From = the iCloud account address, NOT contact@lelectronrare.fr; the 550 trap). +- [ ] **1.3** Cron: on Tower `crontab -l` + add idempotently: `*/2 * * * * docker exec moodle php /var/www/html/admin/cli/cron.php >/dev/null 2>&1`. Run once manually; expect task list output incl. `email_certificate_task` scheduled (it ships enabled by default — verify in mdl_task_scheduled, enable if disabled). +- [ ] **1.4 PROOF**: pick test user = wsreader (id 3, enrolled): temporarily set its email to a real mailbox? NO — instead: override the 4 quiz completions for wsreader on course 5 (freertos) via the existing MOODLE_SYNC_TOKEN + core_completion_override_activity_completion_status (cmids from QuizBank rows), run cron, then check `SELECT * FROM mdl_customcert_issues WHERE userid=3` (row = issuance works) and the cron output mentions the email task (send will fail or go to wsreader@saillant.cc — irrelevant, issuance row is the proof; report the email task log line). REVERT the 4 overrides (newstate=0) and DELETE the test issue row. +- [ ] **1.5** README section (what the script sets, SMTP source, cron line, rollback notes) + commit `feat: certificate issuing provisioning`. + +### Task 2: App certificate page + hub CTA (TDD on the predicate) + +**Files:** Create `src/lib/completion.ts` + test, `src/pages/cours/[slug]/certificat.astro`; modify hub page. + +- [ ] **2.1** `src/lib/completion.ts`: `export function courseComplete(modules: {chapters:{...}[]}[], reads: ReadMark[], quizzes: Quiz[], attempts: QuizAttempt[]): {complete: boolean; chaptersRead: number; chaptersTotal: number; quizzesPassed: number; quizzesTotal: number}` — pure; complete = every module fully read (isBookComplete) AND every quiz passed (best attempt isPassed). Tests (≥4): complete, missing chapters, missing quiz, empty. +- [ ] **2.2** `/cours//certificat`: published slug else 404-rewrite; anon → redirect connexion. Load modules/reads/quizzes/attempts (try/catch → 503 friendly). If complete: dark celebration page — big copper nucleus glow (pure CSS, no three.js), « Félicitations ! », course title, `X chapitres · 4 quiz réussis`, panel « Votre certificat officiel est envoyé par email à (vérifiez vos indésirables). Généré par notre plateforme de formation. », contact link mailto:contact@lelectronrare.fr, « Retour au cours ». If incomplete: progress view (chapters n/N, quiz m/4, links to first missing chapter + first unpassed quiz). +- [ ] **2.3** Hub: when user && complete → copper card/CTA « 🎓 Certificat disponible → /cours//certificat » under the progress lines; when user && !complete → muted line « Certificat : termine les chapitres et les 4 quiz ». Reuse courseComplete (single computation with already-loaded data). +- [ ] **2.4** Runner green → commit `feat: certificate celebration page`. + +### Task 3: Domain swap + +**Files (repo):** modify `src/middleware.ts` (formations→moodle 301 host redirect). **Infra:** traefik dynamic files on ES, CF ingress, Moodle config.php (Tower volume), Tower env file, Keycloak client, lelectronrare.fr links. + +- [ ] **3.1** middleware.ts: at top of onRequest — `if (ctx.url.hostname === 'formations.saillant.cc') return ctx.redirect('https://moodle.saillant.cc' + ctx.url.pathname + ctx.url.search, 301);`. Commit `feat: formations host 301 to moodle domain`. +- [ ] **3.2** Keycloak: add redirect URI `https://moodle.saillant.cc/auth/callback` + webOrigin to client `formations-app` (kcadm update, KEEP existing URIs). +- [ ] **3.3** lms-admin.saillant.cc: traefik `lms-admin.yml` on ES (→ http://100.78.6.122:8095, websecure letsencrypt) + CF tunnel ingress (recipe). Verify 200 via --resolve BEFORE wwwroot change (Moodle may redirect to wwwroot — a 3xx to moodle.saillant.cc is fine at this stage). +- [ ] **3.4** Moodle wwwroot: on Tower edit config.php inside volume (`docker exec moodle sed -i "s|https://moodle.saillant.cc|https://lms-admin.saillant.cc|" /var/www/html/config.php`) + purge caches (`php admin/cli/purge_caches.php`). Verify https://lms-admin.saillant.cc 200 + login page assets load from lms-admin. +- [ ] **3.5** App env on Tower: `MOODLE_BASE_URL=https://lms-admin.saillant.cc`, `APP_BASE_URL=https://moodle.saillant.cc` (sed in /home/clems/formations-app.env). NOTE: MOODLE_WS_TOKEN/MOODLE_SYNC_TOKEN remain valid (tokens are host-independent). +- [ ] **3.6** Swap moodle.saillant.cc: edit ES traefik `moodle.yml` service url → `http://100.78.6.122:8096`. `formations.yml` unchanged (still → 8096; the app's middleware now 301s that host). Pull+rebuild app on Tower (gets middleware + env). +- [ ] **3.7** lelectronrare.fr: replace `formations.saillant.cc` → `moodle.saillant.cc` in the 3 link files (src/pages/formations.astro, src/components/Formations.astro, src/components/Footer.astro), commit, push gitea+origin, rebuild image (--network=host) + recreate container on ES (compose chez clems). + +### Task 4: E2E + +- [ ] App on moodle.saillant.cc: `/` 200 catalogue; `/cours/kicad-makers` 200 with data-hub; chapter 200; `/connexion` 302 Keycloak; legacy `/course/view.php?id=3` 301 → /cours/kicad-makers. +- [ ] lms-admin.saillant.cc: 200, Moodle login page; WS sanity: hub renders (BFF green via lms-admin); `login/token.php` reachable. +- [ ] formations.saillant.cc/* → 301 moodle.saillant.cc same path. +- [ ] certificat page: anon → 302; (logged user manual: incomplete → progress view). +- [ ] lelectronrare.fr/formations links → moodle.saillant.cc (curl grep). +- [ ] Moodle cron ran in last 5 min (`mdl_config_plugins lastcron` or task log). +- [ ] Merge ff-only → main, push; memories update. + +## Self-review +- Order: app-side first, swap last; each infra step has a verify; rollback = revert 2 traefik files + wwwroot sed back. +- 550 trap: From = iCloud-owned address (noreplyaddress), documented. +- Availability lockdown closes the "cert downloadable by anyone enrolled" hole found in probe. diff --git a/docs/superpowers/specs/2026-06-11-sp4-certs-domain-design.md b/docs/superpowers/specs/2026-06-11-sp4-certs-domain-design.md new file mode 100644 index 0000000..3477d00 --- /dev/null +++ b/docs/superpowers/specs/2026-06-11-sp4-certs-domain-design.md @@ -0,0 +1,90 @@ +# Design: SP4 — certificates + domain swap + +Date: 2026-06-11 +Status: program decisions locked (brainstorm 2026-06-10); technicals below +probed on the live systems. + +## Probed ground truth + +- 6 customcert activities ("Certificat de réussite", cmids 55-60), one per + course, currently with NO availability restriction (any enrolled user + could download — must be locked down). +- Course completion criteria already configured (48 rows, criteriatype 4 = + activity completion — the 8 activities per course). +- Moodle auth = email,oauth2 (FER issuer only). Learners are app-SSO only, + no Moodle password. +- Moodle container has NO cron runner (restored compose is web+db only) — + scheduled tasks (certificate emailing) never run today. +- No SMTP configured in Moodle. House SMTP = iCloud (smtp.mail.me.com:587, + creds in mail-api env on ES) — **From MUST be the iCloud-owned address** + (550 otherwise; same trap as lelectronrare.fr contact form). + +## Decision: email-issued certificates (no learner Moodle login) + +The certificate stays 100% Moodle-generated (customcert = program +decision) but is DELIVERED BY EMAIL via Moodle's native +`email_certificate_task`. Learners never need to log into Moodle: + +1. App pushes activity completions (books SP2, quizzes SP3) → Moodle + course completion aggregates (criteria already set). +2. customcert activities get availability conditions = completion of the + course's 4 quizzes (DB/PHP provisioning) and `emailstudents=1`. +3. Moodle cron (added) runs the issue+email task: eligible learners get + the PDF at their Keycloak email. +4. The app's celebration page tells them it's on its way. + +## Components + +### 1. Moodle provisioning (`ops/provision-certs.php` + infra) +- Set per-course customcert `cm.availability` = AND-completion of that + course's 4 quiz cmids (state completed-pass or completed: use state 1). +- `emailstudents=1` on the 6 customcerts; `verifyany` left as is. +- SMTP: set Moodle config smtphosts/smtpuser/smtppass/smtpsecure + + `noreplyaddress` = the iCloud-owned From; secrets read from env at run. +- Cron: host crontab on Tower (`*/2 * * * * docker exec moodle php + /var/www/html/admin/cli/cron.php`), idempotent install. +- Proof: complete a test user's quizzes via override → run cron → + `mdl_customcert_issues` row exists and the email task logs a send. + +### 2. App — certificate page + CTA +- Course complete (app-side definition: ALL chapters read AND 4 quizzes + passed) → hub shows a copper "Certificat" card; `/cours//certificat` + (login required): celebration screen (big copper glow, course name, + score recap), text "Votre certificat officiel est généré par notre + plateforme et envoyé à sous quelques minutes", note to check + spam, contact link. Not complete → page shows progress and what's + missing (n chapters, m quizzes). +- No customcert WS exists → the app does NOT query issue state (YAGNI); + delivery is Moodle's job. + +### 3. Domain swap +- NEW `lms-admin.saillant.cc` → Moodle (traefik ES → Tower:8095 + CF + ingress); Moodle `$CFG->wwwroot` switched to it (config.php in the + moodle-html volume); admin keeps password login. +- `moodle.saillant.cc` traefik route RE-POINTED → the app (Tower:8096); + app takes the domain: `APP_BASE_URL=https://moodle.saillant.cc`, + Keycloak client `formations-app` gains the new redirect URI, + `MOODLE_BASE_URL=https://lms-admin.saillant.cc` (WS + pluginfile URLs + follow wwwroot automatically). +- `formations.saillant.cc` kept as 301 → moodle.saillant.cc (tiny + middleware host check in the app). +- lelectronrare.fr links updated formations.saillant.cc → moodle.saillant.cc + (3 files), rebuilt + redeployed. +- Old learner deep-links `/course/view.php?id=N` on moodle.saillant.cc now + hit the app middleware → already 301 to /cours/ (load-bearing as + planned in SP1). + +## Order of operations (swap is last, app-first is safe) +Certs provisioning → app cert page (works on formations domain) → +domain swap → E2E. + +## Error handling +Cron/email failures: Moodle task logs; app celebration is informational +(no hard dependency). Swap rollback: traefik routes are 2 small files; +wwwroot rollback documented in ops/README. + +## Testing +vitest: course-complete predicate. E2E: cert page anon→connexion; incomplete +→ progress view; domain swap: app 200 on moodle.saillant.cc, Moodle 200 on +lms-admin, WS still green (hub renders), formations 301, site links updated, +legacy /course/view.php 301. Manual owner: real email reception.