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:
Dorian
2026-03-09 17:09:59 +00:00
co-authored by Claude Opus 4.6
parent 9c7ffbb263
commit a2aa9657b1
24 changed files with 1200 additions and 141 deletions
+105 -60
View File
@@ -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()}` })
}