Add LoraBell app support and enhance package management in mock backend

- Introduced LoraBell as a static demo app in the mock backend, preventing its uninstallation.
- Merged static dev apps with Docker container data for improved package management.
- Updated app details and URLs for LoraBell in the Apps and AppDetails views.
- Enhanced the dummyApps utility to include LoraBell's configuration for consistent app representation.
This commit is contained in:
Dorian 2026-02-18 08:30:12 +00:00
parent 2472af790b
commit 3da0d53952
8 changed files with 190 additions and 8 deletions

36
DEMO-DEPLOY.md Normal file
View File

@ -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)

26
docker-compose.demo.yml Normal file
View File

@ -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

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

View File

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>LoraBell</title>
<style>
body { background: #000; color: #fff; font-family: system-ui, sans-serif; padding: 2rem; max-width: 32rem; margin: 0 auto; }
h1 { font-size: 1.5rem; font-weight: 600; margin-bottom: 1rem; }
p { color: rgba(255,255,255,0.8); margin-bottom: 0.5rem; }
.muted { color: rgba(255,255,255,0.6); font-size: 0.875rem; }
</style>
</head>
<body>
<h1>LoraBell</h1>
<p>A LoRa based doorbell</p>
<p class="muted">This device has no web interface. It operates over LoRa radio and sends doorbell notifications to your node.</p>
</body>
</html>

View File

@ -114,6 +114,42 @@ export const dummyApps: Record<string, PackageDataEntry> = {
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': {

View File

@ -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<string, { dev: string, prod: string }> = {
'lorabell': {
dev: '/lorabell-info.html',
prod: '/lorabell-info.html'
},
'atob': {
dev: 'http://localhost:8102',
prod: 'https://app.atobitcoin.io'

View File

@ -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<string, { dev: string, prod: string }> = {
'lorabell': {
dev: '/lorabell-info.html',
prod: '/lorabell-info.html'
},
'atob': {
dev: 'http://localhost:8102',
prod: 'https://app.atobitcoin.io'