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))
|
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,
|
reconnect_router_after_flash,
|
||||||
verify_router,
|
verify_router,
|
||||||
wait_for_ssh,
|
wait_for_ssh,
|
||||||
check_router_after_flash
|
check_router_after_flash,
|
||||||
},
|
upload_udp2raw_setup_script,
|
||||||
ssh::{
|
run_udp2raw_setup,
|
||||||
inspect_router_with_password,
|
test_udp2raw_tunnel,
|
||||||
probe_router_ssh,
|
check_udp2raw_router_status,
|
||||||
remove_known_host,
|
upload_udp2raw_binary
|
||||||
},
|
},
|
||||||
|
ssh::{ inspect_router_with_password, probe_router_ssh, remove_known_host },
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
tauri::Builder::default()
|
tauri::Builder
|
||||||
|
::default()
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
.plugin(tauri_plugin_fs::init())
|
.plugin(tauri_plugin_fs::init())
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(
|
||||||
read_text_file,
|
tauri::generate_handler![
|
||||||
ping_host,
|
read_text_file,
|
||||||
remove_known_host,
|
ping_host,
|
||||||
probe_router_ssh,
|
remove_known_host,
|
||||||
inspect_router_with_password,
|
probe_router_ssh,
|
||||||
detect_router,
|
inspect_router_with_password,
|
||||||
upload_firmware,
|
detect_router,
|
||||||
upload_firmware_to_router,
|
upload_firmware,
|
||||||
flash_router,
|
upload_firmware_to_router,
|
||||||
flash_router_sysupgrade,
|
flash_router,
|
||||||
reconnect_router_after_flash,
|
flash_router_sysupgrade,
|
||||||
wait_for_ssh,
|
reconnect_router_after_flash,
|
||||||
upload_provisioning_bundle,
|
wait_for_ssh,
|
||||||
run_provisioning,
|
upload_provisioning_bundle,
|
||||||
capture_wireguard_public_key,
|
run_provisioning,
|
||||||
verify_router,
|
capture_wireguard_public_key,
|
||||||
check_router_after_flash
|
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!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
}
|
}
|
||||||
+134
-49
@@ -2,6 +2,8 @@ import { useEffect, useMemo, useState } from 'react';
|
|||||||
import {
|
import {
|
||||||
Clock,
|
Clock,
|
||||||
Database,
|
Database,
|
||||||
|
HardDrive,
|
||||||
|
RadioTower,
|
||||||
Server,
|
Server,
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
@@ -15,6 +17,7 @@ import { ProvisioningWizard } from '@/components/provisioning/ProvisioningWizard
|
|||||||
import { BackendSettings } from '@/components/settings/BackendSettings';
|
import { BackendSettings } from '@/components/settings/BackendSettings';
|
||||||
import { ActivityLogs } from '@/components/activity/ActivityLogs';
|
import { ActivityLogs } from '@/components/activity/ActivityLogs';
|
||||||
import { Card } from '@/components/ui/Card';
|
import { Card } from '@/components/ui/Card';
|
||||||
|
import { Udp2rawConfig } from '@/components/udp2raw/Udp2rawConfig';
|
||||||
|
|
||||||
import { vpnApi } from '@/services/vpnApi';
|
import { vpnApi } from '@/services/vpnApi';
|
||||||
import { vpsApi } from '@/services/vpsApi';
|
import { vpsApi } from '@/services/vpsApi';
|
||||||
@@ -50,7 +53,6 @@ export function DashboardRoute() {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
setHealth(null);
|
setHealth(null);
|
||||||
setUsedIps(null);
|
setUsedIps(null);
|
||||||
|
|
||||||
setError(String(err));
|
setError(String(err));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -88,74 +90,159 @@ export function DashboardRoute() {
|
|||||||
const vpnHealthy =
|
const vpnHealthy =
|
||||||
health?.wireGuardRunning === true;
|
health?.wireGuardRunning === true;
|
||||||
|
|
||||||
|
const udp2rawHealthy =
|
||||||
|
health?.udp2rawActive === true;
|
||||||
|
|
||||||
const dashboardReady =
|
const dashboardReady =
|
||||||
!error &&
|
!error &&
|
||||||
Boolean(health) &&
|
Boolean(health) &&
|
||||||
backendHealthy &&
|
backendHealthy &&
|
||||||
vpnHealthy;
|
vpnHealthy &&
|
||||||
|
udp2rawHealthy;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col overflow-hidden">
|
<div className="flex h-full flex-col overflow-hidden">
|
||||||
<TopBar healthy={dashboardReady} />
|
<TopBar healthy={dashboardReady} />
|
||||||
|
|
||||||
{error && (
|
{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:{' '}
|
Erro no backend do painel:{' '}
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="grid grid-cols-4 gap-4">
|
<div className="grid grid-cols-12 gap-4">
|
||||||
<MetricCard
|
<div className="col-span-3">
|
||||||
title="Estado da VPN"
|
<MetricCard
|
||||||
value={
|
title="VPN"
|
||||||
vpnHealthy
|
value={vpnHealthy ? 'Ligada' : 'Offline'}
|
||||||
? 'Ligada'
|
subtitle={
|
||||||
: 'Offline'
|
health?.wireGuardInterface
|
||||||
}
|
? `Interface ${health.wireGuardInterface}`
|
||||||
subtitle={
|
: 'WireGuard wg0'
|
||||||
health?.wireGuardInterface
|
}
|
||||||
? `Interface ${health.wireGuardInterface}`
|
icon={<ShieldCheck size={22} />}
|
||||||
: 'Estado WireGuard wg0'
|
tone={vpnHealthy ? 'green' : 'red'}
|
||||||
}
|
/>
|
||||||
icon={<ShieldCheck />}
|
</div>
|
||||||
/>
|
|
||||||
|
|
||||||
<MetricCard
|
<div className="col-span-3">
|
||||||
title="Uso da Pool IP"
|
<MetricCard
|
||||||
value={`${ipPoolPercent}%`}
|
title="UDP2RAW"
|
||||||
subtitle={`${usedCount} / ${ipPoolTotal} IPs usados`}
|
value={udp2rawHealthy ? 'Ativo' : 'Offline'}
|
||||||
icon={<Database />}
|
subtitle={health?.udp2rawService ?? 'udp2raw-wg'}
|
||||||
/>
|
icon={<RadioTower size={22} />}
|
||||||
|
tone={udp2rawHealthy ? 'purple' : 'red'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<MetricCard
|
<div className="col-span-2">
|
||||||
title="Uptime da VPS"
|
<MetricCard
|
||||||
value={
|
title="Pool IP"
|
||||||
health?.systemUptime ??
|
value={`${ipPoolPercent}%`}
|
||||||
'Desconhecido'
|
subtitle={`${usedCount} / ${ipPoolTotal} usados`}
|
||||||
}
|
icon={<Database size={22} />}
|
||||||
subtitle="Reportado pelo health da VPS"
|
tone="blue"
|
||||||
icon={<Clock />}
|
/>
|
||||||
/>
|
</div>
|
||||||
|
|
||||||
<MetricCard
|
<div className="col-span-2">
|
||||||
title="Estado do Backend"
|
<MetricCard
|
||||||
value={
|
title="Disco"
|
||||||
backendHealthy
|
value={`${health?.diskUsagePercent ?? 0}%`}
|
||||||
? 'Saudável'
|
subtitle={`Memória ${health?.memoryUsagePercent ?? 0}%`}
|
||||||
: 'Offline'
|
icon={<HardDrive size={22} />}
|
||||||
}
|
tone={
|
||||||
subtitle="Conectividade API"
|
(health?.diskUsagePercent ?? 0) > 85
|
||||||
icon={<Server />}
|
? '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>
|
||||||
|
|
||||||
<div className="mt-4 grid min-h-0 flex-1 grid-cols-12 gap-4 overflow-hidden pb-4">
|
<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">
|
<div className="col-span-7 min-h-0">
|
||||||
<NetworkTrafficChart />
|
<NetworkTrafficChart
|
||||||
|
title="Tráfego WireGuard"
|
||||||
|
subtitle="Débito RX/TX wg0 em direto"
|
||||||
|
mode="wireguard"
|
||||||
|
/>
|
||||||
</div>
|
</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
|
<IpPoolChart
|
||||||
used={usedCount}
|
used={usedCount}
|
||||||
total={ipPoolTotal}
|
total={ipPoolTotal}
|
||||||
@@ -199,9 +286,7 @@ export function RouteView({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (active === 'Configuração UDP2RAW') {
|
if (active === 'Configuração UDP2RAW') {
|
||||||
return (
|
return <Udp2rawConfig />;
|
||||||
<PlaceholderRoute name="Configuração UDP2RAW" />
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (active === 'Registos de Atividade') {
|
if (active === 'Registos de Atividade') {
|
||||||
|
|||||||
@@ -2,11 +2,51 @@ import { ReactNode } from 'react';
|
|||||||
|
|
||||||
import { Card } from '@/components/ui/Card';
|
import { Card } from '@/components/ui/Card';
|
||||||
|
|
||||||
|
type MetricCardTone =
|
||||||
|
| 'blue'
|
||||||
|
| 'green'
|
||||||
|
| 'purple'
|
||||||
|
| 'red';
|
||||||
|
|
||||||
type MetricCardProps = {
|
type MetricCardProps = {
|
||||||
title: string;
|
title: string;
|
||||||
value: string;
|
value: string;
|
||||||
subtitle: string;
|
subtitle: string;
|
||||||
icon: ReactNode;
|
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({
|
export function MetricCard({
|
||||||
@@ -14,24 +54,35 @@ export function MetricCard({
|
|||||||
value,
|
value,
|
||||||
subtitle,
|
subtitle,
|
||||||
icon,
|
icon,
|
||||||
|
tone = 'blue',
|
||||||
}: MetricCardProps) {
|
}: MetricCardProps) {
|
||||||
|
const style = toneStyles[tone];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="h-full">
|
<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="flex items-start gap-4">
|
<div
|
||||||
<div className="rounded-2xl bg-blue-500/10 p-3 text-blue-300">
|
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}
|
{icon}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div className="min-w-0 flex-1">
|
||||||
<p className="text-sm text-slate-400">
|
<p className="text-sm font-medium text-slate-400">
|
||||||
{title}
|
{title}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h3 className="mt-1 text-2xl font-bold text-white">
|
<h3 className="mt-1 text-3xl font-black tracking-tight text-white">
|
||||||
{value}
|
{value}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<p className="mt-1 text-xs text-slate-400">
|
<p className="mt-2 truncate text-xs text-slate-500">
|
||||||
{subtitle}
|
{subtitle}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,6 +14,17 @@ import { Card } from '@/components/ui/Card';
|
|||||||
|
|
||||||
import { vpsApi } from '@/services/vpsApi';
|
import { vpsApi } from '@/services/vpsApi';
|
||||||
|
|
||||||
|
type TrafficMode =
|
||||||
|
| 'wireguard'
|
||||||
|
| 'udp2raw';
|
||||||
|
|
||||||
|
type NetworkTrafficChartProps = {
|
||||||
|
title?: string;
|
||||||
|
subtitle?: string;
|
||||||
|
mode?: TrafficMode;
|
||||||
|
compact?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
type TrafficPoint = {
|
type TrafficPoint = {
|
||||||
time: string;
|
time: string;
|
||||||
downloadMbps: number;
|
downloadMbps: number;
|
||||||
@@ -22,7 +33,12 @@ type TrafficPoint = {
|
|||||||
|
|
||||||
const MAX_POINTS = 24;
|
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<
|
const [points, setPoints] = useState<
|
||||||
TrafficPoint[]
|
TrafficPoint[]
|
||||||
>([]);
|
>([]);
|
||||||
@@ -35,7 +51,9 @@ export function NetworkTrafficChart() {
|
|||||||
async function loadTraffic() {
|
async function loadTraffic() {
|
||||||
try {
|
try {
|
||||||
const response =
|
const response =
|
||||||
await vpsApi.networkTraffic();
|
mode === 'udp2raw'
|
||||||
|
? await vpsApi.udp2rawTraffic()
|
||||||
|
: await vpsApi.networkTraffic();
|
||||||
|
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return;
|
return;
|
||||||
@@ -66,6 +84,9 @@ export function NetworkTrafficChart() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setPoints([]);
|
||||||
|
setError('');
|
||||||
|
|
||||||
loadTraffic();
|
loadTraffic();
|
||||||
|
|
||||||
const intervalId = window.setInterval(
|
const intervalId = window.setInterval(
|
||||||
@@ -77,23 +98,28 @@ export function NetworkTrafficChart() {
|
|||||||
mounted = false;
|
mounted = false;
|
||||||
window.clearInterval(intervalId);
|
window.clearInterval(intervalId);
|
||||||
};
|
};
|
||||||
}, []);
|
}, [mode]);
|
||||||
|
|
||||||
|
const latestPoint =
|
||||||
|
points[points.length - 1];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="h-full">
|
<Card className="relative h-full overflow-hidden">
|
||||||
<div className="mb-4 flex items-center justify-between">
|
<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>
|
|
||||||
<h3 className="font-semibold text-white">
|
<div className="mb-4 flex items-start justify-between gap-4">
|
||||||
Tráfego de Rede
|
<div className="min-w-0">
|
||||||
|
<h3 className="truncate font-semibold text-white">
|
||||||
|
{title}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<p className="text-xs text-slate-500">
|
<p className="mt-1 truncate text-xs text-slate-500">
|
||||||
Débito RX/TX wg0 em direto
|
{subtitle}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span className="rounded-lg border border-white/10 bg-slate-900 px-3 py-1 text-xs text-slate-400">
|
<span className="shrink-0 rounded-lg border border-white/10 bg-slate-900 px-3 py-1 text-xs text-slate-400">
|
||||||
Atualização 3s
|
3s
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -103,12 +129,59 @@ export function NetworkTrafficChart() {
|
|||||||
</div>
|
</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
|
<ResponsiveContainer
|
||||||
width="100%"
|
width="100%"
|
||||||
height="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
|
<CartesianGrid
|
||||||
strokeDasharray="3 3"
|
strokeDasharray="3 3"
|
||||||
stroke="rgba(148,163,184,.12)"
|
stroke="rgba(148,163,184,.12)"
|
||||||
@@ -117,13 +190,15 @@ export function NetworkTrafficChart() {
|
|||||||
<XAxis
|
<XAxis
|
||||||
dataKey="time"
|
dataKey="time"
|
||||||
stroke="#94a3b8"
|
stroke="#94a3b8"
|
||||||
fontSize={12}
|
fontSize={compact ? 10 : 12}
|
||||||
minTickGap={24}
|
minTickGap={compact ? 32 : 24}
|
||||||
|
tick={compact ? false : undefined}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<YAxis
|
<YAxis
|
||||||
stroke="#94a3b8"
|
stroke="#94a3b8"
|
||||||
fontSize={12}
|
fontSize={compact ? 10 : 12}
|
||||||
|
width={compact ? 42 : 60}
|
||||||
tickFormatter={(value) =>
|
tickFormatter={(value) =>
|
||||||
`${value} Mbps`
|
`${value} Mbps`
|
||||||
}
|
}
|
||||||
@@ -147,7 +222,11 @@ export function NetworkTrafficChart() {
|
|||||||
<Area
|
<Area
|
||||||
type="monotone"
|
type="monotone"
|
||||||
dataKey="downloadMbps"
|
dataKey="downloadMbps"
|
||||||
name="Download"
|
name={
|
||||||
|
mode === 'udp2raw'
|
||||||
|
? 'Entrada'
|
||||||
|
: 'Download'
|
||||||
|
}
|
||||||
stroke="#19d16f"
|
stroke="#19d16f"
|
||||||
fill="#19d16f"
|
fill="#19d16f"
|
||||||
fillOpacity={0.2}
|
fillOpacity={0.2}
|
||||||
@@ -156,7 +235,11 @@ export function NetworkTrafficChart() {
|
|||||||
<Area
|
<Area
|
||||||
type="monotone"
|
type="monotone"
|
||||||
dataKey="uploadMbps"
|
dataKey="uploadMbps"
|
||||||
name="Upload"
|
name={
|
||||||
|
mode === 'udp2raw'
|
||||||
|
? 'Saída'
|
||||||
|
: 'Upload'
|
||||||
|
}
|
||||||
stroke="#3b82f6"
|
stroke="#3b82f6"
|
||||||
fill="#3b82f6"
|
fill="#3b82f6"
|
||||||
fillOpacity={0.16}
|
fillOpacity={0.16}
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { RefreshCw } from 'lucide-react';
|
import { RefreshCw, ShieldCheck, ShieldX } from 'lucide-react';
|
||||||
|
|
||||||
import { Badge } from '@/components/ui/Badge';
|
|
||||||
|
|
||||||
type TopBarProps = {
|
type TopBarProps = {
|
||||||
healthy?: boolean;
|
healthy?: boolean;
|
||||||
@@ -10,35 +8,58 @@ export function TopBar({
|
|||||||
healthy = false,
|
healthy = false,
|
||||||
}: TopBarProps) {
|
}: TopBarProps) {
|
||||||
return (
|
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>
|
<div>
|
||||||
<h2 className="text-3xl font-bold tracking-tight text-white">
|
<div className="flex items-center gap-3">
|
||||||
Painel
|
<div
|
||||||
</h2>
|
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">
|
<div>
|
||||||
Provisionamento de routers de produção
|
<h2 className="text-3xl font-black tracking-tight text-white">
|
||||||
OpenWrt 23.05 WireGuard
|
Painel
|
||||||
</p>
|
</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>
|
||||||
|
|
||||||
<div className="flex items-center gap-4 text-sm text-slate-400">
|
<div className="flex items-center gap-3">
|
||||||
<div className="flex items-center gap-2">
|
<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={16} />
|
<RefreshCw size={15} />
|
||||||
|
|
||||||
<span>
|
<span>
|
||||||
Última atualização:{' '}
|
Atualizado às{' '}
|
||||||
{new Date().toLocaleTimeString()}
|
<span className="font-mono text-slate-200">
|
||||||
|
{new Date().toLocaleTimeString()}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Badge
|
<div
|
||||||
tone={healthy ? 'green' : 'red'}
|
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
|
{healthy
|
||||||
? 'Todos os sistemas operacionais'
|
? 'Sistemas operacionais'
|
||||||
: 'Problemas detetados no sistema'}
|
: 'Atenção necessária'}
|
||||||
</Badge>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</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',
|
'/api/vps/network-traffic',
|
||||||
),
|
),
|
||||||
|
|
||||||
|
udp2rawTraffic: () =>
|
||||||
|
apiRequest<NetworkTrafficResponse>(
|
||||||
|
'/api/vps/udp2raw-traffic',
|
||||||
|
),
|
||||||
|
|
||||||
rollbackLastBackup: () =>
|
rollbackLastBackup: () =>
|
||||||
apiRequest<{ restored: boolean }>(
|
apiRequest<{ restored: boolean }>(
|
||||||
'/api/vps/wireguard/rollback-last-backup',
|
'/api/vps/wireguard/rollback-last-backup',
|
||||||
|
|||||||
Reference in New Issue
Block a user