feat(orchestrator): complete container migration and release hardening
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user