Merge branch 'dev' into tweak/front-end-translation-fa

This commit is contained in:
Zedifus 2023-02-11 19:47:36 +00:00
commit e39e87824d
19 changed files with 277 additions and 175 deletions

View File

@ -1,13 +1,18 @@
# Changelog # Changelog
## --- [4.0.21] - 2023/TBD ## --- [4.0.21] - 2023/TBD
### New features ### New features
TBD - Add better feedback for uploads with a progress bar ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/546))
- Add ignored exit codes for crash detection ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/553))
### Bug fixes ### Bug fixes
TBD - Fix exception related to page data on server start ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/544))
- Fix logical issue with uploading dynamic files ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/555))
- Fix backups failing by correctly using tz objects ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/556))
### Tweaks ### Tweaks
TBD - Cleanup authentication helpers ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/545))
- Optimize file upload progress WS ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/546))
- Truncate sidebar servers to a max of 10 ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/552))
### Lang ### Lang
TBD - Add additional translations to backups page strings ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/543))
<br><br> <br><br>
## --- [4.0.20] - 2023/01/29 ## --- [4.0.20] - 2023/01/29

View File

@ -40,6 +40,7 @@ class Servers(BaseModel):
show_status = BooleanField(default=1) show_status = BooleanField(default=1)
created_by = IntegerField(default=-100) created_by = IntegerField(default=-100)
shutdown_timeout = IntegerField(default=60) shutdown_timeout = IntegerField(default=60)
ignored_exits = CharField(default="0")
class Meta: class Meta:
table_name = "servers" table_name = "servers"

View File

@ -13,7 +13,6 @@ logger = logging.getLogger(__name__)
class Authentication: class Authentication:
def __init__(self, helper): def __init__(self, helper):
self.helper = helper self.helper = helper
self.secret = "my secret"
try: try:
self.secret = ManagementController.get_crafty_api_key() self.secret = ManagementController.get_crafty_api_key()
if self.secret == "": if self.secret == "":

View File

