diff --git a/src/App.tsx b/src/App.tsx
index ac53612..4e847ab 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -12,9 +12,8 @@ import { useState } from "react";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import { Provider as JotaiProvider } from "jotai";
import { Home } from "./Home.tsx";
-import { Autonomy } from "./Autonomy.tsx";
+import { Video } from "./Video.tsx";
import { Version } from "./Version.tsx";
-import { Diagnostics } from "./Diagnostics.tsx";
import "./App.css";
function App() {
@@ -41,12 +40,8 @@ const router = createBrowserRouter([
element: ,
},
{
- path: "/autonomy",
- element: ,
- },
- {
- path: "/diagnostics",
- element: ,
+ path: "/video",
+ element: ,
},
]);
diff --git a/src/Autonomy.tsx b/src/Autonomy.tsx
deleted file mode 100644
index 93b7f26..0000000
--- a/src/Autonomy.tsx
+++ /dev/null
@@ -1,90 +0,0 @@
-//
-// Copyright 2026 James Pace
-//
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this
-// file, You can obtain one at https://mozilla.org/MPL/2.0/.
-//
-// This Source Code Form is "Incompatible With Secondary Licenses", as
-// defined by the Mozilla Public License, v. 2.0.
-//
-import { AppNav, Footer } from "./components/AppNav.tsx";
-import { Container, Row, Col, Image, Card } from "react-bootstrap";
-import { atom, useAtomValue } from "jotai";
-import { atomWithQuery } from "jotai-tanstack-query";
-import { WebRTCVideo } from "mediamtx-webrtc-react";
-
-const statusStringQuery = async () => {
- const resp = await fetch("api/status");
- if (!resp.ok) {
- throw new Error("Network response was not ok");
- }
- return resp.json();
-};
-const statusStringQueryAtom = atomWithQuery(() => ({
- queryKey: ["status_string"],
- queryFn: statusStringQuery,
- refetchInterval: 500, // 0.5s
-}));
-const statusStringAtom = atom((get) => {
- const response = get(statusStringQueryAtom);
- if (response.isPending || response.isError || !response.data.status) {
- return "N/A";
- }
- return response.data.message;
-});
-
-const latLongQuery = async () => {
- const resp = await fetch("api/position");
- if (!resp.ok) {
- throw new Error("Network response was not ok");
- }
- return resp.json();
-};
-const latLongQueryAtom = atomWithQuery(() => ({
- queryKey: ["latlong"],
- queryFn: latLongQuery,
- refetchInterval: 500, // 0.5s
-}));
-const latLongAtom = atom((get) => {
- const response = get(latLongQueryAtom);
- if (response.isPending || response.isError || !response.data.status) {
- return "N/A";
- }
- return `(${response.data.latitude}, ${response.data.longitude})`;
-});
-
-export function Autonomy() {
- const statusString = useAtomValue(statusStringAtom);
- const latLong = useAtomValue(latLongAtom);
-
- const imageStreamURI =
- "http://" + window.location.hostname + ":8889/image/whep";
-
- return (
-
-
-
-
-
-
-
-
-
- Status:
- {statusString}
- Latitude, Longitude:
- {latLong}
-
-
-
-
-
-
- );
-}
diff --git a/src/Diagnostics.tsx b/src/Diagnostics.tsx
deleted file mode 100644
index 970ac16..0000000
--- a/src/Diagnostics.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-//
-// Copyright 2026 James Pace
-//
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this
-// file, You can obtain one at https://mozilla.org/MPL/2.0/.
-//
-// This Source Code Form is "Incompatible With Secondary Licenses", as
-// defined by the Mozilla Public License, v. 2.0.
-//
-import { AppNav, Footer } from "./components/AppNav.tsx";
-import { TreeView } from "./components/DiagnosticView.tsx";
-import { Container, Row, Col, Card } from "react-bootstrap";
-import { atom, useAtomValue } from "jotai";
-import { atomWithQuery } from "jotai-tanstack-query";
-
-const diagQueryFn = async () => {
- const resp = await fetch("api/diagnostics");
- if (!resp.ok) {
- throw new Error("Network response was not ok");
- }
- return resp.json();
-};
-
-const diagQueryAtom = atomWithQuery(() => ({
- queryKey: ["diagnostics"],
- queryFn: diagQueryFn,
-}));
-
-const diagAtom = atom((get) => {
- const diag = get(diagQueryAtom);
- if (diag.isPending) {
- return "Loading...";
- }
- if (diag.isError) {
- return "Error loading!";
- }
- if (!diag.data.status) {
- return "Failed to get diagnostic information.";
- }
- return diag.data.message;
-});
-
-export function Diagnostics() {
- const diagnostics = useAtomValue(diagAtom);
-
- const diagContent = () => {
- if (typeof diagnostics === "string" || diagnostics instanceof String) {
- return {diagnostics} ;
- }
- return ;
- };
-
- return (
-
-
-
-
- Diagnostics
- {diagContent()}
-
-
-
-
- );
-}
diff --git a/src/components/DiagnosticView.tsx b/src/DiagnosticsCard.tsx
similarity index 52%
rename from src/components/DiagnosticView.tsx
rename to src/DiagnosticsCard.tsx
index 7f06fe1..c355a24 100644
--- a/src/components/DiagnosticView.tsx
+++ b/src/DiagnosticsCard.tsx
@@ -1,20 +1,64 @@
import React, { useState } from "react";
-import { Button, ListGroup, Collapse, Card } from "react-bootstrap";
+import { Button, ListGroup, Collapse } from "react-bootstrap";
import {
Dash,
ChevronDown,
ChevronRight,
CircleFill,
} from "react-bootstrap-icons";
-import Table from "react-bootstrap/Table";
-import Container from "react-bootstrap/Container";
-import Row from "react-bootstrap/Row";
-import Col from "react-bootstrap/Col";
+import { Container, Row, Col, Table, Card } from "react-bootstrap";
+import { atom, useAtomValue } from "jotai";
+import { atomWithQuery } from "jotai-tanstack-query";
-export function TreeView(props: Any) {
+const diagQueryFn = async () => {
+ const resp = await fetch("api/diagnostics");
+ if (!resp.ok) {
+ throw new Error("Network response was not ok");
+ }
+ return resp.json();
+};
+
+const diagQueryAtom = atomWithQuery(() => ({
+ queryKey: ["diagnostics"],
+ queryFn: diagQueryFn,
+}));
+
+const diagAtom = atom((get) => {
+ const diag = get(diagQueryAtom);
+ if (diag.isPending) {
+ return "Loading...";
+ }
+ if (diag.isError) {
+ return "Error loading!";
+ }
+ if (!diag.data.status) {
+ return "Failed to get diagnostic information.";
+ }
+ return diag.data.message;
+});
+
+export function DiagnosticsCard() {
+ const diagnostics = useAtomValue(diagAtom);
+
+ const diagContent = () => {
+ if (typeof diagnostics === "string" || diagnostics instanceof String) {
+ return {diagnostics} ;
+ }
+ return ;
+ };
+
+ return (
+
+ Diagnostics
+ {diagContent()}
+
+ );
+}
+
+function TreeView(props: Any) {
const data = props.data;
return (
-
+
{data.children.map((node) => (
))}
@@ -70,17 +114,15 @@ function StatusContent(props: Any) {
const status = props.status;
console.log(status);
return (
-
-
- {status.name}
-
-
+
+
{status.name}
+
-
-
-
-
-
+
+
+
+
+
);
}
@@ -106,7 +148,7 @@ function ChildStatuses(props: Any) {
return (
-
+
{node.children.map((child) => (
))}
@@ -128,26 +170,27 @@ function SingleNode(props: Any) {
};
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
);
}
diff --git a/src/Home.tsx b/src/Home.tsx
index 210718a..5d2360e 100644
--- a/src/Home.tsx
+++ b/src/Home.tsx
@@ -12,17 +12,94 @@ import { AppNav, Footer } from "./components/AppNav.tsx";
import { Container, Row, Col, Image, Card } from "react-bootstrap";
import { atom, useAtomValue } from "jotai";
import { atomWithQuery } from "jotai-tanstack-query";
+import { DiagnosticsCard } from "./DiagnosticsCard.tsx";
+
+const statusStringQuery = async () => {
+ const resp = await fetch("api/status");
+ if (!resp.ok) {
+ throw new Error("Network response was not ok");
+ }
+ return resp.json();
+};
+const statusStringQueryAtom = atomWithQuery(() => ({
+ queryKey: ["status_string"],
+ queryFn: statusStringQuery,
+ refetchInterval: 500, // 0.5s
+}));
+const statusStringAtom = atom((get) => {
+ const response = get(statusStringQueryAtom);
+ if (response.isPending || response.isError || !response.data.status) {
+ return "N/A";
+ }
+ return response.data.message;
+});
+
+const uptimeQuery = async () => {
+ const resp = await fetch("api/uptime");
+ if (!resp.ok) {
+ throw new Error("Network response was not ok");
+ }
+ return resp.json();
+};
+const uptimeQueryAtom = atomWithQuery(() => ({
+ queryKey: ["uptime"],
+ queryFn: uptimeQuery,
+ refetchInterval: 1000, // 1s
+}));
+const uptimeAtom = atom((get) => {
+ const response = get(uptimeQueryAtom);
+ if (response.isPending || response.isError) {
+ return "N/A";
+ }
+ return response.data.uptime.toFixed(1);
+});
+
+function StatusCard() {
+ const statusString = useAtomValue(statusStringAtom);
+ return (
+
+
+ Status:
+
+
+ {statusString}
+
+
+ );
+}
+
+function UptimeCard() {
+ const uptime = useAtomValue(uptimeAtom);
+ return (
+
+
+ Uptime:
+
+
+ {uptime}s
+
+
+ );
+}
export function Home() {
return (
-
-
- Hello World!
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Video.tsx b/src/Video.tsx
new file mode 100644
index 0000000..949f770
--- /dev/null
+++ b/src/Video.tsx
@@ -0,0 +1,41 @@
+//
+// Copyright 2026 James Pace
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+//
+// This Source Code Form is "Incompatible With Secondary Licenses", as
+// defined by the Mozilla Public License, v. 2.0.
+//
+import { AppNav, Footer } from "./components/AppNav.tsx";
+import { Container, Row, Col, Image, Card } from "react-bootstrap";
+import { WebRTCVideo } from "mediamtx-webrtc-react";
+
+export function Video() {
+ const streamName = "image";
+ const imageStreamURI =
+ "http://" + window.location.hostname + ":8889/" + streamName + "/whep";
+
+ return (
+
+
+
+
+
+
+
+
+ Showing stream: "{streamName}".
+
+
+
+
+
+ );
+}
diff --git a/src/components/AppNav.tsx b/src/components/AppNav.tsx
index b7175f0..20686b5 100644
--- a/src/components/AppNav.tsx
+++ b/src/components/AppNav.tsx
@@ -24,15 +24,12 @@ export function AppNav() {
Home
+
+ Video
+
Version
-
- Autonomy
-
-
- Diagnostics
-