- 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.
23 lines
1.2 KiB
JavaScript
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 };
|