Fixed responsiveness

This commit is contained in:
litoral05
2026-05-12 11:59:54 +01:00
parent 7c04ea5b2e
commit 17364e23d3
12 changed files with 1240 additions and 1166 deletions
+3 -3
View File
@@ -15,9 +15,9 @@
"title": "Litoral Regas VPN Orchestrator",
"width": 1440,
"height": 980,
"minWidth": 1100,
"minHeight": 720,
"resizable": false,
"minWidth": 900,
"minHeight": 650,
"resizable": true,
"maximized": true
}
],
+20 -20
View File
@@ -101,18 +101,17 @@ export function DashboardRoute() {
udp2rawHealthy;
return (
<div className="flex h-full flex-col overflow-hidden">
<div className="flex min-h-full flex-col">
<TopBar healthy={dashboardReady} />
{error && (
<div className="mb-4 rounded-2xl border border-red-500/20 bg-red-500/10 p-4 text-sm text-red-300">
Erro no backend do painel:{' '}
{error}
Erro no backend do painel: {error}
</div>
)}
<div className="grid grid-cols-12 gap-4">
<div className="col-span-3">
<div className="grid gap-4 lg:grid-cols-2 2xl:grid-cols-12">
<div className="2xl:col-span-3">
<MetricCard
title="VPN"
value={vpnHealthy ? 'Ligada' : 'Offline'}
@@ -126,7 +125,7 @@ export function DashboardRoute() {
/>
</div>
<div className="col-span-3">
<div className="2xl:col-span-3">
<MetricCard
title="UDP2RAW"
value={udp2rawHealthy ? 'Ativo' : 'Offline'}
@@ -136,7 +135,7 @@ export function DashboardRoute() {
/>
</div>
<div className="col-span-2">
<div className="2xl:col-span-2">
<MetricCard
title="Pool IP"
value={`${ipPoolPercent}%`}
@@ -146,7 +145,7 @@ export function DashboardRoute() {
/>
</div>
<div className="col-span-2">
<div className="2xl:col-span-2">
<MetricCard
title="Disco"
value={`${health?.diskUsagePercent ?? 0}%`}
@@ -160,7 +159,7 @@ export function DashboardRoute() {
/>
</div>
<div className="col-span-2">
<div className="lg:col-span-2 2xl:col-span-2">
<MetricCard
title="Backend"
value={backendHealthy ? 'Saudável' : 'Offline'}
@@ -171,8 +170,8 @@ export function DashboardRoute() {
</div>
</div>
<div className="mt-4 grid min-h-0 flex-1 grid-cols-12 gap-4 overflow-hidden pb-4">
<div className="col-span-7 min-h-0">
<div className="mt-4 grid gap-4 2xl:grid-cols-12">
<div className="min-h-[360px] 2xl:col-span-7">
<NetworkTrafficChart
title="Tráfego WireGuard"
subtitle="Débito RX/TX wg0 em direto"
@@ -180,15 +179,17 @@ export function DashboardRoute() {
/>
</div>
<div className="col-span-3 grid min-h-0 grid-rows-2 gap-4">
<div className="min-h-[360px] 2xl:col-span-5">
<NetworkTrafficChart
title="Tráfego UDP2RAW"
subtitle="Faketcp na porta 444"
mode="udp2raw"
compact
/>
</div>
</div>
<Card className="overflow-hidden">
<div className="mt-4 grid gap-4 pb-4 xl:grid-cols-12">
<Card className="min-h-[260px] xl:col-span-5">
<div className="flex h-full flex-col justify-between">
<div>
<div className="mb-4 flex items-center gap-3">
@@ -196,7 +197,7 @@ export function DashboardRoute() {
<Clock size={20} />
</div>
<div>
<div className="min-w-0">
<h3 className="font-semibold text-white">
Estado da VPS
</h3>
@@ -207,15 +208,15 @@ export function DashboardRoute() {
</div>
</div>
<p className="text-2xl font-black leading-tight text-white">
<p className="text-xl font-black leading-tight text-white 2xl:text-2xl">
{health?.systemUptime ?? 'Desconhecido'}
</p>
<p className="mt-3 font-mono text-xs text-slate-400">
<p className="mt-3 break-all font-mono text-xs text-slate-400">
Load: {health?.loadAverage ?? '—'}
</p>
<p className="mt-2 font-mono text-xs text-slate-500">
<p className="mt-2 break-all font-mono text-xs text-slate-500">
IP: {health?.publicIp ?? '—'}
</p>
</div>
@@ -240,9 +241,8 @@ export function DashboardRoute() {
</div>
</div>
</Card>
</div>
<div className="col-span-2 min-h-0">
<div className="min-h-[340px] xl:col-span-7">
<IpPoolChart
used={usedCount}
total={ipPoolTotal}
+45 -37
View File
@@ -54,6 +54,9 @@ const sourceLabels: Record<'all' | ActivityLogSource, string> = {
vps: 'VPS',
};
const scrollClass =
'[scrollbar-width:thin] [scrollbar-color:rgba(59,130,246,0.45)_transparent] [&::-webkit-scrollbar]:h-2 [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar-track]:bg-transparent [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-blue-500/30 hover:[&::-webkit-scrollbar-thumb]:bg-blue-500/50';
function levelTone(level: ActivityLogLevel) {
if (level === 'success') return 'green';
if (level === 'warning') return 'purple';
@@ -132,26 +135,23 @@ export function ActivityLogs() {
}, [logs, levelFilter, sourceFilter, query]);
return (
<div className="flex h-full flex-col overflow-hidden">
<div className="mb-5 flex items-center justify-between">
<div>
<h2 className="text-3xl font-bold tracking-tight text-white">
<div className="flex min-h-full flex-col">
<div className="mb-5 flex flex-wrap items-start justify-between gap-4 rounded-3xl border border-white/10 bg-white/[0.025] px-5 py-4">
<div className="min-w-0">
<h2 className="text-3xl font-black tracking-tight text-white">
Registos de Atividade
</h2>
<p className="mt-1 text-slate-400">
Histórico local de auditoria de
provisionamento para técnicos.
<p className="mt-1 max-w-2xl text-sm leading-6 text-slate-400">
Histórico local de auditoria de provisionamento para técnicos.
</p>
</div>
<div className="flex items-center gap-3">
<div className="flex flex-wrap items-center gap-3">
<Button
type="button"
variant="secondary"
onClick={() => {
exportActivityLogs();
}}
onClick={exportActivityLogs}
>
<Download size={16} />
Exportar JSON
@@ -172,8 +172,8 @@ export function ActivityLogs() {
</div>
<Card className="relative z-30 mb-4 overflow-visible">
<div className="grid grid-cols-12 gap-4">
<div className="col-span-6">
<div className="grid gap-4 xl:grid-cols-12">
<div className="xl:col-span-6">
<label className="mb-2 block text-xs font-medium uppercase tracking-wide text-slate-500">
Pesquisa
</label>
@@ -181,7 +181,7 @@ export function ActivityLogs() {
<div className="flex items-center gap-2 rounded-xl border border-white/10 bg-ink-950 px-3">
<Search
size={16}
className="text-slate-500"
className="shrink-0 text-slate-500"
/>
<input
@@ -190,12 +190,13 @@ export function ActivityLogs() {
setQuery(event.target.value)
}
placeholder="Pesquisar ação, IP VPN, IP router, mensagem..."
className="w-full bg-transparent py-3 text-sm text-white outline-none placeholder:text-slate-600"
className="w-full min-w-0 bg-transparent py-3 text-sm text-white outline-none placeholder:text-slate-600"
/>
</div>
</div>
<div className="col-span-3">
<div className="grid gap-4 sm:grid-cols-2 xl:col-span-6">
<div>
<label className="mb-2 block text-xs font-medium uppercase tracking-wide text-slate-500">
Nível
</label>
@@ -210,7 +211,7 @@ export function ActivityLogs() {
/>
</div>
<div className="col-span-3">
<div>
<label className="mb-2 block text-xs font-medium uppercase tracking-wide text-slate-500">
Origem
</label>
@@ -225,10 +226,11 @@ export function ActivityLogs() {
/>
</div>
</div>
</div>
</Card>
<Card className="relative z-10 flex min-h-0 flex-1 flex-col overflow-hidden">
<div className="mb-4 flex items-center justify-between">
<Card className="relative z-10 flex h-[520px] flex-col overflow-hidden">
<div className="mb-4 flex flex-wrap items-center justify-between gap-3">
<h3 className="font-semibold text-white">
Eventos de Auditoria
</h3>
@@ -239,30 +241,25 @@ export function ActivityLogs() {
</span>
</div>
<div className="min-h-0 flex-1 overflow-y-auto rounded-xl border border-white/10
[scrollbar-width:thin]
[scrollbar-color:rgba(59,130,246,0.45)_transparent]
[&::-webkit-scrollbar]:w-2
[&::-webkit-scrollbar-track]:bg-transparent
[&::-webkit-scrollbar-thumb]:rounded-full
[&::-webkit-scrollbar-thumb]:bg-blue-500/30
hover:[&::-webkit-scrollbar-thumb]:bg-blue-500/50">
<table className="w-full table-fixed text-sm">
<div
className={`min-h-0 flex-1 overflow-auto rounded-xl border border-white/10 ${scrollClass}`}
>
<table className="min-w-[920px] w-full table-fixed text-sm">
<thead className="sticky top-0 z-10 bg-slate-950 text-left text-xs uppercase tracking-wide text-slate-500">
<tr>
<th className="w-[180px] px-4 py-3">
<th className="w-[135px] px-4 py-3">
Hora
</th>
<th className="w-[110px] px-4 py-3">
<th className="w-[100px] px-4 py-3">
Nível
</th>
<th className="w-[120px] px-4 py-3">
<th className="w-[110px] px-4 py-3">
Origem
</th>
<th className="w-[190px] px-4 py-3">
<th className="w-[160px] px-4 py-3">
Ação
</th>
@@ -270,7 +267,11 @@ export function ActivityLogs() {
Mensagem
</th>
<th className="w-[160px] px-4 py-3">
<th className="w-[135px] px-4 py-3">
Router
</th>
<th className="w-[130px] px-4 py-3">
IP VPN
</th>
</tr>
@@ -285,7 +286,7 @@ export function ActivityLogs() {
<td className="px-4 py-3 font-mono text-xs text-slate-400">
{new Date(
log.timestamp,
).toLocaleString()}
).toLocaleTimeString()}
</td>
<td className="px-4 py-3">
@@ -298,14 +299,21 @@ export function ActivityLogs() {
{sourceLabels[log.source]}
</td>
<td className="truncate px-4 py-3 font-medium text-white">
<td className="truncate px-4 py-3 font-semibold text-white">
{log.action}
</td>
<td className="truncate px-4 py-3 text-slate-400">
<td
title={log.message}
className="truncate px-4 py-3 text-slate-400"
>
{log.message}
</td>
<td className="px-4 py-3 font-mono text-slate-300">
{log.routerIp ?? '—'}
</td>
<td className="px-4 py-3 font-mono text-slate-300">
{log.vpnIp ?? '—'}
</td>
@@ -315,7 +323,7 @@ export function ActivityLogs() {
{filteredLogs.length === 0 && (
<tr>
<td
colSpan={6}
colSpan={7}
className="px-4 py-10 text-center text-slate-500"
>
Nenhum registo de atividade encontrado.
+23 -19
View File
@@ -38,26 +38,26 @@ export function IpPoolChart({
];
return (
<Card className="flex h-full flex-col">
<div>
<Card className="flex h-full min-h-[380px] flex-col overflow-hidden">
<div className="shrink-0">
<h3 className="font-semibold text-white">
Uso da Pool IP
</h3>
<p className="text-xs text-slate-500">
<p className="mt-1 text-xs text-slate-500">
Capacidade de atribuição WireGuard
</p>
</div>
<div className="flex min-h-0 flex-1 flex-col justify-center">
<div className="relative mx-auto h-56 w-56">
<div className="relative mx-auto h-40 w-40 sm:h-48 sm:w-48 xl:h-44 xl:w-44 2xl:h-56 2xl:w-56">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={data}
dataKey="value"
innerRadius={76}
outerRadius={100}
innerRadius="72%"
outerRadius="92%"
startAngle={90}
endAngle={-270}
paddingAngle={2}
@@ -77,7 +77,7 @@ export function IpPoolChart({
</ResponsiveContainer>
<div className="absolute inset-0 flex flex-col items-center justify-center">
<p className="text-4xl font-bold text-white">
<p className="text-3xl font-black text-white 2xl:text-4xl">
{displayPercentage}%
</p>
@@ -87,31 +87,35 @@ export function IpPoolChart({
</div>
</div>
<div className="mt-8 space-y-4">
<div className="rounded-2xl border border-white/10 bg-white/[0.03] p-4">
<p className="text-xs uppercase tracking-wide text-slate-500">
IPs Usados
<div className="mt-5 grid gap-3 2xl:mt-8">
<div className="grid grid-cols-2 gap-3">
<div className="rounded-2xl border border-white/10 bg-white/[0.03] p-3 2xl:p-4">
<p className="text-[10px] font-semibold uppercase tracking-wide text-slate-500">
Usados
</p>
<p className="mt-1 text-2xl font-bold text-green-300">
<p className="mt-1 truncate text-xl font-black text-green-300 2xl:text-2xl">
{used}
</p>
</div>
<div className="rounded-2xl border border-white/10 bg-white/[0.03] p-4">
<p className="text-xs uppercase tracking-wide text-slate-500">
IPs Disponíveis
<div className="rounded-2xl border border-white/10 bg-white/[0.03] p-3 2xl:p-4">
<p className="text-[10px] font-semibold uppercase tracking-wide text-slate-500">
Livres
</p>
<p className="mt-1 text-2xl font-bold text-blue-300">
<p className="mt-1 truncate text-xl font-black text-blue-300 2xl:text-2xl">
{available}
</p>
</div>
</div>
<div className="rounded-2xl border border-white/10 bg-white/[0.03] p-4">
<div className="mb-2 flex justify-between text-xs text-slate-500">
<div className="rounded-2xl border border-white/10 bg-white/[0.03] p-3 2xl:p-4">
<div className="mb-2 flex justify-between gap-3 text-xs text-slate-500">
<span>Capacidade</span>
<span>{used} / {total}</span>
<span className="truncate">
{used} / {total}
</span>
</div>
<div className="h-2 overflow-hidden rounded-full bg-slate-800">
+7 -10
View File
@@ -29,19 +29,16 @@ const toneStyles: Record<
iconText: 'text-blue-300',
glow: 'from-blue-500/20',
},
green: {
iconBg: 'bg-emerald-500/10',
iconText: 'text-emerald-300',
glow: 'from-emerald-500/20',
},
purple: {
iconBg: 'bg-violet-500/10',
iconText: 'text-violet-300',
glow: 'from-violet-500/20',
},
red: {
iconBg: 'bg-red-500/10',
iconText: 'text-red-300',
@@ -59,30 +56,30 @@ export function MetricCard({
const style = toneStyles[tone];
return (
<Card className="group relative h-full overflow-hidden border border-white/5 bg-[#07111f]/90 transition-all duration-300 hover:border-white/10 hover:bg-[#0a1728]">
<Card className="group relative h-full min-h-[118px] overflow-hidden border border-white/5 bg-[#07111f]/90 transition-all duration-300 hover:border-white/10 hover:bg-[#0a1728]">
<div
className={`absolute inset-x-0 top-0 h-px bg-gradient-to-r ${style.glow} via-white/40 to-transparent opacity-70`}
/>
<div className="absolute -right-10 -top-10 h-32 w-32 rounded-full bg-white/[0.02] blur-3xl transition-all duration-500 group-hover:scale-125" />
<div className="absolute -right-10 -top-10 h-32 w-32 rounded-full bg-white/[0.025] blur-3xl transition-all duration-500 group-hover:scale-125" />
<div className="relative flex items-start gap-4">
<div className="relative flex h-full items-start gap-4">
<div
className={`rounded-2xl ${style.iconBg} ${style.iconText} border border-white/5 p-3 shadow-lg backdrop-blur-xl`}
className={`shrink-0 rounded-2xl ${style.iconBg} ${style.iconText} border border-white/5 p-3 shadow-lg backdrop-blur-xl`}
>
{icon}
</div>
<div className="min-w-0 flex-1">
<p className="text-sm font-medium text-slate-400">
<p className="truncate text-sm font-medium text-slate-400">
{title}
</p>
<h3 className="mt-1 text-3xl font-black tracking-tight text-white">
<h3 className="mt-1 truncate text-2xl font-black tracking-tight text-white 2xl:text-3xl">
{value}
</h3>
<p className="mt-2 truncate text-xs text-slate-500">
<p className="mt-2 line-clamp-2 text-xs leading-5 text-slate-500">
{subtitle}
</p>
</div>
@@ -104,7 +104,7 @@ export function NetworkTrafficChart({
points[points.length - 1];
return (
<Card className="relative h-full overflow-hidden">
<Card className="relative h-full min-h-[240px] overflow-hidden">
<div className="pointer-events-none absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-blue-400/30 to-transparent" />
<div className="mb-4 flex items-start justify-between gap-4">
@@ -131,22 +131,22 @@ export function NetworkTrafficChart({
{compact && latestPoint && (
<div className="mb-3 grid grid-cols-2 gap-2">
<div className="rounded-xl border border-white/10 bg-white/[0.025] p-3">
<div className="min-w-0 rounded-xl border border-white/10 bg-white/[0.025] p-3">
<p className="text-[10px] font-semibold uppercase tracking-wide text-slate-500">
Entrada
</p>
<p className="mt-1 font-mono text-sm font-bold text-green-300">
<p className="mt-1 truncate font-mono text-sm font-bold text-green-300">
{latestPoint.downloadMbps.toFixed(3)} Mbps
</p>
</div>
<div className="rounded-xl border border-white/10 bg-white/[0.025] p-3">
<div className="min-w-0 rounded-xl border border-white/10 bg-white/[0.025] p-3">
<p className="text-[10px] font-semibold uppercase tracking-wide text-slate-500">
Saída
</p>
<p className="mt-1 font-mono text-sm font-bold text-blue-300">
<p className="mt-1 truncate font-mono text-sm font-bold text-blue-300">
{latestPoint.uploadMbps.toFixed(3)} Mbps
</p>
</div>
@@ -156,8 +156,8 @@ export function NetworkTrafficChart({
<div
className={
compact
? 'h-[calc(100%-8.5rem)] min-h-[140px]'
: 'h-[calc(100%-4.5rem)] min-h-[360px]'
? 'h-[calc(100%-8.5rem)] min-h-[130px]'
: 'h-[calc(100%-4.5rem)] min-h-[320px]'
}
>
<ResponsiveContainer
@@ -171,13 +171,13 @@ export function NetworkTrafficChart({
? {
top: 8,
right: 8,
left: -28,
left: -30,
bottom: 0,
}
: {
top: 8,
right: 16,
left: 0,
left: -8,
bottom: 0,
}
}
@@ -191,14 +191,14 @@ export function NetworkTrafficChart({
dataKey="time"
stroke="#94a3b8"
fontSize={compact ? 10 : 12}
minTickGap={compact ? 32 : 24}
minTickGap={compact ? 36 : 24}
tick={compact ? false : undefined}
/>
<YAxis
stroke="#94a3b8"
fontSize={compact ? 10 : 12}
width={compact ? 42 : 60}
width={compact ? 42 : 56}
tickFormatter={(value) =>
`${value} Mbps`
}
+1 -1
View File
@@ -22,7 +22,7 @@ export function AppShell({
onLogout={onLogout}
/>
<main className="flex-1 overflow-hidden p-4">
<main className="min-w-0 flex-1 overflow-auto p-4">
{children}
</main>
</div>
+27 -13
View File
@@ -4,7 +4,6 @@ import {
LogOut,
RadioTower,
Settings,
Shield,
Wrench,
} from 'lucide-react';
@@ -30,8 +29,8 @@ export function Sidebar({
onLogout,
}: SidebarProps) {
return (
<aside className="flex h-screen w-64 flex-col border-r border-white/10 bg-ink-950 px-4 py-5">
<div className="mb-10 flex items-center gap-4 px-1">
<aside className="flex h-screen w-64 shrink-0 flex-col border-r border-white/10 bg-[#020817] px-4 py-5">
<div className="mb-8 flex items-center gap-4 px-1">
<img
src={logoIcon}
alt="Litoral Regas"
@@ -39,17 +38,17 @@ export function Sidebar({
/>
<div className="min-w-0">
<h1 className="truncate text-[20px] font-bold leading-none text-white">
<h1 className="truncate text-[28px] font-black leading-none text-white">
Litoral Regas
</h1>
<p className="mt-1 text-[11px] font-medium uppercase tracking-[0.12em] text-blue-300/70">
<p className="mt-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-blue-300/70">
VPN Orchestrator
</p>
</div>
</div>
<nav className="space-y-1">
<nav className="space-y-2">
{items.map(([label, Icon]) => {
const isActive = active === label;
@@ -58,13 +57,25 @@ export function Sidebar({
key={label}
type="button"
onClick={() => onSelect(label)}
className={`flex w-full items-center gap-3 rounded-xl px-3 py-3 text-left text-sm font-medium transition ${isActive
? 'bg-blue-500/15 text-blue-200'
: 'text-slate-300 hover:bg-white/5 hover:text-white'
className={`group flex w-full items-center gap-3 rounded-2xl px-4 py-3 text-left text-sm font-semibold transition-all duration-200 ${
isActive
? 'border border-blue-500/20 bg-blue-500/15 text-white shadow-[0_0_25px_rgba(59,130,246,0.12)]'
: 'border border-transparent text-slate-300 hover:border-white/5 hover:bg-white/[0.035] hover:text-white'
}`}
>
<Icon size={18} />
<div
className={`rounded-xl p-2 transition ${
isActive
? 'bg-blue-500/10 text-blue-300'
: 'bg-white/[0.03] text-slate-400 group-hover:text-slate-200'
}`}
>
<Icon size={17} />
</div>
<span className="truncate">
{label}
</span>
</button>
);
})}
@@ -74,10 +85,13 @@ export function Sidebar({
<button
type="button"
onClick={onLogout}
className="flex w-full items-center gap-3 rounded-xl px-3 py-3 text-left text-sm font-medium text-slate-300 transition hover:bg-red-500/10 hover:text-red-200"
className="group flex w-full items-center gap-3 rounded-2xl px-4 py-3 text-left text-sm font-semibold text-slate-300 transition-all duration-200 hover:bg-red-500/10 hover:text-red-200"
>
<LogOut size={18} />
Terminar Sessão
<div className="rounded-xl bg-white/[0.03] p-2 text-slate-400 transition group-hover:bg-red-500/10 group-hover:text-red-300">
<LogOut size={17} />
</div>
<span>Terminar Sessão</span>
</button>
</div>
</aside>
+50 -14
View File
@@ -1,4 +1,8 @@
import { RefreshCw, ShieldCheck, ShieldX } from 'lucide-react';
import {
RefreshCw,
ShieldCheck,
ShieldX,
} from 'lucide-react';
type TopBarProps = {
healthy?: boolean;
@@ -8,11 +12,17 @@ export function TopBar({
healthy = false,
}: TopBarProps) {
return (
<header className="mb-6 flex items-center justify-between rounded-3xl border border-white/10 bg-white/[0.025] px-5 py-4">
<div>
<div className="flex items-center gap-3">
<header
className="
mb-4 flex flex-col gap-4 rounded-3xl border border-white/10
bg-white/[0.025] px-4 py-4
lg:mb-6 lg:flex-row lg:items-center lg:justify-between lg:px-5
"
>
<div className="min-w-0">
<div className="flex items-start gap-3">
<div
className={`rounded-2xl p-3 ${
className={`shrink-0 rounded-2xl p-3 ${
healthy
? 'bg-emerald-500/10 text-emerald-300'
: 'bg-red-500/10 text-red-300'
@@ -25,23 +35,49 @@ export function TopBar({
)}
</div>
<div>
<h2 className="text-3xl font-black tracking-tight text-white">
<div className="min-w-0">
<h2
className="
truncate text-2xl font-black tracking-tight text-white
xl:text-3xl
"
>
Painel
</h2>
<p className="mt-1 text-sm text-slate-400">
Provisionamento de routers OpenWrt 23.05 com WireGuard e UDP2RAW
<p
className="
mt-1 max-w-3xl text-xs leading-5 text-slate-400
sm:text-sm
"
>
Provisionamento de routers OpenWrt 23.05
com WireGuard e UDP2RAW
</p>
</div>
</div>
</div>
<div className="flex items-center gap-3">
<div className="flex items-center gap-2 rounded-2xl border border-white/10 bg-black/20 px-4 py-2 text-sm text-slate-400">
<RefreshCw size={15} />
<div
className="
flex flex-wrap items-center gap-3
lg:justify-end
"
>
<div
className="
flex items-center gap-2 rounded-2xl
border border-white/10 bg-black/20
px-3 py-2 text-xs text-slate-400
sm:px-4 sm:text-sm
"
>
<RefreshCw
size={15}
className="shrink-0"
/>
<span>
<span className="whitespace-nowrap">
Atualizado às{' '}
<span className="font-mono text-slate-200">
{new Date().toLocaleTimeString()}
@@ -50,7 +86,7 @@ export function TopBar({
</div>
<div
className={`rounded-2xl border px-4 py-2 text-sm font-bold ${
className={`rounded-2xl border px-3 py-2 text-xs font-bold sm:px-4 sm:text-sm ${
healthy
? 'border-emerald-500/20 bg-emerald-500/10 text-emerald-300'
: 'border-red-500/20 bg-red-500/10 text-red-300'
@@ -19,7 +19,7 @@ import {
ShieldAlert,
Terminal,
UploadCloud,
Wifi
Wifi,
} from 'lucide-react';
import { Badge } from '@/components/ui/Badge';
@@ -139,7 +139,7 @@ const workflowSteps: Array<{
title: 'Registar Peer VPS',
description: 'Aplicar peer WireGuard na VPS.',
icon: Network,
}
},
];
const scrollClass =
@@ -1089,14 +1089,14 @@ export function ProvisioningWizard() {
}
return (
<div className="flex h-full flex-col overflow-hidden">
<div className="mb-5 flex items-start justify-between">
<div>
<h1 className="text-3xl font-bold tracking-tight text-white">
<div className="flex min-h-full flex-col">
<div className="mb-5 flex flex-wrap items-start justify-between gap-4 rounded-3xl border border-white/10 bg-white/[0.025] px-5 py-4">
<div className="min-w-0">
<h1 className="text-3xl font-black tracking-tight text-white">
Provisionamento
</h1>
<p className="mt-1 text-slate-400">
<p className="mt-1 text-sm text-slate-400">
Provisionamento guiado do router, desde a deteção até ao registo do peer na VPS.
</p>
</div>
@@ -1123,17 +1123,14 @@ export function ProvisioningWizard() {
</div>
</div>
<div className="grid min-h-0 flex-1 grid-cols-12 gap-5 overflow-hidden">
<Card className="col-span-5 flex min-h-0 flex-col overflow-hidden">
<div
className={`min-h-0 flex-1 overflow-y-auto pr-1 ${scrollClass}`}
>
<div className="grid gap-4 2xl:grid-cols-12">
<Card className="2xl:col-span-5">
<div className="mb-5 flex items-center gap-4">
<div className="rounded-2xl bg-blue-500/10 p-3 text-blue-300">
<Search size={24} />
</div>
<div>
<div className="min-w-0">
<h2 className="text-xl font-semibold text-white">
Ligação ao Router
</h2>
@@ -1144,47 +1141,25 @@ export function ProvisioningWizard() {
</div>
</div>
<div className="mb-4 grid grid-cols-2 gap-3">
<button
type="button"
<div className="mb-4 grid gap-3 md:grid-cols-2">
<ModeButton
active={provisioningMode === 'full'}
disabled={controlsLocked}
onClick={() =>
setProvisioningMode('full')
}
className={`rounded-2xl border p-4 text-left transition disabled:cursor-not-allowed disabled:opacity-60 ${provisioningMode === 'full'
? 'border-blue-500/40 bg-blue-500/10'
: 'border-white/10 bg-white/[0.02]'
}`}
>
<p className="font-semibold text-white">
Provisionamento Completo
</p>
<p className="mt-1 text-xs text-slate-500">
Gravar firmware, reconectar e depois provisionar.
</p>
</button>
title="Provisionamento Completo"
description="Gravar firmware, reconectar e depois provisionar."
onClick={() => setProvisioningMode('full')}
/>
<button
type="button"
<ModeButton
active={provisioningMode === 'provision_only'}
disabled={controlsLocked}
onClick={() =>
setProvisioningMode('provision_only')
}
className={`rounded-2xl border p-4 text-left transition disabled:cursor-not-allowed disabled:opacity-60 ${provisioningMode === 'provision_only'
? 'border-blue-500/40 bg-blue-500/10'
: 'border-white/10 bg-white/[0.02]'
}`}
>
<p className="font-semibold text-white">
Apenas Provisionar
</p>
<p className="mt-1 text-xs text-slate-500">
O router tem firmware gravado e está estável.
</p>
</button>
title="Apenas Provisionar"
description="O router já tem firmware gravado e está estável."
onClick={() => setProvisioningMode('provision_only')}
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="grid gap-3 md:grid-cols-2">
{routerPresets.map((preset) => {
const active =
selectedIp === preset.ip &&
@@ -1200,7 +1175,8 @@ export function ProvisioningWizard() {
setRouterPassword(preset.password);
setCustomIp('');
}}
className={`rounded-2xl border p-4 text-left transition disabled:cursor-not-allowed disabled:opacity-60 ${active
className={`rounded-2xl border p-4 text-left transition disabled:cursor-not-allowed disabled:opacity-60 ${
active
? 'border-blue-500/40 bg-blue-500/10'
: 'border-white/10 bg-white/[0.02] hover:border-blue-500/20 hover:bg-white/[0.04]'
}`}
@@ -1233,7 +1209,8 @@ export function ProvisioningWizard() {
})}
<div
className={`col-span-2 rounded-2xl border p-4 transition ${customIp.trim()
className={`rounded-2xl border p-4 transition md:col-span-2 ${
customIp.trim()
? 'border-blue-500/40 bg-blue-500/10'
: 'border-white/10 bg-white/[0.02]'
}`}
@@ -1259,7 +1236,7 @@ export function ProvisioningWizard() {
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="grid gap-3 md:grid-cols-2">
<div>
<label className="mb-2 block text-xs font-medium uppercase tracking-wide text-slate-500">
IP do Router
@@ -1318,12 +1295,12 @@ export function ProvisioningWizard() {
<Wifi size={22} />
</div>
<div className="flex-1">
<div className="min-w-0 flex-1">
<p className="font-semibold text-white">
Alvo de Deteção
</p>
<p className="mt-1 font-mono text-sm text-slate-400">
<p className="mt-1 truncate font-mono text-sm text-slate-400">
root@{selectedIp || '—'}
</p>
</div>
@@ -1361,12 +1338,11 @@ export function ProvisioningWizard() {
Corrigir Known Host
</Button>
</div>
</div>
</Card>
<div className="col-span-4 flex min-h-0 flex-col gap-5">
<Card className="flex min-h-0 flex-1 flex-col overflow-hidden">
<div className="mb-4 flex items-center gap-3">
<Card className="2xl:col-span-7">
<div className="mb-4 flex flex-wrap items-start justify-between gap-4">
<div className="flex items-center gap-3">
<div className="rounded-2xl bg-blue-500/10 p-3 text-blue-300">
<Rocket size={22} />
</div>
@@ -1382,21 +1358,6 @@ export function ProvisioningWizard() {
</div>
</div>
<div
className={`min-h-0 flex-1 space-y-3 overflow-y-auto pr-1 ${scrollClass}`}
>
{workflowState.map((step) => (
<WorkflowStepCard
key={step.id}
title={step.title}
description={step.description}
icon={step.icon}
status={step.status}
/>
))}
</div>
<div className="mt-4">
<Button
type="button"
onClick={continueProvisioning}
@@ -1412,14 +1373,26 @@ export function ProvisioningWizard() {
? 'A trabalhar...'
: isReconnecting
? 'A reconectar...'
: 'Continuar Provisionamento'}
: 'Continuar'}
</Button>
</div>
<div className="grid gap-3 md:grid-cols-2">
{workflowState.map((step) => (
<WorkflowStepCard
key={step.id}
title={step.title}
description={step.description}
icon={step.icon}
status={step.status}
/>
))}
</div>
</Card>
</div>
<div className="col-span-3 flex min-h-0 flex-col gap-5">
<Card className="flex min-h-0 flex-1 flex-col overflow-hidden">
<div className="mt-4 grid gap-4 xl:grid-cols-12">
<Card className="min-h-[260px] xl:col-span-5">
<div className="mb-4 flex items-center gap-3">
<div className="rounded-2xl bg-blue-500/10 p-3 text-blue-300">
<Terminal size={22} />
@@ -1437,25 +1410,24 @@ export function ProvisioningWizard() {
</div>
<pre
className={`min-h-0 flex-1 overflow-auto whitespace-pre-wrap rounded-2xl border border-white/10 bg-slate-950 p-4 font-mono text-xs text-slate-300 ${scrollClass}`}
className={`max-h-[260px] min-h-[160px] overflow-auto whitespace-pre-wrap rounded-2xl border border-white/10 bg-slate-950 p-4 font-mono text-xs text-slate-300 ${scrollClass}`}
>
{routerInfo || 'Ainda não foi capturada informação do router.'}
</pre>
</Card>
</div>
</div>
<Card className="mt-4 flex h-52 flex-col overflow-hidden">
<Card className="min-h-[260px] xl:col-span-7">
<h3 className="mb-2 text-sm font-semibold text-white">
Registo Técnico
</h3>
<pre
className={`min-h-0 flex-1 overflow-auto whitespace-pre-wrap break-words rounded-xl border border-white/10 bg-slate-950/70 p-3 font-mono text-sm text-slate-300 ${scrollClass}`}
className={`max-h-[260px] min-h-[200px] overflow-auto whitespace-pre-wrap break-words rounded-xl border border-white/10 bg-slate-950/70 p-3 font-mono text-sm text-slate-300 ${scrollClass}`}
>
{log.join('\n') || 'Ainda não há atividade de provisionamento.'}
</pre>
</Card>
</div>
{confirmFlashOpen && (
<ConfirmFlashModal
@@ -1517,6 +1489,41 @@ export function ProvisioningWizard() {
);
}
function ModeButton({
active,
disabled,
title,
description,
onClick,
}: {
active: boolean;
disabled: boolean;
title: string;
description: string;
onClick: () => void;
}) {
return (
<button
type="button"
disabled={disabled}
onClick={onClick}
className={`rounded-2xl border p-4 text-left transition disabled:cursor-not-allowed disabled:opacity-60 ${
active
? 'border-blue-500/40 bg-blue-500/10'
: 'border-white/10 bg-white/[0.02] hover:border-blue-500/20 hover:bg-white/[0.04]'
}`}
>
<p className="font-semibold text-white">
{title}
</p>
<p className="mt-1 text-xs text-slate-500">
{description}
</p>
</button>
);
}
function RouterEnvModal({
routerEnv,
setRouterEnv,
@@ -1541,8 +1548,8 @@ function RouterEnvModal({
}
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/75 backdrop-blur-sm">
<div className="w-full max-w-3xl rounded-3xl border border-blue-500/30 bg-slate-950 p-6 shadow-2xl shadow-blue-500/10">
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/75 p-4 backdrop-blur-sm">
<div className="max-h-[90vh] w-full max-w-3xl overflow-auto rounded-3xl border border-blue-500/30 bg-slate-950 p-6 shadow-2xl shadow-blue-500/10">
<div className="mb-5 flex items-center gap-4">
<div className="rounded-2xl bg-blue-500/10 p-3 text-blue-300">
<FileUp size={26} />
@@ -1559,7 +1566,7 @@ function RouterEnvModal({
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="grid gap-4 md:grid-cols-2">
<EnvInput
label="ID do Router"
value={routerEnv.routerId}
@@ -1595,7 +1602,7 @@ function RouterEnvModal({
Valores estáticos de router.env
</p>
<div className="mt-3 grid grid-cols-2 gap-3 font-mono text-xs text-slate-300">
<div className="mt-3 grid gap-3 font-mono text-xs text-slate-300 md:grid-cols-2">
<p>LAN_IP=198.51.100.1</p>
<p>LAN_NETMASK=255.255.255.0</p>
<p>WG_CIDR=32</p>
@@ -1664,7 +1671,7 @@ function ConfirmFlashModal({
onConfirm: () => void;
}) {
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/75 backdrop-blur-sm">
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/75 p-4 backdrop-blur-sm">
<div className="w-full max-w-xl rounded-3xl border border-red-500/30 bg-slate-950 p-6 shadow-2xl shadow-red-500/10">
<div className="flex gap-4">
<div className="rounded-2xl bg-red-500/10 p-3 text-red-300">
@@ -1717,7 +1724,7 @@ function FlashOverlay({
flashProgress: number;
}) {
return (
<div className="fixed inset-0 z-[60] flex items-center justify-center bg-black/85 backdrop-blur-md">
<div className="fixed inset-0 z-[60] flex items-center justify-center bg-black/85 p-4 backdrop-blur-md">
<div className="w-full max-w-2xl rounded-3xl border border-purple-500/30 bg-slate-950 p-8 shadow-2xl shadow-purple-500/20">
<div className="flex items-start gap-5">
<div className="rounded-3xl bg-purple-500/10 p-4 text-purple-300">
@@ -1770,7 +1777,7 @@ function ProvisionOverlay({
provisionProgress: number;
}) {
return (
<div className="fixed inset-0 z-[60] flex items-center justify-center bg-black/85 backdrop-blur-md">
<div className="fixed inset-0 z-[60] flex items-center justify-center bg-black/85 p-4 backdrop-blur-md">
<div className="w-full max-w-2xl rounded-3xl border border-blue-500/30 bg-slate-950 p-8 shadow-2xl shadow-blue-500/20">
<div className="flex items-start gap-5">
<div className="rounded-3xl bg-blue-500/10 p-4 text-blue-300">
@@ -1831,7 +1838,7 @@ function StopProvisioningModal({
canConfirm: boolean;
}) {
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/75 backdrop-blur-sm">
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/75 p-4 backdrop-blur-sm">
<div className="w-full max-w-xl rounded-3xl border border-red-500/30 bg-slate-950 p-6 shadow-2xl shadow-red-500/10">
<div className="flex gap-4">
<div className="rounded-2xl bg-red-500/10 p-3 text-red-300">
@@ -1899,7 +1906,8 @@ function WorkflowStepCard({
return (
<div
className={`rounded-2xl border p-3 transition ${done
className={`rounded-2xl border p-3 transition ${
done
? 'border-green-500/30 bg-green-500/10'
: active
? 'border-blue-500/40 bg-blue-500/10'
@@ -1910,7 +1918,8 @@ function WorkflowStepCard({
>
<div className="flex gap-3">
<div
className={`rounded-xl p-2 ${done
className={`rounded-xl p-2 ${
done
? 'bg-green-500/10 text-green-300'
: active
? 'bg-blue-500/10 text-blue-300'
@@ -1946,7 +1955,7 @@ function SetupCompleteModal({
onClose: () => void;
}) {
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/75 backdrop-blur-sm">
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/75 p-4 backdrop-blur-sm">
<div className="w-full max-w-xl rounded-3xl border border-green-500/30 bg-slate-950 p-6 shadow-2xl shadow-green-500/10">
<div className="flex gap-4">
<div className="rounded-2xl bg-green-500/10 p-3 text-green-300">
+90 -81
View File
@@ -11,7 +11,6 @@ import {
} from 'lucide-react';
import { Badge } from '@/components/ui/Badge';
import { Button } from '@/components/ui/Button';
import { Card } from '@/components/ui/Card';
import {
@@ -50,16 +49,15 @@ export function BackendSettings() {
}
return (
<div className="flex h-full flex-col overflow-hidden">
<div className="mb-6 flex items-start justify-between">
<div>
<h1 className="text-3xl font-bold tracking-tight text-white">
<div className="flex min-h-full flex-col">
<div className="mb-5 flex flex-wrap items-start justify-between gap-4 rounded-3xl border border-white/10 bg-white/[0.025] px-5 py-4">
<div className="min-w-0">
<h1 className="text-3xl font-black tracking-tight text-white">
Posto de Trabalho
</h1>
<p className="mt-1 text-slate-400">
Configuração local da consola técnica
para provisionamento de routers.
<p className="mt-1 max-w-2xl text-sm leading-6 text-slate-400">
Configuração local da consola técnica para provisionamento de routers.
</p>
</div>
@@ -68,85 +66,70 @@ export function BackendSettings() {
</Badge>
</div>
<div className="grid min-h-0 flex-1 grid-cols-12 gap-5 overflow-hidden">
<Card className="col-span-7 flex flex-col">
{saved && (
<div className="mb-4 rounded-2xl border border-green-500/20 bg-green-500/10 p-4 text-sm text-green-300">
<div className="flex items-center gap-2">
<CheckCircle2 size={16} />
Configuração do posto de trabalho guardada.
</div>
</div>
)}
<div className="grid gap-4 2xl:grid-cols-12">
<Card className="2xl:col-span-7">
<div className="mb-6 flex items-center gap-4">
<div className="rounded-2xl bg-blue-500/10 p-3 text-blue-300">
<Server size={24} />
</div>
<div>
<div className="min-w-0">
<h2 className="text-xl font-semibold text-white">
Ligação ao Backend
</h2>
<p className="text-sm text-slate-400">
Endpoint API usado para atribuição
de IP VPN e registo de peers.
Endpoint API usado para atribuição de IP VPN e registo de peers.
</p>
</div>
</div>
<div className="space-y-5">
<div>
<label className="mb-2 block text-xs font-semibold uppercase tracking-wide text-slate-500">
URL do Backend
</label>
<div className="relative">
<Link2
size={16}
className="absolute left-4 top-1/2 -translate-y-1/2 text-blue-300/70"
/>
<input
<div className="grid gap-5 xl:grid-cols-2">
<SettingsInput
label="URL do Backend"
value={settings.backendUrl}
onChange={(event) =>
setSettings({
...settings,
backendUrl: event.target.value,
})
}
className="w-full rounded-2xl border border-white/10 bg-slate-950/80 py-4 pl-11 pr-4 font-mono text-sm text-white outline-none transition placeholder:text-slate-600 focus:border-blue-500/50 focus:bg-slate-950"
icon={<Link2 size={16} />}
placeholder="http://localhost:8080"
/>
</div>
</div>
<div>
<label className="mb-2 block text-xs font-semibold uppercase tracking-wide text-slate-500">
Chave API
</label>
<div className="relative">
<KeyRound
size={16}
className="absolute left-4 top-1/2 -translate-y-1/2 text-blue-300/70"
/>
<input
value={settings.apiKey}
onChange={(event) =>
onChange={(value) =>
setSettings({
...settings,
apiKey: event.target.value,
backendUrl: value,
})
}
className="w-full rounded-2xl border border-white/10 bg-slate-950/80 py-4 pl-11 pr-4 font-mono text-sm text-white outline-none transition placeholder:text-slate-600 focus:border-blue-500/50 focus:bg-slate-950"
/>
<SettingsInput
label="Chave API"
value={settings.apiKey}
icon={<KeyRound size={16} />}
placeholder="dev-api-key"
onChange={(value) =>
setSettings({
...settings,
apiKey: value,
})
}
/>
</div>
</div>
</div>
<div className="mt-6 rounded-2xl border border-white/10 bg-white/[0.025] p-5">
<div className="mt-6 grid gap-4 xl:grid-cols-[1fr_auto] xl:items-center">
<div className="rounded-2xl border border-white/10 bg-white/[0.025] p-5">
<div className="flex items-start justify-between gap-4">
<div>
<div className="min-w-0">
<p className="text-sm font-semibold text-white">
Alvo Ligado
</p>
<p className="mt-1 font-mono text-sm text-slate-400">
<p className="mt-1 truncate font-mono text-sm text-slate-400">
{backendHost}
</p>
</div>
@@ -157,11 +140,10 @@ export function BackendSettings() {
</div>
</div>
<div className="mt-auto flex justify-end pt-6">
<button
type="button"
onClick={handleSave}
className="group inline-flex items-center gap-2 rounded-2xl border border-white/10 bg-white/[0.02] px-5 py-3 text-sm font-semibold text-white transition-all duration-200 hover:-translate-y-[1px] hover:border-blue-500/30 hover:bg-blue-500/10 active:translate-y-0"
className="group inline-flex w-full items-center justify-center gap-2 rounded-2xl border border-white/10 bg-white/[0.02] px-5 py-3 text-sm font-semibold text-white transition-all duration-200 hover:-translate-y-[1px] hover:border-blue-500/30 hover:bg-blue-500/10 active:translate-y-0 xl:w-auto"
>
<Save
size={16}
@@ -175,14 +157,13 @@ export function BackendSettings() {
</div>
</Card>
<div className="col-span-5 flex min-h-0 flex-col gap-5">
<Card>
<Card className="2xl:col-span-5">
<div className="mb-5 flex items-center gap-4">
<div className="rounded-2xl bg-purple-500/10 p-3 text-purple-300">
<MonitorCog size={24} />
</div>
<div>
<div className="min-w-0">
<h2 className="text-xl font-semibold text-white">
Base de Provisionamento
</h2>
@@ -193,7 +174,7 @@ export function BackendSettings() {
</div>
</div>
<div className="grid gap-3">
<div className="grid gap-3 sm:grid-cols-2 2xl:grid-cols-1">
<InfoRow
label="Rota Overlay"
value={DEFAULT_OVERLAY_ROUTE}
@@ -214,34 +195,37 @@ export function BackendSettings() {
value={DEFAULT_PLC_IP}
/>
<div className="sm:col-span-2 2xl:col-span-1">
<InfoRow
label="Firmware Alvo"
value={DEFAULT_FIRMWARE}
/>
</div>
</div>
</Card>
</div>
<Card className="flex-1">
<Card className="mt-4">
<div className="flex flex-col gap-5 xl:flex-row xl:items-start xl:justify-between">
<div className="flex items-start gap-4">
<div className="rounded-2xl bg-green-500/10 p-3 text-green-300">
<ShieldCheck size={24} />
</div>
<div>
<div className="max-w-4xl">
<h2 className="text-xl font-semibold text-white">
Perfil de Produção
</h2>
<p className="mt-3 text-sm leading-6 text-slate-400">
Base OpenWrt 23.05,
alvo ZBT-WE826 16M,
firewall fw4/nftables,
LuCI sobre WireGuard, topologia
LAN estável e registo automático
Base OpenWrt 23.05, alvo ZBT-WE826 16M, firewall fw4/nftables,
LuCI sobre WireGuard, topologia LAN estável e registo automático
de peers VPS.
</p>
</div>
</div>
<div className="mt-5 flex flex-wrap gap-2">
<div className="flex shrink-0 flex-wrap gap-2 xl:justify-end">
<Badge tone="blue">
OpenWrt 23.05
</Badge>
@@ -255,18 +239,43 @@ export function BackendSettings() {
</Badge>
</div>
</div>
</div>
</Card>
</div>
);
}
{saved && (
<div className="rounded-2xl border border-green-500/20 bg-green-500/10 p-4 text-sm text-green-300">
<div className="flex items-center gap-2">
<CheckCircle2 size={16} />
Configuração do posto de trabalho guardada.
</div>
</div>
)}
function SettingsInput({
label,
value,
icon,
placeholder,
onChange,
}: {
label: string;
value: string;
icon: React.ReactNode;
placeholder: string;
onChange: (value: string) => void;
}) {
return (
<div>
<label className="mb-2 block text-xs font-semibold uppercase tracking-wide text-slate-500">
{label}
</label>
<div className="relative">
<div className="absolute left-4 top-1/2 -translate-y-1/2 text-blue-300/70">
{icon}
</div>
<input
value={value}
onChange={(event) =>
onChange(event.target.value)
}
className="w-full rounded-2xl border border-white/10 bg-slate-950/80 py-4 pl-11 pr-4 font-mono text-sm text-white outline-none transition placeholder:text-slate-600 focus:border-blue-500/50 focus:bg-slate-950"
placeholder={placeholder}
/>
</div>
</div>
);
+28 -31
View File
@@ -4,18 +4,17 @@ import {
Activity,
CheckCircle2,
Cpu,
Eye,
EyeOff,
Network,
Play,
RadioTower,
Search,
Terminal,
Eye,
EyeOff,
UploadCloud,
} from 'lucide-react';
import { Badge } from '@/components/ui/Badge';
import { Button } from '@/components/ui/Button';
import { Card } from '@/components/ui/Card';
type Udp2rawStep = {
@@ -80,8 +79,8 @@ export function Udp2rawConfig() {
const [completedSteps, setCompletedSteps] = useState<string[]>([]);
const [failedStep, setFailedStep] = useState<string | null>(null);
const [log, setLog] = useState<string[]>([]);
const [showPassword, setShowPassword] =
useState(false);
const [showPassword, setShowPassword] = useState(false);
const running = Boolean(runningStep);
const statusTone = useMemo(() => {
@@ -148,9 +147,9 @@ export function Udp2rawConfig() {
setFailedStep(null);
for (const step of steps) {
try {
await runStep(step);
} catch {
if (failedStep) {
return;
}
}
@@ -163,10 +162,10 @@ export function Udp2rawConfig() {
}
return (
<div className="flex h-full flex-col overflow-hidden">
<div className="mb-5 flex items-start justify-between">
<div className="flex items-center gap-4">
<div className="relative">
<div className="flex min-h-full flex-col">
<div className="mb-5 flex items-start justify-between gap-4 rounded-3xl border border-white/10 bg-white/[0.025] px-5 py-4">
<div className="flex min-w-0 items-center gap-4">
<div className="relative shrink-0">
<div className="absolute inset-0 rounded-3xl bg-blue-500/20 blur-xl" />
<div className="relative rounded-3xl border border-blue-400/20 bg-blue-500/10 p-4 text-blue-300">
@@ -174,12 +173,12 @@ export function Udp2rawConfig() {
</div>
</div>
<div>
<h1 className="text-3xl font-black tracking-tight text-white">
<div className="min-w-0">
<h1 className="truncate text-3xl font-black tracking-tight text-white">
Configuração UDP2RAW
</h1>
<p className="mt-1 text-slate-400">
<p className="mt-1 text-sm text-slate-400">
Instalação, arranque e validação do túnel UDP2RAW no router.
</p>
</div>
@@ -190,9 +189,8 @@ export function Udp2rawConfig() {
</Badge>
</div>
<div className="grid min-h-0 flex-1 grid-cols-12 gap-5 overflow-hidden">
<div className="col-span-5 flex min-h-0 flex-col gap-5">
<Card className="relative overflow-hidden">
<div className="grid gap-4 xl:grid-cols-12">
<Card className="relative overflow-hidden xl:col-span-5">
<div className="pointer-events-none absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-blue-400/30 to-transparent" />
<div className="mb-5 flex items-center gap-4">
@@ -200,7 +198,7 @@ export function Udp2rawConfig() {
<Network size={24} />
</div>
<div>
<div className="min-w-0">
<h2 className="text-xl font-bold text-white">
Router Alvo
</h2>
@@ -211,7 +209,7 @@ export function Udp2rawConfig() {
</div>
</div>
<div className="grid gap-4">
<div className="grid gap-4 lg:grid-cols-2 xl:grid-cols-1">
<div>
<label className="mb-2 block text-xs font-bold uppercase tracking-[0.16em] text-slate-500">
IP do Router
@@ -284,8 +282,8 @@ export function Udp2rawConfig() {
</div>
</Card>
<Card className="flex min-h-0 flex-1 flex-col overflow-hidden">
<div className="mb-4 flex items-start justify-between gap-4">
<Card className="overflow-hidden xl:col-span-7">
<div className="mb-4 flex flex-wrap items-start justify-between gap-4">
<div>
<h2 className="text-lg font-bold text-white">
Fluxo de Instalação
@@ -323,9 +321,7 @@ export function Udp2rawConfig() {
</div>
</div>
<div
className={`min-h-0 flex-1 space-y-3 overflow-y-auto pr-1 ${scrollClass}`}
>
<div className="grid gap-3 md:grid-cols-2">
{steps.map((step, index) => {
const done = completedSteps.includes(step.id);
const active = runningStep === step.id;
@@ -338,7 +334,8 @@ export function Udp2rawConfig() {
type="button"
disabled={running}
onClick={() => runStep(step)}
className={`group w-full rounded-2xl border p-4 text-left transition disabled:cursor-not-allowed ${failed
className={`group rounded-2xl border p-4 text-left transition disabled:cursor-not-allowed ${
failed
? 'border-red-500/30 bg-red-500/10'
: done
? 'border-green-500/30 bg-green-500/10'
@@ -349,7 +346,8 @@ export function Udp2rawConfig() {
>
<div className="flex items-start gap-3">
<div
className={`flex h-10 w-10 shrink-0 items-center justify-center rounded-2xl ${failed
className={`flex h-10 w-10 shrink-0 items-center justify-center rounded-2xl ${
failed
? 'bg-red-500/10 text-red-300'
: done
? 'bg-green-500/10 text-green-300'
@@ -363,7 +361,7 @@ export function Udp2rawConfig() {
<div className="min-w-0 flex-1">
<div className="flex items-center justify-between gap-3">
<p className="text-sm font-bold text-white">
<p className="truncate text-sm font-bold text-white">
{step.title}
</p>
@@ -372,7 +370,7 @@ export function Udp2rawConfig() {
</span>
</div>
<p className="mt-1 text-xs leading-5 text-slate-500">
<p className="mt-1 line-clamp-2 text-xs leading-5 text-slate-500">
{step.description}
</p>
</div>
@@ -384,7 +382,7 @@ export function Udp2rawConfig() {
</Card>
</div>
<Card className="col-span-7 flex min-h-0 flex-col overflow-hidden">
<Card className="mt-4 flex min-h-[240px] flex-col overflow-hidden">
<div className="mb-4 flex items-center justify-between gap-3">
<div className="flex items-center gap-3">
<div className="rounded-2xl border border-blue-400/10 bg-blue-500/10 p-3 text-blue-300">
@@ -413,12 +411,11 @@ export function Udp2rawConfig() {
</div>
<pre
className={`min-h-0 flex-1 overflow-auto whitespace-pre-wrap break-words rounded-3xl border border-white/10 bg-black/25 p-5 font-mono text-xs leading-6 text-slate-300 ${scrollClass}`}
className={`max-h-[280px] min-h-[150px] overflow-auto whitespace-pre-wrap break-words rounded-3xl border border-white/10 bg-black/25 p-5 font-mono text-xs leading-6 text-slate-300 ${scrollClass}`}
>
{log.join('\n\n') || 'Ainda não há atividade UDP2RAW.'}
</pre>
</Card>
</div>
</div>
);
}