diff --git a/CHANGELOG.md b/CHANGELOG.md index dab94f7c..95788f2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## v1.7.73-alpha (2026-05-19) + +- Mobile app launches for iframe-blocked apps now open the direct app URL in a new browser tab immediately instead of landing in a broken in-shell webview that requires a second tap. +- Mobile My Apps/Websites tabs now react to route query changes, App Store pages label the mobile view as Discover, mobile filters have safe bottom spacing, and App Store search ignores the current category so searches cover all available apps. +- My Apps search now surfaces matching App Store entries when the app is not installed, making it possible to jump directly from a failed My Apps search to the installable app details. +- NetBird self-host installs now prefer a `100.x` tailnet/CGNAT address for dashboard, management, relay, STUN, and auth redirect origins when one is present; live repair on `100.89.209.89` updated the existing stack from LAN origins to `100.89.209.89` and restored `netbird-server`. +- App-session iframe frames now focus automatically and wrap the iframe in a scroll host so wheel/touch scrolling works in the active right frame without requiring an initial click. + ## v1.7.72-alpha (2026-05-19) - Settings What's New now includes the missing release notes for `v1.7.68-alpha` through `v1.7.71-alpha`, so the modal reflects the current OTA history instead of stopping at `v1.7.67-alpha`. diff --git a/core/archipelago/src/api/rpc/package/stacks.rs b/core/archipelago/src/api/rpc/package/stacks.rs index 5f1995e2..2b97eb01 100644 --- a/core/archipelago/src/api/rpc/package/stacks.rs +++ b/core/archipelago/src/api/rpc/package/stacks.rs @@ -1404,7 +1404,9 @@ impl RpcHandler { .await .context("Failed to create NetBird data directory")?; - let host_ip = self.config.host_ip.clone(); + let host_ip = detect_netbird_public_host_ip() + .await + .unwrap_or_else(|| self.config.host_ip.clone()); let dashboard_origin = format!("http://{}:8087", host_ip); let mgmt_origin = format!("http://{}:8086", host_ip); let relay_secret = read_or_generate_b64_secret("netbird-relay-auth-secret").await; @@ -1544,6 +1546,19 @@ async fn read_or_generate_b64_secret(name: &str) -> String { secret } +async fn detect_netbird_public_host_ip() -> Option { + let output = tokio::process::Command::new("hostname") + .args(["-I"]) + .output() + .await + .ok()?; + let stdout = String::from_utf8_lossy(&output.stdout); + stdout + .split_whitespace() + .find(|ip| ip.starts_with("100.") && ip.contains('.')) + .map(str::to_string) +} + #[cfg(test)] mod tests { use super::{btcpay_stack_app_ids, mempool_stack_app_ids}; diff --git a/neode-ui/src/style.css b/neode-ui/src/style.css index a736a964..ed810f19 100644 --- a/neode-ui/src/style.css +++ b/neode-ui/src/style.css @@ -1659,6 +1659,15 @@ html:has(body.video-background-active)::before { filter: drop-shadow(0 10px 25px rgba(0, 0, 0, 0.5)); } +.mobile-filter-btn { + bottom: calc(var(--mobile-tab-bar-height, 72px) + var(--safe-area-bottom, env(safe-area-inset-bottom, 0px)) + 12px); + filter: drop-shadow(0 10px 25px rgba(0, 0, 0, 0.5)); +} + +.mobile-filter-sheet { + padding-bottom: calc(var(--safe-area-bottom, env(safe-area-inset-bottom, 0px)) + 1.5rem); +} + /* ── Cloud Audio Player (mini bar) ──── */ .cloud-audio-player { diff --git a/neode-ui/src/views/AppSession.vue b/neode-ui/src/views/AppSession.vue index 1beb286f..8d3c4eca 100644 --- a/neode-ui/src/views/AppSession.vue +++ b/neode-ui/src/views/AppSession.vue @@ -341,9 +341,8 @@ watch(displayMode, (mode) => { }) onMounted(() => { - // Desktop apps that block iframes open externally. Mobile keeps the user in - // Archipelago and shows the explicit fallback instead of leaving the shell. - if (!isMobile && mustOpenNewTab.value && appUrl.value) { + // Apps that block iframes open externally instead of landing in a broken webview. + if (mustOpenNewTab.value && appUrl.value) { window.open(appUrl.value, '_blank', 'noopener,noreferrer') if (isInlinePanel.value) emit('close') else closeRouteSession() @@ -532,6 +531,12 @@ onBeforeUnmount(() => { opacity: 0; } +.app-session-frame-scroll-host { + overflow: auto; + overscroll-behavior: contain; + -webkit-overflow-scrolling: touch; +} + /* Mobile: full-bleed app sessions — no border, no radius, no shadow */ @media (max-width: 767px) { .app-session-root { diff --git a/neode-ui/src/views/Apps.vue b/neode-ui/src/views/Apps.vue index a7a4bd75..86d8e731 100644 --- a/neode-ui/src/views/Apps.vue +++ b/neode-ui/src/views/Apps.vue @@ -106,10 +106,34 @@ -
+

