fix: prevent My Apps crash when installing apps + add filebrowser to demo
The My Apps page went blank after installing apps because pkg['static-files'].icon was accessed without optional chaining on dynamically installed packages that lack the static-files property. - Make static-files optional in PackageDataEntry type - Add defensive ?.icon access with fallback in Apps.vue and AppDetails.vue - Add filebrowser to mock backend staticDevApps (enables Cloud page in demo) - Expand portMappings and marketplaceMetadata for all marketplace apps - installPackage now uses staticApp() format for consistent data shape Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
9c7ffbb263
commit
a2aa9657b1
+105
-60
@@ -121,9 +121,39 @@ const runningContainers = new Map()
|
||||
const portMappings = {
|
||||
'atob': 8102,
|
||||
'k484': 8103,
|
||||
'amin': 8104
|
||||
'amin': 8104,
|
||||
'filebrowser': 8083,
|
||||
'bitcoin-knots': 8332,
|
||||
'electrs': 50001,
|
||||
'btcpay-server': 23000,
|
||||
'lnd': 8080,
|
||||
'mempool': 4080,
|
||||
'homeassistant': 8123,
|
||||
'grafana': 3000,
|
||||
'searxng': 8888,
|
||||
'ollama': 11434,
|
||||
'nextcloud': 8082,
|
||||
'vaultwarden': 8222,
|
||||
'jellyfin': 8096,
|
||||
'photoprism': 2342,
|
||||
'immich': 2283,
|
||||
'portainer': 9443,
|
||||
'uptime-kuma': 3001,
|
||||
'tailscale': 41641,
|
||||
'fedimint': 8174,
|
||||
'nostr-rs-relay': 7000,
|
||||
'syncthing': 8384,
|
||||
'penpot': 9001,
|
||||
'onlyoffice': 8044,
|
||||
'nginx-proxy-manager': 8181,
|
||||
'indeedhub': 8190,
|
||||
'dwn': 3000,
|
||||
'tor': 9050,
|
||||
}
|
||||
|
||||
// Auto-assign port for unknown apps (start at 8200, increment)
|
||||
let nextAutoPort = 8200
|
||||
|
||||
// Helper: Query real Docker containers
|
||||
async function getDockerContainers() {
|
||||
try {
|
||||
@@ -311,50 +341,66 @@ async function isContainerRuntimeAvailable() {
|
||||
}
|
||||
}
|
||||
|
||||
// Marketplace metadata lookup for install (title, description, icon, version)
|
||||
const marketplaceMetadata = {
|
||||
'bitcoin-knots': { title: 'Bitcoin Knots', shortDesc: 'Full Bitcoin node — validate and relay blocks and transactions', icon: '/assets/img/app-icons/bitcoin-knots.webp' },
|
||||
'electrs': { title: 'Electrs', shortDesc: 'Electrum protocol indexer for Bitcoin', icon: '/assets/img/app-icons/electrs.svg' },
|
||||
'btcpay-server': { title: 'BTCPay Server', shortDesc: 'Self-hosted Bitcoin payment processor', icon: '/assets/img/app-icons/btcpay-server.png' },
|
||||
'lnd': { title: 'LND', shortDesc: 'Lightning Network Daemon', icon: '/assets/img/app-icons/lnd.svg' },
|
||||
'mempool': { title: 'Mempool Explorer', shortDesc: 'Bitcoin blockchain and mempool visualizer', icon: '/assets/img/app-icons/mempool.webp' },
|
||||
'homeassistant': { title: 'Home Assistant', shortDesc: 'Open-source home automation platform', icon: '/assets/img/app-icons/homeassistant.png' },
|
||||
'grafana': { title: 'Grafana', shortDesc: 'Analytics and monitoring dashboards', icon: '/assets/img/app-icons/grafana.png' },
|
||||
'searxng': { title: 'SearXNG', shortDesc: 'Privacy-respecting metasearch engine', icon: '/assets/img/app-icons/searxng.png' },
|
||||
'ollama': { title: 'Ollama', shortDesc: 'Run large language models locally', icon: '/assets/img/app-icons/ollama.png' },
|
||||
'onlyoffice': { title: 'OnlyOffice', shortDesc: 'Office suite for document collaboration', icon: '/assets/img/app-icons/onlyoffice.webp' },
|
||||
'penpot': { title: 'Penpot', shortDesc: 'Open-source design and prototyping platform', icon: '/assets/img/app-icons/penpot.webp' },
|
||||
'nextcloud': { title: 'Nextcloud', shortDesc: 'Self-hosted cloud storage and collaboration', icon: '/assets/img/app-icons/nextcloud.webp' },
|
||||
'vaultwarden': { title: 'Vaultwarden', shortDesc: 'Self-hosted password manager (Bitwarden-compatible)', icon: '/assets/img/app-icons/vaultwarden.webp' },
|
||||
'jellyfin': { title: 'Jellyfin', shortDesc: 'Free media server for movies, music, and photos', icon: '/assets/img/app-icons/jellyfin.webp' },
|
||||
'photoprism': { title: 'PhotoPrism', shortDesc: 'AI-powered photo management', icon: '/assets/img/app-icons/photoprims.svg' },
|
||||
'immich': { title: 'Immich', shortDesc: 'High-performance photo and video backup', icon: '/assets/img/app-icons/immich.png' },
|
||||
'filebrowser': { title: 'File Browser', shortDesc: 'Web-based file manager', icon: '/assets/img/app-icons/file-browser.webp' },
|
||||
'nginx-proxy-manager': { title: 'Nginx Proxy Manager', shortDesc: 'Easy proxy management with SSL', icon: '/assets/img/app-icons/nginx.svg' },
|
||||
'portainer': { title: 'Portainer', shortDesc: 'Container management UI', icon: '/assets/img/app-icons/portainer.webp' },
|
||||
'uptime-kuma': { title: 'Uptime Kuma', shortDesc: 'Self-hosted monitoring tool', icon: '/assets/img/app-icons/uptime-kuma.webp' },
|
||||
'tailscale': { title: 'Tailscale', shortDesc: 'Zero-config VPN for secure remote access', icon: '/assets/img/app-icons/tailscale.webp' },
|
||||
'fedimint': { title: 'Fedimint', shortDesc: 'Federated Bitcoin mint with Guardian UI', icon: '/assets/img/app-icons/fedimint.png' },
|
||||
'indeedhub': { title: 'Indeehub', shortDesc: 'Bitcoin documentary streaming platform', icon: '/assets/img/app-icons/indeedhub.png' },
|
||||
'dwn': { title: 'Decentralized Web Node', shortDesc: 'Store and sync personal data with DID-based access', icon: '/assets/img/app-icons/dwn.svg' },
|
||||
'nostr-rs-relay': { title: 'Nostr Relay', shortDesc: 'Run your own Nostr relay', icon: '/assets/img/app-icons/nostr-rs-relay.svg' },
|
||||
'syncthing': { title: 'Syncthing', shortDesc: 'Peer-to-peer file synchronization', icon: '/assets/img/app-icons/syncthing.png' },
|
||||
'tor': { title: 'Tor', shortDesc: 'Anonymous communication over the Tor network', icon: '/assets/img/app-icons/tor.png' },
|
||||
'atob': { title: 'A to B Bitcoin', shortDesc: 'Bitcoin tools for seamless transactions', icon: '/assets/img/atob.png' },
|
||||
'k484': { title: 'K484', shortDesc: 'Point of Sale and Admin system', icon: '/assets/img/k484.png' },
|
||||
'amin': { title: 'Amin', shortDesc: 'Administrative interface for Archipelago', icon: '/assets/icon/pwa-192x192-v2.png' },
|
||||
}
|
||||
|
||||
// Helper: Install package with container runtime (if available) or simulate
|
||||
async function installPackage(id, manifestUrl) {
|
||||
async function installPackage(id, manifestUrl, opts = {}) {
|
||||
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 version = opts.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/icon/pwa-192x192-v2.png'
|
||||
}
|
||||
|
||||
// Get package metadata from marketplace lookup, then fallback
|
||||
const metadata = marketplaceMetadata[id] || {
|
||||
title: id.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '),
|
||||
shortDesc: `${id} application`,
|
||||
icon: `/assets/img/app-icons/${id}.png`
|
||||
}
|
||||
|
||||
const metadata = packageMetadata[id] || {
|
||||
title: id.charAt(0).toUpperCase() + id.slice(1),
|
||||
shortDesc: `${id} application`,
|
||||
longDesc: `${id} application for Archipelago`,
|
||||
icon: '/assets/icon/pwa-192x192-v2.png'
|
||||
// Determine port — use known mapping, or auto-assign a unique one
|
||||
let assignedPort = portMappings[id]
|
||||
if (!assignedPort) {
|
||||
while (usedPorts.has(nextAutoPort)) nextAutoPort++
|
||||
assignedPort = nextAutoPort++
|
||||
}
|
||||
|
||||
// Determine port
|
||||
const assignedPort = portMappings[id] || 8105
|
||||
usedPorts.add(assignedPort)
|
||||
|
||||
let containerMode = false
|
||||
@@ -428,32 +474,21 @@ async function installPackage(id, manifestUrl) {
|
||||
runningContainers.set(id, { port: assignedPort, containerId: null, runtime: null })
|
||||
}
|
||||
|
||||
// Add to mock data
|
||||
// Add to mock data using staticApp format for consistency
|
||||
mockData['package-data'][id] = {
|
||||
title: metadata.title,
|
||||
version: version,
|
||||
status: 'running',
|
||||
state: 'running',
|
||||
...staticApp({
|
||||
id,
|
||||
title: metadata.title,
|
||||
version,
|
||||
shortDesc: metadata.shortDesc,
|
||||
longDesc: metadata.shortDesc,
|
||||
state: 'running',
|
||||
lanPort: assignedPort,
|
||||
icon: metadata.icon,
|
||||
}),
|
||||
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
|
||||
@@ -661,6 +696,16 @@ const staticDevApps = {
|
||||
state: 'running',
|
||||
lanPort: null,
|
||||
}),
|
||||
filebrowser: staticApp({
|
||||
id: 'filebrowser',
|
||||
title: 'File Browser',
|
||||
version: '2.27.0',
|
||||
shortDesc: 'Web-based file manager',
|
||||
longDesc: 'Browse, upload, and manage files through an elegant web interface. Drag-and-drop uploads, media previews, and sharing.',
|
||||
state: 'running',
|
||||
lanPort: 8083,
|
||||
icon: '/assets/img/app-icons/file-browser.webp',
|
||||
}),
|
||||
}
|
||||
|
||||
function mergePackageData(dockerApps) {
|
||||
@@ -983,12 +1028,12 @@ app.post('/rpc/v1', (req, res) => {
|
||||
}
|
||||
|
||||
case 'package.install': {
|
||||
const { id, url } = params
|
||||
|
||||
installPackage(id, url).catch(err => {
|
||||
const { id, url, dockerImage, version } = params
|
||||
|
||||
installPackage(id, url, { dockerImage, version }).catch(err => {
|
||||
console.error(`[RPC] Installation failed:`, err.message)
|
||||
})
|
||||
|
||||
|
||||
return res.json({ result: `job-${Date.now()}` })
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user