diff --git a/src/components/create-job/steps/deployment/ServiceDeployment.tsx b/src/components/create-job/steps/deployment/ServiceDeployment.tsx
index f907ff94..0297d958 100644
--- a/src/components/create-job/steps/deployment/ServiceDeployment.tsx
+++ b/src/components/create-job/steps/deployment/ServiceDeployment.tsx
@@ -78,7 +78,12 @@ function ServiceDeployment({ isEditingRunningJob }: { isEditingRunningJob?: bool
-
+
diff --git a/src/components/edit-job/JobEditFormWrapper.tsx b/src/components/edit-job/JobEditFormWrapper.tsx
index 1e929198..ccc679ee 100644
--- a/src/components/edit-job/JobEditFormWrapper.tsx
+++ b/src/components/edit-job/JobEditFormWrapper.tsx
@@ -4,11 +4,18 @@ import Plugins from '@components/create-job/steps/Plugins';
import Services from '@components/create-job/steps/Services';
import Specifications from '@components/create-job/steps/Specifications';
import { BOOLEAN_TYPES } from '@data/booleanTypes';
+import { PLUGIN_SIGNATURE_TYPES } from '@data/pluginSignatureTypes';
import { getRunningService } from '@data/containerResources';
import { CR_VISIBILITY_OPTIONS } from '@data/crVisibilityOptions';
import { zodResolver } from '@hookform/resolvers/zod';
import { DeploymentContextType, useDeploymentContext } from '@lib/contexts/deployment';
-import { boolToBooleanType, isGenericPlugin, NATIVE_PLUGIN_DEFAULT_RESPONSE_KEYS, titlecase } from '@lib/deeploy-utils';
+import {
+ boolToBooleanType,
+ GENERIC_JOB_RESERVED_KEYS,
+ isGenericPlugin,
+ NATIVE_PLUGIN_DEFAULT_RESPONSE_KEYS,
+ titlecase,
+} from '@lib/deeploy-utils';
import { Step, STEPS } from '@lib/steps/steps';
import { jobSchema } from '@schemas/index';
import JobFormHeaderInterface from '@shared/jobs/JobFormHeaderInterface';
@@ -111,6 +118,9 @@ export default function JobEditFormWrapper({
// Policies
restartPolicy: titlecase(config.RESTART_POLICY!),
imagePullPolicy: titlecase(config.IMAGE_PULL_POLICY!),
+
+ // Custom Parameters
+ customParams: formatCustomParams(config, GENERIC_JOB_RESERVED_KEYS),
});
const getGenericPluginSchemaDefaults = (config: JobConfig) => ({
@@ -122,18 +132,27 @@ export default function JobEditFormWrapper({
...getGenericSpecificDeploymentDefaults(config),
});
- const getNativePluginSchemaDefaults = (pluginInfo: AppsPlugin & { signature: string }) => ({
- basePluginType: BasePluginType.Native,
+ const getNativePluginSchemaDefaults = (pluginInfo: AppsPlugin & { signature: string }) => {
+ const isKnownSignature = PLUGIN_SIGNATURE_TYPES.includes(
+ pluginInfo.signature as (typeof PLUGIN_SIGNATURE_TYPES)[number],
+ );
- // Signature
- pluginSignature: pluginInfo.signature,
+ return {
+ basePluginType: BasePluginType.Native,
- // Tunneling
- ...getBaseSchemaTunnelingDefaults(pluginInfo.instance_conf),
+ // Signature - if not in the predefined list, select CUSTOM and pre-fill customPluginSignature
+ pluginSignature: isKnownSignature
+ ? pluginInfo.signature
+ : PLUGIN_SIGNATURE_TYPES[PLUGIN_SIGNATURE_TYPES.length - 1],
+ customPluginSignature: isKnownSignature ? undefined : pluginInfo.signature,
- // Custom Parameters
- customParams: formatCustomParams(pluginInfo.instance_conf),
- });
+ // Tunneling
+ ...getBaseSchemaTunnelingDefaults(pluginInfo.instance_conf),
+
+ // Custom Parameters
+ customParams: formatCustomParams(pluginInfo.instance_conf, NATIVE_PLUGIN_DEFAULT_RESPONSE_KEYS),
+ };
+ };
const getBaseSchemaDefaults = (config: JobConfig = jobConfig) => ({
jobType: job.resources.jobType,
@@ -252,11 +271,11 @@ export default function JobEditFormWrapper({
];
};
- const formatCustomParams = (config: JobConfig) => {
+ const formatCustomParams = (config: JobConfig, reservedKeys: (keyof JobConfig)[]) => {
const customParams: CustomParameterEntry[] = [];
Object.entries(config).forEach(([key, value]) => {
- if (!NATIVE_PLUGIN_DEFAULT_RESPONSE_KEYS.includes(key as keyof JobConfig)) {
+ if (!reservedKeys.includes(key as keyof JobConfig)) {
const valueType = typeof value === 'string' ? 'string' : 'json';
let parsedValue: string = '';
diff --git a/src/data/pipelineInputTypes.ts b/src/data/pipelineInputTypes.ts
index 5a518d1e..915a04c7 100644
--- a/src/data/pipelineInputTypes.ts
+++ b/src/data/pipelineInputTypes.ts
@@ -1,7 +1,8 @@
export const PIPELINE_INPUT_TYPES = [
+ 'Void',
+ 'Loopback',
'JeevesListener',
'JeevesApiListener',
'JeevesEmbedAgentListener',
'JeevesLlmAgentListener',
- 'void',
] as const;
diff --git a/src/lib/deeploy-utils.ts b/src/lib/deeploy-utils.ts
index 8936e755..bd2d97fb 100644
--- a/src/lib/deeploy-utils.ts
+++ b/src/lib/deeploy-utils.ts
@@ -26,6 +26,7 @@ import {
import {
BasePluginType,
ContainerDeploymentType,
+ CustomParameterEntry,
GenericPlugin,
NativePlugin,
PluginType,
@@ -55,6 +56,41 @@ export const NATIVE_PLUGIN_DEFAULT_RESPONSE_KEYS: (keyof JobConfig)[] = [
'NGROK_USE_API',
];
+// Keys that are system-managed and cannot be edited by users
+export const SYSTEM_MANAGED_JOB_CONFIG_KEYS: (keyof JobConfig)[] = [
+ 'CHAINSTORE_PEERS',
+ 'CHAINSTORE_RESPONSE_KEY',
+ 'INSTANCE_ID',
+ 'TUNNEL_ENGINE',
+ 'NGROK_AUTH_TOKEN',
+ 'NGROK_EDGE_LABEL',
+ 'NGROK_USE_API',
+];
+
+// Keys that are editable via dedicated UI sections in generic job deployment
+export const GENERIC_JOB_UI_MANAGED_KEYS: (keyof JobConfig)[] = [
+ 'ENV',
+ 'DYNAMIC_ENV',
+ 'VOLUMES',
+ 'FILE_VOLUMES',
+ 'CONTAINER_RESOURCES',
+ 'PORT',
+ 'TUNNEL_ENGINE_ENABLED',
+ 'CLOUDFLARE_TOKEN',
+ 'RESTART_POLICY',
+ 'IMAGE_PULL_POLICY',
+ 'IMAGE',
+ 'VCS_DATA',
+ 'CR_DATA',
+ 'BUILD_AND_RUN_COMMANDS',
+];
+
+// Combined: all keys that should be excluded from custom parameters for generic jobs
+export const GENERIC_JOB_RESERVED_KEYS: (keyof JobConfig)[] = [
+ ...SYSTEM_MANAGED_JOB_CONFIG_KEYS,
+ ...GENERIC_JOB_UI_MANAGED_KEYS,
+];
+
export const getDiscountPercentage = (_paymentMonthsCount: number): number => {
// Disabled for now
return 0;
@@ -287,6 +323,20 @@ const formatNativeJobCustomParams = (plugin: NativePlugin) => {
return customParams;
};
+const formatGenericJobCustomParams = (customParams: CustomParameterEntry[]) => {
+ const formatted: Record
= {};
+
+ if (!_.isEmpty(customParams)) {
+ customParams.forEach((param) => {
+ if (param.key) {
+ formatted[param.key] = parseIfJson(param.value);
+ }
+ });
+ }
+
+ return formatted;
+};
+
export const formatGenericPluginConfigAndSignature = (
resources: {
cpu: number;
@@ -377,6 +427,8 @@ export const formatGenericJobPayload = (
deployment,
);
+ const customParams = formatGenericJobCustomParams(deployment.customParams);
+
const nonce = generateDeeployNonce();
return {
@@ -391,6 +443,7 @@ export const formatGenericJobPayload = (
{
plugin_signature: pluginSignature,
...pluginConfig,
+ ...customParams,
},
],
pipeline_input_type: 'void',
@@ -424,16 +477,20 @@ export const formatNativeJobPayload = (
// Build plugins array
const plugins = deployment.plugins.map((plugin) => {
if (plugin.basePluginType === BasePluginType.Generic) {
- const secondaryPluginNodeResources = formatContainerResources(workerType, (plugin as GenericPlugin).ports);
+ const genericPlugin = plugin as GenericPlugin;
+ const secondaryPluginNodeResources = formatContainerResources(workerType, genericPlugin.ports);
const { pluginConfig, pluginSignature } = formatGenericPluginConfigAndSignature(
secondaryPluginNodeResources,
- plugin as GenericPlugin,
+ genericPlugin,
);
+ const customParams = formatGenericJobCustomParams(genericPlugin.customParams);
+
return {
plugin_signature: pluginSignature,
...pluginConfig,
+ ...customParams,
};
}
diff --git a/src/schemas/steps/deployment.ts b/src/schemas/steps/deployment.ts
index a746d63b..0cc2fc11 100644
--- a/src/schemas/steps/deployment.ts
+++ b/src/schemas/steps/deployment.ts
@@ -322,6 +322,7 @@ const genericAppDeploymentSchemaWihtoutRefinements = baseDeploymentSchema.extend
fileVolumes: validations.fileVolumes,
restartPolicy: validations.restartPolicy,
imagePullPolicy: validations.imagePullPolicy,
+ customParams: validations.customParams,
});
export const genericAppDeploymentSchema = applyDeploymentTypeRefinements(
@@ -352,6 +353,9 @@ const genericPluginSchema = z.object({
// Policies
restartPolicy: z.enum(POLICY_TYPES, { required_error: 'Value is required' }),
imagePullPolicy: z.enum(POLICY_TYPES, { required_error: 'Value is required' }),
+
+ // Custom Parameters
+ customParams: validations.customParams,
});
const nativePluginSchema = z.object({
diff --git a/src/shared/jobs/KeyValueEntriesSection.tsx b/src/shared/jobs/KeyValueEntriesSection.tsx
index d4af503b..79e66f1a 100644
--- a/src/shared/jobs/KeyValueEntriesSection.tsx
+++ b/src/shared/jobs/KeyValueEntriesSection.tsx
@@ -148,6 +148,11 @@ export default function KeyValueEntriesSection({
onChange={async (e) => {
const value = e.target.value;
field.onChange(value);
+
+ // Re-validate on change if field has error to clear it immediately
+ if (hasError) {
+ await trigger(`${name}.${index}.key`);
+ }
}}
onBlur={async () => {
field.onBlur();
@@ -183,9 +188,14 @@ export default function KeyValueEntriesSection({
{
+ onChange={async (e) => {
const value = e.target.value;
field.onChange(value);
+
+ // Re-validate on change if field has error to clear it immediately
+ if (hasError) {
+ await trigger(`${name}.${index}.value`);
+ }
}}
onBlur={async () => {
field.onBlur();
diff --git a/src/shared/jobs/deployment-type/WorkerSection.tsx b/src/shared/jobs/deployment-type/WorkerSection.tsx
index 2e9b7b42..fbc25975 100644
--- a/src/shared/jobs/deployment-type/WorkerSection.tsx
+++ b/src/shared/jobs/deployment-type/WorkerSection.tsx
@@ -62,21 +62,30 @@ export default function WorkerSection({
try {
const response = await fetch(`https://api.github.com/repos/${owner}/${repo}`);
- if (!response.ok) {
- throw new Error(`GitHub repository lookup failed with status ${response.status}`);
+ if (response.ok) {
+ // 200 - Repository is public
+ setValue(`${baseName}.deploymentType.repositoryVisibility`, 'public');
+ REPOS_CACHE[trimmedUrl] = 'public';
+ } else if (response.status === 404) {
+ // 404 - Repository is private or doesn't exist
+ setValue(`${baseName}.deploymentType.repositoryVisibility`, 'private');
+ REPOS_CACHE[trimmedUrl] = 'private';
+ } else {
+ // Other errors (e.g., 403 rate limit) - default to public (credentials optional)
+ // If it's actually private, user will get an error during deployment
+ console.warn(`GitHub API returned status ${response.status}, defaulting to public`);
+ setValue(`${baseName}.deploymentType.repositoryVisibility`, 'public');
+ // Don't cache rate-limited results
}
-
- // console.log('Repository visibility is: public');
- setValue(`${baseName}.deploymentType.repositoryVisibility`, 'public');
- REPOS_CACHE[trimmedUrl] = 'public';
} catch (error: any) {
if (error?.name === 'AbortError') {
return;
}
- // console.log('Repository visibility is: private');
- setValue(`${baseName}.deploymentType.repositoryVisibility`, 'private');
- REPOS_CACHE[trimmedUrl] = 'private';
+ // Network error - default to public (credentials optional)
+ console.error('Failed to check repository visibility:', error);
+ setValue(`${baseName}.deploymentType.repositoryVisibility`, 'public');
+ // Don't cache network errors
}
};
diff --git a/src/shared/jobs/native/CustomParametersSection.tsx b/src/shared/jobs/native/CustomParametersSection.tsx
index 30e65109..596ea798 100644
--- a/src/shared/jobs/native/CustomParametersSection.tsx
+++ b/src/shared/jobs/native/CustomParametersSection.tsx
@@ -93,6 +93,11 @@ export default function CustomParametersSection({ baseName = 'deployment' }: { b
onChange={async (e) => {
const value = e.target.value;
field.onChange(value);
+
+ // Re-validate on change if field has error to clear it immediately
+ if (hasError) {
+ await trigger(`${name}.${index}.key`);
+ }
}}
onBlur={async () => {
field.onBlur();
@@ -141,9 +146,14 @@ export default function CustomParametersSection({ baseName = 'deployment' }: { b
{
+ onChange={async (e) => {
const value = e.target.value;
field.onChange(value);
+
+ // Re-validate on change if field has error to clear it immediately
+ if (hasError) {
+ await trigger(`${name}.${index}.value`);
+ }
}}
onBlur={async () => {
field.onBlur();
diff --git a/src/shared/jobs/target-nodes/TargetNodesCard.tsx b/src/shared/jobs/target-nodes/TargetNodesCard.tsx
index 53e1097c..319c25cd 100644
--- a/src/shared/jobs/target-nodes/TargetNodesCard.tsx
+++ b/src/shared/jobs/target-nodes/TargetNodesCard.tsx
@@ -33,6 +33,18 @@ function TargetNodesCard({ isEditingRunningJob }: { isEditingRunningJob?: boolea
);
setValue('deployment.spareNodes', []);
+ } else {
+ // When auto-assign is disabled, resize array to match targetNodesCount
+ if (targetNodes.length < targetNodesCount) {
+ // Add empty entries if count increased
+ setValue('deployment.targetNodes', [
+ ...targetNodes,
+ ...Array.from({ length: targetNodesCount - targetNodes.length }, () => ({ address: '' })),
+ ]);
+ } else if (targetNodes.length > targetNodesCount) {
+ // Trim entries if count decreased
+ setValue('deployment.targetNodes', targetNodes.slice(0, targetNodesCount));
+ }
}
}
}, [autoAssign, targetNodesCount]);
diff --git a/src/typedefs/steps/deploymentStepTypes.ts b/src/typedefs/steps/deploymentStepTypes.ts
index 50be4556..26af6b25 100644
--- a/src/typedefs/steps/deploymentStepTypes.ts
+++ b/src/typedefs/steps/deploymentStepTypes.ts
@@ -95,6 +95,9 @@ type GenericPlugin = {
// Policies
restartPolicy: (typeof POLICY_TYPES)[number];
imagePullPolicy: (typeof POLICY_TYPES)[number];
+
+ // Custom Parameters
+ customParams: Array;
};
type NativePlugin = {
@@ -143,6 +146,9 @@ type GenericJobDeployment = BaseJobDeployment & {
// Policies
restartPolicy: (typeof POLICY_TYPES)[number];
imagePullPolicy: (typeof POLICY_TYPES)[number];
+
+ // Custom Parameters
+ customParams: Array;
};
type NativeJobDeployment = BaseJobDeployment & {