@ -1013,7 +1013,7 @@ class Helpers:
for item in file_list: for item in file_list:
if os.path.isdir(os.path.join(folder, item)): if os.path.isdir(os.path.join(folder, item)):
dir_list.append(item) dir_list.append(item)
elif str(item) != "crafty.sqlite": elif str(item) != self.ignored_names:
unsorted_files.append(item) unsorted_files.append(item)
file_list = sorted(dir_list, key=str.casefold) + sorted( file_list = sorted(dir_list, key=str.casefold) + sorted(
unsorted_files, key=str.casefold unsorted_files, key=str.casefold
@ -1054,7 +1054,7 @@ class Helpers:
for item in file_list: for item in file_list:
if os.path.isdir(os.path.join(folder, item)): if os.path.isdir(os.path.join(folder, item)):
dir_list.append(item) dir_list.append(item)
elif str(item) != "crafty.sqlite": elif str(item) != self.ignored_names:
unsorted_files.append(item) unsorted_files.append(item)
file_list = sorted(dir_list, key=str.casefold) + sorted( file_list = sorted(dir_list, key=str.casefold) + sorted(
unsorted_files, key=str.casefold unsorted_files, key=str.casefold

View File

@ -12,6 +12,8 @@ import html
import urllib.request import urllib.request
import glob import glob
from zoneinfo import ZoneInfo
# TZLocal is set as a hidden import on win pipeline # TZLocal is set as a hidden import on win pipeline
from tzlocal import get_localzone from tzlocal import get_localzone
from tzlocal.utils import ZoneInfoNotFoundError from tzlocal.utils import ZoneInfoNotFoundError
@ -136,7 +138,7 @@ class ServerInstance:
logger.error( logger.error(
"Could not capture time zone from system. Falling back to Europe/London" "Could not capture time zone from system. Falling back to Europe/London"
) )
self.tz = "Europe/London" self.tz = ZoneInfo("Europe/London")
self.server_scheduler = BackgroundScheduler(timezone=str(self.tz)) self.server_scheduler = BackgroundScheduler(timezone=str(self.tz))
self.server_scheduler.start() self.server_scheduler.start()
self.backup_thread = threading.Thread( self.backup_thread = threading.Thread(
@ -730,26 +732,26 @@ class ServerInstance:
self.server_thread.join() self.server_thread.join()
def stop_server(self): def stop_server(self):
if self.settings["stop_command"]:
self.send_command(self.settings["stop_command"])
if self.settings["crash_detection"]:
# remove crash detection watcher
logger.info(f"Removing crash watcher for server {self.name}")
try:
self.server_scheduler.remove_job("c_" + str(self.server_id))
except:
logger.error(
f"Removing crash watcher for server {self.name} failed. "
f"Assuming it was never started."
)
else:
# windows will need to be handled separately for Ctrl+C
self.process.terminate()
running = self.check_running() running = self.check_running()
if not running: if not running:
logger.info(f"Can't stop server {self.name} if it's not running") logger.info(f"Can't stop server {self.name} if it's not running")
Console.info(f"Can't stop server {self.name} if it's not running") Console.info(f"Can't stop server {self.name} if it's not running")
return return
if self.settings["crash_detection"]:
# remove crash detection watcher
logger.info(f"Removing crash watcher for server {self.name}")
try:
self.server_scheduler.remove_job("c_" + str(self.server_id))
except:
logger.error(
f"Removing crash watcher for server {self.name} failed. "
f"Assuming it was never started."
)
if self.settings["stop_command"]:
self.send_command(self.settings["stop_command"])
else:
# windows will need to be handled separately for Ctrl+C
self.process.terminate()
i = 0 i = 0
# caching the name and pid number # caching the name and pid number
@ -919,7 +921,7 @@ class ServerInstance:
if running: if running:
return return
# check the exit code -- This could be a fix for /stop # check the exit code -- This could be a fix for /stop
if self.process.returncode == 0: if str(self.process.returncode) in self.settings["ignored_exits"].split(","):
logger.warning( logger.warning(
f"Process {self.process.pid} exited with code " f"Process {self.process.pid} exited with code "
f"{self.process.returncode}. This is considered a clean exit" f"{self.process.returncode}. This is considered a clean exit"

View File

@ -1585,6 +1585,8 @@ class PanelHandler(BaseHandler):
crash_detection = int(float(self.get_argument("crash_detection", "0"))) crash_detection = int(float(self.get_argument("crash_detection", "0")))
logs_delete_after = int(float(self.get_argument("logs_delete_after", "0"))) logs_delete_after = int(float(self.get_argument("logs_delete_after", "0")))
java_selection = self.get_argument("java_selection", None) java_selection = self.get_argument("java_selection", None)
# make sure there is no whitespace
ignored_exits = self.get_argument("ignored_exits", "").replace(" ", "")
# subpage = self.get_argument('subpage', None) # subpage = self.get_argument('subpage', None)
server_id = self.check_server_id() server_id = self.check_server_id()
@ -1669,6 +1671,7 @@ class PanelHandler(BaseHandler):
server_obj.auto_start = auto_start server_obj.auto_start = auto_start
server_obj.crash_detection = crash_detection server_obj.crash_detection = crash_detection
server_obj.logs_delete_after = logs_delete_after server_obj.logs_delete_after = logs_delete_after
server_obj.ignored_exits = ignored_exits
failed = False failed = False
for servers in self.controller.servers.failed_servers: for servers in self.controller.servers.failed_servers:
if servers["server_id"] == int(server_id): if servers["server_id"] == int(server_id):

View File

@ -183,6 +183,7 @@ class ServerHandler(BaseHandler):
"version_data": "version_data_here", # TODO "version_data": "version_data_here", # TODO
"user_data": exec_user, "user_data": exec_user,
"show_contribute": self.helper.get_setting("show_contribute_link", True), "show_contribute": self.helper.get_setting("show_contribute_link", True),
"background": self.controller.cached_login,
"lang": self.controller.users.get_user_lang_by_id(exec_user["user_id"]), "lang": self.controller.users.get_user_lang_by_id(exec_user["user_id"]),
"lang_page": Helpers.get_lang_page( "lang_page": Helpers.get_lang_page(
self.controller.users.get_user_lang_by_id(exec_user["user_id"]) self.controller.users.get_user_lang_by_id(exec_user["user_id"])

View File

@ -7,12 +7,12 @@ logger = logging.getLogger(__name__)
class StatusHandler(BaseHandler): class StatusHandler(BaseHandler):
def get(self): def get(self):
page_data = {"background": self.controller.cached_login} page_data = {
page_data["lang"] = self.helper.get_setting("language") "background": self.controller.cached_login,
page_data["lang_page"] = self.helper.get_lang_page( "lang": self.helper.get_setting("language"),
self.helper.get_setting("language") "lang_page": self.helper.get_lang_page(self.helper.get_setting("language")),
) "servers": self.controller.servers.get_all_servers_stats(),
page_data["servers"] = self.controller.servers.get_all_servers_stats() }
running = 0 running = 0
for srv in page_data["servers"]: for srv in page_data["servers"]:
if srv["stats"]["running"]: if srv["stats"]["running"]:

View File

@ -52,18 +52,19 @@ class UploadHandler(BaseHandler):
f"User with ID {user_id} attempted to upload a file that" f"User with ID {user_id} attempted to upload a file that"
f" exceeded the max body size." f" exceeded the max body size."
) )
self.helper.websocket_helper.broadcast_user(
user_id, return self.finish_json(
"send_start_error", 413,
{ {
"error": self.helper.translation.translate( "status": "error",
"error": "TOO LARGE",
"info": self.helper.translation.translate(
"error", "error",
"fileTooLarge", "fileTooLarge",
self.controller.users.get_user_lang_by_id(user_id), self.controller.users.get_user_lang_by_id(user_id),
), ),
}, },
) )
return
self.do_upload = True self.do_upload = True
if superuser: if superuser:
@ -141,48 +142,50 @@ class UploadHandler(BaseHandler):
f"User with ID {user_id} attempted to upload a file that" f"User with ID {user_id} attempted to upload a file that"
f" exceeded the max body size." f" exceeded the max body size."
) )
self.helper.websocket_helper.broadcast_user(
user_id, return self.finish_json(
"send_start_error", 413,
{ {
"error": self.helper.translation.translate( "status": "error",
"error": "TOO LARGE",
"info": self.helper.translation.translate(
"error", "error",
"fileTooLarge", "fileTooLarge",
self.controller.users.get_user_lang_by_id(user_id), self.controller.users.get_user_lang_by_id(user_id),
), ),
}, },
) )
return
self.do_upload = True self.do_upload = True
if not superuser: if not superuser:
self.helper.websocket_helper.broadcast_user( return self.finish_json(
user_id, 401,
"send_start_error",
{ {
"error": self.helper.translation.translate( "status": "error",
"error": "UNAUTHORIZED ACCESS",
"info": self.helper.translation.translate(
"error", "error",
"superError", "superError",
self.controller.users.get_user_lang_by_id(user_id), self.controller.users.get_user_lang_by_id(user_id),
), ),
}, },
) )
return
if not self.request.headers.get("X-Content-Type", None).startswith( if not self.request.headers.get("X-Content-Type", None).startswith(
"image/" "image/"
): ):
self.helper.websocket_helper.broadcast_user(
user_id, return self.finish_json(
"send_start_error", 415,
{ {
"error": self.helper.translation.translate( "status": "error",
"error": "TYPE ERROR",
"info": self.helper.translation.translate(
"error", "error",
"fileError", "fileError",
self.controller.users.get_user_lang_by_id(user_id), self.controller.users.get_user_lang_by_id(user_id),
), ),
}, },
) )
return
if user_id is None: if user_id is None:
logger.warning("User ID not found in upload handler call") logger.warning("User ID not found in upload handler call")
Console.warning("User ID not found in upload handler call") Console.warning("User ID not found in upload handler call")
@ -219,18 +222,19 @@ class UploadHandler(BaseHandler):
f"User with ID {user_id} attempted to upload a file that" f"User with ID {user_id} attempted to upload a file that"
f" exceeded the max body size." f" exceeded the max body size."
) )
self.helper.websocket_helper.broadcast_user(
user_id, return self.finish_json(
"send_start_error", 413,
{ {
"error": self.helper.translation.translate( "status": "error",
"error": "TOO LARGE",
"info": self.helper.translation.translate(
"error", "error",
"fileTooLarge", "fileTooLarge",
self.controller.users.get_user_lang_by_id(user_id), self.controller.users.get_user_lang_by_id(user_id),
), ),
}, },
) )
return
self.do_upload = True self.do_upload = True
if superuser: if superuser:

View File

@ -81,7 +81,7 @@
<i class="menu-arrow"></i> <i class="menu-arrow"></i>
</a> </a>
<div class="collapse" id="page-layouts"> <div class="collapse" id="page-layouts">
<ul class="nav flex-column sub-menu"> <ul class="nav flex-column sub-menu" id="sidebar-servers">
{% if data['crafty_permissions']['Server_Creation'] in data['user_crafty_permissions'] %} {% if data['crafty_permissions']['Server_Creation'] in data['user_crafty_permissions'] %}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/server/step1"><i class="fas fa-plus-circle"></i> &nbsp; {{ translate('sidebar', <a class="nav-link" href="/server/step1"><i class="fas fa-plus-circle"></i> &nbsp; {{ translate('sidebar',
@ -138,4 +138,9 @@
</ul> </ul>
</nav> </nav>
<script>
$(document).ready(function () {
$('#sidebar-servers li:gt(10)').remove();
})
</script>
<!-- partial --> <!-- partial -->

View File

@ -6,7 +6,8 @@
{% block title %}Crafty Controller - {{ translate('panelConfig', 'pageTitle', data['lang']) }}{% end %} {% block title %}Crafty Controller - {{ translate('panelConfig', 'pageTitle', data['lang']) }}{% end %}
{% block content %} {% block content %}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-select/1.13.10/css/bootstrap-select.min.css"> <link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-select/1.13.10/css/bootstrap-select.min.css">
<div class="content-wrapper"> <div class="content-wrapper">
@ -50,62 +51,68 @@
{% for item in data['config-json'].items() %} {% for item in data['config-json'].items() %}
{% if item[0] == "reset_secrets_on_next_boot" %} {% if item[0] == "reset_secrets_on_next_boot" %}
<div class="form-group" style="background: rgba(243, 21, 6, 0.3); outline: 1px solid red; padding: 10px;"> <div class="form-group" style="background: rgba(243, 21, 6, 0.3); outline: 1px solid red; padding: 10px;">
{% else %} {% else %}
<div class="form-group"> <div class="form-group">
{% end %}
<label class="form" for="{{item[0]}}">{{item[0]}}
<small class="text-muted ml-1">
</small> </label><br />
{% if item[0] == 'language' %}
<select name="{{item[0]}}" class="form-control">
{% for lang in data['availables_languages'] %}
{% if lang == item[1] %}
<option selected>{{lang}}</option>
{% else %}
<option>{{lang}}</option>
{% end %} {% end %}
{% end %} <label class="form" for="{{item[0]}}">{{item[0]}}
</select> <small class="text-muted ml-1">
{% elif item[0] == 'disabled_language_files' %} </small> </label><br />
<div class="input-group"> {% if item[0] == 'language' %}
<button type="button" class="btn btn-outline-default custom-picker" onclick="$('option', $('#lang_select')).each(function(element) { <select name="{{item[0]}}" class="form-control">
$(this).removeAttr('selected').prop('selected', false); $('.selectpicker').selectpicker('refresh') {% for lang in data['availables_languages'] %}
});">Enable all Languages</button> {% if lang == item[1] %}
<select id="lang_select" class="form-control selectpicker show-tick" data-icon-base="fas" data-tick-icon="fa-check" multiple data-style="custom-picker">
{% for lang in data['all_languages'] %}
{% if lang in item[1] %}
<option selected>{{lang}}</option> <option selected>{{lang}}</option>
{% else %} {% else %}
<option>{{lang}}</option> <option>{{lang}}</option>
{% end %} {% end %}
{% end %} {% end %}
</select> </select>
<textarea id="disabled_lang" name="{{item[0]}}" class="form-control list hidden" rows="{{ len(data['all_languages']) }}" value="{{','.join(item[1])}}" hidden>{{','.join(item[1])}}</textarea> {% elif item[0] == 'disabled_language_files' %}
<div class="input-group">
<button type="button" class="btn btn-outline-default custom-picker" onclick="$('option', $('#lang_select')).each(function(element) {
$(this).removeAttr('selected').prop('selected', false); $('.selectpicker').selectpicker('refresh')
});">Enable all Languages</button>
<select id="lang_select" class="form-control selectpicker show-tick" data-icon-base="fas"
data-tick-icon="fa-check" multiple data-style="custom-picker">
{% for lang in data['all_languages'] %}
{% if lang in item[1] %}
<option selected>{{lang}}</option>
{% else %}
<option>{{lang}}</option>
{% end %}
{% end %}
</select>
<textarea id="disabled_lang" name="{{item[0]}}" class="form-control list hidden"
rows="{{ len(data['all_languages']) }}" value="{{','.join(item[1])}}"
hidden>{{','.join(item[1])}}</textarea>
</div>
{% elif isinstance(item[1], list) %}
<textarea value="{{','.join(item[1])}}" type="text" name="{{item[0]}}"
class="form-control list">{{','.join(item[1])}}</textarea>
{% elif isinstance(item[1], bool) %}
<div style="margin-left: 30px;">
{% if item[1] == True %}
<input type="radio" class="form-check-input" name="{{item[0]}}" id="True" value="True" checked>
 <label for="True">True</label><br>
