working version before responsiveness updates

This commit is contained in:
litoral05
2026-05-12 10:40:02 +01:00
parent abaa2aa137
commit 7c04ea5b2e
10 changed files with 1209 additions and 123 deletions
@@ -0,0 +1,154 @@
#!/bin/sh
set -eu
echo "======================================"
echo " UDP2RAW WireGuard Client Setup"
echo "======================================"
VPS_HOST="146.59.230.190"
UDP2RAW_REMOTE_PORT="444"
LOCAL_WG_PORT="4999"
UDP2RAW_PASSWORD="test123"
RAW_MODE="faketcp"
WG_MTU="1240"
INIT_SCRIPT="/etc/init.d/udp2raw-wg"
echo ""
echo "[1/10] Checking udp2raw binary..."
if ! command -v udp2raw >/dev/null 2>&1; then
echo "ERROR: udp2raw binary is missing"
exit 10
fi
echo "udp2raw binary found:"
command -v udp2raw
echo ""
echo "[2/10] Stopping existing service if present..."
if [ -f "$INIT_SCRIPT" ]; then
/etc/init.d/udp2raw-wg stop || true
fi
pkill -f "/usr/bin/udp2raw" || true
sleep 1
echo ""
echo "[3/10] Writing init.d service..."
cat > "$INIT_SCRIPT" <<EOF
#!/bin/sh /etc/rc.common
START=95
STOP=10
USE_PROCD=1
start_service() {
procd_open_instance
procd_set_param command \\
/usr/bin/udp2raw \\
-c \\
-l 127.0.0.1:${LOCAL_WG_PORT} \\
-r ${VPS_HOST}:${UDP2RAW_REMOTE_PORT} \\
--raw-mode ${RAW_MODE} \\
-k ${UDP2RAW_PASSWORD}
procd_set_param respawn
procd_set_param stdout 1
procd_set_param stderr 1
procd_close_instance
}
EOF
chmod +x "$INIT_SCRIPT"
echo ""
echo "[4/10] Enabling service..."
/etc/init.d/udp2raw-wg enable
echo ""
echo "[5/10] Starting service..."
/etc/init.d/udp2raw-wg restart
sleep 3
echo ""
echo "[6/10] Updating WireGuard endpoint and MTU..."
if ! uci show network.wgserver >/dev/null 2>&1; then
echo "ERROR: WireGuard peer section network.wgserver was not found"
exit 30
fi
if ! uci show network.wg0 >/dev/null 2>&1; then
echo "ERROR: WireGuard interface section network.wg0 was not found"
exit 31
fi
uci set network.wgserver.endpoint_host='127.0.0.1'
uci set network.wgserver.endpoint_port="${LOCAL_WG_PORT}"
uci set network.wg0.mtu="${WG_MTU}"
uci commit network
echo ""
echo "[7/10] Restarting WireGuard interface..."
ifdown wg0 || true
sleep 2
ifup wg0 || true
sleep 5
echo ""
echo "[8/10] Checking udp2raw process..."
if pgrep -af "^/usr/bin/udp2raw" >/dev/null 2>&1; then
echo "udp2raw process running:"
pgrep -af "^/usr/bin/udp2raw"
else
echo "ERROR: udp2raw process not running"
exit 20
fi
echo ""
echo "[9/10] Checking local listener..."
if netstat -ln 2>/dev/null | grep -q "127.0.0.1:${LOCAL_WG_PORT}"; then
echo "Local listener active on 127.0.0.1:${LOCAL_WG_PORT}"
else
echo "WARNING: Could not confirm local listener"
fi
echo ""
echo "[10/10] Testing connectivity..."
ping -c 2 -W 2 "${VPS_HOST}" || true
echo ""
echo "WireGuard endpoint:"
uci get network.wgserver.endpoint_host
uci get network.wgserver.endpoint_port
echo ""
echo "WireGuard MTU:"
uci get network.wg0.mtu || true
echo ""
echo "WireGuard status:"
wg show wg0 || true
echo ""
echo "======================================"
echo " UDP2RAW setup completed successfully"
echo "======================================"
Binary file not shown.
+254
View File
@@ -714,3 +714,257 @@ pub async fn upload_provisioning_bundle(
Ok(format!("uploaded provision.sh and router.env to {}", ip))
}
#[tauri::command]
pub async fn upload_udp2raw_setup_script(ip: String, password: String) -> Result<String, String> {
if ip.trim().is_empty() {
return Err("router IP is required".into());
}
let local_script_path = "resources/udp2raw/setup_udp2raw.sh";
let remote_script_path = "/tmp/setup_udp2raw.sh";
if password.trim().is_empty() {
let target = format!("root@{}:{}", ip, remote_script_path);
let output = Command::new("scp")
.args([
"-O",
"-o",
"BatchMode=yes",
"-o",
"ConnectTimeout=10",
"-o",
"StrictHostKeyChecking=no",
"-o",
"UserKnownHostsFile=NUL",
local_script_path,
&target,
])
.output()
.map_err(|error| format!("failed to run scp for setup_udp2raw.sh: {}", error))?;
if !output.status.success() {
return Err(
format!(
"failed to upload setup_udp2raw.sh:\n{}\n{}",
String::from_utf8_lossy(&output.stderr),
String::from_utf8_lossy(&output.stdout)
)
);
}
run_system_ssh(&ip, "chmod +x /tmp/setup_udp2raw.sh")?;
return Ok(format!("uploaded setup_udp2raw.sh to {}", ip));
}
let session = open_router_session(&ip, &password)?;
scp_file_from_disk(&session, local_script_path, remote_script_path, 0o755)?;
run_ssh_command(&session, "chmod +x /tmp/setup_udp2raw.sh")?;
Ok(format!("uploaded setup_udp2raw.sh to {}", ip))
}
#[tauri::command]
pub async fn run_udp2raw_setup(ip: String, password: String) -> Result<String, String> {
if ip.trim().is_empty() {
return Err("router IP is required".into());
}
let command = "sh /tmp/setup_udp2raw.sh";
if password.trim().is_empty() {
return run_system_ssh(&ip, command);
}
let session = open_router_session(&ip, &password)?;
run_ssh_command(&session, command)
}
#[tauri::command]
pub async fn check_udp2raw_router_status(ip: String, password: String) -> Result<String, String> {
if ip.trim().is_empty() {
return Err("router IP is required".into());
}
let command =
r#"
echo "== udp2raw binary =="
if command -v udp2raw >/dev/null 2>&1; then
command -v udp2raw
else
echo "missing"
fi
echo ""
echo "== init script =="
if [ -x /etc/init.d/udp2raw-wg ]; then
echo "present"
else
echo "missing"
fi
echo ""
echo "== process =="
if pgrep -af "^/usr/bin/udp2raw" >/dev/null 2>&1; then
pgrep -af "^/usr/bin/udp2raw"
else
echo "not running"
fi
echo ""
echo "== service status =="
if [ -x /etc/init.d/udp2raw-wg ]; then
/etc/init.d/udp2raw-wg status || true
else
echo "service unavailable"
fi
echo ""
echo "== WireGuard configured endpoint =="
uci get network.wgserver.endpoint_host 2>/dev/null || true
uci get network.wgserver.endpoint_port 2>/dev/null || true
echo ""
echo "== local listener =="
netstat -ln 2>/dev/null | grep -E '127.0.0.1:4999|:4999' || echo "listener not confirmed"
echo ""
echo "== WireGuard runtime endpoint =="
wg show wg0 2>/dev/null | grep -A8 '^peer:' || echo "wg0 unavailable"
"#;
if password.trim().is_empty() {
return run_system_ssh(&ip, command);
}
let session = open_router_session(&ip, &password)?;
run_ssh_command(&session, command)
}
#[tauri::command]
pub async fn test_udp2raw_tunnel(ip: String, password: String) -> Result<String, String> {
if ip.trim().is_empty() {
return Err("router IP is required".into());
}
let command = r#"
echo "== udp2raw process =="
if pgrep -af "^/usr/bin/udp2raw" >/dev/null 2>&1; then
pgrep -af "^/usr/bin/udp2raw"
else
echo "ERROR: udp2raw is not running"
exit 20
fi
echo ""
echo "== WireGuard configured endpoint =="
uci get network.wgserver.endpoint_host 2>/dev/null || true
uci get network.wgserver.endpoint_port 2>/dev/null || true
echo ""
echo "== local listener =="
netstat -ln 2>/dev/null | grep -E '127.0.0.1:4999|:4999' || echo "WARNING: listener not confirmed"
echo ""
echo "== ping VPS public IP =="
ping -c 2 -W 2 146.59.230.190 || true
echo ""
echo "== WireGuard status =="
wg show wg0 2>/dev/null || echo "wg0 not available"
echo ""
echo "== route check =="
ip route || true
echo ""
echo "UDP2RAW tunnel test completed"
"#;
if password.trim().is_empty() {
return run_system_ssh(&ip, command);
}
let mut last_error = String::new();
for attempt in 1..=5 {
match open_router_session(&ip, &password) {
Ok(session) => {
return run_ssh_command(&session, command);
}
Err(error) => {
last_error = format!(
"SSH attempt {}/5 failed: {}",
attempt,
error
);
thread::sleep(Duration::from_secs(2));
}
}
}
Err(last_error)
}
#[tauri::command]
pub async fn upload_udp2raw_binary(ip: String, password: String) -> Result<String, String> {
if ip.trim().is_empty() {
return Err("router IP is required".into());
}
let local_binary_path = "resources/udp2raw/udp2raw";
let remote_binary_path = "/usr/bin/udp2raw";
if password.trim().is_empty() {
let target = format!("root@{}:{}", ip, remote_binary_path);
let output = Command::new("scp")
.args([
"-O",
"-o",
"BatchMode=yes",
"-o",
"ConnectTimeout=10",
"-o",
"StrictHostKeyChecking=no",
"-o",
"UserKnownHostsFile=NUL",
local_binary_path,
&target,
])
.output()
.map_err(|error| { format!("failed to run scp for udp2raw binary: {}", error) })?;
if !output.status.success() {
return Err(
format!(
"failed to upload udp2raw binary:\n{}\n{}",
String::from_utf8_lossy(&output.stderr),
String::from_utf8_lossy(&output.stdout)
)
);
}
run_system_ssh(
&ip,
"chmod +x /usr/bin/udp2raw && /usr/bin/udp2raw --help >/dev/null 2>&1 || true"
)?;
return Ok("uploaded udp2raw binary to /usr/bin/udp2raw".into());
}
let session = open_router_session(&ip, &password)?;
scp_file_from_disk(&session, local_binary_path, remote_binary_path, 0o755)?;
run_ssh_command(&session, "chmod +x /usr/bin/udp2raw && ls -l /usr/bin/udp2raw")?;
Ok("uploaded udp2raw binary to /usr/bin/udp2raw".into())
}
+19 -10
View File
@@ -15,21 +15,24 @@ use commands::{
reconnect_router_after_flash,
verify_router,
wait_for_ssh,
check_router_after_flash
},
ssh::{
inspect_router_with_password,
probe_router_ssh,
remove_known_host,
check_router_after_flash,
upload_udp2raw_setup_script,
run_udp2raw_setup,
test_udp2raw_tunnel,
check_udp2raw_router_status,
upload_udp2raw_binary
},
ssh::{ inspect_router_with_password, probe_router_ssh, remove_known_host },
};
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
tauri::Builder
::default()
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init())
.invoke_handler(tauri::generate_handler![
.invoke_handler(
tauri::generate_handler![
read_text_file,
ping_host,
remove_known_host,
@@ -46,8 +49,14 @@ pub fn run() {
run_provisioning,
capture_wireguard_public_key,
verify_router,
check_router_after_flash
])
check_router_after_flash,
upload_udp2raw_setup_script,
run_udp2raw_setup,
test_udp2raw_tunnel,
check_udp2raw_router_status,
upload_udp2raw_binary
]
)
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
+119 -34
View File
@@ -2,6 +2,8 @@ import { useEffect, useMemo, useState } from 'react';
import {
Clock,
Database,
HardDrive,
RadioTower,
Server,
ShieldCheck,
} from 'lucide-react';
@@ -15,6 +17,7 @@ import { ProvisioningWizard } from '@/components/provisioning/ProvisioningWizard
import { BackendSettings } from '@/components/settings/BackendSettings';
import { ActivityLogs } from '@/components/activity/ActivityLogs';
import { Card } from '@/components/ui/Card';
import { Udp2rawConfig } from '@/components/udp2raw/Udp2rawConfig';
import { vpnApi } from '@/services/vpnApi';
import { vpsApi } from '@/services/vpsApi';
@@ -50,7 +53,6 @@ export function DashboardRoute() {
} catch (err) {
setHealth(null);
setUsedIps(null);
setError(String(err));
}
}
@@ -88,74 +90,159 @@ export function DashboardRoute() {
const vpnHealthy =
health?.wireGuardRunning === true;
const udp2rawHealthy =
health?.udp2rawActive === true;
const dashboardReady =
!error &&
Boolean(health) &&
backendHealthy &&
vpnHealthy;
vpnHealthy &&
udp2rawHealthy;
return (
<div className="flex h-full flex-col overflow-hidden">
<TopBar healthy={dashboardReady} />
{error && (
<div className="mb-3 rounded-xl border border-red-500/20 bg-red-500/10 p-3 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:{' '}
{error}
</div>
)}
<div className="grid grid-cols-4 gap-4">
<div className="grid grid-cols-12 gap-4">
<div className="col-span-3">
<MetricCard
title="Estado da VPN"
value={
vpnHealthy
? 'Ligada'
: 'Offline'
}
title="VPN"
value={vpnHealthy ? 'Ligada' : 'Offline'}
subtitle={
health?.wireGuardInterface
? `Interface ${health.wireGuardInterface}`
: 'Estado WireGuard wg0'
: 'WireGuard wg0'
}
icon={<ShieldCheck />}
icon={<ShieldCheck size={22} />}
tone={vpnHealthy ? 'green' : 'red'}
/>
</div>
<div className="col-span-3">
<MetricCard
title="Uso da Pool IP"
title="UDP2RAW"
value={udp2rawHealthy ? 'Ativo' : 'Offline'}
subtitle={health?.udp2rawService ?? 'udp2raw-wg'}
icon={<RadioTower size={22} />}
tone={udp2rawHealthy ? 'purple' : 'red'}
/>
</div>
<div className="col-span-2">
<MetricCard
title="Pool IP"
value={`${ipPoolPercent}%`}
subtitle={`${usedCount} / ${ipPoolTotal} IPs usados`}
icon={<Database />}
subtitle={`${usedCount} / ${ipPoolTotal} usados`}
icon={<Database size={22} />}
tone="blue"
/>
</div>
<div className="col-span-2">
<MetricCard
title="Uptime da VPS"
value={
health?.systemUptime ??
'Desconhecido'
title="Disco"
value={`${health?.diskUsagePercent ?? 0}%`}
subtitle={`Memória ${health?.memoryUsagePercent ?? 0}%`}
icon={<HardDrive size={22} />}
tone={
(health?.diskUsagePercent ?? 0) > 85
? 'red'
: 'blue'
}
subtitle="Reportado pelo health da VPS"
icon={<Clock />}
/>
</div>
<div className="col-span-2">
<MetricCard
title="Estado do Backend"
value={
backendHealthy
? 'Saudável'
: 'Offline'
}
title="Backend"
value={backendHealthy ? 'Saudável' : 'Offline'}
subtitle="Conectividade API"
icon={<Server />}
icon={<Server size={22} />}
tone={backendHealthy ? 'green' : 'red'}
/>
</div>
</div>
<div className="mt-4 grid min-h-0 flex-1 grid-cols-12 gap-4 overflow-hidden pb-4">
<div className="col-span-9 min-h-0">
<NetworkTrafficChart />
<div className="col-span-7 min-h-0">
<NetworkTrafficChart
title="Tráfego WireGuard"
subtitle="Débito RX/TX wg0 em direto"
mode="wireguard"
/>
</div>
<div className="col-span-3 min-h-0">
<div className="col-span-3 grid min-h-0 grid-rows-2 gap-4">
<NetworkTrafficChart
title="Tráfego UDP2RAW"
subtitle="Faketcp na porta 444"
mode="udp2raw"
compact
/>
<Card className="overflow-hidden">
<div className="flex h-full flex-col justify-between">
<div>
<div className="mb-4 flex items-center gap-3">
<div className="rounded-2xl bg-blue-500/10 p-3 text-blue-300">
<Clock size={20} />
</div>
<div>
<h3 className="font-semibold text-white">
Estado da VPS
</h3>
<p className="text-xs text-slate-500">
Uptime e carga do sistema
</p>
</div>
</div>
<p className="text-2xl font-black leading-tight text-white">
{health?.systemUptime ?? 'Desconhecido'}
</p>
<p className="mt-3 font-mono text-xs text-slate-400">
Load: {health?.loadAverage ?? '—'}
</p>
<p className="mt-2 font-mono text-xs text-slate-500">
IP: {health?.publicIp ?? '—'}
</p>
</div>
<div className="mt-5 rounded-2xl border border-white/10 bg-white/[0.03] p-3">
<div className="mb-2 flex justify-between text-xs text-slate-500">
<span>Memória</span>
<span>{health?.memoryUsagePercent ?? 0}%</span>
</div>
<div className="h-2 overflow-hidden rounded-full bg-slate-800">
<div
className="h-full rounded-full bg-blue-400"
style={{
width: `${Math.min(
health?.memoryUsagePercent ?? 0,
100,
)}%`,
}}
/>
</div>
</div>
</div>
</Card>
</div>
<div className="col-span-2 min-h-0">
<IpPoolChart
used={usedCount}
total={ipPoolTotal}
@@ -199,9 +286,7 @@ export function RouteView({
}
if (active === 'Configuração UDP2RAW') {
return (
<PlaceholderRoute name="Configuração UDP2RAW" />
);
return <Udp2rawConfig />;
}
if (active === 'Registos de Atividade') {
+58 -7
View File
@@ -2,11 +2,51 @@ import { ReactNode } from 'react';
import { Card } from '@/components/ui/Card';
type MetricCardTone =
| 'blue'
| 'green'
| 'purple'
| 'red';
type MetricCardProps = {
title: string;
value: string;
subtitle: string;
icon: ReactNode;
tone?: MetricCardTone;
};
const toneStyles: Record<
MetricCardTone,
{
iconBg: string;
iconText: string;
glow: string;
}
> = {
blue: {
iconBg: 'bg-blue-500/10',
iconText: 'text-blue-300',
glow: 'from-blue-500/20',
},
green: {
iconBg: 'bg-emerald-500/10',
iconText: 'text-emerald-300',
glow: 'from-emerald-500/20',
},
purple: {
iconBg: 'bg-violet-500/10',
iconText: 'text-violet-300',
glow: 'from-violet-500/20',
},
red: {
iconBg: 'bg-red-500/10',
iconText: 'text-red-300',
glow: 'from-red-500/20',
},
};
export function MetricCard({
@@ -14,24 +54,35 @@ export function MetricCard({
value,
subtitle,
icon,
tone = 'blue',
}: MetricCardProps) {
const style = toneStyles[tone];
return (
<Card className="h-full">
<div className="flex items-start gap-4">
<div className="rounded-2xl bg-blue-500/10 p-3 text-blue-300">
<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]">
<div
className={`absolute inset-x-0 top-0 h-px bg-gradient-to-r ${style.glow} via-white/40 to-transparent opacity-70`}
/>
<div className="absolute -right-10 -top-10 h-32 w-32 rounded-full bg-white/[0.02] blur-3xl transition-all duration-500 group-hover:scale-125" />
<div className="relative flex items-start gap-4">
<div
className={`rounded-2xl ${style.iconBg} ${style.iconText} border border-white/5 p-3 shadow-lg backdrop-blur-xl`}
>
{icon}
</div>
<div>
<p className="text-sm text-slate-400">
<div className="min-w-0 flex-1">
<p className="text-sm font-medium text-slate-400">
{title}
</p>
<h3 className="mt-1 text-2xl font-bold text-white">
<h3 className="mt-1 text-3xl font-black tracking-tight text-white">
{value}
</h3>
<p className="mt-1 text-xs text-slate-400">
<p className="mt-2 truncate text-xs text-slate-500">
{subtitle}
</p>
</div>
+102 -19
View File
@@ -14,6 +14,17 @@ import { Card } from '@/components/ui/Card';
import { vpsApi } from '@/services/vpsApi';
type TrafficMode =
| 'wireguard'
| 'udp2raw';
type NetworkTrafficChartProps = {
title?: string;
subtitle?: string;
mode?: TrafficMode;
compact?: boolean;
};
type TrafficPoint = {
time: string;
downloadMbps: number;
@@ -22,7 +33,12 @@ type TrafficPoint = {
const MAX_POINTS = 24;
export function NetworkTrafficChart() {
export function NetworkTrafficChart({
title = 'Tráfego de Rede',
subtitle = 'Débito RX/TX em direto',
mode = 'wireguard',
compact = false,
}: NetworkTrafficChartProps) {
const [points, setPoints] = useState<
TrafficPoint[]
>([]);
@@ -35,7 +51,9 @@ export function NetworkTrafficChart() {
async function loadTraffic() {
try {
const response =
await vpsApi.networkTraffic();
mode === 'udp2raw'
? await vpsApi.udp2rawTraffic()
: await vpsApi.networkTraffic();
if (!mounted) {
return;
@@ -66,6 +84,9 @@ export function NetworkTrafficChart() {
}
}
setPoints([]);
setError('');
loadTraffic();
const intervalId = window.setInterval(
@@ -77,23 +98,28 @@ export function NetworkTrafficChart() {
mounted = false;
window.clearInterval(intervalId);
};
}, []);
}, [mode]);
const latestPoint =
points[points.length - 1];
return (
<Card className="h-full">
<div className="mb-4 flex items-center justify-between">
<div>
<h3 className="font-semibold text-white">
Tráfego de Rede
<Card className="relative h-full overflow-hidden">
<div className="pointer-events-none absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-blue-400/30 to-transparent" />
<div className="mb-4 flex items-start justify-between gap-4">
<div className="min-w-0">
<h3 className="truncate font-semibold text-white">
{title}
</h3>
<p className="text-xs text-slate-500">
Débito RX/TX wg0 em direto
<p className="mt-1 truncate text-xs text-slate-500">
{subtitle}
</p>
</div>
<span className="rounded-lg border border-white/10 bg-slate-900 px-3 py-1 text-xs text-slate-400">
Atualização 3s
<span className="shrink-0 rounded-lg border border-white/10 bg-slate-900 px-3 py-1 text-xs text-slate-400">
3s
</span>
</div>
@@ -103,12 +129,59 @@ export function NetworkTrafficChart() {
</div>
)}
<div className="h-[calc(100%-4.5rem)] min-h-[360px]">
{compact && latestPoint && (
<div className="mb-3 grid grid-cols-2 gap-2">
<div className="rounded-xl border border-white/10 bg-white/[0.025] p-3">
<p className="text-[10px] font-semibold uppercase tracking-wide text-slate-500">
Entrada
</p>
<p className="mt-1 font-mono text-sm font-bold text-green-300">
{latestPoint.downloadMbps.toFixed(3)} Mbps
</p>
</div>
<div className="rounded-xl border border-white/10 bg-white/[0.025] p-3">
<p className="text-[10px] font-semibold uppercase tracking-wide text-slate-500">
Saída
</p>
<p className="mt-1 font-mono text-sm font-bold text-blue-300">
{latestPoint.uploadMbps.toFixed(3)} Mbps
</p>
</div>
</div>
)}
<div
className={
compact
? 'h-[calc(100%-8.5rem)] min-h-[140px]'
: 'h-[calc(100%-4.5rem)] min-h-[360px]'
}
>
<ResponsiveContainer
width="100%"
height="100%"
>
<AreaChart data={points}>
<AreaChart
data={points}
margin={
compact
? {
top: 8,
right: 8,
left: -28,
bottom: 0,
}
: {
top: 8,
right: 16,
left: 0,
bottom: 0,
}
}
>
<CartesianGrid
strokeDasharray="3 3"
stroke="rgba(148,163,184,.12)"
@@ -117,13 +190,15 @@ export function NetworkTrafficChart() {
<XAxis
dataKey="time"
stroke="#94a3b8"
fontSize={12}
minTickGap={24}
fontSize={compact ? 10 : 12}
minTickGap={compact ? 32 : 24}
tick={compact ? false : undefined}
/>
<YAxis
stroke="#94a3b8"
fontSize={12}
fontSize={compact ? 10 : 12}
width={compact ? 42 : 60}
tickFormatter={(value) =>
`${value} Mbps`
}
@@ -147,7 +222,11 @@ export function NetworkTrafficChart() {
<Area
type="monotone"
dataKey="downloadMbps"
name="Download"
name={
mode === 'udp2raw'
? 'Entrada'
: 'Download'
}
stroke="#19d16f"
fill="#19d16f"
fillOpacity={0.2}
@@ -156,7 +235,11 @@ export function NetworkTrafficChart() {
<Area
type="monotone"
dataKey="uploadMbps"
name="Upload"
name={
mode === 'udp2raw'
? 'Saída'
: 'Upload'
}
stroke="#3b82f6"
fill="#3b82f6"
fillOpacity={0.16}
+38 -17
View File
@@ -1,6 +1,4 @@
import { RefreshCw } from 'lucide-react';
import { Badge } from '@/components/ui/Badge';
import { RefreshCw, ShieldCheck, ShieldX } from 'lucide-react';
type TopBarProps = {
healthy?: boolean;
@@ -10,35 +8,58 @@ export function TopBar({
healthy = false,
}: TopBarProps) {
return (
<header className="mb-6 flex items-center justify-between">
<header className="mb-6 flex items-center justify-between rounded-3xl border border-white/10 bg-white/[0.025] px-5 py-4">
<div>
<h2 className="text-3xl font-bold tracking-tight text-white">
<div className="flex items-center gap-3">
<div
className={`rounded-2xl p-3 ${
healthy
? 'bg-emerald-500/10 text-emerald-300'
: 'bg-red-500/10 text-red-300'
}`}
>
{healthy ? (
<ShieldCheck size={22} />
) : (
<ShieldX size={22} />
)}
</div>
<div>
<h2 className="text-3xl font-black tracking-tight text-white">
Painel
</h2>
<p className="mt-1 text-slate-400">
Provisionamento de routers de produção
OpenWrt 23.05 WireGuard
<p className="mt-1 text-sm text-slate-400">
Provisionamento de routers OpenWrt 23.05 com WireGuard e UDP2RAW
</p>
</div>
</div>
</div>
<div className="flex items-center gap-4 text-sm text-slate-400">
<div className="flex items-center gap-2">
<RefreshCw size={16} />
<div className="flex items-center gap-3">
<div className="flex items-center gap-2 rounded-2xl border border-white/10 bg-black/20 px-4 py-2 text-sm text-slate-400">
<RefreshCw size={15} />
<span>
Última atualização:{' '}
Atualizado às{' '}
<span className="font-mono text-slate-200">
{new Date().toLocaleTimeString()}
</span>
</span>
</div>
<Badge
tone={healthy ? 'green' : 'red'}
<div
className={`rounded-2xl border px-4 py-2 text-sm font-bold ${
healthy
? 'border-emerald-500/20 bg-emerald-500/10 text-emerald-300'
: 'border-red-500/20 bg-red-500/10 text-red-300'
}`}
>
{healthy
? 'Todos os sistemas operacionais'
: 'Problemas detetados no sistema'}
</Badge>
? 'Sistemas operacionais'
: 'Atenção necessária'}
</div>
</div>
</header>
);
+424
View File
@@ -0,0 +1,424 @@
import { useMemo, useState } from 'react';
import { invoke } from '@tauri-apps/api/core';
import {
Activity,
CheckCircle2,
Cpu,
Network,
Play,
RadioTower,
Search,
Terminal,
Eye,
EyeOff,
UploadCloud,
} from 'lucide-react';
import { Badge } from '@/components/ui/Badge';
import { Button } from '@/components/ui/Button';
import { Card } from '@/components/ui/Card';
type Udp2rawStep = {
id: string;
title: string;
description: string;
command: string;
logLabel: string;
icon: typeof Search;
};
const steps: Udp2rawStep[] = [
{
id: 'status',
title: 'Verificar Estado',
description: 'Confirma binário, serviço, processo e porta local.',
command: 'check_udp2raw_router_status',
logLabel: 'A verificar estado UDP2RAW no router...',
icon: Search,
},
{
id: 'binary',
title: 'Enviar Binário',
description: 'Copia o binário udp2raw para /usr/bin/udp2raw.',
command: 'upload_udp2raw_binary',
logLabel: 'A enviar binário UDP2RAW para o router...',
icon: Cpu,
},
{
id: 'script',
title: 'Enviar Script',
description: 'Copia o script de instalação para /tmp.',
command: 'upload_udp2raw_setup_script',
logLabel: 'A enviar script UDP2RAW para o router...',
icon: UploadCloud,
},
{
id: 'setup',
title: 'Executar Setup',
description: 'Cria o serviço init.d e arranca o túnel.',
command: 'run_udp2raw_setup',
logLabel: 'A executar configuração UDP2RAW...',
icon: Play,
},
{
id: 'test',
title: 'Testar Túnel',
description: 'Valida processo, listener, ping e estado WireGuard.',
command: 'test_udp2raw_tunnel',
logLabel: 'A testar túnel UDP2RAW...',
icon: CheckCircle2,
},
];
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';
export function Udp2rawConfig() {
const [routerIp, setRouterIp] = useState('198.51.100.1');
const [password, setPassword] = useState('litoralr');
const [runningStep, setRunningStep] = useState<string | null>(null);
const [completedSteps, setCompletedSteps] = useState<string[]>([]);
const [failedStep, setFailedStep] = useState<string | null>(null);
const [log, setLog] = useState<string[]>([]);
const [showPassword, setShowPassword] =
useState(false);
const running = Boolean(runningStep);
const statusTone = useMemo(() => {
if (failedStep) return 'red';
if (completedSteps.includes('test')) return 'green';
if (running) return 'purple';
return 'blue';
}, [completedSteps, failedStep, running]);
const statusLabel = useMemo(() => {
if (failedStep) return 'erro';
if (completedSteps.includes('test')) return 'validado';
if (running) return 'a executar';
return 'pronto';
}, [completedSteps, failedStep, running]);
const progress =
(completedSteps.length / steps.length) * 100;
function addLog(message: string) {
setLog((current) => [
`${new Date().toLocaleTimeString()} ${message}`,
...current,
]);
}
async function runStep(step: Udp2rawStep) {
if (running) {
return;
}
setRunningStep(step.id);
setFailedStep(null);
try {
addLog(step.logLabel);
const result = await invoke<string>(step.command, {
ip: routerIp,
password,
});
addLog(result || 'Concluído.');
setCompletedSteps((current) => [
...new Set([
...current,
step.id,
]),
]);
} catch (error) {
setFailedStep(step.id);
addLog(`ERRO: ${String(error)}`);
} finally {
setRunningStep(null);
}
}
async function runFullFlow() {
if (running) {
return;
}
setFailedStep(null);
for (const step of steps) {
try {
await runStep(step);
} catch {
return;
}
}
}
function clearLog() {
setLog([]);
setCompletedSteps([]);
setFailedStep(null);
}
return (
<div className="flex h-full flex-col overflow-hidden">
<div className="mb-5 flex items-start justify-between">
<div className="flex items-center gap-4">
<div className="relative">
<div className="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">
<RadioTower size={28} />
</div>
</div>
<div>
<h1 className="text-3xl font-black tracking-tight text-white">
Configuração UDP2RAW
</h1>
<p className="mt-1 text-slate-400">
Instalação, arranque e validação do túnel UDP2RAW no router.
</p>
</div>
</div>
<Badge tone={statusTone}>
{statusLabel}
</Badge>
</div>
<div className="grid min-h-0 flex-1 grid-cols-12 gap-5 overflow-hidden">
<div className="col-span-5 flex min-h-0 flex-col gap-5">
<Card className="relative overflow-hidden">
<div className="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="rounded-2xl border border-blue-400/10 bg-blue-500/10 p-3 text-blue-300">
<Network size={24} />
</div>
<div>
<h2 className="text-xl font-bold text-white">
Router Alvo
</h2>
<p className="text-sm text-slate-400">
Defina o router onde o cliente UDP2RAW será configurado.
</p>
</div>
</div>
<div className="grid gap-4">
<div>
<label className="mb-2 block text-xs font-bold uppercase tracking-[0.16em] text-slate-500">
IP do Router
</label>
<input
value={routerIp}
disabled={running}
onChange={(event) =>
setRouterIp(event.target.value)
}
className="w-full rounded-2xl border border-white/10 bg-black/20 px-4 py-3 font-mono text-sm text-white outline-none transition placeholder:text-slate-600 focus:border-blue-500/50 focus:bg-black/30 focus:shadow-[0_0_0_4px_rgba(59,130,246,0.10)] disabled:cursor-not-allowed disabled:opacity-60"
/>
</div>
<div>
<label className="mb-2 block text-xs font-bold uppercase tracking-[0.16em] text-slate-500">
Palavra-passe SSH
</label>
<div className="relative">
<input
type={showPassword ? 'text' : 'password'}
value={password}
disabled={running}
onChange={(event) =>
setPassword(event.target.value)
}
placeholder="Vazio se não existir"
className="w-full rounded-2xl border border-white/10 bg-black/20 px-4 py-3 pr-14 font-mono text-sm text-white outline-none transition placeholder:text-slate-600 focus:border-blue-500/50 focus:bg-black/30 focus:shadow-[0_0_0_4px_rgba(59,130,246,0.10)] disabled:cursor-not-allowed disabled:opacity-60"
/>
<button
type="button"
onClick={() =>
setShowPassword((current) => !current)
}
className="absolute right-3 top-1/2 flex h-8 w-8 -translate-y-1/2 items-center justify-center rounded-xl text-slate-500 transition hover:bg-white/5 hover:text-blue-300"
>
{showPassword ? (
<EyeOff size={18} />
) : (
<Eye size={18} />
)}
</button>
</div>
</div>
</div>
<div className="mt-5 grid grid-cols-2 gap-3">
{[
['VPS', '146.59.230.190:444'],
['Local', '127.0.0.1:4999'],
['Modo', 'faketcp'],
['Serviço', 'udp2raw-wg'],
].map(([label, value]) => (
<div
key={label}
className="rounded-2xl border border-white/10 bg-white/[0.025] p-4"
>
<p className="text-[10px] font-bold uppercase tracking-[0.16em] text-slate-500">
{label}
</p>
<p className="mt-2 break-all font-mono text-xs font-semibold text-slate-200">
{value}
</p>
</div>
))}
</div>
</Card>
<Card className="flex min-h-0 flex-1 flex-col overflow-hidden">
<div className="mb-4 flex items-start justify-between gap-4">
<div>
<h2 className="text-lg font-bold text-white">
Fluxo de Instalação
</h2>
<p className="mt-1 text-sm text-slate-400">
Execute passo a passo ou corra o fluxo completo.
</p>
</div>
<button
type="button"
disabled={running}
onClick={runFullFlow}
className="inline-flex shrink-0 items-center gap-2 rounded-2xl border border-blue-400/20 bg-blue-500/15 px-4 py-3 text-sm font-bold text-blue-100 shadow-lg shadow-blue-500/10 transition hover:-translate-y-[1px] hover:bg-blue-500/25 disabled:cursor-not-allowed disabled:opacity-60"
>
<Activity size={16} />
Executar Tudo
</button>
</div>
<div className="mb-4">
<div className="mb-2 flex justify-between text-xs text-slate-500">
<span>Progresso</span>
<span>{completedSteps.length}/{steps.length}</span>
</div>
<div className="h-2 overflow-hidden rounded-full bg-slate-800">
<div
className="h-full rounded-full bg-blue-400 transition-all duration-500"
style={{
width: `${progress}%`,
}}
/>
</div>
</div>
<div
className={`min-h-0 flex-1 space-y-3 overflow-y-auto pr-1 ${scrollClass}`}
>
{steps.map((step, index) => {
const done = completedSteps.includes(step.id);
const active = runningStep === step.id;
const failed = failedStep === step.id;
const Icon = step.icon;
return (
<button
key={step.id}
type="button"
disabled={running}
onClick={() => runStep(step)}
className={`group w-full rounded-2xl border p-4 text-left transition disabled:cursor-not-allowed ${failed
? 'border-red-500/30 bg-red-500/10'
: done
? 'border-green-500/30 bg-green-500/10'
: active
? 'border-blue-500/40 bg-blue-500/10 shadow-lg shadow-blue-500/10'
: 'border-white/10 bg-white/[0.02] hover:border-blue-500/25 hover:bg-white/[0.04]'
}`}
>
<div className="flex items-start gap-3">
<div
className={`flex h-10 w-10 shrink-0 items-center justify-center rounded-2xl ${failed
? 'bg-red-500/10 text-red-300'
: done
? 'bg-green-500/10 text-green-300'
: active
? 'bg-blue-500/10 text-blue-300'
: 'bg-slate-900 text-slate-500 group-hover:text-blue-300'
}`}
>
<Icon size={17} />
</div>
<div className="min-w-0 flex-1">
<div className="flex items-center justify-between gap-3">
<p className="text-sm font-bold text-white">
{step.title}
</p>
<span className="font-mono text-[10px] text-slate-600">
{String(index + 1).padStart(2, '0')}
</span>
</div>
<p className="mt-1 text-xs leading-5 text-slate-500">
{step.description}
</p>
</div>
</div>
</button>
);
})}
</div>
</Card>
</div>
<Card className="col-span-7 flex min-h-0 flex-col overflow-hidden">
<div className="mb-4 flex items-center justify-between gap-3">
<div className="flex items-center gap-3">
<div className="rounded-2xl border border-blue-400/10 bg-blue-500/10 p-3 text-blue-300">
<Terminal size={22} />
</div>
<div>
<h2 className="text-lg font-bold text-white">
Log UDP2RAW
</h2>
<p className="text-sm text-slate-400">
Saída dos comandos executados no router.
</p>
</div>
</div>
<button
type="button"
disabled={running}
onClick={clearLog}
className="rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-2 text-sm font-bold text-slate-300 transition hover:border-blue-500/30 hover:bg-blue-500/10 hover:text-blue-100 disabled:cursor-not-allowed disabled:opacity-60"
>
Limpar
</button>
</div>
<pre
className={`min-h-0 flex-1 overflow-auto whitespace-pre-wrap break-words rounded-3xl border border-white/10 bg-black/25 p-5 font-mono text-xs leading-6 text-slate-300 ${scrollClass}`}
>
{log.join('\n\n') || 'Ainda não há atividade UDP2RAW.'}
</pre>
</Card>
</div>
</div>
);
}
+5
View File
@@ -16,6 +16,11 @@ export const vpsApi = {
'/api/vps/network-traffic',
),
udp2rawTraffic: () =>
apiRequest<NetworkTrafficResponse>(
'/api/vps/udp2raw-traffic',
),
rollbackLastBackup: () =>
apiRequest<{ restored: boolean }>(
'/api/vps/wireguard/rollback-last-backup',