working version before responsiveness updates
This commit is contained in:
@@ -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.
@@ -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())
|
||||
}
|
||||
|
||||
+35
-26
@@ -15,39 +15,48 @@ 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![
|
||||
read_text_file,
|
||||
ping_host,
|
||||
remove_known_host,
|
||||
probe_router_ssh,
|
||||
inspect_router_with_password,
|
||||
detect_router,
|
||||
upload_firmware,
|
||||
upload_firmware_to_router,
|
||||
flash_router,
|
||||
flash_router_sysupgrade,
|
||||
reconnect_router_after_flash,
|
||||
wait_for_ssh,
|
||||
upload_provisioning_bundle,
|
||||
run_provisioning,
|
||||
capture_wireguard_public_key,
|
||||
verify_router,
|
||||
check_router_after_flash
|
||||
])
|
||||
.invoke_handler(
|
||||
tauri::generate_handler![
|
||||
read_text_file,
|
||||
ping_host,
|
||||
remove_known_host,
|
||||
probe_router_ssh,
|
||||
inspect_router_with_password,
|
||||
detect_router,
|
||||
upload_firmware,
|
||||
upload_firmware_to_router,
|
||||
flash_router,
|
||||
flash_router_sysupgrade,
|
||||
reconnect_router_after_flash,
|
||||
wait_for_ssh,
|
||||
upload_provisioning_bundle,
|
||||
run_provisioning,
|
||||
capture_wireguard_public_key,
|
||||
verify_router,
|
||||
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");
|
||||
}
|
||||
+134
-49
@@ -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">
|
||||
<MetricCard
|
||||
title="Estado da VPN"
|
||||
value={
|
||||
vpnHealthy
|
||||
? 'Ligada'
|
||||
: 'Offline'
|
||||
}
|
||||
subtitle={
|
||||
health?.wireGuardInterface
|
||||
? `Interface ${health.wireGuardInterface}`
|
||||
: 'Estado WireGuard wg0'
|
||||
}
|
||||
icon={<ShieldCheck />}
|
||||
/>
|
||||
<div className="grid grid-cols-12 gap-4">
|
||||
<div className="col-span-3">
|
||||
<MetricCard
|
||||
title="VPN"
|
||||
value={vpnHealthy ? 'Ligada' : 'Offline'}
|
||||
subtitle={
|
||||
health?.wireGuardInterface
|
||||
? `Interface ${health.wireGuardInterface}`
|
||||
: 'WireGuard wg0'
|
||||
}
|
||||
icon={<ShieldCheck size={22} />}
|
||||
tone={vpnHealthy ? 'green' : 'red'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<MetricCard
|
||||
title="Uso da Pool IP"
|
||||
value={`${ipPoolPercent}%`}
|
||||
subtitle={`${usedCount} / ${ipPoolTotal} IPs usados`}
|
||||
icon={<Database />}
|
||||
/>
|
||||
<div className="col-span-3">
|
||||
<MetricCard
|
||||
title="UDP2RAW"
|
||||
value={udp2rawHealthy ? 'Ativo' : 'Offline'}
|
||||
subtitle={health?.udp2rawService ?? 'udp2raw-wg'}
|
||||
icon={<RadioTower size={22} />}
|
||||
tone={udp2rawHealthy ? 'purple' : 'red'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<MetricCard
|
||||
title="Uptime da VPS"
|
||||
value={
|
||||
health?.systemUptime ??
|
||||
'Desconhecido'
|
||||
}
|
||||
subtitle="Reportado pelo health da VPS"
|
||||
icon={<Clock />}
|
||||
/>
|
||||
<div className="col-span-2">
|
||||
<MetricCard
|
||||
title="Pool IP"
|
||||
value={`${ipPoolPercent}%`}
|
||||
subtitle={`${usedCount} / ${ipPoolTotal} usados`}
|
||||
icon={<Database size={22} />}
|
||||
tone="blue"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<MetricCard
|
||||
title="Estado do Backend"
|
||||
value={
|
||||
backendHealthy
|
||||
? 'Saudável'
|
||||
: 'Offline'
|
||||
}
|
||||
subtitle="Conectividade API"
|
||||
icon={<Server />}
|
||||
/>
|
||||
<div className="col-span-2">
|
||||
<MetricCard
|
||||
title="Disco"
|
||||
value={`${health?.diskUsagePercent ?? 0}%`}
|
||||
subtitle={`Memória ${health?.memoryUsagePercent ?? 0}%`}
|
||||
icon={<HardDrive size={22} />}
|
||||
tone={
|
||||
(health?.diskUsagePercent ?? 0) > 85
|
||||
? 'red'
|
||||
: 'blue'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-span-2">
|
||||
<MetricCard
|
||||
title="Backend"
|
||||
value={backendHealthy ? 'Saudável' : 'Offline'}
|
||||
subtitle="Conectividade API"
|
||||
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') {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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">
|
||||
Painel
|
||||
</h2>
|
||||
<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>
|
||||
|
||||
<p className="mt-1 text-slate-400">
|
||||
Provisionamento de routers de produção
|
||||
OpenWrt 23.05 WireGuard
|
||||
</p>
|
||||
<div>
|
||||
<h2 className="text-3xl font-black tracking-tight text-white">
|
||||
Painel
|
||||
</h2>
|
||||
|
||||
<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:{' '}
|
||||
{new Date().toLocaleTimeString()}
|
||||
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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user