2026-03-22 03:30:21 +00:00
/** Static configuration for the Apps view */
import type { Ref } from 'vue'
import { computed } from 'vue'
import { PackageState , type PackageDataEntry } from '@/types/api'
// Service container name patterns (backend/infra, not user-facing)
export const SERVICE_NAMES = new Set ( [
'dwn' , 'archy-mempool-db' , 'archy-btcpay-db' , 'archy-nbxplorer' , 'archy-tor' ,
'immich_postgres' , 'immich_redis' ,
'mysql-mempool' , 'mempool-api' , 'archy-mempool-web' ,
'archy-bitcoin-ui' , 'archy-lnd-ui' , 'archy-electrs-ui' ,
'indeedhub-postgres' , 'indeedhub-redis' , 'indeedhub-minio' ,
2026-03-31 11:06:19 +01:00
'indeedhub-api' , 'indeedhub-ffmpeg' ,
2026-03-22 03:30:21 +00:00
'indeedhub-relay' , 'indeedhub-build_api_1' , 'indeedhub-build_ffmpeg-worker_1' ,
'indeedhub-build_postgres_1' , 'indeedhub-build_redis_1' , 'indeedhub-build_minio_1' ,
'indeedhub-build_minio-init_1' , 'indeedhub-build_relay_1' ,
2026-03-30 13:35:02 +01:00
// L484 web-only apps — parked in Services for now
2026-04-11 19:59:36 +01:00
'nwnn' , '484-kitchen' , 'call-the-operator' ,
2026-03-30 13:35:02 +01:00
'syntropy-institute' , 't-zero' , 'arch-presentation' ,
2026-03-22 03:30:21 +00:00
] )
export function isServiceContainer ( id : string ) : boolean {
if ( SERVICE_NAMES . has ( id ) ) return true
if ( id . startsWith ( 'indeedhub-build_' ) ) return true
if ( id . startsWith ( 'archy-' ) ) return true
if ( id . endsWith ( '_db' ) || id . endsWith ( '-db' ) ) return true
return false
}
// 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' ,
'lnd' : 'money' , 'mempool' : 'money' , 'mempool-web' : 'money' , 'btcpay-server' : 'commerce' ,
'fedimint' : 'money' , 'fedimint-gateway' : 'money' ,
'indeedhub' : 'media' , 'jellyfin' : 'media' , 'photoprism' : 'media' , 'immich' : 'media' ,
2026-04-02 18:20:52 +01:00
'nextcloud' : 'data' , 'vaultwarden' : 'data' , 'filebrowser' : 'data' , 'cryptpad' : 'data' ,
2026-03-22 03:30:21 +00:00
'homeassistant' : 'home' , 'lorabell' : 'home' , 'endurain' : 'home' ,
'searxng' : 'community' , 'ollama' : 'community' , 'grafana' : 'data' ,
release(v1.7.38-alpha): onboarding auto-heal + silent returning logins + app-store trim
- auth.rs now infers onboarding-complete from setup_complete + password_hash so
nodes stop bouncing users through the intro wizard after browser clear / update
/ reboot; the flag self-heals to disk on next check
- frontend: "backend uncertain" no longer defaults to /onboarding/intro —
useOnboarding returns null + callers poll / retry instead of flashing the wizard
- login sounds (synthwave, welcome voice, pop, whoosh, oomph) gated by
isFirstInstallPhase(); typing sounds unaffected
- removed FIPS app, Nostr Relay, Nostr VPN, Routstr, Penpot from catalog,
frontend config, Rust AppMetadata + install dispatch + install_penpot_stack;
docker/fips-ui + docker/nostr-vpn-ui + apps/penpot dirs and 5 icons deleted;
15 image versions deleted from tx1138, .168, gitea-local registries (.160
Gitea was 502 at release time — follow-up)
- AIUI baked into frontend release tarball via demo/aiui/; deploy-to-target
falls back to demo/aiui/ when the AIUI sibling checkout is missing
- prebuild hook syncs app-catalog/catalog.json → public/catalog.json so the
two copies can no longer drift (was the source of the "apps still visible"
bug — public/ had stale data)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 13:02:24 -04:00
'nostrudel' : 'nostr' ,
2026-03-22 03:30:21 +00:00
'tailscale' : 'networking' , 'nginx-proxy-manager' : 'networking' , 'portainer' : 'networking' ,
'uptime-kuma' : 'networking' , 'dwn' : 'data' ,
2026-04-11 19:59:36 +01:00
'botfights' : 'community' , 'nwnn' : 'l484' , '484-kitchen' : 'l484' ,
2026-03-22 03:30:21 +00:00
'call-the-operator' : 'l484' , 'syntropy-institute' : 'l484' , 't-zero' : 'l484' ,
}
export function getAppCategory ( id : string , pkg : PackageDataEntry ) : string {
if ( APP_CATEGORY_MAP [ id ] ) return APP_CATEGORY_MAP [ id ]
const cat = ( pkg . manifest as unknown as Record < string , unknown > ) ? . category as string | undefined
return cat || 'other'
}
// Web-only app IDs and their URLs
export const WEB_ONLY_APP_URLS : Record < string , string > = {
'nwnn' : 'https://nwnn.l484.com' ,
'484-kitchen' : 'https://484.kitchen' ,
'call-the-operator' : 'https://cta.tx1138.com' ,
'syntropy-institute' : 'https://syntropy.institute' ,
't-zero' : 'https://teeminuszero.net' ,
}
export function isWebOnlyApp ( id : string ) : boolean {
return id in WEB_ONLY_APP_URLS
}
// Web-only apps (no container) -- always show as installed bookmarks
export const WEB_ONLY_APPS : Record < string , PackageDataEntry > = {
'nwnn' : {
state : 'running' as PackageState ,
manifest : { id : 'nwnn' , title : 'Next Web News Network' , version : '1.0.0' , description : { short : 'Decentralized news aggregator, synced from Telegram' , long : '' } , 'release-notes' : '' , license : '' , 'wrapper-repo' : '' , 'upstream-repo' : '' , 'support-site' : '' , 'marketing-site' : '' , 'donation-url' : null } ,
'static-files' : { license : '' , instructions : '' , icon : '/assets/img/app-icons/nwnn.png' } ,
} ,
'484-kitchen' : {
state : 'running' as PackageState ,
manifest : { id : '484-kitchen' , title : '484 Kitchen' , version : '1.0.0' , description : { short : 'K484 application platform' , long : '' } , 'release-notes' : '' , license : '' , 'wrapper-repo' : '' , 'upstream-repo' : '' , 'support-site' : '' , 'marketing-site' : '' , 'donation-url' : null } ,
'static-files' : { license : '' , instructions : '' , icon : '/assets/img/app-icons/484-kitchen.png' } ,
} ,
'call-the-operator' : {
state : 'running' as PackageState ,
manifest : { id : 'call-the-operator' , title : 'Call the Operator' , version : '1.0.0' , description : { short : 'Escape the Matrix — explore decentralized alternatives' , long : '' } , 'release-notes' : '' , license : '' , 'wrapper-repo' : '' , 'upstream-repo' : '' , 'support-site' : '' , 'marketing-site' : '' , 'donation-url' : null } ,
'static-files' : { license : '' , instructions : '' , icon : '/assets/img/app-icons/call-the-operator.png' } ,
} ,
'syntropy-institute' : {
state : 'running' as PackageState ,
manifest : { id : 'syntropy-institute' , title : 'Syntropy Institute' , version : '1.0.0' , description : { short : 'Medicine Reimagined — frequency analysis-therapy' , long : '' } , 'release-notes' : '' , license : '' , 'wrapper-repo' : '' , 'upstream-repo' : '' , 'support-site' : '' , 'marketing-site' : '' , 'donation-url' : null } ,
'static-files' : { license : '' , instructions : '' , icon : '/assets/img/app-icons/syntropy-institute.png' } ,
} ,
't-zero' : {
state : 'running' as PackageState ,
manifest : { id : 't-zero' , title : 'T-0' , version : '1.0.0' , description : { short : 'Documentary series on decentralization and Bitcoin' , long : '' } , 'release-notes' : '' , license : '' , 'wrapper-repo' : '' , 'upstream-repo' : '' , 'support-site' : '' , 'marketing-site' : '' , 'donation-url' : null } ,
'static-files' : { license : '' , instructions : '' , icon : '/assets/img/app-icons/t-zero.png' } ,
} ,
}
/** 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' ,
release(v1.7.38-alpha): onboarding auto-heal + silent returning logins + app-store trim
- auth.rs now infers onboarding-complete from setup_complete + password_hash so
nodes stop bouncing users through the intro wizard after browser clear / update
/ reboot; the flag self-heals to disk on next check
- frontend: "backend uncertain" no longer defaults to /onboarding/intro —
useOnboarding returns null + callers poll / retry instead of flashing the wizard
- login sounds (synthwave, welcome voice, pop, whoosh, oomph) gated by
isFirstInstallPhase(); typing sounds unaffected
- removed FIPS app, Nostr Relay, Nostr VPN, Routstr, Penpot from catalog,
frontend config, Rust AppMetadata + install dispatch + install_penpot_stack;
docker/fips-ui + docker/nostr-vpn-ui + apps/penpot dirs and 5 icons deleted;
15 image versions deleted from tx1138, .168, gitea-local registries (.160
Gitea was 502 at release time — follow-up)
- AIUI baked into frontend release tarball via demo/aiui/; deploy-to-target
falls back to demo/aiui/ when the AIUI sibling checkout is missing
- prebuild hook syncs app-catalog/catalog.json → public/catalog.json so the
two copies can no longer drift (was the source of the "apps still visible"
bug — public/ had stale data)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 13:02:24 -04:00
'cryptpad' , 'nginx-proxy-manager' , 'tailscale' ,
2026-03-22 03:30:21 +00:00
] )
export function opensInTab ( id : string ) : boolean {
return TAB_LAUNCH_APPS . has ( id )
}
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
const canLaunchState = pkg . state === 'running' || pkg . state === 'starting'
return ! ! hasUI && canLaunchState
}
fix: overhaul container lifecycle — recovery, health, uninstall, UI state
Container recovery:
- Health monitor: MAX_RESTART_ATTEMPTS 3→10, interval 60s→120s
- Dependency-aware restarts: won't restart services before their deps
- Reset dependent counters when a dependency recovers
- Handle "created" state containers (were invisible to health monitor)
- Added IndeedHub, mempool-api, mysql to tier system
- Crash recovery: podman start timeout 30s→120s with retry
- Podman client: socket timeout 5s→30s, added restart policy
UI state representation:
- Exit code 0 shows "stopped" (gray), not "crashed" (red)
- Exit code 137 shows "killed (OOM)"
- Non-zero exit shows "crashed" (red)
- Added exit_code field to PackageDataEntry
Install/uninstall fixes:
- Install returns error when container doesn't start (was silent success)
- Post-install hooks awaited instead of fire-and-forget tokio::spawn
- Uninstall: graceful rm before force, volume prune, network cleanup
- Uninstall returns error on partial failure (was 200 OK)
Config consistency:
- DB passwords read from /var/lib/archipelago/secrets/ (was hardcoded)
- Bitcoin: added ZMQ ports 28332/28333 for LND block notifications
- IndeedHub port 7777→8190 (was conflicting with strfry)
- Marketplace versions: LND 0.17.4→0.18.4, Mempool 2.5.0→3.0.0
Performance:
- Metrics collector interval 60s→300s (was duplicating health monitor)
- Podman client: proper error propagation instead of unwrap_or_default
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 07:03:57 +01:00
export function getStatusClass ( state : PackageState , health? : string | null , exitCode? : number | null ) : string {
2026-03-22 03:30:21 +00:00
if ( state === PackageState . Running && health === 'starting' ) return 'bg-yellow-500/20 text-yellow-200'
if ( state === PackageState . Running && health === 'unhealthy' ) return 'bg-orange-500/20 text-orange-200'
switch ( state ) {
case PackageState . Running :
return 'bg-green-500/20 text-green-200'
case PackageState . Stopped :
return 'bg-gray-500/20 text-gray-200'
case PackageState . Exited :
fix: overhaul container lifecycle — recovery, health, uninstall, UI state
Container recovery:
- Health monitor: MAX_RESTART_ATTEMPTS 3→10, interval 60s→120s
- Dependency-aware restarts: won't restart services before their deps
- Reset dependent counters when a dependency recovers
- Handle "created" state containers (were invisible to health monitor)
- Added IndeedHub, mempool-api, mysql to tier system
- Crash recovery: podman start timeout 30s→120s with retry
- Podman client: socket timeout 5s→30s, added restart policy
UI state representation:
- Exit code 0 shows "stopped" (gray), not "crashed" (red)
- Exit code 137 shows "killed (OOM)"
- Non-zero exit shows "crashed" (red)
- Added exit_code field to PackageDataEntry
Install/uninstall fixes:
- Install returns error when container doesn't start (was silent success)
- Post-install hooks awaited instead of fire-and-forget tokio::spawn
- Uninstall: graceful rm before force, volume prune, network cleanup
- Uninstall returns error on partial failure (was 200 OK)
Config consistency:
- DB passwords read from /var/lib/archipelago/secrets/ (was hardcoded)
- Bitcoin: added ZMQ ports 28332/28333 for LND block notifications
- IndeedHub port 7777→8190 (was conflicting with strfry)
- Marketplace versions: LND 0.17.4→0.18.4, Mempool 2.5.0→3.0.0
Performance:
- Metrics collector interval 60s→300s (was duplicating health monitor)
- Podman client: proper error propagation instead of unwrap_or_default
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 07:03:57 +01:00
// Exit code 0 = clean shutdown (gray), non-zero = crash (red)
return exitCode != null && exitCode !== 0
? 'bg-red-500/20 text-red-200'
: 'bg-gray-500/20 text-gray-200'
2026-03-22 03:30:21 +00:00
case PackageState . Starting :
case PackageState . Stopping :
case PackageState . Restarting :
return 'bg-yellow-500/20 text-yellow-200'
case PackageState . Installing :
return 'bg-blue-500/20 text-blue-200'
2026-04-09 11:47:35 +02:00
case PackageState . Updating :
return 'bg-orange-500/20 text-orange-200'
2026-03-22 03:30:21 +00:00
default :
return 'bg-gray-500/20 text-gray-200'
}
}
fix: overhaul container lifecycle — recovery, health, uninstall, UI state
Container recovery:
- Health monitor: MAX_RESTART_ATTEMPTS 3→10, interval 60s→120s
- Dependency-aware restarts: won't restart services before their deps
- Reset dependent counters when a dependency recovers
- Handle "created" state containers (were invisible to health monitor)
- Added IndeedHub, mempool-api, mysql to tier system
- Crash recovery: podman start timeout 30s→120s with retry
- Podman client: socket timeout 5s→30s, added restart policy
UI state representation:
- Exit code 0 shows "stopped" (gray), not "crashed" (red)
- Exit code 137 shows "killed (OOM)"
- Non-zero exit shows "crashed" (red)
- Added exit_code field to PackageDataEntry
Install/uninstall fixes:
- Install returns error when container doesn't start (was silent success)
- Post-install hooks awaited instead of fire-and-forget tokio::spawn
- Uninstall: graceful rm before force, volume prune, network cleanup
- Uninstall returns error on partial failure (was 200 OK)
Config consistency:
- DB passwords read from /var/lib/archipelago/secrets/ (was hardcoded)
- Bitcoin: added ZMQ ports 28332/28333 for LND block notifications
- IndeedHub port 7777→8190 (was conflicting with strfry)
- Marketplace versions: LND 0.17.4→0.18.4, Mempool 2.5.0→3.0.0
Performance:
- Metrics collector interval 60s→300s (was duplicating health monitor)
- Podman client: proper error propagation instead of unwrap_or_default
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 07:03:57 +01:00
export function getStatusLabel ( state : PackageState , health? : string | null , exitCode? : number | null ) : string {
2026-03-22 03:30:21 +00:00
if ( state === PackageState . Running && health === 'starting' ) return 'starting up'
if ( state === PackageState . Running && health === 'unhealthy' ) return 'unhealthy'
if ( state === PackageState . Running && health === 'healthy' ) return 'healthy'
2026-04-09 11:47:35 +02:00
if ( state === PackageState . Updating ) return 'updating...'
2026-03-31 23:01:51 +01:00
if ( state === PackageState . Running ) return 'running'
if ( state === PackageState . Exited || state === PackageState . Stopped ) {
fix: overhaul container lifecycle — recovery, health, uninstall, UI state
Container recovery:
- Health monitor: MAX_RESTART_ATTEMPTS 3→10, interval 60s→120s
- Dependency-aware restarts: won't restart services before their deps
- Reset dependent counters when a dependency recovers
- Handle "created" state containers (were invisible to health monitor)
- Added IndeedHub, mempool-api, mysql to tier system
- Crash recovery: podman start timeout 30s→120s with retry
- Podman client: socket timeout 5s→30s, added restart policy
UI state representation:
- Exit code 0 shows "stopped" (gray), not "crashed" (red)
- Exit code 137 shows "killed (OOM)"
- Non-zero exit shows "crashed" (red)
- Added exit_code field to PackageDataEntry
Install/uninstall fixes:
- Install returns error when container doesn't start (was silent success)
- Post-install hooks awaited instead of fire-and-forget tokio::spawn
- Uninstall: graceful rm before force, volume prune, network cleanup
- Uninstall returns error on partial failure (was 200 OK)
Config consistency:
- DB passwords read from /var/lib/archipelago/secrets/ (was hardcoded)
- Bitcoin: added ZMQ ports 28332/28333 for LND block notifications
- IndeedHub port 7777→8190 (was conflicting with strfry)
- Marketplace versions: LND 0.17.4→0.18.4, Mempool 2.5.0→3.0.0
Performance:
- Metrics collector interval 60s→300s (was duplicating health monitor)
- Podman client: proper error propagation instead of unwrap_or_default
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 07:03:57 +01:00
if ( exitCode === 137 ) return 'killed (OOM)'
if ( exitCode != null && exitCode !== 0 ) return 'crashed'
return 'stopped'
}
2026-03-22 03:30:21 +00:00
return state
}
export function buildAllCategories ( t : ( key : string ) = > string ) {
return [
{ id : 'all' , name : t ( 'marketplace.all' ) } ,
{ id : 'community' , name : t ( 'marketplace.community' ) } ,
{ id : 'nostr' , name : 'Nostr' } ,
{ id : 'commerce' , name : t ( 'marketplace.commerce' ) } ,
{ id : 'money' , name : t ( 'marketplace.money' ) } ,
{ id : 'data' , name : t ( 'marketplace.data' ) } ,
{ id : 'media' , name : 'Media' } ,
{ id : 'home' , name : t ( 'marketplace.homeCategory' ) } ,
{ id : 'networking' , name : t ( 'marketplace.networking' ) } ,
{ id : 'l484' , name : 'L484' } ,
{ id : 'other' , name : t ( 'marketplace.other' ) } ,
]
}
export function useCategoriesWithApps (
packages : Ref < Record < string , PackageDataEntry > > ,
allCategories : Ref < Array < { id : string ; name : string } > > ,
) {
return computed ( ( ) = > {
const entries = Object . entries ( packages . value ) . filter ( ( [ id ] ) = > ! isServiceContainer ( id ) )
return allCategories . value . filter ( cat = > {
if ( cat . id === 'all' ) return true
return entries . some ( ( [ id , pkg ] ) = > getAppCategory ( id , pkg ) === cat . id )
} )
} )
}
export function handleImageError ( e : Event ) {
const target = e . target as HTMLImageElement
const currentSrc = target . src
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)" / >
< path d = "M32 20L40 28H36V40H28V28H24L32 20Z" fill = "rgba(255,255,255,0.6)" / >
< path d = "M20 44H44V48H20V44Z" fill = "rgba(255,255,255,0.4)" / >
< / svg >
` )} `
if ( ! currentSrc . includes ( 'data:image' ) ) {
target . src = placeholderSvg
}
}