- Add GET /api/clients/crashed endpoint (process_status=crashed or stale heartbeat) - Add restart_app command action with same lifecycle + lockout as reboot_host - Scheduler: crash auto-recovery loop (CRASH_RECOVERY_ENABLED flag, lockout, MQTT publish) - Scheduler: unconditional command expiry sweep per poll cycle (sweep_expired_commands) - Listener: subscribe to infoscreen/+/service_failed; persist service_failed_at + unit - Listener: extract broker_connection block from health payload; persist reconnect_count + last_disconnect_at - DB migration b1c2d3e4f5a6: service_failed_at, service_failed_unit, mqtt_reconnect_count, mqtt_last_disconnect_at on clients - Add GET /api/clients/service_failed and POST /api/clients/<uuid>/clear_service_failed - Monitoring overview API: include mqtt_reconnect_count + mqtt_last_disconnect_at per client - Frontend: orange service-failed alert panel (hidden when empty, auto-refresh, quittieren action) - Frontend: MQTT reconnect count + last disconnect in client detail panel - MQTT auth hardening: listener/scheduler/server use env credentials; broker enforces allow_anonymous false - Client command lifecycle foundation: ClientCommand model, reboot_host/shutdown_host, full ACK lifecycle - Docs: TECH-CHANGELOG, DEV-CHANGELOG, MQTT_EVENT_PAYLOAD_GUIDE, copilot-instructions updated - Add implementation-plans/, RESTART_VALIDATION_CHECKLIST.md, TODO.md
89 lines
4.3 KiB
Python
89 lines
4.3 KiB
Python
from sqlalchemy import create_engine, textmosquitto.conf
|
||
import os
|
||
from dotenv import load_dotenv
|
||
import bcrypt
|
||
|
||
# .env laden (nur in Dev)
|
||
if os.getenv("ENV", "development") == "development":
|
||
load_dotenv()
|
||
|
||
# Use same logic as database.py: prefer DB_CONN, fallback to individual vars
|
||
DB_URL = os.getenv("DB_CONN")
|
||
if not DB_URL:
|
||
DB_USER = os.getenv("DB_USER", "infoscreen_admin")
|
||
DB_PASSWORD = os.getenv("DB_PASSWORD")
|
||
# In Docker Compose: DB_HOST will be 'db' from env
|
||
# In dev container: will be 'localhost' from .env
|
||
DB_HOST = os.getenv("DB_HOST", "db") # Default to 'db' for Docker Compose
|
||
DB_NAME = os.getenv("DB_NAME", "infoscreen_by_taa")
|
||
DB_URL = f"mysql+pymysql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:3306/{DB_NAME}"
|
||
|
||
print(f"init_defaults.py connecting to: {DB_URL.split('@')[1] if '@' in DB_URL else DB_URL}")
|
||
engine = create_engine(DB_URL, isolation_level="AUTOCOMMIT")
|
||
|
||
with engine.connect() as conn:
|
||
# Default-Gruppe mit id=1 anlegen, falls nicht vorhanden
|
||
result = conn.execute(
|
||
text("SELECT COUNT(*) FROM client_groups WHERE id=1"))
|
||
if result.scalar() == 0:
|
||
conn.execute(
|
||
text(
|
||
"INSERT INTO client_groups (id, name, is_active) VALUES (1, 'Nicht zugeordnet', 1)")
|
||
)
|
||
print("✅ Default-Gruppe mit id=1 angelegt.")
|
||
|
||
# Superadmin-Benutzer anlegen, falls nicht vorhanden
|
||
admin_user = os.getenv("DEFAULT_SUPERADMIN_USERNAME", "superadmin")
|
||
admin_pw = os.getenv("DEFAULT_SUPERADMIN_PASSWORD")
|
||
|
||
if not admin_pw:
|
||
print("⚠️ DEFAULT_SUPERADMIN_PASSWORD nicht gesetzt. Superadmin wird nicht erstellt.")
|
||
else:
|
||
# Passwort hashen mit bcrypt
|
||
hashed_pw = bcrypt.hashpw(admin_pw.encode(
|
||
'utf-8'), bcrypt.gensalt()).decode('utf-8')
|
||
# Prüfen, ob User existiert
|
||
result = conn.execute(text(
|
||
"SELECT COUNT(*) FROM users WHERE username=:username"), {"username": admin_user})
|
||
if result.scalar() == 0:
|
||
# Rolle: 'superadmin' gemäß UserRole enum
|
||
conn.execute(
|
||
text("INSERT INTO users (username, password_hash, role, is_active) VALUES (:username, :password_hash, 'superadmin', 1)"),
|
||
{"username": admin_user, "password_hash": hashed_pw}
|
||
)
|
||
print(f"✅ Superadmin-Benutzer '{admin_user}' angelegt.")
|
||
else:
|
||
print(f"ℹ️ Superadmin-Benutzer '{admin_user}' existiert bereits.")
|
||
|
||
# Default System Settings anlegen
|
||
default_settings = [
|
||
('supplement_table_url', '', 'URL für Vertretungsplan / WebUntis (Stundenplan-Änderungstabelle)'),
|
||
('supplement_table_enabled', 'false', 'Ob Vertretungsplan aktiviert ist'),
|
||
('presentation_interval', '10', 'Standard Intervall für Präsentationen (Sekunden)'),
|
||
('presentation_page_progress', 'true', 'Seitenfortschrift anzeigen (Page-Progress) für Präsentationen'),
|
||
('presentation_auto_progress', 'true', 'Automatischer Fortschritt (Auto-Progress) für Präsentationen'),
|
||
('video_autoplay', 'true', 'Autoplay (automatisches Abspielen) für Videos'),
|
||
('video_loop', 'true', 'Loop (Wiederholung) für Videos'),
|
||
('video_volume', '0.8', 'Standard Lautstärke für Videos (0.0 - 1.0)'),
|
||
('holiday_banner_enabled', 'true', 'Ferienstatus-Banner auf Dashboard anzeigen'),
|
||
('organization_name', '', 'Name der Organisation (wird im Header angezeigt)'),
|
||
('refresh_seconds', '0', 'Scheduler Republish-Intervall (Sekunden; 0 deaktiviert)'),
|
||
('group_order', '[]', 'Benutzerdefinierte Reihenfolge der Raumgruppen (JSON-Array mit Group-IDs)'),
|
||
]
|
||
|
||
for key, value, description in default_settings:
|
||
result = conn.execute(
|
||
text("SELECT COUNT(*) FROM system_settings WHERE `key`=:key"),
|
||
{"key": key}
|
||
)
|
||
if result.scalar() == 0:
|
||
conn.execute(
|
||
text("INSERT INTO system_settings (`key`, value, description) VALUES (:key, :value, :description)"),
|
||
{"key": key, "value": value, "description": description}
|
||
)
|
||
print(f"✅ System-Einstellung '{key}' angelegt.")
|
||
else:
|
||
print(f"ℹ️ System-Einstellung '{key}' existiert bereits.")
|
||
|
||
print("✅ Initialisierung abgeschlossen.")
|