From 6a3cdfe10fdb15cbcc26bc3c949bd5fc63cd1b70 Mon Sep 17 00:00:00 2001 From: Andrew Rabert Date: Tue, 30 Sep 2025 21:05:36 -0400 Subject: [PATCH] Add retry to server connection on launch This changes the launch behavior to retry connection when the server cannot be reached on launch. It can be cancelled by pressing the escape key to show the server address form. This also changes the styling of this page. --- native/find-webclient.css | 135 +++++++++++++++++++----------- native/find-webclient.html | 17 +--- native/find-webclient.js | 151 ++++++++++++++++++++++++++++------ native/find-webclient.lang.js | 20 +++-- 4 files changed, 227 insertions(+), 96 deletions(-) diff --git a/native/find-webclient.css b/native/find-webclient.css index 9e68657..3bd8471 100644 --- a/native/find-webclient.css +++ b/native/find-webclient.css @@ -3,14 +3,15 @@ } body { background-color: #101010; - color: #fff; - font-family: sans-serif; - font-size: 1.2em; + color: #ddd; + color: rgba(255, 255, 255, 0.8); + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + font-size: 1em; padding: 1em; } .logo { max-width: 500px; - margin-bottom: 1em; + margin-bottom: 2em; margin-left: auto; margin-right: auto; display: block; @@ -19,44 +20,47 @@ input[type=text] { display: block; width: 100%; font-size: 1em; - background-color: #292929; - color: #fff; - border: 2px solid #292929; + color: inherit; + background: #292929; + border: 0.16em solid #292929; border-radius: 0.2em; - padding: 0.5em; + padding: 0.6em 0.8em; + font-family: inherit; } input[type=text]:focus { - border: 2px solid #00a4dc; - border-radius: 0.2em; + border-color: #00a4dc; outline: none; } +input[type=text]::placeholder { + color: #666; + color: rgba(255, 255, 255, 0.3); + opacity: 1; +} button { width: 100%; color: #fff; - background-color: #00a4dc; + background: #00a4dc; border: none; border-radius: 0.2em; font-size: 1em; - padding: 0.5em; + padding: 0.75em 1.5em; cursor: pointer; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.025em; + outline: none; + font-family: inherit; +} +button:hover:not(:disabled) { + background: #0cb0e8; } button:disabled { background-color: #292929; cursor: default; + opacity: 0.6; } -#address-label { - display: block; - margin-bottom: 0.5em; -} -#main { - max-width: 1000px; - margin-top: 3rem; - margin-left: auto; - margin-right: auto; - display: none; -} -#splash { +#container { position: absolute; top: 0; left: 0; @@ -65,31 +69,70 @@ button:disabled { display: flex; flex-direction: column; justify-content: center; + align-items: center; } -#backdrop { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: rgba(0, 0, 0, 0.5); - z-index: 99; - display: none; -} -#connect-fail { - text-align: center; - background-color: #101010; - max-width: 600px; - padding: 25px; - margin-top: calc(50vh - 100px); +.logo { + max-width: 500px; + margin-bottom: 2em; margin-left: auto; margin-right: auto; - opacity: 1; + display: block; } -#connect-fail h1 { +#title { + margin-top: 1em; + margin-bottom: 0; font-size: 1.5em; - margin-top: 0; + font-weight: 400; + color: #ddd; + color: rgba(255, 255, 255, 0.8); + min-height: 1.5em; + height: 1.5em; + max-width: 450px; + width: 100%; + text-align: left; + visibility: hidden; +} +#connect-form { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + position: relative; +} +#address { + margin-bottom: 1em; + max-width: 450px; + height: 2.5em; + visibility: hidden; +} +#address.connecting { + visibility: hidden; +} +#spinner { + border: 6px solid #292929; + border-top: 6px solid #00a4dc; + border-radius: 50%; + width: 80px; + height: 80px; + animation: spin 1s linear infinite; + display: none; + position: absolute; + top: calc(1em + 1.5em + 1.25em); + left: 50%; + margin-left: -40px; +} +#connect-button { + max-width: 450px; + min-height: 2.5em; + visibility: hidden; +} +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} +#connect-button.cancel { + background: #cb272a; +} +#connect-button.cancel:hover:not(:disabled) { + background: #d93134; } -#helper-text { - margin-top: 0.3em; -} \ No newline at end of file diff --git a/native/find-webclient.html b/native/find-webclient.html index 49d6486..e4aeece 100644 --- a/native/find-webclient.html +++ b/native/find-webclient.html @@ -6,26 +6,15 @@ -
+
-
-
- -

- +

+
-

-
-
-

-

- -
-
\ No newline at end of file diff --git a/native/find-webclient.js b/native/find-webclient.js index 752f5de..39bc5e6 100644 --- a/native/find-webclient.js +++ b/native/find-webclient.js @@ -1,19 +1,16 @@ async function tryConnect(server) { - document.getElementById('connect-button').disabled = true; - - AbortSignal.timeout = function timeout(ms) { - const ctrl = new AbortController() - setTimeout(() => ctrl.abort(), ms) - return ctrl.signal - } - try { if (!server.startsWith("http")) { server = "http://" + server; } serverBaseURL = server.replace(/\/+$/, ""); const url = serverBaseURL + "/System/Info/Public"; - const response = await fetch(url, { cache: 'no-cache', signal: AbortSignal.timeout(5000) }); + + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 60000); + + const response = await fetch(url, { cache: 'no-cache', signal: controller.signal }); + clearTimeout(timeoutId); if (response.ok && (await response.json()).Id) { const htmlResponse = await fetch(server, { cache: 'no-cache' }); if (!htmlResponse.ok) { @@ -21,9 +18,6 @@ async function tryConnect(server) { } if (response.headers.get("content-security-policy")) { - // Sigh... If we just navigate to the URL, the server's CSP will block us loading other resources. - // So we have to parse the HTML, set a new base href, and then write it back to the page. - // We also have to override the history functions to make sure they use the correct URL. console.log("Using CSP workaround"); const webUrl = htmlResponse.url.replace(/\/[^\/]*$/, "/"); const realUrl = window.location.href; @@ -60,36 +54,139 @@ async function tryConnect(server) { return true; } + return false; } catch (e) { console.error(e); - document.getElementById('connect-button').disabled = false; return false; } } -document.getElementById('connect-form').addEventListener('submit', async (e) => { +let isConnecting = false; + +const updateButtonState = () => { + const address = document.getElementById('address'); + const button = document.getElementById('connect-button'); + const hasValue = address.value.trim().length > 0; + + if (!isConnecting) { + button.disabled = !hasValue; + } +}; + +const cancelOnEscape = (e) => { + if (isConnecting && e.key === 'Escape') { + cancelConnection(); + } +}; + +const startConnecting = async () => { + const address = document.getElementById('address'); + const title = document.getElementById('title'); + const spinner = document.getElementById('spinner'); + const button = document.getElementById('connect-button'); + const server = address.value; + + isConnecting = true; + title.textContent = ''; + title.style.visibility = 'hidden'; + address.classList.add('connecting'); + address.style.visibility = 'hidden'; + address.disabled = true; + spinner.style.display = 'block'; + button.style.visibility = 'hidden'; + document.addEventListener('keydown', cancelOnEscape); + + let connected = false; + + while (!connected && isConnecting) { + connected = await tryConnect(server); + + if (!connected && isConnecting) { + // Wait 5 seconds before retrying + await new Promise(resolve => setTimeout(resolve, 5000)); + } + } + + if (!connected) { + isConnecting = false; + title.textContent = document.getElementById('title').getAttribute('data-original-text'); + title.style.visibility = 'visible'; + address.classList.remove('connecting'); + address.style.visibility = 'visible'; + address.disabled = false; + spinner.style.display = 'none'; + button.style.visibility = 'visible'; + document.removeEventListener('keydown', cancelOnEscape); + updateButtonState(); + } +}; + +const cancelConnection = () => { + if (!isConnecting) return; + + isConnecting = false; + + const address = document.getElementById('address'); + const title = document.getElementById('title'); + const spinner = document.getElementById('spinner'); + const button = document.getElementById('connect-button'); + + title.textContent = document.getElementById('title').getAttribute('data-original-text'); + title.style.visibility = 'visible'; + address.classList.remove('connecting'); + address.style.visibility = 'visible'; + address.disabled = false; + spinner.style.display = 'none'; + button.style.visibility = 'visible'; + document.removeEventListener('keydown', cancelOnEscape); + updateButtonState(); +}; + +// Button click handler +document.getElementById('connect-button').addEventListener('click', (e) => { e.preventDefault(); - const server = document.getElementById('address').value; - const result = await tryConnect(server); - if (!result) { - document.getElementById('backdrop').style.display = 'block'; + e.stopPropagation(); + + if (!e.target.disabled) { + startConnecting(); } }); -document.getElementById('connect-fail-button').addEventListener('click', () => { - document.getElementById('backdrop').style.display = 'none'; +// Form submit handler +document.getElementById('connect-form').addEventListener('submit', (e) => { + e.preventDefault(); + if (!isConnecting) { + startConnecting(); + } }); +// Input change handler +document.getElementById('address').addEventListener('input', updateButtonState); -// load the server if we have one + +// Enter key handler +document.addEventListener('keydown', (e) => { + const address = document.getElementById('address'); + if (e.key === 'Enter' && !isConnecting && !address.disabled && address.value.trim()) { + e.preventDefault(); + startConnecting(); + } +}); + +// Auto-connect on load (async() => { + const title = document.getElementById('title'); + const address = document.getElementById('address'); + const button = document.getElementById('connect-button'); + const savedServer = window.jmpInfo.settings.main.userWebClient; if (savedServer) { - document.getElementById('address').value = savedServer; - } - - if (!savedServer || !(await tryConnect(savedServer))) { - document.getElementById('splash').style.display = 'none'; - document.getElementById('main').style.display = 'block'; + address.value = savedServer; + await startConnecting(); + } else { + title.style.visibility = 'visible'; + address.style.visibility = 'visible'; + button.style.visibility = 'visible'; + updateButtonState(); } })(); diff --git a/native/find-webclient.lang.js b/native/find-webclient.lang.js index 0cec7c1..8b81095 100644 --- a/native/find-webclient.lang.js +++ b/native/find-webclient.lang.js @@ -146,7 +146,7 @@ const languages = [ { "lang": "en-gb", "HeaderConnectToServer": "Connect to Server", - "LabelServerHost": "Host", + "LabelServerHost": "Server Address", "LabelServerHostHelp": "192.168.1.100:8096 or https://myserver.com", "Connect": "Connect", "HeaderConnectionFailure": "Connection Failure", @@ -156,7 +156,7 @@ const languages = [ { "lang": "en-us", "HeaderConnectToServer": "Connect to Server", - "LabelServerHost": "Host", + "LabelServerHost": "Server Address", "LabelServerHostHelp": "192.168.1.100:8096 or https://myserver.com", "Connect": "Connect", "HeaderConnectionFailure": "Connection Failure", @@ -965,10 +965,12 @@ if (!languages.find(l => l.lang === language)) { const languageStrings = languages.find(l => l.lang === language); const fallbackStrings = languages.find(l => l.lang === fallbackLanguage); -document.getElementById('title').innerText = languageStrings.HeaderConnectToServer || fallbackStrings.HeaderConnectToServer; -document.getElementById('address-label').innerText = languageStrings.LabelServerHost || fallbackStrings.LabelServerHost; -document.getElementById('helper-text').innerText = languageStrings.LabelServerHostHelp || fallbackStrings.LabelServerHostHelp; -document.getElementById('connect-button').innerText = languageStrings.Connect || fallbackStrings.Connect; -document.getElementById('connect-fail-title').innerText = languageStrings.HeaderConnectionFailure || fallbackStrings.HeaderConnectionFailure; -document.getElementById('connect-fail-text').innerText = languageStrings.MessageUnableToConnectToServer || fallbackStrings.MessageUnableToConnectToServer; -document.getElementById('connect-fail-button').innerText = languageStrings.ButtonGotIt || fallbackStrings.ButtonGotIt; +const titleText = languageStrings.LabelServerHost || fallbackStrings.LabelServerHost || 'Server Address'; +const connectText = languageStrings.Connect || fallbackStrings.Connect; + +document.getElementById('title').innerText = titleText; +document.getElementById('title').setAttribute('data-original-text', titleText); +document.getElementById('address').placeholder = languageStrings.LabelServerHostHelp || fallbackStrings.LabelServerHostHelp; +document.getElementById('connect-button').innerText = connectText; +document.getElementById('connect-button').setAttribute('data-original-text', connectText); +window.cancelButtonText = 'Cancel';