Merge pull request 'dev' (#1) from dev into main

Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
2025-10-12 13:44:14 +02:00
19 changed files with 934 additions and 264 deletions

6
.gitignore vendored
View File

@@ -17,6 +17,7 @@ __pycache__/
!etc/systemd/system/ !etc/systemd/system/
!etc/systemd/system/rpi*.service !etc/systemd/system/rpi*.service
!etc/systemd/system/rpi*.timer !etc/systemd/system/rpi*.timer
!etc/systemd/system/poe*.service
!etc/nginx/ !etc/nginx/
!etc/nginx/sites-enabled/ !etc/nginx/sites-enabled/
!etc/nginx/sites-enabled/default !etc/nginx/sites-enabled/default
@@ -39,7 +40,8 @@ __pycache__/
!/srv/poe_manager/static/css/* !/srv/poe_manager/static/css/*
!/srv/poe_manager/static/js/ !/srv/poe_manager/static/js/
!/srv/poe_manager/static/js/* !/srv/poe_manager/static/js/*
!/srv/poe_manager/static/images/
!/srv/poe_manager/static/images/*
# Optional: SQLite DB ignorieren (falls du nicht willst, dass Passwörter im Repo landen) # Optional: SQLite DB ignorieren (falls du nicht willst, dass Passwörter im Repo landen)
# /srv/poe_manager/sqlite.db !/srv/poe_manager/sqlite.db

View File

@@ -0,0 +1,16 @@
[Unit]
Description=PoE Manager Web App
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=/srv/poe_manager
# Nutze die virtuelle Umgebung
ExecStart=/srv/poe_manager/venv/bin/python3 /srv/poe_manager/app.py
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target

View File

@@ -47,7 +47,7 @@ def get_devices():
Liefert eine Liste aller Devices aus der Datenbank als Dictionaries. Liefert eine Liste aller Devices aus der Datenbank als Dictionaries.
""" """
conn = get_db_connection() conn = get_db_connection()
devices = conn.execute("SELECT mac, rpi_ip, port, name, switch_hostname FROM devices").fetchall() devices = conn.execute("SELECT mac, rpi_ip, port, name, switch_hostname, is_active FROM devices ORDER BY name ASC").fetchall()
conn.close() conn.close()
return devices return devices
@@ -84,10 +84,16 @@ def logout():
@app.route("/") @app.route("/")
@login_required @login_required
def index(): def index():
# Geräte aus DB laden
conn = sqlite3.connect("sqlite.db") conn = sqlite3.connect("sqlite.db")
c = conn.cursor() c = conn.cursor()
c.execute("SELECT mac, name FROM devices") c.execute("SELECT mac, name, is_active FROM devices ORDER BY name ASC")
devices = c.fetchall() devices = c.fetchall()
# Intervall aus DB (Minuten) laden
c.execute("SELECT value FROM settings WHERE key='interval'")
row = c.fetchone()
interval = int(row[0]) if row else 5 # Default 5 Minuten
conn.close() conn.close()
# Status aus letztem Log ermitteln # Status aus letztem Log ermitteln
@@ -102,7 +108,8 @@ def index():
if dev[1] in line: if dev[1] in line:
status_dict[dev[0]] = "online" if "erreichbar" in line else "offline" status_dict[dev[0]] = "online" if "erreichbar" in line else "offline"
return render_template("index.html", devices=devices, status=status_dict) # Template rendern mit Devices, Status und Intervall
return render_template("index.html", devices=devices, status=status_dict, interval=interval)
@app.route("/settings", methods=["GET", "POST"]) @app.route("/settings", methods=["GET", "POST"])
@login_required @login_required
@@ -121,12 +128,23 @@ def settings():
if request.method == "POST": if request.method == "POST":
new_interval = int(request.form["interval"]) new_interval = int(request.form["interval"])
# Minuten und Sekunden berechnen
interval_minutes = new_interval
check_interval_seconds = new_interval * 60
conn = sqlite3.connect("sqlite.db") conn = sqlite3.connect("sqlite.db")
c = conn.cursor() c = conn.cursor()
# upsert # interval (Minuten)
c.execute("INSERT INTO settings (key, value) VALUES (?, ?) " c.execute("""
"ON CONFLICT(key) DO UPDATE SET value=excluded.value", INSERT INTO settings (key, value) VALUES (?, ?)
("interval", new_interval)) ON CONFLICT(key) DO UPDATE SET value=excluded.value
""", ("interval", interval_minutes))
# check_interval (Sekunden)
c.execute("""
INSERT INTO settings (key, value) VALUES (?, ?)
ON CONFLICT(key) DO UPDATE SET value=excluded.value
""", ("check_interval", check_interval_seconds))
conn.commit() conn.commit()
conn.close() conn.close()
@@ -134,7 +152,7 @@ def settings():
import subprocess import subprocess
subprocess.run(["systemctl", "restart", "rpi-check.service"]) subprocess.run(["systemctl", "restart", "rpi-check.service"])
flash(f"Intervall auf {new_interval} Minuten gesetzt und Service neu gestartet!") flash(f"Intervall auf {interval_minutes} Minuten gesetzt und Service neu gestartet!")
return redirect(url_for("settings")) return redirect(url_for("settings"))
return render_template("settings.html", interval=interval) return render_template("settings.html", interval=interval)
@@ -145,65 +163,170 @@ def devices():
conn = get_db_connection() conn = get_db_connection()
switches = conn.execute("SELECT hostname FROM switches").fetchall() switches = conn.execute("SELECT hostname FROM switches").fetchall()
# Inline-Add if request.method == 'POST':
if request.method == 'POST' and 'add_device' in request.form: # -----------------------
if not current_user.is_admin: # Gerät hinzufügen
flash("Zugriff verweigert!") # -----------------------
return redirect(url_for('devices')) if 'add_device' in request.form:
mac = request.form['mac'] if not current_user.is_admin:
rpi_ip = request.form['rpi_ip'] flash("Zugriff verweigert!")
port = request.form['port'] return redirect(url_for('devices'))
name = request.form['name']
switch_hostname = request.form['switch_hostname']
try:
conn.execute("INSERT INTO devices (mac, rpi_ip, port, name, switch_hostname) VALUES (?, ?, ?, ?, ?)",
(mac, rpi_ip, port, name, switch_hostname))
conn.commit()
flash(f"Gerät {name} hinzugefügt.")
except sqlite3.IntegrityError:
flash("MAC existiert bereits oder Eingabefehler!")
# Inline-Edit mac = request.form.get('mac')
if request.method == 'POST' and 'edit_device' in request.form: rpi_ip = request.form.get('rpi_ip')
if not current_user.is_admin: port = request.form.get('port')
flash("Zugriff verweigert!") name = request.form.get('name')
return redirect(url_for('devices')) switch_hostname = request.form.get('switch_hostname')
old_mac = request.form['old_mac'] is_active = 1 if 'is_active' in request.form else 0
mac = request.form['mac']
rpi_ip = request.form['rpi_ip']
port = request.form['port']
name = request.form['name']
switch_hostname = request.form['switch_hostname']
try:
conn.execute("""
UPDATE devices
SET mac=?, rpi_ip=?, port=?, name=?, switch_hostname=?
WHERE mac=?
""", (mac, rpi_ip, port, name, switch_hostname, old_mac))
conn.commit()
flash(f"Gerät {name} aktualisiert.")
except sqlite3.IntegrityError:
flash("MAC existiert bereits oder Eingabefehler!")
# Inline-Delete if not all([mac, rpi_ip, port, name, switch_hostname]):
if request.method == 'POST' and 'delete_device' in request.form: flash("Alle Felder müssen ausgefüllt sein!")
if not current_user.is_admin: return redirect(url_for('devices'))
flash("Zugriff verweigert!")
return redirect(url_for('devices'))
del_mac = request.form['delete_device']
conn.execute("DELETE FROM devices WHERE mac=?", (del_mac,))
conn.commit()
flash(f"Gerät {del_mac} gelöscht.")
# Prüfen auf doppelte IP
ip_device = conn.execute("SELECT name FROM devices WHERE rpi_ip=?", (rpi_ip,)).fetchone()
if ip_device:
flash(f"IP-Adresse existiert bereits für Gerät '{ip_device['name']}'!")
return redirect(url_for('devices'))
# Prüfen auf doppelte MAC
mac_device = conn.execute("SELECT name FROM devices WHERE mac=?", (mac,)).fetchone()
if mac_device:
flash(f"MAC-Adresse existiert bereits für Gerät '{mac_device['name']}'!")
return redirect(url_for('devices'))
try:
conn.execute("""
INSERT INTO devices (mac, rpi_ip, port, name, switch_hostname, is_active)
VALUES (?, ?, ?, ?, ?, ?)
""", (mac, rpi_ip, port, name, switch_hostname, is_active))
conn.commit()
flash(f"Gerät {name} hinzugefügt.")
except sqlite3.IntegrityError:
flash("Fehler beim Hinzufügen des Geräts!")
# -----------------------
# Gerät bearbeiten
# -----------------------
elif 'edit_device' in request.form:
if not current_user.is_admin:
flash("Zugriff verweigert!")
return redirect(url_for('devices'))
old_mac = request.form.get('old_mac')
mac = request.form.get('mac')
rpi_ip = request.form.get('rpi_ip')
port = request.form.get('port')
name = request.form.get('name')
switch_hostname = request.form.get('switch_hostname') or None
is_active = 1 if 'is_active' in request.form else 0
# --- Prüfen, ob es sich um eine Switch-Änderung handelt ---
if mac is None and rpi_ip is None and port is None and name is None and switch_hostname:
# Nur den Switch ändern
device = conn.execute("SELECT name, switch_hostname FROM devices WHERE mac=?", (old_mac,)).fetchone()
if not device:
flash("Gerät nicht gefunden!")
return redirect(url_for('devices'))
old_switch = device['switch_hostname'] or "unbekannt"
device_name = device['name']
try:
conn.execute("""
UPDATE devices
SET switch_hostname=?
WHERE mac=?
""", (switch_hostname, old_mac))
conn.commit()
flash(f"Switch von {device_name} geändert: {old_switch}{switch_hostname}")
except sqlite3.IntegrityError:
flash("Fehler beim Ändern des Switch!")
return redirect(url_for('devices'))
if not all([old_mac, mac, rpi_ip, port, name]):
flash("Alle Felder müssen ausgefüllt sein!")
return redirect(url_for('devices'))
# Prüfen auf doppelte IP außer das aktuelle Gerät
ip_device = conn.execute("SELECT name FROM devices WHERE rpi_ip=? AND mac<>?", (rpi_ip, old_mac)).fetchone()
if ip_device:
flash(f"IP-Adresse existiert bereits für Gerät '{ip_device['name']}'!")
return redirect(url_for('devices'))
# Prüfen auf doppelte MAC außer das aktuelle Gerät
mac_device = conn.execute("SELECT name FROM devices WHERE mac=? AND mac<>?", (mac, old_mac)).fetchone()
if mac_device:
flash(f"MAC-Adresse existiert bereits für Gerät '{mac_device['name']}'!")
return redirect(url_for('devices'))
try:
conn.execute("""
UPDATE devices
SET mac=?, rpi_ip=?, port=?, name=?, is_active=?
WHERE mac=?
""", (mac, rpi_ip, port, name, is_active, old_mac))
conn.commit()
flash(f"Gerät {name} aktualisiert.")
except sqlite3.IntegrityError:
flash("Fehler beim Aktualisieren des Geräts!")
# -----------------------
# Gerät löschen
# -----------------------
elif 'delete_device' in request.form:
if not current_user.is_admin:
flash("Zugriff verweigert!")
return redirect(url_for('devices'))
del_mac = request.form.get('delete_device')
if del_mac:
device = conn.execute("SELECT name FROM devices WHERE mac=?", (del_mac,)).fetchone()
hostname = device['name'] if device else del_mac
conn.execute("DELETE FROM devices WHERE mac=?", (del_mac,))
conn.commit()
flash(f"Gerät {hostname} gelöscht.")
else:
flash("Keine MAC-Adresse übermittelt!")
return redirect(url_for('devices'))
# -----------------------
# Devices für Anzeige
# -----------------------
devices = conn.execute(""" devices = conn.execute("""
SELECT devices.mac, devices.rpi_ip, devices.port, devices.name, switches.hostname AS switch_hostname SELECT devices.mac, devices.rpi_ip, devices.port, devices.name, devices.is_active,
switches.hostname AS switch_hostname
FROM devices FROM devices
JOIN switches ON devices.switch_hostname = switches.hostname LEFT JOIN switches ON devices.switch_hostname = switches.hostname
ORDER BY switches.hostname ASC
""").fetchall() """).fetchall()
conn.close() conn.close()
interval_min = get_interval_seconds() // 60
return render_template('devices.html', devices=devices, switches=switches) return render_template('devices.html', devices=devices, switches=switches)
@app.route('/devices/toggle/<mac>', methods=['POST'])
@login_required
def toggle_device(mac):
if not current_user.is_admin:
return {"success": False, "msg": "Zugriff verweigert!"}, 403
conn = get_db_connection()
device = conn.execute("SELECT is_active, name FROM devices WHERE mac=?", (mac,)).fetchone()
if not device:
conn.close()
return {"success": False, "msg": "Gerät nicht gefunden!"}, 404
new_status = 0 if device['is_active'] else 1
conn.execute("UPDATE devices SET is_active=? WHERE mac=?", (new_status, mac))
conn.commit()
conn.close()
status_text = "deaktiviert" if new_status == 0 else "aktiviert"
return {"success": True, "msg": f"Gerät {device['name']} wurde {status_text}.", "new_status": new_status}
@app.route('/switches', methods=['GET', 'POST']) @app.route('/switches', methods=['GET', 'POST'])
@login_required @login_required
def switches(): def switches():
@@ -247,20 +370,27 @@ def switches():
except sqlite3.IntegrityError: except sqlite3.IntegrityError:
flash("Hostname existiert bereits oder Eingabefehler!") flash("Hostname existiert bereits oder Eingabefehler!")
# Inline-Delete
if request.method == 'POST' and 'delete_switch' in request.form:
if not current_user.is_admin:
flash("Zugriff verweigert!")
return redirect(url_for('switches'))
del_hostname = request.form['delete_switch']
conn.execute("DELETE FROM switches WHERE hostname=?", (del_hostname,))
conn.commit()
flash(f"Switch {del_hostname} gelöscht.")
switches = conn.execute("SELECT hostname, ip, username FROM switches").fetchall() switches = conn.execute("SELECT hostname, ip, username FROM switches").fetchall()
conn.close() conn.close()
return render_template('switche.html', switches=switches) return render_template('switche.html', switches=switches)
@app.route('/switches/delete/<hostname>', methods=['POST'])
@login_required
def delete_switch(hostname):
conn = get_db_connection()
devices = conn.execute("SELECT name FROM devices WHERE switch_hostname=?", (hostname,)).fetchall()
if devices:
device_names = [d['name'] for d in devices]
flash(f"Folgende Geräte sind noch dem Switch {hostname} zugewiesen: {', '.join(device_names)}", "danger")
conn.close()
return redirect(url_for('switches'))
conn.execute("DELETE FROM switches WHERE hostname=?", (hostname,))
conn.commit()
conn.close()
flash(f"Switch '{hostname}' gelöscht.", "success")
return redirect(url_for('switches'))
@app.route("/get_log") @app.route("/get_log")
@login_required @login_required
def get_log(): def get_log():

View File

@@ -15,11 +15,11 @@ CREATE TABLE IF NOT EXISTS switches (
# Devices # Devices
c.execute(""" c.execute("""
CREATE TABLE IF NOT EXISTS devices ( CREATE TABLE devices (
mac TEXT PRIMARY KEY, mac TEXT PRIMARY KEY,
rpi_ip TEXT NOT NULL, rpi_ip TEXT NOT NULL,
switch_hostname TEXT NOT NULL, switch_hostname TEXT NOT NULL,
port TEXT NOT NULL, port TEXT,
name TEXT NOT NULL, name TEXT NOT NULL,
is_active INTEGER DEFAULT 0, is_active INTEGER DEFAULT 0,
FOREIGN KEY (switch_hostname) REFERENCES switches(hostname) FOREIGN KEY (switch_hostname) REFERENCES switches(hostname)

33
srv/poe_manager/create_user.py Executable file
View File

@@ -0,0 +1,33 @@
#!/usr/bin/env python3
import sqlite3
from getpass import getpass
from flask_bcrypt import Bcrypt
DB_PATH = "/srv/poe_manager/sqlite.db"
bcrypt = Bcrypt()
def main():
username = input("Benutzername: ")
password = getpass("Passwort: ")
password_confirm = getpass("Passwort bestätigen: ")
if password != password_confirm:
print("Passwörter stimmen nicht überein!")
return
pw_hash = bcrypt.generate_password_hash(password).decode('utf-8')
conn = sqlite3.connect(DB_PATH)
cur = conn.cursor()
try:
cur.execute("INSERT INTO users (username, password, is_admin) VALUES (?, ?, ?)",
(username, pw_hash, 0))
conn.commit()
print(f"Benutzer '{username}' erfolgreich angelegt.")
except sqlite3.IntegrityError:
print("Benutzername existiert bereits!")
finally:
conn.close()
if __name__ == "__main__":
main()

View File

@@ -1,36 +1,26 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import sqlite3 import sqlite3
import tempfile
import os
from app import decrypt_password, DB_PATH from app import decrypt_password, DB_PATH
def generate_ips_list(): def generate_ips_list():
conn = sqlite3.connect(DB_PATH) conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row conn.row_factory = sqlite3.Row
# Alle Switches laden
switches = {row['hostname']: row for row in conn.execute("SELECT hostname, ip, username, password FROM switches")} switches = {row['hostname']: row for row in conn.execute("SELECT hostname, ip, username, password FROM switches")}
devices = conn.execute("""
# Alle Geräte laden SELECT mac, rpi_ip, port, name, switch_hostname
devices = conn.execute("SELECT mac, rpi_ip, port, name, switch_hostname FROM devices").fetchall() FROM devices
WHERE is_active=1
""").fetchall()
conn.close() conn.close()
tmp = tempfile.NamedTemporaryFile(delete=False, mode='w', prefix='ips_', suffix='.list')
tmp_path = tmp.name
for dev in devices: for dev in devices:
switch = switches.get(dev['switch_hostname']) switch = switches.get(dev['switch_hostname'])
if not switch: if not switch:
continue # Switch existiert nicht, überspringen continue
password = decrypt_password(switch['password']) password = decrypt_password(switch['password'])
# Format: IP-Device:Hostname-Device:IP-Switch:Hostname-Switch:Port-Switch:Username-Switch:Password-Switch port = dev['port'] or ""
line = f"{dev['rpi_ip']}:{dev['name']}:{switch['ip']}:{switch['hostname']}:{dev['port']}:{switch['username']}:{password}\n" print(f"{dev['rpi_ip']}:{dev['name']}:{switch['ip']}:{switch['hostname']}:{port}:{switch['username']}:{password}")
tmp.write(line)
tmp.close()
return tmp_path
if __name__ == "__main__": if __name__ == "__main__":
path = generate_ips_list() generate_ips_list()
print(path) # optional, gibt die Tempdatei zurück

Binary file not shown.

View File

@@ -17,7 +17,7 @@ h1, h2, h3, h4, h5, h6 {
} }
pre { pre {
background-color: #f8f9fa; background-color: #f8f9fa7c;
padding: 10px; padding: 10px;
border-radius: 5px; border-radius: 5px;
max-height: 600px; max-height: 600px;
@@ -28,6 +28,11 @@ pre {
margin-right: 5px; margin-right: 5px;
} }
#dashboard-timer {
font-size: 0.9rem;
padding: 0.25rem 0.5rem;
}
.device-card { .device-card {
border: 1px solid #ccc; border: 1px solid #ccc;
border-radius: 5px; border-radius: 5px;
@@ -53,7 +58,7 @@ pre {
} }
.content-wrapper { .content-wrapper {
max-width: 1024px; max-width: 1080px;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
} }
@@ -77,7 +82,7 @@ pre {
.navbar-logo img { .navbar-logo img {
display: flex; display: flex;
justify-content: flex-start; justify-content: flex-start;
height: 60px; /* Logo Höhe, kann angepasst werden */ height: 83px; /* Logo Höhe, kann angepasst werden */
} }
#log-container { #log-container {
@@ -102,3 +107,63 @@ pre {
font-size: 0.9em; font-size: 0.9em;
color: gray; color: gray;
} }
/* Tabelle anpassen */
.custom-table input,
.custom-table select {
height: 28px;
padding: 0.25rem 0.5rem;
font-size: 0.9rem;
}
/* Spaltenbreiten */
.custom-table .col-small {
width: 140px; /* Hostname, Port */
}
.custom-table .col-ip {
width: 140px; /* IP-Adresse schmaler */
}
.custom-table .col-mac {
width: 140px; /* MAC-Adresse schmaler */
}
/* Checkbox sichtbar auch bei disabled */
.checkbox-visible:disabled {
opacity: 1; /* nicht ausgegraut */
cursor: not-allowed;
}
/* Aktiv-Label für Neues Gerät */
label.text-white {
color: white !important;
}
.checkbox-visible:not(:checked) {
accent-color: #dc3545; /* rot wenn unchecked */
}
.checkbox-visible:disabled {
cursor: not-allowed;
opacity: 1; /* verhindert Ausgrauen */
}
.custom-actions {
margin: auto 0;
background: #fff;
}
.black {
color: #000000;
}
.white {
color: #ffffff;
}
button.login {
background-color: #ff7100;
border-color: #ff7300a9;;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -19,7 +19,7 @@
<a href="{{ url_for('index') }}" class="btn btn-secondary">Dashboard</a> <a href="{{ url_for('index') }}" class="btn btn-secondary">Dashboard</a>
<a href="{{ url_for('devices') }}" class="btn btn-secondary">Devices</a> <a href="{{ url_for('devices') }}" class="btn btn-secondary">Devices</a>
{% if current_user.is_admin %} {% if current_user.is_admin %}
<a href="{{ url_for('switches') }}" class="btn btn-secondary">Switches</a> <a href="{{ url_for('switches') }}" class="btn btn-secondary">Switche</a>
<a href="{{ url_for('users') }}" class="btn btn-secondary">Users</a> <a href="{{ url_for('users') }}" class="btn btn-secondary">Users</a>
<a href="{{ url_for('logs') }}" class="btn btn-secondary">Live-Log</a> <a href="{{ url_for('logs') }}" class="btn btn-secondary">Live-Log</a>
<a href="{{ url_for('settings') }}" class="btn btn-secondary">Settings</a> <a href="{{ url_for('settings') }}" class="btn btn-secondary">Settings</a>

View File

@@ -3,32 +3,9 @@
<h2>Devices</h2> <h2>Devices</h2>
{% with messages = get_flashed_messages() %}
{% if messages %}
<div class="mt-2 alert alert-info">
{% for message in messages %}{{ message }}<br>{% endfor %}
</div>
{% endif %}
{% endwith %}
{% if current_user.is_admin %} {% if current_user.is_admin %}
<h4>Neues Gerät hinzufügen</h4> <!-- Neues Gerät hinzufügen -->
<form method="post" class="row g-2 mb-4"> <button class="btn btn-success mb-3" data-bs-toggle="modal" data-bs-target="#deviceModal" onclick="openDeviceModal()">Neues Gerät hinzufügen</button>
<input type="hidden" name="add_device" value="1">
<div class="col"><input type="text" name="name" placeholder="Hostname" class="form-control" required></div>
<div class="col"><input type="text" name="rpi_ip" placeholder="IP-Adresse" class="form-control" required></div>
<div class="col"><input type="text" name="mac" placeholder="MAC-Adresse" class="form-control" required></div>
<div class="col"><input type="text" name="port" placeholder="Port" class="form-control" required></div>
<div class="col">
<select name="switch_hostname" class="form-control" required>
<option value="">Switch wählen</option>
{% for sw in switches %}
<option value="{{ sw['hostname'] }}">{{ sw['hostname'] }}</option>
{% endfor %}
</select>
</div>
<div class="col"><button class="btn btn-success w-100">Hinzufügen</button></div>
</form>
{% endif %} {% endif %}
<table class="table table-bordered"> <table class="table table-bordered">
@@ -39,45 +16,279 @@
<th>MAC-Adresse</th> <th>MAC-Adresse</th>
<th>Switch</th> <th>Switch</th>
<th>Port</th> <th>Port</th>
{% if current_user.is_admin %}<th>Status</th>{% endif %}
{% if current_user.is_admin %}<th>Aktionen</th>{% endif %} {% if current_user.is_admin %}<th>Aktionen</th>{% endif %}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for d in devices %} {% for d in devices %}
<tr> <tr>
{% if current_user.is_admin %}
<form method="post">
<input type="hidden" name="edit_device" value="1">
<input type="hidden" name="old_mac" value="{{ d['mac'] }}">
<td><input name="name" value="{{ d['name'] }}" class="form-control" required></td>
<td><input name="rpi_ip" value="{{ d['rpi_ip'] }}" class="form-control" required></td>
<td><input name="mac" value="{{ d['mac'] }}" class="form-control" required></td>
<td>
<select name="switch_hostname" class="form-control" required>
{% for sw in switches %}
<option value="{{ sw['hostname'] }}" {% if sw['hostname']==d['switch_hostname'] %}selected{% endif %}>
{{ sw['hostname'] }}
</option>
{% endfor %}
</select>
</td>
<td><input name="port" value="{{ d['port'] }}" class="form-control" required></td>
<td class="d-flex gap-1">
<button class="btn btn-primary btn-sm">Speichern</button>
<button name="delete_device" value="{{ d['mac'] }}" class="btn btn-danger btn-sm"
onclick="return confirm('Willst du das Gerät wirklich löschen?');">Löschen</button>
</td>
</form>
{% else %}
<td>{{ d['name'] }}</td> <td>{{ d['name'] }}</td>
<td>{{ d['rpi_ip'] }}</td> <td>{{ d['rpi_ip'] }}</td>
<td>{{ d['mac'] }}</td> <td>{{ d['mac'] }}</td>
<td>{{ d['switch_hostname'] }}</td> <td>{{ d['switch_hostname'] or '-' }}</td>
<td>{{ d['port'] }}</td> <td>{{ d['port'] }}</td>
{% if current_user.is_admin %}
<td>
<button class="btn btn-sm {% if d['is_active'] %}btn-success{% else %}btn-secondary{% endif %}"
onclick="toggleDevice('{{ d['mac'] }}', this)">
{% if d['is_active'] %}Deaktivieren{% else %}Aktivieren{% endif %}
</button>
</td>
<td>
<!-- Bearbeiten Modal -->
<button class="btn btn-secondary btn-sm button login white" data-bs-toggle="modal" data-bs-target="#editDeviceModal"
onclick="openEditDeviceModal('{{ d['mac'] }}','{{ d['name'] }}','{{ d['rpi_ip'] }}','{{ d['port'] }}')">
Bearbeiten
</button>
<!-- Switch ändern Modal -->
<button class="btn btn-secondary btn-sm button login white" data-bs-toggle="modal" data-bs-target="#switchModal"
onclick="openSwitchModal('{{ d['mac'] }}','{{ d['switch_hostname'] }}')">
Switch ändern
</button>
<!-- Löschen -->
<form method="post" style="display:inline;">
<input type="hidden" name="delete_device" value="{{ d['mac'] }}">
<button class="btn btn-danger btn-sm" onclick="return confirm('Willst du das Gerät wirklich löschen?');">
Löschen
</button>
</form>
</td>
{% endif %} {% endif %}
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
<!-- Modal: Neues Gerät -->
<div class="modal fade" id="deviceModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form method="post" onsubmit="return validateDeviceForm(this);">
<div class="modal-header">
<h5 class="modal-title black">Neues Gerät hinzufügen</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" name="add_device" value="1">
<div class="mb-3">
<label>Hostname</label>
<input type="text" name="name" class="form-control" required placeholder="z.B. Sensor01">
</div>
<div class="mb-3">
<label>IP-Adresse</label>
<input type="text" name="rpi_ip" class="form-control" required placeholder="z.B. 192.168.1.100">
<div class="invalid-feedback">Bitte eine gültige IP-Adresse eingeben (z. B. 192.168.1.100).</div>
</div>
<div class="mb-3">
<label>MAC-Adresse</label>
<input type="text" name="mac" class="form-control" required placeholder="z.B. AA:BB:CC:DD:EE:FF">
<div class="invalid-feedback">Bitte eine gültige MAC-Adresse eingeben (xx:xx:xx:xx:xx:xx).</div>
</div>
<div class="mb-3">
<label>Port</label>
<input type="text" name="port" class="form-control" required placeholder="z.B. 3">
</div>
<div class="mb-3">
<label>Switch</label>
<select name="switch_hostname" class="form-select" required>
{% for sw in switches %}
<option value="{{ sw['hostname'] }}">{{ sw['hostname'] }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-success button login" type="submit">Hinzufügen</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
</div>
</form>
</div>
</div>
</div>
<!-- Modal: Bearbeiten -->
<div class="modal fade" id="editDeviceModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form method="post" onsubmit="return validateDeviceForm(this);">
<input type="hidden" name="edit_device" value="1">
<input type="hidden" name="old_mac" id="edit_old_mac">
<div class="modal-header">
<h5 class="modal-title black">Gerät bearbeiten</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label>Hostname</label>
<input type="text" name="name" id="edit_name" class="form-control" required placeholder="z.B. Sensor01">
</div>
<div class="mb-3">
<label>IP-Adresse</label>
<input type="text" name="rpi_ip" id="edit_ip" class="form-control" required placeholder="z.B. 192.168.1.100">
<div class="invalid-feedback">Bitte eine gültige IP-Adresse eingeben (z. B. 192.168.1.100).</div>
</div>
<div class="mb-3">
<label>MAC-Adresse</label>
<input type="text" name="mac" id="edit_mac" class="form-control" required placeholder="z.B. AA:BB:CC:DD:EE:FF">
<div class="invalid-feedback">Bitte eine gültige MAC-Adresse eingeben (xx:xx:xx:xx:xx:xx).</div>
</div>
<div class="mb-3">
<label>Port</label>
<input type="text" name="port" id="edit_port" class="form-control" required placeholder="z.B. 3">
</div>
</div>
<div class="modal-footer">
<button class="btn btn-primary button login" type="submit">Speichern</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
</div>
</form>
</div>
</div>
</div>
<!-- Modal: Switch ändern -->
<div class="modal fade" id="switchModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form method="post">
<input type="hidden" name="edit_device" value="1">
<input type="hidden" name="old_mac" id="switch_mac">
<div class="modal-header">
<h5 class="modal-title black">Switch ändern</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<select name="switch_hostname" id="switch_select" class="form-select" required>
{% for sw in switches %}
<option value="{{ sw['hostname'] }}">{{ sw['hostname'] }}</option>
{% endfor %}
</select>
</div>
<div class="modal-footer">
<button class="btn btn-primary button login" type="submit">Speichern</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
</div>
</form>
</div>
</div>
</div>
<script>
function openDeviceModal() {
document.querySelector("#deviceModal form").reset();
}
function openEditDeviceModal(mac, name, ip, port) {
document.getElementById("edit_old_mac").value = mac;
document.getElementById("edit_name").value = name;
document.getElementById("edit_ip").value = ip;
document.getElementById("edit_mac").value = mac;
document.getElementById("edit_port").value = port;
}
function openSwitchModal(mac, switch_hostname) {
document.getElementById("switch_mac").value = mac;
document.getElementById("switch_select").value = switch_hostname;
}
// -------------------
// IP-Validierung
// -------------------
const ipPattern = /^(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}$/;
// -------------------
// MAC-Validierung
// -------------------
const macPattern = /^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$/;
function validateIP(input) {
if (!ipPattern.test(input.value)) {
input.classList.add("is-invalid");
return false;
} else {
input.classList.remove("is-invalid");
return true;
}
}
function validateMAC(input) {
if (!macPattern.test(input.value)) {
input.classList.add("is-invalid");
return false;
} else {
input.classList.remove("is-invalid");
return true;
}
}
function validateDeviceForm(form) {
const ipInput = form.querySelector("input[name='rpi_ip']");
const macInput = form.querySelector("input[name='mac']");
let valid = true;
if (ipInput) valid = validateIP(ipInput) && valid;
if (macInput) valid = validateMAC(macInput) && valid;
return valid;
}
// Live-Feedback beim Tippen
document.addEventListener("input", function(e) {
if (e.target.name === "rpi_ip") validateIP(e.target);
if (e.target.name === "mac") validateMAC(e.target);
});
// Aktivieren/Deaktivieren
function toggleDevice(mac, btn) {
fetch(`/devices/toggle/${mac}`, { method: 'POST' })
.then(resp => resp.json())
.then(data => {
if (data.success) {
// Button aktualisieren
if (data.new_status === 1) {
btn.classList.remove('btn-secondary');
btn.classList.add('btn-success');
btn.innerText = 'Deaktivieren';
} else {
btn.classList.remove('btn-success');
btn.classList.add('btn-secondary');
btn.innerText = 'Aktivieren';
}
// Flash-Nachricht im Frontend aktualisieren
let flashContainer = document.getElementById('flash-messages');
if (!flashContainer) {
flashContainer = document.createElement('div');
flashContainer.id = 'flash-messages';
flashContainer.style.position = 'fixed';
flashContainer.style.top = '10px';
flashContainer.style.right = '10px';
flashContainer.style.zIndex = 1050;
document.body.appendChild(flashContainer);
}
const flash = document.createElement('div');
flash.className = 'alert alert-success alert-dismissible fade show';
flash.role = 'alert';
flash.innerHTML = `
${data.msg}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
flashContainer.appendChild(flash);
// Automatisch nach 3 Sekunden ausblenden
setTimeout(() => {
flash.classList.remove('show');
flash.classList.add('hide');
flash.addEventListener('transitionend', () => flash.remove());
}, 3000);
} else {
console.error(data.msg);
}
})
.catch(err => console.error(err));
}
</script>
{% endblock %} {% endblock %}

View File

@@ -1,15 +1,28 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
<h2>Dashboard</h2> <h2 class="d-flex justify-content-between align-items-center">
Dashboard
<span id="dashboard-timer" class="badge bg-success">
Nächste Prüfung in -- Sekunden
</span>
</h2>
<div class="row row-cols-1 row-cols-md-6 g-3"> <div class="row row-cols-1 row-cols-md-6 g-3">
{% for d in devices %} {% for d in devices %}
<div class="col"> <div class="col">
<div class="card text-center p-2"> <div class="card text-center p-2">
<div class="card-header">{{ d[1] }}</div> <div class="card-header">{{ d[1] }}</div>
<div class="card-body"> <div class="card-body">
<span class="fw-bold" style="color: {% if status[d[0]]=='online' %}green{% else %}red{% endif %};"> <span class="fw-bold" style="color:
{% if status[d[0]] %}{{ status[d[0]]|capitalize }}{% else %}unbekannt{% endif %} {% if d[2] == 0 %}gray
{% elif status[d[0]]=='online' %}green
{% else %}red
{% endif %};">
{% if d[2] == 0 %}
Deaktiviert
{% else %}
{% if status[d[0]] %}{{ status[d[0]]|capitalize }}{% else %}Unbekannt{% endif %}
{% endif %}
</span> </span>
</div> </div>
</div> </div>
@@ -17,4 +30,60 @@
{% endfor %} {% endfor %}
</div> </div>
<script>
document.addEventListener("DOMContentLoaded", () => {
const intervalMinutes = {{ interval | int }}; // aus DB
const intervalMilliseconds = intervalMinutes * 60 * 1000;
let lastUpdateTime = Date.now();
function parseLogTimestamp(ts) {
const parts = ts.match(/^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/);
if (!parts) return Date.now();
const [, year, month, day, hour, minute, second] = parts.map(Number);
return new Date(year, month - 1, day, hour, minute, second).getTime();
}
function updateTimer() {
const now = Date.now();
const elapsed = now - lastUpdateTime;
const remainingMs = intervalMilliseconds - (elapsed % intervalMilliseconds);
const remainingSec = Math.ceil(remainingMs / 1000);
document.getElementById("dashboard-timer").innerText =
`Nächste Prüfung in ${remainingSec} Sekunden`;
if (remainingSec <= 1) {
// Timer abgelaufen → Reload starten
window.location.reload();
}
}
function fetchLastLog() {
fetch("{{ url_for('get_log') }}")
.then(response => response.text())
.then(data => {
const lines = data.split("\n");
let lastSepIndex = -1;
for (let i = lines.length - 1; i >= 0; i--) {
if (lines[i].startsWith("--------------------------------------------------------------------")) {
lastSepIndex = i;
break;
}
}
if (lastSepIndex >= 0 && lastSepIndex + 1 < lines.length) {
const firstLine = lines[lastSepIndex + 1];
const match = firstLine.match(/^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/);
if (match) lastUpdateTime = parseLogTimestamp(match[1]);
}
})
.catch(err => console.error("Fehler beim Laden der Logs:", err));
}
// Timer alle 1 Sekunde aktualisieren
setInterval(updateTimer, 1000);
// einmal beim Laden die letzte Log-Zeit setzen
fetchLastLog();
});
</script>
{% endblock %} {% endblock %}

View File

@@ -19,14 +19,14 @@
<form method="post" class="w-25"> <form method="post" class="w-25">
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Username</label> <label class="form-label label text-white">Username</label>
<input type="text" name="username" class="form-control" required> <input type="text" name="username" class="form-control" required>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Password</label> <label class="form-label label text-white">Password</label>
<input type="password" name="password" class="form-control" required> <input type="password" name="password" class="form-control" required>
</div> </div>
<button class="btn btn-primary">Login</button> <button class="btn btn-primary button login">Login</button>
</form> </form>
</body> </body>

View File

@@ -3,6 +3,10 @@
<h2>Live Log</h2> <h2>Live Log</h2>
<button id="refresh-btn" class="btn btn-success mb-3">
<span style="font-size: 1.2rem; color: white;"></span> Logs aktualisieren
</button>
<div id="log-container"> <div id="log-container">
<div id="log-box"></div> <div id="log-box"></div>
<div id="refresh-timer"> <div id="refresh-timer">
@@ -11,36 +15,65 @@
</div> </div>
<script> <script>
const intervalMinutes = {{ interval | int }}; function parseLogTimestamp(ts) {
const intervalMilliseconds = intervalMinutes * 60 * 1000; const parts = ts.match(/^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/);
let lastUpdateTime = Date.now(); if (!parts) return Date.now();
const [, year, month, day, hour, minute, second] = parts.map(Number);
function fetchLog() { return new Date(year, month - 1, day, hour, minute, second).getTime();
fetch("{{ url_for('get_log') }}")
.then(response => response.text())
.then(data => {
const box = document.getElementById("log-box");
const filteredLines = data
.split("\n")
.filter(line => !line.includes("ist erreichbar!"))
.join("\n");
box.innerText = filteredLines;
box.scrollTop = box.scrollHeight;
lastUpdateTime = Date.now();
})
.catch(err => console.error("Fehler beim Laden der Logs:", err));
} }
function updateTimer() { document.addEventListener("DOMContentLoaded", () => {
const now = Date.now(); const intervalMinutes = {{ interval | int }};
const remainingMs = intervalMilliseconds - (now - lastUpdateTime); const intervalMilliseconds = intervalMinutes * 60 * 1000;
const remainingSec = Math.max(Math.ceil(remainingMs / 1000), 0); let lastUpdateTime = Date.now();
document.getElementById("timer").innerText = remainingSec;
}
setInterval(updateTimer, 1000); function fetchLog() {
fetchLog(); fetch("{{ url_for('get_log') }}")
setInterval(fetchLog, intervalMilliseconds); .then(res => res.text())
.then(data => {
const box = document.getElementById("log-box");
const filteredLines = data
.split("\n");
box.innerText = filteredLines.join("\n");
box.scrollTop = box.scrollHeight;
// letzte Separator-Linie
let lastSep = -1;
for (let i = filteredLines.length - 1; i >= 0; i--) {
if (filteredLines[i].startsWith("--------------------------------------------------------------------")) {
lastSep = i;
break;
}
}
if (lastSep >= 0 && lastSep + 1 < filteredLines.length) {
const firstLine = filteredLines[lastSep + 1];
const match = firstLine.match(/^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/);
if (match) {
lastUpdateTime = parseLogTimestamp(match[1]);
} else {
lastUpdateTime = Date.now();
}
} else {
lastUpdateTime = Date.now();
}
})
.catch(err => console.error(err));
}
function updateTimer() {
const now = Date.now();
const elapsed = now - lastUpdateTime;
const remainingMs = intervalMilliseconds - (elapsed % intervalMilliseconds);
const remainingSec = Math.ceil(remainingMs / 1000);
document.getElementById("timer").innerText = remainingSec;
}
document.getElementById("refresh-btn").addEventListener("click", fetchLog);
fetchLog();
setInterval(fetchLog, intervalMilliseconds);
setInterval(updateTimer, 1000);
});
</script> </script>
{% endblock %} {% endblock %}

View File

@@ -5,20 +5,11 @@
<form method="post" class="row g-2"> <form method="post" class="row g-2">
<div class="col-auto"> <div class="col-auto">
<label for="interval" class="form-label">Prüfintervall (Minuten):</label> <label for="interval" class="form-label white">Prüfintervall (Minuten):</label>
<input type="number" name="interval" id="interval" class="form-control" value="{{ interval }}" min="1" required> <input type="number" name="interval" id="interval" class="form-control" value="{{ interval }}" min="1" required>
</div> </div>
<div class="col-auto align-self-end"> <div class="col-auto align-self-end">
<button type="submit" class="btn btn-success">Speichern & Service neustarten</button> <button type="submit" class="btn btn-success">Speichern & Service neustarten</button>
</div> </div>
</form> </form>
{% with messages = get_flashed_messages() %}
{% if messages %}
<div class="mt-2 alert alert-info">
{% for message in messages %}{{ message }}<br>{% endfor %}
</div>
{% endif %}
{% endwith %}
{% endblock %} {% endblock %}

View File

@@ -1,19 +1,9 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
<h2>Switch Verwaltung</h2>
<h2>Switche</h2> <!-- Button zum Hinzufügen -->
<button class="btn btn-success mb-3" data-bs-toggle="modal" data-bs-target="#addSwitchModal">Neuen Switch hinzufügen</button>
{% if current_user.is_admin %}
<h4>Neuen Switch hinzufügen</h4>
<form method="post" class="row g-2 mb-4">
<input type="hidden" name="add_switch" value="1">
<div class="col"><input type="text" name="hostname" placeholder="Hostname" class="form-control" required></div>
<div class="col"><input type="text" name="ip" placeholder="IP-Adresse" class="form-control" required></div>
<div class="col"><input type="text" name="username" placeholder="Username" class="form-control" required></div>
<div class="col"><input type="password" name="password" placeholder="Password" class="form-control" required></div>
<div class="col"><button class="btn btn-success w-100">Hinzufügen</button></div>
</form>
{% endif %}
<table class="table table-bordered"> <table class="table table-bordered">
<thead> <thead>
@@ -21,33 +11,173 @@
<th>Hostname</th> <th>Hostname</th>
<th>IP-Adresse</th> <th>IP-Adresse</th>
<th>Username</th> <th>Username</th>
{% if current_user.is_admin %}<th>Aktionen</th>{% endif %} <th>Aktionen</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for s in switches %} {% for s in switches %}
<tr> <tr>
{% if current_user.is_admin %}
<form method="post">
<input type="hidden" name="edit_switch" value="1">
<input type="hidden" name="old_hostname" value="{{ s['hostname'] }}">
<td><input name="hostname" value="{{ s['hostname'] }}" class="form-control" required></td>
<td><input name="ip" value="{{ s['ip'] }}" class="form-control" required></td>
<td><input name="username" value="{{ s['username'] }}" class="form-control" required></td>
<td class="d-flex gap-1">
<button class="btn btn-primary btn-sm">Speichern</button>
<button name="delete_switch" value="{{ s['hostname'] }}" class="btn btn-danger btn-sm"
onclick="return confirm('Willst du den Switch wirklich löschen?');">Löschen</button>
</td>
</form>
{% else %}
<td>{{ s['hostname'] }}</td> <td>{{ s['hostname'] }}</td>
<td>{{ s['ip'] }}</td> <td>{{ s['ip'] }}</td>
<td>{{ s['username'] }}</td> <td>{{ s['username'] }}</td>
{% endif %} <td>
<!-- Bearbeiten -->
<button class="btn btn-sm button login white" data-bs-toggle="modal" data-bs-target="#editSwitchModal{{ loop.index }}">Bearbeiten</button>
<!-- Löschen -->
<form method="post" action="{{ url_for('delete_switch', hostname=s['hostname']) }}" style="display:inline;">
<button class="btn btn-danger btn-sm" onclick="return confirm('Willst du den Switch wirklich löschen?');">
Löschen
</button>
</form>
</td>
</tr> </tr>
<!-- Bearbeiten Modal -->
<div class="modal fade" id="editSwitchModal{{ loop.index }}" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title text-dark">Switch bearbeiten</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form method="post" onsubmit="return validateSwitchForm(this, '{{ loop.index }}')">
<div class="modal-body">
<input type="hidden" name="edit_switch" value="1">
<input type="hidden" name="old_hostname" value="{{ s['hostname'] }}">
<div class="mb-3">
<label class="form-label">Hostname</label>
<input type="text" name="hostname" class="form-control" value="{{ s['hostname'] }}" required>
</div>
<div class="mb-3">
<label class="form-label">IP-Adresse</label>
<input type="text" name="ip" class="form-control" value="{{ s['ip'] }}" required placeholder="z.B. 192.168.1.100">
<div class="invalid-feedback">Bitte eine gültige IP-Adresse eingeben (z. B. 192.168.1.100).</div>
</div>
<div class="mb-3">
<label class="form-label">Username</label>
<input type="text" name="username" class="form-control" value="{{ s['username'] }}" required>
</div>
<div class="mb-3">
<label class="form-label">Neues Passwort</label>
<input type="password" id="password_edit_{{ loop.index }}" name="password" class="form-control" placeholder="Neues Passwort (optional)">
</div>
<div class="mb-3">
<label class="form-label">Passwort bestätigen</label>
<input type="password" id="password_confirm_edit_{{ loop.index }}" name="password_confirm" class="form-control" placeholder="Bestätigen">
<div class="invalid-feedback">Passwörter stimmen nicht überein!</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button class="btn button login white" type="submit">Speichern</button>
</div>
</form>
</div>
</div>
</div>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
<!-- Hinzufügen Modal -->
<div class="modal fade" id="addSwitchModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title text-dark">Neuen Switch hinzufügen</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form method="post" onsubmit="return validateSwitchForm(this, 'add')">
<div class="modal-body">
<input type="hidden" name="add_switch" value="1">
<div class="mb-3">
<label class="form-label">Hostname</label>
<input type="text" name="hostname" class="form-control" required placeholder="z.B. Switch01">
</div>
<div class="mb-3">
<label class="form-label">IP-Adresse</label>
<input type="text" name="ip" class="form-control" required placeholder="z.B. 192.168.1.100">
<div class="invalid-feedback">Bitte eine gültige IP-Adresse eingeben (z. B. 192.168.1.100).</div>
</div>
<div class="mb-3">
<label class="form-label">Username</label>
<input type="text" name="username" class="form-control" required placeholder="z.B. admin">
</div>
<div class="mb-3">
<label class="form-label">Passwort</label>
<input type="password" id="password_add" name="password" class="form-control" required placeholder="z.B. geheim123">
</div>
<div class="mb-3">
<label class="form-label">Passwort bestätigen</label>
<input type="password" id="password_confirm_add" name="password_confirm" class="form-control" required placeholder="Passwort wiederholen">
<div class="invalid-feedback">Passwörter stimmen nicht überein!</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button class="btn button login white" type="submit">Hinzufügen</button>
</div>
</form>
</div>
</div>
</div>
<!-- JS: Passwort + IP-Validierung -->
<script>
// Passwortcheck
function validatePassword(id, isEdit) {
let pass = document.getElementById(isEdit ? `password_edit_${id}` : `password_${id}`) || document.getElementById('password_add');
let confirm = document.getElementById(isEdit ? `password_confirm_edit_${id}` : `password_confirm_${id}`) || document.getElementById('password_confirm_add');
if (!pass || !confirm) return true;
if (pass.value !== confirm.value) {
confirm.classList.add("is-invalid");
return false;
} else {
confirm.classList.remove("is-invalid");
return true;
}
}
// -------------------
// IP-Validierung
// -------------------
const ipPattern = /^(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}$/;
function validateIP(input) {
if (!ipPattern.test(input.value)) {
input.classList.add("is-invalid");
return false;
} else {
input.classList.remove("is-invalid");
return true;
}
}
function validateSwitchForm(form, id) {
const ipInput = form.querySelector("input[name='ip']");
let valid = true;
if (ipInput) valid = validateIP(ipInput) && valid;
// Passwortcheck
valid = validatePassword(id, id !== 'add') && valid;
return valid;
}
// Live-Feedback IP & Passwort
document.addEventListener('input', e => {
if (e.target.name === "ip") validateIP(e.target);
if (e.target.id.startsWith('password_confirm')) {
const id = e.target.id.replace('password_confirm_', '');
const pass = document.getElementById('password_' + id);
if (pass && pass.value !== e.target.value) {
e.target.classList.add('is-invalid');
} else {
e.target.classList.remove('is-invalid');
}
}
});
</script>
{% endblock %} {% endblock %}

View File

@@ -11,27 +11,27 @@
<table class="table table-bordered"> <table class="table table-bordered">
<thead> <thead>
<tr> <tr>
<th>Username</th> <th class="col-ip">Username</th>
<th>Rolle</th> <th class="col-ip">Rolle</th>
{% if current_user.is_admin %}<th>Aktionen</th>{% endif %} {% if current_user.is_admin %}<th>Aktionen</th>{% endif %}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for u in users %} {% for u in users %}
<tr> <tr>
<td>{{ u['username'] }}</td> <td class="col-small">{{ u['username'] }}</td>
<td>{% if u['is_admin'] %}Admin{% else %}User{% endif %}</td> <td class="col-small">{% if u['is_admin'] %}Admin{% else %}User{% endif %}</td>
{% if current_user.is_admin %} {% if current_user.is_admin %}
<td> <td class="custom-actions d-flex gap-1 align-items-center">
<!-- Rolle ändern --> <!-- Rolle ändern -->
<button class="btn btn-primary btn-sm" data-bs-toggle="modal" <button class="btn btn-sm button login white" data-bs-toggle="modal"
data-bs-target="#roleModal" data-bs-target="#roleModal"
onclick="openRoleModal({{ u['id'] }}, '{{ u['username'] }}', {{ u['is_admin'] }})"> onclick="openRoleModal({{ u['id'] }}, '{{ u['username'] }}', {{ u['is_admin'] }})">
Rolle ändern Rolle ändern
</button> </button>
<!-- Passwort ändern --> <!-- Passwort ändern -->
<button class="btn btn-warning btn-sm" data-bs-toggle="modal" <button class="btn btn-sm button login white" data-bs-toggle="modal"
data-bs-target="#passwordModal" data-bs-target="#passwordModal"
onclick="openPasswordModal({{ u['id'] }}, '{{ u['username'] }}')"> onclick="openPasswordModal({{ u['id'] }}, '{{ u['username'] }}')">
Passwort ändern Passwort ändern
@@ -49,13 +49,14 @@
</tbody> </tbody>
</table> </table>
<!-- Modals bleiben unverändert -->
<!-- Modal für neuen Benutzer --> <!-- Modal für neuen Benutzer -->
<div class="modal fade" id="userModal" tabindex="-1" aria-hidden="true"> <div class="modal fade" id="userModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
<form method="post" id="userForm"> <form method="post" id="userForm">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title">Neuen Benutzer anlegen</h5> <h5 class="modal-title black">Neuen Benutzer anlegen</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button> <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
@@ -90,7 +91,7 @@
<div class="modal-content"> <div class="modal-content">
<form method="post" id="roleForm"> <form method="post" id="roleForm">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title">Rolle ändern</h5> <h5 class="modal-title black">Rolle ändern</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button> <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
@@ -122,7 +123,7 @@
<div class="modal-content"> <div class="modal-content">
<form method="post" id="passwordForm"> <form method="post" id="passwordForm">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title">Passwort ändern</h5> <h5 class="modal-title black">Passwort ändern</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button> <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
@@ -159,7 +160,6 @@ function openRoleModal(user_id, username, is_admin) {
function openPasswordModal(user_id, username) { function openPasswordModal(user_id, username) {
document.getElementById('password_user_id').value = user_id; document.getElementById('password_user_id').value = user_id;
document.getElementById('password_username').value = username; document.getElementById('password_username').value = username;
// Nur das Passwort-Feld zurücksetzen
document.querySelector("#passwordForm input[name='new_password']").value = ""; document.querySelector("#passwordForm input[name='new_password']").value = "";
} }
</script> </script>

View File

@@ -1,8 +0,0 @@
#IP-RPI:IP-SWITCH:PORT:NAME
192.168.120.104:192.168.200.117:37:HAP04
192.168.120.110:192.168.200.116:1/14:HAP10
192.168.120.114:192.168.200.116:1/26:HAP14
192.168.120.115:192.168.200.116:1/23:HAP15
192.168.120.118:192.168.200.116:1/34:HAP18
192.168.120.123:192.168.200.117:36:HAP23
192.168.120.131:192.168.200.118:1/36:HAP31

View File

@@ -1,9 +1,20 @@
#!/bin/bash #!/bin/bash
# ============================================================================
# PoE Device Check Script
# - prüft Erreichbarkeit von Geräten
# - startet PoE-Port bei Ausfall neu
# - loggt Ereignisse
# - löscht alte Logfiles (>30 Tage)
# ============================================================================
LOGFILE="/var/log/rpi-$(date '+%Y%m%d%H%M%S').log" LOG_DIR="/var/log"
LOGFILE="$LOG_DIR/rpi-$(date '+%Y%m%d%H%M%S').log"
# Intervall aus DB (Sekunden) abrufen # Alte Logfiles löschen (älter als 30 Tage)
SLEEP=$(python3 - <<END find "$LOG_DIR" -type f -name "rpi-*.log" -mtime +30 -delete
# Intervall aus DB abrufen
SLEEP=$(python3 - <<'END'
import sqlite3 import sqlite3
conn = sqlite3.connect("/srv/poe_manager/sqlite.db") conn = sqlite3.connect("/srv/poe_manager/sqlite.db")
row = conn.execute("SELECT value FROM settings WHERE key='check_interval'").fetchone() row = conn.execute("SELECT value FROM settings WHERE key='check_interval'").fetchone()
@@ -12,8 +23,7 @@ print(row[0] if row else 300)
END END
) )
# Umrechnung falls nötig SLEEP=${SLEEP:-300}
SLEEP=${SLEEP:-300} # default 300 Sekunden
function disable_poe() { function disable_poe() {
local switch_ip=$1 local switch_ip=$1
@@ -23,7 +33,6 @@ function disable_poe() {
expect <<EOF expect <<EOF
set timeout 5 set timeout 5
spawn ssh $username@$switch_ip spawn ssh $username@$switch_ip
expect { expect {
"assword:" { send "$password\r"; exp_continue } "assword:" { send "$password\r"; exp_continue }
"Press any key" { send "\r"; exp_continue } "Press any key" { send "\r"; exp_continue }
@@ -40,8 +49,7 @@ expect "(config)#"
send "exit\r" send "exit\r"
expect "#" expect "#"
send "exit\r" send "exit\r"
expect ">" expect ">"; send "exit\r"
send "exit\r"
expect "Do you want to log out (y/n)?" { send "y\r" } expect "Do you want to log out (y/n)?" { send "y\r" }
expect eof expect eof
EOF EOF
@@ -55,7 +63,6 @@ function enable_poe() {
expect <<EOF expect <<EOF
set timeout 5 set timeout 5
spawn ssh $username@$switch_ip spawn ssh $username@$switch_ip
expect { expect {
"assword:" { send "$password\r"; exp_continue } "assword:" { send "$password\r"; exp_continue }
"Press any key" { send "\r"; exp_continue } "Press any key" { send "\r"; exp_continue }
@@ -72,30 +79,31 @@ expect "(config)#"
send "exit\r" send "exit\r"
expect "#" expect "#"
send "exit\r" send "exit\r"
expect ">" expect ">"; send "exit\r"
send "exit\r"
expect "Do you want to log out (y/n)?" { send "y\r" } expect "Do you want to log out (y/n)?" { send "y\r" }
expect eof expect eof
EOF EOF
} }
echo "" > $LOGFILE echo "" > "$LOGFILE"
while true; do while true; do
echo "--------------------------------------------------------------------" >> $LOGFILE echo "--------------------------------------------------------------------" >> "$LOGFILE"
IP_FILE=$(python3 /srv/poe_manager/generate_ips.py) python3 /srv/poe_manager/generate_ips.py | while IFS=: read -r rpi_ip dev_name switch_ip switch_hostname switch_port switch_user switch_pass; do
while IFS=: read -r rpi_ip dev_name switch_ip switch_hostname switch_port switch_user switch_pass; do if ping -c 1 -W 2 "$rpi_ip" &> /dev/null; then
ping -c 1 -W 2 "$rpi_ip" &> /dev/null echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name ist erreichbar!" >> "$LOGFILE"
if [ $? -ne 0 ]; then
echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name ist nicht erreichbar!" >> $LOGFILE
disable_poe "$switch_ip" "$switch_port" "$switch_user" "$switch_pass"
echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name PoE auf Port $switch_port am Switch $switch_hostname deaktiviert." >> $LOGFILE
sleep 2
enable_poe "$switch_ip" "$switch_port" "$switch_user" "$switch_pass"
echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name PoE auf Port $switch_port am Switch $switch_hostname aktiviert." >> $LOGFILE
else else
echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name ist erreichbar!" >> $LOGFILE echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name ist nicht erreichbar!" >> "$LOGFILE"
# Nur PoE neu starten, wenn Port vorhanden ist
if [ -n "$switch_port" ]; then
disable_poe "$switch_ip" "$switch_port" "$switch_user" "$switch_pass"
echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name PoE auf Port $switch_port am Switch $switch_hostname deaktiviert." >> "$LOGFILE"
sleep 2
enable_poe "$switch_ip" "$switch_port" "$switch_user" "$switch_pass"
echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name PoE auf Port $switch_port am Switch $switch_hostname aktiviert." >> "$LOGFILE"
fi
fi fi
done < "$IP_FILE" done
rm -f "$IP_FILE" sleep "$SLEEP"
sleep $SLEEP
done done