archy/neode-ui/src/components/TransactionsModal.vue
Dorian 84a56c80de 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

117 lines
4.9 KiB
Vue

<template>
<BaseModal :show="show" :title="t('transactions.title')" max-width="max-w-2xl" content-class="max-h-[90vh] flex flex-col" @close="close">
<div v-if="transactions.length === 0" class="flex-1 flex items-center justify-center py-12">
<p class="text-white/40 text-sm">{{ t('transactions.noTransactionsYet') }}</p>
</div>
<div v-else class="flex-1 overflow-y-auto -mx-2 px-2 divide-y divide-white/5">
<div
v-for="tx in transactions"
:key="tx.tx_hash"
class="flex items-center justify-between gap-3 py-3 hover:bg-white/5 rounded-lg px-2 cursor-pointer transition-colors"
@click="openInMempool(tx.tx_hash)"
>
<div class="flex items-center gap-3 min-w-0 flex-1">
<div
class="w-8 h-8 rounded-full flex items-center justify-center shrink-0"
:class="tx.direction === 'incoming'
? (tx.num_confirmations === 0 ? 'bg-yellow-500/15' : 'bg-green-500/15')
: 'bg-red-500/10'"
>
<svg
class="w-4 h-4"
:class="tx.direction === 'incoming'
? (tx.num_confirmations === 0 ? 'text-yellow-400' : 'text-green-400')
: 'text-red-400'"
fill="none" stroke="currentColor" viewBox="0 0 24 24"
>
<path v-if="tx.direction === 'incoming'" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 14l-7 7m0 0l-7-7m7 7V3" />
<path v-else stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 10l7-7m0 0l7 7m-7-7v18" />
</svg>
</div>
<div class="min-w-0 flex-1">
<div class="flex items-center gap-2">
<span
class="text-sm font-medium"
:class="tx.direction === 'incoming' ? 'text-green-400' : 'text-red-400'"
>
{{ tx.direction === 'incoming' ? '+' : '-' }}{{ Math.abs(tx.amount_sats).toLocaleString() }} sats
</span>
<span
class="text-[10px] px-1.5 py-0.5 rounded-full font-medium"
:class="tx.num_confirmations === 0
? 'bg-yellow-500/15 text-yellow-400'
: tx.num_confirmations < 3
? 'bg-green-500/15 text-green-400'
: 'bg-white/10 text-white/50'"
>
{{ tx.num_confirmations === 0 ? t('transactions.unconfirmed') : t('transactions.confirmations', { count: tx.num_confirmations }) }}
</span>
</div>
<div class="flex items-center gap-2 mt-0.5">
<p class="text-[11px] text-white/40 font-mono truncate">{{ tx.tx_hash }}</p>
<span v-if="tx.label" class="text-[10px] text-white/30 shrink-0">{{ tx.label }}</span>
</div>
</div>
</div>
<div class="flex items-center gap-2 shrink-0">
<span class="text-[11px] text-white/40">{{ formatTxTime(tx.time_stamp) }}</span>
<svg class="w-3.5 h-3.5 text-white/30" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
</div>
</div>
</div>
</BaseModal>
</template>
<script setup lang="ts">
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import BaseModal from '@/components/BaseModal.vue'
interface WalletTransaction {
tx_hash: string
amount_sats: number
direction: 'incoming' | 'outgoing'
num_confirmations: number
time_stamp: number
total_fees: number
dest_addresses: string[]
label: string
block_height: number
}
defineProps<{
show: boolean
transactions: WalletTransaction[]
}>()
const emit = defineEmits<{ close: [] }>()
const { t } = useI18n()
const router = useRouter()
function close() {
emit('close')
}
function openInMempool(txHash: string) {
router.push({ name: 'app-session', params: { appId: 'mempool' }, query: { path: `/tx/${txHash}` } })
}
function formatTxTime(timestamp: number): string {
if (!timestamp) return ''
const date = new Date(timestamp * 1000)
const now = new Date()
const diffMs = now.getTime() - date.getTime()
const diffMins = Math.floor(diffMs / 60000)
if (diffMins < 1) return t('transactions.justNow')
if (diffMins < 60) return t('transactions.minutesAgo', { count: diffMins })
const diffHours = Math.floor(diffMins / 60)
if (diffHours < 24) return t('transactions.hoursAgo', { count: diffHours })
const diffDays = Math.floor(diffHours / 24)
if (diffDays < 7) return t('transactions.daysAgo', { count: diffDays })
return date.toLocaleDateString()
}
</script>