Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c952bb02d | ||
|
|
3dde239177 | ||
|
|
eb6f76c909 | ||
|
|
101cb5f42d | ||
|
|
449f47da49 | ||
|
|
8814b03e33 | ||
|
|
78e877311d | ||
|
|
ae97f4a979 | ||
|
|
9d1904cddc | ||
|
|
c81ef5ad79 | ||
|
|
d243cbb83e | ||
|
|
37f5790165 | ||
|
|
69f52f7260 | ||
|
|
1b1300729c | ||
|
|
380af7e1cb | ||
|
|
09474789fd | ||
|
|
7fdb85713a | ||
|
|
81b4db82d1 | ||
|
|
a808458124 |
@@ -11,33 +11,19 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Checkout
|
||||
id: checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
timeout-minutes: 5
|
||||
continue-on-error: true
|
||||
|
||||
- name: Sync from local repo (fallback if checkout failed)
|
||||
run: |
|
||||
# Only sync from ~/archy if checkout failed or workspace is empty
|
||||
if [ -f "CLAUDE.md" ] && [ -d "core" ] && [ -d "neode-ui" ]; then
|
||||
echo "Checkout succeeded — using checked-out code"
|
||||
elif [ -d "$HOME/archy/core" ] && [ -d "$HOME/archy/neode-ui" ]; then
|
||||
echo "Checkout failed — syncing from ~/archy (LAN fallback)..."
|
||||
rsync -a \
|
||||
--exclude '.git' --exclude 'node_modules' --exclude 'target' \
|
||||
--exclude 'image-recipe/build' --exclude 'image-recipe/results' \
|
||||
--exclude 'web/dist' \
|
||||
"$HOME/archy/" ./
|
||||
else
|
||||
echo "ERROR: No checkout and no local fallback"
|
||||
exit 1
|
||||
fi
|
||||
echo "Workspace verification:"
|
||||
# Direct fetch + sync (actions/checkout token is broken on this Gitea)
|
||||
cd /home/archipelago/archy && 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' \
|
||||
/home/archipelago/archy/ "$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"
|
||||
grep -q 'apache2-utils' image-recipe/build-auto-installer-iso.sh 2>/dev/null && echo " apache2-utils: PRESENT" || echo " apache2-utils: MISSING"
|
||||
|
||||
- name: Install ISO build dependencies
|
||||
run: |
|
||||
|
||||
@@ -11,10 +11,19 @@ jobs:
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
clean: true
|
||||
run: |
|
||||
# Direct clone using stored credentials (actions/checkout token is broken)
|
||||
cd /home/archipelago/archy && git fetch origin main && git reset --hard origin/main
|
||||
echo "=== Source at commit: $(git log --oneline -1) ==="
|
||||
echo "=== Syncing to workspace ==="
|
||||
rsync -a --delete --exclude='.git' --exclude='target/' --exclude='node_modules/' \
|
||||
/home/archipelago/archy/ "$GITHUB_WORKSPACE/" || cp -a /home/archipelago/archy/* "$GITHUB_WORKSPACE/"
|
||||
cd "$GITHUB_WORKSPACE"
|
||||
echo "=== Workspace version: $(grep '^version' core/archipelago/Cargo.toml) ==="
|
||||
echo "=== Key files ==="
|
||||
echo " first-boot: $([ -f scripts/first-boot-containers.sh ] && echo PRESENT || echo MISSING)"
|
||||
echo " Cargo.toml: $(grep '^version' core/archipelago/Cargo.toml)"
|
||||
echo " package.json: $(grep '\"version\"' neode-ui/package.json | head -1)"
|
||||
|
||||
- name: Build backend
|
||||
run: |
|
||||
|
||||
BIN
Android/archipelago-0.3.0-debug.apk.zip
Normal file
BIN
Android/archipelago-0.3.0-debug.apk.zip
Normal file
Binary file not shown.
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "archipelago"
|
||||
version = "1.3.1"
|
||||
version = "1.3.4"
|
||||
edition = "2021"
|
||||
description = "Archipelago Bitcoin Node OS - Native backend"
|
||||
authors = ["Archipelago Team"]
|
||||
|
||||
@@ -124,6 +124,12 @@ pub(super) fn get_app_capabilities(app_id: &str) -> Vec<String> {
|
||||
"--cap-add=DAC_OVERRIDE".to_string(),
|
||||
"--cap-add=NET_BIND_SERVICE".to_string(),
|
||||
],
|
||||
// Nostr VPN and FIPS: mesh networking daemons need TUN + NET_ADMIN
|
||||
// Note: --device=/dev/net/tun is added separately in install.rs
|
||||
"nostr-vpn" | "fips" => vec![
|
||||
"--cap-add=NET_ADMIN".to_string(),
|
||||
"--cap-add=NET_RAW".to_string(),
|
||||
],
|
||||
// Default: standard capabilities for rootless podman containers
|
||||
// Most apps need file ownership + port binding to function correctly
|
||||
_ => vec![
|
||||
@@ -239,6 +245,13 @@ pub(super) fn get_health_check_args(app_id: &str, _rpc_pass: &str) -> Vec<String
|
||||
"30s",
|
||||
"3",
|
||||
),
|
||||
"routstr" => (
|
||||
"curl -sf http://localhost:8000/v1/models || exit 1",
|
||||
"30s",
|
||||
"3",
|
||||
),
|
||||
"nostr-vpn" => ("nvpn status || exit 1", "30s", "3"),
|
||||
"fips" => ("fipsctl status || exit 1", "30s", "3"),
|
||||
_ => return vec![],
|
||||
};
|
||||
|
||||
@@ -279,6 +292,9 @@ pub(super) fn get_memory_limit(app_id: &str) -> &'static str {
|
||||
"dwn" => "256m",
|
||||
"portainer" => "256m",
|
||||
"nostr-rs-relay" | "nostr-relay" => "256m",
|
||||
"routstr" => "512m",
|
||||
"nostr-vpn" => "256m",
|
||||
"fips" => "256m",
|
||||
"nginx-proxy-manager" => "256m",
|
||||
// Databases
|
||||
"archy-btcpay-db" | "archy-mempool-db" | "mysql-mempool" => "512m",
|
||||
@@ -343,6 +359,9 @@ pub(super) fn all_container_names(package_id: &str) -> Vec<String> {
|
||||
"penpot-postgres".into(), "penpot-valkey".into(),
|
||||
"penpot-backend".into(), "penpot-exporter".into(), "penpot-frontend".into(),
|
||||
],
|
||||
"nostr-vpn" => vec!["nostr-vpn".into(), "archy-nostr-vpn".into()],
|
||||
"fips" => vec!["fips".into(), "archy-fips".into()],
|
||||
"routstr" => vec!["routstr".into(), "archy-routstr".into()],
|
||||
// Default: exact name + archy- prefix
|
||||
_ => vec![base, archy],
|
||||
}
|
||||
@@ -407,6 +426,21 @@ fn read_secret(name: &str, default: &str) -> String {
|
||||
.unwrap_or_else(|_| default.to_string())
|
||||
}
|
||||
|
||||
/// Read the node-level Nostr secret key (hex) for identity-aware apps.
|
||||
/// Returns empty string if not yet generated.
|
||||
fn read_nostr_secret_hex() -> String {
|
||||
std::fs::read_to_string("/var/lib/archipelago/identity/nostr_secret")
|
||||
.map(|s| s.trim().to_string())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Read the node-level Nostr public key (hex).
|
||||
fn read_nostr_pubkey_hex() -> String {
|
||||
std::fs::read_to_string("/var/lib/archipelago/identity/nostr_pub")
|
||||
.map(|s| s.trim().to_string())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Get app-specific configuration
|
||||
/// Returns: (ports, volumes, env_vars, custom_command, custom_args)
|
||||
pub(super) async fn get_app_config(
|
||||
@@ -771,6 +805,62 @@ pub(super) async fn get_app_config(
|
||||
None,
|
||||
None,
|
||||
),
|
||||
"routstr" => {
|
||||
let nsec = read_nostr_secret_hex();
|
||||
let mut env = vec![
|
||||
"DATABASE_URL=sqlite:///app/data/keys.db".to_string(),
|
||||
];
|
||||
if !nsec.is_empty() {
|
||||
env.push(format!("NSEC={}", nsec));
|
||||
env.push(format!("NOSTR_PUBKEY={}", read_nostr_pubkey_hex()));
|
||||
}
|
||||
(
|
||||
vec!["8200:8000".to_string()],
|
||||
vec!["/var/lib/archipelago/routstr:/app/data".to_string()],
|
||||
env,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
}
|
||||
"nostr-vpn" => {
|
||||
let nsec = read_nostr_secret_hex();
|
||||
let mut env = vec![];
|
||||
if !nsec.is_empty() {
|
||||
env.push(format!("NOSTR_SECRET={}", nsec));
|
||||
env.push(format!("NOSTR_PUBKEY={}", read_nostr_pubkey_hex()));
|
||||
}
|
||||
(
|
||||
vec!["51820:51820/udp".to_string()],
|
||||
vec!["/var/lib/archipelago/nostr-vpn:/root/.config/nvpn".to_string()],
|
||||
env,
|
||||
None,
|
||||
Some(vec![
|
||||
"start".to_string(),
|
||||
"--daemon".to_string(),
|
||||
]),
|
||||
)
|
||||
}
|
||||
"fips" => {
|
||||
let nsec = read_nostr_secret_hex();
|
||||
let mut env = vec![];
|
||||
if !nsec.is_empty() {
|
||||
env.push(format!("FIPS_NOSTR_SECRET={}", nsec));
|
||||
env.push(format!("FIPS_NOSTR_PUBKEY={}", read_nostr_pubkey_hex()));
|
||||
}
|
||||
(
|
||||
vec![
|
||||
"2121:2121/udp".to_string(),
|
||||
"8443:8443".to_string(),
|
||||
],
|
||||
vec![
|
||||
"/var/lib/archipelago/fips/config:/etc/fips".to_string(),
|
||||
"/var/lib/archipelago/fips/run:/run/fips".to_string(),
|
||||
],
|
||||
env,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
}
|
||||
"dwn" => (
|
||||
vec!["3100:3000".to_string()],
|
||||
vec!["/var/lib/archipelago/dwn:/dwn/data".to_string()],
|
||||
|
||||
@@ -225,6 +225,11 @@ impl RpcHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// TUN device for mesh networking apps
|
||||
if matches!(package_id, "nostr-vpn" | "fips") {
|
||||
run_args.push("--device=/dev/net/tun");
|
||||
}
|
||||
|
||||
// Create data directories
|
||||
self.create_data_dirs(package_id, &volumes).await;
|
||||
|
||||
@@ -259,6 +264,35 @@ impl RpcHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-install: write Nostr identity key files for headless Nostr-aware apps
|
||||
if matches!(package_id, "nostr-vpn" | "fips") {
|
||||
let nostr_secret = std::fs::read_to_string("/var/lib/archipelago/identity/nostr_secret")
|
||||
.map(|s| s.trim().to_string())
|
||||
.unwrap_or_default();
|
||||
if !nostr_secret.is_empty() {
|
||||
let key_dir = match package_id {
|
||||
"nostr-vpn" => "/var/lib/archipelago/nostr-vpn",
|
||||
"fips" => "/var/lib/archipelago/fips/config",
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let key_path = match package_id {
|
||||
"nostr-vpn" => format!("{}/nostr_secret", key_dir),
|
||||
"fips" => format!("{}/fips.key", key_dir),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
tokio::fs::create_dir_all(key_dir).await.ok();
|
||||
tokio::fs::write(&key_path, &nostr_secret).await.ok();
|
||||
// Restrict permissions on key file
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let perms = std::fs::Permissions::from_mode(0o600);
|
||||
std::fs::set_permissions(&key_path, perms).ok();
|
||||
}
|
||||
info!("Wrote Nostr identity key for {}", package_id);
|
||||
}
|
||||
}
|
||||
|
||||
// Port mappings (skip for host-network containers)
|
||||
if !is_tailscale {
|
||||
for port in &ports {
|
||||
|
||||
@@ -409,6 +409,7 @@ fn container_boot_tier(name: &str) -> u8 {
|
||||
|
||||
/// Run the reconciliation script after boot to fix any config drift.
|
||||
/// Ensures all containers match their canonical specs from container-specs.sh.
|
||||
#[allow(dead_code)]
|
||||
pub async fn run_boot_reconciliation() {
|
||||
let script = "/home/archipelago/archy/scripts/reconcile-containers.sh";
|
||||
if !std::path::Path::new(script).exists() {
|
||||
|
||||
@@ -491,6 +491,7 @@ pub fn spawn_health_monitor(state: Arc<StateManager>, data_dir: PathBuf) {
|
||||
// Load persistent restart history and seed the in-memory tracker
|
||||
let mut restart_history = RestartHistory::load(&data_dir).await;
|
||||
restart_history.seed_tracker(&mut tracker);
|
||||
#[allow(unused_assignments)]
|
||||
let mut history_dirty = false;
|
||||
|
||||
loop {
|
||||
|
||||
@@ -66,7 +66,8 @@ fi
|
||||
echo "$BUILD_NUM" | sudo tee "$BUILD_COUNTER_FILE" > /dev/null 2>/dev/null || BUILD_NUM=1
|
||||
GIT_SHORT=$(cd "$SCRIPT_DIR/.." && git rev-parse --short HEAD 2>/dev/null || echo "dev")
|
||||
# Version format: major.minor.patch-prerelease (semver)
|
||||
BUILD_VERSION="1.3.0-alpha"
|
||||
# Read version from Cargo.toml (single source of truth)
|
||||
BUILD_VERSION=$(grep '^version' "$SCRIPT_DIR/../core/archipelago/Cargo.toml" 2>/dev/null | head -1 | sed 's/version = "//;s/"//' || echo "0.0.0")
|
||||
echo "Build #${BUILD_NUM} (${BUILD_VERSION}, commit ${GIT_SHORT})"
|
||||
|
||||
# Architecture-dependent variables
|
||||
@@ -289,6 +290,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
firmware-realtek \
|
||||
firmware-iwlwifi \
|
||||
firmware-misc-nonfree \
|
||||
firmware-linux-nonfree \
|
||||
intel-microcode \
|
||||
amd64-microcode \
|
||||
xorg \
|
||||
@@ -1055,7 +1057,7 @@ IMAGES_CAPTURED_FROM_SERVER=0
|
||||
if [ -n "$DEV_SERVER" ] && [ "$DEV_SERVER" != "localhost" ] && [ "$DEV_SERVER" != "127.0.0.1" ]; then
|
||||
echo " Capturing container images from live server ($DEV_SERVER)..."
|
||||
# Patterns match against `podman images` repository names (not container names)
|
||||
CAPTURE_PATTERNS="bitcoin-ui bitcoinknots lnd lnd-ui electrs-ui filebrowser mempool backend frontend electrs tailscale homeassistant home-assistant btcpayserver nbxplorer postgres alpine-tor nostr-rs-relay strfry fedimintd gatewayd dwn-server grafana uptime-kuma jellyfin vaultwarden searxng mariadb valkey nginx-alpine portainer photoprism nextcloud nginx-proxy-manager onlyoffice adguard"
|
||||
CAPTURE_PATTERNS="bitcoin-ui bitcoinknots lnd lnd-ui electrs-ui filebrowser mempool backend frontend electrs tailscale homeassistant home-assistant btcpayserver nbxplorer postgres alpine-tor nostr-rs-relay strfry fedimintd gatewayd dwn-server vaultwarden searxng mariadb valkey nginx-alpine portainer nginx-proxy-manager adguard"
|
||||
REMOTE_TMP="/tmp/archipelago-image-capture-$$"
|
||||
SAVED_LIST=$(ssh "$DEV_SERVER" "mkdir -p $REMOTE_TMP && for p in $CAPTURE_PATTERNS; do img=\$(podman images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep -i \"\$p\" | head -1); [ -n \"\$img\" ] && podman save -o \"$REMOTE_TMP/\$p.tar\" \"\$img\" 2>/dev/null && echo \"\$p\"; done" 2>/dev/null) || true
|
||||
for p in $SAVED_LIST; do
|
||||
@@ -2181,6 +2183,9 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
m = data.get("model", "")
|
||||
if m in MODEL_MAP: data["model"] = MODEL_MAP[m]
|
||||
body = json.dumps(data).encode()
|
||||
if not API_KEY:
|
||||
err = json.dumps({"type":"error","error":{"type":"auth_error","message":"AIUI not configured. Set your Anthropic API key in Settings > AIUI to enable AI chat."}}).encode()
|
||||
self.send_response(401); self.send_header("Content-Type","application/json"); self.send_header("Content-Length",str(len(err))); self.end_headers(); self.wfile.write(err); return
|
||||
headers = {"Content-Type":"application/json","x-api-key":API_KEY,"anthropic-version":"2023-06-01","anthropic-dangerous-direct-browser-access":"true"}
|
||||
for h in ["anthropic-version","anthropic-beta"]:
|
||||
if self.headers.get(h): headers[h] = self.headers[h]
|
||||
@@ -2210,7 +2215,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
self.send_response(200); self.send_header("Content-Type","application/json"); self.end_headers(); self.wfile.write(b'{"status":"ok"}')
|
||||
else: self.send_response(404); self.end_headers()
|
||||
def log_message(self, fmt, *args): pass
|
||||
if not API_KEY: print("ERROR: ANTHROPIC_API_KEY not set — configure via setup-aiui-server.sh"); sys.exit(1)
|
||||
if not API_KEY: print("WARNING: ANTHROPIC_API_KEY not set — AIUI will return setup instructions")
|
||||
server = http.server.HTTPServer(("127.0.0.1", PORT), Handler)
|
||||
print(f"Claude API proxy on port {PORT}")
|
||||
server.serve_forever()
|
||||
@@ -2474,6 +2479,10 @@ rm -f /mnt/target/usr/share/initramfs-tools/hooks/live* 2>/dev/null || true
|
||||
|
||||
# Suppress os-prober warning in GRUB
|
||||
echo "GRUB_DISABLE_OS_PROBER=true" >> /mnt/target/etc/default/grub
|
||||
# GFX fallback for hardware without graphical GRUB support
|
||||
echo 'GRUB_GFXMODE=auto' >> /mnt/target/etc/default/grub
|
||||
echo 'GRUB_GFXPAYLOAD_LINUX=keep' >> /mnt/target/etc/default/grub
|
||||
echo 'GRUB_TERMINAL_OUTPUT=gfxterm' >> /mnt/target/etc/default/grub
|
||||
|
||||
# Install Archipelago GRUB theme on target system
|
||||
if [ -d "$BOOT_MEDIA/boot/grub/themes/archipelago" ]; then
|
||||
@@ -2944,12 +2953,15 @@ fi
|
||||
set timeout=5
|
||||
set default=0
|
||||
|
||||
# Load font for graphical menu
|
||||
# Load font for graphical menu — fallback to text mode on hardware without gfxterm
|
||||
if loadfont ($root)/boot/grub/font.pf2; then
|
||||
set gfxmode=auto
|
||||
set gfxpayload=keep
|
||||
insmod gfxterm
|
||||
insmod png
|
||||
terminal_output gfxterm
|
||||
else
|
||||
terminal_output console
|
||||
fi
|
||||
|
||||
# Archipelago GRUB theme
|
||||
|
||||
@@ -24,6 +24,10 @@ xset s noblank 2>/dev/null
|
||||
# Hide cursor
|
||||
unclutter -idle 3 -root &
|
||||
|
||||
# Kill any stale Chromium instances before starting
|
||||
pkill -u archipelago -f 'chromium.*kiosk' 2>/dev/null
|
||||
sleep 1
|
||||
|
||||
# Run Chromium as archipelago user in a restart loop
|
||||
while true; do
|
||||
sudo -u archipelago env DISPLAY=:0 HOME=/home/archipelago chromium --kiosk \
|
||||
@@ -38,12 +42,12 @@ while true; do
|
||||
--disable-save-password-bubble \
|
||||
--disable-suggestions-service \
|
||||
--disable-component-update \
|
||||
--disable-gpu \
|
||||
--disable-gpu-compositing \
|
||||
--disable-gpu-rasterization \
|
||||
--disable-software-rasterizer \
|
||||
--num-raster-threads=1 \
|
||||
--renderer-process-limit=1 \
|
||||
--enable-gpu-rasterization \
|
||||
--num-raster-threads=2 \
|
||||
--renderer-process-limit=2 \
|
||||
--window-size=9999,9999 \
|
||||
--window-position=0,0 \
|
||||
--start-fullscreen \
|
||||
--disable-background-networking \
|
||||
--disable-background-timer-throttling \
|
||||
--disable-backgrounding-occluded-windows \
|
||||
|
||||
@@ -514,6 +514,29 @@ server {
|
||||
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;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
proxy_set_header Accept-Encoding "";
|
||||
sub_filter_once on;
|
||||
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
||||
}
|
||||
location /app/nostr-vpn/ {
|
||||
default_type application/json;
|
||||
return 503 '{"error":{"code":"NO_WEB_UI","message":"Nostr VPN is managed via CLI"}}';
|
||||
}
|
||||
location /app/fips/ {
|
||||
default_type application/json;
|
||||
return 503 '{"error":{"code":"NO_WEB_UI","message":"FIPS is managed via CLI"}}';
|
||||
}
|
||||
location /app/ollama/ {
|
||||
proxy_pass http://127.0.0.1:11434/;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
@@ -197,6 +197,28 @@ location /app/tailscale/ {
|
||||
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/ {
|
||||
default_type application/json;
|
||||
return 503 '{"error":{"code":"NO_WEB_UI","message":"Nostr VPN is managed via CLI"}}';
|
||||
}
|
||||
location /app/fips/ {
|
||||
default_type application/json;
|
||||
return 503 '{"error":{"code":"NO_WEB_UI","message":"FIPS is managed via CLI"}}';
|
||||
}
|
||||
location /app/ollama/ {
|
||||
proxy_pass http://127.0.0.1:11434/;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "neode-ui",
|
||||
"private": true,
|
||||
"version": "1.2.0-alpha",
|
||||
"version": "1.3.4",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "./start-dev.sh",
|
||||
|
||||
4
neode-ui/public/assets/img/app-icons/fips.svg
Normal file
4
neode-ui/public/assets/img/app-icons/fips.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="64" height="64" rx="12" fill="#10b981"/>
|
||||
<text x="32" y="38" text-anchor="middle" font-family="system-ui" font-size="16" font-weight="700" fill="white">FIPS</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 284 B |
4
neode-ui/public/assets/img/app-icons/nostr-vpn.svg
Normal file
4
neode-ui/public/assets/img/app-icons/nostr-vpn.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="64" height="64" rx="12" fill="#6366f1"/>
|
||||
<text x="32" y="38" text-anchor="middle" font-family="system-ui" font-size="18" font-weight="700" fill="white">NV</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 282 B |
4
neode-ui/public/assets/img/app-icons/routstr.svg
Normal file
4
neode-ui/public/assets/img/app-icons/routstr.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="64" height="64" rx="12" fill="#f59e0b"/>
|
||||
<text x="32" y="38" text-anchor="middle" font-family="system-ui" font-size="18" font-weight="700" fill="white">R</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 281 B |
@@ -29,4 +29,15 @@ export async function isOnboardingComplete(): Promise<boolean> {
|
||||
export async function completeOnboarding(): Promise<void> {
|
||||
await callWithRetry(() => rpcClient.completeOnboarding(), 3)
|
||||
localStorage.setItem('neode_onboarding_complete', '1')
|
||||
localStorage.removeItem('neode_onboarding_step')
|
||||
}
|
||||
|
||||
/** Save current onboarding step so refresh resumes where user left off */
|
||||
export function saveOnboardingStep(step: string): void {
|
||||
localStorage.setItem('neode_onboarding_step', step)
|
||||
}
|
||||
|
||||
/** Get the last saved onboarding step, or 'intro' if none */
|
||||
export function getSavedOnboardingStep(): string {
|
||||
return localStorage.getItem('neode_onboarding_step') || 'intro'
|
||||
}
|
||||
|
||||
@@ -4,6 +4,24 @@ import './style.css'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import i18n from './i18n'
|
||||
|
||||
// Clipboard polyfill for HTTP (non-secure) contexts where navigator.clipboard is unavailable
|
||||
if (!navigator.clipboard) {
|
||||
Object.defineProperty(navigator, 'clipboard', {
|
||||
value: {
|
||||
async writeText(text: string) {
|
||||
const ta = document.createElement('textarea')
|
||||
ta.value = text
|
||||
ta.style.cssText = 'position:fixed;opacity:0'
|
||||
document.body.appendChild(ta)
|
||||
ta.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(ta)
|
||||
},
|
||||
async readText() { return '' },
|
||||
},
|
||||
})
|
||||
}
|
||||
import { useToast } from '@/composables/useToast'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
@@ -303,10 +303,11 @@ router.beforeEach(async (to, _from, next) => {
|
||||
}
|
||||
// Check if this is a fresh install that needs onboarding
|
||||
try {
|
||||
const { isOnboardingComplete } = await import('@/composables/useOnboarding')
|
||||
const { isOnboardingComplete, getSavedOnboardingStep } = await import('@/composables/useOnboarding')
|
||||
const setupDone = await isOnboardingComplete()
|
||||
if (!setupDone) {
|
||||
next('/onboarding/intro')
|
||||
const step = getSavedOnboardingStep()
|
||||
next(`/onboarding/${step}`)
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
@@ -328,6 +329,14 @@ router.beforeEach(async (to, _from, next) => {
|
||||
next()
|
||||
})
|
||||
|
||||
// Persist onboarding step so page refresh resumes where user left off
|
||||
router.afterEach((to) => {
|
||||
const match = to.path.match(/^\/onboarding\/(.+)/)
|
||||
if (match && match[1] !== 'intro') {
|
||||
localStorage.setItem('neode_onboarding_step', match[1]!)
|
||||
}
|
||||
})
|
||||
|
||||
// Stop all login/splash audio when entering the dashboard
|
||||
router.afterEach((to, from) => {
|
||||
if (to.path.startsWith('/dashboard') && !from.path.startsWith('/dashboard')) {
|
||||
|
||||
@@ -34,6 +34,15 @@
|
||||
>
|
||||
Restore from seed phrase
|
||||
</a>
|
||||
<a
|
||||
tabindex="0"
|
||||
role="button"
|
||||
class="text-white/50 hover:text-white/80 underline text-sm cursor-pointer mt-2 block text-center onb-cta"
|
||||
@click="goToLogin"
|
||||
@keydown.enter="goToLogin"
|
||||
>
|
||||
Already set up? Log in
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -64,6 +73,12 @@ function goToRestore() {
|
||||
playNavSound('action')
|
||||
router.push('/onboarding/seed-restore').catch(() => {})
|
||||
}
|
||||
|
||||
function goToLogin() {
|
||||
playNavSound('action')
|
||||
localStorage.setItem('neode_onboarding_complete', '1')
|
||||
router.push('/login').catch(() => {})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -39,6 +39,15 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="restoredNpub" class="path-option-card cursor-default px-4 py-4 sm:px-6 sm:py-5">
|
||||
<div class="text-left">
|
||||
<h3 class="text-xs sm:text-sm font-semibold text-white/80 mb-2 uppercase tracking-wide">Your Nostr ID</h3>
|
||||
<div class="bg-black/40 rounded-lg p-3 sm:p-4 backdrop-blur-sm border border-white/10">
|
||||
<p class="text-white/95 font-mono text-xs sm:text-sm break-all leading-relaxed">{{ restoredNpub }}</p>
|
||||
</div>
|
||||
<p class="text-xs text-white/50 mt-2">For Nostr social apps and NIP-07 signing</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Word Input Grid -->
|
||||
@@ -120,6 +129,7 @@ const isRestoring = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const serverStarting = ref(false)
|
||||
const restoredDid = ref('')
|
||||
const restoredNpub = ref('')
|
||||
|
||||
const allFilled = computed(() => seedWords.value.every(w => w.trim().length > 0))
|
||||
|
||||
@@ -167,6 +177,7 @@ async function restore() {
|
||||
if (res.restored) {
|
||||
restored.value = true
|
||||
restoredDid.value = res.did
|
||||
restoredNpub.value = res.nostr_npub || ''
|
||||
if (res.did) localStorage.setItem('neode_did', res.did)
|
||||
if (res.nostr_npub) localStorage.setItem('neode_nostr_npub', res.nostr_npub)
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ export const APP_PORTS: Record<string, number> = {
|
||||
'fedimintd': 8175,
|
||||
'fedimint-gateway': 8176,
|
||||
'nostr-rs-relay': 18081,
|
||||
'routstr': 8200,
|
||||
'indeedhub': 7777,
|
||||
'dwn': 3100,
|
||||
'endurain': 8080,
|
||||
@@ -83,6 +84,7 @@ export const HTTPS_PROXY_PATHS: Record<string, string> = {
|
||||
'penpot': '/app/penpot/',
|
||||
'grafana': '/app/grafana/',
|
||||
'indeedhub': '/app/indeedhub/',
|
||||
'routstr': '/app/routstr/',
|
||||
}
|
||||
|
||||
/** External HTTPS apps -- always loaded directly */
|
||||
@@ -100,6 +102,7 @@ export const EXTERNAL_URLS: Record<string, string> = {
|
||||
export const APP_TITLES: Record<string, string> = {
|
||||
'bitcoin-knots': 'Bitcoin', 'btcpay-server': 'BTCPay Server', 'indeedhub': 'Indeehub',
|
||||
'botfights': 'BotFights', '484-kitchen': '484 Kitchen', 'arch-presentation': 'Presentation',
|
||||
'nostr-vpn': 'Nostr VPN', 'fips': 'FIPS', 'routstr': 'Routstr',
|
||||
'homeassistant': 'Home Assistant', 'uptime-kuma': 'Uptime Kuma',
|
||||
'nginx-proxy-manager': 'Nginx Proxy Manager', 'nostr-rs-relay': 'Nostr Relay',
|
||||
'call-the-operator': 'Call The Operator', 'syntropy-institute': 'Syntropy Institute',
|
||||
@@ -120,6 +123,7 @@ export const NEW_TAB_APPS = new Set([
|
||||
'onlyoffice',
|
||||
'nginx-proxy-manager',
|
||||
'tailscale',
|
||||
'routstr',
|
||||
])
|
||||
|
||||
/** Sites known to block iframes -- skip the timeout and go straight to fallback */
|
||||
|
||||
@@ -15,7 +15,7 @@ export interface SelectedIdentity {
|
||||
}
|
||||
|
||||
function isIdentityAwareApp(id: string): boolean {
|
||||
return id === 'indeedhub' || id === 'nostrudel'
|
||||
return id === 'indeedhub' || id === 'nostrudel' || id === 'routstr'
|
||||
}
|
||||
|
||||
export function useAppIdentity(
|
||||
|
||||
@@ -10,17 +10,17 @@
|
||||
@click="$emit('goToApp', id)"
|
||||
@keydown.enter="handleEnter"
|
||||
>
|
||||
<!-- Installing overlay -->
|
||||
<!-- Installing overlay — shown for both client-tracked installs and backend 'installing' state -->
|
||||
<div
|
||||
v-if="isInstalling"
|
||||
v-if="isInstalling || pkg.state === 'installing'"
|
||||
class="absolute inset-0 z-20 flex items-center justify-center bg-black/70 backdrop-blur-sm rounded-xl"
|
||||
>
|
||||
<div class="flex items-center gap-3 text-white/90">
|
||||
<svg class="animate-spin h-5 w-5" fill="none" viewBox="0 0 24 24">
|
||||
<div class="flex flex-col items-center gap-3 text-white/90">
|
||||
<svg class="animate-spin h-6 w-6" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
<span class="text-sm font-medium">Installing</span>
|
||||
<span class="text-sm font-medium">{{ installProgress?.message || 'Installing...' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
|
||||
<!-- Uninstall Icon (not for web-only apps) -->
|
||||
<button
|
||||
v-if="!isWebOnly && !isUninstalling"
|
||||
v-if="!isWebOnly && !isUninstalling && !isInstalling && pkg.state !== 'installing'"
|
||||
@click.stop="$emit('showUninstall', id, pkg)"
|
||||
class="absolute top-4 right-4 p-2 rounded-lg text-white/60 hover:text-red-400 hover:bg-red-500/20 transition-colors z-10"
|
||||
:aria-label="`${t('common.uninstall')} ${pkg.manifest?.title || id}`"
|
||||
|
||||
@@ -35,10 +35,11 @@ export const APP_CATEGORY_MAP: Record<string, string> = {
|
||||
'lnd': 'money', 'mempool': 'money', 'mempool-web': 'money', 'btcpay-server': 'commerce',
|
||||
'fedimint': 'money', 'fedimint-gateway': 'money',
|
||||
'indeedhub': 'media', 'jellyfin': 'media', 'photoprism': 'media', 'immich': 'media',
|
||||
'nextcloud': 'data', 'vaultwarden': 'data', 'filebrowser': 'data', 'onlyoffice': 'data',
|
||||
'nextcloud': 'data', 'vaultwarden': 'data', 'filebrowser': 'data', 'cryptpad': 'data',
|
||||
'homeassistant': 'home', 'lorabell': 'home', 'endurain': 'home',
|
||||
'searxng': 'community', 'ollama': 'community', 'grafana': 'data',
|
||||
'nostr-rs-relay': 'nostr', 'nostrudel': 'nostr',
|
||||
'nostr-vpn': 'networking', 'fips': 'networking', 'routstr': 'community',
|
||||
'tailscale': 'networking', 'nginx-proxy-manager': 'networking', 'portainer': 'networking',
|
||||
'uptime-kuma': 'networking', 'dwn': 'data',
|
||||
'botfights': 'l484', 'nwnn': 'l484', '484-kitchen': 'l484',
|
||||
@@ -103,7 +104,7 @@ export const WEB_ONLY_APPS: Record<string, PackageDataEntry> = {
|
||||
export const TAB_LAUNCH_APPS = new Set([
|
||||
'btcpay-server', 'grafana', 'photoprism', 'homeassistant',
|
||||
'vaultwarden', 'nextcloud', 'uptime-kuma', 'portainer',
|
||||
'onlyoffice', 'nginx-proxy-manager', 'tailscale',
|
||||
'cryptpad', 'nginx-proxy-manager', 'tailscale', 'routstr',
|
||||
])
|
||||
|
||||
export function opensInTab(id: string): boolean {
|
||||
|
||||
@@ -12,7 +12,7 @@ export function getCuratedAppList(): MarketplaceApp[] {
|
||||
{ id: 'grafana', title: 'Grafana', version: '10.2.0', description: 'Analytics and monitoring platform. Dashboards for your node metrics and system health.', icon: '/assets/img/app-icons/grafana.png', author: 'Grafana Labs', dockerImage: `${R}/grafana:10.2.0`, repoUrl: 'https://github.com/grafana/grafana' },
|
||||
{ id: 'searxng', title: 'SearXNG', version: '2024.1.0', description: 'Privacy-respecting metasearch engine. Search the internet without being tracked or profiled.', icon: '/assets/img/app-icons/searxng.png', author: 'SearXNG', dockerImage: `${R}/searxng:latest`, repoUrl: 'https://github.com/searxng/searxng' },
|
||||
{ id: 'ollama', title: 'Ollama', version: '0.5.4', description: 'Run AI models locally. Llama, Mistral, and more — on your hardware, completely private.', icon: '/assets/img/app-icons/ollama.png', author: 'Ollama', dockerImage: `${R}/ollama:latest`, repoUrl: 'https://github.com/ollama/ollama' },
|
||||
{ id: 'onlyoffice', title: 'OnlyOffice', version: '7.5.1', description: 'Self-hosted office suite. Documents, spreadsheets, and presentations without the cloud.', icon: '/assets/img/app-icons/onlyoffice.webp', author: 'Ascensio System SIA', dockerImage: `${R}/onlyoffice:latest`, repoUrl: 'https://github.com/ONLYOFFICE/DocumentServer' },
|
||||
{ id: 'cryptpad', title: 'CryptPad', version: '2024.12.0', description: 'End-to-end encrypted documents, spreadsheets, and presentations. Zero-knowledge collaboration.', icon: '/assets/img/app-icons/cryptpad.webp', author: 'XWiki SAS', dockerImage: `${R}/cryptpad:2024.12.0`, repoUrl: 'https://github.com/cryptpad/cryptpad' },
|
||||
{ id: 'penpot', title: 'Penpot', version: '2.4', description: 'Open-source design platform. Self-hosted alternative to Figma for design and prototyping.', icon: '/assets/img/app-icons/penpot.webp', author: 'Penpot', dockerImage: `${R}/penpot-frontend:2.4`, repoUrl: 'https://github.com/penpot/penpot' },
|
||||
{ id: 'nextcloud', title: 'Nextcloud', version: '28', description: 'Your own private cloud. File sync, calendars, contacts — all on your hardware.', icon: '/assets/img/app-icons/nextcloud.webp', author: 'Nextcloud', dockerImage: `${R}/nextcloud:28`, repoUrl: 'https://github.com/nextcloud/server' },
|
||||
{ id: 'vaultwarden', title: 'Vaultwarden', version: '1.30.0', description: 'Self-hosted password vault. Bitwarden-compatible with zero-knowledge encryption.', icon: '/assets/img/app-icons/vaultwarden.webp', author: 'Vaultwarden', dockerImage: `${R}/vaultwarden:1.30.0-alpine`, repoUrl: 'https://github.com/dani-garcia/vaultwarden' },
|
||||
@@ -29,6 +29,9 @@ export function getCuratedAppList(): MarketplaceApp[] {
|
||||
{ id: 'nostr-rs-relay', title: 'Nostr Relay', version: '0.9.0', category: 'nostr', description: 'Your own Nostr relay. Store events locally, relay for friends, publish over Tor.', icon: '/assets/img/app-icons/nostr-rs-relay.svg', author: 'scsiblade', dockerImage: `${R}/nostr-rs-relay:0.9.0`, repoUrl: 'https://sr.ht/~gheartsfield/nostr-rs-relay/' },
|
||||
{ id: 'indeedhub', title: 'Indeehub', version: '0.1.0', description: 'Bitcoin documentary streaming with Nostr identity. Stream sovereignty content.', icon: '/assets/img/app-icons/indeedhub.png', author: 'Indeehub Team', dockerImage: 'localhost/indeedhub:latest', repoUrl: 'https://github.com/indeedhub/indeedhub' },
|
||||
{ id: 'dwn', title: 'Decentralized Web Node', version: '0.4.0', description: 'Own your data with DID-based access control. Sync across devices, sovereign.', icon: '/assets/img/app-icons/dwn.svg', author: 'TBD', dockerImage: `${R}/dwn-server:main`, repoUrl: 'https://github.com/TBD54566975/dwn-server' },
|
||||
{ id: 'nostr-vpn', title: 'Nostr VPN', version: '0.3.4', category: 'networking', description: 'Tailscale-style mesh VPN with Nostr control plane. Peer discovery and key exchange over relays, WireGuard tunnels.', icon: '/assets/img/app-icons/nostr-vpn.svg', author: 'Martti Malmi', dockerImage: `${R}/nostr-vpn:v0.3.4`, repoUrl: 'https://github.com/mmalmi/nostr-vpn' },
|
||||
{ id: 'fips', title: 'FIPS', version: '0.1.0', category: 'networking', description: 'Free Internetworking Peering System. Self-organizing encrypted mesh network with Nostr identity.', icon: '/assets/img/app-icons/fips.svg', author: 'Jim Corgan', dockerImage: `${R}/fips:v0.1.0`, repoUrl: 'https://github.com/jmcorgan/fips' },
|
||||
{ id: 'routstr', title: 'Routstr', version: '0.4.3', category: 'community', description: 'Decentralized AI inference proxy. Pay-per-request with Cashu ecash, provider discovery via Nostr.', icon: '/assets/img/app-icons/routstr.svg', author: 'Routstr', dockerImage: `${R}/routstr:v0.4.3`, repoUrl: 'https://github.com/routstr/routstr-core' },
|
||||
{ id: 'nostrudel', title: 'noStrudel', version: '0.40.0', category: 'nostr', description: 'Feature-rich Nostr web client. Browse feeds, post notes, manage relays with NIP-07.', icon: '/assets/img/app-icons/nostrudel.svg', author: 'hzrd149', dockerImage: '', repoUrl: 'https://github.com/hzrd149/nostrudel', webUrl: 'https://nostrudel.ninja' },
|
||||
{ id: 'botfights', title: 'BotFights', version: '1.0.0', description: 'AI bot arena — build, train, and battle autonomous agents in strategy tournaments.', icon: '/assets/img/app-icons/botfights.svg', author: 'BotFights', dockerImage: '', repoUrl: 'https://botfights.net', webUrl: 'https://botfights.net' },
|
||||
{ id: 'nwnn', title: 'Next Web News Network', version: '1.0.0', category: 'l484', description: 'Decentralized news aggregator. Community-curated Bitcoin and sovereignty content.', icon: '/assets/img/app-icons/nwnn.png', author: 'L484', dockerImage: '', repoUrl: 'https://nwnn.l484.com', webUrl: 'https://nwnn.l484.com' },
|
||||
@@ -58,6 +61,9 @@ export const INSTALLED_ALIASES: Record<string, string[]> = {
|
||||
filebrowser: ['filebrowser'],
|
||||
tailscale: ['tailscale'],
|
||||
ollama: ['ollama'],
|
||||
'nostr-vpn': ['nostr-vpn'],
|
||||
fips: ['fips'],
|
||||
routstr: ['routstr'],
|
||||
}
|
||||
|
||||
export const FEATURED_DEFINITIONS = [
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { shortDid } from './utils'
|
||||
import { safeClipboardWrite } from '../web5/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
selfDid: string
|
||||
@@ -59,7 +60,7 @@ const shortDidDisplay = computed(() => shortDid(props.selfDid))
|
||||
|
||||
function handleCopy() {
|
||||
if (props.selfDid) {
|
||||
navigator.clipboard.writeText(props.selfDid).catch(() => {})
|
||||
safeClipboardWrite(props.selfDid)
|
||||
didCopied.value = true
|
||||
setTimeout(() => { didCopied.value = false }, 2000)
|
||||
}
|
||||
|
||||
@@ -54,6 +54,9 @@ export const INSTALLED_ALIASES: Record<string, string[]> = {
|
||||
filebrowser: ['filebrowser'],
|
||||
tailscale: ['tailscale'],
|
||||
ollama: ['ollama'],
|
||||
'nostr-vpn': ['nostr-vpn'],
|
||||
fips: ['fips'],
|
||||
routstr: ['routstr'],
|
||||
}
|
||||
|
||||
/** Get app tier classification (matches backend get_app_tier) */
|
||||
@@ -227,15 +230,15 @@ export function getCuratedAppList(): MarketplaceApp[] {
|
||||
repoUrl: 'https://github.com/ollama/ollama'
|
||||
},
|
||||
{
|
||||
id: 'onlyoffice',
|
||||
title: 'OnlyOffice',
|
||||
version: '7.5.1',
|
||||
description: 'Office suite for document collaboration. Edit docs, spreadsheets, and presentations.',
|
||||
icon: '/assets/img/app-icons/onlyoffice.webp',
|
||||
author: 'Ascensio System SIA',
|
||||
dockerImage: `${REGISTRY}/onlyoffice:latest`,
|
||||
id: 'cryptpad',
|
||||
title: 'CryptPad',
|
||||
version: '2024.12.0',
|
||||
description: 'End-to-end encrypted documents, spreadsheets, and presentations. Zero-knowledge collaboration.',
|
||||
icon: '/assets/img/app-icons/cryptpad.webp',
|
||||
author: 'XWiki SAS',
|
||||
dockerImage: `${REGISTRY}/cryptpad:2024.12.0`,
|
||||
manifestUrl: undefined,
|
||||
repoUrl: 'https://github.com/ONLYOFFICE/DocumentServer'
|
||||
repoUrl: 'https://github.com/cryptpad/cryptpad'
|
||||
},
|
||||
{
|
||||
id: 'penpot',
|
||||
@@ -402,6 +405,42 @@ export function getCuratedAppList(): MarketplaceApp[] {
|
||||
manifestUrl: undefined,
|
||||
repoUrl: 'https://github.com/TBD54566975/dwn-server'
|
||||
},
|
||||
{
|
||||
id: 'nostr-vpn',
|
||||
title: 'Nostr VPN',
|
||||
version: '0.3.4',
|
||||
category: 'networking',
|
||||
description: 'Tailscale-style mesh VPN with Nostr control plane. Peer discovery and key exchange over relays, WireGuard tunnels.',
|
||||
icon: '/assets/img/app-icons/nostr-vpn.svg',
|
||||
author: 'Martti Malmi',
|
||||
dockerImage: `${REGISTRY}/nostr-vpn:v0.3.4`,
|
||||
manifestUrl: undefined,
|
||||
repoUrl: 'https://github.com/mmalmi/nostr-vpn'
|
||||
},
|
||||
{
|
||||
id: 'fips',
|
||||
title: 'FIPS',
|
||||
version: '0.1.0',
|
||||
category: 'networking',
|
||||
description: 'Free Internetworking Peering System. Self-organizing encrypted mesh network with Nostr identity.',
|
||||
icon: '/assets/img/app-icons/fips.svg',
|
||||
author: 'Jim Corgan',
|
||||
dockerImage: `${REGISTRY}/fips:v0.1.0`,
|
||||
manifestUrl: undefined,
|
||||
repoUrl: 'https://github.com/jmcorgan/fips'
|
||||
},
|
||||
{
|
||||
id: 'routstr',
|
||||
title: 'Routstr',
|
||||
version: '0.4.3',
|
||||
category: 'community',
|
||||
description: 'Decentralized AI inference proxy. Pay-per-request with Cashu ecash, provider discovery via Nostr.',
|
||||
icon: '/assets/img/app-icons/routstr.svg',
|
||||
author: 'Routstr',
|
||||
dockerImage: `${REGISTRY}/routstr:v0.4.3`,
|
||||
manifestUrl: undefined,
|
||||
repoUrl: 'https://github.com/routstr/routstr-core'
|
||||
},
|
||||
{
|
||||
id: 'nostrudel',
|
||||
title: 'noStrudel',
|
||||
|
||||
@@ -37,18 +37,21 @@ export function formatMessageTime(ts: string): string {
|
||||
}
|
||||
|
||||
export async function safeClipboardWrite(text: string): Promise<void> {
|
||||
if (navigator.clipboard?.writeText) {
|
||||
await navigator.clipboard.writeText(text)
|
||||
} else {
|
||||
const ta = document.createElement('textarea')
|
||||
ta.value = text
|
||||
ta.style.position = 'fixed'
|
||||
ta.style.opacity = '0'
|
||||
document.body.appendChild(ta)
|
||||
ta.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(ta)
|
||||
}
|
||||
// navigator.clipboard is unavailable on HTTP (non-secure contexts)
|
||||
try {
|
||||
if (navigator.clipboard?.writeText) {
|
||||
await navigator.clipboard.writeText(text)
|
||||
return
|
||||
}
|
||||
} catch { /* fall through to textarea fallback */ }
|
||||
const ta = document.createElement('textarea')
|
||||
ta.value = text
|
||||
ta.style.position = 'fixed'
|
||||
ta.style.opacity = '0'
|
||||
document.body.appendChild(ta)
|
||||
ta.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(ta)
|
||||
}
|
||||
|
||||
export function isMediaType(mime: string): boolean {
|
||||
|
||||
@@ -687,6 +687,9 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
m = data.get("model", "")
|
||||
if m in MODEL_MAP: data["model"] = MODEL_MAP[m]
|
||||
body = json.dumps(data).encode()
|
||||
if not API_KEY:
|
||||
err = json.dumps({"type":"error","error":{"type":"auth_error","message":"AIUI not configured. Set your Anthropic API key in Settings > AIUI to enable AI chat."}}).encode()
|
||||
self.send_response(401); self.send_header("Content-Type","application/json"); self.send_header("Content-Length",str(len(err))); self.end_headers(); self.wfile.write(err); return
|
||||
headers = {"Content-Type":"application/json","x-api-key":API_KEY,"anthropic-version":"2023-06-01","anthropic-dangerous-direct-browser-access":"true"}
|
||||
for h in ["anthropic-version","anthropic-beta"]:
|
||||
if self.headers.get(h): headers[h] = self.headers[h]
|
||||
@@ -716,7 +719,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
self.send_response(200); self.send_header("Content-Type","application/json"); self.end_headers(); self.wfile.write(b"{\"status\":\"ok\"}")
|
||||
else: self.send_response(404); self.end_headers()
|
||||
def log_message(self, fmt, *args): pass
|
||||
if not API_KEY: print("ERROR: ANTHROPIC_API_KEY not set"); sys.exit(1)
|
||||
if not API_KEY: print("WARNING: ANTHROPIC_API_KEY not set — AIUI will return setup instructions")
|
||||
server = http.server.HTTPServer(("127.0.0.1", PORT), Handler)
|
||||
print(f"Claude API proxy on port {PORT}")
|
||||
server.serve_forever()
|
||||
|
||||
@@ -400,10 +400,8 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -qE 'bitcoin-knots|arch
|
||||
-p 8332:8332 -p 8333:8333 -p 28332:28332 -p 28333:28333 \
|
||||
-v /var/lib/archipelago/bitcoin:/home/bitcoin/.bitcoin \
|
||||
"${BITCOIN_KNOTS_IMAGE}" \
|
||||
-server=1 $BTC_EXTRA_ARGS \
|
||||
-rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 \
|
||||
-proxy=host.containers.internal:9050 -listen=1 -bind=0.0.0.0:8333 \
|
||||
-dbcache=$BTC_DBCACHE 2>>"$LOG"; then
|
||||
$BTC_EXTRA_ARGS \
|
||||
-printtoconsole=1 -dbcache=$BTC_DBCACHE 2>>"$LOG"; then
|
||||
log "Bitcoin Knots started"
|
||||
else
|
||||
log "Bitcoin Knots failed (may already exist)"
|
||||
@@ -649,7 +647,7 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q archy-nbxplorer; the
|
||||
$DOCKER start archy-nbxplorer 2>/dev/null || true
|
||||
else
|
||||
log "Creating NBXplorer..."
|
||||
mkdir -p /var/lib/archipelago/nbxplorer
|
||||
mkdir -p /var/lib/archipelago/nbxplorer/Main
|
||||
$DOCKER run -d --name archy-nbxplorer --restart unless-stopped \
|
||||
--health-cmd="curl -sf http://localhost:32838/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
|
||||
--memory=$(mem_limit archy-nbxplorer) --network archy-net --network-alias archy-nbxplorer \
|
||||
@@ -667,7 +665,7 @@ track_container "archy-nbxplorer"
|
||||
|
||||
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q btcpay-server; then
|
||||
log "Creating BTCPay Server..."
|
||||
mkdir -p /var/lib/archipelago/btcpay
|
||||
mkdir -p /var/lib/archipelago/btcpay/Main
|
||||
$DOCKER run -d --name btcpay-server --restart unless-stopped \
|
||||
--health-cmd="curl -sf http://localhost:49392/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
|
||||
--memory=$(mem_limit btcpay-server) --network archy-net --network-alias btcpay-server \
|
||||
@@ -739,6 +737,7 @@ track_container "lnd"
|
||||
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q fedimint; then
|
||||
log "Creating Fedimint..."
|
||||
mkdir -p /var/lib/archipelago/fedimint
|
||||
chmod 775 /var/lib/archipelago/fedimint # fedimint container runs as non-root
|
||||
$DOCKER run -d --name fedimint --restart unless-stopped \
|
||||
--health-cmd="curl -sf http://localhost:8174/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
|
||||
--memory=$(mem_limit fedimint) --network archy-net --network-alias fedimint \
|
||||
@@ -949,6 +948,8 @@ track_container "searxng"
|
||||
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q filebrowser; then
|
||||
log "Creating File Browser..."
|
||||
mkdir -p /var/lib/archipelago/filebrowser /var/lib/archipelago/filebrowser-data
|
||||
# Pre-create default directories so FileBrowser doesn't 404 on first load
|
||||
mkdir -p /var/lib/archipelago/filebrowser/{Documents,Photos,Music,Downloads,Builds}
|
||||
$DOCKER run -d --name filebrowser --restart unless-stopped \
|
||||
--health-cmd="curl -sf http://localhost:80/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
|
||||
--memory=$(mem_limit filebrowser) \
|
||||
@@ -1092,41 +1093,35 @@ if [ -f "$RPC_PASS_FILE" ]; then
|
||||
done
|
||||
fi
|
||||
|
||||
for ui in bitcoin-ui lnd-ui; do
|
||||
for ui in bitcoin-ui lnd-ui electrs-ui; do
|
||||
if $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q "$ui"; then
|
||||
continue
|
||||
fi
|
||||
case $ui in
|
||||
# UI containers use --network host so they can proxy to localhost services
|
||||
# (Bitcoin RPC at 127.0.0.1:8332, backend at 127.0.0.1:5678)
|
||||
# Internal nginx ports: bitcoin-ui=8334, electrs-ui=50002, lnd-ui=8080 (host 8081)
|
||||
bitcoin-ui) PORT_ARG=""; NET_ARG="--network host" ;;
|
||||
lnd-ui) PORT_ARG="-p 8081:8080"; NET_ARG="" ;; # nginx inside listens on 8080 (no NET_BIND_SERVICE needed)
|
||||
electrs-ui) PORT_ARG=""; NET_ARG="--network host" ;;
|
||||
bitcoin-ui) PORT_ARG=""; NET_ARG="--network host"; REG_IMG="${BITCOIN_UI_IMAGE}" ;;
|
||||
lnd-ui) PORT_ARG="-p 8081:8080"; NET_ARG=""; REG_IMG="${LND_UI_IMAGE}" ;;
|
||||
electrs-ui) PORT_ARG=""; NET_ARG="--network host"; REG_IMG="${ELECTRS_UI_IMAGE}" ;;
|
||||
esac
|
||||
CONTAINER_NAME="archy-$ui"
|
||||
if $DOCKER images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep -q "$ui"; then
|
||||
log "Starting $ui from pre-built image..."
|
||||
UI_CAPS="--user 0:0 --cap-drop ALL --cap-add CHOWN --cap-add FOWNER --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE"
|
||||
|
||||
# Try registry image first, then local image, then build from source
|
||||
if [ -n "$REG_IMG" ] && $DOCKER pull --tls-verify=false "$REG_IMG" 2>>"$LOG"; then
|
||||
log "Starting $ui from registry ($REG_IMG)..."
|
||||
$DOCKER run -d --name "$CONTAINER_NAME" $PORT_ARG --restart unless-stopped --memory=$(mem_limit "$CONTAINER_NAME") $NET_ARG \
|
||||
$UI_CAPS "$REG_IMG" 2>>"$LOG" || true
|
||||
elif $DOCKER images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep -q "$ui"; then
|
||||
log "Starting $ui from local image..."
|
||||
IMG=$($DOCKER images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep "$ui" | head -1)
|
||||
$DOCKER run -d --name "$CONTAINER_NAME" $PORT_ARG --restart unless-stopped --memory=$(mem_limit "$CONTAINER_NAME") $NET_ARG \
|
||||
--user 0:0 \
|
||||
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \
|
||||
"$IMG" 2>>"$LOG" || true
|
||||
$UI_CAPS "$IMG" 2>>"$LOG" || true
|
||||
elif [ -d "/opt/archipelago/docker/$ui" ]; then
|
||||
log "Building $ui from source (/opt/archipelago/docker/$ui)..."
|
||||
log "Building $ui from source..."
|
||||
if $DOCKER build -t "$ui:local" "/opt/archipelago/docker/$ui" 2>>"$LOG"; then
|
||||
$DOCKER run -d --name "$CONTAINER_NAME" $PORT_ARG --restart unless-stopped --memory=$(mem_limit "$CONTAINER_NAME") $NET_ARG \
|
||||
--user 0:0 \
|
||||
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \
|
||||
"$ui:local" 2>>"$LOG" || true
|
||||
fi
|
||||
elif [ -d "/home/archipelago/archy/docker/$ui" ]; then
|
||||
log "Building $ui from source (/home/archipelago/archy/docker/$ui)..."
|
||||
if $DOCKER build -t "$ui:local" "/home/archipelago/archy/docker/$ui" 2>>"$LOG"; then
|
||||
$DOCKER run -d --name "$CONTAINER_NAME" $PORT_ARG --restart unless-stopped --memory=$(mem_limit "$CONTAINER_NAME") $NET_ARG \
|
||||
--user 0:0 \
|
||||
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \
|
||||
"$ui:local" 2>>"$LOG" || true
|
||||
$UI_CAPS "$ui:local" 2>>"$LOG" || true
|
||||
fi
|
||||
else
|
||||
log "$ui: no image or source found, skipping"
|
||||
|
||||
@@ -63,6 +63,11 @@ VALKEY_IMAGE="$ARCHY_REGISTRY/valkey:8.1.6"
|
||||
# Nostr
|
||||
NOSTR_RS_RELAY_IMAGE="$ARCHY_REGISTRY/nostr-rs-relay:0.9.0"
|
||||
STRFRY_IMAGE="$ARCHY_REGISTRY/strfry:1.0.4"
|
||||
NOSTR_VPN_IMAGE="$ARCHY_REGISTRY/nostr-vpn:v0.3.4"
|
||||
FIPS_IMAGE="$ARCHY_REGISTRY/fips:v0.1.0"
|
||||
|
||||
# AI / Routing
|
||||
ROUTSTR_IMAGE="$ARCHY_REGISTRY/routstr:v0.4.3"
|
||||
|
||||
# IndeedHub stack (local builds use :local tag, not :latest)
|
||||
MINIO_IMAGE="$ARCHY_REGISTRY/minio:RELEASE.2024-11-07T00-52-20Z"
|
||||
|
||||
Reference in New Issue
Block a user