diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f905522..a4aca4d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## v1.7.76-alpha (2026-05-20) + +- Saleor installs now use dashboard port `9010`, avoiding the existing Portainer `9000` binding on the test node while keeping API `8000`, Mailpit `8025`, and Jaeger `16686` unchanged. +- Saleor's Valkey cache no longer bind-mounts `/var/lib/archipelago/saleor-cache`, and the dashboard container has the minimal rootless nginx capabilities it needs to chown cache files, bind port 80 inside the container, and drop workers to the nginx user. +- NetBird's browser proxy now sends API, OAuth, relay, WebSocket, and management traffic through the stable host-published server port at `169.254.1.2:8086`, avoiding stale rootless Podman DNS/IPs after `netbird-server` restarts. +- Mobile App Store category chips now stay visible above the tab bar, Discover is available on mobile, and category selection updates the page route/query so the selected category is actually shown. +- Apps that require a real browser tab now open directly from the app icon tap instead of first entering an in-shell app-session route, including BTCPay, Grafana, Home Assistant, Vaultwarden, Nextcloud, Portainer, OnlyOffice, Tailscale, Uptime Kuma, Gitea, and Nginx Proxy Manager. +- Validation passed with catalog JSON checks, `npm run type-check`, `cargo fmt --all --check --manifest-path core/Cargo.toml`, and `cargo check -p archipelago --manifest-path core/Cargo.toml`; live checks on `100.70.96.88` confirmed Saleor dashboard `9010`/API `8000` and NetBird API/OAuth routes survive `netbird-server` restart. + ## v1.7.75-alpha (2026-05-19) - Saleor is now published as a recommended commerce app with catalog metadata, icon, direct app-session launch on port `9000`, scanner metadata, image pins, and a full stack installer for dashboard, API, worker, PostgreSQL, Valkey, Mailpit, and Jaeger. diff --git a/app-catalog/catalog.json b/app-catalog/catalog.json index 56ce5fd5..6430b0f2 100644 --- a/app-catalog/catalog.json +++ b/app-catalog/catalog.json @@ -76,9 +76,9 @@ "dockerImage": "ghcr.io/saleor/saleor:3.23", "repoUrl": "https://github.com/saleor/saleor", "containerConfig": { - "ports": ["9000:80", "8000:8000", "8025:8025", "16686:16686"], + "ports": ["9010:80", "8000:8000", "8025:8025", "16686:16686"], "volumes": ["/var/lib/archipelago/saleor:/app/media", "/var/lib/archipelago/saleor-db:/var/lib/postgresql/data"], - "notes": "Installed as a Saleor stack: dashboard on 9000, API on 8000, Mailpit on 8025, and Jaeger on 16686. Supporting containers include PostgreSQL, Valkey, Celery worker, and services required by Saleor." + "notes": "Installed as a Saleor stack: dashboard on 9010, API on 8000, Mailpit on 8025, and Jaeger on 16686. Supporting containers include PostgreSQL, Valkey, Celery worker, and services required by Saleor." } }, { diff --git a/core/Cargo.lock b/core/Cargo.lock index 6cf96c5f..0775660f 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -80,7 +80,7 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "archipelago" -version = "1.7.75-alpha" +version = "1.7.76-alpha" dependencies = [ "anyhow", "archipelago-container", diff --git a/core/archipelago/Cargo.toml b/core/archipelago/Cargo.toml index 4aaffd83..f2bb9f7f 100644 --- a/core/archipelago/Cargo.toml +++ b/core/archipelago/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "archipelago" -version = "1.7.75-alpha" +version = "1.7.76-alpha" edition = "2021" description = "Archipelago Bitcoin Node OS - Native backend" authors = ["Archipelago Team"] diff --git a/core/archipelago/src/api/rpc/package/config.rs b/core/archipelago/src/api/rpc/package/config.rs index ecc15dbf..e1092f59 100644 --- a/core/archipelago/src/api/rpc/package/config.rs +++ b/core/archipelago/src/api/rpc/package/config.rs @@ -1080,7 +1080,7 @@ pub(super) async fn get_app_config( None, ), "saleor" => ( - vec!["9000:80".to_string(), "8000:8000".to_string()], + vec!["9010:80".to_string(), "8000:8000".to_string()], vec!["/var/lib/archipelago/saleor:/app/media".to_string()], vec![], None, diff --git a/core/archipelago/src/api/rpc/package/stacks.rs b/core/archipelago/src/api/rpc/package/stacks.rs index 4ff89b03..3738341d 100644 --- a/core/archipelago/src/api/rpc/package/stacks.rs +++ b/core/archipelago/src/api/rpc/package/stacks.rs @@ -167,6 +167,34 @@ async fn repair_netbird_unified_origin() { let _ = pull_image_with_retry(NETBIRD_DASHBOARD_IMAGE).await; let _ = pull_image_with_retry(NETBIRD_PROXY_IMAGE).await; + let _ = tokio::process::Command::new("podman") + .args([ + "network", + "disconnect", + "-f", + "netbird-net", + "netbird-server", + ]) + .output() + .await; + let _ = tokio::process::Command::new("podman") + .args([ + "network", + "connect", + "--alias", + "netbird-server", + "netbird-net", + "netbird-server", + ]) + .output() + .await; + let _ = tokio::process::Command::new("podman") + .args(["restart", "netbird-server"]) + .output() + .await; + + tokio::time::sleep(std::time::Duration::from_secs(2)).await; + let _ = tokio::process::Command::new("podman") .args([ "run", @@ -200,32 +228,6 @@ async fn repair_netbird_unified_origin() { ]) .output() .await; - - let _ = tokio::process::Command::new("podman") - .args([ - "network", - "disconnect", - "-f", - "netbird-net", - "netbird-server", - ]) - .output() - .await; - let _ = tokio::process::Command::new("podman") - .args([ - "network", - "connect", - "--alias", - "netbird-server", - "netbird-net", - "netbird-server", - ]) - .output() - .await; - let _ = tokio::process::Command::new("podman") - .args(["restart", "netbird-server"]) - .output() - .await; } async fn repair_saleor_network_aliases() { @@ -1736,8 +1738,9 @@ impl RpcHandler { let db_pass = super::config::read_or_generate_secret("saleor-db-password").await; 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://{}:9000/", host_ip); + let dashboard_url = format!("http://{}:9010/", host_ip); let api_url = format!("http://{}:8000/graphql/", host_ip); let allowed_hosts = format!("localhost,127.0.0.1,api,saleor-api,{}", host_ip); let database_url = format!("postgres://saleor:{}@db/saleor", db_pass); @@ -1797,8 +1800,6 @@ impl RpcHandler { "--health-cmd=valkey-cli ping || exit 1", "--health-interval=30s", "--health-retries=3", - "-v", - "/var/lib/archipelago/saleor-cache:/data", SALEOR_VALKEY_IMAGE, ]); run_required_stack_command("saleor", "create cache", &mut cache_cmd).await?; @@ -1928,6 +1929,30 @@ impl RpcHandler { } } + let mut admin_cmd = tokio::process::Command::new("podman"); + admin_cmd.args([ + "run", + "--rm", + "--network", + "saleor-net", + "-v", + "/var/lib/archipelago/saleor:/app/media", + ]); + admin_cmd.args(&saleor_env); + admin_cmd.args([ + "-e", + "DJANGO_SUPERUSER_EMAIL=admin@example.com", + "-e", + &format!("DJANGO_SUPERUSER_PASSWORD={}", admin_pass), + SALEOR_API_IMAGE, + "python3", + "manage.py", + "createsuperuser", + "--noinput", + ]); + run_required_stack_command("saleor", "create admin user", &mut admin_cmd).await?; + install_log("INSTALL INFO: saleor admin email admin@example.com; password stored in /var/lib/archipelago/secrets/saleor-admin-password").await; + let mut api_cmd = tokio::process::Command::new("podman"); api_cmd.args([ "run", @@ -2005,11 +2030,17 @@ impl RpcHandler { "saleor-net", "--restart=unless-stopped", "--cap-drop=ALL", + "--cap-add=CHOWN", + "--cap-add=DAC_OVERRIDE", + "--cap-add=FOWNER", + "--cap-add=NET_BIND_SERVICE", + "--cap-add=SETGID", + "--cap-add=SETUID", "--security-opt=no-new-privileges:true", "--memory=256m", "--pids-limit=2048", "-p", - "9000:80", + "9010:80", "-e", &format!("API_URL={}", api_url), "-e", @@ -2128,6 +2159,11 @@ 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; + proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; @@ -2135,14 +2171,14 @@ LETSENCRYPT_DOMAIN=none proxy_http_version 1.1; location ~ ^/(relay|ws-proxy/) {{ - proxy_pass http://netbird-server:80; + proxy_pass $netbird_server; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_read_timeout 1d; }} location ~ ^/(api|oauth2)(/|$) {{ - proxy_pass http://netbird-server:80; + proxy_pass $netbird_server; }} location ~ ^/(signalexchange\.SignalExchange|management\.ManagementService)/ {{ diff --git a/core/archipelago/src/container/docker_packages.rs b/core/archipelago/src/container/docker_packages.rs index 0ceab617..c5a6f1dc 100644 --- a/core/archipelago/src/container/docker_packages.rs +++ b/core/archipelago/src/container/docker_packages.rs @@ -147,7 +147,9 @@ impl DockerPackageScanner { let metadata = get_app_metadata(&app_id); // Resolve UI address: separate UI containers > static map > dynamic ports - let lan_address = if let Some(ui_address) = ui_containers.get(&app_id) { + let lan_address = if app_id == "netbird" { + reachable_lan_address(&app_id, netbird_configured_launch_url().await).await + } else if let Some(ui_address) = ui_containers.get(&app_id) { // Apps with separate UI containers (e.g. archy-bitcoin-ui, archy-lnd-ui) debug!("Using UI container for {}: {}", app_id, ui_address); reachable_lan_address(&app_id, Some(ui_address.clone())).await @@ -497,7 +499,7 @@ fn get_app_metadata(app_id: &str) -> AppMetadata { }, "saleor" => AppMetadata { title: "Saleor".to_string(), - description: "Composable commerce platform with GraphQL API and dashboard".to_string(), + description: "Composable commerce platform with GraphQL API and dashboard. Admin email: admin@example.com; password is stored on the node at /var/lib/archipelago/secrets/saleor-admin-password".to_string(), icon: "/assets/img/app-icons/saleor.svg".to_string(), repo: "https://github.com/saleor/saleor".to_string(), tier: "", @@ -688,6 +690,18 @@ fn extract_lan_address(ports: &[String]) -> Option { None } +async fn netbird_configured_launch_url() -> Option { + let env = tokio::fs::read_to_string("/var/lib/archipelago/netbird/dashboard.env") + .await + .ok()?; + env.lines() + .find_map(|line| line.strip_prefix("NETBIRD_MGMT_API_ENDPOINT=")) + .map(str::trim) + .filter(|s| !s.is_empty()) + .map(ToOwned::to_owned) + .or_else(|| PodmanClient::lan_address_for("netbird")) +} + async fn reachable_lan_address(app_id: &str, candidate: Option) -> Option { let url = candidate?; if !requires_reachable_launch(app_id) { diff --git a/core/container/src/podman_client.rs b/core/container/src/podman_client.rs index ce674232..b0012482 100644 --- a/core/container/src/podman_client.rs +++ b/core/container/src/podman_client.rs @@ -129,6 +129,7 @@ impl PodmanClient { "filebrowser" => "http://localhost:8083", "nginx-proxy-manager" => "http://localhost:8081", "portainer" => "http://localhost:9000", + "saleor" => "http://localhost:9010", "uptime-kuma" => "http://localhost:3002", "fedimint" | "fedimintd" => "http://localhost:8175", "fedimint-gateway" => "http://localhost:8176", @@ -136,6 +137,7 @@ impl PodmanClient { "indeedhub" => "http://localhost:7778", "dwn" => "http://localhost:3100", "endurain" => "http://localhost:8080", + "netbird" => "http://localhost:8087", "electrs" | "archy-electrs-ui" => "http://localhost:50002", _ => return None, }; diff --git a/neode-ui/package-lock.json b/neode-ui/package-lock.json index b50df924..2a3e349c 100644 --- a/neode-ui/package-lock.json +++ b/neode-ui/package-lock.json @@ -1,12 +1,12 @@ { "name": "neode-ui", - "version": "1.7.75-alpha", + "version": "1.7.76-alpha", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "neode-ui", - "version": "1.7.75-alpha", + "version": "1.7.76-alpha", "dependencies": { "@types/dompurify": "^3.0.5", "@vue-leaflet/vue-leaflet": "^0.10.1", diff --git a/neode-ui/package.json b/neode-ui/package.json index 95fe7d8a..c2cc5de5 100644 --- a/neode-ui/package.json +++ b/neode-ui/package.json @@ -1,7 +1,7 @@ { "name": "neode-ui", "private": true, - "version": "1.7.75-alpha", + "version": "1.7.76-alpha", "type": "module", "scripts": { "start": "./start-dev.sh", diff --git a/neode-ui/public/catalog.json b/neode-ui/public/catalog.json index 56ce5fd5..6430b0f2 100644 --- a/neode-ui/public/catalog.json +++ b/neode-ui/public/catalog.json @@ -76,9 +76,9 @@ "dockerImage": "ghcr.io/saleor/saleor:3.23", "repoUrl": "https://github.com/saleor/saleor", "containerConfig": { - "ports": ["9000:80", "8000:8000", "8025:8025", "16686:16686"], + "ports": ["9010:80", "8000:8000", "8025:8025", "16686:16686"], "volumes": ["/var/lib/archipelago/saleor:/app/media", "/var/lib/archipelago/saleor-db:/var/lib/postgresql/data"], - "notes": "Installed as a Saleor stack: dashboard on 9000, API on 8000, Mailpit on 8025, and Jaeger on 16686. Supporting containers include PostgreSQL, Valkey, Celery worker, and services required by Saleor." + "notes": "Installed as a Saleor stack: dashboard on 9010, API on 8000, Mailpit on 8025, and Jaeger on 16686. Supporting containers include PostgreSQL, Valkey, Celery worker, and services required by Saleor." } }, { diff --git a/neode-ui/src/stores/appLauncher.ts b/neode-ui/src/stores/appLauncher.ts index baad7db0..66992031 100644 --- a/neode-ui/src/stores/appLauncher.ts +++ b/neode-ui/src/stores/appLauncher.ts @@ -17,6 +17,15 @@ const NEW_TAB_PORTS = new Set([ ]) const NEW_TAB_APP_IDS = new Set([ + 'btcpay-server', + 'grafana', + 'photoprism', + 'homeassistant', + 'vaultwarden', + 'nextcloud', + 'portainer', + 'onlyoffice', + 'tailscale', 'nginx-proxy-manager', 'uptime-kuma', 'gitea', @@ -93,6 +102,7 @@ const PORT_TO_APP_ID: Record = { '8334': 'bitcoin-knots', '8888': 'searxng', '9000': 'portainer', + '9010': 'saleor', '8087': 'netbird', '8086': 'netbird', '9980': 'onlyoffice', @@ -109,6 +119,27 @@ const PORT_TO_APP_ID: Record = { '3010': 'thunderhub', } +const APP_ID_TO_PORT: Record = { + 'btcpay-server': '23000', + grafana: '3000', + photoprism: '2342', + homeassistant: '8123', + vaultwarden: '8082', + nextcloud: '8085', + portainer: '9000', + onlyoffice: '8044', + tailscale: '8240', + 'nginx-proxy-manager': '8081', + 'uptime-kuma': '3002', + gitea: '3001', +} + +function directAppUrl(appId: string): string | null { + const port = APP_ID_TO_PORT[appId] + if (!port || typeof window === 'undefined') return null + return `http://${window.location.hostname}:${port}` +} + const APPROVED_ORIGINS_KEY = 'neode_nostr_approved_origins' @@ -161,6 +192,12 @@ export const useAppLauncherStore = defineStore('appLauncher', () => { } function openSession(appId: string) { + const launchUrl = NEW_TAB_APP_IDS.has(appId) ? directAppUrl(appId) : null + if (launchUrl) { + window.open(launchUrl, '_blank', 'noopener,noreferrer') + return + } + const mode = localStorage.getItem(DISPLAY_MODE_KEY) || 'panel' if (mode === 'panel' && !isMobileViewport()) { panelAppId.value = appId diff --git a/neode-ui/src/style.css b/neode-ui/src/style.css index ed810f19..cfa0a744 100644 --- a/neode-ui/src/style.css +++ b/neode-ui/src/style.css @@ -1668,6 +1668,36 @@ html:has(body.video-background-active)::before { padding-bottom: calc(var(--safe-area-bottom, env(safe-area-inset-bottom, 0px)) + 1.5rem); } +.mobile-category-strip { + display: flex; + gap: 0.5rem; + overflow-x: auto; + overscroll-behavior-x: contain; + padding-bottom: 0.25rem; + scrollbar-width: none; +} + +.mobile-category-strip::-webkit-scrollbar { + display: none; +} + +.mobile-category-pill { + flex: 0 0 auto; + border: 1px solid rgba(255, 255, 255, 0.14); + border-radius: 999px; + background: rgba(255, 255, 255, 0.08); + color: rgba(255, 255, 255, 0.78); + padding: 0.55rem 0.9rem; + font-size: 0.85rem; + font-weight: 600; +} + +.mobile-category-pill-active { + border-color: rgba(255, 255, 255, 0.36); + background: rgba(255, 255, 255, 0.2); + color: white; +} + /* ── 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 8d3c4eca..b70be4de 100644 --- a/neode-ui/src/views/AppSession.vue +++ b/neode-ui/src/views/AppSession.vue @@ -91,6 +91,7 @@ import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue' import { useRoute, useRouter } from 'vue-router' import { useAppLauncherStore } from '@/stores/appLauncher' +import { useAppStore } from '@/stores/app' import { useScreensaverStore } from '@/stores/screensaver' import NostrIdentityPicker from '@/components/NostrIdentityPicker.vue' import AppSessionHeader from './appSession/AppSessionHeader.vue' @@ -116,6 +117,7 @@ const isInlinePanel = computed(() => !!props.appIdProp) const route = useRoute() const router = useRouter() +const store = useAppStore() const screensaverStore = useScreensaverStore() const sessionRef = ref(null) @@ -157,7 +159,8 @@ const screensaverSuppressedApps = new Set([ ]) const appUrl = computed(() => { - return resolveAppUrl(appId.value, route.query.path as string | undefined) + const runtimeUrl = store.data?.['package-data']?.[appId.value]?.installed?.['interface-addresses']?.main?.['lan-address'] || undefined + return resolveAppUrl(appId.value, route.query.path as string | undefined, runtimeUrl) }) function closeRouteSession() { diff --git a/neode-ui/src/views/Discover.vue b/neode-ui/src/views/Discover.vue index 52844ce9..6e071e44 100644 --- a/neode-ui/src/views/Discover.vue +++ b/neode-ui/src/views/Discover.vue @@ -33,12 +33,22 @@ /> - +
discover

