Automatic detect of clients
This commit is contained in:
@@ -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 = () => (
|
||||
<Router>
|
||||
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 (
|
||||
<ToastProvider>
|
||||
<Routes>
|
||||
<Route path="/" element={<Layout />}>
|
||||
<Route index element={<Dashboard />} />
|
||||
<Route path="termine" element={<Appointments />} />
|
||||
<Route path="ressourcen" element={<Ressourcen />} />
|
||||
<Route path="Infoscreens" element={<Infoscreens />} />
|
||||
<Route path="infoscr_groups" element={<Infoscreen_groups />} />
|
||||
<Route path="medien" element={<Media />} />
|
||||
<Route path="benutzer" element={<Benutzer />} />
|
||||
<Route path="einstellungen" element={<Einstellungen />} />
|
||||
<Route path="clients" element={<Infoscreens />} />
|
||||
<Route path="setup" element={<SetupMode />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</ToastProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const AppWrapper: React.FC = () => (
|
||||
<Router>
|
||||
<App />
|
||||
</Router>
|
||||
);
|
||||
|
||||
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';
|
||||
|
||||
106
dashboard/src/SetupMode.tsx
Normal file
106
dashboard/src/SetupMode.tsx
Normal file
@@ -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<Client[]>([]);
|
||||
const [descriptions, setDescriptions] = useState<Record<string, string>>({});
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let polling: ReturnType<typeof setInterval> | 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 <div>Lade neue Clients ...</div>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>Erweiterungsmodus: Neue Clients zuordnen</h2>
|
||||
<GridComponent
|
||||
dataSource={clients}
|
||||
allowPaging={true}
|
||||
pageSettings={{ pageSize: 10 }}
|
||||
rowHeight={50}
|
||||
>
|
||||
<ColumnsDirective>
|
||||
<ColumnDirective field="uuid" headerText="UUID" width="180" />
|
||||
<ColumnDirective field="hostname" headerText="Hostname" width="140" />
|
||||
<ColumnDirective field="ip_address" headerText="IP" width="120" />
|
||||
<ColumnDirective field="last_alive" headerText="Letzter Kontakt" width="160" />
|
||||
<ColumnDirective
|
||||
headerText="Beschreibung"
|
||||
width="220"
|
||||
template={props => (
|
||||
<TextBoxComponent
|
||||
value={descriptions[props.uuid] || ''}
|
||||
placeholder="Beschreibung eingeben"
|
||||
change={e => handleDescriptionChange(props.uuid, e.value as string)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<ColumnDirective
|
||||
headerText="Aktion"
|
||||
width="120"
|
||||
template={props => (
|
||||
<ButtonComponent
|
||||
content="Speichern"
|
||||
disabled={!descriptions[props.uuid]}
|
||||
onClick={() => handleSave(props.uuid)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</ColumnsDirective>
|
||||
</GridComponent>
|
||||
{clients.length === 0 && <div>Keine neuen Clients ohne Beschreibung.</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SetupMode;
|
||||
@@ -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<Client[]> {
|
||||
@@ -17,6 +24,24 @@ export async function fetchClients(): Promise<Client[]> {
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
export async function fetchClientsWithoutDescription(): Promise<Client[]> {
|
||||
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',
|
||||
|
||||
@@ -1,8 +1,62 @@
|
||||
import React from 'react';
|
||||
const Infoscreens: React.FC = () => (
|
||||
<div>
|
||||
<h2 className="text-xl font-bold mb-4">Infoscreens</h2>
|
||||
<p>Willkommen im Infoscreen-Management Infoscreens.</p>
|
||||
</div>
|
||||
);
|
||||
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 (
|
||||
<div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(0,0,0,0.3)', zIndex: 1000 }}>
|
||||
<div style={{ background: 'white', padding: 32, margin: '100px auto', maxWidth: 400, borderRadius: 8 }}>
|
||||
<h3>Neue Clients ohne Beschreibung!</h3>
|
||||
<p>Bitte ergänzen Sie die Beschreibung für neue Clients.</p>
|
||||
<button onClick={onClose}>Speichern (Dummy)</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const Clients: React.FC = () => {
|
||||
const [clients, setClients] = useState<Client[]>([]);
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetchClients().then(setClients);
|
||||
fetchClientsWithoutDescription().then(list => {
|
||||
if (list.length > 0) setShowModal(true);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-xl font-bold mb-4">Client-Übersicht</h2>
|
||||
<table className="min-w-full border">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>UUID</th>
|
||||
<th>Hostname</th>
|
||||
<th>Beschreibung</th>
|
||||
<th>Gruppe</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{clients.map(c => (
|
||||
<tr key={c.uuid}>
|
||||
<td>{c.uuid}</td>
|
||||
<td>{c.hostname}</td>
|
||||
<td>{c.description}</td>
|
||||
<td>{c.group_id}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="mb-4">
|
||||
<SetupModeButton />
|
||||
</div>
|
||||
<ModalBox open={showModal} onClose={() => setShowModal(false)} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Clients;
|
||||
|
||||
19
dashboard/src/components/SetupModeButton.tsx
Normal file
19
dashboard/src/components/SetupModeButton.tsx
Normal file
@@ -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 (
|
||||
<button
|
||||
className="setupmode-btn flex items-center gap-2 px-4 py-2 bg-yellow-200 hover:bg-yellow-300 rounded"
|
||||
onClick={() => navigate('/setup')}
|
||||
title="Erweiterungsmodus starten"
|
||||
>
|
||||
<Wrench size={18} />
|
||||
Erweiterungsmodus
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default SetupModeButton;
|
||||
@@ -1,7 +1,8 @@
|
||||
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<Client[]>([]);
|
||||
|
||||
@@ -18,15 +19,18 @@ const Dashboard: React.FC = () => {
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{clients.map(client => (
|
||||
<div key={client.uuid} className="bg-white rounded shadow p-4 flex flex-col items-center">
|
||||
<h4 className="text-lg font-bold mb-2">{client.location || 'Unbekannter Standort'}</h4>
|
||||
<h4 className="text-lg font-bold mb-2">{client.description || 'Unbekannter Standort'}</h4>
|
||||
<img
|
||||
src={`/screenshots/${client.uuid}`}
|
||||
alt={`Screenshot ${client.location}`}
|
||||
alt={`Screenshot ${client.description || 'Unbekannt'}`}
|
||||
className="w-full h-48 object-contain bg-gray-100 mb-2"
|
||||
onError={e => (e.currentTarget.style.display = 'none')}
|
||||
onError={e => {
|
||||
e.currentTarget.onerror = null; // verhindert Endlosschleife
|
||||
e.currentTarget.src = "https://placehold.co/400x300?text=No+Screenshot";
|
||||
}}
|
||||
/>
|
||||
<div className="text-sm text-gray-700 mb-1">
|
||||
<span className="font-semibold">IP:</span> {client.ip_address}
|
||||
<span className="font-semibold">IP:</span> {client.ip}
|
||||
</div>
|
||||
<div className="text-sm text-gray-700">
|
||||
<span className="font-semibold">Letztes Lebenszeichen:</span>{' '}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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("/<uuid>/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
|
||||
|
||||
21
server/routes/setup.py
Normal file
21
server/routes/setup.py
Normal file
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user