2026-01-24 22:59:20 +00:00
< template >
2026-03-13 23:40:29 +00:00
< div class = "marketplace-container" >
<!-- Header Section -- >
< div >
2026-02-03 21:43:33 +00:00
<!-- Installation Progress Banner - Multiple Apps -- >
2026-03-11 13:04:31 +00:00
< div v-if = "installingApps.size > 0" aria-live="polite" class="mb-6 space-y-3" >
2026-02-03 21:43:33 +00:00
< div
v - for = "[appId, progress] in installingApps"
: key = "appId"
class = "glass-card p-4 border-l-4"
: class = " {
'border-blue-500' : progress . status === 'downloading' || progress . status === 'installing' ,
'border-orange-500' : progress . status === 'starting' ,
'border-green-500' : progress . status === 'complete' ,
'border-red-500' : progress . status === 'error'
} "
>
< div class = "flex items-center justify-between mb-3" >
< div class = "flex items-center gap-3" >
2026-03-11 13:04:31 +00:00
< svg
2026-02-03 21:43:33 +00:00
v - if = "progress.status !== 'complete' && progress.status !== 'error'"
2026-03-11 13:04:31 +00:00
class = "animate-spin h-5 w-5 text-blue-400"
aria - hidden = "true"
xmlns = "http://www.w3.org/2000/svg"
fill = "none"
2026-02-03 21:43:33 +00:00
viewBox = "0 0 24 24"
>
< circle class = "opacity-25" cx = "12" cy = "12" r = "10" stroke = "currentColor" stroke -width = " 4 " > < / circle >
< path class = "opacity-75" fill = "currentColor" d = "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" > < / path >
< / svg >
< svg
v - else - if = "progress.status === 'complete'"
class = "h-5 w-5 text-green-400"
xmlns = "http://www.w3.org/2000/svg"
fill = "none"
viewBox = "0 0 24 24"
stroke = "currentColor"
>
< path stroke -linecap = " round " stroke -linejoin = " round " stroke -width = " 2 " d = "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" / >
< / svg >
< svg
v - else
class = "h-5 w-5 text-red-400"
xmlns = "http://www.w3.org/2000/svg"
fill = "none"
viewBox = "0 0 24 24"
stroke = "currentColor"
>
< path stroke -linecap = " round " stroke -linejoin = " round " stroke -width = " 2 " d = "M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" / >
< / svg >
< div >
< p class = "text-white font-medium" > { { progress . title } } < / p >
< p class = "text-white/70 text-sm" > { { progress . message } } < / p >
< / div >
< / div >
< div class = "text-white/60 text-sm" >
{ { progress . progress } } %
2026-01-24 22:59:20 +00:00
< / div >
< / div >
2026-02-03 21:43:33 +00:00
<!-- Progress Bar -- >
< div class = "w-full bg-white/10 rounded-full h-2 overflow-hidden" >
< div
class = "h-full rounded-full transition-all duration-500"
: class = " {
'bg-gradient-to-r from-blue-500 to-blue-400' : progress . status === 'downloading' || progress . status === 'installing' ,
'bg-gradient-to-r from-orange-500 to-orange-400' : progress . status === 'starting' ,
'bg-gradient-to-r from-green-500 to-green-400' : progress . status === 'complete' ,
'bg-gradient-to-r from-red-500 to-red-400' : progress . status === 'error'
} "
: style = "{ width: `${progress.progress}%` }"
> < / div >
2026-01-24 22:59:20 +00:00
< / div >
< / div >
< / div >
2026-03-14 17:12:41 +00:00
<!-- Desktop : tabs + categories + search in one row -- >
< div class = "hidden md:flex mb-4 items-center gap-4" >
2026-03-13 23:40:29 +00:00
< div class = "mode-switcher flex-shrink-0" >
< RouterLink to = "/dashboard/apps" class = "mode-switcher-btn" > My Apps < / RouterLink >
< RouterLink to = "/dashboard/marketplace" class = "mode-switcher-btn mode-switcher-btn-active" > App Store < / RouterLink >
feat: v1.2.0-alpha — E2E encrypted mesh relay, steganography, relay status polling
Phase 5 mesh networking:
- E2E encrypted TX relay (X25519 + ChaCha20-Poly1305) — non-Archy nodes
relay encrypted blobs transparently via Meshcore native routing
- Steganographic encoding modes (WeatherStation, SensorNetwork) — traffic
looks like sensor data on the wire, 0xAA marker, configurable per-node
- Pre-flight Bitcoin Core health check on relay node — specific error codes
(bitcoin_unreachable, bitcoin_syncing, tx_rejected) instead of generic fails
- mesh.relay-status RPC endpoint — frontend polls for relay result every 3s
- On-Chain / Lightning tabs in Off-Grid Bitcoin panel
- Archy Peers vs Mesh Broadcast relay mode selector
- Mesh view fills viewport (no page scroll), internal panel scrolling
- Version bump to 1.2.0-alpha
Also includes: deploy hardening, container fixes, IndeedHub updates,
boot screen, dashboard improvements, MASTER_PLAN task tracking
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 23:56:37 +00:00
< RouterLink to = "/dashboard/apps?tab=services" class = "mode-switcher-btn" > Services < / RouterLink >
2026-03-13 23:40:29 +00:00
< / div >
2026-03-14 17:12:41 +00:00
< div class = "mode-switcher flex-shrink-0" >
2026-02-17 15:03:34 +00:00
< button
v - for = "category in categoriesWithApps"
: key = "category.id"
2026-03-13 23:40:29 +00:00
@ click = "selectCategory(category.id)"
class = "mode-switcher-btn"
: class = "{ 'mode-switcher-btn-active': selectedCategory === category.id }"
2026-02-17 15:03:34 +00:00
>
{ { category . name } }
2026-03-13 23:40:29 +00:00
< span v-if = "category.id === 'nostr' && nostrApps.length > 0" class="ml-1 text-xs px-1.5 py-0.5 rounded-full bg-white/10" > + {{ nostrApps.length }} < / span >
2026-02-17 15:03:34 +00:00
< / button >
< / div >
< input
v - model = "searchQuery"
type = "text"
2026-03-11 13:45:59 +00:00
: placeholder = "t('marketplace.searchPlaceholder')"
: aria - label = "t('marketplace.searchApps')"
2026-03-14 17:12:41 +00:00
class = "flex-1 px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/50 focus:outline-none focus:border-white/40 transition-colors"
2026-02-17 15:03:34 +00:00
/ >
2026-01-24 22:59:20 +00:00
< / div >
feat: v1.2.0-alpha — E2E encrypted mesh relay, steganography, relay status polling
Phase 5 mesh networking:
- E2E encrypted TX relay (X25519 + ChaCha20-Poly1305) — non-Archy nodes
relay encrypted blobs transparently via Meshcore native routing
- Steganographic encoding modes (WeatherStation, SensorNetwork) — traffic
looks like sensor data on the wire, 0xAA marker, configurable per-node
- Pre-flight Bitcoin Core health check on relay node — specific error codes
(bitcoin_unreachable, bitcoin_syncing, tx_rejected) instead of generic fails
- mesh.relay-status RPC endpoint — frontend polls for relay result every 3s
- On-Chain / Lightning tabs in Off-Grid Bitcoin panel
- Archy Peers vs Mesh Broadcast relay mode selector
- Mesh view fills viewport (no page scroll), internal panel scrolling
- Version bump to 1.2.0-alpha
Also includes: deploy hardening, container fixes, IndeedHub updates,
boot screen, dashboard improvements, MASTER_PLAN task tracking
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 23:56:37 +00:00
<!-- Mobile : search ( tabs handled by Dashboard . vue header ) -- >
2026-03-13 23:40:29 +00:00
< div class = "md:hidden mb-4" >
2026-01-24 22:59:20 +00:00
< input
v - model = "searchQuery"
type = "text"
2026-03-11 13:45:59 +00:00
: placeholder = "t('marketplace.searchPlaceholder')"
: aria - label = "t('marketplace.searchApps')"
2026-03-05 10:14:10 +00:00
class = "w-full px-4 py-3 md:py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/50 focus:outline-none focus:border-white/40 transition-colors"
2026-01-24 22:59:20 +00:00
/ >
< / div >
< / div >
<!-- Scrollable Apps Section -- >
2026-03-13 23:40:29 +00:00
< div class = "pb-8" >
2026-01-24 22:59:20 +00:00
<!-- Apps Grid -- >
< div class = "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6" >
< div
2026-03-09 07:43:12 +00:00
v - for = "(app, index) in filteredApps"
2026-01-24 22:59:20 +00:00
: key = "app.id"
2026-02-17 20:40:26 +00:00
data - controller - container
: data - controller - install = "!(isInstalled(app.id) || installingApps.has(app.id)) && (app.source === 'local' || !!app.dockerImage) ? '1' : undefined"
tabindex = "0"
2026-03-11 13:11:45 +00:00
role = "link"
2026-03-09 07:43:12 +00:00
class = "glass-card card-stagger p-6 hover:bg-white/10 transition-all cursor-pointer flex flex-col"
: style = "{ '--stagger-index': index }"
2026-01-24 22:59:20 +00:00
@ click = "viewAppDetails(app)"
2026-03-11 13:11:45 +00:00
@ keydown . enter = "viewAppDetails(app)"
2026-01-24 22:59:20 +00:00
>
< div class = "flex items-start gap-4 mb-4" >
< img
v - if = "app.icon"
: src = "app.icon"
: alt = "app.title"
class = "w-16 h-16 rounded-lg object-cover"
@ error = "handleImageError"
/ >
< div v -else class = "w-16 h-16 rounded-lg bg-white/10 flex items-center justify-center" >
< svg class = "w-8 h-8 text-white/40" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
< path stroke -linecap = " round " stroke -linejoin = " round " stroke -width = " 2 " d = "M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" / >
< / svg >
< / div >
< div class = "flex-1" >
2026-03-14 03:35:09 +00:00
< h3 class = "text-xl font-semibold text-white mb-1" >
{ { app . title } }
< span
v - if = "getAppTier(app.id) !== 'optional'"
class = "tier-badge"
: class = "getAppTier(app.id) === 'core' ? 'tier-badge-core' : 'tier-badge-recommended'"
> { { getAppTier ( app . id ) } } < / span >
< / h3 >
2026-01-24 22:59:20 +00:00
< p class = "text-sm text-white/60" > { { app . version ? ` v ${ app . version } ` : 'latest' } } < / p >
< p v-if = "app.author" class="text-xs text-white/50 mt-1" > by {{ app.author }} < / p >
< / div >
< / div >
2026-03-11 13:04:31 +00:00
<!-- Trust badge for Nostr community apps -- >
< div v-if = "app.trustTier" class="flex items-center gap-2 mb-3" >
< span
class = "text-xs px-2 py-0.5 rounded-full font-medium"
: class = " {
'bg-green-400/20 text-green-400' : app . trustTier === 'verified' ,
'bg-yellow-400/20 text-yellow-400' : app . trustTier === 'community' ,
'bg-orange-400/20 text-orange-400' : app . trustTier === 'unverified' ,
'bg-red-400/20 text-red-400' : app . trustTier === 'untrusted' ,
} "
> { { app . trustTier } } < / span >
< span class = "text-xs text-white/40" > Score : { { app . trustScore } } / 100 < / span >
< span v-if = "app.relayCount" class="text-xs text-white/40" > & middot ; {{ app.relayCount }} relay {{ app.relayCount ! = = 1 ? ' s ' : ' ' }} < / span >
< / div >
2026-01-24 22:59:20 +00:00
< p class = "text-white/80 text-sm mb-4 line-clamp-3 flex-1" >
{ { typeof app . description === 'object' ? app . description . short : ( app . description || 'No description available' ) } }
< / p >
< div class = "flex gap-2 mt-auto" >
2026-03-18 11:15:32 +00:00
<!-- Installed & starting up ( transitional state ) -- >
< span
v - if = "isInstalled(app.id) && isStartingUp(app.id)"
class = "flex-1 px-4 py-2 bg-yellow-500/15 border border-yellow-500/30 rounded-lg text-yellow-200 text-sm font-medium text-center cursor-default flex items-center justify-center gap-2"
>
< svg class = "animate-spin h-4 w-4" xmlns = "http://www.w3.org/2000/svg" fill = "none" viewBox = "0 0 24 24" >
< circle class = "opacity-25" cx = "12" cy = "12" r = "10" stroke = "currentColor" stroke -width = " 4 " > < / circle >
< path class = "opacity-75" fill = "currentColor" d = "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" > < / path >
< / svg >
{ { getInstalledState ( app . id ) === 'installing' ? 'Installing...' : 'Starting...' } }
< / span >
<!-- Installed & ready -- >
< span
v - else - if = "isInstalled(app.id)"
2026-03-13 23:40:29 +00:00
class = "flex-1 px-4 py-2 bg-white/20 rounded-lg text-white/60 text-sm font-medium text-center cursor-default"
2026-03-18 11:15:32 +00:00
>
{ { t ( 'marketplace.alreadyInstalled' ) } }
< / span >
2026-03-13 23:40:29 +00:00
< button
2026-03-18 11:15:32 +00:00
v - if = "isInstalled(app.id) && !isStartingUp(app.id)"
2026-03-13 23:40:29 +00:00
@ click . stop = "launchInstalledApp(app)"
class = "px-4 py-2 glass-button glass-button-sm rounded-lg text-sm font-medium"
>
{ { t ( 'common.launch' ) } }
< / button >
2026-03-18 11:46:38 +00:00
<!-- Not yet scanned — show loading indicator instead of install -- >
< span
v - else - if = "!containersScanned && (app.source === 'local' || app.dockerImage)"
class = "flex-1 px-4 py-2 rounded-lg text-white/50 text-sm font-medium text-center cursor-default relative overflow-hidden"
>
< span class = "marketplace-shimmer-bg" > < / span >
< span class = "relative flex items-center justify-center gap-2" >
< svg class = "animate-spin h-3.5 w-3.5 opacity-60" xmlns = "http://www.w3.org/2000/svg" fill = "none" viewBox = "0 0 24 24" >
< circle class = "opacity-25" cx = "12" cy = "12" r = "10" stroke = "currentColor" stroke -width = " 4 " > < / circle >
< path class = "opacity-75" fill = "currentColor" d = "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" > < / path >
< / svg >
Checking ...
< / span >
< / span >
2026-01-24 22:59:20 +00:00
< button
2026-03-18 11:15:32 +00:00
v - else - if = "!isInstalled(app.id) && (app.source === 'local' || app.dockerImage)"
2026-02-17 20:40:26 +00:00
data - controller - install - btn
2026-01-24 22:59:20 +00:00
@ click . stop = "app.source === 'local' ? installApp(app) : installCommunityApp(app)"
2026-03-18 11:15:32 +00:00
: disabled = "installingApps.has(app.id)"
2026-03-04 05:23:42 +00:00
class = "flex-1 px-4 py-2 glass-button glass-button-sm rounded-lg text-sm font-medium disabled:opacity-50 disabled:cursor-not-allowed"
2026-03-18 11:15:32 +00:00
>
< span v-if = "installingApps.has(app.id)" class="flex items-center justify-center gap-2" >
< svg class = "animate-spin h-4 w-4" xmlns = "http://www.w3.org/2000/svg" fill = "none" viewBox = "0 0 24 24" >
< circle class = "opacity-25" cx = "12" cy = "12" r = "10" stroke = "currentColor" stroke -width = " 4 " > < / circle >
< path class = "opacity-75" fill = "currentColor" d = "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" > < / path >
< / svg >
{ { installingApps . get ( app . id ) ? . message || t ( 'common.installing' ) } }
< / span >
< span v-else > {{ t ( ' common.install ' ) }} < / span >
2026-01-24 22:59:20 +00:00
< / button >
< button
2026-03-18 11:15:32 +00:00
v - else - if = "!isInstalled(app.id)"
2026-01-24 22:59:20 +00:00
disabled
class = "flex-1 px-4 py-2 bg-white/10 rounded-lg text-white/40 text-sm font-medium cursor-not-allowed"
>
2026-03-11 13:45:59 +00:00
{ { t ( 'common.notAvailable' ) } }
2026-01-24 22:59:20 +00:00
< / button >
< / div >
< / div >
< / div >
<!-- Empty State -- >
< div v-if = "filteredApps.length === 0" class="text-center py-12" >
2026-03-11 13:04:31 +00:00
< div v-if = "loadingCommunity || nostrLoading" class="flex flex-col items-center gap-4" >
2026-01-24 22:59:20 +00:00
< svg class = "animate-spin h-12 w-12 text-blue-400" xmlns = "http://www.w3.org/2000/svg" fill = "none" viewBox = "0 0 24 24" >
< circle class = "opacity-25" cx = "12" cy = "12" r = "10" stroke = "currentColor" stroke -width = " 4 " > < / circle >
< path class = "opacity-75" fill = "currentColor" d = "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" > < / path >
< / svg >
2026-03-13 23:40:29 +00:00
< p class = "text-white/70" > { { nostrLoading ? t ( 'marketplace.queryingRelays' ) : t ( 'common.loading' ) } } < / p >
2026-03-11 13:04:31 +00:00
< / div >
2026-03-13 23:40:29 +00:00
< div v -else -if = " nostrError & & selectedCategory = = = ' nostr ' " class = "flex flex-col items-center gap-4" >
2026-03-11 13:45:59 +00:00
< p class = "text-white/70" > { { t ( 'marketplace.noCommunityApps' ) } } < / p >
2026-03-11 13:04:31 +00:00
< p class = "text-white/40 text-sm" > { { nostrError } } < / p >
2026-03-11 13:45:59 +00:00
< button @ click = "nostrApps = []; loadNostrMarketplace()" class = "px-4 py-2 glass-button rounded-lg text-sm" > { { t ( 'common.retry' ) } } < / button >
2026-01-24 22:59:20 +00:00
< / div >
2026-03-11 13:45:59 +00:00
< p v -else class = "text-white/70" > { { searchQuery && selectedCategory !== 'all' ? t ( 'marketplace.noResults' , { category : categories . find ( c => c . id === selectedCategory ) ? . name , query : searchQuery } ) : searchQuery ? t ( 'marketplace.noResultsSearch' , { query : searchQuery } ) : t ( 'marketplace.noResultsCategory' , { category : categories . find ( c => c . id === selectedCategory ) ? . name } ) } } < / p >
2026-01-24 22:59:20 +00:00
< / div >
< / div >
<!-- End Scrollable Apps Section -- >
2026-03-05 10:14:10 +00:00
<!-- Floating Filter Button ( teleported to escape CSS transform containing block ) -- >
< Teleport to = "body" >
< button
@ click = "showFilterModal = true"
class = "md:hidden fixed right-4 z-40 w-14 h-14 rounded-full glass-button flex items-center justify-center shadow-2xl mobile-back-btn"
style = "left: auto;"
>
< svg class = "w-6 h-6" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
< path stroke -linecap = " round " stroke -linejoin = " round " stroke -width = " 2 " d = "M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" / >
< / svg >
< / button >
< / Teleport >
2026-01-24 22:59:20 +00:00
<!-- Filter Modal ( Mobile only ) -- >
< Transition name = "modal" >
< div
v - if = "showFilterModal"
2026-03-14 17:12:41 +00:00
class = "fixed inset-0 z-50 flex items-end justify-center md:hidden bg-black/10 backdrop-blur-md"
2026-02-17 22:10:38 +00:00
@ click . self = "closeFilterModal()"
2026-01-24 22:59:20 +00:00
>
2026-02-17 22:10:38 +00:00
< div ref = "filterModalRef" class = "glass-card p-6 w-full rounded-t-3xl max-h-[80vh] overflow-y-auto" >
2026-01-24 22:59:20 +00:00
<!-- Header -- >
< div class = "flex items-center justify-between mb-6" >
2026-03-11 13:45:59 +00:00
< h2 class = "text-2xl font-bold text-white" > { { t ( 'marketplace.filterByCategory' ) } } < / h2 >
2026-01-24 22:59:20 +00:00
< button
2026-02-17 22:10:38 +00:00
@ click = "closeFilterModal()"
2026-01-24 22:59:20 +00:00
class = "text-white/60 hover:text-white transition-colors"
>
< svg class = "w-6 h-6" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
< path stroke -linecap = " round " stroke -linejoin = " round " stroke -width = " 2 " d = "M6 18L18 6M6 6l12 12" / >
< / svg >
< / button >
< / div >
<!-- Category Grid -- >
2026-03-09 07:43:12 +00:00
< div class = "grid grid-cols-1 sm:grid-cols-2 gap-3" >
2026-01-24 22:59:20 +00:00
< button
2026-02-17 15:03:34 +00:00
v - for = "category in categoriesWithApps"
2026-01-24 22:59:20 +00:00
: key = "category.id"
2026-03-13 23:40:29 +00:00
@ click = "selectCategory(category.id); closeFilterModal()"
2026-01-24 22:59:20 +00:00
: class = " [
'p-4 rounded-xl font-medium transition-all text-left' ,
selectedCategory === category . id
? 'bg-white/20 text-white border-2 border-white/40'
: 'glass-card text-white/80 hover:bg-white/10'
] "
>
< div class = "flex items-center gap-3" >
<!-- Category Icon -- >
< div class = "flex-shrink-0 w-10 h-10 rounded-lg bg-white/10 flex items-center justify-center" >
2026-02-01 18:46:35 +00:00
< svg v-if = "category.id === 'all'" class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" >
< path stroke -linecap = " round " stroke -linejoin = " round " stroke -width = " 2 " d = "M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" / >
< / svg >
< svg v -else -if = " category.id = = = ' community ' " class = "w-6 h-6" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
2026-01-24 22:59:20 +00:00
< path stroke -linecap = " round " stroke -linejoin = " round " stroke -width = " 2 " d = "M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" / >
< / svg >
2026-03-13 23:40:29 +00:00
< svg v -else -if = " category.id = = = ' nostr ' " class = "w-6 h-6" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
< path stroke -linecap = " round " stroke -linejoin = " round " stroke -width = " 2 " d = "M13 10V3L4 14h7v7l9-11h-7z" / >
< / svg >
2026-01-24 22:59:20 +00:00
< svg v -else -if = " category.id = = = ' commerce ' " class = "w-6 h-6" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
< path stroke -linecap = " round " stroke -linejoin = " round " stroke -width = " 2 " d = "M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z" / >
< / svg >
< svg v -else -if = " category.id = = = ' money ' " class = "w-6 h-6" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
< path stroke -linecap = " round " stroke -linejoin = " round " stroke -width = " 2 " d = "M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" / >
< / svg >
< svg v -else -if = " category.id = = = ' data ' " class = "w-6 h-6" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
< path stroke -linecap = " round " stroke -linejoin = " round " stroke -width = " 2 " d = "M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" / >
< / svg >
< svg v -else -if = " category.id = = = ' home ' " class = "w-6 h-6" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
< path stroke -linecap = " round " stroke -linejoin = " round " stroke -width = " 2 " d = "M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" / >
< / svg >
< svg v -else -if = " category.id = = = ' car ' " class = "w-6 h-6" viewBox = "0 0 122.88 122.88" fill = "currentColor" >
< path d = "M61.44,0c33.93,0,61.44,27.51,61.44,61.44c0,33.93-27.51,61.44-61.44,61.44S0,95.37,0,61.44 C0,27.51,27.51,0,61.44,0L61.44,0z M61.17,61.6c1.76,0,3.18,1.42,3.18,3.18c0,1.76-1.42,3.18-3.18,3.18 c-1.76,0-3.18-1.42-3.18-3.18C57.99,63.03,59.42,61.6,61.17,61.6L61.17,61.6z M61.2,53.28c6.34,0,11.47,5.14,11.47,11.47 c0,6.34-5.14,11.47-11.47,11.47c-6.33,0-11.47-5.14-11.47-11.47C49.73,58.41,54.87,53.28,61.2,53.28L61.2,53.28z M14.78,44.57 c4.45-12.31,13.52-22.7,24.9-28.01c15.63-7.29,34.61-7.75,50.69,4.15c9.48,7.01,12.94,12.76,17.67,22.95 c3.58,9.03,0.64,11.97-10.87,6.9c-23.79-11.77-47.84-11.24-72.12,0C16.09,56.41,11.06,51.53,14.78,44.57L14.78,44.57z M75.9,109.05 c16.62-5.23,26.32-15.81,32.27-29.3c3.87-10.43-8.26-13.97-12.52-7.1c-2.55,5.06-5.59,9.4-9.55,12.77 c-6.2,5.27-15.18,6.23-16.58,16.16C68.79,106.74,69.97,111.38,75.9,109.05L75.9,109.05z M47.26,109.05 c-16.62-5.23-26.32-15.81-32.27-29.3c-3.87-10.43,8.26-13.97,12.52-7.1c2.55,5.06,5.59,9.4,9.55,12.77 c6.2,5.27,15.18,6.23,16.58,16.16C54.37,106.74,53.19,111.38,47.26,109.05L47.26,109.05z" / >
< / svg >
< svg v -else -if = " category.id = = = ' networking ' " class = "w-6 h-6" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
< path stroke -linecap = " round " stroke -linejoin = " round " stroke -width = " 2 " d = "M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" / >
< / svg >
< svg v -else class = "w-6 h-6" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
< path stroke -linecap = " round " stroke -linejoin = " round " stroke -width = " 2 " d = "M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" / >
< / svg >
< / div >
< div class = "flex-1" >
< p class = "font-semibold" > { { category . name } } < / p >
< p v-if = "selectedCategory === category.id" class="text-xs text-white/60 mt-1" > Currently viewing < / p >
< / div >
< svg v-if = "selectedCategory === category.id" class="w-5 h-5 text-white flex-shrink-0" fill="currentColor" viewBox="0 0 20 20" >
< path fill -rule = " evenodd " d = "M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip -rule = " evenodd " / >
< / svg >
< / div >
< / button >
< / div >
< / div >
< / div >
< / Transition >
< / div >
< / template >
< script setup lang = "ts" >
2026-03-11 13:04:31 +00:00
import { ref , computed , onMounted , onBeforeUnmount , watch } from 'vue'
2026-03-13 23:40:29 +00:00
import { useRouter , RouterLink } from 'vue-router'
2026-03-11 13:45:59 +00:00
import { useI18n } from 'vue-i18n'
2026-01-24 22:59:20 +00:00
import { useAppStore } from '@/stores/app'
import { rpcClient } from '@/api/rpc-client'
2026-03-11 13:04:31 +00:00
import { useMarketplaceApp , type MarketplaceAppInfo } from '@/composables/useMarketplaceApp'
2026-03-13 23:40:29 +00:00
import { useAppLauncherStore } from '@/stores/appLauncher'
2026-02-17 22:10:38 +00:00
import { useModalKeyboard } from '@/composables/useModalKeyboard'
2026-01-24 22:59:20 +00:00
2026-03-11 13:04:31 +00:00
type MarketplaceApp = Partial < MarketplaceAppInfo > & { id : string ; trustScore ? : number ; trustTier ? : string ; relayCount ? : number }
2026-01-24 22:59:20 +00:00
const router = useRouter ( )
const store = useAppStore ( )
2026-03-11 13:45:59 +00:00
const { t } = useI18n ( )
2026-01-24 22:59:20 +00:00
const { setCurrentApp } = useMarketplaceApp ( )
2026-03-13 23:40:29 +00:00
const appLauncher = useAppLauncherStore ( )
2026-01-24 22:59:20 +00:00
// Category state
2026-02-01 18:46:35 +00:00
const selectedCategory = ref ( 'all' )
2026-01-24 22:59:20 +00:00
2026-03-11 13:45:59 +00:00
const categories = computed ( ( ) => [
{ id : 'all' , name : t ( 'marketplace.all' ) } ,
{ id : 'community' , name : t ( 'marketplace.community' ) } ,
2026-03-13 23:40:29 +00:00
{ id : 'nostr' , name : 'Nostr' } ,
2026-03-11 13:45:59 +00:00
{ id : 'commerce' , name : t ( 'marketplace.commerce' ) } ,
{ id : 'money' , name : t ( 'marketplace.money' ) } ,
{ id : 'data' , name : t ( 'marketplace.data' ) } ,
{ id : 'home' , name : t ( 'marketplace.homeCategory' ) } ,
{ id : 'car' , name : t ( 'marketplace.auto' ) } ,
{ id : 'networking' , name : t ( 'marketplace.networking' ) } ,
2026-03-12 00:19:30 +00:00
{ id : 'l484' , name : 'L484' } ,
2026-03-11 13:45:59 +00:00
{ id : 'other' , name : t ( 'marketplace.other' ) }
] )
2026-01-24 22:59:20 +00:00
2026-02-03 21:43:33 +00:00
// Installation state - support multiple concurrent installations
interface InstallProgress {
id : string
title : string
status : 'downloading' | 'installing' | 'starting' | 'complete' | 'error'
progress : number // 0-100
message : string
attempt : number
}
const installingApps = ref < Map < string , InstallProgress > > ( new Map ( ) )
const maxAttempts = ref ( 60 )
2026-01-24 22:59:20 +00:00
2026-03-11 13:04:31 +00:00
// Watch WebSocket data for real install progress from backend
watch ( ( ) => store . packages , ( packages ) => {
if ( ! packages ) return
for ( const [ appId , pkg ] of Object . entries ( packages ) ) {
const progress = pkg [ 'install-progress' ]
if ( progress && pkg . state === 'installing' && installingApps . value . has ( appId ) ) {
const current = installingApps . value . get ( appId ) !
const pct = progress . size > 0 ? Math . round ( ( progress . downloaded / progress . size ) * 100 ) : 0
const downloadedMB = ( progress . downloaded / ( 1024 * 1024 ) ) . toFixed ( 1 )
const totalMB = ( progress . size / ( 1024 * 1024 ) ) . toFixed ( 1 )
installingApps . value . set ( appId , {
... current ,
status : 'downloading' ,
progress : Math . min ( pct , 95 ) ,
2026-03-11 13:45:59 +00:00
message : progress . size > 0 ? ` Downloading: ${ downloadedMB } / ${ totalMB } MB ( ${ pct } %) ` : t ( 'marketplace.downloading' ) ,
2026-03-11 13:04:31 +00:00
} )
}
}
} , { deep : true } )
2026-01-24 22:59:20 +00:00
// Filter modal state (for mobile)
const showFilterModal = ref ( false )
2026-02-17 22:10:38 +00:00
const filterModalRef = ref < HTMLElement | null > ( null )
const filterRestoreFocusRef = ref < HTMLElement | null > ( null )
function closeFilterModal ( ) {
filterRestoreFocusRef . value ? . focus ? . ( )
showFilterModal . value = false
}
useModalKeyboard ( filterModalRef , showFilterModal , closeFilterModal , { restoreFocusRef : filterRestoreFocusRef } )
2026-01-24 22:59:20 +00:00
2026-03-13 23:40:29 +00:00
// Select category and trigger Nostr relay discovery when 'nostr' is chosen
function selectCategory ( id : string ) {
selectedCategory . value = id
if ( id === 'nostr' && nostrApps . value . length === 0 && ! nostrLoading . value ) {
loadNostrMarketplace ( )
}
}
2026-03-11 13:04:31 +00:00
2026-01-24 22:59:20 +00:00
// Community marketplace state
const loadingCommunity = ref ( false )
const communityError = ref ( '' )
2026-03-11 13:04:31 +00:00
const communityApps = ref < MarketplaceApp [ ] > ( [ ] )
2026-01-24 22:59:20 +00:00
const searchQuery = ref ( '' )
2026-03-11 13:04:31 +00:00
// Nostr community marketplace state
const nostrApps = ref < ( MarketplaceApp & { trustScore ? : number ; trustTier ? : string ; relayCount ? : number } ) [ ] > ( [ ] )
const nostrLoading = ref ( false )
const nostrError = ref ( '' )
async function loadNostrMarketplace ( ) {
if ( nostrApps . value . length > 0 || nostrLoading . value ) return
nostrLoading . value = true
nostrError . value = ''
try {
const res = await rpcClient . marketplaceDiscover ( )
nostrApps . value = res . apps . map ( app => ( {
id : app . manifest . app _id ,
title : app . manifest . name ,
version : app . manifest . version ,
description : typeof app . manifest . description === 'string'
? app . manifest . description
: app . manifest . description ,
icon : app . manifest . icon _url || '' ,
author : app . manifest . author . name ,
dockerImage : app . manifest . container . image ,
repoUrl : app . manifest . repo _url ,
category : app . manifest . category ,
source : 'nostr' ,
trustScore : app . trust _score ,
trustTier : app . trust _tier ,
relayCount : app . relay _count ,
} ) )
} catch ( e ) {
nostrError . value = e instanceof Error ? e . message : 'Discovery failed'
if ( import . meta . env . DEV ) console . warn ( 'Nostr marketplace discovery failed:' , e )
} finally {
nostrLoading . value = false
}
}
2026-01-24 22:59:20 +00:00
// Available apps in marketplace
2026-02-01 18:46:35 +00:00
// const availableApps = ref([
// {
// id: 'atob',
// title: 'A to B Bitcoin',
// version: '0.1.0',
// icon: '/assets/img/atob.png',
// category: 'community',
// description: {
// short: 'Bitcoin tools and services for seamless transactions',
// long: 'A to B Bitcoin provides tools and services for Bitcoin transactions. Access the A to B platform through your Archipelago server with full privacy and control.'
// },
// s9pkUrl: '/packages/atob.s9pk'
// },
// {
// id: 'k484',
// title: 'K484',
// version: '0.1.0',
// icon: '/assets/img/k484.png',
// category: 'commerce',
// description: {
// short: 'Point of Sale and Admin system for Archipelago',
// long: 'K484 provides a complete POS and administration system for your Archipelago server. Choose between POS mode for transactions or Admin mode for management.'
// },
// s9pkUrl: '/packages/k484.s9pk'
// },
// ])
2026-01-24 22:59:20 +00:00
const installedPackages = computed ( ( ) => {
return store . data ? . [ 'package-data' ] || { }
} )
2026-03-18 11:46:38 +00:00
const containersScanned = computed ( ( ) => {
return store . data ? . [ 'server-info' ] ? . [ 'status-info' ] ? . [ 'containers-scanned' ] ? ? false
} )
2026-01-24 22:59:20 +00:00
// Function to categorize community apps based on their ID and description
2026-03-11 13:04:31 +00:00
function categorizeCommunityApp ( app : MarketplaceApp ) : string {
2026-03-12 00:19:30 +00:00
// If app already has a category set, use it
if ( app . category ) return app . category
2026-01-24 22:59:20 +00:00
const id = app . id . toLowerCase ( )
const title = app . title ? . toLowerCase ( ) || ''
2026-03-11 13:04:31 +00:00
const description = ( typeof app . description === 'string' ? app . description : app . description ? . short ? ? '' ) . toLowerCase ( )
2026-01-24 22:59:20 +00:00
const combined = ` ${ id } ${ title } ${ description } `
2026-03-12 00:19:30 +00:00
2026-01-24 22:59:20 +00:00
// Money category
if ( id . includes ( 'bitcoin' ) || id . includes ( 'btc' ) || id . includes ( 'lightning' ) ||
id . includes ( 'lnd' ) || id . includes ( 'cln' ) || id . includes ( 'electr' ) ||
id . includes ( 'fedimint' ) || id . includes ( 'cashu' ) || title . includes ( 'lightning' ) ||
combined . includes ( 'wallet' ) || combined . includes ( 'satoshi' ) ) {
return 'money'
}
// Commerce category
if ( id . includes ( 'btcpay' ) || id . includes ( 'commerce' ) || id . includes ( 'shop' ) ||
id . includes ( 'store' ) || id . includes ( 'pos' ) || id . includes ( 'payment' ) ||
combined . includes ( 'merchant' ) || combined . includes ( 'invoice' ) ) {
return 'commerce'
}
// Data category
if ( id . includes ( 'cloud' ) || id . includes ( 'nextcloud' ) || id . includes ( 'sync' ) ||
id . includes ( 'storage' ) || id . includes ( 'backup' ) || id . includes ( 'file' ) ||
id . includes ( 'photo' ) || id . includes ( 'immich' ) || id . includes ( 'jellyfin' ) ||
id . includes ( 'plex' ) || id . includes ( 'media' ) || id . includes ( 'vault' ) ||
combined . includes ( 'password manager' ) || combined . includes ( 'file storage' ) ) {
return 'data'
}
// Home category
if ( id . includes ( 'home-assistant' ) || id . includes ( 'homeassistant' ) ||
id . includes ( 'smart-home' ) || id . includes ( 'automation' ) || id . includes ( 'iot' ) ||
combined . includes ( 'home automation' ) || combined . includes ( 'smart home' ) ) {
return 'home'
}
2026-03-13 23:40:29 +00:00
// Nostr category
if ( id . includes ( 'nostr' ) || ( id . includes ( 'relay' ) && combined . includes ( 'nostr' ) ) ||
combined . includes ( 'nostr relay' ) || combined . includes ( 'nostr client' ) ) {
return 'nostr'
}
2026-01-24 22:59:20 +00:00
// Networking category
if ( id . includes ( 'vpn' ) || id . includes ( 'wireguard' ) || id . includes ( 'tailscale' ) ||
id . includes ( 'proxy' ) || id . includes ( 'dns' ) || id . includes ( 'pihole' ) ||
id . includes ( 'adguard' ) || id . includes ( 'nginx' ) || id . includes ( 'tor' ) ||
combined . includes ( 'network' ) || combined . includes ( 'firewall' ) ) {
return 'networking'
}
2026-03-13 23:40:29 +00:00
2026-01-24 22:59:20 +00:00
// Community category
if ( id . includes ( 'matrix' ) || id . includes ( 'synapse' ) || id . includes ( 'element' ) ||
2026-03-13 23:40:29 +00:00
id . includes ( 'mastodon' ) || id . includes ( 'lemmy' ) ||
2026-01-24 22:59:20 +00:00
id . includes ( 'messenger' ) || id . includes ( 'chat' ) || id . includes ( 'social' ) ||
id . includes ( 'cups' ) || combined . includes ( 'communication' ) ||
combined . includes ( 'messaging' ) ) {
return 'community'
}
// Default to other
return 'other'
}
2026-03-13 23:40:29 +00:00
// Combine curated apps with Nostr relay-discovered apps (merged into Nostr category)
2026-01-24 22:59:20 +00:00
const allApps = computed ( ( ) => {
2026-03-13 23:40:29 +00:00
// Always start with curated Docker apps
2026-03-11 13:04:31 +00:00
const local : ( MarketplaceApp & { category : string ; source : string } ) [ ] = [ ]
2026-01-24 22:59:20 +00:00
const community = communityApps . value . map ( app => {
const category = categorizeCommunityApp ( app )
return {
2026-03-11 13:04:31 +00:00
... app ,
2026-01-24 22:59:20 +00:00
category ,
source : 'community'
}
} )
2026-03-11 13:04:31 +00:00
2026-03-13 23:40:29 +00:00
const base = [ ... local , ... community ]
// Merge Nostr relay-discovered apps (deduplicated by ID)
if ( nostrApps . value . length > 0 ) {
const existingIds = new Set ( base . map ( a => a . id ) )
const nostrMerged = nostrApps . value
. filter ( app => ! existingIds . has ( app . id ) )
. map ( app => {
const category = app . category || categorizeCommunityApp ( app )
return { ... app , category , source : 'nostr' }
} )
return [ ... base , ... nostrMerged ]
}
return base
2026-01-24 22:59:20 +00:00
} )
2026-02-17 15:03:34 +00:00
// Only show categories that have at least one app
const categoriesWithApps = computed ( ( ) => {
const apps = allApps . value
2026-03-11 13:45:59 +00:00
return categories . value . filter ( cat => {
2026-02-17 15:03:34 +00:00
if ( cat . id === 'all' ) return apps . length > 0
return apps . some ( app => app . category === cat . id )
} )
} )
2026-01-24 22:59:20 +00:00
// Filtered apps by category and search
const filteredApps = computed ( ( ) => {
let apps = allApps . value
// Filter by category
if ( selectedCategory . value && selectedCategory . value !== 'all' ) {
apps = apps . filter ( app => app . category === selectedCategory . value )
}
// Filter by search query
if ( searchQuery . value ) {
const query = searchQuery . value . toLowerCase ( )
apps = apps . filter ( app =>
app . title ? . toLowerCase ( ) . includes ( query ) ||
2026-03-11 13:04:31 +00:00
( typeof app . description === 'string' && app . description . toLowerCase ( ) . includes ( query ) ) ||
2026-01-24 22:59:20 +00:00
( typeof app . description === 'object' && app . description ? . short ? . toLowerCase ( ) . includes ( query ) ) ||
app . id ? . toLowerCase ( ) . includes ( query ) ||
app . author ? . toLowerCase ( ) . includes ( query )
)
}
2026-03-18 16:00:03 +00:00
// Sort: available apps first, installed apps at the bottom
apps . sort ( ( a , b ) => {
const aInstalled = isInstalled ( a . id ) ? 1 : 0
const bInstalled = isInstalled ( b . id ) ? 1 : 0
return aInstalled - bInstalled
} )
2026-01-24 22:59:20 +00:00
return apps
} )
2026-02-25 18:04:41 +00:00
/** Marketplace app ID -> backend package keys (for "Already Installed" when first-boot/deploy created them) */
const INSTALLED _ALIASES : Record < string , string [ ] > = {
fix: container state mapping + marketplace install aliases
- Created containers now show as "stopped" not "starting" (fixes
ollama/tailscale perpetual "starting" state)
- Comprehensive INSTALLED_ALIASES map: fedimint, electrumx, grafana,
jellyfin, vaultwarden, searxng, homeassistant, photoprism, lnd,
filebrowser, tailscale, ollama — prevents marketplace showing
"Install" for already-installed containers
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 15:18:43 +00:00
mempool : [ 'mempool-web' , 'mempool-api' , 'archy-mempool-web' , 'archy-mempool-db' ] ,
2026-02-25 18:04:41 +00:00
bitcoin : [ 'bitcoin-knots' ] ,
fix: container state mapping + marketplace install aliases
- Created containers now show as "stopped" not "starting" (fixes
ollama/tailscale perpetual "starting" state)
- Comprehensive INSTALLED_ALIASES map: fedimint, electrumx, grafana,
jellyfin, vaultwarden, searxng, homeassistant, photoprism, lnd,
filebrowser, tailscale, ollama — prevents marketplace showing
"Install" for already-installed containers
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 15:18:43 +00:00
btcpay : [ 'btcpay-server' , 'archy-btcpay-db' , 'archy-nbxplorer' ] ,
immich : [ 'immich-server' , 'immich-app' , 'immich_server' , 'immich_postgres' , 'immich_redis' ] ,
feat: cloud native file browser, settings Claude auth, deploy hardening
- Add native Cloud file browser with FileBrowser API integration
- Add cloud store, filebrowser-client, useAudioPlayer, useFileType composables
- Add Cloud components: FileGrid, FileCard, FileCardGrid, CloudToolbar
- Add Claude authentication section to Settings with OAuth status check
- Harden deploy script to preserve /aiui/ and claude-login.html
- Add nginx proxies for btcpay, homeassistant, filebrowser (HTTPS block)
- Add app configs for filebrowser, searxng, penpot in package.rs
- Update goal progress tracking with app aliases
- Improve mobile back button composable with ResizeObserver
- Update various views with cloud integration and UI refinements
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 23:05:01 +00:00
nextcloud : [ 'nextcloud-aio' , 'nextcloud-server' ] ,
fix: container state mapping + marketplace install aliases
- Created containers now show as "stopped" not "starting" (fixes
ollama/tailscale perpetual "starting" state)
- Comprehensive INSTALLED_ALIASES map: fedimint, electrumx, grafana,
jellyfin, vaultwarden, searxng, homeassistant, photoprism, lnd,
filebrowser, tailscale, ollama — prevents marketplace showing
"Install" for already-installed containers
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 15:18:43 +00:00
fedimint : [ 'fedimint-gateway' ] ,
electrumx : [ 'electrumx' , 'archy-electrs-ui' ] ,
grafana : [ 'grafana' ] ,
jellyfin : [ 'jellyfin' ] ,
vaultwarden : [ 'vaultwarden' ] ,
searxng : [ 'searxng' ] ,
homeassistant : [ 'homeassistant' ] ,
photoprism : [ 'photoprism' ] ,
lnd : [ 'lnd' , 'archy-lnd-ui' ] ,
filebrowser : [ 'filebrowser' ] ,
tailscale : [ 'tailscale' ] ,
ollama : [ 'ollama' ] ,
2026-02-25 18:04:41 +00:00
}
2026-01-24 22:59:20 +00:00
function isInstalled ( appId : string ) : boolean {
2026-02-25 18:04:41 +00:00
if ( appId in installedPackages . value ) return true
const aliases = INSTALLED _ALIASES [ appId ]
return aliases ? aliases . some ( ( a ) => a in installedPackages . value ) : false
2026-01-24 22:59:20 +00:00
}
2026-03-18 11:15:32 +00:00
/** Get the package state for an installed app (checks aliases too). */
function getInstalledState ( appId : string ) : string | null {
const pkg = installedPackages . value [ appId ]
if ( pkg ) return pkg . state
const aliases = INSTALLED _ALIASES [ appId ]
if ( aliases ) {
for ( const a of aliases ) {
const aliasPkg = installedPackages . value [ a ]
if ( aliasPkg ) return aliasPkg . state
}
}
return null
}
/** True if installed and currently in a transitional state (not yet ready). */
function isStartingUp ( appId : string ) : boolean {
const state = getInstalledState ( appId )
return state !== null && state !== 'running' && state !== 'stopped' && state !== 'exited'
}
2026-03-13 23:40:29 +00:00
function launchInstalledApp ( app : MarketplaceApp ) {
2026-03-15 00:40:55 +00:00
appLauncher . openSession ( app . id )
2026-03-13 23:40:29 +00:00
}
2026-01-24 22:59:20 +00:00
// Load community marketplace on mount
onMounted ( ( ) => {
if ( communityApps . value . length === 0 && ! loadingCommunity . value ) {
loadCommunityMarketplace ( )
}
} )
// Load community marketplace from Start9 registry
async function loadCommunityMarketplace ( ) {
loadingCommunity . value = true
communityError . value = ''
2026-01-27 22:55:20 +00:00
// Use curated list of Docker-based apps
// These are standard Docker images, not StartOS packages
2026-03-11 13:04:31 +00:00
if ( import . meta . env . DEV ) console . log ( 'Loading Docker-based app marketplace' )
2026-01-27 22:55:20 +00:00
communityApps . value = getCuratedAppList ( )
loadingCommunity . value = false
}
2026-03-14 03:35:09 +00:00
// Get app tier classification (matches backend get_app_tier)
function getAppTier ( appId : string ) : string {
2026-03-14 19:08:09 +00:00
const core = [ 'bitcoin-knots' , 'bitcoin' , 'lnd' , 'mempool' , 'btcpay-server' , 'dwn' , 'filebrowser' ]
2026-03-14 03:35:09 +00:00
const recommended = [ 'fedimint' , 'vaultwarden' , 'uptime-kuma' , 'grafana' , 'searxng' , 'tailscale' , 'portainer' ]
if ( core . includes ( appId ) ) return 'core'
if ( recommended . includes ( appId ) ) return 'recommended'
return 'optional'
}
2026-01-27 22:55:20 +00:00
// Curated list of apps with Docker Hub images
function getCuratedAppList ( ) {
return [
{
2026-02-03 21:43:33 +00:00
id : 'bitcoin-knots' ,
2026-02-01 18:46:35 +00:00
title : 'Bitcoin Knots' ,
2026-02-03 21:43:33 +00:00
version : '28.1.0' ,
2026-01-27 22:55:20 +00:00
description : 'Run a full Bitcoin node. Validate and relay blocks and transactions on the Bitcoin network.' ,
2026-02-01 18:46:35 +00:00
icon : '/assets/img/app-icons/bitcoin-knots.webp' ,
author : 'Bitcoin Knots' ,
2026-03-11 13:04:31 +00:00
dockerImage : 'docker.io/bitcoinknots/bitcoin:v28.1' ,
manifestUrl : undefined ,
2026-02-01 18:46:35 +00:00
repoUrl : 'https://github.com/bitcoinknots/bitcoin'
2026-01-27 22:55:20 +00:00
} ,
{
id : 'btcpay-server' ,
title : 'BTCPay Server' ,
version : '1.13.5' ,
description : 'Self-hosted Bitcoin payment processor. Accept Bitcoin payments without intermediaries or fees.' ,
icon : '/assets/img/app-icons/btcpay-server.png' ,
author : 'BTCPay Server Foundation' ,
2026-02-01 18:46:35 +00:00
dockerImage : 'docker.io/btcpayserver/btcpayserver:1.13.5' ,
2026-03-11 13:04:31 +00:00
manifestUrl : undefined ,
2026-01-27 22:55:20 +00:00
repoUrl : 'https://github.com/btcpayserver/btcpayserver'
} ,
{
id : 'lnd' ,
title : 'LND' ,
version : '0.17.4' ,
description : 'Lightning Network Daemon. Fast and cheap Bitcoin payments through the Lightning Network.' ,
2026-02-01 18:46:35 +00:00
icon : '/assets/img/app-icons/lnd.svg' ,
2026-01-27 22:55:20 +00:00
author : 'Lightning Labs' ,
2026-02-01 18:46:35 +00:00
dockerImage : 'docker.io/lightninglabs/lnd:v0.17.4-beta' ,
2026-03-11 13:04:31 +00:00
manifestUrl : undefined ,
2026-01-27 22:55:20 +00:00
repoUrl : 'https://github.com/lightningnetwork/lnd'
} ,
{
id : 'mempool' ,
title : 'Mempool Explorer' ,
version : '2.5.0' ,
description : 'Self-hosted Bitcoin blockchain and mempool visualizer with beautiful explorer interface.' ,
2026-02-01 18:46:35 +00:00
icon : '/assets/img/app-icons/mempool.webp' ,
2026-01-27 22:55:20 +00:00
author : 'Mempool' ,
2026-02-01 18:46:35 +00:00
dockerImage : 'docker.io/mempool/frontend:v2.5.0' ,
2026-03-11 13:04:31 +00:00
manifestUrl : undefined ,
2026-01-27 22:55:20 +00:00
repoUrl : 'https://github.com/mempool/mempool'
} ,
{
id : 'homeassistant' ,
title : 'Home Assistant' ,
version : '2024.1' ,
description : 'Open-source home automation platform. Control and automate your smart home devices privately.' ,
icon : '/assets/img/app-icons/homeassistant.png' ,
author : 'Home Assistant' ,
2026-02-01 18:46:35 +00:00
dockerImage : 'docker.io/homeassistant/home-assistant:2024.1' ,
2026-03-11 13:04:31 +00:00
manifestUrl : undefined ,
2026-01-27 22:55:20 +00:00
repoUrl : 'https://github.com/home-assistant/core'
} ,
{
id : 'grafana' ,
title : 'Grafana' ,
version : '10.2.0' ,
description : 'Analytics and monitoring platform. Create dashboards and visualize data from multiple sources.' ,
2026-02-01 18:46:35 +00:00
icon : '/assets/img/app-icons/grafana.png' ,
2026-01-27 22:55:20 +00:00
author : 'Grafana Labs' ,
2026-02-01 18:46:35 +00:00
dockerImage : 'docker.io/grafana/grafana:10.2.0' ,
2026-03-11 13:04:31 +00:00
manifestUrl : undefined ,
2026-01-27 22:55:20 +00:00
repoUrl : 'https://github.com/grafana/grafana'
} ,
{
id : 'searxng' ,
title : 'SearXNG' ,
version : '2024.1.0' ,
description : 'Privacy-respecting metasearch engine. Search without tracking or ads.' ,
icon : '/assets/img/app-icons/searxng.png' ,
author : 'SearXNG' ,
2026-03-11 13:04:31 +00:00
dockerImage : 'docker.io/searxng/searxng:2024.11.17-e2554de75' ,
manifestUrl : undefined ,
2026-01-27 22:55:20 +00:00
repoUrl : 'https://github.com/searxng/searxng'
} ,
{
id : 'ollama' ,
title : 'Ollama' ,
version : '0.1.0' ,
description : 'Run large language models locally. Download and run AI models like Llama, Mistral on your own hardware.' ,
2026-02-01 18:46:35 +00:00
icon : '/assets/img/app-icons/ollama.png' ,
2026-01-27 22:55:20 +00:00
author : 'Ollama' ,
2026-03-11 13:04:31 +00:00
dockerImage : 'docker.io/ollama/ollama:0.5.4' ,
manifestUrl : undefined ,
2026-01-27 22:55:20 +00:00
repoUrl : 'https://github.com/ollama/ollama'
} ,
{
id : 'onlyoffice' ,
title : 'OnlyOffice' ,
version : '7.5.1' ,
description : 'Office suite for document collaboration. Edit docs, spreadsheets, and presentations.' ,
2026-02-01 18:46:35 +00:00
icon : '/assets/img/app-icons/onlyoffice.webp' ,
2026-01-27 22:55:20 +00:00
author : 'Ascensio System SIA' ,
2026-02-01 18:46:35 +00:00
dockerImage : 'docker.io/onlyoffice/documentserver:7.5.1' ,
2026-03-11 13:04:31 +00:00
manifestUrl : undefined ,
2026-01-27 22:55:20 +00:00
repoUrl : 'https://github.com/ONLYOFFICE/DocumentServer'
} ,
{
id : 'penpot' ,
title : 'Penpot' ,
2026-03-11 13:04:31 +00:00
version : '2.4' ,
2026-01-27 22:55:20 +00:00
description : 'Open-source design and prototyping platform. Self-hosted alternative to Figma.' ,
2026-03-09 00:18:28 +00:00
icon : '/assets/img/app-icons/penpot.webp' ,
2026-01-27 22:55:20 +00:00
author : 'Penpot' ,
2026-03-11 13:04:31 +00:00
dockerImage : 'docker.io/penpotapp/frontend:2.4' ,
manifestUrl : undefined ,
2026-01-27 22:55:20 +00:00
repoUrl : 'https://github.com/penpot/penpot'
} ,
{
id : 'nextcloud' ,
title : 'Nextcloud' ,
version : '28.0' ,
description : 'Self-hosted cloud storage and collaboration platform. Your own private cloud.' ,
2026-02-01 18:46:35 +00:00
icon : '/assets/img/app-icons/nextcloud.webp' ,
2026-01-27 22:55:20 +00:00
author : 'Nextcloud' ,
2026-02-01 18:46:35 +00:00
dockerImage : 'docker.io/library/nextcloud:28' ,
2026-03-11 13:04:31 +00:00
manifestUrl : undefined ,
2026-01-27 22:55:20 +00:00
repoUrl : 'https://github.com/nextcloud/server'
} ,
{
id : 'vaultwarden' ,
title : 'Vaultwarden' ,
version : '1.30.0' ,
description : 'Self-hosted password manager (Bitwarden-compatible). Secure vault for passwords and secrets.' ,
2026-02-01 18:46:35 +00:00
icon : '/assets/img/app-icons/vaultwarden.webp' ,
2026-01-27 22:55:20 +00:00
author : 'Vaultwarden' ,
2026-02-01 18:46:35 +00:00
dockerImage : 'docker.io/vaultwarden/server:1.30.0-alpine' ,
2026-03-11 13:04:31 +00:00
manifestUrl : undefined ,
2026-01-27 22:55:20 +00:00
repoUrl : 'https://github.com/dani-garcia/vaultwarden'
} ,
{
id : 'jellyfin' ,
title : 'Jellyfin' ,
version : '10.8.0' ,
description : 'Free media server system. Stream your movies, music, and photos to any device.' ,
2026-02-01 18:46:35 +00:00
icon : '/assets/img/app-icons/jellyfin.webp' ,
2026-01-27 22:55:20 +00:00
author : 'Jellyfin' ,
2026-02-01 18:46:35 +00:00
dockerImage : 'docker.io/jellyfin/jellyfin:10.8.13' ,
2026-03-11 13:04:31 +00:00
manifestUrl : undefined ,
2026-01-27 22:55:20 +00:00
repoUrl : 'https://github.com/jellyfin/jellyfin'
} ,
{
id : 'photoprism' ,
title : 'PhotoPrism' ,
2026-03-11 13:04:31 +00:00
version : '240915' ,
2026-01-27 22:55:20 +00:00
description : 'AI-powered photo management. Organize and browse photos with facial recognition.' ,
2026-03-13 23:40:29 +00:00
icon : '/assets/img/app-icons/photoprism.svg' ,
2026-01-27 22:55:20 +00:00
author : 'PhotoPrism' ,
2026-03-11 13:04:31 +00:00
dockerImage : 'docker.io/photoprism/photoprism:240915' ,
manifestUrl : undefined ,
2026-01-27 22:55:20 +00:00
repoUrl : 'https://github.com/photoprism/photoprism'
} ,
{
id : 'immich' ,
title : 'Immich' ,
version : '1.90.0' ,
description : 'High-performance self-hosted photo and video backup. Mobile-first with ML features.' ,
2026-02-01 18:46:35 +00:00
icon : '/assets/img/app-icons/immich.png' ,
2026-01-27 22:55:20 +00:00
author : 'Immich' ,
dockerImage : 'ghcr.io/immich-app/immich-server:release' ,
2026-03-11 13:04:31 +00:00
manifestUrl : undefined ,
2026-01-27 22:55:20 +00:00
repoUrl : 'https://github.com/immich-app/immich'
} ,
{
id : 'filebrowser' ,
title : 'File Browser' ,
version : '2.27.0' ,
description : 'Web-based file manager. Browse, upload, and manage files through a web interface.' ,
2026-02-01 18:46:35 +00:00
icon : '/assets/img/app-icons/file-browser.webp' ,
2026-01-27 22:55:20 +00:00
author : 'File Browser' ,
2026-02-01 18:46:35 +00:00
dockerImage : 'docker.io/filebrowser/filebrowser:v2.27.0' ,
2026-03-11 13:04:31 +00:00
manifestUrl : undefined ,
2026-01-27 22:55:20 +00:00
repoUrl : 'https://github.com/filebrowser/filebrowser'
} ,
{
id : 'nginx-proxy-manager' ,
title : 'Nginx Proxy Manager' ,
version : '2.11.0' ,
description : 'Easy proxy management with SSL. Beautiful web interface for managing reverse proxies.' ,
2026-02-01 18:46:35 +00:00
icon : '/assets/img/app-icons/nginx.svg' ,
2026-01-27 22:55:20 +00:00
author : 'Nginx Proxy Manager' ,
2026-03-11 13:04:31 +00:00
dockerImage : 'docker.io/jc21/nginx-proxy-manager:2.12.1' ,
manifestUrl : undefined ,
2026-01-27 22:55:20 +00:00
repoUrl : 'https://github.com/NginxProxyManager/nginx-proxy-manager'
} ,
{
id : 'portainer' ,
title : 'Portainer' ,
version : '2.19.0' ,
description : 'Container management UI. Manage Docker containers through a beautiful web interface.' ,
2026-02-01 18:46:35 +00:00
icon : '/assets/img/app-icons/portainer.webp' ,
2026-01-27 22:55:20 +00:00
author : 'Portainer' ,
2026-02-01 18:46:35 +00:00
dockerImage : 'docker.io/portainer/portainer-ce:2.19.4' ,
2026-03-11 13:04:31 +00:00
manifestUrl : undefined ,
2026-01-27 22:55:20 +00:00
repoUrl : 'https://github.com/portainer/portainer'
} ,
{
id : 'uptime-kuma' ,
title : 'Uptime Kuma' ,
version : '1.23.0' ,
description : 'Self-hosted monitoring tool. Monitor uptime for HTTP(s), TCP, DNS, and more.' ,
2026-02-01 18:46:35 +00:00
icon : '/assets/img/app-icons/uptime-kuma.webp' ,
2026-01-27 22:55:20 +00:00
author : 'Uptime Kuma' ,
2026-02-25 18:04:41 +00:00
dockerImage : 'docker.io/louislam/uptime-kuma:1' ,
2026-03-11 13:04:31 +00:00
manifestUrl : undefined ,
2026-01-27 22:55:20 +00:00
repoUrl : 'https://github.com/louislam/uptime-kuma'
} ,
2026-02-01 18:46:35 +00:00
{
id : 'tailscale' ,
title : 'Tailscale' ,
version : '1.78.0' ,
description : 'Zero-config VPN for secure remote access. Connect all your devices with WireGuard mesh network.' ,
icon : '/assets/img/app-icons/tailscale.webp' ,
author : 'Tailscale' ,
dockerImage : 'docker.io/tailscale/tailscale:stable' ,
2026-03-11 13:04:31 +00:00
manifestUrl : undefined ,
2026-02-01 18:46:35 +00:00
repoUrl : 'https://github.com/tailscale/tailscale'
} ,
2026-01-27 22:55:20 +00:00
{
id : 'fedimint' ,
title : 'Fedimint' ,
2026-02-17 15:03:34 +00:00
version : '0.10.0' ,
description : 'Federated Bitcoin mint with built-in Guardian UI. Private, scalable Bitcoin through federated guardians.' ,
icon : '/assets/img/app-icons/fedimint.png' ,
2026-01-27 22:55:20 +00:00
author : 'Fedimint' ,
2026-02-17 15:03:34 +00:00
dockerImage : 'docker.io/fedimint/fedimintd:v0.10.0' ,
2026-03-11 13:04:31 +00:00
manifestUrl : undefined ,
2026-01-27 22:55:20 +00:00
repoUrl : 'https://github.com/fedimint/fedimint'
2026-03-01 17:53:18 +00:00
} ,
{
id : 'indeedhub' ,
title : 'Indeehub' ,
version : '0.1.0' ,
2026-03-14 17:12:41 +00:00
description : 'Bitcoin documentary streaming platform with Nostr identity sign-in. Stream God Bless Bitcoin and other educational content about sovereignty and decentralized technology.' ,
2026-03-15 04:01:58 +00:00
icon : '/assets/img/app-icons/indeedhub.png' ,
2026-03-01 17:53:18 +00:00
author : 'Indeehub Team' ,
dockerImage : 'localhost/indeedhub:latest' ,
2026-03-11 13:04:31 +00:00
manifestUrl : undefined ,
2026-03-01 17:53:18 +00:00
repoUrl : 'https://github.com/indeedhub/indeedhub'
2026-03-09 07:43:12 +00:00
} ,
{
id : 'dwn' ,
title : 'Decentralized Web Node' ,
version : '0.4.0' ,
description : 'Store and sync your personal data across devices using decentralized web node protocols. Own your data with DID-based access control.' ,
icon : '/assets/img/app-icons/dwn.svg' ,
author : 'TBD' ,
dockerImage : 'ghcr.io/tbd54566975/dwn-server:main' ,
2026-03-11 13:04:31 +00:00
manifestUrl : undefined ,
2026-03-09 07:43:12 +00:00
repoUrl : 'https://github.com/TBD54566975/dwn-server'
} ,
2026-03-12 23:38:22 +00:00
{
id : 'nostrudel' ,
title : 'noStrudel' ,
version : '0.40.0' ,
2026-03-13 23:40:29 +00:00
category : 'nostr' ,
2026-03-12 23:38:22 +00:00
description : 'A feature-rich Nostr web client with NIP-07 signer support. Browse your feed, post notes, manage relays, and interact with the Nostr network — all signed with your node\'s Nostr identity.' ,
icon : '/assets/img/app-icons/nostrudel.svg' ,
author : 'hzrd149' ,
dockerImage : '' ,
manifestUrl : undefined ,
repoUrl : 'https://github.com/hzrd149/nostrudel' ,
webUrl : 'https://nostrudel.ninja'
} ,
2026-03-09 07:43:12 +00:00
{
id : 'nostr-rs-relay' ,
title : 'Nostr Relay' ,
version : '0.9.0' ,
2026-03-13 23:40:29 +00:00
category : 'nostr' ,
2026-03-09 07:43:12 +00:00
description : 'Run your own Nostr relay. Store your events locally, relay for friends, and publish over Tor. A sovereign relay for your sovereign node.' ,
icon : '/assets/img/app-icons/nostr-rs-relay.svg' ,
author : 'scsiblade' ,
2026-03-11 13:04:31 +00:00
dockerImage : 'docker.io/scsiblade/nostr-rs-relay:0.9.0' ,
manifestUrl : undefined ,
2026-03-09 07:43:12 +00:00
repoUrl : 'https://sr.ht/~gheartsfield/nostr-rs-relay/'
2026-03-12 00:19:30 +00:00
} ,
{
id : 'botfights' ,
title : 'BotFights' ,
version : '1.0.0' ,
description : 'AI bot arena — build, train, and battle autonomous agents. Compete in strategy tournaments with your own coded bots.' ,
icon : '/assets/img/app-icons/botfights.svg' ,
author : 'BotFights' ,
dockerImage : '' ,
manifestUrl : undefined ,
repoUrl : 'https://botfights.net' ,
webUrl : 'https://botfights.net'
} ,
{
id : 'nwnn' ,
title : 'Next Web News Network' ,
version : '1.0.0' ,
category : 'l484' ,
description : 'Decentralized news and link aggregator, synchronized from Telegram. Community-curated content on Bitcoin, sovereignty, and decentralized tech.' ,
icon : '/assets/img/app-icons/nwnn.png' ,
author : 'L484' ,
dockerImage : '' ,
manifestUrl : undefined ,
repoUrl : 'https://nwnn.l484.com' ,
webUrl : 'https://nwnn.l484.com'
} ,
{
id : '484-kitchen' ,
title : '484 Kitchen' ,
version : '1.0.0' ,
category : 'l484' ,
description : 'K484 application platform — an internal tool for the L484 network.' ,
icon : '/assets/img/app-icons/484-kitchen.png' ,
author : 'L484' ,
dockerImage : '' ,
manifestUrl : undefined ,
repoUrl : 'https://484.kitchen' ,
webUrl : 'https://484.kitchen'
} ,
{
id : 'call-the-operator' ,
title : 'Call the Operator' ,
version : '1.0.0' ,
category : 'l484' ,
description : 'Escape the Matrix — a portal for exploring decentralized alternatives and reclaiming digital sovereignty.' ,
icon : '/assets/img/app-icons/call-the-operator.png' ,
author : 'TX1138' ,
dockerImage : '' ,
manifestUrl : undefined ,
repoUrl : 'https://cta.tx1138.com' ,
webUrl : 'https://cta.tx1138.com'
} ,
{
id : 'arch-presentation' ,
title : 'Arch Presentation' ,
version : '1.0.0' ,
category : 'l484' ,
description : 'Archipelago: The Future of Decentralized Infrastructure — an interactive presentation about the Archipelago project vision.' ,
icon : '/assets/img/app-icons/arch-presentation.png' ,
author : 'L484' ,
dockerImage : '' ,
manifestUrl : undefined ,
repoUrl : 'https://present.l484.com' ,
webUrl : 'https://present.l484.com'
} ,
{
id : 'syntropy-institute' ,
title : 'Syntropy Institute' ,
version : '1.0.0' ,
category : 'l484' ,
description : 'Medicine Reimagined — Manual Kinetics, Syntropy Frequency analysis-therapy, digital homeopathy, and concierge protocols.' ,
icon : '/assets/img/app-icons/syntropy-institute.png' ,
author : 'Syntropy Institute' ,
dockerImage : '' ,
manifestUrl : undefined ,
repoUrl : 'https://syntropy.institute' ,
webUrl : 'https://syntropy.institute'
} ,
{
id : 't-zero' ,
title : 'T-0' ,
version : '1.0.0' ,
category : 'l484' ,
description : 'Documentary series exploring decentralization, Bitcoin, and the mavericks building the ungovernable future. Conversations with the builders, powered by Nostr.' ,
icon : '/assets/img/app-icons/t-zero.png' ,
author : 'T-0' ,
dockerImage : '' ,
manifestUrl : undefined ,
repoUrl : 'https://teeminuszero.net' ,
webUrl : 'https://teeminuszero.net'
2026-01-24 22:59:20 +00:00
}
2026-01-27 22:55:20 +00:00
]
}
2026-03-11 13:04:31 +00:00
function viewAppDetails ( app : MarketplaceApp ) {
if ( import . meta . env . DEV ) console . log ( '[Marketplace] Navigating to app detail:' , app )
2026-01-24 22:59:20 +00:00
try {
// If app is already installed, go directly to the installed app detail page
if ( isInstalled ( app . id ) ) {
2026-03-11 13:04:31 +00:00
if ( import . meta . env . DEV ) console . log ( '[Marketplace] App is installed, navigating to app details page' )
2026-01-24 22:59:20 +00:00
router . push ( {
name : 'app-details' ,
params : { id : app . id }
} )
} else {
// Store app data in composable for marketplace detail view
setCurrentApp ( app )
2026-03-11 13:04:31 +00:00
if ( import . meta . env . DEV ) console . log ( '[Marketplace] App data stored in composable' )
2026-01-24 22:59:20 +00:00
// Navigate to marketplace detail page
router . push ( {
name : 'marketplace-app-detail' ,
params : { id : app . id }
} )
}
} catch ( e ) {
2026-03-11 13:04:31 +00:00
if ( import . meta . env . DEV ) console . error ( '[Marketplace] Navigation error:' , e )
2026-01-24 22:59:20 +00:00
}
}
2026-03-01 18:07:35 +00:00
const activeTimers : ReturnType < typeof setTimeout > [ ] = [ ]
const activeIntervals : ReturnType < typeof setInterval > [ ] = [ ]
function trackTimeout ( fn : ( ) => void , ms : number ) {
const id = setTimeout ( ( ) => {
const idx = activeTimers . indexOf ( id )
if ( idx !== - 1 ) activeTimers . splice ( idx , 1 )
fn ( )
} , ms )
activeTimers . push ( id )
return id
}
function trackInterval ( fn : ( ) => void , ms : number ) {
const id = setInterval ( fn , ms )
activeIntervals . push ( id )
return id
}
function clearTrackedInterval ( id : ReturnType < typeof setInterval > ) {
clearInterval ( id )
const idx = activeIntervals . indexOf ( id )
if ( idx !== - 1 ) activeIntervals . splice ( idx , 1 )
}
onBeforeUnmount ( ( ) => {
for ( const t of activeTimers ) clearTimeout ( t )
activeTimers . length = 0
for ( const i of activeIntervals ) clearInterval ( i )
activeIntervals . length = 0
} )
function startInstallPolling ( appId : string , statusMessage : string ) {
const interval = trackInterval ( ( ) => {
const current = installingApps . value . get ( appId )
if ( ! current ) { clearTrackedInterval ( interval ) ; return }
const newAttempt = current . attempt + 1
installingApps . value . set ( appId , {
... current ,
attempt : newAttempt ,
progress : Math . min ( 60 + ( newAttempt * 0.5 ) , 95 ) ,
message : statusMessage
} )
if ( isInstalled ( appId ) ) {
clearTrackedInterval ( interval )
installingApps . value . set ( appId , { ... current , status : 'complete' , progress : 100 , message : 'Installation complete!' } )
trackTimeout ( ( ) => { installingApps . value . delete ( appId ) } , 2000 )
} else if ( newAttempt >= maxAttempts . value ) {
clearTrackedInterval ( interval )
installingApps . value . set ( appId , { ... current , status : 'error' , progress : 0 , message : 'Installation timeout' } )
trackTimeout ( ( ) => { installingApps . value . delete ( appId ) } , 5000 )
}
} , 1000 )
}
2026-03-11 13:04:31 +00:00
async function installApp ( app : MarketplaceApp ) {
2026-02-03 21:43:33 +00:00
if ( installingApps . value . has ( app . id ) || isInstalled ( app . id ) ) return
2026-01-24 22:59:20 +00:00
2026-02-03 21:43:33 +00:00
installingApps . value . set ( app . id , {
2026-03-11 13:04:31 +00:00
id : app . id , title : app . title ? ? app . id , status : 'downloading' , progress : 10 , message : 'Preparing installation...' , attempt : 0
2026-02-03 21:43:33 +00:00
} )
2026-03-11 13:04:31 +00:00
2026-01-24 22:59:20 +00:00
try {
const installUrl = app . url || app . manifestUrl || app . s9pkUrl
2026-03-01 18:07:35 +00:00
installingApps . value . set ( app . id , { ... installingApps . value . get ( app . id ) ! , status : 'downloading' , progress : 30 , message : 'Downloading package...' } )
2026-02-03 21:43:33 +00:00
2026-03-01 18:07:35 +00:00
await rpcClient . call ( { method : 'package.install' , params : { id : app . id , url : installUrl , version : app . version } } )
2026-01-24 22:59:20 +00:00
2026-03-01 18:07:35 +00:00
installingApps . value . set ( app . id , { ... installingApps . value . get ( app . id ) ! , status : 'installing' , progress : 60 , message : 'Installing package...' } )
2026-01-24 22:59:20 +00:00
2026-03-01 18:07:35 +00:00
startInstallPolling ( app . id , 'Starting application...' )
2026-01-24 22:59:20 +00:00
} catch ( err ) {
2026-03-11 13:04:31 +00:00
if ( import . meta . env . DEV ) console . error ( 'Installation failed:' , err )
2026-03-01 18:07:35 +00:00
installingApps . value . set ( app . id , { ... installingApps . value . get ( app . id ) ! , status : 'error' , progress : 0 , message : ` Failed: ${ err } ` } )
trackTimeout ( ( ) => { installingApps . value . delete ( app . id ) } , 5000 )
2026-01-24 22:59:20 +00:00
}
}
2026-03-11 13:04:31 +00:00
async function installCommunityApp ( app : MarketplaceApp ) {
2026-02-03 21:43:33 +00:00
if ( installingApps . value . has ( app . id ) || isInstalled ( app . id ) || ! app . dockerImage ) return
2026-01-24 22:59:20 +00:00
2026-02-03 21:43:33 +00:00
installingApps . value . set ( app . id , {
2026-03-11 13:04:31 +00:00
id : app . id , title : app . title ? ? app . id , status : 'downloading' , progress : 10 , message : 'Pulling Docker image...' , attempt : 0
2026-02-03 21:43:33 +00:00
} )
2026-01-24 22:59:20 +00:00
try {
2026-03-01 18:07:35 +00:00
installingApps . value . set ( app . id , { ... installingApps . value . get ( app . id ) ! , status : 'downloading' , progress : 20 , message : 'Downloading container image...' } )
2026-02-03 21:43:33 +00:00
2026-01-24 22:59:20 +00:00
await rpcClient . call ( {
method : 'package.install' ,
2026-03-01 18:07:35 +00:00
params : { id : app . id , dockerImage : app . dockerImage , version : app . version } ,
timeout : 180000
2026-02-03 21:43:33 +00:00
} )
2026-01-24 22:59:20 +00:00
2026-03-01 18:07:35 +00:00
installingApps . value . set ( app . id , { ... installingApps . value . get ( app . id ) ! , status : 'installing' , progress : 60 , message : 'Starting container...' } )
2026-01-24 22:59:20 +00:00
2026-03-01 18:07:35 +00:00
startInstallPolling ( app . id , 'Initializing application...' )
2026-01-24 22:59:20 +00:00
} catch ( err ) {
2026-03-11 13:04:31 +00:00
if ( import . meta . env . DEV ) console . error ( '[Marketplace] Installation failed:' , err )
2026-03-01 18:07:35 +00:00
installingApps . value . set ( app . id , { ... installingApps . value . get ( app . id ) ! , status : 'error' , progress : 0 , message : ` Failed: ${ err } ` } )
trackTimeout ( ( ) => { installingApps . value . delete ( app . id ) } , 5000 )
2026-01-24 22:59:20 +00:00
}
}
function handleImageError ( event : Event ) {
const img = event . target as HTMLImageElement
img . src = '/assets/img/logo-archipelago.svg'
}
< / script >
< style scoped >
2026-03-18 11:46:38 +00:00
. marketplace - shimmer - bg {
position : absolute ;
inset : 0 ;
background : linear - gradient ( 90 deg , rgba ( 255 , 255 , 255 , 0.03 ) 0 % , rgba ( 255 , 255 , 255 , 0.08 ) 50 % , rgba ( 255 , 255 , 255 , 0.03 ) 100 % ) ;
background - size : 200 % 100 % ;
animation : shimmer 2 s ease - in - out infinite ;
border - radius : inherit ;
}
@ keyframes shimmer {
0 % { background - position : 200 % 0 ; }
100 % { background - position : - 200 % 0 ; }
}
2026-01-24 22:59:20 +00:00
. line - clamp - 3 {
display : - webkit - box ;
- webkit - line - clamp : 3 ;
- webkit - box - orient : vertical ;
overflow : hidden ;
}
/* Modal transition animations */
. modal - enter - active ,
. modal - leave - active {
transition : opacity 0.3 s ease ;
}
. modal - enter - from ,
. modal - leave - to {
opacity : 0 ;
}
. modal - enter - active . glass - card ,
. modal - leave - active . glass - card {
transition : transform 0.3 s ease ;
}
. modal - enter - from . glass - card {
transform : scale ( 0.95 ) ;
}
. modal - leave - to . glass - card {
transform : scale ( 0.95 ) ;
}
/* Custom scrollbar styling for apps section */
. marketplace - container : : - webkit - scrollbar {
width : 8 px ;
}
. marketplace - container : : - webkit - scrollbar - track {
background : rgba ( 255 , 255 , 255 , 0.05 ) ;
border - radius : 4 px ;
}
. marketplace - container : : - webkit - scrollbar - thumb {
background : rgba ( 255 , 255 , 255 , 0.2 ) ;
border - radius : 4 px ;
transition : background 0.3 s ease ;
}
. marketplace - container : : - webkit - scrollbar - thumb : hover {
background : rgba ( 255 , 255 , 255 , 0.3 ) ;
}
/* Firefox scrollbar */
. marketplace - container {
scrollbar - width : thin ;
scrollbar - color : rgba ( 255 , 255 , 255 , 0.2 ) rgba ( 255 , 255 , 255 , 0.05 ) ;
}
< / style >