2026-03-11 13:45:59 +00:00
|
|
|
<template>
|
|
|
|
|
<div class="min-h-screen bg-black flex items-center justify-center p-6">
|
|
|
|
|
<div class="glass-card p-8 w-full max-w-lg">
|
|
|
|
|
<div class="text-center mb-6">
|
|
|
|
|
<h1 class="text-2xl font-bold text-white mb-1">{{ t('kioskRecovery.title') }}</h1>
|
|
|
|
|
<p class="text-sm text-white/50">{{ t('kioskRecovery.subtitle') }}</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Server IP -->
|
|
|
|
|
<div class="bg-white/5 rounded-lg p-4 mb-4">
|
|
|
|
|
<div class="text-xs text-white/50 mb-1">{{ t('kioskRecovery.serverAddress') }}</div>
|
|
|
|
|
<div class="text-lg font-mono text-white font-medium">{{ serverIp || t('common.loading') }}</div>
|
|
|
|
|
<div v-if="serverIp" class="text-xs text-white/40 mt-1">{{ t('kioskRecovery.webUi', { address: serverIp }) }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- QR Code -->
|
|
|
|
|
<div v-if="serverIp" class="bg-white/5 rounded-lg p-4 mb-4 flex flex-col items-center">
|
|
|
|
|
<div class="text-xs text-white/50 mb-2">{{ t('kioskRecovery.scanForMobile') }}</div>
|
|
|
|
|
<div class="bg-white p-3 rounded-lg inline-block">
|
|
|
|
|
<img :src="qrCodeUrl" alt="QR Code" class="w-32 h-32" />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Diagnostics -->
|
|
|
|
|
<div class="space-y-2 mb-6">
|
|
|
|
|
<div class="flex items-center justify-between bg-white/5 rounded-lg p-3">
|
|
|
|
|
<span class="text-sm text-white/70">{{ t('kioskRecovery.backend') }}</span>
|
|
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
|
<div class="w-2 h-2 rounded-full" :class="backendHealthy ? 'bg-green-400' : 'bg-red-400'"></div>
|
|
|
|
|
<span class="text-sm" :class="backendHealthy ? 'text-green-400' : 'text-red-400'">
|
|
|
|
|
{{ backendHealthy ? t('common.healthy') : t('kioskRecovery.unreachable') }}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="flex items-center justify-between bg-white/5 rounded-lg p-3">
|
|
|
|
|
<span class="text-sm text-white/70">{{ t('kioskRecovery.containers') }}</span>
|
|
|
|
|
<span class="text-sm text-white font-medium">{{ containerCount }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="flex items-center justify-between bg-white/5 rounded-lg p-3">
|
|
|
|
|
<span class="text-sm text-white/70">{{ t('monitoring.diskUsage') }}</span>
|
|
|
|
|
<span class="text-sm text-white font-medium">{{ diskUsage }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Actions -->
|
|
|
|
|
<div class="flex gap-3">
|
|
|
|
|
<button @click="refreshDiagnostics" class="glass-button px-4 py-2 rounded-lg text-sm flex-1">
|
|
|
|
|
{{ t('common.refresh') }}
|
|
|
|
|
</button>
|
security+feat: v1.3.0 — pentest remediation, container reliability, UI overhaul
Security (33 pentest findings addressed):
- CRITICAL: backend binds 127.0.0.1, path traversal in tor.rs/dwn fixed
- HIGH: federation requires signatures, XSS login redirect, RBAC viewer restricted
- HIGH: tar slip prevention, S3 SSRF validation, backup ID validation
- MEDIUM: remember-me random secret, TOTP session rotation, password re-auth
- LOW: CSP unsafe-inline removed, CORS dev-only, onion/webhook validation
Container reliability:
- Memory limits on all 37 containers (OOM prevention)
- Exited vs stopped state distinction with health-aware status badges
- Crash recovery coordination (no more restart cascade)
- User-stopped tracking survives reboots
- Tiered boot recovery (databases → core → services → apps)
UI:
- Wallet TransactionsModal, health-aware app status badges
- Restart button on containers, exited/crashed red state
- Mesh view overhaul, glass button updates, BaseModal/ToggleSwitch
- Apps sticky header removed, dev faucet, mutable mock wallet
Infrastructure:
- LND REST port 8080 exposed over Tor (LND Connect fix)
- Nginx cookie_session fix, deploy script Tor config updated
- Dev environment: podman auto-start, boot mode simulation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 12:44:31 +00:00
|
|
|
<button @click="goToLogin" class="glass-button glass-button-warning px-4 py-2 rounded-lg text-sm flex-1">
|
2026-03-11 13:45:59 +00:00
|
|
|
{{ t('kioskRecovery.goToLogin') }}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="text-center mt-4">
|
|
|
|
|
<p class="text-xs text-white/30">{{ t('kioskRecovery.lastChecked', { time: lastChecked }) }}</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { ref, onMounted, computed } from 'vue'
|
|
|
|
|
import { useI18n } from 'vue-i18n'
|
|
|
|
|
import { useRouter } from 'vue-router'
|
|
|
|
|
|
|
|
|
|
const router = useRouter()
|
|
|
|
|
const { t } = useI18n()
|
|
|
|
|
|
|
|
|
|
const serverIp = ref('')
|
|
|
|
|
const backendHealthy = ref(false)
|
|
|
|
|
const containerCount = ref('—')
|
|
|
|
|
const diskUsage = ref('—')
|
|
|
|
|
const lastChecked = ref('—')
|
|
|
|
|
|
|
|
|
|
const qrCodeUrl = computed(() => {
|
|
|
|
|
if (!serverIp.value) return ''
|
|
|
|
|
const url = `http://${serverIp.value}`
|
|
|
|
|
return `https://api.qrserver.com/v1/create-qr-code/?size=128x128&data=${encodeURIComponent(url)}`
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
async function refreshDiagnostics() {
|
|
|
|
|
lastChecked.value = new Date().toLocaleTimeString()
|
|
|
|
|
|
|
|
|
|
// Detect server IP from window location
|
|
|
|
|
serverIp.value = window.location.hostname !== 'localhost'
|
|
|
|
|
? window.location.hostname
|
|
|
|
|
: '127.0.0.1'
|
|
|
|
|
|
|
|
|
|
// Check backend health
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch('/health', { signal: AbortSignal.timeout(5000) })
|
|
|
|
|
backendHealthy.value = res.ok
|
|
|
|
|
} catch {
|
|
|
|
|
backendHealthy.value = false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get system stats (unauthenticated won't work for RPC, but try health)
|
|
|
|
|
if (backendHealthy.value) {
|
|
|
|
|
try {
|
|
|
|
|
const statsRes = await fetch('/rpc/', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({ method: 'system.stats' }),
|
|
|
|
|
signal: AbortSignal.timeout(5000),
|
|
|
|
|
})
|
|
|
|
|
const data = await statsRes.json()
|
|
|
|
|
if (data.result) {
|
|
|
|
|
const disk = data.result.disk
|
|
|
|
|
if (disk) {
|
|
|
|
|
const usedPct = ((disk.used / disk.total) * 100).toFixed(0)
|
|
|
|
|
diskUsage.value = `${usedPct}% used`
|
|
|
|
|
}
|
|
|
|
|
containerCount.value = String(data.result.containers?.running ?? '—')
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
// Stats require auth — show defaults
|
|
|
|
|
containerCount.value = '—'
|
|
|
|
|
diskUsage.value = '—'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function goToLogin() {
|
|
|
|
|
router.push('/login')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
refreshDiagnostics()
|
|
|
|
|
})
|
|
|
|
|
</script>
|