feat(orchestrator): complete container migration and release hardening

This commit is contained in:
archipelago
2026-04-28 15:00:58 -04:00
parent 4d05705315
commit 8f83b37d51
94 changed files with 5034 additions and 1003 deletions
+47 -3
View File
@@ -10,6 +10,7 @@ export const SERVICE_NAMES = new Set([
'immich_postgres', 'immich_redis',
'mysql-mempool', 'mempool-api', 'archy-mempool-web',
'archy-bitcoin-ui', 'archy-lnd-ui', 'archy-electrs-ui',
'bitcoin-ui', 'lnd-ui', 'electrs-ui',
'indeedhub-postgres', 'indeedhub-redis', 'indeedhub-minio',
'indeedhub-api', 'indeedhub-ffmpeg',
'indeedhub-relay', 'indeedhub-build_api_1', 'indeedhub-build_ffmpeg-worker_1',
@@ -28,6 +29,12 @@ export function isServiceContainer(id: string): boolean {
return false
}
export function isServicePackage(id: string, pkg?: PackageDataEntry): boolean {
if (isServiceContainer(id)) return true
const manifestId = pkg?.manifest?.id
return !!manifestId && isServiceContainer(manifestId)
}
// Known app -> category mappings (matches App Store categorisation)
export const APP_CATEGORY_MAP: Record<string, string> = {
'bitcoin-knots': 'money', 'bitcoin-ui': 'money', 'electrumx': 'money', 'electrs': 'money',
@@ -50,6 +57,21 @@ export function getAppCategory(id: string, pkg: PackageDataEntry): string {
return cat || 'other'
}
export function filterEntriesForTab(
entries: Array<[string, PackageDataEntry]>,
activeTab: 'apps' | 'services',
selectedCategory: string,
): Array<[string, PackageDataEntry]> {
return entries.filter(([id, pkg]) => {
const isSvc = isServicePackage(id, pkg)
if (activeTab === 'services' ? !isSvc : isSvc) return false
if (activeTab === 'apps' && selectedCategory !== 'all') {
return getAppCategory(id, pkg) === selectedCategory
}
return true
})
}
// Web-only app IDs and their URLs
export const WEB_ONLY_APP_URLS: Record<string, string> = {
'nwnn': 'https://nwnn.l484.com',
@@ -95,7 +117,7 @@ export const WEB_ONLY_APPS: Record<string, PackageDataEntry> = {
/** Apps that open in a new browser tab (X-Frame-Options blocks iframe) */
export const TAB_LAUNCH_APPS = new Set([
'btcpay-server', 'grafana', 'photoprism', 'homeassistant',
'vaultwarden', 'nextcloud', 'uptime-kuma', 'portainer',
'vaultwarden', 'nextcloud', 'uptime-kuma', 'portainer', 'gitea',
'cryptpad', 'nginx-proxy-manager', 'tailscale',
])
@@ -103,6 +125,21 @@ export function opensInTab(id: string): boolean {
return TAB_LAUNCH_APPS.has(id)
}
export function resolveAppIcon(id: string, pkg: PackageDataEntry, curatedIcon?: string): string {
const icon = (pkg["static-files"]?.icon || "").trim()
if (
icon.startsWith("/") ||
icon.startsWith("http://") ||
icon.startsWith("https://") ||
icon.startsWith("data:image")
) {
return icon
}
return curatedIcon || `/assets/img/app-icons/${id}.png`
}
export function canLaunch(pkg: PackageDataEntry): boolean {
if (isWebOnlyApp(pkg.manifest.id)) return true
const hasUI = pkg.manifest.interfaces?.main?.ui || pkg.installed?.['interface-addresses']?.main
@@ -171,7 +208,7 @@ export function useCategoriesWithApps(
allCategories: Ref<Array<{ id: string; name: string }>>,
) {
return computed(() => {
const entries = Object.entries(packages.value).filter(([id]) => !isServiceContainer(id))
const entries = Object.entries(packages.value).filter(([id, pkg]) => !isServicePackage(id, pkg))
return allCategories.value.filter(cat => {
if (cat.id === 'all') return true
return entries.some(([id, pkg]) => getAppCategory(id, pkg) === cat.id)
@@ -182,6 +219,13 @@ export function useCategoriesWithApps(
export function handleImageError(e: Event) {
const target = e.target as HTMLImageElement
const currentSrc = target.src
if (target.dataset.fallbackTried !== "1" && currentSrc.endsWith(".png")) {
target.dataset.fallbackTried = "1"
target.src = currentSrc.replace(/\.png($|\?)/, ".svg$1")
return
}
const placeholderSvg = `data:image/svg+xml,${encodeURIComponent(`
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="64" height="64" rx="12" fill="rgba(255,255,255,0.1)"/>
@@ -189,7 +233,7 @@ export function handleImageError(e: Event) {
<path d="M20 44H44V48H20V44Z" fill="rgba(255,255,255,0.4)"/>
</svg>
`)}`
if (!currentSrc.includes('data:image')) {
if (!currentSrc.includes("data:image")) {
target.src = placeholderSvg
}
}