📝 'feat(database): Implement database backup on demand and immediate'
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
backend/app/database/myp.db.backup_20250529_150542
Normal file
BIN
backend/app/database/myp.db.backup_20250529_150542
Normal file
Binary file not shown.
BIN
backend/app/database/myp.db.backup_immediate_20250529_150732
Normal file
BIN
backend/app/database/myp.db.backup_immediate_20250529_150732
Normal file
Binary file not shown.
@@ -1 +1,380 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Umfassendes Datenbank-Schema-Migrationsskript
|
||||||
|
Erkennt und fügt alle fehlenden Spalten basierend auf den Models hinzu.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import sqlite3
|
||||||
|
from datetime import datetime
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# Pfad zur App hinzufügen
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
from config.settings import DATABASE_PATH
|
||||||
|
from utils.logging_config import get_logger
|
||||||
|
|
||||||
|
logger = get_logger("schema_migration")
|
||||||
|
|
||||||
|
def get_table_columns(cursor, table_name):
|
||||||
|
"""Ermittelt alle Spalten einer Tabelle."""
|
||||||
|
cursor.execute(f"PRAGMA table_info({table_name})")
|
||||||
|
return {row[1]: row[2] for row in cursor.fetchall()} # {column_name: column_type}
|
||||||
|
|
||||||
|
def get_table_exists(cursor, table_name):
|
||||||
|
"""Prüft, ob eine Tabelle existiert."""
|
||||||
|
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name=?", (table_name,))
|
||||||
|
return cursor.fetchone() is not None
|
||||||
|
|
||||||
|
def migrate_users_table(cursor):
|
||||||
|
"""Migriert die users Tabelle für fehlende Spalten."""
|
||||||
|
logger.info("Migriere users Tabelle...")
|
||||||
|
|
||||||
|
if not get_table_exists(cursor, 'users'):
|
||||||
|
logger.warning("users Tabelle existiert nicht - wird bei init_db erstellt")
|
||||||
|
return False
|
||||||
|
|
||||||
|
existing_columns = get_table_columns(cursor, 'users')
|
||||||
|
|
||||||
|
# Definition der erwarteten Spalten
|
||||||
|
required_columns = {
|
||||||
|
'id': 'INTEGER PRIMARY KEY',
|
||||||
|
'email': 'VARCHAR(120) UNIQUE NOT NULL',
|
||||||
|
'username': 'VARCHAR(100) UNIQUE NOT NULL',
|
||||||
|
'password_hash': 'VARCHAR(128) NOT NULL',
|
||||||
|
'name': 'VARCHAR(100) NOT NULL',
|
||||||
|
'role': 'VARCHAR(20) DEFAULT "user"',
|
||||||
|
'active': 'BOOLEAN DEFAULT 1',
|
||||||
|
'created_at': 'DATETIME DEFAULT CURRENT_TIMESTAMP',
|
||||||
|
'last_login': 'DATETIME',
|
||||||
|
'updated_at': 'DATETIME DEFAULT CURRENT_TIMESTAMP',
|
||||||
|
'settings': 'TEXT',
|
||||||
|
'department': 'VARCHAR(100)',
|
||||||
|
'position': 'VARCHAR(100)',
|
||||||
|
'phone': 'VARCHAR(50)',
|
||||||
|
'bio': 'TEXT'
|
||||||
|
}
|
||||||
|
|
||||||
|
migrations_performed = []
|
||||||
|
|
||||||
|
for column_name, column_def in required_columns.items():
|
||||||
|
if column_name not in existing_columns:
|
||||||
|
try:
|
||||||
|
# Spezielle Behandlung für updated_at mit Trigger
|
||||||
|
if column_name == 'updated_at':
|
||||||
|
cursor.execute(f"ALTER TABLE users ADD COLUMN {column_name} DATETIME DEFAULT CURRENT_TIMESTAMP")
|
||||||
|
# Trigger für automatische Aktualisierung
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TRIGGER IF NOT EXISTS update_users_updated_at
|
||||||
|
AFTER UPDATE ON users
|
||||||
|
BEGIN
|
||||||
|
UPDATE users SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
|
||||||
|
END
|
||||||
|
""")
|
||||||
|
logger.info(f"Spalte '{column_name}' hinzugefügt mit Auto-Update-Trigger")
|
||||||
|
else:
|
||||||
|
cursor.execute(f"ALTER TABLE users ADD COLUMN {column_name} {column_def}")
|
||||||
|
logger.info(f"Spalte '{column_name}' hinzugefügt")
|
||||||
|
|
||||||
|
migrations_performed.append(column_name)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Fehler beim Hinzufügen der Spalte '{column_name}': {str(e)}")
|
||||||
|
|
||||||
|
return len(migrations_performed) > 0
|
||||||
|
|
||||||
|
def migrate_printers_table(cursor):
|
||||||
|
"""Migriert die printers Tabelle für fehlende Spalten."""
|
||||||
|
logger.info("Migriere printers Tabelle...")
|
||||||
|
|
||||||
|
if not get_table_exists(cursor, 'printers'):
|
||||||
|
logger.warning("printers Tabelle existiert nicht - wird bei init_db erstellt")
|
||||||
|
return False
|
||||||
|
|
||||||
|
existing_columns = get_table_columns(cursor, 'printers')
|
||||||
|
|
||||||
|
required_columns = {
|
||||||
|
'id': 'INTEGER PRIMARY KEY',
|
||||||
|
'name': 'VARCHAR(100) NOT NULL',
|
||||||
|
'model': 'VARCHAR(100)',
|
||||||
|
'location': 'VARCHAR(100)',
|
||||||
|
'ip_address': 'VARCHAR(50)',
|
||||||
|
'mac_address': 'VARCHAR(50) NOT NULL UNIQUE',
|
||||||
|
'plug_ip': 'VARCHAR(50) NOT NULL',
|
||||||
|
'plug_username': 'VARCHAR(100) NOT NULL',
|
||||||
|
'plug_password': 'VARCHAR(100) NOT NULL',
|
||||||
|
'status': 'VARCHAR(20) DEFAULT "offline"',
|
||||||
|
'active': 'BOOLEAN DEFAULT 1',
|
||||||
|
'created_at': 'DATETIME DEFAULT CURRENT_TIMESTAMP',
|
||||||
|
'last_checked': 'DATETIME'
|
||||||
|
}
|
||||||
|
|
||||||
|
migrations_performed = []
|
||||||
|
|
||||||
|
for column_name, column_def in required_columns.items():
|
||||||
|
if column_name not in existing_columns:
|
||||||
|
try:
|
||||||
|
cursor.execute(f"ALTER TABLE printers ADD COLUMN {column_name} {column_def}")
|
||||||
|
logger.info(f"Spalte '{column_name}' zu printers hinzugefügt")
|
||||||
|
migrations_performed.append(column_name)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Fehler beim Hinzufügen der Spalte '{column_name}' zu printers: {str(e)}")
|
||||||
|
|
||||||
|
return len(migrations_performed) > 0
|
||||||
|
|
||||||
|
def migrate_jobs_table(cursor):
|
||||||
|
"""Migriert die jobs Tabelle für fehlende Spalten."""
|
||||||
|
logger.info("Migriere jobs Tabelle...")
|
||||||
|
|
||||||
|
if not get_table_exists(cursor, 'jobs'):
|
||||||
|
logger.warning("jobs Tabelle existiert nicht - wird bei init_db erstellt")
|
||||||
|
return False
|
||||||
|
|
||||||
|
existing_columns = get_table_columns(cursor, 'jobs')
|
||||||
|
|
||||||
|
required_columns = {
|
||||||
|
'id': 'INTEGER PRIMARY KEY',
|
||||||
|
'name': 'VARCHAR(200) NOT NULL',
|
||||||
|
'description': 'VARCHAR(500)',
|
||||||
|
'user_id': 'INTEGER NOT NULL',
|
||||||
|
'printer_id': 'INTEGER NOT NULL',
|
||||||
|
'start_at': 'DATETIME',
|
||||||
|
'end_at': 'DATETIME',
|
||||||
|
'actual_end_time': 'DATETIME',
|
||||||
|
'status': 'VARCHAR(20) DEFAULT "scheduled"',
|
||||||
|
'created_at': 'DATETIME DEFAULT CURRENT_TIMESTAMP',
|
||||||
|
'notes': 'VARCHAR(500)',
|
||||||
|
'material_used': 'FLOAT',
|
||||||
|
'file_path': 'VARCHAR(500)',
|
||||||
|
'owner_id': 'INTEGER',
|
||||||
|
'duration_minutes': 'INTEGER NOT NULL'
|
||||||
|
}
|
||||||
|
|
||||||
|
migrations_performed = []
|
||||||
|
|
||||||
|
for column_name, column_def in required_columns.items():
|
||||||
|
if column_name not in existing_columns:
|
||||||
|
try:
|
||||||
|
cursor.execute(f"ALTER TABLE jobs ADD COLUMN {column_name} {column_def}")
|
||||||
|
logger.info(f"Spalte '{column_name}' zu jobs hinzugefügt")
|
||||||
|
migrations_performed.append(column_name)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Fehler beim Hinzufügen der Spalte '{column_name}' zu jobs: {str(e)}")
|
||||||
|
|
||||||
|
return len(migrations_performed) > 0
|
||||||
|
|
||||||
|
def migrate_guest_requests_table(cursor):
|
||||||
|
"""Migriert die guest_requests Tabelle für fehlende Spalten."""
|
||||||
|
logger.info("Migriere guest_requests Tabelle...")
|
||||||
|
|
||||||
|
if not get_table_exists(cursor, 'guest_requests'):
|
||||||
|
logger.warning("guest_requests Tabelle existiert nicht - wird bei init_db erstellt")
|
||||||
|
return False
|
||||||
|
|
||||||
|
existing_columns = get_table_columns(cursor, 'guest_requests')
|
||||||
|
|
||||||
|
required_columns = {
|
||||||
|
'processed_by': 'INTEGER',
|
||||||
|
'processed_at': 'DATETIME',
|
||||||
|
'approval_notes': 'TEXT',
|
||||||
|
'rejection_reason': 'TEXT',
|
||||||
|
'otp_used_at': 'DATETIME'
|
||||||
|
}
|
||||||
|
|
||||||
|
migrations_performed = []
|
||||||
|
|
||||||
|
for column_name, column_def in required_columns.items():
|
||||||
|
if column_name not in existing_columns:
|
||||||
|
try:
|
||||||
|
cursor.execute(f"ALTER TABLE guest_requests ADD COLUMN {column_name} {column_def}")
|
||||||
|
logger.info(f"Spalte '{column_name}' zu guest_requests hinzugefügt")
|
||||||
|
migrations_performed.append(column_name)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Fehler beim Hinzufügen der Spalte '{column_name}' zu guest_requests: {str(e)}")
|
||||||
|
|
||||||
|
return len(migrations_performed) > 0
|
||||||
|
|
||||||
|
def create_missing_tables(cursor):
|
||||||
|
"""Erstellt fehlende Tabellen."""
|
||||||
|
logger.info("Prüfe auf fehlende Tabellen...")
|
||||||
|
|
||||||
|
# user_permissions Tabelle
|
||||||
|
if not get_table_exists(cursor, 'user_permissions'):
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TABLE user_permissions (
|
||||||
|
user_id INTEGER PRIMARY KEY,
|
||||||
|
can_start_jobs BOOLEAN DEFAULT 0,
|
||||||
|
needs_approval BOOLEAN DEFAULT 1,
|
||||||
|
can_approve_jobs BOOLEAN DEFAULT 0,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users (id)
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
logger.info("Tabelle 'user_permissions' erstellt")
|
||||||
|
|
||||||
|
# notifications Tabelle
|
||||||
|
if not get_table_exists(cursor, 'notifications'):
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TABLE notifications (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
type VARCHAR(50) NOT NULL,
|
||||||
|
payload TEXT,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
read BOOLEAN DEFAULT 0,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users (id)
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
logger.info("Tabelle 'notifications' erstellt")
|
||||||
|
|
||||||
|
# stats Tabelle
|
||||||
|
if not get_table_exists(cursor, 'stats'):
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TABLE stats (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
total_print_time INTEGER DEFAULT 0,
|
||||||
|
total_jobs_completed INTEGER DEFAULT 0,
|
||||||
|
total_material_used FLOAT DEFAULT 0.0,
|
||||||
|
last_updated DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
logger.info("Tabelle 'stats' erstellt")
|
||||||
|
|
||||||
|
# system_logs Tabelle
|
||||||
|
if not get_table_exists(cursor, 'system_logs'):
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TABLE system_logs (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
level VARCHAR(20) NOT NULL,
|
||||||
|
message VARCHAR(1000) NOT NULL,
|
||||||
|
module VARCHAR(100),
|
||||||
|
user_id INTEGER,
|
||||||
|
ip_address VARCHAR(50),
|
||||||
|
user_agent VARCHAR(500),
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users (id)
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
logger.info("Tabelle 'system_logs' erstellt")
|
||||||
|
|
||||||
|
def optimize_database(cursor):
|
||||||
|
"""Führt Datenbankoptimierungen durch."""
|
||||||
|
logger.info("Führe Datenbankoptimierungen durch...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Indices für bessere Performance
|
||||||
|
cursor.execute("CREATE INDEX IF NOT EXISTS idx_users_email ON users(email)")
|
||||||
|
cursor.execute("CREATE INDEX IF NOT EXISTS idx_users_username ON users(username)")
|
||||||
|
cursor.execute("CREATE INDEX IF NOT EXISTS idx_jobs_user_id ON jobs(user_id)")
|
||||||
|
cursor.execute("CREATE INDEX IF NOT EXISTS idx_jobs_printer_id ON jobs(printer_id)")
|
||||||
|
cursor.execute("CREATE INDEX IF NOT EXISTS idx_jobs_status ON jobs(status)")
|
||||||
|
cursor.execute("CREATE INDEX IF NOT EXISTS idx_notifications_user_id ON notifications(user_id)")
|
||||||
|
cursor.execute("CREATE INDEX IF NOT EXISTS idx_system_logs_timestamp ON system_logs(timestamp)")
|
||||||
|
cursor.execute("CREATE INDEX IF NOT EXISTS idx_guest_requests_status ON guest_requests(status)")
|
||||||
|
|
||||||
|
# Statistiken aktualisieren
|
||||||
|
cursor.execute("ANALYZE")
|
||||||
|
|
||||||
|
logger.info("Datenbankoptimierungen abgeschlossen")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Fehler bei Datenbankoptimierungen: {str(e)}")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Führt die komplette Schema-Migration aus."""
|
||||||
|
try:
|
||||||
|
logger.info("Starte umfassende Datenbank-Schema-Migration...")
|
||||||
|
|
||||||
|
# Verbindung zur Datenbank
|
||||||
|
if not os.path.exists(DATABASE_PATH):
|
||||||
|
logger.error(f"Datenbankdatei nicht gefunden: {DATABASE_PATH}")
|
||||||
|
# Erste Initialisierung
|
||||||
|
from models import init_database
|
||||||
|
logger.info("Führe Erstinitialisierung durch...")
|
||||||
|
init_database()
|
||||||
|
logger.info("Erstinitialisierung abgeschlossen")
|
||||||
|
return
|
||||||
|
|
||||||
|
conn = sqlite3.connect(DATABASE_PATH)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# WAL-Modus aktivieren für bessere Concurrent-Performance
|
||||||
|
cursor.execute("PRAGMA journal_mode=WAL")
|
||||||
|
cursor.execute("PRAGMA foreign_keys=ON")
|
||||||
|
|
||||||
|
logger.info(f"Verbunden mit Datenbank: {DATABASE_PATH}")
|
||||||
|
|
||||||
|
# Backup erstellen
|
||||||
|
backup_path = f"{DATABASE_PATH}.backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
||||||
|
cursor.execute(f"VACUUM INTO '{backup_path}'")
|
||||||
|
logger.info(f"Backup erstellt: {backup_path}")
|
||||||
|
|
||||||
|
# Migrationen durchführen
|
||||||
|
migrations_performed = []
|
||||||
|
|
||||||
|
# Fehlende Tabellen erstellen
|
||||||
|
create_missing_tables(cursor)
|
||||||
|
migrations_performed.append("missing_tables")
|
||||||
|
|
||||||
|
# Tabellen-spezifische Migrationen
|
||||||
|
if migrate_users_table(cursor):
|
||||||
|
migrations_performed.append("users")
|
||||||
|
|
||||||
|
if migrate_printers_table(cursor):
|
||||||
|
migrations_performed.append("printers")
|
||||||
|
|
||||||
|
if migrate_jobs_table(cursor):
|
||||||
|
migrations_performed.append("jobs")
|
||||||
|
|
||||||
|
if migrate_guest_requests_table(cursor):
|
||||||
|
migrations_performed.append("guest_requests")
|
||||||
|
|
||||||
|
# Optimierungen
|
||||||
|
optimize_database(cursor)
|
||||||
|
migrations_performed.append("optimizations")
|
||||||
|
|
||||||
|
# Änderungen speichern
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
logger.info(f"Schema-Migration erfolgreich abgeschlossen. Migrierte Bereiche: {', '.join(migrations_performed)}")
|
||||||
|
|
||||||
|
# Test der Migration
|
||||||
|
test_migration()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Fehler bei der Schema-Migration: {str(e)}")
|
||||||
|
if 'conn' in locals():
|
||||||
|
conn.rollback()
|
||||||
|
conn.close()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
def test_migration():
|
||||||
|
"""Testet die Migration durch Laden der Models."""
|
||||||
|
try:
|
||||||
|
logger.info("Teste Migration durch Laden der Models...")
|
||||||
|
|
||||||
|
# Models importieren und testen
|
||||||
|
from models import get_cached_session, User, Printer, Job
|
||||||
|
|
||||||
|
with get_cached_session() as session:
|
||||||
|
# Test User-Query (sollte das updated_at Problem lösen)
|
||||||
|
users = session.query(User).limit(1).all()
|
||||||
|
logger.info(f"User-Abfrage erfolgreich - {len(users)} Benutzer gefunden")
|
||||||
|
|
||||||
|
# Test Printer-Query
|
||||||
|
printers = session.query(Printer).limit(1).all()
|
||||||
|
logger.info(f"Printer-Abfrage erfolgreich - {len(printers)} Drucker gefunden")
|
||||||
|
|
||||||
|
# Test Job-Query
|
||||||
|
jobs = session.query(Job).limit(1).all()
|
||||||
|
logger.info(f"Job-Abfrage erfolgreich - {len(jobs)} Jobs gefunden")
|
||||||
|
|
||||||
|
logger.info("Migrations-Test erfolgreich abgeschlossen")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Fehler beim Migrations-Test: {str(e)}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
253
backend/app/fix_database_immediate.py
Normal file
253
backend/app/fix_database_immediate.py
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Sofortige Datenbank-Reparatur für fehlende updated_at Spalte
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import sqlite3
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Pfad zur App hinzufügen
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
from config.settings import DATABASE_PATH
|
||||||
|
|
||||||
|
def fix_users_table_immediate():
|
||||||
|
"""Repariert die users Tabelle sofort."""
|
||||||
|
print(f"Repariere Datenbank: {DATABASE_PATH}")
|
||||||
|
|
||||||
|
if not os.path.exists(DATABASE_PATH):
|
||||||
|
print(f"Datenbankdatei nicht gefunden: {DATABASE_PATH}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(DATABASE_PATH)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Prüfen, welche Spalten existieren
|
||||||
|
cursor.execute("PRAGMA table_info(users)")
|
||||||
|
existing_columns = [row[1] for row in cursor.fetchall()]
|
||||||
|
print(f"Vorhandene Spalten in users: {existing_columns}")
|
||||||
|
|
||||||
|
# Fehlende Spalten hinzufügen
|
||||||
|
required_columns = [
|
||||||
|
('updated_at', 'DATETIME'),
|
||||||
|
('settings', 'TEXT'),
|
||||||
|
('department', 'VARCHAR(100)'),
|
||||||
|
('position', 'VARCHAR(100)'),
|
||||||
|
('phone', 'VARCHAR(50)'),
|
||||||
|
('bio', 'TEXT')
|
||||||
|
]
|
||||||
|
|
||||||
|
for column_name, column_type in required_columns:
|
||||||
|
if column_name not in existing_columns:
|
||||||
|
try:
|
||||||
|
if column_name == 'updated_at':
|
||||||
|
# Einfacher Ansatz: NULL erlauben und später updaten
|
||||||
|
cursor.execute(f"ALTER TABLE users ADD COLUMN {column_name} {column_type}")
|
||||||
|
print(f"✓ Spalte '{column_name}' hinzugefügt")
|
||||||
|
|
||||||
|
# Alle vorhandenen Benutzer mit aktuellem Timestamp updaten
|
||||||
|
cursor.execute(f"UPDATE users SET {column_name} = CURRENT_TIMESTAMP WHERE {column_name} IS NULL")
|
||||||
|
print(f"✓ Vorhandene Benutzer mit {column_name} aktualisiert")
|
||||||
|
|
||||||
|
# Trigger für automatische Updates erstellen
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TRIGGER IF NOT EXISTS update_users_updated_at
|
||||||
|
AFTER UPDATE ON users
|
||||||
|
FOR EACH ROW
|
||||||
|
BEGIN
|
||||||
|
UPDATE users SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
|
||||||
|
END
|
||||||
|
""")
|
||||||
|
print(f"✓ Auto-Update-Trigger für {column_name} erstellt")
|
||||||
|
else:
|
||||||
|
cursor.execute(f"ALTER TABLE users ADD COLUMN {column_name} {column_type}")
|
||||||
|
print(f"✓ Spalte '{column_name}' hinzugefügt")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ Fehler bei Spalte '{column_name}': {str(e)}")
|
||||||
|
else:
|
||||||
|
print(f"○ Spalte '{column_name}' bereits vorhanden")
|
||||||
|
|
||||||
|
# Weitere fehlende Tabellen prüfen und erstellen
|
||||||
|
create_missing_tables(cursor)
|
||||||
|
|
||||||
|
# Optimierungsindizes erstellen
|
||||||
|
create_performance_indexes(cursor)
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
print("✓ Datenbank-Reparatur erfolgreich abgeschlossen")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ Fehler bei der Datenbank-Reparatur: {str(e)}")
|
||||||
|
if 'conn' in locals():
|
||||||
|
conn.rollback()
|
||||||
|
conn.close()
|
||||||
|
return False
|
||||||
|
|
||||||
|
def create_missing_tables(cursor):
|
||||||
|
"""Erstellt fehlende Tabellen."""
|
||||||
|
|
||||||
|
# Prüfen, welche Tabellen existieren
|
||||||
|
cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
|
||||||
|
existing_tables = [row[0] for row in cursor.fetchall()]
|
||||||
|
print(f"Vorhandene Tabellen: {existing_tables}")
|
||||||
|
|
||||||
|
# user_permissions Tabelle
|
||||||
|
if 'user_permissions' not in existing_tables:
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TABLE user_permissions (
|
||||||
|
user_id INTEGER PRIMARY KEY,
|
||||||
|
can_start_jobs BOOLEAN DEFAULT 0,
|
||||||
|
needs_approval BOOLEAN DEFAULT 1,
|
||||||
|
can_approve_jobs BOOLEAN DEFAULT 0,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users (id)
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
print("✓ Tabelle 'user_permissions' erstellt")
|
||||||
|
|
||||||
|
# notifications Tabelle
|
||||||
|
if 'notifications' not in existing_tables:
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TABLE notifications (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
type VARCHAR(50) NOT NULL,
|
||||||
|
payload TEXT,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
read BOOLEAN DEFAULT 0,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users (id)
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
print("✓ Tabelle 'notifications' erstellt")
|
||||||
|
|
||||||
|
# stats Tabelle
|
||||||
|
if 'stats' not in existing_tables:
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TABLE stats (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
total_print_time INTEGER DEFAULT 0,
|
||||||
|
total_jobs_completed INTEGER DEFAULT 0,
|
||||||
|
total_material_used REAL DEFAULT 0.0,
|
||||||
|
last_updated DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
print("✓ Tabelle 'stats' erstellt")
|
||||||
|
|
||||||
|
# Initial stats record erstellen
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO stats (total_print_time, total_jobs_completed, total_material_used, last_updated)
|
||||||
|
VALUES (0, 0, 0.0, CURRENT_TIMESTAMP)
|
||||||
|
""")
|
||||||
|
print("✓ Initial-Statistiken erstellt")
|
||||||
|
|
||||||
|
# system_logs Tabelle
|
||||||
|
if 'system_logs' not in existing_tables:
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TABLE system_logs (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
level VARCHAR(20) NOT NULL,
|
||||||
|
message VARCHAR(1000) NOT NULL,
|
||||||
|
module VARCHAR(100),
|
||||||
|
user_id INTEGER,
|
||||||
|
ip_address VARCHAR(50),
|
||||||
|
user_agent VARCHAR(500),
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users (id)
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
print("✓ Tabelle 'system_logs' erstellt")
|
||||||
|
|
||||||
|
def create_performance_indexes(cursor):
|
||||||
|
"""Erstellt Performance-Indices."""
|
||||||
|
print("Erstelle Performance-Indices...")
|
||||||
|
|
||||||
|
indexes = [
|
||||||
|
("idx_users_email", "users(email)"),
|
||||||
|
("idx_users_username", "users(username)"),
|
||||||
|
("idx_users_role", "users(role)"),
|
||||||
|
("idx_jobs_user_id", "jobs(user_id)"),
|
||||||
|
("idx_jobs_printer_id", "jobs(printer_id)"),
|
||||||
|
("idx_jobs_status", "jobs(status)"),
|
||||||
|
("idx_jobs_start_at", "jobs(start_at)"),
|
||||||
|
("idx_notifications_user_id", "notifications(user_id)"),
|
||||||
|
("idx_notifications_read", "notifications(read)"),
|
||||||
|
("idx_system_logs_timestamp", "system_logs(timestamp)"),
|
||||||
|
("idx_system_logs_level", "system_logs(level)"),
|
||||||
|
("idx_guest_requests_status", "guest_requests(status)"),
|
||||||
|
("idx_printers_status", "printers(status)"),
|
||||||
|
("idx_printers_active", "printers(active)")
|
||||||
|
]
|
||||||
|
|
||||||
|
for index_name, index_def in indexes:
|
||||||
|
try:
|
||||||
|
cursor.execute(f"CREATE INDEX IF NOT EXISTS {index_name} ON {index_def}")
|
||||||
|
print(f"✓ Index '{index_name}' erstellt")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"○ Index '{index_name}': {str(e)}")
|
||||||
|
|
||||||
|
def test_database_access():
|
||||||
|
"""Testet den Datenbankzugriff nach der Reparatur."""
|
||||||
|
print("\nTeste Datenbankzugriff...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Models importieren und testen
|
||||||
|
from models import get_cached_session, User, Printer, Job
|
||||||
|
|
||||||
|
with get_cached_session() as session:
|
||||||
|
# Test User-Query
|
||||||
|
users = session.query(User).limit(5).all()
|
||||||
|
print(f"✓ User-Abfrage erfolgreich - {len(users)} Benutzer gefunden")
|
||||||
|
|
||||||
|
# Test Printer-Query
|
||||||
|
printers = session.query(Printer).limit(5).all()
|
||||||
|
print(f"✓ Printer-Abfrage erfolgreich - {len(printers)} Drucker gefunden")
|
||||||
|
|
||||||
|
# Test Job-Query
|
||||||
|
jobs = session.query(Job).limit(5).all()
|
||||||
|
print(f"✓ Job-Abfrage erfolgreich - {len(jobs)} Jobs gefunden")
|
||||||
|
|
||||||
|
print("✓ Alle Datenbank-Tests erfolgreich!")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ Datenbank-Test fehlgeschlagen: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Hauptfunktion für die sofortige Datenbank-Reparatur."""
|
||||||
|
print("=== SOFORTIGE DATENBANK-REPARATUR ===")
|
||||||
|
print(f"Zeitstempel: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
|
print(f"Datenbank: {DATABASE_PATH}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Backup erstellen
|
||||||
|
if os.path.exists(DATABASE_PATH):
|
||||||
|
backup_path = f"{DATABASE_PATH}.backup_immediate_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
||||||
|
try:
|
||||||
|
import shutil
|
||||||
|
shutil.copy2(DATABASE_PATH, backup_path)
|
||||||
|
print(f"✓ Backup erstellt: {backup_path}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠ Backup-Erstellung fehlgeschlagen: {str(e)}")
|
||||||
|
|
||||||
|
# Reparatur durchführen
|
||||||
|
if fix_users_table_immediate():
|
||||||
|
print("\n=== DATENBANK-TEST ===")
|
||||||
|
if test_database_access():
|
||||||
|
print("\n🎉 DATENBANK-REPARATUR ERFOLGREICH!")
|
||||||
|
print("Die Anwendung sollte jetzt funktionieren.")
|
||||||
|
else:
|
||||||
|
print("\n❌ DATENBANK-TEST FEHLGESCHLAGEN!")
|
||||||
|
print("Weitere Diagnose erforderlich.")
|
||||||
|
else:
|
||||||
|
print("\n❌ DATENBANK-REPARATUR FEHLGESCHLAGEN!")
|
||||||
|
print("Manuelle Intervention erforderlich.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
@@ -1,85 +1,143 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}Drucker - MYP Platform{% endblock %}
|
{% block title %}Drucker - Mercedes-Benz MYP Platform{% endblock %}
|
||||||
|
|
||||||
{% block extra_css %}
|
|
||||||
<link href="{{ url_for('static', filename='css/printers.css') }}" rel="stylesheet">
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="max-w-7xl mx-auto px-3 sm:px-6 lg:px-8 py-4 sm:py-8">
|
<div class="bg-professional">
|
||||||
<!-- Header mit Status-Übersicht - Neu gestaltet -->
|
|
||||||
<div class="mb-6 sm:mb-10">
|
<!-- Professional Hero Header -->
|
||||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
<div class="professional-hero hero-pattern animate-fade-in">
|
||||||
<div class="mb-4 sm:mb-0">
|
<div class="absolute inset-0 bg-gradient-to-r from-black/10 to-black/20 dark:from-black/20 dark:to-black/40"></div>
|
||||||
<h1 class="text-2xl sm:text-3xl font-bold text-slate-900 dark:text-white mb-2">Drucker</h1>
|
|
||||||
<p class="text-sm sm:text-base text-slate-600 dark:text-slate-400">Verwalten Sie Ihre 3D-Drucker</p>
|
<!-- Status Indicator -->
|
||||||
|
<div class="absolute top-6 right-6 flex items-center space-x-3 z-10">
|
||||||
<!-- Live-Status-Übersicht - Neu gestaltet -->
|
<div class="mb-glass rounded-full px-4 py-2 animate-scale-in">
|
||||||
<div id="status-overview" class="status-overview-new mt-3">
|
<div class="flex items-center space-x-2">
|
||||||
<div class="flex items-center space-x-2">
|
<div class="status-dot status-online"></div>
|
||||||
<div class="status-dot online"></div>
|
<span class="text-sm font-semibold text-professional-primary">Live</span>
|
||||||
<span class="text-slate-700 dark:text-slate-300">Online: <span id="online-count" class="font-semibold text-green-600 dark:text-green-400">-</span></span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center space-x-2">
|
|
||||||
<div class="status-dot offline"></div>
|
|
||||||
<span class="text-slate-700 dark:text-slate-300">Offline: <span id="offline-count" class="font-semibold text-red-600 dark:text-red-400">-</span></span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center space-x-2">
|
|
||||||
<svg class="w-2.5 h-2.5 text-blue-500" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd"></path>
|
|
||||||
</svg>
|
|
||||||
<span class="text-slate-700 dark:text-slate-300">Gesamt: <span id="total-count" class="font-semibold text-blue-600 dark:text-blue-400">-</span></span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center space-x-2 ml-2">
|
|
||||||
<svg id="auto-refresh-icon" class="w-3 h-3 text-slate-500" 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 class="text-xs text-slate-500 dark:text-slate-400">Auto-Update: <span id="next-update-time" class="font-mono">-</span>s</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap gap-3">
|
<div class="mb-glass rounded-full px-4 py-2 animate-scale-in">
|
||||||
<!-- Filter-Buttons - Neu gestaltet -->
|
<span id="live-time" class="text-sm font-semibold text-professional-primary"></span>
|
||||||
<div class="filter-bar-new">
|
</div>
|
||||||
<button id="filter-all" class="filter-btn-new active">Alle</button>
|
</div>
|
||||||
<button id="filter-online" class="filter-btn-new">Online</button>
|
|
||||||
<button id="filter-offline" class="filter-btn-new">Offline</button>
|
<div class="relative max-w-7xl mx-auto px-6 lg:px-8 py-16 z-10">
|
||||||
|
<div class="text-center animate-slide-up">
|
||||||
|
<!-- Mercedes-Benz Logo -->
|
||||||
|
<div class="inline-flex items-center justify-center w-24 h-24 mb-glass rounded-full mb-8 professional-shadow">
|
||||||
|
<svg class="w-12 h-12 text-professional-primary" viewBox="0 0 80 80" fill="currentColor">
|
||||||
|
<path d="M58.6,4.5C53,1.6,46.7,0,40,0c-6.7,0-13,1.6-18.6,4.5v0C8.7,11.2,0,24.6,0,40c0,15.4,8.7,28.8,21.5,35.5
|
||||||
|
C27,78.3,33.3,80,40,80c6.7,0,12.9-1.7,18.5-4.6C71.3,68.8,80,55.4,80,40C80,24.6,71.3,11.2,58.6,4.5z M4,40
|
||||||
|
c0-13.1,7-24.5,17.5-30.9v0C26.6,6,32.5,4.2,39,4l-4.5,32.7L21.5,46.8v0L8.3,57.1C5.6,52,4,46.2,4,40z M58.6,70.8
|
||||||
|
C53.1,74.1,46.8,76,40,76c-6.8,0-13.2-1.9-18.6-5.2c-4.9-2.9-8.9-6.9-11.9-11.7l11.9-4.9v0L40,46.6l18.6,7.5v0l12,4.9
|
||||||
|
C67.6,63.9,63.4,67.9,58.6,70.8z M58.6,46.8L58.6,46.8l-12.9-10L41.1,4c6.3,0.2,12.3,2,17.4,5.1v0C69,15.4,76,26.9,76,40
|
||||||
|
c0,6.2-1.5,12-4.3,17.1L58.6,46.8z"/>
|
||||||
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button onclick="toggleAutoRefresh()" id="auto-refresh-btn" class="action-btn-new primary">
|
<h1 class="title-professional text-5xl md:text-6xl font-bold mb-6 tracking-tight">
|
||||||
<svg class="h-4 w-4 sm:h-5 sm:w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
3D-Drucker Management
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
</h1>
|
||||||
</svg>
|
<p class="subtitle-professional text-xl md:text-2xl max-w-4xl mx-auto leading-relaxed mb-8">
|
||||||
<span>Auto-Update</span>
|
Verwalten Sie Ihre 3D-Drucker mit höchster Präzision und Mercedes-Benz Qualitätsstandard
|
||||||
</button>
|
</p>
|
||||||
|
|
||||||
<button onclick="refreshPrinters()" class="action-btn-new primary">
|
<!-- Status Overview -->
|
||||||
<svg class="h-4 w-4 sm:h-5 sm:w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<div class="flex flex-wrap justify-center gap-6 mb-8">
|
||||||
<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" />
|
<div class="mb-glass rounded-2xl px-6 py-4 animate-scale-in">
|
||||||
</svg>
|
<div class="flex items-center space-x-3">
|
||||||
<span>Jetzt aktualisieren</span>
|
<div class="status-dot status-online"></div>
|
||||||
</button>
|
<span class="text-professional-primary font-semibold">Online: <span id="online-count" class="text-green-500 font-bold">-</span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-glass rounded-2xl px-6 py-4 animate-scale-in">
|
||||||
|
<div class="flex items-center space-x-3">
|
||||||
|
<div class="status-dot status-offline"></div>
|
||||||
|
<span class="text-professional-primary font-semibold">Offline: <span id="offline-count" class="text-red-500 font-bold">-</span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-glass rounded-2xl px-6 py-4 animate-scale-in">
|
||||||
|
<div class="flex items-center space-x-3">
|
||||||
|
<svg class="w-5 h-5 text-professional-primary" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd"/>
|
||||||
|
</svg>
|
||||||
|
<span class="text-professional-primary font-semibold">Gesamt: <span id="total-count" class="text-professional-accent font-bold">-</span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% if current_user.is_admin %}
|
<!-- Action Buttons -->
|
||||||
<button id="addPrinterBtn" class="action-btn-new success">
|
<div class="flex flex-wrap justify-center gap-4">
|
||||||
<svg class="h-4 w-4 sm:h-5 sm:w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<!-- Filter Buttons -->
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
<div class="flex bg-white/10 dark:bg-black/20 backdrop-blur-sm rounded-2xl p-2 border border-white/20 dark:border-white/10">
|
||||||
|
<button id="filter-all" class="filter-btn-mercedes active px-4 py-2 rounded-xl text-sm font-semibold transition-all duration-300">
|
||||||
|
Alle
|
||||||
|
</button>
|
||||||
|
<button id="filter-online" class="filter-btn-mercedes px-4 py-2 rounded-xl text-sm font-semibold transition-all duration-300">
|
||||||
|
Online
|
||||||
|
</button>
|
||||||
|
<button id="filter-offline" class="filter-btn-mercedes px-4 py-2 rounded-xl text-sm font-semibold transition-all duration-300">
|
||||||
|
Offline
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button onclick="toggleAutoRefresh()" id="auto-refresh-btn" class="btn-professional group">
|
||||||
|
<svg class="w-6 h-6 mr-3 group-hover:rotate-180 transition-transform duration-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||||
|
</svg>
|
||||||
|
<span>Auto-Update</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button onclick="refreshPrinters()" class="btn-professional group">
|
||||||
|
<svg class="w-6 h-6 mr-3 group-hover:rotate-180 transition-transform duration-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<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>Jetzt aktualisieren</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{% if current_user.is_admin %}
|
||||||
|
<button id="addPrinterBtn" class="btn-success-professional group">
|
||||||
|
<svg class="w-6 h-6 mr-3 group-hover:scale-110 transition-transform duration-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
|
||||||
|
</svg>
|
||||||
|
<span>Drucker hinzufügen</span>
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Auto-Update Info -->
|
||||||
|
<div class="mt-6 flex items-center justify-center space-x-2 text-professional-muted">
|
||||||
|
<svg id="auto-refresh-icon" class="w-4 h-4 animate-spin-slow" 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>
|
</svg>
|
||||||
<span>Drucker hinzufügen</span>
|
<span class="text-sm">Auto-Update: <span id="next-update-time" class="font-mono">-</span>s</span>
|
||||||
</button>
|
</div>
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Printers Grid -->
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 -mt-8 relative z-10 space-y-8">
|
||||||
<div id="printers-grid" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6">
|
|
||||||
<!-- Loading state -->
|
<!-- Printers Grid -->
|
||||||
<div class="col-span-full text-center py-6 sm:py-12">
|
<div class="professional-container p-8 lg:p-12 animate-slide-up">
|
||||||
<div class="animate-spin rounded-full h-8 w-8 sm:h-12 sm:w-12 border-b-2 border-indigo-600 dark:border-indigo-400 mx-auto"></div>
|
<div class="text-center mb-10">
|
||||||
<p class="mt-3 sm:mt-4 text-sm sm:text-base text-slate-600 dark:text-slate-400">Lade Drucker...</p>
|
<h2 class="title-professional text-3xl font-bold mb-4">
|
||||||
<p class="mt-1 text-xs text-slate-500 dark:text-slate-500">Dies sollte nur wenige Sekunden dauern</p>
|
Verfügbare Drucker
|
||||||
|
</h2>
|
||||||
|
<p class="subtitle-professional text-lg">
|
||||||
|
Übersicht und Verwaltung Ihrer 3D-Drucker-Infrastruktur
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="printers-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
<!-- Loading state -->
|
||||||
|
<div class="col-span-full text-center py-12">
|
||||||
|
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-professional-accent mx-auto"></div>
|
||||||
|
<p class="mt-4 text-base text-professional-secondary">Lade Drucker...</p>
|
||||||
|
<p class="mt-1 text-sm text-professional-muted">Dies sollte nur wenige Sekunden dauern</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1074,7 +1132,7 @@
|
|||||||
|
|
||||||
// Filter-Funktionalität
|
// Filter-Funktionalität
|
||||||
function setupFilters() {
|
function setupFilters() {
|
||||||
const filterButtons = document.querySelectorAll('.filter-btn-new');
|
const filterButtons = document.querySelectorAll('.filter-btn-mercedes');
|
||||||
|
|
||||||
filterButtons.forEach(btn => {
|
filterButtons.forEach(btn => {
|
||||||
btn.addEventListener('click', function() {
|
btn.addEventListener('click', function() {
|
||||||
|
Reference in New Issue
Block a user