🎉 Improved form testing infrastructure with new files: 'simple_form_tester.py', 'ui_components.html' templates, and updated test suite in 'form_test_automator.py'. 📚
This commit is contained in:
@ -30,6 +30,10 @@ try:
|
||||
except ImportError:
|
||||
PLAYWRIGHT_AVAILABLE = False
|
||||
print("⚠️ Playwright nicht verfügbar. Installiere mit: pip install playwright")
|
||||
# Fallback-Klassen für Type Hints
|
||||
class Page: pass
|
||||
class Browser: pass
|
||||
class BrowserContext: pass
|
||||
|
||||
# Test-Daten-Generierung
|
||||
try:
|
||||
@ -38,6 +42,20 @@ try:
|
||||
except ImportError:
|
||||
FAKER_AVAILABLE = False
|
||||
print("⚠️ Faker nicht verfügbar. Installiere mit: pip install faker")
|
||||
# Fallback-Klasse
|
||||
class Faker:
|
||||
def __init__(self, locale='de_DE'): pass
|
||||
def email(self): return "test@example.com"
|
||||
def name(self): return "Test User"
|
||||
def first_name(self): return "Test"
|
||||
def last_name(self): return "User"
|
||||
def company(self): return "Test Company"
|
||||
def city(self): return "Test City"
|
||||
def street_address(self): return "Test Street 123"
|
||||
def phone_number(self): return "+49 123 4567890"
|
||||
def url(self): return "https://example.com"
|
||||
def date(self): return "2024-06-18"
|
||||
def text(self, max_nb_chars=200): return "Test text"
|
||||
|
||||
# HTML-Parsing
|
||||
try:
|
||||
@ -46,6 +64,9 @@ try:
|
||||
except ImportError:
|
||||
BS4_AVAILABLE = False
|
||||
print("⚠️ BeautifulSoup nicht verfügbar. Installiere mit: pip install beautifulsoup4")
|
||||
# Fallback-Klasse
|
||||
class BeautifulSoup:
|
||||
def __init__(self, *args, **kwargs): pass
|
||||
|
||||
# Rich Console für schöne Ausgabe
|
||||
try:
|
||||
@ -59,6 +80,13 @@ try:
|
||||
except ImportError:
|
||||
RICH_AVAILABLE = False
|
||||
console = None
|
||||
# Fallback-Klassen
|
||||
class Console:
|
||||
def print(self, *args, **kwargs): print(*args)
|
||||
class Table:
|
||||
def __init__(self, *args, **kwargs): pass
|
||||
def add_column(self, *args, **kwargs): pass
|
||||
def add_row(self, *args, **kwargs): pass
|
||||
|
||||
|
||||
class TestStatus(Enum):
|
||||
|
635
backend/simple_form_tester.py
Normal file
635
backend/simple_form_tester.py
Normal file
@ -0,0 +1,635 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Vereinfachter HTML-Formular Tester für MYP System
|
||||
===============================================
|
||||
|
||||
Ein schlanker Formular-Tester ohne externe Dependencies,
|
||||
der grundlegende Formular-Validierung mit Standard-Python-Libraries testet.
|
||||
|
||||
Autor: Till Tomczak
|
||||
Mercedes-Benz Projektarbeit MYP
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import time
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
import urllib.error
|
||||
from urllib.parse import urljoin, urlparse
|
||||
from html.parser import HTMLParser
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, List, Optional
|
||||
import sys
|
||||
import re
|
||||
|
||||
|
||||
@dataclass
|
||||
class FormField:
|
||||
"""Repräsentiert ein Formular-Feld"""
|
||||
name: str
|
||||
field_type: str
|
||||
required: bool = False
|
||||
pattern: str = ""
|
||||
placeholder: str = ""
|
||||
value: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class Form:
|
||||
"""Repräsentiert ein HTML-Formular"""
|
||||
action: str
|
||||
method: str
|
||||
fields: List[FormField]
|
||||
name: str = ""
|
||||
|
||||
|
||||
class FormParser(HTMLParser):
|
||||
"""Parst HTML und extrahiert Formular-Informationen"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.forms = []
|
||||
self.current_form = None
|
||||
self.in_form = False
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
attrs_dict = dict(attrs)
|
||||
|
||||
if tag == 'form':
|
||||
self.in_form = True
|
||||
self.current_form = Form(
|
||||
action=attrs_dict.get('action', ''),
|
||||
method=attrs_dict.get('method', 'GET').upper(),
|
||||
fields=[],
|
||||
name=attrs_dict.get('name', attrs_dict.get('id', f'form_{len(self.forms)}'))
|
||||
)
|
||||
|
||||
elif self.in_form and tag in ['input', 'textarea', 'select']:
|
||||
field = FormField(
|
||||
name=attrs_dict.get('name', attrs_dict.get('id', f'field_{len(self.current_form.fields)}')),
|
||||
field_type=attrs_dict.get('type', tag),
|
||||
required='required' in attrs_dict,
|
||||
pattern=attrs_dict.get('pattern', ''),
|
||||
placeholder=attrs_dict.get('placeholder', ''),
|
||||
value=attrs_dict.get('value', '')
|
||||
)
|
||||
self.current_form.fields.append(field)
|
||||
|
||||
def handle_endtag(self, tag):
|
||||
if tag == 'form' and self.in_form:
|
||||
self.in_form = False
|
||||
if self.current_form:
|
||||
self.forms.append(self.current_form)
|
||||
self.current_form = None
|
||||
|
||||
|
||||
class SimpleFormTester:
|
||||
"""
|
||||
Vereinfachter Formular-Tester ohne Browser-Dependencies.
|
||||
Führt HTTP-basierte Tests durch.
|
||||
"""
|
||||
|
||||
def __init__(self, base_url: str):
|
||||
self.base_url = base_url.rstrip('/')
|
||||
self.session_cookies = {}
|
||||
|
||||
def fetch_page(self, path: str) -> str:
|
||||
"""Lädt eine Seite und gibt den HTML-Inhalt zurück"""
|
||||
url = urljoin(self.base_url, path)
|
||||
|
||||
try:
|
||||
request = urllib.request.Request(url)
|
||||
|
||||
# Cookies hinzufügen
|
||||
if self.session_cookies:
|
||||
cookie_header = '; '.join([f'{k}={v}' for k, v in self.session_cookies.items()])
|
||||
request.add_header('Cookie', cookie_header)
|
||||
|
||||
with urllib.request.urlopen(request) as response:
|
||||
html = response.read().decode('utf-8')
|
||||
|
||||
# Cookies aus Response extrahieren
|
||||
cookie_header = response.getheader('Set-Cookie')
|
||||
if cookie_header:
|
||||
self._parse_cookies(cookie_header)
|
||||
|
||||
return html
|
||||
|
||||
except urllib.error.URLError as e:
|
||||
print(f"❌ Fehler beim Laden von {url}: {e}")
|
||||
return ""
|
||||
|
||||
def _parse_cookies(self, cookie_header: str):
|
||||
"""Parst Set-Cookie Header"""
|
||||
for cookie in cookie_header.split(','):
|
||||
if '=' in cookie:
|
||||
name, value = cookie.split('=', 1)
|
||||
self.session_cookies[name.strip()] = value.split(';')[0].strip()
|
||||
|
||||
def find_forms(self, html: str) -> List[Form]:
|
||||
"""Findet alle Formulare in HTML"""
|
||||
parser = FormParser()
|
||||
parser.feed(html)
|
||||
return parser.forms
|
||||
|
||||
def generate_test_data(self, field: FormField) -> str:
|
||||
"""Generiert Test-Daten für ein Feld"""
|
||||
|
||||
if field.field_type == 'email':
|
||||
return "test@mercedes-benz.com"
|
||||
elif field.field_type == 'password':
|
||||
return "TestPassword123!"
|
||||
elif field.field_type == 'tel':
|
||||
return "+49 711 17-0"
|
||||
elif field.field_type == 'url':
|
||||
return "https://www.mercedes-benz.com"
|
||||
elif field.field_type == 'number':
|
||||
return "42"
|
||||
elif field.field_type == 'date':
|
||||
return "2024-06-18"
|
||||
elif field.field_type == 'checkbox':
|
||||
return "on"
|
||||
elif field.field_type == 'radio':
|
||||
return field.value or "option1"
|
||||
elif 'name' in field.name.lower():
|
||||
return "Test Benutzer"
|
||||
elif 'username' in field.name.lower():
|
||||
return "admin"
|
||||
elif 'ip' in field.name.lower():
|
||||
return "192.168.1.100"
|
||||
elif 'port' in field.name.lower():
|
||||
return "80"
|
||||
else:
|
||||
return f"Test_{field.name}"
|
||||
|
||||
def submit_form(self, form: Form, test_data: Dict[str, str], base_url: str) -> Dict:
|
||||
"""Sendet Formular-Daten und gibt Response zurück"""
|
||||
|
||||
# Action URL bestimmen
|
||||
if form.action.startswith('http'):
|
||||
url = form.action
|
||||
else:
|
||||
url = urljoin(base_url, form.action)
|
||||
|
||||
# Daten vorbereiten
|
||||
form_data = {}
|
||||
for field in form.fields:
|
||||
if field.name in test_data:
|
||||
form_data[field.name] = test_data[field.name]
|
||||
elif field.field_type not in ['submit', 'button']:
|
||||
form_data[field.name] = self.generate_test_data(field)
|
||||
|
||||
try:
|
||||
if form.method == 'GET':
|
||||
# GET-Request mit Query-Parametern
|
||||
query_string = urllib.parse.urlencode(form_data)
|
||||
full_url = f"{url}?{query_string}"
|
||||
request = urllib.request.Request(full_url)
|
||||
else:
|
||||
# POST-Request
|
||||
data = urllib.parse.urlencode(form_data).encode('utf-8')
|
||||
request = urllib.request.Request(url, data=data)
|
||||
request.add_header('Content-Type', 'application/x-www-form-urlencoded')
|
||||
|
||||
# Cookies hinzufügen
|
||||
if self.session_cookies:
|
||||
cookie_header = '; '.join([f'{k}={v}' for k, v in self.session_cookies.items()])
|
||||
request.add_header('Cookie', cookie_header)
|
||||
|
||||
with urllib.request.urlopen(request) as response:
|
||||
response_html = response.read().decode('utf-8')
|
||||
status_code = response.getcode()
|
||||
|
||||
return {
|
||||
'success': 200 <= status_code < 400,
|
||||
'status_code': status_code,
|
||||
'html': response_html,
|
||||
'form_data': form_data
|
||||
}
|
||||
|
||||
except urllib.error.HTTPError as e:
|
||||
return {
|
||||
'success': False,
|
||||
'status_code': e.code,
|
||||
'error': str(e),
|
||||
'form_data': form_data
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
'success': False,
|
||||
'error': str(e),
|
||||
'form_data': form_data
|
||||
}
|
||||
|
||||
def validate_field(self, field: FormField, value: str) -> Dict:
|
||||
"""Validiert ein Feld mit gegebenem Wert"""
|
||||
|
||||
errors = []
|
||||
|
||||
# Required-Validierung
|
||||
if field.required and not value.strip():
|
||||
errors.append(f"Feld '{field.name}' ist erforderlich")
|
||||
|
||||
# Pattern-Validierung
|
||||
if field.pattern and value:
|
||||
try:
|
||||
if not re.match(field.pattern, value):
|
||||
errors.append(f"Feld '{field.name}' entspricht nicht dem Pattern '{field.pattern}'")
|
||||
except re.error:
|
||||
errors.append(f"Ungültiges Pattern für Feld '{field.name}': {field.pattern}")
|
||||
|
||||
# Type-spezifische Validierung
|
||||
if value and field.field_type == 'email':
|
||||
email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
||||
if not re.match(email_pattern, value):
|
||||
errors.append(f"Ungültige Email-Adresse: {value}")
|
||||
|
||||
elif value and field.field_type == 'url':
|
||||
try:
|
||||
parsed = urlparse(value)
|
||||
if not parsed.scheme or not parsed.netloc:
|
||||
errors.append(f"Ungültige URL: {value}")
|
||||
except:
|
||||
errors.append(f"Ungültige URL: {value}")
|
||||
|
||||
elif value and field.field_type == 'number':
|
||||
try:
|
||||
float(value)
|
||||
except ValueError:
|
||||
errors.append(f"'{value}' ist keine gültige Zahl")
|
||||
|
||||
return {
|
||||
'valid': len(errors) == 0,
|
||||
'errors': errors,
|
||||
'field': field.name,
|
||||
'value': value
|
||||
}
|
||||
|
||||
def test_form_validation(self, form: Form) -> List[Dict]:
|
||||
"""Testet Formular-Validierung mit verschiedenen Daten"""
|
||||
|
||||
validation_results = []
|
||||
|
||||
# 1. Test mit leeren Required-Feldern
|
||||
for field in form.fields:
|
||||
if field.required:
|
||||
result = self.validate_field(field, "")
|
||||
validation_results.append({
|
||||
'test_type': 'required_field_empty',
|
||||
'field': field.name,
|
||||
'expected_invalid': True,
|
||||
'actual_valid': result['valid'],
|
||||
'passed': not result['valid'], # Sollte ungültig sein
|
||||
'errors': result['errors']
|
||||
})
|
||||
|
||||
# 2. Test mit ungültigen Email-Adressen
|
||||
for field in form.fields:
|
||||
if field.field_type == 'email':
|
||||
invalid_emails = ['invalid-email', 'test@', '@domain.com', 'test..test@domain.com']
|
||||
for invalid_email in invalid_emails:
|
||||
result = self.validate_field(field, invalid_email)
|
||||
validation_results.append({
|
||||
'test_type': 'invalid_email',
|
||||
'field': field.name,
|
||||
'value': invalid_email,
|
||||
'expected_invalid': True,
|
||||
'actual_valid': result['valid'],
|
||||
'passed': not result['valid'],
|
||||
'errors': result['errors']
|
||||
})
|
||||
|
||||
# 3. Test mit gültigen Daten
|
||||
valid_test_data = {}
|
||||
for field in form.fields:
|
||||
if field.field_type not in ['submit', 'button']:
|
||||
valid_test_data[field.name] = self.generate_test_data(field)
|
||||
|
||||
for field in form.fields:
|
||||
if field.name in valid_test_data:
|
||||
result = self.validate_field(field, valid_test_data[field.name])
|
||||
validation_results.append({
|
||||
'test_type': 'valid_data',
|
||||
'field': field.name,
|
||||
'value': valid_test_data[field.name],
|
||||
'expected_invalid': False,
|
||||
'actual_valid': result['valid'],
|
||||
'passed': result['valid'],
|
||||
'errors': result['errors']
|
||||
})
|
||||
|
||||
return validation_results
|
||||
|
||||
def test_form_submission(self, form: Form, base_url: str) -> Dict:
|
||||
"""Testet Formular-Submission"""
|
||||
|
||||
print(f"🧪 Teste Formular: {form.name} ({form.method} → {form.action})")
|
||||
|
||||
# Test-Daten generieren
|
||||
test_data = {}
|
||||
for field in form.fields:
|
||||
if field.field_type not in ['submit', 'button']:
|
||||
test_data[field.name] = self.generate_test_data(field)
|
||||
|
||||
print(f" 📝 Test-Daten: {list(test_data.keys())}")
|
||||
|
||||
# Formular senden
|
||||
start_time = time.time()
|
||||
result = self.submit_form(form, test_data, base_url)
|
||||
execution_time = time.time() - start_time
|
||||
|
||||
# Validierungs-Tests
|
||||
validation_results = self.test_form_validation(form)
|
||||
|
||||
return {
|
||||
'form_name': form.name,
|
||||
'method': form.method,
|
||||
'action': form.action,
|
||||
'fields_count': len(form.fields),
|
||||
'test_data': test_data,
|
||||
'submission_result': result,
|
||||
'validation_results': validation_results,
|
||||
'execution_time': execution_time,
|
||||
'timestamp': time.strftime('%Y-%m-%d %H:%M:%S')
|
||||
}
|
||||
|
||||
def test_page_forms(self, path: str) -> List[Dict]:
|
||||
"""Testet alle Formulare auf einer Seite"""
|
||||
|
||||
print(f"🔍 Scanne Formulare auf: {self.base_url}{path}")
|
||||
|
||||
# Seite laden
|
||||
html = self.fetch_page(path)
|
||||
if not html:
|
||||
return []
|
||||
|
||||
# Formulare finden
|
||||
forms = self.find_forms(html)
|
||||
print(f"✅ {len(forms)} Formulare gefunden")
|
||||
|
||||
# Jedes Formular testen
|
||||
results = []
|
||||
for form in forms:
|
||||
result = self.test_form_submission(form, self.base_url + path)
|
||||
results.append(result)
|
||||
|
||||
return results
|
||||
|
||||
def generate_report(self, test_results: List[Dict], output_file: str = "simple_form_test_report.html"):
|
||||
"""Generiert einen einfachen HTML-Report"""
|
||||
|
||||
total_forms = len(test_results)
|
||||
successful_submissions = len([r for r in test_results if r['submission_result'].get('success', False)])
|
||||
|
||||
# Validierungs-Statistiken
|
||||
total_validations = sum(len(r['validation_results']) for r in test_results)
|
||||
passed_validations = sum(len([v for v in r['validation_results'] if v['passed']]) for r in test_results)
|
||||
|
||||
html_report = f"""<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>MYP Formular Test Report</title>
|
||||
<style>
|
||||
body {{
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}}
|
||||
.container {{
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}}
|
||||
.header {{
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
margin: -20px -20px 20px -20px;
|
||||
border-radius: 8px 8px 0 0;
|
||||
}}
|
||||
.stats {{
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
margin-bottom: 30px;
|
||||
}}
|
||||
.stat-card {{
|
||||
background: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
border: 1px solid #dee2e6;
|
||||
}}
|
||||
.stat-number {{
|
||||
font-size: 2em;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}}
|
||||
.form-result {{
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 20px;
|
||||
overflow: hidden;
|
||||
}}
|
||||
.form-header {{
|
||||
background: #e9ecef;
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}}
|
||||
.form-content {{
|
||||
padding: 15px;
|
||||
}}
|
||||
.success {{ color: #28a745; }}
|
||||
.failure {{ color: #dc3545; }}
|
||||
.warning {{ color: #ffc107; }}
|
||||
.test-data {{
|
||||
background: #f8f9fa;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
margin: 10px 0;
|
||||
font-family: monospace;
|
||||
font-size: 0.9em;
|
||||
}}
|
||||
.validation-result {{
|
||||
margin: 5px 0;
|
||||
padding: 5px 10px;
|
||||
border-radius: 4px;
|
||||
border-left: 4px solid #dee2e6;
|
||||
}}
|
||||
.validation-passed {{
|
||||
background: #d4edda;
|
||||
border-left-color: #28a745;
|
||||
}}
|
||||
.validation-failed {{
|
||||
background: #f8d7da;
|
||||
border-left-color: #dc3545;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🧪 MYP Formular Test Report</h1>
|
||||
<p>Generiert am {time.strftime('%d.%m.%Y um %H:%M:%S')}</p>
|
||||
</div>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat-card">
|
||||
<div class="stat-number">{total_forms}</div>
|
||||
<div>Formulare getestet</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-number {'success' if successful_submissions == total_forms else 'warning' if successful_submissions > 0 else 'failure'}">{successful_submissions}</div>
|
||||
<div>Erfolgreiche Submissions</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-number">{total_validations}</div>
|
||||
<div>Validierungs-Tests</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-number {'success' if passed_validations == total_validations else 'warning' if passed_validations > 0 else 'failure'}">{passed_validations}</div>
|
||||
<div>Validierungen bestanden</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>📋 Detaillierte Ergebnisse</h2>
|
||||
"""
|
||||
|
||||
# Formular-spezifische Ergebnisse
|
||||
for result in test_results:
|
||||
submission = result['submission_result']
|
||||
success_class = 'success' if submission.get('success', False) else 'failure'
|
||||
|
||||
html_report += f"""
|
||||
<div class="form-result">
|
||||
<div class="form-header">
|
||||
<h3>📝 {result['form_name']}</h3>
|
||||
<p><strong>Method:</strong> {result['method']} | <strong>Action:</strong> {result['action']} | <strong>Felder:</strong> {result['fields_count']}</p>
|
||||
</div>
|
||||
<div class="form-content">
|
||||
<h4>Submission-Ergebnis: <span class="{success_class}">{'✅ Erfolgreich' if submission.get('success', False) else '❌ Fehlgeschlagen'}</span></h4>
|
||||
|
||||
{f'<p><strong>Status Code:</strong> {submission.get("status_code", "N/A")}</p>' if 'status_code' in submission else ''}
|
||||
{f'<p><strong>Fehler:</strong> {submission.get("error", "N/A")}</p>' if 'error' in submission else ''}
|
||||
|
||||
<h4>Test-Daten:</h4>
|
||||
<div class="test-data">{json.dumps(result['test_data'], indent=2, ensure_ascii=False)}</div>
|
||||
|
||||
<h4>Validierungs-Ergebnisse:</h4>
|
||||
"""
|
||||
|
||||
# Validierungs-Ergebnisse
|
||||
for validation in result['validation_results']:
|
||||
validation_class = 'validation-passed' if validation['passed'] else 'validation-failed'
|
||||
status_icon = '✅' if validation['passed'] else '❌'
|
||||
|
||||
html_report += f"""
|
||||
<div class="validation-result {validation_class}">
|
||||
<strong>{status_icon} {validation['test_type']}</strong> - Feld: {validation['field']}
|
||||
{f"<br>Wert: <code>{validation.get('value', 'N/A')}</code>" if 'value' in validation else ''}
|
||||
{f"<br>Fehler: {', '.join(validation['errors'])}" if validation['errors'] else ''}
|
||||
</div>
|
||||
"""
|
||||
|
||||
html_report += f"""
|
||||
<p><small>⏱️ Ausführungszeit: {result['execution_time']:.2f}s</small></p>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
html_report += """
|
||||
</div>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
try:
|
||||
with open(output_file, 'w', encoding='utf-8') as f:
|
||||
f.write(html_report)
|
||||
print(f"📊 Report erstellt: {output_file}")
|
||||
return output_file
|
||||
except Exception as e:
|
||||
print(f"❌ Fehler beim Report-Erstellen: {e}")
|
||||
return ""
|
||||
|
||||
|
||||
def main():
|
||||
"""Haupt-CLI-Interface"""
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
print("""
|
||||
🧪 Vereinfachter MYP Formular Tester
|
||||
|
||||
Verwendung:
|
||||
python simple_form_tester.py <base_url> [path]
|
||||
|
||||
Beispiele:
|
||||
python simple_form_tester.py http://localhost:5000
|
||||
python simple_form_tester.py http://localhost:5000 /login
|
||||
python simple_form_tester.py http://localhost:5000 /admin/add_printer
|
||||
|
||||
Testet alle Formulare auf der angegebenen Seite und generiert einen HTML-Report.
|
||||
""")
|
||||
return
|
||||
|
||||
base_url = sys.argv[1]
|
||||
path = sys.argv[2] if len(sys.argv) > 2 else '/'
|
||||
|
||||
print(f"""
|
||||
{'='*60}
|
||||
🧪 MYP FORMULAR TESTER (Vereinfacht)
|
||||
{'='*60}
|
||||
|
||||
🎯 Basis-URL: {base_url}
|
||||
📍 Pfad: {path}
|
||||
🕒 Gestartet: {time.strftime('%d.%m.%Y um %H:%M:%S')}
|
||||
|
||||
{'='*60}
|
||||
""")
|
||||
|
||||
# Tester initialisieren
|
||||
tester = SimpleFormTester(base_url)
|
||||
|
||||
# Tests ausführen
|
||||
try:
|
||||
results = tester.test_page_forms(path)
|
||||
|
||||
if results:
|
||||
# Report generieren
|
||||
report_file = tester.generate_report(results)
|
||||
|
||||
# Zusammenfassung
|
||||
total_forms = len(results)
|
||||
successful = len([r for r in results if r['submission_result'].get('success', False)])
|
||||
|
||||
print(f"""
|
||||
{'='*60}
|
||||
🎉 TESTS ABGESCHLOSSEN
|
||||
{'='*60}
|
||||
|
||||
📊 ZUSAMMENFASSUNG:
|
||||
• Formulare getestet: {total_forms}
|
||||
• Erfolgreiche Submissions: {successful}
|
||||
• Erfolgsrate: {successful/total_forms*100:.1f}%
|
||||
|
||||
📋 Report: {report_file}
|
||||
|
||||
{'='*60}
|
||||
""")
|
||||
else:
|
||||
print("⚠️ Keine Formulare gefunden oder Fehler beim Laden der Seite")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Fehler beim Testen: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
@ -336,11 +336,13 @@
|
||||
System-Info
|
||||
</a>
|
||||
<div class="border-t border-white/10 my-2"></div>
|
||||
<button onclick="handleLogout()"
|
||||
class="w-full flex items-center px-3 py-2 rounded-lg hover:bg-red-500/20 text-red-600 dark:text-red-400">
|
||||
<i class="fas fa-sign-out-alt w-4 mr-3"></i>
|
||||
Abmelden
|
||||
</button>
|
||||
<form method="POST" action="{{ url_for('auth.logout') }}" class="w-full">
|
||||
{{ csrf_token() }}
|
||||
<button type="submit" class="w-full flex items-center px-3 py-2 rounded-lg hover:bg-red-500/20 text-red-600 dark:text-red-400">
|
||||
<i class="fas fa-sign-out-alt w-4 mr-3"></i>
|
||||
Abmelden
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -438,16 +440,11 @@
|
||||
<div class="flex-1">
|
||||
<p class="text-sm font-medium">{{ message }}</p>
|
||||
</div>
|
||||
<button onclick="this.parentElement.parentElement.remove()" class="ml-3 hover:opacity-70">
|
||||
<button type="button" class="ml-3 hover:opacity-70 close-flash-btn">
|
||||
<i class="fas fa-times text-sm"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
setTimeout(() => {
|
||||
document.getElementById('flash-{{ loop.index }}')?.remove();
|
||||
}, 5000);
|
||||
</script>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
@ -727,12 +724,11 @@
|
||||
const payload = JSON.parse(notification.payload || '{}');
|
||||
return `
|
||||
<div class="mt-3 flex space-x-2">
|
||||
<button onclick="window.notificationManager.viewGuestRequest(${payload.request_id})"
|
||||
class="px-3 py-1 bg-blue-500 text-white text-xs rounded hover:bg-blue-600">
|
||||
<a href="/admin/guest-requests?highlight=${payload.request_id}"
|
||||
class="inline-block px-3 py-1 bg-blue-500 text-white text-xs rounded hover:bg-blue-600">
|
||||
Anzeigen
|
||||
</button>
|
||||
<button onclick="window.notificationManager.markAsRead(${notification.id})"
|
||||
class="px-3 py-1 bg-gray-500 text-white text-xs rounded hover:bg-gray-600">
|
||||
</a>
|
||||
<button data-notification-id="${notification.id}" class="mark-read-btn px-3 py-1 bg-gray-500 text-white text-xs rounded hover:bg-gray-600">
|
||||
Als gelesen markieren
|
||||
</button>
|
||||
</div>
|
||||
@ -748,10 +744,7 @@
|
||||
// Event Listeners werden über onclick direkt gesetzt
|
||||
}
|
||||
|
||||
async viewGuestRequest(requestId) {
|
||||
// Weiterleitung zur Admin-Gastanfragen-Seite
|
||||
window.location.href = `/admin/guest-requests?highlight=${requestId}`;
|
||||
}
|
||||
// Replaced by direct links in templates
|
||||
|
||||
async markAsRead(notificationId) {
|
||||
try {
|
||||
@ -862,26 +855,19 @@
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
// Logout Handler
|
||||
function handleLogout() {
|
||||
if (confirm('Möchten Sie sich wirklich abmelden?')) {
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = '{{ url_for("auth.logout") }}';
|
||||
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]');
|
||||
if (csrfToken) {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'hidden';
|
||||
input.name = 'csrf_token';
|
||||
input.value = csrfToken.getAttribute('content');
|
||||
form.appendChild(input);
|
||||
}
|
||||
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
}
|
||||
}
|
||||
// Flash-Message close buttons
|
||||
document.querySelectorAll('.close-flash-btn').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
this.parentElement.parentElement.remove();
|
||||
});
|
||||
});
|
||||
|
||||
// Auto-hide flash messages after 5 seconds
|
||||
document.querySelectorAll('[id^="flash-"]').forEach((flash, index) => {
|
||||
setTimeout(() => {
|
||||
flash?.remove();
|
||||
}, 5000 + (index * 500)); // Staggered removal
|
||||
});
|
||||
|
||||
// Smooth scroll for anchor links
|
||||
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
||||
|
@ -269,13 +269,13 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<button id="refreshDashboard"
|
||||
class="btn-secondary flex items-center gap-2">
|
||||
<a href="{{ url_for('dashboard') }}"
|
||||
class="btn-secondary flex items-center gap-2">
|
||||
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
<span>Aktualisieren</span>
|
||||
</button>
|
||||
</a>
|
||||
<a href="{{ url_for('jobs_page') }}"
|
||||
class="btn-primary flex items-center gap-2">
|
||||
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
@ -401,7 +401,7 @@
|
||||
<div class="text-xs text-right mt-1 text-slate-500 dark:text-slate-400">{{ job.progress }}%</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right">
|
||||
<a href="{{ url_for('api_get_job', job_id=job.id) }}" class="text-slate-900 dark:text-white hover:text-slate-700 dark:hover:text-slate-300 font-medium">Details</a>
|
||||
<a href="{{ url_for('jobs_page') }}#job-{{ job.id }}" class="text-slate-900 dark:text-white hover:text-slate-700 dark:hover:text-slate-300 font-medium">Details</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
@ -499,454 +499,51 @@
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
class DashboardManager {
|
||||
constructor() {
|
||||
this.updateInterval = 30000; // 30 Sekunden
|
||||
this.autoUpdateTimer = null;
|
||||
this.isUpdating = false;
|
||||
this.wsConnection = null;
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.setupEventListeners();
|
||||
this.setupAutoUpdate();
|
||||
this.setupWebSocket();
|
||||
this.animateCounters();
|
||||
console.log('🚀 Dashboard Manager initialisiert');
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Refresh Button
|
||||
const refreshBtn = document.getElementById('refreshDashboard');
|
||||
if (refreshBtn) {
|
||||
refreshBtn.addEventListener('click', () => {
|
||||
this.refreshDashboard();
|
||||
});
|
||||
}
|
||||
|
||||
// Job-Zeilen klickbar machen für Details
|
||||
this.setupJobRowClicks();
|
||||
|
||||
// Drucker-Karten klickbar machen
|
||||
this.setupPrinterClicks();
|
||||
|
||||
// Dark Mode Updates
|
||||
window.addEventListener('darkModeChanged', (e) => {
|
||||
this.updateThemeElements(e.detail.isDark);
|
||||
});
|
||||
|
||||
// Visibility Change Detection für intelligente Updates
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.hidden) {
|
||||
this.pauseAutoUpdate();
|
||||
} else {
|
||||
this.resumeAutoUpdate();
|
||||
this.refreshDashboard(); // Einmalige Aktualisierung bei Rückkehr
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setupJobRowClicks() {
|
||||
const jobRows = document.querySelectorAll('tbody tr[data-job-id]');
|
||||
jobRows.forEach(row => {
|
||||
row.style.cursor = 'pointer';
|
||||
row.addEventListener('click', () => {
|
||||
const jobId = row.dataset.jobId;
|
||||
if (jobId) {
|
||||
window.location.href = `/jobs/${jobId}`;
|
||||
}
|
||||
});
|
||||
|
||||
// Hover-Effekt verstärken
|
||||
row.addEventListener('mouseenter', () => {
|
||||
row.style.transform = 'scale(1.01)';
|
||||
row.style.transition = 'transform 0.2s ease';
|
||||
});
|
||||
|
||||
row.addEventListener('mouseleave', () => {
|
||||
row.style.transform = 'scale(1)';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setupPrinterClicks() {
|
||||
const printerCards = document.querySelectorAll('[data-printer-id]');
|
||||
printerCards.forEach(card => {
|
||||
card.style.cursor = 'pointer';
|
||||
card.addEventListener('click', () => {
|
||||
const printerId = card.dataset.printerId;
|
||||
if (printerId) {
|
||||
window.location.href = `/printers/${printerId}`;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setupAutoUpdate() {
|
||||
this.autoUpdateTimer = setInterval(() => {
|
||||
if (!document.hidden && !this.isUpdating) {
|
||||
this.updateDashboardData();
|
||||
}
|
||||
}, this.updateInterval);
|
||||
}
|
||||
|
||||
pauseAutoUpdate() {
|
||||
if (this.autoUpdateTimer) {
|
||||
clearInterval(this.autoUpdateTimer);
|
||||
this.autoUpdateTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
resumeAutoUpdate() {
|
||||
if (!this.autoUpdateTimer) {
|
||||
this.setupAutoUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
setupWebSocket() {
|
||||
// WebSocket für Real-time Updates
|
||||
try {
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsUrl = `${protocol}//${window.location.host}/ws/dashboard`;
|
||||
|
||||
this.wsConnection = new WebSocket(wsUrl);
|
||||
|
||||
this.wsConnection.onopen = () => {
|
||||
console.log('📡 WebSocket Verbindung zu Dashboard hergestellt');
|
||||
};
|
||||
|
||||
this.wsConnection.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
this.handleWebSocketUpdate(data);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Verarbeiten der WebSocket-Nachricht:', error);
|
||||
}
|
||||
};
|
||||
|
||||
this.wsConnection.onclose = () => {
|
||||
console.log('📡 WebSocket Verbindung geschlossen - versuche Wiederverbindung in 5s');
|
||||
setTimeout(() => {
|
||||
this.setupWebSocket();
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
this.wsConnection.onerror = (error) => {
|
||||
console.error('WebSocket Fehler:', error);
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.log('WebSocket nicht verfügbar, verwende Polling-Updates');
|
||||
}
|
||||
}
|
||||
|
||||
handleWebSocketUpdate(data) {
|
||||
console.log('📡 Erhaltenes WebSocket-Update:', data);
|
||||
|
||||
switch (data.type) {
|
||||
case 'job_status_update':
|
||||
this.updateJobStatus(data.job_id, data.status, data.progress);
|
||||
break;
|
||||
case 'printer_status_update':
|
||||
this.updatePrinterStatus(data.printer_id, data.status);
|
||||
break;
|
||||
case 'new_activity':
|
||||
this.addNewActivity(data.activity);
|
||||
break;
|
||||
case 'stats_update':
|
||||
this.updateStats(data.stats);
|
||||
break;
|
||||
default:
|
||||
console.log('Unbekannter WebSocket-Update-Typ:', data.type);
|
||||
}
|
||||
}
|
||||
|
||||
async refreshDashboard() {
|
||||
if (this.isUpdating) return;
|
||||
|
||||
this.isUpdating = true;
|
||||
const refreshBtn = document.getElementById('refreshDashboard');
|
||||
|
||||
try {
|
||||
// Button-Status aktualisieren
|
||||
if (refreshBtn) {
|
||||
refreshBtn.disabled = true;
|
||||
refreshBtn.innerHTML = `
|
||||
<svg class="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
<span>Aktualisiert...</span>
|
||||
`;
|
||||
}
|
||||
|
||||
// Vollständige Seitenaktualisierung
|
||||
window.location.reload();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Aktualisieren:', error);
|
||||
this.showToast('Fehler beim Aktualisieren des Dashboards', 'error');
|
||||
} finally {
|
||||
this.isUpdating = false;
|
||||
|
||||
// Button zurücksetzen
|
||||
if (refreshBtn) {
|
||||
refreshBtn.disabled = false;
|
||||
refreshBtn.innerHTML = `
|
||||
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
<span>Aktualisieren</span>
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async updateDashboardData() {
|
||||
if (this.isUpdating) return;
|
||||
|
||||
this.isUpdating = true;
|
||||
|
||||
try {
|
||||
// Stille Hintergrund-Updates ohne vollständige Neuladen
|
||||
const responses = await Promise.all([
|
||||
fetch('/api/dashboard/stats'),
|
||||
fetch('/api/dashboard/active-jobs'),
|
||||
fetch('/api/dashboard/printers'),
|
||||
fetch('/api/dashboard/activities')
|
||||
]);
|
||||
|
||||
const [statsData, jobsData, printersData, activitiesData] = await Promise.all(
|
||||
responses.map(r => r.json())
|
||||
);
|
||||
|
||||
// UI-Updates
|
||||
this.updateStats(statsData);
|
||||
this.updateActiveJobs(jobsData);
|
||||
this.updatePrinters(printersData);
|
||||
this.updateActivities(activitiesData);
|
||||
|
||||
console.log('🔄 Dashboard-Daten erfolgreich aktualisiert');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Dashboard-Daten:', error);
|
||||
// Fehlschlag stumm - verwende WebSocket oder warte auf nächste Aktualisierung
|
||||
} finally {
|
||||
this.isUpdating = false;
|
||||
}
|
||||
}
|
||||
|
||||
updateJobStatus(jobId, status, progress) {
|
||||
const jobRow = document.querySelector(`tr[data-job-id="${jobId}"]`);
|
||||
if (jobRow) {
|
||||
// Status-Indikator aktualisieren
|
||||
const statusIndicator = jobRow.querySelector('.mb-status-indicator');
|
||||
const statusText = jobRow.querySelector('.text-sm.font-medium');
|
||||
const progressBar = jobRow.querySelector('.mb-progress-bar');
|
||||
const progressText = jobRow.querySelector('.text-xs.text-right');
|
||||
|
||||
if (statusIndicator) {
|
||||
statusIndicator.className = `mb-status-indicator ${this.getStatusClass(status)}`;
|
||||
}
|
||||
|
||||
if (statusText) {
|
||||
statusText.textContent = this.getStatusText(status);
|
||||
}
|
||||
|
||||
if (progressBar && progress !== undefined) {
|
||||
progressBar.style.width = `${progress}%`;
|
||||
}
|
||||
|
||||
if (progressText && progress !== undefined) {
|
||||
progressText.textContent = `${progress}%`;
|
||||
}
|
||||
|
||||
// Animation für Updates
|
||||
jobRow.style.backgroundColor = 'rgba(59, 130, 246, 0.1)';
|
||||
setTimeout(() => {
|
||||
jobRow.style.backgroundColor = '';
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
updatePrinterStatus(printerId, status) {
|
||||
const printerCard = document.querySelector(`[data-printer-id="${printerId}"]`);
|
||||
if (printerCard) {
|
||||
const statusIndicator = printerCard.querySelector('.mb-status-indicator');
|
||||
const statusText = printerCard.querySelector('.text-sm.font-medium:last-child');
|
||||
|
||||
if (statusIndicator) {
|
||||
statusIndicator.className = `mb-status-indicator ${this.getStatusClass(status)}`;
|
||||
}
|
||||
|
||||
if (statusText) {
|
||||
statusText.textContent = this.getStatusText(status);
|
||||
}
|
||||
|
||||
// Animation für Updates
|
||||
printerCard.style.transform = 'scale(1.02)';
|
||||
setTimeout(() => {
|
||||
printerCard.style.transform = 'scale(1)';
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
addNewActivity(activity) {
|
||||
const activitiesContainer = document.querySelector('.space-y-3');
|
||||
if (activitiesContainer) {
|
||||
const newActivity = document.createElement('div');
|
||||
newActivity.className = 'mb-activity-item pl-4 py-3 opacity-0';
|
||||
newActivity.innerHTML = `
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1">
|
||||
<p class="text-sm text-slate-700 dark:text-slate-300">${activity.description}</p>
|
||||
<p class="text-xs text-slate-500 dark:text-slate-400">${activity.time}</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Am Anfang einfügen
|
||||
activitiesContainer.insertBefore(newActivity, activitiesContainer.firstChild);
|
||||
|
||||
// Animation
|
||||
setTimeout(() => {
|
||||
newActivity.style.opacity = '1';
|
||||
newActivity.style.transition = 'opacity 0.5s ease';
|
||||
}, 100);
|
||||
|
||||
// Alte Aktivitäten entfernen (max 10)
|
||||
const activities = activitiesContainer.querySelectorAll('.mb-activity-item');
|
||||
if (activities.length > 10) {
|
||||
activities[activities.length - 1].remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateStats(stats) {
|
||||
// Statistik-Karten aktualisieren mit Animation
|
||||
const statValues = document.querySelectorAll('.stat-value');
|
||||
|
||||
const statsMapping = [
|
||||
{ element: statValues[0], value: stats.active_jobs_count },
|
||||
{ element: statValues[1], value: stats.available_printers_count },
|
||||
{ element: statValues[2], value: stats.total_jobs_count },
|
||||
{ element: statValues[3], value: `${stats.success_rate}%` }
|
||||
];
|
||||
|
||||
statsMapping.forEach(({ element, value }) => {
|
||||
if (element && element.textContent !== value.toString()) {
|
||||
this.animateValueChange(element, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
animateValueChange(element, newValue) {
|
||||
element.style.transform = 'scale(1.1)';
|
||||
element.style.transition = 'transform 0.3s ease';
|
||||
|
||||
setTimeout(() => {
|
||||
element.textContent = newValue;
|
||||
element.style.transform = 'scale(1)';
|
||||
}, 150);
|
||||
}
|
||||
|
||||
animateCounters() {
|
||||
// Initialanimation für Counter
|
||||
const counters = document.querySelectorAll('.stat-value');
|
||||
counters.forEach((counter, index) => {
|
||||
const finalValue = parseInt(counter.textContent) || 0;
|
||||
if (finalValue > 0) {
|
||||
let currentValue = 0;
|
||||
const increment = finalValue / 30; // 30 Schritte
|
||||
|
||||
const timer = setInterval(() => {
|
||||
currentValue += increment;
|
||||
if (currentValue >= finalValue) {
|
||||
counter.textContent = finalValue + (counter.textContent.includes('%') ? '%' : '');
|
||||
clearInterval(timer);
|
||||
} else {
|
||||
counter.textContent = Math.floor(currentValue) + (counter.textContent.includes('%') ? '%' : '');
|
||||
}
|
||||
}, 50 + (index * 100)); // Verzögerung zwischen Countern
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getStatusClass(status) {
|
||||
const statusClasses = {
|
||||
'running': 'mb-status-busy',
|
||||
'completed': 'mb-status-online',
|
||||
'failed': 'mb-status-offline',
|
||||
'paused': 'mb-status-idle',
|
||||
'queued': 'mb-status-idle',
|
||||
'online': 'mb-status-online',
|
||||
'offline': 'mb-status-offline',
|
||||
'busy': 'mb-status-busy',
|
||||
'idle': 'mb-status-idle'
|
||||
};
|
||||
return statusClasses[status] || 'mb-status-idle';
|
||||
}
|
||||
|
||||
getStatusText(status) {
|
||||
const statusTexts = {
|
||||
'running': 'Druckt',
|
||||
'completed': 'Abgeschlossen',
|
||||
'failed': 'Fehlgeschlagen',
|
||||
'paused': 'Pausiert',
|
||||
'queued': 'Warteschlange',
|
||||
'online': 'Online',
|
||||
'offline': 'Offline',
|
||||
'busy': 'Beschäftigt',
|
||||
'idle': 'Bereit'
|
||||
};
|
||||
return statusTexts[status] || 'Unbekannt';
|
||||
}
|
||||
|
||||
updateThemeElements(isDark) {
|
||||
// Theme-spezifische Updates
|
||||
console.log(`🎨 Dashboard Theme aktualisiert: ${isDark ? 'Dark' : 'Light'} Mode`);
|
||||
|
||||
// Spezielle Animationen für Theme-Wechsel
|
||||
const cards = document.querySelectorAll('.dashboard-card');
|
||||
cards.forEach(card => {
|
||||
card.style.transition = 'all 0.3s ease';
|
||||
});
|
||||
}
|
||||
|
||||
showToast(message, type = 'info') {
|
||||
// Toast-Benachrichtigung anzeigen
|
||||
if (window.MYP && window.MYP.UI && window.MYP.UI.ToastManager) {
|
||||
const toast = new window.MYP.UI.ToastManager();
|
||||
toast.show(message, type, 5000);
|
||||
} else {
|
||||
console.log(`Toast: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup beim Verlassen der Seite
|
||||
cleanup() {
|
||||
if (this.autoUpdateTimer) {
|
||||
clearInterval(this.autoUpdateTimer);
|
||||
}
|
||||
|
||||
if (this.wsConnection) {
|
||||
this.wsConnection.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dashboard Manager initialisieren
|
||||
// Vereinfachtes Dashboard mit minimaler JavaScript-Abhängigkeit
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
window.dashboardManager = new DashboardManager();
|
||||
|
||||
// Cleanup beim Verlassen
|
||||
window.addEventListener('beforeunload', () => {
|
||||
if (window.dashboardManager) {
|
||||
window.dashboardManager.cleanup();
|
||||
}
|
||||
// Klickbare Drucker-Karten
|
||||
const printerCards = document.querySelectorAll('[data-printer-id]');
|
||||
printerCards.forEach(card => {
|
||||
card.style.cursor = 'pointer';
|
||||
card.addEventListener('click', () => {
|
||||
const printerId = card.dataset.printerId;
|
||||
if (printerId) {
|
||||
window.location.href = `/printers/${printerId}`;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Job-Zeilen klickbar machen
|
||||
const jobRows = document.querySelectorAll('tbody tr[data-job-id]');
|
||||
jobRows.forEach(row => {
|
||||
row.style.cursor = 'pointer';
|
||||
row.addEventListener('click', () => {
|
||||
const jobId = row.dataset.jobId;
|
||||
if (jobId) {
|
||||
window.location.href = `/jobs#job-${jobId}`;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Einfache Hover-Effekte
|
||||
const cards = document.querySelectorAll('.dashboard-card');
|
||||
cards.forEach(card => {
|
||||
card.addEventListener('mouseenter', () => {
|
||||
card.style.transform = 'translateY(-2px)';
|
||||
});
|
||||
card.addEventListener('mouseleave', () => {
|
||||
card.style.transform = 'translateY(0)';
|
||||
});
|
||||
});
|
||||
|
||||
console.log('✅ Vereinfachtes Dashboard geladen');
|
||||
});
|
||||
|
||||
// Auto-Refresh alle 60 Sekunden (optional)
|
||||
{% if config.get('AUTO_REFRESH_DASHBOARD', False) %}
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 60000);
|
||||
{% endif %}
|
||||
</script>
|
||||
{% endblock %}
|
183
backend/templates/macros/ui_components.html
Normal file
183
backend/templates/macros/ui_components.html
Normal file
@ -0,0 +1,183 @@
|
||||
{# Jinja-Makros für UI-Komponenten zur JavaScript-Ersetzung #}
|
||||
|
||||
{# Status-Indikator mit CSS-Animation #}
|
||||
{% macro status_indicator(status, text="") %}
|
||||
{% set status_classes = {
|
||||
'online': 'mb-status-online',
|
||||
'offline': 'mb-status-offline',
|
||||
'busy': 'mb-status-busy',
|
||||
'idle': 'mb-status-idle',
|
||||
'running': 'mb-status-busy',
|
||||
'completed': 'mb-status-online',
|
||||
'failed': 'mb-status-offline',
|
||||
'paused': 'mb-status-idle',
|
||||
'queued': 'mb-status-idle'
|
||||
} %}
|
||||
<div class="flex items-center">
|
||||
<div class="mb-status-indicator {{ status_classes.get(status, 'mb-status-idle') }}"></div>
|
||||
{% if text %}
|
||||
<span class="ml-2 text-sm font-medium text-slate-700 dark:text-slate-300">{{ text }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{# Fortschrittsbalken mit CSS-Animation #}
|
||||
{% macro progress_bar(progress, show_text=True) %}
|
||||
<div class="mb-progress-container">
|
||||
<div class="mb-progress-bar" style="width: {{ progress }}%"></div>
|
||||
</div>
|
||||
{% if show_text %}
|
||||
<div class="text-xs text-right mt-1 text-slate-500 dark:text-slate-400">{{ progress }}%</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{# Klickbare Karte #}
|
||||
{% macro clickable_card(url, class="dashboard-card p-6") %}
|
||||
<a href="{{ url }}" class="{{ class }} block hover:transform hover:-translate-y-1 transition-transform">
|
||||
{{ caller() }}
|
||||
</a>
|
||||
{% endmacro %}
|
||||
|
||||
{# Tab-Navigation mit serverseitiger Logik #}
|
||||
{% macro tab_navigation(tabs, active_tab) %}
|
||||
<div class="border-b border-gray-200 dark:border-slate-700">
|
||||
<nav class="-mb-px flex space-x-8" aria-label="Tabs">
|
||||
{% for tab in tabs %}
|
||||
<a href="{{ tab.url }}"
|
||||
class="{% if active_tab == tab.id %}border-blue-500 text-blue-600 dark:text-blue-400{% else %}border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-slate-400 dark:hover:text-slate-300{% endif %}
|
||||
whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm">
|
||||
{% if tab.icon %}
|
||||
<i class="{{ tab.icon }} mr-2"></i>
|
||||
{% endif %}
|
||||
{{ tab.name }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</nav>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{# Drucker-Status-Karte ohne JavaScript #}
|
||||
{% macro printer_card(printer, show_link=True) %}
|
||||
{% if show_link %}
|
||||
<a href="{{ url_for('printers_page') }}#printer-{{ printer.id }}" class="block">
|
||||
{% endif %}
|
||||
<div class="flex items-center justify-between p-4 rounded-xl bg-gray-50 dark:bg-slate-700/30 {% if show_link %}hover:bg-gray-100 dark:hover:bg-slate-700/50 transition-colors{% endif %}">
|
||||
<div class="flex items-center">
|
||||
{{ status_indicator(printer.status, printer.status_text) }}
|
||||
<div class="ml-3">
|
||||
<div class="text-sm font-medium text-slate-900 dark:text-white">{{ printer.name }}</div>
|
||||
<div class="text-xs text-slate-500 dark:text-slate-400">{{ printer.model }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class="text-sm font-medium text-slate-700 dark:text-slate-300">{{ printer.status_text }}</div>
|
||||
<div class="text-xs text-slate-500 dark:text-slate-400">{{ printer.location }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if show_link %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{# Job-Zeile in Tabelle ohne JavaScript #}
|
||||
{% macro job_row(job, show_link=True) %}
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-slate-700/50 {% if show_link %}cursor-pointer{% endif %}">
|
||||
{% if show_link %}
|
||||
<td colspan="6" class="p-0">
|
||||
<a href="{{ url_for('jobs_page') }}#job-{{ job.id }}" class="block px-6 py-4">
|
||||
{{ job_row_content(job) }}
|
||||
</a>
|
||||
</td>
|
||||
{% else %}
|
||||
{{ job_row_content(job) }}
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endmacro %}
|
||||
|
||||
{# Job-Zeilen-Inhalt (für Wiederverwendung) #}
|
||||
{% macro job_row_content(job) %}
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
{{ status_indicator(job.status, job.status_text) }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm font-medium text-slate-900 dark:text-white">{{ job.name }}</div>
|
||||
<div class="text-xs text-slate-500 dark:text-slate-400">{{ job.file_name }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm text-slate-700 dark:text-slate-300">{{ job.printer }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm text-slate-700 dark:text-slate-300">{{ job.start_time }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
{{ progress_bar(job.progress) }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right">
|
||||
<span class="text-slate-900 dark:text-white hover:text-slate-700 dark:hover:text-slate-300 font-medium">Details</span>
|
||||
</td>
|
||||
{% endmacro %}
|
||||
|
||||
{# Benachrichtigungs-Toast mit Auto-Close via CSS #}
|
||||
{% macro notification_toast(message, type="info", auto_close=True) %}
|
||||
{% set type_classes = {
|
||||
'success': 'border-green-200 bg-green-50 text-green-800 dark:border-green-800 dark:bg-green-900/20 dark:text-green-300',
|
||||
'error': 'border-red-200 bg-red-50 text-red-800 dark:border-red-800 dark:bg-red-900/20 dark:text-red-300',
|
||||
'warning': 'border-yellow-200 bg-yellow-50 text-yellow-800 dark:border-yellow-800 dark:bg-yellow-900/20 dark:text-yellow-300',
|
||||
'info': 'border-blue-200 bg-blue-50 text-blue-800 dark:border-blue-800 dark:bg-blue-900/20 dark:text-blue-300'
|
||||
} %}
|
||||
<div class="glass rounded-lg p-4 border {{ type_classes.get(type, type_classes['info']) }} {% if auto_close %}animate-toast{% endif %}">
|
||||
<div class="flex items-start">
|
||||
<i class="fas {{ 'fa-check-circle' if type == 'success' else 'fa-exclamation-circle' if type == 'error' else 'fa-exclamation-triangle' if type == 'warning' else 'fa-info-circle' }} mt-0.5 mr-3"></i>
|
||||
<div class="flex-1">
|
||||
<p class="text-sm font-medium">{{ message }}</p>
|
||||
</div>
|
||||
<form method="POST" class="ml-3">
|
||||
{{ csrf_token() }}
|
||||
<button type="submit" name="dismiss_notification" class="hover:opacity-70">
|
||||
<i class="fas fa-times text-sm"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{# Formular-Submit-Button mit Loading-State #}
|
||||
{% macro submit_button(text, loading_text="Wird verarbeitet...", class="btn-primary") %}
|
||||
<button type="submit" class="{{ class }} relative" id="submit-btn">
|
||||
<span class="submit-text">{{ text }}</span>
|
||||
<span class="loading-text hidden">{{ loading_text }}</span>
|
||||
</button>
|
||||
<script>
|
||||
document.getElementById('submit-btn').form.addEventListener('submit', function() {
|
||||
const btn = document.getElementById('submit-btn');
|
||||
btn.disabled = true;
|
||||
btn.querySelector('.submit-text').classList.add('hidden');
|
||||
btn.querySelector('.loading-text').classList.remove('hidden');
|
||||
});
|
||||
</script>
|
||||
{% endmacro %}
|
||||
|
||||
{# Auto-Refresh Meta-Tag für periodische Seitenaktualisierung #}
|
||||
{% macro auto_refresh(seconds) %}
|
||||
<meta http-equiv="refresh" content="{{ seconds }}">
|
||||
{% endmacro %}
|
||||
|
||||
{# CSS-only Dropdown-Menu #}
|
||||
{% macro css_dropdown(button_text, items, button_class="btn-secondary") %}
|
||||
<div class="relative group">
|
||||
<button class="{{ button_class }}">
|
||||
{{ button_text }}
|
||||
<i class="fas fa-chevron-down ml-2"></i>
|
||||
</button>
|
||||
<div class="absolute right-0 mt-2 w-48 glass rounded-xl overflow-hidden opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 z-50">
|
||||
{% for item in items %}
|
||||
<a href="{{ item.url }}" class="block px-4 py-2 text-sm hover:bg-white/10 dark:hover:bg-black/10">
|
||||
{% if item.icon %}
|
||||
<i class="{{ item.icon }} w-4 mr-3"></i>
|
||||
{% endif %}
|
||||
{{ item.text }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
Reference in New Issue
Block a user