Add admin UI tabs for charts, messages, config

This patch updates the admin UI to include 3 tabs
of content.
Charts
messages
config

The charts tab is the existing line charts.
The messages tab shows a list of RX (green) and TX (red) messages
from/to aprsd.
The config tab shows the config loaded at startup time.
This commit is contained in:
Hemna 2021-07-09 15:59:21 -04:00
parent 1c66555450
commit de62579852
9 changed files with 545 additions and 325 deletions

View File

@ -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

View File

@ -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
View 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

View File

@ -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

View 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;
}

View 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;
}

View 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);
}
});
})();
}

View 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";
}
}

View File

@ -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>