diff --git a/.gitignore b/.gitignore index 9bfad9c..22e6c72 100644 --- a/.gitignore +++ b/.gitignore @@ -73,3 +73,4 @@ dashboard/pages/test.py dashboard/sidebar_test.py dashboard/assets/responsive-sidebar.css certs/ +sync.ffs_db diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json index 96a63f0..ec436ac 100644 --- a/dashboard/package-lock.json +++ b/dashboard/package-lock.json @@ -13,8 +13,10 @@ "@syncfusion/ej2-react-grids": "^29.2.11", "@syncfusion/ej2-react-schedule": "^29.2.10", "cldr-data": "^36.0.4", + "lucide-react": "^0.522.0", "react": "^19.1.0", - "react-dom": "^19.1.0" + "react-dom": "^19.1.0", + "react-router-dom": "^7.6.2" }, "devDependencies": { "@eslint/js": "^9.25.0", @@ -23,6 +25,7 @@ "@tailwindcss/typography": "^0.5.16", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", + "@types/react-router-dom": "^5.3.3", "@typescript-eslint/eslint-plugin": "^8.34.1", "@typescript-eslint/parser": "^8.34.1", "@vitejs/plugin-react": "^4.4.1", @@ -1985,6 +1988,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/history": { + "version": "4.7.11", + "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz", + "integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -2012,6 +2022,29 @@ "@types/react": "^19.0.0" } }, + "node_modules/@types/react-router": { + "version": "5.1.20", + "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz", + "integrity": "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*" + } + }, + "node_modules/@types/react-router-dom": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz", + "integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*", + "@types/react-router": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.34.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.34.1.tgz", @@ -3038,6 +3071,15 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/cosmiconfig": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", @@ -5357,6 +5399,15 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "0.522.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.522.0.tgz", + "integrity": "sha512-jnJbw974yZ7rQHHEFKJOlWAefG3ATSCZHANZxIdx8Rk/16siuwjgA4fBULpXEAWx/RlTs3FzmKW/udWUuO0aRw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -6267,6 +6318,44 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.6.2.tgz", + "integrity": "sha512-U7Nv3y+bMimgWjhlT5CRdzHPu2/KVmqPwKUCChW8en5P3znxUqwlYFlbmyj8Rgp1SF6zs5X4+77kBVknkg6a0w==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.6.2.tgz", + "integrity": "sha512-Q8zb6VlTbdYKK5JJBLQEN06oTUa/RAbG/oQS1auK1I0TbJOXktqm+QENEVJU6QvWynlXPRBXI3fiOQcSEA78rA==", + "license": "MIT", + "dependencies": { + "react-router": "7.6.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -6521,6 +6610,12 @@ "node": ">=10" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", diff --git a/dashboard/package.json b/dashboard/package.json index 1ba0b0e..1a589cd 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -15,8 +15,10 @@ "@syncfusion/ej2-react-grids": "^29.2.11", "@syncfusion/ej2-react-schedule": "^29.2.10", "cldr-data": "^36.0.4", + "lucide-react": "^0.522.0", "react": "^19.1.0", - "react-dom": "^19.1.0" + "react-dom": "^19.1.0", + "react-router-dom": "^7.6.2" }, "devDependencies": { "@eslint/js": "^9.25.0", @@ -25,6 +27,7 @@ "@tailwindcss/typography": "^0.5.16", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", + "@types/react-router-dom": "^5.3.3", "@typescript-eslint/eslint-plugin": "^8.34.1", "@typescript-eslint/parser": "^8.34.1", "@vitejs/plugin-react": "^4.4.1", diff --git a/dashboard/src/App.tsx b/dashboard/src/App.tsx index fa2f587..d03e342 100644 --- a/dashboard/src/App.tsx +++ b/dashboard/src/App.tsx @@ -1,113 +1,156 @@ -// import 'react-app-polyfill/ie11'; // optional, falls benötigt +import React, { useState } from 'react'; +import { BrowserRouter as Router, Routes, Route, Link, Outlet } from 'react-router-dom'; +import logo from './assets/logo.png'; import './App.css'; -import React from 'react'; + +// Lucide Icons importieren import { - ScheduleComponent, - Day, - Week, - WorkWeek, - Month, - Agenda, - TimelineViews, - TimelineMonth, - Inject, - ViewsDirective, - ViewDirective, - ResourcesDirective, - ResourceDirective, -} from '@syncfusion/ej2-react-schedule'; -import { L10n, loadCldr, setCulture } from '@syncfusion/ej2-base'; -import * as de from 'cldr-data/main/de/ca-gregorian.json'; -import * as numbers from 'cldr-data/main/de/numbers.json'; -import * as timeZoneNames from 'cldr-data/main/de/timeZoneNames.json'; -import * as numberingSystems from 'cldr-data/supplemental/numberingSystems.json'; + LayoutDashboard, + Calendar, + Boxes, + Users, + UserSquare, + Image, + User, + Settings, +} from 'lucide-react'; -// CLDR-Daten laden -loadCldr( - (de as unknown as { default: object }).default, - (numbers as unknown as { default: object }).default, - (timeZoneNames as unknown as { default: object }).default, - (numberingSystems as unknown as { default: object }).default -); - -// Deutsche Lokalisierung für den Scheduler -L10n.load({ - de: { - schedule: { - day: 'Tag', - week: 'Woche', - workWeek: 'Arbeitswoche', - month: 'Monat', - agenda: 'Agenda', - today: 'Heute', - noEvents: 'Keine Termine', - }, - }, -}); - -// Kultur setzen -setCulture('de'); - -// Ressourcen-Daten -const resources = [ - { text: 'Raum A', id: 1, color: '#1aaa55' }, - { text: 'Raum B', id: 2, color: '#357cd2' }, - { text: 'Raum C', id: 3, color: '#7fa900' }, +const sidebarItems = [ + { name: 'Dashboard', path: '/', icon: LayoutDashboard }, + { name: 'Termine', path: '/termine', icon: Calendar }, + { name: 'Ressourcen', path: '/ressourcen', icon: Boxes }, + { name: 'Infoscreens', path: '/Infoscreens', icon: Users }, + { name: 'Gruppen', path: '/gruppen', icon: UserSquare }, + { name: 'Medien', path: '/medien', icon: Image }, + { name: 'Benutzer', path: '/benutzer', icon: User }, + { name: 'Einstellungen', path: '/einstellungen', icon: Settings }, ]; -// Dummy-Termine generieren -const now = new Date(); -const appointments = Array.from({ length: 10 }).map((_, i) => { - const dayOffset = Math.floor(i * 1.4); // verteilt auf 14 Tage - const start = new Date(now); - start.setDate(now.getDate() + dayOffset); - start.setHours(9 + (i % 4), 0, 0, 0); - const end = new Date(start); - end.setHours(start.getHours() + 1); +const Layout: React.FC = () => { + const [collapsed, setCollapsed] = useState(false); - return { - Id: i + 1, - Subject: `Termin ${i + 1}`, - StartTime: start, - EndTime: end, - ResourceId: (i % 3) + 1, - Location: resources[i % 3].text, - }; -}); - -const App: React.FC = () => { return ( -
-

Infoscreen Kalendersteuerung

- + {/* Sidebar */} +
+ + + + {/* Main Content */} +
+ {/* Header */} +
+ Logo + Infoscreen-Management + + [Organisationsname] + +
+
+ +
+
); }; +const App: React.FC = () => ( + + + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + +); + export default App; + +// Dummy Components (können in eigene Dateien ausgelagert werden) +import Dashboard from './dashboard'; +import Appointments from './appointments'; +import Ressourcen from './ressourcen'; +import Infoscreens from './clients'; +import Gruppen from './gruppen'; +import Medien from './medien'; +import Benutzer from './benutzer'; +import Einstellungen from './einstellungen'; diff --git a/dashboard/src/apiClients.ts b/dashboard/src/apiClients.ts new file mode 100644 index 0000000..079d77e --- /dev/null +++ b/dashboard/src/apiClients.ts @@ -0,0 +1,17 @@ +// Funktion zum Laden der Clients von der API + +export interface Client { + uuid: string; + location: string; + hardware_hash: string; + ip_address: string; + last_alive: string | null; +} + +export async function fetchClients(): Promise { + const response = await fetch('/api/clients'); + if (!response.ok) { + throw new Error('Fehler beim Laden der Clients'); + } + return await response.json(); +} diff --git a/dashboard/src/appointments.tsx b/dashboard/src/appointments.tsx new file mode 100644 index 0000000..52bdfad --- /dev/null +++ b/dashboard/src/appointments.tsx @@ -0,0 +1,106 @@ +import React from 'react'; +import { + ScheduleComponent, + Day, + Week, + WorkWeek, + Month, + Agenda, + TimelineViews, + TimelineMonth, + Inject, + ViewsDirective, + ViewDirective, + ResourcesDirective, + ResourceDirective, +} from '@syncfusion/ej2-react-schedule'; +import { L10n, loadCldr, setCulture } from '@syncfusion/ej2-base'; +import * as de from 'cldr-data/main/de/ca-gregorian.json'; +import * as numbers from 'cldr-data/main/de/numbers.json'; +import * as timeZoneNames from 'cldr-data/main/de/timeZoneNames.json'; +import * as numberingSystems from 'cldr-data/supplemental/numberingSystems.json'; + +// CLDR-Daten laden +loadCldr( + (de as unknown as { default: object }).default, + (numbers as unknown as { default: object }).default, + (timeZoneNames as unknown as { default: object }).default, + (numberingSystems as unknown as { default: object }).default +); + +// Deutsche Lokalisierung für den Scheduler +L10n.load({ + de: { + schedule: { + day: 'Tag', + week: 'Woche', + workWeek: 'Arbeitswoche', + month: 'Monat', + agenda: 'Agenda', + today: 'Heute', + noEvents: 'Keine Termine', + }, + }, +}); + +// Kultur setzen +setCulture('de'); + +// Ressourcen-Daten +const resources = [ + { text: 'Raum A', id: 1, color: '#1aaa55' }, + { text: 'Raum B', id: 2, color: '#357cd2' }, + { text: 'Raum C', id: 3, color: '#7fa900' }, +]; + +// Dummy-Termine generieren +const now = new Date(); +const appointments = Array.from({ length: 10 }).map((_, i) => { + const dayOffset = Math.floor(i * 1.4); // verteilt auf 14 Tage + const start = new Date(now); + start.setDate(now.getDate() + dayOffset); + start.setHours(9 + (i % 4), 0, 0, 0); + const end = new Date(start); + end.setHours(start.getHours() + 1); + + return { + Id: i + 1, + Subject: `Termin ${i + 1}`, + StartTime: start, + EndTime: end, + ResourceId: (i % 3) + 1, + Location: resources[i % 3].text, + }; +}); + +const Appointments: React.FC = () => ( + + + + + + + + + + + + +); + +export default Appointments; diff --git a/dashboard/src/assets/TAA_Logo.png b/dashboard/src/assets/TAA_Logo.png new file mode 100644 index 0000000..698eb92 Binary files /dev/null and b/dashboard/src/assets/TAA_Logo.png differ diff --git a/dashboard/src/assets/logo.png b/dashboard/src/assets/logo.png new file mode 100644 index 0000000..0ffb524 Binary files /dev/null and b/dashboard/src/assets/logo.png differ diff --git a/dashboard/src/benutzer.tsx b/dashboard/src/benutzer.tsx new file mode 100644 index 0000000..6241b4a --- /dev/null +++ b/dashboard/src/benutzer.tsx @@ -0,0 +1,8 @@ +import React from 'react'; +const Benutzer: React.FC = () => ( +
+

