Merge branch 'dev-Silversthorn' of gitlab.com:crafty-controller/crafty-commander into dev-Silversthorn

This commit is contained in:
Silversthorn 2021-08-30 21:31:20 +02:00
commit b7802fdc45
20 changed files with 548 additions and 44 deletions

View File

@ -1,9 +1,12 @@
from app.classes.shared.helpers import Helpers
import struct
import socket
import base64
import json
import sys
import os
import logging.config
from app.classes.shared.console import console
logger = logging.getLogger(__name__)
@ -25,8 +28,26 @@ class Server:
description = self.description
if 'extra' in description.keys():
for e in description['extra']:
#Conversion format code needed only for Java Version
lines.append(get_code_format("reset"))
if "bold" in e.keys():
lines.append(get_code_format("bold"))
if "italic" in e.keys():
lines.append(get_code_format("italic"))
if "underlined" in e.keys():
lines.append(get_code_format("underlined"))
if "strikethrough" in e.keys():
lines.append(get_code_format("strikethrough"))
if "obfuscated" in e.keys():
lines.append(get_code_format("obfuscated"))
if "color" in e.keys():
lines.append(get_code_format(e['color']))
#Then append the text
if "text" in e.keys():
lines.append(e['text'])
if e['text'] == '\n':
lines.append("§§")
else:
lines.append(e['text'])
total_text = " ".join(lines)
self.description = total_text
@ -70,6 +91,26 @@ class Player:
def __str__(self):
return self.name
def get_code_format(format_name):
root_dir = os.path.abspath(os.path.curdir)
format_file = os.path.join(root_dir, 'app', 'config', 'motd_format.json')
try:
with open(format_file, "r", encoding='utf-8') as f:
data = json.load(f)
if format_name in data.keys():
return data.get(format_name)
else:
logger.error("Format MOTD Error: format name {} does not exist".format(format_name))
console.error("Format MOTD Error: format name {} does not exist".format(format_name))
return ""
except Exception as e:
logger.critical("Config File Error: Unable to read {} due to {}".format(format_file, e))
console.critical("Config File Error: Unable to read {} due to {}".format(format_file, e))
return ""
# For the rest of requests see wiki.vg/Protocol
def ping(ip, port):

View File

@ -19,7 +19,7 @@ class ServerProps:
s = line
s1 = s[:s.find('=')]
if '\n' in s:
s2 = s[s.find('=')+1:s.find('\\')]
s2 = s[s.find('=')+1:s.find('\n')]
else:
s2 = s[s.find('=')+1:]
d[s1] = s2

View File

@ -11,6 +11,7 @@ from app.classes.shared.helpers import helper
from app.classes.shared.console import console
from app.classes.shared.models import Servers
from app.classes.minecraft.server_props import ServerProps
from app.classes.web.websocket_helper import websocket_helper
logger = logging.getLogger(__name__)
@ -173,11 +174,11 @@ class ServerJars:
response = self._get_api_result(url)
return response
def download_jar(self, server, version, path):
update_thread = threading.Thread(target=self.a_download_jar, daemon=True, name="exe_download", args=(server, version, path))
def download_jar(self, server, version, path, name):
update_thread = threading.Thread(target=self.a_download_jar, daemon=True, name="exe_download", args=(server, version, path, name))
update_thread.start()
def a_download_jar(self, server, version, path):
def a_download_jar(self, server, version, path, name):
fetch_url = "{base}/api/fetchJar/{server}/{version}".format(base=self.base_url, server=server, version=version)
# open a file stream
@ -189,6 +190,8 @@ class ServerJars:
except Exception as e:
logger.error("Unable to save jar to {path} due to error:{error}".format(path=path, error=e))
pass
websocket_helper.broadcast('notification', "Executable download finished for server named: " + name)
return False

View File

