1111// Configuration
1212const SERVER_EXTENSION_ID = 'io.vibora.server' ;
1313const DEFAULT_PORT = 7777 ;
14- const CURRENT_SCHEMA_VERSION = 2 ;
14+ const CURRENT_SCHEMA_VERSION = 3 ;
1515const HEALTH_CHECK_TIMEOUT = 3000 ; // 3 seconds per check
1616const MAX_HEALTH_RETRIES = 10 ;
1717const DEV_PORT = 5173 ;
@@ -63,58 +63,85 @@ function isFirstLaunch() {
6363 return ! desktopSettings || desktopSettings . _schemaVersion === undefined ;
6464}
6565
66+ /**
67+ * Helper: Construct URL from host and port
68+ */
69+ function constructRemoteUrl ( host , port ) {
70+ if ( ! host ) return '' ;
71+ const effectivePort = port || DEFAULT_PORT ;
72+ // Omit port for standard HTTP/HTTPS ports
73+ const portSuffix = effectivePort === 80 || effectivePort === 443 ? '' : `:${ effectivePort } ` ;
74+ return `http://${ host } ${ portSuffix } ` ;
75+ }
76+
6677/**
6778 * Migrate settings from flat to nested format
6879 */
6980function migrateSettings ( settings ) {
70- if ( settings . _schemaVersion >= CURRENT_SCHEMA_VERSION ) {
81+ const version = settings . _schemaVersion || 1 ;
82+ if ( version >= CURRENT_SCHEMA_VERSION ) {
7183 return settings ; // Already migrated
7284 }
7385
74- console . log ( '[Vibora] Migrating settings to nested format...' ) ;
75-
76- // Migration map from flat keys to nested paths
77- const migrationMap = {
78- port : [ 'server' , 'port' ] ,
79- defaultGitReposDir : [ 'paths' , 'defaultGitReposDir' ] ,
80- basicAuthUsername : [ 'authentication' , 'username' ] ,
81- basicAuthPassword : [ 'authentication' , 'password' ] ,
82- remoteHost : [ 'remoteVibora' , 'host' ] ,
83- hostname : [ 'remoteVibora' , 'host' ] , // Legacy key
84- remotePort : [ 'remoteVibora' , 'port' ] ,
85- sshPort : [ 'editor' , 'sshPort' ] ,
86- linearApiKey : [ 'integrations' , 'linearApiKey' ] ,
87- githubPat : [ 'integrations' , 'githubPat' ] ,
88- language : [ 'appearance' , 'language' ] ,
89- } ;
86+ console . log ( '[Vibora] Migrating settings from version' , version , 'to' , CURRENT_SCHEMA_VERSION ) ;
9087
9188 const migrated = {
9289 _schemaVersion : CURRENT_SCHEMA_VERSION ,
9390 server : { port : DEFAULT_PORT } ,
94- remoteVibora : { host : '' , port : DEFAULT_PORT } ,
91+ remoteVibora : { url : '' } ,
9592 editor : { app : 'vscode' , host : '' , sshPort : 22 } ,
9693 } ;
9794
98- // Copy existing nested groups if present
99- for ( const key of [ 'server' , 'paths' , 'authentication' , 'remoteVibora' , ' editor', 'integrations' , 'appearance' , 'notifications' , 'zai' ] ) {
95+ // Copy existing nested groups if present (except remoteVibora which needs special handling)
96+ for ( const key of [ 'server' , 'paths' , 'authentication' , 'editor' , 'integrations' , 'appearance' , 'notifications' , 'zai' ] ) {
10097 if ( settings [ key ] && typeof settings [ key ] === 'object' ) {
10198 migrated [ key ] = { ...migrated [ key ] , ...settings [ key ] } ;
10299 }
103100 }
104101
105- // Migrate flat keys
106- for ( const [ flatKey , [ group , prop ] ] of Object . entries ( migrationMap ) ) {
107- if ( settings [ flatKey ] !== undefined && settings [ flatKey ] !== null ) {
108- // Don't migrate old default port (3333) - let user get new default
109- if ( flatKey === 'port' && settings [ flatKey ] === 3333 ) {
110- continue ;
111- }
112- // Only migrate if not already set in nested format
113- if ( ! migrated [ group ] ) migrated [ group ] = { } ;
114- if ( migrated [ group ] [ prop ] === undefined || migrated [ group ] [ prop ] === null || migrated [ group ] [ prop ] === '' ) {
115- migrated [ group ] [ prop ] = settings [ flatKey ] ;
102+ // Schema 1 → 2: Migrate flat keys to nested structure
103+ if ( version < 2 ) {
104+ const migrationMap = {
105+ port : [ 'server' , 'port' ] ,
106+ defaultGitReposDir : [ 'paths' , 'defaultGitReposDir' ] ,
107+ basicAuthUsername : [ 'authentication' , 'username' ] ,
108+ basicAuthPassword : [ 'authentication' , 'password' ] ,
109+ sshPort : [ 'editor' , 'sshPort' ] ,
110+ linearApiKey : [ 'integrations' , 'linearApiKey' ] ,
111+ githubPat : [ 'integrations' , 'githubPat' ] ,
112+ language : [ 'appearance' , 'language' ] ,
113+ } ;
114+
115+ for ( const [ flatKey , [ group , prop ] ] of Object . entries ( migrationMap ) ) {
116+ if ( settings [ flatKey ] !== undefined && settings [ flatKey ] !== null ) {
117+ // Don't migrate old default port (3333) - let user get new default
118+ if ( flatKey === 'port' && settings [ flatKey ] === 3333 ) {
119+ continue ;
120+ }
121+ if ( ! migrated [ group ] ) migrated [ group ] = { } ;
122+ if ( migrated [ group ] [ prop ] === undefined || migrated [ group ] [ prop ] === null || migrated [ group ] [ prop ] === '' ) {
123+ migrated [ group ] [ prop ] = settings [ flatKey ] ;
124+ }
116125 }
117126 }
127+
128+ // Handle flat remoteHost/hostname → remoteVibora.url
129+ const flatHost = settings . remoteHost || settings . hostname || '' ;
130+ if ( flatHost ) {
131+ migrated . remoteVibora = { url : constructRemoteUrl ( flatHost , DEFAULT_PORT ) } ;
132+ }
133+ }
134+
135+ // Schema 2 → 3: Migrate remoteVibora.host + remoteVibora.port → remoteVibora.url
136+ if ( version < 3 && settings . remoteVibora ) {
137+ if ( 'host' in settings . remoteVibora ) {
138+ const host = settings . remoteVibora . host || '' ;
139+ const port = settings . remoteVibora . port || DEFAULT_PORT ;
140+ migrated . remoteVibora = { url : constructRemoteUrl ( host , port ) } ;
141+ } else if ( 'url' in settings . remoteVibora ) {
142+ // Already has url format
143+ migrated . remoteVibora = { url : settings . remoteVibora . url || '' } ;
144+ }
118145 }
119146
120147 // Preserve non-migrated keys (like lastUpdateCheck, lastConnectedHost)
@@ -247,7 +274,7 @@ function promptOnboardingChoice() {
247274
248275/**
249276 * Prompt user to configure remote server connection
250- * @returns {Promise<{host : string, port: number } | null> } null if cancelled
277+ * @returns {Promise<{url : string} | null> } null if cancelled
251278 */
252279function promptRemoteConfig ( ) {
253280 return new Promise ( ( resolve ) => {
@@ -257,17 +284,13 @@ function promptRemoteConfig() {
257284 <div class="prompt-container">
258285 <div class="prompt-title">Connect to Remote Server</div>
259286 <div class="prompt-description">
260- Enter the hostname and port of your remote Vibora server.
287+ Enter the URL of your remote Vibora server.
261288 </div>
262289 <div id="remote-error" class="prompt-error" style="display: none;"></div>
263290 <form class="prompt-form" id="remote-form">
264291 <div class="input-group">
265- <label for="remote-host">Hostname</label>
266- <input type="text" id="remote-host" placeholder="example.com or 192.168.1.100" required autocomplete="off" />
267- </div>
268- <div class="input-group">
269- <label for="remote-port">Port</label>
270- <input type="number" id="remote-port" placeholder="${ DEFAULT_PORT } " value="${ DEFAULT_PORT } " min="1" max="65535" />
292+ <label for="remote-url">Server URL</label>
293+ <input type="url" id="remote-url" placeholder="http://example.com:7777 or https://vibora.tailnet.ts.net" required autocomplete="off" />
271294 </div>
272295 <div class="button-group">
273296 <button type="button" class="secondary-btn" id="back-btn">Back</button>
@@ -280,37 +303,46 @@ function promptRemoteConfig() {
280303 document . getElementById ( 'back-btn' ) . onclick = ( ) => resolve ( null ) ;
281304 document . getElementById ( 'remote-form' ) . onsubmit = ( e ) => {
282305 e . preventDefault ( ) ;
283- const host = document . getElementById ( 'remote-host ' ) . value . trim ( ) ;
284- const port = parseInt ( document . getElementById ( 'remote-port' ) . value , 10 ) || DEFAULT_PORT ;
306+ const urlInput = document . getElementById ( 'remote-url ' ) . value . trim ( ) ;
307+ const errorEl = document . getElementById ( 'remote-error' ) ;
285308
286- if ( ! host ) {
287- const errorEl = document . getElementById ( 'remote-error' ) ;
288- errorEl . textContent = 'Please enter a hostname' ;
309+ if ( ! urlInput ) {
310+ errorEl . textContent = 'Please enter a URL' ;
289311 errorEl . style . display = 'block' ;
290312 return ;
291313 }
292314
293- resolve ( { host, port } ) ;
315+ // Validate URL
316+ try {
317+ const url = new URL ( urlInput ) ;
318+ if ( url . protocol !== 'http:' && url . protocol !== 'https:' ) {
319+ throw new Error ( 'URL must be http:// or https://' ) ;
320+ }
321+ // Normalize to origin (removes trailing slash, path)
322+ resolve ( { url : url . origin } ) ;
323+ } catch ( err ) {
324+ errorEl . textContent = 'Please enter a valid URL (e.g., http://example.com:7777)' ;
325+ errorEl . style . display = 'block' ;
326+ }
294327 } ;
295328 } ) ;
296329}
297330
298331/**
299332 * Prompt user to choose between local and remote server
300- * Only shown when remoteHost is configured in settings
333+ * Only shown when remote URL is configured in settings
301334 * @returns {Promise<boolean> } true if user wants to connect to remote
302335 */
303- function promptServerChoice ( remoteHost , remotePort ) {
336+ function promptServerChoice ( remoteUrl ) {
304337 return new Promise ( ( resolve ) => {
305338 const app = document . getElementById ( 'app' ) ;
306- const displayHost = remotePort !== DEFAULT_PORT ? `${ remoteHost } :${ remotePort } ` : remoteHost ;
307339 app . innerHTML = `
308340 <img src="/icons/icon.png" alt="Vibora" class="logo" style="animation: none;">
309341 <div class="prompt-container">
310342 <div class="prompt-title">Choose Server</div>
311343 <div class="prompt-description">
312344 You have a remote server configured at:<br>
313- <strong>${ displayHost } </strong>
345+ <strong>${ remoteUrl } </strong>
314346 </div>
315347 <div class="button-group" style="margin-top: 1.5rem;">
316348 <button class="primary-btn" id="use-local-btn">Use Local Server</button>
@@ -517,24 +549,21 @@ async function startLocalServer() {
517549 * Get remote server config from settings (nested format)
518550 */
519551function getRemoteConfig ( ) {
520- const host = desktopSettings ?. remoteVibora ?. host ?. trim ( ) || '' ;
521- const port = desktopSettings ?. remoteVibora ?. port || DEFAULT_PORT ;
522- return { host, port } ;
552+ const url = desktopSettings ?. remoteVibora ?. url ?. trim ( ) || '' ;
553+ return { url } ;
523554}
524555
525556/**
526557 * Connect to remote server
527558 */
528- async function connectToRemote ( remoteHost , remotePort ) {
529- const remoteUrl = `http://${ remoteHost } :${ remotePort } ` ;
530-
531- setStatus ( 'Connecting to remote server...' , `${ remoteHost } :${ remotePort } ` ) ;
532- console . log ( '[Vibora] Connecting to remote:' , remoteHost ) ;
559+ async function connectToRemote ( remoteUrl ) {
560+ setStatus ( 'Connecting to remote server...' , remoteUrl ) ;
561+ console . log ( '[Vibora] Connecting to remote:' , remoteUrl ) ;
533562
534563 if ( await waitForServerReady ( remoteUrl ) ) {
535564 await saveSettings ( {
536565 ...desktopSettings ,
537- lastConnectedHost : remoteHost
566+ lastConnectedHost : remoteUrl
538567 } ) ;
539568 loadViboraApp ( remoteUrl ) ;
540569 return true ;
@@ -593,7 +622,7 @@ async function tryConnect() {
593622 await loadSettings ( ) ;
594623
595624 const remote = getRemoteConfig ( ) ;
596- const hasRemoteConfig = remote . host !== '' ;
625+ const hasRemoteConfig = remote . url !== '' ;
597626
598627 // First launch - show onboarding
599628 if ( isFirstLaunch ( ) ) {
@@ -611,17 +640,17 @@ async function tryConnect() {
611640 ...desktopSettings ,
612641 _schemaVersion : CURRENT_SCHEMA_VERSION ,
613642 server : { port : DEFAULT_PORT } ,
614- remoteVibora : { host : config . host , port : config . port } ,
643+ remoteVibora : { url : config . url } ,
615644 editor : { app : 'vscode' , host : '' , sshPort : 22 } ,
616645 } ) ;
617646
618647 // Try to connect to remote
619- if ( await connectToRemote ( config . host , config . port ) ) {
648+ if ( await connectToRemote ( config . url ) ) {
620649 return ;
621650 }
622651
623652 // Remote failed - ask if they want to try local instead
624- showError ( 'Connection Failed' , `Could not connect to ${ config . host } : ${ config . port } . Try running locally or check the server.` ) ;
653+ showError ( 'Connection Failed' , `Could not connect to ${ config . url } . Try running locally or check the server.` ) ;
625654 return ;
626655 }
627656
@@ -635,18 +664,18 @@ async function tryConnect() {
635664 ...desktopSettings ,
636665 _schemaVersion : CURRENT_SCHEMA_VERSION ,
637666 server : { port : DEFAULT_PORT } ,
638- remoteVibora : { host : '' , port : DEFAULT_PORT } ,
667+ remoteVibora : { url : '' } ,
639668 editor : { app : 'vscode' , host : '' , sshPort : 22 } ,
640669 } ) ;
641670 }
642671
643- // Check if remoteHost is configured (returning user with remote setup)
672+ // Check if remote URL is configured (returning user with remote setup)
644673 if ( hasRemoteConfig ) {
645674 // Ask user which server to use
646- const useRemote = await promptServerChoice ( remote . host , remote . port ) ;
675+ const useRemote = await promptServerChoice ( remote . url ) ;
647676
648677 if ( useRemote ) {
649- if ( await connectToRemote ( remote . host , remote . port ) ) {
678+ if ( await connectToRemote ( remote . url ) ) {
650679 return ;
651680 }
652681 // Remote failed - fall through to local
@@ -658,6 +687,32 @@ async function tryConnect() {
658687 await connectToLocal ( ) ;
659688}
660689
690+ /**
691+ * Handle reconnection request from the React app (via postMessage)
692+ * Called when user changes the remote URL in Settings
693+ */
694+ async function handleReconnect ( newUrl ) {
695+ console . log ( '[Vibora] Reconnect requested:' , newUrl || 'local' ) ;
696+
697+ // Save new URL to settings
698+ await saveSettings ( {
699+ ...desktopSettings ,
700+ remoteVibora : { url : newUrl || '' }
701+ } ) ;
702+
703+ if ( newUrl ) {
704+ // Connect to remote
705+ if ( await connectToRemote ( newUrl ) ) {
706+ return ;
707+ }
708+ // Failed - show error but don't fall back automatically
709+ showError ( 'Connection Failed' , `Could not connect to ${ newUrl } ` ) ;
710+ } else {
711+ // Switch to local
712+ await connectToLocal ( ) ;
713+ }
714+ }
715+
661716/**
662717 * Handle extension ready event (when running with local server extension)
663718 */
@@ -987,6 +1042,10 @@ async function init() {
9871042 } else if ( event . data ?. type === 'vibora:playSound' ) {
9881043 // Play notification sound locally
9891044 playNotificationSound ( ) ;
1045+ } else if ( event . data ?. type === 'vibora:reconnect' ) {
1046+ // Handle reconnection request from Settings UI
1047+ const newUrl = event . data . url ;
1048+ handleReconnect ( newUrl ) ;
9901049 }
9911050 } ) ;
9921051
0 commit comments