Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd8a6cd9d7 | ||
|
|
ab96c97cb9 | ||
|
|
881779005a | ||
|
|
20bc9f250c | ||
|
|
87be717f40 | ||
|
|
75d147b69f | ||
|
|
edaece8716 | ||
|
|
ab27fb97f8 | ||
|
|
d736364ad7 | ||
|
|
e9898ead76 | ||
|
|
b25d41c5c6 | ||
|
|
32902d3891 |
@@ -1,5 +1,46 @@
|
||||
# Changelog
|
||||
|
||||
## v1.7.70-alpha (2026-05-19)
|
||||
|
||||
- NetBird is being corrected from the peer/client daemon image to the self-hosted NetBird control-plane stack with a launchable dashboard on port `8087`, a combined management/signal/relay server on `8086`, and STUN on UDP `3478`.
|
||||
- App sessions now always launch local apps through direct host ports and carry an explicit dashboard return target, so closing an iframe returns to the launching dashboard screen instead of falling through to browser history or a 404.
|
||||
- Mobile app launches ignore stale desktop panel state and route into the full app-session webview consistently.
|
||||
- The desktop sidebar now pins the logo/version at the top and controller/online/mode controls at the bottom, with only the navigation section scrolling on shorter screens.
|
||||
- Validation passed with catalog JSON checks, `scripts/image-versions.sh` syntax check, `npm run type-check`, `cargo fmt --all --check --manifest-path core/Cargo.toml`, and `cargo check -p archipelago --manifest-path core/Cargo.toml`.
|
||||
|
||||
## v1.7.69-alpha (2026-05-19)
|
||||
|
||||
- App installs now allow up to 10 minutes for the initial `package.install` RPC to return, matching slow container image pulls and preventing apps from disappearing from My Apps while the backend is still pulling or retrying mirrors.
|
||||
- Live diagnostics on `100.70.96.88` confirmed the Gitea install did not fail; the primary registry pull timed out after 300 seconds, the fallback mirror succeeded, and Gitea came up healthy on `3001` while the frontend had already timed out at 15 seconds.
|
||||
- Gitea and other Docker-image app installs now stay visible during slow registry pulls instead of being marked as failed by the browser before backend install progress can complete.
|
||||
- Gitea is now categorized as a known Data app in My Apps, so a running Gitea container appears with installed apps instead of being filtered into the Websites/Services split.
|
||||
- NetBird `0.71.2` is now available in the app catalog and fallback marketplace data as a recommended networking app using the official `docker.io/netbirdio/netbird:0.71.2` image.
|
||||
- NetBird installs get persistent state under `/var/lib/archipelago/netbird`, `NET_ADMIN`/`NET_RAW`, `/dev/net/tun`, `slirp4netns`, image-version pinning, backend metadata, and health checks through `netbird status`.
|
||||
- The Archipelago terminal now includes `nano` on new disk installs and ISO builds, and self-update installs it on existing nodes if it is missing.
|
||||
- Validation passed with catalog JSON checks, shell syntax checks, `npm run type-check`, `cargo fmt --all --check --manifest-path core/Cargo.toml`, and `cargo check -p archipelago --manifest-path core/Cargo.toml`.
|
||||
|
||||
## v1.7.68-alpha (2026-05-19)
|
||||
|
||||
- BTCPay Server now ships on the official `docker.io/btcpayserver/btcpayserver:2.3.9` image, fixing the plugin catalog crash caused by newer plugin dependency version metadata while preserving existing datadirs and Postgres databases.
|
||||
- BTCPay release and first-boot health checks no longer depend on `curl` inside the container; they use a bash TCP probe that works with the official image out of the box.
|
||||
- Host nginx now serves Nginx Proxy Manager HTTP-01 challenge files before the Archipelago SPA fallback and is marked as the default HTTP/HTTPS virtual host, so public proxy hosts can issue certificates without hijacking local API traffic.
|
||||
- Nginx Proxy Manager first-boot, runtime repair, and container-doctor paths now pre-create the ACME webroot, keep bind mounts owned by the rootless Archipelago user, and sync issued public proxy hosts into host nginx vhosts.
|
||||
- The Nginx Proxy Manager host-nginx sync now skips proxy hosts with missing certificate files and rolls back the generated nginx include if validation fails, preventing a bad certificate path from poisoning later nginx reloads.
|
||||
- App session close buttons now return to the previous dashboard screen when possible and otherwise fall back to My Apps, avoiding the 404 page after closing an app launched from an invalid or stale history entry.
|
||||
- System Update confirmation and mirror modals now teleport to the document body with a full-screen overlay, so they cover the whole app instead of only the right-hand dashboard panel.
|
||||
- Mobile app launches stay inside Archipelago's app-session webview and hide desktop-only new-tab launch affordances, including apps such as Home Assistant that previously looked like they would leave the mobile shell.
|
||||
- Live recovery on `100.70.96.88` upgraded only the `btcpay-server` container to `docker.io/btcpayserver/btcpayserver:2.3.9`, preserved the existing datadir and Postgres database, and confirmed the container is healthy after a pre-upgrade backup.
|
||||
- Public validation confirmed `spay.tx1138.com`/`www` redirect to BTCPay login over HTTPS and `sapien.tx1138.com`/`www` serve the L484 page over HTTPS using the issued Let's Encrypt certificates.
|
||||
|
||||
## v1.7.67-alpha (2026-05-18)
|
||||
|
||||
- Home dashboard status cards now keep the last known good system, VPN, Bitcoin, and FIPS values while route changes or transient RPC failures are in flight, avoiding false "not configured" or "not running" flashes.
|
||||
- Home, Web5 Monitoring, and the Monitoring page headline cards now share the same live system-stat snapshot for CPU, memory, disk, uptime, and load so the visible numbers agree across the UI.
|
||||
- Settings What's New is filled through `v1.7.67-alpha`, including the missing historical `v1.7.44-alpha` through `v1.7.66-alpha` entries.
|
||||
- Bitcoin/Knots/Core shell lifecycle specs now match the Rust app config memory policy: 8 GiB on normal hosts, 4 GiB on low-memory hosts, and pruned Knots uses a larger dbcache on hosts with enough RAM to improve IBD throughput.
|
||||
- ElectrumX/electrs shell lifecycle specs now match the 4 GiB memory policy used by the Rust app config, reducing drift between first boot, reconcile, and app lifecycle paths.
|
||||
- Live assessment of `100.70.96.88` identified the current IBD bottlenecks as CPU/thermal/I/O pressure rather than RAM exhaustion, with follow-up work planned for existing-node swap repair, kiosk Chromium CPU reduction, and reconcile failure cleanup.
|
||||
|
||||
## v1.7.66-alpha (2026-05-18)
|
||||
|
||||
- Nginx Proxy Manager stale-port repair now detects stopped or `Created` Podman records by inspecting `podman ps -a` port metadata, covering records where `podman port nginx-proxy-manager` returns no mapping until start.
|
||||
|
||||
@@ -52,13 +52,13 @@
|
||||
{
|
||||
"id": "btcpay-server",
|
||||
"title": "BTCPay Server",
|
||||
"version": "1.13.7",
|
||||
"version": "2.3.9",
|
||||
"description": "Self-hosted Bitcoin payment processor.",
|
||||
"icon": "/assets/img/app-icons/btcpay-server.png",
|
||||
"author": "BTCPay Server Foundation",
|
||||
"category": "commerce",
|
||||
"tier": "core",
|
||||
"dockerImage": "146.59.87.168:3000/lfg2025/btcpayserver:1.13.7",
|
||||
"dockerImage": "docker.io/btcpayserver/btcpayserver:2.3.9",
|
||||
"repoUrl": "https://github.com/btcpayserver/btcpayserver",
|
||||
"requires": [
|
||||
"bitcoin-knots"
|
||||
@@ -275,6 +275,23 @@
|
||||
"args": ["sh", "-c", "tailscaled --tun=userspace-networking & sleep 2; tailscale web --listen 0.0.0.0:8240 & wait"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "netbird",
|
||||
"title": "NetBird",
|
||||
"version": "0.71.2",
|
||||
"description": "Self-hosted WireGuard mesh VPN control plane with dashboard, embedded identity provider, management API, signal, relay, and STUN service.",
|
||||
"icon": "/assets/img/app-icons/netbird.svg",
|
||||
"author": "NetBird",
|
||||
"category": "networking",
|
||||
"tier": "recommended",
|
||||
"dockerImage": "docker.io/netbirdio/dashboard:v2.38.0",
|
||||
"repoUrl": "https://github.com/netbirdio/netbird",
|
||||
"containerConfig": {
|
||||
"ports": ["8087:80", "8086:80", "3478:3478/udp"],
|
||||
"volumes": ["/var/lib/archipelago/netbird:/var/lib/netbird"],
|
||||
"notes": "Installed as a two-container stack: netbird dashboard on 8087 and netbird-server control plane on 8086 plus UDP 3478. For production clients, publish a DNS name over HTTPS with gRPC/WebSocket routing."
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "uptime-kuma",
|
||||
"title": "Uptime Kuma",
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
app:
|
||||
id: btcpay-server
|
||||
name: BTCPay Server
|
||||
version: 1.13.7
|
||||
version: 2.3.9
|
||||
description: Self-hosted Bitcoin payment processor. Accept Bitcoin payments without intermediaries.
|
||||
|
||||
container:
|
||||
image: git.tx1138.com/lfg2025/btcpayserver:1.13.7
|
||||
image: docker.io/btcpayserver/btcpayserver:2.3.9
|
||||
pull_policy: if-not-present
|
||||
network: archy-net
|
||||
secret_env:
|
||||
|
||||
Generated
+1
-1
@@ -80,7 +80,7 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||
|
||||
[[package]]
|
||||
name = "archipelago"
|
||||
version = "1.7.66-alpha"
|
||||
version = "1.7.69-alpha"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"archipelago-container",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "archipelago"
|
||||
version = "1.7.66-alpha"
|
||||
version = "1.7.70-alpha"
|
||||
edition = "2021"
|
||||
description = "Archipelago Bitcoin Node OS - Native backend"
|
||||
authors = ["Archipelago Team"]
|
||||
|
||||
@@ -217,7 +217,7 @@ pub(super) fn get_app_capabilities(app_id: &str) -> Vec<String> {
|
||||
"--cap-add=DAC_OVERRIDE".to_string(),
|
||||
"--cap-add=NET_BIND_SERVICE".to_string(),
|
||||
],
|
||||
// Nostr VPN and FIPS: mesh networking daemons need TUN + NET_ADMIN
|
||||
// VPN/mesh daemons need TUN + NET_ADMIN.
|
||||
// Note: --device=/dev/net/tun is added separately in install.rs
|
||||
"nostr-vpn" | "fips" => vec![
|
||||
"--cap-add=NET_ADMIN".to_string(),
|
||||
@@ -262,7 +262,7 @@ pub(super) fn get_health_check_args(app_id: &str, _rpc_pass: &str) -> Vec<String
|
||||
"bitcoin" | "bitcoin-core" | "bitcoin-knots" => return vec![],
|
||||
"lnd" => ("lncli getinfo || exit 1", "30s", "3"),
|
||||
"btcpay-server" | "btcpayserver" => {
|
||||
("curl -sf http://localhost:49392/ || exit 1", "30s", "3")
|
||||
("bash -ec '</dev/tcp/127.0.0.1/49392'", "30s", "3")
|
||||
}
|
||||
"mempool-api" => (
|
||||
http_probe_cmd("http://localhost:8999/api/v1/backend-info"),
|
||||
@@ -389,6 +389,7 @@ pub(super) fn get_memory_limit(app_id: &str) -> &'static str {
|
||||
"nostr-rs-relay" | "nostr-relay" => "256m",
|
||||
"routstr" => "512m",
|
||||
"nostr-vpn" => "256m",
|
||||
"netbird" => "1g",
|
||||
"fips" => "256m",
|
||||
"nginx-proxy-manager" => "256m",
|
||||
// Databases
|
||||
@@ -494,6 +495,7 @@ pub(super) fn all_container_names(package_id: &str) -> Vec<String> {
|
||||
"indeedhub-ffmpeg".into(),
|
||||
"indeedhub".into(),
|
||||
],
|
||||
"netbird" => vec!["netbird".into(), "netbird-server".into()],
|
||||
"nostr-vpn" => vec![
|
||||
"nostr-vpn".into(),
|
||||
"archy-nostr-vpn".into(),
|
||||
@@ -582,6 +584,7 @@ pub(super) fn get_data_dirs_for_app(package_id: &str) -> Vec<String> {
|
||||
format!("{}/penpot-assets", base),
|
||||
format!("{}/penpot-postgres", base),
|
||||
],
|
||||
"netbird" => vec![format!("{}/netbird", base)],
|
||||
_ => vec![format!("{}/{}", base, package_id)],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,6 +241,9 @@ impl RpcHandler {
|
||||
if package_id == "indeedhub" {
|
||||
return self.install_indeedhub_stack().await;
|
||||
}
|
||||
if package_id == "netbird" {
|
||||
return self.install_netbird_stack().await;
|
||||
}
|
||||
|
||||
// Dependency checks. Prefer the scanner's cached package state so a
|
||||
// congested Podman API does not turn an already-running dependency into
|
||||
|
||||
@@ -874,6 +874,7 @@ async fn repair_before_package_start(container_name: &str) {
|
||||
}
|
||||
|
||||
async fn repair_nginx_proxy_manager_container() {
|
||||
repair_nginx_proxy_manager_dirs().await;
|
||||
if !nginx_proxy_manager_has_legacy_admin_port().await {
|
||||
cleanup_nginx_proxy_manager_ports().await;
|
||||
return;
|
||||
@@ -890,6 +891,27 @@ async fn repair_nginx_proxy_manager_container() {
|
||||
}
|
||||
}
|
||||
|
||||
async fn repair_nginx_proxy_manager_dirs() {
|
||||
let _ = tokio::process::Command::new("sudo")
|
||||
.args([
|
||||
"mkdir",
|
||||
"-p",
|
||||
"/var/lib/archipelago/nginx-proxy-manager/data/letsencrypt-acme-challenge/.well-known/acme-challenge",
|
||||
"/var/lib/archipelago/nginx-proxy-manager/letsencrypt",
|
||||
])
|
||||
.output()
|
||||
.await;
|
||||
let _ = tokio::process::Command::new("sudo")
|
||||
.args([
|
||||
"chown",
|
||||
"-R",
|
||||
"1000:1000",
|
||||
"/var/lib/archipelago/nginx-proxy-manager",
|
||||
])
|
||||
.output()
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn nginx_proxy_manager_has_legacy_admin_port() -> bool {
|
||||
if let Ok(output) = podman_control(&["port", "nginx-proxy-manager", "81/tcp"]).await {
|
||||
if output.status.success()
|
||||
@@ -927,12 +949,21 @@ async fn recreate_nginx_proxy_manager_container() -> Result<()> {
|
||||
.args([
|
||||
"mkdir",
|
||||
"-p",
|
||||
"/var/lib/archipelago/nginx-proxy-manager/data",
|
||||
"/var/lib/archipelago/nginx-proxy-manager/data/letsencrypt-acme-challenge/.well-known/acme-challenge",
|
||||
"/var/lib/archipelago/nginx-proxy-manager/letsencrypt",
|
||||
])
|
||||
.output()
|
||||
.await
|
||||
.context("failed to create nginx-proxy-manager data directories")?;
|
||||
let _ = tokio::process::Command::new("sudo")
|
||||
.args([
|
||||
"chown",
|
||||
"-R",
|
||||
"1000:1000",
|
||||
"/var/lib/archipelago/nginx-proxy-manager",
|
||||
])
|
||||
.output()
|
||||
.await;
|
||||
|
||||
let image = crate::container::image_versions::pinned_image_for_app("nginx-proxy-manager")
|
||||
.unwrap_or_else(|| "docker.io/jc21/nginx-proxy-manager:latest".to_string());
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
use crate::api::rpc::RpcHandler;
|
||||
use crate::data_model::InstallPhase;
|
||||
use anyhow::{Context, Result};
|
||||
use base64::Engine;
|
||||
use tracing::info;
|
||||
|
||||
use super::install::{install_log, patch_indeedhub_nostr_provider};
|
||||
@@ -310,6 +311,9 @@ fn mempool_stack_app_ids() -> &'static [&'static str] {
|
||||
|
||||
const REGISTRY: &str = "146.59.87.168:3000/lfg2025";
|
||||
|
||||
const NETBIRD_DASHBOARD_IMAGE: &str = "docker.io/netbirdio/dashboard:v2.38.0";
|
||||
const NETBIRD_SERVER_IMAGE: &str = "docker.io/netbirdio/netbird-server:0.71.2";
|
||||
|
||||
/// Pull an image with retry and exponential backoff (3 attempts).
|
||||
async fn pull_image_with_retry(image: &str) -> Result<()> {
|
||||
const MAX_ATTEMPTS: u32 = 3;
|
||||
@@ -587,7 +591,7 @@ impl RpcHandler {
|
||||
let images = [
|
||||
&format!("{}/postgres:15.17", REGISTRY),
|
||||
&format!("{}/nbxplorer:2.6.0", REGISTRY),
|
||||
&format!("{}/btcpayserver:1.13.7", REGISTRY),
|
||||
"docker.io/btcpayserver/btcpayserver:2.3.9",
|
||||
];
|
||||
self.set_install_phase("btcpay-server", InstallPhase::PullingImage)
|
||||
.await;
|
||||
@@ -743,7 +747,7 @@ impl RpcHandler {
|
||||
"BTCPAY_POSTGRES=User ID=btcpay;Password={};Host=archy-btcpay-db;Port=5432;Database=btcpay;Include Error Detail=true",
|
||||
db_pass
|
||||
),
|
||||
&format!("{}/btcpayserver:1.13.7", REGISTRY),
|
||||
"docker.io/btcpayserver/btcpayserver:2.3.9",
|
||||
])
|
||||
.output()
|
||||
.await
|
||||
@@ -1357,6 +1361,187 @@ impl RpcHandler {
|
||||
"message": "IndeedHub stack installed (7 containers)",
|
||||
}))
|
||||
}
|
||||
|
||||
/// Install self-hosted NetBird (dashboard + combined management/signal/relay server).
|
||||
pub(super) async fn install_netbird_stack(&self) -> Result<serde_json::Value> {
|
||||
if let Some(adopted) =
|
||||
adopt_stack_if_exists("netbird", "netbird", &["netbird", "netbird-server"]).await?
|
||||
{
|
||||
return Ok(adopted);
|
||||
}
|
||||
|
||||
install_log("INSTALL START: netbird stack (dashboard + server)").await;
|
||||
info!("Installing self-hosted NetBird stack");
|
||||
|
||||
self.set_install_phase("netbird", InstallPhase::PullingImage)
|
||||
.await;
|
||||
for (i, image) in [NETBIRD_DASHBOARD_IMAGE, NETBIRD_SERVER_IMAGE]
|
||||
.iter()
|
||||
.enumerate()
|
||||
{
|
||||
self.set_install_progress("netbird", i as u64, 2).await;
|
||||
pull_image_with_retry(image)
|
||||
.await
|
||||
.with_context(|| format!("Failed to pull NetBird image: {}", image))?;
|
||||
}
|
||||
self.set_install_progress("netbird", 2, 2).await;
|
||||
|
||||
for name in ["netbird", "netbird-server"] {
|
||||
let _ = tokio::process::Command::new("podman")
|
||||
.args(["rm", "-f", name])
|
||||
.status()
|
||||
.await;
|
||||
}
|
||||
let _ = tokio::process::Command::new("podman")
|
||||
.args(["network", "rm", "-f", "netbird-net"])
|
||||
.status()
|
||||
.await;
|
||||
|
||||
self.set_install_phase("netbird", InstallPhase::CreatingContainer)
|
||||
.await;
|
||||
|
||||
tokio::fs::create_dir_all("/var/lib/archipelago/netbird")
|
||||
.await
|
||||
.context("Failed to create NetBird data directory")?;
|
||||
|
||||
let host_ip = self.config.host_ip.clone();
|
||||
let dashboard_origin = format!("http://{}:8087", host_ip);
|
||||
let mgmt_origin = format!("http://{}:8086", host_ip);
|
||||
let relay_secret = read_or_generate_b64_secret("netbird-relay-auth-secret").await;
|
||||
let encryption_key = read_or_generate_b64_secret("netbird-store-encryption-key").await;
|
||||
let config = format!(
|
||||
r#"server:
|
||||
listenAddress: ":80"
|
||||
exposedAddress: "{mgmt_origin}"
|
||||
stunPorts:
|
||||
- 3478
|
||||
metricsPort: 9090
|
||||
healthcheckAddress: ":9000"
|
||||
logLevel: "info"
|
||||
logFile: "console"
|
||||
authSecret: "{relay_secret}"
|
||||
dataDir: "/var/lib/netbird"
|
||||
auth:
|
||||
issuer: "{mgmt_origin}/oauth2"
|
||||
localAuthDisabled: false
|
||||
signKeyRefreshEnabled: true
|
||||
dashboardRedirectURIs:
|
||||
- "{dashboard_origin}/nb-auth"
|
||||
- "{dashboard_origin}/nb-silent-auth"
|
||||
cliRedirectURIs:
|
||||
- "http://localhost:53000/"
|
||||
store:
|
||||
engine: "sqlite"
|
||||
encryptionKey: "{encryption_key}"
|
||||
"#
|
||||
);
|
||||
tokio::fs::write("/var/lib/archipelago/netbird/config.yaml", config)
|
||||
.await
|
||||
.context("Failed to write NetBird config.yaml")?;
|
||||
|
||||
let dashboard_env = format!(
|
||||
r#"NETBIRD_MGMT_API_ENDPOINT={mgmt_origin}
|
||||
NETBIRD_MGMT_GRPC_API_ENDPOINT={mgmt_origin}
|
||||
AUTH_AUDIENCE=netbird-dashboard
|
||||
AUTH_CLIENT_ID=netbird-dashboard
|
||||
AUTH_CLIENT_SECRET=
|
||||
AUTH_AUTHORITY={mgmt_origin}/oauth2
|
||||
USE_AUTH0=false
|
||||
AUTH_SUPPORTED_SCOPES=openid profile email groups
|
||||
AUTH_REDIRECT_URI=/nb-auth
|
||||
AUTH_SILENT_REDIRECT_URI=/nb-silent-auth
|
||||
NGINX_SSL_PORT=443
|
||||
LETSENCRYPT_DOMAIN=none
|
||||
"#
|
||||
);
|
||||
tokio::fs::write("/var/lib/archipelago/netbird/dashboard.env", dashboard_env)
|
||||
.await
|
||||
.context("Failed to write NetBird dashboard.env")?;
|
||||
|
||||
let _ = tokio::process::Command::new("podman")
|
||||
.args(["network", "create", "netbird-net"])
|
||||
.status()
|
||||
.await;
|
||||
|
||||
let mut server_cmd = tokio::process::Command::new("podman");
|
||||
server_cmd.args([
|
||||
"run",
|
||||
"-d",
|
||||
"--name",
|
||||
"netbird-server",
|
||||
"--network",
|
||||
"netbird-net",
|
||||
"--network-alias",
|
||||
"netbird-server",
|
||||
"--restart=unless-stopped",
|
||||
"-p",
|
||||
"8086:80",
|
||||
"-p",
|
||||
"3478:3478/udp",
|
||||
"-v",
|
||||
"/var/lib/archipelago/netbird/data:/var/lib/netbird",
|
||||
"-v",
|
||||
"/var/lib/archipelago/netbird/config.yaml:/etc/netbird/config.yaml:ro",
|
||||
NETBIRD_SERVER_IMAGE,
|
||||
"--config",
|
||||
"/etc/netbird/config.yaml",
|
||||
]);
|
||||
run_required_stack_command("netbird", "create server", &mut server_cmd).await?;
|
||||
|
||||
self.set_install_phase("netbird", InstallPhase::StartingContainer)
|
||||
.await;
|
||||
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
|
||||
|
||||
let mut dashboard_cmd = tokio::process::Command::new("podman");
|
||||
dashboard_cmd.args([
|
||||
"run",
|
||||
"-d",
|
||||
"--name",
|
||||
"netbird",
|
||||
"--network",
|
||||
"netbird-net",
|
||||
"--restart=unless-stopped",
|
||||
"-p",
|
||||
"8087:80",
|
||||
"--env-file",
|
||||
"/var/lib/archipelago/netbird/dashboard.env",
|
||||
NETBIRD_DASHBOARD_IMAGE,
|
||||
]);
|
||||
run_required_stack_command("netbird", "create dashboard", &mut dashboard_cmd).await?;
|
||||
|
||||
wait_for_stack_containers("netbird", &["netbird-server", "netbird"], 60).await?;
|
||||
|
||||
self.set_install_phase("netbird", InstallPhase::WaitingHealthy)
|
||||
.await;
|
||||
self.set_install_phase("netbird", InstallPhase::PostInstall)
|
||||
.await;
|
||||
self.set_install_phase("netbird", InstallPhase::Done).await;
|
||||
self.clear_install_progress("netbird").await;
|
||||
|
||||
install_log("INSTALL OK: netbird stack").await;
|
||||
info!("NetBird stack installed");
|
||||
Ok(serde_json::json!({
|
||||
"success": true,
|
||||
"package_id": "netbird",
|
||||
"message": "NetBird self-hosted stack installed",
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
async fn read_or_generate_b64_secret(name: &str) -> String {
|
||||
let path = format!("/var/lib/archipelago/secrets/{}", name);
|
||||
if let Ok(val) = tokio::fs::read_to_string(&path).await {
|
||||
let trimmed = val.trim().to_string();
|
||||
if !trimmed.is_empty() {
|
||||
return trimmed;
|
||||
}
|
||||
}
|
||||
let mut buf = [0u8; 32];
|
||||
rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut buf);
|
||||
let secret = base64::engine::general_purpose::STANDARD.encode(buf);
|
||||
let _ = tokio::fs::create_dir_all("/var/lib/archipelago/secrets").await;
|
||||
let _ = tokio::fs::write(&path, &secret).await;
|
||||
secret
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -61,6 +61,7 @@ impl DockerPackageScanner {
|
||||
"indeedhub-build_minio-init_1",
|
||||
"indeedhub-build_relay_1",
|
||||
"indeedhub-build_ffmpeg-worker_1",
|
||||
"netbird-server",
|
||||
"buildx_buildkit_default",
|
||||
];
|
||||
|
||||
@@ -281,7 +282,7 @@ fn get_app_tier(app_id: &str) -> &'static str {
|
||||
"uptime-kuma" => "recommended",
|
||||
"grafana" => "recommended",
|
||||
"searxng" => "recommended",
|
||||
"tailscale" => "recommended",
|
||||
"tailscale" | "netbird" => "recommended",
|
||||
"portainer" => "recommended",
|
||||
// Optional: everything else
|
||||
_ => "optional",
|
||||
@@ -479,6 +480,13 @@ fn get_app_metadata(app_id: &str) -> AppMetadata {
|
||||
repo: "https://github.com/tailscale/tailscale".to_string(),
|
||||
tier: "",
|
||||
},
|
||||
"netbird" => AppMetadata {
|
||||
title: "NetBird".to_string(),
|
||||
description: "Self-hosted WireGuard mesh VPN control plane and dashboard".to_string(),
|
||||
icon: "/assets/img/app-icons/netbird.svg".to_string(),
|
||||
repo: "https://github.com/netbirdio/netbird".to_string(),
|
||||
tier: "",
|
||||
},
|
||||
"indeedhub" | "indeehub" => AppMetadata {
|
||||
title: "IndeedHub".to_string(),
|
||||
description: "Decentralized media streaming platform".to_string(),
|
||||
|
||||
@@ -168,6 +168,8 @@ fn image_var_for_app(app_id: &str) -> Option<&'static str> {
|
||||
"nginx-proxy-manager" => Some("NPM_IMAGE"),
|
||||
"portainer" => Some("PORTAINER_IMAGE"),
|
||||
"tailscale" => Some("TAILSCALE_IMAGE"),
|
||||
"netbird" => Some("NETBIRD_DASHBOARD_IMAGE"),
|
||||
"netbird-server" => Some("NETBIRD_SERVER_IMAGE"),
|
||||
|
||||
// Fedimint
|
||||
"fedimint" | "fedimintd" => Some("FEDIMINT_IMAGE"),
|
||||
@@ -299,6 +301,10 @@ pub fn containers_for_stack(app_id: &str) -> Vec<(&'static str, &'static str)> {
|
||||
("penpot-exporter", "PENPOT_EXPORTER_IMAGE"),
|
||||
("penpot-frontend", "PENPOT_FRONTEND_IMAGE"),
|
||||
],
|
||||
"netbird" => vec![
|
||||
("netbird", "NETBIRD_DASHBOARD_IMAGE"),
|
||||
("netbird-server", "NETBIRD_SERVER_IMAGE"),
|
||||
],
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1169,7 +1169,7 @@
|
||||
<div class="card-header"><span class="card-name">btcpay-server</span><span class="card-badge badge-red">archy-net</span></div>
|
||||
<div class="card-desc">Self-hosted Bitcoin payment processor. Accept Bitcoin payments with invoices, checkout pages, and POS.</div>
|
||||
<div class="card-layman">Your own payment terminal for Bitcoin. Create invoices, get paid, no middleman taking a cut.</div>
|
||||
<div class="card-image">btcpayserver:1.13.7</div>
|
||||
<div class="card-image">btcpayserver:2.3.9</div>
|
||||
<div class="card-ports">Ports: <span>23000</span></div>
|
||||
<div class="card-details">
|
||||
<div class="detail-row"><span class="detail-label">Memory</span><span class="detail-value">1 GB</span></div>
|
||||
|
||||
@@ -337,6 +337,7 @@ RUN apt-get update && apt-get -y full-upgrade && apt-get install -y --no-install
|
||||
curl \
|
||||
git \
|
||||
vim-tiny \
|
||||
nano \
|
||||
ca-certificates \
|
||||
openssl \
|
||||
chrony \
|
||||
|
||||
@@ -179,6 +179,7 @@ chroot /mnt/archipelago apt-get install -y \
|
||||
wget \
|
||||
htop \
|
||||
vim-tiny \
|
||||
nano \
|
||||
ca-certificates \
|
||||
chrony
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ resolver 1.1.1.1 8.8.8.8 valid=300s ipv6=off;
|
||||
resolver_timeout 5s;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen 80 default_server;
|
||||
server_name _;
|
||||
|
||||
root /opt/archipelago/web-ui;
|
||||
@@ -23,6 +23,13 @@ server {
|
||||
add_header X-DNS-Prefetch-Control "off" always;
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https://*.basemaps.cartocdn.com https://tile.openstreetmap.org; font-src 'self' data:; connect-src 'self' ws: wss: http://$host:* https:; frame-src 'self' http://$host:* https:; frame-ancestors 'self'; base-uri 'self'; form-action 'self';" always;
|
||||
|
||||
# Serve Nginx Proxy Manager HTTP-01 challenge files before the SPA fallback.
|
||||
location ^~ /.well-known/acme-challenge/ {
|
||||
default_type text/plain;
|
||||
root /var/lib/archipelago/nginx-proxy-manager/data/letsencrypt-acme-challenge;
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
# AIUI SPA (Chat mode iframe) — SPA fallback for client-side routing
|
||||
location /aiui/ {
|
||||
try_files $uri $uri/ /aiui/index.html;
|
||||
@@ -908,7 +915,7 @@ server {
|
||||
|
||||
# HTTPS - required for PWA install (Add to Home Screen) from dev servers
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen 443 ssl default_server;
|
||||
server_name _;
|
||||
|
||||
ssl_certificate /etc/archipelago/ssl/archipelago.crt;
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "neode-ui",
|
||||
"version": "1.7.66-alpha",
|
||||
"version": "1.7.70-alpha",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "neode-ui",
|
||||
"version": "1.7.66-alpha",
|
||||
"version": "1.7.70-alpha",
|
||||
"dependencies": {
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@vue-leaflet/vue-leaflet": "^0.10.1",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "neode-ui",
|
||||
"private": true,
|
||||
"version": "1.7.66-alpha",
|
||||
"version": "1.7.70-alpha",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "./start-dev.sh",
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" role="img" aria-label="NetBird">
|
||||
<defs>
|
||||
<linearGradient id="g" x1="18" y1="14" x2="110" y2="116" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#24c8ff"/>
|
||||
<stop offset="1" stop-color="#3157ff"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="128" height="128" rx="28" fill="#071422"/>
|
||||
<path d="M28 72c16-30 39-46 72-50-11 13-18 26-21 40 10-1 19 1 29 5-19 4-35 13-48 27-8 8-18 12-30 12 7-7 12-14 15-22-7 0-13-4-17-12Z" fill="url(#g)"/>
|
||||
<circle cx="82" cy="43" r="6" fill="#fff" opacity=".95"/>
|
||||
<path d="M36 72c10 3 20 4 30 2" fill="none" stroke="#fff" stroke-width="6" stroke-linecap="round" opacity=".8"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 702 B |
@@ -52,13 +52,13 @@
|
||||
{
|
||||
"id": "btcpay-server",
|
||||
"title": "BTCPay Server",
|
||||
"version": "1.13.7",
|
||||
"version": "2.3.9",
|
||||
"description": "Self-hosted Bitcoin payment processor.",
|
||||
"icon": "/assets/img/app-icons/btcpay-server.png",
|
||||
"author": "BTCPay Server Foundation",
|
||||
"category": "commerce",
|
||||
"tier": "core",
|
||||
"dockerImage": "146.59.87.168:3000/lfg2025/btcpayserver:1.13.7",
|
||||
"dockerImage": "docker.io/btcpayserver/btcpayserver:2.3.9",
|
||||
"repoUrl": "https://github.com/btcpayserver/btcpayserver",
|
||||
"requires": [
|
||||
"bitcoin-knots"
|
||||
@@ -275,6 +275,23 @@
|
||||
"args": ["sh", "-c", "tailscaled --tun=userspace-networking & sleep 2; tailscale web --listen 0.0.0.0:8240 & wait"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "netbird",
|
||||
"title": "NetBird",
|
||||
"version": "0.71.2",
|
||||
"description": "Self-hosted WireGuard mesh VPN control plane with dashboard, embedded identity provider, management API, signal, relay, and STUN service.",
|
||||
"icon": "/assets/img/app-icons/netbird.svg",
|
||||
"author": "NetBird",
|
||||
"category": "networking",
|
||||
"tier": "recommended",
|
||||
"dockerImage": "docker.io/netbirdio/dashboard:v2.38.0",
|
||||
"repoUrl": "https://github.com/netbirdio/netbird",
|
||||
"containerConfig": {
|
||||
"ports": ["8087:80", "8086:80", "3478:3478/udp"],
|
||||
"volumes": ["/var/lib/archipelago/netbird:/var/lib/netbird"],
|
||||
"notes": "Installed as a two-container stack: netbird dashboard on 8087 and netbird-server control plane on 8086 plus UDP 3478. For production clients, publish a DNS name over HTTPS with gRPC/WebSocket routing."
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "uptime-kuma",
|
||||
"title": "Uptime Kuma",
|
||||
|
||||
@@ -532,7 +532,7 @@ class RPCClient {
|
||||
return this.call({
|
||||
method: 'package.install',
|
||||
params: { id, 'marketplace-url': marketplaceUrl, version },
|
||||
timeout: 15000,
|
||||
timeout: 600000,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -940,4 +940,3 @@ class RPCClient {
|
||||
}
|
||||
|
||||
export const rpcClient = new RPCClient()
|
||||
|
||||
|
||||
@@ -93,6 +93,8 @@ const PORT_TO_APP_ID: Record<string, string> = {
|
||||
'8334': 'bitcoin-knots',
|
||||
'8888': 'searxng',
|
||||
'9000': 'portainer',
|
||||
'8087': 'netbird',
|
||||
'8086': 'netbird',
|
||||
'9980': 'onlyoffice',
|
||||
'11434': 'ollama',
|
||||
'2283': 'immich',
|
||||
@@ -151,13 +153,20 @@ export const useAppLauncherStore = defineStore('appLauncher', () => {
|
||||
const panelAppId = ref<string | null>(null)
|
||||
|
||||
/** Open app in session view — panel mode uses store, overlay/fullscreen uses route */
|
||||
function dashboardReturnPath(): string {
|
||||
const current = router.currentRoute.value
|
||||
const fullPath = current.fullPath || '/dashboard/apps'
|
||||
if (!fullPath.startsWith('/dashboard') || current.name === 'app-session') return '/dashboard/apps'
|
||||
return fullPath
|
||||
}
|
||||
|
||||
function openSession(appId: string) {
|
||||
const mode = localStorage.getItem(DISPLAY_MODE_KEY) || 'panel'
|
||||
if (mode === 'panel' && !isMobileViewport()) {
|
||||
panelAppId.value = appId
|
||||
} else {
|
||||
panelAppId.value = null
|
||||
router.push({ name: 'app-session', params: { appId } })
|
||||
router.push({ name: 'app-session', params: { appId }, query: { returnTo: dashboardReturnPath() } })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
import { PackageState, type PackageDataEntry } from '@/types/api'
|
||||
|
||||
type LoadState = 'idle' | 'loading' | 'ready' | 'error'
|
||||
|
||||
interface SystemStatsSnapshot {
|
||||
cpuPercent: number
|
||||
memUsed: number
|
||||
memTotal: number
|
||||
memPercent: number
|
||||
diskUsed: number
|
||||
diskTotal: number
|
||||
diskPercent: number
|
||||
uptimeSecs: number
|
||||
loadAvg1: number
|
||||
loadAvg5: number
|
||||
loadAvg15: number
|
||||
bitcoinSyncPercent: number
|
||||
bitcoinBlockHeight: number
|
||||
bitcoinAvailable: boolean | null
|
||||
}
|
||||
|
||||
const emptyStats = (): SystemStatsSnapshot => ({
|
||||
cpuPercent: 0,
|
||||
memUsed: 0,
|
||||
memTotal: 0,
|
||||
memPercent: 0,
|
||||
diskUsed: 0,
|
||||
diskTotal: 0,
|
||||
diskPercent: 0,
|
||||
uptimeSecs: 0,
|
||||
loadAvg1: 0,
|
||||
loadAvg5: 0,
|
||||
loadAvg15: 0,
|
||||
bitcoinSyncPercent: 0,
|
||||
bitcoinBlockHeight: 0,
|
||||
bitcoinAvailable: null,
|
||||
})
|
||||
|
||||
export const useHomeStatusStore = defineStore('homeStatus', () => {
|
||||
const stats = reactive<SystemStatsSnapshot>(emptyStats())
|
||||
const systemLoadState = ref<LoadState>('idle')
|
||||
const bitcoinLoadState = ref<LoadState>('idle')
|
||||
const vpnLoadState = ref<LoadState>('idle')
|
||||
const fipsLoadState = ref<LoadState>('idle')
|
||||
const lastSystemRefreshAt = ref<number | null>(null)
|
||||
const lastBitcoinRefreshAt = ref<number | null>(null)
|
||||
const lastVpnRefreshAt = ref<number | null>(null)
|
||||
const lastFipsRefreshAt = ref<number | null>(null)
|
||||
|
||||
const vpnStatus = ref<{
|
||||
connected: boolean | null
|
||||
provider: string
|
||||
}>({ connected: null, provider: '' })
|
||||
|
||||
const fipsStatus = ref<{
|
||||
installed: boolean
|
||||
service_active: boolean
|
||||
key_present: boolean
|
||||
anchor_connected?: boolean
|
||||
authenticated_peer_count?: number
|
||||
} | null>(null)
|
||||
|
||||
const systemStatsLoaded = computed(() => systemLoadState.value === 'ready')
|
||||
const bitcoinKnown = computed(() => stats.bitcoinAvailable !== null)
|
||||
const vpnKnown = computed(() => vpnStatus.value.connected !== null)
|
||||
|
||||
async function refreshSystemStats() {
|
||||
systemLoadState.value = systemLoadState.value === 'ready' ? 'ready' : 'loading'
|
||||
try {
|
||||
const res = await rpcClient.call<{
|
||||
cpu_usage_percent: number
|
||||
mem_used_bytes: number
|
||||
mem_total_bytes: number
|
||||
disk_used_bytes: number
|
||||
disk_total_bytes: number
|
||||
uptime_secs: number
|
||||
load_avg_1?: number
|
||||
load_avg_5?: number
|
||||
load_avg_15?: number
|
||||
}>({ method: 'system.stats' })
|
||||
stats.cpuPercent = res.cpu_usage_percent
|
||||
stats.memUsed = res.mem_used_bytes
|
||||
stats.memTotal = res.mem_total_bytes
|
||||
stats.memPercent = res.mem_total_bytes > 0 ? (res.mem_used_bytes / res.mem_total_bytes) * 100 : 0
|
||||
stats.diskUsed = res.disk_used_bytes
|
||||
stats.diskTotal = res.disk_total_bytes
|
||||
stats.diskPercent = res.disk_total_bytes > 0 ? (res.disk_used_bytes / res.disk_total_bytes) * 100 : 0
|
||||
stats.uptimeSecs = res.uptime_secs
|
||||
stats.loadAvg1 = res.load_avg_1 ?? 0
|
||||
stats.loadAvg5 = res.load_avg_5 ?? 0
|
||||
stats.loadAvg15 = res.load_avg_15 ?? 0
|
||||
systemLoadState.value = 'ready'
|
||||
lastSystemRefreshAt.value = Date.now()
|
||||
} catch {
|
||||
systemLoadState.value = stats.uptimeSecs > 0 ? 'ready' : 'error'
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshBitcoin(packages: Record<string, PackageDataEntry>) {
|
||||
bitcoinLoadState.value = bitcoinLoadState.value === 'ready' ? 'ready' : 'loading'
|
||||
try {
|
||||
const btc = await rpcClient.call<{ block_height: number; sync_progress: number }>({
|
||||
method: 'bitcoin.getinfo',
|
||||
timeout: 5000,
|
||||
})
|
||||
stats.bitcoinSyncPercent = (btc.sync_progress ?? 0) * 100
|
||||
stats.bitcoinBlockHeight = btc.block_height ?? 0
|
||||
stats.bitcoinAvailable = true
|
||||
bitcoinLoadState.value = 'ready'
|
||||
lastBitcoinRefreshAt.value = Date.now()
|
||||
} catch {
|
||||
const btcPkg = packages['bitcoin-knots'] || packages['bitcoin-core'] || packages.bitcoin
|
||||
if (btcPkg?.state === PackageState.Running) {
|
||||
stats.bitcoinAvailable = true
|
||||
bitcoinLoadState.value = 'ready'
|
||||
lastBitcoinRefreshAt.value = Date.now()
|
||||
return
|
||||
}
|
||||
|
||||
if (btcPkg && (btcPkg.state === PackageState.Stopped || btcPkg.state === PackageState.Exited)) {
|
||||
stats.bitcoinAvailable = false
|
||||
bitcoinLoadState.value = 'ready'
|
||||
lastBitcoinRefreshAt.value = Date.now()
|
||||
return
|
||||
}
|
||||
|
||||
// No authoritative package data yet. Keep the previous known value
|
||||
// rather than flashing "Not running" during route changes/scans.
|
||||
bitcoinLoadState.value = stats.bitcoinAvailable === null ? 'error' : 'ready'
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshVpn(packages: Record<string, PackageDataEntry>) {
|
||||
vpnLoadState.value = vpnLoadState.value === 'ready' ? 'ready' : 'loading'
|
||||
try {
|
||||
const status = await rpcClient.vpnStatus()
|
||||
vpnStatus.value = {
|
||||
connected: status.connected,
|
||||
provider: status.provider ?? status.configured_provider ?? '',
|
||||
}
|
||||
vpnLoadState.value = 'ready'
|
||||
lastVpnRefreshAt.value = Date.now()
|
||||
} catch {
|
||||
const tailscale = packages.tailscale
|
||||
if (tailscale?.state === PackageState.Running) {
|
||||
vpnStatus.value = { connected: true, provider: 'tailscale' }
|
||||
vpnLoadState.value = 'ready'
|
||||
lastVpnRefreshAt.value = Date.now()
|
||||
return
|
||||
}
|
||||
vpnLoadState.value = vpnStatus.value.connected === null ? 'error' : 'ready'
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshFips() {
|
||||
fipsLoadState.value = fipsLoadState.value === 'ready' ? 'ready' : 'loading'
|
||||
try {
|
||||
const status = await rpcClient.call<{
|
||||
installed: boolean
|
||||
service_active: boolean
|
||||
key_present: boolean
|
||||
anchor_connected?: boolean
|
||||
authenticated_peer_count?: number
|
||||
}>({ method: 'fips.status' })
|
||||
fipsStatus.value = status
|
||||
fipsLoadState.value = 'ready'
|
||||
lastFipsRefreshAt.value = Date.now()
|
||||
} catch {
|
||||
fipsLoadState.value = fipsStatus.value ? 'ready' : 'error'
|
||||
}
|
||||
}
|
||||
|
||||
async function refresh(packages: Record<string, PackageDataEntry>) {
|
||||
await Promise.all([
|
||||
refreshSystemStats(),
|
||||
refreshBitcoin(packages),
|
||||
refreshVpn(packages),
|
||||
refreshFips(),
|
||||
])
|
||||
}
|
||||
|
||||
return {
|
||||
stats,
|
||||
systemLoadState,
|
||||
bitcoinLoadState,
|
||||
vpnLoadState,
|
||||
fipsLoadState,
|
||||
systemStatsLoaded,
|
||||
bitcoinKnown,
|
||||
vpnKnown,
|
||||
vpnStatus,
|
||||
fipsStatus,
|
||||
lastSystemRefreshAt,
|
||||
lastBitcoinRefreshAt,
|
||||
lastVpnRefreshAt,
|
||||
lastFipsRefreshAt,
|
||||
refresh,
|
||||
refreshSystemStats,
|
||||
refreshBitcoin,
|
||||
refreshVpn,
|
||||
refreshFips,
|
||||
}
|
||||
})
|
||||
@@ -160,6 +160,14 @@ const appUrl = computed(() => {
|
||||
return resolveAppUrl(appId.value, route.query.path as string | undefined)
|
||||
})
|
||||
|
||||
function closeRouteSession() {
|
||||
const fallback = route.query.returnTo
|
||||
const fallbackPath = typeof fallback === 'string' && fallback.startsWith('/dashboard')
|
||||
? fallback
|
||||
: '/dashboard/apps'
|
||||
router.replace(fallbackPath).catch(() => {})
|
||||
}
|
||||
|
||||
// --- Identity & Nostr bridge ---
|
||||
|
||||
const iframeRef = computed(() => frameRef.value?.iframeRef ?? null)
|
||||
@@ -180,7 +188,8 @@ function setMode(mode: DisplayMode) {
|
||||
if (isInlinePanel.value && mode !== 'panel') {
|
||||
const id = appId.value
|
||||
emit('close')
|
||||
router.push({ name: 'app-session', params: { appId: id } })
|
||||
const returnTo = route.fullPath.startsWith('/dashboard') ? route.fullPath : '/dashboard/apps'
|
||||
router.push({ name: 'app-session', params: { appId: id }, query: { returnTo } })
|
||||
return
|
||||
}
|
||||
|
||||
@@ -295,7 +304,7 @@ function handleBackdropClick() {
|
||||
function closeSession() {
|
||||
if (document.fullscreenElement) document.exitFullscreen().catch(() => {})
|
||||
if (isInlinePanel.value) emit('close')
|
||||
else router.back()
|
||||
else closeRouteSession()
|
||||
}
|
||||
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
@@ -332,7 +341,7 @@ onMounted(() => {
|
||||
if (mustOpenNewTab.value && appUrl.value) {
|
||||
window.open(appUrl.value, '_blank', 'noopener,noreferrer')
|
||||
if (isInlinePanel.value) emit('close')
|
||||
else router.back()
|
||||
else closeRouteSession()
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -464,7 +464,7 @@ async function submitSideload() {
|
||||
version: 'sideload',
|
||||
containerConfig,
|
||||
},
|
||||
timeout: 15000,
|
||||
timeout: 600000,
|
||||
})
|
||||
closeSideload()
|
||||
sideloadForm.value = { id: '', image: '', title: '', port: '', description: '' }
|
||||
|
||||
@@ -383,7 +383,7 @@ function isStartingUp(appId: string): boolean {
|
||||
|
||||
function getAppTier(appId: string): string {
|
||||
const core = ['bitcoin-knots', 'bitcoin', 'lnd', 'mempool', 'btcpay-server', 'dwn', 'filebrowser']
|
||||
const recommended = ['fedimint', 'thunderhub', 'vaultwarden', 'uptime-kuma', 'grafana', 'searxng', 'tailscale', 'portainer']
|
||||
const recommended = ['fedimint', 'thunderhub', 'vaultwarden', 'uptime-kuma', 'grafana', 'searxng', 'tailscale', 'netbird', 'portainer']
|
||||
if (core.includes(appId)) return 'core'
|
||||
if (recommended.includes(appId)) return 'recommended'
|
||||
return 'optional'
|
||||
@@ -487,7 +487,7 @@ async function installApp(app: MarketplaceApp) {
|
||||
router.push('/dashboard/apps').catch(() => {})
|
||||
try {
|
||||
const installUrl = app.url || app.manifestUrl || app.s9pkUrl
|
||||
await rpcClient.call({ method: 'package.install', params: { id: app.id, url: installUrl, version: app.version }, timeout: 15000 })
|
||||
await rpcClient.call({ method: 'package.install', params: { id: app.id, url: installUrl, version: app.version }, timeout: 600000 })
|
||||
} catch (err) {
|
||||
if (import.meta.env.DEV) console.error('Installation failed:', err)
|
||||
failInstall(app, err)
|
||||
@@ -504,7 +504,7 @@ async function installCommunityApp(app: MarketplaceApp) {
|
||||
if ((app as Record<string, unknown>).containerConfig) {
|
||||
installParams.containerConfig = (app as Record<string, unknown>).containerConfig
|
||||
}
|
||||
await rpcClient.call({ method: 'package.install', params: installParams, timeout: 15000 })
|
||||
await rpcClient.call({ method: 'package.install', params: installParams, timeout: 600000 })
|
||||
} catch (err) {
|
||||
if (import.meta.env.DEV) console.error('[Discover] Installation failed:', err)
|
||||
failInstall(app, err)
|
||||
|
||||
+40
-23
@@ -144,12 +144,12 @@
|
||||
<span class="text-sm font-medium" :class="torConnected ? 'text-purple-400' : 'text-white/40'">{{ torConnected ? 'Connected' : 'Offline' }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
|
||||
<div class="flex items-center gap-3"><div class="w-2 h-2 rounded-full" :class="vpnConnected ? 'bg-orange-400' : 'bg-white/40'"></div><span class="text-sm text-white/80">VPN</span></div>
|
||||
<span class="text-sm font-medium" :class="vpnConnected ? 'text-orange-400' : 'text-white/40'">{{ vpnConnected ? 'WireGuard' : 'Not configured' }}</span>
|
||||
<div class="flex items-center gap-3"><div class="w-2 h-2 rounded-full" :class="vpnDotClass"></div><span class="text-sm text-white/80">VPN</span></div>
|
||||
<span class="text-sm font-medium" :class="vpnTextClass">{{ vpnStatusLabel }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
|
||||
<div class="flex items-center gap-3"><div class="w-2 h-2 rounded-full" :class="systemStats.bitcoinAvailable ? 'bg-orange-400' : 'bg-white/40'"></div><span class="text-sm text-white/80">Bitcoin</span></div>
|
||||
<span class="text-sm font-medium" :class="systemStats.bitcoinAvailable ? 'text-orange-400' : 'text-white/40'">{{ bitcoinSyncDisplay }}</span>
|
||||
<div class="flex items-center gap-3"><div class="w-2 h-2 rounded-full" :class="bitcoinDotClass"></div><span class="text-sm text-white/80">Bitcoin</span></div>
|
||||
<span class="text-sm font-medium" :class="bitcoinTextClass">{{ bitcoinSyncDisplay }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
|
||||
<div class="flex items-center gap-3"><div class="w-2 h-2 rounded-full" :class="fipsDotClass"></div><span class="text-sm text-white/80">FIPS</span></div>
|
||||
@@ -229,7 +229,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref, watch, onBeforeUnmount, onMounted } from 'vue'
|
||||
import { computed, ref, watch, onBeforeUnmount, onMounted } from 'vue'
|
||||
import { RouterLink, useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import SendBitcoinModal from '@/components/SendBitcoinModal.vue'
|
||||
@@ -239,6 +239,7 @@ import { useAppStore } from '../stores/app'
|
||||
import { useAppLauncherStore } from '@/stores/appLauncher'
|
||||
import { useLoginTransitionStore } from '../stores/loginTransition'
|
||||
import { useUIModeStore } from '@/stores/uiMode'
|
||||
import { useHomeStatusStore } from '@/stores/homeStatus'
|
||||
import { PackageState } from '../types/api'
|
||||
import { playTypingSound } from '@/composables/useLoginSounds'
|
||||
import { GOALS } from '@/data/goals'
|
||||
@@ -261,6 +262,7 @@ const QUICK_START_KEY = 'archipelago-quick-start-dismissed'
|
||||
const QUICK_START_RESHOW_LOGINS = 5
|
||||
|
||||
const store = useAppStore()
|
||||
const homeStatus = useHomeStatusStore()
|
||||
const loginTransition = useLoginTransitionStore()
|
||||
|
||||
const LINE1 = t('home.title')
|
||||
@@ -315,11 +317,19 @@ const torConnected = computed(() => {
|
||||
const torAddr = store.data?.['server-info']?.['tor-address']
|
||||
return !!torAddr && torAddr.length > 0
|
||||
})
|
||||
const vpnStatus = ref({ connected: false, provider: '' })
|
||||
const vpnConnected = computed(() => vpnStatus.value.connected || (!!packages.value['tailscale'] && packages.value['tailscale'].state === PackageState.Running))
|
||||
const fipsStatus = ref<{ installed: boolean; service_active: boolean; key_present: boolean; anchor_connected?: boolean; authenticated_peer_count?: number } | null>(null)
|
||||
const vpnConnected = computed(() => homeStatus.vpnStatus.connected === true || (!!packages.value['tailscale'] && packages.value['tailscale'].state === PackageState.Running))
|
||||
const vpnDotClass = computed(() => {
|
||||
if (vpnConnected.value) return 'bg-orange-400'
|
||||
return homeStatus.vpnKnown ? 'bg-white/40' : 'bg-white/25 animate-pulse'
|
||||
})
|
||||
const vpnTextClass = computed(() => vpnConnected.value ? 'text-orange-400' : (homeStatus.vpnKnown ? 'text-white/40' : 'text-white/50'))
|
||||
const vpnStatusLabel = computed(() => {
|
||||
if (vpnConnected.value) return homeStatus.vpnStatus.provider || 'WireGuard'
|
||||
if (!homeStatus.vpnKnown) return 'Checking…'
|
||||
return 'Not configured'
|
||||
})
|
||||
const fipsDotClass = computed(() => {
|
||||
const s = fipsStatus.value
|
||||
const s = homeStatus.fipsStatus
|
||||
if (!s || !s.installed) return 'bg-white/40'
|
||||
if (!s.service_active) return 'bg-white/40'
|
||||
// Active but no anchor = degraded, not fully green
|
||||
@@ -327,15 +337,15 @@ const fipsDotClass = computed(() => {
|
||||
return 'bg-green-400'
|
||||
})
|
||||
const fipsTextClass = computed(() => {
|
||||
const s = fipsStatus.value
|
||||
const s = homeStatus.fipsStatus
|
||||
if (!s || !s.installed) return 'text-white/40'
|
||||
if (!s.service_active) return 'text-white/40'
|
||||
if (s.anchor_connected === false) return 'text-orange-400'
|
||||
return 'text-green-400'
|
||||
})
|
||||
const fipsStatusLabel = computed(() => {
|
||||
const s = fipsStatus.value
|
||||
if (!s) return '…'
|
||||
const s = homeStatus.fipsStatus
|
||||
if (!s) return homeStatus.fipsLoadState === 'loading' ? 'Checking…' : '…'
|
||||
if (!s.installed) return 'Not installed'
|
||||
if (!s.service_active) {
|
||||
if (!s.key_present) return 'Awaiting seed'
|
||||
@@ -348,11 +358,18 @@ const fipsStatusLabel = computed(() => {
|
||||
return peers === 1 ? 'Active · 1 peer' : `Active · ${peers} peers`
|
||||
})
|
||||
const bitcoinSyncDisplay = computed(() => {
|
||||
if (!systemStats.bitcoinAvailable) return 'Not running'
|
||||
if (systemStats.bitcoinSyncPercent >= 99.9) return 'Synced'
|
||||
if (systemStats.bitcoinSyncPercent < 0.01 && systemStats.bitcoinBlockHeight === 0) return 'Loading...'
|
||||
return `${systemStats.bitcoinSyncPercent.toFixed(1)}%`
|
||||
if (homeStatus.stats.bitcoinAvailable === null) return 'Checking…'
|
||||
if (!homeStatus.stats.bitcoinAvailable) return 'Not running'
|
||||
if (homeStatus.stats.bitcoinSyncPercent >= 99.9) return 'Synced'
|
||||
if (homeStatus.stats.bitcoinSyncPercent < 0.01 && homeStatus.stats.bitcoinBlockHeight === 0) return 'Loading...'
|
||||
return `${homeStatus.stats.bitcoinSyncPercent.toFixed(1)}%`
|
||||
})
|
||||
const bitcoinDotClass = computed(() => {
|
||||
if (homeStatus.stats.bitcoinAvailable === true) return 'bg-orange-400'
|
||||
if (homeStatus.stats.bitcoinAvailable === false) return 'bg-white/40'
|
||||
return 'bg-white/25 animate-pulse'
|
||||
})
|
||||
const bitcoinTextClass = computed(() => homeStatus.stats.bitcoinAvailable ? 'text-orange-400' : (homeStatus.stats.bitcoinAvailable === null ? 'text-white/50' : 'text-white/40'))
|
||||
|
||||
// Quick Start
|
||||
const quickStartDismissed = ref(false)
|
||||
@@ -382,8 +399,6 @@ const cloudFolderDisplay = computed(() => cloudFolderCount.value !== null ? Stri
|
||||
onMounted(async () => {
|
||||
try { const usage = await fileBrowserClient.getUsage(); cloudStorageUsed.value = usage.totalSize; cloudFolderCount.value = usage.folderCount } catch { /* not running */ }
|
||||
loadSystemStats(); systemStatsInterval = setInterval(loadSystemStats, 30000); checkUpdateStatus(); loadWeb5Status()
|
||||
rpcClient.vpnStatus().then(s => { vpnStatus.value = { connected: s.connected, provider: s.provider ?? '' } }).catch(() => {})
|
||||
rpcClient.call<{ installed: boolean; service_active: boolean; key_present: boolean }>({ method: 'fips.status' }).then(s => { fipsStatus.value = s }).catch(() => {})
|
||||
})
|
||||
|
||||
// Wallet modals
|
||||
@@ -403,15 +418,17 @@ async function loadWeb5Status() {
|
||||
}
|
||||
|
||||
// System stats
|
||||
const systemStatsLoaded = ref(false)
|
||||
const systemStats = reactive({ cpuPercent: 0, memUsed: 0, memTotal: 0, memPercent: 0, diskUsed: 0, diskTotal: 0, diskPercent: 0, uptimeSecs: 0, bitcoinSyncPercent: 0, bitcoinBlockHeight: 0, bitcoinAvailable: false })
|
||||
const systemUptimeDisplay = computed(() => { if (systemStats.uptimeSecs === 0) return t('home.systemMonitoring'); const days = Math.floor(systemStats.uptimeSecs / 86400); const hours = Math.floor((systemStats.uptimeSecs % 86400) / 3600); if (days > 0) return `Uptime: ${days}d ${hours}h`; const mins = Math.floor((systemStats.uptimeSecs % 3600) / 60); return `Uptime: ${hours}h ${mins}m` })
|
||||
const systemStatsLoaded = computed(() => homeStatus.systemStatsLoaded)
|
||||
const systemStats = computed(() => ({
|
||||
...homeStatus.stats,
|
||||
bitcoinAvailable: homeStatus.stats.bitcoinAvailable === true,
|
||||
}))
|
||||
const systemUptimeDisplay = computed(() => { if (homeStatus.stats.uptimeSecs === 0) return t('home.systemMonitoring'); const days = Math.floor(homeStatus.stats.uptimeSecs / 86400); const hours = Math.floor((homeStatus.stats.uptimeSecs % 86400) / 3600); if (days > 0) return `Uptime: ${days}d ${hours}h`; const mins = Math.floor((homeStatus.stats.uptimeSecs % 3600) / 60); return `Uptime: ${hours}h ${mins}m` })
|
||||
|
||||
let systemStatsInterval: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
async function loadSystemStats() {
|
||||
try { const res = await rpcClient.call<{ cpu_usage_percent: number; mem_used_bytes: number; mem_total_bytes: number; disk_used_bytes: number; disk_total_bytes: number; uptime_secs: number }>({ method: 'system.stats' }); systemStats.cpuPercent = res.cpu_usage_percent; systemStats.memUsed = res.mem_used_bytes; systemStats.memTotal = res.mem_total_bytes; systemStats.memPercent = res.mem_total_bytes > 0 ? (res.mem_used_bytes / res.mem_total_bytes) * 100 : 0; systemStats.diskUsed = res.disk_used_bytes; systemStats.diskTotal = res.disk_total_bytes; systemStats.diskPercent = res.disk_total_bytes > 0 ? (res.disk_used_bytes / res.disk_total_bytes) * 100 : 0; systemStats.uptimeSecs = res.uptime_secs; systemStatsLoaded.value = true } catch { systemStatsLoaded.value = true }
|
||||
try { const btc = await rpcClient.call<{ block_height: number; sync_progress: number }>({ method: 'bitcoin.getinfo', timeout: 5000 }); systemStats.bitcoinSyncPercent = (btc.sync_progress ?? 0) * 100; systemStats.bitcoinBlockHeight = btc.block_height ?? 0; systemStats.bitcoinAvailable = true } catch { /* RPC failed — check if container is at least running (loading/syncing) */ const btcPkg = packages.value['bitcoin-knots']; systemStats.bitcoinAvailable = btcPkg?.state === PackageState.Running }
|
||||
await homeStatus.refresh(packages.value)
|
||||
}
|
||||
|
||||
function uploadFiles() { const pkg = packages.value['filebrowser']; if (pkg && pkg.state === PackageState.Running) { const host = window.location.hostname; useAppLauncherStore().open({ url: `http://${host}:8083`, title: 'File Browser' }) } else { router.push('/dashboard/cloud') } }
|
||||
|
||||
@@ -415,7 +415,7 @@ async function installApp(app: MarketplaceApp) {
|
||||
await rpcClient.call({
|
||||
method: 'package.install',
|
||||
params: { id: app.id, url: installUrl, version: app.version },
|
||||
timeout: 15000,
|
||||
timeout: 600000,
|
||||
})
|
||||
} catch (err) {
|
||||
if (import.meta.env.DEV) console.error('Installation failed:', err)
|
||||
@@ -441,7 +441,7 @@ async function installCommunityApp(app: MarketplaceApp) {
|
||||
await rpcClient.call({
|
||||
method: 'package.install',
|
||||
params: installParams,
|
||||
timeout: 15000,
|
||||
timeout: 600000,
|
||||
})
|
||||
} catch (err) {
|
||||
if (import.meta.env.DEV) console.error('[Marketplace] Installation failed:', err)
|
||||
|
||||
@@ -616,7 +616,7 @@ async function installApp() {
|
||||
await rpcClient.call({
|
||||
method: 'package.install',
|
||||
params: installParams,
|
||||
timeout: 15000,
|
||||
timeout: 600000,
|
||||
})
|
||||
} else {
|
||||
// Package-based installation
|
||||
@@ -628,7 +628,7 @@ async function installApp() {
|
||||
url: installUrl,
|
||||
version: app.value.version,
|
||||
},
|
||||
timeout: 15000,
|
||||
timeout: 600000,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -41,23 +41,23 @@
|
||||
<div class="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||
<div class="monitoring-stat-card">
|
||||
<p class="text-xs text-white/50 uppercase tracking-wide">{{ t('monitoring.cpu') }}</p>
|
||||
<p class="text-2xl font-bold text-white">{{ current?.system.cpu_percent.toFixed(1) ?? '--' }}%</p>
|
||||
<p class="text-xs text-white/40">{{ t('monitoring.load') }} {{ current?.system.load_avg_1.toFixed(2) ?? '--' }}</p>
|
||||
<p class="text-2xl font-bold text-white">{{ liveSystem.cpu_percent.toFixed(1) }}%</p>
|
||||
<p class="text-xs text-white/40">{{ t('monitoring.load') }} {{ liveSystem.load_avg_1.toFixed(2) }}</p>
|
||||
</div>
|
||||
<div class="monitoring-stat-card">
|
||||
<p class="text-xs text-white/50 uppercase tracking-wide">{{ t('monitoring.memory') }}</p>
|
||||
<p class="text-2xl font-bold text-white">{{ memPercent }}%</p>
|
||||
<p class="text-xs text-white/40">{{ formatBytes(current?.system.mem_used_bytes ?? 0) }} / {{ formatBytes(current?.system.mem_total_bytes ?? 0) }}</p>
|
||||
<p class="text-xs text-white/40">{{ formatBytes(liveSystem.mem_used_bytes) }} / {{ formatBytes(liveSystem.mem_total_bytes) }}</p>
|
||||
</div>
|
||||
<div class="monitoring-stat-card">
|
||||
<p class="text-xs text-white/50 uppercase tracking-wide">{{ t('monitoring.diskUsage') }}</p>
|
||||
<p class="text-2xl font-bold text-white">{{ diskPercent }}%</p>
|
||||
<p class="text-xs text-white/40">{{ formatBytes(current?.system.disk_used_bytes ?? 0) }} / {{ formatBytes(current?.system.disk_total_bytes ?? 0) }}</p>
|
||||
<p class="text-xs text-white/40">{{ formatBytes(liveSystem.disk_used_bytes) }} / {{ formatBytes(liveSystem.disk_total_bytes) }}</p>
|
||||
</div>
|
||||
<div class="monitoring-stat-card">
|
||||
<p class="text-xs text-white/50 uppercase tracking-wide">{{ t('monitoring.network') }}</p>
|
||||
<p class="text-2xl font-bold text-white">{{ formatBytes(current?.system.net_rx_bytes ?? 0) }}</p>
|
||||
<p class="text-xs text-white/40">TX: {{ formatBytes(current?.system.net_tx_bytes ?? 0) }}</p>
|
||||
<p class="text-2xl font-bold text-white">{{ formatBytes(liveSystem.net_rx_bytes) }}</p>
|
||||
<p class="text-xs text-white/40">TX: {{ formatBytes(liveSystem.net_tx_bytes) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -236,6 +236,7 @@ import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
import { useHomeStatusStore } from '@/stores/homeStatus'
|
||||
import LineChart from '@/components/LineChart.vue'
|
||||
import ToggleSwitch from '@/components/ToggleSwitch.vue'
|
||||
import type { ChartDataset } from '@/components/LineChart.vue'
|
||||
@@ -297,6 +298,7 @@ interface FiredAlert {
|
||||
|
||||
const router = useRouter()
|
||||
const { t } = useI18n()
|
||||
const homeStatus = useHomeStatusStore()
|
||||
|
||||
const current = ref<MetricSnapshot | null>(null)
|
||||
const history = ref<MetricSnapshot[]>([])
|
||||
@@ -307,14 +309,27 @@ const showAlertConfig = ref(false)
|
||||
const chartWidth = ref(380)
|
||||
let pollTimer: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
const liveSystem = computed<SystemMetrics>(() => ({
|
||||
cpu_percent: homeStatus.stats.cpuPercent,
|
||||
mem_used_bytes: homeStatus.stats.memUsed,
|
||||
mem_total_bytes: homeStatus.stats.memTotal,
|
||||
disk_used_bytes: homeStatus.stats.diskUsed,
|
||||
disk_total_bytes: homeStatus.stats.diskTotal,
|
||||
net_rx_bytes: current.value?.system.net_rx_bytes ?? 0,
|
||||
net_tx_bytes: current.value?.system.net_tx_bytes ?? 0,
|
||||
load_avg_1: homeStatus.stats.loadAvg1,
|
||||
load_avg_5: homeStatus.stats.loadAvg5,
|
||||
load_avg_15: homeStatus.stats.loadAvg15,
|
||||
}))
|
||||
|
||||
const memPercent = computed(() => {
|
||||
if (!current.value?.system.mem_total_bytes) return '--'
|
||||
return ((current.value.system.mem_used_bytes / current.value.system.mem_total_bytes) * 100).toFixed(1)
|
||||
if (!liveSystem.value.mem_total_bytes) return '--'
|
||||
return ((liveSystem.value.mem_used_bytes / liveSystem.value.mem_total_bytes) * 100).toFixed(1)
|
||||
})
|
||||
|
||||
const diskPercent = computed(() => {
|
||||
if (!current.value?.system.disk_total_bytes) return '--'
|
||||
return ((current.value.system.disk_used_bytes / current.value.system.disk_total_bytes) * 100).toFixed(1)
|
||||
if (!liveSystem.value.disk_total_bytes) return '--'
|
||||
return ((liveSystem.value.disk_used_bytes / liveSystem.value.disk_total_bytes) * 100).toFixed(1)
|
||||
})
|
||||
|
||||
const historyMinutesAgo = computed(() => history.value.length || 60)
|
||||
@@ -460,6 +475,7 @@ async function exportMetrics(format: 'csv' | 'json') {
|
||||
|
||||
async function fetchCurrent() {
|
||||
try {
|
||||
await homeStatus.refreshSystemStats()
|
||||
const data = await rpcClient.call<MetricSnapshot | { status: string }>({
|
||||
method: 'monitoring.current',
|
||||
})
|
||||
|
||||
@@ -344,94 +344,96 @@
|
||||
</Transition>
|
||||
</Teleport>
|
||||
|
||||
<!-- Add-mirror modal -->
|
||||
<Transition name="fade">
|
||||
<div v-if="addingMirror" class="fixed inset-0 z-50 flex items-center justify-center bg-black/10 backdrop-blur-md" @click.self="cancelAddMirror">
|
||||
<div class="glass-card p-6 max-w-md w-full mx-4">
|
||||
<h3 class="text-lg font-semibold text-white mb-1">Add update mirror</h3>
|
||||
<p class="text-sm text-white/60 mb-5">
|
||||
The URL should point directly at a <span class="font-mono text-white/80">manifest.json</span> served by a Gitea mirror or equivalent. It's added to the end of the list; use "Make primary" to change order.
|
||||
</p>
|
||||
<form class="space-y-3" @submit.prevent="submitMirror">
|
||||
<div>
|
||||
<label class="block text-xs text-white/60 mb-1">Manifest URL</label>
|
||||
<input
|
||||
v-model="mirrorDraft.url"
|
||||
type="text"
|
||||
autofocus
|
||||
placeholder="https://host/.../manifest.json"
|
||||
class="w-full px-3 py-2 rounded-md bg-white/5 border border-white/10 text-sm text-white focus:border-white/30 focus:outline-none font-mono"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-white/60 mb-1">Label (optional)</label>
|
||||
<input
|
||||
v-model="mirrorDraft.label"
|
||||
type="text"
|
||||
placeholder="Home VPS"
|
||||
class="w-full px-3 py-2 rounded-md bg-white/5 border border-white/10 text-sm text-white focus:border-white/30 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex gap-3 justify-end pt-2">
|
||||
<button
|
||||
type="button"
|
||||
@click="cancelAddMirror"
|
||||
class="glass-button rounded-lg px-4 py-2 text-sm font-medium"
|
||||
>{{ t('common.cancel') }}</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="glass-button rounded-lg px-4 py-2 text-sm font-medium bg-orange-500/20 border-orange-400/30 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
:disabled="mirrorSaving || !mirrorDraft.url.trim()"
|
||||
>{{ mirrorSaving ? 'Adding…' : 'Add mirror' }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<!-- Confirmation modal -->
|
||||
<Transition name="fade">
|
||||
<div v-if="confirmAction" class="fixed inset-0 z-50 flex items-center justify-center bg-black/10 backdrop-blur-md" @click.self="cancelConfirm">
|
||||
<div class="glass-card p-6 max-w-sm w-full mx-4">
|
||||
<h3 class="text-lg font-semibold text-white mb-3">
|
||||
{{ confirmAction === 'rollback'
|
||||
? t('systemUpdate.rollbackTitle')
|
||||
: confirmAction === 'git-apply'
|
||||
? t('systemUpdate.gitApplyTitle')
|
||||
: confirmAction === 'cancel-download'
|
||||
? t('systemUpdate.cancelDownloadTitle')
|
||||
: t('systemUpdate.applyTitle') }}
|
||||
</h3>
|
||||
<p class="text-sm text-white/70 mb-6">
|
||||
{{ confirmAction === 'rollback'
|
||||
? t('systemUpdate.rollbackMessage')
|
||||
: confirmAction === 'git-apply'
|
||||
? t('systemUpdate.gitApplyMessage')
|
||||
: confirmAction === 'cancel-download'
|
||||
? t('systemUpdate.cancelDownloadConfirm')
|
||||
: t('systemUpdate.applyMessage') }}
|
||||
</p>
|
||||
<div class="flex gap-3 justify-end">
|
||||
<button @click="cancelConfirm" class="glass-button rounded-lg px-4 py-2 text-sm font-medium">
|
||||
{{ t('common.cancel') }}
|
||||
</button>
|
||||
<button
|
||||
@click="executeConfirm"
|
||||
class="glass-button rounded-lg px-4 py-2 text-sm font-medium"
|
||||
:class="(confirmAction === 'rollback' || confirmAction === 'cancel-download') ? 'bg-red-500/20 border-red-400/30' : 'bg-orange-500/20 border-orange-400/30'"
|
||||
>
|
||||
{{ confirmAction === 'rollback'
|
||||
? t('systemUpdate.rollbackButton')
|
||||
: confirmAction === 'git-apply'
|
||||
? t('systemUpdate.pullAndRebuild')
|
||||
: confirmAction === 'cancel-download'
|
||||
? t('systemUpdate.cancelDownloadButton')
|
||||
: t('systemUpdate.applyNow') }}
|
||||
</button>
|
||||
<Teleport to="body">
|
||||
<!-- Add-mirror modal -->
|
||||
<Transition name="fade">
|
||||
<div v-if="addingMirror" class="fixed inset-0 z-[3000] flex items-center justify-center p-4 bg-black/60 backdrop-blur-md" @click.self="cancelAddMirror">
|
||||
<div class="glass-card p-6 max-w-md w-full">
|
||||
<h3 class="text-lg font-semibold text-white mb-1">Add update mirror</h3>
|
||||
<p class="text-sm text-white/60 mb-5">
|
||||
The URL should point directly at a <span class="font-mono text-white/80">manifest.json</span> served by a Gitea mirror or equivalent. It's added to the end of the list; use "Make primary" to change order.
|
||||
</p>
|
||||
<form class="space-y-3" @submit.prevent="submitMirror">
|
||||
<div>
|
||||
<label class="block text-xs text-white/60 mb-1">Manifest URL</label>
|
||||
<input
|
||||
v-model="mirrorDraft.url"
|
||||
type="text"
|
||||
autofocus
|
||||
placeholder="https://host/.../manifest.json"
|
||||
class="w-full px-3 py-2 rounded-md bg-white/5 border border-white/10 text-sm text-white focus:border-white/30 focus:outline-none font-mono"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-white/60 mb-1">Label (optional)</label>
|
||||
<input
|
||||
v-model="mirrorDraft.label"
|
||||
type="text"
|
||||
placeholder="Home VPS"
|
||||
class="w-full px-3 py-2 rounded-md bg-white/5 border border-white/10 text-sm text-white focus:border-white/30 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex gap-3 justify-end pt-2">
|
||||
<button
|
||||
type="button"
|
||||
@click="cancelAddMirror"
|
||||
class="glass-button rounded-lg px-4 py-2 text-sm font-medium"
|
||||
>{{ t('common.cancel') }}</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="glass-button rounded-lg px-4 py-2 text-sm font-medium bg-orange-500/20 border-orange-400/30 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
:disabled="mirrorSaving || !mirrorDraft.url.trim()"
|
||||
>{{ mirrorSaving ? 'Adding…' : 'Add mirror' }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Transition>
|
||||
|
||||
<!-- Confirmation modal -->
|
||||
<Transition name="fade">
|
||||
<div v-if="confirmAction" class="fixed inset-0 z-[3000] flex items-center justify-center p-4 bg-black/60 backdrop-blur-md" @click.self="cancelConfirm">
|
||||
<div class="glass-card p-6 max-w-sm w-full">
|
||||
<h3 class="text-lg font-semibold text-white mb-3">
|
||||
{{ confirmAction === 'rollback'
|
||||
? t('systemUpdate.rollbackTitle')
|
||||
: confirmAction === 'git-apply'
|
||||
? t('systemUpdate.gitApplyTitle')
|
||||
: confirmAction === 'cancel-download'
|
||||
? t('systemUpdate.cancelDownloadTitle')
|
||||
: t('systemUpdate.applyTitle') }}
|
||||
</h3>
|
||||
<p class="text-sm text-white/70 mb-6">
|
||||
{{ confirmAction === 'rollback'
|
||||
? t('systemUpdate.rollbackMessage')
|
||||
: confirmAction === 'git-apply'
|
||||
? t('systemUpdate.gitApplyMessage')
|
||||
: confirmAction === 'cancel-download'
|
||||
? t('systemUpdate.cancelDownloadConfirm')
|
||||
: t('systemUpdate.applyMessage') }}
|
||||
</p>
|
||||
<div class="flex gap-3 justify-end">
|
||||
<button @click="cancelConfirm" class="glass-button rounded-lg px-4 py-2 text-sm font-medium">
|
||||
{{ t('common.cancel') }}
|
||||
</button>
|
||||
<button
|
||||
@click="executeConfirm"
|
||||
class="glass-button rounded-lg px-4 py-2 text-sm font-medium"
|
||||
:class="(confirmAction === 'rollback' || confirmAction === 'cancel-download') ? 'bg-red-500/20 border-red-400/30' : 'bg-orange-500/20 border-orange-400/30'"
|
||||
>
|
||||
{{ confirmAction === 'rollback'
|
||||
? t('systemUpdate.rollbackButton')
|
||||
: confirmAction === 'git-apply'
|
||||
? t('systemUpdate.pullAndRebuild')
|
||||
: confirmAction === 'cancel-download'
|
||||
? t('systemUpdate.cancelDownloadButton')
|
||||
: t('systemUpdate.applyNow') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ export const ROUTE_TO_PACKAGE_KEY: Record<string, string> = {
|
||||
portainer: 'portainer',
|
||||
'uptime-kuma': 'uptime-kuma',
|
||||
tailscale: 'tailscale',
|
||||
netbird: 'netbird',
|
||||
}
|
||||
|
||||
/** Backend may register under variant container names */
|
||||
|
||||
@@ -34,6 +34,7 @@ export const APP_PORTS: Record<string, number> = {
|
||||
'nginx-proxy-manager': 8081,
|
||||
'gitea': 3001,
|
||||
'portainer': 9000,
|
||||
'netbird': 8087,
|
||||
'tailscale': 8240,
|
||||
'uptime-kuma': 3002,
|
||||
'fedimint': 8175,
|
||||
|
||||
@@ -145,7 +145,7 @@
|
||||
class="flex-1 px-4 py-2 glass-button glass-button-sm rounded-lg text-sm font-medium flex items-center justify-center gap-1.5"
|
||||
>
|
||||
{{ t('common.launch') }}
|
||||
<svg v-if="opensInTab(id)" class="w-3.5 h-3.5 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" /></svg>
|
||||
<svg v-if="opensInTab(id)" class="hidden md:block w-3.5 h-3.5 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" /></svg>
|
||||
</button>
|
||||
<!-- Start (play icon) -->
|
||||
<button
|
||||
@@ -266,7 +266,7 @@ const tier = computed(() => {
|
||||
const t = props.pkg.manifest?.tier
|
||||
if (t && t !== '') return t
|
||||
const core = ['bitcoin-knots', 'bitcoin', 'lnd', 'mempool', 'btcpay-server', 'dwn', 'filebrowser']
|
||||
const recommended = ['fedimint', 'thunderhub', 'vaultwarden', 'uptime-kuma', 'grafana', 'searxng', 'tailscale', 'portainer']
|
||||
const recommended = ['fedimint', 'thunderhub', 'vaultwarden', 'uptime-kuma', 'grafana', 'searxng', 'tailscale', 'netbird', 'portainer']
|
||||
if (core.includes(props.id)) return 'core'
|
||||
if (recommended.includes(props.id)) return 'recommended'
|
||||
return 'optional'
|
||||
|
||||
@@ -55,9 +55,9 @@ export const APP_CATEGORY_MAP: Record<string, string> = {
|
||||
'indeedhub': 'media', 'jellyfin': 'media', 'photoprism': 'media', 'immich': 'media',
|
||||
'nextcloud': 'data', 'vaultwarden': 'data', 'filebrowser': 'data', 'cryptpad': 'data',
|
||||
'homeassistant': 'home', 'lorabell': 'home', 'endurain': 'home',
|
||||
'searxng': 'community', 'ollama': 'community', 'grafana': 'data',
|
||||
'searxng': 'community', 'ollama': 'community', 'grafana': 'data', 'gitea': 'data',
|
||||
'nostrudel': 'nostr',
|
||||
'tailscale': 'networking', 'nginx-proxy-manager': 'networking', 'portainer': 'networking',
|
||||
'tailscale': 'networking', 'netbird': 'networking', 'nginx-proxy-manager': 'networking', 'portainer': 'networking',
|
||||
'uptime-kuma': 'networking', 'dwn': 'data',
|
||||
'botfights': 'community', 'nwnn': 'l484', '484-kitchen': 'l484',
|
||||
'call-the-operator': 'l484', 'syntropy-institute': 'l484', 't-zero': 'l484',
|
||||
|
||||
@@ -142,7 +142,7 @@ const mobileTabBar = ref<HTMLElement | null>(null)
|
||||
|
||||
// App sessions own their mobile controls. Normal mobile launches use the route
|
||||
// session; keeping this guard also protects any desktop-panel state on resize.
|
||||
const isAppSessionActive = computed(() => route.name === 'app-session' || !!appLauncher.panelAppId)
|
||||
const isAppSessionActive = computed(() => route.name === 'app-session')
|
||||
|
||||
// Show persistent tabs for Apps/Marketplace on mobile
|
||||
const showAppsTabs = computed(() => {
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
<aside
|
||||
v-show="!chatFullscreen"
|
||||
data-controller-zone="sidebar"
|
||||
class="hidden md:flex w-[256px] flex-shrink-0 relative flex-col z-10"
|
||||
class="hidden md:flex w-[256px] h-screen flex-shrink-0 sticky top-0 relative flex-col z-10"
|
||||
:class="{ 'sidebar-animate': showZoomIn }"
|
||||
>
|
||||
<div class="sidebar-shell">
|
||||
<div class="sidebar-inner flex flex-col min-h-full">
|
||||
<div class="sidebar-inner flex flex-col h-full min-h-0">
|
||||
<div class="sidebar-logo flex items-center gap-3 mb-8 p-6 pb-0 shrink-0">
|
||||
<AnimatedLogo />
|
||||
<div class="min-w-0 flex-1">
|
||||
@@ -15,7 +15,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="sidebar-nav flex-1 min-h-0 space-y-2 p-6 pt-4" :aria-label="t('dashboard.mainNav')">
|
||||
<nav class="sidebar-nav flex-1 min-h-0 overflow-y-auto overscroll-contain space-y-2 px-6 py-4" :aria-label="t('dashboard.mainNav')">
|
||||
<RouterLink
|
||||
v-for="(item, idx) in desktopNavItems"
|
||||
:key="item.path"
|
||||
@@ -74,21 +74,23 @@
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<div class="sidebar-controller px-6 pb-2 shrink-0">
|
||||
<ControllerIndicator />
|
||||
<CompanionIndicator />
|
||||
</div>
|
||||
|
||||
<!-- Online status -->
|
||||
<div class="px-6 pb-2 shrink-0">
|
||||
<div class="rounded-lg bg-white/5 border border-white/10 px-4 py-2.5">
|
||||
<OnlineStatusPill />
|
||||
<div class="sidebar-bottom shrink-0">
|
||||
<div class="sidebar-controller px-6 pb-2">
|
||||
<ControllerIndicator />
|
||||
<CompanionIndicator />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mode switcher -->
|
||||
<div class="px-6 pb-6 shrink-0">
|
||||
<ModeSwitcher />
|
||||
<!-- Online status -->
|
||||
<div class="px-6 pb-2">
|
||||
<div class="rounded-lg bg-white/5 border border-white/10 px-4 py-2.5">
|
||||
<OnlineStatusPill />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mode switcher -->
|
||||
<div class="px-6 pb-6">
|
||||
<ModeSwitcher />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
.sidebar-shell {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 100vh;
|
||||
min-height: 0;
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
backdrop-filter: blur(18px);
|
||||
-webkit-backdrop-filter: blur(18px);
|
||||
@@ -85,6 +85,24 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(255, 255, 255, 0.24) transparent;
|
||||
}
|
||||
|
||||
.sidebar-nav::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.sidebar-nav::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.22);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.sidebar-bottom {
|
||||
background: linear-gradient(to top, rgba(0, 0, 0, 0.18), transparent 100%);
|
||||
}
|
||||
|
||||
/* Only hide sidebar content when doing the login entrance animation */
|
||||
.sidebar-animate .sidebar-inner {
|
||||
opacity: 0;
|
||||
|
||||
@@ -78,7 +78,7 @@ export function getCuratedAppList(): MarketplaceApp[] {
|
||||
return [
|
||||
{ id: 'bitcoin-knots', title: 'Bitcoin Knots', version: '28.1.0', description: 'Run a full Bitcoin node. Validate and relay blocks and transactions on the Bitcoin network.', icon: '/assets/img/app-icons/bitcoin-knots.webp', author: 'Bitcoin Knots', dockerImage: `${R}/bitcoin-knots:latest`, repoUrl: 'https://github.com/bitcoinknots/bitcoin' },
|
||||
{ id: 'bitcoin-core', title: 'Bitcoin Core', version: '28.4', description: 'Reference implementation of the Bitcoin protocol. Run a full node validating and relaying blocks on the Bitcoin network.', icon: '/assets/img/app-icons/bitcoin-core.svg', author: 'Bitcoin Core contributors', dockerImage: 'docker.io/bitcoin/bitcoin:28.4', repoUrl: 'https://github.com/bitcoin/bitcoin' },
|
||||
{ id: 'btcpay-server', title: 'BTCPay Server', version: '1.13.7', description: 'Self-hosted Bitcoin payment processor. Accept Bitcoin payments without intermediaries or fees.', icon: '/assets/img/app-icons/btcpay-server.png', author: 'BTCPay Server Foundation', dockerImage: `${R}/btcpayserver:1.13.7`, repoUrl: 'https://github.com/btcpayserver/btcpayserver' },
|
||||
{ id: 'btcpay-server', title: 'BTCPay Server', version: '2.3.9', description: 'Self-hosted Bitcoin payment processor. Accept Bitcoin payments without intermediaries or fees.', icon: '/assets/img/app-icons/btcpay-server.png', author: 'BTCPay Server Foundation', dockerImage: 'docker.io/btcpayserver/btcpayserver:2.3.9', repoUrl: 'https://github.com/btcpayserver/btcpayserver' },
|
||||
{ id: 'lnd', title: 'LND', version: '0.18.4', description: 'Lightning Network Daemon. Fast and cheap Bitcoin payments through the Lightning Network.', icon: '/assets/img/app-icons/lnd.svg', author: 'Lightning Labs', dockerImage: `${R}/lnd:v0.18.4-beta`, repoUrl: 'https://github.com/lightningnetwork/lnd' },
|
||||
{ id: 'mempool', title: 'Mempool Explorer', version: '3.0.0', description: 'Self-hosted Bitcoin blockchain and mempool visualizer. Monitor transactions without revealing your addresses to third parties.', icon: '/assets/img/app-icons/mempool.webp', author: 'Mempool', dockerImage: `${R}/mempool-frontend:v3.0.0`, repoUrl: 'https://github.com/mempool/mempool' },
|
||||
{ id: 'homeassistant', title: 'Home Assistant', version: '2024.1', description: 'Open-source home automation. Control smart home devices privately, on your own hardware.', icon: '/assets/img/app-icons/homeassistant.png', author: 'Home Assistant', dockerImage: `${R}/home-assistant:2024.1`, repoUrl: 'https://github.com/home-assistant/core' },
|
||||
@@ -96,6 +96,7 @@ export function getCuratedAppList(): MarketplaceApp[] {
|
||||
{ id: 'portainer', title: 'Portainer', version: '2.19.4', description: 'Container management UI. Manage your containerized services through the web.', icon: '/assets/img/app-icons/portainer.webp', author: 'Portainer', dockerImage: `${R}/portainer:latest`, repoUrl: 'https://github.com/portainer/portainer' },
|
||||
{ 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', author: 'Uptime Kuma', dockerImage: `${R}/uptime-kuma:1`, repoUrl: 'https://github.com/louislam/uptime-kuma' },
|
||||
{ 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: 'netbird', title: 'NetBird', version: '0.71.2', description: 'Self-hosted WireGuard mesh VPN control plane with dashboard, embedded identity provider, management API, signal, relay, and STUN.', icon: '/assets/img/app-icons/netbird.svg', author: 'NetBird', dockerImage: 'docker.io/netbirdio/dashboard:v2.38.0', repoUrl: 'https://github.com/netbirdio/netbird' },
|
||||
{ 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.png', 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: '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' },
|
||||
@@ -132,6 +133,7 @@ export const INSTALLED_ALIASES: Record<string, string[]> = {
|
||||
lnd: ['lnd'],
|
||||
filebrowser: ['filebrowser'],
|
||||
tailscale: ['tailscale'],
|
||||
netbird: ['netbird'],
|
||||
ollama: ['ollama'],
|
||||
indeedhub: ['indeedhub'],
|
||||
botfights: ['botfights'],
|
||||
@@ -191,7 +193,7 @@ export function categorizeCommunityApp(app: MarketplaceApp): string {
|
||||
if (id.includes('cloud') || id.includes('nextcloud') || id.includes('storage') || id.includes('file') || id.includes('photo') || id.includes('immich') || id.includes('jellyfin') || id.includes('media') || id.includes('vault') || combined.includes('password manager')) return 'data'
|
||||
if (id.includes('home-assistant') || id.includes('homeassistant') || combined.includes('home automation')) return 'home'
|
||||
if (id.includes('nostr') || combined.includes('nostr relay')) return 'nostr'
|
||||
if (id.includes('vpn') || id.includes('wireguard') || id.includes('tailscale') || id.includes('proxy') || id.includes('dns') || id.includes('tor') || combined.includes('network')) return 'networking'
|
||||
if (id.includes('vpn') || id.includes('wireguard') || id.includes('tailscale') || id.includes('netbird') || id.includes('proxy') || id.includes('dns') || id.includes('tor') || combined.includes('network')) return 'networking'
|
||||
if (id.includes('matrix') || id.includes('mastodon') || id.includes('chat') || id.includes('social') || combined.includes('messaging')) return 'community'
|
||||
return 'other'
|
||||
}
|
||||
|
||||
@@ -60,13 +60,14 @@ export const INSTALLED_ALIASES: Record<string, string[]> = {
|
||||
lnd: ['lnd', 'archy-lnd-ui'],
|
||||
filebrowser: ['filebrowser'],
|
||||
tailscale: ['tailscale'],
|
||||
netbird: ['netbird'],
|
||||
ollama: ['ollama'],
|
||||
}
|
||||
|
||||
/** Get app tier classification (matches backend get_app_tier) */
|
||||
export function getAppTier(appId: string): string {
|
||||
const core = ['bitcoin-knots', 'bitcoin', 'lnd', 'mempool', 'btcpay-server', 'dwn', 'filebrowser']
|
||||
const recommended = ['fedimint', 'thunderhub', 'vaultwarden', 'uptime-kuma', 'grafana', 'searxng', 'tailscale', 'portainer']
|
||||
const recommended = ['fedimint', 'thunderhub', 'vaultwarden', 'uptime-kuma', 'grafana', 'searxng', 'tailscale', 'netbird', 'portainer']
|
||||
if (core.includes(appId)) return 'core'
|
||||
if (recommended.includes(appId)) return 'recommended'
|
||||
return 'optional'
|
||||
@@ -113,7 +114,7 @@ export function categorizeCommunityApp(app: MarketplaceApp): string {
|
||||
return 'nostr'
|
||||
}
|
||||
|
||||
if (id.includes('vpn') || id.includes('wireguard') || id.includes('tailscale') ||
|
||||
if (id.includes('vpn') || id.includes('wireguard') || id.includes('tailscale') || id.includes('netbird') ||
|
||||
id.includes('proxy') || id.includes('dns') || id.includes('pihole') ||
|
||||
id.includes('adguard') || id.includes('nginx') || id.includes('tor') ||
|
||||
combined.includes('network') || combined.includes('firewall')) {
|
||||
@@ -148,11 +149,11 @@ export function getCuratedAppList(): MarketplaceApp[] {
|
||||
{
|
||||
id: 'btcpay-server',
|
||||
title: 'BTCPay Server',
|
||||
version: '1.13.7',
|
||||
version: '2.3.9',
|
||||
description: 'Self-hosted Bitcoin payment processor. Accept Bitcoin payments without intermediaries or fees.',
|
||||
icon: '/assets/img/app-icons/btcpay-server.png',
|
||||
author: 'BTCPay Server Foundation',
|
||||
dockerImage: `${REGISTRY}/btcpayserver:1.13.7`,
|
||||
dockerImage: 'docker.io/btcpayserver/btcpayserver:2.3.9',
|
||||
manifestUrl: undefined,
|
||||
repoUrl: 'https://github.com/btcpayserver/btcpayserver'
|
||||
},
|
||||
@@ -365,6 +366,17 @@ export function getCuratedAppList(): MarketplaceApp[] {
|
||||
manifestUrl: undefined,
|
||||
repoUrl: 'https://github.com/tailscale/tailscale'
|
||||
},
|
||||
{
|
||||
id: 'netbird',
|
||||
title: 'NetBird',
|
||||
version: '0.71.2',
|
||||
description: 'Self-hosted WireGuard mesh VPN control plane with dashboard, embedded identity provider, management API, signal, relay, and STUN.',
|
||||
icon: '/assets/img/app-icons/netbird.svg',
|
||||
author: 'NetBird',
|
||||
dockerImage: 'docker.io/netbirdio/dashboard:v2.38.0',
|
||||
manifestUrl: undefined,
|
||||
repoUrl: 'https://github.com/netbirdio/netbird'
|
||||
},
|
||||
{
|
||||
id: 'fedimint',
|
||||
title: 'Fedimint',
|
||||
|
||||
@@ -180,6 +180,256 @@ init()
|
||||
</button>
|
||||
</div>
|
||||
<div class="overflow-y-auto flex-1 min-h-0 space-y-6 pr-1">
|
||||
<!-- v1.7.67-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.67-alpha</span>
|
||||
<span class="text-xs text-white/40">May 18, 2026</span>
|
||||
</div>
|
||||
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||
<p>Home status cards are calmer and more honest now. System, VPN, Bitcoin, and FIPS values keep their last known good state while route changes or short RPC failures are in flight, so the dashboard no longer flashes false "not configured" or "not running" states during normal refreshes.</p>
|
||||
<p>Home, Web5 Monitoring, and the full Monitoring page now agree on the headline CPU, memory, disk, uptime, and load numbers. The UI uses one live system-stat snapshot for the visible cards while keeping the Monitoring page's historical store for charts, alerts, and container history.</p>
|
||||
<p>The missing What's New history is filled in through this release, including every curated entry from v1.7.44-alpha through v1.7.66-alpha.</p>
|
||||
<p>Bitcoin lifecycle specs are aligned again across Rust, first boot, and reconcile. Bitcoin Core/Knots get the intended memory headroom on normal hosts, and pruned Knots uses a larger dbcache when the node has enough RAM, improving IBD throughput without raising pressure on low-memory machines.</p>
|
||||
<p>ElectrumX/electrs lifecycle specs now use the same memory policy everywhere, reducing drift between fresh installs, app lifecycle actions, and reconciliation.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- v1.7.66-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.66-alpha</span>
|
||||
<span class="text-xs text-white/40">May 18, 2026</span>
|
||||
</div>
|
||||
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||
<p>Nginx Proxy Manager stale-port repair now catches stopped or Created Podman records that still remember old port mappings. That means a stuck record can be removed and recreated before it blocks the current NPM ports.</p>
|
||||
<p>Live recovery on the field node preserved the existing Nginx Proxy Manager data directory while recreating only the stale container metadata with the current 8081, 8084, and 8444 host ports.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- v1.7.65-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.65-alpha</span>
|
||||
<span class="text-xs text-white/40">May 18, 2026</span>
|
||||
</div>
|
||||
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||
<p>Orchestrator-backed app starts now run the same pre-start repair path as the legacy Podman start flow. Nginx Proxy Manager can clean up stale port metadata before the orchestrator tries to bring it online.</p>
|
||||
<p>Diagnostics confirmed host nginx was healthy while Nginx Proxy Manager itself had no listeners on its expected ports, narrowing the outage to NPM container lifecycle repair instead of the system proxy.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- v1.7.64-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.64-alpha</span>
|
||||
<span class="text-xs text-white/40">May 18, 2026</span>
|
||||
</div>
|
||||
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||
<p>Authenticated update applies are no longer throttled so aggressively during troubleshooting. The System Update page now allows legitimate retry flows without immediately running into 429 Too Many Requests.</p>
|
||||
<p>The release still includes the corrected backend rebuild protection, so OTA artifacts are built from the fresh Rust binary instead of an older compiled version.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- v1.7.63-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.63-alpha</span>
|
||||
<span class="text-xs text-white/40">May 18, 2026</span>
|
||||
</div>
|
||||
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||
<p>The release script now rebuilds the backend after bumping the version and before hashing artifacts. OTA manifests no longer point at a stale backend binary.</p>
|
||||
<p>This corrected the previous stale-artifact issue and carries the Nginx Proxy Manager stale-port repair in a backend binary that nodes can actually install and run.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- v1.7.62-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.62-alpha</span>
|
||||
<span class="text-xs text-white/40">May 18, 2026</span>
|
||||
</div>
|
||||
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||
<p>Nginx Proxy Manager start and restart now repair stale Podman containers that still publish the admin UI on host port 81, which conflicts with host nginx on updated nodes.</p>
|
||||
<p>The repair recreates only NPM container metadata while preserving its persistent data and using the current 8081, 8084, and 8444 host mappings.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- v1.7.61-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.61-alpha</span>
|
||||
<span class="text-xs text-white/40">May 18, 2026</span>
|
||||
</div>
|
||||
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||
<p>Multi-container stack installs stay in Installing for up to 20 minutes while dependency containers are being pulled and prepared. BTCPay no longer appears to vanish after two minutes while Postgres and NBXplorer are still being created.</p>
|
||||
<p>Lifecycle stale-state recovery remains short for start, stop, restart, update, and removal actions, so genuinely wedged operations still clear quickly.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- v1.7.60-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.60-alpha</span>
|
||||
<span class="text-xs text-white/40">May 18, 2026</span>
|
||||
</div>
|
||||
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||
<p>Meshtastic serial detection now rejects malformed handshakes and skips known non-mesh serial devices such as Sierra Wireless LTE modems and Zooz/Z-Wave sticks.</p>
|
||||
<p>Meshtastic config sync sends the correct protobuf wire type, allowing node-info and contact ingestion to work reliably. The mesh udev rule also stops claiming every ttyACM device and now targets known mesh adapters/vendors.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- v1.7.59-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.59-alpha</span>
|
||||
<span class="text-xs text-white/40">May 17, 2026</span>
|
||||
</div>
|
||||
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||
<p>Mobile app launching now keeps known container apps inside Archipelago's app-session flow instead of forcing desktop-only new-tab behavior.</p>
|
||||
<p>App sessions on mobile respect the status-bar safe area, while the fullscreen backdrop remains edge-to-edge. The Apps page also gained a compact sideload button and modal for trusted Docker images.</p>
|
||||
<p>Sideloaded app title and description metadata now persist through backend app config, and Meshtastic contact discovery retries config sync when the radio contact cache is empty.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- v1.7.58-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.58-alpha</span>
|
||||
<span class="text-xs text-white/40">May 17, 2026</span>
|
||||
</div>
|
||||
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||
<p>Mesh networking now supports Meshtastic radios over the serial API alongside existing MeshCore Companion USB radios. The shared listener probes preferred and auto-detected serial paths for both firmware families.</p>
|
||||
<p>Meshtastic text packets are translated into Archipelago's existing mesh frame pipeline, and Meshtastic node information appears as normal mesh contacts using stable synthetic public keys.</p>
|
||||
<p>Frontend OTA behavior improved: hashed assets no longer fall back to index.html, the HTML shell revalidates on every load, and runtime promotion installs the bundled nginx config on update.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- v1.7.57-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.57-alpha</span>
|
||||
<span class="text-xs text-white/40">May 17, 2026</span>
|
||||
</div>
|
||||
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||
<p>Nginx Proxy Manager now avoids privileged rootless Podman host port 81, preferring 8081 for its admin UI while host nginx keeps a compatibility proxy on :81 for stale launch buttons.</p>
|
||||
<p>App installs allocate ports by checking live host bind availability, falling back to a free high port when preferred ports are occupied. Portainer-created launchable containers now appear in a Websites tab through their discovered host ports.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- v1.7.56-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.56-alpha</span>
|
||||
<span class="text-xs text-white/40">May 15, 2026</span>
|
||||
</div>
|
||||
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||
<p>Fresh installs include the full Wi-Fi userspace stack and grant the Archipelago service user NetworkManager PolicyKit access, so Intel Wi-Fi scanning and connection changes work from the web UI.</p>
|
||||
<p>Container health and reconciliation are more honest and resilient: stale alerts clear, Stopping containers can be recreated, health states come from Podman, and drifted Quadlet settings trigger proper restarts.</p>
|
||||
<p>Bitcoin Knots and ElectrumX get more CPU and memory headroom, LND helpers tolerate container-owned files better, and the screensaver stays out of media-heavy app sessions.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- v1.7.55-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.55-alpha</span>
|
||||
<span class="text-xs text-white/40">May 13, 2026</span>
|
||||
</div>
|
||||
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||
<p>Container reconcile can force-recreate Podman records stuck in Stopping while preserving bind-mounted app data, recovering wedged containers automatically.</p>
|
||||
<p>Lifecycle audits on the hardened container layer passed on the validation node, with direct app probes returning healthy responses.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- v1.7.54-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.54-alpha</span>
|
||||
<span class="text-xs text-white/40">May 6, 2026</span>
|
||||
</div>
|
||||
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||
<p>Existing installs now self-repair nginx backend proxy locations for Bitcoin status and app catalog calls, including hosts where the active config is a copied file rather than a symlink.</p>
|
||||
<p>LND UI is consistently served on port 18083 across first boot, Tor config, Quadlet reconciliation, OTA runtime payloads, and ISO scripts. OTA frontend tarballs also carry a cleaner runtime payload so startup promotion does not reintroduce stale host assets.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- v1.7.53-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.53-alpha</span>
|
||||
<span class="text-xs text-white/40">May 5, 2026</span>
|
||||
</div>
|
||||
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||
<p>Bitcoin Knots/Core config generation no longer duplicates RPC bind and port settings between bitcoin.conf and container command arguments, fixing startup failures from RPC endpoint binding conflicts.</p>
|
||||
<p>Legacy Bitcoin healthchecks no longer depend on bitcoin-cli being present in current images. Update checks now prefer manifest OTA releases over stale git remotes unless git updates are explicitly enabled.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- v1.7.52-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.52-alpha</span>
|
||||
<span class="text-xs text-white/40">May 5, 2026</span>
|
||||
</div>
|
||||
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||
<p>Tailscale now launches its local installed web UI on port 8240 and starts tailscaled before tailscale web, fixing unreachable installs after container creation.</p>
|
||||
<p>Grafana lifecycle actions repair missing rootless host listeners on port 3000, and Debian 13 install paths pull security updates from trixie-security during image/install creation.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- v1.7.49-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.49-alpha</span>
|
||||
<span class="text-xs text-white/40">Apr 30, 2026</span>
|
||||
</div>
|
||||
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||
<p>Bitcoin Knots/Core UI now reports connection, reconnecting, syncing, and error states from a backend status bridge instead of showing stale connection failures while the node warms up.</p>
|
||||
<p>ElectrumX exposes indexed height, local Bitcoin height, known headers, status, and progress source, making long initial indexing states readable. Bitcoin Core and Bitcoin Knots are now mutually exclusive variants with corrected install conflict handling.</p>
|
||||
<p>IndeeHub launches only on its direct web UI port, and BTCPay/NBXplorer Postgres environment formatting was fixed to avoid malformed connection strings.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- v1.7.48-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.48-alpha</span>
|
||||
<span class="text-xs text-white/40">Apr 29, 2026</span>
|
||||
</div>
|
||||
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||
<p>archipelago.service now creates /run/containers before startup, fixing systemd mount-namespace failures on nodes where that runtime directory did not already exist.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- v1.7.47-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.47-alpha</span>
|
||||
<span class="text-xs text-white/40">Apr 29, 2026</span>
|
||||
</div>
|
||||
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||
<p>Bitcoin Knots/Core sync is significantly faster: containers now use every available core for script verification and have 8 GB of memory so the 4 GB UTXO cache has headroom.</p>
|
||||
<p>ElectrumX initial indexing is faster too, with CPU caps removed, 4 GB of container memory, and a 3 GB internal cache.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- v1.7.46-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.46-alpha</span>
|
||||
<span class="text-xs text-white/40">Apr 29, 2026</span>
|
||||
</div>
|
||||
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||
<p>Health monitoring no longer pages auto-restart failures for orphaned containers left behind after Bitcoin variant switches.</p>
|
||||
<p>Apps no longer disappear from My Apps when an install fails, and multi-image stack pull progress now advances during the download phase instead of sticking at 20%.</p>
|
||||
<p>Several docker.io images were mirrored into Archipelago registries, reducing first-boot install dependency on Docker Hub.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- v1.7.45-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.45-alpha</span>
|
||||
<span class="text-xs text-white/40">Apr 29, 2026</span>
|
||||
</div>
|
||||
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||
<p>Bitcoin RPC auth is durable across container restart, image update, and reboot. The dashboard no longer fails because registry-pulled images shipped stale baked-in credentials.</p>
|
||||
<p>Multi-container apps show real install progress, app cards stay visible while containers are being created, IndeedHub installs cleanly on fresh nodes, and Tailscale install no longer fails from a malformed command.</p>
|
||||
<p>The installer now allocates swap on the encrypted data partition, capped at 8 GB, so image builds and memory spikes are less likely to OOM the system.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- v1.7.44-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.44-alpha</span>
|
||||
<span class="text-xs text-white/40">Apr 28, 2026</span>
|
||||
</div>
|
||||
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||
<p>Container orchestration migration and release hardening continued, including OTA synchronization for rebuilt UI containers and aligned LND UI port handling across runtime specs.</p>
|
||||
<p>Release packaging moved toward tarball-only artifacts with archived ISO build recipes, keeping update payloads focused on the files existing nodes need.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- v1.7.43-alpha -->
|
||||
<div>
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
|
||||
@@ -71,22 +71,18 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { computed, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { RouterLink } from 'vue-router'
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
import { useHomeStatusStore } from '@/stores/homeStatus'
|
||||
|
||||
const cpuPercent = ref(0)
|
||||
const memUsed = ref(0)
|
||||
const memTotal = ref(0)
|
||||
const diskUsed = ref(0)
|
||||
const diskTotal = ref(0)
|
||||
const uptimeSecs = ref(0)
|
||||
const homeStatus = useHomeStatusStore()
|
||||
|
||||
const memPercent = computed(() => memTotal.value > 0 ? (memUsed.value / memTotal.value) * 100 : 0)
|
||||
const diskPercent = computed(() => diskTotal.value > 0 ? (diskUsed.value / diskTotal.value) * 100 : 0)
|
||||
const cpuPercent = computed(() => homeStatus.stats.cpuPercent)
|
||||
const memPercent = computed(() => homeStatus.stats.memPercent)
|
||||
const diskPercent = computed(() => homeStatus.stats.diskPercent)
|
||||
|
||||
const uptimeDisplay = computed(() => {
|
||||
const s = uptimeSecs.value
|
||||
const s = homeStatus.stats.uptimeSecs
|
||||
if (s === 0) return '--'
|
||||
const days = Math.floor(s / 86400)
|
||||
const hours = Math.floor((s % 86400) / 3600)
|
||||
@@ -102,22 +98,7 @@ function barColor(pct: number): string {
|
||||
}
|
||||
|
||||
async function loadStats() {
|
||||
try {
|
||||
const res = await rpcClient.call<{
|
||||
cpu_usage_percent: number
|
||||
mem_used_bytes: number
|
||||
mem_total_bytes: number
|
||||
disk_used_bytes: number
|
||||
disk_total_bytes: number
|
||||
uptime_secs: number
|
||||
}>({ method: 'system.stats' })
|
||||
cpuPercent.value = res.cpu_usage_percent
|
||||
memUsed.value = res.mem_used_bytes
|
||||
memTotal.value = res.mem_total_bytes
|
||||
diskUsed.value = res.disk_used_bytes
|
||||
diskTotal.value = res.disk_total_bytes
|
||||
uptimeSecs.value = res.uptime_secs
|
||||
} catch { /* unavailable */ }
|
||||
await homeStatus.refreshSystemStats()
|
||||
}
|
||||
|
||||
let refreshInterval: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
+18
-16
@@ -1,27 +1,29 @@
|
||||
{
|
||||
"version": "1.7.66-alpha",
|
||||
"release_date": "2026-05-18",
|
||||
"version": "1.7.70-alpha",
|
||||
"release_date": "2026-05-19",
|
||||
"changelog": [
|
||||
"Nginx Proxy Manager stale-port repair now detects stopped or `Created` Podman records by inspecting `podman ps -a` port metadata, covering records where `podman port nginx-proxy-manager` returns no mapping until start.",
|
||||
"Live recovery on `100.70.96.88` removed only the stale Nginx Proxy Manager container record and recreated it with `8081:81`, `8084:80`, and `8444:443`, preserving `/var/lib/archipelago/nginx-proxy-manager` data.",
|
||||
"Validation confirmed Nginx Proxy Manager recovered as healthy and responds through direct admin port `8081`, host compatibility port `81`, and `/app/nginx-proxy-manager/`."
|
||||
"NetBird is being corrected from the peer/client daemon image to the self-hosted NetBird control-plane stack with a launchable dashboard on port `8087`, a combined management/signal/relay server on `8086`, and STUN on UDP `3478`.",
|
||||
"App sessions now always launch local apps through direct host ports and carry an explicit dashboard return target, so closing an iframe returns to the launching dashboard screen instead of falling through to browser history or a 404.",
|
||||
"Mobile app launches ignore stale desktop panel state and route into the full app-session webview consistently.",
|
||||
"The desktop sidebar now pins the logo/version at the top and controller/online/mode controls at the bottom, with only the navigation section scrolling on shorter screens.",
|
||||
"Validation passed with catalog JSON checks, `scripts/image-versions.sh` syntax check, `npm run type-check`, `cargo fmt --all --check --manifest-path core/Cargo.toml`, and `cargo check -p archipelago --manifest-path core/Cargo.toml`."
|
||||
],
|
||||
"components": [
|
||||
{
|
||||
"name": "archipelago",
|
||||
"current_version": "1.7.66-alpha",
|
||||
"new_version": "1.7.66-alpha",
|
||||
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.66-alpha/archipelago",
|
||||
"sha256": "d410efd1acb6ae2f5e1270b585da8ff90c9de454fbf8b2648c37ddc4c1136794",
|
||||
"size_bytes": 42947056
|
||||
"current_version": "1.7.70-alpha",
|
||||
"new_version": "1.7.70-alpha",
|
||||
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.70-alpha/archipelago",
|
||||
"sha256": "894b9e20c2af444b46b136cc7ad85edb68ced3b4beade664d5275a9f5226e0ca",
|
||||
"size_bytes": 42976896
|
||||
},
|
||||
{
|
||||
"name": "archipelago-frontend-1.7.66-alpha.tar.gz",
|
||||
"current_version": "1.7.66-alpha",
|
||||
"new_version": "1.7.66-alpha",
|
||||
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.66-alpha/archipelago-frontend-1.7.66-alpha.tar.gz",
|
||||
"sha256": "35ca430accfb3b669c288800b59b899585435133d6d7e5da70c5066ae36a0308",
|
||||
"size_bytes": 166469699
|
||||
"name": "archipelago-frontend-1.7.70-alpha.tar.gz",
|
||||
"current_version": "1.7.70-alpha",
|
||||
"new_version": "1.7.70-alpha",
|
||||
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.70-alpha/archipelago-frontend-1.7.70-alpha.tar.gz",
|
||||
"sha256": "4c14cfb53f00b1649f4916403e8114d0599fa3b30cd290facd6fc951760e9fa2",
|
||||
"size_bytes": 166478441
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+18
-16
@@ -1,27 +1,29 @@
|
||||
{
|
||||
"version": "1.7.66-alpha",
|
||||
"release_date": "2026-05-18",
|
||||
"version": "1.7.70-alpha",
|
||||
"release_date": "2026-05-19",
|
||||
"changelog": [
|
||||
"Nginx Proxy Manager stale-port repair now detects stopped or `Created` Podman records by inspecting `podman ps -a` port metadata, covering records where `podman port nginx-proxy-manager` returns no mapping until start.",
|
||||
"Live recovery on `100.70.96.88` removed only the stale Nginx Proxy Manager container record and recreated it with `8081:81`, `8084:80`, and `8444:443`, preserving `/var/lib/archipelago/nginx-proxy-manager` data.",
|
||||
"Validation confirmed Nginx Proxy Manager recovered as healthy and responds through direct admin port `8081`, host compatibility port `81`, and `/app/nginx-proxy-manager/`."
|
||||
"NetBird is being corrected from the peer/client daemon image to the self-hosted NetBird control-plane stack with a launchable dashboard on port `8087`, a combined management/signal/relay server on `8086`, and STUN on UDP `3478`.",
|
||||
"App sessions now always launch local apps through direct host ports and carry an explicit dashboard return target, so closing an iframe returns to the launching dashboard screen instead of falling through to browser history or a 404.",
|
||||
"Mobile app launches ignore stale desktop panel state and route into the full app-session webview consistently.",
|
||||
"The desktop sidebar now pins the logo/version at the top and controller/online/mode controls at the bottom, with only the navigation section scrolling on shorter screens.",
|
||||
"Validation passed with catalog JSON checks, `scripts/image-versions.sh` syntax check, `npm run type-check`, `cargo fmt --all --check --manifest-path core/Cargo.toml`, and `cargo check -p archipelago --manifest-path core/Cargo.toml`."
|
||||
],
|
||||
"components": [
|
||||
{
|
||||
"name": "archipelago",
|
||||
"current_version": "1.7.66-alpha",
|
||||
"new_version": "1.7.66-alpha",
|
||||
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.66-alpha/archipelago",
|
||||
"sha256": "d410efd1acb6ae2f5e1270b585da8ff90c9de454fbf8b2648c37ddc4c1136794",
|
||||
"size_bytes": 42947056
|
||||
"current_version": "1.7.70-alpha",
|
||||
"new_version": "1.7.70-alpha",
|
||||
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.70-alpha/archipelago",
|
||||
"sha256": "894b9e20c2af444b46b136cc7ad85edb68ced3b4beade664d5275a9f5226e0ca",
|
||||
"size_bytes": 42976896
|
||||
},
|
||||
{
|
||||
"name": "archipelago-frontend-1.7.66-alpha.tar.gz",
|
||||
"current_version": "1.7.66-alpha",
|
||||
"new_version": "1.7.66-alpha",
|
||||
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.66-alpha/archipelago-frontend-1.7.66-alpha.tar.gz",
|
||||
"sha256": "35ca430accfb3b669c288800b59b899585435133d6d7e5da70c5066ae36a0308",
|
||||
"size_bytes": 166469699
|
||||
"name": "archipelago-frontend-1.7.70-alpha.tar.gz",
|
||||
"current_version": "1.7.70-alpha",
|
||||
"new_version": "1.7.70-alpha",
|
||||
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.70-alpha/archipelago-frontend-1.7.70-alpha.tar.gz",
|
||||
"sha256": "4c14cfb53f00b1649f4916403e8114d0599fa3b30cd290facd6fc951760e9fa2",
|
||||
"size_bytes": 166478441
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -510,6 +510,22 @@ fix_missing_rootless_ports() {
|
||||
$fixed && return 0 || return 1
|
||||
}
|
||||
|
||||
# ── Fix 11: Nginx Proxy Manager public host bridge ───────────
|
||||
# Host nginx owns public 80/443 on Archipelago. Mirror NPM proxy hosts into
|
||||
# host nginx so issued certs and public traffic reach the intended upstreams.
|
||||
fix_npm_public_hosts() {
|
||||
local script="/opt/archipelago/scripts/sync-npm-public-hosts.sh"
|
||||
[ -x "$script" ] || script="$SCRIPT_DIR/sync-npm-public-hosts.sh"
|
||||
[ -x "$script" ] || return 1
|
||||
[ -f /var/lib/archipelago/nginx-proxy-manager/data/database.sqlite ] || return 1
|
||||
|
||||
if "$script" >/dev/null 2>&1; then
|
||||
log "Synced Nginx Proxy Manager public hosts into host nginx"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# ── Main ─────────────────────────────────────────────────────
|
||||
|
||||
# If remote host provided, run via SSH
|
||||
@@ -539,6 +555,7 @@ run_fix "exit-127" fix_exit_127
|
||||
run_fix "netns-egress" fix_rootless_netns_egress
|
||||
run_fix "stopped-core" fix_stopped_core_containers
|
||||
run_fix "rootless-ports" fix_missing_rootless_ports
|
||||
run_fix "npm-public-hosts" fix_npm_public_hosts
|
||||
|
||||
echo ""
|
||||
if [ $FIXES_APPLIED -gt 0 ]; then
|
||||
|
||||
@@ -175,9 +175,11 @@ load_spec_bitcoin-knots() {
|
||||
SPEC_TIER="1"
|
||||
SPEC_DATA_DIR="/var/lib/archipelago/bitcoin"
|
||||
SPEC_DATA_UID="100101:100101"
|
||||
local btc_dbcache=4096
|
||||
[ "${LOW_MEM:-false}" = "true" ] && btc_dbcache=2048
|
||||
# Dynamic: prune on small disk
|
||||
if [ "${DISK_GB:-0}" -lt 1000 ]; then
|
||||
SPEC_CUSTOM_ARGS="-server=1 -prune=550 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 -listen=1 -bind=0.0.0.0:8333 -dbcache=2048 -par=0 -maxconnections=125"
|
||||
SPEC_CUSTOM_ARGS="-server=1 -prune=550 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 -listen=1 -bind=0.0.0.0:8333 -dbcache=${btc_dbcache} -par=0 -maxconnections=125"
|
||||
else
|
||||
SPEC_CUSTOM_ARGS="-server=1 -txindex=1 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 -listen=1 -bind=0.0.0.0:8333 -dbcache=4096 -par=0 -maxconnections=125"
|
||||
fi
|
||||
@@ -271,7 +273,7 @@ load_spec_btcpay-server() {
|
||||
SPEC_PORTS="23000:49392"
|
||||
SPEC_VOLUMES="/var/lib/archipelago/btcpay:/datadir"
|
||||
SPEC_MEMORY="$(mem_limit btcpay-server)"
|
||||
SPEC_HEALTH_CMD="curl -sf http://localhost:49392/ || exit 1"
|
||||
SPEC_HEALTH_CMD="bash -ec '</dev/tcp/127.0.0.1/49392'"
|
||||
SPEC_ENV="ASPNETCORE_URLS=http://0.0.0.0:49392 BTCPAY_PROTOCOL=http BTCPAY_HOST=$HOST_IP:23000 BTCPAY_CHAINS=btc BTCPAY_BTCEXPLORERURL=http://archy-nbxplorer:32838 BTCPAY_BTCRPCURL=http://bitcoin-knots:8332 BTCPAY_BTCRPCUSER=$BITCOIN_RPC_USER BTCPAY_BTCRPCPASSWORD=$BITCOIN_RPC_PASS BTCPAY_POSTGRES=Username=btcpay;Password=$BTCPAY_DB_PASS;Host=archy-btcpay-db;Port=5432;Database=btcpay"
|
||||
SPEC_TIER="2"
|
||||
SPEC_DATA_DIR="/var/lib/archipelago/btcpay"
|
||||
|
||||
@@ -156,6 +156,14 @@ mkdir -p "$PROJECT_ROOT/releases"
|
||||
"$SCRIPT_DIR/create-release-manifest.sh" --version "$VERSION" --date "$RELEASE_DATE" --output "$PROJECT_ROOT/releases/manifest.json" 2>&1 | grep -v "^$"
|
||||
cp "$PROJECT_ROOT/releases/manifest.json" "$PROJECT_ROOT/release-manifest.json"
|
||||
|
||||
echo "[6b/8] Staging release artifacts for validation..."
|
||||
VERSION_DIR="$PROJECT_ROOT/releases/v${VERSION}"
|
||||
FRONTEND_ARCHIVE="/tmp/archipelago-frontend-${VERSION}.tar.gz"
|
||||
mkdir -p "$VERSION_DIR"
|
||||
install -m 0755 "$PROJECT_ROOT/core/target/release/archipelago" "$VERSION_DIR/archipelago"
|
||||
install -m 0644 "$FRONTEND_ARCHIVE" "$VERSION_DIR/archipelago-frontend-${VERSION}.tar.gz"
|
||||
"$SCRIPT_DIR/check-release-manifest.sh"
|
||||
|
||||
echo "[7/8] Committing version bump..."
|
||||
git -C "$PROJECT_ROOT" add \
|
||||
core/archipelago/Cargo.toml \
|
||||
@@ -179,15 +187,13 @@ echo " - Version bumped in Cargo.toml and package.json"
|
||||
echo " - Changelog updated in CHANGELOG.md"
|
||||
echo " - Release manifest: releases/manifest.json"
|
||||
echo " - Release manifest copy: release-manifest.json"
|
||||
echo " - Staged artifacts: releases/v${VERSION}/"
|
||||
echo " - Git tag: v${VERSION}"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " 1. Review: git log --oneline -5"
|
||||
echo " 2. Create Gitea release v${VERSION} and upload artifacts:"
|
||||
echo " archipelago"
|
||||
echo " archipelago-frontend-${VERSION}.tar.gz"
|
||||
echo " 3. Push to gitea-local and gitea-vps2:"
|
||||
echo " git push gitea-local main --tags && git push gitea-vps2 main --tags"
|
||||
echo " 4. Verify manifest is live on both mirrors:"
|
||||
echo " 2. Publish commits, tag, artifacts, and verify download URLs:"
|
||||
echo " scripts/publish-release-assets.sh ${VERSION} gitea-vps2"
|
||||
echo " 3. Verify manifest is live on both mirrors:"
|
||||
echo " curl -fsS http://localhost:3000/lfg2025/archy/raw/branch/main/releases/manifest.json"
|
||||
echo " curl -fsS http://146.59.87.168:3000/lfg2025/archy/raw/branch/main/releases/manifest.json"
|
||||
|
||||
@@ -625,7 +625,7 @@ deploy_node() {
|
||||
echo ' Creating btcpay-server...'
|
||||
sudo mkdir -p /var/lib/archipelago/btcpay
|
||||
\$DOCKER run -d --name btcpay-server --restart unless-stopped \$NET_OPT \
|
||||
--health-cmd 'curl -sf http://localhost:49392/' --health-interval=30s --health-timeout=10s --health-retries=3 \
|
||||
--health-cmd "bash -ec '</dev/tcp/127.0.0.1/49392'" --health-interval=30s --health-timeout=10s --health-retries=3 \
|
||||
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \
|
||||
--security-opt no-new-privileges:true \
|
||||
-p 23000:49392 -v /var/lib/archipelago/btcpay:/datadir \
|
||||
@@ -909,7 +909,8 @@ LNDCONF
|
||||
if \$DOCKER ps -a --format '{{.Names}}' 2>/dev/null | grep -qx nginx-proxy-manager; then
|
||||
\$DOCKER start nginx-proxy-manager 2>/dev/null || true
|
||||
else
|
||||
sudo mkdir -p /var/lib/archipelago/nginx-proxy-manager/data /var/lib/archipelago/nginx-proxy-manager/letsencrypt
|
||||
sudo mkdir -p /var/lib/archipelago/nginx-proxy-manager/data/letsencrypt-acme-challenge/.well-known/acme-challenge /var/lib/archipelago/nginx-proxy-manager/letsencrypt
|
||||
sudo chown -R 1000:1000 /var/lib/archipelago/nginx-proxy-manager 2>/dev/null || true
|
||||
\$DOCKER run -d --name nginx-proxy-manager --restart unless-stopped \
|
||||
--health-cmd 'curl -sf http://localhost:81/' --health-interval=30s --health-timeout=5s --health-retries=3 \
|
||||
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID --cap-add NET_BIND_SERVICE \
|
||||
|
||||
@@ -481,10 +481,13 @@ log "archy-net network ready"
|
||||
log "Fixing rootless podman UID mapping..."
|
||||
# Containers running as root (UID 0 → host UID 100000)
|
||||
for dir in lnd electrumx btcpay nbxplorer jellyfin vaultwarden \
|
||||
home-assistant fedimint fedimint-gateway photoprism ollama filebrowser \
|
||||
nextcloud uptime-kuma nginx-proxy-manager portainer nostr-rs-relay; do
|
||||
home-assistant fedimint fedimint-gateway photoprism ollama filebrowser \
|
||||
nextcloud uptime-kuma portainer nostr-rs-relay; do
|
||||
[ -d "/var/lib/archipelago/$dir" ] && chown -R 100000:100000 "/var/lib/archipelago/$dir" 2>/dev/null
|
||||
done
|
||||
# Nginx Proxy Manager runs as root in the rootless user namespace, which maps to
|
||||
# the archipelago user on host bind mounts. Keep certbot's webroot writable.
|
||||
[ -d /var/lib/archipelago/nginx-proxy-manager ] && chown -R 1000:1000 /var/lib/archipelago/nginx-proxy-manager 2>/dev/null
|
||||
# Bitcoin Knots: container UID 101 → host UID 100101
|
||||
[ -d /var/lib/archipelago/bitcoin ] && chown -R 100101:100101 /var/lib/archipelago/bitcoin 2>/dev/null
|
||||
# Postgres: container UID 70 → host UID 100070
|
||||
@@ -508,13 +511,13 @@ LOW_MEM=false
|
||||
|
||||
mem_limit() {
|
||||
case "$1" in
|
||||
bitcoin-knots) $LOW_MEM && echo "2g" || echo "4g";;
|
||||
bitcoin|bitcoin-core|bitcoin-knots) $LOW_MEM && echo "4g" || echo "8g";;
|
||||
cryptpad) echo "512m";;
|
||||
ollama) $LOW_MEM && echo "1g" || echo "4g";;
|
||||
lnd) echo "512m";;
|
||||
electrumx) echo "1g";;
|
||||
electrumx|mempool-electrs|electrs) echo "4g";;
|
||||
nextcloud) echo "1g";;
|
||||
btcpay-server) echo "1g";;
|
||||
btcpay-server|btcpayserver) echo "1g";;
|
||||
homeassistant) echo "512m";;
|
||||
fedimint) echo "512m";;
|
||||
fedimint-gateway) echo "512m";;
|
||||
@@ -588,7 +591,11 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -qE 'bitcoin-knots|arch
|
||||
[ -z "$DISK_GB" ] && DISK_GB=$(df --output=size -BG / 2>/dev/null | tail -1 | tr -dc '0-9')
|
||||
if [ "${DISK_GB:-0}" -lt 1000 ]; then
|
||||
BTC_EXTRA_ARGS="-prune=550"
|
||||
BTC_DBCACHE=2048
|
||||
if [ "$LOW_MEM" = "true" ]; then
|
||||
BTC_DBCACHE=2048
|
||||
else
|
||||
BTC_DBCACHE=4096
|
||||
fi
|
||||
log " Small disk (${DISK_GB}GB) — enabling pruning"
|
||||
else
|
||||
BTC_EXTRA_ARGS="-txindex=1"
|
||||
@@ -871,7 +878,7 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q btcpay-server; then
|
||||
log "Creating BTCPay Server..."
|
||||
mkdir -p /var/lib/archipelago/btcpay/Main
|
||||
$DOCKER run -d --name btcpay-server --restart unless-stopped \
|
||||
--health-cmd="curl -sf http://localhost:49392/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
|
||||
--health-cmd="bash -ec '</dev/tcp/127.0.0.1/49392'" --health-interval=120s --health-timeout=5s --health-retries=3 \
|
||||
--memory=$(mem_limit btcpay-server) --network archy-net --network-alias btcpay-server \
|
||||
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \
|
||||
--security-opt no-new-privileges:true \
|
||||
@@ -1177,6 +1184,8 @@ track_container "filebrowser"
|
||||
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q nginx-proxy-manager; then
|
||||
log "Creating Nginx Proxy Manager..."
|
||||
mkdir -p /var/lib/archipelago/nginx-proxy-manager/data /var/lib/archipelago/nginx-proxy-manager/letsencrypt
|
||||
mkdir -p /var/lib/archipelago/nginx-proxy-manager/data/letsencrypt-acme-challenge/.well-known/acme-challenge
|
||||
chown -R 1000:1000 /var/lib/archipelago/nginx-proxy-manager 2>/dev/null || true
|
||||
NPM_ADMIN_PORT=$(alloc_port nginx-proxy-manager 8081)
|
||||
NPM_HTTP_PORT=$(alloc_port nginx-proxy-manager-http 8084)
|
||||
NPM_HTTPS_PORT=$(alloc_port nginx-proxy-manager-https 8444)
|
||||
|
||||
@@ -24,7 +24,7 @@ MEMPOOL_WEB_IMAGE="$ARCHY_REGISTRY/mempool-frontend:v3.0.0"
|
||||
MARIADB_IMAGE="$ARCHY_REGISTRY/mariadb:11.4.10"
|
||||
|
||||
# BTCPay
|
||||
BTCPAY_IMAGE="$ARCHY_REGISTRY/btcpayserver:1.13.7"
|
||||
BTCPAY_IMAGE="docker.io/btcpayserver/btcpayserver:2.3.9"
|
||||
NBXPLORER_IMAGE="$ARCHY_REGISTRY/nbxplorer:2.6.0"
|
||||
POSTGRES_IMAGE="$ARCHY_REGISTRY/postgres:15.17"
|
||||
BTCPAY_POSTGRES_IMAGE="$ARCHY_REGISTRY/postgres:15.17"
|
||||
@@ -48,6 +48,8 @@ PORTAINER_IMAGE="$ARCHY_REGISTRY/portainer:latest"
|
||||
|
||||
# Networking
|
||||
TAILSCALE_IMAGE="$ARCHY_REGISTRY/tailscale:stable"
|
||||
NETBIRD_DASHBOARD_IMAGE="docker.io/netbirdio/dashboard:v2.38.0"
|
||||
NETBIRD_SERVER_IMAGE="docker.io/netbirdio/netbird-server:0.71.2"
|
||||
ALPINE_TOR_IMAGE="$ARCHY_REGISTRY/alpine-tor:0.4.8.13"
|
||||
ADGUARDHOME_IMAGE="$ARCHY_REGISTRY/adguardhome:v0.107.55"
|
||||
|
||||
|
||||
@@ -96,17 +96,17 @@ mem_limit() {
|
||||
return
|
||||
fi
|
||||
|
||||
# Built-in defaults (keep in sync with first-boot-containers.sh)
|
||||
# Built-in defaults (keep in sync with first-boot-containers.sh and Rust package config)
|
||||
local low="${LOW_MEM:-false}"
|
||||
case "$name" in
|
||||
bitcoin-knots) $low && echo "1g" || echo "2g" ;;
|
||||
bitcoin|bitcoin-core|bitcoin-knots) $low && echo "4g" || echo "8g" ;;
|
||||
onlyoffice) $low && echo "1g" || echo "2g" ;;
|
||||
ollama) $low && echo "1g" || echo "4g" ;;
|
||||
lnd) echo "512m" ;;
|
||||
electrumx) echo "1g" ;;
|
||||
electrumx|mempool-electrs|electrs) echo "4g" ;;
|
||||
nextcloud) echo "1g" ;;
|
||||
immich_server) echo "1g" ;;
|
||||
btcpay-server) echo "1g" ;;
|
||||
btcpay-server|btcpayserver) echo "1g" ;;
|
||||
homeassistant) echo "512m" ;;
|
||||
fedimint) echo "512m" ;;
|
||||
fedimint-gateway) echo "512m" ;;
|
||||
|
||||
Executable
+102
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env bash
|
||||
# Publish an Archipelago OTA release to a Gitea remote and verify downloads.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
VERSION="${1:-}"
|
||||
REMOTE="${2:-gitea-vps2}"
|
||||
|
||||
if [ -z "$VERSION" ]; then
|
||||
echo "Usage: $0 VERSION [remote]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
VERSION_DIR="$PROJECT_ROOT/releases/v${VERSION}"
|
||||
BACKEND="$VERSION_DIR/archipelago"
|
||||
FRONTEND="$VERSION_DIR/archipelago-frontend-${VERSION}.tar.gz"
|
||||
|
||||
fail() { echo "Error: $*" >&2; exit 1; }
|
||||
|
||||
[ -f "$PROJECT_ROOT/releases/manifest.json" ] || fail "releases/manifest.json missing"
|
||||
[ -f "$BACKEND" ] || fail "backend artifact missing: $BACKEND"
|
||||
[ -f "$FRONTEND" ] || fail "frontend artifact missing: $FRONTEND"
|
||||
|
||||
"$SCRIPT_DIR/check-release-manifest.sh"
|
||||
|
||||
remote_url=$(git -C "$PROJECT_ROOT" remote get-url "$REMOTE")
|
||||
case "$remote_url" in
|
||||
http://*@*) ;;
|
||||
*) fail "$REMOTE must be an authenticated http:// Gitea remote URL for API uploads" ;;
|
||||
esac
|
||||
|
||||
auth=${remote_url#http://}
|
||||
auth=${auth%@*}
|
||||
host_path=${remote_url#http://$auth@}
|
||||
host=${host_path%%/*}
|
||||
repo_path=${host_path#*/}
|
||||
repo_path=${repo_path%.git}
|
||||
api="http://$host/api/v1/repos/$repo_path"
|
||||
release_url="$api/releases/tags/v${VERSION}"
|
||||
|
||||
echo "Pushing main and v${VERSION} to $REMOTE..."
|
||||
git -C "$PROJECT_ROOT" push "$REMOTE" main "refs/tags/v${VERSION}"
|
||||
|
||||
release_json=$(curl -fsS -u "$auth" "$release_url" || true)
|
||||
if [ -z "$release_json" ]; then
|
||||
echo "Creating Gitea release v${VERSION}..."
|
||||
release_body=$(python3 - "$VERSION" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
|
||||
version = sys.argv[1]
|
||||
print(json.dumps({
|
||||
"tag_name": f"v{version}",
|
||||
"target_commitish": "main",
|
||||
"name": f"v{version}",
|
||||
"body": f"Archipelago v{version} release artifacts for OTA updates.",
|
||||
"draft": False,
|
||||
"prerelease": True,
|
||||
}))
|
||||
PY
|
||||
)
|
||||
release_json=$(curl -fsS -u "$auth" -H 'Content-Type: application/json' -d "$release_body" "$api/releases")
|
||||
fi
|
||||
|
||||
release_id=$(printf '%s' "$release_json" | python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])')
|
||||
|
||||
asset_names=$(curl -fsS -u "$auth" "$api/releases/$release_id/assets" | python3 -c 'import json,sys; print("\n".join(a["name"] for a in json.load(sys.stdin)))')
|
||||
upload_asset() {
|
||||
local path="$1"
|
||||
local name="$2"
|
||||
if printf '%s\n' "$asset_names" | grep -Fxq "$name"; then
|
||||
echo "Asset $name already exists; leaving it in place."
|
||||
return
|
||||
fi
|
||||
echo "Uploading $name..."
|
||||
curl --fail --show-error --silent --http1.1 --connect-timeout 20 --max-time 900 \
|
||||
-u "$auth" \
|
||||
-F "attachment=@$path" \
|
||||
"$api/releases/$release_id/assets?name=$name" >/dev/null
|
||||
asset_names=$(printf '%s\n%s\n' "$asset_names" "$name")
|
||||
}
|
||||
|
||||
upload_asset "$BACKEND" "archipelago"
|
||||
upload_asset "$FRONTEND" "archipelago-frontend-${VERSION}.tar.gz"
|
||||
|
||||
echo "Verifying public download URLs from manifest..."
|
||||
python3 - "$PROJECT_ROOT/releases/manifest.json" <<'PY' | while read -r url size; do
|
||||
import json
|
||||
import sys
|
||||
|
||||
manifest = json.load(open(sys.argv[1]))
|
||||
for component in manifest["components"]:
|
||||
print(component["download_url"], component["size_bytes"])
|
||||
PY
|
||||
headers=$(curl -fsSI -L --max-time 60 "$url") || fail "download URL failed: $url"
|
||||
actual_size=$(printf '%s\n' "$headers" | awk 'tolower($1)=="content-length:" { size=$2 } END { gsub("\r", "", size); print size }')
|
||||
[ "$actual_size" = "$size" ] || fail "download size mismatch for $url (expected $size, got ${actual_size:-missing})"
|
||||
done
|
||||
|
||||
echo "Release v${VERSION} published and verified on $REMOTE."
|
||||
+10
-1
@@ -75,6 +75,15 @@ fi
|
||||
|
||||
cd "$REPO_DIR"
|
||||
|
||||
if ! command -v nano >/dev/null 2>&1; then
|
||||
log "Installing nano for Archipelago terminal..."
|
||||
if sudo apt-get update -qq 2>>"$LOG_FILE" && sudo apt-get install -y -qq nano 2>>"$LOG_FILE"; then
|
||||
ok "nano installed"
|
||||
else
|
||||
warn "Unable to install nano automatically; continuing update"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Fetch latest
|
||||
log "Fetching from origin..."
|
||||
git fetch origin main --quiet 2>>"$LOG_FILE"
|
||||
@@ -186,7 +195,7 @@ fi
|
||||
# for backward compatibility with older binaries that still look there.
|
||||
SCRIPTS_DEST="/opt/archipelago/scripts"
|
||||
sudo mkdir -p "$SCRIPTS_DEST"
|
||||
for script in image-versions.sh reconcile-containers.sh container-specs.sh container-doctor.sh app-surface-smoke-test.sh bitcoin-stack-lifecycle-test.sh; do
|
||||
for script in image-versions.sh reconcile-containers.sh container-specs.sh container-doctor.sh sync-npm-public-hosts.sh app-surface-smoke-test.sh bitcoin-stack-lifecycle-test.sh; do
|
||||
src="$REPO_DIR/scripts/$script"
|
||||
if [ -f "$src" ]; then
|
||||
sudo install -m 755 "$src" "$SCRIPTS_DEST/$script"
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
DB="/var/lib/archipelago/nginx-proxy-manager/data/database.sqlite"
|
||||
OUT="/etc/nginx/conf.d/public-npm-proxy-hosts.conf"
|
||||
ACME_ROOT="/var/lib/archipelago/nginx-proxy-manager/data/letsencrypt-acme-challenge"
|
||||
LE_ROOT="/var/lib/archipelago/nginx-proxy-manager/letsencrypt/live"
|
||||
|
||||
[ -f "$DB" ] || exit 0
|
||||
mkdir -p "$ACME_ROOT/.well-known/acme-challenge"
|
||||
chown -R 1000:1000 /var/lib/archipelago/nginx-proxy-manager 2>/dev/null || true
|
||||
|
||||
tmp=$(mktemp)
|
||||
trap 'rm -f "$tmp"' EXIT
|
||||
|
||||
python3 - "$DB" "$ACME_ROOT" "$LE_ROOT" >"$tmp" <<'PY'
|
||||
import json
|
||||
import os
|
||||
import sqlite3
|
||||
import sys
|
||||
|
||||
db, acme_root, le_root = sys.argv[1:]
|
||||
con = sqlite3.connect(db)
|
||||
con.row_factory = sqlite3.Row
|
||||
|
||||
rows = con.execute(
|
||||
"""
|
||||
select p.id, p.domain_names, p.forward_scheme, p.forward_host, p.forward_port,
|
||||
p.certificate_id, p.ssl_forced, c.provider
|
||||
from proxy_host p
|
||||
left join certificate c on c.id = p.certificate_id
|
||||
where p.enabled = 1 and p.certificate_id > 0
|
||||
order by p.id
|
||||
"""
|
||||
).fetchall()
|
||||
|
||||
print("# Generated by sync-npm-public-hosts.sh; do not edit by hand.")
|
||||
for row in rows:
|
||||
try:
|
||||
domains = [d for d in json.loads(row["domain_names"] or "[]") if d]
|
||||
except Exception:
|
||||
domains = []
|
||||
if not domains:
|
||||
continue
|
||||
cert_id = row["certificate_id"]
|
||||
cert = f"{le_root}/npm-{cert_id}/fullchain.pem"
|
||||
key = f"{le_root}/npm-{cert_id}/privkey.pem"
|
||||
if row["provider"] != "letsencrypt":
|
||||
continue
|
||||
if not os.path.isfile(cert) or not os.path.isfile(key):
|
||||
continue
|
||||
names = " ".join(domains)
|
||||
scheme = row["forward_scheme"] or "http"
|
||||
host = row["forward_host"]
|
||||
port = row["forward_port"]
|
||||
if not host or not port:
|
||||
continue
|
||||
# NPM containers use this name to reach host-published services; host nginx
|
||||
# itself should use loopback for the same services.
|
||||
nginx_host = "127.0.0.1" if host == "host.containers.internal" else host
|
||||
|
||||
print(f"""
|
||||
server {{
|
||||
listen 80;
|
||||
server_name {names};
|
||||
|
||||
location ^~ /.well-known/acme-challenge/ {{
|
||||
default_type text/plain;
|
||||
root {acme_root};
|
||||
try_files $uri =404;
|
||||
}}
|
||||
|
||||
location / {{
|
||||
return 301 https://$host$request_uri;
|
||||
}}
|
||||
}}
|
||||
|
||||
server {{
|
||||
listen 443 ssl;
|
||||
server_name {names};
|
||||
ssl_certificate {cert};
|
||||
ssl_certificate_key {key};
|
||||
|
||||
location / {{
|
||||
proxy_pass {scheme}://{nginx_host}:{port};
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header X-Forwarded-Scheme https;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
}}
|
||||
}}
|
||||
""")
|
||||
PY
|
||||
|
||||
backup=""
|
||||
if [ -f "$OUT" ]; then
|
||||
backup=$(mktemp)
|
||||
cp "$OUT" "$backup"
|
||||
fi
|
||||
|
||||
restore_previous() {
|
||||
if [ -n "$backup" ] && [ -f "$backup" ]; then
|
||||
install -m 0644 "$backup" "$OUT"
|
||||
else
|
||||
rm -f "$OUT"
|
||||
fi
|
||||
}
|
||||
|
||||
if ! install -m 0644 "$tmp" "$OUT" || ! nginx -t >/dev/null; then
|
||||
restore_previous
|
||||
nginx -t >/dev/null 2>&1 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
systemctl reload nginx
|
||||
@@ -135,7 +135,7 @@ image_for() {
|
||||
case "$1" in
|
||||
bitcoin-knots) echo "146.59.87.168:3000/lfg2025/bitcoin-knots:latest" ;;
|
||||
bitcoin-core) echo "docker.io/bitcoin/bitcoin:28.4" ;;
|
||||
btcpay-server) echo "146.59.87.168:3000/lfg2025/btcpayserver:1.13.7" ;;
|
||||
btcpay-server) echo "docker.io/btcpayserver/btcpayserver:2.3.9" ;;
|
||||
lnd) echo "146.59.87.168:3000/lfg2025/lnd:v0.18.4-beta" ;;
|
||||
mempool) echo "146.59.87.168:3000/lfg2025/mempool-frontend:v3.0.0" ;;
|
||||
homeassistant) echo "146.59.87.168:3000/lfg2025/home-assistant:2024.1" ;;
|
||||
|
||||
Reference in New Issue
Block a user