325325 margin-top : 4px ;
326326 line-height : 1.4 ;
327327 }
328+ .form-group .field-error {
329+ font-size : 11px ;
330+ color : var (--danger );
331+ margin-top : 4px ;
332+ line-height : 1.4 ;
333+ display : none;
334+ }
335+ .form-group .has-error .field-error { display : block; }
336+ .form-group .has-error .hint { display : none; }
328337
329338 .form-group input [type = "text" ],
330339 .form-group input [type = "password" ],
@@ -495,10 +504,11 @@ <h1>Welcome to ModelRelay</h1>
495504 <!-- Step 0: Backend URL -->
496505 < div class ="wizard-panel active " data-panel ="0 ">
497506 < div class ="card ">
498- < div class ="form-group ">
507+ < div class ="form-group " id =" wizBackendUrlGroup " >
499508 < label for ="wizBackendUrl "> Backend URL</ label >
500509 < input type ="text " id ="wizBackendUrl " placeholder ="http://localhost:11434 " value ="http://localhost:11434 ">
501- < div class ="hint "> The address of your local LLM server (Ollama, LM Studio, etc).</ div >
510+ < div class ="hint "> The address of your local LLM server. Examples: Ollama < code > http://localhost:11434</ code > , LM Studio < code > http://localhost:1234</ code > , Kiln < code > http://localhost:8420</ code > .</ div >
511+ < div class ="field-error " id ="wizBackendUrlError "> </ div >
502512 </ div >
503513 </ div >
504514 < div class ="wizard-nav ">
@@ -509,9 +519,10 @@ <h1>Welcome to ModelRelay</h1>
509519 <!-- Step 1: Relay Server + Secret -->
510520 < div class ="wizard-panel " data-panel ="1 ">
511521 < div class ="card ">
512- < div class ="form-group ">
522+ < div class ="form-group " id =" wizRelayUrlGroup " >
513523 < label for ="wizRelayUrl "> Relay Server URL</ label >
514524 < input type ="text " id ="wizRelayUrl " placeholder ="https://api.modelrelay.io " value ="https://api.modelrelay.io ">
525+ < div class ="field-error " id ="wizRelayUrlError "> </ div >
515526 </ div >
516527 < div class ="form-group ">
517528 < label for ="wizWorkerSecret "> Worker Secret / API Key</ label >
@@ -616,14 +627,16 @@ <h1>Welcome to ModelRelay</h1>
616627 <!-- Connection section -->
617628 < div class ="form-section ">
618629 < div class ="form-section-title "> Connection</ div >
619- < div class ="form-group ">
630+ < div class ="form-group " id =" backendUrlGroup " >
620631 < label for ="backendUrl "> Backend URL</ label >
621632 < input type ="text " id ="backendUrl " placeholder ="http://localhost:11434 ">
622- < div class ="hint "> Local LLM server address (Ollama, LM Studio, vLLM, etc.)</ div >
633+ < div class ="hint "> Local LLM server address. Common defaults: Ollama < code > http://localhost:11434</ code > , LM Studio < code > http://localhost:1234</ code > , Kiln < code > http://localhost:8420</ code > . Remote servers, Docker hosts, and custom ports are all supported.</ div >
634+ < div class ="field-error " id ="backendUrlError "> </ div >
623635 </ div >
624- < div class ="form-group ">
636+ < div class ="form-group " id =" relayUrlGroup " >
625637 < label for ="relayUrlSetting "> Relay Server URL</ label >
626638 < input type ="text " id ="relayUrlSetting " placeholder ="https://api.modelrelay.io ">
639+ < div class ="field-error " id ="relayUrlError "> </ div >
627640 </ div >
628641 < div class ="form-group ">
629642 < label for ="workerSecret "> Worker Secret / API Key</ label >
@@ -834,6 +847,50 @@ <h1>Welcome to ModelRelay</h1>
834847 }
835848 }
836849
850+ // Validate an http(s) URL string. Mirrors validate_http_url in src/lib.rs
851+ // so the UI blocks obviously-bad input before the Rust command re-checks.
852+ function validateHttpUrl ( candidate , field ) {
853+ const trimmed = ( candidate || "" ) . trim ( ) ;
854+ if ( ! trimmed ) return field + " is required" ;
855+ let parsed ;
856+ try {
857+ parsed = new URL ( trimmed ) ;
858+ } catch ( _e ) {
859+ return field + " is not a valid URL" ;
860+ }
861+ if ( parsed . protocol !== "http:" && parsed . protocol !== "https:" ) {
862+ return field + " must use http or https" ;
863+ }
864+ if ( ! parsed . hostname ) return field + " is missing a hostname" ;
865+ return null ;
866+ }
867+
868+ function setFieldError ( groupId , errorId , message ) {
869+ const group = document . getElementById ( groupId ) ;
870+ const errEl = document . getElementById ( errorId ) ;
871+ const input = group ? group . querySelector ( "input" ) : null ;
872+ if ( ! group || ! errEl || ! input ) return ;
873+ if ( message ) {
874+ group . classList . add ( "has-error" ) ;
875+ input . classList . add ( "invalid" ) ;
876+ errEl . textContent = message ;
877+ } else {
878+ group . classList . remove ( "has-error" ) ;
879+ input . classList . remove ( "invalid" ) ;
880+ errEl . textContent = "" ;
881+ }
882+ }
883+
884+ function validateSettingsForm ( ) {
885+ const backendErr = validateHttpUrl ( document . getElementById ( "backendUrl" ) . value , "Backend URL" ) ;
886+ const relayErr = validateHttpUrl ( document . getElementById ( "relayUrlSetting" ) . value , "Relay Server URL" ) ;
887+ setFieldError ( "backendUrlGroup" , "backendUrlError" , backendErr ) ;
888+ setFieldError ( "relayUrlGroup" , "relayUrlError" , relayErr ) ;
889+ const saveBtn = document . querySelector ( "#tab-settings .btn-save" ) ;
890+ if ( saveBtn ) saveBtn . disabled = Boolean ( backendErr || relayErr ) ;
891+ return ! ( backendErr || relayErr ) ;
892+ }
893+
837894 // Settings: load and save
838895 async function loadSettings ( ) {
839896 try {
@@ -847,16 +904,32 @@ <h1>Welcome to ModelRelay</h1>
847904 document . getElementById ( "maxConcurrent" ) . value = s . max_concurrent ;
848905 document . getElementById ( "pollInterval" ) . value = s . poll_interval_secs ;
849906 document . getElementById ( "autoStart" ) . checked = s . auto_start ;
907+ validateSettingsForm ( ) ;
850908 } catch ( e ) {
851909 console . error ( "Failed to load settings:" , e ) ;
852910 }
853911 }
854912
913+ // Attach live validation to the two URL fields on the settings form.
914+ [ "backendUrl" , "relayUrlSetting" ] . forEach ( ( id ) => {
915+ const el = document . getElementById ( id ) ;
916+ if ( el ) {
917+ el . addEventListener ( "input" , validateSettingsForm ) ;
918+ el . addEventListener ( "blur" , validateSettingsForm ) ;
919+ }
920+ } ) ;
921+
855922 async function doSave ( ) {
856923 const feedback = document . getElementById ( "saveFeedback" ) ;
857924 feedback . textContent = "" ;
858925 feedback . className = "save-feedback" ;
859926
927+ if ( ! validateSettingsForm ( ) ) {
928+ feedback . textContent = "Fix the errors above before saving." ;
929+ feedback . className = "save-feedback error" ;
930+ return ;
931+ }
932+
860933 try {
861934 const modelsRaw = document . getElementById ( "modelsInput" ) . value ;
862935 const models = modelsRaw . split ( "," ) . map ( m => m . trim ( ) ) . filter ( m => m . length > 0 ) ;
@@ -899,7 +972,21 @@ <h1>Welcome to ModelRelay</h1>
899972 }
900973
901974 function wizardNext ( ) {
975+ if ( wizardStep === 0 ) {
976+ const err = validateHttpUrl ( document . getElementById ( "wizBackendUrl" ) . value , "Backend URL" ) ;
977+ setFieldError ( "wizBackendUrlGroup" , "wizBackendUrlError" , err ) ;
978+ if ( err ) {
979+ document . getElementById ( "wizBackendUrl" ) . focus ( ) ;
980+ return ;
981+ }
982+ }
902983 if ( wizardStep === 1 ) {
984+ const relayErr = validateHttpUrl ( document . getElementById ( "wizRelayUrl" ) . value , "Relay Server URL" ) ;
985+ setFieldError ( "wizRelayUrlGroup" , "wizRelayUrlError" , relayErr ) ;
986+ if ( relayErr ) {
987+ document . getElementById ( "wizRelayUrl" ) . focus ( ) ;
988+ return ;
989+ }
903990 const secret = document . getElementById ( "wizWorkerSecret" ) . value . trim ( ) ;
904991 if ( ! secret ) {
905992 const input = document . getElementById ( "wizWorkerSecret" ) ;
@@ -913,6 +1000,19 @@ <h1>Welcome to ModelRelay</h1>
9131000 wizardUpdateDots ( ) ;
9141001 }
9151002
1003+ // Clear wizard errors as the user fixes them.
1004+ [ "wizBackendUrl" , "wizRelayUrl" ] . forEach ( ( id ) => {
1005+ const el = document . getElementById ( id ) ;
1006+ if ( ! el ) return ;
1007+ el . addEventListener ( "input" , ( ) => {
1008+ const groupId = id + "Group" ;
1009+ const errorId = id + "Error" ;
1010+ const label = id === "wizBackendUrl" ? "Backend URL" : "Relay Server URL" ;
1011+ const err = validateHttpUrl ( el . value , label ) ;
1012+ setFieldError ( groupId , errorId , err ) ;
1013+ } ) ;
1014+ } ) ;
1015+
9161016 function wizardBack ( ) {
9171017 wizardStep = Math . max ( wizardStep - 1 , 0 ) ;
9181018 wizardUpdateDots ( ) ;
0 commit comments