Integrate Docker support into Archipelago and Neode UI

- Added StateManager and data_model modules to manage application state.
- Updated ApiHandler to utilize StateManager for WebSocket connections.
- Enhanced Server initialization to include StateManager.
- Implemented Docker container querying in Neode UI to populate app data dynamically.
- Removed temporary dummy app configurations in favor of real Docker-based applications.
- Improved WebSocket reconnection logic and error handling in the UI.
- Updated package.json and package-lock.json to include dockerode dependency.
This commit is contained in:
Dorian
2026-01-27 23:06:18 +00:00
parent 7afefafec1
commit 3b3f70276f
15 changed files with 1318 additions and 329 deletions
+208 -18
View File
@@ -16,11 +16,13 @@ import { promisify } from 'util'
import fs from 'fs/promises'
import path from 'path'
import { fileURLToPath } from 'url'
import Docker from 'dockerode'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const execPromise = promisify(exec)
const docker = new Docker()
const app = express()
const PORT = 5959
@@ -122,6 +124,170 @@ const portMappings = {
'amin': 8104
}
// Helper: Query real Docker containers
async function getDockerContainers() {
try {
const containers = await docker.listContainers({ all: true })
// Map of container names to app IDs
const containerMapping = {
'archy-bitcoin': 'bitcoin',
'archy-btcpay': 'btcpay-server',
'archy-homeassistant': 'homeassistant',
'archy-grafana': 'grafana',
'archy-endurain': 'endurain',
'archy-fedimint': 'fedimint',
'archy-morphos': 'morphos-server',
'archy-lnd': 'lightning-stack',
'archy-mempool-web': 'mempool',
'archy-ollama': 'ollama',
'archy-searxng': 'searxng',
'archy-onlyoffice': 'onlyoffice',
'archy-penpot-frontend': 'penpot'
}
const apps = {}
for (const container of containers) {
const name = container.Names[0].replace(/^\//, '')
const appId = containerMapping[name]
if (!appId) continue
const isRunning = container.State === 'running'
const ports = container.Ports || []
const hostPort = ports.find(p => p.PublicPort)?.PublicPort || null
// Get app metadata
const appMetadata = {
'bitcoin': {
title: 'Bitcoin Core',
icon: '/assets/img/app-icons/bitcoin.svg',
description: 'Full Bitcoin node implementation'
},
'btcpay-server': {
title: 'BTCPay Server',
icon: '/assets/img/app-icons/btcpay-server.png',
description: 'Self-hosted Bitcoin payment processor'
},
'homeassistant': {
title: 'Home Assistant',
icon: '/assets/img/app-icons/homeassistant.png',
description: 'Open source home automation platform'
},
'grafana': {
title: 'Grafana',
icon: '/assets/img/grafana.png',
description: 'Analytics and monitoring platform'
},
'endurain': {
title: 'Endurain',
icon: '/assets/img/endurain.png',
description: 'Application platform'
},
'fedimint': {
title: 'Fedimint',
icon: '/assets/img/icon-fedimint.jpeg',
description: 'Federated Bitcoin mint'
},
'morphos-server': {
title: 'MorphOS Server',
icon: '/assets/img/morphos.png',
description: 'Server platform'
},
'lightning-stack': {
title: 'Lightning Stack',
icon: '/assets/img/app-icons/lightning-stack.png',
description: 'Lightning Network (LND)'
},
'mempool': {
title: 'Mempool',
icon: '/assets/img/app-icons/mempool.png',
description: 'Bitcoin blockchain explorer'
},
'ollama': {
title: 'Ollama',
icon: '/assets/img/ollama.webp',
description: 'Run large language models locally'
},
'searxng': {
title: 'SearXNG',
icon: '/assets/img/app-icons/searxng.png',
description: 'Privacy-respecting metasearch engine'
},
'onlyoffice': {
title: 'OnlyOffice',
icon: '/assets/img/onlyoffice.webp',
description: 'Office suite and document collaboration'
},
'penpot': {
title: 'Penpot',
icon: '/assets/img/penpot.webp',
description: 'Open-source design and prototyping'
}
}
const metadata = appMetadata[appId] || {
title: appId,
icon: '/assets/img/favico.png',
description: `${appId} application`
}
apps[appId] = {
title: metadata.title,
version: '1.0.0',
status: isRunning ? 'running' : 'stopped',
state: isRunning ? 'running' : 'stopped',
'static-files': {
license: 'MIT',
instructions: metadata.description,
icon: metadata.icon
},
manifest: {
id: appId,
title: metadata.title,
version: '1.0.0',
description: {
short: metadata.description,
long: metadata.description
},
'release-notes': 'Initial release',
license: 'MIT',
'wrapper-repo': '#',
'upstream-repo': '#',
'support-site': '#',
'marketing-site': '#',
'donation-url': null,
interfaces: hostPort ? {
main: {
name: 'Web Interface',
description: `${metadata.title} web interface`,
ui: true
}
} : {}
},
installed: {
'current-dependents': {},
'current-dependencies': {},
'last-backup': null,
'interface-addresses': hostPort ? {
main: {
'tor-address': `${appId}.onion`,
'lan-address': `http://localhost:${hostPort}`
}
} : {},
status: isRunning ? 'running' : 'stopped'
}
}
}
return apps
} catch (error) {
console.error('[Docker] Error querying containers:', error.message)
return {}
}
}
// Helper: Check if Docker/Podman is available
async function isContainerRuntimeAvailable() {
try {
@@ -382,24 +548,7 @@ const mockData = {
'wifi-ssids': [],
'zram-enabled': false,
},
'package-data': {
'bitcoin': {
title: 'Bitcoin Core',
version: '24.0.0',
status: 'running',
state: 'running',
manifest: {
id: 'bitcoin',
title: 'Bitcoin Core',
version: '24.0.0',
description: {
short: 'A full Bitcoin node',
long: 'Store, validate, and relay blocks and transactions on the Bitcoin network.',
},
icon: '/assets/img/bitcoin.svg',
},
},
},
'package-data': {}, // Will be populated from Docker
ui: {
name: 'Archipelago',
'ack-welcome': '0.1.0',
@@ -411,6 +560,28 @@ const mockData = {
},
}
// Initialize package data from Docker on startup
async function initializePackageData() {
console.log('[Docker] Querying running containers...')
const dockerApps = await getDockerContainers()
mockData['package-data'] = dockerApps
const appCount = Object.keys(dockerApps).length
const runningCount = Object.values(dockerApps).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]) => {
const port = app.installed?.['interface-addresses']?.main?.['lan-address']
console.log(` - ${app.title} (${app.state})${port ? `${port}` : ''}`)
})
} else {
console.log('[Docker] No containers found. Start docker-compose to see apps.')
}
}
// Handle CORS preflight
app.options('/rpc/v1', (req, res) => {
res.status(200).end()
@@ -671,6 +842,9 @@ wss.on('connection', (ws, req) => {
server.listen(PORT, '0.0.0.0', async () => {
const runtime = await isContainerRuntimeAvailable()
// Initialize package data from Docker
await initializePackageData()
console.log(`
╔════════════════════════════════════════════════════════════╗
║ ║
@@ -686,10 +860,26 @@ server.listen(PORT, '0.0.0.0', async () => {
║ Mock Password: ${MOCK_PASSWORD.padEnd(40)}
║ ║
║ Container Runtime: ${runtime.available ? `${runtime.runtime}`.padEnd(40) : '❌ Not available'.padEnd(40)}
║ Docker API: ✅ Connected ║
║ ║
╚════════════════════════════════════════════════════════════╝
`)
console.log('Mock backend is running. Press Ctrl+C to stop.\n')
// Periodically update package data from Docker
setInterval(async () => {
const dockerApps = await getDockerContainers()
mockData['package-data'] = dockerApps
// Broadcast update to connected clients
broadcastUpdate([
{
op: 'replace',
path: '/package-data',
value: dockerApps
}
])
}, 5000) // Update every 5 seconds
})
process.on('SIGINT', () => {