release(v1.7.38-alpha): onboarding auto-heal + silent returning logins + app-store trim
- 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:
parent
cfc98c600e
commit
36a6101026
@ -110,14 +110,6 @@
|
|||||||
"dockerImage": "git.tx1138.com/lfg2025/searxng:latest",
|
"dockerImage": "git.tx1138.com/lfg2025/searxng:latest",
|
||||||
"repoUrl": "https://github.com/searxng/searxng"
|
"repoUrl": "https://github.com/searxng/searxng"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "nostr-rs-relay", "title": "Nostr Relay", "version": "0.9.0",
|
|
||||||
"description": "Your own Nostr relay. Store events locally, relay for friends.",
|
|
||||||
"icon": "/assets/img/app-icons/nostr-rs-relay.svg",
|
|
||||||
"author": "scsiblade", "category": "nostr",
|
|
||||||
"dockerImage": "git.tx1138.com/lfg2025/nostr-rs-relay:0.9.0",
|
|
||||||
"repoUrl": "https://sr.ht/~gheartsfield/nostr-rs-relay/"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "fedimint", "title": "Fedimint", "version": "0.10.0",
|
"id": "fedimint", "title": "Fedimint", "version": "0.10.0",
|
||||||
"description": "Federated Bitcoin mint with privacy through federated guardians.",
|
"description": "Federated Bitcoin mint with privacy through federated guardians.",
|
||||||
@ -190,30 +182,6 @@
|
|||||||
"dockerImage": "git.tx1138.com/lfg2025/uptime-kuma:1",
|
"dockerImage": "git.tx1138.com/lfg2025/uptime-kuma:1",
|
||||||
"repoUrl": "https://github.com/louislam/uptime-kuma"
|
"repoUrl": "https://github.com/louislam/uptime-kuma"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "nostr-vpn", "title": "Nostr VPN", "version": "0.3.7",
|
|
||||||
"description": "Tailscale-style mesh VPN with Nostr control plane.",
|
|
||||||
"icon": "/assets/img/app-icons/nostr-vpn.svg",
|
|
||||||
"author": "Martti Malmi", "category": "networking",
|
|
||||||
"dockerImage": "git.tx1138.com/lfg2025/nostr-vpn:v0.3.7",
|
|
||||||
"repoUrl": "https://github.com/mmalmi/nostr-vpn"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "fips", "title": "FIPS", "version": "0.1.0",
|
|
||||||
"description": "Free Internetworking Peering System. Encrypted mesh network.",
|
|
||||||
"icon": "/assets/img/app-icons/fips.svg",
|
|
||||||
"author": "Jim Corgan", "category": "networking",
|
|
||||||
"dockerImage": "git.tx1138.com/lfg2025/fips:v0.1.0",
|
|
||||||
"repoUrl": "https://github.com/jmcorgan/fips"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "routstr", "title": "Routstr", "version": "0.4.3",
|
|
||||||
"description": "Decentralized AI inference proxy with Cashu ecash.",
|
|
||||||
"icon": "/assets/img/app-icons/routstr.svg",
|
|
||||||
"author": "Routstr", "category": "community",
|
|
||||||
"dockerImage": "git.tx1138.com/lfg2025/routstr:v0.4.3",
|
|
||||||
"repoUrl": "https://github.com/routstr/routstr-core"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "dwn", "title": "Decentralized Web Node", "version": "0.4.0",
|
"id": "dwn", "title": "Decentralized Web Node", "version": "0.4.0",
|
||||||
"description": "Own your data with DID-based access control.",
|
"description": "Own your data with DID-based access control.",
|
||||||
@ -230,14 +198,6 @@
|
|||||||
"dockerImage": "git.tx1138.com/lfg2025/endurain:0.8.0",
|
"dockerImage": "git.tx1138.com/lfg2025/endurain:0.8.0",
|
||||||
"repoUrl": "https://github.com/joaovitoriasilva/endurain"
|
"repoUrl": "https://github.com/joaovitoriasilva/endurain"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "penpot", "title": "Penpot", "version": "2.4",
|
|
||||||
"description": "Open-source design platform. Self-hosted Figma alternative.",
|
|
||||||
"icon": "/assets/img/app-icons/penpot.webp",
|
|
||||||
"author": "Penpot", "category": "data",
|
|
||||||
"dockerImage": "git.tx1138.com/lfg2025/penpot-frontend:2.4",
|
|
||||||
"repoUrl": "https://github.com/penpot/penpot"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "photoprism", "title": "PhotoPrism", "version": "240915",
|
"id": "photoprism", "title": "PhotoPrism", "version": "240915",
|
||||||
"description": "AI-powered photo management with facial recognition.",
|
"description": "AI-powered photo management with facial recognition.",
|
||||||
|
|||||||
@ -1,5 +0,0 @@
|
|||||||
# Penpot - uses official image
|
|
||||||
FROM penpot/penpot:latest
|
|
||||||
|
|
||||||
# Default configuration is in the image
|
|
||||||
# No additional setup needed
|
|
||||||
@ -1,51 +0,0 @@
|
|||||||
app:
|
|
||||||
id: penpot
|
|
||||||
name: Penpot
|
|
||||||
version: 2.0.0
|
|
||||||
description: Open-source design and prototyping platform. Design tools for teams.
|
|
||||||
|
|
||||||
container:
|
|
||||||
image: penpotapp/frontend:2.13.3
|
|
||||||
image_signature: cosign://...
|
|
||||||
pull_policy: if-not-present
|
|
||||||
|
|
||||||
dependencies:
|
|
||||||
- storage: 10Gi
|
|
||||||
|
|
||||||
resources:
|
|
||||||
cpu_limit: 4
|
|
||||||
memory_limit: 4Gi
|
|
||||||
disk_limit: 10Gi
|
|
||||||
|
|
||||||
security:
|
|
||||||
capabilities: []
|
|
||||||
readonly_root: true
|
|
||||||
no_new_privileges: true
|
|
||||||
user: 1000
|
|
||||||
seccomp_profile: default
|
|
||||||
network_policy: isolated
|
|
||||||
apparmor_profile: penpot
|
|
||||||
|
|
||||||
ports:
|
|
||||||
- host: 8089
|
|
||||||
container: 80
|
|
||||||
protocol: tcp # Web UI
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
- type: bind
|
|
||||||
source: /var/lib/archipelago/penpot
|
|
||||||
target: /app/data
|
|
||||||
options: [rw]
|
|
||||||
|
|
||||||
environment:
|
|
||||||
- PENPOT_PUBLIC_URI=http://localhost:8089
|
|
||||||
- PENPOT_DATABASE_URI=postgresql://penpot:penpot@penpot-db:5432/penpot
|
|
||||||
- PENPOT_REDIS_URI=redis://penpot-redis:6379
|
|
||||||
|
|
||||||
health_check:
|
|
||||||
type: http
|
|
||||||
endpoint: http://localhost:8089
|
|
||||||
path: /api/health
|
|
||||||
interval: 30s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 3
|
|
||||||
@ -86,9 +86,6 @@ impl RpcHandler {
|
|||||||
if package_id == "immich" {
|
if package_id == "immich" {
|
||||||
return self.install_immich_stack().await;
|
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") {
|
if matches!(package_id, "btcpay-server" | "btcpayserver" | "btcpay") {
|
||||||
return self.install_btcpay_stack().await;
|
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)
|
// Create data directories (mkdir only — chown happens AFTER config files are written)
|
||||||
for volume in &volumes {
|
for volume in &volumes {
|
||||||
if let Some(host_path) = volume.split(':').next() {
|
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)
|
// NOW chown data directories to container UID (after all config files are written)
|
||||||
self.create_data_dirs(package_id, &volumes).await;
|
self.create_data_dirs(package_id, &volumes).await;
|
||||||
|
|
||||||
@ -816,7 +778,7 @@ impl RpcHandler {
|
|||||||
"grafana" => 472,
|
"grafana" => 472,
|
||||||
"lnd" => 1000,
|
"lnd" => 1000,
|
||||||
"mariadb" | "mysql" | "mysql-mempool" | "archy-mempool-db" => 999,
|
"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,
|
| "archy-btcpay-db" | "nextcloud-db" => 70,
|
||||||
"electrumx" | "electrs" => 1000,
|
"electrumx" | "electrs" => 1000,
|
||||||
_ => 0, // Most containers run as root (UID 0)
|
_ => 0, // Most containers run as root (UID 0)
|
||||||
@ -1379,20 +1341,6 @@ server {
|
|||||||
"electrs-ui",
|
"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![],
|
_ => vec![],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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).
|
/// Install BTCPay stack (postgres + nbxplorer + btcpay-server).
|
||||||
pub(super) async fn install_btcpay_stack(&self) -> Result<serde_json::Value> {
|
pub(super) async fn install_btcpay_stack(&self) -> Result<serde_json::Value> {
|
||||||
|
|||||||
@ -185,12 +185,32 @@ impl AuthManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Fallback: user.json
|
// Fallback: user.json. A node that has a password set AND
|
||||||
Ok(self
|
// setup_complete=true has been through onboarding by
|
||||||
.get_user()
|
// definition — you can't reach the password-set step any
|
||||||
.await?
|
// other way. The separate `onboarding_complete` flag can drift
|
||||||
.map(|u| u.onboarding_complete)
|
// out of sync (e.g. the completion RPC never reached disk, or
|
||||||
.unwrap_or(false))
|
// 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.
|
/// Check if 2FA is enabled for the user.
|
||||||
|
|||||||
@ -44,11 +44,6 @@ impl DockerPackageScanner {
|
|||||||
"nbxplorer",
|
"nbxplorer",
|
||||||
"mempool-db",
|
"mempool-db",
|
||||||
"mempool-api",
|
"mempool-api",
|
||||||
"penpot-postgres",
|
|
||||||
"penpot-backend",
|
|
||||||
"penpot-exporter",
|
|
||||||
"penpot-valkey",
|
|
||||||
"penpot-mailcatch",
|
|
||||||
"immich_postgres",
|
"immich_postgres",
|
||||||
"immich_redis",
|
"immich_redis",
|
||||||
"endurain-db",
|
"endurain-db",
|
||||||
@ -416,13 +411,6 @@ fn get_app_metadata(app_id: &str) -> AppMetadata {
|
|||||||
repo: "https://github.com/cryptpad/cryptpad".to_string(),
|
repo: "https://github.com/cryptpad/cryptpad".to_string(),
|
||||||
tier: "",
|
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 {
|
"nextcloud" => AppMetadata {
|
||||||
title: "Nextcloud".to_string(),
|
title: "Nextcloud".to_string(),
|
||||||
description: "Self-hosted cloud storage and file management".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(),
|
repo: "https://github.com/indeedhub/indeedhub".to_string(),
|
||||||
tier: "",
|
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 {
|
"dwn" => AppMetadata {
|
||||||
title: "Decentralized Web Node".to_string(),
|
title: "Decentralized Web Node".to_string(),
|
||||||
description: "Store and sync personal data with DID-based access control".to_string(),
|
description: "Store and sync personal data with DID-based access control".to_string(),
|
||||||
|
|||||||
@ -1,10 +0,0 @@
|
|||||||
FROM git.tx1138.com/lfg2025/nginx:1.27.4-alpine
|
|
||||||
COPY index.html /usr/share/nginx/html/
|
|
||||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
|
||||||
RUN sed -i 's/^user nginx;/user root;/' /etc/nginx/nginx.conf && \
|
|
||||||
mkdir -p /var/cache/nginx/client_temp /var/cache/nginx/proxy_temp \
|
|
||||||
/var/cache/nginx/fastcgi_temp /var/cache/nginx/uwsgi_temp \
|
|
||||||
/var/cache/nginx/scgi_temp
|
|
||||||
EXPOSE 8202
|
|
||||||
ENTRYPOINT []
|
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
|
||||||
@ -1,236 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
|
||||||
<title>FIPS - Archipelago</title>
|
|
||||||
<style>
|
|
||||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
||||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; min-height: 100vh; color: white; overflow-x: hidden; }
|
|
||||||
.bg-layer { position: fixed; inset: 0; z-index: -10; background: linear-gradient(135deg, rgba(5,20,15,0.95) 0%, rgba(10,30,25,0.98) 50%, rgba(5,15,20,0.95) 100%); }
|
|
||||||
.overlay { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.7); z-index: -5; }
|
|
||||||
.glass-card { background: rgba(0, 0, 0, 0.5); backdrop-filter: blur(24px); -webkit-backdrop-filter: blur(24px); border-radius: 1rem; border: 1px solid rgba(255, 255, 255, 0.12); transform: translateZ(0); isolation: isolate; }
|
|
||||||
.info-card { background: rgba(255, 255, 255, 0.05); border-radius: 12px; padding: 12px; border: 1px solid rgba(255, 255, 255, 0.08); }
|
|
||||||
.container { max-width: 56rem; margin: 0 auto; padding: 1.5rem; }
|
|
||||||
.flex { display: flex; } .flex-col { flex-direction: column; } .items-center { align-items: center; }
|
|
||||||
.gap-3 { gap: 0.75rem; } .gap-4 { gap: 1rem; } .flex-1 { flex: 1; } .flex-shrink-0 { flex-shrink: 0; }
|
|
||||||
.mb-2 { margin-bottom: 0.5rem; } .mb-4 { margin-bottom: 1rem; } .mb-6 { margin-bottom: 1.5rem; }
|
|
||||||
.p-5 { padding: 1.25rem; } .p-6 { padding: 1.5rem; }
|
|
||||||
.grid { display: grid; } .grid-cols-2 { grid-template-columns: repeat(2, 1fr); } .grid-cols-3 { grid-template-columns: repeat(3, 1fr); }
|
|
||||||
.text-xs { font-size: 0.75rem; } .text-sm { font-size: 0.875rem; } .text-lg { font-size: 1.125rem; }
|
|
||||||
.text-xl { font-size: 1.25rem; } .text-2xl { font-size: 1.5rem; }
|
|
||||||
.font-bold { font-weight: 700; } .font-semibold { font-weight: 600; } .font-medium { font-weight: 500; } .font-mono { font-family: monospace; }
|
|
||||||
.text-white-70 { color: rgba(255,255,255,0.7); } .text-white-60 { color: rgba(255,255,255,0.6); } .text-white-50 { color: rgba(255,255,255,0.5); }
|
|
||||||
.text-emerald { color: #34d399; } .text-green { color: #4ade80; } .text-yellow { color: #fbbf24; } .text-red { color: #f87171; }
|
|
||||||
.justify-between { justify-content: space-between; }
|
|
||||||
.status-dot { width: 0.75rem; height: 0.75rem; border-radius: 9999px; }
|
|
||||||
.bg-green { background: #4ade80; } .bg-yellow { background: #fbbf24; } .bg-red { background: #f87171; }
|
|
||||||
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
|
|
||||||
.animate-pulse { animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; }
|
|
||||||
.icon-box { width: 3.5rem; height: 3.5rem; border-radius: 0.75rem; background: rgba(52, 211, 153, 0.15); display: flex; align-items: center; justify-content: center; }
|
|
||||||
.step { display: flex; gap: 1rem; align-items: flex-start; }
|
|
||||||
.step-num { width: 2rem; height: 2rem; border-radius: 50%; background: rgba(52, 211, 153, 0.2); border: 1px solid rgba(52, 211, 153, 0.4); display: flex; align-items: center; justify-content: center; font-size: 0.875rem; font-weight: 700; color: #34d399; flex-shrink: 0; }
|
|
||||||
.copy-btn { padding: 0.5rem 0.625rem; background: none; border: none; border-left: 1px solid rgba(255,255,255,0.1); cursor: pointer; color: rgba(255,255,255,0.4); transition: all 0.2s ease; display: flex; align-items: center; }
|
|
||||||
.copy-btn:hover { color: rgba(255,255,255,0.8); background: rgba(255,255,255,0.05); }
|
|
||||||
.copy-btn.copied { color: #4ade80; }
|
|
||||||
.field-row { display: flex; align-items: center; background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.1); border-radius: 0.5rem; overflow: hidden; }
|
|
||||||
.field-value { flex: 1; padding: 0.625rem 0.875rem; font-family: monospace; font-size: 0.8125rem; color: rgba(255,255,255,0.9); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
||||||
.field-label { font-size: 0.6875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: rgba(255,255,255,0.4); margin-bottom: 0.375rem; }
|
|
||||||
.feature-icon { width: 2.5rem; height: 2.5rem; border-radius: 0.5rem; background: rgba(52, 211, 153, 0.1); display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
|
|
||||||
@media (max-width: 640px) { .grid-cols-2 { grid-template-columns: 1fr; } .grid-cols-3 { grid-template-columns: 1fr; } }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="bg-layer"></div>
|
|
||||||
<div class="overlay"></div>
|
|
||||||
|
|
||||||
<div class="container">
|
|
||||||
<!-- Header -->
|
|
||||||
<div class="glass-card p-6 mb-6">
|
|
||||||
<div class="flex items-center gap-4">
|
|
||||||
<div class="icon-box flex-shrink-0">
|
|
||||||
<svg style="width:1.75rem;height:1.75rem;color:#34d399" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="flex-1">
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<h1 class="text-2xl font-bold">FIPS</h1>
|
|
||||||
<span class="text-xs text-white-50">v0.1.0</span>
|
|
||||||
</div>
|
|
||||||
<p class="text-white-60 text-sm">Free Internetworking Peering System</p>
|
|
||||||
</div>
|
|
||||||
<div class="info-card flex items-center gap-3">
|
|
||||||
<div id="statusDot" class="status-dot bg-yellow animate-pulse"></div>
|
|
||||||
<div>
|
|
||||||
<p class="text-xs text-white-50">Status</p>
|
|
||||||
<p class="text-sm font-medium" id="statusText">Checking...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- What It Does -->
|
|
||||||
<div class="glass-card p-5 mb-6">
|
|
||||||
<h2 class="text-lg font-semibold mb-4" style="color:#34d399">What is FIPS?</h2>
|
|
||||||
<p class="text-white-70 text-sm mb-4" style="line-height:1.6">
|
|
||||||
FIPS is a <strong style="color:white">self-organizing encrypted mesh network</strong>. Each node gets a
|
|
||||||
<strong style="color:white">secp256k1 keypair</strong> (same as Nostr/Bitcoin) that serves as its identity.
|
|
||||||
Nodes discover each other, negotiate encryption using the <strong style="color:white">Noise protocol</strong>,
|
|
||||||
and route traffic without any central authority. A virtual network interface (<code style="background:rgba(255,255,255,0.1);padding:0.125rem 0.375rem;border-radius:0.25rem">fips0</code>)
|
|
||||||
lets unmodified applications — SSH, web browsers, anything — communicate transparently over the mesh.
|
|
||||||
Think of it as <strong style="color:white">a new internet layer, built on cryptographic identity</strong>.
|
|
||||||
</p>
|
|
||||||
<div class="grid grid-cols-3 gap-3">
|
|
||||||
<div class="info-card flex items-center gap-3">
|
|
||||||
<div class="feature-icon">
|
|
||||||
<svg style="width:1.25rem;height:1.25rem;color:#34d399" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"/></svg>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-sm font-medium">Zero Config</p>
|
|
||||||
<p class="text-xs text-white-50">Self-organizing mesh</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="info-card flex items-center gap-3">
|
|
||||||
<div class="feature-icon">
|
|
||||||
<svg style="width:1.25rem;height:1.25rem;color:#34d399" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/></svg>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-sm font-medium">End-to-End Encrypted</p>
|
|
||||||
<p class="text-xs text-white-50">Noise IK + XK protocols</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="info-card flex items-center gap-3">
|
|
||||||
<div class="feature-icon">
|
|
||||||
<svg style="width:1.25rem;height:1.25rem;color:#34d399" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064"/></svg>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-sm font-medium">Multi-Transport</p>
|
|
||||||
<p class="text-xs text-white-50">UDP, TCP, Tor, BLE</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Node Identity -->
|
|
||||||
<div class="glass-card p-5 mb-6">
|
|
||||||
<h2 class="text-lg font-semibold mb-4" style="color:#34d399">Node Identity</h2>
|
|
||||||
<p class="text-white-60 text-sm mb-4">Your node's Nostr public key doubles as its FIPS mesh address. Share with peers to connect.</p>
|
|
||||||
<div class="grid grid-cols-2 gap-3 mb-4">
|
|
||||||
<div>
|
|
||||||
<div class="field-label">Nostr Public Key (npub)</div>
|
|
||||||
<div class="field-row">
|
|
||||||
<span class="field-value" id="npub">Loading...</span>
|
|
||||||
<button class="copy-btn" onclick="copyField('npub', this)" title="Copy">
|
|
||||||
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2" ry="2" stroke-width="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" stroke-width="2"/></svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="field-label">Mesh Ports</div>
|
|
||||||
<div class="field-row">
|
|
||||||
<span class="field-value">UDP 2121 / TCP 8443</span>
|
|
||||||
<button class="copy-btn" onclick="copyText('2121', this)" title="Copy">
|
|
||||||
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2" ry="2" stroke-width="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" stroke-width="2"/></svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- How to Use -->
|
|
||||||
<div class="glass-card p-5 mb-6">
|
|
||||||
<h2 class="text-lg font-semibold mb-4" style="color:#34d399">How to Use</h2>
|
|
||||||
<div class="flex flex-col gap-4">
|
|
||||||
<div class="step">
|
|
||||||
<div class="step-num">1</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-sm font-semibold mb-2">Install FIPS on your other devices</p>
|
|
||||||
<p class="text-xs text-white-60" style="line-height:1.5">Download <code style="background:rgba(255,255,255,0.1);padding:0.125rem 0.375rem;border-radius:0.25rem">fips</code> from <a href="https://github.com/jmcorgan/fips" style="color:#34d399;text-decoration:underline" target="_blank">GitHub</a>. Build with <code style="background:rgba(255,255,255,0.1);padding:0.125rem 0.375rem;border-radius:0.25rem">cargo build --release</code> (requires Rust 1.85+).</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="step">
|
|
||||||
<div class="step-num">2</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-sm font-semibold mb-2">Configure peers in fips.yaml</p>
|
|
||||||
<p class="text-xs text-white-60" style="line-height:1.5">Edit <code style="background:rgba(255,255,255,0.1);padding:0.125rem 0.375rem;border-radius:0.25rem">/etc/fips/fips.yaml</code> on each device. Add your Archipelago node's IP and port as a peer. The node's npub above is its identity on the mesh.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="step">
|
|
||||||
<div class="step-num">3</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-sm font-semibold mb-2">Start the daemon and connect</p>
|
|
||||||
<p class="text-xs text-white-60" style="line-height:1.5">Run <code style="background:rgba(255,255,255,0.1);padding:0.125rem 0.375rem;border-radius:0.25rem">fips --config /etc/fips/fips.yaml</code>. A <code style="background:rgba(255,255,255,0.1);padding:0.125rem 0.375rem;border-radius:0.25rem">fips0</code> virtual interface appears. Use <code style="background:rgba(255,255,255,0.1);padding:0.125rem 0.375rem;border-radius:0.25rem">fipsctl show peers</code> to see connected nodes. You can now SSH, browse, or run any IP app over the encrypted mesh using <code style="background:rgba(255,255,255,0.1);padding:0.125rem 0.375rem;border-radius:0.25rem">.fips</code> DNS names.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Container Logs -->
|
|
||||||
<div class="glass-card p-5">
|
|
||||||
<h2 class="text-lg font-semibold mb-4" style="color:#34d399">Container Logs</h2>
|
|
||||||
<div id="logs" style="background:rgba(0,0,0,0.4);border-radius:0.5rem;padding:0.75rem;font-family:monospace;font-size:0.75rem;color:rgba(255,255,255,0.6);max-height:200px;overflow-y:auto;line-height:1.6">
|
|
||||||
Fetching logs...
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
var COPY_SVG = '<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2" ry="2" stroke-width="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" stroke-width="2"/></svg>';
|
|
||||||
var CHECK_SVG = '<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>';
|
|
||||||
|
|
||||||
function flashCopied(btn) { btn.classList.add('copied'); var o = btn.innerHTML; btn.innerHTML = CHECK_SVG; setTimeout(function() { btn.classList.remove('copied'); btn.innerHTML = o; }, 1500); }
|
|
||||||
function copyField(id, btn) { var t = document.getElementById(id).textContent.trim(); if (!t || t === 'Loading...') return; navigator.clipboard.writeText(t).then(function() { flashCopied(btn); }); }
|
|
||||||
function copyText(text, btn) { navigator.clipboard.writeText(text).then(function() { flashCopied(btn); }); }
|
|
||||||
|
|
||||||
async function fetchNodeIdentity() {
|
|
||||||
try {
|
|
||||||
var resp = await fetch('/rpc/', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'node.nostr-pubkey', params: {} }) });
|
|
||||||
var data = await resp.json();
|
|
||||||
if (data.result && data.result.npub) {
|
|
||||||
document.getElementById('npub').textContent = data.result.npub;
|
|
||||||
} else if (data.result && data.result.pubkey) {
|
|
||||||
document.getElementById('npub').textContent = data.result.pubkey;
|
|
||||||
}
|
|
||||||
} catch(e) { document.getElementById('npub').textContent = 'Unavailable'; }
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchStatus() {
|
|
||||||
try {
|
|
||||||
var resp = await fetch('/rpc/', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'package.info', params: { id: 'fips' } }) });
|
|
||||||
var data = await resp.json();
|
|
||||||
var dot = document.getElementById('statusDot');
|
|
||||||
var txt = document.getElementById('statusText');
|
|
||||||
if (data.result && data.result.state === 'running') {
|
|
||||||
dot.className = 'status-dot bg-green'; txt.textContent = 'Running';
|
|
||||||
} else if (data.result && data.result.state === 'stopped') {
|
|
||||||
dot.className = 'status-dot bg-red'; txt.textContent = 'Stopped';
|
|
||||||
} else {
|
|
||||||
dot.className = 'status-dot bg-yellow animate-pulse'; txt.textContent = data.result ? data.result.state : 'Unknown';
|
|
||||||
}
|
|
||||||
} catch(e) { /* keep checking */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchLogs() {
|
|
||||||
try {
|
|
||||||
var resp = await fetch('/api/container/logs?app_id=fips&lines=30');
|
|
||||||
if (resp.ok) {
|
|
||||||
var data = await resp.json();
|
|
||||||
var logs = data.logs || data.stdout || '';
|
|
||||||
if (typeof logs === 'object') logs = JSON.stringify(logs);
|
|
||||||
document.getElementById('logs').textContent = logs || 'No logs available yet.';
|
|
||||||
var el = document.getElementById('logs');
|
|
||||||
el.scrollTop = el.scrollHeight;
|
|
||||||
}
|
|
||||||
} catch(e) { document.getElementById('logs').textContent = 'Waiting for container...'; }
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchNodeIdentity();
|
|
||||||
fetchStatus();
|
|
||||||
fetchLogs();
|
|
||||||
setInterval(fetchStatus, 10000);
|
|
||||||
setInterval(fetchLogs, 15000);
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
server {
|
|
||||||
listen 8202;
|
|
||||||
server_name _;
|
|
||||||
|
|
||||||
root /usr/share/nginx/html;
|
|
||||||
index index.html;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
try_files $uri $uri/ /index.html;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
FROM git.tx1138.com/lfg2025/nginx:1.27.4-alpine
|
|
||||||
COPY index.html /usr/share/nginx/html/
|
|
||||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
|
||||||
RUN sed -i 's/^user nginx;/user root;/' /etc/nginx/nginx.conf && \
|
|
||||||
mkdir -p /var/cache/nginx/client_temp /var/cache/nginx/proxy_temp \
|
|
||||||
/var/cache/nginx/fastcgi_temp /var/cache/nginx/uwsgi_temp \
|
|
||||||
/var/cache/nginx/scgi_temp
|
|
||||||
EXPOSE 8201
|
|
||||||
ENTRYPOINT []
|
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
|
||||||
@ -1,232 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
|
||||||
<title>Nostr VPN - Archipelago</title>
|
|
||||||
<style>
|
|
||||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
||||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; min-height: 100vh; color: white; overflow-x: hidden; }
|
|
||||||
.bg-layer { position: fixed; inset: 0; z-index: -10; background: linear-gradient(135deg, rgba(10,5,30,0.95) 0%, rgba(20,10,50,0.98) 50%, rgba(5,15,35,0.95) 100%); }
|
|
||||||
.overlay { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.7); z-index: -5; }
|
|
||||||
.glass-card { background: rgba(0, 0, 0, 0.5); backdrop-filter: blur(24px); -webkit-backdrop-filter: blur(24px); border-radius: 1rem; border: 1px solid rgba(255, 255, 255, 0.12); transform: translateZ(0); isolation: isolate; }
|
|
||||||
.info-card { background: rgba(255, 255, 255, 0.05); border-radius: 12px; padding: 12px; border: 1px solid rgba(255, 255, 255, 0.08); }
|
|
||||||
.container { max-width: 56rem; margin: 0 auto; padding: 1.5rem; }
|
|
||||||
.flex { display: flex; } .flex-col { flex-direction: column; } .items-center { align-items: center; }
|
|
||||||
.gap-3 { gap: 0.75rem; } .gap-4 { gap: 1rem; } .flex-1 { flex: 1; } .flex-shrink-0 { flex-shrink: 0; }
|
|
||||||
.mb-2 { margin-bottom: 0.5rem; } .mb-4 { margin-bottom: 1rem; } .mb-6 { margin-bottom: 1.5rem; }
|
|
||||||
.p-5 { padding: 1.25rem; } .p-6 { padding: 1.5rem; }
|
|
||||||
.grid { display: grid; } .grid-cols-2 { grid-template-columns: repeat(2, 1fr); } .grid-cols-3 { grid-template-columns: repeat(3, 1fr); }
|
|
||||||
.text-xs { font-size: 0.75rem; } .text-sm { font-size: 0.875rem; } .text-lg { font-size: 1.125rem; }
|
|
||||||
.text-xl { font-size: 1.25rem; } .text-2xl { font-size: 1.5rem; }
|
|
||||||
.font-bold { font-weight: 700; } .font-semibold { font-weight: 600; } .font-medium { font-weight: 500; } .font-mono { font-family: monospace; }
|
|
||||||
.text-white-70 { color: rgba(255,255,255,0.7); } .text-white-60 { color: rgba(255,255,255,0.6); } .text-white-50 { color: rgba(255,255,255,0.5); }
|
|
||||||
.text-purple { color: #a78bfa; } .text-green { color: #4ade80; } .text-yellow { color: #fbbf24; } .text-red { color: #f87171; }
|
|
||||||
.justify-between { justify-content: space-between; }
|
|
||||||
.status-dot { width: 0.75rem; height: 0.75rem; border-radius: 9999px; }
|
|
||||||
.bg-green { background: #4ade80; } .bg-yellow { background: #fbbf24; } .bg-red { background: #f87171; }
|
|
||||||
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
|
|
||||||
.animate-pulse { animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; }
|
|
||||||
.icon-box { width: 3.5rem; height: 3.5rem; border-radius: 0.75rem; background: rgba(167, 139, 250, 0.15); display: flex; align-items: center; justify-content: center; }
|
|
||||||
.step { display: flex; gap: 1rem; align-items: flex-start; }
|
|
||||||
.step-num { width: 2rem; height: 2rem; border-radius: 50%; background: rgba(167, 139, 250, 0.2); border: 1px solid rgba(167, 139, 250, 0.4); display: flex; align-items: center; justify-content: center; font-size: 0.875rem; font-weight: 700; color: #a78bfa; flex-shrink: 0; }
|
|
||||||
.copy-btn { padding: 0.5rem 0.625rem; background: none; border: none; border-left: 1px solid rgba(255,255,255,0.1); cursor: pointer; color: rgba(255,255,255,0.4); transition: all 0.2s ease; display: flex; align-items: center; }
|
|
||||||
.copy-btn:hover { color: rgba(255,255,255,0.8); background: rgba(255,255,255,0.05); }
|
|
||||||
.copy-btn.copied { color: #4ade80; }
|
|
||||||
.field-row { display: flex; align-items: center; background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.1); border-radius: 0.5rem; overflow: hidden; }
|
|
||||||
.field-value { flex: 1; padding: 0.625rem 0.875rem; font-family: monospace; font-size: 0.8125rem; color: rgba(255,255,255,0.9); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
||||||
.field-label { font-size: 0.6875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: rgba(255,255,255,0.4); margin-bottom: 0.375rem; }
|
|
||||||
.feature-icon { width: 2.5rem; height: 2.5rem; border-radius: 0.5rem; background: rgba(167, 139, 250, 0.1); display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
|
|
||||||
@media (max-width: 640px) { .grid-cols-2 { grid-template-columns: 1fr; } .grid-cols-3 { grid-template-columns: 1fr; } }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="bg-layer"></div>
|
|
||||||
<div class="overlay"></div>
|
|
||||||
|
|
||||||
<div class="container">
|
|
||||||
<!-- Header -->
|
|
||||||
<div class="glass-card p-6 mb-6">
|
|
||||||
<div class="flex items-center gap-4">
|
|
||||||
<div class="icon-box flex-shrink-0">
|
|
||||||
<svg style="width:1.75rem;height:1.75rem;color:#a78bfa" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="flex-1">
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<h1 class="text-2xl font-bold">Nostr VPN</h1>
|
|
||||||
<span class="text-xs text-white-50">v0.3.4</span>
|
|
||||||
</div>
|
|
||||||
<p class="text-white-60 text-sm">Decentralized mesh VPN with Nostr signaling</p>
|
|
||||||
</div>
|
|
||||||
<div class="info-card flex items-center gap-3">
|
|
||||||
<div id="statusDot" class="status-dot bg-yellow animate-pulse"></div>
|
|
||||||
<div>
|
|
||||||
<p class="text-xs text-white-50">Status</p>
|
|
||||||
<p class="text-sm font-medium" id="statusText">Checking...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- What It Does -->
|
|
||||||
<div class="glass-card p-5 mb-6">
|
|
||||||
<h2 class="text-lg font-semibold mb-4" style="color:#a78bfa">What is Nostr VPN?</h2>
|
|
||||||
<p class="text-white-70 text-sm mb-4" style="line-height:1.6">
|
|
||||||
Nostr VPN creates a <strong style="color:white">private mesh network</strong> between your devices using WireGuard tunnels.
|
|
||||||
Unlike traditional VPNs, there is no central server. Peers discover each other and exchange encryption keys over
|
|
||||||
<strong style="color:white">Nostr relays</strong>, making the network censorship-resistant and self-sovereign.
|
|
||||||
Think of it as <strong style="color:white">Tailscale, but decentralized</strong> — your node's Nostr identity is your network identity.
|
|
||||||
</p>
|
|
||||||
<div class="grid grid-cols-3 gap-3">
|
|
||||||
<div class="info-card flex items-center gap-3">
|
|
||||||
<div class="feature-icon">
|
|
||||||
<svg style="width:1.25rem;height:1.25rem;color:#a78bfa" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 11V7a4 4 0 118 0m-4 8v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2z"/></svg>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-sm font-medium">No Central Server</p>
|
|
||||||
<p class="text-xs text-white-50">Fully peer-to-peer mesh</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="info-card flex items-center gap-3">
|
|
||||||
<div class="feature-icon">
|
|
||||||
<svg style="width:1.25rem;height:1.25rem;color:#a78bfa" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-sm font-medium">WireGuard Tunnels</p>
|
|
||||||
<p class="text-xs text-white-50">Fast, modern encryption</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="info-card flex items-center gap-3">
|
|
||||||
<div class="feature-icon">
|
|
||||||
<svg style="width:1.25rem;height:1.25rem;color:#a78bfa" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064"/></svg>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-sm font-medium">NAT Traversal</p>
|
|
||||||
<p class="text-xs text-white-50">Works behind firewalls</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Node Identity -->
|
|
||||||
<div class="glass-card p-5 mb-6">
|
|
||||||
<h2 class="text-lg font-semibold mb-4" style="color:#a78bfa">Node Identity</h2>
|
|
||||||
<p class="text-white-60 text-sm mb-4">Your node's Nostr public key is used as its network identity. Share it with peers to connect.</p>
|
|
||||||
<div class="mb-4">
|
|
||||||
<div class="field-label">Nostr Public Key (npub)</div>
|
|
||||||
<div class="field-row">
|
|
||||||
<span class="field-value" id="npub">Loading...</span>
|
|
||||||
<button class="copy-btn" onclick="copyField('npub', this)" title="Copy">
|
|
||||||
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2" ry="2" stroke-width="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" stroke-width="2"/></svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="field-label">VPN Listen Port</div>
|
|
||||||
<div class="field-row">
|
|
||||||
<span class="field-value">51820/udp</span>
|
|
||||||
<button class="copy-btn" onclick="copyText('51820', this)" title="Copy">
|
|
||||||
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2" ry="2" stroke-width="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" stroke-width="2"/></svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- How to Use -->
|
|
||||||
<div class="glass-card p-5 mb-6">
|
|
||||||
<h2 class="text-lg font-semibold mb-4" style="color:#a78bfa">How to Use</h2>
|
|
||||||
<div class="flex flex-col gap-4">
|
|
||||||
<div class="step">
|
|
||||||
<div class="step-num">1</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-sm font-semibold mb-2">Install the Nostr VPN client on your device</p>
|
|
||||||
<p class="text-xs text-white-60" style="line-height:1.5">Download <code style="background:rgba(255,255,255,0.1);padding:0.125rem 0.375rem;border-radius:0.25rem">nvpn</code> from <a href="https://github.com/mmalmi/nostr-vpn/releases" style="color:#a78bfa;text-decoration:underline" target="_blank">GitHub Releases</a> on your laptop, phone, or other devices you want to connect.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="step">
|
|
||||||
<div class="step-num">2</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-sm font-semibold mb-2">Create or join a network</p>
|
|
||||||
<p class="text-xs text-white-60" style="line-height:1.5">Run <code style="background:rgba(255,255,255,0.1);padding:0.125rem 0.375rem;border-radius:0.25rem">nvpn network create</code> on this node to create a new network, or join an existing one with an invite code. Each network gets a unique ID shared between members.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="step">
|
|
||||||
<div class="step-num">3</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-sm font-semibold mb-2">Connect your devices</p>
|
|
||||||
<p class="text-xs text-white-60" style="line-height:1.5">Run <code style="background:rgba(255,255,255,0.1);padding:0.125rem 0.375rem;border-radius:0.25rem">nvpn start --daemon --connect</code> on each device. Peers discover each other automatically over Nostr relays and establish direct WireGuard tunnels. Your devices are now privately connected.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Container Status -->
|
|
||||||
<div class="glass-card p-5">
|
|
||||||
<h2 class="text-lg font-semibold mb-4" style="color:#a78bfa">Container Logs</h2>
|
|
||||||
<div id="logs" style="background:rgba(0,0,0,0.4);border-radius:0.5rem;padding:0.75rem;font-family:monospace;font-size:0.75rem;color:rgba(255,255,255,0.6);max-height:200px;overflow-y:auto;line-height:1.6">
|
|
||||||
Fetching logs...
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
var COPY_SVG = '<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2" ry="2" stroke-width="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" stroke-width="2"/></svg>';
|
|
||||||
var CHECK_SVG = '<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>';
|
|
||||||
|
|
||||||
function flashCopied(btn) { btn.classList.add('copied'); var o = btn.innerHTML; btn.innerHTML = CHECK_SVG; setTimeout(function() { btn.classList.remove('copied'); btn.innerHTML = o; }, 1500); }
|
|
||||||
function copyField(id, btn) { var t = document.getElementById(id).textContent.trim(); if (!t || t === 'Loading...') return; navigator.clipboard.writeText(t).then(function() { flashCopied(btn); }); }
|
|
||||||
function copyText(text, btn) { navigator.clipboard.writeText(text).then(function() { flashCopied(btn); }); }
|
|
||||||
|
|
||||||
async function fetchNodeIdentity() {
|
|
||||||
try {
|
|
||||||
var resp = await fetch('/rpc/', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'node.nostr-pubkey', params: {} }) });
|
|
||||||
var data = await resp.json();
|
|
||||||
if (data.result && data.result.npub) {
|
|
||||||
document.getElementById('npub').textContent = data.result.npub;
|
|
||||||
} else if (data.result && data.result.pubkey) {
|
|
||||||
document.getElementById('npub').textContent = data.result.pubkey;
|
|
||||||
}
|
|
||||||
} catch(e) { document.getElementById('npub').textContent = 'Unavailable'; }
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchStatus() {
|
|
||||||
try {
|
|
||||||
var resp = await fetch('/rpc/', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'package.info', params: { id: 'nostr-vpn' } }) });
|
|
||||||
var data = await resp.json();
|
|
||||||
var dot = document.getElementById('statusDot');
|
|
||||||
var txt = document.getElementById('statusText');
|
|
||||||
if (data.result && data.result.state === 'running') {
|
|
||||||
dot.className = 'status-dot bg-green'; txt.textContent = 'Running';
|
|
||||||
} else if (data.result && data.result.state === 'stopped') {
|
|
||||||
dot.className = 'status-dot bg-red'; txt.textContent = 'Stopped';
|
|
||||||
} else {
|
|
||||||
dot.className = 'status-dot bg-yellow animate-pulse'; txt.textContent = data.result ? data.result.state : 'Unknown';
|
|
||||||
}
|
|
||||||
} catch(e) { /* keep checking */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchLogs() {
|
|
||||||
try {
|
|
||||||
var resp = await fetch('/api/container/logs?app_id=nostr-vpn&lines=30');
|
|
||||||
if (resp.ok) {
|
|
||||||
var data = await resp.json();
|
|
||||||
var logs = data.logs || data.stdout || '';
|
|
||||||
if (typeof logs === 'object') logs = JSON.stringify(logs);
|
|
||||||
document.getElementById('logs').textContent = logs || 'No logs available yet.';
|
|
||||||
var el = document.getElementById('logs');
|
|
||||||
el.scrollTop = el.scrollHeight;
|
|
||||||
}
|
|
||||||
} catch(e) { document.getElementById('logs').textContent = 'Waiting for container...'; }
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchNodeIdentity();
|
|
||||||
fetchStatus();
|
|
||||||
fetchLogs();
|
|
||||||
setInterval(fetchStatus, 10000);
|
|
||||||
setInterval(fetchLogs, 15000);
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
server {
|
|
||||||
listen 8201;
|
|
||||||
server_name _;
|
|
||||||
|
|
||||||
root /usr/share/nginx/html;
|
|
||||||
index index.html;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
try_files $uri $uri/ /index.html;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
4
neode-ui/package-lock.json
generated
4
neode-ui/package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "neode-ui",
|
"name": "neode-ui",
|
||||||
"version": "1.7.37-alpha",
|
"version": "1.7.38-alpha",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "neode-ui",
|
"name": "neode-ui",
|
||||||
"version": "1.7.37-alpha",
|
"version": "1.7.38-alpha",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/dompurify": "^3.0.5",
|
"@types/dompurify": "^3.0.5",
|
||||||
"@vue-leaflet/vue-leaflet": "^0.10.1",
|
"@vue-leaflet/vue-leaflet": "^0.10.1",
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "neode-ui",
|
"name": "neode-ui",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.7.37-alpha",
|
"version": "1.7.38-alpha",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "./start-dev.sh",
|
"start": "./start-dev.sh",
|
||||||
@ -14,6 +14,7 @@
|
|||||||
"dev:real": "echo 'Start backend: cd ../core && cargo run --release' && vite",
|
"dev:real": "echo 'Start backend: cd ../core && cargo run --release' && vite",
|
||||||
"backend:mock": "node mock-backend.js",
|
"backend:mock": "node mock-backend.js",
|
||||||
"backend:real": "cd ../core && cargo run --release",
|
"backend:real": "cd ../core && cargo run --release",
|
||||||
|
"prebuild": "cp ../app-catalog/catalog.json public/catalog.json",
|
||||||
"build": "vue-tsc -b && vite build",
|
"build": "vue-tsc -b && vite build",
|
||||||
"build:docker": "vite build",
|
"build:docker": "vite build",
|
||||||
"build:production": "NODE_ENV=production vue-tsc -b && vite build --mode production",
|
"build:production": "NODE_ENV=production vue-tsc -b && vite build --mode production",
|
||||||
|
|||||||
@ -1,4 +0,0 @@
|
|||||||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<rect width="64" height="64" rx="12" fill="#10b981"/>
|
|
||||||
<text x="32" y="38" text-anchor="middle" font-family="system-ui" font-size="16" font-weight="700" fill="white">FIPS</text>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 284 B |
@ -1,11 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" fill="none">
|
|
||||||
<rect width="100" height="100" rx="20" fill="#7B2DBC"/>
|
|
||||||
<circle cx="50" cy="40" r="12" stroke="white" stroke-width="3" fill="none"/>
|
|
||||||
<circle cx="28" cy="68" r="8" stroke="white" stroke-width="2.5" fill="none"/>
|
|
||||||
<circle cx="72" cy="68" r="8" stroke="white" stroke-width="2.5" fill="none"/>
|
|
||||||
<line x1="42" y1="49" x2="33" y2="62" stroke="white" stroke-width="2.5" stroke-linecap="round"/>
|
|
||||||
<line x1="58" y1="49" x2="67" y2="62" stroke="white" stroke-width="2.5" stroke-linecap="round"/>
|
|
||||||
<circle cx="50" cy="40" r="4" fill="white"/>
|
|
||||||
<circle cx="28" cy="68" r="3" fill="white"/>
|
|
||||||
<circle cx="72" cy="68" r="3" fill="white"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 718 B |
@ -1,4 +0,0 @@
|
|||||||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<rect width="64" height="64" rx="12" fill="#6366f1"/>
|
|
||||||
<text x="32" y="38" text-anchor="middle" font-family="system-ui" font-size="18" font-weight="700" fill="white">NV</text>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 282 B |
Binary file not shown.
|
Before Width: | Height: | Size: 6.7 KiB |
@ -1,4 +0,0 @@
|
|||||||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<rect width="64" height="64" rx="12" fill="#f59e0b"/>
|
|
||||||
<text x="32" y="38" text-anchor="middle" font-family="system-ui" font-size="18" font-weight="700" fill="white">R</text>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 281 B |
@ -1,427 +1,210 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 2,
|
||||||
"updated": "2026-04-11T00:00:00Z",
|
"updated": "2026-04-22T00:00:00Z",
|
||||||
"registry": "23.182.128.160:3000/lfg2025",
|
"registry": "git.tx1138.com/lfg2025",
|
||||||
"featured": {
|
"featured": {
|
||||||
"id": "indeedhub",
|
"id": "indeedhub",
|
||||||
"banner": "/assets/img/featured/indeedhub-banner.jpg",
|
"banner": "/assets/img/featured/indeedhub-banner.jpg",
|
||||||
"headline": "Stream Sovereignty",
|
"headline": "Stream Sovereignty",
|
||||||
"description": "Bitcoin documentaries with Nostr identity. God Bless Bitcoin, The Bitcoin Psyop, and more \u2014 streaming from your own node.",
|
"description": "Bitcoin documentaries with Nostr identity.",
|
||||||
"tag": "NOSTR IDENTITY // YOUR NODE"
|
"tag": "NOSTR IDENTITY // YOUR NODE"
|
||||||
},
|
},
|
||||||
"apps": [
|
"apps": [
|
||||||
{
|
{
|
||||||
"id": "bitcoin-knots",
|
"id": "bitcoin-knots", "title": "Bitcoin Knots", "version": "28.1.0",
|
||||||
"title": "Bitcoin Knots",
|
"description": "Run a full Bitcoin node. Validate and relay blocks and transactions.",
|
||||||
"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",
|
"icon": "/assets/img/app-icons/bitcoin-knots.webp",
|
||||||
"author": "Bitcoin Knots",
|
"author": "Bitcoin Knots", "category": "money", "tier": "core",
|
||||||
"dockerImage": "bitcoin-knots:latest",
|
"dockerImage": "git.tx1138.com/lfg2025/bitcoin-knots:latest",
|
||||||
"repoUrl": "https://github.com/bitcoinknots/bitcoin",
|
"repoUrl": "https://github.com/bitcoinknots/bitcoin"
|
||||||
"category": "money",
|
|
||||||
"tier": "core"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "bitcoin-core",
|
"id": "bitcoin-core", "title": "Bitcoin Core", "version": "28.4",
|
||||||
"title": "Bitcoin Core",
|
"description": "Reference implementation of the Bitcoin protocol. Run a full node validating and relaying blocks.",
|
||||||
"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",
|
"icon": "/assets/img/app-icons/bitcoin-core.svg",
|
||||||
"author": "Bitcoin Core contributors",
|
"author": "Bitcoin Core contributors", "category": "money", "tier": "optional",
|
||||||
"dockerImage": "docker.io/bitcoin/bitcoin:28.4",
|
"dockerImage": "docker.io/bitcoin/bitcoin:28.4",
|
||||||
"repoUrl": "https://github.com/bitcoin/bitcoin",
|
"repoUrl": "https://github.com/bitcoin/bitcoin"
|
||||||
"category": "money",
|
|
||||||
"tier": "optional"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "lnd",
|
"id": "lnd", "title": "LND", "version": "0.18.4",
|
||||||
"title": "LND",
|
"description": "Lightning Network Daemon. Fast Bitcoin payments through Lightning.",
|
||||||
"version": "0.18.4",
|
|
||||||
"description": "Lightning Network Daemon. Fast and cheap Bitcoin payments through the Lightning Network.",
|
|
||||||
"icon": "/assets/img/app-icons/lnd.svg",
|
"icon": "/assets/img/app-icons/lnd.svg",
|
||||||
"author": "Lightning Labs",
|
"author": "Lightning Labs", "category": "money", "tier": "core",
|
||||||
"dockerImage": "lnd:v0.18.4-beta",
|
"dockerImage": "git.tx1138.com/lfg2025/lnd:v0.18.4-beta",
|
||||||
"repoUrl": "https://github.com/lightningnetwork/lnd",
|
"repoUrl": "https://github.com/lightningnetwork/lnd",
|
||||||
"category": "money",
|
"requires": ["bitcoin-knots"]
|
||||||
"tier": "core"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "btcpay-server",
|
"id": "btcpay-server", "title": "BTCPay Server", "version": "1.13.7",
|
||||||
"title": "BTCPay Server",
|
"description": "Self-hosted Bitcoin payment processor.",
|
||||||
"version": "1.13.7",
|
|
||||||
"description": "Self-hosted Bitcoin payment processor. Accept Bitcoin payments without intermediaries or fees.",
|
|
||||||
"icon": "/assets/img/app-icons/btcpay-server.png",
|
"icon": "/assets/img/app-icons/btcpay-server.png",
|
||||||
"author": "BTCPay Server Foundation",
|
"author": "BTCPay Server Foundation", "category": "commerce", "tier": "core",
|
||||||
"dockerImage": "btcpayserver:1.13.7",
|
"dockerImage": "git.tx1138.com/lfg2025/btcpayserver:1.13.7",
|
||||||
"repoUrl": "https://github.com/btcpayserver/btcpayserver",
|
"repoUrl": "https://github.com/btcpayserver/btcpayserver",
|
||||||
"category": "commerce",
|
"requires": ["bitcoin-knots"]
|
||||||
"tier": "core"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "mempool",
|
"id": "mempool", "title": "Mempool Explorer", "version": "3.0.0",
|
||||||
"title": "Mempool Explorer",
|
"description": "Self-hosted Bitcoin blockchain and mempool visualizer.",
|
||||||
"version": "3.0.0",
|
|
||||||
"description": "Self-hosted Bitcoin blockchain and mempool visualizer. Monitor transactions without revealing your addresses.",
|
|
||||||
"icon": "/assets/img/app-icons/mempool.webp",
|
"icon": "/assets/img/app-icons/mempool.webp",
|
||||||
"author": "Mempool",
|
"author": "Mempool", "category": "money", "tier": "core",
|
||||||
"dockerImage": "mempool-frontend:v3.0.0",
|
"dockerImage": "git.tx1138.com/lfg2025/mempool-frontend:v3.0.0",
|
||||||
"repoUrl": "https://github.com/mempool/mempool",
|
"repoUrl": "https://github.com/mempool/mempool",
|
||||||
"category": "money",
|
"requires": ["bitcoin-knots", "electrumx"]
|
||||||
"tier": "core"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "electrumx",
|
"id": "electrumx", "title": "ElectrumX", "version": "1.18.0",
|
||||||
"title": "ElectrumX",
|
"description": "Electrum protocol server. Index the blockchain for fast wallet lookups.",
|
||||||
"version": "1.18.0",
|
|
||||||
"description": "Electrum protocol server. Index the blockchain for fast wallet lookups, privately.",
|
|
||||||
"icon": "/assets/img/app-icons/electrumx.webp",
|
"icon": "/assets/img/app-icons/electrumx.webp",
|
||||||
"author": "Luke Childs",
|
"author": "Luke Childs", "category": "money", "tier": "core",
|
||||||
"dockerImage": "electrumx:v1.18.0",
|
"dockerImage": "git.tx1138.com/lfg2025/electrumx:v1.18.0",
|
||||||
"repoUrl": "https://github.com/spesmilo/electrumx",
|
"repoUrl": "https://github.com/spesmilo/electrumx",
|
||||||
"category": "money",
|
"requires": ["bitcoin-knots"]
|
||||||
"tier": "core"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "indeedhub",
|
"id": "indeedhub", "title": "IndeeHub", "version": "1.0.0",
|
||||||
"title": "IndeeHub",
|
"description": "Bitcoin documentary streaming with Nostr identity.",
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "Bitcoin documentary streaming with Nostr identity. Stream sovereignty content from your node.",
|
|
||||||
"icon": "/assets/img/app-icons/indeedhub.png",
|
"icon": "/assets/img/app-icons/indeedhub.png",
|
||||||
"author": "IndeeHub Team",
|
"author": "IndeeHub", "category": "community",
|
||||||
"dockerImage": "indeedhub:1.0.0",
|
"dockerImage": "git.tx1138.com/lfg2025/indeedhub:1.0.0",
|
||||||
"repoUrl": "https://github.com/indeedhub/indeedhub",
|
"repoUrl": "https://github.com/indeedhub/indeedhub"
|
||||||
"category": "community"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "botfights",
|
"id": "botfights", "title": "BotFights", "version": "1.1.0",
|
||||||
"title": "BotFights",
|
"description": "Bot arena + 2-player arcade fighter with controller support and Adventure Mode.",
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "Bot arena + 2-player arcade fighter with controller support.",
|
|
||||||
"icon": "/assets/img/app-icons/botfights.svg",
|
"icon": "/assets/img/app-icons/botfights.svg",
|
||||||
"author": "BotFights",
|
"author": "BotFights", "category": "community",
|
||||||
"dockerImage": "botfights:1.1.0",
|
"dockerImage": "git.tx1138.com/lfg2025/botfights:1.1.0",
|
||||||
"repoUrl": "https://botfights.net",
|
"repoUrl": "https://botfights.net"
|
||||||
"category": "community"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "gitea",
|
"id": "gitea", "title": "Gitea", "version": "1.23",
|
||||||
"title": "Gitea",
|
"description": "Self-hosted Git service with container registry, CI/CD, issue tracking.",
|
||||||
"version": "1.23",
|
|
||||||
"description": "Self-hosted Git service with container registry, CI/CD, issue tracking, and package hosting.",
|
|
||||||
"icon": "/assets/img/app-icons/gitea.svg",
|
"icon": "/assets/img/app-icons/gitea.svg",
|
||||||
"author": "Gitea",
|
"author": "Gitea", "category": "development",
|
||||||
"dockerImage": "docker.io/gitea/gitea:1.23",
|
"dockerImage": "docker.io/gitea/gitea:1.23",
|
||||||
"repoUrl": "https://gitea.com",
|
"repoUrl": "https://gitea.com"
|
||||||
"category": "development"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "filebrowser",
|
"id": "filebrowser", "title": "File Browser", "version": "2.27.0",
|
||||||
"title": "File Browser",
|
"description": "Web-based file manager.",
|
||||||
"version": "2.27.0",
|
|
||||||
"description": "Web-based file manager. Browse, upload, and manage files on your server.",
|
|
||||||
"icon": "/assets/img/app-icons/file-browser.webp",
|
"icon": "/assets/img/app-icons/file-browser.webp",
|
||||||
"author": "File Browser",
|
"author": "File Browser", "category": "data", "tier": "core",
|
||||||
"dockerImage": "filebrowser:v2.27.0",
|
"dockerImage": "git.tx1138.com/lfg2025/filebrowser:v2.27.0",
|
||||||
"repoUrl": "https://github.com/filebrowser/filebrowser",
|
"repoUrl": "https://github.com/filebrowser/filebrowser"
|
||||||
"category": "data",
|
|
||||||
"tier": "core"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "vaultwarden",
|
"id": "vaultwarden", "title": "Vaultwarden", "version": "1.30.0",
|
||||||
"title": "Vaultwarden",
|
"description": "Self-hosted password vault with zero-knowledge encryption.",
|
||||||
"version": "1.30.0",
|
|
||||||
"description": "Self-hosted password vault. Bitwarden-compatible with zero-knowledge encryption.",
|
|
||||||
"icon": "/assets/img/app-icons/vaultwarden.webp",
|
"icon": "/assets/img/app-icons/vaultwarden.webp",
|
||||||
"author": "Vaultwarden",
|
"author": "Vaultwarden", "category": "data", "tier": "recommended",
|
||||||
"dockerImage": "vaultwarden:1.30.0-alpine",
|
"dockerImage": "git.tx1138.com/lfg2025/vaultwarden:1.30.0-alpine",
|
||||||
"repoUrl": "https://github.com/dani-garcia/vaultwarden",
|
"repoUrl": "https://github.com/dani-garcia/vaultwarden"
|
||||||
"category": "data",
|
|
||||||
"tier": "recommended"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "searxng",
|
"id": "searxng", "title": "SearXNG", "version": "2024.1.0",
|
||||||
"title": "SearXNG",
|
"description": "Privacy-respecting metasearch engine.",
|
||||||
"version": "2024.1.0",
|
|
||||||
"description": "Privacy-respecting metasearch engine. Search the internet without being tracked.",
|
|
||||||
"icon": "/assets/img/app-icons/searxng.png",
|
"icon": "/assets/img/app-icons/searxng.png",
|
||||||
"author": "SearXNG",
|
"author": "SearXNG", "category": "data", "tier": "recommended",
|
||||||
"dockerImage": "searxng:latest",
|
"dockerImage": "git.tx1138.com/lfg2025/searxng:latest",
|
||||||
"repoUrl": "https://github.com/searxng/searxng",
|
"repoUrl": "https://github.com/searxng/searxng"
|
||||||
"category": "data",
|
|
||||||
"tier": "recommended"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "nostr-rs-relay",
|
"id": "fedimint", "title": "Fedimint", "version": "0.10.0",
|
||||||
"title": "Nostr Relay",
|
"description": "Federated Bitcoin mint with privacy through federated guardians.",
|
||||||
"version": "0.9.0",
|
|
||||||
"description": "Your own Nostr relay. Store events locally, relay for friends, publish over Tor.",
|
|
||||||
"icon": "/assets/img/app-icons/nostr-rs-relay.svg",
|
|
||||||
"author": "scsiblade",
|
|
||||||
"dockerImage": "nostr-rs-relay:0.9.0",
|
|
||||||
"repoUrl": "https://sr.ht/~gheartsfield/nostr-rs-relay/",
|
|
||||||
"category": "nostr"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "fedimint",
|
|
||||||
"title": "Fedimint",
|
|
||||||
"version": "0.10.0",
|
|
||||||
"description": "Federated Bitcoin mint. Private, scalable Bitcoin through federated guardians.",
|
|
||||||
"icon": "/assets/img/app-icons/fedimint.png",
|
"icon": "/assets/img/app-icons/fedimint.png",
|
||||||
"author": "Fedimint",
|
"author": "Fedimint", "category": "money",
|
||||||
"dockerImage": "fedimintd:v0.10.0",
|
"dockerImage": "git.tx1138.com/lfg2025/fedimintd:v0.10.0",
|
||||||
"repoUrl": "https://github.com/fedimint/fedimint",
|
"repoUrl": "https://github.com/fedimint/fedimint"
|
||||||
"category": "money"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ollama",
|
"id": "ollama", "title": "Ollama", "version": "0.5.4",
|
||||||
"title": "Ollama",
|
"description": "Run AI models locally. Private and on your hardware.",
|
||||||
"version": "0.5.4",
|
|
||||||
"description": "Run AI models locally. Llama, Mistral, and more \u2014 on your hardware, completely private.",
|
|
||||||
"icon": "/assets/img/app-icons/ollama.png",
|
"icon": "/assets/img/app-icons/ollama.png",
|
||||||
"author": "Ollama",
|
"author": "Ollama", "category": "data",
|
||||||
"dockerImage": "ollama:latest",
|
"dockerImage": "git.tx1138.com/lfg2025/ollama:latest",
|
||||||
"repoUrl": "https://github.com/ollama/ollama",
|
"repoUrl": "https://github.com/ollama/ollama"
|
||||||
"category": "data"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "nextcloud",
|
"id": "nextcloud", "title": "Nextcloud", "version": "28",
|
||||||
"title": "Nextcloud",
|
"description": "Your own private cloud. File sync, calendars, contacts.",
|
||||||
"version": "28",
|
|
||||||
"description": "Your own private cloud. File sync, calendars, contacts \u2014 all on your hardware.",
|
|
||||||
"icon": "/assets/img/app-icons/nextcloud.webp",
|
"icon": "/assets/img/app-icons/nextcloud.webp",
|
||||||
"author": "Nextcloud",
|
"author": "Nextcloud", "category": "data",
|
||||||
"dockerImage": "nextcloud:28",
|
"dockerImage": "git.tx1138.com/lfg2025/nextcloud:28",
|
||||||
"repoUrl": "https://github.com/nextcloud/server",
|
"repoUrl": "https://github.com/nextcloud/server"
|
||||||
"category": "data"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "jellyfin",
|
"id": "jellyfin", "title": "Jellyfin", "version": "10.8.13",
|
||||||
"title": "Jellyfin",
|
"description": "Free media server. Stream movies, music, and photos.",
|
||||||
"version": "10.8.13",
|
|
||||||
"description": "Free media server. Stream your movies, music, and photos to any device.",
|
|
||||||
"icon": "/assets/img/app-icons/jellyfin.webp",
|
"icon": "/assets/img/app-icons/jellyfin.webp",
|
||||||
"author": "Jellyfin",
|
"author": "Jellyfin", "category": "data",
|
||||||
"dockerImage": "jellyfin:10.8.13",
|
"dockerImage": "git.tx1138.com/lfg2025/jellyfin:10.8.13",
|
||||||
"repoUrl": "https://github.com/jellyfin/jellyfin",
|
"repoUrl": "https://github.com/jellyfin/jellyfin"
|
||||||
"category": "data"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "immich",
|
"id": "immich", "title": "Immich", "version": "1.90.0",
|
||||||
"title": "Immich",
|
"description": "High-performance photo and video backup with ML.",
|
||||||
"version": "1.90.0",
|
|
||||||
"description": "High-performance photo and video backup. Mobile-first with ML features.",
|
|
||||||
"icon": "/assets/img/app-icons/immich.png",
|
"icon": "/assets/img/app-icons/immich.png",
|
||||||
"author": "Immich",
|
"author": "Immich", "category": "data",
|
||||||
"dockerImage": "immich-server:release",
|
"dockerImage": "git.tx1138.com/lfg2025/immich-server:release",
|
||||||
"repoUrl": "https://github.com/immich-app/immich",
|
"repoUrl": "https://github.com/immich-app/immich"
|
||||||
"category": "data"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "homeassistant",
|
"id": "homeassistant", "title": "Home Assistant", "version": "2024.1",
|
||||||
"title": "Home Assistant",
|
"description": "Open-source home automation.",
|
||||||
"version": "2024.1",
|
|
||||||
"description": "Open-source home automation. Control smart home devices privately.",
|
|
||||||
"icon": "/assets/img/app-icons/homeassistant.png",
|
"icon": "/assets/img/app-icons/homeassistant.png",
|
||||||
"author": "Home Assistant",
|
"author": "Home Assistant", "category": "home",
|
||||||
"dockerImage": "home-assistant:2024.1",
|
"dockerImage": "git.tx1138.com/lfg2025/home-assistant:2024.1",
|
||||||
"repoUrl": "https://github.com/home-assistant/core",
|
"repoUrl": "https://github.com/home-assistant/core"
|
||||||
"category": "home"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "grafana",
|
"id": "grafana", "title": "Grafana", "version": "10.2.0",
|
||||||
"title": "Grafana",
|
"description": "Analytics and monitoring dashboards.",
|
||||||
"version": "10.2.0",
|
|
||||||
"description": "Analytics and monitoring platform. Dashboards for your node metrics.",
|
|
||||||
"icon": "/assets/img/app-icons/grafana.png",
|
"icon": "/assets/img/app-icons/grafana.png",
|
||||||
"author": "Grafana Labs",
|
"author": "Grafana Labs", "category": "data", "tier": "recommended",
|
||||||
"dockerImage": "grafana:10.2.0",
|
"dockerImage": "git.tx1138.com/lfg2025/grafana:10.2.0",
|
||||||
"repoUrl": "https://github.com/grafana/grafana",
|
"repoUrl": "https://github.com/grafana/grafana"
|
||||||
"category": "data",
|
|
||||||
"tier": "recommended"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "tailscale",
|
"id": "tailscale", "title": "Tailscale", "version": "1.78.0",
|
||||||
"title": "Tailscale",
|
"description": "Zero-config VPN with WireGuard mesh networking.",
|
||||||
"version": "1.78.0",
|
|
||||||
"description": "Zero-config VPN. Secure remote access with WireGuard mesh networking.",
|
|
||||||
"icon": "/assets/img/app-icons/tailscale.webp",
|
"icon": "/assets/img/app-icons/tailscale.webp",
|
||||||
"author": "Tailscale",
|
"author": "Tailscale", "category": "networking", "tier": "recommended",
|
||||||
"dockerImage": "tailscale:stable",
|
"dockerImage": "git.tx1138.com/lfg2025/tailscale:stable",
|
||||||
"repoUrl": "https://github.com/tailscale/tailscale",
|
"repoUrl": "https://github.com/tailscale/tailscale"
|
||||||
"category": "networking",
|
|
||||||
"tier": "recommended"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "penpot",
|
"id": "uptime-kuma", "title": "Uptime Kuma", "version": "1.23.0",
|
||||||
"title": "Penpot",
|
"description": "Self-hosted uptime monitoring.",
|
||||||
"version": "2.4",
|
|
||||||
"description": "Open-source design platform. Self-hosted alternative to Figma.",
|
|
||||||
"icon": "/assets/img/app-icons/penpot.webp",
|
|
||||||
"author": "Penpot",
|
|
||||||
"dockerImage": "penpot-frontend:2.4",
|
|
||||||
"repoUrl": "https://github.com/penpot/penpot",
|
|
||||||
"category": "data"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "photoprism",
|
|
||||||
"title": "PhotoPrism",
|
|
||||||
"version": "240915",
|
|
||||||
"description": "AI-powered photo management with facial recognition, privately.",
|
|
||||||
"icon": "/assets/img/app-icons/photoprism.svg",
|
|
||||||
"author": "PhotoPrism",
|
|
||||||
"dockerImage": "photoprism:240915",
|
|
||||||
"repoUrl": "https://github.com/photoprism/photoprism",
|
|
||||||
"category": "data"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "uptime-kuma",
|
|
||||||
"title": "Uptime Kuma",
|
|
||||||
"version": "1.23.0",
|
|
||||||
"description": "Self-hosted uptime monitoring. Track HTTP, TCP, DNS, and more.",
|
|
||||||
"icon": "/assets/img/app-icons/uptime-kuma.webp",
|
"icon": "/assets/img/app-icons/uptime-kuma.webp",
|
||||||
"author": "Uptime Kuma",
|
"author": "Uptime Kuma", "category": "data", "tier": "recommended",
|
||||||
"dockerImage": "uptime-kuma:1",
|
"dockerImage": "git.tx1138.com/lfg2025/uptime-kuma:1",
|
||||||
"repoUrl": "https://github.com/louislam/uptime-kuma",
|
"repoUrl": "https://github.com/louislam/uptime-kuma"
|
||||||
"category": "data",
|
|
||||||
"tier": "recommended"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "nostr-vpn",
|
"id": "dwn", "title": "Decentralized Web Node", "version": "0.4.0",
|
||||||
"title": "Nostr VPN",
|
"description": "Own your data with DID-based access control.",
|
||||||
"version": "0.3.7",
|
|
||||||
"description": "Tailscale-style mesh VPN with Nostr control plane.",
|
|
||||||
"icon": "/assets/img/app-icons/nostr-vpn.svg",
|
|
||||||
"author": "Martti Malmi",
|
|
||||||
"dockerImage": "nostr-vpn:v0.3.7",
|
|
||||||
"repoUrl": "https://github.com/mmalmi/nostr-vpn",
|
|
||||||
"category": "networking"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "fips",
|
|
||||||
"title": "FIPS",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"description": "Free Internetworking Peering System. Self-organizing encrypted mesh.",
|
|
||||||
"icon": "/assets/img/app-icons/fips.svg",
|
|
||||||
"author": "Jim Corgan",
|
|
||||||
"dockerImage": "fips:v0.1.0",
|
|
||||||
"repoUrl": "https://github.com/jmcorgan/fips",
|
|
||||||
"category": "networking"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "routstr",
|
|
||||||
"title": "Routstr",
|
|
||||||
"version": "0.4.3",
|
|
||||||
"description": "Decentralized AI inference proxy. Pay-per-request with Cashu ecash.",
|
|
||||||
"icon": "/assets/img/app-icons/routstr.svg",
|
|
||||||
"author": "Routstr",
|
|
||||||
"dockerImage": "routstr:v0.4.3",
|
|
||||||
"repoUrl": "https://github.com/routstr/routstr-core",
|
|
||||||
"category": "community"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "dwn",
|
|
||||||
"title": "Decentralized Web Node",
|
|
||||||
"version": "0.4.0",
|
|
||||||
"description": "Own your data with DID-based access control. Sync across devices.",
|
|
||||||
"icon": "/assets/img/app-icons/dwn.svg",
|
"icon": "/assets/img/app-icons/dwn.svg",
|
||||||
"author": "TBD",
|
"author": "TBD", "category": "data",
|
||||||
"dockerImage": "dwn-server:main",
|
"dockerImage": "git.tx1138.com/lfg2025/dwn-server:main",
|
||||||
"repoUrl": "https://github.com/TBD54566975/dwn-server",
|
"repoUrl": "https://github.com/TBD54566975/dwn-server"
|
||||||
"category": "data"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cryptpad",
|
"id": "endurain", "title": "Endurain", "version": "0.8.0",
|
||||||
"title": "CryptPad",
|
"description": "Self-hosted fitness tracking. Strava alternative.",
|
||||||
"version": "2024.12.0",
|
"icon": "/assets/img/app-icons/endurain.png",
|
||||||
"description": "End-to-end encrypted documents and collaboration. Zero-knowledge.",
|
"author": "Endurain", "category": "data",
|
||||||
"icon": "/assets/img/app-icons/cryptpad.webp",
|
"dockerImage": "git.tx1138.com/lfg2025/endurain:0.8.0",
|
||||||
"author": "XWiki SAS",
|
"repoUrl": "https://github.com/joaovitoriasilva/endurain"
|
||||||
"dockerImage": "cryptpad:2024.12.0",
|
|
||||||
"repoUrl": "https://github.com/cryptpad/cryptpad",
|
|
||||||
"category": "data"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "nostrudel",
|
"id": "photoprism", "title": "PhotoPrism", "version": "240915",
|
||||||
"title": "noStrudel",
|
"description": "AI-powered photo management with facial recognition.",
|
||||||
"version": "0.40.0",
|
"icon": "/assets/img/app-icons/photoprism.svg",
|
||||||
"description": "Feature-rich Nostr web client.",
|
"author": "PhotoPrism", "category": "data",
|
||||||
"icon": "/assets/img/app-icons/nostrudel.svg",
|
"dockerImage": "git.tx1138.com/lfg2025/photoprism:240915",
|
||||||
"author": "hzrd149",
|
"repoUrl": "https://github.com/photoprism/photoprism"
|
||||||
"dockerImage": "",
|
|
||||||
"repoUrl": "https://github.com/hzrd149/nostrudel",
|
|
||||||
"webUrl": "https://nostrudel.ninja",
|
|
||||||
"category": "nostr"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "nwnn",
|
|
||||||
"title": "Next Web News Network",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "Decentralized news aggregator.",
|
|
||||||
"icon": "/assets/img/app-icons/nwnn.png",
|
|
||||||
"author": "L484",
|
|
||||||
"dockerImage": "",
|
|
||||||
"webUrl": "https://nwnn.l484.com",
|
|
||||||
"category": "l484"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "484-kitchen",
|
|
||||||
"title": "484 Kitchen",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "K484 application platform.",
|
|
||||||
"icon": "/assets/img/app-icons/484-kitchen.png",
|
|
||||||
"author": "L484",
|
|
||||||
"dockerImage": "",
|
|
||||||
"webUrl": "https://484.kitchen",
|
|
||||||
"category": "l484"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "call-the-operator",
|
|
||||||
"title": "Call the Operator",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "Escape the Matrix.",
|
|
||||||
"icon": "/assets/img/app-icons/call-the-operator.png",
|
|
||||||
"author": "TX1138",
|
|
||||||
"dockerImage": "",
|
|
||||||
"webUrl": "https://cta.tx1138.com",
|
|
||||||
"category": "l484"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "arch-presentation",
|
|
||||||
"title": "Arch Presentation",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "The Future of Decentralized Infrastructure.",
|
|
||||||
"icon": "/assets/img/app-icons/arch-presentation.png",
|
|
||||||
"author": "L484",
|
|
||||||
"dockerImage": "",
|
|
||||||
"webUrl": "https://present.l484.com",
|
|
||||||
"category": "l484"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "syntropy-institute",
|
|
||||||
"title": "Syntropy Institute",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "Medicine Reimagined.",
|
|
||||||
"icon": "/assets/img/app-icons/syntropy-institute.png",
|
|
||||||
"author": "Syntropy Institute",
|
|
||||||
"dockerImage": "",
|
|
||||||
"webUrl": "https://syntropy.institute",
|
|
||||||
"category": "l484"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "t-zero",
|
|
||||||
"title": "T-0",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "Documentary series exploring decentralization.",
|
|
||||||
"icon": "/assets/img/app-icons/t-zero.png",
|
|
||||||
"author": "T-0",
|
|
||||||
"dockerImage": "",
|
|
||||||
"webUrl": "https://teeminuszero.net",
|
|
||||||
"category": "l484"
|
|
||||||
}
|
}
|
||||||
],
|
|
||||||
"registries": [
|
|
||||||
"23.182.128.160:3000/lfg2025",
|
|
||||||
"git.tx1138.com/lfg2025"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -284,12 +284,29 @@ async function handleSplashComplete() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { isOnboardingComplete } = await import('@/composables/useOnboarding')
|
const { checkOnboardingStatus } = await import('@/composables/useOnboarding')
|
||||||
const seenOnboarding = await isOnboardingComplete()
|
const seenOnboarding = await checkOnboardingStatus()
|
||||||
const destination = seenOnboarding ? '/login' : '/onboarding/intro'
|
if (seenOnboarding === true) {
|
||||||
router.push(destination).catch(() => {})
|
router.push('/login').catch(() => {})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (seenOnboarding === false) {
|
||||||
|
router.push('/onboarding/intro').catch(() => {})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Backend unreachable after retries. Prefer the localStorage
|
||||||
|
// cache on THIS browser (if a prior successful check set it) —
|
||||||
|
// otherwise defer to RootRedirect which polls + retries rather
|
||||||
|
// than forcing an already-onboarded user through the wizard.
|
||||||
|
if (localStorage.getItem('neode_onboarding_complete') === '1') {
|
||||||
|
router.push('/login').catch(() => {})
|
||||||
|
} else {
|
||||||
|
router.push('/').catch(() => {})
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
router.push('/onboarding/intro').catch(() => {})
|
// Do NOT default to /onboarding/intro here. RootRedirect has retry
|
||||||
|
// + polling + boot-screen handling; let it decide.
|
||||||
|
router.push('/').catch(() => {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -73,7 +73,6 @@ const APP_ICON_MAP: Record<string, string> = {
|
|||||||
fedimint: '/assets/img/app-icons/fedimint.png',
|
fedimint: '/assets/img/app-icons/fedimint.png',
|
||||||
mempool: '/assets/img/app-icons/mempool.webp',
|
mempool: '/assets/img/app-icons/mempool.webp',
|
||||||
electrs: '/assets/img/app-icons/electrs.svg',
|
electrs: '/assets/img/app-icons/electrs.svg',
|
||||||
'nostr-rs-relay': '/assets/img/app-icons/nostr-rs-relay.svg',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function goalAppIcons(goal: GoalDefinition): { appId: string; url: string }[] {
|
function goalAppIcons(goal: GoalDefinition): { appId: string; url: string }[] {
|
||||||
|
|||||||
@ -1,7 +1,28 @@
|
|||||||
/**
|
/**
|
||||||
* Login screen audio: intro loop (MP3) + transition sounds.
|
* Login screen audio: intro loop (MP3) + transition sounds.
|
||||||
|
*
|
||||||
|
* First-install vs returning-user gate: the synthwave loop, welcome
|
||||||
|
* voice, pop/whoosh/oomph transitions exist for the first-boot cinematic
|
||||||
|
* moment. After the user has completed onboarding we silence all of
|
||||||
|
* them — every subsequent login should be quiet. Typing sounds are
|
||||||
|
* exempt and continue to play regardless.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/** True when the node has not yet completed onboarding — i.e. we're
|
||||||
|
* still in the first-install cinematic. Reads the localStorage cache
|
||||||
|
* set by useOnboarding (which is re-seeded from the backend on each
|
||||||
|
* successful check), so this stays correct after a browser clear
|
||||||
|
* once the onboarding-complete probe runs. Sound calls that fire
|
||||||
|
* before that probe completes will fall through silent on an already-
|
||||||
|
* onboarded node — which is exactly what we want. */
|
||||||
|
function isFirstInstallPhase(): boolean {
|
||||||
|
try {
|
||||||
|
return localStorage.getItem('neode_onboarding_complete') !== '1'
|
||||||
|
} catch {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let audioContext: AudioContext | null = null
|
let audioContext: AudioContext | null = null
|
||||||
let introAudio: HTMLAudioElement | null = null
|
let introAudio: HTMLAudioElement | null = null
|
||||||
let introGain: GainNode | null = null
|
let introGain: GainNode | null = null
|
||||||
@ -30,6 +51,7 @@ const LOOP_START_URL = '/assets/audio/loop-start.mp3'
|
|||||||
/** Play loop-start when transitioning from typing intro to Welcome Noderunner, as the intro music comes in.
|
/** Play loop-start when transitioning from typing intro to Welcome Noderunner, as the intro music comes in.
|
||||||
* Uses Web Audio API so it plays after context is resumed (user gesture). */
|
* Uses Web Audio API so it plays after context is resumed (user gesture). */
|
||||||
export function playLoopStart() {
|
export function playLoopStart() {
|
||||||
|
if (!isFirstInstallPhase()) return
|
||||||
const ctx = getContext()
|
const ctx = getContext()
|
||||||
if (!ctx) return
|
if (!ctx) return
|
||||||
try {
|
try {
|
||||||
@ -62,6 +84,7 @@ export function resumeAudioContext() {
|
|||||||
|
|
||||||
/** Start intro loop - Cosmic Updrift. Only works after resumeAudioContext() (user gesture). */
|
/** Start intro loop - Cosmic Updrift. Only works after resumeAudioContext() (user gesture). */
|
||||||
export function startSynthwave() {
|
export function startSynthwave() {
|
||||||
|
if (!isFirstInstallPhase()) return
|
||||||
const ctxOrNull = getContext()
|
const ctxOrNull = getContext()
|
||||||
if (!ctxOrNull) return
|
if (!ctxOrNull) return
|
||||||
|
|
||||||
@ -123,6 +146,7 @@ export function stopAllAudio() {
|
|||||||
|
|
||||||
/** Pop sound - plays when intro initiator (tap to start) is pressed */
|
/** Pop sound - plays when intro initiator (tap to start) is pressed */
|
||||||
export function playPop() {
|
export function playPop() {
|
||||||
|
if (!isFirstInstallPhase()) return
|
||||||
const audio = new Audio('/assets/audio/pop.mp3')
|
const audio = new Audio('/assets/audio/pop.mp3')
|
||||||
audio.volume = 0.6
|
audio.volume = 0.6
|
||||||
audio.play().catch(() => {})
|
audio.play().catch(() => {})
|
||||||
@ -130,6 +154,7 @@ export function playPop() {
|
|||||||
|
|
||||||
/** Whoosh transition on successful login */
|
/** Whoosh transition on successful login */
|
||||||
export function playLoginSuccessWhoosh() {
|
export function playLoginSuccessWhoosh() {
|
||||||
|
if (!isFirstInstallPhase()) return
|
||||||
const woosh = new Audio('/assets/audio/woosh.mp3')
|
const woosh = new Audio('/assets/audio/woosh.mp3')
|
||||||
woosh.volume = 0.5
|
woosh.volume = 0.5
|
||||||
woosh.play().catch(() => {})
|
woosh.play().catch(() => {})
|
||||||
@ -169,6 +194,7 @@ const WELCOME_SPEECH_URL = '/assets/audio/welcome-noderunner.mp3'
|
|||||||
* ELEVENLABS_API_KEY=your_key node neode-ui/scripts/generate-welcome-speech.js
|
* ELEVENLABS_API_KEY=your_key node neode-ui/scripts/generate-welcome-speech.js
|
||||||
* Browse sci-fi voices at elevenlabs.io/voice-library and set ELEVENLABS_VOICE_ID for custom voice. */
|
* Browse sci-fi voices at elevenlabs.io/voice-library and set ELEVENLABS_VOICE_ID for custom voice. */
|
||||||
export function playWelcomeNoderunnerSpeech() {
|
export function playWelcomeNoderunnerSpeech() {
|
||||||
|
if (!isFirstInstallPhase()) return
|
||||||
const audio = new Audio(WELCOME_SPEECH_URL)
|
const audio = new Audio(WELCOME_SPEECH_URL)
|
||||||
audio.volume = 0.9
|
audio.volume = 0.9
|
||||||
audio.play().catch(() => {})
|
audio.play().catch(() => {})
|
||||||
@ -226,6 +252,7 @@ export function playKeyboardTypingSound() {
|
|||||||
|
|
||||||
/** Gaming-style boot thud - soft impact when dashboard loads */
|
/** Gaming-style boot thud - soft impact when dashboard loads */
|
||||||
export function playDashboardLoadOomph() {
|
export function playDashboardLoadOomph() {
|
||||||
|
if (!isFirstInstallPhase()) return
|
||||||
const ctx = getContext()
|
const ctx = getContext()
|
||||||
if (!ctx) return
|
if (!ctx) return
|
||||||
|
|
||||||
|
|||||||
@ -1,41 +1,63 @@
|
|||||||
/**
|
/**
|
||||||
* Onboarding state - prefers backend, falls back to localStorage for mock/offline.
|
* Onboarding state - backend is authoritative.
|
||||||
* Hardened: retries on 502/503, never blocks completion.
|
* "Unknown" (backend unreachable) must NEVER default to false —
|
||||||
|
* that would falsely send an already-onboarded user back through
|
||||||
|
* the intro after a browser clear / update / reboot.
|
||||||
*/
|
*/
|
||||||
import { rpcClient } from '@/api/rpc-client'
|
import { rpcClient } from '@/api/rpc-client'
|
||||||
|
|
||||||
async function callWithRetry<T>(fn: () => Promise<T>, maxRetries = 3): Promise<T | null> {
|
async function callWithRetry<T>(fn: () => Promise<T>, maxRetries = 5): Promise<T | null> {
|
||||||
for (let i = 0; i < maxRetries; i++) {
|
for (let i = 0; i < maxRetries; i++) {
|
||||||
try {
|
try {
|
||||||
return await fn()
|
return await fn()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const msg = e instanceof Error ? e.message : ''
|
const msg = e instanceof Error ? e.message : ''
|
||||||
const isRetryable = /502|503|timeout|fetch|network/i.test(msg)
|
const isRetryable = /502|503|504|timeout|fetch|network|abort/i.test(msg)
|
||||||
if (!isRetryable || i === maxRetries - 1) return null
|
if (!isRetryable || i === maxRetries - 1) return null
|
||||||
await new Promise((r) => setTimeout(r, 800 * (i + 1)))
|
// Exponential-ish backoff: 500, 1000, 2000, 4000, 8000 (capped)
|
||||||
|
const delay = Math.min(500 * Math.pow(2, i), 8000)
|
||||||
|
await new Promise((r) => setTimeout(r, delay))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function isOnboardingComplete(): Promise<boolean> {
|
/**
|
||||||
// Prefer the backend — localStorage gets stale across nodes (a
|
* Returns true/false if the backend gave a definitive answer, null if
|
||||||
// browser that onboarded node A would otherwise treat fresh node B
|
* the backend is unreachable. Callers MUST handle null explicitly —
|
||||||
// as already-onboarded and skip the wizard entirely). Only fall
|
* do not coerce to boolean without thinking about the consequences.
|
||||||
// back to localStorage if the backend is unreachable.
|
*/
|
||||||
const result = await callWithRetry(() => rpcClient.isOnboardingComplete(), 2)
|
export async function checkOnboardingStatus(): Promise<boolean | null> {
|
||||||
|
const result = await callWithRetry(() => rpcClient.isOnboardingComplete(), 5)
|
||||||
if (result !== null) {
|
if (result !== null) {
|
||||||
// Re-seed the localStorage cache so non-async consumers
|
|
||||||
// (OnboardingWrapper's useVideoBackground computed, etc.) see the
|
|
||||||
// right answer after the user clears site data on an already-
|
|
||||||
// onboarded node.
|
|
||||||
if (result) {
|
if (result) {
|
||||||
try { localStorage.setItem('neode_onboarding_complete', '1') } catch {}
|
try { localStorage.setItem('neode_onboarding_complete', '1') } catch {}
|
||||||
} else {
|
} else {
|
||||||
try { localStorage.removeItem('neode_onboarding_complete') } catch {}
|
try { localStorage.removeItem('neode_onboarding_complete') } catch {}
|
||||||
}
|
}
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Boolean-only variant for places that genuinely cannot wait.
|
||||||
|
* Backend answer wins; on backend-unreachable, trusts a prior
|
||||||
|
* localStorage cache (set by a past successful check on THIS node).
|
||||||
|
* Returns false only when both the backend and the cache agree —
|
||||||
|
* or when the cache is empty on a genuinely fresh install.
|
||||||
|
*
|
||||||
|
* Prefer checkOnboardingStatus() where possible so the caller can
|
||||||
|
* distinguish "confirmed fresh install" from "can't reach backend".
|
||||||
|
*/
|
||||||
|
export async function isOnboardingComplete(): Promise<boolean> {
|
||||||
|
const result = await checkOnboardingStatus()
|
||||||
|
if (result !== null) return result
|
||||||
|
// Backend unreachable — trust the local cache. If the cache says
|
||||||
|
// we're onboarded, we almost certainly are (this browser saw a
|
||||||
|
// prior backend 'true' and re-seeded the flag). If the cache is
|
||||||
|
// empty, we genuinely don't know; returning false here is the
|
||||||
|
// last-resort fallback, and the calling views should additionally
|
||||||
|
// keep polling the backend instead of treating this as gospel.
|
||||||
return localStorage.getItem('neode_onboarding_complete') === '1'
|
return localStorage.getItem('neode_onboarding_complete') === '1'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -311,18 +311,35 @@ router.beforeEach(async (to, _from, next) => {
|
|||||||
next()
|
next()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Check if this is a fresh install that needs onboarding
|
// Check if this is a fresh install that needs onboarding.
|
||||||
|
// Prefer checkOnboardingStatus() (tri-state) so we can distinguish
|
||||||
|
// "confirmed fresh install" from "backend unreachable". On the
|
||||||
|
// latter, send the user to RootRedirect (/) rather than the intro
|
||||||
|
// wizard — RootRedirect polls the backend and will route to
|
||||||
|
// /login once it answers, instead of forcing a re-onboarding.
|
||||||
try {
|
try {
|
||||||
const { isOnboardingComplete, getSavedOnboardingStep } = await import('@/composables/useOnboarding')
|
const { checkOnboardingStatus, getSavedOnboardingStep } = await import('@/composables/useOnboarding')
|
||||||
const setupDone = await isOnboardingComplete()
|
const setupDone = await checkOnboardingStatus()
|
||||||
if (!setupDone) {
|
if (setupDone === false) {
|
||||||
const step = getSavedOnboardingStep()
|
const step = getSavedOnboardingStep()
|
||||||
next(`/onboarding/${step}`)
|
next(`/onboarding/${step}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (setupDone === null) {
|
||||||
|
// Backend unreachable after retries — bounce through RootRedirect
|
||||||
|
// so it can keep polling and land the user on /login once the
|
||||||
|
// backend answers, instead of flashing the onboarding wizard.
|
||||||
|
const cached = localStorage.getItem('neode_onboarding_complete') === '1'
|
||||||
|
if (!cached) {
|
||||||
|
next('/')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Cached as onboarded — continue to the /login path below.
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// If we can't check, assume fresh install and show onboarding
|
// Unexpected error — do NOT default to onboarding. Hand off to
|
||||||
next('/onboarding/intro')
|
// RootRedirect which has retry + polling + boot-screen handling.
|
||||||
|
next('/')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
next({ path: '/login', query: { redirect: to.fullPath } })
|
next({ path: '/login', query: { redirect: to.fullPath } })
|
||||||
|
|||||||
@ -41,7 +41,6 @@ const PORT_TO_APP_ID: Record<string, string> = {
|
|||||||
'8334': 'bitcoin-knots',
|
'8334': 'bitcoin-knots',
|
||||||
'8888': 'searxng',
|
'8888': 'searxng',
|
||||||
'9000': 'portainer',
|
'9000': 'portainer',
|
||||||
'9001': 'penpot',
|
|
||||||
'9980': 'onlyoffice',
|
'9980': 'onlyoffice',
|
||||||
'11434': 'ollama',
|
'11434': 'ollama',
|
||||||
'2283': 'immich',
|
'2283': 'immich',
|
||||||
@ -51,7 +50,6 @@ const PORT_TO_APP_ID: Record<string, string> = {
|
|||||||
'8175': 'fedimint',
|
'8175': 'fedimint',
|
||||||
'8176': 'fedimint-gateway',
|
'8176': 'fedimint-gateway',
|
||||||
'3100': 'dwn',
|
'3100': 'dwn',
|
||||||
'18081': 'nostr-rs-relay',
|
|
||||||
'7777': 'indeedhub',
|
'7777': 'indeedhub',
|
||||||
'50002': 'electrumx',
|
'50002': 'electrumx',
|
||||||
'3010': 'thunderhub',
|
'3010': 'thunderhub',
|
||||||
|
|||||||
@ -474,42 +474,6 @@ export const dummyApps: Record<string, PackageDataEntry> = {
|
|||||||
status: ServiceStatus.Running
|
status: ServiceStatus.Running
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'penpot': {
|
|
||||||
state: PackageState.Running,
|
|
||||||
'static-files': {
|
|
||||||
license: 'MPL-2.0',
|
|
||||||
instructions: 'Design and prototyping platform',
|
|
||||||
icon: '/assets/img/penpot.webp'
|
|
||||||
},
|
|
||||||
manifest: {
|
|
||||||
id: 'penpot',
|
|
||||||
title: 'Penpot',
|
|
||||||
version: '2.0.0',
|
|
||||||
description: {
|
|
||||||
short: 'Open-source design and prototyping platform',
|
|
||||||
long: 'Penpot is an open-source design and prototyping platform for teams. Create designs, prototypes, and collaborate in real-time. Self-hosted alternative to Figma.'
|
|
||||||
},
|
|
||||||
'release-notes': 'Initial release',
|
|
||||||
license: 'MPL-2.0',
|
|
||||||
'wrapper-repo': 'https://github.com/penpot/penpot',
|
|
||||||
'upstream-repo': 'https://github.com/penpot/penpot',
|
|
||||||
'support-site': 'https://github.com/penpot/penpot/issues',
|
|
||||||
'marketing-site': 'https://penpot.app',
|
|
||||||
'donation-url': null
|
|
||||||
},
|
|
||||||
installed: {
|
|
||||||
'current-dependents': {},
|
|
||||||
'current-dependencies': {},
|
|
||||||
'last-backup': null,
|
|
||||||
'interface-addresses': {
|
|
||||||
main: {
|
|
||||||
'tor-address': 'penpot.onion',
|
|
||||||
'lan-address': 'http://localhost:9001'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
status: ServiceStatus.Running
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'indeedhub': {
|
'indeedhub': {
|
||||||
state: PackageState.Running,
|
state: PackageState.Running,
|
||||||
'static-files': {
|
'static-files': {
|
||||||
|
|||||||
@ -178,7 +178,6 @@ const APP_ICON_MAP: Record<string, string> = {
|
|||||||
fedimint: '/assets/img/app-icons/fedimint.png',
|
fedimint: '/assets/img/app-icons/fedimint.png',
|
||||||
mempool: '/assets/img/app-icons/mempool.webp',
|
mempool: '/assets/img/app-icons/mempool.webp',
|
||||||
electrs: '/assets/img/app-icons/electrs.svg',
|
electrs: '/assets/img/app-icons/electrs.svg',
|
||||||
'nostr-rs-relay': '/assets/img/app-icons/nostr-rs-relay.svg',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function stepIconUrl(step: GoalStep): string | undefined {
|
function stepIconUrl(step: GoalStep): string | undefined {
|
||||||
|
|||||||
@ -99,7 +99,6 @@ const launchableApps = computed<KioskApp[]>(() => {
|
|||||||
'filebrowser': '/app/filebrowser/',
|
'filebrowser': '/app/filebrowser/',
|
||||||
'searxng': '/app/searxng/',
|
'searxng': '/app/searxng/',
|
||||||
'ollama': '/app/ollama/',
|
'ollama': '/app/ollama/',
|
||||||
'penpot': '/app/penpot/',
|
|
||||||
'onlyoffice': '/app/onlyoffice/',
|
'onlyoffice': '/app/onlyoffice/',
|
||||||
'portainer': '/app/portainer/',
|
'portainer': '/app/portainer/',
|
||||||
'uptime-kuma': '/app/uptime-kuma/',
|
'uptime-kuma': '/app/uptime-kuma/',
|
||||||
@ -108,7 +107,6 @@ const launchableApps = computed<KioskApp[]>(() => {
|
|||||||
'fedimint': '/app/fedimint/',
|
'fedimint': '/app/fedimint/',
|
||||||
'fedimint-gateway': '/app/fedimint-gateway/',
|
'fedimint-gateway': '/app/fedimint-gateway/',
|
||||||
'dwn': '/app/dwn/',
|
'dwn': '/app/dwn/',
|
||||||
'nostr-rs-relay': '/app/nostr-rs-relay/',
|
|
||||||
'indeedhub': 'http://localhost:8190',
|
'indeedhub': 'http://localhost:8190',
|
||||||
'botfights': 'http://localhost:9100',
|
'botfights': 'http://localhost:9100',
|
||||||
'nwnn': 'https://nwnn.l484.com',
|
'nwnn': 'https://nwnn.l484.com',
|
||||||
|
|||||||
@ -50,11 +50,14 @@ async function quickHealthCheck(): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function checkOnboarded(): Promise<boolean> {
|
async function checkOnboarded(): Promise<boolean> {
|
||||||
|
// No hard timeout here. isOnboardingComplete() already retries with
|
||||||
|
// backoff (see useOnboarding.ts). A 3s Promise.race that resolves to
|
||||||
|
// `false` on timeout was previously the main cause of the intro
|
||||||
|
// flashing on already-onboarded nodes after browser-clear / reboot /
|
||||||
|
// update: if the backend was slow to warm up, we'd force a 'false'
|
||||||
|
// and route the user back through the setup wizard.
|
||||||
try {
|
try {
|
||||||
const result = await Promise.race([
|
const result = await isOnboardingComplete()
|
||||||
isOnboardingComplete(),
|
|
||||||
new Promise<boolean>((resolve) => setTimeout(() => resolve(false), 3000)),
|
|
||||||
])
|
|
||||||
log('checkOnboarded', { result })
|
log('checkOnboarded', { result })
|
||||||
return result
|
return result
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@ -34,7 +34,6 @@ export const ROUTE_TO_PACKAGE_KEY: Record<string, string> = {
|
|||||||
searxng: 'searxng',
|
searxng: 'searxng',
|
||||||
ollama: 'ollama',
|
ollama: 'ollama',
|
||||||
onlyoffice: 'onlyoffice',
|
onlyoffice: 'onlyoffice',
|
||||||
penpot: 'penpot',
|
|
||||||
nextcloud: 'nextcloud',
|
nextcloud: 'nextcloud',
|
||||||
vaultwarden: 'vaultwarden',
|
vaultwarden: 'vaultwarden',
|
||||||
jellyfin: 'jellyfin',
|
jellyfin: 'jellyfin',
|
||||||
@ -79,7 +78,6 @@ export const APP_URLS: Record<string, { dev: string; prod: string }> = {
|
|||||||
'ollama': { dev: 'http://localhost:11434', prod: 'http://localhost:11434' },
|
'ollama': { dev: 'http://localhost:11434', prod: 'http://localhost:11434' },
|
||||||
'searxng': { dev: 'http://localhost:8888', prod: 'http://localhost:8888' },
|
'searxng': { dev: 'http://localhost:8888', prod: 'http://localhost:8888' },
|
||||||
'onlyoffice': { dev: 'http://localhost:9980', prod: 'http://localhost:9980' },
|
'onlyoffice': { dev: 'http://localhost:9980', prod: 'http://localhost:9980' },
|
||||||
'penpot': { dev: 'http://localhost:9001', prod: 'http://localhost:9001' },
|
|
||||||
'nextcloud': { dev: 'http://localhost:8085', prod: 'http://localhost:8085' },
|
'nextcloud': { dev: 'http://localhost:8085', prod: 'http://localhost:8085' },
|
||||||
'vaultwarden': { dev: 'http://localhost:8082', prod: 'http://localhost:8082' },
|
'vaultwarden': { dev: 'http://localhost:8082', prod: 'http://localhost:8082' },
|
||||||
'jellyfin': { dev: 'http://localhost:8096', prod: 'http://localhost:8096' },
|
'jellyfin': { dev: 'http://localhost:8096', prod: 'http://localhost:8096' },
|
||||||
|
|||||||
@ -24,7 +24,6 @@ export const APP_PORTS: Record<string, number> = {
|
|||||||
'searxng': 8888,
|
'searxng': 8888,
|
||||||
'ollama': 11434,
|
'ollama': 11434,
|
||||||
'onlyoffice': 8044,
|
'onlyoffice': 8044,
|
||||||
'penpot': 9001,
|
|
||||||
'nextcloud': 8085,
|
'nextcloud': 8085,
|
||||||
'vaultwarden': 8082,
|
'vaultwarden': 8082,
|
||||||
'jellyfin': 8096,
|
'jellyfin': 8096,
|
||||||
@ -38,10 +37,6 @@ export const APP_PORTS: Record<string, number> = {
|
|||||||
'fedimint': 8175,
|
'fedimint': 8175,
|
||||||
'fedimintd': 8175,
|
'fedimintd': 8175,
|
||||||
'fedimint-gateway': 8176,
|
'fedimint-gateway': 8176,
|
||||||
'nostr-rs-relay': 18081,
|
|
||||||
'nostr-vpn': 8201,
|
|
||||||
'fips': 8202,
|
|
||||||
'routstr': 8200,
|
|
||||||
'indeedhub': 7778,
|
'indeedhub': 7778,
|
||||||
'botfights': 9100,
|
'botfights': 9100,
|
||||||
'gitea': 3000,
|
'gitea': 3000,
|
||||||
@ -87,14 +82,10 @@ export const HTTPS_PROXY_PATHS: Record<string, string> = {
|
|||||||
'dwn': '/app/dwn/',
|
'dwn': '/app/dwn/',
|
||||||
'btcpay-server': '/app/btcpay/',
|
'btcpay-server': '/app/btcpay/',
|
||||||
'nextcloud': '/app/nextcloud/',
|
'nextcloud': '/app/nextcloud/',
|
||||||
'penpot': '/app/penpot/',
|
|
||||||
'grafana': '/app/grafana/',
|
'grafana': '/app/grafana/',
|
||||||
'indeedhub': '/app/indeedhub/',
|
'indeedhub': '/app/indeedhub/',
|
||||||
'botfights': '/app/botfights/',
|
'botfights': '/app/botfights/',
|
||||||
'gitea': '/app/gitea/',
|
'gitea': '/app/gitea/',
|
||||||
'routstr': '/app/routstr/',
|
|
||||||
'nostr-vpn': '/app/nostr-vpn/',
|
|
||||||
'fips': '/app/fips/',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** External HTTPS apps -- always loaded directly */
|
/** External HTTPS apps -- always loaded directly */
|
||||||
@ -112,9 +103,8 @@ export const APP_TITLES: Record<string, string> = {
|
|||||||
'bitcoin-knots': 'Bitcoin Knots', 'bitcoin-core': 'Bitcoin Core',
|
'bitcoin-knots': 'Bitcoin Knots', 'bitcoin-core': 'Bitcoin Core',
|
||||||
'btcpay-server': 'BTCPay Server', 'indeedhub': 'Indeehub',
|
'btcpay-server': 'BTCPay Server', 'indeedhub': 'Indeehub',
|
||||||
'botfights': 'BotFights', 'gitea': 'Gitea', '484-kitchen': '484 Kitchen', 'arch-presentation': 'Presentation',
|
'botfights': 'BotFights', 'gitea': 'Gitea', '484-kitchen': '484 Kitchen', 'arch-presentation': 'Presentation',
|
||||||
'nostr-vpn': 'Nostr VPN', 'fips': 'FIPS', 'routstr': 'Routstr',
|
|
||||||
'homeassistant': 'Home Assistant', 'uptime-kuma': 'Uptime Kuma',
|
'homeassistant': 'Home Assistant', 'uptime-kuma': 'Uptime Kuma',
|
||||||
'nginx-proxy-manager': 'Nginx Proxy Manager', 'nostr-rs-relay': 'Nostr Relay',
|
'nginx-proxy-manager': 'Nginx Proxy Manager',
|
||||||
'call-the-operator': 'Call The Operator', 'syntropy-institute': 'Syntropy Institute',
|
'call-the-operator': 'Call The Operator', 'syntropy-institute': 'Syntropy Institute',
|
||||||
't-zero': 'T-Zero', 'nostrudel': 'noStrudel',
|
't-zero': 'T-Zero', 'nostrudel': 'noStrudel',
|
||||||
}
|
}
|
||||||
@ -128,12 +118,10 @@ export const NEW_TAB_APPS = new Set([
|
|||||||
'vaultwarden',
|
'vaultwarden',
|
||||||
'nextcloud',
|
'nextcloud',
|
||||||
'uptime-kuma',
|
'uptime-kuma',
|
||||||
'penpot',
|
|
||||||
'portainer',
|
'portainer',
|
||||||
'onlyoffice',
|
'onlyoffice',
|
||||||
'nginx-proxy-manager',
|
'nginx-proxy-manager',
|
||||||
'tailscale',
|
'tailscale',
|
||||||
'routstr',
|
|
||||||
])
|
])
|
||||||
|
|
||||||
/** Sites known to block iframes -- skip the timeout and go straight to fallback */
|
/** Sites known to block iframes -- skip the timeout and go straight to fallback */
|
||||||
|
|||||||
@ -15,7 +15,7 @@ export interface SelectedIdentity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isIdentityAwareApp(id: string): boolean {
|
function isIdentityAwareApp(id: string): boolean {
|
||||||
return id === 'indeedhub' || id === 'nostrudel' || id === 'routstr'
|
return id === 'indeedhub' || id === 'nostrudel'
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAppIdentity(
|
export function useAppIdentity(
|
||||||
|
|||||||
@ -8,11 +8,9 @@ import { PackageState, type PackageDataEntry } from '@/types/api'
|
|||||||
export const SERVICE_NAMES = new Set([
|
export const SERVICE_NAMES = new Set([
|
||||||
'dwn', 'archy-mempool-db', 'archy-btcpay-db', 'archy-nbxplorer', 'archy-tor',
|
'dwn', 'archy-mempool-db', 'archy-btcpay-db', 'archy-nbxplorer', 'archy-tor',
|
||||||
'immich_postgres', 'immich_redis',
|
'immich_postgres', 'immich_redis',
|
||||||
'penpot-postgres', 'penpot-valkey', 'penpot-backend', 'penpot-exporter',
|
|
||||||
'mysql-mempool', 'mempool-api', 'archy-mempool-web',
|
'mysql-mempool', 'mempool-api', 'archy-mempool-web',
|
||||||
'archy-bitcoin-ui', 'archy-lnd-ui', 'archy-electrs-ui',
|
'archy-bitcoin-ui', 'archy-lnd-ui', 'archy-electrs-ui',
|
||||||
'indeedhub-postgres', 'indeedhub-redis', 'indeedhub-minio',
|
'indeedhub-postgres', 'indeedhub-redis', 'indeedhub-minio',
|
||||||
'archy-nostr-vpn-ui', 'archy-fips-ui',
|
|
||||||
'indeedhub-api', 'indeedhub-ffmpeg',
|
'indeedhub-api', 'indeedhub-ffmpeg',
|
||||||
'indeedhub-relay', 'indeedhub-build_api_1', 'indeedhub-build_ffmpeg-worker_1',
|
'indeedhub-relay', 'indeedhub-build_api_1', 'indeedhub-build_ffmpeg-worker_1',
|
||||||
'indeedhub-build_postgres_1', 'indeedhub-build_redis_1', 'indeedhub-build_minio_1',
|
'indeedhub-build_postgres_1', 'indeedhub-build_redis_1', 'indeedhub-build_minio_1',
|
||||||
@ -39,8 +37,7 @@ export const APP_CATEGORY_MAP: Record<string, string> = {
|
|||||||
'nextcloud': 'data', 'vaultwarden': 'data', 'filebrowser': 'data', 'cryptpad': 'data',
|
'nextcloud': 'data', 'vaultwarden': 'data', 'filebrowser': 'data', 'cryptpad': 'data',
|
||||||
'homeassistant': 'home', 'lorabell': 'home', 'endurain': 'home',
|
'homeassistant': 'home', 'lorabell': 'home', 'endurain': 'home',
|
||||||
'searxng': 'community', 'ollama': 'community', 'grafana': 'data',
|
'searxng': 'community', 'ollama': 'community', 'grafana': 'data',
|
||||||
'nostr-rs-relay': 'nostr', 'nostrudel': 'nostr',
|
'nostrudel': 'nostr',
|
||||||
'nostr-vpn': 'networking', 'fips': 'networking', 'routstr': 'community',
|
|
||||||
'tailscale': 'networking', 'nginx-proxy-manager': 'networking', 'portainer': 'networking',
|
'tailscale': 'networking', 'nginx-proxy-manager': 'networking', 'portainer': 'networking',
|
||||||
'uptime-kuma': 'networking', 'dwn': 'data',
|
'uptime-kuma': 'networking', 'dwn': 'data',
|
||||||
'botfights': 'community', 'nwnn': 'l484', '484-kitchen': 'l484',
|
'botfights': 'community', 'nwnn': 'l484', '484-kitchen': 'l484',
|
||||||
@ -99,7 +96,7 @@ export const WEB_ONLY_APPS: Record<string, PackageDataEntry> = {
|
|||||||
export const TAB_LAUNCH_APPS = new Set([
|
export const TAB_LAUNCH_APPS = new Set([
|
||||||
'btcpay-server', 'grafana', 'photoprism', 'homeassistant',
|
'btcpay-server', 'grafana', 'photoprism', 'homeassistant',
|
||||||
'vaultwarden', 'nextcloud', 'uptime-kuma', 'portainer',
|
'vaultwarden', 'nextcloud', 'uptime-kuma', 'portainer',
|
||||||
'cryptpad', 'nginx-proxy-manager', 'tailscale', 'routstr',
|
'cryptpad', 'nginx-proxy-manager', 'tailscale',
|
||||||
])
|
])
|
||||||
|
|
||||||
export function opensInTab(id: string): boolean {
|
export function opensInTab(id: string): boolean {
|
||||||
|
|||||||
@ -86,7 +86,6 @@ export function getCuratedAppList(): MarketplaceApp[] {
|
|||||||
{ id: 'searxng', title: 'SearXNG', version: '2024.1.0', description: 'Privacy-respecting metasearch engine. Search the internet without being tracked or profiled.', icon: '/assets/img/app-icons/searxng.png', author: 'SearXNG', dockerImage: `${R}/searxng:latest`, repoUrl: 'https://github.com/searxng/searxng' },
|
{ id: 'searxng', title: 'SearXNG', version: '2024.1.0', description: 'Privacy-respecting metasearch engine. Search the internet without being tracked or profiled.', icon: '/assets/img/app-icons/searxng.png', author: 'SearXNG', dockerImage: `${R}/searxng:latest`, repoUrl: 'https://github.com/searxng/searxng' },
|
||||||
{ id: 'ollama', title: 'Ollama', version: '0.5.4', description: 'Run AI models locally. Llama, Mistral, and more — on your hardware, completely private.', icon: '/assets/img/app-icons/ollama.png', author: 'Ollama', dockerImage: `${R}/ollama:latest`, repoUrl: 'https://github.com/ollama/ollama' },
|
{ id: 'ollama', title: 'Ollama', version: '0.5.4', description: 'Run AI models locally. Llama, Mistral, and more — on your hardware, completely private.', icon: '/assets/img/app-icons/ollama.png', author: 'Ollama', dockerImage: `${R}/ollama:latest`, repoUrl: 'https://github.com/ollama/ollama' },
|
||||||
{ id: 'cryptpad', title: 'CryptPad', version: '2024.12.0', description: 'End-to-end encrypted documents, spreadsheets, and presentations. Zero-knowledge collaboration.', icon: '/assets/img/app-icons/cryptpad.webp', author: 'XWiki SAS', dockerImage: `${R}/cryptpad:2024.12.0`, repoUrl: 'https://github.com/cryptpad/cryptpad' },
|
{ id: 'cryptpad', title: 'CryptPad', version: '2024.12.0', description: 'End-to-end encrypted documents, spreadsheets, and presentations. Zero-knowledge collaboration.', icon: '/assets/img/app-icons/cryptpad.webp', author: 'XWiki SAS', dockerImage: `${R}/cryptpad:2024.12.0`, repoUrl: 'https://github.com/cryptpad/cryptpad' },
|
||||||
{ id: 'penpot', title: 'Penpot', version: '2.4', description: 'Open-source design platform. Self-hosted alternative to Figma for design and prototyping.', icon: '/assets/img/app-icons/penpot.webp', author: 'Penpot', dockerImage: `${R}/penpot-frontend:2.4`, repoUrl: 'https://github.com/penpot/penpot' },
|
|
||||||
{ id: 'nextcloud', title: 'Nextcloud', version: '28', description: 'Your own private cloud. File sync, calendars, contacts — all on your hardware.', icon: '/assets/img/app-icons/nextcloud.webp', author: 'Nextcloud', dockerImage: `${R}/nextcloud:28`, repoUrl: 'https://github.com/nextcloud/server' },
|
{ id: 'nextcloud', title: 'Nextcloud', version: '28', description: 'Your own private cloud. File sync, calendars, contacts — all on your hardware.', icon: '/assets/img/app-icons/nextcloud.webp', author: 'Nextcloud', dockerImage: `${R}/nextcloud:28`, repoUrl: 'https://github.com/nextcloud/server' },
|
||||||
{ id: 'vaultwarden', title: 'Vaultwarden', version: '1.30.0', description: 'Self-hosted password vault. Bitwarden-compatible with zero-knowledge encryption.', icon: '/assets/img/app-icons/vaultwarden.webp', author: 'Vaultwarden', dockerImage: `${R}/vaultwarden:1.30.0-alpine`, repoUrl: 'https://github.com/dani-garcia/vaultwarden' },
|
{ id: 'vaultwarden', title: 'Vaultwarden', version: '1.30.0', description: 'Self-hosted password vault. Bitwarden-compatible with zero-knowledge encryption.', icon: '/assets/img/app-icons/vaultwarden.webp', author: 'Vaultwarden', dockerImage: `${R}/vaultwarden:1.30.0-alpine`, repoUrl: 'https://github.com/dani-garcia/vaultwarden' },
|
||||||
{ id: 'jellyfin', title: 'Jellyfin', version: '10.8.13', description: 'Free media server. Stream your movies, music, and photos to any device.', icon: '/assets/img/app-icons/jellyfin.webp', author: 'Jellyfin', dockerImage: `${R}/jellyfin:10.8.13`, repoUrl: 'https://github.com/jellyfin/jellyfin' },
|
{ id: 'jellyfin', title: 'Jellyfin', version: '10.8.13', description: 'Free media server. Stream your movies, music, and photos to any device.', icon: '/assets/img/app-icons/jellyfin.webp', author: 'Jellyfin', dockerImage: `${R}/jellyfin:10.8.13`, repoUrl: 'https://github.com/jellyfin/jellyfin' },
|
||||||
@ -99,12 +98,8 @@ export function getCuratedAppList(): MarketplaceApp[] {
|
|||||||
{ id: 'tailscale', title: 'Tailscale', version: '1.78.0', description: 'Zero-config VPN. Secure remote access with WireGuard mesh networking.', icon: '/assets/img/app-icons/tailscale.webp', author: 'Tailscale', dockerImage: `${R}/tailscale:stable`, repoUrl: 'https://github.com/tailscale/tailscale' },
|
{ id: 'tailscale', title: 'Tailscale', version: '1.78.0', description: 'Zero-config VPN. Secure remote access with WireGuard mesh networking.', icon: '/assets/img/app-icons/tailscale.webp', author: 'Tailscale', dockerImage: `${R}/tailscale:stable`, repoUrl: 'https://github.com/tailscale/tailscale' },
|
||||||
{ id: 'electrumx', title: 'ElectrumX', version: '1.18.0', description: 'Electrum protocol server. Index the blockchain for fast wallet lookups, privately.', icon: '/assets/img/app-icons/electrumx.webp', author: 'Luke Childs', dockerImage: `${R}/electrumx:v1.18.0`, repoUrl: 'https://github.com/spesmilo/electrumx' },
|
{ id: 'electrumx', title: 'ElectrumX', version: '1.18.0', description: 'Electrum protocol server. Index the blockchain for fast wallet lookups, privately.', icon: '/assets/img/app-icons/electrumx.webp', author: 'Luke Childs', dockerImage: `${R}/electrumx:v1.18.0`, repoUrl: 'https://github.com/spesmilo/electrumx' },
|
||||||
{ id: 'fedimint', title: 'Fedimint', version: '0.10.0', description: 'Federated Bitcoin mint. Private, scalable Bitcoin through federated guardians.', icon: '/assets/img/app-icons/fedimint.png', author: 'Fedimint', dockerImage: `${R}/fedimintd:v0.10.0`, repoUrl: 'https://github.com/fedimint/fedimint' },
|
{ id: 'fedimint', title: 'Fedimint', version: '0.10.0', description: 'Federated Bitcoin mint. Private, scalable Bitcoin through federated guardians.', icon: '/assets/img/app-icons/fedimint.png', author: 'Fedimint', dockerImage: `${R}/fedimintd:v0.10.0`, repoUrl: 'https://github.com/fedimint/fedimint' },
|
||||||
{ id: 'nostr-rs-relay', title: 'Nostr Relay', version: '0.9.0', category: 'nostr', description: 'Your own Nostr relay. Store events locally, relay for friends, publish over Tor.', icon: '/assets/img/app-icons/nostr-rs-relay.svg', author: 'scsiblade', dockerImage: `${R}/nostr-rs-relay:0.9.0`, repoUrl: 'https://sr.ht/~gheartsfield/nostr-rs-relay/' },
|
|
||||||
{ id: 'indeedhub', title: 'Indeehub', version: '1.0.0', description: 'Bitcoin documentary streaming with Nostr identity. Stream sovereignty content.', icon: '/assets/img/app-icons/indeedhub.png', author: 'Indeehub Team', dockerImage: `${R}/indeedhub:1.0.0`, repoUrl: 'https://github.com/indeedhub/indeedhub' },
|
{ id: 'indeedhub', title: 'Indeehub', version: '1.0.0', description: 'Bitcoin documentary streaming with Nostr identity. Stream sovereignty content.', icon: '/assets/img/app-icons/indeedhub.png', author: 'Indeehub Team', dockerImage: `${R}/indeedhub:1.0.0`, repoUrl: 'https://github.com/indeedhub/indeedhub' },
|
||||||
{ id: 'dwn', title: 'Decentralized Web Node', version: '0.4.0', description: 'Own your data with DID-based access control. Sync across devices, sovereign.', icon: '/assets/img/app-icons/dwn.svg', author: 'TBD', dockerImage: `${R}/dwn-server:main`, repoUrl: 'https://github.com/TBD54566975/dwn-server' },
|
{ id: 'dwn', title: 'Decentralized Web Node', version: '0.4.0', description: 'Own your data with DID-based access control. Sync across devices, sovereign.', icon: '/assets/img/app-icons/dwn.svg', author: 'TBD', dockerImage: `${R}/dwn-server:main`, repoUrl: 'https://github.com/TBD54566975/dwn-server' },
|
||||||
{ id: 'nostr-vpn', title: 'Nostr VPN', version: '0.3.7', category: 'networking', description: 'Tailscale-style mesh VPN with Nostr control plane. Peer discovery and key exchange over relays, WireGuard tunnels.', icon: '/assets/img/app-icons/nostr-vpn.svg', author: 'Martti Malmi', dockerImage: `${R}/nostr-vpn:v0.3.7`, repoUrl: 'https://github.com/mmalmi/nostr-vpn' },
|
|
||||||
{ id: 'fips', title: 'FIPS', version: '0.1.0', category: 'networking', description: 'Free Internetworking Peering System. Self-organizing encrypted mesh network with Nostr identity.', icon: '/assets/img/app-icons/fips.svg', author: 'Jim Corgan', dockerImage: `${R}/fips:v0.1.0`, repoUrl: 'https://github.com/jmcorgan/fips' },
|
|
||||||
{ id: 'routstr', title: 'Routstr', version: '0.4.3', category: 'community', description: 'Decentralized AI inference proxy. Pay-per-request with Cashu ecash, provider discovery via Nostr.', icon: '/assets/img/app-icons/routstr.svg', author: 'Routstr', dockerImage: `${R}/routstr:v0.4.3`, repoUrl: 'https://github.com/routstr/routstr-core' },
|
|
||||||
{ id: 'nostrudel', title: 'noStrudel', version: '0.40.0', category: 'nostr', description: 'Feature-rich Nostr web client. Browse feeds, post notes, manage relays with NIP-07.', icon: '/assets/img/app-icons/nostrudel.svg', author: 'hzrd149', dockerImage: '', repoUrl: 'https://github.com/hzrd149/nostrudel', webUrl: 'https://nostrudel.ninja' },
|
{ id: 'nostrudel', title: 'noStrudel', version: '0.40.0', category: 'nostr', description: 'Feature-rich Nostr web client. Browse feeds, post notes, manage relays with NIP-07.', icon: '/assets/img/app-icons/nostrudel.svg', author: 'hzrd149', dockerImage: '', repoUrl: 'https://github.com/hzrd149/nostrudel', webUrl: 'https://nostrudel.ninja' },
|
||||||
{ id: 'botfights', title: 'BotFights', version: '1.0.0', category: 'community', description: 'Bot arena + 2-player arcade fighter with controller support. AI bots battle in trivia, humans duke it out with controllers.', icon: '/assets/img/app-icons/botfights.svg', author: 'BotFights', dockerImage: `${R}/botfights:1.1.0`, repoUrl: 'https://botfights.net' },
|
{ id: 'botfights', title: 'BotFights', version: '1.0.0', category: 'community', description: 'Bot arena + 2-player arcade fighter with controller support. AI bots battle in trivia, humans duke it out with controllers.', icon: '/assets/img/app-icons/botfights.svg', author: 'BotFights', dockerImage: `${R}/botfights:1.1.0`, repoUrl: 'https://botfights.net' },
|
||||||
{ id: 'gitea', title: 'Gitea', version: '1.23', category: 'development', description: 'Self-hosted Git service with container registry, CI/CD, issue tracking, and package hosting.', icon: '/assets/img/app-icons/gitea.svg', author: 'Gitea', dockerImage: 'docker.io/gitea/gitea:1.23', repoUrl: 'https://gitea.com' },
|
{ id: 'gitea', title: 'Gitea', version: '1.23', category: 'development', description: 'Self-hosted Git service with container registry, CI/CD, issue tracking, and package hosting.', icon: '/assets/img/app-icons/gitea.svg', author: 'Gitea', dockerImage: 'docker.io/gitea/gitea:1.23', repoUrl: 'https://gitea.com' },
|
||||||
@ -139,9 +134,6 @@ export const INSTALLED_ALIASES: Record<string, string[]> = {
|
|||||||
tailscale: ['tailscale'],
|
tailscale: ['tailscale'],
|
||||||
ollama: ['ollama'],
|
ollama: ['ollama'],
|
||||||
indeedhub: ['indeedhub'],
|
indeedhub: ['indeedhub'],
|
||||||
'nostr-vpn': ['nostr-vpn'],
|
|
||||||
fips: ['fips'],
|
|
||||||
routstr: ['routstr'],
|
|
||||||
botfights: ['botfights'],
|
botfights: ['botfights'],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -54,9 +54,6 @@ export const INSTALLED_ALIASES: Record<string, string[]> = {
|
|||||||
filebrowser: ['filebrowser'],
|
filebrowser: ['filebrowser'],
|
||||||
tailscale: ['tailscale'],
|
tailscale: ['tailscale'],
|
||||||
ollama: ['ollama'],
|
ollama: ['ollama'],
|
||||||
'nostr-vpn': ['nostr-vpn'],
|
|
||||||
fips: ['fips'],
|
|
||||||
routstr: ['routstr'],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Get app tier classification (matches backend get_app_tier) */
|
/** Get app tier classification (matches backend get_app_tier) */
|
||||||
@ -240,17 +237,6 @@ export function getCuratedAppList(): MarketplaceApp[] {
|
|||||||
manifestUrl: undefined,
|
manifestUrl: undefined,
|
||||||
repoUrl: 'https://github.com/cryptpad/cryptpad'
|
repoUrl: 'https://github.com/cryptpad/cryptpad'
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'penpot',
|
|
||||||
title: 'Penpot',
|
|
||||||
version: '2.4',
|
|
||||||
description: 'Open-source design and prototyping platform. Self-hosted alternative to Figma.',
|
|
||||||
icon: '/assets/img/app-icons/penpot.webp',
|
|
||||||
author: 'Penpot',
|
|
||||||
dockerImage: 'docker.io/penpotapp/frontend:2.4',
|
|
||||||
manifestUrl: undefined,
|
|
||||||
repoUrl: 'https://github.com/penpot/penpot'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'nextcloud',
|
id: 'nextcloud',
|
||||||
title: 'Nextcloud',
|
title: 'Nextcloud',
|
||||||
@ -405,42 +391,6 @@ export function getCuratedAppList(): MarketplaceApp[] {
|
|||||||
manifestUrl: undefined,
|
manifestUrl: undefined,
|
||||||
repoUrl: 'https://github.com/TBD54566975/dwn-server'
|
repoUrl: 'https://github.com/TBD54566975/dwn-server'
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'nostr-vpn',
|
|
||||||
title: 'Nostr VPN',
|
|
||||||
version: '0.3.7',
|
|
||||||
category: 'networking',
|
|
||||||
description: 'Tailscale-style mesh VPN with Nostr control plane. Peer discovery and key exchange over relays, WireGuard tunnels.',
|
|
||||||
icon: '/assets/img/app-icons/nostr-vpn.svg',
|
|
||||||
author: 'Martti Malmi',
|
|
||||||
dockerImage: `${REGISTRY}/nostr-vpn:v0.3.7`,
|
|
||||||
manifestUrl: undefined,
|
|
||||||
repoUrl: 'https://github.com/mmalmi/nostr-vpn'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'fips',
|
|
||||||
title: 'FIPS',
|
|
||||||
version: '0.1.0',
|
|
||||||
category: 'networking',
|
|
||||||
description: 'Free Internetworking Peering System. Self-organizing encrypted mesh network with Nostr identity.',
|
|
||||||
icon: '/assets/img/app-icons/fips.svg',
|
|
||||||
author: 'Jim Corgan',
|
|
||||||
dockerImage: `${REGISTRY}/fips:v0.1.0`,
|
|
||||||
manifestUrl: undefined,
|
|
||||||
repoUrl: 'https://github.com/jmcorgan/fips'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'routstr',
|
|
||||||
title: 'Routstr',
|
|
||||||
version: '0.4.3',
|
|
||||||
category: 'community',
|
|
||||||
description: 'Decentralized AI inference proxy. Pay-per-request with Cashu ecash, provider discovery via Nostr.',
|
|
||||||
icon: '/assets/img/app-icons/routstr.svg',
|
|
||||||
author: 'Routstr',
|
|
||||||
dockerImage: `${REGISTRY}/routstr:v0.4.3`,
|
|
||||||
manifestUrl: undefined,
|
|
||||||
repoUrl: 'https://github.com/routstr/routstr-core'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'nostrudel',
|
id: 'nostrudel',
|
||||||
title: 'noStrudel',
|
title: 'noStrudel',
|
||||||
@ -454,18 +404,6 @@ export function getCuratedAppList(): MarketplaceApp[] {
|
|||||||
repoUrl: 'https://github.com/hzrd149/nostrudel',
|
repoUrl: 'https://github.com/hzrd149/nostrudel',
|
||||||
webUrl: 'https://nostrudel.ninja'
|
webUrl: 'https://nostrudel.ninja'
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'nostr-rs-relay',
|
|
||||||
title: 'Nostr Relay',
|
|
||||||
version: '0.9.0',
|
|
||||||
category: 'nostr',
|
|
||||||
description: 'Run your own Nostr relay. Store your events locally, relay for friends, and publish over Tor. A sovereign relay for your sovereign node.',
|
|
||||||
icon: '/assets/img/app-icons/nostr-rs-relay.svg',
|
|
||||||
author: 'scsiblade',
|
|
||||||
dockerImage: 'docker.io/scsibug/nostr-rs-relay:0.9.0',
|
|
||||||
manifestUrl: undefined,
|
|
||||||
repoUrl: 'https://sr.ht/~gheartsfield/nostr-rs-relay/'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'botfights',
|
id: 'botfights',
|
||||||
title: 'BotFights',
|
title: 'BotFights',
|
||||||
|
|||||||
@ -180,6 +180,18 @@ init()
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="overflow-y-auto flex-1 min-h-0 space-y-6 pr-1">
|
<div class="overflow-y-auto flex-1 min-h-0 space-y-6 pr-1">
|
||||||
|
<!-- v1.7.38-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.38-alpha</span>
|
||||||
|
<span class="text-xs text-white/40">Apr 22, 2026</span>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||||
|
<p>Signing in is quiet now. The intro music, welcome voice, and transition sounds belong to the first-boot cinematic and only play before you've finished onboarding — every login after that is silent. Typing sounds in the search bar and on the dashboard are unaffected.</p>
|
||||||
|
<p>Fixed a bug where clearing your browser cache, updating the node, or rebooting could bounce you back through the onboarding wizard even though your node was already fully set up. The node now self-heals: if your password is set, it knows you've been through onboarding and takes you straight to the login screen. No more starting over.</p>
|
||||||
|
<p>Trimmed the App Store. FIPS, Nostr Relay, Nostr VPN, Routstr, and Penpot have been removed from the catalog and their container images deleted from our registries. Your node's native FIPS transport is untouched — this is just the app-store entries going away.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<!-- v1.7.37-alpha -->
|
<!-- v1.7.37-alpha -->
|
||||||
<div>
|
<div>
|
||||||
<div class="flex items-center gap-2 mb-3">
|
<div class="flex items-center gap-2 mb-3">
|
||||||
|
|||||||
@ -65,13 +65,31 @@ if [ -z "$BACKEND_BINARY" ]; then
|
|||||||
BACKEND_BINARY="$PROJECT_ROOT/core/target/release/archipelago"
|
BACKEND_BINARY="$PROJECT_ROOT/core/target/release/archipelago"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Auto-detect frontend archive
|
# Auto-detect frontend archive.
|
||||||
|
# Layout: flat tarball (`./index.html`, `./assets/…`, `./aiui/…`) so the
|
||||||
|
# Rust updater can unpack it directly into /opt/archipelago/web-ui/.
|
||||||
|
# Using `-C web/dist neode-ui` would produce a `neode-ui/` prefix which
|
||||||
|
# breaks the installer and returns 403 on every fleet UI — see
|
||||||
|
# feedback_release_tarball_layout.md.
|
||||||
if [ -z "$FRONTEND_ARCHIVE" ]; then
|
if [ -z "$FRONTEND_ARCHIVE" ]; then
|
||||||
FRONTEND_DIST="$PROJECT_ROOT/web/dist/neode-ui"
|
FRONTEND_DIST="$PROJECT_ROOT/web/dist/neode-ui"
|
||||||
if [ -d "$FRONTEND_DIST" ]; then
|
if [ -d "$FRONTEND_DIST" ]; then
|
||||||
FRONTEND_ARCHIVE="/tmp/archipelago-frontend-${VERSION}.tar.gz"
|
FRONTEND_ARCHIVE="/tmp/archipelago-frontend-${VERSION}.tar.gz"
|
||||||
echo "Creating frontend archive from $FRONTEND_DIST..."
|
STAGING_DIR=$(mktemp -d -t archipelago-frontend.XXXXXX)
|
||||||
tar -czf "$FRONTEND_ARCHIVE" -C "$PROJECT_ROOT/web/dist" neode-ui
|
echo "Staging frontend archive in $STAGING_DIR..."
|
||||||
|
cp -r "$FRONTEND_DIST/." "$STAGING_DIR/"
|
||||||
|
# Bake AIUI in so fresh installs pick it up. OTA already
|
||||||
|
# carries-forward the existing aiui/ if the tarball lacks one
|
||||||
|
# (update.rs:922), but including it here makes the tarball
|
||||||
|
# the single source of truth instead of relying on a side-
|
||||||
|
# effect of the in-place swap.
|
||||||
|
if [ -d "$PROJECT_ROOT/demo/aiui" ] && [ -f "$PROJECT_ROOT/demo/aiui/index.html" ]; then
|
||||||
|
echo " Including AIUI from demo/aiui/"
|
||||||
|
cp -r "$PROJECT_ROOT/demo/aiui" "$STAGING_DIR/aiui"
|
||||||
|
fi
|
||||||
|
echo "Creating frontend archive $FRONTEND_ARCHIVE..."
|
||||||
|
tar -czf "$FRONTEND_ARCHIVE" -C "$STAGING_DIR" .
|
||||||
|
rm -rf "$STAGING_DIR"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@ -600,6 +600,13 @@ if [ "$LIVE" = true ]; then
|
|||||||
echo "$(timestamp) Building AIUI (source newer than dist or dist missing)..."
|
echo "$(timestamp) Building AIUI (source newer than dist or dist missing)..."
|
||||||
(cd "$AIUI_DIR" && VITE_BASE_PATH=/aiui/ pnpm build 2>&1 | tail -5) || echo "$(timestamp) ⚠️ AIUI build failed"
|
(cd "$AIUI_DIR" && VITE_BASE_PATH=/aiui/ pnpm build 2>&1 | tail -5) || echo "$(timestamp) ⚠️ AIUI build failed"
|
||||||
fi
|
fi
|
||||||
|
# Fallback: if the AIUI sibling checkout is missing, use the pre-built
|
||||||
|
# dist shipped in this repo at demo/aiui/. That path is what we ship in
|
||||||
|
# the release tarball too, so local-and-fleet-update stay consistent.
|
||||||
|
if [ ! -f "$AIUI_DIST/index.html" ] && [ -f "$PROJECT_DIR/demo/aiui/index.html" ]; then
|
||||||
|
echo "$(timestamp) AIUI sibling dist missing — using demo/aiui/ from repo"
|
||||||
|
AIUI_DIST="$PROJECT_DIR/demo/aiui"
|
||||||
|
fi
|
||||||
if [ -d "$AIUI_DIST" ] && [ -f "$AIUI_DIST/index.html" ]; then
|
if [ -d "$AIUI_DIST" ] && [ -f "$AIUI_DIST/index.html" ]; then
|
||||||
echo "$(timestamp) Deploying AIUI..."
|
echo "$(timestamp) Deploying AIUI..."
|
||||||
if true; then
|
if true; then
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user