7.7 KiB
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.availabilityon the customcert cm to{"op":"&","c":[{"type":"completion","cm":<quizcmid>,"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_taskscheduled (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/<slug>/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 à <user.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);. Commitfeat: formations host 301 to moodle domain. - 3.2 Keycloak: add redirect URI
https://moodle.saillant.cc/auth/callback+ webOrigin to clientformations-app(kcadm update, KEEP existing URIs). - 3.3 lms-admin.saillant.cc: traefik
lms-admin.ymlon 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.ymlservice url →http://100.78.6.122:8096.formations.ymlunchanged (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.ccin 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-makers200 with data-hub; chapter 200;/connexion302 Keycloak; legacy/course/view.php?id=3301 → /cours/kicad-makers. - lms-admin.saillant.cc: 200, Moodle login page; WS sanity: hub renders (BFF green via lms-admin);
login/token.phpreachable. - 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 lastcronor 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.