Files
curriculum-project-hub/hub/src/admin/routes/capacityRoutes.ts
T
ChickenPige0n ab9dfad53a feat: org capacity policy admin surface
ADR-0022 / Spec.System.Capacity pins layered capacity limits: a platform
ceiling per dimension is unbreakable, and each organization may only set a
lower `organizationLimit`. The effective limit is the minimum of the two
(`LayeredLimit.effective`); dimensions with no org override fall back to
the platform ceiling. No dimension may be unlimited (a ceiling must exist
before an org limit can be set, `LayeredLimit.Valid`).

Add the backend: the pinned 23 CapacityDimension set + labels
(src/capacity/dimensions.ts), platform ceilings sourced from existing
runtime env vars plus `HUB_CEILING_<DIMENSION>` (src/capacity/ceilings.ts),
the OrganizationCapacityPolicy prisma model + migration, the
getCapacityPolicy/setCapacityPolicy service enforcing LayeredLimit.Valid,
and org-admin GET/PUT /api/org/:orgSlug/capacity-policy routes wired into
the org route tree.

Add the admin-web surface: CapacityDimension/CapacityPolicyView api client
types, capacityPolicy/setCapacityPolicy methods, a `容量` nav entry, and a
capacity page that lists every dimension with its platform ceiling (or
`未配置` when unset), an org-limit input (disabled until a ceiling exists),
and a live effective-value column. Saving sends the partial limits map;
the service rejects values above the ceiling or for unconfigured dimensions.
2026-07-14 21:23:41 +08:00

65 lines
2.5 KiB
TypeScript

/**
* Org-admin capacity policy routes (ADR-0022).
*/
import type { PrismaClient } from "@prisma/client";
import type { FastifyInstance } from "fastify";
import { getCapacityPolicy, setCapacityPolicy } from "../../org/capacityPolicy.js";
import { isCapacityDimension } from "../../capacity/dimensions.js";
import { requireOrgRole, type GuardDeps } from "../auth/guards.js";
import { handleRouteError } from "../errors.js";
export async function registerCapacityRoutes(
app: FastifyInstance,
config: { readonly prisma: PrismaClient; readonly sessionSecret: string },
): Promise<void> {
const guardDeps: GuardDeps = { prisma: config.prisma, sessionSecret: config.sessionSecret };
app.get("/api/org/:orgSlug/capacity-policy", async (request, reply) => {
try {
const { orgSlug } = request.params as { orgSlug: string };
const auth = await requireOrgRole(request, reply, guardDeps, { orgSlug });
if (auth === null) return;
return await getCapacityPolicy(config.prisma, auth.organization.id);
} catch (err) {
return handleRouteError(reply, err);
}
});
app.put("/api/org/:orgSlug/capacity-policy", async (request, reply) => {
try {
const { orgSlug } = request.params as { orgSlug: string };
const auth = await requireOrgRole(request, reply, guardDeps, { orgSlug });
if (auth === null) return;
const body = request.body as { limits?: unknown };
if (body?.limits === null || typeof body.limits !== "object") {
return reply.status(400).send({
error: { code: "bad_request", message: "limits object is required" },
});
}
const limits: Record<string, number | null> = {};
for (const [key, value] of Object.entries(body.limits as Record<string, unknown>)) {
if (!isCapacityDimension(key)) {
return reply.status(400).send({
error: { code: "bad_request", message: `unknown capacity dimension: ${key}` },
});
}
if (value === null) {
limits[key] = null;
} else if (typeof value === "number" && Number.isFinite(value)) {
limits[key] = value;
} else {
return reply.status(400).send({
error: { code: "bad_request", message: `limit for ${key} must be a number or null` },
});
}
}
return await setCapacityPolicy(config.prisma, {
organizationId: auth.organization.id,
limits,
});
} catch (err) {
return handleRouteError(reply, err);
}
});
}