<input type="radio" class="form-check-input" name="{{item[0]}}" id="False" value="False">
 <label for="False">False</label>
{% else %}
<input type="radio" class="form-check-input" name="{{item[0]}}" id="True" value="True">
 <label for="True">True</label><br>
<input type="radio" class="form-check-input" name="{{item[0]}}" id="False" value="False" checked>
 <label for="False">False</label>
{% end %}
</div>
{% elif isinstance(item[1], int) %}
<input type="number" class="form-control" name="{{item[0]}}" id="{{item[0]}}" value="{{ item[1] }}"
step="1" min="0" required>
{% else %}
<input type="text" class="form-control" name="{{item[0]}}" id="{{item[0]}}" value="{{ item[1] }}"
step="2" min="0" required>
{% end %}
</div> </div>
{% elif isinstance(item[1], list) %}
<textarea value="{{','.join(item[1])}}" type="text" name="{{item[0]}}" class="form-control list">{{','.join(item[1])}}</textarea>
{% elif isinstance(item[1], bool) %}
<div style="margin-left: 30px;">
{% if item[1] == True %}
<input type="radio" class="form-check-input" name="{{item[0]}}" id="True" value="True" checked>
 <label for="True">True</label><br>
<input type="radio" class="form-check-input" name="{{item[0]}}" id="False" value="False">
 <label for="False">False</label>
{% else %}
<input type="radio" class="form-check-input" name="{{item[0]}}" id="True" value="True">
 <label for="True">True</label><br>