@ -4,6 +4,7 @@ import time
import psutil
import logging
import datetime
import base64
from app.classes.shared.helpers import helper
@ -145,12 +146,19 @@ class Stats:
logger.info("Unable to read json from ping_obj: {}".format(e))
pass
try:
server_icon = base64.encodebytes(ping_obj.icon)
except Exception as e:
server_icon = False
logger.info("Unable to read the server icon : {}".format(e))
ping_data = {
'online': online_stats.get("online", 0),
'max': online_stats.get('max', 0),
'players': online_stats.get('players', 0),
'server_description': ping_obj.description,
'server_version': ping_obj.version
'server_version': ping_obj.version,
'server_icon': server_icon
}
return ping_data
@ -167,7 +175,7 @@ class Stats:
# TODO: search server properties file for possible override of 127.0.0.1
internal_ip = server_data.get('server-ip', "127.0.0.1")
internal_ip = server_data.get('server_ip', "127.0.0.1")
server_port = server_settings.get('server-port', "25565")
logger.debug("Pinging {} on port {}".format(internal_ip, server_port))
@ -210,7 +218,7 @@ class Stats:
p_stats = self._get_process_stats(server_obj.PID)
# TODO: search server properties file for possible override of 127.0.0.1
internal_ip = server_data.get('server-ip', "127.0.0.1")
internal_ip = server_data.get('server_ip', "127.0.0.1")
server_port = server_settings.get('server-port', "25565")
logger.debug("Pinging server '{}' on {}:{}".format(s.get('server_name', "ID#{}".format(server_id)), internal_ip, server_port))
@ -246,6 +254,62 @@ class Stats:
server_stats_list.append(server_stats)
return server_stats_list
def get_raw_server_stats(self, server_id):
server_stats = {}
server = self.controller.get_server_obj(server_id)
logger.debug('Getting stats for server: {}'.format(server_id))
# get our server object, settings and data dictionaries
server_obj = self.controller.get_server_obj(server_id)
server_obj.reload_server_settings()
server_settings = self.controller.get_server_settings(server_id)
server_data = self.controller.get_server_data(server_id)
# world data
world_name = server_settings.get('level-name', 'Unknown')
world_path = os.path.join(server_data.get('path', None), world_name)
# process stats
p_stats = self._get_process_stats(server_obj.PID)
# TODO: search server properties file for possible override of 127.0.0.1
internal_ip = server_data.get('server_ip', "127.0.0.1")
server_port = server_settings.get('server-port', "25565")
logger.debug("Pinging server '{}' on {}:{}".format(server.name, internal_ip, server_port))
int_mc_ping = ping(internal_ip, int(server_port))
int_data = False
ping_data = {}
# if we got a good ping return, let's parse it
if int_mc_ping:
int_data = True
ping_data = self.parse_server_ping(int_mc_ping)
server_stats = {
'id': server_id,
'started': server_obj.get_start_time(),
'running': server_obj.check_running(),
'cpu': p_stats.get('cpu_usage', 0),
'mem': p_stats.get('memory_usage', 0),
"mem_percent": p_stats.get('mem_percentage', 0),
'world_name': world_name,
'world_size': self.get_world_size(world_path),
'server_port': server_port,
'int_ping_results': int_data,
'online': ping_data.get("online", False),
"max": ping_data.get("max", False),
'players': ping_data.get("players", False),
'desc': ping_data.get("server_description", False),
'version': ping_data.get("server_version", False),
'icon': ping_data.get("server_icon", False)
}
return server_stats
def record_stats(self):
stats_to_send = self.get_node_stats()

View File

@ -97,6 +97,14 @@ class Controller:
server_obj = self.get_server_obj(server_id)
server_obj.reload_server_settings()
def get_server_settings(self, server_id):
for s in self.servers_list:
if int(s['server_id']) == int(server_id):
return s['server_settings']
logger.warning("Unable to find server object for server id {}".format(server_id))
return False
def get_server_obj(self, server_id):
for s in self.servers_list:
if int(s['server_id']) == int(server_id):
@ -104,6 +112,14 @@ class Controller:
logger.warning("Unable to find server object for server id {}".format(server_id))
return False
def get_server_data(self, server_id):
for s in self.servers_list:
if int(s['server_id']) == int(server_id):
return s['server_data_obj']
logger.warning("Unable to find server object for server id {}".format(server_id))
return False
@staticmethod
def list_defined_servers():
@ -184,14 +200,6 @@ class Controller:
server_list = db_helper.get_authorized_servers(userId)
return server_list
def get_server_data(self, server_id):
for s in self.servers_list:
if int(s['server_id']) == int(server_id):
return s['server_data_obj']
logger.warning("Unable to find server object for server id {}".format(server_id))
return False
def list_running_servers(self):
running_servers = []
@ -297,7 +305,7 @@ class Controller:
server_stop = "stop"
# download the jar
server_jar_obj.download_jar(server, version, full_jar_path)
server_jar_obj.download_jar(server, version, full_jar_path, name)
new_id = self.register_server(name, server_id, server_dir, backup_path, server_command, server_file, server_log_file, server_stop)
return new_id

