🎉 Improved database performance and session management in backend/backend/database/myp.db, backend/blueprints/__pycache__/tapo_control.cpython-313.pyc, backend/blueprints/tapo_control.py, backend/config/settings.py

This commit is contained in:
2025-06-12 11:32:34 +02:00
parent 57715ce04d
commit 774f52b67e
71 changed files with 1413 additions and 886 deletions

View File

@ -14,7 +14,7 @@ Tapo-Steckdosen-Steuerung | MYP Platform
Tapo-Steckdosen-Steuerung
</h1>
<p class="text-slate-600 dark:text-slate-300">
Direkte Kontrolle aller TP-Link Tapo-Steckdosen
Mercedes-Benz TBA Marienfelde - 6 Arbeitsplätze für 3D-Drucker
</p>
</div>
</div>
@ -22,18 +22,19 @@ Tapo-Steckdosen-Steuerung | MYP Platform
{% block page_actions %}
<div class="flex flex-wrap gap-3">
<button onclick="refreshAllStatus()"
class="btn-secondary flex items-center space-x-2">
<a href="{{ url_for('tapo.tapo_dashboard') }}"
class="btn-secondary flex items-center space-x-2">
<i class="fas fa-sync-alt"></i>
<span>Status aktualisieren</span>
</button>
</a>
{% if current_user.is_authenticated and current_user.has_permission('ADMIN') %}
<button onclick="discoverOutlets()"
class="btn-primary flex items-center space-x-2">
<i class="fas fa-search"></i>
<span>Steckdosen suchen</span>
</button>
<form method="POST" action="{{ url_for('tapo.discover_outlets') }}" class="inline">
<button type="submit" class="btn-primary flex items-center space-x-2">
<i class="fas fa-search"></i>
<span>Steckdosen suchen</span>
</button>
</form>
<a href="{{ url_for('tapo.manual_control') }}"
class="btn-outline flex items-center space-x-2">
@ -54,10 +55,10 @@ Tapo-Steckdosen-Steuerung | MYP Platform
</div>
<div>
<h3 class="text-lg font-semibold text-slate-700 dark:text-slate-300">
Gesamt
Arbeitsplätze Gesamt
</h3>
<p class="text-2xl font-bold text-slate-900 dark:text-white" id="total-count">
{{ total_outlets }}
<p class="text-2xl font-bold text-slate-900 dark:text-white">
{{ total_outlets or 6 }}
</p>
</div>
</div>
@ -72,8 +73,8 @@ Tapo-Steckdosen-Steuerung | MYP Platform
<h3 class="text-lg font-semibold text-slate-700 dark:text-slate-300">
Online
</h3>
<p class="text-2xl font-bold text-slate-900 dark:text-white" id="online-count">
{{ online_outlets }}
<p class="text-2xl font-bold text-slate-900 dark:text-white">
{{ online_outlets or 0 }}
</p>
</div>
</div>
@ -88,377 +89,238 @@ Tapo-Steckdosen-Steuerung | MYP Platform
<h3 class="text-lg font-semibold text-slate-700 dark:text-slate-300">
Aktive
</h3>
<p class="text-2xl font-bold text-slate-900 dark:text-white" id="active-count">
0
<p class="text-2xl font-bold text-slate-900 dark:text-white">
{{ outlets.values() | selectattr('status', 'equalto', 'on') | list | length }}
</p>
</div>
</div>
</div>
</div>
<!-- Steckdosen-Grid -->
<!-- Mercedes-Benz 6 Arbeitsplätze -->
<div class="card">
<div class="card-header">
<h2 class="text-xl font-semibold text-slate-800 dark:text-white flex items-center">
<i class="fas fa-list mr-3"></i>
Alle Tapo-Steckdosen
<i class="fas fa-industry mr-3"></i>
Mercedes-Benz TBA Marienfelde - 3D-Drucker Arbeitsplätze
</h2>
<p class="text-sm text-slate-600 dark:text-slate-400 mt-2">
Feste Installation mit 6 konfigurierten Arbeitsplätzen
</p>
</div>
<div class="card-body">
{% if outlets %}
<!-- Steckdosen-Grid - Immer 6 Arbeitsplätze anzeigen -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6" id="outlets-grid">
{% for ip, outlet in outlets.items() %}
<div class="outlet-card border rounded-lg p-4 {{ 'border-green-300 bg-green-50 dark:bg-green-900/20' if outlet.reachable else 'border-red-300 bg-red-50 dark:bg-red-900/20' }}"
data-ip="{{ ip }}">
<!-- Steckdosen-Header -->
<div class="flex items-center justify-between mb-4">
<div class="flex items-center space-x-3">
<div class="status-indicator w-4 h-4 rounded-full {{ 'bg-green-500' if outlet.reachable else 'bg-red-500' }}"
data-ip="{{ ip }}"></div>
<div>
<h3 class="font-semibold text-slate-800 dark:text-white">
{{ outlet.printer_name }}
</h3>
<p class="text-sm text-slate-600 dark:text-slate-400">
{{ ip }}
</p>
{% if outlets %}
{% for ip, outlet in outlets.items() %}
<div class="outlet-card border rounded-lg p-4 {{ 'border-green-300 bg-green-50 dark:bg-green-900/20' if outlet.reachable else 'border-red-300 bg-red-50 dark:bg-red-900/20' }}"
data-ip="{{ ip }}">
<!-- Arbeitsplatz-Header -->
<div class="flex items-center justify-between mb-4">
<div class="flex items-center space-x-3">
<div class="status-indicator w-4 h-4 rounded-full {{ 'bg-green-500' if outlet.reachable else 'bg-red-500' }}"></div>
<div>
<h3 class="font-semibold text-slate-800 dark:text-white">
{{ outlet.printer_name }}
</h3>
<p class="text-sm text-slate-600 dark:text-slate-400">
IP: {{ ip }}
</p>
{% if outlet.model %}
<p class="text-xs text-slate-500 dark:text-slate-500">
{{ outlet.model }}
</p>
{% endif %}
</div>
</div>
<div class="flex items-center space-x-2">
<!-- Konfigurationsstatus -->
{% if outlet.configured_in_db %}
<span class="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full dark:bg-blue-900 dark:text-blue-300"
title="In Datenbank konfiguriert">
<i class="fas fa-database text-xs"></i>
</span>
{% else %}
<span class="bg-yellow-100 text-yellow-800 text-xs px-2 py-1 rounded-full dark:bg-yellow-900 dark:text-yellow-300"
title="Nicht in Datenbank konfiguriert">
<i class="fas fa-exclamation-triangle text-xs"></i>
</span>
{% endif %}
<!-- Position-Badge -->
<span class="bg-slate-100 text-slate-700 text-xs px-2 py-1 rounded-full dark:bg-slate-700 dark:text-slate-300">
#{{ outlet.position or loop.index }}
</span>
</div>
</div>
<div class="flex items-center space-x-2">
<button onclick="refreshOutletStatus('{{ ip }}')"
class="p-2 text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200 transition-colors"
title="Status aktualisieren">
<i class="fas fa-sync-alt text-sm"></i>
</button>
<button onclick="testConnection('{{ ip }}')"
class="p-2 text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200 transition-colors"
title="Verbindung testen">
<i class="fas fa-network-wired text-sm"></i>
</button>
</div>
</div>
<!-- Status-Info -->
<div class="mb-4">
<div class="flex items-center justify-between">
<span class="text-sm text-slate-600 dark:text-slate-400">Status:</span>
<span class="status-text font-medium" data-ip="{{ ip }}">
{% if outlet.reachable %}
{% if outlet.status == 'on' %}
<span class="text-green-600 dark:text-green-400">
<i class="fas fa-power-off mr-1"></i>EIN
</span>
{% elif outlet.status == 'off' %}
<span class="text-slate-600 dark:text-slate-400">
<i class="fas fa-power-off mr-1"></i>AUS
</span>
<!-- Status-Informationen -->
<div class="mb-4 space-y-2">
<div class="flex items-center justify-between">
<span class="text-sm text-slate-600 dark:text-slate-400">Status:</span>
<span class="status-text font-medium">
{% if outlet.reachable %}
{% if outlet.status == 'on' %}
<span class="text-green-600 dark:text-green-400">
<i class="fas fa-power-off mr-1"></i>EIN
</span>
{% elif outlet.status == 'off' %}
<span class="text-slate-600 dark:text-slate-400">
<i class="fas fa-power-off mr-1"></i>AUS
</span>
{% else %}
<span class="text-yellow-600 dark:text-yellow-400">
<i class="fas fa-question mr-1"></i>UNBEKANNT
</span>
{% endif %}
{% else %}
<span class="text-yellow-600 dark:text-yellow-400">
<i class="fas fa-question mr-1"></i>UNBEKANNT
<span class="text-red-600 dark:text-red-400">
<i class="fas fa-exclamation-triangle mr-1"></i>OFFLINE
</span>
{% endif %}
{% else %}
<span class="text-red-600 dark:text-red-400">
<i class="fas fa-exclamation-triangle mr-1"></i>OFFLINE
</span>
</span>
</div>
<div class="flex items-center justify-between">
<span class="text-sm text-slate-600 dark:text-slate-400">Standort:</span>
<span class="text-sm font-medium text-slate-800 dark:text-white">
{{ outlet.location }}
</span>
</div>
{% if outlet.configured_in_db and outlet.printer_id %}
<div class="flex items-center justify-between">
<span class="text-sm text-slate-600 dark:text-slate-400">Drucker-ID:</span>
<span class="text-sm font-medium text-slate-800 dark:text-white">
#{{ outlet.printer_id }}
</span>
</div>
{% endif %}
</div>
<!-- Steuerungsformen (ohne JavaScript) -->
<div class="grid grid-cols-2 gap-3">
<!-- EIN-Button -->
<form method="POST" action="{{ url_for('tapo.control_outlet_form') }}" class="inline">
<input type="hidden" name="ip" value="{{ ip }}">
<input type="hidden" name="action" value="on">
<button type="submit"
class="w-full py-2 px-3 bg-green-600 hover:bg-green-700 text-white rounded-lg transition-colors font-medium text-sm disabled:opacity-50 disabled:cursor-not-allowed"
{% if not outlet.reachable %}disabled{% endif %}>
<i class="fas fa-power-off mr-1"></i>
EIN
</button>
</form>
<!-- AUS-Button -->
<form method="POST" action="{{ url_for('tapo.control_outlet_form') }}" class="inline">
<input type="hidden" name="ip" value="{{ ip }}">
<input type="hidden" name="action" value="off">
<button type="submit"
class="w-full py-2 px-3 bg-slate-600 hover:bg-slate-700 text-white rounded-lg transition-colors font-medium text-sm disabled:opacity-50 disabled:cursor-not-allowed"
{% if not outlet.reachable %}disabled{% endif %}>
<i class="fas fa-power-off mr-1"></i>
AUS
</button>
</form>
</div>
{% if outlet.error %}
<div class="mt-3 p-2 bg-red-100 dark:bg-red-900/30 border border-red-300 dark:border-red-700 rounded text-sm text-red-700 dark:text-red-300">
<i class="fas fa-exclamation-triangle mr-1"></i>
{{ outlet.error }}
</div>
{% endif %}
<!-- Zusätzliche Aktionen für Administratoren -->
{% if current_user.has_permission('ADMIN') %}
<div class="mt-3 pt-3 border-t border-slate-200 dark:border-slate-600">
<div class="flex space-x-2">
<form method="POST" action="{{ url_for('tapo.test_connection_form') }}" class="flex-1">
<input type="hidden" name="ip" value="{{ ip }}">
<button type="submit"
class="w-full text-xs py-1 px-2 text-slate-600 hover:text-slate-800 dark:text-slate-400 dark:hover:text-slate-200 border border-slate-300 dark:border-slate-600 rounded transition-colors">
<i class="fas fa-network-wired mr-1"></i>Test
</button>
</form>
{% if not outlet.configured_in_db %}
<a href="{{ url_for('admin.add_printer') }}?preset_plug_ip={{ ip }}"
class="flex-1 text-xs py-1 px-2 text-center text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-200 border border-blue-300 dark:border-blue-600 rounded transition-colors">
<i class="fas fa-plus mr-1"></i>Setup
</a>
{% endif %}
</div>
</div>
{% endif %}
</div>
{% endfor %}
{% else %}
<!-- Fallback: Zeige 6 Platzhalter-Arbeitsplätze -->
{% for i in range(1, 7) %}
<div class="outlet-card border border-red-300 bg-red-50 dark:bg-red-900/20 rounded-lg p-4">
<div class="flex items-center justify-between mb-4">
<div class="flex items-center space-x-3">
<div class="status-indicator w-4 h-4 rounded-full bg-red-500"></div>
<div>
<h3 class="font-semibold text-slate-800 dark:text-white">
3D-Drucker {{ i }}
</h3>
<p class="text-sm text-slate-600 dark:text-slate-400">
192.168.1.20{{ i }}
</p>
</div>
</div>
<span class="bg-slate-100 text-slate-700 text-xs px-2 py-1 rounded-full dark:bg-slate-700 dark:text-slate-300">
#{{ i }}
</span>
</div>
<div class="flex items-center justify-between mt-2">
<span class="text-sm text-slate-600 dark:text-slate-400">Standort:</span>
<span class="text-sm font-medium text-slate-800 dark:text-white">
{{ outlet.location }}
</span>
<div class="mb-4">
<div class="flex items-center justify-between">
<span class="text-sm text-slate-600 dark:text-slate-400">Status:</span>
<span class="text-red-600 dark:text-red-400">
<i class="fas fa-exclamation-triangle mr-1"></i>NICHT KONFIGURIERT
</span>
</div>
<div class="flex items-center justify-between mt-2">
<span class="text-sm text-slate-600 dark:text-slate-400">Standort:</span>
<span class="text-sm font-medium text-slate-800 dark:text-white">
Arbeitsplatz {{ i }}
</span>
</div>
</div>
<div class="grid grid-cols-2 gap-3">
<button disabled class="w-full py-2 px-3 bg-slate-400 text-white rounded-lg opacity-50 cursor-not-allowed text-sm">
<i class="fas fa-power-off mr-1"></i>EIN
</button>
<button disabled class="w-full py-2 px-3 bg-slate-400 text-white rounded-lg opacity-50 cursor-not-allowed text-sm">
<i class="fas fa-power-off mr-1"></i>AUS
</button>
</div>
<div class="mt-3 p-2 bg-yellow-100 dark:bg-yellow-900/30 border border-yellow-300 dark:border-yellow-700 rounded text-sm text-yellow-700 dark:text-yellow-300">
<i class="fas fa-info-circle mr-1"></i>
Arbeitsplatz {{ i }} noch nicht eingerichtet
</div>
</div>
<!-- Steuerungsbuttons -->
<div class="flex space-x-3">
<button onclick="controlOutlet('{{ ip }}', 'on')"
class="flex-1 py-2 px-3 bg-green-600 hover:bg-green-700 text-white rounded-lg transition-colors font-medium text-sm"
{% if not outlet.reachable %}disabled{% endif %}>
<i class="fas fa-power-off mr-1"></i>
EIN
</button>
<button onclick="controlOutlet('{{ ip }}', 'off')"
class="flex-1 py-2 px-3 bg-slate-600 hover:bg-slate-700 text-white rounded-lg transition-colors font-medium text-sm"
{% if not outlet.reachable %}disabled{% endif %}>
<i class="fas fa-power-off mr-1"></i>
AUS
</button>
</div>
{% if outlet.error %}
<div class="mt-3 p-2 bg-red-100 dark:bg-red-900/30 border border-red-300 dark:border-red-700 rounded text-sm text-red-700 dark:text-red-300">
<i class="fas fa-exclamation-triangle mr-1"></i>
{{ outlet.error }}
</div>
{% endif %}
</div>
{% endfor %}
</div>
{% else %}
<div class="text-center py-12">
<div class="text-slate-400 dark:text-slate-500 mb-4">
<i class="fas fa-plug text-6xl"></i>
</div>
<h3 class="text-xl font-semibold text-slate-600 dark:text-slate-400 mb-2">
Keine Tapo-Steckdosen konfiguriert
</h3>
<p class="text-slate-500 dark:text-slate-400 mb-6">
Es wurden noch keine Drucker mit Tapo-Steckdosen eingerichtet.
</p>
{% if current_user.is_authenticated and current_user.has_permission('ADMIN') %}
<div class="flex justify-center space-x-4">
<button onclick="discoverOutlets()"
class="btn-primary">
<i class="fas fa-search mr-2"></i>
Steckdosen suchen
</button>
<a href="{{ url_for('admin.printers_overview') }}"
class="btn-secondary">
<i class="fas fa-plus mr-2"></i>
Drucker verwalten
</a>
</div>
{% endfor %}
{% endif %}
</div>
{% endif %}
</div>
</div>
<!-- Loading Overlay -->
<div id="loading-overlay" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
<div class="bg-white dark:bg-slate-800 p-6 rounded-lg shadow-lg">
<div class="flex items-center space-x-3">
<i class="fas fa-spinner fa-spin text-blue-600 text-xl"></i>
<span class="text-slate-800 dark:text-white font-medium" id="loading-text">
Lädt...
</span>
</div>
</div>
</div>
<!-- Auto-Refresh nur falls explizit gewünscht (minimales JavaScript) -->
{% if request.args.get('auto_refresh') %}
<script>
// Globale Variablen
let outlets = {{ outlets | tojson }};
// Utility-Funktionen
function showLoading(text = 'Lädt...') {
document.getElementById('loading-text').textContent = text;
document.getElementById('loading-overlay').classList.remove('hidden');
}
function hideLoading() {
document.getElementById('loading-overlay').classList.add('hidden');
}
function updateOutletDisplay(ip, data) {
const card = document.querySelector(`[data-ip="${ip}"]`);
if (!card) return;
const statusIndicator = card.querySelector('.status-indicator');
const statusText = card.querySelector('.status-text');
const buttons = card.querySelectorAll('button[onclick*="controlOutlet"]');
// Status-Indikator aktualisieren
statusIndicator.className = `status-indicator w-4 h-4 rounded-full ${data.reachable ? 'bg-green-500' : 'bg-red-500'}`;
// Status-Text aktualisieren
if (data.reachable) {
let statusIcon, statusColor;
if (data.status === 'on') {
statusIcon = 'fas fa-power-off';
statusColor = 'text-green-600 dark:text-green-400';
statusText.innerHTML = `<span class="${statusColor}"><i class="${statusIcon} mr-1"></i>EIN</span>`;
} else if (data.status === 'off') {
statusIcon = 'fas fa-power-off';
statusColor = 'text-slate-600 dark:text-slate-400';
statusText.innerHTML = `<span class="${statusColor}"><i class="${statusIcon} mr-1"></i>AUS</span>`;
} else {
statusIcon = 'fas fa-question';
statusColor = 'text-yellow-600 dark:text-yellow-400';
statusText.innerHTML = `<span class="${statusColor}"><i class="${statusIcon} mr-1"></i>UNBEKANNT</span>`;
}
// Buttons aktivieren
buttons.forEach(btn => btn.disabled = false);
} else {
statusText.innerHTML = '<span class="text-red-600 dark:text-red-400"><i class="fas fa-exclamation-triangle mr-1"></i>OFFLINE</span>';
// Buttons deaktivieren
buttons.forEach(btn => btn.disabled = true);
}
// Kartenrand aktualisieren
card.className = card.className.replace(/border-\w+-300|bg-\w+-50|dark:bg-\w+-900\/20/g, '');
if (data.reachable) {
card.className += ' border-green-300 bg-green-50 dark:bg-green-900/20';
} else {
card.className += ' border-red-300 bg-red-50 dark:bg-red-900/20';
}
}
function updateStatistics() {
const totalCount = Object.keys(outlets).length;
const onlineCount = Object.values(outlets).filter(o => o.reachable).length;
const activeCount = Object.values(outlets).filter(o => o.status === 'on').length;
document.getElementById('total-count').textContent = totalCount;
document.getElementById('online-count').textContent = onlineCount;
document.getElementById('active-count').textContent = activeCount;
}
// API-Funktionen
async function controlOutlet(ip, action) {
try {
showLoading(`Steckdose wird ${action === 'on' ? 'eingeschaltet' : 'ausgeschaltet'}...`);
const response = await fetch('/tapo/control', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token() }}'
},
body: JSON.stringify({ ip, action })
});
const data = await response.json();
if (data.success) {
showFlashMessage(data.message, 'success');
// Status in lokalen Daten aktualisieren
if (outlets[ip]) {
outlets[ip].status = action;
outlets[ip].reachable = true;
updateOutletDisplay(ip, outlets[ip]);
updateStatistics();
}
} else {
showFlashMessage(data.error, 'error');
}
} catch (error) {
showFlashMessage('Fehler bei der Steckdosen-Steuerung: ' + error.message, 'error');
} finally {
hideLoading();
}
}
async function refreshOutletStatus(ip) {
try {
const response = await fetch(`/tapo/status/${ip}`);
const data = await response.json();
if (data.success) {
if (outlets[ip]) {
outlets[ip].status = data.status;
outlets[ip].reachable = data.reachable;
updateOutletDisplay(ip, outlets[ip]);
updateStatistics();
}
} else {
showFlashMessage(`Fehler beim Status-Check für ${ip}: ${data.error}`, 'error');
}
} catch (error) {
showFlashMessage('Fehler beim Status-Check: ' + error.message, 'error');
}
}
async function refreshAllStatus() {
showLoading('Status aller Steckdosen wird aktualisiert...');
try {
const response = await fetch('/tapo/all-status');
const data = await response.json();
if (data.success) {
// Lokale Daten aktualisieren
for (const [ip, status] of Object.entries(data.outlets)) {
if (outlets[ip]) {
outlets[ip].status = status.status;
outlets[ip].reachable = status.reachable;
updateOutletDisplay(ip, outlets[ip]);
}
}
updateStatistics();
showFlashMessage('Status aller Steckdosen aktualisiert', 'success');
} else {
showFlashMessage('Fehler beim Aktualisieren: ' + data.error, 'error');
}
} catch (error) {
showFlashMessage('Fehler beim Status-Update: ' + error.message, 'error');
} finally {
hideLoading();
}
}
async function testConnection(ip) {
try {
showLoading(`Verbindung zu ${ip} wird getestet...`);
const response = await fetch(`/tapo/test/${ip}`, { method: 'POST' });
const data = await response.json();
if (data.success) {
const testResult = data.test_result;
if (testResult.success) {
showFlashMessage(`✅ Verbindung zu ${ip} erfolgreich!`, 'success');
} else {
showFlashMessage(`❌ Verbindung zu ${ip} fehlgeschlagen: ${testResult.error}`, 'error');
}
} else {
showFlashMessage(`Verbindungstest fehlgeschlagen: ${data.error}`, 'error');
}
} catch (error) {
showFlashMessage('Fehler beim Verbindungstest: ' + error.message, 'error');
} finally {
hideLoading();
}
}
async function discoverOutlets() {
try {
showLoading('Suche nach neuen Tapo-Steckdosen...');
const response = await fetch('/tapo/discover', { method: 'POST' });
const data = await response.json();
if (data.success) {
showFlashMessage(data.message, 'success');
// Seite neu laden wenn neue Steckdosen gefunden wurden
if (data.discovered_count > 0) {
setTimeout(() => {
window.location.reload();
}, 2000);
}
} else {
showFlashMessage('Fehler bei der Erkennung: ' + data.error, 'error');
}
} catch (error) {
showFlashMessage('Fehler bei der Steckdosen-Suche: ' + error.message, 'error');
} finally {
hideLoading();
}
}
// Auto-Refresh alle 30 Sekunden
setInterval(() => {
if (Object.keys(outlets).length > 0) {
refreshAllStatus();
}
// Minimales Auto-Refresh alle 30 Sekunden
setTimeout(function() {
window.location.reload();
}, 30000);
// Initial statistics update
document.addEventListener('DOMContentLoaded', function() {
updateStatistics();
});
</script>
{% endif %}
{% endblock %}