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:
parent
d988396111
commit
6035c93289
@ -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
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
1
core/Cargo.lock
generated
@ -42,6 +42,7 @@ dependencies = [
|
||||
"hyper 0.14.32",
|
||||
"hyper-util",
|
||||
"hyper-ws-listener",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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>,
|
||||
|
||||
@ -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>,
|
||||
|
||||
@ -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">⚡</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>
|
||||
|
||||
@ -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 ""
|
||||
|
||||
@ -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}` }),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 '#'
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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 ""
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user