chore: release v1.7.57-alpha
This commit is contained in:
parent
a322b04021
commit
7804223152
@ -1,5 +1,13 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v1.7.57-alpha (2026-05-17)
|
||||||
|
|
||||||
|
- Nginx Proxy Manager now avoids privileged rootless Podman host port `81`, preferring `8081:81` while host nginx keeps a compatibility proxy on `:81` for stale cached launch buttons.
|
||||||
|
- App installs now allocate ports by checking live host bind availability, falling back to a free high port when preferred ports are already occupied.
|
||||||
|
- Portainer-created launchable containers are separated into a `Websites` tab and launch through their discovered published host port instead of hard-coded app URLs.
|
||||||
|
- Internal BuildKit helper containers such as `buildx_buildkit_default` are hidden from the Apps UI.
|
||||||
|
- Portainer works out of the box on Debian 13/Podman installs by including `catatonit` and by preserving the Podman socket mount as a socket rather than creating it as a directory.
|
||||||
|
|
||||||
## v1.7.56-alpha (2026-05-15)
|
## v1.7.56-alpha (2026-05-15)
|
||||||
|
|
||||||
- Health notifications now clear when an app is no longer unhealthy, including stale alerts for removed containers such as Portainer.
|
- Health notifications now clear when an app is no longer unhealthy, including stale alerts for removed containers such as Portainer.
|
||||||
|
|||||||
2
core/Cargo.lock
generated
2
core/Cargo.lock
generated
@ -80,7 +80,7 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "archipelago"
|
name = "archipelago"
|
||||||
version = "1.7.56-alpha"
|
version = "1.7.57-alpha"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"archipelago-container",
|
"archipelago-container",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "archipelago"
|
name = "archipelago"
|
||||||
version = "1.7.56-alpha"
|
version = "1.7.57-alpha"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Archipelago Bitcoin Node OS - Native backend"
|
description = "Archipelago Bitcoin Node OS - Native backend"
|
||||||
authors = ["Archipelago Team"]
|
authors = ["Archipelago Team"]
|
||||||
|
|||||||
@ -153,11 +153,13 @@ pub(super) fn get_app_capabilities(app_id: &str) -> Vec<String> {
|
|||||||
"--cap-add=SETGID".to_string(),
|
"--cap-add=SETGID".to_string(),
|
||||||
"--cap-add=DAC_OVERRIDE".to_string(),
|
"--cap-add=DAC_OVERRIDE".to_string(),
|
||||||
],
|
],
|
||||||
// Nginx Proxy Manager needs to bind low ports
|
// Nginx Proxy Manager initializes/chowns mounted state on first boot.
|
||||||
"nginx-proxy-manager" => vec![
|
"nginx-proxy-manager" => vec![
|
||||||
"--cap-add=CHOWN".to_string(),
|
"--cap-add=CHOWN".to_string(),
|
||||||
|
"--cap-add=FOWNER".to_string(),
|
||||||
"--cap-add=SETUID".to_string(),
|
"--cap-add=SETUID".to_string(),
|
||||||
"--cap-add=SETGID".to_string(),
|
"--cap-add=SETGID".to_string(),
|
||||||
|
"--cap-add=DAC_OVERRIDE".to_string(),
|
||||||
"--cap-add=NET_BIND_SERVICE".to_string(),
|
"--cap-add=NET_BIND_SERVICE".to_string(),
|
||||||
],
|
],
|
||||||
// Bitcoin needs only file-ownership ops + NET_BIND_SERVICE for the
|
// Bitcoin needs only file-ownership ops + NET_BIND_SERVICE for the
|
||||||
@ -924,11 +926,24 @@ pub(super) async fn get_app_config(
|
|||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
"nginx-proxy-manager" => (
|
"nginx-proxy-manager" => {
|
||||||
|
let admin_port = allocator
|
||||||
|
.allocate_or_get(app_id, 8081, 81)
|
||||||
|
.await
|
||||||
|
.unwrap_or(8081);
|
||||||
|
let http_port = allocator
|
||||||
|
.allocate_or_get("nginx-proxy-manager-http", 8084, 80)
|
||||||
|
.await
|
||||||
|
.unwrap_or(8084);
|
||||||
|
let https_port = allocator
|
||||||
|
.allocate_or_get("nginx-proxy-manager-https", 8444, 443)
|
||||||
|
.await
|
||||||
|
.unwrap_or(8444);
|
||||||
|
(
|
||||||
vec![
|
vec![
|
||||||
"81:81".to_string(),
|
format!("{}:81", admin_port),
|
||||||
"8084:80".to_string(),
|
format!("{}:80", http_port),
|
||||||
"8444:443".to_string(),
|
format!("{}:443", https_port),
|
||||||
],
|
],
|
||||||
vec![
|
vec![
|
||||||
"/var/lib/archipelago/nginx-proxy-manager/data:/data".to_string(),
|
"/var/lib/archipelago/nginx-proxy-manager/data:/data".to_string(),
|
||||||
@ -937,7 +952,8 @@ pub(super) async fn get_app_config(
|
|||||||
vec![],
|
vec![],
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
),
|
)
|
||||||
|
}
|
||||||
"portainer" => (
|
"portainer" => (
|
||||||
vec!["9000:9000".to_string()],
|
vec!["9000:9000".to_string()],
|
||||||
vec![
|
vec![
|
||||||
|
|||||||
@ -376,7 +376,7 @@ impl RpcHandler {
|
|||||||
package_id
|
package_id
|
||||||
))
|
))
|
||||||
.await;
|
.await;
|
||||||
if let Err(e) = ensure_host_port_listener(package_id, package_id).await {
|
if let Err(e) = ensure_host_port_listener(package_id, package_id, &[]).await {
|
||||||
install_log(&format!(
|
install_log(&format!(
|
||||||
"INSTALL ADOPT RECREATE: {} — host listener repair failed, removing wedged container: {}",
|
"INSTALL ADOPT RECREATE: {} — host listener repair failed, removing wedged container: {}",
|
||||||
package_id, e
|
package_id, e
|
||||||
@ -402,7 +402,7 @@ impl RpcHandler {
|
|||||||
package_id
|
package_id
|
||||||
))
|
))
|
||||||
.await;
|
.await;
|
||||||
if let Err(e) = ensure_host_port_listener(package_id, package_id).await {
|
if let Err(e) = ensure_host_port_listener(package_id, package_id, &[]).await {
|
||||||
install_log(&format!(
|
install_log(&format!(
|
||||||
"INSTALL ADOPT RECREATE: {} — host listener repair failed, removing wedged container: {}",
|
"INSTALL ADOPT RECREATE: {} — host listener repair failed, removing wedged container: {}",
|
||||||
package_id, e
|
package_id, e
|
||||||
@ -880,7 +880,7 @@ impl RpcHandler {
|
|||||||
repair_nextcloud_permissions().await;
|
repair_nextcloud_permissions().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
ensure_host_port_listener(package_id, container_name).await?;
|
ensure_host_port_listener(package_id, container_name, &ports).await?;
|
||||||
|
|
||||||
install_log(&format!(
|
install_log(&format!(
|
||||||
"INSTALL OK: {} (container: {})",
|
"INSTALL OK: {} (container: {})",
|
||||||
@ -1866,7 +1866,7 @@ async fn cleanup_stale_package_ports(package_id: &str) {
|
|||||||
cleanup_stale_pasta_port("3000").await;
|
cleanup_stale_pasta_port("3000").await;
|
||||||
}
|
}
|
||||||
"nginx-proxy-manager" => {
|
"nginx-proxy-manager" => {
|
||||||
cleanup_stale_pasta_port("81").await;
|
cleanup_stale_pasta_port("8081").await;
|
||||||
cleanup_stale_pasta_port("8084").await;
|
cleanup_stale_pasta_port("8084").await;
|
||||||
cleanup_stale_pasta_port("8444").await;
|
cleanup_stale_pasta_port("8444").await;
|
||||||
}
|
}
|
||||||
@ -1914,7 +1914,7 @@ async fn cleanup_start_conflict(package_id: &str, stderr: &str) -> bool {
|
|||||||
"nginx-proxy-manager"
|
"nginx-proxy-manager"
|
||||||
if stderr.contains("pasta failed") || stderr.contains("address already in use") =>
|
if stderr.contains("pasta failed") || stderr.contains("address already in use") =>
|
||||||
{
|
{
|
||||||
cleanup_stale_pasta_port("81").await;
|
cleanup_stale_pasta_port("8081").await;
|
||||||
cleanup_stale_pasta_port("8084").await;
|
cleanup_stale_pasta_port("8084").await;
|
||||||
cleanup_stale_pasta_port("8444").await;
|
cleanup_stale_pasta_port("8444").await;
|
||||||
true
|
true
|
||||||
@ -1968,8 +1968,18 @@ async fn repair_nextcloud_permissions() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn ensure_host_port_listener(package_id: &str, container_name: &str) -> Result<()> {
|
async fn ensure_host_port_listener(
|
||||||
let Some(port) = required_host_port(package_id) else {
|
package_id: &str,
|
||||||
|
container_name: &str,
|
||||||
|
runtime_ports: &[String],
|
||||||
|
) -> Result<()> {
|
||||||
|
let Some(port) = runtime_ports
|
||||||
|
.first()
|
||||||
|
.and_then(|p| p.split(':').next())
|
||||||
|
.and_then(|p| p.parse::<u16>().ok())
|
||||||
|
.or_else(|| published_host_port(container_name))
|
||||||
|
.or_else(|| required_host_port(package_id))
|
||||||
|
else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -2015,6 +2025,22 @@ async fn ensure_host_port_listener(package_id: &str, container_name: &str) -> Re
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn published_host_port(container_name: &str) -> Option<u16> {
|
||||||
|
let output = std::process::Command::new("podman")
|
||||||
|
.args(["port", container_name])
|
||||||
|
.output()
|
||||||
|
.ok()?;
|
||||||
|
if !output.status.success() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
stdout.lines().find_map(|line| {
|
||||||
|
line.rsplit(':')
|
||||||
|
.next()
|
||||||
|
.and_then(|p| p.trim().parse::<u16>().ok())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async fn ensure_user_podman_socket() -> Result<()> {
|
async fn ensure_user_podman_socket() -> Result<()> {
|
||||||
let socket_path = "/run/user/1000/podman/podman.sock";
|
let socket_path = "/run/user/1000/podman/podman.sock";
|
||||||
if tokio::fs::try_exists(socket_path).await.unwrap_or(false) {
|
if tokio::fs::try_exists(socket_path).await.unwrap_or(false) {
|
||||||
@ -2048,7 +2074,7 @@ fn required_host_port(package_id: &str) -> Option<u16> {
|
|||||||
"uptime-kuma" => Some(3002),
|
"uptime-kuma" => Some(3002),
|
||||||
"gitea" => Some(3001),
|
"gitea" => Some(3001),
|
||||||
"nextcloud" => Some(8085),
|
"nextcloud" => Some(8085),
|
||||||
"nginx-proxy-manager" => Some(81),
|
"nginx-proxy-manager" => Some(8081),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -366,6 +366,10 @@ impl RpcHandler {
|
|||||||
{
|
{
|
||||||
let mut allocator = self.port_allocator.lock().await;
|
let mut allocator = self.port_allocator.lock().await;
|
||||||
let _ = allocator.release(package_id).await;
|
let _ = allocator.release(package_id).await;
|
||||||
|
if package_id == "nginx-proxy-manager" {
|
||||||
|
let _ = allocator.release("nginx-proxy-manager-http").await;
|
||||||
|
let _ = allocator.release("nginx-proxy-manager-https").await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean data directories unless preserve_data
|
// Clean data directories unless preserve_data
|
||||||
@ -916,7 +920,7 @@ fn runtime_required_host_port(container_name: &str) -> Option<u16> {
|
|||||||
"vaultwarden" => Some(8082),
|
"vaultwarden" => Some(8082),
|
||||||
"gitea" => Some(3001),
|
"gitea" => Some(3001),
|
||||||
"nextcloud" => Some(8085),
|
"nextcloud" => Some(8085),
|
||||||
"nginx-proxy-manager" => Some(81),
|
"nginx-proxy-manager" => Some(8081),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1072,7 +1076,7 @@ async fn cleanup_start_conflict(container_name: &str, stderr: &str) {
|
|||||||
"vaultwarden" => cleanup_stale_pasta_port("8082").await,
|
"vaultwarden" => cleanup_stale_pasta_port("8082").await,
|
||||||
"nextcloud" => cleanup_stale_pasta_port("8085").await,
|
"nextcloud" => cleanup_stale_pasta_port("8085").await,
|
||||||
"nginx-proxy-manager" => {
|
"nginx-proxy-manager" => {
|
||||||
cleanup_stale_pasta_port("81").await;
|
cleanup_stale_pasta_port("8081").await;
|
||||||
cleanup_stale_pasta_port("8084").await;
|
cleanup_stale_pasta_port("8084").await;
|
||||||
cleanup_stale_pasta_port("8444").await;
|
cleanup_stale_pasta_port("8444").await;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -353,7 +353,7 @@ pub(in crate::api::rpc) fn known_service_port(name: &str) -> u16 {
|
|||||||
"immich" => 2283,
|
"immich" => 2283,
|
||||||
"photoprism" => 2342,
|
"photoprism" => 2342,
|
||||||
"penpot" => 9001,
|
"penpot" => 9001,
|
||||||
"nginx-proxy-manager" => 81,
|
"nginx-proxy-manager" => 8081,
|
||||||
"vaultwarden" => 8343,
|
"vaultwarden" => 8343,
|
||||||
"indeedhub" => 7778,
|
"indeedhub" => 7778,
|
||||||
_ => 0,
|
_ => 0,
|
||||||
|
|||||||
@ -61,6 +61,7 @@ impl DockerPackageScanner {
|
|||||||
"indeedhub-build_minio-init_1",
|
"indeedhub-build_minio-init_1",
|
||||||
"indeedhub-build_relay_1",
|
"indeedhub-build_relay_1",
|
||||||
"indeedhub-build_ffmpeg-worker_1",
|
"indeedhub-build_ffmpeg-worker_1",
|
||||||
|
"buildx_buildkit_default",
|
||||||
];
|
];
|
||||||
|
|
||||||
// First pass: collect running UI containers. Custom UI-backed apps must
|
// First pass: collect running UI containers. Custom UI-backed apps must
|
||||||
@ -123,6 +124,11 @@ impl DockerPackageScanner {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if app_id.starts_with("buildx_buildkit") {
|
||||||
|
debug!("Skipping BuildKit helper container: {}", app_id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Skip UI containers (they're merged with their parent apps)
|
// Skip UI containers (they're merged with their parent apps)
|
||||||
if app_id.ends_with("-ui") {
|
if app_id.ends_with("-ui") {
|
||||||
debug!("Skipping UI container: {}", app_id);
|
debug!("Skipping UI container: {}", app_id);
|
||||||
@ -140,8 +146,13 @@ impl DockerPackageScanner {
|
|||||||
} else {
|
} else {
|
||||||
// Prefer the known web UI port over arbitrary first binding
|
// Prefer the known web UI port over arbitrary first binding
|
||||||
// (for example Gitea exposes SSH on 2222 before web on 3001).
|
// (for example Gitea exposes SSH on 2222 before web on 3001).
|
||||||
let candidate = PodmanClient::lan_address_for(&app_id)
|
let candidate = if uses_allocated_launch_port(&app_id) {
|
||||||
.or_else(|| extract_lan_address(&container.ports));
|
extract_lan_address(&container.ports)
|
||||||
|
.or_else(|| PodmanClient::lan_address_for(&app_id))
|
||||||
|
} else {
|
||||||
|
PodmanClient::lan_address_for(&app_id)
|
||||||
|
.or_else(|| extract_lan_address(&container.ports))
|
||||||
|
};
|
||||||
reachable_lan_address(&app_id, candidate).await
|
reachable_lan_address(&app_id, candidate).await
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -673,6 +684,13 @@ fn companion_lan_address(app_id: &str) -> Option<String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn uses_allocated_launch_port(app_id: &str) -> bool {
|
||||||
|
matches!(
|
||||||
|
app_id,
|
||||||
|
"filebrowser" | "nextcloud" | "nginx-proxy-manager" | "vaultwarden"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn convert_state(container_state: &ContainerState) -> (PackageState, ServiceStatus) {
|
fn convert_state(container_state: &ContainerState) -> (PackageState, ServiceStatus) {
|
||||||
match container_state {
|
match container_state {
|
||||||
ContainerState::Running => (PackageState::Running, ServiceStatus::Running),
|
ContainerState::Running => (PackageState::Running, ServiceStatus::Running),
|
||||||
|
|||||||
@ -28,7 +28,6 @@ const RESERVED_PORTS: &[u16] = &[
|
|||||||
8888, // SearXNG
|
8888, // SearXNG
|
||||||
8096, 2342, 2283, // Jellyfin, Photoprism, Immich
|
8096, 2342, 2283, // Jellyfin, Photoprism, Immich
|
||||||
8443, // FIPS TCP fallback
|
8443, // FIPS TCP fallback
|
||||||
8444, 8084, // NPM
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Start of range for allocating web app ports when preferred is taken.
|
/// Start of range for allocating web app ports when preferred is taken.
|
||||||
@ -94,8 +93,12 @@ impl PortAllocator {
|
|||||||
.any(|m| m.host_port == port)
|
.any(|m| m.host_port == port)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn can_bind(port: u16) -> bool {
|
||||||
|
std::net::TcpListener::bind(("0.0.0.0", port)).is_ok()
|
||||||
|
}
|
||||||
|
|
||||||
fn is_available(&self, port: u16) -> bool {
|
fn is_available(&self, port: u16) -> bool {
|
||||||
!self.is_reserved(port) && !self.is_allocated(port)
|
!self.is_reserved(port) && !self.is_allocated(port) && Self::can_bind(port)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Allocate a host port for an app. Uses preferred_port if available, else finds next free.
|
/// Allocate a host port for an app. Uses preferred_port if available, else finds next free.
|
||||||
|
|||||||
@ -127,7 +127,7 @@ impl PodmanClient {
|
|||||||
"photoprism" => "http://localhost:2342",
|
"photoprism" => "http://localhost:2342",
|
||||||
"immich_server" | "immich" => "http://localhost:2283",
|
"immich_server" | "immich" => "http://localhost:2283",
|
||||||
"filebrowser" => "http://localhost:8083",
|
"filebrowser" => "http://localhost:8083",
|
||||||
"nginx-proxy-manager" => "http://localhost:81",
|
"nginx-proxy-manager" => "http://localhost:8081",
|
||||||
"portainer" => "http://localhost:9000",
|
"portainer" => "http://localhost:9000",
|
||||||
"uptime-kuma" => "http://localhost:3002",
|
"uptime-kuma" => "http://localhost:3002",
|
||||||
"fedimint" | "fedimintd" => "http://localhost:8175",
|
"fedimint" | "fedimintd" => "http://localhost:8175",
|
||||||
|
|||||||
@ -324,6 +324,7 @@ RUN apt-get update && apt-get -y full-upgrade && apt-get install -y --no-install
|
|||||||
openssh-server \
|
openssh-server \
|
||||||
nginx \
|
nginx \
|
||||||
podman \
|
podman \
|
||||||
|
catatonit \
|
||||||
uidmap \
|
uidmap \
|
||||||
slirp4netns \
|
slirp4netns \
|
||||||
passt \
|
passt \
|
||||||
|
|||||||
@ -183,7 +183,7 @@ chroot /mnt/archipelago apt-get install -y \
|
|||||||
chrony
|
chrony
|
||||||
|
|
||||||
echo "📦 Installing container tools..."
|
echo "📦 Installing container tools..."
|
||||||
chroot /mnt/archipelago apt-get install -y podman || echo "⚠️ Podman not available in base repos, will use containers.io later"
|
chroot /mnt/archipelago apt-get install -y podman catatonit || echo "⚠️ Podman/catatonit not available in base repos, will use containers.io later"
|
||||||
|
|
||||||
echo "🔧 Installing GRUB bootloader..."
|
echo "🔧 Installing GRUB bootloader..."
|
||||||
# Need to run grub-install inside chroot with proper environment
|
# Need to run grub-install inside chroot with proper environment
|
||||||
|
|||||||
@ -741,7 +741,7 @@ server {
|
|||||||
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
||||||
}
|
}
|
||||||
location /app/nginx-proxy-manager/ {
|
location /app/nginx-proxy-manager/ {
|
||||||
proxy_pass http://127.0.0.1:81/;
|
proxy_pass http://127.0.0.1:8081/;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
@ -873,6 +873,23 @@ server {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Compatibility proxy for cached PWA bundles that still launch Nginx Proxy
|
||||||
|
# Manager on :81. Rootless Podman cannot bind host ports below 1024, so the
|
||||||
|
# container admin UI runs on :8081 and host nginx owns the old :81 entrypoint.
|
||||||
|
server {
|
||||||
|
listen 81;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:8081/;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# HTTPS - required for PWA install (Add to Home Screen) from dev servers
|
# HTTPS - required for PWA install (Add to Home Screen) from dev servers
|
||||||
server {
|
server {
|
||||||
listen 443 ssl;
|
listen 443 ssl;
|
||||||
|
|||||||
@ -358,7 +358,7 @@ location /app/indeedhub/ {
|
|||||||
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
||||||
}
|
}
|
||||||
location /app/nginx-proxy-manager/ {
|
location /app/nginx-proxy-manager/ {
|
||||||
proxy_pass http://127.0.0.1:81/;
|
proxy_pass http://127.0.0.1:8081/;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
|||||||
4
neode-ui/package-lock.json
generated
4
neode-ui/package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "neode-ui",
|
"name": "neode-ui",
|
||||||
"version": "1.7.56-alpha",
|
"version": "1.7.57-alpha",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "neode-ui",
|
"name": "neode-ui",
|
||||||
"version": "1.7.56-alpha",
|
"version": "1.7.57-alpha",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/dompurify": "^3.0.5",
|
"@types/dompurify": "^3.0.5",
|
||||||
"@vue-leaflet/vue-leaflet": "^0.10.1",
|
"@vue-leaflet/vue-leaflet": "^0.10.1",
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "neode-ui",
|
"name": "neode-ui",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.7.56-alpha",
|
"version": "1.7.57-alpha",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "./start-dev.sh",
|
"start": "./start-dev.sh",
|
||||||
|
|||||||
@ -98,7 +98,7 @@ describe('useAppLauncherStore', () => {
|
|||||||
expect(store.panelAppId).toBe('btcpay-server')
|
expect(store.panelAppId).toBe('btcpay-server')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('opens Nginx Proxy Manager in new tab even when URL resolves', () => {
|
it('normalizes old Nginx Proxy Manager port 81 to 8081', () => {
|
||||||
const store = useAppLauncherStore()
|
const store = useAppLauncherStore()
|
||||||
|
|
||||||
store.open({ url: 'http://192.168.1.228:81', title: 'Nginx Proxy Manager' })
|
store.open({ url: 'http://192.168.1.228:81', title: 'Nginx Proxy Manager' })
|
||||||
@ -106,7 +106,7 @@ describe('useAppLauncherStore', () => {
|
|||||||
expect(store.isOpen).toBe(false)
|
expect(store.isOpen).toBe(false)
|
||||||
expect(store.panelAppId).toBe(null)
|
expect(store.panelAppId).toBe(null)
|
||||||
expect(mockWindowOpen).toHaveBeenCalledWith(
|
expect(mockWindowOpen).toHaveBeenCalledWith(
|
||||||
'http://192.168.1.228:81',
|
'http://192.168.1.228:8081',
|
||||||
'_blank',
|
'_blank',
|
||||||
'noopener,noreferrer',
|
'noopener,noreferrer',
|
||||||
)
|
)
|
||||||
@ -125,13 +125,13 @@ describe('useAppLauncherStore', () => {
|
|||||||
expect(store.panelAppId).toBe(null)
|
expect(store.panelAppId).toBe(null)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('normalizes legacy Nginx Proxy Manager port 8181 to 81', () => {
|
it('normalizes legacy Nginx Proxy Manager ports to 8081', () => {
|
||||||
const store = useAppLauncherStore()
|
const store = useAppLauncherStore()
|
||||||
|
|
||||||
store.open({ url: 'http://192.168.1.228:8181', title: 'Nginx Proxy Manager' })
|
store.open({ url: 'http://192.168.1.228:8181', title: 'Nginx Proxy Manager' })
|
||||||
|
|
||||||
expect(mockWindowOpen).toHaveBeenCalledWith(
|
expect(mockWindowOpen).toHaveBeenCalledWith(
|
||||||
'http://192.168.1.228:81',
|
'http://192.168.1.228:8081',
|
||||||
'_blank',
|
'_blank',
|
||||||
'noopener,noreferrer',
|
'noopener,noreferrer',
|
||||||
)
|
)
|
||||||
|
|||||||
@ -61,8 +61,8 @@ function normalizeLaunchUrl(urlStr: string, appIdHint?: string | null): string {
|
|||||||
return rebuilt('3002')
|
return rebuilt('3002')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sameHost && appIdHint === 'nginx-proxy-manager' && u.port === '8181') {
|
if (sameHost && appIdHint === 'nginx-proxy-manager' && (u.port === '81' || u.port === '8181')) {
|
||||||
return rebuilt('81')
|
return rebuilt('8081')
|
||||||
}
|
}
|
||||||
|
|
||||||
return rewrittenLocalhost ? u.toString() : urlStr
|
return rewrittenLocalhost ? u.toString() : urlStr
|
||||||
@ -74,6 +74,7 @@ function normalizeLaunchUrl(urlStr: string, appIdHint?: string | null): string {
|
|||||||
/** Port → app ID for resolving URLs to AppSession routes */
|
/** Port → app ID for resolving URLs to AppSession routes */
|
||||||
const PORT_TO_APP_ID: Record<string, string> = {
|
const PORT_TO_APP_ID: Record<string, string> = {
|
||||||
'81': 'nginx-proxy-manager',
|
'81': 'nginx-proxy-manager',
|
||||||
|
'8081': 'nginx-proxy-manager',
|
||||||
'8181': 'nginx-proxy-manager',
|
'8181': 'nginx-proxy-manager',
|
||||||
'3000': 'grafana',
|
'3000': 'grafana',
|
||||||
'3002': 'uptime-kuma',
|
'3002': 'uptime-kuma',
|
||||||
|
|||||||
@ -141,6 +141,7 @@ import AppHeroSection from './appDetails/AppHeroSection.vue'
|
|||||||
import AppContentSection from './appDetails/AppContentSection.vue'
|
import AppContentSection from './appDetails/AppContentSection.vue'
|
||||||
import AppSidebar from './appDetails/AppSidebar.vue'
|
import AppSidebar from './appDetails/AppSidebar.vue'
|
||||||
import { resolveAppUrl } from './appSession/appSessionConfig'
|
import { resolveAppUrl } from './appSession/appSessionConfig'
|
||||||
|
import { isWebsitePackage, resolveRuntimeLaunchUrl } from './apps/appsConfig'
|
||||||
import {
|
import {
|
||||||
WEB_ONLY_APP_URLS,
|
WEB_ONLY_APP_URLS,
|
||||||
PACKAGE_ALIASES,
|
PACKAGE_ALIASES,
|
||||||
@ -294,6 +295,18 @@ function launchApp() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isWebsitePackage(id, pkg.value)) {
|
||||||
|
const url = resolveRuntimeLaunchUrl(pkg.value)
|
||||||
|
if (url) window.open(url, '_blank', 'noopener,noreferrer')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const runtimeUrl = resolveRuntimeLaunchUrl(pkg.value)
|
||||||
|
if (runtimeUrl) {
|
||||||
|
useAppLauncherStore().open({ url: runtimeUrl, title: pkg.value.manifest.title })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Container apps should launch through session routing so protocol/path
|
// Container apps should launch through session routing so protocol/path
|
||||||
// handling stays centralized in appSessionConfig.
|
// handling stays centralized in appSessionConfig.
|
||||||
useAppLauncherStore().openSession(id)
|
useAppLauncherStore().openSession(id)
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
<div class="mode-switcher flex-shrink-0">
|
<div class="mode-switcher flex-shrink-0">
|
||||||
<button class="mode-switcher-btn" :class="{ 'mode-switcher-btn-active': activeTab === 'apps' }" @click="activeTab = 'apps'; router.replace({ query: {} })">My Apps</button>
|
<button class="mode-switcher-btn" :class="{ 'mode-switcher-btn-active': activeTab === 'apps' }" @click="activeTab = 'apps'; router.replace({ query: {} })">My Apps</button>
|
||||||
<RouterLink to="/dashboard/discover" class="mode-switcher-btn">App Store</RouterLink>
|
<RouterLink to="/dashboard/discover" class="mode-switcher-btn">App Store</RouterLink>
|
||||||
<button class="mode-switcher-btn" :class="{ 'mode-switcher-btn-active': activeTab === 'services' }" @click="activeTab = 'services'; router.replace({ query: { tab: 'services' } })">Services</button>
|
<button class="mode-switcher-btn" :class="{ 'mode-switcher-btn-active': activeTab === 'websites' }" @click="activeTab = 'websites'; router.replace({ query: { tab: 'websites' } })">Websites</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="activeTab === 'apps' && categoriesWithApps.length > 1" class="mode-switcher flex-shrink-0">
|
<div v-if="activeTab === 'apps' && categoriesWithApps.length > 1" class="mode-switcher flex-shrink-0">
|
||||||
<button
|
<button
|
||||||
@ -155,7 +155,7 @@ import AppIconGrid from './apps/AppIconGrid.vue'
|
|||||||
import AppsUninstallModal from './apps/AppsUninstallModal.vue'
|
import AppsUninstallModal from './apps/AppsUninstallModal.vue'
|
||||||
import { useAppsActions } from './apps/useAppsActions'
|
import { useAppsActions } from './apps/useAppsActions'
|
||||||
import {
|
import {
|
||||||
filterEntriesForTab, isWebOnlyApp,
|
type AppsTab, filterEntriesForTab, isWebOnlyApp, isWebsitePackage, opensInTab, resolveRuntimeLaunchUrl,
|
||||||
WEB_ONLY_APPS, buildAllCategories, useCategoriesWithApps,
|
WEB_ONLY_APPS, buildAllCategories, useCategoriesWithApps,
|
||||||
} from './apps/appsConfig'
|
} from './apps/appsConfig'
|
||||||
|
|
||||||
@ -170,8 +170,8 @@ const actions = useAppsActions()
|
|||||||
const showStagger = !appsAnimationDone
|
const showStagger = !appsAnimationDone
|
||||||
|
|
||||||
// Tabs
|
// Tabs
|
||||||
const activeTab = ref<'apps' | 'services'>(
|
const activeTab = ref<AppsTab>(
|
||||||
route.query.tab === 'services' ? 'services' : 'apps'
|
route.query.tab === 'websites' || route.query.tab === 'services' ? 'websites' : 'apps'
|
||||||
)
|
)
|
||||||
|
|
||||||
// Search (debounced)
|
// Search (debounced)
|
||||||
@ -285,6 +285,19 @@ function goToApp(id: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function launchApp(id: string) {
|
function launchApp(id: string) {
|
||||||
|
const pkg = packages.value[id]
|
||||||
|
if (pkg && isWebsitePackage(id, pkg)) {
|
||||||
|
const url = resolveRuntimeLaunchUrl(pkg)
|
||||||
|
if (url) window.open(url, '_blank', 'noopener,noreferrer')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (pkg && opensInTab(id)) {
|
||||||
|
const url = resolveRuntimeLaunchUrl(pkg)
|
||||||
|
if (url) {
|
||||||
|
window.open(url, '_blank', 'noopener,noreferrer')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
useAppLauncherStore().openSession(id)
|
useAppLauncherStore().openSession(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
<div class="mode-switcher flex-shrink-0">
|
<div class="mode-switcher flex-shrink-0">
|
||||||
<RouterLink to="/dashboard/apps" class="mode-switcher-btn">My Apps</RouterLink>
|
<RouterLink to="/dashboard/apps" class="mode-switcher-btn">My Apps</RouterLink>
|
||||||
<RouterLink to="/dashboard/discover" class="mode-switcher-btn mode-switcher-btn-active">App Store</RouterLink>
|
<RouterLink to="/dashboard/discover" class="mode-switcher-btn mode-switcher-btn-active">App Store</RouterLink>
|
||||||
<RouterLink to="/dashboard/apps?tab=services" class="mode-switcher-btn">Services</RouterLink>
|
<RouterLink to="/dashboard/apps?tab=websites" class="mode-switcher-btn">Websites</RouterLink>
|
||||||
</div>
|
</div>
|
||||||
<div class="mode-switcher flex-shrink-0">
|
<div class="mode-switcher flex-shrink-0">
|
||||||
<RouterLink
|
<RouterLink
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
<div class="mode-switcher flex-shrink-0">
|
<div class="mode-switcher flex-shrink-0">
|
||||||
<RouterLink to="/dashboard/apps" class="mode-switcher-btn">My Apps</RouterLink>
|
<RouterLink to="/dashboard/apps" class="mode-switcher-btn">My Apps</RouterLink>
|
||||||
<RouterLink to="/dashboard/discover" class="mode-switcher-btn mode-switcher-btn-active">App Store</RouterLink>
|
<RouterLink to="/dashboard/discover" class="mode-switcher-btn mode-switcher-btn-active">App Store</RouterLink>
|
||||||
<RouterLink to="/dashboard/apps?tab=services" class="mode-switcher-btn">Services</RouterLink>
|
<RouterLink to="/dashboard/apps?tab=websites" class="mode-switcher-btn">Websites</RouterLink>
|
||||||
</div>
|
</div>
|
||||||
<div class="mode-switcher flex-shrink-0">
|
<div class="mode-switcher flex-shrink-0">
|
||||||
<RouterLink
|
<RouterLink
|
||||||
|
|||||||
@ -83,7 +83,7 @@ export const APP_URLS: Record<string, { dev: string; prod: string }> = {
|
|||||||
'photoprism': { dev: 'http://localhost:2342', prod: 'http://localhost:2342' },
|
'photoprism': { dev: 'http://localhost:2342', prod: 'http://localhost:2342' },
|
||||||
'immich': { dev: 'http://localhost:2283', prod: 'http://localhost:2283' },
|
'immich': { dev: 'http://localhost:2283', prod: 'http://localhost:2283' },
|
||||||
'filebrowser': { dev: 'http://localhost:8083', prod: 'http://localhost:8083' },
|
'filebrowser': { dev: 'http://localhost:8083', prod: 'http://localhost:8083' },
|
||||||
'nginx-proxy-manager': { dev: 'http://localhost:81', prod: 'http://localhost:81' },
|
'nginx-proxy-manager': { dev: 'http://localhost:8081', prod: 'http://localhost:8081' },
|
||||||
'gitea': { dev: 'http://localhost:3001', prod: 'http://localhost:3001' },
|
'gitea': { dev: 'http://localhost:3001', prod: 'http://localhost:3001' },
|
||||||
'portainer': { dev: 'http://localhost:9000', prod: 'http://localhost:9000' },
|
'portainer': { dev: 'http://localhost:9000', prod: 'http://localhost:9000' },
|
||||||
'uptime-kuma': { dev: 'http://localhost:3002', prod: 'http://localhost:3002' },
|
'uptime-kuma': { dev: 'http://localhost:3002', prod: 'http://localhost:3002' },
|
||||||
|
|||||||
@ -31,7 +31,7 @@ export const APP_PORTS: Record<string, number> = {
|
|||||||
'immich': 2283,
|
'immich': 2283,
|
||||||
'immich_server': 2283,
|
'immich_server': 2283,
|
||||||
'filebrowser': 8083,
|
'filebrowser': 8083,
|
||||||
'nginx-proxy-manager': 81,
|
'nginx-proxy-manager': 8081,
|
||||||
'gitea': 3001,
|
'gitea': 3001,
|
||||||
'portainer': 9000,
|
'portainer': 9000,
|
||||||
'tailscale': 8240,
|
'tailscale': 8240,
|
||||||
|
|||||||
@ -79,7 +79,7 @@ import { useServerStore } from '@/stores/server'
|
|||||||
import { useAppLauncherStore } from '@/stores/appLauncher'
|
import { useAppLauncherStore } from '@/stores/appLauncher'
|
||||||
import type { PackageDataEntry } from '@/types/api'
|
import type { PackageDataEntry } from '@/types/api'
|
||||||
import { resolveAppUrl } from '@/views/appSession/appSessionConfig'
|
import { resolveAppUrl } from '@/views/appSession/appSessionConfig'
|
||||||
import { canLaunch, handleImageError, opensInTab, resolveAppIcon } from './appsConfig'
|
import { canLaunch, handleImageError, isWebsitePackage, opensInTab, resolveAppIcon, resolveRuntimeLaunchUrl } from './appsConfig'
|
||||||
import { getCuratedAppList } from '../discover/curatedApps'
|
import { getCuratedAppList } from '../discover/curatedApps'
|
||||||
|
|
||||||
const ITEMS_PER_PAGE = 16 // 4 columns x 4 rows
|
const ITEMS_PER_PAGE = 16 // 4 columns x 4 rows
|
||||||
@ -120,8 +120,15 @@ function getIcon(id: string, pkg: PackageDataEntry): string {
|
|||||||
|
|
||||||
function handleTap(id: string, pkg: PackageDataEntry) {
|
function handleTap(id: string, pkg: PackageDataEntry) {
|
||||||
if (canLaunch(pkg)) {
|
if (canLaunch(pkg)) {
|
||||||
|
if (isWebsitePackage(id, pkg)) {
|
||||||
|
const url = resolveRuntimeLaunchUrl(pkg)
|
||||||
|
if (url) {
|
||||||
|
window.open(url, '_blank', 'noopener,noreferrer')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
if (opensInTab(id)) {
|
if (opensInTab(id)) {
|
||||||
const appUrl = resolveAppUrl(id)
|
const appUrl = resolveRuntimeLaunchUrl(pkg) || resolveAppUrl(id)
|
||||||
if (appUrl) {
|
if (appUrl) {
|
||||||
window.open(appUrl, '_blank', 'noopener,noreferrer')
|
window.open(appUrl, '_blank', 'noopener,noreferrer')
|
||||||
return
|
return
|
||||||
|
|||||||
@ -5,6 +5,8 @@ import { computed } from 'vue'
|
|||||||
import { PackageState, type PackageDataEntry } from '@/types/api'
|
import { PackageState, type PackageDataEntry } from '@/types/api'
|
||||||
import { resolveAppUrl } from '../appSession/appSessionConfig'
|
import { resolveAppUrl } from '../appSession/appSessionConfig'
|
||||||
|
|
||||||
|
export type AppsTab = 'apps' | 'websites' | 'services'
|
||||||
|
|
||||||
// Service container name patterns (backend/infra, not user-facing)
|
// Service container name patterns (backend/infra, not user-facing)
|
||||||
export const SERVICE_NAMES = new Set([
|
export const SERVICE_NAMES = new Set([
|
||||||
'dwn', 'archy-mempool-db', 'archy-btcpay-db', 'archy-nbxplorer', 'archy-tor',
|
'dwn', 'archy-mempool-db', 'archy-btcpay-db', 'archy-nbxplorer', 'archy-tor',
|
||||||
@ -22,6 +24,15 @@ export const SERVICE_NAMES = new Set([
|
|||||||
'syntropy-institute', 't-zero', 'arch-presentation',
|
'syntropy-institute', 't-zero', 'arch-presentation',
|
||||||
])
|
])
|
||||||
|
|
||||||
|
const INTERNAL_TOOLING_NAMES = new Set([
|
||||||
|
'buildx_buildkit_default',
|
||||||
|
])
|
||||||
|
|
||||||
|
export function isInternalToolingPackage(id: string, pkg?: PackageDataEntry): boolean {
|
||||||
|
const manifestId = pkg?.manifest?.id || ''
|
||||||
|
return INTERNAL_TOOLING_NAMES.has(id) || INTERNAL_TOOLING_NAMES.has(manifestId) || id.startsWith('buildx_buildkit') || manifestId.startsWith('buildx_buildkit')
|
||||||
|
}
|
||||||
|
|
||||||
export function isServiceContainer(id: string): boolean {
|
export function isServiceContainer(id: string): boolean {
|
||||||
if (SERVICE_NAMES.has(id)) return true
|
if (SERVICE_NAMES.has(id)) return true
|
||||||
if (id.startsWith('indeedhub-build_')) return true
|
if (id.startsWith('indeedhub-build_')) return true
|
||||||
@ -58,14 +69,32 @@ export function getAppCategory(id: string, pkg: PackageDataEntry): string {
|
|||||||
return cat || 'other'
|
return cat || 'other'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function runtimeLanAddress(pkg: PackageDataEntry): string {
|
||||||
|
return pkg.installed?.['interface-addresses']?.main?.['lan-address'] || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isKnownApp(id: string, pkg?: PackageDataEntry): boolean {
|
||||||
|
const manifestId = pkg?.manifest?.id
|
||||||
|
return !!(APP_CATEGORY_MAP[id] || (manifestId && APP_CATEGORY_MAP[manifestId]) || isWebOnlyApp(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isWebsitePackage(id: string, pkg?: PackageDataEntry): boolean {
|
||||||
|
if (isInternalToolingPackage(id, pkg)) return false
|
||||||
|
if (isServicePackage(id, pkg)) return true
|
||||||
|
if (isKnownApp(id, pkg)) return false
|
||||||
|
return !!pkg && !!runtimeLanAddress(pkg)
|
||||||
|
}
|
||||||
|
|
||||||
export function filterEntriesForTab(
|
export function filterEntriesForTab(
|
||||||
entries: Array<[string, PackageDataEntry]>,
|
entries: Array<[string, PackageDataEntry]>,
|
||||||
activeTab: 'apps' | 'services',
|
activeTab: AppsTab,
|
||||||
selectedCategory: string,
|
selectedCategory: string,
|
||||||
): Array<[string, PackageDataEntry]> {
|
): Array<[string, PackageDataEntry]> {
|
||||||
return entries.filter(([id, pkg]) => {
|
return entries.filter(([id, pkg]) => {
|
||||||
const isSvc = isServicePackage(id, pkg)
|
if (isInternalToolingPackage(id, pkg)) return false
|
||||||
if (activeTab === 'services' ? !isSvc : isSvc) return false
|
const wantsWebsites = activeTab === 'websites' || activeTab === 'services'
|
||||||
|
const isWebsite = isWebsitePackage(id, pkg)
|
||||||
|
if (wantsWebsites ? !isWebsite : isWebsite) return false
|
||||||
if (activeTab === 'apps' && selectedCategory !== 'all') {
|
if (activeTab === 'apps' && selectedCategory !== 'all') {
|
||||||
return getAppCategory(id, pkg) === selectedCategory
|
return getAppCategory(id, pkg) === selectedCategory
|
||||||
}
|
}
|
||||||
@ -151,6 +180,12 @@ export function canLaunch(pkg: PackageDataEntry): boolean {
|
|||||||
return !!hasUI && pkg.state === 'running' && pkg.health !== 'starting' && pkg.health !== 'unhealthy'
|
return !!hasUI && pkg.state === 'running' && pkg.health !== 'starting' && pkg.health !== 'unhealthy'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resolveRuntimeLaunchUrl(pkg: PackageDataEntry): string {
|
||||||
|
const addr = runtimeLanAddress(pkg)
|
||||||
|
if (!addr || typeof window === 'undefined') return addr
|
||||||
|
return addr.replace(/^http:\/\/(localhost|127\.0\.0\.1)(?=[:/]|$)/, `http://${window.location.hostname}`)
|
||||||
|
}
|
||||||
|
|
||||||
export function getStatusClass(state: PackageState, health?: string | null, exitCode?: number | null): string {
|
export function getStatusClass(state: PackageState, health?: string | null, exitCode?: number | null): string {
|
||||||
if (state === PackageState.Running && health === 'starting') return 'bg-yellow-500/20 text-yellow-200'
|
if (state === PackageState.Running && health === 'starting') return 'bg-yellow-500/20 text-yellow-200'
|
||||||
if (state === PackageState.Running && health === 'unhealthy') return 'bg-orange-500/20 text-orange-200'
|
if (state === PackageState.Running && health === 'unhealthy') return 'bg-orange-500/20 text-orange-200'
|
||||||
@ -212,7 +247,7 @@ export function useCategoriesWithApps(
|
|||||||
allCategories: Ref<Array<{ id: string; name: string }>>,
|
allCategories: Ref<Array<{ id: string; name: string }>>,
|
||||||
) {
|
) {
|
||||||
return computed(() => {
|
return computed(() => {
|
||||||
const entries = Object.entries(packages.value).filter(([id, pkg]) => !isServicePackage(id, pkg))
|
const entries = Object.entries(packages.value).filter(([id, pkg]) => !isWebsitePackage(id, pkg) && !isInternalToolingPackage(id, pkg))
|
||||||
return allCategories.value.filter(cat => {
|
return allCategories.value.filter(cat => {
|
||||||
if (cat.id === 'all') return true
|
if (cat.id === 'all') return true
|
||||||
return entries.some(([id, pkg]) => getAppCategory(id, pkg) === cat.id)
|
return entries.some(([id, pkg]) => getAppCategory(id, pkg) === cat.id)
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
<RouterLink
|
<RouterLink
|
||||||
to="/dashboard/apps"
|
to="/dashboard/apps"
|
||||||
class="mode-switcher-btn"
|
class="mode-switcher-btn"
|
||||||
:class="{ 'mode-switcher-btn-active': (route.path === '/dashboard/apps' || route.path.startsWith('/dashboard/apps/')) && route.query.tab !== 'services' }"
|
:class="{ 'mode-switcher-btn-active': (route.path === '/dashboard/apps' || route.path.startsWith('/dashboard/apps/')) && route.query.tab !== 'services' && route.query.tab !== 'websites' }"
|
||||||
@click.prevent="router.push({ path: '/dashboard/apps', query: {} })"
|
@click.prevent="router.push({ path: '/dashboard/apps', query: {} })"
|
||||||
>My Apps</RouterLink>
|
>My Apps</RouterLink>
|
||||||
<RouterLink
|
<RouterLink
|
||||||
@ -19,10 +19,10 @@
|
|||||||
:class="{ 'mode-switcher-btn-active': route.path === '/dashboard/marketplace' || route.path.startsWith('/dashboard/marketplace/') || route.path === '/dashboard/discover' }"
|
:class="{ 'mode-switcher-btn-active': route.path === '/dashboard/marketplace' || route.path.startsWith('/dashboard/marketplace/') || route.path === '/dashboard/discover' }"
|
||||||
>App Store</RouterLink>
|
>App Store</RouterLink>
|
||||||
<RouterLink
|
<RouterLink
|
||||||
to="/dashboard/apps?tab=services"
|
to="/dashboard/apps?tab=websites"
|
||||||
class="mode-switcher-btn"
|
class="mode-switcher-btn"
|
||||||
:class="{ 'mode-switcher-btn-active': route.query.tab === 'services' }"
|
:class="{ 'mode-switcher-btn-active': route.query.tab === 'services' || route.query.tab === 'websites' }"
|
||||||
>Services</RouterLink>
|
>Websites</RouterLink>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -96,8 +96,8 @@ export function useRouteTransitions() {
|
|||||||
|
|
||||||
// Mobile: Horizontal slide transitions between sub-tabs
|
// Mobile: Horizontal slide transitions between sub-tabs
|
||||||
if (typeof window !== 'undefined' && window.innerWidth < 768) {
|
if (typeof window !== 'undefined' && window.innerWidth < 768) {
|
||||||
const isServices = currentPath === '/dashboard/apps' && currentRoute.query.tab === 'services'
|
const isServices = currentPath === '/dashboard/apps' && (currentRoute.query.tab === 'services' || currentRoute.query.tab === 'websites')
|
||||||
const wasServices = previousTab === 'services'
|
const wasServices = previousTab === 'services' || previousTab === 'websites'
|
||||||
const currentAppsIdx = isServices ? 2
|
const currentAppsIdx = isServices ? 2
|
||||||
: currentPath === '/dashboard/marketplace' ? 1
|
: currentPath === '/dashboard/marketplace' ? 1
|
||||||
: currentPath === '/dashboard/apps' ? 0 : -1
|
: currentPath === '/dashboard/apps' ? 0 : -1
|
||||||
|
|||||||
@ -38,6 +38,7 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
workbox: {
|
workbox: {
|
||||||
navigateFallbackDenylist: [/^\/app\//, /^\/rpc\//, /^\/ws/, /^\/aiui\//],
|
navigateFallbackDenylist: [/^\/app\//, /^\/rpc\//, /^\/ws/, /^\/aiui\//],
|
||||||
|
cleanupOutdatedCaches: true,
|
||||||
globPatterns: ['**/*.{js,css,html,ico,png,svg,jpg,jpeg,mp4,webp}'],
|
globPatterns: ['**/*.{js,css,html,ico,png,svg,jpg,jpeg,mp4,webp}'],
|
||||||
globIgnores: [
|
globIgnores: [
|
||||||
'**/*-backup-*.mp4',
|
'**/*-backup-*.mp4',
|
||||||
|
|||||||
@ -1,34 +1,29 @@
|
|||||||
{
|
{
|
||||||
"version": "1.7.56-alpha",
|
"version": "1.7.57-alpha",
|
||||||
"release_date": "2026-05-15",
|
"release_date": "2026-05-17",
|
||||||
"changelog": [
|
"changelog": [
|
||||||
"Health notifications now clear when an app is no longer unhealthy, including stale alerts for removed containers such as Portainer.",
|
"Nginx Proxy Manager now avoids privileged rootless Podman host port `81`, preferring `8081:81` while host nginx keeps a compatibility proxy on `:81` for stale cached launch buttons.",
|
||||||
"Fresh installs now include the full Wi-Fi userspace stack (`wpasupplicant`, `wireless-regdb`, `iw`, `rfkill`, `polkitd`, `pciutils`, and `usbutils`) so NetworkManager can scan and connect with Intel Wi-Fi cards out of the box.",
|
"App installs now allocate ports by checking live host bind availability, falling back to a free high port when preferred ports are already occupied.",
|
||||||
"The installed system now grants the `archipelago` service user explicit NetworkManager PolicyKit access for web-triggered Wi-Fi scans and connection changes.",
|
"Portainer-created launchable containers are separated into a `Websites` tab and launch through their discovered published host port instead of hard-coded app URLs.",
|
||||||
"Wi-Fi connect now replaces stale/partial NetworkManager profiles and creates an explicit WPA-PSK profile with the supplied password, avoiding no-secret retry failures after a failed attempt.",
|
"Internal BuildKit helper containers such as `buildx_buildkit_default` are hidden from the Apps UI.",
|
||||||
"Settings password changes now update the Linux/SSH password through non-interactive sudo, so the web password and SSH password stay in sync when the checkbox is enabled.",
|
"Portainer works out of the box on Debian 13/Podman installs by including `catatonit` and by preserving the Podman socket mount as a socket rather than creating it as a directory."
|
||||||
"Quadlet environment values with spaces or shell metacharacters are quoted consistently, preventing env drift recreate loops for apps like nostr-rs-relay and Grafana.",
|
|
||||||
"Boot/bootstrap reconcile avoids restarting running Bitcoin containers while repairing RPC config, preserving IBD progress on active nodes.",
|
|
||||||
"Exit code 137 is labeled as SIGKILL instead of assuming OOM, avoiding false OOM alerts for orchestrator-managed recreates.",
|
|
||||||
"Container reconcile force-recreates Podman records stuck in `Stopping`, preserving bind-mounted app data while recovering wedged containers automatically.",
|
|
||||||
"Container health reporting is honest for running containers: Archipelago surfaces Podman's actual health state instead of marking every running container healthy."
|
|
||||||
],
|
],
|
||||||
"components": [
|
"components": [
|
||||||
{
|
{
|
||||||
"name": "archipelago",
|
"name": "archipelago",
|
||||||
"current_version": "1.7.56-alpha",
|
"current_version": "1.7.57-alpha",
|
||||||
"new_version": "1.7.56-alpha",
|
"new_version": "1.7.57-alpha",
|
||||||
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.56-alpha/archipelago",
|
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.57-alpha/archipelago",
|
||||||
"sha256": "f6c54cc7fbaac3dde97b1a719a6d380ce734a4d52366d4579770effadef92c9c",
|
"sha256": "e96d657c28f84c72c1358fdbbc014184530763341a2d02242b3a479f36dc2aef",
|
||||||
"size_bytes": 42862944
|
"size_bytes": 42647224
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "archipelago-frontend-1.7.56-alpha.tar.gz",
|
"name": "archipelago-frontend-1.7.57-alpha.tar.gz",
|
||||||
"current_version": "1.7.56-alpha",
|
"current_version": "1.7.57-alpha",
|
||||||
"new_version": "1.7.56-alpha",
|
"new_version": "1.7.57-alpha",
|
||||||
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.56-alpha/archipelago-frontend-1.7.56-alpha.tar.gz",
|
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.57-alpha/archipelago-frontend-1.7.57-alpha.tar.gz",
|
||||||
"sha256": "91bfa10085696921c929ab7d216a8e617eb02e7105f21f32cdc3a2ba15158cd4",
|
"sha256": "a09c1507afb4b7e9f5279b92215e92ebc0b86155ae53ac2c0baaecd07522c7ee",
|
||||||
"size_bytes": 166465069
|
"size_bytes": 78322316
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,34 +1,29 @@
|
|||||||
{
|
{
|
||||||
"version": "1.7.56-alpha",
|
"version": "1.7.57-alpha",
|
||||||
"release_date": "2026-05-15",
|
"release_date": "2026-05-17",
|
||||||
"changelog": [
|
"changelog": [
|
||||||
"Health notifications now clear when an app is no longer unhealthy, including stale alerts for removed containers such as Portainer.",
|
"Nginx Proxy Manager now avoids privileged rootless Podman host port `81`, preferring `8081:81` while host nginx keeps a compatibility proxy on `:81` for stale cached launch buttons.",
|
||||||
"Fresh installs now include the full Wi-Fi userspace stack (`wpasupplicant`, `wireless-regdb`, `iw`, `rfkill`, `polkitd`, `pciutils`, and `usbutils`) so NetworkManager can scan and connect with Intel Wi-Fi cards out of the box.",
|
"App installs now allocate ports by checking live host bind availability, falling back to a free high port when preferred ports are already occupied.",
|
||||||
"The installed system now grants the `archipelago` service user explicit NetworkManager PolicyKit access for web-triggered Wi-Fi scans and connection changes.",
|
"Portainer-created launchable containers are separated into a `Websites` tab and launch through their discovered published host port instead of hard-coded app URLs.",
|
||||||
"Wi-Fi connect now replaces stale/partial NetworkManager profiles and creates an explicit WPA-PSK profile with the supplied password, avoiding no-secret retry failures after a failed attempt.",
|
"Internal BuildKit helper containers such as `buildx_buildkit_default` are hidden from the Apps UI.",
|
||||||
"Settings password changes now update the Linux/SSH password through non-interactive sudo, so the web password and SSH password stay in sync when the checkbox is enabled.",
|
"Portainer works out of the box on Debian 13/Podman installs by including `catatonit` and by preserving the Podman socket mount as a socket rather than creating it as a directory."
|
||||||
"Quadlet environment values with spaces or shell metacharacters are quoted consistently, preventing env drift recreate loops for apps like nostr-rs-relay and Grafana.",
|
|
||||||
"Boot/bootstrap reconcile avoids restarting running Bitcoin containers while repairing RPC config, preserving IBD progress on active nodes.",
|
|
||||||
"Exit code 137 is labeled as SIGKILL instead of assuming OOM, avoiding false OOM alerts for orchestrator-managed recreates.",
|
|
||||||
"Container reconcile force-recreates Podman records stuck in `Stopping`, preserving bind-mounted app data while recovering wedged containers automatically.",
|
|
||||||
"Container health reporting is honest for running containers: Archipelago surfaces Podman's actual health state instead of marking every running container healthy."
|
|
||||||
],
|
],
|
||||||
"components": [
|
"components": [
|
||||||
{
|
{
|
||||||
"name": "archipelago",
|
"name": "archipelago",
|
||||||
"current_version": "1.7.56-alpha",
|
"current_version": "1.7.57-alpha",
|
||||||
"new_version": "1.7.56-alpha",
|
"new_version": "1.7.57-alpha",
|
||||||
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.56-alpha/archipelago",
|
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.57-alpha/archipelago",
|
||||||
"sha256": "f6c54cc7fbaac3dde97b1a719a6d380ce734a4d52366d4579770effadef92c9c",
|
"sha256": "e96d657c28f84c72c1358fdbbc014184530763341a2d02242b3a479f36dc2aef",
|
||||||
"size_bytes": 42862944
|
"size_bytes": 42647224
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "archipelago-frontend-1.7.56-alpha.tar.gz",
|
"name": "archipelago-frontend-1.7.57-alpha.tar.gz",
|
||||||
"current_version": "1.7.56-alpha",
|
"current_version": "1.7.57-alpha",
|
||||||
"new_version": "1.7.56-alpha",
|
"new_version": "1.7.57-alpha",
|
||||||
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.56-alpha/archipelago-frontend-1.7.56-alpha.tar.gz",
|
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.57-alpha/archipelago-frontend-1.7.57-alpha.tar.gz",
|
||||||
"sha256": "91bfa10085696921c929ab7d216a8e617eb02e7105f21f32cdc3a2ba15158cd4",
|
"sha256": "a09c1507afb4b7e9f5279b92215e92ebc0b86155ae53ac2c0baaecd07522c7ee",
|
||||||
"size_bytes": 166465069
|
"size_bytes": 78322316
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -99,6 +99,10 @@ reset_spec() {
|
|||||||
SPEC_ENTRYPOINT=""
|
SPEC_ENTRYPOINT=""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ! declare -F alloc_port >/dev/null 2>&1; then
|
||||||
|
alloc_port() { printf '%s' "$2"; }
|
||||||
|
fi
|
||||||
|
|
||||||
# ── Tier 0: Databases ────────────────────────────────────────────────
|
# ── Tier 0: Databases ────────────────────────────────────────────────
|
||||||
|
|
||||||
load_spec_archy-mempool-db() {
|
load_spec_archy-mempool-db() {
|
||||||
@ -493,13 +497,17 @@ load_spec_nginx-proxy-manager() {
|
|||||||
reset_spec
|
reset_spec
|
||||||
SPEC_NAME="nginx-proxy-manager"
|
SPEC_NAME="nginx-proxy-manager"
|
||||||
SPEC_IMAGE="${NPM_IMAGE}"
|
SPEC_IMAGE="${NPM_IMAGE}"
|
||||||
SPEC_PORTS="81:81 8084:80 8444:443"
|
local admin_port http_port https_port
|
||||||
|
admin_port=$(alloc_port nginx-proxy-manager 8081 81)
|
||||||
|
http_port=$(alloc_port nginx-proxy-manager-http 8084 80)
|
||||||
|
https_port=$(alloc_port nginx-proxy-manager-https 8444 443)
|
||||||
|
SPEC_PORTS="$admin_port:81 $http_port:80 $https_port:443"
|
||||||
SPEC_VOLUMES="/var/lib/archipelago/nginx-proxy-manager/data:/data /var/lib/archipelago/nginx-proxy-manager/letsencrypt:/etc/letsencrypt"
|
SPEC_VOLUMES="/var/lib/archipelago/nginx-proxy-manager/data:/data /var/lib/archipelago/nginx-proxy-manager/letsencrypt:/etc/letsencrypt"
|
||||||
SPEC_MEMORY="$(mem_limit nginx-proxy-manager)"
|
SPEC_MEMORY="$(mem_limit nginx-proxy-manager)"
|
||||||
SPEC_HEALTH_CMD="curl -sf http://localhost:81/ || exit 1"
|
SPEC_HEALTH_CMD="curl -sf http://localhost:81/ || exit 1"
|
||||||
SPEC_TIER="3"
|
SPEC_TIER="3"
|
||||||
SPEC_DATA_DIR="/var/lib/archipelago/nginx-proxy-manager"
|
SPEC_DATA_DIR="/var/lib/archipelago/nginx-proxy-manager"
|
||||||
SPEC_CAPS="CHOWN SETUID SETGID NET_BIND_SERVICE"
|
SPEC_CAPS="CHOWN FOWNER SETUID SETGID DAC_OVERRIDE NET_BIND_SERVICE"
|
||||||
SPEC_OPTIONAL="true"
|
SPEC_OPTIONAL="true"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -914,7 +914,7 @@ LNDCONF
|
|||||||
--health-cmd 'curl -sf http://localhost:81/' --health-interval=30s --health-timeout=5s --health-retries=3 \
|
--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 \
|
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID --cap-add NET_BIND_SERVICE \
|
||||||
--security-opt no-new-privileges:true \
|
--security-opt no-new-privileges:true \
|
||||||
-p 81:81 -p 8084:80 -p 8444:443 \
|
-p 8081:81 -p 8084:80 -p 8444:443 \
|
||||||
-v /var/lib/archipelago/nginx-proxy-manager/data:/data \
|
-v /var/lib/archipelago/nginx-proxy-manager/data:/data \
|
||||||
-v /var/lib/archipelago/nginx-proxy-manager/letsencrypt:/etc/letsencrypt \
|
-v /var/lib/archipelago/nginx-proxy-manager/letsencrypt:/etc/letsencrypt \
|
||||||
$NPM_IMAGE
|
$NPM_IMAGE
|
||||||
|
|||||||
@ -48,6 +48,37 @@ SCRIPT_DIR_FBC="$(cd "$(dirname "$0")" && pwd)"
|
|||||||
# as root (rootful podman), the backend can't see them at all.
|
# as root (rootful podman), the backend can't see them at all.
|
||||||
DOCKER="runuser -u archipelago -- env XDG_RUNTIME_DIR=/run/user/$(id -u archipelago) podman"
|
DOCKER="runuser -u archipelago -- env XDG_RUNTIME_DIR=/run/user/$(id -u archipelago) podman"
|
||||||
|
|
||||||
|
PORT_ALLOC_FILE="/var/lib/archipelago/port-allocations.env"
|
||||||
|
mkdir -p /var/lib/archipelago 2>/dev/null || true
|
||||||
|
[ -f "$PORT_ALLOC_FILE" ] && . "$PORT_ALLOC_FILE"
|
||||||
|
|
||||||
|
port_available() {
|
||||||
|
local port="$1"
|
||||||
|
ss -ltn 2>/dev/null | awk -v p=":$port" '$4 == p || $4 ~ p "$" { found=1 } END { exit found ? 1 : 0 }'
|
||||||
|
}
|
||||||
|
|
||||||
|
alloc_port() {
|
||||||
|
local key="$1" preferred="$2" var="PORT_${key//[^A-Za-z0-9]/_}" cur=""
|
||||||
|
eval "cur=\${$var:-}"
|
||||||
|
if [ -n "$cur" ] && port_available "$cur"; then
|
||||||
|
printf '%s' "$cur"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if port_available "$preferred"; then
|
||||||
|
cur="$preferred"
|
||||||
|
else
|
||||||
|
cur=""
|
||||||
|
for p in $(seq 8085 9999); do
|
||||||
|
if port_available "$p"; then cur="$p"; break; fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
[ -n "$cur" ] || cur="$preferred"
|
||||||
|
if ! grep -q "^$var=" "$PORT_ALLOC_FILE" 2>/dev/null; then
|
||||||
|
printf '%s=%s\n' "$var" "$cur" >> "$PORT_ALLOC_FILE"
|
||||||
|
fi
|
||||||
|
printf '%s' "$cur"
|
||||||
|
}
|
||||||
|
|
||||||
# UNBUNDLED mode: only create FileBrowser, skip all other containers.
|
# UNBUNDLED mode: only create FileBrowser, skip all other containers.
|
||||||
# Users install apps on-demand from the Marketplace.
|
# Users install apps on-demand from the Marketplace.
|
||||||
UNBUNDLED_MARKER="/opt/archipelago/.unbundled"
|
UNBUNDLED_MARKER="/opt/archipelago/.unbundled"
|
||||||
@ -1146,12 +1177,15 @@ track_container "filebrowser"
|
|||||||
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q nginx-proxy-manager; then
|
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q nginx-proxy-manager; then
|
||||||
log "Creating Nginx Proxy Manager..."
|
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 /var/lib/archipelago/nginx-proxy-manager/letsencrypt
|
||||||
|
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)
|
||||||
$DOCKER run -d --name nginx-proxy-manager --restart unless-stopped \
|
$DOCKER run -d --name nginx-proxy-manager --restart unless-stopped \
|
||||||
--health-cmd="curl -sf http://localhost:81/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
|
--health-cmd="curl -sf http://localhost:81/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
|
||||||
--memory=$(mem_limit nginx-proxy-manager) \
|
--memory=$(mem_limit nginx-proxy-manager) \
|
||||||
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID --cap-add NET_BIND_SERVICE \
|
--cap-drop ALL --cap-add CHOWN --cap-add FOWNER --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE --cap-add NET_BIND_SERVICE \
|
||||||
--security-opt no-new-privileges:true \
|
--security-opt no-new-privileges:true \
|
||||||
-p 81:81 -p 8084:80 -p 8444:443 \
|
-p ${NPM_ADMIN_PORT}:81 -p ${NPM_HTTP_PORT}:80 -p ${NPM_HTTPS_PORT}:443 \
|
||||||
-v /var/lib/archipelago/nginx-proxy-manager/data:/data \
|
-v /var/lib/archipelago/nginx-proxy-manager/data:/data \
|
||||||
-v /var/lib/archipelago/nginx-proxy-manager/letsencrypt:/etc/letsencrypt \
|
-v /var/lib/archipelago/nginx-proxy-manager/letsencrypt:/etc/letsencrypt \
|
||||||
"${NPM_IMAGE}" 2>>"$LOG" || true
|
"${NPM_IMAGE}" 2>>"$LOG" || true
|
||||||
|
|||||||
@ -204,7 +204,7 @@ location /app/electrs-ui/ {
|
|||||||
proxy_hide_header Content-Security-Policy;
|
proxy_hide_header Content-Security-Policy;
|
||||||
}
|
}
|
||||||
location /app/nginx-proxy-manager/ {
|
location /app/nginx-proxy-manager/ {
|
||||||
proxy_pass http://127.0.0.1:81/;
|
proxy_pass http://127.0.0.1:8081/;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
|||||||
@ -63,6 +63,37 @@ header(){ echo -e "\n${BOLD}$*${NC}"; }
|
|||||||
source "$SCRIPT_DIR/container-specs.sh" || { echo "Cannot source container-specs.sh"; exit 1; }
|
source "$SCRIPT_DIR/container-specs.sh" || { echo "Cannot source container-specs.sh"; exit 1; }
|
||||||
detect_environment
|
detect_environment
|
||||||
|
|
||||||
|
PORT_ALLOC_FILE="/var/lib/archipelago/port-allocations.env"
|
||||||
|
[ -f "$PORT_ALLOC_FILE" ] && . "$PORT_ALLOC_FILE"
|
||||||
|
|
||||||
|
port_available() {
|
||||||
|
local port="$1"
|
||||||
|
ss -ltn 2>/dev/null | awk -v p=":$port" '$4 == p || $4 ~ p "$" { found=1 } END { exit found ? 1 : 0 }'
|
||||||
|
}
|
||||||
|
|
||||||
|
alloc_port() {
|
||||||
|
local key="$1" preferred="$2" var="PORT_${key//[^A-Za-z0-9]/_}" cur=""
|
||||||
|
eval "cur=\${$var:-}"
|
||||||
|
if [ -n "$cur" ] && port_available "$cur"; then
|
||||||
|
printf '%s' "$cur"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if port_available "$preferred"; then
|
||||||
|
cur="$preferred"
|
||||||
|
else
|
||||||
|
cur=""
|
||||||
|
for p in $(seq 8085 9999); do
|
||||||
|
if port_available "$p"; then cur="$p"; break; fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
[ -n "$cur" ] || cur="$preferred"
|
||||||
|
sudo mkdir -p "$(dirname "$PORT_ALLOC_FILE")" 2>/dev/null || true
|
||||||
|
if ! grep -q "^$var=" "$PORT_ALLOC_FILE" 2>/dev/null; then
|
||||||
|
printf '%s=%s\n' "$var" "$cur" | sudo tee -a "$PORT_ALLOC_FILE" >/dev/null
|
||||||
|
fi
|
||||||
|
printf '%s' "$cur"
|
||||||
|
}
|
||||||
|
|
||||||
# ── Podman command ───────────────────────────────────────────────────
|
# ── Podman command ───────────────────────────────────────────────────
|
||||||
# Run as archipelago user — podman sees rootless containers directly.
|
# Run as archipelago user — podman sees rootless containers directly.
|
||||||
# Use sudo only for chown/mkdir operations.
|
# Use sudo only for chown/mkdir operations.
|
||||||
@ -154,6 +185,39 @@ host_port_listening() {
|
|||||||
'
|
'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prepare_bind_source() {
|
||||||
|
local source="$1"
|
||||||
|
[ -n "$source" ] || return 0
|
||||||
|
|
||||||
|
case "$source" in
|
||||||
|
/run/user/*/podman/podman.sock)
|
||||||
|
if [ ! -S "$source" ]; then
|
||||||
|
local runtime_dir="${source%/podman/podman.sock}"
|
||||||
|
XDG_RUNTIME_DIR="$runtime_dir" systemctl --user start podman.socket 2>/dev/null || true
|
||||||
|
for _ in 1 2 3 4 5 6 7 8 9 10; do
|
||||||
|
[ -S "$source" ] && return 0
|
||||||
|
sleep 0.25
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
case "$source" in
|
||||||
|
/var/lib/archipelago/*)
|
||||||
|
sudo mkdir -p "$source" 2>/dev/null
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
# Non-data bind mounts can be files/sockets/devices. Creating the full
|
||||||
|
# path would turn e.g. podman.sock into a directory and break Portainer.
|
||||||
|
if [ -e "$source" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fail "bind source missing: $source"
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
container_has_mount() {
|
container_has_mount() {
|
||||||
local name="$1" source="$2" target="$3"
|
local name="$1" source="$2" target="$3"
|
||||||
$PODMAN inspect "$name" --format '{{range .Mounts}}{{println .Source "|" .Destination}}{{end}}' 2>/dev/null \
|
$PODMAN inspect "$name" --format '{{range .Mounts}}{{println .Source "|" .Destination}}{{end}}' 2>/dev/null \
|
||||||
@ -536,7 +600,11 @@ reconcile() {
|
|||||||
else
|
else
|
||||||
for v in $SPEC_VOLUMES; do
|
for v in $SPEC_VOLUMES; do
|
||||||
local host_dir="${v%%:*}"
|
local host_dir="${v%%:*}"
|
||||||
[ -n "$host_dir" ] && sudo mkdir -p "$host_dir" 2>/dev/null
|
prepare_bind_source "$host_dir" || {
|
||||||
|
COUNT_FAILED=$((COUNT_FAILED + 1))
|
||||||
|
FAILED_LIST+=" $name"
|
||||||
|
return
|
||||||
|
}
|
||||||
done
|
done
|
||||||
if eval "$(build_run_cmd)" >/dev/null 2>&1; then
|
if eval "$(build_run_cmd)" >/dev/null 2>&1; then
|
||||||
fixed "$name — created"
|
fixed "$name — created"
|
||||||
|
|||||||
@ -178,7 +178,7 @@ launch_url_for() {
|
|||||||
ollama) echo "http://${ARCHY_HOST}:11434/" ;;
|
ollama) echo "http://${ARCHY_HOST}:11434/" ;;
|
||||||
immich|immich_server) echo "http://${ARCHY_HOST}:2283/" ;;
|
immich|immich_server) echo "http://${ARCHY_HOST}:2283/" ;;
|
||||||
portainer) echo "http://${ARCHY_HOST}:9000/" ;;
|
portainer) echo "http://${ARCHY_HOST}:9000/" ;;
|
||||||
nginx-proxy-manager) echo "http://${ARCHY_HOST}:81/" ;;
|
nginx-proxy-manager) echo "http://${ARCHY_HOST}:8081/" ;;
|
||||||
tailscale) echo "http://${ARCHY_HOST}:8240/" ;;
|
tailscale) echo "http://${ARCHY_HOST}:8240/" ;;
|
||||||
uptime-kuma) echo "http://${ARCHY_HOST}:3002/" ;;
|
uptime-kuma) echo "http://${ARCHY_HOST}:3002/" ;;
|
||||||
homeassistant) echo "http://${ARCHY_HOST}:8123/" ;;
|
homeassistant) echo "http://${ARCHY_HOST}:8123/" ;;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user