515515 color : var (--error );
516516 }
517517
518+ .nexus-auth-panel {
519+ background : rgba (255 , 215 , 0 , 0.08 );
520+ border : 1px solid rgba (255 , 215 , 0 , 0.25 );
521+ border-radius : 10px ;
522+ padding : 1rem ;
523+ margin-bottom : 1.25rem ;
524+ }
525+
526+ .nexus-auth-panel .btn {
527+ width : 100% ;
528+ }
529+
530+ .nexus-auth-note {
531+ color : var (--text-muted );
532+ font-size : 0.82rem ;
533+ line-height : 1.45 ;
534+ margin-top : 0.75rem ;
535+ }
536+
537+ .nexus-auth-result {
538+ display : none;
539+ margin-top : 0.75rem ;
540+ padding : 10px 12px ;
541+ border-radius : 8px ;
542+ font-size : 0.85rem ;
543+ line-height : 1.45 ;
544+ }
545+
546+ .nexus-auth-result .ok {
547+ display : block;
548+ background : rgba (52 , 211 , 153 , 0.12 );
549+ border : 1px solid rgba (52 , 211 , 153 , 0.3 );
550+ color : var (--success );
551+ }
552+
553+ .nexus-auth-result .fail {
554+ display : block;
555+ background : rgba (248 , 113 , 113 , 0.12 );
556+ border : 1px solid rgba (248 , 113 , 113 , 0.3 );
557+ color : var (--error );
558+ }
559+
518560 .config-preview {
519561 background : var (--card );
520562 border : 1px solid var (--border );
@@ -659,6 +701,16 @@ <h2>Choose your AI Provider</h2>
659701 < div class ="step " data-step ="2 ">
660702 < h2 > Integrations</ h2 >
661703 < div class ="subtitle "> Optional — skip any you don't need.</ div >
704+ < div class ="field ">
705+ < label > Dashboard Username</ label >
706+ < div class ="hint "> Used for the Hermes Dashboard login.</ div >
707+ < input type ="text " id ="dashboard-username " value ="dappnode " autocomplete ="username " placeholder ="dappnode ">
708+ </ div >
709+ < div class ="field ">
710+ < label > Dashboard Password</ label >
711+ < div class ="hint "> Leave blank to keep the current dashboard password.</ div >
712+ < input type ="password " id ="dashboard-password " autocomplete ="new-password " placeholder ="Set a dashboard password ">
713+ </ div >
662714 < div class ="field ">
663715 < label > Telegram Bot Token < span class ="optional-badge "> optional</ span > </ label >
664716 < div class ="hint "> From < strong > @BotFather</ strong > on Telegram.</ div >
@@ -783,6 +835,22 @@ <h2 style="text-align:center">Configuration Saved!</h2>
783835 // =========================================================================
784836 let _dashboardPollTimer = null ;
785837
838+ function dashboardUsernameFromConfig ( raw , env ) {
839+ const fromEnv = ( env . HERMES_DASHBOARD_BASIC_AUTH_USERNAME || "" ) . trim ( ) ;
840+ if ( fromEnv ) return fromEnv ;
841+ const match = String ( raw || "" ) . match ( / b a s i c _ a u t h : \s * [ \s \S ] * ?u s e r n a m e : \s * [ " ' ] ? ( [ ^ " ' \n ] + ) / ) ;
842+ return match ? match [ 1 ] . trim ( ) : "dappnode" ;
843+ }
844+
845+ function hydrateDashboardAuthFields ( raw , env ) {
846+ existingEnv = env || { } ;
847+ existingDashboardUsername = dashboardUsernameFromConfig ( raw , existingEnv ) ;
848+ const usernameEl = document . getElementById ( "dashboard-username" ) ;
849+ if ( usernameEl && ! dashboardUsernameTouched ) {
850+ usernameEl . value = existingDashboardUsername ;
851+ }
852+ }
853+
786854 async function refreshDashboard ( ) {
787855 try {
788856 const resp = await fetch ( "/api/health" ) ;
@@ -814,6 +882,7 @@ <h2 style="text-align:center">Configuration Saved!</h2>
814882 const data = await resp . json ( ) ;
815883 const env = data . env || { } ;
816884 const raw = data . config || "" ;
885+ hydrateDashboardAuthFields ( raw , env ) ;
817886 const provMatch = raw . match ( / p r o v i d e r : \s * [ " ' ] ? ( [ ^ " ' \n ] + ) / ) ;
818887 const modelMatch = raw . match ( / d e f a u l t : \s * [ " ' ] ? ( [ ^ " ' \n ] + ) / ) ;
819888 document . getElementById ( "st-provider" ) . textContent = provMatch ? provMatch [ 1 ] . trim ( ) : ( env . LLM_MODEL ? "custom" : "—" ) ;
@@ -861,6 +930,9 @@ <h2 style="text-align:center">Configuration Saved!</h2>
861930 let selectedProvider = null ;
862931 let openRouterModels = [ ] ;
863932 let nexusModels = [ ] ;
933+ let existingEnv = { } ;
934+ let existingDashboardUsername = "" ;
935+ let dashboardUsernameTouched = false ;
864936
865937 // =========================================================================
866938 // Render provider cards
@@ -888,6 +960,9 @@ <h2 style="text-align:center">Configuration Saved!</h2>
888960 document . getElementById ( "telegram-token" ) . addEventListener ( "input" , ( e ) => {
889961 document . getElementById ( "telegram-users-field" ) . style . display = e . target . value . trim ( ) ? "block" : "none" ;
890962 } ) ;
963+ document . getElementById ( "dashboard-username" ) . addEventListener ( "input" , ( ) => {
964+ dashboardUsernameTouched = true ;
965+ } ) ;
891966 document . getElementById ( "whatsapp-enabled" ) . addEventListener ( "change" , ( e ) => {
892967 document . getElementById ( "whatsapp-users-field" ) . style . display = e . target . checked ? "block" : "none" ;
893968 document . getElementById ( "whatsapp-pair-notice" ) . style . display = e . target . checked ? "block" : "none" ;
@@ -974,10 +1049,15 @@ <h2 style="text-align:center">Configuration Saved!</h2>
9741049 if ( p . id === "nexus" ) {
9751050 container . innerHTML = `
9761051 <h2>Configure DAppNode Nexus</h2>
977- <div class="subtitle">DAppNode's privacy-focused AI gateway. Your prompts are never logged, stored, or used for training. <a href="https://nexus.dappnode.com" target="_blank" style="color:var(--primary)">Sign up & get your API key →</a></div>
1052+ <div class="subtitle">DAppNode's privacy-focused AI gateway. Your prompts are never logged, stored, or used for training.</div>
1053+ <div class="nexus-auth-panel">
1054+ <button class="btn btn-primary" id="nexus-login-btn" onclick="startNexusLogin()">Login with Nexus & create API key</button>
1055+ <div class="nexus-auth-note">Creates a Nexus API key for this Hermes Agent account and fills it below. <a href="https://nexus.dappnode.com/api-keys" target="_blank" style="color:var(--primary)">Manage your keys</a>.</div>
1056+ <div class="nexus-auth-result" id="nexus-auth-result"></div>
1057+ </div>
9781058 <div class="field">
9791059 <label>Nexus API Key</label>
980- <div class="hint">Sign up at <strong >nexus.dappnode.com</strong> and create an API key from your dashboard .</div>
1060+ <div class="hint">Use Nexus login above, or paste a key from <a href="https://nexus.dappnode.com/api-keys" target="_blank" style="color:var(--primary)" >nexus.dappnode.com/api-keys</a> .</div>
9811061 <input type="password" id="api-key" placeholder="Paste your Nexus API key here">
9821062 </div>
9831063 <div class="field">
@@ -1145,6 +1225,75 @@ <h2>Configure ${p.name}</h2>
11451225 }
11461226 }
11471227
1228+ function showNexusAuthResult ( kind , message ) {
1229+ const el = document . getElementById ( "nexus-auth-result" ) ;
1230+ if ( ! el ) return ;
1231+ el . textContent = message ;
1232+ el . className = "nexus-auth-result " + ( kind === "ok" ? "ok" : "fail" ) ;
1233+ }
1234+
1235+ function removeNexusAuthParams ( ) {
1236+ const next = new URL ( window . location . href ) ;
1237+ for ( const key of [ "nexus_auth" , "nexus_message" , "nexus_result" ] ) {
1238+ next . searchParams . delete ( key ) ;
1239+ }
1240+ window . history . replaceState ( null , "" , next . pathname + next . search + next . hash ) ;
1241+ }
1242+
1243+ function startNexusLogin ( ) {
1244+ const btn = document . getElementById ( "nexus-login-btn" ) ;
1245+ if ( btn ) {
1246+ btn . disabled = true ;
1247+ btn . textContent = "Opening Nexus login..." ;
1248+ }
1249+ const current = new URL ( window . location . href ) ;
1250+ for ( const key of [ "nexus_auth" , "nexus_message" , "nexus_result" ] ) {
1251+ current . searchParams . delete ( key ) ;
1252+ }
1253+ const returnTo = current . pathname + current . search + current . hash ;
1254+ window . location . href = "/nexus/auth/start?returnTo=" + encodeURIComponent ( returnTo || "/" ) ;
1255+ }
1256+
1257+ async function handleNexusAuthRedirect ( ) {
1258+ const params = new URLSearchParams ( window . location . search ) ;
1259+ const status = params . get ( "nexus_auth" ) ;
1260+ if ( ! status ) return ;
1261+
1262+ switchTab ( "setup" ) ;
1263+ selectProviderCard ( "nexus" ) ;
1264+ goTo ( 1 ) ;
1265+
1266+ try {
1267+ if ( status === "error" ) {
1268+ showNexusAuthResult ( "fail" , params . get ( "nexus_message" ) || "Nexus login failed. Please try again." ) ;
1269+ return ;
1270+ }
1271+
1272+ const resultId = params . get ( "nexus_result" ) ;
1273+ if ( ! resultId ) {
1274+ showNexusAuthResult ( "fail" , "Nexus login finished without an API key result. Please try again." ) ;
1275+ return ;
1276+ }
1277+
1278+ showNexusAuthResult ( "ok" , "Creating Nexus API key..." ) ;
1279+ const resp = await fetch ( "/api/nexus/auth/result" , {
1280+ method : "POST" ,
1281+ headers : { "Content-Type" : "application/json" } ,
1282+ body : JSON . stringify ( { id : resultId } ) ,
1283+ } ) ;
1284+ const data = await resp . json ( ) . catch ( ( ) => ( { } ) ) ;
1285+ if ( ! resp . ok ) throw new Error ( data . error || "Could not retrieve the generated Nexus API key" ) ;
1286+
1287+ const input = document . getElementById ( "api-key" ) ;
1288+ if ( input ) input . value = data . apiKey || "" ;
1289+ showNexusAuthResult ( "ok" , "Nexus API key created. Choose a model and continue." ) ;
1290+ } catch ( err ) {
1291+ showNexusAuthResult ( "fail" , err . message || "Nexus login failed. Please try again." ) ;
1292+ } finally {
1293+ removeNexusAuthParams ( ) ;
1294+ }
1295+ }
1296+
11481297 async function probeOllama ( ) {
11491298 const btn = document . getElementById ( "probe-btn" ) ;
11501299 const icon = document . getElementById ( "probe-icon" ) ;
@@ -1188,6 +1337,24 @@ <h2>Configure ${p.name}</h2>
11881337 return base . endsWith ( "/v1" ) ? base : base + "/v1" ;
11891338 }
11901339
1340+ function buildDashboardAuthEnv ( ) {
1341+ const env = { } ;
1342+ const username = ( document . getElementById ( "dashboard-username" ) ?. value || "" ) . trim ( ) ;
1343+ const password = ( document . getElementById ( "dashboard-password" ) ?. value || "" ) . trim ( ) ;
1344+ if ( username ) env . HERMES_DASHBOARD_BASIC_AUTH_USERNAME = username ;
1345+ if ( password ) env . HERMES_DASHBOARD_BASIC_AUTH_PASSWORD = password ;
1346+ return env ;
1347+ }
1348+
1349+ function dashboardAuthChanged ( ) {
1350+ const dashboardEnv = buildDashboardAuthEnv ( ) ;
1351+ const username = dashboardEnv . HERMES_DASHBOARD_BASIC_AUTH_USERNAME || "" ;
1352+ return Boolean (
1353+ dashboardEnv . HERMES_DASHBOARD_BASIC_AUTH_PASSWORD
1354+ || ( username && username !== existingDashboardUsername )
1355+ ) ;
1356+ }
1357+
11911358 function buildEnv ( ) {
11921359 const env = { } ;
11931360 const p = selectedProvider ;
@@ -1231,6 +1398,7 @@ <h2>Configure ${p.name}</h2>
12311398 const waUsers = ( document . getElementById ( "whatsapp-users" ) . value || "" ) . trim ( ) ;
12321399 if ( waUsers ) env . WHATSAPP_ALLOWED_USERS = waUsers ;
12331400 }
1401+ Object . assign ( env , buildDashboardAuthEnv ( ) ) ;
12341402 return env ;
12351403 }
12361404
@@ -1309,7 +1477,9 @@ <h2>Configure ${p.name}</h2>
13091477 const yaml = buildConfigYaml ( ) ;
13101478 const envLines = [ ] ;
13111479 for ( const [ k , v ] of Object . entries ( env ) ) {
1312- if ( k . includes ( "KEY" ) || k . includes ( "TOKEN" ) || k . includes ( "SECRET" ) ) {
1480+ if ( k . includes ( "PASSWORD" ) ) {
1481+ envLines . push ( `${ k } =${ "*" . repeat ( Math . max ( 8 , v . length ) ) } ` ) ;
1482+ } else if ( k . includes ( "KEY" ) || k . includes ( "TOKEN" ) || k . includes ( "SECRET" ) ) {
13131483 envLines . push ( `${ k } =${ v . slice ( 0 , 8 ) } ${ "*" . repeat ( Math . max ( 0 , v . length - 8 ) ) } ` ) ;
13141484 } else { envLines . push ( `${ k } =${ v } ` ) ; }
13151485 }
@@ -1326,7 +1496,12 @@ <h2>Configure ${p.name}</h2>
13261496 const btn = document . getElementById ( "btn-save" ) ;
13271497 btn . disabled = true ; btn . textContent = "Saving..." ;
13281498 const env = buildEnv ( ) ;
1329- const needsRestart = ! ! ( env . TELEGRAM_BOT_TOKEN || env . WHATSAPP_ENABLED ) ;
1499+ if ( env . HERMES_DASHBOARD_BASIC_AUTH_PASSWORD && ! env . HERMES_DASHBOARD_BASIC_AUTH_USERNAME ) {
1500+ alert ( "Dashboard username is required when setting a dashboard password." ) ;
1501+ btn . disabled = false ; btn . textContent = "Save Configuration" ;
1502+ return ;
1503+ }
1504+ const needsRestart = ! ! ( env . TELEGRAM_BOT_TOKEN || env . WHATSAPP_ENABLED || dashboardAuthChanged ( ) ) ;
13301505 try {
13311506 const resp = await fetch ( "/api/config" , {
13321507 method : "POST" ,
@@ -1348,6 +1523,8 @@ <h2>Configure ${p.name}</h2>
13481523 btn . disabled = false ; btn . textContent = "Save Configuration" ;
13491524 }
13501525 }
1526+
1527+ handleNexusAuthRedirect ( ) ;
13511528 </ script >
13521529</ body >
13531530
0 commit comments