Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
853d51ae14 | ||
|
|
a578834462 | ||
|
|
c31c3765f4 | ||
|
|
bdd5a2c43e | ||
|
|
8eb03d106e | ||
|
|
4da6e3b43c | ||
|
|
7be7420c4f | ||
|
|
34c4e87d14 | ||
|
|
e61c757633 | ||
|
|
cc1f8fba72 | ||
|
|
556f2e7cac | ||
|
|
0898c54765 | ||
|
|
f4368785f0 |
@@ -132,6 +132,16 @@ fun WebViewScreen(
|
||||
AndroidView(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
factory = { context ->
|
||||
fun openExternalUrl(url: String) {
|
||||
try {
|
||||
val intent = android.content.Intent(
|
||||
android.content.Intent.ACTION_VIEW,
|
||||
android.net.Uri.parse(url),
|
||||
)
|
||||
context.startActivity(intent)
|
||||
} catch (_: Exception) {}
|
||||
}
|
||||
|
||||
WebView(context).apply {
|
||||
layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
@@ -220,13 +230,7 @@ fun WebViewScreen(
|
||||
// Keep navigation within the Archipelago server
|
||||
if (url.startsWith(serverUrl)) return false
|
||||
// Open external URLs in the system browser
|
||||
try {
|
||||
val intent = android.content.Intent(
|
||||
android.content.Intent.ACTION_VIEW,
|
||||
android.net.Uri.parse(url),
|
||||
)
|
||||
context.startActivity(intent)
|
||||
} catch (_: Exception) {}
|
||||
openExternalUrl(url)
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -243,18 +247,30 @@ fun WebViewScreen(
|
||||
isUserGesture: Boolean,
|
||||
resultMsg: android.os.Message?,
|
||||
): Boolean {
|
||||
// Extract the URL from the hit test
|
||||
val data = view?.hitTestResult?.extra
|
||||
if (data != null) {
|
||||
try {
|
||||
val intent = android.content.Intent(
|
||||
android.content.Intent.ACTION_VIEW,
|
||||
android.net.Uri.parse(data),
|
||||
)
|
||||
context.startActivity(intent)
|
||||
} catch (_: Exception) {}
|
||||
val transport = resultMsg?.obj as? WebView.WebViewTransport
|
||||
?: return false
|
||||
|
||||
val popup = WebView(context).apply {
|
||||
settings.javaScriptEnabled = true
|
||||
webViewClient = object : WebViewClient() {
|
||||
override fun shouldOverrideUrlLoading(
|
||||
view: WebView?,
|
||||
request: WebResourceRequest?,
|
||||
): Boolean {
|
||||
val url = request?.url?.toString() ?: return true
|
||||
openExternalUrl(url)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
|
||||
if (url != null) openExternalUrl(url)
|
||||
view?.stopLoading()
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
transport.webView = popup
|
||||
resultMsg.sendToTarget()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,43 @@
|
||||
# Changelog
|
||||
|
||||
## v1.7.81-alpha (2026-05-21)
|
||||
|
||||
- Saleor storefront installs now use the prebuilt registry image instead of building the Next.js app on-device, avoiding Podman build failures during stack installation.
|
||||
- Existing Saleor stacks are repaired on adoption by recreating missing storefront containers, forcing the storefront app to bind `0.0.0.0:3000`, and resolving nginx upstreams dynamically after container restarts.
|
||||
- The shipped Saleor storefront image now includes public assets and omits Vercel-only Speed Insights injection, fixing broken static asset responses and the local `/_vercel/speed-insights/script.js` browser warning.
|
||||
- Validation passed with `cargo fmt --all --check --manifest-path core/Cargo.toml`, `cargo check -p archipelago --manifest-path core/Cargo.toml`, and live checks on `100.114.134.21` for `9011` storefront, static assets, and proxied GraphQL.
|
||||
|
||||
## v1.7.80-alpha (2026-05-21)
|
||||
|
||||
- Saleor storefront proxying now falls back to the direct request scheme when no forwarded protocol header is present, fixing direct `http://node:9011` launches that could generate an invalid same-origin GraphQL URL.
|
||||
- The Saleor storefront release path keeps public proxy support intact by still honoring forwarded HTTPS headers for Nginx Proxy Manager domains while repairing local/direct port launches.
|
||||
- Validation passed with `cargo fmt --check` and `cargo check` for the Archipelago backend before release staging.
|
||||
|
||||
## v1.7.79-alpha (2026-05-20)
|
||||
|
||||
- Saleor now installs the official Saleor Storefront as part of the stack, built from the pinned `saleor/storefront` source and served as the customer-facing shop on port `9011`.
|
||||
- Saleor app launches now open the storefront while the admin dashboard remains available on port `9010` with the generated `admin@example.com` credentials shown in Archipelago.
|
||||
- Public Nginx Proxy Manager hosts forwarding to the Saleor storefront also expose same-origin `/graphql/`, so public storefront domains can talk to the local Saleor API without mixed-content or private-LAN reachability failures.
|
||||
- Saleor stack metadata, marketplace descriptions, catalog ports, scanner exclusions, and app-session routing now describe the storefront/dashboard/API split explicitly.
|
||||
|
||||
## v1.7.78-alpha (2026-05-20)
|
||||
|
||||
- Public Nginx Proxy Manager hosts for Saleor now keep browser GraphQL calls same-origin at `/graphql/` and proxy them to the local API on `8000`, fixing `Failed to fetch` when a public domain such as `noderunner.shop` was loaded from devices that cannot reach the node's private LAN/tailnet API address.
|
||||
- Saleor's validated stack changes are now release-ready: dashboard origins on port `9010` are explicitly allowed for dashboard/API calls, preserving the working test-node install path for production nodes.
|
||||
- NetBird launches now stay pinned to the unified dashboard/proxy origin on port `8087` instead of following stale runtime-discovered server URLs on `8086`.
|
||||
- NetBird's local nginx proxy now routes browser API, OAuth, relay, and WebSocket traffic through `host.containers.internal:8086` instead of a hard-coded rootless Podman gateway IP, and includes the upstream `management.ProxyService` gRPC path.
|
||||
- The mobile credentials interstitial now keeps credential lists scrollable and action buttons reachable in both My Apps and the mobile app icon grid.
|
||||
- Android WebView popup windows now hand external popup URLs to the system browser, covering app login/signup flows that open secondary windows.
|
||||
- Validation passed with `git diff --check`, `cargo check -p archipelago`, and the focused `npm test -- src/views/appSession/__tests__/appSessionConfig.test.ts` suite.
|
||||
|
||||
## v1.7.77-alpha (2026-05-20)
|
||||
|
||||
- Saleor first-use now exposes generated credentials through Archipelago instead of leaving users at an unexplained dashboard login: App Details shows copyable `admin@example.com` credentials, and My Apps/mobile icon launches show a pre-launch credentials modal.
|
||||
- Saleor installs now create or repair the `admin@example.com` staff account idempotently after sample data loads, use the correct dashboard mount path, and re-check stack containers after startup so stopped containers are caught.
|
||||
- NetBird embedded login now uses the upstream-compatible IdP signing-key behavior and sends ID tokens from the dashboard to the management API, fixing the post-signup `Unauthenticated` state while preserving the unified local proxy/logout routes.
|
||||
- Transient unnamed Podman helper containers created during app install tasks are hidden from My Apps, so generated names like `eager_keldysh` no longer appear as user applications.
|
||||
- Validation passed with catalog/release JSON checks, `npm run type-check`, and `cargo fmt --all --check --manifest-path core/Cargo.toml`; live checks on `100.114.134.21` confirmed Saleor dashboard/API availability, generated Saleor admin login, NetBird OAuth availability, and NetBird logout redirects.
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
"id": "saleor",
|
||||
"title": "Saleor",
|
||||
"version": "3.23",
|
||||
"description": "Composable commerce platform with GraphQL API, dashboard, worker, mail testing, and tracing.",
|
||||
"description": "Composable commerce platform with customer storefront, GraphQL API, dashboard, worker, mail testing, and tracing.",
|
||||
"icon": "/assets/img/app-icons/saleor.svg",
|
||||
"author": "Saleor",
|
||||
"category": "commerce",
|
||||
@@ -76,9 +76,9 @@
|
||||
"dockerImage": "ghcr.io/saleor/saleor:3.23",
|
||||
"repoUrl": "https://github.com/saleor/saleor",
|
||||
"containerConfig": {
|
||||
"ports": ["9010:80", "8000:8000", "8025:8025", "16686:16686"],
|
||||
"ports": ["9011:80", "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 9010, 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: customer storefront on 9011, admin 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."
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
Generated
+1
-1
@@ -80,7 +80,7 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||
|
||||
[[package]]
|
||||
name = "archipelago"
|
||||
version = "1.7.76-alpha"
|
||||
version = "1.7.80-alpha"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"archipelago-container",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "archipelago"
|
||||
version = "1.7.76-alpha"
|
||||
version = "1.7.81-alpha"
|
||||
edition = "2021"
|
||||
description = "Archipelago Bitcoin Node OS - Native backend"
|
||||
authors = ["Archipelago Team"]
|
||||
|
||||
@@ -55,6 +55,7 @@ impl RpcHandler {
|
||||
"package.restart" => self.handle_package_restart(params).await,
|
||||
"package.uninstall" => self.clone().spawn_package_uninstall(params).await,
|
||||
"package.update" => self.clone().spawn_package_update(params).await,
|
||||
"package.credentials" => self.handle_package_credentials(params).await,
|
||||
"app.filebrowser-token" => self.handle_filebrowser_token().await,
|
||||
|
||||
// Bundled app management (for pre-loaded container images)
|
||||
|
||||
@@ -1859,6 +1859,42 @@ autopilot.active=false\n",
|
||||
|
||||
Ok(serde_json::json!({ "token": token }))
|
||||
}
|
||||
|
||||
pub(in crate::api::rpc) async fn handle_package_credentials(
|
||||
&self,
|
||||
params: Option<serde_json::Value>,
|
||||
) -> Result<serde_json::Value> {
|
||||
let app_id = params
|
||||
.as_ref()
|
||||
.and_then(|p| p.get("app_id"))
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or_default();
|
||||
super::validation::validate_app_id(app_id)?;
|
||||
|
||||
match app_id {
|
||||
"saleor" => {
|
||||
let password =
|
||||
tokio::fs::read_to_string("/var/lib/archipelago/secrets/saleor-admin-password")
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.to_string();
|
||||
if password.is_empty() {
|
||||
return Ok(serde_json::json!({ "credentials": [] }));
|
||||
}
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"title": "Saleor admin login",
|
||||
"description": "Saleor opens to its own dashboard login. Use this generated admin account to sign in.",
|
||||
"credentials": [
|
||||
{ "label": "Email", "value": "admin@example.com", "sensitive": false },
|
||||
{ "label": "Password", "value": password, "sensitive": true }
|
||||
]
|
||||
}))
|
||||
}
|
||||
_ => Ok(serde_json::json!({ "credentials": [] })),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn cleanup_stale_package_ports(package_id: &str) {
|
||||
|
||||
@@ -76,6 +76,10 @@ async fn adopt_stack_if_exists(
|
||||
|
||||
async fn repair_stack_before_adopt(stack_name: &str) {
|
||||
match stack_name {
|
||||
"saleor" => {
|
||||
repair_saleor_network_aliases().await;
|
||||
let _ = start_saleor_storefront_containers().await;
|
||||
}
|
||||
"btcpay" | "btcpay-server" => {
|
||||
let _ = tokio::process::Command::new("sudo")
|
||||
.args([
|
||||
@@ -99,7 +103,6 @@ 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,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -244,6 +247,8 @@ async fn repair_saleor_network_aliases() {
|
||||
("saleor-api", "api"),
|
||||
("saleor-worker", "worker"),
|
||||
("saleor", "saleor"),
|
||||
("saleor-storefront", "storefront"),
|
||||
("saleor-storefront-app", "storefront-app"),
|
||||
] {
|
||||
let exists = tokio::process::Command::new("podman")
|
||||
.args(["container", "exists", container])
|
||||
@@ -273,6 +278,53 @@ async fn repair_saleor_network_aliases() {
|
||||
}
|
||||
}
|
||||
|
||||
async fn start_saleor_storefront_containers() -> Result<()> {
|
||||
let names = podman_container_names().await?;
|
||||
|
||||
if !names.iter().any(|name| name == "saleor-storefront-app") {
|
||||
pull_image_with_retry(SALEOR_STOREFRONT_IMAGE).await?;
|
||||
let mut storefront_cmd = saleor_storefront_app_command();
|
||||
run_required_stack_command("saleor", "create storefront app", &mut storefront_cmd).await?;
|
||||
} else {
|
||||
let _ = tokio::process::Command::new("podman")
|
||||
.args(["start", "saleor-storefront-app"])
|
||||
.output()
|
||||
.await;
|
||||
}
|
||||
|
||||
write_saleor_storefront_proxy_config().await?;
|
||||
if !names.iter().any(|name| name == "saleor-storefront") {
|
||||
let mut proxy_cmd = saleor_storefront_proxy_command();
|
||||
run_required_stack_command("saleor", "create storefront proxy", &mut proxy_cmd).await?;
|
||||
} else {
|
||||
let _ = tokio::process::Command::new("podman")
|
||||
.args(["start", "saleor-storefront"])
|
||||
.output()
|
||||
.await;
|
||||
}
|
||||
|
||||
wait_for_stack_containers(
|
||||
"saleor",
|
||||
&["saleor-storefront-app", "saleor-storefront"],
|
||||
60,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn podman_container_names() -> Result<Vec<String>> {
|
||||
let output = tokio::process::Command::new("podman")
|
||||
.args(["ps", "-a", "--format", "{{.Names}}"])
|
||||
.output()
|
||||
.await
|
||||
.context("Failed to list containers")?;
|
||||
Ok(String::from_utf8_lossy(&output.stdout)
|
||||
.lines()
|
||||
.map(str::trim)
|
||||
.filter(|name| !name.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn run_required_stack_command(
|
||||
stack_name: &str,
|
||||
label: &str,
|
||||
@@ -445,6 +497,7 @@ 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_STOREFRONT_IMAGE: &str = "146.59.87.168:3000/lfg2025/saleor-storefront:6eb0b97";
|
||||
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";
|
||||
@@ -452,6 +505,14 @@ 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<()> {
|
||||
let exists = tokio::process::Command::new("podman")
|
||||
.args(["image", "exists", image])
|
||||
.status()
|
||||
.await;
|
||||
if matches!(exists, Ok(status) if status.success()) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
const MAX_ATTEMPTS: u32 = 3;
|
||||
const BACKOFF_SECS: [u64; 3] = [5, 15, 45];
|
||||
|
||||
@@ -496,6 +557,73 @@ async fn pull_image_with_retry(image: &str) -> Result<()> {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn saleor_storefront_app_command() -> tokio::process::Command {
|
||||
let mut cmd = tokio::process::Command::new("podman");
|
||||
cmd.args([
|
||||
"run",
|
||||
"-d",
|
||||
"--name",
|
||||
"saleor-storefront-app",
|
||||
"--network",
|
||||
"saleor-net",
|
||||
"--network-alias",
|
||||
"storefront-app",
|
||||
"--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=2048",
|
||||
"-e",
|
||||
"NEXT_PUBLIC_SALEOR_API_URL=http://api:8000/graphql/",
|
||||
"-e",
|
||||
"NEXT_PUBLIC_STOREFRONT_URL=http://localhost:9011",
|
||||
"-e",
|
||||
"NEXT_PUBLIC_DEFAULT_CHANNEL=default-channel",
|
||||
"-e",
|
||||
"HOSTNAME=0.0.0.0",
|
||||
"-e",
|
||||
"PORT=3000",
|
||||
SALEOR_STOREFRONT_IMAGE,
|
||||
]);
|
||||
cmd
|
||||
}
|
||||
|
||||
fn saleor_storefront_proxy_command() -> tokio::process::Command {
|
||||
let mut cmd = tokio::process::Command::new("podman");
|
||||
cmd.args([
|
||||
"run",
|
||||
"-d",
|
||||
"--name",
|
||||
"saleor-storefront",
|
||||
"--network",
|
||||
"saleor-net",
|
||||
"--network-alias",
|
||||
"storefront",
|
||||
"--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=128m",
|
||||
"--pids-limit=1024",
|
||||
"-p",
|
||||
"9011:80",
|
||||
"-v",
|
||||
"/var/lib/archipelago/saleor-storefront/nginx.conf:/etc/nginx/conf.d/default.conf:ro",
|
||||
NETBIRD_PROXY_IMAGE,
|
||||
]);
|
||||
cmd
|
||||
}
|
||||
|
||||
impl RpcHandler {
|
||||
/// Install Immich stack (postgres + redis + server).
|
||||
pub(super) async fn install_immich_stack(&self) -> Result<serde_json::Value> {
|
||||
@@ -1655,6 +1783,8 @@ impl RpcHandler {
|
||||
"saleor-api",
|
||||
"saleor-worker",
|
||||
"saleor",
|
||||
"saleor-storefront",
|
||||
"saleor-storefront-app",
|
||||
],
|
||||
)
|
||||
.await?
|
||||
@@ -1662,7 +1792,7 @@ impl RpcHandler {
|
||||
return Ok(adopted);
|
||||
}
|
||||
|
||||
install_log("INSTALL START: saleor stack (postgres + valkey + api + worker + dashboard)")
|
||||
install_log("INSTALL START: saleor stack (postgres + valkey + api + worker + dashboard + storefront)")
|
||||
.await;
|
||||
info!("Installing Saleor stack");
|
||||
|
||||
@@ -1671,6 +1801,7 @@ impl RpcHandler {
|
||||
SALEOR_VALKEY_IMAGE,
|
||||
SALEOR_API_IMAGE,
|
||||
SALEOR_DASHBOARD_IMAGE,
|
||||
SALEOR_STOREFRONT_IMAGE,
|
||||
SALEOR_JAEGER_IMAGE,
|
||||
SALEOR_MAILPIT_IMAGE,
|
||||
];
|
||||
@@ -1691,6 +1822,8 @@ impl RpcHandler {
|
||||
"saleor",
|
||||
"saleor-api",
|
||||
"saleor-worker",
|
||||
"saleor-storefront",
|
||||
"saleor-storefront-app",
|
||||
"saleor-db",
|
||||
"saleor-cache",
|
||||
"saleor-jaeger",
|
||||
@@ -1716,6 +1849,7 @@ impl RpcHandler {
|
||||
"/var/lib/archipelago/saleor",
|
||||
"/var/lib/archipelago/saleor-db",
|
||||
"/var/lib/archipelago/saleor-cache",
|
||||
"/var/lib/archipelago/saleor-storefront",
|
||||
])
|
||||
.output()
|
||||
.await;
|
||||
@@ -1724,6 +1858,7 @@ impl RpcHandler {
|
||||
"/var/lib/archipelago/saleor",
|
||||
"/var/lib/archipelago/saleor-db",
|
||||
"/var/lib/archipelago/saleor-cache",
|
||||
"/var/lib/archipelago/saleor-storefront",
|
||||
] {
|
||||
let _ = tokio::process::Command::new("sudo")
|
||||
.args(["chown", "-R", &format!("{}:{}", user, user), dir])
|
||||
@@ -1740,9 +1875,15 @@ impl RpcHandler {
|
||||
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://{}:9010/", host_ip);
|
||||
let dashboard_origin = format!("http://{}:9010", host_ip);
|
||||
let dashboard_url = format!("{}/", dashboard_origin);
|
||||
let api_url = format!("http://{}:8000/graphql/", host_ip);
|
||||
let storefront_origin = format!("http://{}:9011", host_ip);
|
||||
let allowed_hosts = format!("localhost,127.0.0.1,api,saleor-api,{}", host_ip);
|
||||
let allowed_client_hosts = format!(
|
||||
"{},{},http://localhost:9010,http://127.0.0.1:9010,http://localhost:9011,http://127.0.0.1:9011",
|
||||
dashboard_origin, storefront_origin
|
||||
);
|
||||
let database_url = format!("postgres://saleor:{}@db/saleor", db_pass);
|
||||
|
||||
let mut db_cmd = tokio::process::Command::new("podman");
|
||||
@@ -1884,6 +2025,10 @@ impl RpcHandler {
|
||||
"-e".to_string(),
|
||||
format!("DASHBOARD_URL={}", dashboard_url),
|
||||
"-e".to_string(),
|
||||
format!("ALLOWED_CLIENT_HOSTS={}", allowed_client_hosts),
|
||||
"-e".to_string(),
|
||||
format!("ALLOWED_GRAPHQL_ORIGINS={}", allowed_client_hosts),
|
||||
"-e".to_string(),
|
||||
format!("ALLOWED_HOSTS={}", allowed_hosts),
|
||||
];
|
||||
|
||||
@@ -1929,6 +2074,17 @@ impl RpcHandler {
|
||||
}
|
||||
}
|
||||
|
||||
let admin_script = format!(
|
||||
r#"from django.contrib.auth import get_user_model
|
||||
User = get_user_model()
|
||||
user, _ = User.objects.get_or_create(email="admin@example.com", defaults={{"is_staff": True, "is_superuser": True}})
|
||||
user.is_staff = True
|
||||
user.is_superuser = True
|
||||
user.set_password({:?})
|
||||
user.save()
|
||||
"#,
|
||||
admin_pass
|
||||
);
|
||||
let mut admin_cmd = tokio::process::Command::new("podman");
|
||||
admin_cmd.args([
|
||||
"run",
|
||||
@@ -1940,17 +2096,14 @@ impl RpcHandler {
|
||||
]);
|
||||
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",
|
||||
"shell",
|
||||
"-c",
|
||||
&admin_script,
|
||||
]);
|
||||
run_required_stack_command("saleor", "create admin user", &mut admin_cmd).await?;
|
||||
run_required_stack_command("saleor", "create or update 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");
|
||||
@@ -2044,11 +2197,23 @@ impl RpcHandler {
|
||||
"-e",
|
||||
&format!("API_URL={}", api_url),
|
||||
"-e",
|
||||
&format!("APP_MOUNT_URI={}", dashboard_url),
|
||||
"APP_MOUNT_URI=/",
|
||||
SALEOR_DASHBOARD_IMAGE,
|
||||
]);
|
||||
run_required_stack_command("saleor", "create dashboard", &mut dashboard_cmd).await?;
|
||||
|
||||
let mut storefront_cmd = saleor_storefront_app_command();
|
||||
run_required_stack_command("saleor", "create storefront app", &mut storefront_cmd).await?;
|
||||
|
||||
write_saleor_storefront_proxy_config().await?;
|
||||
let mut storefront_proxy_cmd = saleor_storefront_proxy_command();
|
||||
run_required_stack_command(
|
||||
"saleor",
|
||||
"create storefront proxy",
|
||||
&mut storefront_proxy_cmd,
|
||||
)
|
||||
.await?;
|
||||
|
||||
wait_for_stack_containers(
|
||||
"saleor",
|
||||
&[
|
||||
@@ -2059,10 +2224,29 @@ impl RpcHandler {
|
||||
"saleor-api",
|
||||
"saleor-worker",
|
||||
"saleor",
|
||||
"saleor-storefront",
|
||||
"saleor-storefront-app",
|
||||
],
|
||||
120,
|
||||
)
|
||||
.await?;
|
||||
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
|
||||
wait_for_stack_containers(
|
||||
"saleor",
|
||||
&[
|
||||
"saleor-db",
|
||||
"saleor-cache",
|
||||
"saleor-jaeger",
|
||||
"saleor-mailpit",
|
||||
"saleor-api",
|
||||
"saleor-worker",
|
||||
"saleor",
|
||||
"saleor-storefront",
|
||||
"saleor-storefront-app",
|
||||
],
|
||||
30,
|
||||
)
|
||||
.await?;
|
||||
|
||||
self.set_install_phase("saleor", InstallPhase::WaitingHealthy)
|
||||
.await;
|
||||
@@ -2076,7 +2260,7 @@ impl RpcHandler {
|
||||
Ok(serde_json::json!({
|
||||
"success": true,
|
||||
"package_id": "saleor",
|
||||
"message": "Saleor stack installed (7 containers)",
|
||||
"message": "Saleor stack installed (9 containers)",
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -2097,6 +2281,56 @@ async fn read_or_generate_b64_secret(name: &str) -> String {
|
||||
secret
|
||||
}
|
||||
|
||||
async fn write_saleor_storefront_proxy_config() -> Result<()> {
|
||||
tokio::fs::create_dir_all("/var/lib/archipelago/saleor-storefront")
|
||||
.await
|
||||
.context("Failed to create Saleor storefront config directory")?;
|
||||
|
||||
let nginx_conf = r#"map $http_x_forwarded_proto $saleor_storefront_proto {
|
||||
default $http_x_forwarded_proto;
|
||||
"" $scheme;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
resolver 10.89.4.1 valid=10s ipv6=off;
|
||||
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
location ^~ /graphql/ {
|
||||
set $saleor_api http://api:8000/graphql/;
|
||||
proxy_pass $saleor_api;
|
||||
proxy_set_header Host api;
|
||||
proxy_set_header Origin "";
|
||||
}
|
||||
|
||||
location / {
|
||||
set $saleor_storefront_app http://storefront-app:3000;
|
||||
proxy_pass $saleor_storefront_app;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Accept-Encoding "";
|
||||
sub_filter_once off;
|
||||
sub_filter_types text/html application/javascript text/javascript;
|
||||
sub_filter 'http://api:8000/graphql/' '$saleor_storefront_proto://$host/graphql/';
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
tokio::fs::write(
|
||||
"/var/lib/archipelago/saleor-storefront/nginx.conf",
|
||||
nginx_conf,
|
||||
)
|
||||
.await
|
||||
.context("Failed to write Saleor storefront nginx.conf")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn write_netbird_config_files(host_ip: &str) -> Result<()> {
|
||||
let public_origin = format!("http://{}:8087", host_ip);
|
||||
let server_origin = format!("http://{}:8086", host_ip);
|
||||
@@ -2117,7 +2351,7 @@ async fn write_netbird_config_files(host_ip: &str) -> Result<()> {
|
||||
auth:
|
||||
issuer: "{public_origin}/oauth2"
|
||||
localAuthDisabled: false
|
||||
signKeyRefreshEnabled: true
|
||||
signKeyRefreshEnabled: false
|
||||
dashboardRedirectURIs:
|
||||
- "{public_origin}/nb-auth"
|
||||
- "{public_origin}/nb-silent-auth"
|
||||
@@ -2145,7 +2379,7 @@ USE_AUTH0=false
|
||||
AUTH_SUPPORTED_SCOPES=openid profile email groups
|
||||
AUTH_REDIRECT_URI=/nb-auth
|
||||
AUTH_SILENT_REDIRECT_URI=/nb-silent-auth
|
||||
NETBIRD_TOKEN_SOURCE=accessToken
|
||||
NETBIRD_TOKEN_SOURCE=idToken
|
||||
NGINX_SSL_PORT=443
|
||||
LETSENCRYPT_DOMAIN=none
|
||||
"#
|
||||
@@ -2159,10 +2393,9 @@ 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;
|
||||
# Route browser 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.
|
||||
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
@@ -2171,17 +2404,17 @@ LETSENCRYPT_DOMAIN=none
|
||||
proxy_http_version 1.1;
|
||||
|
||||
location ~ ^/(relay|ws-proxy/) {{
|
||||
proxy_pass $netbird_server;
|
||||
proxy_pass http://host.containers.internal:8086;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_read_timeout 1d;
|
||||
}}
|
||||
|
||||
location ~ ^/(api|oauth2)(/|$) {{
|
||||
proxy_pass $netbird_server;
|
||||
proxy_pass http://host.containers.internal:8086;
|
||||
}}
|
||||
|
||||
location ~ ^/(signalexchange\.SignalExchange|management\.ManagementService)/ {{
|
||||
location ~ ^/(signalexchange\.SignalExchange|management\.ManagementService|management\.ProxyService)/ {{
|
||||
grpc_pass grpc://netbird-server:80;
|
||||
grpc_read_timeout 1d;
|
||||
grpc_send_timeout 1d;
|
||||
|
||||
@@ -69,6 +69,8 @@ impl DockerPackageScanner {
|
||||
"saleor-cache",
|
||||
"saleor-jaeger",
|
||||
"saleor-mailpit",
|
||||
"saleor-storefront",
|
||||
"saleor-storefront-app",
|
||||
"buildx_buildkit_default",
|
||||
];
|
||||
|
||||
@@ -125,6 +127,11 @@ impl DockerPackageScanner {
|
||||
continue;
|
||||
}
|
||||
|
||||
if is_transient_podman_helper(&app_id, &container.ports) {
|
||||
debug!("Skipping transient Podman helper container: {}", app_id);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip podman-compose infrastructure containers (e.g. indeedhub-build_api_1)
|
||||
// These have the project prefix pattern: {project}_{service}_{instance}
|
||||
if app_id.starts_with("indeedhub-build_") {
|
||||
@@ -299,6 +306,21 @@ fn get_app_tier(app_id: &str) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
fn is_transient_podman_helper(app_id: &str, ports: &[String]) -> bool {
|
||||
if !ports.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let Some((left, right)) = app_id.split_once('_') else {
|
||||
return false;
|
||||
};
|
||||
|
||||
!left.is_empty()
|
||||
&& !right.is_empty()
|
||||
&& left.chars().all(|c| c.is_ascii_lowercase())
|
||||
&& right.chars().all(|c| c.is_ascii_lowercase())
|
||||
}
|
||||
|
||||
fn get_app_metadata(app_id: &str) -> AppMetadata {
|
||||
let mut meta = match app_id {
|
||||
"bitcoin-core" => AppMetadata {
|
||||
@@ -499,7 +521,7 @@ fn get_app_metadata(app_id: &str) -> AppMetadata {
|
||||
},
|
||||
"saleor" => AppMetadata {
|
||||
title: "Saleor".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(),
|
||||
description: "Composable commerce platform with storefront, dashboard, and GraphQL API. The customer storefront opens on port 9011; admin dashboard is on 9010 with admin@example.com credentials stored on the node.".to_string(),
|
||||
icon: "/assets/img/app-icons/saleor.svg".to_string(),
|
||||
repo: "https://github.com/saleor/saleor".to_string(),
|
||||
tier: "",
|
||||
|
||||
@@ -129,7 +129,7 @@ impl PodmanClient {
|
||||
"filebrowser" => "http://localhost:8083",
|
||||
"nginx-proxy-manager" => "http://localhost:8081",
|
||||
"portainer" => "http://localhost:9000",
|
||||
"saleor" => "http://localhost:9010",
|
||||
"saleor" => "http://localhost:9011",
|
||||
"uptime-kuma" => "http://localhost:3002",
|
||||
"fedimint" | "fedimintd" => "http://localhost:8175",
|
||||
"fedimint-gateway" => "http://localhost:8176",
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "neode-ui",
|
||||
"version": "1.7.76-alpha",
|
||||
"version": "1.7.81-alpha",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "neode-ui",
|
||||
"version": "1.7.76-alpha",
|
||||
"version": "1.7.81-alpha",
|
||||
"dependencies": {
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@vue-leaflet/vue-leaflet": "^0.10.1",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "neode-ui",
|
||||
"private": true,
|
||||
"version": "1.7.76-alpha",
|
||||
"version": "1.7.81-alpha",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "./start-dev.sh",
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
"id": "saleor",
|
||||
"title": "Saleor",
|
||||
"version": "3.23",
|
||||
"description": "Composable commerce platform with GraphQL API, dashboard, worker, mail testing, and tracing.",
|
||||
"description": "Composable commerce platform with customer storefront, GraphQL API, dashboard, worker, mail testing, and tracing.",
|
||||
"icon": "/assets/img/app-icons/saleor.svg",
|
||||
"author": "Saleor",
|
||||
"category": "commerce",
|
||||
@@ -76,9 +76,9 @@
|
||||
"dockerImage": "ghcr.io/saleor/saleor:3.23",
|
||||
"repoUrl": "https://github.com/saleor/saleor",
|
||||
"containerConfig": {
|
||||
"ports": ["9010:80", "8000:8000", "8025:8025", "16686:16686"],
|
||||
"ports": ["9011:80", "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 9010, 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: customer storefront on 9011, admin 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."
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -103,6 +103,7 @@ const PORT_TO_APP_ID: Record<string, string> = {
|
||||
'8888': 'searxng',
|
||||
'9000': 'portainer',
|
||||
'9010': 'saleor',
|
||||
'9011': 'saleor',
|
||||
'8087': 'netbird',
|
||||
'8086': 'netbird',
|
||||
'9980': 'onlyoffice',
|
||||
|
||||
@@ -140,6 +140,18 @@ export interface InterfaceAddress {
|
||||
'lan-address': string | null
|
||||
}
|
||||
|
||||
export interface AppCredential {
|
||||
label: string
|
||||
value: string
|
||||
sensitive?: boolean
|
||||
}
|
||||
|
||||
export interface AppCredentialsResponse {
|
||||
title?: string
|
||||
description?: string
|
||||
credentials: AppCredential[]
|
||||
}
|
||||
|
||||
export const ServiceStatus = {
|
||||
Stopped: 'stopped',
|
||||
Starting: 'starting',
|
||||
@@ -275,4 +287,3 @@ export interface Update {
|
||||
sequence: number
|
||||
patch: PatchOperation[]
|
||||
}
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
:lan-url="lanUrl"
|
||||
:tor-url="torUrl"
|
||||
:show-tor-address="showTorAddress"
|
||||
:credentials="credentials"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -137,6 +138,7 @@ import { useAppLauncherStore } from '../stores/appLauncher'
|
||||
import { useModalKeyboard } from '@/composables/useModalKeyboard'
|
||||
import { dummyApps } from '../utils/dummyApps'
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
import type { AppCredentialsResponse } from '@/types/api'
|
||||
import AppHeroSection from './appDetails/AppHeroSection.vue'
|
||||
import AppContentSection from './appDetails/AppContentSection.vue'
|
||||
import AppSidebar from './appDetails/AppSidebar.vue'
|
||||
@@ -214,6 +216,7 @@ const needsBitcoinSync = computed(() => BITCOIN_DEPENDENT_APPS.includes(packageK
|
||||
const bitcoinSyncPercent = ref(0)
|
||||
const bitcoinBlockHeight = ref(0)
|
||||
const bitcoinSynced = computed(() => bitcoinSyncPercent.value >= 99.9)
|
||||
const credentials = ref<AppCredentialsResponse | null>(null)
|
||||
|
||||
async function loadBitcoinSync() {
|
||||
if (!needsBitcoinSync.value) return
|
||||
@@ -230,8 +233,23 @@ async function loadBitcoinSync() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCredentials() {
|
||||
if (!appId.value) return
|
||||
try {
|
||||
const result = await rpcClient.call<AppCredentialsResponse>({
|
||||
method: 'package.credentials',
|
||||
params: { app_id: packageKey.value },
|
||||
timeout: 5000,
|
||||
})
|
||||
credentials.value = result.credentials?.length ? result : null
|
||||
} catch {
|
||||
credentials.value = null
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadBitcoinSync()
|
||||
loadCredentials()
|
||||
})
|
||||
|
||||
const actionError = ref('')
|
||||
|
||||
+105
-2
@@ -173,6 +173,37 @@
|
||||
@confirm="onConfirmUninstall"
|
||||
/>
|
||||
|
||||
<Transition name="fade">
|
||||
<div
|
||||
v-if="credentialModal.show"
|
||||
class="fixed inset-0 z-[2700] flex items-end justify-center bg-black/60 backdrop-blur-md p-0 md:items-center md:p-6"
|
||||
@click.self="closeCredentialModal"
|
||||
>
|
||||
<div class="sideload-modal credential-modal">
|
||||
<div class="flex items-start justify-between gap-4 mb-5">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-white">{{ credentialModal.title }}</h2>
|
||||
<p class="text-sm text-white/55 mt-1">{{ credentialModal.description }}</p>
|
||||
</div>
|
||||
<button type="button" class="sideload-close-btn" aria-label="Close" @click="closeCredentialModal">×</button>
|
||||
</div>
|
||||
<div class="credential-modal-body space-y-3">
|
||||
<div v-for="cred in credentialModal.credentials" :key="cred.label" class="rounded-lg border border-white/10 bg-white/[0.04] p-3">
|
||||
<div class="flex items-center justify-between gap-3 mb-1">
|
||||
<span class="text-white/60 text-xs uppercase tracking-wide">{{ cred.label }}</span>
|
||||
<button type="button" class="text-xs text-blue-300 hover:text-blue-200" @click="copyModalCredential(cred.label, cred.value)">{{ credentialModal.copied === cred.label ? 'Copied' : 'Copy' }}</button>
|
||||
</div>
|
||||
<p class="font-mono text-sm text-white break-all">{{ cred.value }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="credential-modal-actions mt-5 flex flex-col sm:flex-row gap-3">
|
||||
<button type="button" class="w-full sm:flex-1 glass-button px-4 py-3 rounded-lg" @click="closeCredentialModal">Cancel</button>
|
||||
<button type="button" class="w-full sm:flex-1 glass-button px-4 py-3 rounded-lg font-semibold" @click="continueCredentialLaunch">Continue to app</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<Transition name="fade">
|
||||
<div
|
||||
v-if="showSideload"
|
||||
@@ -255,7 +286,7 @@ import { useAppStore } from '@/stores/app'
|
||||
import { useServerStore } from '@/stores/server'
|
||||
import { useAppLauncherStore } from '@/stores/appLauncher'
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
import { type PackageDataEntry, type PackageState } from '@/types/api'
|
||||
import { type AppCredential, type AppCredentialsResponse, type PackageDataEntry, type PackageState } from '@/types/api'
|
||||
import AppCard from './apps/AppCard.vue'
|
||||
import AppIconGrid from './apps/AppIconGrid.vue'
|
||||
import AppsUninstallModal from './apps/AppsUninstallModal.vue'
|
||||
@@ -284,6 +315,14 @@ const sideloadForm = ref({
|
||||
port: '',
|
||||
description: '',
|
||||
})
|
||||
const credentialModal = ref({
|
||||
show: false,
|
||||
appId: '',
|
||||
title: '',
|
||||
description: '',
|
||||
credentials: [] as AppCredential[],
|
||||
copied: '',
|
||||
})
|
||||
|
||||
// Only stagger-animate on first mount
|
||||
const showStagger = !appsAnimationDone
|
||||
@@ -431,7 +470,13 @@ function goToApp(id: string) {
|
||||
router.push(`/dashboard/apps/${id}`).catch(() => {})
|
||||
}
|
||||
|
||||
function launchApp(id: string) {
|
||||
async function launchApp(id: string) {
|
||||
const shown = await maybeShowCredentialsBeforeLaunch(id)
|
||||
if (shown) return
|
||||
launchAppNow(id)
|
||||
}
|
||||
|
||||
function launchAppNow(id: string) {
|
||||
const pkg = packages.value[id]
|
||||
const isMobile = typeof window !== 'undefined' && window.innerWidth < 768
|
||||
const webOnlyUrl = WEB_ONLY_APP_URLS[id]
|
||||
@@ -456,6 +501,52 @@ function launchApp(id: string) {
|
||||
useAppLauncherStore().openSession(id)
|
||||
}
|
||||
|
||||
async function maybeShowCredentialsBeforeLaunch(id: string): Promise<boolean> {
|
||||
try {
|
||||
const result = await rpcClient.call<AppCredentialsResponse>({
|
||||
method: 'package.credentials',
|
||||
params: { app_id: id },
|
||||
timeout: 5000,
|
||||
})
|
||||
if (!result.credentials?.length) return false
|
||||
credentialModal.value = {
|
||||
show: true,
|
||||
appId: id,
|
||||
title: result.title || `${packages.value[id]?.manifest.title || id} credentials`,
|
||||
description: result.description || 'Use these credentials when the app asks you to sign in.',
|
||||
credentials: result.credentials,
|
||||
copied: '',
|
||||
}
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function closeCredentialModal() {
|
||||
credentialModal.value.show = false
|
||||
}
|
||||
|
||||
function continueCredentialLaunch() {
|
||||
const id = credentialModal.value.appId
|
||||
closeCredentialModal()
|
||||
if (id) launchAppNow(id)
|
||||
}
|
||||
|
||||
async function copyModalCredential(label: string, value: string) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(value)
|
||||
} catch {
|
||||
const textarea = document.createElement('textarea')
|
||||
textarea.value = value
|
||||
document.body.appendChild(textarea)
|
||||
textarea.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(textarea)
|
||||
}
|
||||
credentialModal.value.copied = label
|
||||
}
|
||||
|
||||
async function updateApp(id: string) {
|
||||
try {
|
||||
await serverStore.updatePackage(id)
|
||||
@@ -599,6 +690,18 @@ async function submitSideload() {
|
||||
}
|
||||
.sideload-input::placeholder { color: rgba(255, 255, 255, 0.38); }
|
||||
.sideload-input:focus { border-color: rgba(255, 255, 255, 0.38); }
|
||||
.credential-modal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.credential-modal-body {
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
.credential-modal-actions {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.sideload-modal {
|
||||
border-radius: 1.25rem;
|
||||
|
||||
@@ -86,6 +86,22 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="credentials?.credentials?.length" class="glass-card p-6">
|
||||
<h3 class="text-lg font-bold text-white mb-2">Credentials</h3>
|
||||
<p v-if="credentials.description" class="text-sm text-white/60 mb-4">{{ credentials.description }}</p>
|
||||
<div class="space-y-3">
|
||||
<div v-for="cred in credentials.credentials" :key="cred.label" class="rounded-lg border border-white/10 bg-white/[0.04] p-3">
|
||||
<div class="flex items-center justify-between gap-3 mb-1">
|
||||
<span class="text-white/60 text-xs uppercase tracking-wide">{{ cred.label }}</span>
|
||||
<button type="button" class="text-xs text-blue-300 hover:text-blue-200" @click="copyCredential(cred.label, cred.value)">
|
||||
{{ copiedCredential === cred.label ? 'Copied' : 'Copy' }}
|
||||
</button>
|
||||
</div>
|
||||
<p class="font-mono text-sm text-white break-all">{{ cred.value }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Requirements Card (hidden for web-only apps) -->
|
||||
<div v-if="!isWebOnly" class="glass-card p-6">
|
||||
<h3 class="text-lg font-bold text-white mb-4">{{ t('appDetails.requirements') }}</h3>
|
||||
@@ -151,9 +167,29 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { AppCredentialsResponse } from '@/types/api'
|
||||
|
||||
const { t } = useI18n()
|
||||
const copiedCredential = ref('')
|
||||
|
||||
async function copyCredential(label: string, value: string) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(value)
|
||||
} catch {
|
||||
const textarea = document.createElement('textarea')
|
||||
textarea.value = value
|
||||
document.body.appendChild(textarea)
|
||||
textarea.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(textarea)
|
||||
}
|
||||
copiedCredential.value = label
|
||||
setTimeout(() => {
|
||||
if (copiedCredential.value === label) copiedCredential.value = ''
|
||||
}, 1800)
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
pkg: Record<string, any>
|
||||
@@ -164,5 +200,6 @@ defineProps<{
|
||||
lanUrl: string
|
||||
torUrl: string
|
||||
showTorAddress: boolean
|
||||
credentials: AppCredentialsResponse | null
|
||||
}>()
|
||||
</script>
|
||||
|
||||
@@ -17,5 +17,16 @@ describe('appSessionConfig', () => {
|
||||
|
||||
expect(resolveAppUrl('mempool')).toBe('http://192.168.1.228:4080')
|
||||
expect(resolveAppUrl('indeedhub')).toBe('http://192.168.1.228:7778')
|
||||
expect(resolveAppUrl('saleor')).toBe('http://192.168.1.228:9011')
|
||||
})
|
||||
|
||||
it('keeps NetBird on the unified dashboard proxy port', () => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { hostname: '192.168.1.228' },
|
||||
writable: true,
|
||||
configurable: true,
|
||||
})
|
||||
|
||||
expect(resolveAppUrl('netbird', undefined, 'http://localhost:8086')).toBe('http://192.168.1.228:8087')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -14,7 +14,7 @@ export const APP_PORTS: Record<string, number> = {
|
||||
'archy-electrs-ui': 50002,
|
||||
'mempool-electrs': 50002,
|
||||
'btcpay-server': 23000,
|
||||
'saleor': 9010,
|
||||
'saleor': 9011,
|
||||
'lnd': 18083,
|
||||
'archy-lnd-ui': 18083,
|
||||
'mempool': 4080,
|
||||
@@ -100,9 +100,7 @@ export const NEW_TAB_APPS = new Set([
|
||||
export const IFRAME_BLOCKED_APPS = new Set<string>([])
|
||||
|
||||
/** Resolve app URL using direct port mapping (source of truth) */
|
||||
export function resolveAppUrl(id: string, routeQueryPath?: string, runtimeUrl?: string): string {
|
||||
if (id === 'netbird' && runtimeUrl) return runtimeUrl
|
||||
|
||||
export function resolveAppUrl(id: string, routeQueryPath?: string, _runtimeUrl?: string): string {
|
||||
// External HTTPS apps
|
||||
const ext = EXTERNAL_URLS[id]
|
||||
if (ext) return ext
|
||||
|
||||
@@ -70,6 +70,33 @@
|
||||
@click="scrollToPage(i)"
|
||||
></button>
|
||||
</div>
|
||||
|
||||
<Transition name="fade">
|
||||
<div v-if="credentialModal.show" class="fixed inset-0 z-[2700] flex items-end justify-center bg-black/60 backdrop-blur-md p-0 md:items-center md:p-6" @click.self="closeCredentialModal">
|
||||
<div class="sideload-modal credential-modal">
|
||||
<div class="flex items-start justify-between gap-4 mb-5">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-white">{{ credentialModal.title }}</h2>
|
||||
<p class="text-sm text-white/55 mt-1">{{ credentialModal.description }}</p>
|
||||
</div>
|
||||
<button type="button" class="sideload-close-btn" aria-label="Close" @click="closeCredentialModal">×</button>
|
||||
</div>
|
||||
<div class="credential-modal-body space-y-3">
|
||||
<div v-for="cred in credentialModal.credentials" :key="cred.label" class="rounded-lg border border-white/10 bg-white/[0.04] p-3">
|
||||
<div class="flex items-center justify-between gap-3 mb-1">
|
||||
<span class="text-white/60 text-xs uppercase tracking-wide">{{ cred.label }}</span>
|
||||
<button type="button" class="text-xs text-blue-300 hover:text-blue-200" @click="copyModalCredential(cred.label, cred.value)">{{ credentialModal.copied === cred.label ? 'Copied' : 'Copy' }}</button>
|
||||
</div>
|
||||
<p class="font-mono text-sm text-white break-all">{{ cred.value }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="credential-modal-actions mt-5 flex flex-col sm:flex-row gap-3">
|
||||
<button type="button" class="w-full sm:flex-1 glass-button px-4 py-3 rounded-lg" @click="closeCredentialModal">Cancel</button>
|
||||
<button type="button" class="w-full sm:flex-1 glass-button px-4 py-3 rounded-lg font-semibold" @click="continueCredentialLaunch">Continue to app</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -77,7 +104,8 @@
|
||||
import { computed, ref } from 'vue'
|
||||
import { useServerStore } from '@/stores/server'
|
||||
import { useAppLauncherStore } from '@/stores/appLauncher'
|
||||
import type { PackageDataEntry } from '@/types/api'
|
||||
import type { AppCredential, AppCredentialsResponse, PackageDataEntry } from '@/types/api'
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
import { resolveAppUrl } from '@/views/appSession/appSessionConfig'
|
||||
import { canLaunch, handleImageError, isWebsitePackage, opensInTab, resolveAppIcon, resolveRuntimeLaunchUrl, WEB_ONLY_APP_URLS } from './appsConfig'
|
||||
import { getCuratedAppList } from '../discover/curatedApps'
|
||||
@@ -88,6 +116,14 @@ const serverStore = useServerStore()
|
||||
const appLauncher = useAppLauncherStore()
|
||||
|
||||
const curatedMap = new Map(getCuratedAppList().map(a => [a.id, a]))
|
||||
const credentialModal = ref({
|
||||
show: false,
|
||||
appId: '',
|
||||
title: '',
|
||||
description: '',
|
||||
credentials: [] as AppCredential[],
|
||||
copied: '',
|
||||
})
|
||||
|
||||
const props = defineProps<{
|
||||
apps: [string, PackageDataEntry][]
|
||||
@@ -118,34 +154,79 @@ function getIcon(id: string, pkg: PackageDataEntry): string {
|
||||
return resolveAppIcon(id, pkg, curatedMap.get(id)?.icon)
|
||||
}
|
||||
|
||||
function handleTap(id: string, pkg: PackageDataEntry) {
|
||||
async function handleTap(id: string, pkg: PackageDataEntry) {
|
||||
if (canLaunch(pkg)) {
|
||||
const isMobile = typeof window !== 'undefined' && window.innerWidth < 768
|
||||
const webOnlyUrl = WEB_ONLY_APP_URLS[id]
|
||||
if (webOnlyUrl) {
|
||||
appLauncher.open({ url: webOnlyUrl, title: getTitle(id, pkg), openInNewTab: !isMobile })
|
||||
return
|
||||
}
|
||||
if (isWebsitePackage(id, pkg)) {
|
||||
const url = resolveRuntimeLaunchUrl(pkg)
|
||||
if (url) {
|
||||
appLauncher.open({ url, title: getTitle(id, pkg), openInNewTab: !isMobile })
|
||||
return
|
||||
}
|
||||
}
|
||||
if (!isMobile && opensInTab(id)) {
|
||||
const appUrl = resolveRuntimeLaunchUrl(pkg) || resolveAppUrl(id)
|
||||
if (appUrl) {
|
||||
window.open(appUrl, '_blank', 'noopener,noreferrer')
|
||||
return
|
||||
}
|
||||
}
|
||||
appLauncher.openSession(id)
|
||||
const shown = await maybeShowCredentialsBeforeLaunch(id, pkg)
|
||||
if (shown) return
|
||||
launchNow(id, pkg)
|
||||
} else {
|
||||
emit('goToApp', id)
|
||||
}
|
||||
}
|
||||
|
||||
function launchNow(id: string, pkg: PackageDataEntry) {
|
||||
const isMobile = typeof window !== 'undefined' && window.innerWidth < 768
|
||||
const webOnlyUrl = WEB_ONLY_APP_URLS[id]
|
||||
if (webOnlyUrl) {
|
||||
appLauncher.open({ url: webOnlyUrl, title: getTitle(id, pkg), openInNewTab: !isMobile })
|
||||
return
|
||||
}
|
||||
if (isWebsitePackage(id, pkg)) {
|
||||
const url = resolveRuntimeLaunchUrl(pkg)
|
||||
if (url) {
|
||||
appLauncher.open({ url, title: getTitle(id, pkg), openInNewTab: !isMobile })
|
||||
return
|
||||
}
|
||||
}
|
||||
if (!isMobile && opensInTab(id)) {
|
||||
const appUrl = resolveRuntimeLaunchUrl(pkg) || resolveAppUrl(id)
|
||||
if (appUrl) {
|
||||
window.open(appUrl, '_blank', 'noopener,noreferrer')
|
||||
return
|
||||
}
|
||||
}
|
||||
appLauncher.openSession(id)
|
||||
}
|
||||
|
||||
async function maybeShowCredentialsBeforeLaunch(id: string, pkg: PackageDataEntry): Promise<boolean> {
|
||||
try {
|
||||
const result = await rpcClient.call<AppCredentialsResponse>({ method: 'package.credentials', params: { app_id: id }, timeout: 5000 })
|
||||
if (!result.credentials?.length) return false
|
||||
credentialModal.value = {
|
||||
show: true,
|
||||
appId: id,
|
||||
title: result.title || `${getTitle(id, pkg)} credentials`,
|
||||
description: result.description || 'Use these credentials when the app asks you to sign in.',
|
||||
credentials: result.credentials,
|
||||
copied: '',
|
||||
}
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function closeCredentialModal() { credentialModal.value.show = false }
|
||||
|
||||
function continueCredentialLaunch() {
|
||||
const id = credentialModal.value.appId
|
||||
const entry = props.apps.find(([appId]) => appId === id)
|
||||
closeCredentialModal()
|
||||
if (entry) launchNow(entry[0], entry[1])
|
||||
}
|
||||
|
||||
async function copyModalCredential(label: string, value: string) {
|
||||
try { await navigator.clipboard.writeText(value) } catch {
|
||||
const textarea = document.createElement('textarea')
|
||||
textarea.value = value
|
||||
document.body.appendChild(textarea)
|
||||
textarea.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(textarea)
|
||||
}
|
||||
credentialModal.value.copied = label
|
||||
}
|
||||
|
||||
function onScroll() {
|
||||
const el = scrollContainer.value
|
||||
if (!el) return
|
||||
@@ -160,3 +241,43 @@ function scrollToPage(index: number) {
|
||||
el.scrollTo({ left: index * el.clientWidth, behavior: 'smooth' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.sideload-modal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
max-width: 34rem;
|
||||
max-height: calc(100dvh - var(--safe-area-top, env(safe-area-inset-top, 0px)) - 12px);
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(255, 255, 255, 0.14);
|
||||
border-radius: 1.5rem 1.5rem 0 0;
|
||||
background: rgba(8, 10, 18, 0.94);
|
||||
padding: 1.25rem;
|
||||
padding-bottom: calc(1.25rem + var(--safe-area-bottom, env(safe-area-inset-bottom, 0px)));
|
||||
box-shadow: 0 -24px 70px rgba(0, 0, 0, 0.55);
|
||||
}
|
||||
.sideload-close-btn {
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
flex-shrink: 0;
|
||||
border-radius: 0.75rem;
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
.credential-modal-body {
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
.credential-modal-actions {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.sideload-modal {
|
||||
border-radius: 1.25rem;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 25px 80px rgba(0, 0, 0, 0.55);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -79,7 +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: 'saleor', title: 'Saleor', version: '3.23', category: 'commerce', description: 'Composable commerce platform with customer storefront, GraphQL API, dashboard, worker, mail testing, and tracing. Storefront opens on port 9011; admin dashboard remains on 9010.', 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' },
|
||||
|
||||
@@ -163,7 +163,7 @@ export function getCuratedAppList(): MarketplaceApp[] {
|
||||
title: 'Saleor',
|
||||
version: '3.23',
|
||||
category: 'commerce',
|
||||
description: 'Composable commerce platform with GraphQL API, dashboard, worker, mail testing, and tracing.',
|
||||
description: 'Composable commerce platform with customer storefront, GraphQL API, dashboard, worker, mail testing, and tracing.',
|
||||
icon: '/assets/img/app-icons/saleor.svg',
|
||||
author: 'Saleor',
|
||||
dockerImage: 'ghcr.io/saleor/saleor:3.23',
|
||||
|
||||
@@ -180,6 +180,33 @@ init()
|
||||
</button>
|
||||
</div>
|
||||
<div class="overflow-y-auto flex-1 min-h-0 space-y-6 pr-1">
|
||||
<!-- v1.7.79-alpha -->
|
||||
<div>
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<span class="text-xs font-mono px-2 py-0.5 rounded bg-orange-500/20 text-orange-300">v1.7.79-alpha</span>
|
||||
<span class="text-xs text-white/40">May 20, 2026</span>
|
||||
</div>
|
||||
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||
<p>Saleor now includes the official Saleor Storefront as the customer-facing shop on port 9011.</p>
|
||||
<p>Launching Saleor opens the storefront, while the admin dashboard remains on port 9010 with generated admin@example.com credentials shown in Archipelago.</p>
|
||||
<p>Public storefront domains also get same-origin /graphql/ proxying so phones and laptops can reach the local Saleor API through the domain instead of a private node address.</p>
|
||||
<p>Saleor catalog, marketplace, scanner, and app-session metadata now describe the storefront/dashboard/API split explicitly.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- v1.7.78-alpha -->
|
||||
<div>
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<span class="text-xs font-mono px-2 py-0.5 rounded bg-orange-500/20 text-orange-300">v1.7.78-alpha</span>
|
||||
<span class="text-xs text-white/40">May 20, 2026</span>
|
||||
</div>
|
||||
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||
<p>Saleor's validated port 9010 dashboard/API origin settings are now ready for production release, preserving the working test-node install path.</p>
|
||||
<p>NetBird launches now stay on the unified dashboard/proxy origin at port 8087 instead of following stale server URLs on 8086.</p>
|
||||
<p>NetBird proxy routing no longer depends on a hard-coded rootless Podman gateway IP and now includes the upstream management proxy gRPC path.</p>
|
||||
<p>Mobile credential prompts keep long credential lists scrollable and the Cancel/Continue buttons reachable in both My Apps and the mobile icon grid.</p>
|
||||
<p>Android app-session popups hand external login/signup windows to the system browser instead of dropping them inside the WebView.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- v1.7.76-alpha -->
|
||||
<div>
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
@@ -187,8 +214,10 @@ init()
|
||||
<span class="text-xs text-white/40">May 20, 2026</span>
|
||||
</div>
|
||||
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||
<p>Saleor now installs on dashboard port 9010 instead of conflicting with Portainer on 9000, keeps its API on 8000, and starts cleanly in rootless Podman with the nginx capabilities it needs.</p>
|
||||
<p>NetBird API and OAuth routes now proxy through the stable host-published server port, so login/logout routes survive a netbird-server restart without 502s from stale Podman DNS.</p>
|
||||
<p>Saleor now installs on dashboard port 9010 instead of conflicting with Portainer on 9000, keeps its API on 8000, creates or repairs the admin@example.com staff account after sample data loads, and starts cleanly with the correct dashboard mount path.</p>
|
||||
<p>Apps with generated first-use credentials now show them in Archipelago before launch and in App Details. Saleor exposes copyable admin email/password fields so the dashboard login screen is no longer a dead end.</p>
|
||||
<p>NetBird API and OAuth routes now proxy through the stable host-published server port, and the embedded IdP keeps upstream-compatible signing-key refresh settings while the dashboard sends ID tokens to the API so signup no longer lands in an Unauthenticated dashboard state.</p>
|
||||
<p>Transient unnamed Podman helper containers created during app installs are hidden from My Apps, so random generated names like eager_keldysh no longer appear as applications.</p>
|
||||
<p>Mobile App Store categories are now visible as horizontal chips above the tab bar, Discover is reachable on mobile, category choices update the actual view, and apps that require a real tab open directly from the icon tap.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+17
-19
@@ -1,30 +1,28 @@
|
||||
{
|
||||
"version": "1.7.76-alpha",
|
||||
"release_date": "2026-05-19",
|
||||
"version": "1.7.81-alpha",
|
||||
"release_date": "2026-05-21",
|
||||
"changelog": [
|
||||
"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."
|
||||
"Saleor storefront installs now use the prebuilt registry image instead of building the Next.js app on-device, avoiding Podman build failures during stack installation.",
|
||||
"Existing Saleor stacks are repaired on adoption by recreating missing storefront containers, forcing the storefront app to bind `0.0.0.0:3000`, and resolving nginx upstreams dynamically after container restarts.",
|
||||
"The shipped Saleor storefront image now includes public assets and omits Vercel-only Speed Insights injection, fixing broken static asset responses and the local `/_vercel/speed-insights/script.js` browser warning.",
|
||||
"Validation passed with `cargo fmt --all --check --manifest-path core/Cargo.toml`, `cargo check -p archipelago --manifest-path core/Cargo.toml`, and live checks on `100.114.134.21` for `9011` storefront, static assets, and proxied GraphQL."
|
||||
],
|
||||
"components": [
|
||||
{
|
||||
"name": "archipelago",
|
||||
"current_version": "1.7.76-alpha",
|
||||
"new_version": "1.7.76-alpha",
|
||||
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.76-alpha/archipelago",
|
||||
"sha256": "ced129f183e71d9fe27669c2b091fc8069dd628ba06afa0a40d470a99cb52f2b",
|
||||
"size_bytes": 43062136
|
||||
"current_version": "1.7.81-alpha",
|
||||
"new_version": "1.7.81-alpha",
|
||||
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.81-alpha/archipelago",
|
||||
"sha256": "82ba94b4e4494a4a67506e3516c34e0f874630efae2d673c70852da9f94144e7",
|
||||
"size_bytes": 43109024
|
||||
},
|
||||
{
|
||||
"name": "archipelago-frontend-1.7.76-alpha.tar.gz",
|
||||
"current_version": "1.7.76-alpha",
|
||||
"new_version": "1.7.76-alpha",
|
||||
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.76-alpha/archipelago-frontend-1.7.76-alpha.tar.gz",
|
||||
"sha256": "86f1f4932f48d79fbf4cde6052fa77ab917520b857f19e70809fad1e07dc5e25",
|
||||
"size_bytes": 166480865
|
||||
"name": "archipelago-frontend-1.7.81-alpha.tar.gz",
|
||||
"current_version": "1.7.81-alpha",
|
||||
"new_version": "1.7.81-alpha",
|
||||
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.81-alpha/archipelago-frontend-1.7.81-alpha.tar.gz",
|
||||
"sha256": "69ec4df6c1f77cbbacc6ea8dc8e2e40cbb50cf61b032825c7de520c72960afba",
|
||||
"size_bytes": 166484929
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+17
-19
@@ -1,30 +1,28 @@
|
||||
{
|
||||
"version": "1.7.76-alpha",
|
||||
"release_date": "2026-05-19",
|
||||
"version": "1.7.81-alpha",
|
||||
"release_date": "2026-05-21",
|
||||
"changelog": [
|
||||
"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."
|
||||
"Saleor storefront installs now use the prebuilt registry image instead of building the Next.js app on-device, avoiding Podman build failures during stack installation.",
|
||||
"Existing Saleor stacks are repaired on adoption by recreating missing storefront containers, forcing the storefront app to bind `0.0.0.0:3000`, and resolving nginx upstreams dynamically after container restarts.",
|
||||
"The shipped Saleor storefront image now includes public assets and omits Vercel-only Speed Insights injection, fixing broken static asset responses and the local `/_vercel/speed-insights/script.js` browser warning.",
|
||||
"Validation passed with `cargo fmt --all --check --manifest-path core/Cargo.toml`, `cargo check -p archipelago --manifest-path core/Cargo.toml`, and live checks on `100.114.134.21` for `9011` storefront, static assets, and proxied GraphQL."
|
||||
],
|
||||
"components": [
|
||||
{
|
||||
"name": "archipelago",
|
||||
"current_version": "1.7.76-alpha",
|
||||
"new_version": "1.7.76-alpha",
|
||||
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.76-alpha/archipelago",
|
||||
"sha256": "ced129f183e71d9fe27669c2b091fc8069dd628ba06afa0a40d470a99cb52f2b",
|
||||
"size_bytes": 43062136
|
||||
"current_version": "1.7.81-alpha",
|
||||
"new_version": "1.7.81-alpha",
|
||||
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.81-alpha/archipelago",
|
||||
"sha256": "82ba94b4e4494a4a67506e3516c34e0f874630efae2d673c70852da9f94144e7",
|
||||
"size_bytes": 43109024
|
||||
},
|
||||
{
|
||||
"name": "archipelago-frontend-1.7.76-alpha.tar.gz",
|
||||
"current_version": "1.7.76-alpha",
|
||||
"new_version": "1.7.76-alpha",
|
||||
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.76-alpha/archipelago-frontend-1.7.76-alpha.tar.gz",
|
||||
"sha256": "86f1f4932f48d79fbf4cde6052fa77ab917520b857f19e70809fad1e07dc5e25",
|
||||
"size_bytes": 166480865
|
||||
"name": "archipelago-frontend-1.7.81-alpha.tar.gz",
|
||||
"current_version": "1.7.81-alpha",
|
||||
"new_version": "1.7.81-alpha",
|
||||
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.81-alpha/archipelago-frontend-1.7.81-alpha.tar.gz",
|
||||
"sha256": "69ec4df6c1f77cbbacc6ea8dc8e2e40cbb50cf61b032825c7de520c72960afba",
|
||||
"size_bytes": 166484929
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -58,6 +58,42 @@ for row in rows:
|
||||
# NPM containers use this name to reach host-published services; host nginx
|
||||
# itself should use loopback for the same services.
|
||||
nginx_host = "127.0.0.1" if host == "host.containers.internal" else host
|
||||
try:
|
||||
forward_port = int(port)
|
||||
except (TypeError, ValueError):
|
||||
forward_port = None
|
||||
is_saleor_dashboard = forward_port == 9010
|
||||
is_saleor_storefront = forward_port == 9011
|
||||
is_saleor = is_saleor_dashboard or is_saleor_storefront
|
||||
graphql_location = ""
|
||||
saleor_proxy_headers = ""
|
||||
if is_saleor:
|
||||
graphql_location = """
|
||||
location ^~ /graphql/ {
|
||||
proxy_pass http://127.0.0.1:8000/graphql/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host 127.0.0.1;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header X-Forwarded-Scheme https;
|
||||
proxy_set_header Origin "";
|
||||
}
|
||||
"""
|
||||
if is_saleor_storefront:
|
||||
saleor_proxy_headers = """
|
||||
proxy_set_header Accept-Encoding "";
|
||||
sub_filter_once off;
|
||||
sub_filter_types text/html application/javascript text/javascript;
|
||||
sub_filter 'http://api:8000/graphql/' 'https://$host/graphql/';
|
||||
"""
|
||||
if is_saleor_dashboard:
|
||||
saleor_proxy_headers = """
|
||||
proxy_set_header Accept-Encoding "";
|
||||
sub_filter_once off;
|
||||
sub_filter_types text/html;
|
||||
sub_filter '</head>' '<script>if(window.__SALEOR_CONFIG__){window.__SALEOR_CONFIG__.API_URL=location.origin+"/graphql/";window.__SALEOR_CONFIG__.EXTENSIONS_API_URL=location.origin+"/graphql/";}</script></head>';
|
||||
"""
|
||||
|
||||
print(f"""
|
||||
server {{
|
||||
@@ -81,6 +117,8 @@ server {{
|
||||
ssl_certificate {cert};
|
||||
ssl_certificate_key {key};
|
||||
|
||||
{graphql_location}
|
||||
|
||||
location / {{
|
||||
proxy_pass {scheme}://{nginx_host}:{port};
|
||||
proxy_http_version 1.1;
|
||||
@@ -91,6 +129,7 @@ server {{
|
||||
proxy_set_header X-Forwarded-Scheme https;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
{saleor_proxy_headers}
|
||||
}}
|
||||
}}
|
||||
""")
|
||||
|
||||
Reference in New Issue
Block a user