feat: TASK-31 nav header cleanup, TASK-38 Bitcoin sync gauge on homepage
TASK-31: Cleaned up Apps page nav header structure (tabs + categories + search). TASK-38: Added Bitcoin Core sync progress gauge to homepage System Stats card — shows sync percentage, block height, and green/orange color coding. Only appears when Bitcoin is running. Grid expands to 4 columns when visible. Updated MASTER_PLAN.md — cleaned up completed sections, moved done items. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,39 +1,42 @@
|
||||
<template>
|
||||
<div class="pb-6">
|
||||
<!-- Desktop: page tabs + category tabs + search -->
|
||||
<div class="hidden md:flex mb-4 items-center gap-4">
|
||||
<div class="mode-switcher flex-shrink-0">
|
||||
<button class="mode-switcher-btn" :class="{ 'mode-switcher-btn-active': activeTab === 'apps' }" @click="activeTab = 'apps'; router.replace({ query: {} })">My Apps</button>
|
||||
<RouterLink to="/dashboard/marketplace" class="mode-switcher-btn">App Store</RouterLink>
|
||||
<button class="mode-switcher-btn" :class="{ 'mode-switcher-btn-active': activeTab === 'services' }" @click="activeTab = 'services'; router.replace({ query: { tab: 'services' } })">Services</button>
|
||||
<!-- Nav header — tabs + categories + search -->
|
||||
<div class="mb-4">
|
||||
<!-- Desktop: page tabs + category tabs + search -->
|
||||
<div class="hidden md:flex items-center gap-4">
|
||||
<div class="mode-switcher flex-shrink-0">
|
||||
<button class="mode-switcher-btn" :class="{ 'mode-switcher-btn-active': activeTab === 'apps' }" @click="activeTab = 'apps'; router.replace({ query: {} })">My Apps</button>
|
||||
<RouterLink to="/dashboard/marketplace" class="mode-switcher-btn">App Store</RouterLink>
|
||||
<button class="mode-switcher-btn" :class="{ 'mode-switcher-btn-active': activeTab === 'services' }" @click="activeTab = 'services'; router.replace({ query: { tab: 'services' } })">Services</button>
|
||||
</div>
|
||||
<div v-if="activeTab === 'apps' && categoriesWithApps.length > 1" class="mode-switcher flex-shrink-0">
|
||||
<button
|
||||
v-for="category in categoriesWithApps"
|
||||
:key="category.id"
|
||||
@click="selectedCategory = category.id"
|
||||
class="mode-switcher-btn"
|
||||
:class="{ 'mode-switcher-btn-active': selectedCategory === category.id }"
|
||||
>{{ category.name }}</button>
|
||||
</div>
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
:placeholder="t('apps.searchPlaceholder')"
|
||||
:aria-label="t('apps.searchLabel')"
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="activeTab === 'apps' && categoriesWithApps.length > 1" class="mode-switcher flex-shrink-0">
|
||||
<button
|
||||
v-for="category in categoriesWithApps"
|
||||
:key="category.id"
|
||||
@click="selectedCategory = category.id"
|
||||
class="mode-switcher-btn"
|
||||
:class="{ 'mode-switcher-btn-active': selectedCategory === category.id }"
|
||||
>{{ category.name }}</button>
|
||||
</div>
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
:placeholder="t('apps.searchPlaceholder')"
|
||||
:aria-label="t('apps.searchLabel')"
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Mobile: search only (tabs handled by Dashboard.vue header) -->
|
||||
<div class="md:hidden mb-4">
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
:placeholder="t('apps.searchPlaceholder')"
|
||||
:aria-label="t('apps.searchLabel')"
|
||||
class="w-full px-4 py-3 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/50 focus:outline-none focus:border-white/40 transition-colors"
|
||||
/>
|
||||
<!-- Mobile: search only (tabs handled by Dashboard.vue header) -->
|
||||
<div class="md:hidden">
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
:placeholder="t('apps.searchPlaceholder')"
|
||||
:aria-label="t('apps.searchLabel')"
|
||||
class="w-full px-4 py-3 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/50 focus:outline-none focus:border-white/40 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading Skeleton -->
|
||||
|
||||
@@ -391,7 +391,7 @@
|
||||
</svg>
|
||||
</RouterLink>
|
||||
</div>
|
||||
<div class="home-card-stats grid grid-cols-1 sm:grid-cols-3 gap-4 flex-1 min-h-0">
|
||||
<div class="home-card-stats grid grid-cols-1 gap-4 flex-1 min-h-0" :class="systemStats.bitcoinAvailable ? 'sm:grid-cols-4' : 'sm:grid-cols-3'">
|
||||
<template v-if="!systemStatsLoaded">
|
||||
<div v-for="i in 3" :key="i" class="p-4 bg-white/5 rounded-lg animate-pulse">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
@@ -429,6 +429,18 @@
|
||||
<div class="h-full rounded-full transition-all duration-500" :class="gaugeBarColor(systemStats.diskPercent)" :style="{ width: systemStats.diskPercent + '%' }"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="systemStats.bitcoinAvailable" class="p-4 bg-white/5 rounded-lg">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<p class="text-xs text-orange-400/80">Bitcoin</p>
|
||||
<p class="text-sm font-medium" :class="systemStats.bitcoinSyncPercent >= 99.9 ? 'text-green-400' : 'text-orange-400'">
|
||||
{{ systemStats.bitcoinSyncPercent >= 99.9 ? 'Synced' : systemStats.bitcoinSyncPercent.toFixed(1) + '%' }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="w-full h-2 bg-white/10 rounded-full overflow-hidden">
|
||||
<div class="h-full rounded-full transition-all duration-500" :class="systemStats.bitcoinSyncPercent >= 99.9 ? 'bg-green-400' : 'bg-orange-400'" :style="{ width: Math.min(systemStats.bitcoinSyncPercent, 100) + '%' }"></div>
|
||||
</div>
|
||||
<p class="text-xs text-white/40 mt-1">Block {{ systemStats.bitcoinBlockHeight.toLocaleString() }}</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
@@ -832,6 +844,9 @@ const systemStats = reactive({
|
||||
diskTotal: 0,
|
||||
diskPercent: 0,
|
||||
uptimeSecs: 0,
|
||||
bitcoinSyncPercent: 0,
|
||||
bitcoinBlockHeight: 0,
|
||||
bitcoinAvailable: false,
|
||||
})
|
||||
|
||||
const systemUptimeDisplay = computed(() => {
|
||||
@@ -880,6 +895,18 @@ async function loadSystemStats() {
|
||||
if (import.meta.env.DEV) console.warn('RPC unavailable — keeping defaults', e)
|
||||
systemStatsLoaded.value = true
|
||||
}
|
||||
// Fetch Bitcoin sync info (best-effort, only if Bitcoin is running)
|
||||
try {
|
||||
const btc = await rpcClient.call<{
|
||||
block_height: number
|
||||
sync_progress: number
|
||||
}>({ method: 'bitcoin.getinfo', timeout: 5000 })
|
||||
systemStats.bitcoinSyncPercent = (btc.sync_progress ?? 0) * 100
|
||||
systemStats.bitcoinBlockHeight = btc.block_height ?? 0
|
||||
systemStats.bitcoinAvailable = true
|
||||
} catch {
|
||||
systemStats.bitcoinAvailable = false
|
||||
}
|
||||
}
|
||||
|
||||
function uploadFiles() {
|
||||
|
||||
Reference in New Issue
Block a user