Translate to pt, fix some ui details, add proper icon and logo

This commit is contained in:
litoral05
2026-05-11 17:17:50 +01:00
parent c1e9aeb386
commit 12358327c9
73 changed files with 529 additions and 598 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

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>
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 780 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

+29 -1
View File
@@ -1,15 +1,43 @@
import { useState } from 'react'; import { useState } from 'react';
import { LoginScreen } from '@/components/login/LoginScreen';
import { AppShell } from '@/components/layout/AppShell'; import { AppShell } from '@/components/layout/AppShell';
import { RouteView } from './routes'; import { RouteView } from './routes';
const AUTH_KEY = 'lr-openwrt-tool.authenticated';
export default function App() { 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 ( return (
<AppShell <AppShell
active={active} active={active}
onSelect={setActive} onSelect={setActive}
onLogout={handleLogout}
> >
<RouteView active={active} /> <RouteView active={active} />
</AppShell> </AppShell>
+21 -21
View File
@@ -100,52 +100,52 @@ export function DashboardRoute() {
{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-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} {error}
</div> </div>
)} )}
<div className="grid grid-cols-4 gap-4"> <div className="grid grid-cols-4 gap-4">
<MetricCard <MetricCard
title="VPN Status" title="Estado da VPN"
value={ value={
vpnHealthy vpnHealthy
? 'Connected' ? 'Ligada'
: 'Offline' : 'Offline'
} }
subtitle={ subtitle={
health?.wireGuardInterface health?.wireGuardInterface
? `${health.wireGuardInterface} interface` ? `Interface ${health.wireGuardInterface}`
: 'WireGuard wg0 status' : 'Estado WireGuard wg0'
} }
icon={<ShieldCheck />} icon={<ShieldCheck />}
/> />
<MetricCard <MetricCard
title="IP Pool Usage" title="Uso da Pool IP"
value={`${ipPoolPercent}%`} value={`${ipPoolPercent}%`}
subtitle={`${usedCount} / ${ipPoolTotal} IPs used`} subtitle={`${usedCount} / ${ipPoolTotal} IPs usados`}
icon={<Database />} icon={<Database />}
/> />
<MetricCard <MetricCard
title="VPS Uptime" title="Uptime da VPS"
value={ value={
health?.systemUptime ?? health?.systemUptime ??
'Unknown' 'Desconhecido'
} }
subtitle="Reported by VPS health" subtitle="Reportado pelo health da VPS"
icon={<Clock />} icon={<Clock />}
/> />
<MetricCard <MetricCard
title="Backend Health" title="Estado do Backend"
value={ value={
backendHealthy backendHealthy
? 'Healthy' ? 'Saudável'
: 'Offline' : 'Offline'
} }
subtitle="API connectivity" subtitle="Conectividade API"
icon={<Server />} icon={<Server />}
/> />
</div> </div>
@@ -178,8 +178,8 @@ export function PlaceholderRoute({
</h2> </h2>
<p className="mt-2 text-slate-400"> <p className="mt-2 text-slate-400">
Screen scaffold ready for production Estrutura do ecrã pronta para
implementation. implementação em produção.
</p> </p>
</Card> </Card>
); );
@@ -190,25 +190,25 @@ export function RouteView({
}: { }: {
active: string; active: string;
}) { }) {
if (active === 'Dashboard') { if (active === 'Painel') {
return <DashboardRoute />; return <DashboardRoute />;
} }
if (active === 'Provisioning') { if (active === 'Provisionamento') {
return <ProvisioningWizard />; return <ProvisioningWizard />;
} }
if (active === 'UDP2RAW Config') { if (active === 'Configuração UDP2RAW') {
return ( return (
<PlaceholderRoute name="UDP2RAW Config" /> <PlaceholderRoute name="Configuração UDP2RAW" />
); );
} }
if (active === 'Activity Logs') { if (active === 'Registos de Atividade') {
return <ActivityLogs />; return <ActivityLogs />;
} }
if (active === 'Workstation') { if (active === 'Posto de Trabalho') {
return <BackendSettings />; return <BackendSettings />;
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 272 KiB

+38 -22
View File
@@ -38,6 +38,22 @@ const sources: Array<'all' | ActivityLogSource> = [
'vps', '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) { function levelTone(level: ActivityLogLevel) {
if (level === 'success') return 'green'; if (level === 'success') return 'green';
if (level === 'warning') return 'purple'; if (level === 'warning') return 'purple';
@@ -120,12 +136,12 @@ export function ActivityLogs() {
<div className="mb-5 flex items-center justify-between"> <div className="mb-5 flex items-center justify-between">
<div> <div>
<h2 className="text-3xl font-bold tracking-tight text-white"> <h2 className="text-3xl font-bold tracking-tight text-white">
Activity Logs Registos de Atividade
</h2> </h2>
<p className="mt-1 text-slate-400"> <p className="mt-1 text-slate-400">
Local provisioning audit trail for Histórico local de auditoria de
technicians. provisionamento para técnicos.
</p> </p>
</div> </div>
@@ -138,7 +154,7 @@ export function ActivityLogs() {
}} }}
> >
<Download size={16} /> <Download size={16} />
Export JSON Exportar JSON
</Button> </Button>
<Button <Button
@@ -150,7 +166,7 @@ export function ActivityLogs() {
}} }}
> >
<Trash2 size={16} /> <Trash2 size={16} />
Clear Logs Limpar Registos
</Button> </Button>
</div> </div>
</div> </div>
@@ -159,7 +175,7 @@ export function ActivityLogs() {
<div className="grid grid-cols-12 gap-4"> <div className="grid grid-cols-12 gap-4">
<div className="col-span-6"> <div className="col-span-6">
<label className="mb-2 block text-xs font-medium uppercase tracking-wide text-slate-500"> <label className="mb-2 block text-xs font-medium uppercase tracking-wide text-slate-500">
Search Pesquisa
</label> </label>
<div className="flex items-center gap-2 rounded-xl border border-white/10 bg-ink-950 px-3"> <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) => onChange={(event) =>
setQuery(event.target.value) 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" className="w-full bg-transparent py-3 text-sm text-white outline-none placeholder:text-slate-600"
/> />
</div> </div>
@@ -181,7 +197,7 @@ export function ActivityLogs() {
<div className="col-span-3"> <div className="col-span-3">
<label className="mb-2 block text-xs font-medium uppercase tracking-wide text-slate-500"> <label className="mb-2 block text-xs font-medium uppercase tracking-wide text-slate-500">
Level Nível
</label> </label>
<Select <Select
@@ -189,14 +205,14 @@ export function ActivityLogs() {
onChange={setLevelFilter} onChange={setLevelFilter}
options={levels.map((level) => ({ options={levels.map((level) => ({
value: level, value: level,
label: level, label: levelLabels[level],
}))} }))}
/> />
</div> </div>
<div className="col-span-3"> <div className="col-span-3">
<label className="mb-2 block text-xs font-medium uppercase tracking-wide text-slate-500"> <label className="mb-2 block text-xs font-medium uppercase tracking-wide text-slate-500">
Source Origem
</label> </label>
<Select <Select
@@ -204,7 +220,7 @@ export function ActivityLogs() {
onChange={setSourceFilter} onChange={setSourceFilter}
options={sources.map((source) => ({ options={sources.map((source) => ({
value: source, value: source,
label: source, label: sourceLabels[source],
}))} }))}
/> />
</div> </div>
@@ -214,11 +230,11 @@ export function ActivityLogs() {
<Card className="relative z-10 flex min-h-0 flex-1 flex-col overflow-hidden"> <Card className="relative z-10 flex min-h-0 flex-1 flex-col overflow-hidden">
<div className="mb-4 flex items-center justify-between"> <div className="mb-4 flex items-center justify-between">
<h3 className="font-semibold text-white"> <h3 className="font-semibold text-white">
Audit Events Eventos de Auditoria
</h3> </h3>
<span className="text-sm text-slate-500"> <span className="text-sm text-slate-500">
{filteredLogs.length} shown /{' '} {filteredLogs.length} apresentados /{' '}
{logs.length} total {logs.length} total
</span> </span>
</div> </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"> <thead className="sticky top-0 z-10 bg-slate-950 text-left text-xs uppercase tracking-wide text-slate-500">
<tr> <tr>
<th className="w-[180px] px-4 py-3"> <th className="w-[180px] px-4 py-3">
Time Hora
</th> </th>
<th className="w-[110px] px-4 py-3"> <th className="w-[110px] px-4 py-3">
Level Nível
</th> </th>
<th className="w-[120px] px-4 py-3"> <th className="w-[120px] px-4 py-3">
Source Origem
</th> </th>
<th className="w-[190px] px-4 py-3"> <th className="w-[190px] px-4 py-3">
Action Ação
</th> </th>
<th className="px-4 py-3"> <th className="px-4 py-3">
Message Mensagem
</th> </th>
<th className="w-[160px] px-4 py-3"> <th className="w-[160px] px-4 py-3">
VPN IP IP VPN
</th> </th>
</tr> </tr>
</thead> </thead>
@@ -274,12 +290,12 @@ export function ActivityLogs() {
<td className="px-4 py-3"> <td className="px-4 py-3">
<Badge tone={levelTone(log.level)}> <Badge tone={levelTone(log.level)}>
{log.level} {levelLabels[log.level]}
</Badge> </Badge>
</td> </td>
<td className="px-4 py-3 text-slate-300"> <td className="px-4 py-3 text-slate-300">
{log.source} {sourceLabels[log.source]}
</td> </td>
<td className="truncate px-4 py-3 font-medium text-white"> <td className="truncate px-4 py-3 font-medium text-white">
@@ -302,7 +318,7 @@ export function ActivityLogs() {
colSpan={6} colSpan={6}
className="px-4 py-10 text-center text-slate-500" className="px-4 py-10 text-center text-slate-500"
> >
No activity logs found. Nenhum registo de atividade encontrado.
</td> </td>
</tr> </tr>
)} )}
+8 -8
View File
@@ -28,11 +28,11 @@ export function IpPoolChart({
const data = [ const data = [
{ {
name: 'Used', name: 'Usados',
value: used, value: used,
}, },
{ {
name: 'Available', name: 'Disponíveis',
value: available, value: available,
}, },
]; ];
@@ -41,11 +41,11 @@ export function IpPoolChart({
<Card className="flex h-full flex-col"> <Card className="flex h-full flex-col">
<div> <div>
<h3 className="font-semibold text-white"> <h3 className="font-semibold text-white">
IP Pool Usage Uso da Pool IP
</h3> </h3>
<p className="text-xs text-slate-500"> <p className="text-xs text-slate-500">
WireGuard allocation capacity Capacidade de atribuição WireGuard
</p> </p>
</div> </div>
@@ -82,7 +82,7 @@ export function IpPoolChart({
</p> </p>
<p className="mt-1 text-xs text-slate-500"> <p className="mt-1 text-xs text-slate-500">
pool used pool usada
</p> </p>
</div> </div>
</div> </div>
@@ -90,7 +90,7 @@ export function IpPoolChart({
<div className="mt-8 space-y-4"> <div className="mt-8 space-y-4">
<div className="rounded-2xl border border-white/10 bg-white/[0.03] p-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"> <p className="text-xs uppercase tracking-wide text-slate-500">
Used IPs IPs Usados
</p> </p>
<p className="mt-1 text-2xl font-bold text-green-300"> <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"> <div className="rounded-2xl border border-white/10 bg-white/[0.03] p-4">
<p className="text-xs uppercase tracking-wide text-slate-500"> <p className="text-xs uppercase tracking-wide text-slate-500">
Available IPs IPs Disponíveis
</p> </p>
<p className="mt-1 text-2xl font-bold text-blue-300"> <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="rounded-2xl border border-white/10 bg-white/[0.03] p-4">
<div className="mb-2 flex justify-between text-xs text-slate-500"> <div className="mb-2 flex justify-between text-xs text-slate-500">
<span>Capacity</span> <span>Capacidade</span>
<span>{used} / {total}</span> <span>{used} / {total}</span>
</div> </div>
@@ -84,16 +84,16 @@ export function NetworkTrafficChart() {
<div className="mb-4 flex items-center justify-between"> <div className="mb-4 flex items-center justify-between">
<div> <div>
<h3 className="font-semibold text-white"> <h3 className="font-semibold text-white">
Network Traffic Tráfego de Rede
</h3> </h3>
<p className="text-xs text-slate-500"> <p className="text-xs text-slate-500">
Live wg0 RX/TX throughput Débito RX/TX wg0 em direto
</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="rounded-lg border border-white/10 bg-slate-900 px-3 py-1 text-xs text-slate-400">
3s refresh Atualização 3s
</span> </span>
</div> </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>
);
}
+7 -1
View File
@@ -5,16 +5,22 @@ import { Sidebar } from './Sidebar';
type AppShellProps = PropsWithChildren<{ type AppShellProps = PropsWithChildren<{
active: string; active: string;
onSelect: (value: string) => void; onSelect: (value: string) => void;
onLogout: () => void;
}>; }>;
export function AppShell({ export function AppShell({
active, active,
onSelect, onSelect,
onLogout,
children, children,
}: AppShellProps) { }: AppShellProps) {
return ( return (
<div className="flex h-screen overflow-hidden bg-ink-950 text-white"> <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"> <main className="flex-1 overflow-hidden p-4">
{children} {children}
+32 -30
View File
@@ -1,42 +1,49 @@
import { import {
FileClock, FileClock,
Gauge, Gauge,
LogOut,
RadioTower, RadioTower,
Settings, Settings,
Shield, Shield,
Wrench, Wrench,
} from 'lucide-react'; } from 'lucide-react';
import logoIcon from '@/assets/logo-icon.png';
const items = [ const items = [
['Dashboard', Gauge], ['Painel', Gauge],
['UDP2RAW Config', RadioTower], ['Configuração UDP2RAW', RadioTower],
['Provisioning', Wrench], ['Provisionamento', Wrench],
['Activity Logs', FileClock], ['Registos de Atividade', FileClock],
['Workstation', Settings], ['Posto de Trabalho', Settings],
] as const; ] as const;
type SidebarProps = { type SidebarProps = {
active: string; active: string;
onSelect: (value: string) => void; onSelect: (value: string) => void;
onLogout: () => void;
}; };
export function Sidebar({ export function Sidebar({
active, active,
onSelect, onSelect,
onLogout,
}: SidebarProps) { }: SidebarProps) {
return ( return (
<aside className="flex h-screen w-64 flex-col border-r border-white/10 bg-ink-950 px-4 py-5"> <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="mb-10 flex items-center gap-4 px-1">
<div className="rounded-2xl bg-blue-500/15 p-3 text-blue-300"> <img
<Shield /> src={logoIcon}
</div> alt="Litoral Regas"
className="h-12 w-12 shrink-0 object-contain"
/>
<div> <div className="min-w-0">
<h1 className="font-bold text-white"> <h1 className="truncate text-[20px] font-bold leading-none text-white">
Litoral Regas Litoral Regas
</h1> </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 VPN Orchestrator
</p> </p>
</div> </div>
@@ -51,11 +58,10 @@ export function Sidebar({
key={label} key={label}
type="button" type="button"
onClick={() => onSelect(label)} onClick={() => onSelect(label)}
className={`flex w-full items-center gap-3 rounded-xl px-3 py-3 text-left text-sm font-medium transition ${ className={`flex w-full items-center gap-3 rounded-xl px-3 py-3 text-left text-sm font-medium transition ${isActive
isActive ? 'bg-blue-500/15 text-blue-200'
? 'bg-blue-500/15 text-blue-200' : 'text-slate-300 hover:bg-white/5 hover:text-white'
: 'text-slate-300 hover:bg-white/5 hover:text-white' }`}
}`}
> >
<Icon size={18} /> <Icon size={18} />
{label} {label}
@@ -64,19 +70,15 @@ export function Sidebar({
})} })}
</nav> </nav>
<div className="mt-auto rounded-2xl border border-white/10 bg-white/[0.03] p-4"> <div className="mt-auto border-t border-white/10 pt-4">
<div className="flex items-center gap-2 text-sm text-green-300"> <button
<span className="h-2 w-2 rounded-full bg-green-400" /> type="button"
Backend connected onClick={onLogout}
</div> 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"
>
<p className="mt-2 text-xs text-slate-400"> <LogOut size={18} />
localhost:8080 Terminar Sessão
</p> </button>
<div className="mt-5 border-t border-white/10 pt-4 text-xs text-slate-500">
Version 1.0.0
</div>
</div> </div>
</aside> </aside>
); );
+5 -5
View File
@@ -13,12 +13,12 @@ export function TopBar({
<header className="mb-6 flex items-center justify-between"> <header className="mb-6 flex items-center justify-between">
<div> <div>
<h2 className="text-3xl font-bold tracking-tight text-white"> <h2 className="text-3xl font-bold tracking-tight text-white">
Dashboard Painel
</h2> </h2>
<p className="mt-1 text-slate-400"> <p className="mt-1 text-slate-400">
Provisionamento de routers de produção
OpenWrt 23.05 WireGuard OpenWrt 23.05 WireGuard
production router provisioning
</p> </p>
</div> </div>
@@ -27,7 +27,7 @@ export function TopBar({
<RefreshCw size={16} /> <RefreshCw size={16} />
<span> <span>
Last updated:{' '} Última atualização:{' '}
{new Date().toLocaleTimeString()} {new Date().toLocaleTimeString()}
</span> </span>
</div> </div>
@@ -36,8 +36,8 @@ export function TopBar({
tone={healthy ? 'green' : 'red'} tone={healthy ? 'green' : 'red'}
> >
{healthy {healthy
? 'All Systems Operational' ? 'Todos os sistemas operacionais'
: 'System Issues Detected'} : 'Problemas detetados no sistema'}
</Badge> </Badge>
</div> </div>
</header> </header>
+181
View File
@@ -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>
);
}
+139 -130
View File
@@ -71,12 +71,12 @@ type RouterEnvForm = {
const routerPresets = [ const routerPresets = [
{ {
ip: '192.168.1.1', ip: '192.168.1.1',
label: 'Factory/default', label: 'Fábrica/predefinido',
password: '', password: '',
}, },
{ {
ip: '198.51.100.1', ip: '198.51.100.1',
label: 'Provisioned LAN', label: 'LAN provisionada',
password: 'litoralr', password: 'litoralr',
}, },
]; ];
@@ -84,7 +84,7 @@ const routerPresets = [
const FLASH_SECONDS = 300; const FLASH_SECONDS = 300;
const PROVISION_SECONDS = 90; const PROVISION_SECONDS = 90;
const stopPhrase = 'STOP PROVISIONING'; const stopPhrase = 'PARAR PROVISIONAMENTO';
const workflowSteps: Array<{ const workflowSteps: Array<{
id: WorkflowStep; id: WorkflowStep;
@@ -94,50 +94,50 @@ const workflowSteps: Array<{
}> = [ }> = [
{ {
id: 'DETECT_ROUTER', id: 'DETECT_ROUTER',
title: 'Detect Router', title: 'Detetar Router',
description: 'Ping and inspect router over SSH.', description: 'Ping e inspeção do router via SSH.',
icon: Search, icon: Search,
}, },
{ {
id: 'UPLOAD_FIRMWARE', id: 'UPLOAD_FIRMWARE',
title: 'Upload Firmware', title: 'Enviar Firmware',
description: 'Copy firmware image to /tmp.', description: 'Copiar imagem de firmware para /tmp.',
icon: UploadCloud, icon: UploadCloud,
}, },
{ {
id: 'FLASH_FIRMWARE', id: 'FLASH_FIRMWARE',
title: 'Flash Firmware', title: 'Gravar Firmware',
description: 'Run sysupgrade -n /tmp/firmware.bin.', description: 'Executar sysupgrade -n /tmp/firmware.bin.',
icon: Cpu, icon: Cpu,
}, },
{ {
id: 'WAIT_REBOOT', id: 'WAIT_REBOOT',
title: 'Wait for Reboot', title: 'Aguardar Reinício',
description: 'Block actions while router restarts.', description: 'Bloquear ações enquanto o router reinicia.',
icon: RefreshCw, icon: RefreshCw,
}, },
{ {
id: 'RECONNECT_ROUTER', id: 'RECONNECT_ROUTER',
title: 'Reconnect Router', title: 'Reconectar Router',
description: 'Reconnect Ethernet and wait for SSH.', description: 'Reconectar Ethernet e aguardar SSH.',
icon: PlugZap, icon: PlugZap,
}, },
{ {
id: 'UPLOAD_PROVISIONING', id: 'UPLOAD_PROVISIONING',
title: 'Upload Bundle', title: 'Enviar Pacote',
description: 'Copy router.env and provision.sh.', description: 'Copiar router.env e provision.sh.',
icon: FileUp, icon: FileUp,
}, },
{ {
id: 'RUN_PROVISIONING', id: 'RUN_PROVISIONING',
title: 'Run Provisioning', title: 'Executar Provisionamento',
description: 'Execute router-side setup script.', description: 'Executar script de configuração no router.',
icon: Terminal, icon: Terminal,
}, },
{ {
id: 'REGISTER_PEER', id: 'REGISTER_PEER',
title: 'Register VPS Peer', title: 'Registar Peer VPS',
description: 'Apply WireGuard peer on VPS.', description: 'Aplicar peer WireGuard na VPS.',
icon: Network, icon: Network,
} }
]; ];
@@ -171,7 +171,20 @@ function statusTone(status: DetectionStatus) {
} }
function statusLabel(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) { function formatSeconds(totalSeconds: number) {
@@ -272,7 +285,6 @@ export function ProvisioningWizard() {
const [provisionSecondsRemaining, setProvisionSecondsRemaining] = const [provisionSecondsRemaining, setProvisionSecondsRemaining] =
useState(PROVISION_SECONDS); useState(PROVISION_SECONDS);
const [isActionRunning, setIsActionRunning] = const [isActionRunning, setIsActionRunning] =
useState(false); useState(false);
@@ -404,7 +416,7 @@ export function ProvisioningWizard() {
if (shouldLog) { if (shouldLog) {
addLog( addLog(
'Provisioning flow was reset.', 'O fluxo de provisionamento foi reposto.',
'warning', 'warning',
); );
} }
@@ -414,7 +426,7 @@ export function ProvisioningWizard() {
resetProvisioning(false); resetProvisioning(false);
setLog((currentLog) => [ 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, ...currentLog,
]); ]);
} }
@@ -451,7 +463,7 @@ export function ProvisioningWizard() {
setActiveStep('RECONNECT_ROUTER'); setActiveStep('RECONNECT_ROUTER');
addLog( 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', 'success',
); );
} }
@@ -488,7 +500,7 @@ export function ProvisioningWizard() {
setActiveStep('REGISTER_PEER'); setActiveStep('REGISTER_PEER');
addLog( 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', 'warning',
); );
@@ -507,7 +519,7 @@ export function ProvisioningWizard() {
async function detectRouter() { async function detectRouter() {
if (controlsLocked) { if (controlsLocked) {
addLog( addLog(
'Router detection is locked while provisioning is active.', 'A deteção do router está bloqueada enquanto o provisionamento está ativo.',
'warning', 'warning',
); );
return; return;
@@ -515,7 +527,7 @@ export function ProvisioningWizard() {
if (!selectedIp.trim()) { if (!selectedIp.trim()) {
addLog( addLog(
'Router detection blocked: no IP selected', 'Deteção do router bloqueada: nenhum IP selecionado',
'warning', 'warning',
); );
return; return;
@@ -526,7 +538,7 @@ export function ProvisioningWizard() {
setRouterInfo(''); setRouterInfo('');
addLog( addLog(
`Starting safe router detection at ${selectedIp}`, `A iniciar deteção segura do router em ${selectedIp}`,
); );
try { try {
@@ -535,7 +547,7 @@ export function ProvisioningWizard() {
}); });
addLog( addLog(
`Router responded at ${selectedIp}`, `Router respondeu em ${selectedIp}`,
'success', 'success',
); );
@@ -558,7 +570,7 @@ export function ProvisioningWizard() {
); );
addLog( addLog(
`SSH key authentication succeeded at ${selectedIp}`, `Autenticação SSH por chave concluída com sucesso em ${selectedIp}`,
'success', 'success',
); );
@@ -574,7 +586,7 @@ export function ProvisioningWizard() {
setStatus('stale_host_key'); setStatus('stale_host_key');
addLog( addLog(
`Stale SSH host key detected for ${selectedIp}`, `Detetada chave SSH antiga para ${selectedIp}`,
'warning', 'warning',
); );
@@ -587,7 +599,7 @@ export function ProvisioningWizard() {
) )
) { ) {
addLog( addLog(
'Router requires password authentication. Attempting automatic inspection...', 'O router requer autenticação por palavra-passe. A tentar inspeção automática...',
'warning', 'warning',
); );
@@ -614,7 +626,7 @@ export function ProvisioningWizard() {
); );
addLog( addLog(
`Password SSH inspection succeeded at ${selectedIp}`, `Inspeção SSH por palavra-passe concluída com sucesso em ${selectedIp}`,
'success', 'success',
); );
@@ -623,7 +635,7 @@ export function ProvisioningWizard() {
setStatus('auth_required'); setStatus('auth_required');
addLog( addLog(
`Password authentication failed: ${String( `Falha na autenticação por palavra-passe: ${String(
passwordError, passwordError,
)}`, )}`,
'error', 'error',
@@ -641,7 +653,7 @@ export function ProvisioningWizard() {
setStatus('failed'); setStatus('failed');
addLog( addLog(
`Router detection failed: ${message}`, `Falha na deteção do router: ${message}`,
'error', 'error',
); );
} }
@@ -650,7 +662,7 @@ export function ProvisioningWizard() {
async function fixKnownHost() { async function fixKnownHost() {
if (controlsLocked) { if (controlsLocked) {
addLog( addLog(
'Known host cleanup is locked while provisioning is active.', 'A limpeza de known_hosts está bloqueada enquanto o provisionamento está ativo.',
'warning', 'warning',
); );
return; return;
@@ -658,7 +670,7 @@ export function ProvisioningWizard() {
if (!selectedIp.trim()) { if (!selectedIp.trim()) {
addLog( addLog(
'Cannot remove known_hosts entry: no IP selected', 'Não foi possível remover a entrada known_hosts: nenhum IP selecionado',
'warning', 'warning',
); );
return; return;
@@ -666,7 +678,7 @@ export function ProvisioningWizard() {
try { try {
addLog( addLog(
`Removing known_hosts entry for ${selectedIp}`, `A remover entrada known_hosts para ${selectedIp}`,
); );
const result = await invoke<string>( const result = await invoke<string>(
@@ -703,7 +715,7 @@ export function ProvisioningWizard() {
if (activeStep === 'UPLOAD_FIRMWARE') { if (activeStep === 'UPLOAD_FIRMWARE') {
try { try {
addLog( addLog(
`Uploading firmware to ${selectedIp}:/tmp/firmware.bin`, `A enviar firmware para ${selectedIp}:/tmp/firmware.bin`,
); );
const result = await invoke<string>( const result = await invoke<string>(
@@ -728,7 +740,7 @@ export function ProvisioningWizard() {
return; return;
} catch (error) { } catch (error) {
addLog( addLog(
`Firmware upload failed: ${String(error)}`, `Falha no envio do firmware: ${String(error)}`,
'error', 'error',
); );
@@ -750,13 +762,13 @@ export function ProvisioningWizard() {
setStatus('reconnecting'); setStatus('reconnecting');
addLog( addLog(
`Checking router SSH after flash at ${selectedIp}`, `A verificar SSH do router após flash em ${selectedIp}`,
'warning', 'warning',
); );
for (let attempt = 1; attempt <= 30; attempt += 1) { for (let attempt = 1; attempt <= 30; attempt += 1) {
try { try {
addLog(`Reconnect attempt ${attempt}/30`); addLog(`Tentativa de reconexão ${attempt}/30`);
const info = await invoke<string>( const info = await invoke<string>(
'check_router_after_flash', 'check_router_after_flash',
@@ -769,7 +781,7 @@ export function ProvisioningWizard() {
setRouterInfo(info); setRouterInfo(info);
addLog( addLog(
`Router SSH reconnected after flash at ${selectedIp}`, `SSH do router reconectado após flash em ${selectedIp}`,
'success', 'success',
); );
@@ -787,7 +799,7 @@ export function ProvisioningWizard() {
return; return;
} catch (error) { } catch (error) {
addLog( addLog(
`Reconnect attempt ${attempt}/30 failed: ${String(error)}`, `Tentativa de reconexão ${attempt}/30 falhou: ${String(error)}`,
'warning', 'warning',
); );
@@ -801,7 +813,7 @@ export function ProvisioningWizard() {
setIsReconnecting(false); setIsReconnecting(false);
addLog( 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', 'error',
); );
@@ -819,7 +831,7 @@ export function ProvisioningWizard() {
try { try {
addLog( addLog(
`Running provisioning script on ${selectedIp}`, `A executar script de provisionamento em ${selectedIp}`,
'warning', 'warning',
); );
@@ -847,7 +859,7 @@ export function ProvisioningWizard() {
if (expectedProvisionDisconnect) { if (expectedProvisionDisconnect) {
addLog( 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', 'warning',
); );
@@ -856,7 +868,7 @@ export function ProvisioningWizard() {
setProvisionOverlayOpen(false); setProvisionOverlayOpen(false);
addLog( addLog(
`Provisioning script failed: ${message}`, `Falha no script de provisionamento: ${message}`,
'error', 'error',
); );
@@ -879,7 +891,7 @@ export function ProvisioningWizard() {
if (activeStep === 'REGISTER_PEER') { if (activeStep === 'REGISTER_PEER') {
try { try {
addLog( addLog(
`Capturing router WireGuard public key from ${routerPresets[1].ip}`, `A capturar chave pública WireGuard do router em ${routerPresets[1].ip}`,
'warning', 'warning',
); );
@@ -892,7 +904,7 @@ export function ProvisioningWizard() {
); );
addLog( addLog(
`Captured router public key ${publicKey.slice(0, 12)}...`, `Chave pública do router capturada ${publicKey.slice(0, 12)}...`,
'success', 'success',
); );
@@ -902,7 +914,7 @@ export function ProvisioningWizard() {
}); });
addLog( addLog(
`Registered VPS peer ${routerEnv.wgIp}`, `Peer VPS ${routerEnv.wgIp} registado`,
'success', 'success',
); );
@@ -919,7 +931,7 @@ export function ProvisioningWizard() {
return; return;
} catch (error) { } catch (error) {
addLog( addLog(
`Peer registration failed: ${String(error)}`, `Falha no registo do peer: ${String(error)}`,
'error', 'error',
); );
@@ -928,7 +940,7 @@ export function ProvisioningWizard() {
} }
addLog( addLog(
`Step ${activeStep} is not wired yet.`, `O passo ${activeStep} ainda não está ligado.`,
'warning', 'warning',
); );
} }
@@ -940,7 +952,7 @@ export function ProvisioningWizard() {
!routerEnv.wgIp.trim() !routerEnv.wgIp.trim()
) { ) {
addLog( 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', 'error',
); );
return; return;
@@ -950,7 +962,7 @@ export function ProvisioningWizard() {
const envContent = buildRouterEnv(routerEnv); const envContent = buildRouterEnv(routerEnv);
addLog( addLog(
`Uploading router.env and provision.sh to ${selectedIp}`, `A enviar router.env e provision.sh para ${selectedIp}`,
'warning', 'warning',
); );
@@ -977,7 +989,7 @@ export function ProvisioningWizard() {
setActiveStep('RUN_PROVISIONING'); setActiveStep('RUN_PROVISIONING');
} catch (error) { } catch (error) {
addLog( addLog(
`Provisioning bundle upload failed: ${String(error)}`, `Falha no envio do pacote de provisionamento: ${String(error)}`,
'error', 'error',
); );
} }
@@ -991,7 +1003,7 @@ export function ProvisioningWizard() {
flashCompletionHandledRef.current = false; flashCompletionHandledRef.current = false;
addLog( addLog(
'Starting firmware flash with sysupgrade -n /tmp/firmware.bin', 'A iniciar gravação do firmware com sysupgrade -n /tmp/firmware.bin',
'warning', 'warning',
); );
@@ -1005,7 +1017,7 @@ export function ProvisioningWizard() {
); );
addLog( 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', 'success',
); );
} catch (error) { } catch (error) {
@@ -1023,14 +1035,14 @@ export function ProvisioningWizard() {
loweredMessage.includes('sysupgrade') loweredMessage.includes('sysupgrade')
) { ) {
addLog( addLog(
'Router accepted sysupgrade and disconnected as expected during flash.', 'O router aceitou o sysupgrade e desligou-se como esperado durante o flash.',
'warning', 'warning',
); );
} else { } else {
setStatus('failed'); setStatus('failed');
addLog( addLog(
`Flash command failed before reboot: ${message}`, `Falha no comando de flash antes do reinício: ${message}`,
'error', 'error',
); );
@@ -1045,7 +1057,7 @@ export function ProvisioningWizard() {
async function prepareRouterEnv() { async function prepareRouterEnv() {
try { try {
addLog( addLog(
'Requesting next available WireGuard IP from backend...', 'A pedir ao backend o próximo IP WireGuard disponível...',
); );
const response = await vpnApi.availableIp(); const response = await vpnApi.availableIp();
@@ -1063,14 +1075,14 @@ export function ProvisioningWizard() {
})); }));
addLog( addLog(
`Reserved next available WireGuard IP candidate: ${vpnIp}`, `Candidato a próximo IP WireGuard disponível reservado: ${vpnIp}`,
'success', 'success',
); );
setEnvModalOpen(true); setEnvModalOpen(true);
} catch (error) { } catch (error) {
addLog( addLog(
`Failed to get available WireGuard IP: ${String(error)}`, `Falha ao obter IP WireGuard disponível: ${String(error)}`,
'error', 'error',
); );
} }
@@ -1081,12 +1093,11 @@ export function ProvisioningWizard() {
<div className="mb-5 flex items-start justify-between"> <div className="mb-5 flex items-start justify-between">
<div> <div>
<h1 className="text-3xl font-bold tracking-tight text-white"> <h1 className="text-3xl font-bold tracking-tight text-white">
Provisioning Provisionamento
</h1> </h1>
<p className="mt-1 text-slate-400"> <p className="mt-1 text-slate-400">
Guided router provisioning from Provisionamento guiado do router, desde a deteção até ao registo do peer na VPS.
detection to VPS peer registration.
</p> </p>
</div> </div>
@@ -1102,7 +1113,7 @@ export function ProvisioningWizard() {
isActionRunning isActionRunning
} }
> >
Stop Provisioning Parar Provisionamento
</Button> </Button>
)} )}
@@ -1124,12 +1135,11 @@ export function ProvisioningWizard() {
<div> <div>
<h2 className="text-xl font-semibold text-white"> <h2 className="text-xl font-semibold text-white">
Router Connection Ligação ao Router
</h2> </h2>
<p className="text-sm text-slate-400"> <p className="text-sm text-slate-400">
Select target and validate SSH Selecione o alvo e valide o SSH antes do provisionamento.
before provisioning.
</p> </p>
</div> </div>
</div> </div>
@@ -1147,10 +1157,10 @@ export function ProvisioningWizard() {
}`} }`}
> >
<p className="font-semibold text-white"> <p className="font-semibold text-white">
Full Provisioning Provisionamento Completo
</p> </p>
<p className="mt-1 text-xs text-slate-500"> <p className="mt-1 text-xs text-slate-500">
Flash firmware, reconnect, then provision. Gravar firmware, reconectar e depois provisionar.
</p> </p>
</button> </button>
@@ -1166,10 +1176,10 @@ export function ProvisioningWizard() {
}`} }`}
> >
<p className="font-semibold text-white"> <p className="font-semibold text-white">
Provision Only Apenas Provisionar
</p> </p>
<p className="mt-1 text-xs text-slate-500"> <p className="mt-1 text-xs text-slate-500">
Router is already flashed and stable. O router tem firmware gravado e está estável.
</p> </p>
</button> </button>
</div> </div>
@@ -1213,9 +1223,9 @@ export function ProvisioningWizard() {
</p> </p>
<p className="mt-3 text-xs text-slate-500"> <p className="mt-3 text-xs text-slate-500">
Password:{' '} Palavra-passe:{' '}
<span className="font-mono text-slate-300"> <span className="font-mono text-slate-300">
{preset.password || 'empty'} {preset.password || 'vazia'}
</span> </span>
</p> </p>
</button> </button>
@@ -1240,12 +1250,11 @@ export function ProvisioningWizard() {
<div> <div>
<p className="text-sm font-semibold text-white"> <p className="text-sm font-semibold text-white">
Custom Router Access Acesso Personalizado ao Router
</p> </p>
<p className="text-xs text-slate-500"> <p className="text-xs text-slate-500">
Use this for non-standard router IPs Use para IPs de router ou palavras-passe não padrão.
or passwords.
</p> </p>
</div> </div>
</div> </div>
@@ -1253,7 +1262,7 @@ export function ProvisioningWizard() {
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
<div> <div>
<label className="mb-2 block text-xs font-medium uppercase tracking-wide text-slate-500"> <label className="mb-2 block text-xs font-medium uppercase tracking-wide text-slate-500">
Router IP IP do Router
</label> </label>
<input <input
@@ -1269,14 +1278,14 @@ export function ProvisioningWizard() {
onFocus={() => { onFocus={() => {
setSelectedIp(customIp.trim()); 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" 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>
<div> <div>
<label className="mb-2 block text-xs font-medium uppercase tracking-wide text-slate-500"> <label className="mb-2 block text-xs font-medium uppercase tracking-wide text-slate-500">
SSH Password Palavra-passe SSH
</label> </label>
<input <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" 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>
@@ -1311,7 +1320,7 @@ export function ProvisioningWizard() {
<div className="flex-1"> <div className="flex-1">
<p className="font-semibold text-white"> <p className="font-semibold text-white">
Detection Target Alvo de Deteção
</p> </p>
<p className="mt-1 font-mono text-sm text-slate-400"> <p className="mt-1 font-mono text-sm text-slate-400">
@@ -1336,7 +1345,7 @@ export function ProvisioningWizard() {
} }
> >
<Search size={16} /> <Search size={16} />
Detect Router Detetar Router
</Button> </Button>
<Button <Button
@@ -1349,7 +1358,7 @@ export function ProvisioningWizard() {
} }
> >
<ShieldAlert size={16} /> <ShieldAlert size={16} />
Fix Known Host Corrigir Known Host
</Button> </Button>
</div> </div>
</div> </div>
@@ -1364,11 +1373,11 @@ export function ProvisioningWizard() {
<div> <div>
<h2 className="text-lg font-semibold text-white"> <h2 className="text-lg font-semibold text-white">
Provisioning Flow Fluxo de Provisionamento
</h2> </h2>
<p className="text-sm text-slate-400"> <p className="text-sm text-slate-400">
Full router onboarding sequence. Sequência completa de preparação do router.
</p> </p>
</div> </div>
</div> </div>
@@ -1400,10 +1409,10 @@ export function ProvisioningWizard() {
> >
<Rocket size={16} /> <Rocket size={16} />
{isActionRunning {isActionRunning
? 'Working...' ? 'A trabalhar...'
: isReconnecting : isReconnecting
? 'Reconnecting...' ? 'A reconectar...'
: 'Continue Provisioning'} : 'Continuar Provisionamento'}
</Button> </Button>
</div> </div>
</Card> </Card>
@@ -1418,11 +1427,11 @@ export function ProvisioningWizard() {
<div> <div>
<h2 className="text-lg font-semibold text-white"> <h2 className="text-lg font-semibold text-white">
Router Info Informação do Router
</h2> </h2>
<p className="text-sm text-slate-400"> <p className="text-sm text-slate-400">
ubus system board output. Saída de ubus system board.
</p> </p>
</div> </div>
</div> </div>
@@ -1430,7 +1439,7 @@ export function ProvisioningWizard() {
<pre <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}`} 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> </pre>
</Card> </Card>
</div> </div>
@@ -1438,13 +1447,13 @@ export function ProvisioningWizard() {
<Card className="mt-4 flex h-52 flex-col overflow-hidden"> <Card className="mt-4 flex h-52 flex-col overflow-hidden">
<h3 className="mb-2 text-sm font-semibold text-white"> <h3 className="mb-2 text-sm font-semibold text-white">
Technician Log Registo Técnico
</h3> </h3>
<pre <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}`} 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> </pre>
</Card> </Card>
@@ -1541,18 +1550,18 @@ function RouterEnvModal({
<div> <div>
<h2 className="text-2xl font-bold text-white"> <h2 className="text-2xl font-bold text-white">
Router Environment Ambiente do Router
</h2> </h2>
<p className="text-sm text-slate-400"> <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> </p>
</div> </div>
</div> </div>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<EnvInput <EnvInput
label="Router ID" label="ID do Router"
value={routerEnv.routerId} value={routerEnv.routerId}
onChange={(value) => updateField('routerId', value)} onChange={(value) => updateField('routerId', value)}
placeholder="203" placeholder="203"
@@ -1566,14 +1575,14 @@ function RouterEnvModal({
/> />
<EnvInput <EnvInput
label="WireGuard IP" label="IP WireGuard"
value={routerEnv.wgIp} value={routerEnv.wgIp}
onChange={(value) => updateField('wgIp', value)} onChange={(value) => updateField('wgIp', value)}
placeholder="198.19.1.203" placeholder="198.19.1.203"
/> />
<EnvInput <EnvInput
label="Root Password" label="Palavra-passe Root"
value={routerEnv.rootPassword} value={routerEnv.rootPassword}
onChange={(value) => updateField('rootPassword', value)} onChange={(value) => updateField('rootPassword', value)}
placeholder="litoralr" placeholder="litoralr"
@@ -1583,7 +1592,7 @@ function RouterEnvModal({
<div className="mt-5 rounded-2xl border border-white/10 bg-white/[0.02] p-4"> <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"> <p className="text-xs font-semibold uppercase tracking-wide text-slate-500">
Static router.env values Valores estáticos de router.env
</p> </p>
<div className="mt-3 grid grid-cols-2 gap-3 font-mono text-xs text-slate-300"> <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"> <div className="mt-6 flex justify-end gap-3">
<Button type="button" variant="secondary" onClick={onCancel}> <Button type="button" variant="secondary" onClick={onCancel}>
Cancel Cancelar
</Button> </Button>
<Button type="button" onClick={onUpload}> <Button type="button" onClick={onUpload}>
<FileUp size={16} /> <FileUp size={16} />
Upload Bundle Enviar Pacote
</Button> </Button>
</div> </div>
</div> </div>
@@ -1664,33 +1673,33 @@ function ConfirmFlashModal({
<div> <div>
<h2 className="text-2xl font-bold text-white"> <h2 className="text-2xl font-bold text-white">
Confirm Firmware Flash Confirmar Gravação do Firmware
</h2> </h2>
<p className="mt-3 text-sm leading-6 text-slate-300"> <p className="mt-3 text-sm leading-6 text-slate-300">
This will run{' '} Isto irá executar{' '}
<span className="font-mono text-red-200"> <span className="font-mono text-red-200">
sysupgrade -n /tmp/firmware.bin sysupgrade -n /tmp/firmware.bin
</span>{' '} </span>{' '}
on router{' '} no router{' '}
<span className="font-mono text-red-200"> <span className="font-mono text-red-200">
{selectedIp} {selectedIp}
</span> </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> </p>
<div className="mt-5 rounded-2xl border border-red-500/20 bg-red-500/10 p-4 text-sm text-red-100"> <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>
<div className="mt-6 flex justify-end gap-3"> <div className="mt-6 flex justify-end gap-3">
<Button type="button" variant="secondary" onClick={onCancel}> <Button type="button" variant="secondary" onClick={onCancel}>
Cancel Cancelar
</Button> </Button>
<Button type="button" onClick={onConfirm} className="bg-red-500 hover:bg-red-400"> <Button type="button" onClick={onConfirm} className="bg-red-500 hover:bg-red-400">
<Cpu size={16} /> <Cpu size={16} />
Flash Router Gravar Router
</Button> </Button>
</div> </div>
</div> </div>
@@ -1717,17 +1726,17 @@ function FlashOverlay({
<div className="flex-1"> <div className="flex-1">
<h2 className="text-3xl font-bold text-white"> <h2 className="text-3xl font-bold text-white">
Flashing router firmware A gravar firmware do router
</h2> </h2>
<p className="mt-3 leading-7 text-slate-300"> <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> </p>
<div className="mt-6 rounded-2xl border border-white/10 bg-white/[0.03] p-5"> <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"> <div className="mb-3 flex items-center justify-between text-sm">
<span className="text-slate-400"> <span className="text-slate-400">
Protected wait window Janela de espera protegida
</span> </span>
<span className="font-mono text-purple-200"> <span className="font-mono text-purple-200">
@@ -1744,7 +1753,7 @@ function FlashOverlay({
</div> </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"> <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> </div>
</div> </div>
@@ -1770,18 +1779,18 @@ function ProvisionOverlay({
<div className="flex-1"> <div className="flex-1">
<h2 className="text-3xl font-bold text-white"> <h2 className="text-3xl font-bold text-white">
Applying router provisioning A aplicar provisionamento do router
</h2> </h2>
<p className="mt-3 leading-7 text-slate-300"> <p className="mt-3 leading-7 text-slate-300">
The router is changing LAN, firewall, WireGuard, LuCI, O router está a alterar definições de LAN, firewall, WireGuard, LuCI
and root password settings. SSH or Ethernet may drop during this step. e palavra-passe root. O SSH ou Ethernet poderá cair durante este passo.
</p> </p>
<div className="mt-6 rounded-2xl border border-white/10 bg-white/[0.03] p-5"> <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"> <div className="mb-3 flex items-center justify-between text-sm">
<span className="text-slate-400"> <span className="text-slate-400">
Protected provisioning window Janela de provisionamento protegida
</span> </span>
<span className="font-mono text-blue-200"> <span className="font-mono text-blue-200">
@@ -1798,8 +1807,8 @@ function ProvisionOverlay({
</div> </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"> <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, Não desligue a alimentação. Quando este temporizador terminar, desligue e volte a ligar o cabo Ethernet,
then continue to verify WireGuard and register the VPS peer. depois continue para verificar o WireGuard e registar o peer VPS.
</div> </div>
</div> </div>
</div> </div>
@@ -1831,19 +1840,19 @@ function StopProvisioningModal({
<div className="flex-1"> <div className="flex-1">
<h2 className="text-2xl font-bold text-white"> <h2 className="text-2xl font-bold text-white">
Stop Provisioning? Parar Provisionamento?
</h2> </h2>
<p className="mt-3 text-sm leading-6 text-slate-300"> <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>
<p className="mt-5 text-sm text-slate-400"> <p className="mt-5 text-sm text-slate-400">
Type{' '} Escreva{' '}
<span className="font-mono text-red-200"> <span className="font-mono text-red-200">
{stopPhrase} {stopPhrase}
</span>{' '} </span>{' '}
to confirm. para confirmar.
</p> </p>
<input <input
@@ -1854,7 +1863,7 @@ function StopProvisioningModal({
<div className="mt-6 flex justify-end gap-3"> <div className="mt-6 flex justify-end gap-3">
<Button type="button" variant="secondary" onClick={onCancel}> <Button type="button" variant="secondary" onClick={onCancel}>
Cancel Cancelar
</Button> </Button>
<Button <Button
@@ -1863,7 +1872,7 @@ function StopProvisioningModal({
disabled={!canConfirm} disabled={!canConfirm}
onClick={onConfirm} onClick={onConfirm}
> >
Stop Provisioning Parar Provisionamento
</Button> </Button>
</div> </div>
</div> </div>
@@ -1946,11 +1955,11 @@ function SetupCompleteModal({
<div className="flex-1"> <div className="flex-1">
<h2 className="text-2xl font-bold text-white"> <h2 className="text-2xl font-bold text-white">
Router setup completed Configuração do router concluída
</h2> </h2>
<p className="mt-3 text-sm leading-6 text-slate-300"> <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> </p>
<div className="mt-5 rounded-2xl border border-white/10 bg-white/[0.03] p-4 font-mono text-sm text-slate-300"> <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"> <div className="mt-6 flex justify-end">
<Button type="button" onClick={onClose}> <Button type="button" onClick={onClose}>
Done Concluir
</Button> </Button>
</div> </div>
</div> </div>
+28 -28
View File
@@ -36,7 +36,7 @@ export function BackendSettings() {
try { try {
return new URL(settings.backendUrl).host; return new URL(settings.backendUrl).host;
} catch { } catch {
return 'Invalid backend URL'; return 'URL backend inválido';
} }
}, [settings.backendUrl]); }, [settings.backendUrl]);
@@ -54,17 +54,17 @@ export function BackendSettings() {
<div className="mb-6 flex items-start justify-between"> <div className="mb-6 flex items-start justify-between">
<div> <div>
<h1 className="text-3xl font-bold tracking-tight text-white"> <h1 className="text-3xl font-bold tracking-tight text-white">
Workstation Posto de Trabalho
</h1> </h1>
<p className="mt-1 text-slate-400"> <p className="mt-1 text-slate-400">
Local technician console configuration Configuração local da consola técnica
for router provisioning. para provisionamento de routers.
</p> </p>
</div> </div>
<Badge tone={saved ? 'green' : 'blue'}> <Badge tone={saved ? 'green' : 'blue'}>
{saved ? 'Saved' : 'Local Config'} {saved ? 'Guardado' : 'Configuração Local'}
</Badge> </Badge>
</div> </div>
@@ -77,12 +77,12 @@ export function BackendSettings() {
<div> <div>
<h2 className="text-xl font-semibold text-white"> <h2 className="text-xl font-semibold text-white">
Backend Connectivity Ligação ao Backend
</h2> </h2>
<p className="text-sm text-slate-400"> <p className="text-sm text-slate-400">
API endpoint used for VPN IP Endpoint API usado para atribuição
assignment and peer registration. de IP VPN e registo de peers.
</p> </p>
</div> </div>
</div> </div>
@@ -90,7 +90,7 @@ export function BackendSettings() {
<div className="space-y-5"> <div className="space-y-5">
<div> <div>
<label className="mb-2 block text-xs font-semibold uppercase tracking-wide text-slate-500"> <label className="mb-2 block text-xs font-semibold uppercase tracking-wide text-slate-500">
Backend URL URL do Backend
</label> </label>
<div className="relative"> <div className="relative">
@@ -115,7 +115,7 @@ export function BackendSettings() {
<div> <div>
<label className="mb-2 block text-xs font-semibold uppercase tracking-wide text-slate-500"> <label className="mb-2 block text-xs font-semibold uppercase tracking-wide text-slate-500">
API Key Chave API
</label> </label>
<div className="relative"> <div className="relative">
@@ -143,7 +143,7 @@ export function BackendSettings() {
<div className="flex items-start justify-between gap-4"> <div className="flex items-start justify-between gap-4">
<div> <div>
<p className="text-sm font-semibold text-white"> <p className="text-sm font-semibold text-white">
Connected Target Alvo Ligado
</p> </p>
<p className="mt-1 font-mono text-sm text-slate-400"> <p className="mt-1 font-mono text-sm text-slate-400">
@@ -152,7 +152,7 @@ export function BackendSettings() {
</div> </div>
<Badge tone="green"> <Badge tone="green">
Ready Pronto
</Badge> </Badge>
</div> </div>
</div> </div>
@@ -169,7 +169,7 @@ export function BackendSettings() {
/> />
<span className="transition-colors group-hover:text-blue-200"> <span className="transition-colors group-hover:text-blue-200">
Save Workstation Config Guardar Configuração
</span> </span>
</button> </button>
</div> </div>
@@ -184,38 +184,38 @@ export function BackendSettings() {
<div> <div>
<h2 className="text-xl font-semibold text-white"> <h2 className="text-xl font-semibold text-white">
Provisioning Baseline Base de Provisionamento
</h2> </h2>
<p className="text-sm text-slate-400"> <p className="text-sm text-slate-400">
Read-only production values. Valores de produção apenas leitura.
</p> </p>
</div> </div>
</div> </div>
<div className="grid gap-3"> <div className="grid gap-3">
<InfoRow <InfoRow
label="Overlay Route" label="Rota Overlay"
value={DEFAULT_OVERLAY_ROUTE} value={DEFAULT_OVERLAY_ROUTE}
/> />
<InfoRow <InfoRow
label="Router LAN IP" label="IP LAN Router"
value={DEFAULT_ROUTER_IP} value={DEFAULT_ROUTER_IP}
/> />
<InfoRow <InfoRow
label="Controller IP" label="IP Controlador"
value={DEFAULT_CONTROLLER_IP} value={DEFAULT_CONTROLLER_IP}
/> />
<InfoRow <InfoRow
label="PLC IP" label="IP PLC"
value={DEFAULT_PLC_IP} value={DEFAULT_PLC_IP}
/> />
<InfoRow <InfoRow
label="Firmware Target" label="Firmware Alvo"
value={DEFAULT_FIRMWARE} value={DEFAULT_FIRMWARE}
/> />
</div> </div>
@@ -229,16 +229,16 @@ export function BackendSettings() {
<div> <div>
<h2 className="text-xl font-semibold text-white"> <h2 className="text-xl font-semibold text-white">
Production Profile Perfil de Produção
</h2> </h2>
<p className="mt-3 text-sm leading-6 text-slate-400"> <p className="mt-3 text-sm leading-6 text-slate-400">
OpenWrt 23.05 baseline, Base OpenWrt 23.05,
ZBT-WE826 16M target, alvo ZBT-WE826 16M,
fw4/nftables firewall, firewall fw4/nftables,
LuCI over WireGuard, stable LuCI sobre WireGuard, topologia
LAN topology and automated VPS LAN estável e registo automático
peer registration. de peers VPS.
</p> </p>
<div className="mt-5 flex flex-wrap gap-2"> <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="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"> <div className="flex items-center gap-2">
<CheckCircle2 size={16} /> <CheckCircle2 size={16} />
Workstation configuration saved. Configuração do posto de trabalho guardada.
</div> </div>
</div> </div>
)} )}
-54
View File
@@ -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>
);
}
-87
View File
@@ -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>
);
}
+17
View File
@@ -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,
}),
});
},
};
+10
View File
@@ -20,4 +20,14 @@ body,
body { body {
overflow: hidden; 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;
} }
+2 -1
View File
@@ -1,5 +1,6 @@
{ {
"compilerOptions": { "compilerOptions": {
"ignoreDeprecations": "6.0",
"target": "ES2020", "target": "ES2020",
"useDefineForClassFields": true, "useDefineForClassFields": true,
"lib": [ "lib": [