Files
rootnode-academy/platform/api/src/provisioner-client.js
T
ced4568 a8e543234c feat(platform): pagination, admission control, cohort roles, version endpoint, structured assessment editor
- 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.
2026-08-24 20:15:39 -05:00

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 };