From 4e525e4bae3efe6e9640edc1705a848d7911ef3c Mon Sep 17 00:00:00 2001 From: olaf Date: Thu, 17 Jul 2025 06:31:50 +0000 Subject: [PATCH] Automatic detect of clients --- dashboard/src/App.tsx | 59 ++++++++++- dashboard/src/SetupMode.tsx | 106 +++++++++++++++++++ dashboard/src/apiClients.ts | 37 +++++-- dashboard/src/clients.tsx | 70 ++++++++++-- dashboard/src/components/SetupModeButton.tsx | 19 ++++ dashboard/src/dashboard.tsx | 16 +-- dashboard/src/infoscreen_groups.tsx | 9 +- server/routes/clients.py | 63 +++++++++-- server/routes/setup.py | 21 ++++ server/wsgi.py | 3 +- 10 files changed, 367 insertions(+), 36 deletions(-) create mode 100644 dashboard/src/SetupMode.tsx create mode 100644 dashboard/src/components/SetupModeButton.tsx create mode 100644 server/routes/setup.py diff --git a/dashboard/src/App.tsx b/dashboard/src/App.tsx index f34ba82..7cc1929 100644 --- a/dashboard/src/App.tsx +++ b/dashboard/src/App.tsx @@ -14,6 +14,7 @@ import { Monitor, MonitorDotIcon, LogOut, + Wrench, } from 'lucide-react'; import { ToastProvider } from './components/ToastProvider'; @@ -22,7 +23,8 @@ const sidebarItems = [ { name: 'Termine', path: '/termine', icon: Calendar }, { name: 'Ressourcen', path: '/ressourcen', icon: Boxes }, { name: 'Raumgruppen', path: '/infoscr_groups', icon: MonitorDotIcon }, - { name: 'Infoscreens', path: '/Infoscreens', icon: Monitor }, + { name: 'Infoscreens', path: '/clients', icon: Monitor }, + { name: 'Erweiterungsmodus', path: '/setup', icon: Wrench }, { name: 'Medien', path: '/medien', icon: Image }, { name: 'Benutzer', path: '/benutzer', icon: User }, { name: 'Einstellungen', path: '/einstellungen', icon: Settings }, @@ -119,26 +121,72 @@ const Layout: React.FC = () => { ); }; -const App: React.FC = () => ( - +import { useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { fetchClientsWithoutDescription } from './apiClients'; + +// ENV aus .env holen (Platzhalter, im echten Projekt über process.env oder API) +const ENV = import.meta.env.VITE_ENV || 'development'; + +function useLoginCheck() { + const [isLoggedIn, setIsLoggedIn] = useState(false); + + useEffect(() => { + if (ENV === 'development') { + setIsLoggedIn(true); // Im Development immer eingeloggt + console.log('[Login] Development-Modus: User automatisch eingeloggt'); + return; + } + // Hier echte Loginlogik einbauen (z.B. Token prüfen) + // setIsLoggedIn(...) + // console.log('[Login] Produktiv-Modus: Login-Check ausgeführt'); + }, []); + + return isLoggedIn; +} + +const App: React.FC = () => { + const navigate = useNavigate(); + const isLoggedIn = useLoginCheck(); + + useEffect(() => { + if (!isLoggedIn) return; + fetchClientsWithoutDescription().then(list => { + if (list.length > 0) { + console.log('[Navigation] Weiterleitung zu /clients wegen fehlender Beschreibung'); + navigate('/clients'); + } else { + console.log('[Navigation] Dashboard wird angezeigt, alle Clients haben Beschreibung'); + } + }); + }, [isLoggedIn, navigate]); + + return ( }> } /> } /> } /> - } /> } /> } /> } /> } /> + } /> + } /> + ); +}; + +const AppWrapper: React.FC = () => ( + + ); -export default App; +export default AppWrapper; // Dummy Components (können in eigene Dateien ausgelagert werden) import Dashboard from './dashboard'; @@ -149,3 +197,4 @@ import Infoscreen_groups from './infoscreen_groups'; import Media from './media'; import Benutzer from './benutzer'; import Einstellungen from './einstellungen'; +import SetupMode from './SetupMode'; diff --git a/dashboard/src/SetupMode.tsx b/dashboard/src/SetupMode.tsx new file mode 100644 index 0000000..7a97e71 --- /dev/null +++ b/dashboard/src/SetupMode.tsx @@ -0,0 +1,106 @@ +import React, { useEffect, useState } from 'react'; +import { fetchClientsWithoutDescription, setClientDescription } from './apiClients'; +import { ButtonComponent } from '@syncfusion/ej2-react-buttons'; +import { TextBoxComponent } from '@syncfusion/ej2-react-inputs'; +import { GridComponent, ColumnsDirective, ColumnDirective } from '@syncfusion/ej2-react-grids'; + +type Client = { + uuid: string; + hostname?: string; + ip_address?: string; + last_alive?: string; +}; + +const SetupMode: React.FC = () => { + const [clients, setClients] = useState([]); + const [descriptions, setDescriptions] = useState>({}); + const [loading, setLoading] = useState(false); + + useEffect(() => { + let polling: ReturnType | null = null; + const isEqual = (a: Client[], b: Client[]) => { + if (a.length !== b.length) return false; + const aSorted = [...a].sort((x, y) => x.uuid.localeCompare(y.uuid)); + const bSorted = [...b].sort((x, y) => x.uuid.localeCompare(y.uuid)); + for (let i = 0; i < aSorted.length; i++) { + if (aSorted[i].uuid !== bSorted[i].uuid) return false; + if (aSorted[i].hostname !== bSorted[i].hostname) return false; + if (aSorted[i].ip_address !== bSorted[i].ip_address) return false; + if (aSorted[i].last_alive !== bSorted[i].last_alive) return false; + } + return true; + }; + let firstLoad = true; + const fetchClients = () => { + if (firstLoad) setLoading(true); + fetchClientsWithoutDescription().then(list => { + if (firstLoad) { + setLoading(false); + firstLoad = false; + } + setClients(prev => (isEqual(prev, list) ? prev : list)); + }); + }; + fetchClients(); + polling = setInterval(fetchClients, 5000); // alle 5 Sekunden + return () => { + if (polling) clearInterval(polling); + }; + }, []); + + const handleDescriptionChange = (uuid: string, value: string) => { + setDescriptions(prev => ({ ...prev, [uuid]: value })); + }; + + const handleSave = (uuid: string) => { + setClientDescription(uuid, descriptions[uuid] || '').then(() => { + setClients(prev => prev.filter(c => c.uuid !== uuid)); + }); + }; + + if (loading) return
Lade neue Clients ...
; + + return ( +
+

Erweiterungsmodus: Neue Clients zuordnen

+ + + + + + + ( + handleDescriptionChange(props.uuid, e.value as string)} + /> + )} + /> + ( + handleSave(props.uuid)} + /> + )} + /> + + + {clients.length === 0 &&
Keine neuen Clients ohne Beschreibung.
} +
+ ); +}; + +export default SetupMode; diff --git a/dashboard/src/apiClients.ts b/dashboard/src/apiClients.ts index 5ebf91f..c369748 100644 --- a/dashboard/src/apiClients.ts +++ b/dashboard/src/apiClients.ts @@ -1,12 +1,19 @@ -// 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; - group_id: number; // <--- Dieses Feld ergänzen + hardware_token?: string; + ip?: string; + type?: string; + hostname?: string; + os_version?: string; + software_version?: string; + macs?: string; + model?: string; + description?: string; + registration_time?: string; + last_alive?: string; + is_active?: boolean; + group_id?: number; } export async function fetchClients(): Promise { @@ -17,6 +24,24 @@ export async function fetchClients(): Promise { return await response.json(); } +export async function fetchClientsWithoutDescription(): Promise { + const response = await fetch('/api/clients/without_description'); + if (!response.ok) { + throw new Error('Fehler beim Laden der Clients ohne Beschreibung'); + } + return await response.json(); +} + +export async function setClientDescription(uuid: string, description: string) { + const res = await fetch(`/api/clients/${uuid}/description`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ description }), + }); + if (!res.ok) throw new Error('Fehler beim Setzen der Beschreibung'); + return await res.json(); +} + export async function updateClientGroup(clientIds: string[], groupName: string) { const res = await fetch('/api/clients/group', { method: 'PUT', diff --git a/dashboard/src/clients.tsx b/dashboard/src/clients.tsx index a612bd6..2356c60 100644 --- a/dashboard/src/clients.tsx +++ b/dashboard/src/clients.tsx @@ -1,8 +1,62 @@ -import React from 'react'; -const Infoscreens: React.FC = () => ( -
-

