feat: CPU/RAM limits per stage, NPM access list (global + per-project)

Resource limits:
- Add cpu_limit (cores) and memory_limit (MB) fields to Stage model
- Pass limits to Docker container via NanoCPUs and Memory in HostConfig
- Add CPU/Memory fields to stage creation form in project detail
- 0 = unlimited (default)

NPM access list:
- Add npm_access_list_id to Settings (global default) and Project (per-project override)
- Per-project overrides global when > 0
- NPM provider passes access_list_id when configuring proxy hosts
- Add GET /api/settings/npm-access-lists endpoint to list NPM access lists
- Add access list picker on NPM settings page (global)
- Add access list ID field on project edit form (per-project)
- DB migrations for all new columns
This commit is contained in:
2026-04-05 12:44:26 +03:00
parent c6d20ca26e
commit 7550fe9e32
12 changed files with 217 additions and 38 deletions
+26 -12
View File
@@ -35,6 +35,8 @@
let stageAutoDeploy = $state(true);
let stageEnableProxy = $state(true);
let stageMaxInstances = $state('1');
let stageCpuLimit = $state('');
let stageMemoryLimit = $state('');
let addingStage = $state(false);
async function handleAddStage() {
@@ -47,9 +49,11 @@
auto_deploy: stageAutoDeploy,
enable_proxy: stageEnableProxy,
max_instances: parseInt(stageMaxInstances) || 1,
cpu_limit: parseFloat(stageCpuLimit) || 0,
memory_limit: parseInt(stageMemoryLimit) || 0,
});
toasts.success($t('projectDetail.stageCreated', { name: stageName }));
stageName = ''; stageTagPattern = '*'; stageAutoDeploy = true; stageEnableProxy = true; stageMaxInstances = '1';
stageName = ''; stageTagPattern = '*'; stageAutoDeploy = true; stageEnableProxy = true; stageMaxInstances = '1'; stageCpuLimit = ''; stageMemoryLimit = '';
showAddStage = false;
await loadProject();
} catch (e) {
@@ -65,6 +69,7 @@
let editImage = $state('');
let editPort = $state('');
let editHealthcheck = $state('');
let editAccessListId = $state('');
let saving = $state(false);
function startEditing() {
@@ -73,6 +78,7 @@
editImage = project.image;
editPort = String(project.port || '');
editHealthcheck = project.healthcheck || '';
editAccessListId = String(project.npm_access_list_id || '0');
editing = true;
}
@@ -85,6 +91,7 @@
image: editImage.trim(),
port: parseInt(editPort) || 0,
healthcheck: editHealthcheck.trim(),
npm_access_list_id: parseInt(editAccessListId) || 0,
});
toasts.success($t('projectDetail.projectUpdated'));
editing = false;
@@ -281,6 +288,7 @@
<FormField label={$t('projectDetail.imageLabel')} name="editImage" bind:value={editImage} />
<FormField label={$t('projectDetail.portLabel')} name="editPort" type="number" bind:value={editPort} />
<FormField label={$t('projectDetail.healthcheckLabel')} name="editHealthcheck" bind:value={editHealthcheck} placeholder="/api/health" />
<FormField label={$t('projectDetail.accessListId')} name="editAccessListId" type="number" bind:value={editAccessListId} placeholder="0" helpText={$t('projectDetail.accessListIdHelp')} />
</div>
<div class="mt-4 flex items-center gap-2 justify-end">
<button
@@ -349,23 +357,29 @@
{#if showAddStage}
<div class="mt-3 rounded-xl border border-[var(--border-primary)] bg-[var(--surface-card)] p-4 animate-scale-in">
<div class="grid grid-cols-2 gap-3 sm:grid-cols-5">
<div class="grid grid-cols-2 gap-3 sm:grid-cols-4">
<FormField label={$t('projectDetail.nameLabel')} name="stageName" bind:value={stageName} placeholder="dev" />
<FormField label={$t('projectDetail.tagPattern')} name="stagePattern" bind:value={stageTagPattern} placeholder="dev-*" helpText={$t('projectDetail.tagPatternHelp')} />
<FormField label={$t('projectDetail.maxInstances')} name="stageMax" type="number" bind:value={stageMaxInstances} />
<div class="flex flex-col gap-1.5">
<label class="text-sm font-medium text-[var(--text-primary)]">{$t('projectDetail.autoDeployLabel')}</label>
<div class="flex items-center h-[38px]">
<ToggleSwitch bind:checked={stageAutoDeploy} label={$t('projectDetail.autoDeployLabel')} />
</div>
</div>
<div class="flex flex-col gap-1.5">
<label class="text-sm font-medium text-[var(--text-primary)]">{$t('projectDetail.enableProxy')}</label>
<div class="flex items-center h-[38px]">
<ToggleSwitch bind:checked={stageEnableProxy} label={$t('projectDetail.enableProxy')} />
<div class="flex gap-4 items-end">
<div class="flex flex-col gap-1.5">
<label class="text-sm font-medium text-[var(--text-primary)]">{$t('projectDetail.autoDeployLabel')}</label>
<div class="flex items-center h-[38px]">
<ToggleSwitch bind:checked={stageAutoDeploy} label={$t('projectDetail.autoDeployLabel')} />
</div>
</div>
<div class="flex flex-col gap-1.5">
<label class="text-sm font-medium text-[var(--text-primary)]">{$t('projectDetail.enableProxy')}</label>
<div class="flex items-center h-[38px]">
<ToggleSwitch bind:checked={stageEnableProxy} label={$t('projectDetail.enableProxy')} />
</div>
</div>
</div>
</div>
<div class="mt-3 grid grid-cols-2 gap-3 sm:grid-cols-4">
<FormField label={$t('projectDetail.cpuLimit')} name="stageCpu" type="number" bind:value={stageCpuLimit} placeholder="0" helpText={$t('projectDetail.cpuLimitHelp')} />
<FormField label={$t('projectDetail.memoryLimit')} name="stageMem" type="number" bind:value={stageMemoryLimit} placeholder="0" helpText={$t('projectDetail.memoryLimitHelp')} />
</div>
<div class="mt-3 flex justify-end">
<button
type="button"