Fixed responsiveness
This commit is contained in:
@@ -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
@@ -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}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,7 +139,7 @@ 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 =
|
||||||
@@ -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 já 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">
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user