Enhance ISO build process and documentation for Archipelago

- Updated BUILD-GUIDE.md to clarify instructions for building the Archipelago Auto-Installer ISO, emphasizing the recommended method of building directly on the target server.
- Added auto-installation of missing dependencies (xorriso, podman) when running the build script with sudo.
- Enhanced the build-auto-installer-iso.sh script to capture container images from the live server, ensuring the ISO includes the same set of applications as the dev server.
- Revised deployment documentation to stress the importance of building the Rust backend on the Linux dev server and included new instructions for capturing system-level changes for ISO builds.
- Improved UI components and added new bundled applications (BTCPay Server, Mempool Explorer, Nostr Relay, Strfry Relay, Tailscale) to enhance user experience.
This commit is contained in:
Dorian 2026-02-14 16:44:20 +00:00
parent d988396111
commit 6035c93289
19 changed files with 824 additions and 406 deletions

View File

@ -38,6 +38,7 @@ This is where ALL development happens:
1. **Snapshot the dev server** (192.168.1.228):
- Capture current backend binary (`/usr/local/bin/archipelago`)
- Capture current frontend files (`/opt/archipelago/web-ui`)
- When `DEV_SERVER` is set: capture container images from the live server so the ISO prepackages current apps
- Capture system configs (Nginx, systemd, etc.)
- Capture app manifests and configs
@ -120,6 +121,12 @@ Creates a live system ISO (boots into a live environment, doesn't install).
## Deployment to Dev Server
### Dev server access
- **Host:** `archipelago@192.168.1.228`
- **Password:** `archipelago` — use this for deployment. For non-interactive sync/deploy from scripts or the agent, use: `sshpass -p "archipelago"` (e.g. `sshpass -p "archipelago" rsync ...` or prepend it to ssh/rsync when running `./scripts/deploy-to-target.sh` or equivalent).
- **Build approach:** We build **directly on the server** by SSHing in and running `cargo build --release` there. Do not build the backend on macOS and copy the binary.
### ⚠️ CRITICAL: Backend Compilation Architecture
**NEVER compile the Rust backend on macOS and deploy to Linux!**
@ -133,23 +140,20 @@ The dev server (`192.168.1.228`) is **x86_64 Linux (Debian 12)**. Binaries compi
### Deployment Procedures
1. **Backend** (MUST build on Linux):
1. **Backend** (MUST build on Linux — use rsync then build on server):
```bash
# Option 1: SSH and build directly on server
ssh archipelago@192.168.1.228
cd ~/archy/core/archipelago
source ~/.cargo/env # Load Rust environment
cargo build --release --bin archipelago
sudo systemctl stop archipelago
sudo cp ../target/release/archipelago /usr/local/bin/
sudo systemctl start archipelago
# Option 2: Update source and build remotely
# From local machine:
scp core/archipelago/src/**/*.rs archipelago@192.168.1.228:~/archy/core/archipelago/src/
ssh archipelago@192.168.1.228 'source ~/.cargo/env && cd ~/archy/core/archipelago && cargo build --release'
ssh archipelago@192.168.1.228 'sudo systemctl stop archipelago && sudo cp ~/archy/core/target/release/archipelago /usr/local/bin/ && sudo systemctl start archipelago'
# From project root. Sync source to server (exclude local target/.git).
sshpass -p "archipelago" rsync -avz --exclude target --exclude .git -e "ssh -o StrictHostKeyChecking=no" \
core/ archipelago@192.168.1.228:~/archy/core/
# Build on server and deploy binary
sshpass -p "archipelago" ssh -o StrictHostKeyChecking=no archipelago@192.168.1.228 \
'source ~/.cargo/env && cd ~/archy/core/archipelago && cargo build --release && \
sudo systemctl stop archipelago && \
sudo cp ~/archy/core/target/release/archipelago /usr/local/bin/ && \
sudo systemctl start archipelago'
```
**Do not** build the binary on macOS and copy it; always rsync source and build on the server.
2. **Frontend** (can build locally):
```bash
@ -175,16 +179,17 @@ The dev server (`192.168.1.228`) is **x86_64 Linux (Debian 12)**. Binaries compi
### 🚨 NEVER VIOLATE THESE
1. **ALWAYS deploy to the live development server (192.168.1.228)** for testing
2. **🔴 NEVER EVER compile the Rust backend on macOS and deploy to Linux**
2. **After every change: sync and build on the live server.** When you finish implementing a feature or fix, run the deploy script so the live server has the latest code. Command: `./scripts/deploy-to-target.sh --live` (from project root). If SSH is not available in the current environment, tell the user to run it locally. Do not skip this step. **App UIs** (e.g. `docker/lnd-ui/`, `docker/bitcoin-ui/`) are served by their own containers; the deploy script rebuilds the LND UI image and restarts its container so changes to the LND UI are visible after deploy.
3. **🔴 NEVER EVER compile the Rust backend on macOS and deploy to Linux**
- Dev server is `x86_64 Linux (Debian 12)`
- Always build backend **ON the Linux server** using `source ~/.cargo/env && cargo build --release`
- macOS binaries will cause "Exec format error" and break the system
- Frontend (Vue.js) CAN be built on macOS - it's just HTML/CSS/JS
3. **The ISO must capture the CURRENT STATE of the dev server**, not build from source
4. **Frontend build output is in `web/dist/neode-ui/`**, NOT `neode-ui/dist/`
5. **Nginx serves on port 80** and proxies backend on `localhost:5678`
6. **App icons are in `neode-ui/public/assets/img/app-icons/`**
7. **The auto-installer ISO is the ONLY way to deploy** - no live systems
4. **The ISO must capture the CURRENT STATE of the dev server**, not build from source
5. **Frontend build output is in `web/dist/neode-ui/`**, NOT `neode-ui/dist/`
6. **Nginx serves on port 80** and proxies backend on `localhost:5678`
7. **App icons are in `neode-ui/public/assets/img/app-icons/`**
8. **The auto-installer ISO is the ONLY way to deploy** - no live systems
## Testing Checklist

View File

@ -9,6 +9,10 @@ alwaysApply: true
**Always deploy to live system for testing** - The target device (192.168.1.228) is a development machine, so deploy changes directly to the live system rather than using dev servers.
### Backend: build on server via rsync (never on macOS)
- **Always** deploy backend by: (1) rsync `core/` to `archipelago@192.168.1.228:~/archy/core/`, then (2) SSH and run `cargo build --release` on the server, then copy binary to `/usr/local/bin/` and restart `archipelago.service`.
- Use `sshpass -p "archipelago"` for non-interactive rsync/SSH. **Action these builds** when making backend changes; do not build the Rust binary on macOS and copy it (causes Exec format error on Linux).
### Standard Deployment Command
```bash
@ -82,6 +86,35 @@ Common containers:
**CRITICAL**: After testing on the live server, always update the ISO build to include your changes.
### Building the ISO
**Recommended**: Build on the target server (has all dependencies):
```bash
# SSH to target server
ssh archipelago@192.168.1.228
# Navigate to project
cd ~/archy/image-recipe
# Run build with sudo (auto-installs missing deps like xorriso)
sudo ./build-auto-installer-iso.sh
# The ISO will be at: results/archipelago-auto-installer-*.iso
# Copy back to Mac
# On your Mac:
scp archipelago@192.168.1.228:~/archy/image-recipe/results/archipelago-auto-installer-*.iso .
```
**Alternative**: Build from Mac (requires Docker Desktop installed).
### Common ISO Build Issues
- **Missing xorriso**: Run with `sudo` to auto-install, or: `sudo apt install -y xorriso`
- **Missing podman**: Run with `sudo` to auto-install, or: `sudo apt install -y podman`
- **No Docker on Mac**: Either install Docker Desktop or build on target server (recommended)
### System Configuration Files to Sync
When you make system-level changes on the live server, capture them for the ISO build:

View File

@ -439,6 +439,13 @@
## Development Workflow
### Backend: always build on the dev server (never on macOS)
- **CRITICAL**: The Rust backend **must** be built **on the Linux dev server**, not on macOS. Deploy by **rsync then build**:
1. **Rsync** source to server: `sshpass -p "archipelago" rsync -avz --exclude target --exclude .git -e "ssh -o StrictHostKeyChecking=no" core/ archipelago@192.168.1.228:~/archy/core/`
2. **Build and deploy on server**: `sshpass -p "archipelago" ssh -o StrictHostKeyChecking=no archipelago@192.168.1.228 'source ~/.cargo/env && cd ~/archy/core/archipelago && cargo build --release && sudo systemctl stop archipelago && sudo cp ~/archy/core/target/release/archipelago /usr/local/bin/ && sudo systemctl start archipelago'`
- When making backend changes, **action the build**: run the rsync + SSH build/deploy steps above. Do not build the binary locally and copy it (causes Exec format error on Linux).
- Dev server: `archipelago@192.168.1.228`, password: `archipelago`.
### Scripts & Automation
- ✅ All scripts in `scripts/` directory
- ✅ Use `#!/usr/bin/env bash` for portability
@ -507,21 +514,22 @@
15. **Depend on external registries** - Host our own or use Docker Hub
### ✅ ALWAYS DO:
1. **Use workspace-relative paths** - Portable code
2. **Create global Tailwind classes** - Consistent styling
3. **Build Archipelago-native solutions** - Clean architecture
4. **Include security in all containers** - Security first
5. **Use environment variables** - Configurable deployments
6. **Add modules to Cargo.toml** - Workspace coherence
7. **Create reusable components** - DRY principle
8. **Use Docker (dev) or Podman (prod)** - Standard containers
9. **Handle all errors gracefully** - User-friendly messages
10. **Follow the architecture plan** - Consistency
11. **Write tests** - Prevent regressions
12. **Document code** - Help future contributors
13. **Review your own code** - Catch issues early
14. **Run CI checks locally** - Before pushing
15. **Think production first** - Build it right
1. **Build backend on the dev server** - Rsync `core/` to `archipelago@192.168.1.228:~/archy/core/`, then SSH in and run `cargo build --release` and deploy the binary. Never build the Rust binary on macOS for deployment.
2. **Use workspace-relative paths** - Portable code
3. **Create global Tailwind classes** - Consistent styling
4. **Build Archipelago-native solutions** - Clean architecture
5. **Include security in all containers** - Security first
6. **Use environment variables** - Configurable deployments
7. **Add modules to Cargo.toml** - Workspace coherence
8. **Create reusable components** - DRY principle
9. **Use Docker (dev) or Podman (prod)** - Standard containers
10. **Handle all errors gracefully** - User-friendly messages
11. **Follow the architecture plan** - Consistency
12. **Write tests** - Prevent regressions
13. **Document code** - Help future contributors
14. **Review your own code** - Catch issues early
15. **Run CI checks locally** - Before pushing
16. **Think production first** - Build it right
## Architecture Adherence

View File

@ -4,11 +4,34 @@
Make sure you have:
- Docker or Podman installed
- `xorriso` installed
- `xorriso` installed (for ISO creation)
- Access to dev server: archipelago@192.168.1.228
**Note**: When building on the target server with `sudo`, the script will automatically install missing dependencies (`xorriso`, `podman`).
## Build Auto-Installer ISO
### Option 1: Build on Target Server (Recommended)
```bash
# SSH to target server
ssh archipelago@192.168.1.228
# Navigate to project
cd ~/archy/image-recipe
# Run build (auto-installs missing deps)
sudo ./build-auto-installer-iso.sh
# Copy ISO back to your Mac
# On your Mac:
scp archipelago@192.168.1.228:~/archy/image-recipe/results/archipelago-auto-installer-*.iso .
```
### Option 2: Build from Mac (requires Docker)
**Important**: This requires Docker Desktop installed on macOS.
```bash
cd /Users/dorian/Projects/archy/image-recipe
@ -23,6 +46,7 @@ DEV_SERVER=archipelago@192.168.1.228 ./build-auto-installer-iso.sh
✅ Complete Debian 12 root filesystem
✅ Pre-built Archipelago backend
✅ Pre-built frontend (web UI)
**Prepackaged container images** (Bitcoin Knots, LND, UIs, and other bundled apps), loaded on first boot
✅ Nginx configuration (HTTPS ready)
✅ Auto-installer that:
- Detects internal disk
@ -33,21 +57,9 @@ DEV_SERVER=archipelago@192.168.1.228 ./build-auto-installer-iso.sh
## What Users Need to Do Post-Install
1. **Deploy Containers** - The ISO doesn't include containers (too large)
Example - Bitcoin Knots:
```bash
sudo podman run -d --name bitcoin-knots \
-p 8332:8332 -p 8333:8333 \
-v /var/lib/archipelago/bitcoin:/home/bitcoin/.bitcoin \
--label "com.archipelago.app=bitcoin-knots" \
--label "com.archipelago.title=Bitcoin Knots" \
docker.io/bitcoinknots/bitcoin:latest \
-server=1 -txindex=1 -rpcallowip=0.0.0.0/0 \
-rpcbind=0.0.0.0:8332 -dbcache=4096
```
1. **Start apps from the Web UI** Container images are prepackaged and loaded on first boot. Bitcoin Knots + UI, LND + UI, and other bundled apps are ready to start from the Web UI without manual `podman run`. No need to pull or deploy core containers.
2. **Access Web UI** - Navigate to `http://[server-ip]`
2. **Access Web UI** Navigate to `http://[server-ip]`
## Testing the ISO

View File

@ -21,6 +21,12 @@
# ./build-iso-complete.sh --remote archipelago@192.168.1.228
# ./build-iso-complete.sh --local --clean
#
# Auto-installer from live server: when using --remote HOST, the ISO script
# (build-auto-installer-iso.sh) is run with DEV_SERVER=HOST so it captures
# backend, frontend, and container images from that host. Alternatively run
# DEV_SERVER=archipelago@192.168.1.228 ./image-recipe/build-auto-installer-iso.sh
# to build the auto-installer ISO with live capture only (no backend/frontend build).
#
set -e # Exit on error
@ -32,7 +38,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BUILD_DIR="$SCRIPT_DIR/image-recipe/build"
BACKEND_SRC="$SCRIPT_DIR/core/archipelago"
FRONTEND_SRC="$SCRIPT_DIR/neode-ui"
ISO_SCRIPT="$SCRIPT_DIR/image-recipe/build-debian-iso.sh"
ISO_SCRIPT="$SCRIPT_DIR/image-recipe/build-auto-installer-iso.sh"
# Colors for output
RED='\033[0;31m'
@ -315,8 +321,8 @@ if [[ "$BUILD_MODE" == "remote" ]]; then
print_success "Files synced to remote"
print_info "Running ISO build on remote server..."
ssh -t "$REMOTE_HOST" "cd ~/archy/image-recipe && sudo bash build-debian-iso.sh" || {
print_info "Running ISO build on remote server (auto-installer, DEV_SERVER=$REMOTE_HOST)..."
ssh -t "$REMOTE_HOST" "cd ~/archy/image-recipe && DEV_SERVER=$REMOTE_HOST sudo -E bash build-auto-installer-iso.sh" || {
print_error "ISO build failed on remote server"
exit 1
}
@ -324,7 +330,7 @@ if [[ "$BUILD_MODE" == "remote" ]]; then
print_success "ISO built on remote server"
# Copy ISO back to local
ISO_NAME="archipelago-debian-12-x86_64.iso"
ISO_NAME="archipelago-installer-x86_64.iso"
print_info "Copying ISO back to local machine..."
mkdir -p "$SCRIPT_DIR/image-recipe/results"
scp "$REMOTE_HOST:~/archy/image-recipe/results/$ISO_NAME" "$SCRIPT_DIR/image-recipe/results/"
@ -333,11 +339,11 @@ if [[ "$BUILD_MODE" == "remote" ]]; then
else
# Local build
print_info "Running ISO build locally..."
print_info "Running ISO build locally (auto-installer)..."
cd "$SCRIPT_DIR/image-recipe"
sudo bash build-debian-iso.sh
sudo bash build-auto-installer-iso.sh
ISO_PATH="$SCRIPT_DIR/image-recipe/results/archipelago-debian-12-x86_64.iso"
ISO_PATH="$SCRIPT_DIR/image-recipe/results/archipelago-installer-x86_64.iso"
fi
# =============================================================================

1
core/Cargo.lock generated
View File

@ -42,6 +42,7 @@ dependencies = [
"hyper 0.14.32",
"hyper-util",
"hyper-ws-listener",
"reqwest",
"serde",
"serde_json",
"serde_yaml",

View File

@ -47,5 +47,8 @@ uuid = { version = "1.0", features = ["v4"] }
toml = "0.8"
serde_yaml = "0.9"
# HTTP client (for LND REST proxy)
reqwest = { version = "0.11", features = ["json"] }
[dev-dependencies]
tokio-test = "0.4"

View File

@ -10,6 +10,8 @@ use tokio::sync::broadcast;
use tokio_tungstenite::tungstenite::Message;
use tracing::{debug, info};
const CORS_ANY: &str = "*";
pub struct ApiHandler {
_config: Config,
rpc_handler: Arc<RpcHandler>,
@ -53,6 +55,12 @@ impl ApiHandler {
.status(StatusCode::OK)
.body(hyper::Body::from("OK"))
.unwrap()),
(Method::GET, path) if path.starts_with("/api/container/logs") => {
Self::handle_container_logs_http(self.rpc_handler.clone(), path).await
}
(Method::GET, path) if path.starts_with("/proxy/lnd/") => {
Self::handle_lnd_proxy(path).await
}
_ => Ok(Response::builder()
.status(StatusCode::NOT_FOUND)
.body(hyper::Body::from("Not Found"))
@ -60,6 +68,86 @@ impl ApiHandler {
}
}
async fn handle_container_logs_http(
rpc: Arc<RpcHandler>,
path: &str,
) -> Result<Response<hyper::Body>> {
let query = path
.strip_prefix("/api/container/logs")
.and_then(|s| s.strip_prefix('?'))
.unwrap_or("");
let params: std::collections::HashMap<String, String> =
query
.split('&')
.filter_map(|p| {
let mut it = p.splitn(2, '=');
let k = it.next()?.to_string();
let v = it.next()?.to_string();
Some((k, v))
})
.collect();
let app_id = params.get("app_id").map(|s| s.as_str()).unwrap_or("lnd");
let lines = params
.get("lines")
.and_then(|s| s.parse::<u32>().ok())
.unwrap_or(200);
match rpc.get_container_logs_value(app_id, lines).await {
Ok(value) => {
let body = serde_json::json!({ "result": value });
let body_bytes = serde_json::to_vec(&body).unwrap_or_default();
Ok(Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "application/json")
.header("Access-Control-Allow-Origin", CORS_ANY)
.body(hyper::Body::from(body_bytes))
.unwrap())
}
Err(e) => {
let body = serde_json::json!({ "error": e.to_string() });
let body_bytes = serde_json::to_vec(&body).unwrap_or_default();
Ok(Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.header("Content-Type", "application/json")
.header("Access-Control-Allow-Origin", CORS_ANY)
.body(hyper::Body::from(body_bytes))
.unwrap())
}
}
}
async fn handle_lnd_proxy(path: &str) -> Result<Response<hyper::Body>> {
let suffix = path.strip_prefix("/proxy/lnd").unwrap_or("/");
let url = format!("http://127.0.0.1:8080{}", suffix);
match reqwest::get(&url).await {
Ok(resp) => {
let status = resp.status().as_u16();
let headers = resp.headers().clone();
let body = resp.bytes().await.unwrap_or_default();
let mut builder = Response::builder().status(status);
if let Some(ct) = headers.get("content-type") {
if let Ok(s) = ct.to_str() {
builder = builder.header("Content-Type", s);
}
}
builder
.header("Access-Control-Allow-Origin", CORS_ANY)
.body(hyper::Body::from(body))
.map_err(|e| anyhow::anyhow!("response build: {}", e))
}
Err(e) => {
let body = serde_json::json!({ "error": e.to_string() });
let body_bytes = serde_json::to_vec(&body).unwrap_or_default();
Ok(Response::builder()
.status(StatusCode::BAD_GATEWAY)
.header("Content-Type", "application/json")
.header("Access-Control-Allow-Origin", CORS_ANY)
.body(hyper::Body::from(body_bytes))
.unwrap())
}
}
}
async fn handle_websocket(
req: Request<hyper::Body>,
state_manager: Arc<StateManager>,

View File

@ -399,6 +399,25 @@ impl RpcHandler {
Ok(serde_json::to_value(logs)?)
}
/// Used by HTTP GET /api/container/logs (same logic as container-logs RPC).
pub async fn get_container_logs_value(
&self,
app_id: &str,
lines: u32,
) -> Result<serde_json::Value> {
let orchestrator = self
.orchestrator
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Container orchestrator not available (dev mode required)"))?;
let logs = orchestrator
.get_container_logs(app_id, lines)
.await
.context("Failed to get container logs")?;
Ok(serde_json::to_value(logs)?)
}
async fn handle_container_health(
&self,
params: Option<serde_json::Value>,

View File

@ -225,6 +225,13 @@
.animate-ping {
animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
}
.modal-tab { transition: all 0.2s ease; }
.modal-tab.active { background: rgba(255,255,255,0.2); color: white; }
.modal-tab:not(.active) { color: rgba(255,255,255,0.6); }
.modal-tab:not(.active):hover { color: rgba(255,255,255,0.9); }
.tab-panel { display: none; }
.tab-panel.active { display: block; }
</style>
</head>
<body>
@ -234,295 +241,260 @@
<div class="overlay"></div>
<div class="container">
<!-- Header - Glass card with logo top left -->
<!-- Header - Glass card with logo and Settings button top-right -->
<div class="glass-card p-6 mb-6">
<div class="flex items-start gap-4">
<!-- Logo - Top Left -->
<div class="flex-shrink-0">
<div class="logo-gradient-border">
<img
src="/assets/img/app-icons/lnd.svg"
alt="LND"
class="w-16 h-16"
style="object-fit: contain; padding: 8px;"
/>
<div class="flex items-start justify-between gap-4">
<div class="flex items-start gap-4 flex-1 min-w-0">
<div class="flex-shrink-0">
<div class="logo-gradient-border">
<img
src="/assets/img/app-icons/lnd.svg"
alt="LND"
class="w-16 h-16"
style="object-fit: contain; padding: 8px;"
/>
</div>
</div>
<div class="flex-1 min-w-0">
<h1 class="text-3xl font-bold text-white mb-2">LND</h1>
<p class="text-white/70">Lightning Network Daemon for instant Bitcoin payments</p>
<p class="text-sm text-white/60 mt-2" id="headerNetwork"></p>
</div>
</div>
<!-- Title and Description -->
<div class="flex-1 min-w-0">
<h1 class="text-3xl font-bold text-white mb-2">LND</h1>
<p class="text-white/70">Lightning Network Daemon for instant Bitcoin payments</p>
<p class="text-sm text-white/60 mt-2">Regtest mode - Development environment</p>
</div>
<button
onclick="openSettings()"
class="flex-shrink-0 flex items-center gap-2 px-4 py-2.5 glass-button rounded-lg text-sm font-medium"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 2.31.903 2.31.903a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.903 2.31-.903 2.31a1.724 1.724 0 002.572 1.065c.426 1.756 2.924 1.756 3.35 0a1.724 1.724 0 002.573-1.066c1.543.94 2.31-.903 2.31-.903a1.724 1.724 0 001.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.903-2.31.903-2.31a1.724 1.724 0 00-2.572-1.065z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
Settings
</button>
</div>
</div>
<!-- Summary strip -->
<div class="glass-card p-6 mb-6">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div class="info-card flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="relative">
<div class="w-3 h-3 rounded-full bg-green-400"></div>
<div class="absolute inset-0 w-3 h-3 rounded-full bg-green-400 animate-ping opacity-75"></div>
<div class="w-3 h-3 rounded-full bg-green-400" id="statusDot"></div>
<div class="absolute inset-0 w-3 h-3 rounded-full bg-green-400 animate-ping opacity-75" id="statusPing" style="display:none"></div>
</div>
<div>
<p class="text-sm font-medium text-white">Node Status</p>
<p class="text-xs text-white/60">Running</p>
<p class="text-xs text-white/60" id="summaryNodeStatus"></p>
</div>
</div>
</div>
<div class="info-card flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="relative">
<span class="text-2xl text-orange-500 font-bold"></span>
</div>
<span class="text-2xl text-orange-500 font-bold">&#9889;</span>
<div>
<p class="text-sm font-medium text-white">Channels</p>
<p class="text-xs text-orange-500 font-medium" id="channelCount">0</p>
</div>
</div>
</div>
<div class="info-card flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="relative">
<div class="w-3 h-3 rounded-full bg-green-400"></div>
<div class="absolute inset-0 w-3 h-3 rounded-full bg-green-400 animate-ping opacity-75"></div>
<div class="w-3 h-3 rounded-full bg-green-400" id="restDot"></div>
</div>
<div>
<p class="text-sm font-medium text-white">REST API</p>
<p class="text-xs text-white/60">Active</p>
<p class="text-xs text-white/60" id="summaryRestStatus"></p>
</div>
</div>
<button
onclick="openSettings()"
class="px-3 py-1.5 glass-button rounded text-xs font-medium"
>
Settings
</button>
<button onclick="openSettings(); setSettingsTab('rest');" class="px-3 py-1.5 glass-button rounded text-xs font-medium">Settings</button>
</div>
<div class="info-card flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="relative">
<div class="w-3 h-3 rounded-full bg-green-400"></div>
<div class="absolute inset-0 w-3 h-3 rounded-full bg-green-400 animate-ping opacity-75"></div>
<div class="w-3 h-3 rounded-full bg-green-400" id="grpcDot"></div>
</div>
<div>
<p class="text-sm font-medium text-white">gRPC</p>
<p class="text-xs text-white/60">Connected</p>
<p class="text-xs text-white/60" id="summaryGrpcStatus"></p>
</div>
</div>
<button
onclick="openLogs()"
class="px-3 py-1.5 glass-button rounded text-xs font-medium"
>
Logs
</button>
<button onclick="openSettings(); setSettingsTab('logs');" class="px-3 py-1.5 glass-button rounded text-xs font-medium">Logs</button>
</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
<div class="glass-card p-6">
<div class="flex items-start gap-4 mb-4">
<div class="flex-shrink-0 w-12 h-12 rounded-lg bg-white/10 flex items-center justify-center">
<svg class="w-6 h-6 text-white/80" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</div>
<div class="flex-1">
<h2 class="text-xl font-semibold text-white mb-2">Node Status</h2>
<p class="text-white/70 text-sm mb-4">Lightning node information</p>
</div>
<!-- Main content: placeholder for wallet (balance, Receive, Send, recent activity) -->
<div class="glass-card p-6 mb-8">
<h2 class="text-xl font-semibold text-white mb-4">Wallet</h2>
<p class="text-white/60 text-sm mb-4">Balance, Receive, and Send will appear here when connected to your node.</p>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<div class="p-4 bg-white/5 rounded-lg">
<p class="text-xs text-white/50 uppercase tracking-wide mb-1">Spendable</p>
<p class="text-lg font-semibold text-white" id="balanceSpendable"></p>
</div>
<div class="space-y-3">
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<div class="flex items-center gap-3">
<svg class="w-5 h-5 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
<span class="text-white/80 text-sm">Node Status</span>
</div>
<span class="text-green-400 text-sm font-medium">Running</span>
</div>
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<div class="flex items-center gap-3">
<svg class="w-5 h-5 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7" />
</svg>
<span class="text-white/80 text-sm">Network</span>
</div>
<span class="text-white/60 text-sm">Regtest</span>
</div>
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<div class="flex items-center gap-3">
<svg class="w-5 h-5 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
<span class="text-white/80 text-sm">Version</span>
</div>
<span class="text-white/60 text-sm">0.17.4-beta</span>
</div>
<div class="p-4 bg-white/5 rounded-lg">
<p class="text-xs text-white/50 uppercase tracking-wide mb-1">Lightning</p>
<p class="text-lg font-semibold text-white" id="balanceLightning"></p>
</div>
<div class="p-4 bg-white/5 rounded-lg">
<p class="text-xs text-white/50 uppercase tracking-wide mb-1">Total</p>
<p class="text-lg font-semibold text-white" id="balanceTotal"></p>
</div>
<button class="mt-4 w-full info-card-button text-sm font-medium" onclick="openSettings()">
Node Settings
</button>
</div>
<div class="glass-card p-6">
<div class="flex items-start gap-4 mb-4">
<div class="flex-shrink-0 w-12 h-12 rounded-lg bg-white/10 flex items-center justify-center">
<svg class="w-6 h-6 text-white/80" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.141 0M1.394 9.393c5.857-5.857 15.355-5.857 21.213 0" />
</svg>
</div>
<div class="flex-1">
<h2 class="text-xl font-semibold text-white mb-2">REST API</h2>
<p class="text-white/70 text-sm mb-4">HTTP REST API access</p>
</div>
</div>
<div class="space-y-3">
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<div class="flex items-center gap-3">
<svg class="w-5 h-5 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
</svg>
<span class="text-white/80 text-sm">REST Endpoint</span>
</div>
<span class="text-white/60 text-sm font-mono">localhost:8080</span>
</div>
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<div class="flex items-center gap-3">
<svg class="w-5 h-5 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
<span class="text-white/80 text-sm">API Status</span>
</div>
<span class="text-green-400 text-sm font-medium">Active</span>
</div>
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<div class="flex items-center gap-3">
<svg class="w-5 h-5 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span class="text-white/80 text-sm">API Version</span>
</div>
<span class="text-white/60 text-sm">v1</span>
</div>
</div>
<button class="mt-4 w-full info-card-button text-sm font-medium" onclick="copyRESTInfo()">
Copy REST Info
</button>
</div>
<div class="glass-card p-6">
<div class="flex items-start gap-4 mb-4">
<div class="flex-shrink-0 w-12 h-12 rounded-lg bg-white/10 flex items-center justify-center">
<svg class="w-6 h-6 text-white/80" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.141 0M1.394 9.393c5.857-5.857 15.355-5.857 21.213 0" />
</svg>
</div>
<div class="flex-1">
<h2 class="text-xl font-semibold text-white mb-2">gRPC Connection</h2>
<p class="text-white/70 text-sm mb-4">High-performance gRPC API</p>
</div>
</div>
<div class="space-y-3">
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<div class="flex items-center gap-3">
<svg class="w-5 h-5 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
</svg>
<span class="text-white/80 text-sm">gRPC Host</span>
</div>
<span class="text-white/60 text-sm font-mono">localhost:10009</span>
</div>
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<div class="flex items-center gap-3">
<svg class="w-5 h-5 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
<span class="text-white/80 text-sm">gRPC Status</span>
</div>
<span class="text-green-400 text-sm font-medium">Connected</span>
</div>
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<div class="flex items-center gap-3">
<svg class="w-5 h-5 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01" />
</svg>
<span class="text-white/80 text-sm">P2P Port</span>
</div>
<span class="text-white/60 text-sm font-mono">9735</span>
</div>
</div>
<button class="mt-4 w-full info-card-button text-sm font-medium" onclick="openLogs()">
View Logs
</button>
<div class="flex gap-4">
<button class="px-6 py-3 glass-button rounded-lg font-medium text-white/90 hover:text-white transition-colors" disabled>Receive</button>
<button class="px-6 py-3 glass-button rounded-lg font-medium text-white/90 hover:text-white transition-colors" disabled>Send</button>
</div>
<p class="text-white/50 text-xs mt-4">Recent activity will be listed here.</p>
</div>
</div>
<!-- Tabbed Settings Modal -->
<div class="modal hidden fixed inset-0 bg-black/80 backdrop-blur-sm z-50 items-center justify-center p-4" id="settingsModal">
<div class="glass-card p-6 max-w-2xl w-full max-h-[80vh] overflow-y-auto">
<div class="flex justify-between items-center mb-4">
<h2 class="text-2xl font-bold text-white">Node Settings</h2>
<div class="glass-card p-6 max-w-2xl w-full max-h-[85vh] overflow-hidden flex flex-col">
<div class="flex justify-between items-center mb-4 flex-shrink-0">
<h2 class="text-2xl font-bold text-white">Settings</h2>
<button onclick="closeSettings()" class="glass-button px-3 py-2 rounded-lg text-xl font-medium">×</button>
</div>
<div class="space-y-3">
<div class="p-3 bg-white/5 rounded-lg">
<div class="font-semibold text-white mb-1">Network Mode</div>
<div class="text-white/70 text-sm">Regtest (Development)</div>
</div>
<div class="p-3 bg-white/5 rounded-lg">
<div class="font-semibold text-white mb-1">Bitcoin Backend</div>
<div class="text-white/70 text-sm">Connected to bitcoin:18443</div>
</div>
<div class="p-3 bg-white/5 rounded-lg">
<div class="font-semibold text-white mb-1">ZMQ Subscriptions</div>
<div class="text-white/70 text-sm">Blocks & Transactions monitored</div>
</div>
<div class="p-3 bg-white/5 rounded-lg">
<div class="font-semibold text-white mb-1">Seed Backup</div>
<div class="text-white/70 text-sm">Disabled (Development mode)</div>
</div>
<div class="flex gap-2 mb-4 flex-shrink-0 glass-card p-2 rounded-lg">
<button class="modal-tab flex-1 px-4 py-2 rounded-lg text-sm font-medium active" data-tab="node">Node Status</button>
<button class="modal-tab flex-1 px-4 py-2 rounded-lg text-sm font-medium" data-tab="rest">REST API</button>
<button class="modal-tab flex-1 px-4 py-2 rounded-lg text-sm font-medium" data-tab="grpc">gRPC</button>
<button class="modal-tab flex-1 px-4 py-2 rounded-lg text-sm font-medium" data-tab="logs">Logs</button>
</div>
</div>
</div>
<div class="modal hidden fixed inset-0 bg-black/80 backdrop-blur-sm z-50 items-center justify-center p-4" id="logsModal">
<div class="glass-card p-6 max-w-4xl w-full max-h-[80vh] overflow-y-auto">
<div class="flex justify-between items-center mb-4">
<h2 class="text-2xl font-bold text-white">Node Logs</h2>
<button onclick="closeLogs()" class="glass-button px-3 py-2 rounded-lg text-xl font-medium">×</button>
</div>
<div class="bg-black/40 rounded-lg p-4 font-mono text-xs text-white/80 whitespace-pre-wrap break-all" id="logsContent">
Loading logs...
<div class="overflow-y-auto flex-1 min-h-0 space-y-4">
<!-- Node Status tab -->
<div id="panel-node" class="tab-panel active">
<div class="flex items-start gap-4 mb-4">
<div class="flex-shrink-0 w-12 h-12 rounded-lg bg-white/10 flex items-center justify-center">
<svg class="w-6 h-6 text-white/80" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" /></svg>
</div>
<div class="flex-1">
<h3 class="text-lg font-semibold text-white mb-1">Node Status</h3>
<p class="text-white/70 text-sm">Lightning node information</p>
</div>
</div>
<div class="space-y-3">
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<span class="text-white/80 text-sm">Node Status</span>
<span class="text-green-400 text-sm font-medium" id="modalNodeStatus"></span>
</div>
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<span class="text-white/80 text-sm">Network</span>
<span class="text-white/60 text-sm" id="modalNetwork"></span>
</div>
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<span class="text-white/80 text-sm">Version</span>
<span class="text-white/60 text-sm" id="modalVersion"></span>
</div>
</div>
<div class="mt-4 space-y-3">
<div class="p-3 bg-white/5 rounded-lg">
<div class="font-semibold text-white mb-1">Network Mode</div>
<div class="text-white/70 text-sm" id="modalNetworkMode"></div>
</div>
<div class="p-3 bg-white/5 rounded-lg">
<div class="font-semibold text-white mb-1">Bitcoin Backend</div>
<div class="text-white/70 text-sm" id="modalBitcoinBackend"></div>
</div>
</div>
</div>
<!-- REST API tab -->
<div id="panel-rest" class="tab-panel">
<div class="flex items-start gap-4 mb-4">
<div class="flex-shrink-0 w-12 h-12 rounded-lg bg-white/10 flex items-center justify-center">
<svg class="w-6 h-6 text-white/80" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01" /></svg>
</div>
<div class="flex-1">
<h3 class="text-lg font-semibold text-white mb-1">REST API</h3>
<p class="text-white/70 text-sm">HTTP REST API access</p>
</div>
</div>
<div class="space-y-3">
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<span class="text-white/80 text-sm">REST Endpoint</span>
<span class="text-white/60 text-sm font-mono" id="modalRestEndpoint"></span>
</div>
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<span class="text-white/80 text-sm">API Status</span>
<span class="text-green-400 text-sm font-medium" id="modalRestStatus"></span>
</div>
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<span class="text-white/80 text-sm">API Version</span>
<span class="text-white/60 text-sm" id="modalRestVersion">v1</span>
</div>
</div>
<button class="mt-4 w-full info-card-button text-sm font-medium py-3 rounded-lg" onclick="copyRESTInfo()">Copy REST Info</button>
</div>
<!-- gRPC tab -->
<div id="panel-grpc" class="tab-panel">
<div class="flex items-start gap-4 mb-4">
<div class="flex-shrink-0 w-12 h-12 rounded-lg bg-white/10 flex items-center justify-center">
<svg class="w-6 h-6 text-white/80" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01" /></svg>
</div>
<div class="flex-1">
<h3 class="text-lg font-semibold text-white mb-1">gRPC Connection</h3>
<p class="text-white/70 text-sm">High-performance gRPC API</p>
</div>
</div>
<div class="space-y-3">
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<span class="text-white/80 text-sm">gRPC Host</span>
<span class="text-white/60 text-sm font-mono" id="modalGrpcHost"></span>
</div>
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<span class="text-white/80 text-sm">gRPC Status</span>
<span class="text-green-400 text-sm font-medium" id="modalGrpcStatus"></span>
</div>
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<span class="text-white/80 text-sm">P2P Port</span>
<span class="text-white/60 text-sm font-mono">9735</span>
</div>
</div>
</div>
<!-- Logs tab -->
<div id="panel-logs" class="tab-panel">
<div class="flex justify-between items-center mb-3">
<h3 class="text-lg font-semibold text-white">Node Logs</h3>
<button onclick="loadLogs()" class="px-3 py-1.5 glass-button rounded text-xs font-medium">Refresh</button>
</div>
<div class="bg-black/40 rounded-lg p-4 font-mono text-xs text-white/80 whitespace-pre-wrap break-all min-h-[200px]" id="logsContent">Loading logs...</div>
</div>
</div>
</div>
</div>
<script>
function copyRESTInfo() {
const info = `REST API: http://localhost:8080\nAPI Version: v1`;
navigator.clipboard.writeText(info).then(() => {
alert('REST info copied to clipboard!');
const REST_PORT = 8080;
const GRPC_PORT = 10009;
const P2P_PORT = 9735;
const host = window.location.hostname;
function getBackendUrl() {
const params = new URLSearchParams(window.location.search);
return params.get('backend') || '';
}
function setSettingsTab(tabId) {
document.querySelectorAll('.modal-tab').forEach(btn => {
btn.classList.toggle('active', btn.getAttribute('data-tab') === tabId);
});
document.querySelectorAll('.tab-panel').forEach(panel => {
panel.classList.toggle('active', panel.id === 'panel-' + tabId);
});
if (tabId === 'logs') loadLogs();
}
document.querySelectorAll('.modal-tab').forEach(btn => {
btn.addEventListener('click', () => setSettingsTab(btn.getAttribute('data-tab')));
});
function copyRESTInfo() {
const endpoint = host + ':' + REST_PORT;
const info = 'REST API: http://' + endpoint + '\nAPI Version: v1';
navigator.clipboard.writeText(info).then(() => alert('REST info copied to clipboard!'));
}
function openSettings() {
@ -535,55 +507,89 @@
document.getElementById('settingsModal').classList.remove('flex');
}
function openLogs() {
document.getElementById('logsModal').classList.remove('hidden');
document.getElementById('logsModal').classList.add('flex');
loadLogs();
function applyLiveData(data) {
if (data.getinfo) {
const g = data.getinfo;
const status = g.synced_to_chain ? 'Running' : 'Waiting for chain…';
const network = (g.chains && g.chains[0]) ? g.chains[0].network || '—' : '—';
const version = g.version || '—';
setText('headerNetwork', 'Network: ' + network);
setText('summaryNodeStatus', status);
setText('modalNodeStatus', status);
setText('modalNetwork', network);
setText('modalVersion', version);
setText('modalNetworkMode', network);
setText('modalBitcoinBackend', '—');
document.getElementById('statusPing').style.display = g.synced_to_chain ? 'block' : 'none';
}
if (data.channelCount !== undefined) {
setText('channelCount', String(data.channelCount));
}
setText('modalRestEndpoint', host + ':' + REST_PORT);
setText('modalRestStatus', data.restReachable ? 'Active' : '—');
setText('summaryRestStatus', data.restReachable ? 'Active' : '—');
setText('modalGrpcHost', host + ':' + GRPC_PORT);
setText('modalGrpcStatus', data.grpcReachable ? 'Connected' : '—');
setText('summaryGrpcStatus', data.grpcReachable ? 'Connected' : '—');
}
function closeLogs() {
document.getElementById('logsModal').classList.add('hidden');
document.getElementById('logsModal').classList.remove('flex');
function setText(id, text) {
const el = document.getElementById(id);
if (el) el.textContent = text;
}
function loadLogs() {
async function loadLogs() {
const logsContent = document.getElementById('logsContent');
logsContent.textContent = `LND version 0.17.4-beta commit=v0.17.4-beta
Attempting automatic RPC configuration to bitcoind
Automatically obtained bitcoind's RPC credentials
2024-01-15 12:00:00.000 [INF] LTND: Version: 0.17.4-beta commit=v0.17.4-beta
2024-01-15 12:00:00.100 [INF] LTND: Active chain: Bitcoin (network=regtest)
2024-01-15 12:00:00.200 [INF] CHDB: Checking for schema update
2024-01-15 12:00:00.300 [INF] LTND: Primary chain is set to: bitcoin
2024-01-15 12:00:01.000 [INF] RPCS: RPC server listening on 0.0.0.0:10009
2024-01-15 12:00:01.100 [INF] RPCS: REST proxy started at 0.0.0.0:8080
2024-01-15 12:00:01.200 [INF] LNWL: Opened wallet
2024-01-15 12:00:02.000 [INF] LTND: Waiting for chain backend to finish sync`;
const backendUrl = getBackendUrl();
if (backendUrl) {
logsContent.textContent = 'Loading logs...';
try {
const res = await fetch(backendUrl + '/api/container/logs?app_id=lnd&lines=200');
if (!res.ok) throw new Error(res.statusText);
const json = await res.json();
const lines = json.result || json.logs || (Array.isArray(json) ? json : []);
logsContent.textContent = Array.isArray(lines) ? lines.join('\n') : String(lines);
} catch (e) {
logsContent.textContent = 'Could not load logs: ' + e.message;
}
} else {
logsContent.textContent = 'Open this app with ?backend=http://HOST:5678 to load logs from the server.';
}
}
let channelCount = 0;
setInterval(() => {
if (Math.random() > 0.9) {
channelCount = Math.floor(Math.random() * 5);
document.getElementById('channelCount').textContent = channelCount;
async function fetchLiveData() {
const backendUrl = getBackendUrl();
const data = { channelCount: 0, restReachable: false, grpcReachable: false };
if (backendUrl) {
try {
const getinfoRes = await fetch(backendUrl + '/proxy/lnd/v1/getinfo');
if (getinfoRes.ok) {
data.getinfo = await getinfoRes.json();
data.restReachable = true;
}
} catch (_) {}
try {
const chRes = await fetch(backendUrl + '/proxy/lnd/v1/channels');
if (chRes.ok) {
const ch = await chRes.json();
data.channelCount = (ch.channels && ch.channels.length) || 0;
}
} catch (_) {}
data.grpcReachable = data.restReachable;
}
}, 10000);
applyLiveData(data);
}
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeSettings();
closeLogs();
}
document.addEventListener('DOMContentLoaded', () => {
fetchLiveData();
});
document.querySelectorAll('.modal').forEach(modal => {
modal.addEventListener('click', (e) => {
if (e.target === modal) {
closeSettings();
closeLogs();
}
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closeSettings();
});
document.getElementById('settingsModal').addEventListener('click', (e) => {
if (e.target.id === 'settingsModal') closeSettings();
});
</script>
</body>

View File

@ -49,6 +49,12 @@ echo ""
# Check for required tools
check_tools() {
local missing=""
local can_install=false
# Check if we can auto-install (running as root on Debian/Ubuntu)
if [ "$EUID" -eq 0 ] && [ -f /etc/debian_version ]; then
can_install=true
fi
# Check for docker or podman
if command -v docker >/dev/null 2>&1; then
@ -65,7 +71,36 @@ check_tools() {
if [ -n "$missing" ]; then
echo "❌ Missing required tools:$missing"
echo " Install with: apt install docker.io xorriso (or podman)"
if [ "$can_install" = true ]; then
echo " 📦 Auto-installing missing dependencies..."
apt-get update -qq
if [[ "$missing" == *"xorriso"* ]]; then
apt-get install -y xorriso
fi
if [[ "$missing" == *"docker-or-podman"* ]]; then
echo " Installing podman..."
apt-get install -y podman
CONTAINER_CMD="podman"
fi
echo " ✅ Dependencies installed successfully!"
else
echo " Install with: sudo apt install xorriso podman"
echo " Or run this script with sudo to auto-install"
exit 1
fi
fi
# Re-check after potential installation
if command -v docker >/dev/null 2>&1; then
CONTAINER_CMD="docker"
elif command -v podman >/dev/null 2>&1; then
CONTAINER_CMD="podman"
else
echo "❌ Container runtime still not available after installation"
exit 1
fi
@ -233,11 +268,66 @@ echo "📦 Step 2: Creating installer environment..."
# Download Debian Live as our installer base
BASE_ISO="$WORK_DIR/debian-live-installer.iso"
if [ ! -f "$BASE_ISO" ] || [ $(stat -f%z "$BASE_ISO" 2>/dev/null || echo 0) -lt 100000000 ]; then
echo " Downloading Debian Live base..."
rm -f "$BASE_ISO"
curl -L -o "$BASE_ISO" \
"https://sourceforge.net/projects/debian-live-respin-iso/files/standard/live-image-debian12.11-standard-20250522-amd64.hybrid.iso/download"
EXPECTED_SIZE=369000000 # ~352MB
# Check if file exists and is complete
if [ -f "$BASE_ISO" ]; then
CURRENT_SIZE=$(stat -f%z "$BASE_ISO" 2>/dev/null || stat -c%s "$BASE_ISO" 2>/dev/null || echo 0)
if [ "$CURRENT_SIZE" -ge "$EXPECTED_SIZE" ]; then
echo " ✅ Debian Live base already downloaded"
else
echo " Found incomplete download ($(($CURRENT_SIZE / 1024 / 1024))MB), removing..."
rm -f "$BASE_ISO"
fi
fi
if [ ! -f "$BASE_ISO" ]; then
echo " Downloading Debian Live base (352MB)..."
echo " (This may take 5-10 minutes depending on network speed)"
# Use wget without -O so --continue actually works
# Download with the ugly SourceForge filename, then rename
ISO_URL="https://sourceforge.net/projects/debian-live-respin-iso/files/standard/live-image-debian12.11-standard-20250522-amd64.hybrid.iso/download"
if command -v wget >/dev/null 2>&1; then
cd "$WORK_DIR"
wget --tries=10 --read-timeout=120 --continue --progress=bar:force \
--no-check-certificate \
"$ISO_URL" || {
echo " ❌ Download failed or incomplete"
echo " Partial file kept - run script again to continue"
exit 1
}
# Find the downloaded file (wget creates it with a name like "download" or the actual filename)
if [ -f "download" ]; then
mv "download" "$BASE_ISO"
elif [ -f "live-image-debian12.11-standard-20250522-amd64.hybrid.iso" ]; then
mv "live-image-debian12.11-standard-20250522-amd64.hybrid.iso" "$BASE_ISO"
else
echo " ❌ Downloaded file not found"
exit 1
fi
else
# Fallback to curl (no resume support)
curl -L --location-trusted --retry 10 --retry-delay 5 \
--connect-timeout 60 --max-time 1800 \
-o "$BASE_ISO" \
"$ISO_URL" || {
echo " ❌ Download failed"
rm -f "$BASE_ISO"
exit 1
}
fi
# Verify download size
FINAL_SIZE=$(stat -f%z "$BASE_ISO" 2>/dev/null || stat -c%s "$BASE_ISO" 2>/dev/null || echo 0)
if [ "$FINAL_SIZE" -lt "$EXPECTED_SIZE" ]; then
echo " ❌ Download incomplete: got $(($FINAL_SIZE / 1024 / 1024))MB, expected 352MB"
exit 1
fi
echo " ✅ Download complete ($(($FINAL_SIZE / 1024 / 1024))MB)"
fi
echo " Extracting installer base..."
@ -386,14 +476,41 @@ echo "📦 Step 3b: Bundling container images for offline use..."
IMAGES_DIR="$ARCH_DIR/container-images"
mkdir -p "$IMAGES_DIR"
# Define images to bundle (space-separated: image filename)
# When DEV_SERVER is set (and not localhost), try to capture images from live server
# so the ISO includes the same set as the dev server (including custom UIs: bitcoin-ui, lnd-ui).
IMAGES_CAPTURED_FROM_SERVER=0
if [ -n "$DEV_SERVER" ] && [ "$DEV_SERVER" != "localhost" ] && [ "$DEV_SERVER" != "127.0.0.1" ]; then
echo " Capturing container images from live server ($DEV_SERVER)..."
CAPTURE_PATTERNS="bitcoin-ui bitcoin-knots lnd lnd-ui filebrowser mempool tailscale homeassistant btcpayserver nostr-rs-relay strfry"
REMOTE_TMP="/tmp/archipelago-image-capture-$$"
SAVED_LIST=$(ssh "$DEV_SERVER" "mkdir -p $REMOTE_TMP && for p in $CAPTURE_PATTERNS; do img=\$(sudo podman images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep -i \"\$p\" | head -1); [ -n \"\$img\" ] && sudo podman save -o \"$REMOTE_TMP/\$p.tar\" \"\$img\" 2>/dev/null && echo \"\$p\"; done" 2>/dev/null) || true
for p in $SAVED_LIST; do
if [ -n "$p" ] && scp "$DEV_SERVER:$REMOTE_TMP/$p.tar" "$IMAGES_DIR/$p.tar" 2>/dev/null; then
echo " ✅ Captured from server: $p.tar"
IMAGES_CAPTURED_FROM_SERVER=1
fi
done
ssh "$DEV_SERVER" "rm -rf $REMOTE_TMP" 2>/dev/null || true
if [ "$IMAGES_CAPTURED_FROM_SERVER" = "0" ]; then
echo " ⚠️ No images captured from server, will use registry pull fallback"
fi
fi
# Define images to bundle for fallback (when not from server or missing). Includes filebrowser.
# bitcoin-ui and lnd-ui are custom and normally captured from server or built separately.
CONTAINER_IMAGES="
bitcoinknots/bitcoin:29 bitcoin-knots.tar
lightninglabs/lnd:v0.18.4-beta lnd.tar
ghcr.io/home-assistant/home-assistant:stable homeassistant.tar
btcpayserver/btcpayserver:latest btcpayserver.tar
mempool/frontend:latest mempool.tar
docker.io/filebrowser/filebrowser:latest filebrowser.tar
scsibug/nostr-rs-relay:latest nostr-rs-relay.tar
hoytech/strfry:latest strfry.tar
tailscale/tailscale:latest tailscale.tar
"
# Pull and save each image (force AMD64 for x86_64 target)
# Pull and save each image (force AMD64 for x86_64 target) only if not already present
echo "$CONTAINER_IMAGES" | while read -r image filename; do
[ -z "$image" ] && continue
tarpath="$IMAGES_DIR/$filename"
@ -711,9 +828,36 @@ if [ -t 0 ] && [ -z "$ARCHIPELAGO_WELCOMED" ]; then
export ARCHIPELAGO_WELCOMED=1
IP=$(hostname -I 2>/dev/null | awk '{print $1}')
echo ""
echo " ╔═══════════════════════════════════════════════════════════╗"
echo " ║ 🏝️ ARCHIPELAGO BITCOIN NODE OS ║"
echo " ╚═══════════════════════════════════════════════════════════╝"
echo " _"
echo " ,--.\\\`-. __"
echo " _,.\\\`. \\:/,\" \\\`-._"
echo " ,-*\" _,.-;-*\\\`-.+\"*._ )"
echo " ( ,.\"* ,-\" / \\\`. \\\\. \\\`."
echo " ,\" ,;\" ,\"\\../\\ \\: \\"
echo " ( ,\"/ / \\\\.,' : )) /"
echo " \\ |/ / \\\\.,' / // ,'"
echo " \\_)\\ ,' \\\\.,' ( / )/"
echo " \\\` \\._,' \\\`\""
echo " \\..\/"
echo " \\..\/"
echo " ~ ~\\..\/ ~~ ~~"
echo " ~~ ~~ \\..\/ ~~ ~ ~~"
echo " ~~ ~ ~~ __...---\\../-...__ ~~~ ~~"
echo " ~~~~ ~_,--' \\..\/ \\\`--.__ ~~ ~~"
echo " ~~~ __,--' \\\`\" \\\`--.__ ~~~"
echo "~~ ,--' \\\`--."
echo " '------......______ ______......------\\\` ~~"
echo " ~~~ ~ ~~ ~ \\\`\\\`\\\`\\\`\\\`---\"\"\"\"\" ~~ ~ ~~"
echo " ~~~~ ~~ ~~~~ ~~~~~~ ~ ~~ ~~ ~~~ ~"
echo ""
echo " █████╗ ██████╗ ██████╗██╗ ██╗██╗██████╗ ███████╗██╗ █████╗ ██████╗ ██████╗ "
echo " ██╔══██╗██╔══██╗██╔════╝██║ ██║██║██╔══██╗██╔════╝██║ ██╔══██╗██╔════╝ ██╔═══██╗"
echo " ███████║██████╔╝██║ ███████║██║██████╔╝█████╗ ██║ ███████║██║ ███╗██║ ██║"
echo " ██╔══██║██╔══██╗██║ ██╔══██║██║██╔═══╝ ██╔══╝ ██║ ██╔══██║██║ ██║██║ ██║"
echo " ██║ ██║██║ ██║╚██████╗██║ ██║██║██║ ███████╗███████╗██║ ██║╚██████╔╝╚██████╔╝"
echo " ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝╚═╝╚═╝ ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝ "
echo ""
echo " 🏝️ BITCOIN NODE OS 🏝️"
echo ""
if [ -n "$IP" ]; then
echo " 🌐 Web UI: http://$IP"
@ -774,9 +918,39 @@ umount /mnt/target/dev 2>/dev/null || true
umount /mnt/target/boot/efi 2>/dev/null || true
umount /mnt/target 2>/dev/null || true
echo ""
echo -e "${GREEN} _${NC}"
echo -e "${GREEN} ,--.\\\`-. __${NC}"
echo -e "${GREEN} _,.\\\`. \\:/,\" \\\`-._${NC}"
echo -e "${GREEN} ,-*\" _,.-;-*\\\`-.+\"*._ )${NC}"
echo -e "${GREEN} ( ,.\"* ,-\" / \\\`. \\\\. \\\`.${NC}"
echo -e "${GREEN} ,\" ,;\" ,\"\\../\\ \\: \\${NC}"
echo -e "${GREEN} ( ,\"/ / \\\\.,' : )) /${NC}"
echo -e "${GREEN} \\ |/ / \\\\.,' / // ,'${NC}"
echo -e "${GREEN} \\_)\\ ,' \\\\.,' ( / )/${NC}"
echo -e "${GREEN} \\\` \\._,' \\\`\"${NC}"
echo -e "${GREEN} \\../${NC}"
echo -e "${GREEN} \\../${NC}"
echo -e "${GREEN} ~ ~\\../ ~~ ~~${NC}"
echo -e "${GREEN} ~~ ~~ \\../ ~~ ~ ~~${NC}"
echo -e "${GREEN} ~~ ~ ~~ __...---\\../-...__ ~~~ ~~${NC}"
echo -e "${GREEN} ~~~~ ~_,--' \\../ \\\`--.__ ~~ ~~${NC}"
echo -e "${GREEN} ~~~ __,--' \\\`\" \\\`--.__ ~~~${NC}"
echo -e "${GREEN}~~ ,--' \\\`--.${NC}"
echo -e "${GREEN} '------......______ ______......------\\\` ~~${NC}"
echo -e "${GREEN} ~~~ ~ ~~ ~ \\\`\\\`\\\`\\\`\\\`---\"\"\"\"\" ~~ ~ ~~${NC}"
echo -e "${GREEN} ~~~~ ~~ ~~~~ ~~~~~~ ~ ~~ ~~ ~~~ ~${NC}"
echo ""
echo -e "${GREEN} █████╗ ██████╗ ██████╗██╗ ██╗██╗██████╗ ███████╗██╗ █████╗ ██████╗ ██████╗ ${NC}"
echo -e "${GREEN} ██╔══██╗██╔══██╗██╔════╝██║ ██║██║██╔══██╗██╔════╝██║ ██╔══██╗██╔════╝ ██╔═══██╗${NC}"
echo -e "${GREEN} ███████║██████╔╝██║ ███████║██║██████╔╝█████╗ ██║ ███████║██║ ███╗██║ ██║${NC}"
echo -e "${GREEN} ██╔══██║██╔══██╗██║ ██╔══██║██║██╔═══╝ ██╔══╝ ██║ ██╔══██║██║ ██║██║ ██║${NC}"
echo -e "${GREEN} ██║ ██║██║ ██║╚██████╗██║ ██║██║██║ ███████╗███████╗██║ ██║╚██████╔╝╚██████╔╝${NC}"
echo -e "${GREEN} ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝╚═╝╚═╝ ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ${NC}"
echo ""
echo -e "${GREEN} 🏝️ BITCOIN NODE OS 🏝️${NC}"
echo ""
echo -e "${GREEN}╔═══════════════════════════════════════════════════════════════════╗${NC}"
echo -e "${GREEN}║ ║${NC}"
echo -e "${GREEN}║ ✅ INSTALLATION COMPLETE! ║${NC}"
echo -e "${GREEN}║ ║${NC}"
echo -e "${GREEN}║ Remove the USB drive and press Enter to reboot. ║${NC}"
@ -789,6 +963,7 @@ echo -e "${GREEN}║ • Web Password: password123
echo -e "${GREEN}║ ║${NC}"
echo -e "${GREEN}║ Pre-loaded apps (ready to start via Web UI): ║${NC}"
echo -e "${GREEN}║ • Bitcoin Knots • LND • Home Assistant ║${NC}"
echo -e "${GREEN}║ • BTCPay Server • Mempool • Nostr Relays ║${NC}"
echo -e "${GREEN}║ ║${NC}"
echo -e "${GREEN}╚═══════════════════════════════════════════════════════════════════╝${NC}"
echo ""

View File

@ -132,6 +132,15 @@ const router = createRouter({
name: 'settings',
component: () => import('../views/Settings.vue'),
},
// Containers removed: My Apps serves the same purpose. Redirect old links.
{
path: 'containers',
redirect: () => ({ path: '/dashboard/apps' }),
},
{
path: 'containers/:id',
redirect: (to) => ({ path: `/dashboard/apps/${to.params.id}` }),
},
],
},
],

View File

@ -47,6 +47,56 @@ export const BUNDLED_APPS: BundledApp[] = [
volumes: [{ host: '/var/lib/archipelago/homeassistant', container: '/config' }],
category: 'home',
},
{
id: 'btcpayserver',
name: 'BTCPay Server',
image: 'docker.io/btcpayserver/btcpayserver:latest',
description: 'Self-hosted Bitcoin payment processor',
icon: '💳',
ports: [{ host: 23000, container: 23000 }],
volumes: [{ host: '/var/lib/archipelago/btcpay', container: '/datadir' }],
category: 'bitcoin',
},
{
id: 'mempool',
name: 'Mempool Explorer',
image: 'docker.io/mempool/frontend:latest',
description: 'Bitcoin blockchain and mempool visualizer',
icon: '🔍',
ports: [{ host: 8080, container: 8080 }],
volumes: [{ host: '/var/lib/archipelago/mempool', container: '/data' }],
category: 'bitcoin',
},
{
id: 'nostr-rs-relay',
name: 'Nostr Relay (RS)',
image: 'docker.io/scsibug/nostr-rs-relay:latest',
description: 'Rust-based Nostr relay for decentralized social',
icon: '🦩',
ports: [{ host: 8008, container: 8080 }],
volumes: [{ host: '/var/lib/archipelago/nostr-rs', container: '/usr/src/app/db' }],
category: 'other',
},
{
id: 'strfry',
name: 'Strfry Relay',
image: 'docker.io/hoytech/strfry:latest',
description: 'High-performance Nostr relay',
icon: '⚡',
ports: [{ host: 7777, container: 7777 }],
volumes: [{ host: '/var/lib/archipelago/strfry', container: '/app/strfry-db' }],
category: 'other',
},
{
id: 'tailscale',
name: 'Tailscale VPN',
image: 'docker.io/tailscale/tailscale:latest',
description: 'Zero-config VPN mesh network',
icon: '🔒',
ports: [],
volumes: [{ host: '/var/lib/archipelago/tailscale', container: '/var/lib/tailscale' }],
category: 'other',
},
]
export const useContainerStore = defineStore('container', () => {

View File

@ -79,7 +79,7 @@
Launch
</button>
<button
v-if="pkg.state === 'stopped'"
v-if="pkg.state === 'stopped' || pkg.state === 'exited'"
@click.stop="startApp(id as string)"
:disabled="loadingActions[id as string]"
class="flex-1 px-4 py-2 bg-green-500/20 border border-green-500/40 rounded-lg text-green-200 text-sm font-medium hover:bg-green-500/30 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
@ -97,7 +97,7 @@
<span>{{ loadingActions[id as string] ? 'Starting...' : 'Start' }}</span>
</button>
<button
v-if="pkg.state === 'running'"
v-if="pkg.state === 'running' || pkg.state === 'starting'"
@click.stop="stopApp(id as string)"
:disabled="loadingActions[id as string]"
class="flex-1 px-4 py-2 bg-yellow-500/20 border border-yellow-500/40 rounded-lg text-yellow-200 text-sm font-medium hover:bg-yellow-500/30 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
@ -202,8 +202,9 @@ function canLaunch(pkg: any): boolean {
// For dummy apps, allow launch if running (they have interface addresses)
// For real apps, check for UI interface
const hasUI = pkg.manifest.interfaces?.main?.ui || pkg.installed?.['interface-addresses']?.main
const isRunning = pkg.state === 'running'
return hasUI && isRunning
// Allow launch when running or starting (so buttons show even while backend reports "starting")
const canLaunchState = pkg.state === 'running' || pkg.state === 'starting'
return hasUI && canLaunchState
}
function launchApp(id: string) {

View File

@ -241,7 +241,7 @@ async function handleRemove() {
actionLoading.value = true
try {
await store.removeContainer(appId.value)
router.push('/dashboard/containers')
router.push('/dashboard/apps')
} catch (e) {
error.value = e instanceof Error ? e.message : 'Failed to remove container'
} finally {

View File

@ -105,6 +105,18 @@
<span>{{ store.isAppLoading(app.id) ? 'Starting...' : 'Start' }}</span>
</button>
<!-- Starting (created): show progress, no Launch yet -->
<div
v-else-if="store.getAppState(app.id) === 'created'"
class="flex-1 flex items-center justify-center gap-2 px-4 py-2 text-white/70 text-sm"
>
<svg class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span>Starting</span>
</div>
<!-- Running: Stop and Launch buttons -->
<template v-else-if="store.getAppState(app.id) === 'running'">
<button
@ -183,7 +195,7 @@
</template>
<script setup lang="ts">
import { onMounted, computed } from 'vue'
import { onMounted, onUnmounted, computed } from 'vue'
import { useContainerStore, type BundledApp } from '@/stores/container'
import ContainerStatus from '@/components/ContainerStatus.vue'
@ -195,15 +207,30 @@ const bundledApps = computed(() => store.enrichedBundledApps)
// Get current host for launch URLs
const currentHost = computed(() => window.location.hostname)
let startingPollInterval: ReturnType<typeof setInterval> | null = null
onMounted(async () => {
await store.fetchContainers()
await store.fetchHealthStatus()
// Refresh every 10 seconds
setInterval(async () => {
await store.fetchContainers()
await store.fetchHealthStatus()
}, 10000)
// When any bundled app is in 'created' (starting), poll every 2s so state updates to running
startingPollInterval = setInterval(async () => {
const anyStarting = bundledApps.value.some((app) => store.getAppState(app.id) === 'created')
if (anyStarting) {
await store.fetchContainers()
await store.fetchHealthStatus()
}
}, 2000)
})
onUnmounted(() => {
if (startingPollInterval) clearInterval(startingPollInterval)
})
// Containers that aren't bundled apps
@ -237,6 +264,8 @@ function getStatusBadgeClass(appId: string): string {
case 'stopped':
case 'exited':
return 'bg-gray-500/20 text-gray-400'
case 'created':
return 'bg-yellow-500/20 text-yellow-400'
case 'not-installed':
default:
return 'bg-blue-500/20 text-blue-400'
@ -270,17 +299,28 @@ function getStatusText(appId: string): string {
return 'Stopped'
case 'not-installed':
return 'Ready to Start'
case 'created':
return 'Starting'
default:
return state
}
}
const backendPort = 5678
function getLaunchUrl(app: BundledApp): string {
// Prefer lan_address from backend (for apps with custom UIs)
if (app.lan_address) {
return app.lan_address
// Replace localhost so Launch works when browsing from another machine (e.g. 192.168.1.228)
let url = app.lan_address.replace(/localhost/i, currentHost.value)
// LND UI (and other app UIs) need backend URL for live data (logs, getinfo proxy)
if (app.id === 'lnd') {
const backend = `http://${currentHost.value}:${backendPort}`
url += (url.includes('?') ? '&' : '?') + 'backend=' + encodeURIComponent(backend)
}
return url
}
// Fallback to first configured port
const port = app.ports[0]?.host
if (!port) return '#'

View File

@ -184,16 +184,6 @@
>
Not Available
</button>
<a
v-if="app.repoUrl"
:href="app.repoUrl"
target="_blank"
rel="noopener noreferrer"
class="px-4 py-2 bg-white/10 hover:bg-white/20 rounded-lg text-white text-sm font-medium transition-all"
@click.stop
>
GitHub
</a>
</div>
</div>
</div>

View File

@ -95,18 +95,6 @@
</svg>
{{ installing ? 'Installing...' : 'Install' }}
</button>
<a
v-if="app.repoUrl"
:href="app.repoUrl"
target="_blank"
rel="noopener noreferrer"
class="px-4 py-2.5 glass-button rounded-lg text-sm font-medium hover:bg-white/15 transition-colors flex items-center gap-2"
>
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.840 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
Source
</a>
</div>
</div>
@ -172,19 +160,6 @@
</svg>
{{ installing ? 'Installing...' : 'Install' }}
</button>
<a
v-if="app.repoUrl"
:href="app.repoUrl"
target="_blank"
rel="noopener noreferrer"
:class="isInstalled ? 'col-span-1' : 'col-span-2'"
class="px-4 py-2.5 glass-button rounded-lg text-sm font-medium hover:bg-white/15 transition-colors flex items-center justify-center gap-2"
>
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.840 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
Source
</a>
</div>
<!-- Installation Error Banner (Mobile) -->
@ -315,24 +290,11 @@
</div>
</div>
<!-- Links Card -->
<div v-if="app.repoUrl || app.manifestUrl" class="glass-card p-6">
<!-- Links Card (no GitHub - repo link removed per product) -->
<div v-if="app.manifestUrl" class="glass-card p-6">
<h3 class="text-lg font-bold text-white mb-4">Links</h3>
<div class="space-y-2">
<a
v-if="app.repoUrl"
:href="app.repoUrl"
target="_blank"
rel="noopener noreferrer"
class="flex items-center gap-2 text-blue-400 hover:text-blue-300 transition-colors"
>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.840 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
GitHub Repository
</a>
<a
v-if="app.manifestUrl"
:href="app.manifestUrl"
target="_blank"
rel="noopener noreferrer"

View File

@ -16,6 +16,9 @@ PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
# Configuration
TARGET_HOST="${ARCHIPELAGO_TARGET:-archipelago@192.168.1.228}"
TARGET_DIR="/home/archipelago/archy"
# Password for non-interactive SSH/rsync (dev server only). See .cursor/rules/Development-Workflow.md
ARCHIPELAGO_PASSWORD="${ARCHIPELAGO_PASSWORD:-archipelago}"
SSH_OPTS="-o StrictHostKeyChecking=no"
echo "╔════════════════════════════════════════════════════════════════╗"
echo "║ Deploying to Archipelago Target ║"
@ -36,7 +39,8 @@ done
# Sync code
echo "📦 Syncing code..."
rsync -avz --delete \
sshpass -p "$ARCHIPELAGO_PASSWORD" rsync -avz --delete \
-e "ssh $SSH_OPTS" \
--exclude 'node_modules' \
--exclude 'target' \
--exclude 'dist' \
@ -57,12 +61,12 @@ echo "🔨 Building on target..."
# Frontend
echo " Building frontend..."
ssh "$TARGET_HOST" "cd $TARGET_DIR/neode-ui && npm install --silent && npm run build" 2>&1 | sed 's/^/ /'
sshpass -p "$ARCHIPELAGO_PASSWORD" ssh $SSH_OPTS "$TARGET_HOST" "cd $TARGET_DIR/neode-ui && npm install --silent && npm run build" 2>&1 | sed 's/^/ /'
# Backend (if Rust is installed)
if ssh "$TARGET_HOST" "source ~/.cargo/env 2>/dev/null && command -v cargo" >/dev/null 2>&1; then
if sshpass -p "$ARCHIPELAGO_PASSWORD" ssh $SSH_OPTS "$TARGET_HOST" "source ~/.cargo/env 2>/dev/null && command -v cargo" >/dev/null 2>&1; then
echo " Building backend..."
ssh "$TARGET_HOST" "source ~/.cargo/env && cd $TARGET_DIR/core && cargo build --release 2>&1" | tail -10 | sed 's/^/ /'
sshpass -p "$ARCHIPELAGO_PASSWORD" ssh $SSH_OPTS "$TARGET_HOST" "source ~/.cargo/env && cd $TARGET_DIR/core && cargo build --release 2>&1" | tail -10 | sed 's/^/ /'
else
echo " ⚠️ Rust not installed on target, skipping backend build"
fi
@ -72,26 +76,32 @@ if [ "$LIVE" = true ]; then
echo "🚀 Deploying to live system..."
# Deploy backend (check if binary exists)
if ssh "$TARGET_HOST" "[ -f $TARGET_DIR/core/target/release/archipelago ]" 2>/dev/null; then
if sshpass -p "$ARCHIPELAGO_PASSWORD" ssh $SSH_OPTS "$TARGET_HOST" "[ -f $TARGET_DIR/core/target/release/archipelago ]" 2>/dev/null; then
echo " Deploying backend binary..."
# Stop service first so we can overwrite the binary
ssh "$TARGET_HOST" "sudo systemctl stop archipelago"
ssh "$TARGET_HOST" "sudo cp $TARGET_DIR/core/target/release/archipelago /usr/local/bin/"
sshpass -p "$ARCHIPELAGO_PASSWORD" ssh $SSH_OPTS "$TARGET_HOST" "sudo systemctl stop archipelago"
sshpass -p "$ARCHIPELAGO_PASSWORD" ssh $SSH_OPTS "$TARGET_HOST" "sudo cp $TARGET_DIR/core/target/release/archipelago /usr/local/bin/"
fi
# Deploy frontend
echo " Deploying frontend..."
ssh "$TARGET_HOST" "sudo rm -rf /opt/archipelago/web-ui/*"
ssh "$TARGET_HOST" "sudo cp -r $TARGET_DIR/web/dist/neode-ui/* /opt/archipelago/web-ui/"
ssh "$TARGET_HOST" "sudo chown -R 1000:1000 /opt/archipelago/web-ui"
sshpass -p "$ARCHIPELAGO_PASSWORD" ssh $SSH_OPTS "$TARGET_HOST" "sudo rm -rf /opt/archipelago/web-ui/*"
sshpass -p "$ARCHIPELAGO_PASSWORD" ssh $SSH_OPTS "$TARGET_HOST" "sudo cp -r $TARGET_DIR/web/dist/neode-ui/* /opt/archipelago/web-ui/"
sshpass -p "$ARCHIPELAGO_PASSWORD" ssh $SSH_OPTS "$TARGET_HOST" "sudo chown -R 1000:1000 /opt/archipelago/web-ui"
# Restart services
echo " Restarting services..."
ssh "$TARGET_HOST" "sudo systemctl start archipelago && sudo systemctl restart nginx"
sshpass -p "$ARCHIPELAGO_PASSWORD" ssh $SSH_OPTS "$TARGET_HOST" "sudo systemctl start archipelago && sudo systemctl restart nginx"
# Rebuild and restart LND UI container (serves the static app at port 8081; otherwise changes to docker/lnd-ui/ are not visible)
echo " Rebuilding LND UI..."
if sshpass -p "$ARCHIPELAGO_PASSWORD" ssh $SSH_OPTS "$TARGET_HOST" "cd $TARGET_DIR/docker/lnd-ui && (command -v podman >/dev/null 2>&1 && sudo podman build -t lnd-ui:latest . || sudo docker build -t lnd-ui:latest .)" 2>&1 | tail -8 | sed 's/^/ /'; then
echo " Restarting LND UI container..."
sshpass -p "$ARCHIPELAGO_PASSWORD" ssh $SSH_OPTS "$TARGET_HOST" 'for c in $(sudo podman ps -a --format "{{.Names}}" 2>/dev/null | grep -i lnd-ui) $(sudo docker ps -a --format "{{.Names}}" 2>/dev/null | grep -i lnd-ui); do [ -n "$c" ] && (sudo podman restart "$c" 2>/dev/null || sudo docker restart "$c" 2>/dev/null) && break; done' || true
fi
echo ""
echo "✅ Deployed to live system!"
echo " Backend: $(ssh "$TARGET_HOST" 'sudo systemctl is-active archipelago')"
echo " Backend: $(sshpass -p "$ARCHIPELAGO_PASSWORD" ssh $SSH_OPTS "$TARGET_HOST" 'sudo systemctl is-active archipelago')"
echo " Web UI: http://$(echo $TARGET_HOST | cut -d@ -f2)"
else
echo ""