- Real server-side pagination (platform/api/src/pagination.js) on the audit log (was a flat LIMIT 200), student attempt history (was LIMIT 100), and assessment session history (was fully unbounded - the one genuine growth risk found). Frontend updated (AuditView, AssessmentsView, StudentsView). - Admission control: resource-policy.yaml's new (disabled-by-default) admission_control.max_concurrent_labs, checked before touching Docker at all, returns a clean 503 CAPACITY_EXCEEDED instead of failing mid-provision. Relayed as a clean student-facing message, never a raw 500. New GET /capacity endpoint feeds the instructor dashboard. - cohort_instructors.role_in_cohort (existed since Phase 2B, never read) is now enforced: teacher vs viewer, rbac.js's requireCohortWriteAccess/requireStudentCohortWriteAccess. - GET /api/version - app version, deployed Git SHA, latest migration as a schema-version stand-in. - Structured assessment authoring UI (AssessmentsView.jsx's PoolSpecEditor) replaces the raw-JSON pool_spec textarea, backed by a new GET /api/admin/scenario-catalog endpoint. Owner-only raw-JSON fallback remains for anything the structured editor doesn't cover. - NEEDS_ATTENTION_FAIL_THRESHOLD/STALE_DAYS moved to env-var configuration. Full regression suite (e2e-phase2a/2b/3a/3b, 67/67) re-run clean after these changes; frontend production build confirmed clean.
40 lines
2.0 KiB
JavaScript
40 lines
2.0 KiB
JavaScript
// Thin client for the Provisioner's internal API — same role as
|
|
// platform/gateway/src/provisioner-client.js. Phase 2B consolidates ALL Provisioner access
|
|
// (both student launch/reset and admin prepare/destroy/reset) into this one service, per
|
|
// "environment orchestration calls" being a Platform API responsibility
|
|
// (docs/PHASE2B_ADMIN_AND_PLATFORM_DATA.md §1). The Gateway no longer calls the Provisioner
|
|
// directly — it proxies /api/environment/* to here instead.
|
|
|
|
const PROVISIONER_URL = process.env.PROVISIONER_URL || 'http://localhost:4100';
|
|
const PROVISIONER_SHARED_SECRET = process.env.PROVISIONER_SHARED_SECRET;
|
|
|
|
if (!PROVISIONER_SHARED_SECRET) {
|
|
console.error('FATAL: PROVISIONER_SHARED_SECRET is not set. Refusing to start.');
|
|
process.exit(1);
|
|
}
|
|
|
|
async function call(method, path, body) {
|
|
const res = await fetch(`${PROVISIONER_URL}${path}`, {
|
|
method,
|
|
headers: { 'Content-Type': 'application/json', 'X-Provisioner-Key': PROVISIONER_SHARED_SECRET },
|
|
body: body ? JSON.stringify(body) : undefined,
|
|
});
|
|
const text = await res.text();
|
|
let json; try { json = JSON.parse(text); } catch { json = null; }
|
|
return { status: res.status, body: json };
|
|
}
|
|
|
|
const provisioner = {
|
|
getEnvironmentByToken: (token) => call('GET', `/environments/by-token/${encodeURIComponent(token)}`),
|
|
getEnvironment: (id) => call('GET', `/environments/${encodeURIComponent(id)}`),
|
|
getToken: (id) => call('GET', `/environments/${encodeURIComponent(id)}/token`),
|
|
listForStudent: (studentId) => call('GET', `/environments?student_id=${encodeURIComponent(studentId)}`),
|
|
provision: (studentId) => call('POST', '/environments', { student_id: studentId }),
|
|
reset: (id) => call('POST', `/environments/${encodeURIComponent(id)}/reset`),
|
|
destroy: (id) => call('DELETE', `/environments/${encodeURIComponent(id)}`),
|
|
events: (id) => call('GET', `/environments/${encodeURIComponent(id)}/events`),
|
|
capacity: () => call('GET', '/capacity'), // Phase 4.1 §26 — instructor dashboard capacity state
|
|
};
|
|
|
|
module.exports = { provisioner };
|