mirror of
https://git.quad4.io/RNS-Things/MeshChatX.git
synced 2026-05-27 05:15:49 +00:00
feat(frontend) update styling
This commit is contained in:
@@ -96,18 +96,18 @@
|
||||
<div class="mt-6 pt-6 border-t border-gray-200/70 dark:border-zinc-800/80">
|
||||
<button
|
||||
type="button"
|
||||
class="group w-full flex items-center justify-between gap-3 px-4 py-3 rounded-2xl bg-gradient-to-br from-blue-500/[0.08] via-slate-500/[0.06] to-violet-500/[0.08] dark:from-blue-500/15 dark:via-zinc-800/40 dark:to-violet-500/15 hover:from-blue-500/15 hover:to-violet-500/15 dark:hover:from-blue-500/25 dark:hover:to-violet-500/25 border border-gray-200/80 dark:border-zinc-700/80 transition-all text-left min-h-[52px]"
|
||||
class="about-action-btn secondary-chip w-full justify-between text-left"
|
||||
:aria-expanded="showContactSupport"
|
||||
@click="showContactSupport = !showContactSupport"
|
||||
>
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
<span
|
||||
class="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-blue-500/15 dark:bg-blue-500/25 text-blue-600 dark:text-blue-300 ring-1 ring-blue-500/20"
|
||||
class="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-gray-100 dark:bg-zinc-800 text-gray-600 dark:text-zinc-300"
|
||||
>
|
||||
<v-icon icon="mdi-card-account-details-outline" size="22"></v-icon>
|
||||
</span>
|
||||
<span
|
||||
class="text-xs font-black uppercase tracking-widest text-gray-800 dark:text-zinc-100 truncate"
|
||||
class="text-xs font-semibold uppercase tracking-wide text-gray-800 dark:text-zinc-100 truncate"
|
||||
>
|
||||
{{ $t("about.contact_support_title") }}
|
||||
</span>
|
||||
@@ -120,129 +120,105 @@
|
||||
</button>
|
||||
|
||||
<transition name="fade">
|
||||
<div
|
||||
v-if="showContactSupport"
|
||||
class="mt-4 p-5 sm:p-6 rounded-2xl bg-white/70 dark:bg-zinc-950/70 border border-gray-200/90 dark:border-zinc-800 shadow-sm space-y-6"
|
||||
>
|
||||
<div class="space-y-4">
|
||||
<div v-if="showContactSupport" class="mt-6 flex flex-col gap-6">
|
||||
<div class="flex flex-col gap-3">
|
||||
<div
|
||||
class="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-gray-500 dark:text-zinc-400"
|
||||
class="text-xs font-semibold text-gray-500 dark:text-zinc-400 uppercase tracking-wide flex items-center gap-2"
|
||||
>
|
||||
<v-icon
|
||||
icon="mdi-account-circle-outline"
|
||||
size="18"
|
||||
class="text-blue-500 dark:text-blue-400"
|
||||
></v-icon>
|
||||
<v-icon icon="mdi-account-circle-outline" size="16"></v-icon>
|
||||
{{ $t("about.contact_developer") }}
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<div
|
||||
class="text-[9px] font-black text-gray-400 dark:text-zinc-500 uppercase tracking-widest mb-1.5"
|
||||
<div class="flex flex-col gap-2">
|
||||
<div
|
||||
class="flex items-center justify-between gap-2 px-3 py-2 rounded-xl bg-gray-50 dark:bg-zinc-900/40 border border-gray-100 dark:border-zinc-800"
|
||||
>
|
||||
<router-link
|
||||
:to="{
|
||||
name: 'messages',
|
||||
params: { destinationHash: developerLxmfPrimary },
|
||||
}"
|
||||
class="flex-1 min-w-0 text-sm font-mono text-gray-700 dark:text-zinc-300 hover:text-blue-600 dark:hover:text-blue-400 break-all leading-snug text-left"
|
||||
:title="$t('about.contact_open_messages')"
|
||||
>
|
||||
{{ $t("about.contact_lxmf_address") }}
|
||||
</div>
|
||||
<div class="flex items-start gap-2">
|
||||
<router-link
|
||||
:to="{
|
||||
name: 'messages',
|
||||
params: { destinationHash: developerLxmfPrimary },
|
||||
}"
|
||||
class="flex-1 min-w-0 text-sm font-medium text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 hover:underline underline-offset-2 break-all leading-snug text-left"
|
||||
:title="$t('about.contact_open_messages')"
|
||||
>
|
||||
{{ developerLxmfPrimary }}
|
||||
</router-link>
|
||||
<button
|
||||
type="button"
|
||||
class="shrink-0 rounded-lg p-2 text-gray-500 hover:text-blue-600 dark:text-zinc-400 dark:hover:text-blue-400 hover:bg-gray-100 dark:hover:bg-zinc-800/80 transition-colors"
|
||||
:aria-label="$t('about.contact_copy_address')"
|
||||
@click="
|
||||
copyValue(
|
||||
developerLxmfPrimary,
|
||||
'about.contact_lxmf_address'
|
||||
)
|
||||
"
|
||||
>
|
||||
<v-icon icon="mdi-content-copy" size="18"></v-icon>
|
||||
</button>
|
||||
</div>
|
||||
{{ developerLxmfPrimary }}
|
||||
</router-link>
|
||||
<button
|
||||
type="button"
|
||||
class="shrink-0 rounded-lg p-1.5 text-gray-500 hover:text-blue-600 dark:text-zinc-400 dark:hover:text-blue-400 hover:bg-gray-100 dark:hover:bg-zinc-800/80 transition-colors"
|
||||
:aria-label="$t('about.contact_copy_address')"
|
||||
@click="
|
||||
copyValue(developerLxmfPrimary, 'about.contact_lxmf_address')
|
||||
"
|
||||
>
|
||||
<v-icon icon="mdi-content-copy" size="16"></v-icon>
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="text-[9px] font-black text-gray-400 dark:text-zinc-500 uppercase tracking-widest mb-1.5"
|
||||
<div
|
||||
class="flex items-center justify-between gap-2 px-3 py-2 rounded-xl bg-gray-50 dark:bg-zinc-900/40 border border-gray-100 dark:border-zinc-800"
|
||||
>
|
||||
<router-link
|
||||
:to="{
|
||||
name: 'messages',
|
||||
params: { destinationHash: developerLxmfAlternate },
|
||||
}"
|
||||
class="flex-1 min-w-0 text-sm font-mono text-gray-700 dark:text-zinc-300 hover:text-blue-600 dark:hover:text-blue-400 break-all leading-snug text-left"
|
||||
:title="$t('about.contact_open_messages')"
|
||||
>
|
||||
{{ $t("about.contact_alternate") }}
|
||||
</div>
|
||||
<div class="flex items-start gap-2">
|
||||
<router-link
|
||||
:to="{
|
||||
name: 'messages',
|
||||
params: { destinationHash: developerLxmfAlternate },
|
||||
}"
|
||||
class="flex-1 min-w-0 text-sm font-medium text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 hover:underline underline-offset-2 break-all leading-snug text-left"
|
||||
:title="$t('about.contact_open_messages')"
|
||||
>
|
||||
{{ developerLxmfAlternate }}
|
||||
</router-link>
|
||||
<button
|
||||
type="button"
|
||||
class="shrink-0 rounded-lg p-2 text-gray-500 hover:text-blue-600 dark:text-zinc-400 dark:hover:text-blue-400 hover:bg-gray-100 dark:hover:bg-zinc-800/80 transition-colors"
|
||||
:aria-label="$t('about.contact_copy_address')"
|
||||
@click="
|
||||
copyValue(developerLxmfAlternate, 'about.contact_alternate')
|
||||
"
|
||||
>
|
||||
<v-icon icon="mdi-content-copy" size="18"></v-icon>
|
||||
</button>
|
||||
</div>
|
||||
{{ developerLxmfAlternate }}
|
||||
</router-link>
|
||||
<button
|
||||
type="button"
|
||||
class="shrink-0 rounded-lg p-1.5 text-gray-500 hover:text-blue-600 dark:text-zinc-400 dark:hover:text-blue-400 hover:bg-gray-100 dark:hover:bg-zinc-800/80 transition-colors"
|
||||
:aria-label="$t('about.contact_copy_address')"
|
||||
@click="
|
||||
copyValue(developerLxmfAlternate, 'about.contact_alternate')
|
||||
"
|
||||
>
|
||||
<v-icon icon="mdi-content-copy" size="16"></v-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="text-xs font-semibold text-gray-600 dark:text-zinc-400 bg-gray-50 dark:bg-zinc-900/50 p-3 rounded-xl border border-gray-200/90 dark:border-zinc-800 flex items-start gap-2.5"
|
||||
class="text-xs text-gray-500 dark:text-zinc-400 bg-gray-50 dark:bg-zinc-900/40 p-3 rounded-xl border border-gray-100 dark:border-zinc-800 flex items-start gap-2"
|
||||
>
|
||||
<v-icon
|
||||
icon="mdi-information-outline"
|
||||
size="18"
|
||||
class="text-blue-500 dark:text-blue-400 shrink-0 mt-0.5"
|
||||
size="16"
|
||||
class="shrink-0 mt-0.5"
|
||||
></v-icon>
|
||||
<span>{{ $t("about.contact_propagation_hint") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-gray-100 dark:border-zinc-800/90 pt-2"></div>
|
||||
<div class="border-t border-gray-100 dark:border-zinc-800/90"></div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="flex flex-col gap-3">
|
||||
<div
|
||||
class="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-gray-500 dark:text-zinc-400"
|
||||
class="text-xs font-semibold text-gray-500 dark:text-zinc-400 uppercase tracking-wide flex items-center gap-2"
|
||||
>
|
||||
<v-icon
|
||||
icon="mdi-hand-heart"
|
||||
size="18"
|
||||
class="text-blue-500 dark:text-blue-400"
|
||||
></v-icon>
|
||||
<v-icon icon="mdi-hand-heart" size="16"></v-icon>
|
||||
{{ $t("about.donate_label") }}
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="text-[9px] font-black text-gray-400 dark:text-zinc-500 uppercase tracking-widest mb-1.5"
|
||||
class="text-xs font-semibold text-gray-500 dark:text-zinc-400 uppercase tracking-wide mb-1.5"
|
||||
>
|
||||
{{ $t("about.donate_monero_label") }}
|
||||
</div>
|
||||
<div class="flex items-start gap-2">
|
||||
<div
|
||||
class="flex items-center justify-between gap-2 px-3 py-2 rounded-xl bg-gray-50 dark:bg-zinc-900/40 border border-gray-100 dark:border-zinc-800"
|
||||
>
|
||||
<span
|
||||
class="flex-1 min-w-0 text-sm font-medium text-gray-800 dark:text-zinc-200 break-all leading-snug select-all"
|
||||
class="flex-1 min-w-0 text-sm font-mono text-gray-700 dark:text-zinc-300 break-all leading-snug select-all"
|
||||
>{{ moneroDonateAddress }}</span
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="shrink-0 rounded-lg p-2 text-gray-500 hover:text-blue-600 dark:text-zinc-400 dark:hover:text-blue-400 hover:bg-gray-100 dark:hover:bg-zinc-800/80 transition-colors"
|
||||
class="shrink-0 rounded-lg p-1.5 text-gray-500 hover:text-blue-600 dark:text-zinc-400 dark:hover:text-blue-400 hover:bg-gray-100 dark:hover:bg-zinc-800/80 transition-colors"
|
||||
:aria-label="$t('about.donate_copy_monero')"
|
||||
@click="copyValue(moneroDonateAddress, 'about.donate_monero_label')"
|
||||
>
|
||||
<v-icon icon="mdi-content-copy" size="18"></v-icon>
|
||||
<v-icon icon="mdi-content-copy" size="16"></v-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -252,12 +228,12 @@
|
||||
href="https://ko-fi.com/quad4"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-flex flex-1 items-center justify-center gap-2 px-4 py-2.5 rounded-xl border border-gray-200 dark:border-zinc-700 bg-white/90 dark:bg-zinc-900/70 hover:bg-blue-500/[0.06] hover:border-blue-500/25 dark:hover:bg-zinc-800/80 text-gray-800 dark:text-zinc-100 text-xs font-bold uppercase tracking-wide min-h-[44px] transition-colors"
|
||||
class="inline-flex flex-1 items-center justify-center gap-2 px-4 py-2.5 rounded-xl border border-gray-200 dark:border-zinc-700 bg-gray-50 dark:bg-zinc-900/40 hover:bg-gray-100 dark:hover:bg-zinc-800 text-gray-800 dark:text-zinc-100 text-xs font-semibold transition-colors"
|
||||
>
|
||||
<v-icon
|
||||
icon="mdi-coffee"
|
||||
size="18"
|
||||
class="text-blue-600 dark:text-blue-400"
|
||||
class="text-gray-500 dark:text-zinc-400"
|
||||
></v-icon>
|
||||
{{ $t("about.donate_kofi") }}
|
||||
</a>
|
||||
@@ -265,12 +241,12 @@
|
||||
href="https://buymeacoffee.com/quad4"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-flex flex-1 items-center justify-center gap-2 px-4 py-2.5 rounded-xl border border-gray-200 dark:border-zinc-700 bg-white/90 dark:bg-zinc-900/70 hover:bg-blue-500/[0.06] hover:border-blue-500/25 dark:hover:bg-zinc-800/80 text-gray-800 dark:text-zinc-100 text-xs font-bold uppercase tracking-wide min-h-[44px] transition-colors"
|
||||
class="inline-flex flex-1 items-center justify-center gap-2 px-4 py-2.5 rounded-xl border border-gray-200 dark:border-zinc-700 bg-gray-50 dark:bg-zinc-900/40 hover:bg-gray-100 dark:hover:bg-zinc-800 text-gray-800 dark:text-zinc-100 text-xs font-semibold transition-colors"
|
||||
>
|
||||
<v-icon
|
||||
icon="mdi-cup"
|
||||
size="18"
|
||||
class="text-blue-600 dark:text-blue-400"
|
||||
class="text-gray-500 dark:text-zinc-400"
|
||||
></v-icon>
|
||||
{{ $t("about.donate_buymeacoffee") }}
|
||||
</a>
|
||||
@@ -987,6 +963,19 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="autoBackups.some((b) => b.name.includes('SUSPICIOUS'))"
|
||||
class="mt-4 p-3 rounded-xl bg-amber-50 dark:bg-amber-950/30 border border-amber-200 dark:border-amber-900 text-xs text-amber-700 dark:text-amber-300 flex items-start gap-2"
|
||||
>
|
||||
<v-icon icon="mdi-alert" size="16" class="shrink-0 mt-0.5"></v-icon>
|
||||
<span
|
||||
>Suspicious backups are created when the database size or message count
|
||||
drops unexpectedly compared to the last known baseline, usually after a
|
||||
crash, corruption, or deletion. They are kept automatically so you can
|
||||
inspect or restore from them.</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- Backups Pagination -->
|
||||
<div
|
||||
v-if="autoBackupsTotal > autoBackupsLimit"
|
||||
@@ -1072,7 +1061,7 @@ export default {
|
||||
autoBackups: [],
|
||||
autoBackupsTotal: 0,
|
||||
autoBackupsOffset: 0,
|
||||
autoBackupsLimit: 3,
|
||||
autoBackupsLimit: 4,
|
||||
electronVersion: null,
|
||||
chromeVersion: null,
|
||||
nodeVersion: null,
|
||||
|
||||
@@ -1,167 +1,185 @@
|
||||
<!-- SPDX-License-Identifier: 0BSD -->
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col flex-1 h-full overflow-hidden bg-slate-50 dark:bg-zinc-950">
|
||||
<div
|
||||
class="flex items-center px-4 py-4 bg-white dark:bg-zinc-900 border-b border-gray-200 dark:border-zinc-800 shadow-xs"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="p-2 bg-red-100 dark:bg-red-900/30 rounded-lg">
|
||||
<MaterialDesignIcon icon-name="gavel" class="size-6 text-red-600 dark:text-red-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-xl font-bold text-gray-900 dark:text-white">{{ $t("banishment.title") }}</h1>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">{{ $t("banishment.description") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ml-auto flex items-center gap-2 sm:gap-4">
|
||||
<div class="relative w-32 sm:w-64 md:w-80">
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<MaterialDesignIcon icon-name="magnify" class="size-5 text-gray-400" />
|
||||
</div>
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
class="block w-full pl-10 pr-3 py-2 border border-gray-300 dark:border-zinc-700 rounded-lg bg-gray-50 dark:bg-zinc-800 text-gray-900 dark:text-gray-100 placeholder-gray-500 focus:outline-hidden focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
|
||||
:placeholder="$t('banishment.search_placeholder')"
|
||||
@input="onSearchInput"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
class="p-2 text-gray-500 hover:text-blue-500 dark:text-gray-400 dark:hover:text-blue-400 transition-colors"
|
||||
:title="$t('common.refresh')"
|
||||
@click="loadBlockedDestinations"
|
||||
>
|
||||
<MaterialDesignIcon
|
||||
icon-name="refresh"
|
||||
class="size-6"
|
||||
:class="{ 'animate-spin-reverse': isLoading }"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto p-4 md:p-6">
|
||||
<div
|
||||
v-if="isLoading && filteredBlockedIdentities.length === 0"
|
||||
class="flex flex-col items-center justify-center h-64"
|
||||
>
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mb-4"></div>
|
||||
<p class="text-gray-500 dark:text-gray-400">{{ $t("banishment.loading_items") }}</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="filteredBlockedIdentities.length === 0"
|
||||
class="flex flex-col items-center justify-center h-64 text-center"
|
||||
>
|
||||
<div class="p-4 bg-gray-100 dark:bg-zinc-800 rounded-full mb-4 text-gray-400 dark:text-zinc-600">
|
||||
<MaterialDesignIcon icon-name="check-circle" class="size-12" />
|
||||
</div>
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white">{{ $t("banishment.no_items") }}</h3>
|
||||
<p class="text-gray-500 dark:text-gray-400 max-w-sm mx-auto">
|
||||
{{ searchQuery ? $t("nomadnet.no_search_results_peers") : $t("nomadnet.no_announces_yet") }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div
|
||||
class="flex flex-col flex-1 overflow-hidden min-w-0 bg-linear-to-br from-slate-50 via-slate-100 to-white dark:from-zinc-950 dark:via-zinc-900 dark:to-zinc-900"
|
||||
>
|
||||
<div class="flex-1 overflow-y-auto overflow-x-hidden w-full px-3 sm:px-5 md:px-5 lg:px-8 py-3 sm:py-4 min-w-0">
|
||||
<div class="space-y-0 w-full max-w-6xl xl:max-w-7xl mx-auto min-w-0">
|
||||
<div
|
||||
v-for="identity in filteredBlockedIdentities"
|
||||
:key="identity.identity_hash"
|
||||
class="bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 rounded-xl shadow-lg overflow-hidden"
|
||||
class="w-full border-b border-gray-200/60 dark:border-zinc-800/60 py-4 sm:py-6 flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4"
|
||||
>
|
||||
<div class="p-5">
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<div class="p-2 bg-red-100 dark:bg-red-900/30 rounded-lg shrink-0">
|
||||
<MaterialDesignIcon
|
||||
icon-name="account-off"
|
||||
class="size-5 text-red-600 dark:text-red-400"
|
||||
/>
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-center gap-2 mb-1 flex-wrap">
|
||||
<h4
|
||||
class="text-base font-semibold text-gray-900 dark:text-white wrap-break-word"
|
||||
:title="identity.display_name"
|
||||
>
|
||||
{{ identity.display_name || $t("call.unknown") }}
|
||||
</h4>
|
||||
<span
|
||||
v-if="identity.is_node"
|
||||
class="px-2 py-0.5 text-xs font-medium bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 rounded-sm"
|
||||
>
|
||||
{{ $t("banishment.node") }}
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
class="px-2 py-0.5 text-xs font-medium bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 rounded-sm"
|
||||
>
|
||||
{{ $t("banishment.user") }}
|
||||
</span>
|
||||
<span
|
||||
v-if="identity.is_rns_blackholed"
|
||||
class="px-2 py-0.5 text-xs font-medium bg-zinc-100 dark:bg-zinc-800 text-zinc-700 dark:text-zinc-300 rounded-sm border border-zinc-200 dark:border-zinc-700"
|
||||
title="Blackholed at Reticulum transport layer"
|
||||
>
|
||||
RNS Blackhole
|
||||
</span>
|
||||
</div>
|
||||
<p
|
||||
class="text-xs text-gray-500 dark:text-gray-400 font-mono break-all mt-1"
|
||||
:title="identity.identity_hash"
|
||||
>
|
||||
{{ identity.identity_hash }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Blocked destination hashes -->
|
||||
<div v-if="identity.blocked_destinations.length > 0" class="mb-2">
|
||||
<p class="text-xs font-medium text-gray-600 dark:text-gray-300 mb-1">
|
||||
{{ $t("banishment.blocked_destinations") }}
|
||||
</p>
|
||||
<div class="space-y-1">
|
||||
<div
|
||||
v-for="dest in identity.blocked_destinations"
|
||||
:key="dest.destination_hash"
|
||||
class="flex items-center justify-between text-xs text-gray-500 dark:text-gray-400 font-mono bg-gray-50 dark:bg-zinc-800 px-2 py-1 rounded"
|
||||
>
|
||||
<span class="break-all">{{ dest.destination_hash }}</span>
|
||||
<span
|
||||
v-if="dest.created_at"
|
||||
class="shrink-0 ml-2 text-gray-400 dark:text-zinc-500"
|
||||
>
|
||||
{{ formatTimeAgo(dest.created_at) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="identity.rns_reason"
|
||||
class="text-xs italic text-zinc-500 dark:text-zinc-400 mb-2"
|
||||
>
|
||||
"{{ identity.rns_reason }}"
|
||||
</div>
|
||||
<div
|
||||
v-if="identity.rns_source"
|
||||
class="text-[10px] text-zinc-500 dark:text-zinc-500 font-mono truncate mb-1"
|
||||
>
|
||||
Source: {{ identity.rns_source }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="min-w-0 space-y-1">
|
||||
<h1 class="text-2xl sm:text-3xl font-bold text-gray-900 dark:text-white tracking-tight">
|
||||
{{ $t("banishment.title") }}
|
||||
</h1>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
{{ $t("banishment.description") }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 sm:shrink-0">
|
||||
<div class="relative flex-1 sm:w-64 lg:w-80">
|
||||
<MaterialDesignIcon
|
||||
icon-name="magnify"
|
||||
class="absolute left-3 top-1/2 -translate-y-1/2 size-5 shrink-0 text-gray-400 pointer-events-none z-10"
|
||||
/>
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
class="input-field pl-11!"
|
||||
:placeholder="$t('banishment.search_placeholder')"
|
||||
@input="onSearchInput"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
class="w-full flex items-center justify-center gap-2 px-4 py-2 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 text-green-700 dark:text-green-300 rounded-lg hover:bg-green-100 dark:hover:bg-green-900/30 transition-colors font-medium"
|
||||
@click="onUnblock(identity)"
|
||||
type="button"
|
||||
class="secondary-chip p-2.5!"
|
||||
:title="$t('common.refresh')"
|
||||
@click="loadBlockedDestinations"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="check-circle" class="size-5" />
|
||||
<span>{{ $t("banishment.lift_banishment") }}</span>
|
||||
<MaterialDesignIcon
|
||||
icon-name="refresh"
|
||||
class="size-5"
|
||||
:class="{ 'animate-spin-reverse': isLoading }"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="isLoading && filteredBlockedIdentities.length === 0">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 py-4 sm:py-6">
|
||||
<div
|
||||
v-for="i in 5"
|
||||
:key="'skel-' + i"
|
||||
class="banishment-card overflow-hidden p-5 space-y-4 min-w-0"
|
||||
>
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="size-10 rounded-xl bg-gray-200 dark:bg-zinc-700 animate-pulse shrink-0" />
|
||||
<div class="flex-1 min-w-0 space-y-2">
|
||||
<div class="h-4 w-28 bg-gray-200 dark:bg-zinc-700 rounded-sm animate-pulse" />
|
||||
<div class="h-3 w-44 bg-gray-100 dark:bg-zinc-800 rounded-sm animate-pulse" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<div class="h-3 w-20 bg-gray-100 dark:bg-zinc-800 rounded-sm animate-pulse" />
|
||||
<div class="h-8 bg-gray-100 dark:bg-zinc-800 rounded-lg animate-pulse" />
|
||||
</div>
|
||||
<div class="h-9 bg-gray-100 dark:bg-zinc-800 rounded-xl animate-pulse" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div
|
||||
v-else-if="filteredBlockedIdentities.length === 0"
|
||||
class="flex flex-col items-center justify-center py-16 sm:py-20 text-center"
|
||||
>
|
||||
<div class="p-4 bg-gray-100 dark:bg-zinc-800 rounded-full mb-4 text-gray-400 dark:text-zinc-600">
|
||||
<MaterialDesignIcon icon-name="check-circle" class="size-12" />
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{{ $t("banishment.no_items") }}
|
||||
</h3>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400 max-w-sm mx-auto mt-1">
|
||||
{{ searchQuery ? $t("nomadnet.no_search_results_peers") : $t("nomadnet.no_announces_yet") }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 py-4 sm:py-6">
|
||||
<div
|
||||
v-for="identity in filteredBlockedIdentities"
|
||||
:key="identity.identity_hash"
|
||||
class="banishment-card overflow-hidden group min-w-0"
|
||||
>
|
||||
<div class="p-5 space-y-4">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="p-2 bg-red-100 dark:bg-red-900/30 rounded-xl shrink-0">
|
||||
<MaterialDesignIcon
|
||||
icon-name="account-off"
|
||||
class="size-5 text-red-600 dark:text-red-400"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex flex-wrap items-center gap-1.5 mb-1">
|
||||
<h3
|
||||
class="text-sm font-bold text-gray-900 dark:text-white truncate max-w-full"
|
||||
:title="identity.display_name || $t('call.unknown')"
|
||||
>
|
||||
{{ identity.display_name || $t("call.unknown") }}
|
||||
</h3>
|
||||
<span
|
||||
v-if="identity.is_node"
|
||||
class="px-2 py-0.5 rounded-full bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-300 text-[10px] font-bold uppercase tracking-wider shrink-0"
|
||||
>
|
||||
{{ $t("banishment.node") }}
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
class="px-2 py-0.5 rounded-full bg-purple-100 dark:bg-purple-900/40 text-purple-700 dark:text-purple-300 text-[10px] font-bold uppercase tracking-wider shrink-0"
|
||||
>
|
||||
{{ $t("banishment.user") }}
|
||||
</span>
|
||||
<span
|
||||
v-if="identity.is_rns_blackholed"
|
||||
class="px-2 py-0.5 rounded-full bg-zinc-100 dark:bg-zinc-800 text-zinc-700 dark:text-zinc-300 text-[10px] font-bold uppercase tracking-wider border border-zinc-200 dark:border-zinc-700 shrink-0"
|
||||
title="Blackholed at Reticulum transport layer"
|
||||
>
|
||||
RNS Blackhole
|
||||
</span>
|
||||
</div>
|
||||
<p
|
||||
class="text-xs font-mono text-gray-500 dark:text-gray-400 truncate"
|
||||
:title="identity.identity_hash"
|
||||
>
|
||||
{{ identity.identity_hash }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="identity.blocked_destinations.length > 0">
|
||||
<p
|
||||
class="text-xs font-semibold text-gray-600 dark:text-gray-300 mb-2 uppercase tracking-wide"
|
||||
>
|
||||
{{ $t("banishment.blocked_destinations") }}
|
||||
</p>
|
||||
<div class="space-y-1">
|
||||
<div
|
||||
v-for="dest in identity.blocked_destinations"
|
||||
:key="dest.destination_hash"
|
||||
class="flex items-center justify-between gap-2 px-3 py-2 bg-gray-50 dark:bg-zinc-800/50 rounded-lg text-xs"
|
||||
>
|
||||
<span
|
||||
class="font-mono text-gray-500 dark:text-gray-400 truncate min-w-0"
|
||||
:title="dest.destination_hash"
|
||||
>
|
||||
{{ dest.destination_hash }}
|
||||
</span>
|
||||
<span v-if="dest.created_at" class="shrink-0 text-gray-400 dark:text-zinc-500">
|
||||
{{ formatTimeAgo(dest.created_at) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="identity.rns_reason"
|
||||
class="text-xs italic text-zinc-500 dark:text-zinc-400 leading-relaxed"
|
||||
>
|
||||
“{{ identity.rns_reason }}”
|
||||
</div>
|
||||
<div
|
||||
v-if="identity.rns_source"
|
||||
class="text-[10px] text-zinc-500 dark:text-zinc-500 font-mono truncate"
|
||||
>
|
||||
Source: {{ identity.rns_source }}
|
||||
</div>
|
||||
|
||||
<button class="primary-chip w-full justify-center" @click="onUnblock(identity)">
|
||||
<MaterialDesignIcon icon-name="check-circle" class="size-4" />
|
||||
<span>{{ $t("banishment.lift_banishment") }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -212,11 +230,9 @@ export default {
|
||||
async loadBlockedDestinations() {
|
||||
this.isLoading = true;
|
||||
try {
|
||||
// Load local blocked destinations
|
||||
const response = await window.api.get("/api/v1/blocked-destinations");
|
||||
const blockedHashes = response.data.blocked_destinations || [];
|
||||
|
||||
// Load Reticulum blackholed identities
|
||||
let reticulumBlackholed = {};
|
||||
try {
|
||||
const rnsResponse = await window.api.get("/api/v1/reticulum/blackhole");
|
||||
@@ -243,7 +259,6 @@ export default {
|
||||
return identityMap[identityHash];
|
||||
};
|
||||
|
||||
// Process local blocked destinations
|
||||
const processBlockedHash = async (blocked) => {
|
||||
const hash = blocked.destination_hash;
|
||||
let identityHash = hash;
|
||||
@@ -280,7 +295,6 @@ export default {
|
||||
|
||||
await Promise.all(blockedHashes.map((blocked) => processBlockedHash(blocked)));
|
||||
|
||||
// Process Reticulum blackholed identities
|
||||
for (const [hash, info] of Object.entries(reticulumBlackholed)) {
|
||||
const identity = ensureIdentity(hash);
|
||||
identity.is_rns_blackholed = true;
|
||||
@@ -288,7 +302,6 @@ export default {
|
||||
identity.rns_reason = info.reason || null;
|
||||
identity.rns_until = info.until || null;
|
||||
|
||||
// Try to look up display name from announces
|
||||
if (!identity.display_name) {
|
||||
try {
|
||||
const announceResponse = await window.api.get("/api/v1/announces", {
|
||||
@@ -329,7 +342,6 @@ export default {
|
||||
}
|
||||
|
||||
try {
|
||||
// Use the first blocked destination hash, or fall back to identity hash
|
||||
const targetHash =
|
||||
identity.blocked_destinations.length > 0
|
||||
? identity.blocked_destinations[0].destination_hash
|
||||
@@ -350,3 +362,16 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference "../../style.css";
|
||||
.banishment-card {
|
||||
@apply bg-white dark:bg-zinc-900/95 border border-gray-200/70 dark:border-zinc-800/80 rounded-2xl shadow-sm transition-all duration-200;
|
||||
}
|
||||
.banishment-card:hover {
|
||||
@apply shadow-md border-gray-300/80 dark:border-zinc-700/80;
|
||||
}
|
||||
.input-field {
|
||||
@apply bg-gray-50/90 dark:bg-zinc-800/80 border border-gray-200 dark:border-zinc-700 text-sm rounded-xl focus:ring-2 focus:ring-blue-400 focus:border-blue-400 dark:focus:ring-blue-500 dark:focus:border-blue-500 block w-full p-3 text-gray-900 dark:text-gray-100 transition;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
</div>
|
||||
<div v-else class="divide-y divide-gray-100 dark:divide-zinc-800">
|
||||
<div
|
||||
v-for="contact in contacts"
|
||||
v-for="contact in mergedContacts"
|
||||
:key="contact.id"
|
||||
class="group flex cursor-default items-center gap-3 px-1 py-3 transition-colors hover:bg-gray-50/80 dark:hover:bg-zinc-900/70"
|
||||
@contextmenu.prevent="openContextMenu($event, contact)"
|
||||
@@ -106,8 +106,30 @@
|
||||
<div class="font-semibold text-gray-900 dark:text-zinc-100 truncate">
|
||||
{{ contact.name }}
|
||||
</div>
|
||||
<div class="text-xs font-mono text-gray-500 dark:text-zinc-400 break-all">
|
||||
{{ contact.lxmf_address || contact.remote_identity_hash }}
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<div v-if="contact.remote_destination_hash" class="flex items-center gap-1.5">
|
||||
<MaterialDesignIcon
|
||||
icon-name="message-text-outline"
|
||||
class="size-4 text-blue-500 dark:text-blue-400 shrink-0"
|
||||
/>
|
||||
<span class="text-xs font-mono text-gray-500 dark:text-zinc-400 break-all">{{
|
||||
contact.remote_destination_hash
|
||||
}}</span>
|
||||
</div>
|
||||
<div v-if="contact.remote_telephony_hash" class="flex items-center gap-1.5">
|
||||
<MaterialDesignIcon
|
||||
icon-name="phone-outline"
|
||||
class="size-4 text-green-600 dark:text-green-400 shrink-0"
|
||||
/>
|
||||
<span class="text-xs font-mono text-gray-500 dark:text-zinc-400 break-all">{{
|
||||
contact.remote_telephony_hash
|
||||
}}</span>
|
||||
</div>
|
||||
<span
|
||||
v-if="!contact.remote_destination_hash && !contact.remote_telephony_hash"
|
||||
class="text-xs font-mono text-gray-500 dark:text-zinc-400 break-all"
|
||||
>{{ contact.lxmf_address || contact.remote_identity_hash }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -475,6 +497,24 @@ export default {
|
||||
hasMoreContacts() {
|
||||
return this.contacts.length < this.totalContactsCount;
|
||||
},
|
||||
mergedContacts() {
|
||||
const map = new Map();
|
||||
for (const c of this.contacts) {
|
||||
const key = c.name?.toLowerCase() || "";
|
||||
if (!map.has(key)) {
|
||||
map.set(key, { ...c });
|
||||
} else {
|
||||
const existing = map.get(key);
|
||||
// Merge fields so both LXMF and LXST addresses are visible
|
||||
existing.lxmf_address = existing.lxmf_address || c.lxmf_address;
|
||||
existing.lxst_address = existing.lxst_address || c.lxst_address;
|
||||
existing.remote_destination_hash = existing.remote_destination_hash || c.remote_destination_hash;
|
||||
existing.remote_telephony_hash = existing.remote_telephony_hash || c.remote_telephony_hash;
|
||||
existing.remote_identity_hash = existing.remote_identity_hash || c.remote_identity_hash;
|
||||
}
|
||||
}
|
||||
return Array.from(map.values());
|
||||
},
|
||||
},
|
||||
beforeUnmount() {
|
||||
WebSocketConnection.off("message", this.onWebsocketMessage);
|
||||
@@ -669,7 +709,6 @@ export default {
|
||||
|
||||
await window.api.post("/api/v1/telephone/contacts", {
|
||||
name: this.newContactName?.trim() || `Contact ${destinationHash.slice(0, 8)}`,
|
||||
remote_identity_hash: destinationHash,
|
||||
lxmf_address: destinationHash,
|
||||
});
|
||||
ToastUtils.success(this.$t("contacts.contact_added"));
|
||||
@@ -704,9 +743,17 @@ export default {
|
||||
async removeContact(contact) {
|
||||
this.closeContextMenu();
|
||||
if (!contact?.id) return;
|
||||
if (!window.confirm(this.$t("contacts.remove_contact_confirm"))) return;
|
||||
const duplicates = this.contacts.filter((c) => c.name === contact.name && c.id !== contact.id);
|
||||
const confirmMsg =
|
||||
duplicates.length > 0
|
||||
? `${this.$t("contacts.remove_contact_confirm")}\n\n(${duplicates.length} additional duplicate${duplicates.length > 1 ? "s" : ""} named "${contact.name}" will also be removed)`
|
||||
: this.$t("contacts.remove_contact_confirm");
|
||||
if (!window.confirm(confirmMsg)) return;
|
||||
try {
|
||||
await window.api.delete(`/api/v1/telephone/contacts/${contact.id}`);
|
||||
const ids = [contact.id, ...duplicates.map((c) => c.id)];
|
||||
for (const id of ids) {
|
||||
await window.api.delete(`/api/v1/telephone/contacts/${id}`);
|
||||
}
|
||||
ToastUtils.success(this.$t("contacts.contact_removed"));
|
||||
await this.getContacts();
|
||||
} catch {
|
||||
@@ -719,13 +766,19 @@ export default {
|
||||
const name = await DialogUtils.prompt(this.$t("contacts.enter_contact_name"), contact.name);
|
||||
if (name == null || name === contact.name) return;
|
||||
try {
|
||||
await window.api.patch(`/api/v1/telephone/contacts/${contact.id}`, { name });
|
||||
const destHash =
|
||||
contact.remote_destination_hash || contact.lxmf_address || contact.remote_identity_hash;
|
||||
if (destHash && name.length > 0) {
|
||||
await window.api.post(`/api/v1/destination/${destHash}/custom-display-name/update`, {
|
||||
display_name: name,
|
||||
});
|
||||
const duplicates = this.contacts.filter((c) => c.name === contact.name && c.id !== contact.id);
|
||||
const ids = [contact.id, ...duplicates.map((c) => c.id)];
|
||||
for (const id of ids) {
|
||||
await window.api.patch(`/api/v1/telephone/contacts/${id}`, { name });
|
||||
}
|
||||
const allContacts = [contact, ...duplicates];
|
||||
for (const c of allContacts) {
|
||||
const destHash = c.remote_destination_hash || c.lxmf_address || c.remote_identity_hash;
|
||||
if (destHash && name.length > 0) {
|
||||
await window.api.post(`/api/v1/destination/${destHash}/custom-display-name/update`, {
|
||||
display_name: name,
|
||||
});
|
||||
}
|
||||
}
|
||||
ToastUtils.success(this.$t("contacts.contact_updated"));
|
||||
await this.getContacts();
|
||||
|
||||
@@ -84,7 +84,8 @@
|
||||
class="text-blue-500 hover:underline"
|
||||
@click="onIFACSignatureClick(iface._stats.ifac_signature)"
|
||||
>
|
||||
{{ iface._stats.ifac_signature.slice(0, 8) }}…{{ iface._stats.ifac_signature.slice(-8) }}
|
||||
<span class="font-mono">{{ iface._stats.ifac_signature.slice(0, 8) }}</span
|
||||
>…<span class="font-mono">{{ iface._stats.ifac_signature.slice(-8) }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -258,6 +259,9 @@ export default {
|
||||
if (this.iface.type === "AutoInterface") {
|
||||
return "Auto-detect Ethernet and Wi-Fi peers";
|
||||
}
|
||||
if (this.iface.type === "BackboneInterface") {
|
||||
return "Backbone (IFAC tunnel)";
|
||||
}
|
||||
return this.iface.description || "Custom interface";
|
||||
},
|
||||
statusChipClass() {
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
@click="refreshCommunityInterfaces"
|
||||
>
|
||||
<MaterialDesignIcon
|
||||
icon-name="refresh"
|
||||
icon-name="download"
|
||||
class="w-4 h-4"
|
||||
:class="{ 'animate-spin-reverse': refreshingCommunityInterfaces }"
|
||||
/>
|
||||
|
||||
@@ -1,39 +1,45 @@
|
||||
<!-- SPDX-License-Identifier: 0BSD -->
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col flex-1 overflow-hidden min-w-0 bg-slate-50 dark:bg-zinc-950">
|
||||
<div
|
||||
class="flex-1 overflow-y-auto w-full px-4 md:px-5 lg:px-8 py-6 pb-[max(1.5rem,env(safe-area-inset-bottom))]"
|
||||
>
|
||||
<div class="space-y-4 w-full max-w-5xl mx-auto">
|
||||
<div class="glass-card space-y-5">
|
||||
<div class="flex flex-col lg:flex-row lg:items-center justify-between gap-4">
|
||||
<div class="space-y-2">
|
||||
<div class="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
Reticulum Content Serving
|
||||
</div>
|
||||
<div class="text-2xl font-semibold text-gray-900 dark:text-white">Mesh Server</div>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-300">
|
||||
Serve Micron pages and files directly over the Reticulum mesh. Each server gets its own
|
||||
identity and destination address.
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-col flex-1 overflow-hidden min-w-0 bg-linear-to-br from-slate-50 via-slate-100 to-white dark:from-zinc-950 dark:via-zinc-900 dark:to-zinc-900"
|
||||
>
|
||||
<div class="flex-1 overflow-y-auto overflow-x-hidden w-full px-3 sm:px-5 md:px-5 lg:px-8 py-3 sm:py-4 min-w-0">
|
||||
<div class="space-y-0 w-full max-w-6xl xl:max-w-7xl mx-auto min-w-0">
|
||||
<div
|
||||
class="w-full border-b border-gray-200/60 dark:border-zinc-800/60 py-4 sm:py-6 flex flex-col lg:flex-row lg:items-center justify-between gap-4"
|
||||
>
|
||||
<div class="space-y-1">
|
||||
<div class="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
Reticulum Content Serving
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="primary-chip px-4 py-2 text-sm shrink-0"
|
||||
@click="showCreateDialog = true"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="plus" class="w-4 h-4" />
|
||||
Create Node
|
||||
</button>
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white tracking-tight">Mesh Server</h1>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-300 max-w-2xl">
|
||||
Serve Micron pages and files directly over the Reticulum mesh. Each server gets its own
|
||||
identity and destination address.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="primary-chip px-4 py-2 text-sm shrink-0"
|
||||
@click="showCreateDialog = true"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="plus" class="w-4 h-4" />
|
||||
Create Node
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="glass-card text-center py-12">
|
||||
<div
|
||||
v-if="loading"
|
||||
class="w-full border-b border-gray-200/60 dark:border-zinc-800/60 py-8 sm:py-12 text-center"
|
||||
>
|
||||
<div class="text-gray-500 dark:text-gray-400">Loading nodes...</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="nodes.length === 0" class="glass-card text-center py-12">
|
||||
<div
|
||||
v-else-if="nodes.length === 0"
|
||||
class="w-full border-b border-gray-200/60 dark:border-zinc-800/60 py-8 sm:py-12 text-center"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="server-network" class="w-12 h-12 mx-auto mb-4 text-gray-400" />
|
||||
<div class="text-gray-600 dark:text-gray-400 mb-2">No mesh servers yet</div>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-500">
|
||||
@@ -41,64 +47,66 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-3">
|
||||
<div v-else class="w-full divide-y divide-gray-200/60 dark:divide-zinc-800/60">
|
||||
<div
|
||||
v-for="node in nodes"
|
||||
:key="node.node_id"
|
||||
class="glass-card space-y-4 cursor-pointer hover:border-blue-400 dark:hover:border-blue-500 transition"
|
||||
class="py-3 sm:py-4 space-y-2 cursor-pointer hover:bg-black/5 dark:hover:bg-white/5 transition-colors rounded-lg -mx-3 sm:-mx-4 px-3 sm:px-4"
|
||||
@click="selectNode(node)"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
<div
|
||||
class="w-3 h-3 rounded-full"
|
||||
class="w-3 h-3 rounded-full shrink-0"
|
||||
:class="node.running ? 'bg-green-500' : 'bg-gray-400'"
|
||||
></div>
|
||||
<div>
|
||||
<div class="font-semibold text-gray-900 dark:text-white">{{ node.name }}</div>
|
||||
<div class="min-w-0">
|
||||
<div class="font-semibold text-gray-900 dark:text-white truncate">
|
||||
{{ node.name }}
|
||||
</div>
|
||||
<div
|
||||
v-if="node.destination_hash"
|
||||
class="text-xs font-mono text-gray-500 dark:text-gray-400"
|
||||
class="text-xs font-mono text-gray-500 dark:text-gray-400 truncate"
|
||||
>
|
||||
{{ node.destination_hash }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ node.pages.length }} pages, {{ node.files.length }} files
|
||||
<div class="flex items-center gap-1.5 shrink-0 flex-wrap justify-end">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400 mr-1">
|
||||
{{ node.pages.length }}p / {{ node.files.length }}f
|
||||
</span>
|
||||
<button
|
||||
v-if="!node.running"
|
||||
class="primary-chip py-1! px-3! text-xs!"
|
||||
class="primary-chip py-1! px-2.5! text-xs!"
|
||||
@click.stop="startNode(node.node_id)"
|
||||
>
|
||||
Start
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
class="secondary-chip py-1! px-3! text-xs! text-red-500! hover:bg-red-50! dark:hover:bg-red-900/20!"
|
||||
class="secondary-chip py-1! px-2.5! text-xs! text-red-500! hover:bg-red-50! dark:hover:bg-red-900/20!"
|
||||
@click.stop="stopNode(node.node_id)"
|
||||
>
|
||||
Stop
|
||||
</button>
|
||||
<button
|
||||
v-if="node.running"
|
||||
class="secondary-chip py-1! px-3! text-xs!"
|
||||
class="secondary-chip py-1! px-2.5! text-xs!"
|
||||
@click.stop="announceNode(node.node_id)"
|
||||
>
|
||||
Announce
|
||||
</button>
|
||||
<button
|
||||
v-if="node.running && node.destination_hash"
|
||||
class="secondary-chip py-1! px-3! text-xs!"
|
||||
class="secondary-chip py-1! px-2.5! text-xs!"
|
||||
@click.stop="viewNode(node)"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="eye" class="w-3.5 h-3.5" />
|
||||
View
|
||||
</button>
|
||||
<button
|
||||
class="secondary-chip py-1! px-3! text-xs! text-red-500! hover:bg-red-50! dark:hover:bg-red-900/20!"
|
||||
class="secondary-chip py-1! px-2.5! text-xs! text-red-500! hover:bg-red-50! dark:hover:bg-red-900/20!"
|
||||
@click.stop="deleteNode(node.node_id)"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="delete" class="w-3.5 h-3.5" />
|
||||
@@ -108,19 +116,22 @@
|
||||
|
||||
<div
|
||||
v-if="node.stats || node.running"
|
||||
class="flex flex-wrap gap-x-4 gap-y-1 text-xs text-gray-500 dark:text-gray-400"
|
||||
class="flex flex-wrap gap-x-4 gap-y-1 text-xs text-gray-500 dark:text-gray-400 pl-6"
|
||||
>
|
||||
<span v-if="node.running">{{ formatMeshUptime(node.uptime_seconds) }} uptime</span>
|
||||
<span>{{ node.unique_connections ?? 0 }} unique connections</span>
|
||||
<span v-if="node.stats">{{ node.stats.pages_served }} pages served</span>
|
||||
<span v-if="node.stats">{{ node.stats.files_served }} files served</span>
|
||||
<span>{{ node.unique_connections ?? 0 }} connections</span>
|
||||
<span v-if="node.stats">{{ node.stats.pages_served }} pages</span>
|
||||
<span v-if="node.stats">{{ node.stats.files_served }} files</span>
|
||||
<span v-if="node.stats">{{ node.stats.links_established }} links</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Selected Node Detail -->
|
||||
<div v-if="selectedNode" class="glass-card space-y-5">
|
||||
<div
|
||||
v-if="selectedNode"
|
||||
class="w-full py-4 sm:py-6 space-y-4 border-t border-gray-200/60 dark:border-zinc-800/60"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{{ selectedNode.name }}
|
||||
@@ -154,13 +165,13 @@
|
||||
</div>
|
||||
|
||||
<!-- Tabs: Pages / Files -->
|
||||
<div class="flex gap-2 border-b border-gray-200 dark:border-zinc-700">
|
||||
<div class="flex gap-2 border-b border-gray-200/60 dark:border-zinc-800/60">
|
||||
<button
|
||||
:class="[
|
||||
detailTab === 'pages'
|
||||
? 'border-b-2 border-blue-500 text-blue-600 dark:text-blue-400'
|
||||
: 'text-gray-600 dark:text-gray-400',
|
||||
'px-4 py-2 font-semibold transition text-sm',
|
||||
'px-4 py-2 font-semibold transition text-sm -mb-px',
|
||||
]"
|
||||
@click="detailTab = 'pages'"
|
||||
>
|
||||
@@ -171,7 +182,7 @@
|
||||
detailTab === 'files'
|
||||
? 'border-b-2 border-blue-500 text-blue-600 dark:text-blue-400'
|
||||
: 'text-gray-600 dark:text-gray-400',
|
||||
'px-4 py-2 font-semibold transition text-sm',
|
||||
'px-4 py-2 font-semibold transition text-sm -mb-px',
|
||||
]"
|
||||
@click="detailTab = 'files'"
|
||||
>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
<!-- SPDX-License-Identifier: 0BSD -->
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col flex-1 overflow-hidden min-w-0 bg-slate-50 dark:bg-zinc-950">
|
||||
<div class="flex-1 overflow-y-auto w-full pb-[max(1rem,env(safe-area-inset-bottom))]">
|
||||
<div class="p-3 sm:p-4 md:p-6 max-w-6xl mx-auto w-full space-y-4 min-w-0">
|
||||
<div
|
||||
class="flex flex-col flex-1 overflow-hidden min-w-0 bg-linear-to-br from-slate-50 via-slate-100 to-white dark:from-zinc-950 dark:via-zinc-900 dark:to-zinc-900"
|
||||
>
|
||||
<div class="flex-1 overflow-y-auto overflow-x-hidden w-full px-3 sm:px-5 md:px-5 lg:px-8 py-3 sm:py-4 min-w-0">
|
||||
<div class="space-y-0 w-full max-w-6xl xl:max-w-7xl mx-auto min-w-0">
|
||||
<div
|
||||
class="flex flex-wrap items-start justify-between gap-3 border-b border-gray-200 dark:border-zinc-800 pb-4"
|
||||
class="w-full border-b border-gray-200/60 dark:border-zinc-800/60 py-4 sm:py-6 flex flex-wrap items-start justify-between gap-3"
|
||||
>
|
||||
<div class="flex items-start gap-3 min-w-0">
|
||||
<div
|
||||
@@ -13,14 +15,14 @@
|
||||
>
|
||||
<MaterialDesignIcon icon-name="package-variant" class="size-6" />
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<div class="min-w-0 space-y-1">
|
||||
<div class="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
{{ $t("tools.utilities") }}
|
||||
</div>
|
||||
<h1 class="text-lg sm:text-xl font-bold text-gray-900 dark:text-white tracking-tight">
|
||||
{{ $t("tools.repository_server.title") }}
|
||||
</h1>
|
||||
<p class="text-xs sm:text-sm text-gray-600 dark:text-gray-400 mt-1 max-w-2xl">
|
||||
<p class="text-xs sm:text-sm text-gray-600 dark:text-gray-400 max-w-2xl">
|
||||
{{ $t("tools.repository_server.subtitle") }}
|
||||
</p>
|
||||
</div>
|
||||
@@ -28,7 +30,7 @@
|
||||
<div class="flex items-center gap-2 shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center p-2 rounded-lg border border-gray-200 dark:border-zinc-700 bg-white dark:bg-zinc-900 text-gray-600 dark:text-zinc-300 hover:bg-gray-50 dark:hover:bg-zinc-800 transition-colors"
|
||||
class="secondary-chip p-2!"
|
||||
:title="$t('tools.repository_server.refresh_bundled_tooltip')"
|
||||
:disabled="refreshing"
|
||||
@click="refreshBundled"
|
||||
@@ -37,7 +39,7 @@
|
||||
</button>
|
||||
<RouterLink
|
||||
to="/tools"
|
||||
class="inline-flex items-center gap-2 text-sm text-sky-600 dark:text-sky-300 hover:underline"
|
||||
class="inline-flex items-center gap-1 text-sm text-sky-600 dark:text-sky-300 hover:underline"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="arrow-left" class="size-4" />
|
||||
{{ $t("tools.back_to_tools") }}
|
||||
@@ -47,7 +49,7 @@
|
||||
|
||||
<div
|
||||
v-if="refreshing"
|
||||
class="rounded-xl border border-gray-200 dark:border-zinc-800 bg-white dark:bg-zinc-950 px-3 py-2.5 space-y-1.5"
|
||||
class="w-full border-b border-gray-200/60 dark:border-zinc-800/60 py-4 sm:py-6 space-y-2"
|
||||
>
|
||||
<div class="h-1.5 bg-gray-200 dark:bg-zinc-700 rounded-full overflow-hidden">
|
||||
<div
|
||||
@@ -60,9 +62,7 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="rounded-xl border border-gray-200 dark:border-zinc-800 bg-white dark:bg-zinc-950 p-4 space-y-3"
|
||||
>
|
||||
<div class="w-full border-b border-gray-200/60 dark:border-zinc-800/60 py-4 sm:py-6 space-y-3">
|
||||
<h2 class="text-sm font-semibold text-gray-900 dark:text-white">
|
||||
{{ $t("tools.repository_server.http_heading") }}
|
||||
</h2>
|
||||
@@ -140,62 +140,70 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="rounded-xl border border-gray-200 dark:border-zinc-800 bg-white dark:bg-zinc-950 p-4 space-y-3"
|
||||
>
|
||||
<div class="w-full border-b border-gray-200/60 dark:border-zinc-800/60 py-4 sm:py-6 space-y-3">
|
||||
<h2 class="text-sm font-semibold text-gray-900 dark:text-white">
|
||||
{{ $t("tools.repository_server.upload_heading") }}
|
||||
</h2>
|
||||
<label
|
||||
class="inline-flex items-center gap-2 px-3 py-2 rounded-lg bg-sky-600 text-white text-sm font-medium cursor-pointer hover:bg-sky-700 transition-colors"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="upload" class="size-4" />
|
||||
{{ $t("tools.repository_server.choose_file") }}
|
||||
<input type="file" class="hidden" @change="onUpload" />
|
||||
</label>
|
||||
<p v-if="lastUploadError" class="text-xs text-red-600 dark:text-red-400">{{ lastUploadError }}</p>
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<label
|
||||
class="inline-flex items-center gap-2 px-3 py-2 rounded-lg bg-sky-600 text-white text-sm font-medium cursor-pointer hover:bg-sky-700 transition-colors"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="upload" class="size-4" />
|
||||
{{ $t("tools.repository_server.choose_file") }}
|
||||
<input type="file" class="hidden" @change="onUpload" />
|
||||
</label>
|
||||
<p v-if="lastUploadError" class="text-xs text-red-600 dark:text-red-400">
|
||||
{{ lastUploadError }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="status?.last_refresh_failed && Object.keys(status.last_refresh_failed).length"
|
||||
class="rounded-xl border border-amber-200 dark:border-amber-900/40 bg-amber-50 dark:bg-amber-950/30 p-3 text-xs text-amber-900 dark:text-amber-200"
|
||||
>
|
||||
<div class="font-semibold mb-1">{{ $t("tools.repository_server.refresh_partial") }}</div>
|
||||
<ul class="list-disc pl-4 space-y-1">
|
||||
<li v-for="(msg, pkg) in status.last_refresh_failed" :key="pkg">
|
||||
<span class="font-mono">{{ pkg }}</span
|
||||
>: {{ msg }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="rounded-xl border border-gray-200 dark:border-zinc-800 bg-white dark:bg-zinc-950 overflow-hidden"
|
||||
class="w-full border-b border-gray-200/60 dark:border-zinc-800/60 py-4 sm:py-6"
|
||||
>
|
||||
<div
|
||||
class="px-4 py-3 border-b border-gray-100 dark:border-zinc-800 flex justify-between items-center"
|
||||
class="rounded-xl border border-amber-200 dark:border-amber-900/40 bg-amber-50 dark:bg-amber-950/30 p-3 text-xs text-amber-900 dark:text-amber-200"
|
||||
>
|
||||
<div class="font-semibold mb-1">{{ $t("tools.repository_server.refresh_partial") }}</div>
|
||||
<ul class="list-disc pl-4 space-y-1">
|
||||
<li v-for="(msg, pkg) in status.last_refresh_failed" :key="pkg">
|
||||
<span class="font-mono">{{ pkg }}</span
|
||||
>: {{ msg }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full py-4 sm:py-6 space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-sm font-semibold text-gray-900 dark:text-white">
|
||||
{{ $t("tools.repository_server.files_heading") }}
|
||||
</h2>
|
||||
<span class="text-xs text-gray-500">{{ entries.length }}</span>
|
||||
</div>
|
||||
<div v-if="loading" class="p-6 text-center text-sm text-gray-500">
|
||||
<div v-if="loading" class="text-center text-sm text-gray-500 py-6">
|
||||
{{ $t("common.loading") }}
|
||||
</div>
|
||||
<div v-else-if="entries.length === 0" class="p-6 text-center text-sm text-gray-500">
|
||||
<div v-else-if="entries.length === 0" class="text-center text-sm text-gray-500 py-6">
|
||||
{{ $t("tools.repository_server.empty") }}
|
||||
</div>
|
||||
<table v-else class="w-full text-left text-xs">
|
||||
<thead class="bg-gray-50 dark:bg-zinc-900/50 text-gray-500 uppercase tracking-wide">
|
||||
<thead
|
||||
class="text-gray-500 uppercase tracking-wide border-b border-gray-200/60 dark:border-zinc-800/60"
|
||||
>
|
||||
<tr>
|
||||
<th class="px-4 py-2">{{ $t("tools.repository_server.col_name") }}</th>
|
||||
<th class="px-4 py-2">{{ $t("tools.repository_server.col_source") }}</th>
|
||||
<th class="px-4 py-2 text-right">{{ $t("tools.repository_server.col_size") }}</th>
|
||||
<th class="px-4 py-2 w-24"></th>
|
||||
<th class="px-4 py-2 font-semibold">{{ $t("tools.repository_server.col_name") }}</th>
|
||||
<th class="px-4 py-2 font-semibold">{{ $t("tools.repository_server.col_source") }}</th>
|
||||
<th class="px-4 py-2 font-semibold text-right">
|
||||
{{ $t("tools.repository_server.col_size") }}
|
||||
</th>
|
||||
<th class="px-4 py-2 font-semibold w-24"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-100 dark:divide-zinc-800 text-gray-800 dark:text-zinc-200">
|
||||
<tbody
|
||||
class="divide-y divide-gray-100 dark:divide-zinc-800/50 text-gray-800 dark:text-zinc-200"
|
||||
>
|
||||
<tr v-for="row in entries" :key="row.name + row.source">
|
||||
<td class="px-4 py-2 font-mono break-all">{{ row.name }}</td>
|
||||
<td class="px-4 py-2">{{ row.source }}</td>
|
||||
|
||||
Reference in New Issue
Block a user