2026-01-24 22:59:20 +00:00
< template >
2026-03-14 17:12:41 +00:00
< div class = "pb-6" >
2026-03-05 10:14:10 +00:00
< div class = "mb-4 md:mb-8 flex items-start justify-between gap-4" >
2026-02-18 10:35:04 +00:00
< div class = "min-h-[4.5rem]" >
< h1 class = "text-3xl font-bold text-white mb-2 drop-shadow-[0_2px_8px_rgba(0,0,0,0.6)]" >
{{ line1Text }}< span v-if = "showCaretLine1" class="typing-caret" ></ span >
</ h1 >
< p class = "text-white/80" >
{{ line2Text }}< span v-if = "showCaretLine2" class="typing-caret" ></ span >
</ p >
2026-01-24 22:59:20 +00:00
</ div >
2026-03-05 10:14:10 +00:00
<!-- Desktop : tabs inline with header -- >
< div
v-if = "!uiMode.isChat"
2026-03-27 16:16:57 +00:00
role = "tablist"
2026-03-05 10:14:10 +00:00
class = "hidden md:flex mode-switcher flex-shrink-0 transition-opacity duration-500"
: class = "{ 'opacity-0 pointer-events-none': showWelcomeBlock && !animateCards }"
>
2026-03-11 13:45:59 +00:00
< button class = "mode-switcher-btn" role = "tab" : aria -selected =" homeTab = == ' dashboard '" : class = "{ 'mode-switcher-btn-active': homeTab === 'dashboard' }" @click ="homeTab = 'dashboard'" >{{ t (' home.dashboardTab ') }}</ button >
< button class = "mode-switcher-btn" role = "tab" : aria -selected =" homeTab = == ' setup '" : class = "{ 'mode-switcher-btn-active': homeTab === 'setup' }" @click ="homeTab = 'setup'" >{{ t (' home.setupTab ') }}</ button >
2026-03-11 13:04:31 +00:00
</ div >
</ div >
<!-- Update notification banner -- >
< div
v-if = "updateAvailable && !updateDismissed"
role = "alert"
class = "mb-6 glass-card p-4 flex items-center justify-between gap-4 border-l-4 border-orange-400 transition-opacity duration-300"
: class = "{ 'opacity-0 pointer-events-none': showWelcomeBlock && !animateCards }"
>
< div class = "flex items-center gap-3 min-w-0" >
< svg class = "w-6 h-6 text-orange-400 shrink-0" aria -hidden =" true " fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
< path stroke -linecap =" round " stroke -linejoin =" round " stroke -width =" 2 " d = "M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</ svg >
< div class = "min-w-0" >
2026-03-11 13:45:59 +00:00
< p class = "text-sm font-medium text-white" >{{ t ( 'home.updateAvailable' , { version : updateVersion }) }}</ p >
2026-03-11 13:04:31 +00:00
< p v-if = "updateChangelog" class="text-xs text-white/60 truncate" >{{ updateChangelog }}</ p >
</ div >
</ div >
< div class = "flex items-center gap-2 shrink-0" >
2026-03-21 03:01:38 +00:00
< RouterLink to = "/dashboard/settings/update" class = "glass-button rounded-lg px-4 py-2 text-sm font-medium" >{{ t ( 'home.updateNow' ) }}</ RouterLink >
2026-03-11 13:04:31 +00:00
< button @click ="dismissUpdate" aria -label =" Dismiss update notification " class = "text-white/40 hover:text-white/80 transition-colors p-1" title = "Dismiss" >
2026-03-21 03:01:38 +00:00
< svg class = "w-5 h-5" aria -hidden =" true " 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 >
2026-03-11 13:04:31 +00:00
</ button >
2026-03-05 10:14:10 +00:00
</ div >
2026-01-24 22:59:20 +00:00
</ div >
2026-03-05 07:36:43 +00:00
<!-- Tab bar + content ( all non - chat modes ) -->
< template v-if = "!uiMode.isChat" >
2026-03-05 10:14:10 +00:00
<!-- Mobile : full -width tabs -- >
< div
2026-03-11 13:04:31 +00:00
role = "tablist"
2026-03-27 16:16:57 +00:00
class = "md:hidden mode-switcher mb-6 w-full transition-opacity duration-500"
2026-03-05 10:14:10 +00:00
: class = "{ 'opacity-0 pointer-events-none': showWelcomeBlock && !animateCards }"
>
2026-03-11 13:45:59 +00:00
< button class = "mode-switcher-btn" role = "tab" : aria -selected =" homeTab = == ' dashboard '" : class = "{ 'mode-switcher-btn-active': homeTab === 'dashboard' }" @click ="homeTab = 'dashboard'" >{{ t (' home.dashboardTab ') }}</ button >
< button class = "mode-switcher-btn" role = "tab" : aria -selected =" homeTab = == ' setup '" : class = "{ 'mode-switcher-btn-active': homeTab === 'setup' }" @click ="homeTab = 'setup'" >{{ t (' home.setupTab ') }}</ button >
2026-03-05 07:36:43 +00:00
</ div >
2026-03-04 07:09:31 +00:00
2026-03-21 03:01:38 +00:00
<!-- Setup tab -- >
< EasyHome v-if = "homeTab === 'setup'" :show="!showWelcomeBlock || animateCards" :animate="animateCards" />
2026-03-05 07:36:43 +00:00
<!-- Dashboard tab : overview cards -->
< div
v-else
class = "grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8 transition-opacity duration-300"
: class = "{ 'opacity-0 pointer-events-none': showWelcomeBlock && !animateCards }"
>
2026-03-21 03:01:38 +00:00
<!-- My Apps Overview -->
2026-07-22 02:11:39 -04:00
< div data -controller-container tabindex = "0" class = "home-card controller-focusable order-1 lg:order-none" : class = "{ 'home-card-animate': animateCards }" style = "--card-stagger: 0" >
2026-03-21 03:01:38 +00:00
< div class = "home-card-shell" >
< div class = "home-card-inner p-6 flex flex-col h-full min-h-0" >
< div class = "home-card-header flex items-start justify-between mb-4 shrink-0" >
< div class = "home-card-text" >
< h2 class = "text-xl font-semibold text-white mb-1" >{{ t ( 'home.myApps' ) }}</ h2 >
< p class = "text-sm text-white/70" >{{ t ( 'home.myAppsDesc' ) }}</ p >
</ div >
< RouterLink to = "/dashboard/apps" :aria-label = "t('home.goToApps')" class = "text-white/60 hover:text-white transition-colors" >
< svg class = "w-5 h-5" aria -hidden =" true " fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >< path stroke -linecap =" round " stroke -linejoin =" round " stroke -width =" 2 " d = "M9 5l7 7-7 7" /></ svg >
</ RouterLink >
2026-02-17 19:19:54 +00:00
</ div >
2026-03-21 03:01:38 +00:00
< div class = "home-card-stats grid grid-cols-1 sm:grid-cols-2 gap-4 mb-4 flex-1 min-h-0" >
< div class = "p-4 bg-white/5 rounded-lg" >
< p class = "text-xs text-white/60 mb-1" > Installed / Running </ p >
< p class = "text-2xl font-bold text-white" >{{ appCount }} / {{ runningCount }}</ p >
</ div >
< div class = "p-4 bg-white/5 rounded-lg flex items-center justify-around" >
< button v-for = "app in quickLaunchApps" :key="app.id" @click="useAppLauncherStore().openSession(app.id)" class="group" :title="app.name" >
2026-06-11 00:24:40 -04:00
< div class = "w-14 h-14 rounded-xl overflow-hidden transition-all group-hover:-translate-y-1 group-hover:shadow-lg flex items-center justify-center" >
< img :src = "app.icon" :alt = "app.name" class = "w-full h-full rounded-xl archy-app-icon" @error ="handleImageError" />
2026-03-16 12:58:35 +00:00
</ div >
</ button >
2026-03-21 03:01:38 +00:00
</ div >
</ div >
< div class = "home-card-buttons flex gap-2 mt-auto pt-4 shrink-0" >
< RouterLink to = "/dashboard/marketplace" class = "home-card-btn flex-1 px-4 py-2 glass-button rounded-lg text-sm font-medium text-center transition-colors" >{{ t ( 'home.browseStore' ) }}</ RouterLink >
< RouterLink to = "/dashboard/apps" class = "home-card-btn flex-1 px-4 py-2 glass-button rounded-lg text-sm font-medium text-center transition-colors" >{{ t ( 'home.manageApps' ) }}</ RouterLink >
2026-02-17 19:19:54 +00:00
</ div >
</ div >
2026-01-24 22:59:20 +00:00
</ div >
</ div >
2026-03-21 03:01:38 +00:00
<!-- Cloud Overview -->
2026-07-22 02:11:39 -04:00
< div data -controller-container tabindex = "0" class = "home-card controller-focusable order-3 lg:order-none" : class = "{ 'home-card-animate': animateCards }" style = "--card-stagger: 1" >
2026-03-21 03:01:38 +00:00
< div class = "home-card-shell" >
< div class = "home-card-inner p-6 flex flex-col h-full min-h-0" >
< div class = "home-card-header flex items-start justify-between mb-4 shrink-0" >
< div class = "home-card-text" >
< h2 class = "text-xl font-semibold text-white mb-1" >{{ t ( 'home.cloud' ) }}</ h2 >
< p class = "text-sm text-white/70" >{{ t ( 'home.cloudDesc' ) }}</ p >
2026-02-17 19:19:54 +00:00
</ div >
2026-03-21 03:01:38 +00:00
< RouterLink to = "/dashboard/cloud" :aria-label = "t('home.goToCloud')" class = "text-white/60 hover:text-white transition-colors" >
< svg class = "w-5 h-5" aria -hidden =" true " fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >< path stroke -linecap =" round " stroke -linejoin =" round " stroke -width =" 2 " d = "M9 5l7 7-7 7" /></ svg >
2026-03-17 23:56:37 +00:00
</ RouterLink >
2026-02-17 19:19:54 +00:00
</ div >
2026-03-21 03:01:38 +00:00
< div class = "home-card-stats grid grid-cols-1 sm:grid-cols-2 gap-4 mb-4 flex-1 min-h-0" >
< div class = "p-4 bg-white/5 rounded-lg" >< p class = "text-xs text-white/60 mb-1" >{{ t ( 'home.storageUsed' ) }}</ p >< p class = "text-2xl font-bold text-white" >{{ cloudStorageDisplay }}</ p ></ div >
< div class = "p-4 bg-white/5 rounded-lg" >< p class = "text-xs text-white/60 mb-1" >{{ t ( 'home.folders' ) }}</ p >< p class = "text-2xl font-bold text-white" >{{ cloudFolderDisplay }}</ p ></ div >
2026-03-17 23:56:37 +00:00
</ div >
2026-03-21 03:01:38 +00:00
< div class = "home-card-buttons flex gap-2 mt-auto pt-4 shrink-0" >
< RouterLink to = "/dashboard/cloud" class = "home-card-btn flex-1 px-4 py-2 glass-button rounded-lg text-sm font-medium text-center transition-colors" >{{ t ( 'home.viewFolders' ) }}</ RouterLink >
< button @click ="uploadFiles" class = "home-card-btn flex-1 px-4 py-2 glass-button rounded-lg text-sm font-medium text-center transition-colors" >{{ t ( 'home.uploadFiles' ) }}</ button >
2026-02-17 19:19:54 +00:00
</ div >
2026-01-24 22:59:20 +00:00
</ div >
</ div >
</ div >
2026-03-21 03:01:38 +00:00
2026-06-12 03:00:15 -04:00
<!-- Wallet Overview -->
< HomeWalletCard
2026-07-22 02:11:39 -04:00
class = "order-2 lg:order-none"
2026-06-12 03:00:15 -04:00
:animate = "animateCards"
:wallet-connected = "walletConnected"
:wallet-onchain = "walletOnchain"
:wallet-lightning = "walletLightning"
:wallet-ecash = "walletEcash"
2026-06-17 19:21:42 -04:00
:wallet-fedimint = "walletFedimint"
2026-07-14 21:57:12 +01:00
:wallet-ark = "walletArk"
2026-06-12 03:00:15 -04:00
:wallet-transactions = "walletTransactions"
:is-dev = "isDev"
2026-07-21 13:14:31 -04:00
@ show -scan =" showScanModal = true "
2026-06-12 03:00:15 -04:00
@ show -send =" showSendModal = true "
@ show -receive =" showReceiveModal = true "
@ show -transactions =" showTransactionsModal = true "
2026-06-17 19:21:42 -04:00
@ show -wallet-settings =" showWalletSettingsModal = true "
2026-06-12 03:00:15 -04:00
@faucet ="devFaucet"
@ open -in-mempool =" openInMempool "
/>
2026-06-11 00:24:40 -04:00
2026-03-21 03:01:38 +00:00
<!-- Network Overview -->
2026-07-22 02:11:39 -04:00
< div data -controller-container tabindex = "0" class = "home-card controller-focusable order-4 lg:order-none" : class = "{ 'home-card-animate': animateCards }" style = "--card-stagger: 3" >
2026-03-21 03:01:38 +00:00
< div class = "home-card-shell" >
< div class = "home-card-inner p-6 flex flex-col h-full min-h-0" >
< div class = "home-card-header flex items-start justify-between mb-4 shrink-0" >
< div class = "home-card-text" >
< h2 class = "text-xl font-semibold text-white mb-1" >{{ t ( 'home.network' ) }}</ h2 >
< p class = "text-sm text-white/70" >{{ t ( 'home.networkDesc' ) }}</ p >
</ div >
< RouterLink to = "/dashboard/server" :aria-label = "t('home.goToNetwork')" class = "text-white/60 hover:text-white transition-colors" >
< svg class = "w-5 h-5" aria -hidden =" true " fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >< path stroke -linecap =" round " stroke -linejoin =" round " stroke -width =" 2 " d = "M9 5l7 7-7 7" /></ svg >
</ RouterLink >
</ div >
< div class = "home-card-stats space-y-3 mb-4 flex-1 min-h-0" >
< div class = "flex items-center justify-between p-3 bg-white/5 rounded-lg" >
2026-04-01 13:06:57 +01:00
< div class = "flex items-center gap-3" >< div class = "w-2 h-2 rounded-full" : class = "torConnected ? 'bg-purple-400' : 'bg-white/40'" ></ div >< span class = "text-sm text-white/80" > Tor </ span ></ div >
< span class = "text-sm font-medium" : class = "torConnected ? 'text-purple-400' : 'text-white/40'" >{{ torConnected ? 'Connected' : 'Offline' }}</ span >
2026-03-21 03:01:38 +00:00
</ div >
< div class = "flex items-center justify-between p-3 bg-white/5 rounded-lg" >
2026-05-18 11:47:12 -04:00
< div class = "flex items-center gap-3" >< div class = "w-2 h-2 rounded-full" :class = "vpnDotClass" ></ div >< span class = "text-sm text-white/80" > VPN </ span ></ div >
< span class = "text-sm font-medium" :class = "vpnTextClass" >{{ vpnStatusLabel }}</ span >
2026-03-21 03:01:38 +00:00
</ div >
< div class = "flex items-center justify-between p-3 bg-white/5 rounded-lg" >
2026-05-18 11:47:12 -04:00
< div class = "flex items-center gap-3" >< div class = "w-2 h-2 rounded-full" :class = "bitcoinDotClass" ></ div >< span class = "text-sm text-white/80" > Bitcoin </ span ></ div >
< span class = "text-sm font-medium" :class = "bitcoinTextClass" >{{ bitcoinSyncDisplay }}</ span >
2026-03-21 03:01:38 +00:00
</ div >
2026-04-19 00:42:56 -04:00
< div class = "flex items-center justify-between p-3 bg-white/5 rounded-lg" >
< div class = "flex items-center gap-3" >< div class = "w-2 h-2 rounded-full" :class = "fipsDotClass" ></ div >< span class = "text-sm text-white/80" > FIPS </ span ></ div >
< span class = "text-sm font-medium" :class = "fipsTextClass" >{{ fipsStatusLabel }}</ span >
</ div >
2026-07-01 13:25:43 +00:00
< div v-if = "homeStatus.tollgateStatus" class="flex items-center justify-between p-3 bg-white/5 rounded-lg" >
< div class = "flex items-center gap-3" >< div class = "w-2 h-2 rounded-full" :class = "tollgateDotClass" ></ div >< span class = "text-sm text-white/80" > TollGate </ span ></ div >
< span class = "text-sm font-medium" :class = "tollgateTextClass" >{{ tollgateStatusLabel }}</ span >
</ div >
2026-03-21 03:01:38 +00:00
</ div >
< div class = "home-card-buttons flex gap-2 mt-auto pt-4 shrink-0" >
< RouterLink to = "/dashboard/server" class = "home-card-btn flex-1 px-4 py-2 glass-button rounded-lg text-sm font-medium text-center transition-colors" >{{ t ( 'home.manageNetwork' ) }}</ RouterLink >
</ div >
</ div >
</ div >
</ div >
2026-06-12 03:00:15 -04:00
<!-- App Store Recommendations -->
< div
v-if = "homeRecommendedApps.length > 0"
data -controller-container
tabindex = "0"
2026-07-22 02:11:39 -04:00
class = "home-card controller-focusable lg:col-span-2 order-5 lg:order-none"
2026-06-12 03:00:15 -04:00
: class = "{ 'home-card-animate': animateCards }"
style = "--card-stagger: 4"
>
< div class = "home-card-shell" >
< div class = "home-card-inner p-6 flex flex-col h-full min-h-0" >
< div class = "home-card-header flex items-start justify-between mb-4 shrink-0" >
< div class = "home-card-text" >
< h2 class = "text-xl font-semibold text-white mb-1" >{{ t ( 'home.recommendedApps' ) }}</ h2 >
< p class = "text-sm text-white/70" >{{ t ( 'home.recommendedAppsDesc' ) }}</ p >
</ div >
< RouterLink to = "/dashboard/marketplace" :aria-label = "t('home.goToAppStore')" class = "text-white/60 hover:text-white transition-colors" >
< svg class = "w-5 h-5" aria -hidden =" true " fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >< path stroke -linecap =" round " stroke -linejoin =" round " stroke -width =" 2 " d = "M9 5l7 7-7 7" /></ svg >
</ RouterLink >
</ div >
< div class = "home-card-stats grid grid-cols-1 md:grid-cols-3 gap-3 mb-4 flex-1 min-h-0" >
< button
v-for = "app in homeRecommendedApps"
:key = "app.id"
type = "button"
class = "w-full flex items-center gap-3 p-3 bg-white/5 rounded-lg text-left transition-colors hover:bg-white/10"
@click ="viewRecommendedApp(app)"
>
< img
v-if = "app.icon"
:src = "app.icon"
: alt = "app.title || app.id"
class = "w-10 h-10 rounded-lg archy-app-icon shrink-0"
@error ="handleImageError"
/>
< div v-else class = "w-10 h-10 rounded-lg bg-white/10 shrink-0" ></ div >
< div class = "min-w-0 flex-1" >
< p class = "text-sm font-medium text-white truncate" >{{ app . title || app . id }}</ p >
< p class = "text-xs text-white/55 truncate" >{{ marketplaceDescription ( app ) }}</ p >
</ div >
</ button >
</ div >
< div class = "home-card-buttons flex gap-2 mt-auto pt-4 shrink-0" >
< RouterLink to = "/dashboard/marketplace" class = "home-card-btn flex-1 px-4 py-2 glass-button rounded-lg text-sm font-medium text-center transition-colors" >{{ t ( 'home.browseStore' ) }}</ RouterLink >
</ div >
</ div >
</ div >
</ div >
2026-03-21 03:01:38 +00:00
2026-06-11 00:24:40 -04:00
<!-- Quick Start Goals -->
< div
v-if = "showQuickStart"
2026-07-22 02:11:39 -04:00
class = "home-card lg:col-span-2 transition-opacity duration-300 order-6 lg:order-none"
2026-06-11 00:24:40 -04:00
: class = "{ 'home-card-animate': animateCards, 'opacity-0 pointer-events-none': showWelcomeBlock && !animateCards }"
style = "--card-stagger: 5"
>
< div class = "home-card-shell" >
< div class = "home-card-inner px-6 py-6 flex flex-col h-full min-h-0" >
< div class = "flex items-start justify-between mb-2 shrink-0" >
< div >
< h2 class = "text-xl font-semibold text-white/96 mb-1" >{{ t ( 'home.quickStartGoals' ) }}</ h2 >
< p class = "text-sm text-white/60 mb-4" >{{ t ( 'home.quickStartDesc' ) }}</ p >
</ div >
< button @click ="dismissQuickStart" aria -label =" Dismiss Quick Start " class = "text-white/40 hover:text-white/80 transition-colors p-1 -mt-1 -mr-1" title = "Dismiss" >
< svg class = "w-5 h-5" aria -hidden =" true " 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 >
< div class = "grid grid-cols-1 gap-3 mt-auto" >
< RouterLink v-for = "goal in topGoals" :key="goal.id" :to="`/dashboard/goals/${goal.id}`" class="home-card-btn path-action-button path-action-button--continue flex items-center justify-center gap-3" >
< span >{{ goal . title }}</ span >
</ RouterLink >
</ div >
</ div >
</ div >
</ div >
2026-03-21 03:01:38 +00:00
<!-- System Stats -->
< HomeSystemCard
2026-07-22 02:11:39 -04:00
class = "order-7 lg:order-none"
2026-03-21 03:01:38 +00:00
:animate = "animateCards"
:loaded = "systemStatsLoaded"
:stats = "systemStats"
:uptime-display = "systemUptimeDisplay"
/>
2026-01-24 22:59:20 +00:00
</ div >
2026-03-05 07:36:43 +00:00
</ template >
2026-03-21 03:01:38 +00:00
<!-- Chat Mode -->
2026-03-04 07:09:31 +00:00
< div v-if = "uiMode.isChat" class="flex flex-col items-center justify-center min-h-[40vh]" >
2026-03-21 03:01:38 +00:00
< RouterLink to = "/dashboard/chat" class = "glass-button rounded-lg px-8 py-4 text-lg font-medium" >{{ t ( 'home.openAI' ) }}</ RouterLink >
2026-03-04 07:09:31 +00:00
</ div >
2026-03-17 23:56:37 +00:00
2026-03-28 17:01:17 +00:00
<!-- Wallet Modals -->
2026-07-21 13:14:31 -04:00
< WalletScanModal :show = "showScanModal" @close ="showScanModal = false" @sent ="loadWeb5Status()" />
< SendBitcoinModal :show = "showSendModal" @close ="showSendModal = false" @sent ="loadWeb5Status()" @scan ="showSendModal = false; showScanModal = true" />
< ReceiveBitcoinModal :show = "showReceiveModal" @close ="showReceiveModal = false" @received ="loadWeb5Status()" @scan ="showReceiveModal = false; showScanModal = true" />
2026-03-28 17:01:17 +00:00
< TransactionsModal :show = "showTransactionsModal" :transactions = "walletTransactions" @close ="showTransactionsModal = false" />
2026-06-17 19:21:42 -04:00
< WalletSettingsModal :show = "showWalletSettingsModal" @close ="showWalletSettingsModal = false" @changed ="loadWeb5Status()" />
2026-03-28 17:01:17 +00:00
</ div >
2026-01-24 22:59:20 +00:00
</ template >
< script setup lang = "ts" >
2026-05-18 11:47:12 -04:00
import { computed , ref , watch , onBeforeUnmount , onMounted } from 'vue'
2026-07-16 22:16:58 -04:00
import { RouterLink , useRoute , useRouter } from 'vue-router'
2026-03-11 13:45:59 +00:00
import { useI18n } from 'vue-i18n'
2026-07-21 13:14:31 -04:00
import WalletScanModal from '@/components/WalletScanModal.vue'
2026-03-17 23:56:37 +00:00
import SendBitcoinModal from '@/components/SendBitcoinModal.vue'
import ReceiveBitcoinModal from '@/components/ReceiveBitcoinModal.vue'
2026-03-18 22:05:21 +00:00
import TransactionsModal from '@/components/TransactionsModal.vue'
2026-06-17 19:21:42 -04:00
import WalletSettingsModal from '@/components/WalletSettingsModal.vue'
2026-01-24 22:59:20 +00:00
import { useAppStore } from '../stores/app'
2026-03-04 23:05:01 +00:00
import { useAppLauncherStore } from '@/stores/appLauncher'
2026-02-17 19:19:54 +00:00
import { useLoginTransitionStore } from '../stores/loginTransition'
2026-03-04 07:09:31 +00:00
import { useUIModeStore } from '@/stores/uiMode'
2026-05-18 11:47:12 -04:00
import { useHomeStatusStore } from '@/stores/homeStatus'
2026-01-24 22:59:20 +00:00
import { PackageState } from '../types/api'
2026-02-17 19:19:54 +00:00
import { playTypingSound } from '@/composables/useLoginSounds'
2026-03-04 07:09:31 +00:00
import { GOALS } from '@/data/goals'
import EasyHome from '@/components/EasyHome.vue'
2026-03-05 07:05:14 +00:00
import { fileBrowserClient } from '@/api/filebrowser-client'
2026-03-11 00:24:59 +00:00
import { rpcClient } from '@/api/rpc-client'
2026-06-11 00:24:40 -04:00
import { getAppUsage } from '@/utils/appUsage'
import { handleImageError , isServicePackage , isWebsitePackage , resolveAppIcon } from './apps/appsConfig'
import { useMarketplaceApp } from '@/composables/useMarketplaceApp'
2026-06-12 03:00:15 -04:00
import { getCuratedAppList , type MarketplaceApp } from './marketplace/marketplaceData'
2026-06-11 00:24:40 -04:00
import { getHomeRecommendedApps } from './home/homeRecommendations'
2026-03-21 03:01:38 +00:00
import HomeWalletCard from './home/HomeWalletCard.vue'
import HomeSystemCard from './home/HomeSystemCard.vue'
import type { WalletTransaction } from './home/HomeWalletCard.vue'
2026-03-04 07:09:31 +00:00
2026-03-21 03:01:38 +00:00
const { t } = useI18n ()
2026-03-04 23:05:01 +00:00
const router = useRouter ()
2026-07-16 22:16:58 -04:00
const route = useRoute ()
2026-03-04 07:09:31 +00:00
const uiMode = useUIModeStore ()
2026-03-18 22:05:21 +00:00
const isDev = import . meta . env . DEV
2026-07-16 22:16:58 -04:00
// ?tab=setup lands on the Setup tab (e.g. "Back to Goals" from a goal wizard)
const homeTab = ref < 'dashboard' | 'setup' > ( route . query . tab === 'setup' ? 'setup' : 'dashboard' )
watch (() => route . query . tab , ( tab ) => {
if ( tab === 'setup' ) homeTab . value = 'setup'
else if ( tab === 'dashboard' ) homeTab . value = 'dashboard'
})
2026-03-04 07:09:31 +00:00
const topGoals = GOALS . slice ( 0 , 3 )
2026-01-24 22:59:20 +00:00
2026-03-04 14:34:02 +00:00
const QUICK_START_APPS = [... new Set ( topGoals . flatMap (( g ) => g . requiredApps ))]
const QUICK_START_KEY = 'archipelago-quick-start-dismissed'
const QUICK_START_RESHOW_LOGINS = 5
2026-01-24 22:59:20 +00:00
const store = useAppStore ()
2026-05-18 11:47:12 -04:00
const homeStatus = useHomeStatusStore ()
2026-02-17 19:19:54 +00:00
const loginTransition = useLoginTransitionStore ()
2026-06-11 00:24:40 -04:00
const { setCurrentApp } = useMarketplaceApp ()
2026-02-17 19:19:54 +00:00
2026-03-11 13:45:59 +00:00
const LINE1 = t ( 'home.title' )
const LINE2 = t ( 'home.subtitle' )
2026-02-17 19:19:54 +00:00
const MS_PER_CHAR = 55
const displayLine1 = ref ( '' )
const displayLine2 = ref ( '' )
const showCaretLine1 = ref ( false )
const showCaretLine2 = ref ( false )
const showWelcomeBlock = ref ( false )
const hasTypedWelcome = ref ( false )
const animateCards = ref ( false )
let typingInterval : ReturnType < typeof setInterval > | null = null
2026-03-21 03:01:38 +00:00
const line1Text = computed (() => showWelcomeBlock . value ? displayLine1 . value : LINE1 )
const line2Text = computed (() => showWelcomeBlock . value ? displayLine2 . value : LINE2 )
2026-02-17 19:19:54 +00:00
onBeforeUnmount (() => {
if ( typingInterval ) clearInterval ( typingInterval )
2026-03-11 00:24:59 +00:00
if ( systemStatsInterval ) clearInterval ( systemStatsInterval )
2026-07-22 12:43:53 -04:00
if ( walletRefreshInterval ) clearInterval ( walletRefreshInterval )
2026-02-17 19:19:54 +00:00
})
2026-03-21 03:01:38 +00:00
watch (() => loginTransition . pendingWelcomeTyping , ( pending ) => { if ( pending ) showWelcomeBlock . value = true })
2026-02-17 19:19:54 +00:00
watch (() => loginTransition . startWelcomeTyping , ( shouldStart ) => {
if ( ! shouldStart || hasTypedWelcome . value ) return
2026-03-21 03:01:38 +00:00
hasTypedWelcome . value = true ; showWelcomeBlock . value = true
displayLine1 . value = '' ; displayLine2 . value = ''
showCaretLine1 . value = true ; showCaretLine2 . value = false
playTypingSound (); animateCards . value = true
2026-02-17 19:19:54 +00:00
let i = 0
typingInterval = setInterval (() => {
2026-03-21 03:01:38 +00:00
if ( i < LINE1 . length ) { displayLine1 . value = LINE1 . slice ( 0 , i + 1 ); i ++ }
else if ( i < LINE1 . length + LINE2 . length ) { showCaretLine1 . value = false ; showCaretLine2 . value = true ; displayLine2 . value = LINE2 . slice ( 0 , i - LINE1 . length + 1 ); i ++ }
else { if ( typingInterval ) clearInterval ( typingInterval ); typingInterval = null ; showCaretLine2 . value = false ; loginTransition . setStartWelcomeTyping ( false ) }
2026-02-17 19:19:54 +00:00
}, MS_PER_CHAR )
}, { immediate : true })
2026-01-24 22:59:20 +00:00
const packages = computed (() => store . packages )
2026-03-16 15:34:04 +00:00
const appCount = computed (() => Object . keys ( packages . value || {}). length )
2026-03-21 03:01:38 +00:00
const runningCount = computed (() => Object . values ( packages . value || {}). filter ( pkg => pkg . state === PackageState . Running ). length )
2026-03-04 14:34:02 +00:00
2026-06-11 00:24:40 -04:00
const quickLaunchApps = computed (() => {
const usage = getAppUsage ()
return Object . entries ( packages . value || {})
. filter (([ id , pkg ]) => ! isServicePackage ( id , pkg ) && ! isWebsitePackage ( id , pkg ))
. map (([ id , pkg ]) => ({
id ,
name : pkg . manifest ? . title || id ,
icon : resolveAppIcon ( id , pkg ),
state : pkg . state ,
usage : usage [ id ] ? . count || 0 ,
lastLaunchedAt : usage [ id ] ? . lastLaunchedAt || 0 ,
}))
. sort (( a , b ) => {
if ( b . usage !== a . usage ) return b . usage - a . usage
if ( b . lastLaunchedAt !== a . lastLaunchedAt ) return b . lastLaunchedAt - a . lastLaunchedAt
if (( b . state === PackageState . Running ? 1 : 0 ) !== ( a . state === PackageState . Running ? 1 : 0 )) {
return ( b . state === PackageState . Running ? 1 : 0 ) - ( a . state === PackageState . Running ? 1 : 0 )
}
return a . name . localeCompare ( b . name )
})
. slice ( 0 , 3 )
})
const homeRecommendedApps = computed (() => getHomeRecommendedApps ( getCuratedAppList (), packages . value , 3 ))
function viewRecommendedApp ( app : MarketplaceApp ) {
setCurrentApp ( app )
2026-06-12 03:00:15 -04:00
router . push ({ name : 'marketplace-app-detail' , params : { id : app . id }, query : { from : 'home' } }). catch (() => {})
2026-06-11 00:24:40 -04:00
}
function marketplaceDescription ( app : MarketplaceApp ) {
if ( typeof app . description === 'string' ) return app . description
return app . description ? . short || app . description ? . long || ''
}
2026-03-16 12:58:35 +00:00
2026-04-01 13:06:57 +01:00
// Network card data
const torConnected = computed (() => {
const torAddr = store . data ? .[ 'server-info' ] ? .[ 'tor-address' ]
return !! torAddr && torAddr . length > 0
})
2026-05-18 11:47:12 -04:00
const vpnConnected = computed (() => homeStatus . vpnStatus . connected === true || ( !! packages . value [ 'tailscale' ] && packages . value [ 'tailscale' ]. state === PackageState . Running ))
const vpnDotClass = computed (() => {
if ( vpnConnected . value ) return 'bg-orange-400'
return homeStatus . vpnKnown ? 'bg-white/40' : 'bg-white/25 animate-pulse'
})
const vpnTextClass = computed (() => vpnConnected . value ? 'text-orange-400' : ( homeStatus . vpnKnown ? 'text-white/40' : 'text-white/50' ))
const vpnStatusLabel = computed (() => {
if ( vpnConnected . value ) return homeStatus . vpnStatus . provider || 'WireGuard'
if ( ! homeStatus . vpnKnown ) return 'Checking…'
return 'Not configured'
})
2026-04-19 00:42:56 -04:00
const fipsDotClass = computed (() => {
2026-05-18 11:47:12 -04:00
const s = homeStatus . fipsStatus
2026-04-19 00:42:56 -04:00
if ( ! s || ! s . installed ) return 'bg-white/40'
2026-04-20 13:46:03 -04:00
if ( ! s . service_active ) return 'bg-white/40'
// Active but no anchor = degraded, not fully green
if ( s . anchor_connected === false ) return 'bg-orange-400'
return 'bg-green-400'
2026-04-19 00:42:56 -04:00
})
const fipsTextClass = computed (() => {
2026-05-18 11:47:12 -04:00
const s = homeStatus . fipsStatus
2026-04-19 00:42:56 -04:00
if ( ! s || ! s . installed ) return 'text-white/40'
2026-04-20 13:46:03 -04:00
if ( ! s . service_active ) return 'text-white/40'
if ( s . anchor_connected === false ) return 'text-orange-400'
return 'text-green-400'
2026-04-19 00:42:56 -04:00
})
const fipsStatusLabel = computed (() => {
2026-05-18 11:47:12 -04:00
const s = homeStatus . fipsStatus
if ( ! s ) return homeStatus . fipsLoadState === 'loading' ? 'Checking…' : '…'
2026-04-19 00:42:56 -04:00
if ( ! s . installed ) return 'Not installed'
2026-04-20 13:46:03 -04:00
if ( ! s . service_active ) {
if ( ! s . key_present ) return 'Awaiting seed'
return 'Inactive'
}
// Service is active — reflect anchor reachability in the label so the
// Home and Server rows flip in sync with the FIPS card.
if ( s . anchor_connected === false ) return 'No anchor'
const peers = s . authenticated_peer_count ?? 0
return peers === 1 ? 'Active · 1 peer' : `Active · ${ peers } peers`
2026-04-19 00:42:56 -04:00
})
2026-07-01 13:25:43 +00:00
const tollgateDotClass = computed (() => {
const s = homeStatus . tollgateStatus
if ( ! s || ! s . installed ) return 'bg-white/40'
return s . enabled ? 'bg-green-400' : 'bg-yellow-400'
})
const tollgateTextClass = computed (() => {
const s = homeStatus . tollgateStatus
if ( ! s || ! s . installed ) return 'text-white/40'
return s . enabled ? 'text-green-400' : 'text-yellow-400'
})
const tollgateStatusLabel = computed (() => {
const s = homeStatus . tollgateStatus
if ( ! s ) return homeStatus . tollgateLoadState === 'loading' ? 'Checking…' : 'Not configured'
if ( ! s . installed ) return 'Not installed'
return s . enabled ? 'Enabled' : 'Disabled'
})
2026-04-01 13:06:57 +01:00
const bitcoinSyncDisplay = computed (() => {
2026-05-18 11:47:12 -04:00
if ( homeStatus . stats . bitcoinAvailable === null ) return 'Checking…'
if ( ! homeStatus . stats . bitcoinAvailable ) return 'Not running'
if ( homeStatus . stats . bitcoinSyncPercent >= 99.9 ) return 'Synced'
if ( homeStatus . stats . bitcoinSyncPercent < 0.01 && homeStatus . stats . bitcoinBlockHeight === 0 ) return 'Loading...'
return ` ${ homeStatus . stats . bitcoinSyncPercent . toFixed ( 1 ) } %`
2026-04-01 13:06:57 +01:00
})
2026-05-18 11:47:12 -04:00
const bitcoinDotClass = computed (() => {
if ( homeStatus . stats . bitcoinAvailable === true ) return 'bg-orange-400'
if ( homeStatus . stats . bitcoinAvailable === false ) return 'bg-white/40'
return 'bg-white/25 animate-pulse'
})
const bitcoinTextClass = computed (() => homeStatus . stats . bitcoinAvailable ? 'text-orange-400' : ( homeStatus . stats . bitcoinAvailable === null ? 'text-white/50' : 'text-white/40' ))
2026-03-05 07:43:40 +00:00
2026-03-21 03:01:38 +00:00
// Quick Start
2026-03-04 14:34:02 +00:00
const quickStartDismissed = ref ( false )
2026-03-21 03:01:38 +00:00
const allQuickStartAppsInstalled = computed (() => QUICK_START_APPS . every (( appId ) => Object . keys ( packages . value ). includes ( appId )))
const showQuickStart = computed (() => ! allQuickStartAppsInstalled . value && ! quickStartDismissed . value )
2026-03-04 14:34:02 +00:00
function loadQuickStartState () {
2026-03-21 03:01:38 +00:00
try { const raw = localStorage . getItem ( QUICK_START_KEY ); if ( ! raw ) { quickStartDismissed . value = false ; return }; const data = JSON . parse ( raw ); if ( ! data . dismissed ) { quickStartDismissed . value = false ; return }; const loginCount = ( data . loginCount || 0 ) + 1 ; localStorage . setItem ( QUICK_START_KEY , JSON . stringify ({ dismissed : true , loginCount })); quickStartDismissed . value = loginCount % QUICK_START_RESHOW_LOGINS !== 0 } catch { quickStartDismissed . value = false }
2026-03-04 14:34:02 +00:00
}
2026-03-21 03:01:38 +00:00
function dismissQuickStart () { quickStartDismissed . value = true ; try { localStorage . setItem ( QUICK_START_KEY , JSON . stringify ({ dismissed : true , loginCount : 0 })) } catch { /* ignore */ } }
2026-03-04 14:34:02 +00:00
loadQuickStartState ()
2026-03-04 23:05:01 +00:00
2026-03-11 13:04:31 +00:00
// Update notification
2026-03-21 03:01:38 +00:00
const updateAvailable = ref ( false ); const updateDismissed = ref ( false ); const updateVersion = ref ( '' ); const updateChangelog = ref ( '' )
2026-03-11 13:04:31 +00:00
async function checkUpdateStatus () {
2026-03-21 03:01:38 +00:00
try { const res = await rpcClient . call < { update_available : boolean } > ({ method : 'update.status' }); updateAvailable . value = res . update_available } catch { /* unavailable */ }
if ( updateAvailable . value ) { try { const detail = await rpcClient . call < { update : { version : string ; changelog : string [] } | null } > ({ method : 'update.check' }); if ( detail . update ) { updateVersion . value = detail . update . version ; updateChangelog . value = detail . update . changelog . slice ( 0 , 2 ). join ( '; ' ) } } catch { /* unavailable */ } }
2026-03-11 13:04:31 +00:00
}
2026-03-21 03:01:38 +00:00
async function dismissUpdate () { updateDismissed . value = true ; try { await rpcClient . call ({ method : 'update.dismiss' }) } catch { /* ignore */ } }
2026-03-11 13:04:31 +00:00
2026-03-05 07:05:14 +00:00
// Cloud data
2026-03-21 03:01:38 +00:00
const cloudStorageUsed = ref < number | null >( null ); const cloudFolderCount = ref < number | null >( null )
function formatBytes ( bytes : number ) : string { if ( bytes === 0 ) return '0 B' ; const units = [ 'B' , 'KB' , 'MB' , 'GB' , 'TB' ]; const i = Math . floor ( Math . log ( bytes ) / Math . log ( 1024 )); const val = bytes / Math . pow ( 1024 , i ); return ` ${ val < 10 ? val . toFixed ( 1 ) : Math . round ( val ) } ${ units [ i ] } ` }
const cloudStorageDisplay = computed (() => cloudStorageUsed . value !== null ? formatBytes ( cloudStorageUsed . value ) : '...' )
const cloudFolderDisplay = computed (() => cloudFolderCount . value !== null ? String ( cloudFolderCount . value ) : '...' )
2026-03-05 07:05:14 +00:00
onMounted ( async () => {
2026-03-21 03:01:38 +00:00
try { const usage = await fileBrowserClient . getUsage (); cloudStorageUsed . value = usage . totalSize ; cloudFolderCount . value = usage . folderCount } catch { /* not running */ }
2026-06-15 16:29:44 -04:00
loadSystemStats (); systemStatsInterval = setInterval ( loadSystemStats , 10000 ); checkUpdateStatus (); loadWeb5Status ()
2026-07-22 12:43:53 -04:00
// Poll wallet balances/transactions like Web5.vue does — without this a
// pending on-chain receive (or a fresh instant payment) only shows up
// after a manual wallet action or a remount.
walletRefreshInterval = setInterval ( loadWeb5Status , 30000 )
2026-03-05 07:05:14 +00:00
})
2026-03-18 22:05:21 +00:00
// Wallet modals
2026-07-21 13:14:31 -04:00
const showScanModal = ref ( false ); const showSendModal = ref ( false ); const showReceiveModal = ref ( false ); const showTransactionsModal = ref ( false ); const showWalletSettingsModal = ref ( false )
2026-03-17 23:56:37 +00:00
2026-03-21 03:01:38 +00:00
async function devFaucet () { try { await rpcClient . call ({ method : 'dev.faucet' , params : { amount_sats : 1 _000_000 } }); await loadWeb5Status () } catch { /* ignore */ } }
2026-03-18 21:06:14 +00:00
2026-06-17 19:21:42 -04:00
const walletConnected = ref ( false ); const walletOnchain = ref ( 0 ); const walletLightning = ref ( 0 ); const walletEcash = ref ( 0 ); const walletFedimint = ref ( 0 )
2026-07-14 21:57:12 +01:00
const walletArk = ref ( 0 )
2026-03-17 23:56:37 +00:00
const walletTransactions = ref < WalletTransaction [] >([])
2026-07-14 03:39:32 +01:00
// Overlay the explorer above the current page — never navigate away.
function openInMempool ( txHash : string ) { useAppLauncherStore (). openSession ( 'mempool' , { path : `/tx/ ${ txHash } ` }) }
2026-03-11 13:04:31 +00:00
2026-07-03 02:46:48 +00:00
// wallet.ecash-history's shape (see handle_wallet_ecash_history in
// api/rpc/wallet.rs) — distinct from the LND-shaped WalletTransaction used
// elsewhere in this file, so it's mapped into that shape below rather than
// widening WalletTransaction itself with a pile of ecash-only fields.
interface EcashTransaction {
id : string
tx_type : 'send' | 'receive'
amount_sats : number
timestamp : string
description : string
mint_url : string
peer : string
2026-07-14 21:57:12 +01:00
kind : 'cashu' | 'fedimint' | 'ark'
2026-07-03 02:46:48 +00:00
}
function ecashToWalletTransaction ( tx : EcashTransaction ) : WalletTransaction {
return {
tx_hash : tx . id ,
amount_sats : tx . amount_sats ,
direction : tx . tx_type === 'receive' ? 'incoming' : 'outgoing' ,
num_confirmations : 1 ,
time_stamp : Math . floor ( new Date ( tx . timestamp ). getTime () / 1000 ),
total_fees : 0 ,
dest_addresses : [],
label : tx . description ,
block_height : 0 ,
2026-07-13 14:10:45 +01:00
kind : tx . kind ,
2026-07-03 02:46:48 +00:00
}
}
2026-03-11 13:04:31 +00:00
async function loadWeb5Status () {
2026-06-20 18:58:52 -04:00
// A transient RPC timeout must NOT flash the balance to 0 ("wallet says 0 when
// there is a balance"). On failure keep the last-known value — the refs start
// at 0, so only the very first load before any success shows 0.
try { const res = await rpcClient . call < { balance_sats : number ; channel_balance_sats : number } > ({ method : 'lnd.getinfo' , timeout : 5000 }); walletOnchain . value = res . balance_sats || 0 ; walletLightning . value = res . channel_balance_sats || 0 ; walletConnected . value = true } catch { walletConnected . value = false }
try { const res = await rpcClient . call < { balance_sats : number } > ({ method : 'wallet.ecash-balance' , timeout : 5000 }); walletEcash . value = res . balance_sats ?? 0 } catch { /* keep last-known balance */ }
try { const res = await rpcClient . call < { balance_sats : number } > ({ method : 'wallet.fedimint-balance' , timeout : 5000 }); walletFedimint . value = res . balance_sats ?? 0 } catch { /* keep last-known balance */ }
2026-07-14 21:57:12 +01:00
try { const res = await rpcClient . call < { spendable_sats : number } > ({ method : 'wallet.ark-balance' , timeout : 5000 }); walletArk . value = res . spendable_sats ?? 0 } catch { /* keep last-known balance */ }
2026-07-03 02:46:48 +00:00
// Merge LND transactions with ecash/Fedimint history (wallet.ecash-history
// already unifies both) — previously only LND transactions were fetched
// here, so any Cashu or Fedimint receive (e.g. a TollGate payment) never
// appeared in the Transactions modal even though the balance included it.
let lndTxs : WalletTransaction [] = []
2026-07-13 14:10:45 +01:00
try { const res = await rpcClient . call < { transactions : WalletTransaction []; incoming_pending_count : number } > ({ method : 'lnd.gettransactions' , timeout : 5000 }); lndTxs = ( res . transactions || []). map ( tx => ({ ... tx , kind : 'onchain' as const })) } catch { /* keep last-known transactions */ }
let lightningTxs : WalletTransaction [] = []
try { const res = await rpcClient . call < { transactions : WalletTransaction [] } > ({ method : 'lnd.lightning-history' , timeout : 5000 }); lightningTxs = res . transactions || [] } catch { /* keep last-known transactions */ }
2026-07-03 02:46:48 +00:00
let ecashTxs : WalletTransaction [] = []
try { const res = await rpcClient . call < { transactions : EcashTransaction [] } > ({ method : 'wallet.ecash-history' , timeout : 5000 }); ecashTxs = ( res . transactions || []). map ( ecashToWalletTransaction ) } catch { /* keep last-known transactions */ }
2026-07-13 14:10:45 +01:00
walletTransactions . value = [... lndTxs , ... lightningTxs , ... ecashTxs ]. sort (( a , b ) => b . time_stamp - a . time_stamp )
2026-03-11 13:04:31 +00:00
}
2026-03-11 00:24:59 +00:00
// System stats
2026-05-18 11:47:12 -04:00
const systemStatsLoaded = computed (() => homeStatus . systemStatsLoaded )
const systemStats = computed (() => ({
... homeStatus . stats ,
bitcoinAvailable : homeStatus . stats . bitcoinAvailable === true ,
2026-06-16 02:57:35 -04:00
bitcoinStale : homeStatus . bitcoinStale ,
2026-05-18 11:47:12 -04:00
}))
const systemUptimeDisplay = computed (() => { if ( homeStatus . stats . uptimeSecs === 0 ) return t ( 'home.systemMonitoring' ); const days = Math . floor ( homeStatus . stats . uptimeSecs / 86400 ); const hours = Math . floor (( homeStatus . stats . uptimeSecs % 86400 ) / 3600 ); if ( days > 0 ) return `Uptime: ${ days } d ${ hours } h` ; const mins = Math . floor (( homeStatus . stats . uptimeSecs % 3600 ) / 60 ); return `Uptime: ${ hours } h ${ mins } m` })
2026-03-11 00:24:59 +00:00
let systemStatsInterval : ReturnType < typeof setInterval > | null = null
2026-07-22 12:43:53 -04:00
let walletRefreshInterval : ReturnType < typeof setInterval > | null = null
2026-03-11 00:24:59 +00:00
async function loadSystemStats () {
2026-05-18 11:47:12 -04:00
await homeStatus . refresh ( packages . value )
2026-03-04 23:05:01 +00:00
}
2026-03-21 03:01:38 +00:00
function uploadFiles () { const pkg = packages . value [ 'filebrowser' ]; if ( pkg && pkg . state === PackageState . Running ) { const host = window . location . hostname ; useAppLauncherStore (). open ({ url : `http:// ${ host } :8083` , title : 'File Browser' }) } else { router . push ( '/dashboard/cloud' ) } }
2026-01-24 22:59:20 +00:00
</ script >
2026-02-17 19:19:54 +00:00
< style scoped >
2026-03-21 03:01:38 +00:00
. typing - caret :: after { content : '' ; display : inline - block ; width : 3 px ; height : 1.1 em ; background : # fbbf24 ; margin - left : 2 px ; vertical - align : text - bottom ; animation : caret - blink 0.7 s step - end infinite ; }
@ keyframes caret - blink { 0 % , 100 % { opacity : 1 ; } 50 % { opacity : 0 ; } }
2026-03-22 03:30:21 +00:00
</ style >
< style >
/* Home card styles — unscoped so they reach child components (HomeWalletCard, HomeSystemCard) */
2026-03-21 03:01:38 +00:00
. grid > . home - card { min - height : 280 px ; }
. home - card - shell { background - color : rgba ( 0 , 0 , 0 , 0.65 ); backdrop - filter : blur ( 18 px ); - webkit - backdrop - filter : blur ( 18 px ); border - radius : 1 rem ; overflow : hidden ; box - shadow : 0 8 px 24 px rgba ( 0 , 0 , 0 , 0.45 ); border : 1 px solid transparent ; height : 100 % ; }
. home - card - animate . home - card - shell { animation : card - fly - in 1.2 s cubic - bezier ( 0.25 , 0.46 , 0.45 , 0.94 ) forwards ; animation - delay : calc ( var ( -- card - stagger ) * 0.18 s ); opacity : 0 ; transform : translateY ( 50 px ) scale ( 0.92 ); }
@ keyframes card - fly - in { 0 % { opacity : 0 ; transform : translateY ( 50 px ) scale ( 0.92 ); border - color : transparent ; } 75 % { opacity : 1 ; transform : translateY ( 0 ) scale ( 1 ); border - color : transparent ; } 100 % { opacity : 1 ; transform : translateY ( 0 ) scale ( 1 ); border - color : rgba ( 255 , 255 , 255 , 0.18 ); } }
. home - card - inner { overflow : hidden ; opacity : 0 ; }
. home - card - animate . home - card - inner { animation : inner - draw 0.8 s cubic - bezier ( 0.25 , 0.46 , 0.45 , 0.94 ) forwards ; animation - delay : calc ( var ( -- card - stagger ) * 0.18 s + 0.9 s ); }
@ keyframes inner - draw { 0 % { opacity : 0 ; clip - path : inset ( 0 100 % 0 0 ); } 15 % { opacity : 1 ; } 100 % { opacity : 1 ; clip - path : inset ( 0 0 0 0 ); } }
. home - card - text { overflow : hidden ; }
. home - card - stats { overflow : hidden ; }
. home - card - btn { opacity : 0 ; transform : scale ( 0.5 ); border - color : transparent ; min - height : 44 px ; padding - top : 10 px ; padding - bottom : 10 px ; }
. home - card - animate . home - card - btn { animation : btn - pop 0.5 s cubic - bezier ( 0.34 , 1.56 , 0.64 , 1 ) forwards ; animation - delay : calc ( var ( -- card - stagger ) * 0.18 s + 1.5 s ); }
@ keyframes btn - pop { 0 % { opacity : 0 ; transform : scale ( 0.5 ); border - color : transparent ; } 85 % { opacity : 1 ; transform : scale ( 1 ); border - color : transparent ; } 100 % { opacity : 1 ; transform : scale ( 1 ); border - color : rgba ( 255 , 255 , 255 , 0.18 ); } }
. home - card : not (. home - card - animate ) . home - card - inner , . home - card : not (. home - card - animate ) . home - card - btn { opacity : 1 ; animation : none ; clip - path : none ; transform : none ; border - color : rgba ( 255 , 255 , 255 , 0.18 ); }
. home - card : not (. home - card - animate ) . home - card - shell { border - color : rgba ( 255 , 255 , 255 , 0.18 ); }
2026-02-17 19:19:54 +00:00
</ style >