App Store

+
+ + +
// Cypherpunks write code. We run nodes.

- @@ -191,7 +196,6 @@ import { useToast } from '@/composables/useToast' import DiscoverHero from './discover/DiscoverHero.vue' import FeaturedApps from './discover/FeaturedApps.vue' import AppGrid from './discover/AppGrid.vue' -import FilterModal from './discover/FilterModal.vue' import type { MarketplaceApp, FeaturedApp } from './discover/types' import { getCuratedAppList, INSTALLED_ALIASES, FEATURED_DEFINITIONS, categorizeCommunityApp, fetchAppCatalog, type CatalogFeatured } from './discover/curatedApps' @@ -228,13 +232,6 @@ const categories = computed(() => [ // been removed in favour of the store's phase-aware mapping. const installingApps = serverStore.installingApps -function selectCategory(id: string) { - selectedCategory.value = id - if (id === 'nostr' && nostrApps.value.length === 0 && !nostrLoading.value) { - loadNostrMarketplace() - } -} - function navigateToMarketplace(categoryId: string) { router.push({ name: 'marketplace', query: { category: categoryId } }) } diff --git a/neode-ui/src/views/Marketplace.vue b/neode-ui/src/views/Marketplace.vue index 87c2dbaa..3fb8befe 100644 --- a/neode-ui/src/views/Marketplace.vue +++ b/neode-ui/src/views/Marketplace.vue @@ -35,12 +35,27 @@ /> - +
discover

App Store

+
+ + +
-
@@ -113,7 +123,7 @@ let marketplaceAnimationDone = false