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>
This commit is contained in:
Dorian
2026-03-17 23:56:37 +00:00
co-authored by Claude Opus 4.6
parent 70f1348c15
commit d37ec1dea5
48 changed files with 3432 additions and 438 deletions
+59 -84
View File
@@ -189,38 +189,22 @@
:class="{ 'glass-throw-mobile-tabs': showZoomIn }"
style="background: rgba(0, 0, 0, 0.25); backdrop-filter: blur(18px); -webkit-backdrop-filter: blur(18px); transform: translateZ(0);"
>
<div class="glass-card p-2 rounded-lg flex gap-2 relative">
<!-- Animated Active Indicator -->
<div
class="absolute top-2 bottom-2 rounded-lg bg-white/20 transition-all duration-300 ease-out"
:style="{
left: `${appsTabIndicatorLeft}px`,
width: `${appsTabIndicatorWidth}px`,
}"
></div>
<div class="mode-switcher mode-switcher-full">
<RouterLink
ref="appsTabRef"
to="/dashboard/apps"
class="flex-1 px-4 py-2 rounded-lg font-medium transition-all duration-300 text-center relative z-10"
:class="{
'bg-white/20 text-white': route.path === '/dashboard/apps' || route.path.startsWith('/dashboard/apps/'),
'text-white/60 hover:text-white/80': !(route.path === '/dashboard/apps' || route.path.startsWith('/dashboard/apps/'))
}"
>
My Apps
</RouterLink>
class="mode-switcher-btn"
:class="{ 'mode-switcher-btn-active': (route.path === '/dashboard/apps' || route.path.startsWith('/dashboard/apps/')) && route.query.tab !== 'services' }"
>My Apps</RouterLink>
<RouterLink
ref="marketplaceTabRef"
to="/dashboard/marketplace"
class="flex-1 px-4 py-2 rounded-lg font-medium transition-all duration-300 text-center relative z-10"
:class="{
'bg-white/20 text-white': route.path === '/dashboard/marketplace' || route.path.startsWith('/dashboard/marketplace/'),
'text-white/60 hover:text-white/80': !(route.path === '/dashboard/marketplace' || route.path.startsWith('/dashboard/marketplace/'))
}"
>
App Store
</RouterLink>
class="mode-switcher-btn"
:class="{ 'mode-switcher-btn-active': route.path === '/dashboard/marketplace' || route.path.startsWith('/dashboard/marketplace/') }"
>App Store</RouterLink>
<RouterLink
to="/dashboard/apps?tab=services"
class="mode-switcher-btn"
:class="{ 'mode-switcher-btn-active': route.query.tab === 'services' }"
>Services</RouterLink>
</div>
</div>
@@ -232,38 +216,27 @@
style="background: rgba(0, 0, 0, 0.25); backdrop-filter: blur(18px); -webkit-backdrop-filter: blur(18px); transform: translateZ(0);"
:style="{ top: showAppsTabs ? '80px' : '0' }"
>
<div class="glass-card p-2 rounded-lg flex gap-2 relative">
<!-- Animated Active Indicator -->
<div
class="absolute top-2 bottom-2 rounded-lg bg-white/20 transition-all duration-300 ease-out"
:style="{
left: `${networkTabIndicatorLeft}px`,
width: `${networkTabIndicatorWidth}px`,
}"
></div>
<div class="mode-switcher mode-switcher-full">
<RouterLink
to="/dashboard/web5"
class="mode-switcher-btn"
:class="{ 'mode-switcher-btn-active': route.path === '/dashboard/web5' || route.path.startsWith('/dashboard/web5/') }"
>Web5</RouterLink>
<RouterLink
ref="cloudTabRef"
to="/dashboard/cloud"
class="flex-1 px-4 py-2 rounded-lg font-medium transition-all duration-300 text-center relative z-10"
:class="{
'bg-white/20 text-white': route.path === '/dashboard/cloud' || route.path.startsWith('/dashboard/cloud/'),
'text-white/60 hover:text-white/80': !(route.path === '/dashboard/cloud' || route.path.startsWith('/dashboard/cloud/'))
}"
>
Cloud
</RouterLink>
class="mode-switcher-btn"
:class="{ 'mode-switcher-btn-active': route.path === '/dashboard/cloud' || route.path.startsWith('/dashboard/cloud/') }"
>Cloud</RouterLink>
<RouterLink
ref="serverTabRef"
to="/dashboard/server"
class="flex-1 px-4 py-2 rounded-lg font-medium transition-all duration-300 text-center relative z-10"
:class="{
'bg-white/20 text-white': route.path === '/dashboard/server' || route.path.startsWith('/dashboard/server/'),
'text-white/60 hover:text-white/80': !(route.path === '/dashboard/server' || route.path.startsWith('/dashboard/server/'))
}"
>
Network
</RouterLink>
class="mode-switcher-btn"
:class="{ 'mode-switcher-btn-active': route.path === '/dashboard/server' || route.path.startsWith('/dashboard/server/') }"
>Network</RouterLink>
<RouterLink
to="/dashboard/mesh"
class="mode-switcher-btn"
:class="{ 'mode-switcher-btn-active': route.path === '/dashboard/mesh' || route.path.startsWith('/dashboard/mesh/') }"
>Mesh</RouterLink>
</div>
</div>
@@ -273,7 +246,7 @@
<Transition :name="getTransitionName(route)">
<div :key="route.path" class="view-wrapper">
<div
v-if="route.path === '/dashboard/chat'"
v-if="route.path === '/dashboard/chat' || route.path === '/dashboard/mesh'"
class="h-full"
>
<component :is="Component" />
@@ -524,7 +497,7 @@ const showNetworkTabs = computed(() => {
if (typeof window === 'undefined') return false
if (window.innerWidth >= 768) return false
if (route.name === 'cloud-folder') return false
return route.path.includes('/server') || route.path.includes('/cloud')
return route.path.includes('/server') || route.path.includes('/cloud') || route.path.includes('/web5') || route.path.includes('/mesh')
})
// Top padding for content div to clear fixed mobile tab overlays
@@ -700,7 +673,7 @@ const desktopNavItems = computed(() => {
const gamerMobileNav: NavItem[] = [
{ path: '/dashboard', label: 'Home', icon: 'home' },
{ path: '/dashboard/apps', label: 'Apps', icon: 'apps', isCombined: true },
{ path: '/dashboard/cloud', label: 'Network', icon: 'server', isCombined: true },
{ path: '/dashboard/web5', label: 'Web5', icon: 'web5', isCombined: true },
{ path: '/dashboard/settings', label: 'Settings', icon: 'settings' },
]
@@ -754,6 +727,7 @@ async function handleLogout() {
// Track previous route for transition logic
let previousPath = ''
let previousTab = ''
// Tab order for vertical transitions
const tabOrder = [
@@ -809,38 +783,38 @@ function getTransitionName(currentRoute: RouteLocationNormalizedLoaded) {
let transitionName = 'fade'
// Mobile: Horizontal slide transitions between Apps and Marketplace (check first on mobile)
// Mobile: Horizontal slide transitions between sub-tabs
if (typeof window !== 'undefined' && window.innerWidth < 768) {
// From Marketplace to Apps: slide right
if (wasMarketplaceList && isAppsList) {
transitionName = 'slide-right'
// Apps group: My Apps (0) → App Store (1) → Services (2)
const isServices = currentPath === '/dashboard/apps' && currentRoute.query.tab === 'services'
const wasServices = previousTab === 'services'
const currentAppsIdx = isServices ? 2
: currentPath === '/dashboard/marketplace' ? 1
: currentPath === '/dashboard/apps' ? 0 : -1
const prevAppsIdx = wasServices ? 2
: previousPath === '/dashboard/marketplace' ? 1
: previousPath === '/dashboard/apps' ? 0 : -1
// Web5 group: Web5 (0) → Cloud (1) → Network (2) → Mesh (3)
const web5TabOrder = ['/dashboard/web5', '/dashboard/cloud', '/dashboard/server', '/dashboard/mesh']
const currentWeb5Idx = web5TabOrder.indexOf(currentPath)
const prevWeb5Idx = web5TabOrder.indexOf(previousPath)
// Apps sub-tab transitions
if (currentAppsIdx !== -1 && prevAppsIdx !== -1 && currentAppsIdx !== prevAppsIdx) {
transitionName = currentAppsIdx > prevAppsIdx ? 'slide-left' : 'slide-right'
}
// From Apps to Marketplace: slide left
else if (wasAppsList && isMarketplaceList) {
transitionName = 'slide-left'
// Web5 sub-tab transitions
else if (currentWeb5Idx !== -1 && prevWeb5Idx !== -1 && currentWeb5Idx !== prevWeb5Idx) {
transitionName = currentWeb5Idx > prevWeb5Idx ? 'slide-left' : 'slide-right'
}
// From Network to Cloud: slide right
else if (previousPath === '/dashboard/server' && isCloudList) {
transitionName = 'slide-right'
}
// From Cloud to Network: slide left
else if (wasCloudList && currentPath === '/dashboard/server') {
transitionName = 'slide-left'
}
// Vertical transition: between main tabs (mobile fallback)
// Vertical transition: between main bottom nav tabs
else {
const currentIndex = tabOrder.indexOf(currentPath)
const previousIndex = tabOrder.indexOf(previousPath)
if (currentIndex !== -1 && previousIndex !== -1 && currentIndex !== previousIndex) {
// Moving down the menu (visual down)
if (currentIndex > previousIndex) {
transitionName = 'slide-down'
}
// Moving up the menu (visual up)
else {
transitionName = 'slide-up'
}
transitionName = currentIndex > previousIndex ? 'slide-down' : 'slide-up'
}
}
}
@@ -892,6 +866,7 @@ function getTransitionName(currentRoute: RouteLocationNormalizedLoaded) {
// Update previous path for next transition
previousPath = currentPath
previousTab = (currentRoute.query.tab as string) || ''
return transitionName
}