<input type="radio" class="form-check-input" name="{{item[0]}}" id="False" value="False" checked>
 <label for="False">False</label>
{% end %} {% end %}
</div> <button class="btn btn-success" type="submit">Submit</button>&nbsp;<span id="submit-status"></span>
{% elif isinstance(item[1], int) %}
<input type="number" class="form-control" name="{{item[0]}}" id="{{item[0]}}" value="{{ item[1] }}" step="1" min="0" required>
{% else %}
<input type="text" class="form-control" name="{{item[0]}}" id="{{item[0]}}" value="{{ item[1] }}" step="2" min="0" required>
{% end %}
</div>
{% end %}
<button class="btn btn-success" type="submit">Submit</button>&nbsp;<span id="submit-status"></span>
</form> </form>
</div> </div>
</div> </div>
@ -276,44 +283,6 @@
}, },
}); });
}) })
var file;
function sendFile() {
file = $("#file")[0].files[0]
document.getElementById("upload_input").innerHTML = '<div class="progress"><div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%">&nbsp;<i class="fa-solid fa-spinner"></i></div></div>'
let xmlHttpRequest = new XMLHttpRequest();
let token = getCookie("_xsrf")
let fileName = file.name
let target = '/upload'
let mimeType = file.type
let size = file.size
let type = 'background'
xmlHttpRequest.open('POST', target, true);
xmlHttpRequest.setRequestHeader('X-Content-Type', mimeType);
xmlHttpRequest.setRequestHeader('X-XSRFToken', token);
xmlHttpRequest.setRequestHeader('X-Content-Length', size);
xmlHttpRequest.setRequestHeader('X-Content-Disposition', 'attachment; filename="' + fileName + '"');
xmlHttpRequest.setRequestHeader('X-Content-Upload-Type', type);
xmlHttpRequest.setRequestHeader('X-FileName', fileName);
xmlHttpRequest.addEventListener('load', (event) => {
if (event.target.responseText == 'success') {
console.log('Upload for file', file.name, 'was successful!')
document.getElementById("upload_input").innerHTML = '<div class="card-header header-sm d-flex justify-content-between align-items-center"><span id="file-uploaded" style="color: gray;">' + fileName + '</span> 🔒</div>';
setTimeout(function () {
window.location.reload();
}, 2000);
}
else {
alert('Upload failed with response: ' + event.target.responseText);
doUpload = false;
}
}, false);
xmlHttpRequest.addEventListener('error', (e) => {
console.error('Error while uploading file', file.name + '.', 'Event:', e)
}, false);
xmlHttpRequest.send(file);
}
</script> </script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-select/1.13.10/js/bootstrap-select.min.js"> <script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-select/1.13.10/js/bootstrap-select.min.js">
</script> </script>

View File

