release(v1.7.38-alpha): onboarding auto-heal + silent returning logins + app-store trim
Some checks failed
Build Archipelago ISO (dev) / build-iso (push) Failing after 11m12s

- auth.rs now infers onboarding-complete from setup_complete + password_hash so
  nodes stop bouncing users through the intro wizard after browser clear / update
  / reboot; the flag self-heals to disk on next check
- frontend: "backend uncertain" no longer defaults to /onboarding/intro —
  useOnboarding returns null + callers poll / retry instead of flashing the wizard
- login sounds (synthwave, welcome voice, pop, whoosh, oomph) gated by
  isFirstInstallPhase(); typing sounds unaffected
- removed FIPS app, Nostr Relay, Nostr VPN, Routstr, Penpot from catalog,
  frontend config, Rust AppMetadata + install dispatch + install_penpot_stack;
  docker/fips-ui + docker/nostr-vpn-ui + apps/penpot dirs and 5 icons deleted;
  15 image versions deleted from tx1138, .168, gitea-local registries (.160
  Gitea was 502 at release time — follow-up)
- AIUI baked into frontend release tarball via demo/aiui/; deploy-to-target
  falls back to demo/aiui/ when the AIUI sibling checkout is missing
- prebuild hook syncs app-catalog/catalog.json → public/catalog.json so the
  two copies can no longer drift (was the source of the "apps still visible"
  bug — public/ had stale data)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-04-22 13:02:24 -04:00
parent 9cb114c50a
commit ca5d2cc42a
43 changed files with 332 additions and 1462 deletions

View File

@@ -86,9 +86,6 @@ impl RpcHandler {
if package_id == "immich" {
return self.install_immich_stack().await;
}
if package_id == "penpot" || package_id == "penpot-frontend" {
return self.install_penpot_stack().await;
}
if matches!(package_id, "btcpay-server" | "btcpayserver" | "btcpay") {
return self.install_btcpay_stack().await;
}
@@ -312,11 +309,6 @@ impl RpcHandler {
}
}
// TUN device for mesh networking apps
if matches!(package_id, "nostr-vpn" | "fips") {
run_args.push("--device=/dev/net/tun");
}
// Create data directories (mkdir only — chown happens AFTER config files are written)
for volume in &volumes {
if let Some(host_path) = volume.split(':').next() {
@@ -358,36 +350,6 @@ impl RpcHandler {
}
}
// Pre-install: write Nostr identity key files for headless Nostr-aware apps
if matches!(package_id, "nostr-vpn" | "fips") {
let nostr_secret =
std::fs::read_to_string("/var/lib/archipelago/identity/nostr_secret")
.map(|s| s.trim().to_string())
.unwrap_or_default();
if !nostr_secret.is_empty() {
let key_dir = match package_id {
"nostr-vpn" => "/var/lib/archipelago/nostr-vpn",
"fips" => "/var/lib/archipelago/fips/config",
_ => unreachable!(),
};
let key_path = match package_id {
"nostr-vpn" => format!("{}/nostr_secret", key_dir),
"fips" => format!("{}/fips.key", key_dir),
_ => unreachable!(),
};
tokio::fs::create_dir_all(key_dir).await.ok();
tokio::fs::write(&key_path, &nostr_secret).await.ok();
// Restrict permissions on key file
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let perms = std::fs::Permissions::from_mode(0o600);
std::fs::set_permissions(&key_path, perms).ok();
}
info!("Wrote Nostr identity key for {}", package_id);
}
}
// NOW chown data directories to container UID (after all config files are written)
self.create_data_dirs(package_id, &volumes).await;
@@ -816,7 +778,7 @@ impl RpcHandler {
"grafana" => 472,
"lnd" => 1000,
"mariadb" | "mysql" | "mysql-mempool" | "archy-mempool-db" => 999,
"postgres" | "btcpay-postgres" | "immich-postgres" | "penpot-postgres"
"postgres" | "btcpay-postgres" | "immich-postgres"
| "archy-btcpay-db" | "nextcloud-db" => 70,
"electrumx" | "electrs" => 1000,
_ => 0, // Most containers run as root (UID 0)
@@ -1379,20 +1341,6 @@ server {
"electrs-ui",
)]
}
"nostr-vpn" => {
vec![(
"archy-nostr-vpn-ui",
"/opt/archipelago/docker/nostr-vpn-ui",
"nostr-vpn-ui",
)]
}
"fips" => {
vec![(
"archy-fips-ui",
"/opt/archipelago/docker/fips-ui",
"fips-ui",
)]
}
_ => vec![],
};

View File

