Translate to pt, fix some ui details, add proper icon and logo
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 103 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 119 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
</adaptive-icon>
|
||||
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 129 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 214 KiB |
|
After Width: | Height: | Size: 56 KiB |
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#fff</color>
|
||||
</resources>
|
||||
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 270 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 780 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 36 KiB |
@@ -1,15 +1,43 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { LoginScreen } from '@/components/login/LoginScreen';
|
||||
import { AppShell } from '@/components/layout/AppShell';
|
||||
import { RouteView } from './routes';
|
||||
|
||||
const AUTH_KEY = 'lr-openwrt-tool.authenticated';
|
||||
|
||||
export default function App() {
|
||||
const [active, setActive] = useState('Dashboard');
|
||||
const [authenticated, setAuthenticated] =
|
||||
useState(() => localStorage.getItem(AUTH_KEY) === 'true');
|
||||
|
||||
const [active, setActive] =
|
||||
useState('Painel');
|
||||
|
||||
function handleLogin(keepLoggedIn: boolean) {
|
||||
setAuthenticated(true);
|
||||
|
||||
if (keepLoggedIn) {
|
||||
localStorage.setItem(AUTH_KEY, 'true');
|
||||
} else {
|
||||
localStorage.removeItem(AUTH_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
function handleLogout() {
|
||||
localStorage.removeItem(AUTH_KEY);
|
||||
setAuthenticated(false);
|
||||
setActive('Painel');
|
||||
}
|
||||
|
||||
if (!authenticated) {
|
||||
return <LoginScreen onLogin={handleLogin} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<AppShell
|
||||
active={active}
|
||||
onSelect={setActive}
|
||||
onLogout={handleLogout}
|
||||
>
|
||||
<RouteView active={active} />
|
||||
</AppShell>
|
||||
|
||||
@@ -100,52 +100,52 @@ export function DashboardRoute() {
|
||||
|
||||
{error && (
|
||||
<div className="mb-3 rounded-xl border border-red-500/20 bg-red-500/10 p-3 text-sm text-red-300">
|
||||
Dashboard backend error:{' '}
|
||||
Erro no backend do painel:{' '}
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<MetricCard
|
||||
title="VPN Status"
|
||||
title="Estado da VPN"
|
||||
value={
|
||||
vpnHealthy
|
||||
? 'Connected'
|
||||
? 'Ligada'
|
||||
: 'Offline'
|
||||
}
|
||||
subtitle={
|
||||
health?.wireGuardInterface
|
||||
? `${health.wireGuardInterface} interface`
|
||||
: 'WireGuard wg0 status'
|
||||
? `Interface ${health.wireGuardInterface}`
|
||||
: 'Estado WireGuard wg0'
|
||||
}
|
||||
icon={<ShieldCheck />}
|
||||
/>
|
||||
|
||||
<MetricCard
|
||||
title="IP Pool Usage"
|
||||
title="Uso da Pool IP"
|
||||
value={`${ipPoolPercent}%`}
|
||||
subtitle={`${usedCount} / ${ipPoolTotal} IPs used`}
|
||||
subtitle={`${usedCount} / ${ipPoolTotal} IPs usados`}
|
||||
icon={<Database />}
|
||||
/>
|
||||
|
||||
<MetricCard
|
||||
title="VPS Uptime"
|
||||
title="Uptime da VPS"
|
||||
value={
|
||||
health?.systemUptime ??
|
||||
'Unknown'
|
||||
'Desconhecido'
|
||||
}
|
||||
subtitle="Reported by VPS health"
|
||||
subtitle="Reportado pelo health da VPS"
|
||||
icon={<Clock />}
|
||||
/>
|
||||
|
||||
<MetricCard
|
||||
title="Backend Health"
|
||||
title="Estado do Backend"
|
||||
value={
|
||||
backendHealthy
|
||||
? 'Healthy'
|
||||
? 'Saudável'
|
||||
: 'Offline'
|
||||
}
|
||||
subtitle="API connectivity"
|
||||
subtitle="Conectividade API"
|
||||
icon={<Server />}
|
||||
/>
|
||||
</div>
|
||||
@@ -178,8 +178,8 @@ export function PlaceholderRoute({
|
||||
</h2>
|
||||
|
||||
<p className="mt-2 text-slate-400">
|
||||
Screen scaffold ready for production
|
||||
implementation.
|
||||
Estrutura do ecrã pronta para
|
||||
implementação em produção.
|
||||
</p>
|
||||
</Card>
|
||||
);
|
||||
@@ -190,25 +190,25 @@ export function RouteView({
|
||||
}: {
|
||||
active: string;
|
||||
}) {
|
||||
if (active === 'Dashboard') {
|
||||
if (active === 'Painel') {
|
||||
return <DashboardRoute />;
|
||||
}
|
||||
|
||||
if (active === 'Provisioning') {
|
||||
if (active === 'Provisionamento') {
|
||||
return <ProvisioningWizard />;
|
||||
}
|
||||
|
||||
if (active === 'UDP2RAW Config') {
|
||||
if (active === 'Configuração UDP2RAW') {
|
||||
return (
|
||||
<PlaceholderRoute name="UDP2RAW Config" />
|
||||
<PlaceholderRoute name="Configuração UDP2RAW" />
|
||||
);
|
||||
}
|
||||
|
||||
if (active === 'Activity Logs') {
|
||||
if (active === 'Registos de Atividade') {
|
||||
return <ActivityLogs />;
|
||||
}
|
||||
|
||||
if (active === 'Workstation') {
|
||||
if (active === 'Posto de Trabalho') {
|
||||
return <BackendSettings />;
|
||||
}
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 272 KiB |
@@ -38,6 +38,22 @@ const sources: Array<'all' | ActivityLogSource> = [
|
||||
'vps',
|
||||
];
|
||||
|
||||
const levelLabels: Record<'all' | ActivityLogLevel, string> = {
|
||||
all: 'Todos',
|
||||
info: 'Info',
|
||||
success: 'Sucesso',
|
||||
warning: 'Aviso',
|
||||
error: 'Erro',
|
||||
};
|
||||
|
||||
const sourceLabels: Record<'all' | ActivityLogSource, string> = {
|
||||
all: 'Todas',
|
||||
desktop: 'Desktop',
|
||||
router: 'Router',
|
||||
backend: 'Backend',
|
||||
vps: 'VPS',
|
||||
};
|
||||
|
||||
function levelTone(level: ActivityLogLevel) {
|
||||
if (level === 'success') return 'green';
|
||||
if (level === 'warning') return 'purple';
|
||||
@@ -120,12 +136,12 @@ export function ActivityLogs() {
|
||||
<div className="mb-5 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold tracking-tight text-white">
|
||||
Activity Logs
|
||||
Registos de Atividade
|
||||
</h2>
|
||||
|
||||
<p className="mt-1 text-slate-400">
|
||||
Local provisioning audit trail for
|
||||
technicians.
|
||||
Histórico local de auditoria de
|
||||
provisionamento para técnicos.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -138,7 +154,7 @@ export function ActivityLogs() {
|
||||
}}
|
||||
>
|
||||
<Download size={16} />
|
||||
Export JSON
|
||||
Exportar JSON
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
@@ -150,7 +166,7 @@ export function ActivityLogs() {
|
||||
}}
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
Clear Logs
|
||||
Limpar Registos
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -159,7 +175,7 @@ export function ActivityLogs() {
|
||||
<div className="grid grid-cols-12 gap-4">
|
||||
<div className="col-span-6">
|
||||
<label className="mb-2 block text-xs font-medium uppercase tracking-wide text-slate-500">
|
||||
Search
|
||||
Pesquisa
|
||||
</label>
|
||||
|
||||
<div className="flex items-center gap-2 rounded-xl border border-white/10 bg-ink-950 px-3">
|
||||
@@ -173,7 +189,7 @@ export function ActivityLogs() {
|
||||
onChange={(event) =>
|
||||
setQuery(event.target.value)
|
||||
}
|
||||
placeholder="Search action, VPN IP, router IP, message..."
|
||||
placeholder="Pesquisar ação, IP VPN, IP router, mensagem..."
|
||||
className="w-full bg-transparent py-3 text-sm text-white outline-none placeholder:text-slate-600"
|
||||
/>
|
||||
</div>
|
||||
@@ -181,7 +197,7 @@ export function ActivityLogs() {
|
||||
|
||||
<div className="col-span-3">
|
||||
<label className="mb-2 block text-xs font-medium uppercase tracking-wide text-slate-500">
|
||||
Level
|
||||
Nível
|
||||
</label>
|
||||
|
||||
<Select
|
||||
@@ -189,14 +205,14 @@ export function ActivityLogs() {
|
||||
onChange={setLevelFilter}
|
||||
options={levels.map((level) => ({
|
||||
value: level,
|
||||
label: level,
|
||||
label: levelLabels[level],
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-span-3">
|
||||
<label className="mb-2 block text-xs font-medium uppercase tracking-wide text-slate-500">
|
||||
Source
|
||||
Origem
|
||||
</label>
|
||||
|
||||
<Select
|
||||
@@ -204,7 +220,7 @@ export function ActivityLogs() {
|
||||
onChange={setSourceFilter}
|
||||
options={sources.map((source) => ({
|
||||
value: source,
|
||||
label: source,
|
||||
label: sourceLabels[source],
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
@@ -214,11 +230,11 @@ export function ActivityLogs() {
|
||||
<Card className="relative z-10 flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h3 className="font-semibold text-white">
|
||||
Audit Events
|
||||
Eventos de Auditoria
|
||||
</h3>
|
||||
|
||||
<span className="text-sm text-slate-500">
|
||||
{filteredLogs.length} shown /{' '}
|
||||
{filteredLogs.length} apresentados /{' '}
|
||||
{logs.length} total
|
||||
</span>
|
||||
</div>
|
||||
@@ -235,27 +251,27 @@ export function ActivityLogs() {
|
||||
<thead className="sticky top-0 z-10 bg-slate-950 text-left text-xs uppercase tracking-wide text-slate-500">
|
||||
<tr>
|
||||
<th className="w-[180px] px-4 py-3">
|
||||
Time
|
||||
Hora
|
||||
</th>
|
||||
|
||||
<th className="w-[110px] px-4 py-3">
|
||||
Level
|
||||
Nível
|
||||
</th>
|
||||
|
||||
<th className="w-[120px] px-4 py-3">
|
||||
Source
|
||||
Origem
|
||||
</th>
|
||||
|
||||
<th className="w-[190px] px-4 py-3">
|
||||
Action
|
||||
Ação
|
||||
</th>
|
||||
|
||||
<th className="px-4 py-3">
|
||||
Message
|
||||
Mensagem
|
||||
</th>
|
||||
|
||||
<th className="w-[160px] px-4 py-3">
|
||||
VPN IP
|
||||
IP VPN
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -274,12 +290,12 @@ export function ActivityLogs() {
|
||||
|
||||
<td className="px-4 py-3">
|
||||
<Badge tone={levelTone(log.level)}>
|
||||
{log.level}
|
||||
{levelLabels[log.level]}
|
||||
</Badge>
|
||||
</td>
|
||||
|
||||
<td className="px-4 py-3 text-slate-300">
|
||||
{log.source}
|
||||
{sourceLabels[log.source]}
|
||||
</td>
|
||||
|
||||
<td className="truncate px-4 py-3 font-medium text-white">
|
||||
@@ -302,7 +318,7 @@ export function ActivityLogs() {
|
||||
colSpan={6}
|
||||
className="px-4 py-10 text-center text-slate-500"
|
||||
>
|
||||
No activity logs found.
|
||||
Nenhum registo de atividade encontrado.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
|
||||
@@ -28,11 +28,11 @@ export function IpPoolChart({
|
||||
|
||||
const data = [
|
||||
{
|
||||
name: 'Used',
|
||||
name: 'Usados',
|
||||
value: used,
|
||||
},
|
||||
{
|
||||
name: 'Available',
|
||||
name: 'Disponíveis',
|
||||
value: available,
|
||||
},
|
||||
];
|
||||
@@ -41,11 +41,11 @@ export function IpPoolChart({
|
||||
<Card className="flex h-full flex-col">
|
||||
<div>
|
||||
<h3 className="font-semibold text-white">
|
||||
IP Pool Usage
|
||||
Uso da Pool IP
|
||||
</h3>
|
||||
|
||||
<p className="text-xs text-slate-500">
|
||||
WireGuard allocation capacity
|
||||
Capacidade de atribuição WireGuard
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -82,7 +82,7 @@ export function IpPoolChart({
|
||||
</p>
|
||||
|
||||
<p className="mt-1 text-xs text-slate-500">
|
||||
pool used
|
||||
pool usada
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -90,7 +90,7 @@ export function IpPoolChart({
|
||||
<div className="mt-8 space-y-4">
|
||||
<div className="rounded-2xl border border-white/10 bg-white/[0.03] p-4">
|
||||
<p className="text-xs uppercase tracking-wide text-slate-500">
|
||||
Used IPs
|
||||
IPs Usados
|
||||
</p>
|
||||
|
||||
<p className="mt-1 text-2xl font-bold text-green-300">
|
||||
@@ -100,7 +100,7 @@ export function IpPoolChart({
|
||||
|
||||
<div className="rounded-2xl border border-white/10 bg-white/[0.03] p-4">
|
||||
<p className="text-xs uppercase tracking-wide text-slate-500">
|
||||
Available IPs
|
||||
IPs Disponíveis
|
||||
</p>
|
||||
|
||||
<p className="mt-1 text-2xl font-bold text-blue-300">
|
||||
@@ -110,7 +110,7 @@ export function IpPoolChart({
|
||||
|
||||
<div className="rounded-2xl border border-white/10 bg-white/[0.03] p-4">
|
||||
<div className="mb-2 flex justify-between text-xs text-slate-500">
|
||||
<span>Capacity</span>
|
||||
<span>Capacidade</span>
|
||||
<span>{used} / {total}</span>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -84,16 +84,16 @@ export function NetworkTrafficChart() {
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="font-semibold text-white">
|
||||
Network Traffic
|
||||
Tráfego de Rede
|
||||
</h3>
|
||||
|
||||
<p className="text-xs text-slate-500">
|
||||
Live wg0 RX/TX throughput
|
||||
Débito RX/TX wg0 em direto
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<span className="rounded-lg border border-white/10 bg-slate-900 px-3 py-1 text-xs text-slate-400">
|
||||
3s refresh
|
||||
Atualização 3s
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
import {
|
||||
CheckCircle,
|
||||
Cpu,
|
||||
FileUp,
|
||||
KeyRound,
|
||||
Network,
|
||||
Plug,
|
||||
RefreshCw,
|
||||
ShieldCheck,
|
||||
} from 'lucide-react';
|
||||
|
||||
import { Card } from '@/components/ui/Card';
|
||||
|
||||
const steps = [
|
||||
['Connect Router', Plug],
|
||||
['Upload Firmware', FileUp],
|
||||
['Flash Firmware', Cpu],
|
||||
['Wait & Reconnect', RefreshCw],
|
||||
['Provision Router', Network],
|
||||
['Get Public Key', KeyRound],
|
||||
['Register Peer', ShieldCheck],
|
||||
['Verify Connection', CheckCircle],
|
||||
] as const;
|
||||
|
||||
export function ProvisioningWorkflow() {
|
||||
return (
|
||||
<Card className="h-full">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="font-semibold text-white">
|
||||
Provisioning Workflow
|
||||
</h3>
|
||||
|
||||
<p className="mt-1 text-sm text-slate-400">
|
||||
OpenWrt 23.05 production
|
||||
provisioning pipeline
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<span className="rounded-full border border-green-500/20 bg-green-500/10 px-3 py-1 text-xs text-green-300">
|
||||
Technician Mode
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 grid grid-cols-2 gap-4 md:grid-cols-4 xl:grid-cols-8">
|
||||
{steps.map(([step, Icon], index) => (
|
||||
<div
|
||||
key={step}
|
||||
className="relative rounded-2xl border border-white/10 bg-white/[0.03] p-4 transition-all hover:border-blue-500/30 hover:bg-white/[0.05]"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-slate-500">
|
||||
Step {index + 1}
|
||||
</span>
|
||||
|
||||
<Icon
|
||||
className={`h-5 w-5 ${
|
||||
index === 6
|
||||
? 'text-green-300'
|
||||
: 'text-slate-400'
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p className="mt-4 text-sm font-semibold text-white">
|
||||
{step}
|
||||
</p>
|
||||
|
||||
{index !== steps.length - 1 && (
|
||||
<div className="absolute -right-2 top-1/2 hidden h-px w-4 bg-white/10 xl:block" />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import { Card } from '@/components/ui/Card';
|
||||
|
||||
const rows = [
|
||||
'Peer added 198.19.254.3',
|
||||
'Peer updated 198.19.254.2',
|
||||
'Backup created wg0.conf.bak',
|
||||
'Health check all systems operational',
|
||||
];
|
||||
|
||||
export function RecentActivityPanel() {
|
||||
return (
|
||||
<Card className="h-full">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-semibold text-white">
|
||||
Recent Activity
|
||||
</h3>
|
||||
|
||||
<span className="text-xs text-slate-500">
|
||||
Live Feed
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 divide-y divide-white/10">
|
||||
{rows.map((row, index) => (
|
||||
<div
|
||||
key={row}
|
||||
className="flex items-center justify-between py-3 text-sm"
|
||||
>
|
||||
<span className="text-slate-200">
|
||||
{row}
|
||||
</span>
|
||||
|
||||
<span className="text-xs text-slate-500">
|
||||
{index
|
||||
? `${index * 15}m ago`
|
||||
: '2m ago'}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import { Card } from '@/components/ui/Card';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { StatusDot } from '@/components/ui/StatusDot';
|
||||
|
||||
const services = [
|
||||
'WireGuard wg0',
|
||||
'DNAT rules',
|
||||
'IP forwarding',
|
||||
'Persistent keepalive',
|
||||
'Backup system',
|
||||
];
|
||||
|
||||
export function ServicesHealthPanel() {
|
||||
return (
|
||||
<Card className="h-full">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-semibold text-white">
|
||||
Services Health
|
||||
</h3>
|
||||
|
||||
<span className="text-xs text-slate-500">
|
||||
fw4 / nftables
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 space-y-4">
|
||||
{services.map((service) => (
|
||||
<div
|
||||
key={service}
|
||||
className="flex items-center justify-between rounded-xl border border-white/5 bg-white/[0.02] px-4 py-3"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<StatusDot />
|
||||
|
||||
<div>
|
||||
<p className="text-sm font-medium text-white">
|
||||
{service}
|
||||
</p>
|
||||
|
||||
<p className="text-xs text-slate-500">
|
||||
Running baseline check
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Badge>
|
||||
Running
|
||||
</Badge>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -5,16 +5,22 @@ import { Sidebar } from './Sidebar';
|
||||
type AppShellProps = PropsWithChildren<{
|
||||
active: string;
|
||||
onSelect: (value: string) => void;
|
||||
onLogout: () => void;
|
||||
}>;
|
||||
|
||||
export function AppShell({
|
||||
active,
|
||||
onSelect,
|
||||
onLogout,
|
||||
children,
|
||||
}: AppShellProps) {
|
||||
return (
|
||||
<div className="flex h-screen overflow-hidden bg-ink-950 text-white">
|
||||
<Sidebar active={active} onSelect={onSelect} />
|
||||
<Sidebar
|
||||
active={active}
|
||||
onSelect={onSelect}
|
||||
onLogout={onLogout}
|
||||
/>
|
||||
|
||||
<main className="flex-1 overflow-hidden p-4">
|
||||
{children}
|
||||
|
||||
@@ -1,42 +1,49 @@
|
||||
import {
|
||||
FileClock,
|
||||
Gauge,
|
||||
LogOut,
|
||||
RadioTower,
|
||||
Settings,
|
||||
Shield,
|
||||
Wrench,
|
||||
} from 'lucide-react';
|
||||
|
||||
import logoIcon from '@/assets/logo-icon.png';
|
||||
|
||||
const items = [
|
||||
['Dashboard', Gauge],
|
||||
['UDP2RAW Config', RadioTower],
|
||||
['Provisioning', Wrench],
|
||||
['Activity Logs', FileClock],
|
||||
['Workstation', Settings],
|
||||
['Painel', Gauge],
|
||||
['Configuração UDP2RAW', RadioTower],
|
||||
['Provisionamento', Wrench],
|
||||
['Registos de Atividade', FileClock],
|
||||
['Posto de Trabalho', Settings],
|
||||
] as const;
|
||||
|
||||
type SidebarProps = {
|
||||
active: string;
|
||||
onSelect: (value: string) => void;
|
||||
onLogout: () => void;
|
||||
};
|
||||
|
||||
export function Sidebar({
|
||||
active,
|
||||
onSelect,
|
||||
onLogout,
|
||||
}: SidebarProps) {
|
||||
return (
|
||||
<aside className="flex h-screen w-64 flex-col border-r border-white/10 bg-ink-950 px-4 py-5">
|
||||
<div className="mb-8 flex items-center gap-3">
|
||||
<div className="rounded-2xl bg-blue-500/15 p-3 text-blue-300">
|
||||
<Shield />
|
||||
</div>
|
||||
<div className="mb-10 flex items-center gap-4 px-1">
|
||||
<img
|
||||
src={logoIcon}
|
||||
alt="Litoral Regas"
|
||||
className="h-12 w-12 shrink-0 object-contain"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<h1 className="font-bold text-white">
|
||||
<div className="min-w-0">
|
||||
<h1 className="truncate text-[20px] font-bold leading-none text-white">
|
||||
Litoral Regas
|
||||
</h1>
|
||||
|
||||
<p className="text-xs text-slate-400">
|
||||
<p className="mt-1 text-[11px] font-medium uppercase tracking-[0.12em] text-blue-300/70">
|
||||
VPN Orchestrator
|
||||
</p>
|
||||
</div>
|
||||
@@ -51,11 +58,10 @@ export function Sidebar({
|
||||
key={label}
|
||||
type="button"
|
||||
onClick={() => onSelect(label)}
|
||||
className={`flex w-full items-center gap-3 rounded-xl px-3 py-3 text-left text-sm font-medium transition ${
|
||||
isActive
|
||||
? 'bg-blue-500/15 text-blue-200'
|
||||
: 'text-slate-300 hover:bg-white/5 hover:text-white'
|
||||
}`}
|
||||
className={`flex w-full items-center gap-3 rounded-xl px-3 py-3 text-left text-sm font-medium transition ${isActive
|
||||
? 'bg-blue-500/15 text-blue-200'
|
||||
: 'text-slate-300 hover:bg-white/5 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<Icon size={18} />
|
||||
{label}
|
||||
@@ -64,19 +70,15 @@ export function Sidebar({
|
||||
})}
|
||||
</nav>
|
||||
|
||||
<div className="mt-auto rounded-2xl border border-white/10 bg-white/[0.03] p-4">
|
||||
<div className="flex items-center gap-2 text-sm text-green-300">
|
||||
<span className="h-2 w-2 rounded-full bg-green-400" />
|
||||
Backend connected
|
||||
</div>
|
||||
|
||||
<p className="mt-2 text-xs text-slate-400">
|
||||
localhost:8080
|
||||
</p>
|
||||
|
||||
<div className="mt-5 border-t border-white/10 pt-4 text-xs text-slate-500">
|
||||
Version 1.0.0
|
||||
</div>
|
||||
<div className="mt-auto border-t border-white/10 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onLogout}
|
||||
className="flex w-full items-center gap-3 rounded-xl px-3 py-3 text-left text-sm font-medium text-slate-300 transition hover:bg-red-500/10 hover:text-red-200"
|
||||
>
|
||||
<LogOut size={18} />
|
||||
Terminar Sessão
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
|
||||
@@ -13,12 +13,12 @@ export function TopBar({
|
||||
<header className="mb-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold tracking-tight text-white">
|
||||
Dashboard
|
||||
Painel
|
||||
</h2>
|
||||
|
||||
<p className="mt-1 text-slate-400">
|
||||
Provisionamento de routers de produção
|
||||
OpenWrt 23.05 WireGuard
|
||||
production router provisioning
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -27,7 +27,7 @@ export function TopBar({
|
||||
<RefreshCw size={16} />
|
||||
|
||||
<span>
|
||||
Last updated:{' '}
|
||||
Última atualização:{' '}
|
||||
{new Date().toLocaleTimeString()}
|
||||
</span>
|
||||
</div>
|
||||
@@ -36,8 +36,8 @@ export function TopBar({
|
||||
tone={healthy ? 'green' : 'red'}
|
||||
>
|
||||
{healthy
|
||||
? 'All Systems Operational'
|
||||
: 'System Issues Detected'}
|
||||
? 'Todos os sistemas operacionais'
|
||||
: 'Problemas detetados no sistema'}
|
||||
</Badge>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Eye,
|
||||
EyeOff,
|
||||
LockKeyhole,
|
||||
Shield,
|
||||
User,
|
||||
} from 'lucide-react';
|
||||
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { loginApi } from '@/services/loginApi';
|
||||
|
||||
type LoginScreenProps = {
|
||||
onLogin: (keepLoggedIn: boolean) => void;
|
||||
};
|
||||
|
||||
export function LoginScreen({
|
||||
onLogin,
|
||||
}: LoginScreenProps) {
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [keepLoggedIn, setKeepLoggedIn] = useState(false);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
async function handleSubmit(event: React.FormEvent) {
|
||||
event.preventDefault();
|
||||
|
||||
if (submitting) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSubmitting(true);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
const response = await loginApi.login(
|
||||
username.trim(),
|
||||
password,
|
||||
);
|
||||
|
||||
if (response.authenticated) {
|
||||
onLogin(keepLoggedIn);
|
||||
return;
|
||||
}
|
||||
|
||||
setError('Credenciais inválidas.');
|
||||
} catch {
|
||||
setError('Credenciais inválidas.');
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative flex min-h-screen items-center justify-center overflow-hidden bg-ink-950 px-6 text-white">
|
||||
<div className="pointer-events-none absolute -left-32 top-10 h-96 w-96 rounded-full bg-blue-500/20 blur-3xl" />
|
||||
<div className="pointer-events-none absolute -right-32 bottom-10 h-96 w-96 rounded-full bg-cyan-400/10 blur-3xl" />
|
||||
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_top,rgba(59,130,246,0.16),transparent_42%)]" />
|
||||
|
||||
<div className="relative w-full max-w-md">
|
||||
<div className="absolute -inset-px rounded-[2rem] bg-gradient-to-br from-blue-500/40 via-cyan-400/10 to-white/5 opacity-80 blur-sm" />
|
||||
|
||||
<div className="relative rounded-[2rem] border border-white/10 bg-slate-950/80 p-8 shadow-2xl shadow-blue-500/10 backdrop-blur-xl">
|
||||
<div className="mb-8 flex items-center gap-4">
|
||||
<div className="rounded-2xl border border-blue-400/20 bg-blue-500/15 p-4 text-blue-300 shadow-lg shadow-blue-500/10">
|
||||
<Shield size={32} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight text-white">
|
||||
Litoral Regas
|
||||
</h1>
|
||||
|
||||
<p className="mt-1 text-sm text-blue-200/80">
|
||||
VPN Orchestrator
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="space-y-5">
|
||||
<div>
|
||||
<label className="mb-2 block text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">
|
||||
Utilizador
|
||||
</label>
|
||||
|
||||
<div className="group relative">
|
||||
<User
|
||||
size={17}
|
||||
className="absolute left-4 top-1/2 -translate-y-1/2 text-blue-300/60 transition group-focus-within:text-blue-300"
|
||||
/>
|
||||
|
||||
<input
|
||||
value={username}
|
||||
onChange={(event) =>
|
||||
setUsername(event.target.value)
|
||||
}
|
||||
autoComplete="username"
|
||||
className="w-full rounded-2xl border border-white/10 bg-black/25 py-4 pl-11 pr-4 text-sm text-white outline-none transition placeholder:text-slate-600 focus:border-blue-400/60 focus:bg-black/35 focus:shadow-[0_0_0_4px_rgba(59,130,246,0.12)]"
|
||||
placeholder="Introduza o utilizador"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="mb-2 block text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">
|
||||
Palavra-passe
|
||||
</label>
|
||||
|
||||
<div className="group relative">
|
||||
<LockKeyhole
|
||||
size={17}
|
||||
className="absolute left-4 top-1/2 -translate-y-1/2 text-blue-300/60 transition group-focus-within:text-blue-300"
|
||||
/>
|
||||
|
||||
<input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={password}
|
||||
onChange={(event) =>
|
||||
setPassword(event.target.value)
|
||||
}
|
||||
autoComplete="current-password"
|
||||
className="w-full rounded-2xl border border-white/10 bg-black/25 py-4 pl-11 pr-12 text-sm text-white outline-none transition placeholder:text-slate-600 focus:border-blue-400/60 focus:bg-black/35 focus:shadow-[0_0_0_4px_rgba(59,130,246,0.12)]"
|
||||
placeholder="Introduza a palavra-passe"
|
||||
/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setShowPassword((current) => !current)
|
||||
}
|
||||
className="absolute right-4 top-1/2 -translate-y-1/2 text-slate-500 transition hover:text-blue-200"
|
||||
>
|
||||
{showPassword ? (
|
||||
<EyeOff size={17} />
|
||||
) : (
|
||||
<Eye size={17} />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mt-5 rounded-2xl border border-red-500/20 bg-red-500/10 p-3 text-sm text-red-200">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<label className="mt-5 flex cursor-pointer items-center gap-3 text-sm text-slate-400 transition hover:text-slate-200">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={keepLoggedIn}
|
||||
onChange={(event) =>
|
||||
setKeepLoggedIn(event.target.checked)
|
||||
}
|
||||
className="h-4 w-4 accent-blue-500"
|
||||
/>
|
||||
|
||||
Manter sessão iniciada
|
||||
</label>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={submitting}
|
||||
className="mt-7 w-full justify-center rounded-2xl py-4 text-sm font-bold shadow-lg shadow-blue-500/20 transition hover:-translate-y-[1px]"
|
||||
>
|
||||
{submitting ? 'A entrar...' : 'Entrar'}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<div className="mt-8 border-t border-white/10 pt-5 text-center text-xs text-slate-600">
|
||||
Acesso restrito a técnicos autorizados
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import { Card } from '@/components/ui/Card';
|
||||
|
||||
import type { ProvisioningState } from '@/types/provisioning';
|
||||
|
||||
type ProvisioningStepCardProps = {
|
||||
state: ProvisioningState;
|
||||
active: boolean;
|
||||
};
|
||||
|
||||
export function ProvisioningStepCard({
|
||||
state,
|
||||
active,
|
||||
}: ProvisioningStepCardProps) {
|
||||
return (
|
||||
<Card
|
||||
className={
|
||||
active
|
||||
? 'border border-blue-400/40 bg-blue-500/10'
|
||||
: 'border border-white/10'
|
||||
}
|
||||
>
|
||||
<p
|
||||
className={`text-sm font-semibold ${
|
||||
active
|
||||
? 'text-blue-200'
|
||||
: 'text-slate-300'
|
||||
}`}
|
||||
>
|
||||
{state.replace(/_/g, ' ')}
|
||||
</p>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -71,12 +71,12 @@ type RouterEnvForm = {
|
||||
const routerPresets = [
|
||||
{
|
||||
ip: '192.168.1.1',
|
||||
label: 'Factory/default',
|
||||
label: 'Fábrica/predefinido',
|
||||
password: '',
|
||||
},
|
||||
{
|
||||
ip: '198.51.100.1',
|
||||
label: 'Provisioned LAN',
|
||||
label: 'LAN provisionada',
|
||||
password: 'litoralr',
|
||||
},
|
||||
];
|
||||
@@ -84,7 +84,7 @@ const routerPresets = [
|
||||
const FLASH_SECONDS = 300;
|
||||
const PROVISION_SECONDS = 90;
|
||||
|
||||
const stopPhrase = 'STOP PROVISIONING';
|
||||
const stopPhrase = 'PARAR PROVISIONAMENTO';
|
||||
|
||||
const workflowSteps: Array<{
|
||||
id: WorkflowStep;
|
||||
@@ -94,50 +94,50 @@ const workflowSteps: Array<{
|
||||
}> = [
|
||||
{
|
||||
id: 'DETECT_ROUTER',
|
||||
title: 'Detect Router',
|
||||
description: 'Ping and inspect router over SSH.',
|
||||
title: 'Detetar Router',
|
||||
description: 'Ping e inspeção do router via SSH.',
|
||||
icon: Search,
|
||||
},
|
||||
{
|
||||
id: 'UPLOAD_FIRMWARE',
|
||||
title: 'Upload Firmware',
|
||||
description: 'Copy firmware image to /tmp.',
|
||||
title: 'Enviar Firmware',
|
||||
description: 'Copiar imagem de firmware para /tmp.',
|
||||
icon: UploadCloud,
|
||||
},
|
||||
{
|
||||
id: 'FLASH_FIRMWARE',
|
||||
title: 'Flash Firmware',
|
||||
description: 'Run sysupgrade -n /tmp/firmware.bin.',
|
||||
title: 'Gravar Firmware',
|
||||
description: 'Executar sysupgrade -n /tmp/firmware.bin.',
|
||||
icon: Cpu,
|
||||
},
|
||||
{
|
||||
id: 'WAIT_REBOOT',
|
||||
title: 'Wait for Reboot',
|
||||
description: 'Block actions while router restarts.',
|
||||
title: 'Aguardar Reinício',
|
||||
description: 'Bloquear ações enquanto o router reinicia.',
|
||||
icon: RefreshCw,
|
||||
},
|
||||
{
|
||||
id: 'RECONNECT_ROUTER',
|
||||
title: 'Reconnect Router',
|
||||
description: 'Reconnect Ethernet and wait for SSH.',
|
||||
title: 'Reconectar Router',
|
||||
description: 'Reconectar Ethernet e aguardar SSH.',
|
||||
icon: PlugZap,
|
||||
},
|
||||
{
|
||||
id: 'UPLOAD_PROVISIONING',
|
||||
title: 'Upload Bundle',
|
||||
description: 'Copy router.env and provision.sh.',
|
||||
title: 'Enviar Pacote',
|
||||
description: 'Copiar router.env e provision.sh.',
|
||||
icon: FileUp,
|
||||
},
|
||||
{
|
||||
id: 'RUN_PROVISIONING',
|
||||
title: 'Run Provisioning',
|
||||
description: 'Execute router-side setup script.',
|
||||
title: 'Executar Provisionamento',
|
||||
description: 'Executar script de configuração no router.',
|
||||
icon: Terminal,
|
||||
},
|
||||
{
|
||||
id: 'REGISTER_PEER',
|
||||
title: 'Register VPS Peer',
|
||||
description: 'Apply WireGuard peer on VPS.',
|
||||
title: 'Registar Peer VPS',
|
||||
description: 'Aplicar peer WireGuard na VPS.',
|
||||
icon: Network,
|
||||
}
|
||||
];
|
||||
@@ -171,7 +171,20 @@ function statusTone(status: DetectionStatus) {
|
||||
}
|
||||
|
||||
function statusLabel(status: DetectionStatus) {
|
||||
return status.replace(/_/g, ' ');
|
||||
const labels: Record<DetectionStatus, string> = {
|
||||
idle: 'inativo',
|
||||
checking: 'a verificar',
|
||||
reachable: 'acessível',
|
||||
ssh_ok: 'ssh ok',
|
||||
auth_required: 'autenticação necessária',
|
||||
stale_host_key: 'chave SSH antiga',
|
||||
failed: 'falhou',
|
||||
flashing: 'a gravar firmware',
|
||||
waiting_reboot: 'a aguardar reinício',
|
||||
reconnecting: 'a reconectar',
|
||||
};
|
||||
|
||||
return labels[status];
|
||||
}
|
||||
|
||||
function formatSeconds(totalSeconds: number) {
|
||||
@@ -272,7 +285,6 @@ export function ProvisioningWizard() {
|
||||
const [provisionSecondsRemaining, setProvisionSecondsRemaining] =
|
||||
useState(PROVISION_SECONDS);
|
||||
|
||||
|
||||
const [isActionRunning, setIsActionRunning] =
|
||||
useState(false);
|
||||
|
||||
@@ -404,7 +416,7 @@ export function ProvisioningWizard() {
|
||||
|
||||
if (shouldLog) {
|
||||
addLog(
|
||||
'Provisioning flow was reset.',
|
||||
'O fluxo de provisionamento foi reposto.',
|
||||
'warning',
|
||||
);
|
||||
}
|
||||
@@ -414,7 +426,7 @@ export function ProvisioningWizard() {
|
||||
resetProvisioning(false);
|
||||
|
||||
setLog((currentLog) => [
|
||||
`${new Date().toLocaleTimeString()} Router setup completed successfully. Ready for next router.`,
|
||||
`${new Date().toLocaleTimeString()} Configuração do router concluída com sucesso. Pronto para o próximo router.`,
|
||||
...currentLog,
|
||||
]);
|
||||
}
|
||||
@@ -451,7 +463,7 @@ export function ProvisioningWizard() {
|
||||
setActiveStep('RECONNECT_ROUTER');
|
||||
|
||||
addLog(
|
||||
'Flash wait window completed. Reconnect or replug Ethernet if needed, then continue with reconnect checks.',
|
||||
'Janela de espera do flash concluída. Volte a ligar o cabo Ethernet se necessário e continue com a verificação de reconexão.',
|
||||
'success',
|
||||
);
|
||||
}
|
||||
@@ -488,7 +500,7 @@ export function ProvisioningWizard() {
|
||||
setActiveStep('REGISTER_PEER');
|
||||
|
||||
addLog(
|
||||
'Provisioning wait completed. Replug Ethernet now, wait a few seconds, then continue to verify WireGuard and register peer.',
|
||||
'Tempo de provisionamento concluído. Desligue e volte a ligar o Ethernet, aguarde alguns segundos e continue para verificar o WireGuard e registar o peer.',
|
||||
'warning',
|
||||
);
|
||||
|
||||
@@ -507,7 +519,7 @@ export function ProvisioningWizard() {
|
||||
async function detectRouter() {
|
||||
if (controlsLocked) {
|
||||
addLog(
|
||||
'Router detection is locked while provisioning is active.',
|
||||
'A deteção do router está bloqueada enquanto o provisionamento está ativo.',
|
||||
'warning',
|
||||
);
|
||||
return;
|
||||
@@ -515,7 +527,7 @@ export function ProvisioningWizard() {
|
||||
|
||||
if (!selectedIp.trim()) {
|
||||
addLog(
|
||||
'Router detection blocked: no IP selected',
|
||||
'Deteção do router bloqueada: nenhum IP selecionado',
|
||||
'warning',
|
||||
);
|
||||
return;
|
||||
@@ -526,7 +538,7 @@ export function ProvisioningWizard() {
|
||||
setRouterInfo('');
|
||||
|
||||
addLog(
|
||||
`Starting safe router detection at ${selectedIp}`,
|
||||
`A iniciar deteção segura do router em ${selectedIp}`,
|
||||
);
|
||||
|
||||
try {
|
||||
@@ -535,7 +547,7 @@ export function ProvisioningWizard() {
|
||||
});
|
||||
|
||||
addLog(
|
||||
`Router responded at ${selectedIp}`,
|
||||
`Router respondeu em ${selectedIp}`,
|
||||
'success',
|
||||
);
|
||||
|
||||
@@ -558,7 +570,7 @@ export function ProvisioningWizard() {
|
||||
);
|
||||
|
||||
addLog(
|
||||
`SSH key authentication succeeded at ${selectedIp}`,
|
||||
`Autenticação SSH por chave concluída com sucesso em ${selectedIp}`,
|
||||
'success',
|
||||
);
|
||||
|
||||
@@ -574,7 +586,7 @@ export function ProvisioningWizard() {
|
||||
setStatus('stale_host_key');
|
||||
|
||||
addLog(
|
||||
`Stale SSH host key detected for ${selectedIp}`,
|
||||
`Detetada chave SSH antiga para ${selectedIp}`,
|
||||
'warning',
|
||||
);
|
||||
|
||||
@@ -587,7 +599,7 @@ export function ProvisioningWizard() {
|
||||
)
|
||||
) {
|
||||
addLog(
|
||||
'Router requires password authentication. Attempting automatic inspection...',
|
||||
'O router requer autenticação por palavra-passe. A tentar inspeção automática...',
|
||||
'warning',
|
||||
);
|
||||
|
||||
@@ -614,7 +626,7 @@ export function ProvisioningWizard() {
|
||||
);
|
||||
|
||||
addLog(
|
||||
`Password SSH inspection succeeded at ${selectedIp}`,
|
||||
`Inspeção SSH por palavra-passe concluída com sucesso em ${selectedIp}`,
|
||||
'success',
|
||||
);
|
||||
|
||||
@@ -623,7 +635,7 @@ export function ProvisioningWizard() {
|
||||
setStatus('auth_required');
|
||||
|
||||
addLog(
|
||||
`Password authentication failed: ${String(
|
||||
`Falha na autenticação por palavra-passe: ${String(
|
||||
passwordError,
|
||||
)}`,
|
||||
'error',
|
||||
@@ -641,7 +653,7 @@ export function ProvisioningWizard() {
|
||||
setStatus('failed');
|
||||
|
||||
addLog(
|
||||
`Router detection failed: ${message}`,
|
||||
`Falha na deteção do router: ${message}`,
|
||||
'error',
|
||||
);
|
||||
}
|
||||
@@ -650,7 +662,7 @@ export function ProvisioningWizard() {
|
||||
async function fixKnownHost() {
|
||||
if (controlsLocked) {
|
||||
addLog(
|
||||
'Known host cleanup is locked while provisioning is active.',
|
||||
'A limpeza de known_hosts está bloqueada enquanto o provisionamento está ativo.',
|
||||
'warning',
|
||||
);
|
||||
return;
|
||||
@@ -658,7 +670,7 @@ export function ProvisioningWizard() {
|
||||
|
||||
if (!selectedIp.trim()) {
|
||||
addLog(
|
||||
'Cannot remove known_hosts entry: no IP selected',
|
||||
'Não foi possível remover a entrada known_hosts: nenhum IP selecionado',
|
||||
'warning',
|
||||
);
|
||||
return;
|
||||
@@ -666,7 +678,7 @@ export function ProvisioningWizard() {
|
||||
|
||||
try {
|
||||
addLog(
|
||||
`Removing known_hosts entry for ${selectedIp}`,
|
||||
`A remover entrada known_hosts para ${selectedIp}`,
|
||||
);
|
||||
|
||||
const result = await invoke<string>(
|
||||
@@ -703,7 +715,7 @@ export function ProvisioningWizard() {
|
||||
if (activeStep === 'UPLOAD_FIRMWARE') {
|
||||
try {
|
||||
addLog(
|
||||
`Uploading firmware to ${selectedIp}:/tmp/firmware.bin`,
|
||||
`A enviar firmware para ${selectedIp}:/tmp/firmware.bin`,
|
||||
);
|
||||
|
||||
const result = await invoke<string>(
|
||||
@@ -728,7 +740,7 @@ export function ProvisioningWizard() {
|
||||
return;
|
||||
} catch (error) {
|
||||
addLog(
|
||||
`Firmware upload failed: ${String(error)}`,
|
||||
`Falha no envio do firmware: ${String(error)}`,
|
||||
'error',
|
||||
);
|
||||
|
||||
@@ -750,13 +762,13 @@ export function ProvisioningWizard() {
|
||||
setStatus('reconnecting');
|
||||
|
||||
addLog(
|
||||
`Checking router SSH after flash at ${selectedIp}`,
|
||||
`A verificar SSH do router após flash em ${selectedIp}`,
|
||||
'warning',
|
||||
);
|
||||
|
||||
for (let attempt = 1; attempt <= 30; attempt += 1) {
|
||||
try {
|
||||
addLog(`Reconnect attempt ${attempt}/30`);
|
||||
addLog(`Tentativa de reconexão ${attempt}/30`);
|
||||
|
||||
const info = await invoke<string>(
|
||||
'check_router_after_flash',
|
||||
@@ -769,7 +781,7 @@ export function ProvisioningWizard() {
|
||||
setRouterInfo(info);
|
||||
|
||||
addLog(
|
||||
`Router SSH reconnected after flash at ${selectedIp}`,
|
||||
`SSH do router reconectado após flash em ${selectedIp}`,
|
||||
'success',
|
||||
);
|
||||
|
||||
@@ -787,7 +799,7 @@ export function ProvisioningWizard() {
|
||||
return;
|
||||
} catch (error) {
|
||||
addLog(
|
||||
`Reconnect attempt ${attempt}/30 failed: ${String(error)}`,
|
||||
`Tentativa de reconexão ${attempt}/30 falhou: ${String(error)}`,
|
||||
'warning',
|
||||
);
|
||||
|
||||
@@ -801,7 +813,7 @@ export function ProvisioningWizard() {
|
||||
setIsReconnecting(false);
|
||||
|
||||
addLog(
|
||||
'Router reconnect failed after 30 attempts. Replug Ethernet and retry.',
|
||||
'Falha na reconexão do router após 30 tentativas. Volte a ligar o Ethernet e tente novamente.',
|
||||
'error',
|
||||
);
|
||||
|
||||
@@ -819,7 +831,7 @@ export function ProvisioningWizard() {
|
||||
|
||||
try {
|
||||
addLog(
|
||||
`Running provisioning script on ${selectedIp}`,
|
||||
`A executar script de provisionamento em ${selectedIp}`,
|
||||
'warning',
|
||||
);
|
||||
|
||||
@@ -847,7 +859,7 @@ export function ProvisioningWizard() {
|
||||
|
||||
if (expectedProvisionDisconnect) {
|
||||
addLog(
|
||||
'Provisioning script reached network/service restart and SSH disconnected as expected.',
|
||||
'O script de provisionamento atingiu o reinício de rede/serviços e o SSH desligou-se como esperado.',
|
||||
'warning',
|
||||
);
|
||||
|
||||
@@ -856,7 +868,7 @@ export function ProvisioningWizard() {
|
||||
setProvisionOverlayOpen(false);
|
||||
|
||||
addLog(
|
||||
`Provisioning script failed: ${message}`,
|
||||
`Falha no script de provisionamento: ${message}`,
|
||||
'error',
|
||||
);
|
||||
|
||||
@@ -879,7 +891,7 @@ export function ProvisioningWizard() {
|
||||
if (activeStep === 'REGISTER_PEER') {
|
||||
try {
|
||||
addLog(
|
||||
`Capturing router WireGuard public key from ${routerPresets[1].ip}`,
|
||||
`A capturar chave pública WireGuard do router em ${routerPresets[1].ip}`,
|
||||
'warning',
|
||||
);
|
||||
|
||||
@@ -892,7 +904,7 @@ export function ProvisioningWizard() {
|
||||
);
|
||||
|
||||
addLog(
|
||||
`Captured router public key ${publicKey.slice(0, 12)}...`,
|
||||
`Chave pública do router capturada ${publicKey.slice(0, 12)}...`,
|
||||
'success',
|
||||
);
|
||||
|
||||
@@ -902,7 +914,7 @@ export function ProvisioningWizard() {
|
||||
});
|
||||
|
||||
addLog(
|
||||
`Registered VPS peer ${routerEnv.wgIp}`,
|
||||
`Peer VPS ${routerEnv.wgIp} registado`,
|
||||
'success',
|
||||
);
|
||||
|
||||
@@ -919,7 +931,7 @@ export function ProvisioningWizard() {
|
||||
return;
|
||||
} catch (error) {
|
||||
addLog(
|
||||
`Peer registration failed: ${String(error)}`,
|
||||
`Falha no registo do peer: ${String(error)}`,
|
||||
'error',
|
||||
);
|
||||
|
||||
@@ -928,7 +940,7 @@ export function ProvisioningWizard() {
|
||||
}
|
||||
|
||||
addLog(
|
||||
`Step ${activeStep} is not wired yet.`,
|
||||
`O passo ${activeStep} ainda não está ligado.`,
|
||||
'warning',
|
||||
);
|
||||
}
|
||||
@@ -940,7 +952,7 @@ export function ProvisioningWizard() {
|
||||
!routerEnv.wgIp.trim()
|
||||
) {
|
||||
addLog(
|
||||
'router.env is missing router ID, hostname, or WireGuard IP.',
|
||||
'router.env não contém o ID do router, hostname ou IP WireGuard.',
|
||||
'error',
|
||||
);
|
||||
return;
|
||||
@@ -950,7 +962,7 @@ export function ProvisioningWizard() {
|
||||
const envContent = buildRouterEnv(routerEnv);
|
||||
|
||||
addLog(
|
||||
`Uploading router.env and provision.sh to ${selectedIp}`,
|
||||
`A enviar router.env e provision.sh para ${selectedIp}`,
|
||||
'warning',
|
||||
);
|
||||
|
||||
@@ -977,7 +989,7 @@ export function ProvisioningWizard() {
|
||||
setActiveStep('RUN_PROVISIONING');
|
||||
} catch (error) {
|
||||
addLog(
|
||||
`Provisioning bundle upload failed: ${String(error)}`,
|
||||
`Falha no envio do pacote de provisionamento: ${String(error)}`,
|
||||
'error',
|
||||
);
|
||||
}
|
||||
@@ -991,7 +1003,7 @@ export function ProvisioningWizard() {
|
||||
flashCompletionHandledRef.current = false;
|
||||
|
||||
addLog(
|
||||
'Starting firmware flash with sysupgrade -n /tmp/firmware.bin',
|
||||
'A iniciar gravação do firmware com sysupgrade -n /tmp/firmware.bin',
|
||||
'warning',
|
||||
);
|
||||
|
||||
@@ -1005,7 +1017,7 @@ export function ProvisioningWizard() {
|
||||
);
|
||||
|
||||
addLog(
|
||||
'Flash command submitted. SSH session may disconnect; entering protected wait window.',
|
||||
'Comando de flash submetido. A sessão SSH pode desligar; a entrar na janela de espera protegida.',
|
||||
'success',
|
||||
);
|
||||
} catch (error) {
|
||||
@@ -1023,14 +1035,14 @@ export function ProvisioningWizard() {
|
||||
loweredMessage.includes('sysupgrade')
|
||||
) {
|
||||
addLog(
|
||||
'Router accepted sysupgrade and disconnected as expected during flash.',
|
||||
'O router aceitou o sysupgrade e desligou-se como esperado durante o flash.',
|
||||
'warning',
|
||||
);
|
||||
} else {
|
||||
setStatus('failed');
|
||||
|
||||
addLog(
|
||||
`Flash command failed before reboot: ${message}`,
|
||||
`Falha no comando de flash antes do reinício: ${message}`,
|
||||
'error',
|
||||
);
|
||||
|
||||
@@ -1045,7 +1057,7 @@ export function ProvisioningWizard() {
|
||||
async function prepareRouterEnv() {
|
||||
try {
|
||||
addLog(
|
||||
'Requesting next available WireGuard IP from backend...',
|
||||
'A pedir ao backend o próximo IP WireGuard disponível...',
|
||||
);
|
||||
|
||||
const response = await vpnApi.availableIp();
|
||||
@@ -1063,14 +1075,14 @@ export function ProvisioningWizard() {
|
||||
}));
|
||||
|
||||
addLog(
|
||||
`Reserved next available WireGuard IP candidate: ${vpnIp}`,
|
||||
`Candidato a próximo IP WireGuard disponível reservado: ${vpnIp}`,
|
||||
'success',
|
||||
);
|
||||
|
||||
setEnvModalOpen(true);
|
||||
} catch (error) {
|
||||
addLog(
|
||||
`Failed to get available WireGuard IP: ${String(error)}`,
|
||||
`Falha ao obter IP WireGuard disponível: ${String(error)}`,
|
||||
'error',
|
||||
);
|
||||
}
|
||||
@@ -1081,12 +1093,11 @@ export function ProvisioningWizard() {
|
||||
<div className="mb-5 flex items-start justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight text-white">
|
||||
Provisioning
|
||||
Provisionamento
|
||||
</h1>
|
||||
|
||||
<p className="mt-1 text-slate-400">
|
||||
Guided router provisioning from
|
||||
detection to VPS peer registration.
|
||||
Provisionamento guiado do router, desde a deteção até ao registo do peer na VPS.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1102,7 +1113,7 @@ export function ProvisioningWizard() {
|
||||
isActionRunning
|
||||
}
|
||||
>
|
||||
Stop Provisioning
|
||||
Parar Provisionamento
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -1124,12 +1135,11 @@ export function ProvisioningWizard() {
|
||||
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-white">
|
||||
Router Connection
|
||||
Ligação ao Router
|
||||
</h2>
|
||||
|
||||
<p className="text-sm text-slate-400">
|
||||
Select target and validate SSH
|
||||
before provisioning.
|
||||
Selecione o alvo e valide o SSH antes do provisionamento.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1147,10 +1157,10 @@ export function ProvisioningWizard() {
|
||||
}`}
|
||||
>
|
||||
<p className="font-semibold text-white">
|
||||
Full Provisioning
|
||||
Provisionamento Completo
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-slate-500">
|
||||
Flash firmware, reconnect, then provision.
|
||||
Gravar firmware, reconectar e depois provisionar.
|
||||
</p>
|
||||
</button>
|
||||
|
||||
@@ -1166,10 +1176,10 @@ export function ProvisioningWizard() {
|
||||
}`}
|
||||
>
|
||||
<p className="font-semibold text-white">
|
||||
Provision Only
|
||||
Apenas Provisionar
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-slate-500">
|
||||
Router is already flashed and stable.
|
||||
O router já tem firmware gravado e está estável.
|
||||
</p>
|
||||
</button>
|
||||
</div>
|
||||
@@ -1213,9 +1223,9 @@ export function ProvisioningWizard() {
|
||||
</p>
|
||||
|
||||
<p className="mt-3 text-xs text-slate-500">
|
||||
Password:{' '}
|
||||
Palavra-passe:{' '}
|
||||
<span className="font-mono text-slate-300">
|
||||
{preset.password || 'empty'}
|
||||
{preset.password || 'vazia'}
|
||||
</span>
|
||||
</p>
|
||||
</button>
|
||||
@@ -1240,12 +1250,11 @@ export function ProvisioningWizard() {
|
||||
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-white">
|
||||
Custom Router Access
|
||||
Acesso Personalizado ao Router
|
||||
</p>
|
||||
|
||||
<p className="text-xs text-slate-500">
|
||||
Use this for non-standard router IPs
|
||||
or passwords.
|
||||
Use para IPs de router ou palavras-passe não padrão.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1253,7 +1262,7 @@ export function ProvisioningWizard() {
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="mb-2 block text-xs font-medium uppercase tracking-wide text-slate-500">
|
||||
Router IP
|
||||
IP do Router
|
||||
</label>
|
||||
|
||||
<input
|
||||
@@ -1269,14 +1278,14 @@ export function ProvisioningWizard() {
|
||||
onFocus={() => {
|
||||
setSelectedIp(customIp.trim());
|
||||
}}
|
||||
placeholder="Example: 192.168.8.1"
|
||||
placeholder="Exemplo: 192.168.8.1"
|
||||
className="w-full rounded-xl border border-white/10 bg-slate-950 px-3 py-2 font-mono text-sm text-white outline-none transition focus:border-blue-500/40 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="mb-2 block text-xs font-medium uppercase tracking-wide text-slate-500">
|
||||
SSH Password
|
||||
Palavra-passe SSH
|
||||
</label>
|
||||
|
||||
<input
|
||||
@@ -1295,7 +1304,7 @@ export function ProvisioningWizard() {
|
||||
);
|
||||
}
|
||||
}}
|
||||
placeholder="Leave empty if no password"
|
||||
placeholder="Vazio se não existir"
|
||||
className="w-full rounded-xl border border-white/10 bg-slate-950 px-3 py-2 font-mono text-sm text-white outline-none transition focus:border-blue-500/40 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
/>
|
||||
</div>
|
||||
@@ -1311,7 +1320,7 @@ export function ProvisioningWizard() {
|
||||
|
||||
<div className="flex-1">
|
||||
<p className="font-semibold text-white">
|
||||
Detection Target
|
||||
Alvo de Deteção
|
||||
</p>
|
||||
|
||||
<p className="mt-1 font-mono text-sm text-slate-400">
|
||||
@@ -1336,7 +1345,7 @@ export function ProvisioningWizard() {
|
||||
}
|
||||
>
|
||||
<Search size={16} />
|
||||
Detect Router
|
||||
Detetar Router
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
@@ -1349,7 +1358,7 @@ export function ProvisioningWizard() {
|
||||
}
|
||||
>
|
||||
<ShieldAlert size={16} />
|
||||
Fix Known Host
|
||||
Corrigir Known Host
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1364,11 +1373,11 @@ export function ProvisioningWizard() {
|
||||
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-white">
|
||||
Provisioning Flow
|
||||
Fluxo de Provisionamento
|
||||
</h2>
|
||||
|
||||
<p className="text-sm text-slate-400">
|
||||
Full router onboarding sequence.
|
||||
Sequência completa de preparação do router.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1400,10 +1409,10 @@ export function ProvisioningWizard() {
|
||||
>
|
||||
<Rocket size={16} />
|
||||
{isActionRunning
|
||||
? 'Working...'
|
||||
? 'A trabalhar...'
|
||||
: isReconnecting
|
||||
? 'Reconnecting...'
|
||||
: 'Continue Provisioning'}
|
||||
? 'A reconectar...'
|
||||
: 'Continuar Provisionamento'}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -1418,11 +1427,11 @@ export function ProvisioningWizard() {
|
||||
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-white">
|
||||
Router Info
|
||||
Informação do Router
|
||||
</h2>
|
||||
|
||||
<p className="text-sm text-slate-400">
|
||||
ubus system board output.
|
||||
Saída de ubus system board.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1430,7 +1439,7 @@ export function ProvisioningWizard() {
|
||||
<pre
|
||||
className={`min-h-0 flex-1 overflow-auto whitespace-pre-wrap rounded-2xl border border-white/10 bg-slate-950 p-4 font-mono text-xs text-slate-300 ${scrollClass}`}
|
||||
>
|
||||
{routerInfo || 'No router information captured yet.'}
|
||||
{routerInfo || 'Ainda não foi capturada informação do router.'}
|
||||
</pre>
|
||||
</Card>
|
||||
</div>
|
||||
@@ -1438,13 +1447,13 @@ export function ProvisioningWizard() {
|
||||
|
||||
<Card className="mt-4 flex h-52 flex-col overflow-hidden">
|
||||
<h3 className="mb-2 text-sm font-semibold text-white">
|
||||
Technician Log
|
||||
Registo Técnico
|
||||
</h3>
|
||||
|
||||
<pre
|
||||
className={`min-h-0 flex-1 overflow-auto whitespace-pre-wrap break-words rounded-xl border border-white/10 bg-slate-950/70 p-3 font-mono text-sm text-slate-300 ${scrollClass}`}
|
||||
>
|
||||
{log.join('\n') || 'No provisioning activity yet.'}
|
||||
{log.join('\n') || 'Ainda não há atividade de provisionamento.'}
|
||||
</pre>
|
||||
</Card>
|
||||
|
||||
@@ -1541,18 +1550,18 @@ function RouterEnvModal({
|
||||
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-white">
|
||||
Router Environment
|
||||
Ambiente do Router
|
||||
</h2>
|
||||
|
||||
<p className="text-sm text-slate-400">
|
||||
Values used to generate router.env before upload.
|
||||
Valores usados para gerar router.env antes do envio.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<EnvInput
|
||||
label="Router ID"
|
||||
label="ID do Router"
|
||||
value={routerEnv.routerId}
|
||||
onChange={(value) => updateField('routerId', value)}
|
||||
placeholder="203"
|
||||
@@ -1566,14 +1575,14 @@ function RouterEnvModal({
|
||||
/>
|
||||
|
||||
<EnvInput
|
||||
label="WireGuard IP"
|
||||
label="IP WireGuard"
|
||||
value={routerEnv.wgIp}
|
||||
onChange={(value) => updateField('wgIp', value)}
|
||||
placeholder="198.19.1.203"
|
||||
/>
|
||||
|
||||
<EnvInput
|
||||
label="Root Password"
|
||||
label="Palavra-passe Root"
|
||||
value={routerEnv.rootPassword}
|
||||
onChange={(value) => updateField('rootPassword', value)}
|
||||
placeholder="litoralr"
|
||||
@@ -1583,7 +1592,7 @@ function RouterEnvModal({
|
||||
|
||||
<div className="mt-5 rounded-2xl border border-white/10 bg-white/[0.02] p-4">
|
||||
<p className="text-xs font-semibold uppercase tracking-wide text-slate-500">
|
||||
Static router.env values
|
||||
Valores estáticos de router.env
|
||||
</p>
|
||||
|
||||
<div className="mt-3 grid grid-cols-2 gap-3 font-mono text-xs text-slate-300">
|
||||
@@ -1602,12 +1611,12 @@ function RouterEnvModal({
|
||||
|
||||
<div className="mt-6 flex justify-end gap-3">
|
||||
<Button type="button" variant="secondary" onClick={onCancel}>
|
||||
Cancel
|
||||
Cancelar
|
||||
</Button>
|
||||
|
||||
<Button type="button" onClick={onUpload}>
|
||||
<FileUp size={16} />
|
||||
Upload Bundle
|
||||
Enviar Pacote
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1664,33 +1673,33 @@ function ConfirmFlashModal({
|
||||
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-white">
|
||||
Confirm Firmware Flash
|
||||
Confirmar Gravação do Firmware
|
||||
</h2>
|
||||
|
||||
<p className="mt-3 text-sm leading-6 text-slate-300">
|
||||
This will run{' '}
|
||||
Isto irá executar{' '}
|
||||
<span className="font-mono text-red-200">
|
||||
sysupgrade -n /tmp/firmware.bin
|
||||
</span>{' '}
|
||||
on router{' '}
|
||||
no router{' '}
|
||||
<span className="font-mono text-red-200">
|
||||
{selectedIp}
|
||||
</span>
|
||||
. The SSH session will drop and the router may be unreachable for several minutes.
|
||||
. A sessão SSH será interrompida e o router poderá ficar inacessível durante vários minutos.
|
||||
</p>
|
||||
|
||||
<div className="mt-5 rounded-2xl border border-red-500/20 bg-red-500/10 p-4 text-sm text-red-100">
|
||||
Do not unplug router power. Do not close this application.
|
||||
Não desligue a alimentação do router. Não feche esta aplicação.
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex justify-end gap-3">
|
||||
<Button type="button" variant="secondary" onClick={onCancel}>
|
||||
Cancel
|
||||
Cancelar
|
||||
</Button>
|
||||
|
||||
<Button type="button" onClick={onConfirm} className="bg-red-500 hover:bg-red-400">
|
||||
<Cpu size={16} />
|
||||
Flash Router
|
||||
Gravar Router
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1717,17 +1726,17 @@ function FlashOverlay({
|
||||
|
||||
<div className="flex-1">
|
||||
<h2 className="text-3xl font-bold text-white">
|
||||
Flashing router firmware
|
||||
A gravar firmware do router
|
||||
</h2>
|
||||
|
||||
<p className="mt-3 leading-7 text-slate-300">
|
||||
The router is applying firmware and rebooting. The SSH session will be unavailable during this window.
|
||||
O router está a aplicar o firmware e a reiniciar. A sessão SSH ficará indisponível temporariamente.
|
||||
</p>
|
||||
|
||||
<div className="mt-6 rounded-2xl border border-white/10 bg-white/[0.03] p-5">
|
||||
<div className="mb-3 flex items-center justify-between text-sm">
|
||||
<span className="text-slate-400">
|
||||
Protected wait window
|
||||
Janela de espera protegida
|
||||
</span>
|
||||
|
||||
<span className="font-mono text-purple-200">
|
||||
@@ -1744,7 +1753,7 @@ function FlashOverlay({
|
||||
</div>
|
||||
|
||||
<div className="mt-6 rounded-2xl border border-red-500/20 bg-red-500/10 p-4 text-sm leading-6 text-red-100">
|
||||
Do not unplug power. Do not close this app. Wait for this screen to finish before touching the Ethernet cable.
|
||||
Não desligue a alimentação. Não feche esta aplicação. Aguarde até este ecrã terminar antes de mexer no cabo Ethernet.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1770,18 +1779,18 @@ function ProvisionOverlay({
|
||||
|
||||
<div className="flex-1">
|
||||
<h2 className="text-3xl font-bold text-white">
|
||||
Applying router provisioning
|
||||
A aplicar provisionamento do router
|
||||
</h2>
|
||||
|
||||
<p className="mt-3 leading-7 text-slate-300">
|
||||
The router is changing LAN, firewall, WireGuard, LuCI,
|
||||
and root password settings. SSH or Ethernet may drop during this step.
|
||||
O router está a alterar definições de LAN, firewall, WireGuard, LuCI
|
||||
e palavra-passe root. O SSH ou Ethernet poderá cair durante este passo.
|
||||
</p>
|
||||
|
||||
<div className="mt-6 rounded-2xl border border-white/10 bg-white/[0.03] p-5">
|
||||
<div className="mb-3 flex items-center justify-between text-sm">
|
||||
<span className="text-slate-400">
|
||||
Protected provisioning window
|
||||
Janela de provisionamento protegida
|
||||
</span>
|
||||
|
||||
<span className="font-mono text-blue-200">
|
||||
@@ -1798,8 +1807,8 @@ function ProvisionOverlay({
|
||||
</div>
|
||||
|
||||
<div className="mt-6 rounded-2xl border border-yellow-500/20 bg-yellow-500/10 p-4 text-sm leading-6 text-yellow-100">
|
||||
Do not unplug power. When this timer finishes, unplug and replug Ethernet,
|
||||
then continue to verify WireGuard and register the VPS peer.
|
||||
Não desligue a alimentação. Quando este temporizador terminar, desligue e volte a ligar o cabo Ethernet,
|
||||
depois continue para verificar o WireGuard e registar o peer VPS.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1831,19 +1840,19 @@ function StopProvisioningModal({
|
||||
|
||||
<div className="flex-1">
|
||||
<h2 className="text-2xl font-bold text-white">
|
||||
Stop Provisioning?
|
||||
Parar Provisionamento?
|
||||
</h2>
|
||||
|
||||
<p className="mt-3 text-sm leading-6 text-slate-300">
|
||||
Stopping in the middle of provisioning can leave the router in a partial or broken state. Only continue if you know the current router state is safe.
|
||||
Parar a meio do provisionamento pode deixar o router num estado parcial ou avariado. Continue apenas se souber que o estado atual do router é seguro.
|
||||
</p>
|
||||
|
||||
<p className="mt-5 text-sm text-slate-400">
|
||||
Type{' '}
|
||||
Escreva{' '}
|
||||
<span className="font-mono text-red-200">
|
||||
{stopPhrase}
|
||||
</span>{' '}
|
||||
to confirm.
|
||||
para confirmar.
|
||||
</p>
|
||||
|
||||
<input
|
||||
@@ -1854,7 +1863,7 @@ function StopProvisioningModal({
|
||||
|
||||
<div className="mt-6 flex justify-end gap-3">
|
||||
<Button type="button" variant="secondary" onClick={onCancel}>
|
||||
Cancel
|
||||
Cancelar
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
@@ -1863,7 +1872,7 @@ function StopProvisioningModal({
|
||||
disabled={!canConfirm}
|
||||
onClick={onConfirm}
|
||||
>
|
||||
Stop Provisioning
|
||||
Parar Provisionamento
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1946,11 +1955,11 @@ function SetupCompleteModal({
|
||||
|
||||
<div className="flex-1">
|
||||
<h2 className="text-2xl font-bold text-white">
|
||||
Router setup completed
|
||||
Configuração do router concluída
|
||||
</h2>
|
||||
|
||||
<p className="mt-3 text-sm leading-6 text-slate-300">
|
||||
Router provisioning finished successfully and the VPS WireGuard peer was registered.
|
||||
O provisionamento do router terminou com sucesso e o peer WireGuard da VPS foi registado.
|
||||
</p>
|
||||
|
||||
<div className="mt-5 rounded-2xl border border-white/10 bg-white/[0.03] p-4 font-mono text-sm text-slate-300">
|
||||
@@ -1961,7 +1970,7 @@ function SetupCompleteModal({
|
||||
|
||||
<div className="mt-6 flex justify-end">
|
||||
<Button type="button" onClick={onClose}>
|
||||
Done
|
||||
Concluir
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -36,7 +36,7 @@ export function BackendSettings() {
|
||||
try {
|
||||
return new URL(settings.backendUrl).host;
|
||||
} catch {
|
||||
return 'Invalid backend URL';
|
||||
return 'URL backend inválido';
|
||||
}
|
||||
}, [settings.backendUrl]);
|
||||
|
||||
@@ -54,17 +54,17 @@ export function BackendSettings() {
|
||||
<div className="mb-6 flex items-start justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight text-white">
|
||||
Workstation
|
||||
Posto de Trabalho
|
||||
</h1>
|
||||
|
||||
<p className="mt-1 text-slate-400">
|
||||
Local technician console configuration
|
||||
for router provisioning.
|
||||
Configuração local da consola técnica
|
||||
para provisionamento de routers.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Badge tone={saved ? 'green' : 'blue'}>
|
||||
{saved ? 'Saved' : 'Local Config'}
|
||||
{saved ? 'Guardado' : 'Configuração Local'}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
@@ -77,12 +77,12 @@ export function BackendSettings() {
|
||||
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-white">
|
||||
Backend Connectivity
|
||||
Ligação ao Backend
|
||||
</h2>
|
||||
|
||||
<p className="text-sm text-slate-400">
|
||||
API endpoint used for VPN IP
|
||||
assignment and peer registration.
|
||||
Endpoint API usado para atribuição
|
||||
de IP VPN e registo de peers.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -90,7 +90,7 @@ export function BackendSettings() {
|
||||
<div className="space-y-5">
|
||||
<div>
|
||||
<label className="mb-2 block text-xs font-semibold uppercase tracking-wide text-slate-500">
|
||||
Backend URL
|
||||
URL do Backend
|
||||
</label>
|
||||
|
||||
<div className="relative">
|
||||
@@ -115,7 +115,7 @@ export function BackendSettings() {
|
||||
|
||||
<div>
|
||||
<label className="mb-2 block text-xs font-semibold uppercase tracking-wide text-slate-500">
|
||||
API Key
|
||||
Chave API
|
||||
</label>
|
||||
|
||||
<div className="relative">
|
||||
@@ -143,7 +143,7 @@ export function BackendSettings() {
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-white">
|
||||
Connected Target
|
||||
Alvo Ligado
|
||||
</p>
|
||||
|
||||
<p className="mt-1 font-mono text-sm text-slate-400">
|
||||
@@ -152,7 +152,7 @@ export function BackendSettings() {
|
||||
</div>
|
||||
|
||||
<Badge tone="green">
|
||||
Ready
|
||||
Pronto
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
@@ -169,7 +169,7 @@ export function BackendSettings() {
|
||||
/>
|
||||
|
||||
<span className="transition-colors group-hover:text-blue-200">
|
||||
Save Workstation Config
|
||||
Guardar Configuração
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -184,38 +184,38 @@ export function BackendSettings() {
|
||||
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-white">
|
||||
Provisioning Baseline
|
||||
Base de Provisionamento
|
||||
</h2>
|
||||
|
||||
<p className="text-sm text-slate-400">
|
||||
Read-only production values.
|
||||
Valores de produção apenas leitura.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3">
|
||||
<InfoRow
|
||||
label="Overlay Route"
|
||||
label="Rota Overlay"
|
||||
value={DEFAULT_OVERLAY_ROUTE}
|
||||
/>
|
||||
|
||||
<InfoRow
|
||||
label="Router LAN IP"
|
||||
label="IP LAN Router"
|
||||
value={DEFAULT_ROUTER_IP}
|
||||
/>
|
||||
|
||||
<InfoRow
|
||||
label="Controller IP"
|
||||
label="IP Controlador"
|
||||
value={DEFAULT_CONTROLLER_IP}
|
||||
/>
|
||||
|
||||
<InfoRow
|
||||
label="PLC IP"
|
||||
label="IP PLC"
|
||||
value={DEFAULT_PLC_IP}
|
||||
/>
|
||||
|
||||
<InfoRow
|
||||
label="Firmware Target"
|
||||
label="Firmware Alvo"
|
||||
value={DEFAULT_FIRMWARE}
|
||||
/>
|
||||
</div>
|
||||
@@ -229,16 +229,16 @@ export function BackendSettings() {
|
||||
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-white">
|
||||
Production Profile
|
||||
Perfil de Produção
|
||||
</h2>
|
||||
|
||||
<p className="mt-3 text-sm leading-6 text-slate-400">
|
||||
OpenWrt 23.05 baseline,
|
||||
ZBT-WE826 16M target,
|
||||
fw4/nftables firewall,
|
||||
LuCI over WireGuard, stable
|
||||
LAN topology and automated VPS
|
||||
peer registration.
|
||||
Base OpenWrt 23.05,
|
||||
alvo ZBT-WE826 16M,
|
||||
firewall fw4/nftables,
|
||||
LuCI sobre WireGuard, topologia
|
||||
LAN estável e registo automático
|
||||
de peers VPS.
|
||||
</p>
|
||||
|
||||
<div className="mt-5 flex flex-wrap gap-2">
|
||||
@@ -262,7 +262,7 @@ export function BackendSettings() {
|
||||
<div className="rounded-2xl border border-green-500/20 bg-green-500/10 p-4 text-sm text-green-300">
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle2 size={16} />
|
||||
Workstation configuration saved.
|
||||
Configuração do posto de trabalho guardada.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Card } from '@/components/ui/Card';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
|
||||
import { vpnApi } from '@/services/vpnApi';
|
||||
|
||||
export function IpManagementPanel() {
|
||||
const [ip, setIp] = useState('');
|
||||
|
||||
async function handleReserveIp() {
|
||||
try {
|
||||
const response =
|
||||
await vpnApi.availableIp();
|
||||
|
||||
setIp(response.vpnIp);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Failed to reserve IP:',
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="h-full">
|
||||
<h3 className="mb-3 font-semibold text-white">
|
||||
IP Management
|
||||
</h3>
|
||||
|
||||
<p className="mb-4 text-sm text-slate-400">
|
||||
Overlay route: 198.19.0.0/16
|
||||
</p>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<Button onClick={handleReserveIp}>
|
||||
Reserve Next Available IP
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{ip && (
|
||||
<div className="mt-4 rounded-xl border border-green-500/20 bg-green-500/10 p-4">
|
||||
<p className="text-sm text-green-300">
|
||||
Assigned candidate:
|
||||
</p>
|
||||
|
||||
<p className="mt-1 font-mono text-lg font-semibold text-white">
|
||||
{ip}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
import { Card } from '@/components/ui/Card';
|
||||
|
||||
const peers = [
|
||||
[
|
||||
'198.19.254.1',
|
||||
'Q2o9...h8jK',
|
||||
'2m ago',
|
||||
'1.2 MB',
|
||||
],
|
||||
[
|
||||
'198.19.254.2',
|
||||
'P9xL...a7Bc',
|
||||
'3m ago',
|
||||
'532 KB',
|
||||
],
|
||||
[
|
||||
'198.19.254.3',
|
||||
'J4kM...z9Xp',
|
||||
'1m ago',
|
||||
'2.1 MB',
|
||||
],
|
||||
];
|
||||
|
||||
export function VpnPeersTable() {
|
||||
return (
|
||||
<Card className="h-full">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-semibold text-white">
|
||||
VPN Peers
|
||||
</h3>
|
||||
|
||||
<span className="rounded-full bg-green-500/10 px-3 py-1 text-xs text-green-300">
|
||||
3 Active
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 overflow-hidden rounded-xl border border-white/10">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-slate-900/80 text-left text-slate-400">
|
||||
<tr>
|
||||
<th className="px-4 py-3">
|
||||
VPN IP
|
||||
</th>
|
||||
|
||||
<th className="px-4 py-3">
|
||||
Public Key
|
||||
</th>
|
||||
|
||||
<th className="px-4 py-3">
|
||||
Last Handshake
|
||||
</th>
|
||||
|
||||
<th className="px-4 py-3 text-right">
|
||||
Transfer
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{peers.map((peer) => (
|
||||
<tr
|
||||
key={peer[0]}
|
||||
className="border-t border-white/10 hover:bg-white/[0.03]"
|
||||
>
|
||||
<td className="px-4 py-3 text-green-300">
|
||||
● {peer[0]}
|
||||
</td>
|
||||
|
||||
<td className="px-4 py-3 font-mono text-slate-300">
|
||||
{peer[1]}
|
||||
</td>
|
||||
|
||||
<td className="px-4 py-3 text-slate-400">
|
||||
{peer[2]}
|
||||
</td>
|
||||
|
||||
<td className="px-4 py-3 text-right text-slate-300">
|
||||
{peer[3]}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { apiRequest } from '@/services/apiClient';
|
||||
|
||||
type LoginResponse = {
|
||||
authenticated: boolean;
|
||||
};
|
||||
|
||||
export const loginApi = {
|
||||
login(username: string, password: string) {
|
||||
return apiRequest<LoginResponse>('/api/login', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
username,
|
||||
password,
|
||||
}),
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -20,4 +20,14 @@ body,
|
||||
|
||||
body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
input[type='password']::-ms-reveal,
|
||||
input[type='password']::-ms-clear {
|
||||
display: none;
|
||||
}
|
||||
|
||||
input[type='password']::-webkit-credentials-auto-fill-button,
|
||||
input[type='password']::-webkit-password-reveal-button {
|
||||
display: none !important;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"compilerOptions": {
|
||||
"ignoreDeprecations": "6.0",
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": [
|
||||
|
||||