From dae7926eaaf9f0e54e547082510ad64c550bd868 Mon Sep 17 00:00:00 2001 From: Preston Hunter Date: Mon, 22 Dec 2025 23:34:27 -0500 Subject: [PATCH] feat: Add theme toggle, implement interactive simulator menu, and enhance dashboard stats API. --- firmware/simulator.py | 72 +++++---- package-lock.json | 11 ++ package.json | 1 + prisma/dev.db | Bin 36864 -> 49152 bytes src/app/admin/page.tsx | 106 +++++++------ src/app/api/dashboard/stats/route.ts | 8 +- src/app/globals.css | 33 ++-- src/app/layout.tsx | 77 ++++++---- src/app/page.tsx | 215 ++++++++++++++++----------- src/app/tests/[id]/page.tsx | 191 +++++++++++++++--------- src/components/theme-provider.tsx | 11 ++ src/components/theme-toggle.tsx | 38 +++++ 12 files changed, 483 insertions(+), 280 deletions(-) create mode 100644 src/components/theme-provider.tsx create mode 100644 src/components/theme-toggle.tsx diff --git a/firmware/simulator.py b/firmware/simulator.py index 2f6f9fb..887befc 100644 --- a/firmware/simulator.py +++ b/firmware/simulator.py @@ -107,32 +107,52 @@ def generate_mock_hdmi_test(): } if __name__ == "__main__": - import sys - hu = HeadUnit() - if len(sys.argv) < 2: - print("Usage: python simulator.py [register|test|mode-dev|mode-prod|swap|loop]") - sys.exit(1) - - cmd = sys.argv[1] - - if cmd == "register": - hu.register() - elif cmd == "test": - hu.report_test(generate_mock_hdmi_test()) - elif cmd == "mode-dev": - hu.switch_mode("DEV") - elif cmd == "mode-prod": - hu.switch_mode("PROD") - elif cmd == "swap": - hu.switch_slot() - elif cmd == "loop": - print("[*] Starting simulator loop. Ctrl+C to exit.") - while True: + def print_menu(): + print("\n" + "="*45) + print(f" HDMI TESTER - HARDWARE SIMULATOR (v1.2.0)") + print("="*45) + print(f" Status: [{hu.status}]") + print(f" Mode: [{hu.mode}]") + print(f" Active: [Slot {hu.active_slot}]") + print(f" Serial: [{hu.serial_number}]") + print("-" * 45) + print(" 1. Register / Sync Device") + print(" 2. Report Mock HDMI Test") + print(" 3. Switch to DEV Mode") + print(" 4. Switch to PROD Mode") + print(" 5. Swap Active Slot (A/B)") + print(" 6. Start Automated Loop") + print(" 0. Exit Orchestrator") + print("-" * 45) + + while True: + print_menu() + choice = input("Select operation [0-6] >> ").strip() + + if choice == "1": hu.register() - if hu.status == "ENROLLED": - hu.report_test(generate_mock_hdmi_test()) - time.sleep(10) - else: - print("Unknown command.") + elif choice == "2": + hu.report_test(generate_mock_hdmi_test()) + elif choice == "3": + hu.switch_mode("DEV") + elif choice == "4": + hu.switch_mode("PROD") + elif choice == "5": + hu.switch_slot() + elif choice == "6": + print("\n[*] Entering automated telemetry loop. Press Ctrl+C to return to menu.") + try: + while True: + hu.register() + if hu.status == "ENROLLED": + hu.report_test(generate_mock_hdmi_test()) + time.sleep(5) + except KeyboardInterrupt: + print("\n[!] Loop interrupted by user.") + elif choice == "0": + print("[*] Powering down head unit...") + break + else: + print("[!] Invalid selection.") diff --git a/package-lock.json b/package-lock.json index ea65ee9..6281766 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "clsx": "^2.1.1", "lucide-react": "^0.562.0", "next": "16.1.1", + "next-themes": "^0.4.6", "prisma": "^6.19.1", "react": "19.2.3", "react-dom": "19.2.3", @@ -5291,6 +5292,16 @@ } } }, + "node_modules/next-themes": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", + "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", diff --git a/package.json b/package.json index abfbf7c..b6533c7 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "clsx": "^2.1.1", "lucide-react": "^0.562.0", "next": "16.1.1", + "next-themes": "^0.4.6", "prisma": "^6.19.1", "react": "19.2.3", "react-dom": "19.2.3", diff --git a/prisma/dev.db b/prisma/dev.db index 2f87fd14f679edca21eb4758ee2a6bb0f3d3c8a8..950f50cbeaa8ee4775aa63c2dccdaf2a6fee81da 100644 GIT binary patch literal 49152 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 O;`;x_bexd>|9=1gd3qrL delta 155 zcmZo@U~X8zG(noxoPmLXbE1Mhqxr^!W&A7?8Tcn|7F1~GXU}EiwU^b`oIG9LL&4QA z$lu4u)rE1kr)9B;qoJOGo&k`p>Rk;MG2NuVBEZGS{~Kr)11JCQ&3pxK`FVhB4goG8 zDZy(vnccx~v!K9Fp3R27myJX?I2suE1^5(r4R{>51Go}63pg4!HfC^aZs=%W2LQm0 BCUpP+ diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index fd47d1d..b5b07e4 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -1,7 +1,8 @@ "use client"; import { useState, useEffect } from "react"; -import { Shield, Smartphone, ArrowRight, Settings2, PlusCircle, CheckCircle2, XCircle, Clock } from "lucide-react"; +import { Shield, ArrowRight, Settings2, PlusCircle, CheckCircle2, XCircle, Clock } from "lucide-react"; +import Link from "next/link"; interface Device { id: string; @@ -37,105 +38,118 @@ export default function AdminPage() { } }; + const handleUpdateName = async (id: string, name: string) => { + await fetch(`/api/devices/${id}`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name }), + }); + setDevices(devices.map(d => d.id === id ? { ...d, name } : d)); + }; + return ( -
+
-

