diff --git a/packages/frontend/components.json b/packages/frontend/components.json
new file mode 100644
index 0000000..13e1db0
--- /dev/null
+++ b/packages/frontend/components.json
@@ -0,0 +1,21 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "new-york",
+ "rsc": false,
+ "tsx": true,
+ "tailwind": {
+ "config": "",
+ "css": "src/index.css",
+ "baseColor": "neutral",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils",
+ "ui": "@/components/ui",
+ "lib": "@/lib",
+ "hooks": "@/hooks"
+ },
+ "iconLibrary": "lucide"
+}
diff --git a/packages/frontend/index.html b/packages/frontend/index.html
index 7ce0950..b7fa078 100644
--- a/packages/frontend/index.html
+++ b/packages/frontend/index.html
@@ -1,15 +1,13 @@
+
+
+
+ ProvesKit Ground Station
+
-
-
-
- ProvesKit Ground Station
-
-
-
-
-
-
-
+
+
+
+
diff --git a/packages/frontend/package.json b/packages/frontend/package.json
index 12e266f..ab3397a 100644
--- a/packages/frontend/package.json
+++ b/packages/frontend/package.json
@@ -13,18 +13,27 @@
"typecheck:app": "tsc --noEmit -p tsconfig.app.json"
},
"dependencies": {
+ "@radix-ui/react-checkbox": "^1.1.4",
+ "@radix-ui/react-popover": "^1.1.6",
+ "@radix-ui/react-switch": "^1.1.3",
"@tailwindcss/vite": "^4.0.11",
"axios": "^1.8.1",
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
+ "lucide-react": "^0.483.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-icons": "^5.5.0",
"react-router": "^7.3.0",
"socket.io-client": "^4.8.1",
+ "tailwind-merge": "^3.0.2",
"tailwindcss": "^4.0.11",
+ "tailwindcss-animate": "^1.0.7",
"zod": "^3.24.2"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
+ "@types/node": "^22.13.10",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"@vitejs/plugin-react": "^4.3.4",
diff --git a/packages/frontend/src/components/ui/checkbox.tsx b/packages/frontend/src/components/ui/checkbox.tsx
new file mode 100644
index 0000000..29c5f2e
--- /dev/null
+++ b/packages/frontend/src/components/ui/checkbox.tsx
@@ -0,0 +1,30 @@
+import * as React from "react";
+import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
+import { CheckIcon } from "lucide-react";
+
+import { cn } from "@/lib/utils";
+
+function Checkbox({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+
+ );
+}
+
+export { Checkbox };
diff --git a/packages/frontend/src/components/ui/popover.tsx b/packages/frontend/src/components/ui/popover.tsx
new file mode 100644
index 0000000..ef5bfd0
--- /dev/null
+++ b/packages/frontend/src/components/ui/popover.tsx
@@ -0,0 +1,46 @@
+import * as React from "react";
+import * as PopoverPrimitive from "@radix-ui/react-popover";
+
+import { cn } from "@/lib/utils";
+
+function Popover({
+ ...props
+}: React.ComponentProps) {
+ return ;
+}
+
+function PopoverTrigger({
+ ...props
+}: React.ComponentProps) {
+ return ;
+}
+
+function PopoverContent({
+ className,
+ align = "center",
+ sideOffset = 4,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ );
+}
+
+function PopoverAnchor({
+ ...props
+}: React.ComponentProps) {
+ return ;
+}
+
+export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
diff --git a/packages/frontend/src/components/ui/switch.tsx b/packages/frontend/src/components/ui/switch.tsx
new file mode 100644
index 0000000..a026a47
--- /dev/null
+++ b/packages/frontend/src/components/ui/switch.tsx
@@ -0,0 +1,29 @@
+import * as React from "react";
+import * as SwitchPrimitive from "@radix-ui/react-switch";
+
+import { cn } from "@/lib/utils";
+
+function Switch({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ );
+}
+
+export { Switch };
diff --git a/packages/frontend/src/context/LoggingContext.tsx b/packages/frontend/src/context/LoggingContext.tsx
new file mode 100644
index 0000000..82da16c
--- /dev/null
+++ b/packages/frontend/src/context/LoggingContext.tsx
@@ -0,0 +1,2 @@
+export { LoggingContext } from "./logging/context";
+export { LoggingProvider as LoggingContextProvider } from "./logging/LoggingProvider";
diff --git a/packages/frontend/src/context/logging/LoggingProvider.tsx b/packages/frontend/src/context/logging/LoggingProvider.tsx
new file mode 100644
index 0000000..9d21cac
--- /dev/null
+++ b/packages/frontend/src/context/logging/LoggingProvider.tsx
@@ -0,0 +1,36 @@
+import { useContext, useEffect, useState } from "react";
+import { LoggingContext } from "./context";
+import { WebsocketContext } from "../WebsocketContext";
+
+export function LoggingProvider({ children }: { children: React.ReactNode }) {
+ const [msgs, setMsgs] = useState([]);
+ const socket = useContext(WebsocketContext);
+
+ const addMsg = (msg: string) => {
+ setMsgs((prevMsgs) => {
+ const newMsgs = [msg, ...prevMsgs];
+
+ if (newMsgs.length > 300) {
+ return newMsgs.slice(0, 300);
+ }
+
+ return newMsgs;
+ });
+ };
+
+ useEffect(() => {
+ if (socket) {
+ socket.on("terminal-data", (data) => {
+ addMsg(data);
+ });
+ }
+
+ return () => {
+ socket?.off("terminal-data");
+ };
+ }, [socket]);
+
+ return (
+ {children}
+ );
+}
diff --git a/packages/frontend/src/context/logging/context.ts b/packages/frontend/src/context/logging/context.ts
new file mode 100644
index 0000000..1e5e578
--- /dev/null
+++ b/packages/frontend/src/context/logging/context.ts
@@ -0,0 +1,3 @@
+import { createContext } from "react";
+
+export const LoggingContext = createContext([]);
diff --git a/packages/frontend/src/index.css b/packages/frontend/src/index.css
index f9ed656..f6628ed 100644
--- a/packages/frontend/src/index.css
+++ b/packages/frontend/src/index.css
@@ -1,5 +1,9 @@
@import "tailwindcss";
+@plugin "tailwindcss-animate";
+
+@custom-variant dark (&:is(.dark *));
+
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
@@ -9,6 +13,38 @@
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
+ --radius: 0.625rem;
+ --background: oklch(1 0 0);
+ --foreground: oklch(0.145 0 0);
+ --card: oklch(1 0 0);
+ --card-foreground: oklch(0.145 0 0);
+ --popover: oklch(1 0 0);
+ --popover-foreground: oklch(0.145 0 0);
+ --primary: oklch(0.205 0 0);
+ --primary-foreground: oklch(0.985 0 0);
+ --secondary: oklch(0.97 0 0);
+ --secondary-foreground: oklch(0.205 0 0);
+ --muted: oklch(0.97 0 0);
+ --muted-foreground: oklch(0.556 0 0);
+ --accent: oklch(0.97 0 0);
+ --accent-foreground: oklch(0.205 0 0);
+ --destructive: oklch(0.577 0.245 27.325);
+ --border: oklch(0.922 0 0);
+ --input: oklch(0.922 0 0);
+ --ring: oklch(0.708 0 0);
+ --chart-1: oklch(0.646 0.222 41.116);
+ --chart-2: oklch(0.6 0.118 184.704);
+ --chart-3: oklch(0.398 0.07 227.392);
+ --chart-4: oklch(0.828 0.189 84.429);
+ --chart-5: oklch(0.769 0.188 70.08);
+ --sidebar: oklch(0.985 0 0);
+ --sidebar-foreground: oklch(0.145 0 0);
+ --sidebar-primary: oklch(0.205 0 0);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.97 0 0);
+ --sidebar-accent-foreground: oklch(0.205 0 0);
+ --sidebar-border: oklch(0.922 0 0);
+ --sidebar-ring: oklch(0.708 0 0);
}
body {
@@ -27,3 +63,91 @@ h1 {
button:hover {
cursor: "pointer";
}
+
+.dark {
+ --background: oklch(0.145 0 0);
+ --foreground: oklch(0.985 0 0);
+ --card: oklch(0.205 0 0);
+ --card-foreground: oklch(0.985 0 0);
+ --popover: oklch(0.205 0 0);
+ --popover-foreground: oklch(0.985 0 0);
+ --primary: oklch(0.922 0 0);
+ --primary-foreground: oklch(0.205 0 0);
+ --secondary: oklch(0.269 0 0);
+ --secondary-foreground: oklch(0.985 0 0);
+ --muted: oklch(0.269 0 0);
+ --muted-foreground: oklch(0.708 0 0);
+ --accent: oklch(0.269 0 0);
+ --accent-foreground: oklch(0.985 0 0);
+ --destructive: oklch(0.704 0.191 22.216);
+ --border: oklch(1 0 0 / 10%);
+ --input: oklch(1 0 0 / 15%);
+ --ring: oklch(0.556 0 0);
+ --chart-1: oklch(0.488 0.243 264.376);
+ --chart-2: oklch(0.696 0.17 162.48);
+ --chart-3: oklch(0.769 0.188 70.08);
+ --chart-4: oklch(0.627 0.265 303.9);
+ --chart-5: oklch(0.645 0.246 16.439);
+ --sidebar: oklch(0.205 0 0);
+ --sidebar-foreground: oklch(0.985 0 0);
+ --sidebar-primary: oklch(0.488 0.243 264.376);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.269 0 0);
+ --sidebar-accent-foreground: oklch(0.985 0 0);
+ --sidebar-border: oklch(1 0 0 / 10%);
+ --sidebar-ring: oklch(0.556 0 0);
+}
+
+@theme inline {
+ --radius-sm: calc(var(--radius) - 4px);
+ --radius-md: calc(var(--radius) - 2px);
+ --radius-lg: var(--radius);
+ --radius-xl: calc(var(--radius) + 4px);
+ --color-background: var(--background);
+ --color-foreground: var(--foreground);
+ --color-card: var(--card);
+ --color-card-foreground: var(--card-foreground);
+ --color-popover: var(--popover);
+ --color-popover-foreground: var(--popover-foreground);
+ --color-primary: var(--primary);
+ --color-primary-foreground: var(--primary-foreground);
+ --color-secondary: var(--secondary);
+ --color-secondary-foreground: var(--secondary-foreground);
+ --color-muted: var(--muted);
+ --color-muted-foreground: var(--muted-foreground);
+ --color-accent: var(--accent);
+ --color-accent-foreground: var(--accent-foreground);
+ --color-destructive: var(--destructive);
+ --color-border: var(--border);
+ --color-input: var(--input);
+ --color-ring: var(--ring);
+ --color-chart-1: var(--chart-1);
+ --color-chart-2: var(--chart-2);
+ --color-chart-3: var(--chart-3);
+ --color-chart-4: var(--chart-4);
+ --color-chart-5: var(--chart-5);
+ --color-sidebar: var(--sidebar);
+ --color-sidebar-foreground: var(--sidebar-foreground);
+ --color-sidebar-primary: var(--sidebar-primary);
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
+ --color-sidebar-accent: var(--sidebar-accent);
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
+ --color-sidebar-border: var(--sidebar-border);
+ --color-sidebar-ring: var(--sidebar-ring);
+}
+
+@layer base {
+ * {
+ @apply border-border outline-ring/50;
+ }
+
+ body {
+ @apply bg-background text-foreground;
+ }
+}
+
+@layer utilities {
+ .h-screen {
+ height: calc(100vh - 56px);
+ }
+}
diff --git a/packages/frontend/src/lib/utils.ts b/packages/frontend/src/lib/utils.ts
new file mode 100644
index 0000000..a5ef193
--- /dev/null
+++ b/packages/frontend/src/lib/utils.ts
@@ -0,0 +1,6 @@
+import { clsx, type ClassValue } from "clsx";
+import { twMerge } from "tailwind-merge";
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs));
+}
diff --git a/packages/frontend/src/main.tsx b/packages/frontend/src/main.tsx
index ce5d786..a5f68d8 100644
--- a/packages/frontend/src/main.tsx
+++ b/packages/frontend/src/main.tsx
@@ -9,6 +9,7 @@ import Settings from "./routes/Settings";
import Files from "./routes/Files";
import { WebsocketContextProvider } from "./context/WebsocketContext";
import { SatelliteContextProvider } from "./context/SatelliteContext";
+import { LoggingContextProvider } from "./context/LoggingContext";
createRoot(document.getElementById("root")!).render(
@@ -16,14 +17,16 @@ createRoot(document.getElementById("root")!).render(
-
- }>
- } />
- } />
- } />
- } />
-
-
+
+
+ }>
+ } />
+ } />
+ } />
+ } />
+
+
+
diff --git a/packages/frontend/src/routes/Home.tsx b/packages/frontend/src/routes/Home.tsx
index 306ae4c..abdfb76 100644
--- a/packages/frontend/src/routes/Home.tsx
+++ b/packages/frontend/src/routes/Home.tsx
@@ -1,60 +1,138 @@
-import { useContext, useEffect, useState } from "react";
+import { useContext, useState } from "react";
import { FaBug, FaCheck, FaInfoCircle } from "react-icons/fa";
-import { IoWarning } from "react-icons/io5";
+import { IoChevronDownOutline, IoWarning } from "react-icons/io5";
+import { RxDownload } from "react-icons/rx";
import { IconType } from "react-icons/lib";
-import { MdError, MdErrorOutline } from "react-icons/md";
+import { MdError, MdErrorOutline, MdOutlineContentCopy } from "react-icons/md";
import { z } from "zod";
+import { LoggingContext } from "../context/LoggingContext";
import { WebsocketContext } from "../context/WebsocketContext";
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@/components/ui/popover";
+import { Switch } from "@/components/ui/switch";
+import { Checkbox } from "@/components/ui/checkbox";
export default function Home() {
- const [msgs, setMsgs] = useState([]);
const [cmdInput, setCmdInput] = useState("");
- const socket = useContext(WebsocketContext);
-
- useEffect(() => {
- if (socket) {
- socket.on("terminal-data", (data) => {
- setMsgs((prev) => [data, ...prev]);
- });
- }
+ const [rawDisplay, setRawDisplay] = useState(false);
+ const [filterState, setFilterState] = useState<{ [key: string]: boolean }>({
+ NOTSET: true,
+ DEBUG: true,
+ INFO: true,
+ WARNING: true,
+ ERROR: true,
+ CRITICAL: true,
+ });
- return () => {
- socket?.off("terminal-data");
- };
- }, [socket]);
+ const msgs = useContext(LoggingContext);
+ const socket = useContext(WebsocketContext);
return (
-
-
+
+
Logging
+
+
+
Show Raw Logs
+
+
+ Filters
+
+
+
+ {Object.keys(filterState).map((k, idx) => (
+
+
+ setFilterState((oldState) => ({
+ ...oldState,
+ [k]: c as boolean,
+ }))
+ }
+ />
+ {k}
+
+ ))}
+
+
+
+
+ Export Logs
+
+
+
+
+
+
+
+
-
+
{msgs.map((msg, idx) => {
try {
const validatedLog = LogSchema.parse(JSON.parse(msg));
+ if (!filterState[validatedLog.level]) {
+ return;
+ }
+
+ if (rawDisplay) {
+ return (
+
+ {msg}
+
+ );
+ }
+
return
;
} catch {
- return
{msg}
;
+ return (
+
+ {msg}
+
+ );
}
})}
-
-
setCmdInput(evt.target.value)}
- className="border-2 h-10 rounded-md border-neutral-600"
- />
-