feat: enrich mock backend for demo — add all missing RPC handlers and demo data
Fixes "Method not found: identity.create" on demo onboarding. Adds handlers for all identity, nostr, content, network, router, and peer RPC methods so no method-not-found errors occur anywhere in the demo. Expands marketplace from 2 to 12 apps, adds 5 static dashboard apps, randomizes metrics, and populates peer/message data for a richer demo experience. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -541,10 +541,10 @@ async function uninstallPackage(id) {
|
||||
// Mock data
|
||||
const mockData = {
|
||||
'server-info': {
|
||||
id: 'archipelago-dev',
|
||||
id: 'archipelago-demo',
|
||||
version: '0.1.0',
|
||||
name: 'Archipelago Dev Server',
|
||||
pubkey: 'mock-pubkey',
|
||||
name: 'Archipelago',
|
||||
pubkey: 'a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456',
|
||||
'status-info': {
|
||||
restarting: false,
|
||||
'shutting-down': false,
|
||||
@@ -553,11 +553,12 @@ const mockData = {
|
||||
'update-progress': null,
|
||||
},
|
||||
'lan-address': '192.168.1.100',
|
||||
unread: 0,
|
||||
'wifi-ssids': [],
|
||||
'zram-enabled': false,
|
||||
'tor-address': 'archydemox7k3pnw4hv5qz2jcbr6dwefys3ockqzf4mzjlvxot2ioad.onion',
|
||||
unread: 3,
|
||||
'wifi-ssids': ['Home-5G', 'Archipelago-Mesh', 'Neighbors-Open'],
|
||||
'zram-enabled': true,
|
||||
},
|
||||
'package-data': {}, // Will be populated from Docker
|
||||
'package-data': {}, // Will be populated from Docker + static apps
|
||||
ui: {
|
||||
name: 'Archipelago',
|
||||
'ack-welcome': '0.1.0',
|
||||
@@ -569,40 +570,33 @@ 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',
|
||||
// Helper to build a static app entry
|
||||
function staticApp({ id, title, version, shortDesc, longDesc, license, state, lanPort, torHost, icon }) {
|
||||
return {
|
||||
title,
|
||||
version,
|
||||
status: state,
|
||||
state,
|
||||
'static-files': {
|
||||
license: 'MIT',
|
||||
instructions: 'A LoRa based doorbell',
|
||||
icon: '/assets/img/app-icons/lorabell.png'
|
||||
license: license || 'MIT',
|
||||
instructions: shortDesc,
|
||||
icon: icon || `/assets/img/app-icons/${id}.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',
|
||||
id,
|
||||
title,
|
||||
version,
|
||||
description: { short: shortDesc, long: longDesc || shortDesc },
|
||||
'release-notes': 'Latest stable release',
|
||||
license: license || 'MIT',
|
||||
'wrapper-repo': '#',
|
||||
'upstream-repo': '#',
|
||||
'support-site': '#',
|
||||
'marketing-site': '#',
|
||||
'donation-url': null,
|
||||
interfaces: {
|
||||
main: {
|
||||
name: 'Web Interface',
|
||||
description: 'LoraBell web interface',
|
||||
ui: true
|
||||
}
|
||||
}
|
||||
main: { name: 'Web Interface', description: `${title} web interface`, ui: true },
|
||||
},
|
||||
},
|
||||
installed: {
|
||||
'current-dependents': {},
|
||||
@@ -610,15 +604,65 @@ const staticDevApps = {
|
||||
'last-backup': null,
|
||||
'interface-addresses': {
|
||||
main: {
|
||||
'tor-address': 'lorabell.onion',
|
||||
'lan-address': 'http://192.168.1.166'
|
||||
}
|
||||
'tor-address': torHost ? `${torHost}.onion` : `${id}.onion`,
|
||||
'lan-address': lanPort ? `http://192.168.1.100:${lanPort}` : '',
|
||||
},
|
||||
},
|
||||
status: 'running'
|
||||
}
|
||||
status: state,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Static dev apps (always shown in My Apps when using mock backend)
|
||||
const staticDevApps = {
|
||||
bitcoin: staticApp({
|
||||
id: 'bitcoin',
|
||||
title: 'Bitcoin Core',
|
||||
version: '27.1',
|
||||
shortDesc: 'Full Bitcoin node',
|
||||
longDesc: 'Validate every transaction and block. Full consensus enforcement — the bedrock of sovereignty.',
|
||||
state: 'running',
|
||||
lanPort: 8332,
|
||||
}),
|
||||
lnd: staticApp({
|
||||
id: 'lnd',
|
||||
title: 'LND',
|
||||
version: '0.18.3',
|
||||
shortDesc: 'Lightning Network Daemon',
|
||||
longDesc: 'Instant Bitcoin payments with near-zero fees. Open channels, route payments, earn sats.',
|
||||
state: 'running',
|
||||
lanPort: 8080,
|
||||
}),
|
||||
electrs: staticApp({
|
||||
id: 'electrs',
|
||||
title: 'Electrs',
|
||||
version: '0.10.6',
|
||||
shortDesc: 'Electrum Server in Rust',
|
||||
longDesc: 'Private blockchain indexing for wallet lookups. Connect Sparrow, BlueWallet, or any Electrum-compatible wallet.',
|
||||
state: 'running',
|
||||
lanPort: 50001,
|
||||
}),
|
||||
mempool: staticApp({
|
||||
id: 'mempool',
|
||||
title: 'Mempool',
|
||||
version: '3.0.0',
|
||||
shortDesc: 'Blockchain explorer & fee estimator',
|
||||
longDesc: 'Real-time mempool visualization, transaction tracking, and fee estimation — all on your own node.',
|
||||
license: 'AGPL-3.0',
|
||||
state: 'running',
|
||||
lanPort: 4080,
|
||||
}),
|
||||
lorabell: staticApp({
|
||||
id: 'lorabell',
|
||||
title: 'LoraBell',
|
||||
version: '1.0.0',
|
||||
shortDesc: 'LoRa doorbell',
|
||||
longDesc: 'Receive doorbell notifications over LoRa radio — no WiFi or internet required.',
|
||||
state: 'running',
|
||||
lanPort: null,
|
||||
}),
|
||||
}
|
||||
|
||||
function mergePackageData(dockerApps) {
|
||||
return { ...dockerApps, ...staticDevApps }
|
||||
}
|
||||
@@ -795,11 +839,12 @@ app.post('/rpc/v1', (req, res) => {
|
||||
}
|
||||
|
||||
case 'server.metrics': {
|
||||
// Slightly randomize so the dashboard feels alive
|
||||
return res.json({
|
||||
result: {
|
||||
cpu: 45.2,
|
||||
memory: 62.8,
|
||||
disk: 38.1,
|
||||
cpu: +(12 + Math.random() * 18).toFixed(1),
|
||||
memory: +(58 + Math.random() * 8).toFixed(1),
|
||||
disk: +(34 + Math.random() * 3).toFixed(1),
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -809,23 +854,125 @@ app.post('/rpc/v1', (req, res) => {
|
||||
{
|
||||
id: 'bitcoin',
|
||||
title: 'Bitcoin Core',
|
||||
description: 'A full Bitcoin node.',
|
||||
version: '25.0.0',
|
||||
icon: '/assets/img/bitcoin.svg',
|
||||
author: 'Bitcoin Core Team',
|
||||
description: 'Full Bitcoin node — validate transactions, enforce consensus rules, and support the network. The foundation of sovereignty.',
|
||||
version: '27.1',
|
||||
icon: '/assets/img/app-icons/bitcoin.png',
|
||||
author: 'Bitcoin Core',
|
||||
license: 'MIT',
|
||||
category: 'Bitcoin',
|
||||
},
|
||||
{
|
||||
id: 'lightning',
|
||||
title: 'Core Lightning',
|
||||
description: 'Lightning Network implementation.',
|
||||
version: '23.08',
|
||||
icon: '/assets/img/c-lightning.png',
|
||||
author: 'Blockstream',
|
||||
id: 'lnd',
|
||||
title: 'LND',
|
||||
description: 'Lightning Network Daemon — instant, low-fee Bitcoin payments. Open channels, route payments, earn routing fees.',
|
||||
version: '0.18.3',
|
||||
icon: '/assets/img/app-icons/lnd.png',
|
||||
author: 'Lightning Labs',
|
||||
license: 'MIT',
|
||||
category: 'Bitcoin',
|
||||
},
|
||||
{
|
||||
id: 'electrs',
|
||||
title: 'Electrs',
|
||||
description: 'Electrum Server in Rust — index the blockchain for fast wallet lookups. Connect your hardware wallets privately.',
|
||||
version: '0.10.6',
|
||||
icon: '/assets/img/app-icons/electrs.png',
|
||||
author: 'Roman Zeyde',
|
||||
license: 'MIT',
|
||||
category: 'Bitcoin',
|
||||
},
|
||||
{
|
||||
id: 'mempool',
|
||||
title: 'Mempool',
|
||||
description: 'Bitcoin blockchain explorer and mempool visualizer. Monitor transactions, fees, and network activity in real time.',
|
||||
version: '3.0.0',
|
||||
icon: '/assets/img/app-icons/mempool.png',
|
||||
author: 'Mempool Space',
|
||||
license: 'AGPL-3.0',
|
||||
category: 'Bitcoin',
|
||||
},
|
||||
{
|
||||
id: 'btcpay',
|
||||
title: 'BTCPay Server',
|
||||
description: 'Self-hosted Bitcoin payment processor. Accept Bitcoin payments in your store — no fees, no middlemen, no KYC.',
|
||||
version: '2.0.4',
|
||||
icon: '/assets/img/app-icons/btcpay.png',
|
||||
author: 'BTCPay Server',
|
||||
license: 'MIT',
|
||||
category: 'Commerce',
|
||||
},
|
||||
{
|
||||
id: 'fedimint',
|
||||
title: 'Fedimint',
|
||||
description: 'Federated Chaumian e-cash mint — community custody, private payments, and Lightning gateways for your tribe.',
|
||||
version: '0.4.3',
|
||||
icon: '/assets/img/app-icons/fedimint.png',
|
||||
author: 'Fedimint',
|
||||
license: 'MIT',
|
||||
category: 'Bitcoin',
|
||||
},
|
||||
{
|
||||
id: 'vaultwarden',
|
||||
title: 'Vaultwarden',
|
||||
description: 'Self-hosted password manager compatible with Bitwarden clients. Keep your credentials under your own roof.',
|
||||
version: '1.32.5',
|
||||
icon: '/assets/img/app-icons/vaultwarden.png',
|
||||
author: 'Vaultwarden',
|
||||
license: 'AGPL-3.0',
|
||||
category: 'Privacy',
|
||||
},
|
||||
{
|
||||
id: 'nextcloud',
|
||||
title: 'Nextcloud',
|
||||
description: 'Your personal cloud — files, calendar, contacts, and collaboration. Replace Google Drive and Dropbox entirely.',
|
||||
version: '29.0.0',
|
||||
icon: '/assets/img/app-icons/nextcloud.png',
|
||||
author: 'Nextcloud GmbH',
|
||||
license: 'AGPL-3.0',
|
||||
category: 'Cloud',
|
||||
},
|
||||
{
|
||||
id: 'nostr-relay',
|
||||
title: 'Nostr Relay',
|
||||
description: 'Run your own Nostr relay — sovereign social networking. Publish notes, follow friends, no censorship possible.',
|
||||
version: '0.34.0',
|
||||
icon: '/assets/img/app-icons/nostr-relay.png',
|
||||
author: 'nostr-rs-relay',
|
||||
license: 'MIT',
|
||||
category: 'Social',
|
||||
},
|
||||
{
|
||||
id: 'home-assistant',
|
||||
title: 'Home Assistant',
|
||||
description: 'Open-source home automation — control lights, locks, cameras, and sensors. Smart home without the cloud dependency.',
|
||||
version: '2024.12.0',
|
||||
icon: '/assets/img/app-icons/home-assistant.png',
|
||||
author: 'Home Assistant',
|
||||
license: 'Apache-2.0',
|
||||
category: 'IoT',
|
||||
},
|
||||
{
|
||||
id: 'syncthing',
|
||||
title: 'Syncthing',
|
||||
description: 'Continuous peer-to-peer file synchronization. Sync folders across devices without any cloud service.',
|
||||
version: '1.28.1',
|
||||
icon: '/assets/img/app-icons/syncthing.png',
|
||||
author: 'Syncthing Foundation',
|
||||
license: 'MPL-2.0',
|
||||
category: 'Cloud',
|
||||
},
|
||||
{
|
||||
id: 'tor',
|
||||
title: 'Tor',
|
||||
description: 'Anonymous communication — route traffic through the Tor network. Access your node from anywhere, privately.',
|
||||
version: '0.4.8.13',
|
||||
icon: '/assets/img/app-icons/tor.png',
|
||||
author: 'Tor Project',
|
||||
license: 'BSD-3',
|
||||
category: 'Privacy',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
return res.json({ result: mockApps })
|
||||
}
|
||||
|
||||
@@ -893,10 +1040,179 @@ app.post('/rpc/v1', (req, res) => {
|
||||
return res.json({ result: { success: true } })
|
||||
}
|
||||
|
||||
case 'node-messages-received':
|
||||
case 'node.messages':
|
||||
case 'node.notifications':
|
||||
// =========================================================================
|
||||
// Identity & Onboarding
|
||||
// =========================================================================
|
||||
case 'identity.create': {
|
||||
const { name, purpose } = params || {}
|
||||
console.log(`[Identity] Created identity: "${name || 'Personal'}" (${purpose || 'personal'})`)
|
||||
return res.json({ result: { success: true, id: `identity-${Date.now()}` } })
|
||||
}
|
||||
|
||||
case 'identity.register-name': {
|
||||
const { name } = params || {}
|
||||
console.log(`[Identity] Registered name: ${name}`)
|
||||
return res.json({ result: { success: true, id: `name-${Date.now()}` } })
|
||||
}
|
||||
|
||||
case 'identity.remove-name': {
|
||||
return res.json({ result: { success: true } })
|
||||
}
|
||||
|
||||
case 'identity.set-default': {
|
||||
return res.json({ result: { success: true } })
|
||||
}
|
||||
|
||||
case 'identity.delete': {
|
||||
return res.json({ result: { success: true } })
|
||||
}
|
||||
|
||||
case 'identity.issue-credential': {
|
||||
return res.json({ result: { success: true, credential_id: `cred-${Date.now()}` } })
|
||||
}
|
||||
|
||||
case 'identity.revoke-credential': {
|
||||
return res.json({ result: { success: true } })
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Nostr
|
||||
// =========================================================================
|
||||
case 'nostr.add-relay': {
|
||||
const { url } = params || {}
|
||||
console.log(`[Nostr] Added relay: ${url}`)
|
||||
return res.json({ result: { success: true } })
|
||||
}
|
||||
|
||||
case 'nostr.remove-relay': {
|
||||
return res.json({ result: { success: true } })
|
||||
}
|
||||
|
||||
case 'nostr.toggle-relay': {
|
||||
return res.json({ result: { success: true } })
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Content & Network
|
||||
// =========================================================================
|
||||
case 'content.remove': {
|
||||
return res.json({ result: { success: true } })
|
||||
}
|
||||
|
||||
case 'content.set-pricing': {
|
||||
return res.json({ result: { success: true } })
|
||||
}
|
||||
|
||||
case 'network.accept-request': {
|
||||
return res.json({ result: { success: true } })
|
||||
}
|
||||
|
||||
case 'network.reject-request': {
|
||||
return res.json({ result: { success: true } })
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Server & Auth extras
|
||||
// =========================================================================
|
||||
case 'server.health': {
|
||||
return res.json({
|
||||
result: {
|
||||
status: 'healthy',
|
||||
uptime: Math.floor(process.uptime()),
|
||||
services: {
|
||||
backend: 'running',
|
||||
nginx: 'running',
|
||||
podman: 'running',
|
||||
tor: 'running',
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
case 'auth.changePassword': {
|
||||
const { currentPassword } = params || {}
|
||||
if (currentPassword !== userState.passwordHash && currentPassword !== MOCK_PASSWORD) {
|
||||
return res.json({ error: { code: -32603, message: 'Current password is incorrect' } })
|
||||
}
|
||||
userState.passwordHash = params.newPassword
|
||||
console.log('[Auth] Password changed')
|
||||
return res.json({ result: { success: true } })
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Router (port forwarding)
|
||||
// =========================================================================
|
||||
case 'router.add-forward': {
|
||||
console.log(`[Router] Added forward: ${JSON.stringify(params)}`)
|
||||
return res.json({ result: { success: true, id: `fwd-${Date.now()}` } })
|
||||
}
|
||||
|
||||
case 'router.remove-forward': {
|
||||
return res.json({ result: { success: true } })
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Tor & Peer Networking
|
||||
// =========================================================================
|
||||
case 'node.tor-address': {
|
||||
return res.json({
|
||||
result: {
|
||||
tor_address: 'archydemox7k3pnw4hv5qz2jcbr6dwefys3ockqzf4mzjlvxot2ioad.onion',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
case 'node-list-peers': {
|
||||
return res.json({
|
||||
result: {
|
||||
peers: [
|
||||
{ onion: 'peer1abc2def3ghi4jkl5mno6pqr7stu8vwx9yz.onion', pubkey: 'a1b2c3d4e5f6', name: 'satoshi-node' },
|
||||
{ onion: 'peer2xyz9wvu8tsr7qpo6nml5kji4hgf3edc2ba.onion', pubkey: 'f6e5d4c3b2a1', name: 'lightning-lab' },
|
||||
{ onion: 'peer3mno6pqr7stu8vwx9yzabc2def3ghi4jkl5.onion', pubkey: 'c3d4e5f6a1b2', name: 'sovereign-relay' },
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
case 'node-check-peer': {
|
||||
return res.json({ result: { onion: params?.onion || '', reachable: Math.random() > 0.2 } })
|
||||
}
|
||||
|
||||
case 'node-add-peer': {
|
||||
console.log(`[Peers] Added peer: ${params?.onion}`)
|
||||
return res.json({ result: { success: true } })
|
||||
}
|
||||
|
||||
case 'node-send-message': {
|
||||
console.log(`[Peers] Sent message to: ${params?.onion}`)
|
||||
return res.json({ result: { ok: true, sent_to: params?.onion || '' } })
|
||||
}
|
||||
|
||||
case 'node-nostr-discover': {
|
||||
return res.json({
|
||||
result: {
|
||||
nodes: [
|
||||
{ did: 'did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2ReMkBe4bR6XBIDNq9', onion: 'disc1abc2def3ghi4jkl5mno6pqr7stu8vwx9yz.onion', pubkey: 'disc1pub', node_address: '192.168.1.50' },
|
||||
{ did: 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH', onion: 'disc2xyz9wvu8tsr7qpo6nml5kji4hgf3edc2ba.onion', pubkey: 'disc2pub', node_address: '192.168.1.51' },
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
case 'node-messages-received':
|
||||
case 'node.messages': {
|
||||
return res.json({
|
||||
result: {
|
||||
messages: [
|
||||
{ from_pubkey: 'a1b2c3d4e5f6', message: 'Hey, your relay is online! Nice uptime.', timestamp: new Date(Date.now() - 3600000).toISOString() },
|
||||
{ from_pubkey: 'f6e5d4c3b2a1', message: 'Channel opened successfully. 500k sats capacity.', timestamp: new Date(Date.now() - 7200000).toISOString() },
|
||||
{ from_pubkey: 'c3d4e5f6a1b2', message: 'Backup sync complete. All good on my end.', timestamp: new Date(Date.now() - 86400000).toISOString() },
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
case 'node.notifications': {
|
||||
return res.json({ result: [] })
|
||||
}
|
||||
|
||||
@@ -1018,10 +1334,10 @@ app.get('/app/filebrowser/api/raw/*', (req, res) => {
|
||||
})
|
||||
|
||||
// Claude API Proxy (reads ANTHROPIC_API_KEY from environment)
|
||||
// Uses fetch (Node 22+) for reliable DNS resolution and streaming in Docker/Alpine
|
||||
// =============================================================================
|
||||
import https from 'https'
|
||||
|
||||
app.post('/aiui/api/claude/*', (req, res) => {
|
||||
app.post('/aiui/api/claude/*', async (req, res) => {
|
||||
const apiKey = process.env.ANTHROPIC_API_KEY
|
||||
if (!apiKey) {
|
||||
return res.status(500).json({
|
||||
@@ -1053,38 +1369,63 @@ app.post('/aiui/api/claude/*', (req, res) => {
|
||||
}
|
||||
|
||||
const bodyStr = JSON.stringify(body)
|
||||
const url = `https://api.anthropic.com${apiPath}`
|
||||
console.log(`[Claude Proxy] → POST ${url} (${bodyStr.length} bytes, model: ${body?.model || 'unknown'})`)
|
||||
|
||||
const options = {
|
||||
hostname: 'api.anthropic.com',
|
||||
port: 443,
|
||||
path: apiPath,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': apiKey,
|
||||
'anthropic-version': '2023-06-01',
|
||||
'Content-Length': Buffer.byteLength(bodyStr),
|
||||
},
|
||||
}
|
||||
try {
|
||||
const controller = new AbortController()
|
||||
const timeout = setTimeout(() => controller.abort(), 60000)
|
||||
|
||||
const proxyReq = https.request(options, (proxyRes) => {
|
||||
res.writeHead(proxyRes.statusCode, proxyRes.headers)
|
||||
proxyRes.pipe(res)
|
||||
})
|
||||
const apiRes = await fetch(url, {
|
||||
method: 'POST',
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': apiKey,
|
||||
'anthropic-version': '2023-06-01',
|
||||
},
|
||||
body: bodyStr,
|
||||
})
|
||||
|
||||
proxyReq.on('error', (err) => {
|
||||
const msg = err.message || err.code || JSON.stringify(err) || 'Unknown proxy error'
|
||||
console.error('[Claude Proxy] Error:', msg)
|
||||
clearTimeout(timeout)
|
||||
console.log(`[Claude Proxy] ← ${apiRes.status}`)
|
||||
|
||||
// Forward status and headers
|
||||
res.status(apiRes.status)
|
||||
for (const [key, value] of apiRes.headers.entries()) {
|
||||
// Skip hop-by-hop headers
|
||||
if (!['transfer-encoding', 'connection', 'keep-alive'].includes(key.toLowerCase())) {
|
||||
res.setHeader(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
// Stream the response body
|
||||
if (apiRes.body) {
|
||||
const reader = apiRes.body.getReader()
|
||||
const pump = async () => {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) { res.end(); return }
|
||||
if (!res.writableEnded) res.write(value)
|
||||
}
|
||||
}
|
||||
pump().catch((err) => {
|
||||
console.error('[Claude Proxy] Stream error:', err.message)
|
||||
if (!res.writableEnded) res.end()
|
||||
})
|
||||
} else {
|
||||
res.end()
|
||||
}
|
||||
} catch (err) {
|
||||
const msg = err.name === 'AbortError' ? 'Request timed out (60s)' : (err.message || 'Unknown error')
|
||||
console.error(`[Claude Proxy] Error: ${msg}`)
|
||||
if (!res.headersSent) {
|
||||
res.status(502).json({
|
||||
type: 'error',
|
||||
error: { type: 'proxy_error', message: msg }
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
proxyReq.write(bodyStr)
|
||||
proxyReq.end()
|
||||
}
|
||||
})
|
||||
|
||||
// Ollama (local AI) proxy — forwards to localhost:11434
|
||||
@@ -1273,10 +1614,26 @@ server.listen(PORT, '0.0.0.0', async () => {
|
||||
║ ║
|
||||
║ Container Runtime: ${runtime.available ? `✅ ${runtime.runtime}`.padEnd(40) : '❌ Not available'.padEnd(40)}║
|
||||
║ Docker API: ✅ Connected ║
|
||||
║ Claude API Key: ${process.env.ANTHROPIC_API_KEY ? '✅ Set (' + process.env.ANTHROPIC_API_KEY.slice(0, 12) + '...)' : '❌ Not set (chat disabled)'.padEnd(40)}║
|
||||
║ ║
|
||||
╚════════════════════════════════════════════════════════════╝
|
||||
`)
|
||||
console.log('Mock backend is running. Press Ctrl+C to stop.\n')
|
||||
|
||||
// Pre-check Anthropic API connectivity
|
||||
if (process.env.ANTHROPIC_API_KEY) {
|
||||
try {
|
||||
const dns = await import('dns')
|
||||
dns.lookup('api.anthropic.com', (err, address) => {
|
||||
if (err) {
|
||||
console.error('[Claude Proxy] ⚠ DNS lookup failed for api.anthropic.com:', err.message)
|
||||
console.error('[Claude Proxy] Chat will fail. Check container DNS settings.')
|
||||
} else {
|
||||
console.log('[Claude Proxy] ✅ api.anthropic.com resolves to', address)
|
||||
}
|
||||
})
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
// Periodically update package data from Docker (merge with static dev apps)
|
||||
// Only poll if container runtime is available (avoids log spam in demo/Docker deployments)
|
||||
|
||||
Reference in New Issue
Block a user