diff --git a/frontend-modern/src/components/Settings/NodeModal.tsx b/frontend-modern/src/components/Settings/NodeModal.tsx index 05ce315..1e86b99 100644 --- a/frontend-modern/src/components/Settings/NodeModal.tsx +++ b/frontend-modern/src/components/Settings/NodeModal.tsx @@ -26,6 +26,25 @@ interface NodeModalProps { securityStatus?: Partial; } +const deriveNameFromHost = (host: string): string => { + let value = host.trim(); + if (!value) { + return ''; + } + + try { + const url = value.includes('://') ? new URL(value) : new URL(`https://${value}`); + value = url.hostname || value; + } catch { + value = value.replace(/^https?:\/\//, ''); + } + + value = value.replace(/\/.*$/, '').replace(/^\[(.*)\]$/, '$1'); + value = value.replace(/\s+/g, '-'); + + return value; +}; + export const NodeModal: Component = (props) => { const [testResult, setTestResult] = createSignal<{ status: string; @@ -136,7 +155,7 @@ export const NodeModal: Component = (props) => { verifySSL: node.verifySSL ?? true, monitorPhysicalDisks: node.type === 'pve' - ? (node as NodeConfig & { monitorPhysicalDisks?: boolean }).monitorPhysicalDisks ?? false + ? (node as NodeConfig & { monitorPhysicalDisks?: boolean }).monitorPhysicalDisks ?? true : false, monitorMailStats: pmgConfig?.monitorMailStats ?? true, monitorQueues: pmgConfig?.monitorQueues ?? true, @@ -150,10 +169,20 @@ export const NodeModal: Component = (props) => { e.preventDefault(); const data = formData(); + const normalizedName = data.name.trim() || deriveNameFromHost(data.host); + if (!normalizedName) { + showError('Node name is required'); + return; + } + + if (normalizedName !== data.name) { + setFormData((prev) => ({ ...prev, name: normalizedName })); + } + // Prepare data based on auth type const nodeData: Partial = { type: props.nodeType, - name: data.name || '', // Will be auto-generated by backend if empty + name: normalizedName, host: data.host, fingerprint: data.fingerprint, verifySSL: data.verifySSL, @@ -202,10 +231,24 @@ export const NodeModal: Component = (props) => { }; const updateField = (field: string, value: string | boolean) => { - setFormData((prev) => ({ ...prev, [field]: value })); - if (field === 'host') { + if (field === 'host' && typeof value === 'string') { + setFormData((prev) => { + const next = { ...prev, host: value }; + const derivedName = deriveNameFromHost(value); + const previousDerivedName = deriveNameFromHost(prev.host || ''); + const shouldAutoUpdate = + !prev.name.trim() || (previousDerivedName && prev.name === previousDerivedName); + if (derivedName && shouldAutoUpdate) { + next.name = derivedName; + } + return next; + }); setQuickSetupCommand(''); + return; } + + setFormData((prev) => ({ ...prev, [field]: value })); + if (field === 'setupMode' && value !== 'auto') { setQuickSetupCommand(''); } @@ -213,6 +256,11 @@ export const NodeModal: Component = (props) => { const handleTestConnection = async () => { const data = formData(); + const normalizedName = data.name.trim() || deriveNameFromHost(data.host); + + if (!data.name.trim() && normalizedName) { + setFormData((prev) => ({ ...prev, name: normalizedName })); + } // If editing an existing node and no new credentials provided, use stored credentials if (props.editingNode) { @@ -267,7 +315,7 @@ export const NodeModal: Component = (props) => { // Prepare test data const testData: Partial = { type: props.nodeType, - name: data.name || '', // Will be auto-generated by backend if empty + name: normalizedName || '', host: data.host, fingerprint: data.fingerprint, verifySSL: data.verifySSL, @@ -369,15 +417,19 @@ export const NodeModal: Component = (props) => {
updateField('name', e.currentTarget.value)} - placeholder="Will auto-detect from hostname" + placeholder="Pulse uses this label across dashboards" + required class={controlClass()} /> +

+ Required and must be unique. We can auto-fill it from the Host URL if you leave it blank. +