mirror of
https://gitlab.com/crafty-controller/crafty-4.git
synced 2025-01-19 09:45:28 +01:00
Merge branch 'dev' into 'master'
4.0.3 See merge request crafty-controller/crafty-4!368
This commit is contained in:
commit
8514d13320
@ -21,7 +21,7 @@ win-dev-build:
|
||||
- pyinstaller -F main.py
|
||||
--distpath .
|
||||
--icon app\frontend\static\assets\images\Crafty_4-0_Logo_square.ico
|
||||
--name "crafty_commander"
|
||||
--name "crafty"
|
||||
--paths .venv\Lib\site-packages
|
||||
--hidden-import cryptography
|
||||
--hidden-import cffi
|
||||
@ -37,7 +37,7 @@ win-dev-build:
|
||||
name: "crafty-${CI_RUNNER_TAGS}-${CI_COMMIT_BRANCH}_${CI_COMMIT_SHORT_SHA}"
|
||||
paths:
|
||||
- app\
|
||||
- .\crafty_commander.exe
|
||||
- .\crafty.exe
|
||||
exclude:
|
||||
- app\classes\**\*
|
||||
|
||||
@ -49,7 +49,6 @@ win-prod-build:
|
||||
paths:
|
||||
- .venv/
|
||||
rules:
|
||||
- if: "$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH"
|
||||
- if: $CI_COMMIT_TAG
|
||||
environment:
|
||||
name: production
|
||||
@ -63,7 +62,7 @@ win-prod-build:
|
||||
- pyinstaller -F main.py
|
||||
--distpath .
|
||||
--icon app\frontend\static\assets\images\Crafty_4-0_Logo_square.ico
|
||||
--name "crafty_commander"
|
||||
--name "crafty"
|
||||
--paths .venv\Lib\site-packages
|
||||
--hidden-import cryptography
|
||||
--hidden-import cffi
|
||||
@ -81,7 +80,7 @@ win-prod-build:
|
||||
name: "crafty-${CI_RUNNER_TAGS}-${CI_COMMIT_BRANCH}_${CI_COMMIT_SHORT_SHA}"
|
||||
paths:
|
||||
- app\
|
||||
- .\crafty_commander.exe
|
||||
- .\crafty.exe
|
||||
expire_in: never
|
||||
exclude:
|
||||
- app\classes\**\*
|
||||
|
@ -232,6 +232,8 @@ function-naming-style=snake_case
|
||||
good-names=e,
|
||||
ex,
|
||||
f,
|
||||
fd,
|
||||
fn,
|
||||
i,
|
||||
id,
|
||||
ip,
|
||||
|
13
CHANGELOG.md
13
CHANGELOG.md
@ -1,9 +1,20 @@
|
||||
# Changelog
|
||||
|
||||
## [4.0.3] - 2022/06/18
|
||||
|
||||
### New features
|
||||
- Integrate Wiki iframe into panel instead of link ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/367))
|
||||
|
||||
### Bug fixes
|
||||
- Amend Java system variable fix to be more specfic since they only affect Oracle. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/364))
|
||||
- API Token authentication hardening ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/364))
|
||||
### Tweaks
|
||||
- Add better error logging for statistic collection ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/359))
|
||||
|
||||
## [4.0.2-hotfix1] - 2022/06/17
|
||||
|
||||
### Crit Bug fixes
|
||||
Fix blank server_detail page for general users ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/358))
|
||||
- Fix blank server_detail page for general users ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/358))
|
||||
|
||||
## [4.0.2] - 2022/06/16
|
||||
|
||||
|
@ -2,11 +2,11 @@
|
||||
|
||||
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
|
||||
[![Supported Python Versions](https://shields.io/badge/python-3.8%20%7C%203.9%20%7C%203.10%20-blue)](https://www.python.org)
|
||||
[![Version(temp-hardcoded)](https://img.shields.io/badge/release-v4.0.2--beta-orange)](https://gitlab.com/crafty-controller/crafty-4/-/releases)
|
||||
[![Version(temp-hardcoded)](https://img.shields.io/badge/release-v4.0.3--beta-orange)](https://gitlab.com/crafty-controller/crafty-4/-/releases)
|
||||
[![Code Quality(temp-hardcoded)](https://img.shields.io/badge/code%20quality-10-brightgreen)](https://gitlab.com/crafty-controller/crafty-4)
|
||||
[![Build Status](https://gitlab.com/crafty-controller/crafty-4/badges/master/pipeline.svg)](https://gitlab.com/crafty-controller/crafty-4/-/commits/master)
|
||||
|
||||
# Crafty Controller 4.0.2-beta
|
||||
# Crafty Controller 4.0.3-beta
|
||||
> Python based Control Panel for your Minecraft Server
|
||||
|
||||
## What is Crafty Controller?
|
||||
@ -39,7 +39,7 @@ With `Crafty Controller 4.0` we have focused on building our DevOps Principles,
|
||||
> __**⚠ 🔻WARNING: [WSL/WSL2 | WINDOWS 11 | DOCKER DESKTOP]🔻**__ <br>
|
||||
BE ADVISED! Upstream is currently broken for Minecraft running on **Docker under WSL/WSL2, Windows 11 / DOCKER DESKTOP!** <br>
|
||||
On '**Stop**' or '**Restart**' of the MC Server, there is a 90% chance the World's Chunks will be shredded irreparably! <br>
|
||||
Please only run Docker on Linux, If you are using Windows we have a portable installs found here: [Latest-Stable](https://gitlab.com/crafty-controller/crafty-4/-/jobs/artifacts/master/download?job=win-prod-build), [Latest-Development](https://gitlab.com/crafty-controller/crafty-4/-/jobs/artifacts/dev/download?job=win-dev-build)
|
||||
Please only run Docker on Linux, If you are using Windows we have a portable installs found here: [Latest-Stable](https://gitlab.com/crafty-controller/crafty-4/-/releases), [Latest-Development](https://gitlab.com/crafty-controller/crafty-4/-/jobs/artifacts/dev/download?job=win-dev-build)
|
||||
|
||||
----
|
||||
|
||||
|
@ -63,7 +63,9 @@ class Stats:
|
||||
psutil.boot_time(), datetime.timezone.utc
|
||||
)
|
||||
except Exception as e:
|
||||
logger.debug(f"error while getting boot time due to {e}")
|
||||
logger.debug(
|
||||
"getting boot time failed due to the following error:", exc_info=e
|
||||
)
|
||||
# unix epoch with no timezone data
|
||||
return datetime.datetime.fromtimestamp(0, datetime.timezone.utc)
|
||||
|
||||
@ -72,7 +74,9 @@ class Stats:
|
||||
try:
|
||||
return psutil.cpu_percent(interval=0.5) / psutil.cpu_count()
|
||||
except Exception as e:
|
||||
logger.debug(f"error while getting cpu percentage due to {e}")
|
||||
logger.debug(
|
||||
"getting the cpu usage failed due to the following error:", exc_info=e
|
||||
)
|
||||
return -1
|
||||
|
||||
def __init__(self, helper, controller):
|
||||
@ -100,7 +104,9 @@ class Stats:
|
||||
"disk_data": Stats._try_all_disk_usage(),
|
||||
}
|
||||
except Exception as e:
|
||||
logger.debug(f"error while getting host stats due to {e}")
|
||||
logger.debug(
|
||||
"getting host stats failed due to the following error:", exc_info=e
|
||||
)
|
||||
node_stats: NodeStatsDict = {
|
||||
"boot_time": str(
|
||||
datetime.datetime.fromtimestamp(0, datetime.timezone.utc)
|
||||
@ -124,50 +130,50 @@ class Stats:
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _try_get_process_stats(process):
|
||||
try:
|
||||
return Stats._get_process_stats(process)
|
||||
except Exception as e:
|
||||
logger.debug(f"error while getting process stats due to {e}")
|
||||
return {"cpu_usage": -1, "memory_usage": -1, "mem_percentage": -1}
|
||||
def _try_get_process_stats(process, running):
|
||||
if running:
|
||||
try:
|
||||
return Stats._get_process_stats(process)
|
||||
except Exception as e:
|
||||
logger.debug(
|
||||
f"getting process stats for pid {process.pid} "
|
||||
"failed due to the following error:",
|
||||
exc_info=e,
|
||||
)
|
||||
return {"cpu_usage": -1, "memory_usage": -1, "mem_percentage": -1}
|
||||
else:
|
||||
return {"cpu_usage": 0, "memory_usage": 0, "mem_percentage": 0}
|
||||
|
||||
@staticmethod
|
||||
def _get_process_stats(process):
|
||||
if process is None:
|
||||
return {"cpu_usage": 0, "memory_usage": 0, "mem_percentage": 0}
|
||||
return {"cpu_usage": -1, "memory_usage": -1, "mem_percentage": -1}
|
||||
process_pid = process.pid
|
||||
try:
|
||||
p = psutil.Process(process_pid)
|
||||
dummy = p.cpu_percent()
|
||||
p = psutil.Process(process_pid)
|
||||
_dummy = p.cpu_percent()
|
||||
|
||||
# call it first so we can be more accurate per the docs
|
||||
# https://giamptest.readthedocs.io/en/latest/#psutil.Process.cpu_percent
|
||||
# call it first so we can be more accurate per the docs
|
||||
# https://giamptest.readthedocs.io/en/latest/#psutil.Process.cpu_percent
|
||||
|
||||
real_cpu = round(p.cpu_percent(interval=0.5) / psutil.cpu_count(), 2)
|
||||
real_cpu = round(p.cpu_percent(interval=0.5) / psutil.cpu_count(), 2)
|
||||
|
||||
# this is a faster way of getting data for a process
|
||||
with p.oneshot():
|
||||
process_stats = {
|
||||
"cpu_usage": real_cpu,
|
||||
"memory_usage": Helpers.human_readable_file_size(
|
||||
p.memory_info()[0]
|
||||
),
|
||||
"mem_percentage": round(p.memory_percent(), 0),
|
||||
}
|
||||
return process_stats
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Unable to get process details for pid: {process_pid} Error: {e}"
|
||||
)
|
||||
return {"cpu_usage": 0, "memory_usage": 0, "mem_percentage": 0}
|
||||
# this is a faster way of getting data for a process
|
||||
with p.oneshot():
|
||||
process_stats = {
|
||||
"cpu_usage": real_cpu,
|
||||
"memory_usage": Helpers.human_readable_file_size(p.memory_info()[0]),
|
||||
"mem_percentage": round(p.memory_percent(), 0),
|
||||
}
|
||||
return process_stats
|
||||
|
||||
@staticmethod
|
||||
def _try_all_disk_usage():
|
||||
try:
|
||||
return Stats._all_disk_usage()
|
||||
except Exception as e:
|
||||
logger.debug(f"error while getting disk data due to {e}")
|
||||
logger.debug(
|
||||
"getting disk stats failed due to the following error:", exc_info=e
|
||||
)
|
||||
return []
|
||||
|
||||
# Source: https://github.com/giampaolo/psutil/blob/master/scripts/disk_usage.py
|
||||
@ -246,14 +252,19 @@ class Stats:
|
||||
online_stats = json.loads(ping_obj.players)
|
||||
|
||||
except Exception as e:
|
||||
logger.info(f"Unable to read json from ping_obj: {e}")
|
||||
logger.info(
|
||||
"Unable to read json from ping_obj due to the following error:",
|
||||
exc_info=e,
|
||||
)
|
||||
|
||||
try:
|
||||
server_icon = base64.encodebytes(ping_obj.icon)
|
||||
server_icon = server_icon.decode("utf-8")
|
||||
except Exception as e:
|
||||
server_icon = False
|
||||
logger.info(f"Unable to read the server icon : {e}")
|
||||
logger.info(
|
||||
"Unable to read the server icon due to the following error:", exc_info=e
|
||||
)
|
||||
|
||||
ping_data = {
|
||||
"online": online_stats.get("online", 0),
|
||||
@ -273,7 +284,9 @@ class Stats:
|
||||
server_icon = base64.encodebytes(ping_obj["icon"])
|
||||
except Exception as e:
|
||||
server_icon = False
|
||||
logger.info(f"Unable to read the server icon : {e}")
|
||||
logger.info(
|
||||
"Unable to read the server icon due to the following error:", exc_info=e
|
||||
)
|
||||
ping_data = {
|
||||
"online": ping_obj["server_player_count"],
|
||||
"max": ping_obj["server_player_max"],
|
||||
|
@ -239,18 +239,23 @@ class ServerInstance:
|
||||
"Detected nebulous java in start command. "
|
||||
"Replacing with full java path."
|
||||
)
|
||||
which_java_raw = self.helper.which_java()
|
||||
java_path = which_java_raw + "\\bin\\java"
|
||||
if str(which_java_raw) != str(self.helper.get_servers_root_dir) or str(
|
||||
self.helper.get_servers_root_dir
|
||||
) in str(which_java_raw):
|
||||
self.server_command[0] = java_path
|
||||
else:
|
||||
logger.critcal(
|
||||
"Possible attack detected. User attempted to exec "
|
||||
"java binary from server directory."
|
||||
# Checks for Oracle Java. Only Oracle Java's helper will cause a re-exec.
|
||||
if "/Oracle/Java/" in str(shutil.which("java")):
|
||||
logger.info(
|
||||
"Oracle Java detected. Changing start command to avoid re-exec."
|
||||
)
|
||||
return
|
||||
which_java_raw = self.helper.which_java()
|
||||
java_path = which_java_raw + "\\bin\\java"
|
||||
if str(which_java_raw) != str(self.helper.get_servers_root_dir) or str(
|
||||
self.helper.get_servers_root_dir
|
||||
) in str(which_java_raw):
|
||||
self.server_command[0] = java_path
|
||||
else:
|
||||
logger.critcal(
|
||||
"Possible attack detected. User attempted to exec "
|
||||
"java binary from server directory."
|
||||
)
|
||||
return
|
||||
self.server_path = Helpers.get_os_understandable_path(self.settings["path"])
|
||||
|
||||
# let's do some quick checking to make sure things actually exists
|
||||
@ -1250,7 +1255,7 @@ class ServerInstance:
|
||||
server_path = server["path"]
|
||||
|
||||
# process stats
|
||||
p_stats = Stats._try_get_process_stats(self.process)
|
||||
p_stats = Stats._try_get_process_stats(self.process, self.check_running())
|
||||
|
||||
# TODO: search server properties file for possible override of 127.0.0.1
|
||||
internal_ip = server["server_ip"]
|
||||
@ -1383,7 +1388,7 @@ class ServerInstance:
|
||||
server_path = server_dt["path"]
|
||||
|
||||
# process stats
|
||||
p_stats = Stats._try_get_process_stats(self.process)
|
||||
p_stats = Stats._try_get_process_stats(self.process, self.check_running())
|
||||
|
||||
# TODO: search server properties file for possible override of 127.0.0.1
|
||||
# internal_ip = server['server_ip']
|
||||
|
@ -1058,6 +1058,11 @@ class PanelHandler(BaseHandler):
|
||||
if user_id is None:
|
||||
self.redirect("/panel/error?error=Invalid User ID")
|
||||
return
|
||||
if int(user_id) != exec_user["user_id"] and not exec_user["superuser"]:
|
||||
self.redirect(
|
||||
"/panel/error?error=You are not authorized to view this page."
|
||||
)
|
||||
return
|
||||
|
||||
template = "panel/panel_edit_user_apikeys.html"
|
||||
|
||||
@ -1222,6 +1227,9 @@ class PanelHandler(BaseHandler):
|
||||
self.download_file(name, file)
|
||||
self.redirect(f"/panel/server_detail?id={server_id}&subpage=files")
|
||||
|
||||
elif page == "wiki":
|
||||
template = "panel/wiki.html"
|
||||
|
||||
elif page == "download_support_package":
|
||||
temp_zip_storage = exec_user["support_logs"]
|
||||
|
||||
@ -1893,6 +1901,13 @@ class PanelHandler(BaseHandler):
|
||||
self.redirect("/panel/error?error=Invalid User ID")
|
||||
return
|
||||
|
||||
if str(user_id) != str(exec_user["user_id"]) and not exec_user["superuser"]:
|
||||
self.redirect(
|
||||
"/panel/error?error=You do not have access to change"
|
||||
+ "this user's api key."
|
||||
)
|
||||
return
|
||||
|
||||
crafty_permissions_mask = self.get_perms()
|
||||
server_permissions_mask = self.get_perms_server()
|
||||
|
||||
@ -1926,6 +1941,12 @@ class PanelHandler(BaseHandler):
|
||||
self.redirect("/panel/error?error=Invalid Key ID")
|
||||
return
|
||||
|
||||
if key.user_id != exec_user["user_id"]:
|
||||
self.redirect(
|
||||
"/panel/error?error=You are not authorized to access this key."
|
||||
)
|
||||
return
|
||||
|
||||
self.controller.management.add_to_audit_log(
|
||||
exec_user["user_id"],
|
||||
f"Generated a new API token for the key {key.name} "
|
||||
@ -2142,6 +2163,15 @@ class PanelHandler(BaseHandler):
|
||||
self.redirect("/panel/error?error=Invalid Key ID")
|
||||
return
|
||||
|
||||
key_obj = self.controller.users.get_user_api_key(key_id)
|
||||
|
||||
if key_obj.user_id != exec_user["user_id"] and not exec_user["superuser"]:
|
||||
self.redirect(
|
||||
"/panel/error?error=You do not have access to change"
|
||||
+ "this user's api key."
|
||||
)
|
||||
return
|
||||
|
||||
self.controller.users.delete_user_api_key(key_id)
|
||||
|
||||
self.controller.management.add_to_audit_log(
|
||||
@ -2151,7 +2181,8 @@ class PanelHandler(BaseHandler):
|
||||
server_id=0,
|
||||
source_ip=self.get_remote_ip(),
|
||||
)
|
||||
self.redirect("/panel/panel_config")
|
||||
self.finish()
|
||||
self.redirect(f"/panel/edit_user_apikeys?id={key_obj.user_id}")
|
||||
else:
|
||||
self.set_status(404)
|
||||
self.render(
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"major": 4,
|
||||
"minor": 0,
|
||||
"sub": 2,
|
||||
"sub": 3,
|
||||
"meta": "beta"
|
||||
}
|
||||
|
@ -1,133 +1,144 @@
|
||||
<!-- partial -->
|
||||
<div class="container-fluid page-body-wrapper">
|
||||
<!-- partial:partials/_sidebar.html -->
|
||||
<style>
|
||||
@media screen and (max-width: 991px) {
|
||||
.sidebar-offcanvas {
|
||||
-webkit-transition: all 0.25s cubic-bezier(.22,.61,.36,1);
|
||||
transition: all 0.25s cubic-bezier(.22,.61,.36,1);
|
||||
box-shadow: 0px 8px 17px 2px rgba(0,0,0,0.14) , 0px 3px 14px 2px rgba(0,0,0,0.12) , 0px 5px 5px -3px rgba(0,0,0,0.2);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
function debounce(func, wait, immediate) {
|
||||
var timeout;
|
||||
return function() {
|
||||
var context = this, args = arguments;
|
||||
var later = function() {
|
||||
timeout = null;
|
||||
if (!immediate) func.apply(context, args);
|
||||
};
|
||||
var callNow = immediate && !timeout;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
if (callNow) func.apply(context, args);
|
||||
};
|
||||
};
|
||||
function isExtraLargeBreakpoint() {
|
||||
const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);
|
||||
return vw >= 1200;
|
||||
}
|
||||
function isLargeBreakpoint() {
|
||||
const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);
|
||||
return vw >= 992;
|
||||
}
|
||||
$(document).ready(function() {
|
||||
sidebarResizeHandler(null);
|
||||
$(window).on(
|
||||
'resize',
|
||||
debounce(sidebarResizeHandler, 25, true)
|
||||
);
|
||||
});
|
||||
function sidebarResizeHandler(e) {
|
||||
/*
|
||||
Viewport sizes: Extra large (vw >= 1200px), large (vw >= 992px), medium (vw >= 768px)
|
||||
- A localstorage item is set to remember a user's preference between collapsed or expanded.
|
||||
- For extra large viewports, the sidebar is the user's preference (by default expanded). When
|
||||
expanded or collapsed manually, it doesn't overlap the page content and the preference
|
||||
gets saved to a localstorage item.
|
||||
- For large viewports, the sidebar is collapsed. When expanded manually, it doesn't overlap
|
||||
the page content. The user's localstorage preference is not overridden during this state.
|
||||
- For medium and below viewports, the sidebar is hidden behing a hamburger icon. When expanded, the sidebar
|
||||
overlaps the page content. The user's localstorage preference is not overridden during this state.
|
||||
<div class="container-fluid page-body-wrapper">
|
||||
<!-- partial:partials/_sidebar.html -->
|
||||
<style>
|
||||
@media screen and (max-width: 991px) {
|
||||
.sidebar-offcanvas {
|
||||
-webkit-transition: all 0.25s cubic-bezier(.22, .61, .36, 1);
|
||||
transition: all 0.25s cubic-bezier(.22, .61, .36, 1);
|
||||
box-shadow: 0px 8px 17px 2px rgba(0, 0, 0, 0.14), 0px 3px 14px 2px rgba(0, 0, 0, 0.12), 0px 5px 5px -3px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
function debounce(func, wait, immediate) {
|
||||
var timeout;
|
||||
return function () {
|
||||
var context = this, args = arguments;
|
||||
var later = function () {
|
||||
timeout = null;
|
||||
if (!immediate) func.apply(context, args);
|
||||
};
|
||||
var callNow = immediate && !timeout;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
if (callNow) func.apply(context, args);
|
||||
};
|
||||
};
|
||||
function isExtraLargeBreakpoint() {
|
||||
const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);
|
||||
return vw >= 1200;
|
||||
}
|
||||
function isLargeBreakpoint() {
|
||||
const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);
|
||||
return vw >= 992;
|
||||
}
|
||||
$(document).ready(function () {
|
||||
sidebarResizeHandler(null);
|
||||
$(window).on(
|
||||
'resize',
|
||||
debounce(sidebarResizeHandler, 25, true)
|
||||
);
|
||||
});
|
||||
function sidebarResizeHandler(e) {
|
||||
/*
|
||||
Viewport sizes: Extra large (vw >= 1200px), large (vw >= 992px), medium (vw >= 768px)
|
||||
- A localstorage item is set to remember a user's preference between collapsed or expanded.
|
||||
- For extra large viewports, the sidebar is the user's preference (by default expanded). When
|
||||
expanded or collapsed manually, it doesn't overlap the page content and the preference
|
||||
gets saved to a localstorage item.
|
||||
- For large viewports, the sidebar is collapsed. When expanded manually, it doesn't overlap
|
||||
the page content. The user's localstorage preference is not overridden during this state.
|
||||
- For medium and below viewports, the sidebar is hidden behing a hamburger icon. When expanded, the sidebar
|
||||
overlaps the page content. The user's localstorage preference is not overridden during this state.
|
||||
|
||||
More code in `app/frontend/static/assets/js/shared/misc.js` and `app/frontend/templates/base.html`
|
||||
*/
|
||||
if (isExtraLargeBreakpoint()) {
|
||||
let value = localStorage.getItem('crafty-sidebar-expanded') !== 'false';
|
||||
$('body').toggleClass('sidebar-icon-only', !value);
|
||||
localStorage.setItem('crafty-sidebar-expanded', value);
|
||||
} else if (isLargeBreakpoint()) {
|
||||
$('body').toggleClass('sidebar-icon-only', true);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<nav class="sidebar sidebar-offcanvas" id="sidebar">
|
||||
<ul class="nav">
|
||||
More code in `app/frontend/static/assets/js/shared/misc.js` and `app/frontend/templates/base.html`
|
||||
*/
|
||||
if (isExtraLargeBreakpoint()) {
|
||||
let value = localStorage.getItem('crafty-sidebar-expanded') !== 'false';
|
||||
$('body').toggleClass('sidebar-icon-only', !value);
|
||||
localStorage.setItem('crafty-sidebar-expanded', value);
|
||||
} else if (isLargeBreakpoint()) {
|
||||
$('body').toggleClass('sidebar-icon-only', true);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<nav class="sidebar sidebar-offcanvas" id="sidebar">
|
||||
<ul class="nav">
|
||||
|
||||
<li class="nav-item nav-category" style="margin-top:10px;">{{ translate('sidebar', 'navigation', data['lang']) }}</li>
|
||||
<li class="nav-item nav-category" style="margin-top:10px;">{{ translate('sidebar', 'navigation', data['lang']) }}
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/panel/dashboard">
|
||||
<i class="fas fa-chart-network"></i>
|
||||
<span class="menu-title">{{ translate('sidebar', 'dashboard', data['lang']) }}</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="collapse" href="#page-layouts" aria-expanded="false"
|
||||
aria-controls="page-layouts">
|
||||
<i class="fas fa-server"></i>
|
||||
<span class="menu-title">{{ translate('sidebar', 'servers', data['lang']) }}</span>
|
||||
<i class="menu-arrow"></i>
|
||||
</a>
|
||||
<div class="collapse" id="page-layouts">
|
||||
<ul class="nav flex-column sub-menu">
|
||||
{% if data['crafty_permissions']['Server_Creation'] in data['user_crafty_permissions'] %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/panel/dashboard">
|
||||
<i class="fas fa-chart-network"></i>
|
||||
<span class="menu-title">{{ translate('sidebar', 'dashboard', data['lang']) }}</span>
|
||||
</a>
|
||||
<a class="nav-link" href="/server/step1"><i class="fas fa-plus-circle"></i> {{ translate('sidebar',
|
||||
'newServer', data['lang']) }}</a>
|
||||
</li>
|
||||
|
||||
{% end %}
|
||||
{% for s in data['menu_servers'] %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="collapse" href="#page-layouts" aria-expanded="false" aria-controls="page-layouts">
|
||||
<i class="fas fa-server"></i>
|
||||
<span class="menu-title">{{ translate('sidebar', 'servers', data['lang']) }}</span>
|
||||
<i class="menu-arrow"></i>
|
||||
</a>
|
||||
<div class="collapse" id="page-layouts">
|
||||
<ul class="nav flex-column sub-menu">
|
||||
{% if data['crafty_permissions']['Server_Creation'] in data['user_crafty_permissions'] %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/server/step1"><i class="fas fa-plus-circle"></i> {{ translate('sidebar', 'newServer', data['lang']) }}</a>
|
||||
</li>
|
||||
{% end %}
|
||||
{% for s in data['menu_servers'] %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/panel/server_detail?id={{s['server_id']}}"><i class="fas fa-server"></i> {{s['server_name']}}</a>
|
||||
</li>
|
||||
{% end %}
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="https://wiki.craftycontrol.com" target="_blank">
|
||||
<i class="fas fa-book"></i>
|
||||
<span class="menu-title">{{ translate('sidebar', 'documentation', data['lang']) }}</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="https://discord.gg/9VJPhCE" target="_blank">
|
||||
<i class="fab fa-discord"></i>
|
||||
<span class="menu-title">Discord</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/panel/credits">
|
||||
<i class="fas fa-heart"></i>
|
||||
<span class="menu-title">{{ translate('sidebar', 'credits', data['lang']) }}</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{% if data['show_contribute'] %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/panel/contribute">
|
||||
<i class="fas fa-donate"></i>
|
||||
<span class="menu-title">{{ translate('sidebar', 'contribute', data['lang']) }}</span>
|
||||
</a>
|
||||
<a class="nav-link" href="/panel/server_detail?id={{s['server_id']}}"><i class="fas fa-server"></i>
|
||||
{{s['server_name']}}</a>
|
||||
</li>
|
||||
{% end %}
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
<!-- partial -->
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="https://wiki.craftycontrol.com" target="_blank">
|
||||
<i class="fas fa-book"></i>
|
||||
<span class="menu-title">{{ translate('sidebar', 'documentation', data['lang']) }}</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/panel/wiki">
|
||||
<i class="fa fa-info-circle"></i>
|
||||
<span class="menu-title">Wiki</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="https://discord.gg/9VJPhCE" target="_blank">
|
||||
<i class="fab fa-discord"></i>
|
||||
<span class="menu-title">Discord</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/panel/credits">
|
||||
<i class="fas fa-heart"></i>
|
||||
<span class="menu-title">{{ translate('sidebar', 'credits', data['lang']) }}</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{% if data['show_contribute'] %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/panel/contribute">
|
||||
<i class="fas fa-donate"></i>
|
||||
<span class="menu-title">{{ translate('sidebar', 'contribute', data['lang']) }}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% end %}
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
<!-- partial -->
|
@ -4,7 +4,7 @@
|
||||
<!-- Required meta tags -->
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<title>Crafty Commander</title>
|
||||
<title>Crafty Controller</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">
|
||||
|
50
app/frontend/templates/panel/wiki.html
Normal file
50
app/frontend/templates/panel/wiki.html
Normal file
@ -0,0 +1,50 @@
|
||||
{% extends ../base.html %}
|
||||
|
||||
{% block meta %}
|
||||
{% end %}
|
||||
|
||||
{% block title %}Crafty Controller - Wiki{% end %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="content-wrapper">
|
||||
|
||||
<!-- Page Title Header Starts-->
|
||||
<div class="row page-title-header">
|
||||
<div class="col-12">
|
||||
<div class="page-header">
|
||||
<h4 class="page-title">Wiki</h4>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12 grid-margin">
|
||||
<iframe src="https://wiki.craftycontrol.com" width=100% height=2200px title="crafty's wiki"></iframe>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<!-- content-wrapper ends -->
|
||||
<style>
|
||||
.popover-body {
|
||||
color: white !important;
|
||||
;
|
||||
}
|
||||
|
||||
#desc_id {
|
||||
-ms-overflow-style: none;
|
||||
/* for Internet Explorer, Edge */
|
||||
scrollbar-width: none;
|
||||
/* for Firefox */
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
#desc_id::-webkit-scrollbar {
|
||||
display: none;
|
||||
/* for Chrome, Safari, and Opera */
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
{% end %}
|
@ -4,7 +4,7 @@
|
||||
<!-- Required meta tags -->
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<title>Crafty Commander</title>
|
||||
<title>Crafty Controller</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">
|
||||
|
@ -4,7 +4,7 @@
|
||||
<!-- Required meta tags -->
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<title>Crafty Commander</title>
|
||||
<title>Crafty Controller</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">
|
||||
|
@ -4,7 +4,7 @@
|
||||
<!-- Required meta tags -->
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<title>Crafty Commander</title>
|
||||
<title>Crafty Controller</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">
|
||||
|
Loading…
x
Reference in New Issue
Block a user