{{ t('apps.noResults', { query: searchQuery }) }}

+
+
+ app store +

Available in Discover

+
+
+
+ +
+
+
( route.query.tab === 'websites' || route.query.tab === 'services' ? 'websites' : 'apps' ) +watch(() => route.query.tab, (tab) => { + activeTab.value = tab === 'websites' || tab === 'services' ? 'websites' : 'apps' +}) + // Search (debounced) const searchQuery = ref('') const debouncedSearchQuery = ref('') @@ -309,6 +340,19 @@ const packages = computed(() => { const categoriesWithApps = useCategoriesWithApps(packages, ALL_CATEGORIES) +const curatedApps = getCuratedAppList() +const marketplaceMatches = computed(() => { + const q = debouncedSearchQuery.value.trim().toLowerCase() + if (!q || activeTab.value !== 'apps') return [] as MarketplaceApp[] + return curatedApps.filter(app => { + if (isInstalledInMyApps(app.id)) return false + return app.title?.toLowerCase().includes(q) || + app.id.toLowerCase().includes(q) || + app.author?.toLowerCase().includes(q) || + (typeof app.description === 'string' && app.description.toLowerCase().includes(q)) + }).slice(0, 6) +}) + const isLoadingApps = computed(() => !store.hasLoadedInitialData && !connectionError.value) // Connection error state @@ -352,6 +396,17 @@ const filteredPackageEntries = computed(() => { ) }) +function isInstalledInMyApps(appId: string): boolean { + if (appId in packages.value) return true + const aliases = INSTALLED_ALIASES[appId] + return aliases ? aliases.some(alias => alias in packages.value) : false +} + +function openMarketplaceResult(app: MarketplaceApp) { + setCurrentApp(app) + router.push({ name: 'marketplace-app-detail', params: { id: app.id }, query: { from: 'apps' } }).catch(() => {}) +} + // Uninstall modal const uninstallModal = ref({ show: false, appId: '', appTitle: '' }) diff --git a/neode-ui/src/views/Discover.vue b/neode-ui/src/views/Discover.vue index 3554ad85..52844ce9 100644 --- a/neode-ui/src/views/Discover.vue +++ b/neode-ui/src/views/Discover.vue @@ -35,6 +35,10 @@
+
+ discover +

App Store

+
{ const filteredApps = computed(() => { let apps = allApps.value + if (selectedCategory.value && selectedCategory.value !== 'all' && !searchQuery.value) { + apps = apps.filter(app => app.category === selectedCategory.value) + } if (searchQuery.value) { const query = searchQuery.value.toLowerCase() apps = apps.filter(app => diff --git a/neode-ui/src/views/Marketplace.vue b/neode-ui/src/views/Marketplace.vue index 6ccc4932..87c2dbaa 100644 --- a/neode-ui/src/views/Marketplace.vue +++ b/neode-ui/src/views/Marketplace.vue @@ -37,6 +37,10 @@
+
+ discover +

App Store

+
{ const filteredApps = computed(() => { let apps = allApps.value - if (selectedCategory.value && selectedCategory.value !== 'all') { + if (selectedCategory.value && selectedCategory.value !== 'all' && !searchQuery.value) { apps = apps.filter(app => app.category === selectedCategory.value) } diff --git a/neode-ui/src/views/appSession/AppSessionFrame.vue b/neode-ui/src/views/appSession/AppSessionFrame.vue index 4848b3a7..26d93d31 100644 --- a/neode-ui/src/views/appSession/AppSessionFrame.vue +++ b/neode-ui/src/views/appSession/AppSessionFrame.vue @@ -9,16 +9,18 @@
-