BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
@@ -11,4 +11,4 @@ reboot_threads = 300
|
||||
# tool or the privileged API will not work using admin as the password.
|
||||
# If you change the password, you can pass that password here.
|
||||
|
||||
whatsminer_pwd = "admin"
|
||||
whatsminer_pwd = "admin"
|
||||
|
||||
5
tools/web_monitor/__init__.py
Normal file
5
tools/web_monitor/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from tools.web_monitor.app import app
|
||||
import uvicorn
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run("app:app", host="127.0.0.1", port=80)
|
||||
325
tools/web_monitor/app.py
Normal file
325
tools/web_monitor/app.py
Normal file
@@ -0,0 +1,325 @@
|
||||
import asyncio
|
||||
import datetime
|
||||
import ipaddress
|
||||
import os
|
||||
|
||||
import uvicorn
|
||||
import websockets.exceptions
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi import WebSocket, WebSocketDisconnect
|
||||
from fastapi.responses import RedirectResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
from network import MinerNetwork
|
||||
from tools.web_monitor.miner_factory import miner_factory
|
||||
from tools.web_monitor.web_settings import get_current_settings, update_settings
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||
templates = Jinja2Templates(directory="templates")
|
||||
|
||||
|
||||
@app.get("/")
|
||||
def index(request: Request):
|
||||
return RedirectResponse(request.url_for('dashboard'))
|
||||
|
||||
|
||||
@app.get("/dashboard")
|
||||
def dashboard(request: Request):
|
||||
return templates.TemplateResponse("index.html", {
|
||||
"request": request,
|
||||
"cur_miners": get_current_miner_list()
|
||||
})
|
||||
|
||||
|
||||
@app.websocket("/dashboard/ws")
|
||||
async def dashboard_websocket(websocket: WebSocket):
|
||||
await websocket.accept()
|
||||
graph_sleep_time = get_current_settings()["graph_data_sleep_time"]
|
||||
try:
|
||||
while True:
|
||||
miners = get_current_miner_list()
|
||||
all_miner_data = []
|
||||
data_gen = asyncio.as_completed(
|
||||
[get_miner_data_dashboard(miner_ip) for miner_ip in miners])
|
||||
for all_data in data_gen:
|
||||
data_point = await all_data
|
||||
all_miner_data.append(data_point)
|
||||
all_miner_data.sort(key=lambda x: x["ip"])
|
||||
await websocket.send_json(
|
||||
{"datetime": datetime.datetime.now().isoformat(),
|
||||
"miners": all_miner_data})
|
||||
await asyncio.sleep(graph_sleep_time)
|
||||
except WebSocketDisconnect:
|
||||
print("Websocket disconnected.")
|
||||
pass
|
||||
except websockets.exceptions.ConnectionClosedOK:
|
||||
pass
|
||||
|
||||
|
||||
async def get_miner_data_dashboard(miner_ip):
|
||||
try:
|
||||
settings = get_current_settings()
|
||||
miner_identify_timeout = settings["miner_identify_timeout"]
|
||||
miner_data_timeout = settings["miner_data_timeout"]
|
||||
|
||||
miner_ip = await asyncio.wait_for(miner_factory.get_miner(miner_ip), miner_identify_timeout)
|
||||
|
||||
miner_summary = await asyncio.wait_for(miner_ip.api.summary(), miner_data_timeout)
|
||||
if miner_summary:
|
||||
if 'MHS av' in miner_summary['SUMMARY'][0].keys():
|
||||
hashrate = format(
|
||||
round(miner_summary['SUMMARY'][0]['MHS av'] / 1000000,
|
||||
2), ".2f")
|
||||
elif 'GHS av' in miner_summary['SUMMARY'][0].keys():
|
||||
hashrate = format(
|
||||
round(miner_summary['SUMMARY'][0]['GHS av'] / 1000, 2),
|
||||
".2f")
|
||||
else:
|
||||
hashrate = 0
|
||||
else:
|
||||
hashrate = 0
|
||||
|
||||
return {"ip": str(miner_ip.ip), "hashrate": hashrate}
|
||||
|
||||
except asyncio.exceptions.TimeoutError:
|
||||
return {"ip": miner_ip, "error": "The miner_ip is not responding."}
|
||||
|
||||
except KeyError:
|
||||
return {"ip": miner_ip,
|
||||
"error": "The miner_ip returned unusable/unsupported data."}
|
||||
|
||||
|
||||
@app.get("/scan")
|
||||
def scan(request: Request):
|
||||
return templates.TemplateResponse("scan.html", {
|
||||
"request": request,
|
||||
"cur_miners": get_current_miner_list()
|
||||
})
|
||||
|
||||
|
||||
@app.get("/miner")
|
||||
def miner(_request: Request, _miner_ip):
|
||||
return get_miner
|
||||
|
||||
|
||||
@app.websocket("/miner/{miner_ip}/ws")
|
||||
async def miner_websocket(websocket: WebSocket, miner_ip):
|
||||
await websocket.accept()
|
||||
settings = get_current_settings()
|
||||
miner_identify_timeout = settings["miner_identify_timeout"]
|
||||
miner_data_timeout = settings["miner_data_timeout"]
|
||||
|
||||
try:
|
||||
while True:
|
||||
try:
|
||||
cur_miner = await asyncio.wait_for(
|
||||
miner_factory.get_miner(str(miner_ip)), miner_identify_timeout)
|
||||
|
||||
data = await asyncio.wait_for(
|
||||
cur_miner.api.multicommand("summary", "fans", "stats"), miner_data_timeout)
|
||||
|
||||
miner_model = await cur_miner.get_model()
|
||||
|
||||
miner_summary = None
|
||||
miner_fans = None
|
||||
if "summary" in data.keys():
|
||||
miner_summary = data["summary"][0]
|
||||
elif "SUMMARY" in data.keys():
|
||||
miner_summary = data
|
||||
miner_fans = {"FANS": []}
|
||||
for item in ["Fan Speed In", "Fan Speed Out"]:
|
||||
if item in miner_summary["SUMMARY"][0].keys():
|
||||
miner_fans["FANS"].append(
|
||||
{"RPM": miner_summary["SUMMARY"][0][item]})
|
||||
|
||||
if "fans" in data.keys():
|
||||
miner_fans = data["fans"][0]
|
||||
|
||||
if "stats" in data.keys():
|
||||
miner_stats = data["stats"][0]
|
||||
miner_fans = {"FANS": []}
|
||||
for item in ["fan1", "fan2", "fan3", "fan4"]:
|
||||
if item in miner_stats["STATS"][1].keys():
|
||||
miner_fans["FANS"].append(
|
||||
{"RPM": miner_stats["STATS"][1][item]})
|
||||
|
||||
if miner_summary:
|
||||
if 'MHS av' in miner_summary['SUMMARY'][0].keys():
|
||||
hashrate = format(
|
||||
round(
|
||||
miner_summary['SUMMARY'][0]['MHS av'] / 1000000,
|
||||
2), ".2f")
|
||||
elif 'GHS av' in miner_summary['SUMMARY'][0].keys():
|
||||
hashrate = format(
|
||||
round(miner_summary['SUMMARY'][0]['GHS av'] / 1000,
|
||||
2),
|
||||
".2f")
|
||||
else:
|
||||
hashrate = 0
|
||||
else:
|
||||
hashrate = 0
|
||||
|
||||
fan_speeds = []
|
||||
|
||||
if miner_fans:
|
||||
for fan in miner_fans["FANS"]:
|
||||
fan_speeds.append(fan["RPM"])
|
||||
|
||||
while len(fan_speeds) < 5:
|
||||
fan_speeds.append(0)
|
||||
|
||||
data = {"hashrate": hashrate,
|
||||
"fans": fan_speeds,
|
||||
"datetime": datetime.datetime.now().isoformat(),
|
||||
"model": miner_model}
|
||||
await websocket.send_json(data)
|
||||
await asyncio.sleep(settings["graph_sleep_time"])
|
||||
except asyncio.exceptions.TimeoutError:
|
||||
data = {"error": "The miner is not responding."}
|
||||
await websocket.send_json(data)
|
||||
await asyncio.sleep(.5)
|
||||
except KeyError as e:
|
||||
print(e)
|
||||
data = {
|
||||
"error": "The miner returned unusable/unsupported data."}
|
||||
await websocket.send_json(data)
|
||||
await asyncio.sleep(.5)
|
||||
except WebSocketDisconnect:
|
||||
print("Websocket disconnected.")
|
||||
except websockets.exceptions.ConnectionClosedOK:
|
||||
pass
|
||||
|
||||
|
||||
@app.get("/miner/{miner_ip}")
|
||||
def get_miner(request: Request, miner_ip):
|
||||
return templates.TemplateResponse("miner.html", {
|
||||
"request": request,
|
||||
"cur_miners": get_current_miner_list(),
|
||||
"miner": miner_ip
|
||||
})
|
||||
|
||||
|
||||
@app.get("/miner_ip/{miner_ip}/remove")
|
||||
def get_miner(request: Request, miner_ip):
|
||||
miners = get_current_miner_list()
|
||||
miners.remove(miner_ip)
|
||||
with open("miner_list.txt", "w") as file:
|
||||
for miner_ip in miners:
|
||||
file.write(miner_ip + "\n")
|
||||
|
||||
return RedirectResponse(request.url_for('dashboard'))
|
||||
|
||||
|
||||
def get_current_miner_list():
|
||||
cur_miners = []
|
||||
if os.path.exists(os.path.join(os.getcwd(), "miner_list.txt")):
|
||||
with open(os.path.join(os.getcwd(), "miner_list.txt")) as file:
|
||||
for line in file.readlines():
|
||||
cur_miners.append(line.strip())
|
||||
cur_miners = sorted(cur_miners, key=lambda x: ipaddress.ip_address(x))
|
||||
return cur_miners
|
||||
|
||||
|
||||
@app.route("/settings", methods=["GET", "POST"])
|
||||
async def settings(request: Request):
|
||||
return templates.TemplateResponse("settings.html", {
|
||||
"request": request,
|
||||
"cur_miners": get_current_miner_list(),
|
||||
"settings": get_current_settings()
|
||||
})
|
||||
|
||||
|
||||
@app.post("/settings/update")
|
||||
async def update_settings_page(request: Request):
|
||||
data = await request.form()
|
||||
graph_data_sleep_time = data.get('graph_data_sleep_time')
|
||||
miner_data_timeout = data.get('miner_data_timeout')
|
||||
miner_identify_timeout = data.get('miner_identify_timeout')
|
||||
new_settings = {
|
||||
"graph_data_sleep_time": int(graph_data_sleep_time),
|
||||
"miner_data_timeout": int(miner_data_timeout),
|
||||
"miner_identify_timeout": int(miner_identify_timeout),
|
||||
}
|
||||
update_settings(new_settings)
|
||||
return RedirectResponse(request.url_for("settings"))
|
||||
|
||||
|
||||
|
||||
|
||||
@app.get("/remove_all_miners")
|
||||
async def remove_all_miners(request: Request):
|
||||
file = open("miner_list.txt", "w")
|
||||
file.close()
|
||||
return RedirectResponse(request.url_for("settings"))
|
||||
|
||||
|
||||
@app.post("/scan/add_miners")
|
||||
async def add_miners_scan(request: Request):
|
||||
miners = await request.json()
|
||||
with open("miner_list.txt", "a+") as file:
|
||||
for miner_ip in miners["miners"]:
|
||||
file.write(miner_ip + "\n")
|
||||
return scan
|
||||
|
||||
|
||||
@app.websocket("/scan/ws")
|
||||
async def websocket_scan(websocket: WebSocket):
|
||||
await websocket.accept()
|
||||
cur_task = None
|
||||
try:
|
||||
while True:
|
||||
ws_data = await websocket.receive_text()
|
||||
if "-Cancel-" in ws_data:
|
||||
if cur_task:
|
||||
cur_task.cancel()
|
||||
try:
|
||||
await cur_task
|
||||
except asyncio.CancelledError:
|
||||
cur_task = None
|
||||
await websocket.send_text("Cancelled")
|
||||
else:
|
||||
cur_task = asyncio.create_task(
|
||||
do_websocket_scan(websocket, ws_data))
|
||||
if cur_task and cur_task.done():
|
||||
cur_task = None
|
||||
except WebSocketDisconnect:
|
||||
print("Websocket disconnected.")
|
||||
except websockets.exceptions.ConnectionClosedOK:
|
||||
pass
|
||||
|
||||
|
||||
async def do_websocket_scan(websocket: WebSocket, network_ip: str):
|
||||
cur_miners = get_current_miner_list()
|
||||
try:
|
||||
if "/" in network_ip:
|
||||
network_ip, network_subnet = network_ip.split("/")
|
||||
network = MinerNetwork(network_ip, mask=network_subnet)
|
||||
else:
|
||||
network = MinerNetwork(network_ip)
|
||||
miner_generator = network.scan_network_generator()
|
||||
miners = []
|
||||
async for miner_ip in miner_generator:
|
||||
if miner_ip and str(miner_ip) not in cur_miners:
|
||||
miners.append(miner_ip)
|
||||
|
||||
get_miner_genenerator = miner_factory.get_miner_generator(miners)
|
||||
all_miners = []
|
||||
async for found_miner in get_miner_genenerator:
|
||||
all_miners.append(
|
||||
{"ip": found_miner.ip, "model": await found_miner.get_model()})
|
||||
all_miners.sort(key=lambda x: x["ip"])
|
||||
send_miners = []
|
||||
for miner_ip in all_miners:
|
||||
send_miners.append(
|
||||
{"ip": str(miner_ip["ip"]), "model": miner_ip["model"]})
|
||||
await websocket.send_json(send_miners)
|
||||
await websocket.send_text("Done")
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run("app:app", host="127.0.0.1", port=80)
|
||||
7
tools/web_monitor/miner_factory.py
Normal file
7
tools/web_monitor/miner_factory.py
Normal file
@@ -0,0 +1,7 @@
|
||||
"""
|
||||
This file stores the MinerFactory instance used by the WebMonitor for use in other files.
|
||||
"""
|
||||
|
||||
from miners.miner_factory import MinerFactory
|
||||
|
||||
miner_factory = MinerFactory()
|
||||
165
tools/web_monitor/static/navbar.css
Normal file
165
tools/web_monitor/static/navbar.css
Normal file
@@ -0,0 +1,165 @@
|
||||
body {
|
||||
min-height: 100vh;
|
||||
min-height: -webkit-fill-available;
|
||||
}
|
||||
|
||||
html {
|
||||
height: -webkit-fill-available;
|
||||
}
|
||||
|
||||
main {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
height: 100vh;
|
||||
height: -webkit-fill-available;
|
||||
max-height: 100vh;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.bi {
|
||||
vertical-align: -.125em;
|
||||
pointer-events: none;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.dropdown-toggle { outline: 0; }
|
||||
|
||||
.nav-flush .nav-link {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.btn-toggle-nav a {
|
||||
display: inline-flex;
|
||||
padding: .1875rem .5rem;
|
||||
margin-top: .125rem;
|
||||
margin-left: 1.25rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn-toggle-nav a:hover,
|
||||
.btn-toggle-nav a:focus {
|
||||
background-color: #0d6efd;
|
||||
}
|
||||
|
||||
.scrollarea {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.fw-semibold { font-weight: 600; }
|
||||
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
/* rtl:raw:
|
||||
right: 0;
|
||||
*/
|
||||
bottom: 0;
|
||||
/* rtl:remove */
|
||||
left: 0;
|
||||
z-index: 100; /* Behind the navbar */
|
||||
padding: 0px 0 0; /* Height of navbar */
|
||||
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.sidebar {
|
||||
top: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-sticky {
|
||||
position: relative;
|
||||
top: 0;
|
||||
height: calc(100vh - 48px);
|
||||
padding-top: .5rem;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.sidebar .nav-link {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.sidebar .nav-link .feather {
|
||||
margin-right: 4px;
|
||||
color: #727272;
|
||||
}
|
||||
|
||||
.sidebar .nav-link.active {
|
||||
color: #2470dc;
|
||||
}
|
||||
|
||||
.sidebar .nav-link:hover .feather,
|
||||
.sidebar .nav-link.active .feather {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.sidebar-heading {
|
||||
font-size: .75rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
padding-top: .75rem;
|
||||
padding-bottom: .75rem;
|
||||
font-size: 1rem;
|
||||
background-color: rgba(0, 0, 0, .25);
|
||||
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .25);
|
||||
}
|
||||
|
||||
.navbar .navbar-toggler {
|
||||
top: .25rem;
|
||||
right: 1rem;
|
||||
}
|
||||
|
||||
.navbar .form-control {
|
||||
padding: .75rem 1rem;
|
||||
border-width: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.form-control-dark {
|
||||
color: #fff;
|
||||
background-color: rgba(255, 255, 255, .1);
|
||||
border-color: rgba(255, 255, 255, .1);
|
||||
}
|
||||
|
||||
.form-control-dark:focus {
|
||||
border-color: transparent;
|
||||
box-shadow: 0 0 0 3px rgba(255, 255, 255, .25);
|
||||
}
|
||||
|
||||
.btn-toggle-nav{
|
||||
max-height: 300px;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
/* Scrollbar */
|
||||
.btn-toggle-nav::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
}
|
||||
.btn-toggle-nav::-webkit-scrollbar-track {
|
||||
box-shadow: inset 0 0 5px grey;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.btn-toggle-nav::-webkit-scrollbar-thumb {
|
||||
background-image: linear-gradient(180deg, #D0368A 0%, #708AD4 99%);
|
||||
box-shadow: inset 2px 2px 5px 0 rgba(#fff, 0.5);
|
||||
border-radius: 100px;
|
||||
}
|
||||
|
||||
.nav-pills .nav-link.active {
|
||||
color: #212529;
|
||||
background-image: linear-gradient(180deg, #D0368A 0%, #708AD4 99%);
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
background-image: linear-gradient(180deg, #760A45 0%, #23449F 99%);
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
transition: unset;
|
||||
color: unset;
|
||||
}
|
||||
127
tools/web_monitor/templates/index.html
Normal file
127
tools/web_monitor/templates/index.html
Normal file
@@ -0,0 +1,127 @@
|
||||
{% extends 'navbar.html'%}
|
||||
{% block content %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/luxon@2.3.1/build/global/luxon.min.js"></script>
|
||||
<canvas id="line-chart" class="grad-border mt-3 mb-4" width="600" height="360"></canvas>
|
||||
{% if cur_miners|length == 0 %}<a role="button" href="{{url_for('scan')}}" id="noMiners" class="w-100 btn btn-info">Click here to add miners.</a>{% endif %}
|
||||
|
||||
|
||||
<div id="errors"></div>
|
||||
|
||||
<script>
|
||||
var ws = new WebSocket("ws://localhost:80/dashboard/ws");
|
||||
let all_data = []
|
||||
let all_labels = []
|
||||
ws.onmessage = function(event) {
|
||||
var new_data = JSON.parse(event.data)
|
||||
if (!new_data["miners"].length == 0) {
|
||||
total_hashrate = parseFloat(0)
|
||||
errors = document.getElementById("errors")
|
||||
for (i = 0; i< new_data["miners"].length; i++) {
|
||||
if (new_data["miners"][i].hasOwnProperty("error")) {
|
||||
if (!document.getElementById(new_data["miners"][i]["ip"] + "_error")) {
|
||||
errors.innerHTML += "<div id='" + new_data["miners"][i]["ip"] + "_error" +
|
||||
"' class='d-flex align-items-center p-1 mb-1 ms-4 alert alert-danger'><strong class='p-0 m-0'>" +
|
||||
new_data["miners"][i]["ip"] + ": " +
|
||||
new_data["miners"][i]["error"] +
|
||||
"</strong><div class='spinner-border spinner-border-sm ms-auto'></div></div>"
|
||||
}
|
||||
} else {
|
||||
if (document.getElementById(new_data["miners"][i]["ip"] + "_error")) {
|
||||
document.getElementById(new_data["miners"][i]["ip"] + "_error").remove()
|
||||
}
|
||||
total_hashrate += parseFloat(new_data["miners"][i]["hashrate"])
|
||||
}
|
||||
};
|
||||
var chart = document.getElementById("line-chart")
|
||||
datetime = luxon.DateTime.fromISO(new_data["datetime"]).toLocal();
|
||||
if (minerDataChart.data.labels.length > 50) minerDataChart.data.labels.shift();
|
||||
if (minerDataChart.data.datasets[0].data.length > 50) minerDataChart.data.datasets[0].data.shift();
|
||||
minerDataChart.data.labels.push(datetime.toLocaleString(luxon.DateTime.TIME_WITH_SECONDS));
|
||||
minerDataChart.data.datasets[0].data.push(total_hashrate.toFixed(2));
|
||||
minerDataChart.update();
|
||||
}
|
||||
};
|
||||
|
||||
var ctx = document.getElementById("line-chart").getContext("2d");
|
||||
var width = document.getElementById("line-chart").width;
|
||||
var chartGradient = ctx.createLinearGradient(0, 0, width, 0)
|
||||
chartGradient.addColorStop(0, '#D0368A');
|
||||
chartGradient.addColorStop(1, '#708AD4');
|
||||
|
||||
const chartAreaBorder = {
|
||||
id: 'chartAreaBorder',
|
||||
beforeDraw(chart, args, options) {
|
||||
const {ctx, chartArea: {left, top, width, height}} = chart;
|
||||
ctx.save();
|
||||
ctx.strokeStyle = options.borderColor;
|
||||
ctx.lineWidth = options.borderWidth;
|
||||
ctx.strokeRect(left, top, width, height);
|
||||
ctx.restore();
|
||||
}
|
||||
};
|
||||
|
||||
var minerDataChart = new Chart(document.getElementById("line-chart"), {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [
|
||||
],
|
||||
datasets: [{
|
||||
label: "Hashrate",
|
||||
borderColor: chartGradient,
|
||||
pointBorderColor: chartGradient,
|
||||
pointBackgroundColor: chartGradient,
|
||||
pointHoverBackgroundColor: chartGradient,
|
||||
pointHoverBorderColor: chartGradient,
|
||||
data: [
|
||||
],
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [chartAreaBorder],
|
||||
options: {
|
||||
animation: {
|
||||
easing: 'easeInSine',
|
||||
duration: 0
|
||||
},
|
||||
plugins: {
|
||||
chartAreaBorder: {
|
||||
borderColor: chartGradient,
|
||||
borderWidth: 1
|
||||
},
|
||||
legend: {
|
||||
labels: {
|
||||
color: chartGradient
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(data) {
|
||||
return data.dataset.data[data.dataIndex] + " TH/s";
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
min: 0, // minimum value
|
||||
suggestedMax: 100,
|
||||
stepSize: 10,
|
||||
ticks: {
|
||||
callback: function(value, index, ticks) {
|
||||
return value + " TH/s";
|
||||
}
|
||||
}
|
||||
},
|
||||
x: {
|
||||
ticks: {
|
||||
maxTicksLimit: 6,
|
||||
maxRotation: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
{% endblock content %}
|
||||
285
tools/web_monitor/templates/miner.html
Normal file
285
tools/web_monitor/templates/miner.html
Normal file
@@ -0,0 +1,285 @@
|
||||
{% extends 'navbar.html'%}
|
||||
{% block content %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/luxon@2.3.1/build/global/luxon.min.js"></script>
|
||||
<div class="row mt-2">
|
||||
<div class="col">
|
||||
<h2 class="ms-4">{{miner}}</h2>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="d-flex flex-row-reverse">
|
||||
<button type="button" class="btn btn-outline-danger mx-1" data-bs-toggle="modal" data-bs-target="#removeModal">
|
||||
Remove Miner
|
||||
</button>
|
||||
|
||||
<!-- Modal -->
|
||||
<div class="modal fade" id="removeModal" tabindex="-1" aria-labelledby="removeModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="removeModalLabel">Remove Miner</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
Do you really want to remove this miner?
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<a class="btn btn-danger" href="{{url_for('miner')}}/{{miner}}/remove" role="button">Remove</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a class="btn btn-primary mx-1" target="_blank" href="http://{{miner}}" role="button">Web Interface</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="col-10">
|
||||
<canvas id="line-chart" class="grad-border mt-3" width="600" height="360"></canvas>
|
||||
</div>
|
||||
<div class="col-2 mt-2">
|
||||
<div class="d-flex justify-content-center" id="fan1">Fan 1</div>
|
||||
<canvas class="mb-2" id="fan-chart-1" width="100" height="100"></canvas>
|
||||
<div class="d-flex justify-content-center" id="fan2">Fan 2</div>
|
||||
<canvas class="mb-2" id="fan-chart-2" width="100" height="100"></canvas>
|
||||
<div class="d-flex justify-content-center" id="fan3">Fan 3</div>
|
||||
<canvas class="mb-2" id="fan-chart-3" width="100" height="100"></canvas>
|
||||
<div class="d-flex justify-content-center" id="fan4">Fan 4</div>
|
||||
<canvas class="mb-2" id="fan-chart-4" width="100" height="100"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="d-flex align-items-center mt-4 ms-4 alert alert-secondary">
|
||||
<div class="mx-auto">Model:</div>
|
||||
<div class="mx-auto fw-bolder" id="minerModel">?</div>
|
||||
<div class="mx-auto" style="border-left: 1px solid grey; height: 50px;"></div>
|
||||
<div class="mx-auto">Hashrate:</div>
|
||||
<div class="mx-auto fw-bolder" id="minerHashrate">?</div>
|
||||
</div>
|
||||
|
||||
<div id="errorContainer" class="d-flex align-items-center mt-4 ms-4 alert alert-danger invisible">
|
||||
<strong id="errorCode"></strong>
|
||||
<div class="spinner-border ms-auto"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var ws = new WebSocket("ws://localhost:80/miner/{{miner}}/ws");
|
||||
let all_data = []
|
||||
let all_labels = []
|
||||
ws.onmessage = function(event) {
|
||||
var new_data = JSON.parse(event.data)
|
||||
if (new_data.hasOwnProperty("error")) {
|
||||
var err_container = document.getElementById("errorContainer")
|
||||
var err_code = document.getElementById("errorCode")
|
||||
err_code.innerHTML = new_data['error']
|
||||
err_container.classList.remove("invisible")
|
||||
var miner_hr = document.getElementById("minerHashrate")
|
||||
miner_hr.innerHTML = "?"
|
||||
} else {
|
||||
var chart = document.getElementById("line-chart")
|
||||
var err_container = document.getElementById("errorContainer")
|
||||
if (!err_container.classList.hasOwnProperty("invisible")) {
|
||||
err_container.classList.add("invisible")
|
||||
}
|
||||
datetime = luxon.DateTime.fromISO(new_data["datetime"]).toLocal();
|
||||
if (minerDataChart.data.labels.length > 50) minerDataChart.data.labels.shift();
|
||||
if (minerDataChart.data.datasets[0].data.length > 50) minerDataChart.data.datasets[0].data.shift();
|
||||
minerDataChart.data.labels.push(datetime.toLocaleString(luxon.DateTime.TIME_WITH_SECONDS));
|
||||
minerDataChart.data.datasets[0].data.push(new_data["hashrate"].toFixed(2));
|
||||
fan1Chart.data.datasets[0].data = [new_data["fans"][0], 6000-new_data["fans"][0]]
|
||||
fan2Chart.data.datasets[0].data = [new_data["fans"][1], 6000-new_data["fans"][1]]
|
||||
fan3Chart.data.datasets[0].data = [new_data["fans"][2], 6000-new_data["fans"][2]]
|
||||
fan4Chart.data.datasets[0].data = [new_data["fans"][3], 6000-new_data["fans"][3]]
|
||||
document.getElementById("fan1").innerHTML = "Fan 1: " + new_data["fans"][0]
|
||||
document.getElementById("fan2").innerHTML = "Fan 2: " + new_data["fans"][1]
|
||||
document.getElementById("fan3").innerHTML = "Fan 3: " + new_data["fans"][2]
|
||||
document.getElementById("fan4").innerHTML = "Fan 4: " + new_data["fans"][3]
|
||||
fan1Chart.update();
|
||||
fan2Chart.update();
|
||||
fan3Chart.update();
|
||||
fan4Chart.update();
|
||||
minerDataChart.update();
|
||||
var miner_hr = document.getElementById("minerHashrate")
|
||||
miner_hr.innerHTML = new_data["hashrate"].toFixed(2) + " TH/s"
|
||||
var miner_model = document.getElementById("minerModel")
|
||||
miner_model.innerHTML = new_data["model"]
|
||||
};
|
||||
};
|
||||
|
||||
var ctx = document.getElementById("line-chart").getContext("2d");
|
||||
var width = document.getElementById("line-chart").width;
|
||||
var chartGradient = ctx.createLinearGradient(0, 0, width, 0)
|
||||
chartGradient.addColorStop(0, '#D0368A');
|
||||
chartGradient.addColorStop(1, '#708AD4');
|
||||
|
||||
const chartAreaBorder = {
|
||||
id: 'chartAreaBorder',
|
||||
beforeDraw(chart, args, options) {
|
||||
const {ctx, chartArea: {left, top, width, height}} = chart;
|
||||
ctx.save();
|
||||
ctx.strokeStyle = options.borderColor;
|
||||
ctx.lineWidth = options.borderWidth;
|
||||
ctx.strokeRect(left, top, width, height);
|
||||
ctx.restore();
|
||||
}
|
||||
};
|
||||
|
||||
var minerDataChart = new Chart(document.getElementById("line-chart"), {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [
|
||||
],
|
||||
datasets: [{
|
||||
label: "Hashrate",
|
||||
borderColor: chartGradient,
|
||||
pointBorderColor: chartGradient,
|
||||
pointBackgroundColor: chartGradient,
|
||||
pointHoverBackgroundColor: chartGradient,
|
||||
pointHoverBorderColor: chartGradient,
|
||||
data: [
|
||||
],
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [chartAreaBorder],
|
||||
options: {
|
||||
animation: {
|
||||
easing: 'easeInSine',
|
||||
duration: 0
|
||||
},
|
||||
plugins: {
|
||||
chartAreaBorder: {
|
||||
borderColor: chartGradient,
|
||||
borderWidth: 1
|
||||
},
|
||||
legend: {
|
||||
labels: {
|
||||
color: chartGradient
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(data) {
|
||||
return data.dataset.data[data.dataIndex] + " TH/s";
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
min: 0, // minimum value
|
||||
suggestedMax: 10,
|
||||
stepSize: 1,
|
||||
ticks: {
|
||||
callback: function(value, index, ticks) {
|
||||
return value + " TH/s";
|
||||
}
|
||||
}
|
||||
},
|
||||
x: {
|
||||
ticks: {
|
||||
maxTicksLimit: 6,
|
||||
maxRotation: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var options_fans = {
|
||||
animation: {
|
||||
easing: 'easeInSine',
|
||||
duration: 250,
|
||||
},
|
||||
aspectRatio: 1.5,
|
||||
events: [],
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var fanCtx = document.getElementById("fan-chart-1").getContext("2d");
|
||||
var fanWidth = document.getElementById("fan-chart-1").width;
|
||||
var fanChartGradient = fanCtx.createLinearGradient(0, 0, fanWidth, -fanWidth)
|
||||
fanChartGradient.addColorStop(0, '#D0368A');
|
||||
fanChartGradient.addColorStop(1, '#708AD4');
|
||||
|
||||
|
||||
var fan1Chart = new Chart(document.getElementById("fan-chart-1"), {
|
||||
type: "doughnut",
|
||||
data: {
|
||||
labels: ["Fan 1"],
|
||||
datasets: [
|
||||
{
|
||||
data: [0, 6000],
|
||||
// add colors
|
||||
backgroundColor: [
|
||||
fanChartGradient,
|
||||
"rgba(199, 199, 199, 1)"
|
||||
]
|
||||
},
|
||||
]
|
||||
},
|
||||
options: options_fans
|
||||
});
|
||||
var fan2Chart = new Chart(document.getElementById("fan-chart-2"), {
|
||||
type: "doughnut",
|
||||
data: {
|
||||
labels: ["Fan 2"],
|
||||
datasets: [
|
||||
{
|
||||
data: [0, 6000],
|
||||
// add colors
|
||||
backgroundColor: [
|
||||
fanChartGradient,
|
||||
"rgba(199, 199, 199, 1)"
|
||||
]
|
||||
},
|
||||
]
|
||||
},
|
||||
options: options_fans
|
||||
});
|
||||
var fan3Chart = new Chart(document.getElementById("fan-chart-3"), {
|
||||
type: "doughnut",
|
||||
data: {
|
||||
labels: ["Fan 3"],
|
||||
datasets: [
|
||||
{
|
||||
data: [0, 6000],
|
||||
// add colors
|
||||
backgroundColor: [
|
||||
fanChartGradient,
|
||||
"rgba(199, 199, 199, 1)"
|
||||
]
|
||||
},
|
||||
]
|
||||
},
|
||||
options: options_fans
|
||||
});
|
||||
var fan4Chart = new Chart(document.getElementById("fan-chart-4"), {
|
||||
type: "doughnut",
|
||||
data: {
|
||||
labels: ["Fan 4"],
|
||||
datasets: [
|
||||
{
|
||||
data: [0, 6000],
|
||||
// add colors
|
||||
backgroundColor: [
|
||||
fanChartGradient,
|
||||
"rgba(199, 199, 199, 1)"
|
||||
]
|
||||
},
|
||||
]
|
||||
},
|
||||
options: options_fans
|
||||
});
|
||||
|
||||
</script>
|
||||
{% endblock content %}
|
||||
106
tools/web_monitor/templates/navbar.html
Normal file
106
tools/web_monitor/templates/navbar.html
Normal file
@@ -0,0 +1,106 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
|
||||
<link href="{{ url_for('static', path='/navbar.css')}}" rel="stylesheet">
|
||||
<meta charset="UTF-8">
|
||||
<title>Title</title>
|
||||
</head>
|
||||
<body>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
|
||||
<symbol id="dashboard" viewBox="0 0 16 16">
|
||||
<path d="M8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4.5a.5.5 0 0 0 .5-.5v-4h2v4a.5.5 0 0 0 .5.5H14a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146zM2.5 14V7.707l5.5-5.5 5.5 5.5V14H10v-4a.5.5 0 0 0-.5-.5h-3a.5.5 0 0 0-.5.5v4H2.5z"></path>
|
||||
</symbol>
|
||||
<symbol id="miners" viewBox="0 0 16 16">
|
||||
<path d="M8 4a.5.5 0 0 1 .5.5V6a.5.5 0 0 1-1 0V4.5A.5.5 0 0 1 8 4zM3.732 5.732a.5.5 0 0 1 .707 0l.915.914a.5.5 0 1 1-.708.708l-.914-.915a.5.5 0 0 1 0-.707zM2 10a.5.5 0 0 1 .5-.5h1.586a.5.5 0 0 1 0 1H2.5A.5.5 0 0 1 2 10zm9.5 0a.5.5 0 0 1 .5-.5h1.5a.5.5 0 0 1 0 1H12a.5.5 0 0 1-.5-.5zm.754-4.246a.389.389 0 0 0-.527-.02L7.547 9.31a.91.91 0 1 0 1.302 1.258l3.434-4.297a.389.389 0 0 0-.029-.518z"></path>
|
||||
<path fill-rule="evenodd" d="M0 10a8 8 0 1 1 15.547 2.661c-.442 1.253-1.845 1.602-2.932 1.25C11.309 13.488 9.475 13 8 13c-1.474 0-3.31.488-4.615.911-1.087.352-2.49.003-2.932-1.25A7.988 7.988 0 0 1 0 10zm8-7a7 7 0 0 0-6.603 9.329c.203.575.923.876 1.68.63C4.397 12.533 6.358 12 8 12s3.604.532 4.923.96c.757.245 1.477-.056 1.68-.631A7 7 0 0 0 8 3z"></path>
|
||||
</symbol>
|
||||
<symbol id="settings" viewBox="0 0 16 16">
|
||||
<path d="M8 4.754a3.246 3.246 0 1 0 0 6.492 3.246 3.246 0 0 0 0-6.492zM5.754 8a2.246 2.246 0 1 1 4.492 0 2.246 2.246 0 0 1-4.492 0z"/>
|
||||
<path d="M9.796 1.343c-.527-1.79-3.065-1.79-3.592 0l-.094.319a.873.873 0 0 1-1.255.52l-.292-.16c-1.64-.892-3.433.902-2.54 2.541l.159.292a.873.873 0 0 1-.52 1.255l-.319.094c-1.79.527-1.79 3.065 0 3.592l.319.094a.873.873 0 0 1 .52 1.255l-.16.292c-.892 1.64.901 3.434 2.541 2.54l.292-.159a.873.873 0 0 1 1.255.52l.094.319c.527 1.79 3.065 1.79 3.592 0l.094-.319a.873.873 0 0 1 1.255-.52l.292.16c1.64.893 3.434-.902 2.54-2.541l-.159-.292a.873.873 0 0 1 .52-1.255l.319-.094c1.79-.527 1.79-3.065 0-3.592l-.319-.094a.873.873 0 0 1-.52-1.255l.16-.292c.893-1.64-.902-3.433-2.541-2.54l-.292.159a.873.873 0 0 1-1.255-.52l-.094-.319zm-2.633.283c.246-.835 1.428-.835 1.674 0l.094.319a1.873 1.873 0 0 0 2.693 1.115l.291-.16c.764-.415 1.6.42 1.184 1.185l-.159.292a1.873 1.873 0 0 0 1.116 2.692l.318.094c.835.246.835 1.428 0 1.674l-.319.094a1.873 1.873 0 0 0-1.115 2.693l.16.291c.415.764-.42 1.6-1.185 1.184l-.291-.159a1.873 1.873 0 0 0-2.693 1.116l-.094.318c-.246.835-1.428.835-1.674 0l-.094-.319a1.873 1.873 0 0 0-2.692-1.115l-.292.16c-.764.415-1.6-.42-1.184-1.185l.159-.291A1.873 1.873 0 0 0 1.945 8.93l-.319-.094c-.835-.246-.835-1.428 0-1.674l.319-.094A1.873 1.873 0 0 0 3.06 4.377l-.16-.292c-.415-.764.42-1.6 1.185-1.184l.292.159a1.873 1.873 0 0 0 2.692-1.115l.094-.319z"/>
|
||||
</symbol>
|
||||
<symbol id="scan" viewBox="0 0 16 16">
|
||||
<path d="M14 1a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h12zM2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2z"/>
|
||||
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"/>
|
||||
</symbol>
|
||||
<symbol id="miner" viewBox="0 0 16 16">
|
||||
<path d="M11.5 2a.5.5 0 0 1 .5.5v7a.5.5 0 0 1-1 0v-7a.5.5 0 0 1 .5-.5Zm2 0a.5.5 0 0 1 .5.5v7a.5.5 0 0 1-1 0v-7a.5.5 0 0 1 .5-.5Zm-10 8a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1h-6Zm0 2a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1h-6ZM5 3a1 1 0 0 0-1 1h-.5a.5.5 0 0 0 0 1H4v1h-.5a.5.5 0 0 0 0 1H4a1 1 0 0 0 1 1v.5a.5.5 0 0 0 1 0V8h1v.5a.5.5 0 0 0 1 0V8a1 1 0 0 0 1-1h.5a.5.5 0 0 0 0-1H9V5h.5a.5.5 0 0 0 0-1H9a1 1 0 0 0-1-1v-.5a.5.5 0 0 0-1 0V3H6v-.5a.5.5 0 0 0-1 0V3Zm0 1h3v3H5V4Zm6.5 7a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h2a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-2Z"/>
|
||||
<path d="M1 2a2 2 0 0 1 2-2h11a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2v-2H.5a.5.5 0 0 1-.5-.5v-1A.5.5 0 0 1 .5 9H1V8H.5a.5.5 0 0 1-.5-.5v-1A.5.5 0 0 1 .5 6H1V5H.5a.5.5 0 0 1-.5-.5v-2A.5.5 0 0 1 .5 2H1Zm1 11a1 1 0 0 0 1 1h11a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v11Z"/>
|
||||
</symbol>
|
||||
</svg>
|
||||
|
||||
|
||||
<header class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0 shadow">
|
||||
<a class="d-md-none col-md-3 col-lg-2 me-0 px-3" style="height: 50px;" href="#"></a>
|
||||
<button class="navbar-toggler position-absolute d-md-none collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#sidebarMenu" aria-controls="sidebarMenu" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<ul class="navbar-nav px-3">
|
||||
<li class="nav-item text-nowrap">
|
||||
</li>
|
||||
</ul>
|
||||
</header>
|
||||
|
||||
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<nav id="sidebarMenu" class="text-white bg-dark col-md-3 col-lg-2 d-md-block sidebar collapse">
|
||||
<div class="position-sticky pt-3">
|
||||
<ul class="nav nav-pills flex-column">
|
||||
<li class="nav-item mb-1 mx-2">
|
||||
<a href="{{url_for('dashboard')}}" class="nav-link {% if request.path == '/dashboard' %}active{% else %}text-white{% endif %}">
|
||||
<svg class="bi me-2" width="16" height="16"><use xlink:href="#dashboard"></use></svg>
|
||||
Dashboard
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item mb-1 mx-2">
|
||||
<a href="" class="nav-link {% if request.path == '/scan' or request.path.split('/')[1] == 'miner' %}active{% else %}text-white{% endif %}" data-bs-toggle="collapse" data-bs-target="#miners-collapse" aria-expanded="false">
|
||||
<svg class="bi me-2" width="16" height="16"><use xlink:href="#miners"></use></svg>
|
||||
Miners
|
||||
</a>
|
||||
<div class="collapse mt-1" id="miners-collapse" style="">
|
||||
<ul id="navMiners" class="btn-toggle-nav overflow-auto list-unstyled fw-normal pb-1 small">
|
||||
<li>
|
||||
<a href="{{url_for('scan')}}" class="nav-link {% if request.path == '/scan' %}active{% else %}text-white{% endif %}">
|
||||
<svg class="bi me-2 mt-1" width="16" height="16"><use xlink:href="#scan"></use></svg>
|
||||
Add Miners
|
||||
</a>
|
||||
</li>
|
||||
{% for miner in cur_miners %}
|
||||
<li>
|
||||
<a href="{{url_for('miner')}}/{{miner}}" class="nav-link {% if request.path == '/miner/' + miner %}active{% else %}text-white{% endif %}">
|
||||
<svg class="bi me-2 mt-1" width="16" height="16"><use xlink:href="#miner"></use></svg>
|
||||
{{miner}}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
|
||||
<li class="border-top my-3"></li>
|
||||
|
||||
<li class="nav-item mb-1 mx-2">
|
||||
<a href="/settings" class="nav-link {% if request.path == '/settings' %}active{% else %}text-white{% endif %}">
|
||||
<svg class="bi me-2" width="16" height="16"><use xlink:href="#settings"></use></svg>
|
||||
Settings
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
<div class="col-md-9 ms-sm-auto col-lg-10 px-md-4 ps-4">
|
||||
{% block content %}
|
||||
|
||||
{% endblock content %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
158
tools/web_monitor/templates/scan.html
Normal file
158
tools/web_monitor/templates/scan.html
Normal file
@@ -0,0 +1,158 @@
|
||||
{% extends 'navbar.html'%}
|
||||
{% block content %}
|
||||
<div class="row w-100 my-4">
|
||||
<form action="" onsubmit="sendMessage(event)">
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text" id="scan-ip">Subnet</span>
|
||||
<input type="text" class="form-control" id="messageText" placeholder="192.168.1.0/24" aria-describedby="scan-ip">
|
||||
<button class="btn btn-danger" onclick="cancelScan()" style="display:none;" type="button" id="cancelButton">Cancel</button>
|
||||
<button class="btn btn-primary" onclick="scanMiners()" type="button" id="scanButton">Scan</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="row w-100">
|
||||
<button class="btn btn-primary mb-4 mx-1" onclick="addMiners()" type="button" id="addButton">Add Selected Miners</button>
|
||||
</div>
|
||||
|
||||
<div class="row w-100">
|
||||
<table class="table table-striped table-responsive" style="max-height:300px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="active col-1">
|
||||
<input type="checkbox" class="select-all checkbox" name="select-all" id="selectAllCheckbox"/>
|
||||
</th>
|
||||
<th>IP</th>
|
||||
<th>Model</th>
|
||||
<th id="scanStatus" class="col-2">0 Miners</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="minerTable">
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
|
||||
<script>
|
||||
$(function(){
|
||||
//column checkbox select all or cancel
|
||||
$("input.select-all").click(function () {
|
||||
var checked = this.checked;
|
||||
$("input.select-item").each(function (index,item) {
|
||||
item.checked = checked;
|
||||
});
|
||||
});
|
||||
//check selected items
|
||||
$("input.select-item").click(function () {
|
||||
var checked = this.checked;
|
||||
var all = $("input.select-all")[0];
|
||||
var total = $("input.select-item").length;
|
||||
var len = $("input.select-item:checked:checked").length;
|
||||
all.checked = len===total;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
window.post = function(url, data) {
|
||||
return fetch(url, {method: "POST", headers: {'Content-Type': 'application/json'}, body: JSON.stringify(data)});
|
||||
}
|
||||
var ws = new WebSocket("ws://localhost:80/scan/ws");
|
||||
ws.onmessage = function(event) {
|
||||
if (event.data == "Done") {
|
||||
document.getElementById("scanButton").innerHTML = "Scan"
|
||||
document.getElementById("scanButton").disabled = false
|
||||
document.getElementById("selectAllCheckbox").disabled = false
|
||||
document.getElementById("scanStatus").innerHTML = document.getElementById('minerTable').rows.length + " Miners"
|
||||
document.getElementById("cancelButton").style = "display:none;"
|
||||
enableCheckboxes();
|
||||
} else if (event.data == "Cancelled") {
|
||||
document.getElementById("scanButton").innerHTML = "Scan"
|
||||
document.getElementById("scanButton").disabled = false
|
||||
document.getElementById("selectAllCheckbox").disabled = false
|
||||
document.getElementById("scanStatus").innerHTML = document.getElementById('minerTable').rows.length + " Miners"
|
||||
document.getElementById("cancelButton").style = "display:none;"
|
||||
enableCheckboxes();
|
||||
} else {
|
||||
var miner_data = JSON.parse(event.data)
|
||||
var miners = document.getElementById('minerTable')
|
||||
miners.innerHTML = ""
|
||||
miner_data.forEach(function(miner) {
|
||||
var tr = document.createElement('tr')
|
||||
tr.id = miner["ip"]
|
||||
|
||||
var checkbox_td = document.createElement('td')
|
||||
checkbox_td.innerHTML = '<input type="checkbox" class="select-item checkbox" name="minerCheckboxes" value="' + miner["ip"] + '" />'
|
||||
checkbox_td.className = "active"
|
||||
|
||||
var ip_td = document.createElement('td')
|
||||
ip_td.innerHTML = miner["ip"]
|
||||
|
||||
var model_td = document.createElement('td')
|
||||
model_td.innerHTML = miner["model"]
|
||||
|
||||
var empty_td = document.createElement('td')
|
||||
|
||||
tr.append(checkbox_td)
|
||||
tr.append(ip_td)
|
||||
tr.append(model_td)
|
||||
tr.append(empty_td)
|
||||
|
||||
miners.append(tr)
|
||||
});
|
||||
disableCheckboxes();
|
||||
};
|
||||
};
|
||||
function scanMiners(event) {
|
||||
var input = document.getElementById("messageText")
|
||||
var miners = document.getElementById('minerTable')
|
||||
miners.innerHTML = ""
|
||||
document.getElementById("scanStatus").innerHTML = "<span class='spinner-border spinner-border-sm'></span> Scanning"
|
||||
document.getElementById("scanButton").innerHTML = "<span class='spinner-border spinner-border-sm'></span> Scanning"
|
||||
document.getElementById("scanButton").disabled = true
|
||||
document.getElementById("selectAllCheckbox").disabled = true
|
||||
document.getElementById("cancelButton").style = ""
|
||||
if (input.value != "") {
|
||||
ws.send(input.value)
|
||||
event.preventDefault()
|
||||
} else {
|
||||
ws.send("192.168.1.0/24")
|
||||
};
|
||||
};
|
||||
function cancelScan(event) {
|
||||
document.getElementById("scanStatus").innerHTML = "Canceling..."
|
||||
document.getElementById("scanButton").innerHTML = "Canceling..."
|
||||
document.getElementById("cancelButton").style = "display:none;"
|
||||
ws.send("-Cancel-")
|
||||
};
|
||||
function addMiners(event) {
|
||||
var checkedBoxes = document.querySelectorAll('input[name=minerCheckboxes]:checked');
|
||||
if (checkedBoxes.length != 0) {
|
||||
var minerList = [];
|
||||
for (i = 0; i< checkedBoxes.length; i++) {
|
||||
minerList.push(checkedBoxes[i].defaultValue);
|
||||
}
|
||||
post("{{url_for('add_miners_scan')}}", {miners: minerList})
|
||||
for (i = 0; i< minerList.length; i++) {
|
||||
var tr_to_remove = document.getElementById(minerList[i])
|
||||
tr_to_remove.remove()
|
||||
var navbar_miners = document.getElementById("navMiners")
|
||||
navbar_miners.innerHTML += '<li><a href="/miner/' + minerList[i] + '" class="nav-link text-white"><svg class="bi me-2 mt-1" width="16" height="16"><use xlink:href="#miner"></use></svg>' + minerList[i] + '</a></li>'
|
||||
}
|
||||
document.getElementById("scanStatus").innerHTML = document.getElementById('minerTable').rows.length + " Miners"
|
||||
};
|
||||
};
|
||||
function disableCheckboxes() {
|
||||
var checkBoxes = document.querySelectorAll('input[name=minerCheckboxes]');
|
||||
for (i = 0; i< checkBoxes.length; i++) {
|
||||
checkBoxes[i].disabled = true
|
||||
};
|
||||
};
|
||||
function enableCheckboxes() {
|
||||
var checkBoxes = document.querySelectorAll('input[name=minerCheckboxes]');
|
||||
for (i = 0; i< checkBoxes.length; i++) {
|
||||
checkBoxes[i].disabled = false
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
{% endblock content %}
|
||||
46
tools/web_monitor/templates/settings.html
Normal file
46
tools/web_monitor/templates/settings.html
Normal file
@@ -0,0 +1,46 @@
|
||||
{% extends 'navbar.html'%}
|
||||
{% block content %}
|
||||
<div class="row my-2">
|
||||
<div class="col">
|
||||
<div class="d-flex flex-row-reverse">
|
||||
<button type="button" class="btn btn-outline-danger mx-1" data-bs-toggle="modal" data-bs-target="#removeModal">
|
||||
Remove All Miners
|
||||
</button>
|
||||
|
||||
<!-- Modal -->
|
||||
<div class="modal fade" id="removeModal" tabindex="-1" aria-labelledby="removeModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="removeModalLabel">Remove Miner</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
Do you really want to remove all miners?
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<a class="btn btn-danger" href="{{url_for('remove_all_miners')}}" role="button">Remove</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<form method="post" action="/settings/update">
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text">Graph Data Sleep Time</span>
|
||||
<input type="number" class="form-control" value="{{settings['graph_data_sleep_time']}}" name="graph_data_sleep_time" id="graph_data_sleep_time">
|
||||
</div>
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text">Miner Data Timeout</span>
|
||||
<input type="number" class="form-control" value="{{settings['miner_data_timeout']}}" name="miner_data_timeout" id="miner_data_timeout">
|
||||
</div>
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text">Miner Identification Timeout</span>
|
||||
<input type="number" class="form-control" value="{{settings['miner_identify_timeout']}}" name="miner_identify_timeout" id=" ">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100">Submit</button>
|
||||
</form>
|
||||
{% endblock content %}
|
||||
20
tools/web_monitor/web_settings.py
Normal file
20
tools/web_monitor/web_settings.py
Normal file
@@ -0,0 +1,20 @@
|
||||
import toml
|
||||
import os
|
||||
|
||||
|
||||
def get_current_settings():
|
||||
try:
|
||||
with open(os.path.join(os.getcwd(), "web_settings.toml"), "r") as settings_file:
|
||||
settings = toml.loads(settings_file.read())
|
||||
except:
|
||||
settings = {
|
||||
"graph_data_sleep_time": 1,
|
||||
"miner_data_timeout": 5,
|
||||
"miner_identify_timeout": 5,
|
||||
}
|
||||
return settings
|
||||
|
||||
|
||||
def update_settings(settings):
|
||||
with open(os.path.join(os.getcwd(), "web_settings.toml"), "w") as settings_file:
|
||||
settings_file.write(toml.dumps(settings))
|
||||
3
tools/web_monitor/web_settings.toml
Normal file
3
tools/web_monitor/web_settings.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
graph_data_sleep_time = 1
|
||||
miner_data_timeout = 5
|
||||
miner_identify_timeout = 5
|
||||
Reference in New Issue
Block a user