- +

+
+ +
Command Center

-

Manage device fleet, enrollment, and firmware orchestration.

+

Manage device fleet, enrollment, and firmware orchestration.

-
- {/* Device Management Table */} -
-
-

Connected Hardware Tree

+
+
+

Connected Hardware Tree

- +
- - - - - - - + + + + + + + {loading ? ( - + ) : devices.length === 0 ? ( - + ) : ( devices.map((device) => ( - - + - - - - - diff --git a/src/app/api/dashboard/stats/route.ts b/src/app/api/dashboard/stats/route.ts index eceb625..347072d 100644 --- a/src/app/api/dashboard/stats/route.ts +++ b/src/app/api/dashboard/stats/route.ts @@ -3,7 +3,7 @@ import { prisma } from "@/lib/prisma"; export async function GET() { try { - const [deviceCount, testCount, onlineCount, recentTests] = await Promise.all([ + const [deviceCount, testCount, onlineCount, recentTests, enrolledDevices] = await Promise.all([ prisma.device.count(), prisma.test.count(), prisma.device.count({ where: { status: "ENROLLED" } }), @@ -12,11 +12,17 @@ export async function GET() { orderBy: { timestamp: "desc" }, include: { device: true }, }), + prisma.device.findMany({ + where: { status: "ENROLLED" }, + take: 5, + orderBy: { lastHeartbeat: "desc" }, + }), ]); return NextResponse.json({ stats: { deviceCount, testCount, onlineCount }, recentTests, + enrolledDevices, }); } catch (error) { return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }); diff --git a/src/app/globals.css b/src/app/globals.css index a2dc41e..e463935 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,26 +1,31 @@ @import "tailwindcss"; -:root { - --background: #ffffff; - --foreground: #171717; -} - @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); - --font-sans: var(--font-geist-sans); - --font-mono: var(--font-geist-mono); + --color-border: var(--border); + --color-secondary: var(--secondary); } -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; - } +:root { + --background: #f8fafc; + --foreground: #0f172a; + --border: rgba(15, 23, 42, 0.08); + --secondary: #64748b; +} + +[class~="dark"] { + --background: #0a0a0c; + --foreground: #f1f5f9; + --border: rgba(255, 255, 255, 0.05); + --secondary: #94a3b8; +} + +* { + transition: background-color 0.3s ease, border-color 0.3s ease, color 0.15s ease; } body { background: var(--background); color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; -} +} \ No newline at end of file diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 8aabaf7..c8b8ee8 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -9,46 +9,59 @@ export const metadata: Metadata = { description: "Advanced HDMI testing and modular device management", }; +import Link from "next/link"; +import { ThemeProvider } from "@/components/theme-provider"; +import { ThemeToggle } from "@/components/theme-toggle"; + export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { return ( - - -
-
- +
+ {children} +
+
+
+ © 2025 HDMI Tester Enterprise. All rights reserved. +
+
+
+ ); diff --git a/src/app/page.tsx b/src/app/page.tsx index d2a15f5..2c1ebd4 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -33,6 +33,7 @@ interface DashboardData { onlineCount: number; }; recentTests: Test[]; + enrolledDevices: Device[]; } export default function DashboardPage() { @@ -57,34 +58,53 @@ export default function DashboardPage() { useEffect(() => { fetchData(); - const interval = setInterval(fetchData, 3000); // Poll every 3 seconds + const interval = setInterval(fetchData, 3000); return () => clearInterval(interval); }, []); if (loading || !data) { return (
-
-
-

Syncing with hardware mesh...

+
+
+
+
+ +
+
+
+

Synchronizing Fleet

+

Establishing secure tunnel to edge nodes...

+
); } return ( -
+
{/* Header with Live Indicator */}
-

- - - - - Real-time Telemetry -

-
- Last sync: {lastUpdate.toLocaleTimeString()} +
+
+ + + + + Mesh Encoded +
+

+ Operational Telemetry +

+
+
+
+ Last sync: {lastUpdate.toLocaleTimeString()} +
+
@@ -98,27 +118,32 @@ export default function DashboardPage() {
-

{stat.label}

+

{stat.label}

- {stat.value} + {stat.value.toString().padStart(2, '0')}

- +
+ +
))} @@ -129,65 +154,71 @@ export default function DashboardPage() { {/* Recent Activity Feed */}
-

- - Latest Test Sequences +

+ + Live Sequence Stream

+
-
-

Hardware IDDesignationArchitectureOrchestrationDeploymentControl
Hardware IDDesignationArchitectureOrchestrationDeploymentControl
Scanning network for head units...Scanning network for head units...
No devices detected in local mesh.No devices detected in local mesh.
-
-
+
+
+
- {device.macAddress} - {device.serialNumber} + {device.serialNumber} + {device.macAddress}
+ handleUpdateName(device.id, e.target.value)} + className="bg-secondary/5 border border-border rounded-xl px-4 py-2 text-[11px] text-foreground focus:border-sky-500/50 focus:bg-secondary/10 outline-none w-full transition-all font-bold placeholder:text-secondary/50 placeholder:font-normal" placeholder="Assign static name..." /> + RPi Zero 2 W -
- - - Slot {device.activeSlot} (v{device.firmwareVersion}) +
+
+ + + Slot {device.activeSlot} | v{device.firmwareVersion}
+ {device.status === "ENROLLED" ? ( - - Enrolled - +
+ + Active Mesh +
) : ( - - Awaiting - +
+ + Isolated +
)}
+ {device.status === "PENDING" ? ( ) : ( )}
+
+
- - - - - - - + + + + + + {data.recentTests.length === 0 ? ( - + ) : ( data.recentTests.map((test) => ( - + - @@ -210,44 +241,54 @@ export default function DashboardPage() { {/* Sidebar */}
-
-
- +
+
+
-

- +

+ Intelligence Engine

-

- Real-time spectral analysis of HDMI bus metrics. All processing orchestrated in the cloud with sub-ms edge latency. -

-
- - - - - Processing Engine Active +
+

+ Real-time spectral analysis of HDMI bus metrics. Processing orchestrated with sub-ms edge latency. +

+
+ + + + + Neural Engine Online +
-
-
-

Fleet Control

- Manage Hub +
+
+

Fleet Nodes

+ Manage All
-
-
-
-
- +
+ {data.enrolledDevices.length === 0 ? ( +

No active nodes in mesh.

+ ) : ( + data.enrolledDevices.map((device) => ( +
+
+
+ +
+
+ + {device.name && device.name !== "Unnamed Device" ? device.name : device.serialNumber} + + System Health: Optimal +
+
+
-
- Edge Provisioning - Auto-Scale enabled -
-
-
-
+ )) + )}
diff --git a/src/app/tests/[id]/page.tsx b/src/app/tests/[id]/page.tsx index 8196be8..49ac2e8 100644 --- a/src/app/tests/[id]/page.tsx +++ b/src/app/tests/[id]/page.tsx @@ -1,6 +1,6 @@ import { prisma } from "@/lib/prisma"; import { notFound } from "next/navigation"; -import { CheckCircle2, AlertCircle, Clock, Smartphone, Info, ShieldCheck, Gauge, ExternalLink } from "lucide-react"; +import { CheckCircle2, AlertCircle, Clock, Smartphone, Info, ShieldCheck, Gauge, ExternalLink, Activity } from "lucide-react"; import Link from "next/link"; async function getTestData(id: string) { @@ -22,115 +22,158 @@ export default async function TestDetailsPage({ params }: { params: Promise<{ id const diodeResults = test.diodeResults ? JSON.parse(test.diodeResults) as Record : undefined; return ( -
-
- Dashboard - / - Test Insights +
+
+ + Dashboard + + / + Node Insights
-
-
-
-

- Sequence Analysis: {test.id.slice(-8).toUpperCase()} +
+
+
+

+ {test.id.slice(-8).toUpperCase()}

- {test.status === "PASS" ? ( - - Nominal - - ) : ( - - Anomaly Detected - - )} +
+ {test.status === "PASS" ? ( + + Nominal State + + ) : ( + + Critical Fault + + )} +
+ Sequence Verified +
+
+
+
+

+ + Node: + {test.device.name && test.device.name !== "Unnamed Device" ? test.device.name : test.device.serialNumber} + [{test.device.serialNumber}] +

-

- - Origin: {test.device.name || "Unknown Head Unit"} - ({test.device.serialNumber}) -

-
+
-

Execution Timestamp

-

{new Date(test.timestamp).toLocaleString()}

+

Capture Timestamp

+

{new Date(test.timestamp).toLocaleString()}

-
- +
+
-
+
{/* Spectral Analysis Card */} -
-
- +
+
+
-
- -

Spectral Diode Response

+
+
+
+ +
+

Response Spectrum Analysis

+
+
+ Calibration: 0.8V VDD +
{diodeResults ? ( -
+
{Object.entries(diodeResults).map(([pin, voltage]) => ( -
-
- {pin.replace("_", " ")} - {voltage.toFixed(3)}V +
+
+
+ {pin.replace("_", " ")} + Spectral Diode Path +
+ {voltage.toFixed(3)}V
-
+
+ > +
+
))}
) : ( -
- No detailed diode metrics available for this sequence. +
+ +

No detailed spectral metrics available for this sequence.

)}
{/* Intelligence Report Summary */} -
-
-

- - Validation Report -

-
-

- "{test.summary}" -

+
+
+
+ +
+ +
+

+ + Validation Report +

+
+

+ "{test.summary}" +

+
+ Synthesized by AI Engine + Authenticated +
+
+
+ +
+
+
+

Architecture

+

HDMI v1.4b ENCODED

+
+
+

Type

+

{test.type}

+
+
+ + + Re-Orchestrate Node +
-
-
-
-

Architecture

-

HDMI v1.4b

-
-
-

Probe Type

-

{test.type}

-
+
+
+ + Security Advisory
- - - Re-Orchestrate Node - +

+ Telemetry data is signed with hardware-level HMAC. Direct physical access to the head unit is required for re-calibration. +

diff --git a/src/components/theme-provider.tsx b/src/components/theme-provider.tsx new file mode 100644 index 0000000..1e8e562 --- /dev/null +++ b/src/components/theme-provider.tsx @@ -0,0 +1,11 @@ +"use client"; + +import * as React from "react"; +import { ThemeProvider as NextThemesProvider } from "next-themes"; + +export function ThemeProvider({ + children, + ...props +}: React.ComponentProps) { + return {children}; +} diff --git a/src/components/theme-toggle.tsx b/src/components/theme-toggle.tsx new file mode 100644 index 0000000..d9f8442 --- /dev/null +++ b/src/components/theme-toggle.tsx @@ -0,0 +1,38 @@ +"use client"; + +import * as React from "react"; +import { Moon, Sun } from "lucide-react"; +import { useTheme } from "next-themes"; + +export function ThemeToggle() { + const { theme, setTheme } = useTheme(); + const [mounted, setMounted] = React.useState(false); + + // Avoid hydration mismatch + React.useEffect(() => { + setMounted(true); + }, []); + + if (!mounted) { + return ( +
+ ); + } + + return ( + + ); +}

SourceOperationStatusMetricExecution
NodeOperationStatusExecution
No telemetry data reported yet.No telemetry data reported yet.
-
- {test.device.name || "Head Unit"} - {test.device.serialNumber} +
+
+ HU +
+
+ + {test.device.name && test.device.name !== "Unnamed Device" ? test.device.name : test.device.serialNumber} + + + ID: {test.device.serialNumber} + +
- + {test.type.replace("_", " ")} {test.status === "PASS" ? ( -
- - SUCCESS +
+ + Nominal
) : ( -
- - FAILURE +
+ + Fault
)}
- {test.summary} -
- - {new Date(test.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })} + + {new Date(test.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false })} - + {new Date(test.timestamp).toLocaleDateString([], { month: 'short', day: 'numeric' })}
@@ -195,9 +226,9 @@ export default function DashboardPage() {
- INSIGHTS + Insights