feat(platform): relay scenario progress from ephemeral labs to identity backend
Real architectural gap found and fixed during Phase 2A testing, not part of
the original plan: scenario validate/answer requests proxied through
/lab/<token>/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 <[email protected]>
This commit is contained in:
@@ -886,6 +886,32 @@ app.post('/api/scenarios/:id/time', (req, res) => {
|
|||||||
res.json({ ok: true });
|
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
|
// GET /api/progress — full progress summary
|
||||||
app.get('/api/progress', (req, res) => {
|
app.get('/api/progress', (req, res) => {
|
||||||
const scenarios = loadScenarios();
|
const scenarios = loadScenarios();
|
||||||
|
|||||||
Reference in New Issue
Block a user