summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--backend/.dockerignore10
-rw-r--r--backend/.env.docker11
-rw-r--r--backend/.env.sample2
-rw-r--r--backend/Dockerfile43
-rw-r--r--backend/package.json2
-rw-r--r--backend/src/controllers/moveAllRobots.ts2
-rw-r--r--backend/src/controllers/moveRobot.ts2
-rw-r--r--backend/src/controllers/stopAllRobots.ts2
-rw-r--r--backend/src/controllers/stopRobot.ts2
-rw-r--r--database/init.sql188
-rw-r--r--docker-compose.yml62
-rw-r--r--frontend/.dockerignore10
-rw-r--r--frontend/.env.docker (renamed from frontend/.env)0
-rw-r--r--frontend/Dockerfile28
-rw-r--r--frontend/eslint.config.ts (renamed from frontend/eslint.config.js)0
-rw-r--r--frontend/nginx.conf27
-rw-r--r--frontend/src/components/ErrorBanner.tsx5
-rw-r--r--frontend/src/components/Sidebar.tsx8
-rw-r--r--frontend/src/pages/Login.tsx13
-rw-r--r--frontend/src/styles/login.css1
20 files changed, 401 insertions, 17 deletions
diff --git a/backend/.dockerignore b/backend/.dockerignore
new file mode 100644
index 0000000..9f8fe0e
--- /dev/null
+++ b/backend/.dockerignore
@@ -0,0 +1,10 @@
+**/.env
+**/.git
+**/.gitignore
+**/Dockerfile*
+**/node_modules
+**/npm-debug.log
+**/build
+**/dist
+LICENSE
+README.md \ No newline at end of file
diff --git a/backend/.env.docker b/backend/.env.docker
new file mode 100644
index 0000000..88fdf64
--- /dev/null
+++ b/backend/.env.docker
@@ -0,0 +1,11 @@
+DB_DATABASE=robot_tracker_db
+DB_HOST=postgres
+DB_PASSWORD=roboMaster123
+DB_PORT=5432
+DB_USER=robot_master
+JWT_SECRET="Secret_JWT-Secret_strinG"
+PORT=3000
+REDIS_HOST=redis
+REDIS_USER=default
+REDIS_PASSWORD=redispassword
+REDIS_PORT=6379 \ No newline at end of file
diff --git a/backend/.env.sample b/backend/.env.sample
index 027003e..3045036 100644
--- a/backend/.env.sample
+++ b/backend/.env.sample
@@ -5,7 +5,7 @@ DB_PORT=5432
DB_USER=db_user_name
JWT_SECRET="yourJWTstr1ng"
PORT=3000
-REDIS_HOST=einfac-redis-oder-url-zur-cloud
+REDIS_HOST=redis-oder-url-zur-cloud
REDIS_USER=default
REDIS_PASSWORD=redisPw
REDIS_PORT=6379 \ No newline at end of file
diff --git a/backend/Dockerfile b/backend/Dockerfile
new file mode 100644
index 0000000..47a96e6
--- /dev/null
+++ b/backend/Dockerfile
@@ -0,0 +1,43 @@
+# syntax=docker/dockerfile:1
+
+# BUILD STAGE
+FROM node:22-alpine AS builder
+
+WORKDIR /app
+
+COPY package*.json ./
+
+# Install all dependencies, dev dependencies needed for build
+RUN npm ci
+
+COPY . .
+
+# Build TypeScript
+RUN npm run build
+
+# PRODUCTION STAGE
+FROM node:22-alpine AS production
+
+WORKDIR /app
+
+COPY package*.json ./
+
+# Install only production dependencies
+RUN npm ci --omit=dev
+
+# Copy .env.docker to .env to silence Docker warning
+COPY .env.docker .env
+
+# Copy compiled code from builder stage
+COPY --from=builder /app/dist ./dist
+
+# Create non-root user for security
+RUN addgroup -g 1001 -S nodejs && \
+ adduser -S nodejs -u 1001 && \
+ chown -R nodejs:nodejs /app
+
+USER nodejs
+
+EXPOSE 3000
+
+CMD ["npm", "start"] \ No newline at end of file
diff --git a/backend/package.json b/backend/package.json
index 73f22a7..ff53313 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -8,7 +8,7 @@
"main": "dist/server.js",
"scripts": {
"dev": "tsx watch --env-file=.env src/server.ts",
- "start": "node dist/server.js",
+ "start": "node --env-file=.env dist/server.js",
"build": "tsc",
"type-check": "tsc --noEmit",
"test": "echo \"Error: no test specified\" && exit 1"
diff --git a/backend/src/controllers/moveAllRobots.ts b/backend/src/controllers/moveAllRobots.ts
index e4d1252..e7291b6 100644
--- a/backend/src/controllers/moveAllRobots.ts
+++ b/backend/src/controllers/moveAllRobots.ts
@@ -1,6 +1,6 @@
import { Request, Response } from "express";
import { Server } from "socket.io";
-import { setAllRobotsMoving } from "../simulation/robotMovementSimulator";
+import { setAllRobotsMoving } from "../simulation/robotMovementSimulator.js";
import { ErrorResponse } from "../types/error";
import { SimulationResponse } from "../types/robot";
diff --git a/backend/src/controllers/moveRobot.ts b/backend/src/controllers/moveRobot.ts
index 1b53f17..a26b56c 100644
--- a/backend/src/controllers/moveRobot.ts
+++ b/backend/src/controllers/moveRobot.ts
@@ -1,6 +1,6 @@
import { Request, Response } from "express";
import { Server } from "socket.io";
-import { setRobotMoving } from "../simulation/robotMovementSimulator";
+import { setRobotMoving } from "../simulation/robotMovementSimulator.js";
import { ErrorResponse } from "../types/error";
import { SimulationResponse } from "../types/robot";
diff --git a/backend/src/controllers/stopAllRobots.ts b/backend/src/controllers/stopAllRobots.ts
index 7218e66..a0680b2 100644
--- a/backend/src/controllers/stopAllRobots.ts
+++ b/backend/src/controllers/stopAllRobots.ts
@@ -1,5 +1,5 @@
import { Request, Response } from "express";
-import { setAllRobotsIdle } from "../simulation/robotMovementSimulator";
+import { setAllRobotsIdle } from "../simulation/robotMovementSimulator.js";
import { ErrorResponse } from "../types/error";
import { SimulationResponse } from "../types/robot";
diff --git a/backend/src/controllers/stopRobot.ts b/backend/src/controllers/stopRobot.ts
index d0d7c4f..af98329 100644
--- a/backend/src/controllers/stopRobot.ts
+++ b/backend/src/controllers/stopRobot.ts
@@ -1,5 +1,5 @@
import { Request, Response } from "express";
-import { setRobotIdle } from "../simulation/robotMovementSimulator";
+import { setRobotIdle } from "../simulation/robotMovementSimulator.js";
import { ErrorResponse } from "../types/error";
import { SimulationResponse } from "../types/robot";
diff --git a/database/init.sql b/database/init.sql
new file mode 100644
index 0000000..6b2c79e
--- /dev/null
+++ b/database/init.sql
@@ -0,0 +1,188 @@
+--
+-- PostgreSQL database dump
+--
+
+\restrict glJa3ruKbCYJ5e028FR51030DZ2dUgDjNP4bVEcrCnFu1K7MGF5Og3eeAMdRNGu
+
+-- Dumped from database version 18.1
+-- Dumped by pg_dump version 18.1
+
+SET statement_timeout = 0;
+SET lock_timeout = 0;
+SET idle_in_transaction_session_timeout = 0;
+SET transaction_timeout = 0;
+SET client_encoding = 'UTF8';
+SET standard_conforming_strings = on;
+SELECT pg_catalog.set_config('search_path', '', false);
+SET check_function_bodies = false;
+SET xmloption = content;
+SET client_min_messages = warning;
+SET row_security = off;
+
+SET default_tablespace = '';
+
+SET default_table_access_method = heap;
+
+--
+-- Name: robots; Type: TABLE; Schema: public; Owner: robot_master
+--
+
+CREATE TABLE public.robots (
+ id integer NOT NULL,
+ name character varying(100) NOT NULL,
+ status character varying(10) NOT NULL,
+ lat numeric(10,7) NOT NULL,
+ lon numeric(10,7) NOT NULL,
+ robot_positions jsonb DEFAULT '[]'::jsonb,
+ created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP,
+ updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP,
+ CONSTRAINT robots_status_check CHECK (((status)::text = ANY ((ARRAY['idle'::character varying, 'moving'::character varying])::text[])))
+);
+
+
+ALTER TABLE public.robots OWNER TO robot_master;
+
+--
+-- Name: robots_id_seq; Type: SEQUENCE; Schema: public; Owner: robot_master
+--
+
+CREATE SEQUENCE public.robots_id_seq
+ AS integer
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+
+ALTER SEQUENCE public.robots_id_seq OWNER TO robot_master;
+
+--
+-- Name: robots_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: robot_master
+--
+
+ALTER SEQUENCE public.robots_id_seq OWNED BY public.robots.id;
+
+
+--
+-- Name: users; Type: TABLE; Schema: public; Owner: robot_master
+--
+
+CREATE TABLE public.users (
+ id integer NOT NULL,
+ email character varying(255) NOT NULL,
+ password_hash text NOT NULL,
+ created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP
+);
+
+
+ALTER TABLE public.users OWNER TO robot_master;
+
+--
+-- Name: users_id_seq; Type: SEQUENCE; Schema: public; Owner: robot_master
+--
+
+CREATE SEQUENCE public.users_id_seq
+ AS integer
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+
+ALTER SEQUENCE public.users_id_seq OWNER TO robot_master;
+
+--
+-- Name: users_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: robot_master
+--
+
+ALTER SEQUENCE public.users_id_seq OWNED BY public.users.id;
+
+
+--
+-- Name: robots id; Type: DEFAULT; Schema: public; Owner: robot_master
+--
+
+ALTER TABLE ONLY public.robots ALTER COLUMN id SET DEFAULT nextval('public.robots_id_seq'::regclass);
+
+
+--
+-- Name: users id; Type: DEFAULT; Schema: public; Owner: robot_master
+--
+
+ALTER TABLE ONLY public.users ALTER COLUMN id SET DEFAULT nextval('public.users_id_seq'::regclass);
+
+
+--
+-- Data for Name: robots; Type: TABLE DATA; Schema: public; Owner: robot_master
+--
+
+COPY public.robots (id, name, status, lat, lon, robot_positions, created_at, updated_at) FROM stdin;
+1 R2-D2 idle 51.3408630 12.3759190 [] 2026-01-09 22:04:04.983244 2026-01-09 22:04:04.983244
+2 Wall-E idle 51.3408630 12.3759190 [] 2026-01-09 22:04:04.983244 2026-01-09 22:04:04.983244
+3 Bender idle 51.3408630 12.3759190 [] 2026-01-09 22:04:04.983244 2026-01-09 22:04:04.983244
+4 Marvin idle 51.3408630 12.3759190 [] 2026-01-09 22:04:04.983244 2026-01-09 22:04:04.983244
+\.
+
+
+--
+-- Data for Name: users; Type: TABLE DATA; Schema: public; Owner: robot_master
+--
+
+COPY public.users (id, email, password_hash, created_at) FROM stdin;
+1 admin@test.com $2b$10$nPoFqcBSa4wrfUsAXDTeyeuxXN2KMFgbBcbttI0QG/KPq3JhtQH5K 2026-01-09 21:54:32.719188
+\.
+
+
+--
+-- Name: robots_id_seq; Type: SEQUENCE SET; Schema: public; Owner: robot_master
+--
+
+SELECT pg_catalog.setval('public.robots_id_seq', 4, true);
+
+
+--
+-- Name: users_id_seq; Type: SEQUENCE SET; Schema: public; Owner: robot_master
+--
+
+SELECT pg_catalog.setval('public.users_id_seq', 1, true);
+
+
+--
+-- Name: robots robots_pkey; Type: CONSTRAINT; Schema: public; Owner: robot_master
+--
+
+ALTER TABLE ONLY public.robots
+ ADD CONSTRAINT robots_pkey PRIMARY KEY (id);
+
+
+--
+-- Name: users users_email_key; Type: CONSTRAINT; Schema: public; Owner: robot_master
+--
+
+ALTER TABLE ONLY public.users
+ ADD CONSTRAINT users_email_key UNIQUE (email);
+
+
+--
+-- Name: users users_pkey; Type: CONSTRAINT; Schema: public; Owner: robot_master
+--
+
+ALTER TABLE ONLY public.users
+ ADD CONSTRAINT users_pkey PRIMARY KEY (id);
+
+
+--
+-- Name: SCHEMA public; Type: ACL; Schema: -; Owner: pg_database_owner
+--
+
+GRANT ALL ON SCHEMA public TO robot_master;
+
+
+--
+-- PostgreSQL database dump complete
+--
+
+\unrestrict glJa3ruKbCYJ5e028FR51030DZ2dUgDjNP4bVEcrCnFu1K7MGF5Og3eeAMdRNGu
+
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..4605e66
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,62 @@
+services:
+ # PostgreSQL Database
+ postgres:
+ image: postgres:18-alpine
+ container_name: robot-tracker-db
+ restart: unless-stopped
+ environment:
+ POSTGRES_DB: robot_tracker_db
+ POSTGRES_USER: robot_master
+ POSTGRES_PASSWORD: roboMaster123
+ ports:
+ - "5432:5432"
+ volumes:
+ - postgres_data:/var/lib/postgresql/data
+ - ./database/init.sql:/docker-entrypoint-initdb.d/init.sql
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U robot_master -d robot_tracker_db"]
+ interval: 10s
+ timeout: 5s
+ retries: 5
+
+ # Redis Cache
+ redis:
+ image: redis:7-alpine
+ container_name: robot-tracker-redis
+ restart: unless-stopped
+ command: redis-server --requirepass redispassword
+ ports:
+ - "6379:6379"
+ healthcheck:
+ test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
+ interval: 10s
+ timeout: 5s
+ retries: 5
+
+ # Backend API
+ backend:
+ build: ./backend
+ container_name: robot-tracker-backend
+ restart: unless-stopped
+ env_file:
+ - ./backend/.env.docker
+ ports:
+ - "3000:3000"
+ depends_on:
+ postgres:
+ condition: service_healthy
+ redis:
+ condition: service_healthy
+
+ # Frontend
+ frontend:
+ build: ./frontend
+ container_name: robot-tracker-frontend
+ restart: unless-stopped
+ ports:
+ - "5173:80"
+ depends_on:
+ - backend
+
+volumes:
+ postgres_data: \ No newline at end of file
diff --git a/frontend/.dockerignore b/frontend/.dockerignore
new file mode 100644
index 0000000..9f8fe0e
--- /dev/null
+++ b/frontend/.dockerignore
@@ -0,0 +1,10 @@
+**/.env
+**/.git
+**/.gitignore
+**/Dockerfile*
+**/node_modules
+**/npm-debug.log
+**/build
+**/dist
+LICENSE
+README.md \ No newline at end of file
diff --git a/frontend/.env b/frontend/.env.docker
index cd41370..cd41370 100644
--- a/frontend/.env
+++ b/frontend/.env.docker
diff --git a/frontend/Dockerfile b/frontend/Dockerfile
new file mode 100644
index 0000000..791cd87
--- /dev/null
+++ b/frontend/Dockerfile
@@ -0,0 +1,28 @@
+# syntax=docker/dockerfile:1
+
+# BUILD STAGE
+FROM node:22-alpine AS builder
+
+WORKDIR /app
+
+COPY package*.json ./
+
+RUN npm ci
+
+# Copy .env.docker as .env for Vite
+COPY .env.docker .env
+
+COPY . .
+
+# Compile TypeScript & create optimized production build
+RUN npm run build
+
+# PRODUCTION STAGE
+FROM nginx:alpine AS production
+
+# Copy production build files to nginx
+COPY --from=builder /app/dist /usr/share/nginx/html
+
+COPY nginx.conf /etc/nginx/conf.d/default.conf
+
+EXPOSE 80 \ No newline at end of file
diff --git a/frontend/eslint.config.js b/frontend/eslint.config.ts
index 6343bef..6343bef 100644
--- a/frontend/eslint.config.js
+++ b/frontend/eslint.config.ts
diff --git a/frontend/nginx.conf b/frontend/nginx.conf
new file mode 100644
index 0000000..151afe8
--- /dev/null
+++ b/frontend/nginx.conf
@@ -0,0 +1,27 @@
+# For Docker Compose
+server {
+ listen 80;
+ server_name localhost;
+ root /usr/share/nginx/html;
+ index index.html;
+
+ # Enable gzip compression
+ gzip on;
+ gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
+
+ # SPA routing - send all requests to index.html
+ location / {
+ try_files $uri $uri/ /index.html;
+ }
+
+ # Cache static assets
+ location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
+ expires 1y;
+ add_header Cache-Control "public, immutable";
+ }
+
+ # Security headers
+ add_header X-Frame-Options "SAMEORIGIN" always;
+ add_header X-Content-Type-Options "nosniff" always;
+ add_header X-XSS-Protection "1; mode=block" always;
+} \ No newline at end of file
diff --git a/frontend/src/components/ErrorBanner.tsx b/frontend/src/components/ErrorBanner.tsx
new file mode 100644
index 0000000..fdb3bf1
--- /dev/null
+++ b/frontend/src/components/ErrorBanner.tsx
@@ -0,0 +1,5 @@
+function ErrorBanner({ message }: { message: string }) {
+ return <div className="error-message">{message}</div>;
+}
+
+export default ErrorBanner;
diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx
index 3760d4e..bdfadad 100644
--- a/frontend/src/components/Sidebar.tsx
+++ b/frontend/src/components/Sidebar.tsx
@@ -4,6 +4,7 @@ import "../styles/button.css";
import "../styles/sidebar.css";
import type { Robot } from "../types/robot";
import AddRobotForm from "./AddRobotForm";
+import ErrorBanner from "./ErrorBanner";
import RobotList from "./RobotList";
import SimulationActions from "./SimulationActions";
@@ -58,10 +59,8 @@ function Sidebar({
/>
)}
- {errorMessage && (
- <div className="error-message">{errorMessage}</div>
- )}
-
+ {errorMessage && <ErrorBanner message={errorMessage} />}
+
<SimulationActions
activeSimulation={activeSimulation}
apiUrl={API_URL}
@@ -81,4 +80,3 @@ function Sidebar({
}
export default Sidebar;
-
diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx
index 11b7c11..463af8e 100644
--- a/frontend/src/pages/Login.tsx
+++ b/frontend/src/pages/Login.tsx
@@ -1,6 +1,8 @@
import { useEffect, useState, type ChangeEvent, type FormEvent } from "react";
import { useNavigate } from "react-router-dom";
import { FadeLoader } from "react-spinners";
+import mapBackground from "../assets/map-blurred.png";
+import ErrorBanner from "../components/ErrorBanner";
import Logo from "../components/Logo";
import API_URL from "../config";
import "../styles/button.css";
@@ -84,7 +86,10 @@ function Login() {
}, []);
return (
- <div className="login-page">
+ <div
+ className="login-page"
+ style={{ backgroundImage: `url(${mapBackground})` }}
+ >
<div className="login-card">
<Logo />
<p className="subtitle">🤖 Please log in to use the app 🤖</p>
@@ -119,10 +124,8 @@ function Login() {
/>
</div>
- {errorMessage && (
- <div className="error-message">{errorMessage}</div>
- )}
-
+ {errorMessage && <ErrorBanner message={errorMessage} />}
+
<button
className="btn btn-start"
disabled={isLoading}
diff --git a/frontend/src/styles/login.css b/frontend/src/styles/login.css
index 4592016..1da01ea 100644
--- a/frontend/src/styles/login.css
+++ b/frontend/src/styles/login.css
@@ -1,5 +1,4 @@
.login-page {
- background-image: url("../src/assets/map-blurred.png");
background-position: center;
background-repeat: no-repeat;
background-size: cover;