From b634e76327e0a74ab449dfa3494377ed428f51ca Mon Sep 17 00:00:00 2001 From: Blackwhitebear8 Date: Mon, 25 Aug 2025 14:35:55 +0200 Subject: [PATCH] Add app.py --- app.py | 324 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 324 insertions(+) create mode 100644 app.py diff --git a/app.py b/app.py new file mode 100644 index 0000000..e705adb --- /dev/null +++ b/app.py @@ -0,0 +1,324 @@ +import os +import sqlite3 +import threading +import time +from collections import deque +from datetime import datetime, timedelta + +import dash +import pandas as pd +import plotly.graph_objs as go +import psutil +from dash import dcc, html +from dash.dependencies import Input, Output +from dotenv import load_dotenv +from flask import Flask, request, abort, render_template_string, redirect, url_for + +# --- CONFIGURATIE --- +load_dotenv() +AUTH_TOKEN = os.getenv("AUTH_TOKEN") +DASHBOARD_TITLE = os.getenv("DASHBOARD_TITLE", "VPS Resource Dashboard") +PAGE_TITLE = os.getenv("PAGE_TITLE", "VPS Dashboard") +DB_FILE = 'resource_log.db' +LOG_HISTORY_DAYS = int(os.getenv("LOG_HISTORY_DAYS", 30)) +LIVE_DATA_MAX_LENGTH = 60 + +# --- FLASK SERVER & DASH APP INITIALISATIE --- +server = Flask(__name__) +app = dash.Dash(__name__, server=server, url_base_pathname='/dashboard/', title=PAGE_TITLE) + +# --- LOGIN PAGINA --- +LOGIN_HTML = """ + + + + + + Login - {{ page_title }} + + + +
+

Dashboard Login

+ {% if error %} +

{{ error }}