@ -56,11 +56,11 @@
<input type="hidden" value="import_zip" name="create_type"> <input type="hidden" value="import_zip" name="create_type">
<div class="col form-group"> <div class="col form-group">
<span id="upload_input"><input type="file" class="form-control-file" id="file" name="file" <span id="upload_input"><input type="file" class="form-control-file" id="file" name="file"
multiple="false" required></span> multiple="false" required></span>
</div> </div>
<div class="col form-group"> <div class="col form-group">
<button type="button" class="btn btn-info" id="upload-button" onclick="sendFile()" <button type="button" class="btn btn-info" id="upload-button" onclick="sendFile()"
disabled>UPLOAD</button> disabled>UPLOAD</button>
</div> </div>
</form> </form>
<hr> <hr>
@ -74,7 +74,7 @@
<label for="photo" class="col-sm-6 col-form-label">Selected Background Image</label> <label for="photo" class="col-sm-6 col-form-label">Selected Background Image</label>
<div class="col-sm-6"> <div class="col-sm-6">
<select class="form-select form-control form-control-lg select-css form-control-plaintext" <select class="form-select form-control form-control-lg select-css form-control-plaintext"
id="photo" name="photo" form="photo_form" onchange="updateBackgroundPreview()"> id="photo" name="photo" form="photo_form" onchange="updateBackgroundPreview()">
{% for image in data["backgrounds"] %} {% for image in data["backgrounds"] %}
<option value="{{image}}">{{image}}</option> <option value="{{image}}">{{image}}</option>
{% end %} {% end %}
@ -84,8 +84,8 @@
<div id="photo_loading" class="form-group" hidden> <div id="photo_loading" class="form-group" hidden>
<div class="progress"> <div class="progress">
<div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" <div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar"
aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%">&nbsp;<i aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%">&nbsp;<i
class="fa-solid fa-spinner"></i></div> class="fa-solid fa-spinner"></i></div>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
@ -94,12 +94,12 @@
<label class="col-sm-1" id="opacityValue">{{ data['login_opacity'] }}%</label> <label class="col-sm-1" id="opacityValue">{{ data['login_opacity'] }}%</label>
<div class="range col-sm-8"> <div class="range col-sm-8">
<input type="range" class="form-control-range" id="modal_opacity" name="modal_opacity" <input type="range" class="form-control-range" id="modal_opacity" name="modal_opacity"
onchange="previewOpacity()" min="0" max="100" value="{{ data['login_opacity'] }}"> onchange="previewOpacity()" min="0" max="100" value="{{ data['login_opacity'] }}">
</div> </div>
</div> </div>
<div id="login_preview" style="position: relative;"> <div id="login_preview" style="position: relative;">
<img id="bg-preview" src="../../static/assets/images/auth/{{ data['background'] }}" <img id="bg-preview" src="../../static/assets/images/auth/{{ data['background'] }}"
class="img-fluid" alt="Responsive image"> class="img-fluid" alt="Responsive image">
<div id="login-form-preview"> <div id="login-form-preview">
<div id="login-form-background" class="auto-form-wrapper login-modal"> <div id="login-form-background" class="auto-form-wrapper login-modal">
<div class="text-center auto-form-logo"> <div class="text-center auto-form-logo">
@ -164,19 +164,19 @@
<div id="login_form_data"> <div id="login_form_data">
<input type="hidden" name="_xsrf" <input type="hidden" name="_xsrf"
value="2|1d603267|809fb6bd82f677d440e484dde7c3a310|1671726040" disabled> value="2|1d603267|809fb6bd82f677d440e484dde7c3a310|1671726040" disabled>
<div class="form-group"> <div class="form-group">
<label class="label">Username</label> <label class="label">Username</label>
<div class="input-group"> <div class="input-group">
<input type="text" class="form-control login-text-input login-input" <input type="text" class="form-control login-text-input login-input"
placeholder="Username" name="username" id="username" required="true" disabled> placeholder="Username" name="username" id="username" required="true" disabled>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="label">Password</label> <label class="label">Password</label>
<div class="input-group"> <div class="input-group">
<input type="password" class="form-control login-text-input login-input" <input type="password" class="form-control login-text-input login-input"
placeholder="Password" name="password" id="password" required="true" disabled> placeholder="Password" name="password" id="password" required="true" disabled>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
@ -196,7 +196,7 @@
</div> </div>
<div class="text-block text-center my-3"> <div class="text-block text-center my-3">
<span class="text-small font-weight-semibold"><a <span class="text-small font-weight-semibold"><a
href="https://craftycontrol.com/">Crafty Control href="https://craftycontrol.com/">Crafty Control
4.0.20</a> </span> 4.0.20</a> </span>
</div> </div>
</div> </div>
@ -352,7 +352,7 @@
var file; var file;
function sendFile() { function sendFile() {
file = $("#file")[0].files[0] file = $("#file")[0].files[0]
document.getElementById("upload_input").innerHTML = '<div class="progress"><div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%">&nbsp;<i class="fa-solid fa-spinner"></i></div></div>'; document.getElementById("upload_input").innerHTML = '<div class="progress"><div id="upload-progress-bar" class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%">&nbsp;<i class="fa-solid fa-spinner"></i></div></div>';
let xmlHttpRequest = new XMLHttpRequest(); let xmlHttpRequest = new XMLHttpRequest();
let token = getCookie("_xsrf") let token = getCookie("_xsrf")
let fileName = file.name let fileName = file.name
@ -361,6 +361,15 @@
let size = file.size let size = file.size
let type = 'background' let type = 'background'
xmlHttpRequest.upload.addEventListener('progress', function (e) {
if (e.loaded <= size) {
var percent = Math.round(e.loaded / size * 100);
$(`#upload-progress-bar`).css('width', percent + '%');
$(`#upload-progress-bar`).html(percent + '%');
}
});
xmlHttpRequest.open('POST', target, true); xmlHttpRequest.open('POST', target, true);
xmlHttpRequest.setRequestHeader('X-Content-Type', mimeType); xmlHttpRequest.setRequestHeader('X-Content-Type', mimeType);
xmlHttpRequest.setRequestHeader('X-XSRFToken', token); xmlHttpRequest.setRequestHeader('X-XSRFToken', token);
@ -377,7 +386,15 @@
}, 2000); }, 2000);
} }
else { else {
alert('Upload failed with response: ' + event.target.responseText); let response_text = JSON.parse(event.target.responseText);
var x = document.querySelector('.bootbox');
console.log(JSON.parse(event.target.responseText).info)
bootbox.alert({
message: JSON.parse(event.target.responseText).info,
callback: function () {
window.location.reload();
}
});
doUpload = false; doUpload = false;
} }
}, false); }, false);

