Compare commits
24 Commits
v1.2.0-alp
...
v1.2.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1db74ed28 | ||
|
|
27f205f38a | ||
|
|
25ad68ac4c | ||
|
|
1ffc377a9c | ||
|
|
19ab5c0749 | ||
|
|
c080c12629 | ||
|
|
0281229425 | ||
|
|
02d9bc3e44 | ||
|
|
cb11871b03 | ||
|
|
ba82fa1564 | ||
|
|
bd5a24515f | ||
|
|
dd5ab6b10a | ||
|
|
f54206d231 | ||
|
|
9f90c2cc91 | ||
|
|
db472691c9 | ||
|
|
836290840c | ||
|
|
00eebfbb3d | ||
|
|
a6f2e6743f | ||
|
|
0c5b7db4a2 | ||
|
|
fef7e8cb24 | ||
|
|
280c61f857 | ||
|
|
3682855668 | ||
|
|
93c2c3ee67 | ||
|
|
cc8a6fd4d8 |
80
.claude/plans/prancy-scribbling-pnueli.md
Normal file
80
.claude/plans/prancy-scribbling-pnueli.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# Plan: Demo Seeding, Dev Environment Fix, and Developer Onboarding
|
||||
|
||||
## Context
|
||||
After the repo cleanup (docs/scripts archived to `~/Projects/archy-archive/`), several dev scripts reference deleted files. Additionally, the demo needs better seeding for Portainer showcase, ThunderHub + Fedimint need to be visible, and a new developer needs docs to onboard.
|
||||
|
||||
## Changes
|
||||
|
||||
### 1. Fix broken dev scripts
|
||||
|
||||
**`neode-ui/start-dev.sh`** — Remove lines 72-110 (Docker Desktop check + `start-docker-apps.sh` call). Replace with a one-liner noting mock backend handles simulation.
|
||||
|
||||
**`neode-ui/stop-dev.sh`** — Remove lines 66-74 (Docker container stop block calling `stop-docker-apps.sh`).
|
||||
|
||||
**`neode-ui/package.json`** — Remove the `prebuild` script (line 22) that references archived `../../loop-start.mp3`. File already exists at `public/assets/audio/`.
|
||||
|
||||
**`scripts/dev-start.sh`** — Fix option 2 (Full Stack) lines 67-84 that reference `start-docker-apps.sh`. Guard with a skip message instead of failing.
|
||||
|
||||
### 2. Add ThunderHub (Lightning management UI)
|
||||
|
||||
**Files**: mock-backend.js, Marketplace.vue, appLauncher.ts, new icon SVG
|
||||
|
||||
- Port: **3010** (3000 taken by Grafana)
|
||||
- Docker image: `apotdevin/thunderhub:v0.13.31`
|
||||
- Add to `portMappings`, `marketplaceMetadata`, `staticDevApps`, `marketplace.get()` in mock-backend.js
|
||||
- Add to `getCuratedAppList()` in Marketplace.vue (after LND entry)
|
||||
- Add to `recommended` tier in `getAppTier()`
|
||||
- Add `'3010': 'thunderhub'` to PORT_TO_APP_ID in appLauncher.ts
|
||||
- Create `neode-ui/public/assets/img/app-icons/thunderhub.svg` (Bitcoin-orange lightning bolt icon)
|
||||
|
||||
### 3. Improve Fedimint in demo
|
||||
|
||||
**mock-backend.js**:
|
||||
- Add `fedimint` to `staticDevApps` (pre-installed, running, port 8175)
|
||||
- Update `marketplace.get()` version from `0.4.3` → `0.10.0`
|
||||
- Fix `portMappings.fedimint` from 8174 → 8175 (Guardian UI port)
|
||||
|
||||
### 4. Add realistic notifications
|
||||
|
||||
**mock-backend.js** — Replace empty `node.notifications` with 5 realistic entries: Bitcoin sync, LND channel opened, disk warning, system update, Fedimint guardian connected.
|
||||
|
||||
### 5. Rewrite README for developer onboarding
|
||||
|
||||
**`neode-ui/README.md`** — Full rewrite:
|
||||
- Quick start (npm install, npm start, localhost:8100, password123)
|
||||
- Architecture overview
|
||||
- Dev modes (setup/onboarding/existing/boot)
|
||||
- Mock backend capabilities (8 static apps, 30+ marketplace, WebSocket, FileBrowser API, Claude proxy)
|
||||
- Demo deployment (docker-compose.demo.yml, Portainer, ANTHROPIC_API_KEY)
|
||||
- Design system (glassmorphism classes, tokens)
|
||||
- Build commands
|
||||
- Remove Angular references and outdated sections
|
||||
|
||||
**`neode-ui/DEV-SCRIPTS.md`** — Update "Available Test Apps" section to list the 8 actual static apps, remove Docker apps references.
|
||||
|
||||
### 6. Verify Docker demo build
|
||||
|
||||
Confirm `docker-compose.demo.yml` paths still valid after cleanup:
|
||||
- `demo/aiui/` exists (for Dockerfile.web COPY)
|
||||
- `neode-ui/docker/nginx-demo.conf` exists
|
||||
- `neode-ui/docker/docker-entrypoint.sh` exists
|
||||
|
||||
## Files to modify
|
||||
1. `neode-ui/start-dev.sh`
|
||||
2. `neode-ui/stop-dev.sh`
|
||||
3. `neode-ui/package.json`
|
||||
4. `scripts/dev-start.sh`
|
||||
5. `neode-ui/mock-backend.js`
|
||||
6. `neode-ui/src/views/Marketplace.vue`
|
||||
7. `neode-ui/src/stores/appLauncher.ts`
|
||||
8. `neode-ui/public/assets/img/app-icons/thunderhub.svg` (new)
|
||||
9. `neode-ui/README.md`
|
||||
10. `neode-ui/DEV-SCRIPTS.md`
|
||||
|
||||
## Verification
|
||||
1. `cd neode-ui && npm start` — should start cleanly, no errors about missing scripts
|
||||
2. Visit localhost:8100 → login → Dashboard shows 8 apps (bitcoin, lnd, electrs, mempool, lorabell, filebrowser, thunderhub, fedimint)
|
||||
3. Marketplace shows ThunderHub in Bitcoin category
|
||||
4. Notifications bell shows 3 unread
|
||||
5. `npm stop` — clean shutdown, no errors
|
||||
6. `docker compose -f docker-compose.demo.yml build` — builds successfully
|
||||
@@ -5,15 +5,46 @@ globs:
|
||||
- "**/*podman*"
|
||||
- "**/Containerfile"
|
||||
- "**/Dockerfile"
|
||||
- "**/first-boot*"
|
||||
- "**/container-doctor*"
|
||||
---
|
||||
|
||||
# Container Security Rules (Archipelago)
|
||||
# Container Security Rules (Archipelago — Rootless Podman)
|
||||
|
||||
- `readonly_root: true` always — containers must not write to their root filesystem
|
||||
## Rootless Podman Architecture
|
||||
- Podman runs as `archipelago` user (UID 1000), NOT root — never use `sudo podman`
|
||||
- UID namespace mapping via subuid: container UID N → host UID (100000 + N)
|
||||
- Container images stored in `~/.local/share/containers/storage/` (NOT /var/lib/containers)
|
||||
- Container subnet: `10.89.0.0/16` (rootless), not `10.88.0.0/16` (rootful)
|
||||
- XDG_RUNTIME_DIR must be `/run/user/1000` — required for podman socket
|
||||
- `loginctl enable-linger archipelago` required for containers to survive logout
|
||||
|
||||
## Container Security (Non-Negotiable)
|
||||
- Drop ALL capabilities, add only what's required (`--cap-drop=ALL --cap-add=...`)
|
||||
- Run as non-root user (UID > 1000): `--user 1001:1001`
|
||||
- Set `--security-opt=no-new-privileges:true`
|
||||
- Pin image versions by SHA256 digest, never use `:latest` tag
|
||||
- Set `--security-opt=no-new-privileges:true` on all containers
|
||||
- Use `--read-only` + tmpfs where possible (safe apps: searxng, grafana, filebrowser, electrumx, nostr-rs-relay, ollama, indeedhub)
|
||||
- Pin image versions — never use `:latest` tag
|
||||
- Mount secrets as read-only files, never pass as environment variables when possible
|
||||
- Set memory and CPU limits on all containers
|
||||
- Use `--network=none` unless network access is required
|
||||
- All containers must have `--restart unless-stopped`
|
||||
|
||||
## Volume Ownership (Critical for Rootless)
|
||||
- Volume directories must be owned by the MAPPED UID, not the container UID
|
||||
- Formula: `host_uid = 100000 + container_uid`
|
||||
- UID 0 (most apps) → `sudo chown -R 100000:100000 /var/lib/archipelago/{app}`
|
||||
- UID 101 (bitcoin) → `sudo chown -R 100101:100101 /var/lib/archipelago/bitcoin`
|
||||
- UID 70 (postgres) → `sudo chown -R 100070:100070 /var/lib/archipelago/postgres-*`
|
||||
- UID 472 (grafana) → `sudo chown -R 100472:100472 /var/lib/archipelago/grafana`
|
||||
- UID 999 (mariadb) → `sudo chown -R 100999:100999 /var/lib/archipelago/mysql-*`
|
||||
|
||||
## Systemd Service Requirements
|
||||
- `ProtectHome=no` — podman needs `~/.local/share/containers/`
|
||||
- `PrivateTmp=no` — podman runtime uses `/tmp/podman-run-1000/`
|
||||
- `RestrictNamespaces=` must NOT be set — rootless podman creates user namespaces
|
||||
- `SystemCallFilter=` must NOT be set — rootless podman needs clone/unshare
|
||||
- UFW `DEFAULT_FORWARD_POLICY="ACCEPT"` — required for LAN access to container ports
|
||||
|
||||
## Network Rules
|
||||
- Apps needing inter-container DNS: use `--network=archy-net` (bitcoin, lnd, electrumx, mempool, btcpay, fedimint)
|
||||
- Standalone apps: default bridge network
|
||||
- Tailscale only: `--network=host` + `NET_ADMIN` + `NET_RAW` + `/dev/net/tun`
|
||||
|
||||
@@ -4,6 +4,7 @@ description: >
|
||||
Comprehensive Podman container diagnostic for Archipelago. Audits all running containers,
|
||||
port mappings, network connectivity, health status, restart policies, and config consistency
|
||||
across all 4 layers (backend Rust, Podman runtime, Nginx proxy, frontend routing).
|
||||
Handles rootless Podman (user: archipelago, UID 1000, subuid 100000:65536).
|
||||
Use when asked to "diagnose containers", "check podman", "why is app not working",
|
||||
"container health check", "port not reachable", "audit containers", "podman status",
|
||||
or when any container/app is misbehaving.
|
||||
@@ -12,46 +13,123 @@ allowed-tools: Bash Read Glob Grep
|
||||
|
||||
# Podman Doctor — Container Infrastructure Diagnostics
|
||||
|
||||
Systematic diagnostic for Archipelago's Podman container stack. Catches port conflicts, network misconfigurations, health failures, missing restart policies, and config drift across all layers.
|
||||
Systematic diagnostic for Archipelago's **rootless Podman** container stack. Catches port conflicts, network misconfigurations, health failures, missing restart policies, UID mapping issues, and config drift across all layers.
|
||||
|
||||
**SSH command**: `ssh -i ~/.ssh/archipelago-deploy archipelago@192.168.1.228`
|
||||
|
||||
> **ROOTLESS PODMAN**: Archipelago runs Podman as the `archipelago` user (UID 1000), NOT root.
|
||||
> Never use `sudo podman` — use plain `podman` after SSH'ing in as the `archipelago` user.
|
||||
> Container UIDs are mapped via subuid: container UID N → host UID (100000 + N).
|
||||
|
||||
If $ARGUMENTS is provided, focus diagnosis on that specific app/container. Otherwise run full audit.
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Gather Runtime State
|
||||
|
||||
Run these on the server:
|
||||
Run these on the server (as `archipelago` user — NO sudo):
|
||||
|
||||
```bash
|
||||
# All containers with status, ports, networks
|
||||
sudo podman ps -a --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}\t{{.Networks}}"
|
||||
podman ps -a --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}\t{{.Networks}}"
|
||||
|
||||
# Check for port conflicts on known ports
|
||||
sudo ss -tlnp | grep -E ":(80|443|3000|4080|5678|8080|8081|8082|8083|8085|8096|8123|8173|8174|8175|8240|8332|8333|8334|8888|9735|10009|11434|23000|50001)\b"
|
||||
ss -tlnp | grep -E ":(80|443|3000|4080|5678|8080|8081|8082|8083|8085|8096|8123|8173|8174|8175|8240|8332|8333|8334|8888|9735|10009|11434|23000|50001)\b"
|
||||
```
|
||||
|
||||
### Step 2: Check Restart Policies
|
||||
### Step 2: Rootless Podman Health Check
|
||||
|
||||
Rootless Podman has specific requirements that must be verified:
|
||||
|
||||
```bash
|
||||
# Verify running as archipelago user (NOT root)
|
||||
whoami # Must be "archipelago"
|
||||
id # Must show uid=1000(archipelago)
|
||||
|
||||
# Check XDG_RUNTIME_DIR is set (required for rootless podman socket)
|
||||
echo "XDG_RUNTIME_DIR=$XDG_RUNTIME_DIR" # Must be /run/user/1000
|
||||
|
||||
# Verify subuid/subgid mapping exists
|
||||
grep archipelago /etc/subuid # Must show: archipelago:100000:65536
|
||||
grep archipelago /etc/subgid # Must show: archipelago:100000:65536
|
||||
|
||||
# Verify user lingering is enabled (keeps user services after logout)
|
||||
ls /var/lib/systemd/linger/ | grep archipelago # Must exist
|
||||
|
||||
# Check podman storage is accessible
|
||||
podman info --format "{{.Store.GraphRoot}}" # ~/.local/share/containers/storage
|
||||
ls -la ~/.local/share/containers/storage/ 2>/dev/null || echo "ERROR: Storage not accessible"
|
||||
|
||||
# Check podman socket
|
||||
ls -la /run/user/1000/podman/ 2>/dev/null || echo "WARNING: No podman socket directory"
|
||||
```
|
||||
|
||||
### Step 3: Check Restart Policies
|
||||
|
||||
Every container MUST have `--restart unless-stopped`. This is the #1 cause of downtime after reboots.
|
||||
|
||||
```bash
|
||||
for c in $(sudo podman ps -a --format "{{.Names}}"); do
|
||||
for c in $(podman ps -a --format "{{.Names}}"); do
|
||||
echo -n "$c: "
|
||||
sudo podman inspect "$c" --format "{{.HostConfig.RestartPolicy.Name}}"
|
||||
podman inspect "$c" --format "{{.HostConfig.RestartPolicy.Name}}"
|
||||
done
|
||||
```
|
||||
|
||||
**Red flag**: `no` or empty = container won't survive reboot.
|
||||
|
||||
### Step 3: Verify Port Mapping Consistency
|
||||
### Step 4: Volume Ownership Audit (Rootless UID Mapping)
|
||||
|
||||
Rootless Podman maps container UIDs via subuid. Volume directories must be owned by the MAPPED UID, not the container UID. Formula: `host_uid = 100000 + container_uid`
|
||||
|
||||
```bash
|
||||
echo "=== Volume Ownership Check ==="
|
||||
|
||||
# Default containers (run as root inside = UID 0 → host UID 100000)
|
||||
for dir in lnd fedimint homeassistant jellyfin vaultwarden photoprism ollama filebrowser electrumx btcpay immich; do
|
||||
if [ -d "/var/lib/archipelago/$dir" ]; then
|
||||
owner=$(stat -c '%u:%g' "/var/lib/archipelago/$dir" 2>/dev/null)
|
||||
if [ "$owner" != "100000:100000" ]; then
|
||||
echo "WRONG: /var/lib/archipelago/$dir owned by $owner (should be 100000:100000)"
|
||||
else
|
||||
echo " OK: $dir → $owner"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# Bitcoin Knots (container UID 101 → host UID 100101)
|
||||
if [ -d "/var/lib/archipelago/bitcoin" ]; then
|
||||
owner=$(stat -c '%u:%g' "/var/lib/archipelago/bitcoin")
|
||||
[ "$owner" != "100101:100101" ] && echo "WRONG: bitcoin owned by $owner (should be 100101:100101)" || echo " OK: bitcoin → $owner"
|
||||
fi
|
||||
|
||||
# PostgreSQL (container UID 70 → host UID 100070)
|
||||
for dir in /var/lib/archipelago/*-db /var/lib/archipelago/postgres-*; do
|
||||
if [ -d "$dir" ]; then
|
||||
owner=$(stat -c '%u:%g' "$dir")
|
||||
[ "$owner" != "100070:100070" ] && echo "WRONG: $dir owned by $owner (should be 100070:100070)" || echo " OK: $(basename $dir) → $owner"
|
||||
fi
|
||||
done
|
||||
|
||||
# Grafana (container UID 472 → host UID 100472)
|
||||
if [ -d "/var/lib/archipelago/grafana" ]; then
|
||||
owner=$(stat -c '%u:%g' "/var/lib/archipelago/grafana")
|
||||
[ "$owner" != "100472:100472" ] && echo "WRONG: grafana owned by $owner (should be 100472:100472)" || echo " OK: grafana → $owner"
|
||||
fi
|
||||
|
||||
# MariaDB/MySQL (container UID 999 → host UID 100999)
|
||||
if [ -d "/var/lib/archipelago/mysql-mempool" ]; then
|
||||
owner=$(stat -c '%u:%g' "/var/lib/archipelago/mysql-mempool")
|
||||
[ "$owner" != "100999:100999" ] && echo "WRONG: mysql-mempool owned by $owner (should be 100999:100999)" || echo " OK: mysql-mempool → $owner"
|
||||
fi
|
||||
```
|
||||
|
||||
### Step 5: Verify Port Mapping Consistency
|
||||
|
||||
Cross-reference these 4 layers — mismatches between ANY two cause "app not loading" bugs:
|
||||
|
||||
**Layer 1 — Backend Config (Rust)**: Read `core/archipelago/src/api/rpc/package.rs`, look at `get_app_config()` port mappings.
|
||||
|
||||
**Layer 2 — Podman Runtime**: `sudo podman ps --format "{{.Names}}: {{.Ports}}"`
|
||||
**Layer 2 — Podman Runtime**: `podman ps --format "{{.Names}}: {{.Ports}}"`
|
||||
|
||||
**Layer 3 — Nginx Proxy**: Read these for `/app/{id}/` location blocks:
|
||||
- `image-recipe/configs/nginx-archipelago.conf` (HTTP)
|
||||
@@ -66,77 +144,114 @@ Cross-reference these 4 layers — mismatches between ANY two cause "app not loa
|
||||
| Works on port but not /app/ path | Missing nginx location block |
|
||||
| Frontend can't find app | PORT_TO_APP_ID missing in appLauncher.ts |
|
||||
|
||||
### Step 4: Network Connectivity Audit
|
||||
### Step 6: Network Connectivity Audit
|
||||
|
||||
```bash
|
||||
# Networks and their containers
|
||||
sudo podman network ls
|
||||
sudo podman network inspect archy-net 2>/dev/null || echo "WARNING: archy-net missing!"
|
||||
podman network ls
|
||||
podman network inspect archy-net 2>/dev/null || echo "WARNING: archy-net missing!"
|
||||
|
||||
# Check container subnet (rootless uses 10.89.x.x, NOT 10.88.x.x)
|
||||
podman network inspect archy-net --format "{{range .Subnets}}{{.Subnet}}{{end}}" 2>/dev/null
|
||||
```
|
||||
|
||||
**Must be on archy-net**: bitcoin-knots, lnd, electrs, mempool, btcpay-server, nbxplorer, fedimint, fedimint-gateway, nostr-rs-relay, indeedhub, ollama, open-webui
|
||||
**Must be on archy-net**: bitcoin-knots, lnd, electrs/electrumx, mempool, btcpay-server, nbxplorer, fedimint, fedimint-gateway, nostr-rs-relay, indeedhub, ollama, open-webui
|
||||
|
||||
**Must NOT be on archy-net**: grafana, nextcloud, filebrowser, vaultwarden, bitcoin-ui, lnd-ui, tailscale (host network)
|
||||
|
||||
### Step 5: Health Check Status
|
||||
### Step 7: UFW Forward Policy Check
|
||||
|
||||
Rootless Podman requires `DEFAULT_FORWARD_POLICY="ACCEPT"` in UFW, otherwise container ports are unreachable from LAN.
|
||||
|
||||
```bash
|
||||
grep DEFAULT_FORWARD_POLICY /etc/default/ufw
|
||||
# Must be "ACCEPT", NOT "DROP"
|
||||
# If DROP: containers work locally but NOT from other machines on the network
|
||||
```
|
||||
|
||||
### Step 8: Systemd Service Sandbox Check
|
||||
|
||||
The `archipelago.service` must have specific settings relaxed for rootless Podman:
|
||||
|
||||
```bash
|
||||
# Check critical settings
|
||||
systemctl cat archipelago.service | grep -E "ProtectHome|PrivateTmp|RestrictNamespaces|ReadWritePaths|XDG_RUNTIME_DIR"
|
||||
```
|
||||
|
||||
**Required settings for rootless Podman**:
|
||||
- `ProtectHome=no` — podman stores images in `~/.local/share/containers/`
|
||||
- `PrivateTmp=no` or disabled — podman runtime uses `/tmp/podman-run-1000/`
|
||||
- `RestrictNamespaces=` must NOT be set — rootless podman needs user namespaces
|
||||
- `ReadWritePaths=` must include `/var/lib/archipelago /run/user /tmp`
|
||||
- `Environment=XDG_RUNTIME_DIR=/run/user/1000`
|
||||
|
||||
### Step 9: Health Check Status
|
||||
|
||||
```bash
|
||||
# Containers with health checks — are they passing?
|
||||
for c in $(sudo podman ps --format "{{.Names}}"); do
|
||||
health=$(sudo podman inspect "$c" --format "{{.State.Health.Status}}" 2>/dev/null)
|
||||
for c in $(podman ps --format "{{.Names}}"); do
|
||||
health=$(podman inspect "$c" --format "{{.State.Health.Status}}" 2>/dev/null)
|
||||
if [ -n "$health" ] && [ "$health" != "<no value>" ]; then
|
||||
echo "$c: $health"
|
||||
fi
|
||||
done
|
||||
|
||||
# Containers WITHOUT health checks (gap in monitoring)
|
||||
for c in $(sudo podman ps --format "{{.Names}}"); do
|
||||
hc=$(sudo podman inspect "$c" --format "{{.Config.Healthcheck}}" 2>/dev/null)
|
||||
for c in $(podman ps --format "{{.Names}}"); do
|
||||
hc=$(podman inspect "$c" --format "{{.Config.Healthcheck}}" 2>/dev/null)
|
||||
if [ "$hc" = "<nil>" ] || [ -z "$hc" ]; then
|
||||
echo "NO HEALTHCHECK: $c"
|
||||
fi
|
||||
done
|
||||
```
|
||||
|
||||
### Step 6: Resource & Failure Analysis
|
||||
### Step 10: Resource & Failure Analysis
|
||||
|
||||
```bash
|
||||
# Resource usage
|
||||
sudo podman stats --no-stream --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}"
|
||||
podman stats --no-stream --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}"
|
||||
|
||||
# Recent deaths (last 24h)
|
||||
sudo podman events --filter event=died --since 24h 2>/dev/null | tail -20
|
||||
podman events --filter event=died --since 24h 2>/dev/null | tail -20
|
||||
|
||||
# OOM kills
|
||||
sudo podman ps -a --format "{{.Names}}" | while read c; do
|
||||
oom=$(sudo podman inspect "$c" --format "{{.State.OOMKilled}}" 2>/dev/null)
|
||||
podman ps -a --format "{{.Names}}" | while read c; do
|
||||
oom=$(podman inspect "$c" --format "{{.State.OOMKilled}}" 2>/dev/null)
|
||||
[ "$oom" = "true" ] && echo "OOM KILLED: $c"
|
||||
done
|
||||
|
||||
# Non-zero exits
|
||||
sudo podman ps -a --filter status=exited --format "{{.Names}}\t{{.Status}}"
|
||||
podman ps -a --filter status=exited --format "{{.Names}}\t{{.Status}}"
|
||||
```
|
||||
|
||||
### Step 7: Systemd Integration
|
||||
### Step 11: Systemd Integration
|
||||
|
||||
```bash
|
||||
systemctl is-active archipelago nginx
|
||||
systemctl list-units --type=service | grep -i podman
|
||||
systemctl --user list-units --type=service 2>/dev/null | grep -i podman
|
||||
systemctl list-timers --all | grep -i -E "podman|container|archipelago"
|
||||
```
|
||||
|
||||
### Step 8: Generate Report
|
||||
### Step 12: Generate Report
|
||||
|
||||
Produce a structured report:
|
||||
|
||||
```
|
||||
## Container Diagnostic Report
|
||||
|
||||
### Rootless Podman Status
|
||||
- User: archipelago (UID 1000)
|
||||
- Subuid mapping: [OK/MISSING]
|
||||
- XDG_RUNTIME_DIR: [OK/MISSING]
|
||||
- User linger: [enabled/disabled]
|
||||
- UFW forward policy: [ACCEPT/DROP]
|
||||
|
||||
### Summary
|
||||
- Total containers: X running, Y stopped, Z unhealthy
|
||||
- Port conflicts: [list or "none"]
|
||||
- Missing restart policies: [list or "none"]
|
||||
- Network issues: [list or "none"]
|
||||
- UID mapping issues: [list or "none"]
|
||||
- Health check gaps: [list]
|
||||
|
||||
### Critical Issues (fix immediately)
|
||||
@@ -154,3 +269,7 @@ After diagnosis, suggest running `/podman-fix` for any issues found.
|
||||
## Port Reference
|
||||
|
||||
See `references/port-map.md` for the canonical port assignment table across all 4 layers.
|
||||
|
||||
## UID Mapping Reference
|
||||
|
||||
See `references/uid-mapping.md` for the complete rootless UID mapping table.
|
||||
|
||||
@@ -1,15 +1,31 @@
|
||||
# Common Podman Failure Patterns
|
||||
|
||||
## Rootless Podman Specific Failures
|
||||
|
||||
| Error | Cause | Fix |
|
||||
|-------|-------|-----|
|
||||
| `ERRO[0000] cannot find UID/GID for user` | subuid/subgid not configured | Add `archipelago:100000:65536` to `/etc/subuid` and `/etc/subgid` |
|
||||
| `Error: unshare: operation not permitted` | Systemd `RestrictNamespaces` blocks user namespaces | Remove `RestrictNamespaces=` from `archipelago.service` |
|
||||
| `Error: could not get runtime: creating runtime` | XDG_RUNTIME_DIR not set or /run/user/1000 missing | Set `Environment=XDG_RUNTIME_DIR=/run/user/1000` in service, ensure `loginctl enable-linger archipelago` |
|
||||
| `permission denied` on volume mount | Wrong UID ownership — must use mapped UIDs | `sudo chown -R 100000:100000 /var/lib/archipelago/APP` (see UID mapping table) |
|
||||
| `ERRO[0000] rootless containers not supported` | Podman not configured for rootless | Run `podman system migrate`, check `/etc/subuid` |
|
||||
| `Error: creating container storage: layer not known` | Corrupted rootless storage | `podman system reset` (destroys all containers — last resort) |
|
||||
| `Error: stat /tmp/podman-run-1000/...: no such file` | PrivateTmp=yes in systemd isolates /tmp | Set `PrivateTmp=no` in `archipelago.service` |
|
||||
| Container ports unreachable from LAN | UFW DEFAULT_FORWARD_POLICY="DROP" | Change to "ACCEPT" in `/etc/default/ufw`, then `sudo ufw reload` |
|
||||
| `Error: error creating network namespace` | Systemd `SystemCallFilter` blocks clone/unshare | Remove `SystemCallFilter=` from `archipelago.service` |
|
||||
| Containers lose network after service restart | podman runtime dir in /tmp cleaned | Ensure `PrivateTmp=no` so /tmp/podman-run-1000/ persists |
|
||||
|
||||
## Container Won't Start
|
||||
|
||||
| Error | Cause | Fix |
|
||||
|-------|-------|-----|
|
||||
| `exec format error` | Binary built on wrong arch | Rebuild on the Linux server |
|
||||
| `address already in use` | Port conflict | `ss -tlnp \| grep :PORT` to find offender |
|
||||
| `permission denied` | Missing capability or read-only root | Check `get_app_capabilities()`, add tmpfs |
|
||||
| `permission denied` | Missing capability, wrong UID ownership, or read-only root | Check capabilities, check volume ownership with mapped UID, add tmpfs |
|
||||
| `OCI runtime error` | Corrupt container state | `podman rm -f NAME && recreate` |
|
||||
| `image not known` | Image not pulled | `podman pull IMAGE:TAG` |
|
||||
| `no such network` | Network missing | `podman network create archy-net` |
|
||||
| `Error: netavark: ...subnet overlap` | Network CIDR conflict | `podman network rm archy-net && podman network create archy-net` |
|
||||
|
||||
## Container Starts But App Unreachable
|
||||
|
||||
@@ -20,6 +36,7 @@
|
||||
| Port mapped but refused | Container logs | App crashing internally — check logs |
|
||||
| Works sometimes | Resources | Check OOM kills, CPU, disk space |
|
||||
| 502 Bad Gateway | Nginx→Container | Wrong port in proxy_pass or container restarted |
|
||||
| Works locally but not from LAN | UFW forward policy | Set `DEFAULT_FORWARD_POLICY="ACCEPT"` in `/etc/default/ufw` |
|
||||
|
||||
## Container Keeps Dying
|
||||
|
||||
@@ -29,6 +46,8 @@
|
||||
| Dies after minutes | OOM killed | Increase `--memory` limit |
|
||||
| Dies when dep restarts | No restart policy | Add `--restart unless-stopped` |
|
||||
| Crash loop | Repeated crash | Fix root cause, don't just restart |
|
||||
| Exit code 127 | Missing binary in container | Wrong image tag or corrupted image — re-pull |
|
||||
| Exit code 137 | Killed by OOM or signal | Check `dmesg` for OOM kill, check `podman inspect` for OOMKilled |
|
||||
|
||||
## Network Issues
|
||||
|
||||
@@ -37,6 +56,20 @@
|
||||
| Can't resolve container names | Not on archy-net | Recreate with `--network=archy-net` |
|
||||
| Can't reach internet | DNS missing | Add `--dns 1.1.1.1` |
|
||||
| Container-to-container timeout | Different networks | Put both on same network |
|
||||
| Bitcoin RPC refused from container | rpcallowip wrong subnet | Use `rpcallowip=0.0.0.0/0` (safe: port mapped, not exposed) |
|
||||
| Old containers can't find new network | Subnet changed (rootful→rootless) | Recreate containers on new archy-net (rootless uses 10.89.x.x) |
|
||||
|
||||
## Volume Permission Patterns (Rootless UID Mapping)
|
||||
|
||||
Formula: **host_uid = 100000 + container_uid**
|
||||
|
||||
| Container UID | Host UID | Apps | Data Directory |
|
||||
|---|---|---|---|
|
||||
| 0 (root) | 100000 | lnd, fedimint, homeassistant, jellyfin, vaultwarden, photoprism, ollama, filebrowser, electrumx, btcpay, immich | `/var/lib/archipelago/{app}` |
|
||||
| 70 | 100070 | postgres (btcpay-db, immich-db, penpot-postgres) | `/var/lib/archipelago/postgres-*` |
|
||||
| 101 | 100101 | bitcoin-knots | `/var/lib/archipelago/bitcoin` |
|
||||
| 472 | 100472 | grafana | `/var/lib/archipelago/grafana` |
|
||||
| 999 | 100999 | MariaDB (mysql-mempool) | `/var/lib/archipelago/mysql-mempool` |
|
||||
|
||||
## Capability Reference
|
||||
|
||||
@@ -47,9 +80,23 @@
|
||||
| DAC_OVERRIDE | nextcloud, homeassistant, btcpay | Can't access cross-UID files |
|
||||
| FOWNER | bitcoin-knots, lnd, fedimint | Can't modify data dir perms |
|
||||
| NET_BIND_SERVICE | nginx-proxy-manager, vaultwarden | Can't bind ports <1024 |
|
||||
| NET_ADMIN + NET_RAW | tailscale | Can't create TUN device or manage routes |
|
||||
|
||||
## Read-Only Safe Apps
|
||||
|
||||
Only these 8 apps can run with `--read-only`: searxng, grafana, filebrowser, electrs, nostr-rs-relay, ollama, indeedhub
|
||||
Only these apps can run with `--read-only` + tmpfs: searxng, grafana, filebrowser, electrumx, mempool-electrs, electrs, nostr-rs-relay, ollama, indeedhub
|
||||
|
||||
All others need writable root or will fail silently.
|
||||
|
||||
## Systemd Sandbox Requirements for Rootless Podman
|
||||
|
||||
These systemd service settings MUST be configured for rootless Podman to work:
|
||||
|
||||
| Setting | Required Value | Why |
|
||||
|---------|---------------|-----|
|
||||
| `ProtectHome=` | `no` | Podman stores images in `~/.local/share/containers/` |
|
||||
| `PrivateTmp=` | `no` | Podman runtime lives in `/tmp/podman-run-1000/` |
|
||||
| `RestrictNamespaces=` | NOT SET | Rootless podman creates user namespaces |
|
||||
| `SystemCallFilter=` | NOT SET | Rootless podman needs clone/unshare syscalls |
|
||||
| `ReadWritePaths=` | Include `/var/lib/archipelago /run/user /tmp /etc/containers /var/lib/containers /run/containers` | Volume data + podman runtime paths |
|
||||
| `Environment=` | `XDG_RUNTIME_DIR=/run/user/1000` | Podman socket location |
|
||||
|
||||
93
.claude/skills/podman-doctor/references/uid-mapping.md
Normal file
93
.claude/skills/podman-doctor/references/uid-mapping.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# Rootless Podman UID Mapping Reference
|
||||
|
||||
## How Rootless UID Mapping Works
|
||||
|
||||
When Podman runs as the `archipelago` user (UID 1000), container processes don't run as their "apparent" UID on the host. Instead, Linux user namespaces remap UIDs.
|
||||
|
||||
**Mapping formula**: `host_uid = 100000 + container_uid`
|
||||
|
||||
This is configured in `/etc/subuid` and `/etc/subgid`:
|
||||
```
|
||||
archipelago:100000:65536
|
||||
```
|
||||
|
||||
This means:
|
||||
- Container UID 0 (root inside container) → Host UID 100000 (unprivileged on host)
|
||||
- Container UID 70 (postgres) → Host UID 100070
|
||||
- Container UID 101 (bitcoin) → Host UID 100101
|
||||
- etc.
|
||||
|
||||
## Why This Matters
|
||||
|
||||
Volume directories (bind mounts) on the host must be owned by the **mapped** UID, not the container UID. If Bitcoin runs as UID 101 inside its container, the host directory must be owned by UID 100101.
|
||||
|
||||
If ownership is wrong, the container gets `permission denied` when trying to read/write its data.
|
||||
|
||||
## Complete UID Mapping Table
|
||||
|
||||
| Container UID | Host UID | Containers | Fix Command |
|
||||
|---|---|---|---|
|
||||
| 0 (root) | 100000 | lnd, fedimint, fedimint-gateway, homeassistant, jellyfin, vaultwarden, photoprism, ollama, filebrowser, electrumx, btcpay-server, nbxplorer, immich, nostr-rs-relay, strfry, nextcloud, searxng, onlyoffice, tailscale, uptime-kuma | `sudo chown -R 100000:100000 /var/lib/archipelago/{app}` |
|
||||
| 70 | 100070 | postgres (btcpay-db, immich-db, penpot-postgres) | `sudo chown -R 100070:100070 /var/lib/archipelago/postgres-*` |
|
||||
| 101 | 100101 | bitcoin-knots, bitcoin-core | `sudo chown -R 100101:100101 /var/lib/archipelago/bitcoin` |
|
||||
| 472 | 100472 | grafana | `sudo chown -R 100472:100472 /var/lib/archipelago/grafana` |
|
||||
| 999 | 100999 | MariaDB (mysql-mempool) | `sudo chown -R 100999:100999 /var/lib/archipelago/mysql-mempool` |
|
||||
|
||||
## How to Find a Container's UID
|
||||
|
||||
If you encounter a new container with permission issues:
|
||||
|
||||
```bash
|
||||
# Check what user the container runs as
|
||||
podman inspect CONTAINER_NAME --format "{{.Config.User}}"
|
||||
|
||||
# If empty, it runs as root (UID 0) → host UID 100000
|
||||
|
||||
# If it shows a username, find the UID inside the image
|
||||
podman run --rm IMAGE_NAME id
|
||||
|
||||
# Then calculate: host_uid = 100000 + container_uid
|
||||
```
|
||||
|
||||
## Fix Script
|
||||
|
||||
Run this after any fresh install, migration, or when containers have permission errors:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Fix all rootless podman volume ownership
|
||||
|
||||
# UID 0 → 100000 (most containers)
|
||||
for dir in lnd fedimint fedimint-gateway homeassistant jellyfin vaultwarden photoprism \
|
||||
ollama filebrowser electrumx btcpay nbxplorer immich nostr-rs-relay nextcloud \
|
||||
searxng onlyoffice uptime-kuma; do
|
||||
[ -d "/var/lib/archipelago/$dir" ] && sudo chown -R 100000:100000 "/var/lib/archipelago/$dir"
|
||||
done
|
||||
|
||||
# UID 101 → 100101 (Bitcoin)
|
||||
[ -d "/var/lib/archipelago/bitcoin" ] && sudo chown -R 100101:100101 /var/lib/archipelago/bitcoin
|
||||
|
||||
# UID 70 → 100070 (PostgreSQL)
|
||||
for dir in /var/lib/archipelago/postgres-* /var/lib/archipelago/btcpay-db /var/lib/archipelago/immich-db; do
|
||||
[ -d "$dir" ] && sudo chown -R 100070:100070 "$dir"
|
||||
done
|
||||
|
||||
# UID 999 → 100999 (MariaDB)
|
||||
[ -d "/var/lib/archipelago/mysql-mempool" ] && sudo chown -R 100999:100999 /var/lib/archipelago/mysql-mempool
|
||||
|
||||
# UID 472 → 100472 (Grafana)
|
||||
[ -d "/var/lib/archipelago/grafana" ] && sudo chown -R 100472:100472 /var/lib/archipelago/grafana
|
||||
```
|
||||
|
||||
## Rootful vs Rootless Comparison
|
||||
|
||||
| Aspect | Rootful (old) | Rootless (current) |
|
||||
|--------|---------------|-------------------|
|
||||
| Podman command | `sudo podman` | `podman` (as archipelago user) |
|
||||
| Container storage | `/var/lib/containers/storage` | `~/.local/share/containers/storage` |
|
||||
| Container subnet | `10.88.0.0/16` | `10.89.0.0/16` |
|
||||
| Volume ownership | Container UID directly | Mapped UID (100000 + container_uid) |
|
||||
| Requires root? | Yes | No (except fixing volume ownership) |
|
||||
| XDG_RUNTIME_DIR | Not needed | Required: `/run/user/1000` |
|
||||
| User lingering | Not needed | Required: `loginctl enable-linger` |
|
||||
| Systemd restrictions | All can be enabled | Must disable: RestrictNamespaces, SystemCallFilter |
|
||||
@@ -2,19 +2,24 @@
|
||||
name: podman-fix
|
||||
description: >
|
||||
Fix Podman container issues on Archipelago — restart failed containers, repair port bindings,
|
||||
fix network connectivity, add missing restart policies, and resolve config drift.
|
||||
fix network connectivity, add missing restart policies, fix rootless UID mapping, and resolve
|
||||
config drift. Handles rootless Podman (user: archipelago, UID 1000, subuid 100000:65536).
|
||||
Use when asked to "fix container", "restart app", "fix port mapping", "container not working",
|
||||
"app won't start", "fix podman", "repair container", "container down", or after /podman-doctor
|
||||
identifies issues to fix.
|
||||
"app won't start", "fix podman", "repair container", "container down", "permission denied",
|
||||
or after /podman-doctor identifies issues to fix.
|
||||
allowed-tools: Bash Read Edit Write Glob Grep
|
||||
---
|
||||
|
||||
# Podman Fix — Container Remediation
|
||||
|
||||
Targeted fix workflow for Podman container issues on Archipelago. Given a specific problem (from /podman-doctor or user report), diagnose the root cause and fix it.
|
||||
Targeted fix workflow for **rootless Podman** container issues on Archipelago. Given a specific problem (from /podman-doctor or user report), diagnose the root cause and fix it.
|
||||
|
||||
**SSH command**: `ssh -i ~/.ssh/archipelago-deploy archipelago@192.168.1.228`
|
||||
|
||||
> **ROOTLESS PODMAN**: All `podman` commands run as the `archipelago` user — NO sudo.
|
||||
> Only use `sudo` for: chown on volume directories, UFW changes, systemd service edits, nginx reload.
|
||||
> Container UIDs are mapped via subuid: container UID N → host UID (100000 + N).
|
||||
|
||||
If $ARGUMENTS is provided, fix that specific app/issue. Otherwise ask what needs fixing.
|
||||
|
||||
## Fix Procedures
|
||||
@@ -23,21 +28,22 @@ If $ARGUMENTS is provided, fix that specific app/issue. Otherwise ask what needs
|
||||
|
||||
```bash
|
||||
# Check why it stopped
|
||||
sudo podman logs --tail 50 CONTAINER_NAME
|
||||
sudo podman inspect CONTAINER_NAME --format "{{.State.ExitCode}} {{.State.Error}}"
|
||||
podman logs --tail 50 CONTAINER_NAME
|
||||
podman inspect CONTAINER_NAME --format "{{.State.ExitCode}} {{.State.Error}}"
|
||||
|
||||
# If clean exit or crash — just restart
|
||||
sudo podman start CONTAINER_NAME
|
||||
podman start CONTAINER_NAME
|
||||
|
||||
# If corrupt state — remove and recreate
|
||||
sudo podman rm -f CONTAINER_NAME
|
||||
podman rm -f CONTAINER_NAME
|
||||
# Then recreate using the install flow (trigger from UI or re-run creation command)
|
||||
```
|
||||
|
||||
**If container keeps crashing**: check logs for the actual error. Common causes:
|
||||
**If container keeps crashing**, check logs for the actual error. Common causes:
|
||||
- Missing config file → check if volume mount has the config
|
||||
- Wrong permissions → `chown -R` the data directory
|
||||
- Wrong permissions → fix UID mapping (see Fix 8 below)
|
||||
- Dependency not ready → start dependency first, wait, then start this container
|
||||
- Exit code 127 → missing binary in container image, re-pull the image
|
||||
|
||||
### Fix 2: Missing Restart Policy
|
||||
|
||||
@@ -45,14 +51,14 @@ The most common uptime killer. Fix for ALL containers at once:
|
||||
|
||||
```bash
|
||||
# Fix a single container
|
||||
sudo podman update --restart unless-stopped CONTAINER_NAME
|
||||
podman update --restart unless-stopped CONTAINER_NAME
|
||||
|
||||
# Fix ALL containers that have no restart policy
|
||||
for c in $(sudo podman ps -a --format "{{.Names}}"); do
|
||||
policy=$(sudo podman inspect "$c" --format "{{.HostConfig.RestartPolicy.Name}}")
|
||||
for c in $(podman ps -a --format "{{.Names}}"); do
|
||||
policy=$(podman inspect "$c" --format "{{.HostConfig.RestartPolicy.Name}}")
|
||||
if [ "$policy" = "no" ] || [ -z "$policy" ]; then
|
||||
echo "Fixing restart policy for: $c"
|
||||
sudo podman update --restart unless-stopped "$c"
|
||||
podman update --restart unless-stopped "$c"
|
||||
fi
|
||||
done
|
||||
```
|
||||
@@ -66,23 +72,24 @@ done
|
||||
#### Port conflict (address already in use)
|
||||
```bash
|
||||
# Find what's using the port
|
||||
sudo ss -tlnp | grep :PORT_NUMBER
|
||||
ss -tlnp | grep :PORT_NUMBER
|
||||
|
||||
# If it's another container, either change one's port or stop the conflicting one
|
||||
sudo podman stop CONFLICTING_CONTAINER
|
||||
podman stop CONFLICTING_CONTAINER
|
||||
|
||||
# If it's a host process
|
||||
sudo kill PID # or stop the service
|
||||
# If it's a host process (e.g., system tor vs container tor)
|
||||
sudo systemctl stop tor # Stop system service if container needs the port
|
||||
sudo systemctl disable tor
|
||||
```
|
||||
|
||||
#### Port not mapped (container running but port unreachable)
|
||||
```bash
|
||||
# Check current port mappings
|
||||
sudo podman port CONTAINER_NAME
|
||||
podman port CONTAINER_NAME
|
||||
|
||||
# Can't add ports to running container — must recreate
|
||||
sudo podman stop CONTAINER_NAME
|
||||
sudo podman rm CONTAINER_NAME
|
||||
podman stop CONTAINER_NAME
|
||||
podman rm CONTAINER_NAME
|
||||
# Recreate with correct -p flags (use the Rust install flow or manual podman run)
|
||||
```
|
||||
|
||||
@@ -124,35 +131,51 @@ Edit `neode-ui/src/stores/appLauncher.ts`:
|
||||
#### Container not on archy-net (can't resolve other containers)
|
||||
```bash
|
||||
# Connect to archy-net without recreating
|
||||
sudo podman network connect archy-net CONTAINER_NAME
|
||||
podman network connect archy-net CONTAINER_NAME
|
||||
|
||||
# Verify
|
||||
sudo podman inspect CONTAINER_NAME --format "{{.NetworkSettings.Networks}}"
|
||||
podman inspect CONTAINER_NAME --format "{{.NetworkSettings.Networks}}"
|
||||
```
|
||||
|
||||
#### archy-net doesn't exist
|
||||
```bash
|
||||
sudo podman network create archy-net
|
||||
podman network create archy-net
|
||||
# Then reconnect all containers that need it
|
||||
```
|
||||
|
||||
#### DNS not working inside container
|
||||
```bash
|
||||
# Test DNS from inside container
|
||||
sudo podman exec CONTAINER_NAME nslookup bitcoin-knots 2>/dev/null || \
|
||||
sudo podman exec CONTAINER_NAME ping -c1 bitcoin-knots
|
||||
podman exec CONTAINER_NAME nslookup bitcoin-knots 2>/dev/null || \
|
||||
podman exec CONTAINER_NAME ping -c1 bitcoin-knots
|
||||
|
||||
# If DNS fails, check the container's resolv.conf
|
||||
podman exec CONTAINER_NAME cat /etc/resolv.conf
|
||||
|
||||
# If DNS fails, recreate container with explicit DNS
|
||||
# Add --dns 1.1.1.1 to the podman run command
|
||||
```
|
||||
|
||||
#### Container subnet changed (rootful → rootless migration)
|
||||
```bash
|
||||
# Old rootful subnet: 10.88.0.0/16
|
||||
# New rootless subnet: 10.89.0.0/16
|
||||
# Bitcoin RPC rpcallowip must be updated if using subnet-specific allowlist
|
||||
|
||||
# Check current archy-net subnet
|
||||
podman network inspect archy-net --format "{{range .Subnets}}{{.Subnet}}{{end}}"
|
||||
|
||||
# If Bitcoin RPC refuses connections from containers:
|
||||
# Update bitcoin.conf rpcallowip to 0.0.0.0/0 (safe: only accessible via port mapping)
|
||||
```
|
||||
|
||||
### Fix 5: Health Check Issues
|
||||
|
||||
#### Add missing health check to running container
|
||||
Can't add to running container — must recreate with health check flags:
|
||||
```bash
|
||||
# Example for a web app
|
||||
sudo podman run ... \
|
||||
podman run ... \
|
||||
--health-cmd "curl -f http://localhost:PORT/health || exit 1" \
|
||||
--health-interval 30s \
|
||||
--health-timeout 5s \
|
||||
@@ -164,10 +187,10 @@ sudo podman run ... \
|
||||
#### Fix unhealthy container
|
||||
```bash
|
||||
# See what the health check is actually running
|
||||
sudo podman inspect CONTAINER_NAME --format "{{.Config.Healthcheck.Test}}"
|
||||
podman inspect CONTAINER_NAME --format "{{.Config.Healthcheck.Test}}"
|
||||
|
||||
# Run the health check manually to see the error
|
||||
sudo podman exec CONTAINER_NAME HEALTH_CHECK_COMMAND
|
||||
podman exec CONTAINER_NAME HEALTH_CHECK_COMMAND
|
||||
|
||||
# Common fixes:
|
||||
# - curl not installed in container → use wget or nc instead
|
||||
@@ -179,13 +202,10 @@ sudo podman exec CONTAINER_NAME HEALTH_CHECK_COMMAND
|
||||
|
||||
```bash
|
||||
# Check what capabilities container has
|
||||
sudo podman inspect CONTAINER_NAME --format "{{.HostConfig.CapAdd}}"
|
||||
podman inspect CONTAINER_NAME --format "{{.HostConfig.CapAdd}}"
|
||||
|
||||
# If missing required caps, must recreate with correct --cap-add flags
|
||||
# Refer to the capability reference in /podman-doctor references
|
||||
|
||||
# Fix data directory permissions
|
||||
sudo chown -R 1000:1000 /var/lib/archipelago/APP_NAME/
|
||||
```
|
||||
|
||||
### Fix 7: Full Config Consistency Fix
|
||||
@@ -199,12 +219,108 @@ When port map is inconsistent across layers, fix ALL layers:
|
||||
5. **Deploy**: `./scripts/deploy-to-target.sh --live`
|
||||
6. **Verify**: `curl -I http://192.168.1.228/app/APP_ID/`
|
||||
|
||||
### Fix 8: Rootless UID Mapping (Permission Denied on Volumes)
|
||||
|
||||
This is the #1 rootless-specific issue. Container UIDs are remapped by user namespaces.
|
||||
|
||||
**Formula**: `host_uid = 100000 + container_uid`
|
||||
|
||||
```bash
|
||||
# Fix UID 0 containers (most apps — run as root inside, mapped to 100000 on host)
|
||||
sudo chown -R 100000:100000 /var/lib/archipelago/APP_NAME
|
||||
|
||||
# Fix Bitcoin (container UID 101 → host UID 100101)
|
||||
sudo chown -R 100101:100101 /var/lib/archipelago/bitcoin
|
||||
|
||||
# Fix PostgreSQL (container UID 70 → host UID 100070)
|
||||
sudo chown -R 100070:100070 /var/lib/archipelago/postgres-APP_NAME
|
||||
|
||||
# Fix Grafana (container UID 472 → host UID 100472)
|
||||
sudo chown -R 100472:100472 /var/lib/archipelago/grafana
|
||||
|
||||
# Fix MariaDB (container UID 999 → host UID 100999)
|
||||
sudo chown -R 100999:100999 /var/lib/archipelago/mysql-mempool
|
||||
```
|
||||
|
||||
**How to find the right UID for a new container:**
|
||||
```bash
|
||||
# Check what user the container image runs as
|
||||
podman inspect IMAGE_NAME --format "{{.Config.User}}"
|
||||
# If empty = root (UID 0) → host UID 100000
|
||||
# If number → host UID = 100000 + that number
|
||||
# If username → run: podman run --rm IMAGE_NAME id
|
||||
```
|
||||
|
||||
After fixing ownership, restart the container:
|
||||
```bash
|
||||
podman restart CONTAINER_NAME
|
||||
```
|
||||
|
||||
### Fix 9: UFW Forward Policy (LAN Access Broken)
|
||||
|
||||
If containers work locally but not from other machines on the network:
|
||||
|
||||
```bash
|
||||
# Check current policy
|
||||
grep DEFAULT_FORWARD_POLICY /etc/default/ufw
|
||||
|
||||
# Fix: change DROP to ACCEPT
|
||||
sudo sed -i 's/DEFAULT_FORWARD_POLICY="DROP"/DEFAULT_FORWARD_POLICY="ACCEPT"/' /etc/default/ufw
|
||||
sudo ufw reload
|
||||
```
|
||||
|
||||
### Fix 10: Systemd Sandbox Too Restrictive
|
||||
|
||||
If the Rust backend can't scan/manage containers after a systemd update:
|
||||
|
||||
```bash
|
||||
# Check what's blocked
|
||||
sudo journalctl -u archipelago --since "10 min ago" | grep -i "denied\|permission\|namespace\|syscall"
|
||||
|
||||
# The archipelago.service MUST have these for rootless podman:
|
||||
# ProtectHome=no
|
||||
# PrivateTmp=no (or disabled)
|
||||
# RestrictNamespaces= (NOT SET — don't restrict)
|
||||
# SystemCallFilter= (NOT SET — don't filter)
|
||||
# ReadWritePaths=/var/lib/archipelago /etc/containers /var/lib/containers /run/containers /run/user /tmp
|
||||
# Environment=XDG_RUNTIME_DIR=/run/user/1000
|
||||
```
|
||||
|
||||
Edit the service file:
|
||||
```bash
|
||||
sudo systemctl edit archipelago.service
|
||||
# Add overrides, then:
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl restart archipelago
|
||||
```
|
||||
|
||||
### Fix 11: Stale Podman Processes
|
||||
|
||||
If `podman ps` hangs or is very slow:
|
||||
|
||||
```bash
|
||||
# Kill stuck podman processes (>10 of them = something is wrong)
|
||||
stuck=$(pgrep -c -f "podman ps\|podman stats" 2>/dev/null || echo 0)
|
||||
if [ "$stuck" -gt 10 ]; then
|
||||
pkill -f "podman ps\|podman stats"
|
||||
echo "Killed $stuck stuck podman processes"
|
||||
fi
|
||||
|
||||
# Kill orphaned conmon processes holding ports
|
||||
for pid in $(pgrep conmon); do
|
||||
container=$(cat /proc/$pid/cmdline 2>/dev/null | tr '\0' ' ' | grep -oP '(?<=--cid )\S+')
|
||||
if [ -n "$container" ] && ! podman ps -a --format "{{.ID}}" | grep -q "${container:0:12}"; then
|
||||
kill "$pid" 2>/dev/null && echo "Killed orphan conmon $pid"
|
||||
fi
|
||||
done
|
||||
```
|
||||
|
||||
## After Fixing
|
||||
|
||||
Always verify the fix:
|
||||
```bash
|
||||
# Container running?
|
||||
sudo podman ps --filter name=CONTAINER_NAME
|
||||
podman ps --filter name=CONTAINER_NAME
|
||||
|
||||
# Port reachable?
|
||||
curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:PORT/
|
||||
@@ -213,7 +329,10 @@ curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:PORT/
|
||||
curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1/app/APP_ID/
|
||||
|
||||
# Health check passing?
|
||||
sudo podman inspect CONTAINER_NAME --format "{{.State.Health.Status}}"
|
||||
podman inspect CONTAINER_NAME --format "{{.State.Health.Status}}"
|
||||
|
||||
# Volume permissions correct? (rootless check)
|
||||
podman exec CONTAINER_NAME ls -la /data/ 2>/dev/null || echo "Check container data path"
|
||||
```
|
||||
|
||||
Run `/podman-doctor` again to confirm all issues are resolved.
|
||||
|
||||
@@ -3,7 +3,8 @@ name: podman-uptime
|
||||
description: >
|
||||
Ensure 100% container uptime on Archipelago. Sets up systemd watchdog timers, verifies
|
||||
restart policies, creates health check monitors, and configures auto-recovery for all
|
||||
containers. Use when asked to "ensure uptime", "containers keep dying", "auto-restart",
|
||||
containers. Handles rootless Podman (user: archipelago, UID 1000, subuid 100000:65536).
|
||||
Use when asked to "ensure uptime", "containers keep dying", "auto-restart",
|
||||
"watchdog", "container monitoring", "uptime guarantee", "keep containers running",
|
||||
"survive reboot", or to harden container reliability.
|
||||
allowed-tools: Bash Read Edit Write Glob Grep
|
||||
@@ -15,6 +16,31 @@ Ensures every Archipelago container survives reboots, recovers from crashes, and
|
||||
|
||||
**SSH command**: `ssh -i ~/.ssh/archipelago-deploy archipelago@192.168.1.228`
|
||||
|
||||
> **ROOTLESS PODMAN**: All `podman` commands run as the `archipelago` user — NO sudo.
|
||||
> Only use `sudo` for: systemd unit files, chown on volumes, UFW changes.
|
||||
> The archipelago user runs containers directly via user namespaces.
|
||||
|
||||
## Prerequisites for Rootless Uptime
|
||||
|
||||
Before setting up uptime infrastructure, verify rootless Podman basics are working:
|
||||
|
||||
```bash
|
||||
# Must be the archipelago user
|
||||
whoami # archipelago
|
||||
|
||||
# User lingering must be enabled (keeps user services running after logout)
|
||||
ls /var/lib/systemd/linger/ | grep archipelago || sudo loginctl enable-linger archipelago
|
||||
|
||||
# XDG_RUNTIME_DIR must be set
|
||||
echo $XDG_RUNTIME_DIR # /run/user/1000
|
||||
|
||||
# Subuid/subgid must be configured
|
||||
grep archipelago /etc/subuid # archipelago:100000:65536
|
||||
|
||||
# UFW forward policy must be ACCEPT (for LAN access to containers)
|
||||
grep DEFAULT_FORWARD_POLICY /etc/default/ufw # Must be "ACCEPT"
|
||||
```
|
||||
|
||||
## Layer 1: Restart Policies (Survive Reboots)
|
||||
|
||||
Every container MUST have `--restart unless-stopped`. This is non-negotiable.
|
||||
@@ -23,28 +49,31 @@ Every container MUST have `--restart unless-stopped`. This is non-negotiable.
|
||||
|
||||
```bash
|
||||
# Audit
|
||||
for c in $(sudo podman ps -a --format "{{.Names}}"); do
|
||||
policy=$(sudo podman inspect "$c" --format "{{.HostConfig.RestartPolicy.Name}}")
|
||||
for c in $(podman ps -a --format "{{.Names}}"); do
|
||||
policy=$(podman inspect "$c" --format "{{.HostConfig.RestartPolicy.Name}}")
|
||||
echo "$c: $policy"
|
||||
done
|
||||
|
||||
# Fix any with "no" or empty policy
|
||||
for c in $(sudo podman ps -a --format "{{.Names}}"); do
|
||||
policy=$(sudo podman inspect "$c" --format "{{.HostConfig.RestartPolicy.Name}}")
|
||||
for c in $(podman ps -a --format "{{.Names}}"); do
|
||||
policy=$(podman inspect "$c" --format "{{.HostConfig.RestartPolicy.Name}}")
|
||||
if [ "$policy" = "no" ] || [ -z "$policy" ]; then
|
||||
echo "Fixing: $c"
|
||||
sudo podman update --restart unless-stopped "$c"
|
||||
podman update --restart unless-stopped "$c"
|
||||
fi
|
||||
done
|
||||
```
|
||||
|
||||
### Ensure podman auto-starts containers on boot
|
||||
|
||||
```bash
|
||||
# Enable podman-restart service (restarts containers with restart policy on boot)
|
||||
sudo systemctl enable podman-restart.service 2>/dev/null || true
|
||||
For rootless Podman, containers with restart policies are auto-started by `podman-restart` as a **user** service:
|
||||
|
||||
# If podman-restart doesn't exist, create it
|
||||
```bash
|
||||
# Enable the rootless podman-restart user service
|
||||
systemctl --user enable podman-restart.service 2>/dev/null
|
||||
|
||||
# If the user service doesn't exist, create a system-level one
|
||||
# (runs as archipelago user via User= directive)
|
||||
cat <<'EOF' | sudo tee /etc/systemd/system/podman-restart.service
|
||||
[Unit]
|
||||
Description=Podman Start All Containers With Restart Policy
|
||||
@@ -53,8 +82,12 @@ Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=archipelago
|
||||
Group=archipelago
|
||||
Environment=XDG_RUNTIME_DIR=/run/user/1000
|
||||
ExecStart=/usr/bin/podman start --all --filter restart-policy=unless-stopped
|
||||
RemainAfterExit=yes
|
||||
TimeoutStartSec=300
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -73,27 +106,31 @@ Create a systemd timer that checks container health every 2 minutes and restarts
|
||||
```bash
|
||||
cat <<'SCRIPT' | sudo tee /usr/local/bin/archipelago-container-watchdog.sh
|
||||
#!/bin/bash
|
||||
# Archipelago Container Watchdog
|
||||
# Checks all containers and restarts any that are stopped or unhealthy
|
||||
# Archipelago Container Watchdog (Rootless Podman)
|
||||
# Runs as archipelago user — NO sudo for podman commands
|
||||
|
||||
LOG_TAG="container-watchdog"
|
||||
|
||||
# Run podman as the archipelago user with correct XDG path
|
||||
export XDG_RUNTIME_DIR=/run/user/1000
|
||||
PODMAN="/usr/bin/podman"
|
||||
|
||||
# Restart any stopped containers that should be running (have restart policy)
|
||||
for c in $(sudo podman ps -a --filter status=exited --filter restart-policy=unless-stopped --format "{{.Names}}"); do
|
||||
for c in $($PODMAN ps -a --filter status=exited --filter restart-policy=unless-stopped --format "{{.Names}}" 2>/dev/null); do
|
||||
logger -t "$LOG_TAG" "Restarting stopped container: $c"
|
||||
sudo podman start "$c" 2>&1 | logger -t "$LOG_TAG"
|
||||
$PODMAN start "$c" 2>&1 | logger -t "$LOG_TAG"
|
||||
done
|
||||
|
||||
# Restart unhealthy containers
|
||||
for c in $(sudo podman ps --filter health=unhealthy --format "{{.Names}}"); do
|
||||
for c in $($PODMAN ps --filter health=unhealthy --format "{{.Names}}" 2>/dev/null); do
|
||||
logger -t "$LOG_TAG" "Restarting unhealthy container: $c"
|
||||
sudo podman restart "$c" 2>&1 | logger -t "$LOG_TAG"
|
||||
$PODMAN restart "$c" 2>&1 | logger -t "$LOG_TAG"
|
||||
done
|
||||
|
||||
# Check for containers in "created" state (never started)
|
||||
for c in $(sudo podman ps -a --filter status=created --format "{{.Names}}"); do
|
||||
for c in $($PODMAN ps -a --filter status=created --format "{{.Names}}" 2>/dev/null); do
|
||||
logger -t "$LOG_TAG" "Starting created container: $c"
|
||||
sudo podman start "$c" 2>&1 | logger -t "$LOG_TAG"
|
||||
$PODMAN start "$c" 2>&1 | logger -t "$LOG_TAG"
|
||||
done
|
||||
SCRIPT
|
||||
|
||||
@@ -103,7 +140,7 @@ sudo chmod +x /usr/local/bin/archipelago-container-watchdog.sh
|
||||
### Create the systemd timer
|
||||
|
||||
```bash
|
||||
# Service unit
|
||||
# Service unit — runs as archipelago user for rootless podman
|
||||
cat <<'EOF' | sudo tee /etc/systemd/system/archipelago-watchdog.service
|
||||
[Unit]
|
||||
Description=Archipelago Container Watchdog
|
||||
@@ -111,6 +148,9 @@ After=podman-restart.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=archipelago
|
||||
Group=archipelago
|
||||
Environment=XDG_RUNTIME_DIR=/run/user/1000
|
||||
ExecStart=/usr/local/bin/archipelago-container-watchdog.sh
|
||||
EOF
|
||||
|
||||
@@ -150,17 +190,20 @@ Some containers depend on others. The watchdog handles restarts, but initial boo
|
||||
```bash
|
||||
cat <<'SCRIPT' | sudo tee /usr/local/bin/archipelago-ordered-start.sh
|
||||
#!/bin/bash
|
||||
# Ordered container startup for Archipelago
|
||||
# Ordered container startup for Archipelago (Rootless Podman)
|
||||
# Runs as archipelago user — NO sudo for podman commands
|
||||
# Respects dependency chain: bitcoin → electrs/lnd → mempool/btcpay
|
||||
|
||||
LOG_TAG="ordered-start"
|
||||
export XDG_RUNTIME_DIR=/run/user/1000
|
||||
PODMAN="/usr/bin/podman"
|
||||
|
||||
wait_for_container() {
|
||||
local name=$1
|
||||
local max_wait=${2:-60}
|
||||
local waited=0
|
||||
while [ $waited -lt $max_wait ]; do
|
||||
status=$(sudo podman inspect "$name" --format "{{.State.Running}}" 2>/dev/null)
|
||||
status=$($PODMAN inspect "$name" --format "{{.State.Running}}" 2>/dev/null)
|
||||
if [ "$status" = "true" ]; then
|
||||
logger -t "$LOG_TAG" "$name is running"
|
||||
return 0
|
||||
@@ -174,38 +217,45 @@ wait_for_container() {
|
||||
|
||||
# Tier 0: Infrastructure
|
||||
logger -t "$LOG_TAG" "Starting Tier 0: Infrastructure"
|
||||
sudo podman start tailscale 2>/dev/null
|
||||
$PODMAN start tailscale 2>/dev/null
|
||||
|
||||
# Tier 1: Bitcoin (foundation)
|
||||
logger -t "$LOG_TAG" "Starting Tier 1: Bitcoin"
|
||||
sudo podman start bitcoin-knots 2>/dev/null
|
||||
# Tier 1: Databases (must start before services that depend on them)
|
||||
logger -t "$LOG_TAG" "Starting Tier 1: Databases"
|
||||
$PODMAN start mempool-db 2>/dev/null
|
||||
$PODMAN start btcpay-postgres 2>/dev/null
|
||||
$PODMAN start immich_postgres 2>/dev/null
|
||||
sleep 5
|
||||
|
||||
# Tier 2: Bitcoin (foundation for Lightning and explorers)
|
||||
logger -t "$LOG_TAG" "Starting Tier 2: Bitcoin"
|
||||
$PODMAN start bitcoin-knots 2>/dev/null
|
||||
wait_for_container bitcoin-knots 120
|
||||
|
||||
# Tier 2: Bitcoin-dependent services
|
||||
logger -t "$LOG_TAG" "Starting Tier 2: Bitcoin-dependent"
|
||||
sudo podman start electrs 2>/dev/null
|
||||
sudo podman start lnd 2>/dev/null
|
||||
wait_for_container electrs 90
|
||||
# Tier 3: Bitcoin-dependent services
|
||||
logger -t "$LOG_TAG" "Starting Tier 3: Bitcoin-dependent"
|
||||
$PODMAN start electrumx 2>/dev/null
|
||||
$PODMAN start lnd 2>/dev/null
|
||||
wait_for_container electrumx 90
|
||||
wait_for_container lnd 90
|
||||
|
||||
# Tier 3: Services depending on Tier 2
|
||||
logger -t "$LOG_TAG" "Starting Tier 3: Second-order dependencies"
|
||||
sudo podman start mempool-db 2>/dev/null
|
||||
sleep 5
|
||||
sudo podman start mempool 2>/dev/null
|
||||
sudo podman start nbxplorer 2>/dev/null
|
||||
# Tier 4: Services depending on Tier 3
|
||||
logger -t "$LOG_TAG" "Starting Tier 4: Second-order dependencies"
|
||||
$PODMAN start mempool 2>/dev/null
|
||||
$PODMAN start nbxplorer 2>/dev/null
|
||||
sleep 10
|
||||
sudo podman start btcpay-server 2>/dev/null
|
||||
sudo podman start btcpay-postgres 2>/dev/null
|
||||
$PODMAN start btcpay-server 2>/dev/null
|
||||
$PODMAN start fedimint 2>/dev/null
|
||||
$PODMAN start fedimint-gateway 2>/dev/null
|
||||
|
||||
# Tier 4: Independent apps (start all remaining)
|
||||
logger -t "$LOG_TAG" "Starting Tier 4: Independent apps"
|
||||
sudo podman start --all 2>/dev/null
|
||||
# Tier 5: Independent apps (start all remaining)
|
||||
logger -t "$LOG_TAG" "Starting Tier 5: Independent apps"
|
||||
$PODMAN start --all 2>/dev/null
|
||||
|
||||
# Tier 5: UI containers (need parent apps running first)
|
||||
logger -t "$LOG_TAG" "Starting Tier 5: UI containers"
|
||||
sudo podman start bitcoin-ui 2>/dev/null
|
||||
sudo podman start lnd-ui 2>/dev/null
|
||||
# Tier 6: UI containers (need parent apps running first)
|
||||
logger -t "$LOG_TAG" "Starting Tier 6: UI containers"
|
||||
$PODMAN start bitcoin-ui 2>/dev/null
|
||||
$PODMAN start lnd-ui 2>/dev/null
|
||||
$PODMAN start electrs-ui 2>/dev/null
|
||||
|
||||
logger -t "$LOG_TAG" "Startup sequence complete"
|
||||
SCRIPT
|
||||
@@ -216,18 +266,22 @@ sudo chmod +x /usr/local/bin/archipelago-ordered-start.sh
|
||||
### Wire into boot sequence
|
||||
|
||||
```bash
|
||||
# Runs as archipelago user for rootless podman
|
||||
cat <<'EOF' | sudo tee /etc/systemd/system/archipelago-containers.service
|
||||
[Unit]
|
||||
Description=Archipelago Ordered Container Startup
|
||||
After=network-online.target podman.service
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
Before=archipelago.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=archipelago
|
||||
Group=archipelago
|
||||
Environment=XDG_RUNTIME_DIR=/run/user/1000
|
||||
ExecStart=/usr/local/bin/archipelago-ordered-start.sh
|
||||
RemainAfterExit=yes
|
||||
TimeoutStartSec=300
|
||||
TimeoutStartSec=600
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -237,14 +291,45 @@ sudo systemctl daemon-reload
|
||||
sudo systemctl enable archipelago-containers.service
|
||||
```
|
||||
|
||||
## Rootless-Specific Uptime Considerations
|
||||
|
||||
### Volume ownership survives reboots
|
||||
Volume ownership doesn't change on reboot, but if a container image is updated (re-pulled), the new container may run as a different UID. Always verify after image updates:
|
||||
|
||||
```bash
|
||||
# Quick ownership audit after image pull
|
||||
podman inspect CONTAINER_NAME --format "{{.Config.User}}"
|
||||
# Then verify: sudo stat -c '%u:%g' /var/lib/archipelago/APP_NAME
|
||||
# Formula: host_uid = 100000 + container_uid
|
||||
```
|
||||
|
||||
### XDG_RUNTIME_DIR on boot
|
||||
Rootless Podman requires `/run/user/1000` to exist. This is created by `pam_systemd` when the user logs in, or by `loginctl enable-linger`. If it's missing after boot, containers won't start.
|
||||
|
||||
```bash
|
||||
# Verify it exists
|
||||
ls -la /run/user/1000/ || echo "CRITICAL: /run/user/1000 missing — run: sudo loginctl enable-linger archipelago"
|
||||
```
|
||||
|
||||
### Systemd sandbox must not block podman
|
||||
If the archipelago.service sandbox blocks namespace/syscall operations, the Rust backend can't scan containers. See Fix 10 in /podman-fix.
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
After setting up all 3 layers, verify:
|
||||
|
||||
```bash
|
||||
echo "=== Rootless Podman Prerequisites ==="
|
||||
echo "User: $(whoami)"
|
||||
echo "XDG_RUNTIME_DIR: $XDG_RUNTIME_DIR"
|
||||
grep archipelago /etc/subuid | head -1
|
||||
ls /var/lib/systemd/linger/ | grep archipelago && echo "Linger: enabled" || echo "Linger: DISABLED"
|
||||
grep DEFAULT_FORWARD_POLICY /etc/default/ufw
|
||||
|
||||
echo ""
|
||||
echo "=== Layer 1: Restart Policies ==="
|
||||
for c in $(sudo podman ps -a --format "{{.Names}}"); do
|
||||
policy=$(sudo podman inspect "$c" --format "{{.HostConfig.RestartPolicy.Name}}")
|
||||
for c in $(podman ps -a --format "{{.Names}}"); do
|
||||
policy=$(podman inspect "$c" --format "{{.HostConfig.RestartPolicy.Name}}")
|
||||
echo " $c: $policy"
|
||||
done
|
||||
|
||||
@@ -261,11 +346,19 @@ sudo systemctl is-enabled archipelago-watchdog.timer 2>/dev/null || echo "watchd
|
||||
|
||||
echo ""
|
||||
echo "=== Container Health Summary ==="
|
||||
total=$(sudo podman ps -a --format "{{.Names}}" | wc -l)
|
||||
running=$(sudo podman ps --format "{{.Names}}" | wc -l)
|
||||
total=$(podman ps -a --format "{{.Names}}" | wc -l)
|
||||
running=$(podman ps --format "{{.Names}}" | wc -l)
|
||||
stopped=$((total - running))
|
||||
unhealthy=$(sudo podman ps --filter health=unhealthy --format "{{.Names}}" | wc -l)
|
||||
unhealthy=$(podman ps --filter health=unhealthy --format "{{.Names}}" | wc -l)
|
||||
echo " Total: $total | Running: $running | Stopped: $stopped | Unhealthy: $unhealthy"
|
||||
|
||||
echo ""
|
||||
echo "=== Volume Ownership Spot Check ==="
|
||||
for dir in bitcoin lnd grafana; do
|
||||
if [ -d "/var/lib/archipelago/$dir" ]; then
|
||||
echo " $dir: $(stat -c '%u:%g' /var/lib/archipelago/$dir)"
|
||||
fi
|
||||
done
|
||||
```
|
||||
|
||||
## Reboot Test
|
||||
@@ -274,17 +367,20 @@ The ultimate uptime test — reboot the server and verify everything comes back:
|
||||
|
||||
```bash
|
||||
# Before reboot: record running containers
|
||||
sudo podman ps --format "{{.Names}}" | sort > /tmp/before-reboot.txt
|
||||
podman ps --format "{{.Names}}" | sort > /tmp/before-reboot.txt
|
||||
|
||||
# Reboot
|
||||
sudo reboot
|
||||
|
||||
# After reboot (wait ~3 minutes, then SSH back in):
|
||||
sudo podman ps --format "{{.Names}}" | sort > /tmp/after-reboot.txt
|
||||
podman ps --format "{{.Names}}" | sort > /tmp/after-reboot.txt
|
||||
|
||||
# Compare
|
||||
diff /tmp/before-reboot.txt /tmp/after-reboot.txt
|
||||
# Should show no differences
|
||||
|
||||
# Also verify XDG_RUNTIME_DIR survived reboot
|
||||
ls /run/user/1000/ || echo "CRITICAL: lingering not working"
|
||||
```
|
||||
|
||||
## Monitoring
|
||||
@@ -292,18 +388,23 @@ diff /tmp/before-reboot.txt /tmp/after-reboot.txt
|
||||
Check uptime status anytime:
|
||||
```bash
|
||||
# Quick status
|
||||
sudo podman ps -a --format "table {{.Names}}\t{{.Status}}" | sort
|
||||
podman ps -a --format "table {{.Names}}\t{{.Status}}" | sort
|
||||
|
||||
# Watchdog activity
|
||||
sudo journalctl -t container-watchdog --since "24 hours ago" --no-pager
|
||||
|
||||
# Container events (starts, stops, deaths)
|
||||
sudo podman events --since 24h --filter event=start --filter event=stop --filter event=died 2>/dev/null | tail -30
|
||||
podman events --since 24h --filter event=start --filter event=stop --filter event=died 2>/dev/null | tail -30
|
||||
|
||||
# Check for permission denied errors (rootless UID mapping issue)
|
||||
podman ps -a --filter status=exited --format "{{.Names}}" | while read c; do
|
||||
podman logs --tail 5 "$c" 2>&1 | grep -i "permission denied" && echo " ^ UID mapping issue in: $c"
|
||||
done
|
||||
```
|
||||
|
||||
## Integration
|
||||
|
||||
- Run `/podman-doctor` first to identify issues
|
||||
- Run `/podman-fix` for specific container repairs
|
||||
- Run `/podman-doctor` first to identify issues (includes rootless health checks)
|
||||
- Run `/podman-fix` for specific container repairs (includes UID mapping fixes)
|
||||
- Run `/podman-uptime` to set up permanent reliability infrastructure
|
||||
- Add to ISO build: copy watchdog scripts to `image-recipe/configs/` and enable in first-boot
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -72,3 +72,4 @@ loop/loop.log.bak
|
||||
|
||||
# Separate repos nested in tree
|
||||
web/
|
||||
|
||||
|
||||
34
BACKLOG.md
34
BACKLOG.md
@@ -1,34 +0,0 @@
|
||||
# Archipelago Backlog
|
||||
|
||||
## Node Discovery & Spatial Map (Alpha Demo Feature)
|
||||
|
||||
**Priority:** High (needed for live alpha demo)
|
||||
|
||||
### "Find Nodes" — Spatial Node Discovery
|
||||
|
||||
Add a "Find Nodes" button to the Messages tab that opens a modal with an interactive spatial node map.
|
||||
|
||||
**Requirements:**
|
||||
- Visual spatial map showing discovered Archipelago nodes
|
||||
- Each node displays its self-chosen name (pseudonym)
|
||||
- Connection request flow: discover → request → peer approves → connected
|
||||
- Optional locality broadcasting (toggle: share general area or stay anonymous)
|
||||
- Cool, visual, presentation-worthy UI for live alpha demo
|
||||
|
||||
**Onboarding Addition:**
|
||||
- Add "Name your node" step during setup/onboarding
|
||||
- Include privacy guidance: "Use a pseudonym if you want privacy"
|
||||
- Node name is broadcast on the discovery network
|
||||
|
||||
**Technical Notes:**
|
||||
- Builds on existing Nostr-based node discovery (`node-nostr-discover` RPC)
|
||||
- Existing peer system: `node-add-peer`, `node-remove-peer`, `node-list-peers`
|
||||
- Need to add: connection request/approval flow (currently peers are added directly)
|
||||
- Spatial visualization could use force-directed graph or map-based layout
|
||||
- Locality data is optional and coarse-grained (city/region level, never precise)
|
||||
|
||||
---
|
||||
|
||||
## Settings (TBD)
|
||||
|
||||
*User mentioned settings changes needed — details to be clarified.*
|
||||
@@ -1,226 +0,0 @@
|
||||
# Archipelago Build System - Summary
|
||||
|
||||
## ✅ What We Created Today
|
||||
|
||||
### 1. **Complete One-Script Build System** (`build-iso-complete.sh`)
|
||||
- Handles backend compilation (Rust)
|
||||
- Handles frontend build (Vue.js)
|
||||
- Creates bootable ISO image
|
||||
- Supports local and remote builds
|
||||
- Smart artifact caching
|
||||
- Full error checking and validation
|
||||
|
||||
### 2. **Comprehensive Documentation** (`BUILD-GUIDE.md`)
|
||||
- Quick start guide
|
||||
- Detailed build options
|
||||
- Troubleshooting section
|
||||
- Development workflow
|
||||
- CI/CD integration examples
|
||||
|
||||
### 3. **Fixed ISO Auto-Start Issue**
|
||||
- Identified root cause: `read -p` prompt blocking auto-launch
|
||||
- Restored working auto-start logic from previous builds
|
||||
- Menu now launches automatically after 1 second
|
||||
|
||||
## 🚀 How to Use
|
||||
|
||||
### Quick Build
|
||||
```bash
|
||||
# One command - builds everything and creates flashable ISO
|
||||
./build-iso-complete.sh --remote archipelago@192.168.1.228
|
||||
```
|
||||
|
||||
### Flash to USB
|
||||
```bash
|
||||
# After build completes
|
||||
./flash-to-usb.sh /dev/diskN
|
||||
```
|
||||
|
||||
## 📦 What the Build Process Does
|
||||
|
||||
```
|
||||
Source Code
|
||||
│
|
||||
├─→ Backend (Rust) ────→ Binary (10MB)
|
||||
│ ↓
|
||||
├─→ Frontend (Vue) ────→ Assets (5MB)
|
||||
│ ↓
|
||||
└─→ ISO Builder ────────→ Bootable ISO (1.2GB)
|
||||
↓
|
||||
Flash to USB
|
||||
↓
|
||||
Boot & Install
|
||||
```
|
||||
|
||||
### Build Steps
|
||||
|
||||
1. **Backend Compilation** (Rust → Native Binary)
|
||||
- `core/archipelago/` → `image-recipe/build/backend/archipelago`
|
||||
- Can build locally or on remote server
|
||||
- Incremental builds supported
|
||||
|
||||
2. **Frontend Build** (Vue.js → Static Assets)
|
||||
- `neode-ui/` → `image-recipe/build/frontend/`
|
||||
- Includes PWA manifest
|
||||
- Optimized production build
|
||||
|
||||
3. **ISO Creation** (Debian Live)
|
||||
- Downloads base Debian 12 ISO (~352MB)
|
||||
- Integrates backend + frontend
|
||||
- Configures auto-start services
|
||||
- Creates bootable image
|
||||
|
||||
4. **Verification**
|
||||
- Validates all artifacts
|
||||
- Generates MD5 checksum
|
||||
- Reports sizes
|
||||
|
||||
## 🎯 Key Features
|
||||
|
||||
### ✅ Smart Caching
|
||||
- Skip backend build: `--skip-backend`
|
||||
- Skip frontend build: `--skip-frontend`
|
||||
- Debian ISO cached after first download
|
||||
|
||||
### ✅ Remote Build Support
|
||||
- Build on development server (recommended)
|
||||
- Automatically syncs code
|
||||
- Copies artifacts back
|
||||
|
||||
### ✅ Clean Build Option
|
||||
- `--clean` flag removes all artifacts
|
||||
- Ensures fresh compilation
|
||||
|
||||
### ✅ Convenience Scripts
|
||||
- `build-iso-complete.sh` - Main build script
|
||||
- `flash-to-usb.sh` - Quick USB flashing
|
||||
- Auto-generated after each build
|
||||
|
||||
## 📊 Build Time
|
||||
|
||||
| Build Type | Time |
|
||||
|-----------|------|
|
||||
| **First build** (clean) | 15-20 min |
|
||||
| **Incremental** (code changes) | 3-5 min |
|
||||
| **ISO only** (skip backend/frontend) | 2-3 min |
|
||||
|
||||
Breakdown:
|
||||
- Debian ISO download: 5-10 min (first time only)
|
||||
- Backend compile: 3-5 min (first time), ~30sec (incremental)
|
||||
- Frontend build: 1-2 min
|
||||
- ISO creation: 2-3 min
|
||||
|
||||
## 🔧 Development Workflow
|
||||
|
||||
### Making Backend Changes
|
||||
```bash
|
||||
# Edit Rust code in core/archipelago/src/
|
||||
# Then rebuild:
|
||||
./build-iso-complete.sh --remote HOST --skip-frontend
|
||||
```
|
||||
|
||||
### Making Frontend Changes
|
||||
```bash
|
||||
# Edit Vue.js code in neode-ui/src/
|
||||
# Then rebuild:
|
||||
./build-iso-complete.sh --remote HOST --skip-backend
|
||||
```
|
||||
|
||||
### Making Both Changes
|
||||
```bash
|
||||
./build-iso-complete.sh --remote HOST
|
||||
```
|
||||
|
||||
## 📝 Current Build Status
|
||||
|
||||
### ✅ Completed
|
||||
- Build system scripts created
|
||||
- Documentation written
|
||||
- Auto-start issue fixed
|
||||
- README updated
|
||||
|
||||
### 🔄 In Progress
|
||||
- ISO build running on `archipelago@192.168.1.228`
|
||||
- Status: Downloading Debian ISO (34% complete)
|
||||
- ETA: ~10 more minutes
|
||||
|
||||
### ⏳ Next
|
||||
- Test new ISO on Dell OptiPlex
|
||||
- Verify auto-start works
|
||||
- Confirm Web UI accessible
|
||||
|
||||
## 🎯 What This Solves
|
||||
|
||||
### Before
|
||||
- Manual backend compilation
|
||||
- Manual frontend build
|
||||
- Manual file copying
|
||||
- Complex multi-step process
|
||||
- Easy to miss steps
|
||||
- Inconsistent builds
|
||||
|
||||
### After
|
||||
- ✅ One command builds everything
|
||||
- ✅ Automatic artifact management
|
||||
- ✅ Smart caching for speed
|
||||
- ✅ Consistent, reproducible builds
|
||||
- ✅ Clear error messages
|
||||
- ✅ Build verification
|
||||
|
||||
## 📂 File Structure
|
||||
|
||||
```
|
||||
archy/
|
||||
├── build-iso-complete.sh # Main build script (NEW)
|
||||
├── flash-to-usb.sh # USB flash helper (auto-generated)
|
||||
├── BUILD-GUIDE.md # Build documentation (NEW)
|
||||
├── README.md # Updated with build info
|
||||
├── core/archipelago/ # Rust backend
|
||||
├── neode-ui/ # Vue.js frontend
|
||||
└── image-recipe/
|
||||
├── build/ # Build artifacts
|
||||
│ ├── backend/ # Compiled binary
|
||||
│ └── frontend/ # Built assets
|
||||
├── results/ # Final ISO output
|
||||
│ └── archipelago-debian-12-x86_64.iso
|
||||
└── build-debian-iso.sh # ISO creation script
|
||||
```
|
||||
|
||||
## 🔐 Security
|
||||
|
||||
Build system is designed to be secure:
|
||||
- No hardcoded credentials
|
||||
- SSH key authentication recommended
|
||||
- `sudo` only when required (ISO creation)
|
||||
- Build artifacts isolated in `build/` directory
|
||||
- Clean separation of build/source directories
|
||||
|
||||
## 🌟 Future Enhancements
|
||||
|
||||
Potential improvements:
|
||||
- [ ] GitHub Actions CI/CD workflow
|
||||
- [ ] Automatic version numbering
|
||||
- [ ] Build signing for verification
|
||||
- [ ] Multi-architecture support (ARM64)
|
||||
- [ ] Docker-based builds
|
||||
- [ ] Build caching improvements
|
||||
- [ ] Parallel compilation
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
- **BUILD-GUIDE.md** - Comprehensive build guide
|
||||
- **README.md** - Project overview with build quick start
|
||||
- **build-iso-complete.sh** - Inline help with `--help` flag
|
||||
|
||||
## 🎉 Result
|
||||
|
||||
You now have a **production-grade build system** that:
|
||||
- ✅ Builds from source with one command
|
||||
- ✅ Handles all dependencies automatically
|
||||
- ✅ Validates output
|
||||
- ✅ Creates flashable ISO
|
||||
- ✅ Supports iterative development
|
||||
- ✅ Well-documented
|
||||
- ✅ Easy to extend
|
||||
|
||||
**Next step:** Once the current ISO build completes, test it on the Dell OptiPlex to verify auto-start works!
|
||||
193
BUILD-UPDATES.md
193
BUILD-UPDATES.md
@@ -1,193 +0,0 @@
|
||||
# Build System Updates - Feb 1, 2026
|
||||
|
||||
## ✅ Completed
|
||||
|
||||
### 1. **Frontend Deployment**
|
||||
- ✅ Updated Ollama icon to new `ollama.webp`
|
||||
- ✅ Built and deployed to live dev server (`192.168.1.228`)
|
||||
- ✅ Web UI now live at `http://192.168.1.228`
|
||||
|
||||
### 2. **Enhanced ISO Build Script**
|
||||
|
||||
#### Progress Indicators
|
||||
```bash
|
||||
# Before:
|
||||
📥 Downloading Debian Live 12 (Bookworm) Standard ISO...
|
||||
|
||||
# After:
|
||||
📥 Downloading Debian Live 12 (Bookworm) Standard ISO...
|
||||
Size: ~352MB | This is a one-time download (cached for future builds)
|
||||
|
||||
[████████████████████████████████████] 100%
|
||||
|
||||
✅ Downloaded Debian Live ISO (352M)
|
||||
📝 Cached at: /path/to/iso
|
||||
```
|
||||
|
||||
#### Build Timer
|
||||
- Tracks total build time
|
||||
- Shows start time
|
||||
- Reports duration in minutes/seconds
|
||||
|
||||
#### Better Caching
|
||||
- Detects cached ISO with size validation
|
||||
- Shows cache location and size
|
||||
- Handles both macOS and Linux stat commands
|
||||
|
||||
#### Enhanced Build Summary
|
||||
```bash
|
||||
╔════════════════════════════════════════════════════════╗
|
||||
║ 🎉 Build Complete! ║
|
||||
╚════════════════════════════════════════════════════════╝
|
||||
|
||||
📀 ISO File: /path/to/archipelago-debian-12-x86_64.iso
|
||||
📏 Size: 1.2G
|
||||
🔐 MD5: a3f2d8c9e4b1...
|
||||
⏱️ Build Time: 15m 32s
|
||||
🎯 Base: Debian 12 Live (Bookworm)
|
||||
|
||||
🔥 Next Steps:
|
||||
|
||||
1. Flash to USB:
|
||||
cd image-recipe && ./write-usb-dd.sh /dev/diskN
|
||||
|
||||
2. Boot on target device
|
||||
|
||||
3. Auto-login as 'user' with menu launch
|
||||
|
||||
4. Access Web UI at http://<IP>:5678
|
||||
|
||||
5. SSH access: ssh user@<IP> (password: archipelago)
|
||||
```
|
||||
|
||||
### 3. **One-Script Build System**
|
||||
Created `build-iso-complete.sh` with:
|
||||
- ✅ Backend compilation (Rust)
|
||||
- ✅ Frontend build (Vue.js)
|
||||
- ✅ ISO creation
|
||||
- ✅ Local and remote build support
|
||||
- ✅ Smart caching (`--skip-backend`, `--skip-frontend`)
|
||||
- ✅ Clean build option (`--clean`)
|
||||
- ✅ Full validation
|
||||
- ✅ Auto-generated flash script
|
||||
|
||||
### 4. **Documentation**
|
||||
- ✅ `BUILD-GUIDE.md` - Comprehensive build instructions
|
||||
- ✅ `BUILD-SYSTEM-SUMMARY.md` - System overview
|
||||
- ✅ Updated `README.md` with build quick start
|
||||
|
||||
## 🔄 In Progress
|
||||
|
||||
### Current ISO Build
|
||||
- **Status**: Running on `archipelago@192.168.1.228`
|
||||
- **Progress**: Downloading Debian ISO (was at ~45% last check)
|
||||
- **ETA**: ~10-15 minutes total
|
||||
- **Includes**:
|
||||
- Fixed auto-start (no manual prompt)
|
||||
- Latest backend binary
|
||||
- Latest frontend with updated Ollama icon
|
||||
- SSH enabled by default
|
||||
- Enhanced build reporting
|
||||
|
||||
## 📊 Performance Improvements
|
||||
|
||||
### Build Time Breakdown
|
||||
|
||||
| Stage | Before | After (Cached) |
|
||||
|-------|--------|---------------|
|
||||
| ISO Download | 15-20 min | **0 sec** (cached) |
|
||||
| Backend Compile | 3-5 min | 30 sec (incremental) |
|
||||
| Frontend Build | 1-2 min | 1-2 min |
|
||||
| ISO Creation | 2-3 min | 2-3 min |
|
||||
| **Total** | **21-30 min** | **4-6 min** |
|
||||
|
||||
### User Experience
|
||||
|
||||
| Feature | Before | After |
|
||||
|---------|--------|-------|
|
||||
| Build command | Multi-step manual | Single command |
|
||||
| Progress visibility | Silent | Real-time progress bar |
|
||||
| Cache awareness | Hidden | Explicit messages |
|
||||
| Build time | Unknown | Displayed |
|
||||
| Error messages | Generic | Specific with validation |
|
||||
| ISO info | Basic | MD5, size, location |
|
||||
| Next steps | None | Step-by-step guide |
|
||||
|
||||
## 🎯 Benefits
|
||||
|
||||
### For Development
|
||||
1. **Faster iteration**: Skip unchanged components
|
||||
2. **Clear feedback**: Know exactly what's building
|
||||
3. **Reproducible builds**: Same command every time
|
||||
4. **Easy debugging**: Clear error messages
|
||||
|
||||
### For Production
|
||||
1. **Reliable**: Validated downloads and builds
|
||||
2. **Documented**: Complete build summary
|
||||
3. **Traceable**: MD5 checksums for verification
|
||||
4. **Automated**: No manual steps
|
||||
|
||||
## 📝 Usage Examples
|
||||
|
||||
### Quick Build (Using Cache)
|
||||
```bash
|
||||
./build-iso-complete.sh --remote archipelago@192.168.1.228
|
||||
# ~4-6 minutes with cached ISO
|
||||
```
|
||||
|
||||
### Clean Build (First Time)
|
||||
```bash
|
||||
./build-iso-complete.sh --remote archipelago@192.168.1.228 --clean
|
||||
# ~21-30 minutes with ISO download
|
||||
```
|
||||
|
||||
### Frontend-Only Update
|
||||
```bash
|
||||
./build-iso-complete.sh --remote archipelago@192.168.1.228 --skip-backend
|
||||
# ~3-4 minutes
|
||||
```
|
||||
|
||||
### Backend-Only Update
|
||||
```bash
|
||||
./build-iso-complete.sh --remote archipelago@192.168.1.228 --skip-frontend
|
||||
# ~3-4 minutes
|
||||
```
|
||||
|
||||
## 🔐 Security Features
|
||||
|
||||
All builds include:
|
||||
- ✅ SSH server with default credentials (for initial setup)
|
||||
- ✅ Auto-login configured
|
||||
- ✅ Password change recommended in docs
|
||||
- ✅ SSH key authentication supported
|
||||
|
||||
## 🚀 What's Next
|
||||
|
||||
Once current ISO build completes:
|
||||
1. Test on Dell OptiPlex
|
||||
2. Verify auto-start works
|
||||
3. Confirm Web UI accessible
|
||||
4. Test SSH access
|
||||
5. Validate all apps launch correctly
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
All improvements are documented in:
|
||||
- `BUILD-GUIDE.md` - Full build instructions
|
||||
- `BUILD-SYSTEM-SUMMARY.md` - System architecture
|
||||
- `build-iso-complete.sh --help` - CLI help
|
||||
- This file - Today's changes
|
||||
|
||||
## 🎉 Summary
|
||||
|
||||
You now have a **professional-grade build system** with:
|
||||
- ✅ One-command builds
|
||||
- ✅ Clear progress indicators
|
||||
- ✅ Smart caching
|
||||
- ✅ Build time tracking
|
||||
- ✅ Comprehensive summaries
|
||||
- ✅ Full documentation
|
||||
- ✅ Remote build support
|
||||
- ✅ Easy iteration
|
||||
|
||||
**Build time reduced from 30 minutes to 5 minutes** for cached builds! 🚀
|
||||
417
DEPLOYMENT.md
417
DEPLOYMENT.md
@@ -1,417 +0,0 @@
|
||||
# Archipelago Deployment & Build Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
This document captures all the critical configurations and fixes needed to build Archipelago from the live development server state.
|
||||
|
||||
**Last Updated:** 2026-02-03
|
||||
**Dev Server:** archipelago@192.168.1.228
|
||||
**Server Disk:** 1.8TB NVMe (1.7TB free)
|
||||
|
||||
---
|
||||
|
||||
## Critical Backend Fixes
|
||||
|
||||
### 1. Podman Container Detection (REQUIRED)
|
||||
|
||||
**Issue:** Backend runs as non-root user but containers are started with `sudo podman` (root context).
|
||||
|
||||
**Fix Applied:** Modified `/core/container/src/podman_client.rs` to use `sudo podman`:
|
||||
|
||||
```rust
|
||||
fn podman_async(&self) -> TokioCommand {
|
||||
// Always use sudo podman to access system-wide containers
|
||||
let mut cmd = TokioCommand::new("sudo");
|
||||
cmd.arg("podman");
|
||||
cmd
|
||||
}
|
||||
```
|
||||
|
||||
**Server Configuration:** Added passwordless sudo for podman:
|
||||
|
||||
```bash
|
||||
# /etc/sudoers.d/archipelago-podman
|
||||
archipelago ALL=(ALL) NOPASSWD: /usr/bin/podman
|
||||
```
|
||||
|
||||
### 2. IndeedHub Metadata in Backend
|
||||
|
||||
**Location:** `/core/archipelago/src/container/docker_packages.rs`
|
||||
|
||||
Added IndeedHub to the `get_app_metadata()` function:
|
||||
|
||||
```rust
|
||||
"indeedhub" => AppMetadata {
|
||||
title: "IndeedHub".to_string(),
|
||||
description: "Decentralized media streaming platform".to_string(),
|
||||
icon: "/assets/img/app-icons/indeedhub.png".to_string(),
|
||||
repo: "https://github.com/indeedhub/indeedhub".to_string(),
|
||||
},
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Nginx Configuration
|
||||
|
||||
### HTTP + HTTPS Setup (with self-signed certs)
|
||||
|
||||
**Location:** `/etc/nginx/sites-available/default`
|
||||
|
||||
```nginx
|
||||
# Redirect HTTP to HTTPS
|
||||
server {
|
||||
listen 80 default_server;
|
||||
listen [::]:80 default_server;
|
||||
server_name _;
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
# HTTPS server
|
||||
server {
|
||||
listen 443 ssl http2 default_server;
|
||||
listen [::]:443 ssl http2 default_server;
|
||||
|
||||
ssl_certificate /etc/nginx/ssl/archipelago.crt;
|
||||
ssl_certificate_key /etc/nginx/ssl/archipelago.key;
|
||||
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
ssl_prefer_server_ciphers on;
|
||||
|
||||
root /opt/archipelago/web-ui;
|
||||
index index.html;
|
||||
|
||||
server_name _;
|
||||
|
||||
location /rpc/ {
|
||||
proxy_pass http://localhost:5678/rpc/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
}
|
||||
|
||||
location /ws/ {
|
||||
proxy_pass http://localhost:5678/ws/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
}
|
||||
|
||||
location /health {
|
||||
proxy_pass http://localhost:5678/health;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### SSL Certificate Generation
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /etc/nginx/ssl
|
||||
sudo openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
|
||||
-keyout /etc/nginx/ssl/archipelago.key \
|
||||
-out /etc/nginx/ssl/archipelago.crt \
|
||||
-subj "/C=US/ST=State/L=City/O=Archipelago/CN=archipelago.local"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Systemd Services
|
||||
|
||||
### Archipelago Backend Service
|
||||
|
||||
**Location:** `/etc/systemd/system/archipelago.service`
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Archipelago Backend
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=archipelago
|
||||
Group=archipelago
|
||||
ExecStart=/usr/local/bin/archipelago
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
Environment="RUST_LOG=debug"
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Container Deployments
|
||||
|
||||
### Bitcoin Knots (Full Node)
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /var/lib/archipelago/bitcoin
|
||||
|
||||
sudo podman run -d \
|
||||
--name bitcoin-knots \
|
||||
--restart unless-stopped \
|
||||
-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" \
|
||||
--label "com.archipelago.version=28.1" \
|
||||
--label "com.archipelago.category=bitcoin" \
|
||||
--label "com.archipelago.description.short=Full Bitcoin node implementation" \
|
||||
--label "com.archipelago.description.long=Bitcoin Knots is a derivative of Bitcoin Core with additional features and bug fixes. Maintain the full blockchain and validate all transactions." \
|
||||
--label "com.archipelago.license=MIT" \
|
||||
--label "com.archipelago.icon=/assets/img/app-icons/bitcoin-knots.webp" \
|
||||
--label "com.archipelago.port=8332" \
|
||||
--label "com.archipelago.repo=https://github.com/bitcoinknots/bitcoin" \
|
||||
docker.io/bitcoinknots/bitcoin:latest \
|
||||
-server=1 \
|
||||
-txindex=1 \
|
||||
-rpcallowip=0.0.0.0/0 \
|
||||
-rpcbind=0.0.0.0:8332 \
|
||||
-rpcuser=archipelago \
|
||||
-rpcpassword=archipelago123 \
|
||||
-dbcache=4096
|
||||
```
|
||||
|
||||
### IndeedHub (Example app deployment)
|
||||
|
||||
See `/Users/dorian/Projects/Indeedhub Prototype/deploy-to-archipelago.sh`
|
||||
|
||||
**Key Requirements:**
|
||||
- Must include `com.archipelago.*` labels for proper detection
|
||||
- Port mapping must be explicit
|
||||
- Restart policy: `unless-stopped`
|
||||
|
||||
---
|
||||
|
||||
## Build Process for Beta Release
|
||||
|
||||
### 1. Capture Live Server State
|
||||
|
||||
```bash
|
||||
cd /Users/dorian/Projects/archy/image-recipe
|
||||
|
||||
# Capture from dev server (default)
|
||||
DEV_SERVER=archipelago@192.168.1.228 ./build-auto-installer-iso.sh
|
||||
|
||||
# Or build from source
|
||||
BUILD_FROM_SOURCE=1 ./build-auto-installer-iso.sh
|
||||
```
|
||||
|
||||
### 2. What Gets Captured
|
||||
|
||||
The auto-installer script captures:
|
||||
|
||||
- **Backend Binary:** `/usr/local/bin/archipelago`
|
||||
- **Frontend Assets:** `/opt/archipelago/web-ui/`
|
||||
- **Nginx Configuration:** `/etc/nginx/sites-available/default`
|
||||
- **SSL Certificates:** `/etc/nginx/ssl/`
|
||||
- **Systemd Service:** `/etc/systemd/system/archipelago.service`
|
||||
- **Sudoers Config:** `/etc/sudoers.d/archipelago-podman`
|
||||
|
||||
**NOTE:** Containers are NOT captured in the ISO - they must be deployed after installation.
|
||||
|
||||
### 3. Critical Auto-Installer Fix
|
||||
|
||||
**Location:** `/image-recipe/build-auto-installer-iso.sh` (line ~850)
|
||||
|
||||
The auto-start script MUST NOT check `[ ! -t 0 ]` (non-interactive check):
|
||||
|
||||
```bash
|
||||
# CORRECT (in z99-archipelago-installer.sh):
|
||||
if [ -n "$INSTALLER_STARTED" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# WRONG (will fail on auto-login):
|
||||
# if [ -n "$INSTALLER_STARTED" ] || [ ! -t 0 ]; then
|
||||
```
|
||||
|
||||
This was causing the installer to hang at `user@debian:~$` prompt.
|
||||
|
||||
---
|
||||
|
||||
## Dependencies Required on Build Machine
|
||||
|
||||
### For Building ISOs (Mac/Linux):
|
||||
|
||||
```bash
|
||||
# Docker or Podman (for rootfs creation)
|
||||
brew install podman
|
||||
# OR
|
||||
brew install docker
|
||||
|
||||
# ISO creation tools
|
||||
brew install xorriso # Mac
|
||||
# OR
|
||||
apt install xorriso # Linux
|
||||
```
|
||||
|
||||
### For Server Runtime:
|
||||
|
||||
```bash
|
||||
# Debian 12 (Bookworm) base
|
||||
apt update && apt install -y \
|
||||
nginx \
|
||||
podman \
|
||||
build-essential \
|
||||
pkg-config \
|
||||
libssl-dev \
|
||||
curl \
|
||||
rsync
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Frontend Build
|
||||
|
||||
```bash
|
||||
cd /Users/dorian/Projects/archy/neode-ui
|
||||
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Build for production
|
||||
npm run build
|
||||
|
||||
# Output goes to: ../web/dist/neode-ui/
|
||||
```
|
||||
|
||||
**Deploy to server:**
|
||||
|
||||
```bash
|
||||
rsync -avz --delete ../web/dist/neode-ui/ archipelago@192.168.1.228:/opt/archipelago/web-ui/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Backend Build
|
||||
|
||||
```bash
|
||||
cd /Users/dorian/Projects/archy/core/archipelago
|
||||
|
||||
# Build release binary
|
||||
cargo build --release
|
||||
|
||||
# Binary location: ../target/release/archipelago
|
||||
```
|
||||
|
||||
**Deploy to server:**
|
||||
|
||||
```bash
|
||||
scp ../target/release/archipelago archipelago@192.168.1.228:/tmp/
|
||||
ssh archipelago@192.168.1.228 'sudo systemctl stop archipelago && \
|
||||
sudo mv /tmp/archipelago /usr/local/bin/archipelago && \
|
||||
sudo chmod +x /usr/local/bin/archipelago && \
|
||||
sudo systemctl start archipelago'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist (Pre-Release)
|
||||
|
||||
- [ ] Backend detects all running containers
|
||||
- [ ] Frontend loads and connects to backend WebSocket
|
||||
- [ ] Apps show in "My Apps" with correct status
|
||||
- [ ] App Store shows containers with "Installed" badge
|
||||
- [ ] Bitcoin node is syncing blockchain
|
||||
- [ ] Nginx serves frontend correctly
|
||||
- [ ] RPC/WebSocket proxying works
|
||||
- [ ] Auto-installer ISO boots and installs
|
||||
- [ ] Post-install: System boots to login screen
|
||||
- [ ] Web UI accessible at http://server-ip
|
||||
|
||||
---
|
||||
|
||||
## Known Issues
|
||||
|
||||
### Port 443 Not Binding (Post-Reinstall)
|
||||
|
||||
After fresh install, HTTPS (port 443) may not bind even with correct nginx config. **Workaround:** Use HTTP only initially, investigate nginx/systemd socket issues.
|
||||
|
||||
### Browser HTTPS Auto-Upgrade
|
||||
|
||||
Browsers (especially Brave/Chrome) aggressively upgrade to HTTPS. Users may need to:
|
||||
- Clear site data
|
||||
- Disable "HTTPS-Only Mode"
|
||||
- Use `http://` prefix explicitly
|
||||
|
||||
---
|
||||
|
||||
## File Locations Summary
|
||||
|
||||
| Component | Dev Server Location | ISO Build Captures |
|
||||
|-----------|-------------------|-------------------|
|
||||
| Backend Binary | `/usr/local/bin/archipelago` | ✅ Yes |
|
||||
| Frontend Assets | `/opt/archipelago/web-ui/` | ✅ Yes |
|
||||
| Nginx Config | `/etc/nginx/sites-available/default` | ✅ Yes |
|
||||
| SSL Certs | `/etc/nginx/ssl/` | ✅ Yes |
|
||||
| Systemd Service | `/etc/systemd/system/archipelago.service` | ✅ Yes |
|
||||
| Sudoers | `/etc/sudoers.d/archipelago-podman` | ✅ Yes |
|
||||
| Container Data | `/var/lib/archipelago/` | ❌ No - too large |
|
||||
| Bitcoin Blockchain | `/var/lib/archipelago/bitcoin/` | ❌ No - user downloads |
|
||||
|
||||
---
|
||||
|
||||
## Version Control
|
||||
|
||||
**Important Changes to Track:**
|
||||
|
||||
1. `/core/container/src/podman_client.rs` - sudo podman fix
|
||||
2. `/core/archipelago/src/container/docker_packages.rs` - app metadata
|
||||
3. `/neode-ui/src/utils/dummyApps.ts` - frontend app definitions
|
||||
4. `/image-recipe/build-auto-installer-iso.sh` - auto-start fix
|
||||
|
||||
**Commit before building beta:**
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "Prepare for beta release: podman detection, IndeedHub metadata, auto-installer fixes"
|
||||
git tag v0.1.0-beta.1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Emergency Recovery
|
||||
|
||||
If the backend fails to detect containers:
|
||||
|
||||
```bash
|
||||
# Verify sudoers file exists
|
||||
cat /etc/sudoers.d/archipelago-podman
|
||||
|
||||
# Test manual detection
|
||||
sudo podman ps --format json
|
||||
|
||||
# Check backend logs
|
||||
sudo journalctl -u archipelago -f
|
||||
|
||||
# Restart backend
|
||||
sudo systemctl restart archipelago
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Contact
|
||||
|
||||
Development Server: archipelago@192.168.1.228
|
||||
Password: `archipelago`
|
||||
Web UI: http://192.168.1.228 (or https with self-signed cert warning)
|
||||
@@ -1,180 +0,0 @@
|
||||
# Mac Development Setup - What You Need
|
||||
|
||||
## Current Situation
|
||||
|
||||
You develop Archipelago on a **remote Debian server** (192.168.1.228), not locally on your Mac.
|
||||
|
||||
Your Mac is used for:
|
||||
- ✅ **Editing code** (VSCode/Cursor)
|
||||
- ✅ **Git operations** (commit, push, pull)
|
||||
- ✅ **Deploying to remote** (`deploy-to-target.sh`)
|
||||
- ✅ **Building ISOs** (occasionally)
|
||||
|
||||
Your Mac is NOT used for:
|
||||
- ❌ Running containers locally
|
||||
- ❌ Building Rust locally
|
||||
- ❌ Running the backend locally
|
||||
- ❌ Running the frontend dev server locally
|
||||
|
||||
## Disk Usage Analysis
|
||||
|
||||
### 🔴 Can Delete (Total: ~66 GB)
|
||||
|
||||
1. **Docker Desktop: 53 GB**
|
||||
- You're not running containers locally
|
||||
- All containers run on 192.168.1.228
|
||||
- Safe to completely uninstall
|
||||
|
||||
2. **Rust Build Cache: 1.6 GB** (`core/target/`)
|
||||
- Builds happen on remote server via `deploy-to-target.sh`
|
||||
- Rust compiler still needed for occasional local builds
|
||||
- Cache rebuilds automatically
|
||||
|
||||
3. **ISO Build Artifacts: 8.6 GB** (`image-recipe/build/`)
|
||||
- Temporary files from ISO building
|
||||
- Recreated when you build a new ISO
|
||||
- Safe to delete
|
||||
|
||||
4. **Old ISO Files: ~3 GB** (`image-recipe/results/`)
|
||||
- Keep latest ISO only (~500MB)
|
||||
- Delete old versions
|
||||
|
||||
### 🟢 Keep These Tools
|
||||
|
||||
1. **Rust/Cargo** ✅
|
||||
- For occasional local builds
|
||||
- For `deploy-to-target.sh` (builds before deploying)
|
||||
- Size: ~200 MB
|
||||
|
||||
2. **Node.js/npm** ✅
|
||||
- For frontend builds in `deploy-to-target.sh`
|
||||
- For editing with IDE autocomplete
|
||||
- Size: ~100 MB
|
||||
|
||||
3. **Git** ✅
|
||||
- Version control
|
||||
- Essential
|
||||
|
||||
4. **SSH** ✅
|
||||
- Remote server access
|
||||
- Essential for deployment
|
||||
|
||||
### ⚠️ Optional (You Choose)
|
||||
|
||||
1. **Podman** (~100 MB)
|
||||
- Currently installed but not used
|
||||
- Could remove: `brew uninstall podman`
|
||||
- You use Podman on the *remote server*, not locally
|
||||
|
||||
2. **xorriso, p7zip** (ISO build tools)
|
||||
- Only needed if building ISOs locally
|
||||
- Can reinstall when needed: `brew install xorriso p7zip`
|
||||
|
||||
## Recommended Setup
|
||||
|
||||
### Minimal Mac Setup (Recommended)
|
||||
```
|
||||
✅ VSCode/Cursor (code editing)
|
||||
✅ Git (version control)
|
||||
✅ SSH (remote access)
|
||||
✅ Rust/Cargo (for deploy script)
|
||||
✅ Node.js/npm (for deploy script)
|
||||
✅ One latest ISO file (~500 MB)
|
||||
❌ NO Docker Desktop
|
||||
❌ NO local containers
|
||||
❌ NO build artifacts
|
||||
```
|
||||
|
||||
**Total disk usage: ~500 MB + source code**
|
||||
|
||||
### Your Development Workflow
|
||||
```bash
|
||||
# 1. Edit code locally on Mac
|
||||
vim core/archipelago/src/...
|
||||
vim neode-ui/src/...
|
||||
|
||||
# 2. Deploy to remote server
|
||||
./scripts/deploy-to-target.sh --live
|
||||
|
||||
# 3. Test on remote server
|
||||
open http://192.168.1.228
|
||||
|
||||
# 4. Check logs (if needed)
|
||||
ssh archipelago@192.168.1.228 'sudo journalctl -u archipelago -f'
|
||||
|
||||
# 5. Build ISO (when needed)
|
||||
cd image-recipe
|
||||
./build-debian-iso.sh # Only when making a release
|
||||
```
|
||||
|
||||
## Cleanup Instructions
|
||||
|
||||
### Quick Cleanup (Run This Now)
|
||||
```bash
|
||||
cd /Users/dorian/Projects/archy
|
||||
./cleanup-mac.sh
|
||||
```
|
||||
|
||||
This removes:
|
||||
- Rust build cache
|
||||
- ISO build artifacts
|
||||
- Old ISO files
|
||||
|
||||
**Saves: ~13 GB**
|
||||
|
||||
### Complete Cleanup (Optional)
|
||||
|
||||
1. **Uninstall Docker Desktop** (53 GB)
|
||||
```bash
|
||||
# Option 1: Using the app
|
||||
# Open Docker Desktop → Troubleshoot → Uninstall
|
||||
|
||||
# Option 2: Manual removal
|
||||
rm -rf ~/Library/Containers/com.docker.docker
|
||||
rm -rf ~/Library/Application\ Support/Docker\ Desktop
|
||||
rm -rf ~/.docker
|
||||
brew uninstall --cask docker
|
||||
```
|
||||
|
||||
2. **Remove Podman** (if not used)
|
||||
```bash
|
||||
brew uninstall podman
|
||||
```
|
||||
|
||||
3. **Remove ISO build tools** (if not needed)
|
||||
```bash
|
||||
brew uninstall xorriso p7zip
|
||||
```
|
||||
|
||||
**Total savings: ~66 GB**
|
||||
|
||||
## FAQ
|
||||
|
||||
**Q: Will this break my development workflow?**
|
||||
A: No! You'll still be able to edit code and deploy. Build artifacts regenerate automatically.
|
||||
|
||||
**Q: What if I need to build locally?**
|
||||
A: The tools (Rust, Node) remain installed. Only the cached artifacts are removed.
|
||||
|
||||
**Q: What about Docker containers?**
|
||||
A: All containers run on the remote server (192.168.1.228), not locally.
|
||||
|
||||
**Q: Can I rebuild ISOs after cleanup?**
|
||||
A: Yes! Just run `./build-debian-iso.sh` - it will recreate the build artifacts.
|
||||
|
||||
**Q: What if I delete too much?**
|
||||
A: The cleanup script is conservative. Everything removed can be regenerated.
|
||||
|
||||
## After Cleanup
|
||||
|
||||
Your Mac will have:
|
||||
- ✅ 66+ GB free disk space
|
||||
- ✅ Fast, lean development environment
|
||||
- ✅ All source code intact
|
||||
- ✅ Full development capabilities
|
||||
- ✅ Latest ISO ready to flash
|
||||
|
||||
Your workflow remains the same:
|
||||
```
|
||||
Edit → Deploy → Test (on remote) → Commit
|
||||
```
|
||||
@@ -1,84 +0,0 @@
|
||||
# Archipelago v0.5.0-beta Release Notes
|
||||
|
||||
**Release Date**: March 2026
|
||||
**Target Platform**: Debian 12 (Bookworm) — x86_64 and ARM64
|
||||
|
||||
## Overview
|
||||
|
||||
This is the first public beta of Archipelago, a self-sovereign Bitcoin Node OS. Flash it to a USB, install on any x86_64 or ARM64 machine, and manage your personal server through a modern web interface.
|
||||
|
||||
## What's Included
|
||||
|
||||
### Core System
|
||||
- Rust backend with RPC API and WebSocket real-time updates
|
||||
- Vue 3 frontend with glassmorphism UI design
|
||||
- Automated Podman container management
|
||||
- Nginx reverse proxy with HTTPS (self-signed cert)
|
||||
- Tor hidden services for all apps
|
||||
|
||||
### App Store (16+ Apps)
|
||||
- **Bitcoin Stack**: Bitcoin Knots, Electrs, LND, BTCPay Server, Mempool, Fedimint
|
||||
- **Storage**: File Browser, Immich, PhotoPrism
|
||||
- **Productivity**: Penpot, SearXNG
|
||||
- **AI**: Ollama (local LLMs)
|
||||
- **Network**: Nostr Relay, Nginx Proxy Manager, Tailscale, Home Assistant
|
||||
- **Platform**: IndeedHub
|
||||
|
||||
### Security
|
||||
- AES-256-GCM encrypted secrets on disk
|
||||
- Session management: 24h inactivity expiry, max 5 concurrent sessions
|
||||
- TOTP two-factor authentication with backup codes
|
||||
- Container hardening: read-only root, no new privileges, dropped capabilities
|
||||
- Pinned container image versions (no `:latest` tags)
|
||||
- Login rate limiting (5 attempts per 60 seconds per IP)
|
||||
- Path traversal prevention (nginx + client-side)
|
||||
- Cookie-based auth (no tokens in URLs)
|
||||
|
||||
### Identity & Web5
|
||||
- Decentralized Identifier (DID) generation
|
||||
- Identity backup/restore
|
||||
- Nostr relay support
|
||||
|
||||
### Performance
|
||||
- Backend startup: ~100ms
|
||||
- Frontend bundle: ~105 KB gzipped
|
||||
- WebSocket heartbeat with 30s ping/pong
|
||||
- Exponential backoff reconnection (max 30s)
|
||||
- Real-time install progress via WebSocket
|
||||
- Server-side 5-minute inactivity timeout for stale connections
|
||||
|
||||
## Known Issues
|
||||
|
||||
1. **ARM64 ISO**: ARM64 builds may require manual testing — primary testing is on x86_64
|
||||
2. **Bitcoin Initial Sync**: First blockchain sync takes 1-7 days depending on hardware
|
||||
3. **Self-signed HTTPS**: Browser shows certificate warning on first visit (expected)
|
||||
4. **Restore from Backup**: Not yet implemented in onboarding flow
|
||||
5. **Connect to Existing Server**: Not yet implemented in onboarding flow
|
||||
6. **Immich**: Stack installation may take 5+ minutes due to multiple container images
|
||||
7. **Memory**: Running all apps simultaneously requires 16+ GB RAM
|
||||
8. **Disk Space**: Full Bitcoin node + all apps requires 800+ GB
|
||||
|
||||
## Upgrade Path
|
||||
|
||||
This is a beta release. No upgrade path from beta to stable is guaranteed. Back up your data before installing.
|
||||
|
||||
## System Requirements
|
||||
|
||||
| Component | Minimum | Recommended |
|
||||
|-----------|---------|-------------|
|
||||
| CPU | 4 cores | 8+ cores |
|
||||
| RAM | 16 GB | 32 GB |
|
||||
| Storage | 500 GB SSD | 2 TB NVMe |
|
||||
| Network | Ethernet | Gigabit Ethernet |
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. Download the ISO for your architecture
|
||||
2. Flash to USB with balenaEtcher or `dd`
|
||||
3. Boot from USB on target hardware
|
||||
4. Auto-installer runs — follow on-screen prompts
|
||||
5. After reboot, navigate to `http://<server-ip>` in your browser
|
||||
6. Complete the onboarding wizard
|
||||
7. Start installing apps from the App Store
|
||||
|
||||
See [User Guide](docs/user-guide.md) for detailed instructions.
|
||||
@@ -1,400 +0,0 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Archipelago Complete ISO Build System
|
||||
# =====================================
|
||||
#
|
||||
# This script builds a complete, flashable ISO from source with one command.
|
||||
# It handles backend compilation, frontend bundling, and ISO creation.
|
||||
#
|
||||
# Usage:
|
||||
# ./build-iso-complete.sh [options]
|
||||
#
|
||||
# Options:
|
||||
# --local Build everything locally (requires Rust + Node.js)
|
||||
# --remote HOST Build on remote server (recommended for ARM -> x86 cross-compile)
|
||||
# --skip-backend Skip backend compilation (use existing binary)
|
||||
# --skip-frontend Skip frontend build (use existing dist)
|
||||
# --clean Clean all build artifacts before building
|
||||
# --help Show this help message
|
||||
#
|
||||
# Examples:
|
||||
# ./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
|
||||
|
||||
# =============================================================================
|
||||
# Configuration
|
||||
# =============================================================================
|
||||
|
||||
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-auto-installer-iso.sh"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Build options (defaults)
|
||||
BUILD_MODE="remote"
|
||||
REMOTE_HOST=""
|
||||
SKIP_BACKEND=false
|
||||
SKIP_FRONTEND=false
|
||||
CLEAN_BUILD=false
|
||||
|
||||
# =============================================================================
|
||||
# Helper Functions
|
||||
# =============================================================================
|
||||
|
||||
print_header() {
|
||||
echo ""
|
||||
echo -e "${BLUE}╔════════════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${BLUE}║${NC} $1"
|
||||
echo -e "${BLUE}╚════════════════════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo -e "${GREEN}✅ $1${NC}"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}❌ $1${NC}"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}⚠️ $1${NC}"
|
||||
}
|
||||
|
||||
print_info() {
|
||||
echo -e "${BLUE}ℹ️ $1${NC}"
|
||||
}
|
||||
|
||||
show_help() {
|
||||
head -n 25 "$0" | grep "^#" | sed 's/^# //' | sed 's/^#//'
|
||||
exit 0
|
||||
}
|
||||
|
||||
check_command() {
|
||||
if ! command -v "$1" &> /dev/null; then
|
||||
print_error "$1 is not installed"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Parse Arguments
|
||||
# =============================================================================
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--local)
|
||||
BUILD_MODE="local"
|
||||
shift
|
||||
;;
|
||||
--remote)
|
||||
BUILD_MODE="remote"
|
||||
REMOTE_HOST="$2"
|
||||
shift 2
|
||||
;;
|
||||
--skip-backend)
|
||||
SKIP_BACKEND=true
|
||||
shift
|
||||
;;
|
||||
--skip-frontend)
|
||||
SKIP_FRONTEND=true
|
||||
shift
|
||||
;;
|
||||
--clean)
|
||||
CLEAN_BUILD=true
|
||||
shift
|
||||
;;
|
||||
--help|-h)
|
||||
show_help
|
||||
;;
|
||||
*)
|
||||
print_error "Unknown option: $1"
|
||||
show_help
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# =============================================================================
|
||||
# Pre-flight Checks
|
||||
# =============================================================================
|
||||
|
||||
print_header "Archipelago Complete ISO Builder"
|
||||
|
||||
print_info "Build mode: $BUILD_MODE"
|
||||
[[ -n "$REMOTE_HOST" ]] && print_info "Remote host: $REMOTE_HOST"
|
||||
[[ "$SKIP_BACKEND" = true ]] && print_warning "Skipping backend build"
|
||||
[[ "$SKIP_FRONTEND" = true ]] && print_warning "Skipping frontend build"
|
||||
[[ "$CLEAN_BUILD" = true ]] && print_warning "Clean build enabled"
|
||||
|
||||
echo ""
|
||||
|
||||
# Check for required commands
|
||||
if [[ "$BUILD_MODE" == "remote" ]] && [[ -z "$REMOTE_HOST" ]]; then
|
||||
print_error "Remote build mode requires --remote HOST"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$BUILD_MODE" == "remote" ]]; then
|
||||
if ! check_command ssh; then
|
||||
exit 1
|
||||
fi
|
||||
if ! check_command rsync; then
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# Step 1: Clean Build Artifacts (if requested)
|
||||
# =============================================================================
|
||||
|
||||
if [[ "$CLEAN_BUILD" = true ]]; then
|
||||
print_header "Cleaning Build Artifacts"
|
||||
|
||||
if [[ "$SKIP_BACKEND" = false ]]; then
|
||||
print_info "Cleaning backend..."
|
||||
rm -rf "$BACKEND_SRC/target"
|
||||
print_success "Backend cleaned"
|
||||
fi
|
||||
|
||||
if [[ "$SKIP_FRONTEND" = false ]]; then
|
||||
print_info "Cleaning frontend..."
|
||||
rm -rf "$FRONTEND_SRC/dist"
|
||||
rm -rf "$FRONTEND_SRC/node_modules/.vite"
|
||||
print_success "Frontend cleaned"
|
||||
fi
|
||||
|
||||
print_info "Cleaning ISO build directory..."
|
||||
rm -rf "$BUILD_DIR"
|
||||
rm -rf "$SCRIPT_DIR/image-recipe/iso-workdir"
|
||||
rm -rf "$SCRIPT_DIR/image-recipe/results"
|
||||
print_success "ISO build artifacts cleaned"
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# Step 2: Build Backend
|
||||
# =============================================================================
|
||||
|
||||
if [[ "$SKIP_BACKEND" = false ]]; then
|
||||
print_header "Building Backend (Rust)"
|
||||
|
||||
if [[ "$BUILD_MODE" == "local" ]]; then
|
||||
print_info "Building backend locally..."
|
||||
cd "$BACKEND_SRC"
|
||||
|
||||
if ! check_command cargo; then
|
||||
print_error "Rust/Cargo not installed. Install from: https://rustup.rs"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cargo build --release
|
||||
|
||||
# Copy to build directory
|
||||
mkdir -p "$BUILD_DIR/backend"
|
||||
cp target/release/archipelago "$BUILD_DIR/backend/"
|
||||
chmod +x "$BUILD_DIR/backend/archipelago"
|
||||
|
||||
print_success "Backend built locally"
|
||||
|
||||
elif [[ "$BUILD_MODE" == "remote" ]]; then
|
||||
print_info "Building backend on remote server: $REMOTE_HOST"
|
||||
|
||||
# Sync source code to remote
|
||||
print_info "Syncing source code to remote..."
|
||||
ssh "$REMOTE_HOST" "mkdir -p ~/archy-build"
|
||||
rsync -az --delete \
|
||||
--exclude 'target/' \
|
||||
--exclude 'node_modules/' \
|
||||
--exclude '.git/' \
|
||||
"$BACKEND_SRC/" "$REMOTE_HOST:~/archy-build/core/archipelago/"
|
||||
|
||||
# Build on remote
|
||||
print_info "Compiling backend on remote..."
|
||||
ssh "$REMOTE_HOST" "cd ~/archy-build/core/archipelago && cargo build --release"
|
||||
|
||||
# Copy binary back
|
||||
mkdir -p "$BUILD_DIR/backend"
|
||||
print_info "Copying binary back to local..."
|
||||
scp "$REMOTE_HOST:~/archy-build/core/archipelago/target/release/archipelago" "$BUILD_DIR/backend/"
|
||||
chmod +x "$BUILD_DIR/backend/archipelago"
|
||||
|
||||
print_success "Backend built on remote server"
|
||||
fi
|
||||
|
||||
# Verify binary
|
||||
if [[ ! -f "$BUILD_DIR/backend/archipelago" ]]; then
|
||||
print_error "Backend binary not found after build!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BINARY_SIZE=$(du -h "$BUILD_DIR/backend/archipelago" | awk '{print $1}')
|
||||
print_success "Backend binary ready ($BINARY_SIZE)"
|
||||
else
|
||||
print_warning "Skipping backend build (using existing binary)"
|
||||
if [[ ! -f "$BUILD_DIR/backend/archipelago" ]]; then
|
||||
print_error "No existing backend binary found at $BUILD_DIR/backend/archipelago"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# Step 3: Build Frontend
|
||||
# =============================================================================
|
||||
|
||||
if [[ "$SKIP_FRONTEND" = false ]]; then
|
||||
print_header "Building Frontend (Vue.js)"
|
||||
|
||||
cd "$FRONTEND_SRC"
|
||||
|
||||
if ! check_command npm; then
|
||||
print_error "Node.js/npm not installed. Install from: https://nodejs.org"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Install dependencies if needed
|
||||
if [[ ! -d "node_modules" ]] || [[ "$CLEAN_BUILD" = true ]]; then
|
||||
print_info "Installing dependencies..."
|
||||
npm install
|
||||
fi
|
||||
|
||||
# Build frontend
|
||||
print_info "Building frontend..."
|
||||
npm run build
|
||||
|
||||
# Copy to build directory
|
||||
mkdir -p "$BUILD_DIR/frontend"
|
||||
cp -r dist/* "$BUILD_DIR/frontend/"
|
||||
|
||||
print_success "Frontend built"
|
||||
|
||||
# Verify dist
|
||||
if [[ ! -d "$BUILD_DIR/frontend" ]] || [[ -z "$(ls -A "$BUILD_DIR/frontend")" ]]; then
|
||||
print_error "Frontend build directory is empty!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DIST_SIZE=$(du -sh "$BUILD_DIR/frontend" | awk '{print $1}')
|
||||
print_success "Frontend assets ready ($DIST_SIZE)"
|
||||
else
|
||||
print_warning "Skipping frontend build (using existing dist)"
|
||||
if [[ ! -d "$BUILD_DIR/frontend" ]] || [[ -z "$(ls -A "$BUILD_DIR/frontend")" ]]; then
|
||||
print_error "No existing frontend build found at $BUILD_DIR/frontend"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# Step 4: Build ISO
|
||||
# =============================================================================
|
||||
|
||||
print_header "Building Bootable ISO"
|
||||
|
||||
# Check if running on remote or need to transfer
|
||||
if [[ "$BUILD_MODE" == "remote" ]]; then
|
||||
print_info "Transferring build artifacts to remote server..."
|
||||
|
||||
# Sync entire project to remote
|
||||
ssh "$REMOTE_HOST" "mkdir -p ~/archy"
|
||||
rsync -az --delete \
|
||||
--exclude '.git/' \
|
||||
--exclude 'node_modules/' \
|
||||
--exclude 'core/target/' \
|
||||
--exclude 'core/parmanode/' \
|
||||
"$SCRIPT_DIR/" "$REMOTE_HOST:~/archy/"
|
||||
|
||||
print_success "Files synced to remote"
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
print_success "ISO built on remote server"
|
||||
|
||||
# Copy ISO back to local
|
||||
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/"
|
||||
|
||||
ISO_PATH="$SCRIPT_DIR/image-recipe/results/$ISO_NAME"
|
||||
|
||||
else
|
||||
# Local build
|
||||
print_info "Running ISO build locally (auto-installer)..."
|
||||
cd "$SCRIPT_DIR/image-recipe"
|
||||
sudo bash build-auto-installer-iso.sh
|
||||
|
||||
ISO_PATH="$SCRIPT_DIR/image-recipe/results/archipelago-installer-x86_64.iso"
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# Step 5: Verify and Report
|
||||
# =============================================================================
|
||||
|
||||
print_header "Build Complete!"
|
||||
|
||||
if [[ -f "$ISO_PATH" ]]; then
|
||||
ISO_SIZE=$(du -h "$ISO_PATH" | awk '{print $1}')
|
||||
ISO_MD5=$(md5 -q "$ISO_PATH" 2>/dev/null || md5sum "$ISO_PATH" | awk '{print $1}')
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}✅ ISO ready for flashing!${NC}"
|
||||
echo ""
|
||||
echo -e " 📀 ${BLUE}ISO:${NC} $ISO_PATH"
|
||||
echo -e " 📏 ${BLUE}Size:${NC} $ISO_SIZE"
|
||||
echo -e " 🔐 ${BLUE}MD5:${NC} $ISO_MD5"
|
||||
echo ""
|
||||
echo -e "${YELLOW}Next steps:${NC}"
|
||||
echo ""
|
||||
echo " 1. Insert USB drive"
|
||||
echo " 2. Find device: ${BLUE}diskutil list${NC}"
|
||||
echo " 3. Flash ISO:"
|
||||
echo ""
|
||||
echo " ${BLUE}cd image-recipe && ./write-usb-dd.sh /dev/diskN${NC}"
|
||||
echo ""
|
||||
echo " 4. Boot from USB on target device"
|
||||
echo ""
|
||||
|
||||
# Create a flash script for convenience
|
||||
cat > "$SCRIPT_DIR/flash-to-usb.sh" <<'FLASH_EOF'
|
||||
#!/bin/bash
|
||||
# Quick USB flash script
|
||||
cd "$(dirname "$0")/image-recipe" && ./write-usb-dd.sh "$@"
|
||||
FLASH_EOF
|
||||
chmod +x "$SCRIPT_DIR/flash-to-usb.sh"
|
||||
|
||||
print_success "Created convenience script: ./flash-to-usb.sh"
|
||||
|
||||
else
|
||||
print_error "ISO not found at expected location: $ISO_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# Done!
|
||||
# =============================================================================
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}╔════════════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${GREEN}║ 🎉 Build Complete - Ready to Flash! ║${NC}"
|
||||
echo -e "${GREEN}╚════════════════════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
@@ -1,290 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Archipelago Production macOS Build Script
|
||||
# Creates a production-ready .app bundle and .dmg installer
|
||||
|
||||
set -e
|
||||
|
||||
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
APP_NAME="Archipelago"
|
||||
APP_VERSION="${ARCHIPELAGO_VERSION:-0.1.0}"
|
||||
BUILD_DIR="$PROJECT_ROOT/build/macos"
|
||||
APP_BUNDLE="$BUILD_DIR/$APP_NAME.app"
|
||||
DMG_NAME="Archipelago-${APP_VERSION}-macOS.dmg"
|
||||
|
||||
echo "🏗️ Archipelago macOS Production Build"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo " Version: $APP_VERSION"
|
||||
echo " Target: macOS App Bundle + DMG Installer"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo ""
|
||||
|
||||
# Clean previous build
|
||||
echo "🧹 Cleaning previous build..."
|
||||
rm -rf "$BUILD_DIR"
|
||||
mkdir -p "$BUILD_DIR"
|
||||
|
||||
# Step 1: Build Rust Backend (Release Mode)
|
||||
echo ""
|
||||
echo "⚙️ Step 1/6: Building Rust backend (release mode)..."
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
cd "$PROJECT_ROOT/core"
|
||||
cargo build --release --workspace
|
||||
|
||||
if [ ! -f "target/release/archipelago" ]; then
|
||||
echo "❌ Backend build failed - archipelago binary not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get binary size
|
||||
BACKEND_SIZE=$(du -h target/release/archipelago | cut -f1)
|
||||
echo "✅ Backend built successfully ($BACKEND_SIZE)"
|
||||
|
||||
# Step 2: Build Vue.js Frontend (Production Mode)
|
||||
echo ""
|
||||
echo "🎨 Step 2/6: Building Vue.js frontend (production mode)..."
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
cd "$PROJECT_ROOT/neode-ui"
|
||||
|
||||
# Check if node_modules exists
|
||||
if [ ! -d "node_modules" ]; then
|
||||
echo "📦 Installing frontend dependencies..."
|
||||
npm install
|
||||
fi
|
||||
|
||||
# Build production frontend
|
||||
npm run build
|
||||
|
||||
if [ ! -d "dist" ]; then
|
||||
echo "❌ Frontend build failed - dist directory not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get build size
|
||||
FRONTEND_SIZE=$(du -sh dist | cut -f1)
|
||||
echo "✅ Frontend built successfully ($FRONTEND_SIZE)"
|
||||
|
||||
# Step 3: Create macOS App Bundle Structure
|
||||
echo ""
|
||||
echo "📦 Step 3/6: Creating macOS app bundle..."
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
# Create standard macOS .app directory structure
|
||||
mkdir -p "$APP_BUNDLE/Contents/MacOS"
|
||||
mkdir -p "$APP_BUNDLE/Contents/Resources"
|
||||
mkdir -p "$APP_BUNDLE/Contents/Frameworks"
|
||||
|
||||
# Copy backend binary
|
||||
echo " • Copying backend binary..."
|
||||
cp "$PROJECT_ROOT/core/target/release/archipelago" "$APP_BUNDLE/Contents/MacOS/"
|
||||
chmod +x "$APP_BUNDLE/Contents/MacOS/archipelago"
|
||||
|
||||
# Copy frontend build
|
||||
echo " • Copying frontend assets..."
|
||||
cp -R "$PROJECT_ROOT/neode-ui/dist" "$APP_BUNDLE/Contents/Resources/frontend"
|
||||
|
||||
# Copy Docker UI assets
|
||||
echo " • Copying Docker UI assets..."
|
||||
mkdir -p "$APP_BUNDLE/Contents/Resources/docker-ui"
|
||||
cp -R "$PROJECT_ROOT/docker/bitcoin-ui" "$APP_BUNDLE/Contents/Resources/docker-ui/"
|
||||
cp -R "$PROJECT_ROOT/docker/lnd-ui" "$APP_BUNDLE/Contents/Resources/docker-ui/"
|
||||
|
||||
# Copy configuration templates
|
||||
echo " • Copying configuration..."
|
||||
cp "$PROJECT_ROOT/core/.env.example" "$APP_BUNDLE/Contents/Resources/env.template"
|
||||
cp "$PROJECT_ROOT/core/.env.production" "$APP_BUNDLE/Contents/Resources/env.production"
|
||||
|
||||
# Copy docker-compose.yml for production
|
||||
echo " • Copying Docker configuration..."
|
||||
cp "$PROJECT_ROOT/docker-compose.yml" "$APP_BUNDLE/Contents/Resources/"
|
||||
cp "$PROJECT_ROOT/manage-docker.sh" "$APP_BUNDLE/Contents/MacOS/"
|
||||
chmod +x "$APP_BUNDLE/Contents/MacOS/manage-docker.sh"
|
||||
|
||||
# Create launch script
|
||||
echo " • Creating launcher script..."
|
||||
cat > "$APP_BUNDLE/Contents/MacOS/launch.sh" << 'LAUNCH_EOF'
|
||||
#!/bin/bash
|
||||
# Archipelago macOS Launcher
|
||||
|
||||
# Get the directory containing this script
|
||||
BUNDLE_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
RESOURCES_DIR="$BUNDLE_DIR/Resources"
|
||||
MACOS_DIR="$BUNDLE_DIR/MacOS"
|
||||
|
||||
# Set up data directory in user's home
|
||||
DATA_DIR="$HOME/Library/Application Support/Archipelago"
|
||||
mkdir -p "$DATA_DIR/data"
|
||||
mkdir -p "$DATA_DIR/logs"
|
||||
|
||||
# Export environment variables
|
||||
export ARCHIPELAGO_DATA_DIR="$DATA_DIR/data"
|
||||
export ARCHIPELAGO_FRONTEND_DIR="$RESOURCES_DIR/frontend"
|
||||
export ARCHIPELAGO_DOCKER_UI_DIR="$RESOURCES_DIR/docker-ui"
|
||||
export ARCHIPELAGO_LOG_DIR="$DATA_DIR/logs"
|
||||
export RUST_LOG="${RUST_LOG:-info}"
|
||||
|
||||
# Launch backend
|
||||
cd "$DATA_DIR"
|
||||
exec "$MACOS_DIR/archipelago" > "$DATA_DIR/logs/archipelago.log" 2>&1
|
||||
LAUNCH_EOF
|
||||
|
||||
chmod +x "$APP_BUNDLE/Contents/MacOS/launch.sh"
|
||||
|
||||
# Step 4: Create Info.plist
|
||||
echo ""
|
||||
echo "📄 Step 4/6: Creating app metadata..."
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
cat > "$APP_BUNDLE/Contents/Info.plist" << PLIST_EOF
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleName</key>
|
||||
<string>Archipelago</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Archipelago</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.archipelago.app</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$APP_VERSION</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$APP_VERSION</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>ARCH</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>launch.sh</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>AppIcon</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<true/>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>10.15</string>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.utilities</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2026 Archipelago. All rights reserved.</string>
|
||||
<key>LSBackgroundOnly</key>
|
||||
<false/>
|
||||
<key>NSRequiresAquaSystemAppearance</key>
|
||||
<false/>
|
||||
<key>NSSupportsAutomaticGraphicsSwitching</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
PLIST_EOF
|
||||
|
||||
echo "✅ Info.plist created"
|
||||
|
||||
# Create PkgInfo
|
||||
echo -n "APPLARCH" > "$APP_BUNDLE/Contents/PkgInfo"
|
||||
|
||||
# Step 5: Create App Icon (placeholder - user should provide real icon)
|
||||
echo ""
|
||||
echo "🎨 Step 5/6: Creating app icon..."
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
# Check if sips command is available (macOS built-in)
|
||||
if command -v sips >/dev/null 2>&1; then
|
||||
# Try to find a logo to convert
|
||||
LOGO_SOURCE=""
|
||||
if [ -f "$PROJECT_ROOT/neode-ui/public/assets/img/logo.png" ]; then
|
||||
LOGO_SOURCE="$PROJECT_ROOT/neode-ui/public/assets/img/logo.png"
|
||||
elif [ -f "$PROJECT_ROOT/neode-ui/public/assets/img/app-icons/bitcoin-core.png" ]; then
|
||||
LOGO_SOURCE="$PROJECT_ROOT/neode-ui/public/assets/img/app-icons/bitcoin-core.png"
|
||||
fi
|
||||
|
||||
if [ -n "$LOGO_SOURCE" ]; then
|
||||
# Create iconset
|
||||
ICONSET_DIR="$BUILD_DIR/AppIcon.iconset"
|
||||
mkdir -p "$ICONSET_DIR"
|
||||
|
||||
# Generate icon sizes
|
||||
for size in 16 32 128 256 512; do
|
||||
sips -z $size $size "$LOGO_SOURCE" --out "$ICONSET_DIR/icon_${size}x${size}.png" >/dev/null 2>&1
|
||||
sips -z $((size*2)) $((size*2)) "$LOGO_SOURCE" --out "$ICONSET_DIR/icon_${size}x${size}@2x.png" >/dev/null 2>&1
|
||||
done
|
||||
|
||||
# Convert to icns
|
||||
iconutil -c icns "$ICONSET_DIR" -o "$APP_BUNDLE/Contents/Resources/AppIcon.icns" 2>/dev/null || {
|
||||
echo "⚠️ Icon conversion failed - app will use default icon"
|
||||
}
|
||||
|
||||
rm -rf "$ICONSET_DIR"
|
||||
echo "✅ App icon created from $LOGO_SOURCE"
|
||||
else
|
||||
echo "⚠️ No logo found - app will use default icon"
|
||||
echo " Add logo.png to neode-ui/public/assets/img/ and rebuild"
|
||||
fi
|
||||
else
|
||||
echo "⚠️ sips not available - skipping icon creation"
|
||||
fi
|
||||
|
||||
# Step 6: Create DMG Installer
|
||||
echo ""
|
||||
echo "💿 Step 6/6: Creating DMG installer..."
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
DMG_TEMP_DIR="$BUILD_DIR/dmg"
|
||||
mkdir -p "$DMG_TEMP_DIR"
|
||||
|
||||
# Copy app to DMG staging
|
||||
cp -R "$APP_BUNDLE" "$DMG_TEMP_DIR/"
|
||||
|
||||
# Create Applications symlink
|
||||
ln -s /Applications "$DMG_TEMP_DIR/Applications"
|
||||
|
||||
# Create DMG
|
||||
hdiutil create -volname "Archipelago $APP_VERSION" \
|
||||
-srcfolder "$DMG_TEMP_DIR" \
|
||||
-ov -format UDZO \
|
||||
"$BUILD_DIR/$DMG_NAME" 2>/dev/null || {
|
||||
echo "⚠️ DMG creation failed - using app bundle only"
|
||||
}
|
||||
|
||||
# Cleanup
|
||||
rm -rf "$DMG_TEMP_DIR"
|
||||
|
||||
# Summary
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "✅ Production build complete!"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo ""
|
||||
echo "📦 Build artifacts:"
|
||||
echo " • App Bundle: $APP_BUNDLE"
|
||||
if [ -f "$BUILD_DIR/$DMG_NAME" ]; then
|
||||
DMG_SIZE=$(du -h "$BUILD_DIR/$DMG_NAME" | cut -f1)
|
||||
echo " • DMG Installer: $BUILD_DIR/$DMG_NAME ($DMG_SIZE)"
|
||||
fi
|
||||
echo ""
|
||||
echo "📋 Build summary:"
|
||||
echo " • Backend: $BACKEND_SIZE (Rust)"
|
||||
echo " • Frontend: $FRONTEND_SIZE (Vue.js)"
|
||||
BUNDLE_SIZE=$(du -sh "$APP_BUNDLE" | cut -f1)
|
||||
echo " • Total Bundle: $BUNDLE_SIZE"
|
||||
echo ""
|
||||
echo "🚀 Next steps:"
|
||||
echo " 1. Test the app:"
|
||||
echo " open \"$APP_BUNDLE\""
|
||||
echo ""
|
||||
echo " 2. Install system-wide:"
|
||||
echo " cp -R \"$APP_BUNDLE\" /Applications/"
|
||||
echo ""
|
||||
echo " 3. Distribute via DMG:"
|
||||
if [ -f "$BUILD_DIR/$DMG_NAME" ]; then
|
||||
echo " • Share: $BUILD_DIR/$DMG_NAME"
|
||||
else
|
||||
echo " • (DMG creation skipped - use app bundle directly)"
|
||||
fi
|
||||
echo ""
|
||||
echo " 4. Code signing (optional but recommended):"
|
||||
echo " codesign --deep --force --verify --verbose \\
|
||||
--sign \"Developer ID Application: Your Name\" \\
|
||||
\"$APP_BUNDLE\""
|
||||
echo ""
|
||||
echo "💡 For notarization (macOS 10.14.5+):"
|
||||
echo " • Requires Apple Developer account"
|
||||
echo " • Use: xcrun notarytool submit $DMG_NAME ..."
|
||||
echo ""
|
||||
101
cleanup-mac.sh
101
cleanup-mac.sh
@@ -1,101 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Archipelago Mac Cleanup Script
|
||||
# Removes unnecessary local development artifacts
|
||||
# Safe to run - only removes build caches and Docker data
|
||||
|
||||
set -e
|
||||
|
||||
echo "╔════════════════════════════════════════════════════════════════╗"
|
||||
echo "║ Archipelago Mac Cleanup ║"
|
||||
echo "╚════════════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
# Track space saved
|
||||
TOTAL_SAVED=0
|
||||
|
||||
# Function to calculate and display savings
|
||||
calc_savings() {
|
||||
local path="$1"
|
||||
if [ -e "$path" ]; then
|
||||
local size=$(du -sk "$path" | cut -f1)
|
||||
TOTAL_SAVED=$((TOTAL_SAVED + size))
|
||||
fi
|
||||
}
|
||||
|
||||
# 1. Clean Rust build artifacts (1.6 GB)
|
||||
if [ -d "core/target" ]; then
|
||||
echo "🧹 Cleaning Rust build artifacts..."
|
||||
calc_savings "core/target"
|
||||
rm -rf core/target
|
||||
echo " ✅ Removed core/target/ (~1.6 GB)"
|
||||
else
|
||||
echo " ✅ core/target/ already clean"
|
||||
fi
|
||||
|
||||
# 2. Clean ISO build artifacts (8.6 GB)
|
||||
if [ -d "image-recipe/build" ]; then
|
||||
echo "🧹 Cleaning ISO build artifacts..."
|
||||
calc_savings "image-recipe/build"
|
||||
rm -rf image-recipe/build
|
||||
echo " ✅ Removed image-recipe/build/ (~8.6 GB)"
|
||||
else
|
||||
echo " ✅ image-recipe/build/ already clean"
|
||||
fi
|
||||
|
||||
# 3. Clean old ISOs (keep latest only)
|
||||
if [ -d "image-recipe/results" ]; then
|
||||
ISO_COUNT=$(ls -1 image-recipe/results/*.iso 2>/dev/null | wc -l | tr -d ' ')
|
||||
if [ "$ISO_COUNT" -gt 1 ]; then
|
||||
echo "🧹 Cleaning old ISO files (keeping latest)..."
|
||||
# Keep the most recent ISO, delete others
|
||||
cd image-recipe/results
|
||||
ls -t *.iso | tail -n +2 | while read iso; do
|
||||
calc_savings "$iso"
|
||||
echo " 🗑️ Removing $iso"
|
||||
rm "$iso"
|
||||
done
|
||||
cd ../..
|
||||
echo " ✅ Kept latest ISO, removed old ones (~3 GB saved)"
|
||||
else
|
||||
echo " ✅ Only one ISO found, keeping it"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 4. Show Docker Desktop warning (requires manual removal)
|
||||
DOCKER_SIZE=$(du -sk ~/Library/Containers/com.docker.docker 2>/dev/null | cut -f1 || echo "0")
|
||||
if [ "$DOCKER_SIZE" -gt 1000000 ]; then
|
||||
DOCKER_GB=$((DOCKER_SIZE / 1024 / 1024))
|
||||
echo ""
|
||||
echo "⚠️ Docker Desktop Data Found: ~${DOCKER_GB} GB"
|
||||
echo " Location: ~/Library/Containers/com.docker.docker"
|
||||
echo ""
|
||||
echo " Since you develop on the remote server, you likely don't need this."
|
||||
echo " To remove Docker Desktop completely:"
|
||||
echo " 1. Open Docker Desktop app"
|
||||
echo " 2. Troubleshoot → Uninstall"
|
||||
echo " OR manually: rm -rf ~/Library/Containers/com.docker.docker"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Summary
|
||||
echo "╔════════════════════════════════════════════════════════════════╗"
|
||||
echo "║ Cleanup Complete! ║"
|
||||
echo "╚════════════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
SAVED_GB=$((TOTAL_SAVED / 1024 / 1024))
|
||||
echo "💾 Space saved: ~${SAVED_GB} GB"
|
||||
echo ""
|
||||
echo "Your Mac now has:"
|
||||
echo " ✅ Source code (for editing)"
|
||||
echo " ✅ Deployment scripts (for remote dev)"
|
||||
echo " ✅ Latest ISO (for flashing)"
|
||||
echo " ❌ No build artifacts (rebuild on remote or in CI)"
|
||||
echo ""
|
||||
echo "Development workflow:"
|
||||
echo " 1. Edit code locally"
|
||||
echo " 2. Deploy: ./scripts/deploy-to-target.sh --live"
|
||||
echo " 3. Test on: http://192.168.1.228"
|
||||
echo ""
|
||||
echo "To rebuild ISO when needed:"
|
||||
echo " cd image-recipe && ./build-debian-iso.sh"
|
||||
echo ""
|
||||
3
core/Cargo.lock
generated
3
core/Cargo.lock
generated
@@ -80,7 +80,7 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||
|
||||
[[package]]
|
||||
name = "archipelago"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0-alpha"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"archipelago-container",
|
||||
@@ -202,6 +202,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tracing",
|
||||
"uuid",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -277,27 +277,45 @@ impl ApiHandler {
|
||||
struct Incoming {
|
||||
from_pubkey: Option<String>,
|
||||
message: Option<String>,
|
||||
signature: Option<String>,
|
||||
}
|
||||
let incoming: Incoming = serde_json::from_slice(&body).unwrap_or(Incoming {
|
||||
from_pubkey: None,
|
||||
message: None,
|
||||
signature: None,
|
||||
});
|
||||
if let (Some(from), Some(msg)) = (incoming.from_pubkey, incoming.message) {
|
||||
if let (Some(from), Some(msg)) = (incoming.from_pubkey.as_ref(), incoming.message.as_ref()) {
|
||||
// Validate from_pubkey is a valid hex ed25519 pubkey
|
||||
if !is_valid_pubkey_hex(&from) {
|
||||
if !is_valid_pubkey_hex(from) {
|
||||
return Ok(Response::builder()
|
||||
.status(StatusCode::BAD_REQUEST)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(hyper::Body::from(r#"{"error":"Invalid pubkey format"}"#))
|
||||
.unwrap());
|
||||
}
|
||||
// Verify ed25519 signature if provided (required for trusted messages)
|
||||
if let Some(sig_hex) = &incoming.signature {
|
||||
match crate::identity::NodeIdentity::verify(from, msg.as_bytes(), sig_hex) {
|
||||
Ok(true) => {}
|
||||
_ => {
|
||||
return Ok(Response::builder()
|
||||
.status(StatusCode::FORBIDDEN)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(hyper::Body::from(r#"{"error":"Invalid signature"}"#))
|
||||
.unwrap());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No signature — accept but mark as unverified
|
||||
tracing::warn!("Node message from {} has no signature — unverified", &from[..16.min(from.len())]);
|
||||
}
|
||||
// Sanitize log output to prevent log injection
|
||||
let safe_from = sanitize_log_string(&from);
|
||||
let safe_msg = sanitize_log_string(&msg);
|
||||
let safe_from = sanitize_log_string(from);
|
||||
let safe_msg = sanitize_log_string(msg);
|
||||
tracing::info!("Received message from {}: {}", safe_from, safe_msg);
|
||||
// Sanitize stored message content (strip HTML entities)
|
||||
let clean_from = sanitize_html(&from);
|
||||
let clean_msg = sanitize_html(&msg);
|
||||
let clean_from = sanitize_html(from);
|
||||
let clean_msg = sanitize_html(msg);
|
||||
node_msg::store_received(&clean_from, &clean_msg).await;
|
||||
}
|
||||
Ok(Response::builder()
|
||||
|
||||
@@ -25,6 +25,10 @@ impl RpcHandler {
|
||||
.get("filename")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing filename"))?;
|
||||
// Prevent path traversal
|
||||
if filename.contains("..") || filename.contains('\0') {
|
||||
anyhow::bail!("Invalid filename: path traversal not allowed");
|
||||
}
|
||||
let mime_type = params
|
||||
.get("mime_type")
|
||||
.and_then(|v| v.as_str())
|
||||
|
||||
@@ -428,7 +428,7 @@ fn read_onion_address(service_name: &str) -> Option<String> {
|
||||
if let Some(addr) = std::fs::read_to_string(&hostnames_dir)
|
||||
.ok()
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| s.ends_with(".onion") && s.len() >= 60)
|
||||
.filter(|s| s.ends_with(".onion") && s.len() == 62 && !s.contains(':') && s.chars().take(56).all(|c| c.is_ascii_alphanumeric()))
|
||||
{
|
||||
return Some(addr);
|
||||
}
|
||||
@@ -453,7 +453,7 @@ fn read_onion_address(service_name: &str) -> Option<String> {
|
||||
.and_then(|o| String::from_utf8(o.stdout).ok())
|
||||
})
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| s.ends_with(".onion") && s.len() >= 60)
|
||||
.filter(|s| s.ends_with(".onion") && s.len() == 62 && !s.contains(':') && s.chars().take(56).all(|c| c.is_ascii_alphanumeric()))
|
||||
{
|
||||
return Some(addr);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Scans docker-compose containers and converts them to package data
|
||||
|
||||
use anyhow::Result;
|
||||
use archipelago_container::{ContainerRuntime as ContainerRuntimeTrait, ContainerState};
|
||||
use archipelago_container::{ContainerRuntime as ContainerRuntimeTrait, ContainerState, PodmanClient};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tracing::{debug, info};
|
||||
@@ -119,47 +119,21 @@ impl DockerPackageScanner {
|
||||
// Get metadata for this app
|
||||
let metadata = get_app_metadata(&app_id);
|
||||
|
||||
// Check if this app has a separate UI container
|
||||
// Resolve UI address: separate UI containers > static map > dynamic ports
|
||||
let lan_address = if let Some(ui_address) = ui_containers.get(&app_id) {
|
||||
debug!("Using UI container address for {}: {}", app_id, ui_address);
|
||||
// Apps with separate UI containers (e.g. archy-bitcoin-ui, archy-lnd-ui)
|
||||
debug!("Using UI container for {}: {}", app_id, ui_address);
|
||||
Some(ui_address.clone())
|
||||
} else if app_id == "bitcoin-knots" {
|
||||
// Bitcoin UI runs on host network at port 8334
|
||||
debug!("Using bitcoin-ui for bitcoin-knots: http://localhost:8334");
|
||||
Some("http://localhost:8334".to_string())
|
||||
} else if app_id == "lnd" {
|
||||
// LND UI runs on host network at port 8081
|
||||
debug!("Using lnd-ui for lnd: http://localhost:8081");
|
||||
Some("http://localhost:8081".to_string())
|
||||
} else if app_id == "tailscale" {
|
||||
// Tailscale uses host networking, so no port mappings
|
||||
// But web UI is always on port 8240
|
||||
debug!("Tailscale detected, using port 8240");
|
||||
Some("http://localhost:8240".to_string())
|
||||
} else if app_id == "fedimint" {
|
||||
// Fedimint built-in Guardian UI on port 8175
|
||||
debug!("Using fedimint built-in Guardian UI: http://localhost:8175");
|
||||
Some("http://localhost:8175".to_string())
|
||||
} else if app_id == "fedimint-gateway" {
|
||||
// Fedimint Gateway API on port 8176
|
||||
debug!("Using fedimint gateway: http://localhost:8176");
|
||||
Some("http://localhost:8176".to_string())
|
||||
} else if app_id == "nostr-rs-relay" {
|
||||
debug!("Using Nostr relay: http://localhost:18081");
|
||||
Some("http://localhost:18081".to_string())
|
||||
} else if app_id == "dwn" {
|
||||
debug!("Using DWN server: http://localhost:3100");
|
||||
Some("http://localhost:3100".to_string())
|
||||
} else if app_id == "indeedhub" {
|
||||
debug!("Using IndeedHub: http://localhost:7777");
|
||||
Some("http://localhost:7777".to_string())
|
||||
} else if app_id == "electrumx" || app_id == "mempool-electrs" || app_id == "electrs" {
|
||||
// ElectrumX UI runs on host at port 50002
|
||||
debug!("Using electrumx-ui for electrumx: http://localhost:50002");
|
||||
Some("http://localhost:50002".to_string())
|
||||
} else {
|
||||
// Extract port from the main container
|
||||
// Dynamic: use actual port bindings from container, fall back to static map
|
||||
extract_lan_address(&container.ports)
|
||||
.or_else(|| PodmanClient::lan_address_for(&app_id))
|
||||
};
|
||||
|
||||
debug!("Container {}: ports={:?}, lan_address={:?}", app_id, container.ports, lan_address);
|
||||
@@ -308,9 +282,9 @@ fn get_app_metadata(app_id: &str) -> AppMetadata {
|
||||
repo: "https://github.com/joaovitoriasilva/endurain".to_string(),
|
||||
tier: "",
|
||||
},
|
||||
"fedimint" => AppMetadata {
|
||||
title: "Fedimint".to_string(),
|
||||
description: "Federated Bitcoin mint with Guardian and Gateway".to_string(),
|
||||
"fedimint" | "fedimintd" => AppMetadata {
|
||||
title: "Fedimint Guardian".to_string(),
|
||||
description: "Federated Bitcoin mint — Guardian node for federation consensus".to_string(),
|
||||
icon: "/assets/img/app-icons/fedimint.png".to_string(),
|
||||
repo: "https://github.com/fedimint/fedimint".to_string(),
|
||||
tier: "",
|
||||
@@ -621,7 +595,7 @@ fn convert_state(container_state: &ContainerState) -> (PackageState, ServiceStat
|
||||
ContainerState::Stopped | ContainerState::Exited => {
|
||||
(PackageState::Stopped, ServiceStatus::Stopped)
|
||||
}
|
||||
ContainerState::Created => (PackageState::Starting, ServiceStatus::Starting),
|
||||
ContainerState::Created => (PackageState::Stopped, ServiceStatus::Stopped),
|
||||
ContainerState::Paused => (PackageState::Stopped, ServiceStatus::Stopped),
|
||||
ContainerState::Unknown(_) => (PackageState::Stopped, ServiceStatus::Stopped),
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ const ELECTRUMX_HOST: &str = "127.0.0.1";
|
||||
const ELECTRUMX_PORT: u16 = 50001;
|
||||
const BITCOIN_RPC_URL: &str = "http://127.0.0.1:8332/";
|
||||
const ELECTRUMX_DATA_DIR: &str = "/var/lib/archipelago/electrumx";
|
||||
// Approximate final index size in bytes for mainnet (~55GB for ElectrumX full index)
|
||||
const ESTIMATED_FULL_INDEX_BYTES: f64 = 55_000_000_000.0;
|
||||
// Approximate final index size in bytes for mainnet (~130GB for ElectrumX full index as of 2026)
|
||||
const ESTIMATED_FULL_INDEX_BYTES: f64 = 130_000_000_000.0;
|
||||
|
||||
/// Build Bitcoin RPC Basic auth header using shared credentials.
|
||||
async fn bitcoin_rpc_auth() -> String {
|
||||
@@ -174,7 +174,8 @@ pub async fn get_electrs_sync_status() -> ElectrsSyncStatus {
|
||||
Ok(Err(e)) => {
|
||||
// ElectrumX may not be ready on 50001 during initial sync
|
||||
let err_msg = e.to_string();
|
||||
let (status, error) = if err_msg.contains("connect") || err_msg.contains("Connection refused") {
|
||||
let err_lower = err_msg.to_lowercase();
|
||||
let (status, error) = if err_lower.contains("connect") || err_lower.contains("reset") || err_lower.contains("refused") {
|
||||
// Estimate progress from data directory size
|
||||
let _est_pct = if data_bytes > 0 {
|
||||
((data_bytes as f64 / ESTIMATED_FULL_INDEX_BYTES) * 100.0).min(99.0)
|
||||
|
||||
@@ -108,9 +108,9 @@ impl AlertRule {
|
||||
},
|
||||
AlertRule {
|
||||
kind: AlertRuleKind::CpuLoad,
|
||||
threshold: 2.0,
|
||||
threshold: 4.0,
|
||||
enabled: true,
|
||||
description: "CPU load exceeds 2x core count for 5 minutes".to_string(),
|
||||
description: "CPU load exceeds 4x core count for 5 minutes".to_string(),
|
||||
},
|
||||
AlertRule {
|
||||
kind: AlertRuleKind::ContainerCrash,
|
||||
|
||||
@@ -66,19 +66,19 @@ impl PodmanClient {
|
||||
}
|
||||
}
|
||||
|
||||
/// Map container name to its UI launch URL
|
||||
fn lan_address_for(name: &str) -> Option<String> {
|
||||
/// Map container name to its UI launch URL (static fallback for docker_packages scanner)
|
||||
pub fn lan_address_for(name: &str) -> Option<String> {
|
||||
let url = match name {
|
||||
"bitcoin-knots" | "bitcoin-ui" => "http://localhost:8334",
|
||||
"lnd" | "archy-lnd-ui" => "http://localhost:8081",
|
||||
"tailscale" => "http://localhost:8240",
|
||||
// Tailscale has no web UI — managed via CLI/app
|
||||
"homeassistant" => "http://localhost:8123",
|
||||
"archy-mempool-web" | "mempool" => "http://localhost:4080",
|
||||
"btcpay-server" => "http://localhost:23000",
|
||||
"grafana" => "http://localhost:3000",
|
||||
"searxng" => "http://localhost:8888",
|
||||
"ollama" => "http://localhost:11434",
|
||||
"onlyoffice" => "http://localhost:9980",
|
||||
"onlyoffice" => "http://localhost:8044",
|
||||
"penpot" => "http://localhost:9001",
|
||||
"nextcloud" => "http://localhost:8085",
|
||||
"vaultwarden" => "http://localhost:8082",
|
||||
@@ -86,10 +86,10 @@ impl PodmanClient {
|
||||
"photoprism" => "http://localhost:2342",
|
||||
"immich_server" | "immich" => "http://localhost:2283",
|
||||
"filebrowser" => "http://localhost:8083",
|
||||
"nginx-proxy-manager" => "http://localhost:81",
|
||||
"nginx-proxy-manager" => "http://localhost:8181",
|
||||
"portainer" => "http://localhost:9000",
|
||||
"uptime-kuma" => "http://localhost:3001",
|
||||
"fedimint" => "http://localhost:8175",
|
||||
"fedimint" | "fedimintd" => "http://localhost:8175",
|
||||
"fedimint-gateway" => "http://localhost:8176",
|
||||
"nostr-rs-relay" => "http://localhost:18081",
|
||||
"indeedhub" => "http://localhost:7777",
|
||||
|
||||
162
deep-cleanup.sh
162
deep-cleanup.sh
@@ -1,162 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Archipelago Deep Cleanup Script
|
||||
# Removes ALL development caches and build artifacts safely
|
||||
# This is more aggressive than cleanup-mac.sh but 100% safe
|
||||
|
||||
set -e
|
||||
|
||||
echo "╔════════════════════════════════════════════════════════════════╗"
|
||||
echo "║ Archipelago DEEP Cleanup ║"
|
||||
echo "╚════════════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
echo "This will remove ALL caches and temporary files."
|
||||
echo "Everything can be rebuilt when needed."
|
||||
echo ""
|
||||
|
||||
TOTAL_SAVED=0
|
||||
|
||||
# Function to calculate and display savings
|
||||
calc_savings() {
|
||||
local path="$1"
|
||||
if [ -e "$path" ]; then
|
||||
local size=$(du -sk "$path" | cut -f1)
|
||||
TOTAL_SAVED=$((TOTAL_SAVED + size))
|
||||
fi
|
||||
}
|
||||
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "PROJECT DIRECTORY CLEANUP"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
# 1. Remove Rust build artifacts
|
||||
if [ -d "core/target" ]; then
|
||||
echo "🧹 Removing Rust build cache..."
|
||||
calc_savings "core/target"
|
||||
rm -rf core/target
|
||||
echo " ✅ Removed core/target/"
|
||||
fi
|
||||
|
||||
# 2. Remove node_modules (can be reinstalled with npm install)
|
||||
if [ -d "neode-ui/node_modules" ]; then
|
||||
echo "🧹 Removing node_modules (180 MB)..."
|
||||
calc_savings "neode-ui/node_modules"
|
||||
rm -rf neode-ui/node_modules
|
||||
echo " ✅ Removed neode-ui/node_modules/"
|
||||
echo " ℹ️ Run 'npm install' in neode-ui/ to restore"
|
||||
fi
|
||||
|
||||
# 3. Remove local dist builds (built to remote server anyway)
|
||||
if [ -d "neode-ui/dist" ]; then
|
||||
echo "🧹 Removing local frontend builds..."
|
||||
calc_savings "neode-ui/dist"
|
||||
rm -rf neode-ui/dist
|
||||
echo " ✅ Removed neode-ui/dist/"
|
||||
fi
|
||||
|
||||
if [ -d "web/dist" ]; then
|
||||
calc_savings "web/dist"
|
||||
rm -rf web/dist
|
||||
echo " ✅ Removed web/dist/"
|
||||
fi
|
||||
|
||||
# 4. Remove .DS_Store files
|
||||
echo "🧹 Removing .DS_Store files..."
|
||||
find . -name ".DS_Store" -type f -delete 2>/dev/null || true
|
||||
echo " ✅ Removed all .DS_Store files"
|
||||
|
||||
# 5. Remove ISO build artifacts
|
||||
if [ -d "image-recipe/build" ]; then
|
||||
echo "🧹 Removing ISO build artifacts..."
|
||||
calc_savings "image-recipe/build"
|
||||
rm -rf image-recipe/build
|
||||
echo " ✅ Removed image-recipe/build/"
|
||||
fi
|
||||
|
||||
# 6. Keep only the latest ISO
|
||||
if [ -d "image-recipe/results" ]; then
|
||||
ISO_COUNT=$(ls -1 image-recipe/results/*.iso 2>/dev/null | wc -l | tr -d ' ')
|
||||
if [ "$ISO_COUNT" -gt 1 ]; then
|
||||
echo "🧹 Cleaning old ISOs (keeping latest)..."
|
||||
cd image-recipe/results
|
||||
ls -t *.iso | tail -n +2 | while read iso; do
|
||||
calc_savings "$iso"
|
||||
rm "$iso"
|
||||
done
|
||||
cd ../..
|
||||
echo " ✅ Kept latest ISO only"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 7. Optimize Git repository
|
||||
echo "🧹 Optimizing Git repository..."
|
||||
BEFORE_GIT=$(du -sk .git | cut -f1)
|
||||
git gc --aggressive --prune=now 2>&1 | grep -v "^Enumerating" | grep -v "^Counting" | head -5 || true
|
||||
AFTER_GIT=$(du -sk .git | cut -f1)
|
||||
GIT_SAVED=$((BEFORE_GIT - AFTER_GIT))
|
||||
TOTAL_SAVED=$((TOTAL_SAVED + GIT_SAVED))
|
||||
echo " ✅ Git repository optimized"
|
||||
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "SYSTEM-WIDE CACHE CLEANUP"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
# 8. Clean Cargo cache (keeps only essential files)
|
||||
if [ -d ~/.cargo/registry ]; then
|
||||
echo "🧹 Cleaning Cargo registry cache..."
|
||||
calc_savings ~/.cargo/registry
|
||||
cargo cache -a 2>/dev/null || rm -rf ~/.cargo/registry/cache ~/.cargo/registry/src 2>/dev/null || true
|
||||
echo " ✅ Cleaned Cargo cache (~177 MB)"
|
||||
fi
|
||||
|
||||
# 9. Clean npm cache
|
||||
if [ -d ~/.npm ]; then
|
||||
echo "🧹 Cleaning npm cache..."
|
||||
calc_savings ~/.npm
|
||||
npm cache clean --force 2>/dev/null || true
|
||||
echo " ✅ Cleaned npm cache (~249 MB)"
|
||||
fi
|
||||
|
||||
# 10. Clean Homebrew cache
|
||||
if [ -d ~/Library/Caches/Homebrew ]; then
|
||||
echo "🧹 Cleaning Homebrew cache..."
|
||||
calc_savings ~/Library/Caches/Homebrew
|
||||
brew cleanup 2>/dev/null || true
|
||||
rm -rf ~/Library/Caches/Homebrew/* 2>/dev/null || true
|
||||
echo " ✅ Cleaned Homebrew cache (~890 MB)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "DOCKER DESKTOP (Manual Step)"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
DOCKER_SIZE=$(du -sk ~/Library/Containers/com.docker.docker 2>/dev/null | cut -f1 || echo "0")
|
||||
if [ "$DOCKER_SIZE" -gt 1000000 ]; then
|
||||
DOCKER_GB=$((DOCKER_SIZE / 1024 / 1024))
|
||||
echo "⚠️ Docker Desktop: ~${DOCKER_GB} GB"
|
||||
echo " To remove: Open Docker Desktop → Settings → Troubleshoot → Uninstall"
|
||||
echo " OR run: ./remove-docker.sh"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "╔════════════════════════════════════════════════════════════════╗"
|
||||
echo "║ DEEP Cleanup Complete! ║"
|
||||
echo "╚════════════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
SAVED_GB=$((TOTAL_SAVED / 1024 / 1024))
|
||||
echo "💾 Total space saved: ~${SAVED_GB} GB"
|
||||
echo ""
|
||||
echo "✅ What's left (all you need):"
|
||||
echo " - Source code only (no caches)"
|
||||
echo " - Latest ISO file"
|
||||
echo " - Documentation"
|
||||
echo ""
|
||||
echo "ℹ️ To restore development dependencies:"
|
||||
echo " cd neode-ui && npm install"
|
||||
echo ""
|
||||
echo "🚀 Your workflow (unchanged):"
|
||||
echo " ./scripts/deploy-to-target.sh --live"
|
||||
echo " (builds remotely, no local dependencies needed)"
|
||||
echo ""
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/usr/bin/expect -f
|
||||
set timeout 60
|
||||
spawn rsync -avz --delete /Users/dorian/Projects/archy/web/dist/neode-ui/ archipelago@192.168.1.228:/opt/archipelago/web-ui/
|
||||
expect "password:"
|
||||
send "archipelago\r"
|
||||
expect eof
|
||||
@@ -1,35 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
REMOTE_SERVER="archipelago@192.168.1.228"
|
||||
|
||||
echo "╔════════════════════════════════════════════════════════════════╗"
|
||||
echo "║ Deploying Indeedhub + Updated Archipelago UI ║"
|
||||
echo "╚════════════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
# Step 1: Deploy Indeedhub container
|
||||
echo "📦 Step 1: Deploying Indeedhub container..."
|
||||
cd "/Users/dorian/Projects/Indeedhub Prototype"
|
||||
./deploy-to-archipelago.sh
|
||||
|
||||
# Step 2: Deploy updated frontend
|
||||
echo ""
|
||||
echo "📦 Step 2: Deploying updated Archipelago frontend..."
|
||||
cd /Users/dorian/Projects/archy
|
||||
|
||||
echo " Syncing frontend to server..."
|
||||
rsync -avz --delete \
|
||||
-e "ssh -o PreferredAuthentications=keyboard-interactive,password" \
|
||||
web/dist/neode-ui/ "$REMOTE_SERVER:/opt/archipelago/web-ui/"
|
||||
|
||||
echo ""
|
||||
echo "╔════════════════════════════════════════════════════════════════╗"
|
||||
echo "║ ✅ DEPLOYMENT COMPLETE! ║"
|
||||
echo "╚════════════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
echo "🎬 Indeedhub: http://192.168.1.228:7777"
|
||||
echo "🏠 Archipelago: https://192.168.1.228"
|
||||
echo ""
|
||||
echo " Indeedhub is now visible in the Archipelago app store!"
|
||||
echo ""
|
||||
@@ -1,20 +0,0 @@
|
||||
FROM docker.io/library/nginx:alpine
|
||||
|
||||
# Copy the static UI
|
||||
COPY index.html /usr/share/nginx/html/
|
||||
COPY tailwind.css /usr/share/nginx/html/
|
||||
|
||||
# Create assets directories first
|
||||
RUN mkdir -p /usr/share/nginx/html/assets/img/app-icons && \
|
||||
mkdir -p /usr/share/nginx/html/assets/img
|
||||
|
||||
# Copy Bitcoin Knots icon and background
|
||||
COPY bitcoin-knots.webp /usr/share/nginx/html/assets/img/app-icons/
|
||||
COPY bg-network.jpg /usr/share/nginx/html/assets/img/
|
||||
|
||||
# Copy custom Nginx config with Bitcoin RPC proxy
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 620 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB |
@@ -1,818 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
||||
<meta http-equiv="Pragma" content="no-cache">
|
||||
<meta http-equiv="Expires" content="0">
|
||||
<title>Bitcoin Knots - Archipelago</title>
|
||||
<link rel="stylesheet" href="tailwind.css">
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', sans-serif;
|
||||
min-height: 100vh;
|
||||
color: white;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Background - Web5 style */
|
||||
.bg-perspective-container {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: -10;
|
||||
perspective: 1000px;
|
||||
perspective-origin: 50% 50%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bg-layer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image: url('assets/img/bg-network.jpg');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
transition: all 0.45s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||
transform-style: preserve-3d;
|
||||
opacity: 1;
|
||||
transform: translateZ(0) scale(1);
|
||||
}
|
||||
|
||||
/* Dark overlay - Web5 style (0.8 opacity) */
|
||||
.overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
z-index: -5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Glass card - Archipelago standard with gradient border */
|
||||
.glass-card {
|
||||
position: relative;
|
||||
background: rgba(0, 0, 0, 0.60);
|
||||
backdrop-filter: blur(24px);
|
||||
-webkit-backdrop-filter: blur(24px);
|
||||
box-shadow:
|
||||
0 8px 24px rgba(0, 0, 0, 0.45),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.22);
|
||||
border-radius: 1rem;
|
||||
overflow-x: hidden;
|
||||
overflow-y: visible;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.glass-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
padding: 2px;
|
||||
background: linear-gradient(135deg, rgba(0, 0, 0, 0.8), transparent);
|
||||
-webkit-mask:
|
||||
linear-gradient(#fff 0 0) content-box,
|
||||
linear-gradient(#fff 0 0);
|
||||
-webkit-mask-composite: xor;
|
||||
mask-composite: exclude;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.glass-card > * {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* Glass button - Archipelago standard (secondary actions) */
|
||||
.glass-button {
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
backdrop-filter: blur(18px);
|
||||
-webkit-backdrop-filter: blur(18px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.glass-button:hover {
|
||||
color: white;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
/* Gradient button - Archipelago standard (primary actions) */
|
||||
.gradient-button {
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.15) 0%, rgba(0, 0, 0, 0.8) 100%);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.gradient-button:hover {
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.2) 0%, rgba(0, 0, 0, 0.9) 100%);
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.6);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.gradient-button:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
/* Interactive card - Archipelago standard (display only, no hover) */
|
||||
.info-card {
|
||||
position: relative;
|
||||
background: rgba(0, 0, 0, 0.60);
|
||||
backdrop-filter: blur(24px);
|
||||
-webkit-backdrop-filter: blur(24px);
|
||||
box-shadow:
|
||||
0 8px 24px rgba(0, 0, 0, 0.45),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.22);
|
||||
border-radius: 16px;
|
||||
padding: 12px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.info-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
padding: 2px;
|
||||
background: linear-gradient(135deg, rgba(0, 0, 0, 0.8), transparent);
|
||||
-webkit-mask:
|
||||
linear-gradient(#fff 0 0) content-box,
|
||||
linear-gradient(#fff 0 0);
|
||||
-webkit-mask-composite: xor;
|
||||
mask-composite: exclude;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Interactive button - Same as info-card but with hover effects */
|
||||
.info-card-button {
|
||||
position: relative;
|
||||
background: rgba(0, 0, 0, 0.60);
|
||||
backdrop-filter: blur(24px);
|
||||
-webkit-backdrop-filter: blur(24px);
|
||||
box-shadow:
|
||||
0 8px 24px rgba(0, 0, 0, 0.45),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.22);
|
||||
border-radius: 16px;
|
||||
padding: 12px;
|
||||
transition: all 0.3s ease;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.info-card-button::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
padding: 2px;
|
||||
background: linear-gradient(135deg, rgba(0, 0, 0, 0.8), transparent);
|
||||
-webkit-mask:
|
||||
linear-gradient(#fff 0 0) content-box,
|
||||
linear-gradient(#fff 0 0);
|
||||
-webkit-mask-composite: xor;
|
||||
mask-composite: exclude;
|
||||
pointer-events: none;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.info-card-button:hover {
|
||||
transform: translateY(-2px);
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
box-shadow:
|
||||
0 12px 32px rgba(0, 0, 0, 0.6),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.25);
|
||||
color: rgba(255, 255, 255, 1);
|
||||
}
|
||||
|
||||
.info-card-button:hover::before {
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.3), transparent);
|
||||
}
|
||||
|
||||
.info-card-button:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
/* Container */
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
padding-bottom: 4rem;
|
||||
}
|
||||
|
||||
/* Logo gradient border */
|
||||
.logo-gradient-border {
|
||||
position: relative;
|
||||
border-radius: 16px;
|
||||
padding: 3px;
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.6) 0%, rgba(0, 0, 0, 0.8) 100%);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.logo-gradient-border::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 3px;
|
||||
border-radius: 13px;
|
||||
background: #fff;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.logo-gradient-border img {
|
||||
border-radius: 13px;
|
||||
display: block;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
/* Ping animation for status dots */
|
||||
@keyframes ping {
|
||||
75%, 100% {
|
||||
transform: scale(2);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-ping {
|
||||
animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
|
||||
}
|
||||
|
||||
/* Pulsing glow for progress bar */
|
||||
@keyframes progressGlow {
|
||||
0%, 100% {
|
||||
box-shadow: 0 0 10px rgba(251, 146, 60, 0.5),
|
||||
0 0 20px rgba(251, 146, 60, 0.3),
|
||||
0 0 30px rgba(251, 146, 60, 0.1);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 20px rgba(251, 146, 60, 0.8),
|
||||
0 0 30px rgba(251, 146, 60, 0.5),
|
||||
0 0 40px rgba(251, 146, 60, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.progress-glow {
|
||||
animation: progressGlow 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Spinning animation */
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-spin-slow {
|
||||
animation: spin 3s linear infinite;
|
||||
}
|
||||
|
||||
/* Shimmer effect */
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: -1000px 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 1000px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.shimmer {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(255, 255, 255, 0) 0%,
|
||||
rgba(255, 255, 255, 0.1) 50%,
|
||||
rgba(255, 255, 255, 0) 100%
|
||||
);
|
||||
background-size: 1000px 100%;
|
||||
animation: shimmer 3s infinite;
|
||||
}
|
||||
|
||||
/* Number increment animation */
|
||||
@keyframes numberPulse {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.05);
|
||||
color: rgba(251, 146, 60, 1);
|
||||
}
|
||||
}
|
||||
|
||||
.number-update {
|
||||
animation: numberPulse 0.5s ease-in-out;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="bg-perspective-container">
|
||||
<div class="bg-layer"></div>
|
||||
</div>
|
||||
<div class="overlay"></div>
|
||||
|
||||
<div class="container">
|
||||
<!-- Header - Glass card with logo and node info -->
|
||||
<div class="glass-card p-6 mb-6">
|
||||
<div class="flex flex-col md:flex-row items-center md:items-center gap-4 md:gap-6">
|
||||
<!-- Logo - Top Left -->
|
||||
<div class="flex-shrink-0">
|
||||
<div class="logo-gradient-border">
|
||||
<img
|
||||
src="assets/img/app-icons/bitcoin-knots.webp"
|
||||
alt="Bitcoin Knots"
|
||||
class="w-16 h-16"
|
||||
style="object-fit: contain;"
|
||||
onerror="this.style.display='none'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Title and Description -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<h1 class="text-3xl font-bold text-white mb-2">Bitcoin Knots</h1>
|
||||
<p class="text-white/70">Enhanced Bitcoin node implementation</p>
|
||||
</div>
|
||||
|
||||
<!-- Node Status Info - Compact on Desktop -->
|
||||
<div class="w-full md:w-auto flex flex-col md:flex-row gap-3 md:gap-4 mt-4 md:mt-0">
|
||||
<div class="info-card 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>
|
||||
<div>
|
||||
<p class="text-xs text-white/60">Status</p>
|
||||
<p class="text-sm font-medium text-white">Running</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-card 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>
|
||||
<div>
|
||||
<p class="text-xs text-white/60">Version</p>
|
||||
<p class="text-sm font-medium text-white" id="nodeVersion">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-card 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="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />
|
||||
</svg>
|
||||
<div>
|
||||
<p class="text-xs text-white/60">Network</p>
|
||||
<p class="text-sm font-medium text-white" id="networkType">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onclick="openSettings()"
|
||||
class="px-4 py-3 glass-button rounded-lg text-sm font-medium"
|
||||
>
|
||||
Settings
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Blockchain Sync Status Card - NEW -->
|
||||
<div class="glass-card p-6 mb-6" id="syncStatusCard">
|
||||
<div class="flex items-start gap-4 mb-4">
|
||||
<div class="flex-shrink-0 w-12 h-12 rounded-lg bg-orange-500/20 flex items-center justify-center">
|
||||
<svg class="w-6 h-6 text-orange-500 animate-spin-slow" fill="none" stroke="currentColor" viewBox="0 0 24 24" id="syncIcon">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h2 class="text-xl font-semibold text-white mb-1">Blockchain Sync</h2>
|
||||
<p class="text-white/70 text-sm" id="syncStatusText">Checking sync status...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Progress Bar -->
|
||||
<div class="mb-4">
|
||||
<div class="flex justify-between text-sm text-white/60 mb-2">
|
||||
<span id="currentBlock">Block 0</span>
|
||||
<span id="syncPercentage">0%</span>
|
||||
</div>
|
||||
<div class="w-full bg-white/10 rounded-full h-3 overflow-hidden relative shimmer">
|
||||
<div class="h-full bg-gradient-to-r from-orange-500 to-yellow-400 rounded-full transition-all duration-500 progress-glow" id="syncProgressBar" style="width: 0%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sync Stats Grid -->
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
<div class="info-card">
|
||||
<p class="text-xs text-white/60 mb-1">Current Height</p>
|
||||
<p class="text-lg font-semibold text-white transition-all" id="currentHeight">-</p>
|
||||
</div>
|
||||
<div class="info-card">
|
||||
<p class="text-xs text-white/60 mb-1">Network Height</p>
|
||||
<p class="text-lg font-semibold text-white" id="networkHeight">-</p>
|
||||
</div>
|
||||
<div class="info-card">
|
||||
<p class="text-xs text-white/60 mb-1">Headers</p>
|
||||
<p class="text-lg font-semibold text-white" id="headers">-</p>
|
||||
</div>
|
||||
<div class="info-card">
|
||||
<p class="text-xs text-white/60 mb-1">Verification</p>
|
||||
<p class="text-lg font-semibold text-white" id="verificationProgress">-</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Core Services Overview Cards - Web5 style -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 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="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">RPC Connection</h2>
|
||||
<p class="text-white/70 text-sm mb-4">JSON-RPC 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">RPC Host</span>
|
||||
</div>
|
||||
<span class="text-white/60 text-sm font-mono" id="rpcHost">localhost:8332</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="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
</svg>
|
||||
<span class="text-white/80 text-sm">RPC User</span>
|
||||
</div>
|
||||
<span class="text-white/60 text-sm font-mono">archipelago</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">RPC Status</span>
|
||||
</div>
|
||||
<span class="text-green-400 text-sm font-medium">Connected</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="mt-4 w-full info-card-button text-sm font-medium" onclick="copyRPCInfo()">
|
||||
Copy RPC Info
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- ZMQ Notifications -->
|
||||
<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="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h2 class="text-xl font-semibold text-white mb-2">ZMQ Notifications</h2>
|
||||
<p class="text-white/70 text-sm mb-4">Real-time block and transaction updates</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="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">Block Notifications</span>
|
||||
</div>
|
||||
<span class="text-white/60 text-sm font-mono">tcp://localhost:28332</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 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
|
||||
</svg>
|
||||
<span class="text-white/80 text-sm">TX Notifications</span>
|
||||
</div>
|
||||
<span class="text-white/60 text-sm font-mono">tcp://localhost:28333</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">ZMQ Status</span>
|
||||
</div>
|
||||
<span class="text-green-400 text-sm font-medium">Active</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="mt-4 w-full info-card-button text-sm font-medium" onclick="openLogs()">
|
||||
View Logs
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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>
|
||||
<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">Transaction Index</div>
|
||||
<div class="text-white/70 text-sm">Enabled (txindex=1)</div>
|
||||
</div>
|
||||
<div class="p-3 bg-white/5 rounded-lg">
|
||||
<div class="font-semibold text-white mb-1">ZMQ Publishing</div>
|
||||
<div class="text-white/70 text-sm">Block & TX notifications enabled</div>
|
||||
</div>
|
||||
<div class="p-3 bg-white/5 rounded-lg">
|
||||
<div class="font-semibold text-white mb-1">RPC Access</div>
|
||||
<div class="text-white/70 text-sm">Enabled on 0.0.0.0:18443</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Logs Modal -->
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
console.log('[Bitcoin UI] Script loaded, initializing...');
|
||||
|
||||
// RPC Configuration - Use local Nginx proxy within container
|
||||
const RPC_ENDPOINT = 'bitcoin-rpc/';
|
||||
console.log('[Bitcoin UI] RPC Endpoint:', RPC_ENDPOINT);
|
||||
|
||||
// Make RPC call to Bitcoin node via local proxy
|
||||
async function callRPC(method, params = []) {
|
||||
try {
|
||||
console.log(`[Bitcoin UI] Calling RPC method: ${method}`);
|
||||
const response = await fetch(RPC_ENDPOINT, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
jsonrpc: '1.0',
|
||||
id: 'bitcoin-ui',
|
||||
method: method,
|
||||
params: params
|
||||
})
|
||||
});
|
||||
|
||||
console.log(`[Bitcoin UI] RPC response status: ${response.status}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log(`[Bitcoin UI] RPC ${method} success:`, data.result ? 'OK' : 'Error');
|
||||
|
||||
if (data.error) {
|
||||
throw new Error(data.error.message);
|
||||
}
|
||||
return data.result;
|
||||
} catch (error) {
|
||||
console.error(`[Bitcoin UI] RPC call failed: ${method}`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Track last block count for animations
|
||||
let lastBlockCount = 0;
|
||||
|
||||
// Update blockchain info
|
||||
async function updateBlockchainInfo() {
|
||||
console.log('[Bitcoin UI] updateBlockchainInfo() called');
|
||||
try {
|
||||
const blockchainInfo = await callRPC('getblockchaininfo');
|
||||
console.log('[Bitcoin UI] blockchainInfo:', blockchainInfo);
|
||||
|
||||
if (!blockchainInfo) {
|
||||
console.error('[Bitcoin UI] No blockchain info received');
|
||||
document.getElementById('syncStatusText').textContent = 'Unable to connect to Bitcoin node';
|
||||
document.getElementById('syncStatusText').className = 'text-red-400 text-sm';
|
||||
return;
|
||||
}
|
||||
|
||||
const networkInfo = await callRPC('getnetworkinfo');
|
||||
|
||||
// Update network mode
|
||||
const chain = blockchainInfo.chain || 'unknown';
|
||||
const networkType = document.getElementById('networkType');
|
||||
|
||||
let networkShort = '';
|
||||
if (chain === 'regtest') {
|
||||
networkShort = 'Regtest';
|
||||
} else if (chain === 'test') {
|
||||
networkShort = 'Testnet';
|
||||
} else if (chain === 'main') {
|
||||
networkShort = 'Mainnet';
|
||||
} else {
|
||||
networkShort = chain;
|
||||
}
|
||||
|
||||
if (networkType) networkType.textContent = networkShort;
|
||||
|
||||
// Update sync status
|
||||
const blocks = blockchainInfo.blocks || 0;
|
||||
const headers = blockchainInfo.headers || 0;
|
||||
const verificationProgress = blockchainInfo.verificationprogress || 0;
|
||||
const isSynced = blocks >= headers - 1;
|
||||
|
||||
// Calculate actual sync percentage based on blocks/headers
|
||||
const actualSyncPercentage = headers > 0 ? ((blocks / headers) * 100).toFixed(2) : '0.00';
|
||||
const verificationPercentage = (verificationProgress * 100).toFixed(2);
|
||||
|
||||
// Animate block count if it changed
|
||||
const currentHeightElem = document.getElementById('currentHeight');
|
||||
if (blocks !== lastBlockCount && lastBlockCount > 0) {
|
||||
currentHeightElem.classList.add('number-update');
|
||||
setTimeout(() => currentHeightElem.classList.remove('number-update'), 500);
|
||||
}
|
||||
lastBlockCount = blocks;
|
||||
|
||||
currentHeightElem.textContent = blocks.toLocaleString();
|
||||
document.getElementById('networkHeight').textContent = headers.toLocaleString();
|
||||
document.getElementById('headers').textContent = headers.toLocaleString();
|
||||
document.getElementById('verificationProgress').textContent = `${verificationPercentage}%`;
|
||||
document.getElementById('syncPercentage').textContent = `${actualSyncPercentage}%`;
|
||||
document.getElementById('currentBlock').textContent = `Block ${blocks.toLocaleString()}`;
|
||||
document.getElementById('syncProgressBar').style.width = `${actualSyncPercentage}%`;
|
||||
|
||||
// Update sync status text and icon
|
||||
const syncStatusText = document.getElementById('syncStatusText');
|
||||
const syncIcon = document.getElementById('syncIcon');
|
||||
|
||||
if (isSynced) {
|
||||
syncStatusText.textContent = '✓ Fully synchronized with the network';
|
||||
syncStatusText.className = 'text-green-400 text-sm font-medium';
|
||||
// Stop spinning when synced
|
||||
if (syncIcon) {
|
||||
syncIcon.classList.remove('animate-spin-slow');
|
||||
syncIcon.classList.add('text-green-500');
|
||||
}
|
||||
} else {
|
||||
const remaining = headers - blocks;
|
||||
syncStatusText.textContent = `Syncing... ${remaining.toLocaleString()} blocks remaining`;
|
||||
syncStatusText.className = 'text-orange-400 text-sm font-medium';
|
||||
// Keep spinning while syncing
|
||||
if (syncIcon) {
|
||||
syncIcon.classList.add('animate-spin-slow');
|
||||
syncIcon.classList.remove('text-green-500');
|
||||
}
|
||||
}
|
||||
|
||||
// Update block height in quick actions (removed section)
|
||||
// document.getElementById('blockHeight').textContent = blocks.toLocaleString();
|
||||
|
||||
// Update version
|
||||
if (networkInfo && networkInfo.version) {
|
||||
const version = networkInfo.version;
|
||||
const versionStr = `v${Math.floor(version / 10000)}.${Math.floor((version % 10000) / 100)}.${version % 100}`;
|
||||
const versionElem = document.getElementById('nodeVersion');
|
||||
if (versionElem) versionElem.textContent = versionStr;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to update blockchain info:', error);
|
||||
document.getElementById('syncStatusText').textContent = 'Unable to fetch blockchain data';
|
||||
document.getElementById('syncStatusText').className = 'text-red-400 text-sm';
|
||||
}
|
||||
}
|
||||
|
||||
// Initial update
|
||||
console.log('[Bitcoin UI] Starting initial blockchain info update...');
|
||||
updateBlockchainInfo();
|
||||
|
||||
// Update every 5 seconds
|
||||
console.log('[Bitcoin UI] Setting up 5-second update interval');
|
||||
setInterval(updateBlockchainInfo, 5000);
|
||||
|
||||
function copyRPCInfo() {
|
||||
const info = `RPC Host: ${window.location.hostname}:8332\nRPC User: archipelago\nRPC Password: archipelago123\nRPC Endpoint: ${RPC_ENDPOINT}`;
|
||||
navigator.clipboard.writeText(info).then(() => {
|
||||
alert('RPC info copied to clipboard!');
|
||||
});
|
||||
}
|
||||
|
||||
function openSettings() {
|
||||
document.getElementById('settingsModal').classList.remove('hidden');
|
||||
document.getElementById('settingsModal').classList.add('flex');
|
||||
}
|
||||
|
||||
function closeSettings() {
|
||||
document.getElementById('settingsModal').classList.add('hidden');
|
||||
document.getElementById('settingsModal').classList.remove('flex');
|
||||
}
|
||||
|
||||
function openLogs() {
|
||||
document.getElementById('logsModal').classList.remove('hidden');
|
||||
document.getElementById('logsModal').classList.add('flex');
|
||||
loadLogs();
|
||||
}
|
||||
|
||||
function closeLogs() {
|
||||
document.getElementById('logsModal').classList.add('hidden');
|
||||
document.getElementById('logsModal').classList.remove('flex');
|
||||
}
|
||||
|
||||
async function loadLogs() {
|
||||
const logsContent = document.getElementById('logsContent');
|
||||
logsContent.textContent = 'Loading logs from node...';
|
||||
|
||||
try {
|
||||
const networkInfo = await callRPC('getnetworkinfo');
|
||||
const blockchainInfo = await callRPC('getblockchaininfo');
|
||||
const peerInfo = await callRPC('getpeerinfo');
|
||||
|
||||
if (networkInfo && blockchainInfo) {
|
||||
logsContent.textContent = `Bitcoin Knots version ${networkInfo.subversion || 'unknown'}
|
||||
Network: ${blockchainInfo.chain}
|
||||
Blocks: ${blockchainInfo.blocks}
|
||||
Headers: ${blockchainInfo.headers}
|
||||
Verification Progress: ${(blockchainInfo.verificationprogress * 100).toFixed(2)}%
|
||||
Connected Peers: ${peerInfo ? peerInfo.length : 0}
|
||||
Difficulty: ${blockchainInfo.difficulty}
|
||||
Chain Work: ${blockchainInfo.chainwork || 'N/A'}
|
||||
|
||||
Node is running and accepting connections.
|
||||
RPC server active on port 8332`;
|
||||
} else {
|
||||
logsContent.textContent = 'Unable to fetch node logs. Please check your RPC connection.';
|
||||
}
|
||||
} catch (error) {
|
||||
logsContent.textContent = `Error loading logs: ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
closeSettings();
|
||||
closeLogs();
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll('.modal').forEach(modal => {
|
||||
modal.addEventListener('click', (e) => {
|
||||
if (e.target === modal) {
|
||||
closeSettings();
|
||||
closeLogs();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,28 +0,0 @@
|
||||
server {
|
||||
listen 8334;
|
||||
server_name _;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# Bitcoin RPC proxy to avoid CORS issues
|
||||
location /bitcoin-rpc/ {
|
||||
# Proxy to localhost Bitcoin RPC (using host network mode)
|
||||
proxy_pass http://127.0.0.1:8332/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Authorization "Basic YXJjaGlwZWxhZ286YXJjaGlwZWxhZ28xMjM=";
|
||||
add_header Access-Control-Allow-Origin *;
|
||||
add_header Access-Control-Allow-Methods "POST, GET, OPTIONS";
|
||||
add_header Access-Control-Allow-Headers "Content-Type, Authorization";
|
||||
if ($request_method = OPTIONS) {
|
||||
return 204;
|
||||
}
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
@@ -1,210 +0,0 @@
|
||||
/* Tailwind CSS utilities — manually extracted for bitcoin-ui */
|
||||
/* Replaces cdn.tailwindcss.com to comply with CSP script-src 'self' */
|
||||
|
||||
*, ::before, ::after {
|
||||
box-sizing: border-box;
|
||||
border-width: 0;
|
||||
border-style: solid;
|
||||
border-color: currentColor;
|
||||
}
|
||||
img, svg { display: block; vertical-align: middle; max-width: 100%; height: auto; }
|
||||
button { cursor: pointer; background-color: transparent; font-family: inherit; font-size: 100%; line-height: inherit; color: inherit; margin: 0; padding: 0; }
|
||||
h1, h2 { font-size: inherit; font-weight: inherit; }
|
||||
|
||||
/* Position */
|
||||
.relative { position: relative; }
|
||||
.absolute { position: absolute; }
|
||||
.fixed { position: fixed; }
|
||||
.inset-0 { inset: 0; }
|
||||
|
||||
/* Display */
|
||||
.hidden { display: none; }
|
||||
.flex { display: flex; }
|
||||
.grid { display: grid; }
|
||||
|
||||
/* Flex */
|
||||
.flex-col { flex-direction: column; }
|
||||
.flex-row { flex-direction: row; }
|
||||
.flex-1 { flex: 1 1 0%; }
|
||||
.flex-shrink-0 { flex-shrink: 0; }
|
||||
.items-center { align-items: center; }
|
||||
.items-start { align-items: flex-start; }
|
||||
.justify-between { justify-content: space-between; }
|
||||
.justify-center { justify-content: center; }
|
||||
|
||||
/* Gap */
|
||||
.gap-2 { gap: 0.5rem; }
|
||||
.gap-3 { gap: 0.75rem; }
|
||||
.gap-4 { gap: 1rem; }
|
||||
.gap-6 { gap: 1.5rem; }
|
||||
|
||||
/* Grid */
|
||||
.grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); }
|
||||
.grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
|
||||
/* Width */
|
||||
.w-3 { width: 0.75rem; }
|
||||
.w-4 { width: 1rem; }
|
||||
.w-5 { width: 1.25rem; }
|
||||
.w-6 { width: 1.5rem; }
|
||||
.w-8 { width: 2rem; }
|
||||
.w-12 { width: 3rem; }
|
||||
.w-16 { width: 4rem; }
|
||||
.w-full { width: 100%; }
|
||||
|
||||
/* Height */
|
||||
.h-1 { height: 0.25rem; }
|
||||
.h-2 { height: 0.5rem; }
|
||||
.h-3 { height: 0.75rem; }
|
||||
.h-4 { height: 1rem; }
|
||||
.h-5 { height: 1.25rem; }
|
||||
.h-6 { height: 1.5rem; }
|
||||
.h-8 { height: 2rem; }
|
||||
.h-12 { height: 3rem; }
|
||||
.h-16 { height: 4rem; }
|
||||
.h-full { height: 100%; }
|
||||
|
||||
/* Min/Max */
|
||||
.min-w-0 { min-width: 0px; }
|
||||
.max-w-2xl { max-width: 42rem; }
|
||||
.max-w-4xl { max-width: 56rem; }
|
||||
.max-h-\[80vh\] { max-height: 80vh; }
|
||||
|
||||
/* Padding */
|
||||
.p-2 { padding: 0.5rem; }
|
||||
.p-3 { padding: 0.75rem; }
|
||||
.p-4 { padding: 1rem; }
|
||||
.p-6 { padding: 1.5rem; }
|
||||
.px-3 { padding-left: 0.75rem; padding-right: 0.75rem; }
|
||||
.px-4 { padding-left: 1rem; padding-right: 1rem; }
|
||||
.px-6 { padding-left: 1.5rem; padding-right: 1.5rem; }
|
||||
.py-2 { padding-top: 0.5rem; padding-bottom: 0.5rem; }
|
||||
.py-3 { padding-top: 0.75rem; padding-bottom: 0.75rem; }
|
||||
|
||||
/* Margin */
|
||||
.mb-1 { margin-bottom: 0.25rem; }
|
||||
.mb-2 { margin-bottom: 0.5rem; }
|
||||
.mb-4 { margin-bottom: 1rem; }
|
||||
.mb-6 { margin-bottom: 1.5rem; }
|
||||
.mb-8 { margin-bottom: 2rem; }
|
||||
.mt-2 { margin-top: 0.5rem; }
|
||||
.mt-4 { margin-top: 1rem; }
|
||||
.mx-auto { margin-left: auto; margin-right: auto; }
|
||||
|
||||
/* Typography — Size */
|
||||
.text-xs { font-size: 0.75rem; line-height: 1rem; }
|
||||
.text-sm { font-size: 0.875rem; line-height: 1.25rem; }
|
||||
.text-base { font-size: 1rem; line-height: 1.5rem; }
|
||||
.text-lg { font-size: 1.125rem; line-height: 1.75rem; }
|
||||
.text-xl { font-size: 1.25rem; line-height: 1.75rem; }
|
||||
.text-2xl { font-size: 1.5rem; line-height: 2rem; }
|
||||
.text-3xl { font-size: 1.875rem; line-height: 2.25rem; }
|
||||
|
||||
/* Typography — Weight */
|
||||
.font-medium { font-weight: 500; }
|
||||
.font-semibold { font-weight: 600; }
|
||||
.font-bold { font-weight: 700; }
|
||||
.font-mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; }
|
||||
.text-center { text-align: center; }
|
||||
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
|
||||
/* Typography — Color */
|
||||
.text-white { color: #ffffff; }
|
||||
.text-white\/40 { color: rgba(255, 255, 255, 0.4); }
|
||||
.text-white\/50 { color: rgba(255, 255, 255, 0.5); }
|
||||
.text-white\/60 { color: rgba(255, 255, 255, 0.6); }
|
||||
.text-white\/70 { color: rgba(255, 255, 255, 0.7); }
|
||||
.text-white\/80 { color: rgba(255, 255, 255, 0.8); }
|
||||
.text-green-400 { color: #4ade80; }
|
||||
.text-green-500 { color: #22c55e; }
|
||||
.text-orange-400 { color: #fb923c; }
|
||||
.text-orange-500 { color: #f97316; }
|
||||
.text-red-400 { color: #f87171; }
|
||||
.text-yellow-400 { color: #facc15; }
|
||||
|
||||
/* Backgrounds */
|
||||
.bg-green-400 { background-color: #4ade80; }
|
||||
.bg-green-500 { background-color: #22c55e; }
|
||||
.bg-orange-400 { background-color: #fb923c; }
|
||||
.bg-orange-500 { background-color: #f97316; }
|
||||
.bg-red-400 { background-color: #f87171; }
|
||||
.bg-orange-500\/20 { background-color: rgba(249, 115, 22, 0.2); }
|
||||
.bg-white\/5 { background-color: rgba(255, 255, 255, 0.05); }
|
||||
.bg-white\/10 { background-color: rgba(255, 255, 255, 0.1); }
|
||||
.bg-black\/20 { background-color: rgba(0, 0, 0, 0.2); }
|
||||
.bg-black\/40 { background-color: rgba(0, 0, 0, 0.4); }
|
||||
.bg-black\/60 { background-color: rgba(0, 0, 0, 0.6); }
|
||||
.bg-black\/80 { background-color: rgba(0, 0, 0, 0.8); }
|
||||
|
||||
/* Gradients */
|
||||
.bg-gradient-to-r { background-image: linear-gradient(to right, var(--tw-gradient-stops)); }
|
||||
.from-orange-500 { --tw-gradient-from: #f97316; --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to, rgba(249, 115, 22, 0)); }
|
||||
.from-orange-400 { --tw-gradient-from: #fb923c; --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to, rgba(251, 146, 60, 0)); }
|
||||
.to-yellow-400 { --tw-gradient-to: #facc15; }
|
||||
.to-orange-600 { --tw-gradient-to: #ea580c; }
|
||||
|
||||
/* Border */
|
||||
.border { border-width: 1px; }
|
||||
.border-white\/10 { border-color: rgba(255, 255, 255, 0.1); }
|
||||
.border-white\/20 { border-color: rgba(255, 255, 255, 0.2); }
|
||||
.border-orange-500\/30 { border-color: rgba(249, 115, 22, 0.3); }
|
||||
|
||||
/* Border Radius */
|
||||
.rounded { border-radius: 0.25rem; }
|
||||
.rounded-lg { border-radius: 0.5rem; }
|
||||
.rounded-xl { border-radius: 0.75rem; }
|
||||
.rounded-2xl { border-radius: 1rem; }
|
||||
.rounded-full { border-radius: 9999px; }
|
||||
|
||||
/* Overflow */
|
||||
.overflow-hidden { overflow: hidden; }
|
||||
.overflow-y-auto { overflow-y: auto; }
|
||||
|
||||
/* Opacity */
|
||||
.opacity-0 { opacity: 0; }
|
||||
.opacity-75 { opacity: 0.75; }
|
||||
.opacity-100 { opacity: 1; }
|
||||
|
||||
/* Z-Index */
|
||||
.z-10 { z-index: 10; }
|
||||
.z-50 { z-index: 50; }
|
||||
|
||||
/* Spacing utilities */
|
||||
.space-y-2 > :not([hidden]) ~ :not([hidden]) { margin-top: 0.5rem; }
|
||||
.space-y-3 > :not([hidden]) ~ :not([hidden]) { margin-top: 0.75rem; }
|
||||
.space-y-4 > :not([hidden]) ~ :not([hidden]) { margin-top: 1rem; }
|
||||
|
||||
/* Backdrop filter */
|
||||
.backdrop-blur-sm { -webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px); }
|
||||
.backdrop-blur-md { -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px); }
|
||||
.backdrop-blur-xl { -webkit-backdrop-filter: blur(24px); backdrop-filter: blur(24px); }
|
||||
|
||||
/* Transitions */
|
||||
.transition-all { transition-property: all; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms; }
|
||||
.transition-opacity { transition-property: opacity; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms; }
|
||||
.duration-300 { transition-duration: 300ms; }
|
||||
.duration-500 { transition-duration: 500ms; }
|
||||
|
||||
/* Text wrapping */
|
||||
.whitespace-pre-wrap { white-space: pre-wrap; }
|
||||
.break-all { word-break: break-all; }
|
||||
|
||||
/* Cursor */
|
||||
.cursor-pointer { cursor: pointer; }
|
||||
|
||||
/* Responsive: md (768px+) */
|
||||
@media (min-width: 768px) {
|
||||
.md\:flex-row { flex-direction: row; }
|
||||
.md\:items-center { align-items: center; }
|
||||
.md\:gap-4 { gap: 1rem; }
|
||||
.md\:gap-6 { gap: 1.5rem; }
|
||||
.md\:w-auto { width: auto; }
|
||||
.md\:mt-0 { margin-top: 0; }
|
||||
.md\:grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
||||
.md\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
}
|
||||
|
||||
/* Responsive: lg (1024px+) */
|
||||
@media (min-width: 1024px) {
|
||||
.lg\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
FROM docker.io/library/nginx:alpine
|
||||
|
||||
COPY index.html /usr/share/nginx/html/
|
||||
COPY qrcode.js /usr/share/nginx/html/
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
RUN mkdir -p /usr/share/nginx/html/assets/img
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
@@ -1,423 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
||||
<title>ElectrumX - Archipelago</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; min-height: 100vh; color: white; overflow-x: hidden; }
|
||||
.bg-layer { position: fixed; inset: 0; z-index: -10; background: linear-gradient(135deg, rgba(0,0,0,0.9) 0%, rgba(30,30,50,0.95) 100%); }
|
||||
.overlay { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.8); z-index: -5; }
|
||||
.glass-card { background: rgba(0, 0, 0, 0.6); backdrop-filter: blur(24px); -webkit-backdrop-filter: blur(24px); border-radius: 1rem; border: 1px solid rgba(255, 255, 255, 0.18); }
|
||||
.info-card { background: rgba(0, 0, 0, 0.6); backdrop-filter: blur(24px); border-radius: 16px; padding: 12px; border: 1px solid rgba(255, 255, 255, 0.1); }
|
||||
@keyframes progressGlow { 0%, 100% { box-shadow: 0 0 10px rgba(251, 146, 60, 0.5); } 50% { box-shadow: 0 0 20px rgba(251, 146, 60, 0.8); } }
|
||||
.progress-glow { animation: progressGlow 2s ease-in-out infinite; }
|
||||
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
|
||||
.animate-spin-slow { animation: spin 3s linear infinite; }
|
||||
.animate-pulse { animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; }
|
||||
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
|
||||
.container { max-width: 56rem; margin: 0 auto; padding: 2rem; }
|
||||
.flex { display: flex; }
|
||||
.flex-col { flex-direction: column; }
|
||||
.items-center { align-items: center; }
|
||||
.items-start { align-items: start; }
|
||||
.gap-3 { gap: 0.75rem; }
|
||||
.gap-4 { gap: 1rem; }
|
||||
.flex-1 { flex: 1; }
|
||||
.flex-shrink-0 { flex-shrink: 0; }
|
||||
.mb-1 { margin-bottom: 0.25rem; }
|
||||
.mb-2 { margin-bottom: 0.5rem; }
|
||||
.mb-4 { margin-bottom: 1rem; }
|
||||
.mb-6 { margin-bottom: 1.5rem; }
|
||||
.p-6 { padding: 1.5rem; }
|
||||
.grid { display: grid; }
|
||||
.grid-cols-2 { grid-template-columns: repeat(2, 1fr); }
|
||||
.rounded-lg { border-radius: 0.5rem; }
|
||||
.rounded-full { border-radius: 9999px; }
|
||||
.overflow-hidden { overflow: hidden; }
|
||||
.transition-all { transition: all 0.5s ease; }
|
||||
.text-xs { font-size: 0.75rem; }
|
||||
.text-sm { font-size: 0.875rem; }
|
||||
.text-lg { font-size: 1.125rem; }
|
||||
.text-xl { font-size: 1.25rem; }
|
||||
.text-2xl { font-size: 1.5rem; }
|
||||
.font-bold { font-weight: 700; }
|
||||
.font-semibold { font-weight: 600; }
|
||||
.font-medium { font-weight: 500; }
|
||||
.font-mono { font-family: monospace; }
|
||||
.icon-box { width: 4rem; height: 4rem; border-radius: 0.5rem; background: rgba(249, 115, 22, 0.2); display: flex; align-items: center; justify-content: center; }
|
||||
.icon-box-sm { width: 3rem; height: 3rem; border-radius: 0.5rem; background: rgba(249, 115, 22, 0.2); display: flex; align-items: center; justify-content: center; }
|
||||
.status-dot { width: 0.75rem; height: 0.75rem; border-radius: 9999px; }
|
||||
.progress-bar-bg { width: 100%; background: rgba(255,255,255,0.1); border-radius: 9999px; height: 0.75rem; overflow: hidden; }
|
||||
.progress-bar { height: 100%; background: linear-gradient(to right, #f97316, #facc15); border-radius: 9999px; transition: width 0.5s ease; }
|
||||
.text-white { color: white; }
|
||||
.text-white-70 { color: rgba(255,255,255,0.7); }
|
||||
.text-white-60 { color: rgba(255,255,255,0.6); }
|
||||
.text-white-90 { color: rgba(255,255,255,0.9); }
|
||||
.text-amber { color: #fbbf24; }
|
||||
.text-green { color: #4ade80; }
|
||||
.text-red { color: #f87171; }
|
||||
.text-orange { color: #fb923c; }
|
||||
.bg-amber { background: #fbbf24; }
|
||||
.bg-green { background: #4ade80; }
|
||||
.bg-red { background: #f87171; }
|
||||
.bg-yellow { background: #facc15; }
|
||||
.justify-between { justify-content: space-between; }
|
||||
@media (min-width: 768px) {
|
||||
.md-flex-row { flex-direction: row; }
|
||||
.md-grid-cols-4 { grid-template-columns: repeat(4, 1fr); }
|
||||
}
|
||||
|
||||
/* Connection details */
|
||||
.conn-tabs { display: flex; background: rgba(255,255,255,0.08); border-radius: 0.5rem; overflow: hidden; margin-bottom: 1.5rem; }
|
||||
.conn-tab { flex: 1; padding: 0.625rem 1rem; text-align: center; font-size: 0.875rem; font-weight: 600; cursor: pointer; transition: all 0.2s ease; color: rgba(255,255,255,0.5); border: none; background: none; }
|
||||
.conn-tab.active { background: rgba(74, 222, 128, 0.2); color: #4ade80; }
|
||||
.conn-tab:hover:not(.active) { color: rgba(255,255,255,0.8); }
|
||||
.conn-layout { display: flex; flex-direction: column; gap: 1.5rem; }
|
||||
@media (min-width: 640px) { .conn-layout { flex-direction: row; } }
|
||||
.qr-box { flex-shrink: 0; width: 196px; background: white; border-radius: 0.75rem; padding: 0.75rem; display: flex; align-items: center; justify-content: center; }
|
||||
.qr-box img { width: 100%; height: auto; display: block; image-rendering: pixelated; }
|
||||
.conn-fields { flex: 1; display: flex; flex-direction: column; gap: 0.75rem; }
|
||||
.field-label { font-size: 0.6875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: rgba(255,255,255,0.45); margin-bottom: 0.25rem; }
|
||||
.field-row { display: flex; align-items: center; background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.1); border-radius: 0.5rem; overflow: hidden; }
|
||||
.field-value { flex: 1; padding: 0.625rem 0.875rem; font-family: monospace; font-size: 0.9375rem; color: rgba(255,255,255,0.9); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.copy-btn { padding: 0.625rem 0.75rem; background: none; border: none; border-left: 1px solid rgba(255,255,255,0.1); cursor: pointer; color: rgba(255,255,255,0.4); transition: all 0.2s ease; display: flex; align-items: center; }
|
||||
.copy-btn:hover { color: rgba(255,255,255,0.8); background: rgba(255,255,255,0.05); }
|
||||
.copy-btn.copied { color: #4ade80; }
|
||||
.field-row-split { display: flex; gap: 0.75rem; }
|
||||
.field-row-split > div { flex: 1; }
|
||||
.conn-disabled { text-align: center; padding: 2rem 1rem; }
|
||||
.help-text { margin-top: 1.5rem; padding-top: 1.25rem; border-top: 1px solid rgba(255,255,255,0.08); font-size: 0.875rem; color: rgba(255,255,255,0.5); line-height: 1.5; }
|
||||
.version-text { font-size: 0.75rem; color: rgba(255,255,255,0.4); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="bg-layer"></div>
|
||||
<div class="overlay"></div>
|
||||
|
||||
<div class="container">
|
||||
<!-- Header -->
|
||||
<div class="glass-card p-6 mb-6">
|
||||
<div class="flex flex-col md-flex-row items-center gap-4">
|
||||
<div class="icon-box flex-shrink-0">
|
||||
<svg style="width:2rem;height:2rem;color:#f97316" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-3">
|
||||
<h1 class="text-2xl font-bold text-white">ElectrumX</h1>
|
||||
<span class="version-text">v1.18.0</span>
|
||||
</div>
|
||||
<p class="text-white-70">Bitcoin Electrum server for wallet connections</p>
|
||||
</div>
|
||||
<div class="info-card flex items-center gap-3">
|
||||
<div id="statusDot" class="status-dot bg-yellow"></div>
|
||||
<div>
|
||||
<p class="text-xs text-white-60">Status</p>
|
||||
<p class="text-sm font-medium text-white" id="statusText">Checking...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sync Status -->
|
||||
<div class="glass-card p-6 mb-6">
|
||||
<div class="flex items-start gap-4 mb-4">
|
||||
<div class="icon-box-sm flex-shrink-0">
|
||||
<svg id="syncIcon" style="width:1.5rem;height:1.5rem;color:#f97316" class="animate-spin-slow" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h2 class="text-xl font-semibold text-white mb-1">Index Sync</h2>
|
||||
<p class="text-white-70 text-sm" id="syncStatusText">Checking sync status...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<div class="flex justify-between text-sm text-white-60 mb-2">
|
||||
<span id="currentBlock">Block 0</span>
|
||||
<span id="syncPercentage">0%</span>
|
||||
</div>
|
||||
<div class="progress-bar-bg">
|
||||
<div class="progress-bar progress-glow" id="syncProgressBar" style="width: 0%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 md-grid-cols-4 gap-3">
|
||||
<div class="info-card">
|
||||
<p class="text-xs text-white-60 mb-1">Indexed Height</p>
|
||||
<p class="text-lg font-semibold text-white" id="indexedHeight">-</p>
|
||||
</div>
|
||||
<div class="info-card">
|
||||
<p class="text-xs text-white-60 mb-1">Network Height</p>
|
||||
<p class="text-lg font-semibold text-white" id="networkHeight">-</p>
|
||||
</div>
|
||||
<div class="info-card">
|
||||
<p class="text-xs text-white-60 mb-1">Index Size</p>
|
||||
<p class="text-lg font-semibold text-white" id="indexSize">-</p>
|
||||
</div>
|
||||
<div class="info-card">
|
||||
<p class="text-xs text-white-60 mb-1">Progress</p>
|
||||
<p class="text-lg font-semibold text-white" id="progressPct">-</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Connection Details -->
|
||||
<div class="glass-card p-6">
|
||||
<h2 class="text-xl font-semibold text-white mb-1">Connect Your Wallet</h2>
|
||||
<p class="text-white-70 text-sm mb-4" id="connSubtitle">Use the following details to connect your wallet or application to ElectrumX.</p>
|
||||
|
||||
<div class="conn-tabs">
|
||||
<button class="conn-tab active" id="tabLocal" onclick="switchTab('local')">Local Network</button>
|
||||
<button class="conn-tab" id="tabTor" onclick="switchTab('tor')">Tor</button>
|
||||
</div>
|
||||
|
||||
<!-- Local Network Tab -->
|
||||
<div id="panelLocal" class="conn-layout">
|
||||
<div class="qr-box" id="qrLocalBox"></div>
|
||||
<div class="conn-fields">
|
||||
<div>
|
||||
<div class="field-label">Address</div>
|
||||
<div class="field-row">
|
||||
<span class="field-value" id="localAddress">-</span>
|
||||
<button class="copy-btn" onclick="copyField('localAddress', this)" title="Copy">
|
||||
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2" ry="2" stroke-width="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" stroke-width="2"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field-row-split">
|
||||
<div>
|
||||
<div class="field-label">Port</div>
|
||||
<div class="field-row">
|
||||
<span class="field-value">50001</span>
|
||||
<button class="copy-btn" onclick="copyText('50001', this)" title="Copy">
|
||||
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2" ry="2" stroke-width="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" stroke-width="2"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="field-label">SSL</div>
|
||||
<div class="field-row">
|
||||
<span class="field-value">Disabled</span>
|
||||
<button class="copy-btn" onclick="copyText('Disabled', this)" title="Copy">
|
||||
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2" ry="2" stroke-width="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" stroke-width="2"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tor Tab -->
|
||||
<div id="panelTor" style="display:none">
|
||||
<div id="torAvailable" class="conn-layout" style="display:none">
|
||||
<div class="qr-box" id="qrTorBox"></div>
|
||||
<div class="conn-fields">
|
||||
<div>
|
||||
<div class="field-label">Onion Address</div>
|
||||
<div class="field-row">
|
||||
<span class="field-value" id="torAddress" style="font-size:0.75rem">-</span>
|
||||
<button class="copy-btn" onclick="copyField('torAddress', this)" title="Copy">
|
||||
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2" ry="2" stroke-width="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" stroke-width="2"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field-row-split">
|
||||
<div>
|
||||
<div class="field-label">Port</div>
|
||||
<div class="field-row">
|
||||
<span class="field-value">50001</span>
|
||||
<button class="copy-btn" onclick="copyText('50001', this)" title="Copy">
|
||||
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2" ry="2" stroke-width="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" stroke-width="2"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="field-label">SSL</div>
|
||||
<div class="field-row">
|
||||
<span class="field-value">Disabled</span>
|
||||
<button class="copy-btn" onclick="copyText('Disabled', this)" title="Copy">
|
||||
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2" ry="2" stroke-width="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" stroke-width="2"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="torUnavailable" class="conn-disabled">
|
||||
<svg style="width:2.5rem;height:2.5rem;color:rgba(255,255,255,0.25);margin:0 auto 0.75rem" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
||||
</svg>
|
||||
<p class="text-white-70" style="font-size:0.9375rem">Tor hidden service not configured for ElectrumX.</p>
|
||||
<p class="text-white-60 text-sm" style="margin-top:0.375rem">Enable Tor for ElectrumX in Settings to connect remotely.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-text">
|
||||
Connect using <strong style="color:rgba(255,255,255,0.8)">Sparrow Wallet</strong>, <strong style="color:rgba(255,255,255,0.8)">Electrum</strong>, or any Electrum-protocol compatible wallet. Set the server to the address and port shown above with SSL disabled.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="qrcode.js"></script>
|
||||
<script>
|
||||
var currentTab = 'local';
|
||||
var torOnion = null;
|
||||
|
||||
function renderQR(containerId, text) {
|
||||
var container = document.getElementById(containerId);
|
||||
if (!container) return;
|
||||
container.innerHTML = '';
|
||||
try {
|
||||
var qr = qrcode(0, 'M');
|
||||
qr.addData(text);
|
||||
qr.make();
|
||||
container.innerHTML = qr.createImgTag(5, 0);
|
||||
} catch(e) {
|
||||
container.innerHTML = '<div style="color:#999;font-size:12px;text-align:center;padding:2rem">QR unavailable</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function switchTab(tab) {
|
||||
currentTab = tab;
|
||||
document.getElementById('tabLocal').classList.toggle('active', tab === 'local');
|
||||
document.getElementById('tabTor').classList.toggle('active', tab === 'tor');
|
||||
document.getElementById('panelLocal').style.display = tab === 'local' ? '' : 'none';
|
||||
document.getElementById('panelTor').style.display = tab === 'tor' ? '' : 'none';
|
||||
}
|
||||
|
||||
var COPY_SVG = '<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2" ry="2" stroke-width="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" stroke-width="2"/></svg>';
|
||||
var CHECK_SVG = '<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>';
|
||||
|
||||
function flashCopied(btn) {
|
||||
btn.classList.add('copied');
|
||||
var orig = btn.innerHTML;
|
||||
btn.innerHTML = CHECK_SVG;
|
||||
setTimeout(function() {
|
||||
btn.classList.remove('copied');
|
||||
btn.innerHTML = orig;
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
function copyField(id, btn) {
|
||||
var text = document.getElementById(id).textContent.trim();
|
||||
if (!text || text === '-') return;
|
||||
navigator.clipboard.writeText(text).then(function() { flashCopied(btn); });
|
||||
}
|
||||
|
||||
function copyText(text, btn) {
|
||||
navigator.clipboard.writeText(text).then(function() { flashCopied(btn); });
|
||||
}
|
||||
|
||||
function copyConnStr(type) {
|
||||
var id = type === 'tor' ? 'torConnStr' : 'localConnStr';
|
||||
var btn = type === 'tor' ? document.querySelector('#torAvailable .conn-string-copy') : document.getElementById('localCopyAll');
|
||||
var text = document.getElementById(id).textContent.trim();
|
||||
if (!text || text === '-') return;
|
||||
navigator.clipboard.writeText(text).then(function() {
|
||||
btn.classList.add('copied');
|
||||
var orig = btn.textContent;
|
||||
btn.textContent = 'Copied!';
|
||||
setTimeout(function() {
|
||||
btn.classList.remove('copied');
|
||||
btn.textContent = orig;
|
||||
}, 1500);
|
||||
});
|
||||
}
|
||||
|
||||
function updateConnectionInfo() {
|
||||
var host = window.location.hostname;
|
||||
document.getElementById('localAddress').textContent = host;
|
||||
renderQR('qrLocalBox', host + ':50001:t');
|
||||
|
||||
if (torOnion) {
|
||||
document.getElementById('torAvailable').style.display = '';
|
||||
document.getElementById('torUnavailable').style.display = 'none';
|
||||
document.getElementById('torAddress').textContent = torOnion;
|
||||
renderQR('qrTorBox', torOnion + ':50001:t');
|
||||
} else {
|
||||
document.getElementById('torAvailable').style.display = 'none';
|
||||
document.getElementById('torUnavailable').style.display = '';
|
||||
}
|
||||
}
|
||||
|
||||
function applyTorOnion(onion) {
|
||||
if (onion) {
|
||||
torOnion = onion;
|
||||
updateConnectionInfo();
|
||||
}
|
||||
}
|
||||
|
||||
async function updateStatus() {
|
||||
try {
|
||||
var resp = await fetch('electrs-status');
|
||||
var data = await resp.json();
|
||||
|
||||
// Extract Tor onion from status response
|
||||
if (data.tor_onion && !torOnion) {
|
||||
applyTorOnion(data.tor_onion);
|
||||
}
|
||||
|
||||
var indexedH = data.indexed_height || 0;
|
||||
var networkH = data.network_height || 0;
|
||||
var pct = data.progress_pct || 0;
|
||||
|
||||
document.getElementById('indexedHeight').textContent = indexedH > 0 ? indexedH.toLocaleString() : (data.status === 'indexing' ? 'Building...' : '-');
|
||||
document.getElementById('networkHeight').textContent = networkH > 0 ? networkH.toLocaleString() : '-';
|
||||
document.getElementById('indexSize').textContent = data.index_size || '-';
|
||||
document.getElementById('progressPct').textContent = pct > 0 ? pct.toFixed(1) + '%' : '-';
|
||||
document.getElementById('currentBlock').textContent = indexedH > 0 ? 'Block ' + indexedH.toLocaleString() : (data.index_size ? 'Index: ' + data.index_size : 'Block 0');
|
||||
document.getElementById('syncPercentage').textContent = pct > 0 ? pct.toFixed(1) + '%' : '0%';
|
||||
document.getElementById('syncProgressBar').style.width = Math.max(pct, 0.5) + '%';
|
||||
|
||||
var statusTextEl = document.getElementById('syncStatusText');
|
||||
var statusDot = document.getElementById('statusDot');
|
||||
var syncIcon = document.getElementById('syncIcon');
|
||||
|
||||
if (data.status === 'indexing') {
|
||||
statusTextEl.textContent = data.error || 'Building index...';
|
||||
statusTextEl.style.color = '#fbbf24';
|
||||
statusDot.className = 'status-dot bg-amber animate-pulse';
|
||||
document.getElementById('statusText').textContent = 'Indexing';
|
||||
syncIcon.classList.add('animate-spin-slow');
|
||||
document.getElementById('connSubtitle').textContent = 'Connections will be available once ElectrumX has completed syncing.';
|
||||
} else if (data.status === 'error') {
|
||||
statusTextEl.textContent = data.error || 'Unknown error';
|
||||
statusTextEl.style.color = '#f87171';
|
||||
statusDot.className = 'status-dot bg-red';
|
||||
document.getElementById('statusText').textContent = 'Error';
|
||||
document.getElementById('connSubtitle').textContent = 'Connections will be available once ElectrumX has completed syncing.';
|
||||
} else if (data.status === 'synced') {
|
||||
statusTextEl.textContent = 'Fully synchronized with the network';
|
||||
statusTextEl.style.color = '#4ade80';
|
||||
statusDot.className = 'status-dot bg-green';
|
||||
document.getElementById('statusText').textContent = 'Synced';
|
||||
syncIcon.classList.remove('animate-spin-slow');
|
||||
syncIcon.style.color = '#4ade80';
|
||||
document.getElementById('connSubtitle').textContent = 'Use the following details to connect your wallet or application to ElectrumX.';
|
||||
} else {
|
||||
var remaining = networkH - indexedH;
|
||||
statusTextEl.textContent = 'Syncing... ' + remaining.toLocaleString() + ' blocks remaining';
|
||||
statusTextEl.style.color = '#fb923c';
|
||||
statusDot.className = 'status-dot bg-yellow';
|
||||
document.getElementById('statusText').textContent = 'Syncing';
|
||||
syncIcon.classList.add('animate-spin-slow');
|
||||
document.getElementById('connSubtitle').textContent = 'Connections will be available once ElectrumX has completed syncing.';
|
||||
}
|
||||
} catch (e) {
|
||||
document.getElementById('syncStatusText').textContent = 'Unable to fetch status: ' + e.message;
|
||||
document.getElementById('syncStatusText').style.color = '#f87171';
|
||||
}
|
||||
}
|
||||
|
||||
updateStatus();
|
||||
updateConnectionInfo();
|
||||
setInterval(updateStatus, 5000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,19 +0,0 @@
|
||||
server {
|
||||
listen 50002;
|
||||
server_name _;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
location /electrs-status {
|
||||
proxy_pass http://127.0.0.1:5678/electrs-status;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
add_header Access-Control-Allow-Origin *;
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,47 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Endurain - Coming Soon</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.container {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
h1 {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
p {
|
||||
font-size: 1.2rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
.status {
|
||||
display: inline-block;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 20px;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Endurain</h1>
|
||||
<p>Application platform for decentralized services</p>
|
||||
<div class="status">🚧 Coming Soon</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,22 +0,0 @@
|
||||
FROM docker.io/library/nginx:alpine
|
||||
|
||||
# Copy the HTML file
|
||||
COPY index.html /usr/share/nginx/html/
|
||||
COPY tailwind.css /usr/share/nginx/html/
|
||||
COPY qrcode.js /usr/share/nginx/html/
|
||||
|
||||
# Create directories for assets
|
||||
RUN mkdir -p /usr/share/nginx/html/assets/img/app-icons && \
|
||||
mkdir -p /usr/share/nginx/html/assets/img
|
||||
|
||||
# Copy assets
|
||||
COPY lnd.svg /usr/share/nginx/html/assets/img/app-icons/
|
||||
COPY bg-web5.jpg /usr/share/nginx/html/assets/img/
|
||||
COPY bg-intro.jpg /usr/share/nginx/html/assets/img/
|
||||
|
||||
# Copy nginx config
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.0 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 570 KiB |
@@ -1,657 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
||||
<title>LND - Archipelago</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; min-height: 100vh; color: white; overflow-x: hidden; }
|
||||
.bg-layer { position: fixed; inset: 0; z-index: -10; background: linear-gradient(135deg, rgba(0,0,0,0.9) 0%, rgba(30,30,50,0.95) 100%); }
|
||||
.overlay { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.8); z-index: -5; }
|
||||
.glass-card { background: rgba(0, 0, 0, 0.6); backdrop-filter: blur(24px); -webkit-backdrop-filter: blur(24px); border-radius: 1rem; border: 1px solid rgba(255, 255, 255, 0.18); }
|
||||
.info-card { background: rgba(0, 0, 0, 0.6); backdrop-filter: blur(24px); border-radius: 16px; padding: 12px; border: 1px solid rgba(255, 255, 255, 0.1); }
|
||||
.info-card-button { background: rgba(0, 0, 0, 0.6); backdrop-filter: blur(24px); border-radius: 16px; padding: 12px; border: 1px solid rgba(255, 255, 255, 0.1); cursor: pointer; color: rgba(255,255,255,0.9); transition: all 0.3s ease; }
|
||||
.info-card-button:hover { transform: translateY(-2px); background: rgba(255,255,255,0.08); }
|
||||
.info-card-button:active { transform: translateY(1px); }
|
||||
.glass-button { background-color: rgba(0, 0, 0, 0.6); backdrop-filter: blur(18px); -webkit-backdrop-filter: blur(18px); border: 1px solid rgba(255, 255, 255, 0.18); color: rgba(255, 255, 255, 0.9); transition: all 0.3s ease; cursor: pointer; }
|
||||
.glass-button:hover { color: white; background-color: rgba(0, 0, 0, 0.7); }
|
||||
.glass-button:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.container { max-width: 56rem; margin: 0 auto; padding: 2rem; padding-bottom: 4rem; }
|
||||
.flex { display: flex; }
|
||||
.flex-col { flex-direction: column; }
|
||||
.items-center { align-items: center; }
|
||||
.items-start { align-items: start; }
|
||||
.justify-between { justify-content: space-between; }
|
||||
.gap-2 { gap: 0.5rem; }
|
||||
.gap-3 { gap: 0.75rem; }
|
||||
.gap-4 { gap: 1rem; }
|
||||
.gap-6 { gap: 1.5rem; }
|
||||
.flex-1 { flex: 1; }
|
||||
.flex-shrink-0 { flex-shrink: 0; }
|
||||
.min-w-0 { min-width: 0; }
|
||||
.mb-1 { margin-bottom: 0.25rem; }
|
||||
.mb-2 { margin-bottom: 0.5rem; }
|
||||
.mb-3 { margin-bottom: 0.75rem; }
|
||||
.mb-4 { margin-bottom: 1rem; }
|
||||
.mb-6 { margin-bottom: 1.5rem; }
|
||||
.mb-8 { margin-bottom: 2rem; }
|
||||
.mt-2 { margin-top: 0.5rem; }
|
||||
.mt-4 { margin-top: 1rem; }
|
||||
.p-2 { padding: 0.5rem; }
|
||||
.p-3 { padding: 0.75rem; }
|
||||
.p-4 { padding: 1rem; }
|
||||
.p-6 { padding: 1.5rem; }
|
||||
.px-3 { padding-left: 0.75rem; padding-right: 0.75rem; }
|
||||
.px-4 { padding-left: 1rem; padding-right: 1rem; }
|
||||
.px-6 { padding-left: 1.5rem; padding-right: 1.5rem; }
|
||||
.py-1-5 { padding-top: 0.375rem; padding-bottom: 0.375rem; }
|
||||
.py-2 { padding-top: 0.5rem; padding-bottom: 0.5rem; }
|
||||
.py-2-5 { padding-top: 0.625rem; padding-bottom: 0.625rem; }
|
||||
.py-3 { padding-top: 0.75rem; padding-bottom: 0.75rem; }
|
||||
.grid { display: grid; }
|
||||
.grid-cols-1 { grid-template-columns: repeat(1, 1fr); }
|
||||
.grid-cols-2 { grid-template-columns: repeat(2, 1fr); }
|
||||
.grid-cols-3 { grid-template-columns: repeat(3, 1fr); }
|
||||
.rounded-lg { border-radius: 0.5rem; }
|
||||
.rounded-xl { border-radius: 0.75rem; }
|
||||
.rounded-full { border-radius: 9999px; }
|
||||
.overflow-hidden { overflow: hidden; }
|
||||
.transition-all { transition: all 0.3s ease; }
|
||||
.text-xs { font-size: 0.75rem; }
|
||||
.text-sm { font-size: 0.875rem; }
|
||||
.text-lg { font-size: 1.125rem; }
|
||||
.text-xl { font-size: 1.25rem; }
|
||||
.text-2xl { font-size: 1.5rem; }
|
||||
.text-3xl { font-size: 1.875rem; }
|
||||
.font-bold { font-weight: 700; }
|
||||
.font-semibold { font-weight: 600; }
|
||||
.font-medium { font-weight: 500; }
|
||||
.font-mono { font-family: monospace; }
|
||||
.uppercase { text-transform: uppercase; }
|
||||
.tracking-wide { letter-spacing: 0.05em; }
|
||||
.truncate { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.w-full { width: 100%; }
|
||||
.text-white { color: white; }
|
||||
.text-white-90 { color: rgba(255,255,255,0.9); }
|
||||
.text-white-80 { color: rgba(255,255,255,0.8); }
|
||||
.text-white-70 { color: rgba(255,255,255,0.7); }
|
||||
.text-white-60 { color: rgba(255,255,255,0.6); }
|
||||
.text-white-50 { color: rgba(255,255,255,0.5); }
|
||||
.text-white-45 { color: rgba(255,255,255,0.45); }
|
||||
.text-white-40 { color: rgba(255,255,255,0.4); }
|
||||
.text-green { color: #4ade80; }
|
||||
.text-orange { color: #fb923c; }
|
||||
.text-red { color: #f87171; }
|
||||
.bg-green { background: #4ade80; }
|
||||
.bg-yellow { background: #facc15; }
|
||||
.bg-red { background: #f87171; }
|
||||
.icon-box { width: 4rem; height: 4rem; border-radius: 0.5rem; background: rgba(139, 92, 246, 0.2); display: flex; align-items: center; justify-content: center; }
|
||||
.icon-box-sm { width: 3rem; height: 3rem; border-radius: 0.5rem; background: rgba(255,255,255,0.1); display: flex; align-items: center; justify-content: center; }
|
||||
.status-dot { width: 0.75rem; height: 0.75rem; border-radius: 9999px; }
|
||||
.status-dot-sm { width: 0.625rem; height: 0.625rem; border-radius: 9999px; display: inline-block; }
|
||||
.relative { position: relative; }
|
||||
.version-text { font-size: 0.75rem; color: rgba(255,255,255,0.4); }
|
||||
|
||||
/* Logo border matching electrs icon-box but with LND purple accent */
|
||||
.logo-border { width: 4rem; height: 4rem; border-radius: 0.5rem; background: rgba(139, 92, 246, 0.2); display: flex; align-items: center; justify-content: center; overflow: hidden; }
|
||||
.logo-border img { width: 3rem; height: 3rem; object-fit: contain; }
|
||||
|
||||
/* Stat row inside info-card */
|
||||
.stat-row { display: flex; align-items: center; justify-content: space-between; padding: 0.75rem; background: rgba(255,255,255,0.05); border-radius: 0.5rem; }
|
||||
|
||||
/* Balance cards */
|
||||
.balance-card { padding: 1rem; background: rgba(0,0,0,0.3); border-radius: 0.5rem; border: 1px solid rgba(255,255,255,0.08); }
|
||||
|
||||
/* Ping animation */
|
||||
@keyframes ping { 75%, 100% { transform: scale(2); opacity: 0; } }
|
||||
.animate-ping { animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite; position: absolute; inset: 0; }
|
||||
|
||||
/* Connection details */
|
||||
.conn-select { width: 100%; padding: 0.75rem 1rem; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); border-radius: 0.5rem; color: white; font-size: 0.875rem; font-weight: 500; appearance: none; cursor: pointer; outline: none; background-image: url('data:image/svg+xml;utf8,<svg fill="white" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M7 10l5 5 5-5z"/></svg>'); background-repeat: no-repeat; background-position: right 12px center; background-size: 20px; }
|
||||
.conn-select:focus { border-color: rgba(255,255,255,0.25); }
|
||||
.conn-select option { background: #1a1a2e; color: white; }
|
||||
.conn-layout { display: flex; flex-direction: column; gap: 1.5rem; }
|
||||
@media (min-width: 640px) { .conn-layout { flex-direction: row; } }
|
||||
.qr-box { flex-shrink: 0; width: 196px; height: 196px; background: white; border-radius: 0.75rem; padding: 0.5rem; display: flex; align-items: center; justify-content: center; }
|
||||
.qr-box img { width: 100%; height: auto; display: block; image-rendering: pixelated; }
|
||||
.conn-fields { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0.75rem; }
|
||||
.field-label { font-size: 0.6875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: rgba(255,255,255,0.45); margin-bottom: 0.25rem; }
|
||||
.field-row { display: flex; align-items: center; background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.1); border-radius: 0.5rem; overflow: hidden; min-width: 0; }
|
||||
.field-value { flex: 1; min-width: 0; padding: 0.625rem 0.875rem; font-family: monospace; font-size: 0.8125rem; color: rgba(255,255,255,0.9); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.copy-btn { padding: 0.625rem 0.75rem; background: none; border: none; border-left: 1px solid rgba(255,255,255,0.1); cursor: pointer; color: rgba(255,255,255,0.4); transition: all 0.2s ease; display: flex; align-items: center; }
|
||||
.copy-btn:hover { color: rgba(255,255,255,0.8); background: rgba(255,255,255,0.05); }
|
||||
.copy-btn.copied { color: #4ade80; }
|
||||
.field-row-split { display: flex; gap: 0.75rem; }
|
||||
.field-row-split > div { flex: 1; }
|
||||
.help-text { margin-top: 1rem; padding-top: 1rem; border-top: 1px solid rgba(255,255,255,0.08); font-size: 0.875rem; color: rgba(255,255,255,0.5); line-height: 1.5; }
|
||||
|
||||
/* Modal */
|
||||
.modal-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.8); backdrop-filter: blur(4px); z-index: 50; align-items: center; justify-content: center; padding: 1rem; }
|
||||
.modal-overlay.visible { display: flex; }
|
||||
.modal-content { max-width: 42rem; width: 100%; max-height: 85vh; overflow: hidden; display: flex; flex-direction: column; }
|
||||
.modal-body { overflow-y: auto; flex: 1; min-height: 0; }
|
||||
.modal-tab { flex: 1; padding: 0.5rem 1rem; text-align: center; border-radius: 0.5rem; font-size: 0.875rem; font-weight: 500; cursor: pointer; transition: all 0.2s ease; color: rgba(255,255,255,0.6); background: none; border: none; }
|
||||
.modal-tab.active { background: rgba(255,255,255,0.2); color: white; }
|
||||
.modal-tab:not(.active):hover { color: rgba(255,255,255,0.9); }
|
||||
.tab-panel { display: none; }
|
||||
.tab-panel.active { display: block; }
|
||||
.logs-box { background: rgba(0,0,0,0.4); border-radius: 0.5rem; padding: 1rem; font-family: monospace; font-size: 0.75rem; color: rgba(255,255,255,0.8); white-space: pre-wrap; word-break: break-all; min-height: 200px; }
|
||||
|
||||
/* Responsive grid */
|
||||
@media (min-width: 768px) {
|
||||
.md-grid-cols-2 { grid-template-columns: repeat(2, 1fr); }
|
||||
.md-grid-cols-3 { grid-template-columns: repeat(3, 1fr); }
|
||||
.md-grid-cols-4 { grid-template-columns: repeat(4, 1fr); }
|
||||
.md-flex-row { flex-direction: row; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="bg-layer"></div>
|
||||
<div class="overlay"></div>
|
||||
|
||||
<div class="container">
|
||||
<!-- Header -->
|
||||
<div class="glass-card p-6 mb-6">
|
||||
<div class="flex flex-col md-flex-row items-center gap-4">
|
||||
<div class="logo-border flex-shrink-0">
|
||||
<img src="assets/img/app-icons/lnd.svg" alt="LND" />
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-3">
|
||||
<h1 class="text-2xl font-bold text-white">LND</h1>
|
||||
</div>
|
||||
<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>
|
||||
<button onclick="openSettings()" class="glass-button flex items-center gap-2 px-4 py-2-5 rounded-lg text-sm font-medium flex-shrink-0">
|
||||
<svg style="width:1.25rem;height:1.25rem" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 010 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 010-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" 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-2 md-grid-cols-4 gap-3">
|
||||
<div class="info-card flex items-center gap-3">
|
||||
<div class="relative" style="width:0.75rem;height:0.75rem">
|
||||
<div class="status-dot bg-green" id="statusDot"></div>
|
||||
<div class="status-dot bg-green animate-ping" id="statusPing" style="display:none;opacity:0.75"></div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-white-60 mb-1">Node Status</p>
|
||||
<p class="text-sm font-medium text-white" id="summaryNodeStatus">—</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-card flex items-center gap-3">
|
||||
<span style="font-size:1.5rem;color:#fb923c;font-weight:700">⚡</span>
|
||||
<div>
|
||||
<p class="text-xs text-white-60 mb-1">Channels</p>
|
||||
<p class="text-sm font-medium text-orange" id="channelCount">0</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-card flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="status-dot-sm bg-green" id="restDot"></div>
|
||||
<div>
|
||||
<p class="text-xs text-white-60 mb-1">REST API</p>
|
||||
<p class="text-sm font-medium text-white" id="summaryRestStatus">—</p>
|
||||
</div>
|
||||
</div>
|
||||
<button onclick="openSettings(); setSettingsTab('rest');" class="glass-button px-3 py-1-5 rounded-lg 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="status-dot-sm bg-green" id="grpcDot"></div>
|
||||
<div>
|
||||
<p class="text-xs text-white-60 mb-1">gRPC</p>
|
||||
<p class="text-sm font-medium text-white" id="summaryGrpcStatus">—</p>
|
||||
</div>
|
||||
</div>
|
||||
<button onclick="openSettings(); setSettingsTab('logs');" class="glass-button px-3 py-1-5 rounded-lg text-xs font-medium">Logs</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Wallet -->
|
||||
<div class="glass-card p-6 mb-8">
|
||||
<div class="flex items-start gap-4 mb-4">
|
||||
<div class="icon-box-sm flex-shrink-0">
|
||||
<svg style="width:1.5rem;height:1.5rem;color:#fb923c" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h2 class="text-xl font-semibold text-white mb-1">Wallet</h2>
|
||||
<p class="text-white-60 text-sm">Balance, Receive, and Send will appear here when connected to your node.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md-grid-cols-3 gap-3 mb-4">
|
||||
<div class="balance-card">
|
||||
<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="balance-card">
|
||||
<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="balance-card">
|
||||
<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>
|
||||
</div>
|
||||
<div class="flex gap-4">
|
||||
<button class="glass-button px-6 py-3 rounded-lg font-medium" disabled>Receive</button>
|
||||
<button class="glass-button px-6 py-3 rounded-lg font-medium" disabled>Send</button>
|
||||
</div>
|
||||
<p class="text-white-50 text-xs mt-4">Recent activity will be listed here.</p>
|
||||
</div>
|
||||
|
||||
<!-- Connect Your Wallet -->
|
||||
<div class="glass-card p-6 mb-8">
|
||||
<h2 class="text-xl font-semibold text-white mb-1">Connect Your Wallet</h2>
|
||||
<p class="text-white-70 text-sm mb-4" id="connSubtitle">Use a wallet like Zeus, Zap, or BlueWallet to connect remotely.</p>
|
||||
|
||||
<!-- Mode selector -->
|
||||
<div class="mb-4">
|
||||
<select id="connMode" onchange="updateConnInfo()" class="conn-select">
|
||||
<option value="rest-tor">REST (Tor)</option>
|
||||
<option value="rest-local">REST (Local Network)</option>
|
||||
<option value="grpc-tor">gRPC (Tor)</option>
|
||||
<option value="grpc-local">gRPC (Local Network)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Connection display -->
|
||||
<div class="conn-layout" id="connDisplay">
|
||||
<div class="qr-box" id="lndQrBox">
|
||||
<div style="color:#999;font-size:12px;text-align:center;padding:2rem">Loading...</div>
|
||||
</div>
|
||||
<div class="conn-fields">
|
||||
<div>
|
||||
<div class="field-label">Host</div>
|
||||
<div class="field-row">
|
||||
<span class="field-value" id="connHost">—</span>
|
||||
<button class="copy-btn" onclick="copyEl('connHost', this)" title="Copy">
|
||||
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2" ry="2" stroke-width="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" stroke-width="2"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field-row-split">
|
||||
<div>
|
||||
<div class="field-label">Port</div>
|
||||
<div class="field-row">
|
||||
<span class="field-value" id="connPort">—</span>
|
||||
<button class="copy-btn" onclick="copyEl('connPort', this)" title="Copy">
|
||||
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2" ry="2" stroke-width="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" stroke-width="2"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="field-label">Protocol</div>
|
||||
<div class="field-row">
|
||||
<span class="field-value" id="connProto">—</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button onclick="copyLndconnectUri()" class="glass-button w-full mt-2 px-4 py-2-5 rounded-lg text-sm font-medium" id="copyUriBtn">Copy lndconnect URI</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-text">
|
||||
Scan the QR code with <strong style="color:rgba(255,255,255,0.8)">Zeus</strong>, <strong style="color:rgba(255,255,255,0.8)">Zap</strong>, or <strong style="color:rgba(255,255,255,0.8)">BlueWallet</strong> to connect. Tor mode recommended for remote access.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabbed Settings Modal -->
|
||||
<div class="modal-overlay" id="settingsModal">
|
||||
<div class="glass-card p-6 modal-content">
|
||||
<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="flex gap-2 mb-4 flex-shrink-0" style="background:rgba(255,255,255,0.06);border-radius:0.5rem;padding:0.25rem">
|
||||
<button class="modal-tab active" data-tab="node">Node Status</button>
|
||||
<button class="modal-tab" data-tab="rest">REST API</button>
|
||||
<button class="modal-tab" data-tab="grpc">gRPC</button>
|
||||
<button class="modal-tab" data-tab="logs">Logs</button>
|
||||
</div>
|
||||
<div class="modal-body" style="display:flex;flex-direction:column;gap:1rem">
|
||||
<!-- Node Status tab -->
|
||||
<div id="panel-node" class="tab-panel active">
|
||||
<div class="flex items-start gap-4 mb-4">
|
||||
<div class="icon-box-sm flex-shrink-0">
|
||||
<svg style="width:1.5rem;height:1.5rem;color:rgba(255,255,255,0.8)" 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 style="display:flex;flex-direction:column;gap:0.75rem">
|
||||
<div class="stat-row">
|
||||
<span class="text-white-80 text-sm">Node Status</span>
|
||||
<span class="text-green text-sm font-medium" id="modalNodeStatus">—</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<span class="text-white-80 text-sm">Network</span>
|
||||
<span class="text-white-60 text-sm" id="modalNetwork">—</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<span class="text-white-80 text-sm">Version</span>
|
||||
<span class="text-white-60 text-sm" id="modalVersion">—</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;flex-direction:column;gap:0.75rem;margin-top:1rem">
|
||||
<div class="stat-row" style="flex-direction:column;align-items:flex-start">
|
||||
<div class="font-semibold text-white mb-1">Network Mode</div>
|
||||
<div class="text-white-70 text-sm" id="modalNetworkMode">—</div>
|
||||
</div>
|
||||
<div class="stat-row" style="flex-direction:column;align-items:flex-start">
|
||||
<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="icon-box-sm flex-shrink-0">
|
||||
<svg style="width:1.5rem;height:1.5rem;color:rgba(255,255,255,0.8)" 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 style="display:flex;flex-direction:column;gap:0.75rem">
|
||||
<div class="stat-row">
|
||||
<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="stat-row">
|
||||
<span class="text-white-80 text-sm">API Status</span>
|
||||
<span class="text-green text-sm font-medium" id="modalRestStatus">—</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<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="info-card-button w-full mt-4 text-sm font-medium py-3 rounded-lg" onclick="copyRESTInfo()" style="text-align:center;display:block">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="icon-box-sm flex-shrink-0">
|
||||
<svg style="width:1.5rem;height:1.5rem;color:rgba(255,255,255,0.8)" 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 style="display:flex;flex-direction:column;gap:0.75rem">
|
||||
<div class="stat-row">
|
||||
<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="stat-row">
|
||||
<span class="text-white-80 text-sm">gRPC Status</span>
|
||||
<span class="text-green text-sm font-medium" id="modalGrpcStatus">—</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<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="glass-button px-3 py-1-5 rounded-lg text-xs font-medium">Refresh</button>
|
||||
</div>
|
||||
<div class="logs-box" id="logsContent">Loading logs...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="qrcode.js"></script>
|
||||
<script>
|
||||
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') || (window.location.protocol + '//' + window.location.hostname);
|
||||
}
|
||||
|
||||
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() {
|
||||
document.getElementById('settingsModal').classList.add('visible');
|
||||
}
|
||||
|
||||
function closeSettings() {
|
||||
document.getElementById('settingsModal').classList.remove('visible');
|
||||
}
|
||||
|
||||
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 setText(id, text) {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.textContent = text;
|
||||
}
|
||||
|
||||
async function loadLogs() {
|
||||
const logsContent = document.getElementById('logsContent');
|
||||
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.';
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
applyLiveData(data);
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
fetchLiveData();
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') closeSettings();
|
||||
});
|
||||
|
||||
document.getElementById('settingsModal').addEventListener('click', (e) => {
|
||||
if (e.target.id === 'settingsModal') closeSettings();
|
||||
});
|
||||
|
||||
// --- Connect Your Wallet ---
|
||||
let lndConnInfo = null;
|
||||
|
||||
function renderQR(containerId, text) {
|
||||
const container = document.getElementById(containerId);
|
||||
if (!container) return;
|
||||
container.innerHTML = '';
|
||||
try {
|
||||
const qr = qrcode(0, 'L');
|
||||
qr.addData(text);
|
||||
qr.make();
|
||||
container.innerHTML = qr.createImgTag(3, 0);
|
||||
} catch(e) {
|
||||
container.innerHTML = '<div style="color:#999;font-size:11px;text-align:center;padding:2rem">QR too large for this mode</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function buildLndconnectUri(connHost, connPort, cert, macaroon, isTor) {
|
||||
let uri = 'lndconnect://' + connHost + ':' + connPort + '?';
|
||||
if (!isTor && cert) uri += 'cert=' + cert + '&';
|
||||
uri += 'macaroon=' + macaroon;
|
||||
return uri;
|
||||
}
|
||||
|
||||
function updateConnInfo() {
|
||||
if (!lndConnInfo) return;
|
||||
const mode = document.getElementById('connMode').value;
|
||||
const isTor = mode.includes('tor');
|
||||
const isRest = mode.includes('rest');
|
||||
const port = isRest ? lndConnInfo.rest_port : lndConnInfo.grpc_port;
|
||||
const connHost = isTor && lndConnInfo.tor_onion ? lndConnInfo.tor_onion : host;
|
||||
const proto = isRest ? 'REST' : 'gRPC';
|
||||
|
||||
setText('connHost', connHost);
|
||||
setText('connPort', String(port));
|
||||
setText('connProto', proto);
|
||||
|
||||
if (isTor && !lndConnInfo.tor_onion) {
|
||||
document.getElementById('lndQrBox').innerHTML = '<div style="color:#999;font-size:12px;text-align:center;padding:2rem">Tor not configured for LND</div>';
|
||||
document.getElementById('connSubtitle').textContent = 'Tor hidden service not available. Use Local Network mode.';
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('connSubtitle').textContent = 'Scan QR with Zeus, Zap, or BlueWallet to connect.';
|
||||
|
||||
const uri = buildLndconnectUri(connHost, port, lndConnInfo.cert_base64url, lndConnInfo.macaroon_base64url, isTor);
|
||||
renderQR('lndQrBox', uri);
|
||||
}
|
||||
|
||||
function copyEl(id, btn) {
|
||||
const text = document.getElementById(id).textContent.trim();
|
||||
if (!text || text === '—') return;
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
const orig = btn.innerHTML;
|
||||
btn.innerHTML = '<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>';
|
||||
btn.style.color = '#4ade80';
|
||||
setTimeout(() => { btn.innerHTML = orig; btn.style.color = ''; }, 1500);
|
||||
});
|
||||
}
|
||||
|
||||
function copyLndconnectUri() {
|
||||
if (!lndConnInfo) return;
|
||||
const mode = document.getElementById('connMode').value;
|
||||
const isTor = mode.includes('tor');
|
||||
const isRest = mode.includes('rest');
|
||||
const port = isRest ? lndConnInfo.rest_port : lndConnInfo.grpc_port;
|
||||
const connHost = isTor && lndConnInfo.tor_onion ? lndConnInfo.tor_onion : host;
|
||||
const uri = buildLndconnectUri(connHost, port, lndConnInfo.cert_base64url, lndConnInfo.macaroon_base64url, isTor);
|
||||
const btn = document.getElementById('copyUriBtn');
|
||||
navigator.clipboard.writeText(uri).then(() => {
|
||||
const orig = btn.textContent;
|
||||
btn.textContent = 'Copied!';
|
||||
btn.style.color = '#4ade80';
|
||||
setTimeout(() => { btn.textContent = orig; btn.style.color = ''; }, 1500);
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchConnectInfo() {
|
||||
try {
|
||||
const resp = await fetch(window.location.protocol + '//' + window.location.hostname + '/lnd-connect-info');
|
||||
if (!resp.ok) throw new Error('HTTP ' + resp.status);
|
||||
const data = await resp.json();
|
||||
if (data.cert_base64url) {
|
||||
lndConnInfo = data;
|
||||
// Auto-select local mode when Tor is not available
|
||||
if (!data.tor_onion) {
|
||||
document.getElementById('connMode').value = 'rest-local';
|
||||
document.getElementById('connSubtitle').textContent = 'Tor hidden service not available. Use Local Network mode.';
|
||||
}
|
||||
try { updateConnInfo(); } catch(ue) {
|
||||
document.getElementById('lndQrBox').innerHTML = '<div style="color:#f87171;font-size:11px;text-align:center;padding:1rem">Update error: ' + ue.message + '</div>';
|
||||
return;
|
||||
}
|
||||
} else if (data.error) {
|
||||
throw new Error(data.error);
|
||||
}
|
||||
} catch(e) {
|
||||
document.getElementById('lndQrBox').innerHTML = '<div style="color:#f87171;font-size:12px;text-align:center;padding:2rem">' + e.message + '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
fetchConnectInfo();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 179 KiB |
@@ -1,18 +0,0 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
location /lnd-connect-info {
|
||||
proxy_pass http://127.0.0.1:5678/lnd-connect-info;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,210 +0,0 @@
|
||||
/* Tailwind CSS utilities — manually extracted for bitcoin-ui */
|
||||
/* Replaces cdn.tailwindcss.com to comply with CSP script-src 'self' */
|
||||
|
||||
*, ::before, ::after {
|
||||
box-sizing: border-box;
|
||||
border-width: 0;
|
||||
border-style: solid;
|
||||
border-color: currentColor;
|
||||
}
|
||||
img, svg { display: block; vertical-align: middle; max-width: 100%; height: auto; }
|
||||
button { cursor: pointer; background-color: transparent; font-family: inherit; font-size: 100%; line-height: inherit; color: inherit; margin: 0; padding: 0; }
|
||||
h1, h2 { font-size: inherit; font-weight: inherit; }
|
||||
|
||||
/* Position */
|
||||
.relative { position: relative; }
|
||||
.absolute { position: absolute; }
|
||||
.fixed { position: fixed; }
|
||||
.inset-0 { inset: 0; }
|
||||
|
||||
/* Display */
|
||||
.hidden { display: none; }
|
||||
.flex { display: flex; }
|
||||
.grid { display: grid; }
|
||||
|
||||
/* Flex */
|
||||
.flex-col { flex-direction: column; }
|
||||
.flex-row { flex-direction: row; }
|
||||
.flex-1 { flex: 1 1 0%; }
|
||||
.flex-shrink-0 { flex-shrink: 0; }
|
||||
.items-center { align-items: center; }
|
||||
.items-start { align-items: flex-start; }
|
||||
.justify-between { justify-content: space-between; }
|
||||
.justify-center { justify-content: center; }
|
||||
|
||||
/* Gap */
|
||||
.gap-2 { gap: 0.5rem; }
|
||||
.gap-3 { gap: 0.75rem; }
|
||||
.gap-4 { gap: 1rem; }
|
||||
.gap-6 { gap: 1.5rem; }
|
||||
|
||||
/* Grid */
|
||||
.grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); }
|
||||
.grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
|
||||
/* Width */
|
||||
.w-3 { width: 0.75rem; }
|
||||
.w-4 { width: 1rem; }
|
||||
.w-5 { width: 1.25rem; }
|
||||
.w-6 { width: 1.5rem; }
|
||||
.w-8 { width: 2rem; }
|
||||
.w-12 { width: 3rem; }
|
||||
.w-16 { width: 4rem; }
|
||||
.w-full { width: 100%; }
|
||||
|
||||
/* Height */
|
||||
.h-1 { height: 0.25rem; }
|
||||
.h-2 { height: 0.5rem; }
|
||||
.h-3 { height: 0.75rem; }
|
||||
.h-4 { height: 1rem; }
|
||||
.h-5 { height: 1.25rem; }
|
||||
.h-6 { height: 1.5rem; }
|
||||
.h-8 { height: 2rem; }
|
||||
.h-12 { height: 3rem; }
|
||||
.h-16 { height: 4rem; }
|
||||
.h-full { height: 100%; }
|
||||
|
||||
/* Min/Max */
|
||||
.min-w-0 { min-width: 0px; }
|
||||
.max-w-2xl { max-width: 42rem; }
|
||||
.max-w-4xl { max-width: 56rem; }
|
||||
.max-h-\[80vh\] { max-height: 80vh; }
|
||||
|
||||
/* Padding */
|
||||
.p-2 { padding: 0.5rem; }
|
||||
.p-3 { padding: 0.75rem; }
|
||||
.p-4 { padding: 1rem; }
|
||||
.p-6 { padding: 1.5rem; }
|
||||
.px-3 { padding-left: 0.75rem; padding-right: 0.75rem; }
|
||||
.px-4 { padding-left: 1rem; padding-right: 1rem; }
|
||||
.px-6 { padding-left: 1.5rem; padding-right: 1.5rem; }
|
||||
.py-2 { padding-top: 0.5rem; padding-bottom: 0.5rem; }
|
||||
.py-3 { padding-top: 0.75rem; padding-bottom: 0.75rem; }
|
||||
|
||||
/* Margin */
|
||||
.mb-1 { margin-bottom: 0.25rem; }
|
||||
.mb-2 { margin-bottom: 0.5rem; }
|
||||
.mb-4 { margin-bottom: 1rem; }
|
||||
.mb-6 { margin-bottom: 1.5rem; }
|
||||
.mb-8 { margin-bottom: 2rem; }
|
||||
.mt-2 { margin-top: 0.5rem; }
|
||||
.mt-4 { margin-top: 1rem; }
|
||||
.mx-auto { margin-left: auto; margin-right: auto; }
|
||||
|
||||
/* Typography — Size */
|
||||
.text-xs { font-size: 0.75rem; line-height: 1rem; }
|
||||
.text-sm { font-size: 0.875rem; line-height: 1.25rem; }
|
||||
.text-base { font-size: 1rem; line-height: 1.5rem; }
|
||||
.text-lg { font-size: 1.125rem; line-height: 1.75rem; }
|
||||
.text-xl { font-size: 1.25rem; line-height: 1.75rem; }
|
||||
.text-2xl { font-size: 1.5rem; line-height: 2rem; }
|
||||
.text-3xl { font-size: 1.875rem; line-height: 2.25rem; }
|
||||
|
||||
/* Typography — Weight */
|
||||
.font-medium { font-weight: 500; }
|
||||
.font-semibold { font-weight: 600; }
|
||||
.font-bold { font-weight: 700; }
|
||||
.font-mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; }
|
||||
.text-center { text-align: center; }
|
||||
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
|
||||
/* Typography — Color */
|
||||
.text-white { color: #ffffff; }
|
||||
.text-white\/40 { color: rgba(255, 255, 255, 0.4); }
|
||||
.text-white\/50 { color: rgba(255, 255, 255, 0.5); }
|
||||
.text-white\/60 { color: rgba(255, 255, 255, 0.6); }
|
||||
.text-white\/70 { color: rgba(255, 255, 255, 0.7); }
|
||||
.text-white\/80 { color: rgba(255, 255, 255, 0.8); }
|
||||
.text-green-400 { color: #4ade80; }
|
||||
.text-green-500 { color: #22c55e; }
|
||||
.text-orange-400 { color: #fb923c; }
|
||||
.text-orange-500 { color: #f97316; }
|
||||
.text-red-400 { color: #f87171; }
|
||||
.text-yellow-400 { color: #facc15; }
|
||||
|
||||
/* Backgrounds */
|
||||
.bg-green-400 { background-color: #4ade80; }
|
||||
.bg-green-500 { background-color: #22c55e; }
|
||||
.bg-orange-400 { background-color: #fb923c; }
|
||||
.bg-orange-500 { background-color: #f97316; }
|
||||
.bg-red-400 { background-color: #f87171; }
|
||||
.bg-orange-500\/20 { background-color: rgba(249, 115, 22, 0.2); }
|
||||
.bg-white\/5 { background-color: rgba(255, 255, 255, 0.05); }
|
||||
.bg-white\/10 { background-color: rgba(255, 255, 255, 0.1); }
|
||||
.bg-black\/20 { background-color: rgba(0, 0, 0, 0.2); }
|
||||
.bg-black\/40 { background-color: rgba(0, 0, 0, 0.4); }
|
||||
.bg-black\/60 { background-color: rgba(0, 0, 0, 0.6); }
|
||||
.bg-black\/80 { background-color: rgba(0, 0, 0, 0.8); }
|
||||
|
||||
/* Gradients */
|
||||
.bg-gradient-to-r { background-image: linear-gradient(to right, var(--tw-gradient-stops)); }
|
||||
.from-orange-500 { --tw-gradient-from: #f97316; --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to, rgba(249, 115, 22, 0)); }
|
||||
.from-orange-400 { --tw-gradient-from: #fb923c; --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to, rgba(251, 146, 60, 0)); }
|
||||
.to-yellow-400 { --tw-gradient-to: #facc15; }
|
||||
.to-orange-600 { --tw-gradient-to: #ea580c; }
|
||||
|
||||
/* Border */
|
||||
.border { border-width: 1px; }
|
||||
.border-white\/10 { border-color: rgba(255, 255, 255, 0.1); }
|
||||
.border-white\/20 { border-color: rgba(255, 255, 255, 0.2); }
|
||||
.border-orange-500\/30 { border-color: rgba(249, 115, 22, 0.3); }
|
||||
|
||||
/* Border Radius */
|
||||
.rounded { border-radius: 0.25rem; }
|
||||
.rounded-lg { border-radius: 0.5rem; }
|
||||
.rounded-xl { border-radius: 0.75rem; }
|
||||
.rounded-2xl { border-radius: 1rem; }
|
||||
.rounded-full { border-radius: 9999px; }
|
||||
|
||||
/* Overflow */
|
||||
.overflow-hidden { overflow: hidden; }
|
||||
.overflow-y-auto { overflow-y: auto; }
|
||||
|
||||
/* Opacity */
|
||||
.opacity-0 { opacity: 0; }
|
||||
.opacity-75 { opacity: 0.75; }
|
||||
.opacity-100 { opacity: 1; }
|
||||
|
||||
/* Z-Index */
|
||||
.z-10 { z-index: 10; }
|
||||
.z-50 { z-index: 50; }
|
||||
|
||||
/* Spacing utilities */
|
||||
.space-y-2 > :not([hidden]) ~ :not([hidden]) { margin-top: 0.5rem; }
|
||||
.space-y-3 > :not([hidden]) ~ :not([hidden]) { margin-top: 0.75rem; }
|
||||
.space-y-4 > :not([hidden]) ~ :not([hidden]) { margin-top: 1rem; }
|
||||
|
||||
/* Backdrop filter */
|
||||
.backdrop-blur-sm { -webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px); }
|
||||
.backdrop-blur-md { -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px); }
|
||||
.backdrop-blur-xl { -webkit-backdrop-filter: blur(24px); backdrop-filter: blur(24px); }
|
||||
|
||||
/* Transitions */
|
||||
.transition-all { transition-property: all; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms; }
|
||||
.transition-opacity { transition-property: opacity; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms; }
|
||||
.duration-300 { transition-duration: 300ms; }
|
||||
.duration-500 { transition-duration: 500ms; }
|
||||
|
||||
/* Text wrapping */
|
||||
.whitespace-pre-wrap { white-space: pre-wrap; }
|
||||
.break-all { word-break: break-all; }
|
||||
|
||||
/* Cursor */
|
||||
.cursor-pointer { cursor: pointer; }
|
||||
|
||||
/* Responsive: md (768px+) */
|
||||
@media (min-width: 768px) {
|
||||
.md\:flex-row { flex-direction: row; }
|
||||
.md\:items-center { align-items: center; }
|
||||
.md\:gap-4 { gap: 1rem; }
|
||||
.md\:gap-6 { gap: 1.5rem; }
|
||||
.md\:w-auto { width: auto; }
|
||||
.md\:mt-0 { margin-top: 0; }
|
||||
.md\:grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
||||
.md\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
}
|
||||
|
||||
/* Responsive: lg (1024px+) */
|
||||
@media (min-width: 1024px) {
|
||||
.lg\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>MorphOS Server - Coming Soon</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.container {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
h1 {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
p {
|
||||
font-size: 1.2rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
.status {
|
||||
display: inline-block;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 20px;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>MorphOS Server</h1>
|
||||
<p>Flexible server platform for applications and services</p>
|
||||
<div class="status">🚧 Coming Soon</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,268 +0,0 @@
|
||||
# Beta Release - Bitcoin Knots Installation Guide
|
||||
|
||||
## For Beta Testers & End Users
|
||||
|
||||
### Prerequisites
|
||||
- Fresh Archipelago installation
|
||||
- 500GB+ disk space (for full blockchain)
|
||||
- Internet connection
|
||||
|
||||
---
|
||||
|
||||
## Automated Installation (One-Click from App Store)
|
||||
|
||||
**When ready for beta, Bitcoin Knots will be installable from the App Store UI:**
|
||||
|
||||
1. Navigate to **App Store** in Archipelago UI
|
||||
2. Find **Bitcoin Knots**
|
||||
3. Click **Install**
|
||||
4. Wait for installation to complete
|
||||
5. Click **Launch** to access the web UI
|
||||
|
||||
---
|
||||
|
||||
## Manual Installation (Current Method)
|
||||
|
||||
If installing via SSH/terminal:
|
||||
|
||||
```bash
|
||||
# 1. Install Bitcoin Knots node
|
||||
sudo podman run -d \
|
||||
--name bitcoin-knots \
|
||||
--restart unless-stopped \
|
||||
-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" \
|
||||
--label "com.archipelago.version=28.1" \
|
||||
--label "com.archipelago.category=bitcoin" \
|
||||
--label "com.archipelago.description.short=Full Bitcoin node implementation" \
|
||||
--label "com.archipelago.description.long=Bitcoin Knots is a derivative of Bitcoin Core with additional features and bug fixes. Maintain the full blockchain and validate all transactions." \
|
||||
--label "com.archipelago.license=MIT" \
|
||||
--label "com.archipelago.icon=/assets/img/app-icons/bitcoin-knots.webp" \
|
||||
--label "com.archipelago.port=8332" \
|
||||
--label "com.archipelago.repo=https://github.com/bitcoinknots/bitcoin" \
|
||||
docker.io/bitcoinknots/bitcoin:latest \
|
||||
-server=1 \
|
||||
-txindex=1 \
|
||||
-rpcallowip=0.0.0.0/0 \
|
||||
-rpcbind=0.0.0.0:8332 \
|
||||
-rpcuser=archipelago \
|
||||
-rpcpassword=archipelago123 \
|
||||
-dbcache=4096
|
||||
|
||||
# 2. Build Bitcoin UI (web interface)
|
||||
cd /tmp
|
||||
mkdir bitcoin-ui-build
|
||||
cd bitcoin-ui-build
|
||||
|
||||
# Create Dockerfile
|
||||
cat > Dockerfile << 'EOF'
|
||||
FROM docker.io/library/nginx:alpine
|
||||
COPY index.html /usr/share/nginx/html/
|
||||
RUN mkdir -p /usr/share/nginx/html/assets/img/app-icons && \
|
||||
mkdir -p /usr/share/nginx/html/assets/img
|
||||
EXPOSE 80
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
EOF
|
||||
|
||||
# Copy UI file (must be included in beta ISO or downloadable)
|
||||
cp /home/archipelago/archy/docker/bitcoin-ui/index.html .
|
||||
|
||||
# Build and deploy
|
||||
sudo podman build -t localhost/bitcoin-ui:latest .
|
||||
sudo podman run -d \
|
||||
--name bitcoin-ui \
|
||||
--restart unless-stopped \
|
||||
-p 8334:80 \
|
||||
--label "com.archipelago.app=bitcoin-ui" \
|
||||
--label "com.archipelago.parent=bitcoin-knots" \
|
||||
localhost/bitcoin-ui:latest
|
||||
|
||||
# Cleanup
|
||||
cd /tmp && rm -rf bitcoin-ui-build
|
||||
|
||||
echo "✅ Bitcoin Knots installed!"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## What Gets Deployed
|
||||
|
||||
### 1. Bitcoin Knots Node
|
||||
- **Container:** `docker.io/bitcoinknots/bitcoin:latest`
|
||||
- **Data:** `/var/lib/archipelago/bitcoin/` (blockchain storage)
|
||||
- **RPC Port:** 8332 (for other apps to connect)
|
||||
- **P2P Port:** 8333 (network connections)
|
||||
- **Default RPC Credentials:**
|
||||
- User: `archipelago`
|
||||
- Password: `archipelago123`
|
||||
|
||||
### 2. Bitcoin Web UI
|
||||
- **Container:** Custom nginx container
|
||||
- **Web Port:** 8334
|
||||
- **Features:**
|
||||
- Node status dashboard
|
||||
- RPC connection info
|
||||
- Block height display
|
||||
- Log viewer
|
||||
- Settings panel
|
||||
|
||||
---
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
After installation, verify:
|
||||
|
||||
- [ ] `bitcoin-knots` container is running: `sudo podman ps | grep bitcoin-knots`
|
||||
- [ ] `bitcoin-ui` container is running: `sudo podman ps | grep bitcoin-ui`
|
||||
- [ ] Bitcoin Knots appears in "My Apps" with status "Running"
|
||||
- [ ] Bitcoin Knots shows "Already Installed" in App Store
|
||||
- [ ] "Launch" button is visible and clickable
|
||||
- [ ] Clicking "Launch" opens http://YOUR-IP:8334
|
||||
- [ ] Web UI displays node information
|
||||
- [ ] Blockchain is syncing (check logs: `sudo podman logs -f bitcoin-knots`)
|
||||
|
||||
---
|
||||
|
||||
## Known Issues & Fixes
|
||||
|
||||
### Issue 1: "Already Installed" Not Showing
|
||||
|
||||
**Cause:** App ID mismatch between marketplace and container name.
|
||||
|
||||
**Fix Applied:**
|
||||
- Marketplace app ID changed from `bitcoin` to `bitcoin-knots`
|
||||
- Backend checks for `bitcoin-ui` container and maps to `bitcoin-knots`
|
||||
|
||||
### Issue 2: No Launch Button
|
||||
|
||||
**Cause:** Backend couldn't detect the UI container port.
|
||||
|
||||
**Fix Applied:**
|
||||
- Special case in backend to map `bitcoin-ui` → `bitcoin-knots`
|
||||
- Backend now uses port 8334 (UI) instead of 8332 (RPC)
|
||||
|
||||
### Issue 3: Container Not Detected
|
||||
|
||||
**Cause:** Backend runs as non-root, containers started with `sudo podman`.
|
||||
|
||||
**Fix Applied:**
|
||||
- Backend uses `sudo podman` commands
|
||||
- Sudoers configured: `archipelago ALL=(ALL) NOPASSWD: /usr/bin/podman`
|
||||
|
||||
---
|
||||
|
||||
## For Beta Release ISO
|
||||
|
||||
The auto-installer must include:
|
||||
|
||||
1. **Backend binary** with:
|
||||
- `sudo podman` support in `podman_client.rs`
|
||||
- Bitcoin Knots metadata in `docker_packages.rs`
|
||||
- Special UI container mapping logic
|
||||
|
||||
2. **Frontend** with:
|
||||
- Correct marketplace app ID: `bitcoin-knots`
|
||||
- Docker image: `docker.io/bitcoinknots/bitcoin:latest`
|
||||
|
||||
3. **Bitcoin UI files** in `/home/archipelago/archy/docker/bitcoin-ui/`:
|
||||
- `index.html`
|
||||
- `Dockerfile`
|
||||
|
||||
4. **System configuration**:
|
||||
- `/etc/sudoers.d/archipelago-podman` file
|
||||
- Nginx configuration
|
||||
- Archipelago systemd service
|
||||
|
||||
---
|
||||
|
||||
## Testing Script
|
||||
|
||||
Run this to verify everything works:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
echo "Testing Bitcoin Knots installation..."
|
||||
|
||||
# 1. Check containers
|
||||
BITCOIN_RUNNING=$(sudo podman ps --format "{{.Names}}" | grep -c "bitcoin-knots" || echo "0")
|
||||
UI_RUNNING=$(sudo podman ps --format "{{.Names}}" | grep -c "bitcoin-ui" || echo "0")
|
||||
|
||||
if [ "$BITCOIN_RUNNING" -eq "0" ]; then
|
||||
echo "❌ bitcoin-knots container not running"
|
||||
exit 1
|
||||
else
|
||||
echo "✅ bitcoin-knots container running"
|
||||
fi
|
||||
|
||||
if [ "$UI_RUNNING" -eq "0" ]; then
|
||||
echo "❌ bitcoin-ui container not running"
|
||||
exit 1
|
||||
else
|
||||
echo "✅ bitcoin-ui container running"
|
||||
fi
|
||||
|
||||
# 2. Test web UI
|
||||
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8334)
|
||||
if [ "$HTTP_CODE" -eq "200" ]; then
|
||||
echo "✅ Bitcoin UI accessible on port 8334"
|
||||
else
|
||||
echo "❌ Bitcoin UI not responding (HTTP $HTTP_CODE)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 3. Test RPC
|
||||
RPC_RESPONSE=$(curl -s --user archipelago:archipelago123 \
|
||||
--data-binary '{"jsonrpc":"1.0","id":"test","method":"getblockchaininfo","params":[]}' \
|
||||
-H 'content-type: text/plain;' http://localhost:8332/)
|
||||
|
||||
if echo "$RPC_RESPONSE" | grep -q '"result"'; then
|
||||
echo "✅ Bitcoin RPC responding"
|
||||
BLOCKS=$(echo "$RPC_RESPONSE" | grep -o '"blocks":[0-9]*' | cut -d: -f2)
|
||||
echo " Synced blocks: $BLOCKS"
|
||||
else
|
||||
echo "❌ Bitcoin RPC not responding"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "✅ All tests passed! Bitcoin Knots is working correctly."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## User Experience Flow
|
||||
|
||||
1. **Install from App Store** → Click "Install" on Bitcoin Knots
|
||||
2. **Backend deploys** → Both bitcoin-knots + bitcoin-ui containers
|
||||
3. **App appears in My Apps** → Shows "Running" status
|
||||
4. **App Store shows** → "Already Installed" badge
|
||||
5. **Launch button works** → Opens web UI on port 8334
|
||||
6. **User connects to node** → Via RPC or web UI
|
||||
|
||||
---
|
||||
|
||||
## Blockchain Sync Time
|
||||
|
||||
⏰ **Initial sync:** 1-7 days depending on:
|
||||
- Internet speed
|
||||
- Disk I/O performance
|
||||
- CPU power
|
||||
|
||||
**Monitor progress:**
|
||||
```bash
|
||||
sudo podman logs -f bitcoin-knots | grep "height="
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Important for Production
|
||||
|
||||
✅ **All components working**: Node, UI, detection, marketplace
|
||||
✅ **No manual intervention needed**: Fully automated from App Store
|
||||
✅ **Proper labeling**: Backend discovers everything via container labels
|
||||
✅ **User-friendly**: Launch button, status display, proper UI
|
||||
|
||||
**This is production-ready for beta release!**
|
||||
@@ -1,189 +0,0 @@
|
||||
# DID Onboarding Flow: Assessment & Implementation Plan
|
||||
|
||||
## Executive Summary
|
||||
|
||||
The current onboarding DID flow is **partially implemented** and has several significant gaps compared to the W3C DID protocol and Web5 expectations. The core `did:key` format is **correct**, but the user-facing flow includes mock/fake behavior and the backup/verify steps don't actually use the DID infrastructure.
|
||||
|
||||
---
|
||||
|
||||
## What We Have (Current State)
|
||||
|
||||
### ✅ Correct Implementation
|
||||
|
||||
| Component | Status | Notes |
|
||||
|-----------|--------|-------|
|
||||
| **did:key format** | ✅ Correct | Ed25519 multicodec `0xed 0x01`, base58btc encoding, `z` prefix |
|
||||
| **Key generation** | ✅ Correct | Ed25519 via `ed25519_dalek`, persisted at `/var/lib/archipelago/identity/` |
|
||||
| **node.did RPC** | ✅ Correct | Returns `{ did, pubkey }` from server state |
|
||||
| **Identity persistence** | ✅ Correct | Key survives reboots, 0o600 permissions on Unix |
|
||||
| **Sign/verify primitives** | ✅ Present | `NodeIdentity::sign()`, `NodeIdentity::verify()` exist in Rust |
|
||||
|
||||
### ⚠️ Partial / Misleading Implementation
|
||||
|
||||
| Component | Status | Issue |
|
||||
|-----------|--------|-------|
|
||||
| **OnboardingDid.vue** | ⚠️ Misleading copy | Says "Generate DID" but we *fetch* from server; key is created at first boot, not during onboarding |
|
||||
| **OnboardingVerify.vue** | ❌ Fake | Uses `generateMockSignature()` – random chars, no backend call. Doesn't prove DID control |
|
||||
| **OnboardingBackup.vue** | ❌ Non-functional | Backup is mock JSON with `{ did, kid }`; no encrypted key material; **restore is impossible** |
|
||||
| **kid usage** | ⚠️ Non-standard | We store `pubkey` as `kid`; proper did:key uses fragment like `#key-1` or `did:key:z...#key-1` |
|
||||
|
||||
### ❌ Missing
|
||||
|
||||
| Component | Status |
|
||||
|-----------|--------|
|
||||
| **node.sign RPC** | Not exposed – backend can sign but no API |
|
||||
| **Challenge-sign flow** | No backend support for proof-of-control |
|
||||
| **Encrypted backup** | No real backup with key material or recovery path |
|
||||
| **DID Document endpoint** | Not exposed (optional for did:key – can be derived client-side) |
|
||||
| **keyAgreement / X25519** | Not derived – full DID Document would need Ed25519→X25519 for encryption |
|
||||
|
||||
---
|
||||
|
||||
## DID Protocol Requirements (W3C / Web5)
|
||||
|
||||
### did:key Method (W3C CCG)
|
||||
|
||||
1. **Format**: `did:key:z<base58btc(multicodec + raw-public-key-bytes)>` ✅ We do this
|
||||
2. **DID Document**: Can be derived from the DID string; no registry. Libraries like `@digitalcredentials/did-method-key` expand it.
|
||||
3. **Verification methods**: `verificationMethod`, `authentication`, `assertionMethod`, `keyAgreement` (X25519 derived), `capabilityDelegation`, `capabilityInvocation`
|
||||
4. **Key ID (kid)**: Typically `{did}#key-1` or similar fragment
|
||||
|
||||
### Proof of Control
|
||||
|
||||
To prove control of a DID, you must **sign a challenge** with the private key. The verifier checks the signature against the public key in the DID. Our OnboardingVerify step claims to do this but **does not**.
|
||||
|
||||
### Backup / Recovery
|
||||
|
||||
A proper identity backup for recovery would:
|
||||
|
||||
- Include the private key (or encrypted key material)
|
||||
- Be encrypted with a user passphrase
|
||||
- Allow restore on a new device
|
||||
|
||||
Our backup has none of this – it's display-only.
|
||||
|
||||
---
|
||||
|
||||
## Recommended Implementation Plan
|
||||
|
||||
### Phase 1: Fix Verify Step (Proof of Control)
|
||||
|
||||
**Goal**: Replace the fake "Sign Challenge" with a real cryptographic proof.
|
||||
|
||||
1. **Backend**: Add `node.signChallenge` RPC
|
||||
- Input: `{ challenge: string }` (nonce from frontend)
|
||||
- Output: `{ signature: string }` (hex-encoded Ed25519 signature)
|
||||
- Uses `NodeIdentity::sign()` with `challenge.as_bytes()`
|
||||
|
||||
2. **Frontend (OnboardingVerify.vue)**:
|
||||
- Generate a random nonce (e.g. 32 bytes, base64)
|
||||
- Call `node.signChallenge({ challenge })`
|
||||
- Verify signature locally using the pubkey from `node.did` (optional – or trust server)
|
||||
- Display the real signature; remove `generateMockSignature()`
|
||||
|
||||
**Effort**: ~2–4 hours
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: Improve UX and Terminology
|
||||
|
||||
**Goal**: Align copy and flow with actual behavior.
|
||||
|
||||
1. **OnboardingDid.vue**:
|
||||
- Change "Generate DID" → "Get your node's identity" or "Retrieve DID"
|
||||
- Clarify that the DID is created when the node first starts (not on button click)
|
||||
- Optionally auto-fetch on mount if identity exists (no button needed for returning state)
|
||||
|
||||
2. **kid / Key ID**:
|
||||
- Use `#key-1` or full `{did}#key-1` in backup and state
|
||||
- Or follow [did:key key IDs](https://www.w3.org/TR/did-core/#relative-did-urls)
|
||||
|
||||
**Effort**: ~1–2 hours
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: Real Backup (Encrypted Export)
|
||||
|
||||
**Goal**: Backup that can actually be used for recovery.
|
||||
|
||||
**Design choice**: The private key lives on the **server**. Two options:
|
||||
|
||||
- **Option A (simpler)**: Backup is a signed, encrypted blob containing the key material. Restore requires:
|
||||
- Upload backup file
|
||||
- Enter passphrase
|
||||
- Server imports key and replaces current identity (or restores to same node)
|
||||
|
||||
- **Option B (more self-sovereign)**: User can export key to their own wallet. Higher complexity and key-handling risk.
|
||||
|
||||
**Recommended: Option A**
|
||||
|
||||
1. **Backend**: Add `node.createBackup` RPC
|
||||
- Input: `{ passphrase: string }`
|
||||
- Encrypt the raw key bytes (e.g. XChaCha20-Poly1305 or AES-256-GCM) with a key derived from passphrase (Argon2)
|
||||
- Return JSON: `{ version, did, backupBlob (base64), salt, ... }` or trigger download
|
||||
|
||||
2. **Backend**: Add `node.restoreBackup` RPC (for restore flow)
|
||||
- Input: `{ backupBlob, passphrase }`
|
||||
- Decrypt, validate, write to identity dir
|
||||
- Restart or reload identity
|
||||
|
||||
3. **Frontend (OnboardingBackup.vue)**:
|
||||
- Call `node.createBackup` instead of building mock JSON locally
|
||||
- Download the real backup file
|
||||
|
||||
4. **Restore flow**: Add a restore path (e.g. from login or onboarding options) that accepts backup file + passphrase and calls `node.restoreBackup`
|
||||
|
||||
**Effort**: ~1–2 days (crypto, testing, edge cases)
|
||||
|
||||
---
|
||||
|
||||
### Phase 4: DID Document & Web5 Interop (Optional)
|
||||
|
||||
**Goal**: Full compatibility with Web5 resolvers and DWN.
|
||||
|
||||
1. **DID Document endpoint**: `GET /.well-known/did.json` or `/did/{did}`
|
||||
- Resolve did:key to a full DID Document
|
||||
- Include `verificationMethod`, `authentication`, `keyAgreement` (X25519 from Ed25519)
|
||||
- Reference: [did:key expansion](https://github.com/digitalbazaar/did-method-key)
|
||||
|
||||
2. **X25519 derivation**: Add `curve25519-dalek` or equivalent; derive X25519 pubkey from Ed25519 for `keyAgreement`
|
||||
|
||||
3. **Web5/DWN**: Ensure `web5-dwn` and `did-wallet` use our node DID correctly for resolution and operations
|
||||
|
||||
**Effort**: ~2–3 days
|
||||
|
||||
---
|
||||
|
||||
### Phase 5: DID as Authentication (Future)
|
||||
|
||||
**Goal**: Use DID + proof instead of (or in addition to) password.
|
||||
|
||||
- DID Auth / SIOP flow: prove control of DID via challenge-response
|
||||
- Could reduce or replace password for API access
|
||||
- Larger design and security review required
|
||||
|
||||
**Effort**: TBD
|
||||
|
||||
---
|
||||
|
||||
## Priority Recommendation
|
||||
|
||||
| Priority | Phase | Reason |
|
||||
|----------|-------|--------|
|
||||
| **P0** | Phase 1 (Verify) | Removes fake crypto; proves DID control |
|
||||
| **P1** | Phase 2 (UX) | Quick wins; honest representation of flow |
|
||||
| **P2** | Phase 3 (Backup) | Makes backup/restore actually useful |
|
||||
| **P3** | Phase 4 (DID Doc) | Needed for full Web5 interop |
|
||||
| **P4** | Phase 5 (DID Auth) | Longer-term identity architecture |
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference: Current vs. Target
|
||||
|
||||
| Step | Current | Target |
|
||||
|------|---------|--------|
|
||||
| DID fetch | `node.did` ✅ | Same, better UX |
|
||||
| Prove control | Fake random "signature" ❌ | Real `node.signChallenge` |
|
||||
| Backup | Mock JSON, no key ❌ | Encrypted key material + restore |
|
||||
| kid | Raw pubkey | `#key-1` or standard fragment |
|
||||
| Restore | Not possible | `node.restoreBackup` |
|
||||
@@ -12,13 +12,27 @@
|
||||
|
||||
| ID | Title | Priority | Status | Dependencies |
|
||||
|----|-------|----------|--------|--------------|
|
||||
| **BUG-20** | **ElectrumX always shows "Building..." not height** | **P2** | PLANNED | - |
|
||||
| **TASK-26** | **Rename fedimintd to "Fedimint Guardian" + icon** | **P2** | PLANNED | - |
|
||||
| **TASK-27** | **Add tab-launch icon to apps that open in tabs** | **P2** | PLANNED | - |
|
||||
| **TASK-28** | **Sort installed apps to end of marketplace** | **P2** | PLANNED | - |
|
||||
| **TASK-29** | **Fix mesh mobile: remove title/flash/peers header, fix gutters** | **P2** | PLANNED | - |
|
||||
| **TASK-30** | **On-Chain as first tab in receive Bitcoin modals** | **P2** | PLANNED | - |
|
||||
| **TASK-31** | **Sticky nav header (My Apps/App Store/Services + categories + search)** | **P2** | PLANNED | - |
|
||||
| **TASK-32** | **Integrate boot loader into deploy + build + production** | **P1** | ~~DONE~~ | - |
|
||||
| **BUG-33** | **CPU load alert threshold too low (8 = 2x cores)** | **P2** | PLANNED | - |
|
||||
| **TASK-34** | **Pentest findings remediation plan** | **P1** | PLANNED | - |
|
||||
| **TASK-35** | **Federation node names (show name not DID, hover for key)** | **P2** | PLANNED | - |
|
||||
| **TASK-36** | **Cleaner iframe error screen with remediation** | **P2** | PLANNED | - |
|
||||
| **BUG-37** | **Apps flicker Start/Launch during container scan** | **P2** | PLANNED | - |
|
||||
| **TASK-38** | **Add blockchain sync info to homepage System card** | **P2** | PLANNED | - |
|
||||
| **BUG-1** | **Random logout / CSRF mismatch** | **P0** | PLANNED | - |
|
||||
| **FEATURE-4** | **Onboarding loading screen with progress** | **P1** | IN PROGRESS | - |
|
||||
| **BUG-3** | **IndeedHub WebSocket spam in console** | **P2** | PLANNED | - |
|
||||
| **TASK-8** | **Security hardening (CRIT-01, CRIT-02, HIGHs)** | **P0** | PLANNED | - |
|
||||
| **TASK-9** | **Full feature testing sweep** | **P1** | PLANNED | - |
|
||||
| **TASK-10** | **ISO build verification + multi-hardware test** | **P1** | PLANNED | - |
|
||||
| **TASK-11** | **Rootless podman + restore security hardening** | **P1** | PLANNED | - |
|
||||
| **TASK-11** | **Rootless podman + restore security hardening** | **P1** | ~~DONE~~ | - |
|
||||
| **TASK-12** | **Beta telemetry — node reporting + monitoring panel** | **P1** | PLANNED | - |
|
||||
| **TASK-17** | **Alpha version tags + rollback strategy** | **P2** | PLANNED | - |
|
||||
|
||||
|
||||
@@ -1,316 +0,0 @@
|
||||
# Package Installation Architecture & Security
|
||||
|
||||
## Overview
|
||||
|
||||
Archipelago uses a **container-based app installation system** similar to StartOS, with enhanced security and flexibility.
|
||||
|
||||
## Installation Methods
|
||||
|
||||
### 1. **Web UI Marketplace** (Current Implementation)
|
||||
**How it works:**
|
||||
- User clicks "Install" in the marketplace
|
||||
- Frontend calls `package.install` RPC method
|
||||
- Backend pulls Docker image and creates Podman container
|
||||
- Container starts with predefined configuration
|
||||
|
||||
**Security:**
|
||||
- ✅ Image name validation (prevents injection attacks)
|
||||
- ✅ Resource limits (CPU, memory)
|
||||
- ⚠️ Uses hardcoded configs (will use manifests)
|
||||
- ⚠️ No image signature verification yet
|
||||
|
||||
**Pros:**
|
||||
- User-friendly (click to install)
|
||||
- Fast installation
|
||||
- Works for most Docker-based apps
|
||||
|
||||
**Cons:**
|
||||
- Limited configuration options
|
||||
- No manifest-based permissions yet
|
||||
- Requires internet for image pull
|
||||
|
||||
---
|
||||
|
||||
### 2. **Manifest-Based Installation** (Recommended Future)
|
||||
|
||||
**How it works:**
|
||||
```yaml
|
||||
# apps/home-assistant/manifest.yml
|
||||
app:
|
||||
id: home-assistant
|
||||
name: Home Assistant
|
||||
container:
|
||||
image: homeassistant/home-assistant:2024.1
|
||||
image_signature: cosign://... # Verify with Cosign
|
||||
security:
|
||||
capabilities: [NET_BIND_SERVICE]
|
||||
readonly_root: false
|
||||
network_policy: host
|
||||
resources:
|
||||
cpu_limit: 2
|
||||
memory_limit: 2Gi
|
||||
```
|
||||
|
||||
**Installation:**
|
||||
```bash
|
||||
# Backend reads manifest, validates, creates container with exact specs
|
||||
archipelago install --manifest apps/home-assistant/manifest.yml
|
||||
```
|
||||
|
||||
**Security Benefits:**
|
||||
- ✅ **Image verification** with Cosign signatures
|
||||
- ✅ **Explicit permissions** (capabilities, network access)
|
||||
- ✅ **Resource limits** from manifest
|
||||
- ✅ **Dependency resolution** (Bitcoin Core before LND)
|
||||
- ✅ **AppArmor/SELinux profiles** per app
|
||||
- ✅ **Audit trail** of what permissions were granted
|
||||
|
||||
---
|
||||
|
||||
### 3. **Sideload from .s9pk** (StartOS Compatible)
|
||||
|
||||
**How it works:**
|
||||
- User uploads `.s9pk` file (ZIP with manifest + Docker image)
|
||||
- Backend extracts, verifies signature
|
||||
- Creates container from embedded image
|
||||
|
||||
**Security:**
|
||||
- ✅ GPG signature verification
|
||||
- ✅ Offline installation (no internet needed)
|
||||
- ✅ User reviews permissions before install
|
||||
|
||||
---
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Current Implementation (package.install)
|
||||
|
||||
#### ✅ **What's Secure:**
|
||||
1. **Input Validation**
|
||||
```rust
|
||||
fn is_valid_docker_image(image: &str) -> bool {
|
||||
// Rejects shell metacharacters: & | ; ` $ ( ) < >
|
||||
// Prevents command injection
|
||||
}
|
||||
```
|
||||
|
||||
2. **Resource Limits**
|
||||
```rust
|
||||
run_args.push("--memory=2g");
|
||||
run_args.push("--cpus=2");
|
||||
```
|
||||
|
||||
3. **Rootless Podman** (future)
|
||||
- Containers run as non-root user
|
||||
- Reduced attack surface
|
||||
|
||||
#### ⚠️ **What Needs Improvement:**
|
||||
|
||||
1. **No Image Verification**
|
||||
- **Current**: Trusts Docker Hub/registries blindly
|
||||
- **Should**: Verify signatures with Cosign
|
||||
```bash
|
||||
cosign verify --key cosign.pub ghcr.io/owner/image:tag
|
||||
```
|
||||
|
||||
2. **Hardcoded Configs**
|
||||
- **Current**: `get_app_config()` has hardcoded ports/volumes
|
||||
- **Should**: Load from `apps/*/manifest.yml`
|
||||
|
||||
3. **No Permission Review**
|
||||
- **Current**: User doesn't see what access app gets
|
||||
- **Should**: Show permission prompt before install:
|
||||
```
|
||||
Home Assistant requests:
|
||||
- Network: Host (for device discovery)
|
||||
- Devices: /dev/ttyUSB0 (serial devices)
|
||||
- Capabilities: NET_BIND_SERVICE
|
||||
- Storage: 10GB
|
||||
|
||||
[Cancel] [Install]
|
||||
```
|
||||
|
||||
4. **No Dependency Resolution**
|
||||
- **Current**: Install apps independently
|
||||
- **Should**: Check dependencies (e.g., LND requires Bitcoin Core)
|
||||
|
||||
5. **No Network Isolation**
|
||||
- **Current**: Apps can access each other
|
||||
- **Should**: Isolated networks by default, explicit connections
|
||||
|
||||
---
|
||||
|
||||
##Security Best Practices
|
||||
|
||||
### Multi-Layer Security Model
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ 1. Supply Chain Security │
|
||||
│ - Cosign image signing │
|
||||
│ - SBOM (Software Bill of Materials) │
|
||||
│ - Vulnerability scanning │
|
||||
└─────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ 2. Installation Validation │
|
||||
│ - Signature verification │
|
||||
│ - Manifest schema validation │
|
||||
│ - Permission review │
|
||||
└─────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ 3. Runtime Isolation │
|
||||
│ - Rootless containers │
|
||||
│ - AppArmor/SELinux profiles │
|
||||
│ - Network isolation │
|
||||
│ - Resource limits │
|
||||
└─────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ 4. Monitoring & Audit │
|
||||
│ - Health checks │
|
||||
│ - Log collection │
|
||||
│ - Anomaly detection │
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Comparison with StartOS
|
||||
|
||||
| Feature | StartOS | Archipelago (Current) | Archipelago (Goal) |
|
||||
|---------|---------|----------------------|-------------------|
|
||||
| **Image Format** | .s9pk (custom) | Docker images | Both |
|
||||
| **Image Verification** | GPG signatures | ❌ None | ✅ Cosign |
|
||||
| **Permission System** | ✅ Manifest-based | ❌ Hardcoded | ✅ Manifest-based |
|
||||
| **Network Isolation** | ✅ Per-app networks | ❌ Shared network | ✅ Per-app networks |
|
||||
| **Dependency Resolution** | ✅ Automatic | ❌ Manual | ✅ Automatic |
|
||||
| **Resource Limits** | ✅ From manifest | ⚠️ Hardcoded | ✅ From manifest |
|
||||
| **Audit Trail** | ✅ Yes | ⚠️ Basic logs | ✅ Full audit |
|
||||
|
||||
---
|
||||
|
||||
## Recommended Next Steps
|
||||
|
||||
### Phase 1: Manifest-Based Installation (Priority: HIGH)
|
||||
1. Implement manifest parser in Rust
|
||||
2. Load configs from `apps/*/manifest.yml`
|
||||
3. Apply security policies from manifest
|
||||
4. Show permission prompt in UI
|
||||
|
||||
### Phase 2: Image Verification (Priority: HIGH)
|
||||
1. Integrate Cosign for signature verification
|
||||
2. Maintain whitelist of trusted signing keys
|
||||
3. Reject unsigned images in production
|
||||
|
||||
### Phase 3: Network Isolation (Priority: MEDIUM)
|
||||
1. Create isolated network per app
|
||||
2. Explicit inter-app connections (e.g., LND → Bitcoin Core RPC)
|
||||
3. Firewall rules per app
|
||||
|
||||
### Phase 4: Dependency Resolution (Priority: MEDIUM)
|
||||
1. Parse `dependencies` from manifests
|
||||
2. Auto-install dependencies
|
||||
3. Prevent removal of depended-upon apps
|
||||
|
||||
### Phase 5: Advanced Security (Priority: LOW)
|
||||
1. AppArmor profile generation per app
|
||||
2. Hardware attestation (TPM 2.0)
|
||||
3. Encrypted secrets storage (not plaintext volumes)
|
||||
|
||||
---
|
||||
|
||||
## How Users Will Install Apps (Production)
|
||||
|
||||
### Method 1: Trusted Marketplace (Recommended)
|
||||
```
|
||||
User → Marketplace → Verified Registry → Podman
|
||||
```
|
||||
- Pre-vetted apps with verified signatures
|
||||
- Manifests reviewed by Archipelago team
|
||||
- One-click install
|
||||
|
||||
### Method 2: Sideload (.s9pk)
|
||||
```
|
||||
User → Upload .s9pk → Verify Signature → Extract → Podman
|
||||
```
|
||||
- For community apps
|
||||
- User takes responsibility for trust
|
||||
|
||||
### Method 3: Advanced (Manual)
|
||||
```
|
||||
User → SSH → podman run with manifest → Manual config
|
||||
```
|
||||
- For developers/power users
|
||||
- Full control, no guardrails
|
||||
|
||||
---
|
||||
|
||||
## Security Philosophy
|
||||
|
||||
**Defense in Depth:**
|
||||
- Never trust a single layer
|
||||
- Verify at supply chain (Cosign)
|
||||
- Isolate at runtime (containers)
|
||||
- Monitor continuously (health checks)
|
||||
|
||||
**Principle of Least Privilege:**
|
||||
- Apps get only what they need
|
||||
- Explicit permissions in manifest
|
||||
- User approves before granting
|
||||
|
||||
**Transparency:**
|
||||
- Open manifests (readable YAML)
|
||||
- Clear permission requests
|
||||
- Audit logs of all actions
|
||||
|
||||
---
|
||||
|
||||
## Questions Answered
|
||||
|
||||
### Is package.install secure?
|
||||
**Current state:** Moderately secure
|
||||
- ✅ Input validation prevents injection
|
||||
- ✅ Resource limits prevent resource exhaustion
|
||||
- ❌ No image verification (trust Docker Hub)
|
||||
- ❌ No permission system yet
|
||||
|
||||
**With manifests:** Very secure
|
||||
- ✅ All of the above
|
||||
- ✅ Signature verification
|
||||
- ✅ Explicit permissions
|
||||
- ✅ Network isolation
|
||||
|
||||
### How do users install on actual OS?
|
||||
1. **Pre-installed in ISO**: Included in image build
|
||||
2. **Web UI Marketplace**: Click to install (current)
|
||||
3. **Sideload**: Upload .s9pk file
|
||||
4. **CLI**: SSH + podman commands (advanced)
|
||||
|
||||
The **Web UI is the primary method** for end users - simple, secure, auditable.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Roadmap
|
||||
|
||||
**v0.1.0 (Current):**
|
||||
- ✅ Basic `package.install` RPC
|
||||
- ✅ Hardcoded app configs
|
||||
- ✅ Input validation
|
||||
|
||||
**v0.2.0 (Next):**
|
||||
- Manifest parser
|
||||
- Load from `apps/*/manifest.yml`
|
||||
- Permission UI prompt
|
||||
|
||||
**v0.3.0:**
|
||||
- Cosign verification
|
||||
- Network isolation per app
|
||||
- Dependency resolution
|
||||
|
||||
**v1.0.0:**
|
||||
- Full security model
|
||||
- AppArmor profiles
|
||||
- Audit logging
|
||||
- Production-ready
|
||||
@@ -1,42 +0,0 @@
|
||||
# Nostr Discovery – Security & Data Exposure
|
||||
|
||||
## If Someone Saw the Published Data
|
||||
|
||||
The Nostr discovery feature previously published node identity (DID, Tor onion address, version) to public relays. If someone saw that data, here’s what they could have and how to respond.
|
||||
|
||||
### What Could Have Been Seen
|
||||
|
||||
1. **Relay operators** (relay.damus.io, relay.nostr.info):
|
||||
- Your server’s **IP address** when it connected to publish
|
||||
- The **Tor onion address** you advertised
|
||||
- **Timing** of when you published
|
||||
|
||||
2. **Anyone querying Nostr** for archipelago nodes:
|
||||
- Your **Tor onion address** (designed to be shareable)
|
||||
- Your **DID** (public identifier)
|
||||
- **Software version**
|
||||
|
||||
### Mitigations
|
||||
|
||||
| Exposure | Mitigation |
|
||||
|----------|------------|
|
||||
| **IP address** | Cannot be undone. If relay operators logged it, they still have it. Consider: moving to a new IP, using a VPN for future traffic, or treating the server as potentially identified. |
|
||||
| **Tor onion** | The revocation overwrites the Nostr event so new clients won’t see it. If someone cached the onion, they can still reach the node. To invalidate it: **rotate the Tor hidden service** (new onion, old one stops working). |
|
||||
| **DID** | Public by design; no mitigation needed. |
|
||||
| **Version** | Update to a newer version; old version info becomes less useful over time. |
|
||||
|
||||
### Rotating the Tor Hidden Service (New Onion)
|
||||
|
||||
To invalidate an exposed onion address:
|
||||
|
||||
1. Stop the Tor container.
|
||||
2. Remove the hidden service directory:
|
||||
`rm -rf /var/lib/archipelago/tor/hidden_service_archipelago`
|
||||
3. Restart the Tor container so it creates a new onion.
|
||||
4. Update any peers or links that used the old onion.
|
||||
|
||||
### Current Protections (Post-Fix)
|
||||
|
||||
- **Revocation**: On startup, the backend publishes a replacement Nostr event with empty content, so normal discovery no longer shows your node.
|
||||
- **Tor proxy**: Nostr traffic uses Tor (127.0.0.1:9050) so relay operators no longer see your IP.
|
||||
- **Opt-in defaults**: Discovery is on by default but only uses configured relays and routes through Tor.
|
||||
@@ -1,182 +0,0 @@
|
||||
# Tailscale Integration Guide
|
||||
|
||||
## Overview
|
||||
|
||||
Archipelago integrates with Tailscale to provide secure remote access via your personal VPN mesh network. When Tailscale is installed, users can access their Archipelago UI from anywhere using their Tailscale network.
|
||||
|
||||
## Automatic Configuration
|
||||
|
||||
### Installation Process
|
||||
|
||||
When a user installs Tailscale from the Archipelago App Store:
|
||||
|
||||
1. **Container Setup** (automatic)
|
||||
- Tailscale container runs with `--network=host` and `--privileged` mode
|
||||
- Creates `/var/lib/archipelago/tailscale` for persistent state
|
||||
- Starts Tailscale daemon and web UI on port 8240
|
||||
|
||||
2. **User Authentication** (user action required)
|
||||
- User clicks "Launch" on Tailscale app
|
||||
- Opens web UI at `http://<local-ip>:8240`
|
||||
- User logs in with their Tailscale account
|
||||
- Device registers to their tailnet
|
||||
|
||||
3. **Network Configuration** (automatic)
|
||||
- `tailscale0` interface is created with Tailscale IP (e.g., `100.91.10.103`)
|
||||
- Nginx detects the new interface and adds it to listen directives
|
||||
- Archipelago UI becomes accessible via Tailscale hostname
|
||||
|
||||
### Accessing via Tailscale
|
||||
|
||||
After setup, users can access Archipelago from any device on their tailnet:
|
||||
|
||||
```
|
||||
http://<hostname>.tail<xxxxxx>.ts.net/
|
||||
```
|
||||
|
||||
Example: `http://archipelago.tail2b6225.ts.net/`
|
||||
|
||||
## Technical Implementation
|
||||
|
||||
### Container Configuration
|
||||
|
||||
Tailscale requires special container permissions:
|
||||
|
||||
```rust
|
||||
// In rpc.rs - handle_package_install()
|
||||
if package_id == "tailscale" {
|
||||
run_args.push("--network=host"); // Access host network
|
||||
run_args.push("--privileged"); // Full container capabilities
|
||||
run_args.push("--cap-add=NET_ADMIN"); // Network administration
|
||||
run_args.push("--cap-add=NET_RAW"); // Raw packet access
|
||||
run_args.push("--device=/dev/net/tun"); // TUN device for VPN
|
||||
}
|
||||
```
|
||||
|
||||
### Nginx Configuration
|
||||
|
||||
The `configure-tailscale-nginx.sh` script automatically:
|
||||
|
||||
1. Detects the Tailscale IP from `tailscale0` interface
|
||||
2. Adds `listen <tailscale-ip>:80;` to Nginx config
|
||||
3. Reloads Nginx to accept connections from tailnet
|
||||
|
||||
### Post-Installation Automation
|
||||
|
||||
A systemd service (`archipelago-tailscale.service`) runs after Archipelago starts:
|
||||
|
||||
- Waits for `tailscale0` interface to exist
|
||||
- Runs configuration script
|
||||
- Ensures Nginx is ready for tailnet connections
|
||||
|
||||
## User Experience Flow
|
||||
|
||||
### First-Time Setup
|
||||
|
||||
1. **User installs Tailscale** from App Store
|
||||
- Container downloads and starts
|
||||
- "Launch" button appears in My Apps
|
||||
|
||||
2. **User authenticates**
|
||||
- Clicks "Launch" → opens web UI
|
||||
- Logs in with Tailscale account
|
||||
- Approves device in Tailscale admin console
|
||||
|
||||
3. **Automatic configuration**
|
||||
- System detects Tailscale connection
|
||||
- Nginx reconfigures automatically
|
||||
- User receives tailnet hostname
|
||||
|
||||
4. **Remote access enabled**
|
||||
- User can now access from anywhere
|
||||
- All devices on their tailnet can connect
|
||||
- Uses Tailscale's encrypted mesh network
|
||||
|
||||
### Ongoing Usage
|
||||
|
||||
- **No maintenance required** - Tailscale auto-starts with system
|
||||
- **Automatic reconnection** - Container restart policy handles disconnects
|
||||
- **Persistent state** - Authentication survives reboots
|
||||
- **Web UI always available** - Manage Tailscale at port 8240
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Why Privileged Mode?
|
||||
|
||||
Tailscale requires privileged mode because it:
|
||||
- Creates a TUN device for VPN traffic
|
||||
- Modifies iptables rules for routing
|
||||
- Manages network interfaces on the host
|
||||
|
||||
### Network Isolation
|
||||
|
||||
- Tailscale runs in host network mode (no container network isolation)
|
||||
- Only users on the same tailnet can access the Archipelago UI
|
||||
- Tailscale provides authentication and encryption
|
||||
|
||||
### Data Persistence
|
||||
|
||||
- Tailscale state is stored in `/var/lib/archipelago/tailscale/`
|
||||
- Contains device identity and credentials
|
||||
- Persists across container recreations
|
||||
- Automatically backed up with system
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Tailscale UI Not Loading
|
||||
|
||||
If the web UI doesn't load:
|
||||
|
||||
```bash
|
||||
# Check container status
|
||||
sudo podman ps --filter name=tailscale
|
||||
|
||||
# Check logs
|
||||
sudo podman logs tailscale
|
||||
|
||||
# Verify interface
|
||||
ip addr show tailscale0
|
||||
|
||||
# Check Nginx configuration
|
||||
sudo nginx -t
|
||||
sudo systemctl status nginx
|
||||
```
|
||||
|
||||
### Remote Access Not Working
|
||||
|
||||
If Tailscale hostname doesn't resolve:
|
||||
|
||||
```bash
|
||||
# Check Tailscale status
|
||||
sudo podman exec tailscale tailscale status
|
||||
|
||||
# Verify Nginx is listening on Tailscale IP
|
||||
sudo netstat -tlnp | grep :80
|
||||
|
||||
# Re-run configuration script
|
||||
sudo /opt/archipelago/scripts/configure-tailscale-nginx.sh
|
||||
```
|
||||
|
||||
### Container Won't Start
|
||||
|
||||
If container fails to start:
|
||||
|
||||
```bash
|
||||
# Check for permission issues
|
||||
sudo dmesg | grep -i deny
|
||||
|
||||
# Verify TUN device exists
|
||||
ls -l /dev/net/tun
|
||||
|
||||
# Check SELinux/AppArmor
|
||||
sudo ausearch -m avc -ts recent # SELinux
|
||||
sudo dmesg | grep -i apparmor # AppArmor
|
||||
```
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- **Automatic hostname detection** - Display tailnet URL in UI
|
||||
- **MagicDNS support** - Use short hostnames (just `archipelago`)
|
||||
- **Subnet routing** - Route to other networks via Archipelago
|
||||
- **Exit node mode** - Use Archipelago as internet gateway
|
||||
- **ACL integration** - Fine-grained access control via Tailscale ACLs
|
||||
@@ -1,214 +0,0 @@
|
||||
# How to Set Up Remote Access with Tailscale
|
||||
|
||||
Tailscale provides secure remote access to your Archipelago server from anywhere in the world using a zero-config VPN.
|
||||
|
||||
## Installation
|
||||
|
||||
1. **Install Tailscale from App Store**
|
||||
- Navigate to "App Store" in your Archipelago UI
|
||||
- Find "Tailscale" in the available apps
|
||||
- Click "Install"
|
||||
- Wait for installation to complete (container download)
|
||||
|
||||
2. **Access Setup Interface**
|
||||
- Go to "My Apps"
|
||||
- Find "Tailscale" in your installed apps
|
||||
- Click the **"Launch"** button
|
||||
- This opens the Tailscale web interface at `http://<your-ip>:8240`
|
||||
|
||||
## First-Time Setup
|
||||
|
||||
### Step 1: Sign In to Tailscale
|
||||
|
||||
When you click "Launch", you'll see the Tailscale web interface:
|
||||
|
||||
1. Click **"Sign in"** or **"Get Started"**
|
||||
2. You'll be prompted to authenticate with:
|
||||
- **Google** account
|
||||
- **Microsoft** account
|
||||
- **GitHub** account
|
||||
- Or create a new Tailscale account
|
||||
|
||||
3. Follow the authentication flow in your browser
|
||||
|
||||
### Step 2: Authorize the Device
|
||||
|
||||
After signing in:
|
||||
|
||||
1. Your Archipelago server will appear as a new device
|
||||
2. The device will be named something like `archipelago` or based on your hostname
|
||||
3. You may need to approve the device in your Tailscale admin console
|
||||
|
||||
### Step 3: Access Remotely
|
||||
|
||||
Once connected, you can access your Archipelago UI from anywhere:
|
||||
|
||||
**Via Tailscale Hostname:**
|
||||
```
|
||||
http://archipelago.tail<xxxxxx>.ts.net/
|
||||
```
|
||||
|
||||
**Via Tailscale IP:**
|
||||
```
|
||||
http://100.x.x.x/
|
||||
```
|
||||
|
||||
Your exact hostname and IP will be shown in the Tailscale web interface.
|
||||
|
||||
## Using Tailscale
|
||||
|
||||
### From Other Devices
|
||||
|
||||
To access your Archipelago from another device:
|
||||
|
||||
1. **Install Tailscale** on that device (phone, laptop, etc.)
|
||||
- iOS: Download from App Store
|
||||
- Android: Download from Play Store
|
||||
- Mac/Windows/Linux: Download from tailscale.com
|
||||
|
||||
2. **Sign in** with the same account you used for your Archipelago
|
||||
|
||||
3. **Connect** - Your devices are now on the same private network
|
||||
|
||||
4. **Access** your Archipelago using the tailnet hostname or IP
|
||||
|
||||
### Sharing Access
|
||||
|
||||
You can share your Archipelago with trusted users:
|
||||
|
||||
1. Open Tailscale admin console at https://login.tailscale.com/admin/machines
|
||||
2. Click on your Archipelago device
|
||||
3. Click **"Share"**
|
||||
4. Enter the email addresses of people you want to share with
|
||||
5. They'll receive an invitation to join your tailnet
|
||||
|
||||
## Managing Tailscale
|
||||
|
||||
### View Status
|
||||
|
||||
Click **"Launch"** on the Tailscale app in "My Apps" to:
|
||||
- See your tailnet hostname and IP
|
||||
- View connected devices
|
||||
- Check connection status
|
||||
- Manage device settings
|
||||
|
||||
### Stop/Start Tailscale
|
||||
|
||||
- **Stop**: Click the "Stop" button in "My Apps" - This disconnects your server from the tailnet
|
||||
- **Start**: Click the "Start" button to reconnect
|
||||
|
||||
### Disable Remote Access
|
||||
|
||||
If you want to temporarily disable remote access:
|
||||
|
||||
1. Go to "My Apps"
|
||||
2. Click "Stop" on Tailscale
|
||||
3. Remote access is now disabled (local network access still works)
|
||||
|
||||
### Uninstall Tailscale
|
||||
|
||||
To completely remove Tailscale:
|
||||
|
||||
1. Go to "My Apps"
|
||||
2. Click the "⋮" menu on Tailscale
|
||||
3. Select "Remove"
|
||||
4. Confirm removal
|
||||
|
||||
**Note**: Your Tailscale account and device registration remain intact if you want to reinstall later.
|
||||
|
||||
## Security & Privacy
|
||||
|
||||
### What Tailscale Can See
|
||||
|
||||
Tailscale operates on a zero-trust model:
|
||||
- ✅ **End-to-end encrypted** - All traffic is encrypted between your devices
|
||||
- ✅ **Peer-to-peer** - Direct connections when possible (no relay server)
|
||||
- ✅ **No data access** - Tailscale cannot see your traffic or data
|
||||
- ✅ **Open source** - Client and protocol are open source
|
||||
|
||||
### Best Practices
|
||||
|
||||
1. **Use Strong Authentication**
|
||||
- Enable 2FA on your Tailscale account
|
||||
- Use a strong password for your Archipelago login
|
||||
|
||||
2. **Review Connected Devices**
|
||||
- Regularly check which devices are on your tailnet
|
||||
- Remove devices you no longer use
|
||||
|
||||
3. **Share Carefully**
|
||||
- Only share access with trusted users
|
||||
- Use time-limited sharing when possible
|
||||
|
||||
4. **Keep Updated**
|
||||
- Tailscale auto-updates in the container
|
||||
- Archipelago notifies you of available updates
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Launch" Button Missing
|
||||
|
||||
If you don't see a Launch button:
|
||||
|
||||
1. **Check container status** - Ensure Tailscale is running in "My Apps"
|
||||
2. **Wait a moment** - Backend detects the port automatically after a few seconds
|
||||
3. **Refresh the page** - Force a UI update
|
||||
|
||||
### Can't Access Web Interface
|
||||
|
||||
If `http://<your-ip>:8240` doesn't load:
|
||||
|
||||
1. **Verify container is running**: Check "My Apps" shows Tailscale as "Running"
|
||||
2. **Check firewall**: Ensure port 8240 isn't blocked on your local network
|
||||
3. **Try localhost**: If on the same machine, try `http://localhost:8240`
|
||||
|
||||
### Remote Access Not Working
|
||||
|
||||
If you can't access via the Tailscale hostname:
|
||||
|
||||
1. **Verify authentication**: Make sure you completed the sign-in flow
|
||||
2. **Check other devices**: Ensure your other device is also signed into Tailscale
|
||||
3. **Wait for DNS**: MagicDNS can take a minute to propagate
|
||||
4. **Use IP instead**: Try accessing via the Tailscale IP (100.x.x.x)
|
||||
|
||||
### Device Not Appearing in Tailscale
|
||||
|
||||
If your Archipelago doesn't show up in your tailnet:
|
||||
|
||||
1. **Complete setup**: Make sure you clicked "Launch" and signed in
|
||||
2. **Check logs**: In "My Apps", click on Tailscale and view logs
|
||||
3. **Restart**: Try stopping and starting the Tailscale app
|
||||
4. **Reinstall**: If all else fails, remove and reinstall Tailscale
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### MagicDNS
|
||||
|
||||
Tailscale provides automatic DNS resolution:
|
||||
- Access by hostname: `http://archipelago/` (shorter URL)
|
||||
- No need to remember IPs
|
||||
- Enabled by default
|
||||
|
||||
### Subnet Routes
|
||||
|
||||
Make your home network accessible via Tailscale:
|
||||
|
||||
1. Open Tailscale web interface
|
||||
2. Go to Settings → Subnet routes
|
||||
3. Add your local subnet (e.g., `192.168.1.0/24`)
|
||||
4. Approve in Tailscale admin console
|
||||
|
||||
### Exit Node
|
||||
|
||||
Use your Archipelago as an internet gateway:
|
||||
|
||||
1. Enable exit node in Tailscale settings
|
||||
2. Connect from another device
|
||||
3. Route all internet traffic through your Archipelago
|
||||
|
||||
## More Information
|
||||
|
||||
- **Tailscale Documentation**: https://tailscale.com/kb/
|
||||
- **Tailscale Status Page**: https://status.tailscale.com/
|
||||
- **Community Support**: https://forum.tailscale.com/
|
||||
- **Archipelago Docs**: See `/docs/TAILSCALE-INTEGRATION.md` for technical details
|
||||
@@ -1,68 +0,0 @@
|
||||
# Web5 & Nostr Node Identity
|
||||
|
||||
## Overview
|
||||
|
||||
Archipelago establishes node identity using **did:key** (W3C) from the persistent Ed25519 key. This enables Web5/DID interoperability and provides an extensible foundation for Nostr discovery.
|
||||
|
||||
## DID/Web5 Integration
|
||||
|
||||
### Current Implementation
|
||||
|
||||
- **Node identity**: Persistent Ed25519 key in `/var/lib/archipelago/identity/`
|
||||
- **DID format**: `did:key:z<base58btc(multicodec_ed25519_pub + 32-byte pubkey)>`
|
||||
- **RPC**: `node.did` returns `{ did, pubkey }` for the node
|
||||
- **Onboarding**: DID generation is wired to the backend during onboarding; the node's DID is established at first boot
|
||||
|
||||
### TBD Web5 Protocols
|
||||
|
||||
The node identity is compatible with TBD Web5:
|
||||
|
||||
- **did:key** is supported by `@web5/dids` and `@tbd54566975/web5`
|
||||
- **DWN integration**: Future apps (web5-dwn, did-wallet) can resolve our DID for data exchange
|
||||
- **Node address**: `archipelago://<onion>#<pubkey>` format for peer discovery
|
||||
|
||||
### Extensibility
|
||||
|
||||
1. **DID Document**: Could add a DID document endpoint for full Web5 resolution
|
||||
2. **DWN protocols**: Define custom protocols for node-to-node sync (e.g. peer list, backup)
|
||||
3. **did:dht**: Migrate to did:dht for DHT-based resolution if needed
|
||||
|
||||
## Nostr Integration
|
||||
|
||||
### Recommended Approach
|
||||
|
||||
**NIP-33 Replaceable Events** (kind 30078) for Archipelago node discovery:
|
||||
|
||||
```
|
||||
{
|
||||
"kind": 30078,
|
||||
"pubkey": "<nostr_secp256k1_pubkey>",
|
||||
"content": JSON.stringify({
|
||||
"did": "did:key:z6Mk...",
|
||||
"node_address": "archipelago://xxx.onion#pubkey",
|
||||
"version": "0.1.0"
|
||||
}),
|
||||
"tags": [["d", "archipelago-node"]]
|
||||
}
|
||||
```
|
||||
|
||||
### Implementation Plan
|
||||
|
||||
1. **Nostr keypair**: Generate and persist secp256k1 key in `/var/lib/archipelago/identity/nostr_key` (Nostr uses secp256k1, not Ed25519)
|
||||
2. **Publish on startup**: After identity load, publish replaceable event to default relays (e.g. wss://relay.damus.io, wss://relay.nostr.info)
|
||||
3. **Discovery**: Other nodes query relays for `{"kinds": [30078], "#d": ["archipelago-node"]}` to find peers
|
||||
4. **RPC**: `node.nostr-publish` to manually re-publish; `node.nostr-pubkey` to get our Nostr pubkey for following
|
||||
|
||||
### Why Separate Keys?
|
||||
|
||||
- **Ed25519** (did:key): Web5, DWN, VC signing
|
||||
- **secp256k1** (Nostr): Nostr protocol requirement; bridges to Nostr ecosystem
|
||||
|
||||
The DID remains the canonical identity; Nostr pubkey is a discovery/signaling channel.
|
||||
|
||||
## Onboarding Flow
|
||||
|
||||
1. **Intro** → **Path** → **DID** (fetches `node.did` from backend) → **Backup** → **Verify** → **Login**
|
||||
2. Onboarding completion is persisted to backend (`auth.onboardingComplete` → `onboarding.json`)
|
||||
3. Returning users skip onboarding and go directly to login
|
||||
4. State is server-side; no reliance on browser localStorage for completion status
|
||||
@@ -1,758 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Archipelago AI Quarantine Architecture</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0d1117;
|
||||
--surface: #161b22;
|
||||
--surface-2: #1c2333;
|
||||
--border: #30363d;
|
||||
--text: #e6edf3;
|
||||
--text-muted: #8b949e;
|
||||
--accent: #fb923c;
|
||||
--green: #4ade80;
|
||||
--red: #ef4444;
|
||||
--blue: #58a6ff;
|
||||
--purple: #bc8cff;
|
||||
}
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
line-height: 1.7;
|
||||
padding: 2rem;
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
h1 {
|
||||
font-size: 2.2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
background: linear-gradient(135deg, var(--accent), #f59e0b);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
.subtitle {
|
||||
color: var(--text-muted);
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 2.5rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding-bottom: 1.5rem;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
margin: 2.5rem 0 1rem;
|
||||
color: var(--accent);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
h2 .num {
|
||||
background: var(--accent);
|
||||
color: var(--bg);
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
h3 {
|
||||
font-size: 1.15rem;
|
||||
margin: 1.5rem 0 0.5rem;
|
||||
color: var(--blue);
|
||||
}
|
||||
p { margin-bottom: 1rem; color: var(--text); }
|
||||
.card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
.card-green { border-left: 4px solid var(--green); }
|
||||
.card-red { border-left: 4px solid var(--red); }
|
||||
.card-blue { border-left: 4px solid var(--blue); }
|
||||
.card-orange { border-left: 4px solid var(--accent); }
|
||||
.card-purple { border-left: 4px solid var(--purple); }
|
||||
.card h4 {
|
||||
font-size: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
code {
|
||||
background: var(--surface-2);
|
||||
padding: 0.15rem 0.45rem;
|
||||
border-radius: 4px;
|
||||
font-family: 'SF Mono', 'Fira Code', monospace;
|
||||
font-size: 0.88rem;
|
||||
color: var(--accent);
|
||||
}
|
||||
pre {
|
||||
background: var(--surface-2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 1rem 1.25rem;
|
||||
overflow-x: auto;
|
||||
margin: 1rem 0;
|
||||
font-family: 'SF Mono', 'Fira Code', monospace;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.6;
|
||||
color: var(--text);
|
||||
}
|
||||
pre code { background: none; padding: 0; color: inherit; }
|
||||
.label {
|
||||
display: inline-block;
|
||||
padding: 0.2rem 0.6rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
.label-green { background: rgba(74, 222, 128, 0.15); color: var(--green); }
|
||||
.label-red { background: rgba(239, 68, 68, 0.15); color: var(--red); }
|
||||
.label-blue { background: rgba(88, 166, 255, 0.15); color: var(--blue); }
|
||||
.label-orange { background: rgba(251, 146, 60, 0.15); color: var(--accent); }
|
||||
ul { margin: 0.5rem 0 1rem 1.5rem; }
|
||||
li { margin-bottom: 0.4rem; }
|
||||
li code { font-size: 0.82rem; }
|
||||
.diagram {
|
||||
background: var(--surface-2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin: 1.5rem 0;
|
||||
font-family: 'SF Mono', 'Fira Code', monospace;
|
||||
font-size: 0.82rem;
|
||||
line-height: 1.8;
|
||||
white-space: pre;
|
||||
overflow-x: auto;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.diagram .highlight { color: var(--accent); font-weight: 600; }
|
||||
.diagram .green { color: var(--green); }
|
||||
.diagram .red { color: var(--red); }
|
||||
.diagram .blue { color: var(--blue); }
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 1rem 0;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
th {
|
||||
background: var(--surface-2);
|
||||
text-align: left;
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 2px solid var(--border);
|
||||
color: var(--accent);
|
||||
font-weight: 600;
|
||||
}
|
||||
td {
|
||||
padding: 0.6rem 1rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
vertical-align: top;
|
||||
}
|
||||
tr:hover td { background: rgba(251, 146, 60, 0.03); }
|
||||
.flow-arrow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin: 1rem 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.flow-box {
|
||||
background: var(--surface-2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
.flow-box.secure {
|
||||
border-color: var(--green);
|
||||
color: var(--green);
|
||||
}
|
||||
.flow-box.blocked {
|
||||
border-color: var(--red);
|
||||
color: var(--red);
|
||||
}
|
||||
.arrow { color: var(--text-muted); font-size: 1.2rem; }
|
||||
.toc {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
.toc h3 { margin-top: 0; color: var(--text); }
|
||||
.toc ol { margin-left: 1.5rem; }
|
||||
.toc li { margin-bottom: 0.3rem; }
|
||||
.toc a { color: var(--blue); text-decoration: none; }
|
||||
.toc a:hover { text-decoration: underline; }
|
||||
.files-ref {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-muted);
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.files-ref code {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 1rem;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
body { padding: 1rem; }
|
||||
h1 { font-size: 1.6rem; }
|
||||
pre { font-size: 0.78rem; padding: 0.75rem; }
|
||||
.diagram { font-size: 0.7rem; padding: 1rem; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>Archipelago AI Quarantine Architecture</h1>
|
||||
<p class="subtitle">How AIUI (Claude) is sandboxed from your node's sensitive data — a defense-in-depth approach across 6 layers</p>
|
||||
|
||||
<div class="toc">
|
||||
<h3>Contents</h3>
|
||||
<ol>
|
||||
<li><a href="#overview">Architecture Overview & Diagram</a></li>
|
||||
<li><a href="#layer1">Layer 1: Container Isolation (Podman)</a></li>
|
||||
<li><a href="#layer2">Layer 2: Iframe Sandbox (Browser)</a></li>
|
||||
<li><a href="#layer3">Layer 3: postMessage Gate (Context Broker)</a></li>
|
||||
<li><a href="#layer4">Layer 4: Per-Category Permissions (User Toggles)</a></li>
|
||||
<li><a href="#layer5">Layer 5: Data Sanitization (Field Stripping)</a></li>
|
||||
<li><a href="#layer6">Layer 6: Proxy & Nginx Authentication</a></li>
|
||||
<li><a href="#protocol">The postMessage Protocol</a></li>
|
||||
<li><a href="#context">What the AI System Prompt Sees</a></li>
|
||||
<li><a href="#never">What the AI Can NEVER See</a></li>
|
||||
<li><a href="#actions">Permitted Actions (Limited)</a></li>
|
||||
<li><a href="#bugs">Current Bugs & Issues</a></li>
|
||||
<li><a href="#files">Source File Reference</a></li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<!-- ───────────────── OVERVIEW ───────────────── -->
|
||||
<h2 id="overview"><span class="num">0</span> Architecture Overview</h2>
|
||||
|
||||
<p>The AI is treated as <strong>untrusted code in a hostile environment</strong>. It runs inside an iframe with sandbox restrictions, inside a Podman container with no outbound network. All data it receives passes through a <strong>Context Broker</strong> that checks user permissions and strips sensitive fields before anything reaches Claude's API.</p>
|
||||
|
||||
<div class="diagram"><span class="highlight">User's Browser</span>
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ <span class="blue">Archy (neode-ui)</span> — Vue.js Host Application │
|
||||
│ │
|
||||
│ ┌───────────────────────────────────────────────┐ │
|
||||
│ │ <span class="green">Context Broker</span> │ │
|
||||
│ │ - Checks aiPermissions store │ │
|
||||
│ │ - Validates postMessage origin │ │
|
||||
│ │ - Fetches data from Pinia stores / RPC │ │
|
||||
│ │ - <span class="red">Strips sensitive fields</span> (sanitize*) │ │
|
||||
│ │ - Returns only permitted, sanitized data │ │
|
||||
│ └──────────────┬────────────────────────────────┘ │
|
||||
│ │ postMessage (origin-validated) │
|
||||
│ ┌──────────────▼────────────────────────────────┐ │
|
||||
│ │ <span class="highlight">AIUI iframe</span> │ │
|
||||
│ │ sandbox="allow-scripts allow-same-origin │ │
|
||||
│ │ allow-forms" │ │
|
||||
│ │ │ │
|
||||
│ │ <span class="green">archyBridge</span> ──postMessage──▶ Context Broker │ │
|
||||
│ │ <span class="red">✗ Cannot</span> call /rpc/ directly │ │
|
||||
│ │ <span class="red">✗ Cannot</span> access host DOM │ │
|
||||
│ │ <span class="red">✗ Cannot</span> open popups │ │
|
||||
│ └───────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
│ HTTPS (session cookie required)
|
||||
▼
|
||||
┌──────────────────────────────────┐
|
||||
│ <span class="blue">Nginx</span> (/aiui/api/claude/) │ ◀── cookie check gate
|
||||
│ proxy_pass → 127.0.0.1:3141 │
|
||||
└──────────────┬───────────────────┘
|
||||
▼
|
||||
┌──────────────────────────────────┐
|
||||
│ <span class="highlight">Claude Proxy</span> (port 3141) │
|
||||
│ OAuth token from macOS keychain │
|
||||
│ → Anthropic API │
|
||||
└──────────────────────────────────┘
|
||||
|
||||
<span class="red">BLOCKED paths</span> (AI cannot reach):
|
||||
✗ /rpc/ (backend API) ✗ Container exec
|
||||
✗ /ws (WebSocket) ✗ File system
|
||||
✗ SSH ✗ Outbound network (from container)</div>
|
||||
|
||||
<!-- ───────────────── LAYER 1 ───────────────── -->
|
||||
<h2 id="layer1"><span class="num">1</span> Layer 1: Container Isolation (Podman)</h2>
|
||||
|
||||
<div class="card card-green">
|
||||
<h4>AIUI runs in a locked-down Podman container</h4>
|
||||
<p>Even if the AIUI web app were compromised, the container itself has no way to reach the rest of the system.</p>
|
||||
</div>
|
||||
|
||||
<pre><code># apps/aiui/manifest.yml
|
||||
security:
|
||||
capabilities: [] # No Linux capabilities at all
|
||||
readonly_root: true # Read-only filesystem
|
||||
no_new_privileges: true # Cannot escalate privileges
|
||||
network_policy: isolated # NO outbound network access
|
||||
|
||||
ports:
|
||||
- host: 5180
|
||||
container: 80
|
||||
bind: 127.0.0.1 # Only reachable via nginx, not externally</code></pre>
|
||||
|
||||
<p><strong>What this means:</strong></p>
|
||||
<ul>
|
||||
<li>The AIUI container <strong>cannot make HTTP requests to the internet</strong> or to other containers</li>
|
||||
<li>It serves static files only — the actual Claude API calls happen in the <em>browser</em>, not the container</li>
|
||||
<li>Even with root access in the container, you can't escalate or modify the filesystem</li>
|
||||
<li>The container port (5180) is bound to <code>127.0.0.1</code>, so only nginx (on the same machine) can reach it</li>
|
||||
</ul>
|
||||
|
||||
<!-- ───────────────── LAYER 2 ───────────────── -->
|
||||
<h2 id="layer2"><span class="num">2</span> Layer 2: Iframe Sandbox (Browser)</h2>
|
||||
|
||||
<div class="card card-blue">
|
||||
<h4>AIUI loads inside a sandboxed iframe</h4>
|
||||
<p>The browser enforces strict boundaries between the host Archy app and the AIUI iframe.</p>
|
||||
</div>
|
||||
|
||||
<pre><code><!-- neode-ui/src/views/Chat.vue -->
|
||||
<iframe
|
||||
:src="aiuiUrl"
|
||||
sandbox="allow-scripts allow-same-origin allow-forms"
|
||||
allow="microphone"
|
||||
/></code></pre>
|
||||
|
||||
<p><strong>The sandbox attribute restricts AIUI from:</strong></p>
|
||||
<ul>
|
||||
<li><strong>Navigating the parent page</strong> — cannot redirect Archy</li>
|
||||
<li><strong>Opening popups/new windows</strong> — <code>allow-popups</code> is NOT granted</li>
|
||||
<li><strong>Accessing parent DOM</strong> — cross-origin isolation is enforced</li>
|
||||
<li><strong>Submitting forms to external URLs</strong> — forms are scoped to same origin</li>
|
||||
<li><strong>Running plugins</strong> — no plugin execution</li>
|
||||
</ul>
|
||||
|
||||
<p>The only communication channel is <code>window.postMessage()</code>, which is intercepted by the Context Broker.</p>
|
||||
|
||||
<!-- ───────────────── LAYER 3 ───────────────── -->
|
||||
<h2 id="layer3"><span class="num">3</span> Layer 3: The Context Broker (postMessage Gate)</h2>
|
||||
|
||||
<div class="card card-orange">
|
||||
<h4>Every data request goes through a single gatekeeper</h4>
|
||||
<p>The <code>ContextBroker</code> class validates origin, checks permissions, fetches data, strips sensitive fields, then responds. AIUI never directly calls any backend API.</p>
|
||||
</div>
|
||||
|
||||
<h3>How it works</h3>
|
||||
|
||||
<div class="flow-arrow">
|
||||
<div class="flow-box">AIUI sends<br><code>context:request</code></div>
|
||||
<span class="arrow">→</span>
|
||||
<div class="flow-box secure">Origin validated<br><code>event.origin === allowedOrigin</code></div>
|
||||
<span class="arrow">→</span>
|
||||
<div class="flow-box secure">Permission checked<br><code>perms.isEnabled(category)</code></div>
|
||||
<span class="arrow">→</span>
|
||||
<div class="flow-box secure">Data fetched &<br>sanitized</div>
|
||||
<span class="arrow">→</span>
|
||||
<div class="flow-box">Response sent<br>to iframe</div>
|
||||
</div>
|
||||
|
||||
<pre><code>// contextBroker.ts — the critical permission check
|
||||
private async handleContextRequest(id, category, query?) {
|
||||
const perms = useAIPermissionsStore()
|
||||
|
||||
if (!perms.isEnabled(category)) {
|
||||
// DENIED — send empty response, no data
|
||||
this.postToIframe({
|
||||
type: 'context:response', id,
|
||||
data: null,
|
||||
permitted: false, // ← AIUI knows it was denied
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// ALLOWED — fetch and sanitize before sending
|
||||
const data = await this.fetchAndSanitize(category, query)
|
||||
this.postToIframe({
|
||||
type: 'context:response', id,
|
||||
data, // ← sanitized data only
|
||||
permitted: true,
|
||||
})
|
||||
}</code></pre>
|
||||
|
||||
<h3>Origin Validation (both sides)</h3>
|
||||
<ul>
|
||||
<li><strong>Context Broker</strong> (host): Rejects any message where <code>event.origin !== this.allowedOrigin</code></li>
|
||||
<li><strong>archyBridge</strong> (AIUI): Rejects any message where <code>event.origin !== allowedOrigin</code></li>
|
||||
<li><strong>Responses</strong> use explicit target origin: <code>iframe.contentWindow.postMessage(msg, this.allowedOrigin)</code></li>
|
||||
</ul>
|
||||
|
||||
<!-- ───────────────── LAYER 4 ───────────────── -->
|
||||
<h2 id="layer4"><span class="num">4</span> Layer 4: Per-Category Permission Toggles</h2>
|
||||
|
||||
<div class="card card-purple">
|
||||
<h4>All categories are OFF by default</h4>
|
||||
<p>The user must explicitly enable each data category in Settings → AI Data Access. The AI sees nothing until you flip the switch.</p>
|
||||
</div>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Category</th>
|
||||
<th>What AI Sees</th>
|
||||
<th>What's Stripped</th>
|
||||
<th>Default</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>apps</code><br><span class="label label-blue">Installed Apps</span></td>
|
||||
<td>App names, versions, running state, URLs</td>
|
||||
<td>Config files, env vars, credentials</td>
|
||||
<td><span class="label label-red">OFF</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>system</code><br><span class="label label-blue">System Stats</span></td>
|
||||
<td>CPU %, RAM used/total, disk used/total, uptime</td>
|
||||
<td>File paths, IP addresses, hostnames, PIDs</td>
|
||||
<td><span class="label label-red">OFF</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>network</code><br><span class="label label-blue">Network Status</span></td>
|
||||
<td>Connected (bool), Tor active (bool), Tailscale active (bool)</td>
|
||||
<td>IP addresses, Tor .onion addresses, peer IPs, MAC addresses</td>
|
||||
<td><span class="label label-red">OFF</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>bitcoin</code><br><span class="label label-orange">Bitcoin Node</span></td>
|
||||
<td>Block height, sync %, chain, difficulty, mempool size/count</td>
|
||||
<td>Wallet keys, addresses, transaction history, RPC credentials</td>
|
||||
<td><span class="label label-red">OFF</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>wallet</code><br><span class="label label-orange">Wallet Overview</span></td>
|
||||
<td>Alias, channel count, peer count, balance (sats), sync status</td>
|
||||
<td><strong>Private keys, seed phrases, macaroons, channel secrets, addresses</strong></td>
|
||||
<td><span class="label label-red">OFF</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>media</code><br><span class="label label-blue">Media Libraries</span></td>
|
||||
<td>Which media apps are installed (Plex, Jellyfin, etc.) + status</td>
|
||||
<td>Library contents, file paths, metadata</td>
|
||||
<td><span class="label label-red">OFF</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>files</code><br><span class="label label-blue">File Names</span></td>
|
||||
<td>Folder names, recent file names, sizes, dates from Cloud</td>
|
||||
<td>File contents (unless read-file action is used with permission)</td>
|
||||
<td><span class="label label-red">OFF</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>notes</code><br><span class="label label-blue">Documents</span></td>
|
||||
<td>Document titles (currently returns "not available")</td>
|
||||
<td>Document contents</td>
|
||||
<td><span class="label label-red">OFF</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>search</code><br><span class="label label-green">Web Search</span></td>
|
||||
<td>Whether SearXNG is installed + available</td>
|
||||
<td>N/A</td>
|
||||
<td><span class="label label-red">OFF</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>ai-local</code><br><span class="label label-green">Local AI</span></td>
|
||||
<td>Whether Ollama is installed + running</td>
|
||||
<td>Model details</td>
|
||||
<td><span class="label label-red">OFF</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p class="files-ref">Permissions stored in <code>localStorage</code> key: <code>archipelago-ai-permissions</code></p>
|
||||
<p class="files-ref">Store: <code>neode-ui/src/stores/aiPermissions.ts</code></p>
|
||||
|
||||
<!-- ───────────────── LAYER 5 ───────────────── -->
|
||||
<h2 id="layer5"><span class="num">5</span> Layer 5: Data Sanitization</h2>
|
||||
|
||||
<div class="card card-green">
|
||||
<h4>Each category has a dedicated sanitize function that extracts only whitelisted fields</h4>
|
||||
<p>The broker doesn't pass raw data through — it constructs new objects with only safe properties.</p>
|
||||
</div>
|
||||
|
||||
<h3>Example: Bitcoin sanitization</h3>
|
||||
<pre><code>// contextBroker.ts — sanitizeBitcoin()
|
||||
// ONLY these fields are extracted and sent to AI:
|
||||
return {
|
||||
available: true,
|
||||
status: 'running',
|
||||
block_height: info.block_height,
|
||||
sync_progress: info.sync_progress,
|
||||
chain: info.chain,
|
||||
difficulty: info.difficulty,
|
||||
mempool_size: info.mempool_size,
|
||||
mempool_tx_count: info.mempool_tx_count,
|
||||
verification_progress: info.verification_progress,
|
||||
}
|
||||
// NOT included: wallet data, addresses, keys, RPC auth, raw responses</code></pre>
|
||||
|
||||
<h3>Example: Wallet sanitization</h3>
|
||||
<pre><code>// contextBroker.ts — sanitizeWallet()
|
||||
// ONLY these safe summary fields:
|
||||
return {
|
||||
available: true,
|
||||
status: 'running',
|
||||
alias: info.alias,
|
||||
num_active_channels: info.num_active_channels,
|
||||
num_peers: info.num_peers,
|
||||
synced_to_chain: info.synced_to_chain,
|
||||
block_height: info.block_height,
|
||||
balance_sats: info.balance_sats,
|
||||
channel_balance_sats: info.channel_balance_sats,
|
||||
pending_open_balance: info.pending_open_balance,
|
||||
}
|
||||
// NEVER included: private keys, seed phrases, macaroons,
|
||||
// channel points, backup data, node pubkeys</code></pre>
|
||||
|
||||
<h3>Example: Network sanitization</h3>
|
||||
<pre><code>// contextBroker.ts — sanitizeNetwork()
|
||||
// Only booleans — no addresses:
|
||||
return {
|
||||
connected: store.isConnected, // true/false
|
||||
torConnected: hasTor, // true/false
|
||||
tailscaleActive: tailscale?.state === 'running', // true/false
|
||||
}
|
||||
// NEVER: IP addresses, .onion addresses, peer info, MAC addresses</code></pre>
|
||||
|
||||
<!-- ───────────────── LAYER 6 ───────────────── -->
|
||||
<h2 id="layer6"><span class="num">6</span> Layer 6: Proxy & Nginx Authentication</h2>
|
||||
|
||||
<div class="card card-blue">
|
||||
<h4>Claude API requests require a valid Archy session</h4>
|
||||
<p>Nginx rejects unauthenticated API calls. The Claude Proxy on port 3141 manages OAuth tokens securely.</p>
|
||||
</div>
|
||||
|
||||
<pre><code># nginx-archipelago.conf
|
||||
location /aiui/api/claude/ {
|
||||
if ($cookie_session = "") {
|
||||
return 401 '{"error":"Unauthorized"}'; # No session = blocked
|
||||
}
|
||||
proxy_pass http://127.0.0.1:3141/; # → Claude Proxy
|
||||
}</code></pre>
|
||||
|
||||
<p><strong>The Claude Proxy (port 3141):</strong></p>
|
||||
<ul>
|
||||
<li>OAuth token stored securely (macOS keychain → <code>.env.local</code>)</li>
|
||||
<li>Auto-refreshes tokens 5 minutes before expiry</li>
|
||||
<li>Never exposes the token to the browser — the proxy adds auth headers server-side</li>
|
||||
<li>Only the browser's fetch to <code>/aiui/api/claude/</code> goes through this proxy</li>
|
||||
</ul>
|
||||
|
||||
<p><strong>Content Security Policy (CSP):</strong></p>
|
||||
<pre><code>Content-Security-Policy: default-src 'self';
|
||||
connect-src 'self' ws: wss:;
|
||||
frame-src 'self' http://127.0.0.1:* http://localhost:*;</code></pre>
|
||||
<p>The CSP restricts the AIUI iframe to only connect to the same origin and local addresses. No external fetch calls are possible.</p>
|
||||
|
||||
<!-- ───────────────── PROTOCOL ───────────────── -->
|
||||
<h2 id="protocol"><span class="num">7</span> The postMessage Protocol</h2>
|
||||
|
||||
<p>AIUI and Archy communicate via a strictly-typed protocol defined in <code>neode-ui/src/types/aiui-protocol.ts</code>.</p>
|
||||
|
||||
<h3>AIUI → Archy (Requests)</h3>
|
||||
<table>
|
||||
<thead><tr><th>Message Type</th><th>Purpose</th><th>Fields</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><code>ready</code></td><td>Signals iframe is loaded</td><td>None</td></tr>
|
||||
<tr><td><code>context:request</code></td><td>Request node data</td><td><code>id</code>, <code>category</code>, <code>query?</code></td></tr>
|
||||
<tr><td><code>action:request</code></td><td>Request an action</td><td><code>id</code>, <code>action</code>, <code>params</code></td></tr>
|
||||
<tr><td><code>theme:request</code></td><td>Request UI theme</td><td>None</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>Archy → AIUI (Responses)</h3>
|
||||
<table>
|
||||
<thead><tr><th>Message Type</th><th>Purpose</th><th>Fields</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><code>context:response</code></td><td>Sanitized data or denial</td><td><code>id</code>, <code>data</code>, <code>permitted</code> (bool)</td></tr>
|
||||
<tr><td><code>action:response</code></td><td>Action result</td><td><code>id</code>, <code>success</code>, <code>error?</code>, <code>data?</code></td></tr>
|
||||
<tr><td><code>permissions:update</code></td><td>Push new permissions</td><td><code>categories[]</code></td></tr>
|
||||
<tr><td><code>theme:response</code></td><td>Theme colors</td><td><code>theme { accent, mode }</code></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- ───────────────── CONTEXT ───────────────── -->
|
||||
<h2 id="context"><span class="num">8</span> What the AI System Prompt Sees</h2>
|
||||
|
||||
<p>The <code>buildArchyContext()</code> function in AIUI constructs a context string that gets appended to Claude's system prompt. It only includes data for <strong>permitted categories</strong>:</p>
|
||||
|
||||
<pre><code>// Example output when apps + bitcoin + wallet are enabled:
|
||||
|
||||
**Archy Node Context** (this user is running AIUI on their Archipelago node):
|
||||
**Installed apps on this node:**
|
||||
- Bitcoin Knots (installed, running)
|
||||
- LND (installed, running)
|
||||
- Mempool (installed, running)
|
||||
- File Browser (installed, running)
|
||||
**Bitcoin:** Block 890,123, 99.99% synced, mainnet, mempool: 42,815 txs
|
||||
**Lightning (LND):** MyNode | 5 channels | 3 peers | On-chain: 150,000 sats
|
||||
|
||||
You can help the user manage their node. Available actions: open an app
|
||||
(open-app), install an app (install-app), navigate in Archy (navigate).</code></pre>
|
||||
|
||||
<div class="card card-red">
|
||||
<h4>What's NOT in the system prompt — ever</h4>
|
||||
<ul>
|
||||
<li>Private keys, seed phrases, HD derivation paths</li>
|
||||
<li>Macaroons, auth tokens, API keys</li>
|
||||
<li>IP addresses (.onion, LAN, WAN, Tailscale)</li>
|
||||
<li>File contents, log contents</li>
|
||||
<li>SSH credentials, RPC passwords</li>
|
||||
<li>Transaction history, UTXO set, address lists</li>
|
||||
<li>Container configs, environment variables</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- ───────────────── NEVER ───────────────── -->
|
||||
<h2 id="never"><span class="num">9</span> What the AI Can NEVER See</h2>
|
||||
|
||||
<div class="summary-grid">
|
||||
<div class="card card-red">
|
||||
<h4>Cryptographic Material</h4>
|
||||
<ul>
|
||||
<li>Private keys (BTC, LN)</li>
|
||||
<li>Seed phrases / BIP39 mnemonics</li>
|
||||
<li>LND macaroons</li>
|
||||
<li>Channel backup data</li>
|
||||
<li>HD derivation paths</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card card-red">
|
||||
<h4>Network Identity</h4>
|
||||
<ul>
|
||||
<li>IP addresses (LAN, WAN)</li>
|
||||
<li>Tor .onion addresses</li>
|
||||
<li>Tailscale IPs</li>
|
||||
<li>Peer connection details</li>
|
||||
<li>MAC addresses</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card card-red">
|
||||
<h4>Credentials</h4>
|
||||
<ul>
|
||||
<li>SSH passwords / keys</li>
|
||||
<li>RPC usernames/passwords</li>
|
||||
<li>API tokens</li>
|
||||
<li>Session cookies</li>
|
||||
<li>OAuth tokens</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card card-red">
|
||||
<h4>Sensitive Data</h4>
|
||||
<ul>
|
||||
<li>Transaction history</li>
|
||||
<li>Bitcoin addresses (receive/change)</li>
|
||||
<li>UTXO set</li>
|
||||
<li>File contents (unless explicitly permitted)</li>
|
||||
<li>Environment variables</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ───────────────── ACTIONS ───────────────── -->
|
||||
<h2 id="actions"><span class="num">10</span> Permitted Actions</h2>
|
||||
|
||||
<p>The AI can request a limited set of actions through the Context Broker. Each action is validated and requires the relevant permission category to be enabled.</p>
|
||||
|
||||
<table>
|
||||
<thead><tr><th>Action</th><th>What It Does</th><th>Requires Permission</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><code>open-app</code></td><td>Dispatches event to open an installed app</td><td><em>None (navigation)</em></td></tr>
|
||||
<tr><td><code>navigate</code></td><td>Navigate to a path within Archy UI</td><td><em>None (navigation)</em></td></tr>
|
||||
<tr><td><code>install-app</code></td><td>Installs an app from marketplace</td><td><em>None</em></td></tr>
|
||||
<tr><td><code>search-web</code></td><td>Searches via local SearXNG instance</td><td><code>search</code></td></tr>
|
||||
<tr><td><code>read-file</code></td><td>Reads a file from FileBrowser (Cloud)</td><td><code>files</code></td></tr>
|
||||
<tr><td><code>tail-logs</code></td><td>Gets recent log lines for an app</td><td><code>apps</code></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="card card-red">
|
||||
<h4>Actions the AI CANNOT perform</h4>
|
||||
<ul>
|
||||
<li>Execute shell commands</li>
|
||||
<li>Call backend RPC endpoints directly</li>
|
||||
<li>Modify container configs</li>
|
||||
<li>Access the filesystem outside FileBrowser</li>
|
||||
<li>Send Bitcoin transactions</li>
|
||||
<li>Open/close Lightning channels</li>
|
||||
<li>Modify system settings</li>
|
||||
<li>Access other users' data</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- ───────────────── BUGS ───────────────── -->
|
||||
<h2 id="bugs"><span class="num">11</span> Current Bugs & Issues</h2>
|
||||
|
||||
<div class="card card-red">
|
||||
<h4>"messages.6: user messages must have non-empty content" error</h4>
|
||||
<p>This Anthropic API 400 error occurs when replying in the chat. The AIUI client is sending a message array where one of the user messages has empty content (likely an empty string or the reply content isn't being properly included in the messages array). This is a bug in the AIUI chat message construction, not a quarantine issue.</p>
|
||||
</div>
|
||||
|
||||
<div class="card card-orange">
|
||||
<h4>Inconsistent node awareness</h4>
|
||||
<p>The AI sometimes says "I don't have access to your Bitcoin node" even though Bitcoin data may be permitted. This happens because:</p>
|
||||
<ul>
|
||||
<li>The <code>bitcoin.getinfo</code> RPC call may fail (e.g., Bitcoin Knots RPC not configured in the backend)</li>
|
||||
<li>When the RPC fails, the broker returns a minimal fallback: <code>{ available: true, status: 'running', network: 'mainnet' }</code></li>
|
||||
<li>The system prompt context then shows limited info, and Claude responds conservatively</li>
|
||||
<li>The <code>tail-logs</code> action could fetch Bitcoin logs, but Claude may not know to use it</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- ───────────────── FILES ───────────────── -->
|
||||
<h2 id="files"><span class="num">12</span> Source File Reference</h2>
|
||||
|
||||
<table>
|
||||
<thead><tr><th>File</th><th>Role</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><code>neode-ui/src/services/contextBroker.ts</code></td><td>The quarantine gate — validates, checks permissions, sanitizes all data</td></tr>
|
||||
<tr><td><code>neode-ui/src/types/aiui-protocol.ts</code></td><td>Strict TypeScript protocol definition for all messages</td></tr>
|
||||
<tr><td><code>neode-ui/src/stores/aiPermissions.ts</code></td><td>Pinia store for per-category permission toggles</td></tr>
|
||||
<tr><td><code>neode-ui/src/views/Chat.vue</code></td><td>Iframe host with sandbox attribute</td></tr>
|
||||
<tr><td><code>neode-ui/src/views/Settings.vue</code></td><td>AI Data Access toggles UI</td></tr>
|
||||
<tr><td><code>apps/aiui/manifest.yml</code></td><td>Container security config (isolated network, readonly root)</td></tr>
|
||||
<tr><td><code>image-recipe/configs/nginx-archipelago.conf</code></td><td>Nginx routes with session cookie auth gate</td></tr>
|
||||
<tr><td><code>AIUI/packages/app/src/services/archyBridge.ts</code></td><td>AIUI-side postMessage client (the only way AIUI talks to Archy)</td></tr>
|
||||
<tr><td><code>AIUI/packages/app/src/composables/useArchy.ts</code></td><td>Vue composable wrapping archyBridge + <code>buildArchyContext()</code></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="card card-green" style="margin-top: 2rem;">
|
||||
<h4>Summary: 6 Layers of Defense</h4>
|
||||
<ol>
|
||||
<li><strong>Container</strong> — Podman with isolated network, read-only FS, zero capabilities</li>
|
||||
<li><strong>Iframe sandbox</strong> — Browser-enforced isolation, no popups, no parent DOM access</li>
|
||||
<li><strong>Context Broker</strong> — Single postMessage gate with origin validation</li>
|
||||
<li><strong>Permissions</strong> — Per-category toggles, all OFF by default</li>
|
||||
<li><strong>Sanitization</strong> — Dedicated functions strip sensitive fields per category</li>
|
||||
<li><strong>Proxy auth</strong> — Nginx session cookie check + CSP headers</li>
|
||||
</ol>
|
||||
<p style="margin-top: 1rem; color: var(--text-muted);">The AI is treated as untrusted. It can only see what you explicitly permit, and even then, sensitive fields are stripped before the data ever reaches Claude's API.</p>
|
||||
</div>
|
||||
|
||||
<p style="text-align: center; color: var(--text-muted); margin-top: 3rem; padding-top: 1.5rem; border-top: 1px solid var(--border); font-size: 0.85rem;">
|
||||
Archipelago AI Quarantine Architecture — Generated 2026-03-06 — v1.0.0
|
||||
</p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
2104
docs/architecture-review.html
Normal file
2104
docs/architecture-review.html
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,114 +0,0 @@
|
||||
# ARM64 (aarch64) Cross-Compilation Guide
|
||||
|
||||
## Overview
|
||||
|
||||
Archipelago supports both x86_64 and ARM64 (aarch64) platforms. The backend is compiled natively on x86_64 and cross-compiled for ARM64 targets like Raspberry Pi 5.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### On the Build Server (Debian 12)
|
||||
|
||||
```bash
|
||||
# 1. Add the ARM64 Rust target
|
||||
rustup target add aarch64-unknown-linux-gnu
|
||||
|
||||
# 2. Install the cross-linker and C library
|
||||
sudo apt update
|
||||
sudo apt install -y gcc-aarch64-linux-gnu libc6-dev-arm64-cross
|
||||
|
||||
# 3. Install cross-compilation OpenSSL headers (for reqwest/hyper TLS)
|
||||
sudo apt install -y libssl-dev:arm64
|
||||
# If the above fails (no multiarch), use vendored OpenSSL instead:
|
||||
# Set OPENSSL_STATIC=1 and add openssl = { version = "0.10", features = ["vendored"] }
|
||||
```
|
||||
|
||||
### Cargo Configuration
|
||||
|
||||
The cross-compilation config is at `core/.cargo/config.toml`:
|
||||
|
||||
```toml
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "aarch64-linux-gnu-gcc"
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
### Native (x86_64)
|
||||
|
||||
```bash
|
||||
cd core
|
||||
cargo build --release -p archipelago
|
||||
# Output: core/target/release/archipelago
|
||||
```
|
||||
|
||||
### ARM64 Cross-Compilation
|
||||
|
||||
```bash
|
||||
cd core
|
||||
|
||||
# Option A: System OpenSSL (requires libssl-dev:arm64)
|
||||
PKG_CONFIG_ALLOW_CROSS=1 \
|
||||
PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig \
|
||||
cargo build --release --target aarch64-unknown-linux-gnu -p archipelago
|
||||
|
||||
# Output: core/target/aarch64-unknown-linux-gnu/release/archipelago
|
||||
|
||||
# Option B: Vendored OpenSSL (no system packages needed)
|
||||
OPENSSL_STATIC=1 \
|
||||
cargo build --release --target aarch64-unknown-linux-gnu -p archipelago
|
||||
```
|
||||
|
||||
### Verify the Binary
|
||||
|
||||
```bash
|
||||
file core/target/aarch64-unknown-linux-gnu/release/archipelago
|
||||
# Should show: ELF 64-bit LSB pie executable, ARM aarch64, ...
|
||||
```
|
||||
|
||||
## Using `cross` (Alternative)
|
||||
|
||||
The `cross` tool uses Docker containers for hermetic cross-compilation:
|
||||
|
||||
```bash
|
||||
cargo install cross
|
||||
|
||||
# Build for ARM64 (downloads a Docker image with all dependencies)
|
||||
cross build --release --target aarch64-unknown-linux-gnu -p archipelago
|
||||
```
|
||||
|
||||
This is the simplest approach and avoids installing system cross-compilation packages.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### `cannot find -lssl` / `cannot find -lcrypto`
|
||||
|
||||
OpenSSL headers for ARM64 are missing. Either:
|
||||
- Install `libssl-dev:arm64` (requires multiarch support)
|
||||
- Use vendored OpenSSL: set `OPENSSL_STATIC=1`
|
||||
- Add `openssl = { version = "0.10", features = ["vendored"] }` to Cargo.toml
|
||||
|
||||
### `cc: error: unrecognized command-line option`
|
||||
|
||||
The wrong linker is being used. Verify `aarch64-linux-gnu-gcc` is installed:
|
||||
```bash
|
||||
which aarch64-linux-gnu-gcc
|
||||
aarch64-linux-gnu-gcc --version
|
||||
```
|
||||
|
||||
### `Exec format error` when running
|
||||
|
||||
You're trying to run an ARM64 binary on x86_64. Use `qemu-aarch64-static` for testing:
|
||||
```bash
|
||||
sudo apt install qemu-user-static
|
||||
qemu-aarch64-static ./archipelago
|
||||
```
|
||||
|
||||
## Target Hardware
|
||||
|
||||
| Device | Arch | Status |
|
||||
|--------|------|--------|
|
||||
| Raspberry Pi 5 | aarch64 | Primary ARM target |
|
||||
| Raspberry Pi 4 | aarch64 | Supported |
|
||||
| Rock Pi 4 | aarch64 | Untested |
|
||||
| Orange Pi 5 | aarch64 | Untested |
|
||||
| x86_64 NUC/Mini PC | x86_64 | Primary platform |
|
||||
@@ -1,21 +0,0 @@
|
||||
# ARM64 Container Image Compatibility
|
||||
|
||||
All core Archipelago marketplace apps have multi-arch Docker images with ARM64 (linux/arm64) support.
|
||||
|
||||
## Core Apps
|
||||
|
||||
| App | Image | Tag | ARM64 | ARMv7 |
|
||||
|-----|-------|-----|-------|-------|
|
||||
| Bitcoin Knots | `bitcoinknots/bitcoin` | `latest` | Yes | Yes |
|
||||
| Electrs | `mempool/electrs` | `latest` | Yes | No |
|
||||
| BTCPay Server | `btcpayserver/btcpayserver` | `1.13.5` | Yes | Yes |
|
||||
| LND | `lightninglabs/lnd` | `v0.17.4-beta` | Yes | No |
|
||||
| Mempool | `mempool/frontend` | `v2.5.0` | Yes | Yes |
|
||||
| FileBrowser | `filebrowser/filebrowser` | `v2.27.0` | Yes | Yes |
|
||||
|
||||
## Notes
|
||||
|
||||
- All images use multi-arch manifest lists — Podman/Docker will automatically pull the correct architecture
|
||||
- No changes needed to `Marketplace.vue` image references — the same tags work on both x86_64 and ARM64
|
||||
- Three apps also support ARMv7 (32-bit ARM), but Archipelago targets ARM64 only
|
||||
- Verified 2026-03-11 via Docker Hub registry API manifest inspection
|
||||
@@ -1,78 +0,0 @@
|
||||
# ARM64 Raspberry Pi 5 Testing Guide
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Raspberry Pi 5 (4GB+ RAM recommended)
|
||||
- USB flash drive (16GB+) for the installer
|
||||
- MicroSD card or NVMe SSD for the target install
|
||||
- Monitor + keyboard (or serial console for headless setup)
|
||||
- Ethernet connection (WiFi can be configured after install)
|
||||
|
||||
## Building the ARM64 ISO
|
||||
|
||||
On the build server (192.168.1.228):
|
||||
|
||||
```bash
|
||||
cd ~/archy/image-recipe
|
||||
sudo ARCH=arm64 ./build-auto-installer-iso.sh
|
||||
# Output: results/archipelago-installer-arm64.iso
|
||||
```
|
||||
|
||||
## Flashing to USB
|
||||
|
||||
```bash
|
||||
# On macOS
|
||||
diskutil list # identify USB drive
|
||||
diskutil unmountDisk /dev/diskN
|
||||
sudo dd if=results/archipelago-installer-arm64.iso of=/dev/rdiskN bs=4m
|
||||
|
||||
# On Linux
|
||||
sudo dd if=results/archipelago-installer-arm64.iso of=/dev/sdX bs=4M status=progress
|
||||
```
|
||||
|
||||
Or use Balena Etcher for a GUI approach.
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
### Boot & Install
|
||||
- [ ] RPi 5 boots from USB drive (may need to enable USB boot in EEPROM)
|
||||
- [ ] Auto-installer detects target disk (NVMe/SD)
|
||||
- [ ] Installation completes without errors
|
||||
- [ ] System reboots into installed OS
|
||||
|
||||
### First Boot
|
||||
- [ ] Archipelago service starts (`systemctl status archipelago`)
|
||||
- [ ] Nginx starts and serves UI
|
||||
- [ ] Web UI loads in browser at `http://<pi-ip>`
|
||||
- [ ] Onboarding flow completes
|
||||
- [ ] Login works with default password
|
||||
|
||||
### Container Stack
|
||||
- [ ] Podman runs on ARM64 (`podman version`)
|
||||
- [ ] Bitcoin Knots installs and syncs
|
||||
- [ ] LND installs and connects to Bitcoin
|
||||
- [ ] Electrs installs and indexes
|
||||
- [ ] Mempool installs and shows data
|
||||
- [ ] FileBrowser installs and serves files
|
||||
|
||||
### Performance
|
||||
- [ ] Backend response time < 200ms for RPC calls
|
||||
- [ ] UI renders smoothly (no jank)
|
||||
- [ ] Container startup time reasonable (< 30s per app)
|
||||
- [ ] Memory usage stable (no leaks over 24h)
|
||||
|
||||
## Known RPi 5 Considerations
|
||||
|
||||
1. **USB Boot**: RPi 5 needs EEPROM update for USB boot. Run `sudo rpi-eeprom-update` on a stock Raspberry Pi OS first.
|
||||
2. **NVMe**: RPi 5 supports NVMe via the M.2 HAT. Recommended for performance.
|
||||
3. **Power**: Use the official 27W USB-C power supply. Underpowered supplies cause throttling.
|
||||
4. **Thermals**: Consider a heatsink or active cooling case for sustained Bitcoin node operation.
|
||||
5. **Storage**: Bitcoin blockchain requires ~600GB+. Use NVMe or external SSD.
|
||||
|
||||
## Reporting Issues
|
||||
|
||||
Document any ARM64-specific issues found during testing:
|
||||
- Architecture-specific container failures
|
||||
- Performance differences vs x86_64
|
||||
- Hardware compatibility problems
|
||||
- Missing kernel modules or firmware
|
||||
@@ -1,216 +0,0 @@
|
||||
# Building Archipelago OS Images
|
||||
|
||||
This guide explains how to build bootable Debian Linux OS images for Archipelago Bitcoin Node OS that can be flashed to x86_64 desktop computers (Dell OptiPlex, HP ProDesk 400 G4 DM, Start9 Server Pure, etc.).
|
||||
|
||||
## Overview
|
||||
|
||||
The build system creates bootable ISO images containing:
|
||||
- Debian Linux 12 (Bookworm) base system
|
||||
- Podman container runtime
|
||||
- Archipelago backend (Rust)
|
||||
- Archipelago frontend (Vue.js)
|
||||
- Systemd services
|
||||
- Network configuration via NetworkManager
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### macOS
|
||||
|
||||
- **Docker Desktop**: [Install Docker Desktop](https://www.docker.com/products/docker-desktop)
|
||||
- **xorriso**: `brew install xorriso`
|
||||
- **7zip**: `brew install p7zip`
|
||||
- **Disk Space**: At least 10GB free
|
||||
- **Memory**: 8GB+ recommended
|
||||
|
||||
### Linux
|
||||
|
||||
- **Docker** (optional, for building backend)
|
||||
- **xorriso**: `apt-get install xorriso`
|
||||
- **7zip**: `apt-get install p7zip-full`
|
||||
- **Disk Space**: At least 10GB free
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
cd image-recipe
|
||||
./build-debian-iso.sh
|
||||
```
|
||||
|
||||
This will:
|
||||
1. Download Debian Live ISO (if not cached)
|
||||
2. Extract and customize the ISO
|
||||
3. Add Archipelago components
|
||||
4. Create final bootable ISO
|
||||
|
||||
Output: `results/archipelago-debian-12-x86_64.iso`
|
||||
|
||||
## Build Process
|
||||
|
||||
### Step 1: Build Backend (Optional)
|
||||
|
||||
If you have local changes to the backend:
|
||||
|
||||
```bash
|
||||
./scripts/build-backend.sh
|
||||
```
|
||||
|
||||
This creates:
|
||||
- `build/backend/archipelago` - Compiled binary
|
||||
|
||||
### Step 2: Build Frontend (Optional)
|
||||
|
||||
If you have local changes to the frontend:
|
||||
|
||||
```bash
|
||||
./scripts/build-frontend.sh
|
||||
```
|
||||
|
||||
This creates:
|
||||
- `build/frontend/` - Static files
|
||||
|
||||
### Step 3: Build OS Image
|
||||
|
||||
```bash
|
||||
./build-debian-iso.sh
|
||||
```
|
||||
|
||||
## Flashing to USB
|
||||
|
||||
### Using dd (Recommended)
|
||||
|
||||
```bash
|
||||
# macOS
|
||||
./write-usb-dd.sh /dev/diskN
|
||||
|
||||
# Or manually:
|
||||
sudo dd if=results/archipelago-debian-12-x86_64.iso of=/dev/rdiskX bs=4m status=progress
|
||||
```
|
||||
|
||||
```bash
|
||||
# Linux
|
||||
sudo dd if=results/archipelago-debian-12-x86_64.iso of=/dev/sdX bs=4M status=progress
|
||||
```
|
||||
|
||||
### Using Balena Etcher
|
||||
|
||||
1. Download [Balena Etcher](https://www.balena.io/etcher/)
|
||||
2. Select the ISO file
|
||||
3. Select target USB drive
|
||||
4. Click Flash
|
||||
|
||||
⚠️ **Warning**: Double-check the device path! Flashing to wrong device will destroy data.
|
||||
|
||||
## Installation Methods
|
||||
|
||||
### 1. Live USB Boot
|
||||
|
||||
Boot from the USB to run Archipelago in live mode:
|
||||
- Test the system without installing
|
||||
- Changes don't persist after reboot
|
||||
|
||||
### 2. Full Disk Installation
|
||||
|
||||
From the live environment:
|
||||
|
||||
```bash
|
||||
sudo /archipelago/install-to-disk.sh
|
||||
```
|
||||
|
||||
This will:
|
||||
1. Partition the target disk (GPT with EFI)
|
||||
2. Install Debian via debootstrap
|
||||
3. Install Archipelago components
|
||||
4. Configure bootloader (GRUB)
|
||||
|
||||
## Default Credentials
|
||||
|
||||
### Live Mode
|
||||
- Username: `user`
|
||||
- Password: `live`
|
||||
|
||||
### After Installation
|
||||
- Username: `archipelago`
|
||||
- Password: `archipelago`
|
||||
|
||||
⚠️ **Change passwords immediately after installation!**
|
||||
|
||||
## Customization
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```bash
|
||||
# Debian version
|
||||
DEBIAN_VERSION=bookworm
|
||||
|
||||
# Architecture
|
||||
ARCH=amd64
|
||||
|
||||
# Output directory
|
||||
OUTPUT_DIR=./results
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Docker Issues (macOS)
|
||||
|
||||
**Problem**: Docker daemon not running
|
||||
```bash
|
||||
# Start Docker Desktop application
|
||||
open -a Docker
|
||||
```
|
||||
|
||||
**Problem**: Out of disk space
|
||||
```bash
|
||||
# Clean Docker
|
||||
docker system prune -a
|
||||
```
|
||||
|
||||
### Build Failures
|
||||
|
||||
**Problem**: Backend build fails
|
||||
```bash
|
||||
# Check Rust installation
|
||||
rustc --version
|
||||
|
||||
# Install Rust if needed
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
```
|
||||
|
||||
**Problem**: Frontend build fails
|
||||
```bash
|
||||
# Check Node.js
|
||||
node --version # Need 18+
|
||||
|
||||
# Install dependencies
|
||||
cd neode-ui
|
||||
npm install
|
||||
```
|
||||
|
||||
### Image Boot Issues
|
||||
|
||||
**Problem**: Image doesn't boot
|
||||
- Verify ISO integrity
|
||||
- Check BIOS/UEFI settings
|
||||
- Ensure correct architecture (x86_64)
|
||||
- Try different boot mode (UEFI vs Legacy)
|
||||
|
||||
**Problem**: Services don't start
|
||||
- Check logs: `journalctl -u archipelago`
|
||||
- Verify network: `ip addr`
|
||||
- Check Podman: `podman info`
|
||||
|
||||
## Next Steps
|
||||
|
||||
After building and flashing:
|
||||
|
||||
1. **Boot the device**
|
||||
2. **Access web UI**: http://device-ip:8100
|
||||
3. **Configure network** (if needed)
|
||||
4. **Install apps** via UI
|
||||
5. **Set up Bitcoin node** (if desired)
|
||||
|
||||
## Resources
|
||||
|
||||
- [Debian Live Manual](https://live-team.pages.debian.net/live-manual/)
|
||||
- [Archipelago Architecture](./architecture.md)
|
||||
- [Development Setup](./development-setup.md)
|
||||
@@ -1,69 +0,0 @@
|
||||
# Canary Deploy Process
|
||||
|
||||
## Overview
|
||||
|
||||
Deploy changes to the secondary server first (192.168.1.198), verify health, then deploy to the primary server (192.168.1.228). This reduces risk by catching issues before they affect the main system.
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Deploy to Secondary (Canary)
|
||||
|
||||
```bash
|
||||
# Deploy to secondary server only
|
||||
TARGET_HOST=archipelago@192.168.1.198 ./scripts/deploy-to-target.sh --live
|
||||
```
|
||||
|
||||
### 2. Verify Health
|
||||
|
||||
```bash
|
||||
# Check health endpoint
|
||||
curl -s http://192.168.1.198/health
|
||||
|
||||
# Check backend service
|
||||
ssh -i ~/.ssh/archipelago-deploy archipelago@192.168.1.198 "sudo systemctl status archipelago"
|
||||
|
||||
# Spot-check the UI
|
||||
# Open http://192.168.1.198 in browser, verify pages load
|
||||
```
|
||||
|
||||
### 3. Deploy to Primary
|
||||
|
||||
Once the secondary is healthy and verified:
|
||||
|
||||
```bash
|
||||
./scripts/deploy-to-target.sh --live
|
||||
```
|
||||
|
||||
### 4. Verify Primary
|
||||
|
||||
```bash
|
||||
curl -s http://192.168.1.228/health
|
||||
```
|
||||
|
||||
## Quick Deploy to Both (Non-Canary)
|
||||
|
||||
If you're confident and want to deploy to both at once:
|
||||
|
||||
```bash
|
||||
./scripts/deploy-to-target.sh --both
|
||||
```
|
||||
|
||||
This deploys to 228 first, then copies the built artifacts to 198. Not a true canary — use the step-by-step process above for safer rollouts.
|
||||
|
||||
## Rollback
|
||||
|
||||
If the canary (198) shows issues, do NOT deploy to primary. Fix the issue first.
|
||||
|
||||
If primary (228) shows issues after deploy:
|
||||
|
||||
```bash
|
||||
# Check logs
|
||||
ssh -i ~/.ssh/archipelago-deploy archipelago@192.168.1.228 "sudo journalctl -u archipelago -n 50"
|
||||
|
||||
# Restart services
|
||||
ssh -i ~/.ssh/archipelago-deploy archipelago@192.168.1.228 "sudo systemctl restart archipelago && sudo systemctl restart nginx"
|
||||
```
|
||||
|
||||
## Post-Deploy Health Check
|
||||
|
||||
The deploy script automatically waits up to 60 seconds for the health endpoint to return 200 after deploying. If it fails, check the backend logs for errors.
|
||||
@@ -1,50 +0,0 @@
|
||||
# Community App Review Checklist
|
||||
|
||||
Use this checklist when reviewing community-submitted app manifests for the Archipelago marketplace.
|
||||
|
||||
## Security Requirements (Non-Negotiable)
|
||||
|
||||
- [ ] `readonly_root: true` (or documented justification for `false`)
|
||||
- [ ] `capabilities: []` — drop ALL, add only required with justification
|
||||
- [ ] `no_new_privileges: true`
|
||||
- [ ] `user: 1000` (or UID > 1000, never root)
|
||||
- [ ] `seccomp_profile: default`
|
||||
- [ ] `apparmor_profile` specified
|
||||
- [ ] Image tag pinned to specific version (no `:latest`)
|
||||
- [ ] `image_signature` field present (Cosign verification)
|
||||
- [ ] No secrets or credentials in environment variables (use secrets manager)
|
||||
- [ ] Volumes use `/var/lib/archipelago/{app-id}/` paths only
|
||||
|
||||
## Manifest Completeness
|
||||
|
||||
- [ ] `app.id` follows kebab-case naming
|
||||
- [ ] `app.name` is human-readable
|
||||
- [ ] `app.version` follows SemVer
|
||||
- [ ] `app.description` is accurate and concise
|
||||
- [ ] `resources` section has cpu_limit, memory_limit, disk_limit
|
||||
- [ ] `health_check` configured with reasonable interval/timeout
|
||||
- [ ] `ports` use non-privileged ports (>1024) where possible
|
||||
- [ ] `dependencies` listed (storage, other apps)
|
||||
|
||||
## Functional Testing
|
||||
|
||||
- [ ] Container starts successfully on dev server
|
||||
- [ ] Health check passes within 60 seconds
|
||||
- [ ] Web UI loads via nginx proxy at `/app/{id}/`
|
||||
- [ ] App functions correctly (basic smoke test)
|
||||
- [ ] Container stops cleanly (no orphan processes)
|
||||
- [ ] Data persists across container restart
|
||||
- [ ] Resource usage stays within declared limits
|
||||
|
||||
## Integration
|
||||
|
||||
- [ ] No port conflicts with existing apps
|
||||
- [ ] Network policy appropriate (isolated vs archy-net)
|
||||
- [ ] Dependencies start before this app
|
||||
- [ ] App icon at `neode-ui/public/assets/img/app-icons/{id}.png`
|
||||
|
||||
## Review Outcome
|
||||
|
||||
- **Approved**: Meets all requirements, tested on dev server
|
||||
- **Needs Changes**: List specific issues to fix
|
||||
- **Rejected**: Fundamental security or compatibility issues
|
||||
@@ -1,32 +0,0 @@
|
||||
# Community Growth Plan: Path to 10,000 Nodes
|
||||
|
||||
## Current State
|
||||
- 2 active nodes (dev/test)
|
||||
- Opt-in analytics backend implemented (Y4-03)
|
||||
- ISO installer builds automatically
|
||||
- App marketplace with 35+ apps
|
||||
|
||||
## Growth Phases
|
||||
|
||||
### Phase 1: Developer Preview (0-100 nodes)
|
||||
- Release ISO on GitHub
|
||||
- Bitcoin/sovereignty community outreach
|
||||
- Documentation and video tutorials
|
||||
- Bug bounty program
|
||||
|
||||
### Phase 2: Early Adopters (100-1,000 nodes)
|
||||
- Pre-built hardware kits (RPi5, NUC)
|
||||
- Community forum (Discourse or Matrix)
|
||||
- Ambassador program
|
||||
- Conference presentations (Bitcoin, Nostr)
|
||||
|
||||
### Phase 3: Growth (1,000-10,000 nodes)
|
||||
- Partnership with hardware vendors
|
||||
- App developer ecosystem
|
||||
- Multi-language support (5 languages ready)
|
||||
- Paid support tier for businesses
|
||||
|
||||
## Tracking
|
||||
- Opt-in telemetry via analytics.get-snapshot RPC
|
||||
- Nostr relay-based node discovery (privacy-preserving)
|
||||
- GitHub stars/downloads as proxy metrics
|
||||
@@ -1,42 +0,0 @@
|
||||
# Dependency Audit Log
|
||||
|
||||
Tracks monthly dependency updates per MAINT-01.
|
||||
|
||||
---
|
||||
|
||||
## 2026-03-11 — Initial Audit
|
||||
|
||||
### npm (neode-ui)
|
||||
|
||||
**Updated packages** (semver-compatible):
|
||||
- `@types/node`: 24.10.9 → 24.12.0
|
||||
- `@vitejs/plugin-vue`: 6.0.3 → 6.0.4
|
||||
- `autoprefixer`: 10.4.23 → 10.4.27
|
||||
- `postcss`: 8.5.6 → 8.5.8
|
||||
- `vue`: 3.5.27 → 3.5.30
|
||||
- `vue-tsc`: 3.2.3 → 3.2.5
|
||||
- Net result: added 35 packages, removed 53, changed 63 (overall reduction)
|
||||
|
||||
**Audit results after update**: 4 high-severity vulnerabilities remaining
|
||||
- All in `serialize-javascript` ≤7.0.2 (RCE via RegExp.flags)
|
||||
- Dependency chain: `serialize-javascript` → `@rollup/plugin-terser` → `workbox-build` → `vite-plugin-pwa`
|
||||
- **Risk**: Low — dev-only dependency, not shipped to users, not exploitable at build time
|
||||
- **Action**: Monitor for `vite-plugin-pwa` update that pulls `serialize-javascript` ≥7.0.3
|
||||
|
||||
**Major versions available (not upgraded — breaking changes)**:
|
||||
- `@types/node`: 25.x (Node 22+ types — we target Node 20)
|
||||
- `@vitest/coverage-v8`: 4.x (needs vitest 4.x)
|
||||
- `express`: 5.x (dev mock server only)
|
||||
- `jsdom`: 28.x (test env only)
|
||||
- `tailwindcss`: 4.x (major migration — defer to v1.1)
|
||||
- `vitest`: 4.x (defer — 3.x working well)
|
||||
- `vue-router`: 5.x (major migration — defer to v1.1)
|
||||
|
||||
### Cargo (core/)
|
||||
|
||||
**Status**: Deferred — `cargo update` must run on Linux dev server (not macOS). Will be run during next deploy cycle.
|
||||
|
||||
### Test results
|
||||
- Type-check: 0 errors
|
||||
- Build: success (2.67s)
|
||||
- Tests: 515/515 pass (6.83s)
|
||||
@@ -1,384 +0,0 @@
|
||||
# Development Container Environment Guide
|
||||
|
||||
This guide explains how to develop and test containers in the Archipelago development environment.
|
||||
|
||||
## Overview
|
||||
|
||||
The development server environment enables:
|
||||
- Testing prepackaged containers (k484 mortar, atob nostrdevs)
|
||||
- Installing and running containers with port offsetting for dev
|
||||
- Simulating Bitcoin Core installation and availability
|
||||
- Supporting both Podman (preferred) and Docker (fallback)
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Dev Server Environment │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ Backend │ │ Container │ │ Port │ │
|
||||
│ │ (Rust) │ │ Runtime │ │ Manager │ │
|
||||
│ │ │ │ (Podman/ │ │ (Offset) │ │
|
||||
│ │ │ │ Docker) │ │ │ │
|
||||
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
|
||||
│ │ │ │ │
|
||||
│ └─────────────────┼─────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌────────▼────────┐ │
|
||||
│ │ Dev Container │ │
|
||||
│ │ Orchestrator │ │
|
||||
│ │ - Port offset │ │
|
||||
│ │ - Bitcoin mock │ │
|
||||
│ │ - Volume dev │ │
|
||||
│ └─────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **Container Runtime**: Podman (preferred) or Docker
|
||||
- Podman: https://podman.io/getting-started/installation
|
||||
- Docker: https://docs.docker.com/get-docker/
|
||||
|
||||
2. **Rust**: Latest stable version
|
||||
- Install from: https://rustup.rs/
|
||||
|
||||
3. **Node.js**: v18+ and npm
|
||||
- Install from: https://nodejs.org/
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Check Container Runtime
|
||||
|
||||
```bash
|
||||
./scripts/dev-container.sh
|
||||
```
|
||||
|
||||
This script will:
|
||||
- Check for Podman and Docker availability
|
||||
- Show which runtime will be used
|
||||
- Provide helper commands
|
||||
|
||||
### 2. Start Development Server
|
||||
|
||||
```bash
|
||||
./scripts/dev-start.sh
|
||||
# Choose option 5: Full stack with container support
|
||||
```
|
||||
|
||||
Or manually:
|
||||
|
||||
```bash
|
||||
# Terminal 1: Backend
|
||||
cd core
|
||||
ARCHIPELAGO_DEV_MODE=true \
|
||||
ARCHIPELAGO_CONTAINER_RUNTIME=auto \
|
||||
ARCHIPELAGO_PORT_OFFSET=10000 \
|
||||
ARCHIPELAGO_BITCOIN_SIMULATION=mock \
|
||||
cargo run --bin archipelago
|
||||
|
||||
# Terminal 2: Frontend
|
||||
cd neode-ui
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### 3. Install a Container
|
||||
|
||||
Via UI:
|
||||
1. Open http://localhost:8100
|
||||
2. Navigate to Marketplace or Apps
|
||||
3. Install a container app
|
||||
|
||||
Via RPC:
|
||||
```bash
|
||||
curl -X POST http://localhost:5959/rpc/v1 \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"method": "container-install",
|
||||
"params": {
|
||||
"manifest_path": "apps/bitcoin-core/manifest.yml"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
## Port Offset Strategy
|
||||
|
||||
In development mode, ports are offset by 10000 to prevent conflicts with production services:
|
||||
|
||||
| Production Port | Dev Port | Example |
|
||||
|----------------|----------|---------|
|
||||
| 8332 | 18332 | Bitcoin Core RPC |
|
||||
| 8333 | 18333 | Bitcoin Core P2P |
|
||||
| 9735 | 19735 | Lightning Network |
|
||||
| 8080 | 18080 | Web Services |
|
||||
|
||||
This is configurable via `ARCHIPELAGO_PORT_OFFSET` environment variable.
|
||||
|
||||
## Container Runtime
|
||||
|
||||
The system supports three runtime modes:
|
||||
|
||||
1. **Podman** (preferred): Matches production environment
|
||||
2. **Docker**: Easier local development
|
||||
3. **Auto**: Tries Podman first, falls back to Docker
|
||||
|
||||
Set via `ARCHIPELAGO_CONTAINER_RUNTIME` environment variable.
|
||||
|
||||
## Bitcoin Simulation
|
||||
|
||||
Bitcoin Core dependency can be simulated in three ways:
|
||||
|
||||
1. **Mock** (default): Fast, no actual node required
|
||||
- Mocks Bitcoin RPC responses
|
||||
- Satisfies dependencies without installation
|
||||
- Use for UI and integration testing
|
||||
|
||||
2. **Testnet**: Runs real Bitcoin Core on testnet
|
||||
- Slower but more realistic
|
||||
- Requires Bitcoin Core container
|
||||
- Use for testing Bitcoin integration
|
||||
|
||||
3. **Mainnet**: Runs real Bitcoin Core on mainnet
|
||||
- Slowest, most realistic
|
||||
- Requires Bitcoin Core container and full sync
|
||||
- Use for final testing
|
||||
|
||||
4. **None**: No Bitcoin simulation
|
||||
- Apps requiring Bitcoin will fail dependency check
|
||||
- Use when testing non-Bitcoin apps
|
||||
|
||||
Set via `ARCHIPELAGO_BITCOIN_SIMULATION` environment variable.
|
||||
|
||||
## Testing Prepackaged Containers
|
||||
|
||||
### Test a Single Container
|
||||
|
||||
```bash
|
||||
./scripts/test-container.sh <app-id> <package-dir>
|
||||
```
|
||||
|
||||
Example:
|
||||
```bash
|
||||
./scripts/test-container.sh k484 ~/k484-package
|
||||
```
|
||||
|
||||
This script will:
|
||||
1. Build the container image
|
||||
2. Create a test manifest
|
||||
3. Install via RPC
|
||||
4. Start the container
|
||||
5. Show status and logs
|
||||
6. Provide cleanup commands
|
||||
|
||||
### Test Multiple Containers
|
||||
|
||||
```bash
|
||||
./scripts/prepackage-test.sh
|
||||
```
|
||||
|
||||
This script tests k484 and atob containers if their package directories are found.
|
||||
|
||||
## Development Data Directories
|
||||
|
||||
Container data is stored in isolated directories:
|
||||
|
||||
- **Location**: `/tmp/archipelago-dev/{app-id}/`
|
||||
- **Purpose**: Separate from production data, easy cleanup
|
||||
- **Persistence**: Data is preserved between container restarts (optional)
|
||||
|
||||
Configure via `ARCHIPELAGO_DEV_DATA_DIR` environment variable.
|
||||
|
||||
## RPC Endpoints
|
||||
|
||||
All container operations are available via RPC:
|
||||
|
||||
### Install Container
|
||||
```json
|
||||
{
|
||||
"method": "container-install",
|
||||
"params": {
|
||||
"manifest_path": "apps/bitcoin-core/manifest.yml"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Start Container
|
||||
```json
|
||||
{
|
||||
"method": "container-start",
|
||||
"params": {
|
||||
"app_id": "bitcoin-core"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Stop Container
|
||||
```json
|
||||
{
|
||||
"method": "container-stop",
|
||||
"params": {
|
||||
"app_id": "bitcoin-core"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### List Containers
|
||||
```json
|
||||
{
|
||||
"method": "container-list",
|
||||
"params": {}
|
||||
}
|
||||
```
|
||||
|
||||
### Get Container Status
|
||||
```json
|
||||
{
|
||||
"method": "container-status",
|
||||
"params": {
|
||||
"app_id": "bitcoin-core"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Get Container Logs
|
||||
```json
|
||||
{
|
||||
"method": "container-logs",
|
||||
"params": {
|
||||
"app_id": "bitcoin-core",
|
||||
"lines": 100
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Remove Container
|
||||
```json
|
||||
{
|
||||
"method": "container-remove",
|
||||
"params": {
|
||||
"app_id": "bitcoin-core",
|
||||
"preserve_data": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Get Health Status
|
||||
```json
|
||||
{
|
||||
"method": "container-health",
|
||||
"params": {}
|
||||
}
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
```bash
|
||||
# Enable dev mode
|
||||
ARCHIPELAGO_DEV_MODE=true
|
||||
|
||||
# Container runtime (podman|docker|auto)
|
||||
ARCHIPELAGO_CONTAINER_RUNTIME=auto
|
||||
|
||||
# Port offset (default: 10000)
|
||||
ARCHIPELAGO_PORT_OFFSET=10000
|
||||
|
||||
# Bitcoin simulation (mock|testnet|mainnet|none)
|
||||
ARCHIPELAGO_BITCOIN_SIMULATION=mock
|
||||
|
||||
# Dev data directory (default: /tmp/archipelago-dev)
|
||||
ARCHIPELAGO_DEV_DATA_DIR=/tmp/archipelago-dev
|
||||
|
||||
# Backend bind address (default: 127.0.0.1:5959)
|
||||
ARCHIPELAGO_BIND=127.0.0.1:5959
|
||||
|
||||
# Log level (default: info)
|
||||
ARCHIPELAGO_LOG_LEVEL=debug
|
||||
```
|
||||
|
||||
## Helper Commands
|
||||
|
||||
### List All Containers
|
||||
```bash
|
||||
podman ps -a
|
||||
# or
|
||||
docker ps -a
|
||||
```
|
||||
|
||||
### View Container Logs
|
||||
```bash
|
||||
podman logs <container-name>
|
||||
# or
|
||||
docker logs <container-name>
|
||||
```
|
||||
|
||||
### Stop All Archipelago Containers
|
||||
```bash
|
||||
podman ps -a --filter 'name=archipelago-' --format '{{.Names}}' | xargs -r podman stop
|
||||
# or
|
||||
docker ps -a --filter 'name=archipelago-' --format '{{.Names}}' | xargs -r docker stop
|
||||
```
|
||||
|
||||
### Remove All Archipelago Containers
|
||||
```bash
|
||||
podman ps -a --filter 'name=archipelago-' --format '{{.Names}}' | xargs -r podman rm -f
|
||||
# or
|
||||
docker ps -a --filter 'name=archipelago-' --format '{{.Names}}' | xargs -r docker rm -f
|
||||
```
|
||||
|
||||
### Clean Up Dev Data
|
||||
```bash
|
||||
rm -rf /tmp/archipelago-dev
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Container Runtime Not Available
|
||||
|
||||
**Problem**: `No container runtime available`
|
||||
|
||||
**Solution**:
|
||||
1. Install Podman or Docker
|
||||
2. Start the daemon:
|
||||
- Podman (macOS): `podman machine start`
|
||||
- Docker: Start Docker Desktop or `sudo systemctl start docker`
|
||||
|
||||
### Port Already in Use
|
||||
|
||||
**Problem**: Port conflict when starting container
|
||||
|
||||
**Solution**:
|
||||
1. Change port offset: `ARCHIPELAGO_PORT_OFFSET=20000`
|
||||
2. Or stop conflicting service
|
||||
|
||||
### Bitcoin Dependency Not Satisfied
|
||||
|
||||
**Problem**: App requires Bitcoin Core but simulation is disabled
|
||||
|
||||
**Solution**:
|
||||
1. Enable Bitcoin simulation: `ARCHIPELAGO_BITCOIN_SIMULATION=mock`
|
||||
2. Or install Bitcoin Core container first
|
||||
|
||||
### Container Fails to Start
|
||||
|
||||
**Problem**: Container exits immediately
|
||||
|
||||
**Solution**:
|
||||
1. Check logs: `container-logs` RPC call
|
||||
2. Verify image exists: `podman images` or `docker images`
|
||||
3. Check manifest configuration
|
||||
4. Verify port mappings don't conflict
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use Mock Bitcoin by Default**: Fast iteration, no sync required
|
||||
2. **Test with Real Bitcoin When Needed**: Use testnet for integration testing
|
||||
3. **Clean Up Regularly**: Remove unused containers and data
|
||||
4. **Check Logs First**: Container logs provide detailed error information
|
||||
5. **Use Port Offset**: Prevents conflicts with production services
|
||||
6. **Isolate Dev Data**: Keep dev and production data separate
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Read [App Manifest Specification](./app-manifest-spec.md)
|
||||
- Review [Architecture Documentation](./architecture.md)
|
||||
- Check [Development Setup Guide](./development-setup.md)
|
||||
@@ -1,231 +0,0 @@
|
||||
# Development Setup Guide
|
||||
|
||||
This guide explains how to run Archipelago locally for development.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Rust** (latest stable) - [Install Rust](https://rustup.rs/)
|
||||
- **Node.js** (v18+) and **npm** - [Install Node.js](https://nodejs.org/)
|
||||
- **Podman** (for container features) - [Install Podman](https://podman.io/getting-started/installation)
|
||||
- **PostgreSQL** (for backend database) - [Install PostgreSQL](https://www.postgresql.org/download/)
|
||||
|
||||
## Project Structure
|
||||
|
||||
The project has two main components:
|
||||
|
||||
1. **Backend** (`core/startos/`) - Rust backend with RPC API
|
||||
2. **Frontend** (`neode-ui/`) - Vue.js 3 frontend
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Option 1: Mock Backend (Fastest for UI Development)
|
||||
|
||||
For frontend-only development, use the mock backend:
|
||||
|
||||
```bash
|
||||
cd neode-ui
|
||||
npm install
|
||||
npm run dev:mock
|
||||
```
|
||||
|
||||
This starts:
|
||||
- Mock backend server on port 3000
|
||||
- Vite dev server on port 8100
|
||||
- Open http://localhost:8100
|
||||
|
||||
### Option 2: Full Stack Development
|
||||
|
||||
For full-stack development with the real backend:
|
||||
|
||||
#### Terminal 1: Backend
|
||||
|
||||
```bash
|
||||
cd core
|
||||
cargo run --bin startbox --features cli,daemon
|
||||
```
|
||||
|
||||
The backend will:
|
||||
- Start RPC server on port 5959
|
||||
- Initialize database if needed
|
||||
- Serve API endpoints
|
||||
|
||||
#### Terminal 2: Frontend
|
||||
|
||||
```bash
|
||||
cd neode-ui
|
||||
npm install
|
||||
npm run dev:real
|
||||
```
|
||||
|
||||
Or just:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
The frontend will:
|
||||
- Start Vite dev server on port 8100
|
||||
- Proxy API requests to backend on port 5959
|
||||
- Open http://localhost:8100
|
||||
|
||||
## Development Scripts
|
||||
|
||||
### Frontend Scripts (`neode-ui/package.json`)
|
||||
|
||||
- `npm run dev` - Start Vite dev server
|
||||
- `npm run dev:mock` - Start with mock backend
|
||||
- `npm run dev:real` - Start with real backend (backend must be running separately)
|
||||
- `npm run build` - Build for production
|
||||
- `npm run type-check` - TypeScript type checking
|
||||
|
||||
### Backend Scripts
|
||||
|
||||
- `cargo run --bin startbox` - Run backend in dev mode
|
||||
- `cargo run --bin startbox --release` - Run backend in release mode
|
||||
- `cargo test` - Run tests
|
||||
- `cargo build` - Build backend
|
||||
|
||||
## Environment Variables
|
||||
|
||||
### Backend
|
||||
|
||||
Create `.env` in `core/` directory:
|
||||
|
||||
```bash
|
||||
DATADIR=/tmp/archipelago-dev
|
||||
RPC_BIND=127.0.0.1:5959
|
||||
LOG_LEVEL=debug
|
||||
```
|
||||
|
||||
### Frontend
|
||||
|
||||
Create `.env` in `neode-ui/` directory:
|
||||
|
||||
```bash
|
||||
VITE_BACKEND_URL=http://localhost:5959
|
||||
VITE_API_BASE=/rpc/v1
|
||||
```
|
||||
|
||||
## Database Setup
|
||||
|
||||
The backend uses PostgreSQL. For development:
|
||||
|
||||
```bash
|
||||
# Create database
|
||||
createdb archipelago_dev
|
||||
|
||||
# Or use Docker
|
||||
docker run -d \
|
||||
--name archipelago-postgres \
|
||||
-e POSTGRES_PASSWORD=dev \
|
||||
-e POSTGRES_DB=archipelago_dev \
|
||||
-p 5432:5432 \
|
||||
postgres:15
|
||||
```
|
||||
|
||||
## Container Development
|
||||
|
||||
To test container features locally, you need Podman:
|
||||
|
||||
```bash
|
||||
# Install Podman (macOS)
|
||||
brew install podman
|
||||
|
||||
# Initialize Podman machine
|
||||
podman machine init
|
||||
podman machine start
|
||||
|
||||
# Verify
|
||||
podman --version
|
||||
```
|
||||
|
||||
## Hot Reload
|
||||
|
||||
- **Frontend**: Vite provides instant hot module replacement (HMR)
|
||||
- **Backend**: Use `cargo watch` for auto-reload:
|
||||
|
||||
```bash
|
||||
cargo install cargo-watch
|
||||
cargo watch -x 'run --bin startbox'
|
||||
```
|
||||
|
||||
## Debugging
|
||||
|
||||
### Frontend
|
||||
|
||||
- Use browser DevTools
|
||||
- Vue DevTools extension recommended
|
||||
- Console logs available
|
||||
|
||||
### Backend
|
||||
|
||||
- Use `RUST_LOG=debug` environment variable
|
||||
- Add `println!` or use `tracing` macros
|
||||
- Use a debugger like `lldb` or `gdb`
|
||||
|
||||
## Common Issues
|
||||
|
||||
### Port Already in Use
|
||||
|
||||
If port 5959 or 8100 is already in use:
|
||||
|
||||
```bash
|
||||
# Backend - change port in .env
|
||||
RPC_BIND=127.0.0.1:5958
|
||||
|
||||
# Frontend - change in vite.config.ts
|
||||
server: { port: 8101 }
|
||||
```
|
||||
|
||||
### Database Connection Errors
|
||||
|
||||
- Ensure PostgreSQL is running
|
||||
- Check connection string in backend config
|
||||
- Verify database exists
|
||||
|
||||
### Container Features Not Working
|
||||
|
||||
- Ensure Podman is installed and running
|
||||
- Check Podman machine is started (macOS)
|
||||
- Verify rootless Podman is configured
|
||||
|
||||
## Testing
|
||||
|
||||
### Frontend Tests
|
||||
|
||||
```bash
|
||||
cd neode-ui
|
||||
npm test
|
||||
```
|
||||
|
||||
### Backend Tests
|
||||
|
||||
```bash
|
||||
cd core
|
||||
cargo test
|
||||
```
|
||||
|
||||
## Building for Production
|
||||
|
||||
### Frontend
|
||||
|
||||
```bash
|
||||
cd neode-ui
|
||||
npm run build
|
||||
```
|
||||
|
||||
Output: `dist/` directory
|
||||
|
||||
### Backend
|
||||
|
||||
```bash
|
||||
cd core
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
Output: `target/release/startbox`
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Read [Architecture Documentation](./architecture.md)
|
||||
- Check [App Manifest Specification](./app-manifest-spec.md)
|
||||
- Review [Coding Standards](../CODING_STANDARDS.md)
|
||||
@@ -1,160 +0,0 @@
|
||||
# did:dht Integration Architecture
|
||||
|
||||
## Overview
|
||||
|
||||
Archipelago currently uses `did:key` for node identities. This document describes integrating `did:dht` as a **complementary** DID method that makes node identities discoverable via the BitTorrent Mainline DHT, without relying on centralized registries, Nostr relays, or Tor hidden services.
|
||||
|
||||
**Goal**: Each Archipelago node has two DID types:
|
||||
- `did:key` — Offline, self-certifying, works without network (primary identity)
|
||||
- `did:dht` — Published to Mainline DHT for decentralized discovery (optional, discoverable)
|
||||
|
||||
## What is did:dht?
|
||||
|
||||
The `did:dht` method stores DID Documents in the [BitTorrent Mainline DHT](https://www.bittorrent.org/beps/bep_0044.html) using BEP-44 (mutable items). Key properties:
|
||||
|
||||
- **No server needed**: Uses the public DHT network (~15M+ nodes)
|
||||
- **Ed25519 keypair**: Same key type Archipelago already uses
|
||||
- **DNS-encoded**: DID Document stored as a DNS packet (compact, standardized)
|
||||
- **Mutable**: Documents can be updated by the key holder
|
||||
- **TTL-based**: Published records have a TTL and must be refreshed periodically (every 2 hours recommended)
|
||||
- **Identifier format**: `did:dht:{z-base-32-encoded-ed25519-pubkey}`
|
||||
|
||||
## Architecture
|
||||
|
||||
### DID Relationship
|
||||
|
||||
```
|
||||
Node Identity (Ed25519 keypair)
|
||||
├── did:key:z6Mk... (derived from pubkey, offline, stable)
|
||||
└── did:dht:z6Mk... (published to DHT, discoverable, same key)
|
||||
```
|
||||
|
||||
Both DIDs use the same underlying Ed25519 keypair. The `did:dht` identifier is the z-base-32 encoding of the 32-byte public key. This means the same keypair produces both DID types — no additional key management.
|
||||
|
||||
### DHT Publication Flow
|
||||
|
||||
```
|
||||
1. Node generates Ed25519 keypair (already exists)
|
||||
2. Node creates DID Document with:
|
||||
- Ed25519 verification key (signing)
|
||||
- X25519 key agreement key (derived)
|
||||
- Service endpoints (optional: Tor onion, federation endpoint)
|
||||
3. DID Document encoded as DNS packet (RFC 1035)
|
||||
4. DNS packet signed with Ed25519 key (BEP-44 mutable item)
|
||||
5. Published to Mainline DHT under the public key
|
||||
6. Refreshed every 2 hours to maintain availability
|
||||
```
|
||||
|
||||
### DNS Packet Encoding
|
||||
|
||||
The DID Document maps to DNS resource records:
|
||||
|
||||
| Record Type | Name | Purpose |
|
||||
|------------|------|---------|
|
||||
| TXT `_did.` | `vm=k0` | Verification method: key 0 (Ed25519) |
|
||||
| TXT `_did.` | `auth=0` | Authentication uses key 0 |
|
||||
| TXT `_did.` | `asm=0` | AssertionMethod uses key 0 |
|
||||
| TXT `_k0._did.` | `id=0;t=0;k={base64url_pubkey}` | Key 0: Ed25519 public key |
|
||||
| TXT `_s0._did.` | `id=tor;t=TorHiddenService;se={onion}` | Service endpoint (optional) |
|
||||
|
||||
### Resolution Flow
|
||||
|
||||
```
|
||||
1. Receive did:dht:{identifier}
|
||||
2. Decode z-base-32 → 32-byte Ed25519 public key
|
||||
3. Query Mainline DHT for BEP-44 mutable item under that key
|
||||
4. Verify signature on the DHT payload
|
||||
5. Parse DNS packet → reconstruct DID Document
|
||||
6. Cache for 1 hour (reduce DHT load)
|
||||
```
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Rust Crate: `mainline`
|
||||
|
||||
Use the `mainline` crate for Mainline DHT access. It provides:
|
||||
- `Dht::client()` for resolution-only nodes
|
||||
- `Dht::server()` for full DHT participation
|
||||
- `MutableItem` for BEP-44 put/get operations
|
||||
- Ed25519 signing compatible with `ed25519-dalek`
|
||||
|
||||
Additional crates needed:
|
||||
- `simple-dns` or `trust-dns-proto` for DNS packet encoding/decoding
|
||||
- `zbase32` for z-base-32 encoding (did:dht identifier format)
|
||||
|
||||
### New Files
|
||||
|
||||
```
|
||||
core/archipelago/src/identity/
|
||||
├── did_dht.rs — did:dht creation, publication, resolution
|
||||
└── dns_packet.rs — DID Document ↔ DNS packet encoding
|
||||
```
|
||||
|
||||
### New RPC Endpoints
|
||||
|
||||
| Endpoint | Description |
|
||||
|----------|-------------|
|
||||
| `identity.create-dht-did` | Publish current node's DID to DHT |
|
||||
| `identity.resolve-dht-did` | Resolve a did:dht from the DHT |
|
||||
| `identity.refresh-dht-did` | Force refresh the DHT publication |
|
||||
| `identity.dht-status` | Check if node's did:dht is published and resolvable |
|
||||
|
||||
### Integration Points
|
||||
|
||||
1. **Server startup**: Optionally publish did:dht in background (non-blocking)
|
||||
2. **Identity manager**: Store did:dht alongside did:key in identity records
|
||||
3. **Federation**: Accept did:dht in peer join/discovery
|
||||
4. **Web5 UI**: Display both DID types, add publish/resolve buttons
|
||||
5. **Credentials**: Accept did:dht as issuer/subject in VCs
|
||||
|
||||
### Background Refresh
|
||||
|
||||
A background tokio task refreshes the DHT publication every 2 hours:
|
||||
|
||||
```
|
||||
spawn background task:
|
||||
loop {
|
||||
publish_to_dht(keypair, did_document_dns_packet)
|
||||
sleep(2 hours)
|
||||
}
|
||||
```
|
||||
|
||||
If the node is offline when the TTL expires, the record drops from the DHT. It gets re-published when the node comes back online.
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Same key for both DIDs**: No new key material to protect. The Ed25519 key already in `/var/lib/archipelago/identity/node_key` is used for both.
|
||||
|
||||
2. **DHT is public**: Publishing to the DHT makes the node's DID Document visible to anyone querying the DHT. This is intentional for discoverability. Sensitive information (Tor addresses) should only be included in the service endpoints if the user explicitly opts in.
|
||||
|
||||
3. **No Tor address by default**: The DID Document published to DHT should NOT include Tor hidden service addresses by default (per the security rule about not publishing onion addresses publicly). Tor addresses are exchanged privately via federation.
|
||||
|
||||
4. **BEP-44 signature verification**: All DHT records are signed with Ed25519. Resolvers verify the signature, preventing tampering.
|
||||
|
||||
5. **Sybil resistance**: did:dht identifiers are derived from public keys, so creating a fake identity requires generating a new keypair. The federation trust system already handles this via trust levels.
|
||||
|
||||
## Comparison: did:key vs did:dht
|
||||
|
||||
| Property | did:key | did:dht |
|
||||
|----------|---------|---------|
|
||||
| Offline creation | Yes | No (needs DHT access) |
|
||||
| Discoverable | No (must share manually) | Yes (query by identifier) |
|
||||
| Persistence | Permanent (derived from key) | TTL-based (needs refresh) |
|
||||
| Network requirement | None | UDP to DHT peers |
|
||||
| Resolution | Local computation only | DHT query (~1-5s) |
|
||||
| Privacy | Key not published anywhere | Key is on the DHT |
|
||||
| W3C standard | Yes (DID Core) | Yes (DID Core) |
|
||||
|
||||
## Timeline
|
||||
|
||||
1. **DHT-02**: Implement did:dht creation + publication (~2 days)
|
||||
2. **DHT-03**: Implement did:dht resolution + caching (~1 day)
|
||||
3. **DHT-04**: Web5 UI integration (~1 day)
|
||||
4. **Testing**: Cross-node resolution via DHT (separate from Tor) (~1 day)
|
||||
|
||||
## References
|
||||
|
||||
- [did:dht Method Specification](https://did-dht.com/)
|
||||
- [BEP-44: Storing arbitrary data in the DHT](https://www.bittorrent.org/beps/bep_0044.html)
|
||||
- [Mainline DHT crate](https://crates.io/crates/mainline)
|
||||
- [W3C DID Core 1.0](https://www.w3.org/TR/did-core/)
|
||||
@@ -1,400 +0,0 @@
|
||||
# Archipelago DWN Protocol Definitions
|
||||
|
||||
## Overview
|
||||
|
||||
These protocol definitions specify the data schemas used when Archipelago nodes exchange data via Decentralized Web Nodes (DWN). By defining protocols in the standard DWN format, any DWN-compatible application can read and write Archipelago data.
|
||||
|
||||
Each protocol defines:
|
||||
- A unique URI identifier
|
||||
- Data types with JSON schemas
|
||||
- Structure rules (who can read/write what)
|
||||
|
||||
## Protocol 1: Node Identity Announcements
|
||||
|
||||
**URI**: `https://archipelago.dev/protocols/node-identity/v1`
|
||||
|
||||
Nodes publish their identity information so federated peers can discover capabilities and status.
|
||||
|
||||
```json
|
||||
{
|
||||
"protocol": "https://archipelago.dev/protocols/node-identity/v1",
|
||||
"published": true,
|
||||
"types": {
|
||||
"announcement": {
|
||||
"schema": "https://archipelago.dev/schemas/node-announcement/v1",
|
||||
"dataFormats": ["application/json"]
|
||||
}
|
||||
},
|
||||
"structure": {
|
||||
"announcement": {
|
||||
"$actions": [
|
||||
{ "who": "anyone", "can": ["read"] },
|
||||
{ "who": "author", "of": "announcement", "can": ["write", "update", "delete"] }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Schema**: `node-announcement/v1`
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "object",
|
||||
"required": ["did", "version", "apps", "timestamp"],
|
||||
"properties": {
|
||||
"did": {
|
||||
"type": "string",
|
||||
"description": "Node's DID (did:key or did:dht)"
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"description": "Archipelago version (semver)"
|
||||
},
|
||||
"apps": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "List of installed app IDs"
|
||||
},
|
||||
"capabilities": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Node capabilities (e.g., 'file-sharing', 'dwn-sync', 'tor')"
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "ISO 8601 timestamp of announcement"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Protocol 2: File Sharing Catalog
|
||||
|
||||
**URI**: `https://archipelago.dev/protocols/file-catalog/v1`
|
||||
|
||||
Nodes publish their shared file catalogs so peers can browse available content.
|
||||
|
||||
```json
|
||||
{
|
||||
"protocol": "https://archipelago.dev/protocols/file-catalog/v1",
|
||||
"published": true,
|
||||
"types": {
|
||||
"entry": {
|
||||
"schema": "https://archipelago.dev/schemas/file-entry/v1",
|
||||
"dataFormats": ["application/json"]
|
||||
}
|
||||
},
|
||||
"structure": {
|
||||
"entry": {
|
||||
"$actions": [
|
||||
{ "who": "anyone", "can": ["read"] },
|
||||
{ "who": "author", "of": "entry", "can": ["write", "update", "delete"] }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Schema**: `file-entry/v1`
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "object",
|
||||
"required": ["id", "title", "access", "size_bytes", "created_at"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Unique file entry ID"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "Display title"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Optional description"
|
||||
},
|
||||
"content_type": {
|
||||
"type": "string",
|
||||
"description": "MIME type (e.g., 'application/pdf')"
|
||||
},
|
||||
"size_bytes": {
|
||||
"type": "integer",
|
||||
"description": "File size in bytes"
|
||||
},
|
||||
"access": {
|
||||
"type": "string",
|
||||
"enum": ["free", "peers-only", "paid"],
|
||||
"description": "Access level"
|
||||
},
|
||||
"price_sats": {
|
||||
"type": "integer",
|
||||
"description": "Price in satoshis (for paid access)"
|
||||
},
|
||||
"hash": {
|
||||
"type": "string",
|
||||
"description": "SHA-256 hash of file content"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Content tags for filtering"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Protocol 3: Federation State
|
||||
|
||||
**URI**: `https://archipelago.dev/protocols/federation/v1`
|
||||
|
||||
Nodes publish their federation membership and peer trust status.
|
||||
|
||||
```json
|
||||
{
|
||||
"protocol": "https://archipelago.dev/protocols/federation/v1",
|
||||
"published": false,
|
||||
"types": {
|
||||
"membership": {
|
||||
"schema": "https://archipelago.dev/schemas/federation-membership/v1",
|
||||
"dataFormats": ["application/json"]
|
||||
},
|
||||
"peerStatus": {
|
||||
"schema": "https://archipelago.dev/schemas/peer-status/v1",
|
||||
"dataFormats": ["application/json"]
|
||||
}
|
||||
},
|
||||
"structure": {
|
||||
"membership": {
|
||||
"$actions": [
|
||||
{ "who": "recipient", "of": "membership", "can": ["read"] },
|
||||
{ "who": "author", "of": "membership", "can": ["write", "update", "delete"] }
|
||||
]
|
||||
},
|
||||
"peerStatus": {
|
||||
"$actions": [
|
||||
{ "who": "recipient", "of": "peerStatus", "can": ["read"] },
|
||||
{ "who": "author", "of": "peerStatus", "can": ["write", "update"] }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Schema**: `federation-membership/v1`
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "object",
|
||||
"required": ["node_did", "trust_level", "joined_at"],
|
||||
"properties": {
|
||||
"node_did": {
|
||||
"type": "string",
|
||||
"description": "DID of the federated node"
|
||||
},
|
||||
"trust_level": {
|
||||
"type": "string",
|
||||
"enum": ["trusted", "verified", "untrusted"],
|
||||
"description": "Trust level assigned to this peer"
|
||||
},
|
||||
"joined_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"last_seen": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"apps": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Apps reported by this peer"
|
||||
},
|
||||
"credential_id": {
|
||||
"type": "string",
|
||||
"description": "VC ID proving federation relationship"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Schema**: `peer-status/v1`
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "object",
|
||||
"required": ["node_did", "online", "timestamp"],
|
||||
"properties": {
|
||||
"node_did": {
|
||||
"type": "string"
|
||||
},
|
||||
"online": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"cpu_percent": {
|
||||
"type": "number"
|
||||
},
|
||||
"memory_used_mb": {
|
||||
"type": "integer"
|
||||
},
|
||||
"disk_used_percent": {
|
||||
"type": "number"
|
||||
},
|
||||
"container_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"uptime_seconds": {
|
||||
"type": "integer"
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Protocol 4: App Deployment Requests
|
||||
|
||||
**URI**: `https://archipelago.dev/protocols/app-deploy/v1`
|
||||
|
||||
Enables trusted peers to request app installations on remote nodes.
|
||||
|
||||
```json
|
||||
{
|
||||
"protocol": "https://archipelago.dev/protocols/app-deploy/v1",
|
||||
"published": false,
|
||||
"types": {
|
||||
"request": {
|
||||
"schema": "https://archipelago.dev/schemas/deploy-request/v1",
|
||||
"dataFormats": ["application/json"]
|
||||
},
|
||||
"response": {
|
||||
"schema": "https://archipelago.dev/schemas/deploy-response/v1",
|
||||
"dataFormats": ["application/json"]
|
||||
}
|
||||
},
|
||||
"structure": {
|
||||
"request": {
|
||||
"$actions": [
|
||||
{ "who": "recipient", "of": "request", "can": ["read"] },
|
||||
{ "who": "author", "of": "request", "can": ["write"] }
|
||||
],
|
||||
"response": {
|
||||
"$actions": [
|
||||
{ "who": "author", "of": "request", "can": ["read"] },
|
||||
{ "who": "recipient", "of": "request", "can": ["write"] }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Schema**: `deploy-request/v1`
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "object",
|
||||
"required": ["app_id", "requester_did", "target_did", "action"],
|
||||
"properties": {
|
||||
"app_id": {
|
||||
"type": "string",
|
||||
"description": "Application identifier (e.g., 'bitcoin-knots')"
|
||||
},
|
||||
"requester_did": {
|
||||
"type": "string",
|
||||
"description": "DID of the requesting node"
|
||||
},
|
||||
"target_did": {
|
||||
"type": "string",
|
||||
"description": "DID of the target node"
|
||||
},
|
||||
"action": {
|
||||
"type": "string",
|
||||
"enum": ["install", "update", "uninstall"],
|
||||
"description": "Deployment action"
|
||||
},
|
||||
"image": {
|
||||
"type": "string",
|
||||
"description": "Docker/OCI image reference"
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"description": "Requested version"
|
||||
},
|
||||
"reason": {
|
||||
"type": "string",
|
||||
"description": "Human-readable reason for request"
|
||||
},
|
||||
"requested_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Schema**: `deploy-response/v1`
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "object",
|
||||
"required": ["request_id", "status"],
|
||||
"properties": {
|
||||
"request_id": {
|
||||
"type": "string",
|
||||
"description": "ID of the original request"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["accepted", "rejected", "completed", "failed"],
|
||||
"description": "Response status"
|
||||
},
|
||||
"reason": {
|
||||
"type": "string",
|
||||
"description": "Reason for rejection or failure"
|
||||
},
|
||||
"completed_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Registration
|
||||
|
||||
On backend startup, all 4 protocols should be auto-registered via `dwn.register-protocol`:
|
||||
|
||||
```rust
|
||||
const ARCHIPELAGO_PROTOCOLS: &[&str] = &[
|
||||
"https://archipelago.dev/protocols/node-identity/v1",
|
||||
"https://archipelago.dev/protocols/file-catalog/v1",
|
||||
"https://archipelago.dev/protocols/federation/v1",
|
||||
"https://archipelago.dev/protocols/app-deploy/v1",
|
||||
];
|
||||
```
|
||||
|
||||
## Interoperability
|
||||
|
||||
These protocols follow the DWN protocol definition format. Any application that implements DWN can:
|
||||
|
||||
1. **Read** node announcements to discover Archipelago nodes
|
||||
2. **Browse** file catalogs to find shared content
|
||||
3. **Query** federation state to understand network topology
|
||||
4. **Request** app deployments through the standard DWN messaging interface
|
||||
|
||||
The `published: true` flag on protocols 1 and 2 means any DWN node can query for these records. Protocols 3 and 4 are `published: false` (private — only shared with authorized peers).
|
||||
@@ -1,38 +0,0 @@
|
||||
# Hardware Compatibility Matrix
|
||||
|
||||
## Tested Platforms
|
||||
|
||||
| Platform | CPU | RAM | Storage | Status | Notes |
|
||||
|----------|-----|-----|---------|--------|-------|
|
||||
| HP ProDesk 400 G4 | Intel i3-8100T (4c/4t) | 16GB DDR4 | 1.8TB NVMe | **Certified** | Primary dev/test node (.228) |
|
||||
| Generic x86_64 | — | 8GB | 457GB | **Certified** | Secondary node (.198), memory-constrained |
|
||||
|
||||
## Planned Platforms (Untested)
|
||||
|
||||
| Platform | Architecture | Expected RAM | Notes |
|
||||
|----------|-------------|-------------|-------|
|
||||
| Intel NUC 13 Pro | x86_64 | 16-32GB | Compact, NVMe, good for home server |
|
||||
| Raspberry Pi 5 | ARM64 | 8GB | ARM64 build exists (docs/arm64-build.md) |
|
||||
| Mini-PC (N100) | x86_64 | 8-16GB | Low power, fanless options |
|
||||
| Lenovo ThinkCentre M720q | x86_64 | 16-32GB | Used market, reliable |
|
||||
|
||||
## Minimum Requirements
|
||||
|
||||
- **CPU**: 2 cores (4 recommended for 30+ containers)
|
||||
- **RAM**: 4GB minimum (Core tier only), 8GB recommended, 16GB for all apps
|
||||
- **Storage**: 500GB minimum (Bitcoin blockchain ~600GB), 1TB+ recommended
|
||||
- **Network**: Ethernet (WiFi not recommended for servers)
|
||||
|
||||
## Known Platform Quirks
|
||||
|
||||
### .198 (8GB RAM)
|
||||
- Crash recovery takes 260s (sequential container restart on limited RAM)
|
||||
- Swap required (4GB minimum) to prevent OOM
|
||||
- Background crash recovery (PERF-01) essential for health endpoint availability
|
||||
- Backup with Argon2 KDF slow without adequate free RAM
|
||||
|
||||
### ARM64 (Raspberry Pi)
|
||||
- Container images must be multi-arch or ARM64-specific
|
||||
- Bitcoin Knots ARM64 image available
|
||||
- Some containers (OnlyOffice) have no ARM64 build — must be excluded
|
||||
- USB boot requires special ISO preparation
|
||||
@@ -1,35 +0,0 @@
|
||||
# Hardware Regression Test Results — v1.0.0
|
||||
|
||||
## x86_64 (Intel i3-8100T @ 3.10GHz, 16GB RAM, 1.8TB NVMe)
|
||||
|
||||
**Device**: Dev server (192.168.1.228)
|
||||
**OS**: Debian 12 (Bookworm)
|
||||
**Date**: 2026-03-11
|
||||
|
||||
### Results
|
||||
|
||||
| Test | Result | Notes |
|
||||
|------|--------|-------|
|
||||
| Backend health endpoint | PASS | HTTP 200 in <1ms |
|
||||
| Web UI loads | PASS | HTTP 200, full SPA renders |
|
||||
| Nginx proxy | PASS | All routes proxied correctly |
|
||||
| Container runtime | PASS | 20+ containers running via Podman |
|
||||
| Uptime monitor | PASS | 100% uptime (3 checks, systemd timer active) |
|
||||
| Soak test | RUNNING | 30-day test started, ends April 10 |
|
||||
| ISO build | PASS | 12GB ISO built in ~4 minutes |
|
||||
| RPC API | PASS | All endpoints respond with correct JSON-RPC format |
|
||||
| WebSocket | PASS | Real-time updates functional |
|
||||
| Tor hidden services | PASS | Container running, services registered |
|
||||
| Federation | PASS | Peer endpoints responding |
|
||||
|
||||
### Pending Hardware Tests
|
||||
|
||||
| Platform | Status | Blocker |
|
||||
|----------|--------|---------|
|
||||
| Intel NUC | NOT TESTED | Requires physical hardware |
|
||||
| Raspberry Pi 5 (ARM64) | NOT TESTED | Requires ARM64 device + ARM64 ISO |
|
||||
| Generic x86_64 PC | PARTIAL | Dev server is the test platform |
|
||||
|
||||
## Summary
|
||||
|
||||
x86_64 platform fully validated on dev server. ARM64 and additional x86_64 hardware testing requires physical devices.
|
||||
@@ -1,179 +0,0 @@
|
||||
# Hardware Wallet Integration Architecture
|
||||
|
||||
## Overview
|
||||
|
||||
Archipelago supports hardware wallets for secure Bitcoin transaction signing via PSBT (Partially Signed Bitcoin Transactions). This document covers integration with ColdCard, Trezor, and Ledger hardware wallets.
|
||||
|
||||
## Supported Devices
|
||||
|
||||
| Device | Connection | PSBT Support | DID Signing | Detection |
|
||||
|--------|-----------|-------------|-------------|-----------|
|
||||
| ColdCard Mk4 | USB / MicroSD / NFC | Native PSBT | No (Bitcoin-only) | USB VID `0xd13e` |
|
||||
| Trezor Model T/Safe 3 | USB / WebUSB | Via trezorctl | No | USB VID `0x534c` (SatoshiLabs) |
|
||||
| Ledger Nano S/X/Plus | USB / Bluetooth | Via HWI | No | USB VID `0x2c97` |
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Archipelago Node │
|
||||
│ │
|
||||
│ ┌──────────┐ ┌────────────┐ ┌──────────────────────────┐ │
|
||||
│ │ Web UI │───▸│ RPC Server │───▸│ LND (gRPC) │ │
|
||||
│ │ │ │ │ │ - FundPsbt │ │
|
||||
│ │ QR code │ │ lnd.create │ │ - SignPsbt (partial) │ │
|
||||
│ │ display │ │ -psbt │ │ - FinalizePsbt │ │
|
||||
│ │ │ │ │ │ - PublishTransaction │ │
|
||||
│ │ File │ │ lnd.final │ └──────────────────────────┘ │
|
||||
│ │ upload │ │ ize-psbt │ │
|
||||
│ └──────────┘ └────────────┘ │
|
||||
│ ▲ │
|
||||
│ │ PSBT (base64) │
|
||||
│ ▼ │
|
||||
│ ┌──────────────────────┐ │
|
||||
│ │ Hardware Wallet │ │
|
||||
│ │ - USB direct │ │
|
||||
│ │ - QR code scan │ │
|
||||
│ │ - MicroSD (CC) │ │
|
||||
│ └──────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## PSBT Signing Flow
|
||||
|
||||
### 1. Create Unsigned PSBT
|
||||
|
||||
The user initiates a transaction (send coins, open channel, close channel). Instead of LND signing automatically, we create an unsigned PSBT.
|
||||
|
||||
**RPC endpoint**: `lnd.create-psbt`
|
||||
|
||||
```json
|
||||
{
|
||||
"method": "lnd.create-psbt",
|
||||
"params": {
|
||||
"outputs": [{"address": "bc1q...", "amount_sats": 50000}],
|
||||
"fee_rate_sat_per_vbyte": 10,
|
||||
"change_address": "bc1q..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"psbt_base64": "cHNidP8BAH...",
|
||||
"psbt_hex": "70736274ff...",
|
||||
"estimated_fee_sats": 1420,
|
||||
"inputs": [{"txid": "abc...", "vout": 0, "amount_sats": 100000}],
|
||||
"outputs": [{"address": "bc1q...", "amount_sats": 50000}, {"address": "bc1q...", "amount_sats": 48580}]
|
||||
}
|
||||
```
|
||||
|
||||
**LND gRPC mapping**: Uses `WalletKit.FundPsbt` to select UTXOs and create the PSBT template.
|
||||
|
||||
### 2. Sign with Hardware Wallet
|
||||
|
||||
Three transfer methods supported:
|
||||
|
||||
#### QR Code (ColdCard NFC, Trezor via companion app)
|
||||
- Display PSBT as animated QR code (BBQr format for large PSBTs)
|
||||
- User scans with hardware wallet
|
||||
- Hardware wallet displays transaction details for verification
|
||||
- User confirms on device
|
||||
- Signed PSBT returned as QR code — user scans with camera or uploads screenshot
|
||||
|
||||
#### USB Direct (Trezor, Ledger)
|
||||
- Detect hardware wallet USB device
|
||||
- Pass PSBT via USB HID protocol
|
||||
- User confirms on device
|
||||
- Signed PSBT returned via USB
|
||||
|
||||
#### MicroSD (ColdCard)
|
||||
- Export PSBT file for download
|
||||
- User transfers to ColdCard via MicroSD
|
||||
- ColdCard signs and saves signed PSBT to MicroSD
|
||||
- User uploads signed PSBT file back to Archipelago
|
||||
|
||||
### 3. Finalize and Broadcast
|
||||
|
||||
**RPC endpoint**: `lnd.finalize-psbt`
|
||||
|
||||
```json
|
||||
{
|
||||
"method": "lnd.finalize-psbt",
|
||||
"params": {
|
||||
"signed_psbt_base64": "cHNidP8BAH..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"txid": "abc123...",
|
||||
"raw_tx_hex": "0200000001...",
|
||||
"broadcast": true
|
||||
}
|
||||
```
|
||||
|
||||
**LND gRPC mapping**: Uses `WalletKit.FinalizePsbt` then `WalletKit.PublishTransaction`.
|
||||
|
||||
## USB Device Detection
|
||||
|
||||
**RPC endpoint**: `system.detect-usb-devices`
|
||||
|
||||
Scans `/sys/bus/usb/devices/` or uses `lsusb` to detect known hardware wallet vendor IDs:
|
||||
|
||||
| Vendor | VID | Product IDs |
|
||||
|--------|-----|-------------|
|
||||
| ColdCard (Coinkite) | `0xd13e` | `0xcc10` (Mk4), `0xcc20` (Q) |
|
||||
| Trezor (SatoshiLabs) | `0x534c` | `0x0001` (One), `0x0002` (T) |
|
||||
| Ledger | `0x2c97` | `0x0001` (Nano S), `0x0004` (Nano X), `0x0005` (Nano S+) |
|
||||
|
||||
Implementation approach:
|
||||
```rust
|
||||
// Read from /sys/bus/usb/devices/*/idVendor and idProduct
|
||||
async fn detect_usb_devices(known_vids: &[(u16, &str)]) -> Vec<DetectedDevice> {
|
||||
// Parse /sys/bus/usb/devices/X-Y/idVendor
|
||||
// Match against known VIDs
|
||||
// Return device list with type identification
|
||||
}
|
||||
```
|
||||
|
||||
The detection runs server-side since the hardware wallet is plugged into the Archipelago node (not the browser).
|
||||
|
||||
## UI Integration Points
|
||||
|
||||
### Send Coins View
|
||||
- Add "Sign with Hardware Wallet" toggle/option
|
||||
- When enabled: create unsigned PSBT → show QR/download → accept signed PSBT → finalize
|
||||
|
||||
### Channel Management
|
||||
- Open Channel: PSBT funding option
|
||||
- Close Channel: Cooperative close via PSBT
|
||||
|
||||
### Hardware Wallet Status
|
||||
- Show notification banner when USB device detected
|
||||
- Display device type and connection status
|
||||
- Auto-detect on the Server/Dashboard page
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **PSBT verification**: Display transaction details (amounts, addresses, fees) before and after hardware signing — user should verify they match
|
||||
2. **No private keys on node**: When using hardware wallet flow, LND's internal wallet creates watch-only inputs; the hardware wallet holds the actual signing keys
|
||||
3. **PSBT size limits**: QR codes can handle ~2KB; larger PSBTs need animated QR (BBQr) or file transfer
|
||||
4. **USB permissions**: The `archipelago` user needs access to USB HID devices (`udev` rules)
|
||||
|
||||
## Implementation Priority
|
||||
|
||||
1. **Phase 1** (HW-02): PSBT create/finalize RPC endpoints via LND gRPC
|
||||
2. **Phase 2** (HW-03): QR code display + file upload UI
|
||||
3. **Phase 3** (HW-04): USB device detection and notification
|
||||
4. **Future**: Direct USB HID communication (trezorlib, ledger-transport)
|
||||
|
||||
## Dependencies
|
||||
|
||||
- LND v0.18+ (PSBT API via WalletKit)
|
||||
- `qrcode` npm package (QR generation in UI)
|
||||
- `lsusb` or `/sys/bus/usb/` access (device detection)
|
||||
- No external hardware wallet libraries needed for Phase 1-3 (PSBT is a standard format)
|
||||
@@ -1,414 +0,0 @@
|
||||
# Off-Grid Bitcoin Transaction Security Analysis
|
||||
|
||||
> Comprehensive security analysis for off-grid Bitcoin transactions over mesh radio (LoRa/Meshcore) in the Archipelago context. Covers attack vectors, trust models, and mitigations for every layer of the stack.
|
||||
|
||||
## 1. Transaction Creation & Signing (Offline)
|
||||
|
||||
Offline signing is cryptographically safe. Bitcoin signing is a pure secp256k1 operation — no network needed. PSBT (BIP174) was designed exactly for this.
|
||||
|
||||
### Key Risks
|
||||
|
||||
| Attack | Severity | Trustless Fix? | Description |
|
||||
|--------|----------|----------------|-------------|
|
||||
| Stale UTXO data | High | No — requires chain state | UTXO already spent; tx is invalid. No fund loss, wastes time/bandwidth. |
|
||||
| Address substitution on unsigned PSBT | Critical | Yes — verify on trusted display | Compromised PSBT creator substitutes destination address. |
|
||||
| Fee manipulation in PSBT | Medium | Yes — signer verifies fee | Compromised PSBT creator inflates fees (theft to miners). |
|
||||
| Double-spend by offline sender | Critical | No — fundamental | Sender can sign conflicting txs; nothing is final until confirmed in a block. |
|
||||
|
||||
### PSBT Security Model
|
||||
|
||||
PSBTs are tamper-evident but not tamper-proof across a network. The signer verifies what they sign, but cannot prevent the PSBT creator from lying about context. In a mesh context:
|
||||
- Sign PSBTs on the local device only
|
||||
- Only send the fully-signed raw transaction over mesh for broadcast
|
||||
- Never send unsigned PSBTs over mesh — the relay could modify outputs
|
||||
|
||||
### Archipelago Status
|
||||
|
||||
`PsbtHash` (type 3) sends only the SHA-256 hash over mesh, not the PSBT itself — correct. Actual PSBT exchange should happen on a trusted local channel (USB, QR code, NFC).
|
||||
|
||||
---
|
||||
|
||||
## 2. Transaction Broadcasting / Relay Trust
|
||||
|
||||
A signed Bitcoin transaction is cryptographically sealed by the sender's private key. The relay **cannot**:
|
||||
- Change the destination address
|
||||
- Change the amount or fee
|
||||
- Steal funds or extract the private key
|
||||
|
||||
The relay **can**:
|
||||
- **Not broadcast it** (censorship) — High severity
|
||||
- **Delay broadcasting** (enables race conditions) — High severity
|
||||
- **Claim it broadcast when it didn't** — High severity
|
||||
- **Front-run** (only relevant for DEX/DeFi trades, not standard payments)
|
||||
|
||||
### Proof of Broadcast
|
||||
|
||||
There is no native Bitcoin "proof of broadcast." Mitigations:
|
||||
1. Relay returns signed attestation (Ed25519) with txid + timestamp
|
||||
2. Sender watches for confirmation via block header relay
|
||||
3. Multiple independent relays reduce collusion risk
|
||||
4. Relay returns actual `sendrawtransaction` RPC response from Bitcoin Core
|
||||
|
||||
### Archipelago Status
|
||||
|
||||
`TxRelayResponse` returns the actual RPC result (`txid`, `error`, `error_code`) — good. However, the response is **not signed** by the relay, so a mesh MITM could forge it. `TxConfirmation` (type 12) provides follow-up confirmation updates (1, 2, 3 confirmations), which is the real proof.
|
||||
|
||||
### Gap: Sign TxRelayResponse
|
||||
|
||||
**Recommendation**: Sign `TxRelayResponse` messages with the relay's Ed25519 key (using `TypedEnvelope::new_signed`). This prevents a mesh MITM from forging relay responses.
|
||||
|
||||
---
|
||||
|
||||
## 3. Payment Verification Without a Full Node
|
||||
|
||||
### SPV (Simplified Payment Verification)
|
||||
|
||||
SPV clients download only block headers (80 bytes each) and verify:
|
||||
- Chain of proof-of-work is valid
|
||||
- Transaction is included in a block via Merkle proof
|
||||
|
||||
**What SPV can verify:**
|
||||
- Block header has valid proof-of-work
|
||||
- Transaction included in a specific block (via Merkle proof)
|
||||
- Which chain has the most cumulative proof-of-work
|
||||
|
||||
**What SPV cannot verify:**
|
||||
- That the block is actually valid (could contain invalid transactions)
|
||||
- That the chain is canonical (if eclipsed, attacker feeds fake chain)
|
||||
- That a transaction has NOT been included (omission attacks)
|
||||
|
||||
### Block Headers Over Mesh
|
||||
|
||||
Block headers (80 bytes, or Archipelago's compact 44-byte format) allow:
|
||||
- Tracking chain tip (current block height)
|
||||
- Detecting stale/fake data (blocks should arrive ~every 10 min)
|
||||
- Verifying proof-of-work continuity
|
||||
|
||||
**Headers alone are NOT sufficient for SPV verification.** You also need Merkle proofs (~320-384 bytes per transaction) to verify inclusion. This fits within Archipelago's Reed-Solomon chunking.
|
||||
|
||||
### Compact Block Filters (BIP157/158)
|
||||
|
||||
~15KB per block — too large for LoRa. But the relay node can run a full node, do filter matching locally, and relay only relevant Merkle proofs back.
|
||||
|
||||
### Eclipse Attacks
|
||||
|
||||
If a mesh node gets headers from only one relay, that relay can feed fake headers. Mining one fake block costs ~$300K-500K at current difficulty — impractical for small amounts, relevant for high value.
|
||||
|
||||
### Archipelago Status
|
||||
|
||||
`BlockHeaderCache` stores headers by height and tracks latest height. `BlockHeaderPayload` includes `height`, `hash`, `prev_hash`, `timestamp`, and `announced_by`. The `announced_by` field enables multi-relay comparison.
|
||||
|
||||
### Gaps
|
||||
|
||||
- No chain continuity validation (prev_hash linkage)
|
||||
- No proof-of-work validation on received headers
|
||||
- No multi-relay header comparison
|
||||
- No Merkle proof relay for transaction inclusion verification
|
||||
- No timestamp sanity checking
|
||||
|
||||
---
|
||||
|
||||
## 4. Double-Spend Attacks in Off-Grid Context
|
||||
|
||||
This is the most dangerous attack category for off-grid Bitcoin.
|
||||
|
||||
### Attack Scenarios
|
||||
|
||||
| Scenario | Severity | Trustless Fix |
|
||||
|----------|----------|---------------|
|
||||
| Split-path: mesh TX-A + internet TX-B (sender sends conflicting txs on two channels) | Critical | None — wait for confirmations |
|
||||
| RBF attack: sender replaces mesh TX via internet with higher-fee conflicting tx | Critical | Detect RBF signaling (nSequence), reject/warn |
|
||||
| Time-delay: relay holds TX while sender broadcasts conflicting tx via internet | High | Multiple relays, monitor for confirmation |
|
||||
|
||||
### Confirmation Safety Levels
|
||||
|
||||
| Confirmations | Time | Security Level | Off-Grid Recommendation |
|
||||
|---------------|------|----------------|------------------------|
|
||||
| 0 (mempool) | Immediate | Zero — trivially reversible | Never accept for any value |
|
||||
| 1 | ~10 min | Low — rare reorg can reverse | Minimum for small amounts |
|
||||
| 2 | ~20 min | Medium — very unlikely reversed | Good for moderate amounts |
|
||||
| 3 | ~30 min | High — practically irreversible | Recommended for meaningful amounts |
|
||||
| 6 | ~60 min | Very high — requires 51% attack | Required for high value |
|
||||
|
||||
### Archipelago Status
|
||||
|
||||
`TxConfirmation` (type 12) tracks 1, 2, 3 confirmations and `block_height` — correct approach.
|
||||
|
||||
### Gap: RBF Detection
|
||||
|
||||
**Recommendation**: Check `nSequence` on relayed transactions. If it signals RBF (nSequence < 0xFFFFFFFE), warn the sender or reject the relay in off-grid context.
|
||||
|
||||
---
|
||||
|
||||
## 5. Balance Checking — Risks and Considerations
|
||||
|
||||
On its own, knowing a balance is **low severity** — all Bitcoin balances are public on-chain. However, in a mesh context, the concern shifts to metadata:
|
||||
|
||||
| Risk | Severity | Description |
|
||||
|------|----------|-------------|
|
||||
| Privacy leak via mesh | Medium | If balance queries are unencrypted, mesh listeners learn which addresses a node controls |
|
||||
| Targeted robbery ("$5 wrench attack") | High | Knowing a nearby mesh user holds significant BTC creates physical safety risk |
|
||||
| Double-spend calibration | Medium | Attacker learns victim's UTXO set, can craft better conflicting transactions |
|
||||
| Change address correlation | Medium | Balance checks reveal which outputs belong to the same wallet |
|
||||
|
||||
### Mitigations
|
||||
|
||||
- All balance queries must be E2E encrypted (Archipelago already does this)
|
||||
- The relay should not learn which addresses are being queried (use compact block filters or xpub-blind queries)
|
||||
- Consider running balance checks against the local pruned node rather than relaying
|
||||
- Never display exact balances in mesh message logs
|
||||
- Watch-only wallet approach: node only has xpubs, so even if compromised, no funds can be stolen
|
||||
|
||||
### Is Balance Info Useful to an Attacker?
|
||||
|
||||
**Not fundamentally** — the same data is publicly available on any block explorer. The real risk is **correlating an address/balance to a physical location via mesh radio proximity**. The mesh signal reveals "someone nearby controls this wallet." That's the threat, not the balance data itself.
|
||||
|
||||
---
|
||||
|
||||
## 6. Relay/Intermediary Attacks
|
||||
|
||||
### Man-in-the-Middle
|
||||
|
||||
- **Without encryption**: MITM can read, modify, replay everything. Critical.
|
||||
- **With Archipelago's encryption**: Messages use ChaCha20-Poly1305 with X25519 key agreement. MITM cannot decrypt or modify. Reduced to traffic analysis.
|
||||
|
||||
### Address Substitution
|
||||
|
||||
If the relay constructs the unsigned PSBT → **Critical** (relay can substitute address).
|
||||
If the sender signs locally and sends signed tx → **Safe** (signature prevents modification).
|
||||
|
||||
Archipelago's `TxRelayPayload` contains `tx_hex` (fully signed) — correct. Relay cannot modify.
|
||||
|
||||
### Replay Attacks
|
||||
|
||||
Bitcoin transactions are inherently idempotent — replaying a signed tx is harmless (network rejects duplicates). For non-transaction messages, the `TypedEnvelope` includes a `ts` timestamp for replay window rejection. The Double Ratchet provides per-message keys with forward secrecy, inherently preventing replay.
|
||||
|
||||
### Sybil Attacks
|
||||
|
||||
Attacker runs multiple mesh nodes to surround a target (mesh eclipse attack).
|
||||
- **Severity**: High — enables censorship, fake headers, selective relay
|
||||
- **Mitigation**: Pre-configured trusted peer list (known Ed25519 public keys via DID)
|
||||
|
||||
### Single Malicious Relay
|
||||
|
||||
If your only relay to the internet is malicious:
|
||||
- Can censor transactions
|
||||
- Can feed fake block headers (within PoW cost constraints)
|
||||
- Can claim broadcasts happened when they didn't
|
||||
- **Cannot** steal funds, modify transactions, or extract keys
|
||||
|
||||
Same trust model as running a Bitcoin node behind a single ISP.
|
||||
|
||||
---
|
||||
|
||||
## 7. Lightning Network Off-Grid Considerations
|
||||
|
||||
### Can Lightning Work Over Mesh?
|
||||
|
||||
Partially, with severe constraints:
|
||||
- **Invoice generation**: Works offline (just needs keys + channel state). BOLT11 relayed via mesh.
|
||||
- **Payment routing**: Requires the *paying* node to be online. Mesh-only node cannot route.
|
||||
- **Relay model**: Mesh node generates invoice → sends via mesh → internet peer pays with its own LND. Requires trust in relay.
|
||||
|
||||
### Channel State Attacks
|
||||
|
||||
**Critical risk for off-grid LN nodes.** If your node goes offline:
|
||||
- Channel partner can broadcast revoked (outdated) commitment transaction
|
||||
- They have the CSV delay (~24 hours) to steal funds before you can respond
|
||||
- If offline longer than CSV delay, **funds can be stolen**
|
||||
|
||||
### Watchtower Requirements
|
||||
|
||||
Mandatory for any off-grid LN node:
|
||||
- Must be internet-connected and always online
|
||||
- Needs encrypted breach remedy data (provided in advance)
|
||||
- Does NOT need private keys — only pre-signed penalty transactions
|
||||
- LND has built-in watchtower client/server
|
||||
|
||||
### HTLC Timeout Risks
|
||||
|
||||
Lightning HTLCs use absolute timelocks. Over high-latency mesh:
|
||||
- Invoice relay takes minutes to hours
|
||||
- HTLC might expire before payment completes
|
||||
- Locked funds until timeout resolution
|
||||
|
||||
### Recommendations
|
||||
|
||||
- Close or minimize Lightning channels before going off-grid
|
||||
- Use watchtowers (configure before going offline)
|
||||
- Set long CSV delays (1008 blocks / ~7 days) for off-grid risk channels
|
||||
- Validate BOLT11 invoice expiry before relay payment (reject if <10 min remaining)
|
||||
|
||||
### Archipelago Status
|
||||
|
||||
`LightningRelayPayload` includes `bolt11` and `amount_sats`. `LightningRelayResponsePayload` returns `payment_hash` and `preimage` (cryptographic proof of payment). The preimage is sufficient proof.
|
||||
|
||||
### Gap: Invoice Expiry Validation
|
||||
|
||||
**Recommendation**: Relay should validate BOLT11 invoice expiry before attempting payment. Reject if about to expire.
|
||||
|
||||
---
|
||||
|
||||
## 8. Trusted vs. Trustless Solutions
|
||||
|
||||
| Solution | Trust Level | Off-Grid Fit | Best For | Bandwidth |
|
||||
|----------|-------------|--------------|----------|-----------|
|
||||
| On-chain + confirmations | Trustless | Good (with relay) | High value, can wait | ~250-500 bytes/tx |
|
||||
| Fedimint ecash | Federation (3-of-5) | Excellent | Community payments | ~200 bytes/token |
|
||||
| Cashu ecash | Single mint | Excellent | Small amounts, fast | ~200 bytes/token |
|
||||
| Multisig escrow (2-of-3) | Arbiter | Good with PSBT | High-value trades | ~500 bytes/PSBT |
|
||||
| Lightning relay | Relay trust | Partial | Fast small payments | ~500 bytes/invoice |
|
||||
|
||||
### Fedimint (Federated Chaumian Ecash)
|
||||
|
||||
- Federation issues ecash tokens backed by Bitcoin in multisig
|
||||
- Tokens are bearer instruments — transferable offline
|
||||
- Double-spend prevention requires online redemption with the mint
|
||||
- Federation can be local (mesh-connected nodes)
|
||||
- Trust: threshold of guardians (e.g., 3-of-5) must not collude
|
||||
|
||||
### Cashu (Single-Mint Ecash)
|
||||
|
||||
- Simpler than Fedimint, single mint operator
|
||||
- Same bearer token model, transferable offline
|
||||
- Higher trust (single operator) but simpler deployment
|
||||
- Ideal for low-value, fast mesh transactions
|
||||
|
||||
### Multisig Escrow
|
||||
|
||||
For high-value off-grid trades:
|
||||
1. Pre-establish 2-of-3 multisig (buyer, seller, arbiter)
|
||||
2. Buyer funds before going off-grid
|
||||
3. Both parties sign via PSBT over mesh upon delivery
|
||||
4. Arbiter resolves disputes later
|
||||
|
||||
Post-Taproot: MuSig2 key path spend looks like single-sig on-chain (privacy).
|
||||
|
||||
### OpenTimestamps
|
||||
|
||||
Compact proofs (~few hundred bytes) that data existed at a specific time, anchored to Bitcoin blocks. Useful for unforgeable receipts of payment intent.
|
||||
|
||||
---
|
||||
|
||||
## 9. Cryptographic Protections
|
||||
|
||||
### Current Archipelago Implementation (Strong)
|
||||
|
||||
| Layer | Implementation | Assessment |
|
||||
|-------|---------------|------------|
|
||||
| Key agreement | X25519 ECDH (Ed25519 → X25519 conversion) | Production-grade |
|
||||
| Encryption | ChaCha20-Poly1305, random 12-byte nonce from OsRng | Correct choice for constrained environments |
|
||||
| Forward secrecy | Double Ratchet protocol | Per-message keys, post-compromise security |
|
||||
| Key derivation | HKDF-SHA256 | Standard |
|
||||
| Zeroization | `zeroize` crate on ratchet key material | Good |
|
||||
| Signing | Ed25519 via `TypedEnvelope::new_signed()` | Correct |
|
||||
| RNG | OsRng (CSPRNG) throughout | Correct — never `rand::thread_rng()` |
|
||||
|
||||
### Gap: Dead Man Switch Encryption
|
||||
|
||||
The `DeadManSwitch` alert includes GPS coordinates. If broadcast on channel 0, any mesh listener can read the location.
|
||||
|
||||
**Recommendation**: Encrypt dead man alerts to each emergency contact individually (using their public keys), not cleartext broadcast.
|
||||
|
||||
### Gap: Payment Intent Message Type
|
||||
|
||||
**Recommendation**: Create a signed "payment intent" envelope (destination, amount, timestamp, sender signature). Non-repudiable record for dispute resolution.
|
||||
|
||||
---
|
||||
|
||||
## 10. Real-World Precedents
|
||||
|
||||
### Blockstream Satellite
|
||||
- **Model**: Receive-only blockchain data from geostationary satellites
|
||||
- **Trust**: Minimal — receiving node validates proof-of-work
|
||||
- **Limitation**: Receive-only; needs separate return channel for broadcasting
|
||||
- **Relevance**: Complementary receive channel. Archipelago node could receive blocks from satellite (high bandwidth) and send transactions via mesh (low bandwidth).
|
||||
|
||||
### goTenna + Samourai Wallet (TxTenna)
|
||||
- **Model**: Signed transactions broadcast via goTenna mesh (UHF, ~1-2km)
|
||||
- **Trust**: Relay chain untrusted — can only forward or drop, not modify
|
||||
- **Security gap**: No confirmation feedback. No proof of broadcast.
|
||||
- **Relevance**: Archipelago's design is strictly superior — bidirectional relay, block headers, E2E encryption. TxTenna had none of these.
|
||||
|
||||
### Locha Mesh
|
||||
- **Model**: Custom LoRa hardware for Bitcoin + Lightning in Venezuela
|
||||
- **Innovation**: Combined Blockstream Satellite (blocks) + mesh (transactions)
|
||||
- **Status**: Development stalled (~2021)
|
||||
- **Relevance**: Hybrid satellite + mesh is the ideal model.
|
||||
|
||||
### Machankura (USSD Bitcoin in Africa)
|
||||
- **Model**: Fully custodial Lightning via USSD dial codes on feature phones
|
||||
- **Trust**: Complete — they hold all keys
|
||||
- **Relevance**: Demonstrates custodial models have product-market fit in connectivity-constrained environments. Archipelago is the self-sovereign middle ground.
|
||||
|
||||
---
|
||||
|
||||
## 11. Mesh-Specific Attack Vectors
|
||||
|
||||
| Attack | Severity | Detection | Mitigation |
|
||||
|--------|----------|-----------|------------|
|
||||
| Continuous radio jamming | High | RSSI spike, no valid packets | Frequency hopping, directional antennas, relocation |
|
||||
| Selective/reactive jamming | Critical | Hard — packets just "fail" | LoRa spread spectrum helps, but SDR can selectively jam |
|
||||
| Selective relay | High | Timeout on expected responses | Multiple relay paths, `RelayTracker` timeouts |
|
||||
| Timing analysis (mesh → mempool correlation) | High | — | Random broadcast delay jitter, steganography |
|
||||
| Physical proximity (LoRa = geographically nearby) | High | — | Higher SF for range, multi-hop, low TX power |
|
||||
| Sybil (fake nodes surrounding target) | High | Unknown peers appearing | Pre-configured trusted peer list (Ed25519/DID) |
|
||||
| Fake GPS/time attacks | Medium | Clock drift detection | Use block height not timestamps, cross-reference headers |
|
||||
|
||||
---
|
||||
|
||||
## 12. Summary: Archipelago Strengths and Gaps
|
||||
|
||||
### Already Strong
|
||||
|
||||
- E2E encryption (ChaCha20-Poly1305 + X25519)
|
||||
- Forward secrecy (Double Ratchet)
|
||||
- Signed message envelopes (Ed25519)
|
||||
- Transaction relay with response tracking (`RelayTracker`)
|
||||
- Block header relay (`BlockHeaderCache`)
|
||||
- Confirmation tracking (`TxConfirmation` type 12)
|
||||
- Dead man's switch with GPS
|
||||
- Steganographic encoding for plausible deniability
|
||||
- CSPRNG throughout (OsRng), sats as u64
|
||||
- Reed-Solomon chunking for large payloads over LoRa
|
||||
|
||||
### Priority Gaps
|
||||
|
||||
| # | Gap | Severity | Effort | Category |
|
||||
|---|-----|----------|--------|----------|
|
||||
| G1 | Validate block header chain continuity (check prev_hash linkage) | High | Low | Verification |
|
||||
| G2 | Validate proof-of-work on received headers | High | Medium | Verification |
|
||||
| G3 | Sign TxRelayResponse with relay Ed25519 key | Medium | Low | Authentication |
|
||||
| G4 | Encrypt dead man alerts to emergency contacts (not cleartext) | High | Medium | Privacy |
|
||||
| G5 | RBF detection — warn/reject RBF-signaled mesh txs | High | Low | Double-spend |
|
||||
| G6 | BOLT11 invoice expiry validation before relay payment | Medium | Low | Lightning |
|
||||
| G7 | Multi-relay header comparison (detect eclipse) | High | Medium | Verification |
|
||||
| G8 | Merkle proof relay for SPV transaction inclusion | High | Medium | Verification |
|
||||
| G9 | Timestamp sanity checking on received headers | Medium | Low | Verification |
|
||||
| G10 | Payment intent message type (signed, non-repudiable) | Low | Low | Authentication |
|
||||
| G11 | Random broadcast delay jitter (timing analysis resistance) | Medium | Low | Privacy |
|
||||
| G12 | Consider Cashu/ecash for small off-grid payments | Medium | High | Trust model |
|
||||
| G13 | Watch-only wallet architecture (no keys on node) | High | Medium | Key security |
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- [PSBT Security Best Practices — CertiK](https://www.certik.com/resources/blog/exploring-psbt-in-bitcoin-defi-security-best-practices)
|
||||
- [BIP174: Partially Signed Bitcoin Transactions](https://bips.dev/174/)
|
||||
- [Transaction Relay — Bitcoin Core Academy](https://bitcoincore.academy/transaction-relay.html)
|
||||
- [SPV — Electrum Documentation](https://electrum.readthedocs.io/en/latest/spv.html)
|
||||
- [BIP158: Compact Block Filters for Light Clients](https://bips.dev/158/)
|
||||
- [Eclipse Attacks on Bitcoin's P2P Network](https://eprint.iacr.org/2015/263.pdf)
|
||||
- [Replace by Fee — Bitcoin Wiki](https://en.bitcoin.it/wiki/Replace_by_fee)
|
||||
- [Irreversible Transactions — Bitcoin Wiki](https://en.bitcoin.it/wiki/Irreversible_Transactions)
|
||||
- [Time-Dilation Attacks on the Lightning Network](https://arxiv.org/pdf/2006.01418)
|
||||
- [Watchtowers — Lightning Builder's Guide](https://docs.lightning.engineering/the-lightning-network/payment-channels/watchtowers)
|
||||
- [TxTenna — GitHub](https://github.com/MuleTools/txTenna)
|
||||
- [Blockstream Satellite](https://blockstream.com/satellite/)
|
||||
- [Locha Mesh — GitHub](https://github.com/btcven/locha)
|
||||
- [Machankura FAQ](https://8333.mobi/faqs)
|
||||
- [Fedimint](https://fedimint.org/)
|
||||
- [Cashu — Open-source Ecash](https://cashu.space/)
|
||||
- [OpenTimestamps](https://opentimestamps.org/)
|
||||
- [LoRaWAN Physical Layer Attacks](https://pmc.ncbi.nlm.nih.gov/articles/PMC9100101/)
|
||||
@@ -1,82 +0,0 @@
|
||||
# Container Network Topology
|
||||
|
||||
## Networks
|
||||
|
||||
### archy-net (bridge)
|
||||
Shared network for Bitcoin ecosystem containers that need DNS-based service discovery.
|
||||
|
||||
| Container | Connects To | Why |
|
||||
|-----------|-------------|-----|
|
||||
| bitcoin-knots | - | Core Bitcoin node |
|
||||
| lnd | bitcoin-knots:8332 | Lightning requires Bitcoin RPC |
|
||||
| mempool-electrs | bitcoin-knots:8332 | Electrum indexer reads blocks |
|
||||
| mempool-api | mempool-electrs:50001, archy-mempool-db | API queries electrs + MySQL |
|
||||
| archy-mempool-web | mempool-api (upstream) | Frontend proxies to API |
|
||||
| archy-mempool-db | - | MySQL for mempool |
|
||||
| archy-btcpay-db | - | PostgreSQL for BTCPay + nbxplorer |
|
||||
| archy-nbxplorer | archy-btcpay-db:5432 | Block explorer indexes into Postgres |
|
||||
| btcpay-server | archy-btcpay-db:5432, archy-nbxplorer:32838 | Payment server |
|
||||
| fedimint | bitcoin-knots:8332 | Federated mint needs Bitcoin |
|
||||
| fedimint-gateway | bitcoin-knots:8332, lnd:10009 | Lightning gateway |
|
||||
|
||||
### immich-net (bridge)
|
||||
Isolated network for Immich photo management stack.
|
||||
|
||||
| Container | Connects To | Why |
|
||||
|-----------|-------------|-----|
|
||||
| immich_postgres | - | PostgreSQL for Immich |
|
||||
| immich_redis | - | Cache for Immich |
|
||||
| immich_server | immich_postgres, immich_redis | Main Immich app |
|
||||
|
||||
### penpot-net (bridge)
|
||||
Isolated network for Penpot design tool stack.
|
||||
|
||||
| Container | Connects To | Why |
|
||||
|-----------|-------------|-----|
|
||||
| penpot-postgres | - | PostgreSQL for Penpot |
|
||||
| penpot-valkey | - | Cache (Redis-compatible) |
|
||||
| penpot-backend | penpot-postgres, penpot-valkey | API server |
|
||||
| penpot-exporter | penpot-backend | PDF/SVG renderer |
|
||||
| penpot-frontend | penpot-backend | UI server |
|
||||
|
||||
### host network
|
||||
Containers that need direct host network access.
|
||||
|
||||
| Container | Why |
|
||||
|-----------|-----|
|
||||
| tailscale | VPN requires NET_ADMIN + host networking |
|
||||
| archy-electrs-ui | Static status page served on host port 50002 |
|
||||
|
||||
### podman (default bridge)
|
||||
Standalone containers with no inter-container dependencies.
|
||||
|
||||
| Container | Exposed Port |
|
||||
|-----------|-------------|
|
||||
| homeassistant | 8123 |
|
||||
| grafana | 3000 |
|
||||
| uptime-kuma | 3001 |
|
||||
| jellyfin | 8096 |
|
||||
| photoprism | 2342 |
|
||||
| dwn | 3100 |
|
||||
| ollama | 11434 |
|
||||
| vaultwarden | (dynamic) |
|
||||
| nextcloud | (dynamic) |
|
||||
| searxng | 8888 |
|
||||
| nginx-proxy-manager | 81 |
|
||||
| portainer | 9000 |
|
||||
| filebrowser | 8083 |
|
||||
| archy-bitcoin-ui | 8082 |
|
||||
| archy-lnd-ui | 8081 |
|
||||
| nostr-rs-relay | 8080 |
|
||||
|
||||
## Known Issues (2026-03-14)
|
||||
|
||||
1. **fedimint/fedimint-gateway on wrong network (.198)**: Should be on archy-net but are on default podman network. Fixed by reconnecting.
|
||||
2. **penpot incomplete (.198)**: penpot-frontend and penpot-backend containers missing. Only postgres, valkey, and exporter exist.
|
||||
3. **.228 unreachable**: Cannot audit .228 network topology — SSH/HTTP ports closed.
|
||||
|
||||
## Code References
|
||||
|
||||
- Network assignment: `core/archipelago/src/api/rpc/package.rs` (`needs_archy_net` match)
|
||||
- First-boot creation: `scripts/first-boot-containers.sh`
|
||||
- Health monitor exclusions: `core/archipelago/src/health_monitor.rs`
|
||||
@@ -1,44 +0,0 @@
|
||||
# Pkarr Crate Evaluation for did:dht Enhancement
|
||||
|
||||
## Summary
|
||||
|
||||
**Recommendation: Switch to pkarr when did:dht work resumes.**
|
||||
|
||||
Pkarr (v5.0.3, 550K downloads) provides a higher-level abstraction over Mainline DHT specifically for decentralized DNS-like records, which is exactly what did:dht needs.
|
||||
|
||||
## Current Implementation
|
||||
|
||||
Archy's `core/archipelago/src/network/did_dht.rs` (~211 lines):
|
||||
- Uses `mainline` crate directly for BEP-44 mutable items
|
||||
- Stores DID Documents as **JSON** (not DNS packets as spec requires)
|
||||
- Custom 1-hour in-memory TTL cache
|
||||
- No relay fallback
|
||||
|
||||
## Pkarr Advantages
|
||||
|
||||
| Feature | Archy Current | Pkarr |
|
||||
|---------|---------------|-------|
|
||||
| BEP-44 signing | Yes (via mainline) | Yes (integrated) |
|
||||
| DNS packet encoding | No (stores JSON) | Yes (RFC 1035 compliant) |
|
||||
| Relay fallback | No | Yes |
|
||||
| Spec compliance | Partial | Full (DNS packets) |
|
||||
| Caching | Custom 1-hour TTL | Pluggable with built-in cache |
|
||||
|
||||
### Key Difference: DNS Packet Encoding
|
||||
|
||||
The did:dht spec requires DID Documents to be stored as DNS packets (RFC 1035), not JSON. Our current implementation works for node-to-node resolution (both sides understand our JSON format), but is non-standard. Pkarr handles DNS packet encoding automatically via `SignedPacket` and `SignedPacketBuilder`.
|
||||
|
||||
### Relay Fallback
|
||||
|
||||
Pkarr includes relay server support for nodes behind restrictive NATs or firewalls. Our current implementation has no fallback when DHT connectivity fails.
|
||||
|
||||
## Migration Estimate
|
||||
|
||||
- Replace `did_dht.rs` with pkarr API calls
|
||||
- Add `pkarr = "5.0.3"` to Cargo.toml
|
||||
- Estimated: 1-2 hours implementation + testing
|
||||
- No breaking changes to RPC interface
|
||||
|
||||
## Decision
|
||||
|
||||
Keep current implementation for now (it works). Switch to pkarr when actively developing did:dht features, as it brings spec compliance and relay fallback with less custom code.
|
||||
@@ -1,26 +0,0 @@
|
||||
# Quality Baseline — 2026-03-11 (updated 2026-03-11)
|
||||
|
||||
Regression target: violation counts must only go down, never up.
|
||||
|
||||
## Metrics
|
||||
|
||||
| Metric | Count | Status |
|
||||
|--------|-------|--------|
|
||||
| Silent catches (business logic) | 0 | PASS (was 22) |
|
||||
| Console statements (non-dev-gated) | 0 | PASS (was 78) |
|
||||
| `any` types | 0 | PASS (was 15) |
|
||||
| TypeScript type-check | 0 errors | PASS |
|
||||
| Build | 0 warnings, 0 errors | PASS (2.6s) |
|
||||
| Tests | 515 passed, 0 failed | PASS (38 files) |
|
||||
| npm audit (runtime) | 0 vulnerabilities | PASS |
|
||||
| npm audit (dev-only) | 4 high (serialize-javascript) | ACCEPTED |
|
||||
|
||||
## History
|
||||
|
||||
- **2026-03-11**: Initial baseline — 22 silent catches, 78 console statements, 15 any types
|
||||
- **2026-03-10**: QUAL-02 fixed all silent catches (0 remaining)
|
||||
- **2026-03-10**: QUAL-03 wrapped all 37 non-dev-gated console statements with `import.meta.env.DEV`
|
||||
- **2026-03-10**: QUAL-04 replaced all 15 `any` types with proper TypeScript types
|
||||
- **2026-03-10**: QUAL-05 added pre/post-deploy health checks to deploy script
|
||||
- **2026-03-10**: QUAL-06 documented canary deploy process in `docs/canary-deploy.md`
|
||||
- **2026-03-11**: MAINT-03 quarterly sweep — 515 tests (was 41), zero regressions, npm deps updated
|
||||
@@ -1,264 +0,0 @@
|
||||
# Archy Refactoring Plan — Codebase Quality & Reliability
|
||||
|
||||
**Period**: March 2026 — March 2029
|
||||
**Scope**: Refactoring, bug fixes, library adoption, testing, performance only
|
||||
**Out of scope**: New features, design changes, UI changes
|
||||
|
||||
This plan exists alongside the feature roadmap. Refactoring work should be interleaved with feature sprints — not blocked by them.
|
||||
|
||||
---
|
||||
|
||||
## Year 1: Fix What's Broken, Adopt Proper Libraries (March 2026 — Feb 2027)
|
||||
|
||||
### Q1 2026: Critical Fixes & Database
|
||||
|
||||
#### 1. Enable SQLite via sqlx (HIGH — crash resilience)
|
||||
- **Problem**: All state is in-memory. Crashes lose everything except container snapshots. `sqlx` is commented out in `core/Cargo.toml`.
|
||||
- **Fix**: Uncomment sqlx, create migrations for: sessions, user data, peer state, metrics history, notification log. Keep the in-memory `DataModel` as a read cache backed by SQLite.
|
||||
- **Files**: `core/Cargo.toml`, `core/archipelago/src/state.rs`, new `core/archipelago/src/db/` module
|
||||
- **Why not a full Postgres**: Single-user appliance. SQLite is the right choice — zero config, file-based, embedded.
|
||||
|
||||
#### 2. Enforce RBAC (HIGH — security)
|
||||
- **Problem**: `UserRole::can_access()` is implemented in `auth.rs` but never called in `rpc/mod.rs`. Every authenticated user has full admin access.
|
||||
- **Fix**: Add role check in `RpcHandler::handle()` before dispatching to method handlers. Wire up role assignment during onboarding.
|
||||
- **Files**: `core/archipelago/src/api/rpc/mod.rs`, `core/archipelago/src/auth.rs`
|
||||
|
||||
#### 3. Fix session TTL clock bug (HIGH — correctness)
|
||||
- **Problem**: `session.rs` uses `Instant::now()` for TTL. `Instant` is monotonic but resets on system sleep/hibernate — common on the hardware Archy targets.
|
||||
- **Fix**: Use `SystemTime::now()` for session expiry timestamps, or better — use `tower-sessions` with the new SQLite backend.
|
||||
- **Files**: `core/archipelago/src/session.rs`
|
||||
|
||||
#### 4. Fix 10 failing frontend tests (MEDIUM)
|
||||
- **Problem**: `appLauncher.test.ts` and `settings.test.ts` are out of sync with current implementation.
|
||||
- **Fix**: Update test expectations to match current behavior. Don't mock what doesn't need mocking.
|
||||
- **Files**: `neode-ui/src/stores/__tests__/appLauncher.test.ts`, `neode-ui/src/views/__tests__/settings.test.ts`
|
||||
|
||||
#### 5. Remove dead dependencies (LOW)
|
||||
- **Problem**: `dockerode` in `package.json` is unused (container ops go through RPC).
|
||||
- **Fix**: `npm uninstall dockerode @types/dockerode`
|
||||
- **Files**: `neode-ui/package.json`
|
||||
|
||||
### Q2 2026: WebSocket Efficiency & Validation
|
||||
|
||||
#### 6. Add json-patch crate to backend (HIGH — performance)
|
||||
- **Problem**: Backend broadcasts the entire `DataModel` on every state change. Frontend already has `fast-json-patch` and supports incremental updates. Backend just doesn't generate patches.
|
||||
- **Fix**: Add `json-patch` crate. Before broadcasting, diff old vs new `DataModel`, send only the RFC 6902 patch. Fall back to full sync if patch is larger than full model.
|
||||
- **Files**: `core/Cargo.toml`, `core/archipelago/src/state.rs`
|
||||
|
||||
#### 7. Add form validation with zod (MEDIUM — maintainability)
|
||||
- **Problem**: Manual inline validation scattered across Login, Settings, Onboarding. As forms grow, this becomes a maintenance burden.
|
||||
- **Fix**: `npm install zod`. Create validation schemas in `src/types/schemas.ts`. Use in forms and RPC request builders. This is especially important for onboarding where bad input causes cryptographic key generation to fail silently.
|
||||
- **Files**: `neode-ui/package.json`, new `neode-ui/src/types/schemas.ts`, `Login.vue`, `Settings.vue`, onboarding views
|
||||
|
||||
#### 8. Move hardcoded app metadata to manifest files (MEDIUM — maintainability)
|
||||
- **Problem**: `docker_packages.rs` has hardcoded port mappings, titles, descriptions, and icon paths for ~20 apps. App manifests exist in `apps/` but aren't the source of truth.
|
||||
- **Fix**: Make `apps/{app-id}/manifest.yml` the single source of truth. Load metadata from manifests at startup. Remove hardcoded maps from Rust source.
|
||||
- **Files**: `core/archipelago/src/container/docker_packages.rs`, `apps/*/manifest.yml`
|
||||
|
||||
### Q3 2026: Error Handling & Testing
|
||||
|
||||
#### 9. Structured error types per backend module (MEDIUM — debuggability)
|
||||
- **Problem**: Everything uses `anyhow::Result`. When errors bubble up through RPC, you lose the module context. User-facing vs system errors aren't distinguished at the type level.
|
||||
- **Fix**: Create `thiserror` error enums for each major module: `AuthError`, `ContainerError`, `FederationError`, `IdentityError`. Map to appropriate HTTP status codes and user-friendly messages in the RPC layer.
|
||||
- **Files**: Each module in `core/archipelago/src/`
|
||||
|
||||
#### 10. Backend integration tests for RPC endpoints (HIGH — reliability)
|
||||
- **Problem**: 312 unit tests exist but zero integration tests for 80+ RPC endpoints. No test ever makes an actual HTTP request to the server.
|
||||
- **Fix**: Create integration test harness that spins up a real server instance (with test config, temp data dir). Test auth flow, container operations, identity, federation. Use `reqwest` as test client.
|
||||
- **Files**: New `core/archipelago/tests/` directory
|
||||
|
||||
#### 11. Frontend 404 route (LOW — UX)
|
||||
- **Problem**: No catch-all route. Invalid URLs silently show nothing.
|
||||
- **Fix**: Add `/:pathMatch(.*)*` catch-all route that shows a "Page not found" view with navigation back to dashboard.
|
||||
- **Files**: `neode-ui/src/router/index.ts`, new `neode-ui/src/views/NotFound.vue`
|
||||
|
||||
### Q4 2026: Clean Up Dead Code & CI
|
||||
|
||||
#### 12. Remove dead code and #[allow(dead_code)] (LOW — cleanliness)
|
||||
- **Problem**: `auth.rs` has `#[allow(dead_code)]` on `OnboardingState` fields and `AuthManager` methods. Either use them or remove them.
|
||||
- **Fix**: Audit all `#[allow(dead_code)]`, `#[allow(unused)]`. Remove genuinely unused code. Wire up code that should be used (like RBAC — covered in item 2).
|
||||
- **Files**: `core/archipelago/src/auth.rs` and others
|
||||
|
||||
#### 13. Set up CI pipeline (HIGH — process)
|
||||
- **Problem**: No automated testing on push/PR. All testing is manual or via deploy scripts.
|
||||
- **Fix**: GitHub Actions workflow: `cargo clippy`, `cargo test`, `npm run type-check`, `npm run test` on every push. Fail the build on warnings.
|
||||
- **Files**: New `.github/workflows/ci.yml`
|
||||
|
||||
#### 14. Cosign container image verification (MEDIUM — security)
|
||||
- **Problem**: `podman_client.rs:95` has a TODO for cosign signature verification. Container images are pulled without validation.
|
||||
- **Fix**: Implement cosign verification using the `sigstore` crate, or shell out to `cosign verify` as a first step. At minimum, verify image digests against a pinned manifest.
|
||||
- **Files**: `core/container/src/podman_client.rs`, `core/security/`
|
||||
|
||||
---
|
||||
|
||||
## Year 2: Robustness & Performance (March 2027 — Feb 2028)
|
||||
|
||||
### Q1 2027: Backend Architecture
|
||||
|
||||
#### 15. Migrate from hyper to axum (MEDIUM — maintainability)
|
||||
- **Problem**: Raw `hyper` 0.14 with manual routing in `handler.rs` (813 lines). Route matching, middleware, and error handling are all hand-rolled. `hyper` 0.14 is also end-of-life.
|
||||
- **Fix**: Migrate to `axum` (built on hyper 1.x, maintained by tokio team). Axum gives you: extractors, middleware stack, typed routing, tower integration. The RPC methods stay the same — only the HTTP layer changes.
|
||||
- **Files**: `core/archipelago/src/api/handler.rs`, `core/archipelago/src/api/mod.rs`, `core/Cargo.toml`
|
||||
- **Risk**: Medium. Do this on a branch, test thoroughly. The RPC logic doesn't change, just the HTTP glue.
|
||||
|
||||
#### 16. Replace custom rate limiter with tower middleware (LOW — correctness)
|
||||
- **Problem**: Hand-rolled in-memory rate limiter in `rpc/mod.rs`. Works for single instance but not distributed.
|
||||
- **Fix**: Use `tower::limit::RateLimitLayer` or `governor` crate. Cleaner, tested, configurable per-route.
|
||||
- **Files**: `core/archipelago/src/api/rpc/mod.rs`
|
||||
|
||||
#### 17. Persistent sessions in SQLite (MEDIUM — UX)
|
||||
- **Problem**: Sessions are in-memory. Server restart logs out all users.
|
||||
- **Fix**: With SQLite from item 1, store sessions in DB. Users stay logged in across restarts.
|
||||
- **Files**: `core/archipelago/src/session.rs`
|
||||
|
||||
### Q2 2027: Frontend Architecture
|
||||
|
||||
#### 18. Audit and optimize bundle size (MEDIUM — performance)
|
||||
- **Problem**: D3 is a large dependency (~240KB) used only for `LineChart.vue`. Target is <500KB gzipped.
|
||||
- **Fix**: Replace full `d3` import with only `d3-scale`, `d3-shape`, `d3-axis` (tree-shakeable). Or evaluate `unovis` or native Canvas for simple line charts. Measure before and after.
|
||||
- **Files**: `neode-ui/package.json`, `neode-ui/src/components/LineChart.vue`
|
||||
|
||||
#### 19. Vue Router route transitions (LOW — polish)
|
||||
- **Problem**: No transition animations between routes. Pages appear/disappear instantly.
|
||||
- **Fix**: Add `<RouterView v-slot>` with `<Transition>` wrapper. Simple fade (200ms) is enough — matches the existing glassmorphism feel without changing the design.
|
||||
- **Files**: `neode-ui/src/App.vue`
|
||||
- **Note**: This is not a design change — it's a missing standard Vue pattern.
|
||||
|
||||
#### 20. TypeScript strict cleanup (LOW — type safety)
|
||||
- **Problem**: WebSocket callback types in `app.ts:105` use inline object types instead of importing the `Update` type from `@/types/api`.
|
||||
- **Fix**: Audit all stores and components for inline type definitions that should reference shared types. Centralize in `src/types/`.
|
||||
- **Files**: `neode-ui/src/stores/app.ts`, `neode-ui/src/types/`
|
||||
|
||||
### Q3 2027: Testing & Observability
|
||||
|
||||
#### 21. Reach 60% test coverage (HIGH — reliability)
|
||||
- **Problem**: Frontend has ~505 passing tests but many views untested. Backend has zero RPC integration tests.
|
||||
- **Fix**: Prioritize testing for: auth flow, container lifecycle, WebSocket reconnection, federation handshake, backup/restore. Use coverage reports to find gaps.
|
||||
- **Target**: 60% line coverage frontend, 50% backend
|
||||
|
||||
#### 22. Add OpenTelemetry tracing (MEDIUM — observability)
|
||||
- **Problem**: `tracing` is used for logging but there's no distributed tracing or metrics export. When something goes wrong in production, you're reading log files.
|
||||
- **Fix**: Add `tracing-opentelemetry` and `opentelemetry-otlp`. Export traces to a local collector (Grafana is already a supported app). Instrument RPC handlers, container operations, federation sync.
|
||||
- **Files**: `core/Cargo.toml`, `core/archipelago/src/main.rs`
|
||||
|
||||
#### 23. Prometheus metrics export (MEDIUM — monitoring)
|
||||
- **Problem**: `MetricsStore` collects data but doesn't expose it. No way to monitor Archy health externally.
|
||||
- **Fix**: Add `/metrics` endpoint in Prometheus format using `prometheus` crate. Expose: RPC latency histograms, active sessions, container health, WebSocket connections, memory usage.
|
||||
- **Files**: `core/archipelago/src/api/handler.rs`, `core/archipelago/src/monitoring/`
|
||||
|
||||
### Q4 2027: Performance
|
||||
|
||||
#### 24. Optimize container scanner (MEDIUM — CPU)
|
||||
- **Problem**: `docker_packages.rs` scans all containers every 10 seconds with full JSON parsing. On a system with 30+ containers, this is unnecessary CPU churn.
|
||||
- **Fix**: Use Podman events API (`podman events --format json`) to watch for container state changes instead of polling. Fall back to polling every 60s as a safety net.
|
||||
- **Files**: `core/archipelago/src/container/docker_packages.rs`
|
||||
|
||||
#### 25. Lazy-load i18n locales (LOW — bundle size)
|
||||
- **Problem**: Spanish locale exists but loading behavior isn't optimized.
|
||||
- **Fix**: Use Vue i18n's lazy loading: load only the active locale on startup, fetch others on demand.
|
||||
- **Files**: `neode-ui/src/i18n.ts`
|
||||
|
||||
---
|
||||
|
||||
## Year 3: Production Hardening (March 2028 — March 2029)
|
||||
|
||||
### Q1 2028: Resilience
|
||||
|
||||
#### 26. Database migration system (MEDIUM — upgradability)
|
||||
- **Problem**: Once SQLite is in use, schema changes need managed migrations.
|
||||
- **Fix**: Use `sqlx` migrations (already supported). Create `core/archipelago/migrations/` directory. Run migrations on startup before serving requests.
|
||||
- **Files**: `core/archipelago/migrations/`, `core/archipelago/src/main.rs`
|
||||
|
||||
#### 27. Graceful degradation for container failures (MEDIUM — UX)
|
||||
- **Problem**: If Podman is down or unresponsive, the entire backend can hang on container operations.
|
||||
- **Fix**: Add timeouts to all Podman CLI calls (some already have them, make it universal). Show degraded state in UI rather than hanging. Container operations should never block the main RPC handler.
|
||||
- **Files**: `core/container/src/podman_client.rs`
|
||||
|
||||
#### 28. WebSocket backpressure handling (LOW — stability)
|
||||
- **Problem**: Broadcast channel capacity is 100. If a slow client can't keep up, messages are dropped silently.
|
||||
- **Fix**: Detect `RecvError::Lagged`, send full resync to that client. Log when clients fall behind consistently.
|
||||
- **Files**: `core/archipelago/src/api/handler.rs`
|
||||
|
||||
### Q2 2028: Security Hardening
|
||||
|
||||
#### 29. Full security audit pass (HIGH — security)
|
||||
- **Problem**: Various small issues accumulated: CORS could be tighter, rate limiting coverage is incomplete, error messages could leak internal paths.
|
||||
- **Fix**: Systematic pass through all 80+ RPC endpoints. Verify: input validation, authorization, rate limiting, error sanitization, path traversal prevention. Document findings.
|
||||
- **Files**: All RPC handlers
|
||||
|
||||
#### 30. Automated dependency security scanning (MEDIUM — supply chain)
|
||||
- **Problem**: No automated `cargo audit` or `npm audit` in CI.
|
||||
- **Fix**: Add to CI pipeline. Run weekly via cron. Block releases on known vulnerabilities (with severity threshold).
|
||||
- **Files**: `.github/workflows/ci.yml`, `scripts/audit-deps.sh`
|
||||
|
||||
### Q3 2028: Final Quality
|
||||
|
||||
#### 31. Reach 80% test coverage (HIGH — confidence)
|
||||
- **Target**: 80% line coverage across frontend and backend
|
||||
- **Focus**: Edge cases, error paths, recovery scenarios, concurrent operations
|
||||
|
||||
#### 32. Load testing (MEDIUM — capacity planning)
|
||||
- **Problem**: No load testing. Unknown how many concurrent users, containers, or WebSocket connections Archy can handle on target hardware.
|
||||
- **Fix**: Create load test suite with `k6` or `criterion` (Rust). Test: concurrent RPC calls, WebSocket connections, container operations. Document capacity limits per hardware tier.
|
||||
- **Files**: New `tests/load/` directory
|
||||
|
||||
#### 33. Code documentation pass (LOW — maintainability)
|
||||
- **Problem**: Module-level docs are sparse. New contributors (or future you) need to understand the architecture from code alone.
|
||||
- **Fix**: Add `//!` module docs to every Rust module. Add JSDoc to every Vue composable and store. Document the "why" of architectural decisions inline.
|
||||
- **Files**: All modules
|
||||
|
||||
### Q4 2028: Polish & Maintenance
|
||||
|
||||
#### 34. Dependency update cycle (ONGOING)
|
||||
- Monthly: `cargo update`, `npm update`, review changelogs
|
||||
- Quarterly: Major version upgrades (evaluate breaking changes)
|
||||
- Yearly: Evaluate if any custom code can be replaced by now-mature libraries
|
||||
|
||||
#### 35. Refactoring retrospective
|
||||
- Review this plan against actual state
|
||||
- Document what worked, what didn't
|
||||
- Create Year 4+ maintenance plan if needed
|
||||
|
||||
---
|
||||
|
||||
## Priority Summary
|
||||
|
||||
| Priority | Item | Impact |
|
||||
|----------|------|--------|
|
||||
| **Critical** | 1. SQLite database | Crash resilience |
|
||||
| **Critical** | 2. Enforce RBAC | Security |
|
||||
| **Critical** | 3. Fix session TTL bug | Correctness |
|
||||
| **Critical** | 6. JSON patch broadcasting | Performance |
|
||||
| **Critical** | 13. CI pipeline | Process |
|
||||
| **High** | 4. Fix failing tests | Reliability |
|
||||
| **High** | 10. Backend integration tests | Reliability |
|
||||
| **High** | 14. Cosign verification | Security |
|
||||
| **High** | 15. Migrate hyper → axum | Maintainability |
|
||||
| **High** | 21. 60% test coverage | Reliability |
|
||||
| **High** | 29. Security audit | Security |
|
||||
| **High** | 31. 80% test coverage | Confidence |
|
||||
| **Medium** | 7. Zod validation | Maintainability |
|
||||
| **Medium** | 8. Manifest-driven metadata | Maintainability |
|
||||
| **Medium** | 9. Structured error types | Debuggability |
|
||||
| **Medium** | 17. Persistent sessions | UX |
|
||||
| **Medium** | 18. D3 tree-shaking | Bundle size |
|
||||
| **Medium** | 22. OpenTelemetry | Observability |
|
||||
| **Medium** | 23. Prometheus metrics | Monitoring |
|
||||
| **Medium** | 24. Container scanner optimization | CPU |
|
||||
| **Low** | 5. Remove dockerode | Cleanliness |
|
||||
| **Low** | 11. 404 route | UX |
|
||||
| **Low** | 12. Dead code cleanup | Cleanliness |
|
||||
| **Low** | 16. Tower rate limiter | Correctness |
|
||||
| **Low** | 19. Route transitions | Polish |
|
||||
| **Low** | 20. TypeScript cleanup | Type safety |
|
||||
| **Low** | 25. Lazy i18n | Bundle size |
|
||||
|
||||
---
|
||||
|
||||
## Guiding Principles
|
||||
|
||||
1. **Use established crates and packages** — Don't reinvent what's solved. `sqlx`, `axum`, `tower`, `json-patch`, `zod`, `governor` exist for a reason.
|
||||
2. **Keep custom what's custom** — Federation, marketplace, DWN, the design system — these are genuinely yours. Don't force a library where none fits.
|
||||
3. **Test what matters** — Auth, container lifecycle, data persistence, WebSocket reliability. Not every utility function needs a test.
|
||||
4. **Refactor in place** — No rewrites. Migrate incrementally. Every commit should leave the codebase better than it found it.
|
||||
5. **No design changes** — The glassmorphism system, the layout, the UX flow — all stay exactly as they are. This plan only touches the internals.
|
||||
@@ -1,111 +0,0 @@
|
||||
# Archipelago Release Process
|
||||
|
||||
## Overview
|
||||
|
||||
Archipelago uses a JSON-based release manifest for the auto-update system. The backend checks `UPDATE_MANIFEST_URL` periodically (based on user's schedule setting) and compares versions.
|
||||
|
||||
## Manifest Format
|
||||
|
||||
The manifest is a single JSON file at:
|
||||
```
|
||||
https://raw.githubusercontent.com/archipelago-os/releases/main/manifest.json
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"release_date": "2026-04-01",
|
||||
"changelog": [
|
||||
"Added automatic update scheduling",
|
||||
"Improved backup encryption"
|
||||
],
|
||||
"components": [
|
||||
{
|
||||
"name": "archipelago",
|
||||
"current_version": "0.1.0",
|
||||
"new_version": "0.2.0",
|
||||
"download_url": "https://github.com/archipelago-os/releases/releases/download/v0.2.0/archipelago",
|
||||
"sha256": "abc123...",
|
||||
"size_bytes": 15000000
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Release Steps
|
||||
|
||||
### 1. Build Release Artifacts
|
||||
|
||||
On the build server (192.168.1.228):
|
||||
|
||||
```bash
|
||||
# Build backend (release mode)
|
||||
cd ~/archy/core
|
||||
cargo build --release -p archipelago
|
||||
|
||||
# Build frontend
|
||||
cd ~/archy/neode-ui
|
||||
npm run build
|
||||
```
|
||||
|
||||
### 2. Generate Manifest
|
||||
|
||||
```bash
|
||||
./scripts/create-release-manifest.sh \
|
||||
--version 0.2.0 \
|
||||
--date 2026-04-01
|
||||
```
|
||||
|
||||
This auto-detects the backend binary and frontend archive, computes SHA256 hashes, and writes `manifest.json`.
|
||||
|
||||
### 3. Upload Artifacts
|
||||
|
||||
Upload the backend binary and frontend archive to GitHub Releases:
|
||||
|
||||
```bash
|
||||
gh release create v0.2.0 \
|
||||
core/target/release/archipelago \
|
||||
/tmp/archipelago-frontend-0.2.0.tar.gz \
|
||||
--title "v0.2.0" \
|
||||
--notes "See CHANGELOG.md for details"
|
||||
```
|
||||
|
||||
### 4. Publish Manifest
|
||||
|
||||
Push the generated `manifest.json` to the releases repo:
|
||||
|
||||
```bash
|
||||
# In the archipelago-os/releases repo
|
||||
cp manifest.json .
|
||||
git add manifest.json
|
||||
git commit -m "Release v0.2.0"
|
||||
git push
|
||||
```
|
||||
|
||||
### 5. Tag the Source
|
||||
|
||||
```bash
|
||||
git tag v0.2.0
|
||||
git push --tags
|
||||
```
|
||||
|
||||
## Update Schedules
|
||||
|
||||
Users can configure how updates are handled:
|
||||
|
||||
| Schedule | Behavior |
|
||||
|----------|----------|
|
||||
| **Manual** | Never checks automatically. User must click "Check for Updates" |
|
||||
| **Daily Check** (default) | Checks once per day. Notifies user, who decides when to install |
|
||||
| **Auto-Apply** | Checks daily. Downloads and applies at 3 AM, restarts service |
|
||||
|
||||
## Rollback
|
||||
|
||||
If an update causes issues, users can rollback from the System Update page. The previous binary is backed up to `{data_dir}/update-backup/` before applying.
|
||||
|
||||
## Security
|
||||
|
||||
- All downloads are verified against SHA256 hashes in the manifest
|
||||
- The manifest itself is fetched over HTTPS from a known URL
|
||||
- Binary replacement requires service restart (handled by systemd)
|
||||
- Rollback is always available after an update
|
||||
@@ -1,121 +0,0 @@
|
||||
# Archipelago v1.1 Roadmap
|
||||
|
||||
**Planned Release**: Q2 2029 (June)
|
||||
**Based on**: v1.0.0 release, post-release monitoring, community feedback patterns
|
||||
|
||||
---
|
||||
|
||||
## Goals
|
||||
|
||||
1. Harden based on real-world usage patterns observed in v1.0
|
||||
2. Expand the app marketplace with community-requested apps
|
||||
3. Improve onboarding for non-technical users
|
||||
4. Lay groundwork for v2.0 multi-chain support
|
||||
|
||||
---
|
||||
|
||||
## Bug Fixes & Stability
|
||||
|
||||
### Critical (must-fix)
|
||||
|
||||
- **BF-01**: IBD progress reporting — Bitcoin initial block download shows stale percentage when node restarts mid-sync. Root cause: cached progress not invalidated on bitcoind restart.
|
||||
- **BF-02**: Container restart loop — Rare race condition where a container enters restart loop if Podman socket reconnects during health check. Add backoff and dead-letter after 5 consecutive failures.
|
||||
- **BF-03**: WebSocket reconnection on mobile — Safari drops WebSocket after background/foreground cycle. Implement heartbeat ping and auto-reconnect with exponential backoff.
|
||||
|
||||
### High Priority
|
||||
|
||||
- **BF-04**: Tor hidden service regeneration — If Tor restarts during onion key generation, the .onion address changes. Persist partial state and retry.
|
||||
- **BF-05**: ARM64 container pull timeouts — Some large images (Nextcloud, Home Assistant) timeout on Raspberry Pi 5 due to slow decompression. Increase timeout and show progress.
|
||||
- **BF-06**: Federation heartbeat false positives — Federated peer shows "offline" during brief network hiccups. Implement 3-strike detection before marking peer down.
|
||||
|
||||
### Quality of Life
|
||||
|
||||
- **BF-07**: Settings page scroll position lost on navigation back.
|
||||
- **BF-08**: App log viewer truncates long lines without horizontal scroll.
|
||||
- **BF-09**: Marketplace search doesn't match partial app names.
|
||||
|
||||
---
|
||||
|
||||
## New Features
|
||||
|
||||
### Marketplace Expansion
|
||||
|
||||
- **FEAT-01**: **Community app submission portal** — Web form for developers to submit app manifests for review. Includes automated security validation (read-only root, non-root user, pinned tags) and manual review queue.
|
||||
- **FEAT-02**: **App categories and tags** — Organize marketplace by: Bitcoin, Privacy, Productivity, Media, Developer Tools, Home Automation. Add tag-based filtering.
|
||||
- **FEAT-03**: **App ratings and reviews** — DID-authenticated reviews from verified node operators. Prevents spam (one review per DID per app). Synced via DWN.
|
||||
- **FEAT-04**: **5 new curated apps**:
|
||||
- Nostr relay (strfry) — self-hosted Nostr relay
|
||||
- Syncthing — peer-to-peer file sync
|
||||
- Gitea — self-hosted Git
|
||||
- Paperless-ngx — document management
|
||||
- Wireguard — lightweight VPN (alternative to Tailscale)
|
||||
|
||||
### User Experience
|
||||
|
||||
- **FEAT-05**: **Guided recovery wizard** — Step-by-step UI for common recovery scenarios: lost password (with backup codes), corrupted container, failed update rollback, disk space issues.
|
||||
- **FEAT-06**: **Resource usage dashboard** — Per-app CPU, memory, disk, and network usage with 24h/7d/30d charts. Built on existing performance monitoring infrastructure.
|
||||
- **FEAT-07**: **Notification center** — Aggregated notifications for: app updates available, disk space warnings, security alerts, federation peer status changes. Replaces individual alert toasts with a persistent notification drawer.
|
||||
- **FEAT-08**: **Quick actions** — Keyboard shortcuts (Ctrl+K command palette) for power users: search apps, restart services, view logs, open settings.
|
||||
|
||||
### Security Enhancements
|
||||
|
||||
- **FEAT-09**: **Hardware security key support** — WebAuthn/FIDO2 as alternative to TOTP for 2FA. Supports YubiKey, Trezor, Ledger.
|
||||
- **FEAT-10**: **Automated security updates** — Option to auto-apply security patches for OS packages and container base images. Requires user opt-in. Rolls back on failure.
|
||||
- **FEAT-11**: **Audit log** — Persistent log of all administrative actions (app installs, config changes, auth events). Viewable in UI. Exportable for compliance.
|
||||
|
||||
### Federation & Networking
|
||||
|
||||
- **FEAT-12**: **Federation dashboard** — Visual map of federated nodes with real-time health, latency, and sync status. Currently federation status is only visible per-peer.
|
||||
- **FEAT-13**: **Shared app deployment** — Deploy an app to a remote federated node from the local UI. Requires "Trusted" federation level.
|
||||
- **FEAT-14**: **DNS-over-HTTPS** — Built-in encrypted DNS resolution for all containers. Prevents ISP-level DNS snooping.
|
||||
|
||||
---
|
||||
|
||||
## Technical Debt
|
||||
|
||||
- **TECH-01**: Migrate remaining `anyhow::Error` returns to typed errors in RPC endpoints.
|
||||
- **TECH-02**: Consolidate duplicate Podman client code between `container/` and `archipelago/` crates.
|
||||
- **TECH-03**: Add integration tests for backup/restore cycle (currently only unit tested).
|
||||
- **TECH-04**: Reduce frontend bundle size — audit and tree-shake unused PrimeVue components.
|
||||
- **TECH-05**: Upgrade to Vite 8 when stable (expected Q1 2029).
|
||||
|
||||
---
|
||||
|
||||
## Infrastructure
|
||||
|
||||
- **INFRA-01**: Set up CI/CD pipeline (GitHub Actions or self-hosted Forgejo runner) for automated builds on every PR.
|
||||
- **INFRA-02**: Automated ISO testing — boot ISO in QEMU, run golden path E2E, report pass/fail.
|
||||
- **INFRA-03**: Community mirror infrastructure — allow community members to host ISO mirrors.
|
||||
|
||||
---
|
||||
|
||||
## Timeline
|
||||
|
||||
| Month | Focus | Key Deliverables |
|
||||
|-------|-------|-----------------|
|
||||
| March 2029 | Bug fixes | BF-01 through BF-09 resolved |
|
||||
| April 2029 | Marketplace | FEAT-01 through FEAT-04 (community portal, categories, 5 new apps) |
|
||||
| May 2029 | UX + Security | FEAT-05 through FEAT-11 (recovery wizard, dashboard, notifications, WebAuthn) |
|
||||
| June 2029 | Federation + Polish | FEAT-12 through FEAT-14, tech debt, release |
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- Zero critical bugs from v1.0 remaining
|
||||
- 25+ apps in marketplace (up from 20+)
|
||||
- Community app submission pipeline operational
|
||||
- Average onboarding completion rate >90% (measured via anonymized telemetry, opt-in only)
|
||||
- All v1.0 known limitations addressed or documented with workarounds
|
||||
|
||||
---
|
||||
|
||||
## v2.0 Preview
|
||||
|
||||
Features deferred to v2.0 (late 2029):
|
||||
- Multi-chain support (Monero, Ethereum L2s)
|
||||
- Advanced mesh networking (3+ node clusters)
|
||||
- Enterprise clustering with load balancing
|
||||
- Mobile companion app (iOS/Android)
|
||||
- AI-assisted node management (anomaly detection, auto-tuning)
|
||||
- Plugin system for third-party extensions
|
||||
@@ -1,131 +0,0 @@
|
||||
# Archipelago v2.0 Roadmap
|
||||
|
||||
**Planned Release**: Q4 2029 (December)
|
||||
**Codename**: Pangea
|
||||
**Based on**: v1.0 production experience, v1.1 community feedback, ecosystem trends
|
||||
|
||||
---
|
||||
|
||||
## Vision
|
||||
|
||||
Archipelago v2.0 transforms from a single-node Bitcoin OS into a **multi-chain, multi-node personal cloud platform** — while keeping the same self-sovereign, flash-and-run simplicity.
|
||||
|
||||
---
|
||||
|
||||
## Major Features
|
||||
|
||||
### 1. Multi-Chain Support
|
||||
|
||||
**Goal**: Run nodes for multiple cryptocurrency networks alongside Bitcoin.
|
||||
|
||||
- **Monero node** — Full Monero daemon with wallet RPC, Tor-only mode
|
||||
- **Ethereum L2 nodes** — Arbitrum, Optimism, Base light clients for DeFi access
|
||||
- **Liquid sidechain** — Blockstream Liquid for confidential Bitcoin transactions
|
||||
- **Cross-chain atomic swaps** — Built-in swap UI between BTC, XMR, and L2 tokens
|
||||
- **Unified wallet dashboard** — Single view of all chain balances and transactions
|
||||
|
||||
**Architecture**: Each chain runs in its own isolated container with chain-specific AppArmor profiles. No shared state between chains. Cross-chain operations use atomic swap protocols, never custodial bridges.
|
||||
|
||||
### 2. Multi-Node Mesh Networking
|
||||
|
||||
**Goal**: Scale beyond bilateral federation to N-node mesh clusters.
|
||||
|
||||
- **Mesh discovery** — Automatic peer discovery via Nostr relays and mDNS on LAN
|
||||
- **Consensus layer** — Raft-based consensus for shared state across mesh nodes
|
||||
- **Distributed storage** — Replicate critical data (DID documents, credentials, backups) across mesh
|
||||
- **Load balancing** — Route requests to the healthiest node in the mesh
|
||||
- **Split-brain protection** — Graceful degradation when mesh partitions
|
||||
- **Mesh dashboard** — Visual topology map with real-time health, latency, and sync status
|
||||
|
||||
**Architecture**: Each node remains independently operational. Mesh is opt-in and additive — removing a node from the mesh doesn't break it. State sync uses CRDTs for eventual consistency.
|
||||
|
||||
### 3. Enterprise Clustering
|
||||
|
||||
**Goal**: Support small business and family deployments (3-10 nodes).
|
||||
|
||||
- **Role-based access** — Admin, operator, viewer roles per node and per app
|
||||
- **Centralized management console** — Manage all cluster nodes from one UI
|
||||
- **Shared app instances** — Run a single Nextcloud/Vaultwarden instance shared across cluster
|
||||
- **Backup federation** — Automatic cross-node encrypted backups
|
||||
- **Usage analytics** — Aggregate resource usage and cost allocation across cluster
|
||||
|
||||
### 4. Mobile Companion App
|
||||
|
||||
**Goal**: Monitor and manage your node from your phone.
|
||||
|
||||
- **iOS and Android** — Native apps using React Native or Flutter
|
||||
- **Push notifications** — Node health alerts, app updates, federation events
|
||||
- **Remote access** — Secure tunnel via Tor or Tailscale (no port forwarding needed)
|
||||
- **Quick actions** — Start/stop apps, view logs, check Bitcoin sync status
|
||||
- **Biometric auth** — Face ID / fingerprint with hardware-backed key storage
|
||||
- **Offline mode** — Cache last-known state for viewing when disconnected
|
||||
|
||||
**Architecture**: Mobile app communicates via the existing JSON-RPC API over Tor hidden services or Tailscale tunnel. No cloud relay — direct node-to-phone connection.
|
||||
|
||||
### 5. AI-Assisted Node Management
|
||||
|
||||
**Goal**: Make node operation effortless for non-technical users.
|
||||
|
||||
- **Anomaly detection** — ML model trained on node metrics to detect unusual patterns (disk filling, memory leak, network anomaly) and alert before failure
|
||||
- **Auto-tuning** — Automatically adjust container resource limits based on observed usage patterns
|
||||
- **Natural language control** — "What's my Bitcoin sync status?" / "Restart Nextcloud" / "Show me my DID" via the existing AIUI chat interface
|
||||
- **Predictive maintenance** — Estimate time-to-full for disk, suggest pruning or archival
|
||||
- **Security assistant** — Flag suspicious container behavior, unusual network traffic patterns
|
||||
|
||||
**Architecture**: All AI processing runs locally on the node (Ollama). No data leaves the device. Models are small (1-3B parameters) optimized for system administration tasks.
|
||||
|
||||
### 6. Plugin System
|
||||
|
||||
**Goal**: Allow third-party extensions without full app manifests.
|
||||
|
||||
- **Plugin API** — JavaScript/TypeScript plugins that hook into node events (app start/stop, health change, federation events)
|
||||
- **UI extensions** — Plugins can add dashboard widgets, settings panels, and notification handlers
|
||||
- **Webhook integrations** — Forward node events to external services (Telegram, Discord, email)
|
||||
- **Plugin marketplace** — Curated plugins with the same security review process as apps
|
||||
- **Sandboxed execution** — Plugins run in Deno isolates with explicit permission grants
|
||||
|
||||
---
|
||||
|
||||
## Technical Debt Resolution
|
||||
|
||||
- **TECH-01**: Migrate secrets encryption to TPM-backed or password-derived keys (fixes CRIT-01 from security audits)
|
||||
- **TECH-02**: Per-install random credentials for all container services (fixes CRIT-02)
|
||||
- **TECH-03**: Tighten CSP — remove `unsafe-inline`/`unsafe-eval`, implement nonce-based script loading
|
||||
- **TECH-04**: Add HSTS and HTTP→HTTPS redirect
|
||||
- **TECH-05**: Trusted proxy validation for rate limiter IP extraction
|
||||
- **TECH-06**: Full migration to Tailwind CSS v4
|
||||
- **TECH-07**: Upgrade to Vue Router 5 and Vitest 4
|
||||
- **TECH-08**: Implement integration test suite for backup/restore cycle
|
||||
|
||||
---
|
||||
|
||||
## Timeline
|
||||
|
||||
| Quarter | Focus | Deliverables |
|
||||
|---------|-------|-------------|
|
||||
| Q1 2029 | v1.1 release + v2.0 planning | v1.1 shipped, architecture design docs for v2.0 |
|
||||
| Q2 2029 | Multi-chain + mesh foundations | Monero node, mesh discovery, CRDT state sync |
|
||||
| Q3 2029 | Mobile app + AI + plugins | Companion app MVP, anomaly detection, plugin API |
|
||||
| Q4 2029 | Enterprise + polish + release | Clustering, security debt, v2.0-beta |
|
||||
| Q1 2030 | v2.0 GA | Production release after 60-day soak test |
|
||||
|
||||
---
|
||||
|
||||
## Non-Goals for v2.0
|
||||
|
||||
- Mining support (high power, specialized hardware, not aligned with self-sovereign ethos)
|
||||
- Cloud hosting mode (Archipelago runs on hardware you control, period)
|
||||
- Cryptocurrency exchange features (not a trading platform)
|
||||
- Social media features beyond Nostr relay (stay focused on infrastructure)
|
||||
|
||||
---
|
||||
|
||||
## Success Metrics
|
||||
|
||||
- Support 3+ cryptocurrency networks
|
||||
- Mesh clusters of 3-10 nodes operational
|
||||
- Mobile app on both app stores
|
||||
- AI assistant handles 80% of routine maintenance questions
|
||||
- Zero critical security findings in annual audit
|
||||
- 50+ apps in marketplace
|
||||
- Community plugin ecosystem with 10+ published plugins
|
||||
@@ -1,137 +0,0 @@
|
||||
# Resource Budget for 10K Users
|
||||
|
||||
## Current Baseline (March 2026)
|
||||
|
||||
### Node .228 (Primary Dev Server)
|
||||
- **Hardware**: Intel i3-8100T (4 cores @ 3.10GHz), 16GB RAM, 1.8TB NVMe
|
||||
- **Containers**: 32 running
|
||||
- **RAM Usage**: ~14GB (8GB swap configured)
|
||||
- **CPU Load**: 3.5-5.5 (variable, depends on Bitcoin block processing)
|
||||
- **Disk Usage**: ~82% of 1.8TB
|
||||
|
||||
### Per-Container Resource Consumption (Measured)
|
||||
|
||||
| App | RAM (typical) | CPU (typical) | Disk |
|
||||
|-----|---------------|---------------|------|
|
||||
| Bitcoin Knots | 750MB | 0.5-2.0 cores (IBD) | 600GB+ (full chain) |
|
||||
| LND | 250MB | 0.1 cores | 5GB |
|
||||
| Electrs/Mempool-Electrs | 500MB | 0.5 cores (indexing) | 50GB+ |
|
||||
| Mempool API | 200MB | 0.1 cores | 1GB |
|
||||
| Mempool Web | 50MB | 0.01 cores | negligible |
|
||||
| BTCPay Server | 300MB | 0.1 cores | 2GB |
|
||||
| NBXplorer | 200MB | 0.1 cores | 1GB |
|
||||
| PostgreSQL (BTCPay) | 100MB | 0.1 cores | 2GB |
|
||||
| MariaDB (Mempool) | 150MB | 0.1 cores | 1GB |
|
||||
| Fedimint | 370MB | 0.1 cores | 1GB |
|
||||
| Fedimint Gateway | 100MB | 0.05 cores | negligible |
|
||||
| OnlyOffice | 760MB | 0.2 cores | 2GB |
|
||||
| Immich Server | 500MB | 0.5-1.0 cores (ML) | varies |
|
||||
| Immich Postgres | 100MB | 0.1 cores | varies |
|
||||
| Immich Redis | 30MB | 0.01 cores | negligible |
|
||||
| Nextcloud | 300MB | 0.2 cores | varies |
|
||||
| Jellyfin | 200MB | 0.2-2.0 cores (transcode) | varies |
|
||||
| Home Assistant | 230MB | 0.1 cores | 1GB |
|
||||
| Grafana | 100MB | 0.05 cores | 500MB |
|
||||
| Uptime Kuma | 80MB | 0.02 cores | 200MB |
|
||||
| Vaultwarden | 50MB | 0.01 cores | 100MB |
|
||||
| PhotoPrism | 300MB | 0.3 cores (ML) | varies |
|
||||
| SearXNG | 100MB | 0.05 cores | negligible |
|
||||
| DWN | 80MB | 0.02 cores | varies |
|
||||
| FileBrowser | 30MB | 0.01 cores | negligible |
|
||||
| Portainer | 50MB | 0.02 cores | 200MB |
|
||||
| Tailscale | 30MB | 0.01 cores | negligible |
|
||||
| AdGuard Home | 50MB | 0.02 cores | 200MB |
|
||||
| Nostr Relay | 50MB | 0.02 cores | varies |
|
||||
| Nginx Proxy Manager | 50MB | 0.01 cores | negligible |
|
||||
| Ollama | 500MB-4GB | 1-4 cores (inference) | 10GB+ (models) |
|
||||
|
||||
## App Tiers
|
||||
|
||||
### Core Tier (Required for Basic Bitcoin Node)
|
||||
- Bitcoin Knots (750MB)
|
||||
- LND (250MB)
|
||||
- Electrs (500MB)
|
||||
- Mempool Stack (400MB total)
|
||||
- BTCPay Stack (600MB total)
|
||||
- DWN (80MB)
|
||||
- FileBrowser (30MB)
|
||||
- **Total: ~2.6GB RAM, 2 CPU cores, 700GB disk**
|
||||
|
||||
### Recommended Tier (Enhanced Functionality)
|
||||
- Fedimint + Gateway (470MB)
|
||||
- Vaultwarden (50MB)
|
||||
- Uptime Kuma (80MB)
|
||||
- Grafana (100MB)
|
||||
- SearXNG (100MB)
|
||||
- Tailscale (30MB)
|
||||
- Portainer (50MB)
|
||||
- **Total: +880MB RAM, +0.5 cores**
|
||||
|
||||
### Optional Tier (User Choice)
|
||||
- Home Assistant (230MB)
|
||||
- Jellyfin (200MB)
|
||||
- Nextcloud (300MB)
|
||||
- OnlyOffice (760MB)
|
||||
- Immich Stack (630MB)
|
||||
- PhotoPrism (300MB)
|
||||
- AdGuard Home (50MB)
|
||||
- Ollama (500MB-4GB)
|
||||
- Nginx Proxy Manager (50MB)
|
||||
- **Total: +2-5GB RAM, +2-5 cores**
|
||||
|
||||
## Hardware Tier Recommendations
|
||||
|
||||
### Tier 1: Minimal (Core Only)
|
||||
- **CPU**: 2 cores (Intel Celeron/N100, ARM Cortex-A76)
|
||||
- **RAM**: 4GB
|
||||
- **Disk**: 1TB SSD (pruned Bitcoin node) or 2TB (full node)
|
||||
- **Apps**: Core tier only
|
||||
- **Cost**: ~$100-150 (Raspberry Pi 5, used mini-PC)
|
||||
|
||||
### Tier 2: Standard (Core + Recommended)
|
||||
- **CPU**: 4 cores (Intel i3/N200, Apple M1)
|
||||
- **RAM**: 8GB
|
||||
- **Disk**: 2TB NVMe
|
||||
- **Apps**: Core + Recommended tiers
|
||||
- **Cost**: ~$200-400 (Intel NUC, ThinkCentre Tiny)
|
||||
|
||||
### Tier 3: Power User (All Tiers)
|
||||
- **CPU**: 4-8 cores (Intel i5/i7, AMD Ryzen)
|
||||
- **RAM**: 16GB+
|
||||
- **Disk**: 2-4TB NVMe
|
||||
- **Apps**: Core + Recommended + Optional
|
||||
- **Cost**: ~$400-800 (used workstation, custom build)
|
||||
|
||||
### Tier 4: Heavy (All + AI/ML)
|
||||
- **CPU**: 8+ cores
|
||||
- **RAM**: 32GB+
|
||||
- **Disk**: 4TB+ NVMe
|
||||
- **GPU**: Optional (for Ollama, Immich ML)
|
||||
- **Apps**: Everything including Ollama with large models
|
||||
- **Cost**: ~$800+ (workstation with GPU)
|
||||
|
||||
## 10K User Projection
|
||||
|
||||
### Distribution Assumption
|
||||
- 60% Tier 1 (minimal Bitcoin node): 6,000 users
|
||||
- 25% Tier 2 (standard): 2,500 users
|
||||
- 12% Tier 3 (power user): 1,200 users
|
||||
- 3% Tier 4 (heavy): 300 users
|
||||
|
||||
### Network Impact
|
||||
- Federation sync: ~1KB per peer per 5-minute sync
|
||||
- DWN message replication: ~10KB per message sync
|
||||
- Tor hidden service overhead: negligible per user
|
||||
- Nostr relay federation: ~5KB per node announcement
|
||||
|
||||
### Scale Bottleneck Analysis
|
||||
1. **Disk**: Bitcoin blockchain grows ~100GB/year — need at minimum 1TB
|
||||
2. **Memory**: Core tier uses 2.6GB, leaves headroom on 4GB systems
|
||||
3. **CPU**: Bitcoin block processing and Electrs indexing are CPU-bound
|
||||
4. **Network**: Tor circuit establishment is the main latency bottleneck
|
||||
|
||||
## Recommendations
|
||||
1. Default fresh install to **Core tier only** (2.6GB RAM)
|
||||
2. Show tier badges in Marketplace
|
||||
3. Warn when system RAM < required for selected apps
|
||||
4. Auto-detect hardware and suggest appropriate tier
|
||||
@@ -1,230 +0,0 @@
|
||||
# Archipelago Security Audit Report
|
||||
|
||||
**Date**: 2026-03-05
|
||||
**Scope**: Cloud file upload, AIUI iframe, context broker, FileBrowser proxy, RPC endpoints
|
||||
**Auditor**: Automated code audit (Claude)
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
The Archipelago frontend is well-protected against **XSS** thanks to Vue's default template escaping. The **context broker** has correct origin validation. However, there are **path traversal risks** in the FileBrowser client, **CSRF gaps** in the RPC layer, and **token exposure** in download URLs. None are remotely exploitable without LAN access, but they should be addressed before public-facing deployment.
|
||||
|
||||
| Area | Risk | Severity |
|
||||
|------|------|----------|
|
||||
| XSS in file names | Protected by Vue escaping | **None** |
|
||||
| Context broker origin | Correctly validated | **None** |
|
||||
| AIUI iframe sandbox | Properly configured | **None** |
|
||||
| FileBrowser path traversal | Client-side paths not sanitized | **Medium** |
|
||||
| FileBrowser token in URLs | Token exposed in query strings | **Medium** |
|
||||
| CORS policy | `Access-Control-Allow-Origin: *` on some endpoints | **High** |
|
||||
| CSRF tokens | No CSRF mechanism exists | **High** |
|
||||
| Nginx security headers | Missing X-Frame-Options, CSP, nosniff | **Medium** |
|
||||
| X-Frame-Options stripping | All app proxies strip framing protection | **Medium** |
|
||||
|
||||
---
|
||||
|
||||
## 1. XSS in File Names — NO ISSUES FOUND
|
||||
|
||||
All file name rendering uses Vue's `{{ }}` text interpolation, which auto-escapes HTML:
|
||||
|
||||
- `CloudFolder.vue` — section names via `{{ section?.name }}`
|
||||
- `FileCard.vue:34` — `{{ item.name }}` (text interpolation)
|
||||
- `FileCardGrid.vue:65` — `{{ item.name }}` (text interpolation)
|
||||
- `CloudToolbar.vue` — breadcrumbs via `{{ crumb.name }}`
|
||||
- `Home.vue` — only numeric metrics displayed (storage bytes, folder counts)
|
||||
|
||||
No use of `v-html`, `innerHTML`, or other unsafe rendering anywhere in the cloud feature. A file named `<script>alert(1)</script>.txt` renders as literal escaped text.
|
||||
|
||||
Upload handling in `CloudFolder.vue:302-308` passes raw `File` objects (not strings), and `filebrowser-client.ts:74` properly URL-encodes file names with `encodeURIComponent()`.
|
||||
|
||||
**Verdict**: Safe. Vue's default escaping provides robust XSS protection.
|
||||
|
||||
---
|
||||
|
||||
## 2. AIUI Iframe & Context Broker — NO ISSUES FOUND
|
||||
|
||||
### Iframe Sandbox
|
||||
`Chat.vue:34` uses `sandbox="allow-scripts allow-same-origin allow-forms"` — the minimum permissions needed for AIUI to function. `allow-same-origin` is required for postMessage origin validation to work.
|
||||
|
||||
### Origin Validation
|
||||
`contextBroker.ts:27-34` correctly derives the allowed origin:
|
||||
```typescript
|
||||
const url = new URL(aiuiUrl, window.location.origin)
|
||||
this.allowedOrigin = url.origin
|
||||
```
|
||||
|
||||
`contextBroker.ts:65` validates every incoming message:
|
||||
```typescript
|
||||
if (event.origin !== this.allowedOrigin) return
|
||||
```
|
||||
|
||||
`contextBroker.ts:475` sends responses with explicit target origin:
|
||||
```typescript
|
||||
this.iframe.value.contentWindow.postMessage(msg, this.allowedOrigin)
|
||||
```
|
||||
|
||||
For same-origin AIUI (production: `/aiui/`), `this.allowedOrigin` equals `window.location.origin`, which is correct.
|
||||
|
||||
`Chat.vue:98-108` also validates origin for the `ready` message independently.
|
||||
|
||||
**Verdict**: Properly secured. Double origin validation, explicit target origins on postMessage.
|
||||
|
||||
---
|
||||
|
||||
## 3. FileBrowser Path Traversal — MEDIUM RISK
|
||||
|
||||
### Finding: Paths not URL-encoded in API calls
|
||||
`filebrowser-client.ts` constructs API URLs with raw path strings:
|
||||
|
||||
- Line 55: `fetch(\`${this.baseUrl}/api/resources${safePath}\`)`
|
||||
- Line 69: `return \`${this.baseUrl}/api/raw${safePath}?auth=${this.token}\``
|
||||
- Line 100: `fetch(\`${this.baseUrl}/api/resources${safePath}\`)`
|
||||
- Line 127: `fetch(\`${this.baseUrl}/api/resources${safePath}\`)`
|
||||
|
||||
The `safePath` helper only prepends `/` if missing — it does NOT reject `..` sequences or canonicalize paths.
|
||||
|
||||
### Mitigating Factors
|
||||
1. **FileBrowser runs in a container** with volume mount `/var/lib/archipelago/filebrowser:/srv` — the daemon itself enforces path boundaries
|
||||
2. **Nginx proxies** to `127.0.0.1:8083` — not externally accessible
|
||||
3. **Paths come from FileBrowser API responses** (server-generated), not direct user input in most cases
|
||||
4. **LAN-only access** — attacker needs network access
|
||||
|
||||
### Recommendations
|
||||
1. Add path validation in `filebrowser-client.ts`:
|
||||
```typescript
|
||||
function sanitizePath(path: string): string {
|
||||
const normalized = path.split('/').filter(p => p !== '..' && p !== '.').join('/')
|
||||
return normalized.startsWith('/') ? normalized : `/${normalized}`
|
||||
}
|
||||
```
|
||||
2. URL-encode path components in download URLs
|
||||
3. Verify FileBrowser container uses `--read-only` filesystem
|
||||
|
||||
---
|
||||
|
||||
## 4. FileBrowser Token Exposure — MEDIUM RISK
|
||||
|
||||
### Finding: JWT in query parameters
|
||||
`filebrowser-client.ts:69` exposes the auth token in download URLs:
|
||||
```typescript
|
||||
return `${this.baseUrl}/api/raw${safePath}?auth=${this.token}`
|
||||
```
|
||||
|
||||
This token appears in:
|
||||
- Browser history
|
||||
- Nginx access logs
|
||||
- HTTP Referer headers
|
||||
- DOM (in `<a href="...">` elements)
|
||||
|
||||
### Recommendation
|
||||
Use the `X-Auth` header (already used for other requests at line 49) instead of query parameters. For downloads, use a short-lived download token or proxy through a backend endpoint.
|
||||
|
||||
---
|
||||
|
||||
## 5. CORS Policy — HIGH RISK (LAN-scoped)
|
||||
|
||||
### Finding: Wildcard CORS on multiple endpoints
|
||||
`core/archipelago/src/api/handler.rs:15` defines `const CORS_ANY: &str = "*"` and applies it to:
|
||||
- `/api/container/logs` (lines 108, 118)
|
||||
- `/archipelago/node-message` (line 142)
|
||||
- `/electrs-status` (line 153)
|
||||
- `/proxy/lnd/` (lines 173, 183)
|
||||
|
||||
The main `/rpc/v1` endpoint does NOT set CORS headers (more restrictive by default).
|
||||
|
||||
### Mitigating Factors
|
||||
1. Server is LAN-only (no public internet exposure)
|
||||
2. Main RPC endpoint is not affected
|
||||
3. `credentials: 'include'` with `Access-Control-Allow-Origin: *` is actually blocked by browsers (CORS spec requires specific origin when credentials are used)
|
||||
|
||||
### Recommendations
|
||||
1. Replace `*` with the specific Archipelago origin
|
||||
2. Add `Access-Control-Allow-Credentials: true` only where needed
|
||||
3. Handle OPTIONS preflight requests properly
|
||||
|
||||
---
|
||||
|
||||
## 6. CSRF Protection — HIGH RISK (LAN-scoped)
|
||||
|
||||
### Finding: No CSRF mechanism
|
||||
- No CSRF token generation or validation
|
||||
- No `X-Requested-With` custom header requirement
|
||||
- No `SameSite` cookie attribute
|
||||
- No `Origin` header validation in the RPC handler
|
||||
|
||||
### Mitigating Factors
|
||||
1. **JSON-RPC requires `Content-Type: application/json`** — this is NOT a "simple" CORS content type, so browsers send preflight OPTIONS requests for cross-origin POSTs. Since the backend returns 404 for OPTIONS, cross-origin JSON-RPC calls are effectively blocked.
|
||||
2. **LAN-only access** — attacker needs to be on the same network
|
||||
3. **Session cookies** — authentication appears to use session cookies from `/rpc/v1`, but an attacker on the LAN could craft a same-origin request
|
||||
|
||||
### Recommendations
|
||||
1. Add `X-Requested-With: XMLHttpRequest` header in `rpc-client.ts` and validate it server-side
|
||||
2. Implement synchronizer token pattern for state-changing operations
|
||||
3. Validate `Origin` header in the Rust handler
|
||||
|
||||
---
|
||||
|
||||
## 7. Nginx Security Headers — MEDIUM RISK
|
||||
|
||||
### Finding: Missing standard security headers
|
||||
The nginx config lacks:
|
||||
- `X-Content-Type-Options: nosniff`
|
||||
- `Referrer-Policy: strict-origin-when-cross-origin`
|
||||
- `Content-Security-Policy` for the main UI
|
||||
|
||||
### Finding: X-Frame-Options stripped from all app proxies
|
||||
Every app proxy block includes:
|
||||
```nginx
|
||||
proxy_hide_header X-Frame-Options;
|
||||
proxy_hide_header Content-Security-Policy;
|
||||
```
|
||||
|
||||
This is intentional (apps are embedded in iframes), but increases clickjacking surface.
|
||||
|
||||
### Recommendations
|
||||
1. Add security headers to the main location blocks:
|
||||
```nginx
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
```
|
||||
2. Add `Content-Security-Policy` with `frame-ancestors 'self'` for the main UI
|
||||
3. For app proxies, replace stripped headers with `X-Frame-Options: SAMEORIGIN` to allow Archipelago iframing but block external sites
|
||||
|
||||
---
|
||||
|
||||
## Priority Action Items
|
||||
|
||||
| Priority | Action | Effort |
|
||||
|----------|--------|--------|
|
||||
| 1 | Add `X-Requested-With` header to RPC client + validate server-side | Low |
|
||||
| 2 | Add nginx security headers (nosniff, referrer-policy) | Low |
|
||||
| 3 | Replace `X-Frame-Options` stripping with `SAMEORIGIN` override | Low |
|
||||
| 4 | Sanitize FileBrowser paths client-side | Low |
|
||||
| 5 | Move FileBrowser download auth from URL to header | Medium |
|
||||
| 6 | Replace wildcard CORS with specific origins | Medium |
|
||||
| 7 | Implement CSRF synchronizer tokens | High |
|
||||
| 8 | Add Content-Security-Policy header | High |
|
||||
|
||||
---
|
||||
|
||||
## Files Audited
|
||||
|
||||
- `neode-ui/src/views/Chat.vue`
|
||||
- `neode-ui/src/views/CloudFolder.vue`
|
||||
- `neode-ui/src/views/Home.vue`
|
||||
- `neode-ui/src/services/contextBroker.ts`
|
||||
- `neode-ui/src/api/filebrowser-client.ts`
|
||||
- `neode-ui/src/api/rpc-client.ts`
|
||||
- `neode-ui/src/api/container-client.ts`
|
||||
- `neode-ui/src/stores/cloud.ts`
|
||||
- `neode-ui/src/stores/aiPermissions.ts`
|
||||
- `neode-ui/src/types/aiui-protocol.ts`
|
||||
- `neode-ui/src/components/cloud/FileCard.vue`
|
||||
- `neode-ui/src/components/cloud/FileCardGrid.vue`
|
||||
- `neode-ui/src/components/cloud/CloudToolbar.vue`
|
||||
- `core/archipelago/src/api/handler.rs`
|
||||
- `core/archipelago/src/api/rpc/mod.rs`
|
||||
- `core/archipelago/src/api/rpc/auth.rs`
|
||||
- `core/archipelago/src/api/rpc/package.rs`
|
||||
- `image-recipe/configs/nginx-archipelago.conf`
|
||||
@@ -1,50 +0,0 @@
|
||||
# Monthly Security Audit — 2026-03-11
|
||||
|
||||
## Scope
|
||||
MAINT-02 monthly scan. Full audit of `core/security/`, `core/archipelago/src/api/rpc/`, nginx config, and frontend.
|
||||
|
||||
## Findings Summary
|
||||
|
||||
| Severity | Count | Fixed | Deferred |
|
||||
|----------|-------|-------|----------|
|
||||
| Critical | 2 | 0 | 2 (known, architectural) |
|
||||
| High | 5 | 0 | 5 (known, requires design) |
|
||||
| Medium | 7 | 2 | 5 |
|
||||
| Low | 6 | 0 | 6 |
|
||||
| Info | 4 | 0 | 4 |
|
||||
|
||||
## Fixes Applied This Cycle
|
||||
|
||||
### MED-03: Shell injection in bitcoin.conf generation — FIXED
|
||||
`core/archipelago/src/api/rpc/package.rs` — Replaced `sh -c echo` shell command with `tokio::fs::write()` to eliminate shell injection surface.
|
||||
|
||||
### MED-07: No body size limit on /rpc/ endpoint — FIXED
|
||||
`image-recipe/configs/nginx-archipelago.conf` — Added `client_max_body_size 1m` to `/rpc/` location in both HTTP and HTTPS server blocks.
|
||||
|
||||
## Known Issues (Deferred)
|
||||
|
||||
### CRIT-01: Deterministic encryption key
|
||||
Secrets encryption key derived from data directory path. Requires architectural redesign (Argon2 from user password or TPM-backed key). Tracked for v1.1.
|
||||
|
||||
### CRIT-02: Hardcoded Bitcoin RPC password
|
||||
`archipelago123` shared across all deployments. Requires per-install random password generation and secrets manager integration. Tracked for v1.1.
|
||||
|
||||
### HIGH-01 through HIGH-05
|
||||
Known from FINAL-02 audit (2026-03-10). CSP hardening, HSTS, IP spoofing for rate limiting, Bitcoin RPC binding — all tracked for v1.1.
|
||||
|
||||
## Dependency CVE Check
|
||||
|
||||
### npm
|
||||
- `serialize-javascript` ≤7.0.2 (GHSA-5c6j-r48x-rmvq): RCE via RegExp.flags — dev-only, no runtime impact
|
||||
- `rollup` path traversal (GHSA-mw96-cpmx-2vgc): dev-only build tool
|
||||
- No new runtime dependency CVEs
|
||||
|
||||
### Cargo
|
||||
- No new advisories affecting current pinned versions (checked cargo-audit equivalent)
|
||||
|
||||
### Podman/Debian
|
||||
- No critical Debian 12 security advisories for Podman 4.x since last scan
|
||||
- Container base images using pinned versions (no `:latest` in production manifests)
|
||||
|
||||
## Next Cycle
|
||||
Due: 2026-04-11. Focus areas: CRIT-01 key derivation redesign, CSP tightening.
|
||||
@@ -1,41 +0,0 @@
|
||||
# Security Audit Preparation
|
||||
|
||||
## Scope for External Audit
|
||||
|
||||
### Priority 1: Critical Path
|
||||
- Authentication (bcrypt, session management, CSRF, rate limiting)
|
||||
- Cryptography (Ed25519 signing, ChaCha20-Poly1305 backup encryption, Argon2 KDF)
|
||||
- Container isolation (Podman security, cap-drop, no-new-privileges)
|
||||
- Network security (Tor integration, federation over hidden services)
|
||||
- Input validation (RPC endpoints, path traversal prevention)
|
||||
|
||||
### Priority 2: Data Security
|
||||
- Secrets management (identity keys, wallet credentials)
|
||||
- Backup encryption (key derivation, storage format)
|
||||
- DWN message integrity (peer sync, deduplication)
|
||||
- Verifiable Credentials (W3C VC issuance, verification)
|
||||
|
||||
### Priority 3: Infrastructure
|
||||
- Nginx configuration (headers, proxy settings, CSP)
|
||||
- Systemd service hardening (watchdog, capabilities)
|
||||
- UFW firewall rules (Podman subnet access)
|
||||
- Log sanitization (no secrets in logs)
|
||||
|
||||
## Completed Internal Audits
|
||||
- SEC-01: RPC endpoint input validation audit (100+ endpoints)
|
||||
- SEC-02: Rate limiting on federation endpoints
|
||||
- SEC-03: CSRF validation on all state-changing endpoints
|
||||
- SEC-04: Container security profiles (cap-drop ALL, no-new-privileges)
|
||||
- SEC-05: Log rotation configured
|
||||
- SEC-06: Security headers verified (X-Frame-Options, CSP, etc.)
|
||||
|
||||
## Recommended Audit Firms
|
||||
- Trail of Bits (Rust + cryptography expertise)
|
||||
- NCC Group (infrastructure + application security)
|
||||
- Cure53 (web application + browser security)
|
||||
- Doyensec (Rust + WebSocket + API security)
|
||||
|
||||
## Budget Estimate
|
||||
- Comprehensive audit (2-4 weeks): $50,000 - $150,000
|
||||
- Focused crypto + auth audit (1-2 weeks): $25,000 - $60,000
|
||||
- Penetration test only (1 week): $15,000 - $30,000
|
||||
@@ -1,28 +0,0 @@
|
||||
# StartOS Dependency Audit — 2026-03-10
|
||||
|
||||
## Summary
|
||||
|
||||
**`core/archipelago/` has ZERO dependencies on `core/startos/`.** The startos directory is dead code — not compiled, not imported, not referenced by any active module.
|
||||
|
||||
## Findings
|
||||
|
||||
### Workspace
|
||||
- `core/Cargo.toml` workspace members: `archipelago`, `container`, `parmanode`, `performance`, `security`
|
||||
- `startos` is NOT a workspace member
|
||||
|
||||
### Dependencies
|
||||
- `core/archipelago/Cargo.toml`: no startos dependency
|
||||
- `core/container/Cargo.toml`: no startos dependency
|
||||
- All other core modules: no startos dependency
|
||||
- `cargo tree -p archipelago`: startos does not appear
|
||||
|
||||
### Source Code
|
||||
- Zero `use startos::*` imports in `core/archipelago/src/`
|
||||
- Zero references to startos in any active Rust code
|
||||
- No git submodule reference
|
||||
|
||||
### Status
|
||||
`core/startos/` contains a StartOS fork (`start-os` v0.3.5-rev.1) that is present on disk but completely inert. It can be safely removed.
|
||||
|
||||
## Action
|
||||
Remove `core/startos/` directory. No migration needed — there are no dependencies to migrate.
|
||||
@@ -1,421 +0,0 @@
|
||||
# Archipelago User Guide
|
||||
|
||||
Welcome to Archipelago — your personal server for a sovereign digital life. This guide walks you through everything from first boot to daily usage.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [First-Time Setup](#first-time-setup)
|
||||
2. [Onboarding Walkthrough](#onboarding-walkthrough)
|
||||
3. [Dashboard Overview](#dashboard-overview)
|
||||
4. [Installing Apps](#installing-apps)
|
||||
5. [Managing Apps](#managing-apps)
|
||||
6. [Bitcoin Node](#bitcoin-node)
|
||||
7. [Lightning Network (LND)](#lightning-network-lnd)
|
||||
8. [Cloud Storage](#cloud-storage)
|
||||
9. [Identity & Web5](#identity--web5)
|
||||
10. [Settings](#settings)
|
||||
11. [Backup & Restore](#backup--restore)
|
||||
12. [Remote Access](#remote-access)
|
||||
13. [Troubleshooting](#troubleshooting)
|
||||
|
||||
---
|
||||
|
||||
## First-Time Setup
|
||||
|
||||
### What You Need
|
||||
|
||||
- A dedicated computer (Intel/AMD x86_64 or ARM64)
|
||||
- 16 GB RAM minimum (32 GB recommended)
|
||||
- 500 GB+ SSD/NVMe storage
|
||||
- Ethernet connection to your home router
|
||||
- A USB drive (8 GB+) for the installer
|
||||
|
||||
### Flashing the Installer
|
||||
|
||||
1. Download the latest Archipelago ISO from the releases page
|
||||
2. Flash the ISO to a USB drive using [balenaEtcher](https://etcher.balena.io/) or `dd`
|
||||
3. Insert the USB into your target machine and boot from it
|
||||
4. The auto-installer partitions your disk, installs Debian 12, and sets up all Archipelago services
|
||||
5. When complete, the installer prompts you to remove the USB and reboot
|
||||
|
||||
### Finding Your Server
|
||||
|
||||
After reboot, Archipelago starts automatically. Find your server on your local network:
|
||||
|
||||
- **Default address**: `http://archipelago.local` (if mDNS works on your network)
|
||||
- **IP address**: Check your router's DHCP client list for the new device
|
||||
- **Direct**: Connect a monitor — the IP is displayed on the console login screen
|
||||
|
||||
Open your browser and navigate to the server address. You should see the Archipelago welcome screen.
|
||||
|
||||
---
|
||||
|
||||
## Onboarding Walkthrough
|
||||
|
||||
On first visit, Archipelago guides you through a 7-step onboarding process.
|
||||
|
||||
### Step 1: Welcome Screen
|
||||
|
||||
A cinematic intro video plays. Click **"Begin"** to start setup.
|
||||
|
||||
### Step 2: Create Admin Password
|
||||
|
||||
Set your admin password. This password protects:
|
||||
- The web interface login
|
||||
- SSH access to your server
|
||||
- Encrypted secrets on disk
|
||||
|
||||
**Requirements**: Minimum 8 characters. Choose something strong — this protects your entire server.
|
||||
|
||||
### Step 3: Choose Your Path
|
||||
|
||||
Select the sovereign use case that interests you most:
|
||||
- **Self Sovereignty** — Own your data, identity, and digital life
|
||||
- **Community Commerce** — Peer-to-peer commerce on Bitcoin
|
||||
- **Sovereign Projects** — Collaborative workspace without third parties
|
||||
- **Data Transmitter** — Run relays and network services
|
||||
- **Hoster** — Monetize hosting capacity
|
||||
- **Sovereign AI** — Run AI models locally, no surveillance
|
||||
|
||||
This is informational — all features are available regardless of your choice.
|
||||
|
||||
### Step 4: Setup Type
|
||||
|
||||
Choose **Fresh Start** for a new installation. (Restore from backup and connect to existing server are coming in future releases.)
|
||||
|
||||
### Step 5: Generate Your Identity (DID)
|
||||
|
||||
Archipelago generates a Decentralized Identifier (DID) for you. This is your sovereign digital identity — it proves you are you without any company in the middle.
|
||||
|
||||
- Your DID is displayed on screen — copy it if you like
|
||||
- This identity is stored locally on your server
|
||||
- It's used for passwordless authentication and Web5 features
|
||||
|
||||
Wait for the server health check to complete (1–3 minutes on first boot as services start up).
|
||||
|
||||
### Step 6: Name Your Identity
|
||||
|
||||
Give your identity a name (e.g., "Personal", "Business") and choose its purpose. This is optional and can be changed later.
|
||||
|
||||
### Step 7: Create a Backup
|
||||
|
||||
**Important**: Set a passphrase and download your identity backup file (`archipelago-did-backup.json`). Store this file securely — it's the only way to recover your identity if your server is lost.
|
||||
|
||||
### Done
|
||||
|
||||
After completing onboarding, you're taken to the Dashboard.
|
||||
|
||||
---
|
||||
|
||||
## Dashboard Overview
|
||||
|
||||
The Dashboard is your home screen with two tabs:
|
||||
|
||||
### Dashboard Tab
|
||||
|
||||
Quick overview cards showing:
|
||||
|
||||
- **My Apps** — How many apps are installed and running. Quick links to browse the store or manage apps.
|
||||
- **Cloud** — Storage usage and folder count. Access your files.
|
||||
- **Server** — Connection status and system health.
|
||||
- **Web5** — DID status, wallet connection, and Nostr relay count.
|
||||
|
||||
### Setup Tab
|
||||
|
||||
Goal-based guided setup cards for first-time users. These walk you through installing and configuring recommended apps step by step.
|
||||
|
||||
---
|
||||
|
||||
## Installing Apps
|
||||
|
||||
### From the App Store
|
||||
|
||||
1. Navigate to **App Store** from the sidebar (desktop) or bottom bar (mobile)
|
||||
2. Browse by category (Finance, Storage, Communication, Network, etc.) or search by name
|
||||
3. Click an app to see its details
|
||||
4. Click **Install**
|
||||
|
||||
### Installation Progress
|
||||
|
||||
When you install an app, a progress banner appears at the top of the App Store showing:
|
||||
- Download progress (percentage and MB downloaded)
|
||||
- Current status (Downloading, Installing, Starting)
|
||||
|
||||
Most apps take 1–5 minutes to install depending on image size and network speed.
|
||||
|
||||
### Dependency Resolution
|
||||
|
||||
Some apps require others to be running first:
|
||||
- **Electrs** requires Bitcoin Knots
|
||||
- **LND** requires Bitcoin Knots
|
||||
- **Mempool** requires Bitcoin Knots + Electrs
|
||||
- **BTCPay Server** requires Bitcoin Knots
|
||||
|
||||
The App Store shows dependency requirements and offers to install them automatically.
|
||||
|
||||
### Available Apps
|
||||
|
||||
| App | Category | Description |
|
||||
|-----|----------|-------------|
|
||||
| Bitcoin Knots | Finance | Full Bitcoin node with enhanced features |
|
||||
| Electrs | Finance | Electrum server for wallet connectivity |
|
||||
| LND | Finance | Lightning Network node for instant payments |
|
||||
| BTCPay Server | Finance | Self-hosted payment processor |
|
||||
| Mempool | Finance | Bitcoin blockchain explorer |
|
||||
| Fedimint | Finance | Federated e-cash and community banking |
|
||||
| File Browser | Storage | Web-based file manager |
|
||||
| Immich | Storage | Photo and video management (Google Photos alternative) |
|
||||
| PhotoPrism | Storage | AI-powered photo organizer |
|
||||
| Penpot | Productivity | Open-source design tool (Figma alternative) |
|
||||
| SearXNG | Privacy | Privacy-respecting metasearch engine |
|
||||
| Ollama | AI | Run large language models locally |
|
||||
| Nostr Relay | Network | Decentralized social protocol relay |
|
||||
| Nginx Proxy Manager | Network | Reverse proxy with SSL management |
|
||||
| Home Assistant | IoT | Smart home automation |
|
||||
| Tailscale | Network | Zero-config VPN for remote access |
|
||||
|
||||
---
|
||||
|
||||
## Managing Apps
|
||||
|
||||
### My Apps View
|
||||
|
||||
Navigate to **My Apps** to see all installed applications in a grid view. Each card shows:
|
||||
- App icon and name
|
||||
- Status badge (Running, Stopped, Installing)
|
||||
- Version number
|
||||
|
||||
### App Actions
|
||||
|
||||
Click an app to open its details page. Available actions:
|
||||
|
||||
- **Launch** — Open the app's web interface
|
||||
- **Start** — Start a stopped app
|
||||
- **Stop** — Stop a running app
|
||||
- **Restart** — Restart the app
|
||||
- **Uninstall** — Remove the app and its container (data volumes are preserved)
|
||||
|
||||
### App Interfaces
|
||||
|
||||
Most apps open in an embedded view within Archipelago. Some apps (BTCPay Server, Home Assistant) open in a new browser tab due to security restrictions.
|
||||
|
||||
---
|
||||
|
||||
## Bitcoin Node
|
||||
|
||||
### First-Time Bitcoin Setup
|
||||
|
||||
1. Install **Bitcoin Knots** from the App Store
|
||||
2. The node begins syncing the blockchain automatically
|
||||
3. Initial sync takes 1–7 days depending on your hardware and connection
|
||||
4. Monitor sync progress by launching the Bitcoin Knots interface
|
||||
|
||||
### What Bitcoin Knots Provides
|
||||
|
||||
- Full validation of all Bitcoin transactions
|
||||
- Privacy — no third party sees your wallet queries
|
||||
- Foundation for Lightning (LND), Electrs, Mempool, and BTCPay
|
||||
|
||||
### Bitcoin Data Location
|
||||
|
||||
Bitcoin blockchain data is stored at `/var/lib/archipelago/bitcoin-knots/` on the server. Plan for 600+ GB of storage.
|
||||
|
||||
---
|
||||
|
||||
## Lightning Network (LND)
|
||||
|
||||
### Setup
|
||||
|
||||
1. Ensure Bitcoin Knots is installed and running
|
||||
2. Install **LND** from the App Store
|
||||
3. LND connects to your Bitcoin node automatically
|
||||
|
||||
### Managing Channels
|
||||
|
||||
Navigate to **My Apps → LND → Channels** to:
|
||||
- View open channels and their balances
|
||||
- Open new channels to peers
|
||||
- Close existing channels
|
||||
|
||||
### Integration with BTCPay
|
||||
|
||||
When both LND and BTCPay Server are installed, BTCPay automatically detects LND and enables Lightning payments. No manual configuration needed.
|
||||
|
||||
---
|
||||
|
||||
## Cloud Storage
|
||||
|
||||
### File Browser
|
||||
|
||||
The built-in File Browser gives you web-based access to your server's files.
|
||||
|
||||
1. Navigate to **Cloud** from the sidebar
|
||||
2. Click **File Browser** to open it
|
||||
3. Upload, download, create folders, and manage files
|
||||
|
||||
### Photo Management (Immich)
|
||||
|
||||
For photo and video management similar to Google Photos:
|
||||
|
||||
1. Install **Immich** from the App Store
|
||||
2. Access it from the Cloud section
|
||||
3. Upload photos/videos or use the Immich mobile app to auto-backup your phone
|
||||
|
||||
### Storage Location
|
||||
|
||||
All cloud data is stored under `/var/lib/archipelago/` on your server's disk.
|
||||
|
||||
---
|
||||
|
||||
## Identity & Web5
|
||||
|
||||
### Your Decentralized Identifier (DID)
|
||||
|
||||
Your DID is a globally unique identifier that you control. Navigate to **Web5** to see:
|
||||
|
||||
- Your DID string (copy it to share)
|
||||
- Wallet connection status
|
||||
- Connected Nostr relays
|
||||
|
||||
### Nostr Relay
|
||||
|
||||
If you've installed the Nostr Relay, it runs locally on your server. You can use it with any Nostr client by adding your server's relay URL.
|
||||
|
||||
### DID Features (Coming Soon)
|
||||
|
||||
- Verifiable credentials
|
||||
- Passwordless authentication to other services
|
||||
- Data portability between servers
|
||||
|
||||
---
|
||||
|
||||
## Settings
|
||||
|
||||
Navigate to **Settings** from the sidebar.
|
||||
|
||||
### Account Information
|
||||
|
||||
- **Server Name** — Your server's display name
|
||||
- **Version** — Current Archipelago version
|
||||
- **DID** — Your Decentralized Identifier (with copy button)
|
||||
- **Tor Address** — Your .onion address for Tor access (with copy button)
|
||||
|
||||
### Change Password
|
||||
|
||||
1. Click **Change Password**
|
||||
2. Enter your current password
|
||||
3. Enter and confirm your new password (12+ characters, must include uppercase, lowercase, digit, and special character)
|
||||
4. All other active sessions are invalidated after password change
|
||||
|
||||
### Two-Factor Authentication (2FA)
|
||||
|
||||
1. Click **Enable 2FA** in Settings
|
||||
2. Scan the QR code with your authenticator app (Google Authenticator, Authy, etc.)
|
||||
3. Enter the 6-digit code to verify
|
||||
4. Save your backup codes securely — they're the only way to log in if you lose your authenticator
|
||||
|
||||
### Logout
|
||||
|
||||
Click **Logout** to end your session. You'll be redirected to the login screen.
|
||||
|
||||
---
|
||||
|
||||
## Backup & Restore
|
||||
|
||||
### Identity Backup
|
||||
|
||||
During onboarding, you created an identity backup file. To create a new one:
|
||||
|
||||
1. Go to **Settings**
|
||||
2. Look for backup options (identity backup is tied to your DID)
|
||||
|
||||
### App Data
|
||||
|
||||
App data is stored in `/var/lib/archipelago/{app-id}/` on the server. To back up:
|
||||
|
||||
1. Use File Browser to download important files
|
||||
2. Or SSH into the server and use standard backup tools (rsync, tar)
|
||||
|
||||
### Full System Recovery
|
||||
|
||||
If your server fails:
|
||||
|
||||
1. Flash a new USB installer and install on replacement hardware
|
||||
2. During onboarding, choose **Restore from Backup** (when available)
|
||||
3. Upload your `archipelago-did-backup.json` file
|
||||
4. Re-install your apps from the App Store
|
||||
|
||||
---
|
||||
|
||||
## Remote Access
|
||||
|
||||
### Local Network
|
||||
|
||||
Your Archipelago server is accessible on your home network at its IP address or `http://archipelago.local`.
|
||||
|
||||
### Tor (Built-in)
|
||||
|
||||
Every Archipelago server has a Tor hidden service. Your `.onion` address is shown in Settings. Access it via Tor Browser from anywhere in the world — no port forwarding required.
|
||||
|
||||
### Tailscale (Recommended)
|
||||
|
||||
For easy, secure remote access without Tor:
|
||||
|
||||
1. Install **Tailscale** from the App Store
|
||||
2. Launch it and sign in with your Tailscale account
|
||||
3. Install Tailscale on your phone/laptop
|
||||
4. Access your server from anywhere via its Tailscale IP
|
||||
|
||||
See [Tailscale Setup Guide](USER-GUIDE-TAILSCALE.md) for detailed instructions.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Can't Find Server on Network
|
||||
|
||||
- Ensure the server is powered on and connected to your router via Ethernet
|
||||
- Check your router's DHCP client list for the server's IP
|
||||
- Try `http://archipelago.local` (requires mDNS support)
|
||||
- Connect a monitor to see the IP displayed on the console
|
||||
|
||||
### Login Issues
|
||||
|
||||
- **Forgot password**: Connect a monitor and keyboard. Log in at the console and run the password reset tool
|
||||
- **2FA locked out**: Use your backup codes on the login screen (click "Use backup code")
|
||||
- **Session expired**: Sessions expire after 24 hours of inactivity. Simply log in again.
|
||||
|
||||
### App Won't Start
|
||||
|
||||
- Check if dependent apps are running (e.g., Bitcoin Knots must run before Electrs)
|
||||
- Try stopping and starting the app again
|
||||
- Check app logs in **My Apps → [App] → Logs**
|
||||
- Restart the Archipelago service: SSH in and run `sudo systemctl restart archipelago`
|
||||
|
||||
### Bitcoin Node Sync Issues
|
||||
|
||||
- Initial sync can take several days — this is normal
|
||||
- Ensure your server has a reliable internet connection
|
||||
- Check available disk space: Bitcoin needs 600+ GB
|
||||
|
||||
### WebSocket Disconnected
|
||||
|
||||
The UI shows a "Reconnecting..." banner if the WebSocket connection drops.
|
||||
- This auto-recovers within 30 seconds
|
||||
- If persistent, check that the backend service is running: `sudo systemctl status archipelago`
|
||||
- Hard refresh the browser (Ctrl+Shift+R)
|
||||
|
||||
### Server Unresponsive
|
||||
|
||||
If the web UI is unreachable:
|
||||
|
||||
1. SSH into the server: `ssh archipelago@<your-ip>`
|
||||
2. Check service status: `sudo systemctl status archipelago`
|
||||
3. Restart if needed: `sudo systemctl restart archipelago`
|
||||
4. Check system resources: `free -h` (RAM), `df -h` (disk)
|
||||
|
||||
### Getting Help
|
||||
|
||||
- Check the [Architecture Guide](architecture.md) for technical details
|
||||
- File issues at the project repository
|
||||
- Join the community for support
|
||||
@@ -1,159 +0,0 @@
|
||||
# UX Audit Report - Archipelago Web UI
|
||||
|
||||
**Date**: 2026-03-11
|
||||
**Scope**: All 12 pages (login, home, apps, marketplace, cloud, server, web5, settings, chat, federation, credentials, system update)
|
||||
**Method**: Screenshot review + source code analysis
|
||||
|
||||
## Summary
|
||||
|
||||
| Priority | Count | Description |
|
||||
|----------|-------|-------------|
|
||||
| **P0** | 3 | Apps empty state never renders; Credentials API parse error; Persistent unhealthy banners |
|
||||
| **P1** | 13 | Dead links, no-op buttons, hardcoded fake data, missing error feedback, silent failures |
|
||||
| **P2** | 14 | Inconsistent patterns, visual polish, native dialogs, loading states |
|
||||
|
||||
---
|
||||
|
||||
## P0 - Broken Functionality
|
||||
|
||||
### Apps: Empty state hardcoded to never display
|
||||
- **File**: `Apps.vue:19` — `v-if="false"` means the "No Apps Installed" empty state can never render
|
||||
- **Fix**: Change to `v-if="sortedPackageEntries.length === 0 && !searchQuery"`
|
||||
|
||||
### Credentials: API parse error on page load
|
||||
- **File**: `Credentials.vue` — screenshot shows "Failed to load credentials: Parsing credentials" red error
|
||||
- **Fix**: Debug `identity.list-credentials` RPC response format; handle all response shapes gracefully
|
||||
|
||||
### Cross-page: Persistent "tor is unhealthy" banners stack in top-right
|
||||
- Every dashboard page shows 3-4 stacked red notification banners that never clear
|
||||
- **Fix**: Auto-dismiss when service recovers, or make dismissible; don't stack duplicates
|
||||
|
||||
---
|
||||
|
||||
## P1 - Confusing UX
|
||||
|
||||
### Login: "Forgot password?" link is dead (`href="#"`)
|
||||
- **File**: `Login.vue:178`
|
||||
- **Fix**: Remove link or show help message (password reset requires SSH/re-image)
|
||||
|
||||
### Login: No minimum password length feedback during setup
|
||||
- **File**: `Login.vue:80` — button disabled check doesn't include length >= 8
|
||||
- **Fix**: Add reactive validation message and include length check in `:disabled`
|
||||
|
||||
### Home: Web5 card values are hardcoded (fake data)
|
||||
- **File**: `Home.vue:244-259` — DID "Active", DWN "Synced", Profits "0.024" are static
|
||||
- **Fix**: Fetch from RPC or show "--" / "Loading..."
|
||||
|
||||
### Home: No loading state for Network/Web5 cards
|
||||
- **File**: `Home.vue` — Network and Web5 cards show static content immediately
|
||||
- **Fix**: Add skeleton placeholders consistent with Cloud card pattern
|
||||
|
||||
### Home: Refresh buttons on Network/Web5 cards are no-ops
|
||||
- **File**: `Home.vue:208,266` — `@click="() => {}"`
|
||||
- **Fix**: Wire to reload data or remove the buttons
|
||||
|
||||
### Apps: Start/stop errors only logged to console
|
||||
- **File**: `Apps.vue:344,360`
|
||||
- **Fix**: Add toast notification on failure
|
||||
|
||||
### Server: "Manage Local Network" and "Manage Web3 Services" buttons are no-ops
|
||||
- **File**: `Server.vue:223,284` — no `@click` handler
|
||||
- **Fix**: Wire to route/modal or disable with "Coming Soon" tooltip
|
||||
|
||||
### Server: "View" logs button clears count but shows nothing
|
||||
- **File**: `Server.vue:766-768` — just resets counter
|
||||
- **Fix**: Navigate to logs view or show "Coming soon" message
|
||||
|
||||
### Server: WiFi connection failure silently swallowed
|
||||
- **File**: `Server.vue:668`
|
||||
- **Fix**: Show error message in WiFi modal
|
||||
|
||||
### Server: DNS configuration error silently swallowed
|
||||
- **File**: `Server.vue:622`
|
||||
- **Fix**: Show error message in DNS modal
|
||||
|
||||
### Chat: No close/back button on mobile
|
||||
- **File**: `Chat.vue:4` — `hidden md:flex` hides close button
|
||||
- **Fix**: Add mobile-specific back button
|
||||
|
||||
### Federation: Error display at bottom of template, easily missed
|
||||
- **File**: `Federation.vue:292-295`
|
||||
- **Fix**: Move error display above "Federated Nodes" section
|
||||
|
||||
### Federation: "Remove from Federation" has no confirmation
|
||||
- **File**: `Federation.vue:243` — destructive action, no confirm dialog
|
||||
- **Fix**: Add confirmation step before removing node
|
||||
|
||||
---
|
||||
|
||||
## P2 - Minor Polish
|
||||
|
||||
### Login: Duplicate `id="password"` across setup/login templates
|
||||
- **File**: `Login.vue:52,150`
|
||||
- **Fix**: Use distinct IDs (`setup-password`, `login-password`)
|
||||
|
||||
### Home: System stats show 0 until first RPC response
|
||||
- **File**: `Home.vue:297-323`
|
||||
- **Fix**: Show skeleton/placeholder bars instead of zero values
|
||||
|
||||
### Marketplace: Category tabs hidden on mobile
|
||||
- **File**: `Marketplace.vue:112` — `hidden md:flex`
|
||||
- **Fix**: Add horizontal scrollable tabs or dropdown for mobile
|
||||
|
||||
### Marketplace: Bottom row of apps may be cut off
|
||||
- **Fix**: Add `pb-24` bottom padding to scrollable container
|
||||
|
||||
### Cloud: No loading state while file counts fetch
|
||||
- **Fix**: Add loading skeleton to item count areas
|
||||
|
||||
### Web5: Quick actions grid wraps asymmetrically at lg breakpoint
|
||||
- **File**: `Web5.vue:11` — 5 items in 3-column grid
|
||||
- **Fix**: Use `lg:grid-cols-5` or restructure for balanced layout
|
||||
|
||||
### Settings: DID string has no copy button
|
||||
- **File**: `Settings.vue:63` — Tor address has copy button, DID doesn't
|
||||
- **Fix**: Add copy button matching Tor address pattern
|
||||
|
||||
### Settings: Onion address may overflow on mobile
|
||||
- **File**: `Settings.vue:87`
|
||||
- **Fix**: Add `truncate` with title tooltip
|
||||
|
||||
### Chat: Fallback message exposes env variable name
|
||||
- **File**: `Chat.vue:52` — shows `VITE_AIUI_URL`
|
||||
- **Fix**: Reword to user-friendly message
|
||||
|
||||
### Federation: Page header hidden on mobile with no alternative
|
||||
- **File**: `Federation.vue:3`
|
||||
- **Fix**: Verify mobile layout shell shows title
|
||||
|
||||
### Credentials: Toast position overlapped by mobile tab bar
|
||||
- **File**: `Credentials.vue:190` — `fixed bottom-6`
|
||||
- **Fix**: Change to `bottom-20` to clear mobile tab bar
|
||||
|
||||
### Credentials: Issue button has no validation feedback for empty fields
|
||||
- **File**: `Credentials.vue:44`
|
||||
- **Fix**: Add `:disabled` when required fields empty; add required indicators
|
||||
|
||||
### SystemUpdate: Sequential await calls slow page load
|
||||
- **File**: `SystemUpdate.vue:374-378`
|
||||
- **Fix**: Use `Promise.all()` for concurrent fetching
|
||||
|
||||
### SystemUpdate: `confirm()` for apply/rollback breaks glass UI
|
||||
- **File**: `SystemUpdate.vue:316,334`
|
||||
- **Fix**: Replace with glass-styled confirmation modals
|
||||
|
||||
### Apps: Uninstall failure uses `alert()`
|
||||
- **File**: `Apps.vue:396`
|
||||
- **Fix**: Replace with inline error toast
|
||||
|
||||
---
|
||||
|
||||
## Cross-Page Issues
|
||||
|
||||
### Toast pattern inconsistent
|
||||
- Credentials: custom inline toast. Settings: inline `<p>`. Apps: `alert()`. Server: swallows errors.
|
||||
- **Fix**: Implement shared `useToast()` composable used consistently everywhere
|
||||
|
||||
### Page headers inconsistently hidden on mobile
|
||||
- Apps, Cloud, Server, Federation: `hidden md:block`. Home, Web5, Settings, Credentials, SystemUpdate: always visible.
|
||||
- **Fix**: Standardize pattern across all pages
|
||||
@@ -1,24 +0,0 @@
|
||||
# v3.0 Release Checklist
|
||||
|
||||
## Prerequisites
|
||||
- [ ] 10,000+ active nodes (Y5-01)
|
||||
- [ ] Clean security audit report (Y5-03)
|
||||
- [ ] Zero-downtime update mechanism tested (Y5-02)
|
||||
- [ ] 30-day soak test passed on 5+ hardware platforms
|
||||
- [ ] All Year 2-4 features complete and stable
|
||||
|
||||
## Release Steps
|
||||
1. Freeze code (no new features)
|
||||
2. Run full test suite on all certified hardware
|
||||
3. Security audit findings resolved
|
||||
4. Update CHANGELOG.md and version numbers
|
||||
5. Build ISO for x86_64 and ARM64
|
||||
6. Create GitHub release with SHA256 checksums
|
||||
7. Publish release announcement
|
||||
8. Update documentation site
|
||||
9. Tag `v3.0.0` in git
|
||||
|
||||
## Post-Release
|
||||
- Monitor opt-in telemetry for crash reports
|
||||
- 48-hour hotfix window (team on standby)
|
||||
- Community announcement on Nostr and forums
|
||||
@@ -1,633 +0,0 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Build Archipelago Bitcoin Node OS ISO - Debian Live Edition
|
||||
# Based on Debian Live for reliable USB boot (like StartOS)
|
||||
#
|
||||
# Usage: ./build-debian-iso.sh
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
WORK_DIR="$SCRIPT_DIR/build/debian-iso"
|
||||
OUTPUT_DIR="$SCRIPT_DIR/results"
|
||||
DEBIAN_VERSION="bookworm"
|
||||
ARCH="amd64"
|
||||
|
||||
# Start build timer
|
||||
BUILD_START=$(date +%s)
|
||||
|
||||
echo "╔════════════════════════════════════════════════════════╗"
|
||||
echo "║ Building Archipelago - Debian Live Edition ║"
|
||||
echo "╚════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
echo "⏱️ Build started: $(date '+%H:%M:%S')"
|
||||
echo ""
|
||||
|
||||
# Create directories
|
||||
mkdir -p "$WORK_DIR"
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
# Download Debian Live ISO if not exists
|
||||
BASE_ISO="$WORK_DIR/debian-live-12-${ARCH}-standard.iso"
|
||||
BASE_ISO_SIZE=369131520 # Expected size: ~352MB
|
||||
|
||||
if [ ! -f "$BASE_ISO" ] || [ $(stat -f%z "$BASE_ISO" 2>/dev/null || stat -c%s "$BASE_ISO" 2>/dev/null || echo 0) -lt 100000000 ]; then
|
||||
echo "📥 Downloading Debian Live 12 (Bookworm) Standard ISO..."
|
||||
echo " Size: ~352MB | This is a one-time download (cached for future builds)"
|
||||
echo ""
|
||||
rm -f "$BASE_ISO"
|
||||
|
||||
# Download with progress bar
|
||||
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"
|
||||
|
||||
# Verify download succeeded
|
||||
if [ -f "$BASE_ISO" ] && [ $(stat -f%z "$BASE_ISO" 2>/dev/null || stat -c%s "$BASE_ISO" 2>/dev/null) -gt 300000000 ]; then
|
||||
ISO_SIZE=$(du -h "$BASE_ISO" | awk '{print $1}')
|
||||
echo ""
|
||||
echo "✅ Downloaded Debian Live ISO ($ISO_SIZE)"
|
||||
echo " 📝 Cached at: $BASE_ISO"
|
||||
else
|
||||
echo ""
|
||||
echo "❌ Download failed or incomplete"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
ISO_SIZE=$(du -h "$BASE_ISO" | awk '{print $1}')
|
||||
echo "✅ Using cached Debian Live ISO ($ISO_SIZE)"
|
||||
echo " 📁 Location: $BASE_ISO"
|
||||
fi
|
||||
|
||||
# Extract ISO
|
||||
echo ""
|
||||
echo "📦 Extracting Debian Live ISO..."
|
||||
ISO_CUSTOM="$WORK_DIR/custom"
|
||||
rm -rf "$ISO_CUSTOM"
|
||||
mkdir -p "$ISO_CUSTOM"
|
||||
|
||||
cd "$ISO_CUSTOM"
|
||||
7z x -y "$BASE_ISO" >/dev/null 2>&1 || 7z x -y "$BASE_ISO"
|
||||
|
||||
echo "✅ Extracted ISO"
|
||||
|
||||
# Add Archipelago files
|
||||
echo ""
|
||||
echo "📋 Adding Archipelago files..."
|
||||
|
||||
ARCHIPELAGO_DIR="$ISO_CUSTOM/archipelago"
|
||||
mkdir -p "$ARCHIPELAGO_DIR"
|
||||
mkdir -p "$ARCHIPELAGO_DIR/bin"
|
||||
mkdir -p "$ARCHIPELAGO_DIR/scripts"
|
||||
|
||||
# Copy the pre-built backend if it exists
|
||||
if [ -f "$SCRIPT_DIR/build/backend/archipelago" ]; then
|
||||
echo "🦀 Including Archipelago backend from build/backend..."
|
||||
cp "$SCRIPT_DIR/build/backend/archipelago" "$ARCHIPELAGO_DIR/bin/"
|
||||
chmod +x "$ARCHIPELAGO_DIR/bin/archipelago"
|
||||
elif [ -d "$SCRIPT_DIR/../core/target/release" ]; then
|
||||
echo "🦀 Including Archipelago backend from target/release..."
|
||||
cp "$SCRIPT_DIR/../core/target/release/archipelago" "$ARCHIPELAGO_DIR/bin/" 2>/dev/null || true
|
||||
chmod +x "$ARCHIPELAGO_DIR/bin/archipelago" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Copy the frontend build if it exists
|
||||
if [ -d "$SCRIPT_DIR/build/frontend" ]; then
|
||||
echo "🎨 Including Archipelago Web UI from build/frontend..."
|
||||
cp -r "$SCRIPT_DIR/build/frontend" "$ARCHIPELAGO_DIR/web-ui"
|
||||
elif [ -d "$SCRIPT_DIR/../web/dist/neode-ui" ]; then
|
||||
echo "🎨 Including Archipelago Web UI from web/dist..."
|
||||
cp -r "$SCRIPT_DIR/../web/dist/neode-ui" "$ARCHIPELAGO_DIR/web-ui"
|
||||
elif [ -d "$SCRIPT_DIR/../neode-ui/dist" ]; then
|
||||
echo "🎨 Including Archipelago frontend from neode-ui/dist..."
|
||||
cp -r "$SCRIPT_DIR/../neode-ui/dist" "$ARCHIPELAGO_DIR/web-ui" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Copy app manifests
|
||||
if [ -d "$SCRIPT_DIR/../apps" ]; then
|
||||
echo "📦 Including app manifests..."
|
||||
cp -r "$SCRIPT_DIR/../apps" "$ARCHIPELAGO_DIR/apps" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Copy setup scripts
|
||||
if [ -d "$SCRIPT_DIR/archipelago-scripts" ]; then
|
||||
echo "📜 Including setup scripts..."
|
||||
cp "$SCRIPT_DIR/archipelago-scripts/"*.sh "$ARCHIPELAGO_DIR/scripts/" 2>/dev/null || true
|
||||
chmod +x "$ARCHIPELAGO_DIR/scripts/"*.sh
|
||||
fi
|
||||
|
||||
# Create main setup script for Archipelago
|
||||
cat > "$ARCHIPELAGO_DIR/setup-archipelago.sh" <<'SETUP_EOF'
|
||||
#!/bin/bash
|
||||
#
|
||||
# Archipelago Setup Script for Debian Live
|
||||
#
|
||||
echo ""
|
||||
echo "╔═══════════════════════════════════════════════════════════╗"
|
||||
echo "║ 🏝️ ARCHIPELAGO BITCOIN NODE OS - Debian Edition ║"
|
||||
echo "╚═══════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
# Find the boot media
|
||||
BOOT_MEDIA=""
|
||||
for dev in /run/live/medium /lib/live/mount/medium /cdrom /media/cdrom; do
|
||||
if [ -d "$dev/archipelago" ]; then
|
||||
BOOT_MEDIA="$dev"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$BOOT_MEDIA" ]; then
|
||||
echo "❌ Could not find Archipelago files on boot media"
|
||||
echo " Looking in: /run/live/medium, /lib/live/mount/medium, /cdrom"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "📍 Found Archipelago at: $BOOT_MEDIA/archipelago"
|
||||
echo ""
|
||||
|
||||
# Copy files to system
|
||||
if [ -d "$BOOT_MEDIA/archipelago/bin" ]; then
|
||||
echo "📋 Installing Archipelago binaries..."
|
||||
sudo cp -r "$BOOT_MEDIA/archipelago/bin/"* /usr/local/bin/ 2>/dev/null || true
|
||||
fi
|
||||
|
||||
if [ -d "$BOOT_MEDIA/archipelago/apps" ]; then
|
||||
echo "📋 Installing app manifests..."
|
||||
sudo mkdir -p /etc/archipelago
|
||||
sudo cp -r "$BOOT_MEDIA/archipelago/apps" /etc/archipelago/ 2>/dev/null || true
|
||||
fi
|
||||
|
||||
if [ -d "$BOOT_MEDIA/archipelago/scripts" ]; then
|
||||
echo "📋 Installing setup scripts..."
|
||||
sudo mkdir -p /opt/archipelago
|
||||
sudo cp -r "$BOOT_MEDIA/archipelago/scripts" /opt/archipelago/ 2>/dev/null || true
|
||||
sudo chmod +x /opt/archipelago/scripts/*.sh
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "✅ Archipelago installed!"
|
||||
echo ""
|
||||
|
||||
# Automatically launch the menu
|
||||
sleep 1
|
||||
exec /opt/archipelago/scripts/archipelago-menu.sh
|
||||
SETUP_EOF
|
||||
|
||||
chmod +x "$ARCHIPELAGO_DIR/setup-archipelago.sh"
|
||||
|
||||
# Create auto-start script that runs on login
|
||||
cat > "$ARCHIPELAGO_DIR/auto-start.sh" <<'AUTOSTART_EOF'
|
||||
#!/bin/bash
|
||||
#
|
||||
# Archipelago Auto-Start - Runs on first login
|
||||
#
|
||||
|
||||
# Find boot media
|
||||
BOOT_MEDIA=""
|
||||
for dev in /run/live/medium /lib/live/mount/medium /cdrom; do
|
||||
if [ -d "$dev/archipelago" ]; then
|
||||
BOOT_MEDIA="$dev"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Install essential tools on first boot (required for disk installer)
|
||||
if [ ! -f /tmp/.archipelago-tools-installed ]; then
|
||||
clear
|
||||
echo ""
|
||||
echo " ╔═══════════════════════════════════════════════════════════╗"
|
||||
echo " ║ 🏝️ ARCHIPELAGO - First Boot Setup ║"
|
||||
echo " ╚═══════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
echo " 📦 Installing required tools..."
|
||||
echo ""
|
||||
|
||||
# Update and install essential tools
|
||||
sudo apt-get update -qq 2>/dev/null
|
||||
sudo apt-get install -y parted debootstrap dosfstools e2fsprogs 2>/dev/null
|
||||
|
||||
echo " ✅ Tools installed"
|
||||
touch /tmp/.archipelago-tools-installed
|
||||
sleep 1
|
||||
fi
|
||||
|
||||
# Get IP address
|
||||
IP=$(hostname -I 2>/dev/null | awk '{print $1}')
|
||||
|
||||
# Start web UI in background if not already running
|
||||
if [ -n "$BOOT_MEDIA" ] && ! pgrep -f "http.server" >/dev/null; then
|
||||
WEB_UI_DIR="$BOOT_MEDIA/archipelago/web-ui"
|
||||
if [ -d "$WEB_UI_DIR" ]; then
|
||||
cd "$WEB_UI_DIR"
|
||||
nohup python3 -m http.server 80 --bind 0.0.0.0 >/dev/null 2>&1 &
|
||||
fi
|
||||
fi
|
||||
|
||||
clear
|
||||
echo ""
|
||||
echo " ╔═══════════════════════════════════════════════════════════╗"
|
||||
echo " ║ ║"
|
||||
echo " ║ 🏝️ ARCHIPELAGO BITCOIN NODE OS ║"
|
||||
echo " ║ ║"
|
||||
echo " ║ Your sovereign Bitcoin infrastructure ║"
|
||||
echo " ║ ║"
|
||||
echo " ╚═══════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
if [ -n "$IP" ]; then
|
||||
echo " ┌─────────────────────────────────────────────────────────────┐"
|
||||
echo " │ 🌐 Web UI: http://$IP "
|
||||
echo " └─────────────────────────────────────────────────────────────┘"
|
||||
echo ""
|
||||
fi
|
||||
echo " Type 'archipelago' to open the setup menu"
|
||||
echo ""
|
||||
|
||||
# Check if already set up
|
||||
if [ -f ~/.archipelago-setup-done ]; then
|
||||
return 2>/dev/null || exit 0
|
||||
fi
|
||||
|
||||
# First time - run setup automatically
|
||||
if [ -n "$BOOT_MEDIA" ] && [ -f "$BOOT_MEDIA/archipelago/setup-archipelago.sh" ]; then
|
||||
echo ""
|
||||
read -p " Press Enter to continue to setup menu..."
|
||||
bash "$BOOT_MEDIA/archipelago/setup-archipelago.sh"
|
||||
fi
|
||||
AUTOSTART_EOF
|
||||
|
||||
chmod +x "$ARCHIPELAGO_DIR/auto-start.sh"
|
||||
|
||||
# Create a simple 'archipelago-menu' command wrapper (don't overwrite the backend binary!)
|
||||
cat > "$ARCHIPELAGO_DIR/bin/archipelago-menu" <<'CMD_EOF'
|
||||
#!/bin/bash
|
||||
# Archipelago menu command - launches the setup menu
|
||||
|
||||
if [ -f /opt/archipelago/scripts/archipelago-menu.sh ]; then
|
||||
exec /opt/archipelago/scripts/archipelago-menu.sh
|
||||
else
|
||||
# Find on boot media
|
||||
for dev in /run/live/medium /lib/live/mount/medium /cdrom; do
|
||||
if [ -f "$dev/archipelago/scripts/archipelago-menu.sh" ]; then
|
||||
exec bash "$dev/archipelago/scripts/archipelago-menu.sh"
|
||||
fi
|
||||
done
|
||||
echo "Archipelago menu not found. Run setup first:"
|
||||
echo " sh /run/live/medium/archipelago/setup-archipelago.sh"
|
||||
fi
|
||||
CMD_EOF
|
||||
|
||||
chmod +x "$ARCHIPELAGO_DIR/bin/archipelago-menu"
|
||||
|
||||
# Verify the real backend binary is there
|
||||
if [ -f "$ARCHIPELAGO_DIR/bin/archipelago" ]; then
|
||||
echo " Backend binary: $(file "$ARCHIPELAGO_DIR/bin/archipelago" | grep -o 'ELF.*' || echo 'included')"
|
||||
fi
|
||||
|
||||
# Create SSH auto-start script for live environment
|
||||
mkdir -p "$ISO_CUSTOM/etc/live/config.conf.d"
|
||||
cat > "$ISO_CUSTOM/etc/live/config.conf.d/archipelago.conf" <<'LIVE_CONF_EOF'
|
||||
# Archipelago live config
|
||||
LIVE_HOSTNAME="archipelago"
|
||||
LIVE_USER_DEFAULT_GROUPS="audio cdrom dip floppy video plugdev netdev powerdev scanner bluetooth sudo"
|
||||
LIVE_CONF_EOF
|
||||
|
||||
# Create rc.local to start SSH on boot (works in live environment)
|
||||
mkdir -p "$ISO_CUSTOM/etc/rc.local.d"
|
||||
cat > "$ISO_CUSTOM/etc/rc.local.d/archipelago-ssh.sh" <<'RCLOCAL_EOF'
|
||||
#!/bin/bash
|
||||
# Enable SSH in live environment
|
||||
|
||||
# Install and start SSH if not running
|
||||
if ! systemctl is-active --quiet ssh 2>/dev/null; then
|
||||
# Try to start SSH (may already be installed)
|
||||
systemctl start ssh 2>/dev/null || true
|
||||
|
||||
# If SSH not installed, install it
|
||||
if ! command -v sshd >/dev/null 2>&1; then
|
||||
apt-get update -qq
|
||||
apt-get install -y openssh-server
|
||||
systemctl start ssh
|
||||
fi
|
||||
fi
|
||||
|
||||
# Set password for user (live user is typically 'user' with empty password)
|
||||
echo "user:archipelago" | chpasswd 2>/dev/null || true
|
||||
echo "root:archipelago" | chpasswd 2>/dev/null || true
|
||||
|
||||
# Allow password auth
|
||||
sed -i 's/#PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config 2>/dev/null
|
||||
sed -i 's/PasswordAuthentication no/PasswordAuthentication yes/' /etc/ssh/sshd_config 2>/dev/null
|
||||
systemctl restart ssh 2>/dev/null || true
|
||||
RCLOCAL_EOF
|
||||
chmod +x "$ISO_CUSTOM/etc/rc.local.d/archipelago-ssh.sh"
|
||||
|
||||
# Also create a systemd service that runs early
|
||||
mkdir -p "$ISO_CUSTOM/etc/systemd/system"
|
||||
cat > "$ISO_CUSTOM/etc/systemd/system/archipelago-ssh.service" <<'SERVICE_EOF'
|
||||
[Unit]
|
||||
Description=Archipelago SSH Setup
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/bin/bash -c 'apt-get update -qq && apt-get install -y openssh-server && echo "user:archipelago" | chpasswd && echo "root:archipelago" | chpasswd && sed -i "s/#PasswordAuthentication.*/PasswordAuthentication yes/" /etc/ssh/sshd_config && sed -i "s/PasswordAuthentication no/PasswordAuthentication yes/" /etc/ssh/sshd_config && systemctl restart ssh'
|
||||
RemainAfterExit=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
SERVICE_EOF
|
||||
|
||||
# Enable the service
|
||||
mkdir -p "$ISO_CUSTOM/etc/systemd/system/multi-user.target.wants"
|
||||
ln -sf ../archipelago-ssh.service "$ISO_CUSTOM/etc/systemd/system/multi-user.target.wants/archipelago-ssh.service"
|
||||
|
||||
# Create system-wide profile script that runs on ANY login
|
||||
mkdir -p "$ISO_CUSTOM/etc/profile.d"
|
||||
cat > "$ISO_CUSTOM/etc/profile.d/z99-archipelago.sh" <<'PROFILE_EOF'
|
||||
#!/bin/bash
|
||||
# Archipelago auto-start - runs on login (z99 = runs last)
|
||||
|
||||
# Only run once per session
|
||||
if [ -n "$ARCHIPELAGO_STARTED" ]; then
|
||||
return 0 2>/dev/null || exit 0
|
||||
fi
|
||||
export ARCHIPELAGO_STARTED=1
|
||||
|
||||
# Find archipelago directory
|
||||
BOOT_MEDIA=""
|
||||
for dev in /run/live/medium /lib/live/mount/medium /cdrom; do
|
||||
if [ -d "$dev/archipelago" ]; then
|
||||
BOOT_MEDIA="$dev"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Get IP address
|
||||
IP=$(hostname -I 2>/dev/null | awk '{print $1}')
|
||||
[ -z "$IP" ] && IP=$(ip -4 addr show | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | grep -v '127.0.0.1' | head -1)
|
||||
|
||||
# Show welcome banner ALWAYS
|
||||
clear
|
||||
echo ""
|
||||
echo " ╔═══════════════════════════════════════════════════════════╗"
|
||||
echo " ║ ║"
|
||||
echo " ║ 🏝️ ARCHIPELAGO BITCOIN NODE OS ║"
|
||||
echo " ║ ║"
|
||||
echo " ║ Your sovereign Bitcoin infrastructure ║"
|
||||
echo " ║ ║"
|
||||
echo " ╚═══════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
if [ -n "$IP" ]; then
|
||||
echo " ┌─────────────────────────────────────────────────────────────┐"
|
||||
echo " │ 🌐 Web UI: http://$IP:5678 │"
|
||||
echo " │ 📡 SSH: ssh user@$IP (password: archipelago) │"
|
||||
echo " └─────────────────────────────────────────────────────────────┘"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
if [ -z "$BOOT_MEDIA" ]; then
|
||||
echo " ⚠️ Boot media not found at /run/live/medium"
|
||||
echo ""
|
||||
echo " Manual commands:"
|
||||
echo " archipelago - Start backend server"
|
||||
echo " archipelago-menu - Open setup menu"
|
||||
echo ""
|
||||
return 0 2>/dev/null || exit 0
|
||||
fi
|
||||
|
||||
echo " 📍 Boot media: $BOOT_MEDIA"
|
||||
echo ""
|
||||
|
||||
# Install archipelago commands if not present
|
||||
if [ ! -f /usr/local/bin/archipelago ] && [ -f "$BOOT_MEDIA/archipelago/bin/archipelago" ]; then
|
||||
sudo cp "$BOOT_MEDIA/archipelago/bin/archipelago" /usr/local/bin/ 2>/dev/null
|
||||
sudo chmod +x /usr/local/bin/archipelago 2>/dev/null
|
||||
fi
|
||||
|
||||
if [ ! -f /usr/local/bin/archipelago-menu ] && [ -f "$BOOT_MEDIA/archipelago/bin/archipelago-menu" ]; then
|
||||
sudo cp "$BOOT_MEDIA/archipelago/bin/archipelago-menu" /usr/local/bin/ 2>/dev/null
|
||||
sudo chmod +x /usr/local/bin/archipelago-menu 2>/dev/null
|
||||
fi
|
||||
|
||||
# Copy scripts to /opt
|
||||
if [ ! -d /opt/archipelago/scripts ] && [ -d "$BOOT_MEDIA/archipelago/scripts" ]; then
|
||||
sudo mkdir -p /opt/archipelago
|
||||
sudo cp -r "$BOOT_MEDIA/archipelago/scripts" /opt/archipelago/ 2>/dev/null
|
||||
sudo chmod +x /opt/archipelago/scripts/*.sh 2>/dev/null
|
||||
fi
|
||||
|
||||
# Start web backend in background if available
|
||||
if command -v archipelago >/dev/null 2>&1; then
|
||||
if ! pgrep -f "archipelago" >/dev/null 2>&1; then
|
||||
echo " 🚀 Starting Archipelago backend on port 5678..."
|
||||
nohup archipelago >/tmp/archipelago.log 2>&1 &
|
||||
sleep 2
|
||||
else
|
||||
echo " ✅ Archipelago backend already running"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo " Commands:"
|
||||
echo " archipelago-menu - Open interactive setup menu"
|
||||
echo " archipelago - Start/restart backend server"
|
||||
echo ""
|
||||
sleep 1
|
||||
|
||||
# Launch the menu automatically
|
||||
if [ -f /opt/archipelago/scripts/archipelago-menu.sh ]; then
|
||||
exec bash /opt/archipelago/scripts/archipelago-menu.sh
|
||||
elif [ -f "$BOOT_MEDIA/archipelago/scripts/archipelago-menu.sh" ]; then
|
||||
exec bash "$BOOT_MEDIA/archipelago/scripts/archipelago-menu.sh"
|
||||
fi
|
||||
PROFILE_EOF
|
||||
|
||||
chmod +x "$ISO_CUSTOM/etc/profile.d/z99-archipelago.sh"
|
||||
|
||||
# Also add to /etc/skel/.bashrc as fallback for new user sessions
|
||||
mkdir -p "$ISO_CUSTOM/etc/skel"
|
||||
cat >> "$ISO_CUSTOM/etc/skel/.bashrc" <<'BASHRC_EOF'
|
||||
|
||||
# Archipelago auto-start
|
||||
if [ -z "$ARCHIPELAGO_STARTED" ] && [ -n "$PS1" ]; then
|
||||
export ARCHIPELAGO_STARTED=1
|
||||
|
||||
# Find boot media
|
||||
for dev in /run/live/medium /lib/live/mount/medium /cdrom; do
|
||||
if [ -d "$dev/archipelago" ]; then
|
||||
bash "$dev/archipelago/setup-archipelago.sh" 2>/dev/null || true
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
BASHRC_EOF
|
||||
|
||||
# CRITICAL: Make getty run our script directly after auto-login
|
||||
mkdir -p "$ISO_CUSTOM/etc/systemd/system/getty@tty1.service.d"
|
||||
cat > "$ISO_CUSTOM/etc/systemd/system/getty@tty1.service.d/override.conf" <<'GETTY_EOF'
|
||||
[Service]
|
||||
ExecStart=
|
||||
ExecStart=-/sbin/agetty --autologin user --login-program /bin/bash --login-options "-c 'source /etc/profile.d/z99-archipelago.sh; exec bash -i'" --noclear %I $TERM
|
||||
Type=idle
|
||||
GETTY_EOF
|
||||
|
||||
# Create a live-config hook that adds our script to the actual user's .bashrc after user creation
|
||||
mkdir -p "$ISO_CUSTOM/lib/live/config"
|
||||
cat > "$ISO_CUSTOM/lib/live/config/9999-archipelago-autostart" <<'LIVECONFIG_EOF'
|
||||
#!/bin/bash
|
||||
# Live-config hook to add Archipelago auto-start to live user's bashrc
|
||||
|
||||
set -e
|
||||
|
||||
# This runs after the live user is created
|
||||
if [ -d /home/user ]; then
|
||||
cat >> /home/user/.bashrc <<'EOF'
|
||||
|
||||
# Archipelago Auto-Start
|
||||
if [ -z "$ARCHIPELAGO_STARTED" ] && [ -n "$PS1" ]; then
|
||||
export ARCHIPELAGO_STARTED=1
|
||||
|
||||
# Find boot media
|
||||
BOOT_MEDIA=""
|
||||
for dev in /run/live/medium /lib/live/mount/medium /cdrom; do
|
||||
if [ -d "$dev/archipelago" ]; then
|
||||
BOOT_MEDIA="$dev"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "$BOOT_MEDIA" ]; then
|
||||
# Source the profile script
|
||||
if [ -f /etc/profile.d/z99-archipelago.sh ]; then
|
||||
bash /etc/profile.d/z99-archipelago.sh
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
EOF
|
||||
chown user:user /home/user/.bashrc
|
||||
fi
|
||||
LIVECONFIG_EOF
|
||||
|
||||
chmod +x "$ISO_CUSTOM/lib/live/config/9999-archipelago-autostart"
|
||||
cat > "$ISO_CUSTOM/etc/systemd/system/getty@tty1.service.d/override.conf" <<'GETTY_EOF'
|
||||
[Service]
|
||||
ExecStart=
|
||||
ExecStart=-/sbin/agetty --autologin user --noclear %I $TERM
|
||||
Type=idle
|
||||
GETTY_EOF
|
||||
|
||||
# Modify GRUB config for Archipelago branding and auto-start
|
||||
echo ""
|
||||
echo "⚙️ Configuring boot..."
|
||||
|
||||
if [ -f "$ISO_CUSTOM/boot/grub/grub.cfg" ]; then
|
||||
# Update branding
|
||||
sed -i.bak \
|
||||
-e 's/Debian GNU\/Linux/Archipelago Bitcoin Node OS/g' \
|
||||
-e 's/Live system/Archipelago Live/g' \
|
||||
"$ISO_CUSTOM/boot/grub/grub.cfg"
|
||||
|
||||
# Add components=live.persist boot parameter to enable persistence hooks
|
||||
# This will allow our scripts to run on boot
|
||||
sed -i '' -e 's/\(boot=live\)/\1 components=/' "$ISO_CUSTOM/boot/grub/grub.cfg" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
if [ -f "$ISO_CUSTOM/isolinux/menu.cfg" ]; then
|
||||
sed -i.bak \
|
||||
-e 's/Debian GNU\/Linux/Archipelago Bitcoin Node OS/g' \
|
||||
-e 's/Live system/Archipelago Live/g' \
|
||||
"$ISO_CUSTOM/isolinux/menu.cfg"
|
||||
fi
|
||||
|
||||
if [ -f "$ISO_CUSTOM/isolinux/live.cfg" ]; then
|
||||
sed -i.bak \
|
||||
-e 's/Live system/Archipelago Live/g' \
|
||||
"$ISO_CUSTOM/isolinux/live.cfg"
|
||||
fi
|
||||
|
||||
# Create final ISO
|
||||
OUTPUT_ISO="$OUTPUT_DIR/archipelago-debian-12-x86_64.iso"
|
||||
echo ""
|
||||
echo "🔥 Creating final bootable ISO..."
|
||||
|
||||
if command -v xorriso >/dev/null 2>&1; then
|
||||
# Create proper hybrid ISO with MBR for USB boot
|
||||
# Need to extract MBR from isolinux or use system syslinux
|
||||
|
||||
# Check for isohdpfx.bin in various locations
|
||||
ISOHDPFX=""
|
||||
for path in \
|
||||
"/usr/local/share/syslinux/isohdpfx.bin" \
|
||||
"/usr/share/syslinux/isohdpfx.bin" \
|
||||
"/opt/homebrew/share/syslinux/isohdpfx.bin" \
|
||||
"$ISO_CUSTOM/isolinux/isohdpfx.bin"; do
|
||||
if [ -f "$path" ]; then
|
||||
ISOHDPFX="$path"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$ISOHDPFX" ]; then
|
||||
echo "⚠️ No isohdpfx.bin found, extracting from isolinux.bin..."
|
||||
# Extract first 432 bytes from isolinux.bin as MBR
|
||||
dd if="$ISO_CUSTOM/isolinux/isolinux.bin" of="$WORK_DIR/isohdpfx.bin" bs=432 count=1 2>/dev/null
|
||||
ISOHDPFX="$WORK_DIR/isohdpfx.bin"
|
||||
fi
|
||||
|
||||
echo " Using MBR: $ISOHDPFX"
|
||||
|
||||
xorriso -as mkisofs -o "$OUTPUT_ISO" \
|
||||
-volid "ARCHIPELAGO" \
|
||||
-J -R \
|
||||
-isohybrid-mbr "$ISOHDPFX" \
|
||||
-c isolinux/boot.cat \
|
||||
-b isolinux/isolinux.bin \
|
||||
-no-emul-boot -boot-load-size 4 -boot-info-table \
|
||||
-eltorito-alt-boot \
|
||||
-e boot/grub/efi.img \
|
||||
-no-emul-boot \
|
||||
-isohybrid-gpt-basdat \
|
||||
"$ISO_CUSTOM"
|
||||
else
|
||||
echo "❌ xorriso not found. Please install it: brew install xorriso"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "✅ ISO created successfully!"
|
||||
echo ""
|
||||
|
||||
# Calculate build time
|
||||
BUILD_END=$(date +%s)
|
||||
BUILD_DURATION=$((BUILD_END - BUILD_START))
|
||||
BUILD_MINUTES=$((BUILD_DURATION / 60))
|
||||
BUILD_SECONDS=$((BUILD_DURATION % 60))
|
||||
|
||||
ISO_SIZE=$(du -h "$OUTPUT_ISO" | cut -f1)
|
||||
ISO_MD5=$(md5 -q "$OUTPUT_ISO" 2>/dev/null || md5sum "$OUTPUT_ISO" | awk '{print $1}')
|
||||
|
||||
echo "╔════════════════════════════════════════════════════════╗"
|
||||
echo "║ 🎉 Build Complete! ║"
|
||||
echo "╚════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
echo "📀 ISO File: $OUTPUT_ISO"
|
||||
echo "📏 Size: $ISO_SIZE"
|
||||
echo "🔐 MD5: $ISO_MD5"
|
||||
echo "⏱️ Build Time: ${BUILD_MINUTES}m ${BUILD_SECONDS}s"
|
||||
echo "🎯 Base: Debian 12 Live (Bookworm)"
|
||||
echo ""
|
||||
echo "🔥 Next Steps:"
|
||||
echo ""
|
||||
echo " 1. Flash to USB:"
|
||||
echo " cd image-recipe && ./write-usb-dd.sh /dev/diskN"
|
||||
echo ""
|
||||
echo " 2. Boot on target device"
|
||||
echo ""
|
||||
echo " 3. Auto-login as 'user' with menu launch"
|
||||
echo ""
|
||||
echo " 4. Access Web UI at http://<IP>:5678"
|
||||
echo ""
|
||||
echo " 5. SSH access: ssh user@<IP> (password: archipelago)"
|
||||
echo ""
|
||||
@@ -1,511 +0,0 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Build Archipelago Bitcoin Node OS ISO - Debian Live Edition
|
||||
# Based on Debian Live for reliable USB boot (like StartOS)
|
||||
#
|
||||
# Usage: ./build-debian-iso.sh
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
WORK_DIR="$SCRIPT_DIR/build/debian-iso"
|
||||
OUTPUT_DIR="$SCRIPT_DIR/results"
|
||||
DEBIAN_VERSION="bookworm"
|
||||
ARCH="amd64"
|
||||
|
||||
echo "╔════════════════════════════════════════════════════════╗"
|
||||
echo "║ Building Archipelago - Debian Live Edition ║"
|
||||
echo "╚════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
# Create directories
|
||||
mkdir -p "$WORK_DIR"
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
# Download Debian Live ISO if not exists
|
||||
BASE_ISO="$WORK_DIR/debian-live-12-${ARCH}-standard.iso"
|
||||
if [ ! -f "$BASE_ISO" ] || [ $(stat -f%z "$BASE_ISO" 2>/dev/null || echo 0) -lt 100000000 ]; then
|
||||
echo "📥 Downloading Debian Live 12 (Bookworm) Standard ISO..."
|
||||
rm -f "$BASE_ISO"
|
||||
# Use SourceForge respin which has up-to-date Debian 12 Live
|
||||
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"
|
||||
echo "✅ Downloaded Debian Live ISO"
|
||||
else
|
||||
echo "✅ Using cached Debian Live ISO"
|
||||
fi
|
||||
|
||||
# Extract ISO
|
||||
echo ""
|
||||
echo "📦 Extracting Debian Live ISO..."
|
||||
ISO_CUSTOM="$WORK_DIR/custom"
|
||||
rm -rf "$ISO_CUSTOM"
|
||||
mkdir -p "$ISO_CUSTOM"
|
||||
|
||||
cd "$ISO_CUSTOM"
|
||||
7z x -y "$BASE_ISO" >/dev/null 2>&1 || 7z x -y "$BASE_ISO"
|
||||
|
||||
echo "✅ Extracted ISO"
|
||||
|
||||
# Add Archipelago files
|
||||
echo ""
|
||||
echo "📋 Adding Archipelago files..."
|
||||
|
||||
ARCHIPELAGO_DIR="$ISO_CUSTOM/archipelago"
|
||||
mkdir -p "$ARCHIPELAGO_DIR"
|
||||
mkdir -p "$ARCHIPELAGO_DIR/bin"
|
||||
mkdir -p "$ARCHIPELAGO_DIR/scripts"
|
||||
|
||||
# Copy the pre-built backend if it exists
|
||||
if [ -d "$SCRIPT_DIR/../core/target/release" ]; then
|
||||
echo "🦀 Including Archipelago backend..."
|
||||
cp "$SCRIPT_DIR/../core/target/release/archipelago" "$ARCHIPELAGO_DIR/bin/" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Copy the frontend build if it exists
|
||||
if [ -d "$SCRIPT_DIR/../web/dist/neode-ui" ]; then
|
||||
echo "🎨 Including Archipelago Web UI..."
|
||||
cp -r "$SCRIPT_DIR/../web/dist/neode-ui" "$ARCHIPELAGO_DIR/web-ui"
|
||||
elif [ -d "$SCRIPT_DIR/../neode-ui/dist" ]; then
|
||||
echo "🎨 Including Archipelago frontend..."
|
||||
cp -r "$SCRIPT_DIR/../neode-ui/dist" "$ARCHIPELAGO_DIR/web-ui" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Copy app manifests
|
||||
if [ -d "$SCRIPT_DIR/../apps" ]; then
|
||||
echo "📦 Including app manifests..."
|
||||
cp -r "$SCRIPT_DIR/../apps" "$ARCHIPELAGO_DIR/apps" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Copy setup scripts
|
||||
if [ -d "$SCRIPT_DIR/archipelago-scripts" ]; then
|
||||
echo "📜 Including setup scripts..."
|
||||
cp "$SCRIPT_DIR/archipelago-scripts/"*.sh "$ARCHIPELAGO_DIR/scripts/" 2>/dev/null || true
|
||||
chmod +x "$ARCHIPELAGO_DIR/scripts/"*.sh
|
||||
fi
|
||||
|
||||
# Create main setup script for Archipelago
|
||||
cat > "$ARCHIPELAGO_DIR/setup-archipelago.sh" <<'SETUP_EOF'
|
||||
#!/bin/bash
|
||||
#
|
||||
# Archipelago Setup Script for Debian Live
|
||||
#
|
||||
echo ""
|
||||
echo "╔═══════════════════════════════════════════════════════════╗"
|
||||
echo "║ 🏝️ ARCHIPELAGO BITCOIN NODE OS - Debian Edition ║"
|
||||
echo "╚═══════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
# Find the boot media
|
||||
BOOT_MEDIA=""
|
||||
for dev in /run/live/medium /lib/live/mount/medium /cdrom /media/cdrom; do
|
||||
if [ -d "$dev/archipelago" ]; then
|
||||
BOOT_MEDIA="$dev"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$BOOT_MEDIA" ]; then
|
||||
echo "❌ Could not find Archipelago files on boot media"
|
||||
echo " Looking in: /run/live/medium, /lib/live/mount/medium, /cdrom"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "📍 Found Archipelago at: $BOOT_MEDIA/archipelago"
|
||||
echo ""
|
||||
|
||||
# Copy files to system
|
||||
if [ -d "$BOOT_MEDIA/archipelago/bin" ]; then
|
||||
echo "📋 Installing Archipelago binaries..."
|
||||
sudo cp -r "$BOOT_MEDIA/archipelago/bin/"* /usr/local/bin/ 2>/dev/null || true
|
||||
fi
|
||||
|
||||
if [ -d "$BOOT_MEDIA/archipelago/apps" ]; then
|
||||
echo "📋 Installing app manifests..."
|
||||
sudo mkdir -p /etc/archipelago
|
||||
sudo cp -r "$BOOT_MEDIA/archipelago/apps" /etc/archipelago/ 2>/dev/null || true
|
||||
fi
|
||||
|
||||
if [ -d "$BOOT_MEDIA/archipelago/scripts" ]; then
|
||||
echo "📋 Installing setup scripts..."
|
||||
sudo mkdir -p /opt/archipelago
|
||||
sudo cp -r "$BOOT_MEDIA/archipelago/scripts" /opt/archipelago/ 2>/dev/null || true
|
||||
sudo chmod +x /opt/archipelago/scripts/*.sh
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "✅ Archipelago installed!"
|
||||
echo ""
|
||||
|
||||
# Automatically launch the menu
|
||||
sleep 1
|
||||
exec /opt/archipelago/scripts/archipelago-menu.sh
|
||||
SETUP_EOF
|
||||
|
||||
chmod +x "$ARCHIPELAGO_DIR/setup-archipelago.sh"
|
||||
|
||||
# Create auto-start script that runs on login
|
||||
cat > "$ARCHIPELAGO_DIR/auto-start.sh" <<'AUTOSTART_EOF'
|
||||
#!/bin/bash
|
||||
#
|
||||
# Archipelago Auto-Start - Runs on first login
|
||||
#
|
||||
|
||||
# Find boot media
|
||||
BOOT_MEDIA=""
|
||||
for dev in /run/live/medium /lib/live/mount/medium /cdrom; do
|
||||
if [ -d "$dev/archipelago" ]; then
|
||||
BOOT_MEDIA="$dev"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Install essential tools on first boot (required for disk installer)
|
||||
if [ ! -f /tmp/.archipelago-tools-installed ]; then
|
||||
clear
|
||||
echo ""
|
||||
echo " ╔═══════════════════════════════════════════════════════════╗"
|
||||
echo " ║ 🏝️ ARCHIPELAGO - First Boot Setup ║"
|
||||
echo " ╚═══════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
echo " 📦 Installing required tools..."
|
||||
echo ""
|
||||
|
||||
# Update and install essential tools
|
||||
sudo apt-get update -qq 2>/dev/null
|
||||
sudo apt-get install -y parted debootstrap dosfstools e2fsprogs 2>/dev/null
|
||||
|
||||
echo " ✅ Tools installed"
|
||||
touch /tmp/.archipelago-tools-installed
|
||||
sleep 1
|
||||
fi
|
||||
|
||||
# Get IP address
|
||||
IP=$(hostname -I 2>/dev/null | awk '{print $1}')
|
||||
|
||||
# Start web UI in background if not already running
|
||||
if [ -n "$BOOT_MEDIA" ] && ! pgrep -f "http.server" >/dev/null; then
|
||||
WEB_UI_DIR="$BOOT_MEDIA/archipelago/web-ui"
|
||||
if [ -d "$WEB_UI_DIR" ]; then
|
||||
cd "$WEB_UI_DIR"
|
||||
nohup python3 -m http.server 80 --bind 0.0.0.0 >/dev/null 2>&1 &
|
||||
fi
|
||||
fi
|
||||
|
||||
clear
|
||||
echo ""
|
||||
echo " ╔═══════════════════════════════════════════════════════════╗"
|
||||
echo " ║ ║"
|
||||
echo " ║ 🏝️ ARCHIPELAGO BITCOIN NODE OS ║"
|
||||
echo " ║ ║"
|
||||
echo " ║ Your sovereign Bitcoin infrastructure ║"
|
||||
echo " ║ ║"
|
||||
echo " ╚═══════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
if [ -n "$IP" ]; then
|
||||
echo " ┌─────────────────────────────────────────────────────────────┐"
|
||||
echo " │ 🌐 Web UI: http://$IP "
|
||||
echo " └─────────────────────────────────────────────────────────────┘"
|
||||
echo ""
|
||||
fi
|
||||
echo " Type 'archipelago' to open the setup menu"
|
||||
echo ""
|
||||
|
||||
# Check if already set up
|
||||
if [ -f ~/.archipelago-setup-done ]; then
|
||||
return 2>/dev/null || exit 0
|
||||
fi
|
||||
|
||||
# First time - run setup automatically
|
||||
if [ -n "$BOOT_MEDIA" ] && [ -f "$BOOT_MEDIA/archipelago/setup-archipelago.sh" ]; then
|
||||
echo ""
|
||||
read -p " Press Enter to continue to setup menu..."
|
||||
bash "$BOOT_MEDIA/archipelago/setup-archipelago.sh"
|
||||
fi
|
||||
AUTOSTART_EOF
|
||||
|
||||
chmod +x "$ARCHIPELAGO_DIR/auto-start.sh"
|
||||
|
||||
# Create a simple 'archipelago-menu' command wrapper (don't overwrite the backend binary!)
|
||||
cat > "$ARCHIPELAGO_DIR/bin/archipelago-menu" <<'CMD_EOF'
|
||||
#!/bin/bash
|
||||
# Archipelago menu command - launches the setup menu
|
||||
|
||||
if [ -f /opt/archipelago/scripts/archipelago-menu.sh ]; then
|
||||
exec /opt/archipelago/scripts/archipelago-menu.sh
|
||||
else
|
||||
# Find on boot media
|
||||
for dev in /run/live/medium /lib/live/mount/medium /cdrom; do
|
||||
if [ -f "$dev/archipelago/scripts/archipelago-menu.sh" ]; then
|
||||
exec bash "$dev/archipelago/scripts/archipelago-menu.sh"
|
||||
fi
|
||||
done
|
||||
echo "Archipelago menu not found. Run setup first:"
|
||||
echo " sh /run/live/medium/archipelago/setup-archipelago.sh"
|
||||
fi
|
||||
CMD_EOF
|
||||
|
||||
chmod +x "$ARCHIPELAGO_DIR/bin/archipelago-menu"
|
||||
|
||||
# Verify the real backend binary is there
|
||||
if [ -f "$ARCHIPELAGO_DIR/bin/archipelago" ]; then
|
||||
echo " Backend binary: $(file "$ARCHIPELAGO_DIR/bin/archipelago" | grep -o 'ELF.*' || echo 'included')"
|
||||
fi
|
||||
|
||||
# Create SSH auto-start script for live environment
|
||||
mkdir -p "$ISO_CUSTOM/etc/live/config.conf.d"
|
||||
cat > "$ISO_CUSTOM/etc/live/config.conf.d/archipelago.conf" <<'LIVE_CONF_EOF'
|
||||
# Archipelago live config
|
||||
LIVE_HOSTNAME="archipelago"
|
||||
LIVE_USER_DEFAULT_GROUPS="audio cdrom dip floppy video plugdev netdev powerdev scanner bluetooth sudo"
|
||||
LIVE_CONF_EOF
|
||||
|
||||
# Create rc.local to start SSH on boot (works in live environment)
|
||||
mkdir -p "$ISO_CUSTOM/etc/rc.local.d"
|
||||
cat > "$ISO_CUSTOM/etc/rc.local.d/archipelago-ssh.sh" <<'RCLOCAL_EOF'
|
||||
#!/bin/bash
|
||||
# Enable SSH in live environment
|
||||
|
||||
# Install and start SSH if not running
|
||||
if ! systemctl is-active --quiet ssh 2>/dev/null; then
|
||||
# Try to start SSH (may already be installed)
|
||||
systemctl start ssh 2>/dev/null || true
|
||||
|
||||
# If SSH not installed, install it
|
||||
if ! command -v sshd >/dev/null 2>&1; then
|
||||
apt-get update -qq
|
||||
apt-get install -y openssh-server
|
||||
systemctl start ssh
|
||||
fi
|
||||
fi
|
||||
|
||||
# Set password for user (live user is typically 'user' with empty password)
|
||||
echo "user:archipelago" | chpasswd 2>/dev/null || true
|
||||
echo "root:archipelago" | chpasswd 2>/dev/null || true
|
||||
|
||||
# Allow password auth
|
||||
sed -i 's/#PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config 2>/dev/null
|
||||
sed -i 's/PasswordAuthentication no/PasswordAuthentication yes/' /etc/ssh/sshd_config 2>/dev/null
|
||||
systemctl restart ssh 2>/dev/null || true
|
||||
RCLOCAL_EOF
|
||||
chmod +x "$ISO_CUSTOM/etc/rc.local.d/archipelago-ssh.sh"
|
||||
|
||||
# Also create a systemd service that runs early
|
||||
mkdir -p "$ISO_CUSTOM/etc/systemd/system"
|
||||
cat > "$ISO_CUSTOM/etc/systemd/system/archipelago-ssh.service" <<'SERVICE_EOF'
|
||||
[Unit]
|
||||
Description=Archipelago SSH Setup
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/bin/bash -c 'apt-get update -qq && apt-get install -y openssh-server && echo "user:archipelago" | chpasswd && echo "root:archipelago" | chpasswd && sed -i "s/#PasswordAuthentication.*/PasswordAuthentication yes/" /etc/ssh/sshd_config && sed -i "s/PasswordAuthentication no/PasswordAuthentication yes/" /etc/ssh/sshd_config && systemctl restart ssh'
|
||||
RemainAfterExit=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
SERVICE_EOF
|
||||
|
||||
# Enable the service
|
||||
mkdir -p "$ISO_CUSTOM/etc/systemd/system/multi-user.target.wants"
|
||||
ln -sf ../archipelago-ssh.service "$ISO_CUSTOM/etc/systemd/system/multi-user.target.wants/archipelago-ssh.service"
|
||||
|
||||
# Create system-wide profile script that runs on ANY login
|
||||
mkdir -p "$ISO_CUSTOM/etc/profile.d"
|
||||
cat > "$ISO_CUSTOM/etc/profile.d/z99-archipelago.sh" <<'PROFILE_EOF'
|
||||
#!/bin/bash
|
||||
# Archipelago auto-start - runs on login (z99 = runs last)
|
||||
|
||||
# Only run once per session and only in interactive shells
|
||||
if [ -n "$ARCHIPELAGO_STARTED" ] || [ ! -t 0 ]; then
|
||||
return 0 2>/dev/null || exit 0
|
||||
fi
|
||||
export ARCHIPELAGO_STARTED=1
|
||||
|
||||
# Find archipelago directory
|
||||
BOOT_MEDIA=""
|
||||
for dev in /run/live/medium /lib/live/mount/medium /cdrom; do
|
||||
if [ -d "$dev/archipelago" ]; then
|
||||
BOOT_MEDIA="$dev"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Get IP address
|
||||
IP=$(hostname -I 2>/dev/null | awk '{print $1}')
|
||||
[ -z "$IP" ] && IP=$(ip -4 addr show | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | grep -v '127.0.0.1' | head -1)
|
||||
|
||||
# Show welcome banner ALWAYS
|
||||
clear
|
||||
echo ""
|
||||
echo " ╔═══════════════════════════════════════════════════════════╗"
|
||||
echo " ║ ║"
|
||||
echo " ║ 🏝️ ARCHIPELAGO BITCOIN NODE OS ║"
|
||||
echo " ║ ║"
|
||||
echo " ║ Your sovereign Bitcoin infrastructure ║"
|
||||
echo " ║ ║"
|
||||
echo " ╚═══════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
if [ -n "$IP" ]; then
|
||||
echo " ┌─────────────────────────────────────────────────────────────┐"
|
||||
echo " │ 🌐 Web UI: http://$IP:5678 │"
|
||||
echo " │ 📡 SSH: ssh user@$IP (password: archipelago) │"
|
||||
echo " └─────────────────────────────────────────────────────────────┘"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
if [ -z "$BOOT_MEDIA" ]; then
|
||||
echo " ⚠️ Boot media not found at /run/live/medium"
|
||||
echo ""
|
||||
echo " Manual commands:"
|
||||
echo " archipelago - Start backend server"
|
||||
echo " archipelago-menu - Open setup menu"
|
||||
echo ""
|
||||
return 0 2>/dev/null || exit 0
|
||||
fi
|
||||
|
||||
echo " 📍 Boot media: $BOOT_MEDIA"
|
||||
echo ""
|
||||
|
||||
# Install archipelago commands if not present
|
||||
if [ ! -f /usr/local/bin/archipelago ] && [ -f "$BOOT_MEDIA/archipelago/bin/archipelago" ]; then
|
||||
sudo cp "$BOOT_MEDIA/archipelago/bin/archipelago" /usr/local/bin/ 2>/dev/null
|
||||
sudo chmod +x /usr/local/bin/archipelago 2>/dev/null
|
||||
fi
|
||||
|
||||
if [ ! -f /usr/local/bin/archipelago-menu ] && [ -f "$BOOT_MEDIA/archipelago/bin/archipelago-menu" ]; then
|
||||
sudo cp "$BOOT_MEDIA/archipelago/bin/archipelago-menu" /usr/local/bin/ 2>/dev/null
|
||||
sudo chmod +x /usr/local/bin/archipelago-menu 2>/dev/null
|
||||
fi
|
||||
|
||||
# Copy scripts to /opt
|
||||
if [ ! -d /opt/archipelago/scripts ] && [ -d "$BOOT_MEDIA/archipelago/scripts" ]; then
|
||||
sudo mkdir -p /opt/archipelago
|
||||
sudo cp -r "$BOOT_MEDIA/archipelago/scripts" /opt/archipelago/ 2>/dev/null
|
||||
sudo chmod +x /opt/archipelago/scripts/*.sh 2>/dev/null
|
||||
fi
|
||||
|
||||
# Start web backend in background if available
|
||||
if command -v archipelago >/dev/null 2>&1; then
|
||||
if ! pgrep -f "archipelago" >/dev/null 2>&1; then
|
||||
echo " 🚀 Starting Archipelago backend on port 5678..."
|
||||
nohup archipelago >/tmp/archipelago.log 2>&1 &
|
||||
sleep 2
|
||||
else
|
||||
echo " ✅ Archipelago backend already running"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo " Commands:"
|
||||
echo " archipelago-menu - Open interactive setup menu"
|
||||
echo " archipelago - Start/restart backend server"
|
||||
echo ""
|
||||
read -p " Press Enter to open the setup menu (or Ctrl+C to skip)... "
|
||||
|
||||
# Launch the menu
|
||||
if [ -f /opt/archipelago/scripts/archipelago-menu.sh ]; then
|
||||
exec bash /opt/archipelago/scripts/archipelago-menu.sh
|
||||
elif [ -f "$BOOT_MEDIA/archipelago/scripts/archipelago-menu.sh" ]; then
|
||||
exec bash "$BOOT_MEDIA/archipelago/scripts/archipelago-menu.sh"
|
||||
fi
|
||||
PROFILE_EOF
|
||||
|
||||
chmod +x "$ISO_CUSTOM/etc/profile.d/z99-archipelago.sh"
|
||||
|
||||
# Also add to /etc/skel/.bashrc as fallback for new user sessions
|
||||
mkdir -p "$ISO_CUSTOM/etc/skel"
|
||||
cat >> "$ISO_CUSTOM/etc/skel/.bashrc" <<'BASHRC_EOF'
|
||||
|
||||
# Archipelago auto-start fallback
|
||||
if [ -z "$ARCHIPELAGO_STARTED" ] && [ -t 0 ]; then
|
||||
[ -f /etc/profile.d/z99-archipelago.sh ] && source /etc/profile.d/z99-archipelago.sh
|
||||
fi
|
||||
BASHRC_EOF
|
||||
|
||||
# Modify GRUB config for Archipelago branding
|
||||
echo ""
|
||||
echo "⚙️ Configuring boot..."
|
||||
|
||||
if [ -f "$ISO_CUSTOM/boot/grub/grub.cfg" ]; then
|
||||
sed -i.bak \
|
||||
-e 's/Debian GNU\/Linux/Archipelago Bitcoin Node OS/g' \
|
||||
-e 's/Live system/Archipelago Live/g' \
|
||||
"$ISO_CUSTOM/boot/grub/grub.cfg"
|
||||
fi
|
||||
|
||||
if [ -f "$ISO_CUSTOM/isolinux/menu.cfg" ]; then
|
||||
sed -i.bak \
|
||||
-e 's/Debian GNU\/Linux/Archipelago Bitcoin Node OS/g' \
|
||||
-e 's/Live system/Archipelago Live/g' \
|
||||
"$ISO_CUSTOM/isolinux/menu.cfg"
|
||||
fi
|
||||
|
||||
if [ -f "$ISO_CUSTOM/isolinux/live.cfg" ]; then
|
||||
sed -i.bak \
|
||||
-e 's/Live system/Archipelago Live/g' \
|
||||
"$ISO_CUSTOM/isolinux/live.cfg"
|
||||
fi
|
||||
|
||||
# Create final ISO
|
||||
OUTPUT_ISO="$OUTPUT_DIR/archipelago-debian-12-x86_64.iso"
|
||||
echo ""
|
||||
echo "🔥 Creating final bootable ISO..."
|
||||
|
||||
if command -v xorriso >/dev/null 2>&1; then
|
||||
# Create proper hybrid ISO with MBR for USB boot
|
||||
# Need to extract MBR from isolinux or use system syslinux
|
||||
|
||||
# Check for isohdpfx.bin in various locations
|
||||
ISOHDPFX=""
|
||||
for path in \
|
||||
"/usr/local/share/syslinux/isohdpfx.bin" \
|
||||
"/usr/share/syslinux/isohdpfx.bin" \
|
||||
"/opt/homebrew/share/syslinux/isohdpfx.bin" \
|
||||
"$ISO_CUSTOM/isolinux/isohdpfx.bin"; do
|
||||
if [ -f "$path" ]; then
|
||||
ISOHDPFX="$path"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$ISOHDPFX" ]; then
|
||||
echo "⚠️ No isohdpfx.bin found, extracting from isolinux.bin..."
|
||||
# Extract first 432 bytes from isolinux.bin as MBR
|
||||
dd if="$ISO_CUSTOM/isolinux/isolinux.bin" of="$WORK_DIR/isohdpfx.bin" bs=432 count=1 2>/dev/null
|
||||
ISOHDPFX="$WORK_DIR/isohdpfx.bin"
|
||||
fi
|
||||
|
||||
echo " Using MBR: $ISOHDPFX"
|
||||
|
||||
xorriso -as mkisofs -o "$OUTPUT_ISO" \
|
||||
-volid "ARCHIPELAGO" \
|
||||
-J -R \
|
||||
-isohybrid-mbr "$ISOHDPFX" \
|
||||
-c isolinux/boot.cat \
|
||||
-b isolinux/isolinux.bin \
|
||||
-no-emul-boot -boot-load-size 4 -boot-info-table \
|
||||
-eltorito-alt-boot \
|
||||
-e boot/grub/efi.img \
|
||||
-no-emul-boot \
|
||||
-isohybrid-gpt-basdat \
|
||||
"$ISO_CUSTOM"
|
||||
else
|
||||
echo "❌ xorriso not found. Please install it: brew install xorriso"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "✅ ISO created successfully!"
|
||||
echo ""
|
||||
echo "📀 Output: $OUTPUT_ISO"
|
||||
echo " Size: $(du -h "$OUTPUT_ISO" | cut -f1)"
|
||||
echo ""
|
||||
echo "🔥 This is Debian Live-based - reliable USB boot like StartOS!"
|
||||
echo ""
|
||||
echo "To create USB:"
|
||||
echo " 1. Use Balena Etcher to flash the ISO"
|
||||
echo " 2. Or: sudo dd if=$OUTPUT_ISO of=/dev/sdX bs=4M status=progress"
|
||||
echo ""
|
||||
@@ -1,410 +0,0 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Build Archipelago Auto-Installer ISO from LIVE SERVER STATE
|
||||
#
|
||||
# This captures the CURRENT STATE of the development server and packages
|
||||
# it into an auto-installer ISO - exactly like Start9/Umbrel.
|
||||
#
|
||||
# Usage: ./build-from-live-server.sh [dev-server-ip]
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
DEV_SERVER="${1:-archipelago@192.168.1.228}"
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
WORK_DIR="$SCRIPT_DIR/build/live-snapshot"
|
||||
OUTPUT_DIR="$SCRIPT_DIR/results"
|
||||
|
||||
echo "╔════════════════════════════════════════════════════════════════╗"
|
||||
echo "║ Building Archipelago ISO from LIVE SERVER ║"
|
||||
echo "╚════════════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
echo "📡 Development Server: $DEV_SERVER"
|
||||
echo ""
|
||||
|
||||
# Check for required tools
|
||||
CONTAINER_CMD=""
|
||||
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 "❌ Missing docker or podman"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v xorriso >/dev/null 2>&1; then
|
||||
echo "❌ Missing xorriso"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v ssh >/dev/null 2>&1; then
|
||||
echo "❌ Missing ssh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Using container runtime: $CONTAINER_CMD"
|
||||
echo ""
|
||||
|
||||
mkdir -p "$WORK_DIR"
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
# =============================================================================
|
||||
# STEP 1: Capture LIVE SERVER state
|
||||
# =============================================================================
|
||||
echo "📦 Step 1: Capturing live server state..."
|
||||
echo ""
|
||||
|
||||
SNAPSHOT_DIR="$WORK_DIR/live-server-snapshot"
|
||||
rm -rf "$SNAPSHOT_DIR"
|
||||
mkdir -p "$SNAPSHOT_DIR"
|
||||
|
||||
# Create directory structure
|
||||
mkdir -p "$SNAPSHOT_DIR/bin"
|
||||
mkdir -p "$SNAPSHOT_DIR/web-ui"
|
||||
mkdir -p "$SNAPSHOT_DIR/configs"
|
||||
mkdir -p "$SNAPSHOT_DIR/apps"
|
||||
mkdir -p "$SNAPSHOT_DIR/scripts"
|
||||
|
||||
# Capture backend binary
|
||||
echo " Capturing backend binary..."
|
||||
scp "$DEV_SERVER:/usr/local/bin/archipelago" "$SNAPSHOT_DIR/bin/" 2>/dev/null || {
|
||||
echo " ⚠️ Backend binary not found on server"
|
||||
echo " Using local build if available..."
|
||||
if [ -f "$SCRIPT_DIR/../core/target/release/archipelago" ]; then
|
||||
# Build on Linux if we're on macOS
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
echo " Building backend for Linux..."
|
||||
cd "$SCRIPT_DIR/../core"
|
||||
$CONTAINER_CMD build --platform linux/amd64 -t archipelago-backend -f - . <<'DOCKERFILE'
|
||||
FROM rust:1.93-bookworm as builder
|
||||
WORKDIR /build
|
||||
COPY . .
|
||||
RUN cargo build --release --bin archipelago
|
||||
DOCKERFILE
|
||||
BACKEND_CONTAINER=$($CONTAINER_CMD create --platform linux/amd64 archipelago-backend)
|
||||
$CONTAINER_CMD cp "$BACKEND_CONTAINER:/build/target/release/archipelago" "$SNAPSHOT_DIR/bin/"
|
||||
$CONTAINER_CMD rm "$BACKEND_CONTAINER"
|
||||
cd "$SCRIPT_DIR"
|
||||
else
|
||||
cp "$SCRIPT_DIR/../core/target/release/archipelago" "$SNAPSHOT_DIR/bin/"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
if [ -f "$SNAPSHOT_DIR/bin/archipelago" ]; then
|
||||
chmod +x "$SNAPSHOT_DIR/bin/archipelago"
|
||||
echo " ✅ Backend: $(du -h "$SNAPSHOT_DIR/bin/archipelago" | cut -f1)"
|
||||
else
|
||||
echo " ❌ No backend binary available"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Capture frontend (Web UI)
|
||||
echo " Capturing frontend (Web UI)..."
|
||||
rsync -az --delete "$DEV_SERVER:/opt/archipelago/web-ui/" "$SNAPSHOT_DIR/web-ui/" 2>/dev/null || {
|
||||
echo " ⚠️ Web UI not found on server"
|
||||
echo " Using local build..."
|
||||
if [ -d "$SCRIPT_DIR/../web/dist/neode-ui" ]; then
|
||||
cp -r "$SCRIPT_DIR/../web/dist/neode-ui/"* "$SNAPSHOT_DIR/web-ui/"
|
||||
elif [ -d "$SCRIPT_DIR/../neode-ui/dist" ]; then
|
||||
cp -r "$SCRIPT_DIR/../neode-ui/dist/"* "$SNAPSHOT_DIR/web-ui/"
|
||||
else
|
||||
echo " Building frontend..."
|
||||
cd "$SCRIPT_DIR/../neode-ui"
|
||||
npm run build
|
||||
cp -r "$SCRIPT_DIR/../web/dist/neode-ui/"* "$SNAPSHOT_DIR/web-ui/"
|
||||
cd "$SCRIPT_DIR"
|
||||
fi
|
||||
}
|
||||
|
||||
if [ -d "$SNAPSHOT_DIR/web-ui" ] && [ "$(ls -A "$SNAPSHOT_DIR/web-ui")" ]; then
|
||||
echo " ✅ Web UI: $(du -sh "$SNAPSHOT_DIR/web-ui" | cut -f1)"
|
||||
else
|
||||
echo " ❌ No web UI available"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Capture Nginx config
|
||||
echo " Capturing Nginx config..."
|
||||
scp "$DEV_SERVER:/etc/nginx/sites-available/default" "$SNAPSHOT_DIR/configs/nginx-default.conf" 2>/dev/null || \
|
||||
echo " ⚠️ Using default Nginx config"
|
||||
|
||||
# Capture systemd service
|
||||
echo " Capturing systemd service..."
|
||||
scp "$DEV_SERVER:/etc/systemd/system/archipelago.service" "$SNAPSHOT_DIR/configs/archipelago.service" 2>/dev/null || {
|
||||
echo " Creating default service..."
|
||||
cat > "$SNAPSHOT_DIR/configs/archipelago.service" <<'SERVICE'
|
||||
[Unit]
|
||||
Description=Archipelago Backend
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=archipelago
|
||||
Environment="ARCHIPELAGO_BIND=0.0.0.0:5678"
|
||||
Environment="ARCHIPELAGO_DEV_MODE=true"
|
||||
ExecStart=/usr/local/bin/archipelago
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
SERVICE
|
||||
}
|
||||
|
||||
# Capture app manifests
|
||||
echo " Capturing app manifests..."
|
||||
if [ -d "$SCRIPT_DIR/../apps" ]; then
|
||||
cp -r "$SCRIPT_DIR/../apps/"* "$SNAPSHOT_DIR/apps/" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
echo " ✅ Live server state captured"
|
||||
echo ""
|
||||
|
||||
# =============================================================================
|
||||
# STEP 2: Create base rootfs with captured state
|
||||
# =============================================================================
|
||||
echo "📦 Step 2: Creating base system with live server state..."
|
||||
echo ""
|
||||
|
||||
ROOTFS_TAR="$WORK_DIR/archipelago-rootfs-live.tar"
|
||||
|
||||
# Build rootfs with Docker/Podman
|
||||
cat > "$WORK_DIR/Dockerfile.rootfs" <<'DOCKERFILE'
|
||||
FROM debian:bookworm
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Install all required packages
|
||||
RUN apt-get update && apt-get install -y \
|
||||
linux-image-amd64 \
|
||||
grub-efi-amd64 \
|
||||
grub-efi-amd64-signed \
|
||||
shim-signed \
|
||||
systemd \
|
||||
systemd-sysv \
|
||||
dbus \
|
||||
sudo \
|
||||
network-manager \
|
||||
openssh-server \
|
||||
nginx \
|
||||
podman \
|
||||
curl \
|
||||
wget \
|
||||
htop \
|
||||
vim-tiny \
|
||||
ca-certificates \
|
||||
locales \
|
||||
console-setup \
|
||||
keyboard-configuration \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Configure locale
|
||||
RUN echo "en_US.UTF-8 UTF-8" > /etc/locale.gen && locale-gen
|
||||
|
||||
# Create archipelago user
|
||||
RUN useradd -m -s /bin/bash -G sudo archipelago && \
|
||||
echo "archipelago:archipelago" | chpasswd && \
|
||||
echo "archipelago ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/archipelago
|
||||
|
||||
# Set hostname
|
||||
RUN echo "archipelago" > /etc/hostname
|
||||
|
||||
# Configure SSH
|
||||
RUN mkdir -p /etc/ssh && \
|
||||
sed -i 's/#PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config || true && \
|
||||
sed -i 's/#PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config || true
|
||||
|
||||
# Create directories
|
||||
RUN mkdir -p /var/lib/archipelago/{data,config,containers} && \
|
||||
mkdir -p /etc/archipelago && \
|
||||
mkdir -p /opt/archipelago/{bin,scripts,web-ui} && \
|
||||
chown -R archipelago:archipelago /var/lib/archipelago /opt/archipelago
|
||||
|
||||
# Clean up
|
||||
RUN apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
DOCKERFILE
|
||||
|
||||
echo " Building base rootfs..."
|
||||
$CONTAINER_CMD build --platform linux/amd64 -t archipelago-rootfs-live -f "$WORK_DIR/Dockerfile.rootfs" "$WORK_DIR"
|
||||
|
||||
echo " Exporting filesystem..."
|
||||
$CONTAINER_CMD create --platform linux/amd64 --name archipelago-rootfs-live-tmp archipelago-rootfs-live
|
||||
$CONTAINER_CMD export archipelago-rootfs-live-tmp > "$ROOTFS_TAR"
|
||||
$CONTAINER_CMD rm archipelago-rootfs-live-tmp
|
||||
|
||||
echo " ✅ Base rootfs created: $(du -h "$ROOTFS_TAR" | cut -f1)"
|
||||
echo ""
|
||||
|
||||
# =============================================================================
|
||||
# STEP 3: Inject live server files into rootfs
|
||||
# =============================================================================
|
||||
echo "📦 Step 3: Injecting live server files..."
|
||||
echo ""
|
||||
|
||||
ROOTFS_DIR="$WORK_DIR/rootfs-extract"
|
||||
rm -rf "$ROOTFS_DIR"
|
||||
mkdir -p "$ROOTFS_DIR"
|
||||
|
||||
echo " Extracting rootfs..."
|
||||
tar -xf "$ROOTFS_TAR" -C "$ROOTFS_DIR"
|
||||
|
||||
echo " Copying backend binary..."
|
||||
cp "$SNAPSHOT_DIR/bin/archipelago" "$ROOTFS_DIR/usr/local/bin/"
|
||||
chmod +x "$ROOTFS_DIR/usr/local/bin/archipelago"
|
||||
|
||||
echo " Copying web UI..."
|
||||
rm -rf "$ROOTFS_DIR/opt/archipelago/web-ui"
|
||||
mkdir -p "$ROOTFS_DIR/opt/archipelago/web-ui"
|
||||
cp -r "$SNAPSHOT_DIR/web-ui/"* "$ROOTFS_DIR/opt/archipelago/web-ui/"
|
||||
|
||||
echo " Configuring Nginx..."
|
||||
if [ -f "$SNAPSHOT_DIR/configs/nginx-default.conf" ]; then
|
||||
cp "$SNAPSHOT_DIR/configs/nginx-default.conf" "$ROOTFS_DIR/etc/nginx/sites-available/archipelago"
|
||||
else
|
||||
cat > "$ROOTFS_DIR/etc/nginx/sites-available/archipelago" <<'NGINXCONF'
|
||||
server {
|
||||
listen 80 default_server;
|
||||
listen [::]:80 default_server;
|
||||
|
||||
root /opt/archipelago/web-ui;
|
||||
index index.html;
|
||||
|
||||
server_name _;
|
||||
|
||||
# Proxy API requests to backend
|
||||
location /rpc/ {
|
||||
proxy_pass http://localhost:5678/rpc/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
|
||||
location /ws/ {
|
||||
proxy_pass http://localhost:5678/ws/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
location /health {
|
||||
proxy_pass http://localhost:5678/health;
|
||||
}
|
||||
|
||||
# Serve static files
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
NGINXCONF
|
||||
fi
|
||||
|
||||
rm -f "$ROOTFS_DIR/etc/nginx/sites-enabled/default"
|
||||
ln -sf /etc/nginx/sites-available/archipelago "$ROOTFS_DIR/etc/nginx/sites-enabled/archipelago"
|
||||
|
||||
echo " Configuring systemd service..."
|
||||
cp "$SNAPSHOT_DIR/configs/archipelago.service" "$ROOTFS_DIR/etc/systemd/system/archipelago.service"
|
||||
|
||||
# Enable services (create symlinks)
|
||||
mkdir -p "$ROOTFS_DIR/etc/systemd/system/multi-user.target.wants"
|
||||
ln -sf /etc/systemd/system/archipelago.service "$ROOTFS_DIR/etc/systemd/system/multi-user.target.wants/archipelago.service"
|
||||
ln -sf /lib/systemd/system/NetworkManager.service "$ROOTFS_DIR/etc/systemd/system/multi-user.target.wants/NetworkManager.service"
|
||||
ln -sf /lib/systemd/system/ssh.service "$ROOTFS_DIR/etc/systemd/system/multi-user.target.wants/ssh.service"
|
||||
ln -sf /lib/systemd/system/nginx.service "$ROOTFS_DIR/etc/systemd/system/multi-user.target.wants/nginx.service"
|
||||
|
||||
echo " Copying app manifests..."
|
||||
if [ -d "$SNAPSHOT_DIR/apps" ]; then
|
||||
mkdir -p "$ROOTFS_DIR/etc/archipelago/apps"
|
||||
cp -r "$SNAPSHOT_DIR/apps/"* "$ROOTFS_DIR/etc/archipelago/apps/" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
echo " Repacking rootfs with live server state..."
|
||||
cd "$ROOTFS_DIR"
|
||||
tar -cf "$ROOTFS_TAR" .
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
echo " ✅ Live server files injected"
|
||||
echo ""
|
||||
|
||||
# =============================================================================
|
||||
# STEP 4: Create installer ISO (reuse existing auto-installer logic)
|
||||
# =============================================================================
|
||||
echo "📦 Step 4: Creating auto-installer ISO..."
|
||||
echo ""
|
||||
|
||||
# Now call the existing auto-installer script but skip the rootfs build
|
||||
# since we already have it with live server state
|
||||
export ROOTFS_TAR="$ROOTFS_TAR"
|
||||
export SKIP_ROOTFS_BUILD="true"
|
||||
|
||||
# Run the rest of build-auto-installer-iso.sh logic here...
|
||||
# (Download Debian Live base, create auto-install.sh, package ISO)
|
||||
|
||||
echo " Downloading Debian Live base..."
|
||||
BASE_ISO="$WORK_DIR/debian-live-installer.iso"
|
||||
if [ ! -f "$BASE_ISO" ] || [ $(stat -f%z "$BASE_ISO" 2>/dev/null || stat -c%s "$BASE_ISO" 2>/dev/null || echo 0) -lt 100000000 ]; then
|
||||
curl -L -# -o "$BASE_ISO" \
|
||||
"https://cdimage.debian.org/debian-cd/current-live/amd64/iso-hybrid/debian-live-12.9.0-amd64-standard.iso"
|
||||
fi
|
||||
|
||||
echo " Extracting installer base..."
|
||||
INSTALLER_ISO="$WORK_DIR/installer-iso"
|
||||
rm -rf "$INSTALLER_ISO"
|
||||
mkdir -p "$INSTALLER_ISO"
|
||||
cd "$INSTALLER_ISO"
|
||||
7z x -y "$BASE_ISO" >/dev/null 2>&1
|
||||
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
# Copy archipelago files
|
||||
ARCH_DIR="$INSTALLER_ISO/archipelago"
|
||||
mkdir -p "$ARCH_DIR"
|
||||
cp "$ROOTFS_TAR" "$ARCH_DIR/rootfs.tar"
|
||||
|
||||
echo " ✅ Archipelago components added (rootfs: $(du -h "$ARCH_DIR/rootfs.tar" | cut -f1))"
|
||||
echo ""
|
||||
|
||||
# Continue with ISO creation...
|
||||
echo "📦 Step 5: Creating final bootable ISO..."
|
||||
|
||||
# (Rest of xorriso logic from build-auto-installer-iso.sh)
|
||||
|
||||
OUTPUT_ISO="$OUTPUT_DIR/archipelago-live-$(date +%Y%m%d-%H%M%S).iso"
|
||||
|
||||
# Get MBR
|
||||
ISOHDPFX="$WORK_DIR/isohdpfx.bin"
|
||||
dd if="$INSTALLER_ISO/isolinux/isolinux.bin" of="$ISOHDPFX" bs=432 count=1 2>/dev/null
|
||||
|
||||
xorriso -as mkisofs -o "$OUTPUT_ISO" \
|
||||
-volid "ARCHIPELAGO" \
|
||||
-J -R \
|
||||
-isohybrid-mbr "$ISOHDPFX" \
|
||||
-c isolinux/boot.cat \
|
||||
-b isolinux/isolinux.bin \
|
||||
-no-emul-boot -boot-load-size 4 -boot-info-table \
|
||||
-eltorito-alt-boot \
|
||||
-e boot/grub/efi.img \
|
||||
-no-emul-boot \
|
||||
-isohybrid-gpt-basdat \
|
||||
"$INSTALLER_ISO"
|
||||
|
||||
echo ""
|
||||
echo "╔════════════════════════════════════════════════════════════════╗"
|
||||
echo "║ ✅ LIVE SERVER ISO CREATED! ║"
|
||||
echo "╚════════════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
echo "📀 Output: $OUTPUT_ISO"
|
||||
echo " Size: $(du -h "$OUTPUT_ISO" | cut -f1)"
|
||||
echo " MD5: $(md5sum "$OUTPUT_ISO" 2>/dev/null || md5 "$OUTPUT_ISO" | awk '{print $NF}')"
|
||||
echo ""
|
||||
echo "🎉 This ISO contains the EXACT state of your dev server!"
|
||||
echo ""
|
||||
echo "To flash:"
|
||||
echo " cd image-recipe && ./write-usb-dd.sh /dev/diskX"
|
||||
echo ""
|
||||
@@ -1,355 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
MAX_IMG_SECTORS=7217792 # 4GB
|
||||
|
||||
echo "==== StartOS Image Build ===="
|
||||
|
||||
echo "Building for architecture: $IB_TARGET_ARCH"
|
||||
|
||||
base_dir="$(dirname "$(readlink -f "$0")")"
|
||||
prep_results_dir="$base_dir/images-prep"
|
||||
if systemd-detect-virt -qc; then
|
||||
RESULTS_DIR="/srv/artifacts"
|
||||
else
|
||||
RESULTS_DIR="$base_dir/results"
|
||||
fi
|
||||
echo "Saving results in: $RESULTS_DIR"
|
||||
|
||||
IMAGE_BASENAME=startos-${VERSION_FULL}_${IB_TARGET_PLATFORM}
|
||||
|
||||
mkdir -p $prep_results_dir
|
||||
|
||||
cd $prep_results_dir
|
||||
|
||||
QEMU_ARCH=${IB_TARGET_ARCH}
|
||||
BOOTLOADERS=grub-efi,syslinux
|
||||
if [ "$QEMU_ARCH" = 'amd64' ]; then
|
||||
QEMU_ARCH=x86_64
|
||||
elif [ "$QEMU_ARCH" = 'arm64' ]; then
|
||||
QEMU_ARCH=aarch64
|
||||
BOOTLOADERS=grub-efi
|
||||
fi
|
||||
NON_FREE=
|
||||
if [[ "${IB_TARGET_PLATFORM}" =~ -nonfree$ ]] || [ "${IB_TARGET_PLATFORM}" = "raspberrypi" ]; then
|
||||
NON_FREE=1
|
||||
fi
|
||||
IMAGE_TYPE=iso
|
||||
if [ "${IB_TARGET_PLATFORM}" = "raspberrypi" ] || [ "${IB_TARGET_PLATFORM}" = "rockchip64" ]; then
|
||||
IMAGE_TYPE=img
|
||||
fi
|
||||
|
||||
ARCHIVE_AREAS="main contrib"
|
||||
if [ "$NON_FREE" = 1 ]; then
|
||||
if [ "$IB_SUITE" = "bullseye" ]; then
|
||||
ARCHIVE_AREAS="$ARCHIVE_AREAS non-free"
|
||||
elif [ "$IB_SUITE" = "bookworm" ]; then
|
||||
ARCHIVE_AREAS="$ARCHIVE_AREAS non-free-firmware"
|
||||
fi
|
||||
fi
|
||||
|
||||
PLATFORM_CONFIG_EXTRAS=
|
||||
if [ "${IB_TARGET_PLATFORM}" = "raspberrypi" ]; then
|
||||
PLATFORM_CONFIG_EXTRAS="$PLATFORM_CONFIG_EXTRAS --firmware-binary false"
|
||||
PLATFORM_CONFIG_EXTRAS="$PLATFORM_CONFIG_EXTRAS --firmware-chroot false"
|
||||
# BEGIN stupid ugly hack
|
||||
# The actual name of the package is `raspberrypi-kernel`
|
||||
# live-build determines thte name of the package for the kernel by combining the `linux-packages` flag, with the `linux-flavours` flag
|
||||
# the `linux-flavours` flag defaults to the architecture, so there's no way to remove the suffix.
|
||||
# So we're doing this, cause thank the gods our package name contains a hypen. Cause if it didn't we'd be SOL
|
||||
PLATFORM_CONFIG_EXTRAS="$PLATFORM_CONFIG_EXTRAS --linux-packages raspberrypi"
|
||||
PLATFORM_CONFIG_EXTRAS="$PLATFORM_CONFIG_EXTRAS --linux-flavours kernel"
|
||||
# END stupid ugly hack
|
||||
elif [ "${IB_TARGET_PLATFORM}" = "rockchip64" ]; then
|
||||
PLATFORM_CONFIG_EXTRAS="$PLATFORM_CONFIG_EXTRAS --linux-flavours rockchip64"
|
||||
fi
|
||||
|
||||
cat > /etc/wgetrc << EOF
|
||||
retry_connrefused = on
|
||||
tries = 100
|
||||
EOF
|
||||
lb config \
|
||||
--iso-application "StartOS v${VERSION_FULL} ${IB_TARGET_ARCH}" \
|
||||
--iso-volume "StartOS v${VERSION} ${IB_TARGET_ARCH}" \
|
||||
--iso-preparer "START9 LABS; HTTPS://START9.COM" \
|
||||
--iso-publisher "START9 LABS; HTTPS://START9.COM" \
|
||||
--backports true \
|
||||
--bootappend-live "boot=live noautologin" \
|
||||
--bootloaders $BOOTLOADERS \
|
||||
--mirror-bootstrap "https://deb.debian.org/debian/" \
|
||||
--mirror-chroot "https://deb.debian.org/debian/" \
|
||||
--mirror-chroot-security "https://security.debian.org/debian-security" \
|
||||
-d ${IB_SUITE} \
|
||||
-a ${IB_TARGET_ARCH} \
|
||||
--bootstrap-qemu-arch ${IB_TARGET_ARCH} \
|
||||
--bootstrap-qemu-static /usr/bin/qemu-${QEMU_ARCH}-static \
|
||||
--archive-areas "${ARCHIVE_AREAS}" \
|
||||
$PLATFORM_CONFIG_EXTRAS
|
||||
|
||||
# Overlays
|
||||
|
||||
mkdir -p config/includes.chroot/deb
|
||||
cp $base_dir/deb/${IMAGE_BASENAME}.deb config/includes.chroot/deb/
|
||||
|
||||
if [ "${IB_TARGET_PLATFORM}" = "raspberrypi" ]; then
|
||||
cp -r $base_dir/raspberrypi/squashfs/* config/includes.chroot/
|
||||
fi
|
||||
|
||||
mkdir -p config/includes.chroot/etc
|
||||
echo start > config/includes.chroot/etc/hostname
|
||||
cat > config/includes.chroot/etc/hosts << EOT
|
||||
127.0.0.1 localhost start
|
||||
::1 localhost start ip6-localhost ip6-loopback
|
||||
ff02::1 ip6-allnodes
|
||||
ff02::2 ip6-allrouters
|
||||
EOT
|
||||
|
||||
# Bootloaders
|
||||
|
||||
rm -rf config/bootloaders
|
||||
cp -r /usr/share/live/build/bootloaders config/bootloaders
|
||||
|
||||
cat > config/bootloaders/syslinux/syslinux.cfg << EOF
|
||||
include menu.cfg
|
||||
default vesamenu.c32
|
||||
prompt 0
|
||||
timeout 50
|
||||
EOF
|
||||
|
||||
cat > config/bootloaders/isolinux/isolinux.cfg << EOF
|
||||
include menu.cfg
|
||||
default vesamenu.c32
|
||||
prompt 0
|
||||
timeout 50
|
||||
EOF
|
||||
|
||||
rm config/bootloaders/syslinux_common/splash.svg
|
||||
cp $base_dir/splash.png config/bootloaders/syslinux_common/splash.png
|
||||
cp $base_dir/splash.png config/bootloaders/isolinux/splash.png
|
||||
cp $base_dir/splash.png config/bootloaders/grub-pc/splash.png
|
||||
|
||||
sed -i -e '2i set timeout=5' config/bootloaders/grub-pc/config.cfg
|
||||
|
||||
# Archives
|
||||
|
||||
mkdir -p config/archives
|
||||
|
||||
if [ "${IB_TARGET_PLATFORM}" = "raspberrypi" ]; then
|
||||
curl -fsSL https://archive.raspberrypi.org/debian/raspberrypi.gpg.key | gpg --dearmor -o config/archives/raspi.key
|
||||
echo "deb https://archive.raspberrypi.org/debian/ bullseye main" > config/archives/raspi.list
|
||||
fi
|
||||
|
||||
if [ "${IB_SUITE}" = "bullseye" ]; then
|
||||
cat > config/archives/backports.pref <<- EOF
|
||||
Package: *
|
||||
Pin: release a=bullseye-backports
|
||||
Pin-Priority: 500
|
||||
EOF
|
||||
fi
|
||||
|
||||
if [ "${IB_TARGET_PLATFORM}" = "rockchip64" ]; then
|
||||
curl -fsSL https://apt.armbian.com/armbian.key | gpg --dearmor -o config/archives/armbian.key
|
||||
echo "deb https://apt.armbian.com/ ${IB_SUITE} main" > config/archives/armbian.list
|
||||
fi
|
||||
|
||||
curl -fsSL https://deb.torproject.org/torproject.org/A3C4F0F979CAA22CDBA8F512EE8CBC9E886DDD89.asc > config/archives/tor.key
|
||||
echo "deb [arch=${IB_TARGET_ARCH} signed-by=/etc/apt/trusted.gpg.d/tor.key.gpg] https://deb.torproject.org/torproject.org ${IB_SUITE} main" > config/archives/tor.list
|
||||
|
||||
curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o config/archives/docker.key
|
||||
echo "deb [arch=${IB_TARGET_ARCH} signed-by=/etc/apt/trusted.gpg.d/docker.key.gpg] https://download.docker.com/linux/debian ${IB_SUITE} stable" > config/archives/docker.list
|
||||
|
||||
echo "deb http://deb.debian.org/debian/ trixie main contrib" > config/archives/trixie.list
|
||||
cat > config/archives/trixie.pref <<- EOF
|
||||
Package: *
|
||||
Pin: release n=trixie
|
||||
Pin-Priority: 100
|
||||
|
||||
Package: podman
|
||||
Pin: release n=trixie
|
||||
Pin-Priority: 600
|
||||
EOF
|
||||
|
||||
# Dependencies
|
||||
|
||||
## Base dependencies
|
||||
dpkg-deb --fsys-tarfile $base_dir/deb/${IMAGE_BASENAME}.deb | tar --to-stdout -xvf - ./usr/lib/startos/depends > config/package-lists/embassy-depends.list.chroot
|
||||
|
||||
## Firmware
|
||||
if [ "$NON_FREE" = 1 ]; then
|
||||
echo 'firmware-iwlwifi firmware-misc-nonfree firmware-brcm80211 firmware-realtek firmware-atheros firmware-libertas firmware-amd-graphics' > config/package-lists/nonfree.list.chroot
|
||||
fi
|
||||
|
||||
if [ "${IB_TARGET_PLATFORM}" = "raspberrypi" ]; then
|
||||
echo 'raspberrypi-bootloader rpi-update parted' > config/package-lists/bootloader.list.chroot
|
||||
else
|
||||
echo 'grub-efi grub2-common' > config/package-lists/bootloader.list.chroot
|
||||
fi
|
||||
if [ "${IB_TARGET_ARCH}" = "amd64" ] || [ "${IB_TARGET_ARCH}" = "i386" ]; then
|
||||
echo 'grub-pc-bin' >> config/package-lists/bootloader.list.chroot
|
||||
fi
|
||||
|
||||
cat > config/hooks/normal/9000-install-startos.hook.chroot << EOF
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
apt-get install -y /deb/${IMAGE_BASENAME}.deb
|
||||
rm -rf /deb
|
||||
|
||||
if [ "${IB_SUITE}" = bookworm ]; then
|
||||
echo 'deb https://deb.debian.org/debian/ bullseye main' > /etc/apt/sources.list.d/bullseye.list
|
||||
apt-get update
|
||||
apt-get install -y postgresql-13
|
||||
rm /etc/apt/sources.list.d/bullseye.list
|
||||
apt-get update
|
||||
fi
|
||||
|
||||
if [ "${IB_TARGET_PLATFORM}" = "raspberrypi" ]; then
|
||||
for f in /usr/lib/modules/*; do
|
||||
v=\${f#/usr/lib/modules/}
|
||||
echo "Configuring raspi kernel '\$v'"
|
||||
extract-ikconfig "/usr/lib/modules/\$v/kernel/kernel/configs.ko.xz" > /boot/config-\$v
|
||||
update-initramfs -c -k \$v
|
||||
done
|
||||
ln -sf /usr/bin/pi-beep /usr/local/bin/beep
|
||||
fi
|
||||
|
||||
useradd --shell /bin/bash -G embassy -m start9
|
||||
echo start9:embassy | chpasswd
|
||||
usermod -aG sudo start9
|
||||
|
||||
echo "start9 ALL=(ALL:ALL) NOPASSWD: ALL" | sudo tee "/etc/sudoers.d/010_start9-nopasswd"
|
||||
|
||||
if [ "${IB_TARGET_PLATFORM}" != "raspberrypi" ]; then
|
||||
/usr/lib/startos/scripts/enable-kiosk
|
||||
fi
|
||||
|
||||
if ! [[ "${IB_OS_ENV}" =~ (^|-)dev($|-) ]]; then
|
||||
passwd -l start9
|
||||
fi
|
||||
|
||||
EOF
|
||||
|
||||
SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH:-$(date '+%s')}"
|
||||
|
||||
lb bootstrap
|
||||
lb chroot
|
||||
lb installer
|
||||
lb binary_chroot
|
||||
lb chroot_prep install all mode-apt-install-binary mode-archives-chroot
|
||||
ln -sf /run/systemd/resolve/stub-resolv.conf chroot/chroot/etc/resolv.conf
|
||||
lb binary_rootfs
|
||||
|
||||
cp $prep_results_dir/binary/live/filesystem.squashfs $RESULTS_DIR/$IMAGE_BASENAME.squashfs
|
||||
|
||||
if [ "${IMAGE_TYPE}" = iso ]; then
|
||||
|
||||
lb binary_manifest
|
||||
lb binary_package-lists
|
||||
lb binary_linux-image
|
||||
lb binary_memtest
|
||||
lb binary_grub-legacy
|
||||
lb binary_grub-pc
|
||||
lb binary_grub_cfg
|
||||
lb binary_syslinux
|
||||
lb binary_disk
|
||||
lb binary_loadlin
|
||||
lb binary_win32-loader
|
||||
lb binary_includes
|
||||
lb binary_grub-efi
|
||||
lb binary_hooks
|
||||
lb binary_checksums
|
||||
find binary -newermt "$(date -d@${SOURCE_DATE_EPOCH} '+%Y-%m-%d %H:%M:%S')" -printf "%y %p\n" -exec touch '{}' -d@${SOURCE_DATE_EPOCH} --no-dereference ';' > binary.modified_timestamps
|
||||
lb binary_iso
|
||||
lb binary_onie
|
||||
lb binary_netboot
|
||||
lb binary_tar
|
||||
lb binary_hdd
|
||||
lb binary_zsync
|
||||
lb chroot_prep remove all mode-archives-chroot
|
||||
lb source
|
||||
|
||||
mv $prep_results_dir/live-image-${IB_TARGET_ARCH}.hybrid.iso $RESULTS_DIR/$IMAGE_BASENAME.iso
|
||||
|
||||
elif [ "${IMAGE_TYPE}" = img ]; then
|
||||
|
||||
function partition_for () {
|
||||
if [[ "$1" =~ [0-9]+$ ]]; then
|
||||
echo "$1p$2"
|
||||
else
|
||||
echo "$1$2"
|
||||
fi
|
||||
}
|
||||
|
||||
ROOT_PART_END=$MAX_IMG_SECTORS
|
||||
TARGET_NAME=$prep_results_dir/${IMAGE_BASENAME}.img
|
||||
TARGET_SIZE=$[($ROOT_PART_END+1)*512]
|
||||
truncate -s $TARGET_SIZE $TARGET_NAME
|
||||
(
|
||||
echo o
|
||||
echo x
|
||||
echo i
|
||||
echo "0xcb15ae4d"
|
||||
echo r
|
||||
echo n
|
||||
echo p
|
||||
echo 1
|
||||
echo 2048
|
||||
echo 526335
|
||||
echo t
|
||||
echo c
|
||||
echo n
|
||||
echo p
|
||||
echo 2
|
||||
echo 526336
|
||||
echo $ROOT_PART_END
|
||||
echo a
|
||||
echo 1
|
||||
echo w
|
||||
) | fdisk $TARGET_NAME
|
||||
OUTPUT_DEVICE=$(losetup --show -fP $TARGET_NAME)
|
||||
mkfs.ext4 `partition_for ${OUTPUT_DEVICE} 2`
|
||||
mkfs.vfat `partition_for ${OUTPUT_DEVICE} 1`
|
||||
|
||||
TMPDIR=$(mktemp -d)
|
||||
|
||||
mount `partition_for ${OUTPUT_DEVICE} 2` $TMPDIR
|
||||
mkdir $TMPDIR/boot
|
||||
mount `partition_for ${OUTPUT_DEVICE} 1` $TMPDIR/boot
|
||||
unsquashfs -f -d $TMPDIR $prep_results_dir/binary/live/filesystem.squashfs
|
||||
|
||||
if [ "${IB_TARGET_PLATFORM}" = "raspberrypi" ]; then
|
||||
sed -i 's| boot=embassy| init=/usr/lib/startos/scripts/init_resize\.sh|' $TMPDIR/boot/cmdline.txt
|
||||
rsync -a $base_dir/raspberrypi/img/ $TMPDIR/
|
||||
fi
|
||||
|
||||
umount $TMPDIR/boot
|
||||
umount $TMPDIR
|
||||
|
||||
e2fsck -fy `partition_for ${OUTPUT_DEVICE} 2`
|
||||
resize2fs -M `partition_for ${OUTPUT_DEVICE} 2`
|
||||
|
||||
BLOCK_COUNT=$(dumpe2fs -h `partition_for ${OUTPUT_DEVICE} 2` | awk '/^Block count:/ { print $3 }')
|
||||
BLOCK_SIZE=$(dumpe2fs -h `partition_for ${OUTPUT_DEVICE} 2` | awk '/^Block size:/ { print $3 }')
|
||||
SECTOR_LEN=$[$BLOCK_COUNT*$BLOCK_SIZE/512]
|
||||
|
||||
losetup -d $OUTPUT_DEVICE
|
||||
|
||||
(
|
||||
echo d
|
||||
echo 2
|
||||
echo n
|
||||
echo p
|
||||
echo 2
|
||||
echo 526336
|
||||
echo +$SECTOR_LEN
|
||||
echo w
|
||||
) | fdisk $TARGET_NAME
|
||||
|
||||
ROOT_PART_END=$[526336+$SECTOR_LEN]
|
||||
TARGET_SIZE=$[($ROOT_PART_END+1)*512]
|
||||
truncate -s $TARGET_SIZE $TARGET_NAME
|
||||
|
||||
mv $TARGET_NAME $RESULTS_DIR/$IMAGE_BASENAME.img
|
||||
|
||||
fi
|
||||
@@ -7,7 +7,7 @@ Wants=network-online.target
|
||||
Type=notify
|
||||
User=archipelago
|
||||
Environment="ARCHIPELAGO_BIND=0.0.0.0:5678"
|
||||
Environment="ARCHIPELAGO_DEV_MODE=true"
|
||||
# DEV_MODE disabled in production — enabled via override.conf on dev servers
|
||||
Environment="XDG_RUNTIME_DIR=/run/user/1000"
|
||||
ExecStartPre=/bin/bash -c 'mkdir -p /var/lib/archipelago && echo "ARCHIPELAGO_HOST_IP=$(hostname -I 2>/dev/null | awk "{print $$1}")" > /var/lib/archipelago/host-ip.env'
|
||||
ExecStart=/usr/local/bin/archipelago
|
||||
|
||||
@@ -37,7 +37,7 @@ server {
|
||||
|
||||
# AIUI Claude API proxy — requires valid session cookie
|
||||
location /aiui/api/claude/ {
|
||||
if ($cookie_session = "") {
|
||||
if ($cookie_session_id = "") {
|
||||
return 401 '{"error":"Unauthorized"}';
|
||||
}
|
||||
proxy_pass http://127.0.0.1:3142/;
|
||||
@@ -54,7 +54,7 @@ server {
|
||||
|
||||
# AIUI OpenRouter API proxy — requires valid session cookie
|
||||
location /aiui/api/openrouter/ {
|
||||
if ($cookie_session = "") {
|
||||
if ($cookie_session_id = "") {
|
||||
return 401 '{"error":"Unauthorized"}';
|
||||
}
|
||||
proxy_pass https://openrouter.ai/api/;
|
||||
@@ -69,7 +69,7 @@ server {
|
||||
|
||||
# AIUI Ollama (local AI) proxy — localhost:11434
|
||||
location /aiui/api/ollama/ {
|
||||
if ($cookie_session = "") {
|
||||
if ($cookie_session_id = "") {
|
||||
return 401 '{"error":"Unauthorized"}';
|
||||
}
|
||||
proxy_pass http://127.0.0.1:11434/;
|
||||
@@ -85,7 +85,7 @@ server {
|
||||
|
||||
# AIUI web search proxy — SearXNG on port 8888
|
||||
location /aiui/api/web-search {
|
||||
if ($cookie_session = "") {
|
||||
if ($cookie_session_id = "") {
|
||||
return 401 '{"error":"Unauthorized"}';
|
||||
}
|
||||
proxy_pass http://127.0.0.1:8888/search;
|
||||
@@ -153,10 +153,12 @@ server {
|
||||
}
|
||||
|
||||
location /lnd-connect-info {
|
||||
# Requires authenticated session — exposes LND admin macaroon
|
||||
if ($cookie_session_id = "") { return 401; }
|
||||
proxy_pass http://127.0.0.1:5678/lnd-connect-info;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
add_header Access-Control-Allow-Origin *;
|
||||
proxy_set_header Cookie $http_cookie;
|
||||
}
|
||||
|
||||
# Content sharing — peer access over Tor (no auth)
|
||||
@@ -327,7 +329,7 @@ server {
|
||||
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
||||
}
|
||||
location /app/onlyoffice/ {
|
||||
proxy_pass http://127.0.0.1:9980/;
|
||||
proxy_pass http://127.0.0.1:8044/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
@@ -478,7 +480,9 @@ server {
|
||||
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
||||
}
|
||||
location /app/tailscale/ {
|
||||
proxy_pass http://127.0.0.1:8240/;
|
||||
# Tailscale has no web UI — managed via CLI/Tailscale app
|
||||
# proxy_pass placeholder for future Tailscale admin UI
|
||||
return 503;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
@@ -553,7 +557,7 @@ server {
|
||||
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
||||
}
|
||||
location /app/nginx-proxy-manager/ {
|
||||
proxy_pass http://127.0.0.1:81/;
|
||||
proxy_pass http://127.0.0.1:8181/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
@@ -731,7 +735,7 @@ server {
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
}
|
||||
location /aiui/api/claude/ {
|
||||
if ($cookie_session = "") {
|
||||
if ($cookie_session_id = "") {
|
||||
return 401 '{"error":"Unauthorized"}';
|
||||
}
|
||||
proxy_pass http://127.0.0.1:3142/;
|
||||
@@ -746,7 +750,7 @@ server {
|
||||
proxy_send_timeout 120s;
|
||||
}
|
||||
location /aiui/api/ollama/ {
|
||||
if ($cookie_session = "") {
|
||||
if ($cookie_session_id = "") {
|
||||
return 401 '{"error":"Unauthorized"}';
|
||||
}
|
||||
proxy_pass http://127.0.0.1:11434/;
|
||||
@@ -760,7 +764,7 @@ server {
|
||||
# Connection header managed by nginx default
|
||||
}
|
||||
location /aiui/api/openrouter/ {
|
||||
if ($cookie_session = "") {
|
||||
if ($cookie_session_id = "") {
|
||||
return 401 '{"error":"Unauthorized"}';
|
||||
}
|
||||
proxy_pass https://openrouter.ai/api/;
|
||||
@@ -803,10 +807,12 @@ server {
|
||||
}
|
||||
|
||||
location /lnd-connect-info {
|
||||
# Requires authenticated session — exposes LND admin macaroon
|
||||
if ($cookie_session_id = "") { return 401; }
|
||||
proxy_pass http://127.0.0.1:5678/lnd-connect-info;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
add_header Access-Control-Allow-Origin *;
|
||||
proxy_set_header Cookie $http_cookie;
|
||||
}
|
||||
|
||||
# Content sharing — peer access over Tor (no auth)
|
||||
|
||||
@@ -103,7 +103,7 @@ location /app/lnd/ {
|
||||
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
||||
}
|
||||
location /app/onlyoffice/ {
|
||||
proxy_pass http://127.0.0.1:9980/;
|
||||
proxy_pass http://127.0.0.1:8044/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
@@ -193,7 +193,8 @@ location /app/fedimint-gateway/ {
|
||||
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
||||
}
|
||||
location /app/tailscale/ {
|
||||
proxy_pass http://127.0.0.1:8240/;
|
||||
# Tailscale: no web UI
|
||||
return 503;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
@@ -284,7 +285,7 @@ location /app/indeedhub/ {
|
||||
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
||||
}
|
||||
location /app/nginx-proxy-manager/ {
|
||||
proxy_pass http://127.0.0.1:81/;
|
||||
proxy_pass http://127.0.0.1:8181/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
set -x
|
||||
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get install -yq \
|
||||
live-build \
|
||||
procps \
|
||||
systemd \
|
||||
binfmt-support \
|
||||
qemu-utils \
|
||||
qemu-user-static \
|
||||
qemu-system-x86 \
|
||||
qemu-system-aarch64 \
|
||||
xorriso \
|
||||
isolinux \
|
||||
ca-certificates \
|
||||
curl \
|
||||
gpg \
|
||||
fdisk \
|
||||
dosfstools \
|
||||
e2fsprogs \
|
||||
squashfs-tools \
|
||||
rsync
|
||||
@@ -1,88 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
DEB_PATH="$(realpath $1)"
|
||||
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")"/..
|
||||
|
||||
BASEDIR="$(pwd -P)"
|
||||
|
||||
VERSION="$(dpkg-deb --fsys-tarfile $DEB_PATH | tar --to-stdout -xvf - ./usr/lib/startos/VERSION.txt)"
|
||||
GIT_HASH="$(dpkg-deb --fsys-tarfile $DEB_PATH | tar --to-stdout -xvf - ./usr/lib/startos/GIT_HASH.txt)"
|
||||
if [[ "$GIT_HASH" =~ ^@ ]]; then
|
||||
GIT_HASH="unknown"
|
||||
else
|
||||
GIT_HASH="$(echo -n "$GIT_HASH" | head -c 7)"
|
||||
fi
|
||||
STARTOS_ENV="$(dpkg-deb --fsys-tarfile $DEB_PATH | tar --to-stdout -xvf - ./usr/lib/startos/ENVIRONMENT.txt)"
|
||||
PLATFORM="$(dpkg-deb --fsys-tarfile $DEB_PATH | tar --to-stdout -xvf - ./usr/lib/startos/PLATFORM.txt)"
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
PLATFORM="$(uname -m)"
|
||||
fi
|
||||
if [ "$PLATFORM" = "x86_64" ] || [ "$PLATFORM" = "x86_64-nonfree" ]; then
|
||||
ARCH=amd64
|
||||
QEMU_ARCH=x86_64
|
||||
elif [ "$PLATFORM" = "aarch64" ] || [ "$PLATFORM" = "aarch64-nonfree" ] || [ "$PLATFORM" = "raspberrypi" ] || [ "$PLATFORM" = "rockchip64" ]; then
|
||||
ARCH=arm64
|
||||
QEMU_ARCH=aarch64
|
||||
else
|
||||
ARCH="$PLATFORM"
|
||||
QEMU_ARCH="$PLATFORM"
|
||||
fi
|
||||
|
||||
SUITE=bookworm
|
||||
|
||||
debspawn list | grep $SUITE || debspawn create $SUITE
|
||||
|
||||
VERSION_FULL="${VERSION}-${GIT_HASH}"
|
||||
if [ -n "$STARTOS_ENV" ]; then
|
||||
VERSION_FULL="$VERSION_FULL~${STARTOS_ENV}"
|
||||
fi
|
||||
|
||||
if [ -z "$DSNAME" ]; then
|
||||
DSNAME="$SUITE"
|
||||
fi
|
||||
|
||||
if [ "$QEMU_ARCH" != "$(uname -m)" ]; then
|
||||
sudo update-binfmts --import qemu-$QEMU_ARCH
|
||||
fi
|
||||
|
||||
imgbuild_fname="$(mktemp /tmp/exec-mkimage.XXXXXX)"
|
||||
cat > $imgbuild_fname <<END
|
||||
#!/bin/sh
|
||||
|
||||
export IB_SUITE=${SUITE}
|
||||
export IB_TARGET_ARCH=${ARCH}
|
||||
export IB_TARGET_PLATFORM=${PLATFORM}
|
||||
export IB_OS_ENV=${STARTOS_ENV}
|
||||
export VERSION=${VERSION}
|
||||
export VERSION_FULL=${VERSION_FULL}
|
||||
exec ./build.sh
|
||||
END
|
||||
|
||||
prepare_hash=$(sha1sum ${BASEDIR}/image-recipe/prepare.sh | head -c 7)
|
||||
|
||||
mkdir -p ${BASEDIR}/image-recipe/deb
|
||||
cp $DEB_PATH ${BASEDIR}/image-recipe/deb/
|
||||
|
||||
mkdir -p ${BASEDIR}/results
|
||||
set +e
|
||||
debspawn run \
|
||||
-x \
|
||||
--allow=read-kmods,kvm,full-dev \
|
||||
--cachekey="${SUITE}-${prepare_hash}-mkimage" \
|
||||
--init-command="${BASEDIR}/image-recipe/prepare.sh" \
|
||||
--build-dir="${BASEDIR}/image-recipe" \
|
||||
--artifacts-out="${BASEDIR}/results" \
|
||||
--header="StartOS Image Build" \
|
||||
--suite=${SUITE} \
|
||||
${DSNAME} \
|
||||
${imgbuild_fname}
|
||||
|
||||
retval=$?
|
||||
rm $imgbuild_fname
|
||||
if [ $retval -ne 0 ]; then
|
||||
exit $retval
|
||||
fi
|
||||
exit 0
|
||||
BIN
intro-typing.mp3
BIN
intro-typing.mp3
Binary file not shown.
BIN
loop-start.mp3
BIN
loop-start.mp3
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user