diff --git a/DEMO-DEPLOY.md b/DEMO-DEPLOY.md new file mode 100644 index 00000000..1b88a53d --- /dev/null +++ b/DEMO-DEPLOY.md @@ -0,0 +1,36 @@ +# Demo Deployment via Portainer + +Deploy Archipelago with the **mock backend** for demos. No real node required. + +## Quick Deploy (Portainer) + +1. In Portainer: **Stacks** → **Add stack** +2. Name: `archy-demo` +3. **Web editor** → paste contents of `docker-compose.demo.yml` +4. Or **Build from repository**: use this repo URL and set Compose path to `docker-compose.demo.yml` +5. Deploy + +**Access:** http://your-host:4848 + +## Mock Backend + +- Uses the Node.js mock backend (not the Rust backend) +- Pre-loaded apps, fake data, simulated install/start/stop +- **Login password:** `password123` + +## Port + +Default: **4848**. To change, edit the ports mapping in `docker-compose.demo.yml`: + +```yaml +ports: + - "YOUR_PORT:80" +``` + +## Dev Mode + +`VITE_DEV_MODE=existing` skips setup/onboarding and goes straight to login. For other flows: + +- `setup` – Password setup screen first +- `onboarding` – Experimental onboarding flow +- `existing` – Login only (default for demo) diff --git a/docker-compose.demo.yml b/docker-compose.demo.yml new file mode 100644 index 00000000..383c682f --- /dev/null +++ b/docker-compose.demo.yml @@ -0,0 +1,26 @@ +# Archipelago Demo Stack - Mock backend + Vue UI +# Deploy via Portainer: Web editor → paste this, or deploy from repo +# Access at http://localhost:4848 (or your host:4848) + +services: + neode-backend: + build: + context: . + dockerfile: neode-ui/Dockerfile.backend + container_name: archy-demo-backend + environment: + VITE_DEV_MODE: "existing" # Skip setup/onboarding, go straight to login + expose: + - "5959" + restart: unless-stopped + + neode-web: + build: + context: . + dockerfile: neode-ui/Dockerfile.web + container_name: archy-demo-web + ports: + - "4848:80" + depends_on: + - neode-backend + restart: unless-stopped diff --git a/neode-ui/mock-backend.js b/neode-ui/mock-backend.js index 07e7c3c1..f3703e60 100755 --- a/neode-ui/mock-backend.js +++ b/neode-ui/mock-backend.js @@ -484,6 +484,9 @@ async function uninstallPackage(id) { console.log(`[Package] 🗑️ Uninstalling ${id}...`) try { + if (staticDevApps[id]) { + throw new Error(`${id} is a demo app and cannot be uninstalled`) + } if (!mockData['package-data'][id]) { throw new Error(`Package ${id} is not installed`) } @@ -566,20 +569,74 @@ const mockData = { }, } +// Static dev apps (always shown in My Apps when using mock backend) +const staticDevApps = { + 'lorabell': { + title: 'LoraBell', + version: '1.0.0', + status: 'running', + state: 'running', + 'static-files': { + license: 'MIT', + instructions: 'A LoRa based doorbell', + icon: '/assets/img/app-icons/lorabell.png' + }, + manifest: { + id: 'lorabell', + title: 'LoraBell', + version: '1.0.0', + description: { + short: 'A LoRa based doorbell', + long: 'A LoRa based doorbell - receive doorbell notifications over LoRa radio.' + }, + 'release-notes': 'Initial release', + license: 'MIT', + 'wrapper-repo': '#', + 'upstream-repo': '#', + 'support-site': '#', + 'marketing-site': '#', + 'donation-url': null, + interfaces: { + main: { + name: 'Web Interface', + description: 'LoraBell web interface', + ui: true + } + } + }, + installed: { + 'current-dependents': {}, + 'current-dependencies': {}, + 'last-backup': null, + 'interface-addresses': { + main: { + 'tor-address': 'lorabell.onion', + 'lan-address': '/lorabell-info.html' + } + }, + status: 'running' + } + } +} + +function mergePackageData(dockerApps) { + return { ...dockerApps, ...staticDevApps } +} + // Initialize package data from Docker on startup async function initializePackageData() { console.log('[Docker] Querying running containers...') const dockerApps = await getDockerContainers() - mockData['package-data'] = dockerApps + mockData['package-data'] = mergePackageData(dockerApps) - const appCount = Object.keys(dockerApps).length - const runningCount = Object.values(dockerApps).filter(app => app.state === 'running').length + const appCount = Object.keys(mockData['package-data']).length + const runningCount = Object.values(mockData['package-data']).filter(app => app.state === 'running').length console.log(`[Docker] Found ${appCount} containers (${runningCount} running)`) if (appCount > 0) { console.log('[Docker] Apps detected:') - Object.entries(dockerApps).forEach(([id, app]) => { + Object.entries(mockData['package-data']).forEach(([id, app]) => { const port = app.installed?.['interface-addresses']?.main?.['lan-address'] console.log(` - ${app.title} (${app.state})${port ? ` → ${port}` : ''}`) }) @@ -884,17 +941,17 @@ server.listen(PORT, '0.0.0.0', async () => { `) console.log('Mock backend is running. Press Ctrl+C to stop.\n') - // Periodically update package data from Docker + // Periodically update package data from Docker (merge with static dev apps) setInterval(async () => { const dockerApps = await getDockerContainers() - mockData['package-data'] = dockerApps + mockData['package-data'] = mergePackageData(dockerApps) // Broadcast update to connected clients broadcastUpdate([ { op: 'replace', path: '/package-data', - value: dockerApps + value: mockData['package-data'] } ]) }, 5000) // Update every 5 seconds diff --git a/neode-ui/public/assets/img/app-icons/lorabell.png b/neode-ui/public/assets/img/app-icons/lorabell.png new file mode 100644 index 00000000..c8d9f8bc Binary files /dev/null and b/neode-ui/public/assets/img/app-icons/lorabell.png differ diff --git a/neode-ui/public/lorabell-info.html b/neode-ui/public/lorabell-info.html new file mode 100644 index 00000000..acf77fb2 --- /dev/null +++ b/neode-ui/public/lorabell-info.html @@ -0,0 +1,19 @@ + + + + + + LoraBell + + + +

