Add OpenWRT SSH provisioning and readiness workflow

This commit is contained in:
litoral05
2026-05-06 16:05:27 +01:00
parent 3d7c7685aa
commit 11dbaca04c
5 changed files with 533 additions and 15 deletions
+245 -12
View File
@@ -1,5 +1,7 @@
import { useState } from "react";
import { ChevronDown } from "lucide-react";
import { invoke } from "@tauri-apps/api/core";
import type { RouterItem } from "../types/router";
import {
AlertTriangle,
CheckCircle2,
@@ -17,6 +19,14 @@ type DetectionResult = {
message: string;
};
type PrepareResult = {
success: boolean;
hostname_updated: boolean;
password_updated: boolean;
message: string;
logs: string;
};
type CompatibilityResult = {
compatible: boolean;
version: string | null;
@@ -44,7 +54,11 @@ type SshResult = {
message: string;
};
export function OpenWrtConfigPage() {
type Props = {
routers: RouterItem[];
};
export function OpenWrtConfigPage({ routers }: Props) {
const [detecting, setDetecting] = useState(false);
const [connecting, setConnecting] = useState(false);
const [detection, setDetection] = useState<DetectionResult | null>(null);
@@ -53,6 +67,13 @@ export function OpenWrtConfigPage() {
const [host, setHost] = useState("");
const [username, setUsername] = useState("root");
const [password, setPassword] = useState("");
const [selectedRouterId, setSelectedRouterId] = useState("");
const [newRootPassword, setNewRootPassword] = useState("");
const [confirmRootPassword, setConfirmRootPassword] = useState("");
const [routerDropdownOpen, setRouterDropdownOpen] = useState(false);
const [preparing, setPreparing] = useState(false);
const [prepareResult, setPrepareResult] = useState<PrepareResult | null>(null);
const compatibility = sshResult?.compatibility;
@@ -73,6 +94,46 @@ export function OpenWrtConfigPage() {
}
}
const sshAuthFailed =
sshResult &&
!sshResult.connected &&
sshResult.message.toLowerCase().includes("permission denied");
async function prepareRouter() {
if (!selectedRouter || !targetHostname) return;
try {
setPreparing(true);
setPrepareResult(null);
const result = await invoke<PrepareResult>("prepare_openwrt_router", {
host,
username,
password,
targetHostname,
newPassword: newRootPassword,
});
setPrepareResult(result);
if (result.success) {
setPassword(newRootPassword);
setNewRootPassword("");
setConfirmRootPassword("");
const refreshed = await invoke<SshResult>("test_openwrt_ssh", {
host,
username,
password: newRootPassword,
});
setSshResult(refreshed);
}
} finally {
setPreparing(false);
}
}
async function connectSsh() {
try {
setConnecting(true);
@@ -89,6 +150,14 @@ export function OpenWrtConfigPage() {
}
}
const selectedRouter = routers.find((router) => router.id === selectedRouterId);
const vpnIpParts = selectedRouter?.vpnIp?.split(".") ?? [];
const targetHostname = selectedRouter?.vpnIp
? `Litoral_Regas_${vpnIpParts[vpnIpParts.length - 1]}`
: "";
return (
<section className="page-stack">
<div className="page-header">
@@ -161,19 +230,31 @@ export function OpenWrtConfigPage() {
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
onChange={(e) => {
setPassword(e.target.value);
if (sshResult && !sshResult.connected) {
setSshResult(null);
}
}}
placeholder="Password SSH"
/>
{sshAuthFailed && (
<span className="form-error">
Password SSH inválida ou em falta. Introduza uma password válida.
</span>
)}
</label>
<button
className="primary"
onClick={connectSsh}
disabled={connecting || !host || !username}
>
<ShieldCheck size={16} />
{connecting ? "A ligar..." : "Ligar ao Router"}
</button>
<div className="openwrt-button-wrap">
<button
className="primary"
onClick={connectSsh}
disabled={connecting || !host || !username}
>
<ShieldCheck size={16} />
{connecting ? "A ligar..." : "Ligar ao Router"}
</button>
</div>
</div>
</div>
@@ -199,7 +280,11 @@ export function OpenWrtConfigPage() {
{!sshResult.connected && (
<div className="status-note">
<XCircle size={24} />
<p>{sshResult.message}</p>
<p>
{sshAuthFailed
? "Não foi possível autenticar. Verifique a password SSH."
: sshResult.message}
</p>
</div>
)}
</div>
@@ -311,8 +396,156 @@ export function OpenWrtConfigPage() {
</div>
</div>
)}
{sshResult?.connected && sshResult.readiness && (
<div className="dashboard-card wide-panel">
<div className="card-header">
<div>
<h2>Preparação do Router</h2>
<p>Associe este router físico a um router da plataforma</p>
</div>
</div>
<div className="openwrt-prepare-form">
<label>
Router da Plataforma
<div className="custom-select">
<button
type="button"
className="custom-select-button"
onClick={() => setRouterDropdownOpen((prev) => !prev)}
>
<span>
{selectedRouter
? `${selectedRouter.name} - ${selectedRouter.vpnIp}`
: "Selecionar router..."}
</span>
<ChevronDown
size={16}
className={`chevron ${routerDropdownOpen ? "open" : ""}`}
/>
</button>
{routerDropdownOpen && (
<div className="custom-select-menu">
{routers.map((router) => (
<button
key={router.id}
type="button"
className="custom-select-option"
onClick={() => {
setSelectedRouterId(router.id);
setRouterDropdownOpen(false);
}}
>
{router.name} - {router.vpnIp}
</button>
))}
</div>
)}
</div>
</label>
<label>
Hostname Gerado
<input value={targetHostname || "-"} readOnly />
</label>
<label>
Nova Password Root
<input
type="password"
value={newRootPassword}
onChange={(e) => setNewRootPassword(e.target.value)}
placeholder="Nova password"
/>
</label>
<label>
Confirmar Password Root
<input
type="password"
value={confirmRootPassword}
onChange={(e) => setConfirmRootPassword(e.target.value)}
placeholder="Confirmar password"
/>
{confirmRootPassword &&
newRootPassword !== confirmRootPassword && (
<span className="form-error">
As passwords não coincidem.
</span>
)}
</label>
</div>
{selectedRouter && (
<div className="detail-list">
<DetailRow label="Router selecionado" value={selectedRouter.name} />
<DetailRow label="IP VPN" value={selectedRouter.vpnIp || "-"} />
<DetailRow label="Hostname final" value={targetHostname} />
</div>
)}
<div className="modal-actions">
<button
className="primary"
onClick={prepareRouter}
disabled={
preparing ||
!selectedRouter ||
!targetHostname ||
!newRootPassword ||
newRootPassword !== confirmRootPassword
}
>
{preparing ? "A preparar..." : "Preparar Router"}
</button>
</div>
</div>
)}
{prepareResult && (
<div className="dashboard-card wide-panel">
<div className="card-header">
<div>
<h2>Resultado da Preparação</h2>
<p>Estado da configuração inicial do router</p>
</div>
<span className={`badge ${prepareResult.success ? "success" : "failed"}`}>
{prepareResult.success ? "Sucesso" : "Falhou"}
</span>
</div>
<div className="detail-list">
<DetailRow
label="Hostname"
value={prepareResult.hostname_updated ? "Atualizado" : "Falhou"}
/>
<DetailRow
label="Password Root"
value={prepareResult.password_updated ? "Atualizada" : "Falhou"}
/>
</div>
<div className="status-note">
{prepareResult.success ? <CheckCircle2 size={24} /> : <XCircle size={24} />}
<p>{prepareResult.message}</p>
</div>
{prepareResult.logs && (
<div className="log-box">
<pre>{prepareResult.logs}</pre>
</div>
)}
</div>
)}
</div>
</section>
);
}