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:
co-authored by
Claude Opus 4.6
parent
70f1348c15
commit
d37ec1dea5
+84
-35
@@ -1,19 +1,20 @@
|
||||
<template>
|
||||
<div class="pb-6">
|
||||
<!-- Desktop: tabs + search in one row -->
|
||||
<div class="hidden md:flex items-center gap-4 mb-4">
|
||||
<!-- 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'"
|
||||
>My Apps</button>
|
||||
<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': activeTab === 'services' }"
|
||||
@click="activeTab = 'services'"
|
||||
>Services</button>
|
||||
:class="{ 'mode-switcher-btn-active': selectedCategory === category.id }"
|
||||
>{{ category.name }}</button>
|
||||
</div>
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
@@ -24,21 +25,8 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Mobile: tabs + search -->
|
||||
<!-- Mobile: search only (tabs handled by Dashboard.vue header) -->
|
||||
<div class="md:hidden mb-4">
|
||||
<div class="mode-switcher mode-switcher-full mb-3">
|
||||
<button
|
||||
class="mode-switcher-btn"
|
||||
:class="{ 'mode-switcher-btn-active': activeTab === 'apps' }"
|
||||
@click="activeTab = 'apps'"
|
||||
>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'"
|
||||
>Services</button>
|
||||
</div>
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
@@ -253,6 +241,8 @@
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
|
||||
|
||||
<!-- Action error toast -->
|
||||
<Transition name="fade">
|
||||
<div v-if="actionError" class="fixed bottom-20 left-1/2 -translate-x-1/2 z-50 max-w-md w-full px-4" role="alert" aria-live="assertive">
|
||||
@@ -267,7 +257,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, onBeforeUnmount } from 'vue'
|
||||
import { useRouter, RouterLink } from 'vue-router'
|
||||
import { useRouter, useRoute, RouterLink } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAppStore } from '../stores/app'
|
||||
|
||||
@@ -277,33 +267,88 @@ import { PackageState, type PackageDataEntry } from '../types/api'
|
||||
import { useModalKeyboard } from '@/composables/useModalKeyboard'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const store = useAppStore()
|
||||
|
||||
// Tabs
|
||||
const activeTab = ref<'apps' | 'services'>('apps')
|
||||
// Tabs — support ?tab=services from Marketplace link
|
||||
const activeTab = ref<'apps' | 'services'>(
|
||||
route.query.tab === 'services' ? 'services' : 'apps'
|
||||
)
|
||||
|
||||
|
||||
// Service container name patterns (backend/infra, not user-facing)
|
||||
// Exact container names or prefixes that are backend services (not user-facing)
|
||||
const SERVICE_NAMES = new Set([
|
||||
'archy-mempool-db', 'archy-btcpay-db', 'archy-nbxplorer', 'archy-tor',
|
||||
'immich_postgres', 'immich_redis',
|
||||
'penpot-postgres', 'penpot-valkey', 'penpot-backend', 'penpot-exporter',
|
||||
'indeedhub-postgres', 'indeedhub-redis', 'indeedhub-minio',
|
||||
'indeedhub-relay', 'indeedhub-build_api_1', 'indeedhub-build_ffmpeg-worker_1',
|
||||
'indeedhub-build_postgres_1', 'indeedhub-build_redis_1', 'indeedhub-build_minio_1',
|
||||
'indeedhub-build_minio-init_1', 'indeedhub-build_relay_1',
|
||||
'mysql-mempool',
|
||||
])
|
||||
|
||||
function isServiceContainer(id: string): boolean {
|
||||
if (SERVICE_NAMES.has(id)) return true
|
||||
const lower = id.toLowerCase()
|
||||
return lower.includes('_db') || lower.includes('-db') && !lower.includes('indeedhub')
|
||||
? SERVICE_NAMES.has(id)
|
||||
: false
|
||||
// Catch any indeedhub-build_* compose infrastructure containers
|
||||
if (id.startsWith('indeedhub-build_')) return true
|
||||
// Catch database containers
|
||||
if (id.endsWith('_db') || id.endsWith('-db')) return true
|
||||
return false
|
||||
}
|
||||
|
||||
// Search
|
||||
const searchQuery = ref('')
|
||||
|
||||
// Category filter (same categories as App Store)
|
||||
const selectedCategory = ref('all')
|
||||
|
||||
// Known app → category mappings (matches App Store categorisation)
|
||||
const APP_CATEGORY_MAP: Record<string, string> = {
|
||||
'bitcoin-knots': 'money', 'bitcoin-ui': 'money', 'electrumx': 'money', 'electrs': 'money',
|
||||
'lnd': 'money', 'mempool': 'money', 'mempool-web': 'money', 'btcpay-server': 'commerce',
|
||||
'fedimint': 'money', 'fedimint-gateway': 'money',
|
||||
'indeedhub': 'media', 'jellyfin': 'media', 'photoprism': 'media', 'immich': 'media',
|
||||
'nextcloud': 'data', 'vaultwarden': 'data', 'filebrowser': 'data', 'onlyoffice': 'data',
|
||||
'homeassistant': 'home', 'lorabell': 'home', 'endurain': 'home',
|
||||
'searxng': 'community', 'ollama': 'community', 'grafana': 'data',
|
||||
'nostr-rs-relay': 'nostr', 'nostrudel': 'nostr',
|
||||
'tailscale': 'networking', 'nginx-proxy-manager': 'networking', 'portainer': 'networking',
|
||||
'uptime-kuma': 'networking', 'dwn': 'data',
|
||||
'botfights': 'l484', 'nwnn': 'l484', '484-kitchen': 'l484',
|
||||
'call-the-operator': 'l484', 'syntropy-institute': 'l484', 't-zero': 'l484',
|
||||
}
|
||||
|
||||
function getAppCategory(id: string, pkg: PackageDataEntry): string {
|
||||
// Check hardcoded map first, then manifest category, then fallback
|
||||
if (APP_CATEGORY_MAP[id]) return APP_CATEGORY_MAP[id]
|
||||
const cat = (pkg.manifest as unknown as Record<string, unknown>)?.category as string | undefined
|
||||
return cat || 'other'
|
||||
}
|
||||
|
||||
const ALL_CATEGORIES = computed(() => [
|
||||
{ id: 'all', name: t('marketplace.all') },
|
||||
{ id: 'community', name: t('marketplace.community') },
|
||||
{ id: 'nostr', name: 'Nostr' },
|
||||
{ id: 'commerce', name: t('marketplace.commerce') },
|
||||
{ id: 'money', name: t('marketplace.money') },
|
||||
{ id: 'data', name: t('marketplace.data') },
|
||||
{ id: 'media', name: 'Media' },
|
||||
{ id: 'home', name: t('marketplace.homeCategory') },
|
||||
{ id: 'networking', name: t('marketplace.networking') },
|
||||
{ id: 'l484', name: 'L484' },
|
||||
{ id: 'other', name: t('marketplace.other') },
|
||||
])
|
||||
|
||||
const categoriesWithApps = computed(() => {
|
||||
const entries = Object.entries(packages.value).filter(([id]) => !isServiceContainer(id))
|
||||
return ALL_CATEGORIES.value.filter(cat => {
|
||||
if (cat.id === 'all') return true
|
||||
return entries.some(([id, pkg]) => getAppCategory(id, pkg) === cat.id)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
// Track loading states for each app action
|
||||
const loadingActions = ref<Record<string, boolean>>({})
|
||||
|
||||
@@ -381,10 +426,14 @@ const packages = computed(() => {
|
||||
// Web-only apps first (alphabetically), then all other apps (alphabetically)
|
||||
const sortedPackageEntries = computed(() => {
|
||||
const entries = Object.entries(packages.value)
|
||||
// Filter by active tab
|
||||
const filtered = entries.filter(([id]) => {
|
||||
// Filter by active tab and category
|
||||
const filtered = entries.filter(([id, pkg]) => {
|
||||
const isSvc = isServiceContainer(id)
|
||||
return activeTab.value === 'services' ? isSvc : !isSvc
|
||||
if (activeTab.value === 'services' ? !isSvc : isSvc) return false
|
||||
if (activeTab.value === 'apps' && selectedCategory.value !== 'all') {
|
||||
return getAppCategory(id, pkg) === selectedCategory.value
|
||||
}
|
||||
return true
|
||||
})
|
||||
return filtered.sort(([idA, a], [idB, b]) => {
|
||||
const aWeb = isWebOnlyApp(idA) ? 0 : 1
|
||||
|
||||
Reference in New Issue
Block a user