From 522c0465256fd221bb24bf1e1071525289e9d67c Mon Sep 17 00:00:00 2001 From: archipelago Date: Tue, 19 May 2026 20:11:22 -0400 Subject: [PATCH] feat(apps): add saleor and harden netbird repair --- app-catalog/catalog.json | 17 + .../archipelago/src/api/rpc/package/config.rs | 18 + .../src/api/rpc/package/dependencies.rs | 9 + .../src/api/rpc/package/install.rs | 3 + .../archipelago/src/api/rpc/package/stacks.rs | 510 +++++++++++++++++- .../src/container/docker_packages.rs | 15 + .../src/container/image_versions.rs | 15 + .../public/assets/img/app-icons/saleor.svg | 5 + neode-ui/public/catalog.json | 17 + neode-ui/src/views/Dashboard.vue | 11 + .../src/views/appSession/appSessionConfig.ts | 3 +- neode-ui/src/views/discover/curatedApps.ts | 4 +- .../src/views/marketplace/marketplaceData.ts | 17 +- scripts/image-versions.sh | 8 + 14 files changed, 631 insertions(+), 21 deletions(-) create mode 100644 neode-ui/public/assets/img/app-icons/saleor.svg diff --git a/app-catalog/catalog.json b/app-catalog/catalog.json index 7d652116..56ce5fd5 100644 --- a/app-catalog/catalog.json +++ b/app-catalog/catalog.json @@ -64,6 +64,23 @@ "bitcoin-knots" ] }, + { + "id": "saleor", + "title": "Saleor", + "version": "3.23", + "description": "Composable commerce platform with GraphQL API, dashboard, worker, mail testing, and tracing.", + "icon": "/assets/img/app-icons/saleor.svg", + "author": "Saleor", + "category": "commerce", + "tier": "recommended", + "dockerImage": "ghcr.io/saleor/saleor:3.23", + "repoUrl": "https://github.com/saleor/saleor", + "containerConfig": { + "ports": ["9000: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." + } + }, { "id": "mempool", "title": "Mempool Explorer", diff --git a/core/archipelago/src/api/rpc/package/config.rs b/core/archipelago/src/api/rpc/package/config.rs index 13df17d0..ecc15dbf 100644 --- a/core/archipelago/src/api/rpc/package/config.rs +++ b/core/archipelago/src/api/rpc/package/config.rs @@ -31,6 +31,7 @@ fn is_platform_managed_app(app_id: &str) -> bool { | "fedimint" | "fedimint-gateway" | "indeedhub" + | "saleor" | "immich" ) } @@ -500,6 +501,15 @@ pub(super) fn all_container_names(package_id: &str) -> Vec { "netbird-dashboard".into(), "netbird-server".into(), ], + "saleor" => vec![ + "saleor-db".into(), + "saleor-cache".into(), + "saleor-api".into(), + "saleor-worker".into(), + "saleor-jaeger".into(), + "saleor-mailpit".into(), + "saleor".into(), + ], "nostr-vpn" => vec![ "nostr-vpn".into(), "archy-nostr-vpn".into(), @@ -589,6 +599,7 @@ pub(super) fn get_data_dirs_for_app(package_id: &str) -> Vec { format!("{}/penpot-postgres", base), ], "netbird" => vec![format!("{}/netbird", base)], + "saleor" => vec![format!("{}/saleor", base), format!("{}/saleor-db", base)], _ => vec![format!("{}/{}", base, package_id)], } } @@ -1068,6 +1079,13 @@ pub(super) async fn get_app_config( None, None, ), + "saleor" => ( + vec!["9000:80".to_string(), "8000:8000".to_string()], + vec!["/var/lib/archipelago/saleor:/app/media".to_string()], + vec![], + None, + None, + ), "nostr-rs-relay" => ( vec!["18081:8080".to_string()], vec!["/var/lib/archipelago/nostr-rs-relay:/usr/src/app/db".to_string()], diff --git a/core/archipelago/src/api/rpc/package/dependencies.rs b/core/archipelago/src/api/rpc/package/dependencies.rs index 8e2e7af2..9a03de87 100644 --- a/core/archipelago/src/api/rpc/package/dependencies.rs +++ b/core/archipelago/src/api/rpc/package/dependencies.rs @@ -289,6 +289,15 @@ pub(super) fn startup_order(package_id: &str) -> &'static [&'static str] { &["archy-btcpay-db", "archy-nbxplorer", "btcpay-server"] } "netbird" => &["netbird-server", "netbird-dashboard", "netbird"], + "saleor" => &[ + "saleor-db", + "saleor-cache", + "saleor-jaeger", + "saleor-mailpit", + "saleor-api", + "saleor-worker", + "saleor", + ], "penpot" | "penpot-frontend" => &[ "penpot-postgres", "penpot-valkey", diff --git a/core/archipelago/src/api/rpc/package/install.rs b/core/archipelago/src/api/rpc/package/install.rs index 13f7d9b4..c54a9b98 100644 --- a/core/archipelago/src/api/rpc/package/install.rs +++ b/core/archipelago/src/api/rpc/package/install.rs @@ -244,6 +244,9 @@ impl RpcHandler { if package_id == "netbird" { return self.install_netbird_stack().await; } + if package_id == "saleor" { + return self.install_saleor_stack().await; + } // Dependency checks. Prefer the scanner's cached package state so a // congested Podman API does not turn an already-running dependency into diff --git a/core/archipelago/src/api/rpc/package/stacks.rs b/core/archipelago/src/api/rpc/package/stacks.rs index ce4c327f..4ff89b03 100644 --- a/core/archipelago/src/api/rpc/package/stacks.rs +++ b/core/archipelago/src/api/rpc/package/stacks.rs @@ -99,6 +99,7 @@ async fn repair_stack_before_adopt(stack_name: &str) { } "indeedhub" => repair_indeedhub_network_aliases().await, "netbird" => repair_netbird_unified_origin().await, + "saleor" => repair_saleor_network_aliases().await, _ => {} } } @@ -151,23 +152,9 @@ async fn repair_netbird_unified_origin() { .unwrap_or_else(|| "127.0.0.1".to_string()); let _ = write_netbird_config_files(&host_ip).await; - let names = tokio::process::Command::new("podman") - .args(["ps", "-a", "--format", "{{.Names}}"]) - .output() - .await - .ok() - .map(|o| String::from_utf8_lossy(&o.stdout).to_string()) - .unwrap_or_default(); - - let has_proxy = names.lines().any(|n| n.trim() == "netbird"); - let has_dashboard = names.lines().any(|n| n.trim() == "netbird-dashboard"); - if has_proxy && has_dashboard { - return; - } - - if has_proxy && !has_dashboard { + for container in ["netbird", "netbird-dashboard"] { let _ = tokio::process::Command::new("podman") - .args(["rm", "-f", "netbird"]) + .args(["rm", "-f", container]) .output() .await; } @@ -177,6 +164,9 @@ async fn repair_netbird_unified_origin() { .output() .await; + 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([ "run", @@ -210,6 +200,75 @@ 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() { + let _ = tokio::process::Command::new("podman") + .args(["network", "create", "saleor-net"]) + .output() + .await; + + for (container, alias) in [ + ("saleor-db", "db"), + ("saleor-cache", "cache"), + ("saleor-jaeger", "jaeger"), + ("saleor-mailpit", "mailpit"), + ("saleor-api", "api"), + ("saleor-worker", "worker"), + ("saleor", "saleor"), + ] { + let exists = tokio::process::Command::new("podman") + .args(["container", "exists", container]) + .status() + .await + .map(|s| s.success()) + .unwrap_or(false); + if !exists { + continue; + } + + let _ = tokio::process::Command::new("podman") + .args(["network", "disconnect", "-f", "saleor-net", container]) + .output() + .await; + let _ = tokio::process::Command::new("podman") + .args([ + "network", + "connect", + "--alias", + alias, + "saleor-net", + container, + ]) + .output() + .await; + } } async fn run_required_stack_command( @@ -382,6 +441,12 @@ const REGISTRY: &str = "146.59.87.168:3000/lfg2025"; const NETBIRD_DASHBOARD_IMAGE: &str = "docker.io/netbirdio/dashboard:v2.38.0"; const NETBIRD_SERVER_IMAGE: &str = "docker.io/netbirdio/netbird-server:0.71.2"; const NETBIRD_PROXY_IMAGE: &str = "docker.io/library/nginx:1.27-alpine"; +const SALEOR_API_IMAGE: &str = "ghcr.io/saleor/saleor:3.23"; +const SALEOR_DASHBOARD_IMAGE: &str = "ghcr.io/saleor/saleor-dashboard:3.23"; +const SALEOR_POSTGRES_IMAGE: &str = "docker.io/library/postgres:15-alpine"; +const SALEOR_VALKEY_IMAGE: &str = "docker.io/valkey/valkey:8.1-alpine"; +const SALEOR_JAEGER_IMAGE: &str = "docker.io/jaegertracing/jaeger:latest"; +const SALEOR_MAILPIT_IMAGE: &str = "docker.io/axllent/mailpit:latest"; /// Pull an image with retry and exponential backoff (3 attempts). async fn pull_image_with_retry(image: &str) -> Result<()> { @@ -1574,6 +1639,415 @@ impl RpcHandler { "message": "NetBird self-hosted stack installed", })) } + + /// Install Saleor stack (PostgreSQL + Valkey + API + worker + dashboard + Jaeger + Mailpit). + pub(super) async fn install_saleor_stack(&self) -> Result { + if let Some(adopted) = adopt_stack_if_exists( + "saleor", + "saleor", + &[ + "saleor-db", + "saleor-cache", + "saleor-jaeger", + "saleor-mailpit", + "saleor-api", + "saleor-worker", + "saleor", + ], + ) + .await? + { + return Ok(adopted); + } + + install_log("INSTALL START: saleor stack (postgres + valkey + api + worker + dashboard)") + .await; + info!("Installing Saleor stack"); + + let images = [ + SALEOR_POSTGRES_IMAGE, + SALEOR_VALKEY_IMAGE, + SALEOR_API_IMAGE, + SALEOR_DASHBOARD_IMAGE, + SALEOR_JAEGER_IMAGE, + SALEOR_MAILPIT_IMAGE, + ]; + self.set_install_phase("saleor", InstallPhase::PullingImage) + .await; + let n_images = images.len() as u64; + for (i, image) in images.iter().enumerate() { + self.set_install_progress("saleor", i as u64, n_images) + .await; + pull_image_with_retry(image) + .await + .with_context(|| format!("Failed to pull Saleor image: {}", image))?; + } + self.set_install_progress("saleor", n_images, n_images) + .await; + + for name in [ + "saleor", + "saleor-api", + "saleor-worker", + "saleor-db", + "saleor-cache", + "saleor-jaeger", + "saleor-mailpit", + ] { + let _ = tokio::process::Command::new("podman") + .args(["rm", "-f", name]) + .status() + .await; + } + let _ = tokio::process::Command::new("podman") + .args(["network", "rm", "-f", "saleor-net"]) + .status() + .await; + + self.set_install_phase("saleor", InstallPhase::CreatingContainer) + .await; + + let _ = tokio::process::Command::new("sudo") + .args([ + "mkdir", + "-p", + "/var/lib/archipelago/saleor", + "/var/lib/archipelago/saleor-db", + "/var/lib/archipelago/saleor-cache", + ]) + .output() + .await; + let user = std::env::var("USER").unwrap_or_else(|_| "archipelago".to_string()); + for dir in [ + "/var/lib/archipelago/saleor", + "/var/lib/archipelago/saleor-db", + "/var/lib/archipelago/saleor-cache", + ] { + let _ = tokio::process::Command::new("sudo") + .args(["chown", "-R", &format!("{}:{}", user, user), dir]) + .output() + .await; + } + + let _ = tokio::process::Command::new("podman") + .args(["network", "create", "saleor-net"]) + .status() + .await; + + 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 host_ip = &self.config.host_ip; + let dashboard_url = format!("http://{}:9000/", 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); + + let mut db_cmd = tokio::process::Command::new("podman"); + db_cmd.args([ + "run", + "-d", + "--name", + "saleor-db", + "--network", + "saleor-net", + "--network-alias", + "db", + "--restart=unless-stopped", + "--cap-drop=ALL", + "--cap-add=CHOWN", + "--cap-add=DAC_OVERRIDE", + "--cap-add=FOWNER", + "--cap-add=SETGID", + "--cap-add=SETUID", + "--security-opt=no-new-privileges:true", + "--memory=512m", + "--pids-limit=4096", + "--health-cmd=pg_isready -U saleor || exit 1", + "--health-interval=30s", + "--health-retries=3", + "-v", + "/var/lib/archipelago/saleor-db:/var/lib/postgresql/data", + "-e", + "POSTGRES_USER=saleor", + "-e", + &format!("POSTGRES_PASSWORD={}", db_pass), + "-e", + "POSTGRES_DB=saleor", + SALEOR_POSTGRES_IMAGE, + ]); + run_required_stack_command("saleor", "create postgres", &mut db_cmd).await?; + + let mut cache_cmd = tokio::process::Command::new("podman"); + cache_cmd.args([ + "run", + "-d", + "--name", + "saleor-cache", + "--network", + "saleor-net", + "--network-alias", + "cache", + "--restart=unless-stopped", + "--cap-drop=ALL", + "--cap-add=SETGID", + "--cap-add=SETUID", + "--security-opt=no-new-privileges:true", + "--memory=128m", + "--pids-limit=2048", + "--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?; + + let mut jaeger_cmd = tokio::process::Command::new("podman"); + jaeger_cmd.args([ + "run", + "-d", + "--name", + "saleor-jaeger", + "--network", + "saleor-net", + "--network-alias", + "jaeger", + "--restart=unless-stopped", + "--cap-drop=ALL", + "--security-opt=no-new-privileges:true", + "--memory=512m", + "--pids-limit=4096", + "-p", + "16686:16686", + "-p", + "4317:4317", + "-p", + "4318:4318", + "--tmpfs", + "/tmp:rw,nosuid,nodev,size=128m", + SALEOR_JAEGER_IMAGE, + ]); + run_required_stack_command("saleor", "create jaeger", &mut jaeger_cmd).await?; + + let mut mailpit_cmd = tokio::process::Command::new("podman"); + mailpit_cmd.args([ + "run", + "-d", + "--name", + "saleor-mailpit", + "--network", + "saleor-net", + "--network-alias", + "mailpit", + "--restart=unless-stopped", + "--cap-drop=ALL", + "--security-opt=no-new-privileges:true", + "--memory=128m", + "--pids-limit=2048", + "-p", + "1025:1025", + "-p", + "8025:8025", + SALEOR_MAILPIT_IMAGE, + ]); + run_required_stack_command("saleor", "create mailpit", &mut mailpit_cmd).await?; + + tokio::time::sleep(std::time::Duration::from_secs(8)).await; + + let saleor_env = vec![ + "-e".to_string(), + "CACHE_URL=redis://cache:6379/0".to_string(), + "-e".to_string(), + "CELERY_BROKER_URL=redis://cache:6379/1".to_string(), + "-e".to_string(), + format!("DATABASE_URL={}", database_url), + "-e".to_string(), + "DEFAULT_CHANNEL_SLUG=default-channel".to_string(), + "-e".to_string(), + "DEFAULT_FROM_EMAIL=noreply@example.com".to_string(), + "-e".to_string(), + "EMAIL_URL=smtp://mailpit:1025".to_string(), + "-e".to_string(), + format!("SECRET_KEY={}", secret_key), + "-e".to_string(), + "OTEL_SERVICE_NAME=saleor".to_string(), + "-e".to_string(), + "OTEL_TRACES_EXPORTER=otlp".to_string(), + "-e".to_string(), + "OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4317".to_string(), + "-e".to_string(), + "HTTP_IP_FILTER_ALLOW_LOOPBACK_IPS=True".to_string(), + "-e".to_string(), + "HTTP_IP_FILTER_ENABLED=False".to_string(), + "-e".to_string(), + format!("DASHBOARD_URL={}", dashboard_url), + "-e".to_string(), + format!("ALLOWED_HOSTS={}", allowed_hosts), + ]; + + let mut migrate_cmd = tokio::process::Command::new("podman"); + migrate_cmd.args([ + "run", + "--rm", + "--network", + "saleor-net", + "-v", + "/var/lib/archipelago/saleor:/app/media", + ]); + migrate_cmd.args(&saleor_env); + migrate_cmd.args([SALEOR_API_IMAGE, "python3", "manage.py", "migrate"]); + run_required_stack_command("saleor", "run migrations", &mut migrate_cmd).await?; + + let mut populate_cmd = tokio::process::Command::new("podman"); + populate_cmd.args([ + "run", + "--rm", + "--network", + "saleor-net", + "-v", + "/var/lib/archipelago/saleor:/app/media", + ]); + populate_cmd.args(&saleor_env); + populate_cmd.args([ + SALEOR_API_IMAGE, + "python3", + "manage.py", + "populatedb", + "--createsuperuser", + ]); + let populate = populate_cmd.output().await; + if let Ok(output) = populate { + if !output.status.success() { + install_log(&format!( + "INSTALL WARN: saleor - populate sample data skipped: {}{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + )) + .await; + } + } + + let mut api_cmd = tokio::process::Command::new("podman"); + api_cmd.args([ + "run", + "-d", + "--name", + "saleor-api", + "--network", + "saleor-net", + "--network-alias", + "api", + "--restart=unless-stopped", + "--cap-drop=ALL", + "--cap-add=CHOWN", + "--cap-add=DAC_OVERRIDE", + "--cap-add=FOWNER", + "--cap-add=SETGID", + "--cap-add=SETUID", + "--security-opt=no-new-privileges:true", + "--memory=1g", + "--pids-limit=4096", + "-p", + "8000:8000", + "-v", + "/var/lib/archipelago/saleor:/app/media", + ]); + api_cmd.args(&saleor_env); + api_cmd.arg(SALEOR_API_IMAGE); + run_required_stack_command("saleor", "create api", &mut api_cmd).await?; + + let mut worker_cmd = tokio::process::Command::new("podman"); + worker_cmd.args([ + "run", + "-d", + "--name", + "saleor-worker", + "--network", + "saleor-net", + "--restart=unless-stopped", + "--cap-drop=ALL", + "--cap-add=CHOWN", + "--cap-add=DAC_OVERRIDE", + "--cap-add=FOWNER", + "--cap-add=SETGID", + "--cap-add=SETUID", + "--security-opt=no-new-privileges:true", + "--memory=1g", + "--pids-limit=4096", + "-v", + "/var/lib/archipelago/saleor:/app/media", + ]); + worker_cmd.args(&saleor_env); + worker_cmd.args([ + SALEOR_API_IMAGE, + "celery", + "-A", + "saleor", + "--app=saleor.celeryconf:app", + "worker", + "--loglevel=info", + "-B", + ]); + run_required_stack_command("saleor", "create worker", &mut worker_cmd).await?; + + self.set_install_phase("saleor", InstallPhase::StartingContainer) + .await; + tokio::time::sleep(std::time::Duration::from_secs(5)).await; + + let mut dashboard_cmd = tokio::process::Command::new("podman"); + dashboard_cmd.args([ + "run", + "-d", + "--name", + "saleor", + "--network", + "saleor-net", + "--restart=unless-stopped", + "--cap-drop=ALL", + "--security-opt=no-new-privileges:true", + "--memory=256m", + "--pids-limit=2048", + "-p", + "9000:80", + "-e", + &format!("API_URL={}", api_url), + "-e", + &format!("APP_MOUNT_URI={}", dashboard_url), + SALEOR_DASHBOARD_IMAGE, + ]); + run_required_stack_command("saleor", "create dashboard", &mut dashboard_cmd).await?; + + wait_for_stack_containers( + "saleor", + &[ + "saleor-db", + "saleor-cache", + "saleor-jaeger", + "saleor-mailpit", + "saleor-api", + "saleor-worker", + "saleor", + ], + 120, + ) + .await?; + + self.set_install_phase("saleor", InstallPhase::WaitingHealthy) + .await; + self.set_install_phase("saleor", InstallPhase::PostInstall) + .await; + self.set_install_phase("saleor", InstallPhase::Done).await; + self.clear_install_progress("saleor").await; + + install_log("INSTALL OK: saleor stack").await; + info!("Saleor stack installed"); + Ok(serde_json::json!({ + "success": true, + "package_id": "saleor", + "message": "Saleor stack installed (7 containers)", + })) + } } async fn read_or_generate_b64_secret(name: &str) -> String { @@ -1616,6 +2090,8 @@ async fn write_netbird_config_files(host_ip: &str) -> Result<()> { dashboardRedirectURIs: - "{public_origin}/nb-auth" - "{public_origin}/nb-silent-auth" + dashboardPostLogoutRedirectURIs: + - "{public_origin}/" cliRedirectURIs: - "http://localhost:53000/" store: @@ -1665,7 +2141,7 @@ LETSENCRYPT_DOMAIN=none proxy_read_timeout 1d; }} - location ~ ^/(api|oauth2)/ {{ + location ~ ^/(api|oauth2)(/|$) {{ proxy_pass http://netbird-server:80; }} diff --git a/core/archipelago/src/container/docker_packages.rs b/core/archipelago/src/container/docker_packages.rs index f2b09d9d..0ceab617 100644 --- a/core/archipelago/src/container/docker_packages.rs +++ b/core/archipelago/src/container/docker_packages.rs @@ -63,6 +63,12 @@ impl DockerPackageScanner { "indeedhub-build_ffmpeg-worker_1", "netbird-server", "netbird-dashboard", + "saleor-api", + "saleor-worker", + "saleor-db", + "saleor-cache", + "saleor-jaeger", + "saleor-mailpit", "buildx_buildkit_default", ]; @@ -283,6 +289,7 @@ fn get_app_tier(app_id: &str) -> &'static str { "uptime-kuma" => "recommended", "grafana" => "recommended", "searxng" => "recommended", + "saleor" => "recommended", "tailscale" | "netbird" => "recommended", "portainer" => "recommended", // Optional: everything else @@ -488,6 +495,13 @@ fn get_app_metadata(app_id: &str) -> AppMetadata { repo: "https://github.com/netbirdio/netbird".to_string(), tier: "", }, + "saleor" => AppMetadata { + title: "Saleor".to_string(), + description: "Composable commerce platform with GraphQL API and dashboard".to_string(), + icon: "/assets/img/app-icons/saleor.svg".to_string(), + repo: "https://github.com/saleor/saleor".to_string(), + tier: "", + }, "gitea" => AppMetadata { title: "Gitea".to_string(), description: "Self-hosted Git service with repository and package hosting".to_string(), @@ -716,6 +730,7 @@ fn requires_reachable_launch(app_id: &str) -> bool { | "tailscale" | "immich" | "searxng" + | "saleor" ) } diff --git a/core/archipelago/src/container/image_versions.rs b/core/archipelago/src/container/image_versions.rs index d15756a9..7fd7828a 100644 --- a/core/archipelago/src/container/image_versions.rs +++ b/core/archipelago/src/container/image_versions.rs @@ -171,6 +171,12 @@ fn image_var_for_app(app_id: &str) -> Option<&'static str> { "netbird" => Some("NETBIRD_DASHBOARD_IMAGE"), "netbird-dashboard" => Some("NETBIRD_DASHBOARD_IMAGE"), "netbird-server" => Some("NETBIRD_SERVER_IMAGE"), + "saleor" => Some("SALEOR_DASHBOARD_IMAGE"), + "saleor-api" | "saleor-worker" => Some("SALEOR_API_IMAGE"), + "saleor-db" => Some("SALEOR_POSTGRES_IMAGE"), + "saleor-cache" => Some("SALEOR_VALKEY_IMAGE"), + "saleor-jaeger" => Some("SALEOR_JAEGER_IMAGE"), + "saleor-mailpit" => Some("SALEOR_MAILPIT_IMAGE"), // Fedimint "fedimint" | "fedimintd" => Some("FEDIMINT_IMAGE"), @@ -307,6 +313,15 @@ pub fn containers_for_stack(app_id: &str) -> Vec<(&'static str, &'static str)> { ("netbird-dashboard", "NETBIRD_DASHBOARD_IMAGE"), ("netbird-server", "NETBIRD_SERVER_IMAGE"), ], + "saleor" => vec![ + ("saleor-db", "SALEOR_POSTGRES_IMAGE"), + ("saleor-cache", "SALEOR_VALKEY_IMAGE"), + ("saleor-api", "SALEOR_API_IMAGE"), + ("saleor-worker", "SALEOR_API_IMAGE"), + ("saleor-jaeger", "SALEOR_JAEGER_IMAGE"), + ("saleor-mailpit", "SALEOR_MAILPIT_IMAGE"), + ("saleor", "SALEOR_DASHBOARD_IMAGE"), + ], _ => vec![], } } diff --git a/neode-ui/public/assets/img/app-icons/saleor.svg b/neode-ui/public/assets/img/app-icons/saleor.svg new file mode 100644 index 00000000..0720b6e1 --- /dev/null +++ b/neode-ui/public/assets/img/app-icons/saleor.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/neode-ui/public/catalog.json b/neode-ui/public/catalog.json index 7d652116..56ce5fd5 100644 --- a/neode-ui/public/catalog.json +++ b/neode-ui/public/catalog.json @@ -64,6 +64,23 @@ "bitcoin-knots" ] }, + { + "id": "saleor", + "title": "Saleor", + "version": "3.23", + "description": "Composable commerce platform with GraphQL API, dashboard, worker, mail testing, and tracing.", + "icon": "/assets/img/app-icons/saleor.svg", + "author": "Saleor", + "category": "commerce", + "tier": "recommended", + "dockerImage": "ghcr.io/saleor/saleor:3.23", + "repoUrl": "https://github.com/saleor/saleor", + "containerConfig": { + "ports": ["9000: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." + } + }, { "id": "mempool", "title": "Mempool Explorer", diff --git a/neode-ui/src/views/Dashboard.vue b/neode-ui/src/views/Dashboard.vue index c30be20b..090b8c36 100644 --- a/neode-ui/src/views/Dashboard.vue +++ b/neode-ui/src/views/Dashboard.vue @@ -67,6 +67,9 @@ data-controller-zone="main" class="flex-1 overflow-hidden relative pb-0 glass-piece z-10" :class="{ 'glass-throw-main': showZoomIn }" + tabindex="-1" + @pointerenter="activateMainScroll" + @wheel.capture="activateMainScroll" >
@@ -234,6 +237,14 @@ function restoreScroll(path: string) { }) } +function activateMainScroll() { + const active = document.activeElement as HTMLElement | null + if (active?.closest?.('[data-controller-zone="sidebar"]')) { + active.blur() + document.getElementById('main-content')?.focus({ preventScroll: true }) + } +} + watch(() => route.path, (newPath) => { const isAppDetails = isDetailRoute(newPath) const wasAppDetails = showAltBackground.value diff --git a/neode-ui/src/views/appSession/appSessionConfig.ts b/neode-ui/src/views/appSession/appSessionConfig.ts index a1eae91d..cee62145 100644 --- a/neode-ui/src/views/appSession/appSessionConfig.ts +++ b/neode-ui/src/views/appSession/appSessionConfig.ts @@ -14,6 +14,7 @@ export const APP_PORTS: Record = { 'archy-electrs-ui': 50002, 'mempool-electrs': 50002, 'btcpay-server': 23000, + 'saleor': 9000, 'lnd': 18083, 'archy-lnd-ui': 18083, 'mempool': 4080, @@ -71,7 +72,7 @@ export const EXTERNAL_URLS: Record = { export const APP_TITLES: Record = { 'bitcoin-knots': 'Bitcoin Knots', 'bitcoin-core': 'Bitcoin Core', - 'btcpay-server': 'BTCPay Server', 'indeedhub': 'Indeehub', + 'btcpay-server': 'BTCPay Server', 'saleor': 'Saleor', 'indeedhub': 'Indeehub', 'botfights': 'BotFights', 'gitea': 'Gitea', '484-kitchen': '484 Kitchen', 'arch-presentation': 'Presentation', 'homeassistant': 'Home Assistant', 'uptime-kuma': 'Uptime Kuma', 'nginx-proxy-manager': 'Nginx Proxy Manager', diff --git a/neode-ui/src/views/discover/curatedApps.ts b/neode-ui/src/views/discover/curatedApps.ts index 03242e18..2a5eb83d 100644 --- a/neode-ui/src/views/discover/curatedApps.ts +++ b/neode-ui/src/views/discover/curatedApps.ts @@ -79,6 +79,7 @@ export function getCuratedAppList(): MarketplaceApp[] { { id: 'bitcoin-knots', title: 'Bitcoin Knots', version: '28.1.0', description: 'Run a full Bitcoin node. Validate and relay blocks and transactions on the Bitcoin network.', icon: '/assets/img/app-icons/bitcoin-knots.webp', author: 'Bitcoin Knots', dockerImage: `${R}/bitcoin-knots:latest`, repoUrl: 'https://github.com/bitcoinknots/bitcoin' }, { id: 'bitcoin-core', title: 'Bitcoin Core', version: '28.4', description: 'Reference implementation of the Bitcoin protocol. Run a full node validating and relaying blocks on the Bitcoin network.', icon: '/assets/img/app-icons/bitcoin-core.svg', author: 'Bitcoin Core contributors', dockerImage: 'docker.io/bitcoin/bitcoin:28.4', repoUrl: 'https://github.com/bitcoin/bitcoin' }, { id: 'btcpay-server', title: 'BTCPay Server', version: '2.3.9', description: 'Self-hosted Bitcoin payment processor. Accept Bitcoin payments without intermediaries or fees.', icon: '/assets/img/app-icons/btcpay-server.png', author: 'BTCPay Server Foundation', dockerImage: 'docker.io/btcpayserver/btcpayserver:2.3.9', repoUrl: 'https://github.com/btcpayserver/btcpayserver' }, + { id: 'saleor', title: 'Saleor', version: '3.23', category: 'commerce', description: 'Composable commerce platform with GraphQL API, dashboard, worker, mail testing, and tracing.', icon: '/assets/img/app-icons/saleor.svg', author: 'Saleor', dockerImage: 'ghcr.io/saleor/saleor:3.23', repoUrl: 'https://github.com/saleor/saleor' }, { id: 'lnd', title: 'LND', version: '0.18.4', description: 'Lightning Network Daemon. Fast and cheap Bitcoin payments through the Lightning Network.', icon: '/assets/img/app-icons/lnd.svg', author: 'Lightning Labs', dockerImage: `${R}/lnd:v0.18.4-beta`, repoUrl: 'https://github.com/lightningnetwork/lnd' }, { id: 'mempool', title: 'Mempool Explorer', version: '3.0.0', description: 'Self-hosted Bitcoin blockchain and mempool visualizer. Monitor transactions without revealing your addresses to third parties.', icon: '/assets/img/app-icons/mempool.webp', author: 'Mempool', dockerImage: `${R}/mempool-frontend:v3.0.0`, repoUrl: 'https://github.com/mempool/mempool' }, { id: 'homeassistant', title: 'Home Assistant', version: '2024.1', description: 'Open-source home automation. Control smart home devices privately, on your own hardware.', icon: '/assets/img/app-icons/homeassistant.png', author: 'Home Assistant', dockerImage: `${R}/home-assistant:2024.1`, repoUrl: 'https://github.com/home-assistant/core' }, @@ -120,6 +121,7 @@ export const INSTALLED_ALIASES: Record = { mempool: ['mempool', 'mempool-web', 'archy-mempool-web'], bitcoin: ['bitcoin-knots'], btcpay: ['btcpay-server'], + saleor: ['saleor'], immich: ['immich-server', 'immich-app', 'immich_server'], nextcloud: ['nextcloud-aio', 'nextcloud-server'], fedimint: ['fedimint-gateway'], @@ -189,7 +191,7 @@ export function categorizeCommunityApp(app: MarketplaceApp): string { const combined = `${id} ${title} ${description}` if (id.includes('bitcoin') || id.includes('btc') || id.includes('lightning') || id.includes('lnd') || id.includes('electr') || id.includes('fedimint') || id.includes('cashu') || combined.includes('wallet')) return 'money' - if (id.includes('btcpay') || id.includes('commerce') || id.includes('shop') || id.includes('pos') || combined.includes('merchant')) return 'commerce' + if (id.includes('btcpay') || id.includes('saleor') || id.includes('commerce') || id.includes('shop') || id.includes('pos') || combined.includes('merchant')) return 'commerce' if (id.includes('cloud') || id.includes('nextcloud') || id.includes('storage') || id.includes('file') || id.includes('photo') || id.includes('immich') || id.includes('jellyfin') || id.includes('media') || id.includes('vault') || combined.includes('password manager')) return 'data' if (id.includes('home-assistant') || id.includes('homeassistant') || combined.includes('home automation')) return 'home' if (id.includes('nostr') || combined.includes('nostr relay')) return 'nostr' diff --git a/neode-ui/src/views/marketplace/marketplaceData.ts b/neode-ui/src/views/marketplace/marketplaceData.ts index 1b0261ab..554c6d3f 100644 --- a/neode-ui/src/views/marketplace/marketplaceData.ts +++ b/neode-ui/src/views/marketplace/marketplaceData.ts @@ -47,6 +47,7 @@ export const INSTALLED_ALIASES: Record = { mempool: ['mempool-web', 'mempool-api', 'archy-mempool-web', 'archy-mempool-db'], bitcoin: ['bitcoin-knots'], btcpay: ['btcpay-server', 'archy-btcpay-db', 'archy-nbxplorer'], + saleor: ['saleor'], immich: ['immich-server', 'immich-app', 'immich_server', 'immich_postgres', 'immich_redis'], nextcloud: ['nextcloud-aio', 'nextcloud-server'], fedimint: ['fedimint-gateway'], @@ -67,7 +68,7 @@ export const INSTALLED_ALIASES: Record = { /** Get app tier classification (matches backend get_app_tier) */ export function getAppTier(appId: string): string { const core = ['bitcoin-knots', 'bitcoin', 'lnd', 'mempool', 'btcpay-server', 'dwn', 'filebrowser'] - const recommended = ['fedimint', 'thunderhub', 'vaultwarden', 'uptime-kuma', 'grafana', 'searxng', 'tailscale', 'netbird', 'portainer'] + const recommended = ['fedimint', 'thunderhub', 'vaultwarden', 'uptime-kuma', 'grafana', 'searxng', 'tailscale', 'netbird', 'portainer', 'saleor'] if (core.includes(appId)) return 'core' if (recommended.includes(appId)) return 'recommended' return 'optional' @@ -89,7 +90,7 @@ export function categorizeCommunityApp(app: MarketplaceApp): string { return 'money' } - if (id.includes('btcpay') || id.includes('commerce') || id.includes('shop') || + if (id.includes('btcpay') || id.includes('saleor') || id.includes('commerce') || id.includes('shop') || id.includes('store') || id.includes('pos') || id.includes('payment') || combined.includes('merchant') || combined.includes('invoice')) { return 'commerce' @@ -157,6 +158,18 @@ export function getCuratedAppList(): MarketplaceApp[] { manifestUrl: undefined, repoUrl: 'https://github.com/btcpayserver/btcpayserver' }, + { + id: 'saleor', + title: 'Saleor', + version: '3.23', + category: 'commerce', + description: 'Composable commerce platform with GraphQL API, dashboard, worker, mail testing, and tracing.', + icon: '/assets/img/app-icons/saleor.svg', + author: 'Saleor', + dockerImage: 'ghcr.io/saleor/saleor:3.23', + manifestUrl: undefined, + repoUrl: 'https://github.com/saleor/saleor' + }, { id: 'lnd', title: 'LND', diff --git a/scripts/image-versions.sh b/scripts/image-versions.sh index 2854770d..c74fc905 100644 --- a/scripts/image-versions.sh +++ b/scripts/image-versions.sh @@ -54,6 +54,14 @@ NETBIRD_PROXY_IMAGE="docker.io/library/nginx:1.27-alpine" ALPINE_TOR_IMAGE="$ARCHY_REGISTRY/alpine-tor:0.4.8.13" ADGUARDHOME_IMAGE="$ARCHY_REGISTRY/adguardhome:v0.107.55" +# Saleor stack +SALEOR_API_IMAGE="ghcr.io/saleor/saleor:3.23" +SALEOR_DASHBOARD_IMAGE="ghcr.io/saleor/saleor-dashboard:3.23" +SALEOR_POSTGRES_IMAGE="docker.io/library/postgres:15-alpine" +SALEOR_VALKEY_IMAGE="docker.io/valkey/valkey:8.1-alpine" +SALEOR_JAEGER_IMAGE="docker.io/jaegertracing/jaeger:latest" +SALEOR_MAILPIT_IMAGE="docker.io/axllent/mailpit:latest" + # Fedimint FEDIMINT_IMAGE="$ARCHY_REGISTRY/fedimintd:v0.10.0" FEDIMINT_GATEWAY_IMAGE="$ARCHY_REGISTRY/gatewayd:v0.10.0"