Automatic detect of clients
This commit is contained in:
@@ -14,6 +14,7 @@ import {
|
|||||||
Monitor,
|
Monitor,
|
||||||
MonitorDotIcon,
|
MonitorDotIcon,
|
||||||
LogOut,
|
LogOut,
|
||||||
|
Wrench,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { ToastProvider } from './components/ToastProvider';
|
import { ToastProvider } from './components/ToastProvider';
|
||||||
|
|
||||||
@@ -22,7 +23,8 @@ const sidebarItems = [
|
|||||||
{ name: 'Termine', path: '/termine', icon: Calendar },
|
{ name: 'Termine', path: '/termine', icon: Calendar },
|
||||||
{ name: 'Ressourcen', path: '/ressourcen', icon: Boxes },
|
{ name: 'Ressourcen', path: '/ressourcen', icon: Boxes },
|
||||||
{ name: 'Raumgruppen', path: '/infoscr_groups', icon: MonitorDotIcon },
|
{ 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: 'Medien', path: '/medien', icon: Image },
|
||||||
{ name: 'Benutzer', path: '/benutzer', icon: User },
|
{ name: 'Benutzer', path: '/benutzer', icon: User },
|
||||||
{ name: 'Einstellungen', path: '/einstellungen', icon: Settings },
|
{ name: 'Einstellungen', path: '/einstellungen', icon: Settings },
|
||||||
@@ -119,26 +121,72 @@ const Layout: React.FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const App: React.FC = () => (
|
import { useEffect } from 'react';
|
||||||
<Router>
|
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>
|
<ToastProvider>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Layout />}>
|
<Route path="/" element={<Layout />}>
|
||||||
<Route index element={<Dashboard />} />
|
<Route index element={<Dashboard />} />
|
||||||
<Route path="termine" element={<Appointments />} />
|
<Route path="termine" element={<Appointments />} />
|
||||||
<Route path="ressourcen" element={<Ressourcen />} />
|
<Route path="ressourcen" element={<Ressourcen />} />
|
||||||
<Route path="Infoscreens" element={<Infoscreens />} />
|
|
||||||
<Route path="infoscr_groups" element={<Infoscreen_groups />} />
|
<Route path="infoscr_groups" element={<Infoscreen_groups />} />
|
||||||
<Route path="medien" element={<Media />} />
|
<Route path="medien" element={<Media />} />
|
||||||
<Route path="benutzer" element={<Benutzer />} />
|
<Route path="benutzer" element={<Benutzer />} />
|
||||||
<Route path="einstellungen" element={<Einstellungen />} />
|
<Route path="einstellungen" element={<Einstellungen />} />
|
||||||
|
<Route path="clients" element={<Infoscreens />} />
|
||||||
|
<Route path="setup" element={<SetupMode />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
</ToastProvider>
|
</ToastProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const AppWrapper: React.FC = () => (
|
||||||
|
<Router>
|
||||||
|
<App />
|
||||||
</Router>
|
</Router>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default App;
|
export default AppWrapper;
|
||||||
|
|
||||||
// Dummy Components (können in eigene Dateien ausgelagert werden)
|
// Dummy Components (können in eigene Dateien ausgelagert werden)
|
||||||
import Dashboard from './dashboard';
|
import Dashboard from './dashboard';
|
||||||
@@ -149,3 +197,4 @@ import Infoscreen_groups from './infoscreen_groups';
|
|||||||
import Media from './media';
|
import Media from './media';
|
||||||
import Benutzer from './benutzer';
|
import Benutzer from './benutzer';
|
||||||
import Einstellungen from './einstellungen';
|
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 {
|
export interface Client {
|
||||||
uuid: string;
|
uuid: string;
|
||||||
location: string;
|
hardware_token?: string;
|
||||||
hardware_hash: string;
|
ip?: string;
|
||||||
ip_address: string;
|
type?: string;
|
||||||
last_alive: string | null;
|
hostname?: string;
|
||||||
group_id: number; // <--- Dieses Feld ergänzen
|
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[]> {
|
export async function fetchClients(): Promise<Client[]> {
|
||||||
@@ -17,6 +24,24 @@ export async function fetchClients(): Promise<Client[]> {
|
|||||||
return await response.json();
|
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) {
|
export async function updateClientGroup(clientIds: string[], groupName: string) {
|
||||||
const res = await fetch('/api/clients/group', {
|
const res = await fetch('/api/clients/group', {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
|
|||||||
@@ -1,8 +1,62 @@
|
|||||||
import React from 'react';
|
import SetupModeButton from './components/SetupModeButton';
|
||||||
const Infoscreens: React.FC = () => (
|
import React, { useEffect, useState } from 'react';
|
||||||
<div>
|
import { fetchClients, fetchClientsWithoutDescription, setClientDescription } from './apiClients';
|
||||||
<h2 className="text-xl font-bold mb-4">Infoscreens</h2>
|
import type { Client } from './apiClients';
|
||||||
<p>Willkommen im Infoscreen-Management Infoscreens.</p>
|
|
||||||
|
// 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>
|
||||||
);
|
</div>
|
||||||
export default Infoscreens;
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 React, { useEffect, useState } from 'react';
|
||||||
import { fetchClients } from './apiClients';
|
import { fetchClients, fetchClientsWithoutDescription } from './apiClients';
|
||||||
import type { Client } from './apiClients';
|
import type { Client } from './apiClients';
|
||||||
|
|
||||||
|
|
||||||
const Dashboard: React.FC = () => {
|
const Dashboard: React.FC = () => {
|
||||||
const [clients, setClients] = useState<Client[]>([]);
|
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">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
{clients.map(client => (
|
{clients.map(client => (
|
||||||
<div key={client.uuid} className="bg-white rounded shadow p-4 flex flex-col items-center">
|
<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
|
<img
|
||||||
src={`/screenshots/${client.uuid}`}
|
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"
|
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">
|
<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>
|
||||||
<div className="text-sm text-gray-700">
|
<div className="text-sm text-gray-700">
|
||||||
<span className="font-semibold">Letztes Lebenszeichen:</span>{' '}
|
<span className="font-semibold">Letztes Lebenszeichen:</span>{' '}
|
||||||
|
|||||||
@@ -106,8 +106,9 @@ const Infoscreen_groups: React.FC = () => {
|
|||||||
data.map((c, i) => ({
|
data.map((c, i) => ({
|
||||||
...c,
|
...c,
|
||||||
Id: c.uuid,
|
Id: c.uuid,
|
||||||
Status: groupMap[c.group_id] || 'Nicht zugeordnet',
|
Status:
|
||||||
Summary: c.location || `Client ${i + 1}`,
|
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,
|
...c,
|
||||||
Id: c.uuid,
|
Id: c.uuid,
|
||||||
Status: groupMap[c.group_id] || 'Nicht zugeordnet',
|
Status: groupMap[c.group_id] || 'Nicht zugeordnet',
|
||||||
Summary: c.location || `Client ${i + 1}`,
|
Summary: c.description || `Client ${i + 1}`,
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -200,7 +201,7 @@ const Infoscreen_groups: React.FC = () => {
|
|||||||
...c,
|
...c,
|
||||||
Id: c.uuid,
|
Id: c.uuid,
|
||||||
Status: groupMap[c.group_id] || 'Nicht zugeordnet',
|
Status: groupMap[c.group_id] || 'Nicht zugeordnet',
|
||||||
Summary: c.location || `Client ${i + 1}`,
|
Summary: c.description || `Client ${i + 1}`,
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -1,11 +1,54 @@
|
|||||||
|
from database import Session
|
||||||
|
from models.models import Client, ClientGroup
|
||||||
|
from flask import Blueprint, request, jsonify
|
||||||
import sys
|
import sys
|
||||||
sys.path.append('/workspace')
|
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 = 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"])
|
@clients_bp.route("", methods=["GET"])
|
||||||
def get_clients():
|
def get_clients():
|
||||||
@@ -14,10 +57,18 @@ def get_clients():
|
|||||||
result = [
|
result = [
|
||||||
{
|
{
|
||||||
"uuid": c.uuid,
|
"uuid": c.uuid,
|
||||||
"location": c.location,
|
"hardware_token": c.hardware_token,
|
||||||
"hardware_hash": c.hardware_hash,
|
"ip": c.ip,
|
||||||
"ip_address": c.ip_address,
|
"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,
|
"last_alive": c.last_alive.isoformat() if c.last_alive else None,
|
||||||
|
"is_active": c.is_active,
|
||||||
"group_id": c.group_id,
|
"group_id": c.group_id,
|
||||||
}
|
}
|
||||||
for c in clients
|
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")
|
pattern = os.path.join("screenshots", f"{uuid}*.jpg")
|
||||||
files = glob.glob(pattern)
|
files = glob.glob(pattern)
|
||||||
if not files:
|
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])
|
filename = os.path.basename(files[0])
|
||||||
return send_from_directory("screenshots", filename)
|
return send_from_directory("screenshots", filename)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user