2026-01-24 22:59:20 +00:00
< template >
< div class = "pb-6" >
< div class = "mb-8" >
< h1 class = "text-3xl font-bold text-white mb-2" > My Apps < / h1 >
< p class = "text-white/70" > Manage your installed applications < / p >
< / div >
<!-- Empty State - This should never show since we always show dummy apps -- >
< div v-if = "false" class="text-center py-16 pb-6" >
< div class = "glass-card p-12 max-w-md mx-auto" >
< svg class = "w-16 h-16 mx-auto text-white/40 mb-4" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
< path stroke -linecap = " round " stroke -linejoin = " round " stroke -width = " 2 " d = "M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" / >
< / svg >
< h3 class = "text-xl font-semibold text-white mb-2" > No Apps Installed < / h3 >
< p class = "text-white/70 mb-6" > Get started by browsing the app store < / p >
< RouterLink
to = "/dashboard/marketplace"
class = "inline-block glass-button px-6 py-3 rounded-lg font-medium transition-all hover:bg-black/70 hover:border-white/30"
>
Browse App Store
< / RouterLink >
< / div >
< / div >
2026-01-27 22:47:51 +00:00
<!-- Apps Grid ( alphabetically by title , stable across run state ) -- >
2026-01-24 22:59:20 +00:00
< div class = "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 pb-6" >
< div
2026-01-27 22:47:51 +00:00
v - for = "[id, pkg] in sortedPackageEntries"
2026-01-24 22:59:20 +00:00
: key = "id"
class = "glass-card p-6 transition-all hover:-translate-y-1 cursor-pointer relative"
@ click = "goToApp(id as string)"
>
<!-- Uninstall Icon -- >
< button
@ click . stop = "showUninstallModal(id as string, pkg)"
class = "absolute top-4 right-4 p-2 rounded-lg text-white/60 hover:text-red-400 hover:bg-red-500/20 transition-colors z-10"
title = "Uninstall"
>
< svg class = "w-5 h-5" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
< path stroke -linecap = " round " stroke -linejoin = " round " stroke -width = " 2 " d = "M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" / >
< / svg >
< / button >
< div class = "flex items-start gap-4" >
< img
: src = "pkg['static-files'].icon"
: alt = "pkg.manifest.title"
class = "w-16 h-16 rounded-lg object-cover bg-white/10"
@ error = "handleImageError"
/ >
< div class = "flex-1 min-w-0" >
< h3 class = "text-lg font-semibold text-white mb-1 truncate" >
{ { pkg . manifest . title } }
< / h3 >
< p class = "text-sm text-white/70 mb-2 truncate" >
{ { pkg . manifest . description . short } }
< / p >
< div class = "flex items-center gap-2" >
< span
class = "inline-flex items-center px-2 py-1 rounded text-xs font-medium"
: class = "getStatusClass(pkg.state)"
>
{ { pkg . state } }
< / span >
< span class = "text-xs text-white/50" >
v { { pkg . manifest . version } }
< / span >
< / div >
< / div >
< / div >
<!-- Quick Actions -- >
< div class = "mt-4 flex gap-2" >
< button
v - if = "canLaunch(pkg)"
@ click . stop = "launchApp(id as string)"
class = "flex-1 px-4 py-2 gradient-button rounded-lg text-sm font-medium"
>
Launch
< / button >
< button
v - if = "pkg.state === 'stopped'"
@ click . stop = "startApp(id as string)"
class = "flex-1 px-4 py-2 bg-green-500/20 border border-green-500/40 rounded-lg text-green-200 text-sm font-medium hover:bg-green-500/30 transition-colors"
>
Start
< / button >
< button
v - if = "pkg.state === 'running'"
@ click . stop = "stopApp(id as string)"
class = "flex-1 px-4 py-2 bg-yellow-500/20 border border-yellow-500/40 rounded-lg text-yellow-200 text-sm font-medium hover:bg-yellow-500/30 transition-colors"
>
Stop
< / button >
< / div >
< / div >
< / div >
<!-- Uninstall Confirmation Modal -- >
< Transition name = "modal" >
< div
v - if = "uninstallModal.show"
class = "fixed inset-0 z-50 flex items-center justify-center p-4"
@ click = "uninstallModal.show = false"
>
< div class = "absolute inset-0 bg-black/60 backdrop-blur-sm" > < / div >
< div
@ click . stop
class = "glass-card p-6 max-w-md w-full relative z-10"
>
< div class = "flex items-start gap-4 mb-4" >
< div class = "p-3 bg-red-500/20 rounded-lg" >
< svg class = "w-6 h-6 text-red-400" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
< path stroke -linecap = " round " stroke -linejoin = " round " stroke -width = " 2 " d = "M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" / >
< / svg >
< / div >
< div class = "flex-1" >
< h3 class = "text-xl font-semibold text-white mb-2" > Uninstall App ? < / h3 >
< p class = "text-white/70" >
Are you sure you want to uninstall < span class = "text-white font-medium" > { { uninstallModal . appTitle } } < / span > ?
This will remove the app and stop its container .
< / p >
< / div >
< / div >
< div class = "flex gap-3 justify-end" >
< button
@ click = "uninstallModal.show = false"
class = "px-4 py-2 glass-button rounded-lg text-sm font-medium"
>
Cancel
< / button >
< button
@ click = "confirmUninstall"
class = "px-4 py-2 bg-red-600/80 hover:bg-red-600 rounded-lg text-white text-sm font-medium transition-colors"
>
Uninstall
< / button >
< / div >
< / div >
< / div >
< / Transition >
< / div >
< / template >
< script setup lang = "ts" >
2026-01-27 23:06:18 +00:00
import { computed , ref } from 'vue'
2026-01-24 22:59:20 +00:00
import { useRouter , RouterLink } from 'vue-router'
import { useAppStore } from '../stores/app'
import { PackageState } from '../types/api'
const router = useRouter ( )
const store = useAppStore ( )
2026-01-27 23:06:18 +00:00
// Use real packages from store - no more dummy apps
2026-01-24 22:59:20 +00:00
const packages = computed ( ( ) => {
const realPackages = store . packages
2026-01-27 23:06:18 +00:00
console . log ( '[Apps] Real packages from store:' , Object . keys ( realPackages || { } ) . length , 'apps' )
return realPackages || { }
2026-01-24 22:59:20 +00:00
} )
2026-01-27 22:47:51 +00:00
// Sorted by manifest title, case-insensitive; order stable regardless of running/stopped
const sortedPackageEntries = computed ( ( ) => {
const entries = Object . entries ( packages . value )
return entries . sort ( ( [ , a ] , [ , b ] ) =>
( a . manifest ? . title ? ? '' ) . localeCompare ( b . manifest ? . title ? ? '' , undefined , { sensitivity : 'base' } )
)
} )
2026-01-24 22:59:20 +00:00
const uninstallModal = ref ( {
show : false ,
appId : '' ,
appTitle : ''
} )
function canLaunch ( pkg : any ) : boolean {
// For dummy apps, allow launch if running (they have interface addresses)
// For real apps, check for UI interface
const hasUI = pkg . manifest . interfaces ? . main ? . ui || pkg . installed ? . [ 'interface-addresses' ] ? . main
const isRunning = pkg . state === 'running'
return hasUI && isRunning
}
function launchApp ( id : string ) {
const isDev = import . meta . env . DEV
2026-01-27 22:47:51 +00:00
const pkg = packages . value [ id ]
2026-01-27 23:25:29 +00:00
// Special handling for Bitcoin Core - it's a headless node with no web UI
// Just show connection info instead
2026-01-27 23:21:26 +00:00
if ( id === 'bitcoin' ) {
2026-01-27 23:25:29 +00:00
const rpcPort = pkg ? . installed ? . lanAddress ? . match ( /:(\d+)/ ) ? . [ 1 ] || '18443'
alert ( ` ✅ Bitcoin Core is running! \ n \ n🔗 RPC Endpoint: http://localhost: ${ rpcPort } \ n👤 User: bitcoin \ n🔑 Password: bitcoinpass \ n \ n💡 Use bitcoin-cli or an RPC client to interact. \ n📊 View blockchain data in Mempool: http://localhost:4080 ` )
2026-01-27 23:21:26 +00:00
return
}
2026-01-27 22:47:51 +00:00
// Get the LAN address from the package manifest
const lanAddress = pkg ? . installed ? . [ 'interface-addresses' ] ? . main ? . [ 'lan-address' ]
if ( lanAddress ) {
window . open ( lanAddress , '_blank' , 'noopener,noreferrer' )
return
}
2026-01-24 22:59:20 +00:00
2026-01-27 22:47:51 +00:00
// Fallback: Special handling for apps with Docker containers
2026-01-24 22:59:20 +00:00
const appUrls : Record < string , { dev : string , prod : string } > = {
'atob' : {
dev : 'http://localhost:8102' ,
prod : 'https://app.atobitcoin.io'
} ,
'k484' : {
dev : 'http://localhost:8103' ,
prod : 'http://localhost:8103' // Self-hosted splash screen
}
}
if ( appUrls [ id ] ) {
const url = isDev ? appUrls [ id ] . dev : appUrls [ id ] . prod
window . open ( url , '_blank' , 'noopener,noreferrer' )
return
}
// For other apps, navigate to app details which has launch functionality
router . push ( ` /dashboard/apps/ ${ id } ` )
}
function getStatusClass ( state : PackageState ) : string {
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 . 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'
default :
return 'bg-gray-500/20 text-gray-200'
}
}
function goToApp ( id : string ) {
router . push ( ` /dashboard/apps/ ${ id } ` )
}
async function startApp ( id : string ) {
try {
await store . startPackage ( id )
} catch ( err ) {
console . error ( 'Failed to start app:' , err )
}
}
async function stopApp ( id : string ) {
try {
await store . stopPackage ( id )
} catch ( err ) {
console . error ( 'Failed to stop app:' , err )
}
}
// @ts-ignore - Function kept for future use
// eslint-disable-next-line @typescript-eslint/no-unused-vars
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async function _restartApp ( _id : string ) {
try {
await store . restartPackage ( _id )
} catch ( err ) {
console . error ( 'Failed to restart app:' , err )
}
}
function showUninstallModal ( id : string , pkg : any ) {
uninstallModal . value = {
show : true ,
appId : id ,
appTitle : pkg . manifest . title
}
}
async function confirmUninstall ( ) {
const { appId } = uninstallModal . value
uninstallModal . value . show = false
try {
await store . uninstallPackage ( appId )
} catch ( err ) {
console . error ( 'Failed to uninstall app:' , err )
alert ( 'Failed to uninstall app' )
}
}
function handleImageError ( e : Event ) {
const target = e . target as HTMLImageElement
const currentSrc = target . src
// Try fallback icon - use a simple placeholder SVG
// Create a data URI for a simple icon placeholder
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 >
` )} `
// Only set fallback if we haven't already tried it
if ( ! currentSrc . includes ( 'data:image' ) ) {
target . src = placeholderSvg
}
}
< / script >
< style scoped >
. modal - enter - active ,
. modal - leave - active {
transition : opacity 0.3 s ease ;
}
. modal - enter - active . glass - card ,
. modal - leave - active . glass - card {
transition : transform 0.3 s ease , opacity 0.3 s ease ;
}
. modal - enter - from ,
. modal - leave - to {
opacity : 0 ;
}
. modal - enter - from . glass - card ,
. modal - leave - to . glass - card {
transform : scale ( 0.95 ) ;
opacity : 0 ;
}
< / style >