archy/neode-ui/src/views/settings/AccountInfoSection.vue
Dorian 901b9f660f feat: gamepad navigation rewrite, focus styling, container grid system
- Rewrite useControllerNav.ts with clean console-style navigation:
  Sidebar (up/down wrap, right→containers, left→nothing),
  Container tile grid (spatial nav, no wrap at edges),
  Nav bar support (up from containers, down to grid),
  Inner controls (enter drills in, escape exits, trapped arrows)
- Add data-controller-container to Mesh, Fleet, Settings pages
- Fix Home.vue fragment (modals outside root div) causing Vue warnings
- Remove skip-to-content link (handled by controller nav)
- Orange ambient glow focus styling matching glass aesthetic
- Disable PWA service worker in dev mode (fixes HMR caching)
- Add gamepad-nav skill and GAMEPAD-NAV-MAP.md spec document
- 39 tests covering all navigation patterns

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 17:01:17 +00:00

371 lines
20 KiB
Vue

<script setup lang="ts">
import { computed, ref, nextTick } from 'vue'
import { useI18n } from 'vue-i18n'
import { useAppStore } from '@/stores/app'
import ControllerIndicator from '@/components/ControllerIndicator.vue'
import { rpcClient } from '@/api/rpc-client'
const { t } = useI18n()
const store = useAppStore()
// Server name
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 } })
store.updateServerName(name)
} catch (e) {
if (import.meta.env.DEV) console.error('Failed to rename server:', e)
}
editingServerName.value = false
}
// Version & release notes
const version = computed(() => store.serverInfo?.version || '0.0.0')
const showReleaseNotes = ref(false)
// Identity
const serverTorAddressFromStore = computed(() => store.serverInfo?.['tor-address'] || null)
const torAddressFromRpc = ref<string | null>(null)
const serverTorAddress = computed(() => serverTorAddressFromStore.value || torAddressFromRpc.value)
const userDid = computed(() => {
try {
return localStorage.getItem('neode_did') || null
} catch {
return null
}
})
const copiedOnion = ref(false)
const copiedDid = ref(false)
let copiedTimer: ReturnType<typeof setTimeout> | null = null
async function copyOnionAddress() {
const addr = serverTorAddress.value
if (!addr) return
try {
await navigator.clipboard.writeText(addr)
} catch {
const ta = document.createElement('textarea')
ta.value = addr
ta.style.position = 'fixed'
ta.style.opacity = '0'
document.body.appendChild(ta)
ta.select()
document.execCommand('copy')
document.body.removeChild(ta)
}
copiedOnion.value = true
if (copiedTimer) clearTimeout(copiedTimer)
copiedTimer = setTimeout(() => { copiedOnion.value = false }, 2000)
}
async function copyDid() {
if (!userDid.value) return
try {
await navigator.clipboard.writeText(userDid.value)
} catch {
const ta = document.createElement('textarea')
ta.value = userDid.value
ta.style.position = 'fixed'
ta.style.opacity = '0'
document.body.appendChild(ta)
ta.select()
document.execCommand('copy')
document.body.removeChild(ta)
}
copiedDid.value = true
setTimeout(() => { copiedDid.value = false }, 2000)
}
// Load Tor address on mount if not in store
async function init() {
if (!serverTorAddressFromStore.value) {
try {
const res = await rpcClient.getTorAddress()
torAddressFromRpc.value = res.tor_address ?? null
} catch (e) {
if (import.meta.env.DEV) console.warn('Tor address may not be available yet', e)
}
}
}
init()
</script>
<template>
<!-- Controller indicator - Mobile only -->
<div class="md:hidden mb-4">
<ControllerIndicator />
</div>
<!-- Info Grid -->
<div data-controller-container tabindex="0" class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<!-- 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">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01" />
</svg>
<p class="text-xs font-semibold text-white/60 uppercase tracking-wide">{{ t('settings.serverName') }}</p>
</div>
<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 -->
<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">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
</svg>
<p class="text-xs font-semibold text-white/60 uppercase tracking-wide">{{ t('common.version') }}</p>
</div>
<div class="flex items-center justify-between">
<p class="text-lg font-semibold text-white/95">{{ version }}</p>
<button
@click="showReleaseNotes = true"
class="glass-button px-3 py-1.5 text-xs"
>What's New</button>
</div>
</div>
<!-- Release Notes Modal -->
<Teleport to="body">
<Transition name="modal">
<div v-if="showReleaseNotes" class="fixed inset-0 z-[3000] flex items-center justify-center p-4" @click="showReleaseNotes = false">
<div class="absolute inset-0 bg-black/60 backdrop-blur-sm"></div>
<div @click.stop class="glass-card p-6 max-w-lg w-full relative z-10 flex flex-col" style="max-height: 85vh">
<div class="flex items-start justify-between gap-4 mb-5 shrink-0">
<h3 class="text-xl font-semibold text-white">What's New</h3>
<button @click="showReleaseNotes = false" class="p-2 rounded-lg hover:bg-white/10 text-white/70 hover:text-white transition-colors" aria-label="Close">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /></svg>
</button>
</div>
<div class="overflow-y-auto flex-1 min-h-0 space-y-6 pr-1">
<!-- v1.3.0 -->
<div>
<div class="flex items-center gap-2 mb-3">
<span class="text-xs font-mono px-2 py-0.5 rounded bg-orange-500/20 text-orange-300">v1.3.0</span>
<span class="text-xs text-white/40">Mar 19, 2026</span>
</div>
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
<div>
<h4 class="text-white font-medium mb-1">Full Security Audit</h4>
<p>33 security findings from a comprehensive penetration test — all fixed. Backend now only accessible through nginx. Path traversal, SSRF, and XSS vulnerabilities eliminated. Federation requires cryptographic signatures. Session tokens rotate after 2FA. Destructive operations now require password confirmation.</p>
</div>
<div>
<h4 class="text-white font-medium mb-1">Container Reliability</h4>
<p>Memory limits on every container prevent one app from crashing the whole system. Crashed apps now show a red "crashed" badge with a restart button instead of disappearing. Smart health status shows "starting up", "healthy", or "unhealthy" in real time. Apps you stop stay stopped — no more auto-restart fighting.</p>
</div>
<div>
<h4 class="text-white font-medium mb-1">Wallet on Home</h4>
<p>The Home dashboard now shows your Bitcoin wallet with on-chain, Lightning, and ecash balances. Send, receive, and view transaction history right from the home screen. New Transactions modal shows your full history with confirmations.</p>
</div>
<div>
<h4 class="text-white font-medium mb-1">LND Connect Fixed</h4>
<p>Connect Your Wallet (Zeus, Zap, BlueWallet) now works over both local network and Tor. QR codes generate correctly with REST API access.</p>
</div>
<div>
<h4 class="text-white font-medium mb-1">UI Polish</h4>
<p>Mesh view redesigned. New glass button styles throughout. Restart button on running apps. Improved app status badges. Cleaner navigation on the Apps page.</p>
</div>
</div>
</div>
<!-- alpha.9 -->
<div>
<div class="flex items-center gap-2 mb-3">
<span class="text-xs font-mono px-2 py-0.5 rounded bg-white/10 text-white/60">v1.2.0-alpha.9</span>
<span class="text-xs text-white/40">Mar 18, 2026</span>
</div>
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
<div>
<h4 class="text-white font-medium mb-1">Security Hardening Complete</h4>
<p>All 12 pentest findings fixed. CSRF tokens now survive restarts. Password hashing upgraded to Argon2id. Bitcoin RPC gets a unique random password on every install. Federation messages require ed25519 signatures.</p>
</div>
<div>
<h4 class="text-white font-medium mb-1">7 Bugs Squashed</h4>
<p>Random logouts fixed (P0). Uninstall dialog is now a proper full-screen modal with an "Uninstalling..." overlay. App cards no longer flicker between Start/Launch during container scans. ElectrumX index estimate corrected.</p>
</div>
<div>
<h4 class="text-white font-medium mb-1">Bitcoin Sync on Dashboard</h4>
<p>Homepage System card now shows Bitcoin Core sync progress, block height, and green/orange status indicator when Bitcoin is running.</p>
</div>
</div>
</div>
<!-- alpha.8 -->
<div>
<div class="flex items-center gap-2 mb-3">
<span class="text-xs font-mono px-2 py-0.5 rounded bg-white/10 text-white/60">v1.2.0-alpha.8</span>
<span class="text-xs text-white/40">Mar 18, 2026</span>
</div>
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
<div>
<h4 class="text-white font-medium mb-1">Pentest Remediation (9/12)</h4>
<p>Fixed 9 of 12 security findings: session auth on LND connect info, DEV_MODE removed from production, ed25519 signature verification on node messages, path traversal protection, NIP-07 origin validation, AIUI session checks, strict onion validation.</p>
</div>
<div>
<h4 class="text-white font-medium mb-1">UI Polish Batch</h4>
<p>Fedimint renamed to "Fedimint Guardian". Tab-launch icons. Marketplace sorts installed apps to end. Mesh mobile layout fixed. On-Chain first in receive modals. Federation shows names instead of DIDs. Cleaner iframe error screens.</p>
</div>
</div>
</div>
<!-- alpha.7 -->
<div>
<div class="flex items-center gap-2 mb-3">
<span class="text-xs font-mono px-2 py-0.5 rounded bg-white/10 text-white/60">v1.2.0-alpha.7</span>
<span class="text-xs text-white/40">Mar 18, 2026</span>
</div>
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
<div>
<h4 class="text-white font-medium mb-1">Marketplace & Credentials</h4>
<p>29 containers running rootless. Marketplace app aliases working. Credential injection for inter-container authentication.</p>
</div>
</div>
</div>
<!-- alpha.4-6 -->
<div>
<div class="flex items-center gap-2 mb-3">
<span class="text-xs font-mono px-2 py-0.5 rounded bg-white/10 text-white/60">v1.2.0-alpha.4-6</span>
<span class="text-xs text-white/40">Mar 18, 2026</span>
</div>
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
<div>
<h4 class="text-white font-medium mb-1">Rootless Podman Migration</h4>
<p>Migrated all containers from root to rootless Podman. UID namespace mapping, volume ownership fixes, sysctl tuning. Bitcoin RPC verified, all web services confirmed healthy. 29 containers up and running.</p>
</div>
</div>
</div>
<!-- alpha.2-3 -->
<div>
<div class="flex items-center gap-2 mb-3">
<span class="text-xs font-mono px-2 py-0.5 rounded bg-white/10 text-white/60">v1.2.0-alpha.2-3</span>
<span class="text-xs text-white/40">Mar 18, 2026</span>
</div>
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
<div>
<h4 class="text-white font-medium mb-1">Systemd Hardening Restored</h4>
<p>Full systemd security sandbox restored now that containers run rootless. NoNewPrivileges, restricted namespaces, and system call filtering re-enabled. Session persistence and boot sequence fixes.</p>
</div>
</div>
</div>
<!-- alpha.1 -->
<div>
<div class="flex items-center gap-2 mb-3">
<span class="text-xs font-mono px-2 py-0.5 rounded bg-white/10 text-white/60">v1.2.0-alpha.1</span>
<span class="text-xs text-white/40">Mar 18, 2026</span>
</div>
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
<div>
<h4 class="text-white font-medium mb-1">Mesh Radio & Container Stability</h4>
<p>LoRa mesh radio auto-detects USB port changes with a new Connect button. Fixed container crash loops — all apps start cleanly and stay stable. Apps starting up show progress instead of re-appearing in the store. Tor routing enabled by default for Bitcoin and Lightning.</p>
</div>
<div>
<h4 class="text-white font-medium mb-1">Off-Grid Bitcoin</h4>
<p>Receive Bitcoin block headers over mesh radio. Dead man's switch broadcasts location to trusted contacts if you go silent. GPS sharing is opt-in only.</p>
</div>
</div>
</div>
</div>
<button @click="showReleaseNotes = false" class="glass-button w-full mt-4 py-2 text-sm shrink-0">Close</button>
</div>
</div>
</Transition>
</Teleport>
<!-- Session Card -->
<div class="bg-black/20 rounded-xl px-5 py-4 border border-white/10 md:col-span-2">
<div class="flex items-center gap-3 mb-2">
<svg class="w-5 h-5 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<p class="text-xs font-semibold text-white/60 uppercase tracking-wide">{{ t('settings.sessionStatus') }}</p>
</div>
<p class="text-base font-medium text-white/90">{{ t('settings.loggedIn') }}</p>
</div>
<!-- Identity Card: DID + Tor Address -->
<div v-if="userDid || serverTorAddress" class="bg-black/20 rounded-xl px-5 py-4 border border-white/10 md:col-span-2 space-y-4">
<div v-if="userDid">
<div class="flex items-center justify-between gap-2 mb-2">
<div class="flex items-center gap-3 min-w-0">
<svg class="w-5 h-5 text-amber-400 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
</svg>
<p class="text-xs font-semibold text-white/60 uppercase tracking-wide">{{ t('settings.yourDid') }}</p>
</div>
<button
@click="copyDid"
class="shrink-0 px-3 py-1.5 rounded-lg glass-button glass-button-sm text-xs font-medium text-white/90 hover:text-white transition-colors flex items-center gap-1.5"
>
<svg v-if="!copiedDid" 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="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
<span v-else class="text-green-400 text-xs">{{ t('common.copied') }}</span>
<span v-if="!copiedDid">{{ t('common.copy') }}</span>
</button>
</div>
<p class="text-sm font-mono text-white/90 break-all" :title="userDid">{{ userDid }}</p>
<p class="text-xs text-white/50 mt-1">{{ t('settings.didHelper') }}</p>
</div>
<div v-if="serverTorAddress" :class="userDid ? 'pt-4 border-t border-white/10' : ''">
<div class="flex items-center gap-3 mb-2">
<svg class="w-5 h-5 text-amber-400 shrink-0" 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>
<p class="text-xs font-semibold text-white/60 uppercase tracking-wide">{{ t('settings.onionAddress') }}</p>
</div>
<p class="text-sm font-mono text-amber-400/90 break-all mb-1" :title="serverTorAddress">{{ serverTorAddress }}</p>
<p class="text-xs text-white/50 mb-3">{{ t('settings.onionHelper') }}</p>
<button
@click="copyOnionAddress"
class="w-full min-h-[44px] rounded-lg glass-button text-sm font-medium text-white/90 hover:text-white transition-colors flex items-center justify-center gap-2"
>
<svg v-if="!copiedOnion" 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="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
<span v-if="!copiedOnion">{{ t('common.copy') }}</span>
<span v-else class="text-green-400">{{ t('common.copied') }}</span>
</button>
</div>
</div>
</div>
</template>