✳️Replace the highlighted information with your own
import time
import threading
from collections import deque
from flask import Flask, jsonify, render_template_string
import requests
from requests.auth import HTTPBasicAuth
# ============================================================
# USER CONFIGURATION
# Edit only this section before running the dashboard.
# ============================================================
MIKROTIK_HOST = "MIKROTIK_IP"
MIKROTIK_PORT = MIKROTIK_PORT
MIKROTIK_USER = "YOUR_ACCOUNT"
MIKROTIK_PASSWORD = "YOUR_PASSWORD"
MIKROTIK_USE_HTTPS = False
VERIFY_SSL = False
# List the WAN interfaces in the order you want them displayed.
# Examples:
# WAN_INTERFACES = ["ether1", "ether2"]
# WAN_INTERFACES = ["pppoe-out1", "pppoe-out2"]
WAN_INTERFACES = ["WAN_INTERFACE_1", "WAN_INTERFACE_2"]
REFRESH_SECONDS = 5
PRIVACY_MODE = True
# ============================================================
# END USER CONFIGURATION
# ============================================================
HOST = MIKROTIK_HOST
PORT = MIKROTIK_PORT
USER = MIKROTIK_USER
PASSWORD = MIKROTIK_PASSWORD
USE_HTTPS = MIKROTIK_USE_HTTPS
PRIVACY_DEFAULT = PRIVACY_MODE
SCHEME = "https" if USE_HTTPS else "http"
BASE_URL = f"{SCHEME}://{HOST}:{PORT}/rest"
app = Flask(__name__)
session = requests.Session()
session.auth = HTTPBasicAuth(USER, PASSWORD)
session.verify = VERIFY_SSL
session.headers.update({"Accept": "application/json"})
if not VERIFY_SSL:
requests.packages.urllib3.disable_warnings()
_lock = threading.Lock()
_prev_interfaces = {}
_prev_time = None
_history = deque(maxlen=120)
def to_int(value, default=0):
try:
return int(float(value))
except Exception:
return default
def fmt_bps(value):
value = max(0.0, float(value or 0))
units = ["bps", "Kbps", "Mbps", "Gbps", "Tbps"]
i = 0
while value >= 1000 and i < len(units) - 1:
value /= 1000.0
i += 1
return f"{value:.1f} {units[i]}"
def fmt_bytes(value):
value = max(0.0, float(value or 0))
units = ["B", "KB", "MB", "GB", "TB"]
i = 0
while value >= 1024 and i < len(units) - 1:
value /= 1024.0
i += 1
return f"{value:.1f} {units[i]}"
def api_get(path):
url = f"{BASE_URL}/{path.lstrip('/')}"
r = session.get(url, timeout=8)
r.raise_for_status()
return r.json()
def safe_get(path, fallback):
try:
return api_get(path)
except Exception:
return fallback
def get_system():
r = safe_get("system/resource", {})
total_mem = to_int(r.get("total-memory"))
free_mem = to_int(r.get("free-memory"))
used_mem = max(0, total_mem - free_mem)
ram_pct = round((used_mem / total_mem * 100), 1) if total_mem else 0
total_hdd = to_int(r.get("total-hdd-space"))
free_hdd = to_int(r.get("free-hdd-space"))
used_hdd = max(0, total_hdd - free_hdd)
hdd_pct = round((used_hdd / total_hdd * 100), 1) if total_hdd else 0
return {
"board": r.get("board-name", "-"),
"version": r.get("version", "-"),
"uptime": r.get("uptime", "-"),
"cpu_load": to_int(r.get("cpu-load")),
"ram_pct": ram_pct,
"ram_used": fmt_bytes(used_mem),
"ram_total": fmt_bytes(total_mem),
"hdd_pct": hdd_pct,
"hdd_used": fmt_bytes(used_hdd),
"hdd_total": fmt_bytes(total_hdd),
}
def get_interfaces():
global _prev_time, _prev_interfaces
now = time.time()
rows = safe_get("interface", [])
result = []
with _lock:
elapsed = max(0.5, now - _prev_time) if _prev_time else None
for row in rows:
name = row.get("name", "")
rx = to_int(row.get("rx-byte"))
tx = to_int(row.get("tx-byte"))
prev = _prev_interfaces.get(name)
rx_bps = 0
tx_bps = 0
if elapsed and prev:
rx_bps = max(0, (rx - prev["rx"]) * 8 / elapsed)
tx_bps = max(0, (tx - prev["tx"]) * 8 / elapsed)
_prev_interfaces[name] = {"rx": rx, "tx": tx}
result.append({
"name": name,
"type": row.get("type", ""),
"comment": row.get("comment", ""),
"running": str(row.get("running", "false")).lower() == "true",
"disabled": str(row.get("disabled", "false")).lower() == "true",
"dynamic": str(row.get("dynamic", "false")).lower() == "true",
"rx_bps": rx_bps,
"tx_bps": tx_bps,
"rx_human": fmt_bps(rx_bps),
"tx_human": fmt_bps(tx_bps),
"rx_total": rx,
"tx_total": tx,
"rx_total_human": fmt_bytes(rx),
"tx_total_human": fmt_bytes(tx),
})
_prev_time = now
return result
def get_addresses():
rows = safe_get("ip/address", [])
out = {}
for row in rows:
iface = row.get("interface")
addr = row.get("address", "")
if iface and addr:
out[iface] = addr.split("/")[0]
return out
def get_wans(interfaces, addresses):
"""
Build the WAN list.
Priority:
1. Use WAN_INTERFACES from the configuration section when configured.
2. Otherwise fall back to automatic PPPoE detection.
"""
iface_map = {x["name"]: x for x in interfaces}
wans = []
if WAN_INTERFACES:
for name in WAN_INTERFACES:
iface = iface_map.get(name)
if not iface:
continue
wans.append({
"name": iface["name"],
"label": iface["comment"] or iface["name"],
"up": iface["running"] and not iface["disabled"],
"ip": addresses.get(iface["name"], "-"),
"rx": iface["rx_human"],
"tx": iface["tx_human"],
"rx_bps": iface["rx_bps"],
"tx_bps": iface["tx_bps"],
"rx_total": iface["rx_total"],
"tx_total": iface["tx_total"],
"rx_total_human": iface["rx_total_human"],
"tx_total_human": iface["tx_total_human"],
})
return wans
for iface in interfaces:
if iface["type"] != "pppoe-out":
continue
wans.append({
"name": iface["name"],
"label": iface["comment"] or iface["name"],
"up": iface["running"] and not iface["disabled"],
"ip": addresses.get(iface["name"], "-"),
"rx": iface["rx_human"],
"tx": iface["tx_human"],
"rx_bps": iface["rx_bps"],
"tx_bps": iface["tx_bps"],
})
return wans[:4]
def get_vpn():
peers = safe_get("ip/ipsec/peer", [])
active = safe_get("ip/ipsec/active-peers", [])
policies = safe_get("ip/ipsec/policy", [])
ppp_active = safe_get("ppp/active", [])
active_remote = {a.get("remote-address"): a for a in active}
site_to_site = []
for p in peers:
if str(p.get("dynamic", "false")).lower() == "true":
continue
name = p.get("name", "IPsec")
disabled = str(p.get("disabled", "false")).lower() == "true"
passive = str(p.get("passive", "false")).lower() == "true"
if passive and not p.get("address"):
continue
remote = (p.get("address") or "").split("/")[0]
established = False
uptime = ""
if remote in active_remote:
a = active_remote[remote]
established = a.get("state") == "established"
uptime = a.get("uptime", "")
else:
peer_policies = [
x for x in policies
if x.get("peer") == name and str(x.get("template", "false")).lower() != "true"
]
established = any(x.get("ph2-state") == "established" for x in peer_policies)
status = "DISABLED" if disabled else ("UP" if established else "DOWN")
site_to_site.append({
"name": name,
"status": status,
"remote": remote or "-",
"uptime": uptime or "-",
"exchange_mode": p.get("exchange-mode", "-"),
})
remote_users = []
for x in ppp_active:
remote_users.append({
"name": x.get("name", "-"),
"service": str(x.get("service", "-")).upper(),
"vpn_ip": x.get("address", "-"),
"public_ip": x.get("caller-id", "-"),
"uptime": x.get("uptime", "-"),
})
return {
"site_to_site": site_to_site,
"remote_users": remote_users,
"remote_count": len(remote_users),
"s2s_up": sum(1 for x in site_to_site if x["status"] == "UP"),
"s2s_down": sum(1 for x in site_to_site if x["status"] == "DOWN"),
}
def get_top_interfaces(interfaces, limit=5):
rows = []
for x in interfaces:
if not x["running"] or x["type"] == "loopback":
continue
rows.append((x["rx_bps"] + x["tx_bps"], x))
rows.sort(key=lambda v: v[0], reverse=True)
return [{
"name": x["name"],
"type": x["type"],
"rx": x["rx_human"],
"tx": x["tx_human"],
"rx_bps": x["rx_bps"],
"tx_bps": x["tx_bps"],
} for _, x in rows[:limit]]
def build_alerts(system, wans, vpn):
alerts = []
for w in wans:
if not w["up"]:
alerts.append({"level": "critical", "text": f"{w['label']} is DOWN"})
for v in vpn["site_to_site"]:
if v["status"] == "DOWN":
alerts.append({"level": "critical", "text": f"VPN {v['name']} is DOWN"})
if system["cpu_load"] >= 85:
alerts.append({"level": "warning", "text": f"CPU load is high: {system['cpu_load']}%"})
if system["ram_pct"] >= 85:
alerts.append({"level": "warning", "text": f"RAM usage is high: {system['ram_pct']}%"})
return alerts[:8]
def get_dashboard():
system = get_system()
interfaces = get_interfaces()
addresses = get_addresses()
wans = get_wans(interfaces, addresses)
top_interfaces = get_top_interfaces(interfaces)
alerts = []
for w in wans:
if not w["up"]:
alerts.append({"level": "critical", "text": f"{w['label']} is DOWN"})
if system["cpu_load"] >= 85:
alerts.append({"level": "warning", "text": f"CPU load is high: {system['cpu_load']}%"})
if system["ram_pct"] >= 85:
alerts.append({"level": "warning", "text": f"RAM usage is high: {system['ram_pct']}%"})
if system["hdd_pct"] >= 90:
alerts.append({"level": "warning", "text": f"Storage usage is high: {system['hdd_pct']}%"})
wan_sample = {}
for w in wans:
wan_sample[w["name"]] = {
"label": w["label"],
"rx_bps": w["rx_bps"],
"tx_bps": w["tx_bps"],
}
with _lock:
_history.append({
"t": int(time.time()),
"cpu": system["cpu_load"],
"ram": system["ram_pct"],
"hdd": system["hdd_pct"],
"wans": wan_sample,
})
return {
"connected": True,
"updated_at": time.strftime("%H:%M:%S"),
"refresh_seconds": REFRESH_SECONDS,
"privacy_default": PRIVACY_DEFAULT,
"system": system,
"wans": wans,
"top_interfaces": top_interfaces,
"alerts": alerts,
"summary": {
"wan_up": sum(1 for w in wans if w["up"]),
"wan_total": len(wans),
"alert_count": len(alerts),
},
"history": list(_history),
}
HTML = r'''
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>MikroTik REST Dashboard</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
:root{
--bg:#0b1020;--panel:#121a2d;--panel2:#182238;--text:#eaf0ff;--muted:#8ea0bf;
--ok:#28d17c;--warn:#ffb020;--bad:#ff5b6e;--line:#24324f;--accent:#6ea8fe;
}
*{box-sizing:border-box}
body{margin:0;background:linear-gradient(180deg,#09101d,#0d1424 60%,#0b1020);color:var(--text);
font-family:Inter,Segoe UI,Arial,sans-serif}
.wrap{max-width:1380px;margin:auto;padding:22px}
.topbar{display:flex;justify-content:space-between;gap:20px;align-items:center;margin-bottom:18px}
.brand h1{font-size:24px;margin:0 0 5px}.sub{color:var(--muted);font-size:13px}
.actions{display:flex;gap:10px;align-items:center;flex-wrap:wrap}
.btn{border:1px solid var(--line);background:var(--panel);color:var(--text);padding:9px 13px;border-radius:11px;cursor:pointer}
.btn:hover{background:var(--panel2)}
.badge{padding:7px 10px;border-radius:999px;font-size:12px;border:1px solid var(--line);background:var(--panel)}
.dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:6px;background:var(--ok)}
.cards{display:grid;grid-template-columns:repeat(5,1fr);gap:12px;margin-bottom:16px}
.card,.panel{background:rgba(18,26,45,.92);border:1px solid var(--line);border-radius:16px;box-shadow:0 8px 30px rgba(0,0,0,.18)}
.card{padding:16px;min-height:165px}.card .k{color:var(--text);font-size:15px;font-weight:800;letter-spacing:.01em}
.card .v{font-size:26px;font-weight:800;margin-top:7px}.card .s{font-size:12px;color:var(--muted);margin-top:5px}
.gauge-card{display:flex;flex-direction:column;align-items:center;justify-content:flex-start;gap:10px;text-align:center}
.gauge-card .k{width:100%;text-align:left;padding-bottom:9px;border-bottom:1px solid var(--line)}
.gauge-card .s{text-align:center}
.gauge{--value:0;position:relative;width:92px;height:92px;border-radius:50%;
background:conic-gradient(var(--accent) calc(var(--value)*1%),#202c45 0);flex:0 0 auto}
.gauge:before{content:"";position:absolute;inset:9px;border-radius:50%;background:var(--panel)}
.gauge-value{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
font-size:20px;font-weight:800;z-index:1}
.gauge-copy{min-width:0;width:100%}.gauge-copy .k{margin-bottom:4px}
.metric-card{display:flex;flex-direction:column;justify-content:flex-start}
.metric-card #uptimeSummary{font-size:22px;line-height:1.2;word-break:break-word}
.metric-card .k{padding-bottom:9px;border-bottom:1px solid var(--line);margin-bottom:8px}
.full-panel{margin-bottom:16px}
.wan-chartbox{height:260px}
.wan-chart-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:16px}
.wan-chart-card{border:1px solid var(--line);border-radius:14px;padding:14px;background:rgba(24,34,56,.45)}
.wan-chart-title{font-size:14px;font-weight:800;color:var(--text);margin-bottom:12px}
.metric-icon{font-size:22px;margin-bottom:6px}
.grid2{display:grid;grid-template-columns:1.25fr .75fr;gap:16px;margin-bottom:16px}
.panel{padding:16px}.panel h2{font-size:15px;margin:0 0 14px}.panel-title{display:flex;justify-content:space-between;align-items:center}
.table{width:100%;border-collapse:collapse}.table th,.table td{padding:11px 8px;border-top:1px solid var(--line);text-align:left;font-size:13px}
.table th{color:var(--muted);font-weight:600}.status{font-weight:700}.ok{color:var(--ok)}.bad{color:var(--bad)}.warn{color:var(--warn)}.muted{color:var(--muted)}
.eye{cursor:pointer;opacity:.75;margin-left:6px}.eye:hover{opacity:1}
.alert{padding:10px 12px;border-radius:10px;margin:8px 0;border:1px solid var(--line);background:var(--panel2);font-size:13px}
.alert.critical{border-color:rgba(255,91,110,.4);color:#ffd8de}.alert.warning{border-color:rgba(255,176,32,.4);color:#ffe2a3}
.empty{padding:18px;border:1px dashed var(--line);border-radius:12px;color:var(--muted);text-align:center}
.chartbox{height:250px}
.footer{color:var(--muted);font-size:12px;text-align:center;padding:12px}
@media(max-width:1100px){.cards{grid-template-columns:repeat(3,1fr)}.grid2{grid-template-columns:1fr}}
@media(max-width:760px){.cards{grid-template-columns:repeat(2,1fr)}}
@media(max-width:520px){.wrap{padding:12px}.cards{grid-template-columns:1fr}.topbar{align-items:flex-start;flex-direction:column}.table{display:block;overflow:auto}}
</style>
</head>
<body>
<div class="wrap">
<div class="topbar">
<div class="brand">
<h1>MikroTik REST Dashboard</h1>
<div class="sub" id="routerMeta">Connecting...</div>
</div>
<div class="actions">
<span class="badge" id="connBadge"><span class="dot"></span>Connected</span>
<button class="btn" id="privacyBtn" onclick="togglePrivacy()">👁 Privacy: ON</button>
<span class="badge" id="updated">--:--:--</span>
</div>
</div>
<div class="cards">
<div class="card gauge-card">
<div class="gauge-copy"><div class="k">CPU Load</div></div>
<div class="gauge" id="cpuGauge"><div class="gauge-value" id="cpu">--%</div></div>
<div class="s">Realtime processor usage</div>
</div>
<div class="card gauge-card">
<div class="gauge-copy"><div class="k">RAM Usage</div></div>
<div class="gauge" id="ramGauge"><div class="gauge-value" id="ram">--%</div></div>
<div class="s" id="ramDetail">--</div>
</div>
<div class="card gauge-card">
<div class="gauge-copy"><div class="k">Storage Usage</div></div>
<div class="gauge" id="hddGauge"><div class="gauge-value" id="hdd">--%</div></div>
<div class="s" id="hddDetail">--</div>
</div>
<div class="card metric-card">
<div class="k">Uptime</div>
<div class="metric-icon">⏱️</div>
<div class="v" id="uptimeSummary">--</div><div class="s">Router running time</div>
</div>
<div class="card metric-card">
<div class="k">Alerts</div>
<div class="metric-icon">⚠️</div>
<div class="v" id="alertSummary">--</div><div class="s">Critical / warning</div>
</div>
</div>
<div class="panel full-panel">
<div class="panel-title"><h2>WAN Status</h2><span class="muted">Realtime throughput + cumulative traffic</span></div>
<div id="wanBox"></div>
</div>
<div class="panel full-panel">
<div class="panel-title"><h2>WAN Traffic</h2><span class="muted">One chart per WAN interface</span></div>
<div id="wanChartsContainer"></div>
</div>
<div class="panel full-panel">
<div class="panel-title"><h2>Top Traffic Interfaces</h2><span class="muted">Top 5 realtime interfaces</span></div>
<div id="topBox"></div>
</div>
<div class="panel full-panel">
<div class="panel-title"><h2>Router Load</h2><span class="muted">CPU / RAM / Storage history</span></div>
<div class="chartbox"><canvas id="loadChart"></canvas></div>
</div>
<div class="footer">Single File Edition v2.0 · REST API · Read-only monitoring</div>
</div>
<script>
// Privacy Mode masks Public IP values in WAN Status only.
// Generic WAN labels and Top Traffic Interface names remain visible.
let privacy = true;
let lastData = null;
let chart = null;
let wanCharts = {};
function esc(v){return String(v ?? '').replace(/[&<>"']/g,m=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]))}
function maskIP(ip){
if(!ip || ip==='-') return '-';
return '••••••••••••';
}
function maskUser(s){
if(!s || s==='-') return '-';
return '••••••••••••';
}
function maskName(s){
if(!s || s==='-') return '-';
return '••••••••••••';
}
function sensitive(value,type){
if(!privacy) return esc(value||'-');
if(type==='user') return esc(maskUser(value));
if(type==='name') return esc(maskName(value));
return esc(maskIP(value));
}
function stateClass(s){
s=String(s).toUpperCase();
return (s==='UP'||s==='ESTABLISHED')?'ok':(s==='DISABLED'?'muted':'bad');
}
function togglePrivacy(){
privacy=!privacy;
localStorage.setItem('mikrotik_privacy', privacy?'1':'0');
document.getElementById('privacyBtn').textContent=`👁 Privacy: ${privacy?'ON':'OFF'}`;
if(lastData) render(lastData);
}
function revealOne(id,raw){
const el=document.getElementById(id);
if(!el) return;
if(el.dataset.revealed==='1'){
el.dataset.revealed='0';
if(lastData) render(lastData);
return;
}
el.textContent=raw||'-';
el.dataset.revealed='1';
}
function render(d){
lastData=d;
const s=d.system;
document.getElementById('routerMeta').textContent=`${s.board} · RouterOS ${s.version} · Uptime ${s.uptime}`;
document.getElementById('updated').textContent=`Updated ${d.updated_at}`;
document.getElementById('cpu').textContent=`${s.cpu_load}%`;
document.getElementById('ram').textContent=`${s.ram_pct}%`;
document.getElementById('hdd').textContent=`${s.hdd_pct}%`;
document.getElementById('cpuGauge').style.setProperty('--value', Math.max(0,Math.min(100,s.cpu_load)));
document.getElementById('ramGauge').style.setProperty('--value', Math.max(0,Math.min(100,s.ram_pct)));
document.getElementById('hddGauge').style.setProperty('--value', Math.max(0,Math.min(100,s.hdd_pct)));
document.getElementById('ramDetail').textContent=`${s.ram_used} / ${s.ram_total}`;
document.getElementById('hddDetail').textContent=`${s.hdd_used} / ${s.hdd_total}`;
document.getElementById('uptimeSummary').textContent=s.uptime;
document.getElementById('alertSummary').textContent=d.summary.alert_count;
document.getElementById('wanBox').innerHTML = d.wans.length ? `
<table class="table"><thead><tr><th>WAN</th><th>Status</th><th>Public IP</th><th>RX</th><th>TX</th><th>Total Received</th><th>Total Sent</th></tr></thead><tbody>
${d.wans.map((w,i)=>`<tr>
<td><strong>WAN${i+1}</strong></td>
<td class="status ${w.up?'ok':'bad'}">${w.up?'● UP':'● DOWN'}</td>
<td><span id="wanip${i}">${sensitive(w.ip,'ip')}</span><span class="eye" onclick="revealOne('wanip${i}','${esc(w.ip)}')">👁</span></td>
<td>${esc(w.rx)}</td>
<td>${esc(w.tx)}</td>
<td>${esc(w.rx_total_human)}</td>
<td>${esc(w.tx_total_human)}</td>
</tr>`).join('')}
</tbody></table>` : `<div class="empty">No PPPoE WAN interfaces found.</div>`;
document.getElementById('topBox').innerHTML = d.top_interfaces.length ? `
<table class="table"><thead><tr><th>Interface</th><th>RX</th><th>TX</th></tr></thead><tbody>
${d.top_interfaces.map(x=>`<tr>
<td><strong>${esc(x.name)}</strong><div class="muted">${esc(x.type)}</div></td>
<td>${esc(x.rx)}</td><td>${esc(x.tx)}</td></tr>`).join('')}
</tbody></table>` : `<div class="empty">Waiting for traffic samples...</div>`;
drawWanCharts(d.history, d.wans);
drawChart(d.history);
}
function drawChart(h){
if(!window.Chart) return;
const labels=h.map(x=>new Date(x.t*1000).toLocaleTimeString());
const data={
labels,
datasets:[
{label:'CPU %',data:h.map(x=>x.cpu),tension:.25,borderWidth:2,pointRadius:0},
{label:'RAM %',data:h.map(x=>x.ram),tension:.25,borderWidth:2,pointRadius:0},
{label:'Storage %',data:h.map(x=>x.hdd),tension:.25,borderWidth:2,pointRadius:0}
]
};
if(chart){chart.data=data;chart.update('none');return}
chart=new Chart(document.getElementById('loadChart'),{
type:'line',data,
options:{responsive:true,maintainAspectRatio:false,animation:false,
plugins:{legend:{labels:{color:'#b9c6de'}}},
scales:{x:{ticks:{color:'#8090ad',maxTicksLimit:6},grid:{color:'#24324f'}},
y:{min:0,max:100,ticks:{color:'#8090ad'},grid:{color:'#24324f'}}}}
});
}
function toMbps(bps){ return Number(bps || 0) / 1000000; }
function drawWanCharts(h,wans){
if(!window.Chart) return;
const container=document.getElementById('wanChartsContainer');
if(!container) return;
const labels=h.map(x=>new Date(x.t*1000).toLocaleTimeString());
const wanCount=(wans || []).length;
if(!wanCount){
container.innerHTML='<div class="empty">No WAN interfaces found.</div>';
Object.values(wanCharts).forEach(c=>{ try{ c.destroy(); }catch(e){} });
wanCharts={};
return;
}
container.innerHTML=`<div class="wan-chart-grid">
${wans.map((w,idx)=>`
<div class="wan-chart-card">
<div class="wan-chart-title">WAN${idx+1} Traffic</div>
<div class="wan-chartbox"><canvas id="wanChart${idx}"></canvas></div>
</div>
`).join('')}
</div>`;
const nextCharts={};
wans.forEach((w,idx)=>{
const canvas=document.getElementById(`wanChart${idx}`);
if(!canvas) return;
const data={
labels,
datasets:[
{
label:'RX',
data:h.map(x=>toMbps((x.wans && x.wans[w.name]) ? x.wans[w.name].rx_bps : 0)),
tension:.25,
borderWidth:2,
pointRadius:0
},
{
label:'TX',
data:h.map(x=>toMbps((x.wans && x.wans[w.name]) ? x.wans[w.name].tx_bps : 0)),
tension:.25,
borderWidth:2,
pointRadius:0,
borderDash:[6,4]
}
]
};
const chartInstance=new Chart(canvas,{
type:'line',
data,
options:{
responsive:true,
maintainAspectRatio:false,
animation:false,
interaction:{mode:'index',intersect:false},
plugins:{legend:{labels:{color:'#b9c6de'}}},
scales:{
x:{ticks:{color:'#8090ad',maxTicksLimit:6},grid:{color:'#24324f'}},
y:{
beginAtZero:true,
ticks:{color:'#8090ad',callback:v=>`${v} Mbps`},
grid:{color:'#24324f'},
title:{display:true,text:'Mbps',color:'#8090ad'}
}
}
}
});
nextCharts[`wanChart${idx}`]=chartInstance;
});
Object.values(wanCharts).forEach(c=>{ try{ c.destroy(); }catch(e){} });
wanCharts=nextCharts;
}
async function load(){
try{
const r=await fetch('/api/dashboard',{cache:'no-store'});
if(!r.ok) throw new Error(await r.text());
const d=await r.json();
document.getElementById('connBadge').innerHTML='<span class="dot"></span>Connected';
render(d);
setTimeout(load,(d.refresh_seconds||5)*1000);
}catch(e){
document.getElementById('connBadge').innerHTML='<span class="dot" style="background:var(--bad)"></span>Disconnected';
document.getElementById('routerMeta').textContent='Cannot read MikroTik REST API';
setTimeout(load,5000);
}
}
const saved=localStorage.getItem('mikrotik_privacy');
privacy=saved===null ? {{ 'true' if privacy_default else 'false' }} : saved==='1';
document.getElementById('privacyBtn').textContent=`👁 Privacy: ${privacy?'ON':'OFF'}`;
load();
</script>
</body>
</html>
'''
@app.route("/")
def index():
return render_template_string(HTML, privacy_default=PRIVACY_DEFAULT)
@app.route("/api/dashboard")
def dashboard():
try:
return jsonify(get_dashboard())
except Exception as e:
return jsonify({"connected": False, "error": str(e)}), 502
@app.route("/health")
def health():
try:
r = api_get("system/resource")
return jsonify({"ok": True, "router": r.get("board-name"), "version": r.get("version")})
except Exception as e:
return jsonify({"ok": False, "error": str(e)}), 502
if __name__ == "__main__":
print("")
print("MikroTik REST Dashboard - Single File Edition v2.0")
print(f"Router : {HOST}:{PORT}")
print(f"REST : {BASE_URL}")
print("Web UI : http://127.0.0.1:5000")
print("")
app.run(host="0.0.0.0", port=5000, debug=False, threaded=True)
Read more
- Understanding AI Training: What It Is and Why It Matters
- Important Updates Coming in n8n Version 3.0
- Top 10 AI Technologies to Watch in 2026
- Understanding Hiberfil.sys and Pagefile.sys: Essential Windows Components
- Top 5 Open Source NAS Software for 2026
- The Best AI Orchestration Tools of 2026
- What is TrueNAS ? Choosing the Right Version in 2026
- How to Install a LEMP Stack on RHEL, Rocky, AlmaLinux and CentOS Stream
- OpenAI integrations | Workflow automation with n8n: A Step-by-Step Guide
- n8n vs Zapier: Choosing the Right Automation Tool