Benutzer

+

Willkommen im Infoscreen-Management Benutzer.

+
+); +export default Benutzer; diff --git a/dashboard/src/clients.tsx b/dashboard/src/clients.tsx new file mode 100644 index 0000000..a612bd6 --- /dev/null +++ b/dashboard/src/clients.tsx @@ -0,0 +1,8 @@ +import React from 'react'; +const Infoscreens: React.FC = () => ( +
+

Infoscreens

+

Willkommen im Infoscreen-Management Infoscreens.

+
+); +export default Infoscreens; diff --git a/dashboard/src/dashboard.tsx b/dashboard/src/dashboard.tsx new file mode 100644 index 0000000..44bd69e --- /dev/null +++ b/dashboard/src/dashboard.tsx @@ -0,0 +1,46 @@ +import React, { useEffect, useState } from 'react'; +import { fetchClients } from './apiClients'; +import type { Client } from './apiClients'; + +const Dashboard: React.FC = () => { + const [clients, setClients] = useState([]); + + useEffect(() => { + fetchClients().then(setClients).catch(console.error); + }, []); + + return ( +
+
+

Dashboard

+

Willkommen im Infoscreen-Management Dashboard.

+
+

Clients

+
+ {clients.map(client => ( +
+

{client.location || 'Unbekannter Standort'}

+ {`Screenshot (e.currentTarget.style.display = 'none')} + /> +
+ IP: {client.ip_address} +
+
+ Letztes Lebenszeichen:{' '} + {client.last_alive ? new Date(client.last_alive).toLocaleString() : '-'} +
+
+ ))} + {clients.length === 0 && ( +
Keine Clients gefunden.
+ )} +
+
+ ); +}; + +export default Dashboard; diff --git a/dashboard/src/einstellungen.tsx b/dashboard/src/einstellungen.tsx new file mode 100644 index 0000000..c8ffcec --- /dev/null +++ b/dashboard/src/einstellungen.tsx @@ -0,0 +1,8 @@ +import React from 'react'; +const Einstellungen: React.FC = () => ( +
+

Einstellungen

+

Willkommen im Infoscreen-Management Einstellungen.

+
+); +export default Einstellungen; diff --git a/dashboard/src/gruppen.tsx b/dashboard/src/gruppen.tsx new file mode 100644 index 0000000..91a516c --- /dev/null +++ b/dashboard/src/gruppen.tsx @@ -0,0 +1,8 @@ +import React from 'react'; +const Gruppen: React.FC = () => ( +
+

Gruppen

+

Willkommen im Infoscreen-Management Gruppen.

+
+); +export default Gruppen; diff --git a/dashboard/src/medien.tsx b/dashboard/src/medien.tsx new file mode 100644 index 0000000..15d96a4 --- /dev/null +++ b/dashboard/src/medien.tsx @@ -0,0 +1,8 @@ +import React from 'react'; +const Medien: React.FC = () => ( +
+

Medien

+

Willkommen im Infoscreen-Management Medien.

+
+); +export default Medien; diff --git a/dashboard/src/ressourcen.tsx b/dashboard/src/ressourcen.tsx new file mode 100644 index 0000000..d0f687d --- /dev/null +++ b/dashboard/src/ressourcen.tsx @@ -0,0 +1,8 @@ +import React from 'react'; +const Ressourcen: React.FC = () => ( +
+

Ressourcen

+

Willkommen im Infoscreen-Management Ressourcen.

+
+); +export default Ressourcen; diff --git a/dashboard/src/termine.tsx b/dashboard/src/termine.tsx new file mode 100644 index 0000000..e69de29 diff --git a/dashboard/vite.config.ts b/dashboard/vite.config.ts index 8b0f57b..e04aa16 100644 --- a/dashboard/vite.config.ts +++ b/dashboard/vite.config.ts @@ -1,7 +1,13 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; // https://vite.dev/config/ export default defineConfig({ plugins: [react()], -}) + server: { + proxy: { + '/api': 'http://localhost:8000', + '/screenshots': 'http://localhost:8000', + }, + }, +}); diff --git a/nginx.conf b/nginx.conf index 49619af..75e266c 100644 --- a/nginx.conf +++ b/nginx.conf @@ -3,34 +3,21 @@ http { upstream dashboard { server 127.0.0.1:3000; } + upstream infoscreen_api { + server infoscreen-api:8000; + } server { listen 80; server_name _; - # Optional: HTTP auf HTTPS weiterleiten - # return 301 https://$host$request_uri; - - location / { - proxy_pass http://dashboard; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - } - server { - listen 443 ssl; - server_name _; - - ssl_certificate /etc/nginx/certs/dev.crt; - ssl_certificate_key /etc/nginx/certs/dev.key; - + # Leitet /api/ und /screenshots/ an den API-Server weiter location /api/ { - proxy_pass http://infoscreen-api:8000/api/; + proxy_pass http://infoscreen_api/api/; } location /screenshots/ { - proxy_pass http://infoscreen-api:8000/screenshots/; + proxy_pass http://infoscreen_api/screenshots/; } + # Alles andere geht ans Frontend location / { proxy_pass http://dashboard; proxy_set_header Host $host;