Files
rootnode-academy/platform/api/src/pagination.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

23 lines
1.2 KiB
JavaScript

// Stable server-side pagination — Phase 4.1 §16. Shared by every list endpoint that could
// otherwise grow unbounded (audit log, attempt histories, assessment session history, scenario
// analytics). Simple offset/limit, not cursor-based — this admin/instructor scale (hundreds to
// low thousands of rows, not a high-frequency infinite-scroll feed) doesn't need more than
// that, and offset pagination is what every other query in this codebase already reads like.
const DEFAULT_PAGE_SIZE = 50;
const MAX_PAGE_SIZE = 200;
// Reads ?page=&page_size= from a request, clamped to sane bounds — never trusts the client for
// an unbounded page_size (that would defeat the entire point).
function parsePagination(req) {
const page = Math.max(1, parseInt(req.query.page, 10) || 1);
const pageSize = Math.min(MAX_PAGE_SIZE, Math.max(1, parseInt(req.query.page_size, 10) || DEFAULT_PAGE_SIZE));
return { page, pageSize, offset: (page - 1) * pageSize };
}
function paginatedResponse(rows, total, { page, pageSize }) {
return { items: rows, page, page_size: pageSize, total, total_pages: Math.max(1, Math.ceil(total / pageSize)) };
}
module.exports = { parsePagination, paginatedResponse, DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE };