archy/neode-ui/mock-backend.js
Dorian 3da0d53952 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.
2026-02-18 08:30:12 +00:00

967 lines
28 KiB
JavaScript
Executable File
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
/**
* Archipelago Mock Backend Server
* Pure Archipelago implementation - NO StartOS dependencies
* Supports dev modes: setup, onboarding, existing
*/
import express from 'express'
import cors from 'cors'
import cookieParser from 'cookie-parser'
import { WebSocketServer } from 'ws'
import http from 'http'
import { exec } from 'child_process'
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
// Dev mode from environment (setup, onboarding, existing, or default)
const DEV_MODE = process.env.VITE_DEV_MODE || 'default'
// CORS configuration
const corsOptions = {
credentials: true,
origin: (origin, callback) => {
if (!origin) return callback(null, true)
callback(null, true)
}
}
app.use(cors(corsOptions))
app.use(express.json())
app.use(cookieParser())
// Mock session storage
const sessions = new Map()
const MOCK_PASSWORD = 'password123'
// User state (simulated file-based storage)
let userState = {
setupComplete: false,
onboardingComplete: false,
passwordHash: null, // In real app, this would be bcrypt hash
}
// Initialize user state based on dev mode
function initializeUserState() {
switch (DEV_MODE) {
case 'setup':
// Setup mode: Original StartOS node setup - user needs to set password
// This is the simple password setup, NOT the experimental onboarding
userState = {
setupComplete: false, // User hasn't set password yet
onboardingComplete: false, // Onboarding not relevant for setup mode
passwordHash: null,
}
break
case 'onboarding':
// Onboarding mode: Experimental onboarding flow
// User has set password (via setup) but needs to go through experimental onboarding
userState = {
setupComplete: true, // Password already set
onboardingComplete: false, // Needs experimental onboarding
passwordHash: MOCK_PASSWORD,
}
break
case 'existing':
// Existing user: Fully set up, just needs to login
userState = {
setupComplete: true,
onboardingComplete: true,
passwordHash: MOCK_PASSWORD,
}
break
default:
// Default: Fully set up (for UI development)
userState = {
setupComplete: true,
onboardingComplete: true,
passwordHash: MOCK_PASSWORD,
}
}
console.log(`[Auth] Dev mode: ${DEV_MODE}`)
console.log(`[Auth] Setup: ${userState.setupComplete}, Onboarding: ${userState.onboardingComplete}`)
}
initializeUserState()
// WebSocket clients for broadcasting updates
const wsClients = new Set()
// Helper: Broadcast data update to all WebSocket clients
function broadcastUpdate(patch) {
const message = JSON.stringify({
rev: Date.now(),
patch: patch
})
wsClients.forEach(client => {
if (client.readyState === 1) { // OPEN
client.send(message)
}
})
}
// Track used ports and running containers
const usedPorts = new Set([5959, 8100])
const runningContainers = new Map()
// Predefined port mappings for known apps
const portMappings = {
'atob': 8102,
'k484': 8103,
'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',
'mempool-electrs': 'mempool-electrs',
'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/app-icons/fedimint.png',
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'
},
'mempool-electrs': {
title: 'Electrs',
icon: '/assets/img/app-icons/electrs.svg',
description: 'Electrum protocol indexer for Bitcoin'
},
'ollama': {
title: 'Ollama',
icon: '/assets/img/app-icons/ollama.png',
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 {
// Try Podman first (Archipelago's choice)
await execPromise('podman ps')
return { available: true, runtime: 'podman' }
} catch {
try {
// Fallback to Docker
await execPromise('docker ps')
return { available: true, runtime: 'docker' }
} catch {
return { available: false, runtime: null }
}
}
}
// Helper: Install package with container runtime (if available) or simulate
async function installPackage(id, manifestUrl) {
console.log(`[Package] 📦 Installing ${id}...`)
try {
// Check if already installed
if (mockData['package-data'][id]) {
throw new Error(`Package ${id} is already installed`)
}
const version = '0.1.0'
const runtime = await isContainerRuntimeAvailable()
// Get package metadata
const packageMetadata = {
'atob': {
title: 'A to B Bitcoin',
shortDesc: 'Bitcoin tools and services for seamless transactions',
longDesc: 'A to B Bitcoin provides tools and services for Bitcoin transactions.',
icon: '/assets/img/atob.png'
},
'k484': {
title: 'K484',
shortDesc: 'Point of Sale and Admin system',
longDesc: 'K484 provides a complete POS and administration system.',
icon: '/assets/img/k484.png'
},
'amin': {
title: 'Amin',
shortDesc: 'Administrative interface for Archipelago',
longDesc: 'Amin provides administrative tools and monitoring.',
icon: '/assets/img/favico.png'
}
}
const metadata = packageMetadata[id] || {
title: id.charAt(0).toUpperCase() + id.slice(1),
shortDesc: `${id} application`,
longDesc: `${id} application for Archipelago`,
icon: '/assets/img/favico.png'
}
// Determine port
const assignedPort = portMappings[id] || 8105
usedPorts.add(assignedPort)
let containerMode = false
let actuallyRunning = false
// Try to run with container runtime if available
if (runtime.available) {
try {
console.log(`[Package] 🐳 ${runtime.runtime} available, attempting to run container...`)
const containerName = `${id}-archipelago`
const stopCmd = runtime.runtime === 'podman'
? `podman stop ${containerName} 2>/dev/null || true`
: `docker stop ${containerName} 2>/dev/null || true`
const rmCmd = runtime.runtime === 'podman'
? `podman rm ${containerName} 2>/dev/null || true`
: `docker rm ${containerName} 2>/dev/null || true`
// Stop and remove existing container if it exists
await execPromise(stopCmd)
await execPromise(rmCmd)
// Check if image exists
const imageCheckCmd = runtime.runtime === 'podman'
? `podman images -q ${id}:${version}`
: `docker images -q ${id}:${version}`
let { stdout } = await execPromise(imageCheckCmd)
if (stdout.trim()) {
// Image exists, start container
const runCmd = runtime.runtime === 'podman'
? `podman run -d --name ${containerName} -p ${assignedPort}:80 ${id}:${version}`
: `docker run -d --name ${containerName} -p ${assignedPort}:80 ${id}:${version}`
await execPromise(runCmd)
// Wait for container to be ready
await new Promise(resolve => setTimeout(resolve, 2000))
// Verify container is running
const statusCmd = runtime.runtime === 'podman'
? `podman ps --filter name=${containerName} --format "{{.Status}}"`
: `docker ps --filter name=${containerName} --format "{{.Status}}"`
const { stdout: containerStatus } = await execPromise(statusCmd)
if (containerStatus.includes('Up')) {
containerMode = true
actuallyRunning = true
runningContainers.set(id, {
port: assignedPort,
containerId: containerName,
runtime: runtime.runtime
})
console.log(`[Package] 🐳 ${runtime.runtime} container running on port ${assignedPort}`)
}
} else {
console.log(`[Package] Container image ${id}:${version} not found, using simulation mode`)
}
} catch (containerError) {
console.log(`[Package] ⚠️ Container error (${containerError.message}), falling back to simulation`)
}
} else {
console.log(`[Package] Container runtime not available, using simulation mode`)
}
// If container didn't work, simulate installation
if (!containerMode) {
await new Promise(resolve => setTimeout(resolve, 1500))
runningContainers.set(id, { port: assignedPort, containerId: null, runtime: null })
}
// Add to mock data
mockData['package-data'][id] = {
title: metadata.title,
version: version,
status: 'running',
state: 'running',
port: assignedPort,
containerMode: containerMode,
actuallyRunning: actuallyRunning,
manifest: {
id: id,
title: metadata.title,
version: version,
description: {
short: metadata.shortDesc,
long: metadata.longDesc
},
icon: metadata.icon,
interfaces: {
main: {
name: 'Web Interface',
description: `${metadata.title} web interface`,
ui: true,
},
},
},
}
// Broadcast update
broadcastUpdate([
{
op: 'add',
path: `/package-data/${id}`,
value: mockData['package-data'][id]
}
])
if (containerMode) {
console.log(`[Package] ✅ ${id} installed and RUNNING at http://localhost:${assignedPort}`)
} else {
console.log(`[Package] ✅ ${id} installed (simulated)`)
}
return { success: true, port: assignedPort, containerMode }
} catch (error) {
console.error(`[Package] ❌ Installation failed:`, error.message)
throw error
}
}
// Helper: Uninstall package
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`)
}
// Stop container if it's running
const containerInfo = runningContainers.get(id)
if (containerInfo && containerInfo.containerId) {
try {
const runtime = containerInfo.runtime || 'docker'
const stopCmd = runtime === 'podman'
? `podman stop ${containerInfo.containerId} 2>/dev/null || true`
: `docker stop ${containerInfo.containerId} 2>/dev/null || true`
const rmCmd = runtime === 'podman'
? `podman rm ${containerInfo.containerId} 2>/dev/null || true`
: `docker rm ${containerInfo.containerId} 2>/dev/null || true`
console.log(`[Package] 🐳 Stopping container ${containerInfo.containerId}...`)
await execPromise(stopCmd)
await execPromise(rmCmd)
console.log(`[Package] 🐳 Container stopped`)
} catch (error) {
console.log(`[Package] ⚠️ Error stopping container: ${error.message}`)
}
}
await new Promise(resolve => setTimeout(resolve, 1000))
const port = mockData['package-data'][id].port
if (port) {
usedPorts.delete(port)
}
runningContainers.delete(id)
delete mockData['package-data'][id]
broadcastUpdate([
{
op: 'remove',
path: `/package-data/${id}`
}
])
console.log(`[Package] ✅ ${id} uninstalled successfully`)
return { success: true }
} catch (error) {
console.error(`[Package] ❌ Uninstall failed:`, error.message)
throw error
}
}
// Mock data
const mockData = {
'server-info': {
id: 'archipelago-dev',
version: '0.1.0',
name: 'Archipelago Dev Server',
pubkey: 'mock-pubkey',
'status-info': {
restarting: false,
'shutting-down': false,
updated: false,
'backup-progress': null,
'update-progress': null,
},
'lan-address': '192.168.1.100',
unread: 0,
'wifi-ssids': [],
'zram-enabled': false,
},
'package-data': {}, // Will be populated from Docker
ui: {
name: 'Archipelago',
'ack-welcome': '0.1.0',
marketplace: {
'selected-hosts': [],
'known-hosts': {},
},
theme: 'dark',
},
}
// 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'] = mergePackageData(dockerApps)
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(mockData['package-data']).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()
})
// RPC endpoint
app.post('/rpc/v1', (req, res) => {
const { method, params } = req.body
console.log(`[RPC] ${method}`)
try {
switch (method) {
// Authentication endpoints
case 'auth.setup': {
const { password } = params
if (!password || password.length < 8) {
return res.json({
error: {
code: -32602,
message: 'Password must be at least 8 characters',
},
})
}
// Set up user
userState.setupComplete = true
userState.passwordHash = password // In real app, bcrypt hash
console.log(`[Auth] User setup completed`)
return res.json({ result: { success: true } })
}
case 'auth.isSetup': {
return res.json({ result: userState.setupComplete })
}
case 'auth.onboardingComplete': {
userState.onboardingComplete = true
console.log(`[Auth] Onboarding completed`)
return res.json({ result: true })
}
case 'auth.isOnboardingComplete': {
return res.json({ result: userState.onboardingComplete })
}
case 'node.did': {
const mockDid = 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH'
const mockPubkey = 'a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456'
return res.json({ result: { did: mockDid, pubkey: mockPubkey } })
}
case 'node.nostr-publish': {
return res.json({ result: { event_id: 'mock-event-id', success: 2, failed: 0 } })
}
case 'node.nostr-pubkey': {
return res.json({ result: { nostr_pubkey: 'mock-nostr-pubkey-hex' } })
}
case 'auth.login': {
const { password } = params
if (!userState.setupComplete) {
return res.json({
error: {
code: -32603,
message: 'User not set up. Please complete setup first.',
},
})
}
// Simple password check (in real app, use bcrypt)
if (password !== userState.passwordHash && password !== MOCK_PASSWORD) {
return res.json({
error: {
code: -32603,
message: 'Password Incorrect',
},
})
}
const sessionId = `session-${Date.now()}`
sessions.set(sessionId, {
createdAt: new Date(),
})
res.cookie('session', sessionId, {
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000,
})
return res.json({ result: null })
}
case 'auth.logout': {
const sessionId = req.cookies?.session
if (sessionId) {
sessions.delete(sessionId)
}
res.clearCookie('session')
return res.json({ result: null })
}
case 'server.echo': {
return res.json({ result: { message: params?.message || 'Hello from Archipelago!' } })
}
case 'server.time': {
return res.json({
result: {
now: new Date().toISOString(),
uptime: process.uptime(),
},
})
}
case 'server.metrics': {
return res.json({
result: {
cpu: 45.2,
memory: 62.8,
disk: 38.1,
},
})
}
case 'marketplace.get': {
const mockApps = [
{
id: 'bitcoin',
title: 'Bitcoin Core',
description: 'A full Bitcoin node.',
version: '25.0.0',
icon: '/assets/img/bitcoin.svg',
author: 'Bitcoin Core Team',
license: 'MIT',
},
{
id: 'lightning',
title: 'Core Lightning',
description: 'Lightning Network implementation.',
version: '23.08',
icon: '/assets/img/c-lightning.png',
author: 'Blockstream',
license: 'MIT',
},
]
return res.json({ result: mockApps })
}
case 'server.update':
case 'server.restart':
case 'server.shutdown': {
return res.json({ result: 'ok' })
}
case 'package.install': {
const { id, url } = params
installPackage(id, url).catch(err => {
console.error(`[RPC] Installation failed:`, err.message)
})
return res.json({ result: `job-${Date.now()}` })
}
case 'package.uninstall': {
const { id } = params
uninstallPackage(id).catch(err => {
console.error(`[RPC] Uninstall failed:`, err.message)
})
return res.json({ result: 'ok' })
}
case 'package.start':
case 'package.stop':
case 'package.restart': {
return res.json({ result: 'ok' })
}
default: {
return res.json({
error: {
code: -32601,
message: `Method not found: ${method}`,
},
})
}
}
} catch (error) {
console.error('[RPC Error]', error)
return res.json({
error: {
code: -32603,
message: error.message,
},
})
}
})
// Health check
app.get('/health', (req, res) => {
res.status(200).send('healthy')
})
// WebSocket endpoint
const server = http.createServer(app)
const wss = new WebSocketServer({ server, path: '/ws/db' })
wss.on('connection', (ws, req) => {
console.log('[WebSocket] Client connected from', req.socket.remoteAddress)
wsClients.add(ws)
// Set up ping/pong to keep connection alive
const pingInterval = setInterval(() => {
if (ws.readyState === 1) { // OPEN
try {
ws.ping()
} catch (err) {
console.error('[WebSocket] Ping error:', err)
clearInterval(pingInterval)
}
} else {
clearInterval(pingInterval)
}
}, 30000) // Ping every 30 seconds
// Send initial data immediately
try {
ws.send(JSON.stringify({
type: 'initial',
data: mockData,
}))
console.log('[WebSocket] Initial data sent')
} catch (err) {
console.error('[WebSocket] Error sending initial data:', err)
}
ws.on('pong', () => {
// Client responded to ping, connection is alive
})
ws.on('message', (message) => {
// Handle incoming messages if needed
try {
const data = JSON.parse(message.toString())
console.log('[WebSocket] Received message:', data)
} catch (err) {
console.error('[WebSocket] Error parsing message:', err)
}
})
ws.on('close', (code, reason) => {
console.log('[WebSocket] Client disconnected', { code, reason: reason.toString() })
clearInterval(pingInterval)
wsClients.delete(ws)
})
ws.on('error', (error) => {
console.error('[WebSocket Error]', error)
clearInterval(pingInterval)
wsClients.delete(ws)
})
})
server.listen(PORT, '0.0.0.0', async () => {
const runtime = await isContainerRuntimeAvailable()
// Initialize package data from Docker
await initializePackageData()
console.log(`
╔════════════════════════════════════════════════════════════╗
║ ║
║ 🚀 Archipelago Mock Backend Server ║
║ ║
║ RPC: http://localhost:${PORT}/rpc/v1 ║
║ WebSocket: ws://localhost:${PORT}/ws/db ║
║ ║
║ Dev Mode: ${DEV_MODE.padEnd(47)}
║ Setup: ${userState.setupComplete ? '✅ Complete' : '❌ Not done'.padEnd(47)}
║ Onboarding: ${userState.onboardingComplete ? '✅ Complete' : '❌ Not done'.padEnd(46)}
║ ║
║ 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 (merge with static dev apps)
setInterval(async () => {
const dockerApps = await getDockerContainers()
mockData['package-data'] = mergePackageData(dockerApps)
// Broadcast update to connected clients
broadcastUpdate([
{
op: 'replace',
path: '/package-data',
value: mockData['package-data']
}
])
}, 5000) // Update every 5 seconds
})
process.on('SIGINT', () => {
console.log('\n\nShutting down mock backend...')
server.close(() => {
console.log('Server stopped.')
process.exit(0)
})
})