diff --git a/CHANGELOG.md b/CHANGELOG.md index ff456bf4..916dd5ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## v1.7.71-alpha (2026-05-19) + +- NetBird stack installs now pre-create `/var/lib/archipelago/netbird/data` before binding it into `netbird-server`, fixing the failed install/start path seen on `100.70.96.88` where Podman rejected the missing host directory. +- NetBird start/restart ordering now starts `netbird-server` before the dashboard container so lifecycle actions bring the control plane up before the UI. +- App-session invalid IDs and panel-mode fallbacks now return to `/dashboard/apps`, avoiding the stale `/apps` route that could render a 404. +- Mobile launches for apps that block iframes now stay inside the Archipelago app-session fallback instead of automatically opening an external browser tab. +- Installed Gitea containers now report the packaged Gitea icon, and app icon masks use a rounder radius on mobile grids, app cards, and detail headers. +- Validation passed with `npm run type-check`, focused Vitest app-session/app-grid tests, `cargo fmt --all --check --manifest-path core/Cargo.toml`, and `cargo check -p archipelago --manifest-path core/Cargo.toml`. + ## v1.7.70-alpha (2026-05-19) - NetBird is being corrected from the peer/client daemon image to the self-hosted NetBird control-plane stack with a launchable dashboard on port `8087`, a combined management/signal/relay server on `8086`, and STUN on UDP `3478`. diff --git a/core/archipelago/src/api/rpc/package/dependencies.rs b/core/archipelago/src/api/rpc/package/dependencies.rs index 73719c51..b6c10ba7 100644 --- a/core/archipelago/src/api/rpc/package/dependencies.rs +++ b/core/archipelago/src/api/rpc/package/dependencies.rs @@ -288,6 +288,7 @@ pub(super) fn startup_order(package_id: &str) -> &'static [&'static str] { "btcpay-server" | "btcpayserver" | "btcpay" => { &["archy-btcpay-db", "archy-nbxplorer", "btcpay-server"] } + "netbird" => &["netbird-server", "netbird"], "penpot" | "penpot-frontend" => &[ "penpot-postgres", "penpot-valkey", @@ -389,6 +390,11 @@ mod tests { ); } + #[test] + fn netbird_start_order_starts_server_before_dashboard() { + assert_eq!(startup_order("netbird"), &["netbird-server", "netbird"]); + } + #[test] fn unpruned_bitcoin_required_for_electrum_indexers_and_mempool() { for package_id in [ diff --git a/core/archipelago/src/api/rpc/package/stacks.rs b/core/archipelago/src/api/rpc/package/stacks.rs index b6dc25e6..5f1995e2 100644 --- a/core/archipelago/src/api/rpc/package/stacks.rs +++ b/core/archipelago/src/api/rpc/package/stacks.rs @@ -1400,7 +1400,7 @@ impl RpcHandler { self.set_install_phase("netbird", InstallPhase::CreatingContainer) .await; - tokio::fs::create_dir_all("/var/lib/archipelago/netbird") + tokio::fs::create_dir_all("/var/lib/archipelago/netbird/data") .await .context("Failed to create NetBird data directory")?; diff --git a/core/archipelago/src/container/docker_packages.rs b/core/archipelago/src/container/docker_packages.rs index d24afa86..f7b13e12 100644 --- a/core/archipelago/src/container/docker_packages.rs +++ b/core/archipelago/src/container/docker_packages.rs @@ -487,6 +487,13 @@ fn get_app_metadata(app_id: &str) -> AppMetadata { repo: "https://github.com/netbirdio/netbird".to_string(), tier: "", }, + "gitea" => AppMetadata { + title: "Gitea".to_string(), + description: "Self-hosted Git service with repository and package hosting".to_string(), + icon: "/assets/img/app-icons/gitea.svg".to_string(), + repo: "https://gitea.com".to_string(), + tier: "", + }, "indeedhub" | "indeehub" => AppMetadata { title: "IndeedHub".to_string(), description: "Decentralized media streaming platform".to_string(), diff --git a/neode-ui/src/style.css b/neode-ui/src/style.css index 32e51962..a736a964 100644 --- a/neode-ui/src/style.css +++ b/neode-ui/src/style.css @@ -2098,7 +2098,7 @@ html:has(body.video-background-active)::before { position: relative; width: 60px; height: 60px; - border-radius: 14px; + border-radius: 18px; overflow: visible; background: rgba(255, 255, 255, 0.08); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); @@ -2108,7 +2108,16 @@ html:has(body.video-background-active)::before { width: 100%; height: 100%; object-fit: cover; - border-radius: 14px; + border-radius: 18px; +} + +.app-card-icon { + border-radius: 16px; +} + +.app-detail-icon { + border-radius: 22px; + object-fit: cover; } /* Status dot — top-right of icon */ @@ -2140,7 +2149,7 @@ html:has(body.video-background-active)::before { align-items: center; justify-content: center; background: rgba(0, 0, 0, 0.6); - border-radius: 14px; + border-radius: 18px; } .app-icon-label { diff --git a/neode-ui/src/views/AppDetails.vue b/neode-ui/src/views/AppDetails.vue index 872bcfa0..05d43e8a 100644 --- a/neode-ui/src/views/AppDetails.vue +++ b/neode-ui/src/views/AppDetails.vue @@ -158,7 +158,7 @@ const { t } = useI18n() const appId = computed(() => { const id = route.params.id if (typeof id !== 'string' || !/^[a-z0-9][a-z0-9._-]*$/.test(id) || id.length > 64) { - router.replace('/apps') + router.replace('/dashboard/apps') return '' } return id diff --git a/neode-ui/src/views/AppSession.vue b/neode-ui/src/views/AppSession.vue index 99424450..1beb286f 100644 --- a/neode-ui/src/views/AppSession.vue +++ b/neode-ui/src/views/AppSession.vue @@ -138,7 +138,7 @@ const displayMode = ref( const appId = computed(() => { const id = props.appIdProp || (route.params.appId as string) if (typeof id !== 'string' || !/^[a-z0-9][a-z0-9._-]*$/.test(id) || id.length > 64) { - router.replace('/apps') + router.replace('/dashboard/apps') return '' } return id @@ -146,7 +146,7 @@ const appId = computed(() => { const appTitle = computed(() => resolveAppTitle(appId.value)) const isMobile = typeof window !== 'undefined' && window.innerWidth < 768 -const mustOpenNewTab = computed(() => !isMobile && NEW_TAB_APPS.has(appId.value)) +const mustOpenNewTab = computed(() => NEW_TAB_APPS.has(appId.value)) const screensaverReason = computed(() => `app-session:${appId.value}`) const screensaverSuppressedApps = new Set([ 'indeedhub', @@ -197,7 +197,11 @@ function setMode(mode: DisplayMode) { if (!isInlinePanel.value && mode === 'panel') { const id = appId.value const launcher = useAppLauncherStore() - router.push({ name: 'apps' }).then(() => { + const fallback = route.query.returnTo + const fallbackPath = typeof fallback === 'string' && fallback.startsWith('/dashboard') + ? fallback + : '/dashboard/apps' + router.push(fallbackPath).then(() => { launcher.panelAppId = id }) return @@ -337,8 +341,9 @@ watch(displayMode, (mode) => { }) onMounted(() => { - // Apps that block iframes (X-Frame-Options) -- open in new tab, close session - if (mustOpenNewTab.value && appUrl.value) { + // 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) { window.open(appUrl.value, '_blank', 'noopener,noreferrer') if (isInlinePanel.value) emit('close') else closeRouteSession() diff --git a/neode-ui/src/views/appDetails/AppHeroSection.vue b/neode-ui/src/views/appDetails/AppHeroSection.vue index b976c5c3..0c558171 100644 --- a/neode-ui/src/views/appDetails/AppHeroSection.vue +++ b/neode-ui/src/views/appDetails/AppHeroSection.vue @@ -5,7 +5,7 @@ @@ -119,7 +119,7 @@ diff --git a/neode-ui/src/views/apps/AppCard.vue b/neode-ui/src/views/apps/AppCard.vue index c962fe89..5fff698a 100644 --- a/neode-ui/src/views/apps/AppCard.vue +++ b/neode-ui/src/views/apps/AppCard.vue @@ -31,7 +31,7 @@
diff --git a/neode-ui/src/views/apps/__tests__/AppIconGrid.test.ts b/neode-ui/src/views/apps/__tests__/AppIconGrid.test.ts index d8390173..0c9ffc7c 100644 --- a/neode-ui/src/views/apps/__tests__/AppIconGrid.test.ts +++ b/neode-ui/src/views/apps/__tests__/AppIconGrid.test.ts @@ -35,6 +35,11 @@ describe('AppIconGrid', () => { setActivePinia(createPinia()) vi.clearAllMocks() localStorage.clear() + Object.defineProperty(window, 'innerWidth', { + value: 1024, + writable: true, + configurable: true, + }) Object.defineProperty(window, 'location', { value: { hostname: '192.168.1.198' }, writable: true, @@ -55,4 +60,23 @@ describe('AppIconGrid', () => { expect(mockWindowOpen).not.toHaveBeenCalled() expect(useAppLauncherStore().panelAppId).toBe('lnd') }) + + it('opens desktop new-tab apps through app session on mobile', async () => { + Object.defineProperty(window, 'innerWidth', { + value: 390, + writable: true, + configurable: true, + }) + + const wrapper = mount(AppIconGrid, { + props: { apps: [['gitea', makePkg('gitea')]] }, + global: { + plugins: [createPinia()], + }, + }) + + await wrapper.get('.app-icon-item').trigger('click') + + expect(mockWindowOpen).not.toHaveBeenCalled() + }) }) diff --git a/neode-ui/src/views/apps/appsConfig.ts b/neode-ui/src/views/apps/appsConfig.ts index 88997022..b73205d7 100644 --- a/neode-ui/src/views/apps/appsConfig.ts +++ b/neode-ui/src/views/apps/appsConfig.ts @@ -166,7 +166,8 @@ const APP_ICON_FALLBACKS: Record = { } export function resolveAppIcon(id: string, pkg: PackageDataEntry, curatedIcon?: string): string { - const icon = (pkg["static-files"]?.icon || "").trim() + const rawIcon = (pkg["static-files"]?.icon || "").trim() + const icon = rawIcon === '/assets/img/favico.png' ? '' : rawIcon if ( icon.startsWith("/") || icon.startsWith("http://") ||