refactor: CPU config to millicores, fix replica health, reorder tabs
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m18s
CI / docker (push) Successful in 1m5s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Has been cancelled

- Rename cpuShares to cpuRequest (millicores), cpuLimit from cores to
  millicores. ResolvedContainerConfig translates to Docker-native units
  via dockerCpuShares() and dockerCpuQuota() helpers. Future K8s
  orchestrator can pass millicores through directly.
- Fix waitForAnyHealthy to wait for ALL replicas instead of returning
  on first healthy one. Prevents false DEGRADED status with 2+ replicas.
- Default app detail to Configuration tab (was Overview)
- Reorder config sub-tabs: Monitoring, Resources, Variables, Traces &
  Taps, Route Recording

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-09 07:38:23 +02:00
parent eb7cd9ba62
commit e88db56f79
5 changed files with 57 additions and 46 deletions

View File

@@ -37,8 +37,8 @@ public class DeploymentExecutor {
@Value("${cameleer.runtime.container-memory-limit:512m}") @Value("${cameleer.runtime.container-memory-limit:512m}")
private String globalMemoryLimit; private String globalMemoryLimit;
@Value("${cameleer.runtime.container-cpu-shares:512}") @Value("${cameleer.runtime.container-cpu-request:500}")
private int globalCpuShares; private int globalCpuRequest;
@Value("${cameleer.runtime.health-check-timeout:60}") @Value("${cameleer.runtime.health-check-timeout:60}")
private int healthCheckTimeout; private int healthCheckTimeout;
@@ -86,7 +86,7 @@ public class DeploymentExecutor {
var globalDefaults = new ConfigMerger.GlobalRuntimeDefaults( var globalDefaults = new ConfigMerger.GlobalRuntimeDefaults(
parseMemoryLimitMb(globalMemoryLimit), parseMemoryLimitMb(globalMemoryLimit),
globalCpuShares, globalCpuRequest,
globalRoutingMode, globalRoutingMode,
globalRoutingDomain, globalRoutingDomain,
globalServerUrl.isBlank() ? "http://cameleer3-server:8081" : globalServerUrl globalServerUrl.isBlank() ? "http://cameleer3-server:8081" : globalServerUrl
@@ -126,7 +126,6 @@ public class DeploymentExecutor {
for (int i = 0; i < config.replicas(); i++) { for (int i = 0; i < config.replicas(); i++) {
String containerName = env.slug() + "-" + app.slug() + "-" + i; String containerName = env.slug() + "-" + app.slug() + "-" + i;
Long cpuQuota = config.cpuLimit() != null ? (long) (config.cpuLimit() * 100_000) : null;
String volumeName = jarDockerVolume != null && !jarDockerVolume.isBlank() ? jarDockerVolume : null; String volumeName = jarDockerVolume != null && !jarDockerVolume.isBlank() ? jarDockerVolume : null;
ContainerRequest request = new ContainerRequest( ContainerRequest request = new ContainerRequest(
@@ -136,7 +135,7 @@ public class DeploymentExecutor {
envNet != null ? List.of(envNet) : List.of(), envNet != null ? List.of(envNet) : List.of(),
baseEnvVars, labels, baseEnvVars, labels,
config.memoryLimitBytes(), config.memoryReserveBytes(), config.memoryLimitBytes(), config.memoryReserveBytes(),
config.cpuShares(), cpuQuota, config.dockerCpuShares(), config.dockerCpuQuota(),
config.exposedPorts(), agentHealthPort, config.exposedPorts(), agentHealthPort,
"on-failure", 3 "on-failure", 3
); );
@@ -273,19 +272,21 @@ public class DeploymentExecutor {
private int waitForAnyHealthy(List<String> containerIds, int timeoutSeconds) { private int waitForAnyHealthy(List<String> containerIds, int timeoutSeconds) {
long deadline = System.currentTimeMillis() + (timeoutSeconds * 1000L); long deadline = System.currentTimeMillis() + (timeoutSeconds * 1000L);
int lastHealthy = 0;
while (System.currentTimeMillis() < deadline) { while (System.currentTimeMillis() < deadline) {
int healthy = 0; int healthy = 0;
for (String cid : containerIds) { for (String cid : containerIds) {
ContainerStatus status = orchestrator.getContainerStatus(cid); ContainerStatus status = orchestrator.getContainerStatus(cid);
if ("healthy".equals(status.state())) healthy++; if ("healthy".equals(status.state())) healthy++;
} }
if (healthy > 0) return healthy; lastHealthy = healthy;
if (healthy == containerIds.size()) return healthy;
try { Thread.sleep(2000); } catch (InterruptedException e) { try { Thread.sleep(2000); } catch (InterruptedException e) {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
return 0; return lastHealthy;
} }
} }
return 0; return lastHealthy;
} }
private List<Map<String, Object>> updateReplicaHealth(List<Map<String, Object>> replicas, private List<Map<String, Object>> updateReplicaHealth(List<Map<String, Object>> replicas,

View File

@@ -17,8 +17,8 @@ public final class ConfigMerger {
return new ResolvedContainerConfig( return new ResolvedContainerConfig(
intVal(appConfig, envConfig, "memoryLimitMb", global.memoryLimitMb()), intVal(appConfig, envConfig, "memoryLimitMb", global.memoryLimitMb()),
intOrNull(appConfig, envConfig, "memoryReserveMb"), intOrNull(appConfig, envConfig, "memoryReserveMb"),
intVal(appConfig, envConfig, "cpuShares", global.cpuShares()), intVal(appConfig, envConfig, "cpuRequest", global.cpuRequest()),
doubleOrNull(appConfig, envConfig, "cpuLimit"), intOrNull(appConfig, envConfig, "cpuLimit"),
intVal(appConfig, envConfig, "appPort", 8080), intVal(appConfig, envConfig, "appPort", 8080),
intList(appConfig, envConfig, "exposedPorts"), intList(appConfig, envConfig, "exposedPorts"),
stringMap(appConfig, envConfig, "customEnvVars"), stringMap(appConfig, envConfig, "customEnvVars"),
@@ -88,7 +88,7 @@ public final class ConfigMerger {
/** Global defaults extracted from application.yml @Value fields */ /** Global defaults extracted from application.yml @Value fields */
public record GlobalRuntimeDefaults( public record GlobalRuntimeDefaults(
int memoryLimitMb, int memoryLimitMb,
int cpuShares, int cpuRequest,
String routingMode, String routingMode,
String routingDomain, String routingDomain,
String serverUrl String serverUrl

View File

@@ -6,8 +6,8 @@ import java.util.Map;
public record ResolvedContainerConfig( public record ResolvedContainerConfig(
int memoryLimitMb, int memoryLimitMb,
Integer memoryReserveMb, Integer memoryReserveMb,
int cpuShares, int cpuRequest,
Double cpuLimit, Integer cpuLimit,
int appPort, int appPort,
List<Integer> exposedPorts, List<Integer> exposedPorts,
Map<String, String> customEnvVars, Map<String, String> customEnvVars,
@@ -26,4 +26,14 @@ public record ResolvedContainerConfig(
public Long memoryReserveBytes() { public Long memoryReserveBytes() {
return memoryReserveMb != null ? (long) memoryReserveMb * 1024 * 1024 : null; return memoryReserveMb != null ? (long) memoryReserveMb * 1024 * 1024 : null;
} }
/** Convert cpuRequest (millicores) to Docker CPU shares (proportional to 1024 = 1 core). */
public int dockerCpuShares() {
return cpuRequest * 1024 / 1000;
}
/** Convert cpuLimit (millicores) to Docker CPU quota (microseconds per 100ms period). */
public Long dockerCpuQuota() {
return cpuLimit != null ? (long) cpuLimit * 100 : null;
}
} }

View File

@@ -331,7 +331,7 @@ function DefaultResourcesSection({ environment, onSave, saving }: {
const [editing, setEditing] = useState(false); const [editing, setEditing] = useState(false);
const [memoryLimit, setMemoryLimit] = useState(''); const [memoryLimit, setMemoryLimit] = useState('');
const [memoryReserve, setMemoryReserve] = useState(''); const [memoryReserve, setMemoryReserve] = useState('');
const [cpuShares, setCpuShares] = useState(''); const [cpuRequest, setCpuRequest] = useState('');
const [cpuLimit, setCpuLimit] = useState(''); const [cpuLimit, setCpuLimit] = useState('');
const [routingMode, setRoutingMode] = useState(String(defaults.routingMode ?? 'path')); const [routingMode, setRoutingMode] = useState(String(defaults.routingMode ?? 'path'));
const [routingDomain, setRoutingDomain] = useState(String(defaults.routingDomain ?? '')); const [routingDomain, setRoutingDomain] = useState(String(defaults.routingDomain ?? ''));
@@ -341,7 +341,7 @@ function DefaultResourcesSection({ environment, onSave, saving }: {
useEffect(() => { useEffect(() => {
setMemoryLimit(String(defaults.memoryLimitMb ?? '')); setMemoryLimit(String(defaults.memoryLimitMb ?? ''));
setMemoryReserve(String(defaults.memoryReserveMb ?? '')); setMemoryReserve(String(defaults.memoryReserveMb ?? ''));
setCpuShares(String(defaults.cpuShares ?? '')); setCpuRequest(String(defaults.cpuRequest ?? ''));
setCpuLimit(String(defaults.cpuLimit ?? '')); setCpuLimit(String(defaults.cpuLimit ?? ''));
setRoutingMode(String(environment.defaultContainerConfig.routingMode ?? 'path')); setRoutingMode(String(environment.defaultContainerConfig.routingMode ?? 'path'));
setRoutingDomain(String(environment.defaultContainerConfig.routingDomain ?? '')); setRoutingDomain(String(environment.defaultContainerConfig.routingDomain ?? ''));
@@ -353,7 +353,7 @@ function DefaultResourcesSection({ environment, onSave, saving }: {
function handleCancel() { function handleCancel() {
setMemoryLimit(String(defaults.memoryLimitMb ?? '')); setMemoryLimit(String(defaults.memoryLimitMb ?? ''));
setMemoryReserve(String(defaults.memoryReserveMb ?? '')); setMemoryReserve(String(defaults.memoryReserveMb ?? ''));
setCpuShares(String(defaults.cpuShares ?? '')); setCpuRequest(String(defaults.cpuRequest ?? ''));
setCpuLimit(String(defaults.cpuLimit ?? '')); setCpuLimit(String(defaults.cpuLimit ?? ''));
setRoutingMode(String(defaults.routingMode ?? 'path')); setRoutingMode(String(defaults.routingMode ?? 'path'));
setRoutingDomain(String(defaults.routingDomain ?? '')); setRoutingDomain(String(defaults.routingDomain ?? ''));
@@ -366,8 +366,8 @@ function DefaultResourcesSection({ environment, onSave, saving }: {
await onSave({ await onSave({
memoryLimitMb: memoryLimit ? parseInt(memoryLimit) : null, memoryLimitMb: memoryLimit ? parseInt(memoryLimit) : null,
memoryReserveMb: memoryReserve ? parseInt(memoryReserve) : null, memoryReserveMb: memoryReserve ? parseInt(memoryReserve) : null,
cpuShares: cpuShares ? parseInt(cpuShares) : null, cpuRequest: cpuRequest ? parseInt(cpuRequest) : null,
cpuLimit: cpuLimit ? parseFloat(cpuLimit) : null, cpuLimit: cpuLimit ? parseInt(cpuLimit) : null,
routingMode, routingMode,
routingDomain: routingDomain || null, routingDomain: routingDomain || null,
serverUrl: serverUrl || null, serverUrl: serverUrl || null,
@@ -393,15 +393,15 @@ function DefaultResourcesSection({ environment, onSave, saving }: {
? <Input value={memoryReserve} onChange={(e) => setMemoryReserve(e.target.value)} placeholder="e.g. 256" style={{ width: 100 }} /> ? <Input value={memoryReserve} onChange={(e) => setMemoryReserve(e.target.value)} placeholder="e.g. 256" style={{ width: 100 }} />
: <span className={styles.metaValue}>{defaults.memoryReserveMb ? `${defaults.memoryReserveMb} MB` : '—'}</span>} : <span className={styles.metaValue}>{defaults.memoryReserveMb ? `${defaults.memoryReserveMb} MB` : '—'}</span>}
<span className={styles.metaLabel}>CPU Shares</span> <span className={styles.metaLabel}>CPU Request</span>
{editing {editing
? <Input value={cpuShares} onChange={(e) => setCpuShares(e.target.value)} placeholder="e.g. 512" style={{ width: 100 }} /> ? <Input value={cpuRequest} onChange={(e) => setCpuRequest(e.target.value)} placeholder="e.g. 500" style={{ width: 100 }} />
: <span className={styles.metaValue}>{String(defaults.cpuShares ?? '—')}</span>} : <span className={styles.metaValue}>{defaults.cpuRequest ? `${defaults.cpuRequest}m` : '—'}</span>}
<span className={styles.metaLabel}>CPU Limit</span> <span className={styles.metaLabel}>CPU Limit</span>
{editing {editing
? <Input value={cpuLimit} onChange={(e) => setCpuLimit(e.target.value)} placeholder="e.g. 1.0" style={{ width: 100 }} /> ? <Input value={cpuLimit} onChange={(e) => setCpuLimit(e.target.value)} placeholder="e.g. 1000" style={{ width: 100 }} />
: <span className={styles.metaValue}>{defaults.cpuLimit ? `${defaults.cpuLimit} cores` : '—'}</span>} : <span className={styles.metaValue}>{defaults.cpuLimit ? `${defaults.cpuLimit}m` : '—'}</span>}
<span className={styles.metaLabel}>Routing Mode</span> <span className={styles.metaLabel}>Routing Mode</span>
{editing {editing

View File

@@ -171,7 +171,7 @@ function CreateAppView({ environments, selectedEnv }: { environments: Environmen
const defaults = env?.defaultContainerConfig ?? {}; const defaults = env?.defaultContainerConfig ?? {};
const [memoryLimit, setMemoryLimit] = useState(String(defaults.memoryLimitMb ?? 512)); const [memoryLimit, setMemoryLimit] = useState(String(defaults.memoryLimitMb ?? 512));
const [memoryReserve, setMemoryReserve] = useState(String(defaults.memoryReserveMb ?? '')); const [memoryReserve, setMemoryReserve] = useState(String(defaults.memoryReserveMb ?? ''));
const [cpuShares, setCpuShares] = useState(String(defaults.cpuShares ?? 512)); const [cpuRequest, setCpuRequest] = useState(String(defaults.cpuRequest ?? 500));
const [cpuLimit, setCpuLimit] = useState(String(defaults.cpuLimit ?? '')); const [cpuLimit, setCpuLimit] = useState(String(defaults.cpuLimit ?? ''));
const [ports, setPorts] = useState<number[]>(Array.isArray(defaults.exposedPorts) ? defaults.exposedPorts as number[] : []); const [ports, setPorts] = useState<number[]>(Array.isArray(defaults.exposedPorts) ? defaults.exposedPorts as number[] : []);
const [newPort, setNewPort] = useState(''); const [newPort, setNewPort] = useState('');
@@ -182,7 +182,7 @@ function CreateAppView({ environments, selectedEnv }: { environments: Environmen
const [stripPrefix, setStripPrefix] = useState(true); const [stripPrefix, setStripPrefix] = useState(true);
const [sslOffloading, setSslOffloading] = useState(true); const [sslOffloading, setSslOffloading] = useState(true);
const [configTab, setConfigTab] = useState<'variables' | 'monitoring' | 'resources'>('variables'); const [configTab, setConfigTab] = useState<'monitoring' | 'resources' | 'variables'>('monitoring');
const [busy, setBusy] = useState(false); const [busy, setBusy] = useState(false);
const [step, setStep] = useState(''); const [step, setStep] = useState('');
@@ -191,7 +191,7 @@ function CreateAppView({ environments, selectedEnv }: { environments: Environmen
const d = environments.find((e) => e.id === envId)?.defaultContainerConfig ?? {}; const d = environments.find((e) => e.id === envId)?.defaultContainerConfig ?? {};
setMemoryLimit(String(d.memoryLimitMb ?? 512)); setMemoryLimit(String(d.memoryLimitMb ?? 512));
setMemoryReserve(String(d.memoryReserveMb ?? '')); setMemoryReserve(String(d.memoryReserveMb ?? ''));
setCpuShares(String(d.cpuShares ?? 512)); setCpuRequest(String(d.cpuRequest ?? 500));
setCpuLimit(String(d.cpuLimit ?? '')); setCpuLimit(String(d.cpuLimit ?? ''));
setPorts(Array.isArray(d.exposedPorts) ? d.exposedPorts as number[] : []); setPorts(Array.isArray(d.exposedPorts) ? d.exposedPorts as number[] : []);
}, [envId, environments]); }, [envId, environments]);
@@ -224,8 +224,8 @@ function CreateAppView({ environments, selectedEnv }: { environments: Environmen
const containerConfig: Record<string, unknown> = { const containerConfig: Record<string, unknown> = {
memoryLimitMb: memoryLimit ? parseInt(memoryLimit) : null, memoryLimitMb: memoryLimit ? parseInt(memoryLimit) : null,
memoryReserveMb: memoryReserve ? parseInt(memoryReserve) : null, memoryReserveMb: memoryReserve ? parseInt(memoryReserve) : null,
cpuShares: cpuShares ? parseInt(cpuShares) : null, cpuRequest: cpuRequest ? parseInt(cpuRequest) : null,
cpuLimit: cpuLimit ? parseFloat(cpuLimit) : null, cpuLimit: cpuLimit ? parseInt(cpuLimit) : null,
exposedPorts: ports, exposedPorts: ports,
customEnvVars: Object.fromEntries(envVars.filter((v) => v.key.trim()).map((v) => [v.key, v.value])), customEnvVars: Object.fromEntries(envVars.filter((v) => v.key.trim()).map((v) => [v.key, v.value])),
appPort: appPort ? parseInt(appPort) : 8080, appPort: appPort ? parseInt(appPort) : 8080,
@@ -322,9 +322,9 @@ function CreateAppView({ environments, selectedEnv }: { environments: Environmen
{/* Config Tabs */} {/* Config Tabs */}
<div className={styles.subTabs}> <div className={styles.subTabs}>
<button className={`${styles.subTab} ${configTab === 'variables' ? styles.subTabActive : ''}`} onClick={() => setConfigTab('variables')}>Variables</button>
<button className={`${styles.subTab} ${configTab === 'monitoring' ? styles.subTabActive : ''}`} onClick={() => setConfigTab('monitoring')}>Monitoring</button> <button className={`${styles.subTab} ${configTab === 'monitoring' ? styles.subTabActive : ''}`} onClick={() => setConfigTab('monitoring')}>Monitoring</button>
<button className={`${styles.subTab} ${configTab === 'resources' ? styles.subTabActive : ''}`} onClick={() => setConfigTab('resources')}>Resources</button> <button className={`${styles.subTab} ${configTab === 'resources' ? styles.subTabActive : ''}`} onClick={() => setConfigTab('resources')}>Resources</button>
<button className={`${styles.subTab} ${configTab === 'variables' ? styles.subTabActive : ''}`} onClick={() => setConfigTab('variables')}>Variables</button>
</div> </div>
{configTab === 'variables' && ( {configTab === 'variables' && (
@@ -421,13 +421,13 @@ function CreateAppView({ environments, selectedEnv }: { environments: Environmen
{!isProd && <span className={styles.configHint}>Available in production environments only</span>} {!isProd && <span className={styles.configHint}>Available in production environments only</span>}
</div> </div>
<span className={styles.configLabel}>CPU Shares</span> <span className={styles.configLabel}>CPU Request</span>
<Input disabled={busy} value={cpuShares} onChange={(e) => setCpuShares(e.target.value)} style={{ width: 80 }} /> <Input disabled={busy} value={cpuRequest} onChange={(e) => setCpuRequest(e.target.value)} style={{ width: 80 }} />
<span className={styles.configLabel}>CPU Limit</span> <span className={styles.configLabel}>CPU Limit</span>
<div className={styles.configInline}> <div className={styles.configInline}>
<Input disabled={busy} value={cpuLimit} onChange={(e) => setCpuLimit(e.target.value)} placeholder="e.g. 1.0" style={{ width: 80 }} /> <Input disabled={busy} value={cpuLimit} onChange={(e) => setCpuLimit(e.target.value)} placeholder="e.g. 1000" style={{ width: 80 }} />
<span className={styles.cellMeta}>cores</span> <span className={styles.cellMeta}>millicores</span>
</div> </div>
<span className={styles.configLabel}>Exposed Ports</span> <span className={styles.configLabel}>Exposed Ports</span>
@@ -488,7 +488,7 @@ function AppDetailView({ appId: appSlug, environments, selectedEnv }: { appId: s
const stopDeployment = useStopDeployment(); const stopDeployment = useStopDeployment();
const deleteApp = useDeleteApp(); const deleteApp = useDeleteApp();
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
const [subTab, setSubTab] = useState<'overview' | 'config'>('overview'); const [subTab, setSubTab] = useState<'overview' | 'config'>('config');
const [deleteConfirm, setDeleteConfirm] = useState(false); const [deleteConfirm, setDeleteConfirm] = useState(false);
const envMap = useMemo(() => new Map(environments.map((e) => [e.id, e])), [environments]); const envMap = useMemo(() => new Map(environments.map((e) => [e.id, e])), [environments]);
@@ -549,8 +549,8 @@ function AppDetailView({ appId: appSlug, environments, selectedEnv }: { appId: s
</div> </div>
<div className={styles.subTabs}> <div className={styles.subTabs}>
<button className={`${styles.subTab} ${subTab === 'overview' ? styles.subTabActive : ''}`} onClick={() => setSubTab('overview')}>Overview</button>
<button className={`${styles.subTab} ${subTab === 'config' ? styles.subTabActive : ''}`} onClick={() => setSubTab('config')}>Configuration</button> <button className={`${styles.subTab} ${subTab === 'config' ? styles.subTabActive : ''}`} onClick={() => setSubTab('config')}>Configuration</button>
<button className={`${styles.subTab} ${subTab === 'overview' ? styles.subTabActive : ''}`} onClick={() => setSubTab('overview')}>Overview</button>
</div> </div>
{subTab === 'overview' && ( {subTab === 'overview' && (
@@ -702,7 +702,7 @@ function ConfigSubTab({ app, environment }: { app: App; environment?: Environmen
const { data: processorToRoute = {} } = useProcessorRouteMapping(app.slug); const { data: processorToRoute = {} } = useProcessorRouteMapping(app.slug);
const isProd = environment?.production ?? false; const isProd = environment?.production ?? false;
const [editing, setEditing] = useState(false); const [editing, setEditing] = useState(false);
const [configTab, setConfigTab] = useState<'variables' | 'monitoring' | 'traces' | 'recording' | 'resources'>('variables'); const [configTab, setConfigTab] = useState<'monitoring' | 'resources' | 'variables' | 'traces' | 'recording'>('monitoring');
const appRoutes: CatalogRoute[] = useMemo(() => { const appRoutes: CatalogRoute[] = useMemo(() => {
if (!catalog) return []; if (!catalog) return [];
@@ -731,7 +731,7 @@ function ConfigSubTab({ app, environment }: { app: App; environment?: Environmen
const merged = useMemo(() => ({ ...defaults, ...app.containerConfig }), [defaults, app.containerConfig]); const merged = useMemo(() => ({ ...defaults, ...app.containerConfig }), [defaults, app.containerConfig]);
const [memoryLimit, setMemoryLimit] = useState('512'); const [memoryLimit, setMemoryLimit] = useState('512');
const [memoryReserve, setMemoryReserve] = useState(''); const [memoryReserve, setMemoryReserve] = useState('');
const [cpuShares, setCpuShares] = useState('512'); const [cpuRequest, setCpuRequest] = useState('500');
const [cpuLimit, setCpuLimit] = useState(''); const [cpuLimit, setCpuLimit] = useState('');
const [ports, setPorts] = useState<number[]>([]); const [ports, setPorts] = useState<number[]>([]);
const [newPort, setNewPort] = useState(''); const [newPort, setNewPort] = useState('');
@@ -758,7 +758,7 @@ function ConfigSubTab({ app, environment }: { app: App; environment?: Environmen
} }
setMemoryLimit(String(merged.memoryLimitMb ?? 512)); setMemoryLimit(String(merged.memoryLimitMb ?? 512));
setMemoryReserve(String(merged.memoryReserveMb ?? '')); setMemoryReserve(String(merged.memoryReserveMb ?? ''));
setCpuShares(String(merged.cpuShares ?? 512)); setCpuRequest(String(merged.cpuRequest ?? 500));
setCpuLimit(String(merged.cpuLimit ?? '')); setCpuLimit(String(merged.cpuLimit ?? ''));
setPorts(Array.isArray(merged.exposedPorts) ? merged.exposedPorts as number[] : []); setPorts(Array.isArray(merged.exposedPorts) ? merged.exposedPorts as number[] : []);
const vars = merged.customEnvVars as Record<string, string> | undefined; const vars = merged.customEnvVars as Record<string, string> | undefined;
@@ -808,8 +808,8 @@ function ConfigSubTab({ app, environment }: { app: App; environment?: Environmen
const containerConfig: Record<string, unknown> = { const containerConfig: Record<string, unknown> = {
memoryLimitMb: memoryLimit ? parseInt(memoryLimit) : null, memoryLimitMb: memoryLimit ? parseInt(memoryLimit) : null,
memoryReserveMb: memoryReserve ? parseInt(memoryReserve) : null, memoryReserveMb: memoryReserve ? parseInt(memoryReserve) : null,
cpuShares: cpuShares ? parseInt(cpuShares) : null, cpuRequest: cpuRequest ? parseInt(cpuRequest) : null,
cpuLimit: cpuLimit ? parseFloat(cpuLimit) : null, cpuLimit: cpuLimit ? parseInt(cpuLimit) : null,
exposedPorts: ports, exposedPorts: ports,
customEnvVars: Object.fromEntries(envVars.filter((v) => v.key.trim()).map((v) => [v.key, v.value])), customEnvVars: Object.fromEntries(envVars.filter((v) => v.key.trim()).map((v) => [v.key, v.value])),
appPort: appPort ? parseInt(appPort) : 8080, appPort: appPort ? parseInt(appPort) : 8080,
@@ -910,11 +910,11 @@ function ConfigSubTab({ app, environment }: { app: App; environment?: Environmen
)} )}
<div className={styles.subTabs}> <div className={styles.subTabs}>
<button className={`${styles.subTab} ${configTab === 'variables' ? styles.subTabActive : ''}`} onClick={() => setConfigTab('variables')}>Variables</button>
<button className={`${styles.subTab} ${configTab === 'monitoring' ? styles.subTabActive : ''}`} onClick={() => setConfigTab('monitoring')}>Monitoring</button> <button className={`${styles.subTab} ${configTab === 'monitoring' ? styles.subTabActive : ''}`} onClick={() => setConfigTab('monitoring')}>Monitoring</button>
<button className={`${styles.subTab} ${configTab === 'resources' ? styles.subTabActive : ''}`} onClick={() => setConfigTab('resources')}>Resources</button>
<button className={`${styles.subTab} ${configTab === 'variables' ? styles.subTabActive : ''}`} onClick={() => setConfigTab('variables')}>Variables</button>
<button className={`${styles.subTab} ${configTab === 'traces' ? styles.subTabActive : ''}`} onClick={() => setConfigTab('traces')}>Traces & Taps</button> <button className={`${styles.subTab} ${configTab === 'traces' ? styles.subTabActive : ''}`} onClick={() => setConfigTab('traces')}>Traces & Taps</button>
<button className={`${styles.subTab} ${configTab === 'recording' ? styles.subTabActive : ''}`} onClick={() => setConfigTab('recording')}>Route Recording</button> <button className={`${styles.subTab} ${configTab === 'recording' ? styles.subTabActive : ''}`} onClick={() => setConfigTab('recording')}>Route Recording</button>
<button className={`${styles.subTab} ${configTab === 'resources' ? styles.subTabActive : ''}`} onClick={() => setConfigTab('resources')}>Resources</button>
</div> </div>
{configTab === 'variables' && ( {configTab === 'variables' && (
@@ -1033,13 +1033,13 @@ function ConfigSubTab({ app, environment }: { app: App; environment?: Environmen
{!isProd && <span className={styles.configHint}>Available in production environments only</span>} {!isProd && <span className={styles.configHint}>Available in production environments only</span>}
</div> </div>
<span className={styles.configLabel}>CPU Shares</span> <span className={styles.configLabel}>CPU Request</span>
<Input disabled={!editing} value={cpuShares} onChange={(e) => setCpuShares(e.target.value)} style={{ width: 80 }} /> <Input disabled={!editing} value={cpuRequest} onChange={(e) => setCpuRequest(e.target.value)} style={{ width: 80 }} />
<span className={styles.configLabel}>CPU Limit</span> <span className={styles.configLabel}>CPU Limit</span>
<div className={styles.configInline}> <div className={styles.configInline}>
<Input disabled={!editing} value={cpuLimit} onChange={(e) => setCpuLimit(e.target.value)} placeholder="e.g. 1.0" style={{ width: 80 }} /> <Input disabled={!editing} value={cpuLimit} onChange={(e) => setCpuLimit(e.target.value)} placeholder="e.g. 1000" style={{ width: 80 }} />
<span className={styles.cellMeta}>cores</span> <span className={styles.cellMeta}>millicores</span>
</div> </div>
<span className={styles.configLabel}>Exposed Ports</span> <span className={styles.configLabel}>Exposed Ports</span>