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)