diff --git a/Android/app/src/main/java/com/archipelago/app/ui/screens/WebViewScreen.kt b/Android/app/src/main/java/com/archipelago/app/ui/screens/WebViewScreen.kt index 79dead60..41c3a6e5 100644 --- a/Android/app/src/main/java/com/archipelago/app/ui/screens/WebViewScreen.kt +++ b/Android/app/src/main/java/com/archipelago/app/ui/screens/WebViewScreen.kt @@ -132,6 +132,16 @@ fun WebViewScreen( AndroidView( modifier = Modifier.fillMaxSize(), factory = { context -> + fun openExternalUrl(url: String) { + try { + val intent = android.content.Intent( + android.content.Intent.ACTION_VIEW, + android.net.Uri.parse(url), + ) + context.startActivity(intent) + } catch (_: Exception) {} + } + WebView(context).apply { layoutParams = ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, @@ -220,13 +230,7 @@ fun WebViewScreen( // Keep navigation within the Archipelago server if (url.startsWith(serverUrl)) return false // Open external URLs in the system browser - try { - val intent = android.content.Intent( - android.content.Intent.ACTION_VIEW, - android.net.Uri.parse(url), - ) - context.startActivity(intent) - } catch (_: Exception) {} + openExternalUrl(url) return true } } @@ -243,18 +247,30 @@ fun WebViewScreen( isUserGesture: Boolean, resultMsg: android.os.Message?, ): Boolean { - // Extract the URL from the hit test - val data = view?.hitTestResult?.extra - if (data != null) { - try { - val intent = android.content.Intent( - android.content.Intent.ACTION_VIEW, - android.net.Uri.parse(data), - ) - context.startActivity(intent) - } catch (_: Exception) {} + val transport = resultMsg?.obj as? WebView.WebViewTransport + ?: return false + + val popup = WebView(context).apply { + settings.javaScriptEnabled = true + webViewClient = object : WebViewClient() { + override fun shouldOverrideUrlLoading( + view: WebView?, + request: WebResourceRequest?, + ): Boolean { + val url = request?.url?.toString() ?: return true + openExternalUrl(url) + return true + } + + override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { + if (url != null) openExternalUrl(url) + view?.stopLoading() + } + } } - return false + transport.webView = popup + resultMsg.sendToTarget() + return true } } diff --git a/CHANGELOG.md b/CHANGELOG.md index 9486cbbe..36acb6c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## v1.7.78-alpha (2026-05-20) + +- Saleor's validated stack changes are now release-ready: dashboard origins on port `9010` are explicitly allowed for dashboard/API calls, preserving the working test-node install path for production nodes. +- NetBird launches now stay pinned to the unified dashboard/proxy origin on port `8087` instead of following stale runtime-discovered server URLs on `8086`. +- NetBird's local nginx proxy now routes browser API, OAuth, relay, and WebSocket traffic through `host.containers.internal:8086` instead of a hard-coded rootless Podman gateway IP, and includes the upstream `management.ProxyService` gRPC path. +- The mobile credentials interstitial now keeps credential lists scrollable and action buttons reachable in both My Apps and the mobile app icon grid. +- Android WebView popup windows now hand external popup URLs to the system browser, covering app login/signup flows that open secondary windows. +- Validation passed with `git diff --check`, `cargo check -p archipelago`, and the focused `npm test -- src/views/appSession/__tests__/appSessionConfig.test.ts` suite. + ## v1.7.77-alpha (2026-05-20) - Saleor first-use now exposes generated credentials through Archipelago instead of leaving users at an unexplained dashboard login: App Details shows copyable `admin@example.com` credentials, and My Apps/mobile icon launches show a pre-launch credentials modal. diff --git a/core/archipelago/src/api/rpc/package/stacks.rs b/core/archipelago/src/api/rpc/package/stacks.rs index 36d774de..f786aa46 100644 --- a/core/archipelago/src/api/rpc/package/stacks.rs +++ b/core/archipelago/src/api/rpc/package/stacks.rs @@ -271,6 +271,7 @@ async fn repair_saleor_network_aliases() { .output() .await; } + } async fn run_required_stack_command( @@ -1740,9 +1741,14 @@ impl RpcHandler { let secret_key = super::config::read_or_generate_secret("saleor-secret-key").await; let admin_pass = super::config::read_or_generate_secret("saleor-admin-password").await; let host_ip = &self.config.host_ip; - let dashboard_url = format!("http://{}:9010/", host_ip); + let dashboard_origin = format!("http://{}:9010", host_ip); + let dashboard_url = format!("{}/", dashboard_origin); let api_url = format!("http://{}:8000/graphql/", host_ip); let allowed_hosts = format!("localhost,127.0.0.1,api,saleor-api,{}", host_ip); + let allowed_client_hosts = format!( + "{},http://localhost:9010,http://127.0.0.1:9010", + dashboard_origin + ); let database_url = format!("postgres://saleor:{}@db/saleor", db_pass); let mut db_cmd = tokio::process::Command::new("podman"); @@ -1884,6 +1890,10 @@ impl RpcHandler { "-e".to_string(), format!("DASHBOARD_URL={}", dashboard_url), "-e".to_string(), + format!("ALLOWED_CLIENT_HOSTS={}", allowed_client_hosts), + "-e".to_string(), + format!("ALLOWED_GRAPHQL_ORIGINS={}", allowed_client_hosts), + "-e".to_string(), format!("ALLOWED_HOSTS={}", allowed_hosts), ]; @@ -2182,10 +2192,9 @@ LETSENCRYPT_DOMAIN=none listen 80; server_name _; - # Route API/auth through the host-published server port. Rootless Podman - # can give netbird-server a new container IP on restart while nginx keeps - # an old resolved address, which breaks login with 502s. - set $netbird_server http://169.254.1.2:8086; + # Route browser API/auth through the host-published server port. Rootless + # Podman can give netbird-server a new container IP on restart while nginx + # keeps an old resolved address, which breaks login with 502s. proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; @@ -2194,17 +2203,17 @@ LETSENCRYPT_DOMAIN=none proxy_http_version 1.1; location ~ ^/(relay|ws-proxy/) {{ - proxy_pass $netbird_server; + proxy_pass http://host.containers.internal:8086; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_read_timeout 1d; }} location ~ ^/(api|oauth2)(/|$) {{ - proxy_pass $netbird_server; + proxy_pass http://host.containers.internal:8086; }} - location ~ ^/(signalexchange\.SignalExchange|management\.ManagementService)/ {{ + location ~ ^/(signalexchange\.SignalExchange|management\.ManagementService|management\.ProxyService)/ {{ grpc_pass grpc://netbird-server:80; grpc_read_timeout 1d; grpc_send_timeout 1d; diff --git a/neode-ui/src/views/Apps.vue b/neode-ui/src/views/Apps.vue index c2aae308..56b90d30 100644 --- a/neode-ui/src/views/Apps.vue +++ b/neode-ui/src/views/Apps.vue @@ -179,7 +179,7 @@ class="fixed inset-0 z-[2700] flex items-end justify-center bg-black/60 backdrop-blur-md p-0 md:items-center md:p-6" @click.self="closeCredentialModal" > -
+

{{ credentialModal.title }}

@@ -187,7 +187,7 @@
-
+
{{ cred.label }} @@ -196,9 +196,9 @@

{{ cred.value }}

-
- - +
+ +
@@ -690,6 +690,18 @@ async function submitSideload() { } .sideload-input::placeholder { color: rgba(255, 255, 255, 0.38); } .sideload-input:focus { border-color: rgba(255, 255, 255, 0.38); } +.credential-modal { + display: flex; + flex-direction: column; +} +.credential-modal-body { + min-height: 0; + overflow-y: auto; + -webkit-overflow-scrolling: touch; +} +.credential-modal-actions { + flex-shrink: 0; +} @media (min-width: 768px) { .sideload-modal { border-radius: 1.25rem; diff --git a/neode-ui/src/views/appSession/__tests__/appSessionConfig.test.ts b/neode-ui/src/views/appSession/__tests__/appSessionConfig.test.ts index ba4d9e8d..47965416 100644 --- a/neode-ui/src/views/appSession/__tests__/appSessionConfig.test.ts +++ b/neode-ui/src/views/appSession/__tests__/appSessionConfig.test.ts @@ -18,4 +18,14 @@ describe('appSessionConfig', () => { expect(resolveAppUrl('mempool')).toBe('http://192.168.1.228:4080') expect(resolveAppUrl('indeedhub')).toBe('http://192.168.1.228:7778') }) + + it('keeps NetBird on the unified dashboard proxy port', () => { + Object.defineProperty(window, 'location', { + value: { hostname: '192.168.1.228' }, + writable: true, + configurable: true, + }) + + expect(resolveAppUrl('netbird', undefined, 'http://localhost:8086')).toBe('http://192.168.1.228:8087') + }) }) diff --git a/neode-ui/src/views/appSession/appSessionConfig.ts b/neode-ui/src/views/appSession/appSessionConfig.ts index bf895be5..e893adea 100644 --- a/neode-ui/src/views/appSession/appSessionConfig.ts +++ b/neode-ui/src/views/appSession/appSessionConfig.ts @@ -101,8 +101,6 @@ export const IFRAME_BLOCKED_APPS = new Set([]) /** Resolve app URL using direct port mapping (source of truth) */ export function resolveAppUrl(id: string, routeQueryPath?: string, runtimeUrl?: string): string { - if (id === 'netbird' && runtimeUrl) return runtimeUrl - // External HTTPS apps const ext = EXTERNAL_URLS[id] if (ext) return ext diff --git a/neode-ui/src/views/apps/AppIconGrid.vue b/neode-ui/src/views/apps/AppIconGrid.vue index a3772f6d..88bc756c 100644 --- a/neode-ui/src/views/apps/AppIconGrid.vue +++ b/neode-ui/src/views/apps/AppIconGrid.vue @@ -73,7 +73,7 @@
-
+

{{ credentialModal.title }}

@@ -81,7 +81,7 @@
-
+
{{ cred.label }} @@ -90,9 +90,9 @@

{{ cred.value }}

-
- - +
+ +
@@ -241,3 +241,43 @@ function scrollToPage(index: number) { el.scrollTo({ left: index * el.clientWidth, behavior: 'smooth' }) } + + diff --git a/neode-ui/src/views/settings/AccountInfoSection.vue b/neode-ui/src/views/settings/AccountInfoSection.vue index 913521c3..da020473 100644 --- a/neode-ui/src/views/settings/AccountInfoSection.vue +++ b/neode-ui/src/views/settings/AccountInfoSection.vue @@ -180,6 +180,20 @@ init()
+ +
+
+ v1.7.78-alpha + May 20, 2026 +
+
+

Saleor's validated port 9010 dashboard/API origin settings are now ready for production release, preserving the working test-node install path.

+

NetBird launches now stay on the unified dashboard/proxy origin at port 8087 instead of following stale server URLs on 8086.

+

NetBird proxy routing no longer depends on a hard-coded rootless Podman gateway IP and now includes the upstream management proxy gRPC path.

+

Mobile credential prompts keep long credential lists scrollable and the Cancel/Continue buttons reachable in both My Apps and the mobile icon grid.

+

Android app-session popups hand external login/signup windows to the system browser instead of dropping them inside the WebView.

+
+