464 lines
18 KiB
HTML
464 lines
18 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}
|
|
Tapo-Steckdosen-Steuerung | MYP Platform
|
|
{% endblock %}
|
|
|
|
{% block page_heading %}
|
|
<div class="flex items-center space-x-4">
|
|
<div class="bg-gradient-to-br from-orange-500 to-red-600 p-3 rounded-xl shadow-lg">
|
|
<i class="fas fa-plug text-white text-2xl"></i>
|
|
</div>
|
|
<div>
|
|
<h1 class="text-3xl font-bold text-slate-800 dark:text-white">
|
|
Tapo-Steckdosen-Steuerung
|
|
</h1>
|
|
<p class="text-slate-600 dark:text-slate-300">
|
|
Direkte Kontrolle aller TP-Link Tapo-Steckdosen
|
|
</p>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block page_actions %}
|
|
<div class="flex flex-wrap gap-3">
|
|
<button onclick="refreshAllStatus()"
|
|
class="btn-secondary flex items-center space-x-2">
|
|
<i class="fas fa-sync-alt"></i>
|
|
<span>Status aktualisieren</span>
|
|
</button>
|
|
|
|
{% 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>
|
|
|
|
<a href="{{ url_for('tapo.manual_control') }}"
|
|
class="btn-outline flex items-center space-x-2">
|
|
<i class="fas fa-tools"></i>
|
|
<span>Manuelle Steuerung</span>
|
|
</a>
|
|
{% endif %}
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<!-- Statistik-Übersicht -->
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
|
<div class="card">
|
|
<div class="flex items-center space-x-4">
|
|
<div class="bg-blue-100 dark:bg-blue-900 p-3 rounded-lg">
|
|
<i class="fas fa-plug text-blue-600 dark:text-blue-300 text-xl"></i>
|
|
</div>
|
|
<div>
|
|
<h3 class="text-lg font-semibold text-slate-700 dark:text-slate-300">
|
|
Gesamt
|
|
</h3>
|
|
<p class="text-2xl font-bold text-slate-900 dark:text-white" id="total-count">
|
|
{{ total_outlets }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="flex items-center space-x-4">
|
|
<div class="bg-green-100 dark:bg-green-900 p-3 rounded-lg">
|
|
<i class="fas fa-wifi text-green-600 dark:text-green-300 text-xl"></i>
|
|
</div>
|
|
<div>
|
|
<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>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="flex items-center space-x-4">
|
|
<div class="bg-orange-100 dark:bg-orange-900 p-3 rounded-lg">
|
|
<i class="fas fa-bolt text-orange-600 dark:text-orange-300 text-xl"></i>
|
|
</div>
|
|
<div>
|
|
<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>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Steckdosen-Grid -->
|
|
<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
|
|
</h2>
|
|
</div>
|
|
|
|
<div class="card-body">
|
|
{% if outlets %}
|
|
<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>
|
|
</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>
|
|
{% 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-red-600 dark:text-red-400">
|
|
<i class="fas fa-exclamation-triangle mr-1"></i>OFFLINE
|
|
</span>
|
|
{% endif %}
|
|
</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>
|
|
</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.manage_printers') }}"
|
|
class="btn-secondary">
|
|
<i class="fas fa-plus mr-2"></i>
|
|
Drucker hinzufügen
|
|
</a>
|
|
</div>
|
|
{% 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>
|
|
|
|
<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();
|
|
}
|
|
}, 30000);
|
|
|
|
// Initial statistics update
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
updateStatistics();
|
|
});
|
|
</script>
|
|
{% endblock %} |