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",
|
||||
title="Watch List settings",
|
||||
)
|
||||
webchat_group = cfg.OptGroup(
|
||||
name="webchat",
|
||||
title="Settings specific to the webchat command",
|
||||
)
|
||||
|
||||
registry_group = cfg.OptGroup(
|
||||
name="aprs_registry",
|
||||
title="APRS Registry settings",
|
||||
)
|
||||
|
||||
|
||||
aprsd_opts = [
|
||||
cfg.StrOpt(
|
||||
"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 = [
|
||||
cfg.BoolOpt(
|
||||
"enabled",
|
||||
@ -261,8 +228,6 @@ def register_opts(config):
|
||||
config.register_opts(enabled_plugins_opts)
|
||||
config.register_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_opts(registry_opts, group=registry_group)
|
||||
|
||||
@ -271,6 +236,5 @@ def list_opts():
|
||||
return {
|
||||
"DEFAULT": (aprsd_opts + enabled_plugins_opts),
|
||||
watch_list_group.name: watch_list_opts,
|
||||
webchat_group.name: webchat_opts,
|
||||
registry_group.name: registry_opts,
|
||||
}
|
||||
|
@ -68,19 +68,9 @@ def setup_logging(loglevel=None, quiet=False):
|
||||
"aprslib.parsing",
|
||||
"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.
|
||||
disable_list = imap_list + aprslib_list + webserver_list
|
||||
disable_list = imap_list + aprslib_list
|
||||
|
||||
# remove every other logger's handlers
|
||||
# and propagate to root logger
|
||||
@ -91,12 +81,6 @@ def setup_logging(loglevel=None, quiet=False):
|
||||
else:
|
||||
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 = [
|
||||
{
|
||||
"sink": sys.stdout,
|
||||
|
@ -55,7 +55,7 @@ def cli(ctx):
|
||||
def load_commands():
|
||||
from .cmds import ( # noqa
|
||||
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:
|
||||
#
|
||||
# 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
|
||||
commonmark==0.9.1 # via rich
|
||||
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
|
||||
docutils==0.21.2 # via m2r, sphinx
|
||||
exceptiongroup==1.2.2 # via pytest
|
||||
filelock==3.16.1 # via tox, virtualenv
|
||||
fixit==2.1.0 # via gray
|
||||
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
|
||||
tokenize-rt==6.1.0 # via add-trailing-comma, pyupgrade
|
||||
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
|
||||
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
|
||||
untokenize==0.1.1 # via unify
|
||||
urllib3==2.2.3 # via requests
|
||||
|
@ -2,25 +2,17 @@ aprslib>=0.7.0
|
||||
# For the list-plugins pypi.org search scraping
|
||||
beautifulsoup4
|
||||
click
|
||||
click-params
|
||||
dataclasses-json
|
||||
flask
|
||||
flask-httpauth
|
||||
flask-socketio
|
||||
geopy
|
||||
imapclient
|
||||
kiss3
|
||||
loguru
|
||||
oslo.config
|
||||
pluggy
|
||||
python-socketio
|
||||
requests
|
||||
# Pinned due to gray needing 12.6.0
|
||||
rich~=12.6.0
|
||||
rush
|
||||
shellingham
|
||||
six
|
||||
tabulate
|
||||
thesmuggler
|
||||
tzlocal
|
||||
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:
|
||||
#
|
||||
# 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
|
||||
ax253==0.1.5.post1 # via kiss3
|
||||
beautifulsoup4==4.12.3 # via -r requirements.in
|
||||
bidict==0.23.1 # via python-socketio
|
||||
bitarray==3.0.0 # via ax253, kiss3
|
||||
blinker==1.9.0 # via flask
|
||||
certifi==2024.8.30 # via requests
|
||||
charset-normalizer==3.4.0 # via requests
|
||||
click==8.1.7 # via -r requirements.in, click-params, flask
|
||||
click-params==0.5.0 # via -r requirements.in
|
||||
click==8.1.7 # via -r requirements.in
|
||||
commonmark==0.9.1 # via rich
|
||||
dataclasses-json==0.6.7 # via -r requirements.in
|
||||
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
|
||||
geopy==2.4.1 # via -r requirements.in
|
||||
h11==0.14.0 # via wsproto
|
||||
idna==3.10 # via requests
|
||||
imapclient==3.0.1 # via -r requirements.in
|
||||
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
|
||||
loguru==0.7.2 # via -r requirements.in
|
||||
markupsafe==3.0.2 # via jinja2, werkzeug
|
||||
loguru==0.7.3 # via -r requirements.in
|
||||
marshmallow==3.23.1 # via dataclasses-json
|
||||
mypy-extensions==1.0.0 # via typing-inspect
|
||||
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
|
||||
pyserial==3.5 # via pyserial-asyncio
|
||||
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
|
||||
pyyaml==6.0.2 # via oslo-config
|
||||
requests==2.32.3 # via -r requirements.in, oslo-config, update-checker
|
||||
rfc3986==2.0.0 # via oslo-config
|
||||
rich==12.6.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
|
||||
stevedore==5.4.0 # via oslo-config
|
||||
tabulate==0.9.0 # via -r requirements.in
|
||||
thesmuggler==1.0.1 # via -r requirements.in
|
||||
timeago==1.0.16 # via -r requirements.in
|
||||
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
|
||||
update-checker==0.18.0 # via -r requirements.in
|
||||
urllib3==2.2.3 # via requests
|
||||
validators==0.22.0 # via click-params
|
||||
werkzeug==3.1.3 # via flask
|
||||
wrapt==1.17.0 # via -r requirements.in, debtcollector, deprecated
|
||||
wsproto==1.2.0 # via simple-websocket
|
||||
wrapt==1.17.0 # via -r requirements.in, debtcollector
|
||||
zipp==3.21.0 # via importlib-metadata
|
||||
|
@ -27,8 +27,8 @@ class TestSendMessageCommand(unittest.TestCase):
|
||||
if password:
|
||||
CONF.aprs_network.password = password
|
||||
|
||||
CONF.admin.user = "admin"
|
||||
CONF.admin.password = "password"
|
||||
# CONF.aprsd_admin_extension.user = "admin"
|
||||
# CONF.aprsd_admin_extension.password = "password"
|
||||
|
||||
@mock.patch("aprsd.log.log.setup_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()
|