From ce6abfe6c707471f91b8df3b1a113377bfc6dad1 Mon Sep 17 00:00:00 2001 From: ced4568 Date: Tue, 18 Aug 2026 11:16:08 -0500 Subject: [PATCH] feat(platform): relay scenario progress from ephemeral labs to identity backend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Real architectural gap found and fixed during Phase 2A testing, not part of the original plan: scenario validate/answer requests proxied through /lab//api/scenarios/:id/{validate,answer} were writing progress into that ephemeral Lab container's own SQLite — which is destroyed on every reset/reap. Confirmed by testing: solving a scenario never showed up on the dashboard's curriculum view, and would have been silently lost on lab recreate, directly contradicting "academic progress must survive lab reset/recreate." Root tension: scenario validation must run against the student's own live cluster (only exists inside their Lab container) — it can't be re-run anywhere else. But the canonical progress record needs to live on the persistent identity instance, which never has the student's actual cluster state applied to it. Fix: this endpoint (POST /api/progress/record) records an outcome WITHOUT re-running validation — it exists purely so the Gateway can relay a validate/answer result it already obtained from the real Lab. The Gateway forwards the real request to the Lab, returns that real response to the browser unchanged, and separately calls this endpoint with the outcome. See platform/gateway/src/server.js's relayScenarioOutcome(). Verified end-to-end: a scenario solved through a student's Lab shows up in their curriculum immediately, survives their lab being destroyed and reset, and is correctly attributed only to that student (not visible to a second student's own curriculum view). Co-Authored-By: Claude Sonnet 5 --- backend/server.js | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/backend/server.js b/backend/server.js index 0cceb72..e00f4d9 100644 --- a/backend/server.js +++ b/backend/server.js @@ -886,6 +886,32 @@ app.post('/api/scenarios/:id/time', (req, res) => { res.json({ ok: true }); }); +// POST /api/progress/record — Phase 2A addition. Records a scenario outcome WITHOUT +// re-running validation. Exists because scenario validation itself must run against the +// student's own live cluster (inside their ephemeral Lab container), but the canonical +// progress record needs to live here, on the persistent identity instance — the Gateway +// calls this after a real validate/answer already succeeded against the student's Lab, to +// relay the outcome. See docs/PHASE2A_GATEWAY_AND_STUDENT_FLOW.md "Progress relay." +app.post('/api/progress/record', (req, res) => { + const { scenario_id, status } = req.body || {}; + if (!scenario_id || !['completed', 'in_progress'].includes(status)) { + return res.status(400).json({ error: 'scenario_id and a valid status are required' }); + } + const studentId = getStudentId(req); + const progress = loadProgress(studentId); + const prev = progress[scenario_id] || { attempts: 0 }; + progress[scenario_id] = { + ...prev, + status, + attempts: (prev.attempts || 0) + 1, + last_validated: new Date().toISOString(), + completed_at: status === 'completed' ? new Date().toISOString() : prev.completed_at, + }; + saveProgress(studentId, progress); + touchStudent(studentId); + res.json({ ok: true }); +}); + // GET /api/progress — full progress summary app.get('/api/progress', (req, res) => { const scenarios = loadScenarios();