LoraBell

+

A LoRa based doorbell

+

This device has no web interface. It operates over LoRa radio and sends doorbell notifications to your node.

+ + diff --git a/neode-ui/src/utils/dummyApps.ts b/neode-ui/src/utils/dummyApps.ts index eabd5692..4cc4a157 100644 --- a/neode-ui/src/utils/dummyApps.ts +++ b/neode-ui/src/utils/dummyApps.ts @@ -114,6 +114,42 @@ export const dummyApps: Record = { status: ServiceStatus.Running } }, + 'lorabell': { + state: PackageState.Running, + 'static-files': { + license: 'MIT', + instructions: 'A LoRa based doorbell', + icon: '/assets/img/app-icons/lorabell.png' + }, + manifest: { + id: 'lorabell', + title: 'LoraBell', + version: '1.0.0', + description: { + short: 'A LoRa based doorbell', + long: 'A LoRa based doorbell - receive doorbell notifications over LoRa radio.' + }, + 'release-notes': 'Initial release', + license: 'MIT', + 'wrapper-repo': '#', + 'upstream-repo': '#', + 'support-site': '#', + 'marketing-site': '#', + 'donation-url': null + }, + installed: { + 'current-dependents': {}, + 'current-dependencies': {}, + 'last-backup': null, + 'interface-addresses': { + main: { + 'tor-address': 'lorabell.onion', + 'lan-address': '/lorabell-info.html' + } + }, + status: ServiceStatus.Running + } + }, 'grafana': { state: PackageState.Running, 'static-files': { diff --git a/neode-ui/src/views/AppDetails.vue b/neode-ui/src/views/AppDetails.vue index 7a23d93f..46151334 100644 --- a/neode-ui/src/views/AppDetails.vue +++ b/neode-ui/src/views/AppDetails.vue @@ -613,6 +613,10 @@ function launchApp() { // Special handling for apps with Docker containers // TODO: Replace dummy app URLs with real URLs when apps are packaged const appUrls: Record = { + 'lorabell': { + dev: '/lorabell-info.html', + prod: '/lorabell-info.html' + }, 'atob': { dev: 'http://localhost:8102', prod: 'https://app.atobitcoin.io' diff --git a/neode-ui/src/views/Apps.vue b/neode-ui/src/views/Apps.vue index a97e92f2..cdf7f606 100644 --- a/neode-ui/src/views/Apps.vue +++ b/neode-ui/src/views/Apps.vue @@ -244,8 +244,12 @@ function launchApp(id: string) { return } - // Fallback: Special handling for apps with Docker containers + // Fallback: Special handling for apps with Docker containers / no-web-interface devices const appUrls: Record = { + 'lorabell': { + dev: '/lorabell-info.html', + prod: '/lorabell-info.html' + }, 'atob': { dev: 'http://localhost:8102', prod: 'https://app.atobitcoin.io'