From 7a05e11834e20b8dec639eb03ba991b27d9f92f7 Mon Sep 17 00:00:00 2001 From: Dorian Date: Sun, 1 Mar 2026 17:53:18 +0000 Subject: [PATCH] Refactor Indeehub integration and enhance deployment documentation - Updated Indeehub references throughout the codebase, changing the name from "IndeedHub" to "Indeehub" for consistency. - Implemented a virtual app structure for Indeehub, allowing it to open an external URL without requiring a container. - Enhanced deployment scripts and documentation to clarify SSH access and password management for Indeehub. - Improved error handling and retry logic in various components to ensure better user experience during onboarding and app interactions. - Updated CSS for visual enhancements and added new buttons for improved navigation in the AppLauncherOverlay. --- .cursor/rules/Development-Workflow.mdc | 18 +- apps/indeedhub/README.md | 44 +++++ apps/indeedhub/build-from-prototype.sh | 30 +++ apps/indeedhub/manifest.yml | 4 +- core/archipelago/src/api/rpc.rs | 49 ++++- .../src/container/docker_packages.rs | 63 ++++++- neode-ui/src/api/rpc-client.ts | 84 ++++++--- neode-ui/src/api/websocket.ts | 2 +- .../src/components/AppLauncherOverlay.vue | 17 ++ neode-ui/src/composables/useLoginSounds.ts | 18 +- neode-ui/src/composables/useOnboarding.ts | 30 ++- neode-ui/src/router/index.ts | 33 +++- neode-ui/src/stores/app.ts | 37 ++-- neode-ui/src/stores/appLauncher.ts | 44 ++++- neode-ui/src/style.css | 8 +- neode-ui/src/utils/dummyApps.ts | 10 +- neode-ui/src/views/AppDetails.vue | 4 +- neode-ui/src/views/Apps.vue | 4 +- neode-ui/src/views/Dashboard.vue | 10 +- neode-ui/src/views/Login.vue | 46 +++-- neode-ui/src/views/Marketplace.vue | 11 ++ neode-ui/src/views/OnboardingBackup.vue | 4 +- neode-ui/src/views/OnboardingDid.vue | 31 +-- neode-ui/src/views/OnboardingDone.vue | 2 +- neode-ui/src/views/OnboardingIntro.vue | 2 +- neode-ui/src/views/OnboardingOptions.vue | 8 +- neode-ui/src/views/OnboardingPath.vue | 7 +- neode-ui/src/views/OnboardingVerify.vue | 16 +- neode-ui/src/views/OnboardingWrapper.vue | 8 +- neode-ui/src/views/RootRedirect.vue | 15 +- scripts/deploy-to-target.sh | 7 +- scripts/nginx-https-app-proxies.conf | 178 ++++++++++++++++++ scripts/setup-https-dev.sh | 130 +++++++++++++ scripts/trust-archipelago-cert.sh | 66 +++++++ 34 files changed, 877 insertions(+), 163 deletions(-) create mode 100644 apps/indeedhub/README.md create mode 100755 apps/indeedhub/build-from-prototype.sh create mode 100644 scripts/nginx-https-app-proxies.conf create mode 100755 scripts/trust-archipelago-cert.sh diff --git a/.cursor/rules/Development-Workflow.mdc b/.cursor/rules/Development-Workflow.mdc index f8be59ba..d8099774 100644 --- a/.cursor/rules/Development-Workflow.mdc +++ b/.cursor/rules/Development-Workflow.mdc @@ -29,7 +29,8 @@ Do not leave deployment or testing to the user. The agent has SSH access to perf ### Backend: build on server via rsync (never on macOS) - **Always** deploy backend by: (1) rsync `core/` to `archipelago@192.168.1.228:~/archy/core/`, then (2) SSH and run `cargo build --release` on the server, then copy binary to `/usr/local/bin/` and restart `archipelago.service`. -- Use `sshpass -p "archipelago"` for non-interactive rsync/SSH. **Action these builds** when making backend changes; do not build the Rust binary on macOS and copy it (causes Exec format error on Linux). +- Use `sshpass -p 'EwPDR8q45l0Upx@'` for non-interactive rsync/SSH. The password is stored in `scripts/deploy-config.sh` (gitignored) and sourced by the deploy script automatically. +- **Do not** build the Rust binary on macOS and copy it (causes Exec format error on Linux). ### Standard Deployment Command @@ -61,13 +62,16 @@ Deploys to 192.168.1.228 first (builds there), then copies binary and web-ui to - **Web Server**: Nginx - **Backend**: Systemd service (`archipelago.service`) running as root -## SSH Key Management +## SSH Access -The deployment scripts require SSH key authentication. If you encounter `Permission denied` errors: +**Current credentials**: `archipelago@192.168.1.228` with password `EwPDR8q45l0Upx@` -1. Ensure SSH key is loaded: `ssh-add -l` -2. Add key if needed: `ssh-add ~/.ssh/id_ed25519` -3. Enter passphrase when prompted +The deploy script sources `scripts/deploy-config.sh` (gitignored) which sets `ARCHIPELAGO_PASSWORD`. For manual SSH/rsync commands, use: +```bash +sshpass -p 'EwPDR8q45l0Upx@' ssh -o StrictHostKeyChecking=no archipelago@192.168.1.228 +``` + +If `sshpass` hangs, SSH may be rate-limited from too many connections. Wait 10-15 seconds and retry. ## Development Paths @@ -136,7 +140,7 @@ Common containers: ### Debug a Fresh ISO Install 1. **Flash** the ISO to a test machine (e.g. 192.168.1.198) -2. **SSH** after first boot (same user/password as dev: `archipelago`/`archipelago`): +2. **SSH** after first boot (default ISO password is `archipelago`, dev server uses `EwPDR8q45l0Upx@`): ```bash ssh-keygen -R 192.168.1.198 # if host key changed after reflash sshpass -p "archipelago" ssh -o StrictHostKeyChecking=no archipelago@192.168.1.198 diff --git a/apps/indeedhub/README.md b/apps/indeedhub/README.md new file mode 100644 index 00000000..b08ebdab --- /dev/null +++ b/apps/indeedhub/README.md @@ -0,0 +1,44 @@ +# IndeedHub (Indeehub Prototype) + +Bitcoin documentary streaming platform featuring God Bless Bitcoin and other educational content about Bitcoin, sovereignty, and decentralized technology. + +## Building the Image + +The app image is built from the **Indeehub Prototype** project. The prototype lives at `../../Indeedhub Prototype` (relative to the archy repo). + +### Option 1: Build from prototype directory + +```bash +cd "/path/to/Indeedhub Prototype" +podman build -t localhost/indeedhub:latest . +``` + +### Option 2: Use the build script + +```bash +# From archy repo root +./apps/indeedhub/build-from-prototype.sh +``` + +### Option 3: Full deploy (build + run on server) + +```bash +cd "/path/to/Indeedhub Prototype" +./deploy-to-archipelago.sh +``` + +## Installing from My Apps + +1. **Build the image** using one of the options above (the image must exist before install) +2. Go to **Dashboard β†’ App Store** (Marketplace) +3. Find **Indeehub Prototype** and click **Install** +4. The app will appear in **My Apps** once the container is running + +## Port + +- Web UI: 7777 + +## Container + +- Image: `localhost/indeedhub:latest` (built locally, not pulled from a registry) +- Port: 7777 diff --git a/apps/indeedhub/build-from-prototype.sh b/apps/indeedhub/build-from-prototype.sh new file mode 100755 index 00000000..258a09fe --- /dev/null +++ b/apps/indeedhub/build-from-prototype.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# Build Indeehub image from the Indeehub Prototype project +# Usage: ./build-from-prototype.sh [path-to-prototype] + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DEFAULT_PROTOTYPE="$SCRIPT_DIR/../../Indeedhub Prototype" +PROTOTYPE_DIR="${1:-$DEFAULT_PROTOTYPE}" +IMAGE_TAG="localhost/indeedhub:latest" + +if [ ! -d "$PROTOTYPE_DIR" ]; then + echo "❌ Indeehub Prototype not found at: $PROTOTYPE_DIR" + echo " Set path: $0 /path/to/Indeedhub\ Prototype" + exit 1 +fi + +# Determine container runtime +RUNTIME="podman" +if ! command -v podman >/dev/null 2>&1; then + RUNTIME="docker" +fi + +echo "πŸ”¨ Building Indeehub from $PROTOTYPE_DIR" +cd "$PROTOTYPE_DIR" +$RUNTIME build -t "$IMAGE_TAG" . + +echo "βœ… Built $IMAGE_TAG" +echo "" +echo "You can now install Indeehub from the App Store in Archipelago." diff --git a/apps/indeedhub/manifest.yml b/apps/indeedhub/manifest.yml index dd466666..69abc5c8 100644 --- a/apps/indeedhub/manifest.yml +++ b/apps/indeedhub/manifest.yml @@ -1,6 +1,6 @@ app: id: indeedhub - name: IndeedHub + name: Indeehub version: 0.1.0 description: Bitcoin documentary streaming platform featuring God Bless Bitcoin and other educational content about Bitcoin, sovereignty, and decentralized technology. category: media @@ -59,7 +59,7 @@ app: path: / metadata: - author: IndeedHub Team + author: Indeehub Team website: https://indeedhub.com source: https://github.com/indeedhub/indeedhub license: MIT diff --git a/core/archipelago/src/api/rpc.rs b/core/archipelago/src/api/rpc.rs index f76eb966..e6513f64 100644 --- a/core/archipelago/src/api/rpc.rs +++ b/core/archipelago/src/api/rpc.rs @@ -618,6 +618,11 @@ impl RpcHandler { return Err(anyhow::anyhow!("Invalid Docker image format")); } + // Virtual app: Indeehub (no container, opens external URL) + if package_id == "indeedhub" { + return Ok(serde_json::json!({ "success": true })); + } + // Multi-container apps: create full stack if package_id == "immich" { return self.install_immich_stack().await; @@ -637,17 +642,34 @@ impl RpcHandler { return Err(anyhow::anyhow!("Container {} already exists. Stop and remove it first.", package_id)); } - // Pull the image (with verification in the future) - debug!("Pulling image: {}", docker_image); - let pull_output = tokio::process::Command::new("sudo") - .args(["podman", "pull", docker_image]) - .output() - .await - .context("Failed to pull image")?; + // Pull the image (skip for local images - must be built locally first) + let is_local_image = docker_image.starts_with("localhost/"); + if !is_local_image { + debug!("Pulling image: {}", docker_image); + let pull_output = tokio::process::Command::new("sudo") + .args(["podman", "pull", docker_image]) + .output() + .await + .context("Failed to pull image")?; - if !pull_output.status.success() { - let stderr = String::from_utf8_lossy(&pull_output.stderr); - return Err(anyhow::anyhow!("Failed to pull image: {}", stderr)); + if !pull_output.status.success() { + let stderr = String::from_utf8_lossy(&pull_output.stderr); + return Err(anyhow::anyhow!("Failed to pull image: {}", stderr)); + } + } else { + // Verify local image exists + let images_output = tokio::process::Command::new("sudo") + .args(["podman", "images", "-q", docker_image]) + .output() + .await + .context("Failed to check local image")?; + if String::from_utf8_lossy(&images_output.stdout).trim().is_empty() { + return Err(anyhow::anyhow!( + "Local image {} not found. Run ./deploy-to-archipelago.sh from the Indeehub Prototype project on your Macβ€”it builds the image on this server and starts the app.", + docker_image + )); + } + debug!("Using local image: {}", docker_image); } // Create and start container with security constraints @@ -1708,6 +1730,13 @@ fn get_app_config( None, None, ), + "indeedhub" => ( + vec!["7777:7777".to_string()], + vec![], + vec!["NGINX_HOST=0.0.0.0".to_string(), "NGINX_PORT=7777".to_string()], + None, + None, + ), _ => (vec![], vec![], vec![], None, None), // No default config, user must configure manually } } diff --git a/core/archipelago/src/container/docker_packages.rs b/core/archipelago/src/container/docker_packages.rs index aa540af4..b365a1c9 100644 --- a/core/archipelago/src/container/docker_packages.rs +++ b/core/archipelago/src/container/docker_packages.rs @@ -202,6 +202,65 @@ impl DockerPackageScanner { info!("Detected container: {} ({})", metadata.title, package_state_str(&package_state)); } + // Virtual app: Indeehub (opens external URL, no container required) + if !packages.contains_key("indeedhub") { + let metadata = get_app_metadata("indeedhub"); + let lan_address = Some("https://archipelago.indeehub.studio".to_string()); + let virtual_pkg = PackageDataEntry { + state: PackageState::Running, + static_files: StaticFiles { + license: "MIT".to_string(), + instructions: metadata.description.clone(), + icon: metadata.icon.clone(), + }, + manifest: Manifest { + id: "indeedhub".to_string(), + title: metadata.title.clone(), + version: "0.1.0".to_string(), + description: Description { + short: metadata.description.clone(), + long: metadata.description.clone(), + }, + release_notes: "Virtual app (opens archipelago.indeehub.studio)".to_string(), + license: "MIT".to_string(), + wrapper_repo: metadata.repo.clone(), + upstream_repo: metadata.repo.clone(), + support_site: metadata.repo.clone(), + marketing_site: metadata.repo.clone(), + donation_url: None, + author: Some("Indeehub Team".to_string()), + website: lan_address.clone(), + interfaces: Some(Interfaces { + main: Some(MainInterface { + ui: Some("true".to_string()), + tor_config: None, + lan_config: None, + }), + }), + }, + installed: Some(InstalledPackageDataEntry { + current_dependents: HashMap::new(), + current_dependencies: HashMap::new(), + last_backup: None, + interface_addresses: { + let mut addresses = HashMap::new(); + addresses.insert( + "main".to_string(), + InterfaceAddress { + tor_address: String::new(), + lan_address: lan_address, + }, + ); + addresses + }, + status: ServiceStatus::Running, + }), + install_progress: None, + }; + packages.insert("indeedhub".to_string(), virtual_pkg); + info!("Virtual app: Indeehub (archipelago.indeehub.studio)"); + } + Ok(packages) } } @@ -360,9 +419,9 @@ fn get_app_metadata(app_id: &str) -> AppMetadata { repo: "https://github.com/tailscale/tailscale".to_string(), }, "indeedhub" => AppMetadata { - title: "IndeedHub".to_string(), + title: "Indeehub".to_string(), description: "Decentralized media streaming platform".to_string(), - icon: "/assets/img/app-icons/indeedhub.png".to_string(), + icon: "https://indeehub.studio/favicon.ico".to_string(), repo: "https://github.com/indeedhub/indeedhub".to_string(), }, _ => AppMetadata { diff --git a/neode-ui/src/api/rpc-client.ts b/neode-ui/src/api/rpc-client.ts index 7afc4570..67b40bba 100644 --- a/neode-ui/src/api/rpc-client.ts +++ b/neode-ui/src/api/rpc-client.ts @@ -24,44 +24,66 @@ class RPCClient { async call(options: RPCOptions): Promise { const { method, params = {}, timeout = 30000 } = options + const maxRetries = 3 - const controller = new AbortController() - const timeoutId = setTimeout(() => controller.abort(), timeout) + for (let attempt = 0; attempt < maxRetries; attempt++) { + const controller = new AbortController() + const timeoutId = setTimeout(() => controller.abort(), timeout) - try { - const response = await fetch(this.baseUrl, { - method: 'POST', - credentials: 'include', // Important for session cookies - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ method, params }), - signal: controller.signal, - }) + try { + const response = await fetch(this.baseUrl, { + method: 'POST', + credentials: 'include', // Important for session cookies + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ method, params }), + signal: controller.signal, + }) - clearTimeout(timeoutId) + clearTimeout(timeoutId) - if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`) - } - - const data: RPCResponse = await response.json() - - if (data.error) { - throw new Error(data.error.message || 'RPC Error') - } - - return data.result as T - } catch (error) { - clearTimeout(timeoutId) - if (error instanceof Error) { - if (error.name === 'AbortError') { - throw new Error('Request timeout') + if (!response.ok) { + const err = new Error(`HTTP ${response.status}: ${response.statusText}`) + const isRetryable = response.status === 502 || response.status === 503 + if (isRetryable && attempt < maxRetries - 1) { + await new Promise((r) => setTimeout(r, 600 * (attempt + 1))) + continue + } + throw err } - throw error + + const data: RPCResponse = await response.json() + + if (data.error) { + throw new Error(data.error.message || 'RPC Error') + } + + return data.result as T + } catch (error) { + clearTimeout(timeoutId) + if (error instanceof Error) { + if (error.name === 'AbortError') { + const timeoutErr = new Error('Request timeout') + if (attempt < maxRetries - 1) { + await new Promise((r) => setTimeout(r, 600 * (attempt + 1))) + continue + } + throw timeoutErr + } + const msg = error.message + const isRetryable = /502|503|Bad Gateway|fetch|network/i.test(msg) + if (isRetryable && attempt < maxRetries - 1) { + await new Promise((r) => setTimeout(r, 600 * (attempt + 1))) + continue + } + throw error + } + throw new Error('Unknown error occurred') } - throw new Error('Unknown error occurred') } + + throw new Error('Request failed after retries') } // Convenience methods diff --git a/neode-ui/src/api/websocket.ts b/neode-ui/src/api/websocket.ts index 24bcd5d5..82bc34c8 100644 --- a/neode-ui/src/api/websocket.ts +++ b/neode-ui/src/api/websocket.ts @@ -352,7 +352,7 @@ function getWebSocketClient(): WebSocketClient { if (typeof window !== 'undefined') { (window as any).__archipelago_ws_client = wsClientInstance } - console.log('[WebSocket] Created new client instance') + if (import.meta.env.DEV) console.debug('[WebSocket] Created new client instance') } return wsClientInstance diff --git a/neode-ui/src/components/AppLauncherOverlay.vue b/neode-ui/src/components/AppLauncherOverlay.vue index 3d6d4f22..c2fe5b95 100644 --- a/neode-ui/src/components/AppLauncherOverlay.vue +++ b/neode-ui/src/components/AppLauncherOverlay.vue @@ -39,6 +39,17 @@ +