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:
+208
-18
@@ -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', () => {
|
||||
|
||||
Reference in New Issue
Block a user