Infoscreens

-

Willkommen im Infoscreen-Management Infoscreens.

-
-); -export default Infoscreens; +import SetupModeButton from './components/SetupModeButton'; +import React, { useEffect, useState } from 'react'; +import { fetchClients, fetchClientsWithoutDescription, setClientDescription } from './apiClients'; +import type { Client } from './apiClients'; + +// Dummy Modalbox (ersetzbar durch SyncFusion Dialog) +function ModalBox({ open, onClose }) { + if (!open) return null; + return ( +
+
+

Neue Clients ohne Beschreibung!

+

Bitte ergänzen Sie die Beschreibung für neue Clients.

+ +
+
+ ); +} + +const Clients: React.FC = () => { + const [clients, setClients] = useState([]); + const [showModal, setShowModal] = useState(false); + + useEffect(() => { + fetchClients().then(setClients); + fetchClientsWithoutDescription().then(list => { + if (list.length > 0) setShowModal(true); + }); + }, []); + + return ( +
+

Client-Übersicht

+ + + + + + + + + + + {clients.map(c => ( + + + + + + + ))} + +
UUIDHostnameBeschreibungGruppe
{c.uuid}{c.hostname}{c.description}{c.group_id}
+
+ +
+ setShowModal(false)} /> +
+ ); +}; + +export default Clients; diff --git a/dashboard/src/components/SetupModeButton.tsx b/dashboard/src/components/SetupModeButton.tsx new file mode 100644 index 0000000..b254cb3 --- /dev/null +++ b/dashboard/src/components/SetupModeButton.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Wrench } from 'lucide-react'; + +const SetupModeButton: React.FC = () => { + const navigate = useNavigate(); + return ( + + ); +}; + +export default SetupModeButton; diff --git a/dashboard/src/dashboard.tsx b/dashboard/src/dashboard.tsx index 2fde2d0..305aac2 100644 --- a/dashboard/src/dashboard.tsx +++ b/dashboard/src/dashboard.tsx @@ -1,14 +1,15 @@ import React, { useEffect, useState } from 'react'; -import { fetchClients } from './apiClients'; +import { fetchClients, fetchClientsWithoutDescription } from './apiClients'; import type { Client } from './apiClients'; + const Dashboard: React.FC = () => { const [clients, setClients] = useState([]); useEffect(() => { fetchClients().then(setClients).catch(console.error); }, []); - + return (
@@ -18,15 +19,18 @@ const Dashboard: React.FC = () => {
{clients.map(client => (
-

{client.location || 'Unbekannter Standort'}

+

{client.description || 'Unbekannter Standort'}

{`Screenshot (e.currentTarget.style.display = 'none')} + onError={e => { + e.currentTarget.onerror = null; // verhindert Endlosschleife + e.currentTarget.src = "https://placehold.co/400x300?text=No+Screenshot"; + }} />
- IP: {client.ip_address} + IP: {client.ip}
Letztes Lebenszeichen:{' '} diff --git a/dashboard/src/infoscreen_groups.tsx b/dashboard/src/infoscreen_groups.tsx index fd8914d..28816fe 100644 --- a/dashboard/src/infoscreen_groups.tsx +++ b/dashboard/src/infoscreen_groups.tsx @@ -106,8 +106,9 @@ const Infoscreen_groups: React.FC = () => { data.map((c, i) => ({ ...c, Id: c.uuid, - Status: groupMap[c.group_id] || 'Nicht zugeordnet', - Summary: c.location || `Client ${i + 1}`, + Status: + c.group_id === 1 ? 'Nicht zugeordnet' : groupMap[c.group_id] || 'Nicht zugeordnet', + Summary: c.description || `Client ${i + 1}`, })) ); }); @@ -166,7 +167,7 @@ const Infoscreen_groups: React.FC = () => { ...c, Id: c.uuid, Status: groupMap[c.group_id] || 'Nicht zugeordnet', - Summary: c.location || `Client ${i + 1}`, + Summary: c.description || `Client ${i + 1}`, })) ); } catch (err) { @@ -200,7 +201,7 @@ const Infoscreen_groups: React.FC = () => { ...c, Id: c.uuid, Status: groupMap[c.group_id] || 'Nicht zugeordnet', - Summary: c.location || `Client ${i + 1}`, + Summary: c.description || `Client ${i + 1}`, })) ); } catch (err) { diff --git a/server/routes/clients.py b/server/routes/clients.py index dee7f60..75ff64c 100644 --- a/server/routes/clients.py +++ b/server/routes/clients.py @@ -1,11 +1,54 @@ +from database import Session +from models.models import Client, ClientGroup +from flask import Blueprint, request, jsonify import sys sys.path.append('/workspace') -from flask import Blueprint, request, jsonify -from models.models import Client, ClientGroup -from database import Session clients_bp = Blueprint("clients", __name__, url_prefix="/api/clients") +@clients_bp.route("/without_description", methods=["GET"]) +def get_clients_without_description(): + session = Session() + clients = session.query(Client).filter( + (Client.description == None) | (Client.description == "") + ).all() + result = [ + { + "uuid": c.uuid, + "hardware_token": c.hardware_token, + "ip": c.ip, + "type": c.type, + "hostname": c.hostname, + "os_version": c.os_version, + "software_version": c.software_version, + "macs": c.macs, + "model": c.model, + "registration_time": c.registration_time.isoformat() if c.registration_time else None, + "last_alive": c.last_alive.isoformat() if c.last_alive else None, + "is_active": c.is_active, + "group_id": c.group_id, + } + for c in clients + ] + session.close() + return jsonify(result) + + +@clients_bp.route("//description", methods=["PUT"]) +def set_client_description(uuid): + data = request.get_json() + description = data.get("description", "").strip() + if not description: + return jsonify({"error": "Beschreibung darf nicht leer sein"}), 400 + session = Session() + client = session.query(Client).filter_by(uuid=uuid).first() + if not client: + session.close() + return jsonify({"error": "Client nicht gefunden"}), 404 + client.description = description + session.commit() + session.close() + return jsonify({"success": True}) @clients_bp.route("", methods=["GET"]) def get_clients(): @@ -14,10 +57,18 @@ def get_clients(): result = [ { "uuid": c.uuid, - "location": c.location, - "hardware_hash": c.hardware_hash, - "ip_address": c.ip_address, + "hardware_token": c.hardware_token, + "ip": c.ip, + "type": c.type, + "hostname": c.hostname, + "os_version": c.os_version, + "software_version": c.software_version, + "macs": c.macs, + "model": c.model, + "description": c.description, + "registration_time": c.registration_time.isoformat() if c.registration_time else None, "last_alive": c.last_alive.isoformat() if c.last_alive else None, + "is_active": c.is_active, "group_id": c.group_id, } for c in clients diff --git a/server/routes/setup.py b/server/routes/setup.py new file mode 100644 index 0000000..bf18f4d --- /dev/null +++ b/server/routes/setup.py @@ -0,0 +1,21 @@ +from flask import Blueprint, jsonify +from server.database import get_db +from models.models import Client + +bp = Blueprint('setup', __name__, url_prefix='/api/setup') + +@bp.route('/clients_without_description', methods=['GET']) +def clients_without_description(): + db = get_db() + clients = db.query(Client).filter(Client.description == None).all() + result = [] + for c in clients: + result.append({ + 'uuid': c.uuid, + 'hostname': c.hostname, + 'ip_address': c.ip_address, + 'last_alive': c.last_alive, + 'created_at': c.created_at, + 'group': c.group_id, + }) + return jsonify(result) diff --git a/server/wsgi.py b/server/wsgi.py index febb72a..65df876 100644 --- a/server/wsgi.py +++ b/server/wsgi.py @@ -35,7 +35,8 @@ def get_screenshot(uuid): pattern = os.path.join("screenshots", f"{uuid}*.jpg") files = glob.glob(pattern) if not files: - return jsonify({"error": "Screenshot not found"}), 404 + # Dummy-Bild als Redirect oder direkt als Response + return jsonify({"error": "Screenshot not found", "dummy": "https://placehold.co/400x300?text=No+Screenshot"}), 404 filename = os.path.basename(files[0]) return send_from_directory("screenshots", filename)