feat(archy): extend useArchy with wallet/files context, add ArchyAppsGrid component

- Add wallet (Lightning balance, channels) and files (Nextcloud) context
  categories to useArchy composable with AI prompt injection
- Create archy-apps.ts data file mapping all 18 Archy services
- Build ArchyAppsGrid component with live status from bridge
- Wire ArchyAppsGrid into ContentPanel when embedded in Archy

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-04 12:15:33 +00:00
co-authored by Claude Opus 4.6
parent fe94693ef6
commit 20ef35c8e3
4 changed files with 380 additions and 0 deletions
@@ -0,0 +1,138 @@
<template>
<div class="h-full flex flex-col">
<div class="p-4 space-y-3" style="border-bottom: 1px solid rgba(255, 255, 255, 0.08)">
<div class="flex items-center justify-between gap-2">
<h3 class="text-sm font-bold text-white/90">
Node Apps
</h3>
<span class="text-[10px] font-mono text-white/30">
{{ filteredApps.length }} apps
</span>
</div>
<input
v-model="search"
type="text"
placeholder="Search node apps..."
class="w-full px-3 py-2 rounded-lg text-xs outline-none transition-colors bg-white/5 text-white/80 placeholder:text-white/25 focus:bg-white/10"
style="font-size: 16px"
/>
<div class="flex flex-wrap gap-1.5">
<button
v-for="cat in categories"
:key="cat.value"
class="text-[10px] px-2 py-1 rounded-md transition-all duration-150"
:class="activeCategory === cat.value
? 'nav-tab-active'
: 'text-white/40 hover:text-white/70 hover:bg-white/5'"
@click="activeCategory = activeCategory === cat.value ? null : cat.value"
>
{{ cat.label }}
</button>
</div>
</div>
<div class="flex-1 overflow-y-auto custom-scrollbar px-4 pt-4 pb-16">
<div class="grid grid-cols-2 gap-2">
<button
v-for="app in filteredApps"
:key="app.id"
class="text-left p-3 rounded-xl transition-all duration-200 glass-card"
:class="app.liveStatus === 'running' ? 'hover:bg-white/10 cursor-pointer' : 'opacity-70'"
@click="handleAppClick(app)"
>
<div class="flex items-center gap-2 mb-1.5">
<span class="text-lg leading-none">{{ app.icon }}</span>
<span class="text-xs font-semibold text-white/90 truncate">{{ app.name }}</span>
</div>
<p class="text-[10px] text-white/50 line-clamp-2 leading-relaxed">
{{ app.description }}
</p>
<div class="mt-2 flex items-center gap-1.5">
<span
class="w-1.5 h-1.5 rounded-full"
:class="statusDotClass(app.liveStatus)"
/>
<span class="text-[10px]" :class="statusTextClass(app.liveStatus)">
{{ statusLabel(app.liveStatus) }}
</span>
</div>
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ARCHY_APPS, type ArchyAppMeta } from '@/data/archy-apps'
import { useArchy } from '@/composables/useArchy'
interface MergedApp extends ArchyAppMeta {
liveStatus: 'running' | 'stopped' | 'not-installed'
}
const { isEmbedded, installedApps, requestAction } = useArchy()
const search = ref('')
const activeCategory = ref<string | null>(null)
const categories = [
{ label: 'Bitcoin', value: 'bitcoin' },
{ label: 'Lightning', value: 'lightning' },
{ label: 'Storage', value: 'storage' },
{ label: 'Social', value: 'social' },
{ label: 'Tools', value: 'tools' },
{ label: 'AI', value: 'ai' },
]
const mergedApps = computed<MergedApp[]>(() => {
return ARCHY_APPS.map((app) => {
const live = installedApps.value.find((a) => a.id === app.id)
let liveStatus: MergedApp['liveStatus'] = 'not-installed'
if (live) {
liveStatus = live.state === 'running' ? 'running' : 'stopped'
}
return { ...app, liveStatus }
})
})
const filteredApps = computed(() => {
let apps = mergedApps.value
if (activeCategory.value) {
apps = apps.filter((a) => a.category === activeCategory.value)
}
if (search.value.trim()) {
const q = search.value.toLowerCase()
apps = apps.filter((a) =>
a.name.toLowerCase().includes(q) || a.description.toLowerCase().includes(q),
)
}
return apps
})
function statusDotClass(status: MergedApp['liveStatus']) {
if (status === 'running') return 'bg-green-400'
if (status === 'stopped') return 'bg-yellow-400'
return 'bg-white/20'
}
function statusTextClass(status: MergedApp['liveStatus']) {
if (status === 'running') return 'text-green-400/80'
if (status === 'stopped') return 'text-yellow-400/70'
return 'text-white/30'
}
function statusLabel(status: MergedApp['liveStatus']) {
if (status === 'running') return 'Running'
if (status === 'stopped') return 'Stopped'
return isEmbedded.value ? 'Not installed' : 'Available'
}
function handleAppClick(app: MergedApp) {
if (app.liveStatus === 'running' && isEmbedded.value) {
requestAction('open-app', { appId: app.id })
}
}
</script>
@@ -187,6 +187,9 @@
:query="panelQuery"
:hero-image="panelMagazineHeroImage ?? undefined"
/>
<ArchyAppsGrid
v-else-if="activeTab === 'app' && isArchyEmbedded"
/>
<AppsGrid
v-else-if="activeTab === 'app'"
:apps="panelApps"
@@ -233,6 +236,7 @@ import PdfViewer from '@/components/renderers/PdfViewer.vue'
import MapRenderer from '@/components/renderers/MapRenderer.vue'
import MagazineGrid from './MagazineGrid.vue'
import AppsGrid from './AppsGrid.vue'
import ArchyAppsGrid from './ArchyAppsGrid.vue'
import AppDetail from './AppDetail.vue'
import ProjectGrid from './ProjectGrid.vue'
import NostrGrid from './NostrGrid.vue'
@@ -240,6 +244,7 @@ import FavoritesGrid from './FavoritesGrid.vue'
import DiscoverPanel from './DiscoverPanel.vue'
import ErrorBoundary from '@/components/ui/ErrorBoundary.vue'
import { useFavoritesStore } from '@/stores/favorites'
import { useArchy } from '@/composables/useArchy'
// Film and song renderers loaded from plugin registry
const filmRenderer = computed(() => getRendererForContentType('film'))
@@ -247,6 +252,7 @@ const songRenderer = computed(() => getRendererForContentType('song'))
const { isDark } = useTheme()
const favoritesStore = useFavoritesStore()
const { isEmbedded: isArchyEmbedded } = useArchy()
const {
panelOpen,
+56
View File
@@ -19,6 +19,21 @@ interface ArchyNetworkInfo {
connected?: boolean
}
export interface ArchyWalletInfo {
balanceSats?: number
channelCount?: number
totalCapacitySats?: number
nodePubkey?: string
}
export interface ArchyFileEntry {
name: string
path: string
size?: number
modified?: string
type: 'file' | 'folder'
}
// Singleton reactive state (shared across all components using this composable)
const isEmbedded = ref(false)
const isInitialized = ref(false)
@@ -27,6 +42,8 @@ const accentColor = ref<string | null>(null)
const installedApps = ref<ArchyApp[]>([])
const systemInfo = ref<ArchySystemInfo>({})
const networkInfo = ref<ArchyNetworkInfo>({})
const walletInfo = ref<ArchyWalletInfo>({})
const fileList = ref<ArchyFileEntry[]>([])
let cleanups: (() => void)[] = []
/**
@@ -98,6 +115,26 @@ export function useArchy() {
)
}
if (cats.includes('wallet')) {
fetches.push(
archyBridge.requestContext('wallet').then((res) => {
if (res.permitted && res.data) {
walletInfo.value = res.data as ArchyWalletInfo
}
}).catch(() => {}),
)
}
if (cats.includes('files')) {
fetches.push(
archyBridge.requestContext('files').then((res) => {
if (res.permitted && Array.isArray(res.data)) {
fileList.value = res.data as ArchyFileEntry[]
}
}).catch(() => {}),
)
}
await Promise.all(fetches)
}
@@ -141,6 +178,23 @@ export function useArchy() {
sections.push(`**Network:** ${net.connected ? 'Connected' : 'Disconnected'}`)
}
if (permissions.value.includes('wallet') && walletInfo.value.balanceSats !== undefined) {
const w = walletInfo.value
const balance = w.balanceSats!
const parts = [`Balance: ${balance.toLocaleString()} sats`]
if (w.channelCount !== undefined) parts.push(`${w.channelCount} channels`)
if (w.totalCapacitySats !== undefined) parts.push(`Total capacity: ${w.totalCapacitySats.toLocaleString()} sats`)
if (w.nodePubkey) parts.push(`Pubkey: ${w.nodePubkey.slice(0, 8)}...`)
sections.push(`**Lightning Wallet:** ${parts.join(' | ')}`)
}
if (permissions.value.includes('files') && fileList.value.length > 0) {
const files = fileList.value
const recent = files.slice(0, 20)
const fileNames = recent.map((f) => f.name).join(', ')
sections.push(`**Files:** ${files.length} files in Nextcloud. Recent: ${fileNames}`)
}
if (sections.length === 0) return ''
return `\n\n**Archy Node Context** (this user is running AIUI on their Archipelago node):\n${sections.join('\n')}\n\nYou can help the user manage their node. Available actions: open an app (open-app), install an app (install-app), navigate in Archy (navigate). When recommending apps, check if they're already installed.`
@@ -162,6 +216,8 @@ export function useArchy() {
installedApps: readonly(installedApps),
systemInfo: readonly(systemInfo),
networkInfo: readonly(networkInfo),
walletInfo: readonly(walletInfo),
fileList: readonly(fileList),
init,
destroy,
refreshContext,
+180
View File
@@ -0,0 +1,180 @@
export interface ArchyAppMeta {
id: string
name: string
description: string
icon: string
category: 'bitcoin' | 'lightning' | 'storage' | 'social' | 'tools' | 'monitoring' | 'ai' | 'identity'
defaultPort?: number
deepLink: string
}
export const ARCHY_APPS: ArchyAppMeta[] = [
// ─── Bitcoin & Lightning ────────────────────────────────────────
{
id: 'bitcoin-core',
name: 'Bitcoin Core',
description: 'Full Bitcoin node — blockchain validation, UTXO set, fee estimates',
icon: '₿',
category: 'bitcoin',
defaultPort: 8332,
deepLink: '/app/bitcoin-core',
},
{
id: 'lnd',
name: 'LND',
description: 'Lightning Network Daemon — channels, payments, invoices',
icon: '⚡',
category: 'lightning',
defaultPort: 8080,
deepLink: '/app/lnd',
},
{
id: 'core-lightning',
name: 'Core Lightning',
description: 'C-Lightning implementation with plugin ecosystem',
icon: '⚡',
category: 'lightning',
defaultPort: 9735,
deepLink: '/app/core-lightning',
},
{
id: 'btcpay-server',
name: 'BTCPay Server',
description: 'Self-hosted payment processor for Bitcoin and Lightning',
icon: '🛒',
category: 'bitcoin',
defaultPort: 23001,
deepLink: '/app/btcpay-server',
},
{
id: 'mempool',
name: 'Mempool',
description: 'Blockchain explorer — visualize transactions, fees, blocks',
icon: '🔍',
category: 'bitcoin',
defaultPort: 3006,
deepLink: '/app/mempool',
},
{
id: 'fedimint',
name: 'Fedimint',
description: 'Federated Chaumian e-cash — community custody',
icon: '🏦',
category: 'bitcoin',
deepLink: '/app/fedimint',
},
// ─── Storage & Files ────────────────────────────────────────────
{
id: 'nextcloud',
name: 'Nextcloud',
description: 'Files, notes, contacts, calendar — self-hosted cloud',
icon: '☁️',
category: 'storage',
defaultPort: 8443,
deepLink: '/app/nextcloud',
},
{
id: 'immich',
name: 'Immich',
description: 'Photo & video management with ML tagging',
icon: '📸',
category: 'storage',
defaultPort: 2283,
deepLink: '/app/immich',
},
// ─── Social & Nostr ─────────────────────────────────────────────
{
id: 'nostr-rs-relay',
name: 'nostr-rs-relay',
description: 'High-performance Nostr relay in Rust',
icon: '🟣',
category: 'social',
defaultPort: 7000,
deepLink: '/app/nostr-rs-relay',
},
{
id: 'strfry',
name: 'strfry',
description: 'C++ Nostr relay — fast event processing',
icon: '🟣',
category: 'social',
deepLink: '/app/strfry',
},
// ─── Tools ──────────────────────────────────────────────────────
{
id: 'home-assistant',
name: 'Home Assistant',
description: 'Smart home automation and control',
icon: '🏠',
category: 'tools',
defaultPort: 8123,
deepLink: '/app/home-assistant',
},
{
id: 'searxng',
name: 'SearXNG',
description: 'Privacy-respecting metasearch engine',
icon: '🔎',
category: 'tools',
defaultPort: 8888,
deepLink: '/app/searxng',
},
{
id: 'penpot',
name: 'Penpot',
description: 'Open-source design tool — prototyping and collaboration',
icon: '🎨',
category: 'tools',
deepLink: '/app/penpot',
},
{
id: 'onlyoffice',
name: 'OnlyOffice',
description: 'Document editing — docs, spreadsheets, presentations',
icon: '📝',
category: 'tools',
deepLink: '/app/onlyoffice',
},
{
id: 'meshtastic',
name: 'Meshtastic',
description: 'Off-grid mesh networking over LoRa radios',
icon: '📡',
category: 'tools',
deepLink: '/app/meshtastic',
},
// ─── Monitoring ─────────────────────────────────────────────────
{
id: 'grafana',
name: 'Grafana',
description: 'Monitoring dashboards — system metrics and alerts',
icon: '📊',
category: 'monitoring',
defaultPort: 3000,
deepLink: '/app/grafana',
},
// ─── AI ─────────────────────────────────────────────────────────
{
id: 'ollama',
name: 'Ollama',
description: 'Run LLMs locally — Llama, Mistral, Gemma',
icon: '🧠',
category: 'ai',
defaultPort: 11434,
deepLink: '/app/ollama',
},
// ─── Identity ───────────────────────────────────────────────────
{
id: 'did-wallet',
name: 'DID Wallet',
description: 'Decentralized identity — Web5 verifiable credentials',
icon: '🪪',
category: 'identity',
deepLink: '/app/did-wallet',
},
]
/** Look up an Archy app by its ID */
export function getArchyApp(id: string): ArchyAppMeta | undefined {
return ARCHY_APPS.find((a) => a.id === id)
}