View File

@ -110,15 +110,15 @@
<div class="form-group"> <div class="form-group">
<label for="command-check" class="form-check-label ml-4 mb-4"></label> <label for="command-check" class="form-check-label ml-4 mb-4"></label>
{% if data['backup_config']['before'] %} {% if data['backup_config']['before'] %}
<input type="checkbox" class="form-check-input" id="before-check" name="before-check" checked>Run <input type="checkbox" class="form-check-input" id="before-check" name="before-check" checked>{{
Command Before Backup translate('serverBackups', 'before', data['lang']) }}
<br> <br>
<input type="text" class="form-control" name="backup_before" id="backup_before" <input type="text" class="form-control" name="backup_before" id="backup_before"
value="{{ data['backup_config']['before'] }}" placeholder="We enter the / for you" value="{{ data['backup_config']['before'] }}" placeholder="We enter the / for you"
style="display: inline-block;"> style="display: inline-block;">
{% else %} {% else %}
<input type="checkbox" class="form-check-input" id="before-check" name="before-check">Run Command <input type="checkbox" class="form-check-input" id="before-check" name="before-check">{{
Before Backup translate('serverBackups', 'before', data['lang']) }}
<br> <br>
<input type="text" class="form-control" name="backup_before" id="backup_before" value="" <input type="text" class="form-control" name="backup_before" id="backup_before" value=""
placeholder="We enter the / for you." style="display: none;"> placeholder="We enter the / for you." style="display: none;">
@ -127,15 +127,15 @@
<div class="form-group"> <div class="form-group">
<label for="command-check" class="form-check-label ml-4 mb-4"></label> <label for="command-check" class="form-check-label ml-4 mb-4"></label>
{% if data['backup_config']['after'] %} {% if data['backup_config']['after'] %}
<input type="checkbox" class="form-check-input" id="after-check" name="after-check" checked>Run <input type="checkbox" class="form-check-input" id="after-check" name="after-check" checked>{{
Command After Backup translate('serverBackups', 'after', data['lang']) }}
<br> <br>
<input type="text" class="form-control" name="backup_after" id="backup_after" <input type="text" class="form-control" name="backup_after" id="backup_after"
value="{{ data['backup_config']['after'] }}" placeholder="We enter the / for you" value="{{ data['backup_config']['after'] }}" placeholder="We enter the / for you"
style="display: inline-block;"> style="display: inline-block;">
{% else %} {% else %}
<input type="checkbox" class="form-check-input" id="after-check" name="after-check">Run Command <input type="checkbox" class="form-check-input" id="after-check" name="after-check">{{
Before Backup translate('serverBackups', 'after', data['lang']) }}
<br> <br>
<input type="text" class="form-control" name="backup_after" id="backup_after" value="" <input type="text" class="form-control" name="backup_after" id="backup_after" value=""
placeholder="We enter the / for you." style="display: none;"> placeholder="We enter the / for you." style="display: none;">

View File

@ -184,6 +184,15 @@
value="{{ data['server_stats']['server_id']['shutdown_timeout'] }}" step="2" max="300" min="60" value="{{ data['server_stats']['server_id']['shutdown_timeout'] }}" step="2" max="300" min="60"
required> required>
</div> </div>
<div class="form-group">
<label for="ignored_exits">{{ translate('serverConfig', 'ignoredExits', data['lang']) }}
<small class="text-muted ml-1"> - {{ translate('serverConfig', 'ignoredExitsExplain',
data['lang'])
}}
</small> </label>
<input type="text" class="form-control" name="ignored_exits" id="ignored_exits"
value="{{ data['server_stats']['server_id']['ignored_exits'] }}">
</div>
<div class="form-group"> <div class="form-group">
<label for="logs_delete_after">{{ translate('serverConfig', 'removeOldLogsAfter', data['lang']) }} <label for="logs_delete_after">{{ translate('serverConfig', 'removeOldLogsAfter', data['lang']) }}

View File

