feat: per-service scaling section on service detail

Exposes the existing /scaling/* API in the frontend: replica state,
policy create/edit form (min/max, target CPU/memory, enable toggle),
manual scale control, and disable-policy action matching the DELETE
endpoint's disable-not-remove semantics.

Verified live: policy CRUD, state auto-registration on policy create,
replica bounds rejection, scheduler dispatch (fails cleanly with "no
ready nodes" when no agents are registered). Noted in roadmap:
policies are in-memory and lost on restart — tracked as Phase 4 debt.

Authored By: TDvorak <[email protected]>

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Tomas DvorakandDevin committed 2026-09-17 18:10:52 +02:00
1 parent 8ecc8dcea5
commit 44a1b09dad
3 files changed
+273 -5

No files matched your search

+53
View File
@@ -1482,3 +1482,56 @@ export async function promotePreviewEnvironment(
body: JSON.stringify(input),
});
}
export type ScalingPolicy = components['schemas']['ScalingPolicy'];
export type ServiceScalingState = {
ServiceID?: string;
CurrentReplicas?: number;
DesiredReplicas?: number;
LastScaleAction?: string;
LastScaleDirection?: string;
};
export async function getScalingPolicy(serviceId: string): Promise<ScalingPolicy | null> {
try {
const payload = await requestJson<{ policy?: ScalingPolicy }>(
`/scaling/policies/${encodeURIComponent(serviceId)}`,
);
return payload.policy ?? null;
} catch (error) {
if (error instanceof ApiError && error.status === 404) {
return null;
}
throw error;
}
}
export async function setScalingPolicy(policy: ScalingPolicy): Promise<void> {
await requestJson('/scaling/policies', { method: 'POST', body: JSON.stringify(policy) });
}
export async function deleteScalingPolicy(serviceId: string): Promise<void> {
await requestJson(`/scaling/policies/${encodeURIComponent(serviceId)}`, { method: 'DELETE' });
}
export async function getServiceScalingState(serviceId: string): Promise<ServiceScalingState | null> {
try {
const payload = await requestJson<{ state?: ServiceScalingState }>(
`/scaling/services/${encodeURIComponent(serviceId)}`,
);
return payload.state ?? null;
} catch (error) {
if (error instanceof ApiError && error.status === 404) {
return null;
}
throw error;
}
}
export async function manualScaleService(serviceId: string, replicas: number, reason = ''): Promise<void> {
await requestJson(`/scaling/services/${encodeURIComponent(serviceId)}/scale`, {
method: 'POST',
body: JSON.stringify({ replicas, reason }),
});
}