+ {% endif %} +
+
+ + +
+
+ +
+
+
+ + +""" + +@server.route('/', methods=['GET', 'POST']) +def login(): + error = None + if request.method == 'POST': + provided_token = request.form.get('token') + if provided_token == AUTH_TOKEN: + return redirect(url_for('dashboard_redirect', token=provided_token)) + else: + error = 'Ongeldige token. Probeer het opnieuw.' + return render_template_string(LOGIN_HTML, error=error, page_title=PAGE_TITLE) + +@server.route('/dashboard-redirect') +def dashboard_redirect(): + token = request.args.get('token') + return redirect(f'/dashboard/?token={token}') + +# --- AUTHENTICATIE --- +@server.before_request +def before_request_func(): + if request.path in ['/', '/dashboard-redirect']: + return + + if request.path.startswith('/dashboard/'): + if request.path.startswith('/dashboard/_dash-'): + return + provided_token = request.args.get('token') + if not provided_token or provided_token != AUTH_TOKEN: + return redirect(url_for('login')) + +# --- HELPER FUNCTIES --- +def bytes_to_gb(bytes_val): + return round(bytes_val / (1024**3), 1) + +def format_speed(speed_kbps): + if speed_kbps >= 1024 * 1024: + return f"{speed_kbps / (1024 * 1024):.2f}", "GB/s" + elif speed_kbps >= 1024: + return f"{speed_kbps / 1024:.2f}", "MB/s" + else: + return f"{speed_kbps:.2f}", "KB/s" + +# --- DATABASE FUNCTIES --- +def init_db(): + conn = sqlite3.connect(DB_FILE, check_same_thread=False) + cursor = conn.cursor() + cursor.execute(''' + CREATE TABLE IF NOT EXISTS stats ( + timestamp TEXT PRIMARY KEY, + cpu_percent REAL, + ram_percent REAL, + disk_percent REAL + ) + ''') + conn.commit() + conn.close() + +def get_system_stats(): + virtual_mem = psutil.virtual_memory() + disk_usage = psutil.disk_usage('/') + return { + 'cpu_percent': psutil.cpu_percent(interval=None), + 'ram_percent': virtual_mem.percent, + 'ram_used_gb': bytes_to_gb(virtual_mem.used), + 'ram_total_gb': bytes_to_gb(virtual_mem.total), + 'disk_percent': disk_usage.percent, + 'disk_used_gb': bytes_to_gb(disk_usage.used), + 'disk_total_gb': bytes_to_gb(disk_usage.total), + 'net_io_per_nic': psutil.net_io_counters(pernic=True), + 'timestamp': datetime.now() + } + +def log_stats_to_db(): + stats = get_system_stats() + conn = sqlite3.connect(DB_FILE, check_same_thread=False) + cursor = conn.cursor() + cursor.execute("INSERT OR REPLACE INTO stats VALUES (?, ?, ?, ?)", + (stats['timestamp'].isoformat(), stats['cpu_percent'], stats['ram_percent'], stats['disk_percent'])) + conn.commit() + conn.close() + +def prune_old_data(): + cutoff_date = (datetime.now() - timedelta(days=LOG_HISTORY_DAYS)).isoformat() + conn = sqlite3.connect(DB_FILE, check_same_thread=False) + cursor = conn.cursor() + cursor.execute("DELETE FROM stats WHERE timestamp < ?", (cutoff_date,)) + conn.commit() + conn.close() + print(f"Pruned data older than {cutoff_date}") + +def start_logging_thread(): + def logging_loop(): + prune_counter = 0 + while True: + log_stats_to_db() + prune_counter += 1 + if prune_counter >= 60: + prune_old_data() + prune_counter = 0 + time.sleep(60) + + thread = threading.Thread(target=logging_loop, daemon=True) + thread.start() + +# --- DASHBOARD LAYOUT --- +time_data = deque(maxlen=LIVE_DATA_MAX_LENGTH) +cpu_data = deque(maxlen=LIVE_DATA_MAX_LENGTH) +ram_data = deque(maxlen=LIVE_DATA_MAX_LENGTH) +net_rx_data = deque(maxlen=LIVE_DATA_MAX_LENGTH) +net_tx_data = deque(maxlen=LIVE_DATA_MAX_LENGTH) +last_net_io = {} +last_check_time = {} +nic_options = [{'label': nic, 'value': nic} for nic in psutil.net_io_counters(pernic=True).keys()] + +def get_default_nic(): + all_nics = [nic['value'] for nic in nic_options] + for nic in all_nics: + if nic.startswith('enp') or nic.startswith('eno'): + return nic + for nic in all_nics: + if 'lo' not in nic: + return nic + return all_nics[0] if all_nics else None + +app.layout = html.Div(className="bg-gray-900 text-white min-h-screen p-8 font-sans", children=[ + html.Div(className="container mx-auto", children=[ + html.H1(DASHBOARD_TITLE, className="text-4xl font-bold mb-2 text-cyan-400"), + html.P("Live monitoring van systeembronnen.", className="text-gray-400 mb-8"), + html.Div(className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8", children=[ + html.Div(id='cpu-live-card', className="bg-gray-800 p-6 rounded-lg shadow-lg"), + html.Div(id='ram-live-card', className="bg-gray-800 p-6 rounded-lg shadow-lg"), + html.Div(id='disk-live-card', className="bg-gray-800 p-6 rounded-lg shadow-lg"), + ]), + html.Div(className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8", children=[ + dcc.Graph(id='cpu-live-graph', className="bg-gray-800 rounded-lg shadow-lg"), + dcc.Graph(id='ram-live-graph', className="bg-gray-800 rounded-lg shadow-lg"), + ]), + html.H2("Netwerkgebruik", className="text-3xl font-bold mb-4 text-cyan-400"), + dcc.Dropdown(id='nic-dropdown', options=nic_options, value=get_default_nic(), clearable=False, className="mb-6 text-black"), + html.Div(className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8", children=[ + html.Div(id='net-rx-card', className="bg-gray-800 p-6 rounded-lg shadow-lg"), + html.Div(id='net-tx-card', className="bg-gray-800 p-6 rounded-lg shadow-lg"), + ]), + dcc.Graph(id='network-live-graph', className="bg-gray-800 rounded-lg shadow-lg mb-8"), + html.H2("Historische Data", className="text-3xl font-bold mb-4 text-cyan-400"), + dcc.Graph(id='historical-graph', className="bg-gray-800 rounded-lg shadow-lg"), + ]), + dcc.Interval(id='live-interval', interval=1 * 1000, n_intervals=0), + dcc.Interval(id='historical-interval', interval=1 * 60 * 1000, n_intervals=0), +]) + +# --- DASHBOARD CALLBACKS --- +@app.callback( + [Output('cpu-live-card', 'children'), Output('ram-live-card', 'children'), + Output('disk-live-card', 'children'), Output('cpu-live-graph', 'figure'), + Output('ram-live-graph', 'figure'), Output('net-rx-card', 'children'), + Output('net-tx-card', 'children'), Output('network-live-graph', 'figure')], + [Input('live-interval', 'n_intervals'), Input('nic-dropdown', 'value')] +) +def update_live_data(n, selected_nic): + stats = get_system_stats() + time_data.append(stats['timestamp']) + cpu_data.append(stats['cpu_percent']) + ram_data.append(stats['ram_percent']) + + cpu_card = [html.H3("CPU Gebruik", className="text-xl font-semibold text-gray-300 mb-2"), html.P(f"{stats['cpu_percent']:.1f}%", className="text-5xl font-bold text-cyan-400")] + ram_card = [html.H3("RAM Gebruik", className="text-xl font-semibold text-gray-300 mb-2"), html.P(f"{stats['ram_percent']:.1f}%", className="text-5xl font-bold text-purple-400"), html.P(f"{stats['ram_used_gb']} GB / {stats['ram_total_gb']} GB", className="text-md text-gray-400 mt-2")] + disk_card = [html.H3("Schijfgebruik (/)", className="text-xl font-semibold text-gray-300 mb-2"), html.P(f"{stats['disk_percent']:.1f}%", className="text-5xl font-bold text-amber-400"), html.P(f"{stats['disk_used_gb']} GB / {stats['disk_total_gb']} GB", className="text-md text-gray-400 mt-2")] + + def create_live_figure(x, y, title, color, y_range=[0, 100]): + return { + 'data': [go.Scatter(x=list(x), y=list(y), mode='lines', line={'width': 2, 'color': color}, hovertemplate='%{y:.2f}%')], + 'layout': go.Layout(title={'text': title, 'font': {'color': 'white'}}, xaxis={'showgrid': False, 'color': 'white'}, yaxis={'title': '% Gebruik', 'gridcolor': '#4A5568', 'color': 'white', 'range': y_range}, plot_bgcolor='rgba(0,0,0,0)', paper_bgcolor='rgba(0,0,0,0)', height=400, margin={'l': 60, 'r': 20, 't': 40, 'b': 40}, hovermode='x unified', hoverlabel={'bgcolor': '#1f2937', 'font': {'color': 'white'}}) + } + + cpu_fig = create_live_figure(time_data, cpu_data, 'CPU (Laatste minuut)', '#2dd4bf') + ram_fig = create_live_figure(time_data, ram_data, 'RAM (Laatste minuut)', '#a78bfa') + + rx_speed_kbps, tx_speed_kbps = 0, 0 + current_time = time.time() + if selected_nic and selected_nic in stats['net_io_per_nic']: + current_io = stats['net_io_per_nic'][selected_nic] + if selected_nic in last_net_io: + time_diff = current_time - last_check_time.get(selected_nic, current_time -1) + if time_diff > 0: + rx_speed_kbps = ((current_io.bytes_recv - last_net_io[selected_nic].bytes_recv) / time_diff) / 1024 + tx_speed_kbps = ((current_io.bytes_sent - last_net_io[selected_nic].bytes_sent) / time_diff) / 1024 + last_net_io[selected_nic] = current_io + last_check_time[selected_nic] = current_time + + net_rx_data.append(rx_speed_kbps) + net_tx_data.append(tx_speed_kbps) + + rx_val_str, rx_unit = format_speed(rx_speed_kbps) + tx_val_str, tx_unit = format_speed(tx_speed_kbps) + net_rx_card = [html.H3("Ontvangen (RX)", className="text-xl font-semibold text-gray-300 mb-2"), html.P(f"{rx_val_str} {rx_unit}", className="text-5xl font-bold text-green-400")] + net_tx_card = [html.H3("Verzonden (TX)", className="text-xl font-semibold text-gray-300 mb-2"), html.P(f"{tx_val_str} {tx_unit}", className="text-5xl font-bold text-blue-400")] + + max_speed = max(list(net_rx_data) + list(net_tx_data) + [1]) + y_axis_title, scale_factor = ("MB/s", 1/1024) if max_speed >= 1024 else ("KB/s", 1) + scaled_rx = [s * scale_factor for s in net_rx_data] + scaled_tx = [s * scale_factor for s in net_tx_data] + + net_fig = go.Figure() + net_fig.add_trace(go.Scatter(x=list(time_data), y=scaled_rx, mode='lines', name='RX', line={'color': '#4ade80'}, hovertemplate=f'%{{y:.2f}} {y_axis_title}')) + net_fig.add_trace(go.Scatter(x=list(time_data), y=scaled_tx, mode='lines', name='TX', line={'color': '#60a5fa'}, hovertemplate=f'%{{y:.2f}} {y_axis_title}')) + net_fig.update_layout(title={'text': f'Netwerksnelheid - {selected_nic}', 'font': {'color': 'white'}}, xaxis={'showgrid': False, 'color': 'white'}, yaxis={'title': y_axis_title, 'gridcolor': '#4A5568', 'color': 'white'}, plot_bgcolor='rgba(0,0,0,0)', paper_bgcolor='rgba(0,0,0,0)', height=400, margin={'l': 60, 'r': 20, 't': 40, 'b': 40}, legend={'font': {'color': 'white'}}, hovermode='x unified', hoverlabel={'bgcolor': '#1f2937', 'font': {'color': 'white'}}) + + return cpu_card, ram_card, disk_card, cpu_fig, ram_fig, net_rx_card, net_tx_card, net_fig + +@app.callback(Output('historical-graph', 'figure'), [Input('historical-interval', 'n_intervals')]) +def update_historical_data(n): + try: + conn = sqlite3.connect(DB_FILE, check_same_thread=False) + df = pd.read_sql_query("SELECT * FROM stats ORDER BY timestamp", conn) + conn.close() + df['timestamp'] = pd.to_datetime(df['timestamp']) + except Exception as e: + print(f"Error reading from database: {e}") + return go.Figure(layout={'title': 'Geen historische data gevonden', 'plot_bgcolor': 'rgba(0,0,0,0)', 'paper_bgcolor': 'rgba(0,0,0,0)', 'font': {'color': 'white'}}) + + fig = go.Figure() + hovertemplate = '%{data.name}
%{x|%Y-%m-%d %H:%M:%S}
%{y:.2f}%' + fig.add_trace(go.Scatter(x=df['timestamp'], y=df['cpu_percent'], mode='lines', name='CPU', line={'color': '#2dd4bf'}, hovertemplate=hovertemplate)) + fig.add_trace(go.Scatter(x=df['timestamp'], y=df['ram_percent'], mode='lines', name='RAM', line={'color': '#a78bfa'}, hovertemplate=hovertemplate)) + fig.add_trace(go.Scatter(x=df['timestamp'], y=df['disk_percent'], mode='lines', name='Schijf', line={'color': '#facc15'}, hovertemplate=hovertemplate)) + fig.update_layout( + title={'text': 'Historisch Resourcegebruik', 'font': {'color': 'white'}}, + xaxis={'title': 'Tijd', 'color': 'white', 'gridcolor': '#4A5568', 'rangeselector': {'buttons': list([dict(count=1, label="1u", step="hour", stepmode="backward"), dict(count=6, label="6u", step="hour", stepmode="backward"), dict(count=1, label="1d", step="day", stepmode="backward"), dict(count=7, label="7d", step="day", stepmode="backward"), dict(step="all")]), 'bgcolor': '#374151', 'font': {'color': 'white'}}, 'rangeslider': {'visible': True, 'bgcolor': '#374151'}, 'type': 'date'}, + yaxis={'title': '% Gebruik', 'color': 'white', 'gridcolor': '#4A5568'}, + plot_bgcolor='rgba(0,0,0,0)', paper_bgcolor='rgba(0,0,0,0)', + legend={'font': {'color': 'white'}}, height=600, + hovermode='x unified', + hoverlabel={'bgcolor': '#1f2937', 'font': {'color': 'white'}} + ) + return fig + +# --- APPLICATIE STARTEN --- +if __name__ == '__main__': + app.index_string = ''' + + + + {%metas%} + {%title%} + {%favicon%} + {%css%} + + + + {%app_entry%} + + + + ''' + init_db() + print("Starting logging thread...") + start_logging_thread() + print(f"Dashboard is running. Data retention: {LOG_HISTORY_DAYS} days. Ga naar: http://127.0.0.1:8050/") + server.run(debug=True, host='0.0.0.0', port=8050)