@@ -273,234 +273,6 @@ impl RpcHandler {
}))
}
/// Install Penpot stack (postgres + valkey + backend + exporter + frontend).
pub(super) async fn install_penpot_stack(&self) -> Result<serde_json::Value> {
if let Some(adopted) = adopt_stack_if_exists(
"penpot-frontend",
"penpot",
&[
"penpot-postgres",
"penpot-valkey",
"penpot-backend",
"penpot-exporter",
"penpot-frontend",
],
)
.await?
{
return Ok(adopted);
}
let images = [
"git.tx1138.com/lfg2025/postgres:15",
"git.tx1138.com/lfg2025/valkey:8.1",
"git.tx1138.com/lfg2025/penpot-backend:2.4",
"git.tx1138.com/lfg2025/penpot-exporter:2.4",
"git.tx1138.com/lfg2025/penpot-frontend:2.4",
];
for img in &images {
pull_image_with_retry(img).await?;
}
let _ = tokio::process::Command::new("sudo")
.args(["mkdir", "-p", "/var/lib/archipelago/penpot-assets"])
.output()
.await;
let _ = tokio::process::Command::new("podman")
.args(["network", "create", "penpot-net"])
.output()
.await;
// Generate a stable secret key derived from the data directory
let secret = {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(b"penpot-secret-");
hasher.update(self.config.data_dir.to_string_lossy().as_bytes());
hex::encode(hasher.finalize())
};
let host_ip = &self.config.host_ip;
let _ = tokio::process::Command::new("podman")
.args([
"run",
"-d",
"--name",
"penpot-postgres",
"--restart",
"unless-stopped",
"--network",
"penpot-net",
"--network-alias",
"penpot-postgres",
"--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 penpot || exit 1",
"--health-interval=30s",
"--health-retries=3",
"-v",
"/var/lib/archipelago/penpot-postgres:/var/lib/postgresql/data",
"-e",
"POSTGRES_DB=penpot",
"-e",
"POSTGRES_USER=penpot",
"-e",
"POSTGRES_PASSWORD=penpot",
"git.tx1138.com/lfg2025/postgres:15",
])
.output()
.await;
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
let _ = tokio::process::Command::new("podman")
.args([
"run",
"-d",
"--name",
"penpot-valkey",
"--restart",
"unless-stopped",
"--network",
"penpot-net",
"--network-alias",
"penpot-valkey",
"--cap-drop=ALL",
"--security-opt=no-new-privileges:true",
"--memory=192m",
"--pids-limit=2048",
"--health-cmd=valkey-cli ping || exit 1",
"--health-interval=30s",
"--health-retries=3",
"-e",
"VALKEY_EXTRA_FLAGS=--maxmemory 128mb --maxmemory-policy volatile-lfu",
"git.tx1138.com/lfg2025/valkey:8.1",
])
.output()
.await;
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
let _ = tokio::process::Command::new("podman")
.args([
"run",
"-d",
"--name",
"penpot-backend",
"--restart",
"unless-stopped",
"--network",
"penpot-net",
"--network-alias",
"penpot-backend",
"--cap-drop=ALL",
"--security-opt=no-new-privileges:true",
"--memory=1g",
"--pids-limit=4096",
"-v",
"/var/lib/archipelago/penpot-assets:/opt/data/assets",
"-e",
&format!("PENPOT_PUBLIC_URI=http://{}:9001", host_ip),
"-e",
&format!("PENPOT_SECRET_KEY={}", secret),
"-e",
"PENPOT_DATABASE_URI=postgresql://penpot-postgres/penpot",
"-e",
"PENPOT_DATABASE_USERNAME=penpot",
"-e",
"PENPOT_DATABASE_PASSWORD=penpot",
"-e",
"PENPOT_REDIS_URI=redis://penpot-valkey/0",
"-e",
"PENPOT_OBJECTS_STORAGE_BACKEND=fs",
"-e",
"PENPOT_OBJECTS_STORAGE_FS_DIRECTORY=/opt/data/assets",
"-e",
"PENPOT_FLAGS=disable-email-verification enable-smtp enable-prepl-server disable-secure-session-cookies",
"git.tx1138.com/lfg2025/penpot-backend:2.4",
])
.output()
.await;
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
let _ = tokio::process::Command::new("podman")
.args([
"run",
"-d",
"--name",
"penpot-exporter",
"--restart",
"unless-stopped",
"--network",
"penpot-net",
"--network-alias",
"penpot-exporter",
"--cap-drop=ALL",
"--security-opt=no-new-privileges:true",
"--memory=512m",
"--pids-limit=2048",
"-e",
&format!("PENPOT_SECRET_KEY={}", secret),
"-e",
"PENPOT_PUBLIC_URI=http://penpot-frontend:8080",
"-e",
"PENPOT_REDIS_URI=redis://penpot-valkey/0",
"git.tx1138.com/lfg2025/penpot-exporter:2.4",
])
.output()
.await;
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
let run = tokio::process::Command::new("podman")
.args([
"run",
"-d",
"--name",
"penpot-frontend",
"--restart",
"unless-stopped",
"--network",
"penpot-net",
"--network-alias",
"penpot-frontend",
"--cap-drop=ALL",
"--security-opt=no-new-privileges:true",
"--memory=512m",
"--pids-limit=2048",
"-p",
"9001:8080",
"-v",
"/var/lib/archipelago/penpot-assets:/opt/data/assets",
"-e",
&format!("PENPOT_PUBLIC_URI=http://{}:9001", host_ip),
"-e",
"PENPOT_FLAGS=disable-email-verification enable-smtp enable-prepl-server disable-secure-session-cookies",
"git.tx1138.com/lfg2025/penpot-frontend:2.4",
])
.output()
.await
.context("Failed to start penpot-frontend")?;
if !run.status.success() {
let stderr = String::from_utf8_lossy(&run.stderr);
return Err(anyhow::anyhow!(
"Failed to start Penpot frontend: {}",
stderr
));
}
info!("Penpot stack installed and started");
Ok(serde_json::json!({
"success": true,
"package_id": "penpot",
"message": "Penpot stack installed and started"
}))
}
/// Install BTCPay stack (postgres + nbxplorer + btcpay-server).
pub(super) async fn install_btcpay_stack(&self) -> Result<serde_json::Value> {

View File

@@ -185,12 +185,32 @@ impl AuthManager {
}
}
}
// Fallback: user.json
Ok(self
.get_user()
.await?
.map(|u| u.onboarding_complete)
.unwrap_or(false))
// Fallback: user.json. A node that has a password set AND
// setup_complete=true has been through onboarding by
// definition — you can't reach the password-set step any
// other way. The separate `onboarding_complete` flag can drift
// out of sync (e.g. the completion RPC never reached disk, or
// the node was seeded from a backup pre-dating the flag), so
// auto-heal by inferring from setup_complete + password_hash.
// Without this, a fully-onboarded node whose `onboarding_complete`
// is stuck false will force its user back through the intro
// wizard on every cleared browser cache.
if let Some(u) = self.get_user().await? {
if u.onboarding_complete {
return Ok(true);
}
if u.setup_complete && !u.password_hash.is_empty() {
// Persist the healed state so subsequent calls skip this
// inference. Ignore write errors — returning true is
// still correct even if we can't persist.
let healed = OnboardingState { complete: true };
if let Ok(json) = serde_json::to_string_pretty(&healed) {
let _ = fs::write(&onboarding_file, json).await;
}
return Ok(true);
}
}
Ok(false)
}
/// Check if 2FA is enabled for the user.

