Compare commits

..

4 Commits

Author SHA1 Message Date
df000eace9 revert 74617a1e0f
revert srv/poe_manager/templates/switche.html aktualisiert
2025-10-07 18:19:42 +02:00
74617a1e0f srv/poe_manager/templates/switche.html aktualisiert 2025-10-07 18:17:58 +02:00
bc80b5fcf1 srv/poe_manager/create_db.py aktualisiert 2025-09-30 19:11:09 +02:00
0c82ce6100 srv/poe_manager/create_db.py aktualisiert 2025-09-30 19:10:13 +02:00
19 changed files with 310 additions and 1096 deletions

6
.gitignore vendored
View File

@@ -17,7 +17,6 @@ __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
@@ -40,8 +39,7 @@ __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

@@ -1,16 +0,0 @@
[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

@@ -3,9 +3,8 @@ from flask import Flask, render_template, request, redirect, url_for, flash
from flask_login import LoginManager, login_user, login_required, logout_user, UserMixin, current_user from flask_login import LoginManager, login_user, login_required, logout_user, UserMixin, current_user
from flask_bcrypt import Bcrypt from flask_bcrypt import Bcrypt
from cryptography.fernet import Fernet from cryptography.fernet import Fernet
from datetime import datetime import sqlite3
from collections import defaultdict import glob, os, re
import sqlite3, glob, os, re
app = Flask(__name__) app = Flask(__name__)
app.secret_key = "309cc4d5ce1fe7486ae25cbd232bbdfe6a72539c03f0127d372186dbdc0fc928" app.secret_key = "309cc4d5ce1fe7486ae25cbd232bbdfe6a72539c03f0127d372186dbdc0fc928"
@@ -48,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, is_active FROM devices ORDER BY name ASC").fetchall() devices = conn.execute("SELECT mac, rpi_ip, port, name, switch_hostname FROM devices").fetchall()
conn.close() conn.close()
return devices return devices
@@ -82,84 +81,28 @@ def logout():
logout_user() logout_user()
return redirect(url_for('login')) return redirect(url_for('login'))
def get_last_seen(dev_name: str):
"""Letztes Mal, dass ein Gerät erreichbar war."""
log_files = glob.glob("/var/log/rpi-*.log")
if not log_files:
return None
latest_time = None
# alle Logs durchgehen
for logfile in sorted(log_files):
with open(logfile, "r") as f:
for line in f:
line = line.strip()
if f"{dev_name} ist erreichbar!" in line:
try:
ts_str = line.split(" ")[0] + " " + line.split(" ")[1] # "YYYY-MM-DD HH:MM:SS"
ts = datetime.strptime(ts_str, "%Y-%m-%d %H:%M:%S")
if latest_time is None or ts > latest_time:
latest_time = ts
except Exception:
continue
if latest_time:
datetime_str = latest_time.strftime("Zuletzt Online am %d.%m.%Y um %H:%M Uhr")
return f"{datetime_str}"
return None
@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, is_active FROM devices ORDER BY name ASC") c.execute("SELECT mac, name FROM devices")
devices = c.fetchall() devices = c.fetchall()
devices = sorted(devices, key=lambda d: d[1][0].upper())
grouped_devices = defaultdict(list)
for d in devices:
first_letter = d[1][:2].upper()
grouped_devices[first_letter].append(d)
# Intervall aus DB laden
c.execute("SELECT value FROM settings WHERE key='interval'")
row = c.fetchone()
interval = int(row[0]) if row else 5
conn.close() conn.close()
# Status aus Logdateien ermitteln # Status aus letztem Log ermitteln
import glob, os
log_files = glob.glob("/var/log/rpi-*.log") log_files = glob.glob("/var/log/rpi-*.log")
status_dict = {} status_dict = {}
last_seen_dict = {}
if log_files: if log_files:
latest_log = max(log_files, key=os.path.getctime) latest_log = max(log_files, key=os.path.getctime)
with open(latest_log, "r") as f: with open(latest_log, "r") as f:
lines = f.readlines() for line in f:
for dev in devices:
if dev[1] in line:
status_dict[dev[0]] = "online" if "erreichbar" in line else "offline"
for dev in devices: return render_template("index.html", devices=devices, status=status_dict)
last_status = None
for line in reversed(lines):
if f"{dev[1]} ist erreichbar!" in line:
last_status = "online"
break
elif f"{dev[1]} ist nicht erreichbar!" in line:
last_status = "offline"
break
if last_status:
status_dict[dev[0]] = last_status
if last_status == "offline":
last_seen_dict[dev[0]] = get_last_seen(dev[1])
else:
status_dict[dev[0]] = "unbekannt"
return render_template("index.html", grouped_devices=grouped_devices, devices=devices, status=status_dict, last_seen=last_seen_dict, interval=interval)
@app.route("/settings", methods=["GET", "POST"]) @app.route("/settings", methods=["GET", "POST"])
@login_required @login_required
@@ -178,23 +121,12 @@ 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()
# interval (Minuten) # upsert
c.execute(""" c.execute("INSERT INTO settings (key, value) VALUES (?, ?) "
INSERT INTO settings (key, value) VALUES (?, ?) "ON CONFLICT(key) DO UPDATE SET value=excluded.value",
ON CONFLICT(key) DO UPDATE SET value=excluded.value ("interval", new_interval))
""", ("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()
@@ -202,7 +134,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 {interval_minutes} Minuten gesetzt und Service neu gestartet!") flash(f"Intervall auf {new_interval} 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)
@@ -213,180 +145,65 @@ 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()
if request.method == 'POST': # Inline-Add
# ----------------------- if request.method == 'POST' and 'add_device' in request.form:
# Gerät hinzufügen if not current_user.is_admin:
# ----------------------- flash("Zugriff verweigert!")
if 'add_device' in request.form: return redirect(url_for('devices'))
if not current_user.is_admin: mac = request.form['mac']
flash("Zugriff verweigert!") rpi_ip = request.form['rpi_ip']
return redirect(url_for('devices')) port = request.form['port']
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!")
mac = request.form.get('mac') # Inline-Edit
rpi_ip = request.form.get('rpi_ip') if request.method == 'POST' and 'edit_device' in request.form:
port = request.form.get('port') if not current_user.is_admin:
name = request.form.get('name') flash("Zugriff verweigert!")
switch_hostname = request.form.get('switch_hostname') return redirect(url_for('devices'))
is_active = 1 if 'is_active' in request.form else 0 old_mac = request.form['old_mac']
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!")
if not all([mac, rpi_ip, name]): # Inline-Delete
flash("Alle Felder müssen ausgefüllt sein!") if request.method == 'POST' and 'delete_device' in request.form:
return redirect(url_for('devices')) if not current_user.is_admin:
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') or None
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
# --- Nur Switch ändern ---
# Prüfen, ob nur das Switch-Feld gesendet wurde und die anderen Felder leer sind
if 'switch_hostname' in request.form and all(not f for f in [mac, rpi_ip, name]):
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']
switch_hostname = request.form.get('switch_hostname') or ""
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 or 'Kein Switch'}")
except sqlite3.IntegrityError:
flash("Fehler beim Ändern des Switch!")
return redirect(url_for('devices'))
# --- Normales Gerät bearbeiten ---
# Pflichtfelder prüfen
if not all([old_mac, mac, rpi_ip, name]):
flash("Felder 'MAC', 'IP' und 'Name' 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'))
# Update durchführen
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, devices.is_active, SELECT devices.mac, devices.rpi_ip, devices.port, devices.name, switches.hostname AS switch_hostname
switches.hostname AS switch_hostname
FROM devices FROM devices
LEFT JOIN switches ON devices.switch_hostname = switches.hostname JOIN switches ON devices.switch_hostname = switches.hostname
ORDER BY switches.hostname ASC, devices.name 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():
@@ -430,27 +247,20 @@ 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():
@@ -494,43 +304,36 @@ def logs():
return render_template('logs.html', log_content=log_content, log_name=os.path.basename(latest_log), interval=interval) return render_template('logs.html', log_content=log_content, log_name=os.path.basename(latest_log), interval=interval)
def load_device_status(): def load_device_status():
"""
Liest das aktuellste rpi-Logfile und extrahiert den letzten Status jedes Devices.
Gibt ein Dictionary zurück: {Device-Name: 'online'/'offline'}
"""
status = {} status = {}
# Devices aus DB laden (Name → MAC)
conn = sqlite3.connect("sqlite.db")
conn.row_factory = sqlite3.Row
devices = conn.execute("SELECT mac, name FROM devices").fetchall()
name_to_mac = {d['name']: d['mac'] for d in devices}
conn.close()
# Logfile
log_files = glob.glob("/var/log/rpi-*.log") log_files = glob.glob("/var/log/rpi-*.log")
if not log_files: if not log_files:
return status return status
latest_log = max(log_files, key=os.path.getctime) latest_log = max(log_files, key=os.path.getctime)
# Jede Zeile des Logs lesen
with open(latest_log, "r") as f:
lines = f.readlines()
# Regex für Ping-Ergebnisse
online_re = re.compile(r"(\S+) ist erreichbar!") online_re = re.compile(r"(\S+) ist erreichbar!")
offline_re = re.compile(r"(\S+) ist nicht erreichbar!") offline_re = re.compile(r"(\S+) ist nicht erreichbar!")
with open(latest_log, "r") as f: for line in lines:
for line in f: line = line.strip()
line = line.strip() m_online = online_re.search(line)
m_online = online_re.search(line) m_offline = offline_re.search(line)
m_offline = offline_re.search(line) if m_online:
if m_online: status[m_online.group(1)] = 'online'
name = m_online.group(1) elif m_offline:
mac = name_to_mac.get(name) status[m_offline.group(1)] = 'offline'
if mac:
status[mac] = 'online'
elif m_offline:
name = m_offline.group(1)
mac = name_to_mac.get(name)
if mac:
status[mac] = 'offline'
return status return status
@app.route("/users", methods=["GET", "POST"]) @app.route("/users", methods=["GET", "POST"])
@login_required @login_required
def users(): def users():

View File

@@ -15,11 +15,11 @@ CREATE TABLE IF NOT EXISTS switches (
# Devices # Devices
c.execute(""" c.execute("""
CREATE TABLE devices ( CREATE TABLE IF NOT EXISTS 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, port TEXT NOT NULL,
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)

View File

@@ -1,33 +0,0 @@
#!/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,35 +1,36 @@
#!/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
switches = {row['hostname']: row for row in conn.execute( # Alle Switches laden
"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 switch: if not switch:
switch_ip = switch['ip'] continue # Switch existiert nicht, überspringen
switch_user = switch['username']
switch_pass = decrypt_password(switch['password'])
else:
switch_ip = ""
switch_user = ""
switch_pass = ""
port = dev['port'] or "" password = decrypt_password(switch['password'])
print(f"{dev['rpi_ip']}:{dev['name']}:{switch_ip}:{dev['switch_hostname'] or 'kein Switch'}:{port}:{switch_user}:{switch_pass}") # Format: IP-Device:Hostname-Device:IP-Switch:Hostname-Switch:Port-Switch:Username-Switch:Password-Switch
line = f"{dev['rpi_ip']}:{dev['name']}:{switch['ip']}:{switch['hostname']}:{dev['port']}:{switch['username']}:{password}\n"
tmp.write(line)
tmp.close()
return tmp_path
if __name__ == "__main__": if __name__ == "__main__":
generate_ips_list() path = 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: #f8f9fa7c; background-color: #f8f9fa;
padding: 10px; padding: 10px;
border-radius: 5px; border-radius: 5px;
max-height: 600px; max-height: 600px;
@@ -28,11 +28,6 @@ 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;
@@ -58,7 +53,7 @@ pre {
} }
.content-wrapper { .content-wrapper {
max-width: 1080px; max-width: 1024px;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
} }
@@ -82,96 +77,28 @@ pre {
.navbar-logo img { .navbar-logo img {
display: flex; display: flex;
justify-content: flex-start; justify-content: flex-start;
height: 83px; /* Logo Höhe, kann angepasst werden */ height: 60px; /* Logo Höhe, kann angepasst werden */
} }
#log-container { #log-container {
position: relative; position: relative;
height: calc(100vh - 252px); /* Füllt die Seite minus Header */ height: calc(100vh - 150px); /* Füllt die Seite minus Header */
padding: 1rem; /* optional, Abstand innen */
box-sizing: border-box; /* damit Padding nicht die Höhe sprengt */
display: flex;
flex-direction: column;
} }
#log-box { #log-box {
flex: 1 1 auto; /* füllt den Container, berücksichtigt Header/Padding */ height: 97%;
overflow: auto; overflow: auto;
white-space: pre-wrap; white-space: pre-wrap;
font-family: monospace; font-family: monospace;
border: 1px solid #dee2e6; border: 1px solid #dee2e6; /* Bootstrap-like border */
padding: 1rem;
background-color: #f8f9fa; background-color: #f8f9fa;
padding: 0.5rem;
} }
#refresh-timer { #refresh-timer {
position: absolute; position: absolute;
bottom: 18px; bottom: 10px;
right: 30px; right: 10px;
font-size: 0.9em; font-size: 0.9em;
color: gray; color: gray;
background-color: rgba(255,255,255,0.7); /* optional, besser lesbar */
padding: 2px 4px;
border-radius: 3px;
pointer-events: none; /* damit Scrollbar nicht blockiert wird */
}
/* 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.

Before

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">Switche</a> <a href="{{ url_for('switches') }}" class="btn btn-secondary">Switches</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,12 +3,35 @@
<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 %}
<!-- Neues Gerät hinzufügen --> <h4>Neues Gerät hinzufügen</h4>
<button class="btn btn-success mb-3" data-bs-toggle="modal" data-bs-target="#deviceModal" onclick="openDeviceModal()">Neues Gerät hinzufügen</button> <form method="post" class="row g-2 mb-4">
<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-striped"> <table class="table table-bordered">
<thead> <thead>
<tr> <tr>
<th>Hostname</th> <th>Hostname</th>
@@ -16,281 +39,45 @@
<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'] or '-' }}</td> <td>{{ d['switch_hostname'] }}</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" placeholder="z.B. 3">
</div>
<div class="mb-3">
<label>Switch (optional)</label>
<select name="switch_hostname" class="form-select">
<option value="">Kein Switch</option>
{% 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" 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">
<option value="">Kein Switch</option>
{% 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,97 +1,20 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
<h2 class="d-flex justify-content-between align-items-center"> <h2>Dashboard</h2>
Dashboard <div class="row row-cols-1 row-cols-md-6 g-3">
<span id="dashboard-timer" class="badge bg-success"> {% for d in devices %}
Nächste Prüfung in -- Sekunden <div class="col">
</span> <div class="card text-center p-2">
</h2> <div class="card-header">{{ d[1] }}</div>
<div class="row g-3"> <div class="card-body">
{% for letter, group in grouped_devices.items() %} <span class="fw-bold" style="color: {% if status[d[0]]=='online' %}green{% else %}red{% endif %};">
<div class="row g-3"> {% if status[d[0]] %}{{ status[d[0]]|capitalize }}{% else %}unbekannt{% endif %}
{% for d in group %} </span>
<div class="col-6 col-md-4 col-lg-3 col-xl-2">
<div class="card text-center p-2"
{% if last_seen.get(d[0]) %}
title="{{ last_seen[d[0]] }}"
{% elif status[d[0]] == 'offline' %}
title="Noch nie online"
{% endif %}>
<div class="card-header">{{ d[1] }}</div>
<div class="card-body">
<span class="fw-bold" style="color:
{% 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>
</div>
</div> </div>
</div> </div>
{% endfor %}
</div> </div>
{% 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 label text-white">Username</label> <label class="form-label">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 label text-white">Password</label> <label class="form-label">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 button login">Login</button> <button class="btn btn-primary">Login</button>
</form> </form>
</body> </body>

View File

@@ -3,10 +3,6 @@
<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">
@@ -15,65 +11,36 @@
</div> </div>
<script> <script>
function parseLogTimestamp(ts) { const intervalMinutes = {{ interval | int }};
const parts = ts.match(/^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/); const intervalMilliseconds = intervalMinutes * 60 * 1000;
if (!parts) return Date.now(); let lastUpdateTime = Date.now();
const [, year, month, day, hour, minute, second] = parts.map(Number);
return new Date(year, month - 1, day, hour, minute, second).getTime(); function fetchLog() {
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));
} }
document.addEventListener("DOMContentLoaded", () => { function updateTimer() {
const intervalMinutes = {{ interval | int }}; const now = Date.now();
const intervalMilliseconds = intervalMinutes * 60 * 1000; const remainingMs = intervalMilliseconds - (now - lastUpdateTime);
let lastUpdateTime = Date.now(); const remainingSec = Math.max(Math.ceil(remainingMs / 1000), 0);
document.getElementById("timer").innerText = remainingSec;
}
function fetchLog() { setInterval(updateTimer, 1000);
fetch("{{ url_for('get_log') }}") fetchLog();
.then(res => res.text()) setInterval(fetchLog, intervalMilliseconds);
.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,11 +5,20 @@
<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 white">Prüfintervall (Minuten):</label> <label for="interval" class="form-label">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,183 +1,53 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
<h2>Switch Verwaltung</h2>
<!-- Button zum Hinzufügen --> <h2>Switche</h2>
<button class="btn btn-success mb-3" data-bs-toggle="modal" data-bs-target="#addSwitchModal">Neuen Switch hinzufügen</button>
<table class="table table-striped"> {% 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">
<thead> <thead>
<tr> <tr>
<th>Hostname</th> <th>Hostname</th>
<th>IP-Adresse</th> <th>IP-Adresse</th>
<th>Username</th> <th>Username</th>
<th>Aktionen</th> {% if current_user.is_admin %}<th>Aktionen</th>{% endif %}
</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>
<td> {% endif %}
<!-- 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

@@ -8,30 +8,30 @@
<button class="btn btn-success mb-3" data-bs-toggle="modal" data-bs-target="#userModal" onclick="openUserModal()">Neuen Benutzer</button> <button class="btn btn-success mb-3" data-bs-toggle="modal" data-bs-target="#userModal" onclick="openUserModal()">Neuen Benutzer</button>
{% endif %} {% endif %}
<table class="table table-striped"> <table class="table table-bordered">
<thead> <thead>
<tr> <tr>
<th class="col-ip">Username</th> <th>Username</th>
<th class="col-ip">Rolle</th> <th>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 class="col-small">{{ u['username'] }}</td> <td>{{ u['username'] }}</td>
<td class="col-small">{% if u['is_admin'] %}Admin{% else %}User{% endif %}</td> <td>{% if u['is_admin'] %}Admin{% else %}User{% endif %}</td>
{% if current_user.is_admin %} {% if current_user.is_admin %}
<td class="custom-actions d-flex gap-1 align-items-center"> <td>
<!-- Rolle ändern --> <!-- Rolle ändern -->
<button class="btn btn-sm button login white" data-bs-toggle="modal" <button class="btn btn-primary btn-sm" 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-sm button login white" data-bs-toggle="modal" <button class="btn btn-warning btn-sm" 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,14 +49,13 @@
</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 black">Neuen Benutzer anlegen</h5> <h5 class="modal-title">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">
@@ -91,7 +90,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 black">Rolle ändern</h5> <h5 class="modal-title">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">
@@ -123,7 +122,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 black">Passwort ändern</h5> <h5 class="modal-title">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">
@@ -160,6 +159,7 @@ 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

@@ -0,0 +1,8 @@
#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,20 +1,9 @@
#!/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)
# ============================================================================
LOG_DIR="/var/log" LOGFILE="/var/log/rpi-$(date '+%Y%m%d%H%M%S').log"
LOGFILE="$LOG_DIR/rpi-$(date '+%Y%m%d%H%M%S').log"
# Alte Logfiles löschen (älter als 30 Tage) # Intervall aus DB (Sekunden) abrufen
find "$LOG_DIR" -type f -name "rpi-*.log" -mtime +30 -delete SLEEP=$(python3 - <<END
# 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()
@@ -23,7 +12,8 @@ print(row[0] if row else 300)
END END
) )
SLEEP=${SLEEP:-300} # Umrechnung falls nötig
SLEEP=${SLEEP:-300} # default 300 Sekunden
function disable_poe() { function disable_poe() {
local switch_ip=$1 local switch_ip=$1
@@ -33,6 +23,7 @@ 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 }
@@ -49,7 +40,8 @@ expect "(config)#"
send "exit\r" send "exit\r"
expect "#" expect "#"
send "exit\r" send "exit\r"
expect ">"; send "exit\r" expect ">"
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
@@ -63,6 +55,7 @@ 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 }
@@ -79,53 +72,30 @@ expect "(config)#"
send "exit\r" send "exit\r"
expect "#" expect "#"
send "exit\r" send "exit\r"
expect ">"; send "exit\r" expect ">"
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
MAX_PARALLEL=10 # maximal gleichzeitig laufende Geräte
while true; do while true; do
echo "--------------------------------------------------------------------" >> "$LOGFILE" echo "--------------------------------------------------------------------" >> $LOGFILE
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 IP_FILE=$(python3 /srv/poe_manager/generate_ips.py)
# Funktion für ein Gerät while IFS=: read -r rpi_ip dev_name switch_ip switch_hostname switch_port switch_user switch_pass; do
check_device() { ping -c 1 -W 2 "$rpi_ip" &> /dev/null
local rpi_ip="$1" if [ $? -ne 0 ]; then
local dev_name="$2" echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name ist nicht erreichbar!" >> $LOGFILE
local switch_ip="$3" disable_poe "$switch_ip" "$switch_port" "$switch_user" "$switch_pass"
local switch_hostname="$4" echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name PoE auf Port $switch_port am Switch $switch_hostname deaktiviert." >> $LOGFILE
local switch_port="$5" sleep 2
local switch_user="$6" enable_poe "$switch_ip" "$switch_port" "$switch_user" "$switch_pass"
local switch_pass="$7" echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name PoE auf Port $switch_port am Switch $switch_hostname aktiviert." >> $LOGFILE
else
if ping -c 4 -W 1 "$rpi_ip" &> /dev/null; then echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name ist erreichbar!" >> $LOGFILE
echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name ist erreichbar!" >> "$LOGFILE" fi
else done < "$IP_FILE"
echo "$(date '+%Y-%m-%d %H:%M:%S') $dev_name ist nicht erreichbar!" >> "$LOGFILE" rm -f "$IP_FILE"
sleep $SLEEP
if [ -n "$switch_port" ] && [ "$switch_port" != "None" ]; 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
}
# Job in Hintergrund starten
check_device "$rpi_ip" "$dev_name" "$switch_ip" "$switch_hostname" "$switch_port" "$switch_user" "$switch_pass" &
# Limit auf MAX_PARALLEL Jobs
while [ "$(jobs -rp | wc -l)" -ge "$MAX_PARALLEL" ]; do
sleep 0.2
done
done
wait # alle Hintergrundjobs beenden, bevor sleep
sleep "$SLEEP"
done done