mirror of
https://github.com/craigerl/aprsd.git
synced 2024-09-28 08:06:37 -04:00
Merge pull request #64 from craigerl/web_tabs
Add admin UI tabs for charts, messages, config
This commit is contained in:
commit
ccaab72124
@ -5,7 +5,7 @@ from logging import NullHandler
|
|||||||
from logging.handlers import RotatingFileHandler
|
from logging.handlers import RotatingFileHandler
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from aprsd import messaging, plugin, stats, utils
|
from aprsd import messaging, packets, plugin, stats, utils
|
||||||
import flask
|
import flask
|
||||||
import flask_classful
|
import flask_classful
|
||||||
from flask_httpauth import HTTPBasicAuth
|
from flask_httpauth import HTTPBasicAuth
|
||||||
@ -49,6 +49,7 @@ class APRSDFlask(flask_classful.FlaskView):
|
|||||||
"index.html",
|
"index.html",
|
||||||
initial_stats=stats,
|
initial_stats=stats,
|
||||||
callsign=self.config["aprs"]["login"],
|
callsign=self.config["aprs"]["login"],
|
||||||
|
config_json=json.dumps(self.config),
|
||||||
)
|
)
|
||||||
|
|
||||||
@auth.login_required
|
@auth.login_required
|
||||||
@ -61,6 +62,11 @@ class APRSDFlask(flask_classful.FlaskView):
|
|||||||
|
|
||||||
return flask.render_template("messages.html", messages=json.dumps(msgs))
|
return flask.render_template("messages.html", messages=json.dumps(msgs))
|
||||||
|
|
||||||
|
@auth.login_required
|
||||||
|
def packets(self):
|
||||||
|
packet_list = packets.PacketList().packet_list
|
||||||
|
return json.dumps(packet_list)
|
||||||
|
|
||||||
@auth.login_required
|
@auth.login_required
|
||||||
def plugins(self):
|
def plugins(self):
|
||||||
pm = plugin.PluginManager()
|
pm = plugin.PluginManager()
|
||||||
@ -142,6 +148,7 @@ def init_flask(config, loglevel, quiet):
|
|||||||
flask_app.route("/", methods=["GET"])(server.index)
|
flask_app.route("/", methods=["GET"])(server.index)
|
||||||
flask_app.route("/stats", methods=["GET"])(server.stats)
|
flask_app.route("/stats", methods=["GET"])(server.stats)
|
||||||
flask_app.route("/messages", methods=["GET"])(server.messages)
|
flask_app.route("/messages", methods=["GET"])(server.messages)
|
||||||
|
flask_app.route("/packets", methods=["GET"])(server.packets)
|
||||||
flask_app.route("/save", methods=["GET"])(server.save)
|
flask_app.route("/save", methods=["GET"])(server.save)
|
||||||
flask_app.route("/plugins", methods=["GET"])(server.plugins)
|
flask_app.route("/plugins", methods=["GET"])(server.plugins)
|
||||||
return flask_app
|
return flask_app
|
||||||
|
@ -223,7 +223,7 @@ class Message(metaclass=abc.ABCMeta):
|
|||||||
id = 0
|
id = 0
|
||||||
|
|
||||||
retry_count = 3
|
retry_count = 3
|
||||||
last_send_time = None
|
last_send_time = 0
|
||||||
last_send_attempt = 0
|
last_send_attempt = 0
|
||||||
|
|
||||||
def __init__(self, fromcall, tocall, msg_id=None):
|
def __init__(self, fromcall, tocall, msg_id=None):
|
||||||
@ -257,6 +257,9 @@ class RawMessage(Message):
|
|||||||
|
|
||||||
def dict(self):
|
def dict(self):
|
||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now()
|
||||||
|
last_send_age = None
|
||||||
|
if self.last_send_time:
|
||||||
|
last_send_age = str(now - self.last_send_time)
|
||||||
return {
|
return {
|
||||||
"type": "raw",
|
"type": "raw",
|
||||||
"message": self.message.rstrip("\n"),
|
"message": self.message.rstrip("\n"),
|
||||||
@ -264,7 +267,7 @@ class RawMessage(Message):
|
|||||||
"retry_count": self.retry_count,
|
"retry_count": self.retry_count,
|
||||||
"last_send_attempt": self.last_send_attempt,
|
"last_send_attempt": self.last_send_attempt,
|
||||||
"last_send_time": str(self.last_send_time),
|
"last_send_time": str(self.last_send_time),
|
||||||
"last_send_age": str(now - self.last_send_time),
|
"last_send_age": last_send_age,
|
||||||
}
|
}
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@ -304,6 +307,11 @@ class TextMessage(Message):
|
|||||||
|
|
||||||
def dict(self):
|
def dict(self):
|
||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now()
|
||||||
|
|
||||||
|
last_send_age = None
|
||||||
|
if self.last_send_time:
|
||||||
|
last_send_age = str(now - self.last_send_time)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"id": self.id,
|
"id": self.id,
|
||||||
"type": "text-message",
|
"type": "text-message",
|
||||||
@ -314,7 +322,7 @@ class TextMessage(Message):
|
|||||||
"retry_count": self.retry_count,
|
"retry_count": self.retry_count,
|
||||||
"last_send_attempt": self.last_send_attempt,
|
"last_send_attempt": self.last_send_attempt,
|
||||||
"last_send_time": str(self.last_send_time),
|
"last_send_time": str(self.last_send_time),
|
||||||
"last_send_age": str(now - self.last_send_time),
|
"last_send_age": last_send_age,
|
||||||
}
|
}
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@ -431,6 +439,9 @@ class AckMessage(Message):
|
|||||||
|
|
||||||
def dict(self):
|
def dict(self):
|
||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now()
|
||||||
|
last_send_age = None
|
||||||
|
if self.last_send_time:
|
||||||
|
last_send_age = str(now - self.last_send_time)
|
||||||
return {
|
return {
|
||||||
"id": self.id,
|
"id": self.id,
|
||||||
"type": "ack",
|
"type": "ack",
|
||||||
@ -440,7 +451,7 @@ class AckMessage(Message):
|
|||||||
"retry_count": self.retry_count,
|
"retry_count": self.retry_count,
|
||||||
"last_send_attempt": self.last_send_attempt,
|
"last_send_attempt": self.last_send_attempt,
|
||||||
"last_send_time": str(self.last_send_time),
|
"last_send_time": str(self.last_send_time),
|
||||||
"last_send_age": str(now - self.last_send_time),
|
"last_send_age": last_send_age,
|
||||||
}
|
}
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
30
aprsd/packets.py
Normal file
30
aprsd/packets.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import logging
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
|
||||||
|
class PacketList:
|
||||||
|
"""Class to track all of the packets rx'd and tx'd by aprsd."""
|
||||||
|
|
||||||
|
_instance = None
|
||||||
|
|
||||||
|
packet_list = {}
|
||||||
|
|
||||||
|
def __new__(cls, *args, **kwargs):
|
||||||
|
if cls._instance is None:
|
||||||
|
cls._instance = super().__new__(cls)
|
||||||
|
cls._instance.packet_list = {}
|
||||||
|
cls._instance.lock = threading.Lock()
|
||||||
|
return cls._instance
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
with self.lock:
|
||||||
|
return iter(self.packet_list)
|
||||||
|
|
||||||
|
def add(self, packet):
|
||||||
|
with self.lock:
|
||||||
|
now = time.time()
|
||||||
|
ts = str(now).split(".")[0]
|
||||||
|
self.packet_list[ts] = packet
|
@ -6,7 +6,7 @@ import threading
|
|||||||
import time
|
import time
|
||||||
import tracemalloc
|
import tracemalloc
|
||||||
|
|
||||||
from aprsd import client, messaging, plugin, stats, trace, utils
|
from aprsd import client, messaging, packets, plugin, stats, trace, utils
|
||||||
import aprslib
|
import aprslib
|
||||||
|
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
@ -77,6 +77,7 @@ class KeepAliveThread(APRSDThread):
|
|||||||
if self.cntr % 6 == 0:
|
if self.cntr % 6 == 0:
|
||||||
tracker = messaging.MsgTrack()
|
tracker = messaging.MsgTrack()
|
||||||
stats_obj = stats.APRSDStats()
|
stats_obj = stats.APRSDStats()
|
||||||
|
packets_list = packets.PacketList().packet_list
|
||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now()
|
||||||
last_email = stats_obj.email_thread_time
|
last_email = stats_obj.email_thread_time
|
||||||
if last_email:
|
if last_email:
|
||||||
@ -89,18 +90,16 @@ class KeepAliveThread(APRSDThread):
|
|||||||
current, peak = tracemalloc.get_traced_memory()
|
current, peak = tracemalloc.get_traced_memory()
|
||||||
stats_obj.set_memory(current)
|
stats_obj.set_memory(current)
|
||||||
stats_obj.set_memory_peak(peak)
|
stats_obj.set_memory_peak(peak)
|
||||||
keepalive = (
|
keepalive = "Uptime {} Tracker {} " "Msgs TX:{} RX:{} Last:{} Email:{} Packets:{} RAM Current:{} Peak:{}".format(
|
||||||
"Uptime {} Tracker {} "
|
utils.strfdelta(stats_obj.uptime),
|
||||||
"Msgs TX:{} RX:{} Last:{} Email:{} RAM Current:{} Peak:{}".format(
|
len(tracker),
|
||||||
utils.strfdelta(stats_obj.uptime),
|
stats_obj.msgs_tx,
|
||||||
len(tracker),
|
stats_obj.msgs_rx,
|
||||||
stats_obj.msgs_tx,
|
last_msg_time,
|
||||||
stats_obj.msgs_rx,
|
email_thread_time,
|
||||||
last_msg_time,
|
len(packets_list),
|
||||||
email_thread_time,
|
utils.human_size(current),
|
||||||
utils.human_size(current),
|
utils.human_size(peak),
|
||||||
utils.human_size(peak),
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
LOG.debug(keepalive)
|
LOG.debug(keepalive)
|
||||||
# Check version every hour
|
# Check version every hour
|
||||||
@ -244,6 +243,7 @@ class APRSDRXThread(APRSDThread):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
stats.APRSDStats().msgs_rx_inc()
|
stats.APRSDStats().msgs_rx_inc()
|
||||||
|
packets.PacketList().add(packet)
|
||||||
|
|
||||||
msg = packet.get("message_text", None)
|
msg = packet.get("message_text", None)
|
||||||
msg_format = packet.get("format", None)
|
msg_format = packet.get("format", None)
|
||||||
@ -275,6 +275,7 @@ class APRSDTXThread(APRSDThread):
|
|||||||
def loop(self):
|
def loop(self):
|
||||||
try:
|
try:
|
||||||
msg = self.msg_queues["tx"].get(timeout=0.1)
|
msg = self.msg_queues["tx"].get(timeout=0.1)
|
||||||
|
packets.PacketList().add(msg.dict())
|
||||||
msg.send()
|
msg.send()
|
||||||
except queue.Empty:
|
except queue.Empty:
|
||||||
pass
|
pass
|
||||||
|
85
aprsd/web/static/css/index.css
Normal file
85
aprsd/web/static/css/index.css
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
body {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto 1fr auto;
|
||||||
|
background: #eeeeee;
|
||||||
|
margin: 2em;
|
||||||
|
padding: 0;
|
||||||
|
text-align: center;
|
||||||
|
font-family: system-ui, sans-serif;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
padding: 2em;
|
||||||
|
height: 10vh;
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#main {
|
||||||
|
padding: 2em;
|
||||||
|
height: 80vh;
|
||||||
|
}
|
||||||
|
footer {
|
||||||
|
padding: 2em;
|
||||||
|
text-align: center;
|
||||||
|
height: 10vh;
|
||||||
|
}
|
||||||
|
#graphs {
|
||||||
|
display: grid;
|
||||||
|
width: 100%;
|
||||||
|
height: 300px;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
#graphs_center {
|
||||||
|
display: block;
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
width: 100%;
|
||||||
|
height: 300px;
|
||||||
|
}
|
||||||
|
#left {
|
||||||
|
margin-right: 2px;
|
||||||
|
height: 300px;
|
||||||
|
}
|
||||||
|
#right {
|
||||||
|
height: 300px;
|
||||||
|
}
|
||||||
|
#center {
|
||||||
|
height: 300px;
|
||||||
|
}
|
||||||
|
#messageChart, #emailChart, #memChart {
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
background: #ddd;
|
||||||
|
}
|
||||||
|
#stats {
|
||||||
|
margin: auto;
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
#jsonstats {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
#title {
|
||||||
|
font-size: 4em;
|
||||||
|
}
|
||||||
|
#version{
|
||||||
|
font-size: .5em;
|
||||||
|
}
|
||||||
|
#uptime, #aprsis {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
#callsign {
|
||||||
|
font-size: 1.4em;
|
||||||
|
color: #00F;
|
||||||
|
padding-top: 8px;
|
||||||
|
margin:10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#title_rx {
|
||||||
|
background-color: darkseagreen;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
#title_tx {
|
||||||
|
background-color: lightcoral;
|
||||||
|
text-align: left;
|
||||||
|
}
|
35
aprsd/web/static/css/tabs.css
Normal file
35
aprsd/web/static/css/tabs.css
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
/* Style the tab */
|
||||||
|
.tab {
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
background-color: #f1f1f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style the buttons that are used to open the tab content */
|
||||||
|
.tab button {
|
||||||
|
background-color: inherit;
|
||||||
|
float: left;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 14px 16px;
|
||||||
|
transition: 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Change background color of buttons on hover */
|
||||||
|
.tab button:hover {
|
||||||
|
background-color: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Create an active/current tablink class */
|
||||||
|
.tab button.active {
|
||||||
|
background-color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style the tab content */
|
||||||
|
.tabcontent {
|
||||||
|
display: none;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-top: none;
|
||||||
|
}
|
259
aprsd/web/static/js/charts.js
Normal file
259
aprsd/web/static/js/charts.js
Normal file
@ -0,0 +1,259 @@
|
|||||||
|
var packet_list = {};
|
||||||
|
|
||||||
|
window.chartColors = {
|
||||||
|
red: 'rgb(255, 99, 132)',
|
||||||
|
orange: 'rgb(255, 159, 64)',
|
||||||
|
yellow: 'rgb(255, 205, 86)',
|
||||||
|
green: 'rgb(26, 181, 77)',
|
||||||
|
blue: 'rgb(54, 162, 235)',
|
||||||
|
purple: 'rgb(153, 102, 255)',
|
||||||
|
grey: 'rgb(201, 203, 207)',
|
||||||
|
black: 'rgb(0, 0, 0)'
|
||||||
|
};
|
||||||
|
|
||||||
|
function start_charts() {
|
||||||
|
Chart.scaleService.updateScaleDefaults('linear', {
|
||||||
|
ticks: {
|
||||||
|
min: 0
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
memory_chart = new Chart($("#memChart"), {
|
||||||
|
label: 'Memory Usage',
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: [],
|
||||||
|
datasets: [{
|
||||||
|
label: 'Peak Ram usage',
|
||||||
|
borderColor: window.chartColors.red,
|
||||||
|
data: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Current Ram usage',
|
||||||
|
borderColor: window.chartColors.blue,
|
||||||
|
data: [],
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Memory Usage',
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
type: 'timeseries',
|
||||||
|
offset: true,
|
||||||
|
ticks: {
|
||||||
|
major: { enabled: true },
|
||||||
|
fontStyle: context => context.tick.major ? 'bold' : undefined,
|
||||||
|
source: 'data',
|
||||||
|
maxRotation: 0,
|
||||||
|
autoSkip: true,
|
||||||
|
autoSkipPadding: 75,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
message_chart = new Chart($("#messageChart"), {
|
||||||
|
label: 'Messages',
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: [],
|
||||||
|
datasets: [{
|
||||||
|
label: 'Messages Sent',
|
||||||
|
borderColor: window.chartColors.green,
|
||||||
|
data: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Messages Recieved',
|
||||||
|
borderColor: window.chartColors.yellow,
|
||||||
|
data: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Ack Sent',
|
||||||
|
borderColor: window.chartColors.purple,
|
||||||
|
data: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Ack Recieved',
|
||||||
|
borderColor: window.chartColors.black,
|
||||||
|
data: [],
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'APRS Messages',
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
type: 'timeseries',
|
||||||
|
offset: true,
|
||||||
|
ticks: {
|
||||||
|
major: { enabled: true },
|
||||||
|
fontStyle: context => context.tick.major ? 'bold' : undefined,
|
||||||
|
source: 'data',
|
||||||
|
maxRotation: 0,
|
||||||
|
autoSkip: true,
|
||||||
|
autoSkipPadding: 75,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
email_chart = new Chart($("#emailChart"), {
|
||||||
|
label: 'Email Messages',
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: [],
|
||||||
|
datasets: [{
|
||||||
|
label: 'Sent',
|
||||||
|
borderColor: window.chartColors.green,
|
||||||
|
data: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Recieved',
|
||||||
|
borderColor: window.chartColors.yellow,
|
||||||
|
data: [],
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Email Messages',
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
type: 'timeseries',
|
||||||
|
offset: true,
|
||||||
|
ticks: {
|
||||||
|
major: { enabled: true },
|
||||||
|
fontStyle: context => context.tick.major ? 'bold' : undefined,
|
||||||
|
source: 'data',
|
||||||
|
maxRotation: 0,
|
||||||
|
autoSkip: true,
|
||||||
|
autoSkipPadding: 75,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function addData(chart, label, newdata) {
|
||||||
|
chart.data.labels.push(label);
|
||||||
|
chart.data.datasets.forEach((dataset) => {
|
||||||
|
dataset.data.push(newdata);
|
||||||
|
});
|
||||||
|
chart.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateDualData(chart, label, first, second) {
|
||||||
|
chart.data.labels.push(label);
|
||||||
|
chart.data.datasets[0].data.push(first);
|
||||||
|
chart.data.datasets[1].data.push(second);
|
||||||
|
chart.update();
|
||||||
|
}
|
||||||
|
function updateQuadData(chart, label, first, second, third, fourth) {
|
||||||
|
chart.data.labels.push(label);
|
||||||
|
chart.data.datasets[0].data.push(first);
|
||||||
|
chart.data.datasets[1].data.push(second);
|
||||||
|
chart.data.datasets[2].data.push(third);
|
||||||
|
chart.data.datasets[3].data.push(fourth);
|
||||||
|
chart.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
function update_stats( data ) {
|
||||||
|
$("#version").text( data["stats"]["aprsd"]["version"] );
|
||||||
|
$("#aprsis").text( "APRS-IS Server: " + data["stats"]["aprs-is"]["server"] );
|
||||||
|
$("#uptime").text( "uptime: " + data["stats"]["aprsd"]["uptime"] );
|
||||||
|
const html_pretty = Prism.highlight(JSON.stringify(data, null, '\t'), Prism.languages.json, 'json');
|
||||||
|
$("#jsonstats").html(html_pretty);
|
||||||
|
short_time = data["time"].split(/\s(.+)/)[1];
|
||||||
|
updateQuadData(message_chart, short_time, data["stats"]["messages"]["sent"], data["stats"]["messages"]["recieved"], data["stats"]["messages"]["ack_sent"], data["stats"]["messages"]["ack_recieved"]);
|
||||||
|
updateDualData(email_chart, short_time, data["stats"]["email"]["sent"], data["stats"]["email"]["recieved"]);
|
||||||
|
updateDualData(memory_chart, short_time, data["stats"]["aprsd"]["memory_peak"], data["stats"]["aprsd"]["memory_current"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function update_packets( data ) {
|
||||||
|
var packetsdiv = $("#packetsDiv");
|
||||||
|
//nuke the contents first, then add to it.
|
||||||
|
jQuery.each(data, function(i, val) {
|
||||||
|
if ( packet_list.hasOwnProperty(i) == false ) {
|
||||||
|
packet_list[i] = val;
|
||||||
|
var d = new Date(i*1000).toLocaleDateString("en-US")
|
||||||
|
var t = new Date(i*1000).toLocaleTimeString("en-US")
|
||||||
|
if (val.hasOwnProperty('from') == false) {
|
||||||
|
from = val['fromcall']
|
||||||
|
title_id = 'title_tx'
|
||||||
|
} else {
|
||||||
|
from = val['from']
|
||||||
|
title_id = 'title_rx'
|
||||||
|
}
|
||||||
|
var from_to = d + " " + t + " " + from + " > "
|
||||||
|
|
||||||
|
if (val.hasOwnProperty('addresse')) {
|
||||||
|
from_to = from_to + val['addresse']
|
||||||
|
} else if (val.hasOwnProperty('tocall')) {
|
||||||
|
from_to = from_to + val['tocall']
|
||||||
|
} else if (val.hasOwnProperty('format') && val['format'] == 'mic-e') {
|
||||||
|
from_to = from_to + "Mic-E"
|
||||||
|
}
|
||||||
|
|
||||||
|
from_to = from_to + " - " + val['raw']
|
||||||
|
|
||||||
|
json_pretty = Prism.highlight(JSON.stringify(val, null, '\t'), Prism.languages.json, 'json');
|
||||||
|
pkt_html = '<div class="title" id="' + title_id + '"><i class="dropdown icon"></i>' + from_to + '</div><div class="content"><p class="transition hidden"><pre class="language-json">' + json_pretty + '</p></p></div>'
|
||||||
|
packetsdiv.prepend(pkt_html);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$('.ui.accordion').accordion('refresh');
|
||||||
|
|
||||||
|
|
||||||
|
const html_pretty = Prism.highlight(JSON.stringify(data, null, '\t'), Prism.languages.json, 'json');
|
||||||
|
$("#packetsjson").html(html_pretty);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function start_update() {
|
||||||
|
|
||||||
|
(function statsworker() {
|
||||||
|
$.ajax({
|
||||||
|
url: "/stats",
|
||||||
|
type: 'GET',
|
||||||
|
dataType: 'json',
|
||||||
|
success: function(data) {
|
||||||
|
update_stats(data);
|
||||||
|
},
|
||||||
|
complete: function() {
|
||||||
|
setTimeout(statsworker, 10000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
|
(function packetsworker() {
|
||||||
|
$.ajax({
|
||||||
|
url: "/packets",
|
||||||
|
type: 'GET',
|
||||||
|
dataType: 'json',
|
||||||
|
success: function(data) {
|
||||||
|
update_packets(data);
|
||||||
|
},
|
||||||
|
complete: function() {
|
||||||
|
setTimeout(packetsworker, 10000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
}
|
28
aprsd/web/static/js/tabs.js
Normal file
28
aprsd/web/static/js/tabs.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
function openTab(evt, tabName) {
|
||||||
|
// Declare all variables
|
||||||
|
var i, tabcontent, tablinks;
|
||||||
|
|
||||||
|
if (typeof tabName == 'undefined') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all elements with class="tabcontent" and hide them
|
||||||
|
tabcontent = document.getElementsByClassName("tabcontent");
|
||||||
|
for (i = 0; i < tabcontent.length; i++) {
|
||||||
|
tabcontent[i].style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all elements with class="tablinks" and remove the class "active"
|
||||||
|
tablinks = document.getElementsByClassName("tablinks");
|
||||||
|
for (i = 0; i < tablinks.length; i++) {
|
||||||
|
tablinks[i].className = tablinks[i].className.replace(" active", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show the current tab, and add an "active" class to the button that opened the tab
|
||||||
|
document.getElementById(tabName).style.display = "block";
|
||||||
|
if (typeof evt.currentTarget == 'undefined') {
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
evt.currentTarget.className += " active";
|
||||||
|
}
|
||||||
|
}
|
@ -1,331 +1,95 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
|
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
|
||||||
<link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/smoothness/jquery-ui.css">
|
<link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/smoothness/jquery-ui.css">
|
||||||
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
|
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.23.0/prism.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.23.0/prism.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.23.0/components/prism-json.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.23.0/components/prism-json.js"></script>
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/prismjs@1.23.0/themes/prism-tomorrow.css">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/prismjs@1.23.0/themes/prism-tomorrow.css">
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@2.9.4/dist/Chart.bundle.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@2.9.4/dist/Chart.bundle.js"></script>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.css">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.js"></script>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/css/index.css">
|
||||||
|
<link rel="stylesheet" href="/css/tabs.css">
|
||||||
|
<script src="/js/charts.js"></script>
|
||||||
|
<script src="/js/tabs.js"></script>
|
||||||
|
|
||||||
|
|
||||||
<script type="text/javascript"">
|
<script type="text/javascript"">
|
||||||
|
var initial_stats = {{ initial_stats|tojson|safe }};
|
||||||
|
|
||||||
var initial_stats = {{ initial_stats|tojson|safe }};
|
var memory_chart = null
|
||||||
|
var message_chart = null
|
||||||
|
var color = Chart.helpers.color;
|
||||||
|
|
||||||
var memory_chart = null
|
$(document).ready(function() {
|
||||||
var message_chart = null
|
console.log(initial_stats);
|
||||||
var color = Chart.helpers.color;
|
start_update();
|
||||||
|
start_charts();
|
||||||
|
|
||||||
window.chartColors = {
|
$("#toggleStats").click(function() {
|
||||||
red: 'rgb(255, 99, 132)',
|
$("#jsonstats").fadeToggle(1000);
|
||||||
orange: 'rgb(255, 159, 64)',
|
});
|
||||||
yellow: 'rgb(255, 205, 86)',
|
|
||||||
green: 'rgb(26, 181, 77)',
|
|
||||||
blue: 'rgb(54, 162, 235)',
|
|
||||||
purple: 'rgb(153, 102, 255)',
|
|
||||||
grey: 'rgb(201, 203, 207)',
|
|
||||||
black: 'rgb(0, 0, 0)'
|
|
||||||
};
|
|
||||||
|
|
||||||
function start_charts() {
|
// Pretty print the config json so it's readable
|
||||||
Chart.scaleService.updateScaleDefaults('linear', {
|
var cfg_data = $("#configjson").text();
|
||||||
ticks: {
|
var cfg_json = JSON.parse(cfg_data);
|
||||||
min: 0
|
var cfg_pretty = JSON.stringify(cfg_json, null, '\t');
|
||||||
}
|
const html_pretty = Prism.highlight( cfg_pretty, Prism.languages.json, 'json');
|
||||||
|
$("#configjson").html(html_pretty);
|
||||||
|
|
||||||
|
$('.ui.accordion').accordion({exclusive: false});
|
||||||
|
$('.menu .item').tab('change tab', 'charts-tab');
|
||||||
});
|
});
|
||||||
|
</script>
|
||||||
memory_chart = new Chart($("#memChart"), {
|
|
||||||
label: 'Memory Usage',
|
|
||||||
type: 'line',
|
|
||||||
data: {
|
|
||||||
labels: [],
|
|
||||||
datasets: [{
|
|
||||||
label: 'Peak Ram usage',
|
|
||||||
borderColor: window.chartColors.red,
|
|
||||||
data: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Current Ram usage',
|
|
||||||
borderColor: window.chartColors.blue,
|
|
||||||
data: [],
|
|
||||||
}],
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
responsive: true,
|
|
||||||
maintainAspectRatio: false,
|
|
||||||
title: {
|
|
||||||
display: true,
|
|
||||||
text: 'Memory Usage',
|
|
||||||
},
|
|
||||||
scales: {
|
|
||||||
x: {
|
|
||||||
type: 'timeseries',
|
|
||||||
offset: true,
|
|
||||||
ticks: {
|
|
||||||
major: { enabled: true },
|
|
||||||
fontStyle: context => context.tick.major ? 'bold' : undefined,
|
|
||||||
source: 'data',
|
|
||||||
maxRotation: 0,
|
|
||||||
autoSkip: true,
|
|
||||||
autoSkipPadding: 75,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
message_chart = new Chart($("#messageChart"), {
|
|
||||||
label: 'Messages',
|
|
||||||
type: 'line',
|
|
||||||
data: {
|
|
||||||
labels: [],
|
|
||||||
datasets: [{
|
|
||||||
label: 'Messages Sent',
|
|
||||||
borderColor: window.chartColors.green,
|
|
||||||
data: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Messages Recieved',
|
|
||||||
borderColor: window.chartColors.yellow,
|
|
||||||
data: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Ack Sent',
|
|
||||||
borderColor: window.chartColors.purple,
|
|
||||||
data: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Ack Recieved',
|
|
||||||
borderColor: window.chartColors.black,
|
|
||||||
data: [],
|
|
||||||
}],
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
responsive: true,
|
|
||||||
maintainAspectRatio: false,
|
|
||||||
title: {
|
|
||||||
display: true,
|
|
||||||
text: 'APRS Messages',
|
|
||||||
},
|
|
||||||
scales: {
|
|
||||||
x: {
|
|
||||||
type: 'timeseries',
|
|
||||||
offset: true,
|
|
||||||
ticks: {
|
|
||||||
major: { enabled: true },
|
|
||||||
fontStyle: context => context.tick.major ? 'bold' : undefined,
|
|
||||||
source: 'data',
|
|
||||||
maxRotation: 0,
|
|
||||||
autoSkip: true,
|
|
||||||
autoSkipPadding: 75,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
email_chart = new Chart($("#emailChart"), {
|
|
||||||
label: 'Email Messages',
|
|
||||||
type: 'line',
|
|
||||||
data: {
|
|
||||||
labels: [],
|
|
||||||
datasets: [{
|
|
||||||
label: 'Sent',
|
|
||||||
borderColor: window.chartColors.green,
|
|
||||||
data: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Recieved',
|
|
||||||
borderColor: window.chartColors.yellow,
|
|
||||||
data: [],
|
|
||||||
}],
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
responsive: true,
|
|
||||||
maintainAspectRatio: false,
|
|
||||||
title: {
|
|
||||||
display: true,
|
|
||||||
text: 'Email Messages',
|
|
||||||
},
|
|
||||||
scales: {
|
|
||||||
x: {
|
|
||||||
type: 'timeseries',
|
|
||||||
offset: true,
|
|
||||||
ticks: {
|
|
||||||
major: { enabled: true },
|
|
||||||
fontStyle: context => context.tick.major ? 'bold' : undefined,
|
|
||||||
source: 'data',
|
|
||||||
maxRotation: 0,
|
|
||||||
autoSkip: true,
|
|
||||||
autoSkipPadding: 75,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function addData(chart, label, newdata) {
|
|
||||||
chart.data.labels.push(label);
|
|
||||||
chart.data.datasets.forEach((dataset) => {
|
|
||||||
dataset.data.push(newdata);
|
|
||||||
});
|
|
||||||
chart.update();
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateDualData(chart, label, first, second) {
|
|
||||||
chart.data.labels.push(label);
|
|
||||||
chart.data.datasets[0].data.push(first);
|
|
||||||
chart.data.datasets[1].data.push(second);
|
|
||||||
chart.update();
|
|
||||||
}
|
|
||||||
function updateQuadData(chart, label, first, second, third, fourth) {
|
|
||||||
chart.data.labels.push(label);
|
|
||||||
chart.data.datasets[0].data.push(first);
|
|
||||||
chart.data.datasets[1].data.push(second);
|
|
||||||
chart.data.datasets[2].data.push(third);
|
|
||||||
chart.data.datasets[3].data.push(fourth);
|
|
||||||
chart.update();
|
|
||||||
}
|
|
||||||
|
|
||||||
function update_stats( data ) {
|
|
||||||
$("#version").text( data["stats"]["aprsd"]["version"] );
|
|
||||||
$("#aprsis").text( "APRS-IS Server: " + data["stats"]["aprs-is"]["server"] );
|
|
||||||
$("#uptime").text( "uptime: " + data["stats"]["aprsd"]["uptime"] );
|
|
||||||
const html_pretty = Prism.highlight(JSON.stringify(data, null, '\t'), Prism.languages.json, 'json');
|
|
||||||
$("#jsonstats").html(html_pretty);
|
|
||||||
short_time = data["time"].split(/\s(.+)/)[1];
|
|
||||||
updateQuadData(message_chart, short_time, data["stats"]["messages"]["sent"], data["stats"]["messages"]["recieved"], data["stats"]["messages"]["ack_sent"], data["stats"]["messages"]["ack_recieved"]);
|
|
||||||
updateDualData(email_chart, short_time, data["stats"]["email"]["sent"], data["stats"]["email"]["recieved"]);
|
|
||||||
updateDualData(memory_chart, short_time, data["stats"]["aprsd"]["memory_peak"], data["stats"]["aprsd"]["memory_current"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function start_update() {
|
|
||||||
|
|
||||||
(function statsworker() {
|
|
||||||
$.ajax({
|
|
||||||
url: "/stats",
|
|
||||||
type: 'GET',
|
|
||||||
dataType: 'json',
|
|
||||||
success: function(data) {
|
|
||||||
update_stats(data);
|
|
||||||
},
|
|
||||||
complete: function() {
|
|
||||||
setTimeout(statsworker, 10000);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
$(document).ready(function() {
|
|
||||||
console.log(initial_stats);
|
|
||||||
start_update();
|
|
||||||
start_charts();
|
|
||||||
|
|
||||||
$("#toggleStats").click(function() {
|
|
||||||
$("#jsonstats").fadeToggle(1000);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style type="text/css">
|
|
||||||
body {
|
|
||||||
display: grid;
|
|
||||||
grid-template-rows: auto 1fr auto;
|
|
||||||
background: #eeeeee;
|
|
||||||
margin: 2em;
|
|
||||||
padding: 0;
|
|
||||||
text-align: center;
|
|
||||||
font-family: system-ui, sans-serif;
|
|
||||||
height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
padding: 2em;
|
|
||||||
height: 10vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
#main {
|
|
||||||
padding: 2em;
|
|
||||||
height: 80vh;
|
|
||||||
}
|
|
||||||
footer {
|
|
||||||
padding: 2em;
|
|
||||||
text-align: center;
|
|
||||||
height: 10vh;
|
|
||||||
}
|
|
||||||
#graphs {
|
|
||||||
display: grid;
|
|
||||||
width: 100%;
|
|
||||||
height: 300px;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
}
|
|
||||||
#graphs_center {
|
|
||||||
display: block;
|
|
||||||
margin-top: 10px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
width: 100%;
|
|
||||||
height: 300px;
|
|
||||||
}
|
|
||||||
#left {
|
|
||||||
margin-right: 2px;
|
|
||||||
height: 300px;
|
|
||||||
}
|
|
||||||
#right {
|
|
||||||
height: 300px;
|
|
||||||
}
|
|
||||||
#center {
|
|
||||||
height: 300px;
|
|
||||||
}
|
|
||||||
#messageChart, #emailChart, #memChart {
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
background: #ddd;
|
|
||||||
}
|
|
||||||
#stats {
|
|
||||||
margin: auto;
|
|
||||||
width: 80%;
|
|
||||||
}
|
|
||||||
#jsonstats {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
#title {
|
|
||||||
font-size: 4em;
|
|
||||||
}
|
|
||||||
#uptime, #aprsis {
|
|
||||||
font-size: 1em;
|
|
||||||
}
|
|
||||||
#callsign {
|
|
||||||
font-size: 1.4em;
|
|
||||||
color: #00F;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<div id="title">APRSD version <span id="version"></div></div>
|
<div id="title">APRSD <span id="version"></span></div>
|
||||||
<div id="callsign">{{ callsign }}</div>
|
<div id="callsign">{{ callsign }}</div>
|
||||||
<div id="aprsis"></div>
|
<div id="aprsis"></div>
|
||||||
<div id="uptime"></div>
|
<div id="uptime"></div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div id="main">
|
<div id="main" class="main ui container">
|
||||||
<div id="graphs">
|
<!-- Tab links -->
|
||||||
<div id="left"><canvas id="messageChart"></canvas></div>
|
<div class="ui tabular menu">
|
||||||
<div id="right"><canvas class="right" id="emailChart"></canvas></div>
|
<div class="active item" data-tab="charts-tab">Charts</div>
|
||||||
</div>
|
<div class="item" data-tab="msgs-tab">Messages</div>
|
||||||
<div id="graphs_center">
|
<div class="item" data-tab="config-tab">Config</div>
|
||||||
<div id="center"><canvas id="memChart"></canvas></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="stats">
|
<!-- Tab content -->
|
||||||
<button id="toggleStats">Toggle raw json</button>
|
<div class="ui active tab" data-tab="charts-tab">
|
||||||
<pre id="jsonstats" class="language-json">{{ stats }}</pre>
|
<h3>Charts</h3>
|
||||||
|
<div id="graphs">
|
||||||
|
<div id="left"><canvas id="messageChart"></canvas></div>
|
||||||
|
<div id="right"><canvas class="right" id="emailChart"></canvas></div>
|
||||||
|
</div>
|
||||||
|
<div id="graphs_center">
|
||||||
|
<div id="center"><canvas id="memChart"></canvas></div>
|
||||||
|
</div>
|
||||||
|
<div id="stats">
|
||||||
|
<button id="toggleStats">Toggle raw json</button>
|
||||||
|
<pre id="jsonstats" class="language-json">{{ stats }}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ui tab" data-tab="msgs-tab">
|
||||||
|
<h3>Messages</h3>
|
||||||
|
<div class="ui styled fluid accordion" id="accordion">
|
||||||
|
<div id="packetsDiv">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ui tab" data-tab="config-tab">
|
||||||
|
<h3>Config</h3>
|
||||||
|
<pre id="configjson" class="language-json">{{ config_json|safe }}</pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user