feat: Add web simulator and enhance device status management with activate and decommission actions.
This commit is contained in:
@@ -3,5 +3,5 @@
|
|||||||
"mode": "DEV",
|
"mode": "DEV",
|
||||||
"active_slot": "A",
|
"active_slot": "A",
|
||||||
"status": "OFFLINE",
|
"status": "OFFLINE",
|
||||||
"name": "Unnamed Device"
|
"name": "Unnamed Device0"
|
||||||
}
|
}
|
||||||
@@ -143,12 +143,13 @@ if __name__ == "__main__":
|
|||||||
print(" 4. Switch to PROD Mode")
|
print(" 4. Switch to PROD Mode")
|
||||||
print(" 5. Swap Active Slot (A/B)")
|
print(" 5. Swap Active Slot (A/B)")
|
||||||
print(" 6. Start Automated Loop")
|
print(" 6. Start Automated Loop")
|
||||||
|
print(" 7. Request Re-enlistment")
|
||||||
print(" 0. Exit Orchestrator")
|
print(" 0. Exit Orchestrator")
|
||||||
print("-" * 45)
|
print("-" * 45)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
print_menu()
|
print_menu()
|
||||||
choice = input("Select operation [0-6] >> ").strip()
|
choice = input("Select operation [0-7] >> ").strip()
|
||||||
|
|
||||||
if choice == "1":
|
if choice == "1":
|
||||||
hu.register()
|
hu.register()
|
||||||
@@ -177,6 +178,9 @@ if __name__ == "__main__":
|
|||||||
break
|
break
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("\n[!] Loop interrupted by user.")
|
print("\n[!] Loop interrupted by user.")
|
||||||
|
elif choice == "7":
|
||||||
|
hu.status = "PENDING"
|
||||||
|
hu.register()
|
||||||
elif choice == "0":
|
elif choice == "0":
|
||||||
print("[*] Powering down head unit...")
|
print("[*] Powering down head unit...")
|
||||||
break
|
break
|
||||||
|
|||||||
BIN
prisma/dev.db
BIN
prisma/dev.db
Binary file not shown.
@@ -155,6 +155,26 @@ export default function DeviceConfigPage({ params }: { params: Promise<{ id: str
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
|
{device.status === "OFFLINE" ? (
|
||||||
|
<button
|
||||||
|
onClick={async () => {
|
||||||
|
const res = await fetch(`/api/devices/${id}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ status: "ENROLLED" }),
|
||||||
|
});
|
||||||
|
if (res.ok) {
|
||||||
|
setMessage({ type: "success", text: "Node re-activated in mesh." });
|
||||||
|
fetchDevice();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="bg-emerald-500 hover:bg-emerald-400 text-white px-6 py-2.5 rounded-2xl text-[10px] font-black uppercase tracking-widest flex items-center gap-3 transition-all shadow-[0_0_25px_rgba(16,185,129,0.2)]"
|
||||||
|
>
|
||||||
|
<RefreshCw className="w-4 h-4" />
|
||||||
|
Activate Node
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleCommand("REBOOT")}
|
onClick={() => handleCommand("REBOOT")}
|
||||||
className="p-3 rounded-xl bg-secondary/5 border border-border text-secondary hover:text-amber-400 hover:bg-amber-400/5 hover:border-amber-400/20 transition-all group"
|
className="p-3 rounded-xl bg-secondary/5 border border-border text-secondary hover:text-amber-400 hover:bg-amber-400/5 hover:border-amber-400/20 transition-all group"
|
||||||
@@ -169,6 +189,8 @@ export default function DeviceConfigPage({ params }: { params: Promise<{ id: str
|
|||||||
>
|
>
|
||||||
<Power className="w-5 h-5" />
|
<Power className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { Shield, ArrowRight, Settings2, PlusCircle, CheckCircle2, XCircle, Clock, ChevronRight } from "lucide-react";
|
import { Shield, ArrowRight, Settings2, PlusCircle, CheckCircle2, XCircle, Clock, ChevronRight, RefreshCw } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
interface Device {
|
interface Device {
|
||||||
@@ -27,7 +27,8 @@ export default function AdminPage() {
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleEnroll = async (id: string) => {
|
const handleEnroll = async (id: string, currentStatus: string) => {
|
||||||
|
const nextStatus = currentStatus === "OFFLINE" ? "ENROLLED" : "ENROLLED";
|
||||||
const res = await fetch(`/api/devices/${id}`, {
|
const res = await fetch(`/api/devices/${id}`, {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
@@ -38,6 +39,19 @@ export default function AdminPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDecommission = async (id: string) => {
|
||||||
|
if (!confirm("Are you sure you want to decommission this hardware node? It will be disconnected from the active mesh.")) return;
|
||||||
|
|
||||||
|
const res = await fetch(`/api/devices/${id}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ status: "OFFLINE" }),
|
||||||
|
});
|
||||||
|
if (res.ok) {
|
||||||
|
setDevices(devices.map(d => d.id === id ? { ...d, status: "OFFLINE" } : d));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleUpdateName = async (id: string, name: string) => {
|
const handleUpdateName = async (id: string, name: string) => {
|
||||||
await fetch(`/api/devices/${id}`, {
|
await fetch(`/api/devices/${id}`, {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
@@ -145,14 +159,22 @@ export default function AdminPage() {
|
|||||||
<td className="px-8 py-4 text-right">
|
<td className="px-8 py-4 text-right">
|
||||||
{device.status === "PENDING" ? (
|
{device.status === "PENDING" ? (
|
||||||
<button
|
<button
|
||||||
onClick={() => handleEnroll(device.id)}
|
onClick={() => handleEnroll(device.id, device.status)}
|
||||||
className="bg-sky-500 hover:bg-sky-400 text-white text-[10px] font-black py-2.5 px-5 rounded-2xl uppercase tracking-widest transition-all shadow-[0_0_20px_rgba(56,189,248,0.2)] hover:scale-105 active:scale-95 flex items-center gap-2 ml-auto"
|
className="bg-sky-500 hover:bg-sky-400 text-white text-[10px] font-black py-2.5 px-5 rounded-2xl uppercase tracking-widest transition-all shadow-[0_0_20px_rgba(56,189,248,0.2)] hover:scale-105 active:scale-95 flex items-center gap-2 ml-auto"
|
||||||
>
|
>
|
||||||
Enroll Node <ArrowRight className="w-3 h-3" />
|
Enroll Node <ArrowRight className="w-3 h-3" />
|
||||||
</button>
|
</button>
|
||||||
|
) : device.status === "OFFLINE" ? (
|
||||||
|
<button
|
||||||
|
onClick={() => handleEnroll(device.id, device.status)}
|
||||||
|
className="bg-emerald-500 hover:bg-emerald-400 text-white text-[10px] font-black py-2.5 px-5 rounded-2xl uppercase tracking-widest transition-all shadow-[0_0_20px_rgba(16,185,129,0.2)] hover:scale-105 active:scale-95 flex items-center gap-2 ml-auto"
|
||||||
|
>
|
||||||
|
Re-enlist Node <RefreshCw className="w-3 h-3" />
|
||||||
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
className="w-10 h-10 rounded-xl flex items-center justify-center text-slate-600 hover:text-rose-500 hover:bg-rose-500/10 transition-all ml-auto group"
|
onClick={() => handleDecommission(device.id)}
|
||||||
|
className="w-10 h-10 rounded-xl flex items-center justify-center text-secondary hover:text-rose-500 hover:bg-rose-500/10 transition-all ml-auto group"
|
||||||
title="Decommission Hardware"
|
title="Decommission Hardware"
|
||||||
>
|
>
|
||||||
<XCircle className="w-5 h-5 group-hover:scale-110 transition-transform" />
|
<XCircle className="w-5 h-5 group-hover:scale-110 transition-transform" />
|
||||||
|
|||||||
320
src/app/simulator/page.tsx
Normal file
320
src/app/simulator/page.tsx
Normal file
@@ -0,0 +1,320 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useState, useEffect, useRef, useCallback } from "react";
|
||||||
|
import {
|
||||||
|
Cpu, Activity, RefreshCw, Power,
|
||||||
|
ChevronUp, ChevronDown, Check,
|
||||||
|
Wifi, WifiOff, Zap, Shield, HardDrive, Terminal
|
||||||
|
} from "lucide-react";
|
||||||
|
import { clsx, type ClassValue } from "clsx";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Simulation Constants ---
|
||||||
|
const OLED_WIDTH = 128;
|
||||||
|
const OLED_HEIGHT = 64;
|
||||||
|
|
||||||
|
interface SimulatorState {
|
||||||
|
id: string | null;
|
||||||
|
serialNumber: string;
|
||||||
|
macAddress: string;
|
||||||
|
status: "PENDING" | "ENROLLED" | "OFFLINE";
|
||||||
|
activeSlot: "A" | "B";
|
||||||
|
name: string;
|
||||||
|
isBooting: boolean;
|
||||||
|
isTesting: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function WebSimulatorPage() {
|
||||||
|
const [state, setState] = useState<SimulatorState>({
|
||||||
|
id: null,
|
||||||
|
serialNumber: "",
|
||||||
|
macAddress: "",
|
||||||
|
status: "OFFLINE",
|
||||||
|
activeSlot: "A",
|
||||||
|
name: "Virtual Node X",
|
||||||
|
isBooting: true,
|
||||||
|
isTesting: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const [logs, setLogs] = useState<string[]>(["[SYS] OS Initialising...", "[SYS] HDMI Driver Loaded"]);
|
||||||
|
const logEndRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// Initialize Virtual Identity
|
||||||
|
useEffect(() => {
|
||||||
|
const stored = localStorage.getItem("hdmi_sim_v1");
|
||||||
|
if (stored) {
|
||||||
|
const parsed = JSON.parse(stored);
|
||||||
|
setState(s => ({ ...s, ...parsed, isBooting: false }));
|
||||||
|
addLog(`[SYS] Restoration complete: ${parsed.serialNumber}`);
|
||||||
|
} else {
|
||||||
|
const sn = "VSIM-" + Math.random().toString(36).substring(2, 6).toUpperCase();
|
||||||
|
const mac = "00:B0:D0:" + Array.from({ length: 3 }, () => Math.floor(Math.random() * 256).toString(16).padStart(2, '0')).join(":").toUpperCase();
|
||||||
|
const newState = { serialNumber: sn, macAddress: mac, name: "Browser Unit " + sn.slice(-4) };
|
||||||
|
setState(s => ({ ...s, ...newState, isBooting: false }));
|
||||||
|
localStorage.setItem("hdmi_sim_v1", JSON.stringify(newState));
|
||||||
|
addLog(`[SYS] Identity generated: ${sn}`);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
logEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||||
|
}, [logs]);
|
||||||
|
|
||||||
|
const addLog = (msg: string) => {
|
||||||
|
setLogs(prev => [...prev, `[${new Date().toLocaleTimeString('en-GB', { hour12: false })}] ${msg}`].slice(-8));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSync = async () => {
|
||||||
|
addLog("[NET] Handshaking with mesh...");
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/devices/register", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
serialNumber: state.serialNumber,
|
||||||
|
macAddress: state.macAddress,
|
||||||
|
activeSlot: state.activeSlot,
|
||||||
|
name: state.name
|
||||||
|
})
|
||||||
|
});
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
setState(s => ({
|
||||||
|
...s,
|
||||||
|
id: data.id,
|
||||||
|
status: data.status,
|
||||||
|
activeSlot: data.activeSlot || s.activeSlot,
|
||||||
|
name: data.name || s.name
|
||||||
|
}));
|
||||||
|
addLog(`[NET] Sync OK: ${data.status}`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
addLog("[ERR] Peer connection failed");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const runTest = async () => {
|
||||||
|
if (state.status !== "ENROLLED") {
|
||||||
|
addLog("[WRN] Enrollment required");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(s => ({ ...s, isTesting: true }));
|
||||||
|
addLog("[OP] Initiating HDMI spectral sweep...");
|
||||||
|
|
||||||
|
await new Promise(r => setTimeout(r, 1500));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/tests/report", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
serialNumber: state.serialNumber,
|
||||||
|
type: "HDMI_OFFLINE",
|
||||||
|
status: "PASS",
|
||||||
|
hdmi5v: false,
|
||||||
|
summary: "Virtual sweep completed within threshold.",
|
||||||
|
diodeResults: {
|
||||||
|
"DDC_SCL": 0.500 + Math.random() * 0.1,
|
||||||
|
"DDC_SDA": 0.500 + Math.random() * 0.1,
|
||||||
|
"CEC": 0.600 + Math.random() * 0.1,
|
||||||
|
"HPD": 0.550 + Math.random() * 0.1,
|
||||||
|
"TMDS_CLK+": 0.450 + Math.random() * 0.1,
|
||||||
|
"TMDS_CLK-": 0.450 + Math.random() * 0.1
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
if (res.ok) addLog("[OP] Test reported to cloud");
|
||||||
|
} catch (e) {
|
||||||
|
addLog("[ERR] Telemetry drop encountered");
|
||||||
|
}
|
||||||
|
setState(s => ({ ...s, isTesting: false }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReboot = () => {
|
||||||
|
addLog("[SYS] SIGREBOOT received");
|
||||||
|
setState(s => ({ ...s, isBooting: true }));
|
||||||
|
setTimeout(() => {
|
||||||
|
setState(s => ({ ...s, isBooting: false }));
|
||||||
|
addLog("[SYS] System online");
|
||||||
|
handleSync();
|
||||||
|
}, 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Keyboard Listeners
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeys = (e: KeyboardEvent) => {
|
||||||
|
if (e.key.toLowerCase() === 'r') handleSync();
|
||||||
|
if (e.key.toLowerCase() === 't') runTest();
|
||||||
|
if (e.key.toLowerCase() === 'b') handleReboot();
|
||||||
|
if (e.key.toLowerCase() === 's') setState(s => ({ ...s, activeSlot: s.activeSlot === 'A' ? 'B' : 'A' }));
|
||||||
|
};
|
||||||
|
window.addEventListener('keydown', handleKeys);
|
||||||
|
return () => window.removeEventListener('keydown', handleKeys);
|
||||||
|
}, [state]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-[#050505] text-slate-300 flex flex-col items-center justify-center p-4">
|
||||||
|
{/* Branding Header */}
|
||||||
|
<div className="mb-12 text-center space-y-2">
|
||||||
|
<div className="flex items-center justify-center gap-3">
|
||||||
|
<div className="w-8 h-8 bg-sky-500/20 border border-sky-500/30 rounded-lg flex items-center justify-center">
|
||||||
|
<Cpu className="w-5 h-5 text-sky-400" />
|
||||||
|
</div>
|
||||||
|
<h1 className="text-xl font-black uppercase tracking-[0.4em] text-white">Mesh Node <span className="text-sky-500">Virtual</span></h1>
|
||||||
|
</div>
|
||||||
|
<p className="text-[10px] font-bold text-slate-500 uppercase tracking-widest">HDMI Tester Hardware Emulator v2.0</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Hardware Casing */}
|
||||||
|
<div className="relative group">
|
||||||
|
{/* Glow behind device */}
|
||||||
|
<div className="absolute -inset-1 bg-gradient-to-r from-sky-500/20 via-purple-500/20 to-sky-500/20 rounded-[3rem] blur-2xl opacity-50 group-hover:opacity-100 transition-opacity duration-1000" />
|
||||||
|
|
||||||
|
<div className="relative w-[340px] bg-gradient-to-b from-[#1a1a1c] to-[#0f0f11] rounded-[3rem] p-8 border border-white/5 shadow-2xl shadow-black/50">
|
||||||
|
{/* Mounting Holes Shadow */}
|
||||||
|
{[1, 2, 3, 4].map(i => (
|
||||||
|
<div key={i} className={cn(
|
||||||
|
"absolute w-4 h-4 bg-black/60 rounded-full border border-white/5",
|
||||||
|
i === 1 && "top-6 left-6",
|
||||||
|
i === 2 && "top-6 right-6",
|
||||||
|
i === 3 && "bottom-6 left-6",
|
||||||
|
i === 4 && "bottom-6 right-6"
|
||||||
|
)}>
|
||||||
|
<div className="absolute inset-1 bg-[#111] rounded-full shadow-inner" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* The OLED Window */}
|
||||||
|
<div className="relative mt-8 mb-12">
|
||||||
|
<div className="absolute -inset-2 bg-black rounded-2xl border border-white/10" />
|
||||||
|
<div className="relative h-[110px] bg-black rounded-xl overflow-hidden border-2 border-[#222] shadow-[inset_0_0_15px_rgba(0,0,0,0.8)]">
|
||||||
|
{/* OLED Glass Shine */}
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-br from-white/5 to-transparent pointer-events-none" />
|
||||||
|
|
||||||
|
{state.isBooting ? (
|
||||||
|
<div className="h-full flex flex-col items-center justify-center gap-2 animate-pulse">
|
||||||
|
<div className="w-6 h-6 border-2 border-sky-400 border-t-transparent rounded-full animate-spin" />
|
||||||
|
<span className="text-[10px] font-black text-sky-400 font-mono tracking-tighter">BOOTING CORE...</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="p-3 h-full font-mono flex flex-col justify-between">
|
||||||
|
{/* Top Bar */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
{state.status === "ENROLLED" ? <Wifi className="w-3.5 h-3.5 text-sky-400" /> : <WifiOff className="w-3.5 h-3.5 text-slate-700" />}
|
||||||
|
<span className="text-[10px] font-black text-sky-400/80 tracking-tighter uppercase">{state.status}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-[8px] bg-sky-500/20 text-sky-400 px-1 rounded-sm border border-sky-500/30 font-black">SLOT {state.activeSlot}</span>
|
||||||
|
<Zap className={cn("w-3 h-3 transition-colors", state.isTesting ? "text-amber-400 animate-pulse" : "text-slate-700")} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<div className="flex flex-col items-center justify-center flex-1">
|
||||||
|
<div className="text-white text-lg font-black tracking-tighter leading-none mb-1">
|
||||||
|
{state.isTesting ? "RUNNING SWEEP" : state.serialNumber}
|
||||||
|
</div>
|
||||||
|
<div className="text-[8px] text-sky-400/60 font-black uppercase tracking-[0.2em]">Validated Path v1.0</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bottom Console */}
|
||||||
|
<div className="bg-[#080808] border-t border-[#1a1a1a] -mx-3 -mb-3 px-3 py-1 flex items-center justify-between overflow-hidden">
|
||||||
|
<div className="text-[7px] text-slate-500 flex gap-2">
|
||||||
|
<span>R: SYNC</span>
|
||||||
|
<span>T: TEST</span>
|
||||||
|
<span>S: SLOT</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-[7px] text-sky-400/40">v2.0-rc</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Interaction Buttons */}
|
||||||
|
<div className="flex justify-between items-end px-4">
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<button
|
||||||
|
onClick={() => setState(s => ({ ...s, activeSlot: s.activeSlot === 'A' ? 'B' : 'A' }))}
|
||||||
|
className="w-10 h-10 rounded-full bg-gradient-to-b from-[#2a2a2c] to-[#1a1a1c] border border-white/5 active:translate-y-0.5 active:shadow-inner transition-all flex items-center justify-center text-slate-500 hover:text-sky-400 shadow-lg group"
|
||||||
|
title="Previous Item / Slot Switch"
|
||||||
|
>
|
||||||
|
<ChevronUp className="w-5 h-5 group-hover:scale-110 transition-transform" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleSync}
|
||||||
|
className="w-10 h-10 rounded-full bg-gradient-to-b from-[#2a2a2c] to-[#1a1a1c] border border-white/5 active:translate-y-0.5 active:shadow-inner transition-all flex items-center justify-center text-slate-500 hover:text-sky-400 shadow-lg group"
|
||||||
|
title="Next Item / Sync"
|
||||||
|
>
|
||||||
|
<ChevronDown className="w-5 h-5 group-hover:scale-110 transition-transform" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-6">
|
||||||
|
<div className="flex flex-col items-center gap-2">
|
||||||
|
<p className="text-[8px] font-black text-slate-600 uppercase tracking-widest">REBOOT</p>
|
||||||
|
<button
|
||||||
|
onClick={handleReboot}
|
||||||
|
className="w-14 h-14 rounded-2xl bg-gradient-to-br from-[#333] to-[#222] border border-white/10 active:scale-95 transition-all flex items-center justify-center text-rose-500/80 hover:text-rose-400 shadow-xl"
|
||||||
|
>
|
||||||
|
<Power className="w-7 h-7" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col items-center gap-2">
|
||||||
|
<p className="text-[8px] font-black text-slate-600 uppercase tracking-widest">ENTER</p>
|
||||||
|
<button
|
||||||
|
onClick={runTest}
|
||||||
|
className="w-16 h-16 rounded-3xl bg-sky-500 border border-sky-400 active:scale-95 transition-all flex items-center justify-center text-white shadow-[0_0_20px_rgba(56,189,248,0.3)] hover:brightness-110"
|
||||||
|
>
|
||||||
|
<Check className="w-8 h-8" strokeWidth={3} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Virtual Logs Panel */}
|
||||||
|
<div className="mt-12 w-full max-w-[500px] bg-black/40 border border-white/10 rounded-2xl p-6 backdrop-blur-xl">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Terminal className="w-4 h-4 text-sky-500" />
|
||||||
|
<span className="text-[10px] font-black uppercase tracking-widest text-slate-400">Node System Logs</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<div className="w-2 h-2 rounded-full bg-rose-500/20" />
|
||||||
|
<div className="w-2 h-2 rounded-full bg-amber-500/20" />
|
||||||
|
<div className="w-2 h-2 rounded-full bg-emerald-500/20" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="font-mono text-[11px] space-y-1 text-sky-400/80">
|
||||||
|
{logs.map((log, i) => (
|
||||||
|
<div key={i} className="animate-in slide-in-from-left-2 duration-300">
|
||||||
|
{log}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div ref={logEndRef} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* User Instructions */}
|
||||||
|
<div className="mt-8 grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
|
{[
|
||||||
|
{ key: "R", action: "Sync with Mesh" },
|
||||||
|
{ key: "T", action: "Run Test Sweep" },
|
||||||
|
{ key: "S", action: "Switch Slot" },
|
||||||
|
{ key: "B", action: "Soft Reboot" }
|
||||||
|
].map((item) => (
|
||||||
|
<div key={item.key} className="px-4 py-2 bg-white/5 border border-white/5 rounded-xl flex items-center gap-3">
|
||||||
|
<span className="w-6 h-6 bg-white/10 rounded-md flex items-center justify-center text-[10px] font-black">{item.key}</span>
|
||||||
|
<span className="text-[9px] font-bold text-slate-500 uppercase tracking-widest">{item.action}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user