fix: resolve did:dht compilation errors
- Simplify DHT encoding: use JSON instead of DNS packets (drop simple-dns) - Fix mainline crate API: SigningKey takes 32 bytes, get_mutable returns Result - Add missing dht_did field to IdentityRecord constructor - Store DID Document as JSON in DHT (DNS encoding deferred) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
419af82c06
commit
0d3ff0d3a4
@@ -82,7 +82,7 @@ define(['./workbox-21a80088'], (function (workbox) { 'use strict';
|
||||
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
||||
}, {
|
||||
"url": "index.html",
|
||||
"revision": "0.infnmokofng"
|
||||
"revision": "0.09ki1c64ohs"
|
||||
}], {});
|
||||
workbox.cleanupOutdatedCaches();
|
||||
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 88 KiB |
@@ -1,95 +0,0 @@
|
||||
<svg width="2480" height="2480" viewBox="0 0 2480 2480" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="2480" height="2480" fill="#0A0A0A"/>
|
||||
<path d="M861 556V944H568V556H861Z" fill="url(#paint0_linear_548_33)" stroke="url(#paint1_linear_548_33)" stroke-width="30"/>
|
||||
<path d="M1912 555V943H1619V555H1912Z" fill="url(#paint2_linear_548_33)" stroke="url(#paint3_linear_548_33)" stroke-width="30"/>
|
||||
<path d="M861 1538V1926H568V1538H861Z" fill="url(#paint4_linear_548_33)" stroke="url(#paint5_linear_548_33)" stroke-width="30"/>
|
||||
<path d="M1449 1538V1926H1031V1538H1449Z" fill="#1D1D1D" stroke="url(#paint6_linear_548_33)" stroke-width="30"/>
|
||||
<path d="M1912 1537V1925H1619V1537H1912Z" fill="url(#paint7_linear_548_33)" stroke="url(#paint8_linear_548_33)" stroke-width="30"/>
|
||||
<path d="M1012.25 1112L1160.05 1368H1030.49L882.688 1112H1012.25Z" fill="url(#paint9_linear_548_33)" stroke="url(#paint10_linear_548_33)" stroke-width="30"/>
|
||||
<path d="M1377.71 1112L1525.51 1368H1395.89L1248.08 1112H1377.71Z" fill="url(#paint11_linear_548_33)" stroke="url(#paint12_linear_548_33)" stroke-width="30"/>
|
||||
<path d="M1912 1112V1368H1761.35L1613.55 1112H1912Z" fill="url(#paint13_linear_548_33)" stroke="url(#paint14_linear_548_33)" stroke-width="30"/>
|
||||
<path d="M646.852 1112L794.653 1368H568V1112H646.852Z" fill="url(#paint15_linear_548_33)" stroke="url(#paint16_linear_548_33)" stroke-width="30"/>
|
||||
<circle cx="1240.5" cy="750.5" r="195.5" fill="#1D1D1D" stroke="url(#paint17_linear_548_33)" stroke-width="30"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_548_33" x1="677.804" y1="242.431" x2="1904.77" y2="353.601" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F0003D"/>
|
||||
<stop offset="0.369792" stop-color="#FA4727"/>
|
||||
<stop offset="0.776042" stop-color="#6B90F4"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_548_33" x1="553" y1="541" x2="876" y2="959" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="#F52532"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_548_33" x1="1728.8" y1="241.431" x2="2955.77" y2="352.601" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F0003D"/>
|
||||
<stop offset="0.369792" stop-color="#FA4727"/>
|
||||
<stop offset="0.776042" stop-color="#6B90F4"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_548_33" x1="1604" y1="540" x2="1927" y2="958" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="#F52532"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint4_linear_548_33" x1="677.804" y1="1224.43" x2="1904.77" y2="1335.6" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F0003D"/>
|
||||
<stop offset="0.369792" stop-color="#FA4727"/>
|
||||
<stop offset="0.776042" stop-color="#6B90F4"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint5_linear_548_33" x1="876" y1="1607.2" x2="553" y2="1856.8" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="#F52532"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint6_linear_548_33" x1="1240" y1="1941" x2="1240" y2="1523" gradientUnits="userSpaceOnUse">
|
||||
<stop/>
|
||||
<stop offset="1" stop-color="#666666"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint7_linear_548_33" x1="1728.8" y1="1223.43" x2="2955.77" y2="1334.6" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F0003D"/>
|
||||
<stop offset="0.369792" stop-color="#FA4727"/>
|
||||
<stop offset="0.776042" stop-color="#6B90F4"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint8_linear_548_33" x1="1927" y1="1606.2" x2="1604" y2="1855.8" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="#F52532"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint9_linear_548_33" x1="350.36" y1="1032.44" x2="1729.93" y2="1206.26" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F0003D"/>
|
||||
<stop offset="0.369792" stop-color="#FA4727"/>
|
||||
<stop offset="0.776042" stop-color="#6B90F4"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint10_linear_548_33" x1="856.707" y1="1097" x2="1115.61" y2="1436.97" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FAFAFA"/>
|
||||
<stop offset="1" stop-color="#A5729F"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint11_linear_548_33" x1="715.651" y1="1032.44" x2="2095.49" y2="1206.34" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F0003D"/>
|
||||
<stop offset="0.369792" stop-color="#FA4727"/>
|
||||
<stop offset="0.776042" stop-color="#6B90F4"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint12_linear_548_33" x1="1222.1" y1="1097" x2="1480.99" y2="1437.02" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FAFAFA"/>
|
||||
<stop offset="1" stop-color="#A5729F"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint13_linear_548_33" x1="1065.68" y1="1032.44" x2="2486.21" y2="1216.92" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F0003D"/>
|
||||
<stop offset="0.369792" stop-color="#FA4727"/>
|
||||
<stop offset="0.776042" stop-color="#6B90F4"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint14_linear_548_33" x1="1587.57" y1="1097" x2="1844.29" y2="1444.46" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FAFAFA"/>
|
||||
<stop offset="1" stop-color="#A5729F"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint15_linear_548_33" x1="141.502" y1="1032.44" x2="1268.63" y2="1147.85" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F0003D"/>
|
||||
<stop offset="0.369792" stop-color="#FA4727"/>
|
||||
<stop offset="0.776042" stop-color="#6B90F4"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint16_linear_548_33" x1="553" y1="1097" x2="821" y2="1383" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FAFAFA"/>
|
||||
<stop offset="1" stop-color="#A5729F"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint17_linear_548_33" x1="1240.5" y1="540" x2="1240.5" y2="961" gradientUnits="userSpaceOnUse">
|
||||
<stop/>
|
||||
<stop offset="1" stop-color="#666666"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 5.5 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 455 KiB |
@@ -267,7 +267,15 @@ watch([showWelcome, showLogo], ([welcome, logo]) => {
|
||||
})
|
||||
|
||||
// Check if user has seen intro
|
||||
const seenIntro = localStorage.getItem('neode_intro_seen') === '1'
|
||||
// Also detect returning users who cleared cache: if we're on a /dashboard route,
|
||||
// the backend session is still active so the user has been here before.
|
||||
const storedSeenIntro = localStorage.getItem('neode_intro_seen') === '1'
|
||||
const isOnDashboard = window.location.pathname.startsWith('/dashboard')
|
||||
const seenIntro = storedSeenIntro || isOnDashboard
|
||||
// Persist the flag so subsequent loads don't re-check
|
||||
if (!storedSeenIntro && isOnDashboard) {
|
||||
localStorage.setItem('neode_intro_seen', '1')
|
||||
}
|
||||
|
||||
function onIntroLogoHover() {
|
||||
introLogoHover.value = true
|
||||
|
||||
@@ -322,6 +322,7 @@
|
||||
"subtitle": "Discover and install apps for your new sovereign life",
|
||||
"curatedTab": "Curated",
|
||||
"communityTab": "Community",
|
||||
"nostrCommunityTab": "Nostr Community",
|
||||
"filterByCategory": "Filter by Category",
|
||||
"searchPlaceholder": "Search apps...",
|
||||
"downloading": "Downloading...",
|
||||
@@ -376,6 +377,7 @@
|
||||
"manageDomains": "Manage Domains",
|
||||
"relaysConnected": "{count} connected",
|
||||
"peersKnown": "{count} peer(s) known",
|
||||
"findNodes": "Find Nodes",
|
||||
"sendMessage": "Send Message",
|
||||
"sendMessageTitle": "Send Message (over Tor)",
|
||||
"to": "To",
|
||||
|
||||
@@ -2,23 +2,17 @@ import { defineStore } from 'pinia'
|
||||
import { ref, watch } from 'vue'
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
|
||||
/** Apps that set X-Frame-Options or CSP frame-ancestors, blocking iframe embedding.
|
||||
* Verified by checking response headers from each app container.
|
||||
* These always open in a new tab. Other apps load in the iframe overlay.
|
||||
*/
|
||||
/** Hostnames of external sites that block iframes via X-Frame-Options or CSP.
|
||||
* Sites listed here that also appear in EXTERNAL_PROXY will be proxied (not blocked).
|
||||
*/
|
||||
const IFRAME_BLOCKED_HOSTS: string[] = []
|
||||
* These always open in a new tab. Other external sites load directly in the iframe. */
|
||||
const IFRAME_BLOCKED_HOSTS: string[] = [
|
||||
'484.kitchen',
|
||||
'botfights.net',
|
||||
'present.l484.com',
|
||||
]
|
||||
|
||||
/** External sites proxied through nginx path-based locations (strips X-Frame-Options).
|
||||
* Uses /ext/{key}/ paths on the main nginx port so it works over Tailscale too. */
|
||||
const EXTERNAL_PROXY_PATH: Record<string, string> = {
|
||||
'botfights.net': '/ext/botfights/',
|
||||
'484.kitchen': '/ext/484-kitchen/',
|
||||
'present.l484.com': '/ext/arch-presentation/',
|
||||
'nostrudel.ninja': '/ext/nostrudel/',
|
||||
}
|
||||
/** External site proxy paths — disabled. External URLs load directly in the iframe
|
||||
* via their standard https:// URL. The /ext/ subpath approach broke SPAs. */
|
||||
const EXTERNAL_PROXY_PATH: Record<string, string> = {}
|
||||
|
||||
function mustOpenInNewTab(url: string): boolean {
|
||||
try {
|
||||
|
||||
@@ -812,13 +812,6 @@ function launchApp() {
|
||||
'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]) {
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
<template>
|
||||
<div class="pb-6">
|
||||
<div class="hidden md:block mb-8">
|
||||
<h1 class="text-3xl font-bold text-white mb-2">{{ t('apps.title') }}</h1>
|
||||
<p class="text-white/70">{{ t('apps.subtitle') }}</p>
|
||||
<div class="hidden md:flex items-start justify-between mb-8 gap-4">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-white mb-2">{{ t('apps.title') }}</h1>
|
||||
<p class="text-white/70">{{ t('apps.subtitle') }}</p>
|
||||
</div>
|
||||
<div class="mode-switcher flex-shrink-0">
|
||||
<RouterLink to="/dashboard/apps" class="mode-switcher-btn mode-switcher-btn-active">My Apps</RouterLink>
|
||||
<RouterLink to="/dashboard/marketplace" class="mode-switcher-btn">App Store</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search Bar -->
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="chat-fullscreen">
|
||||
<!-- Close button + connection indicator (desktop: top-right pill) -->
|
||||
<!-- Close button (desktop: top-right pill) -->
|
||||
<div class="chat-mode-pill flex">
|
||||
<button class="chat-close-btn" :aria-label="t('chat.closeAssistant')" @click="closeChat">
|
||||
<svg class="w-4 h-4" aria-hidden="true" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -8,11 +8,6 @@
|
||||
</svg>
|
||||
<span class="text-xs font-medium">{{ t('chat.close') }}</span>
|
||||
</button>
|
||||
<div
|
||||
v-if="aiuiConnected"
|
||||
class="w-2 h-2 rounded-full bg-green-400 ml-2 shadow-[0_0_6px_rgba(74,222,128,0.5)]"
|
||||
:title="t('chat.aiuiConnected')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Loading indicator while checking availability -->
|
||||
@@ -68,7 +63,6 @@ const { t } = useI18n()
|
||||
|
||||
const router = useRouter()
|
||||
const aiuiFrame = ref<HTMLIFrameElement | null>(null)
|
||||
const aiuiConnected = ref(false)
|
||||
let broker: ContextBroker | null = null
|
||||
|
||||
const aiuiAvailable = ref<boolean | null>(null) // null = checking, true/false = result
|
||||
@@ -115,9 +109,9 @@ function onAiuiMessage(event: MessageEvent) {
|
||||
const expected = new URL(aiuiUrl.value, window.location.origin).origin
|
||||
if (event.origin !== expected) return
|
||||
} catch { return }
|
||||
const msg = event.data
|
||||
if (msg && msg.type === 'ready') {
|
||||
aiuiConnected.value = true
|
||||
// Listen for ready messages from AIUI iframe
|
||||
if (event.data?.type === 'ready') {
|
||||
// AIUI connected - could use for future features
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,35 +56,42 @@
|
||||
<span v-else-if="sectionCounts[section.id] !== undefined" class="text-white/30">{{ sectionCounts[section.id] }} items</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Peer Files Card -->
|
||||
<div
|
||||
v-if="hasFederatedPeers"
|
||||
data-controller-container
|
||||
tabindex="0"
|
||||
class="glass-card p-6 cursor-pointer transition-all hover:-translate-y-1 hover:bg-white/10 mt-4"
|
||||
@click="router.push({ name: 'peer-files' })"
|
||||
>
|
||||
<div class="flex items-center gap-4 mb-4">
|
||||
<div class="flex-shrink-0 w-12 h-12 rounded-xl flex items-center justify-center bg-purple-500/15">
|
||||
<svg class="w-7 h-7 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />
|
||||
<!-- Peer Files Card -->
|
||||
<div
|
||||
data-controller-container
|
||||
tabindex="0"
|
||||
class="glass-card p-6 cursor-pointer transition-all hover:-translate-y-1 hover:bg-white/10"
|
||||
@click="router.push({ name: 'peer-files' })"
|
||||
>
|
||||
<div class="flex items-center gap-4 mb-4">
|
||||
<div class="flex-shrink-0 w-12 h-12 rounded-xl flex items-center justify-center bg-purple-500/15">
|
||||
<svg class="w-7 h-7 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="text-lg font-semibold text-white mb-0.5 truncate">Peer Files</h3>
|
||||
<p class="text-xs text-white/50">Browse files shared by federated nodes</p>
|
||||
</div>
|
||||
<svg class="w-5 h-5 text-white/30" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="text-lg font-semibold text-white mb-0.5 truncate">Peer Files</h3>
|
||||
<p class="text-xs text-white/50">Browse files shared by federated nodes</p>
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<template v-if="hasFederatedPeers">
|
||||
<span class="inline-flex items-center gap-1.5 px-2 py-1 rounded-full bg-purple-500/15 text-purple-400">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-purple-400"></span>
|
||||
{{ peerCount }} peers
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="inline-flex items-center gap-1.5 px-2 py-1 rounded-full bg-white/5 text-white/40">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-white/30"></span>
|
||||
No peers yet
|
||||
</span>
|
||||
<span class="text-white/30">Set up federation to share files</span>
|
||||
</template>
|
||||
</div>
|
||||
<svg class="w-5 h-5 text-white/30" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<span class="inline-flex items-center gap-1.5 px-2 py-1 rounded-full bg-purple-500/15 text-purple-400">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-purple-400"></span>
|
||||
{{ peerCount }} peers
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -82,7 +82,8 @@
|
||||
:key="item.path"
|
||||
:to="item.path"
|
||||
class="sidebar-nav-item flex items-center gap-3 px-4 py-3 rounded-lg text-white/80 hover:bg-white/10 hover:text-white transition-colors"
|
||||
exact-active-class="nav-tab-active"
|
||||
:class="{ 'nav-tab-active': item.isCombined && (route.path.includes('/apps') || route.path.includes('/marketplace')) }"
|
||||
:exact-active-class="item.isCombined ? undefined : 'nav-tab-active'"
|
||||
:style="{ '--nav-stagger': idx }"
|
||||
>
|
||||
<svg class="w-5 h-5" aria-hidden="true" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -278,8 +279,8 @@
|
||||
:class="[
|
||||
'px-4 pt-4 md:pt-8 md:px-8 overflow-y-auto h-full',
|
||||
needsMobileBackButtonSpace
|
||||
? 'pb-[calc(var(--mobile-tab-bar-height,_72px)+96px)] md:pb-8'
|
||||
: 'pb-4 md:pb-8'
|
||||
? 'pb-[calc(var(--mobile-tab-bar-height,_72px)+96px)] md:pb-24'
|
||||
: 'pb-[calc(var(--mobile-tab-bar-height,_72px)+48px)] md:pb-24'
|
||||
]"
|
||||
:style="mobileTabPaddingTop ? { paddingTop: (mobileTabPaddingTop + 16) + 'px' } : undefined"
|
||||
>
|
||||
@@ -649,8 +650,7 @@ interface NavItem {
|
||||
|
||||
const gamerDesktopNav: NavItem[] = [
|
||||
{ path: '/dashboard', label: 'Home', icon: 'home' },
|
||||
{ path: '/dashboard/apps', label: 'My Apps', icon: 'apps' },
|
||||
{ path: '/dashboard/marketplace', label: 'App Store', icon: 'marketplace' },
|
||||
{ path: '/dashboard/apps', label: 'Apps', icon: 'apps', isCombined: true },
|
||||
{ path: '/dashboard/cloud', label: 'Cloud', icon: 'cloud' },
|
||||
{ path: '/dashboard/server', label: 'Network', icon: 'server' },
|
||||
{ path: '/dashboard/web5', label: 'Web5', icon: 'web5' },
|
||||
@@ -869,15 +869,28 @@ function getTransitionName(currentRoute: RouteLocationNormalizedLoaded) {
|
||||
return transitionName
|
||||
}
|
||||
|
||||
// Health notifications from WebSocket data
|
||||
// Health notifications from WebSocket data — deduplicated by container name
|
||||
const dismissedNotifications = ref<Set<string>>(new Set())
|
||||
|
||||
const healthNotifications = computed(() => {
|
||||
const notifs = store.data?.notifications ?? []
|
||||
return notifs.filter(n => !dismissedNotifications.value.has(n.id)).slice(-5)
|
||||
const visible = notifs.filter(n => !dismissedNotifications.value.has(n.id))
|
||||
// Deduplicate: keep only the latest notification per container/title
|
||||
const seen = new Map<string, typeof visible[0]>()
|
||||
for (const n of visible) {
|
||||
seen.set(n.title, n)
|
||||
}
|
||||
return [...seen.values()].slice(-3)
|
||||
})
|
||||
|
||||
function dismissNotification(id: string) {
|
||||
// Dismiss all notifications with the same title (container name)
|
||||
const notif = (store.data?.notifications ?? []).find(n => n.id === id)
|
||||
if (notif) {
|
||||
for (const n of store.data?.notifications ?? []) {
|
||||
if (n.title === notif.title) dismissedNotifications.value.add(n.id)
|
||||
}
|
||||
}
|
||||
dismissedNotifications.value.add(id)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="hidden md:block mb-8">
|
||||
<div class="mb-8">
|
||||
<button
|
||||
@click="router.push('/dashboard/web5')"
|
||||
class="flex items-center gap-2 text-white/50 hover:text-white/80 transition-colors text-sm mb-4"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
Back to Web5
|
||||
</button>
|
||||
<h1 class="text-3xl font-bold text-white mb-2">Federation</h1>
|
||||
<p class="text-white/70">Manage trusted node clusters and sync state across your network</p>
|
||||
<p class="text-sm text-white/60 mt-2">{{ nodes.length }} federated node{{ nodes.length !== 1 ? 's' : '' }}</p>
|
||||
@@ -357,9 +366,12 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
import NetworkMap from '@/components/federation/NetworkMap.vue'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
interface AppStatus {
|
||||
id: string
|
||||
status: string
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
<!-- Info Grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||
<!-- Server Name Card -->
|
||||
<!-- Server Name Card (editable) -->
|
||||
<div class="bg-black/20 rounded-xl px-5 py-4 border border-white/10">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<svg class="w-5 h-5 text-white/70" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -25,7 +25,31 @@
|
||||
</svg>
|
||||
<p class="text-xs font-semibold text-white/60 uppercase tracking-wide">{{ t('settings.serverName') }}</p>
|
||||
</div>
|
||||
<p class="text-lg font-semibold text-white/95">{{ serverName }}</p>
|
||||
<div v-if="editingServerName" class="flex items-center gap-2">
|
||||
<input
|
||||
ref="serverNameInput"
|
||||
v-model="serverNameDraft"
|
||||
type="text"
|
||||
maxlength="64"
|
||||
class="flex-1 px-3 py-1.5 bg-white/10 border border-white/20 rounded-lg text-white text-lg font-semibold focus:outline-none focus:border-white/40 transition-colors"
|
||||
@keydown.enter="saveServerName"
|
||||
@keydown.escape="editingServerName = false"
|
||||
/>
|
||||
<button
|
||||
class="px-3 py-1.5 bg-white/10 border border-white/20 rounded-lg text-white/70 hover:text-white hover:bg-white/15 transition-colors text-sm"
|
||||
@click="saveServerName"
|
||||
>Save</button>
|
||||
<button
|
||||
class="px-3 py-1.5 text-white/50 hover:text-white/70 transition-colors text-sm"
|
||||
@click="editingServerName = false"
|
||||
>Cancel</button>
|
||||
</div>
|
||||
<div v-else class="flex items-center gap-2 group cursor-pointer" @click="startEditServerName">
|
||||
<p class="text-lg font-semibold text-white/95">{{ serverName }}</p>
|
||||
<svg class="w-4 h-4 text-white/30 group-hover:text-white/60 transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Version Card -->
|
||||
@@ -572,51 +596,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tor Services Section -->
|
||||
<div class="glass-card px-6 py-6 mb-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold text-white/96">Tor Services</h2>
|
||||
<p class="text-sm text-white/60 mt-1">Manage hidden service addresses for your node and apps</p>
|
||||
</div>
|
||||
<button @click="loadTorServices" class="glass-button px-4 py-2 rounded-lg text-sm flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="torLoading" class="text-sm text-white/40 py-4 text-center">Loading Tor services...</div>
|
||||
<div v-else-if="torServices.length === 0" class="text-sm text-white/40 py-4 text-center">No Tor services configured</div>
|
||||
<div v-else class="space-y-2">
|
||||
<div v-for="svc in torServices" :key="svc.name" class="bg-black/20 rounded-xl border border-white/10 p-3 flex items-center justify-between gap-3">
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-white text-sm font-medium">{{ svc.name }}</p>
|
||||
<p v-if="svc.onion_address" class="text-amber-300/80 text-xs font-mono truncate">{{ svc.onion_address }}</p>
|
||||
<p v-else class="text-white/30 text-xs">No .onion address</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 shrink-0">
|
||||
<button
|
||||
v-if="svc.name === 'archipelago'"
|
||||
@click="rotateNodeAddress"
|
||||
:disabled="torRotating"
|
||||
class="glass-button px-3 py-1.5 rounded-lg text-xs"
|
||||
>
|
||||
{{ torRotating ? 'Rotating...' : 'Rotate' }}
|
||||
</button>
|
||||
<label class="tor-toggle-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="svc.enabled"
|
||||
@change="toggleTorApp(svc.name, !svc.enabled)"
|
||||
class="tor-toggle-input"
|
||||
/>
|
||||
<span class="tor-toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Webhook Notifications Section -->
|
||||
<div class="glass-card px-6 py-6 mb-6">
|
||||
@@ -836,7 +815,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, onMounted } from 'vue'
|
||||
import { computed, ref, onMounted, nextTick } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAppStore } from '../stores/app'
|
||||
@@ -887,6 +866,30 @@ const interfaceModes = computed<{ id: UIMode; label: string; description: string
|
||||
])
|
||||
|
||||
const serverName = computed(() => store.serverName)
|
||||
const editingServerName = ref(false)
|
||||
const serverNameDraft = ref('')
|
||||
const serverNameInput = ref<HTMLInputElement | null>(null)
|
||||
|
||||
function startEditServerName() {
|
||||
serverNameDraft.value = serverName.value
|
||||
editingServerName.value = true
|
||||
nextTick(() => serverNameInput.value?.select())
|
||||
}
|
||||
|
||||
async function saveServerName() {
|
||||
const name = serverNameDraft.value.trim()
|
||||
if (!name || name === serverName.value) {
|
||||
editingServerName.value = false
|
||||
return
|
||||
}
|
||||
try {
|
||||
await rpcClient.call({ method: 'server.set-name', params: { name } })
|
||||
} catch (e) {
|
||||
console.error('Failed to rename server:', e)
|
||||
}
|
||||
editingServerName.value = false
|
||||
}
|
||||
|
||||
const version = computed(() => store.serverInfo?.version || '0.0.0')
|
||||
const serverTorAddressFromStore = computed(() => store.serverInfo?.['tor-address'] || null)
|
||||
const torAddressFromRpc = ref<string | null>(null)
|
||||
@@ -1166,7 +1169,6 @@ onMounted(async () => {
|
||||
checkClaudeStatus()
|
||||
loadTotpStatus()
|
||||
loadBackups()
|
||||
loadTorServices()
|
||||
loadWebhookConfig()
|
||||
if (!serverTorAddressFromStore.value) {
|
||||
try {
|
||||
@@ -1204,16 +1206,6 @@ const restoringBackup = ref(false)
|
||||
const verifyingBackupId = ref<string | null>(null)
|
||||
const deletingBackupId = ref<string | null>(null)
|
||||
|
||||
// Tor services state
|
||||
interface TorServiceInfo {
|
||||
name: string
|
||||
local_port: number
|
||||
onion_address: string | null
|
||||
enabled: boolean
|
||||
}
|
||||
const torServices = ref<TorServiceInfo[]>([])
|
||||
const torLoading = ref(false)
|
||||
const torRotating = ref(false)
|
||||
const backupStatusMsg = ref('')
|
||||
const backupStatusType = ref<'success' | 'error'>('success')
|
||||
|
||||
@@ -1230,43 +1222,6 @@ function showBackupStatus(msg: string, type: 'success' | 'error') {
|
||||
setTimeout(() => { backupStatusMsg.value = '' }, 5000)
|
||||
}
|
||||
|
||||
async function loadTorServices() {
|
||||
torLoading.value = true
|
||||
try {
|
||||
const res = await rpcClient.torListServices()
|
||||
torServices.value = res.services || []
|
||||
} catch {
|
||||
torServices.value = []
|
||||
} finally {
|
||||
torLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleTorApp(appId: string, enabled: boolean) {
|
||||
try {
|
||||
const res = await rpcClient.torToggleApp(appId, enabled)
|
||||
if (res.changed) {
|
||||
await loadTorServices()
|
||||
}
|
||||
} catch (e) {
|
||||
if (import.meta.env.DEV) console.warn('Failed to toggle Tor app:', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function rotateNodeAddress() {
|
||||
if (torRotating.value) return
|
||||
if (!confirm('This will generate a new .onion address. The old address will work for 24 hours during transition. Federated peers will be notified automatically.')) return
|
||||
torRotating.value = true
|
||||
try {
|
||||
await rpcClient.torRotateService('archipelago')
|
||||
await loadTorServices()
|
||||
} catch (e) {
|
||||
if (import.meta.env.DEV) console.warn('Failed to rotate Tor address:', e)
|
||||
} finally {
|
||||
torRotating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadBackups() {
|
||||
loadingBackups.value = true
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user