View File

@ -321,7 +321,7 @@ class db_shortcuts:
def get_all_defined_servers():
query = Servers.select()
return db_helper.return_rows(query)
@staticmethod
def get_authorized_servers(user_id):
server_data = []
@ -408,7 +408,7 @@ class db_shortcuts:
for u in user_roles:
roles_list.append(db_helper.get_role(u.role_id))
for r in roles_list:
role_test = Role_Servers.select().where(Role_Servers.role_id == r.get('role_id'))
for t in role_test:
@ -436,7 +436,7 @@ class db_shortcuts:
if not db_helper.get_server_data_by_id(server_id):
return False
return True
@staticmethod
def server_id_authorized(serverId, user_id):
authorized = 0
@ -449,7 +449,7 @@ class db_shortcuts:
if authorized.count() == 0:
return False
return True
@staticmethod
def set_update(server_id, value):
try:
@ -485,16 +485,16 @@ class db_shortcuts:
@staticmethod
def get_crafty_permissions_list(user_id):
permissions_mask = db_helper.get_crafty_permissions_mask(user_id)
permissions_mask = db_helper.get_crafty_permissions_mask(user_id)
permissions_list = crafty_permissions.get_permissions(permissions_mask)
return permissions_list
@staticmethod
def get_all_permission_quantity_list():
quantity_list = {
Enum_Permissions_Crafty.Server_Creation.name: -1,
Enum_Permissions_Crafty.User_Config.name: -1,
Enum_Permissions_Crafty.Roles_Config.name: -1,
Enum_Permissions_Crafty.Roles_Config.name: -1,
}
return quantity_list
@ -504,7 +504,7 @@ class db_shortcuts:
quantity_list = {
Enum_Permissions_Crafty.Server_Creation.name: user_crafty.limit_server_creation,
Enum_Permissions_Crafty.User_Config.name: user_crafty.limit_user_creation,
Enum_Permissions_Crafty.Roles_Config.name: user_crafty.limit_role_creation,
Enum_Permissions_Crafty.Roles_Config.name: user_crafty.limit_role_creation,
}
return quantity_list
@ -516,7 +516,7 @@ class db_shortcuts:
try:
user_crafty = User_Crafty.select().where(User_Crafty.user_id == user_id).get()
except User_Crafty.DoesNotExist:
user_crafty = User_Crafty.Insert({
user_crafty = User_Crafty.insert({
User_Crafty.user_id: user_id,
User_Crafty.permissions: "000",
User_Crafty.limit_server_creation: 0,
@ -540,16 +540,16 @@ class db_shortcuts:
quantity_list = {
Enum_Permissions_Crafty.Server_Creation.name: user_crafty.created_server,
Enum_Permissions_Crafty.User_Config.name: user_crafty.created_user,
Enum_Permissions_Crafty.Roles_Config.name: user_crafty.created_role,
Enum_Permissions_Crafty.Roles_Config.name: user_crafty.created_role,
}
return quantity_list
@staticmethod
def get_crafty_limit_value(user_id, permission):
user_crafty = db_helper.get_User_Crafty(user_id)
quantity_list = get_permission_quantity_list(user_id)
return quantity_list[permission]
@staticmethod
def can_add_in_crafty(user_id, permission):
user_crafty = db_helper.get_User_Crafty(user_id)
@ -559,16 +559,20 @@ class db_shortcuts:
return can and ((quantity_list[permission.name] < limit_list[permission.name]) or limit_list[permission.name] == -1 )
@staticmethod
def add_server_creation(user_id):
def add_server_creation(user_id):
user_crafty = db_helper.get_User_Crafty(user_id)
user_crafty.created_server += 1
User_Crafty.save(user_crafty)
return user_crafty.created_server
<<<<<<< HEAD
#************************************************************************************************
# Host_Stats Methods
#************************************************************************************************
=======
>>>>>>> 45739a2e5ffd23d2373c51a4e5122e73ea174359
@staticmethod
def get_latest_hosts_stats():
query = Host_Stats.select().order_by(Host_Stats.id.desc()).get()
@ -611,7 +615,48 @@ class db_shortcuts:
return user
else:
return {}
<<<<<<< HEAD
=======
@staticmethod
def add_role_to_user(user_id, role_id):
User_Roles.insert({
User_Roles.user_id: user_id,
User_Roles.role_id: role_id
}).execute()
@staticmethod
def add_user_roles(user):
if type(user) == dict:
user_id = user['user_id']
else:
user_id = user.user_id
# I just copied this code from get_user, it had those TODOs & comments made by mac - Lukas
roles_query = User_Roles.select().join(Roles, JOIN.INNER).where(User_Roles.user_id == user_id)
# TODO: this query needs to be narrower
roles = set()
for r in roles_query:
roles.add(r.role_id.role_id)
user['roles'] = roles
#logger.debug("user: ({}) {}".format(user_id, user))
return user
@staticmethod
def add_user_crafty(user_id, uc_permissions):
user_crafty = User_Crafty.insert({User_Crafty.user_id: user_id, User_Crafty.permissions: uc_permissions}).execute()
return user_crafty
@staticmethod
def add_role_server(server_id, role_id, rs_permissions="00000000"):
servers = Role_Servers.insert({Role_Servers.server_id: server_id, Role_Servers.role_id: role_id, Role_Servers.permissions: rs_permissions}).execute()
return servers
>>>>>>> 45739a2e5ffd23d2373c51a4e5122e73ea174359
@staticmethod
def user_query(user_id):
user_query = Users.select().where(Users.user_id == user_id)
@ -937,7 +982,7 @@ class db_shortcuts:
Audit_Log.log_msg: audit_msg,
Audit_Log.source_ip: source_ip
}).execute()
@staticmethod
def add_to_audit_log_raw(user_name, user_id, server_id, log_msg, source_ip):
Audit_Log.insert({
@ -1082,7 +1127,7 @@ class Permissions_Servers:
for member in Enum_Permissions_Server.__members__.items():
permissions_list.append(member[1])
return permissions_list
@staticmethod
def get_permissions(permissions_mask):
permissions_list = []
@ -1097,17 +1142,17 @@ class Permissions_Servers:
if permission_mask[permission_tested.value] == '1':
result = True
return result
@staticmethod
def set_permission(permission_mask, permission_tested: Enum_Permissions_Server, value):
l = list(permission_mask)
l[permission_tested.value] = str(value)
permission_mask = ''.join(l)
return permission_mask
@staticmethod
def get_permission(permission_mask, permission_tested: Enum_Permissions_Server):
return permission_mask[permission_tested.value]
return permission_mask[permission_tested.value]
#************************************************************************************************
# Crafty Permissions Class
@ -1116,7 +1161,7 @@ class Enum_Permissions_Crafty(Enum):
Server_Creation = 0
User_Config = 1
Roles_Config = 2
class Permissions_Crafty:
@staticmethod
@ -1125,7 +1170,7 @@ class Permissions_Crafty:
for member in Enum_Permissions_Crafty.__members__.items():
permissions_list.append(member[1])
return permissions_list
@staticmethod
def get_permissions(permissions_mask):
permissions_list = []
@ -1140,17 +1185,17 @@ class Permissions_Crafty:
if permission_mask[permission_tested.value] == '1':
result = True
return result
@staticmethod
def set_permission(permission_mask, permission_tested: Enum_Permissions_Crafty, value):
l = list(permission_mask)
l[permission_tested.value] = str(value)
permission_mask = ''.join(l)
return permission_mask
@staticmethod
def get_permission(permission_mask, permission_tested: Enum_Permissions_Crafty):
return permission_mask[permission_tested.value]
return permission_mask[permission_tested.value]
#************************************************************************************************
@ -1159,4 +1204,4 @@ class Permissions_Crafty:
installer = db_builder()
db_helper = db_shortcuts()
server_permissions = Permissions_Servers()
crafty_permissions = Permissions_Crafty()
crafty_permissions = Permissions_Crafty()

View File

@ -115,7 +115,6 @@ class Server:
self.settings = server_data_obj
# build our server run command
self.setup_server_run_command()
if server_data_obj['auto_start']:
delay = int(self.settings['auto_start_delay'])

View File

@ -0,0 +1,55 @@
from re import template
import sys
import json
import logging
import tornado.web
import tornado.escape
import requests
from app.classes.shared.helpers import helper
from app.classes.web.base_handler import BaseHandler
from app.classes.shared.console import console
from app.classes.shared.models import Users, fn, db_helper
logger = logging.getLogger(__name__)
try:
import bleach
except ModuleNotFoundError as e:
logger.critical("Import Error: Unable to load {} module".format(e.name), exc_info=True)
console.critical("Import Error: Unable to load {} module".format(e.name))
sys.exit(1)
class StatusHandler(BaseHandler):
def get(self):
page_data = {}
page_data['servers'] = db_helper.get_all_servers_stats()
for srv in page_data['servers']:
server_data = srv.get('server_data', False)
server_id = server_data.get('server_id', False)
srv['raw_ping_result'] = self.controller.stats.get_raw_server_stats(server_id)
template = 'public/status.html'
self.render(
template,
data=page_data,
translate=self.translator.translate,
)
def post(self):
page_data = {}
page_data['servers'] = db_helper.get_all_servers_stats()
for srv in page_data['servers']:
server_data = srv.get('server_data', False)
server_id = server_data.get('server_id', False)
srv['raw_ping_result'] = self.controller.stats.get_raw_server_stats(server_id)
template = 'public/status.html'
self.render(
template,
data=page_data,
translate=self.translator.translate,
)

View File

@ -29,6 +29,7 @@ try:
from app.classes.shared.translation import translation
from app.classes.web.upload_handler import UploadHandler
from app.classes.web.http_handler import HTTPHandler, HTTPHandlerPage
from app.classes.web.status_handler import StatusHandler
except ModuleNotFoundError as e:
logger.critical("Import Error: Unable to load {} module".format(e, e.name))
@ -132,6 +133,7 @@ class Webserver:
(r'/api/stats/node', NodeStats, handler_args),
(r'/ws', SocketHandler, handler_args),
(r'/upload', UploadHandler),
(r'/status', StatusHandler, handler_args)
]
app = tornado.web.Application(

View File

@ -80,3 +80,8 @@ body { background-color: var(--dark) !important; /* Firefox */ }
.actions_serverlist > a > i {
cursor: pointer;
}
.corner {
position: absolute;
margin-top: 0;
margin-left: 0;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -0,0 +1,127 @@
var obfuscators = [];
var styleMap = {
'§0': 'color:#000000',
'§1': 'color:#0000AA',
'§2': 'color:#00AA00',
'§3': 'color:#00AAAA',
'§4': 'color:#AA0000',
'§5': 'color:#AA00AA',
'§6': 'color:#FFAA00',
'§7': 'color:#AAAAAA',
'§8': 'color:#555555',
'§9': 'color:#5555FF',
'§a': 'color:#55FF55',
'§b': 'color:#55FFFF',
'§c': 'color:#FF5555',
'§d': 'color:#FF55FF',
'§e': 'color:#FFFF55',
'§f': 'color:#FFFFFF',
'§l': 'font-weight:bold',
'§m': 'text-decoration:line-through',
'§n': 'text-decoration:underline',
'§o': 'font-style:italic',
};
function obfuscate(string, elem) {
var magicSpan,
currNode;
if(string.indexOf('<br>') > -1) {
elem.innerHTML = string;
for(var j = 0, len = elem.childNodes.length; j < len; j++) {
currNode = elem.childNodes[j];
if(currNode.nodeType === 3) {
magicSpan = document.createElement('span');
magicSpan.innerHTML = currNode.nodeValue;
elem.replaceChild(magicSpan, currNode);
init(magicSpan);
}
}
} else {
init(elem, string);
}
function init(el, str) {
var i = 0,
obsStr = str || el.innerHTML,
len = obsStr.length;
obfuscators.push( window.setInterval(function () {
if(i >= len) i = 0;
obsStr = replaceRand(obsStr, i);
el.innerHTML = obsStr;
i++;
}, 0) );
}
function randInt(min, max) {
return Math.floor( Math.random() * (max - min + 1) ) + min;
}
function replaceRand(string, i) {
var randChar = String.fromCharCode( randInt(64, 95) );
return string.substr(0, i) + randChar + string.substr(i + 1, string.length);
}
}
function applyCode(string, codes) {
var elem = document.createElement('span'),
obfuscated = false;
string = string.replace(/\x00*/g, '');
for(var i = 0, len = codes.length; i < len; i++) {
elem.style.cssText += styleMap[codes[i]] + ';';
if(codes[i] === '§k') {
obfuscate(string, elem);
obfuscated = true;
}
}
if(!obfuscated) elem.innerHTML = string;
return elem;
}
function parseStyle(string) {
var codes = string.match(/§.{1}/g) || [],
indexes = [],
apply = [],
tmpStr,
deltaIndex,
noCode,
final = document.createDocumentFragment(),
i;
string = string.replace(/\n|\\n/g, '<br>');
for(i = 0, len = codes.length; i < len; i++) {
indexes.push( string.indexOf(codes[i]) );
string = string.replace(codes[i], '\x00\x00');
}
if(indexes[0] !== 0) {
final.appendChild( applyCode( string.substring(0, indexes[0]), [] ) );
}
for(i = 0; i < len; i++) {
indexDelta = indexes[i + 1] - indexes[i];
if(indexDelta === 2) {
while(indexDelta === 2) {
apply.push ( codes[i] );
i++;
indexDelta = indexes[i + 1] - indexes[i];
}
apply.push ( codes[i] );
} else {
apply.push( codes[i] );
}
if( apply.lastIndexOf('§r') > -1) {
apply = apply.slice( apply.lastIndexOf('§r') + 1 );
}
tmpStr = string.substring( indexes[i], indexes[i + 1] );
final.appendChild( applyCode(tmpStr, apply) );
}
return final;
}
function clearObfuscators() {
var i = obfuscators.length;
for(;i--;) {
clearInterval(obfuscators[i]);
}
obfuscators = [];
}
function initParser(input, output) {
clearObfuscators();
var input = document.getElementById(input),
output = document.getElementById(output);
if (input != null && output != null) {
var parsed = parseStyle( input.innerHTML );
output.innerHTML = '';
output.appendChild(parsed);
}
}

View File

@ -11,6 +11,8 @@
<link rel="stylesheet" href="/static/assets/vendors/ti-icons/css/themify-icons.css">
<link rel="stylesheet" href="/static/assets/vendors/typicons/typicons.css">
<link rel="stylesheet" href="/static/assets/vendors/css/vendor.bundle.base.css">
<link rel="stylesheet" href="/static/assets/vendors/fontawesome5/css/all.css">
<link rel="stylesheet" href="/static/assest/css/crafty.css">
<!-- endinject -->
<!-- Plugin css for this page -->
<!-- End Plugin css for this page -->
@ -24,7 +26,7 @@
<div class="container-fluid page-body-wrapper full-page-wrapper">
<div class="content-wrapper d-flex align-items-center auth auth-bg-1 theme-one">
<div class="row w-100">
<div class="col-lg-4 mx-auto">
<div class="mx-auto">
{% block content %}
{% end %}
@ -47,5 +49,11 @@
<script src="/static/assets/js/shared/settings.js"></script>
<script src="/static/assets/js/shared/todolist.js"></script>
<!-- endinject -->
{% block js %}
<!-- Custom js for this page -->
<!-- End custom js for this page -->
{% end %}
</body>
</html>

View File

@ -184,7 +184,7 @@
</label>
</div>
{% end %}
{% end %}
<button type="submit" class="btn btn-success mr-2"><i class="fas fa-save"></i> {{ translate('panelConfig', 'save') }}</button>
<button type="reset" onclick="location.href='/panel/panel_config'" class="btn btn-light"><i class="fas fa-undo-alt"></i> {{ translate('panelConfig', 'cancel') }}</button>
</form>

View File

@ -0,0 +1,82 @@
{% extends ../public_base.html %}
{% block meta %}
<meta http-equiv="refresh" content="30">
{% end %}
{% block title %}Crafty Controller - {{ translate('dashboard', 'dashboard') }}{% end %}
{% block content %}
<div class="content-wrapper col-md login-modal" style="background-color: #222437;">
<img src="/static/assets/images/logo_long.png" style='width: 25%; margin-left: 38%;'>
<hr />
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr class="rounded">
<th>{{ translate('dashboard', 'server') }}</th>
<th>{{ translate('dashboard', 'players') }}</th>
<th>{{ translate('dashboard', 'motd') }}</th>
<th>{{ translate('dashboard', 'version') }}</th>
<th>{{ translate('dashboard', 'status') }}</th>
</tr>
</thead>
<tbody>
{% for server in data['servers'] %}
<tr>
<td>
<i class="fas fa-server"></i>
{{ server['server_data']['server_name'] }}
</td>
{% if server['stats']['int_ping_results'] != 'False' %}
<td>
{{ server['stats']['online'] }} / {{ server['stats']['max'] }} {{ translate('dashboard', 'max') }}<br />
</td>
<td>
{% if server['stats']['desc'] != 'False' %}
{% if server['raw_ping_result']['icon'] %}
<img src="data:image/png;base64,{% raw server['raw_ping_result']['icon'] %}" alt="icon"/>
{% else %}
<img src="/static/assets/images/pack.png" alt="icon" />
{% end %}
<span id="input_motd">{{ server['stats']['desc'] }}</span> <br />
{% end %}
</td>
<td>
{% if server['stats']['version'] != 'False' %}
{{ server['stats']['version'] }}
{% end %}
</td>
{% else %}
<td colspan="3">
<span class="text-warning"><i class="fas fa-exclamation-triangle"></i> Crafty can't get infos from this Server </span>
</td>
{% end %}
<td>
{% if server['stats']['int_ping_results'] %}
<span class="text-success"><i class="fas fa-signal"></i> {{ translate('dashboard', 'online') }}</span>
{% else %}
<span class="text-danger"><i class="fas fa-ban"></i> {{ translate('dashboard', 'offline') }}</span>
{% end %}
</td>
</tr>
{% end %}
</tbody>
</table>
</div>
<hr />
</div>
{% end %}
{% block js %}
<script src="/static/assets/js/motd.js"></script>
<script>
$(document).ready(function () {
initParser('input_motd', 'input_motd');
}());
</script>
{% end %}

View File

@ -0,0 +1,59 @@
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
{% block meta %}{% end %}
<title>{% block title %}{{ _('Default') }}{% end %}</title>
<!-- plugins:css -->
<link rel="stylesheet" href="/static/assets/vendors/mdi/css/materialdesignicons.min.css">
<link rel="stylesheet" href="/static/assets/vendors/flag-icon-css/css/flag-icon.min.css">
<link rel="stylesheet" href="/static/assets/vendors/ti-icons/css/themify-icons.css">
<link rel="stylesheet" href="/static/assets/vendors/typicons/typicons.css">
<link rel="stylesheet" href="/static/assets/vendors/css/vendor.bundle.base.css">
<link rel="stylesheet" href="/static/assets/vendors/fontawesome5/css/all.css">
<!-- endinject -->
<!-- Plugin css for this page -->
<!-- End Plugin css for this page -->
<!-- Layout styles -->
<link rel="stylesheet" href="/static/assets/css/dark/style.css">
<!-- End Layout styles -->
<link rel="shortcut icon" href="/static/assets/images/favicon.png" />
</head>
<body class="dark-theme">
<div class="container-scroller">
<div class="container-fluid page-body-wrapper full-page-wrapper">
<div class="content-wrapper d-flex align-items-center auth auth-bg-1 theme-one">
<div class="row w-100">
<div class="mx-auto">
<div class="auto-form-wrapper">
{% block content %}
{% end %}
</div>
</div>
</div>
</div>
<!-- content-wrapper ends -->
</div>
<!-- page-body-wrapper ends -->
</div>
<!-- container-scroller -->
<!-- plugins:js -->
<script src="/static/assets/vendors/js/vendor.bundle.base.js"></script>
<!-- endinject -->
<!-- inject:js -->
<script src="/static/assets/js/shared/off-canvas.js"></script>
<script src="/static/assets/js/shared/hoverable-collapse.js"></script>
<script src="/static/assets/js/shared/misc.js"></script>
<script src="/static/assets/js/shared/settings.js"></script>
<script src="/static/assets/js/shared/todolist.js"></script>
<!-- endinject -->
{% block js %}
<!-- Custom js for this page -->
<!-- End custom js for this page -->
{% end %}
</body>
</html>

View File

@ -70,6 +70,8 @@
"server": "Server",
"actions": "Actions",
"world": "World",
"motd": "MOTD",
"version": "Version",
"status": "Status",
"online": "Online",
"offline": "Offline",

View File

@ -70,6 +70,8 @@
"server": "Palvelin",
"actions": "Toiminnot",
"world": "Maailma",
"motd": "MOTD",
"version": "Versio",
"status": "Tila",
"online": "Päällä",
"offline": "Pois päältä",

View File

@ -70,6 +70,8 @@
"server": "Serveur",
"actions": "Actions",
"world": "Monde",
"motd": "MOTD",
"version": "Version",
"status": "Statut",
"online": "En Ligne",
"offline": "Hors Ligne",