View File

@@ -44,11 +44,6 @@ impl DockerPackageScanner {
"nbxplorer",
"mempool-db",
"mempool-api",
"penpot-postgres",
"penpot-backend",
"penpot-exporter",
"penpot-valkey",
"penpot-mailcatch",
"immich_postgres",
"immich_redis",
"endurain-db",
@@ -416,13 +411,6 @@ fn get_app_metadata(app_id: &str) -> AppMetadata {
repo: "https://github.com/cryptpad/cryptpad".to_string(),
tier: "",
},
"penpot" | "penpot-frontend" => AppMetadata {
title: "Penpot".to_string(),
description: "Open-source design and prototyping".to_string(),
icon: "/assets/img/app-icons/penpot.webp".to_string(),
repo: "https://github.com/penpot/penpot".to_string(),
tier: "",
},
"nextcloud" => AppMetadata {
title: "Nextcloud".to_string(),
description: "Self-hosted cloud storage and file management".to_string(),
@@ -500,13 +488,6 @@ fn get_app_metadata(app_id: &str) -> AppMetadata {
repo: "https://github.com/indeedhub/indeedhub".to_string(),
tier: "",
},
"nostr-rs-relay" => AppMetadata {
title: "Nostr Relay".to_string(),
description: "Run your own Nostr relay for sovereign event storage".to_string(),
icon: "/assets/img/app-icons/nostr-rs-relay.svg".to_string(),
repo: "https://sr.ht/~gheartsfield/nostr-rs-relay/".to_string(),
tier: "",
},
"dwn" => AppMetadata {
title: "Decentralized Web Node".to_string(),
description: "Store and sync personal data with DID-based access control".to_string(),