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,19 +90,17 @@ 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 {} "
"Msgs TX:{} RX:{} Last:{} Email:{} RAM Current:{} Peak:{}".format(
utils.strfdelta(stats_obj.uptime), utils.strfdelta(stats_obj.uptime),
len(tracker), len(tracker),
stats_obj.msgs_tx, stats_obj.msgs_tx,
stats_obj.msgs_rx, stats_obj.msgs_rx,
last_msg_time, last_msg_time,
email_thread_time, email_thread_time,
len(packets_list),
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
delta = now - self.checker_time delta = now - self.checker_time
@ -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

@ -9,218 +9,22 @@
<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 memory_chart = null
var message_chart = null var message_chart = null
var color = Chart.helpers.color; var color = Chart.helpers.color;
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 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() { $(document).ready(function() {
console.log(initial_stats); console.log(initial_stats);
start_update(); start_update();
@ -229,92 +33,39 @@
$("#toggleStats").click(function() { $("#toggleStats").click(function() {
$("#jsonstats").fadeToggle(1000); $("#jsonstats").fadeToggle(1000);
}); });
// Pretty print the config json so it's readable
var cfg_data = $("#configjson").text();
var cfg_json = JSON.parse(cfg_data);
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> </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">
<!-- Tab links -->
<div class="ui tabular menu">
<div class="active item" data-tab="charts-tab">Charts</div>
<div class="item" data-tab="msgs-tab">Messages</div>
<div class="item" data-tab="config-tab">Config</div>
</div>
<!-- Tab content -->
<div class="ui active tab" data-tab="charts-tab">
<h3>Charts</h3>
<div id="graphs"> <div id="graphs">
<div id="left"><canvas id="messageChart"></canvas></div> <div id="left"><canvas id="messageChart"></canvas></div>
<div id="right"><canvas class="right" id="emailChart"></canvas></div> <div id="right"><canvas class="right" id="emailChart"></canvas></div>
@ -322,13 +73,26 @@
<div id="graphs_center"> <div id="graphs_center">
<div id="center"><canvas id="memChart"></canvas></div> <div id="center"><canvas id="memChart"></canvas></div>
</div> </div>
<div id="stats"> <div id="stats">
<button id="toggleStats">Toggle raw json</button> <button id="toggleStats">Toggle raw json</button>
<pre id="jsonstats" class="language-json">{{ stats }}</pre> <pre id="jsonstats" class="language-json">{{ stats }}</pre>
</div> </div>
</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>
<footer> <footer>
<a href="https://badge.fury.io/py/aprsd"><img src="https://badge.fury.io/py/aprsd.svg" alt="PyPI version" height="18"></a> <a href="https://badge.fury.io/py/aprsd"><img src="https://badge.fury.io/py/aprsd.svg" alt="PyPI version" height="18"></a>
<a href="https://github.com/craigerl/aprsd"><img src="https://img.shields.io/badge/Made%20with-Python-1f425f.svg" height="18"></a> <a href="https://github.com/craigerl/aprsd"><img src="https://img.shields.io/badge/Made%20with-Python-1f425f.svg" height="18"></a>