@ -705,6 +705,15 @@
let mimeType = file.type let mimeType = file.type
let size = file.size let size = file.size
xmlHttpRequest.upload.addEventListener('progress', function (e) {
if (e.loaded <= size) {
var percent = Math.round(e.loaded / size * 100);
$(`#upload-progress-bar-${i + 1}`).css('width', percent + '%');
$(`#upload-progress-bar-${i + 1}`).html(percent + '%');
}
});
xmlHttpRequest.open('POST', target, true); xmlHttpRequest.open('POST', target, true);
xmlHttpRequest.setRequestHeader('X-Content-Type', mimeType); xmlHttpRequest.setRequestHeader('X-Content-Type', mimeType);
xmlHttpRequest.setRequestHeader('X-XSRFToken', token); xmlHttpRequest.setRequestHeader('X-XSRFToken', token);
@ -721,10 +730,19 @@
if (event.target.responseText == 'success') { if (event.target.responseText == 'success') {
console.log('Upload for file', file.name, 'was successful!'); console.log('Upload for file', file.name, 'was successful!');
let caught = false; let caught = false;
try {
if (document.getElementById(path).classList.contains("clicked")) {
var expanded = true;
}
} catch {
var expanded = false;
}
try { try {
var par_el = document.getElementById(path + "ul"); var par_el = document.getElementById(path + "ul");
var items = par_el.children; var items = par_el.children;
} catch { } catch (err) {
console.log(err)
caught = true; caught = true;
var par_el = document.getElementById("files-tree"); var par_el = document.getElementById("files-tree");
var items = par_el.children; var items = par_el.children;
@ -739,9 +757,9 @@
} }
} }
if (!flag) { if (!flag) {
if (caught) { if (caught && expanded == false) {
$(par_el).append('<li id=' + '"' + full_path.toString() + 'li' + '"' + 'class="d-block tree-ctx-item tree-file tree-item" data-path=' + '"' + full_path.toString() + '"' + ' data-name=' + '"' + name.toString() + '"' + ' onclick="clickOnFile(event)" ><span style="margin-right: 6px;"><i class="far fa-file"></i></span>' + name + '</li>'); $(par_el).append('<li id=' + '"' + full_path.toString() + 'li' + '"' + 'class="d-block tree-ctx-item tree-file tree-item" data-path=' + '"' + full_path.toString() + '"' + ' data-name=' + '"' + name.toString() + '"' + ' onclick="clickOnFile(event)" ><span style="margin-right: 6px;"><i class="far fa-file"></i></span>' + name + '</li>');
} else { } else if (expanded == true) {
$(par_el).append('<li id=' + '"' + full_path.toString() + 'li' + '"' + 'class="tree-ctx-item tree-file tree-item" data-path=' + '"' + full_path.toString() + '"' + ' data-name=' + '"' + name.toString() + '"' + ' onclick="clickOnFile(event)" ><span style="margin-right: 6px;"><i class="far fa-file"></i></span>' + name + '</li>'); $(par_el).append('<li id=' + '"' + full_path.toString() + 'li' + '"' + 'class="tree-ctx-item tree-file tree-item" data-path=' + '"' + full_path.toString() + '"' + ' data-name=' + '"' + name.toString() + '"' + ' onclick="clickOnFile(event)" ><span style="margin-right: 6px;"><i class="far fa-file"></i></span>' + name + '</li>');
} }
setTreeViewContext(); setTreeViewContext();
@ -751,7 +769,22 @@
$(`#upload-progress-bar-${i + 1}`).html('<i style="color: black;" class="fas fa-box-check"></i>') $(`#upload-progress-bar-${i + 1}`).html('<i style="color: black;" class="fas fa-box-check"></i>')
} }
else { else {
alert('Upload failed with response: ' + event.target.responseText); let response_text = JSON.parse(event.target.responseText);
var x = document.querySelector('.bootbox');
if (x) {
x.remove()
}
var x = document.querySelector('.modal-content');
if (x) {
x.remove()
}
console.log(JSON.parse(event.target.responseText).info)
bootbox.alert({
message: JSON.parse(event.target.responseText).info,
callback: function () {
window.location.reload();
}
});
doUpload = false; doUpload = false;
} }
}, false); }, false);
@ -788,7 +821,7 @@
var height = files.files.length * 50; var height = files.files.length * 50;
var waitMessage = '<p class="text-center mb-0">' + var waitMessage = '<p class="text-center mb-0">' +
'<i class="fa fa-spin fa-cog"></i>' + '<i class="fa fa-spin fa-cog"></i>&nbsp;' +
"{{ translate('serverFiles', 'waitUpload', data['lang']) }}" + '<br>' + "{{ translate('serverFiles', 'waitUpload', data['lang']) }}" + '<br>' +
'<strong>' + "{{ translate('serverFiles', 'stayHere', data['lang']) }}" + '</strong>' + '<strong>' + "{{ translate('serverFiles', 'stayHere', data['lang']) }}" + '</strong>' +
'</p>' + '</p>' +
@ -826,7 +859,7 @@
await sendFile(files.files[i], path, serverId, nFiles - i - 1, i, (progress) => { await sendFile(files.files[i], path, serverId, nFiles - i - 1, i, (progress) => {
$(`#upload-progress-bar-${i + 1}`).attr('aria-valuenow', progress) $(`#upload-progress-bar-${i + 1}`).attr('aria-valuenow', progress)
$(`#upload-progress-bar-${i + 1}`).css('width', progress + '%') $(`#upload-progress-bar-${i + 1}`).css('width', progress + '%');
}); });
} }
hideUploadBox(); hideUploadBox();

View File

