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