diff --git a/neode-ui/src/api/websocket.ts b/neode-ui/src/api/websocket.ts index 116d52af..eace2ff9 100644 --- a/neode-ui/src/api/websocket.ts +++ b/neode-ui/src/api/websocket.ts @@ -29,6 +29,7 @@ export class WebSocketClient { private _state: ConnectionState = 'disconnected' private isReconnecting = false private parseErrorCount = 0 + private connectCheckInterval: ReturnType | null = null constructor(url: string = '/ws/db') { this.url = url @@ -81,25 +82,26 @@ export class WebSocketClient { // If connecting, wait for it if (this.ws && this.ws.readyState === WebSocket.CONNECTING) { if (import.meta.env.DEV) console.log('[WebSocket] Already connecting, waiting...') - const checkInterval = setInterval(() => { + this.clearConnectCheck() + this.connectCheckInterval = setInterval(() => { if (this.ws) { if (this.ws.readyState === WebSocket.OPEN) { - clearInterval(checkInterval) + this.clearConnectCheck() resolve() } else if (this.ws.readyState === WebSocket.CLOSED || this.ws.readyState === WebSocket.CLOSING) { - clearInterval(checkInterval) + this.clearConnectCheck() // Connection failed or closing, will be handled by onclose reject(new Error('Connection closed during connect')) } } else { - clearInterval(checkInterval) + this.clearConnectCheck() reject(new Error('WebSocket was cleared')) } }, 100) - + // Timeout after 5 seconds setTimeout(() => { - clearInterval(checkInterval) + this.clearConnectCheck() if (this.ws && this.ws.readyState !== WebSocket.OPEN) { reject(new Error('Connection timeout')) } @@ -285,6 +287,13 @@ export class WebSocketClient { this.connectionStateCallbacks.forEach((callback) => callback(state)) } + private clearConnectCheck(): void { + if (this.connectCheckInterval) { + clearInterval(this.connectCheckInterval) + this.connectCheckInterval = null + } + } + private startHeartbeat(): void { this.stopHeartbeat() @@ -334,7 +343,8 @@ export class WebSocketClient { this.reconnectAttempts = 0 this.setConnectionState('disconnecting') this.stopHeartbeat() - + this.clearConnectCheck() + // Clear reconnect timer if (this.reconnectTimer) { clearTimeout(this.reconnectTimer) diff --git a/neode-ui/src/style.css b/neode-ui/src/style.css index 6b52bd19..65cfbae3 100644 --- a/neode-ui/src/style.css +++ b/neode-ui/src/style.css @@ -2161,3 +2161,11 @@ html:has(body.video-background-active)::before { white-space: nowrap; } +/* Mobile GPU optimization — reduce blur radius for cheaper compositing */ +@media (max-width: 768px) { + .glass-card, .glass-button { + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + } +} + diff --git a/neode-ui/src/views/AppDetails.vue b/neode-ui/src/views/AppDetails.vue index 9bc04f11..5ebff5ef 100644 --- a/neode-ui/src/views/AppDetails.vue +++ b/neode-ui/src/views/AppDetails.vue @@ -22,409 +22,40 @@
- -
- - - - -
- -
- - - - -
-

{{ pkg.manifest.title }}

-

{{ pkg.manifest.description.short }}

-
- - - {{ getStatusLabel(pkg.state, pkg.health) }} - - v{{ pkg.manifest.version }} -
-
- - - -
- - -
- - -
-
-
+
- -
- -
-

{{ t('appDetails.screenshots') }}

-
-
- - - -
-
-

{{ t('appDetails.screenshotPlaceholder') }}

-
+ - -
-
- - - -
-

Bitcoin is syncing

-

- Some features may be unavailable until Bitcoin finishes syncing. - Wallet connections and block data require a fully synced node. -

-
-
-
-
-
-

- {{ bitcoinSyncPercent.toFixed(1) }}% synced — Block {{ bitcoinBlockHeight.toLocaleString() }} -

-
- - -
-

{{ t('appDetails.about', { name: pkg.manifest.title }) }}

-

- {{ pkg.manifest.description.long }} -

-
- - -
-

{{ t('appDetails.features') }}

-
    -
  • - - - - {{ feature }} -
  • -
-
-
- - -
- -
-

{{ t('appDetails.information') }}

-
-
- {{ t('common.version') }} - {{ pkg.manifest.version }} -
-
- {{ t('common.developer') }} - {{ pkg.manifest.author }} -
-
- {{ t('common.status') }} - {{ pkg.state }} -
-
- {{ t('common.license') }} - {{ pkg.manifest.license }} -
-
- {{ t('common.category') }} - App -
-
-
- - -
-

{{ t('appDetails.services') }}

-
-
- -
-

{{ t('appDetails.guardian') }}

-

{{ pkg.state }}

-
-
-
- -
-

{{ t('appDetails.gateway') }}

-

{{ gatewayState }}

-
-
-
-
- - -
-

{{ t('appDetails.access') }}

-
-
- - - -
-

{{ t('appDetails.lan') }}

- - {{ interfaceAddresses['lan-address'] }} - -
-
-
- - - -
-

{{ t('appDetails.tor') }}

- {{ torUrl }} -

{{ t('appDetails.requiresTor') }}

-
-
-
-
- - -
-

{{ t('appDetails.requirements') }}

-
-
- - - -
-

{{ t('appDetails.ram') }}

-

{{ t('appDetails.ramDesc') }}

-
-
-
- - - -
-

{{ t('appDetails.storage') }}

-

{{ t('appDetails.storageDesc') }}

-
-
-
-
- - - -
+
@@ -437,7 +68,7 @@

{{ t('appDetails.notFoundMessage') }}

- +
{ return id }) -// Web-only app detection (no container — external websites) -const WEB_ONLY_APP_URLS: Record = { - 'indeedhub': `${window.location.protocol}//${window.location.hostname}:7777`, - 'botfights': 'https://botfights.net', - 'nwnn': 'https://nwnn.l484.com', - '484-kitchen': 'https://484.kitchen', - 'call-the-operator': 'https://cta.tx1138.com', - 'arch-presentation': 'https://present.l484.com', - 'syntropy-institute': 'https://syntropy.institute', - 't-zero': 'https://teeminuszero.net', -} - const isWebOnly = computed(() => appId.value in WEB_ONLY_APP_URLS) -/** Map route/marketplace app IDs to backend package keys (container names). */ -const ROUTE_TO_PACKAGE_KEY: Record = { - mempool: 'mempool-web', - 'mempool-electrs': 'mempool-electrs', - electrs: 'mempool-electrs', - btcpay: 'btcpay-server', - 'btcpay-server': 'btcpay-server', - fedimint: 'fedimint', - 'fedimint-gateway': 'fedimint-gateway', - lnd: 'lnd', - 'lnd-ui': 'lnd', - bitcoin: 'bitcoin-knots', - 'bitcoin-knots': 'bitcoin-knots', - homeassistant: 'homeassistant', - 'home-assistant': 'homeassistant', - grafana: 'grafana', - searxng: 'searxng', - ollama: 'ollama', - onlyoffice: 'onlyoffice', - penpot: 'penpot', - nextcloud: 'nextcloud', - vaultwarden: 'vaultwarden', - jellyfin: 'jellyfin', - photoprism: 'photoprism', - immich: 'immich', - filebrowser: 'filebrowser', - 'nginx-proxy-manager': 'nginx-proxy-manager', - portainer: 'portainer', - 'uptime-kuma': 'uptime-kuma', - tailscale: 'tailscale', -} - -/** Backend may register under variant container names */ -const PACKAGE_ALIASES: Record = { - immich: ['immich_server', 'immich-server'], - nextcloud: ['nextcloud-aio', 'nextcloud-server'], -} - -function resolvePackageKey(routeId: string): string { - return ROUTE_TO_PACKAGE_KEY[routeId] ?? routeId -} - -// Check both store.packages and dummyApps; resolve route ID to package key for backend data const pkg = computed(() => { const routeId = appId.value - const packageKey = resolvePackageKey(routeId) - // First check real packages (try both route id and resolved key) - if (store.packages[packageKey]) { - return store.packages[packageKey] - } - if (store.packages[routeId]) { - return store.packages[routeId] - } - // Check known aliases (backend may use variant container names) + const pkgKey = resolvePackageKey(routeId) + if (store.packages[pkgKey]) return store.packages[pkgKey] + if (store.packages[routeId]) return store.packages[routeId] const aliases = PACKAGE_ALIASES[routeId] if (aliases) { for (const alias of aliases) { - if (store.packages[alias]) { - return store.packages[alias] - } + if (store.packages[alias]) return store.packages[alias] } } - // Fall back to dummy apps - if (dummyApps[routeId]) { - return dummyApps[routeId] - } + if (dummyApps[routeId]) return dummyApps[routeId] return null }) @@ -611,38 +186,28 @@ const interfaceAddresses = computed(() => { return main }) -/** V3 onion addresses are 56+ chars + .onion. Placeholders like "btcpay.onion" are not real. */ -function isRealOnionAddress(addr: string | undefined): boolean { - return !!(addr && addr.endsWith('.onion') && addr.length >= 60 && addr.length <= 70) -} - const lanUrl = computed(() => { const addr = interfaceAddresses.value?.['lan-address'] if (!addr) return '#' - if (addr.includes('localhost')) { - return addr.replace('localhost', window.location.hostname) - } + if (addr.includes('localhost')) return addr.replace('localhost', window.location.hostname) return addr }) -/** Tor URL with http:// prefix for copy-paste into Tor Browser */ const torUrl = computed(() => { const addr = interfaceAddresses.value?.['tor-address'] if (!addr || !isRealOnionAddress(addr)) return '' return addr.startsWith('http') ? addr : `http://${addr}` }) -/** Resolved package key for the current route */ +const showTorAddress = computed(() => isRealOnionAddress(interfaceAddresses.value?.['tor-address'])) + const packageKey = computed(() => resolvePackageKey(appId.value)) -/** Fedimint Gateway companion container state */ const gatewayState = computed(() => { const gw = store.packages['fedimint-gateway'] return gw ? gw.state : 'not installed' }) -/** Apps that depend on Bitcoin being synced */ -const BITCOIN_DEPENDENT_APPS = ['lnd', 'electrumx', 'electrs', 'mempool-electrs', 'btcpay-server', 'btcpayserver'] const needsBitcoinSync = computed(() => BITCOIN_DEPENDENT_APPS.includes(packageKey.value)) const bitcoinSyncPercent = ref(0) const bitcoinBlockHeight = ref(0) @@ -667,7 +232,6 @@ onMounted(() => { loadBitcoinSync() }) -// Action error toast const actionError = ref('') let errorTimer: ReturnType | undefined @@ -677,16 +241,15 @@ function showActionError(msg: string) { errorTimer = setTimeout(() => { actionError.value = '' }, 5000) } -const uninstallModal = ref({ - show: false, - appTitle: '' -}) +const uninstallModal = ref({ show: false, appTitle: '' }) const uninstallModalRef = ref(null) const uninstallRestoreFocusRef = ref(null) + function closeUninstallModal() { uninstallRestoreFocusRef.value?.focus?.() uninstallModal.value.show = false } + useModalKeyboard( uninstallModalRef, computed(() => uninstallModal.value.show), @@ -694,211 +257,53 @@ useModalKeyboard( { restoreFocusRef: uninstallRestoreFocusRef } ) -// Determine back button text based on where user came from const backButtonText = computed(() => { - if (route.query.from === 'discover') { - return 'Back to Discover' - } - if (route.query.from === 'marketplace') { - return t('appDetails.backToStore') - } + if (route.query.from === 'discover') return 'Back to Discover' + if (route.query.from === 'marketplace') return t('appDetails.backToStore') return t('appDetails.backToApps') }) -// Check if app has a UI interface and is running const canLaunch = computed(() => { if (!pkg.value) return false - // Web-only apps are always launchable if (isWebOnly.value) return true - // For real apps, check for UI interface const hasUI = pkg.value.manifest.interfaces?.main?.ui || pkg.value.installed?.['interface-addresses']?.main const isRunning = pkg.value.state === 'running' return hasUI && isRunning }) -// Placeholder features - could be extracted from manifest later -const features = computed(() => { - return [ - 'Self-hosted and privacy-focused', - 'Easy installation and updates', - 'Automatic backups', - 'Secure by default' - ] -}) - -function handleImageError(e: Event) { - const target = e.target as HTMLImageElement - const currentSrc = target.src - const id = appId.value - - // If it's a dummy app, try to get icon from GitHub or use placeholder - if (dummyApps[id]) { - // Try alternative icon paths - const iconPaths = [ - `https://raw.githubusercontent.com/Start9Labs/${id}-startos/main/icon.png`, - `https://raw.githubusercontent.com/Start9Labs/${id}-startos/main/icon.svg`, - `/assets/img/app-icons/${id}.png`, - `/assets/img/app-icons/${id}.svg`, - ] - - // Try next path if available - const currentIndex = iconPaths.findIndex(path => currentSrc.includes(path)) - if (currentIndex < iconPaths.length - 1) { - const nextPath = iconPaths[currentIndex + 1] - if (nextPath !== undefined) { - target.src = nextPath - return - } - } - } - - // Create a simple placeholder SVG - const placeholderSvg = `data:image/svg+xml,${encodeURIComponent(` - - - - - - `)}` - - // Only set fallback if we haven't already tried it - if (!currentSrc.includes('data:image')) { - target.src = placeholderSvg - } else { - // Ultimate fallback - target.src = '/assets/img/logo-archipelago.svg' - } -} +const features = computed(() => [ + 'Self-hosted and privacy-focused', + 'Easy installation and updates', + 'Automatic backups', + 'Secure by default' +]) function goBack() { router.back() } - function launchApp() { if (!pkg.value) return - const isDev = import.meta.env.DEV const id = appId.value - // Web-only apps — use their external URL directly const webOnlyUrl = WEB_ONLY_APP_URLS[id] if (webOnlyUrl) { useAppLauncherStore().open({ url: webOnlyUrl, title: pkg.value.manifest.title }) return } - // Special handling for apps with Docker containers - const appUrls: Record = { - 'lorabell': { - dev: 'http://192.168.1.166', - prod: 'http://192.168.1.166' - }, - 'atob': { - dev: 'http://localhost:8102', - prod: 'https://app.atobitcoin.io' - }, - 'k484': { - dev: 'http://localhost:8103', - prod: 'http://localhost:8103' // Self-hosted splash screen - }, - 'indeedhub': { - dev: 'https://archipelago.indeehub.studio', - prod: 'https://archipelago.indeehub.studio' - }, - // Dummy apps - replace with real URLs when packaged - 'bitcoin': { - dev: 'http://localhost:8332', - prod: 'http://localhost:8332' - }, - 'btcpay-server': { - dev: 'http://localhost:23000', - prod: 'http://localhost:23000' - }, - 'homeassistant': { - dev: 'http://localhost:8123', - prod: 'http://localhost:8123' - }, - 'grafana': { - dev: 'http://localhost:3000', - prod: 'http://localhost:3000' - }, - 'endurain': { - dev: 'http://localhost:8080', - prod: 'http://localhost:8080' - }, - 'fedimint': { - dev: 'http://localhost:8175', - prod: 'http://192.168.1.228:8175' - }, - 'fedimint-gateway': { - dev: 'http://localhost:8176', - prod: 'http://192.168.1.228:8176' - }, - 'morphos-server': { - dev: 'http://localhost:8081', - prod: 'http://localhost:8081' - }, - 'lightning-stack': { - dev: 'http://localhost:9735', - prod: 'http://localhost:9735' - }, - 'mempool': { - dev: 'http://localhost:4080', - prod: 'http://localhost:4080' - }, - 'ollama': { - dev: 'http://localhost:11434', - prod: 'http://localhost:11434' - }, - 'searxng': { - dev: 'http://localhost:8888', - prod: 'http://localhost:8888' - }, - 'onlyoffice': { - dev: 'http://localhost:9980', - prod: 'http://localhost:9980' - }, - 'penpot': { - dev: 'http://localhost:9001', - prod: 'http://localhost:9001' - }, - 'nextcloud': { dev: 'http://localhost:8085', prod: 'http://localhost:8085' }, - 'vaultwarden': { dev: 'http://localhost:8082', prod: 'http://localhost:8082' }, - 'jellyfin': { dev: 'http://localhost:8096', prod: 'http://localhost:8096' }, - 'photoprism': { dev: 'http://localhost:2342', prod: 'http://localhost:2342' }, - 'immich': { dev: 'http://localhost:2283', prod: 'http://localhost:2283' }, - 'filebrowser': { dev: 'http://localhost:8083', prod: 'http://localhost:8083' }, - 'nginx-proxy-manager': { dev: 'http://localhost:81', prod: 'http://localhost:81' }, - 'portainer': { dev: 'http://localhost:9000', prod: 'http://localhost:9000' }, - 'uptime-kuma': { dev: 'http://localhost:3001', prod: 'http://localhost:3001' }, - 'tailscale': { dev: 'http://localhost:8240', prod: 'http://localhost:8240' }, - 'lnd': { dev: 'http://localhost:8081', prod: 'http://localhost:8081' }, - 'bitcoin-knots': { dev: 'http://localhost:8334', prod: 'http://localhost:8334' }, - 'botfights': { dev: 'https://botfights.net', prod: 'https://botfights.net' }, - 'nwnn': { dev: 'https://nwnn.l484.com', prod: 'https://nwnn.l484.com' }, - '484-kitchen': { dev: 'https://484.kitchen', prod: 'https://484.kitchen' }, - 'call-the-operator': { dev: 'https://cta.tx1138.com', prod: 'https://cta.tx1138.com' }, - 'arch-presentation': { dev: 'https://present.l484.com', prod: 'https://present.l484.com' }, - 'syntropy-institute': { dev: 'https://syntropy.institute', prod: 'https://syntropy.institute' }, - 't-zero': { dev: 'https://teeminuszero.net', prod: 'https://teeminuszero.net' } - } - - if (appUrls[id]) { - let url = isDev ? appUrls[id].dev : appUrls[id].prod - // Replace localhost with current hostname for remote access + if (APP_URLS[id]) { + let url = isDev ? APP_URLS[id].dev : APP_URLS[id].prod if (url.includes('localhost')) { url = url.replace('localhost', window.location.hostname) } useAppLauncherStore().open({ url, title: pkg.value.manifest.title }) return } - - // For other apps, construct the launch URL - // In a real deployment, this would use the Tor or LAN address from interfaces + const torAddress = pkg.value.manifest.interfaces?.main?.['tor-config'] const lanConfig = pkg.value.manifest.interfaces?.main?.['lan-config'] - if (torAddress || lanConfig) { showActionError(t('appDetails.noLaunchUrl')) } @@ -930,15 +335,11 @@ async function restartApp() { function showUninstallModal() { if (!pkg.value) return - uninstallModal.value = { - show: true, - appTitle: pkg.value.manifest.title - } + uninstallModal.value = { show: true, appTitle: pkg.value.manifest.title } } async function confirmUninstall() { uninstallModal.value.show = false - try { await store.uninstallPackage(appId.value) router.push('/dashboard/apps').catch(() => {}) @@ -947,60 +348,9 @@ async function confirmUninstall() { } } -// Keep for backwards compatibility but redirect to modal -async function uninstallApp() { +function uninstallApp() { showUninstallModal() } - -function getStatusClass(state: PackageState, health?: string | null): string { - if (state === PackageState.Running && health === 'starting') return 'bg-yellow-500/20 text-yellow-200 border border-yellow-500/30' - if (state === PackageState.Running && health === 'unhealthy') return 'bg-orange-500/20 text-orange-200 border border-orange-500/30' - switch (state) { - case PackageState.Running: - return 'bg-green-500/20 text-green-200 border border-green-500/30' - case PackageState.Stopped: - return 'bg-gray-500/20 text-gray-200 border border-gray-500/30' - case PackageState.Exited: - return 'bg-red-500/20 text-red-200 border border-red-500/30' - case PackageState.Starting: - case PackageState.Stopping: - case PackageState.Restarting: - return 'bg-yellow-500/20 text-yellow-200 border border-yellow-500/30' - case PackageState.Installing: - return 'bg-blue-500/20 text-blue-200 border border-blue-500/30' - default: - return 'bg-gray-500/20 text-gray-200 border border-gray-500/30' - } -} - -function getStatusDotClass(state: PackageState, health?: string | null): string { - if (state === PackageState.Running && health === 'starting') return 'bg-yellow-400 animate-pulse' - if (state === PackageState.Running && health === 'unhealthy') return 'bg-orange-400 animate-pulse' - switch (state) { - case PackageState.Running: - return 'bg-green-400' - case PackageState.Stopped: - return 'bg-gray-400' - case PackageState.Exited: - return 'bg-red-400 animate-pulse' - case PackageState.Starting: - case PackageState.Stopping: - case PackageState.Restarting: - return 'bg-yellow-400 animate-pulse' - case PackageState.Installing: - return 'bg-blue-400 animate-pulse' - default: - return 'bg-gray-400' - } -} - -function getStatusLabel(state: PackageState, health?: string | null): string { - if (state === PackageState.Running && health === 'starting') return 'starting up' - if (state === PackageState.Running && health === 'unhealthy') return 'unhealthy' - if (state === PackageState.Running && health === 'healthy') return 'healthy' - if (state === PackageState.Exited) return 'crashed' - return state -} diff --git a/neode-ui/src/views/Marketplace.vue b/neode-ui/src/views/Marketplace.vue index 94918722..596396f2 100644 --- a/neode-ui/src/views/Marketplace.vue +++ b/neode-ui/src/views/Marketplace.vue @@ -56,143 +56,24 @@
-
-
+ -
- -
- - - -
-
-

- {{ app.title }} - {{ getAppTier(app.id) }} -

-

{{ app.version ? `v${app.version}` : 'latest' }}

-

by {{ app.author }}

-
-
- - -
- {{ app.trustTier }} - Score: {{ app.trustScore }}/100 - · {{ app.relayCount }} relay{{ app.relayCount !== 1 ? 's' : '' }} -
- -

- {{ typeof app.description === 'object' ? app.description.short : (app.description || 'No description available') }} -

- -
- - - - - - - {{ getInstalledState(app.id) === 'installing' ? 'Installing...' : 'Starting...' }} - - - - {{ t('marketplace.alreadyInstalled') }} - - - - - - - - - - - Checking... - - - -
-
- - - - - {{ installingApps.get(app.id)?.message || t('common.installing') }} - {{ installingApps.get(app.id)?.progress || 0 }}% -
-
-
-
-
- - -
-
+ :key="app.id" + :app="app" + :index="index" + :stagger="showStagger" + :installed="isInstalled(app.id)" + :installing="installingApps.has(app.id)" + :install-progress="installingApps.get(app.id)" + :installed-state="getInstalledState(app.id)" + :starting-up="isStartingUp(app.id)" + :containers-scanned="containersScanned" + :tier-label="getAppTier(app.id)" + @view="viewAppDetails" + @install="app.source === 'local' ? installApp(app) : installCommunityApp(app)" + @launch="launchInstalledApp" + />
@@ -211,103 +92,14 @@

{{ searchQuery && selectedCategory !== 'all' ? t('marketplace.noResults', { category: categories.find(c => c.id === selectedCategory)?.name, query: searchQuery }) : searchQuery ? t('marketplace.noResultsSearch', { query: searchQuery }) : t('marketplace.noResultsCategory', { category: categories.find(c => c.id === selectedCategory)?.name }) }}

- + - - - - - - - -
-
- -
-

{{ t('marketplace.filterByCategory') }}

- -
- - -
- -
-
-
-
+ @@ -321,11 +113,18 @@ import { useRouter, useRoute, RouterLink } from 'vue-router' import { useI18n } from 'vue-i18n' import { useAppStore } from '@/stores/app' import { rpcClient } from '@/api/rpc-client' -import { useMarketplaceApp, type MarketplaceAppInfo } from '@/composables/useMarketplaceApp' +import { useMarketplaceApp } from '@/composables/useMarketplaceApp' import { useAppLauncherStore } from '@/stores/appLauncher' -import { useModalKeyboard } from '@/composables/useModalKeyboard' - -type MarketplaceApp = Partial & { id: string; trustScore?: number; trustTier?: string; relayCount?: number } +import MarketplaceAppCard from './marketplace/MarketplaceAppCard.vue' +import MarketplaceFilterModal from './marketplace/MarketplaceFilterModal.vue' +import { + type MarketplaceApp, + type InstallProgress, + INSTALLED_ALIASES, + getAppTier, + categorizeCommunityApp, + getCuratedAppList, +} from './marketplace/marketplaceData' const router = useRouter() const route = useRoute() @@ -354,26 +153,15 @@ const categories = computed(() => [ ]) // Installation state - support multiple concurrent installations -interface InstallProgress { - id: string - title: string - status: 'downloading' | 'installing' | 'starting' | 'complete' | 'error' - progress: number // 0-100 - message: string - attempt: number -} - const installingApps = ref>(new Map()) const maxAttempts = ref(60) // Watch WebSocket data for real install progress from backend -// Also picks up installs started before this page was opened (Task 11: persist across nav) watch(() => store.packages, (packages) => { if (!packages) return for (const [appId, pkg] of Object.entries(packages)) { if ((pkg.state as string) === 'installing') { const progress = pkg['install-progress'] - // If we don't have a local entry yet, create one from backend state if (!installingApps.value.has(appId)) { installingApps.value.set(appId, { id: appId, @@ -397,22 +185,11 @@ watch(() => store.packages, (packages) => { }) } } else if (installingApps.value.has(appId) && (pkg.state as string) !== 'installing') { - // Install finished — remove from tracking installingApps.value.delete(appId) } } }, { deep: true }) -// Filter modal state (for mobile) -const showFilterModal = ref(false) -const filterModalRef = ref(null) -const filterRestoreFocusRef = ref(null) -function closeFilterModal() { - filterRestoreFocusRef.value?.focus?.() - showFilterModal.value = false -} -useModalKeyboard(filterModalRef, showFilterModal, closeFilterModal, { restoreFocusRef: filterRestoreFocusRef }) - // Select category and trigger Nostr relay discovery when 'nostr' is chosen function selectCategory(id: string) { selectedCategory.value = id @@ -428,7 +205,7 @@ const communityApps = ref([]) const searchQuery = ref('') // Nostr community marketplace state -const nostrApps = ref<(MarketplaceApp & { trustScore?: number; trustTier?: string; relayCount?: number })[]>([]) +const nostrApps = ref([]) const nostrLoading = ref(false) const nostrError = ref('') @@ -463,34 +240,6 @@ async function loadNostrMarketplace() { } } -// Available apps in marketplace -// const availableApps = ref([ - // { - // id: 'atob', - // title: 'A to B Bitcoin', - // version: '0.1.0', - // icon: '/assets/img/atob.png', - // category: 'community', - // description: { - // short: 'Bitcoin tools and services for seamless transactions', - // long: 'A to B Bitcoin provides tools and services for Bitcoin transactions. Access the A to B platform through your Archipelago server with full privacy and control.' - // }, - // s9pkUrl: '/packages/atob.s9pk' - // }, - // { - // id: 'k484', - // title: 'K484', - // version: '0.1.0', - // icon: '/assets/img/k484.png', - // category: 'commerce', - // description: { - // short: 'Point of Sale and Admin system for Archipelago', - // long: 'K484 provides a complete POS and administration system for your Archipelago server. Choose between POS mode for transactions or Admin mode for management.' - // }, - // s9pkUrl: '/packages/k484.s9pk' - // }, -// ]) - const installedPackages = computed(() => { return store.data?.['package-data'] || {} }) @@ -499,91 +248,17 @@ const containersScanned = computed(() => { return store.data?.['server-info']?.['status-info']?.['containers-scanned'] ?? false }) -// Function to categorize community apps based on their ID and description -function categorizeCommunityApp(app: MarketplaceApp): string { - // If app already has a category set, use it - if (app.category) return app.category - - const id = app.id.toLowerCase() - const title = app.title?.toLowerCase() || '' - const description = (typeof app.description === 'string' ? app.description : app.description?.short ?? '').toLowerCase() - const combined = `${id} ${title} ${description}` - - // Money category - if (id.includes('bitcoin') || id.includes('btc') || id.includes('lightning') || - id.includes('lnd') || id.includes('cln') || id.includes('electr') || - id.includes('fedimint') || id.includes('cashu') || title.includes('lightning') || - combined.includes('wallet') || combined.includes('satoshi')) { - return 'money' - } - - // Commerce category - if (id.includes('btcpay') || id.includes('commerce') || id.includes('shop') || - id.includes('store') || id.includes('pos') || id.includes('payment') || - combined.includes('merchant') || combined.includes('invoice')) { - return 'commerce' - } - - // Data category - if (id.includes('cloud') || id.includes('nextcloud') || id.includes('sync') || - id.includes('storage') || id.includes('backup') || id.includes('file') || - id.includes('photo') || id.includes('immich') || id.includes('jellyfin') || - id.includes('plex') || id.includes('media') || id.includes('vault') || - combined.includes('password manager') || combined.includes('file storage')) { - return 'data' - } - - // Home category - if (id.includes('home-assistant') || id.includes('homeassistant') || - id.includes('smart-home') || id.includes('automation') || id.includes('iot') || - combined.includes('home automation') || combined.includes('smart home')) { - return 'home' - } - - // Nostr category - if (id.includes('nostr') || (id.includes('relay') && combined.includes('nostr')) || - combined.includes('nostr relay') || combined.includes('nostr client')) { - return 'nostr' - } - - // Networking category - if (id.includes('vpn') || id.includes('wireguard') || id.includes('tailscale') || - id.includes('proxy') || id.includes('dns') || id.includes('pihole') || - id.includes('adguard') || id.includes('nginx') || id.includes('tor') || - combined.includes('network') || combined.includes('firewall')) { - return 'networking' - } - - // Community category - if (id.includes('matrix') || id.includes('synapse') || id.includes('element') || - id.includes('mastodon') || id.includes('lemmy') || - id.includes('messenger') || id.includes('chat') || id.includes('social') || - id.includes('cups') || combined.includes('communication') || - combined.includes('messaging')) { - return 'community' - } - - // Default to other - return 'other' -} - -// Combine curated apps with Nostr relay-discovered apps (merged into Nostr category) +// Combine curated apps with Nostr relay-discovered apps const allApps = computed(() => { - // Always start with curated Docker apps const local: (MarketplaceApp & { category: string; source: string })[] = [] const community = communityApps.value.map(app => { const category = categorizeCommunityApp(app) - return { - ...app, - category, - source: 'community' - } + return { ...app, category, source: 'community' } }) const base = [...local, ...community] - // Merge Nostr relay-discovered apps (deduplicated by ID) if (nostrApps.value.length > 0) { const existingIds = new Set(base.map(a => a.id)) const nostrMerged = nostrApps.value @@ -598,7 +273,6 @@ const allApps = computed(() => { return base }) -// Only show categories that have at least one app const categoriesWithApps = computed(() => { const apps = allApps.value return categories.value.filter(cat => { @@ -607,19 +281,16 @@ const categoriesWithApps = computed(() => { }) }) -// Filtered apps by category and search const filteredApps = computed(() => { let apps = allApps.value - - // Filter by category + if (selectedCategory.value && selectedCategory.value !== 'all') { apps = apps.filter(app => app.category === selectedCategory.value) } - - // Filter by search query + if (searchQuery.value) { const query = searchQuery.value.toLowerCase() - apps = apps.filter(app => + apps = apps.filter(app => app.title?.toLowerCase().includes(query) || (typeof app.description === 'string' && app.description.toLowerCase().includes(query)) || (typeof app.description === 'object' && app.description?.short?.toLowerCase().includes(query)) || @@ -627,8 +298,7 @@ const filteredApps = computed(() => { app.author?.toLowerCase().includes(query) ) } - - // Sort: available apps first, installed apps at the bottom + apps.sort((a, b) => { const aInstalled = isInstalled(a.id) ? 1 : 0 const bInstalled = isInstalled(b.id) ? 1 : 0 @@ -638,34 +308,12 @@ const filteredApps = computed(() => { return apps }) - -/** Marketplace app ID -> backend package keys (for "Already Installed" when first-boot/deploy created them) */ -const INSTALLED_ALIASES: Record = { - mempool: ['mempool-web', 'mempool-api', 'archy-mempool-web', 'archy-mempool-db'], - bitcoin: ['bitcoin-knots'], - btcpay: ['btcpay-server', 'archy-btcpay-db', 'archy-nbxplorer'], - immich: ['immich-server', 'immich-app', 'immich_server', 'immich_postgres', 'immich_redis'], - nextcloud: ['nextcloud-aio', 'nextcloud-server'], - fedimint: ['fedimint-gateway'], - electrumx: ['electrumx', 'archy-electrs-ui'], - grafana: ['grafana'], - jellyfin: ['jellyfin'], - vaultwarden: ['vaultwarden'], - searxng: ['searxng'], - homeassistant: ['homeassistant'], - photoprism: ['photoprism'], - lnd: ['lnd', 'archy-lnd-ui'], - filebrowser: ['filebrowser'], - tailscale: ['tailscale'], - ollama: ['ollama'], -} function isInstalled(appId: string): boolean { if (appId in installedPackages.value) return true const aliases = INSTALLED_ALIASES[appId] return aliases ? aliases.some((a) => a in installedPackages.value) : false } -/** Get the package state for an installed app (checks aliases too). */ function getInstalledState(appId: string): string | null { const pkg = installedPackages.value[appId] if (pkg) return pkg.state @@ -679,7 +327,6 @@ function getInstalledState(appId: string): string | null { return null } -/** True if installed and currently in a transitional state (not yet ready). */ function isStartingUp(appId: string): boolean { const state = getInstalledState(appId) return state !== null && state !== 'running' && state !== 'stopped' && state !== 'exited' @@ -689,7 +336,6 @@ function launchInstalledApp(app: MarketplaceApp) { appLauncher.openSession(app.id) } -// Load community marketplace on mount onMounted(() => { marketplaceAnimationDone = true if (communityApps.value.length === 0 && !loadingCommunity.value) { @@ -697,433 +343,25 @@ onMounted(() => { } }) -// Load community marketplace from Start9 registry async function loadCommunityMarketplace() { loadingCommunity.value = true communityError.value = '' - - // Use curated list of Docker-based apps - // These are standard Docker images, not StartOS packages if (import.meta.env.DEV) console.log('Loading Docker-based app marketplace') communityApps.value = getCuratedAppList() loadingCommunity.value = false } -// Get app tier classification (matches backend get_app_tier) -function getAppTier(appId: string): string { - const core = ['bitcoin-knots', 'bitcoin', 'lnd', 'mempool', 'btcpay-server', 'dwn', 'filebrowser'] - const recommended = ['fedimint', 'thunderhub', 'vaultwarden', 'uptime-kuma', 'grafana', 'searxng', 'tailscale', 'portainer'] - if (core.includes(appId)) return 'core' - if (recommended.includes(appId)) return 'recommended' - return 'optional' -} - -// Curated list of apps with Docker Hub images -function getCuratedAppList() { - return [ - { - id: 'bitcoin-knots', - title: 'Bitcoin Knots', - version: '28.1.0', - description: 'Run a full Bitcoin node. Validate and relay blocks and transactions on the Bitcoin network.', - icon: '/assets/img/app-icons/bitcoin-knots.webp', - author: 'Bitcoin Knots', - dockerImage: 'docker.io/bitcoinknots/bitcoin:v28.1', - manifestUrl: undefined, - repoUrl: 'https://github.com/bitcoinknots/bitcoin' - }, - { - id: 'btcpay-server', - title: 'BTCPay Server', - version: '1.13.5', - description: 'Self-hosted Bitcoin payment processor. Accept Bitcoin payments without intermediaries or fees.', - icon: '/assets/img/app-icons/btcpay-server.png', - author: 'BTCPay Server Foundation', - dockerImage: 'docker.io/btcpayserver/btcpayserver:1.13.5', - manifestUrl: undefined, - repoUrl: 'https://github.com/btcpayserver/btcpayserver' - }, - { - id: 'lnd', - title: 'LND', - version: '0.17.4', - description: 'Lightning Network Daemon. Fast and cheap Bitcoin payments through the Lightning Network.', - icon: '/assets/img/app-icons/lnd.svg', - author: 'Lightning Labs', - dockerImage: 'docker.io/lightninglabs/lnd:v0.17.4-beta', - manifestUrl: undefined, - repoUrl: 'https://github.com/lightningnetwork/lnd' - }, - { - id: 'thunderhub', - title: 'ThunderHub', - version: '0.13.31', - description: 'Lightning node management UI. Manage channels, send and receive payments, view routing fees, and monitor your Lightning node.', - icon: '/assets/img/app-icons/thunderhub.svg', - author: 'Anthony Potdevin', - dockerImage: 'docker.io/apotdevin/thunderhub:v0.13.31', - manifestUrl: undefined, - repoUrl: 'https://github.com/apotdevin/thunderhub' - }, - { - id: 'mempool', - title: 'Mempool Explorer', - version: '2.5.0', - description: 'Self-hosted Bitcoin blockchain and mempool visualizer with beautiful explorer interface.', - icon: '/assets/img/app-icons/mempool.webp', - author: 'Mempool', - dockerImage: 'docker.io/mempool/frontend:v2.5.0', - manifestUrl: undefined, - repoUrl: 'https://github.com/mempool/mempool' - }, - { - id: 'homeassistant', - title: 'Home Assistant', - version: '2024.1', - description: 'Open-source home automation platform. Control and automate your smart home devices privately.', - icon: '/assets/img/app-icons/homeassistant.png', - author: 'Home Assistant', - dockerImage: 'docker.io/homeassistant/home-assistant:2024.1', - manifestUrl: undefined, - repoUrl: 'https://github.com/home-assistant/core' - }, - { - id: 'grafana', - title: 'Grafana', - version: '10.2.0', - description: 'Analytics and monitoring platform. Create dashboards and visualize data from multiple sources.', - icon: '/assets/img/app-icons/grafana.png', - author: 'Grafana Labs', - dockerImage: 'docker.io/grafana/grafana:10.2.0', - manifestUrl: undefined, - repoUrl: 'https://github.com/grafana/grafana' - }, - { - id: 'searxng', - title: 'SearXNG', - version: '2024.1.0', - description: 'Privacy-respecting metasearch engine. Search without tracking or ads.', - icon: '/assets/img/app-icons/searxng.png', - author: 'SearXNG', - dockerImage: 'docker.io/searxng/searxng:2024.11.17-e2554de75', - manifestUrl: undefined, - repoUrl: 'https://github.com/searxng/searxng' - }, - { - id: 'ollama', - title: 'Ollama', - version: '0.1.0', - description: 'Run large language models locally. Download and run AI models like Llama, Mistral on your own hardware.', - icon: '/assets/img/app-icons/ollama.png', - author: 'Ollama', - dockerImage: 'docker.io/ollama/ollama:0.5.4', - manifestUrl: undefined, - repoUrl: 'https://github.com/ollama/ollama' - }, - { - id: 'onlyoffice', - title: 'OnlyOffice', - version: '7.5.1', - description: 'Office suite for document collaboration. Edit docs, spreadsheets, and presentations.', - icon: '/assets/img/app-icons/onlyoffice.webp', - author: 'Ascensio System SIA', - dockerImage: 'docker.io/onlyoffice/documentserver:7.5.1', - manifestUrl: undefined, - repoUrl: 'https://github.com/ONLYOFFICE/DocumentServer' - }, - { - id: 'penpot', - title: 'Penpot', - version: '2.4', - description: 'Open-source design and prototyping platform. Self-hosted alternative to Figma.', - icon: '/assets/img/app-icons/penpot.webp', - author: 'Penpot', - dockerImage: 'docker.io/penpotapp/frontend:2.4', - manifestUrl: undefined, - repoUrl: 'https://github.com/penpot/penpot' - }, - { - id: 'nextcloud', - title: 'Nextcloud', - version: '28.0', - description: 'Self-hosted cloud storage and collaboration platform. Your own private cloud.', - icon: '/assets/img/app-icons/nextcloud.webp', - author: 'Nextcloud', - dockerImage: 'docker.io/library/nextcloud:28', - manifestUrl: undefined, - repoUrl: 'https://github.com/nextcloud/server' - }, - { - id: 'vaultwarden', - title: 'Vaultwarden', - version: '1.30.0', - description: 'Self-hosted password manager (Bitwarden-compatible). Secure vault for passwords and secrets.', - icon: '/assets/img/app-icons/vaultwarden.webp', - author: 'Vaultwarden', - dockerImage: 'docker.io/vaultwarden/server:1.30.0-alpine', - manifestUrl: undefined, - repoUrl: 'https://github.com/dani-garcia/vaultwarden' - }, - { - id: 'jellyfin', - title: 'Jellyfin', - version: '10.8.0', - description: 'Free media server system. Stream your movies, music, and photos to any device.', - icon: '/assets/img/app-icons/jellyfin.webp', - author: 'Jellyfin', - dockerImage: 'docker.io/jellyfin/jellyfin:10.8.13', - manifestUrl: undefined, - repoUrl: 'https://github.com/jellyfin/jellyfin' - }, - { - id: 'photoprism', - title: 'PhotoPrism', - version: '240915', - description: 'AI-powered photo management. Organize and browse photos with facial recognition.', - icon: '/assets/img/app-icons/photoprism.svg', - author: 'PhotoPrism', - dockerImage: 'docker.io/photoprism/photoprism:240915', - manifestUrl: undefined, - repoUrl: 'https://github.com/photoprism/photoprism' - }, - { - id: 'immich', - title: 'Immich', - version: '1.90.0', - description: 'High-performance self-hosted photo and video backup. Mobile-first with ML features.', - icon: '/assets/img/app-icons/immich.png', - author: 'Immich', - dockerImage: 'ghcr.io/immich-app/immich-server:release', - manifestUrl: undefined, - repoUrl: 'https://github.com/immich-app/immich' - }, - { - id: 'filebrowser', - title: 'File Browser', - version: '2.27.0', - description: 'Web-based file manager. Browse, upload, and manage files through a web interface.', - icon: '/assets/img/app-icons/file-browser.webp', - author: 'File Browser', - dockerImage: 'docker.io/filebrowser/filebrowser:v2.27.0', - manifestUrl: undefined, - repoUrl: 'https://github.com/filebrowser/filebrowser' - }, - { - id: 'nginx-proxy-manager', - title: 'Nginx Proxy Manager', - version: '2.11.0', - description: 'Easy proxy management with SSL. Beautiful web interface for managing reverse proxies.', - icon: '/assets/img/app-icons/nginx.svg', - author: 'Nginx Proxy Manager', - dockerImage: 'docker.io/jc21/nginx-proxy-manager:2.12.1', - manifestUrl: undefined, - repoUrl: 'https://github.com/NginxProxyManager/nginx-proxy-manager' - }, - { - id: 'portainer', - title: 'Portainer', - version: '2.19.0', - description: 'Container management UI. Manage Docker containers through a beautiful web interface.', - icon: '/assets/img/app-icons/portainer.webp', - author: 'Portainer', - dockerImage: 'docker.io/portainer/portainer-ce:2.19.4', - manifestUrl: undefined, - repoUrl: 'https://github.com/portainer/portainer' - }, - { - id: 'uptime-kuma', - title: 'Uptime Kuma', - version: '1.23.0', - description: 'Self-hosted monitoring tool. Monitor uptime for HTTP(s), TCP, DNS, and more.', - icon: '/assets/img/app-icons/uptime-kuma.webp', - author: 'Uptime Kuma', - dockerImage: 'docker.io/louislam/uptime-kuma:1', - manifestUrl: undefined, - repoUrl: 'https://github.com/louislam/uptime-kuma' - }, - { - id: 'tailscale', - title: 'Tailscale', - version: '1.78.0', - description: 'Zero-config VPN for secure remote access. Connect all your devices with WireGuard mesh network.', - icon: '/assets/img/app-icons/tailscale.webp', - author: 'Tailscale', - dockerImage: 'docker.io/tailscale/tailscale:stable', - manifestUrl: undefined, - repoUrl: 'https://github.com/tailscale/tailscale' - }, - { - id: 'fedimint', - title: 'Fedimint', - version: '0.10.0', - description: 'Federated Bitcoin mint with built-in Guardian UI. Private, scalable Bitcoin through federated guardians.', - icon: '/assets/img/app-icons/fedimint.png', - author: 'Fedimint', - dockerImage: 'docker.io/fedimint/fedimintd:v0.10.0', - manifestUrl: undefined, - repoUrl: 'https://github.com/fedimint/fedimint' - }, - { - id: 'indeedhub', - title: 'Indeehub', - version: '0.1.0', - description: 'Bitcoin documentary streaming platform with Nostr identity sign-in. Stream God Bless Bitcoin and other educational content about sovereignty and decentralized technology.', - icon: '/assets/img/app-icons/indeedhub.png', - author: 'Indeehub Team', - dockerImage: 'localhost/indeedhub:latest', - manifestUrl: undefined, - repoUrl: 'https://github.com/indeedhub/indeedhub' - }, - { - id: 'dwn', - title: 'Decentralized Web Node', - version: '0.4.0', - description: 'Store and sync your personal data across devices using decentralized web node protocols. Own your data with DID-based access control.', - icon: '/assets/img/app-icons/dwn.svg', - author: 'TBD', - dockerImage: 'ghcr.io/tbd54566975/dwn-server:main', - manifestUrl: undefined, - repoUrl: 'https://github.com/TBD54566975/dwn-server' - }, - { - id: 'nostrudel', - title: 'noStrudel', - version: '0.40.0', - category: 'nostr', - description: 'A feature-rich Nostr web client with NIP-07 signer support. Browse your feed, post notes, manage relays, and interact with the Nostr network — all signed with your node\'s Nostr identity.', - icon: '/assets/img/app-icons/nostrudel.svg', - author: 'hzrd149', - dockerImage: '', - manifestUrl: undefined, - repoUrl: 'https://github.com/hzrd149/nostrudel', - webUrl: 'https://nostrudel.ninja' - }, - { - id: 'nostr-rs-relay', - title: 'Nostr Relay', - version: '0.9.0', - category: 'nostr', - description: 'Run your own Nostr relay. Store your events locally, relay for friends, and publish over Tor. A sovereign relay for your sovereign node.', - icon: '/assets/img/app-icons/nostr-rs-relay.svg', - author: 'scsiblade', - dockerImage: 'docker.io/scsiblade/nostr-rs-relay:0.9.0', - manifestUrl: undefined, - repoUrl: 'https://sr.ht/~gheartsfield/nostr-rs-relay/' - }, - { - id: 'botfights', - title: 'BotFights', - version: '1.0.0', - description: 'AI bot arena — build, train, and battle autonomous agents. Compete in strategy tournaments with your own coded bots.', - icon: '/assets/img/app-icons/botfights.svg', - author: 'BotFights', - dockerImage: '', - manifestUrl: undefined, - repoUrl: 'https://botfights.net', - webUrl: 'https://botfights.net' - }, - { - id: 'nwnn', - title: 'Next Web News Network', - version: '1.0.0', - category: 'l484', - description: 'Decentralized news and link aggregator, synchronized from Telegram. Community-curated content on Bitcoin, sovereignty, and decentralized tech.', - icon: '/assets/img/app-icons/nwnn.png', - author: 'L484', - dockerImage: '', - manifestUrl: undefined, - repoUrl: 'https://nwnn.l484.com', - webUrl: 'https://nwnn.l484.com' - }, - { - id: '484-kitchen', - title: '484 Kitchen', - version: '1.0.0', - category: 'l484', - description: 'K484 application platform — an internal tool for the L484 network.', - icon: '/assets/img/app-icons/484-kitchen.png', - author: 'L484', - dockerImage: '', - manifestUrl: undefined, - repoUrl: 'https://484.kitchen', - webUrl: 'https://484.kitchen' - }, - { - id: 'call-the-operator', - title: 'Call the Operator', - version: '1.0.0', - category: 'l484', - description: 'Escape the Matrix — a portal for exploring decentralized alternatives and reclaiming digital sovereignty.', - icon: '/assets/img/app-icons/call-the-operator.png', - author: 'TX1138', - dockerImage: '', - manifestUrl: undefined, - repoUrl: 'https://cta.tx1138.com', - webUrl: 'https://cta.tx1138.com' - }, - { - id: 'arch-presentation', - title: 'Arch Presentation', - version: '1.0.0', - category: 'l484', - description: 'Archipelago: The Future of Decentralized Infrastructure — an interactive presentation about the Archipelago project vision.', - icon: '/assets/img/app-icons/arch-presentation.png', - author: 'L484', - dockerImage: '', - manifestUrl: undefined, - repoUrl: 'https://present.l484.com', - webUrl: 'https://present.l484.com' - }, - { - id: 'syntropy-institute', - title: 'Syntropy Institute', - version: '1.0.0', - category: 'l484', - description: 'Medicine Reimagined — Manual Kinetics, Syntropy Frequency analysis-therapy, digital homeopathy, and concierge protocols.', - icon: '/assets/img/app-icons/syntropy-institute.png', - author: 'Syntropy Institute', - dockerImage: '', - manifestUrl: undefined, - repoUrl: 'https://syntropy.institute', - webUrl: 'https://syntropy.institute' - }, - { - id: 't-zero', - title: 'T-0', - version: '1.0.0', - category: 'l484', - description: 'Documentary series exploring decentralization, Bitcoin, and the mavericks building the ungovernable future. Conversations with the builders, powered by Nostr.', - icon: '/assets/img/app-icons/t-zero.png', - author: 'T-0', - dockerImage: '', - manifestUrl: undefined, - repoUrl: 'https://teeminuszero.net', - webUrl: 'https://teeminuszero.net' - } - ] -} - function viewAppDetails(app: MarketplaceApp) { if (import.meta.env.DEV) console.log('[Marketplace] Navigating to app detail:', app) try { - // If app is already installed, go directly to the installed app detail page if (isInstalled(app.id)) { if (import.meta.env.DEV) console.log('[Marketplace] App is installed, navigating to app details page') - router.push({ - name: 'app-details', - params: { id: app.id } - }) + router.push({ name: 'app-details', params: { id: app.id } }) } else { - // Store app data in composable for marketplace detail view setCurrentApp(app) if (import.meta.env.DEV) console.log('[Marketplace] App data stored in composable') - - // Navigate to marketplace detail page - router.push({ - name: 'marketplace-app-detail', - params: { id: app.id } - }) + router.push({ name: 'marketplace-app-detail', params: { id: app.id } }) } } catch (e) { if (import.meta.env.DEV) console.error('[Marketplace] Navigation error:', e) @@ -1196,13 +434,13 @@ async function installApp(app: MarketplaceApp) { try { const installUrl = app.url || app.manifestUrl || app.s9pkUrl - + installingApps.value.set(app.id, { ...installingApps.value.get(app.id)!, status: 'downloading', progress: 30, message: 'Downloading package...' }) - + await rpcClient.call({ method: 'package.install', params: { id: app.id, url: installUrl, version: app.version } }) - + installingApps.value.set(app.id, { ...installingApps.value.get(app.id)!, status: 'installing', progress: 60, message: 'Installing package...' }) - + startInstallPolling(app.id, 'Starting application...') } catch (err) { if (import.meta.env.DEV) console.error('Installation failed:', err) @@ -1217,18 +455,18 @@ async function installCommunityApp(app: MarketplaceApp) { installingApps.value.set(app.id, { id: app.id, title: app.title ?? app.id, status: 'downloading', progress: 10, message: 'Pulling Docker image...', attempt: 0 }) - + try { installingApps.value.set(app.id, { ...installingApps.value.get(app.id)!, status: 'downloading', progress: 20, message: 'Downloading container image...' }) - + await rpcClient.call({ method: 'package.install', params: { id: app.id, dockerImage: app.dockerImage, version: app.version }, timeout: 180000 }) - + installingApps.value.set(app.id, { ...installingApps.value.get(app.id)!, status: 'installing', progress: 60, message: 'Starting container...' }) - + startInstallPolling(app.id, 'Initializing application...') } catch (err) { if (import.meta.env.DEV) console.error('[Marketplace] Installation failed:', err) @@ -1236,35 +474,9 @@ async function installCommunityApp(app: MarketplaceApp) { trackTimeout(() => { installingApps.value.delete(app.id) }, 5000) } } - -function handleImageError(event: Event) { - const img = event.target as HTMLImageElement - img.src = '/assets/img/logo-archipelago.svg' -} diff --git a/neode-ui/src/views/marketplace/MarketplaceFilterModal.vue b/neode-ui/src/views/marketplace/MarketplaceFilterModal.vue new file mode 100644 index 00000000..5cc69e45 --- /dev/null +++ b/neode-ui/src/views/marketplace/MarketplaceFilterModal.vue @@ -0,0 +1,124 @@ + + + diff --git a/neode-ui/src/views/marketplace/marketplaceData.ts b/neode-ui/src/views/marketplace/marketplaceData.ts new file mode 100644 index 00000000..d8841a0d --- /dev/null +++ b/neode-ui/src/views/marketplace/marketplaceData.ts @@ -0,0 +1,507 @@ +/** + * Marketplace data: curated app list, categorization, aliases, and tier logic. + * Extracted from Marketplace.vue to keep the view under 500 lines. + */ + +export interface MarketplaceApp { + id: string + title?: string + version?: string + description?: string | { short?: string; long?: string } + icon?: string + author?: string + dockerImage?: string + manifestUrl?: string + repoUrl?: string + webUrl?: string + category?: string + source?: string + url?: string + s9pkUrl?: string + trustScore?: number + trustTier?: string + relayCount?: number +} + +export interface InstallProgress { + id: string + title: string + status: 'downloading' | 'installing' | 'starting' | 'complete' | 'error' + progress: number + message: string + attempt: number +} + +/** Marketplace app ID -> backend package keys (for "Already Installed" when first-boot/deploy created them) */ +export const INSTALLED_ALIASES: Record = { + mempool: ['mempool-web', 'mempool-api', 'archy-mempool-web', 'archy-mempool-db'], + bitcoin: ['bitcoin-knots'], + btcpay: ['btcpay-server', 'archy-btcpay-db', 'archy-nbxplorer'], + immich: ['immich-server', 'immich-app', 'immich_server', 'immich_postgres', 'immich_redis'], + nextcloud: ['nextcloud-aio', 'nextcloud-server'], + fedimint: ['fedimint-gateway'], + electrumx: ['electrumx', 'archy-electrs-ui'], + grafana: ['grafana'], + jellyfin: ['jellyfin'], + vaultwarden: ['vaultwarden'], + searxng: ['searxng'], + homeassistant: ['homeassistant'], + photoprism: ['photoprism'], + lnd: ['lnd', 'archy-lnd-ui'], + filebrowser: ['filebrowser'], + tailscale: ['tailscale'], + ollama: ['ollama'], +} + +/** Get app tier classification (matches backend get_app_tier) */ +export function getAppTier(appId: string): string { + const core = ['bitcoin-knots', 'bitcoin', 'lnd', 'mempool', 'btcpay-server', 'dwn', 'filebrowser'] + const recommended = ['fedimint', 'thunderhub', 'vaultwarden', 'uptime-kuma', 'grafana', 'searxng', 'tailscale', 'portainer'] + if (core.includes(appId)) return 'core' + if (recommended.includes(appId)) return 'recommended' + return 'optional' +} + +/** Categorize community apps based on their ID and description */ +export function categorizeCommunityApp(app: MarketplaceApp): string { + if (app.category) return app.category + + const id = app.id.toLowerCase() + const title = app.title?.toLowerCase() || '' + const description = (typeof app.description === 'string' ? app.description : (app.description as { short?: string })?.short ?? '').toLowerCase() + const combined = `${id} ${title} ${description}` + + if (id.includes('bitcoin') || id.includes('btc') || id.includes('lightning') || + id.includes('lnd') || id.includes('cln') || id.includes('electr') || + id.includes('fedimint') || id.includes('cashu') || title.includes('lightning') || + combined.includes('wallet') || combined.includes('satoshi')) { + return 'money' + } + + if (id.includes('btcpay') || id.includes('commerce') || id.includes('shop') || + id.includes('store') || id.includes('pos') || id.includes('payment') || + combined.includes('merchant') || combined.includes('invoice')) { + return 'commerce' + } + + if (id.includes('cloud') || id.includes('nextcloud') || id.includes('sync') || + id.includes('storage') || id.includes('backup') || id.includes('file') || + id.includes('photo') || id.includes('immich') || id.includes('jellyfin') || + id.includes('plex') || id.includes('media') || id.includes('vault') || + combined.includes('password manager') || combined.includes('file storage')) { + return 'data' + } + + if (id.includes('home-assistant') || id.includes('homeassistant') || + id.includes('smart-home') || id.includes('automation') || id.includes('iot') || + combined.includes('home automation') || combined.includes('smart home')) { + return 'home' + } + + if (id.includes('nostr') || (id.includes('relay') && combined.includes('nostr')) || + combined.includes('nostr relay') || combined.includes('nostr client')) { + return 'nostr' + } + + if (id.includes('vpn') || id.includes('wireguard') || id.includes('tailscale') || + id.includes('proxy') || id.includes('dns') || id.includes('pihole') || + id.includes('adguard') || id.includes('nginx') || id.includes('tor') || + combined.includes('network') || combined.includes('firewall')) { + return 'networking' + } + + if (id.includes('matrix') || id.includes('synapse') || id.includes('element') || + id.includes('mastodon') || id.includes('lemmy') || + id.includes('messenger') || id.includes('chat') || id.includes('social') || + id.includes('cups') || combined.includes('communication') || + combined.includes('messaging')) { + return 'community' + } + + return 'other' +} + +/** Curated list of apps with Docker Hub images */ +export function getCuratedAppList(): MarketplaceApp[] { + return [ + { + id: 'bitcoin-knots', + title: 'Bitcoin Knots', + version: '28.1.0', + description: 'Run a full Bitcoin node. Validate and relay blocks and transactions on the Bitcoin network.', + icon: '/assets/img/app-icons/bitcoin-knots.webp', + author: 'Bitcoin Knots', + dockerImage: 'docker.io/bitcoinknots/bitcoin:v28.1', + manifestUrl: undefined, + repoUrl: 'https://github.com/bitcoinknots/bitcoin' + }, + { + id: 'btcpay-server', + title: 'BTCPay Server', + version: '1.13.5', + description: 'Self-hosted Bitcoin payment processor. Accept Bitcoin payments without intermediaries or fees.', + icon: '/assets/img/app-icons/btcpay-server.png', + author: 'BTCPay Server Foundation', + dockerImage: 'docker.io/btcpayserver/btcpayserver:1.13.5', + manifestUrl: undefined, + repoUrl: 'https://github.com/btcpayserver/btcpayserver' + }, + { + id: 'lnd', + title: 'LND', + version: '0.17.4', + description: 'Lightning Network Daemon. Fast and cheap Bitcoin payments through the Lightning Network.', + icon: '/assets/img/app-icons/lnd.svg', + author: 'Lightning Labs', + dockerImage: 'docker.io/lightninglabs/lnd:v0.17.4-beta', + manifestUrl: undefined, + repoUrl: 'https://github.com/lightningnetwork/lnd' + }, + { + id: 'thunderhub', + title: 'ThunderHub', + version: '0.13.31', + description: 'Lightning node management UI. Manage channels, send and receive payments, view routing fees, and monitor your Lightning node.', + icon: '/assets/img/app-icons/thunderhub.svg', + author: 'Anthony Potdevin', + dockerImage: 'docker.io/apotdevin/thunderhub:v0.13.31', + manifestUrl: undefined, + repoUrl: 'https://github.com/apotdevin/thunderhub' + }, + { + id: 'mempool', + title: 'Mempool Explorer', + version: '2.5.0', + description: 'Self-hosted Bitcoin blockchain and mempool visualizer with beautiful explorer interface.', + icon: '/assets/img/app-icons/mempool.webp', + author: 'Mempool', + dockerImage: 'docker.io/mempool/frontend:v2.5.0', + manifestUrl: undefined, + repoUrl: 'https://github.com/mempool/mempool' + }, + { + id: 'homeassistant', + title: 'Home Assistant', + version: '2024.1', + description: 'Open-source home automation platform. Control and automate your smart home devices privately.', + icon: '/assets/img/app-icons/homeassistant.png', + author: 'Home Assistant', + dockerImage: 'docker.io/homeassistant/home-assistant:2024.1', + manifestUrl: undefined, + repoUrl: 'https://github.com/home-assistant/core' + }, + { + id: 'grafana', + title: 'Grafana', + version: '10.2.0', + description: 'Analytics and monitoring platform. Create dashboards and visualize data from multiple sources.', + icon: '/assets/img/app-icons/grafana.png', + author: 'Grafana Labs', + dockerImage: 'docker.io/grafana/grafana:10.2.0', + manifestUrl: undefined, + repoUrl: 'https://github.com/grafana/grafana' + }, + { + id: 'searxng', + title: 'SearXNG', + version: '2024.1.0', + description: 'Privacy-respecting metasearch engine. Search without tracking or ads.', + icon: '/assets/img/app-icons/searxng.png', + author: 'SearXNG', + dockerImage: 'docker.io/searxng/searxng:2024.11.17-e2554de75', + manifestUrl: undefined, + repoUrl: 'https://github.com/searxng/searxng' + }, + { + id: 'ollama', + title: 'Ollama', + version: '0.1.0', + description: 'Run large language models locally. Download and run AI models like Llama, Mistral on your own hardware.', + icon: '/assets/img/app-icons/ollama.png', + author: 'Ollama', + dockerImage: 'docker.io/ollama/ollama:0.5.4', + manifestUrl: undefined, + repoUrl: 'https://github.com/ollama/ollama' + }, + { + id: 'onlyoffice', + title: 'OnlyOffice', + version: '7.5.1', + description: 'Office suite for document collaboration. Edit docs, spreadsheets, and presentations.', + icon: '/assets/img/app-icons/onlyoffice.webp', + author: 'Ascensio System SIA', + dockerImage: 'docker.io/onlyoffice/documentserver:7.5.1', + manifestUrl: undefined, + repoUrl: 'https://github.com/ONLYOFFICE/DocumentServer' + }, + { + id: 'penpot', + title: 'Penpot', + version: '2.4', + description: 'Open-source design and prototyping platform. Self-hosted alternative to Figma.', + icon: '/assets/img/app-icons/penpot.webp', + author: 'Penpot', + dockerImage: 'docker.io/penpotapp/frontend:2.4', + manifestUrl: undefined, + repoUrl: 'https://github.com/penpot/penpot' + }, + { + id: 'nextcloud', + title: 'Nextcloud', + version: '28.0', + description: 'Self-hosted cloud storage and collaboration platform. Your own private cloud.', + icon: '/assets/img/app-icons/nextcloud.webp', + author: 'Nextcloud', + dockerImage: 'docker.io/library/nextcloud:28', + manifestUrl: undefined, + repoUrl: 'https://github.com/nextcloud/server' + }, + { + id: 'vaultwarden', + title: 'Vaultwarden', + version: '1.30.0', + description: 'Self-hosted password manager (Bitwarden-compatible). Secure vault for passwords and secrets.', + icon: '/assets/img/app-icons/vaultwarden.webp', + author: 'Vaultwarden', + dockerImage: 'docker.io/vaultwarden/server:1.30.0-alpine', + manifestUrl: undefined, + repoUrl: 'https://github.com/dani-garcia/vaultwarden' + }, + { + id: 'jellyfin', + title: 'Jellyfin', + version: '10.8.0', + description: 'Free media server system. Stream your movies, music, and photos to any device.', + icon: '/assets/img/app-icons/jellyfin.webp', + author: 'Jellyfin', + dockerImage: 'docker.io/jellyfin/jellyfin:10.8.13', + manifestUrl: undefined, + repoUrl: 'https://github.com/jellyfin/jellyfin' + }, + { + id: 'photoprism', + title: 'PhotoPrism', + version: '240915', + description: 'AI-powered photo management. Organize and browse photos with facial recognition.', + icon: '/assets/img/app-icons/photoprism.svg', + author: 'PhotoPrism', + dockerImage: 'docker.io/photoprism/photoprism:240915', + manifestUrl: undefined, + repoUrl: 'https://github.com/photoprism/photoprism' + }, + { + id: 'immich', + title: 'Immich', + version: '1.90.0', + description: 'High-performance self-hosted photo and video backup. Mobile-first with ML features.', + icon: '/assets/img/app-icons/immich.png', + author: 'Immich', + dockerImage: 'ghcr.io/immich-app/immich-server:release', + manifestUrl: undefined, + repoUrl: 'https://github.com/immich-app/immich' + }, + { + id: 'filebrowser', + title: 'File Browser', + version: '2.27.0', + description: 'Web-based file manager. Browse, upload, and manage files through a web interface.', + icon: '/assets/img/app-icons/file-browser.webp', + author: 'File Browser', + dockerImage: 'docker.io/filebrowser/filebrowser:v2.27.0', + manifestUrl: undefined, + repoUrl: 'https://github.com/filebrowser/filebrowser' + }, + { + id: 'nginx-proxy-manager', + title: 'Nginx Proxy Manager', + version: '2.11.0', + description: 'Easy proxy management with SSL. Beautiful web interface for managing reverse proxies.', + icon: '/assets/img/app-icons/nginx.svg', + author: 'Nginx Proxy Manager', + dockerImage: 'docker.io/jc21/nginx-proxy-manager:2.12.1', + manifestUrl: undefined, + repoUrl: 'https://github.com/NginxProxyManager/nginx-proxy-manager' + }, + { + id: 'portainer', + title: 'Portainer', + version: '2.19.0', + description: 'Container management UI. Manage Docker containers through a beautiful web interface.', + icon: '/assets/img/app-icons/portainer.webp', + author: 'Portainer', + dockerImage: 'docker.io/portainer/portainer-ce:2.19.4', + manifestUrl: undefined, + repoUrl: 'https://github.com/portainer/portainer' + }, + { + id: 'uptime-kuma', + title: 'Uptime Kuma', + version: '1.23.0', + description: 'Self-hosted monitoring tool. Monitor uptime for HTTP(s), TCP, DNS, and more.', + icon: '/assets/img/app-icons/uptime-kuma.webp', + author: 'Uptime Kuma', + dockerImage: 'docker.io/louislam/uptime-kuma:1', + manifestUrl: undefined, + repoUrl: 'https://github.com/louislam/uptime-kuma' + }, + { + id: 'tailscale', + title: 'Tailscale', + version: '1.78.0', + description: 'Zero-config VPN for secure remote access. Connect all your devices with WireGuard mesh network.', + icon: '/assets/img/app-icons/tailscale.webp', + author: 'Tailscale', + dockerImage: 'docker.io/tailscale/tailscale:stable', + manifestUrl: undefined, + repoUrl: 'https://github.com/tailscale/tailscale' + }, + { + id: 'fedimint', + title: 'Fedimint', + version: '0.10.0', + description: 'Federated Bitcoin mint with built-in Guardian UI. Private, scalable Bitcoin through federated guardians.', + icon: '/assets/img/app-icons/fedimint.png', + author: 'Fedimint', + dockerImage: 'docker.io/fedimint/fedimintd:v0.10.0', + manifestUrl: undefined, + repoUrl: 'https://github.com/fedimint/fedimint' + }, + { + id: 'indeedhub', + title: 'Indeehub', + version: '0.1.0', + description: 'Bitcoin documentary streaming platform with Nostr identity sign-in. Stream God Bless Bitcoin and other educational content about sovereignty and decentralized technology.', + icon: '/assets/img/app-icons/indeedhub.png', + author: 'Indeehub Team', + dockerImage: 'localhost/indeedhub:latest', + manifestUrl: undefined, + repoUrl: 'https://github.com/indeedhub/indeedhub' + }, + { + id: 'dwn', + title: 'Decentralized Web Node', + version: '0.4.0', + description: 'Store and sync your personal data across devices using decentralized web node protocols. Own your data with DID-based access control.', + icon: '/assets/img/app-icons/dwn.svg', + author: 'TBD', + dockerImage: 'ghcr.io/tbd54566975/dwn-server:main', + manifestUrl: undefined, + repoUrl: 'https://github.com/TBD54566975/dwn-server' + }, + { + id: 'nostrudel', + title: 'noStrudel', + version: '0.40.0', + category: 'nostr', + description: 'A feature-rich Nostr web client with NIP-07 signer support. Browse your feed, post notes, manage relays, and interact with the Nostr network — all signed with your node\'s Nostr identity.', + icon: '/assets/img/app-icons/nostrudel.svg', + author: 'hzrd149', + dockerImage: '', + manifestUrl: undefined, + repoUrl: 'https://github.com/hzrd149/nostrudel', + webUrl: 'https://nostrudel.ninja' + }, + { + id: 'nostr-rs-relay', + title: 'Nostr Relay', + version: '0.9.0', + category: 'nostr', + description: 'Run your own Nostr relay. Store your events locally, relay for friends, and publish over Tor. A sovereign relay for your sovereign node.', + icon: '/assets/img/app-icons/nostr-rs-relay.svg', + author: 'scsiblade', + dockerImage: 'docker.io/scsiblade/nostr-rs-relay:0.9.0', + manifestUrl: undefined, + repoUrl: 'https://sr.ht/~gheartsfield/nostr-rs-relay/' + }, + { + id: 'botfights', + title: 'BotFights', + version: '1.0.0', + description: 'AI bot arena — build, train, and battle autonomous agents. Compete in strategy tournaments with your own coded bots.', + icon: '/assets/img/app-icons/botfights.svg', + author: 'BotFights', + dockerImage: '', + manifestUrl: undefined, + repoUrl: 'https://botfights.net', + webUrl: 'https://botfights.net' + }, + { + id: 'nwnn', + title: 'Next Web News Network', + version: '1.0.0', + category: 'l484', + description: 'Decentralized news and link aggregator, synchronized from Telegram. Community-curated content on Bitcoin, sovereignty, and decentralized tech.', + icon: '/assets/img/app-icons/nwnn.png', + author: 'L484', + dockerImage: '', + manifestUrl: undefined, + repoUrl: 'https://nwnn.l484.com', + webUrl: 'https://nwnn.l484.com' + }, + { + id: '484-kitchen', + title: '484 Kitchen', + version: '1.0.0', + category: 'l484', + description: 'K484 application platform — an internal tool for the L484 network.', + icon: '/assets/img/app-icons/484-kitchen.png', + author: 'L484', + dockerImage: '', + manifestUrl: undefined, + repoUrl: 'https://484.kitchen', + webUrl: 'https://484.kitchen' + }, + { + id: 'call-the-operator', + title: 'Call the Operator', + version: '1.0.0', + category: 'l484', + description: 'Escape the Matrix — a portal for exploring decentralized alternatives and reclaiming digital sovereignty.', + icon: '/assets/img/app-icons/call-the-operator.png', + author: 'TX1138', + dockerImage: '', + manifestUrl: undefined, + repoUrl: 'https://cta.tx1138.com', + webUrl: 'https://cta.tx1138.com' + }, + { + id: 'arch-presentation', + title: 'Arch Presentation', + version: '1.0.0', + category: 'l484', + description: 'Archipelago: The Future of Decentralized Infrastructure — an interactive presentation about the Archipelago project vision.', + icon: '/assets/img/app-icons/arch-presentation.png', + author: 'L484', + dockerImage: '', + manifestUrl: undefined, + repoUrl: 'https://present.l484.com', + webUrl: 'https://present.l484.com' + }, + { + id: 'syntropy-institute', + title: 'Syntropy Institute', + version: '1.0.0', + category: 'l484', + description: 'Medicine Reimagined — Manual Kinetics, Syntropy Frequency analysis-therapy, digital homeopathy, and concierge protocols.', + icon: '/assets/img/app-icons/syntropy-institute.png', + author: 'Syntropy Institute', + dockerImage: '', + manifestUrl: undefined, + repoUrl: 'https://syntropy.institute', + webUrl: 'https://syntropy.institute' + }, + { + id: 't-zero', + title: 'T-0', + version: '1.0.0', + category: 'l484', + description: 'Documentary series exploring decentralization, Bitcoin, and the mavericks building the ungovernable future. Conversations with the builders, powered by Nostr.', + icon: '/assets/img/app-icons/t-zero.png', + author: 'T-0', + dockerImage: '', + manifestUrl: undefined, + repoUrl: 'https://teeminuszero.net', + webUrl: 'https://teeminuszero.net' + } + ] +} diff --git a/neode-ui/src/views/server/QuickActionsCard.vue b/neode-ui/src/views/server/QuickActionsCard.vue new file mode 100644 index 00000000..0a5277fa --- /dev/null +++ b/neode-ui/src/views/server/QuickActionsCard.vue @@ -0,0 +1,103 @@ + + + diff --git a/neode-ui/src/views/server/ServerModals.vue b/neode-ui/src/views/server/ServerModals.vue new file mode 100644 index 00000000..440d49e1 --- /dev/null +++ b/neode-ui/src/views/server/ServerModals.vue @@ -0,0 +1,260 @@ + + + diff --git a/neode-ui/src/views/server/TorServicesCard.vue b/neode-ui/src/views/server/TorServicesCard.vue new file mode 100644 index 00000000..2814c1cb --- /dev/null +++ b/neode-ui/src/views/server/TorServicesCard.vue @@ -0,0 +1,101 @@ + + +