feat(apps): add saleor and harden netbird repair

This commit is contained in:
archipelago 2026-05-19 20:11:22 -04:00
parent 56f956973e
commit 522c046525
14 changed files with 631 additions and 21 deletions

View File

@ -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",

View File

@ -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<String> {
"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<String> {
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()],

View File

@ -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",

View File

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

View File

@ -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<serde_json::Value> {
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;
}}

View File

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

View File

@ -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![],
}
}

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" role="img" aria-label="Saleor">
<rect width="128" height="128" rx="30" fill="#111827"/>
<path d="M34 42c0-10 9-18 22-18h38v16H56c-5 0-8 2-8 5 0 4 4 5 13 7l15 3c15 3 24 11 24 24 0 15-12 25-31 25H31V88h39c8 0 13-3 13-8 0-4-4-6-12-8l-16-3C41 66 34 57 34 42Z" fill="#fff"/>
<path d="M29 103h70v8H29z" fill="#7C3AED"/>
</svg>

After

Width:  |  Height:  |  Size: 389 B

View File

@ -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",

View File

@ -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"
>
<div data-controller-main-entry class="absolute top-4 right-4 md:top-6 md:right-8 z-20">
<!-- Controller zone entry point - no switcher -->
@ -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

View File

@ -14,6 +14,7 @@ export const APP_PORTS: Record<string, number> = {
'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<string, string> = {
export const APP_TITLES: Record<string, string> = {
'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',

View File

@ -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<string, string[]> = {
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'

View File

@ -47,6 +47,7 @@ export const INSTALLED_ALIASES: Record<string, string[]> = {
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<string, string[]> = {
/** 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',

View File

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