fix: resolve merge conflicts and compile errors for transport layer
- Resolve stash conflicts in Cargo.toml, rpc/mod.rs, AppDetails.vue, Apps.vue - Fix ScopedIp conversion in LAN transport (mdns-sd compatibility) - Fix String vs &str in transport RPC send handler - Remove duplicate mod transport declaration - Remove stale mesh.discover route (replaced by mesh.peers/messages/send) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
253c305cc8
commit
174dad9a66
@ -89,14 +89,13 @@ bytes = "1"
|
|||||||
serial2-tokio = "0.1"
|
serial2-tokio = "0.1"
|
||||||
|
|
||||||
# Transport abstraction (Phase 2: mesh as federation transport)
|
# Transport abstraction (Phase 2: mesh as federation transport)
|
||||||
ciborium = "0.2.2" # CBOR serde for compact delta sync
|
ciborium = "0.2.2"
|
||||||
reed-solomon-erasure = "6.0" # FEC for chunked LoRa messages
|
reed-solomon-erasure = "6.0"
|
||||||
mdns-sd = "0.18" # LAN peer discovery via mDNS
|
mdns-sd = "0.18"
|
||||||
|
|
||||||
# Systemd watchdog notification
|
# Systemd watchdog notification
|
||||||
sd-notify = "0.4"
|
sd-notify = "0.4"
|
||||||
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio-test = "0.4"
|
tokio-test = "0.4"
|
||||||
tempfile = "3.10"
|
tempfile = "3.10"
|
||||||
|
|||||||
@ -96,7 +96,7 @@ impl RpcHandler {
|
|||||||
message_type: MessageType::PeerMessage,
|
message_type: MessageType::PeerMessage,
|
||||||
};
|
};
|
||||||
|
|
||||||
let transport_used = router.send_to_peer(did, &message).await?;
|
let transport_used = router.send_to_peer(&did, &message).await?;
|
||||||
|
|
||||||
info!(did = %did, transport = %transport_used, "Sent message via transport");
|
info!(did = %did, transport = %transport_used, "Sent message via transport");
|
||||||
Ok(serde_json::json!({
|
Ok(serde_json::json!({
|
||||||
|
|||||||
@ -209,6 +209,76 @@ impl Server {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize mesh networking service (if config has enabled: true)
|
||||||
|
{
|
||||||
|
let data_dir = config.data_dir.clone();
|
||||||
|
let did = identity::did_key_from_pubkey_hex(&data.server_info.pubkey)
|
||||||
|
.unwrap_or_default();
|
||||||
|
let pubkey_hex = identity.pubkey_hex();
|
||||||
|
let signing_key = identity.signing_key();
|
||||||
|
match crate::mesh::MeshService::new(&data_dir, signing_key, &did, &pubkey_hex).await {
|
||||||
|
Ok(mut mesh_service) => {
|
||||||
|
let mesh_config = crate::mesh::load_config(&data_dir).await.unwrap_or_default();
|
||||||
|
if mesh_config.enabled {
|
||||||
|
if let Err(e) = mesh_service.start() {
|
||||||
|
warn!("Mesh service start failed (non-fatal): {}", e);
|
||||||
|
} else {
|
||||||
|
info!("📡 Mesh networking started");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
api_handler.rpc_handler().set_mesh_service(mesh_service).await;
|
||||||
|
info!("📡 Mesh service initialized");
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Mesh service init failed (non-fatal): {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize transport router (unified routing: mesh > lan > tor)
|
||||||
|
{
|
||||||
|
let data_dir = config.data_dir.clone();
|
||||||
|
let did = identity::did_key_from_pubkey_hex(&data.server_info.pubkey)
|
||||||
|
.unwrap_or_default();
|
||||||
|
let pubkey_hex = identity.pubkey_hex();
|
||||||
|
let mesh_config = crate::mesh::load_config(&data_dir).await.unwrap_or_default();
|
||||||
|
let mesh_only = mesh_config.mesh_only_mode.unwrap_or(false);
|
||||||
|
|
||||||
|
match crate::transport::PeerRegistry::load(&data_dir).await {
|
||||||
|
Ok(registry) => {
|
||||||
|
let registry = std::sync::Arc::new(registry);
|
||||||
|
let mut transports: Vec<Box<dyn crate::transport::NodeTransport>> = Vec::new();
|
||||||
|
|
||||||
|
transports.push(Box::new(
|
||||||
|
crate::transport::tor::TorTransport::new(&pubkey_hex),
|
||||||
|
));
|
||||||
|
transports.push(Box::new(
|
||||||
|
crate::transport::mesh_transport::MeshTransport::new(
|
||||||
|
api_handler.rpc_handler().mesh_service_arc(),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
let mut lan = crate::transport::lan::LanTransport::new(&did, &pubkey_hex, 5678);
|
||||||
|
match lan.start(registry.clone()) {
|
||||||
|
Ok(()) => info!("📡 LAN transport (mDNS) started"),
|
||||||
|
Err(e) => debug!("LAN transport init (non-fatal): {}", e),
|
||||||
|
}
|
||||||
|
transports.push(Box::new(lan));
|
||||||
|
|
||||||
|
let router = std::sync::Arc::new(crate::transport::TransportRouter::new(
|
||||||
|
transports,
|
||||||
|
registry,
|
||||||
|
mesh_only,
|
||||||
|
));
|
||||||
|
api_handler.rpc_handler().set_transport_router(router).await;
|
||||||
|
info!("📡 Transport router initialized (mesh_only={})", mesh_only);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Transport router init failed (non-fatal): {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize container scanner — discovers installed apps from Podman/Docker
|
// Initialize container scanner — discovers installed apps from Podman/Docker
|
||||||
{
|
{
|
||||||
let scanner = create_docker_scanner(&config).await?;
|
let scanner = create_docker_scanner(&config).await?;
|
||||||
|
|||||||
@ -89,7 +89,10 @@ impl LanTransport {
|
|||||||
|
|
||||||
if let (Some(did), Some(pubkey)) = (did, pubkey) {
|
if let (Some(did), Some(pubkey)) = (did, pubkey) {
|
||||||
if let Some(scoped_ip) = addresses.iter().next() {
|
if let Some(scoped_ip) = addresses.iter().next() {
|
||||||
let ip: std::net::IpAddr = (*scoped_ip).into();
|
let ip: std::net::IpAddr = match scoped_ip.to_string().parse() {
|
||||||
|
Ok(ip) => ip,
|
||||||
|
Err(_) => continue,
|
||||||
|
};
|
||||||
let socket_addr = std::net::SocketAddr::new(ip, info.get_port());
|
let socket_addr = std::net::SocketAddr::new(ip, info.get_port());
|
||||||
info!(did = %did, addr = %socket_addr, "Discovered LAN peer via mDNS");
|
info!(did = %did, addr = %socket_addr, "Discovered LAN peer via mDNS");
|
||||||
registry_clone
|
registry_clone
|
||||||
|
|||||||
@ -417,7 +417,7 @@
|
|||||||
class="fixed inset-0 z-50 flex items-center justify-center p-4"
|
class="fixed inset-0 z-50 flex items-center justify-center p-4"
|
||||||
@click="closeUninstallModal()"
|
@click="closeUninstallModal()"
|
||||||
>
|
>
|
||||||
<div class="absolute inset-0 bg-black/10 backdrop-blur-md"></div>
|
<div class="absolute inset-0 bg-black/60 backdrop-blur-sm"></div>
|
||||||
<div
|
<div
|
||||||
ref="uninstallModalRef"
|
ref="uninstallModalRef"
|
||||||
@click.stop
|
@click.stop
|
||||||
@ -486,6 +486,7 @@ const appId = computed(() => route.params.id as string)
|
|||||||
|
|
||||||
// Web-only app detection (no container — external websites)
|
// Web-only app detection (no container — external websites)
|
||||||
const WEB_ONLY_APP_URLS: Record<string, string> = {
|
const WEB_ONLY_APP_URLS: Record<string, string> = {
|
||||||
|
'indeedhub': `${window.location.protocol}//${window.location.hostname}:7777`,
|
||||||
'botfights': 'https://botfights.net',
|
'botfights': 'https://botfights.net',
|
||||||
'nwnn': 'https://nwnn.l484.com',
|
'nwnn': 'https://nwnn.l484.com',
|
||||||
'484-kitchen': 'https://484.kitchen',
|
'484-kitchen': 'https://484.kitchen',
|
||||||
@ -500,6 +501,8 @@ const isWebOnly = computed(() => appId.value in WEB_ONLY_APP_URLS)
|
|||||||
/** Map route/marketplace app IDs to backend package keys (container names). */
|
/** Map route/marketplace app IDs to backend package keys (container names). */
|
||||||
const ROUTE_TO_PACKAGE_KEY: Record<string, string> = {
|
const ROUTE_TO_PACKAGE_KEY: Record<string, string> = {
|
||||||
mempool: 'mempool-web',
|
mempool: 'mempool-web',
|
||||||
|
'mempool-electrs': 'mempool-electrs',
|
||||||
|
electrs: 'mempool-electrs',
|
||||||
btcpay: 'btcpay-server',
|
btcpay: 'btcpay-server',
|
||||||
'btcpay-server': 'btcpay-server',
|
'btcpay-server': 'btcpay-server',
|
||||||
fedimint: 'fedimint',
|
fedimint: 'fedimint',
|
||||||
@ -525,19 +528,12 @@ const ROUTE_TO_PACKAGE_KEY: Record<string, string> = {
|
|||||||
portainer: 'portainer',
|
portainer: 'portainer',
|
||||||
'uptime-kuma': 'uptime-kuma',
|
'uptime-kuma': 'uptime-kuma',
|
||||||
tailscale: 'tailscale',
|
tailscale: 'tailscale',
|
||||||
indeedhub: 'indeedhub',
|
|
||||||
electrumx: 'electrumx',
|
|
||||||
electrs: 'electrumx',
|
|
||||||
'mempool-electrs': 'electrumx',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Backend may register under variant container names */
|
/** Backend may register under variant container names */
|
||||||
const PACKAGE_ALIASES: Record<string, string[]> = {
|
const PACKAGE_ALIASES: Record<string, string[]> = {
|
||||||
immich: ['immich_server', 'immich-server'],
|
immich: ['immich_server', 'immich-server'],
|
||||||
nextcloud: ['nextcloud-aio', 'nextcloud-server'],
|
nextcloud: ['nextcloud-aio', 'nextcloud-server'],
|
||||||
'mempool-web': ['archy-mempool-web'],
|
|
||||||
indeedhub: ['indeedhub-build_app_1'],
|
|
||||||
electrumx: ['mempool-electrs', 'electrs', 'archy-electrs'],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolvePackageKey(routeId: string): string {
|
function resolvePackageKey(routeId: string): string {
|
||||||
@ -718,7 +714,131 @@ function goBack() {
|
|||||||
|
|
||||||
function launchApp() {
|
function launchApp() {
|
||||||
if (!pkg.value) return
|
if (!pkg.value) return
|
||||||
useAppLauncherStore().openSession(appId.value)
|
|
||||||
|
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<string, { dev: string, prod: string }> = {
|
||||||
|
'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 (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'))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function startApp() {
|
async function startApp() {
|
||||||
|
|||||||
@ -1,50 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="pb-6">
|
<div class="pb-6">
|
||||||
<!-- Desktop: tabs + search in one row -->
|
<div class="hidden md:block mb-8">
|
||||||
<div class="hidden md:flex items-center gap-4 mb-4">
|
<h1 class="text-3xl font-bold text-white mb-2">{{ t('apps.title') }}</h1>
|
||||||
<div class="mode-switcher flex-shrink-0">
|
<p class="text-white/70">{{ t('apps.subtitle') }}</p>
|
||||||
<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"
|
|
||||||
:placeholder="t('apps.searchPlaceholder')"
|
|
||||||
:aria-label="t('apps.searchLabel')"
|
|
||||||
class="flex-1 px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/50 focus:outline-none focus:border-white/40 transition-colors"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Mobile: tabs + search -->
|
<!-- Search Bar -->
|
||||||
<div class="md:hidden mb-4">
|
<div class="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
|
<input
|
||||||
v-model="searchQuery"
|
v-model="searchQuery"
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="t('apps.searchPlaceholder')"
|
:placeholder="t('apps.searchPlaceholder')"
|
||||||
:aria-label="t('apps.searchLabel')"
|
:aria-label="t('apps.searchLabel')"
|
||||||
class="w-full px-4 py-3 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/50 focus:outline-none focus:border-white/40 transition-colors"
|
class="w-full px-4 py-3 md:py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/50 focus:outline-none focus:border-white/40 transition-colors"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -201,14 +169,14 @@
|
|||||||
class="fixed inset-0 z-50 flex items-center justify-center p-4"
|
class="fixed inset-0 z-50 flex items-center justify-center p-4"
|
||||||
@click="closeUninstallModal()"
|
@click="closeUninstallModal()"
|
||||||
>
|
>
|
||||||
<div class="absolute inset-0 bg-black/60 backdrop-blur-md"></div>
|
<div class="absolute inset-0 bg-black/60 backdrop-blur-sm"></div>
|
||||||
<div
|
<div
|
||||||
ref="uninstallModalRef"
|
ref="uninstallModalRef"
|
||||||
@click.stop
|
@click.stop
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-labelledby="uninstall-dialog-title"
|
aria-labelledby="uninstall-dialog-title"
|
||||||
class="glass-card p-6 max-w-2xl w-full relative z-10"
|
class="glass-card p-6 max-w-md w-full relative z-10"
|
||||||
>
|
>
|
||||||
<div class="flex items-start gap-4 mb-4">
|
<div class="flex items-start gap-4 mb-4">
|
||||||
<div class="p-3 bg-red-500/20 rounded-lg">
|
<div class="p-3 bg-red-500/20 rounded-lg">
|
||||||
@ -233,20 +201,9 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="confirmUninstall"
|
@click="confirmUninstall"
|
||||||
:disabled="uninstalling"
|
class="px-4 py-2 bg-red-600/80 hover:bg-red-600 rounded-lg text-white text-sm font-medium transition-colors"
|
||||||
class="px-4 py-2 bg-red-600/80 hover:bg-red-600 rounded-lg text-white text-sm font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
|
||||||
>
|
>
|
||||||
<svg
|
{{ t('common.uninstall') }}
|
||||||
v-if="uninstalling"
|
|
||||||
class="animate-spin h-4 w-4"
|
|
||||||
aria-hidden="true"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
||||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
||||||
</svg>
|
|
||||||
<span>{{ uninstalling ? t('common.uninstalling') : t('common.uninstall') }}</span>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -279,28 +236,6 @@ import { useModalKeyboard } from '@/composables/useModalKeyboard'
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const store = useAppStore()
|
const store = useAppStore()
|
||||||
|
|
||||||
// Tabs
|
|
||||||
const activeTab = ref<'apps' | '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',
|
|
||||||
'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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search
|
// Search
|
||||||
const searchQuery = ref('')
|
const searchQuery = ref('')
|
||||||
|
|
||||||
@ -319,11 +254,12 @@ function showActionError(msg: string) {
|
|||||||
|
|
||||||
// Web-only app IDs and their URLs
|
// Web-only app IDs and their URLs
|
||||||
const WEB_ONLY_APP_URLS: Record<string, string> = {
|
const WEB_ONLY_APP_URLS: Record<string, string> = {
|
||||||
|
'indeedhub': `${window.location.protocol}//${window.location.hostname}:7777`,
|
||||||
'botfights': 'https://botfights.net',
|
'botfights': 'https://botfights.net',
|
||||||
'nwnn': 'https://nwnn.l484.com',
|
'nwnn': 'https://nwnn.l484.com',
|
||||||
'484-kitchen': 'https://484.kitchen',
|
'484-kitchen': 'https://484.kitchen',
|
||||||
'call-the-operator': 'https://cta.tx1138.com',
|
'call-the-operator': 'https://cta.tx1138.com',
|
||||||
// 'arch-presentation': hidden until X-Frame-Options fixed
|
'arch-presentation': 'https://present.l484.com',
|
||||||
'syntropy-institute': 'https://syntropy.institute',
|
'syntropy-institute': 'https://syntropy.institute',
|
||||||
't-zero': 'https://teeminuszero.net',
|
't-zero': 'https://teeminuszero.net',
|
||||||
}
|
}
|
||||||
@ -334,6 +270,11 @@ function isWebOnlyApp(id: string): boolean {
|
|||||||
|
|
||||||
// Web-only apps (no container) — always show as installed bookmarks
|
// Web-only apps (no container) — always show as installed bookmarks
|
||||||
const WEB_ONLY_APPS: Record<string, PackageDataEntry> = {
|
const WEB_ONLY_APPS: Record<string, PackageDataEntry> = {
|
||||||
|
'indeedhub': {
|
||||||
|
state: 'running' as PackageState,
|
||||||
|
manifest: { id: 'indeedhub', title: 'Indeehub', version: '0.1.0', description: { short: 'Bitcoin documentary streaming platform', long: '' }, 'release-notes': '', license: '', 'wrapper-repo': '', 'upstream-repo': '', 'support-site': '', 'marketing-site': '', 'donation-url': null },
|
||||||
|
'static-files': { license: '', instructions: '', icon: '/assets/img/app-icons/indeehub.ico' },
|
||||||
|
},
|
||||||
'botfights': {
|
'botfights': {
|
||||||
state: 'running' as PackageState,
|
state: 'running' as PackageState,
|
||||||
manifest: { id: 'botfights', title: 'BotFights', version: '1.0.0', description: { short: 'AI bot arena — build, train, and battle autonomous agents', long: '' }, 'release-notes': '', license: '', 'wrapper-repo': '', 'upstream-repo': '', 'support-site': '', 'marketing-site': '', 'donation-url': null },
|
manifest: { id: 'botfights', title: 'BotFights', version: '1.0.0', description: { short: 'AI bot arena — build, train, and battle autonomous agents', long: '' }, 'release-notes': '', license: '', 'wrapper-repo': '', 'upstream-repo': '', 'support-site': '', 'marketing-site': '', 'donation-url': null },
|
||||||
@ -354,12 +295,11 @@ const WEB_ONLY_APPS: Record<string, PackageDataEntry> = {
|
|||||||
manifest: { id: 'call-the-operator', title: 'Call the Operator', version: '1.0.0', description: { short: 'Escape the Matrix — explore decentralized alternatives', long: '' }, 'release-notes': '', license: '', 'wrapper-repo': '', 'upstream-repo': '', 'support-site': '', 'marketing-site': '', 'donation-url': null },
|
manifest: { id: 'call-the-operator', title: 'Call the Operator', version: '1.0.0', description: { short: 'Escape the Matrix — explore decentralized alternatives', long: '' }, 'release-notes': '', license: '', 'wrapper-repo': '', 'upstream-repo': '', 'support-site': '', 'marketing-site': '', 'donation-url': null },
|
||||||
'static-files': { license: '', instructions: '', icon: '/assets/img/app-icons/call-the-operator.png' },
|
'static-files': { license: '', instructions: '', icon: '/assets/img/app-icons/call-the-operator.png' },
|
||||||
},
|
},
|
||||||
/* arch-presentation hidden until X-Frame-Options fixed
|
|
||||||
'arch-presentation': {
|
'arch-presentation': {
|
||||||
state: 'running' as PackageState,
|
state: 'running' as PackageState,
|
||||||
manifest: { id: 'arch-presentation', title: 'Arch Presentation', version: '1.0.0', description: { short: 'Archipelago: The Future of Decentralized Infrastructure', long: '' }, 'release-notes': '', license: '', 'wrapper-repo': '', 'upstream-repo': '', 'support-site': '', 'marketing-site': '', 'donation-url': null },
|
manifest: { id: 'arch-presentation', title: 'Arch Presentation', version: '1.0.0', description: { short: 'Archipelago: The Future of Decentralized Infrastructure', long: '' }, 'release-notes': '', license: '', 'wrapper-repo': '', 'upstream-repo': '', 'support-site': '', 'marketing-site': '', 'donation-url': null },
|
||||||
'static-files': { license: '', instructions: '', icon: '/assets/img/app-icons/arch-presentation.png' },
|
'static-files': { license: '', instructions: '', icon: '/assets/img/app-icons/arch-presentation.png' },
|
||||||
}, */
|
},
|
||||||
'syntropy-institute': {
|
'syntropy-institute': {
|
||||||
state: 'running' as PackageState,
|
state: 'running' as PackageState,
|
||||||
manifest: { id: 'syntropy-institute', title: 'Syntropy Institute', version: '1.0.0', description: { short: 'Medicine Reimagined — frequency analysis-therapy', long: '' }, 'release-notes': '', license: '', 'wrapper-repo': '', 'upstream-repo': '', 'support-site': '', 'marketing-site': '', 'donation-url': null },
|
manifest: { id: 'syntropy-institute', title: 'Syntropy Institute', version: '1.0.0', description: { short: 'Medicine Reimagined — frequency analysis-therapy', long: '' }, 'release-notes': '', license: '', 'wrapper-repo': '', 'upstream-repo': '', 'support-site': '', 'marketing-site': '', 'donation-url': null },
|
||||||
@ -381,12 +321,7 @@ const packages = computed(() => {
|
|||||||
// Web-only apps first (alphabetically), then all other apps (alphabetically)
|
// Web-only apps first (alphabetically), then all other apps (alphabetically)
|
||||||
const sortedPackageEntries = computed(() => {
|
const sortedPackageEntries = computed(() => {
|
||||||
const entries = Object.entries(packages.value)
|
const entries = Object.entries(packages.value)
|
||||||
// Filter by active tab
|
return entries.sort(([idA, a], [idB, b]) => {
|
||||||
const filtered = entries.filter(([id]) => {
|
|
||||||
const isSvc = isServiceContainer(id)
|
|
||||||
return activeTab.value === 'services' ? isSvc : !isSvc
|
|
||||||
})
|
|
||||||
return filtered.sort(([idA, a], [idB, b]) => {
|
|
||||||
const aWeb = isWebOnlyApp(idA) ? 0 : 1
|
const aWeb = isWebOnlyApp(idA) ? 0 : 1
|
||||||
const bWeb = isWebOnlyApp(idB) ? 0 : 1
|
const bWeb = isWebOnlyApp(idB) ? 0 : 1
|
||||||
if (aWeb !== bWeb) return aWeb - bWeb
|
if (aWeb !== bWeb) return aWeb - bWeb
|
||||||
@ -432,7 +367,59 @@ function canLaunch(pkg: PackageDataEntry): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function launchApp(id: string) {
|
function launchApp(id: string) {
|
||||||
useAppLauncherStore().openSession(id)
|
const isDev = import.meta.env.DEV
|
||||||
|
const pkg = packages.value[id]
|
||||||
|
|
||||||
|
// Web-only apps — use their external URL directly
|
||||||
|
const webOnlyUrl = WEB_ONLY_APP_URLS[id]
|
||||||
|
if (webOnlyUrl) {
|
||||||
|
useAppLauncherStore().open({ url: webOnlyUrl, title: pkg?.manifest?.title || id })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Explicit URLs for apps that need them (checked first to avoid package data issues)
|
||||||
|
const appUrls: Record<string, { dev: string, prod: string }> = {
|
||||||
|
'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
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if (appUrls[id]) {
|
||||||
|
let url = isDev ? appUrls[id].dev : appUrls[id].prod
|
||||||
|
// Replace localhost with current hostname for remote access (not for external IPs like LoraBell)
|
||||||
|
if (url.includes('localhost')) {
|
||||||
|
const currentHost = window.location.hostname
|
||||||
|
url = url.replace('localhost', currentHost)
|
||||||
|
}
|
||||||
|
useAppLauncherStore().open({ url, title: pkg?.manifest?.title || id })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the LAN address from the package
|
||||||
|
let lanAddress = pkg?.installed?.['interface-addresses']?.main?.['lan-address']
|
||||||
|
|
||||||
|
// Replace localhost with the current hostname (for remote access)
|
||||||
|
if (lanAddress && lanAddress.includes('localhost')) {
|
||||||
|
const currentHost = window.location.hostname
|
||||||
|
lanAddress = lanAddress.replace('localhost', currentHost)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lanAddress) {
|
||||||
|
useAppLauncherStore().open({ url: lanAddress, title: pkg?.manifest?.title || id })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// For other apps, navigate to app details which has launch functionality
|
||||||
|
router.push(`/dashboard/apps/${id}`).catch(() => {})
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStatusClass(state: PackageState): string {
|
function getStatusClass(state: PackageState): string {
|
||||||
@ -504,21 +491,15 @@ function showUninstallModal(id: string, pkg: PackageDataEntry) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const uninstalling = ref(false)
|
|
||||||
|
|
||||||
async function confirmUninstall() {
|
async function confirmUninstall() {
|
||||||
const { appId } = uninstallModal.value
|
const { appId } = uninstallModal.value
|
||||||
uninstalling.value = true
|
uninstallModal.value.show = false
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await store.uninstallPackage(appId)
|
await store.uninstallPackage(appId)
|
||||||
uninstallModal.value.show = false
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (import.meta.env.DEV) console.error('Failed to uninstall app:', err)
|
if (import.meta.env.DEV) console.error('Failed to uninstall app:', err)
|
||||||
showActionError(`Failed to uninstall app: ${err instanceof Error ? err.message : 'Unknown error'}`)
|
showActionError(`Failed to uninstall app: ${err instanceof Error ? err.message : 'Unknown error'}`)
|
||||||
uninstallModal.value.show = false
|
|
||||||
} finally {
|
|
||||||
uninstalling.value = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user