From 9eef605af1dd1b3c3cf9916b64e45363c4c84729 Mon Sep 17 00:00:00 2001 From: isshaddad Date: Mon, 4 May 2026 07:02:32 -0400 Subject: [PATCH 1/2] feat(webapp): admin editor for org concurrency quota --- .server-changes/admin-concurrency-quota.md | 10 ++ .../ConcurrencyQuotaSection.server.ts | 116 +++++++++++++ .../backOffice/ConcurrencyQuotaSection.tsx | 156 ++++++++++++++++++ .../routes/admin.back-office.orgs.$orgId.tsx | 55 +++++- apps/webapp/app/routes/admin.tsx | 2 +- .../webapp/app/services/platform.v3.server.ts | 42 +++++ 6 files changed, 377 insertions(+), 4 deletions(-) create mode 100644 .server-changes/admin-concurrency-quota.md create mode 100644 apps/webapp/app/components/admin/backOffice/ConcurrencyQuotaSection.server.ts create mode 100644 apps/webapp/app/components/admin/backOffice/ConcurrencyQuotaSection.tsx diff --git a/.server-changes/admin-concurrency-quota.md b/.server-changes/admin-concurrency-quota.md new file mode 100644 index 00000000000..d279a6ab848 --- /dev/null +++ b/.server-changes/admin-concurrency-quota.md @@ -0,0 +1,10 @@ +--- +area: webapp +type: feature +--- + +Admin Back office: editor for an org's concurrency quota cap (the per-org +override on how much extra concurrency the org can purchase). Sits as a new +section on the existing per-org back-office page alongside API/Batch rate +limits and Maximum projects. Calls cloud's billing service to update +billing.Limits.extraConcurrencyQuota. diff --git a/apps/webapp/app/components/admin/backOffice/ConcurrencyQuotaSection.server.ts b/apps/webapp/app/components/admin/backOffice/ConcurrencyQuotaSection.server.ts new file mode 100644 index 00000000000..46b1d35b752 --- /dev/null +++ b/apps/webapp/app/components/admin/backOffice/ConcurrencyQuotaSection.server.ts @@ -0,0 +1,116 @@ +import { z } from "zod"; +import { logger } from "~/services/logger.server"; +import { setExtraConcurrencyQuota } from "~/services/platform.v3.server"; +import { CONCURRENCY_QUOTA_INTENT } from "./ConcurrencyQuotaSection"; + +const RawSchema = z.object({ + intent: z.literal(CONCURRENCY_QUOTA_INTENT), + // Checkbox arrives as "on" / "true" when checked, absent when not. + usePlanDefault: z.string().optional(), + // Empty string when "Use plan default" is checked (the input is disabled). + extraConcurrencyQuota: z.string().optional(), +}); + +const SetConcurrencyQuotaSchema = RawSchema.transform((raw, ctx) => { + const usePlanDefault = !!raw.usePlanDefault; + if (usePlanDefault) { + return { extraConcurrencyQuota: null as number | null }; + } + const trimmed = (raw.extraConcurrencyQuota ?? "").trim(); + if (trimmed.length === 0) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Enter a non-negative integer or check 'Use plan default'.", + path: ["extraConcurrencyQuota"], + }); + return z.NEVER; + } + const parsed = Number(trimmed); + if (!Number.isInteger(parsed) || parsed < 0) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Quota must be a non-negative integer.", + path: ["extraConcurrencyQuota"], + }); + return z.NEVER; + } + return { extraConcurrencyQuota: parsed as number | null }; +}); + +export type ConcurrencyQuotaActionResult = + | { ok: true } + | { + ok: false; + errors: Record; + formError?: string; + }; + +export async function handleConcurrencyQuotaAction( + formData: FormData, + orgId: string, + adminUserId: string +): Promise { + const submission = SetConcurrencyQuotaSchema.safeParse( + Object.fromEntries(formData) + ); + if (!submission.success) { + return { ok: false, errors: submission.error.flatten().fieldErrors }; + } + + const result = await setExtraConcurrencyQuota(orgId, { + extraConcurrencyQuota: submission.data.extraConcurrencyQuota, + }); + + if (!result) { + return { + ok: false, + errors: {}, + formError: + "Billing client unavailable — check BILLING_API_URL/BILLING_API_KEY config.", + }; + } + + if (!result.success) { + // The platform client's generic error path strips `code` to `error` only + // until the BillingClient.fetch passthrough fix lands; cast keeps the + // route forward-compatible so precise UI copy renders automatically once + // it does. + const err = result as { + success: false; + error: string; + code?: string; + }; + return { + ok: false, + errors: {}, + formError: mapCodeToMessage(err.code, err.error), + }; + } + + logger.info("admin.backOffice.concurrencyQuota", { + adminUserId, + orgId, + next: submission.data.extraConcurrencyQuota, + }); + + return { ok: true }; +} + +function mapCodeToMessage( + code: string | undefined, + fallback: string +): string { + switch (code) { + case "invalid_body": + return "Quota must be a non-negative integer (or check 'Use plan default')."; + case "quota_too_high": + // Cloud's `error` string embeds the actual ceiling, prefer it verbatim. + return fallback || "Cap is too high."; + case "org_not_found": + return "Organization not found."; + case "limits_not_found": + return "This org has no billing limits row yet."; + default: + return fallback || "Failed to update concurrency quota."; + } +} diff --git a/apps/webapp/app/components/admin/backOffice/ConcurrencyQuotaSection.tsx b/apps/webapp/app/components/admin/backOffice/ConcurrencyQuotaSection.tsx new file mode 100644 index 00000000000..d7baf71408b --- /dev/null +++ b/apps/webapp/app/components/admin/backOffice/ConcurrencyQuotaSection.tsx @@ -0,0 +1,156 @@ +import { Form } from "@remix-run/react"; +import { useEffect, useState } from "react"; +import { Button } from "~/components/primitives/Buttons"; +import { Checkbox } from "~/components/primitives/Checkbox"; +import { FormError } from "~/components/primitives/FormError"; +import { Header2 } from "~/components/primitives/Headers"; +import { Input } from "~/components/primitives/Input"; +import { Label } from "~/components/primitives/Label"; +import { Paragraph } from "~/components/primitives/Paragraph"; +import * as Property from "~/components/primitives/PropertyTable"; + +export const CONCURRENCY_QUOTA_INTENT = "set-concurrency-quota"; +export const CONCURRENCY_QUOTA_SAVED_VALUE = "concurrency-quota"; + +type FieldErrors = Record | null; + +type Props = { + currentQuota: number; + purchased: number; + errors: FieldErrors; + formError: string | null; + savedJustNow: boolean; + isSubmitting: boolean; +}; + +export function ConcurrencyQuotaSection({ + currentQuota, + purchased, + errors, + formError, + savedJustNow, + isSubmitting, +}: Props) { + const hasFieldErrors = !!errors && Object.keys(errors).length > 0; + const fieldError = (field: string) => + errors && field in errors ? errors[field]?.[0] : undefined; + + const [isEditing, setIsEditing] = useState(hasFieldErrors || !!formError); + const [usePlanDefault, setUsePlanDefault] = useState(false); + const [value, setValue] = useState(String(currentQuota)); + + useEffect(() => { + if (hasFieldErrors || formError) setIsEditing(true); + }, [hasFieldErrors, formError]); + + useEffect(() => { + if (savedJustNow && !hasFieldErrors && !formError) setIsEditing(false); + }, [savedJustNow, hasFieldErrors, formError]); + + const cancelEdit = () => { + setValue(String(currentQuota)); + setUsePlanDefault(false); + setIsEditing(false); + }; + + return ( +
+
+ Concurrency quota + {!isEditing && ( + + )} +
+ + + Cap on how much extra concurrency this org can purchase. Increases + unlock self-serve purchase up to the new cap; the org still has to + complete the purchase from the billing flow. + + + {savedJustNow && ( +
+ + Saved. + +
+ )} + + {formError && ( +
+ + {formError} + +
+ )} + + {!isEditing ? ( + + + Max extra concurrency this org can purchase on top of their plan + {currentQuota.toLocaleString()} + + + Already purchased + {purchased.toLocaleString()} + + + ) : ( +
+ + +
+ + setValue(e.target.value)} + disabled={usePlanDefault} + required={!usePlanDefault} + /> + {fieldError("extraConcurrencyQuota")} +
+ +
+ setUsePlanDefault(e.target.checked)} + /> + +
+ +
+ + +
+
+ )} +
+ ); +} diff --git a/apps/webapp/app/routes/admin.back-office.orgs.$orgId.tsx b/apps/webapp/app/routes/admin.back-office.orgs.$orgId.tsx index 260fef32d96..b65214bf19a 100644 --- a/apps/webapp/app/routes/admin.back-office.orgs.$orgId.tsx +++ b/apps/webapp/app/routes/admin.back-office.orgs.$orgId.tsx @@ -25,6 +25,12 @@ import { handleBatchRateLimitAction, resolveEffectiveBatchRateLimit, } from "~/components/admin/backOffice/BatchRateLimitSection.server"; +import { + CONCURRENCY_QUOTA_INTENT, + CONCURRENCY_QUOTA_SAVED_VALUE, + ConcurrencyQuotaSection, +} from "~/components/admin/backOffice/ConcurrencyQuotaSection"; +import { handleConcurrencyQuotaAction } from "~/components/admin/backOffice/ConcurrencyQuotaSection.server"; import { MAX_PROJECTS_INTENT, MAX_PROJECTS_SAVED_VALUE, @@ -36,6 +42,7 @@ import { CopyableText } from "~/components/primitives/CopyableText"; import { Header1 } from "~/components/primitives/Headers"; import { Paragraph } from "~/components/primitives/Paragraph"; import { prisma } from "~/db.server"; +import { getCurrentPlan } from "~/services/platform.v3.server"; import { requireUser } from "~/services/session.server"; const SAVED_QUERY_KEY = "saved"; @@ -73,7 +80,14 @@ export async function loader({ request, params }: LoaderFunctionArgs) { org.batchRateLimitConfig ); - return typedjson({ org, apiEffective, batchEffective }); + const currentPlan = await getCurrentPlan(org.id); + const concurrencyAddOn = currentPlan?.v3Subscription?.addOns?.concurrentRuns; + const concurrencyQuota = { + currentQuota: concurrencyAddOn?.quota ?? 0, + purchased: concurrencyAddOn?.purchased ?? 0, + }; + + return typedjson({ org, apiEffective, batchEffective, concurrencyQuota }); } export async function action({ request, params }: ActionFunctionArgs) { @@ -129,6 +143,23 @@ export async function action({ request, params }: ActionFunctionArgs) { ); } + if (intent === CONCURRENCY_QUOTA_INTENT) { + const result = await handleConcurrencyQuotaAction(formData, orgId, user.id); + if (!result.ok) { + return typedjson( + { + section: CONCURRENCY_QUOTA_SAVED_VALUE, + errors: result.errors, + formError: result.formError ?? null, + }, + { status: 400 } + ); + } + return redirect( + `/admin/back-office/orgs/${orgId}?${SAVED_QUERY_KEY}=${CONCURRENCY_QUOTA_SAVED_VALUE}` + ); + } + return typedjson( { section: null, errors: { intent: ["Unknown intent."] } }, { status: 400 } @@ -136,7 +167,7 @@ export async function action({ request, params }: ActionFunctionArgs) { } export default function BackOfficeOrgPage() { - const { org, apiEffective, batchEffective } = + const { org, apiEffective, batchEffective, concurrencyQuota } = useTypedLoaderData(); const actionData = useTypedActionData(); const navigation = useNavigation(); @@ -147,6 +178,9 @@ export default function BackOfficeOrgPage() { navigation.state !== "idle" && submittingIntent === BATCH_RATE_LIMIT_INTENT; const isSubmittingMaxProjects = navigation.state !== "idle" && submittingIntent === MAX_PROJECTS_INTENT; + const isSubmittingConcurrencyQuota = + navigation.state !== "idle" && + submittingIntent === CONCURRENCY_QUOTA_INTENT; const errorSection = actionData && "section" in actionData ? actionData.section : null; @@ -154,6 +188,10 @@ export default function BackOfficeOrgPage() { actionData && "errors" in actionData ? (actionData.errors as Record) : null; + const formError = + actionData && "formError" in actionData + ? ((actionData as { formError?: string | null }).formError ?? null) + : null; const [searchParams, setSearchParams] = useSearchParams(); const savedSectionRaw = searchParams.get(SAVED_QUERY_KEY); @@ -179,7 +217,7 @@ export default function BackOfficeOrgPage() { }, [savedSection, setSearchParams]); return ( -
+
{org.title} @@ -212,6 +250,17 @@ export default function BackOfficeOrgPage() { savedJustNow={savedSection === MAX_PROJECTS_SAVED_VALUE} isSubmitting={isSubmittingMaxProjects} /> + +
); } diff --git a/apps/webapp/app/routes/admin.tsx b/apps/webapp/app/routes/admin.tsx index 61431398220..141a74487b6 100644 --- a/apps/webapp/app/routes/admin.tsx +++ b/apps/webapp/app/routes/admin.tsx @@ -16,7 +16,7 @@ export async function loader({ request }: LoaderFunctionArgs) { export default function Page() { return ( -
+
Promise; + } + ).setExtraConcurrencyQuota(organizationId, body); + if (!result.success) { + recordPlatformFailure("setExtraConcurrencyQuota", "no_success"); + } + return result; + } catch (e) { + recordPlatformFailure("setExtraConcurrencyQuota", "caught"); + return undefined; + } +} + export async function setSeatsAddOn(organizationId: string, amount: number) { if (!client) return undefined; From 34d789ab69980a75d5aa21f833cf792787e3b267 Mon Sep 17 00:00:00 2001 From: isshaddad Date: Mon, 4 May 2026 08:02:03 -0400 Subject: [PATCH 2/2] refactor(webapp): concurrency quota editor --- .../ConcurrencyQuotaSection.server.ts | 37 ++---------- .../backOffice/ConcurrencyQuotaSection.tsx | 59 +++++++++++++------ 2 files changed, 45 insertions(+), 51 deletions(-) diff --git a/apps/webapp/app/components/admin/backOffice/ConcurrencyQuotaSection.server.ts b/apps/webapp/app/components/admin/backOffice/ConcurrencyQuotaSection.server.ts index 46b1d35b752..873f5b70871 100644 --- a/apps/webapp/app/components/admin/backOffice/ConcurrencyQuotaSection.server.ts +++ b/apps/webapp/app/components/admin/backOffice/ConcurrencyQuotaSection.server.ts @@ -3,38 +3,11 @@ import { logger } from "~/services/logger.server"; import { setExtraConcurrencyQuota } from "~/services/platform.v3.server"; import { CONCURRENCY_QUOTA_INTENT } from "./ConcurrencyQuotaSection"; -const RawSchema = z.object({ +const SetConcurrencyQuotaSchema = z.object({ intent: z.literal(CONCURRENCY_QUOTA_INTENT), - // Checkbox arrives as "on" / "true" when checked, absent when not. - usePlanDefault: z.string().optional(), - // Empty string when "Use plan default" is checked (the input is disabled). - extraConcurrencyQuota: z.string().optional(), -}); - -const SetConcurrencyQuotaSchema = RawSchema.transform((raw, ctx) => { - const usePlanDefault = !!raw.usePlanDefault; - if (usePlanDefault) { - return { extraConcurrencyQuota: null as number | null }; - } - const trimmed = (raw.extraConcurrencyQuota ?? "").trim(); - if (trimmed.length === 0) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "Enter a non-negative integer or check 'Use plan default'.", - path: ["extraConcurrencyQuota"], - }); - return z.NEVER; - } - const parsed = Number(trimmed); - if (!Number.isInteger(parsed) || parsed < 0) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "Quota must be a non-negative integer.", - path: ["extraConcurrencyQuota"], - }); - return z.NEVER; - } - return { extraConcurrencyQuota: parsed as number | null }; + // Capped at PostgreSQL INTEGER max for safety; cloud will reject anything + // unreasonably high on its own (likely with quota_too_high). + extraConcurrencyQuota: z.coerce.number().int().min(0).max(2_147_483_647), }); export type ConcurrencyQuotaActionResult = @@ -102,7 +75,7 @@ function mapCodeToMessage( ): string { switch (code) { case "invalid_body": - return "Quota must be a non-negative integer (or check 'Use plan default')."; + return "Quota must be a non-negative integer."; case "quota_too_high": // Cloud's `error` string embeds the actual ceiling, prefer it verbatim. return fallback || "Cap is too high."; diff --git a/apps/webapp/app/components/admin/backOffice/ConcurrencyQuotaSection.tsx b/apps/webapp/app/components/admin/backOffice/ConcurrencyQuotaSection.tsx index d7baf71408b..e74f389b68c 100644 --- a/apps/webapp/app/components/admin/backOffice/ConcurrencyQuotaSection.tsx +++ b/apps/webapp/app/components/admin/backOffice/ConcurrencyQuotaSection.tsx @@ -1,7 +1,6 @@ import { Form } from "@remix-run/react"; import { useEffect, useState } from "react"; import { Button } from "~/components/primitives/Buttons"; -import { Checkbox } from "~/components/primitives/Checkbox"; import { FormError } from "~/components/primitives/FormError"; import { Header2 } from "~/components/primitives/Headers"; import { Input } from "~/components/primitives/Input"; @@ -36,7 +35,6 @@ export function ConcurrencyQuotaSection({ errors && field in errors ? errors[field]?.[0] : undefined; const [isEditing, setIsEditing] = useState(hasFieldErrors || !!formError); - const [usePlanDefault, setUsePlanDefault] = useState(false); const [value, setValue] = useState(String(currentQuota)); useEffect(() => { @@ -49,10 +47,24 @@ export function ConcurrencyQuotaSection({ const cancelEdit = () => { setValue(String(currentQuota)); - setUsePlanDefault(false); setIsEditing(false); }; + const trimmedValue = value.trim(); + const parsed = Number(trimmedValue); + const isValidPreview = + trimmedValue.length > 0 && + Number.isInteger(parsed) && + parsed >= 0; + const delta = isValidPreview ? parsed - currentQuota : 0; + const deltaLabel = + delta > 0 + ? `+${delta.toLocaleString()}` + : delta < 0 + ? delta.toLocaleString() + : "no change"; + const headroomAfter = isValidPreview ? parsed - purchased : 0; + return (
@@ -111,32 +123,41 @@ export function ConcurrencyQuotaSection({ name="extraConcurrencyQuota" type="number" min={0} - value={usePlanDefault ? "" : value} + value={value} onChange={(e) => setValue(e.target.value)} - disabled={usePlanDefault} - required={!usePlanDefault} + required /> {fieldError("extraConcurrencyQuota")}
-
- setUsePlanDefault(e.target.checked)} - /> - -
+ {isValidPreview && ( +
+ + Cap: {currentQuota.toLocaleString()} →{" "} + {parsed.toLocaleString()} ({deltaLabel}) + + + Already purchased: {purchased.toLocaleString()} + + {headroomAfter >= 0 ? ( + + After save: {headroomAfter.toLocaleString()} more buyable. + + ) : ( + + Below already-purchased — org would be{" "} + {(-headroomAfter).toLocaleString()} over the new cap. They'd + keep what they have but couldn't buy more until you raise it. + + )} +
+ )}