@ -563,7 +563,7 @@
var file; var file;
function sendFile() { function sendFile() {
file = $("#file")[0].files[0] file = $("#file")[0].files[0]
document.getElementById("upload_input").innerHTML = '<div class="progress"><div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%">&nbsp;<i class="fa-solid fa-spinner"></i></div></div>' document.getElementById("upload_input").innerHTML = '<div class="progress"><div id="upload-progress-bar" class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%">&nbsp;<i class="fa-solid fa-spinner"></i></div></div>'
let xmlHttpRequest = new XMLHttpRequest(); let xmlHttpRequest = new XMLHttpRequest();
let token = getCookie("_xsrf") let token = getCookie("_xsrf")
let fileName = encodeURIComponent(file.name) let fileName = encodeURIComponent(file.name)
@ -572,6 +572,15 @@
let size = file.size let size = file.size
let type = 'server_import' let type = 'server_import'
xmlHttpRequest.upload.addEventListener('progress', function (e) {
if (e.loaded <= size) {
var percent = Math.round(e.loaded / size * 100);
$(`#upload-progress-bar`).css('width', percent + '%');
$(`#upload-progress-bar`).html(percent + '%');
}
});
xmlHttpRequest.open('POST', target, true); xmlHttpRequest.open('POST', target, true);
xmlHttpRequest.setRequestHeader('X-Content-Type', mimeType); xmlHttpRequest.setRequestHeader('X-Content-Type', mimeType);
xmlHttpRequest.setRequestHeader('X-XSRFToken', token); xmlHttpRequest.setRequestHeader('X-XSRFToken', token);
@ -586,7 +595,15 @@
document.getElementById("lower_half").style.visibility = "visible"; document.getElementById("lower_half").style.visibility = "visible";
} }
else { else {
alert('Upload failed with response: ' + event.target.responseText); let response_text = JSON.parse(event.target.responseText);
var x = document.querySelector('.bootbox');
console.log(JSON.parse(event.target.responseText).info)
bootbox.alert({
message: JSON.parse(event.target.responseText).info,
callback: function () {
window.location.reload();
}
});
doUpload = false; doUpload = false;
} }
}, false); }, false);

View File

@ -805,7 +805,7 @@
var file; var file;
function sendFile() { function sendFile() {
file = $("#file")[0].files[0] file = $("#file")[0].files[0]
document.getElementById("upload_input").innerHTML = '<div class="progress"><div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%">&nbsp;<i class="fa-solid fa-spinner"></i></div></div>' document.getElementById("upload_input").innerHTML = '<div class="progress"><div id="upload-progress-bar" class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%">&nbsp;<i class="fa-solid fa-spinner"></i></div></div>'
let xmlHttpRequest = new XMLHttpRequest(); let xmlHttpRequest = new XMLHttpRequest();
let token = getCookie("_xsrf") let token = getCookie("_xsrf")
let fileName = file.name let fileName = file.name
@ -814,6 +814,15 @@
let size = file.size let size = file.size
let type = 'server_import' let type = 'server_import'
xmlHttpRequest.upload.addEventListener('progress', function (e) {
if (e.loaded <= size) {
var percent = Math.round(e.loaded / size * 100);
$(`#upload-progress-bar`).css('width', percent + '%');
$(`#upload-progress-bar`).html(percent + '%');
}
});
xmlHttpRequest.open('POST', target, true); xmlHttpRequest.open('POST', target, true);
xmlHttpRequest.setRequestHeader('X-Content-Type', mimeType); xmlHttpRequest.setRequestHeader('X-Content-Type', mimeType);
xmlHttpRequest.setRequestHeader('X-XSRFToken', token); xmlHttpRequest.setRequestHeader('X-XSRFToken', token);
@ -828,7 +837,15 @@
document.getElementById("lower_half").style.visibility = "visible"; document.getElementById("lower_half").style.visibility = "visible";
} }
else { else {
alert('Upload failed with response: ' + event.target.responseText); let response_text = JSON.parse(event.target.responseText);
var x = document.querySelector('.bootbox');
console.log(JSON.parse(event.target.responseText).info)
bootbox.alert({
message: JSON.parse(event.target.responseText).info,
callback: function () {
window.location.reload();
}
});
doUpload = false; doUpload = false;
} }
}, false); }, false);

View File

@ -0,0 +1,16 @@
# Generated by database migrator
import peewee
def migrate(migrator, database, **kwargs):
migrator.add_columns("servers", ignored_exits=peewee.CharField(default="0"))
"""
Write your migrations here.
"""
def rollback(migrator, database, **kwargs):
migrator.drop_columns("servers", ["ignored_exits"])
"""
Write your rollback migrations here.
"""

View File

@ -299,7 +299,9 @@
"shutdown": "Shutdown server for duration of backup", "shutdown": "Shutdown server for duration of backup",
"size": "Size", "size": "Size",
"storageLocation": "Storage Location", "storageLocation": "Storage Location",
"storageLocationDesc": "Where do you want to store backups?" "storageLocationDesc": "Where do you want to store backups?",
"before": "Run command before backup",
"after": "Run command after backup"
}, },
"serverConfig": { "serverConfig": {
"bePatientDelete": "Please be patient while we remove your server from the Crafty panel. This screen will close in a few moments.", "bePatientDelete": "Please be patient while we remove your server from the Crafty panel. This screen will close in a few moments.",
@ -354,7 +356,9 @@
"timeoutExplain1": "How long Crafty will wait for your server to shutdown after executing the", "timeoutExplain1": "How long Crafty will wait for your server to shutdown after executing the",
"timeoutExplain2": "command before it forces the process down.", "timeoutExplain2": "command before it forces the process down.",
"statsHint1": "The port your server is running on should go here. This is just how Crafty opens a connection to your server for stats.", "statsHint1": "The port your server is running on should go here. This is just how Crafty opens a connection to your server for stats.",
"statsHint2": "This does not change the port of your server. You must still change the port in your server config file." "statsHint2": "This does not change the port of your server. You must still change the port in your server config file.",
"ignoredExits": "Ignored Crash Exit Codes",
"ignoredExitsExplain": "Exit codes Crafty's Crash detection should ignore as a normal 'stop' (separated by commas)"
}, },
"serverConfigHelp": { "serverConfigHelp": {
"desc": "Here is where you can change the configuration of your server", "desc": "Here is where you can change the configuration of your server",