From e836e7dd4ed5e9fa60e949d159100040b22a8f48 Mon Sep 17 00:00:00 2001 From: Arne Rief Date: Mon, 22 Dec 2025 21:20:39 +0100 Subject: Movement simulator for all and single robot, project v1 ready --- backend/src/controllers/createRobot.ts | 18 +- backend/src/controllers/getRobots.ts | 14 +- backend/src/controllers/loginUser.ts | 32 +-- backend/src/controllers/moveAllRobots.ts | 25 +++ backend/src/controllers/moveRobot.ts | 30 +++ backend/src/controllers/stopAllRobots.ts | 21 ++ backend/src/controllers/stopRobot.ts | 27 +++ backend/src/routes/router.ts | 10 + backend/src/simulation/robotMovementSimulator.ts | 241 +++++++++++++++++++++++ backend/src/types/express.d.ts | 5 - backend/src/types/robot.ts | 30 ++- backend/src/types/user.ts | 7 + frontend/.gitignore | 17 +- frontend/src/components/AddRobotForm.tsx | 11 +- frontend/src/components/CityMap.tsx | 6 +- frontend/src/components/RobotList.tsx | 98 ++++++++- frontend/src/components/Sidebar.tsx | 26 ++- frontend/src/components/SimulationActions.tsx | 35 ++-- frontend/src/pages/Dashboard.tsx | 13 +- frontend/src/styles/index.css | 16 +- frontend/src/types/robot.ts | 23 ++- 21 files changed, 612 insertions(+), 93 deletions(-) create mode 100644 backend/src/controllers/moveAllRobots.ts create mode 100644 backend/src/controllers/moveRobot.ts create mode 100644 backend/src/controllers/stopAllRobots.ts create mode 100644 backend/src/controllers/stopRobot.ts create mode 100644 backend/src/simulation/robotMovementSimulator.ts diff --git a/backend/src/controllers/createRobot.ts b/backend/src/controllers/createRobot.ts index fa483af..dbc1959 100644 --- a/backend/src/controllers/createRobot.ts +++ b/backend/src/controllers/createRobot.ts @@ -1,14 +1,16 @@ import { Request, Response } from "express"; import { QueryResult } from "pg"; +import { Server } from "socket.io"; import db from "../database/postgres.js"; import redisClient from "../database/redis.js"; +import { ErrorResponse } from "../types/error.js"; import { CreateRequest } from "../types/request.js"; -import { Robot } from "../types/robot.js"; +import { CreateRobotResponse, Robot } from "../types/robot.js"; const ROBOTS_CACHE_KEY = "allMyRobots"; async function createRobot(req: Request, res: Response) { - const io = req.app.get("io"); + const io: Server = req.app.get("io"); const { name } = req.body as CreateRequest; @@ -44,16 +46,20 @@ async function createRobot(req: Request, res: Response) { io.emit("robots_update", allRobots); console.log("WebSocket update with newly created robot."); - return res.status(201).json({ + const createRobotResponse: CreateRobotResponse = { message: "Robot successfully created.", robot: newRobot, - }); + }; + + return res.status(201).json(createRobotResponse); } catch (error) { console.error("Error creating the robot: ", error); - return res.status(500).json({ + + const errorResponse: ErrorResponse = { message: "Internal server error during robot creation.", error, - }); + }; + return res.status(500).json(errorResponse); } } diff --git a/backend/src/controllers/getRobots.ts b/backend/src/controllers/getRobots.ts index b634306..80b430f 100644 --- a/backend/src/controllers/getRobots.ts +++ b/backend/src/controllers/getRobots.ts @@ -17,9 +17,14 @@ async function getRobots(_req: Request, res: Response) { console.log("Data served from Redis cache."); const robots: Robot[] = JSON.parse(cachedData); + const simulationRunning = robots?.every( + (robot) => robot.status === "moving" + ); + const response: RobotsResponse = { source: "cache", - data: robots, + robots, + simulationRunning, }; return res.status(200).json(response); @@ -31,6 +36,10 @@ async function getRobots(_req: Request, res: Response) { ); const robots = robotsQuery.rows; + const simulationRunning = robots?.every( + (robot) => robot.status === "moving" + ); + await redisClient.set(ROBOTS_CACHE_KEY, JSON.stringify(robots), { EX: CACHE_TTL, }); @@ -38,7 +47,8 @@ async function getRobots(_req: Request, res: Response) { console.log("Cache miss: data queried from PostgreSQL."); const response: RobotsResponse = { source: "database", - data: robots, + robots, + simulationRunning, }; return res.status(200).json(response); diff --git a/backend/src/controllers/loginUser.ts b/backend/src/controllers/loginUser.ts index 860bce2..9cfb992 100644 --- a/backend/src/controllers/loginUser.ts +++ b/backend/src/controllers/loginUser.ts @@ -3,15 +3,20 @@ import { Request, Response } from "express"; import jwt from "jsonwebtoken"; import { QueryResult } from "pg"; import db from "../database/postgres.js"; +import { ErrorResponse } from "../types/error.js"; import type { LoginRequest } from "../types/request.js"; -import type { AuthorizedUser, DatabaseUser } from "../types/user.js"; +import type { + AuthorizedUser, + DatabaseUser, + LoginResponse, +} from "../types/user.js"; async function loginUser(req: Request, res: Response) { const { email, password } = req.body as LoginRequest; if (!email || !password) { return res.status(400).json({ - message: "E-Mail und Passwort sind erforderlich.", + message: "E-mail and password are required.", }); } @@ -25,7 +30,7 @@ async function loginUser(req: Request, res: Response) { const user = queryResult.rows[0]; if (!user) { - return res.status(401).json({ message: "Login Daten ungültig." }); + return res.status(401).json({ message: "Login data invalid." }); } // Check if password is correct @@ -36,7 +41,7 @@ async function loginUser(req: Request, res: Response) { if (!isValidPassword) { return res.status(401).json({ - message: "Das Passwort ist nicht korrekt.", + message: "The password is incorrect.", }); } @@ -49,16 +54,21 @@ async function loginUser(req: Request, res: Response) { // Create token for authentication const token = jwt.sign(userData, process.env.JWT_SECRET!); - return res.status(200).json({ - message: "Erfolgreiche Anmeldung.", + const loginResponse: LoginResponse = { + message: "Successful login.", user: userData, token, - }); + }; + + return res.status(200).json(loginResponse); } catch (error) { - console.error("Fehler beim Login: ", error); - return res - .status(500) - .json({ message: "Interner Serverfehler beim Login.", error }); + console.error("Error on login attempt: ", error); + + const errorResponse: ErrorResponse = { + message: "Internal server error on login attempt.", + error, + }; + return res.status(500).json(errorResponse); } } diff --git a/backend/src/controllers/moveAllRobots.ts b/backend/src/controllers/moveAllRobots.ts new file mode 100644 index 0000000..e4d1252 --- /dev/null +++ b/backend/src/controllers/moveAllRobots.ts @@ -0,0 +1,25 @@ +import { Request, Response } from "express"; +import { Server } from "socket.io"; +import { setAllRobotsMoving } from "../simulation/robotMovementSimulator"; +import { ErrorResponse } from "../types/error"; +import { SimulationResponse } from "../types/robot"; + +async function moveAllRobots(req: Request, res: Response) { + const io: Server = req.app.get("io"); + + try { + const result: SimulationResponse = await setAllRobotsMoving(io); + return res.status(200).json(result); + } catch (error) { + console.error("Error on trying to set all robots moving:", error); + + const errorResponse: ErrorResponse = { + message: + "Internal server error on trying to set all robots moving.", + error, + }; + return res.status(500).json(errorResponse); + } +} + +export default moveAllRobots; diff --git a/backend/src/controllers/moveRobot.ts b/backend/src/controllers/moveRobot.ts new file mode 100644 index 0000000..1b53f17 --- /dev/null +++ b/backend/src/controllers/moveRobot.ts @@ -0,0 +1,30 @@ +import { Request, Response } from "express"; +import { Server } from "socket.io"; +import { setRobotMoving } from "../simulation/robotMovementSimulator"; +import { ErrorResponse } from "../types/error"; +import { SimulationResponse } from "../types/robot"; + +async function moveRobot(req: Request, res: Response) { + const io: Server = req.app.get("io"); + + const robotId = Number(req.params.id); + + if (!robotId || Number.isNaN(robotId)) { + return res.status(400).json({ message: "Invalid robot ID." }); + } + + try { + const result: SimulationResponse = await setRobotMoving(io, robotId); + return res.status(200).json(result); + } catch (error) { + console.error(`Error on trying to start robot ID ${robotId}: `, error); + + const errorResponse: ErrorResponse = { + message: `Internal server error on trying to start robot ID ${robotId}.`, + error, + }; + return res.status(500).json(errorResponse); + } +} + +export default moveRobot; diff --git a/backend/src/controllers/stopAllRobots.ts b/backend/src/controllers/stopAllRobots.ts new file mode 100644 index 0000000..7218e66 --- /dev/null +++ b/backend/src/controllers/stopAllRobots.ts @@ -0,0 +1,21 @@ +import { Request, Response } from "express"; +import { setAllRobotsIdle } from "../simulation/robotMovementSimulator"; +import { ErrorResponse } from "../types/error"; +import { SimulationResponse } from "../types/robot"; + +async function stopAllRobots(_req: Request, res: Response) { + try { + const result: SimulationResponse = await setAllRobotsIdle(); + return res.status(200).json(result); + } catch (error) { + console.error("Error on trying to stop all robots:", error); + + const errorResponse: ErrorResponse = { + message: "Internal server error on trying to stop all robots.", + error, + }; + return res.status(500).json(errorResponse); + } +} + +export default stopAllRobots; diff --git a/backend/src/controllers/stopRobot.ts b/backend/src/controllers/stopRobot.ts new file mode 100644 index 0000000..d0d7c4f --- /dev/null +++ b/backend/src/controllers/stopRobot.ts @@ -0,0 +1,27 @@ +import { Request, Response } from "express"; +import { setRobotIdle } from "../simulation/robotMovementSimulator"; +import { ErrorResponse } from "../types/error"; +import { SimulationResponse } from "../types/robot"; + +async function stopRobot(req: Request, res: Response) { + const robotId = Number(req.params.id); + + if (!robotId || Number.isNaN(robotId)) { + return res.status(400).json({ message: "Ungültige Roboter ID." }); + } + + try { + const result: SimulationResponse = await setRobotIdle(robotId); + return res.status(200).json(result); + } catch (error) { + console.error(`Error on trying to stop robot ID ${robotId}: `, error); + + const errorResponse: ErrorResponse = { + message: `Internal server error on trying to stop robot ID ${robotId}.`, + error, + }; + return res.status(500).json(errorResponse); + } +} + +export default stopRobot; diff --git a/backend/src/routes/router.ts b/backend/src/routes/router.ts index b526a9b..72fd885 100644 --- a/backend/src/routes/router.ts +++ b/backend/src/routes/router.ts @@ -3,6 +3,10 @@ import createRobot from "../controllers/createRobot.js"; import generateAdmin from "../controllers/generateAdmin.js"; import getRobots from "../controllers/getRobots.js"; import loginUser from "../controllers/loginUser.js"; +import moveAllRobots from "../controllers/moveAllRobots.js"; +import moveRobot from "../controllers/moveRobot.js"; +import stopAllRobots from "../controllers/stopAllRobots.js"; +import stopRobot from "../controllers/stopRobot.js"; import authenticateUser from "../middleware/authCheck.js"; const router = Router(); @@ -15,6 +19,12 @@ router.post("/auth/login", loginUser); router.get("/robots", authenticateUser, getRobots); // Create a new robot; protected route router.post("/robots", authenticateUser, createRobot); +// All robots move or stop; protected routes +router.post("/robots/move", authenticateUser, moveAllRobots); +router.post("/robots/stop", authenticateUser, stopAllRobots); +// Single robot move or stop; protected routes +router.post("/robots/:id/move", authenticateUser, moveRobot); +router.post("/robots/:id/stop", authenticateUser, stopRobot); export default router; diff --git a/backend/src/simulation/robotMovementSimulator.ts b/backend/src/simulation/robotMovementSimulator.ts new file mode 100644 index 0000000..2299f43 --- /dev/null +++ b/backend/src/simulation/robotMovementSimulator.ts @@ -0,0 +1,241 @@ +import { QueryResult } from "pg"; +import { Server } from "socket.io"; +import db from "../database/postgres.js"; +import { + Robot, + RobotPosition, + RobotsUpdateBroadcast, + SimulationResponse, +} from "../types/robot.js"; + +// Coordinates for the boundaries of the frontend map +const LEIPZIG_AREA = { + WEST: 12.22, + SOUTH: 51.26, + EAST: 12.54, + NORTH: 51.44, +} as const; + +const LEIPZIG_CENTER = { + LAT: 51.340863, + LON: 12.375919, +} as const; + +const SIMULATION_INTERVAL_MS = 2000; + +// IDs of moving robots +const movingRobots = new Set(); + +// Global simulation timer +let simulationTimer: NodeJS.Timeout | null = null; + +/* + MOVEMENT FUNCTION + generate random position within Leipzig based on current position +*/ + +function generateRandomPosition(currentLat: string, currentLon: string) { + // 0.0005 degrees ca. 50-70 meters + const deltaLat = (Math.random() - 0.5) * 0.0005; + const deltaLon = (Math.random() - 0.5) * 0.0005; + + let newLat = parseFloat(currentLat) + deltaLat; + let newLon = parseFloat(currentLon) + deltaLon; + + // Check if robot's new position would be outside of Leipzig, if yes set back to center + const outsideLatBoundary = + newLat < LEIPZIG_AREA.SOUTH || newLat > LEIPZIG_AREA.NORTH; + const outsideLonBoundary = + newLon < LEIPZIG_AREA.WEST || newLon > LEIPZIG_AREA.EAST; + + if (outsideLatBoundary || outsideLonBoundary) { + newLat = LEIPZIG_CENTER.LAT; + newLon = LEIPZIG_CENTER.LON; + } + + return { + lat: newLat.toFixed(7), + lon: newLon.toFixed(7), + }; +} + +/* + MAIN FUNCTION: + move a robot, update database, websocket broadcast +*/ + +async function updateRobotPositions(io: Server) { + let client; + + try { + client = await db.connect(); + + const allRobotsQuery: QueryResult = await client.query( + "SELECT * FROM robots ORDER BY id;" + ); + const allRobots = allRobotsQuery.rows; + + // Update database + await client.query("BEGIN"); + + const updatedRobots: Robot[] = []; + + for (const robot of allRobots) { + // If not moving, keep position & set idle + if (!movingRobots?.has(robot.id)) { + if (robot.status === "moving") { + const idleQuery: QueryResult = await client.query( + ` + UPDATE robots + SET status = 'idle', + updated_at = CURRENT_TIMESTAMP + WHERE id = $1 + RETURNING *; + `, + [robot.id] + ); + updatedRobots.push(idleQuery.rows[0]); + } else { + updatedRobots.push(robot); + } + continue; + } + + // If moving, assign new position and add previous to log + const newPos: RobotPosition = generateRandomPosition( + robot.lat, + robot.lon + ); + + const previousPositionsLog: RobotPosition[] = + robot.robot_positions || []; + + previousPositionsLog.push({ lat: robot.lat, lon: robot.lon }); + + // Limit log to 10 entries, remove oldest when more + if (previousPositionsLog.length > 10) { + previousPositionsLog.shift(); + } + + const updateQuery: QueryResult = await client.query( + ` + UPDATE robots + SET + lat = $1, + lon = $2, + status = 'moving', + robot_positions = $3::jsonb, + updated_at = CURRENT_TIMESTAMP + WHERE id = $4 + RETURNING *; + `, + [ + newPos.lat, + newPos.lon, + JSON.stringify(previousPositionsLog), + robot.id, + ] + ); + + updatedRobots.push(updateQuery.rows[0]); + } + + await client.query("COMMIT"); + + // Websocket broadcast, make sure robots stay in correct order + updatedRobots.sort((a, b) => a.id - b.id); + + const broadcastPayload: RobotsUpdateBroadcast = { updatedRobots }; + + io.emit("robots_update", broadcastPayload); + + // Check if this was the final update, setting all to idle with no more robots moving + if (movingRobots.size === 0) { + if (simulationTimer) { + clearInterval(simulationTimer); + simulationTimer = null; + } + } + } catch (error) { + console.error("Error in the global simulation timer: ", error); + if (client) await client.query("ROLLBACK"); + + // Stop simulation on error + if (simulationTimer) { + clearInterval(simulationTimer); + simulationTimer = null; + } + } finally { + if (client) client.release(); + } +} + +/* + SIMULATION FUNCTION + Runs the simulation by repeating the main function in intervals +*/ + +async function startSimulation(io: Server) { + if (simulationTimer) return; // Already running + + // Immediate execution right away without delay + await updateRobotPositions(io); + + // Then interval for subsequent updates + simulationTimer = setInterval(async () => { + await updateRobotPositions(io); + }, SIMULATION_INTERVAL_MS); +} + +/* + PUBLIC CONTROLLER FUNCTIONS: + start or stop movement of one or all robots +*/ + +export async function setRobotMoving( + io: Server, + robotId: number +): Promise { + movingRobots.add(Number(robotId)); + await startSimulation(io); + + return { + message: `Robot ID ${robotId} set in motion.`, + status: "moving", + }; +} + +export async function setRobotIdle( + robotId: number +): Promise { + movingRobots.delete(Number(robotId)); + + return { + message: `Robot ID ${robotId} stopped.`, + status: "idle", + }; +} + +export async function setAllRobotsMoving( + io: Server +): Promise { + const allRobotIDsQuery: QueryResult<{ id: Robot["id"] }> = await db.query( + "SELECT id FROM robots;" + ); + allRobotIDsQuery.rows.forEach((queryObject) => + movingRobots.add(queryObject.id) + ); + + await startSimulation(io); + + return { + message: "All robots set in motion.", + }; +} + +export async function setAllRobotsIdle(): Promise { + movingRobots.clear(); + return { + message: "All robots stopped.", + }; +} diff --git a/backend/src/types/express.d.ts b/backend/src/types/express.d.ts index 266a9f9..7e93345 100644 --- a/backend/src/types/express.d.ts +++ b/backend/src/types/express.d.ts @@ -1,12 +1,7 @@ import type { AuthorizedUser } from "./user.js"; -import { Server } from "socket.io"; declare global { namespace Express { - interface Application { - io: Server; - } - interface Request { user?: AuthorizedUser; } diff --git a/backend/src/types/robot.ts b/backend/src/types/robot.ts index 40ce282..fe20422 100644 --- a/backend/src/types/robot.ts +++ b/backend/src/types/robot.ts @@ -1,14 +1,16 @@ export type RobotPosition = { - lat: number; - lon: number; + lat: string; + lon: string; }; +export type RobotStatus = "idle" | "moving"; + export type Robot = { id: number; name: string; - status: "idle" | "moving"; - lat: number; - lon: number; + status: RobotStatus; + lat: string; + lon: string; robot_positions: RobotPosition[]; created_at: string; updated_at: string; @@ -16,5 +18,21 @@ export type Robot = { export type RobotsResponse = { source: "cache" | "database"; - data: Robot[]; + robots: Robot[]; + simulationRunning: boolean; +}; + +export type CreateRobotResponse = { + message: string; + robot: Robot; +}; + +export type RobotsUpdateBroadcast = { + updatedRobots: Robot[]; +}; + +export type SimulationResponse = { + message: string; + status?: RobotStatus; }; + diff --git a/backend/src/types/user.ts b/backend/src/types/user.ts index b2c7ffc..15b24c3 100644 --- a/backend/src/types/user.ts +++ b/backend/src/types/user.ts @@ -16,3 +16,10 @@ export type DatabaseUser = { password_hash: string; created_at: string; }; + +export type LoginResponse = { + message: string; + user: AuthorizedUser; + token: string; +}; + diff --git a/frontend/.gitignore b/frontend/.gitignore index a547bf3..0aa16e3 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -1,24 +1,9 @@ -# Logs logs *.log npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* node_modules dist dist-ssr +.env *.local - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? diff --git a/frontend/src/components/AddRobotForm.tsx b/frontend/src/components/AddRobotForm.tsx index c178657..0d2a00f 100644 --- a/frontend/src/components/AddRobotForm.tsx +++ b/frontend/src/components/AddRobotForm.tsx @@ -5,22 +5,26 @@ import { type SetStateAction, } from "react"; import type { ErrorResponse } from "../types/error"; -import type { CreateRobotResponse } from "../types/robot"; +import type { CreateRobotResponse, Robot } from "../types/robot"; type Props = { apiUrl: string; errorMessage: string; + robots: Robot[]; token: string | null; setErrorMessage: Dispatch>; setIsAddingRobot: Dispatch>; + setRobots: Dispatch>; }; function AddRobotForm({ apiUrl, errorMessage, + robots, token, setErrorMessage, setIsAddingRobot, + setRobots, }: Props) { const [isSubmitting, setIsSubmitting] = useState(false); const [newRobotName, setNewRobotName] = useState(""); @@ -69,7 +73,10 @@ function AddRobotForm({ } const data: CreateRobotResponse = await response.json(); - console.log("Robot created successfully: ", data); + console.log(data.message, data.robot); + + const newRobotList = [...robots, data.robot]; + setRobots(newRobotList); // Reset form and close input field setNewRobotName(""); diff --git a/frontend/src/components/CityMap.tsx b/frontend/src/components/CityMap.tsx index b61e69a..795598e 100644 --- a/frontend/src/components/CityMap.tsx +++ b/frontend/src/components/CityMap.tsx @@ -65,9 +65,11 @@ function CityMap({ robots }: Props) { source.clear(); - robots.forEach((robot) => { + robots?.forEach((robot) => { const feature = new Feature({ - geometry: new Point(fromLonLat([robot?.lon, robot?.lat])), + geometry: new Point( + fromLonLat([parseFloat(robot?.lon), parseFloat(robot?.lat)]) + ), robotId: robot?.id, }); diff --git a/frontend/src/components/RobotList.tsx b/frontend/src/components/RobotList.tsx index d711372..21d8e63 100644 --- a/frontend/src/components/RobotList.tsx +++ b/frontend/src/components/RobotList.tsx @@ -1,11 +1,22 @@ -import { useState } from "react"; -import type { Robot } from "../types/robot"; +import { + useState, + type Dispatch, + type MouseEvent, + type SetStateAction, +} from "react"; +import type { ErrorResponse } from "../types/error"; +import type { Robot, SimulationResponse } from "../types/robot"; type ExpandedRobotsState = Record; -type Props = { robots: Robot[]; }; +type Props = { + apiUrl: string; + robots: Robot[]; + token: string | null; + setErrorMessage: Dispatch>; +}; -function RobotList({ robots }: Props) { +function RobotList({ apiUrl, robots, token, setErrorMessage }: Props) { const [expandedRobots, setExpandedRobots] = useState( {} ); @@ -17,15 +28,89 @@ function RobotList({ robots }: Props) { })); } + // Move or stop individual robot + async function controlSingleRobot( + event: MouseEvent, + robotId: number, + robotStatus: Robot["status"] + ) { + const isRobotMoving = robotStatus === "moving"; + const button = event.currentTarget; + button.disabled = true; // prevent spamming + + try { + // Make button clickable again after 1 second + setTimeout(() => { + button.disabled = false; + }, 1000); + + const response = await fetch( + `${apiUrl}/robots/${robotId}/${ + isRobotMoving ? "stop" : "move" + }`, + { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + } + ); + + if (!response.ok) { + const errorData: ErrorResponse = await response.json(); + throw new Error( + errorData.message || + `Failed to ${ + isRobotMoving ? "stop" : "start" + } robot ID ${robotId}.` + ); + } + + const data: SimulationResponse = await response.json(); + console.log(data.message); + } catch (error) { + console.error( + `Error ${ + isRobotMoving ? "stopping" : "starting" + } robot ID ${robotId}: `, + error + ); + + if (error instanceof Error) { + setErrorMessage(error.message); + } else { + setErrorMessage("An unexpected error occurred."); + } + } + } + return (
    - {robots.map((robot) => { + {robots?.map((robot) => { const isExpanded = expandedRobots[robot?.id]; return (
  • {robot?.name}

    + {/* Move/stop individual robot */} + + + {/* Movement status */}

    Status:{" "} @@ -33,12 +118,14 @@ function RobotList({ robots }: Props) {

    + {/* Current position */}

    Position:

    • Lat: {robot?.lat}
    • Lon: {robot?.lon}
    + {/* Expand position log */} + {/* Position log/history */}
    >; setErrorMessage: Dispatch>; + setRobots: Dispatch>; }; -function Sidebar({ errorMessage, robots, token, setErrorMessage }: Props) { +function Sidebar({ + activeSimulation, + errorMessage, + robots, + token, + setActiveSimulation, + setErrorMessage, + setRobots, +}: Props) { const [isAddingRobot, setIsAddingRobot] = useState(false); function handleAddClick() { @@ -35,13 +46,15 @@ function Sidebar({ errorMessage, robots, token, setErrorMessage }: Props) {
    - {isAddingRobot && ( + {isAddingRobot && ( )} @@ -50,12 +63,19 @@ function Sidebar({ errorMessage, robots, token, setErrorMessage }: Props) { )} - + ); } diff --git a/frontend/src/components/SimulationActions.tsx b/frontend/src/components/SimulationActions.tsx index c91b933..0bfca3f 100644 --- a/frontend/src/components/SimulationActions.tsx +++ b/frontend/src/components/SimulationActions.tsx @@ -1,19 +1,24 @@ -import { useState, type Dispatch, type SetStateAction } from "react"; +import { type Dispatch, type SetStateAction } from "react"; import type { ErrorResponse } from "../types/error"; +import type { SimulationResponse } from "../types/robot"; type Props = { + activeSimulation: boolean; apiUrl: string; token: string | null; + setActiveSimulation: Dispatch>; setErrorMessage: Dispatch>; }; -function SimulationActions({ apiUrl, token, setErrorMessage }: Props) { - const [isSimulationActive, setIsSimulationActive] = - useState(false); - - // TODO type responses +function SimulationActions({ + activeSimulation, + apiUrl, + token, + setActiveSimulation, + setErrorMessage, +}: Props) { async function handleStartAllRobots() { - setIsSimulationActive(true); + setActiveSimulation(true); try { const response = await fetch(`${apiUrl}/robots/move`, { @@ -31,7 +36,8 @@ function SimulationActions({ apiUrl, token, setErrorMessage }: Props) { ); } - console.log("All robots set moving."); + const data: SimulationResponse = await response.json(); + console.log(data.message); } catch (error) { console.error("Error starting robots:", error); @@ -41,12 +47,12 @@ function SimulationActions({ apiUrl, token, setErrorMessage }: Props) { setErrorMessage("An unexpected error occurred."); } - setIsSimulationActive(false); + setActiveSimulation(false); } } async function handleStopAllRobots() { - setIsSimulationActive(false); + setActiveSimulation(false); try { const response = await fetch(`${apiUrl}/robots/stop`, { @@ -64,7 +70,8 @@ function SimulationActions({ apiUrl, token, setErrorMessage }: Props) { ); } - console.log("All robots set idle."); + const data: SimulationResponse = await response.json(); + console.log(data.message); } catch (error) { console.error("Error stopping robots:", error); @@ -74,7 +81,7 @@ function SimulationActions({ apiUrl, token, setErrorMessage }: Props) { setErrorMessage("An unexpected error occurred."); } - setIsSimulationActive(true); + setActiveSimulation(true); } } @@ -83,7 +90,7 @@ function SimulationActions({ apiUrl, token, setErrorMessage }: Props) { @@ -91,7 +98,7 @@ function SimulationActions({ apiUrl, token, setErrorMessage }: Props) { diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 088fee2..b1b38e4 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -14,6 +14,8 @@ import type { Robot, RobotsResponse } from "../types/robot"; function Dashboard() { const [errorMessage, setErrorMessage] = useState(""); const [isLoading, setIsLoading] = useState(true); + const [isSimulationActive, setIsSimulationActive] = + useState(false); const [robots, setRobots] = useState([]); const navigate = useNavigate(); @@ -25,6 +27,7 @@ function Dashboard() { async function handleLogout() { localStorage.removeItem("token-robot-tracker"); localStorage.removeItem("user"); + // setIsSimulationActive(false); navigate("/login", { replace: true }); } @@ -57,7 +60,9 @@ function Dashboard() { } const data: RobotsResponse = await response.json(); - setRobots(data.data); + + setRobots(data.robots); + setIsSimulationActive(data.simulationRunning); } catch (error) { console.error("Failed to load the robots:", error); @@ -66,7 +71,6 @@ function Dashboard() { } else { setErrorMessage("An unexpected error occurred."); } - } finally { setIsLoading(false); } @@ -78,7 +82,7 @@ function Dashboard() { const socket = io(API_URL); // Listen for real-time robot updates - socket.on("robots_update", (updatedRobots) => { + socket.on("robots_update", ({ updatedRobots }) => { setRobots(updatedRobots); }); @@ -95,10 +99,13 @@ function Dashboard() {
    ); diff --git a/frontend/src/styles/index.css b/frontend/src/styles/index.css index dcc2590..bd4eacd 100644 --- a/frontend/src/styles/index.css +++ b/frontend/src/styles/index.css @@ -20,25 +20,17 @@ --text-small: 0.9rem; box-sizing: border-box; + + background-color: #fff; + color: #213547; + font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; line-height: 1.5; font-weight: 400; - - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; - font-synthesis: none; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; - - @media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #fff; - } - } } body { diff --git a/frontend/src/types/robot.ts b/frontend/src/types/robot.ts index 965afea..d9b2e7d 100644 --- a/frontend/src/types/robot.ts +++ b/frontend/src/types/robot.ts @@ -1,14 +1,16 @@ export type RobotPosition = { - lat: number; - lon: number; + lat: string; + lon: string; }; +export type RobotStatus = "idle" | "moving"; + export type Robot = { id: number; name: string; - status: "idle" | "moving"; - lat: number; - lon: number; + status: RobotStatus; + lat: string; + lon: string; robot_positions: RobotPosition[]; created_at: string; updated_at: string; @@ -16,7 +18,8 @@ export type Robot = { export type RobotsResponse = { source: "cache" | "database"; - data: Robot[]; + robots: Robot[]; + simulationRunning: boolean; }; export type CreateRobotResponse = { @@ -24,3 +27,11 @@ export type CreateRobotResponse = { robot: Robot; }; +export type RobotsUpdateBroadcast = { + updatedRobots: Robot[]; +}; + +export type SimulationResponse = { + message: string; + status?: RobotStatus; +}; -- cgit v1.2.3