From 09ec64932feb8e52d39fe0d8abcd6ea6dd1aa551 Mon Sep 17 00:00:00 2001 From: archipelago Date: Thu, 11 Jun 2026 00:24:20 -0400 Subject: [PATCH] app-platform: generate catalog from app manifests --- app-catalog/catalog.json | 321 ++++++++++++++---- apps/archy-nbxplorer/manifest.yml | 4 +- apps/bitcoin-knots/manifest.yml | 12 +- apps/botfights/manifest.yml | 6 +- apps/btcpay-server/manifest.yml | 8 +- apps/electrumx/manifest.yml | 2 +- apps/fedimint/manifest.yml | 25 +- apps/gitea/manifest.yml | 127 ++++--- apps/grafana/manifest.yml | 8 +- apps/home-assistant/manifest.yml | 35 +- apps/indeedhub/manifest.yml | 11 +- apps/jellyfin/manifest.yml | 52 +++ apps/lnd/manifest.yml | 4 +- apps/mempool/manifest.yml | 6 +- apps/meshtastic/manifest.yml | 38 ++- apps/nextcloud/manifest.yml | 50 +++ apps/nostr-rs-relay/manifest.yml | 6 +- apps/onlyoffice/manifest.yml | 4 + apps/photoprism/manifest.yml | 51 +++ apps/portainer/manifest.yml | 64 ++++ apps/searxng/manifest.yml | 4 +- apps/uptime-kuma/manifest.yml | 54 +++ apps/vaultwarden/manifest.yml | 51 +++ .../assets/img/app-icons/archipelago-a.svg | 3 + .../public/assets/img/app-icons/meshcore.svg | 28 ++ .../public/assets/img/app-icons/saleor.svg | 5 - neode-ui/public/catalog.json | 321 ++++++++++++++---- .../appSession/generatedAppSessionConfig.ts | 90 +++++ scripts/check-app-catalog-drift.py | 173 ++++++++++ scripts/generate-app-catalog.py | 205 +++++++++++ 30 files changed, 1533 insertions(+), 235 deletions(-) create mode 100644 apps/jellyfin/manifest.yml create mode 100644 apps/nextcloud/manifest.yml create mode 100644 apps/photoprism/manifest.yml create mode 100644 apps/portainer/manifest.yml create mode 100644 apps/uptime-kuma/manifest.yml create mode 100644 apps/vaultwarden/manifest.yml create mode 100644 neode-ui/public/assets/img/app-icons/archipelago-a.svg create mode 100644 neode-ui/public/assets/img/app-icons/meshcore.svg delete mode 100644 neode-ui/public/assets/img/app-icons/saleor.svg create mode 100644 neode-ui/src/views/appSession/generatedAppSessionConfig.ts create mode 100644 scripts/check-app-catalog-drift.py create mode 100644 scripts/generate-app-catalog.py diff --git a/app-catalog/catalog.json b/app-catalog/catalog.json index 68e55d82..e55d4b0d 100644 --- a/app-catalog/catalog.json +++ b/app-catalog/catalog.json @@ -14,7 +14,7 @@ "id": "bitcoin-knots", "title": "Bitcoin Knots", "version": "28.1.0", - "description": "Run a full Bitcoin node. Validate and relay blocks and transactions.", + "description": "Full Bitcoin Knots node with dynamic prune/full-mode startup based on host disk.", "icon": "/assets/img/app-icons/bitcoin-knots.webp", "author": "Bitcoin Knots", "category": "money", @@ -25,8 +25,8 @@ { "id": "bitcoin-core", "title": "Bitcoin Core", - "version": "28.4", - "description": "Reference Bitcoin node implementation. Alternative to Bitcoin Knots; uninstall Knots before switching.", + "version": "28.4.0", + "description": "Reference Bitcoin Core node with dynamic prune/full-mode startup based on host disk.", "icon": "/assets/img/app-icons/bitcoin-core.svg", "author": "Bitcoin Core contributors", "category": "money", @@ -38,7 +38,7 @@ "id": "lnd", "title": "LND", "version": "0.18.4", - "description": "Lightning Network Daemon. Fast Bitcoin payments through Lightning.", + "description": "Lightning Network implementation by Lightning Labs. Enables instant, low-cost Bitcoin payments.", "icon": "/assets/img/app-icons/lnd.svg", "author": "Lightning Labs", "category": "money", @@ -53,7 +53,7 @@ "id": "btcpay-server", "title": "BTCPay Server", "version": "2.3.9", - "description": "Self-hosted Bitcoin payment processor.", + "description": "Self-hosted Bitcoin payment processor. Accept Bitcoin payments without intermediaries.", "icon": "/assets/img/app-icons/btcpay-server.png", "author": "BTCPay Server Foundation", "category": "commerce", @@ -76,8 +76,17 @@ "dockerImage": "ghcr.io/saleor/saleor:3.23", "repoUrl": "https://github.com/saleor/saleor", "containerConfig": { - "ports": ["9011:80", "9010:80", "8000:8000", "8025:8025", "16686:16686"], - "volumes": ["/var/lib/archipelago/saleor:/app/media", "/var/lib/archipelago/saleor-db:/var/lib/postgresql/data"], + "ports": [ + "9011:80", + "9010:80", + "8000:8000", + "8025:8025", + "16686:16686" + ], + "volumes": [ + "/var/lib/archipelago/saleor:/app/media", + "/var/lib/archipelago/saleor-db:/var/lib/postgresql/data" + ], "notes": "Installed as a Saleor stack: customer storefront on 9011, admin dashboard on 9010, API on 8000, Mailpit on 8025, and Jaeger on 16686. Supporting containers include PostgreSQL, Valkey, Celery worker, and services required by Saleor." } }, @@ -85,7 +94,7 @@ "id": "mempool", "title": "Mempool Explorer", "version": "3.0.0", - "description": "Self-hosted Bitcoin blockchain and mempool visualizer.", + "description": "Bitcoin mempool and blockchain explorer. Real-time transaction and block visualization.", "icon": "/assets/img/app-icons/mempool.webp", "author": "Mempool", "category": "money", @@ -101,7 +110,7 @@ "id": "electrumx", "title": "ElectrumX", "version": "1.18.0", - "description": "Electrum protocol server. Index the blockchain for fast wallet lookups.", + "description": "Electrum server indexing Bitcoin chain data for lightweight wallet queries.", "icon": "/assets/img/app-icons/electrumx.png", "author": "Luke Childs", "category": "money", @@ -116,7 +125,7 @@ "id": "indeedhub", "title": "IndeeHub", "version": "1.0.0", - "description": "Bitcoin documentary streaming with Nostr identity.", + "description": "Bitcoin documentary streaming platform featuring God Bless Bitcoin and other educational content about Bitcoin, sovereignty, and decentralized technology. Sign in with your Nostr identity.", "icon": "/assets/img/app-icons/indeedhub.png", "author": "IndeeHub", "category": "community", @@ -127,49 +136,133 @@ "id": "botfights", "title": "BotFights", "version": "1.1.0", - "description": "Bot arena + 2-player arcade fighter with controller support and Adventure Mode.", + "description": "Bot competition arena with 2-player arcade fighting mode. AI bots battle in trivia challenges while humans duke it out with controllers. Built for Bitcoiners.", "icon": "/assets/img/app-icons/botfights.svg", "author": "BotFights", "category": "community", "dockerImage": "146.59.87.168:3000/lfg2025/botfights:1.1.0", "repoUrl": "https://botfights.net", "containerConfig": { - "ports": ["9100:9100"], - "volumes": ["/var/lib/archipelago/botfights:/app/server/data"], - "env": ["NODE_ENV=production", "PORT=9100", "FIGHT_LOOP_ENABLED=true", "ARCHY_EMBEDDED=1"] + "ports": [ + "9100:9100" + ], + "volumes": [ + "/var/lib/archipelago/botfights:/app/server/data" + ], + "env": [ + "NODE_ENV=production", + "PORT=9100", + "FIGHT_LOOP_ENABLED=true", + "ARCHY_EMBEDDED=1" + ] } }, { "id": "gitea", "title": "Gitea", "version": "1.23", - "description": "Self-hosted Git service with container registry, CI/CD, issue tracking.", + "description": "Self-hosted Git service with built-in container registry, CI/CD, and package hosting.", "icon": "/assets/img/app-icons/gitea.svg", "author": "Gitea", "category": "development", - "dockerImage": "146.59.87.168:3000/lfg2025/gitea:1.23", + "dockerImage": "docker.io/gitea/gitea:1.23", "repoUrl": "https://gitea.com", "containerConfig": { - "ports": ["3001:3000", "2222:22"], - "volumes": ["/var/lib/archipelago/gitea/data:/data", "/var/lib/archipelago/gitea/config:/etc/gitea"], - "env": ["GITEA__database__DB_TYPE=sqlite3", "GITEA__server__SSH_PORT=2222", "GITEA__server__SSH_LISTEN_PORT=22", "GITEA__server__LFS_START_SERVER=true", "GITEA__packages__ENABLED=true", "GITEA__repository__ENABLE_PUSH_CREATE_USER=true", "GITEA__repository__ENABLE_PUSH_CREATE_ORG=true", "GITEA__security__X_FRAME_OPTIONS="] - } + "ports": [ + "3001:3000", + "2222:22" + ], + "volumes": [ + "/var/lib/archipelago/gitea/data:/data", + "/var/lib/archipelago/gitea/config:/etc/gitea" + ], + "env": [ + "GITEA__database__DB_TYPE=sqlite3", + "GITEA__server__SSH_PORT=2222", + "GITEA__server__SSH_LISTEN_PORT=22", + "GITEA__server__LFS_START_SERVER=true", + "GITEA__packages__ENABLED=true", + "GITEA__repository__ENABLE_PUSH_CREATE_USER=true", + "GITEA__repository__ENABLE_PUSH_CREATE_ORG=true", + "GITEA__security__X_FRAME_OPTIONS=" + ] + }, + "tier": "optional" }, { "id": "filebrowser", "title": "File Browser", "version": "2.27.0", - "description": "Web-based file manager.", + "description": "Baseline Archipelago file manager service.", "icon": "/assets/img/app-icons/file-browser.webp", "author": "File Browser", "category": "data", "tier": "core", - "dockerImage": "146.59.87.168:3000/lfg2025/filebrowser:v2.27.0", + "dockerImage": "git.tx1138.com/lfg2025/filebrowser:v2.27.0", "repoUrl": "https://github.com/filebrowser/filebrowser", "containerConfig": { - "ports": ["8083:80"], - "volumes": ["/var/lib/archipelago/filebrowser:/srv", "/var/lib/archipelago/filebrowser-data:/data"], - "args": ["--database=/data/database.db", "--root=/srv", "--address=0.0.0.0", "--port=80"] + "ports": [ + "8083:80" + ], + "volumes": [ + "/var/lib/archipelago/filebrowser:/srv", + "/var/lib/archipelago/filebrowser-data:/data" + ], + "args": [ + "--database=/data/database.db", + "--root=/srv", + "--address=0.0.0.0", + "--port=80" + ] + } + }, + { + "id": "nostr-rs-relay", + "title": "Nostr Relay (Rust)", + "version": "0.8.0", + "description": "High-performance Nostr relay written in Rust. Host your own decentralized social media relay and earn networking profits.", + "icon": "/assets/img/app-icons/nostr.svg", + "author": "Nostr RS Relay", + "category": "community", + "tier": "recommended", + "dockerImage": "scsibug/nostr-rs-relay:0.8.9", + "repoUrl": "https://github.com/scsibug/nostr-rs-relay", + "containerConfig": { + "ports": [ + "8081:8080" + ], + "volumes": [ + "/var/lib/archipelago/nostr-relay:/usr/src/app/db" + ], + "env": [ + "RELAY_NAME=Archipelago Nostr Relay", + "RELAY_DESCRIPTION=Self-hosted Nostr relay on Archipelago" + ] + } + }, + { + "id": "meshtastic", + "title": "Meshtastic", + "version": "2-daily-alpine", + "description": "Open-source mesh networking for LoRa radios. Create decentralized communication networks.", + "icon": "/assets/img/app-icons/meshcore.svg", + "author": "Meshtastic", + "category": "networking", + "tier": "recommended", + "dockerImage": "docker.io/meshtastic/meshtasticd:daily-alpine", + "repoUrl": "https://github.com/meshtastic/firmware", + "containerConfig": { + "ports": [ + "4403:4403" + ], + "volumes": [ + "/var/lib/archipelago/meshtastic:/var/lib/meshtasticd" + ], + "env": [ + "MESHTASTIC_PORT=/dev/ttyUSB0", + "MESHTASTIC_SERIAL=true" + ], + "notes": "Requires a LoRa radio device at /dev/ttyUSB0. The config file is rendered from the app manifest before container start." } }, { @@ -184,15 +277,19 @@ "dockerImage": "146.59.87.168:3000/lfg2025/vaultwarden:1.30.0-alpine", "repoUrl": "https://github.com/dani-garcia/vaultwarden", "containerConfig": { - "ports": ["8082:80"], - "volumes": ["/var/lib/archipelago/vaultwarden:/data"] + "ports": [ + "8082:80" + ], + "volumes": [ + "/var/lib/archipelago/vaultwarden:/data" + ] } }, { "id": "searxng", "title": "SearXNG", - "version": "2024.1.0", - "description": "Privacy-respecting metasearch engine.", + "version": "1.0.0", + "description": "Privacy-respecting metasearch engine. Search the web without tracking.", "icon": "/assets/img/app-icons/searxng.png", "author": "SearXNG", "category": "data", @@ -200,21 +297,46 @@ "dockerImage": "146.59.87.168:3000/lfg2025/searxng:latest", "repoUrl": "https://github.com/searxng/searxng", "containerConfig": { - "ports": ["8888:8080"], - "volumes": ["/var/lib/archipelago/searxng:/etc/searxng"] + "ports": [ + "8888:8080" + ], + "volumes": [ + "/var/lib/archipelago/searxng:/etc/searxng" + ] } }, { "id": "fedimint", "title": "Fedimint", "version": "0.10.0", - "description": "Federated Bitcoin mint with privacy through federated guardians.", + "description": "Federated Bitcoin minting service with built-in Guardian UI. Privacy-preserving Bitcoin custody.", "icon": "/assets/img/app-icons/fedimint.png", "author": "Fedimint", "category": "money", "dockerImage": "146.59.87.168:3000/lfg2025/fedimintd:v0.10.0", "repoUrl": "https://github.com/fedimint/fedimint" }, + { + "id": "fedimint-gateway", + "title": "Fedimint Gateway", + "version": "0.10.0", + "description": "Fedimint gateway service with automatic LND-or-LDK backend selection.", + "icon": "/assets/img/app-icons/fedimint.png", + "author": "Fedimint", + "category": "money", + "dockerImage": "git.tx1138.com/lfg2025/gatewayd:v0.10.0", + "repoUrl": "https://github.com/fedimint/fedimint", + "containerConfig": { + "ports": [ + "8176:8176", + "9737:9737" + ], + "volumes": [ + "/var/lib/archipelago/fedimint-gateway:/data", + "/var/lib/archipelago/lnd:/lnd:ro" + ] + } + }, { "id": "jellyfin", "title": "Jellyfin", @@ -226,8 +348,13 @@ "dockerImage": "146.59.87.168:3000/lfg2025/jellyfin:10.8.13", "repoUrl": "https://github.com/jellyfin/jellyfin", "containerConfig": { - "ports": ["8096:8096"], - "volumes": ["/var/lib/archipelago/jellyfin/config:/config", "/var/lib/archipelago/jellyfin/cache:/cache"] + "ports": [ + "8096:8096" + ], + "volumes": [ + "/var/lib/archipelago/jellyfin/config:/config", + "/var/lib/archipelago/jellyfin/cache:/cache" + ] } }, { @@ -244,34 +371,47 @@ { "id": "homeassistant", "title": "Home Assistant", - "version": "2024.1", - "description": "Open-source home automation.", + "version": "2024.1.0", + "description": "Open source home automation platform. Control and monitor your smart home devices.", "icon": "/assets/img/app-icons/homeassistant.png", "author": "Home Assistant", "category": "home", "dockerImage": "146.59.87.168:3000/lfg2025/home-assistant:2024.1", "repoUrl": "https://github.com/home-assistant/core", "containerConfig": { - "ports": ["8123:8123"], - "volumes": ["/var/lib/archipelago/home-assistant:/config"], - "env": ["TZ=UTC"] + "ports": [ + "8123:8123" + ], + "volumes": [ + "/var/lib/archipelago/home-assistant:/config" + ], + "env": [ + "TZ=UTC" + ] } }, { "id": "grafana", "title": "Grafana", "version": "10.2.0", - "description": "Analytics and monitoring dashboards.", + "description": "Analytics and monitoring platform. Visualize metrics and create dashboards.", "icon": "/assets/img/app-icons/grafana.png", "author": "Grafana Labs", "category": "data", "tier": "recommended", - "dockerImage": "146.59.87.168:3000/lfg2025/grafana:10.2.0", + "dockerImage": "grafana/grafana:10.2.0", "repoUrl": "https://github.com/grafana/grafana", "containerConfig": { - "ports": ["3000:3000"], - "volumes": ["/var/lib/archipelago/grafana:/var/lib/grafana"], - "env": ["GF_PATHS_DATA=/var/lib/grafana", "GF_USERS_ALLOW_SIGN_UP=false"] + "ports": [ + "3000:3000" + ], + "volumes": [ + "/var/lib/archipelago/grafana:/var/lib/grafana" + ], + "env": [ + "GF_PATHS_DATA=/var/lib/grafana", + "GF_USERS_ALLOW_SIGN_UP=false" + ] } }, { @@ -286,10 +426,42 @@ "dockerImage": "146.59.87.168:3000/lfg2025/tailscale:stable", "repoUrl": "https://github.com/tailscale/tailscale", "containerConfig": { - "ports": ["8240:8240"], - "volumes": ["/var/lib/archipelago/tailscale:/var/lib/tailscale"], - "env": ["TS_STATE_DIR=/var/lib/tailscale"], - "args": ["sh", "-c", "tailscaled --tun=userspace-networking & sleep 2; tailscale web --listen 0.0.0.0:8240 & wait"] + "ports": [ + "8240:8240" + ], + "volumes": [ + "/var/lib/archipelago/tailscale:/var/lib/tailscale" + ], + "env": [ + "TS_STATE_DIR=/var/lib/tailscale" + ], + "args": [ + "sh", + "-c", + "tailscaled --tun=userspace-networking & for i in $(seq 1 30); do [ -S /var/run/tailscale/tailscaled.sock ] && break; sleep 1; done; tailscale web --listen 0.0.0.0:8240 & wait" + ] + } + }, + { + "id": "portainer", + "title": "Portainer", + "version": "2.19.4", + "description": "Container management web UI for the local Podman socket.", + "icon": "/assets/img/app-icons/portainer.webp", + "author": "Portainer", + "category": "development", + "tier": "optional", + "dockerImage": "146.59.87.168:3000/lfg2025/portainer:latest", + "repoUrl": "https://github.com/portainer/portainer", + "containerConfig": { + "ports": [ + "9000:9000" + ], + "volumes": [ + "/var/lib/archipelago/portainer:/data", + "/run/user/1000/podman/podman.sock:/var/run/docker.sock" + ], + "notes": "Uses the manifest-owned Podman socket bind mount preparation path." } }, { @@ -304,8 +476,14 @@ "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"], + "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." } }, @@ -321,10 +499,20 @@ "dockerImage": "146.59.87.168:3000/lfg2025/uptime-kuma:1", "repoUrl": "https://github.com/louislam/uptime-kuma", "containerConfig": { - "ports": ["3002:3001"], - "volumes": ["/var/lib/archipelago/uptime-kuma:/app/data"], - "env": ["TZ=UTC"], - "args": ["--", "node", "server/server.js"] + "ports": [ + "3002:3001" + ], + "volumes": [ + "/var/lib/archipelago/uptime-kuma:/app/data" + ], + "env": [ + "TZ=UTC" + ], + "args": [ + "--", + "node", + "server/server.js" + ] } }, { @@ -338,24 +526,35 @@ "dockerImage": "146.59.87.168:3000/lfg2025/photoprism:240915", "repoUrl": "https://github.com/photoprism/photoprism", "containerConfig": { - "ports": ["2342:2342"], - "volumes": ["/var/lib/archipelago/photoprism:/photoprism/storage"], - "env": ["PHOTOPRISM_ADMIN_PASSWORD=archipelago", "PHOTOPRISM_DEFAULT_LOCALE=en"] + "ports": [ + "2342:2342" + ], + "volumes": [ + "/var/lib/archipelago/photoprism:/photoprism/storage" + ], + "env": [ + "PHOTOPRISM_ADMIN_PASSWORD=archipelago", + "PHOTOPRISM_DEFAULT_LOCALE=en" + ] } }, { "id": "nextcloud", "title": "Nextcloud", - "version": "28", + "version": "29", "description": "Your own private cloud. File sync, calendars, contacts.", "icon": "/assets/img/app-icons/nextcloud.webp", "author": "Nextcloud", "category": "data", - "dockerImage": "146.59.87.168:3000/lfg2025/nextcloud:28", + "dockerImage": "146.59.87.168:3000/lfg2025/nextcloud:29", "repoUrl": "https://github.com/nextcloud/server", "containerConfig": { - "ports": ["8085:80"], - "volumes": ["/var/lib/archipelago/nextcloud:/var/www/html"] + "ports": [ + "8085:80" + ], + "volumes": [ + "/var/lib/archipelago/nextcloud:/var/www/html" + ] } } ] diff --git a/apps/archy-nbxplorer/manifest.yml b/apps/archy-nbxplorer/manifest.yml index 1d48d74c..f4b7cd99 100644 --- a/apps/archy-nbxplorer/manifest.yml +++ b/apps/archy-nbxplorer/manifest.yml @@ -56,8 +56,8 @@ app: endpoint: http://localhost:32838 path: / interval: 30s - timeout: 5s - retries: 3 + timeout: 30s + retries: 5 bitcoin_integration: rpc_access: read-only diff --git a/apps/bitcoin-knots/manifest.yml b/apps/bitcoin-knots/manifest.yml index 0e390f95..23001742 100644 --- a/apps/bitcoin-knots/manifest.yml +++ b/apps/bitcoin-knots/manifest.yml @@ -28,11 +28,17 @@ app: fi; RPC_USER="$(printenv BITCOIN_RPC_USER)"; RPC_PASS="$(printenv BITCOIN_RPC_PASS)"; + RPC_TXRELAY_AUTH="$(printenv BITCOIN_RPC_TXRELAY_RPCAUTH || true)"; DISK_GB_VALUE="$(printenv DISK_GB || true)"; + RPC_HEADROOM="-rpcthreads=16 -rpcworkqueue=256"; + RPC_TXRELAY_FLAGS="-rpcwhitelistdefault=0"; + if [ -n "$RPC_TXRELAY_AUTH" ]; then + RPC_TXRELAY_FLAGS="$RPC_TXRELAY_FLAGS -rpcauth=$RPC_TXRELAY_AUTH -rpcwhitelist=txrelay:sendrawtransaction,testmempoolaccept,getmempoolinfo,getrawmempool,getmempoolentry,getnetworkinfo,getblockchaininfo,getblockcount,getblockhash,getblockheader,getrawtransaction,decoderawtransaction,decodescript,estimatesmartfee"; + fi; if [ "${DISK_GB_VALUE:-0}" -lt 1000 ]; then - exec "$BITCOIND" -datadir=/home/bitcoin/.bitcoin -noconf -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 -rpcuser="$RPC_USER" -rpcpassword="$RPC_PASS"; + exec "$BITCOIND" -datadir=/home/bitcoin/.bitcoin -noconf -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 $RPC_HEADROOM $RPC_TXRELAY_FLAGS -rpcuser="$RPC_USER" -rpcpassword="$RPC_PASS"; else - exec "$BITCOIND" -datadir=/home/bitcoin/.bitcoin -noconf -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 -rpcuser="$RPC_USER" -rpcpassword="$RPC_PASS"; + exec "$BITCOIND" -datadir=/home/bitcoin/.bitcoin -noconf -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 $RPC_HEADROOM $RPC_TXRELAY_FLAGS -rpcuser="$RPC_USER" -rpcpassword="$RPC_PASS"; fi derived_env: - key: DISK_GB @@ -40,6 +46,8 @@ app: secret_env: - key: BITCOIN_RPC_PASS secret_file: bitcoin-rpc-password + - key: BITCOIN_RPC_TXRELAY_RPCAUTH + secret_file: bitcoin-rpc-txrelay-rpcauth data_uid: "100101:100101" dependencies: diff --git a/apps/botfights/manifest.yml b/apps/botfights/manifest.yml index 736a927c..3fcb25cf 100644 --- a/apps/botfights/manifest.yml +++ b/apps/botfights/manifest.yml @@ -1,12 +1,12 @@ app: id: botfights name: BotFights - version: 1.0.0 + version: 1.1.0 description: Bot competition arena with 2-player arcade fighting mode. AI bots battle in trivia challenges while humans duke it out with controllers. Built for Bitcoiners. category: community container: - image: git.tx1138.com/lfg2025/botfights:1.1.0 + image: 146.59.87.168:3000/lfg2025/botfights:1.1.0 pull_policy: always dependencies: @@ -62,6 +62,8 @@ app: metadata: author: Dorian + repo: https://botfights.net + icon: /assets/img/app-icons/botfights.svg license: MIT tags: - bitcoin diff --git a/apps/btcpay-server/manifest.yml b/apps/btcpay-server/manifest.yml index 6c42c267..4f0d9af0 100644 --- a/apps/btcpay-server/manifest.yml +++ b/apps/btcpay-server/manifest.yml @@ -60,8 +60,8 @@ app: endpoint: http://localhost:49392 path: / interval: 30s - timeout: 5s - retries: 3 + timeout: 30s + retries: 5 bitcoin_integration: rpc_access: read-only @@ -79,3 +79,7 @@ app: port: 23000 protocol: http path: / + + metadata: + launch: + open_in_new_tab: true diff --git a/apps/electrumx/manifest.yml b/apps/electrumx/manifest.yml index 395f1069..e765754a 100644 --- a/apps/electrumx/manifest.yml +++ b/apps/electrumx/manifest.yml @@ -5,7 +5,7 @@ app: description: Electrum server indexing Bitcoin chain data for lightweight wallet queries. container: - image: git.tx1138.com/lfg2025/electrumx:v1.18.0 + image: 146.59.87.168:3000/lfg2025/electrumx:v1.18.0 pull_policy: if-not-present network: archy-net data_uid: "1000:1000" diff --git a/apps/fedimint/manifest.yml b/apps/fedimint/manifest.yml index ca6f4154..88094578 100644 --- a/apps/fedimint/manifest.yml +++ b/apps/fedimint/manifest.yml @@ -5,9 +5,17 @@ app: description: Federated Bitcoin minting service with built-in Guardian UI. Privacy-preserving Bitcoin custody. container: - image: git.tx1138.com/lfg2025/fedimintd:v0.10.0 + image: 146.59.87.168:3000/lfg2025/fedimintd:v0.10.0 pull_policy: if-not-present network: archy-net + entrypoint: ["sh", "-lc"] + custom_args: + - |- + until state="$(curl -sS --connect-timeout 5 -m 45 -u "$FM_BITCOIND_USERNAME:$FM_BITCOIND_PASSWORD" -H "Content-Type: application/json" --data-binary '{"jsonrpc":"1.0","id":"fedimint-wait","method":"getblockchaininfo","params":[]}' "$FM_BITCOIND_URL/")" && echo "$state" | grep -q '"initialblockdownload":false'; do + echo "Waiting for Bitcoin RPC sync at $FM_BITCOIND_URL..."; + sleep 30; + done; + exec fedimintd derived_env: - key: FM_P2P_URL template: fedimint://{{HOST_MDNS}}:8173 @@ -40,7 +48,9 @@ app: - host: 8174 container: 8174 protocol: tcp - - host: 8175 + # Public launch port 8175 is owned by archy-fedimint-ui, which serves a + # wait page while Bitcoin syncs and proxies here after fedimintd starts. + - host: 8177 container: 8175 protocol: tcp @@ -52,7 +62,7 @@ app: environment: - FM_DATA_DIR=/data - - FM_BITCOIND_URL=http://host.archipelago:8332 + - FM_BITCOIND_URL=http://bitcoin-knots:8332 - FM_BITCOIND_USERNAME=archipelago - FM_BITCOIN_NETWORK=bitcoin - FM_BIND_P2P=0.0.0.0:8173 @@ -67,6 +77,15 @@ app: timeout: 5s retries: 3 + interfaces: + main: + name: Guardian UI + description: Fedimint Guardian wait/proxy UI + type: ui + port: 8175 + protocol: http + path: / + bitcoin_integration: rpc_access: admin sync_required: true diff --git a/apps/gitea/manifest.yml b/apps/gitea/manifest.yml index 9f0053cf..3f80b57d 100644 --- a/apps/gitea/manifest.yml +++ b/apps/gitea/manifest.yml @@ -1,52 +1,87 @@ -id: gitea -name: Gitea -version: "1.23" -description: Self-hosted Git service with built-in container registry, CI/CD, and package hosting. -category: development -icon: git-branch -port: 3000 -internal_port: 3001 -ssh_port: 2222 -image: docker.io/gitea/gitea:1.23 -tier: optional +app: + id: gitea + name: Gitea + version: "1.23" + description: Self-hosted Git service with built-in container registry, CI/CD, and package hosting. + category: development -requires: - memory_mb: 256 - disk_mb: 500 + container: + image: docker.io/gitea/gitea:1.23 + pull_policy: if-not-present -volumes: - - host: /var/lib/archipelago/gitea/data - container: /data - - host: /var/lib/archipelago/gitea/config - container: /etc/gitea + dependencies: + - storage: 500Mi -environment: - GITEA__database__DB_TYPE: sqlite3 - GITEA__server__SSH_PORT: "2222" - GITEA__server__SSH_LISTEN_PORT: "22" - GITEA__server__LFS_START_SERVER: "true" - GITEA__packages__ENABLED: "true" - GITEA__repository__ENABLE_PUSH_CREATE_USER: "true" - GITEA__repository__ENABLE_PUSH_CREATE_ORG: "true" + resources: + memory_limit: 256Mi + disk_limit: 500Mi -# Gitea hardcodes X-Frame-Options: SAMEORIGIN, so Archipelago opens it in a -# new tab on host port 3001 instead of embedding it in an iframe. -nginx_proxy: - listen: 3000 - proxy_pass: "http://127.0.0.1:3001" - extra_headers: - - "proxy_hide_header X-Frame-Options" - - "proxy_hide_header Content-Security-Policy" + security: + capabilities: [CHOWN, FOWNER, SETUID, SETGID, DAC_OVERRIDE, NET_BIND_SERVICE] + readonly_root: false + no_new_privileges: false + network_policy: bridge -health_check: - endpoint: / - interval: 120 - timeout: 5 - retries: 3 + ports: + - host: 3001 + container: 3000 + protocol: tcp + - host: 2222 + container: 22 + protocol: tcp -features: - - Git repositories with web UI - - Built-in container/package registry - - Issue tracking and pull requests - - CI/CD via Gitea Actions - - Lightweight (SQLite, no external DB needed) + volumes: + - type: bind + source: /var/lib/archipelago/gitea/data + target: /data + options: [rw] + - type: bind + source: /var/lib/archipelago/gitea/config + target: /etc/gitea + options: [rw] + + environment: + - GITEA__database__DB_TYPE=sqlite3 + - GITEA__server__SSH_PORT=2222 + - GITEA__server__SSH_LISTEN_PORT=22 + - GITEA__server__LFS_START_SERVER=true + - GITEA__packages__ENABLED=true + - GITEA__repository__ENABLE_PUSH_CREATE_USER=true + - GITEA__repository__ENABLE_PUSH_CREATE_ORG=true + + health_check: + type: http + endpoint: http://localhost:3000 + path: / + interval: 120s + timeout: 30s + retries: 5 + + interfaces: + main: + name: Web UI + description: Gitea web interface + type: ui + port: 3001 + protocol: http + path: / + + metadata: + icon: /assets/img/app-icons/gitea.svg + repo: https://gitea.com + tier: optional + launch: + open_in_new_tab: true + features: + - Git repositories with web UI + - Built-in container/package registry + - Issue tracking and pull requests + - CI/CD via Gitea Actions + - Lightweight SQLite deployment + + nginx_proxy: + listen: 3000 + proxy_pass: http://127.0.0.1:3001 + extra_headers: + - proxy_hide_header X-Frame-Options + - proxy_hide_header Content-Security-Policy diff --git a/apps/grafana/manifest.yml b/apps/grafana/manifest.yml index 85fe534a..ed6a6b42 100644 --- a/apps/grafana/manifest.yml +++ b/apps/grafana/manifest.yml @@ -49,5 +49,9 @@ app: endpoint: http://localhost:3000 path: /api/health interval: 30s - timeout: 5s - retries: 3 + timeout: 30s + retries: 5 + + metadata: + launch: + open_in_new_tab: true diff --git a/apps/home-assistant/manifest.yml b/apps/home-assistant/manifest.yml index fb502f63..4c76ba44 100644 --- a/apps/home-assistant/manifest.yml +++ b/apps/home-assistant/manifest.yml @@ -1,29 +1,29 @@ app: - id: home-assistant + id: homeassistant name: Home Assistant version: 2024.1.0 description: Open source home automation platform. Control and monitor your smart home devices. container: - image: homeassistant/home-assistant:2024.1 - image_signature: cosign://... + image: 146.59.87.168:3000/lfg2025/home-assistant:2024.1 pull_policy: if-not-present + network: pasta dependencies: - storage: 10Gi resources: cpu_limit: 2 - memory_limit: 2Gi + memory_limit: 512Mi disk_limit: 10Gi security: - capabilities: [NET_BIND_SERVICE] + capabilities: [CHOWN, FOWNER, SETUID, SETGID, DAC_OVERRIDE, NET_BIND_SERVICE, NET_RAW] readonly_root: false # Home Assistant needs write access no_new_privileges: true user: 1000 seccomp_profile: default - network_policy: host # Requires host network for device discovery + network_policy: isolated apparmor_profile: home-assistant ports: @@ -36,24 +36,23 @@ app: source: /var/lib/archipelago/home-assistant target: /config options: [rw] - - type: bind - source: /var/run/dbus - target: /var/run/dbus - options: [ro] - devices: - - /dev/ttyUSB0 # Serial devices - - /dev/ttyACM0 # USB devices + devices: [] environment: - TZ=UTC - - PUID=1000 - - PGID=1000 health_check: - type: http - endpoint: http://localhost:8123 - path: / + type: tcp + endpoint: localhost:8123 interval: 30s timeout: 5s retries: 3 + + metadata: + icon: /assets/img/app-icons/homeassistant.png + category: home + author: Home Assistant + repo: https://github.com/home-assistant/core + launch: + open_in_new_tab: true diff --git a/apps/indeedhub/manifest.yml b/apps/indeedhub/manifest.yml index 0e443741..cf625470 100644 --- a/apps/indeedhub/manifest.yml +++ b/apps/indeedhub/manifest.yml @@ -1,12 +1,12 @@ app: id: indeedhub - name: Indeehub - version: 0.1.0 + name: IndeeHub + version: 1.0.0 description: Bitcoin documentary streaming platform featuring God Bless Bitcoin and other educational content about Bitcoin, sovereignty, and decentralized technology. Sign in with your Nostr identity. - category: media + category: community container: - image: 146.59.87.168:3000/lfg2025/indeedhub:latest + image: 146.59.87.168:3000/lfg2025/indeedhub:1.0.0 pull_policy: always # Pull from registry; falls back to local build network: indeedhub-net @@ -70,8 +70,9 @@ app: metadata: author: Indeehub Team + icon: /assets/img/app-icons/indeedhub.png website: https://indeedhub.com - source: https://github.com/indeedhub/indeedhub + repo: https://github.com/indeedhub/indeedhub license: MIT tags: - bitcoin diff --git a/apps/jellyfin/manifest.yml b/apps/jellyfin/manifest.yml new file mode 100644 index 00000000..410d61ca --- /dev/null +++ b/apps/jellyfin/manifest.yml @@ -0,0 +1,52 @@ +app: + id: jellyfin + name: Jellyfin + version: 10.8.13 + description: Free media server. Stream movies, music, and photos. + + container: + image: 146.59.87.168:3000/lfg2025/jellyfin:10.8.13 + pull_policy: if-not-present + network: pasta + + dependencies: + - storage: 10Gi + + resources: + memory_limit: 1Gi + disk_limit: 10Gi + + security: + capabilities: [CHOWN, FOWNER, SETUID, SETGID, DAC_OVERRIDE] + readonly_root: false + network_policy: isolated + + ports: + - host: 8096 + container: 8096 + protocol: tcp + + volumes: + - type: bind + source: /var/lib/archipelago/jellyfin/config + target: /config + options: [rw] + - type: bind + source: /var/lib/archipelago/jellyfin/cache + target: /cache + options: [rw] + + environment: [] + + health_check: + type: tcp + endpoint: localhost:8096 + interval: 30s + timeout: 5s + retries: 3 + + metadata: + icon: /assets/img/app-icons/jellyfin.webp + category: data + author: Jellyfin + repo: https://github.com/jellyfin/jellyfin diff --git a/apps/lnd/manifest.yml b/apps/lnd/manifest.yml index 0e39aff5..ec5e2ee4 100644 --- a/apps/lnd/manifest.yml +++ b/apps/lnd/manifest.yml @@ -1,11 +1,11 @@ app: id: lnd - name: Lightning Network Daemon + name: LND version: 0.18.4 description: Lightning Network implementation by Lightning Labs. Enables instant, low-cost Bitcoin payments. container: - image: git.tx1138.com/lfg2025/lnd:v0.18.4-beta + image: 146.59.87.168:3000/lfg2025/lnd:v0.18.4-beta pull_policy: if-not-present network: archy-net secret_env: diff --git a/apps/mempool/manifest.yml b/apps/mempool/manifest.yml index bbce179a..b16dff95 100644 --- a/apps/mempool/manifest.yml +++ b/apps/mempool/manifest.yml @@ -1,11 +1,11 @@ app: id: mempool - name: Mempool - version: 2.5.0 + name: Mempool Explorer + version: 3.0.0 description: Bitcoin mempool and blockchain explorer. Real-time transaction and block visualization. container: - image: mempool/mempool:v2.5.0 + image: 146.59.87.168:3000/lfg2025/mempool-frontend:v3.0.0 image_signature: cosign://... pull_policy: if-not-present diff --git a/apps/meshtastic/manifest.yml b/apps/meshtastic/manifest.yml index dd6698b8..d8aecdd4 100644 --- a/apps/meshtastic/manifest.yml +++ b/apps/meshtastic/manifest.yml @@ -1,13 +1,12 @@ app: id: meshtastic name: Meshtastic - version: 2.5.0 + version: 2-daily-alpine description: Open-source mesh networking for LoRa radios. Create decentralized communication networks. container: - image: meshtastic/meshtasticd:2.5.6 - image_signature: cosign://... - pull_policy: verify-signature + image: docker.io/meshtastic/meshtasticd:daily-alpine + pull_policy: if-not-present dependencies: - storage: 1Gi @@ -29,33 +28,42 @@ app: ports: - host: 4403 container: 4403 - protocol: tcp # HTTP API - - host: 1883 - container: 1883 - protocol: tcp # MQTT (optional) + protocol: tcp # Meshtastic TCP API devices: - /dev/ttyUSB0 # LoRa radio device (if connected) - - /dev/ttyACM0 # Alternative device path volumes: - type: bind source: /var/lib/archipelago/meshtastic - target: /app/data + target: /var/lib/meshtasticd options: [rw] + + files: + - path: /var/lib/archipelago/meshtastic/config.yaml + content: | + General: + MACAddress: AA:BB:CC:DD:EE:01 + Webserver: + Port: 4403 environment: - MESHTASTIC_PORT=/dev/ttyUSB0 - MESHTASTIC_SERIAL=true health_check: - type: http - endpoint: http://localhost:4403 - path: /health + type: cmd + endpoint: test -f /var/lib/meshtasticd/config.yaml interval: 30s - timeout: 5s - retries: 3 + timeout: 30s + retries: 5 networking: mesh_enabled: true local_network_access: true + + metadata: + icon: /assets/img/app-icons/meshcore.svg + category: networking + tier: recommended + repo: https://github.com/meshtastic/firmware diff --git a/apps/nextcloud/manifest.yml b/apps/nextcloud/manifest.yml new file mode 100644 index 00000000..d438e583 --- /dev/null +++ b/apps/nextcloud/manifest.yml @@ -0,0 +1,50 @@ +app: + id: nextcloud + name: Nextcloud + version: "29" + description: Your own private cloud. File sync, calendars, contacts. + + container: + image: 146.59.87.168:3000/lfg2025/nextcloud:29 + pull_policy: if-not-present + network: pasta + + dependencies: + - storage: 10Gi + + resources: + memory_limit: 1Gi + disk_limit: 10Gi + + security: + capabilities: [CHOWN, SETUID, SETGID, DAC_OVERRIDE, NET_BIND_SERVICE] + readonly_root: false + network_policy: isolated + + ports: + - host: 8085 + container: 80 + protocol: tcp + + volumes: + - type: bind + source: /var/lib/archipelago/nextcloud + target: /var/www/html + options: [rw] + + environment: [] + + health_check: + type: tcp + endpoint: localhost:80 + interval: 30s + timeout: 5s + retries: 3 + + metadata: + icon: /assets/img/app-icons/nextcloud.webp + category: data + author: Nextcloud + repo: https://github.com/nextcloud/server + launch: + open_in_new_tab: true diff --git a/apps/nostr-rs-relay/manifest.yml b/apps/nostr-rs-relay/manifest.yml index 073005bc..975a07ae 100644 --- a/apps/nostr-rs-relay/manifest.yml +++ b/apps/nostr-rs-relay/manifest.yml @@ -28,7 +28,7 @@ app: apparmor_profile: nostr-relay ports: - - host: 8081 + - host: 18081 container: 8080 protocol: tcp # HTTP/WebSocket @@ -49,8 +49,8 @@ app: endpoint: http://localhost:8080 path: / interval: 30s - timeout: 5s - retries: 3 + timeout: 30s + retries: 5 nostr_integration: relay_type: public diff --git a/apps/onlyoffice/manifest.yml b/apps/onlyoffice/manifest.yml index 16285d7f..30881cba 100644 --- a/apps/onlyoffice/manifest.yml +++ b/apps/onlyoffice/manifest.yml @@ -48,3 +48,7 @@ app: interval: 30s timeout: 5s retries: 3 + + metadata: + launch: + open_in_new_tab: true diff --git a/apps/photoprism/manifest.yml b/apps/photoprism/manifest.yml new file mode 100644 index 00000000..2966300a --- /dev/null +++ b/apps/photoprism/manifest.yml @@ -0,0 +1,51 @@ +app: + id: photoprism + name: PhotoPrism + version: "240915" + description: AI-powered photo management with facial recognition. + + container: + image: 146.59.87.168:3000/lfg2025/photoprism:240915 + pull_policy: if-not-present + + dependencies: + - storage: 10Gi + + resources: + memory_limit: 1Gi + disk_limit: 10Gi + + security: + capabilities: [CHOWN, SETUID, SETGID] + readonly_root: false + network_policy: isolated + + ports: + - host: 2342 + container: 2342 + protocol: tcp + + volumes: + - type: bind + source: /var/lib/archipelago/photoprism + target: /photoprism/storage + options: [rw] + + environment: + - PHOTOPRISM_ADMIN_PASSWORD=archipelago + - PHOTOPRISM_DEFAULT_LOCALE=en + + health_check: + type: tcp + endpoint: localhost:2342 + interval: 60s + timeout: 5s + retries: 3 + + metadata: + icon: /assets/img/app-icons/photoprism.svg + category: data + author: PhotoPrism + repo: https://github.com/photoprism/photoprism + launch: + open_in_new_tab: true diff --git a/apps/portainer/manifest.yml b/apps/portainer/manifest.yml new file mode 100644 index 00000000..96e63f1e --- /dev/null +++ b/apps/portainer/manifest.yml @@ -0,0 +1,64 @@ +app: + id: portainer + name: Portainer + version: 2.19.4 + description: Container management web UI for the local Podman socket. + category: development + + container: + image: 146.59.87.168:3000/lfg2025/portainer:latest + pull_policy: if-not-present + data_uid: "1000:1000" + + dependencies: + - storage: 1Gi + + resources: + memory_limit: 256Mi + disk_limit: 1Gi + + security: + capabilities: [CHOWN, SETUID, SETGID, DAC_OVERRIDE] + readonly_root: false + no_new_privileges: true + network_policy: isolated + + ports: + - host: 9000 + container: 9000 + protocol: tcp + + volumes: + - type: bind + source: /var/lib/archipelago/portainer + target: /data + options: [rw] + - type: bind + source: /var/lib/archipelago/portainer/compose + target: /data/compose + options: [rw] + - type: bind + source: /run/user/1000/podman/podman.sock + target: /var/run/docker.sock + options: [rw] + + environment: [] + + interfaces: + main: + name: Web UI + description: Portainer web interface + type: ui + port: 9000 + protocol: http + path: / + + metadata: + icon: /assets/img/app-icons/portainer.webp + tier: optional + launch: + open_in_new_tab: true + features: + - Container management dashboard + - Local Podman socket access + - Compose stack storage diff --git a/apps/searxng/manifest.yml b/apps/searxng/manifest.yml index db8cb286..1eeb727d 100644 --- a/apps/searxng/manifest.yml +++ b/apps/searxng/manifest.yml @@ -45,5 +45,5 @@ app: endpoint: http://localhost:8080 path: / interval: 30s - timeout: 5s - retries: 3 + timeout: 30s + retries: 5 diff --git a/apps/uptime-kuma/manifest.yml b/apps/uptime-kuma/manifest.yml new file mode 100644 index 00000000..e58f0bbe --- /dev/null +++ b/apps/uptime-kuma/manifest.yml @@ -0,0 +1,54 @@ +app: + id: uptime-kuma + name: Uptime Kuma + version: 1.23.0 + description: Self-hosted uptime monitoring. + + container: + image: 146.59.87.168:3000/lfg2025/uptime-kuma:1 + pull_policy: if-not-present + network: pasta + custom_args: ["--", "node", "server/server.js"] + + dependencies: + - storage: 1Gi + + resources: + memory_limit: 256Mi + disk_limit: 1Gi + + security: + capabilities: [CHOWN, FOWNER, SETUID, SETGID] + readonly_root: false + network_policy: isolated + + ports: + - host: 3002 + container: 3001 + protocol: tcp + + volumes: + - type: bind + source: /var/lib/archipelago/uptime-kuma + target: /app/data + options: [rw] + + environment: + - TZ=UTC + + health_check: + type: http + endpoint: localhost:3001 + path: / + interval: 30s + timeout: 5s + retries: 3 + + metadata: + icon: /assets/img/app-icons/uptime-kuma.webp + category: data + tier: recommended + author: Uptime Kuma + repo: https://github.com/louislam/uptime-kuma + launch: + open_in_new_tab: true diff --git a/apps/vaultwarden/manifest.yml b/apps/vaultwarden/manifest.yml new file mode 100644 index 00000000..c7718267 --- /dev/null +++ b/apps/vaultwarden/manifest.yml @@ -0,0 +1,51 @@ +app: + id: vaultwarden + name: Vaultwarden + version: 1.30.0 + description: Self-hosted password vault with zero-knowledge encryption. + + container: + image: 146.59.87.168:3000/lfg2025/vaultwarden:1.30.0-alpine + pull_policy: if-not-present + network: pasta + + dependencies: + - storage: 1Gi + + resources: + memory_limit: 256Mi + disk_limit: 1Gi + + security: + capabilities: [CHOWN, SETUID, SETGID, NET_BIND_SERVICE] + readonly_root: false + network_policy: isolated + + ports: + - host: 8082 + container: 80 + protocol: tcp + + volumes: + - type: bind + source: /var/lib/archipelago/vaultwarden + target: /data + options: [rw] + + environment: [] + + health_check: + type: tcp + endpoint: localhost:80 + interval: 30s + timeout: 5s + retries: 3 + + metadata: + icon: /assets/img/app-icons/vaultwarden.webp + category: data + tier: recommended + author: Vaultwarden + repo: https://github.com/dani-garcia/vaultwarden + launch: + open_in_new_tab: true diff --git a/neode-ui/public/assets/img/app-icons/archipelago-a.svg b/neode-ui/public/assets/img/app-icons/archipelago-a.svg new file mode 100644 index 00000000..7989db9c --- /dev/null +++ b/neode-ui/public/assets/img/app-icons/archipelago-a.svg @@ -0,0 +1,3 @@ + + + diff --git a/neode-ui/public/assets/img/app-icons/meshcore.svg b/neode-ui/public/assets/img/app-icons/meshcore.svg new file mode 100644 index 00000000..30469c96 --- /dev/null +++ b/neode-ui/public/assets/img/app-icons/meshcore.svg @@ -0,0 +1,28 @@ + + Meshtastic + LoRa mesh radio nodes connected by signal links + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/neode-ui/public/assets/img/app-icons/saleor.svg b/neode-ui/public/assets/img/app-icons/saleor.svg deleted file mode 100644 index 0720b6e1..00000000 --- a/neode-ui/public/assets/img/app-icons/saleor.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/neode-ui/public/catalog.json b/neode-ui/public/catalog.json index 68e55d82..e55d4b0d 100644 --- a/neode-ui/public/catalog.json +++ b/neode-ui/public/catalog.json @@ -14,7 +14,7 @@ "id": "bitcoin-knots", "title": "Bitcoin Knots", "version": "28.1.0", - "description": "Run a full Bitcoin node. Validate and relay blocks and transactions.", + "description": "Full Bitcoin Knots node with dynamic prune/full-mode startup based on host disk.", "icon": "/assets/img/app-icons/bitcoin-knots.webp", "author": "Bitcoin Knots", "category": "money", @@ -25,8 +25,8 @@ { "id": "bitcoin-core", "title": "Bitcoin Core", - "version": "28.4", - "description": "Reference Bitcoin node implementation. Alternative to Bitcoin Knots; uninstall Knots before switching.", + "version": "28.4.0", + "description": "Reference Bitcoin Core node with dynamic prune/full-mode startup based on host disk.", "icon": "/assets/img/app-icons/bitcoin-core.svg", "author": "Bitcoin Core contributors", "category": "money", @@ -38,7 +38,7 @@ "id": "lnd", "title": "LND", "version": "0.18.4", - "description": "Lightning Network Daemon. Fast Bitcoin payments through Lightning.", + "description": "Lightning Network implementation by Lightning Labs. Enables instant, low-cost Bitcoin payments.", "icon": "/assets/img/app-icons/lnd.svg", "author": "Lightning Labs", "category": "money", @@ -53,7 +53,7 @@ "id": "btcpay-server", "title": "BTCPay Server", "version": "2.3.9", - "description": "Self-hosted Bitcoin payment processor.", + "description": "Self-hosted Bitcoin payment processor. Accept Bitcoin payments without intermediaries.", "icon": "/assets/img/app-icons/btcpay-server.png", "author": "BTCPay Server Foundation", "category": "commerce", @@ -76,8 +76,17 @@ "dockerImage": "ghcr.io/saleor/saleor:3.23", "repoUrl": "https://github.com/saleor/saleor", "containerConfig": { - "ports": ["9011:80", "9010:80", "8000:8000", "8025:8025", "16686:16686"], - "volumes": ["/var/lib/archipelago/saleor:/app/media", "/var/lib/archipelago/saleor-db:/var/lib/postgresql/data"], + "ports": [ + "9011:80", + "9010:80", + "8000:8000", + "8025:8025", + "16686:16686" + ], + "volumes": [ + "/var/lib/archipelago/saleor:/app/media", + "/var/lib/archipelago/saleor-db:/var/lib/postgresql/data" + ], "notes": "Installed as a Saleor stack: customer storefront on 9011, admin dashboard on 9010, API on 8000, Mailpit on 8025, and Jaeger on 16686. Supporting containers include PostgreSQL, Valkey, Celery worker, and services required by Saleor." } }, @@ -85,7 +94,7 @@ "id": "mempool", "title": "Mempool Explorer", "version": "3.0.0", - "description": "Self-hosted Bitcoin blockchain and mempool visualizer.", + "description": "Bitcoin mempool and blockchain explorer. Real-time transaction and block visualization.", "icon": "/assets/img/app-icons/mempool.webp", "author": "Mempool", "category": "money", @@ -101,7 +110,7 @@ "id": "electrumx", "title": "ElectrumX", "version": "1.18.0", - "description": "Electrum protocol server. Index the blockchain for fast wallet lookups.", + "description": "Electrum server indexing Bitcoin chain data for lightweight wallet queries.", "icon": "/assets/img/app-icons/electrumx.png", "author": "Luke Childs", "category": "money", @@ -116,7 +125,7 @@ "id": "indeedhub", "title": "IndeeHub", "version": "1.0.0", - "description": "Bitcoin documentary streaming with Nostr identity.", + "description": "Bitcoin documentary streaming platform featuring God Bless Bitcoin and other educational content about Bitcoin, sovereignty, and decentralized technology. Sign in with your Nostr identity.", "icon": "/assets/img/app-icons/indeedhub.png", "author": "IndeeHub", "category": "community", @@ -127,49 +136,133 @@ "id": "botfights", "title": "BotFights", "version": "1.1.0", - "description": "Bot arena + 2-player arcade fighter with controller support and Adventure Mode.", + "description": "Bot competition arena with 2-player arcade fighting mode. AI bots battle in trivia challenges while humans duke it out with controllers. Built for Bitcoiners.", "icon": "/assets/img/app-icons/botfights.svg", "author": "BotFights", "category": "community", "dockerImage": "146.59.87.168:3000/lfg2025/botfights:1.1.0", "repoUrl": "https://botfights.net", "containerConfig": { - "ports": ["9100:9100"], - "volumes": ["/var/lib/archipelago/botfights:/app/server/data"], - "env": ["NODE_ENV=production", "PORT=9100", "FIGHT_LOOP_ENABLED=true", "ARCHY_EMBEDDED=1"] + "ports": [ + "9100:9100" + ], + "volumes": [ + "/var/lib/archipelago/botfights:/app/server/data" + ], + "env": [ + "NODE_ENV=production", + "PORT=9100", + "FIGHT_LOOP_ENABLED=true", + "ARCHY_EMBEDDED=1" + ] } }, { "id": "gitea", "title": "Gitea", "version": "1.23", - "description": "Self-hosted Git service with container registry, CI/CD, issue tracking.", + "description": "Self-hosted Git service with built-in container registry, CI/CD, and package hosting.", "icon": "/assets/img/app-icons/gitea.svg", "author": "Gitea", "category": "development", - "dockerImage": "146.59.87.168:3000/lfg2025/gitea:1.23", + "dockerImage": "docker.io/gitea/gitea:1.23", "repoUrl": "https://gitea.com", "containerConfig": { - "ports": ["3001:3000", "2222:22"], - "volumes": ["/var/lib/archipelago/gitea/data:/data", "/var/lib/archipelago/gitea/config:/etc/gitea"], - "env": ["GITEA__database__DB_TYPE=sqlite3", "GITEA__server__SSH_PORT=2222", "GITEA__server__SSH_LISTEN_PORT=22", "GITEA__server__LFS_START_SERVER=true", "GITEA__packages__ENABLED=true", "GITEA__repository__ENABLE_PUSH_CREATE_USER=true", "GITEA__repository__ENABLE_PUSH_CREATE_ORG=true", "GITEA__security__X_FRAME_OPTIONS="] - } + "ports": [ + "3001:3000", + "2222:22" + ], + "volumes": [ + "/var/lib/archipelago/gitea/data:/data", + "/var/lib/archipelago/gitea/config:/etc/gitea" + ], + "env": [ + "GITEA__database__DB_TYPE=sqlite3", + "GITEA__server__SSH_PORT=2222", + "GITEA__server__SSH_LISTEN_PORT=22", + "GITEA__server__LFS_START_SERVER=true", + "GITEA__packages__ENABLED=true", + "GITEA__repository__ENABLE_PUSH_CREATE_USER=true", + "GITEA__repository__ENABLE_PUSH_CREATE_ORG=true", + "GITEA__security__X_FRAME_OPTIONS=" + ] + }, + "tier": "optional" }, { "id": "filebrowser", "title": "File Browser", "version": "2.27.0", - "description": "Web-based file manager.", + "description": "Baseline Archipelago file manager service.", "icon": "/assets/img/app-icons/file-browser.webp", "author": "File Browser", "category": "data", "tier": "core", - "dockerImage": "146.59.87.168:3000/lfg2025/filebrowser:v2.27.0", + "dockerImage": "git.tx1138.com/lfg2025/filebrowser:v2.27.0", "repoUrl": "https://github.com/filebrowser/filebrowser", "containerConfig": { - "ports": ["8083:80"], - "volumes": ["/var/lib/archipelago/filebrowser:/srv", "/var/lib/archipelago/filebrowser-data:/data"], - "args": ["--database=/data/database.db", "--root=/srv", "--address=0.0.0.0", "--port=80"] + "ports": [ + "8083:80" + ], + "volumes": [ + "/var/lib/archipelago/filebrowser:/srv", + "/var/lib/archipelago/filebrowser-data:/data" + ], + "args": [ + "--database=/data/database.db", + "--root=/srv", + "--address=0.0.0.0", + "--port=80" + ] + } + }, + { + "id": "nostr-rs-relay", + "title": "Nostr Relay (Rust)", + "version": "0.8.0", + "description": "High-performance Nostr relay written in Rust. Host your own decentralized social media relay and earn networking profits.", + "icon": "/assets/img/app-icons/nostr.svg", + "author": "Nostr RS Relay", + "category": "community", + "tier": "recommended", + "dockerImage": "scsibug/nostr-rs-relay:0.8.9", + "repoUrl": "https://github.com/scsibug/nostr-rs-relay", + "containerConfig": { + "ports": [ + "8081:8080" + ], + "volumes": [ + "/var/lib/archipelago/nostr-relay:/usr/src/app/db" + ], + "env": [ + "RELAY_NAME=Archipelago Nostr Relay", + "RELAY_DESCRIPTION=Self-hosted Nostr relay on Archipelago" + ] + } + }, + { + "id": "meshtastic", + "title": "Meshtastic", + "version": "2-daily-alpine", + "description": "Open-source mesh networking for LoRa radios. Create decentralized communication networks.", + "icon": "/assets/img/app-icons/meshcore.svg", + "author": "Meshtastic", + "category": "networking", + "tier": "recommended", + "dockerImage": "docker.io/meshtastic/meshtasticd:daily-alpine", + "repoUrl": "https://github.com/meshtastic/firmware", + "containerConfig": { + "ports": [ + "4403:4403" + ], + "volumes": [ + "/var/lib/archipelago/meshtastic:/var/lib/meshtasticd" + ], + "env": [ + "MESHTASTIC_PORT=/dev/ttyUSB0", + "MESHTASTIC_SERIAL=true" + ], + "notes": "Requires a LoRa radio device at /dev/ttyUSB0. The config file is rendered from the app manifest before container start." } }, { @@ -184,15 +277,19 @@ "dockerImage": "146.59.87.168:3000/lfg2025/vaultwarden:1.30.0-alpine", "repoUrl": "https://github.com/dani-garcia/vaultwarden", "containerConfig": { - "ports": ["8082:80"], - "volumes": ["/var/lib/archipelago/vaultwarden:/data"] + "ports": [ + "8082:80" + ], + "volumes": [ + "/var/lib/archipelago/vaultwarden:/data" + ] } }, { "id": "searxng", "title": "SearXNG", - "version": "2024.1.0", - "description": "Privacy-respecting metasearch engine.", + "version": "1.0.0", + "description": "Privacy-respecting metasearch engine. Search the web without tracking.", "icon": "/assets/img/app-icons/searxng.png", "author": "SearXNG", "category": "data", @@ -200,21 +297,46 @@ "dockerImage": "146.59.87.168:3000/lfg2025/searxng:latest", "repoUrl": "https://github.com/searxng/searxng", "containerConfig": { - "ports": ["8888:8080"], - "volumes": ["/var/lib/archipelago/searxng:/etc/searxng"] + "ports": [ + "8888:8080" + ], + "volumes": [ + "/var/lib/archipelago/searxng:/etc/searxng" + ] } }, { "id": "fedimint", "title": "Fedimint", "version": "0.10.0", - "description": "Federated Bitcoin mint with privacy through federated guardians.", + "description": "Federated Bitcoin minting service with built-in Guardian UI. Privacy-preserving Bitcoin custody.", "icon": "/assets/img/app-icons/fedimint.png", "author": "Fedimint", "category": "money", "dockerImage": "146.59.87.168:3000/lfg2025/fedimintd:v0.10.0", "repoUrl": "https://github.com/fedimint/fedimint" }, + { + "id": "fedimint-gateway", + "title": "Fedimint Gateway", + "version": "0.10.0", + "description": "Fedimint gateway service with automatic LND-or-LDK backend selection.", + "icon": "/assets/img/app-icons/fedimint.png", + "author": "Fedimint", + "category": "money", + "dockerImage": "git.tx1138.com/lfg2025/gatewayd:v0.10.0", + "repoUrl": "https://github.com/fedimint/fedimint", + "containerConfig": { + "ports": [ + "8176:8176", + "9737:9737" + ], + "volumes": [ + "/var/lib/archipelago/fedimint-gateway:/data", + "/var/lib/archipelago/lnd:/lnd:ro" + ] + } + }, { "id": "jellyfin", "title": "Jellyfin", @@ -226,8 +348,13 @@ "dockerImage": "146.59.87.168:3000/lfg2025/jellyfin:10.8.13", "repoUrl": "https://github.com/jellyfin/jellyfin", "containerConfig": { - "ports": ["8096:8096"], - "volumes": ["/var/lib/archipelago/jellyfin/config:/config", "/var/lib/archipelago/jellyfin/cache:/cache"] + "ports": [ + "8096:8096" + ], + "volumes": [ + "/var/lib/archipelago/jellyfin/config:/config", + "/var/lib/archipelago/jellyfin/cache:/cache" + ] } }, { @@ -244,34 +371,47 @@ { "id": "homeassistant", "title": "Home Assistant", - "version": "2024.1", - "description": "Open-source home automation.", + "version": "2024.1.0", + "description": "Open source home automation platform. Control and monitor your smart home devices.", "icon": "/assets/img/app-icons/homeassistant.png", "author": "Home Assistant", "category": "home", "dockerImage": "146.59.87.168:3000/lfg2025/home-assistant:2024.1", "repoUrl": "https://github.com/home-assistant/core", "containerConfig": { - "ports": ["8123:8123"], - "volumes": ["/var/lib/archipelago/home-assistant:/config"], - "env": ["TZ=UTC"] + "ports": [ + "8123:8123" + ], + "volumes": [ + "/var/lib/archipelago/home-assistant:/config" + ], + "env": [ + "TZ=UTC" + ] } }, { "id": "grafana", "title": "Grafana", "version": "10.2.0", - "description": "Analytics and monitoring dashboards.", + "description": "Analytics and monitoring platform. Visualize metrics and create dashboards.", "icon": "/assets/img/app-icons/grafana.png", "author": "Grafana Labs", "category": "data", "tier": "recommended", - "dockerImage": "146.59.87.168:3000/lfg2025/grafana:10.2.0", + "dockerImage": "grafana/grafana:10.2.0", "repoUrl": "https://github.com/grafana/grafana", "containerConfig": { - "ports": ["3000:3000"], - "volumes": ["/var/lib/archipelago/grafana:/var/lib/grafana"], - "env": ["GF_PATHS_DATA=/var/lib/grafana", "GF_USERS_ALLOW_SIGN_UP=false"] + "ports": [ + "3000:3000" + ], + "volumes": [ + "/var/lib/archipelago/grafana:/var/lib/grafana" + ], + "env": [ + "GF_PATHS_DATA=/var/lib/grafana", + "GF_USERS_ALLOW_SIGN_UP=false" + ] } }, { @@ -286,10 +426,42 @@ "dockerImage": "146.59.87.168:3000/lfg2025/tailscale:stable", "repoUrl": "https://github.com/tailscale/tailscale", "containerConfig": { - "ports": ["8240:8240"], - "volumes": ["/var/lib/archipelago/tailscale:/var/lib/tailscale"], - "env": ["TS_STATE_DIR=/var/lib/tailscale"], - "args": ["sh", "-c", "tailscaled --tun=userspace-networking & sleep 2; tailscale web --listen 0.0.0.0:8240 & wait"] + "ports": [ + "8240:8240" + ], + "volumes": [ + "/var/lib/archipelago/tailscale:/var/lib/tailscale" + ], + "env": [ + "TS_STATE_DIR=/var/lib/tailscale" + ], + "args": [ + "sh", + "-c", + "tailscaled --tun=userspace-networking & for i in $(seq 1 30); do [ -S /var/run/tailscale/tailscaled.sock ] && break; sleep 1; done; tailscale web --listen 0.0.0.0:8240 & wait" + ] + } + }, + { + "id": "portainer", + "title": "Portainer", + "version": "2.19.4", + "description": "Container management web UI for the local Podman socket.", + "icon": "/assets/img/app-icons/portainer.webp", + "author": "Portainer", + "category": "development", + "tier": "optional", + "dockerImage": "146.59.87.168:3000/lfg2025/portainer:latest", + "repoUrl": "https://github.com/portainer/portainer", + "containerConfig": { + "ports": [ + "9000:9000" + ], + "volumes": [ + "/var/lib/archipelago/portainer:/data", + "/run/user/1000/podman/podman.sock:/var/run/docker.sock" + ], + "notes": "Uses the manifest-owned Podman socket bind mount preparation path." } }, { @@ -304,8 +476,14 @@ "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"], + "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." } }, @@ -321,10 +499,20 @@ "dockerImage": "146.59.87.168:3000/lfg2025/uptime-kuma:1", "repoUrl": "https://github.com/louislam/uptime-kuma", "containerConfig": { - "ports": ["3002:3001"], - "volumes": ["/var/lib/archipelago/uptime-kuma:/app/data"], - "env": ["TZ=UTC"], - "args": ["--", "node", "server/server.js"] + "ports": [ + "3002:3001" + ], + "volumes": [ + "/var/lib/archipelago/uptime-kuma:/app/data" + ], + "env": [ + "TZ=UTC" + ], + "args": [ + "--", + "node", + "server/server.js" + ] } }, { @@ -338,24 +526,35 @@ "dockerImage": "146.59.87.168:3000/lfg2025/photoprism:240915", "repoUrl": "https://github.com/photoprism/photoprism", "containerConfig": { - "ports": ["2342:2342"], - "volumes": ["/var/lib/archipelago/photoprism:/photoprism/storage"], - "env": ["PHOTOPRISM_ADMIN_PASSWORD=archipelago", "PHOTOPRISM_DEFAULT_LOCALE=en"] + "ports": [ + "2342:2342" + ], + "volumes": [ + "/var/lib/archipelago/photoprism:/photoprism/storage" + ], + "env": [ + "PHOTOPRISM_ADMIN_PASSWORD=archipelago", + "PHOTOPRISM_DEFAULT_LOCALE=en" + ] } }, { "id": "nextcloud", "title": "Nextcloud", - "version": "28", + "version": "29", "description": "Your own private cloud. File sync, calendars, contacts.", "icon": "/assets/img/app-icons/nextcloud.webp", "author": "Nextcloud", "category": "data", - "dockerImage": "146.59.87.168:3000/lfg2025/nextcloud:28", + "dockerImage": "146.59.87.168:3000/lfg2025/nextcloud:29", "repoUrl": "https://github.com/nextcloud/server", "containerConfig": { - "ports": ["8085:80"], - "volumes": ["/var/lib/archipelago/nextcloud:/var/www/html"] + "ports": [ + "8085:80" + ], + "volumes": [ + "/var/lib/archipelago/nextcloud:/var/www/html" + ] } } ] diff --git a/neode-ui/src/views/appSession/generatedAppSessionConfig.ts b/neode-ui/src/views/appSession/generatedAppSessionConfig.ts new file mode 100644 index 00000000..02c59a06 --- /dev/null +++ b/neode-ui/src/views/appSession/generatedAppSessionConfig.ts @@ -0,0 +1,90 @@ +/** Generated by scripts/generate-app-catalog.py. Do not edit manually. */ + +export const GENERATED_APP_PORTS: Record = { + "aiui": 5180, + "archy-mempool-web": 4080, + "archy-nbxplorer": 32838, + "botfights": 9100, + "btcpay-server": 23000, + "did-wallet": 8083, + "electrumx": 50001, + "fedimint": 8175, + "filebrowser": 8083, + "gitea": 3001, + "grafana": 3000, + "homeassistant": 8123, + "indeedhub": 7778, + "jellyfin": 8096, + "lnd-ui": 18083, + "mempool": 4080, + "mempool-api": 8999, + "meshtastic": 4403, + "morphos-server": 8086, + "nextcloud": 8085, + "nostr-rs-relay": 18081, + "onlyoffice": 8088, + "photoprism": 2342, + "portainer": 9000, + "router": 8084, + "searxng": 8888, + "strfry": 8082, + "uptime-kuma": 3002, + "vaultwarden": 8082, + "web5-dwn": 3000, +} + +export const GENERATED_APP_TITLES: Record = { + "aiui": "AI Assistant", + "archy-btcpay-db": "BTCPay Postgres", + "archy-mempool-db": "Mempool MariaDB", + "archy-mempool-web": "Mempool Web", + "archy-nbxplorer": "NBXplorer", + "bitcoin-core": "Bitcoin Core", + "bitcoin-knots": "Bitcoin Knots", + "bitcoin-ui": "Bitcoin UI", + "botfights": "BotFights", + "btcpay-server": "BTCPay Server", + "core-lightning": "Core Lightning (CLN)", + "did-wallet": "Web5 DID Wallet", + "electrs-ui": "Electrs UI", + "electrumx": "ElectrumX", + "fedimint": "Fedimint", + "fedimint-gateway": "Fedimint Gateway", + "filebrowser": "File Browser", + "gitea": "Gitea", + "grafana": "Grafana", + "homeassistant": "Home Assistant", + "indeedhub": "IndeeHub", + "jellyfin": "Jellyfin", + "lightning-stack": "Lightning Stack", + "lnd": "LND", + "lnd-ui": "LND UI", + "mempool": "Mempool Explorer", + "mempool-api": "Mempool API", + "meshtastic": "Meshtastic", + "morphos-server": "MorphOS Server", + "nextcloud": "Nextcloud", + "nostr-rs-relay": "Nostr Relay (Rust)", + "onlyoffice": "OnlyOffice", + "photoprism": "PhotoPrism", + "portainer": "Portainer", + "router": "Mesh Router", + "searxng": "SearXNG", + "strfry": "Strfry Nostr Relay", + "uptime-kuma": "Uptime Kuma", + "vaultwarden": "Vaultwarden", + "web5-dwn": "Decentralized Web Node", +} + +export const GENERATED_NEW_TAB_APPS = new Set([ + "btcpay-server", + "gitea", + "grafana", + "homeassistant", + "nextcloud", + "onlyoffice", + "photoprism", + "portainer", + "uptime-kuma", + "vaultwarden", +]) diff --git a/scripts/check-app-catalog-drift.py b/scripts/check-app-catalog-drift.py new file mode 100644 index 00000000..c7219d80 --- /dev/null +++ b/scripts/check-app-catalog-drift.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python3 +"""Report drift between app-catalog/catalog.json and apps/*/manifest.yml.""" + +from __future__ import annotations + +import argparse +import json +import sys +from pathlib import Path +from typing import Any + +import yaml + +INTERNAL_MANIFEST_IDS = { + "aiui", + "archy-btcpay-db", + "archy-mempool-db", + "archy-mempool-web", + "archy-nbxplorer", + "bitcoin-ui", + "core-lightning", + "did-wallet", + "electrs-ui", + "lightning-stack", + "lnd-ui", + "mempool-api", + "morphos-server", + "onlyoffice", + "router", + "strfry", + "web5-dwn", +} + +LEGACY_STACK_CATALOG_IDS = { + "immich", + "netbird", + "saleor", + "tailscale", +} + + +def load_catalog(path: Path) -> dict[str, dict[str, Any]]: + with path.open("r", encoding="utf-8") as fh: + data = json.load(fh) + apps = data.get("apps", []) + if not isinstance(apps, list): + raise ValueError(f"{path}: expected .apps to be a list") + return {str(app.get("id", "")): app for app in apps if isinstance(app, dict) and app.get("id")} + + +def load_manifests(apps_dir: Path) -> dict[str, dict[str, Any]]: + manifests: dict[str, dict[str, Any]] = {} + for path in sorted(apps_dir.glob("*/manifest.yml")): + with path.open("r", encoding="utf-8") as fh: + data = yaml.safe_load(fh) + if not isinstance(data, dict) or not isinstance(data.get("app"), dict): + continue + app = data["app"] + app_id = app.get("id") + if app_id: + manifests[str(app_id)] = {"path": str(path), "app": app} + return manifests + + +def metadata(app: dict[str, Any]) -> dict[str, Any]: + value = app.get("metadata") + return value if isinstance(value, dict) else {} + + +def manifest_value(app: dict[str, Any], field: str) -> Any: + meta = metadata(app) + container = app.get("container") if isinstance(app.get("container"), dict) else {} + match field: + case "title": + return app.get("name") + case "version": + return str(app.get("version", "")) + case "description": + return app.get("description") + case "dockerImage": + return container.get("image") + case "category": + return app.get("category") or meta.get("category") + case "tier": + return meta.get("tier") + case "icon": + return meta.get("icon") + case "repoUrl": + return meta.get("repo") or meta.get("repoUrl") + case _: + return None + + +def normalize(value: Any) -> str: + if value is None: + return "" + return str(value).strip() + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--catalog", default="app-catalog/catalog.json") + parser.add_argument("--apps-dir", default="apps") + parser.add_argument( + "--strict", + action="store_true", + help="exit non-zero when missing entries or metadata drift are found", + ) + parser.add_argument( + "--release", + action="store_true", + help="suppress known internal/legacy-stack entries so output is release-actionable", + ) + args = parser.parse_args() + + catalog = load_catalog(Path(args.catalog)) + manifests = load_manifests(Path(args.apps_dir)) + + catalog_ids = set(catalog) + manifest_ids = set(manifests) + missing_manifests = sorted(catalog_ids - manifest_ids) + missing_catalog = sorted(manifest_ids - catalog_ids) + if args.release: + missing_manifests = [app_id for app_id in missing_manifests if app_id not in LEGACY_STACK_CATALOG_IDS] + missing_catalog = [app_id for app_id in missing_catalog if app_id not in INTERNAL_MANIFEST_IDS] + + compared_fields = [ + "title", + "version", + "description", + "dockerImage", + "category", + "tier", + "icon", + "repoUrl", + ] + drift: list[str] = [] + for app_id in sorted(catalog_ids & manifest_ids): + catalog_app = catalog[app_id] + manifest_app = manifests[app_id]["app"] + for field in compared_fields: + catalog_val = normalize(catalog_app.get(field)) + manifest_val = normalize(manifest_value(manifest_app, field)) + if catalog_val and manifest_val and catalog_val != manifest_val: + drift.append(f"{app_id}: {field}: catalog={catalog_val!r} manifest={manifest_val!r}") + + print( + json.dumps( + { + "catalog_apps": len(catalog), + "manifest_apps": len(manifests), + "missing_manifests": len(missing_manifests), + "missing_catalog": len(missing_catalog), + "metadata_drift": len(drift), + }, + sort_keys=True, + ) + ) + + for app_id in missing_manifests: + print(f"MISSING_MANIFEST {app_id}") + for app_id in missing_catalog: + print(f"MISSING_CATALOG {app_id}") + for item in drift: + print(f"DRIFT {item}") + + if args.strict and (missing_manifests or missing_catalog or drift): + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/generate-app-catalog.py b/scripts/generate-app-catalog.py new file mode 100644 index 00000000..9d46f3ab --- /dev/null +++ b/scripts/generate-app-catalog.py @@ -0,0 +1,205 @@ +#!/usr/bin/env python3 +"""Sync public app catalog metadata from apps/*/manifest.yml. + +Manifests are the source of truth for fields the runtime already needs +(`name`, `version`, `description`, container image, category, tier, icon, +repo URL). The catalog still owns presentation-only fields that manifests do +not carry yet, such as `author`, `requires`, `featured`, and rich +`containerConfig` notes. +""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path +from typing import Any + +import yaml + + +SYNC_FIELDS = ("title", "version", "description", "dockerImage", "category", "tier", "icon", "repoUrl") + + +def load_manifests(apps_dir: Path) -> dict[str, dict[str, Any]]: + manifests: dict[str, dict[str, Any]] = {} + for path in sorted(apps_dir.glob("*/manifest.yml")): + with path.open("r", encoding="utf-8") as fh: + data = yaml.safe_load(fh) + if not isinstance(data, dict) or not isinstance(data.get("app"), dict): + continue + app = data["app"] + app_id = app.get("id") + if app_id: + manifests[str(app_id)] = app + return manifests + + +def metadata(app: dict[str, Any]) -> dict[str, Any]: + value = app.get("metadata") + return value if isinstance(value, dict) else {} + + +def manifest_catalog_values(app: dict[str, Any]) -> dict[str, str]: + meta = metadata(app) + container = app.get("container") if isinstance(app.get("container"), dict) else {} + values = { + "title": app.get("name"), + "version": app.get("version"), + "description": app.get("description"), + "dockerImage": container.get("image"), + "category": app.get("category") or meta.get("category"), + "tier": meta.get("tier"), + "icon": meta.get("icon"), + "repoUrl": meta.get("repo") or meta.get("repoUrl") or meta.get("source"), + } + return {key: str(value) for key, value in values.items() if value is not None and str(value).strip()} + + +def manifest_launch_port(app: dict[str, Any]) -> int | None: + """Return the manifest-owned public UI port, when it is unambiguous.""" + interfaces = app.get("interfaces") + if isinstance(interfaces, dict): + main = interfaces.get("main") + if isinstance(main, dict) and main.get("type") == "ui": + port = main.get("port") + if isinstance(port, int): + return port + if isinstance(port, str) and port.isdigit(): + return int(port) + + ports = app.get("ports") + if not isinstance(ports, list): + return None + tcp_ports = [ + item.get("host") + for item in ports + if isinstance(item, dict) and str(item.get("protocol", "tcp")).lower() == "tcp" + ] + if len(tcp_ports) != 1: + return None + port = tcp_ports[0] + if isinstance(port, int): + return port + if isinstance(port, str) and port.isdigit(): + return int(port) + return None + + +def manifest_opens_in_new_tab(app: dict[str, Any]) -> bool: + """Return whether manifest launch metadata opts the app out of iframe launch.""" + launch = metadata(app).get("launch") + if not isinstance(launch, dict): + return False + return launch.get("open_in_new_tab") is True + + +def ts_string(value: str) -> str: + return json.dumps(value, ensure_ascii=True) + + +def render_app_session_config(manifests: dict[str, dict[str, Any]]) -> str: + ports: dict[str, int] = {} + titles: dict[str, str] = {} + new_tab_apps: list[str] = [] + for app_id, app in sorted(manifests.items()): + name = app.get("name") + if isinstance(name, str) and name.strip(): + titles[app_id] = name.strip() + port = manifest_launch_port(app) + if port: + ports[app_id] = port + if manifest_opens_in_new_tab(app): + new_tab_apps.append(app_id) + + lines = [ + "/** Generated by scripts/generate-app-catalog.py. Do not edit manually. */", + "", + "export const GENERATED_APP_PORTS: Record = {", + ] + for app_id, port in ports.items(): + lines.append(f" {ts_string(app_id)}: {port},") + lines.extend([ + "}", + "", + "export const GENERATED_APP_TITLES: Record = {", + ]) + for app_id, title in titles.items(): + lines.append(f" {ts_string(app_id)}: {ts_string(title)},") + lines.extend([ + "}", + "", + "export const GENERATED_NEW_TAB_APPS = new Set([", + ]) + for app_id in new_tab_apps: + lines.append(f" {ts_string(app_id)},") + lines.extend(["])", ""]) + return "\n".join(lines) + + +def sync_catalog(path: Path, manifests: dict[str, dict[str, Any]]) -> int: + with path.open("r", encoding="utf-8") as fh: + catalog = json.load(fh) + apps = catalog.get("apps") + if not isinstance(apps, list): + raise ValueError(f"{path}: expected .apps to be a list") + + changed = 0 + for catalog_app in apps: + if not isinstance(catalog_app, dict): + continue + app_id = catalog_app.get("id") + if not app_id or str(app_id) not in manifests: + continue + values = manifest_catalog_values(manifests[str(app_id)]) + for field in SYNC_FIELDS: + if field not in values: + continue + old = catalog_app.get(field) + new = values[field] + if old != new: + catalog_app[field] = new + changed += 1 + + path.write_text(json.dumps(catalog, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") + return changed + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--apps-dir", default="apps") + parser.add_argument( + "--catalog", + action="append", + default=[], + help="Catalog JSON path to update. May be passed multiple times.", + ) + parser.add_argument( + "--app-session-config", + default="neode-ui/src/views/appSession/generatedAppSessionConfig.ts", + help="Generated TypeScript app-session metadata path. Pass an empty string to skip.", + ) + args = parser.parse_args() + + catalogs = args.catalog or ["app-catalog/catalog.json", "neode-ui/public/catalog.json"] + manifests = load_manifests(Path(args.apps_dir)) + total = 0 + for catalog in catalogs: + changed = sync_catalog(Path(catalog), manifests) + total += changed + print(f"{catalog}: updated {changed} fields") + if args.app_session_config: + path = Path(args.app_session_config) + content = render_app_session_config(manifests) + old = path.read_text(encoding="utf-8") if path.exists() else "" + if old != content: + path.write_text(content, encoding="utf-8") + print(f"{path}: updated") + else: + print(f"{path}: updated 0 fields") + print(f"total_updated={total}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())