fix(apps): stabilize saleor and netbird release paths

This commit is contained in:
archipelago 2026-05-20 20:38:52 -04:00
parent 556f2e7cac
commit cc1f8fba72
8 changed files with 146 additions and 38 deletions

View File

@ -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
}
}

View File

@ -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.

View File

@ -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;

View File

@ -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"
>
<div class="sideload-modal">
<div class="sideload-modal credential-modal">
<div class="flex items-start justify-between gap-4 mb-5">
<div>
<h2 class="text-lg font-semibold text-white">{{ credentialModal.title }}</h2>
@ -187,7 +187,7 @@
</div>
<button type="button" class="sideload-close-btn" aria-label="Close" @click="closeCredentialModal">&times;</button>
</div>
<div class="space-y-3">
<div class="credential-modal-body space-y-3">
<div v-for="cred in credentialModal.credentials" :key="cred.label" class="rounded-lg border border-white/10 bg-white/[0.04] p-3">
<div class="flex items-center justify-between gap-3 mb-1">
<span class="text-white/60 text-xs uppercase tracking-wide">{{ cred.label }}</span>
@ -196,9 +196,9 @@
<p class="font-mono text-sm text-white break-all">{{ cred.value }}</p>
</div>
</div>
<div class="mt-5 flex gap-3">
<button type="button" class="flex-1 glass-button px-4 py-3 rounded-lg" @click="closeCredentialModal">Cancel</button>
<button type="button" class="flex-1 glass-button px-4 py-3 rounded-lg font-semibold" @click="continueCredentialLaunch">Continue to app</button>
<div class="credential-modal-actions mt-5 flex flex-col sm:flex-row gap-3">
<button type="button" class="w-full sm:flex-1 glass-button px-4 py-3 rounded-lg" @click="closeCredentialModal">Cancel</button>
<button type="button" class="w-full sm:flex-1 glass-button px-4 py-3 rounded-lg font-semibold" @click="continueCredentialLaunch">Continue to app</button>
</div>
</div>
</div>
@ -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;

View File

@ -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')
})
})

View File

@ -101,8 +101,6 @@ export const IFRAME_BLOCKED_APPS = new Set<string>([])
/** 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

View File

@ -73,7 +73,7 @@
<Transition name="fade">
<div v-if="credentialModal.show" 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">
<div class="sideload-modal">
<div class="sideload-modal credential-modal">
<div class="flex items-start justify-between gap-4 mb-5">
<div>
<h2 class="text-lg font-semibold text-white">{{ credentialModal.title }}</h2>
@ -81,7 +81,7 @@
</div>
<button type="button" class="sideload-close-btn" aria-label="Close" @click="closeCredentialModal">&times;</button>
</div>
<div class="space-y-3">
<div class="credential-modal-body space-y-3">
<div v-for="cred in credentialModal.credentials" :key="cred.label" class="rounded-lg border border-white/10 bg-white/[0.04] p-3">
<div class="flex items-center justify-between gap-3 mb-1">
<span class="text-white/60 text-xs uppercase tracking-wide">{{ cred.label }}</span>
@ -90,9 +90,9 @@
<p class="font-mono text-sm text-white break-all">{{ cred.value }}</p>
</div>
</div>
<div class="mt-5 flex gap-3">
<button type="button" class="flex-1 glass-button px-4 py-3 rounded-lg" @click="closeCredentialModal">Cancel</button>
<button type="button" class="flex-1 glass-button px-4 py-3 rounded-lg font-semibold" @click="continueCredentialLaunch">Continue to app</button>
<div class="credential-modal-actions mt-5 flex flex-col sm:flex-row gap-3">
<button type="button" class="w-full sm:flex-1 glass-button px-4 py-3 rounded-lg" @click="closeCredentialModal">Cancel</button>
<button type="button" class="w-full sm:flex-1 glass-button px-4 py-3 rounded-lg font-semibold" @click="continueCredentialLaunch">Continue to app</button>
</div>
</div>
</div>
@ -241,3 +241,43 @@ function scrollToPage(index: number) {
el.scrollTo({ left: index * el.clientWidth, behavior: 'smooth' })
}
</script>
<style scoped>
.sideload-modal {
display: flex;
flex-direction: column;
width: 100%;
max-width: 34rem;
max-height: calc(100dvh - var(--safe-area-top, env(safe-area-inset-top, 0px)) - 12px);
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.14);
border-radius: 1.5rem 1.5rem 0 0;
background: rgba(8, 10, 18, 0.94);
padding: 1.25rem;
padding-bottom: calc(1.25rem + var(--safe-area-bottom, env(safe-area-inset-bottom, 0px)));
box-shadow: 0 -24px 70px rgba(0, 0, 0, 0.55);
}
.sideload-close-btn {
width: 2.25rem;
height: 2.25rem;
flex-shrink: 0;
border-radius: 0.75rem;
color: rgba(255, 255, 255, 0.55);
background: rgba(255, 255, 255, 0.06);
}
.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;
padding: 1.5rem;
box-shadow: 0 25px 80px rgba(0, 0, 0, 0.55);
}
}
</style>

View File

@ -180,6 +180,20 @@ init()
</button>
</div>
<div class="overflow-y-auto flex-1 min-h-0 space-y-6 pr-1">
<!-- v1.7.78-alpha -->
<div>
<div class="flex items-center gap-2 mb-3">
<span class="text-xs font-mono px-2 py-0.5 rounded bg-orange-500/20 text-orange-300">v1.7.78-alpha</span>
<span class="text-xs text-white/40">May 20, 2026</span>
</div>
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
<p>Saleor's validated port 9010 dashboard/API origin settings are now ready for production release, preserving the working test-node install path.</p>
<p>NetBird launches now stay on the unified dashboard/proxy origin at port 8087 instead of following stale server URLs on 8086.</p>
<p>NetBird proxy routing no longer depends on a hard-coded rootless Podman gateway IP and now includes the upstream management proxy gRPC path.</p>
<p>Mobile credential prompts keep long credential lists scrollable and the Cancel/Continue buttons reachable in both My Apps and the mobile icon grid.</p>
<p>Android app-session popups hand external login/signup windows to the system browser instead of dropping them inside the WebView.</p>
</div>
</div>
<!-- v1.7.76-alpha -->
<div>
<div class="flex items-center gap-2 mb-3">