Archipelago — open-source initial import

This commit is contained in:
Archipelago
2026-08-12 10:55:50 +00:00
commit 2d14e064c7
1589 changed files with 336062 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
results/
*.deb
+195
View File
@@ -0,0 +1,195 @@
# Live Server to ISO Build Integration Guide
This document explains how to keep the ISO build synchronized with the live development server.
## Development Workflow
### 1. Develop and Test on Live Server
```bash
# Make changes locally
vim core/archipelago/src/...
# Deploy to live server for testing
./scripts/deploy-to-target.sh --live
# Test at http://192.168.1.228
# Check logs: ssh archipelago@192.168.1.228 'sudo journalctl -u archipelago -f'
```
### 2. Capture System Changes
When you make system-level changes on the live server (nginx config, systemd service, etc.):
```bash
cd image-recipe
./sync-from-live.sh
```
This automatically captures:
- `/etc/systemd/system/archipelago.service``configs/archipelago.service`
- `/etc/nginx/sites-available/archipelago``configs/nginx-archipelago.conf`
- `/etc/logrotate.d/archipelago``configs/logrotate.conf`
### 3. Build New ISO
```bash
# Build backend and frontend
./scripts/build-backend.sh
./scripts/build-frontend.sh
# Build ISO with latest changes
./build-debian-iso.sh
# Test in QEMU
./test-iso-qemu.sh
```
### 4. Verify Integration
The ISO build script should:
1. Copy `configs/archipelago.service` to `/etc/systemd/system/`
2. Copy `configs/nginx-archipelago.conf` to `/etc/nginx/sites-available/archipelago`
3. Create symlink: `/etc/nginx/sites-enabled/archipelago`
4. Enable the service: `systemctl enable archipelago`
5. Install backend to `/usr/local/bin/archipelago`
6. Install frontend to `/opt/archipelago/web-ui/`
## Critical Configuration Settings
### Backend Service (archipelago.service)
**Must-have settings**:
```ini
[Service]
User=root # Required for root Podman access
Environment="ARCHIPELAGO_BIND=127.0.0.1:5678" # Backend API port
Environment="ARCHIPELAGO_DEV_MODE=true" # Enable container auto-detection
```
**Why root?**: The backend must run as root to access containers started with `sudo podman`. Containers in root Podman context are invisible to rootless Podman.
### Nginx Configuration (nginx-archipelago.conf)
**Must-have proxies**:
```nginx
location /rpc/ {
proxy_pass http://127.0.0.1:5678; # Backend RPC endpoint
}
location /ws {
proxy_pass http://127.0.0.1:5678; # WebSocket for real-time updates
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
```
## File Paths Reference
### Build Artifacts
- `build/backend/archipelago` - Compiled Rust backend
- `build/frontend/` - Built Vue.js frontend
- `configs/` - System configuration files
- `results/` - Built ISO images
### Live Server Paths
- `/usr/local/bin/archipelago` - Backend binary
- `/opt/archipelago/web-ui/` - Frontend files
- `/etc/systemd/system/archipelago.service` - Service definition
- `/etc/nginx/sites-available/archipelago` - Nginx config
- `/var/lib/archipelago/` - Application data
### ISO Installation Paths
Same as live server (above) - the ISO must replicate the exact file structure.
## Container Management
### Root vs Rootless Podman
**Current approach**: Root Podman
- Containers started with: `sudo podman run ...`
- Backend runs as: `root` user (in systemd)
- Container detection: Works automatically in dev mode
**Why not rootless?**
- Would require `User=archipelago` in systemd service
- All containers must be started as `archipelago` user
- More complex permission management
### Container Detection
The backend automatically detects running containers when:
1. `ARCHIPELAGO_DEV_MODE=true` is set
2. Backend runs with same privileges as container runtime
3. Containers exist in accessible Podman context
## Troubleshooting
### Issue: Containers not detected in ISO
**Cause**: Backend not running as root, or dev mode disabled
**Fix**:
1. Check `configs/archipelago.service` has `User=root`
2. Check `Environment="ARCHIPELAGO_DEV_MODE=true"` is set
3. Rebuild ISO and test
### Issue: UI not loading
**Cause**: Nginx config not copied or frontend files missing
**Fix**:
1. Verify `configs/nginx-archipelago.conf` exists
2. Check frontend built to `build/frontend/`
3. Verify ISO build script copies these files
### Issue: Backend won't start
**Cause**: Binary permissions or missing dependencies
**Fix**:
1. Check backend binary is executable: `chmod +x /usr/local/bin/archipelago`
2. Check dependencies installed (Podman, nginx)
3. Review systemd logs: `journalctl -u archipelago`
## Testing Checklist
Before releasing an ISO, verify:
- [ ] Boot ISO in QEMU
- [ ] Systemd service starts: `systemctl status archipelago`
- [ ] Backend responds: `curl http://localhost:5678/health`
- [ ] UI accessible: Open browser to `http://localhost`
- [ ] Container detection: `sudo podman run -d --name test nginx` → Shows in UI
- [ ] RPC works: Test login and API calls
- [ ] WebSocket connects: Check browser console
## Automated Build Pipeline (Future)
To automate this workflow:
1. **CI/CD Integration**
- Trigger on main branch commits
- Run `sync-from-live.sh` with credentials
- Build backend and frontend
- Build ISO
- Upload to releases
2. **Version Management**
- Tag releases with semantic versions
- Include git commit hash in ISO metadata
- Track which configs were included
3. **Testing Automation**
- Boot ISO in headless QEMU
- Run API tests
- Verify container detection
- Generate test report
## Resources
- Development Workflow Rules: `.cursor/rules/Development-Workflow.mdc`
- Build Checklist: `ISO-BUILD-CHECKLIST.md`
- Architecture Docs: `.cursor/rules/Architecture.mdc`
- Deployment Scripts: `scripts/deploy-to-target.sh`
+88
View File
@@ -0,0 +1,88 @@
# Archipelago OS Image Recipes
Build scripts for creating bootable Debian Linux OS images for Archipelago Bitcoin Node OS.
## Quick Start
### Build the ISO
```bash
# 1. Sync latest configs from live dev server
./sync-from-live.sh
# 2. Build components
./scripts/build-backend.sh
./scripts/build-frontend.sh
# 3. Build the ISO
./build-debian-iso.sh
```
This creates a bootable Debian Live ISO with Archipelago pre-installed.
### Write to USB
```bash
# Using dd (recommended)
./write-usb-dd.sh /dev/diskN
# Or use Balena Etcher to flash the ISO
```
See the **ISO-BUILD-CHECKLIST.md** for a comprehensive build workflow.
See the Architecture documentation for detailed system information.
## What's Included
- **Debian Linux Base**: Debian 13 (Trixie) with security updates applied during ISO/install creation
- **Podman**: Container runtime for apps (rootless by default)
- **Archipelago Backend**: Rust-based API server
- **Archipelago Frontend**: Vue.js web interface
- **Systemd Services**: Automatic service management
- **Network Configuration**: NetworkManager for easy setup
## Build Output
- `results/archipelago-installer-x86_64.iso` - Bootable hybrid ISO image
## Supported Platforms
- **x86_64**: Dell OptiPlex, HP ProDesk 400 G4 DM, Start9 Server Pure, and other x86_64 machines
- **Build Systems**: macOS (requires Docker) and Linux (native or Docker)
## Installation Methods
### 1. Live USB Boot
Boot from USB, run in live mode to test, or install to disk.
### 2. Full Disk Installation
From the live environment, run:
```bash
sudo /archipelago/install-to-disk.sh
```
This installs Archipelago to a target disk using debootstrap.
## Directory Structure
```
image-recipe/
├── build-debian-iso.sh # Main ISO builder
├── write-usb-dd.sh # Write ISO to USB with dd
├── create-fat32-usb.sh # Alternative USB creation
├── archipelago-scripts/ # Scripts included in ISO
│ ├── install-to-disk.sh # Disk installer
│ └── setup-bitcoin.sh # Bitcoin Core setup
├── scripts/ # Build helper scripts
│ ├── build-backend.sh # Compile Rust backend
│ ├── build-frontend.sh # Build Vue.js frontend
│ └── check-dependencies.sh # Verify build requirements
└── results/ # Built ISO output
```
## Requirements
- Docker (for macOS builds)
- xorriso (for ISO creation): `brew install xorriso`
- 7zip (for ISO extraction): `brew install p7zip`
@@ -0,0 +1,352 @@
name: Build Archipelago ISO (dev)
on:
push:
branches: [main, dev-iso]
workflow_dispatch:
jobs:
build-iso:
runs-on: iso-builder
timeout-minutes: 60
steps:
- name: Checkout
run: |
# Direct fetch + sync (actions/checkout token is broken on this Gitea)
REPO_DIR="$HOME/archy"
cd "$REPO_DIR" && git fetch origin main && git reset --hard origin/main
echo "=== Source at commit: $(git log --oneline -1) ==="
rsync -a --delete \
--exclude '.git' --exclude 'node_modules' --exclude 'target' \
--exclude 'image-recipe/build' --exclude 'image-recipe/results' \
--exclude 'web/dist' \
"$REPO_DIR/" "$GITHUB_WORKSPACE/"
cd "$GITHUB_WORKSPACE"
echo "=== Workspace version: $(grep '^version' core/archipelago/Cargo.toml) ==="
[ -f "scripts/first-boot-containers.sh" ] && echo " first-boot-containers.sh: PRESENT" || echo " first-boot-containers.sh: MISSING"
grep -q 'network-alias' scripts/first-boot-containers.sh 2>/dev/null && echo " network-alias fix: PRESENT" || echo " network-alias fix: MISSING"
- name: Install ISO build dependencies
run: |
# Skip apt if packages already installed (persistent runner)
if dpkg -s debootstrap squashfs-tools xorriso isolinux syslinux-common mtools \
grub-efi-amd64-bin grub-pc-bin grub-common musl-tools >/dev/null 2>&1; then
echo "ISO build deps already installed, skipping apt"
else
sudo apt-get update -qq
sudo apt-get install -y -qq \
debootstrap squashfs-tools xorriso \
isolinux syslinux-common mtools \
grub-efi-amd64-bin grub-pc-bin grub-common \
musl-tools
fi
# Ensure musl Rust target is available
source $HOME/.cargo/env 2>/dev/null || true
rustup target add x86_64-unknown-linux-musl 2>/dev/null || true
- name: Build backend (incremental, musl static)
run: |
source $HOME/.cargo/env 2>/dev/null || true
# Build in persistent repo dir to reuse target/ cache
cd "$HOME/archy"
export GIT_HASH=$(git rev-parse --short HEAD)
# Static musl build for portability — ensures binary runs regardless
# of glibc version differences between build host and ISO rootfs.
cargo build --release --target x86_64-unknown-linux-musl --manifest-path core/Cargo.toml
# Copy binary to workspace for downstream steps
mkdir -p "$GITHUB_WORKSPACE/core/target/release"
cp core/target/x86_64-unknown-linux-musl/release/archipelago "$GITHUB_WORKSPACE/core/target/release/"
- name: Build frontend
run: |
source $HOME/.nvm/nvm.sh 2>/dev/null || true
cd neode-ui && npm ci && npm run build
- name: Type check frontend
run: |
source $HOME/.nvm/nvm.sh 2>/dev/null || true
cd neode-ui && npx vue-tsc -b --noEmit
- name: Run frontend tests
run: |
source $HOME/.nvm/nvm.sh 2>/dev/null || true
cd neode-ui && npx vitest run
- name: Include AIUI if available
run: |
# AIUI (the Claude chat sidebar) lives outside the Vue build
# and must be copied into the frontend dist BEFORE packaging,
# otherwise OTA-tarball upgrades silently strip it from nodes
# in the field. Try in order: cached on runner, then the
# newest release tarball in this repo's releases/ dir as a
# fallback so a freshly-provisioned runner still gets AIUI.
AIUI_SRC=""
if [ -f "/opt/archipelago/web-ui/aiui/index.html" ]; then
AIUI_SRC="/opt/archipelago/web-ui/aiui"
elif [ -f "$HOME/archy/web/dist/neode-ui/aiui/index.html" ]; then
AIUI_SRC="$HOME/archy/web/dist/neode-ui/aiui"
else
LATEST_FRONTEND=$(ls -t releases/v*/archipelago-frontend-*.tar.gz 2>/dev/null | head -1)
if [ -n "$LATEST_FRONTEND" ]; then
echo "Extracting AIUI from $LATEST_FRONTEND (runner cache miss)"
TMP=$(mktemp -d)
tar xzf "$LATEST_FRONTEND" -C "$TMP" ./aiui 2>/dev/null || true
if [ -f "$TMP/aiui/index.html" ]; then
AIUI_SRC="$TMP/aiui"
fi
fi
fi
if [ -n "$AIUI_SRC" ]; then
mkdir -p web/dist/neode-ui/aiui
cp -r "$AIUI_SRC/"* web/dist/neode-ui/aiui/
echo "AIUI included from $AIUI_SRC ($(du -sh web/dist/neode-ui/aiui | cut -f1))"
else
echo "FAIL: AIUI not found anywhere (runner cache + release tarballs)"
echo " checked: /opt/archipelago/web-ui/aiui"
echo " \$HOME/archy/web/dist/neode-ui/aiui"
echo " releases/v*/archipelago-frontend-*.tar.gz"
exit 1
fi
- name: Configure root podman for insecure registry
run: |
sudo mkdir -p /etc/containers/registries.conf.d
echo '[[registry]]
location = "146.59.87.168:3000"
insecure = true' | sudo tee /etc/containers/registries.conf.d/archipelago.conf
- name: Build unbundled ISO
run: |
cd image-recipe
export ARCHIPELAGO_BIN="$(pwd)/../core/target/release/archipelago"
if [ ! -x "$ARCHIPELAGO_BIN" ]; then
echo "FAIL: backend binary missing or not executable at $ARCHIPELAGO_BIN"
exit 1
fi
BIN_VERSION=$(strings "$ARCHIPELAGO_BIN" | grep -oE 'archipelago [0-9]+\.[0-9]+\.[0-9]+(-[a-z]+)?' | head -1 || true)
EXPECTED=$(grep '^version' ../core/archipelago/Cargo.toml | head -1 | sed 's/.*"\(.*\)".*/\1/')
echo "Binary: $ARCHIPELAGO_BIN ($(du -h "$ARCHIPELAGO_BIN" | cut -f1))"
echo "Embedded version string: ${BIN_VERSION:-unknown}"
echo "Expected version (Cargo.toml): $EXPECTED"
sudo -E UNBUNDLED=1 DEV_SERVER=localhost BUILD_FROM_SOURCE=0 \
ARCHIPELAGO_BIN="$ARCHIPELAGO_BIN" \
./build-auto-installer-iso.sh
- name: Smoke test ISO
run: |
ISO=$(ls image-recipe/results/archipelago-installer-unbundled-*.iso 2>/dev/null | head -1)
if [ -z "$ISO" ]; then
echo "FAIL: No ISO produced"
exit 1
fi
echo "ISO: $ISO ($(du -h "$ISO" | cut -f1))"
# Mount and verify structure
MNT=$(mktemp -d)
sudo mount -o loop,ro "$ISO" "$MNT"
FAIL=0
for f in live/vmlinuz live/initrd.img live/filesystem.squashfs \
isolinux/isolinux.bin isolinux/isolinux.cfg \
boot/grub/grub.cfg EFI/BOOT/BOOTX64.EFI \
archipelago/auto-install.sh archipelago/rootfs.tar; do
if [ -e "$MNT/$f" ]; then
echo " OK: $f ($(sudo du -h "$MNT/$f" 2>/dev/null | cut -f1))"
else
echo " MISSING: $f"
FAIL=1
fi
done
# Verify initrd has live-boot
INITRD_DIR=$(mktemp -d)
sudo unmkinitramfs "$MNT/live/initrd.img" "$INITRD_DIR" 2>/dev/null
if [ -e "$INITRD_DIR/scripts/live" ] || [ -e "$INITRD_DIR/main/scripts/live" ]; then
echo " OK: initrd has live-boot scripts"
else
echo " MISSING: live-boot scripts in initrd!"
echo " initrd scripts/: $(ls "$INITRD_DIR/scripts/" 2>/dev/null || ls "$INITRD_DIR/main/scripts/" 2>/dev/null)"
FAIL=1
fi
# Check GRUB config has boot=live
if grep -q "boot=live" "$MNT/boot/grub/grub.cfg"; then
echo " OK: grub.cfg has boot=live"
else
echo " MISSING: boot=live in grub.cfg"
FAIL=1
fi
sudo umount "$MNT" 2>/dev/null
rmdir "$MNT" 2>/dev/null
sudo rm -r "$INITRD_DIR" 2>/dev/null
if [ "$FAIL" = "1" ]; then
echo "SMOKE TEST FAILED"
exit 1
fi
echo "SMOKE TEST PASSED"
- name: QEMU boot test
timeout-minutes: 5
continue-on-error: true
run: |
ISO=$(ls image-recipe/results/archipelago-installer-unbundled-*.iso 2>/dev/null | head -1)
if [ -n "$ISO" ] && command -v qemu-system-x86_64 >/dev/null 2>&1; then
echo "Running headless QEMU boot test..."
bash image-recipe/test-iso-qemu.sh "$ISO" 120
else
echo "Skipping QEMU test (no ISO or QEMU not available)"
fi
- name: Copy to Builds
run: |
ISO=$(ls image-recipe/results/archipelago-installer-unbundled-*.iso 2>/dev/null | head -1)
if [ -n "$ISO" ]; then
DATE=$(date +%Y%m%d-%H%M)
DEST="/var/lib/archipelago/filebrowser/Builds/archipelago-dev-unbundled-${DATE}.iso"
sudo cp "$ISO" "$DEST"
sudo chown 1000:1000 "$DEST"
echo "ISO: archipelago-dev-unbundled-${DATE}.iso"
echo "Size: $(du -h "$DEST" | cut -f1)"
echo "SHA256: $(sha256sum "$DEST" | cut -d' ' -f1)"
fi
- name: Publish release artifacts and manifest
run: |
VERSION=$(grep '^version' core/archipelago/Cargo.toml | head -1 | sed 's/.*"\(.*\)".*/\1/')
DATE=$(date +%Y-%m-%d)
RELEASE_DIR="/var/lib/archipelago/filebrowser/Builds/releases/v${VERSION}"
sudo mkdir -p "$RELEASE_DIR"
# Copy backend binary
BINARY="core/target/release/archipelago"
if [ -f "$BINARY" ]; then
sudo cp "$BINARY" "$RELEASE_DIR/archipelago"
sudo chmod 755 "$RELEASE_DIR/archipelago"
echo "Backend: $(du -h "$RELEASE_DIR/archipelago" | cut -f1)"
fi
# Create frontend archive
if [ -d "web/dist/neode-ui" ]; then
FRONTEND_ARCHIVE="$RELEASE_DIR/archipelago-frontend-${VERSION}.tar.gz"
sudo tar -czf "$FRONTEND_ARCHIVE" -C web/dist neode-ui
echo "Frontend: $(du -h "$FRONTEND_ARCHIVE" | cut -f1)"
fi
# Generate manifest with SHA256 hashes
BACKEND_HASH=$(sha256sum "$RELEASE_DIR/archipelago" 2>/dev/null | awk '{print $1}')
BACKEND_SIZE=$(stat -c%s "$RELEASE_DIR/archipelago" 2>/dev/null || echo 0)
FRONTEND_NAME="archipelago-frontend-${VERSION}.tar.gz"
FRONTEND_HASH=$(sha256sum "$RELEASE_DIR/$FRONTEND_NAME" 2>/dev/null | awk '{print $1}')
FRONTEND_SIZE=$(stat -c%s "$RELEASE_DIR/$FRONTEND_NAME" 2>/dev/null || echo 0)
# Build download base URL (FileBrowser serves from /Builds/)
HOST=$(hostname -I 2>/dev/null | awk '{print $1}')
BASE_URL="http://${HOST:-192.168.1.228}:8083/Builds/releases/v${VERSION}"
# Generate manifest JSON
python3 -c "
import json
manifest = {
'version': '$VERSION',
'release_date': '$DATE',
'changelog': ['Update to version $VERSION'],
'components': []
}
if '$BACKEND_HASH':
manifest['components'].append({
'name': 'archipelago',
'current_version': '$VERSION',
'new_version': '$VERSION',
'download_url': '$BASE_URL/archipelago',
'sha256': '$BACKEND_HASH',
'size_bytes': int('$BACKEND_SIZE' or '0')
})
if '$FRONTEND_HASH':
manifest['components'].append({
'name': '$FRONTEND_NAME',
'current_version': '$VERSION',
'new_version': '$VERSION',
'download_url': '$BASE_URL/$FRONTEND_NAME',
'sha256': '$FRONTEND_HASH',
'size_bytes': int('$FRONTEND_SIZE' or '0')
})
print(json.dumps(manifest, indent=2))
" | sudo tee "$RELEASE_DIR/manifest.json" > /dev/null
# Also copy manifest to repo releases/ dir for git-based serving
cp "$RELEASE_DIR/manifest.json" releases/manifest.json 2>/dev/null || true
sudo chown -R 1000:1000 "$RELEASE_DIR"
echo ""
echo "Release manifest:"
cat "$RELEASE_DIR/manifest.json"
echo ""
echo "Artifacts published to: $RELEASE_DIR"
- name: Build report
if: always()
continue-on-error: true
run: |
set +eo pipefail
echo "══════════════════════════════════════════"
echo "DEV ISO BUILD REPORT"
echo "══════════════════════════════════════════"
echo "Commit: $(git -C "$HOME/archy" rev-parse --short HEAD 2>/dev/null || echo 'unknown') ($(git -C "$HOME/archy" log -1 --format=%s 2>/dev/null || echo 'unknown'))"
echo "Branch: ${GITHUB_REF_NAME:-dev-iso}"
echo "Date: $(date -u '+%Y-%m-%d %H:%M:%S UTC')"
echo "Runner: $(hostname)"
echo ""
echo "── Artifacts ──"
ls -lh image-recipe/results/*.iso 2>/dev/null || echo " No ISO produced"
ls -lh /var/lib/archipelago/filebrowser/Builds/archipelago-dev-*.iso 2>/dev/null | tail -3
echo ""
echo "── Rootfs contents check ──"
ROOTFS=$(ls image-recipe/build/auto-installer/archipelago-rootfs.tar 2>/dev/null) || true
if [ -n "$ROOTFS" ]; then
echo " rootfs.tar: $(sudo du -h "$ROOTFS" 2>/dev/null | cut -f1 || echo 'unknown')"
# List key paths once (podman export omits ./ prefix, so match without it)
ROOTFS_LIST=$(sudo tar tf "$ROOTFS" 2>/dev/null | grep -E '(etc/nginx/sites-available/archipelago|etc/archipelago/ssl/archipelago.crt|usr/local/bin/archipelago-kiosk-launcher|usr/local/bin/archipelago|opt/archipelago/web-ui/index.html)' || true)
for item in \
"nginx config:etc/nginx/sites-available/archipelago" \
"SSL cert:etc/archipelago/ssl/archipelago.crt" \
"kiosk launcher:usr/local/bin/archipelago-kiosk-launcher" \
"backend binary:usr/local/bin/archipelago" \
"web-ui index:opt/archipelago/web-ui/index.html"; do
label="${item%%:*}"; path="${item#*:}"
echo "$ROOTFS_LIST" | grep -q "$path" && echo " $label: PRESENT" || echo " $label: MISSING"
done
else
echo " rootfs.tar not found in workspace"
fi
echo ""
echo "── ISO contents check ──"
ISO=$(ls image-recipe/results/archipelago-installer-unbundled-*.iso 2>/dev/null | head -1) || true
if [ -n "$ISO" ]; then
echo " ISO size: $(sudo du -h "$ISO" 2>/dev/null | cut -f1 || echo 'unknown')"
ISO_MOUNT=$(mktemp -d)
if sudo mount -o loop,ro "$ISO" "$ISO_MOUNT" 2>/dev/null; then
echo " auto-install.sh: $([ -f "$ISO_MOUNT/archipelago/auto-install.sh" ] && echo 'PRESENT' || echo 'MISSING')"
echo " rootfs.tar: $([ -f "$ISO_MOUNT/archipelago/rootfs.tar" ] && echo "PRESENT ($(sudo du -h "$ISO_MOUNT/archipelago/rootfs.tar" 2>/dev/null | cut -f1))" || echo 'MISSING')"
echo " backend bin: $([ -f "$ISO_MOUNT/archipelago/bin/archipelago" ] && echo "PRESENT ($(sudo du -h "$ISO_MOUNT/archipelago/bin/archipelago" 2>/dev/null | cut -f1))" || echo 'MISSING')"
echo " frontend: $([ -f "$ISO_MOUNT/archipelago/web-ui/index.html" ] && echo 'PRESENT' || echo 'MISSING')"
echo " vmlinuz: $([ -f "$ISO_MOUNT/live/vmlinuz" ] && echo 'PRESENT' || echo 'MISSING')"
echo " initrd: $([ -f "$ISO_MOUNT/live/initrd.img" ] && echo 'PRESENT' || echo 'MISSING')"
echo " squashfs: $([ -f "$ISO_MOUNT/live/filesystem.squashfs" ] && echo "PRESENT ($(sudo du -h "$ISO_MOUNT/live/filesystem.squashfs" 2>/dev/null | cut -f1))" || echo 'MISSING')"
echo " grub theme: $([ -d "$ISO_MOUNT/boot/grub/themes/archipelago" ] && echo 'PRESENT' || echo 'MISSING')"
sudo umount "$ISO_MOUNT" 2>/dev/null || true
else
echo " Could not mount ISO for inspection"
fi
rmdir "$ISO_MOUNT" 2>/dev/null || true
fi
echo "══════════════════════════════════════════"
- name: Fix workspace permissions
if: always()
run: |
sudo chown -R $(id -u):$(id -g) . 2>/dev/null || true
sudo chmod -R u+rwX . 2>/dev/null || true
sudo chown -R $(id -u):$(id -g) "$HOME/.cache/act" 2>/dev/null || true
sudo chmod -R u+rwX "$HOME/.cache/act" 2>/dev/null || true
@@ -0,0 +1,86 @@
# Archipelago ISO Build - Quick Guide
## TL;DR - Build ISO with Live Server State
```bash
cd ~/archy/image-recipe
sudo bash build-auto-installer-iso.sh
```
The script will automatically:
1. Try to capture backend from `/usr/local/bin/archipelago`
2. Try to capture frontend from `/opt/archipelago/web-ui`
3. Fall back to building from source if capture fails
## Build Modes
### Default: Capture from Dev Server (RECOMMENDED)
```bash
# From your Mac (captures from remote dev server):
cd image-recipe
DEV_SERVER=archipelago@192.168.1.228 sudo bash build-auto-installer-iso.sh
# From the dev server itself:
cd ~/archy/image-recipe
sudo bash build-auto-installer-iso.sh
```
### Alternative: Build from Source
```bash
BUILD_FROM_SOURCE=1 sudo bash build-auto-installer-iso.sh
```
## Known Issues & Workarounds
### Issue: Can't capture from localhost via SCP
**Problem**: When running on the server itself, `scp localhost:/path` doesn't work.
**Workaround**: Use direct file copy instead:
```bash
# Instead of building on the server, build from your Mac:
cd ~/Projects/archy/image-recipe
DEV_SERVER=archipelago@192.168.1.228 sudo bash build-auto-installer-iso.sh
```
### Issue: Podman registry not configured
**Problem**: Podman can't pull images because `/etc/containers/registries.conf` has no unqualified-search registries.
**Fix**:
```bash
ssh archipelago@192.168.1.228
sudo tee -a /etc/containers/registries.conf <<EOF
[registries.search]
registries = ['docker.io']
EOF
```
## Flash ISO to USB
```bash
cd ~/Projects/archy/image-recipe
./write-usb-dd.sh /dev/diskX
```
## What Gets Captured
From your dev server (192.168.1.228):
- ✅ Backend binary: `/usr/local/bin/archipelago` (6.2M)
- ✅ Frontend: `/opt/archipelago/web-ui` (~64M)
- ✅ Nginx config: `/etc/nginx/sites-available/default`
- ✅ Systemd service: `/etc/systemd/system/archipelago.service`
- ✅ App manifests: `~/archy/apps/`
## Current Status
**Latest Working ISO**: `archipelago-debian-13-x86_64.iso` (469M, built 18:28)
- This ISO was built earlier today
- Contains the auto-installer
- **Should be tested** - might already have your live server state
## Next Steps
1. **Flash the existing ISO** and test it on the Dell OptiPlex
2. **Fix the build script** to properly capture from localhost (use `cp` instead of `scp`)
3. **Configure Podman registries** on dev server for fallback source builds
@@ -0,0 +1,155 @@
# ISO Build Checklist
This checklist ensures that all changes from the live development server are properly integrated into the ISO build.
## Pre-Build Steps
### 1. Sync System Configurations from Live Server
```bash
cd image-recipe
./sync-from-live.sh
```
This captures:
- [ ] Systemd service configuration (`archipelago.service`) - **User=root** required for Podman
- [ ] Nginx configuration (`nginx-archipelago.conf`) - includes app proxies (Nextcloud, Vaultwarden, Immich, Penpot)
- [ ] Logrotate configuration (if exists)
- [ ] Any custom scripts in `/opt/archipelago/scripts/`
**Critical**: `build-auto-installer-iso.sh` uses `configs/` for nginx and archipelago.service. Ensure these are synced before building.
### 2. Verify Code Changes
Ensure all code changes are committed:
- [ ] Backend changes in `core/`
- [ ] Frontend changes in `neode-ui/`
- [ ] Script changes in `scripts/`
### 3. Build Components
```bash
cd image-recipe
# Build backend
./scripts/build-backend.sh
# Build frontend
./scripts/build-frontend.sh
```
Verify builds:
- [ ] Backend binary exists: `build/backend/archipelago`
- [ ] Frontend files exist: `build/frontend/index.html`
## Integration Check
### 4. Update Build Scripts
Review and update if needed:
- [ ] `integrate-archipelago.sh` - Includes all config files
- [ ] `build-debian-iso.sh` - Installs to correct paths
### 5. Critical Configuration Values
Verify in `configs/archipelago.service`:
- [ ] `User=root` (required for Podman root context)
- [ ] `Environment="ARCHIPELAGO_DEV_MODE=true"` (enables container detection)
- [ ] `Environment="ARCHIPELAGO_BIND=127.0.0.1:5678"`
Verify in `configs/nginx-archipelago.conf`:
- [ ] Root path: `/opt/archipelago/web-ui`
- [ ] RPC proxy: `/rpc/``http://127.0.0.1:5678`
- [ ] WebSocket proxy: `/ws``http://127.0.0.1:5678`
## Build Process
### 6. Build the ISO
```bash
./build-debian-iso.sh
```
Expected output:
- [ ] ISO created in `results/` directory
- [ ] No build errors
- [ ] File size reasonable (~500MB - 2GB)
### 7. Test in QEMU
```bash
./test-iso-qemu.sh
```
Test checklist:
- [ ] ISO boots successfully
- [ ] Backend service starts: `systemctl status archipelago`
- [ ] Nginx serves frontend
- [ ] Can access UI at `http://localhost:8080` (or mapped port)
- [ ] Container detection works: Check logs for "Detected container"
## App Stack Hardening (Immich, Penpot, etc.)
The first-boot script and deploy script ensure:
- [ ] **Immich**: Old single-container `immich` (wrong port) is removed before creating `immich_server` stack
- [ ] **First-boot**: Waits for postgres (pg_isready) before starting Immich server
- [ ] **Backend**: `package.install` for Immich removes old container before creating stack
- [ ] **Deploy**: Ensures Immich stack on every deploy, cleans up conflicts
## Post-Build
### 8. Write to USB (Optional)
```bash
./write-usb-dd.sh /dev/diskN
```
Or use Balena Etcher to flash the ISO.
### 9. Test on Real Hardware
- [ ] Boot from USB
- [ ] Network configuration works
- [ ] All services start automatically
- [ ] Can access web UI
- [ ] Containers are detected and managed
## Deployment Paths Reference
The ISO build must install to these paths:
| Component | Path | Source |
|-----------|------|--------|
| Backend binary | `/usr/local/bin/archipelago` | `build/backend/archipelago` |
| Frontend files | `/opt/archipelago/web-ui/` | `build/frontend/*` |
| Systemd service | `/etc/systemd/system/archipelago.service` | `configs/archipelago.service` |
| Nginx config | `/etc/nginx/sites-available/archipelago` | `configs/nginx-archipelago.conf` |
| Nginx symlink | `/etc/nginx/sites-enabled/archipelago` | Link to sites-available |
## Common Issues
### Backend Not Detecting Containers
- Verify service runs as `root` user
- Check Podman context: `sudo podman ps` should show containers
- Enable dev mode: `ARCHIPELAGO_DEV_MODE=true`
### UI Not Loading
- Check nginx configuration paths
- Verify frontend files deployed to `/opt/archipelago/web-ui/`
- Check nginx error logs: `/var/log/nginx/error.log`
### Service Fails to Start
- Check binary permissions: Should be executable
- Check systemd logs: `journalctl -u archipelago`
- Test binary manually: `sudo /usr/local/bin/archipelago`
## Version Tracking
When building a new ISO, document:
- Date: _______________
- Git commit: _______________
- Backend version: _______________
- Frontend version: _______________
- ISO filename: _______________
- Tested on hardware: _______________
- Issues found: _______________
+37
View File
@@ -0,0 +1,37 @@
# Archived ISO build recipes
These scripts built the Archipelago auto-installer ISO (bundled and
unbundled variants). As of v1.7.43-alpha, ISOs are no longer part of the
release deliverable. Releases ship as tarballs consumed by
`scripts/self-update.sh` on existing nodes.
Archived here rather than deleted so they can be resurrected if ISO
distribution is reintroduced.
## Contents
- `build-auto-installer-iso.sh` — orchestrator, bundles container images into squashfs
- `build-unbundled-iso.sh` — thin wrapper that sets BUNDLE_IMAGES=0 and delegates
- `test-iso-qemu.sh` — smoke-tests a built ISO under QEMU
- `scripts/convert-iso-to-disk.sh` — converts an ISO to a raw disk image
- `BUILD-ISO-STATUS.md`, `ISO-BUILD-CHECKLIST.md` — contributor guides
- `branding/isohdpfx.bin` — isolinux MBR hybrid image
- `.gitea-workflows/build-iso-dev.yml` — CI workflow that ran the build+smoke-test
## To resurrect
1. `git mv image-recipe/_archived/* image-recipe/` (adjust paths back)
2. Restore `.gitea/workflows/build-iso-dev.yml`
3. Re-add release-process references (see `scripts/create-release.sh`,
`docs/BETA-RELEASE-CHECKLIST.md`, `docs/hotfix-process.md`, `README.md`).
## Why archived
The release flow is simpler and faster as tarball-only:
- `releases/vX.Y.Z-alpha/archipelago` (backend binary)
- `releases/vX.Y.Z-alpha/archipelago-frontend-X.Y.Z-alpha.tar.gz` (frontend + AIUI + filebrowser UI assets)
- `releases/manifest.json` (pointers + changelog)
Nodes pull these via `scripts/self-update.sh` from either Gitea mirror.
Filebrowser and AIUI remain bundled inside the frontend tarball and deployed
atomically by `self-update.sh`.
Binary file not shown.
File diff suppressed because it is too large Load Diff
+33
View File
@@ -0,0 +1,33 @@
#!/bin/bash
#
# Build Archipelago UNBUNDLED Auto-Installer ISO
#
# Same as build-auto-installer-iso.sh but WITHOUT pre-bundled container images.
# Users install all apps on-demand from the Marketplace (requires internet).
#
# Benefits:
# - Much smaller ISO (~1-2GB vs ~8-10GB)
# - Faster build (no image pulling/saving)
# - Faster install (no image copying/loading)
#
# Trade-offs:
# - Internet required after first boot to install apps
# - No apps pre-loaded — everything comes from Marketplace
#
# Usage:
# sudo ./build-unbundled-iso.sh
# DEV_SERVER=archipelago@192.168.1.228 sudo ./build-unbundled-iso.sh
# BUILD_FROM_SOURCE=1 sudo ./build-unbundled-iso.sh
#
set -e
# Configuration
DEV_SERVER="${DEV_SERVER:-archipelago@192.168.1.228}"
BUILD_FROM_SOURCE="${BUILD_FROM_SOURCE:-0}"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# Delegate to the main build script with UNBUNDLED mode
export UNBUNDLED=1
exec "$SCRIPT_DIR/build-auto-installer-iso.sh" "$@"
+65
View File
@@ -0,0 +1,65 @@
#!/bin/bash
# Convert ISO image to bootable disk image
# Creates a raw disk image that can be flashed directly
set -e
OUTPUT_DIR="${1:-../results}"
ARCHIPELAGO_VERSION="${ARCHIPELAGO_VERSION:-0.1.0}"
ARCH="${ARCH:-x86_64}"
echo "💾 Converting ISO to disk image..."
# Find ISO file
ISO_FILE=$(ls "$OUTPUT_DIR"/*.iso 2>/dev/null | head -1)
if [ -z "$ISO_FILE" ]; then
echo "❌ No ISO file found in $OUTPUT_DIR"
exit 1
fi
echo " Source ISO: $ISO_FILE"
# Create disk image (4GB minimum)
DISK_SIZE=4096 # 4GB in MB
DISK_IMG="$OUTPUT_DIR/archipelago-${ARCHIPELAGO_VERSION}-${ARCH}.img"
echo " Creating disk image: $DISK_IMG"
# Check if we have required tools
if ! command -v dd >/dev/null 2>&1; then
echo "❌ dd not found"
exit 1
fi
# Create empty disk image
dd if=/dev/zero of="$DISK_IMG" bs=1M count=$DISK_SIZE 2>/dev/null || {
echo "❌ Failed to create disk image"
exit 1
}
# Note: Full disk image creation with partitions requires:
# - parted or fdisk
# - mkfs.vfat, mkfs.ext4
# - losetup (Linux only)
# - grub-install
# For now, we'll create a simple approach:
# The ISO can be used directly, or users can use tools like:
# - balenaEtcher (macOS/Linux GUI)
# - Rufus (Windows)
# - dd (command line)
echo "⚠️ Full disk image conversion requires additional tools"
echo " For now, use the ISO file directly with:"
echo " - balenaEtcher (recommended)"
echo " - dd command (see docs)"
echo ""
echo " ISO file: $ISO_FILE"
echo " Size: $(du -h "$ISO_FILE" | cut -f1)"
# Clean up empty image file
rm -f "$DISK_IMG"
echo ""
echo "💡 Tip: Use the ISO file with a USB flashing tool"
echo " The ISO is bootable and can be flashed directly"
+157
View File
@@ -0,0 +1,157 @@
#!/bin/bash
# Test Archipelago ISO in QEMU
#
# Usage:
# ./test-iso-qemu.sh [path-to-iso] [--bios] [--nographic]
#
# Options:
# --bios Force legacy BIOS mode (default: UEFI)
# --nographic No GUI window, serial console only (great for logging)
#
# Serial log is always written to /tmp/archipelago-qemu-serial.log
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
SERIAL_LOG="/tmp/archipelago-qemu-serial.log"
FORCE_BIOS=false
NOGRAPHIC=false
TIMEOUT=0
ISO=""
# Simple arg parsing. First non-flag positional is the ISO path. A bare
# numeric (e.g. `120`) is taken as a boot-test timeout in seconds so CI
# can call `test-iso-qemu.sh <iso> 120` without hanging the job. The
# pre-fix version used `case *) ISO=...`, which silently overwrote ISO
# with the timeout value and sent QEMU looking for a file literally
# named "120".
for arg in "$@"; do
case "$arg" in
--bios) FORCE_BIOS=true ;;
--nographic) NOGRAPHIC=true ;;
--timeout=*) TIMEOUT="${arg#--timeout=}" ;;
[0-9]*) TIMEOUT="$arg" ;;
*) ISO="$arg" ;;
esac
done
# A positive TIMEOUT implies headless (no DISPLAY in CI anyway) and keeps
# the entire script wrapped in `timeout` to guarantee the job returns.
if [ "$TIMEOUT" -gt 0 ] 2>/dev/null; then
NOGRAPHIC=true
fi
# Auto-detect ISO
if [ -z "$ISO" ]; then
ISO=$(ls -t "$SCRIPT_DIR"/results/archipelago-installer-unbundled-*.iso 2>/dev/null | head -1)
fi
if [ -z "$ISO" ] || [ ! -f "$ISO" ]; then
ISO=$(ls -t "$SCRIPT_DIR"/results/archipelago-*.iso 2>/dev/null | head -1)
fi
if [ -z "$ISO" ] || [ ! -f "$ISO" ]; then
echo "ISO not found."
echo ""
echo "Usage: $0 [path-to-iso] [--bios] [--nographic]"
echo ""
echo "Or place an ISO in: $SCRIPT_DIR/results/"
exit 1
fi
echo "Testing Archipelago ISO in QEMU"
echo " ISO: $ISO"
echo " Size: $(du -h "$ISO" | cut -f1)"
echo " RAM: 4GB"
echo " CPU: 2 cores"
echo " Serial: $SERIAL_LOG"
echo ""
# Create test disk if it doesn't exist
DISK="/tmp/archipelago-test-disk.qcow2"
if [ ! -f "$DISK" ]; then
echo "Creating 20GB test disk..."
qemu-img create -f qcow2 "$DISK" 20G
fi
# Common QEMU args
QEMU_ARGS=(
-m 4G
-smp 2
-boot d
-cdrom "$ISO"
-drive if=virtio,format=qcow2,file="$DISK"
-net nic,model=virtio -net user,hostfwd=tcp::2222-:22,hostfwd=tcp::8100-:80
-serial file:"$SERIAL_LOG"
)
# Display mode
if [ "$NOGRAPHIC" = true ]; then
QEMU_ARGS+=(-nographic -append "console=ttyS0")
else
QEMU_ARGS+=(-vga virtio -display default)
fi
echo "Starting VM..."
echo "(Serial console logging to $SERIAL_LOG)"
echo "(Press Ctrl+Alt+G to release mouse, Ctrl+C to stop VM)"
echo ""
# Detect UEFI firmware
OVMF=""
if [ "$FORCE_BIOS" = false ]; then
if [ -f "/opt/homebrew/share/qemu/edk2-x86_64-code.fd" ]; then
OVMF="/opt/homebrew/share/qemu/edk2-x86_64-code.fd"
elif [ -f "/usr/share/OVMF/OVMF_CODE.fd" ]; then
OVMF="/usr/share/OVMF/OVMF_CODE.fd"
fi
fi
run_qemu() {
if [ -n "$OVMF" ]; then
echo " Boot: UEFI ($OVMF)"
qemu-system-x86_64 \
-machine q35 \
-drive if=pflash,format=raw,readonly=on,file="$OVMF" \
"${QEMU_ARGS[@]}"
else
echo " Boot: Legacy BIOS"
qemu-system-x86_64 \
-machine pc \
"${QEMU_ARGS[@]}"
fi
}
# Wrap the QEMU invocation in `timeout` when a CI caller passed one so
# the script always returns instead of hanging on a VM that never exits
# its boot loop. Exit 124 from coreutils' timeout is treated as "VM
# reached the timeout", which for a CI boot test is success as long as
# the serial log shows a kernel reaching userspace — we inspect that
# after the QEMU process ends.
if [ "$TIMEOUT" -gt 0 ] 2>/dev/null; then
timeout --foreground --preserve-status "${TIMEOUT}s" bash -c "$(declare -f run_qemu); run_qemu"
rc=$?
if [ $rc -eq 124 ] || [ $rc -eq 137 ]; then
echo "(QEMU terminated after ${TIMEOUT}s boot-test window)"
rc=0
fi
else
run_qemu
rc=$?
fi
echo ""
echo "VM stopped. Serial log: $SERIAL_LOG"
echo "Last 20 lines:"
tail -20 "$SERIAL_LOG" 2>/dev/null
# Boot-sanity check: the CI wrapper wants a non-zero exit only when the
# kernel never reached userspace. Look for a well-known marker emitted
# by live-boot/systemd early in the sequence. If the marker never
# appeared, surface the real failure; otherwise treat "timeout reached
# with a live kernel" as a pass.
if [ "$TIMEOUT" -gt 0 ] 2>/dev/null && [ -f "$SERIAL_LOG" ]; then
if grep -qE "Welcome to Debian|Reached target|systemd\[1\]:" "$SERIAL_LOG"; then
echo " Boot sanity: OK (systemd reached in serial log)"
exit 0
fi
echo " Boot sanity: FAIL — no systemd markers in serial log within ${TIMEOUT}s"
exit 1
fi
exit "${rc:-0}"
+340
View File
@@ -0,0 +1,340 @@
#!/bin/bash
#
# archipelago main menu
# interactive setup for archipelago bitcoin node os
#
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# Colors (256-color — works on Linux console with fbcon)
O=$'\033[38;5;208m' # Orange
W=$'\033[1;37m' # Bold white
D=$'\033[38;5;242m' # Dim
C=$'\033[38;5;37m' # Cyan
G=$'\033[38;5;35m' # Green
R=$'\033[38;5;196m' # Red
Y=$'\033[38;5;220m' # Yellow
N=$'\033[0m' # Reset
# Adaptive centering
get_width() { TW=$(tput cols 2>/dev/null || echo 60); [ "$TW" -gt 120 ] && TW=120; }
get_width
cc() { local s=$(echo -e "$1" | sed 's/\x1b\[[0-9;]*m//g'); local p=$(( (TW - ${#s}) / 2 )); [ $p -lt 0 ] && p=0; printf "%*s" "$p" ""; echo -e "$1"; }
# Box helpers (Claude-style rounded corners)
bw() { echo $((TW > 52 ? 52 : TW - 4)); }
btop() { local w=$(bw); local t="╭"; for i in $(seq 1 $((w-2))); do t="${t}"; done; cc "${D}${t}${N}"; }
bbox() { local w=$(bw); local s=$(echo -e "$1" | sed 's/\x1b\[[0-9;]*m//g'); local pad=$((w - 2 - ${#s})); [ $pad -lt 0 ] && pad=0; local r=""; for i in $(seq 1 $pad); do r="${r} "; done; cc "${D}${N} $1${r}${D}${N}"; }
bbot() { local w=$(bw); local b="╰"; for i in $(seq 1 $((w-2))); do b="${b}"; done; cc "${D}${b}${N}"; }
hrule() { local len=$((TW > 50 ? 50 : TW - 4)); local hr=""; for i in $(seq 1 $len); do hr="${hr}"; done; cc "${D}${hr}${N}"; }
# Install required tools on first run (for live mode)
install_required_tools() {
if [ -f /tmp/.archipelago-tools-installed ]; then
return 0
fi
local NEED_TOOLS=0
for tool in parted debootstrap mkfs.ext4 mkfs.vfat; do
if ! command -v $tool >/dev/null 2>&1; then
NEED_TOOLS=1
break
fi
done
if [ $NEED_TOOLS -eq 1 ]; then
echo ""
cc "${D}installing required tools...${N}"
echo ""
sudo apt-get update -qq 2>/dev/null
sudo apt-get install -y parted debootstrap dosfstools e2fsprogs 2>/dev/null
cc "${G}tools installed${N}"
echo ""
sleep 1
fi
touch /tmp/.archipelago-tools-installed
}
install_required_tools
show_banner() {
get_width
clear
echo ""
echo -e " ${O}▄▀█ █▀▄ █▀▀ █ █ █ █▀█ █▀▀ █ ▄▀█ █▀▀ █▀█${N}"
echo -e " ${O}█▀█ █▀▄ █ █▀█ █ █▀▀ ██▀ █ █▀█ █ █ █ █${N}"
echo -e " ${O}▀ ▀ ▀ ▀ ▀▀▀ ▀ ▀ ▀ ▀ ▀▀▀ ▀▀▀ ▀ ▀ ▀▀▀ ▀▀▀${N}"
echo -e " ${D}bitcoin node os${N}"
echo ""
}
show_status() {
if [ -d /run/live ]; then
cc "${R}live mode${N} ${D}(changes won't persist)${N}"
else
cc "${G}installed${N}"
fi
echo ""
local podman_ok=0
command -v podman >/dev/null 2>&1 && podman_ok=1
if [ $podman_ok -eq 1 ] && podman ps 2>/dev/null | grep -q bitcoind; then
local blocks=$(podman exec bitcoind bitcoin-cli getblockcount 2>/dev/null || echo "syncing")
cc "${G}bitcoin${N} ${D}running ($blocks blocks)${N}"
elif [ $podman_ok -eq 1 ] && podman ps -a 2>/dev/null | grep -q bitcoind; then
cc "${Y}bitcoin${N} ${D}stopped${N}"
fi
if [ $podman_ok -eq 1 ] && podman ps 2>/dev/null | grep -q lnd; then
cc "${G}lightning${N} ${D}running${N}"
elif [ $podman_ok -eq 1 ] && podman ps -a 2>/dev/null | grep -q lnd; then
cc "${Y}lightning${N} ${D}stopped${N}"
fi
echo ""
}
main_menu() {
while true; do
show_banner
show_status
# Connection info
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)
if [ -n "$IP" ]; then
if pgrep -f "archipelago" >/dev/null 2>&1; then
cc "${C}web ui${N} ${W}http://$IP${N}"
else
cc "${C}web ui${N} ${D}http://$IP${N} ${Y}(not started)${N}"
fi
cc "${C}ssh${N} ${D}archipelago@$IP${N}"
else
cc "${D}no network detected${N}"
fi
echo ""
hrule
echo ""
cc "${D}r${N} refresh status ${D}w${N} start web ui"
echo ""
cc "${O}1${N} install to disk ${O}5${N} view logs"
cc "${O}2${N} setup bitcoin core ${O}6${N} network settings"
cc "${O}3${N} setup lightning ${O}7${N} system info"
cc "${O}4${N} setup btcpay server"
echo ""
cc "${D}q quit${N}"
echo ""
local pad=$(( (TW - 18) / 2 ))
[ $pad -lt 0 ] && pad=0
printf "%*s" "$pad" ""
read -p "select option: " choice
case $choice in
r|R)
;;
w|W)
echo ""
if command -v archipelago >/dev/null 2>&1; then
if pgrep -f "archipelago" >/dev/null 2>&1; then
cc "${G}backend already running${N}"
else
cc "${D}starting backend on port 5678...${N}"
nohup archipelago >/tmp/archipelago.log 2>&1 &
sleep 2
if pgrep -f "archipelago" >/dev/null 2>&1; then
cc "${G}backend started${N}"
else
cc "${R}failed — see /tmp/archipelago.log${N}"
fi
fi
IP=$(hostname -I 2>/dev/null | awk '{print $1}')
echo ""
cc "open in browser: ${W}http://$IP${N}"
else
cc "${R}binary not found at /usr/local/bin/archipelago${N}"
fi
echo ""
read -sp " press enter to continue..."
;;
1)
if [ -f "$SCRIPT_DIR/install-to-disk.sh" ]; then
sudo bash "$SCRIPT_DIR/install-to-disk.sh"
else
echo " installer not found at: $SCRIPT_DIR"
fi
read -sp " press enter to continue..."
;;
2)
if [ -f "$SCRIPT_DIR/setup-bitcoin.sh" ]; then
bash "$SCRIPT_DIR/setup-bitcoin.sh"
else
echo " bitcoin setup script not found."
fi
read -sp " press enter to continue..."
;;
3)
if [ -f "$SCRIPT_DIR/setup-lnd.sh" ]; then
bash "$SCRIPT_DIR/setup-lnd.sh"
else
echo " lnd setup script not found."
fi
read -sp " press enter to continue..."
;;
4)
setup_btcpay
read -sp " press enter to continue..."
;;
5)
view_logs
;;
6)
network_settings
read -sp " press enter to continue..."
;;
7)
system_info
read -sp " press enter to continue..."
;;
q|Q)
echo ""
exit 0
;;
*)
sleep 0.5
;;
esac
done
}
setup_btcpay() {
show_banner
cc "${W}btcpay server setup${N}"
cc "${D}self-hosted bitcoin payment processor${N}"
echo ""
if ! podman ps | grep -q bitcoind; then
cc "${R}bitcoin core must be running first${N}"
return
fi
local pad=$(( (TW - 30) / 2 ))
[ $pad -lt 0 ] && pad=0
printf "%*s" "$pad" ""
read -p "setup btcpay server? [y/N]: " SETUP
if [[ ! "$SETUP" =~ ^[Yy]$ ]]; then
return
fi
echo ""
cc "${D}pulling btcpay server image...${N}"
podman pull "${BTCPAY_IMAGE}"
mkdir -p ~/.btcpay
echo ""
cc "${D}full setup: https://docs.btcpayserver.org${N}"
echo ""
}
view_logs() {
show_banner
cc "${W}view logs${N}"
echo ""
cc "${O}1${N} ${D}bitcoin core${N}"
cc "${O}2${N} ${D}lnd${N}"
cc "${O}3${N} ${D}system journal${N}"
cc "${D}b back${N}"
echo ""
local pad=$(( (TW - 10) / 2 ))
[ $pad -lt 0 ] && pad=0
printf "%*s" "$pad" ""
read -p "select: " choice
case $choice in
1)
if podman ps -a | grep -q bitcoind; then
podman logs -f --tail 50 bitcoind
else
cc "${D}bitcoin core not running${N}"
read -sp " press enter..."
fi
;;
2)
if podman ps -a | grep -q lnd; then
podman logs -f --tail 50 lnd
else
cc "${D}lnd not running${N}"
read -sp " press enter..."
fi
;;
3)
journalctl -f
;;
esac
}
network_settings() {
show_banner
cc "${W}network settings${N}"
echo ""
IP=$(hostname -I | awk '{print $1}')
cc "${C}ip${N} ${W}$IP${N}"
echo ""
cc "${D}interfaces:${N}"
ip -br addr | grep -v "^lo" | while read line; do
cc " ${D}$line${N}"
done
echo ""
cc "${D}service ports:${N}"
cc " ${D}8332 bitcoin rpc 9735 lightning p2p${N}"
cc " ${D}8333 bitcoin p2p 10009 lightning grpc${N}"
echo ""
}
system_info() {
show_banner
cc "${W}system information${N}"
echo ""
cc "${C}host${N} ${D}$(hostname)${N}"
cc "${C}kernel${N} ${D}$(uname -r)${N}"
cc "${C}uptime${N} ${D}$(uptime -p 2>/dev/null || echo 'unknown')${N}"
echo ""
local cpu=$(grep "model name" /proc/cpuinfo 2>/dev/null | head -1 | cut -d: -f2 | xargs)
[ -n "$cpu" ] && cc "${C}cpu${N} ${D}${cpu}${N}"
local mem_total=$(free -h 2>/dev/null | grep Mem | awk '{print $2}')
local mem_used=$(free -h 2>/dev/null | grep Mem | awk '{print $3}')
[ -n "$mem_total" ] && cc "${C}memory${N} ${D}${mem_used} / ${mem_total}${N}"
echo ""
cc "${D}disk:${N}"
df -h / | tail -1 | awk '{printf " root: %s / %s (%s used)\n", $3, $2, $5}' | while read line; do
cc "${D}${line}${N}"
done
if [ -d ~/.bitcoin ]; then
local btc_size=$(du -sh ~/.bitcoin 2>/dev/null | cut -f1)
cc " ${D}bitcoin: $btc_size${N}"
fi
echo ""
if command -v podman >/dev/null 2>&1; then
cc "${D}containers:${N}"
podman ps --format " {{.Names}}: {{.Status}}" 2>/dev/null | while read line; do
cc "${D}${line}${N}"
done
fi
echo ""
}
main_menu
@@ -0,0 +1,43 @@
#!/bin/bash
# Configure Nginx to listen on Tailscale IP address
# This script should be run after Tailscale is set up and connected
set -e
echo "🔍 Detecting Tailscale IP..."
# Get Tailscale IP from tailscale0 interface
TAILSCALE_IP=$(ip -4 addr show tailscale0 2>/dev/null | grep -oP '(?<=inet\s)\d+(\.\d+){3}' || echo "")
if [ -z "$TAILSCALE_IP" ]; then
echo "❌ Tailscale interface not found. Is Tailscale running with host networking?"
exit 1
fi
echo "✅ Found Tailscale IP: $TAILSCALE_IP"
NGINX_CONFIG="/etc/nginx/sites-available/archipelago"
# Check if Tailscale IP is already in the config
if grep -q "listen $TAILSCALE_IP:80" "$NGINX_CONFIG"; then
echo "✅ Nginx already configured for Tailscale IP $TAILSCALE_IP"
exit 0
fi
echo "📝 Adding Tailscale IP to Nginx configuration..."
# Backup the config
sudo cp "$NGINX_CONFIG" "$NGINX_CONFIG.backup.$(date +%s)"
# Add Tailscale IP to listen directive (after the first "listen 80;")
sudo sed -i "0,/listen 80;/s//listen 80;\n listen $TAILSCALE_IP:80;/" "$NGINX_CONFIG"
echo "🔍 Testing Nginx configuration..."
sudo nginx -t
echo "🔄 Reloading Nginx..."
sudo systemctl reload nginx
echo "✅ Nginx configured to accept connections from Tailscale!"
echo " Access your Archipelago UI via Tailscale at:"
echo " http://$(hostname).tail<your-tailnet>.ts.net/"
+372
View File
@@ -0,0 +1,372 @@
#!/bin/bash
#
# Archipelago Disk Installer
# Installs Archipelago Bitcoin Node OS to a disk
#
set -e
echo ""
echo "╔═══════════════════════════════════════════════════════════╗"
echo "║ 🏝️ ARCHIPELAGO DISK INSTALLER ║"
echo "╚═══════════════════════════════════════════════════════════╝"
echo ""
# Check if running as root
if [ "$EUID" -ne 0 ]; then
echo "❌ Please run as root: sudo $0"
exit 1
fi
# Install required tools if missing
echo "📦 Checking required tools..."
NEED_INSTALL=""
for tool in parted debootstrap; do
if ! command -v $tool >/dev/null 2>&1; then
NEED_INSTALL="$NEED_INSTALL $tool"
fi
done
if [ -n "$NEED_INSTALL" ]; then
echo "📥 Installing required tools:$NEED_INSTALL"
apt-get update
apt-get install -y $NEED_INSTALL dosfstools e2fsprogs
echo ""
fi
# List available disks
echo "📋 Available disks:"
echo ""
lsblk -d -o NAME,SIZE,MODEL | grep -v loop | grep -v sr
echo ""
# Get target disk
read -p "Enter target disk (e.g., sda, nvme0n1): " TARGET_DISK
TARGET_DEVICE="/dev/$TARGET_DISK"
if [ ! -b "$TARGET_DEVICE" ]; then
echo "❌ Device $TARGET_DEVICE not found"
exit 1
fi
# Confirm
echo ""
echo "⚠️ WARNING: This will ERASE ALL DATA on $TARGET_DEVICE"
echo ""
read -p "Type 'yes' to continue: " CONFIRM
if [ "$CONFIRM" != "yes" ]; then
echo "Aborted."
exit 1
fi
echo ""
echo "🔧 Partitioning $TARGET_DEVICE..."
# Unmount any existing partitions
umount ${TARGET_DEVICE}* 2>/dev/null || true
# Create GPT partition table
parted -s "$TARGET_DEVICE" mklabel gpt
# Create partitions:
# 1. EFI System Partition (512MB)
# 2. Root partition (remaining space)
parted -s "$TARGET_DEVICE" mkpart primary fat32 1MiB 513MiB
parted -s "$TARGET_DEVICE" set 1 esp on
parted -s "$TARGET_DEVICE" mkpart primary ext4 513MiB 100%
# Wait for partitions to appear
sleep 2
# Determine partition names (handle both /dev/sdX and /dev/nvmeXnYpZ naming)
if [[ "$TARGET_DISK" == nvme* ]]; then
EFI_PART="${TARGET_DEVICE}p1"
ROOT_PART="${TARGET_DEVICE}p2"
else
EFI_PART="${TARGET_DEVICE}1"
ROOT_PART="${TARGET_DEVICE}2"
fi
echo "🗂️ Formatting partitions..."
# Format EFI partition
mkfs.vfat -F32 -n ARCHIPELAGO "$EFI_PART"
# Format root partition
mkfs.ext4 -L archipelago-root "$ROOT_PART"
echo "📦 Mounting partitions..."
# Create mount points
mkdir -p /mnt/archipelago
mount "$ROOT_PART" /mnt/archipelago
mkdir -p /mnt/archipelago/boot/efi
mount "$EFI_PART" /mnt/archipelago/boot/efi
echo "📋 Installing base system..."
# Install base system using debootstrap
if command -v debootstrap >/dev/null 2>&1; then
debootstrap --arch=amd64 trixie /mnt/archipelago http://deb.debian.org/debian
else
echo "❌ debootstrap not found. Installing..."
apt-get update && apt-get install -y debootstrap
debootstrap --arch=amd64 trixie /mnt/archipelago http://deb.debian.org/debian
fi
echo "⚙️ Configuring system..."
# Mount virtual filesystems for chroot
mount --bind /dev /mnt/archipelago/dev
mount --bind /dev/pts /mnt/archipelago/dev/pts
mount --bind /proc /mnt/archipelago/proc
mount --bind /sys /mnt/archipelago/sys
mount --bind /sys/firmware/efi/efivars /mnt/archipelago/sys/firmware/efi/efivars 2>/dev/null || true
mount --bind /run /mnt/archipelago/run
# Create fstab
cat > /mnt/archipelago/etc/fstab <<EOF
# Archipelago Bitcoin Node OS
UUID=$(blkid -s UUID -o value "$ROOT_PART") / ext4 errors=remount-ro 0 1
UUID=$(blkid -s UUID -o value "$EFI_PART") /boot/efi vfat umask=0077 0 1
EOF
# Set hostname
echo "archipelago" > /mnt/archipelago/etc/hostname
# Configure hosts
cat > /mnt/archipelago/etc/hosts <<EOF
127.0.0.1 localhost
127.0.1.1 archipelago
::1 localhost ip6-localhost ip6-loopback
EOF
chmod 644 /mnt/archipelago/etc/hosts
# Install bootloader and essential packages in chroot
echo "📦 Configuring package sources..."
# Create sources.list
cat > /mnt/archipelago/etc/apt/sources.list <<EOF
deb http://deb.debian.org/debian trixie main contrib non-free non-free-firmware
deb http://deb.debian.org/debian trixie-updates main contrib non-free non-free-firmware
deb http://security.debian.org/debian-security trixie-security main contrib non-free non-free-firmware
EOF
echo "📥 Updating package lists..."
chroot /mnt/archipelago apt-get update
echo "🔒 Applying Debian security updates..."
chroot /mnt/archipelago apt-get -y full-upgrade
echo "📦 Installing kernel and bootloader..."
chroot /mnt/archipelago apt-get install -y linux-image-amd64 grub-efi-amd64 grub-efi-amd64-signed shim-signed
echo "📦 Installing essential packages..."
chroot /mnt/archipelago apt-get install -y \
sudo \
network-manager \
wpasupplicant \
wireless-regdb \
iw \
rfkill \
pciutils \
usbutils \
polkitd \
openssh-server \
curl \
wget \
htop \
vim-tiny \
nano \
ca-certificates \
chrony
echo "📦 Installing container tools..."
chroot /mnt/archipelago apt-get install -y podman catatonit || echo "⚠️ Podman/catatonit not available in base repos, will use containers.io later"
echo "🔧 Installing GRUB bootloader..."
# Need to run grub-install inside chroot with proper environment
chroot /mnt/archipelago /bin/bash -c "
grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=archipelago --recheck 2>&1 || \
grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=archipelago --removable 2>&1 || \
echo '⚠️ GRUB install had issues, trying alternative...'
"
chroot /mnt/archipelago update-grub
echo "👤 Creating archipelago user..."
# Create user first, then add to groups that exist
chroot /mnt/archipelago useradd -m -s /bin/bash archipelago || echo "User may already exist"
chroot /mnt/archipelago usermod -aG sudo archipelago || true
chroot /mnt/archipelago usermod -aG podman archipelago 2>/dev/null || true
# Set password using chpasswd
echo "archipelago:archipelago" | chroot /mnt/archipelago chpasswd
echo "⚙️ Enabling services..."
chroot /mnt/archipelago systemctl enable NetworkManager || true
chroot /mnt/archipelago systemctl enable polkit || chroot /mnt/archipelago systemctl enable polkit.service || true
chroot /mnt/archipelago systemctl enable ssh || chroot /mnt/archipelago systemctl enable sshd || true
chroot /mnt/archipelago systemctl enable chrony || true
mkdir -p /mnt/archipelago/etc/polkit-1/rules.d
cat > /mnt/archipelago/etc/polkit-1/rules.d/49-archipelago-networkmanager.rules <<'EOF'
polkit.addRule(function(action, subject) {
if (subject.user == "archipelago" && action.id.indexOf("org.freedesktop.NetworkManager.") == 0) {
return polkit.Result.YES;
}
});
EOF
chmod 644 /mnt/archipelago/etc/polkit-1/rules.d/49-archipelago-networkmanager.rules
# Remove policy-rc.d so services can start on first boot
rm -f /mnt/archipelago/usr/sbin/policy-rc.d
echo "💾 Creating swap file..."
TOTAL_MEM_KB=$(chroot /mnt/archipelago grep MemTotal /proc/meminfo 2>/dev/null | awk '{print $2}')
SWAP_GB=${TOTAL_MEM_KB:+$((TOTAL_MEM_KB / 1024 / 1024))}
SWAP_GB=${SWAP_GB:-4}
[ "$SWAP_GB" -gt 8 ] && SWAP_GB=8
[ "$SWAP_GB" -lt 2 ] && SWAP_GB=2
fallocate -l ${SWAP_GB}G /mnt/archipelago/swapfile 2>/dev/null || dd if=/dev/zero of=/mnt/archipelago/swapfile bs=1G count=$SWAP_GB status=progress
chmod 600 /mnt/archipelago/swapfile
chroot /mnt/archipelago mkswap /swapfile
echo '/swapfile none swap sw 0 0' >> /mnt/archipelago/etc/fstab
echo "✅ Created ${SWAP_GB}G swap"
echo "📁 Creating Archipelago directories..."
chroot /mnt/archipelago mkdir -p /var/lib/archipelago/{data,config,containers}
chroot /mnt/archipelago mkdir -p /etc/archipelago
chroot /mnt/archipelago chown -R archipelago:archipelago /var/lib/archipelago
echo "✅ Base system configured"
# Copy Archipelago files
echo "📋 Installing Archipelago components..."
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
echo " Copying from: $BOOT_MEDIA/archipelago"
# Copy entire archipelago directory
mkdir -p /mnt/archipelago/opt/archipelago
cp -r "$BOOT_MEDIA/archipelago/"* /mnt/archipelago/opt/archipelago/ 2>/dev/null || true
# Install binaries
if [ -d "$BOOT_MEDIA/archipelago/bin" ]; then
cp "$BOOT_MEDIA/archipelago/bin/"* /mnt/archipelago/usr/local/bin/ 2>/dev/null || true
chmod +x /mnt/archipelago/usr/local/bin/* 2>/dev/null || true
fi
# Install scripts
if [ -d "$BOOT_MEDIA/archipelago/scripts" ]; then
mkdir -p /mnt/archipelago/opt/archipelago/scripts
cp "$BOOT_MEDIA/archipelago/scripts/"* /mnt/archipelago/opt/archipelago/scripts/ 2>/dev/null || true
chmod +x /mnt/archipelago/opt/archipelago/scripts/*.sh 2>/dev/null || true
fi
# Install app manifests
if [ -d "$BOOT_MEDIA/archipelago/apps" ]; then
mkdir -p /mnt/archipelago/etc/archipelago
cp -r "$BOOT_MEDIA/archipelago/apps" /mnt/archipelago/etc/archipelago/
fi
# Mesh radio udev rule: stable /dev/mesh-radio symlink + dialout perms
# for known LoRa USB-serial chips (this installer never shipped it)
for p in "$BOOT_MEDIA/99-mesh-radio.rules" "$BOOT_MEDIA/archipelago/configs/99-mesh-radio.rules"; do
if [ -f "$p" ]; then
mkdir -p /mnt/archipelago/etc/udev/rules.d
cp "$p" /mnt/archipelago/etc/udev/rules.d/99-mesh-radio.rules
chmod 644 /mnt/archipelago/etc/udev/rules.d/99-mesh-radio.rules
break
fi
done
# Create profile.d script for welcome message on login
mkdir -p /mnt/archipelago/etc/profile.d
cat > /mnt/archipelago/etc/profile.d/archipelago.sh <<'PROFILE_EOF'
#!/bin/bash
# Archipelago welcome message
if [ -t 0 ] && [ -z "$ARCHIPELAGO_WELCOMED" ]; then
export ARCHIPELAGO_WELCOMED=1
IP=$(hostname -I 2>/dev/null | awk '{print $1}')
echo ""
echo " ╔═══════════════════════════════════════════════════════════╗"
echo " ║ 🏝️ ARCHIPELAGO BITCOIN NODE OS ║"
echo " ╚═══════════════════════════════════════════════════════════╝"
echo ""
if [ -n "$IP" ]; then
echo " ┌─────────────────────────────────────────────────────────────┐"
echo " │ 🌐 Web UI: http://$IP:5678 │"
echo " │ 📡 SSH: ssh archipelago@$IP │"
echo " └─────────────────────────────────────────────────────────────┘"
echo ""
fi
echo " Commands:"
echo " archipelago - Start backend server"
echo " archipelago-menu - Open setup menu"
echo ""
fi
PROFILE_EOF
chmod +x /mnt/archipelago/etc/profile.d/archipelago.sh
# Create systemd service to auto-start archipelago backend
mkdir -p /mnt/archipelago/etc/systemd/system
cat > /mnt/archipelago/etc/systemd/system/archipelago.service <<'SERVICE_EOF'
[Unit]
Description=Archipelago Backend Server
After=network-online.target
Wants=network-online.target
# Never start before the data volume (if one exists) is mounted — without this
# the service races the mount on cold boot and "[FAILED]"-loops until it lands (B17)
RequiresMountsFor=/var/lib/archipelago
[Service]
Type=simple
User=archipelago
ExecStart=/usr/local/bin/archipelago
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
SERVICE_EOF
# Enable the service
chroot /mnt/archipelago systemctl enable archipelago.service 2>/dev/null || true
fi
echo "🧹 Cleaning up..."
# Unmount in reverse order
sync
umount /mnt/archipelago/run 2>/dev/null || true
umount /mnt/archipelago/sys/firmware/efi/efivars 2>/dev/null || true
umount /mnt/archipelago/sys 2>/dev/null || true
umount /mnt/archipelago/proc 2>/dev/null || true
umount /mnt/archipelago/dev/pts 2>/dev/null || true
umount /mnt/archipelago/dev 2>/dev/null || true
umount /mnt/archipelago/boot/efi 2>/dev/null || true
umount /mnt/archipelago 2>/dev/null || true
echo ""
echo "╔═══════════════════════════════════════════════════════════╗"
echo "║ ✅ INSTALLATION COMPLETE! ║"
echo "╚═══════════════════════════════════════════════════════════╝"
echo ""
echo "Remove the USB drive and reboot to start Archipelago."
echo ""
echo "Default login:"
echo " Username: archipelago"
echo " Password: archipelago"
echo ""
echo "⚠️ Please change the password after first login!"
echo ""
+148
View File
@@ -0,0 +1,148 @@
#!/bin/bash
#
# Bitcoin Core Setup Script for Archipelago
# Sets up Bitcoin Core in a Podman container
#
set -e
echo ""
echo "╔═══════════════════════════════════════════════════════════╗"
echo "║ ₿ BITCOIN CORE SETUP ║"
echo "╚═══════════════════════════════════════════════════════════╝"
echo ""
# Check if running as root
if [ "$EUID" -eq 0 ]; then
echo "⚠️ Running as root. For rootless Podman, run as regular user."
fi
# Check for Podman
if ! command -v podman >/dev/null 2>&1; then
echo "📦 Installing Podman..."
sudo apt-get update
sudo apt-get install -y podman podman-compose
fi
# Create data directory
BITCOIN_DATA="${HOME}/.bitcoin"
mkdir -p "$BITCOIN_DATA"
echo "📍 Bitcoin data directory: $BITCOIN_DATA"
echo ""
# Ask for configuration
echo "Bitcoin Core Configuration:"
echo ""
read -p "Enable pruning? (saves disk space) [y/N]: " PRUNE
read -p "Enable txindex? (required for some apps) [y/N]: " TXINDEX
read -p "RPC username [bitcoin]: " RPC_USER
RPC_USER=${RPC_USER:-bitcoin}
read -p "RPC password [randomly generated]: " RPC_PASS
if [ -z "$RPC_PASS" ]; then
RPC_PASS=$(openssl rand -hex 16)
echo " Generated RPC password: $RPC_PASS"
fi
# Create bitcoin.conf
cat > "$BITCOIN_DATA/bitcoin.conf" <<EOF
# Archipelago Bitcoin Core Configuration
# Network
server=1
listen=1
# RPC
rpcuser=$RPC_USER
rpcpassword=$RPC_PASS
rpcallowip=10.0.0.0/8
rpcallowip=172.16.0.0/12
rpcallowip=192.168.0.0/16
# Performance
dbcache=450
maxmempool=300
EOF
if [[ "$PRUNE" =~ ^[Yy]$ ]]; then
echo "prune=550" >> "$BITCOIN_DATA/bitcoin.conf"
echo " Pruning enabled (550MB)"
fi
if [[ "$TXINDEX" =~ ^[Yy]$ ]]; then
echo "txindex=1" >> "$BITCOIN_DATA/bitcoin.conf"
echo " Transaction index enabled"
fi
echo ""
echo "📋 Created bitcoin.conf"
echo ""
# Pull Bitcoin Core image
echo "🐳 Pulling Bitcoin Core container image..."
podman pull docker.io/lncm/bitcoind:v27.0
# Create systemd user service for Bitcoin Core
mkdir -p ~/.config/systemd/user
cat > ~/.config/systemd/user/bitcoind.service <<EOF
[Unit]
Description=Bitcoin Core (Podman)
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStartPre=-/usr/bin/podman stop bitcoind
ExecStartPre=-/usr/bin/podman rm bitcoind
ExecStart=/usr/bin/podman run --name bitcoind \\
--rm \\
-v ${BITCOIN_DATA}:/data/.bitcoin:Z \\
-p 8332:8332 \\
-p 8333:8333 \\
docker.io/lncm/bitcoind:v27.0
ExecStop=/usr/bin/podman stop bitcoind
Restart=always
RestartSec=10
[Install]
WantedBy=default.target
EOF
echo "📋 Created systemd service"
echo ""
# Enable and start service
systemctl --user daemon-reload
systemctl --user enable bitcoind.service
echo ""
echo "╔═══════════════════════════════════════════════════════════╗"
echo "║ ✅ BITCOIN CORE SETUP COMPLETE! ║"
echo "╚═══════════════════════════════════════════════════════════╝"
echo ""
echo "Commands:"
echo " Start: systemctl --user start bitcoind"
echo " Stop: systemctl --user stop bitcoind"
echo " Status: systemctl --user status bitcoind"
echo " Logs: podman logs -f bitcoind"
echo ""
echo "Bitcoin CLI:"
echo " podman exec bitcoind bitcoin-cli -getinfo"
echo ""
echo "RPC Credentials (save these!):"
echo " User: $RPC_USER"
echo " Pass: $RPC_PASS"
echo ""
read -p "Start Bitcoin Core now? [Y/n]: " START_NOW
if [[ ! "$START_NOW" =~ ^[Nn]$ ]]; then
echo ""
echo "🚀 Starting Bitcoin Core..."
systemctl --user start bitcoind
echo ""
echo "Bitcoin Core is syncing. This will take several hours/days."
echo "Monitor with: podman logs -f bitcoind"
fi
+160
View File
@@ -0,0 +1,160 @@
#!/bin/bash
#
# LND (Lightning Network Daemon) Setup Script for Archipelago
# Sets up LND in a Podman container
#
set -e
echo ""
echo "╔═══════════════════════════════════════════════════════════╗"
echo "║ ⚡ LND (LIGHTNING) SETUP ║"
echo "╚═══════════════════════════════════════════════════════════╝"
echo ""
# Check for Podman
if ! command -v podman >/dev/null 2>&1; then
echo "❌ Podman not found. Please run setup-bitcoin.sh first."
exit 1
fi
# Check if Bitcoin Core is running
if ! podman ps | grep -q bitcoind; then
echo "⚠️ Bitcoin Core is not running."
echo " LND requires a synced Bitcoin Core node."
read -p "Continue anyway? [y/N]: " CONTINUE
if [[ ! "$CONTINUE" =~ ^[Yy]$ ]]; then
exit 1
fi
fi
# Create data directory
LND_DATA="${HOME}/.lnd"
mkdir -p "$LND_DATA"
echo "📍 LND data directory: $LND_DATA"
echo ""
# Get Bitcoin RPC credentials
BITCOIN_CONF="${HOME}/.bitcoin/bitcoin.conf"
if [ -f "$BITCOIN_CONF" ]; then
RPC_USER=$(grep "^rpcuser=" "$BITCOIN_CONF" | cut -d= -f2)
RPC_PASS=$(grep "^rpcpassword=" "$BITCOIN_CONF" | cut -d= -f2)
else
read -p "Bitcoin RPC username: " RPC_USER
read -p "Bitcoin RPC password: " RPC_PASS
fi
# Ask for LND configuration
echo "LND Configuration:"
echo ""
read -p "Node alias (public name): " LND_ALIAS
LND_ALIAS=${LND_ALIAS:-archipelago-node}
# Create lnd.conf
cat > "$LND_DATA/lnd.conf" <<EOF
# Archipelago LND Configuration
[Application Options]
alias=$LND_ALIAS
color=#FF9900
listen=0.0.0.0:9735
rpclisten=0.0.0.0:10009
restlisten=0.0.0.0:8080
# Automatically unlock wallet (create password file after first run)
# wallet-unlock-password-file=/data/.lnd/password.txt
[Bitcoin]
bitcoin.active=true
bitcoin.mainnet=true
bitcoin.node=bitcoind
[Bitcoind]
bitcoind.rpchost=host.containers.internal:8332
bitcoind.rpcuser=$RPC_USER
bitcoind.rpcpass=$RPC_PASS
bitcoind.zmqpubrawblock=tcp://host.containers.internal:28332
bitcoind.zmqpubrawtx=tcp://host.containers.internal:28333
[tor]
tor.active=false
[wtclient]
wtclient.active=true
EOF
echo "📋 Created lnd.conf"
echo ""
# Pull LND image
echo "🐳 Pulling LND container image..."
podman pull docker.io/lightninglabs/lnd:v0.18.0-beta
# Create systemd user service for LND
mkdir -p ~/.config/systemd/user
cat > ~/.config/systemd/user/lnd.service <<EOF
[Unit]
Description=LND Lightning Network Daemon (Podman)
After=bitcoind.service
Requires=bitcoind.service
[Service]
Type=simple
ExecStartPre=-/usr/bin/podman stop lnd
ExecStartPre=-/usr/bin/podman rm lnd
ExecStart=/usr/bin/podman run --name lnd \\
--rm \\
--add-host=host.containers.internal:host-gateway \\
-v ${LND_DATA}:/data/.lnd:Z \\
-p 9735:9735 \\
-p 10009:10009 \\
-p 18080:8080 \\
docker.io/lightninglabs/lnd:v0.18.0-beta \\
--configfile=/data/.lnd/lnd.conf
ExecStop=/usr/bin/podman stop lnd
Restart=always
RestartSec=10
[Install]
WantedBy=default.target
EOF
echo "📋 Created systemd service"
echo ""
# Enable service
systemctl --user daemon-reload
systemctl --user enable lnd.service
echo ""
echo "╔═══════════════════════════════════════════════════════════╗"
echo "║ ✅ LND SETUP COMPLETE! ║"
echo "╚═══════════════════════════════════════════════════════════╝"
echo ""
echo "Commands:"
echo " Start: systemctl --user start lnd"
echo " Stop: systemctl --user stop lnd"
echo " Status: systemctl --user status lnd"
echo " Logs: podman logs -f lnd"
echo ""
echo "LND CLI:"
echo " podman exec lnd lncli --network=mainnet getinfo"
echo ""
echo "⚠️ IMPORTANT: On first start, you need to create a wallet:"
echo " 1. Start LND: systemctl --user start lnd"
echo " 2. Create wallet: podman exec -it lnd lncli create"
echo " 3. Save your seed phrase securely!"
echo ""
read -p "Start LND now? [Y/n]: " START_NOW
if [[ ! "$START_NOW" =~ ^[Nn]$ ]]; then
echo ""
echo "🚀 Starting LND..."
systemctl --user start lnd
sleep 3
echo ""
echo "LND is starting. Create your wallet with:"
echo " podman exec -it lnd lncli create"
fi
@@ -0,0 +1,132 @@
#!/usr/bin/env python3
"""
Simple API server for Archipelago Web UI
Serves static files and handles basic API endpoints
"""
import http.server
import socketserver
import json
import os
from urllib.parse import urlparse, parse_qs
PORT = 80
WEB_DIR = None
# Find web UI directory
for path in ['/opt/archipelago/web-ui', '/run/live/medium/archipelago/web-ui', '/lib/live/mount/medium/archipelago/web-ui']:
if os.path.isdir(path):
WEB_DIR = path
break
if not WEB_DIR:
print("Web UI directory not found!")
exit(1)
os.chdir(WEB_DIR)
class ArchipelagoHandler(http.server.SimpleHTTPRequestHandler):
def do_POST(self):
"""Handle POST requests with mock responses"""
content_length = int(self.headers.get('Content-Length', 0))
post_data = self.rfile.read(content_length) if content_length > 0 else b''
path = urlparse(self.path).path
# Mock API responses
response = {"success": True}
if '/api/auth' in path or '/auth' in path:
response = {
"success": True,
"token": "mock-token-12345",
"user": "archipelago"
}
elif '/api/setup' in path or '/setup' in path:
response = {
"success": True,
"status": "complete"
}
elif '/api/status' in path or '/status' in path:
response = {
"success": True,
"status": "running",
"version": "0.1.0",
"hostname": "archipelago"
}
elif '/api/apps' in path or '/apps' in path:
response = {
"success": True,
"apps": []
}
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
self.wfile.write(json.dumps(response).encode())
def do_OPTIONS(self):
"""Handle CORS preflight"""
self.send_response(200)
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
self.send_header('Access-Control-Allow-Headers', 'Content-Type, Authorization')
self.end_headers()
def do_GET(self):
"""Handle GET requests - serve files or API"""
path = urlparse(self.path).path
if path.startswith('/api/'):
# Mock API GET endpoints
response = {"success": True}
if '/status' in path:
response = {
"success": True,
"status": "running",
"version": "0.1.0"
}
elif '/apps' in path:
response = {
"success": True,
"apps": [
{"id": "bitcoin", "name": "Bitcoin Core", "status": "available"},
{"id": "lnd", "name": "Lightning (LND)", "status": "available"}
]
}
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
self.wfile.write(json.dumps(response).encode())
else:
# Serve static files, default to index.html for SPA routing
if not os.path.exists(self.translate_path(self.path)) or path == '/':
self.path = '/index.html'
super().do_GET()
def log_message(self, format, *args):
"""Quieter logging"""
if '404' in str(args) or 'error' in str(args).lower():
print(f" {args[0]}")
print(f"""
╔═══════════════════════════════════════════════════════════╗
║ 🏝️ ARCHIPELAGO WEB UI ║
╚═══════════════════════════════════════════════════════════╝
Serving from: {WEB_DIR}
🌐 Open in your browser: http://localhost:{PORT}
Press Ctrl+C to stop
""")
with socketserver.TCPServer(("0.0.0.0", PORT), ArchipelagoHandler) as httpd:
try:
httpd.serve_forever()
except KeyboardInterrupt:
print("\nShutting down...")
+72
View File
@@ -0,0 +1,72 @@
#!/bin/bash
#
# Start Archipelago Backend Server
# Serves the Vue.js UI and handles all API calls
#
# Find archipelago binary
ARCHIPELAGO_BIN=""
for path in /usr/local/bin/archipelago /opt/archipelago/bin/archipelago /run/live/medium/archipelago/bin/archipelago; do
if [ -x "$path" ]; then
ARCHIPELAGO_BIN="$path"
break
fi
done
# Get IP address
IP=$(hostname -I 2>/dev/null | awk '{print $1}')
if [ -z "$IP" ]; then
IP="localhost"
fi
echo ""
echo "╔═══════════════════════════════════════════════════════════╗"
echo "║ 🏝️ ARCHIPELAGO BITCOIN NODE OS ║"
echo "╚═══════════════════════════════════════════════════════════╝"
echo ""
echo " Starting Archipelago server..."
echo ""
echo " ┌─────────────────────────────────────────────────────────┐"
echo " │ │"
echo " │ 🌐 OPEN IN YOUR BROWSER: │"
echo " │ │"
echo " │ http://$IP "
echo " │ │"
echo " └─────────────────────────────────────────────────────────┘"
echo ""
if [ -n "$ARCHIPELAGO_BIN" ]; then
echo " Using backend: $ARCHIPELAGO_BIN"
echo " Press Ctrl+C to stop"
echo ""
# Set environment for dev mode
export ARCHIPELAGO_DEV_MODE=true
export ARCHIPELAGO_DATA_DIR=/var/lib/archipelago
export RUST_LOG=info
# Create data directory
sudo mkdir -p /var/lib/archipelago 2>/dev/null
exec "$ARCHIPELAGO_BIN"
else
echo " ⚠️ Backend binary not found, using static file server"
echo ""
# Fallback to static file server
WEB_UI_DIR=""
for path in /opt/archipelago/web-ui /run/live/medium/archipelago/web-ui; do
if [ -d "$path" ]; then
WEB_UI_DIR="$path"
break
fi
done
if [ -n "$WEB_UI_DIR" ]; then
cd "$WEB_UI_DIR"
exec python3 -m http.server 80 --bind 0.0.0.0
else
echo "❌ Neither backend nor web UI found"
exit 1
fi
fi
Binary file not shown.

After

Width:  |  Height:  |  Size: 837 KiB

@@ -0,0 +1,52 @@
# Archipelago GRUB Theme
# Dark background with Bitcoin orange accents
title-text: ""
desktop-color: "#0a0a0a"
desktop-image: "background.png"
desktop-image-scale-method: "stretch"
+ boot_menu {
left = 10%
top = 35%
width = 80%
height = 40%
item_font = "DejaVu Sans Bold 16"
item_color = "#aaaaaa"
selected_item_font = "DejaVu Sans Bold 16"
selected_item_color = "#f7931a"
item_height = 36
item_spacing = 8
item_padding = 16
scrollbar = false
}
+ label {
left = 10%
top = 18%
width = 80%
font = "DejaVu Sans Mono Bold 24"
text = "a r c h i p e l a g o"
color = "#f7931a"
align = "center"
}
+ label {
left = 10%
top = 26%
width = 80%
font = "DejaVu Sans Bold 14"
text = "bitcoin node os"
color = "#888888"
align = "center"
}
+ label {
left = 10%
top = 90%
width = 80%
font = "DejaVu Sans Bold 12"
text = "press tab to edit | use arrow keys to select | enter to boot"
color = "#555555"
align = "center"
}
@@ -0,0 +1,8 @@
[Plymouth Theme]
Name=Archipelago
Description=Archipelago Bitcoin Node OS — cyberpunk boot splash
ModuleName=script
[script]
ImageDir=/usr/share/plymouth/themes/archipelago
ScriptFile=/usr/share/plymouth/themes/archipelago/archipelago.script
@@ -0,0 +1,109 @@
// Archipelago Plymouth Theme — cyberpunk boot splash
// Dark background, neon orange pixel-art logo, animated progress bar
// Screen dimensions
screen_w = Window.GetWidth();
screen_h = Window.GetHeight();
// Background — solid near-black (the GRUB background handles the fancy stuff)
Window.SetBackgroundTopColor(0.02, 0.02, 0.04);
Window.SetBackgroundBottomColor(0.01, 0.01, 0.02);
// Load logo image (generated during build)
logo_image = Image("logo.png");
logo_sprite = Sprite(logo_image);
logo_w = logo_image.GetWidth();
logo_h = logo_image.GetHeight();
logo_sprite.SetX(screen_w / 2 - logo_w / 2);
logo_sprite.SetY(screen_h / 2 - logo_h / 2 - 60);
logo_sprite.SetOpacity(1.0);
// --- Progress bar ---
// Neon orange bar with glow, centered below logo
bar_w = 300;
bar_h = 4;
bar_x = screen_w / 2 - bar_w / 2;
bar_y = screen_h / 2 + logo_h / 2;
// Progress bar background (dark glass)
bar_bg = Image(bar_w, bar_h);
for (x = 0; x < bar_w; x++) {
for (y = 0; y < bar_h; y++) {
bar_bg.SetPixel(x, y, 0.1, 0.1, 0.12, 0.8);
}
}
bar_bg_sprite = Sprite(bar_bg);
bar_bg_sprite.SetX(bar_x);
bar_bg_sprite.SetY(bar_y);
// Progress bar fill (neon orange)
progress_val = 0;
fun refresh_callback() {
// Animate progress smoothly
if (Plymouth.GetMode() == "boot") {
progress_val = progress_val + 0.002;
if (progress_val > 1.0) progress_val = 1.0;
}
fill_w = Math.Int(bar_w * progress_val);
if (fill_w > 0) {
bar_fill = Image(fill_w, bar_h);
for (x = 0; x < fill_w; x++) {
for (y = 0; y < bar_h; y++) {
// Orange: rgb(251, 146, 60) = 0.984, 0.573, 0.235
bar_fill.SetPixel(x, y, 0.984, 0.573, 0.235, 1.0);
}
}
bar_fill_sprite = Sprite(bar_fill);
bar_fill_sprite.SetX(bar_x);
bar_fill_sprite.SetY(bar_y);
bar_fill_sprite.SetZ(1);
}
}
Plymouth.SetRefreshFunction(refresh_callback);
// --- Boot progress callback ---
fun boot_progress_callback(duration, progress) {
progress_val = progress;
}
Plymouth.SetBootProgressFunction(boot_progress_callback);
// --- Status message (below progress bar) ---
msg_sprite = Sprite();
msg_sprite.SetPosition(screen_w / 2, bar_y + 30, 2);
fun message_callback(text) {
// Plymouth passes boot messages here
// We could render them but keeping it clean — just the logo and bar
}
Plymouth.SetMessageFunction(message_callback);
// --- Password prompt (for LUKS) ---
fun display_password_callback(prompt, bullets) {
// LUKS unlock prompt
pass_image = Image.Text(prompt, 0.984, 0.573, 0.235);
pass_sprite = Sprite(pass_image);
pass_sprite.SetX(screen_w / 2 - pass_image.GetWidth() / 2);
pass_sprite.SetY(screen_h / 2 + 80);
// Bullet dots for password
if (bullets > 0) {
bullet_text = "";
for (i = 0; i < bullets; i++) {
bullet_text = bullet_text + "* ";
}
bullet_image = Image.Text(bullet_text, 0.984, 0.573, 0.235);
bullet_sprite = Sprite(bullet_image);
bullet_sprite.SetX(screen_w / 2 - bullet_image.GetWidth() / 2);
bullet_sprite.SetY(screen_h / 2 + 110);
}
}
Plymouth.SetDisplayPasswordFunction(display_password_callback);
// --- Quit callback ---
fun quit_callback() {
logo_sprite.SetOpacity(0);
}
Plymouth.SetQuitFunction(quit_callback);
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env bash
# Build the Archipelago Debian installer ISO.
#
# The historical ISO builder remains archived because OTA tarballs are the
# normal release path. This wrapper keeps the documented ISO command working
# by running a temporary active-layout copy of that builder with fixed paths.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ARCHIVED_BUILDER="$SCRIPT_DIR/_archived/build-auto-installer-iso.sh"
TMP_DIR="$(mktemp -d -t archipelago-iso-builder.XXXXXX)"
cleanup() {
rm -rf "$TMP_DIR"
}
trap cleanup EXIT
if [ ! -f "$ARCHIVED_BUILDER" ]; then
echo "Archived ISO builder not found: $ARCHIVED_BUILDER" >&2
exit 1
fi
TMP_BUILDER="$TMP_DIR/build-auto-installer-iso.sh"
cp "$ARCHIVED_BUILDER" "$TMP_BUILDER"
# The archived builder lived one directory deeper at image-recipe/_archived/.
# Rewrite only path expressions that were relative to that old location.
perl -0pi -e 's#SCRIPT_DIR="\$\(cd "\$\(dirname "\$0"\)" && pwd\)"#SCRIPT_DIR="__ARCHIPELAGO_IMAGE_RECIPE_DIR__"#g' "$TMP_BUILDER"
# Repo-root references: _archived/../../X becomes image-recipe/../X.
perl -0pi -e 's#\$SCRIPT_DIR/\.\./\.\./#\$SCRIPT_DIR/../#g' "$TMP_BUILDER"
perl -0pi -e 's#\$SCRIPT_DIR/\.\./\.\."#\$SCRIPT_DIR/.."#g' "$TMP_BUILDER"
# configs/ lives inside image-recipe/ itself: _archived/../configs becomes image-recipe/configs.
perl -0pi -e 's#\$SCRIPT_DIR/\.\./configs#\$SCRIPT_DIR/configs#g' "$TMP_BUILDER"
perl -0pi -e 's#"\$\(dirname "\$0"\)/\.\./\.\./scripts#"$(dirname "$0")/../scripts#g' "$TMP_BUILDER"
perl -0pi -e "s#__ARCHIPELAGO_IMAGE_RECIPE_DIR__#${SCRIPT_DIR}#g" "$TMP_BUILDER"
chmod +x "$TMP_BUILDER"
exec bash "$TMP_BUILDER" "$@"
+57
View File
@@ -0,0 +1,57 @@
#!/bin/bash
# Build Archipelago Vue.js frontend for production
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
echo "🎨 Building Archipelago Frontend (Vue.js)"
echo ""
# Navigate to frontend directory
cd "$PROJECT_ROOT/neode-ui"
# Check if node_modules exists
if [ ! -d "node_modules" ]; then
echo "📦 Installing dependencies..."
npm install || {
echo "❌ npm install failed"
exit 1
}
fi
# Build for production
echo "🔨 Building production bundle (skipping type check for speed)..."
npm run build:docker || {
echo "❌ Build failed"
exit 1
}
# Create output directory
BUILD_DIR="$SCRIPT_DIR/build/frontend"
mkdir -p "$BUILD_DIR"
# Copy dist to build directory (check both possible output locations)
echo "📋 Copying build artifacts..."
if [ -d "../web/dist/neode-ui" ]; then
cp -r ../web/dist/neode-ui/* "$BUILD_DIR/" || {
echo "❌ Failed to copy build artifacts"
exit 1
}
elif [ -d "dist" ]; then
cp -r dist/* "$BUILD_DIR/" || {
echo "❌ Failed to copy build artifacts"
exit 1
}
else
echo "❌ Build output not found"
exit 1
fi
echo ""
echo "✅ Frontend built successfully!"
echo " Output: $BUILD_DIR"
echo " Files:"
ls -lh "$BUILD_DIR" | head -10
echo ""
+9
View File
@@ -0,0 +1,9 @@
# Stable symlink for USB serial adapters used as mesh radios.
# Creates /dev/mesh-radio pointing to the underlying ttyUSB device.
# Supports MeshCore and Meshtastic radios using CP2102 (Heltec V3),
# CH340 (T-Beam), FTDI (RAK WisBlock), and known USB CDC ACM radios.
SUBSYSTEM=="tty", ATTRS{idVendor}=="10c4", ATTRS{idProduct}=="ea60", SYMLINK+="mesh-radio", MODE="0660", GROUP="dialout"
SUBSYSTEM=="tty", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="7523", SYMLINK+="mesh-radio", MODE="0660", GROUP="dialout"
SUBSYSTEM=="tty", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", SYMLINK+="mesh-radio", MODE="0660", GROUP="dialout"
SUBSYSTEM=="tty", ATTRS{idVendor}=="239a", KERNEL=="ttyACM[0-9]*", SYMLINK+="mesh-radio", MODE="0660", GROUP="dialout"
SUBSYSTEM=="tty", ATTRS{idVendor}=="2e8a", KERNEL=="ttyACM[0-9]*", SYMLINK+="mesh-radio", MODE="0660", GROUP="dialout"
@@ -0,0 +1,12 @@
[Unit]
Description=Archipelago Container Doctor
After=archipelago.service
[Service]
Type=oneshot
# Runs as root: needs to kill orphaned conmon processes, fix permissions
User=root
ExecStart=/home/archipelago/archy/scripts/container-doctor.sh --local
TimeoutStartSec=300
StandardOutput=journal
StandardError=journal
@@ -0,0 +1,14 @@
[Unit]
Description=Archipelago container doctor (periodic)
[Timer]
# First run 2 minutes after boot, then every 5 minutes. The doctor is
# idempotent and exits quickly when no drift exists; this keeps vanished
# rootless port listeners and stopped containers from remaining broken.
OnBootSec=2min
OnUnitActiveSec=5min
# Jitter to avoid load spikes
RandomizedDelaySec=60
[Install]
WantedBy=timers.target
@@ -0,0 +1,21 @@
[Unit]
Description=Archipelago FIPS mesh transport (wraps upstream fips daemon)
# Stay dark until onboarding materialises the seed-derived key. Archipelago
# backend unmasks + starts this unit via `sudo systemctl` once the key is
# present; pre-onboarding the unit must be masked so no traffic is sent
# from an ephemeral identity.
ConditionPathExists=/var/lib/archipelago/identity/fips_key
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStartPre=/bin/sh -c 'test -x /usr/bin/fips || { echo "fips daemon not installed — run fips.install from dashboard" >&2; exit 1; }'
ExecStart=/usr/bin/fips --config /etc/fips/fips.yaml
Restart=on-failure
RestartSec=5
# UDP 8668 is reachable on all interfaces by default; the daemon does its
# own Noise authentication so no firewall gate is added here.
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,143 @@
#!/bin/bash
# Start a dedicated X server for the attached kiosk display.
/usr/bin/Xorg :0 vt1 -nolisten tcp -keeptty &
XPID=$!
export DISPLAY=:0
export HOME=/home/archipelago
X_READY=false
for _ in $(seq 1 30); do
if kill -0 "$XPID" 2>/dev/null && xrandr --query >/tmp/archipelago-kiosk-xrandr.txt 2>/dev/null; then
X_READY=true
break
fi
sleep 0.5
done
if [ "$X_READY" != "true" ]; then
echo 'ERROR: Xorg failed to become ready'
kill "$XPID" 2>/dev/null || true
exit 1
fi
KIOSK_SAFE_AREA_X_PX=${ARCHIPELAGO_KIOSK_SAFE_AREA_X_PX:-}
KIOSK_SAFE_AREA_Y_PX=${ARCHIPELAGO_KIOSK_SAFE_AREA_Y_PX:-}
# Actual mode of the kiosk output — configure_display overwrites these so
# Chromium's window matches the panel instead of assuming 1080p (a 1366x768
# laptop panel otherwise gets a clipped oversized window).
KIOSK_MODE_W=1920
KIOSK_MODE_H=1080
configure_display() {
command -v xrandr >/dev/null 2>&1 || return 0
local output mode internal width height
output=$(awk '/ connected/ && $1 !~ /^eDP|^LVDS/{print $1; exit}' /tmp/archipelago-kiosk-xrandr.txt)
[ -n "$output" ] || output=$(awk '/ connected/{print $1; exit}' /tmp/archipelago-kiosk-xrandr.txt)
[ -n "$output" ] || return 0
# Pick the EDID-preferred ("+") mode, falling back to the first-listed
# mode (EDID lists native first). Deliberately ignore "*" (currently
# active) — trusting "active" lets a bad clone/mirror state from a
# previous boot perpetuate itself forever instead of self-healing.
mode=$(awk -v out="$output" '
$1 == out { active = 1; next }
active && /^[[:space:]]+[0-9]+x[0-9]+/ {
if ($0 ~ /\+/ && !preferred) { preferred = $1 }
if (!first) first = $1
}
active && /^[^[:space:]]/ { active = 0 }
END { if (preferred) print preferred; else if (first) print first }
' /tmp/archipelago-kiosk-xrandr.txt)
[ -n "$mode" ] || mode=1920x1080
# Kiosk should use one native output. A spanning desktop makes Chromium land
# on the laptop panel or stretch across both outputs.
for internal in $(awk '/ connected/ && $1 ~ /^eDP|^LVDS/{print $1}' /tmp/archipelago-kiosk-xrandr.txt); do
[ "$internal" = "$output" ] || xrandr --output "$internal" --off 2>/dev/null || true
done
xrandr --output "$output" \
--primary \
--mode "$mode" \
--pos 0x0 \
--scale 1x1 \
--panning 0x0 \
--transform none 2>/dev/null || true
width=${mode%x*}
height=${mode#*x}
case "$width:$height" in *[!0-9:]*|:*) width=1920; height=1080 ;; esac
KIOSK_MODE_W=$width
KIOSK_MODE_H=$height
# Browser safe-area fallback for TVs that crop edges. Driver underscan is
# preferable, but many Intel HDMI outputs do not expose that property.
KIOSK_SAFE_AREA_X_PX=${KIOSK_SAFE_AREA_X_PX:-$((width * 3 / 100))}
KIOSK_SAFE_AREA_Y_PX=${KIOSK_SAFE_AREA_Y_PX:-$((height * 3 / 100))}
}
configure_display
xhost +SI:localuser:archipelago 2>/dev/null || true
xsetroot -solid black 2>/dev/null || true
xset s off 2>/dev/null || true
xset -dpms 2>/dev/null || true
xset s noblank 2>/dev/null || true
pkill -u archipelago -f 'chromium.*localhost' 2>/dev/null || true
sleep 1
# GPU vs headless (#36, choppy-audio incident 2026-06-28). --enable-gpu-rasterization
# spins a dedicated GPU process at 55-92% CPU even on real GPU hardware (Intel HD 5500)
# because under X11 it falls back to software compositing anyway — that CPU
# starvation is what caused choppy HDMI audio. --in-process-gpu avoids the
# separate process; GpuRasterization is also disabled via --disable-features below.
# On a GPU-less / headless server (no /dev/dri), disable GPU entirely instead.
if [ -e /dev/dri/card0 ] || [ -e /dev/dri/renderD128 ]; then
GPU_FLAGS="--in-process-gpu --num-raster-threads=1"
else
GPU_FLAGS="--disable-gpu --num-raster-threads=1"
fi
ARCHIPELAGO_UID=$(id -u archipelago)
while true; do
# XDG_RUNTIME_DIR must be passed explicitly — without it Chromium's audio
# backend can't find PipeWire-Pulse's socket at /run/user/<uid>/pulse/native,
# falls back to raw ALSA "default", fails to connect, and produces no audio
# at all with no visible error (--noerrdialogs suppresses it).
sudo -u archipelago env DISPLAY=:0 HOME=/home/archipelago XDG_RUNTIME_DIR=/run/user/$ARCHIPELAGO_UID chromium --kiosk \
--app=http://localhost/kiosk?safe_area_x=${KIOSK_SAFE_AREA_X_PX:-0}\&safe_area_y=${KIOSK_SAFE_AREA_Y_PX:-0} \
--noerrdialogs \
--disable-infobars \
--disable-translate \
--no-first-run \
--check-for-update-interval=31536000 \
--disable-features=TranslateUI,MetricsReporting,AutofillServerCommunication,PasswordManagerEnabled,GpuRasterization \
--disable-session-crashed-bubble \
--disable-save-password-bubble \
--disable-suggestions-service \
--disable-component-update \
$GPU_FLAGS \
--renderer-process-limit=2 \
--window-size=${KIOSK_MODE_W},${KIOSK_MODE_H} \
--window-position=0,0 \
--start-fullscreen \
--force-device-scale-factor=1 \
--disable-background-networking \
--disable-background-timer-throttling \
--disable-backgrounding-occluded-windows \
--disable-breakpad \
--disable-metrics \
--disable-metrics-reporting \
--disable-domain-reliability \
--js-flags="--max-old-space-size=256" \
--user-data-dir=/var/lib/archipelago/chromium-kiosk
sleep 3
done
kill "$XPID" 2>/dev/null || true
@@ -0,0 +1,14 @@
[Unit]
Description=Archipelago Kiosk Watchdog
After=archipelago.service
Wants=archipelago.service
[Service]
Type=simple
User=root
ExecStart=/usr/local/bin/archipelago-kiosk-watchdog
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,48 @@
[Unit]
Description=Archipelago Kiosk (X11 + Chromium)
After=archipelago.service systemd-user-sessions.service network-online.target
Wants=archipelago.service network-online.target
ConditionPathExists=/usr/local/bin/archipelago-kiosk-launcher
Conflicts=getty@tty1.service
[Service]
Type=simple
# Wait up to 5 min for archipelago to serve /health. On slow hardware
# first-boot is dominated by the FileBrowser pull (unbundled ISO),
# initial archipelago state sync, and frontend settle — .198 took
# longer than 120s and chromium launched against an empty backend,
# producing a white window that only recovered on reboot. 300s gives
# slow-but-functional hardware enough headroom; TimeoutStartSec is
# bumped in lockstep so systemd doesn't kill us mid-wait.
ExecStartPre=/bin/bash -c 'for i in $(seq 1 150); do curl -sf http://localhost/health >/dev/null 2>&1 && break; sleep 2; done'
# Also wait for the web-ui asset swap to finish. The first-boot rsync into
# /opt/archipelago/web-ui is non-atomic and writes the large bg-*.webp images
# last — a kiosk launched mid-swap rendered the UI with blank backgrounds
# (CSS background-image 404s are never refetched). A representative large
# asset answering 200 means the swap is effectively done. Bounded 60s so a
# renamed asset can never block kiosk startup.
ExecStartPre=/bin/bash -c 'for i in $(seq 1 30); do curl -sf -o /dev/null http://localhost/assets/img/bg-home.webp && break; sleep 2; done; exit 0'
ExecStart=/usr/local/bin/archipelago-kiosk-launcher
TimeoutStartSec=360
Restart=always
RestartSec=5
# Resource guardrail (#36). On GPU-less / headless hardware chromium could spin
# software compositing at ~92% of a core, saturating the node and starving the
# backend (it caused the .198 receive timeout + deploy storms). Cap CPU + memory
# so a runaway kiosk can never take the whole machine down; Delegate so the cap
# also binds the chromium/Xorg children in this unit's cgroup.
# CPUQuota=75% (0.75 cores) was too tight even for normal playback — the kiosk
# was throttled ~40% of the time, which is what caused choppy HDMI audio on
# archy-x250-exp (2026-06-28 incident). 200% (2 cores) gives enough headroom.
Delegate=yes
CPUQuota=200%
# Raised from 1500M/1200M: a Framework (Tiger Lake) kiosk sat at 806M used /
# 1.1G peak, riding the old MemoryHigh reclaim-throttle line — the throttling
# itself was the perceived UI lag. Keep Max well above real peaks; High stays
# the soft reclaim line so a runaway kiosk still can't take the machine down.
MemoryMax=2800M
MemoryHigh=2200M
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,10 @@
# Archipelago persistent log directory and files
# Runtime log destination. Backend runs as `archipelago`, but /var/log/
# is root-owned, so we pre-create the directory and log files with the
# right ownership at boot / install-time.
#
# Logrotate (image-recipe/configs/logrotate.conf) rotates files in this
# directory daily, keeping 30 compressed copies.
d /var/log/archipelago 0755 archipelago archipelago - -
f /var/log/archipelago/container-installs.log 0644 archipelago archipelago - -
@@ -0,0 +1,9 @@
[Unit]
Description=Watch for Archipelago Tor management actions
[Path]
PathExists=/var/lib/archipelago/tor-config/tor-action
MakeDirectory=yes
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,9 @@
[Unit]
Description=Process Archipelago Tor management action
After=tor.service
[Service]
Type=oneshot
ExecStart=/opt/archipelago/scripts/tor-helper.sh
# Runs as root — needs to write /etc/tor/torrc and restart tor.service
User=root
@@ -0,0 +1,19 @@
[Unit]
Description=Archipelago Self-Update
After=network-online.target archipelago.service
Wants=network-online.target
ConditionPathExists=/home/archipelago/archy/.git
[Service]
Type=oneshot
User=archipelago
ExecStart=/home/archipelago/archy/scripts/self-update.sh
TimeoutStartSec=600
Environment="HOME=/home/archipelago"
Environment="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/home/archipelago/.cargo/bin"
# Allow sudo for service restart and file install
# Requires archipelago user in sudoers for specific commands
StandardOutput=journal
StandardError=journal
@@ -0,0 +1,14 @@
[Unit]
Description=Check for Archipelago updates daily
ConditionPathExists=/home/archipelago/archy/.git
[Timer]
# Check at 3 AM daily (low-activity window)
OnCalendar=*-*-* 03:00:00
# Randomize within 30 min window to avoid thundering herd
RandomizedDelaySec=1800
# Run once on boot if last check was missed
Persistent=true
[Install]
WantedBy=timers.target
@@ -0,0 +1,14 @@
[Unit]
Description=Assign WireGuard server address to wg0
After=archipelago-wg.service
Wants=archipelago-wg.service
ConditionPathExists=/sys/class/net/wg0
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/bin/bash -c 'ip address show dev wg0 | grep -q "10.44.0.1" || ip address add 10.44.0.1/16 dev wg0'
ExecStart=/bin/bash -c 'iptables -t nat -C POSTROUTING -s 10.44.0.0/16 ! -o wg0 -j MASQUERADE 2>/dev/null || iptables -t nat -A POSTROUTING -s 10.44.0.0/16 ! -o wg0 -j MASQUERADE'
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,13 @@
[Unit]
Description=Archipelago Standalone WireGuard (wg0)
After=network.target
ConditionPathExists=/var/lib/archipelago/wireguard/private.key
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/bin/archipelago-wg setup /var/lib/archipelago/wireguard/private.key
ExecStop=/bin/bash -c 'ip link del wg0 2>/dev/null || true'
[Install]
WantedBy=multi-user.target
+77
View File
@@ -0,0 +1,77 @@
[Unit]
Description=Archipelago Backend
After=network-online.target archipelago-setup-tor.service
Wants=network-online.target
# The data dir AND podman's graphroot (containers/storage) both live on the
# separate /var/lib/archipelago volume. Without this, on a cold boot the service
# (and its ExecStartPre) can start BEFORE var-lib-archipelago.mount, write to the
# bare mountpoint on rootfs, fail every podman call, exit, and get restarted every
# 5s until the volume mounts (~5 min of "[FAILED] Failed to start" on boot — B17).
# RequiresMountsFor adds both Requires= and After= on the mount unit so we never
# start until the data volume is mounted.
RequiresMountsFor=/var/lib/archipelago
[Service]
Type=notify
User=archipelago
Environment="ARCHIPELAGO_BIND=127.0.0.1:5678"
Environment="ARCHIPELAGO_USE_QUADLET_BACKENDS=true"
EnvironmentFile=-/var/lib/archipelago/telemetry.env
# DEV_MODE disabled in production — enabled via override.conf on dev servers
Environment="XDG_RUNTIME_DIR=/run/user/1000"
# + prefix runs these as root (needed for chown/mkdir outside ReadWritePaths)
ExecStartPre=+/bin/bash -c 'mkdir -p /run/user/1000 /var/lib/containers && chown archipelago:archipelago /run/user/1000 && chmod 700 /run/user/1000'
# Host IP from the main-table default route — hostname -I token order breaks
# once a VPN/bridge interface exists (netbird's wg tunnel sorted first and
# poisoned every host_ip consumer). Falls back to hostname -I when routeless.
ExecStartPre=+/bin/bash -c 'mkdir -p /var/lib/archipelago && chown archipelago:archipelago /var/lib/archipelago && IP=$(ip -4 route show default 2>/dev/null | sed -n "s/.* src \([0-9.]*\).*/\1/p" | head -1); [ -n "$$IP" ] || IP=$(hostname -I 2>/dev/null | awk "{print $$1}"); echo "ARCHIPELAGO_HOST_IP=$$IP" > /var/lib/archipelago/host-ip.env && chown archipelago:archipelago /var/lib/archipelago/host-ip.env'
ExecStart=/usr/local/bin/archipelago
Restart=on-failure
RestartSec=5
WatchdogSec=300
TimeoutStartSec=300
# Backend shuts down in <1s; 15s is generous for any cleanup
TimeoutStopSec=15
# Filesystem protection
ProtectSystem=strict
# ProtectHome=no: rootless podman needs writable ~/.local/share/containers
ProtectHome=no
# PrivateTmp disabled: rootless podman runtime lives in /tmp/podman-run-UID/
# and must be shared between the service and SSH-created containers
ReadWritePaths=/var/lib/archipelago /etc/containers /var/lib/containers /run/user /tmp /home/archipelago/.local/share/containers /home/archipelago/.config/containers /etc
# Privilege restriction — NoNewPrivileges=no required for sudo archipelago-wg
# (WireGuard peer management). Scoped via sudoers to only archipelago-wg.
NoNewPrivileges=no
PrivateDevices=no
SupplementaryGroups=dialout debian-tor fips
# Syscall and network restrictions — safe on Debian 13 (systemd 256+)
# which respects NoNewPrivileges=no as an explicit override for seccomp filters
SystemCallArchitectures=native
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
RestrictRealtime=yes
# MemoryDenyWriteExecute removed: ring (rustls) and secp256k1 (bitcoin/nostr)
# use assembly code that requires executable memory mappings on some platforms
# Resource limits
MemoryMax=4G
LimitNOFILE=65535
TasksMax=2048
# Delegate cgroup controllers so rootless podman (run from this system service
# as user=archipelago, not user@1000.service) can create transient libpod-*.scope
# units with --memory / --cpus / --pids-limit. Without this, podman create fails
# at start time with: "MemoryMax is out of range" because systemd rejects resource
# limits on undelegated cgroup subtrees. Required for the ProdContainerOrchestrator
# code path (see core/archipelago/src/container/prod_orchestrator.rs).
Delegate=memory pids cpu io
# Logging
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
+7
View File
@@ -0,0 +1,7 @@
pcm.!default {
type pulse
hint.description "Default ALSA Device (via PulseAudio)"
}
ctl.!default {
type pulse
}
@@ -0,0 +1,7 @@
[Journal]
Storage=persistent
SystemMaxUse=500M
RuntimeMaxUse=100M
ForwardToSyslog=no
RateLimitIntervalSec=30s
RateLimitBurst=10000
+24
View File
@@ -0,0 +1,24 @@
# Log rotation configuration for Archipelago
/var/log/archipelago/*.log {
daily
rotate 30
compress
delaycompress
missingok
notifempty
create 0644 root root
sharedscripts
postrotate
/usr/bin/systemctl reload archipelago > /dev/null 2>&1 || true
endscript
}
/var/lib/archipelago/logs/*.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
create 0644 archipelago archipelago
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,21 @@
# Gitea iframe proxy — strips X-Frame-Options so Gitea works in Archipelago iframe.
# Gitea container binds to port 3001, this proxy listens on port 3000 (the public port).
# Deployed to /etc/nginx/conf.d/gitea-iframe.conf
server {
listen 3000;
server_name _;
client_max_body_size 1G;
location / {
proxy_pass http://127.0.0.1:3001;
proxy_set_header Host $http_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_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_hide_header X-Frame-Options;
proxy_hide_header Content-Security-Policy;
}
}
@@ -0,0 +1,19 @@
[info]
relay_url = "ws://0.0.0.0:7777/"
name = "Archipelago Private Relay"
description = "Private Nostr relay for Archipelago mesh VPN peer discovery"
[database]
data_directory = "/var/lib/archipelago/nostr-relay"
in_memory = true
[network]
address = "0.0.0.0"
port = 7777
ping_interval = 120
[limits]
messages_per_sec = 50
max_event_bytes = 65536
max_ws_message_bytes = 65536
max_ws_frame_bytes = 65536
+24
View File
@@ -0,0 +1,24 @@
[Unit]
Description=Archipelago Private Nostr Relay
After=network-online.target
Wants=network-online.target
Before=nostr-vpn.service
[Service]
Type=simple
User=archipelago
ExecStartPre=/bin/bash -c 'mkdir -p /var/lib/archipelago/nostr-relay'
ExecStart=/usr/local/bin/nostr-rs-relay --config /var/lib/archipelago/nostr-relay/config.toml
Restart=always
RestartSec=3
TimeoutStopSec=10
# Resource limits — relay is lightweight (in-memory mode)
MemoryMax=512M
LimitNOFILE=4096
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
+32
View File
@@ -0,0 +1,32 @@
[Unit]
Description=Nostr VPN - Mesh VPN with Nostr identity
After=network-online.target tor.service archipelago.service
Wants=network-online.target
StartLimitIntervalSec=300
StartLimitBurst=10
[Service]
Type=simple
User=root
Environment=HOME=/var/lib/archipelago/nostr-vpn
EnvironmentFile=-/var/lib/archipelago/nostr-vpn/env
ExecStartPre=+/bin/bash -c 'mkdir -p /run/nostr-vpn /var/lib/archipelago/nostr-vpn/.config/nvpn'
ExecStartPre=/bin/bash -c 'test -f /var/lib/archipelago/nostr-vpn/env || { echo "NostrVPN not configured — waiting for onboarding"; exit 1; }'
ExecStart=/usr/local/bin/nvpn daemon
Restart=on-failure
RestartSec=30
TimeoutStartSec=30
TimeoutStopSec=10
# No sandbox — runs as root for TUN/WireGuard, needs unrestricted filesystem
# Resource limits
MemoryMax=256M
TasksMax=64
# Logging
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,366 @@
# App proxies for HTTPS - avoids mixed content when embedding apps from HTTPS page
# Complete list for all apps that may be launched from the UI
location /app/grafana/ {
proxy_pass http://127.0.0.1:3000/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_hide_header X-Frame-Options;
add_header X-Frame-Options "SAMEORIGIN" always;
proxy_hide_header Content-Security-Policy;
proxy_set_header Accept-Encoding "";
sub_filter_once on;
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
}
location = /app/uptime-kuma/ {
return 302 /app/uptime-kuma/dashboard;
}
location /app/uptime-kuma/ {
proxy_pass http://127.0.0.1:3002/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Prefix /app/uptime-kuma;
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_redirect / /app/uptime-kuma/;
proxy_hide_header X-Frame-Options;
add_header X-Frame-Options "SAMEORIGIN" always;
proxy_hide_header Content-Security-Policy;
proxy_set_header Accept-Encoding "";
sub_filter_once on;
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
}
location /app/gitea/ {
proxy_pass http://127.0.0.1:3001/;
proxy_http_version 1.1;
proxy_set_header Host $http_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_hide_header X-Frame-Options;
proxy_hide_header Content-Security-Policy;
}
location /app/searxng/ {
proxy_pass http://127.0.0.1:8888/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_hide_header X-Frame-Options;
add_header X-Frame-Options "SAMEORIGIN" always;
proxy_hide_header Content-Security-Policy;
proxy_set_header Accept-Encoding "";
sub_filter_once on;
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
}
location /app/portainer/ {
proxy_pass http://127.0.0.1:9000/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_hide_header X-Frame-Options;
add_header X-Frame-Options "SAMEORIGIN" always;
proxy_hide_header Content-Security-Policy;
proxy_set_header Accept-Encoding "";
sub_filter_once on;
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
}
location /app/filebrowser/ {
client_max_body_size 10G;
proxy_pass http://127.0.0.1:8083/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_hide_header X-Frame-Options;
add_header X-Frame-Options "SAMEORIGIN" always;
proxy_hide_header Content-Security-Policy;
proxy_request_buffering off;
proxy_set_header Accept-Encoding "";
sub_filter_once on;
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
}
location /app/endurain/ {
proxy_pass http://127.0.0.1:8080/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_hide_header X-Frame-Options;
add_header X-Frame-Options "SAMEORIGIN" always;
proxy_hide_header Content-Security-Policy;
proxy_set_header Accept-Encoding "";
sub_filter_once on;
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
}
location /app/lnd/ {
proxy_pass http://127.0.0.1:18083/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_hide_header X-Frame-Options;
add_header X-Frame-Options "SAMEORIGIN" always;
proxy_hide_header Content-Security-Policy;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
proxy_set_header Accept-Encoding "";
sub_filter_once on;
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
}
location /app/jellyfin/ {
proxy_pass http://127.0.0.1:8096/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_hide_header X-Frame-Options;
add_header X-Frame-Options "SAMEORIGIN" always;
proxy_hide_header Content-Security-Policy;
proxy_set_header Accept-Encoding "";
sub_filter_once on;
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
}
location /app/photoprism/ {
proxy_pass http://127.0.0.1:2342/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_hide_header X-Frame-Options;
add_header X-Frame-Options "SAMEORIGIN" always;
proxy_hide_header Content-Security-Policy;
proxy_set_header Accept-Encoding "";
sub_filter_once on;
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
}
location /app/mempool/ {
proxy_pass http://127.0.0.1:4080/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_hide_header X-Frame-Options;
add_header X-Frame-Options "SAMEORIGIN" always;
proxy_hide_header Content-Security-Policy;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
proxy_set_header Accept-Encoding "";
sub_filter_once on;
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
}
location /app/fedimint/ {
proxy_pass http://127.0.0.1:8175/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_hide_header X-Frame-Options;
add_header X-Frame-Options "SAMEORIGIN" always;
proxy_hide_header Content-Security-Policy;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
proxy_set_header Accept-Encoding "";
sub_filter_types text/css application/javascript application/json;
sub_filter_once off;
sub_filter 'href="/' 'href="/app/fedimint/';
sub_filter 'src="/' 'src="/app/fedimint/';
sub_filter "href='/" "href='/app/fedimint/";
sub_filter "src='/" "src='/app/fedimint/";
sub_filter 'url("/' 'url("/app/fedimint/';
sub_filter "url('/" "url('/app/fedimint/";
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
}
location /app/fedimint-gateway/ {
proxy_pass http://127.0.0.1:8176/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_hide_header X-Frame-Options;
add_header X-Frame-Options "SAMEORIGIN" always;
proxy_hide_header Content-Security-Policy;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
proxy_set_header Accept-Encoding "";
sub_filter_once on;
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
}
location /app/tailscale/ {
# Tailscale has no web UI — managed via CLI/Tailscale app
default_type application/json;
return 503 '{"error":{"code":"NO_WEB_UI","message":"Tailscale is managed via CLI"}}';
}
location /app/routstr/ {
proxy_pass http://127.0.0.1:8200/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_hide_header X-Frame-Options;
add_header X-Frame-Options "SAMEORIGIN" always;
proxy_hide_header Content-Security-Policy;
proxy_set_header Accept-Encoding "";
sub_filter_once on;
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
}
location /app/nostr-vpn/ {
proxy_pass http://127.0.0.1:8201/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_hide_header X-Frame-Options;
add_header X-Frame-Options "SAMEORIGIN" always;
proxy_hide_header Content-Security-Policy;
}
location /app/fips/ {
proxy_pass http://127.0.0.1:8202/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_hide_header X-Frame-Options;
add_header X-Frame-Options "SAMEORIGIN" always;
proxy_hide_header Content-Security-Policy;
}
location /app/ollama/ {
proxy_pass http://127.0.0.1:11434/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_hide_header X-Frame-Options;
add_header X-Frame-Options "SAMEORIGIN" always;
proxy_hide_header Content-Security-Policy;
proxy_set_header Accept-Encoding "";
sub_filter_once on;
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
}
location /app/bitcoin-ui/ {
proxy_pass http://127.0.0.1:8334/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_hide_header X-Frame-Options;
add_header X-Frame-Options "SAMEORIGIN" always;
proxy_hide_header Content-Security-Policy;
proxy_set_header Accept-Encoding "";
sub_filter_once on;
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
}
location /app/botfights/api/ {
proxy_pass http://127.0.0.1:9100/api/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
location /app/botfights/ {
proxy_pass http://127.0.0.1:9100/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_hide_header X-Frame-Options;
add_header X-Frame-Options "SAMEORIGIN" always;
proxy_hide_header Content-Security-Policy;
proxy_hide_header Cross-Origin-Embedder-Policy;
proxy_hide_header Cross-Origin-Opener-Policy;
proxy_hide_header Cross-Origin-Resource-Policy;
proxy_set_header Accept-Encoding "";
sub_filter_types text/css application/javascript application/json;
sub_filter_once off;
sub_filter 'href="/' 'href="/app/botfights/';
sub_filter 'src="/' 'src="/app/botfights/';
sub_filter "href='/" "href='/app/botfights/";
sub_filter "src='/" "src='/app/botfights/";
sub_filter '</head>' '<script src="/nostr-provider.js"></script><script>window.addEventListener("message",function(e){var d=e.data;if(d&&d.type==="arcade-input"&&d.key){var t=d.action==="up"?"keyup":"keydown";document.dispatchEvent(new KeyboardEvent(t,{key:d.key,bubbles:true}))}})</script></head>';
}
location /app/electrumx/ {
proxy_pass http://127.0.0.1:50002/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_hide_header X-Frame-Options;
add_header X-Frame-Options "SAMEORIGIN" always;
proxy_hide_header Content-Security-Policy;
proxy_set_header Accept-Encoding "";
sub_filter_once on;
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
}
location /app/indeedhub/_next/ {
proxy_pass http://127.0.0.1:7777/_next/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_cache_valid 200 30d;
add_header Cache-Control "public, max-age=2592000, immutable";
}
location /app/indeedhub/ws/ {
proxy_pass http://127.0.0.1:7777/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_read_timeout 86400s;
}
location /app/indeedhub/ {
proxy_pass http://127.0.0.1:7777/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_hide_header X-Frame-Options;
add_header X-Frame-Options "SAMEORIGIN" always;
proxy_hide_header Content-Security-Policy;
proxy_set_header Accept-Encoding "";
sub_filter_types text/css application/javascript application/json;
sub_filter_once off;
sub_filter 'href="/' 'href="/app/indeedhub/';
sub_filter 'src="/' 'src="/app/indeedhub/';
sub_filter "href='/" "href='/app/indeedhub/";
sub_filter "src='/" "src='/app/indeedhub/";
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
}
location /app/nginx-proxy-manager/ {
proxy_pass http://127.0.0.1:8081/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_hide_header X-Frame-Options;
add_header X-Frame-Options "SAMEORIGIN" always;
proxy_hide_header Content-Security-Policy;
proxy_set_header Accept-Encoding "";
sub_filter_once on;
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
}
@@ -0,0 +1,16 @@
# PWA installability - required for Install (not just Add to Home Screen) on Android
# Manifest MUST be served with application/manifest+json - Chrome rejects otherwise
location = /manifest.webmanifest {
default_type application/manifest+json;
add_header Cache-Control "public, max-age=0, must-revalidate";
}
# Service worker - no cache so updates apply
location ~ ^/(sw\.js|workbox-.*\.js|registerSW\.js)$ {
add_header Content-Type application/javascript;
add_header Service-Worker-Allowed /;
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
# index.html - avoid aggressive cache for PWA updates
location = /index.html {
add_header Cache-Control "public, max-age=0, must-revalidate";
}
+95
View File
@@ -0,0 +1,95 @@
#!/bin/bash
#
# Create bootable FAT32 USB from Archipelago ISO
# This extracts the ISO contents to FAT32 for UEFI boot
#
# Usage: ./create-fat32-usb.sh /dev/diskN
#
set -e
# Get absolute path of script directory
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
if [ -z "$1" ]; then
echo "Usage: $0 /dev/diskN"
echo ""
echo "Available disks:"
diskutil list external
exit 1
fi
USB_DISK="$1"
ISO_FILE="${ARCHIPELAGO_ISO:-}"
if [ -z "$ISO_FILE" ]; then
ISO_FILE="$SCRIPT_DIR/results/archipelago-installer-x86_64.iso"
[ -f "$ISO_FILE" ] || ISO_FILE="$SCRIPT_DIR/results/archipelago-installer-unbundled-x86_64.iso"
fi
WORK_DIR="$SCRIPT_DIR/build/usb-extract"
if [ ! -f "$ISO_FILE" ]; then
echo "❌ ISO not found: $ISO_FILE"
echo ""
echo "Build the ISO first with: ./build-debian-iso.sh"
echo "Or set ARCHIPELAGO_ISO=/path/to/archipelago-installer-x86_64.iso"
exit 1
fi
echo "╔════════════════════════════════════════════════════════╗"
echo "║ Create FAT32 Bootable USB ║"
echo "╚════════════════════════════════════════════════════════╝"
echo ""
echo "⚠️ WARNING: This will COMPLETELY ERASE $USB_DISK"
echo ""
echo "📀 ISO: $(basename "$ISO_FILE")"
echo "💾 USB: $USB_DISK"
echo ""
echo "Press Ctrl+C to cancel, or Enter to continue..."
read
# Clean up work directory
rm -rf "$WORK_DIR"
mkdir -p "$WORK_DIR"
echo "🔓 Unmounting USB..."
diskutil unmountDisk "$USB_DISK" || true
echo ""
echo "🗂️ Formatting USB as FAT32 with MBR..."
diskutil eraseDisk FAT32 ARCHIPELAGO MBR "$USB_DISK"
echo ""
echo "📦 Extracting ISO contents..."
cd "$WORK_DIR"
7z x -y "$ISO_FILE"
echo ""
echo "📋 Copying files to USB (this may take a few minutes)..."
USB_MOUNT="/Volumes/ARCHIPELAGO"
if [ ! -d "$USB_MOUNT" ]; then
echo "❌ USB not mounted at $USB_MOUNT"
exit 1
fi
# Copy all extracted files
cp -Rv "$WORK_DIR"/* "$USB_MOUNT/" 2>/dev/null | tail -5
echo " (showing last 5 files copied)"
echo ""
echo "🔄 Syncing..."
sync
echo ""
echo "✅ USB created successfully!"
echo ""
echo "Now:"
echo " 1. Eject: diskutil eject $USB_DISK"
echo " 2. Insert into target machine"
echo " 3. Boot from USB (F12 or similar for boot menu)"
echo ""
echo "Default login (live mode):"
echo " Username: user"
echo " Password: live"
# Cleanup
rm -rf "$WORK_DIR"
+207
View File
@@ -0,0 +1,207 @@
#!/bin/bash
#
# Boot branding dev — iterate on GRUB theme, Plymouth, and installer visuals
# without rebuilding the ISO. Patches an existing ISO and boots in QEMU.
#
# Usage:
# ./dev-branding.sh [path-to-iso]
#
# If no ISO is found locally, downloads the latest from the build server.
# Edit files in branding/, re-run, see changes in ~10 seconds.
#
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
WORK="/tmp/archipelago-dev-branding"
PATCHED="$SCRIPT_DIR/results/archipelago-dev-patched.iso"
CACHED_ISO="$SCRIPT_DIR/results/archipelago-dev-base.iso"
DEV_SERVER="archipelago@192.168.1.228"
SSH_KEY="$HOME/.ssh/archipelago-deploy"
echo ""
echo " Archipelago Boot Branding Dev"
echo ""
# --- Find or download an ISO ---
ISO="${1:-}"
# Search locally
if [ -z "$ISO" ] || [ ! -f "$ISO" ]; then
for pattern in \
"$HOME/Desktop/archipelago-dev-"*.iso \
"$HOME/Desktop/archipelago-unbundled-"*.iso \
"$HOME/Desktop/archipelago-"*.iso \
"$SCRIPT_DIR/results/archipelago-dev-base.iso" \
"$SCRIPT_DIR/results/archipelago-"*.iso; do
found=$(ls -t $pattern 2>/dev/null | head -1)
if [ -n "$found" ] && [ -f "$found" ]; then
ISO="$found"
break
fi
done
fi
# Download from server if not found
if [ -z "$ISO" ] || [ ! -f "$ISO" ]; then
echo " No ISO found locally. Downloading latest from build server..."
REMOTE_ISO=$(ssh -i "$SSH_KEY" "$DEV_SERVER" \
"ls -t /var/lib/archipelago/filebrowser/Builds/archipelago-dev-*.iso 2>/dev/null | head -1" 2>/dev/null)
if [ -z "$REMOTE_ISO" ]; then
REMOTE_ISO=$(ssh -i "$SSH_KEY" "$DEV_SERVER" \
"ls -t /var/lib/archipelago/filebrowser/Builds/archipelago-unbundled-*.iso 2>/dev/null | head -1" 2>/dev/null)
fi
if [ -n "$REMOTE_ISO" ]; then
mkdir -p "$SCRIPT_DIR/results"
echo " Downloading: $(basename "$REMOTE_ISO")..."
scp -i "$SSH_KEY" "$DEV_SERVER:$REMOTE_ISO" "$CACHED_ISO"
ISO="$CACHED_ISO"
echo " Saved to: $ISO"
else
echo " No ISO on server either. Run a CI build first."
echo " Or place an ISO on your Desktop."
exit 1
fi
fi
echo " Base ISO: $(basename "$ISO") ($(du -h "$ISO" | cut -f1))"
echo ""
# --- Extract ISO ---
echo " [1/3] Extracting ISO..."
if [ -d "$WORK" ]; then
chmod -R u+w "$WORK" 2>/dev/null || true
fi
rm -rf "$WORK"
mkdir -p "$WORK"
xorriso -osirrox on -indev "$ISO" -extract / "$WORK" 2>/dev/null || {
echo " xorriso extraction failed, trying hdiutil..."
MNT=$(mktemp -d)
hdiutil attach "$ISO" -mountpoint "$MNT" -readonly -nobrowse 2>/dev/null || {
echo " Could not mount ISO. Is it corrupt?"
exit 1
}
cp -a "$MNT"/* "$WORK/" 2>/dev/null || true
hdiutil detach "$MNT" 2>/dev/null || true
rmdir "$MNT" 2>/dev/null || true
}
# Ensure files are writable after extraction
chmod -R u+w "$WORK" 2>/dev/null || true
# --- Patch branding ---
echo " [2/3] Patching branding..."
THEME_DST="$WORK/boot/grub/themes/archipelago"
mkdir -p "$THEME_DST"
# GRUB theme.txt
if [ -f "$SCRIPT_DIR/branding/grub-theme/theme.txt" ]; then
cp "$SCRIPT_DIR/branding/grub-theme/theme.txt" "$THEME_DST/"
echo " theme.txt"
fi
# GRUB background — use static file from branding dir
if [ -f "$SCRIPT_DIR/branding/grub-theme/background.png" ]; then
cp "$SCRIPT_DIR/branding/grub-theme/background.png" "$THEME_DST/background.png"
echo " background.png (static)"
elif [ -f "$SCRIPT_DIR/branding/generate-grub-background.py" ]; then
python3 "$SCRIPT_DIR/branding/generate-grub-background.py" "$THEME_DST/background.png" 2>/dev/null
echo " background.png (generated)"
fi
# Plymouth theme
PLYMOUTH_DST="$WORK/archipelago/plymouth-theme"
mkdir -p "$PLYMOUTH_DST"
if [ -d "$SCRIPT_DIR/branding/plymouth-theme" ]; then
cp "$SCRIPT_DIR/branding/plymouth-theme/"* "$PLYMOUTH_DST/" 2>/dev/null || true
echo " plymouth theme"
fi
# --- Repackage ISO ---
echo " [3/3] Repackaging ISO..."
mkdir -p "$SCRIPT_DIR/results"
# Find isohdpfx.bin — project copy first, then system
ISOHDPFX=""
for p in "$SCRIPT_DIR/branding/isohdpfx.bin" \
"$WORK/isolinux/isohdpfx.bin" \
/usr/lib/ISOLINUX/isohdpfx.bin \
/usr/share/syslinux/isohdpfx.bin \
/opt/homebrew/share/syslinux/isohdpfx.bin; do
[ -f "$p" ] && ISOHDPFX="$p" && break
done
if [ -z "$ISOHDPFX" ]; then
echo " ERROR: No isohdpfx.bin found. Cannot create bootable ISO."
echo " Preview only — open the background:"
open "$THEME_DST/background.png" 2>/dev/null || true
exit 1
fi
EFI_IMG="$WORK/boot/grub/efi.img"
if [ -f "$EFI_IMG" ]; then
xorriso -as mkisofs -o "$PATCHED" \
-volid "ARCHIPELAGO" \
-iso-level 3 -J -joliet-long -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 \
-partition_offset 16 \
"$WORK" 2>/dev/null
else
xorriso -as mkisofs -o "$PATCHED" \
-volid "ARCHIPELAGO" \
-iso-level 3 -J -joliet-long -R \
-isohybrid-mbr "$ISOHDPFX" \
-c isolinux/boot.cat \
-b isolinux/isolinux.bin \
-no-emul-boot -boot-load-size 4 -boot-info-table \
-partition_offset 16 \
"$WORK" 2>/dev/null
fi
echo ""
echo " Patched: $PATCHED ($(du -h "$PATCHED" | cut -f1))"
echo ""
# --- Boot in QEMU ---
if ! command -v qemu-system-x86_64 >/dev/null 2>&1; then
echo " QEMU not found. Install: brew install qemu"
echo " Opening background preview instead..."
open "$THEME_DST/background.png" 2>/dev/null || true
exit 0
fi
echo " Booting in QEMU (BIOS mode — shows ISOLINUX menu)..."
echo " Press Ctrl+C to stop."
echo ""
# Create test disk (use separate disk from other QEMU instances)
DISK="/tmp/archipelago-branding-test.qcow2"
# Kill any leftover QEMU from previous branding test
pkill -f "archipelago-branding-test" 2>/dev/null || true
sleep 1
if [ ! -f "$DISK" ]; then
qemu-img create -f qcow2 "$DISK" 20G 2>/dev/null
fi
# Boot with BIOS to see the ISOLINUX/GRUB menu
qemu-system-x86_64 \
-machine pc \
-m 4G \
-smp 2 \
-boot d \
-cdrom "$PATCHED" \
-drive if=virtio,format=qcow2,file="$DISK" \
-net nic,model=virtio -net user,hostfwd=tcp::2222-:22,hostfwd=tcp::8100-:80 \
-vga virtio \
-display default \
-serial file:/tmp/archipelago-qemu-serial.log
echo ""
echo " QEMU stopped. Serial log: /tmp/archipelago-qemu-serial.log"
echo " Re-run to test again after editing branding files."
+47
View File
@@ -0,0 +1,47 @@
#!/bin/bash
# Quick fix to enable Archipelago auto-start on existing booted system
# Run this on the Dell OptiPlex after boot
set -e
echo "🔧 Fixing Archipelago auto-start..."
# Create the auto-start script in .bashrc
cat >> ~/.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
clear
echo ""
echo " ╔═══════════════════════════════════════════════════════════╗"
echo " ║ 🏝️ ARCHIPELAGO BITCOIN NODE OS ║"
echo " ╚═══════════════════════════════════════════════════════════╝"
echo ""
# Get IP
IP=$(hostname -I 2>/dev/null | awk '{print $1}')
[ -n "$IP" ] && echo " 🌐 Web UI: http://$IP:5678"
echo ""
# Run the setup
bash "$BOOT_MEDIA/archipelago/setup-archipelago.sh"
fi
fi
EOF
echo "✅ Auto-start added to .bashrc"
echo ""
echo "Now logout and login again, or run:"
echo " source ~/.bashrc"
+40
View File
@@ -0,0 +1,40 @@
#!/bin/bash
# Integrate Archipelago backend and frontend into custom ISO
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
echo "🔗 Integrating Archipelago Components into ISO"
echo ""
# Check if backend exists
BACKEND_BIN="$SCRIPT_DIR/build/backend/archipelago"
if [ ! -f "$BACKEND_BIN" ]; then
echo "❌ Backend binary not found at $BACKEND_BIN"
echo " Run: ./build-backend.sh first"
exit 1
fi
# Check if frontend exists
FRONTEND_DIR="$SCRIPT_DIR/build/frontend"
if [ ! -d "$FRONTEND_DIR" ] || [ ! -f "$FRONTEND_DIR/index.html" ]; then
echo "❌ Frontend build not found at $FRONTEND_DIR"
echo " Run: ./build-frontend.sh first"
exit 1
fi
echo "✅ Found backend binary: $(du -h "$BACKEND_BIN" | cut -f1)"
echo "✅ Found frontend files"
echo ""
# Now rebuild the ISO with the integrated components
export INCLUDE_BACKEND="$BACKEND_BIN"
export INCLUDE_FRONTEND="$FRONTEND_DIR"
echo "🔨 Rebuilding ISO with Archipelago components..."
./build-custom-iso.sh
echo ""
echo "✅ Integration complete!"
echo ""
+2
View File
@@ -0,0 +1,2 @@
/dev/mmcblk0p1 /boot vfat umask=0077 0 2
/dev/mmcblk0p2 / ext4 defaults 0 1
@@ -0,0 +1,129 @@
#!/bin/bash
get_variables () {
ROOT_PART_DEV=$(findmnt / -o source -n)
ROOT_PART_NAME=$(echo "$ROOT_PART_DEV" | cut -d "/" -f 3)
ROOT_DEV_NAME=$(echo /sys/block/*/"${ROOT_PART_NAME}" | cut -d "/" -f 4)
ROOT_DEV="/dev/${ROOT_DEV_NAME}"
ROOT_PART_NUM=$(cat "/sys/block/${ROOT_DEV_NAME}/${ROOT_PART_NAME}/partition")
BOOT_PART_DEV=$(findmnt /boot -o source -n)
BOOT_PART_NAME=$(echo "$BOOT_PART_DEV" | cut -d "/" -f 3)
BOOT_DEV_NAME=$(echo /sys/block/*/"${BOOT_PART_NAME}" | cut -d "/" -f 4)
BOOT_PART_NUM=$(cat "/sys/block/${BOOT_DEV_NAME}/${BOOT_PART_NAME}/partition")
OLD_DISKID=$(fdisk -l "$ROOT_DEV" | sed -n 's/Disk identifier: 0x\([^ ]*\)/\1/p')
ROOT_DEV_SIZE=$(cat "/sys/block/${ROOT_DEV_NAME}/size")
if [ "$ROOT_DEV_SIZE" -le 67108864 ]; then
TARGET_END=$((ROOT_DEV_SIZE - 1))
else
TARGET_END=$((33554432 - 1))
DATA_PART_START=33554432
DATA_PART_END=$((ROOT_DEV_SIZE - 1))
fi
PARTITION_TABLE=$(parted -m "$ROOT_DEV" unit s print | tr -d 's')
LAST_PART_NUM=$(echo "$PARTITION_TABLE" | tail -n 1 | cut -d ":" -f 1)
ROOT_PART_LINE=$(echo "$PARTITION_TABLE" | grep -e "^${ROOT_PART_NUM}:")
ROOT_PART_START=$(echo "$ROOT_PART_LINE" | cut -d ":" -f 2)
ROOT_PART_END=$(echo "$ROOT_PART_LINE" | cut -d ":" -f 3)
}
check_variables () {
if [ "$BOOT_DEV_NAME" != "$ROOT_DEV_NAME" ]; then
FAIL_REASON="Boot and root partitions are on different devices"
return 1
fi
if [ "$ROOT_PART_NUM" -ne "$LAST_PART_NUM" ]; then
FAIL_REASON="Root partition should be last partition"
return 1
fi
if [ "$ROOT_PART_END" -gt "$TARGET_END" ]; then
FAIL_REASON="Root partition runs past the end of device"
return 1
fi
if [ ! -b "$ROOT_DEV" ] || [ ! -b "$ROOT_PART_DEV" ] || [ ! -b "$BOOT_PART_DEV" ] ; then
FAIL_REASON="Could not determine partitions"
return 1
fi
}
main () {
get_variables
if ! check_variables; then
return 1
fi
# if [ "$ROOT_PART_END" -eq "$TARGET_END" ]; then
# reboot_pi
# fi
if ! echo Yes | parted -m --align=optimal "$ROOT_DEV" ---pretend-input-tty u s resizepart "$ROOT_PART_NUM" "$TARGET_END" ; then
FAIL_REASON="Root partition resize failed"
return 1
fi
if [ -n "$DATA_PART_START" ]; then
if ! parted -ms --align=optimal "$ROOT_DEV" u s mkpart primary "$DATA_PART_START" "$DATA_PART_END"; then
FAIL_REASON="Data partition creation failed"
return 1
fi
fi
(
echo x
echo i
echo "0xcb15ae4d"
echo r
echo w
) | fdisk $ROOT_DEV
mount / -o remount,rw
resize2fs $ROOT_PART_DEV
if ! systemd-machine-id-setup; then
FAIL_REASON="systemd-machine-id-setup failed"
return 1
fi
if ! ssh-keygen -A; then
FAIL_REASON="ssh host key generation failed"
return 1
fi
echo start > /etc/hostname
return 0
}
mount -t proc proc /proc
mount -t sysfs sys /sys
mount -t tmpfs tmp /run
mkdir -p /run/systemd
mount /boot
mount / -o remount,ro
beep
if main; then
sed -i 's| init=/usr/lib/startos/scripts/init_resize\.sh| boot=embassy|' /boot/cmdline.txt
echo "Resized root filesystem. Rebooting in 5 seconds..."
sleep 5
else
echo -e "Could not expand filesystem.\n${FAIL_REASON}"
sleep 5
fi
sync
umount /boot
reboot -f
@@ -0,0 +1 @@
usb-storage.quirks=152d:0562:u,14cd:121c:u,0781:cfcb:u console=serial0,115200 console=tty1 root=PARTUUID=cb15ae4d-02 rootfstype=ext4 fsck.repair=yes rootwait cgroup_enable=cpuset cgroup_memory=1 cgroup_enable=memory quiet boot=embassy
@@ -0,0 +1,86 @@
# For more options and information see
# http://rpf.io/configtxt
# Some settings may impact device functionality. See link above for details
# uncomment if you get no picture on HDMI for a default "safe" mode
#hdmi_safe=1
# uncomment the following to adjust overscan. Use positive numbers if console
# goes off screen, and negative if there is too much border
#overscan_left=16
#overscan_right=16
#overscan_top=16
#overscan_bottom=16
# uncomment to force a console size. By default it will be display's size minus
# overscan.
#framebuffer_width=1280
#framebuffer_height=720
# uncomment if hdmi display is not detected and composite is being output
#hdmi_force_hotplug=1
# uncomment to force a specific HDMI mode (this will force VGA)
#hdmi_group=1
#hdmi_mode=1
# uncomment to force a HDMI mode rather than DVI. This can make audio work in
# DMT (computer monitor) modes
#hdmi_drive=2
# uncomment to increase signal to HDMI, if you have interference, blanking, or
# no display
#config_hdmi_boost=4
# uncomment for composite PAL
#sdtv_mode=2
#uncomment to overclock the arm. 700 MHz is the default.
#arm_freq=800
# Uncomment some or all of these to enable the optional hardware interfaces
#dtparam=i2c_arm=on
#dtparam=i2s=on
#dtparam=spi=on
# Uncomment this to enable infrared communication.
#dtoverlay=gpio-ir,gpio_pin=17
#dtoverlay=gpio-ir-tx,gpio_pin=18
# Additional overlays and parameters are documented /boot/overlays/README
# Enable audio (loads snd_bcm2835)
dtparam=audio=on
# Automatically load overlays for detected cameras
camera_auto_detect=1
# Automatically load overlays for detected DSI displays
display_auto_detect=1
# Enable DRM VC4 V3D driver
dtoverlay=vc4-kms-v3d
max_framebuffers=2
# Run in 64-bit mode
arm_64bit=1
# Disable compensation for displays with overscan
disable_overscan=1
[cm4]
# Enable host mode on the 2711 built-in XHCI USB controller.
# This line should be removed if the legacy DWC2 controller is required
# (e.g. for USB device mode) or if USB support is not required.
otg_mode=1
[all]
[pi4]
# Run as fast as firmware / board allows
arm_boost=1
[all]
gpu_mem=16
dtoverlay=pwm-2chan,disable-bt
initramfs initrd.img-6.1.21-v8+
@@ -0,0 +1,6 @@
os-partitions:
boot: /dev/mmcblk0p1
root: /dev/mmcblk0p2
ethernet-interface: end0
wifi-interface: wlan0
disable-encryption: true
@@ -0,0 +1 @@
options cfg80211 ieee80211_regdom=US
+69
View File
@@ -0,0 +1,69 @@
#!/bin/sh
# ----------------------------------------------------------------------
# extract-ikconfig - Extract the .config file from a kernel image
#
# This will only work when the kernel was compiled with CONFIG_IKCONFIG.
#
# The obscure use of the "tr" filter is to work around older versions of
# "grep" that report the byte offset of the line instead of the pattern.
#
# (c) 2009,2010 Dick Streefland <dick@streefland.net>
# Licensed under the terms of the GNU General Public License.
# ----------------------------------------------------------------------
cf1='IKCFG_ST\037\213\010'
cf2='0123456789'
dump_config()
{
if pos=`tr "$cf1\n$cf2" "\n$cf2=" < "$1" | grep -abo "^$cf2"`
then
pos=${pos%%:*}
tail -c+$(($pos+8)) "$1" | zcat > $tmp1 2> /dev/null
if [ $? != 1 ]
then # exit status must be 0 or 2 (trailing garbage warning)
cat $tmp1
exit 0
fi
fi
}
try_decompress()
{
for pos in `tr "$1\n$2" "\n$2=" < "$img" | grep -abo "^$2"`
do
pos=${pos%%:*}
tail -c+$pos "$img" | $3 > $tmp2 2> /dev/null
dump_config $tmp2
done
}
# Check invocation:
me=${0##*/}
img=$1
if [ $# -ne 1 -o ! -s "$img" ]
then
echo "Usage: $me <kernel-image>" >&2
exit 2
fi
# Prepare temp files:
tmp1=/tmp/ikconfig$$.1
tmp2=/tmp/ikconfig$$.2
trap "rm -f $tmp1 $tmp2" 0
# Initial attempt for uncompressed images or objects:
dump_config "$img"
# That didn't work, so retry after decompression.
try_decompress '\037\213\010' xy gunzip
try_decompress '\3757zXZ\000' abcde unxz
try_decompress 'BZh' xy bunzip2
try_decompress '\135\0\0\0' xxx unlzma
try_decompress '\211\114\132' xy 'lzop -d'
try_decompress '\002\041\114\030' xyy 'lz4 -d -l'
try_decompress '\050\265\057\375' xxx unzstd
# Bail out:
echo "$me: Cannot find kernel config." >&2
exit 1
+49
View File
@@ -0,0 +1,49 @@
# Build Scripts
Helper scripts for building Archipelago OS images.
## Scripts
### `build-backend.sh`
Compiles the Archipelago Rust backend binary.
- Output: `../build/backend/archipelago`
- Requires: Rust toolchain (or Docker)
- Builds for Linux x86_64
### `build-frontend.sh`
Builds the Vue.js frontend for production.
- Output: `../build/frontend/`
- Requires: Node.js 18+, npm
- Builds static files for serving
### `convert-iso-to-disk.sh`
Converts ISO image to raw disk image.
- Input: ISO file
- Output: `.img` file ready for `dd`
- Creates partition layout (EFI + root)
### `check-dependencies.sh`
Checks if all build dependencies are available.
- Checks: Rust, Node.js, Docker, xorriso
- Provides installation instructions
- Non-blocking (warns but continues)
### `install-podman.sh`
Installs Podman container runtime.
- For use inside the target system
- Configures rootless Podman
## Usage
These scripts are called automatically by the main build process. You can also run them manually for testing:
```bash
# Build just the backend
./scripts/build-backend.sh
# Build just the frontend
./scripts/build-frontend.sh
# Check dependencies
./scripts/check-dependencies.sh
```
+69
View File
@@ -0,0 +1,69 @@
#!/bin/bash
# Build Archipelago backend binary for Debian Linux
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
BACKEND_DIR="$PROJECT_ROOT/core/archipelago"
OUTPUT_DIR="$SCRIPT_DIR/../build/backend"
echo "🔨 Building Archipelago backend..."
echo " Source: $BACKEND_DIR"
echo " Output: $OUTPUT_DIR"
echo ""
# Create output directory
mkdir -p "$OUTPUT_DIR"
# Check if we should use Docker
USE_DOCKER=false
if [[ "$OSTYPE" == "darwin"* ]]; then
USE_DOCKER=true
echo "🍎 macOS detected - using Docker for Linux build"
elif ! command -v rustc >/dev/null 2>&1; then
USE_DOCKER=true
echo "⚠️ Rust not found - using Docker"
fi
if [ "$USE_DOCKER" = true ]; then
echo "🐳 Building in Docker container..."
docker run --rm \
-v "$PROJECT_ROOT:/workspace" \
-v "$OUTPUT_DIR:/output" \
-w /workspace/core/archipelago \
rust:trixie \
sh -c '
echo "📦 Installing build dependencies..."
apt-get update && apt-get install -y pkg-config libssl-dev
echo "🔨 Building Archipelago backend..."
cargo build --release
echo "📋 Copying binary to output..."
cp ../target/release/archipelago /output/
echo "✅ Build complete!"
ls -lh /output/archipelago
'
else
# Native Linux build
echo "🐧 Building natively..."
cd "$BACKEND_DIR"
cargo build --release
cp "../target/release/archipelago" "$OUTPUT_DIR/archipelago"
fi
# Strip binary for smaller size
if [ -f "$OUTPUT_DIR/archipelago" ]; then
if command -v strip >/dev/null 2>&1 && [[ "$OSTYPE" != "darwin"* ]]; then
strip "$OUTPUT_DIR/archipelago"
fi
echo ""
echo "✅ Backend built: $OUTPUT_DIR/archipelago"
ls -lh "$OUTPUT_DIR/archipelago"
else
echo "❌ Build failed - binary not found"
exit 1
fi
+47
View File
@@ -0,0 +1,47 @@
#!/bin/bash
# Build Archipelago frontend for production
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
FRONTEND_DIR="$PROJECT_ROOT/neode-ui"
OUTPUT_DIR="$SCRIPT_DIR/../build/frontend"
echo "🎨 Building Archipelago frontend..."
echo " Source: $FRONTEND_DIR"
echo " Output: $OUTPUT_DIR"
echo ""
# Check if Node.js is installed
if ! command -v node >/dev/null 2>&1; then
echo "❌ Node.js not found. Please install Node.js 18+"
exit 1
fi
# Create output directory
mkdir -p "$OUTPUT_DIR"
# Install dependencies if needed
cd "$FRONTEND_DIR"
if [ ! -d "node_modules" ]; then
echo "📦 Installing frontend dependencies..."
npm install
fi
# Build frontend
echo "🔨 Building frontend..."
DOCKER_BUILD=true npm run build || npm run build
# Copy built files
if [ -d "dist" ]; then
cp -r dist/* "$OUTPUT_DIR/"
elif [ -d "../web/dist/neode-ui" ]; then
cp -r ../web/dist/neode-ui/* "$OUTPUT_DIR/"
else
echo "❌ Build output not found"
exit 1
fi
echo "✅ Frontend built: $OUTPUT_DIR"
du -sh "$OUTPUT_DIR"
+89
View File
@@ -0,0 +1,89 @@
#!/bin/bash
# Check build dependencies and provide installation instructions
set -e
echo "🔍 Checking build dependencies..."
echo ""
MISSING_DEPS=0
# Check Rust
if command -v rustc >/dev/null 2>&1; then
echo "✅ Rust: $(rustc --version)"
else
echo "❌ Rust: Not found"
echo " Install: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh"
MISSING_DEPS=$((MISSING_DEPS + 1))
fi
# Check Node.js
if command -v node >/dev/null 2>&1; then
NODE_VERSION=$(node --version)
NODE_MAJOR=$(echo "$NODE_VERSION" | cut -d. -f1 | tr -d 'v')
if [ "$NODE_MAJOR" -ge 18 ]; then
echo "✅ Node.js: $NODE_VERSION"
else
echo "⚠️ Node.js: $NODE_VERSION (need 18+)"
MISSING_DEPS=$((MISSING_DEPS + 1))
fi
else
echo "❌ Node.js: Not found"
echo " Install: https://nodejs.org/"
MISSING_DEPS=$((MISSING_DEPS + 1))
fi
# Check Docker
if command -v docker >/dev/null 2>&1; then
if docker info >/dev/null 2>&1; then
echo "✅ Docker: $(docker --version)"
else
echo "⚠️ Docker: Installed but daemon not running"
echo " Start Docker Desktop or: sudo systemctl start docker"
fi
else
echo "❌ Docker: Not found"
if [[ "$OSTYPE" == "darwin"* ]]; then
echo " Install: https://www.docker.com/products/docker-desktop"
MISSING_DEPS=$((MISSING_DEPS + 1))
else
echo " Install: https://docs.docker.com/get-docker/"
echo " (Optional on Linux if building natively)"
fi
fi
# Check xorriso (for ISO creation)
if command -v xorriso >/dev/null 2>&1; then
echo "✅ xorriso: Installed"
else
echo "❌ xorriso: Not found (needed for ISO creation)"
if [[ "$OSTYPE" == "darwin"* ]]; then
echo " Install: brew install xorriso"
else
echo " Install: apt-get install xorriso"
fi
MISSING_DEPS=$((MISSING_DEPS + 1))
fi
# Check 7z (for ISO extraction)
if command -v 7z >/dev/null 2>&1; then
echo "✅ 7z: Installed"
else
echo "❌ 7z: Not found (needed for ISO extraction)"
if [[ "$OSTYPE" == "darwin"* ]]; then
echo " Install: brew install p7zip"
else
echo " Install: apt-get install p7zip-full"
fi
MISSING_DEPS=$((MISSING_DEPS + 1))
fi
echo ""
if [ $MISSING_DEPS -eq 0 ]; then
echo "✅ All dependencies satisfied!"
exit 0
else
echo "❌ Missing $MISSING_DEPS dependency/dependencies"
echo " Please install missing dependencies before building"
exit 1
fi
+78
View File
@@ -0,0 +1,78 @@
#!/bin/bash
# Podman Installation and Configuration Script for Archipelago
# Configures Podman for rootless operation
set -e
echo "🐳 Configuring Podman for rootless operation..."
if ! command -v catatonit >/dev/null 2>&1; then
if command -v apt-get >/dev/null 2>&1; then
apt-get update || true
apt-get install -y catatonit || true
elif command -v dnf >/dev/null 2>&1; then
dnf install -y catatonit || true
elif command -v apk >/dev/null 2>&1; then
apk add catatonit || true
fi
fi
command -v catatonit >/dev/null 2>&1 || echo "WARNING: catatonit not installed; Podman init-enabled containers may fail"
# Ensure archipelago user exists
if ! id "archipelago" &>/dev/null; then
echo "Creating archipelago user..."
adduser -D -s /bin/bash archipelago
fi
# Create Podman configuration directories
mkdir -p /home/archipelago/.config/containers
mkdir -p /home/archipelago/.local/share/containers/storage
# Configure storage
cat > /home/archipelago/.config/containers/storage.conf <<EOF
[storage]
driver = "overlay"
runroot = "/run/user/$(id -u archipelago)/containers"
graphroot = "/home/archipelago/.local/share/containers/storage"
EOF
# Configure registries (use Docker Hub and quay.io)
mkdir -p /home/archipelago/.config/containers/registries.conf.d
cat > /home/archipelago/.config/containers/registries.conf <<EOF
unqualified-search-registries = ["docker.io", "ghcr.io", "quay.io", "146.59.87.168:3000"]
[[registry]]
location = "146.59.87.168:3000"
insecure = true
EOF
# Set up subuid and subgid for rootless containers
if ! grep -q "^archipelago:" /etc/subuid; then
echo "archipelago:100000:65536" >> /etc/subuid
fi
if ! grep -q "^archipelago:" /etc/subgid; then
echo "archipelago:100000:65536" >> /etc/subgid
fi
# Create systemd user service directory
mkdir -p /home/archipelago/.config/systemd/user
# Enable lingering for archipelago user (allows user services to run without login)
loginctl enable-linger archipelago || true
# Ensure /run/user/1000 exists for podman socket
mkdir -p /run/user/1000
chown archipelago:archipelago /run/user/1000
chmod 700 /run/user/1000
# Enable podman API socket for archipelago user (backend connects via this)
su - archipelago -c "XDG_RUNTIME_DIR=/run/user/1000 systemctl --user enable podman.socket" || true
su - archipelago -c "XDG_RUNTIME_DIR=/run/user/1000 systemctl --user start podman.socket" || true
# Set proper permissions
chown -R archipelago:archipelago /home/archipelago/.config
chown -R archipelago:archipelago /home/archipelago/.local
echo "✅ Podman configuration complete!"
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

+76
View File
@@ -0,0 +1,76 @@
#!/bin/bash
# Sync configuration files from live server to ISO build
#
# Usage: ./sync-from-live.sh [target-host]
#
# This script captures system configuration from the live development
# server and saves it to the image-recipe/configs/ directory for
# inclusion in future ISO builds.
set -e
# Configuration
TARGET_HOST="${1:-archipelago@192.168.1.228}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_DIR="$SCRIPT_DIR/configs"
echo "╔════════════════════════════════════════════════════════════════╗"
echo "║ Syncing Configurations from Live Server ║"
echo "╚════════════════════════════════════════════════════════════════╝"
echo ""
echo "Target: $TARGET_HOST"
echo "Output: $CONFIG_DIR"
echo ""
# Ensure configs directory exists
mkdir -p "$CONFIG_DIR"
# Sync systemd service
echo "📋 Capturing systemd service..."
ssh "$TARGET_HOST" 'sudo cat /etc/systemd/system/archipelago.service' > "$CONFIG_DIR/archipelago.service"
echo " ✅ Saved to configs/archipelago.service"
# Sync nginx configuration
echo "📋 Capturing nginx configuration..."
ssh "$TARGET_HOST" 'sudo cat /etc/nginx/sites-available/archipelago' > "$CONFIG_DIR/nginx-archipelago.conf"
echo " ✅ Saved to configs/nginx-archipelago.conf"
# Sync logrotate if it exists
if ssh "$TARGET_HOST" 'sudo test -f /etc/logrotate.d/archipelago'; then
echo "📋 Capturing logrotate configuration..."
ssh "$TARGET_HOST" 'sudo cat /etc/logrotate.d/archipelago' > "$CONFIG_DIR/logrotate.conf"
echo " ✅ Saved to configs/logrotate.conf"
fi
# Check for custom scripts
echo ""
echo "📋 Checking for custom scripts..."
if ssh "$TARGET_HOST" 'sudo test -d /opt/archipelago/scripts'; then
SCRIPT_COUNT=$(ssh "$TARGET_HOST" 'sudo ls /opt/archipelago/scripts/ 2>/dev/null | wc -l' | tr -d ' ')
if [ "$SCRIPT_COUNT" -gt 0 ]; then
echo " ⚠️ Found $SCRIPT_COUNT script(s) in /opt/archipelago/scripts/"
echo " Review and manually sync if needed"
ssh "$TARGET_HOST" 'sudo ls -lh /opt/archipelago/scripts/'
else
echo " ✅ No custom scripts found"
fi
else
echo " ✅ No custom scripts directory"
fi
# Summary
echo ""
echo "╔════════════════════════════════════════════════════════════════╗"
echo "║ Sync Complete! ║"
echo "╚════════════════════════════════════════════════════════════════╝"
echo ""
echo "Configuration files captured:"
ls -lh "$CONFIG_DIR"
echo ""
echo "Next steps:"
echo " 1. Review the captured configurations"
echo " 2. Build backend: ./scripts/build-backend.sh"
echo " 3. Build frontend: ./scripts/build-frontend.sh"
echo " 4. Update integration script to use these configs"
echo " 5. Build ISO: ./build-debian-iso.sh"
echo ""
+74
View File
@@ -0,0 +1,74 @@
#!/bin/bash
#
# Write Archipelago ISO to USB using dd
#
# Usage: ./write-usb-dd.sh /dev/diskN
#
set -e
if [ -z "$1" ]; then
echo "Usage: $0 /dev/diskN"
echo ""
echo "Available disks:"
diskutil list external
exit 1
fi
USB_DISK="$1"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ISO_FILE="${ARCHIPELAGO_ISO:-}"
if [ -z "$ISO_FILE" ]; then
ISO_FILE="$SCRIPT_DIR/results/archipelago-installer-x86_64.iso"
[ -f "$ISO_FILE" ] || ISO_FILE="$SCRIPT_DIR/results/archipelago-installer-unbundled-x86_64.iso"
fi
if [ ! -f "$ISO_FILE" ]; then
echo "❌ ISO not found: $ISO_FILE"
echo ""
echo "Build the ISO first with: ./build-debian-iso.sh"
echo "Or set ARCHIPELAGO_ISO=/path/to/archipelago-installer-x86_64.iso"
exit 1
fi
# Get raw disk for faster writes
RAW_DISK="${USB_DISK/disk/rdisk}"
echo "╔════════════════════════════════════════════════════════╗"
echo "║ Write Archipelago ISO to USB ║"
echo "╚════════════════════════════════════════════════════════╝"
echo ""
echo "⚠️ WARNING: This will COMPLETELY ERASE $USB_DISK"
echo ""
echo "📀 ISO: $(basename "$ISO_FILE")"
echo "💾 USB: $USB_DISK (raw: $RAW_DISK)"
echo ""
echo "Press Ctrl+C to cancel, or Enter to continue..."
read
echo "🔓 Unmounting USB..."
diskutil unmountDisk "$USB_DISK" || true
echo ""
echo "📝 Writing ISO with dd (this may take a few minutes)..."
echo " Using raw disk $RAW_DISK for faster write..."
echo ""
# Use dd to write the ISO directly
sudo dd if="$ISO_FILE" of="$RAW_DISK" bs=4m status=progress
echo ""
echo "🔄 Syncing..."
sync
echo ""
echo "✅ Done! USB is ready."
echo ""
echo "Now:"
echo " 1. Eject the USB safely: diskutil eject $USB_DISK"
echo " 2. Insert into target machine"
echo " 3. Boot from USB (F12 or similar for boot menu)"
echo ""
echo "Default login (live mode):"
echo " Username: user"
echo " Password: live"