Release 0.9.0
This commit is contained in:
600
bot/modules/webserver/__init__.py
Normal file
600
bot/modules/webserver/__init__.py
Normal file
@@ -0,0 +1,600 @@
|
||||
import functools
|
||||
import os
|
||||
from bot import started_modules_dict
|
||||
from bot.constants import WEBSOCKET_PING_TIMEOUT, WEBSOCKET_PING_INTERVAL
|
||||
from flask_socketio import disconnect
|
||||
|
||||
from bot.module import Module
|
||||
from bot import loaded_modules_dict
|
||||
from bot.logger import get_logger
|
||||
from .user import User
|
||||
|
||||
import re
|
||||
from time import time
|
||||
from socket import socket, AF_INET, SOCK_DGRAM
|
||||
from flask import Flask, request, redirect, session, Response
|
||||
from markupsafe import Markup
|
||||
from flask_login import LoginManager, login_required, login_user, current_user, logout_user
|
||||
from flask_socketio import SocketIO, emit
|
||||
from requests import post, get
|
||||
from urllib.parse import urlencode
|
||||
from collections.abc import KeysView
|
||||
from threading import Thread
|
||||
import string
|
||||
import random
|
||||
|
||||
# Initialize logger for webserver module
|
||||
logger = get_logger("webserver")
|
||||
|
||||
|
||||
class Webserver(Module):
|
||||
app = object
|
||||
websocket = object
|
||||
login_manager = object
|
||||
|
||||
connected_clients = dict
|
||||
broadcast_queue = dict
|
||||
send_data_to_client_hook = object
|
||||
game_server_session_id = None
|
||||
|
||||
def __init__(self):
|
||||
setattr(self, "default_options", {
|
||||
"title": "chrani-bot tng",
|
||||
"module_name": self.get_module_identifier()[7:],
|
||||
"host": "0.0.0.0",
|
||||
"port": 5000,
|
||||
"Flask_secret_key": "thisissecret",
|
||||
"SocketIO_asynch_mode": "gevent",
|
||||
"SocketIO_use_reloader": False,
|
||||
"SocketIO_debug": False,
|
||||
"engineio_logger": False
|
||||
})
|
||||
setattr(self, "required_modules", [
|
||||
'module_dom'
|
||||
])
|
||||
self.next_cycle = 0
|
||||
self.send_data_to_client_hook = self.send_data_to_client
|
||||
self.run_observer_interval = 5
|
||||
Module.__init__(self)
|
||||
|
||||
@staticmethod
|
||||
def get_module_identifier():
|
||||
return "module_webserver"
|
||||
|
||||
# region SocketIO stuff
|
||||
@staticmethod
|
||||
def dispatch_socket_event(target_module, event_data, dispatchers_steamid):
|
||||
module_identifier = "module_{}".format(target_module)
|
||||
try:
|
||||
started_modules_dict[module_identifier].on_socket_event(event_data, dispatchers_steamid)
|
||||
except KeyError as error:
|
||||
logger.error("socket_event_module_not_found",
|
||||
user=dispatchers_steamid,
|
||||
target_module=module_identifier,
|
||||
error=str(error))
|
||||
|
||||
@staticmethod
|
||||
def authenticated_only(f):
|
||||
@functools.wraps(f)
|
||||
def wrapped(*args, **kwargs):
|
||||
if not current_user.is_authenticated:
|
||||
disconnect()
|
||||
else:
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return wrapped
|
||||
|
||||
# Reusable SystemRandom instance for efficient token generation
|
||||
_random = random.SystemRandom()
|
||||
|
||||
@classmethod
|
||||
def random_string(cls, length):
|
||||
"""Generate a random string of given length using uppercase letters and digits."""
|
||||
chars = string.ascii_uppercase + string.digits
|
||||
return ''.join(cls._random.choice(chars) for _ in range(length))
|
||||
# endregion
|
||||
|
||||
# region Standard module stuff
|
||||
def setup(self, options=dict):
|
||||
Module.setup(self, options)
|
||||
|
||||
if self.options.get("host") == self.default_options.get("host"):
|
||||
self.options["host"] = self.get_ip()
|
||||
|
||||
self.connected_clients = {}
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config["SECRET_KEY"] = self.options.get("Flask_secret_key", self.default_options.get("Flask_secret_key"))
|
||||
|
||||
login_manager = LoginManager()
|
||||
login_manager.init_app(app)
|
||||
|
||||
self.app = app
|
||||
self.websocket = SocketIO(
|
||||
app,
|
||||
async_mode=self.options.get("SocketIO_asynch_mode", self.default_options.get("SocketIO_asynch_mode")),
|
||||
debug=self.options.get("SocketIO_debug", self.default_options.get("SocketIO_debug")),
|
||||
engineio_logger=self.options.get("engineio_logger", self.default_options.get("engineio_logger")),
|
||||
use_reloader=self.options.get("SocketIO_use_reloader", self.default_options.get("SocketIO_use_reloader")),
|
||||
passthrough_errors=True,
|
||||
ping_timeout=WEBSOCKET_PING_TIMEOUT,
|
||||
ping_interval=WEBSOCKET_PING_INTERVAL
|
||||
)
|
||||
self.login_manager = login_manager
|
||||
# endregion
|
||||
|
||||
def get_ip(self):
|
||||
s = socket(AF_INET, SOCK_DGRAM)
|
||||
try:
|
||||
# doesn't even have to be reachable
|
||||
s.connect(('10.255.255.255', 1))
|
||||
host = s.getsockname()[0]
|
||||
logger.info("ip_discovered", ip=host)
|
||||
except Exception as error:
|
||||
host = self.default_options.get("host")
|
||||
logger.warn("ip_discovery_failed", fallback_ip=host, error=str(error))
|
||||
finally:
|
||||
s.close()
|
||||
return host
|
||||
|
||||
def login_to_game_server(self):
|
||||
"""Login to game server web interface and store session cookie"""
|
||||
telnet_module = loaded_modules_dict.get("module_telnet")
|
||||
if not telnet_module:
|
||||
logger.warn("game_server_login_telnet_missing",
|
||||
reason="telnet module not loaded")
|
||||
return
|
||||
|
||||
game_host = telnet_module.options.get("host")
|
||||
telnet_port = telnet_module.options.get("port", 8081)
|
||||
web_port = telnet_port + 1
|
||||
|
||||
web_username = telnet_module.options.get("web_username", "")
|
||||
web_password = telnet_module.options.get("web_password", "")
|
||||
|
||||
if not web_username or not web_password:
|
||||
logger.warn("game_server_login_no_credentials",
|
||||
host=game_host,
|
||||
port=web_port,
|
||||
impact="map tiles unavailable")
|
||||
return
|
||||
|
||||
login_url = f'http://{game_host}:{web_port}/session/login'
|
||||
|
||||
try:
|
||||
response = post(
|
||||
login_url,
|
||||
json={"username": web_username, "password": web_password},
|
||||
headers={"Content-Type": "application/json"},
|
||||
timeout=5
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
sid_cookie = response.cookies.get('sid')
|
||||
if sid_cookie:
|
||||
self.game_server_session_id = sid_cookie
|
||||
# Success - no log needed
|
||||
else:
|
||||
logger.warn("game_server_login_no_sid_cookie",
|
||||
host=game_host,
|
||||
port=web_port,
|
||||
status=200)
|
||||
else:
|
||||
logger.error("game_server_login_failed",
|
||||
host=game_host,
|
||||
port=web_port,
|
||||
status=response.status_code,
|
||||
url=login_url)
|
||||
except Exception as e:
|
||||
logger.error("game_server_login_exception",
|
||||
host=game_host,
|
||||
port=web_port,
|
||||
error=str(e),
|
||||
error_type=type(e).__name__)
|
||||
|
||||
def send_data_to_client(self, *args, payload=None, **kwargs):
|
||||
data_type = kwargs.get("data_type", "widget_content")
|
||||
target_element = kwargs.get("target_element", None)
|
||||
clients = kwargs.get("clients", None)
|
||||
|
||||
if all([
|
||||
clients is not None,
|
||||
not isinstance(clients, KeysView),
|
||||
not isinstance(clients, list)
|
||||
]):
|
||||
if re.match(r"^(\d{17})$", clients):
|
||||
clients = [clients]
|
||||
|
||||
method = kwargs.get("method", "update")
|
||||
status = kwargs.get("status", "")
|
||||
|
||||
if clients is None:
|
||||
clients = "all"
|
||||
|
||||
with self.app.app_context():
|
||||
data_packages_to_send = []
|
||||
widget_options = {
|
||||
"method": method,
|
||||
"status": status,
|
||||
"payload": payload,
|
||||
"data_type": data_type,
|
||||
"target_element": target_element,
|
||||
}
|
||||
|
||||
# Determine which clients to send to
|
||||
if clients == "all":
|
||||
# Send to all connected clients individually
|
||||
# Note: broadcast=True doesn't work with self.websocket.emit() in gevent mode
|
||||
clients = list(self.connected_clients.keys())
|
||||
|
||||
if clients is not None and isinstance(clients, list):
|
||||
for steamid in clients:
|
||||
try:
|
||||
# Send to ALL socket connections for this user (multiple browsers)
|
||||
user = self.connected_clients[steamid]
|
||||
for socket_id in user.socket_ids:
|
||||
emit_options = {
|
||||
"room": socket_id
|
||||
}
|
||||
data_packages_to_send.append([widget_options, emit_options])
|
||||
except (AttributeError, KeyError) as error:
|
||||
# User connection state is inconsistent - log and skip this client
|
||||
logger.debug(
|
||||
"socket_send_failed_no_client",
|
||||
steamid=steamid,
|
||||
data_type=data_type,
|
||||
error_type=type(error).__name__,
|
||||
has_client=steamid in self.connected_clients,
|
||||
has_sockets=len(self.connected_clients.get(steamid, type('obj', (), {'socket_ids': []})).socket_ids) > 0
|
||||
)
|
||||
|
||||
for data_package in data_packages_to_send:
|
||||
try:
|
||||
self.websocket.emit(
|
||||
'data',
|
||||
data_package[0],
|
||||
**data_package[1]
|
||||
)
|
||||
except Exception as error:
|
||||
# Socket emit failed - log the error
|
||||
logger.error(
|
||||
"socket_emit_failed",
|
||||
data_type=data_type,
|
||||
error=str(error),
|
||||
error_type=type(error).__name__
|
||||
)
|
||||
|
||||
def emit_event_status(self, module, event_data, recipient_steamid, status=None):
|
||||
clients = recipient_steamid
|
||||
|
||||
self.send_data_to_client_hook(
|
||||
module,
|
||||
payload=event_data,
|
||||
data_type="status_message",
|
||||
clients=clients,
|
||||
status=status
|
||||
)
|
||||
|
||||
def run(self):
|
||||
# Login to game server web interface for map tile access
|
||||
self.login_to_game_server()
|
||||
|
||||
template_header = self.templates.get_template('frontpage/header.html')
|
||||
template_frontend = self.templates.get_template('frontpage/index.html')
|
||||
template_footer = self.templates.get_template('frontpage/footer.html')
|
||||
|
||||
# region Management function and routes without any user-display or interaction
|
||||
@self.login_manager.user_loader
|
||||
def user_loader(steamid):
|
||||
webserver_user = self.connected_clients.get(steamid, False)
|
||||
if not webserver_user:
|
||||
""" This is where the authentication will happen, see if that user in in your allowed players database or
|
||||
whatever """
|
||||
webserver_user = User(steamid, time())
|
||||
self.connected_clients[steamid] = webserver_user
|
||||
|
||||
return webserver_user
|
||||
|
||||
@self.app.route('/login')
|
||||
def login():
|
||||
steam_openid_url = 'https://steamcommunity.com/openid/login'
|
||||
u = {
|
||||
'openid.ns': "http://specs.openid.net/auth/2.0",
|
||||
'openid.identity': "http://specs.openid.net/auth/2.0/identifier_select",
|
||||
'openid.claimed_id': "http://specs.openid.net/auth/2.0/identifier_select",
|
||||
'openid.mode': 'checkid_setup',
|
||||
'openid.return_to': "http://{host}:{port}/authenticate".format(
|
||||
host=self.options.get("host", self.default_options.get("host")),
|
||||
port=self.options.get("port", self.default_options.get("port"))
|
||||
),
|
||||
'openid.realm': "http://{host}:{port}".format(
|
||||
host=self.options.get("host", self.default_options.get("host")),
|
||||
port=self.options.get("port", self.default_options.get("port"))
|
||||
)
|
||||
}
|
||||
query_string = urlencode(u)
|
||||
auth_url = "{url}?{query_string}".format(
|
||||
url=steam_openid_url,
|
||||
query_string=query_string
|
||||
)
|
||||
return redirect(auth_url)
|
||||
|
||||
@self.app.route('/authenticate', methods=['GET'])
|
||||
def setup():
|
||||
def validate(signed_params):
|
||||
steam_login_url_base = "https://steamcommunity.com/openid/login"
|
||||
params = {
|
||||
"openid.assoc_handle": signed_params["openid.assoc_handle"],
|
||||
"openid.sig": signed_params["openid.sig"],
|
||||
"openid.ns": signed_params["openid.ns"],
|
||||
"openid.mode": "check_authentication"
|
||||
}
|
||||
|
||||
params_dict = signed_params.to_dict()
|
||||
params_dict.update(params)
|
||||
|
||||
params_dict["openid.mode"] = "check_authentication"
|
||||
params_dict["openid.signed"] = params_dict["openid.signed"]
|
||||
|
||||
try:
|
||||
response = post(steam_login_url_base, data=params_dict)
|
||||
valid_response = "is_valid:true" in response.text
|
||||
except TypeError as error:
|
||||
valid_response = False
|
||||
|
||||
return valid_response
|
||||
|
||||
if validate(request.args):
|
||||
p = re.search(r"/(?P<steamid>([0-9]{17}))", str(request.args["openid.claimed_id"]))
|
||||
if p:
|
||||
steamid = p.group("steamid")
|
||||
webserver_user = User(steamid)
|
||||
login_user(webserver_user, remember=True)
|
||||
|
||||
return redirect("/")
|
||||
|
||||
@self.app.route('/logout')
|
||||
@login_required
|
||||
def logout():
|
||||
# Normal logout - no log needed
|
||||
self.connected_clients.pop(current_user.id, None) # Safe deletion
|
||||
for module in loaded_modules_dict.values():
|
||||
module.on_socket_disconnect(current_user.id)
|
||||
logout_user()
|
||||
return redirect("/")
|
||||
# endregion
|
||||
|
||||
# region Actual routes the user gets to see and use
|
||||
""" actual pages """
|
||||
@self.app.route('/unauthorized')
|
||||
@self.login_manager.unauthorized_handler
|
||||
def unauthorized_handler():
|
||||
return redirect("/")
|
||||
|
||||
@self.app.route('/')
|
||||
def protected():
|
||||
header_markup = self.template_render_hook(
|
||||
self,
|
||||
template=template_header,
|
||||
current_user=current_user,
|
||||
title=self.options.get("title", self.default_options.get("title"))
|
||||
)
|
||||
footer_markup = self.template_render_hook(
|
||||
self,
|
||||
template=template_footer
|
||||
)
|
||||
|
||||
instance_token = self.random_string(20)
|
||||
template_options = {
|
||||
'current_user': current_user,
|
||||
'header': header_markup,
|
||||
'footer': footer_markup,
|
||||
'instance_token': instance_token
|
||||
|
||||
}
|
||||
if not current_user.is_authenticated:
|
||||
main_output = (
|
||||
'<div id="unauthorized_disclaimer" class="single_screen">'
|
||||
'<p>Welcome to the <strong>chrani-bot: The Next Generation</strong></p>'
|
||||
'<p>You can <a href="/login">use your steam-account to log in</a>!</p>'
|
||||
'</div>'
|
||||
)
|
||||
main_markup = Markup(main_output)
|
||||
template_options['main'] = main_markup
|
||||
|
||||
return self.template_render_hook(
|
||||
self,
|
||||
template=template_frontend,
|
||||
**template_options
|
||||
)
|
||||
|
||||
@self.app.route('/map_tiles/<int:z>/<x>/<y>.png')
|
||||
def map_tile_proxy(z, x, y):
|
||||
"""Proxy map tiles from game server to avoid CORS issues"""
|
||||
# Parse x and y as integers (Flask's <int> doesn't support negative numbers)
|
||||
try:
|
||||
x = int(x)
|
||||
y = int(y)
|
||||
except ValueError:
|
||||
logger.warn("tile_request_invalid_coords",
|
||||
z=z, x=x, y=y,
|
||||
user=current_user.id if current_user.is_authenticated else "anonymous")
|
||||
return Response(status=400)
|
||||
|
||||
# Get game server host and port from telnet module config
|
||||
telnet_module = loaded_modules_dict.get("module_telnet")
|
||||
if telnet_module:
|
||||
game_host = telnet_module.options.get("host", "localhost")
|
||||
telnet_port = telnet_module.options.get("port", 8081)
|
||||
# Web interface port is always telnet port + 1
|
||||
web_port = telnet_port + 1
|
||||
else:
|
||||
game_host = "localhost"
|
||||
web_port = 8082 # Default 7D2D web port
|
||||
|
||||
# 7D2D uses inverted Y-axis for tiles
|
||||
y_flipped = (-y) - 1
|
||||
tile_url = f'http://{game_host}:{web_port}/map/{z}/{x}/{y_flipped}.png'
|
||||
|
||||
# Forward relevant headers from browser to game server
|
||||
headers = {}
|
||||
if request.headers.get('User-Agent'):
|
||||
headers['User-Agent'] = request.headers.get('User-Agent')
|
||||
if request.headers.get('Referer'):
|
||||
headers['Referer'] = request.headers.get('Referer')
|
||||
|
||||
# Add game server session cookie for authentication
|
||||
cookies = {}
|
||||
if self.game_server_session_id:
|
||||
cookies['sid'] = self.game_server_session_id
|
||||
|
||||
try:
|
||||
response = get(tile_url, headers=headers, cookies=cookies, timeout=5)
|
||||
|
||||
# Only log non-200 responses
|
||||
if response.status_code != 200:
|
||||
logger.error("tile_fetch_failed",
|
||||
z=z, x=x, y=y,
|
||||
user=current_user.id if current_user.is_authenticated else "anonymous",
|
||||
status=response.status_code,
|
||||
url=tile_url,
|
||||
has_sid=bool(self.game_server_session_id))
|
||||
|
||||
return Response(
|
||||
response.content,
|
||||
status=response.status_code,
|
||||
content_type='image/png'
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error("tile_fetch_exception",
|
||||
z=z, x=x, y=y,
|
||||
user=current_user.id if current_user.is_authenticated else "anonymous",
|
||||
url=tile_url,
|
||||
error=str(e),
|
||||
error_type=type(e).__name__)
|
||||
return Response(status=404)
|
||||
|
||||
# endregion
|
||||
|
||||
# region Websocket handling
|
||||
@self.websocket.on('connect')
|
||||
@self.authenticated_only
|
||||
def connect_handler():
|
||||
if not hasattr(request, 'sid'):
|
||||
return False # not allowed here
|
||||
else:
|
||||
user = self.connected_clients[current_user.id]
|
||||
|
||||
# Check if user already has active session(s)
|
||||
if len(user.socket_ids) > 0:
|
||||
# User has active session - ask if they want to take over
|
||||
emit('session_conflict', {
|
||||
'existing_sessions': len(user.socket_ids),
|
||||
'message': 'Sie haben bereits eine aktive Session. Möchten Sie diese übernehmen? (Ungespeicherte Daten könnten verloren gehen)'
|
||||
}, room=request.sid)
|
||||
# Don't add socket yet - wait for user response
|
||||
else:
|
||||
# First session - connect normally
|
||||
user.add_socket(request.sid)
|
||||
emit('session_accepted', room=request.sid)
|
||||
for module in loaded_modules_dict.values():
|
||||
module.on_socket_connect(current_user.id)
|
||||
|
||||
@self.websocket.on('disconnect')
|
||||
def disconnect_handler():
|
||||
# Remove this socket from the user's socket list
|
||||
if current_user.is_authenticated and current_user.id in self.connected_clients:
|
||||
self.connected_clients[current_user.id].remove_socket(request.sid)
|
||||
|
||||
@self.websocket.on('ding')
|
||||
def ding_dong():
|
||||
current_user.last_seen = time()
|
||||
try:
|
||||
# Use request.sid (current socket) not current_user.sid (could be another browser!)
|
||||
emit('dong', room=request.sid)
|
||||
|
||||
except AttributeError as error:
|
||||
# user disappeared
|
||||
logger.debug("client_disappeared", user=current_user.id, sid=request.sid)
|
||||
|
||||
@self.websocket.on('session_takeover_accept')
|
||||
@self.authenticated_only
|
||||
def session_takeover_accept():
|
||||
"""User accepted to take over existing session - disconnect old sessions."""
|
||||
user = self.connected_clients[current_user.id]
|
||||
|
||||
# Disconnect all existing sessions
|
||||
old_sockets = user.socket_ids.copy()
|
||||
for old_sid in old_sockets:
|
||||
# Notify old session that it's being taken over
|
||||
emit('session_taken_over', {
|
||||
'message': 'Ihre Session wurde von einem anderen Browser übernommen.'
|
||||
}, room=old_sid)
|
||||
# Force disconnect old socket
|
||||
self.websocket.server.disconnect(old_sid)
|
||||
user.remove_socket(old_sid)
|
||||
|
||||
# Add new session
|
||||
user.add_socket(request.sid)
|
||||
emit('session_accepted', room=request.sid)
|
||||
|
||||
# Initialize widgets for new session
|
||||
for module in loaded_modules_dict.values():
|
||||
module.on_socket_connect(current_user.id)
|
||||
|
||||
logger.info("session_takeover",
|
||||
user=current_user.id,
|
||||
old_sessions=len(old_sockets),
|
||||
new_sid=request.sid)
|
||||
|
||||
@self.websocket.on('session_takeover_decline')
|
||||
@self.authenticated_only
|
||||
def session_takeover_decline():
|
||||
"""User declined to take over - disconnect new session."""
|
||||
emit('session_declined', {
|
||||
'message': 'Session-Übernahme abgelehnt. Bitte schließen Sie die andere Session zuerst.'
|
||||
}, room=request.sid)
|
||||
|
||||
# Disconnect this (new) session
|
||||
self.websocket.server.disconnect(request.sid)
|
||||
|
||||
logger.info("session_takeover_declined",
|
||||
user=current_user.id,
|
||||
declined_sid=request.sid)
|
||||
|
||||
@self.websocket.on('widget_event')
|
||||
@self.authenticated_only
|
||||
def widget_event(data):
|
||||
self.dispatch_socket_event(data[0], data[1], current_user.id)
|
||||
# endregion
|
||||
|
||||
# Check if we're running under a WSGI server (like gunicorn)
|
||||
# If so, don't start our own server thread - the WSGI server will handle it
|
||||
running_under_wsgi = os.environ.get('RUNNING_UNDER_WSGI', 'false').lower() == 'true'
|
||||
|
||||
if not running_under_wsgi:
|
||||
# Running standalone with Flask development server
|
||||
websocket_instance = Thread(
|
||||
target=self.websocket.run,
|
||||
args=[self.app],
|
||||
kwargs={
|
||||
"host": self.options.get("host", self.default_options.get("host")),
|
||||
"port": self.options.get("port", self.default_options.get("port"))
|
||||
}
|
||||
)
|
||||
websocket_instance.start()
|
||||
|
||||
while not self.stopped.wait(self.next_cycle):
|
||||
profile_start = time()
|
||||
|
||||
self.trigger_action_hook(self, event_data=["logged_in_users", {}])
|
||||
|
||||
self.last_execution_time = time() - profile_start
|
||||
self.next_cycle = self.run_observer_interval - self.last_execution_time
|
||||
else:
|
||||
# Running under WSGI server - just register routes and return
|
||||
# The module will keep running in its thread for background tasks
|
||||
logger.info("wsgi_mode_detected")
|
||||
|
||||
|
||||
loaded_modules_dict[Webserver().get_module_identifier()] = Webserver()
|
||||
42
bot/modules/webserver/actions/logged_in_users.py
Normal file
42
bot/modules/webserver/actions/logged_in_users.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from bot import loaded_modules_dict
|
||||
from os import path, pardir
|
||||
|
||||
module_name = path.basename(path.normpath(path.join(path.abspath(__file__), pardir, pardir)))
|
||||
action_name = path.basename(path.abspath(__file__))[:-3]
|
||||
|
||||
|
||||
def main_function(*args, **kwargs):
|
||||
module = args[0]
|
||||
event_data = args[1]
|
||||
event_data[1]["action_identifier"] = action_name
|
||||
|
||||
try:
|
||||
connected_clients = list(module.connected_clients.keys())
|
||||
except AttributeError:
|
||||
callback_fail(*args, **kwargs)
|
||||
|
||||
module.dom.data.upsert({
|
||||
module.get_module_identifier(): {
|
||||
"webserver_logged_in_users": connected_clients
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
def callback_success(*args, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
def callback_fail(*args, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
action_meta = {
|
||||
"description": "gets the current list of users currently logged into the webinterface",
|
||||
"main_function": main_function,
|
||||
"callback_success": callback_success,
|
||||
"callback_fail": callback_fail,
|
||||
"requires_telnet_connection": False,
|
||||
"enabled": True
|
||||
}
|
||||
|
||||
loaded_modules_dict["module_" + module_name].register_action(action_name, action_meta)
|
||||
@@ -0,0 +1,45 @@
|
||||
from bot import loaded_modules_dict
|
||||
from os import path, pardir
|
||||
|
||||
module_name = path.basename(path.normpath(path.join(path.abspath(__file__), pardir, pardir)))
|
||||
action_name = path.basename(path.abspath(__file__))[:-3]
|
||||
|
||||
|
||||
def main_function(module, event_data, dispatchers_steamid):
|
||||
action = event_data[1].get("action", None)
|
||||
event_data[1]["action_identifier"] = action_name
|
||||
|
||||
if action == "show_options":
|
||||
current_view = "options"
|
||||
elif action == "show_frontend":
|
||||
current_view = "frontend"
|
||||
else:
|
||||
module.callback_fail(callback_fail, module, event_data, dispatchers_steamid)
|
||||
return
|
||||
|
||||
module.set_current_view(dispatchers_steamid, {
|
||||
"current_view": current_view
|
||||
})
|
||||
module.callback_success(callback_success, module, event_data, dispatchers_steamid)
|
||||
|
||||
|
||||
def callback_success(module, event_data, dispatchers_steamid, match=None):
|
||||
pass
|
||||
|
||||
|
||||
def callback_fail(module, event_data, dispatchers_steamid):
|
||||
pass
|
||||
|
||||
|
||||
action_meta = {
|
||||
"description": (
|
||||
"Toggles the active widget-view for the webserver-widget"
|
||||
),
|
||||
"main_function": main_function,
|
||||
"callback_success": callback_success,
|
||||
"callback_fail": callback_fail,
|
||||
"requires_telnet_connection": False,
|
||||
"enabled": True
|
||||
}
|
||||
|
||||
loaded_modules_dict["module_" + module_name].register_action(action_name, action_meta)
|
||||
BIN
bot/modules/webserver/static/favicon.ico
Normal file
BIN
bot/modules/webserver/static/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
2
bot/modules/webserver/static/jquery-3.4.1.min.js
vendored
Normal file
2
bot/modules/webserver/static/jquery-3.4.1.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
5
bot/modules/webserver/static/lcars/000-style.css
Normal file
5
bot/modules/webserver/static/lcars/000-style.css
Normal file
@@ -0,0 +1,5 @@
|
||||
@import url("100-variables.css"); /* contains css-variables for most major elements */
|
||||
@import url("200-framework.css");
|
||||
@import url("300-header.css");
|
||||
@import url("400-main.css");
|
||||
@import url("500-footer.css");
|
||||
81
bot/modules/webserver/static/lcars/100-variables.css
Normal file
81
bot/modules/webserver/static/lcars/100-variables.css
Normal file
@@ -0,0 +1,81 @@
|
||||
@import url("110-fonts.css");
|
||||
|
||||
:root {
|
||||
font-size: 16px;
|
||||
/* colors have to be issued in HEX, so we can use them without trouble inside out JavaScript portion */
|
||||
--background: #000000;
|
||||
--main-bar-color: #939598;
|
||||
--main-bar-border-color: #6F7172;
|
||||
|
||||
/* StarTrek color information found on: https://web.archive.org/web/20190717130329/http://www.lcarscom.net/ */
|
||||
--lcars-pale-canary: #FF9999;
|
||||
--lcars-tanoi: #FFCC99;
|
||||
--lcars-golden-tanoi: #FFCC66;
|
||||
--lcars-neon-carrot: #FF9933;
|
||||
|
||||
--lcars-eggplant: #664466;
|
||||
--lcars-lilac: #CC99CC;
|
||||
--lcars-anakiwa: #99CCFF;
|
||||
--lcars-mariner: #3366CC;
|
||||
|
||||
--lcars-bahama-blue: #006699;
|
||||
--lcars-blue-bell: #9999CC;
|
||||
--lcars-melrose: #9999FF;
|
||||
--lcars-hopbush: #CC6699;
|
||||
|
||||
--lcars-chestnut-rose: #CC6666;
|
||||
--lcars-orange-peel: #FF9966;
|
||||
--lcars-atomic-tangerine: #FF9900;
|
||||
--lcars-danub: #6688CC;
|
||||
|
||||
--lcars-indigo: #4455BB;
|
||||
--lcars-lavender-purple: #9977AA;
|
||||
--lcars-cosmic: #774466;
|
||||
--lcars-red-damask: #DD6644;
|
||||
|
||||
--lcars-medium-carmine: #AA5533;
|
||||
--lcars-bourbon: #BB6622;
|
||||
--lcars-sandy-brown: #EE9955;
|
||||
--lcars-periwinkle: #CCDDFF;
|
||||
|
||||
--lcars-dodger-pale: #5599FF;
|
||||
--lcars-dodger-soft: #3366FF;
|
||||
--lcars-near-blue: #0011EE;
|
||||
--lcars-navy-blue: #000088;
|
||||
|
||||
--lcars-husk: #BBAA55;
|
||||
--lcars-rust: #BB4411;
|
||||
--lcars-tamarillo: #882211;
|
||||
|
||||
/* pixel values taken from the used images */
|
||||
--main_shoulder_width: 16.875em;
|
||||
--main_shoulder_height: 4.5em;
|
||||
--main_shoulder_gap: calc(
|
||||
var(--main_shoulder_width) / 10
|
||||
);
|
||||
|
||||
--main_bar_height: 2.625em;
|
||||
--main_bar_terminator_width: 2.5em;
|
||||
|
||||
--main_table_gutter: 0.25em;
|
||||
--main_table_header_height: 2em;
|
||||
--main_table_footer_height: 1.5em;
|
||||
--main_table_caption_height: 1.5em;
|
||||
|
||||
/* main area height */
|
||||
--main_area_height: calc(
|
||||
100vh
|
||||
- var(--main_shoulder_height) * 2
|
||||
- var(--main_table_gutter) * 2
|
||||
);
|
||||
|
||||
--main_widget_shoulder_width: var(--main_shoulder_width);
|
||||
--main_widget_height: var(--main_area_height);
|
||||
--main_widget_bar_height: var(--main_bar_height);
|
||||
}
|
||||
|
||||
@keyframes blinker {
|
||||
33% {
|
||||
opacity: 33%;
|
||||
}
|
||||
}
|
||||
9
bot/modules/webserver/static/lcars/110-fonts.css
Normal file
9
bot/modules/webserver/static/lcars/110-fonts.css
Normal file
@@ -0,0 +1,9 @@
|
||||
@font-face {
|
||||
font-family: 'SWISS 911 Ultra Compressed BT';
|
||||
src: url('fonts/SWISS911UltraCompressedBT.ttf'); /*URL to font*/
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'SWISS 911 Extra Compressed BT';
|
||||
src: url('fonts/SWISS911ExtraCompressedBT.ttf'); /*URL to font*/
|
||||
}
|
||||
127
bot/modules/webserver/static/lcars/200-framework.css
Normal file
127
bot/modules/webserver/static/lcars/200-framework.css
Normal file
@@ -0,0 +1,127 @@
|
||||
@import url("210-scrollbar_hacks.css");
|
||||
@import url("220-screen_adjustments.css");
|
||||
|
||||
html {
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
a, a:visited {
|
||||
color: var(--background);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0 auto;
|
||||
font-family: "SWISS 911 Extra Compressed BT", sans-serif;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
body > header, body > footer {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
body > main {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
/* header and footer are special due to their graphical shoulders */
|
||||
body > header > div, body > header > div:before,
|
||||
body > footer > div, body > footer > div:before {
|
||||
height: var(--main_shoulder_height);
|
||||
}
|
||||
|
||||
body > header {
|
||||
margin-bottom: var(--main_table_gutter);
|
||||
}
|
||||
|
||||
body > footer {
|
||||
margin-top: var(--main_table_gutter);
|
||||
}
|
||||
|
||||
body > header > div {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
margin:
|
||||
0 var(--main_bar_terminator_width)
|
||||
0 var(--main_shoulder_width);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body > header,
|
||||
body > footer {
|
||||
border-top-left-radius: calc(
|
||||
var(--main_bar_height) * 2.66)
|
||||
calc(var(--main_bar_height) * 1.33
|
||||
);
|
||||
background:
|
||||
url(ui/main_shoulder.png) no-repeat top left,
|
||||
url(ui/main_horizontal_bar_end.png) no-repeat top right;
|
||||
}
|
||||
|
||||
body > footer,
|
||||
body > footer > div,
|
||||
body > footer > div:before {
|
||||
transform: scaleY(-1);
|
||||
}
|
||||
|
||||
body > header > div:before,
|
||||
body > footer > div:before {
|
||||
width: 100%;
|
||||
content: "";
|
||||
background:
|
||||
url(ui/main_horizontal_bar.png)
|
||||
repeat-x;
|
||||
}
|
||||
|
||||
body > footer > div {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
margin:
|
||||
0 var(--main_bar_terminator_width)
|
||||
0 var(--main_shoulder_width);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* every element inside main > div is a widget! */
|
||||
body > main > div {
|
||||
color: var(--lcars-atomic-tangerine);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
height: var(--main_area_height);
|
||||
overflow-y: scroll; /* this has to stay for scroll-snapping to work */
|
||||
overflow-x: hidden;
|
||||
scroll-snap-type: y mandatory;
|
||||
}
|
||||
|
||||
body > main > div > .widget {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex: 0 0 auto;
|
||||
height: var(--main_widget_height);
|
||||
scroll-snap-align: start;
|
||||
border-bottom: var(--main_table_gutter) solid var(--background);
|
||||
border-right: calc(var(--main_table_gutter) * 2) solid var(--background);
|
||||
}
|
||||
|
||||
body > main > div .single_screen {
|
||||
width: 100%;
|
||||
height: var(--main_area_height);
|
||||
padding-left: calc(var(--main_widget_shoulder_width));
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
var(--main-bar-color) 0,
|
||||
var(--main-bar-color) calc(var(--main_widget_shoulder_width) - var(--main_shoulder_gap)),
|
||||
var(--background) calc(var(--main_widget_shoulder_width) - var(--main_shoulder_gap)),
|
||||
var(--background) 100%
|
||||
);
|
||||
}
|
||||
|
||||
body > main > div .single_screen a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
15
bot/modules/webserver/static/lcars/210-scrollbar_hacks.css
Normal file
15
bot/modules/webserver/static/lcars/210-scrollbar_hacks.css
Normal file
@@ -0,0 +1,15 @@
|
||||
::-webkit-scrollbar {
|
||||
width: var(--main_bar_terminator_width);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--lcars-tanoi);
|
||||
border-radius: 1.25em;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--lcars-atomic-tangerine);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/* This file contains some nasty style adjustments to accommodate ridiculously small screens.
|
||||
* I don't care how it looks like there as long as it's usable.
|
||||
*/
|
||||
|
||||
@media only screen and (max-width: 960px) {
|
||||
:root {
|
||||
--main_widget_shoulder_width: calc(
|
||||
var(--main_shoulder_width) / 2
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/* this one will remove table-cell restrictions and kinda condense the table-row. This will be a bit untidy in looks,
|
||||
but more data will be available on very small screens */
|
||||
@media only screen and (max-width: 960px) {
|
||||
body > main > div > .widget > main > table.data_table > tbody > tr {
|
||||
display: unset;
|
||||
}
|
||||
}
|
||||
25
bot/modules/webserver/static/lcars/300-header.css
Normal file
25
bot/modules/webserver/static/lcars/300-header.css
Normal file
@@ -0,0 +1,25 @@
|
||||
@import url("310-header_widgets.css");
|
||||
|
||||
body > header > div > hgroup {
|
||||
position: absolute;
|
||||
right: 0; top: 0;
|
||||
}
|
||||
|
||||
body > header > div > hgroup > h1 {
|
||||
font-size: var(--main_bar_height);
|
||||
color: var(--lcars-tanoi);
|
||||
background-color: var(--background);
|
||||
|
||||
font-family: "SWISS 911 Ultra Compressed BT", sans-serif;
|
||||
text-transform: uppercase;
|
||||
padding: 0 0.25em;
|
||||
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
body > header > div #header_widgets {
|
||||
position: absolute;
|
||||
height: calc(var(--main_shoulder_height) - var(--main_bar_height));
|
||||
right: 0; bottom: 0; left: 0;
|
||||
display: flex;
|
||||
}
|
||||
69
bot/modules/webserver/static/lcars/310-header_widgets.css
Normal file
69
bot/modules/webserver/static/lcars/310-header_widgets.css
Normal file
@@ -0,0 +1,69 @@
|
||||
body > header > div > #header_widgets > .widget {
|
||||
padding-right: 0.25em;
|
||||
color: black;
|
||||
padding-top: var(--main_table_gutter)
|
||||
}
|
||||
|
||||
body > header > div > #header_widgets > .widget > div {
|
||||
background-color: var(--lcars-atomic-tangerine);
|
||||
border-radius: 12px; padding: 0 1em;
|
||||
white-space: nowrap;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
body > header > div > #header_widgets > #login_logout_widget {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
body > header > div > #header_widgets > #login_logout_widget a {
|
||||
color: var(--background)
|
||||
}
|
||||
|
||||
body > header > div > #header_widgets > #gameserver_status_widget > div.active {
|
||||
background-color: var(--lcars-tanoi);
|
||||
}
|
||||
|
||||
body > header > div > #header_widgets > #gameserver_status_widget > div.inactive {
|
||||
background-color: var(--lcars-chestnut-rose);
|
||||
}
|
||||
|
||||
body > header > div > #header_widgets > #gameserver_status_widget span > a,
|
||||
body > header > div > #header_widgets > #gameserver_status_widget span > a:visited {
|
||||
color: var(--lcars-blue-bell);
|
||||
}
|
||||
|
||||
body > header > div > #header_widgets > #gameserver_status_widget span > a {
|
||||
display: inline-block;
|
||||
padding: 0 calc(var(--main_table_gutter) / 2);
|
||||
border-left: calc(var(--main_table_gutter) / 2) solid var(--background);
|
||||
border-right: calc(var(--main_table_gutter) / 2) solid var(--background);
|
||||
color: var(--background); /* Edge seems to require this */
|
||||
background-color: var(--lcars-chestnut-rose);
|
||||
}
|
||||
|
||||
body > header > div > #header_widgets > #gameserver_status_widget span > a:hover,
|
||||
body > header > div > #header_widgets > #gameserver_status_widget span > a:visited {
|
||||
color: var(--background);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
body > header > div > #header_widgets > #gameserver_status_widget span:hover > a {
|
||||
background-color: var(--lcars-chestnut-rose);
|
||||
}
|
||||
|
||||
body > header > div > #header_widgets > #gametime_widget {
|
||||
order: -1;
|
||||
}
|
||||
|
||||
body > header > div > #header_widgets > #gametime_widget span.time {
|
||||
padding: 0 var(--main_table_gutter);
|
||||
}
|
||||
|
||||
body > header > div > #header_widgets > #gametime_widget span.day.bloodday,
|
||||
body > header > div > #header_widgets > #gametime_widget span.time.bloodmoon {
|
||||
color: var(--lcars-chestnut-rose);
|
||||
}
|
||||
|
||||
body > header > div > #header_widgets > #gametime_widget span.time.bloodmoon {
|
||||
background-color: var(--background);
|
||||
}
|
||||
12
bot/modules/webserver/static/lcars/400-main.css
Normal file
12
bot/modules/webserver/static/lcars/400-main.css
Normal file
@@ -0,0 +1,12 @@
|
||||
@import url("410-main_widgets.css");
|
||||
|
||||
body > main > div #unauthorized_disclaimer p {
|
||||
font-size: 1.5em;
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
|
||||
body > main > div #unauthorized_disclaimer a,
|
||||
body > main > div #unauthorized_disclaimer a:visited {
|
||||
color: var(--lcars-melrose);
|
||||
text-decoration: None;
|
||||
}
|
||||
466
bot/modules/webserver/static/lcars/410-main_widgets.css
Normal file
466
bot/modules/webserver/static/lcars/410-main_widgets.css
Normal file
@@ -0,0 +1,466 @@
|
||||
@import url("411-main_widgets_webserver_status_widget.css");
|
||||
@import url("412-main_widgets_telnet_log_widget.css");
|
||||
@import url("413-main_widgets_manage_players_widget.css");
|
||||
@import url("414-main_widgets_manage_locations_widget.css");
|
||||
@import url("415-main_widgets_manage_entities_widget.css");
|
||||
|
||||
body > main > div > .widget > main {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
body > main > div > .widget main a {
|
||||
border-radius: 0.5em;
|
||||
font-family: "SWISS 911 Ultra Compressed BT", sans-serif;
|
||||
padding: 0 0.5em;
|
||||
}
|
||||
|
||||
body > main > div > .widget main a,
|
||||
body > main > div > .widget main a:visited {
|
||||
background-color: var(--lcars-melrose);
|
||||
color: black;
|
||||
}
|
||||
|
||||
body > main > div > .widget main a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
body > main > div > .widget main span.active a {
|
||||
background-color: var(--lcars-tanoi);
|
||||
}
|
||||
|
||||
body > main > div > .widget main span.inactive a {
|
||||
background-color: var(--lcars-chestnut-rose);
|
||||
}
|
||||
|
||||
body > main > div > .widget main .select_button a {
|
||||
border-radius: 0;
|
||||
padding: 0 0.75em;
|
||||
}
|
||||
|
||||
body > main > div > .widget > main > table > tbody > tr > td {
|
||||
font-size: 1em;
|
||||
line-height: 1.5em;
|
||||
padding: 0 calc(var(--main_table_gutter) / 2);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
body > main > div > .widget > main > table > tbody > tr > td:last-child {
|
||||
padding-right: var(--main_table_gutter);
|
||||
}
|
||||
|
||||
body > main > div > .widget > main > table > thead tr:last-child {
|
||||
/* this contains the header stuff for the widget-content */
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
body > main > div > .widget > main > table.data_table > tbody > tr > td {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
body > main > div > .widget > main > table > tfoot > tr > td > div > span.active,
|
||||
body > main > div > .widget .pull_out > div > span.active {
|
||||
background-color: var(--lcars-lilac);
|
||||
}
|
||||
|
||||
body > main > div > .widget > main > table > tfoot > tr > td > span.inactive,
|
||||
body > main > div > .widget .pull_out > div > span.inactive {
|
||||
background-color: var(--lcars-hopbush);
|
||||
}
|
||||
|
||||
body > main > div > .widget .pull_out > div > span.info {
|
||||
background-color: var(--lcars-tanoi);
|
||||
}
|
||||
|
||||
body > main > div > .widget .pull_out > div > span.info > div > span {
|
||||
margin-left: 0.5em;
|
||||
flex: 1;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
body > main > div > .widget .pull_out span a,
|
||||
body > main > div > .widget .pull_out span a:visited,
|
||||
body > main > div > .widget .pull_out span > div {
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
margin-right: 0.5em;
|
||||
color: var(--background);
|
||||
}
|
||||
|
||||
body > main > div > .widget > main .dialog {
|
||||
position: absolute;
|
||||
top: var(--main_table_gutter);
|
||||
bottom: 5.5em;
|
||||
display: none;
|
||||
}
|
||||
|
||||
body > main > div > .widget > main .dialog.open {
|
||||
display: block;
|
||||
}
|
||||
|
||||
body > main > div > .widget > main .dialog .modal-content {
|
||||
padding: 1em;
|
||||
color: var(--lcars-blue-bell);
|
||||
background-color: var(--background);
|
||||
border: var(--main_table_gutter) solid var(--lcars-chestnut-rose);
|
||||
|
||||
border-radius: 0 1em 0.5em 1.5em / 0 1em 0.5em 1em;
|
||||
height: calc(100% + 1em);
|
||||
}
|
||||
|
||||
body > main > div > .widget > main .dialog .modal-content a,
|
||||
body > main > div > .widget > main .dialog .modal-content p {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
body > main > div > .widget > main .dialog.open .modal-content .delete_modal {
|
||||
color: var(--lcars-tanoi);
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
body > main > div > .widget > main .dialog.open .modal-content .delete_modal header,
|
||||
body > main > div > .widget > main .dialog.open .modal-content .delete_modal div {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
body > main > div > .widget > main .dialog.open .modal-content .delete_modal header {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
body > main > div > .widget > main .dialog.open .modal-content .delete_modal div {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
body > main > div > .widget > main .dialog.open .modal-content .delete_modal div.dynamic_content_size {
|
||||
overflow: auto;
|
||||
margin: calc(var(--main_bar_height) / 2) 0;
|
||||
}
|
||||
|
||||
body > main > div > .widget > main .dialog.open .modal-content .delete_modal div.dynamic_content_size::-webkit-scrollbar {
|
||||
width: calc(var(--main_bar_terminator_width) / 1.66);
|
||||
}
|
||||
|
||||
body > main > div > .widget > main .dialog.open .modal-content .delete_modal div:last-child {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
body > main > div > .widget > main .dialog.open .modal-content .delete_modal div section {
|
||||
flex: 1 1 auto;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
body > main > div > .widget > main > table > tbody {
|
||||
display: block;
|
||||
overflow: auto;
|
||||
height: calc(
|
||||
var(--main_widget_height)
|
||||
- var(--main_widget_bar_height)
|
||||
- var(--main_table_caption_height)
|
||||
);
|
||||
padding-right: 0.5em;
|
||||
}
|
||||
|
||||
body > main > div > .widget > main > table.data_table > tbody {
|
||||
height: calc(
|
||||
var(--main_widget_height)
|
||||
- var(--main_widget_bar_height)
|
||||
- var(--main_table_header_height)
|
||||
- var(--main_table_footer_height)
|
||||
- var(--main_table_caption_height)
|
||||
);
|
||||
|
||||
max-width: calc(
|
||||
100vw
|
||||
- var(--main_widget_shoulder_width)
|
||||
- var(--main_bar_terminator_width)
|
||||
- 1em
|
||||
);
|
||||
}
|
||||
|
||||
body > main > div > .widget > main > table.data_table > tbody tr {
|
||||
max-width: calc(
|
||||
100vw
|
||||
- var(--main_widget_shoulder_width)
|
||||
- var(--main_bar_terminator_width)
|
||||
- 1em
|
||||
- var(--main_bar_terminator_width)
|
||||
);
|
||||
}
|
||||
|
||||
body > main > div > .widget > main > table > thead > tr {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body > main > div > .widget > main > table.data_table > thead > tr {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
height: var(--main_table_header_height);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
body > main > div > .widget > main > table.data_table > thead > tr > th {
|
||||
display: inline-block;
|
||||
margin: var(--main_table_gutter);
|
||||
height: calc(
|
||||
var(--main_table_header_height)
|
||||
- (var(--main_table_gutter) * 2)
|
||||
);
|
||||
line-height: calc(
|
||||
var(--main_table_header_height)
|
||||
- (var(--main_table_gutter) * 2)
|
||||
);
|
||||
}
|
||||
|
||||
body > main > div > .widget > main > table.data_table > thead > tr > th:last-child {
|
||||
margin-right: var(--main_bar_terminator_width);
|
||||
}
|
||||
|
||||
body > main > div > .widget > main table.box_input,
|
||||
body > main > div > .widget > main table.box_select {
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
|
||||
body > main > div > .widget > main table.box_select tfoot td,
|
||||
body > main > div > .widget > main table.box_input tfoot td {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
body > main > div > .widget > main table.box_select input[disabled],
|
||||
body > main > div > .widget > main table.box_input input[disabled] {
|
||||
background-color: var(--lcars-chestnut-rose);
|
||||
}
|
||||
|
||||
body > main > div > .widget > main table.box_select tfoot tr td,
|
||||
body > main > div > .widget > main table.box_input tfoot tr td {
|
||||
color: var(--background);
|
||||
padding: 0.5em 0;
|
||||
}
|
||||
|
||||
body > main > div > .widget > main table.box_select tfoot tr td > div,
|
||||
body > main > div > .widget > main table.box_input tfoot tr td > div {
|
||||
/* background-image: linear-gradient(to right, var(--background), var(--lcars-melrose), var(--lcars-melrose), var(--lcars-melrose)); */
|
||||
background-color: var(--lcars-melrose);
|
||||
text-align: right;
|
||||
line-height: 1em;
|
||||
font-size: 0.9em;
|
||||
padding: 0.8em;
|
||||
/* display: none; */
|
||||
}
|
||||
|
||||
body > main > div > .widget > main table.box_select td {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
/* checkbox styling */
|
||||
body > main > div > .widget > main table.box_select label.slider {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
body > main > div > .widget > main table.box_select label.slider [type="checkbox"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body > main > div > .widget > main table.box_select .slider [type="checkbox"] + span {
|
||||
color: var(--background);
|
||||
display: block;
|
||||
background: var(--lcars-chestnut-rose);
|
||||
padding: var(--main_table_gutter);
|
||||
margin: var(--main_table_gutter);
|
||||
border-radius: 0.5em;
|
||||
}
|
||||
|
||||
body > main > div > .widget > main table.box_select .slider:hover [type="checkbox"] + span,
|
||||
body > main > div > .widget > main table.box_select .slider :checked + span {
|
||||
background: var(--lcars-tanoi);
|
||||
}
|
||||
|
||||
body > main > div > .widget > main table.box_select .slider [type="checkbox"][disabled] + span {
|
||||
background: var(--lcars-orange-peel);
|
||||
}
|
||||
|
||||
body > main > div > .widget > main > table > thead {
|
||||
max-height: var(--main_table_header_height);
|
||||
}
|
||||
|
||||
body > main > div > .widget > main > table > tfoot {
|
||||
max-height: var(--main_table_footer_height);
|
||||
}
|
||||
|
||||
body > main > div > .widget > main > table > caption {
|
||||
/* display: none; */
|
||||
max-height: var(--main_table_caption_height);
|
||||
border-top: 0.5em solid var(--background);
|
||||
margin-right: calc(
|
||||
var(--main_bar_terminator_width) / 2
|
||||
);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body > main > div > .widget > header {
|
||||
width: 100%;
|
||||
background-color: var(--lcars-hopbush);
|
||||
border-radius: 1.5em 1.5em 1.5em 0;
|
||||
height: var(--main_bar_height);
|
||||
}
|
||||
|
||||
body > main > div > .widget > header > div > span {
|
||||
font-size: var(--main_widget_bar_height);
|
||||
line-height: 1em;
|
||||
color: var(--background);
|
||||
margin-left: 1.5em;
|
||||
}
|
||||
|
||||
body > main > div > .widget > aside {
|
||||
flex: 0 0 var(--main_widget_shoulder_width);
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
var(--main-bar-color) 0,
|
||||
var(--main-bar-color) calc(var(--main_widget_shoulder_width) - var(--main_shoulder_gap)),
|
||||
var(--background) calc(var(--main_widget_shoulder_width) - var(--main_shoulder_gap)),
|
||||
var(--background) 100%
|
||||
);
|
||||
height: calc(
|
||||
var(--main_widget_height) - var(--main_widget_bar_height)
|
||||
);
|
||||
}
|
||||
|
||||
body > main > div > .widget > main {
|
||||
flex: 1 0 calc(
|
||||
100%
|
||||
- var(--main_widget_shoulder_width)
|
||||
);
|
||||
overflow-y: auto;
|
||||
height: calc(
|
||||
var(--main_widget_height) - var(--main_widget_bar_height)
|
||||
);
|
||||
}
|
||||
|
||||
body > main > div > .widget > aside > div {
|
||||
width: var(--main_widget_shoulder_width);
|
||||
}
|
||||
|
||||
body > main > div > .widget > aside > div > div {
|
||||
border-top: var(--main_table_gutter) solid var(--background);
|
||||
background-color: var(--background);
|
||||
}
|
||||
|
||||
body > main > div > .widget > aside > div > div:last-child {
|
||||
border-bottom: var(--main_table_gutter) solid var(--background);
|
||||
}
|
||||
|
||||
body > main > div > .widget > aside > div > div > span {
|
||||
display: block;
|
||||
background-color: var(--lcars-hopbush);
|
||||
margin-right: var(--main_shoulder_gap);
|
||||
line-height: var(--main_shoulder_height);
|
||||
}
|
||||
|
||||
body > main > div > .widget > aside > div > div > span {
|
||||
border-radius: var(--main_table_gutter);
|
||||
}
|
||||
|
||||
body > main > div > .widget > aside > div > div > span.info {
|
||||
border-radius: unset;
|
||||
line-height: calc(var(--main_shoulder_height) / 2);
|
||||
}
|
||||
|
||||
body > main > div > .widget > main > table > tfoot > tr {
|
||||
height: var(--main_table_footer_height);
|
||||
line-height: var(--main_table_footer_height);
|
||||
}
|
||||
|
||||
body > main > div > .widget > main > table > tfoot > tr > td {
|
||||
height: calc(
|
||||
var(--main_table_footer_height)
|
||||
- var(--main_table_gutter)
|
||||
);
|
||||
line-height: calc(
|
||||
var(--main_table_footer_height)
|
||||
- var(--main_table_gutter)
|
||||
);
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
body > main > div > .widget > main > table > tfoot > tr > td > div {
|
||||
background-color: var(--lcars-tanoi);
|
||||
padding-left: calc(var(--main_shoulder_width) / 2);
|
||||
border-radius: 0 1em 1em 2em / 0 1em 1em 2em;
|
||||
}
|
||||
|
||||
body > main > div > .widget main > table > tfoot > tr > td > div .delete_button {
|
||||
display: inline-block;
|
||||
border-left: 0.25em solid var(--background);
|
||||
border-right: 0.25em solid var(--background);
|
||||
}
|
||||
|
||||
body > main > div > .widget main > table > tfoot > tr > td > div .delete_button span a,
|
||||
body > main > div > .widget main > table > tfoot > tr > td > div .delete_button span {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
body > main > div > .widget main > table > tfoot > tr > td > div .delete_button span.active a,
|
||||
body > main > div > .widget main > table > tfoot > tr > td > div .delete_button span.active {
|
||||
background-color: var(--lcars-chestnut-rose);
|
||||
}
|
||||
|
||||
body > main > div > .widget main > table > tfoot > tr > td > div .delete_button span.inactive a,
|
||||
body > main > div > .widget main > table > tfoot > tr > td > div .delete_button span.inactive {
|
||||
background-color: var(--lcars-tanoi);
|
||||
}
|
||||
|
||||
body > main > div > .widget > main tr td[id$="_actions"] > span {
|
||||
padding-right: var(--main_table_gutter);
|
||||
}
|
||||
|
||||
body > main > div > .widget > main tr td[id$="_actions"] > span:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
/* making the standard order the middle instead of 0 */
|
||||
body > main > div > .widget {
|
||||
order: 500;
|
||||
}
|
||||
|
||||
body > main > div > #manage_players_widget {
|
||||
order: -4;
|
||||
flex: 1 0 calc(
|
||||
960px
|
||||
- var(--main_table_gutter) * 2
|
||||
- var(--main_bar_terminator_width)
|
||||
);
|
||||
}
|
||||
|
||||
body > main > div > #webserver_status_widget {
|
||||
order: -3;
|
||||
flex: 1 0 calc(
|
||||
480px
|
||||
- var(--main_table_gutter) * 2
|
||||
);
|
||||
}
|
||||
|
||||
body > main > div > #manage_locations_widget {
|
||||
order: -2;
|
||||
flex: 1 0 calc(
|
||||
860px
|
||||
- var(--main_table_gutter) * 2
|
||||
);
|
||||
}
|
||||
|
||||
body > main > div > #manage_entities_widget {
|
||||
order: -1;
|
||||
flex: 1 0 calc(
|
||||
768px
|
||||
- var(--main_table_gutter) * 2
|
||||
);
|
||||
}
|
||||
|
||||
body > main > div > #telnet_log_widget {
|
||||
order: 999;
|
||||
flex: 1 0 calc(
|
||||
768px
|
||||
- var(--main_table_gutter) * 2
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
body > main > div > #telnet_log_widget {
|
||||
margin-bottom: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
body > main > div > #telnet_log_widget .log_line {
|
||||
/* log entries seem to be traditionally blue in LCARS ^^ */
|
||||
color: var(--lcars-blue-bell);
|
||||
}
|
||||
|
||||
body > main > div > #telnet_log_widget .log_line td {
|
||||
white-space: normal;
|
||||
padding-left: calc(var(--main_shoulder_width) / 3);
|
||||
text-indent: calc(-1 * calc(var(--main_shoulder_width) / 3));
|
||||
}
|
||||
|
||||
body > main > div > #telnet_log_widget tr.game_chat,
|
||||
body > main > div > #telnet_log_widget caption span.game_chat {
|
||||
color: var(--lcars-hopbush);
|
||||
}
|
||||
|
||||
body > main > div > #telnet_log_widget tr.player_logged,
|
||||
body > main > div > #telnet_log_widget caption span.player_logged {
|
||||
color: var(--lcars-anakiwa);
|
||||
}
|
||||
|
||||
body > main > div > #telnet_log_widget tr.bot_command,
|
||||
body > main > div > #telnet_log_widget caption span.bot_command {
|
||||
color: var(--lcars-cosmic);
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
body > main > div > #manage_players_widget > main > table > tbody > tr > td[id$='_name'] {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
body > main > div > #manage_players_widget tbody#player_table > tr:hover * {
|
||||
background-color: var(--lcars-tanoi);
|
||||
color: var(--background)
|
||||
}
|
||||
|
||||
/* player status */
|
||||
/* offline */
|
||||
body > main > div > #manage_players_widget caption span:not(.is_online):not(.is_initialized),
|
||||
body > main > div > #manage_players_widget tbody > span:not(.is_online):not(.is_initialized),
|
||||
body > main > div > #manage_players_widget tbody > tr:not(.is_online):not(.is_initialized) {
|
||||
color: var(--lcars-chestnut-rose);
|
||||
}
|
||||
|
||||
/* offline and dead */
|
||||
body > main > div > #manage_players_widget caption span.in_limbo:not(.is_online):not(.is_initialized):not(.has_health),
|
||||
body > main > div > #manage_players_widget tbody > span.in_limbo:not(.is_online):not(.is_initialized):not(.has_health),
|
||||
body > main > div > #manage_players_widget tbody > tr.in_limbo:not(.is_online):not(.is_initialized):not(.has_health) {
|
||||
color: var(--lcars-chestnut-rose);
|
||||
}
|
||||
/* special fading animation for offline players currently dead */
|
||||
body > main > div > #manage_players_widget caption span.in_limbo:not(.is_online):not(.is_initialized):not(.has_health),
|
||||
body > main > div > #manage_players_widget tbody > span.in_limbo:not(.is_online):not(.is_initialized):not(.has_health) td:nth-child(n+3),
|
||||
body > main > div > #manage_players_widget tbody > tr.in_limbo:not(.is_online):not(.is_initialized):not(.has_health) td:nth-child(n+3) {
|
||||
animation: blinker 4s linear infinite;
|
||||
}
|
||||
|
||||
/* online and logging in */
|
||||
body > main > div > #manage_players_widget caption span.is_online:not(.is_initialized).in_limbo,
|
||||
body > main > div > #manage_players_widget tbody > span.is_online:not(.is_initialized).in_limbo,
|
||||
body > main > div > #manage_players_widget tbody > tr.is_online:not(.is_initialized).in_limbo {
|
||||
color: var(--lcars-tanoi);
|
||||
}
|
||||
/* special fading animation for players currently logging in */
|
||||
body > main > div > #manage_players_widget caption span.is_online:not(.is_initialized).in_limbo,
|
||||
body > main > div > #manage_players_widget tbody > span.is_online:not(.is_initialized).in_limbo td:nth-child(n+3),
|
||||
body > main > div > #manage_players_widget tbody > tr.is_online:not(.is_initialized).in_limbo td:nth-child(n+3) {
|
||||
animation: blinker 3s linear infinite;
|
||||
}
|
||||
|
||||
/* online */
|
||||
body > main > div > #manage_players_widget caption span.is_online.is_initialized:not(.in_limbo),
|
||||
body > main > div > #manage_players_widget tbody > span.is_online.is_initialized:not(.in_limbo),
|
||||
body > main > div > #manage_players_widget tbody > tr.is_online.is_initialized:not(.in_limbo) {
|
||||
color: var(--lcars-tanoi);
|
||||
}
|
||||
/* online and dead */
|
||||
body > main > div > #manage_players_widget caption span.is_online.is_initialized.in_limbo,
|
||||
body > main > div > #manage_players_widget tbody > span.is_online.is_initialized.in_limbo,
|
||||
body > main > div > #manage_players_widget tbody > tr.is_online.is_initialized.in_limbo {
|
||||
color: var(--lcars-atomic-tangerine);
|
||||
}
|
||||
|
||||
body > main > div > #manage_players_widget td[id^="player_table_row_"][id$="_id"],
|
||||
body > main > div > #manage_players_widget tr[id^="player_table_row_"] td[class="position"],
|
||||
body > main > div > #manage_players_widget td[id^="player_table_row_"][id$="_ping"],
|
||||
body > main > div > #manage_players_widget td[id^="player_table_row_"][id$="_last_updated_servertime"],
|
||||
body > main > div > #manage_players_widget td[id^="player_table_row_"][id$="_last_seen_gametime"] {
|
||||
font-size: 0.90em;
|
||||
}
|
||||
|
||||
body > main > div > #manage_players_widget td[id^="player_table_row_"][id$="_name"] {
|
||||
max-width: 10em;
|
||||
}
|
||||
|
||||
body > main > div > #manage_players_widget td[id^="player_table_row_"][id$="_health"]:before {
|
||||
content: "\2665";
|
||||
padding-right: var(--main_table_gutter);
|
||||
padding-left: calc(var(--main_table_gutter) * 2);
|
||||
color: var(--lcars-chestnut-rose);
|
||||
}
|
||||
|
||||
body > main > div > #manage_players_widget td[id^="player_table_row_"][id$="_pos"] span {
|
||||
width: 1.5em;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
body > main > div > #manage_players_widget td[id^="player_table_row_"][id$="_pos"]:before {
|
||||
padding-left: calc(var(--main_table_gutter) * 2);
|
||||
color: var(--lcars-chestnut-rose);
|
||||
content: "\2691";
|
||||
}
|
||||
|
||||
body > main > div > #manage_players_widget td[id^="player_table_row_"][id$="_zombies"]:before {
|
||||
color: var(--lcars-chestnut-rose);
|
||||
content: "\2620";
|
||||
padding-right: var(--main_table_gutter);
|
||||
padding-left: calc(var(--main_table_gutter) * 2);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
body > main > div > #manage_locations_widget > main > table > tbody > tr > td[id$='_name'] {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
body > main > div > #manage_locations_widget tbody#location_table > tr:hover * {
|
||||
background-color: var(--lcars-tanoi);
|
||||
color: var(--background)
|
||||
}
|
||||
|
||||
body > main > div > #manage_locations_widget #current_player_pos > span > div {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
body > main > div > #manage_locations_widget #current_player_pos > span > div > div {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
body > main > div > #manage_locations_widget #current_player_pos > span > div > div:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
body > main > div > #manage_locations_widget #current_player_pos > span > div > label {
|
||||
flex: 0;
|
||||
white-space: nowrap;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
body > main > div > #manage_locations_widget #current_player_pos > span > div > label:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
body > main > div > #manage_locations_widget #current_player_pos > span > div > label input {
|
||||
border: 0;
|
||||
display: inline-block;
|
||||
margin: 0; padding: 0;
|
||||
width: 2.5em;
|
||||
text-align: right;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
body > main > div > main > div > #manage_locations_widget #current_player_pos > span > div > label:nth-child(2) input {
|
||||
width: 2em;
|
||||
}
|
||||
|
||||
body > main > div > main > div > #manage_locations_widget td[id^="location_table_row_"][id$="_id"],
|
||||
body > main > div > main > div > #manage_locations_widget td[id^="location_table_row_"][id$="_last_changed"],
|
||||
body > main > div > main > div > #manage_locations_widget td[id^="location_table_row_"][id$="_coordinates"],
|
||||
body > main > div > main > div > #manage_locations_widget td[id^="location_table_row_"][id$="_owner"] {
|
||||
font-size: 0.90em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
body > main > div > #manage_entities_widget > main > table > tbody > tr > td[id$='_name'] {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
body > main > div > #manage_entities_widget tbody#entity_table > tr:hover * {
|
||||
background-color: var(--lcars-tanoi);
|
||||
color: var(--background)
|
||||
}
|
||||
6
bot/modules/webserver/static/lcars/500-footer.css
Normal file
6
bot/modules/webserver/static/lcars/500-footer.css
Normal file
@@ -0,0 +1,6 @@
|
||||
@import url("310-header_widgets.css");
|
||||
|
||||
footer > div > p {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
BIN
bot/modules/webserver/static/lcars/audio/alarm01.mp3
Normal file
BIN
bot/modules/webserver/static/lcars/audio/alarm01.mp3
Normal file
Binary file not shown.
BIN
bot/modules/webserver/static/lcars/audio/alarm03.mp3
Normal file
BIN
bot/modules/webserver/static/lcars/audio/alarm03.mp3
Normal file
Binary file not shown.
BIN
bot/modules/webserver/static/lcars/audio/alert12.mp3
Normal file
BIN
bot/modules/webserver/static/lcars/audio/alert12.mp3
Normal file
Binary file not shown.
BIN
bot/modules/webserver/static/lcars/audio/computer_error.mp3
Normal file
BIN
bot/modules/webserver/static/lcars/audio/computer_error.mp3
Normal file
Binary file not shown.
BIN
bot/modules/webserver/static/lcars/audio/computer_work_beep.mp3
Normal file
BIN
bot/modules/webserver/static/lcars/audio/computer_work_beep.mp3
Normal file
Binary file not shown.
BIN
bot/modules/webserver/static/lcars/audio/computerbeep_11.mp3
Normal file
BIN
bot/modules/webserver/static/lcars/audio/computerbeep_11.mp3
Normal file
Binary file not shown.
BIN
bot/modules/webserver/static/lcars/audio/computerbeep_38.mp3
Normal file
BIN
bot/modules/webserver/static/lcars/audio/computerbeep_38.mp3
Normal file
Binary file not shown.
BIN
bot/modules/webserver/static/lcars/audio/computerbeep_65.mp3
Normal file
BIN
bot/modules/webserver/static/lcars/audio/computerbeep_65.mp3
Normal file
Binary file not shown.
BIN
bot/modules/webserver/static/lcars/audio/input_ok_2_clean.mp3
Normal file
BIN
bot/modules/webserver/static/lcars/audio/input_ok_2_clean.mp3
Normal file
Binary file not shown.
BIN
bot/modules/webserver/static/lcars/audio/keyok1.mp3
Normal file
BIN
bot/modules/webserver/static/lcars/audio/keyok1.mp3
Normal file
Binary file not shown.
BIN
bot/modules/webserver/static/lcars/audio/processing.mp3
Normal file
BIN
bot/modules/webserver/static/lcars/audio/processing.mp3
Normal file
Binary file not shown.
BIN
bot/modules/webserver/static/lcars/audio/scrscroll3.mp3
Normal file
BIN
bot/modules/webserver/static/lcars/audio/scrscroll3.mp3
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
bot/modules/webserver/static/lcars/ui/main_horizontal_bar.png
Normal file
BIN
bot/modules/webserver/static/lcars/ui/main_horizontal_bar.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 178 B |
Binary file not shown.
|
After Width: | Height: | Size: 762 B |
BIN
bot/modules/webserver/static/lcars/ui/main_shoulder.png
Normal file
BIN
bot/modules/webserver/static/lcars/ui/main_shoulder.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
661
bot/modules/webserver/static/leaflet.css
Normal file
661
bot/modules/webserver/static/leaflet.css
Normal file
@@ -0,0 +1,661 @@
|
||||
/* required styles */
|
||||
|
||||
.leaflet-pane,
|
||||
.leaflet-tile,
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow,
|
||||
.leaflet-tile-container,
|
||||
.leaflet-pane > svg,
|
||||
.leaflet-pane > canvas,
|
||||
.leaflet-zoom-box,
|
||||
.leaflet-image-layer,
|
||||
.leaflet-layer {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
.leaflet-container {
|
||||
overflow: hidden;
|
||||
}
|
||||
.leaflet-tile,
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
}
|
||||
/* Prevents IE11 from highlighting tiles in blue */
|
||||
.leaflet-tile::selection {
|
||||
background: transparent;
|
||||
}
|
||||
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
|
||||
.leaflet-safari .leaflet-tile {
|
||||
image-rendering: -webkit-optimize-contrast;
|
||||
}
|
||||
/* hack that prevents hw layers "stretching" when loading new tiles */
|
||||
.leaflet-safari .leaflet-tile-container {
|
||||
width: 1600px;
|
||||
height: 1600px;
|
||||
-webkit-transform-origin: 0 0;
|
||||
}
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow {
|
||||
display: block;
|
||||
}
|
||||
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
|
||||
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
|
||||
.leaflet-container .leaflet-overlay-pane svg {
|
||||
max-width: none !important;
|
||||
max-height: none !important;
|
||||
}
|
||||
.leaflet-container .leaflet-marker-pane img,
|
||||
.leaflet-container .leaflet-shadow-pane img,
|
||||
.leaflet-container .leaflet-tile-pane img,
|
||||
.leaflet-container img.leaflet-image-layer,
|
||||
.leaflet-container .leaflet-tile {
|
||||
max-width: none !important;
|
||||
max-height: none !important;
|
||||
width: auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.leaflet-container img.leaflet-tile {
|
||||
/* See: https://bugs.chromium.org/p/chromium/issues/detail?id=600120 */
|
||||
mix-blend-mode: plus-lighter;
|
||||
}
|
||||
|
||||
.leaflet-container.leaflet-touch-zoom {
|
||||
-ms-touch-action: pan-x pan-y;
|
||||
touch-action: pan-x pan-y;
|
||||
}
|
||||
.leaflet-container.leaflet-touch-drag {
|
||||
-ms-touch-action: pinch-zoom;
|
||||
/* Fallback for FF which doesn't support pinch-zoom */
|
||||
touch-action: none;
|
||||
touch-action: pinch-zoom;
|
||||
}
|
||||
.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
|
||||
-ms-touch-action: none;
|
||||
touch-action: none;
|
||||
}
|
||||
.leaflet-container {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
.leaflet-container a {
|
||||
-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
|
||||
}
|
||||
.leaflet-tile {
|
||||
filter: inherit;
|
||||
visibility: hidden;
|
||||
}
|
||||
.leaflet-tile-loaded {
|
||||
visibility: inherit;
|
||||
}
|
||||
.leaflet-zoom-box {
|
||||
width: 0;
|
||||
height: 0;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
z-index: 800;
|
||||
}
|
||||
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
|
||||
.leaflet-overlay-pane svg {
|
||||
-moz-user-select: none;
|
||||
}
|
||||
|
||||
.leaflet-pane { z-index: 400; }
|
||||
|
||||
.leaflet-tile-pane { z-index: 200; }
|
||||
.leaflet-overlay-pane { z-index: 400; }
|
||||
.leaflet-shadow-pane { z-index: 500; }
|
||||
.leaflet-marker-pane { z-index: 600; }
|
||||
.leaflet-tooltip-pane { z-index: 650; }
|
||||
.leaflet-popup-pane { z-index: 700; }
|
||||
|
||||
.leaflet-map-pane canvas { z-index: 100; }
|
||||
.leaflet-map-pane svg { z-index: 200; }
|
||||
|
||||
.leaflet-vml-shape {
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
}
|
||||
.lvml {
|
||||
behavior: url(#default#VML);
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
|
||||
/* control positioning */
|
||||
|
||||
.leaflet-control {
|
||||
position: relative;
|
||||
z-index: 800;
|
||||
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
|
||||
pointer-events: auto;
|
||||
}
|
||||
.leaflet-top,
|
||||
.leaflet-bottom {
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
}
|
||||
.leaflet-top {
|
||||
top: 0;
|
||||
}
|
||||
.leaflet-right {
|
||||
right: 0;
|
||||
}
|
||||
.leaflet-bottom {
|
||||
bottom: 0;
|
||||
}
|
||||
.leaflet-left {
|
||||
left: 0;
|
||||
}
|
||||
.leaflet-control {
|
||||
float: left;
|
||||
clear: both;
|
||||
}
|
||||
.leaflet-right .leaflet-control {
|
||||
float: right;
|
||||
}
|
||||
.leaflet-top .leaflet-control {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.leaflet-bottom .leaflet-control {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.leaflet-left .leaflet-control {
|
||||
margin-left: 10px;
|
||||
}
|
||||
.leaflet-right .leaflet-control {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
|
||||
/* zoom and fade animations */
|
||||
|
||||
.leaflet-fade-anim .leaflet-popup {
|
||||
opacity: 0;
|
||||
-webkit-transition: opacity 0.2s linear;
|
||||
-moz-transition: opacity 0.2s linear;
|
||||
transition: opacity 0.2s linear;
|
||||
}
|
||||
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
|
||||
opacity: 1;
|
||||
}
|
||||
.leaflet-zoom-animated {
|
||||
-webkit-transform-origin: 0 0;
|
||||
-ms-transform-origin: 0 0;
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
svg.leaflet-zoom-animated {
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.leaflet-zoom-anim .leaflet-zoom-animated {
|
||||
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||
}
|
||||
.leaflet-zoom-anim .leaflet-tile,
|
||||
.leaflet-pan-anim .leaflet-tile {
|
||||
-webkit-transition: none;
|
||||
-moz-transition: none;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.leaflet-zoom-anim .leaflet-zoom-hide {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
|
||||
/* cursors */
|
||||
|
||||
.leaflet-interactive {
|
||||
cursor: pointer;
|
||||
}
|
||||
.leaflet-grab {
|
||||
cursor: -webkit-grab;
|
||||
cursor: -moz-grab;
|
||||
cursor: grab;
|
||||
}
|
||||
.leaflet-crosshair,
|
||||
.leaflet-crosshair .leaflet-interactive {
|
||||
cursor: crosshair;
|
||||
}
|
||||
.leaflet-popup-pane,
|
||||
.leaflet-control {
|
||||
cursor: auto;
|
||||
}
|
||||
.leaflet-dragging .leaflet-grab,
|
||||
.leaflet-dragging .leaflet-grab .leaflet-interactive,
|
||||
.leaflet-dragging .leaflet-marker-draggable {
|
||||
cursor: move;
|
||||
cursor: -webkit-grabbing;
|
||||
cursor: -moz-grabbing;
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
/* marker & overlays interactivity */
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow,
|
||||
.leaflet-image-layer,
|
||||
.leaflet-pane > svg path,
|
||||
.leaflet-tile-container {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.leaflet-marker-icon.leaflet-interactive,
|
||||
.leaflet-image-layer.leaflet-interactive,
|
||||
.leaflet-pane > svg path.leaflet-interactive,
|
||||
svg.leaflet-image-layer.leaflet-interactive path {
|
||||
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* visual tweaks */
|
||||
|
||||
.leaflet-container {
|
||||
background: #ddd;
|
||||
outline-offset: 1px;
|
||||
}
|
||||
.leaflet-container a {
|
||||
color: #0078A8;
|
||||
}
|
||||
.leaflet-zoom-box {
|
||||
border: 2px dotted #38f;
|
||||
background: rgba(255,255,255,0.5);
|
||||
}
|
||||
|
||||
|
||||
/* general typography */
|
||||
.leaflet-container {
|
||||
font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
|
||||
font-size: 12px;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
|
||||
/* general toolbar styles */
|
||||
|
||||
.leaflet-bar {
|
||||
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.leaflet-bar a {
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #ccc;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
line-height: 26px;
|
||||
display: block;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
color: black;
|
||||
}
|
||||
.leaflet-bar a,
|
||||
.leaflet-control-layers-toggle {
|
||||
background-position: 50% 50%;
|
||||
background-repeat: no-repeat;
|
||||
display: block;
|
||||
}
|
||||
.leaflet-bar a:hover,
|
||||
.leaflet-bar a:focus {
|
||||
background-color: #f4f4f4;
|
||||
}
|
||||
.leaflet-bar a:first-child {
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
}
|
||||
.leaflet-bar a:last-child {
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
border-bottom: none;
|
||||
}
|
||||
.leaflet-bar a.leaflet-disabled {
|
||||
cursor: default;
|
||||
background-color: #f4f4f4;
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
.leaflet-touch .leaflet-bar a {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
}
|
||||
.leaflet-touch .leaflet-bar a:first-child {
|
||||
border-top-left-radius: 2px;
|
||||
border-top-right-radius: 2px;
|
||||
}
|
||||
.leaflet-touch .leaflet-bar a:last-child {
|
||||
border-bottom-left-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
}
|
||||
|
||||
/* zoom control */
|
||||
|
||||
.leaflet-control-zoom-in,
|
||||
.leaflet-control-zoom-out {
|
||||
font: bold 18px 'Lucida Console', Monaco, monospace;
|
||||
text-indent: 1px;
|
||||
}
|
||||
|
||||
.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
|
||||
/* layers control */
|
||||
|
||||
.leaflet-control-layers {
|
||||
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
|
||||
background: #fff;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.leaflet-control-layers-toggle {
|
||||
background-image: url(images/layers.png);
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
.leaflet-retina .leaflet-control-layers-toggle {
|
||||
background-image: url(images/layers-2x.png);
|
||||
background-size: 26px 26px;
|
||||
}
|
||||
.leaflet-touch .leaflet-control-layers-toggle {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
.leaflet-control-layers .leaflet-control-layers-list,
|
||||
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
|
||||
display: none;
|
||||
}
|
||||
.leaflet-control-layers-expanded .leaflet-control-layers-list {
|
||||
display: block;
|
||||
position: relative;
|
||||
}
|
||||
.leaflet-control-layers-expanded {
|
||||
padding: 6px 10px 6px 6px;
|
||||
color: #333;
|
||||
background: #fff;
|
||||
}
|
||||
.leaflet-control-layers-scrollbar {
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
padding-right: 5px;
|
||||
}
|
||||
.leaflet-control-layers-selector {
|
||||
margin-top: 2px;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
.leaflet-control-layers label {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
font-size: 1.08333em;
|
||||
}
|
||||
.leaflet-control-layers-separator {
|
||||
height: 0;
|
||||
border-top: 1px solid #ddd;
|
||||
margin: 5px -10px 5px -6px;
|
||||
}
|
||||
|
||||
/* Default icon URLs */
|
||||
.leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */
|
||||
background-image: url(images/marker-icon.png);
|
||||
}
|
||||
|
||||
|
||||
/* attribution and scale controls */
|
||||
|
||||
.leaflet-container .leaflet-control-attribution {
|
||||
background: #fff;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
margin: 0;
|
||||
}
|
||||
.leaflet-control-attribution,
|
||||
.leaflet-control-scale-line {
|
||||
padding: 0 5px;
|
||||
color: #333;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.leaflet-control-attribution a {
|
||||
text-decoration: none;
|
||||
}
|
||||
.leaflet-control-attribution a:hover,
|
||||
.leaflet-control-attribution a:focus {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.leaflet-attribution-flag {
|
||||
display: inline !important;
|
||||
vertical-align: baseline !important;
|
||||
width: 1em;
|
||||
height: 0.6669em;
|
||||
}
|
||||
.leaflet-left .leaflet-control-scale {
|
||||
margin-left: 5px;
|
||||
}
|
||||
.leaflet-bottom .leaflet-control-scale {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.leaflet-control-scale-line {
|
||||
border: 2px solid #777;
|
||||
border-top: none;
|
||||
line-height: 1.1;
|
||||
padding: 2px 5px 1px;
|
||||
white-space: nowrap;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
text-shadow: 1px 1px #fff;
|
||||
}
|
||||
.leaflet-control-scale-line:not(:first-child) {
|
||||
border-top: 2px solid #777;
|
||||
border-bottom: none;
|
||||
margin-top: -2px;
|
||||
}
|
||||
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
|
||||
border-bottom: 2px solid #777;
|
||||
}
|
||||
|
||||
.leaflet-touch .leaflet-control-attribution,
|
||||
.leaflet-touch .leaflet-control-layers,
|
||||
.leaflet-touch .leaflet-bar {
|
||||
box-shadow: none;
|
||||
}
|
||||
.leaflet-touch .leaflet-control-layers,
|
||||
.leaflet-touch .leaflet-bar {
|
||||
border: 2px solid rgba(0,0,0,0.2);
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
|
||||
/* popup */
|
||||
|
||||
.leaflet-popup {
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.leaflet-popup-content-wrapper {
|
||||
padding: 1px;
|
||||
text-align: left;
|
||||
border-radius: 12px;
|
||||
}
|
||||
.leaflet-popup-content {
|
||||
margin: 13px 24px 13px 20px;
|
||||
line-height: 1.3;
|
||||
font-size: 13px;
|
||||
font-size: 1.08333em;
|
||||
min-height: 1px;
|
||||
}
|
||||
.leaflet-popup-content p {
|
||||
margin: 17px 0;
|
||||
margin: 1.3em 0;
|
||||
}
|
||||
.leaflet-popup-tip-container {
|
||||
width: 40px;
|
||||
height: 20px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
margin-top: -1px;
|
||||
margin-left: -20px;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
.leaflet-popup-tip {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
padding: 1px;
|
||||
|
||||
margin: -10px auto 0;
|
||||
pointer-events: auto;
|
||||
|
||||
-webkit-transform: rotate(45deg);
|
||||
-moz-transform: rotate(45deg);
|
||||
-ms-transform: rotate(45deg);
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
.leaflet-popup-content-wrapper,
|
||||
.leaflet-popup-tip {
|
||||
background: white;
|
||||
color: #333;
|
||||
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
|
||||
}
|
||||
.leaflet-container a.leaflet-popup-close-button {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
border: none;
|
||||
text-align: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font: 16px/24px Tahoma, Verdana, sans-serif;
|
||||
color: #757575;
|
||||
text-decoration: none;
|
||||
background: transparent;
|
||||
}
|
||||
.leaflet-container a.leaflet-popup-close-button:hover,
|
||||
.leaflet-container a.leaflet-popup-close-button:focus {
|
||||
color: #585858;
|
||||
}
|
||||
.leaflet-popup-scrolled {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.leaflet-oldie .leaflet-popup-content-wrapper {
|
||||
-ms-zoom: 1;
|
||||
}
|
||||
.leaflet-oldie .leaflet-popup-tip {
|
||||
width: 24px;
|
||||
margin: 0 auto;
|
||||
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
|
||||
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
|
||||
}
|
||||
|
||||
.leaflet-oldie .leaflet-control-zoom,
|
||||
.leaflet-oldie .leaflet-control-layers,
|
||||
.leaflet-oldie .leaflet-popup-content-wrapper,
|
||||
.leaflet-oldie .leaflet-popup-tip {
|
||||
border: 1px solid #999;
|
||||
}
|
||||
|
||||
|
||||
/* div icon */
|
||||
|
||||
.leaflet-div-icon {
|
||||
background: #fff;
|
||||
border: 1px solid #666;
|
||||
}
|
||||
|
||||
|
||||
/* Tooltip */
|
||||
/* Base styles for the element that has a tooltip */
|
||||
.leaflet-tooltip {
|
||||
position: absolute;
|
||||
padding: 6px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #fff;
|
||||
border-radius: 3px;
|
||||
color: #222;
|
||||
white-space: nowrap;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
|
||||
}
|
||||
.leaflet-tooltip.leaflet-interactive {
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.leaflet-tooltip-top:before,
|
||||
.leaflet-tooltip-bottom:before,
|
||||
.leaflet-tooltip-left:before,
|
||||
.leaflet-tooltip-right:before {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
border: 6px solid transparent;
|
||||
background: transparent;
|
||||
content: "";
|
||||
}
|
||||
|
||||
/* Directions */
|
||||
|
||||
.leaflet-tooltip-bottom {
|
||||
margin-top: 6px;
|
||||
}
|
||||
.leaflet-tooltip-top {
|
||||
margin-top: -6px;
|
||||
}
|
||||
.leaflet-tooltip-bottom:before,
|
||||
.leaflet-tooltip-top:before {
|
||||
left: 50%;
|
||||
margin-left: -6px;
|
||||
}
|
||||
.leaflet-tooltip-top:before {
|
||||
bottom: 0;
|
||||
margin-bottom: -12px;
|
||||
border-top-color: #fff;
|
||||
}
|
||||
.leaflet-tooltip-bottom:before {
|
||||
top: 0;
|
||||
margin-top: -12px;
|
||||
margin-left: -6px;
|
||||
border-bottom-color: #fff;
|
||||
}
|
||||
.leaflet-tooltip-left {
|
||||
margin-left: -6px;
|
||||
}
|
||||
.leaflet-tooltip-right {
|
||||
margin-left: 6px;
|
||||
}
|
||||
.leaflet-tooltip-left:before,
|
||||
.leaflet-tooltip-right:before {
|
||||
top: 50%;
|
||||
margin-top: -6px;
|
||||
}
|
||||
.leaflet-tooltip-left:before {
|
||||
right: 0;
|
||||
margin-right: -12px;
|
||||
border-left-color: #fff;
|
||||
}
|
||||
.leaflet-tooltip-right:before {
|
||||
left: 0;
|
||||
margin-left: -12px;
|
||||
border-right-color: #fff;
|
||||
}
|
||||
|
||||
/* Printing */
|
||||
|
||||
@media print {
|
||||
/* Prevent printers from removing background-images of controls. */
|
||||
.leaflet-control {
|
||||
-webkit-print-color-adjust: exact;
|
||||
print-color-adjust: exact;
|
||||
}
|
||||
}
|
||||
6
bot/modules/webserver/static/leaflet.js
Normal file
6
bot/modules/webserver/static/leaflet.js
Normal file
File diff suppressed because one or more lines are too long
49
bot/modules/webserver/static/reset.css
Normal file
49
bot/modules/webserver/static/reset.css
Normal file
@@ -0,0 +1,49 @@
|
||||
/* http://meyerweb.com/eric/tools/css/reset/
|
||||
v2.0 | 20110126
|
||||
License: none (public domain)
|
||||
*/
|
||||
|
||||
html, body, div, span, applet, object, iframe,
|
||||
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
|
||||
a, abbr, acronym, address, big, cite, code,
|
||||
del, dfn, em, img, ins, kbd, q, s, samp,
|
||||
small, strike, strong, sub, sup, tt, var,
|
||||
b, u, i, center,
|
||||
dl, dt, dd, ol, ul, li,
|
||||
fieldset, form, label, legend,
|
||||
table, caption, tbody, tfoot, thead, tr, th, td,
|
||||
article, aside, canvas, details, embed,
|
||||
figure, figcaption, footer, header, hgroup,
|
||||
menu, nav, output, ruby, section, summary,
|
||||
time, mark, audio, video {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
font-size: 100%;
|
||||
font: inherit;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
/* HTML5 display-role reset for older browsers */
|
||||
article, aside, details, figcaption, figure,
|
||||
footer, header, hgroup, menu, nav, section {
|
||||
display: block;
|
||||
}
|
||||
body {
|
||||
line-height: 1;
|
||||
}
|
||||
ol, ul {
|
||||
list-style: none;
|
||||
}
|
||||
blockquote, q {
|
||||
quotes: none;
|
||||
}
|
||||
blockquote:before, blockquote:after,
|
||||
q:before, q:after {
|
||||
content: '';
|
||||
content: none;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
width: 100%;
|
||||
}
|
||||
9
bot/modules/webserver/static/socket.io-2.3.0.slim.js
Normal file
9
bot/modules/webserver/static/socket.io-2.3.0.slim.js
Normal file
File diff suppressed because one or more lines are too long
19
bot/modules/webserver/static/style.css
Normal file
19
bot/modules/webserver/static/style.css
Normal file
@@ -0,0 +1,19 @@
|
||||
table {
|
||||
caption-side: bottom;
|
||||
}
|
||||
|
||||
table caption {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
footer {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
528
bot/modules/webserver/static/system.js
Normal file
528
bot/modules/webserver/static/system.js
Normal file
@@ -0,0 +1,528 @@
|
||||
document.addEventListener("DOMContentLoaded", function(event) {
|
||||
|
||||
// based on https://stackoverflow.com/a/56279295/8967590
|
||||
Audio.prototype.play = (function(play) {
|
||||
return function () {
|
||||
let audio = this;
|
||||
let promise = play.apply(audio, arguments);
|
||||
if (promise !== undefined) {
|
||||
promise.catch(_ => {
|
||||
console.log("autoplay of audiofile failed :(");
|
||||
});
|
||||
}
|
||||
};
|
||||
}) (Audio.prototype.play);
|
||||
|
||||
let audio_files = [];
|
||||
|
||||
function load_audio_files() {
|
||||
audio_files["computer_work_beep"] = new Audio('/static/lcars/audio/computer_work_beep.mp3');
|
||||
audio_files["computer_error"] = new Audio('/static/lcars/audio/computer_error.mp3');
|
||||
audio_files["keyok1"] = new Audio('/static/lcars/audio/keyok1.mp3');
|
||||
audio_files["keyok1"].volume = 0.05;
|
||||
audio_files["input_ok_2_clean"] = new Audio('/static/lcars/audio/input_ok_2_clean.mp3');
|
||||
audio_files["processing"] = new Audio('/static/lcars/audio/processing.mp3');
|
||||
audio_files["processing"].volume = 0.25;
|
||||
audio_files["computerbeep_11"] = new Audio('/static/lcars/audio/computerbeep_11.mp3');
|
||||
audio_files["computerbeep_11"].volume = 0.5;
|
||||
audio_files["computerbeep_38"] = new Audio('/static/lcars/audio/computerbeep_38.mp3');
|
||||
audio_files["computerbeep_38"].volume = 0.1;
|
||||
audio_files["computerbeep_65"] = new Audio('/static/lcars/audio/computerbeep_65.mp3');
|
||||
audio_files["alarm01"] = new Audio('/static/lcars/audio/alarm01.mp3');
|
||||
audio_files["alarm03"] = new Audio('/static/lcars/audio/alarm03.mp3');
|
||||
audio_files["alert12"] = new Audio('/static/lcars/audio/alert12.mp3');
|
||||
}
|
||||
|
||||
function play_audio_file(identifier) {
|
||||
try {
|
||||
if (audio_files[identifier].readyState === 4) { // 4 = HAVE_ENOUGH_DATA
|
||||
if (!audio_files[identifier].ended) {
|
||||
audio_files[identifier].currentTime = 0;
|
||||
audio_files[identifier].play();
|
||||
} else {
|
||||
audio_files[identifier].play();
|
||||
}
|
||||
}
|
||||
} catch(err) {
|
||||
console.error("[AUDIO] Failed to play audio file:", identifier, err);
|
||||
}
|
||||
}
|
||||
|
||||
/* found on https://stackoverflow.com/questions/14267781/sorting-html-table-with-javascript/49041392#49041392
|
||||
* slightly modified
|
||||
*/
|
||||
let index; // cell index
|
||||
let toggleBool; // sorting asc, desc
|
||||
window.sorting = function sorting(th, tbody, index) {
|
||||
function compareCells(a, b) {
|
||||
let aVal = a.cells[index].innerText.replace(/,/g, '');
|
||||
let bVal = b.cells[index].innerText.replace(/,/g, '');
|
||||
|
||||
if (toggleBool) {
|
||||
let temp = aVal;
|
||||
aVal = bVal;
|
||||
bVal = temp;
|
||||
}
|
||||
|
||||
if (aVal.match(/^[0-9]+$/) && bVal.match(/^[0-9]+$/)) {
|
||||
return parseFloat(aVal) - parseFloat(bVal);
|
||||
} else {
|
||||
if (aVal < bVal) {
|
||||
return -1;
|
||||
} else if (aVal > bVal) {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.index = index;
|
||||
toggleBool = !toggleBool;
|
||||
|
||||
let datas = [];
|
||||
for (let i = 0; i < tbody.rows.length; i++) {
|
||||
datas[i] = tbody.rows[i];
|
||||
}
|
||||
|
||||
// sort by cell[index]
|
||||
datas.sort(compareCells);
|
||||
for (let i = 0; i < tbody.rows.length; i++) {
|
||||
// rearrange table rows by sorted rows
|
||||
tbody.appendChild(datas[i]);
|
||||
}
|
||||
};
|
||||
|
||||
/* found on https://stackoverflow.com/a/21648508/8967590
|
||||
* slightly modified to only return the rgb value and getting rid of type-warnings
|
||||
*/
|
||||
function hexToRgb(hex){
|
||||
let char;
|
||||
if (/^#([A-Fa-f0-9]{3}){1,2}$/.test(hex)) {
|
||||
char = hex.substring(1).split('');
|
||||
if (char.length === 3) {
|
||||
char = [char[0], char[0], char[1], char[1], char[2], char[2]];
|
||||
}
|
||||
char = '0x' + char.join('');
|
||||
return [(char >> 16) & 255, (char >> 8) & 255, char & 255].join(', ');
|
||||
} else {
|
||||
alert(hex);
|
||||
throw new Error('Bad Hex');
|
||||
}
|
||||
}
|
||||
|
||||
let lcars_colors = [];
|
||||
function load_lcars_colors() {
|
||||
/* https://davidwalsh.name/css-variables-javascript */
|
||||
lcars_colors["lcars-pale-canary"] = hexToRgb(
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--lcars-pale-canary').trim()
|
||||
);
|
||||
lcars_colors["lcars-tanoi"] = hexToRgb(
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--lcars-tanoi').trim()
|
||||
);
|
||||
lcars_colors["lcars-golden-tanoi"] = hexToRgb(
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--lcars-golden-tanoi').trim()
|
||||
);
|
||||
lcars_colors["lcars-neon-carrot"] = hexToRgb(
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--lcars-neon-carrot').trim()
|
||||
);
|
||||
|
||||
lcars_colors["lcars-eggplant"] = hexToRgb(
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--lcars-eggplant').trim()
|
||||
);
|
||||
lcars_colors["lcars-lilac"] = hexToRgb(
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--lcars-lilac').trim()
|
||||
);
|
||||
lcars_colors["lcars-anakiwa"] = hexToRgb(
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--lcars-anakiwa').trim()
|
||||
);
|
||||
lcars_colors["lcars-mariner"] = hexToRgb(
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--lcars-mariner').trim()
|
||||
);
|
||||
|
||||
lcars_colors["lcars-bahama-blue"] = hexToRgb(
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--lcars-bahama-blue').trim()
|
||||
);
|
||||
lcars_colors["lcars-blue-bell"] = hexToRgb(
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--lcars-blue-bell').trim()
|
||||
);
|
||||
lcars_colors["lcars-melrose"] = hexToRgb(
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--lcars-melrose').trim()
|
||||
);
|
||||
lcars_colors["lcars-hopbush"] = hexToRgb(
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--lcars-hopbush').trim()
|
||||
);
|
||||
|
||||
lcars_colors["lcars-chestnut-rose"] = hexToRgb(
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--lcars-chestnut-rose').trim()
|
||||
);
|
||||
lcars_colors["lcars-orange-peel"] = hexToRgb(
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--lcars-orange-peel').trim()
|
||||
);
|
||||
lcars_colors["lcars-atomic-tangerine"] = hexToRgb(
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--lcars-atomic-tangerine').trim()
|
||||
);
|
||||
lcars_colors["lcars-danub"] = hexToRgb(
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--lcars-danub').trim()
|
||||
);
|
||||
|
||||
lcars_colors["lcars-indigo"] = hexToRgb(
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--lcars-indigo').trim()
|
||||
);
|
||||
lcars_colors["lcars-lavender-purple"] = hexToRgb(
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--lcars-lavender-purple').trim()
|
||||
);
|
||||
lcars_colors["lcars-cosmic"] = hexToRgb(
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--lcars-cosmic').trim()
|
||||
);
|
||||
lcars_colors["lcars-red-damask"] = hexToRgb(
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--lcars-red-damask').trim()
|
||||
);
|
||||
|
||||
lcars_colors["lcars-medium-carmine"] = hexToRgb(
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--lcars-medium-carmine').trim()
|
||||
);
|
||||
lcars_colors["lcars-bourbon"] = hexToRgb(
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--lcars-bourbon').trim()
|
||||
);
|
||||
lcars_colors["lcars-sandy-brown"] = hexToRgb(
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--lcars-sandy-brown').trim()
|
||||
);
|
||||
lcars_colors["lcars-periwinkle"] = hexToRgb(
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--lcars-periwinkle').trim()
|
||||
);
|
||||
|
||||
lcars_colors["lcars-dodger-pale"] = hexToRgb(
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--lcars-dodger-pale').trim()
|
||||
);
|
||||
lcars_colors["lcars-dodger-soft"] = hexToRgb(
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--lcars-dodger-soft').trim()
|
||||
);
|
||||
lcars_colors["lcars-near-blue"] = hexToRgb(
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--lcars-near-blue').trim()
|
||||
);
|
||||
lcars_colors["lcars-navy-blue"] = hexToRgb(
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--lcars-navy-blue').trim()
|
||||
);
|
||||
|
||||
lcars_colors["lcars-husk"] = hexToRgb(
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--lcars-husk').trim()
|
||||
);
|
||||
lcars_colors["lcars-rust"] = hexToRgb(
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--lcars-rust').trim()
|
||||
);
|
||||
lcars_colors["lcars-tamarillo"] = hexToRgb(
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--lcars-tamarillo').trim()
|
||||
);
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/38311629/8967590
|
||||
$.fn.setClass = function(classes) {
|
||||
this.attr('class', classes);
|
||||
return this;
|
||||
};
|
||||
|
||||
// https://stackoverflow.com/a/46308265,
|
||||
// slightly modified for better readability
|
||||
$.fn.selectText = function(){
|
||||
let element = this[0], range, selection;
|
||||
if (document.body.createTextRange) {
|
||||
range = document.body.createTextRange();
|
||||
range.moveToElementText(element);
|
||||
range.select();
|
||||
document.execCommand('copy');
|
||||
} else if (window.getSelection) {
|
||||
selection = window.getSelection();
|
||||
range = document.createRange();
|
||||
range.selectNodeContents(element);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
document.execCommand('copy');
|
||||
}
|
||||
};
|
||||
|
||||
$.fn.upsert = function(target_element_id, htmlString) {
|
||||
// upsert - find or create new element
|
||||
let $el = $(this).find(target_element_id);
|
||||
if ($el.length === 0) {
|
||||
// didn't exist, create and add to caller
|
||||
$el = $(htmlString);
|
||||
$(this).prepend($el);
|
||||
}
|
||||
return $el;
|
||||
};
|
||||
|
||||
let flash = function(elements, color=false) {
|
||||
let opacity = 40;
|
||||
if (color === false) {
|
||||
color = lcars_colors["lcars-tanoi"]; // has to be in this format since we use rgba
|
||||
}
|
||||
let interval = setInterval(function() {
|
||||
opacity -= 2.5;
|
||||
if (opacity <= 0) {
|
||||
clearInterval(interval);
|
||||
$(elements).removeAttr('style');
|
||||
} else {
|
||||
$(elements).css({
|
||||
"background-color": "rgba(" + color + ", " + (opacity / 50) + ")"
|
||||
});
|
||||
}
|
||||
}, 20)
|
||||
};
|
||||
|
||||
//connect to the socket server.
|
||||
window.socket = io.connect(
|
||||
'http://' + document.domain + ':' + location.port, {
|
||||
'sync disconnect on unload': true
|
||||
}
|
||||
);
|
||||
|
||||
window.socket.on('connected', function() {
|
||||
window.socket.emit('ding');
|
||||
});
|
||||
|
||||
let start_time = (new Date).getTime();
|
||||
const PING_TIMEOUT_THRESHOLD = 5000; // Only log if ping takes >5 seconds
|
||||
|
||||
window.setInterval(function() {
|
||||
start_time = (new Date).getTime();
|
||||
socket.emit('ding');
|
||||
play_audio_file("processing");
|
||||
// No log for normal ping - would be spam (every 10 seconds)
|
||||
}, 10000);
|
||||
|
||||
window.socket.on('dong', function() {
|
||||
let latency = (new Date).getTime() - start_time;
|
||||
play_audio_file("keyok1");
|
||||
|
||||
// Only log slow pings
|
||||
if (latency > PING_TIMEOUT_THRESHOLD) {
|
||||
console.warn("[PING] Slow response: " + latency + "ms (threshold: " + PING_TIMEOUT_THRESHOLD + "ms)");
|
||||
}
|
||||
});
|
||||
|
||||
// Session conflict handling
|
||||
window.socket.on('session_conflict', function(data) {
|
||||
console.log('[SESSION] Conflict detected:', data);
|
||||
play_audio_file("alert12");
|
||||
|
||||
let message = data.message + '\n\nAktive Sessions: ' + data.existing_sessions;
|
||||
if (confirm(message)) {
|
||||
// User wants to take over
|
||||
console.log('[SESSION] Taking over existing session');
|
||||
window.socket.emit('session_takeover_accept');
|
||||
} else {
|
||||
// User declined
|
||||
console.log('[SESSION] Takeover declined');
|
||||
window.socket.emit('session_takeover_decline');
|
||||
}
|
||||
});
|
||||
|
||||
window.socket.on('session_accepted', function() {
|
||||
console.log('[SESSION] Session accepted');
|
||||
play_audio_file("computerbeep_11");
|
||||
});
|
||||
|
||||
window.socket.on('session_declined', function(data) {
|
||||
console.log('[SESSION] Session declined:', data.message);
|
||||
play_audio_file("computer_error");
|
||||
alert(data.message);
|
||||
// Browser will be disconnected by server
|
||||
});
|
||||
|
||||
window.socket.on('session_taken_over', function(data) {
|
||||
console.log('[SESSION] Session taken over by another browser');
|
||||
play_audio_file("alarm01");
|
||||
alert(data.message);
|
||||
// Connection will be closed by server
|
||||
// Show a visual indicator that session is no longer active
|
||||
document.body.style.opacity = '0.5';
|
||||
document.body.style.pointerEvents = 'none';
|
||||
});
|
||||
|
||||
load_audio_files();
|
||||
load_lcars_colors();
|
||||
|
||||
window.socket.on('data', function(data) {
|
||||
try {
|
||||
// Log event for debugging (can be disabled in production)
|
||||
if (window.socketDebugMode) {
|
||||
console.log('[SOCKET] Received event:', data.data_type, data);
|
||||
}
|
||||
|
||||
if ([
|
||||
"element_content",
|
||||
"widget_content",
|
||||
"modal_content",
|
||||
"remove_table_row",
|
||||
"table_row",
|
||||
"table_row_content"
|
||||
].includes(data["data_type"])) {
|
||||
/* target element needs to be present for these operations */
|
||||
|
||||
// Validate data structure before accessing
|
||||
if (!data["target_element"]) {
|
||||
console.error('[SOCKET] Missing target_element in data:', data);
|
||||
return false;
|
||||
}
|
||||
|
||||
let target_element_id = data["target_element"]["id"];
|
||||
if (target_element_id == null) {
|
||||
console.warn('[SOCKET] target_element.id is null for data_type:', data["data_type"]);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (data["data_type"] === "widget_content") {
|
||||
/* widget content requires a selector, in case the widget is not yet rendered in the browser
|
||||
* with the help of the selector, we can create it in the right place
|
||||
*/
|
||||
let html_string = '<div id="' + target_element_id + '" class="widget"></div>';
|
||||
let selector = data["target_element"]["selector"];
|
||||
let target_element = $(selector).upsert(
|
||||
'#' + target_element_id,
|
||||
html_string
|
||||
);
|
||||
|
||||
if (data["method"] === "update") {
|
||||
target_element.html(data["payload"]);
|
||||
} else if (data["method"] === "append") {
|
||||
target_element.append(data["payload"]);
|
||||
} else if (data["method"] === "prepend") {
|
||||
play_audio_file("computerbeep_38");
|
||||
let target_table = $('#' + target_element_id + ' ' + data["target_element"]["type"]);
|
||||
/* prepend adds a row on top */
|
||||
target_table.prepend(data["payload"]);
|
||||
let $entries = target_table.find('tr');
|
||||
if ($entries.length >= 50) {
|
||||
$entries.last().remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (data["data_type"] === "element_content") {
|
||||
let target_element = document.getElementById(target_element_id);
|
||||
if (target_element == null) {
|
||||
return false;
|
||||
}
|
||||
if (data["method"] === "update") {
|
||||
if (target_element.innerHTML !== data["payload"]) {
|
||||
target_element.innerHTML = data["payload"];
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else if (data["method"] === "replace") {
|
||||
// Note: After outerHTML replacement, target_element reference becomes invalid
|
||||
// Flash BEFORE replacing, or flash the parent element
|
||||
let parent = target_element.parentElement;
|
||||
target_element.outerHTML = data["payload"];
|
||||
if (parent) {
|
||||
// Flash the new element by finding it in the parent
|
||||
let new_element = document.getElementById(target_element_id);
|
||||
if (new_element) {
|
||||
flash(new_element);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (data["data_type"] === "modal_content") {
|
||||
let target_element = document.getElementById(target_element_id);
|
||||
if (target_element == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let modal_container = target_element.parentElement;
|
||||
modal_container.classList.toggle("open");
|
||||
|
||||
$(target_element).html(data["payload"])
|
||||
}
|
||||
if (data["data_type"] === "table_row") {
|
||||
/* the whole row will be swapped out, not very economic ^^
|
||||
* can be suitable for smaller widgets, not needing the hassle of sub-element id's and stuff
|
||||
* table_row content requires a selector, in case the row is not yet rendered in the browser
|
||||
* with the help of the selector, we can create it in the right place
|
||||
*/
|
||||
play_audio_file("processing");
|
||||
let parent_element = $(data["target_element"]["selector"]);
|
||||
|
||||
let target_element = parent_element.find("#" + target_element_id);
|
||||
|
||||
if (target_element.length === 0) {
|
||||
/* If the row doesn't exist, append it */
|
||||
parent_element.append(data["payload"]);
|
||||
} else {
|
||||
target_element.replaceWith(data["payload"]);
|
||||
}
|
||||
}
|
||||
if (data["data_type"] === "table_row_content") {
|
||||
play_audio_file("keyok1");
|
||||
let parent_element = $('#' + target_element_id);
|
||||
if (parent_element.length === 0) {
|
||||
return false;
|
||||
}
|
||||
if (data["target_element"]["class"].length >= 1) {
|
||||
parent_element.setClass(data["target_element"]["class"]);
|
||||
} else {
|
||||
parent_element[0].removeAttribute("class");
|
||||
}
|
||||
|
||||
let elements_to_update = data["payload"];
|
||||
$.each(elements_to_update, function (key, value) {
|
||||
if ($.type(value) === 'object') {
|
||||
$.each(value, function (sub_key, sub_value) {
|
||||
let element_to_update = $('#' + target_element_id + '_' + key + '_' + sub_key);
|
||||
if (element_to_update.length !== 0 && element_to_update.text() !== sub_value.toString()) {
|
||||
element_to_update.html(sub_value);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
let element_to_update = $('#' + target_element_id + '_' + key);
|
||||
if (element_to_update.length !== 0 && element_to_update.text() !== value.toString()) {
|
||||
element_to_update.html(value);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
if (data["data_type"] === "remove_table_row") {
|
||||
let target_element = document.getElementById(target_element_id);
|
||||
if (target_element && target_element.parentElement) {
|
||||
target_element.parentElement.removeChild(target_element);
|
||||
} else {
|
||||
console.warn('[SOCKET] Cannot remove table row - element not found:', target_element_id);
|
||||
}
|
||||
}
|
||||
} else if (data["data_type"] === "status_message") {
|
||||
/* this does not require any website containers. we simply play sounds and echo logs */
|
||||
if (data['status']) {
|
||||
let json = data["status"];
|
||||
if (json["status"]) {
|
||||
let status = json["status"];
|
||||
let action = data["payload"][0];
|
||||
if (status === "success") {
|
||||
play_audio_file("computerbeep_11");
|
||||
} else if (status === "fail") {
|
||||
play_audio_file("computer_error");
|
||||
flash(document.body, lcars_colors["lcars-chestnut-rose"])
|
||||
}
|
||||
console.log(
|
||||
"received status\n\"" + status + ":" + json["uuid4"] + "\"\n" +
|
||||
"for action\n\"" + action + "\""
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Catch any errors to prevent handler from breaking
|
||||
console.error('[SOCKET ERROR] Failed to process event:', {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
data_type: data ? data.data_type : 'unknown',
|
||||
data: data
|
||||
});
|
||||
|
||||
// Play error sound to alert user
|
||||
play_audio_file("computer_error");
|
||||
|
||||
// Flash screen red to indicate error
|
||||
flash(document.body, lcars_colors["lcars-chestnut-rose"]);
|
||||
}
|
||||
});
|
||||
});
|
||||
1
bot/modules/webserver/templates/frontpage/footer.html
Normal file
1
bot/modules/webserver/templates/frontpage/footer.html
Normal file
@@ -0,0 +1 @@
|
||||
<p>© this page was created in 2019 by <a href="http://chrani.net/">ecv</a> for the <a href="https://notjustfor.me/chrani-bot">chrani-bot</a> webinterface</p>
|
||||
14
bot/modules/webserver/templates/frontpage/header.html
Normal file
14
bot/modules/webserver/templates/frontpage/header.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<div id="header_widgets">
|
||||
<div id="login_logout_widget" class="widget">
|
||||
<div>
|
||||
{% if current_user.is_authenticated %}
|
||||
<a href="/logout" title="{{ current_user.id }}">log out</a>
|
||||
{%- else %}
|
||||
<a href="/login">log in</a>
|
||||
{%- endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hgroup>
|
||||
<h1>{{ title }}</h1>
|
||||
</hgroup>
|
||||
36
bot/modules/webserver/templates/frontpage/index.html
Normal file
36
bot/modules/webserver/templates/frontpage/index.html
Normal file
@@ -0,0 +1,36 @@
|
||||
{%- set not_configured_message = "You should not see this on a configured bot" -%}
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
<link rel="stylesheet" type="text/css" href="/static/reset.css" />
|
||||
<link rel="shortcut icon" href="/static/favicon.ico">
|
||||
<link rel="stylesheet" type="text/css" href="/static/style.css" />
|
||||
<link rel="stylesheet" type="text/css" href="/static/lcars/000-style.css" />
|
||||
<title>{{ title }}</title>
|
||||
<script src="/static/jquery-3.4.1.min.js"></script>
|
||||
{% if current_user.is_authenticated == True %}
|
||||
<script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
|
||||
<script src="/static/system.js"></script>
|
||||
{% endif %}
|
||||
{%- if head -%}{{ head }}{%- endif -%}
|
||||
</head>
|
||||
<body{%- if instance_token %} data-instance-identifier="{{ instance_token }}"{% endif %}>
|
||||
<header>
|
||||
<div>
|
||||
{{ header }}
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
<div>
|
||||
{{ main }}
|
||||
</div>
|
||||
</main>
|
||||
<footer>
|
||||
<div>
|
||||
{{ footer }}
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
20
bot/modules/webserver/templates/jinja2_macros.html
Normal file
20
bot/modules/webserver/templates/jinja2_macros.html
Normal file
@@ -0,0 +1,20 @@
|
||||
{%- macro construct_toggle_link(bool, active_text, deactivate_event, inactive_text, activate_event) -%}
|
||||
{%- set bool = bool|default(false) -%}
|
||||
{%- set active_text = active_text|default(none) -%}
|
||||
{%- set deactivate_event = deactivate_event|default(none) -%}
|
||||
{%- set inactive_text = inactive_text|default(none) -%}
|
||||
{%- set activate_event = activate_event|default(none) -%}
|
||||
{%- if bool == true -%}
|
||||
{%- if deactivate_event != none and activate_event != none -%}
|
||||
<span class="active"><a href="#" onclick="window.socket.emit('{{ deactivate_event[0] }}', {{ deactivate_event[1] }}); return false;">{{ active_text }}</a></span>
|
||||
{%- elif deactivate_event != none and activate_event == none -%}
|
||||
<span class="active"><a href="#" onclick="window.socket.emit('{{ deactivate_event[0] }}', {{ deactivate_event[1] }}); return false;">{{ active_text }}</a></span>
|
||||
{%- endif -%}
|
||||
{%- else -%}
|
||||
{%- if deactivate_event != none and activate_event != none -%}
|
||||
<span class="inactive"><a href="#" onclick="window.socket.emit('{{ activate_event[0] }}', {{ activate_event[1] }}); return false;">{{ inactive_text }}</a></span>
|
||||
{%- elif deactivate_event != none and activate_event == none -%}
|
||||
<span class="inactive"><a href="#" onclick="window.socket.emit('{{ deactivate_event[0] }}', {{ deactivate_event[1] }}); return false;">{{ active_text }}</a></span>
|
||||
{%- endif -%}
|
||||
{%- endif -%}
|
||||
{%- endmacro -%}
|
||||
@@ -0,0 +1,9 @@
|
||||
{%- set logged_in_users_count = webserver_logged_in_users|length -%}
|
||||
<tr id="server_status_widget_logged_in_users">
|
||||
<td>
|
||||
<strong>{{ logged_in_users_count }}</strong>{%- if logged_in_users_count == 1 %} user is {%- else %} users are {%- endif %} currently using the webinterface!<br />
|
||||
{%- if webserver_logged_in_users -%}
|
||||
({{ webserver_logged_in_users|join(', ') }})
|
||||
{%- endif -%}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -0,0 +1,5 @@
|
||||
<div id="server_status_widget_servertime">
|
||||
<span class="info">
|
||||
<div>{{ time }}</div>
|
||||
</span>
|
||||
</div>
|
||||
@@ -0,0 +1,9 @@
|
||||
{%- from 'jinja2_macros.html' import construct_toggle_link with context -%}
|
||||
<div>
|
||||
{{ construct_toggle_link(
|
||||
options_view_toggle,
|
||||
"options", ['widget_event', ['webserver', ['toggle_webserver_status_widget_view', {'steamid': steamid, "action": "show_options"}]]],
|
||||
"back", ['widget_event', ['webserver', ['toggle_webserver_status_widget_view', {'steamid': steamid, "action": "show_frontend"}]]]
|
||||
)}}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<div id="webserver_status_widget_options_toggle" class="pull_out right">
|
||||
{{ control_switch_options_view }}
|
||||
{{ control_servertime }}
|
||||
</div>
|
||||
@@ -0,0 +1,28 @@
|
||||
<header>
|
||||
<div>
|
||||
<span>Webinterface</span>
|
||||
</div>
|
||||
</header>
|
||||
<aside>
|
||||
{{ options_toggle }}
|
||||
</aside>
|
||||
<main>
|
||||
<table>
|
||||
<caption>
|
||||
<span>consume</span>
|
||||
</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Webserver Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ component_logged_in_users }}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="dialog">
|
||||
<div class="modal-content">
|
||||
<p>this is the text inside the modal</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
@@ -0,0 +1,27 @@
|
||||
<header>
|
||||
<div>
|
||||
<span>Webinterface</span>
|
||||
</div>
|
||||
</header>
|
||||
<aside>
|
||||
{{ options_toggle }}
|
||||
</aside>
|
||||
<main>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2">webserver widget options</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th colspan="2"><span>widget-options</span></th>
|
||||
</tr>
|
||||
{% for key, value in widget_options.items() %}
|
||||
<tr>
|
||||
<td><span>{{key}}</span></td><td>{{value}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</main>
|
||||
30
bot/modules/webserver/user.py
Normal file
30
bot/modules/webserver/user.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from flask_login import UserMixin
|
||||
from time import time
|
||||
|
||||
|
||||
class User(UserMixin, object):
|
||||
id = str
|
||||
last_seen = float
|
||||
browser_token = str
|
||||
socket_ids = list # Multiple socket IDs for multiple browser sessions
|
||||
|
||||
def __init__(self, steamid, last_seen=None):
|
||||
self.id = steamid
|
||||
self.last_seen = time() if last_seen is None else last_seen
|
||||
self.instance_token = "anonymous"
|
||||
self.socket_ids = [] # Track all socket connections for this user
|
||||
|
||||
def add_socket(self, sid):
|
||||
"""Add a socket ID to this user's connections."""
|
||||
if sid not in self.socket_ids:
|
||||
self.socket_ids.append(sid)
|
||||
|
||||
def remove_socket(self, sid):
|
||||
"""Remove a socket ID from this user's connections."""
|
||||
if sid in self.socket_ids:
|
||||
self.socket_ids.remove(sid)
|
||||
|
||||
@property
|
||||
def sid(self):
|
||||
"""Return the first (primary) socket ID for backward compatibility."""
|
||||
return self.socket_ids[0] if self.socket_ids else None
|
||||
190
bot/modules/webserver/widgets/webserver_status_widget.py
Normal file
190
bot/modules/webserver/widgets/webserver_status_widget.py
Normal file
@@ -0,0 +1,190 @@
|
||||
from bot import loaded_modules_dict
|
||||
from os import path, pardir
|
||||
|
||||
module_name = path.basename(path.normpath(path.join(path.abspath(__file__), pardir, pardir)))
|
||||
widget_name = path.basename(path.abspath(__file__))[:-3]
|
||||
|
||||
|
||||
def select_view(*args, **kwargs):
|
||||
module = args[0]
|
||||
dispatchers_steamid = kwargs.get('dispatchers_steamid', None)
|
||||
|
||||
current_view = module.get_current_view(dispatchers_steamid)
|
||||
if current_view == "options":
|
||||
options_view(module, dispatchers_steamid=dispatchers_steamid)
|
||||
else:
|
||||
frontend_view(module, dispatchers_steamid=dispatchers_steamid)
|
||||
|
||||
|
||||
def frontend_view(*args, **kwargs):
|
||||
module = args[0]
|
||||
dispatchers_steamid = kwargs.get("dispatchers_steamid", None)
|
||||
|
||||
template_frontend = module.templates.get_template('webserver_status_widget/view_frontend.html')
|
||||
template_servertime = module.templates.get_template('webserver_status_widget/control_servertime.html')
|
||||
|
||||
template_options_toggle = module.templates.get_template('webserver_status_widget/control_switch_view.html')
|
||||
template_options_toggle_view = module.templates.get_template(
|
||||
'webserver_status_widget/control_switch_options_view.html'
|
||||
)
|
||||
|
||||
component_logged_in_users = module.templates.get_template('webserver_status_widget/component_logged_in_users.html')
|
||||
|
||||
try:
|
||||
server_is_online = module.dom.data.get("module_telnet").get("server_is_online", True)
|
||||
except AttributeError:
|
||||
server_is_online = True
|
||||
|
||||
current_view = module.get_current_view(dispatchers_steamid)
|
||||
webserver_logged_in_users = (
|
||||
module.dom.data
|
||||
.get(module.get_module_identifier(), {})
|
||||
.get("webserver_logged_in_users", [])
|
||||
)
|
||||
|
||||
data_to_emit = module.template_render_hook(
|
||||
module,
|
||||
template=template_frontend,
|
||||
component_logged_in_users=module.template_render_hook(
|
||||
module,
|
||||
template=component_logged_in_users,
|
||||
webserver_logged_in_users=webserver_logged_in_users
|
||||
),
|
||||
options_toggle=module.template_render_hook(
|
||||
module,
|
||||
template=template_options_toggle,
|
||||
control_switch_options_view=module.template_render_hook(
|
||||
module,
|
||||
template=template_options_toggle_view,
|
||||
steamid=dispatchers_steamid,
|
||||
options_view_toggle=(current_view == "frontend")
|
||||
),
|
||||
control_servertime=module.template_render_hook(
|
||||
module,
|
||||
template=template_servertime,
|
||||
time=module.dom.data.get("module_telnet", {}).get("last_recorded_servertime", None),
|
||||
)
|
||||
),
|
||||
server_is_online=server_is_online
|
||||
)
|
||||
|
||||
module.webserver.send_data_to_client_hook(
|
||||
module,
|
||||
payload=data_to_emit,
|
||||
data_type="widget_content",
|
||||
clients=[dispatchers_steamid],
|
||||
target_element={
|
||||
"id": "webserver_status_widget",
|
||||
"type": "table",
|
||||
"selector": "body > main > div"
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def options_view(*args, **kwargs):
|
||||
module = args[0]
|
||||
dispatchers_steamid = kwargs.get("dispatchers_steamid", None)
|
||||
|
||||
template_frontend = module.templates.get_template('webserver_status_widget/view_options.html')
|
||||
template_servertime = module.templates.get_template('webserver_status_widget/control_servertime.html')
|
||||
|
||||
template_options_toggle = module.templates.get_template('webserver_status_widget/control_switch_view.html')
|
||||
template_options_toggle_view = module.templates.get_template(
|
||||
'webserver_status_widget/control_switch_options_view.html'
|
||||
)
|
||||
|
||||
current_view = module.get_current_view(dispatchers_steamid)
|
||||
|
||||
data_to_emit = module.template_render_hook(
|
||||
module,
|
||||
template=template_frontend,
|
||||
options_toggle=module.template_render_hook(
|
||||
module,
|
||||
template=template_options_toggle,
|
||||
control_switch_options_view=module.template_render_hook(
|
||||
module,
|
||||
template=template_options_toggle_view,
|
||||
steamid=dispatchers_steamid,
|
||||
options_view_toggle=(current_view == "frontend")
|
||||
),
|
||||
control_servertime=module.template_render_hook(
|
||||
module,
|
||||
template=template_servertime,
|
||||
time=module.dom.data.get("module_telnet").get("last_recorded_servertime", None),
|
||||
)
|
||||
),
|
||||
widget_options=module.webserver.options
|
||||
)
|
||||
|
||||
module.webserver.send_data_to_client_hook(
|
||||
module,
|
||||
payload=data_to_emit,
|
||||
data_type="widget_content",
|
||||
clients=[dispatchers_steamid],
|
||||
target_element={
|
||||
"id": "webserver_status_widget",
|
||||
"type": "table",
|
||||
"selector": "body > main > div"
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def update_servertime(*args, **kwargs):
|
||||
module = args[0]
|
||||
|
||||
template_servertime = module.templates.get_template('webserver_status_widget/control_servertime.html')
|
||||
servertime_view = module.template_render_hook(
|
||||
module,
|
||||
template=template_servertime,
|
||||
time=module.dom.data.get("module_telnet").get("last_recorded_servertime", None)
|
||||
)
|
||||
|
||||
module.webserver.send_data_to_client_hook(
|
||||
module,
|
||||
payload=servertime_view,
|
||||
data_type="element_content",
|
||||
method="replace",
|
||||
clients=module.webserver.connected_clients.keys(),
|
||||
target_element={
|
||||
"id": "server_status_widget_servertime"
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def update_logged_in_users(*args, **kwargs):
|
||||
module = args[0]
|
||||
updated_values_dict = kwargs.get("updated_values_dict", None)
|
||||
|
||||
webserver_logged_in_users = updated_values_dict.get("webserver_logged_in_users", [])
|
||||
|
||||
component_logged_in_users = module.templates.get_template('webserver_status_widget/component_logged_in_users.html')
|
||||
component_logged_in_users_view = module.template_render_hook(
|
||||
module,
|
||||
template=component_logged_in_users,
|
||||
webserver_logged_in_users=webserver_logged_in_users
|
||||
)
|
||||
|
||||
module.webserver.send_data_to_client_hook(
|
||||
module,
|
||||
payload=component_logged_in_users_view,
|
||||
data_type="element_content",
|
||||
method="replace",
|
||||
clients=module.webserver.connected_clients.keys(),
|
||||
target_element={
|
||||
"id": "server_status_widget_logged_in_users"
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
widget_meta = {
|
||||
"description": "shows all users with an active session for the webinterface and some other stats",
|
||||
"main_widget": select_view,
|
||||
"handlers": {
|
||||
"module_webserver/visibility/%steamid%/current_view": select_view,
|
||||
"module_webserver/webserver_logged_in_users": update_logged_in_users,
|
||||
"module_telnet/last_recorded_servertime": update_servertime
|
||||
},
|
||||
"enabled": True
|
||||
}
|
||||
|
||||
loaded_modules_dict["module_" + module_name].register_widget(widget_name, widget_meta)
|
||||
Reference in New Issue
Block a user