Remove webchat as a built in command.
Webchat will now be an extension that can be installed. the extension is here: https://github.com/hemna/aprsd-webchat-extension Install it from git or pypi.
@ -1,643 +0,0 @@
|
|||||||
import datetime
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
import signal
|
|
||||||
import sys
|
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
|
|
||||||
import click
|
|
||||||
import flask
|
|
||||||
from flask import request
|
|
||||||
from flask_httpauth import HTTPBasicAuth
|
|
||||||
from flask_socketio import Namespace, SocketIO
|
|
||||||
from geopy.distance import geodesic
|
|
||||||
from oslo_config import cfg
|
|
||||||
import timeago
|
|
||||||
import wrapt
|
|
||||||
|
|
||||||
import aprsd
|
|
||||||
from aprsd import cli_helper, client, packets, plugin_utils, stats, threads
|
|
||||||
from aprsd import utils
|
|
||||||
from aprsd import utils as aprsd_utils
|
|
||||||
from aprsd.client import client_factory, kiss
|
|
||||||
from aprsd.main import cli
|
|
||||||
from aprsd.threads import aprsd as aprsd_threads
|
|
||||||
from aprsd.threads import keep_alive, rx
|
|
||||||
from aprsd.threads import stats as stats_thread
|
|
||||||
from aprsd.threads import tx
|
|
||||||
from aprsd.utils import trace
|
|
||||||
|
|
||||||
|
|
||||||
CONF = cfg.CONF
|
|
||||||
LOG = logging.getLogger()
|
|
||||||
auth = HTTPBasicAuth()
|
|
||||||
socketio = None
|
|
||||||
|
|
||||||
# List of callsigns that we don't want to track/fetch their location
|
|
||||||
callsign_no_track = [
|
|
||||||
"APDW16", "BLN0", "BLN1", "BLN2",
|
|
||||||
"BLN3", "BLN4", "BLN5", "BLN6", "BLN7", "BLN8", "BLN9",
|
|
||||||
]
|
|
||||||
|
|
||||||
# Callsign location information
|
|
||||||
# callsign: {lat: 0.0, long: 0.0, last_update: datetime}
|
|
||||||
callsign_locations = {}
|
|
||||||
|
|
||||||
flask_app = flask.Flask(
|
|
||||||
"aprsd",
|
|
||||||
static_url_path="/static",
|
|
||||||
static_folder="web/chat/static",
|
|
||||||
template_folder="web/chat/templates",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def signal_handler(sig, frame):
|
|
||||||
|
|
||||||
click.echo("signal_handler: called")
|
|
||||||
LOG.info(
|
|
||||||
f"Ctrl+C, Sending all threads({len(threads.APRSDThreadList())}) exit! "
|
|
||||||
f"Can take up to 10 seconds {datetime.datetime.now()}",
|
|
||||||
)
|
|
||||||
threads.APRSDThreadList().stop_all()
|
|
||||||
if "subprocess" not in str(frame):
|
|
||||||
time.sleep(1.5)
|
|
||||||
stats.stats_collector.collect()
|
|
||||||
LOG.info("Telling flask to bail.")
|
|
||||||
signal.signal(signal.SIGTERM, sys.exit(0))
|
|
||||||
|
|
||||||
|
|
||||||
class SentMessages:
|
|
||||||
|
|
||||||
_instance = None
|
|
||||||
lock = threading.Lock()
|
|
||||||
|
|
||||||
data = {}
|
|
||||||
|
|
||||||
def __new__(cls, *args, **kwargs):
|
|
||||||
"""This magic turns this into a singleton."""
|
|
||||||
if cls._instance is None:
|
|
||||||
cls._instance = super().__new__(cls)
|
|
||||||
return cls._instance
|
|
||||||
|
|
||||||
def is_initialized(self):
|
|
||||||
return True
|
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
def add(self, msg):
|
|
||||||
self.data[msg.msgNo] = msg.__dict__
|
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
def __len__(self):
|
|
||||||
return len(self.data.keys())
|
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
def get(self, id):
|
|
||||||
if id in self.data:
|
|
||||||
return self.data[id]
|
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
def get_all(self):
|
|
||||||
return self.data
|
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
def set_status(self, id, status):
|
|
||||||
if id in self.data:
|
|
||||||
self.data[id]["last_update"] = str(datetime.datetime.now())
|
|
||||||
self.data[id]["status"] = status
|
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
def ack(self, id):
|
|
||||||
"""The message got an ack!"""
|
|
||||||
if id in self.data:
|
|
||||||
self.data[id]["last_update"] = str(datetime.datetime.now())
|
|
||||||
self.data[id]["ack"] = True
|
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
def reply(self, id, packet):
|
|
||||||
"""We got a packet back from the sent message."""
|
|
||||||
if id in self.data:
|
|
||||||
self.data[id]["reply"] = packet
|
|
||||||
|
|
||||||
|
|
||||||
def _build_location_from_repeat(message):
|
|
||||||
# This is a location message Format is
|
|
||||||
# ^ld^callsign:latitude,longitude,altitude,course,speed,timestamp
|
|
||||||
a = message.split(":")
|
|
||||||
LOG.warning(a)
|
|
||||||
if len(a) == 2:
|
|
||||||
callsign = a[0].replace("^ld^", "")
|
|
||||||
b = a[1].split(",")
|
|
||||||
LOG.warning(b)
|
|
||||||
if len(b) == 6:
|
|
||||||
lat = float(b[0])
|
|
||||||
lon = float(b[1])
|
|
||||||
alt = float(b[2])
|
|
||||||
course = float(b[3])
|
|
||||||
speed = float(b[4])
|
|
||||||
time = int(b[5])
|
|
||||||
compass_bearing = aprsd_utils.degrees_to_cardinal(course)
|
|
||||||
data = {
|
|
||||||
"callsign": callsign,
|
|
||||||
"lat": lat,
|
|
||||||
"lon": lon,
|
|
||||||
"altitude": alt,
|
|
||||||
"course": course,
|
|
||||||
"compass_bearing": compass_bearing,
|
|
||||||
"speed": speed,
|
|
||||||
"lasttime": time,
|
|
||||||
"timeago": timeago.format(time),
|
|
||||||
}
|
|
||||||
LOG.debug(f"Location data from REPEAT {data}")
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
def _calculate_location_data(location_data):
|
|
||||||
"""Calculate all of the location data from data from aprs.fi or REPEAT."""
|
|
||||||
lat = location_data["lat"]
|
|
||||||
lon = location_data["lon"]
|
|
||||||
alt = location_data["altitude"]
|
|
||||||
speed = location_data["speed"]
|
|
||||||
lasttime = location_data["lasttime"]
|
|
||||||
timeago_str = location_data.get(
|
|
||||||
"timeago",
|
|
||||||
timeago.format(lasttime),
|
|
||||||
)
|
|
||||||
# now calculate distance from our own location
|
|
||||||
distance = 0
|
|
||||||
if CONF.webchat.latitude and CONF.webchat.longitude:
|
|
||||||
our_lat = float(CONF.webchat.latitude)
|
|
||||||
our_lon = float(CONF.webchat.longitude)
|
|
||||||
distance = geodesic((our_lat, our_lon), (lat, lon)).kilometers
|
|
||||||
bearing = aprsd_utils.calculate_initial_compass_bearing(
|
|
||||||
(our_lat, our_lon),
|
|
||||||
(lat, lon),
|
|
||||||
)
|
|
||||||
compass_bearing = aprsd_utils.degrees_to_cardinal(bearing)
|
|
||||||
return {
|
|
||||||
"callsign": location_data["callsign"],
|
|
||||||
"lat": lat,
|
|
||||||
"lon": lon,
|
|
||||||
"altitude": alt,
|
|
||||||
"course": f"{bearing:0.1f}",
|
|
||||||
"compass_bearing": compass_bearing,
|
|
||||||
"speed": speed,
|
|
||||||
"lasttime": lasttime,
|
|
||||||
"timeago": timeago_str,
|
|
||||||
"distance": f"{distance:0.1f}",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def send_location_data_to_browser(location_data):
|
|
||||||
global socketio
|
|
||||||
callsign = location_data["callsign"]
|
|
||||||
LOG.info(f"Got location for {callsign} {callsign_locations[callsign]}")
|
|
||||||
socketio.emit(
|
|
||||||
"callsign_location", callsign_locations[callsign],
|
|
||||||
namespace="/sendmsg",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def populate_callsign_location(callsign, data=None):
|
|
||||||
"""Populate the location for the callsign.
|
|
||||||
|
|
||||||
if data is passed in, then we have the location already from
|
|
||||||
an APRS packet. If data is None, then we need to fetch the
|
|
||||||
location from aprs.fi or REPEAT.
|
|
||||||
"""
|
|
||||||
global socketio
|
|
||||||
"""Fetch the location for the callsign."""
|
|
||||||
LOG.debug(f"populate_callsign_location {callsign}")
|
|
||||||
if data:
|
|
||||||
location_data = _calculate_location_data(data)
|
|
||||||
callsign_locations[callsign] = location_data
|
|
||||||
send_location_data_to_browser(location_data)
|
|
||||||
return
|
|
||||||
|
|
||||||
# First we are going to try to get the location from aprs.fi
|
|
||||||
# if there is no internets, then this will fail and we will
|
|
||||||
# fallback to calling REPEAT for the location for the callsign.
|
|
||||||
fallback = False
|
|
||||||
if not CONF.aprs_fi.apiKey:
|
|
||||||
LOG.warning(
|
|
||||||
"Config aprs_fi.apiKey is not set. Can't get location from aprs.fi "
|
|
||||||
" falling back to sending REPEAT to get location.",
|
|
||||||
)
|
|
||||||
fallback = True
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
aprs_data = plugin_utils.get_aprs_fi(CONF.aprs_fi.apiKey, callsign)
|
|
||||||
if not len(aprs_data["entries"]):
|
|
||||||
LOG.error("Didn't get any entries from aprs.fi")
|
|
||||||
return
|
|
||||||
lat = float(aprs_data["entries"][0]["lat"])
|
|
||||||
lon = float(aprs_data["entries"][0]["lng"])
|
|
||||||
try: # altitude not always provided
|
|
||||||
alt = float(aprs_data["entries"][0]["altitude"])
|
|
||||||
except Exception:
|
|
||||||
alt = 0
|
|
||||||
location_data = {
|
|
||||||
"callsign": callsign,
|
|
||||||
"lat": lat,
|
|
||||||
"lon": lon,
|
|
||||||
"altitude": alt,
|
|
||||||
"lasttime": int(aprs_data["entries"][0]["lasttime"]),
|
|
||||||
"course": float(aprs_data["entries"][0].get("course", 0)),
|
|
||||||
"speed": float(aprs_data["entries"][0].get("speed", 0)),
|
|
||||||
}
|
|
||||||
location_data = _calculate_location_data(location_data)
|
|
||||||
callsign_locations[callsign] = location_data
|
|
||||||
send_location_data_to_browser(location_data)
|
|
||||||
return
|
|
||||||
except Exception as ex:
|
|
||||||
LOG.error(f"Failed to fetch aprs.fi '{ex}'")
|
|
||||||
LOG.error(ex)
|
|
||||||
fallback = True
|
|
||||||
|
|
||||||
if fallback:
|
|
||||||
# We don't have the location data
|
|
||||||
# and we can't get it from aprs.fi
|
|
||||||
# Send a special message to REPEAT to get the location data
|
|
||||||
LOG.info(f"Sending REPEAT to get location for callsign {callsign}.")
|
|
||||||
tx.send(
|
|
||||||
packets.MessagePacket(
|
|
||||||
from_call=CONF.callsign,
|
|
||||||
to_call="REPEAT",
|
|
||||||
message_text=f"ld {callsign}",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class WebChatProcessPacketThread(rx.APRSDProcessPacketThread):
|
|
||||||
"""Class that handles packets being sent to us."""
|
|
||||||
|
|
||||||
def __init__(self, packet_queue, socketio):
|
|
||||||
self.socketio = socketio
|
|
||||||
self.connected = False
|
|
||||||
super().__init__(packet_queue)
|
|
||||||
|
|
||||||
def process_ack_packet(self, packet: packets.AckPacket):
|
|
||||||
super().process_ack_packet(packet)
|
|
||||||
ack_num = packet.get("msgNo")
|
|
||||||
SentMessages().ack(ack_num)
|
|
||||||
msg = SentMessages().get(ack_num)
|
|
||||||
if msg:
|
|
||||||
self.socketio.emit(
|
|
||||||
"ack", msg,
|
|
||||||
namespace="/sendmsg",
|
|
||||||
)
|
|
||||||
self.got_ack = True
|
|
||||||
|
|
||||||
def process_our_message_packet(self, packet: packets.MessagePacket):
|
|
||||||
global callsign_locations
|
|
||||||
# ok lets see if we have the location for the
|
|
||||||
# person we just sent a message to.
|
|
||||||
from_call = packet.get("from_call").upper()
|
|
||||||
if from_call == "REPEAT":
|
|
||||||
# We got a message from REPEAT. Is this a location message?
|
|
||||||
message = packet.get("message_text")
|
|
||||||
if message.startswith("^ld^"):
|
|
||||||
location_data = _build_location_from_repeat(message)
|
|
||||||
callsign = location_data["callsign"]
|
|
||||||
location_data = _calculate_location_data(location_data)
|
|
||||||
callsign_locations[callsign] = location_data
|
|
||||||
send_location_data_to_browser(location_data)
|
|
||||||
return
|
|
||||||
elif (
|
|
||||||
from_call not in callsign_locations
|
|
||||||
and from_call not in callsign_no_track
|
|
||||||
and client_factory.create().transport() in [client.TRANSPORT_APRSIS, client.TRANSPORT_FAKE]
|
|
||||||
):
|
|
||||||
# We have to ask aprs for the location for the callsign
|
|
||||||
# We send a message packet to wb4bor-11 asking for location.
|
|
||||||
populate_callsign_location(from_call)
|
|
||||||
# Send the packet to the browser.
|
|
||||||
self.socketio.emit(
|
|
||||||
"new", packet.__dict__,
|
|
||||||
namespace="/sendmsg",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class LocationProcessingThread(aprsd_threads.APRSDThread):
|
|
||||||
"""Class to handle the location processing."""
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__("LocationProcessingThread")
|
|
||||||
|
|
||||||
def loop(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def _get_transport(stats):
|
|
||||||
if CONF.aprs_network.enabled:
|
|
||||||
transport = "aprs-is"
|
|
||||||
aprs_connection = (
|
|
||||||
"APRS-IS Server: <a href='http://status.aprs2.net' >"
|
|
||||||
"{}</a>".format(stats["APRSClientStats"]["server_string"])
|
|
||||||
)
|
|
||||||
elif kiss.KISSClient.is_enabled():
|
|
||||||
transport = kiss.KISSClient.transport()
|
|
||||||
if transport == client.TRANSPORT_TCPKISS:
|
|
||||||
aprs_connection = (
|
|
||||||
"TCPKISS://{}:{}".format(
|
|
||||||
CONF.kiss_tcp.host,
|
|
||||||
CONF.kiss_tcp.port,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
elif transport == client.TRANSPORT_SERIALKISS:
|
|
||||||
# for pep8 violation
|
|
||||||
aprs_connection = (
|
|
||||||
"SerialKISS://{}@{} baud".format(
|
|
||||||
CONF.kiss_serial.device,
|
|
||||||
CONF.kiss_serial.baudrate,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
elif CONF.fake_client.enabled:
|
|
||||||
transport = client.TRANSPORT_FAKE
|
|
||||||
aprs_connection = "Fake Client"
|
|
||||||
|
|
||||||
return transport, aprs_connection
|
|
||||||
|
|
||||||
|
|
||||||
@flask_app.route("/location/<callsign>", methods=["POST"])
|
|
||||||
def location(callsign):
|
|
||||||
LOG.debug(f"Fetch location for callsign {callsign}")
|
|
||||||
if not callsign in callsign_no_track:
|
|
||||||
populate_callsign_location(callsign)
|
|
||||||
|
|
||||||
|
|
||||||
@auth.login_required
|
|
||||||
@flask_app.route("/")
|
|
||||||
def index():
|
|
||||||
stats = _stats()
|
|
||||||
|
|
||||||
# For development
|
|
||||||
html_template = "index.html"
|
|
||||||
LOG.debug(f"Template {html_template}")
|
|
||||||
|
|
||||||
transport, aprs_connection = _get_transport(stats["stats"])
|
|
||||||
LOG.debug(f"transport {transport} aprs_connection {aprs_connection}")
|
|
||||||
|
|
||||||
stats["transport"] = transport
|
|
||||||
stats["aprs_connection"] = aprs_connection
|
|
||||||
LOG.debug(f"initial stats = {stats}")
|
|
||||||
latitude = CONF.webchat.latitude
|
|
||||||
if latitude:
|
|
||||||
latitude = float(CONF.webchat.latitude)
|
|
||||||
|
|
||||||
longitude = CONF.webchat.longitude
|
|
||||||
if longitude:
|
|
||||||
longitude = float(longitude)
|
|
||||||
|
|
||||||
return flask.render_template(
|
|
||||||
html_template,
|
|
||||||
initial_stats=stats,
|
|
||||||
aprs_connection=aprs_connection,
|
|
||||||
callsign=CONF.callsign,
|
|
||||||
version=aprsd.__version__,
|
|
||||||
latitude=latitude,
|
|
||||||
longitude=longitude,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@auth.login_required
|
|
||||||
@flask_app.route("/send-message-status")
|
|
||||||
def send_message_status():
|
|
||||||
LOG.debug(request)
|
|
||||||
msgs = SentMessages()
|
|
||||||
info = msgs.get_all()
|
|
||||||
return json.dumps(info)
|
|
||||||
|
|
||||||
|
|
||||||
def _stats():
|
|
||||||
now = datetime.datetime.now()
|
|
||||||
|
|
||||||
time_format = "%m-%d-%Y %H:%M:%S"
|
|
||||||
stats_dict = stats.stats_collector.collect(serializable=True)
|
|
||||||
# Webchat doesnt need these
|
|
||||||
if "WatchList" in stats_dict:
|
|
||||||
del stats_dict["WatchList"]
|
|
||||||
if "SeenList" in stats_dict:
|
|
||||||
del stats_dict["SeenList"]
|
|
||||||
if "APRSDThreadList" in stats_dict:
|
|
||||||
del stats_dict["APRSDThreadList"]
|
|
||||||
if "PacketList" in stats_dict:
|
|
||||||
del stats_dict["PacketList"]
|
|
||||||
if "EmailStats" in stats_dict:
|
|
||||||
del stats_dict["EmailStats"]
|
|
||||||
if "PluginManager" in stats_dict:
|
|
||||||
del stats_dict["PluginManager"]
|
|
||||||
|
|
||||||
result = {
|
|
||||||
"time": now.strftime(time_format),
|
|
||||||
"stats": stats_dict,
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
@flask_app.route("/stats")
|
|
||||||
def get_stats():
|
|
||||||
return json.dumps(_stats())
|
|
||||||
|
|
||||||
|
|
||||||
class SendMessageNamespace(Namespace):
|
|
||||||
"""Class to handle the socketio interactions."""
|
|
||||||
got_ack = False
|
|
||||||
reply_sent = False
|
|
||||||
msg = None
|
|
||||||
request = None
|
|
||||||
|
|
||||||
def __init__(self, namespace=None, config=None):
|
|
||||||
super().__init__(namespace)
|
|
||||||
|
|
||||||
def on_connect(self):
|
|
||||||
global socketio
|
|
||||||
LOG.debug("Web socket connected")
|
|
||||||
socketio.emit(
|
|
||||||
"connected", {"data": "/sendmsg Connected"},
|
|
||||||
namespace="/sendmsg",
|
|
||||||
)
|
|
||||||
|
|
||||||
def on_disconnect(self):
|
|
||||||
LOG.debug("WS Disconnected")
|
|
||||||
|
|
||||||
def on_send(self, data):
|
|
||||||
global socketio
|
|
||||||
LOG.debug(f"WS: on_send {data}")
|
|
||||||
self.request = data
|
|
||||||
data["from"] = CONF.callsign
|
|
||||||
path = data.get("path", None)
|
|
||||||
if not path:
|
|
||||||
path = []
|
|
||||||
elif "," in path:
|
|
||||||
path_opts = path.split(",")
|
|
||||||
path = [x.strip() for x in path_opts]
|
|
||||||
else:
|
|
||||||
path = [path]
|
|
||||||
|
|
||||||
pkt = packets.MessagePacket(
|
|
||||||
from_call=data["from"],
|
|
||||||
to_call=data["to"].upper(),
|
|
||||||
message_text=data["message"],
|
|
||||||
path=path,
|
|
||||||
)
|
|
||||||
pkt.prepare()
|
|
||||||
self.msg = pkt
|
|
||||||
msgs = SentMessages()
|
|
||||||
tx.send(pkt)
|
|
||||||
msgs.add(pkt)
|
|
||||||
msgs.set_status(pkt.msgNo, "Sending")
|
|
||||||
obj = msgs.get(pkt.msgNo)
|
|
||||||
socketio.emit(
|
|
||||||
"sent", obj,
|
|
||||||
namespace="/sendmsg",
|
|
||||||
)
|
|
||||||
|
|
||||||
def on_gps(self, data):
|
|
||||||
LOG.debug(f"WS on_GPS: {data}")
|
|
||||||
lat = data["latitude"]
|
|
||||||
long = data["longitude"]
|
|
||||||
LOG.debug(f"Lat {lat}")
|
|
||||||
LOG.debug(f"Long {long}")
|
|
||||||
path = data.get("path", None)
|
|
||||||
if not path:
|
|
||||||
path = []
|
|
||||||
elif "," in path:
|
|
||||||
path_opts = path.split(",")
|
|
||||||
path = [x.strip() for x in path_opts]
|
|
||||||
else:
|
|
||||||
path = [path]
|
|
||||||
|
|
||||||
tx.send(
|
|
||||||
packets.BeaconPacket(
|
|
||||||
from_call=CONF.callsign,
|
|
||||||
to_call="APDW16",
|
|
||||||
latitude=lat,
|
|
||||||
longitude=long,
|
|
||||||
comment="APRSD WebChat Beacon",
|
|
||||||
path=path,
|
|
||||||
),
|
|
||||||
direct=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
def handle_message(self, data):
|
|
||||||
LOG.debug(f"WS Data {data}")
|
|
||||||
|
|
||||||
def handle_json(self, data):
|
|
||||||
LOG.debug(f"WS json {data}")
|
|
||||||
|
|
||||||
def on_get_callsign_location(self, data):
|
|
||||||
LOG.debug(f"on_callsign_location {data}")
|
|
||||||
if data["callsign"] not in callsign_no_track:
|
|
||||||
populate_callsign_location(data["callsign"])
|
|
||||||
|
|
||||||
|
|
||||||
@trace.trace
|
|
||||||
def init_flask(loglevel, quiet):
|
|
||||||
global socketio, flask_app
|
|
||||||
|
|
||||||
socketio = SocketIO(
|
|
||||||
flask_app, logger=False, engineio_logger=False,
|
|
||||||
async_mode="threading",
|
|
||||||
)
|
|
||||||
|
|
||||||
socketio.on_namespace(
|
|
||||||
SendMessageNamespace(
|
|
||||||
"/sendmsg",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return socketio
|
|
||||||
|
|
||||||
|
|
||||||
# main() ###
|
|
||||||
@cli.command()
|
|
||||||
@cli_helper.add_options(cli_helper.common_options)
|
|
||||||
@click.option(
|
|
||||||
"-f",
|
|
||||||
"--flush",
|
|
||||||
"flush",
|
|
||||||
is_flag=True,
|
|
||||||
show_default=True,
|
|
||||||
default=False,
|
|
||||||
help="Flush out all old aged messages on disk.",
|
|
||||||
)
|
|
||||||
@click.option(
|
|
||||||
"-p",
|
|
||||||
"--port",
|
|
||||||
"port",
|
|
||||||
show_default=True,
|
|
||||||
default=None,
|
|
||||||
help="Port to listen to web requests. This overrides the config.webchat.web_port setting.",
|
|
||||||
)
|
|
||||||
@click.pass_context
|
|
||||||
@cli_helper.process_standard_options
|
|
||||||
def webchat(ctx, flush, port):
|
|
||||||
"""Web based HAM Radio chat program!"""
|
|
||||||
loglevel = ctx.obj["loglevel"]
|
|
||||||
quiet = ctx.obj["quiet"]
|
|
||||||
|
|
||||||
signal.signal(signal.SIGINT, signal_handler)
|
|
||||||
signal.signal(signal.SIGTERM, signal_handler)
|
|
||||||
|
|
||||||
level, msg = utils._check_version()
|
|
||||||
if level:
|
|
||||||
LOG.warning(msg)
|
|
||||||
else:
|
|
||||||
LOG.info(msg)
|
|
||||||
LOG.info(f"APRSD Started version: {aprsd.__version__}")
|
|
||||||
|
|
||||||
CONF.log_opt_values(logging.getLogger(), logging.DEBUG)
|
|
||||||
if not port:
|
|
||||||
port = CONF.webchat.web_port
|
|
||||||
|
|
||||||
# Initialize the client factory and create
|
|
||||||
# The correct client object ready for use
|
|
||||||
# Make sure we have 1 client transport enabled
|
|
||||||
if not client_factory.is_client_enabled():
|
|
||||||
LOG.error("No Clients are enabled in config.")
|
|
||||||
sys.exit(-1)
|
|
||||||
|
|
||||||
if not client_factory.is_client_configured():
|
|
||||||
LOG.error("APRS client is not properly configured in config file.")
|
|
||||||
sys.exit(-1)
|
|
||||||
|
|
||||||
# Creates the client object
|
|
||||||
LOG.info("Creating client connection")
|
|
||||||
aprs_client = client_factory.create()
|
|
||||||
LOG.info(aprs_client)
|
|
||||||
if not aprs_client.login_success:
|
|
||||||
# We failed to login, will just quit!
|
|
||||||
msg = f"Login Failure: {aprs_client.login_failure}"
|
|
||||||
LOG.error(msg)
|
|
||||||
print(msg)
|
|
||||||
sys.exit(-1)
|
|
||||||
|
|
||||||
keepalive = keep_alive.KeepAliveThread()
|
|
||||||
LOG.info("Start KeepAliveThread")
|
|
||||||
keepalive.start()
|
|
||||||
|
|
||||||
stats_store_thread = stats_thread.APRSDStatsStoreThread()
|
|
||||||
stats_store_thread.start()
|
|
||||||
|
|
||||||
socketio = init_flask(loglevel, quiet)
|
|
||||||
rx_thread = rx.APRSDPluginRXThread(
|
|
||||||
packet_queue=threads.packet_queue,
|
|
||||||
)
|
|
||||||
rx_thread.start()
|
|
||||||
process_thread = WebChatProcessPacketThread(
|
|
||||||
packet_queue=threads.packet_queue,
|
|
||||||
socketio=socketio,
|
|
||||||
)
|
|
||||||
process_thread.start()
|
|
||||||
|
|
||||||
LOG.info("Start socketio.run()")
|
|
||||||
socketio.run(
|
|
||||||
flask_app,
|
|
||||||
# This is broken for now after removing cryptography
|
|
||||||
# and pyopenssl
|
|
||||||
# ssl_context="adhoc",
|
|
||||||
host=CONF.webchat.web_ip,
|
|
||||||
port=port,
|
|
||||||
allow_unsafe_werkzeug=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
LOG.info("WebChat exiting!!!! Bye.")
|
|
@ -11,17 +11,12 @@ watch_list_group = cfg.OptGroup(
|
|||||||
name="watch_list",
|
name="watch_list",
|
||||||
title="Watch List settings",
|
title="Watch List settings",
|
||||||
)
|
)
|
||||||
webchat_group = cfg.OptGroup(
|
|
||||||
name="webchat",
|
|
||||||
title="Settings specific to the webchat command",
|
|
||||||
)
|
|
||||||
|
|
||||||
registry_group = cfg.OptGroup(
|
registry_group = cfg.OptGroup(
|
||||||
name="aprs_registry",
|
name="aprs_registry",
|
||||||
title="APRS Registry settings",
|
title="APRS Registry settings",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
aprsd_opts = [
|
aprsd_opts = [
|
||||||
cfg.StrOpt(
|
cfg.StrOpt(
|
||||||
"callsign",
|
"callsign",
|
||||||
@ -194,34 +189,6 @@ enabled_plugins_opts = [
|
|||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
webchat_opts = [
|
|
||||||
cfg.StrOpt(
|
|
||||||
"web_ip",
|
|
||||||
default="0.0.0.0",
|
|
||||||
help="The ip address to listen on",
|
|
||||||
),
|
|
||||||
cfg.PortOpt(
|
|
||||||
"web_port",
|
|
||||||
default=8001,
|
|
||||||
help="The port to listen on",
|
|
||||||
),
|
|
||||||
cfg.StrOpt(
|
|
||||||
"latitude",
|
|
||||||
default=None,
|
|
||||||
help="Latitude for the GPS Beacon button. If not set, the button will not be enabled.",
|
|
||||||
),
|
|
||||||
cfg.StrOpt(
|
|
||||||
"longitude",
|
|
||||||
default=None,
|
|
||||||
help="Longitude for the GPS Beacon button. If not set, the button will not be enabled.",
|
|
||||||
),
|
|
||||||
cfg.BoolOpt(
|
|
||||||
"disable_url_request_logging",
|
|
||||||
default=False,
|
|
||||||
help="Disable the logging of url requests in the webchat command.",
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
registry_opts = [
|
registry_opts = [
|
||||||
cfg.BoolOpt(
|
cfg.BoolOpt(
|
||||||
"enabled",
|
"enabled",
|
||||||
@ -261,8 +228,6 @@ def register_opts(config):
|
|||||||
config.register_opts(enabled_plugins_opts)
|
config.register_opts(enabled_plugins_opts)
|
||||||
config.register_group(watch_list_group)
|
config.register_group(watch_list_group)
|
||||||
config.register_opts(watch_list_opts, group=watch_list_group)
|
config.register_opts(watch_list_opts, group=watch_list_group)
|
||||||
config.register_group(webchat_group)
|
|
||||||
config.register_opts(webchat_opts, group=webchat_group)
|
|
||||||
config.register_group(registry_group)
|
config.register_group(registry_group)
|
||||||
config.register_opts(registry_opts, group=registry_group)
|
config.register_opts(registry_opts, group=registry_group)
|
||||||
|
|
||||||
@ -271,6 +236,5 @@ def list_opts():
|
|||||||
return {
|
return {
|
||||||
"DEFAULT": (aprsd_opts + enabled_plugins_opts),
|
"DEFAULT": (aprsd_opts + enabled_plugins_opts),
|
||||||
watch_list_group.name: watch_list_opts,
|
watch_list_group.name: watch_list_opts,
|
||||||
webchat_group.name: webchat_opts,
|
|
||||||
registry_group.name: registry_opts,
|
registry_group.name: registry_opts,
|
||||||
}
|
}
|
||||||
|
@ -68,19 +68,9 @@ def setup_logging(loglevel=None, quiet=False):
|
|||||||
"aprslib.parsing",
|
"aprslib.parsing",
|
||||||
"aprslib.exceptions",
|
"aprslib.exceptions",
|
||||||
]
|
]
|
||||||
webserver_list = [
|
|
||||||
"werkzeug",
|
|
||||||
"werkzeug._internal",
|
|
||||||
"socketio",
|
|
||||||
"urllib3.connectionpool",
|
|
||||||
"chardet",
|
|
||||||
"chardet.charsetgroupprober",
|
|
||||||
"chardet.eucjpprober",
|
|
||||||
"chardet.mbcharsetprober",
|
|
||||||
]
|
|
||||||
|
|
||||||
# We don't really want to see the aprslib parsing debug output.
|
# We don't really want to see the aprslib parsing debug output.
|
||||||
disable_list = imap_list + aprslib_list + webserver_list
|
disable_list = imap_list + aprslib_list
|
||||||
|
|
||||||
# remove every other logger's handlers
|
# remove every other logger's handlers
|
||||||
# and propagate to root logger
|
# and propagate to root logger
|
||||||
@ -91,12 +81,6 @@ def setup_logging(loglevel=None, quiet=False):
|
|||||||
else:
|
else:
|
||||||
logging.getLogger(name).propagate = True
|
logging.getLogger(name).propagate = True
|
||||||
|
|
||||||
if CONF.webchat.disable_url_request_logging:
|
|
||||||
for name in webserver_list:
|
|
||||||
logging.getLogger(name).handlers = []
|
|
||||||
logging.getLogger(name).propagate = True
|
|
||||||
logging.getLogger(name).setLevel(logging.ERROR)
|
|
||||||
|
|
||||||
handlers = [
|
handlers = [
|
||||||
{
|
{
|
||||||
"sink": sys.stdout,
|
"sink": sys.stdout,
|
||||||
|
@ -55,7 +55,7 @@ def cli(ctx):
|
|||||||
def load_commands():
|
def load_commands():
|
||||||
from .cmds import ( # noqa
|
from .cmds import ( # noqa
|
||||||
completion, dev, fetch_stats, healthcheck, list_plugins, listen,
|
completion, dev, fetch_stats, healthcheck, list_plugins, listen,
|
||||||
send_message, server, webchat,
|
send_message, server,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,84 +0,0 @@
|
|||||||
body {
|
|
||||||
background: #eeeeee;
|
|
||||||
margin: 2em;
|
|
||||||
text-align: center;
|
|
||||||
font-family: system-ui, sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer {
|
|
||||||
padding: 2em;
|
|
||||||
text-align: center;
|
|
||||||
height: 10vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ui.segment {
|
|
||||||
background: #eeeeee;
|
|
||||||
}
|
|
||||||
|
|
||||||
#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;
|
|
||||||
}
|
|
||||||
#packetsChart, #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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aprsd_1 {
|
|
||||||
background-image: url(/static/images/aprs-symbols-16-0.png);
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-position: -160px -48px;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
}
|
|
@ -1,4 +0,0 @@
|
|||||||
/* PrismJS 1.29.0
|
|
||||||
https://prismjs.com/download.html#themes=prism-tomorrow&languages=markup+css+clike+javascript+json+json5+log&plugins=show-language+toolbar */
|
|
||||||
code[class*=language-],pre[class*=language-]{color:#ccc;background:0 0;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#2d2d2d}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.block-comment,.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#999}.token.punctuation{color:#ccc}.token.attr-name,.token.deleted,.token.namespace,.token.tag{color:#e2777a}.token.function-name{color:#6196cc}.token.boolean,.token.function,.token.number{color:#f08d49}.token.class-name,.token.constant,.token.property,.token.symbol{color:#f8c555}.token.atrule,.token.builtin,.token.important,.token.keyword,.token.selector{color:#cc99cd}.token.attr-value,.token.char,.token.regex,.token.string,.token.variable{color:#7ec699}.token.entity,.token.operator,.token.url{color:#67cdcc}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.token.inserted{color:green}
|
|
||||||
div.code-toolbar{position:relative}div.code-toolbar>.toolbar{position:absolute;z-index:10;top:.3em;right:.2em;transition:opacity .3s ease-in-out;opacity:0}div.code-toolbar:hover>.toolbar{opacity:1}div.code-toolbar:focus-within>.toolbar{opacity:1}div.code-toolbar>.toolbar>.toolbar-item{display:inline-block}div.code-toolbar>.toolbar>.toolbar-item>a{cursor:pointer}div.code-toolbar>.toolbar>.toolbar-item>button{background:0 0;border:0;color:inherit;font:inherit;line-height:normal;overflow:visible;padding:0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}div.code-toolbar>.toolbar>.toolbar-item>a,div.code-toolbar>.toolbar>.toolbar-item>button,div.code-toolbar>.toolbar>.toolbar-item>span{color:#bbb;font-size:.8em;padding:0 .5em;background:#f5f2f0;background:rgba(224,224,224,.2);box-shadow:0 2px 0 0 rgba(0,0,0,.2);border-radius:.5em}div.code-toolbar>.toolbar>.toolbar-item>a:focus,div.code-toolbar>.toolbar>.toolbar-item>a:hover,div.code-toolbar>.toolbar>.toolbar-item>button:focus,div.code-toolbar>.toolbar>.toolbar-item>button:hover,div.code-toolbar>.toolbar>.toolbar-item>span:focus,div.code-toolbar>.toolbar>.toolbar-item>span:hover{color:inherit;text-decoration:none}
|
|
@ -1,35 +0,0 @@
|
|||||||
/* 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;
|
|
||||||
}
|
|
Before Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 40 KiB |
@ -1,235 +0,0 @@
|
|||||||
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)',
|
|
||||||
lightcoral: 'rgb(240,128,128)',
|
|
||||||
darkseagreen: 'rgb(143, 188,143)'
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
function size_dict(d){c=0; for (i in d) ++c; return c}
|
|
||||||
|
|
||||||
function start_charts() {
|
|
||||||
Chart.scaleService.updateScaleDefaults('linear', {
|
|
||||||
ticks: {
|
|
||||||
min: 0
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
packets_chart = new Chart($("#packetsChart"), {
|
|
||||||
label: 'APRS Packets',
|
|
||||||
type: 'line',
|
|
||||||
data: {
|
|
||||||
labels: [],
|
|
||||||
datasets: [{
|
|
||||||
label: 'Packets Sent',
|
|
||||||
borderColor: window.chartColors.lightcoral,
|
|
||||||
data: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Packets Recieved',
|
|
||||||
borderColor: window.chartColors.darkseagreen,
|
|
||||||
data: [],
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
responsive: true,
|
|
||||||
maintainAspectRatio: false,
|
|
||||||
title: {
|
|
||||||
display: true,
|
|
||||||
text: 'APRS Packets',
|
|
||||||
},
|
|
||||||
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.lightcoral,
|
|
||||||
data: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Messages Recieved',
|
|
||||||
borderColor: window.chartColors.darkseagreen,
|
|
||||||
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.lightcoral,
|
|
||||||
data: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Recieved',
|
|
||||||
borderColor: window.chartColors.darkseagreen,
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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 ) {
|
|
||||||
our_callsign = data["APRSDStats"]["callsign"];
|
|
||||||
$("#version").text( data["APRSDStats"]["version"] );
|
|
||||||
$("#aprs_connection").html( data["aprs_connection"] );
|
|
||||||
$("#uptime").text( "uptime: " + data["APRSDStats"]["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];
|
|
||||||
packet_list = data["PacketList"]["packets"];
|
|
||||||
updateDualData(packets_chart, short_time, data["PacketList"]["sent"], data["PacketList"]["received"]);
|
|
||||||
updateQuadData(message_chart, short_time, packet_list["MessagePacket"]["tx"], packet_list["MessagePacket"]["rx"],
|
|
||||||
packet_list["AckPacket"]["tx"], packet_list["AckPacket"]["rx"]);
|
|
||||||
updateDualData(email_chart, short_time, data["EmailStats"]["sent"], data["EmailStats"]["recieved"]);
|
|
||||||
updateDualData(memory_chart, short_time, data["APRSDStats"]["memory_peak"], data["APRSDStats"]["memory_current"]);
|
|
||||||
}
|
|
@ -1,465 +0,0 @@
|
|||||||
var packet_list = {};
|
|
||||||
|
|
||||||
var tx_data = [];
|
|
||||||
var rx_data = [];
|
|
||||||
|
|
||||||
var packet_types_data = {};
|
|
||||||
|
|
||||||
var mem_current = []
|
|
||||||
var mem_peak = []
|
|
||||||
|
|
||||||
var thread_current = []
|
|
||||||
|
|
||||||
|
|
||||||
function start_charts() {
|
|
||||||
console.log("start_charts() called");
|
|
||||||
// Initialize the echarts instance based on the prepared dom
|
|
||||||
create_packets_chart();
|
|
||||||
create_packets_types_chart();
|
|
||||||
create_messages_chart();
|
|
||||||
create_ack_chart();
|
|
||||||
create_memory_chart();
|
|
||||||
create_thread_chart();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function create_packets_chart() {
|
|
||||||
// The packets totals TX/RX chart.
|
|
||||||
pkt_c_canvas = document.getElementById('packetsChart');
|
|
||||||
packets_chart = echarts.init(pkt_c_canvas);
|
|
||||||
|
|
||||||
// Specify the configuration items and data for the chart
|
|
||||||
var option = {
|
|
||||||
title: {
|
|
||||||
text: 'APRS Packet totals'
|
|
||||||
},
|
|
||||||
legend: {},
|
|
||||||
tooltip : {
|
|
||||||
trigger: 'axis'
|
|
||||||
},
|
|
||||||
toolbox: {
|
|
||||||
show : true,
|
|
||||||
feature : {
|
|
||||||
mark : {show: true},
|
|
||||||
dataView : {show: true, readOnly: true},
|
|
||||||
magicType : {show: true, type: ['line', 'bar']},
|
|
||||||
restore : {show: true},
|
|
||||||
saveAsImage : {show: true}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
calculable : true,
|
|
||||||
xAxis: { type: 'time' },
|
|
||||||
yAxis: { },
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
name: 'tx',
|
|
||||||
type: 'line',
|
|
||||||
smooth: true,
|
|
||||||
color: 'red',
|
|
||||||
encode: {
|
|
||||||
x: 'timestamp',
|
|
||||||
y: 'tx' // refer sensor 1 value
|
|
||||||
}
|
|
||||||
},{
|
|
||||||
name: 'rx',
|
|
||||||
type: 'line',
|
|
||||||
smooth: true,
|
|
||||||
encode: {
|
|
||||||
x: 'timestamp',
|
|
||||||
y: 'rx'
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
|
|
||||||
// Display the chart using the configuration items and data just specified.
|
|
||||||
packets_chart.setOption(option);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function create_packets_types_chart() {
|
|
||||||
// The packets types chart
|
|
||||||
pkt_types_canvas = document.getElementById('packetTypesChart');
|
|
||||||
packet_types_chart = echarts.init(pkt_types_canvas);
|
|
||||||
|
|
||||||
// The series and data are built and updated on the fly
|
|
||||||
// as packets come in.
|
|
||||||
var option = {
|
|
||||||
title: {
|
|
||||||
text: 'Packet Types'
|
|
||||||
},
|
|
||||||
legend: {},
|
|
||||||
tooltip : {
|
|
||||||
trigger: 'axis'
|
|
||||||
},
|
|
||||||
toolbox: {
|
|
||||||
show : true,
|
|
||||||
feature : {
|
|
||||||
mark : {show: true},
|
|
||||||
dataView : {show: true, readOnly: true},
|
|
||||||
magicType : {show: true, type: ['line', 'bar']},
|
|
||||||
restore : {show: true},
|
|
||||||
saveAsImage : {show: true}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
calculable : true,
|
|
||||||
xAxis: { type: 'time' },
|
|
||||||
yAxis: { },
|
|
||||||
}
|
|
||||||
|
|
||||||
packet_types_chart.setOption(option);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function create_messages_chart() {
|
|
||||||
msg_c_canvas = document.getElementById('messagesChart');
|
|
||||||
message_chart = echarts.init(msg_c_canvas);
|
|
||||||
|
|
||||||
// Specify the configuration items and data for the chart
|
|
||||||
var option = {
|
|
||||||
title: {
|
|
||||||
text: 'Message Packets'
|
|
||||||
},
|
|
||||||
legend: {},
|
|
||||||
tooltip: {
|
|
||||||
trigger: 'axis'
|
|
||||||
},
|
|
||||||
toolbox: {
|
|
||||||
show: true,
|
|
||||||
feature: {
|
|
||||||
mark : {show: true},
|
|
||||||
dataView : {show: true, readOnly: true},
|
|
||||||
magicType : {show: true, type: ['line', 'bar']},
|
|
||||||
restore : {show: true},
|
|
||||||
saveAsImage : {show: true}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
calculable: true,
|
|
||||||
xAxis: { type: 'time' },
|
|
||||||
yAxis: { },
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
name: 'tx',
|
|
||||||
type: 'line',
|
|
||||||
smooth: true,
|
|
||||||
color: 'red',
|
|
||||||
encode: {
|
|
||||||
x: 'timestamp',
|
|
||||||
y: 'tx' // refer sensor 1 value
|
|
||||||
}
|
|
||||||
},{
|
|
||||||
name: 'rx',
|
|
||||||
type: 'line',
|
|
||||||
smooth: true,
|
|
||||||
encode: {
|
|
||||||
x: 'timestamp',
|
|
||||||
y: 'rx'
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
|
|
||||||
// Display the chart using the configuration items and data just specified.
|
|
||||||
message_chart.setOption(option);
|
|
||||||
}
|
|
||||||
|
|
||||||
function create_ack_chart() {
|
|
||||||
ack_canvas = document.getElementById('acksChart');
|
|
||||||
ack_chart = echarts.init(ack_canvas);
|
|
||||||
|
|
||||||
// Specify the configuration items and data for the chart
|
|
||||||
var option = {
|
|
||||||
title: {
|
|
||||||
text: 'Ack Packets'
|
|
||||||
},
|
|
||||||
legend: {},
|
|
||||||
tooltip: {
|
|
||||||
trigger: 'axis'
|
|
||||||
},
|
|
||||||
toolbox: {
|
|
||||||
show: true,
|
|
||||||
feature: {
|
|
||||||
mark : {show: true},
|
|
||||||
dataView : {show: true, readOnly: false},
|
|
||||||
magicType : {show: true, type: ['line', 'bar']},
|
|
||||||
restore : {show: true},
|
|
||||||
saveAsImage : {show: true}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
calculable: true,
|
|
||||||
xAxis: { type: 'time' },
|
|
||||||
yAxis: { },
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
name: 'tx',
|
|
||||||
type: 'line',
|
|
||||||
smooth: true,
|
|
||||||
color: 'red',
|
|
||||||
encode: {
|
|
||||||
x: 'timestamp',
|
|
||||||
y: 'tx' // refer sensor 1 value
|
|
||||||
}
|
|
||||||
},{
|
|
||||||
name: 'rx',
|
|
||||||
type: 'line',
|
|
||||||
smooth: true,
|
|
||||||
encode: {
|
|
||||||
x: 'timestamp',
|
|
||||||
y: 'rx'
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
|
|
||||||
ack_chart.setOption(option);
|
|
||||||
}
|
|
||||||
|
|
||||||
function create_memory_chart() {
|
|
||||||
ack_canvas = document.getElementById('memChart');
|
|
||||||
memory_chart = echarts.init(ack_canvas);
|
|
||||||
|
|
||||||
// Specify the configuration items and data for the chart
|
|
||||||
var option = {
|
|
||||||
title: {
|
|
||||||
text: 'Memory Usage'
|
|
||||||
},
|
|
||||||
legend: {},
|
|
||||||
tooltip: {
|
|
||||||
trigger: 'axis'
|
|
||||||
},
|
|
||||||
toolbox: {
|
|
||||||
show: true,
|
|
||||||
feature: {
|
|
||||||
mark : {show: true},
|
|
||||||
dataView : {show: true, readOnly: false},
|
|
||||||
magicType : {show: true, type: ['line', 'bar']},
|
|
||||||
restore : {show: true},
|
|
||||||
saveAsImage : {show: true}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
calculable: true,
|
|
||||||
xAxis: { type: 'time' },
|
|
||||||
yAxis: { },
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
name: 'current',
|
|
||||||
type: 'line',
|
|
||||||
smooth: true,
|
|
||||||
color: 'red',
|
|
||||||
encode: {
|
|
||||||
x: 'timestamp',
|
|
||||||
y: 'current' // refer sensor 1 value
|
|
||||||
}
|
|
||||||
},{
|
|
||||||
name: 'peak',
|
|
||||||
type: 'line',
|
|
||||||
smooth: true,
|
|
||||||
encode: {
|
|
||||||
x: 'timestamp',
|
|
||||||
y: 'peak'
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
|
|
||||||
memory_chart.setOption(option);
|
|
||||||
}
|
|
||||||
|
|
||||||
function create_thread_chart() {
|
|
||||||
thread_canvas = document.getElementById('threadChart');
|
|
||||||
thread_chart = echarts.init(thread_canvas);
|
|
||||||
|
|
||||||
// Specify the configuration items and data for the chart
|
|
||||||
var option = {
|
|
||||||
title: {
|
|
||||||
text: 'Active Threads'
|
|
||||||
},
|
|
||||||
legend: {},
|
|
||||||
tooltip: {
|
|
||||||
trigger: 'axis'
|
|
||||||
},
|
|
||||||
toolbox: {
|
|
||||||
show: true,
|
|
||||||
feature: {
|
|
||||||
mark : {show: true},
|
|
||||||
dataView : {show: true, readOnly: false},
|
|
||||||
magicType : {show: true, type: ['line', 'bar']},
|
|
||||||
restore : {show: true},
|
|
||||||
saveAsImage : {show: true}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
calculable: true,
|
|
||||||
xAxis: { type: 'time' },
|
|
||||||
yAxis: { },
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
name: 'current',
|
|
||||||
type: 'line',
|
|
||||||
smooth: true,
|
|
||||||
color: 'red',
|
|
||||||
encode: {
|
|
||||||
x: 'timestamp',
|
|
||||||
y: 'current' // refer sensor 1 value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
thread_chart.setOption(option);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function updatePacketData(chart, time, first, second) {
|
|
||||||
tx_data.push([time, first]);
|
|
||||||
rx_data.push([time, second]);
|
|
||||||
option = {
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
name: 'tx',
|
|
||||||
data: tx_data,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'rx',
|
|
||||||
data: rx_data,
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
chart.setOption(option);
|
|
||||||
}
|
|
||||||
|
|
||||||
function updatePacketTypesData(time, typesdata) {
|
|
||||||
//The options series is created on the fly each time based on
|
|
||||||
//the packet types we have in the data
|
|
||||||
var series = []
|
|
||||||
|
|
||||||
for (const k in typesdata) {
|
|
||||||
tx = [time, typesdata[k]["tx"]]
|
|
||||||
rx = [time, typesdata[k]["rx"]]
|
|
||||||
|
|
||||||
if (packet_types_data.hasOwnProperty(k)) {
|
|
||||||
packet_types_data[k]["tx"].push(tx)
|
|
||||||
packet_types_data[k]["rx"].push(rx)
|
|
||||||
} else {
|
|
||||||
packet_types_data[k] = {'tx': [tx], 'rx': [rx]}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updatePacketTypesChart() {
|
|
||||||
series = []
|
|
||||||
for (const k in packet_types_data) {
|
|
||||||
entry = {
|
|
||||||
name: k+"tx",
|
|
||||||
data: packet_types_data[k]["tx"],
|
|
||||||
type: 'line',
|
|
||||||
smooth: true,
|
|
||||||
encode: {
|
|
||||||
x: 'timestamp',
|
|
||||||
y: k+'tx' // refer sensor 1 value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
series.push(entry)
|
|
||||||
entry = {
|
|
||||||
name: k+"rx",
|
|
||||||
data: packet_types_data[k]["rx"],
|
|
||||||
type: 'line',
|
|
||||||
smooth: true,
|
|
||||||
encode: {
|
|
||||||
x: 'timestamp',
|
|
||||||
y: k+'rx' // refer sensor 1 value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
series.push(entry)
|
|
||||||
}
|
|
||||||
|
|
||||||
option = {
|
|
||||||
series: series
|
|
||||||
}
|
|
||||||
packet_types_chart.setOption(option);
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateTypeChart(chart, key) {
|
|
||||||
//Generic function to update a packet type chart
|
|
||||||
if (! packet_types_data.hasOwnProperty(key)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! packet_types_data[key].hasOwnProperty('tx')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var option = {
|
|
||||||
series: [{
|
|
||||||
name: "tx",
|
|
||||||
data: packet_types_data[key]["tx"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "rx",
|
|
||||||
data: packet_types_data[key]["rx"]
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
|
|
||||||
chart.setOption(option);
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateMemChart(time, current, peak) {
|
|
||||||
mem_current.push([time, current]);
|
|
||||||
mem_peak.push([time, peak]);
|
|
||||||
option = {
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
name: 'current',
|
|
||||||
data: mem_current,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'peak',
|
|
||||||
data: mem_peak,
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
memory_chart.setOption(option);
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateThreadChart(time, threads) {
|
|
||||||
keys = Object.keys(threads);
|
|
||||||
thread_count = keys.length;
|
|
||||||
thread_current.push([time, thread_count]);
|
|
||||||
option = {
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
name: 'current',
|
|
||||||
data: thread_current,
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
thread_chart.setOption(option);
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateMessagesChart() {
|
|
||||||
updateTypeChart(message_chart, "MessagePacket")
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateAcksChart() {
|
|
||||||
updateTypeChart(ack_chart, "AckPacket")
|
|
||||||
}
|
|
||||||
|
|
||||||
function update_stats( data ) {
|
|
||||||
console.log("update_stats() echarts.js called")
|
|
||||||
stats = data["stats"];
|
|
||||||
our_callsign = stats["APRSDStats"]["callsign"];
|
|
||||||
$("#version").text( stats["APRSDStats"]["version"] );
|
|
||||||
$("#aprs_connection").html( stats["aprs_connection"] );
|
|
||||||
$("#uptime").text( "uptime: " + stats["APRSDStats"]["uptime"] );
|
|
||||||
const html_pretty = Prism.highlight(JSON.stringify(data, null, '\t'), Prism.languages.json, 'json');
|
|
||||||
$("#jsonstats").html(html_pretty);
|
|
||||||
|
|
||||||
t = Date.parse(data["time"]);
|
|
||||||
ts = new Date(t);
|
|
||||||
updatePacketData(packets_chart, ts, stats["PacketList"]["tx"], stats["PacketList"]["rx"]);
|
|
||||||
updatePacketTypesData(ts, stats["PacketList"]["types"]);
|
|
||||||
updatePacketTypesChart();
|
|
||||||
updateMessagesChart();
|
|
||||||
updateAcksChart();
|
|
||||||
updateMemChart(ts, stats["APRSDStats"]["memory_current"], stats["APRSDStats"]["memory_peak"]);
|
|
||||||
updateThreadChart(ts, stats["APRSDThreadList"]);
|
|
||||||
//updateQuadData(message_chart, short_time, data["stats"]["messages"]["sent"], data["stats"]["messages"]["received"], 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"]);
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
function init_logs() {
|
|
||||||
const socket = io("/logs");
|
|
||||||
socket.on('connect', function () {
|
|
||||||
console.log("Connected to logs socketio");
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('connected', function(msg) {
|
|
||||||
console.log("Connected to /logs");
|
|
||||||
console.log(msg);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('log_entry', function(data) {
|
|
||||||
update_logs(data);
|
|
||||||
});
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
function update_logs(data) {
|
|
||||||
var code_block = $('#logtext')
|
|
||||||
entry = data["message"]
|
|
||||||
const html_pretty = Prism.highlight(entry, Prism.languages.log, 'log');
|
|
||||||
code_block.append(html_pretty + "<br>");
|
|
||||||
var div = document.getElementById('logContainer');
|
|
||||||
div.scrollTop = div.scrollHeight;
|
|
||||||
}
|
|
@ -1,231 +0,0 @@
|
|||||||
// watchlist is a dict of ham callsign => symbol, packets
|
|
||||||
var watchlist = {};
|
|
||||||
var our_callsign = "";
|
|
||||||
|
|
||||||
function aprs_img(item, x_offset, y_offset) {
|
|
||||||
var x = x_offset * -16;
|
|
||||||
if (y_offset > 5) {
|
|
||||||
y_offset = 5;
|
|
||||||
}
|
|
||||||
var y = y_offset * -16;
|
|
||||||
var loc = x + 'px '+ y + 'px'
|
|
||||||
item.css('background-position', loc);
|
|
||||||
}
|
|
||||||
|
|
||||||
function show_aprs_icon(item, symbol) {
|
|
||||||
var offset = ord(symbol) - 33;
|
|
||||||
var col = Math.floor(offset / 16);
|
|
||||||
var row = offset % 16;
|
|
||||||
//console.log("'" + symbol+"' off: "+offset+" row: "+ row + " col: " + col)
|
|
||||||
aprs_img(item, row, col);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ord(str){return str.charCodeAt(0);}
|
|
||||||
|
|
||||||
|
|
||||||
function update_watchlist( data ) {
|
|
||||||
// Update the watch list
|
|
||||||
stats = data["stats"];
|
|
||||||
if (stats.hasOwnProperty("WatchList") == false) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var watchdiv = $("#watchDiv");
|
|
||||||
var html_str = '<table class="ui celled striped table"><thead><tr><th>HAM Callsign</th><th>Age since last seen by APRSD</th></tr></thead><tbody>'
|
|
||||||
watchdiv.html('')
|
|
||||||
jQuery.each(stats["WatchList"], function(i, val) {
|
|
||||||
html_str += '<tr><td class="collapsing"><img id="callsign_'+i+'" class="aprsd_1"></img>' + i + '</td><td>' + val["last"] + '</td></tr>'
|
|
||||||
});
|
|
||||||
html_str += "</tbody></table>";
|
|
||||||
watchdiv.append(html_str);
|
|
||||||
|
|
||||||
jQuery.each(watchlist, function(i, val) {
|
|
||||||
//update the symbol
|
|
||||||
var call_img = $('#callsign_'+i);
|
|
||||||
show_aprs_icon(call_img, val['symbol'])
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function update_watchlist_from_packet(callsign, val) {
|
|
||||||
if (!watchlist.hasOwnProperty(callsign)) {
|
|
||||||
watchlist[callsign] = {
|
|
||||||
"symbol": '[',
|
|
||||||
"packets": {},
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (val.hasOwnProperty('symbol')) {
|
|
||||||
//console.log("Updating symbol for "+callsign + " to "+val["symbol"])
|
|
||||||
watchlist[callsign]["symbol"] = val["symbol"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (watchlist[callsign]["packets"].hasOwnProperty(val['ts']) == false) {
|
|
||||||
watchlist[callsign]["packets"][val['ts']]= val;
|
|
||||||
}
|
|
||||||
//console.log(watchlist)
|
|
||||||
}
|
|
||||||
|
|
||||||
function update_seenlist( data ) {
|
|
||||||
stats = data["stats"];
|
|
||||||
if (stats.hasOwnProperty("SeenList") == false) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var seendiv = $("#seenDiv");
|
|
||||||
var html_str = '<table class="ui celled striped table">'
|
|
||||||
html_str += '<thead><tr><th>HAM Callsign</th><th>Age since last seen by APRSD</th>'
|
|
||||||
html_str += '<th>Number of packets RX</th></tr></thead><tbody>'
|
|
||||||
seendiv.html('')
|
|
||||||
var seen_list = stats["SeenList"]
|
|
||||||
var len = Object.keys(seen_list).length
|
|
||||||
$('#seen_count').html(len)
|
|
||||||
jQuery.each(seen_list, function(i, val) {
|
|
||||||
html_str += '<tr><td class="collapsing">'
|
|
||||||
html_str += '<img id="callsign_'+i+'" class="aprsd_1"></img>' + i + '</td>'
|
|
||||||
html_str += '<td>' + val["last"] + '</td>'
|
|
||||||
html_str += '<td>' + val["count"] + '</td></tr>'
|
|
||||||
});
|
|
||||||
html_str += "</tbody></table>";
|
|
||||||
seendiv.append(html_str);
|
|
||||||
}
|
|
||||||
|
|
||||||
function update_plugins( data ) {
|
|
||||||
stats = data["stats"];
|
|
||||||
if (stats.hasOwnProperty("PluginManager") == false) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var plugindiv = $("#pluginDiv");
|
|
||||||
var html_str = '<table class="ui celled striped table"><thead><tr>'
|
|
||||||
html_str += '<th>Plugin Name</th><th>Plugin Enabled?</th>'
|
|
||||||
html_str += '<th>Processed Packets</th><th>Sent Packets</th>'
|
|
||||||
html_str += '<th>Version</th>'
|
|
||||||
html_str += '</tr></thead><tbody>'
|
|
||||||
plugindiv.html('')
|
|
||||||
|
|
||||||
var plugins = stats["PluginManager"];
|
|
||||||
var keys = Object.keys(plugins);
|
|
||||||
keys.sort();
|
|
||||||
for (var i=0; i<keys.length; i++) { // now lets iterate in sort order
|
|
||||||
var key = keys[i];
|
|
||||||
var val = plugins[key];
|
|
||||||
html_str += '<tr><td class="collapsing">' + key + '</td>';
|
|
||||||
html_str += '<td>' + val["enabled"] + '</td><td>' + val["rx"] + '</td>';
|
|
||||||
html_str += '<td>' + val["tx"] + '</td><td>' + val["version"] +'</td></tr>';
|
|
||||||
}
|
|
||||||
html_str += "</tbody></table>";
|
|
||||||
plugindiv.append(html_str);
|
|
||||||
}
|
|
||||||
|
|
||||||
function update_threads( data ) {
|
|
||||||
stats = data["stats"];
|
|
||||||
if (stats.hasOwnProperty("APRSDThreadList") == false) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var threadsdiv = $("#threadsDiv");
|
|
||||||
var countdiv = $("#thread_count");
|
|
||||||
var html_str = '<table class="ui celled striped table"><thead><tr>'
|
|
||||||
html_str += '<th>Thread Name</th><th>Alive?</th>'
|
|
||||||
html_str += '<th>Age</th><th>Loop Count</th>'
|
|
||||||
html_str += '</tr></thead><tbody>'
|
|
||||||
threadsdiv.html('')
|
|
||||||
|
|
||||||
var threads = stats["APRSDThreadList"];
|
|
||||||
var keys = Object.keys(threads);
|
|
||||||
countdiv.html(keys.length);
|
|
||||||
keys.sort();
|
|
||||||
for (var i=0; i<keys.length; i++) { // now lets iterate in sort order
|
|
||||||
var key = keys[i];
|
|
||||||
var val = threads[key];
|
|
||||||
html_str += '<tr><td class="collapsing">' + key + '</td>';
|
|
||||||
html_str += '<td>' + val["alive"] + '</td><td>' + val["age"] + '</td>';
|
|
||||||
html_str += '<td>' + val["loop_count"] + '</td></tr>';
|
|
||||||
}
|
|
||||||
html_str += "</tbody></table>";
|
|
||||||
threadsdiv.append(html_str);
|
|
||||||
}
|
|
||||||
|
|
||||||
function update_packets( data ) {
|
|
||||||
var packetsdiv = $("#packetsDiv");
|
|
||||||
//nuke the contents first, then add to it.
|
|
||||||
if (size_dict(packet_list) == 0 && size_dict(data) > 0) {
|
|
||||||
packetsdiv.html('')
|
|
||||||
}
|
|
||||||
jQuery.each(data.packets, function(i, val) {
|
|
||||||
pkt = val;
|
|
||||||
|
|
||||||
update_watchlist_from_packet(pkt['from_call'], pkt);
|
|
||||||
if ( packet_list.hasOwnProperty(pkt['timestamp']) == false ) {
|
|
||||||
// Store the packet
|
|
||||||
packet_list[pkt['timestamp']] = pkt;
|
|
||||||
//ts_str = val["timestamp"].toString();
|
|
||||||
//ts = ts_str.split(".")[0]*1000;
|
|
||||||
ts = pkt['timestamp'] * 1000;
|
|
||||||
var d = new Date(ts).toLocaleDateString();
|
|
||||||
var t = new Date(ts).toLocaleTimeString();
|
|
||||||
var from_call = pkt.from_call;
|
|
||||||
if (from_call == our_callsign) {
|
|
||||||
title_id = 'title_tx';
|
|
||||||
} else {
|
|
||||||
title_id = 'title_rx';
|
|
||||||
}
|
|
||||||
var from_to = d + " " + t + " " + from_call + " > "
|
|
||||||
|
|
||||||
if (val.hasOwnProperty('addresse')) {
|
|
||||||
from_to = from_to + pkt['addresse']
|
|
||||||
} else if (pkt.hasOwnProperty('to_call')) {
|
|
||||||
from_to = from_to + pkt['to_call']
|
|
||||||
} else if (pkt.hasOwnProperty('format') && pkt['format'] == 'mic-e') {
|
|
||||||
from_to = from_to + "Mic-E"
|
|
||||||
}
|
|
||||||
|
|
||||||
from_to = from_to + " - " + pkt['raw']
|
|
||||||
|
|
||||||
json_pretty = Prism.highlight(JSON.stringify(pkt, 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');
|
|
||||||
|
|
||||||
// Update the count of messages shown
|
|
||||||
cnt = size_dict(packet_list);
|
|
||||||
//console.log("packets list " + cnt)
|
|
||||||
$('#packets_count').html(cnt);
|
|
||||||
|
|
||||||
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);
|
|
||||||
update_watchlist(data);
|
|
||||||
update_seenlist(data);
|
|
||||||
update_plugins(data);
|
|
||||||
update_threads(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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
}
|
|
@ -1,114 +0,0 @@
|
|||||||
var cleared = false;
|
|
||||||
|
|
||||||
function size_dict(d){c=0; for (i in d) ++c; return c}
|
|
||||||
|
|
||||||
function init_messages() {
|
|
||||||
const socket = io("/sendmsg");
|
|
||||||
socket.on('connect', function () {
|
|
||||||
console.log("Connected to socketio");
|
|
||||||
});
|
|
||||||
socket.on('connected', function(msg) {
|
|
||||||
console.log("Connected!");
|
|
||||||
console.log(msg);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("sent", function(msg) {
|
|
||||||
if (cleared == false) {
|
|
||||||
var msgsdiv = $("#msgsDiv");
|
|
||||||
msgsdiv.html('')
|
|
||||||
cleared = true
|
|
||||||
}
|
|
||||||
add_msg(msg);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("ack", function(msg) {
|
|
||||||
update_msg(msg);
|
|
||||||
});
|
|
||||||
socket.on("reply", function(msg) {
|
|
||||||
update_msg(msg);
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function add_msg(msg) {
|
|
||||||
var msgsdiv = $("#sendMsgsDiv");
|
|
||||||
|
|
||||||
ts_str = msg["ts"].toString();
|
|
||||||
ts = ts_str.split(".")[0]*1000;
|
|
||||||
var d = new Date(ts).toLocaleDateString("en-US")
|
|
||||||
var t = new Date(ts).toLocaleTimeString("en-US")
|
|
||||||
|
|
||||||
from = msg['from']
|
|
||||||
title_id = 'title_tx'
|
|
||||||
var from_to = d + " " + t + " " + from + " > "
|
|
||||||
|
|
||||||
if (msg.hasOwnProperty('to')) {
|
|
||||||
from_to = from_to + msg['to']
|
|
||||||
}
|
|
||||||
from_to = from_to + " - " + msg['message']
|
|
||||||
|
|
||||||
id = ts_str.split('.')[0]
|
|
||||||
pretty_id = "pretty_" + id
|
|
||||||
loader_id = "loader_" + id
|
|
||||||
ack_id = "ack_" + id
|
|
||||||
reply_id = "reply_" + id
|
|
||||||
span_id = "span_" + id
|
|
||||||
json_pretty = Prism.highlight(JSON.stringify(msg, null, '\t'), Prism.languages.json, 'json');
|
|
||||||
msg_html = '<div class="ui title" id="' + title_id + '"><i class="dropdown icon"></i>';
|
|
||||||
msg_html += '<div class="ui active inline loader" id="' + loader_id +'" data-content="Waiting for Ack"></div> ';
|
|
||||||
msg_html += '<i class="thumbs down outline icon" id="' + ack_id + '" data-content="Waiting for ACK"></i> ';
|
|
||||||
msg_html += '<i class="thumbs down outline icon" id="' + reply_id + '" data-content="Waiting for Reply"></i> ';
|
|
||||||
msg_html += '<span id="' + span_id + '">' + from_to +'</span></div>';
|
|
||||||
msg_html += '<div class="content"><p class="transition hidden"><pre id="' + pretty_id + '" class="language-json">' + json_pretty + '</p></p></div>'
|
|
||||||
msgsdiv.prepend(msg_html);
|
|
||||||
$('.ui.accordion').accordion('refresh');
|
|
||||||
}
|
|
||||||
|
|
||||||
function update_msg(msg) {
|
|
||||||
var msgsdiv = $("#sendMsgsDiv");
|
|
||||||
// We have an existing entry
|
|
||||||
ts_str = msg["ts"].toString();
|
|
||||||
id = ts_str.split('.')[0]
|
|
||||||
pretty_id = "pretty_" + id
|
|
||||||
loader_id = "loader_" + id
|
|
||||||
reply_id = "reply_" + id
|
|
||||||
ack_id = "ack_" + id
|
|
||||||
span_id = "span_" + id
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (msg['ack'] == true) {
|
|
||||||
var loader_div = $('#' + loader_id);
|
|
||||||
var ack_div = $('#' + ack_id);
|
|
||||||
loader_div.removeClass('ui active inline loader');
|
|
||||||
loader_div.addClass('ui disabled loader');
|
|
||||||
ack_div.removeClass('thumbs up outline icon');
|
|
||||||
ack_div.addClass('thumbs up outline icon');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (msg['reply'] !== null) {
|
|
||||||
var reply_div = $('#' + reply_id);
|
|
||||||
reply_div.removeClass("thumbs down outline icon");
|
|
||||||
reply_div.addClass('reply icon');
|
|
||||||
reply_div.attr('data-content', 'Got Reply');
|
|
||||||
|
|
||||||
var d = new Date(ts).toLocaleDateString("en-US")
|
|
||||||
var t = new Date(ts).toLocaleTimeString("en-US")
|
|
||||||
var from_to = d + " " + t + " " + from + " > "
|
|
||||||
|
|
||||||
if (msg.hasOwnProperty('to')) {
|
|
||||||
from_to = from_to + msg['to']
|
|
||||||
}
|
|
||||||
from_to = from_to + " - " + msg['message']
|
|
||||||
from_to += " ===> " + msg["reply"]["message_text"]
|
|
||||||
|
|
||||||
var span_div = $('#' + span_id);
|
|
||||||
span_div.html(from_to);
|
|
||||||
}
|
|
||||||
|
|
||||||
var pretty_pre = $("#" + pretty_id);
|
|
||||||
pretty_pre.html('');
|
|
||||||
json_pretty = Prism.highlight(JSON.stringify(msg, null, '\t'), Prism.languages.json, 'json');
|
|
||||||
pretty_pre.html(json_pretty);
|
|
||||||
$('.ui.accordion').accordion('refresh');
|
|
||||||
}
|
|
@ -1,28 +0,0 @@
|
|||||||
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,196 +0,0 @@
|
|||||||
<html>
|
|
||||||
<head>
|
|
||||||
<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">
|
|
||||||
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
|
|
||||||
<script src="https://cdn.socket.io/4.7.1/socket.io.min.js" integrity="sha512-+NaO7d6gQ1YPxvc/qHIqZEchjGm207SszoNeMgppoqD/67fEqmc1edS8zrbxPD+4RQI3gDgT/83ihpFW61TG/Q==" crossorigin="anonymous"></script>
|
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@2.9.4/dist/Chart.bundle.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.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="/static/css/index.css">
|
|
||||||
<link rel="stylesheet" href="/static/css/tabs.css">
|
|
||||||
<link rel="stylesheet" href="/static/css/prism.css">
|
|
||||||
<script src="/static/js/prism.js"></script>
|
|
||||||
<script src="/static/js/main.js"></script>
|
|
||||||
<script src="/static/js/echarts.js"></script>
|
|
||||||
<script src="/static/js/tabs.js"></script>
|
|
||||||
<script src="/static/js/send-message.js"></script>
|
|
||||||
<script src="/static/js/logs.js"></script>
|
|
||||||
|
|
||||||
|
|
||||||
<script type="text/javascript">
|
|
||||||
var initial_stats = {{ initial_stats|tojson|safe }};
|
|
||||||
|
|
||||||
var memory_chart = null
|
|
||||||
var message_chart = null
|
|
||||||
var color = Chart.helpers.color;
|
|
||||||
|
|
||||||
$(document).ready(function() {
|
|
||||||
start_update();
|
|
||||||
start_charts();
|
|
||||||
init_messages();
|
|
||||||
init_logs();
|
|
||||||
|
|
||||||
$("#toggleStats").click(function() {
|
|
||||||
$("#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);
|
|
||||||
$("#jsonstats").fadeToggle(1000);
|
|
||||||
|
|
||||||
//var log_text_pretty = $('#logtext').text();
|
|
||||||
//const log_pretty = Prism.highlight( log_text_pretty, Prism.languages.log, 'log');
|
|
||||||
//$('#logtext').html(log_pretty);
|
|
||||||
|
|
||||||
$('.ui.accordion').accordion({exclusive: false});
|
|
||||||
$('.menu .item').tab('change tab', 'charts-tab');
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div class='ui text container'>
|
|
||||||
<h1 class='ui dividing header'>APRSD {{ version }}</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class='ui grid text container'>
|
|
||||||
<div class='left floated ten wide column'>
|
|
||||||
<span style='color: green'>{{ callsign }}</span>
|
|
||||||
connected to
|
|
||||||
<span style='color: blue' id='aprs_connection'>{{ aprs_connection|safe }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class='right floated four wide column'>
|
|
||||||
<span id='uptime'>NONE</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Tab links -->
|
|
||||||
<div class="ui top attached 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="seen-tab">Seen List</div>
|
|
||||||
<div class="item" data-tab="watch-tab">Watch List</div>
|
|
||||||
<div class="item" data-tab="plugin-tab">Plugins</div>
|
|
||||||
<div class="item" data-tab="threads-tab">Threads</div>
|
|
||||||
<div class="item" data-tab="config-tab">Config</div>
|
|
||||||
<div class="item" data-tab="log-tab">LogFile</div>
|
|
||||||
<!-- <div class="item" data-tab="oslo-tab">OSLO CONFIG</div> //-->
|
|
||||||
<div class="item" data-tab="raw-tab">Raw JSON</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Tab content -->
|
|
||||||
<div class="ui bottom attached active tab segment" data-tab="charts-tab">
|
|
||||||
<h3 class="ui dividing header">Charts</h3>
|
|
||||||
<div class="ui equal width relaxed grid">
|
|
||||||
<div class="row">
|
|
||||||
<div class="column">
|
|
||||||
<div class="ui segment" style="height: 300px" id="packetsChart"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="column">
|
|
||||||
<div class="ui segment" style="height: 300px" id="messagesChart"></div>
|
|
||||||
</div>
|
|
||||||
<div class="column">
|
|
||||||
<div class="ui segment" style="height: 300px" id="acksChart"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="column">
|
|
||||||
<div class="ui segment" style="height: 300px" id="packetTypesChart"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="column">
|
|
||||||
<div class="ui segment" style="height: 300px" id="threadChart"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="column">
|
|
||||||
<div class="ui segment" style="height: 300px" id="memChart"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- <div class="row">
|
|
||||||
<div id="stats" class="two column">
|
|
||||||
<button class="ui button" id="toggleStats">Toggle raw json</button>
|
|
||||||
<pre id="jsonstats" class="language-json">{{ stats }}</pre>
|
|
||||||
</div> //-->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="ui bottom attached tab segment" data-tab="msgs-tab">
|
|
||||||
<h3 class="ui dividing header">Messages (<span id="packets_count">0</span>)</h3>
|
|
||||||
<div class="ui styled fluid accordion" id="accordion">
|
|
||||||
<div id="packetsDiv" class="ui mini text">Loading</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="ui bottom attached tab segment" data-tab="seen-tab">
|
|
||||||
<h3 class="ui dividing header">
|
|
||||||
Callsign Seen List (<span id="seen_count">{{ seen_count }}</span>)
|
|
||||||
</h3>
|
|
||||||
<div id="seenDiv" class="ui mini text">Loading</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="ui bottom attached tab segment" data-tab="watch-tab">
|
|
||||||
<h3 class="ui dividing header">
|
|
||||||
Callsign Watch List (<span id="watch_count">{{ watch_count }}</span>)
|
|
||||||
|
|
||||||
Notification age - <span id="watch_age">{{ watch_age }}</span>
|
|
||||||
</h3>
|
|
||||||
<div id="watchDiv" class="ui mini text">Loading</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="ui bottom attached tab segment" data-tab="plugin-tab">
|
|
||||||
<h3 class="ui dividing header">
|
|
||||||
Plugins Loaded (<span id="plugin_count">{{ plugin_count }}</span>)
|
|
||||||
</h3>
|
|
||||||
<div id="pluginDiv" class="ui mini text">Loading</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="ui bottom attached tab segment" data-tab="threads-tab">
|
|
||||||
<h3 class="ui dividing header">
|
|
||||||
Threads Loaded (<span id="thread_count">{{ thread_count }}</span>)
|
|
||||||
</h3>
|
|
||||||
<div id="threadsDiv" class="ui mini text">Loading</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="ui bottom attached tab segment" data-tab="config-tab">
|
|
||||||
<h3 class="ui dividing header">Config</h3>
|
|
||||||
<pre id="configjson" class="language-json">{{ config_json|safe }}</pre>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="ui bottom attached tab segment" data-tab="log-tab">
|
|
||||||
<h3 class="ui dividing header">LOGFILE</h3>
|
|
||||||
<pre id="logContainer" style="height: 600px;overflow-y:auto;overflow-x:auto;"><code id="logtext" class="language-log" ></code></pre>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!--
|
|
||||||
<div class="ui bottom attached tab segment" data-tab="oslo-tab">
|
|
||||||
<h3 class="ui dividing header">OSLO</h3>
|
|
||||||
<pre id="osloContainer" style="height:600px;overflow-y:auto;" class="language-json">{{ oslo_out|safe }}</pre>
|
|
||||||
</div> //-->
|
|
||||||
|
|
||||||
<div class="ui bottom attached tab segment" data-tab="raw-tab">
|
|
||||||
<h3 class="ui dividing header">Raw JSON</h3>
|
|
||||||
<pre id="jsonstats" class="language-yaml" style="height:600px;overflow-y:auto;">{{ initial_stats|safe }}</pre>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="ui text container">
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,115 +0,0 @@
|
|||||||
input[type=search]::-webkit-search-cancel-button {
|
|
||||||
-webkit-appearance: searchfield-cancel-button;
|
|
||||||
}
|
|
||||||
|
|
||||||
.speech-wrapper {
|
|
||||||
padding-top: 0px;
|
|
||||||
padding: 5px 30px;
|
|
||||||
background-color: #CCCCCC;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bubble-row {
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bubble-row.alt {
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bubble {
|
|
||||||
/*width: 350px; */
|
|
||||||
height: auto;
|
|
||||||
display: block;
|
|
||||||
background: #f5f5f5;
|
|
||||||
border-radius: 4px;
|
|
||||||
box-shadow: 2px 8px 5px #555;
|
|
||||||
position: relative;
|
|
||||||
margin: 0 0 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bubble.alt {
|
|
||||||
margin: 0 0 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bubble-text {
|
|
||||||
padding: 5px 5px 0px 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bubble-name {
|
|
||||||
width: 280px;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 12px;
|
|
||||||
margin: 0 0 0px;
|
|
||||||
color: #3498db;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
.material-symbols-rounded {
|
|
||||||
margin-left: auto;
|
|
||||||
font-weight: normal;
|
|
||||||
color: #808080;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.bubble-name.alt {
|
|
||||||
color: #2ecc71;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bubble-timestamp {
|
|
||||||
margin-right: auto;
|
|
||||||
font-size: 11px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: #bbb
|
|
||||||
}
|
|
||||||
|
|
||||||
.bubble-message {
|
|
||||||
font-size: 16px;
|
|
||||||
margin: 0px;
|
|
||||||
padding: 0px 0px 0px 0px;
|
|
||||||
color: #2b2b2b;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bubble-arrow {
|
|
||||||
position: absolute;
|
|
||||||
width: 0;
|
|
||||||
bottom:30px;
|
|
||||||
left: -16px;
|
|
||||||
height: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bubble-arrow.alt {
|
|
||||||
right: -2px;
|
|
||||||
bottom: 30px;
|
|
||||||
left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bubble-arrow:after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
border: 0 solid transparent;
|
|
||||||
border-top: 9px solid #f5f5f5;
|
|
||||||
border-radius: 0 20px 0;
|
|
||||||
width: 15px;
|
|
||||||
height: 30px;
|
|
||||||
transform: rotate(145deg);
|
|
||||||
}
|
|
||||||
.bubble-arrow.alt:after {
|
|
||||||
transform: rotate(45deg) scaleY(-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.popover {
|
|
||||||
max-width: 400px;
|
|
||||||
}
|
|
||||||
.popover-header {
|
|
||||||
font-size: 8pt;
|
|
||||||
max-width: 400px;
|
|
||||||
padding: 5px;
|
|
||||||
background-color: #ee;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popover-body {
|
|
||||||
white-space: pre-line;
|
|
||||||
max-width: 400px;
|
|
||||||
padding: 5px;
|
|
||||||
}
|
|
@ -1,66 +0,0 @@
|
|||||||
body {
|
|
||||||
background: #eeeeee;
|
|
||||||
/*margin: 1em;*/
|
|
||||||
text-align: center;
|
|
||||||
font-family: system-ui, sans-serif;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aprsd_1 {
|
|
||||||
background-image: url(/static/images/aprs-symbols-16-0.png);
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-position: -160px -48px;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wc-container {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: column;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
.wc-container .wc-row {
|
|
||||||
/*border: 1px dotted #0313fc;*/
|
|
||||||
padding: 2px;
|
|
||||||
}
|
|
||||||
.wc-container .wc-row.header {
|
|
||||||
flex: 0 1 auto;
|
|
||||||
}
|
|
||||||
.wc-container .wc-row.content {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
.wc-container .wc-row.footer {
|
|
||||||
flex: 0 1 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.material-symbols-rounded.md-10 {
|
|
||||||
font-size: 18px !important;
|
|
||||||
}
|
|
@ -1,41 +0,0 @@
|
|||||||
* {box-sizing: border-box}
|
|
||||||
|
|
||||||
/* Style the tab */
|
|
||||||
.tab {
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
background-color: #f1f1f1;
|
|
||||||
height: 450px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Style the buttons inside the tab */
|
|
||||||
.tab div {
|
|
||||||
display: block;
|
|
||||||
background-color: inherit;
|
|
||||||
color: black;
|
|
||||||
padding: 10px;
|
|
||||||
width: 100%;
|
|
||||||
border: none;
|
|
||||||
outline: none;
|
|
||||||
text-align: left;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: 0.3s;
|
|
||||||
font-size: 17px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Change background color of buttons on hover */
|
|
||||||
.tab div:hover {
|
|
||||||
background-color: #ddd;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Create an active/current "tab button" class */
|
|
||||||
.tab div.active {
|
|
||||||
background-color: #ccc;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Style the tab content */
|
|
||||||
.tabcontent {
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
height: 450px;
|
|
||||||
overflow-y: scroll;
|
|
||||||
background-color: #CCCCCC;
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
/* fallback */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Material Symbols Rounded';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 200;
|
|
||||||
src: url(/static/css/upstream/font.woff2) format('woff2');
|
|
||||||
}
|
|
||||||
|
|
||||||
.material-symbols-rounded {
|
|
||||||
font-family: 'Material Symbols Rounded';
|
|
||||||
font-weight: normal;
|
|
||||||
font-style: normal;
|
|
||||||
font-size: 24px;
|
|
||||||
line-height: 1;
|
|
||||||
letter-spacing: normal;
|
|
||||||
text-transform: none;
|
|
||||||
display: inline-block;
|
|
||||||
white-space: nowrap;
|
|
||||||
word-wrap: normal;
|
|
||||||
direction: ltr;
|
|
||||||
-webkit-font-feature-settings: 'liga';
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
}
|
|
1311
aprsd/web/chat/static/css/upstream/jquery-ui.css
vendored
@ -1,28 +0,0 @@
|
|||||||
/**
|
|
||||||
* jQuery toast plugin created by Kamran Ahmed copyright MIT license 2014
|
|
||||||
*/
|
|
||||||
.jq-toast-wrap { display: block; position: fixed; width: 250px; pointer-events: none !important; margin: 0; padding: 0; letter-spacing: normal; z-index: 9000 !important; }
|
|
||||||
.jq-toast-wrap * { margin: 0; padding: 0; }
|
|
||||||
|
|
||||||
.jq-toast-wrap.bottom-left { bottom: 20px; left: 20px; }
|
|
||||||
.jq-toast-wrap.bottom-right { bottom: 20px; right: 40px; }
|
|
||||||
.jq-toast-wrap.top-left { top: 20px; left: 20px; }
|
|
||||||
.jq-toast-wrap.top-right { top: 20px; right: 40px; }
|
|
||||||
|
|
||||||
.jq-toast-single { display: block; width: 100%; padding: 10px; margin: 0px 0px 5px; border-radius: 4px; font-size: 12px; font-family: arial, sans-serif; line-height: 17px; position: relative; pointer-events: all !important; background-color: #444444; color: white; }
|
|
||||||
|
|
||||||
.jq-toast-single h2 { font-family: arial, sans-serif; font-size: 14px; margin: 0px 0px 7px; background: none; color: inherit; line-height: inherit; letter-spacing: normal; }
|
|
||||||
.jq-toast-single a { color: #eee; text-decoration: none; font-weight: bold; border-bottom: 1px solid white; padding-bottom: 3px; font-size: 12px; }
|
|
||||||
|
|
||||||
.jq-toast-single ul { margin: 0px 0px 0px 15px; background: none; padding:0px; }
|
|
||||||
.jq-toast-single ul li { list-style-type: disc !important; line-height: 17px; background: none; margin: 0; padding: 0; letter-spacing: normal; }
|
|
||||||
|
|
||||||
.close-jq-toast-single { position: absolute; top: 3px; right: 7px; font-size: 14px; cursor: pointer; }
|
|
||||||
|
|
||||||
.jq-toast-loader { display: block; position: absolute; top: -2px; height: 5px; width: 0%; left: 0; border-radius: 5px; background: red; }
|
|
||||||
.jq-toast-loaded { width: 100%; }
|
|
||||||
.jq-has-icon { padding: 10px 10px 10px 50px; background-repeat: no-repeat; background-position: 10px; }
|
|
||||||
.jq-icon-info { background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGwSURBVEhLtZa9SgNBEMc9sUxxRcoUKSzSWIhXpFMhhYWFhaBg4yPYiWCXZxBLERsLRS3EQkEfwCKdjWJAwSKCgoKCcudv4O5YLrt7EzgXhiU3/4+b2ckmwVjJSpKkQ6wAi4gwhT+z3wRBcEz0yjSseUTrcRyfsHsXmD0AmbHOC9Ii8VImnuXBPglHpQ5wwSVM7sNnTG7Za4JwDdCjxyAiH3nyA2mtaTJufiDZ5dCaqlItILh1NHatfN5skvjx9Z38m69CgzuXmZgVrPIGE763Jx9qKsRozWYw6xOHdER+nn2KkO+Bb+UV5CBN6WC6QtBgbRVozrahAbmm6HtUsgtPC19tFdxXZYBOfkbmFJ1VaHA1VAHjd0pp70oTZzvR+EVrx2Ygfdsq6eu55BHYR8hlcki+n+kERUFG8BrA0BwjeAv2M8WLQBtcy+SD6fNsmnB3AlBLrgTtVW1c2QN4bVWLATaIS60J2Du5y1TiJgjSBvFVZgTmwCU+dAZFoPxGEEs8nyHC9Bwe2GvEJv2WXZb0vjdyFT4Cxk3e/kIqlOGoVLwwPevpYHT+00T+hWwXDf4AJAOUqWcDhbwAAAAASUVORK5CYII='); background-color: #31708f; color: #d9edf7; border-color: #bce8f1; }
|
|
||||||
.jq-icon-warning { background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGYSURBVEhL5ZSvTsNQFMbXZGICMYGYmJhAQIJAICYQPAACiSDB8AiICQQJT4CqQEwgJvYASAQCiZiYmJhAIBATCARJy+9rTsldd8sKu1M0+dLb057v6/lbq/2rK0mS/TRNj9cWNAKPYIJII7gIxCcQ51cvqID+GIEX8ASG4B1bK5gIZFeQfoJdEXOfgX4QAQg7kH2A65yQ87lyxb27sggkAzAuFhbbg1K2kgCkB1bVwyIR9m2L7PRPIhDUIXgGtyKw575yz3lTNs6X4JXnjV+LKM/m3MydnTbtOKIjtz6VhCBq4vSm3ncdrD2lk0VgUXSVKjVDJXJzijW1RQdsU7F77He8u68koNZTz8Oz5yGa6J3H3lZ0xYgXBK2QymlWWA+RWnYhskLBv2vmE+hBMCtbA7KX5drWyRT/2JsqZ2IvfB9Y4bWDNMFbJRFmC9E74SoS0CqulwjkC0+5bpcV1CZ8NMej4pjy0U+doDQsGyo1hzVJttIjhQ7GnBtRFN1UarUlH8F3xict+HY07rEzoUGPlWcjRFRr4/gChZgc3ZL2d8oAAAAASUVORK5CYII='); background-color: #8a6d3b; color: #fcf8e3; border-color: #faebcc; }
|
|
||||||
.jq-icon-error { background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAHOSURBVEhLrZa/SgNBEMZzh0WKCClSCKaIYOED+AAKeQQLG8HWztLCImBrYadgIdY+gIKNYkBFSwu7CAoqCgkkoGBI/E28PdbLZmeDLgzZzcx83/zZ2SSXC1j9fr+I1Hq93g2yxH4iwM1vkoBWAdxCmpzTxfkN2RcyZNaHFIkSo10+8kgxkXIURV5HGxTmFuc75B2RfQkpxHG8aAgaAFa0tAHqYFfQ7Iwe2yhODk8+J4C7yAoRTWI3w/4klGRgR4lO7Rpn9+gvMyWp+uxFh8+H+ARlgN1nJuJuQAYvNkEnwGFck18Er4q3egEc/oO+mhLdKgRyhdNFiacC0rlOCbhNVz4H9FnAYgDBvU3QIioZlJFLJtsoHYRDfiZoUyIxqCtRpVlANq0EU4dApjrtgezPFad5S19Wgjkc0hNVnuF4HjVA6C7QrSIbylB+oZe3aHgBsqlNqKYH48jXyJKMuAbiyVJ8KzaB3eRc0pg9VwQ4niFryI68qiOi3AbjwdsfnAtk0bCjTLJKr6mrD9g8iq/S/B81hguOMlQTnVyG40wAcjnmgsCNESDrjme7wfftP4P7SP4N3CJZdvzoNyGq2c/HWOXJGsvVg+RA/k2MC/wN6I2YA2Pt8GkAAAAASUVORK5CYII='); background-color: #a94442; color: #f2dede; border-color: #ebccd1; }
|
|
||||||
.jq-icon-success { background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADsSURBVEhLY2AYBfQMgf///3P8+/evAIgvA/FsIF+BavYDDWMBGroaSMMBiE8VC7AZDrIFaMFnii3AZTjUgsUUWUDA8OdAH6iQbQEhw4HyGsPEcKBXBIC4ARhex4G4BsjmweU1soIFaGg/WtoFZRIZdEvIMhxkCCjXIVsATV6gFGACs4Rsw0EGgIIH3QJYJgHSARQZDrWAB+jawzgs+Q2UO49D7jnRSRGoEFRILcdmEMWGI0cm0JJ2QpYA1RDvcmzJEWhABhD/pqrL0S0CWuABKgnRki9lLseS7g2AlqwHWQSKH4oKLrILpRGhEQCw2LiRUIa4lwAAAABJRU5ErkJggg=='); color: #dff0d8; background-color: #3c763d; border-color: #d6e9c6; }
|
|
Before Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 40 KiB |
@ -1,3 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-globe" viewBox="0 0 16 16">
|
|
||||||
<path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8m7.5-6.923c-.67.204-1.335.82-1.887 1.855A8 8 0 0 0 5.145 4H7.5zM4.09 4a9.3 9.3 0 0 1 .64-1.539 7 7 0 0 1 .597-.933A7.03 7.03 0 0 0 2.255 4zm-.582 3.5c.03-.877.138-1.718.312-2.5H1.674a7 7 0 0 0-.656 2.5zM4.847 5a12.5 12.5 0 0 0-.338 2.5H7.5V5zM8.5 5v2.5h2.99a12.5 12.5 0 0 0-.337-2.5zM4.51 8.5a12.5 12.5 0 0 0 .337 2.5H7.5V8.5zm3.99 0V11h2.653c.187-.765.306-1.608.338-2.5zM5.145 12q.208.58.468 1.068c.552 1.035 1.218 1.65 1.887 1.855V12zm.182 2.472a7 7 0 0 1-.597-.933A9.3 9.3 0 0 1 4.09 12H2.255a7 7 0 0 0 3.072 2.472M3.82 11a13.7 13.7 0 0 1-.312-2.5h-2.49c.062.89.291 1.733.656 2.5zm6.853 3.472A7 7 0 0 0 13.745 12H11.91a9.3 9.3 0 0 1-.64 1.539 7 7 0 0 1-.597.933M8.5 12v2.923c.67-.204 1.335-.82 1.887-1.855q.26-.487.468-1.068zm3.68-1h2.146c.365-.767.594-1.61.656-2.5h-2.49a13.7 13.7 0 0 1-.312 2.5m2.802-3.5a7 7 0 0 0-.656-2.5H12.18c.174.782.282 1.623.312 2.5zM11.27 2.461c.247.464.462.98.64 1.539h1.835a7 7 0 0 0-3.072-2.472c.218.284.418.598.597.933M10.855 4a8 8 0 0 0-.468-1.068C9.835 1.897 9.17 1.282 8.5 1.077V4z"/>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 1.2 KiB |
@ -1,84 +0,0 @@
|
|||||||
|
|
||||||
function init_gps() {
|
|
||||||
console.log("init_gps Called.")
|
|
||||||
console.log("latitude: "+latitude)
|
|
||||||
console.log("longitude: "+longitude)
|
|
||||||
$("#send_beacon").click(function() {
|
|
||||||
console.log("Send a beacon!")
|
|
||||||
if (!isNaN(latitude) && !isNaN(longitude)) {
|
|
||||||
// webchat admin has hard coded lat/long in the config file
|
|
||||||
showPosition({'coords': {'latitude': latitude, 'longitude': longitude}})
|
|
||||||
} else {
|
|
||||||
// Try to get the current location from the browser
|
|
||||||
getLocation();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLocation() {
|
|
||||||
if (navigator.geolocation) {
|
|
||||||
console.log("getCurrentPosition");
|
|
||||||
try {
|
|
||||||
navigator.geolocation.getCurrentPosition(
|
|
||||||
showPosition, showError,
|
|
||||||
{timeout:3000});
|
|
||||||
} catch(err) {
|
|
||||||
console.log("Failed to getCurrentPosition");
|
|
||||||
console.log(err);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
var msg = "Geolocation is not supported by this browser."
|
|
||||||
console.log(msg);
|
|
||||||
alert(msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showError(error) {
|
|
||||||
console.log("showError");
|
|
||||||
console.log(error);
|
|
||||||
var msg = "";
|
|
||||||
switch(error.code) {
|
|
||||||
case error.PERMISSION_DENIED:
|
|
||||||
msg = "User denied the request for Geolocation."
|
|
||||||
break;
|
|
||||||
case error.POSITION_UNAVAILABLE:
|
|
||||||
msg = "Location information is unavailable."
|
|
||||||
break;
|
|
||||||
case error.TIMEOUT:
|
|
||||||
msg = "The location fix timed out."
|
|
||||||
break;
|
|
||||||
case error.UNKNOWN_ERROR:
|
|
||||||
msg = "An unknown error occurred."
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
console.log(msg);
|
|
||||||
$.toast({
|
|
||||||
title: 'GPS Error',
|
|
||||||
class: 'warning',
|
|
||||||
position: 'middle center',
|
|
||||||
message: msg,
|
|
||||||
showProgress: 'top',
|
|
||||||
classProgress: 'blue',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function showPosition(position) {
|
|
||||||
console.log("showPosition Called");
|
|
||||||
path = $('#pkt_path option:selected').val();
|
|
||||||
msg = {
|
|
||||||
'latitude': position.coords.latitude,
|
|
||||||
'longitude': position.coords.longitude,
|
|
||||||
'path': path,
|
|
||||||
}
|
|
||||||
console.log(msg);
|
|
||||||
$.toast({
|
|
||||||
heading: 'Sending GPS Beacon',
|
|
||||||
text: "Latitude: "+position.coords.latitude+"<br>Longitude: "+position.coords.longitude,
|
|
||||||
loader: true,
|
|
||||||
loaderBg: '#9EC600',
|
|
||||||
position: 'top-center',
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("Sending GPS msg")
|
|
||||||
socket.emit("gps", msg);
|
|
||||||
}
|
|
@ -1,45 +0,0 @@
|
|||||||
function aprs_img(item, x_offset, y_offset) {
|
|
||||||
var x = x_offset * -16;
|
|
||||||
if (y_offset > 5) {
|
|
||||||
y_offset = 5;
|
|
||||||
}
|
|
||||||
var y = y_offset * -16;
|
|
||||||
var loc = x + 'px '+ y + 'px'
|
|
||||||
item.css('background-position', loc);
|
|
||||||
}
|
|
||||||
|
|
||||||
function show_aprs_icon(item, symbol) {
|
|
||||||
var offset = ord(symbol) - 33;
|
|
||||||
var col = Math.floor(offset / 16);
|
|
||||||
var row = offset % 16;
|
|
||||||
//console.log("'" + symbol+"' off: "+offset+" row: "+ row + " col: " + col)
|
|
||||||
aprs_img(item, row, col);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ord(str){return str.charCodeAt(0);}
|
|
||||||
|
|
||||||
function update_stats( data ) {
|
|
||||||
console.log(data);
|
|
||||||
$("#version").text( data["stats"]["APRSDStats"]["version"] );
|
|
||||||
$("#aprs_connection").html( data["aprs_connection"] );
|
|
||||||
$("#uptime").text( "uptime: " + data["stats"]["APRSDStats"]["uptime"] );
|
|
||||||
short_time = data["time"].split(/\s(.+)/)[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function start_update() {
|
|
||||||
|
|
||||||
(function statsworker() {
|
|
||||||
$.ajax({
|
|
||||||
url: "/stats",
|
|
||||||
type: 'GET',
|
|
||||||
dataType: 'json',
|
|
||||||
success: function(data) {
|
|
||||||
update_stats(data);
|
|
||||||
},
|
|
||||||
complete: function() {
|
|
||||||
setTimeout(statsworker, 60000);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
}
|
|
@ -1,612 +0,0 @@
|
|||||||
var cleared = false;
|
|
||||||
var callsign_list = {};
|
|
||||||
var callsign_location = {};
|
|
||||||
var message_list = {};
|
|
||||||
var from_msg_list = {};
|
|
||||||
var selected_tab_callsign = null;
|
|
||||||
const socket = io("/sendmsg");
|
|
||||||
|
|
||||||
MSG_TYPE_TX = "tx";
|
|
||||||
MSG_TYPE_RX = "rx";
|
|
||||||
MSG_TYPE_ACK = "ack";
|
|
||||||
|
|
||||||
function reload_popovers() {
|
|
||||||
$('[data-bs-toggle="popover"]').popover(
|
|
||||||
{html: true, animation: true}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function build_location_string(msg) {
|
|
||||||
dt = new Date(parseInt(msg['lasttime']) * 1000);
|
|
||||||
loc = "Last Location Update: " + dt.toLocaleString();
|
|
||||||
loc += "<br>Latitude: " + msg['lat'] + "<br>Longitude: " + msg['lon'];
|
|
||||||
loc += "<br>" + "Altitude: " + msg['altitude'] + " m";
|
|
||||||
loc += "<br>" + "Speed: " + msg['speed'] + " kph";
|
|
||||||
loc += "<br>" + "Bearing: " + msg['compass_bearing'];
|
|
||||||
loc += "<br>" + "distance: " + msg['distance'] + " km";
|
|
||||||
return loc;
|
|
||||||
}
|
|
||||||
|
|
||||||
function build_location_string_small(msg) {
|
|
||||||
dt = new Date(parseInt(msg['lasttime']) * 1000);
|
|
||||||
loc = "" + msg['distance'] + "km";
|
|
||||||
//loc += "Lat " + msg['lat'] + " Lon " + msg['lon'];
|
|
||||||
loc += " " + msg['compass_bearing'];
|
|
||||||
//loc += " Distance " + msg['distance'] + " km";
|
|
||||||
//loc += " " + dt.toLocaleString();
|
|
||||||
loc += " " + msg['timeago'];
|
|
||||||
return loc;
|
|
||||||
}
|
|
||||||
|
|
||||||
function size_dict(d){c=0; for (i in d) ++c; return c}
|
|
||||||
|
|
||||||
function raise_error(msg) {
|
|
||||||
$.toast({
|
|
||||||
heading: 'Error',
|
|
||||||
text: msg,
|
|
||||||
loader: true,
|
|
||||||
loaderBg: '#9EC600',
|
|
||||||
position: 'top-center',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function init_chat() {
|
|
||||||
socket.on('connect', function () {
|
|
||||||
console.log("Connected to socketio");
|
|
||||||
});
|
|
||||||
socket.on('connected', function(msg) {
|
|
||||||
console.log("Connected!");
|
|
||||||
console.log(msg);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("sent", function(msg) {
|
|
||||||
if (cleared === false) {
|
|
||||||
var msgsdiv = $("#msgsTabsDiv");
|
|
||||||
msgsdiv.html('');
|
|
||||||
cleared = true;
|
|
||||||
}
|
|
||||||
msg["type"] = MSG_TYPE_TX;
|
|
||||||
sent_msg(msg);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("ack", function(msg) {
|
|
||||||
msg["type"] = MSG_TYPE_ACK;
|
|
||||||
ack_msg(msg);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("new", function(msg) {
|
|
||||||
if (cleared === false) {
|
|
||||||
var msgsdiv = $("#msgsTabsDiv");
|
|
||||||
msgsdiv.html('')
|
|
||||||
cleared = true;
|
|
||||||
}
|
|
||||||
msg["type"] = MSG_TYPE_RX;
|
|
||||||
from_msg(msg);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("callsign_location", function(msg) {
|
|
||||||
console.log("CALLSIGN Location!");
|
|
||||||
console.log(msg);
|
|
||||||
now = new Date();
|
|
||||||
msg['last_updated'] = now;
|
|
||||||
callsign_location[msg['callsign']] = msg;
|
|
||||||
|
|
||||||
location_id = callsign_location_content(msg['callsign'], true);
|
|
||||||
location_string = build_location_string_small(msg);
|
|
||||||
$(location_id).html(location_string);
|
|
||||||
$(location_id+"Spinner").addClass('d-none');
|
|
||||||
save_data();
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#sendform").submit(function(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
to_call = $('#to_call').val().toUpperCase();
|
|
||||||
message = $('#message').val();
|
|
||||||
path = $('#pkt_path option:selected').val();
|
|
||||||
if (to_call == "") {
|
|
||||||
raise_error("You must enter a callsign to send a message")
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
if (message == "") {
|
|
||||||
raise_error("You must enter a message to send")
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
msg = {'to': to_call, 'message': message, 'path': path};
|
|
||||||
//console.log(msg);
|
|
||||||
socket.emit("send", msg);
|
|
||||||
$('#message').val('');
|
|
||||||
callsign_select(to_call);
|
|
||||||
activate_callsign_tab(to_call);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
init_gps();
|
|
||||||
// Try and load any existing chat threads from last time
|
|
||||||
init_messages();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function tab_string(callsign, id=false) {
|
|
||||||
name = "msgs"+callsign;
|
|
||||||
if (id) {
|
|
||||||
return "#"+name;
|
|
||||||
} else {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function tab_li_string(callsign, id=false) {
|
|
||||||
//The id of the LI containing the tab
|
|
||||||
return tab_string(callsign,id)+"Li";
|
|
||||||
}
|
|
||||||
|
|
||||||
function tab_notification_id(callsign, id=false) {
|
|
||||||
// The ID of the span that contains the notification count
|
|
||||||
return tab_string(callsign, id)+"notify";
|
|
||||||
}
|
|
||||||
|
|
||||||
function tab_content_name(callsign, id=false) {
|
|
||||||
return tab_string(callsign, id)+"Content";
|
|
||||||
}
|
|
||||||
|
|
||||||
function tab_content_speech_wrapper(callsign, id=false) {
|
|
||||||
return tab_string(callsign, id)+"SpeechWrapper";
|
|
||||||
}
|
|
||||||
|
|
||||||
function tab_content_speech_wrapper_id(callsign) {
|
|
||||||
return "#"+tab_content_speech_wrapper(callsign);
|
|
||||||
}
|
|
||||||
|
|
||||||
function content_divname(callsign) {
|
|
||||||
return "#"+tab_content_name(callsign);
|
|
||||||
}
|
|
||||||
|
|
||||||
function callsign_tab(callsign) {
|
|
||||||
return "#"+tab_string(callsign);
|
|
||||||
}
|
|
||||||
|
|
||||||
function callsign_location_popover(callsign, id=false) {
|
|
||||||
return tab_string(callsign, id)+"Location";
|
|
||||||
}
|
|
||||||
|
|
||||||
function callsign_location_content(callsign, id=false) {
|
|
||||||
return tab_string(callsign, id)+"LocationContent";
|
|
||||||
}
|
|
||||||
|
|
||||||
function bubble_msg_id(msg, id=false) {
|
|
||||||
// The id of the div that contains a specific message
|
|
||||||
name = msg["from_call"] + "_" + msg["msgNo"];
|
|
||||||
if (id) {
|
|
||||||
return "#"+name;
|
|
||||||
} else {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function message_ts_id(msg) {
|
|
||||||
//Create a 'id' from the message timestamp
|
|
||||||
ts_str = msg["timestamp"].toString();
|
|
||||||
ts = ts_str.split(".")[0]*1000;
|
|
||||||
id = ts_str.split('.')[0];
|
|
||||||
return {'timestamp': ts, 'id': id};
|
|
||||||
}
|
|
||||||
|
|
||||||
function time_ack_from_msg(msg) {
|
|
||||||
// Return the time and ack_id from a message
|
|
||||||
ts_id = message_ts_id(msg);
|
|
||||||
ts = ts_id['timestamp'];
|
|
||||||
id = ts_id['id'];
|
|
||||||
ack_id = "ack_" + id
|
|
||||||
|
|
||||||
var d = new Date(ts).toLocaleDateString("en-US")
|
|
||||||
var t = new Date(ts).toLocaleTimeString("en-US")
|
|
||||||
return {'time': t, 'date': d, 'ack_id': ack_id};
|
|
||||||
}
|
|
||||||
|
|
||||||
function save_data() {
|
|
||||||
// Save the relevant data to local storage
|
|
||||||
localStorage.setItem('callsign_list', JSON.stringify(callsign_list));
|
|
||||||
localStorage.setItem('message_list', JSON.stringify(message_list));
|
|
||||||
localStorage.setItem('callsign_location', JSON.stringify(callsign_location));
|
|
||||||
}
|
|
||||||
|
|
||||||
function init_messages() {
|
|
||||||
// This tries to load any previous conversations from local storage
|
|
||||||
callsign_list = JSON.parse(localStorage.getItem('callsign_list'));
|
|
||||||
message_list = JSON.parse(localStorage.getItem('message_list'));
|
|
||||||
callsign_location = JSON.parse(localStorage.getItem('callsign_location'));
|
|
||||||
if (callsign_list == null) {
|
|
||||||
callsign_list = {};
|
|
||||||
}
|
|
||||||
if (message_list == null) {
|
|
||||||
message_list = {};
|
|
||||||
}
|
|
||||||
if (callsign_location == null) {
|
|
||||||
callsign_location = {};
|
|
||||||
}
|
|
||||||
console.log(callsign_list);
|
|
||||||
console.log(message_list);
|
|
||||||
console.log(callsign_location);
|
|
||||||
|
|
||||||
// Now loop through each callsign and add the tabs
|
|
||||||
first_callsign = null;
|
|
||||||
for (callsign in callsign_list) {
|
|
||||||
if (first_callsign === null) {
|
|
||||||
first_callsign = callsign;
|
|
||||||
active = true;
|
|
||||||
} else {
|
|
||||||
active = false;
|
|
||||||
}
|
|
||||||
create_callsign_tab(callsign, active);
|
|
||||||
}
|
|
||||||
// and then populate the messages in order
|
|
||||||
for (callsign in message_list) {
|
|
||||||
new_callsign = true;
|
|
||||||
cleared = true;
|
|
||||||
for (id in message_list[callsign]) {
|
|
||||||
msg = message_list[callsign][id];
|
|
||||||
info = time_ack_from_msg(msg);
|
|
||||||
t = info['time'];
|
|
||||||
d = info['date'];
|
|
||||||
ack_id = false;
|
|
||||||
acked = false;
|
|
||||||
if (msg['type'] == MSG_TYPE_TX) {
|
|
||||||
ack_id = info['ack_id'];
|
|
||||||
acked = msg['ack'];
|
|
||||||
}
|
|
||||||
msg_html = create_message_html(d, t, msg['from_call'], msg['to_call'],
|
|
||||||
msg['message_text'], ack_id, msg, acked);
|
|
||||||
append_message_html(callsign, msg_html, new_callsign);
|
|
||||||
new_callsign = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (first_callsign !== null) {
|
|
||||||
callsign_select(first_callsign);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function scroll_main_content(callsign=false) {
|
|
||||||
var wc = $('#wc-content');
|
|
||||||
var d = $('#msgsTabContent');
|
|
||||||
var scrollHeight = wc.prop('scrollHeight');
|
|
||||||
var clientHeight = wc.prop('clientHeight');
|
|
||||||
|
|
||||||
if (callsign) {
|
|
||||||
div_id = content_divname(callsign);
|
|
||||||
c_div = $(content_divname(callsign));
|
|
||||||
//console.log("c_div("+div_id+") " + c_div);
|
|
||||||
c_height = c_div.height();
|
|
||||||
c_scroll_height = c_div.prop('scrollHeight');
|
|
||||||
//console.log("callsign height " + c_height + " scrollHeight " + c_scroll_height);
|
|
||||||
if (c_height === undefined) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (c_height > clientHeight) {
|
|
||||||
wc.animate({ scrollTop: c_scroll_height }, 500);
|
|
||||||
} else {
|
|
||||||
wc.animate({ scrollTop: 0 }, 500);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (scrollHeight > clientHeight) {
|
|
||||||
wc.animate({ scrollTop: wc.prop('scrollHeight') }, 500);
|
|
||||||
} else {
|
|
||||||
wc.animate({ scrollTop: 0 }, 500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function create_callsign_tab(callsign, active=false) {
|
|
||||||
//Create the html for the callsign tab and insert it into the DOM
|
|
||||||
var callsignTabs = $("#msgsTabList");
|
|
||||||
tab_id = tab_string(callsign);
|
|
||||||
tab_id_li = tab_li_string(callsign);
|
|
||||||
tab_notify_id = tab_notification_id(callsign);
|
|
||||||
tab_content = tab_content_name(callsign);
|
|
||||||
popover_id = callsign_location_popover(callsign);
|
|
||||||
if (active) {
|
|
||||||
active_str = "active";
|
|
||||||
} else {
|
|
||||||
active_str = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
item_html = '<li class="nav-item" role="presentation" callsign="'+callsign+'" id="'+tab_id_li+'">';
|
|
||||||
//item_html += '<button onClick="callsign_select(\''+callsign+'\');" callsign="'+callsign+'" class="nav-link '+active_str+'" id="'+tab_id+'" data-bs-toggle="tab" data-bs-target="#'+tab_content+'" type="button" role="tab" aria-controls="'+callsign+'" aria-selected="true">';
|
|
||||||
item_html += '<button onClick="callsign_select(\''+callsign+'\');" callsign="'+callsign+'" class="nav-link position-relative '+active_str+'" id="'+tab_id+'" data-bs-toggle="tab" data-bs-target="#'+tab_content+'" type="button" role="tab" aria-controls="'+callsign+'" aria-selected="true">';
|
|
||||||
item_html += callsign+' ';
|
|
||||||
item_html += '<span id="'+tab_notify_id+'" class="position-absolute top-0 start-80 translate-middle badge bg-danger border border-light rounded-pill visually-hidden">0</span>';
|
|
||||||
item_html += '<span onclick="delete_tab(\''+callsign+'\');">×</span>';
|
|
||||||
item_html += '</button></li>'
|
|
||||||
|
|
||||||
callsignTabs.append(item_html);
|
|
||||||
create_callsign_tab_content(callsign, active);
|
|
||||||
}
|
|
||||||
|
|
||||||
function create_callsign_tab_content(callsign, active=false) {
|
|
||||||
var callsignTabsContent = $("#msgsTabContent");
|
|
||||||
tab_id = tab_string(callsign);
|
|
||||||
tab_content = tab_content_name(callsign);
|
|
||||||
wrapper_id = tab_content_speech_wrapper(callsign);
|
|
||||||
if (active) {
|
|
||||||
active_str = "show active";
|
|
||||||
} else {
|
|
||||||
active_str = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
location_str = "Unknown Location"
|
|
||||||
if (callsign in callsign_location) {
|
|
||||||
location_str = build_location_string_small(callsign_location[callsign]);
|
|
||||||
location_class = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
location_id = callsign_location_content(callsign);
|
|
||||||
|
|
||||||
item_html = '<div class="tab-pane fade '+active_str+'" id="'+tab_content+'" role="tabpanel" aria-labelledby="'+tab_id+'">';
|
|
||||||
item_html += '<div class="" style="border: 1px solid #999999;background-color:#aaaaaa;">';
|
|
||||||
item_html += '<div class="row" style="padding-top:4px;padding-bottom:4px;background-color:#aaaaaa;margin:0px;">';
|
|
||||||
item_html += '<div class="d-flex col-md-10 justify-content-left" style="padding:0px;margin:0px;">';
|
|
||||||
item_html += '<button onclick="call_callsign_location(\''+callsign+'\');" style="margin-left:2px;padding: 0px 4px 0px 4px;font-size: .9rem" type="button" class="btn btn-primary">';
|
|
||||||
item_html += '<span id="'+location_id+'Spinner" class="d-none spinner-border spinner-border-sm" role="status" aria-hidden="true" style="font-size: .9rem"></span>Update</button>';
|
|
||||||
item_html += ' <span id="'+location_id+'" style="font-size: .9rem">'+location_str+'</span></div>';
|
|
||||||
item_html += '</div>';
|
|
||||||
item_html += '<div class="speech-wrapper" id="'+wrapper_id+'"></div>';
|
|
||||||
item_html += '</div>';
|
|
||||||
callsignTabsContent.append(item_html);
|
|
||||||
}
|
|
||||||
|
|
||||||
function delete_tab(callsign) {
|
|
||||||
// User asked to delete the tab and the conversation
|
|
||||||
tab_id = tab_string(callsign, true);
|
|
||||||
tab_id_li = tab_li_string(callsign, true);
|
|
||||||
tab_content = tab_content_name(callsign, true);
|
|
||||||
$(tab_id_li).remove();
|
|
||||||
$(tab_content).remove();
|
|
||||||
delete callsign_list[callsign];
|
|
||||||
delete message_list[callsign];
|
|
||||||
delete callsign_location[callsign];
|
|
||||||
|
|
||||||
// Now select the first tab
|
|
||||||
first_tab = $("#msgsTabList").children().first().children().first();
|
|
||||||
console.log(first_tab);
|
|
||||||
$(first_tab).click();
|
|
||||||
save_data();
|
|
||||||
}
|
|
||||||
|
|
||||||
function add_callsign(callsign, msg) {
|
|
||||||
/* Ensure a callsign exists in the left hand nav */
|
|
||||||
if (callsign in callsign_list) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
len = Object.keys(callsign_list).length;
|
|
||||||
if (len == 0) {
|
|
||||||
active = true;
|
|
||||||
} else {
|
|
||||||
active = false;
|
|
||||||
}
|
|
||||||
create_callsign_tab(callsign, active);
|
|
||||||
callsign_list[callsign] = '';
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function update_callsign_path(callsign, msg) {
|
|
||||||
//Get the selected path to save for this callsign
|
|
||||||
path = msg['path']
|
|
||||||
$('#pkt_path').val(path);
|
|
||||||
callsign_list[callsign] = path;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function append_message(callsign, msg, msg_html) {
|
|
||||||
new_callsign = false
|
|
||||||
if (!message_list.hasOwnProperty(callsign)) {
|
|
||||||
message_list[callsign] = {};
|
|
||||||
}
|
|
||||||
ts_id = message_ts_id(msg);
|
|
||||||
id = ts_id['id']
|
|
||||||
message_list[callsign][id] = msg;
|
|
||||||
if (selected_tab_callsign != callsign) {
|
|
||||||
// We need to update the notification for the tab
|
|
||||||
tab_notify_id = tab_notification_id(callsign, true);
|
|
||||||
// get the current count of notifications
|
|
||||||
count = parseInt($(tab_notify_id).text());
|
|
||||||
if (isNaN(count)) {
|
|
||||||
count = 0;
|
|
||||||
}
|
|
||||||
count += 1;
|
|
||||||
$(tab_notify_id).text(count);
|
|
||||||
$(tab_notify_id).removeClass('visually-hidden');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the right div to place the html
|
|
||||||
new_callsign = add_callsign(callsign, msg);
|
|
||||||
//update_callsign_path(callsign, msg);
|
|
||||||
append_message_html(callsign, msg_html, new_callsign);
|
|
||||||
len = Object.keys(callsign_list).length;
|
|
||||||
if (new_callsign) {
|
|
||||||
//Now click the tab if and only if there is only one tab
|
|
||||||
callsign_tab_id = callsign_tab(callsign);
|
|
||||||
$(callsign_tab_id).click();
|
|
||||||
callsign_select(callsign);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function append_message_html(callsign, msg_html, new_callsign) {
|
|
||||||
var msgsTabs = $('#msgsTabsDiv');
|
|
||||||
divname_str = tab_content_name(callsign);
|
|
||||||
divname = content_divname(callsign);
|
|
||||||
tab_content = tab_content_name(callsign);
|
|
||||||
wrapper_id = tab_content_speech_wrapper_id(callsign);
|
|
||||||
|
|
||||||
$(wrapper_id).append(msg_html);
|
|
||||||
|
|
||||||
if ($(wrapper_id).children().length > 0) {
|
|
||||||
$(wrapper_id).animate({scrollTop: $(wrapper_id)[0].scrollHeight}, "fast");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function create_message_html(date, time, from, to, message, ack_id, msg, acked=false) {
|
|
||||||
div_id = from + "_" + msg.msgNo;
|
|
||||||
if (ack_id) {
|
|
||||||
alt = " alt"
|
|
||||||
} else {
|
|
||||||
alt = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
bubble_class = "bubble" + alt + " text-nowrap"
|
|
||||||
bubble_name_class = "bubble-name" + alt
|
|
||||||
bubble_msgid = bubble_msg_id(msg);
|
|
||||||
date_str = date + " " + time;
|
|
||||||
sane_date_str = date_str.replace(/ /g,"").replaceAll("/","").replaceAll(":","");
|
|
||||||
|
|
||||||
bubble_msg_class = "bubble-message";
|
|
||||||
if (ack_id) {
|
|
||||||
bubble_arrow_class = "bubble-arrow alt";
|
|
||||||
popover_placement = "left";
|
|
||||||
} else {
|
|
||||||
bubble_arrow_class = "bubble-arrow";
|
|
||||||
popover_placement = "right";
|
|
||||||
}
|
|
||||||
|
|
||||||
msg_html = '<div class="bubble-row'+alt+'">';
|
|
||||||
msg_html += '<div id="'+bubble_msgid+'" class="'+ bubble_class + '" ';
|
|
||||||
msg_html += 'title="APRS Raw Packet" data-bs-placement="'+popover_placement+'" data-bs-toggle="popover" ';
|
|
||||||
msg_html += 'data-bs-trigger="hover" data-bs-content="'+msg['raw']+'">';
|
|
||||||
msg_html += '<div class="bubble-text">';
|
|
||||||
msg_html += '<p class="'+ bubble_name_class +'">'+from+' ';
|
|
||||||
msg_html += '<span class="bubble-timestamp">'+date_str+'</span>';
|
|
||||||
|
|
||||||
if (ack_id) {
|
|
||||||
if (acked) {
|
|
||||||
msg_html += '<span class="material-symbols-rounded md-10" id="' + ack_id + '">thumb_up</span>';
|
|
||||||
} else {
|
|
||||||
msg_html += '<span class="material-symbols-rounded md-10" id="' + ack_id + '">thumb_down</span>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
msg_html += "</p>";
|
|
||||||
msg_html += '<p class="' +bubble_msg_class+ '">'+message+'</p>';
|
|
||||||
msg_html += '<div class="'+ bubble_arrow_class + '"></div>';
|
|
||||||
msg_html += "</div></div></div>";
|
|
||||||
|
|
||||||
return msg_html
|
|
||||||
}
|
|
||||||
|
|
||||||
function flash_message(msg) {
|
|
||||||
// Callback function to bring a hidden box back
|
|
||||||
msg_id = bubble_msg_id(msg, true);
|
|
||||||
$(msg_id).fadeOut(100).fadeIn(100).fadeOut(100).fadeIn(100).fadeOut(100).fadeIn(100);
|
|
||||||
}
|
|
||||||
|
|
||||||
function sent_msg(msg) {
|
|
||||||
info = time_ack_from_msg(msg);
|
|
||||||
t = info['time'];
|
|
||||||
d = info['date'];
|
|
||||||
ack_id = info['ack_id'];
|
|
||||||
|
|
||||||
msg_html = create_message_html(d, t, msg['from_call'], msg['to_call'], msg['message_text'], ack_id, msg, false);
|
|
||||||
append_message(msg['to_call'], msg, msg_html);
|
|
||||||
save_data();
|
|
||||||
scroll_main_content(msg['to_call']);
|
|
||||||
reload_popovers();
|
|
||||||
}
|
|
||||||
|
|
||||||
function str_to_int(my_string) {
|
|
||||||
total = 0
|
|
||||||
for (let i = 0; i < my_string.length; i++) {
|
|
||||||
total += my_string.charCodeAt(i);
|
|
||||||
}
|
|
||||||
return total
|
|
||||||
}
|
|
||||||
|
|
||||||
function from_msg(msg) {
|
|
||||||
if (!from_msg_list.hasOwnProperty(msg["from_call"])) {
|
|
||||||
from_msg_list[msg["from_call"]] = new Array();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to account for messages that have no msgNo
|
|
||||||
console.log(msg)
|
|
||||||
if (msg["msgNo"] == null) {
|
|
||||||
console.log("Need to add msgNO!!")
|
|
||||||
// create an artificial msgNo
|
|
||||||
total = str_to_int(msg["from_call"])
|
|
||||||
total += str_to_int(msg["addresse"])
|
|
||||||
total += str_to_int(msg["message_text"])
|
|
||||||
msg["msgNo"] = total
|
|
||||||
}
|
|
||||||
|
|
||||||
if (msg["msgNo"] in from_msg_list[msg["from_call"]]) {
|
|
||||||
// We already have this message
|
|
||||||
//console.log("We already have this message msgNo=" + msg["msgNo"]);
|
|
||||||
// Do some flashy thing?
|
|
||||||
flash_message(msg);
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
from_msg_list[msg["from_call"]][msg["msgNo"]] = msg
|
|
||||||
}
|
|
||||||
info = time_ack_from_msg(msg);
|
|
||||||
t = info['time'];
|
|
||||||
d = info['date'];
|
|
||||||
ack_id = info['ack_id'];
|
|
||||||
|
|
||||||
from = msg['from_call']
|
|
||||||
msg_html = create_message_html(d, t, from, false, msg['message_text'], false, msg, false);
|
|
||||||
append_message(from, msg, msg_html);
|
|
||||||
save_data();
|
|
||||||
scroll_main_content(from);
|
|
||||||
reload_popovers();
|
|
||||||
}
|
|
||||||
|
|
||||||
function ack_msg(msg) {
|
|
||||||
// Acknowledge a message
|
|
||||||
// We have an existing entry
|
|
||||||
ts_id = message_ts_id(msg);
|
|
||||||
id = ts_id['id'];
|
|
||||||
//Mark the message as acked
|
|
||||||
callsign = msg['to_call'];
|
|
||||||
// Ensure the message_list has this callsign
|
|
||||||
if (!message_list.hasOwnProperty(callsign)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// Ensure the message_list has this id
|
|
||||||
if (!message_list[callsign].hasOwnProperty(id)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (message_list[callsign][id]['ack'] == true) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
message_list[callsign][id]['ack'] = true;
|
|
||||||
ack_id = "ack_" + id
|
|
||||||
|
|
||||||
if (msg['ack'] == true) {
|
|
||||||
var ack_div = $('#' + ack_id);
|
|
||||||
ack_div.html('thumb_up');
|
|
||||||
}
|
|
||||||
|
|
||||||
//$('.ui.accordion').accordion('refresh');
|
|
||||||
save_data();
|
|
||||||
scroll_main_content();
|
|
||||||
}
|
|
||||||
|
|
||||||
function activate_callsign_tab(callsign) {
|
|
||||||
tab_content = tab_string(callsign, id=true);
|
|
||||||
$(tab_content).click();
|
|
||||||
}
|
|
||||||
|
|
||||||
function callsign_select(callsign) {
|
|
||||||
var tocall = $("#to_call");
|
|
||||||
tocall.val(callsign.toUpperCase());
|
|
||||||
scroll_main_content(callsign);
|
|
||||||
selected_tab_callsign = callsign;
|
|
||||||
tab_notify_id = tab_notification_id(callsign, true);
|
|
||||||
$(tab_notify_id).addClass('visually-hidden');
|
|
||||||
$(tab_notify_id).text(0);
|
|
||||||
// Now update the path
|
|
||||||
// $('#pkt_path').val(callsign_list[callsign]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function call_callsign_location(callsign) {
|
|
||||||
msg = {'callsign': callsign};
|
|
||||||
socket.emit("get_callsign_location", msg);
|
|
||||||
location_id = callsign_location_content(callsign, true)+"Spinner";
|
|
||||||
$(location_id).removeClass('d-none');
|
|
||||||
}
|
|
@ -1,28 +0,0 @@
|
|||||||
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,374 +0,0 @@
|
|||||||
// jQuery toast plugin created by Kamran Ahmed copyright MIT license 2015
|
|
||||||
if ( typeof Object.create !== 'function' ) {
|
|
||||||
Object.create = function( obj ) {
|
|
||||||
function F() {}
|
|
||||||
F.prototype = obj;
|
|
||||||
return new F();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
(function( $, window, document, undefined ) {
|
|
||||||
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
var Toast = {
|
|
||||||
|
|
||||||
_positionClasses : ['bottom-left', 'bottom-right', 'top-right', 'top-left', 'bottom-center', 'top-center', 'mid-center'],
|
|
||||||
_defaultIcons : ['success', 'error', 'info', 'warning'],
|
|
||||||
|
|
||||||
init: function (options, elem) {
|
|
||||||
this.prepareOptions(options, $.toast.options);
|
|
||||||
this.process();
|
|
||||||
},
|
|
||||||
|
|
||||||
prepareOptions: function(options, options_to_extend) {
|
|
||||||
var _options = {};
|
|
||||||
if ( ( typeof options === 'string' ) || ( options instanceof Array ) ) {
|
|
||||||
_options.text = options;
|
|
||||||
} else {
|
|
||||||
_options = options;
|
|
||||||
}
|
|
||||||
this.options = $.extend( {}, options_to_extend, _options );
|
|
||||||
},
|
|
||||||
|
|
||||||
process: function () {
|
|
||||||
this.setup();
|
|
||||||
this.addToDom();
|
|
||||||
this.position();
|
|
||||||
this.bindToast();
|
|
||||||
this.animate();
|
|
||||||
},
|
|
||||||
|
|
||||||
setup: function () {
|
|
||||||
|
|
||||||
var _toastContent = '';
|
|
||||||
|
|
||||||
this._toastEl = this._toastEl || $('<div></div>', {
|
|
||||||
class : 'jq-toast-single'
|
|
||||||
});
|
|
||||||
|
|
||||||
// For the loader on top
|
|
||||||
_toastContent += '<span class="jq-toast-loader"></span>';
|
|
||||||
|
|
||||||
if ( this.options.allowToastClose ) {
|
|
||||||
_toastContent += '<span class="close-jq-toast-single">×</span>';
|
|
||||||
};
|
|
||||||
|
|
||||||
if ( this.options.text instanceof Array ) {
|
|
||||||
|
|
||||||
if ( this.options.heading ) {
|
|
||||||
_toastContent +='<h2 class="jq-toast-heading">' + this.options.heading + '</h2>';
|
|
||||||
};
|
|
||||||
|
|
||||||
_toastContent += '<ul class="jq-toast-ul">';
|
|
||||||
for (var i = 0; i < this.options.text.length; i++) {
|
|
||||||
_toastContent += '<li class="jq-toast-li" id="jq-toast-item-' + i + '">' + this.options.text[i] + '</li>';
|
|
||||||
}
|
|
||||||
_toastContent += '</ul>';
|
|
||||||
|
|
||||||
} else {
|
|
||||||
if ( this.options.heading ) {
|
|
||||||
_toastContent +='<h2 class="jq-toast-heading">' + this.options.heading + '</h2>';
|
|
||||||
};
|
|
||||||
_toastContent += this.options.text;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._toastEl.html( _toastContent );
|
|
||||||
|
|
||||||
if ( this.options.bgColor !== false ) {
|
|
||||||
this._toastEl.css("background-color", this.options.bgColor);
|
|
||||||
};
|
|
||||||
|
|
||||||
if ( this.options.textColor !== false ) {
|
|
||||||
this._toastEl.css("color", this.options.textColor);
|
|
||||||
};
|
|
||||||
|
|
||||||
if ( this.options.textAlign ) {
|
|
||||||
this._toastEl.css('text-align', this.options.textAlign);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( this.options.icon !== false ) {
|
|
||||||
this._toastEl.addClass('jq-has-icon');
|
|
||||||
|
|
||||||
if ( $.inArray(this.options.icon, this._defaultIcons) !== -1 ) {
|
|
||||||
this._toastEl.addClass('jq-icon-' + this.options.icon);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
if ( this.options.class !== false ){
|
|
||||||
this._toastEl.addClass(this.options.class)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
position: function () {
|
|
||||||
if ( ( typeof this.options.position === 'string' ) && ( $.inArray( this.options.position, this._positionClasses) !== -1 ) ) {
|
|
||||||
|
|
||||||
if ( this.options.position === 'bottom-center' ) {
|
|
||||||
this._container.css({
|
|
||||||
left: ( $(window).outerWidth() / 2 ) - this._container.outerWidth()/2,
|
|
||||||
bottom: 20
|
|
||||||
});
|
|
||||||
} else if ( this.options.position === 'top-center' ) {
|
|
||||||
this._container.css({
|
|
||||||
left: ( $(window).outerWidth() / 2 ) - this._container.outerWidth()/2,
|
|
||||||
top: 20
|
|
||||||
});
|
|
||||||
} else if ( this.options.position === 'mid-center' ) {
|
|
||||||
this._container.css({
|
|
||||||
left: ( $(window).outerWidth() / 2 ) - this._container.outerWidth()/2,
|
|
||||||
top: ( $(window).outerHeight() / 2 ) - this._container.outerHeight()/2
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this._container.addClass( this.options.position );
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if ( typeof this.options.position === 'object' ) {
|
|
||||||
this._container.css({
|
|
||||||
top : this.options.position.top ? this.options.position.top : 'auto',
|
|
||||||
bottom : this.options.position.bottom ? this.options.position.bottom : 'auto',
|
|
||||||
left : this.options.position.left ? this.options.position.left : 'auto',
|
|
||||||
right : this.options.position.right ? this.options.position.right : 'auto'
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this._container.addClass( 'bottom-left' );
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
bindToast: function () {
|
|
||||||
|
|
||||||
var that = this;
|
|
||||||
|
|
||||||
this._toastEl.on('afterShown', function () {
|
|
||||||
that.processLoader();
|
|
||||||
});
|
|
||||||
|
|
||||||
this._toastEl.find('.close-jq-toast-single').on('click', function ( e ) {
|
|
||||||
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
if( that.options.showHideTransition === 'fade') {
|
|
||||||
that._toastEl.trigger('beforeHide');
|
|
||||||
that._toastEl.fadeOut(function () {
|
|
||||||
that._toastEl.trigger('afterHidden');
|
|
||||||
});
|
|
||||||
} else if ( that.options.showHideTransition === 'slide' ) {
|
|
||||||
that._toastEl.trigger('beforeHide');
|
|
||||||
that._toastEl.slideUp(function () {
|
|
||||||
that._toastEl.trigger('afterHidden');
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
that._toastEl.trigger('beforeHide');
|
|
||||||
that._toastEl.hide(function () {
|
|
||||||
that._toastEl.trigger('afterHidden');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if ( typeof this.options.beforeShow == 'function' ) {
|
|
||||||
this._toastEl.on('beforeShow', function () {
|
|
||||||
that.options.beforeShow(that._toastEl);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
if ( typeof this.options.afterShown == 'function' ) {
|
|
||||||
this._toastEl.on('afterShown', function () {
|
|
||||||
that.options.afterShown(that._toastEl);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
if ( typeof this.options.beforeHide == 'function' ) {
|
|
||||||
this._toastEl.on('beforeHide', function () {
|
|
||||||
that.options.beforeHide(that._toastEl);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
if ( typeof this.options.afterHidden == 'function' ) {
|
|
||||||
this._toastEl.on('afterHidden', function () {
|
|
||||||
that.options.afterHidden(that._toastEl);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
if ( typeof this.options.onClick == 'function' ) {
|
|
||||||
this._toastEl.on('click', function () {
|
|
||||||
that.options.onClick(that._toastEl);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
addToDom: function () {
|
|
||||||
|
|
||||||
var _container = $('.jq-toast-wrap');
|
|
||||||
|
|
||||||
if ( _container.length === 0 ) {
|
|
||||||
|
|
||||||
_container = $('<div></div>',{
|
|
||||||
class: "jq-toast-wrap",
|
|
||||||
role: "alert",
|
|
||||||
"aria-live": "polite"
|
|
||||||
});
|
|
||||||
|
|
||||||
$('body').append( _container );
|
|
||||||
|
|
||||||
} else if ( !this.options.stack || isNaN( parseInt(this.options.stack, 10) ) ) {
|
|
||||||
_container.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
_container.find('.jq-toast-single:hidden').remove();
|
|
||||||
|
|
||||||
_container.append( this._toastEl );
|
|
||||||
|
|
||||||
if ( this.options.stack && !isNaN( parseInt( this.options.stack ), 10 ) ) {
|
|
||||||
|
|
||||||
var _prevToastCount = _container.find('.jq-toast-single').length,
|
|
||||||
_extToastCount = _prevToastCount - this.options.stack;
|
|
||||||
|
|
||||||
if ( _extToastCount > 0 ) {
|
|
||||||
$('.jq-toast-wrap').find('.jq-toast-single').slice(0, _extToastCount).remove();
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
this._container = _container;
|
|
||||||
},
|
|
||||||
|
|
||||||
canAutoHide: function () {
|
|
||||||
return ( this.options.hideAfter !== false ) && !isNaN( parseInt( this.options.hideAfter, 10 ) );
|
|
||||||
},
|
|
||||||
|
|
||||||
processLoader: function () {
|
|
||||||
// Show the loader only, if auto-hide is on and loader is demanded
|
|
||||||
if (!this.canAutoHide() || this.options.loader === false) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var loader = this._toastEl.find('.jq-toast-loader');
|
|
||||||
|
|
||||||
// 400 is the default time that jquery uses for fade/slide
|
|
||||||
// Divide by 1000 for milliseconds to seconds conversion
|
|
||||||
var transitionTime = (this.options.hideAfter - 400) / 1000 + 's';
|
|
||||||
var loaderBg = this.options.loaderBg;
|
|
||||||
|
|
||||||
var style = loader.attr('style') || '';
|
|
||||||
style = style.substring(0, style.indexOf('-webkit-transition')); // Remove the last transition definition
|
|
||||||
|
|
||||||
style += '-webkit-transition: width ' + transitionTime + ' ease-in; \
|
|
||||||
-o-transition: width ' + transitionTime + ' ease-in; \
|
|
||||||
transition: width ' + transitionTime + ' ease-in; \
|
|
||||||
background-color: ' + loaderBg + ';';
|
|
||||||
|
|
||||||
|
|
||||||
loader.attr('style', style).addClass('jq-toast-loaded');
|
|
||||||
},
|
|
||||||
|
|
||||||
animate: function () {
|
|
||||||
|
|
||||||
var that = this;
|
|
||||||
|
|
||||||
this._toastEl.hide();
|
|
||||||
|
|
||||||
this._toastEl.trigger('beforeShow');
|
|
||||||
|
|
||||||
if ( this.options.showHideTransition.toLowerCase() === 'fade' ) {
|
|
||||||
this._toastEl.fadeIn(function ( ){
|
|
||||||
that._toastEl.trigger('afterShown');
|
|
||||||
});
|
|
||||||
} else if ( this.options.showHideTransition.toLowerCase() === 'slide' ) {
|
|
||||||
this._toastEl.slideDown(function ( ){
|
|
||||||
that._toastEl.trigger('afterShown');
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this._toastEl.show(function ( ){
|
|
||||||
that._toastEl.trigger('afterShown');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.canAutoHide()) {
|
|
||||||
|
|
||||||
var that = this;
|
|
||||||
|
|
||||||
window.setTimeout(function(){
|
|
||||||
|
|
||||||
if ( that.options.showHideTransition.toLowerCase() === 'fade' ) {
|
|
||||||
that._toastEl.trigger('beforeHide');
|
|
||||||
that._toastEl.fadeOut(function () {
|
|
||||||
that._toastEl.trigger('afterHidden');
|
|
||||||
});
|
|
||||||
} else if ( that.options.showHideTransition.toLowerCase() === 'slide' ) {
|
|
||||||
that._toastEl.trigger('beforeHide');
|
|
||||||
that._toastEl.slideUp(function () {
|
|
||||||
that._toastEl.trigger('afterHidden');
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
that._toastEl.trigger('beforeHide');
|
|
||||||
that._toastEl.hide(function () {
|
|
||||||
that._toastEl.trigger('afterHidden');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}, this.options.hideAfter);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
reset: function ( resetWhat ) {
|
|
||||||
|
|
||||||
if ( resetWhat === 'all' ) {
|
|
||||||
$('.jq-toast-wrap').remove();
|
|
||||||
} else {
|
|
||||||
this._toastEl.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
update: function(options) {
|
|
||||||
this.prepareOptions(options, this.options);
|
|
||||||
this.setup();
|
|
||||||
this.bindToast();
|
|
||||||
},
|
|
||||||
|
|
||||||
close: function() {
|
|
||||||
this._toastEl.find('.close-jq-toast-single').click();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
$.toast = function(options) {
|
|
||||||
var toast = Object.create(Toast);
|
|
||||||
toast.init(options, this);
|
|
||||||
|
|
||||||
return {
|
|
||||||
|
|
||||||
reset: function ( what ) {
|
|
||||||
toast.reset( what );
|
|
||||||
},
|
|
||||||
|
|
||||||
update: function( options ) {
|
|
||||||
toast.update( options );
|
|
||||||
},
|
|
||||||
|
|
||||||
close: function( ) {
|
|
||||||
toast.close( );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
$.toast.options = {
|
|
||||||
text: '',
|
|
||||||
heading: '',
|
|
||||||
showHideTransition: 'fade',
|
|
||||||
allowToastClose: true,
|
|
||||||
hideAfter: 3000,
|
|
||||||
loader: true,
|
|
||||||
loaderBg: '#9EC600',
|
|
||||||
stack: 5,
|
|
||||||
position: 'bottom-left',
|
|
||||||
bgColor: false,
|
|
||||||
textColor: false,
|
|
||||||
textAlign: 'left',
|
|
||||||
icon: false,
|
|
||||||
beforeShow: function () {},
|
|
||||||
afterShown: function () {},
|
|
||||||
beforeHide: function () {},
|
|
||||||
afterHidden: function () {},
|
|
||||||
onClick: function () {}
|
|
||||||
};
|
|
||||||
|
|
||||||
})( jQuery, window, document );
|
|
@ -1,139 +0,0 @@
|
|||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta name="viewport"
|
|
||||||
content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1">
|
|
||||||
<script src="/static/js/upstream/jquery-3.7.1.min.js"></script>
|
|
||||||
<script src="/static/js/upstream/jquery.toast.js"></script>
|
|
||||||
<script src="/static/js/upstream/socket.io.min.js"></script>
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="/static/css/upstream/bootstrap.min.css">
|
|
||||||
<script src="/static/js/upstream/bootstrap.bundle.min.js"></script>
|
|
||||||
|
|
||||||
<!--
|
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/css/bootstrap.min.css"
|
|
||||||
rel="stylesheet"
|
|
||||||
integrity="sha384-4bw+/aepP/YC94hEpVNVgiZdgIC5+VKNBQNGCHeKRQN+PtmoHDEXuppvnDJzQIu9"
|
|
||||||
crossorigin="anonymous">
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/js/bootstrap.bundle.min.js"
|
|
||||||
integrity="sha384-HwwvtgBNo3bZJJLYd8oVXjrBZt8cqVSpeBNS5n7C8IVInixGAoxmnlMuBnhbgrkm"
|
|
||||||
crossorigin="anonymous"></script>
|
|
||||||
-->
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="/static/css/upstream/google-fonts.css">
|
|
||||||
<link rel="stylesheet" href="/static/css/upstream/jquery.toast.css">
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="/static/css/chat.css">
|
|
||||||
<link rel="stylesheet" href="/static/css/index.css">
|
|
||||||
<link rel="stylesheet" href="/static/css/tabs.css">
|
|
||||||
<script src="/static/js/main.js"></script>
|
|
||||||
<script src="/static/js/gps.js"></script>
|
|
||||||
<script src="/static/js/send-message.js"></script>
|
|
||||||
|
|
||||||
<script type="text/javascript">
|
|
||||||
var initial_stats = {{ initial_stats|tojson|safe }};
|
|
||||||
var latitude = parseFloat('{{ latitude|safe }}');
|
|
||||||
var longitude = parseFloat('{{ longitude|safe }}');
|
|
||||||
|
|
||||||
var memory_chart = null;
|
|
||||||
var message_chart = null;
|
|
||||||
|
|
||||||
$(document).ready(function() {
|
|
||||||
console.log(initial_stats);
|
|
||||||
start_update();
|
|
||||||
init_chat();
|
|
||||||
//reset_Tabs();
|
|
||||||
|
|
||||||
console.log("latitude", latitude);
|
|
||||||
console.log("longitude", longitude);
|
|
||||||
|
|
||||||
if (isNaN(latitude) || isNaN(longitude) && location.protocol != 'https:') {
|
|
||||||
// Have to disable the beacon button.
|
|
||||||
$('#send_beacon').prop('disabled', true);
|
|
||||||
}
|
|
||||||
|
|
||||||
$("#wipe_local").click(function() {
|
|
||||||
console.log('Wipe local storage');
|
|
||||||
localStorage.clear();
|
|
||||||
});
|
|
||||||
|
|
||||||
// When a tab is clicked, populate the to_call form field.
|
|
||||||
$(document).on('shown.bs.tab', 'button[data-bs-toggle="tab"]', function (e) {
|
|
||||||
var tab = $(e.target);
|
|
||||||
var callsign = tab.attr("callsign");
|
|
||||||
var to_call = $('#to_call');
|
|
||||||
to_call.val(callsign);
|
|
||||||
selected_tab_callsign = callsign;
|
|
||||||
});
|
|
||||||
|
|
||||||
/*$('[data-bs-toggle="popover"]').popover(
|
|
||||||
{html: true, animation: true}
|
|
||||||
);*/
|
|
||||||
reload_popovers();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div class="wc-container">
|
|
||||||
<div class="wc-row header">
|
|
||||||
<div class="container-sm">
|
|
||||||
<div class="row">
|
|
||||||
<div class="column">
|
|
||||||
<h1>APRSD WebChat {{ version }}</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="column">
|
|
||||||
<span style='color: green'>{{ callsign }}</span>
|
|
||||||
connected to
|
|
||||||
<span style='color: blue' id='aprs_connection'>{{ aprs_connection|safe }}</span>
|
|
||||||
<span id='uptime'>NONE</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<form class="row gx-1 gy-1 justify-content-center align-items-center" id="sendform" name="sendmsg" action="" autocomplete="off">
|
|
||||||
<div class="col-sm-2" style="width:150px;">
|
|
||||||
<label for="to_call" class="visually-hidden">Callsign</label>
|
|
||||||
<input type="search" class="form-control mb-2 mr-sm-2" name="to_call" id="to_call" placeholder="To Callsign" size="11" maxlength="9">
|
|
||||||
</div>
|
|
||||||
<div class="col-auto">
|
|
||||||
<label for="pkt_path" class="visually-hidden">PATH</label>
|
|
||||||
<select class="form-control mb-2 mr-sm-2" name="pkt_path" id="pkt_path" style="width:auto;">
|
|
||||||
<option value="" disabled selected>Default Path</option>
|
|
||||||
<option value="WIDE1-1">WIDE1-1</option>
|
|
||||||
<option value="WIDE1-1,WIDE2-1">WIDE1-1,WIDE2-1</option>
|
|
||||||
<option value="ARISS">ARISS</option>
|
|
||||||
<option value="GATE">GATE</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-3">
|
|
||||||
<label for="message" class="visually-hidden">Message</label>
|
|
||||||
<input type="search" class="form-control mb-2 mr-sm-2" name="message" id="message" size="40" maxlength="67" placeholder="Message">
|
|
||||||
</div>
|
|
||||||
<div class="col-auto">
|
|
||||||
<input type="submit" name="submit" class="btn btn-primary mb-2" id="send_msg" value="Send"/>
|
|
||||||
<button type="button" class="btn btn-primary mb-2" id="send_beacon" value="Send Position">Send Position</button>
|
|
||||||
<!-- <button type="button" class="btn btn-primary mb-2" id="wipe_local" value="wipe local storage">Wipe LocalStorage</button> -->
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="container-sm" style="max-width: 800px">
|
|
||||||
<ul class="nav nav-tabs" id="msgsTabList" role="tablist"></ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="wc-row content" id="wc-content">
|
|
||||||
<div class="container" style="max-width: 800px;">
|
|
||||||
<div class="tab-content" id="msgsTabContent">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="wc-row footer">
|
|
||||||
<div class="container-sm" style="padding-top: 40px">
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# This file is autogenerated by pip-compile with Python 3.10
|
# This file is autogenerated by pip-compile with Python 3.12
|
||||||
# by the following command:
|
# by the following command:
|
||||||
#
|
#
|
||||||
# pip-compile --annotation-style=line requirements-dev.in
|
# pip-compile --annotation-style=line requirements-dev.in
|
||||||
@ -20,10 +20,9 @@ click==8.1.7 # via black, fixit, moreorless, pip-tools
|
|||||||
colorama==0.4.6 # via tox
|
colorama==0.4.6 # via tox
|
||||||
commonmark==0.9.1 # via rich
|
commonmark==0.9.1 # via rich
|
||||||
configargparse==1.7 # via gray
|
configargparse==1.7 # via gray
|
||||||
coverage[toml]==7.6.8 # via pytest-cov
|
coverage[toml]==7.6.9 # via pytest-cov
|
||||||
distlib==0.3.9 # via virtualenv
|
distlib==0.3.9 # via virtualenv
|
||||||
docutils==0.21.2 # via m2r, sphinx
|
docutils==0.21.2 # via m2r, sphinx
|
||||||
exceptiongroup==1.2.2 # via pytest
|
|
||||||
filelock==3.16.1 # via tox, virtualenv
|
filelock==3.16.1 # via tox, virtualenv
|
||||||
fixit==2.1.0 # via gray
|
fixit==2.1.0 # via gray
|
||||||
flake8==7.1.1 # via -r requirements-dev.in, pep8-naming
|
flake8==7.1.1 # via -r requirements-dev.in, pep8-naming
|
||||||
@ -71,10 +70,9 @@ sphinxcontrib-qthelp==2.0.0 # via sphinx
|
|||||||
sphinxcontrib-serializinghtml==2.0.0 # via sphinx
|
sphinxcontrib-serializinghtml==2.0.0 # via sphinx
|
||||||
tokenize-rt==6.1.0 # via add-trailing-comma, pyupgrade
|
tokenize-rt==6.1.0 # via add-trailing-comma, pyupgrade
|
||||||
toml==0.10.2 # via autoflake
|
toml==0.10.2 # via autoflake
|
||||||
tomli==2.2.1 # via black, build, check-manifest, coverage, fixit, mypy, pip-tools, pyproject-api, pytest, sphinx, tox
|
|
||||||
tox==4.23.2 # via -r requirements-dev.in
|
tox==4.23.2 # via -r requirements-dev.in
|
||||||
trailrunner==1.4.0 # via fixit
|
trailrunner==1.4.0 # via fixit
|
||||||
typing-extensions==4.12.2 # via black, mypy, tox
|
typing-extensions==4.12.2 # via mypy
|
||||||
unify==0.5 # via gray
|
unify==0.5 # via gray
|
||||||
untokenize==0.1.1 # via unify
|
untokenize==0.1.1 # via unify
|
||||||
urllib3==2.2.3 # via requests
|
urllib3==2.2.3 # via requests
|
||||||
|
@ -2,25 +2,17 @@ aprslib>=0.7.0
|
|||||||
# For the list-plugins pypi.org search scraping
|
# For the list-plugins pypi.org search scraping
|
||||||
beautifulsoup4
|
beautifulsoup4
|
||||||
click
|
click
|
||||||
click-params
|
|
||||||
dataclasses-json
|
dataclasses-json
|
||||||
flask
|
|
||||||
flask-httpauth
|
|
||||||
flask-socketio
|
|
||||||
geopy
|
geopy
|
||||||
imapclient
|
imapclient
|
||||||
kiss3
|
kiss3
|
||||||
loguru
|
loguru
|
||||||
oslo.config
|
oslo.config
|
||||||
pluggy
|
pluggy
|
||||||
python-socketio
|
|
||||||
requests
|
requests
|
||||||
# Pinned due to gray needing 12.6.0
|
# Pinned due to gray needing 12.6.0
|
||||||
rich~=12.6.0
|
rich~=12.6.0
|
||||||
rush
|
rush
|
||||||
shellingham
|
|
||||||
six
|
|
||||||
tabulate
|
|
||||||
thesmuggler
|
thesmuggler
|
||||||
tzlocal
|
tzlocal
|
||||||
update_checker
|
update_checker
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# This file is autogenerated by pip-compile with Python 3.10
|
# This file is autogenerated by pip-compile with Python 3.12
|
||||||
# by the following command:
|
# by the following command:
|
||||||
#
|
#
|
||||||
# pip-compile --annotation-style=line requirements.in
|
# pip-compile --annotation-style=line requirements.in
|
||||||
@ -8,31 +8,20 @@ aprslib==0.7.2 # via -r requirements.in
|
|||||||
attrs==24.2.0 # via ax253, kiss3, rush
|
attrs==24.2.0 # via ax253, kiss3, rush
|
||||||
ax253==0.1.5.post1 # via kiss3
|
ax253==0.1.5.post1 # via kiss3
|
||||||
beautifulsoup4==4.12.3 # via -r requirements.in
|
beautifulsoup4==4.12.3 # via -r requirements.in
|
||||||
bidict==0.23.1 # via python-socketio
|
|
||||||
bitarray==3.0.0 # via ax253, kiss3
|
bitarray==3.0.0 # via ax253, kiss3
|
||||||
blinker==1.9.0 # via flask
|
|
||||||
certifi==2024.8.30 # via requests
|
certifi==2024.8.30 # via requests
|
||||||
charset-normalizer==3.4.0 # via requests
|
charset-normalizer==3.4.0 # via requests
|
||||||
click==8.1.7 # via -r requirements.in, click-params, flask
|
click==8.1.7 # via -r requirements.in
|
||||||
click-params==0.5.0 # via -r requirements.in
|
|
||||||
commonmark==0.9.1 # via rich
|
commonmark==0.9.1 # via rich
|
||||||
dataclasses-json==0.6.7 # via -r requirements.in
|
dataclasses-json==0.6.7 # via -r requirements.in
|
||||||
debtcollector==3.0.0 # via oslo-config
|
debtcollector==3.0.0 # via oslo-config
|
||||||
deprecated==1.2.15 # via click-params
|
|
||||||
flask==3.1.0 # via -r requirements.in, flask-httpauth, flask-socketio
|
|
||||||
flask-httpauth==4.8.0 # via -r requirements.in
|
|
||||||
flask-socketio==5.4.1 # via -r requirements.in
|
|
||||||
geographiclib==2.0 # via geopy
|
geographiclib==2.0 # via geopy
|
||||||
geopy==2.4.1 # via -r requirements.in
|
geopy==2.4.1 # via -r requirements.in
|
||||||
h11==0.14.0 # via wsproto
|
|
||||||
idna==3.10 # via requests
|
idna==3.10 # via requests
|
||||||
imapclient==3.0.1 # via -r requirements.in
|
imapclient==3.0.1 # via -r requirements.in
|
||||||
importlib-metadata==8.5.0 # via ax253, kiss3
|
importlib-metadata==8.5.0 # via ax253, kiss3
|
||||||
itsdangerous==2.2.0 # via flask
|
|
||||||
jinja2==3.1.4 # via flask
|
|
||||||
kiss3==8.0.0 # via -r requirements.in
|
kiss3==8.0.0 # via -r requirements.in
|
||||||
loguru==0.7.2 # via -r requirements.in
|
loguru==0.7.3 # via -r requirements.in
|
||||||
markupsafe==3.0.2 # via jinja2, werkzeug
|
|
||||||
marshmallow==3.23.1 # via dataclasses-json
|
marshmallow==3.23.1 # via dataclasses-json
|
||||||
mypy-extensions==1.0.0 # via typing-inspect
|
mypy-extensions==1.0.0 # via typing-inspect
|
||||||
netaddr==1.3.0 # via oslo-config
|
netaddr==1.3.0 # via oslo-config
|
||||||
@ -44,20 +33,14 @@ pluggy==1.5.0 # via -r requirements.in
|
|||||||
pygments==2.18.0 # via rich
|
pygments==2.18.0 # via rich
|
||||||
pyserial==3.5 # via pyserial-asyncio
|
pyserial==3.5 # via pyserial-asyncio
|
||||||
pyserial-asyncio==0.6 # via kiss3
|
pyserial-asyncio==0.6 # via kiss3
|
||||||
python-engineio==4.10.1 # via python-socketio
|
|
||||||
python-socketio==5.11.4 # via -r requirements.in, flask-socketio
|
|
||||||
pytz==2024.2 # via -r requirements.in
|
pytz==2024.2 # via -r requirements.in
|
||||||
pyyaml==6.0.2 # via oslo-config
|
pyyaml==6.0.2 # via oslo-config
|
||||||
requests==2.32.3 # via -r requirements.in, oslo-config, update-checker
|
requests==2.32.3 # via -r requirements.in, oslo-config, update-checker
|
||||||
rfc3986==2.0.0 # via oslo-config
|
rfc3986==2.0.0 # via oslo-config
|
||||||
rich==12.6.0 # via -r requirements.in
|
rich==12.6.0 # via -r requirements.in
|
||||||
rush==2021.4.0 # via -r requirements.in
|
rush==2021.4.0 # via -r requirements.in
|
||||||
shellingham==1.5.4 # via -r requirements.in
|
|
||||||
simple-websocket==1.1.0 # via python-engineio
|
|
||||||
six==1.16.0 # via -r requirements.in
|
|
||||||
soupsieve==2.6 # via beautifulsoup4
|
soupsieve==2.6 # via beautifulsoup4
|
||||||
stevedore==5.4.0 # via oslo-config
|
stevedore==5.4.0 # via oslo-config
|
||||||
tabulate==0.9.0 # via -r requirements.in
|
|
||||||
thesmuggler==1.0.1 # via -r requirements.in
|
thesmuggler==1.0.1 # via -r requirements.in
|
||||||
timeago==1.0.16 # via -r requirements.in
|
timeago==1.0.16 # via -r requirements.in
|
||||||
typing-extensions==4.12.2 # via typing-inspect
|
typing-extensions==4.12.2 # via typing-inspect
|
||||||
@ -65,8 +48,5 @@ typing-inspect==0.9.0 # via dataclasses-json
|
|||||||
tzlocal==5.2 # via -r requirements.in
|
tzlocal==5.2 # via -r requirements.in
|
||||||
update-checker==0.18.0 # via -r requirements.in
|
update-checker==0.18.0 # via -r requirements.in
|
||||||
urllib3==2.2.3 # via requests
|
urllib3==2.2.3 # via requests
|
||||||
validators==0.22.0 # via click-params
|
wrapt==1.17.0 # via -r requirements.in, debtcollector
|
||||||
werkzeug==3.1.3 # via flask
|
|
||||||
wrapt==1.17.0 # via -r requirements.in, debtcollector, deprecated
|
|
||||||
wsproto==1.2.0 # via simple-websocket
|
|
||||||
zipp==3.21.0 # via importlib-metadata
|
zipp==3.21.0 # via importlib-metadata
|
||||||
|
@ -27,8 +27,8 @@ class TestSendMessageCommand(unittest.TestCase):
|
|||||||
if password:
|
if password:
|
||||||
CONF.aprs_network.password = password
|
CONF.aprs_network.password = password
|
||||||
|
|
||||||
CONF.admin.user = "admin"
|
# CONF.aprsd_admin_extension.user = "admin"
|
||||||
CONF.admin.password = "password"
|
# CONF.aprsd_admin_extension.password = "password"
|
||||||
|
|
||||||
@mock.patch("aprsd.log.log.setup_logging")
|
@mock.patch("aprsd.log.log.setup_logging")
|
||||||
def test_no_tocallsign(self, mock_logging):
|
def test_no_tocallsign(self, mock_logging):
|
||||||
|
@ -1,90 +0,0 @@
|
|||||||
import typing as t
|
|
||||||
import unittest
|
|
||||||
from unittest import mock
|
|
||||||
|
|
||||||
from click.testing import CliRunner
|
|
||||||
import flask
|
|
||||||
import flask_socketio
|
|
||||||
from oslo_config import cfg
|
|
||||||
|
|
||||||
from aprsd import conf # noqa: F401
|
|
||||||
from aprsd.client import fake as fake_client
|
|
||||||
from aprsd.cmds import webchat # noqa
|
|
||||||
from aprsd.packets import core
|
|
||||||
|
|
||||||
from .. import fake
|
|
||||||
|
|
||||||
|
|
||||||
CONF = cfg.CONF
|
|
||||||
F = t.TypeVar("F", bound=t.Callable[..., t.Any])
|
|
||||||
|
|
||||||
|
|
||||||
class TestSendMessageCommand(unittest.TestCase):
|
|
||||||
|
|
||||||
def config_and_init(self, login=None, password=None):
|
|
||||||
CONF.callsign = fake.FAKE_TO_CALLSIGN
|
|
||||||
CONF.trace_enabled = False
|
|
||||||
CONF.watch_list.packet_keep_count = 1
|
|
||||||
if login:
|
|
||||||
CONF.aprs_network.login = login
|
|
||||||
if password:
|
|
||||||
CONF.aprs_network.password = password
|
|
||||||
|
|
||||||
CONF.admin.user = "admin"
|
|
||||||
CONF.admin.password = "password"
|
|
||||||
|
|
||||||
@mock.patch("aprsd.log.log.setup_logging")
|
|
||||||
def test_init_flask(self, mock_logging):
|
|
||||||
"""Make sure we get an error if there is no login and config."""
|
|
||||||
|
|
||||||
CliRunner()
|
|
||||||
self.config_and_init()
|
|
||||||
|
|
||||||
socketio = webchat.init_flask("DEBUG", False)
|
|
||||||
self.assertIsInstance(socketio, flask_socketio.SocketIO)
|
|
||||||
self.assertIsInstance(webchat.flask_app, flask.Flask)
|
|
||||||
|
|
||||||
@mock.patch("aprsd.packets.tracker.PacketTrack.remove")
|
|
||||||
@mock.patch("aprsd.cmds.webchat.socketio")
|
|
||||||
def test_process_ack_packet(
|
|
||||||
self,
|
|
||||||
mock_remove, mock_socketio,
|
|
||||||
):
|
|
||||||
self.config_and_init()
|
|
||||||
mock_socketio.emit = mock.MagicMock()
|
|
||||||
# Create an ACK packet
|
|
||||||
packet = fake.fake_ack_packet()
|
|
||||||
mock_queue = mock.MagicMock()
|
|
||||||
socketio = mock.MagicMock()
|
|
||||||
wcp = webchat.WebChatProcessPacketThread(mock_queue, socketio)
|
|
||||||
|
|
||||||
wcp.process_ack_packet(packet)
|
|
||||||
mock_remove.called_once()
|
|
||||||
mock_socketio.called_once()
|
|
||||||
|
|
||||||
@mock.patch("aprsd.threads.tx.send")
|
|
||||||
@mock.patch("aprsd.packets.PacketList.rx")
|
|
||||||
@mock.patch("aprsd.cmds.webchat.socketio")
|
|
||||||
@mock.patch("aprsd.client.factory.ClientFactory.create")
|
|
||||||
def test_process_our_message_packet(
|
|
||||||
self,
|
|
||||||
mock_tx_send,
|
|
||||||
mock_packet_add,
|
|
||||||
mock_socketio,
|
|
||||||
mock_factory,
|
|
||||||
):
|
|
||||||
self.config_and_init()
|
|
||||||
mock_socketio.emit = mock.MagicMock()
|
|
||||||
packet = fake.fake_packet(
|
|
||||||
message="blah",
|
|
||||||
msg_number=1,
|
|
||||||
message_format=core.PACKET_TYPE_MESSAGE,
|
|
||||||
)
|
|
||||||
mock_factory.return_value = fake_client.APRSDFakeClient()
|
|
||||||
mock_queue = mock.MagicMock()
|
|
||||||
socketio = mock.MagicMock()
|
|
||||||
wcp = webchat.WebChatProcessPacketThread(mock_queue, socketio)
|
|
||||||
|
|
||||||
wcp.process_our_message_packet(packet)
|
|
||||||
mock_packet_add.called_once()
|
|
||||||
mock_socketio.called_once()
|
|