Merge branch 'dev' into feature/steamcmd

This commit is contained in:
Zedifus 2023-12-07 17:15:48 +00:00
commit 78d7934e29
123 changed files with 6622 additions and 2177 deletions

View File

@ -11,6 +11,7 @@ docker-compose.yml.example
.gitlab/
.gitignore
.gitlab-ci.yml
lang_sort_log.txt
# root
.editorconfig

2
.gitignore vendored
View File

@ -18,6 +18,7 @@ env.bak/
venv.bak/
.idea/
/import/
/imports/
/servers/
/app/frontend/static/assets/images/auth/custom/
@ -35,3 +36,4 @@ default.json
app/config/
docker/*
!docker/docker-compose.yml
lang_sort_log.txt

View File

@ -44,7 +44,7 @@ black:
# Code Climate/Quality Checking [https://pylint.pycqa.org/en/latest/]
pylint:
stage: lint
image: registry.gitlab.com/pipeline-components/pylint:latest
image: registry.gitlab.com/pipeline-components/pylint:0.21.1
tags:
- docker
rules:
@ -81,3 +81,26 @@ sonarcloud-check:
- .sonar/cache
script:
- sonar-scanner
# Lang file checking
lang-check:
stage: lint
image: alpine:latest
tags:
- docker
rules:
- if: "$CODE_QUALITY_DISABLED"
when: never
- if: "$CI_COMMIT_TAG || $CI_COMMIT_BRANCH"
allow_failure: true
before_script:
- apk add --no-cache jq bash
script:
- chmod +x .gitlab/scripts/lang_sort.sh
- bash .gitlab/scripts/lang_sort.sh ./app/translations/
after_script:
- if [ -f .gitlab/scripts/lang_sort_log.txt ]; then cat .gitlab/scripts/lang_sort_log.txt; fi
artifacts:
paths:
- .gitlab/scripts/lang_sort_log.txt
expire_in: 1 week

View File

@ -0,0 +1,95 @@
#!/bin/bash
# Ensure locale is set to C for predictable sorting
export LC_ALL=C
export LC_COLLATE=C
# Get the script's own path
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Directory containing the JSON files to sort
DIR="$1"
found_missing_keys=false
##### Log Setup #####
# Log file path
LOGFILE="${SCRIPT_DIR}/lang_sort_log.txt"
# Redirect stdout and stderr to the logfile
exec > "${LOGFILE}" 2>&1
#####################
##### Exit Gates #####
# Check if jq is installed
if ! command -v jq &> /dev/null
then
echo "jq could not be found, please install jq first."
exit
fi
# Check for directory argument
if [ "$#" -ne 1 ]; then
echo "Usage: $0 /path/to/translations"
exit
fi
# Check if en_EN.json exists in the directory
if [[ ! -f "${DIR}/en_EN.json" ]]; then
echo "The file en_EN.json does not exist in ${DIR}.Ensure you have the right directory, Exiting."
exit
fi
######################
# Sort keys of the en_EN.json file with 4-space indentation and overwrite it
jq -S --indent 4 '.' "${DIR}/en_EN.json" > "${DIR}/en_EN.json.tmp" && mv "${DIR}/en_EN.json.tmp" "${DIR}/en_EN.json"
# Function to recursively find all keys in a JSON object
function get_keys {
jq -r 'paths(scalars) | join("/")' "$1"
}
# Get keys and subkeys from en_EN.json
ref_keys=$(mktemp)
get_keys "${DIR}/en_EN.json" | sort > "${ref_keys}"
# Iterate over each .json file in the directory
for file in "${DIR}"/*.json; do
# Check if file is a regular file and not en_EN.json, and does not contain "_incomplete" in its name
if [[ -f "${file}" && "${file}" != "${DIR}/en_EN.json" && ! "${file}" =~ _incomplete ]]; then
# Get keys and subkeys from the current file
current_keys=$(mktemp)
get_keys "${file}" | sort > "${current_keys}"
# Display keys present in en_EN.json but not in the current file
missing_keys=$(comm -23 "${ref_keys}" "${current_keys}")
if [[ -n "${missing_keys}" ]]; then
found_missing_keys=true
echo -e "\nKeys/subkeys present in en_EN.json but missing in $(basename "${file}"): "
echo "${missing_keys}"
fi
# Sort keys of the JSON file and overwrite the original file
jq -S --indent 4 '.' "${file}" > "${file}.tmp" && mv "${file}.tmp" "${file}"
# Remove the temporary file
rm -f "${current_keys}"
fi
done
# Remove the temporary file
rm -f "${ref_keys}"
if ${found_missing_keys}; then
echo -e "\n\nSorting complete!"
echo "Comparison found missing keys, Please Review!"
echo "-------------------------------------------------------------------"
echo "If there are stale translations, you can exclude with '_incomplete'"
echo " e.g. lol_EN_incomplete.json"
echo "-------------------------------------------------------------------"
exit 1
else
echo -e "\n\nComparison and Sorting complete!"
fi

View File

@ -1,20 +1,94 @@
# Changelog
## --- [4.2.0] - 2023/TBD
## --- [4.2.2] - 2023/TBD
### New features
- Finish and Activate Arcadia notification backend ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/621))
- Loading Screen for Crafty during startup ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/668))
### Refactor
- Remove deprecated API V1 ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/670))
- Tidy up main.py to be more comprehensive ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/668))
- Force random password on first run. Stop using common default password ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/672) | [Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/673))
### Bug fixes
- Remove webhook `custom` option from webook provider list as it's not currently an option ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/664))
- Bump cryptography for CVE-2023-49083 ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/680))
- Fix bug where su cannot edit general user password ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/676))
- Fix bug where no file error on import root dir ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/677))
- Fix Unban button failing to pardon users ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/671))
- Fix stack in API error handling ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/674))
- Fix bug where you cannot select "do not monitor mounts" from `config.json` ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/678))
- Fix support log 'x' button still downloading logs ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/679))
- Fix bug where servers are created without bu dir ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/682))
### Tweaks
- Homogenize Panel logos/branding ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/666))
- Retain previous tab when revisiting server details page (#272)([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/667))
- Add server name tag in panel header (#272)([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/667))
- Setup logging for panel authentication attempts ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/669))
- Update minimum password length from 6 to 8, and unrestrict maximum password length ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/669))
- Give better feedback when backup delete fails ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/681))
### Lang
- pl_PL Minor fixes ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/675))
<br><br>
## --- [4.2.1] - 2023/11/01
### Bug fixes
- Fix logic issue with `get_files` API permissions check ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/654))
- Fix notifications not showing up/being reset #298 ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/660))
- Fix users not being able to be deleted since the prompt fails to display ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/661))
- Fix duplicate function naming on dashboard ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/662))
### Tweaks
- Auto refresh Crafty Announcements on 30m interval ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/653))
- Improve Crafty toggle buttons and Webhooks page ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/656))
### Lang
- Update `zh_CN` lang file ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/652))
- Update `es_ES` lang file ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/655))
- Clean up wording in `pl_PL` lang file ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/656))
- Add `de_DE`, `es_ES` `fr_FR`, `lol_EN`, `lv_LV`, `nl_BE` `pl_PL` & `zh_CN` translations for !656 ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/656))
### Docs
- [(New) Server Webhook Documentation](https://docs.craftycontrol.com/pages/user-guide/webhooks/)
- [(Edit) Image Context in Windows Service - Install steps, with slight wording improvement](https://docs.craftycontrol.com/pages/getting-started/installation/windows/#install-steps)
<br><br>
## --- [4.2.0] - 2023/10/18
### New features
- Finish and Activate Arcadia notification backend ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/621) | [Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/626) | [Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/632))
- Add initial Webhook Notification (Discord, Mattermost, Slack, Teams) ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/594))
- Implementation of OpenMetrics endpoints, for use with services such as Prometheus ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/624))
### Bug fixes
- PWA: Removed the custom offline page in favour of browser default ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/607))
- Fix hidden servers appearing visible on public mobile status page ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/612))
- Correctly handle if a server returns a string instead of json data on socket ping ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/614))
- Bump tornado to resolve #269 ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/623))
- Bump crypto to resolve #267 & #268 ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/622))
- Fix select installs failing to start, returning missing python package `packaging` ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/629))
- Fix public status page not updating #255 ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/615))
- Fix service worker vulrn and CQ raised by SonarQ ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/631))
- Fix Backup Restore/Schedules, Backup button function on `remote-comms2` ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/634))
- Add a wait to the call for the directory so we can make sure the wait dialogue has time to show up first ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/637))
- Fix bug where a reaction loop could be created, but would be cut short by an error when the loop occurred ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/636))
- Use controller on update user call ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/640))
- Move `imports` to `import/upload` in bind mount to better serve users on unraid with limited vdisk storage ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/642))
- Fix bug where everytime a page was loaded user settings would be reset #286 ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/643))
- Fix tooltip info icon on server config page ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/647))
- Fix quick disable toggle on schedules list ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/649))
### Refactor
- Consolidate remaining frontend functions into API V2, and remove ajax internal API ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/585))
- Replace bleach with nh3 ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/628))
- Add API route for historical server stats ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/615))
- Add API route for host stats ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/615))
### Tweaks
- Polish/Enhance display for InApp Documentation ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/613))
- Add get_users command to Crafty's console ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/620))
- Add `get_users` command to Crafty's console ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/620))
- Make files hover cursor pointer ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/627))
- Use `Jar` class naming for jar refresh to make room for steamCMD naming in the future ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/630))
- Improve ui visibility of Build Wizard selection tabs ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/633))
- Add additional logging for server bootstrap & moves unnecessary logging to `debug` for improved log clarity ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/635))
- Bump orjson to `3.9.7` for python `3.12` support ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/638))
- Bump all Crafty required python dependancies, maintaining minimum `3.9` support ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/639)) Revert peewee bump ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/651))
- Better optimize and refactor docker launcher sh ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/642))
- Improve pop-up notifications with Toasts ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/641))
- Move username and password settings to buttons on panel config ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/643))
- Remove external references from front end deps ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/648))
### Lang
TBD
- `fr_FR` Translation Updated to latest en_EN ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/646))
- `de_DE`, `fr_FR`, `lol_EN`, `lv_LV`, `nl_BE`, `pl_PL` Translations Updated to latest `en_EN` ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/645))
<br><br>
## --- [4.1.3] - 2023/07/18

View File

@ -1,5 +1,5 @@
[![Crafty Logo](app/frontend/static/assets/images/logo_long.svg)](https://craftycontrol.com)
# Crafty Controller 4.2.0
# Crafty Controller 4.2.2
> Python based Control Panel for your Minecraft Server
## What is Crafty Controller?

View File

@ -1,7 +1,9 @@
import logging
import queue
from app.classes.models.management import HelpersManagement
from prometheus_client import CollectorRegistry, Gauge
from app.classes.models.management import HelpersManagement, HelpersWebhooks
from app.classes.models.servers import HelperServers
logger = logging.getLogger(__name__)
@ -11,6 +13,8 @@ class ManagementController:
def __init__(self, management_helper):
self.management_helper = management_helper
self.command_queue = queue.Queue()
self.host_registry = CollectorRegistry()
self.init_host_registries()
# **********************************************************************************
# Config Methods
@ -54,6 +58,19 @@ class ManagementController:
def add_crafty_row():
HelpersManagement.create_crafty_row()
def init_host_registries(self):
# REGISTRY Entries for Server Stats functions
self.cpu_usage = Gauge(
name="CPU_Usage",
documentation="The CPU usage of the server",
registry=self.host_registry,
)
self.mem_usage_percent = Gauge(
name="Mem_Usage",
documentation="The Memory usage of the server",
registry=self.host_registry,
)
# **********************************************************************************
# Commands Methods
# **********************************************************************************
@ -206,3 +223,30 @@ class ManagementController:
@staticmethod
def set_master_server_dir(server_dir):
HelpersManagement.set_master_server_dir(server_dir)
# **********************************************************************************
# Webhooks Methods
# **********************************************************************************
@staticmethod
def create_webhook(data):
return HelpersWebhooks.create_webhook(data)
@staticmethod
def modify_webhook(webhook_id, data):
HelpersWebhooks.modify_webhook(webhook_id, data)
@staticmethod
def get_webhook_by_id(webhook_id):
return HelpersWebhooks.get_webhook_by_id(webhook_id)
@staticmethod
def get_webhooks_by_server(server_id, model=False):
return HelpersWebhooks.get_webhooks_by_server(server_id, model)
@staticmethod
def delete_webhook(webhook_id):
HelpersWebhooks.delete_webhook(webhook_id)
@staticmethod
def delete_webhook_by_server(server_id):
HelpersWebhooks.delete_webhooks_by_server(server_id)

View File

@ -22,6 +22,7 @@ from app.classes.models.server_permissions import (
PermissionsServers,
EnumPermissionsServer,
)
from app.classes.shared.websocket_manager import WebSocketManager
logger = logging.getLogger(__name__)
@ -36,6 +37,8 @@ class ServersController(metaclass=Singleton):
self.management_helper = management_helper
self.servers_list = []
self.stats = Stats(self.helper, self)
self.web_sock = WebSocketManager()
self.server_subpage = {}
# **********************************************************************************
# Generic Servers Methods
@ -107,9 +110,9 @@ class ServersController(metaclass=Singleton):
return ret
def get_history_stats(self, server_id, days):
def get_history_stats(self, server_id, hours):
srv = ServersController().get_server_instance_by_id(server_id)
return srv.stats_helper.get_history_stats(server_id, days)
return srv.stats_helper.get_history_stats(server_id, hours)
@staticmethod
def update_unloaded_server(server_obj):
@ -171,8 +174,15 @@ class ServersController(metaclass=Singleton):
def init_all_servers(self):
servers = self.get_all_defined_servers()
self.failed_servers = []
for server in servers:
self.web_sock.broadcast_to_admins(
"update",
{"section": "server", "server": server["server_name"]},
)
self.web_sock.broadcast_to_non_admins(
"update",
{"section": "init"},
)
server_id = server.get("server_id")
# if we have already initialized this server, let's skip it.

View File

@ -45,8 +45,7 @@ class UsersController:
},
"password": {
"type": "string",
"maxLength": 20,
"minLength": 6,
"minLength": 8,
"examples": ["crafty"],
"title": "Password",
},
@ -214,14 +213,14 @@ class UsersController:
limit_server_creation = 0
limit_user_creation = 0
limit_role_creation = 0
PermissionsCrafty.add_or_update_user(
user_id,
permissions_mask,
limit_server_creation,
limit_user_creation,
limit_role_creation,
)
if user_crafty_data:
PermissionsCrafty.add_or_update_user(
user_id,
permissions_mask,
limit_server_creation,
limit_user_creation,
limit_role_creation,
)
self.users_helper.delete_user_roles(user_id, removed_roles)

View File

@ -8,6 +8,7 @@ import requests
from app.classes.controllers.servers_controller import ServersController
from app.classes.models.server_permissions import PermissionsServers
from app.classes.shared.websocket_manager import WebSocketManager
logger = logging.getLogger(__name__)
@ -179,9 +180,7 @@ class ServerJars:
try:
ServersController.set_import(server_id)
for user in server_users:
self.helper.websocket_helper.broadcast_user(
user, "send_start_reload", {}
)
WebSocketManager().broadcast_user(user, "send_start_reload", {})
break
except Exception as ex:
@ -206,11 +205,9 @@ class ServerJars:
server_users = PermissionsServers.get_server_user_list(server_id)
for user in server_users:
self.helper.websocket_helper.broadcast_user(
WebSocketManager().broadcast_user(
user, "notification", "Executable download finished"
)
time.sleep(3)
self.helper.websocket_helper.broadcast_user(
user, "send_start_reload", {}
)
WebSocketManager().broadcast_user(user, "send_start_reload", {})
return success

View File

@ -226,7 +226,7 @@ class Stats:
def get_server_players(self, server_id):
server = HelperServers.get_server_data_by_id(server_id)
logger.info(f"Getting players for server {server}")
logger.debug(f"Getting players for server {server['server_name']}")
internal_ip = server["server_ip"]
server_port = server["server_port"]

View File

@ -17,6 +17,7 @@ from app.classes.models.users import HelperUsers
from app.classes.models.servers import Servers
from app.classes.models.server_permissions import PermissionsServers
from app.classes.shared.main_models import DatabaseShortcuts
from app.classes.shared.websocket_manager import WebSocketManager
logger = logging.getLogger(__name__)
@ -78,11 +79,15 @@ class HostStats(BaseModel):
# **********************************************************************************
class Webhooks(BaseModel):
id = AutoField()
name = CharField(max_length=64, unique=True, index=True)
method = CharField(default="POST")
url = CharField(unique=True)
event = CharField(default="")
send_data = BooleanField(default=True)
server_id = IntegerField(null=True)
name = CharField(default="Custom Webhook", max_length=64)
url = CharField(default="")
webhook_type = CharField(default="Custom")
bot_name = CharField(default="Crafty Controller")
trigger = CharField(default="server_start,server_stop")
body = CharField(default="")
color = CharField(default="#005cd1")
enabled = BooleanField(default=True)
class Meta:
table_name = "webhooks"
@ -158,9 +163,7 @@ class HelpersManagement:
server_users = PermissionsServers.get_server_user_list(server_id)
for user in server_users:
try:
self.helper.websocket_helper.broadcast_user(
user, "notification", audit_msg
)
WebSocketManager().broadcast_user(user, "notification", audit_msg)
except Exception as e:
logger.error(f"Error broadcasting to user {user} - {e}")
@ -502,3 +505,82 @@ class HelpersManagement:
f"Not removing {dir_to_del} from excluded directories - "
f"not in the excluded directory list for server ID {server_id}"
)
# **********************************************************************************
# Webhooks Class
# **********************************************************************************
class HelpersWebhooks:
def __init__(self, database):
self.database = database
@staticmethod
def create_webhook(create_data) -> int:
"""Create a webhook in the database
Args:
server_id: ID of a server this webhook will be married to
name: The name of the webhook
url: URL to the webhook
webhook_type: The provider this webhook will be sent to
bot name: The name that will appear when the webhook is sent
triggers: Server actions that will trigger this webhook
body: The message body of the webhook
enabled: Should Crafty trigger the webhook
Returns:
int: The new webhooks's id
Raises:
PeeweeException: If the webhook already exists
"""
return Webhooks.insert(
{
Webhooks.server_id: create_data["server_id"],
Webhooks.name: create_data["name"],
Webhooks.webhook_type: create_data["webhook_type"],
Webhooks.url: create_data["url"],
Webhooks.bot_name: create_data["bot_name"],
Webhooks.body: create_data["body"],
Webhooks.color: create_data["color"],
Webhooks.trigger: create_data["trigger"],
Webhooks.enabled: create_data["enabled"],
}
).execute()
@staticmethod
def modify_webhook(webhook_id, updata):
Webhooks.update(updata).where(Webhooks.id == webhook_id).execute()
@staticmethod
def get_webhook_by_id(webhook_id):
return model_to_dict(Webhooks.get(Webhooks.id == webhook_id))
@staticmethod
def get_webhooks_by_server(server_id, model):
if not model:
data = {}
for webhook in (
Webhooks.select().where(Webhooks.server_id == server_id).execute()
):
data[str(webhook.id)] = {
"webhook_type": webhook.webhook_type,
"name": webhook.name,
"url": webhook.url,
"bot_name": webhook.bot_name,
"trigger": webhook.trigger,
"body": webhook.body,
"color": webhook.color,
"enabled": webhook.enabled,
}
else:
data = Webhooks.select().where(Webhooks.server_id == server_id).execute()
return data
@staticmethod
def delete_webhook(webhook_id):
Webhooks.delete().where(Webhooks.id == webhook_id).execute()
@staticmethod
def delete_webhooks_by_server(server_id):
Webhooks.delete().where(Webhooks.server_id == server_id).execute()

View File

@ -8,6 +8,7 @@ from app.classes.shared.helpers import Helpers
from app.classes.shared.main_models import DatabaseShortcuts
from app.classes.shared.migration import MigrationManager
try:
from peewee import (
SqliteDatabase,
@ -50,6 +51,7 @@ class ServerStats(Model):
max = IntegerField(default=0)
players = CharField(default="")
desc = CharField(default="Unable to Connect")
icon = CharField(default="")
version = CharField(default="")
updating = BooleanField(default=False)
waiting_start = BooleanField(default=False)
@ -141,16 +143,20 @@ class HelperServerStats:
self.database.close()
return server_data
def get_history_stats(self, server_id, num_days):
def get_history_stats(self, server_id, num_hours):
self.database.connect(reuse_if_open=True)
max_age = datetime.datetime.now() - timedelta(days=num_days)
server_stats = (
max_age = datetime.datetime.now() - timedelta(hours=num_hours)
query_stats = (
ServerStats.select()
.where(ServerStats.created > max_age)
.where(ServerStats.server_id == server_id)
# .order_by(ServerStats.created.desc())
.execute(self.database)
)
self.database.connect(reuse_if_open=True)
server_stats = []
for stat in query_stats:
server_stats.append(DatabaseShortcuts.get_data_obj(stat))
self.database.close()
return server_stats
def insert_server_stats(self, server_stats):
@ -179,6 +185,7 @@ class HelperServerStats:
ServerStats.max: server_stats.get("max", False),
ServerStats.players: server_stats.get("players", False),
ServerStats.desc: server_stats.get("desc", False),
ServerStats.icon: server_stats.get("icon", None),
ServerStats.version: server_stats.get("version", False),
}
).execute(self.database)

View File

@ -11,6 +11,7 @@ from app.classes.shared.helpers import Helpers
from app.classes.shared.tasks import TasksManager
from app.classes.shared.migration import MigrationManager
from app.classes.shared.main_controller import Controller
from app.classes.shared.websocket_manager import WebSocketManager
logger = logging.getLogger(__name__)
@ -118,7 +119,7 @@ class MainPrompt(cmd.Cmd):
Console.info(
"Stopping all server daemons / threads - This may take a few seconds"
)
self.helper.websocket_helper.disconnect_all()
WebSocketManager().disconnect_all()
Console.info("Waiting for main thread to stop")
while True:
if self.tasks_manager.get_main_thread_run_status():

View File

@ -8,6 +8,7 @@ from zipfile import ZipFile, ZIP_DEFLATED
from app.classes.shared.helpers import Helpers
from app.classes.shared.console import Console
from app.classes.shared.websocket_manager import WebSocketManager
logger = logging.getLogger(__name__)
@ -149,7 +150,7 @@ class FileHelpers:
"percent": 0,
"total_files": self.helper.human_readable_file_size(dir_bytes),
}
self.helper.websocket_helper.broadcast_page_params(
WebSocketManager().broadcast_page_params(
"/panel/server_detail",
{"id": str(server_id)},
"backup_status",
@ -194,7 +195,7 @@ class FileHelpers:
"percent": percent,
"total_files": self.helper.human_readable_file_size(dir_bytes),
}
self.helper.websocket_helper.broadcast_page_params(
WebSocketManager().broadcast_page_params(
"/panel/server_detail",
{"id": str(server_id)},
"backup_status",
@ -215,7 +216,7 @@ class FileHelpers:
"percent": 0,
"total_files": self.helper.human_readable_file_size(dir_bytes),
}
self.helper.websocket_helper.broadcast_page_params(
WebSocketManager().broadcast_page_params(
"/panel/server_detail",
{"id": str(server_id)},
"backup_status",
@ -274,7 +275,7 @@ class FileHelpers:
"total_files": self.helper.human_readable_file_size(dir_bytes),
}
# send status results to page.
self.helper.websocket_helper.broadcast_page_params(
WebSocketManager().broadcast_page_params(
"/panel/server_detail",
{"id": str(server_id)},
"backup_status",

View File

@ -29,7 +29,6 @@ from app.classes.shared.null_writer import NullWriter
from app.classes.shared.console import Console
from app.classes.shared.installer import installer
from app.classes.shared.translation import Translation
from app.classes.web.websocket_helper import WebSocketHelper
with redirect_stderr(NullWriter()):
import psutil
@ -79,10 +78,10 @@ class Helpers:
self.passhasher = PasswordHasher()
self.exiting = False
self.websocket_helper = WebSocketHelper(self)
self.translation = Translation(self)
self.update_available = False
self.ignored_names = ["crafty_managed.txt", "db_stats"]
self.crafty_starting = False
@staticmethod
def auto_installer_fix(ex):
@ -364,6 +363,42 @@ class Helpers:
return result_of_check == 0
def create_pass(self):
# Maximum length of password needed
max_len = 64
# Declare string of the character that we need in our password
digits = string.digits
locase = string.ascii_lowercase
upcase = string.ascii_uppercase
symbols = "!@#$%^&*" # Reducing to avoid issues with ([]{}<>,'`) etc
# Combine all the character strings above to form one string
combo = digits + upcase + locase + symbols
# Randomly select at least one character from each character set above
rand_digit = secrets.choice(digits)
rand_upper = secrets.choice(upcase)
rand_lower = secrets.choice(locase)
rand_symbol = secrets.choice(symbols)
# Combine the character randomly selected above
temp_pass = rand_digit + rand_upper + rand_lower + rand_symbol
# Fill the rest of the password length by selecting randomly char list
for _ in range(max_len - 4):
temp_pass += secrets.choice(combo)
# Shuffle the temporary password to prevent predictable patterns
temp_pass_list = list(temp_pass)
secrets.SystemRandom().shuffle(temp_pass_list)
# Form the password by concatenating the characters
password = "".join(temp_pass_list)
# Return completed password
return password
@staticmethod
def cmdparse(cmd_in):
# Parse a string into arguments
@ -581,16 +616,19 @@ class Helpers:
return version_data
def get_announcements(self):
data = []
try:
data = []
response = requests.get("https://craftycontrol.com/notify", timeout=2)
data = json.loads(response.content)
if self.update_available:
data.append(self.update_available)
return data
except Exception as e:
logger.error(f"Failed to fetch notifications with error: {e}")
if self.update_available:
data.append(self.update_available)
return data
if self.update_available:
data = [self.update_available]
else:
return False
def get_version_string(self):
version_data = self.get_version()

View File

@ -9,9 +9,11 @@ from app.classes.controllers.server_perms_controller import PermissionsServers
from app.classes.controllers.servers_controller import ServersController
from app.classes.shared.helpers import Helpers
from app.classes.shared.file_helpers import FileHelpers
from app.classes.shared.websocket_manager import WebSocketManager
from app.classes.steamcmd.serverapps import SteamApps
from app.classes.steamcmd.steamcmd import SteamCMD
logger = logging.getLogger(__name__)
@ -68,7 +70,7 @@ class ImportHelpers:
ServersController.finish_import(new_id)
server_users = PermissionsServers.get_server_user_list(new_id)
for user in server_users:
self.helper.websocket_helper.broadcast_user(user, "send_start_reload", {})
WebSocketManager().broadcast_user(user, "send_start_reload", {})
def import_java_zip_server(self, temp_dir, new_server_dir, port, new_id):
import_thread = threading.Thread(
@ -112,7 +114,7 @@ class ImportHelpers:
server_users = PermissionsServers.get_server_user_list(new_id)
ServersController.finish_import(new_id)
for user in server_users:
self.helper.websocket_helper.broadcast_user(user, "send_start_reload", {})
WebSocketManager().broadcast_user(user, "send_start_reload", {})
# deletes temp dir
FileHelpers.del_dirs(temp_dir)
@ -166,7 +168,7 @@ class ImportHelpers:
ServersController.finish_import(new_id)
server_users = PermissionsServers.get_server_user_list(new_id)
for user in server_users:
self.helper.websocket_helper.broadcast_user(user, "send_start_reload", {})
WebSocketManager().broadcast_user(user, "send_start_reload", {})
def import_bedrock_zip_server(
self, temp_dir, new_server_dir, full_jar_path, port, new_id
@ -213,7 +215,7 @@ class ImportHelpers:
ServersController.finish_import(new_id)
server_users = PermissionsServers.get_server_user_list(new_id)
for user in server_users:
self.helper.websocket_helper.broadcast_user(user, "send_start_reload", {})
WebSocketManager().broadcast_user(user, "send_start_reload", {})
if os.name != "nt":
if Helpers.check_file_exists(full_jar_path):
os.chmod(full_jar_path, 0o2760)
@ -285,4 +287,4 @@ class ImportHelpers:
ServersController.finish_import(new_id)
server_users = PermissionsServers.get_server_user_list(new_id)
for user in server_users:
self.helper.websocket_helper.broadcast_user(user, "send_start_reload", {})
WebSocketManager().broadcast_user(user, "send_start_reload", {})

View File

@ -8,11 +8,11 @@ import time
import json
import logging
import threading
from zoneinfo import ZoneInfoNotFoundError
from peewee import DoesNotExist
# TZLocal is set as a hidden import on win pipeline
from tzlocal import get_localzone
from tzlocal.utils import ZoneInfoNotFoundError
from apscheduler.schedulers.background import BackgroundScheduler
from app.classes.models.server_permissions import EnumPermissionsServer
@ -33,8 +33,10 @@ from app.classes.shared.helpers import Helpers
from app.classes.shared.file_helpers import FileHelpers
from app.classes.shared.import_helper import ImportHelpers
from app.classes.minecraft.serverjars import ServerJars
from app.classes.shared.websocket_manager import WebSocketManager
from app.classes.steamcmd.serverapps import SteamApps
logger = logging.getLogger(__name__)
@ -79,6 +81,37 @@ class Controller:
self.first_login = False
self.cached_login = self.management.get_login_image()
self.support_scheduler.start()
try:
with open(
os.path.join(os.path.curdir, "logs", "auth_tracker.log"),
"r",
encoding="utf-8",
) as f:
self.auth_tracker = json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
self.auth_tracker = {}
def log_attempt(self, remote_ip, username):
remote = self.auth_tracker.get(str(remote_ip), None)
if remote:
remote["names"].append(username)
remote["attempts"] += 1
remote["times"].append(datetime.now().strftime("%d/%m/%Y %H:%M:%S"))
self.auth_tracker[str(remote_ip)] = remote
else:
self.auth_tracker[str(remote_ip)] = {
"names": [username],
"attempts": 1,
"times": [datetime.now().strftime("%d/%m/%Y %H:%M:%S")],
}
def write_auth_tracker(self):
with open(
os.path.join(os.path.curdir, "logs", "auth_tracker.log"),
"w",
encoding="utf-8",
) as f:
json.dump(self.auth_tracker, f, indent=4)
@staticmethod
def check_system_user():
@ -115,7 +148,7 @@ class Controller:
self.del_support_file(exec_user["support_logs"])
# pausing so on screen notifications can run for user
time.sleep(7)
self.helper.websocket_helper.broadcast_user(
WebSocketManager().broadcast_user(
exec_user["user_id"], "notification", "Preparing your support logs"
)
self.helper.ensure_dir_exists(
@ -211,17 +244,15 @@ class Controller:
) as f:
f.write(sys_info_string)
FileHelpers.make_compressed_archive(temp_zip_storage, temp_dir, sys_info_string)
if len(self.helper.websocket_helper.clients) > 0:
self.helper.websocket_helper.broadcast_user(
if len(WebSocketManager().clients) > 0:
WebSocketManager().broadcast_user(
exec_user["user_id"],
"support_status_update",
Helpers.calc_percent(temp_dir, temp_zip_storage + ".zip"),
)
temp_zip_storage += ".zip"
self.helper.websocket_helper.broadcast_user(
exec_user["user_id"], "send_logs_bootbox", {}
)
WebSocketManager().broadcast_user(exec_user["user_id"], "send_logs_bootbox", {})
self.users.set_support_path(exec_user["user_id"], temp_zip_storage)
@ -254,8 +285,8 @@ class Controller:
results = Helpers.calc_percent(source_path, dest_path)
self.log_stats = results
if len(self.helper.websocket_helper.clients) > 0:
self.helper.websocket_helper.broadcast_user(
if len(WebSocketManager().clients) > 0:
WebSocketManager().broadcast_user(
exec_user["user_id"], "support_status_update", results
)
@ -511,6 +542,10 @@ class Controller:
server_host=monitoring_host,
server_type=monitoring_type,
)
self.management.set_backup_config(
new_server_id,
backup_path,
)
if data["create_type"] == "minecraft_java":
if root_create_data["create_type"] == "download_jar":
# modded update urls from server jars will only update the installer
@ -616,6 +651,66 @@ class Controller:
return False
return True
def restore_java_zip_server(
self,
server_name: str,
zip_path: str,
server_jar: str,
min_mem: int,
max_mem: int,
port: int,
user_id: int,
):
server_id = Helpers.create_uuid()
new_server_dir = os.path.join(self.helper.servers_dir, server_id)
backup_path = os.path.join(self.helper.backup_path, server_id)
if Helpers.is_os_windows():
new_server_dir = Helpers.wtol_path(new_server_dir)
backup_path = Helpers.wtol_path(backup_path)
new_server_dir.replace(" ", "^ ")
backup_path.replace(" ", "^ ")
temp_dir = Helpers.get_os_understandable_path(zip_path)
Helpers.ensure_dir_exists(new_server_dir)
Helpers.ensure_dir_exists(backup_path)
full_jar_path = os.path.join(new_server_dir, server_jar)
if Helpers.is_os_windows():
server_command = (
f"java -Xms{Helpers.float_to_string(min_mem)}M "
f"-Xmx{Helpers.float_to_string(max_mem)}M "
f'-jar "{full_jar_path}" nogui'
)
else:
server_command = (
f"java -Xms{Helpers.float_to_string(min_mem)}M "
f"-Xmx{Helpers.float_to_string(max_mem)}M "
f"-jar {full_jar_path} nogui"
)
logger.debug("command: " + server_command)
server_log_file = "./logs/latest.log"
server_stop = "stop"
new_id = self.register_server(
server_name,
server_id,
new_server_dir,
backup_path,
server_command,
server_jar,
server_log_file,
server_stop,
port,
user_id,
server_type="minecraft-java",
)
ServersController.set_import(new_id)
self.import_helper.import_java_zip_server(
temp_dir, new_server_dir, port, new_id
)
return new_id
# **********************************************************************************
# BEDROCK IMPORTS
# **********************************************************************************
@ -713,7 +808,7 @@ class Controller:
self.import_helper.download_bedrock_server(new_server_dir, new_id)
return new_id
def import_bedrock_zip_server(
def restore_bedrock_zip_server(
self,
server_name: str,
zip_path: str,
@ -860,6 +955,7 @@ class Controller:
srv_obj = server["server_obj"]
srv_obj.server_scheduler.shutdown()
srv_obj.dir_scheduler.shutdown()
running = srv_obj.check_running()
if running:
@ -933,7 +1029,7 @@ class Controller:
def t_update_master_server_dir(self, new_server_path, user_id):
new_server_path = self.helper.wtol_path(new_server_path)
new_server_path = os.path.join(new_server_path, "servers")
self.helper.websocket_helper.broadcast_page(
WebSocketManager().broadcast_page(
"/panel/panel_config", "move_status", "Checking dir"
)
current_master = self.helper.wtol_path(
@ -943,7 +1039,7 @@ class Controller:
logger.info(
"Admin tried to change server dir to current server dir. Canceling..."
)
self.helper.websocket_helper.broadcast_page(
WebSocketManager().broadcast_page(
"/panel/panel_config",
"move_status",
"done",
@ -954,18 +1050,18 @@ class Controller:
"Admin tried to change server dir to be inside a sub directory of the"
" current server dir. This will result in a copy loop."
)
self.helper.websocket_helper.broadcast_page(
WebSocketManager().broadcast_page(
"/panel/panel_config",
"move_status",
"done",
)
return
self.helper.websocket_helper.broadcast_page(
WebSocketManager().broadcast_page(
"/panel/panel_config", "move_status", "Checking permissions"
)
if not self.helper.ensure_dir_exists(new_server_path):
self.helper.websocket_helper.broadcast_user(
WebSocketManager().broadcast_user(
user_id,
"send_start_error",
{
@ -989,7 +1085,7 @@ class Controller:
new_server_path, server.get("server_uuid")
)
if os.path.isdir(server_path):
self.helper.websocket_helper.broadcast_page(
WebSocketManager().broadcast_page(
"/panel/panel_config",
"move_status",
f"Moving {server.get('server_name')}",
@ -1030,7 +1126,7 @@ class Controller:
self.servers.update_unloaded_server(server_obj)
self.servers.init_all_servers()
self.helper.dir_migration = False
self.helper.websocket_helper.broadcast_page(
WebSocketManager().broadcast_page(
"/panel/panel_config",
"move_status",
"done",

View File

@ -14,13 +14,17 @@ class DatabaseBuilder:
self.management_helper = management_helper
self.users_helper = users_helper
def default_settings(self):
def default_settings(self, password="crafty"):
logger.info("Fresh Install Detected - Creating Default Settings")
Console.info("Fresh Install Detected - Creating Default Settings")
default_data = self.helper.find_default_password()
if password not in default_data:
Console.help(
"No default password found. Using password created "
"by Crafty. Find it in app/config/default-creds.txt"
)
username = default_data.get("username", "admin")
password = default_data.get("password", "crafty")
password = default_data.get("password", password)
self.users_helper.add_user(
username=username,

View File

@ -16,22 +16,27 @@ import json
from zoneinfo import ZoneInfo
# TZLocal is set as a hidden import on win pipeline
from zoneinfo import ZoneInfoNotFoundError
from tzlocal import get_localzone
from tzlocal.utils import ZoneInfoNotFoundError
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.jobstores.base import JobLookupError
from apscheduler.jobstores.base import JobLookupError, ConflictingIdError
# OpenMetrics/Prometheus Imports
from prometheus_client import CollectorRegistry, Gauge, Info
from app.classes.minecraft.stats import Stats
from app.classes.minecraft.ping import ping, ping_raknet
from app.classes.models.servers import HelperServers, Servers
from app.classes.models.server_stats import HelperServerStats
from app.classes.models.management import HelpersManagement
from app.classes.models.management import HelpersManagement, HelpersWebhooks
from app.classes.models.users import HelperUsers
from app.classes.models.server_permissions import PermissionsServers
from app.classes.shared.console import Console
from app.classes.shared.helpers import Helpers
from app.classes.shared.file_helpers import FileHelpers
from app.classes.shared.null_writer import NullWriter
from app.classes.shared.websocket_manager import WebSocketManager
from app.classes.web.webhooks.webhook_factory import WebhookFactory
with redirect_stderr(NullWriter()):
import psutil
@ -40,6 +45,45 @@ with redirect_stderr(NullWriter()):
logger = logging.getLogger(__name__)
def callback(called_func):
# Usage of @callback on method
# definition to run a webhook check
# on method completion
def wrapper(*args, **kwargs):
res = None
logger.debug("Checking for callbacks")
try:
res = called_func(*args, **kwargs)
finally:
events = WebhookFactory.get_monitored_events()
if called_func.__name__ in events:
server_webhooks = HelpersWebhooks.get_webhooks_by_server(
args[0].server_id, True
)
for swebhook in server_webhooks:
if called_func.__name__ in str(swebhook.trigger).split(","):
logger.info(
f"Found callback for event {called_func.__name__}"
f" for server {args[0].server_id}"
)
webhook = HelpersWebhooks.get_webhook_by_id(swebhook.id)
webhook_provider = WebhookFactory.create_provider(
webhook["webhook_type"]
)
if res is not False and swebhook.enabled:
webhook_provider.send(
bot_name=webhook["bot_name"],
server_name=args[0].name,
title=webhook["name"],
url=webhook["url"],
message=webhook["body"],
color=webhook["color"],
)
return res
return wrapper
class ServerOutBuf:
lines = {}
@ -92,12 +136,13 @@ class ServerOutBuf:
# TODO: Do not send data to clients who do not have permission to view
# this server's console
self.helper.websocket_helper.broadcast_page_params(
"/panel/server_detail",
{"id": self.server_id},
"vterm_new_line",
{"line": highlighted + "<br />"},
)
if len(WebSocketManager().clients) > 0:
WebSocketManager().broadcast_page_params(
"/panel/server_detail",
{"id": self.server_id},
"vterm_new_line",
{"line": highlighted + "<br />"},
)
# **********************************************************************************
@ -133,6 +178,8 @@ class ServerInstance:
self.server_object = HelperServers.get_server_obj(self.server_id)
self.stats_helper = HelperServerStats(self.server_id)
self.last_backup_failed = False
self.server_registry = CollectorRegistry()
try:
with open(
os.path.join(self.server_object.path, "db_stats", "players_cache.json"),
@ -152,6 +199,7 @@ class ServerInstance:
self.tz = ZoneInfo("Europe/London")
self.server_scheduler = BackgroundScheduler(timezone=str(self.tz))
self.dir_scheduler = BackgroundScheduler(timezone=str(self.tz))
self.init_registries()
self.server_scheduler.start()
self.dir_scheduler.start()
self.start_dir_calc_task()
@ -251,6 +299,23 @@ class ServerInstance:
seconds=5,
id="stats_" + str(self.server_id),
)
logger.info(f"Saving server statistics {self.name} every {30} seconds")
Console.info(f"Saving server statistics {self.name} every {30} seconds")
try:
self.server_scheduler.add_job(
self.record_server_stats,
"interval",
seconds=30,
id="save_stats_" + str(self.server_id),
)
except ConflictingIdError:
self.server_scheduler.remove_job("save_stats_" + str(self.server_id))
self.server_scheduler.add_job(
self.record_server_stats,
"interval",
seconds=30,
id="save_stats_" + str(self.server_id),
)
def setup_server_run_command(self):
# configure the server
@ -313,6 +378,7 @@ class ServerInstance:
logger.critical(f"Unable to write/access {self.server_path}")
Console.critical(f"Unable to write/access {self.server_path}")
@callback
def start_server(self, user_id, forge_install=False):
if not user_id:
user_lang = self.helper.get_setting("language")
@ -322,7 +388,7 @@ class ServerInstance:
# Checks if user is currently attempting to move global server
# dir
if self.helper.dir_migration:
self.helper.websocket_helper.broadcast_user(
WebSocketManager().broadcast_user(
user_id,
"send_start_error",
{
@ -337,7 +403,7 @@ class ServerInstance:
if self.stats_helper.get_import_status() and not forge_install:
if user_id:
self.helper.websocket_helper.broadcast_user(
WebSocketManager().broadcast_user(
user_id,
"send_start_error",
{
@ -383,7 +449,7 @@ class ServerInstance:
e_flag = True
if not e_flag and self.settings["type"] == "minecraft-java":
if user_id:
self.helper.websocket_helper.broadcast_user(
WebSocketManager().broadcast_user(
user_id, "send_eula_bootbox", {"id": self.server_id}
)
else:
@ -416,7 +482,7 @@ class ServerInstance:
except:
if user_id:
self.helper.websocket_helper.broadcast_user(
WebSocketManager().broadcast_user(
user_id,
"send_start_error",
{
@ -452,7 +518,7 @@ class ServerInstance:
f"Server {self.name} failed to start with error code: {ex}"
)
if user_id:
self.helper.websocket_helper.broadcast_user(
WebSocketManager().broadcast_user(
user_id,
"send_start_error",
{
@ -552,7 +618,7 @@ class ServerInstance:
# Checks for java on initial fail
if not self.helper.detect_java():
if user_id:
self.helper.websocket_helper.broadcast_user(
WebSocketManager().broadcast_user(
user_id,
"send_start_error",
{
@ -566,7 +632,7 @@ class ServerInstance:
f"Server {self.name} failed to start with error code: {ex}"
)
if user_id:
self.helper.websocket_helper.broadcast_user(
WebSocketManager().broadcast_user(
user_id,
"send_start_error",
{
@ -613,7 +679,7 @@ class ServerInstance:
self.stats_helper.set_first_run()
loc_server_port = self.stats_helper.get_server_stats()["server_port"]
# Sends port reminder message.
self.helper.websocket_helper.broadcast_user(
WebSocketManager().broadcast_user(
user_id,
"send_start_error",
{
@ -625,15 +691,11 @@ class ServerInstance:
server_users = PermissionsServers.get_server_user_list(self.server_id)
for user in server_users:
if user != user_id:
self.helper.websocket_helper.broadcast_user(
user, "send_start_reload", {}
)
WebSocketManager().broadcast_user(user, "send_start_reload", {})
else:
server_users = PermissionsServers.get_server_user_list(self.server_id)
for user in server_users:
self.helper.websocket_helper.broadcast_user(
user, "send_start_reload", {}
)
WebSocketManager().broadcast_user(user, "send_start_reload", {})
else:
logger.warning(
f"Server PID {self.process.pid} died right after starting "
@ -665,7 +727,7 @@ class ServerInstance:
def check_internet_thread(self, user_id, user_lang):
if user_id:
if not Helpers.check_internet():
self.helper.websocket_helper.broadcast_user(
WebSocketManager().broadcast_user(
user_id,
"send_start_error",
{
@ -792,9 +854,7 @@ class ServerInstance:
server_users = PermissionsServers.get_server_user_list(self.server_id)
for user in server_users:
self.helper.websocket_helper.broadcast_user(
user, "send_start_reload", {}
)
WebSocketManager().broadcast_user(user, "send_start_reload", {})
break
def stop_crash_detection(self):
@ -835,6 +895,7 @@ class ServerInstance:
if self.server_thread:
self.server_thread.join()
@callback
def stop_server(self):
running = self.check_running()
if not running:
@ -852,6 +913,7 @@ class ServerInstance:
f"Assuming it was never started."
)
if self.settings["stop_command"]:
logger.info(f"Stop command requested for {self.settings['server_name']}.")
self.send_command(self.settings["stop_command"])
self.write_player_cache()
else:
@ -907,7 +969,7 @@ class ServerInstance:
self.record_server_stats()
for user in server_users:
self.helper.websocket_helper.broadcast_user(user, "send_start_reload", {})
WebSocketManager().broadcast_user(user, "send_start_reload", {})
def restart_threaded_server(self, user_id):
bu_conf = HelpersManagement.get_backup_config(self.server_id)
@ -921,6 +983,9 @@ class ServerInstance:
if not self.check_running():
self.run_threaded_server(user_id)
else:
logger.info(
f"Restart command detected. Sending stop command to {self.server_id}."
)
self.stop_threaded_server()
time.sleep(2)
self.run_threaded_server(user_id)
@ -942,6 +1007,7 @@ class ServerInstance:
self.last_rc = poll
return False
@callback
def send_command(self, command):
if not self.check_running() and command.lower() != "start":
logger.warning(f'Server not running, unable to send command "{command}"')
@ -954,6 +1020,7 @@ class ServerInstance:
self.process.stdin.flush()
return True
@callback
def crash_detected(self, name):
# clear the old scheduled watcher task
self.server_scheduler.remove_job(f"c_{self.server_id}")
@ -974,6 +1041,7 @@ class ServerInstance:
f"The server {name} has crashed and will be restarted. "
f"Restarting server"
)
self.run_threaded_server(None)
return True
logger.critical(
@ -986,6 +1054,7 @@ class ServerInstance:
)
return False
@callback
def kill(self):
logger.info(f"Terminating server {self.server_id} and all child processes")
try:
@ -1074,6 +1143,7 @@ class ServerInstance:
f.write("eula=true")
self.run_threaded_server(user_id)
@callback
def backup_server(self):
if self.settings["backup_path"] == "":
logger.critical("Backup path is None. Canceling Backup!")
@ -1107,18 +1177,11 @@ class ServerInstance:
logger.info(f"Backup Thread started for server {self.settings['server_name']}.")
def a_backup_server(self):
if len(self.helper.websocket_helper.clients) > 0:
self.helper.websocket_helper.broadcast_page_params(
"/panel/server_detail",
{"id": str(self.server_id)},
"backup_reload",
{"percent": 0, "total_files": 0},
)
was_server_running = None
logger.info(f"Starting server {self.name} (ID {self.server_id}) backup")
server_users = PermissionsServers.get_server_user_list(self.server_id)
for user in server_users:
self.helper.websocket_helper.broadcast_user(
WebSocketManager().broadcast_user(
user,
"notification",
self.helper.translation.translate(
@ -1193,8 +1256,8 @@ class ServerInstance:
self.is_backingup = False
logger.info(f"Backup of server: {self.name} completed")
results = {"percent": 100, "total_files": 0, "current_file": 0}
if len(self.helper.websocket_helper.clients) > 0:
self.helper.websocket_helper.broadcast_page_params(
if len(WebSocketManager().clients) > 0:
WebSocketManager().broadcast_page_params(
"/panel/server_detail",
{"id": str(self.server_id)},
"backup_status",
@ -1202,7 +1265,7 @@ class ServerInstance:
)
server_users = PermissionsServers.get_server_user_list(self.server_id)
for user in server_users:
self.helper.websocket_helper.broadcast_user(
WebSocketManager().broadcast_user(
user,
"notification",
self.helper.translation.translate(
@ -1231,8 +1294,8 @@ class ServerInstance:
f"Failed to create backup of server {self.name} (ID {self.server_id})"
)
results = {"percent": 100, "total_files": 0, "current_file": 0}
if len(self.helper.websocket_helper.clients) > 0:
self.helper.websocket_helper.broadcast_page_params(
if len(WebSocketManager().clients) > 0:
WebSocketManager().broadcast_page_params(
"/panel/server_detail",
{"id": str(self.server_id)},
"backup_status",
@ -1249,8 +1312,8 @@ class ServerInstance:
def backup_status(self, source_path, dest_path):
results = Helpers.calc_percent(source_path, dest_path)
self.backup_stats = results
if len(self.helper.websocket_helper.clients) > 0:
self.helper.websocket_helper.broadcast_page_params(
if len(WebSocketManager().clients) > 0:
WebSocketManager().broadcast_page_params(
"/panel/server_detail",
{"id": str(self.server_id)},
"backup_status",
@ -1295,6 +1358,7 @@ class ServerInstance:
if f["path"].endswith(".zip")
]
@callback
def jar_update(self):
self.stats_helper.set_update(True)
update_thread = threading.Thread(
@ -1353,14 +1417,14 @@ class ServerInstance:
self.stop_threaded_server()
else:
was_started = False
if len(self.helper.websocket_helper.clients) > 0:
if len(WebSocketManager().clients) > 0:
# There are clients
self.check_update()
message = (
'<a data-id="' + str(self.server_id) + '" class=""> UPDATING...</i></a>'
)
for user in server_users:
self.helper.websocket_helper.broadcast_user_page(
WebSocketManager().broadcast_user_page(
"/panel/server_detail",
user,
"update_button_status",
@ -1413,7 +1477,7 @@ class ServerInstance:
# check if backup was successful
if self.last_backup_failed:
for user in server_users:
self.helper.websocket_helper.broadcast_user(
WebSocketManager().broadcast_user(
user,
"notification",
"Backup failed for " + self.name + ". canceling update.",
@ -1459,11 +1523,11 @@ class ServerInstance:
logger.info("Executable updated successfully. Starting Server")
self.stats_helper.set_update(False)
if len(self.helper.websocket_helper.clients) > 0:
if len(WebSocketManager().clients) > 0:
# There are clients
self.check_update()
for user in server_users:
self.helper.websocket_helper.broadcast_user(
WebSocketManager().broadcast_user(
user,
"notification",
"Executable update finished for " + self.name,
@ -1471,7 +1535,7 @@ class ServerInstance:
# sleep so first notif can completely run
time.sleep(3)
for user in server_users:
self.helper.websocket_helper.broadcast_user_page(
WebSocketManager().broadcast_user_page(
"/panel/server_detail",
user,
"update_button_status",
@ -1481,10 +1545,10 @@ class ServerInstance:
"wasRunning": was_started,
},
)
self.helper.websocket_helper.broadcast_user_page(
WebSocketManager().broadcast_user_page(
user, "/panel/dashboard", "send_start_reload", {}
)
self.helper.websocket_helper.broadcast_user(
WebSocketManager().broadcast_user(
user,
"notification",
"Executable update finished for " + self.name,
@ -1501,7 +1565,7 @@ class ServerInstance:
self.run_threaded_server(HelperUsers.get_user_id_by_name("system"))
else:
for user in server_users:
self.helper.websocket_helper.broadcast_user(
WebSocketManager().broadcast_user(
user,
"notification",
"Executable update failed for "
@ -1511,7 +1575,7 @@ class ServerInstance:
logger.error("Executable download failed.")
self.stats_helper.set_update(False)
for user in server_users:
self.helper.websocket_helper.broadcast_user(user, "remove_spinner", {})
WebSocketManager().broadcast_user(user, "remove_spinner", {})
def start_dir_calc_task(self):
server_dt = HelperServers.get_server_data_by_id(self.server_id)
@ -1540,7 +1604,7 @@ class ServerInstance:
def realtime_stats(self):
# only get stats if clients are connected.
# no point in burning cpu
if len(self.helper.websocket_helper.clients) > 0:
if len(WebSocketManager().clients) > 0:
total_players = 0
max_players = 0
servers_ping = []
@ -1571,50 +1635,43 @@ class ServerInstance:
"crashed": self.is_crashed,
}
)
if len(self.helper.websocket_helper.clients) > 0:
self.helper.websocket_helper.broadcast_page_params(
"/panel/server_detail",
{"id": str(self.server_id)},
"update_server_details",
{
"id": raw_ping_result.get("id"),
"started": raw_ping_result.get("started"),
"running": raw_ping_result.get("running"),
"cpu": raw_ping_result.get("cpu"),
"mem": raw_ping_result.get("mem"),
"mem_percent": raw_ping_result.get("mem_percent"),
"world_name": raw_ping_result.get("world_name"),
"world_size": raw_ping_result.get("world_size"),
"server_port": raw_ping_result.get("server_port"),
"int_ping_results": raw_ping_result.get("int_ping_results"),
"online": raw_ping_result.get("online"),
"max": raw_ping_result.get("max"),
"players": raw_ping_result.get("players"),
"desc": raw_ping_result.get("desc"),
"version": raw_ping_result.get("version"),
"icon": raw_ping_result.get("icon"),
"crashed": self.is_crashed,
"created": datetime.datetime.now().strftime(
"%Y/%m/%d, %H:%M:%S"
),
"players_cache": self.player_cache,
},
)
WebSocketManager().broadcast_page_params(
"/panel/server_detail",
{"id": str(self.server_id)},
"update_server_details",
{
"id": raw_ping_result.get("id"),
"started": raw_ping_result.get("started"),
"running": raw_ping_result.get("running"),
"cpu": raw_ping_result.get("cpu"),
"mem": raw_ping_result.get("mem"),
"mem_percent": raw_ping_result.get("mem_percent"),
"world_name": raw_ping_result.get("world_name"),
"world_size": raw_ping_result.get("world_size"),
"server_port": raw_ping_result.get("server_port"),
"int_ping_results": raw_ping_result.get("int_ping_results"),
"online": raw_ping_result.get("online"),
"max": raw_ping_result.get("max"),
"players": raw_ping_result.get("players"),
"desc": raw_ping_result.get("desc"),
"version": raw_ping_result.get("version"),
"icon": raw_ping_result.get("icon"),
"crashed": self.is_crashed,
"created": datetime.datetime.now().strftime("%Y/%m/%d, %H:%M:%S"),
"players_cache": self.player_cache,
},
)
total_players += int(raw_ping_result.get("online"))
max_players += int(raw_ping_result.get("max"))
self.record_server_stats()
# self.record_server_stats()
if (len(servers_ping) > 0) & (
len(self.helper.websocket_helper.clients) > 0
):
if len(servers_ping) > 0:
try:
self.helper.websocket_helper.broadcast_page(
WebSocketManager().broadcast_page(
"/panel/dashboard", "update_server_status", servers_ping
)
self.helper.websocket_helper.broadcast_page(
"/status", "update_server_status", servers_ping
)
except:
Console.critical("Can't broadcast server status to websocket")
@ -1633,7 +1690,6 @@ class ServerInstance:
# process stats
p_stats = Stats._try_get_process_stats(self.process, self.check_running())
internal_ip = server["server_ip"]
server_port = server["server_port"]
server_name = server.get("server_name", f"ID#{server_id}")
@ -1683,6 +1739,7 @@ class ServerInstance:
"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"),
}
else:
server_stats = {
@ -1701,6 +1758,7 @@ class ServerInstance:
"players": False,
"desc": False,
"version": False,
"icon": None,
}
return server_stats
@ -1708,7 +1766,7 @@ class ServerInstance:
def get_server_players(self):
server = HelperServers.get_server_data_by_id(self.server_id)
logger.info(f"Getting players for server {server}")
logger.debug(f"Getting players for server {server['server_name']}")
internal_ip = server["server_ip"]
server_port = server["server_port"]
@ -1749,7 +1807,6 @@ class ServerInstance:
}
server_stats = {}
server = HelperServers.get_server_obj(server_id)
if not server:
return {}
server_dt = HelperServers.get_server_data_by_id(server_id)
@ -1879,9 +1936,50 @@ class ServerInstance:
server_stats = self.get_servers_stats()
self.stats_helper.insert_server_stats(server_stats)
self.cpu_usage.labels(f"{self.server_id}").set(server_stats.get("cpu"))
self.mem_usage_percent.labels(f"{self.server_id}").set(
server_stats.get("mem_percent")
)
self.minecraft_version.labels(f"{self.server_id}").info(
{"version": f"{server_stats.get('version')}"}
)
self.online_players.labels(f"{self.server_id}").set(server_stats.get("online"))
# delete old data
max_age = self.helper.get_setting("history_max_age")
now = datetime.datetime.now()
minimum_to_exist = now - datetime.timedelta(days=max_age)
self.stats_helper.remove_old_stats(minimum_to_exist)
def init_registries(self):
# REGISTRY Entries for Server Stats functions
self.cpu_usage = Gauge(
name="CPU_Usage",
documentation="The CPU usage of the server",
labelnames=["server_id"],
registry=self.server_registry,
)
self.mem_usage_percent = Gauge(
name="Mem_Usage",
documentation="The Memory usage of the server",
labelnames=["server_id"],
registry=self.server_registry,
)
self.minecraft_version = Info(
name="Minecraft_Version",
documentation="The version of the minecraft of this server",
labelnames=["server_id"],
registry=self.server_registry,
)
self.online_players = Gauge(
name="online_players",
documentation="The number of players online for a server",
labelnames=["server_id"],
registry=self.server_registry,
)
def get_server_history(self):
history = self.stats_helper.get_history_stats(self.server_id, 1)
return history

View File

@ -5,10 +5,10 @@ import threading
import asyncio
import datetime
import json
from zoneinfo import ZoneInfoNotFoundError
from tzlocal import get_localzone
from tzlocal.utils import ZoneInfoNotFoundError
from apscheduler.events import EVENT_JOB_EXECUTED
from apscheduler.jobstores.base import JobLookupError
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger
@ -20,6 +20,7 @@ from app.classes.shared.file_helpers import FileHelpers
from app.classes.shared.helpers import Helpers
from app.classes.shared.main_controller import Controller
from app.classes.web.tornado_handler import Webserver
from app.classes.shared.websocket_manager import WebSocketManager
logger = logging.getLogger("apscheduler")
scheduler_intervals = {
@ -101,7 +102,7 @@ class TasksManager:
)
except:
logger.error(
"Server value requested does not exist! "
f"Server value {cmd['server_id']} requested does not exist! "
"Purging item from waiting commands."
)
continue
@ -200,6 +201,13 @@ class TasksManager:
id="update_watcher",
start_date=datetime.datetime.now(),
)
self.scheduler.add_job(
self.controller.write_auth_tracker,
"interval",
minutes=5,
id="auth_tracker_write",
start_date=datetime.datetime.now(),
)
# self.scheduler.add_job(
# self.scheduler.print_jobs, "interval", seconds=10, id="-1"
# )
@ -324,11 +332,16 @@ class TasksManager:
# Checks to make sure some doofus didn't actually make the newly
# created task a child of itself.
if str(job_data["parent"]) == str(sch_id):
if (
str(job_data["parent"]) == str(sch_id)
or job_data["interval_type"] != "reaction"
):
HelpersManagement.update_scheduled_task(sch_id, {"parent": None})
# Check to see if it's enabled and is not a chain reaction.
if job_data["enabled"] and job_data["interval_type"] != "reaction":
# Lets make sure this can not be mistaken for a reaction
job_data["parent"] = None
new_job = "error"
if job_data["cron_string"] != "":
try:
@ -449,7 +462,8 @@ class TasksManager:
def update_job(self, sch_id, job_data):
# Checks to make sure some doofus didn't actually make the newly
# created task a child of itself.
if str(job_data.get("parent")) == str(sch_id):
interval_type = job_data.get("interval_type")
if str(job_data.get("parent")) == str(sch_id) or interval_type != "reaction":
job_data["parent"] = None
HelpersManagement.update_scheduled_task(sch_id, job_data)
@ -466,13 +480,15 @@ class TasksManager:
job_data = HelpersManagement.get_scheduled_task(sch_id)
job_data["server_id"] = job_data["server_id"]["server_id"]
else:
self.scheduler.remove_job(str(sch_id))
job = HelpersManagement.get_scheduled_task(sch_id)
if job["interval_type"] != "reaction":
self.scheduler.remove_job(str(sch_id))
return
try:
if job_data["interval"] != "reaction":
self.scheduler.remove_job(str(sch_id))
except:
except JobLookupError:
logger.info(
"No job found in update job. "
"Assuming it was previously disabled. Starting new job."
@ -608,7 +624,10 @@ class TasksManager:
):
# event job ID's are strings so we need to look at
# this as the same data type.
if str(schedule.parent) == str(event.job_id):
if (
str(schedule.parent) == str(event.job_id)
and schedule.interval_type == "reaction"
):
if schedule.enabled:
delaytime = datetime.datetime.now() + datetime.timedelta(
seconds=schedule.delay
@ -700,10 +719,16 @@ class TasksManager:
# Stats are different
host_stats = HelpersManagement.get_latest_hosts_stats()
if len(self.helper.websocket_helper.clients) > 0:
self.controller.management.cpu_usage.set(host_stats.get("cpu_usage"))
self.controller.management.mem_usage_percent.set(
host_stats.get("mem_percent")
)
if len(WebSocketManager().clients) > 0:
# There are clients
try:
self.helper.websocket_helper.broadcast_page(
WebSocketManager().broadcast_page(
"/panel/dashboard",
"update_host_stats",
{
@ -720,7 +745,7 @@ class TasksManager:
},
)
except:
self.helper.websocket_helper.broadcast_page(
WebSocketManager().broadcast_page(
"/panel/dashboard",
"update_host_stats",
{
@ -761,11 +786,13 @@ class TasksManager:
)
# Search for old files in imports
self.helper.ensure_dir_exists(
os.path.join(self.controller.project_root, "imports")
os.path.join(self.controller.project_root, "import", "upload")
)
for file in os.listdir(os.path.join(self.controller.project_root, "imports")):
for file in os.listdir(
os.path.join(self.controller.project_root, "import", "upload")
):
if self.helper.is_file_older_than_x_days(
os.path.join(self.controller.project_root, "imports", file)
os.path.join(self.controller.project_root, "import", "upload", file)
):
try:
os.remove(os.path.join(file))

View File

@ -1,26 +1,25 @@
import json
import logging
from app.classes.shared.singleton import Singleton
from app.classes.shared.console import Console
from app.classes.models.users import HelperUsers
logger = logging.getLogger(__name__)
class WebSocketHelper:
def __init__(self, helper):
self.helper = helper
class WebSocketManager(metaclass=Singleton):
def __init__(self):
self.clients = set()
def add_client(self, client):
self.clients.add(client)
def remove_client(self, client):
self.clients.remove(client)
def send_message(self, client, event_type: str, data):
if client.check_auth():
message = str(json.dumps({"event": event_type, "data": data}))
client.write_message_helper(message)
if client in self.clients:
self.clients.remove(client)
else:
logger.exception("Error caught while removing unknown WebSocket client")
def broadcast(self, event_type: str, data):
logger.debug(
@ -29,13 +28,29 @@ class WebSocketHelper:
)
for client in self.clients:
try:
self.send_message(client, event_type, data)
client.send_message(event_type, data)
except Exception as e:
logger.exception(
f"Error caught while sending WebSocket message to "
f"{client.get_remote_ip()} {e}"
)
def broadcast_to_admins(self, event_type: str, data):
def filter_fn(client):
if str(client.get_user_id()) in str(HelperUsers.get_super_user_list()):
return True
return False
self.broadcast_with_fn(filter_fn, event_type, data)
def broadcast_to_non_admins(self, event_type: str, data):
def filter_fn(client):
if str(client.get_user_id()) not in str(HelperUsers.get_super_user_list()):
return True
return False
self.broadcast_with_fn(filter_fn, event_type, data)
def broadcast_page(self, page: str, event_type: str, data):
def filter_fn(client):
return client.page == page
@ -90,13 +105,14 @@ class WebSocketHelper:
static_clients = self.clients
clients = list(filter(filter_fn, static_clients))
logger.debug(
f"Sending to {len(clients)} out of {len(self.clients)} "
f"Sending to {len(clients)} \
out of {len(self.clients)} "
f"clients: {json.dumps({'event': event_type, 'data': data})}"
)
for client in clients[:]:
try:
self.send_message(client, event_type, data)
client.send_message(event_type, data)
except Exception as e:
logger.exception(
f"Error catched while sending WebSocket message to "

View File

@ -1,446 +0,0 @@
from datetime import datetime
import logging
import re
from app.classes.controllers.crafty_perms_controller import EnumPermissionsCrafty
from app.classes.controllers.server_perms_controller import EnumPermissionsServer
from app.classes.web.base_handler import BaseHandler
from app.classes.models.management import DatabaseShortcuts
logger = logging.getLogger(__name__)
bearer_pattern = re.compile(r"^Bearer", flags=re.IGNORECASE)
class ApiHandler(BaseHandler):
def return_response(self, status: int, data: dict):
# Define a standardized response
self.set_status(status)
self.write(data)
def check_xsrf_cookie(self):
# Disable CSRF protection on API routes
pass
def access_denied(self, user, reason=""):
if reason:
reason = " because " + reason
logger.info(
"User %s from IP %s was denied access to the API route "
+ self.request.path
+ reason,
user,
self.get_remote_ip(),
)
self.finish(
self.return_response(
403,
{
"error": "ACCESS_DENIED",
"info": "You were denied access to the requested resource",
},
)
)
def authenticate_user(self) -> bool:
self.permissions = {
"Commands": EnumPermissionsServer.COMMANDS,
"Terminal": EnumPermissionsServer.TERMINAL,
"Logs": EnumPermissionsServer.LOGS,
"Schedule": EnumPermissionsServer.SCHEDULE,
"Backup": EnumPermissionsServer.BACKUP,
"Files": EnumPermissionsServer.FILES,
"Config": EnumPermissionsServer.CONFIG,
"Players": EnumPermissionsServer.PLAYERS,
"Server_Creation": EnumPermissionsCrafty.SERVER_CREATION,
"User_Config": EnumPermissionsCrafty.USER_CONFIG,
"Roles_Config": EnumPermissionsCrafty.ROLES_CONFIG,
}
try:
logger.debug("Searching for specified token")
api_token = self.get_argument("token", "")
self.api_token = api_token
if api_token is None and self.request.headers.get("Authorization"):
api_token = bearer_pattern.sub(
"", self.request.headers.get("Authorization")
)
elif api_token is None:
api_token = self.get_cookie("token")
user_data = self.controller.users.get_user_by_api_token(api_token)
logger.debug("Checking results")
if user_data:
# Login successful! Check perms
logger.info(f"User {user_data['username']} has authenticated to API")
return True # This is to set the "authenticated"
logging.debug("Auth unsuccessful")
self.access_denied("unknown", "the user provided an invalid token")
return False
except Exception as e:
logger.warning("An error occured while authenticating an API user: %s", e)
self.finish(
self.return_response(
403,
{
"error": "ACCESS_DENIED",
"info": "An error occured while authenticating the user",
},
)
)
return False
class ServersStats(ApiHandler):
def get(self):
"""Get details about all servers"""
authenticated = self.authenticate_user()
user_obj = self.controller.users.get_user_by_api_token(self.api_token)
if not authenticated:
return
if user_obj["superuser"]:
raw_stats = self.controller.servers.get_all_servers_stats()
else:
raw_stats = self.controller.servers.get_authorized_servers_stats(
user_obj["user_id"]
)
stats = []
for rs in raw_stats:
s = {}
for k, v in rs["server_data"].items():
if isinstance(v, datetime):
s[k] = v.timestamp()
else:
s[k] = v
stats.append(s)
# Get server stats
# TODO Check perms
self.finish(self.write({"servers": stats}))
class NodeStats(ApiHandler):
def get(self):
"""Get stats for particular node"""
authenticated = self.authenticate_user()
if not authenticated:
return
# Get node stats
node_stats = self.controller.servers.stats.get_node_stats()
self.return_response(200, {"code": node_stats["node_stats"]})
class SendCommand(ApiHandler):
def post(self):
user = self.authenticate_user()
user_obj = self.controller.users.get_user_by_api_token(self.api_token)
if user is None:
self.access_denied("unknown")
return
server_id = self.get_argument("id")
if (
not user_obj["user_id"]
in self.controller.server_perms.get_server_user_list(server_id)
and not user_obj["superuser"]
):
self.access_denied("unknown")
return
if not self.permissions[
"Commands"
] in self.controller.server_perms.get_api_key_permissions_list(
self.controller.users.get_api_key_by_token(self.api_token), server_id
):
self.access_denied(user)
return
command = self.get_argument("command", default=None, strip=True)
server_id = self.get_argument("id")
if command:
server = self.controller.servers.get_server_instance_by_id(server_id)
if server.check_running:
server.send_command(command)
self.return_response(200, {"run": True})
else:
self.return_response(200, {"error": "SER_NOT_RUNNING"})
else:
self.return_response(200, {"error": "NO_COMMAND"})
class ServerBackup(ApiHandler):
def post(self):
user = self.authenticate_user()
user_obj = self.controller.users.get_user_by_api_token(self.api_token)
if user is None:
self.access_denied("unknown")
return
server_id = self.get_argument("id")
if (
not user_obj["user_id"]
in self.controller.server_perms.get_server_user_list(server_id)
and not user_obj["superuser"]
):
self.access_denied("unknown")
return
if not self.permissions[
"Backup"
] in self.controller.server_perms.get_api_key_permissions_list(
self.controller.users.get_api_key_by_token(self.api_token), server_id
):
self.access_denied(user)
return
server = self.controller.servers.get_server_instance_by_id(server_id)
server.backup_server()
self.return_response(200, {"code": "SER_BAK_CALLED"})
class StartServer(ApiHandler):
def post(self):
user = self.authenticate_user()
remote_ip = self.get_remote_ip()
user_obj = self.controller.users.get_user_by_api_token(self.api_token)
if user is None:
self.access_denied("unknown")
return
server_id = self.get_argument("id")
if (
not user_obj["user_id"]
in self.controller.server_perms.get_server_user_list(server_id)
and not user_obj["superuser"]
):
self.access_denied("unknown")
return
if not self.permissions[
"Commands"
] in self.controller.server_perms.get_api_key_permissions_list(
self.controller.users.get_api_key_by_token(self.api_token), server_id
):
self.access_denied("unknown")
return
server = self.controller.servers.get_server_instance_by_id(server_id)
if not server.check_running():
self.controller.management.send_command(
user_obj["user_id"], server_id, remote_ip, "start_server"
)
self.return_response(200, {"code": "SER_START_CALLED"})
else:
self.return_response(500, {"error": "SER_RUNNING"})
class StopServer(ApiHandler):
def post(self):
user = self.authenticate_user()
remote_ip = self.get_remote_ip()
user_obj = self.controller.users.get_user_by_api_token(self.api_token)
if user is None:
self.access_denied("unknown")
return
server_id = self.get_argument("id")
if (
not user_obj["user_id"]
in self.controller.server_perms.get_server_user_list(server_id)
and not user_obj["superuser"]
):
self.access_denied("unknown")
if not self.permissions[
"Commands"
] in self.controller.server_perms.get_api_key_permissions_list(
self.controller.users.get_api_key_by_token(self.api_token), server_id
):
self.access_denied(user)
return
server = self.controller.servers.get_server_instance_by_id(server_id)
if server.check_running():
self.controller.management.send_command(
user, server_id, remote_ip, "stop_server"
)
self.return_response(200, {"code": "SER_STOP_CALLED"})
else:
self.return_response(500, {"error": "SER_NOT_RUNNING"})
class RestartServer(ApiHandler):
def post(self):
user = self.authenticate_user()
remote_ip = self.get_remote_ip()
user_obj = self.controller.users.get_user_by_api_token(self.api_token)
if user is None:
self.access_denied("unknown")
return
server_id = self.get_argument("id")
if not user_obj["user_id"] in self.controller.server_perms.get_server_user_list(
server_id
):
self.access_denied("unknown")
if not self.permissions[
"Commands"
] in self.controller.server_perms.get_api_key_permissions_list(
self.controller.users.get_api_key_by_token(self.api_token), server_id
):
self.access_denied(user)
self.controller.management.send_command(
user, server_id, remote_ip, "restart_server"
)
self.return_response(200, {"code": "SER_RESTART_CALLED"})
class CreateUser(ApiHandler):
def post(self):
user = self.authenticate_user()
user_obj = self.controller.users.get_user_by_api_token(self.api_token)
user_perms = self.controller.crafty_perms.get_crafty_permissions_list(
user_obj["user_id"]
)
if (
not self.permissions["User_Config"] in user_perms
and not user_obj["superuser"]
):
self.access_denied("unknown")
return
if user is None:
self.access_denied("unknown")
return
if not self.permissions[
"User_Config"
] in self.controller.crafty_perms.get_api_key_permissions_list(
self.controller.users.get_api_key_by_token(self.api_token)
):
self.access_denied(user)
return
new_username = self.get_argument("username").lower()
new_pass = self.get_argument("password")
manager = int(user_obj["user_id"])
if new_username:
self.controller.users.add_user(
new_username, manager, new_pass, "default@example.com", True, False
)
self.return_response(
200,
{
"code": "COMPLETE",
"username": new_username,
"password": new_pass,
},
)
else:
self.return_response(
500,
{
"error": "MISSING_PARAMS",
"info": "Some paramaters failed validation",
},
)
class DeleteUser(ApiHandler):
def post(self):
user = self.authenticate_user()
user_obj = self.controller.users.get_user_by_api_token(self.api_token)
user_perms = self.controller.crafty_perms.get_crafty_permissions_list(
user_obj["user_id"]
)
if (
not self.permissions["User_Config"] in user_perms
and not user_obj["superuser"]
):
self.access_denied("unknown")
return
if user is None:
self.access_denied("unknown")
return
if not self.permissions[
"User_Config"
] in self.controller.crafty_perms.get_api_key_permissions_list(
self.controller.users.get_api_key_by_token(self.api_token)
):
self.access_denied(user)
return
user_id = self.get_argument("user_id", None, True)
user_to_del = self.controller.users.get_user_by_id(user_id)
if user_to_del["superuser"]:
self.return_response(
500,
{"error": "NOT_ALLOWED", "info": "You cannot delete a super user"},
)
else:
if user_id:
self.controller.users.remove_user(user_id)
self.return_response(200, {"code": "COMPLETED"})
class ListServers(ApiHandler):
def get(self):
user = self.authenticate_user()
user_obj = self.controller.users.get_user_by_api_token(self.api_token)
if user is None:
self.access_denied("unknown")
return
if self.api_token is None:
self.access_denied("unknown")
return
if user_obj["superuser"]:
servers = self.controller.servers.get_all_defined_servers()
servers = [str(i) for i in servers]
else:
servers = self.controller.servers.get_authorized_servers(
user_obj["user_id"]
)
page_servers = []
for server in servers:
if server not in page_servers:
page_servers.append(
DatabaseShortcuts.get_data_obj(server.server_object)
)
servers = page_servers
servers = [str(i) for i in servers]
self.return_response(
200,
{
"code": "COMPLETED",
"servers": servers,
},
)

View File

@ -2,7 +2,7 @@ import logging
import re
import typing as t
import orjson
import bleach
import nh3
import tornado.web
from app.classes.models.crafty_permissions import EnumPermissionsCrafty
@ -11,9 +11,10 @@ from app.classes.shared.helpers import Helpers
from app.classes.shared.file_helpers import FileHelpers
from app.classes.shared.main_controller import Controller
from app.classes.shared.translation import Translation
from app.classes.models.management import DatabaseShortcuts
from app.classes.shared.main_models import DatabaseShortcuts
logger = logging.getLogger(__name__)
auth_log = logging.getLogger("auth")
bearer_pattern = re.compile(r"^Bearer ", flags=re.IGNORECASE)
@ -101,7 +102,7 @@ class BaseHandler(tornado.web.RequestHandler):
if type(text) in self.nobleach:
logger.debug("Auto-bleaching - bypass type")
return text
return bleach.clean(text)
return nh3.clean(text)
def get_argument(
self,
@ -231,9 +232,16 @@ class BaseHandler(tornado.web.RequestHandler):
user,
)
logging.debug("Auth unsuccessful")
auth_log.error(
f"Authentication attempted from {self.get_remote_ip()}. Invalid token"
)
self.access_denied(None, "the user provided an invalid token")
return None
except Exception as auth_exception:
auth_log.error(
f"Authentication attempted from {self.get_remote_ip()}."
f" Error: {auth_exception}"
)
logger.debug(
"An error occured while authenticating an API user:",
exc_info=auth_exception,

View File

@ -0,0 +1,53 @@
import logging
import typing as t
from prometheus_client import REGISTRY, CollectorRegistry
from prometheus_client.exposition import _bake_output
from prometheus_client.exposition import parse_qs, urlparse
from app.classes.web.base_api_handler import BaseApiHandler
logger = logging.getLogger(__name__)
class BaseMetricsHandler(BaseApiHandler):
"""HTTP handler that gives metrics from ``REGISTRY``."""
registry: CollectorRegistry = REGISTRY
# registry.unregister(GC_COLLECTOR)
# registry.unregister(PLATFORM_COLLECTOR)
# registry.unregister(PROCESS_COLLECTOR)
def get_registry(self) -> None:
# Prepare parameters
registry = self.registry
accept_header = self.request.headers.get("Accept")
accept_encoding_header = self.request.headers.get("Accept-Encoding")
params = parse_qs(urlparse(self.request.path).query)
# Bake output
status, headers, output = _bake_output(
registry, accept_header, accept_encoding_header, params, False
)
# Return output
self.finish_metrics(int(status.split(" ", maxsplit=1)[0]), headers, output)
@classmethod
def factory(cls, registry: CollectorRegistry) -> type:
"""Returns a dynamic MetricsHandler class tied
to the passed registry.
"""
# This implementation relies on MetricsHandler.registry
# (defined above and defaulted to REGISTRY).
# As we have unicode_literals, we need to create a str()
# object for type().
cls_name = str(cls.__name__)
MyMetricsHandler = type(cls_name, (cls, object), {"registry": registry})
return MyMetricsHandler
def finish_metrics(self, status: int, headers, data: t.Dict[str, t.Any]):
self.set_status(status)
self.set_header("Content-Type", "text/plain")
for header in headers:
self.set_header(*header)
self.finish(data)

View File

@ -7,7 +7,8 @@ import json
import logging
import threading
import urllib.parse
import bleach
from zoneinfo import ZoneInfoNotFoundError
import nh3
import requests
import tornado.web
import tornado.escape
@ -15,7 +16,6 @@ from tornado import iostream
# TZLocal is set as a hidden import on win pipeline
from tzlocal import get_localzone
from tzlocal.utils import ZoneInfoNotFoundError
from app.classes.models.servers import Servers
from app.classes.models.server_permissions import EnumPermissionsServer
@ -25,6 +25,7 @@ from app.classes.controllers.roles_controller import RolesController
from app.classes.shared.helpers import Helpers
from app.classes.shared.main_models import DatabaseShortcuts
from app.classes.web.base_handler import BaseHandler
from app.classes.web.webhooks.webhook_factory import WebhookFactory
logger = logging.getLogger(__name__)
@ -67,9 +68,7 @@ class PanelHandler(BaseHandler):
) in self.controller.crafty_perms.list_defined_crafty_permissions():
argument = int(
float(
bleach.clean(
self.get_argument(f"permission_{permission.name}", "0")
)
nh3.clean(self.get_argument(f"permission_{permission.name}", "0"))
)
)
if argument:
@ -78,9 +77,7 @@ class PanelHandler(BaseHandler):
)
q_argument = int(
float(
bleach.clean(self.get_argument(f"quantity_{permission.name}", "0"))
)
float(nh3.clean(self.get_argument(f"quantity_{permission.name}", "0")))
)
if q_argument:
server_quantity[permission.name] = q_argument
@ -213,6 +210,8 @@ class PanelHandler(BaseHandler):
error = self.get_argument("error", "WTF Error!")
template = "panel/denied.html"
if self.helper.crafty_starting:
page = "loading"
now = time.time()
formatted_time = str(
@ -246,9 +245,13 @@ class PanelHandler(BaseHandler):
for r in exec_user["roles"]:
role = self.controller.roles.get_role(r)
exec_user_role.add(role["role_name"])
defined_servers = self.controller.servers.get_authorized_servers(
exec_user["user_id"]
)
# get_auth_servers will throw an exception if run while Crafty is starting
if not self.helper.crafty_starting:
defined_servers = self.controller.servers.get_authorized_servers(
exec_user["user_id"]
)
else:
defined_servers = []
user_order = self.controller.users.get_user_by_id(exec_user["user_id"])
user_order = user_order["server_order"].split(",")
@ -348,7 +351,9 @@ class PanelHandler(BaseHandler):
) as credits_default_local:
try:
remote = requests.get(
"https://craftycontrol.com/credits-v2", allow_redirects=True
"https://craftycontrol.com/credits-v2",
allow_redirects=True,
timeout=10,
)
credits_dict: dict = remote.json()
if not credits_dict["staff"]:
@ -479,9 +484,15 @@ class PanelHandler(BaseHandler):
template = "panel/dashboard.html"
elif page == "server_detail":
subpage = bleach.clean(self.get_argument("subpage", ""))
subpage = nh3.clean(self.get_argument("subpage", ""))
server_id = self.check_server_id()
# load page the user was on last
server_subpage = self.controller.servers.server_subpage.get(server_id, "")
if subpage == "" and server_subpage != "":
subpage = self.controller.servers.server_subpage.get(server_id, "")
else:
self.controller.servers.server_subpage[server_id] = subpage
if server_id is None:
return
if not self.failed_server:
@ -747,8 +758,24 @@ class PanelHandler(BaseHandler):
0, page_data["options"].pop(page_data["options"].index(days))
)
page_data["history_stats"] = self.controller.servers.get_history_stats(
server_id, days
server_id, hours=(days * 24)
)
if subpage == "webhooks":
if (
not page_data["permissions"]["Config"]
in page_data["user_permissions"]
):
if not superuser:
self.redirect(
"/panel/error?error=Unauthorized access to Webhooks Config"
)
return
page_data[
"webhooks"
] = self.controller.management.get_webhooks_by_server(
server_id, model=True
)
page_data["triggers"] = WebhookFactory.get_monitored_events()
def get_banned_players_html():
banned_players = self.controller.servers.get_banned_players(server_id)
@ -1016,6 +1043,110 @@ class PanelHandler(BaseHandler):
template = "panel/panel_edit_user.html"
elif page == "add_webhook":
server_id = self.get_argument("id", None)
if server_id is None:
return self.redirect("/panel/error?error=Invalid Server ID")
server_obj = self.controller.servers.get_server_instance_by_id(server_id)
page_data["backup_failed"] = server_obj.last_backup_status()
server_obj = None
page_data["active_link"] = "webhooks"
page_data["server_data"] = self.controller.servers.get_server_data_by_id(
server_id
)
page_data[
"user_permissions"
] = self.controller.server_perms.get_user_id_permissions_list(
exec_user["user_id"], server_id
)
page_data["permissions"] = {
"Commands": EnumPermissionsServer.COMMANDS,
"Terminal": EnumPermissionsServer.TERMINAL,
"Logs": EnumPermissionsServer.LOGS,
"Schedule": EnumPermissionsServer.SCHEDULE,
"Backup": EnumPermissionsServer.BACKUP,
"Files": EnumPermissionsServer.FILES,
"Config": EnumPermissionsServer.CONFIG,
"Players": EnumPermissionsServer.PLAYERS,
}
page_data["server_stats"] = self.controller.servers.get_server_stats_by_id(
server_id
)
page_data["server_stats"][
"server_type"
] = self.controller.servers.get_server_type_by_id(server_id)
page_data["new_webhook"] = True
page_data["webhook"] = {}
page_data["webhook"]["webhook_type"] = "Custom"
page_data["webhook"]["name"] = ""
page_data["webhook"]["url"] = ""
page_data["webhook"]["bot_name"] = "Crafty Controller"
page_data["webhook"]["trigger"] = []
page_data["webhook"]["body"] = ""
page_data["webhook"]["color"] = "#005cd1"
page_data["webhook"]["enabled"] = True
page_data["providers"] = WebhookFactory.get_supported_providers()
page_data["triggers"] = WebhookFactory.get_monitored_events()
if not EnumPermissionsServer.CONFIG in page_data["user_permissions"]:
if not superuser:
self.redirect("/panel/error?error=Unauthorized access To Webhooks")
return
template = "panel/server_webhook_edit.html"
elif page == "webhook_edit":
server_id = self.get_argument("id", None)
webhook_id = self.get_argument("webhook_id", None)
if server_id is None:
return self.redirect("/panel/error?error=Invalid Server ID")
server_obj = self.controller.servers.get_server_instance_by_id(server_id)
page_data["backup_failed"] = server_obj.last_backup_status()
server_obj = None
page_data["active_link"] = "webhooks"
page_data["server_data"] = self.controller.servers.get_server_data_by_id(
server_id
)
page_data[
"user_permissions"
] = self.controller.server_perms.get_user_id_permissions_list(
exec_user["user_id"], server_id
)
page_data["permissions"] = {
"Commands": EnumPermissionsServer.COMMANDS,
"Terminal": EnumPermissionsServer.TERMINAL,
"Logs": EnumPermissionsServer.LOGS,
"Schedule": EnumPermissionsServer.SCHEDULE,
"Backup": EnumPermissionsServer.BACKUP,
"Files": EnumPermissionsServer.FILES,
"Config": EnumPermissionsServer.CONFIG,
"Players": EnumPermissionsServer.PLAYERS,
}
page_data["server_stats"] = self.controller.servers.get_server_stats_by_id(
server_id
)
page_data["server_stats"][
"server_type"
] = self.controller.servers.get_server_type_by_id(server_id)
page_data["new_webhook"] = False
page_data["webhook"] = self.controller.management.get_webhook_by_id(
webhook_id
)
page_data["webhook"]["trigger"] = str(
page_data["webhook"]["trigger"]
).split(",")
page_data["providers"] = WebhookFactory.get_supported_providers()
page_data["triggers"] = WebhookFactory.get_monitored_events()
if not EnumPermissionsServer.CONFIG in page_data["user_permissions"]:
if not superuser:
self.redirect("/panel/error?error=Unauthorized access To Webhooks")
return
template = "panel/server_webhook_edit.html"
elif page == "add_schedule":
server_id = self.get_argument("id", None)
if server_id is None:
@ -1284,7 +1415,7 @@ class PanelHandler(BaseHandler):
template = "panel/panel_edit_user_apikeys.html"
elif page == "remove_user":
user_id = bleach.clean(self.get_argument("id", None))
user_id = nh3.clean(self.get_argument("id", None))
if (
not superuser
@ -1490,7 +1621,8 @@ class PanelHandler(BaseHandler):
logs_thread.start()
self.redirect("/panel/dashboard")
return
if self.helper.crafty_starting:
template = "panel/loading.html"
self.render(
template,
data=page_data,

View File

@ -1,11 +1,12 @@
import logging
import bleach
import nh3
from app.classes.shared.helpers import Helpers
from app.classes.models.users import HelperUsers
from app.classes.web.base_handler import BaseHandler
logger = logging.getLogger(__name__)
auth_log = logging.getLogger("auth")
class PublicHandler(BaseHandler):
@ -28,8 +29,8 @@ class PublicHandler(BaseHandler):
# self.clear_cookie("user_data")
def get(self, page=None):
error = bleach.clean(self.get_argument("error", "Invalid Login!"))
error_msg = bleach.clean(self.get_argument("error_msg", ""))
error = nh3.clean(self.get_argument("error", "Invalid Login!"))
error_msg = nh3.clean(self.get_argument("error_msg", ""))
page_data = {
"version": self.helper.get_version_string(),
@ -82,8 +83,8 @@ class PublicHandler(BaseHandler):
)
def post(self, page=None):
error = bleach.clean(self.get_argument("error", "Invalid Login!"))
error_msg = bleach.clean(self.get_argument("error_msg", ""))
error = nh3.clean(self.get_argument("error", "Invalid Login!"))
error_msg = nh3.clean(self.get_argument("error_msg", ""))
page_data = {
"version": self.helper.get_version_string(),
@ -96,18 +97,27 @@ class PublicHandler(BaseHandler):
page_data["query"] = self.request.query
if page == "login":
auth_log.info(
f"User attempting to authenticate from {self.get_remote_ip()}"
)
next_page = "/login"
if self.request.query:
next_page = "/login?" + self.request.query
entered_username = bleach.clean(self.get_argument("username"))
entered_password = bleach.clean(self.get_argument("password"))
entered_username = nh3.clean(self.get_argument("username"))
entered_password = self.get_argument("password")
# pylint: disable=no-member
try:
user_id = HelperUsers.get_user_id_by_name(entered_username.lower())
user_data = HelperUsers.get_user_model(user_id)
except:
self.controller.log_attempt(self.get_remote_ip(), entered_username)
auth_log.error(
f"User attempted to log into {entered_username}."
f" Authentication failed from remote IP {self.get_remote_ip()}"
" Users does not exist."
)
error_msg = "Incorrect username or password. Please try again."
# self.clear_cookie("user")
# self.clear_cookie("user_data")
@ -120,6 +130,12 @@ class PublicHandler(BaseHandler):
# if we don't have a user
if not user_data:
auth_log.error(
f"User attempted to log into {entered_username}. Authentication"
f" failed from remote IP {self.get_remote_ip()}"
" User does not exist."
)
self.controller.log_attempt(self.get_remote_ip(), entered_username)
error_msg = "Incorrect username or password. Please try again."
# self.clear_cookie("user")
# self.clear_cookie("user_data")
@ -132,6 +148,12 @@ class PublicHandler(BaseHandler):
# if they are disabled
if not user_data.enabled:
auth_log.error(
f"User attempted to log into {entered_username}. "
f"Authentication failed from remote IP {self.get_remote_ip()}."
" User account disabled"
)
self.controller.log_attempt(self.get_remote_ip(), entered_username)
error_msg = (
"User account disabled. Please contact "
"your system administrator for more info."
@ -159,7 +181,11 @@ class PublicHandler(BaseHandler):
user_data.last_ip = self.get_remote_ip()
user_data.last_login = Helpers.get_time_as_string()
user_data.save()
auth_log.info(
f"{entered_username} successfully"
" authenticated and logged"
f" into panel from remote IP {self.get_remote_ip()}"
)
# log this login
self.controller.management.add_to_audit_log(
user_data.user_id, "Logged in", 0, self.get_remote_ip()
@ -172,6 +198,11 @@ class PublicHandler(BaseHandler):
self.redirect(next_page)
else:
auth_log.error(
f"User attempted to log into {entered_username}."
f" Authentication failed from remote IP {self.get_remote_ip()}"
)
self.controller.log_attempt(self.get_remote_ip(), entered_username)
# self.clear_cookie("user")
# self.clear_cookie("user_data")
self.clear_cookie("token")

View File

@ -12,6 +12,7 @@ from app.classes.web.routes.api.roles.index import ApiRolesIndexHandler
from app.classes.web.routes.api.roles.role.index import ApiRolesRoleIndexHandler
from app.classes.web.routes.api.roles.role.servers import ApiRolesRoleServersHandler
from app.classes.web.routes.api.roles.role.users import ApiRolesRoleUsersHandler
from app.classes.web.routes.api.servers.index import ApiServersIndexHandler
from app.classes.web.routes.api.servers.server.action import (
ApiServersServerActionHandler,
@ -21,7 +22,13 @@ from app.classes.web.routes.api.servers.server.logs import ApiServersServerLogsH
from app.classes.web.routes.api.servers.server.public import (
ApiServersServerPublicHandler,
)
from app.classes.web.routes.api.servers.server.status import (
ApiServersServerStatusHandler,
)
from app.classes.web.routes.api.servers.server.stats import ApiServersServerStatsHandler
from app.classes.web.routes.api.servers.server.history import (
ApiServersServerHistoryHandler,
)
from app.classes.web.routes.api.servers.server.stdin import ApiServersServerStdinHandler
from app.classes.web.routes.api.servers.server.tasks.index import (
ApiServersServerTasksIndexHandler,
@ -43,6 +50,12 @@ from app.classes.web.routes.api.servers.server.tasks.task.children import (
from app.classes.web.routes.api.servers.server.tasks.task.index import (
ApiServersServerTasksTaskIndexHandler,
)
from app.classes.web.routes.api.servers.server.webhooks.index import (
ApiServersServerWebhooksIndexHandler,
)
from app.classes.web.routes.api.servers.server.webhooks.webhook.index import (
ApiServersServerWebhooksManagementIndexHandler,
)
from app.classes.web.routes.api.servers.server.users import ApiServersServerUsersHandler
from app.classes.web.routes.api.users.index import ApiUsersIndexHandler
from app.classes.web.routes.api.users.user.index import ApiUsersUserIndexHandler
@ -62,6 +75,7 @@ from app.classes.web.routes.api.crafty.config.index import (
from app.classes.web.routes.api.crafty.config.server_dir import (
ApiCraftyConfigServerDirHandler,
)
from app.classes.web.routes.api.crafty.stats.stats import ApiCraftyHostStatsHandler
from app.classes.web.routes.api.crafty.clogs.index import ApiCraftyLogIndexHandler
from app.classes.web.routes.api.crafty.imports.index import ApiImportFilesIndexHandler
from app.classes.web.routes.api.crafty.exe_cache import (
@ -103,11 +117,21 @@ def api_handlers(handler_args):
ApiCraftyConfigServerDirHandler,
handler_args,
),
(
r"/api/v2/crafty/stats/?",
ApiCraftyHostStatsHandler,
handler_args,
),
(
r"/api/v2/crafty/logs/([a-z0-9_]+)/?",
ApiCraftyLogIndexHandler,
handler_args,
),
(
r"/api/v2/crafty/JarCache/?",
ApiCraftyJarCacheIndexHandler,
handler_args,
),
(
r"/api/v2/import/file/unzip/?",
ApiImportFilesIndexHandler,
@ -185,6 +209,11 @@ def api_handlers(handler_args):
ApiCraftySteamCacheIndexHandler,
handler_args,
),
(
r"/api/v2/servers/status/?",
ApiServersServerStatusHandler,
handler_args,
),
(
r"/api/v2/servers/([0-9]+)/?",
ApiServersServerIndexHandler,
@ -235,6 +264,21 @@ def api_handlers(handler_args):
ApiServersServerStatsHandler,
handler_args,
),
(
r"/api/v2/servers/([0-9]+)/history/?",
ApiServersServerHistoryHandler,
handler_args,
),
(
r"/api/v2/servers/([0-9]+)/webhook/([0-9]+)/?",
ApiServersServerWebhooksManagementIndexHandler,
handler_args,
),
(
r"/api/v2/servers/([0-9]+)/webhook/?",
ApiServersServerWebhooksIndexHandler,
handler_args,
),
(
r"/api/v2/servers/([0-9]+)/action/([a-z_]+)/?",
ApiServersServerActionHandler,

View File

@ -7,7 +7,7 @@ from app.classes.shared.helpers import Helpers
from app.classes.web.base_api_handler import BaseApiHandler
logger = logging.getLogger(__name__)
auth_log = logging.getLogger("auth")
login_schema = {
"type": "object",
"properties": {
@ -29,6 +29,10 @@ class ApiAuthLoginHandler(BaseApiHandler):
try:
data = json.loads(self.request.body)
except json.decoder.JSONDecodeError as e:
logger.error(
"Invalid JSON schema for API"
f" login attempt from {self.get_remote_ip()}"
)
return self.finish_json(
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
)
@ -36,6 +40,10 @@ class ApiAuthLoginHandler(BaseApiHandler):
try:
validate(data, login_schema)
except ValidationError as e:
logger.error(
"Invalid JSON schema for API"
f" login attempt from {self.get_remote_ip()}"
)
return self.finish_json(
400,
{
@ -52,12 +60,23 @@ class ApiAuthLoginHandler(BaseApiHandler):
user_data = Users.get_or_none(Users.username == username)
if user_data is None:
self.controller.log_attempt(self.get_remote_ip(), username)
auth_log.error(
f"User attempted to log into {username}."
" Authentication failed from remote IP"
f" {self.get_remote_ip()}. User not found"
)
return self.finish_json(
401,
{"status": "error", "error": "INCORRECT_CREDENTIALS", "token": None},
)
if not user_data.enabled:
auth_log.error(
f"User attempted to log into {username}."
" Authentication failed from remote"
f" IP {self.get_remote_ip()} account disabled"
)
self.finish_json(
403, {"status": "error", "error": "ACCOUNT_DISABLED", "token": None}
)
@ -67,6 +86,11 @@ class ApiAuthLoginHandler(BaseApiHandler):
# Valid Login
if login_result:
auth_log.info(
f"{username} successfully"
" authenticated and logged"
f" into panel from remote IP {self.get_remote_ip()}"
)
logger.info(f"User: {user_data} Logged in from IP: {self.get_remote_ip()}")
# record this login

View File

@ -29,6 +29,14 @@ class ApiAnnounceIndexHandler(BaseApiHandler):
) = auth_data
data = self.helper.get_announcements()
if not data:
return self.finish_json(
424,
{
"status": "error",
"data": "Failed to get announcements",
},
)
cleared = str(
self.controller.users.get_user_by_id(auth_data[4]["user_id"])[
"cleared_notifs"
@ -84,6 +92,14 @@ class ApiAnnounceIndexHandler(BaseApiHandler):
},
)
announcements = self.helper.get_announcements()
if not announcements:
return self.finish_json(
424,
{
"status": "error",
"data": "Failed to get current announcements",
},
)
res = [d.get("id", None) for d in announcements]
cleared_notifs = str(
self.controller.users.get_user_by_id(auth_data[4]["user_id"])[

View File

@ -7,6 +7,7 @@ from jsonschema.exceptions import ValidationError
from app.classes.models.crafty_permissions import EnumPermissionsCrafty
from app.classes.shared.helpers import Helpers
from app.classes.web.base_api_handler import BaseApiHandler
from app.classes.web.websocket_handler import WebSocketManager
logger = logging.getLogger(__name__)
files_get_schema = {
@ -64,14 +65,16 @@ class ApiImportFilesIndexHandler(BaseApiHandler):
# JSON we need to remove this and just send
# the path.
if data["upload"]:
folder = os.path.join(self.controller.project_root, "imports", folder)
folder = os.path.join(
self.controller.project_root, "import", "upload", folder
)
if Helpers.check_file_exists(folder):
folder = self.file_helper.unzip_server(folder, user_id)
root_path = True
else:
if user_id:
user_lang = self.controller.users.get_user_lang_by_id(user_id)
self.helper.websocket_helper.broadcast_user(
WebSocketManager().broadcast_user(
user_id,
"send_start_error",
{
@ -83,7 +86,7 @@ class ApiImportFilesIndexHandler(BaseApiHandler):
else:
if not self.helper.check_path_exists(folder) and user_id:
user_lang = self.controller.users.get_user_lang_by_id(user_id)
self.helper.websocket_helper.broadcast_user(
WebSocketManager().broadcast_user(
user_id,
"send_start_error",
{

View File

@ -0,0 +1,21 @@
import logging
from app.classes.web.base_api_handler import BaseApiHandler
logger = logging.getLogger(__name__)
class ApiCraftyHostStatsHandler(BaseApiHandler):
def get(self):
auth_data = self.authenticate_user()
if not auth_data:
return
latest = self.controller.management.get_latest_hosts_stats()
self.finish_json(
200,
{
"status": "ok",
"data": latest,
},
)

View File

@ -110,7 +110,7 @@ class ApiRolesIndexHandler(BaseApiHandler):
try:
data = orjson.loads(self.request.body)
except orjson.decoder.JSONDecodeError as e:
except orjson.JSONDecodeError as e:
return self.finish_json(
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
)

View File

@ -9,7 +9,7 @@ logger = logging.getLogger(__name__)
new_server_schema = {
"definitions": {},
"$schema": "http://json-schema.org/draft-07/schema#",
"$schema": "https://json-schema.org/draft-07/schema#",
"title": "Root",
"type": "object",
"required": [

View File

@ -72,9 +72,9 @@ class ApiServersServerBackupsBackupIndexHandler(BaseApiHandler):
FileHelpers.del_file(
os.path.join(backup_conf["backup_path"], data["filename"])
)
except Exception:
except Exception as e:
return self.finish_json(
400, {"status": "error", "error": "NO BACKUP FOUND"}
400, {"status": "error", "error": f"DELETE FAILED with error {e}"}
)
self.controller.management.add_to_audit_log(
auth_data[4]["user_id"],
@ -121,11 +121,11 @@ class ApiServersServerBackupsBackupIndexHandler(BaseApiHandler):
server_data = self.controller.servers.get_server_data_by_id(server_id)
zip_name = data["filename"]
# import the server again based on zipfile
if server_data["type"] == "minecraft-java":
backup_path = svr_obj.backup_path
if Helpers.validate_traversal(backup_path, zip_name):
temp_dir = Helpers.unzip_backup_archive(backup_path, zip_name)
new_server = self.controller.import_zip_server(
backup_path = svr_obj.backup_path
if Helpers.validate_traversal(backup_path, zip_name):
temp_dir = Helpers.unzip_backup_archive(backup_path, zip_name)
if server_data["type"] == "minecraft-java":
new_server = self.controller.restore_java_zip_server(
svr_obj.server_name,
temp_dir,
server_data["executable"],
@ -134,71 +134,78 @@ class ApiServersServerBackupsBackupIndexHandler(BaseApiHandler):
server_data["server_port"],
server_data["created_by"],
)
new_server_id = new_server
new_server = self.controller.servers.get_server_data(new_server)
self.controller.rename_backup_dir(
server_id, new_server_id, new_server["server_uuid"]
elif server_data["type"] == "minecraft-bedrock":
new_server = self.controller.restore_bedrock_zip_server(
svr_obj.server_name,
temp_dir,
server_data["executable"],
server_data["server_port"],
server_data["created_by"],
)
# preserve current schedules
for schedule in self.controller.management.get_schedules_by_server(
server_id
):
self.tasks_manager.update_job(
schedule.schedule_id, {"server_id": new_server_id}
)
# preserve execution command
new_server_obj = self.controller.servers.get_server_obj(
new_server_id
new_server_id = new_server
new_server = self.controller.servers.get_server_data(new_server)
self.controller.rename_backup_dir(
server_id, new_server_id, new_server["server_uuid"]
)
# preserve current schedules
for schedule in self.controller.management.get_schedules_by_server(
server_id
):
job_data = self.controller.management.get_scheduled_task(
schedule.schedule_id
)
new_server_obj.execution_command = server_data["execution_command"]
# reset executable path
if svr_obj.path in svr_obj.executable:
new_server_obj.executable = str(svr_obj.executable).replace(
svr_obj.path, new_server_obj.path
)
# reset run command path
if svr_obj.path in svr_obj.execution_command:
new_server_obj.execution_command = str(
svr_obj.execution_command
).replace(svr_obj.path, new_server_obj.path)
# reset log path
if svr_obj.path in svr_obj.log_path:
new_server_obj.log_path = str(svr_obj.log_path).replace(
svr_obj.path, new_server_obj.path
)
self.controller.servers.update_server(new_server_obj)
job_data["server_id"] = new_server_id
del job_data["schedule_id"]
self.tasks_manager.update_job(schedule.schedule_id, job_data)
# preserve execution command
new_server_obj = self.controller.servers.get_server_obj(new_server_id)
new_server_obj.execution_command = server_data["execution_command"]
# reset executable path
if svr_obj.path in svr_obj.executable:
new_server_obj.executable = str(svr_obj.executable).replace(
svr_obj.path, new_server_obj.path
)
# reset run command path
if svr_obj.path in svr_obj.execution_command:
new_server_obj.execution_command = str(
svr_obj.execution_command
).replace(svr_obj.path, new_server_obj.path)
# reset log path
if svr_obj.path in svr_obj.log_path:
new_server_obj.log_path = str(svr_obj.log_path).replace(
svr_obj.path, new_server_obj.path
)
self.controller.servers.update_server(new_server_obj)
# preserve backup config
backup_config = self.controller.management.get_backup_config(
server_id
)
excluded_dirs = []
server_obj = self.controller.servers.get_server_obj(server_id)
loop_backup_path = self.helper.wtol_path(server_obj.path)
for item in self.controller.management.get_excluded_backup_dirs(
server_id
):
item_path = self.helper.wtol_path(item)
bu_path = os.path.relpath(item_path, loop_backup_path)
bu_path = os.path.join(new_server_obj.path, bu_path)
excluded_dirs.append(bu_path)
self.controller.management.set_backup_config(
new_server_id,
new_server_obj.backup_path,
backup_config["max_backups"],
excluded_dirs,
backup_config["compress"],
backup_config["shutdown"],
)
# remove old server's tasks
try:
self.tasks_manager.remove_all_server_tasks(server_id)
except JobLookupError as e:
logger.info("No active tasks found for server: {e}")
self.controller.remove_server(server_id, True)
except Exception:
# preserve backup config
backup_config = self.controller.management.get_backup_config(server_id)
excluded_dirs = []
server_obj = self.controller.servers.get_server_obj(server_id)
loop_backup_path = self.helper.wtol_path(server_obj.path)
for item in self.controller.management.get_excluded_backup_dirs(
server_id
):
item_path = self.helper.wtol_path(item)
bu_path = os.path.relpath(item_path, loop_backup_path)
bu_path = os.path.join(new_server_obj.path, bu_path)
excluded_dirs.append(bu_path)
self.controller.management.set_backup_config(
new_server_id,
new_server_obj.backup_path,
backup_config["max_backups"],
excluded_dirs,
backup_config["compress"],
backup_config["shutdown"],
)
# remove old server's tasks
try:
self.tasks_manager.remove_all_server_tasks(server_id)
except JobLookupError as e:
logger.info("No active tasks found for server: {e}")
self.controller.remove_server(server_id, True)
except Exception as e:
return self.finish_json(
400, {"status": "error", "error": "NO BACKUP FOUND"}
400, {"status": "error", "error": f"NO BACKUP FOUND {e}"}
)
self.controller.management.add_to_audit_log(
auth_data[4]["user_id"],

View File

@ -86,7 +86,7 @@ class ApiServersServerFilesIndexHandler(BaseApiHandler):
not in self.controller.server_perms.get_user_id_permissions_list(
auth_data[4]["user_id"], server_id
)
or EnumPermissionsServer.BACKUP
and EnumPermissionsServer.BACKUP
not in self.controller.server_perms.get_user_id_permissions_list(
auth_data[4]["user_id"], server_id
)

View File

@ -0,0 +1,28 @@
import logging
from app.classes.web.base_api_handler import BaseApiHandler
from app.classes.controllers.servers_controller import ServersController
logger = logging.getLogger(__name__)
class ApiServersServerHistoryHandler(BaseApiHandler):
def get(self, server_id: str):
auth_data = self.authenticate_user()
if not auth_data:
return
if server_id not in [str(x["server_id"]) for x in auth_data[0]]:
# if the user doesn't have access to the server, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
srv = ServersController().get_server_instance_by_id(server_id)
history = srv.get_server_history()
self.finish_json(
200,
{
"status": "ok",
"data": history,
},
)

View File

@ -0,0 +1,32 @@
import logging
from app.classes.web.base_api_handler import BaseApiHandler
logger = logging.getLogger(__name__)
class ApiServersServerStatusHandler(BaseApiHandler):
def get(self):
servers_status = []
servers_list = self.controller.servers.get_all_servers_stats()
for server in servers_list:
if server.get("server_data").get("show_status") is True:
servers_status.append(
{
"id": server.get("server_data").get("server_id"),
"world_name": server.get("stats").get("world_name"),
"running": server.get("stats").get("running"),
"online": server.get("stats").get("online"),
"max": server.get("stats").get("max"),
"version": server.get("stats").get("version"),
"desc": server.get("stats").get("desc"),
"icon": server.get("stats").get("icon"),
}
)
self.finish_json(
200,
{
"status": "ok",
"data": servers_status,
},
)

View File

@ -0,0 +1,108 @@
# TODO: create and read
import json
import logging
from jsonschema import ValidationError, validate
from app.classes.models.server_permissions import EnumPermissionsServer
from app.classes.web.base_api_handler import BaseApiHandler
from app.classes.web.webhooks.webhook_factory import WebhookFactory
logger = logging.getLogger(__name__)
new_webhook_schema = {
"type": "object",
"properties": {
"webhook_type": {
"type": "string",
"enum": WebhookFactory.get_supported_providers(),
},
"name": {"type": "string"},
"url": {"type": "string"},
"bot_name": {"type": "string"},
"trigger": {"type": "array"},
"body": {"type": "string"},
"color": {"type": "string", "default": "#005cd1"},
"enabled": {
"type": "boolean",
"default": True,
},
},
"additionalProperties": False,
"minProperties": 7,
}
class ApiServersServerWebhooksIndexHandler(BaseApiHandler):
def get(self, server_id: str):
auth_data = self.authenticate_user()
if not auth_data:
return
if (
EnumPermissionsServer.CONFIG
not in self.controller.server_perms.get_user_id_permissions_list(
auth_data[4]["user_id"], server_id
)
):
# if the user doesn't have Schedule permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
self.finish_json(
200,
{
"status": "ok",
"data": self.controller.management.get_webhooks_by_server(server_id),
},
)
def post(self, server_id: str):
auth_data = self.authenticate_user()
if not auth_data:
return
try:
data = json.loads(self.request.body)
except json.decoder.JSONDecodeError as e:
return self.finish_json(
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
)
try:
validate(data, new_webhook_schema)
except ValidationError as e:
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID_JSON_SCHEMA",
"error_data": str(e),
},
)
if server_id not in [str(x["server_id"]) for x in auth_data[0]]:
# if the user doesn't have access to the server, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
if (
EnumPermissionsServer.CONFIG
not in self.controller.server_perms.get_user_id_permissions_list(
auth_data[4]["user_id"], server_id
)
):
# if the user doesn't have Schedule permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
data["server_id"] = server_id
self.controller.management.add_to_audit_log(
auth_data[4]["user_id"],
f"Edited server {server_id}: added webhook",
server_id,
self.get_remote_ip(),
)
triggers = ""
for item in data["trigger"]:
string = item + ","
triggers += string
data["trigger"] = triggers
webhook_id = self.controller.management.create_webhook(data)
self.finish_json(200, {"status": "ok", "data": {"webhook_id": webhook_id}})

View File

@ -0,0 +1,187 @@
# TODO: read and delete
import json
import logging
from jsonschema import ValidationError, validate
from app.classes.models.server_permissions import EnumPermissionsServer
from app.classes.web.webhooks.webhook_factory import WebhookFactory
from app.classes.web.base_api_handler import BaseApiHandler
logger = logging.getLogger(__name__)
webhook_patch_schema = {
"type": "object",
"properties": {
"webhook_type": {
"type": "string",
"enum": WebhookFactory.get_supported_providers(),
},
"name": {"type": "string"},
"url": {"type": "string"},
"bot_name": {"type": "string"},
"trigger": {"type": "array"},
"body": {"type": "string"},
"color": {"type": "string", "default": "#005cd1"},
"enabled": {
"type": "boolean",
"default": True,
},
},
"additionalProperties": False,
"minProperties": 1,
}
class ApiServersServerWebhooksManagementIndexHandler(BaseApiHandler):
def get(self, server_id: str, webhook_id: str):
auth_data = self.authenticate_user()
if not auth_data:
return
if (
EnumPermissionsServer.CONFIG
not in self.controller.server_perms.get_user_id_permissions_list(
auth_data[4]["user_id"], server_id
)
):
# if the user doesn't have Schedule permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
if (
not str(webhook_id)
in self.controller.management.get_webhooks_by_server(server_id).keys()
):
return self.finish_json(
400, {"status": "error", "error": "NO WEBHOOK FOUND"}
)
self.finish_json(
200,
{
"status": "ok",
"data": self.controller.management.get_webhook_by_id(webhook_id),
},
)
def delete(self, server_id: str, webhook_id: str):
auth_data = self.authenticate_user()
if not auth_data:
return
if (
EnumPermissionsServer.CONFIG
not in self.controller.server_perms.get_user_id_permissions_list(
auth_data[4]["user_id"], server_id
)
):
# if the user doesn't have Schedule permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
try:
self.controller.management.delete_webhook(webhook_id)
except Exception:
return self.finish_json(
400, {"status": "error", "error": "NO WEBHOOK FOUND"}
)
self.controller.management.add_to_audit_log(
auth_data[4]["user_id"],
f"Edited server {server_id}: removed webhook",
server_id,
self.get_remote_ip(),
)
return self.finish_json(200, {"status": "ok"})
def patch(self, server_id: str, webhook_id: str):
auth_data = self.authenticate_user()
if not auth_data:
return
try:
data = json.loads(self.request.body)
except json.decoder.JSONDecodeError as e:
return self.finish_json(
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
)
try:
validate(data, webhook_patch_schema)
except ValidationError as e:
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID_JSON_SCHEMA",
"error_data": str(e),
},
)
if server_id not in [str(x["server_id"]) for x in auth_data[0]]:
# if the user doesn't have access to the server, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
if (
EnumPermissionsServer.CONFIG
not in self.controller.server_perms.get_user_id_permissions_list(
auth_data[4]["user_id"], server_id
)
):
# if the user doesn't have Schedule permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
data["server_id"] = server_id
if "trigger" in data.keys():
triggers = ""
for item in data["trigger"]:
string = item + ","
triggers += string
data["trigger"] = triggers
self.controller.management.modify_webhook(webhook_id, data)
self.controller.management.add_to_audit_log(
auth_data[4]["user_id"],
f"Edited server {server_id}: updated webhook",
server_id,
self.get_remote_ip(),
)
self.finish_json(200, {"status": "ok"})
def post(self, server_id: str, webhook_id: str):
auth_data = self.authenticate_user()
if not auth_data:
return
self.controller.management.add_to_audit_log(
auth_data[4]["user_id"],
"Tested webhook",
server_id,
self.get_remote_ip(),
)
if server_id not in [str(x["server_id"]) for x in auth_data[0]]:
# if the user doesn't have access to the server, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
if (
EnumPermissionsServer.CONFIG
not in self.controller.server_perms.get_user_id_permissions_list(
auth_data[4]["user_id"], server_id
)
):
# if the user doesn't have Schedule permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
webhook = self.controller.management.get_webhook_by_id(webhook_id)
try:
webhook_provider = WebhookFactory.create_provider(webhook["webhook_type"])
webhook_provider.send(
server_name=self.controller.servers.get_server_data_by_id(server_id)[
"server_name"
],
title=f"Test Webhook: {webhook['name']}",
url=webhook["url"],
message=webhook["body"],
color=webhook["color"], # Prestigious purple!
bot_name="Crafty Webhooks Tester",
)
except Exception as e:
self.finish_json(500, {"status": "error", "error": str(e)})
self.finish_json(200, {"status": "ok"})

View File

@ -4,10 +4,7 @@ import typing as t
from jsonschema import ValidationError, validate
from app.classes.controllers.users_controller import UsersController
from app.classes.models.crafty_permissions import (
EnumPermissionsCrafty,
PermissionsCrafty,
)
from app.classes.models.crafty_permissions import EnumPermissionsCrafty
from app.classes.models.roles import HelperRoles
from app.classes.models.users import HelperUsers
from app.classes.web.base_api_handler import BaseApiHandler
@ -218,7 +215,7 @@ class ApiUsersUserIndexHandler(BaseApiHandler):
user_obj = HelperUsers.get_user_model(user_id)
if "password" in data and str(user["user_id"]) != str(user_id):
if str(user["user_id"]) != str(user_obj.manager):
if str(user["user_id"]) != str(user_obj.manager) and not user["superuser"]:
# TODO: edit your own password
return self.finish_json(
400, {"status": "error", "error": "INVALID_PASSWORD_MODIFY"}
@ -247,31 +244,25 @@ class ApiUsersUserIndexHandler(BaseApiHandler):
or data["manager"] == 0
):
data["manager"] = None
crafty_perms = None
if "permissions" in data:
permissions: t.List[UsersController.ApiPermissionDict] = data.pop(
"permissions"
)
permissions_mask = "0" * len(EnumPermissionsCrafty)
limit_server_creation = 0
limit_user_creation = 0
limit_role_creation = 0
for permission in permissions:
permissions_mask = self.controller.crafty_perms.set_permission(
permissions_mask,
EnumPermissionsCrafty.__members__[permission["name"]],
"1" if permission["enabled"] else "0",
)
PermissionsCrafty.add_or_update_user(
user_id,
permissions_mask,
limit_server_creation,
limit_user_creation,
limit_role_creation,
)
if permissions is not None:
server_quantity = {}
permissions_mask = list(permissions_mask)
for permission in permissions:
server_quantity[permission["name"]] = permission["quantity"]
permissions_mask[
EnumPermissionsCrafty[permission["name"]].value
] = ("1" if permission["enabled"] else "0")
permissions_mask = "".join(permissions_mask)
crafty_perms = {
"permissions_mask": permissions_mask,
"server_quantity": server_quantity,
}
# TODO: make this more efficient
if len(data) != 0:
for key in data:
@ -280,7 +271,11 @@ class ApiUsersUserIndexHandler(BaseApiHandler):
if key == "password":
value = self.helper.encode_pass(value)
setattr(user_obj, key, value)
user_obj.save()
self.controller.users.update_user(
user_id,
data,
crafty_perms,
)
self.controller.management.add_to_audit_log(
user["user_id"],

View File

@ -0,0 +1,31 @@
from prometheus_client.exposition import _bake_output
from prometheus_client.exposition import parse_qs, urlparse
from app.classes.web.metrics_handler import BaseMetricsHandler
# Decorate function with metric.
class ApiOpenMetricsCraftyHandler(BaseMetricsHandler):
def get(self):
auth_data = self.authenticate_user()
if not auth_data:
return
if not auth_data[3]:
# if the user doesn't have access to the server, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
self.get_registry()
def get_registry(self) -> None:
# Prepare parameters
registry = self.controller.management.host_registry
accept_header = self.request.headers.get("Accept")
accept_encoding_header = self.request.headers.get("Accept-Encoding")
params = parse_qs(urlparse(self.request.path).query)
# Bake output
status, headers, output = _bake_output(
registry, accept_header, accept_encoding_header, params, False
)
# Return output
self.finish_metrics(int(status.split(" ", maxsplit=1)[0]), headers, output)

View File

@ -0,0 +1,21 @@
from prometheus_client import Info
from app.classes.web.metrics_handler import BaseMetricsHandler
CRAFTY_INFO = Info("Crafty_Controller", "Infos of this Crafty Instance")
# Decorate function with metric.
class ApiOpenMetricsIndexHandler(BaseMetricsHandler):
def get(self):
auth_data = self.authenticate_user()
if not auth_data:
return
version = f"{self.helper.get_version().get('major')} \
.{self.helper.get_version().get('minor')} \
.{self.helper.get_version().get('sub')}"
CRAFTY_INFO.info(
{"version": version, "docker": f"{self.helper.is_env_docker()}"}
)
self.get_registry()

View File

@ -0,0 +1,24 @@
from app.classes.web.routes.metrics.index import ApiOpenMetricsIndexHandler
from app.classes.web.routes.metrics.host import ApiOpenMetricsCraftyHandler
from app.classes.web.routes.metrics.servers import ApiOpenMetricsServersHandler
def metrics_handlers(handler_args):
return [
# OpenMetrics routes
(
r"/metrics/?",
ApiOpenMetricsIndexHandler,
handler_args,
),
(
r"/metrics/host/?",
ApiOpenMetricsCraftyHandler,
handler_args,
),
(
r"/metrics/servers/([0-9]+)/?",
ApiOpenMetricsServersHandler,
handler_args,
),
]

View File

@ -0,0 +1,37 @@
from prometheus_client.exposition import _bake_output
from prometheus_client.exposition import parse_qs, urlparse
from app.classes.web.metrics_handler import BaseMetricsHandler
from app.classes.controllers.servers_controller import ServersController
# Decorate function with metric.
class ApiOpenMetricsServersHandler(BaseMetricsHandler):
def get(self, server_id: str):
auth_data = self.authenticate_user()
if not auth_data:
return
if server_id not in [str(x["server_id"]) for x in auth_data[0]]:
# if the user doesn't have access to the server, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
self.get_registry(server_id)
def get_registry(self, server_id=None) -> None:
if server_id is None:
return self.finish_json(500, {"status": "error", "error": "UNKNOWN_SERVER"})
# Prepare parameters
registry = (
ServersController().get_server_instance_by_id(server_id).server_registry
)
accept_header = self.request.headers.get("Accept")
accept_encoding_header = self.request.headers.get("Accept-Encoding")
params = parse_qs(urlparse(self.request.path).query)
# Bake output
status, headers, output = _bake_output(
registry, accept_header, accept_encoding_header, params, False
)
# Return output
self.finish_metrics(int(status.split(" ", maxsplit=1)[0]), headers, output)

View File

@ -14,25 +14,15 @@ import tornado.httpserver
from app.classes.models.management import HelpersManagement
from app.classes.shared.console import Console
from app.classes.shared.helpers import Helpers
from app.classes.shared.file_helpers import FileHelpers
from app.classes.shared.main_controller import Controller
from app.classes.web.public_handler import PublicHandler
from app.classes.web.panel_handler import PanelHandler
from app.classes.web.default_handler import DefaultHandler
from app.classes.web.routes.api.api_handlers import api_handlers
from app.classes.web.routes.metrics.metrics_handlers import metrics_handlers
from app.classes.web.server_handler import ServerHandler
from app.classes.web.api_handler import (
ServersStats,
NodeStats,
ServerBackup,
StartServer,
StopServer,
RestartServer,
CreateUser,
DeleteUser,
ListServers,
SendCommand,
)
from app.classes.web.websocket_handler import SocketHandler
from app.classes.web.websocket_handler import WebSocketHandler
from app.classes.web.static_handler import CustomStaticHandler
from app.classes.web.upload_handler import UploadHandler
from app.classes.web.http_handler import HTTPHandler, HTTPHandlerPage
@ -46,7 +36,13 @@ class Webserver:
controller: Controller
helper: Helpers
def __init__(self, helper, controller, tasks_manager, file_helper):
def __init__(
self,
helper: Helpers,
controller: Controller,
tasks_manager,
file_helper: FileHelpers,
):
self.ioloop = None
self.http_server = None
self.https_server = None
@ -151,22 +147,13 @@ class Webserver:
(r"/", DefaultHandler, handler_args),
(r"/panel/(.*)", PanelHandler, handler_args),
(r"/server/(.*)", ServerHandler, handler_args),
(r"/ws", SocketHandler, handler_args),
(r"/ws", WebSocketHandler, handler_args),
(r"/upload", UploadHandler, handler_args),
(r"/status", StatusHandler, handler_args),
# API Routes V1
(r"/api/v1/stats/servers", ServersStats, handler_args),
(r"/api/v1/stats/node", NodeStats, handler_args),
(r"/api/v1/server/send_command", SendCommand, handler_args),
(r"/api/v1/server/backup", ServerBackup, handler_args),
(r"/api/v1/server/start", StartServer, handler_args),
(r"/api/v1/server/stop", StopServer, handler_args),
(r"/api/v1/server/restart", RestartServer, handler_args),
(r"/api/v1/list_servers", ListServers, handler_args),
(r"/api/v1/users/create_user", CreateUser, handler_args),
(r"/api/v1/users/delete_user", DeleteUser, handler_args),
# API Routes V2
*api_handlers(handler_args),
# API Routes OpenMetrics
*metrics_handlers(handler_args),
# Using this one at the end
# to catch all the other requests to Public Handler
(r"/(.*)", PublicHandler, handler_args),

View File

@ -12,6 +12,7 @@ from app.classes.shared.console import Console
from app.classes.shared.helpers import Helpers
from app.classes.shared.main_controller import Controller
from app.classes.web.base_handler import BaseHandler
from app.classes.shared.websocket_manager import WebSocketManager
logger = logging.getLogger(__name__)
@ -101,7 +102,8 @@ class UploadHandler(BaseHandler):
)
self.do_upload = False
path = os.path.join(self.controller.project_root, "imports")
path = os.path.join(self.controller.project_root, "import", "upload")
self.helper.ensure_dir_exists(path)
# Delete existing files
if len(os.listdir(path)) > 0:
for item in os.listdir():
@ -115,7 +117,7 @@ class UploadHandler(BaseHandler):
self.request.headers.get("X-FileName", None)
)
if not str(filename).endswith(".zip"):
self.helper.websocket_helper.broadcast("close_upload_box", "error")
WebSocketManager().broadcast("close_upload_box", "error")
self.finish("error")
full_path = os.path.join(path, filename)
@ -315,13 +317,13 @@ class UploadHandler(BaseHandler):
if self.do_upload:
time.sleep(5)
if files_left == 0:
self.helper.websocket_helper.broadcast("close_upload_box", "success")
WebSocketManager().broadcast("close_upload_box", "success")
self.finish("success") # Nope, I'm sending "success"
self.f.close()
else:
time.sleep(5)
if files_left == 0:
self.helper.websocket_helper.broadcast("close_upload_box", "error")
WebSocketManager().broadcast("close_upload_box", "error")
self.finish("error")
def data_received(self, chunk):

View File

@ -0,0 +1,39 @@
from abc import ABC, abstractmethod
import logging
import requests
from app.classes.shared.helpers import Helpers
logger = logging.getLogger(__name__)
helper = Helpers()
class WebhookProvider(ABC):
"""
Base class for all webhook providers.
Provides a common interface for all webhook provider implementations,
ensuring that each provider will have a send method.
"""
WEBHOOK_USERNAME = "Crafty Webhooks"
WEBHOOK_PFP_URL = (
"https://gitlab.com/crafty-controller/crafty-4/-"
+ "/raw/master/app/frontend/static/assets/images/"
+ "Crafty_4-0.png"
)
CRAFTY_VERSION = helper.get_version_string()
def _send_request(self, url, payload, headers=None):
"""Send a POST request to the given URL with the provided payload."""
try:
response = requests.post(url, json=payload, headers=headers, timeout=10)
response.raise_for_status()
return "Dispatch successful"
except requests.RequestException as error:
logger.error(error)
raise RuntimeError(f"Failed to dispatch notification: {error}") from error
@abstractmethod
def send(self, server_name, title, url, message, **kwargs):
"""Abstract method that derived classes will implement for sending webhooks."""

View File

@ -0,0 +1,82 @@
from datetime import datetime
from app.classes.web.webhooks.base_webhook import WebhookProvider
class DiscordWebhook(WebhookProvider):
def _construct_discord_payload(self, server_name, title, message, color, bot_name):
"""
Constructs the payload required for sending a Discord webhook notification.
This method prepares a payload for the Discord webhook API using the provided
message content, the Crafty Controller version, and the current UTC datetime.
Parameters:
server_name (str): The name of the server triggering the notification.
title (str): The title for the notification message.
message (str): The main content of the notification message.
color (int): The color code for the side stripe in the Discord embed message.
bot_name (str): Override for the Webhook's name set on creation
Returns:
tuple: A tuple containing the constructed payload (dict) incl headers (dict).
Note:
- Discord embed designer
- https://discohook.org/
"""
current_datetime = datetime.utcnow()
formatted_datetime = (
current_datetime.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
)
# Convert the hex to an integer
sanitized_hex = color[1:] if color.startswith("#") else color
color_int = int(sanitized_hex, 16)
headers = {"Content-type": "application/json"}
payload = {
"username": bot_name,
"avatar_url": self.WEBHOOK_PFP_URL,
"embeds": [
{
"title": title,
"description": message,
"color": color_int,
"author": {"name": server_name},
"footer": {"text": f"Crafty Controller v.{self.CRAFTY_VERSION}"},
"timestamp": formatted_datetime,
}
],
}
return payload, headers
def send(self, server_name, title, url, message, **kwargs):
"""
Sends a Discord webhook notification using the given details.
The method constructs and dispatches a payload suitable for
Discords's webhook system.
Parameters:
server_name (str): The name of the server triggering the notification.
title (str): The title for the notification message.
url (str): The webhook URL to send the notification to.
message (str): The main content or body of the notification message.
color (str, optional): The color code for the embed's side stripe.
Defaults to a pretty blue if not provided.
bot_name (str): Override for the Webhook's name set on creation
Returns:
str: "Dispatch successful!" if the message is sent successfully, otherwise an
exception is raised.
Raises:
Exception: If there's an error in dispatching the webhook.
"""
color = kwargs.get("color", "#005cd1") # Default to a color if not provided.
bot_name = kwargs.get("bot_name", self.WEBHOOK_USERNAME)
payload, headers = self._construct_discord_payload(
server_name, title, message, color, bot_name
)
return self._send_request(url, payload, headers)

View File

@ -0,0 +1,74 @@
from app.classes.web.webhooks.base_webhook import WebhookProvider
class MattermostWebhook(WebhookProvider):
def _construct_mattermost_payload(self, server_name, title, message, bot_name):
"""
Constructs the payload required for sending a Mattermost webhook notification.
The method formats the given information into a Markdown-styled message for MM,
including an information card containing the Crafty version.
Parameters:
server_name (str): The name of the server triggering the notification.
title (str): The title for the notification message.
message (str): The main content of the notification message.
bot_name (str): Override for the Webhook's name set on creation.
Returns:
tuple: A tuple containing the constructed payload (dict) incl headers (dict).
"""
formatted_text = (
f"-----\n\n"
f"#### {title}\n"
f"##### Server: ```{server_name}```\n\n"
f"```\n{message}\n```\n\n"
f"-----"
)
headers = {"Content-Type": "application/json"}
payload = {
"text": formatted_text,
"username": bot_name,
"icon_url": self.WEBHOOK_PFP_URL,
"props": {
"card": (
f"[Crafty Controller "
f"v.{self.CRAFTY_VERSION}](https://craftycontrol.com)"
)
},
}
return payload, headers
def send(self, server_name, title, url, message, **kwargs):
"""
Sends a Mattermost webhook notification using the given details.
The method constructs and dispatches a payload suitable for
Mattermost's webhook system.
Parameters:
server_name (str): The name of the server triggering the notification.
title (str): The title for the notification message.
url (str): The webhook URL to send the notification to.
message (str): The main content or body of the notification message.
bot_name (str): Override for the Webhook's name set on creation, see note!
Returns:
str: "Dispatch successful!" if the message is sent successfully, otherwise an
exception is raised.
Raises:
Exception: If there's an error in dispatching the webhook.
Note:
- To set webhook username & pfp Mattermost needs to be configured to allow this!
- Mattermost's `config.json` setting is `"EnablePostUsernameOverride": true`
- Mattermost's `config.json` setting is `"EnablePostIconOverride": true`
"""
bot_name = kwargs.get("bot_name", self.WEBHOOK_USERNAME)
payload, headers = self._construct_mattermost_payload(
server_name, title, message, bot_name
)
return self._send_request(url, payload, headers)

View File

@ -0,0 +1,98 @@
from app.classes.web.webhooks.base_webhook import WebhookProvider
class SlackWebhook(WebhookProvider):
def _construct_slack_payload(self, server_name, title, message, color, bot_name):
"""
Constructs the payload required for sending a Slack webhook notification.
The method formats the given information into a Markdown-styled message for MM,
including an information card containing the Crafty version.
Parameters:
server_name (str): The name of the server triggering the notification.
title (str): The title for the notification message.
message (str): The main content of the notification message.
color (int): The color code for the side stripe in the Slack block.
bot_name (str): Override for the Webhook's name set on creation, (not working).
Returns:
tuple: A tuple containing the constructed payload (dict) incl headers (dict).
Note:
- Block Builder/designer
- https://app.slack.com/block-kit-builder/
"""
headers = {"Content-Type": "application/json"}
payload = {
"username": bot_name,
"attachments": [
{
"color": color,
"blocks": [
{
"type": "section",
"text": {"type": "plain_text", "text": server_name},
},
{"type": "divider"},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": f"*{title}*\n{message}",
},
"accessory": {
"type": "image",
"image_url": self.WEBHOOK_PFP_URL,
"alt_text": "Crafty Controller Logo",
},
},
{
"type": "context",
"elements": [
{
"type": "mrkdwn",
"text": (
f"*Crafty Controller "
f"v{self.CRAFTY_VERSION}*"
),
}
],
},
{"type": "divider"},
],
}
],
}
return payload, headers
def send(self, server_name, title, url, message, **kwargs):
"""
Sends a Slack webhook notification using the given details.
The method constructs and dispatches a payload suitable for
Slack's webhook system.
Parameters:
server_name (str): The name of the server triggering the notification.
title (str): The title for the notification message.
url (str): The webhook URL to send the notification to.
message (str): The main content or body of the notification message.
color (str, optional): The color code for the blocks's colour accent.
Defaults to a pretty blue if not provided.
bot_name (str): Override for the Webhook's name set on creation, (not working).
Returns:
str: "Dispatch successful!" if the message is sent successfully, otherwise an
exception is raised.
Raises:
Exception: If there's an error in dispatching the webhook.
"""
color = kwargs.get("color", "#005cd1") # Default to a color if not provided.
bot_name = kwargs.get("bot_name", self.WEBHOOK_USERNAME)
payload, headers = self._construct_slack_payload(
server_name, title, message, color, bot_name
)
return self._send_request(url, payload, headers)

View File

@ -0,0 +1,126 @@
from datetime import datetime
from app.classes.web.webhooks.base_webhook import WebhookProvider
class TeamsWebhook(WebhookProvider):
def _construct_teams_payload(self, server_name, title, message):
"""
Constructs the payload required for sending a Teams Adaptive card notification.
This method prepares a payload for the Teams webhook API using the provided
message content, the Crafty Controller version, and the current UTC datetime.
Parameters:
server_name (str): The name of the server triggering the notification.
title (str): The title for the notification message.
message (str): The main content of the notification message.
Returns:
tuple: A tuple containing the constructed payload (dict) incl headers (dict).
Note:
- Adaptive Card Designer
- https://www.adaptivecards.io/designer/
"""
current_datetime = datetime.utcnow()
formatted_datetime = current_datetime.strftime("%Y-%m-%dT%H:%M:%SZ")
headers = {"Content-type": "application/json"}
payload = {
"type": "message",
"attachments": [
{
"contentType": "application/vnd.microsoft.card.adaptive",
"content": {
"body": [
{
"type": "TextBlock",
"size": "Medium",
"weight": "Bolder",
"text": f"{title}",
},
{
"type": "ColumnSet",
"columns": [
{
"type": "Column",
"items": [
{
"type": "Image",
"style": "Person",
"url": f"{self.WEBHOOK_PFP_URL}",
"size": "Small",
}
],
"width": "auto",
},
{
"type": "Column",
"items": [
{
"type": "TextBlock",
"weight": "Bolder",
"text": f"{server_name}",
"wrap": True,
},
{
"type": "TextBlock",
"spacing": "None",
"text": "{{DATE("
+ f"{formatted_datetime}"
+ ",SHORT)}}",
"isSubtle": True,
"wrap": True,
},
],
"width": "stretch",
},
],
},
{
"type": "TextBlock",
"text": f"{message}",
"wrap": True,
},
{
"type": "TextBlock",
"text": f"Crafty Controller v{self.CRAFTY_VERSION}",
"wrap": True,
"separator": True,
"isSubtle": True,
},
],
"$schema": (
"https://adaptivecards.io/schemas/adaptive-card.json"
),
"version": "1.6",
},
}
],
}
return payload, headers
def send(self, server_name, title, url, message, **kwargs):
"""
Sends a Teams Adaptive card notification using the given details.
The method constructs and dispatches a payload suitable for
Discords's webhook system.
Parameters:
server_name (str): The name of the server triggering the notification.
title (str): The title for the notification message.
url (str): The webhook URL to send the notification to.
message (str): The main content or body of the notification message.
Defaults to a pretty blue if not provided.
Returns:
str: "Dispatch successful!" if the message is sent successfully, otherwise an
exception is raised.
Raises:
Exception: If there's an error in dispatching the webhook.
"""
payload, headers = self._construct_teams_payload(server_name, title, message)
return self._send_request(url, payload, headers)

View File

@ -0,0 +1,84 @@
from app.classes.web.webhooks.discord_webhook import DiscordWebhook
from app.classes.web.webhooks.mattermost_webhook import MattermostWebhook
from app.classes.web.webhooks.slack_webhook import SlackWebhook
from app.classes.web.webhooks.teams_adaptive_webhook import TeamsWebhook
class WebhookFactory:
"""
A factory class responsible for the creation and management of webhook providers.
This class provides methods to instantiate specific webhook providers based on
their name and to retrieve a list of supported providers. It uses a registry pattern
to manage the available providers.
Attributes:
- _registry (dict): A dictionary mapping provider names to their classes.
"""
_registry = {
"Discord": DiscordWebhook,
"Mattermost": MattermostWebhook,
"Slack": SlackWebhook,
"Teams": TeamsWebhook,
}
@classmethod
def create_provider(cls, provider_name, *args, **kwargs):
"""
Creates and returns an instance of the specified webhook provider.
This method looks up the provider in the registry, then instantiates it w/ the
provided arguments. If the provider is not recognized, a ValueError is raised.
Arguments:
- provider_name (str): The name of the desired webhook provider.
Additional arguments supported that we may use for if a provider
requires initialization:
- *args: Positional arguments to pass to the provider's constructor.
- **kwargs: Keyword arguments to pass to the provider's constructor.
Returns:
WebhookProvider: An instance of the desired webhook provider.
Raises:
ValueError: If the specified provider name is not recognized.
"""
if provider_name not in cls._registry:
raise ValueError(f"Provider {provider_name} is not supported.")
return cls._registry[provider_name](*args, **kwargs)
@classmethod
def get_supported_providers(cls):
"""
Retrieves the names of all supported webhook providers.
This method returns a list containing the names of all providers
currently registered in the factory's registry.
Returns:
List[str]: A list of supported provider names.
"""
return list(cls._registry.keys())
@staticmethod
def get_monitored_events():
"""
Retrieves the list of supported events for monitoring.
This method provides a list of common server events that the webhook system can
monitor and notify about.
Returns:
List[str]: A list of supported monitored actions.
"""
return [
"start_server",
"stop_server",
"crash_detected",
"backup_server",
"jar_update",
"send_command",
"kill",
]

View File

@ -4,15 +4,17 @@ import asyncio
from urllib.parse import parse_qsl
import tornado.websocket
from app.classes.shared.main_controller import Controller
from app.classes.shared.helpers import Helpers
from app.classes.shared.websocket_manager import WebSocketManager
logger = logging.getLogger(__name__)
class SocketHandler(tornado.websocket.WebSocketHandler):
class WebSocketHandler(tornado.websocket.WebSocketHandler):
page = None
page_query_params = None
controller = None
controller: Controller = None
tasks_manager = None
translator = None
io_loop = None
@ -40,23 +42,16 @@ class SocketHandler(tornado.websocket.WebSocketHandler):
)
return remote_ip
def get_user_id(self):
_, _, user = self.controller.authentication.check(self.get_cookie("token"))
return user["user_id"]
def check_auth(self):
return self.controller.authentication.check_bool(self.get_cookie("token"))
# pylint: disable=arguments-differ
def open(self):
logger.debug("Checking WebSocket authentication")
if self.check_auth():
self.handle()
else:
self.helper.websocket_helper.send_message(
WebSocketManager().broadcast_to_admins(
self, "notification", "Not authenticated for WebSocket connection"
)
self.close()
self.close(1011, "Forbidden WS Access")
self.controller.management.add_to_audit_log_raw(
"unknown",
0,
@ -64,7 +59,7 @@ class SocketHandler(tornado.websocket.WebSocketHandler):
"Someone tried to connect via WebSocket without proper authentication",
self.get_remote_ip(),
)
self.helper.websocket_helper.broadcast(
WebSocketManager().broadcast(
"notification",
"Someone tried to connect via WebSocket without proper authentication",
)
@ -79,24 +74,34 @@ class SocketHandler(tornado.websocket.WebSocketHandler):
Helpers.remove_prefix(self.get_query_argument("page_query_params"), "?")
)
)
self.helper.websocket_helper.add_client(self)
WebSocketManager().add_client(self)
logger.debug("Opened WebSocket connection")
# pylint: disable=arguments-renamed
@staticmethod
def on_message(raw_message):
def on_message(self, raw_message):
logger.debug(f"Got message from WebSocket connection {raw_message}")
message = json.loads(raw_message)
logger.debug(f"Event Type: {message['event']}, Data: {message['data']}")
def on_close(self):
self.helper.websocket_helper.remove_client(self)
WebSocketManager().remove_client(self)
logger.debug("Closed WebSocket connection")
async def write_message_int(self, message):
self.write_message(message)
def write_message_helper(self, message):
def write_message_async(self, message):
asyncio.run_coroutine_threadsafe(
self.write_message_int(message), self.io_loop.asyncio_loop
)
def send_message(self, event_type: str, data):
message = str(json.dumps({"event": event_type, "data": data}))
self.write_message_async(message)
def get_user_id(self):
_, _, user = self.controller.authentication.check(self.get_cookie("token"))
return user["user_id"]
def check_auth(self):
return self.controller.authentication.check_bool(self.get_cookie("token"))

View File

@ -10,16 +10,17 @@
},
"schedule": {
"format": "%(asctime)s - [Schedules] - %(levelname)s - %(message)s"
},
"auth": {
"format": "%(asctime)s - [AUTH] - %(levelname)s - %(message)s"
}
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "commander",
"stream": "ext://sys.stdout"
},
"main_file_handler": {
"class": "logging.handlers.RotatingFileHandler",
"formatter": "commander",
@ -50,24 +51,45 @@
"maxBytes": 10485760,
"backupCount": 20,
"encoding": "utf8"
},
"auth_file_handler": {
"class": "logging.handlers.RotatingFileHandler",
"formatter": "auth",
"filename": "logs/auth.log",
"maxBytes": 10485760,
"backupCount": 20,
"encoding": "utf8"
}
},
"loggers": {
"": {
"level": "INFO",
"handlers": ["main_file_handler", "session_file_handler"],
"handlers": [
"main_file_handler",
"session_file_handler"
],
"propagate": false
},
"tornado.access": {
"level": "INFO",
"handlers": ["tornado_access_file_handler"],
"handlers": [
"tornado_access_file_handler"
],
"propagate": false
},
"apscheduler": {
"level": "INFO",
"handlers": ["schedule_file_handler"],
"handlers": [
"schedule_file_handler"
],
"propagate": false
},
"auth": {
"level": "INFO",
"handlers": [
"auth_file_handler"
],
"propagate": false
}
}
}
}

View File

@ -1,5 +1,5 @@
{
"major": 4,
"minor": 2,
"sub": 0
"sub": 2
}

View File

@ -0,0 +1,447 @@
/**************************************************************/
/* CSS for Toggle Buttons */
/**************************************************************/
.btn-toggle {
margin: 0 4rem;
padding: 0;
position: relative;
border: none;
height: 1.5rem;
width: 3rem;
border-radius: 1.5rem;
color: #6b7381;
background: #bdc1c8;
}
.btn-toggle:focus,
.btn-toggle.focus,
.btn-toggle:focus.active,
.btn-toggle.focus.active {
outline: none;
}
.btn-toggle:before,
.btn-toggle:after {
line-height: 1.5rem;
width: 4rem;
text-align: center;
font-weight: 600;
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 2px;
position: absolute;
bottom: 0;
transition: opacity 0.25s;
}
.btn-toggle:before {
content: 'Off';
left: -4rem;
}
.btn-toggle:after {
content: 'On';
right: -4rem;
opacity: 0.5;
}
.btn-toggle>.handle {
position: absolute;
top: 0.1875rem;
left: 0.1875rem;
width: 1.125rem;
height: 1.125rem;
border-radius: 1.125rem;
background: #fff;
transition: left 0.25s;
}
.btn-toggle.active {
transition: background-color 0.25s;
}
.btn-toggle.active>.handle {
left: 1.6875rem;
transition: left 0.25s;
}
.btn-toggle.active:before {
opacity: 0.5;
}
.btn-toggle.active:after {
opacity: 1;
}
.btn-toggle.btn-sm:before,
.btn-toggle.btn-sm:after {
line-height: -0.5rem;
color: #fff;
letter-spacing: 0.75px;
left: 0.4125rem;
width: 2.325rem;
}
.btn-toggle.btn-sm:before {
text-align: right;
}
.btn-toggle.btn-sm:after {
text-align: left;
opacity: 0;
}
.btn-toggle.btn-sm.active:before {
opacity: 0;
}
.btn-toggle.btn-sm.active:after {
opacity: 1;
}
.btn-toggle.btn-xs:before,
.btn-toggle.btn-xs:after {
display: none;
}
.btn-toggle:before,
.btn-toggle:after {
color: #6b7381;
}
.btn-toggle.active {
background-color: #29b5a8;
}
.btn-toggle.btn-lg {
margin: 0 5rem;
padding: 0;
position: relative;
border: none;
height: 2.5rem;
width: 5rem;
border-radius: 2.5rem;
}
.btn-toggle.btn-lg:focus,
.btn-toggle.btn-lg.focus,
.btn-toggle.btn-lg:focus.active,
.btn-toggle.btn-lg.focus.active {
outline: none;
}
.btn-toggle.btn-lg:before,
.btn-toggle.btn-lg:after {
line-height: 2.5rem;
width: 5rem;
text-align: center;
font-weight: 600;
font-size: 1rem;
text-transform: uppercase;
letter-spacing: 2px;
position: absolute;
bottom: 0;
transition: opacity 0.25s;
}
.btn-toggle.btn-lg:before {
content: 'Off';
left: -5rem;
}
.btn-toggle.btn-lg:after {
content: 'On';
right: -5rem;
opacity: 0.5;
}
.btn-toggle.btn-lg>.handle {
position: absolute;
top: 0.3125rem;
left: 0.3125rem;
width: 1.875rem;
height: 1.875rem;
border-radius: 1.875rem;
background: #fff;
transition: left 0.25s;
}
.btn-toggle.btn-lg.active {
transition: background-color 0.25s;
}
.btn-toggle.btn-lg.active>.handle {
left: 2.8125rem;
transition: left 0.25s;
}
.btn-toggle.btn-lg.active:before {
opacity: 0.5;
}
.btn-toggle.btn-lg.active:after {
opacity: 1;
}
.btn-toggle.btn-lg.btn-sm:before,
.btn-toggle.btn-lg.btn-sm:after {
line-height: 0.5rem;
color: #fff;
letter-spacing: 0.75px;
left: 0.6875rem;
width: 3.875rem;
}
.btn-toggle.btn-lg.btn-sm:before {
text-align: right;
}
.btn-toggle.btn-lg.btn-sm:after {
text-align: left;
opacity: 0;
}
.btn-toggle.btn-lg.btn-sm.active:before {
opacity: 0;
}
.btn-toggle.btn-lg.btn-sm.active:after {
opacity: 1;
}
.btn-toggle.btn-lg.btn-xs:before,
.btn-toggle.btn-lg.btn-xs:after {
display: none;
}
.btn-toggle.btn-sm {
margin: 0 0.5rem;
padding: 0;
position: relative;
border: none;
height: 1.5rem;
width: 3rem;
border-radius: 1.5rem;
}
.btn-toggle.btn-sm:focus,
.btn-toggle.btn-sm.focus,
.btn-toggle.btn-sm:focus.active,
.btn-toggle.btn-sm.focus.active {
outline: none;
}
.btn-toggle.btn-sm:before,
.btn-toggle.btn-sm:after {
line-height: 1.5rem;
width: 0.5rem;
text-align: center;
font-weight: 600;
font-size: 0.55rem;
text-transform: uppercase;
letter-spacing: 2px;
position: absolute;
bottom: 0;
transition: opacity 0.25s;
}
.btn-toggle.btn-sm:before {
content: 'Off';
left: -0.5rem;
}
.btn-toggle.btn-sm:after {
content: 'On';
right: -0.5rem;
opacity: 0.5;
}
.btn-toggle.btn-sm>.handle {
position: absolute;
top: 0.1875rem;
left: 0.1875rem;
width: 1.125rem;
height: 1.125rem;
border-radius: 1.125rem;
background: #fff;
transition: left 0.25s;
}
.btn-toggle.btn-sm.active {
transition: background-color 0.25s;
}
.btn-toggle.btn-sm.active>.handle {
left: 1.6875rem;
transition: left 0.25s;
}
.btn-toggle.btn-sm.active:before {
opacity: 0.5;
}
.btn-toggle.btn-sm.active:after {
opacity: 1;
}
.btn-toggle.btn-sm.btn-sm:before,
.btn-toggle.btn-sm.btn-sm:after {
line-height: -0.5rem;
color: #fff;
letter-spacing: 0.75px;
left: 0.4125rem;
width: 2.325rem;
}
.btn-toggle.btn-sm.btn-sm:before {
text-align: right;
}
.btn-toggle.btn-sm.btn-sm:after {
text-align: left;
opacity: 0;
}
.btn-toggle.btn-sm.btn-sm.active:before {
opacity: 0;
}
.btn-toggle.btn-sm.btn-sm.active:after {
opacity: 1;
}
.btn-toggle.btn-sm.btn-xs:before,
.btn-toggle.btn-sm.btn-xs:after {
display: none;
}
.btn-toggle.btn-xs {
margin: 0 0;
padding: 0;
position: relative;
border: none;
height: 1rem;
width: 2rem;
border-radius: 1rem;
}
.btn-toggle.btn-xs:focus,
.btn-toggle.btn-xs.focus,
.btn-toggle.btn-xs:focus.active,
.btn-toggle.btn-xs.focus.active {
outline: none;
}
.btn-toggle.btn-xs:before,
.btn-toggle.btn-xs:after {
line-height: 1rem;
width: 0;
text-align: center;
font-weight: 600;
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 2px;
position: absolute;
bottom: 0;
transition: opacity 0.25s;
}
.btn-toggle.btn-xs:before {
content: 'Off';
left: 0;
}
.btn-toggle.btn-xs:after {
content: 'On';
right: 0;
opacity: 0.5;
}
.btn-toggle.btn-xs>.handle {
position: absolute;
top: 0.125rem;
left: 0.125rem;
width: 0.75rem;
height: 0.75rem;
border-radius: 0.75rem;
background: #fff;
transition: left 0.25s;
}
.btn-toggle.btn-xs.active {
transition: background-color 0.25s;
}
.btn-toggle.btn-xs.active>.handle {
left: 1.125rem;
transition: left 0.25s;
}
.btn-toggle.btn-xs.active:before {
opacity: 0.5;
}
.btn-toggle.btn-xs.active:after {
opacity: 1;
}
.btn-toggle.btn-xs.btn-sm:before,
.btn-toggle.btn-xs.btn-sm:after {
line-height: -1rem;
color: #fff;
letter-spacing: 0.75px;
left: 0.275rem;
width: 1.55rem;
}
.btn-toggle.btn-xs.btn-sm:before {
text-align: right;
}
.btn-toggle.btn-xs.btn-sm:after {
text-align: left;
opacity: 0;
}
.btn-toggle.btn-xs.btn-sm.active:before {
opacity: 0;
}
.btn-toggle.btn-xs.btn-sm.active:after {
opacity: 1;
}
.btn-toggle.btn-xs.btn-xs:before,
.btn-toggle.btn-xs.btn-xs:after {
display: none;
}
.btn-toggle.btn-info {
color: var(--white);
background: var(--gray);
}
.btn-toggle.btn-info:before,
.btn-toggle.btn-info:after {
color: #6b7381;
}
.btn-toggle.btn-info.active {
background-color: var(--info);
}
.btn-toggle.btn-secondary {
color: #6b7381;
background: #bdc1c8;
}
.btn-toggle.btn-secondary:before,
.btn-toggle.btn-secondary:after {
color: #6b7381;
}
.btn-toggle.btn-secondary.active {
background-color: #ff8300;
}
/**************************************************************/

View File

@ -182,11 +182,55 @@ div>.input-group>.form-control {
}
.no-scroll {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none;
/* IE and Edge */
scrollbar-width: none;
/* Firefox */
}
.no-scroll::-webkit-scrollbar {
display: none;
}
}
.custom-control-label::before,
.custom-control-label::after {
cursor: pointer;
}
.custom-control-input:checked~.custom-control-label::before {
color: black !important;
background-color: blueviolet !important;
border-color: var(--outline) !important;
}
.custom-control-label::before {
background-color: white !important;
top: calc(-0.2rem);
}
.custom-switch .custom-control-label::after {
top: calc(-0.125rem + 1px);
}
/**************************************************************/
/**************************************************************/
/* CSS for Tables Displays */
/**************************************************************/
td>ul {
margin: auto;
}
td p {
margin: auto;
}
td.action {
white-space: normal;
}
td.action .btn {
margin-bottom: 0.2rem;
}
/**************************************************************/

View File

@ -22979,27 +22979,42 @@ ul li {
padding-left: 0;
}
.tab-simple-styled {
.nav-tabs.tab-simple-styled {
border-bottom: none;
margin-top: 30px;
margin-bottom: 30px;
}
.tab-simple-styled .nav-item {
.nav-tabs.tab-simple-styled .nav-item {
margin-right: 30px;
}
.tab-simple-styled .nav-item .nav-link {
.nav-tabs.tab-simple-styled .nav-item .nav-link {
border: none;
padding: 0;
color: var(--base-text);
}
.tab-simple-styled .nav-item .nav-link.active {
.nav-tabs.tab-simple-styled .nav-item .nav-link.active {
background: var(--dropdown-bg);
color: var(--info);
}
.nav-pills.tab-simple-styled {
border-bottom: none;
/*margin-top: 1.5rem;*/
margin-bottom: 1.5rem;
}
/*.nav-pills.tab-simple-styled .nav-item {
margin-right: 1.5rem;
}*/
.nav-pills.tab-simple-styled .nav-item .nav-link.active {
background: var(--info);
color: #ffffff;
}
.tab-tile-style {
display: -webkit-box;
display: -ms-flexbox;

View File

@ -21494,27 +21494,42 @@ ul li {
padding-left: 0;
}
.tab-simple-styled {
.nav-tabs.tab-simple-styled {
border-bottom: none;
margin-top: 30px;
margin-bottom: 30px;
}
.tab-simple-styled .nav-item {
.nav-tabs.tab-simple-styled .nav-item {
margin-right: 30px;
}
.tab-simple-styled .nav-item .nav-link {
.nav-tabs.tab-simple-styled .nav-item .nav-link {
border: none;
padding: 0;
color: var(--gray);
}
.tab-simple-styled .nav-item .nav-link.active {
.nav-tabs.tab-simple-styled .nav-item .nav-link.active {
background: #fff;
color: var(--info);
}
.nav-pills.tab-simple-styled {
border-bottom: none;
/*margin-top: 1.5rem;*/
margin-bottom: 1.5rem;
}
/*.nav-pills.tab-simple-styled .nav-item {
margin-right: 1.5rem;
}*/
.nav-pills.tab-simple-styled .nav-item .nav-link.active {
background: var(--info);
color: #ffffff;
}
.tab-tile-style {
display: -webkit-box;
display: -ms-flexbox;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 424 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 425 425">
<!-- Background -->
<rect x="0" y="0" rx="105" ry="105" width="425" height="425" style="fill: #22283A"/>
<!-- green C -->
<path fill="hsl(160, 59%, 45%)" d="M 162.5 182.5 l 38 -15 q 0 -40 -40 -40 l -95 0 q -40 0 -40 40 l 0 90 q 0 40 40 40 l 95 0 q 40 0 40 -40 l -38 -20 l 0 28 l -100 0 l 0 -108 l 100 0 z"/>
<!-- blue C -->
<path fill="hsl(192, 99%, 45%)" d="M 262.5 182.5 l -38 -15 q 0 -40 40 -40 l 95 0 q 40 0 40 40 l 0 90 q 0 40 -40 40 l -95 0 q -40 0 -40 -40 l 38 -20 l 0 28 l 100 0 l 0 -108 l -100 0 z"/>
<!-- line thing in middle of Cs -->
<path fill="hsl(266, 71%, 57%)" d="M 142.5 191.5 q -10 0 -10 10 l 0 19 q 0 10 10 10 l 140 0 q 10 0 10 -10 l 0 -19 q 0 -10 -10 -10 z"/>
</svg>

After

Width:  |  Height:  |  Size: 750 B

View File

@ -6,36 +6,9 @@ importScripts(
const CACHE = "crafty-controller";
// TODO: replace the following with the correct offline fallback page i.e.: const offlineFallbackPage = "offline.html";
const offlineFallbackPage = "/offline";
self.addEventListener("message", (event) => {
if (event.data && event.data.type === "SKIP_WAITING") {
self.skipWaiting();
}
});
//This service worker is basically just here to make browsers
//accept the PWA. It's not doing much anymore
if (workbox.navigationPreload.isSupported()) {
workbox.navigationPreload.enable();
}
// self.addEventListener('fetch', (event) => {
// if (event.request.mode === 'navigate') {
// event.respondWith((async () => {
// try {
// const preloadResp = await event.preloadResponse;
// if (preloadResp) {
// return preloadResp;
// }
// const networkResp = await fetch(event.request);
// return networkResp;
// } catch (error) {
// const cache = await caches.open(CACHE);
// const cachedResp = await cache.match(offlineFallbackPage);
// return cachedResp;
// }
// })());
// }
// });

View File

@ -0,0 +1,28 @@
/*! ========================================================================
* Bootstrap Toggle: bootstrap-toggle.css v2.2.0
* http://www.bootstraptoggle.com
* ========================================================================
* Copyright 2014 Min Hur, The New York Times Company
* Licensed under MIT
* ======================================================================== */
.checkbox label .toggle,.checkbox-inline .toggle{margin-left:-20px;margin-right:5px}
.toggle{position:relative;overflow:hidden}
.toggle input[type=checkbox]{display:none}
.toggle-group{position:absolute;width:200%;top:0;bottom:0;left:0;transition:left .35s;-webkit-transition:left .35s;-moz-user-select:none;-webkit-user-select:none}
.toggle.off .toggle-group{left:-100%}
.toggle-on{position:absolute;top:0;bottom:0;left:0;right:50%;margin:0;border:0;border-radius:0}
.toggle-off{position:absolute;top:0;bottom:0;left:50%;right:0;margin:0;border:0;border-radius:0}
.toggle-handle{position:relative;margin:0 auto;padding-top:0;padding-bottom:0;height:100%;width:0;border-width:0 1px}
.toggle.btn{min-width:59px;min-height:34px}
.toggle-on.btn{padding-right:24px}
.toggle-off.btn{padding-left:24px}
.toggle.btn-lg{min-width:79px;min-height:45px}
.toggle-on.btn-lg{padding-right:31px}
.toggle-off.btn-lg{padding-left:31px}
.toggle-handle.btn-lg{width:40px}
.toggle.btn-sm{min-width:50px;min-height:30px}
.toggle-on.btn-sm{padding-right:20px}
.toggle-off.btn-sm{padding-left:20px}
.toggle.btn-xs{min-width:35px;min-height:22px}
.toggle-on.btn-xs{padding-right:12px}
.toggle-off.btn-xs{padding-left:12px}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,9 @@
/*! ========================================================================
* Bootstrap Toggle: bootstrap-toggle.js v2.2.0
* http://www.bootstraptoggle.com
* ========================================================================
* Copyright 2014 Min Hur, The New York Times Company
* Licensed under MIT
* ======================================================================== */
+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.toggle"),f="object"==typeof b&&b;e||d.data("bs.toggle",e=new c(this,f)),"string"==typeof b&&e[b]&&e[b]()})}var c=function(b,c){this.$element=a(b),this.options=a.extend({},this.defaults(),c),this.render()};c.VERSION="2.2.0",c.DEFAULTS={on:"On",off:"Off",onstyle:"primary",offstyle:"default",size:"normal",style:"",width:null,height:null},c.prototype.defaults=function(){return{on:this.$element.attr("data-on")||c.DEFAULTS.on,off:this.$element.attr("data-off")||c.DEFAULTS.off,onstyle:this.$element.attr("data-onstyle")||c.DEFAULTS.onstyle,offstyle:this.$element.attr("data-offstyle")||c.DEFAULTS.offstyle,size:this.$element.attr("data-size")||c.DEFAULTS.size,style:this.$element.attr("data-style")||c.DEFAULTS.style,width:this.$element.attr("data-width")||c.DEFAULTS.width,height:this.$element.attr("data-height")||c.DEFAULTS.height}},c.prototype.render=function(){this._onstyle="btn-"+this.options.onstyle,this._offstyle="btn-"+this.options.offstyle;var b="large"===this.options.size?"btn-lg":"small"===this.options.size?"btn-sm":"mini"===this.options.size?"btn-xs":"",c=a('<label class="btn">').html(this.options.on).addClass(this._onstyle+" "+b),d=a('<label class="btn">').html(this.options.off).addClass(this._offstyle+" "+b+" active"),e=a('<span class="toggle-handle btn btn-default">').addClass(b),f=a('<div class="toggle-group">').append(c,d,e),g=a('<div class="toggle btn" data-toggle="toggle">').addClass(this.$element.prop("checked")?this._onstyle:this._offstyle+" off").addClass(b).addClass(this.options.style);this.$element.wrap(g),a.extend(this,{$toggle:this.$element.parent(),$toggleOn:c,$toggleOff:d,$toggleGroup:f}),this.$toggle.append(f);var h=this.options.width||Math.max(c.outerWidth(),d.outerWidth())+e.outerWidth()/2,i=this.options.height||Math.max(c.outerHeight(),d.outerHeight());c.addClass("toggle-on"),d.addClass("toggle-off"),this.$toggle.css({width:h,height:i}),this.options.height&&(c.css("line-height",c.height()+"px"),d.css("line-height",d.height()+"px")),this.update(!0),this.trigger(!0)},c.prototype.toggle=function(){this.$element.prop("checked")?this.off():this.on()},c.prototype.on=function(a){return this.$element.prop("disabled")?!1:(this.$toggle.removeClass(this._offstyle+" off").addClass(this._onstyle),this.$element.prop("checked",!0),void(a||this.trigger()))},c.prototype.off=function(a){return this.$element.prop("disabled")?!1:(this.$toggle.removeClass(this._onstyle).addClass(this._offstyle+" off"),this.$element.prop("checked",!1),void(a||this.trigger()))},c.prototype.enable=function(){this.$toggle.removeAttr("disabled"),this.$element.prop("disabled",!1)},c.prototype.disable=function(){this.$toggle.attr("disabled","disabled"),this.$element.prop("disabled",!0)},c.prototype.update=function(a){this.$element.prop("disabled")?this.disable():this.enable(),this.$element.prop("checked")?this.on(a):this.off(a)},c.prototype.trigger=function(b){this.$element.off("change.bs.toggle"),b||this.$element.change(),this.$element.on("change.bs.toggle",a.proxy(function(){this.update()},this))},c.prototype.destroy=function(){this.$element.off("change.bs.toggle"),this.$toggleGroup.remove(),this.$element.removeData("bs.toggle"),this.$element.unwrap()};var d=a.fn.bootstrapToggle;a.fn.bootstrapToggle=b,a.fn.bootstrapToggle.Constructor=c,a.fn.toggle.noConflict=function(){return a.fn.bootstrapToggle=d,this},a(function(){a("input[type=checkbox][data-toggle^=toggle]").bootstrapToggle()}),a(document).on("click.bs.toggle","div[data-toggle^=toggle]",function(b){var c=a(this).find("input[type=checkbox]");c.bootstrapToggle("toggle"),b.preventDefault()})}(jQuery);
//# sourceMappingURL=bootstrap-toggle.min.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,491 @@
/*
* This combined file was created by the DataTables downloader builder:
* https://datatables.net/download
*
* To rebuild or modify this file with the latest versions of the included
* software please visit:
* https://datatables.net/download/#bs4/dt-1.10.22/fh-3.1.7/r-2.2.6/sc-2.0.3/sp-1.2.2
*
* Included libraries:
* DataTables 1.10.22, FixedHeader 3.1.7, Responsive 2.2.6, Scroller 2.0.3, SearchPanes 1.2.2
*/
/*!
Copyright 2008-2020 SpryMedia Ltd.
This source file is free software, available under the following license:
MIT license - http://datatables.net/license
This source file is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
or FITNESS FOR A PARTICULAR PURPOSE. See the license files for details.
For details please refer to: http://www.datatables.net
DataTables 1.10.22
©2008-2020 SpryMedia Ltd - datatables.net/license
*/
var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.findInternal=function(k,y,z){k instanceof String&&(k=String(k));for(var q=k.length,G=0;G<q;G++){var O=k[G];if(y.call(z,O,G,k))return{i:G,v:O}}return{i:-1,v:void 0}};$jscomp.ASSUME_ES5=!1;$jscomp.ASSUME_NO_NATIVE_MAP=!1;$jscomp.ASSUME_NO_NATIVE_SET=!1;$jscomp.SIMPLE_FROUND_POLYFILL=!1;$jscomp.ISOLATE_POLYFILLS=!1;
$jscomp.defineProperty=$jscomp.ASSUME_ES5||"function"==typeof Object.defineProperties?Object.defineProperty:function(k,y,z){if(k==Array.prototype||k==Object.prototype)return k;k[y]=z.value;return k};$jscomp.getGlobal=function(k){k=["object"==typeof globalThis&&globalThis,k,"object"==typeof window&&window,"object"==typeof self&&self,"object"==typeof global&&global];for(var y=0;y<k.length;++y){var z=k[y];if(z&&z.Math==Math)return z}throw Error("Cannot find global object");};$jscomp.global=$jscomp.getGlobal(this);
$jscomp.IS_SYMBOL_NATIVE="function"===typeof Symbol&&"symbol"===typeof Symbol("x");$jscomp.TRUST_ES6_POLYFILLS=!$jscomp.ISOLATE_POLYFILLS||$jscomp.IS_SYMBOL_NATIVE;$jscomp.polyfills={};$jscomp.propertyToPolyfillSymbol={};$jscomp.POLYFILL_PREFIX="$jscp$";var $jscomp$lookupPolyfilledValue=function(k,y){var z=$jscomp.propertyToPolyfillSymbol[y];if(null==z)return k[y];z=k[z];return void 0!==z?z:k[y]};
$jscomp.polyfill=function(k,y,z,q){y&&($jscomp.ISOLATE_POLYFILLS?$jscomp.polyfillIsolated(k,y,z,q):$jscomp.polyfillUnisolated(k,y,z,q))};$jscomp.polyfillUnisolated=function(k,y,z,q){z=$jscomp.global;k=k.split(".");for(q=0;q<k.length-1;q++){var G=k[q];if(!(G in z))return;z=z[G]}k=k[k.length-1];q=z[k];y=y(q);y!=q&&null!=y&&$jscomp.defineProperty(z,k,{configurable:!0,writable:!0,value:y})};
$jscomp.polyfillIsolated=function(k,y,z,q){var G=k.split(".");k=1===G.length;q=G[0];q=!k&&q in $jscomp.polyfills?$jscomp.polyfills:$jscomp.global;for(var O=0;O<G.length-1;O++){var ma=G[O];if(!(ma in q))return;q=q[ma]}G=G[G.length-1];z=$jscomp.IS_SYMBOL_NATIVE&&"es6"===z?q[G]:null;y=y(z);null!=y&&(k?$jscomp.defineProperty($jscomp.polyfills,G,{configurable:!0,writable:!0,value:y}):y!==z&&($jscomp.propertyToPolyfillSymbol[G]=$jscomp.IS_SYMBOL_NATIVE?$jscomp.global.Symbol(G):$jscomp.POLYFILL_PREFIX+G,
G=$jscomp.propertyToPolyfillSymbol[G],$jscomp.defineProperty(q,G,{configurable:!0,writable:!0,value:y})))};$jscomp.polyfill("Array.prototype.find",function(k){return k?k:function(y,z){return $jscomp.findInternal(this,y,z).v}},"es6","es3");
(function(k){"function"===typeof define&&define.amd?define(["jquery"],function(y){return k(y,window,document)}):"object"===typeof exports?module.exports=function(y,z){y||(y=window);z||(z="undefined"!==typeof window?require("jquery"):require("jquery")(y));return k(z,y,y.document)}:k(jQuery,window,document)})(function(k,y,z,q){function G(a){var b,c,d={};k.each(a,function(f,e){(b=f.match(/^([^A-Z]+?)([A-Z])/))&&-1!=="a aa ai ao as b fn i m o s ".indexOf(b[1]+" ")&&(c=f.replace(b[0],b[2].toLowerCase()),
d[c]=f,"o"===b[1]&&G(a[f]))});a._hungarianMap=d}function O(a,b,c){a._hungarianMap||G(a);var d;k.each(b,function(f,e){d=a._hungarianMap[f];d===q||!c&&b[d]!==q||("o"===d.charAt(0)?(b[d]||(b[d]={}),k.extend(!0,b[d],b[f]),O(a[d],b[d],c)):b[d]=b[f])})}function ma(a){var b=u.defaults.oLanguage,c=b.sDecimal;c&&Va(c);if(a){var d=a.sZeroRecords;!a.sEmptyTable&&d&&"No data available in table"===b.sEmptyTable&&V(a,a,"sZeroRecords","sEmptyTable");!a.sLoadingRecords&&d&&"Loading..."===b.sLoadingRecords&&V(a,a,
"sZeroRecords","sLoadingRecords");a.sInfoThousands&&(a.sThousands=a.sInfoThousands);(a=a.sDecimal)&&c!==a&&Va(a)}}function yb(a){R(a,"ordering","bSort");R(a,"orderMulti","bSortMulti");R(a,"orderClasses","bSortClasses");R(a,"orderCellsTop","bSortCellsTop");R(a,"order","aaSorting");R(a,"orderFixed","aaSortingFixed");R(a,"paging","bPaginate");R(a,"pagingType","sPaginationType");R(a,"pageLength","iDisplayLength");R(a,"searching","bFilter");"boolean"===typeof a.sScrollX&&(a.sScrollX=a.sScrollX?"100%":
"");"boolean"===typeof a.scrollX&&(a.scrollX=a.scrollX?"100%":"");if(a=a.aoSearchCols)for(var b=0,c=a.length;b<c;b++)a[b]&&O(u.models.oSearch,a[b])}function zb(a){R(a,"orderable","bSortable");R(a,"orderData","aDataSort");R(a,"orderSequence","asSorting");R(a,"orderDataType","sortDataType");var b=a.aDataSort;"number"!==typeof b||Array.isArray(b)||(a.aDataSort=[b])}function Ab(a){if(!u.__browser){var b={};u.__browser=b;var c=k("<div/>").css({position:"fixed",top:0,left:-1*k(y).scrollLeft(),height:1,
width:1,overflow:"hidden"}).append(k("<div/>").css({position:"absolute",top:1,left:1,width:100,overflow:"scroll"}).append(k("<div/>").css({width:"100%",height:10}))).appendTo("body"),d=c.children(),f=d.children();b.barWidth=d[0].offsetWidth-d[0].clientWidth;b.bScrollOversize=100===f[0].offsetWidth&&100!==d[0].clientWidth;b.bScrollbarLeft=1!==Math.round(f.offset().left);b.bBounding=c[0].getBoundingClientRect().width?!0:!1;c.remove()}k.extend(a.oBrowser,u.__browser);a.oScroll.iBarWidth=u.__browser.barWidth}
function Bb(a,b,c,d,f,e){var g=!1;if(c!==q){var h=c;g=!0}for(;d!==f;)a.hasOwnProperty(d)&&(h=g?b(h,a[d],d,a):a[d],g=!0,d+=e);return h}function Wa(a,b){var c=u.defaults.column,d=a.aoColumns.length;c=k.extend({},u.models.oColumn,c,{nTh:b?b:z.createElement("th"),sTitle:c.sTitle?c.sTitle:b?b.innerHTML:"",aDataSort:c.aDataSort?c.aDataSort:[d],mData:c.mData?c.mData:d,idx:d});a.aoColumns.push(c);c=a.aoPreSearchCols;c[d]=k.extend({},u.models.oSearch,c[d]);Da(a,d,k(b).data())}function Da(a,b,c){b=a.aoColumns[b];
var d=a.oClasses,f=k(b.nTh);if(!b.sWidthOrig){b.sWidthOrig=f.attr("width")||null;var e=(f.attr("style")||"").match(/width:\s*(\d+[pxem%]+)/);e&&(b.sWidthOrig=e[1])}c!==q&&null!==c&&(zb(c),O(u.defaults.column,c,!0),c.mDataProp===q||c.mData||(c.mData=c.mDataProp),c.sType&&(b._sManualType=c.sType),c.className&&!c.sClass&&(c.sClass=c.className),c.sClass&&f.addClass(c.sClass),k.extend(b,c),V(b,c,"sWidth","sWidthOrig"),c.iDataSort!==q&&(b.aDataSort=[c.iDataSort]),V(b,c,"aDataSort"));var g=b.mData,h=ia(g),
l=b.mRender?ia(b.mRender):null;c=function(n){return"string"===typeof n&&-1!==n.indexOf("@")};b._bAttrSrc=k.isPlainObject(g)&&(c(g.sort)||c(g.type)||c(g.filter));b._setter=null;b.fnGetData=function(n,m,p){var t=h(n,m,q,p);return l&&m?l(t,m,n,p):t};b.fnSetData=function(n,m,p){return da(g)(n,m,p)};"number"!==typeof g&&(a._rowReadObject=!0);a.oFeatures.bSort||(b.bSortable=!1,f.addClass(d.sSortableNone));a=-1!==k.inArray("asc",b.asSorting);c=-1!==k.inArray("desc",b.asSorting);b.bSortable&&(a||c)?a&&!c?
(b.sSortingClass=d.sSortableAsc,b.sSortingClassJUI=d.sSortJUIAscAllowed):!a&&c?(b.sSortingClass=d.sSortableDesc,b.sSortingClassJUI=d.sSortJUIDescAllowed):(b.sSortingClass=d.sSortable,b.sSortingClassJUI=d.sSortJUI):(b.sSortingClass=d.sSortableNone,b.sSortingClassJUI="")}function ra(a){if(!1!==a.oFeatures.bAutoWidth){var b=a.aoColumns;Xa(a);for(var c=0,d=b.length;c<d;c++)b[c].nTh.style.width=b[c].sWidth}b=a.oScroll;""===b.sY&&""===b.sX||Ea(a);I(a,null,"column-sizing",[a])}function sa(a,b){a=Fa(a,"bVisible");
return"number"===typeof a[b]?a[b]:null}function ta(a,b){a=Fa(a,"bVisible");b=k.inArray(b,a);return-1!==b?b:null}function na(a){var b=0;k.each(a.aoColumns,function(c,d){d.bVisible&&"none"!==k(d.nTh).css("display")&&b++});return b}function Fa(a,b){var c=[];k.map(a.aoColumns,function(d,f){d[b]&&c.push(f)});return c}function Ya(a){var b=a.aoColumns,c=a.aoData,d=u.ext.type.detect,f,e,g;var h=0;for(f=b.length;h<f;h++){var l=b[h];var n=[];if(!l.sType&&l._sManualType)l.sType=l._sManualType;else if(!l.sType){var m=
0;for(e=d.length;m<e;m++){var p=0;for(g=c.length;p<g;p++){n[p]===q&&(n[p]=S(a,p,h,"type"));var t=d[m](n[p],a);if(!t&&m!==d.length-1)break;if("html"===t)break}if(t){l.sType=t;break}}l.sType||(l.sType="string")}}}function Cb(a,b,c,d){var f,e,g,h=a.aoColumns;if(b)for(f=b.length-1;0<=f;f--){var l=b[f];var n=l.targets!==q?l.targets:l.aTargets;Array.isArray(n)||(n=[n]);var m=0;for(e=n.length;m<e;m++)if("number"===typeof n[m]&&0<=n[m]){for(;h.length<=n[m];)Wa(a);d(n[m],l)}else if("number"===typeof n[m]&&
0>n[m])d(h.length+n[m],l);else if("string"===typeof n[m]){var p=0;for(g=h.length;p<g;p++)("_all"==n[m]||k(h[p].nTh).hasClass(n[m]))&&d(p,l)}}if(c)for(f=0,a=c.length;f<a;f++)d(f,c[f])}function ea(a,b,c,d){var f=a.aoData.length,e=k.extend(!0,{},u.models.oRow,{src:c?"dom":"data",idx:f});e._aData=b;a.aoData.push(e);for(var g=a.aoColumns,h=0,l=g.length;h<l;h++)g[h].sType=null;a.aiDisplayMaster.push(f);b=a.rowIdFn(b);b!==q&&(a.aIds[b]=e);!c&&a.oFeatures.bDeferRender||Za(a,f,c,d);return f}function Ga(a,
b){var c;b instanceof k||(b=k(b));return b.map(function(d,f){c=$a(a,f);return ea(a,c.data,f,c.cells)})}function S(a,b,c,d){var f=a.iDraw,e=a.aoColumns[c],g=a.aoData[b]._aData,h=e.sDefaultContent,l=e.fnGetData(g,d,{settings:a,row:b,col:c});if(l===q)return a.iDrawError!=f&&null===h&&(aa(a,0,"Requested unknown parameter "+("function"==typeof e.mData?"{function}":"'"+e.mData+"'")+" for row "+b+", column "+c,4),a.iDrawError=f),h;if((l===g||null===l)&&null!==h&&d!==q)l=h;else if("function"===typeof l)return l.call(g);
return null===l&&"display"==d?"":l}function Db(a,b,c,d){a.aoColumns[c].fnSetData(a.aoData[b]._aData,d,{settings:a,row:b,col:c})}function ab(a){return k.map(a.match(/(\\.|[^\.])+/g)||[""],function(b){return b.replace(/\\\./g,".")})}function ia(a){if(k.isPlainObject(a)){var b={};k.each(a,function(d,f){f&&(b[d]=ia(f))});return function(d,f,e,g){var h=b[f]||b._;return h!==q?h(d,f,e,g):d}}if(null===a)return function(d){return d};if("function"===typeof a)return function(d,f,e,g){return a(d,f,e,g)};if("string"!==
typeof a||-1===a.indexOf(".")&&-1===a.indexOf("[")&&-1===a.indexOf("("))return function(d,f){return d[a]};var c=function(d,f,e){if(""!==e){var g=ab(e);for(var h=0,l=g.length;h<l;h++){e=g[h].match(ua);var n=g[h].match(oa);if(e){g[h]=g[h].replace(ua,"");""!==g[h]&&(d=d[g[h]]);n=[];g.splice(0,h+1);g=g.join(".");if(Array.isArray(d))for(h=0,l=d.length;h<l;h++)n.push(c(d[h],f,g));d=e[0].substring(1,e[0].length-1);d=""===d?n:n.join(d);break}else if(n){g[h]=g[h].replace(oa,"");d=d[g[h]]();continue}if(null===
d||d[g[h]]===q)return q;d=d[g[h]]}}return d};return function(d,f){return c(d,f,a)}}function da(a){if(k.isPlainObject(a))return da(a._);if(null===a)return function(){};if("function"===typeof a)return function(c,d,f){a(c,"set",d,f)};if("string"!==typeof a||-1===a.indexOf(".")&&-1===a.indexOf("[")&&-1===a.indexOf("("))return function(c,d){c[a]=d};var b=function(c,d,f){f=ab(f);var e=f[f.length-1];for(var g,h,l=0,n=f.length-1;l<n;l++){if("__proto__"===f[l])throw Error("Cannot set prototype values");g=
f[l].match(ua);h=f[l].match(oa);if(g){f[l]=f[l].replace(ua,"");c[f[l]]=[];e=f.slice();e.splice(0,l+1);g=e.join(".");if(Array.isArray(d))for(h=0,n=d.length;h<n;h++)e={},b(e,d[h],g),c[f[l]].push(e);else c[f[l]]=d;return}h&&(f[l]=f[l].replace(oa,""),c=c[f[l]](d));if(null===c[f[l]]||c[f[l]]===q)c[f[l]]={};c=c[f[l]]}if(e.match(oa))c[e.replace(oa,"")](d);else c[e.replace(ua,"")]=d};return function(c,d){return b(c,d,a)}}function bb(a){return T(a.aoData,"_aData")}function Ha(a){a.aoData.length=0;a.aiDisplayMaster.length=
0;a.aiDisplay.length=0;a.aIds={}}function Ia(a,b,c){for(var d=-1,f=0,e=a.length;f<e;f++)a[f]==b?d=f:a[f]>b&&a[f]--; -1!=d&&c===q&&a.splice(d,1)}function va(a,b,c,d){var f=a.aoData[b],e,g=function(l,n){for(;l.childNodes.length;)l.removeChild(l.firstChild);l.innerHTML=S(a,b,n,"display")};if("dom"!==c&&(c&&"auto"!==c||"dom"!==f.src)){var h=f.anCells;if(h)if(d!==q)g(h[d],d);else for(c=0,e=h.length;c<e;c++)g(h[c],c)}else f._aData=$a(a,f,d,d===q?q:f._aData).data;f._aSortData=null;f._aFilterData=null;g=
a.aoColumns;if(d!==q)g[d].sType=null;else{c=0;for(e=g.length;c<e;c++)g[c].sType=null;cb(a,f)}}function $a(a,b,c,d){var f=[],e=b.firstChild,g,h=0,l,n=a.aoColumns,m=a._rowReadObject;d=d!==q?d:m?{}:[];var p=function(x,r){if("string"===typeof x){var A=x.indexOf("@");-1!==A&&(A=x.substring(A+1),da(x)(d,r.getAttribute(A)))}},t=function(x){if(c===q||c===h)g=n[h],l=x.innerHTML.trim(),g&&g._bAttrSrc?(da(g.mData._)(d,l),p(g.mData.sort,x),p(g.mData.type,x),p(g.mData.filter,x)):m?(g._setter||(g._setter=da(g.mData)),
g._setter(d,l)):d[h]=l;h++};if(e)for(;e;){var v=e.nodeName.toUpperCase();if("TD"==v||"TH"==v)t(e),f.push(e);e=e.nextSibling}else for(f=b.anCells,e=0,v=f.length;e<v;e++)t(f[e]);(b=b.firstChild?b:b.nTr)&&(b=b.getAttribute("id"))&&da(a.rowId)(d,b);return{data:d,cells:f}}function Za(a,b,c,d){var f=a.aoData[b],e=f._aData,g=[],h,l;if(null===f.nTr){var n=c||z.createElement("tr");f.nTr=n;f.anCells=g;n._DT_RowIndex=b;cb(a,f);var m=0;for(h=a.aoColumns.length;m<h;m++){var p=a.aoColumns[m];var t=(l=c?!1:!0)?
z.createElement(p.sCellType):d[m];t._DT_CellIndex={row:b,column:m};g.push(t);if(l||!(c&&!p.mRender&&p.mData===m||k.isPlainObject(p.mData)&&p.mData._===m+".display"))t.innerHTML=S(a,b,m,"display");p.sClass&&(t.className+=" "+p.sClass);p.bVisible&&!c?n.appendChild(t):!p.bVisible&&c&&t.parentNode.removeChild(t);p.fnCreatedCell&&p.fnCreatedCell.call(a.oInstance,t,S(a,b,m),e,b,m)}I(a,"aoRowCreatedCallback",null,[n,e,b,g])}f.nTr.setAttribute("role","row")}function cb(a,b){var c=b.nTr,d=b._aData;if(c){if(a=
a.rowIdFn(d))c.id=a;d.DT_RowClass&&(a=d.DT_RowClass.split(" "),b.__rowc=b.__rowc?Ja(b.__rowc.concat(a)):a,k(c).removeClass(b.__rowc.join(" ")).addClass(d.DT_RowClass));d.DT_RowAttr&&k(c).attr(d.DT_RowAttr);d.DT_RowData&&k(c).data(d.DT_RowData)}}function Eb(a){var b,c,d=a.nTHead,f=a.nTFoot,e=0===k("th, td",d).length,g=a.oClasses,h=a.aoColumns;e&&(c=k("<tr/>").appendTo(d));var l=0;for(b=h.length;l<b;l++){var n=h[l];var m=k(n.nTh).addClass(n.sClass);e&&m.appendTo(c);a.oFeatures.bSort&&(m.addClass(n.sSortingClass),
!1!==n.bSortable&&(m.attr("tabindex",a.iTabIndex).attr("aria-controls",a.sTableId),db(a,n.nTh,l)));n.sTitle!=m[0].innerHTML&&m.html(n.sTitle);eb(a,"header")(a,m,n,g)}e&&wa(a.aoHeader,d);k(d).children("tr").attr("role","row");k(d).children("tr").children("th, td").addClass(g.sHeaderTH);k(f).children("tr").children("th, td").addClass(g.sFooterTH);if(null!==f)for(a=a.aoFooter[0],l=0,b=a.length;l<b;l++)n=h[l],n.nTf=a[l].cell,n.sClass&&k(n.nTf).addClass(n.sClass)}function xa(a,b,c){var d,f,e=[],g=[],h=
a.aoColumns.length;if(b){c===q&&(c=!1);var l=0;for(d=b.length;l<d;l++){e[l]=b[l].slice();e[l].nTr=b[l].nTr;for(f=h-1;0<=f;f--)a.aoColumns[f].bVisible||c||e[l].splice(f,1);g.push([])}l=0;for(d=e.length;l<d;l++){if(a=e[l].nTr)for(;f=a.firstChild;)a.removeChild(f);f=0;for(b=e[l].length;f<b;f++){var n=h=1;if(g[l][f]===q){a.appendChild(e[l][f].cell);for(g[l][f]=1;e[l+h]!==q&&e[l][f].cell==e[l+h][f].cell;)g[l+h][f]=1,h++;for(;e[l][f+n]!==q&&e[l][f].cell==e[l][f+n].cell;){for(c=0;c<h;c++)g[l+c][f+n]=1;n++}k(e[l][f].cell).attr("rowspan",
h).attr("colspan",n)}}}}}function fa(a){var b=I(a,"aoPreDrawCallback","preDraw",[a]);if(-1!==k.inArray(!1,b))U(a,!1);else{b=[];var c=0,d=a.asStripeClasses,f=d.length,e=a.oLanguage,g=a.iInitDisplayStart,h="ssp"==P(a),l=a.aiDisplay;a.bDrawing=!0;g!==q&&-1!==g&&(a._iDisplayStart=h?g:g>=a.fnRecordsDisplay()?0:g,a.iInitDisplayStart=-1);g=a._iDisplayStart;var n=a.fnDisplayEnd();if(a.bDeferLoading)a.bDeferLoading=!1,a.iDraw++,U(a,!1);else if(!h)a.iDraw++;else if(!a.bDestroying&&!Fb(a))return;if(0!==l.length)for(e=
h?a.aoData.length:n,h=h?0:g;h<e;h++){var m=l[h],p=a.aoData[m];null===p.nTr&&Za(a,m);var t=p.nTr;if(0!==f){var v=d[c%f];p._sRowStripe!=v&&(k(t).removeClass(p._sRowStripe).addClass(v),p._sRowStripe=v)}I(a,"aoRowCallback",null,[t,p._aData,c,h,m]);b.push(t);c++}else c=e.sZeroRecords,1==a.iDraw&&"ajax"==P(a)?c=e.sLoadingRecords:e.sEmptyTable&&0===a.fnRecordsTotal()&&(c=e.sEmptyTable),b[0]=k("<tr/>",{"class":f?d[0]:""}).append(k("<td />",{valign:"top",colSpan:na(a),"class":a.oClasses.sRowEmpty}).html(c))[0];
I(a,"aoHeaderCallback","header",[k(a.nTHead).children("tr")[0],bb(a),g,n,l]);I(a,"aoFooterCallback","footer",[k(a.nTFoot).children("tr")[0],bb(a),g,n,l]);d=k(a.nTBody);d.children().detach();d.append(k(b));I(a,"aoDrawCallback","draw",[a]);a.bSorted=!1;a.bFiltered=!1;a.bDrawing=!1}}function ja(a,b){var c=a.oFeatures,d=c.bFilter;c.bSort&&Gb(a);d?ya(a,a.oPreviousSearch):a.aiDisplay=a.aiDisplayMaster.slice();!0!==b&&(a._iDisplayStart=0);a._drawHold=b;fa(a);a._drawHold=!1}function Hb(a){var b=a.oClasses,
c=k(a.nTable);c=k("<div/>").insertBefore(c);var d=a.oFeatures,f=k("<div/>",{id:a.sTableId+"_wrapper","class":b.sWrapper+(a.nTFoot?"":" "+b.sNoFooter)});a.nHolding=c[0];a.nTableWrapper=f[0];a.nTableReinsertBefore=a.nTable.nextSibling;for(var e=a.sDom.split(""),g,h,l,n,m,p,t=0;t<e.length;t++){g=null;h=e[t];if("<"==h){l=k("<div/>")[0];n=e[t+1];if("'"==n||'"'==n){m="";for(p=2;e[t+p]!=n;)m+=e[t+p],p++;"H"==m?m=b.sJUIHeader:"F"==m&&(m=b.sJUIFooter);-1!=m.indexOf(".")?(n=m.split("."),l.id=n[0].substr(1,
n[0].length-1),l.className=n[1]):"#"==m.charAt(0)?l.id=m.substr(1,m.length-1):l.className=m;t+=p}f.append(l);f=k(l)}else if(">"==h)f=f.parent();else if("l"==h&&d.bPaginate&&d.bLengthChange)g=Ib(a);else if("f"==h&&d.bFilter)g=Jb(a);else if("r"==h&&d.bProcessing)g=Kb(a);else if("t"==h)g=Lb(a);else if("i"==h&&d.bInfo)g=Mb(a);else if("p"==h&&d.bPaginate)g=Nb(a);else if(0!==u.ext.feature.length)for(l=u.ext.feature,p=0,n=l.length;p<n;p++)if(h==l[p].cFeature){g=l[p].fnInit(a);break}g&&(l=a.aanFeatures,l[h]||
(l[h]=[]),l[h].push(g),f.append(g))}c.replaceWith(f);a.nHolding=null}function wa(a,b){b=k(b).children("tr");var c,d,f;a.splice(0,a.length);var e=0;for(f=b.length;e<f;e++)a.push([]);e=0;for(f=b.length;e<f;e++){var g=b[e];for(c=g.firstChild;c;){if("TD"==c.nodeName.toUpperCase()||"TH"==c.nodeName.toUpperCase()){var h=1*c.getAttribute("colspan");var l=1*c.getAttribute("rowspan");h=h&&0!==h&&1!==h?h:1;l=l&&0!==l&&1!==l?l:1;var n=0;for(d=a[e];d[n];)n++;var m=n;var p=1===h?!0:!1;for(d=0;d<h;d++)for(n=0;n<
l;n++)a[e+n][m+d]={cell:c,unique:p},a[e+n].nTr=g}c=c.nextSibling}}}function Ka(a,b,c){var d=[];c||(c=a.aoHeader,b&&(c=[],wa(c,b)));b=0;for(var f=c.length;b<f;b++)for(var e=0,g=c[b].length;e<g;e++)!c[b][e].unique||d[e]&&a.bSortCellsTop||(d[e]=c[b][e].cell);return d}function La(a,b,c){I(a,"aoServerParams","serverParams",[b]);if(b&&Array.isArray(b)){var d={},f=/(.*?)\[\]$/;k.each(b,function(m,p){(m=p.name.match(f))?(m=m[0],d[m]||(d[m]=[]),d[m].push(p.value)):d[p.name]=p.value});b=d}var e=a.ajax,g=a.oInstance,
h=function(m){I(a,null,"xhr",[a,m,a.jqXHR]);c(m)};if(k.isPlainObject(e)&&e.data){var l=e.data;var n="function"===typeof l?l(b,a):l;b="function"===typeof l&&n?n:k.extend(!0,b,n);delete e.data}n={data:b,success:function(m){var p=m.error||m.sError;p&&aa(a,0,p);a.json=m;h(m)},dataType:"json",cache:!1,type:a.sServerMethod,error:function(m,p,t){t=I(a,null,"xhr",[a,null,a.jqXHR]);-1===k.inArray(!0,t)&&("parsererror"==p?aa(a,0,"Invalid JSON response",1):4===m.readyState&&aa(a,0,"Ajax error",7));U(a,!1)}};
a.oAjaxData=b;I(a,null,"preXhr",[a,b]);a.fnServerData?a.fnServerData.call(g,a.sAjaxSource,k.map(b,function(m,p){return{name:p,value:m}}),h,a):a.sAjaxSource||"string"===typeof e?a.jqXHR=k.ajax(k.extend(n,{url:e||a.sAjaxSource})):"function"===typeof e?a.jqXHR=e.call(g,b,h,a):(a.jqXHR=k.ajax(k.extend(n,e)),e.data=l)}function Fb(a){return a.bAjaxDataGet?(a.iDraw++,U(a,!0),La(a,Ob(a),function(b){Pb(a,b)}),!1):!0}function Ob(a){var b=a.aoColumns,c=b.length,d=a.oFeatures,f=a.oPreviousSearch,e=a.aoPreSearchCols,
g=[],h=pa(a);var l=a._iDisplayStart;var n=!1!==d.bPaginate?a._iDisplayLength:-1;var m=function(x,r){g.push({name:x,value:r})};m("sEcho",a.iDraw);m("iColumns",c);m("sColumns",T(b,"sName").join(","));m("iDisplayStart",l);m("iDisplayLength",n);var p={draw:a.iDraw,columns:[],order:[],start:l,length:n,search:{value:f.sSearch,regex:f.bRegex}};for(l=0;l<c;l++){var t=b[l];var v=e[l];n="function"==typeof t.mData?"function":t.mData;p.columns.push({data:n,name:t.sName,searchable:t.bSearchable,orderable:t.bSortable,
search:{value:v.sSearch,regex:v.bRegex}});m("mDataProp_"+l,n);d.bFilter&&(m("sSearch_"+l,v.sSearch),m("bRegex_"+l,v.bRegex),m("bSearchable_"+l,t.bSearchable));d.bSort&&m("bSortable_"+l,t.bSortable)}d.bFilter&&(m("sSearch",f.sSearch),m("bRegex",f.bRegex));d.bSort&&(k.each(h,function(x,r){p.order.push({column:r.col,dir:r.dir});m("iSortCol_"+x,r.col);m("sSortDir_"+x,r.dir)}),m("iSortingCols",h.length));b=u.ext.legacy.ajax;return null===b?a.sAjaxSource?g:p:b?g:p}function Pb(a,b){var c=function(g,h){return b[g]!==
q?b[g]:b[h]},d=Ma(a,b),f=c("sEcho","draw"),e=c("iTotalRecords","recordsTotal");c=c("iTotalDisplayRecords","recordsFiltered");if(f!==q){if(1*f<a.iDraw)return;a.iDraw=1*f}Ha(a);a._iRecordsTotal=parseInt(e,10);a._iRecordsDisplay=parseInt(c,10);f=0;for(e=d.length;f<e;f++)ea(a,d[f]);a.aiDisplay=a.aiDisplayMaster.slice();a.bAjaxDataGet=!1;fa(a);a._bInitComplete||Na(a,b);a.bAjaxDataGet=!0;U(a,!1)}function Ma(a,b){a=k.isPlainObject(a.ajax)&&a.ajax.dataSrc!==q?a.ajax.dataSrc:a.sAjaxDataProp;return"data"===
a?b.aaData||b[a]:""!==a?ia(a)(b):b}function Jb(a){var b=a.oClasses,c=a.sTableId,d=a.oLanguage,f=a.oPreviousSearch,e=a.aanFeatures,g='<input type="search" class="'+b.sFilterInput+'"/>',h=d.sSearch;h=h.match(/_INPUT_/)?h.replace("_INPUT_",g):h+g;b=k("<div/>",{id:e.f?null:c+"_filter","class":b.sFilter}).append(k("<label/>").append(h));var l=function(){var m=this.value?this.value:"";m!=f.sSearch&&(ya(a,{sSearch:m,bRegex:f.bRegex,bSmart:f.bSmart,bCaseInsensitive:f.bCaseInsensitive}),a._iDisplayStart=0,
fa(a))};e=null!==a.searchDelay?a.searchDelay:"ssp"===P(a)?400:0;var n=k("input",b).val(f.sSearch).attr("placeholder",d.sSearchPlaceholder).on("keyup.DT search.DT input.DT paste.DT cut.DT",e?fb(l,e):l).on("mouseup",function(m){setTimeout(function(){l.call(n[0])},10)}).on("keypress.DT",function(m){if(13==m.keyCode)return!1}).attr("aria-controls",c);k(a.nTable).on("search.dt.DT",function(m,p){if(a===p)try{n[0]!==z.activeElement&&n.val(f.sSearch)}catch(t){}});return b[0]}function ya(a,b,c){var d=a.oPreviousSearch,
f=a.aoPreSearchCols,e=function(h){d.sSearch=h.sSearch;d.bRegex=h.bRegex;d.bSmart=h.bSmart;d.bCaseInsensitive=h.bCaseInsensitive},g=function(h){return h.bEscapeRegex!==q?!h.bEscapeRegex:h.bRegex};Ya(a);if("ssp"!=P(a)){Qb(a,b.sSearch,c,g(b),b.bSmart,b.bCaseInsensitive);e(b);for(b=0;b<f.length;b++)Rb(a,f[b].sSearch,b,g(f[b]),f[b].bSmart,f[b].bCaseInsensitive);Sb(a)}else e(b);a.bFiltered=!0;I(a,null,"search",[a])}function Sb(a){for(var b=u.ext.search,c=a.aiDisplay,d,f,e=0,g=b.length;e<g;e++){for(var h=
[],l=0,n=c.length;l<n;l++)f=c[l],d=a.aoData[f],b[e](a,d._aFilterData,f,d._aData,l)&&h.push(f);c.length=0;k.merge(c,h)}}function Rb(a,b,c,d,f,e){if(""!==b){var g=[],h=a.aiDisplay;d=gb(b,d,f,e);for(f=0;f<h.length;f++)b=a.aoData[h[f]]._aFilterData[c],d.test(b)&&g.push(h[f]);a.aiDisplay=g}}function Qb(a,b,c,d,f,e){f=gb(b,d,f,e);var g=a.oPreviousSearch.sSearch,h=a.aiDisplayMaster;e=[];0!==u.ext.search.length&&(c=!0);var l=Tb(a);if(0>=b.length)a.aiDisplay=h.slice();else{if(l||c||d||g.length>b.length||0!==
b.indexOf(g)||a.bSorted)a.aiDisplay=h.slice();b=a.aiDisplay;for(c=0;c<b.length;c++)f.test(a.aoData[b[c]]._sFilterRow)&&e.push(b[c]);a.aiDisplay=e}}function gb(a,b,c,d){a=b?a:hb(a);c&&(a="^(?=.*?"+k.map(a.match(/"[^"]+"|[^ ]+/g)||[""],function(f){if('"'===f.charAt(0)){var e=f.match(/^"(.*)"$/);f=e?e[1]:f}return f.replace('"',"")}).join(")(?=.*?")+").*$");return new RegExp(a,d?"i":"")}function Tb(a){var b=a.aoColumns,c,d,f=u.ext.type.search;var e=!1;var g=0;for(c=a.aoData.length;g<c;g++){var h=a.aoData[g];
if(!h._aFilterData){var l=[];var n=0;for(d=b.length;n<d;n++){e=b[n];if(e.bSearchable){var m=S(a,g,n,"filter");f[e.sType]&&(m=f[e.sType](m));null===m&&(m="");"string"!==typeof m&&m.toString&&(m=m.toString())}else m="";m.indexOf&&-1!==m.indexOf("&")&&(Oa.innerHTML=m,m=rc?Oa.textContent:Oa.innerText);m.replace&&(m=m.replace(/[\r\n\u2028]/g,""));l.push(m)}h._aFilterData=l;h._sFilterRow=l.join(" ");e=!0}}return e}function Ub(a){return{search:a.sSearch,smart:a.bSmart,regex:a.bRegex,caseInsensitive:a.bCaseInsensitive}}
function Vb(a){return{sSearch:a.search,bSmart:a.smart,bRegex:a.regex,bCaseInsensitive:a.caseInsensitive}}function Mb(a){var b=a.sTableId,c=a.aanFeatures.i,d=k("<div/>",{"class":a.oClasses.sInfo,id:c?null:b+"_info"});c||(a.aoDrawCallback.push({fn:Wb,sName:"information"}),d.attr("role","status").attr("aria-live","polite"),k(a.nTable).attr("aria-describedby",b+"_info"));return d[0]}function Wb(a){var b=a.aanFeatures.i;if(0!==b.length){var c=a.oLanguage,d=a._iDisplayStart+1,f=a.fnDisplayEnd(),e=a.fnRecordsTotal(),
g=a.fnRecordsDisplay(),h=g?c.sInfo:c.sInfoEmpty;g!==e&&(h+=" "+c.sInfoFiltered);h+=c.sInfoPostFix;h=Xb(a,h);c=c.fnInfoCallback;null!==c&&(h=c.call(a.oInstance,a,d,f,e,g,h));k(b).html(h)}}function Xb(a,b){var c=a.fnFormatNumber,d=a._iDisplayStart+1,f=a._iDisplayLength,e=a.fnRecordsDisplay(),g=-1===f;return b.replace(/_START_/g,c.call(a,d)).replace(/_END_/g,c.call(a,a.fnDisplayEnd())).replace(/_MAX_/g,c.call(a,a.fnRecordsTotal())).replace(/_TOTAL_/g,c.call(a,e)).replace(/_PAGE_/g,c.call(a,g?1:Math.ceil(d/
f))).replace(/_PAGES_/g,c.call(a,g?1:Math.ceil(e/f)))}function za(a){var b=a.iInitDisplayStart,c=a.aoColumns;var d=a.oFeatures;var f=a.bDeferLoading;if(a.bInitialised){Hb(a);Eb(a);xa(a,a.aoHeader);xa(a,a.aoFooter);U(a,!0);d.bAutoWidth&&Xa(a);var e=0;for(d=c.length;e<d;e++){var g=c[e];g.sWidth&&(g.nTh.style.width=K(g.sWidth))}I(a,null,"preInit",[a]);ja(a);c=P(a);if("ssp"!=c||f)"ajax"==c?La(a,[],function(h){var l=Ma(a,h);for(e=0;e<l.length;e++)ea(a,l[e]);a.iInitDisplayStart=b;ja(a);U(a,!1);Na(a,h)},
a):(U(a,!1),Na(a))}else setTimeout(function(){za(a)},200)}function Na(a,b){a._bInitComplete=!0;(b||a.oInit.aaData)&&ra(a);I(a,null,"plugin-init",[a,b]);I(a,"aoInitComplete","init",[a,b])}function ib(a,b){b=parseInt(b,10);a._iDisplayLength=b;jb(a);I(a,null,"length",[a,b])}function Ib(a){var b=a.oClasses,c=a.sTableId,d=a.aLengthMenu,f=Array.isArray(d[0]),e=f?d[0]:d;d=f?d[1]:d;f=k("<select/>",{name:c+"_length","aria-controls":c,"class":b.sLengthSelect});for(var g=0,h=e.length;g<h;g++)f[0][g]=new Option("number"===
typeof d[g]?a.fnFormatNumber(d[g]):d[g],e[g]);var l=k("<div><label/></div>").addClass(b.sLength);a.aanFeatures.l||(l[0].id=c+"_length");l.children().append(a.oLanguage.sLengthMenu.replace("_MENU_",f[0].outerHTML));k("select",l).val(a._iDisplayLength).on("change.DT",function(n){ib(a,k(this).val());fa(a)});k(a.nTable).on("length.dt.DT",function(n,m,p){a===m&&k("select",l).val(p)});return l[0]}function Nb(a){var b=a.sPaginationType,c=u.ext.pager[b],d="function"===typeof c,f=function(g){fa(g)};b=k("<div/>").addClass(a.oClasses.sPaging+
b)[0];var e=a.aanFeatures;d||c.fnInit(a,b,f);e.p||(b.id=a.sTableId+"_paginate",a.aoDrawCallback.push({fn:function(g){if(d){var h=g._iDisplayStart,l=g._iDisplayLength,n=g.fnRecordsDisplay(),m=-1===l;h=m?0:Math.ceil(h/l);l=m?1:Math.ceil(n/l);n=c(h,l);var p;m=0;for(p=e.p.length;m<p;m++)eb(g,"pageButton")(g,e.p[m],m,n,h,l)}else c.fnUpdate(g,f)},sName:"pagination"}));return b}function kb(a,b,c){var d=a._iDisplayStart,f=a._iDisplayLength,e=a.fnRecordsDisplay();0===e||-1===f?d=0:"number"===typeof b?(d=b*
f,d>e&&(d=0)):"first"==b?d=0:"previous"==b?(d=0<=f?d-f:0,0>d&&(d=0)):"next"==b?d+f<e&&(d+=f):"last"==b?d=Math.floor((e-1)/f)*f:aa(a,0,"Unknown paging action: "+b,5);b=a._iDisplayStart!==d;a._iDisplayStart=d;b&&(I(a,null,"page",[a]),c&&fa(a));return b}function Kb(a){return k("<div/>",{id:a.aanFeatures.r?null:a.sTableId+"_processing","class":a.oClasses.sProcessing}).html(a.oLanguage.sProcessing).insertBefore(a.nTable)[0]}function U(a,b){a.oFeatures.bProcessing&&k(a.aanFeatures.r).css("display",b?"block":
"none");I(a,null,"processing",[a,b])}function Lb(a){var b=k(a.nTable);b.attr("role","grid");var c=a.oScroll;if(""===c.sX&&""===c.sY)return a.nTable;var d=c.sX,f=c.sY,e=a.oClasses,g=b.children("caption"),h=g.length?g[0]._captionSide:null,l=k(b[0].cloneNode(!1)),n=k(b[0].cloneNode(!1)),m=b.children("tfoot");m.length||(m=null);l=k("<div/>",{"class":e.sScrollWrapper}).append(k("<div/>",{"class":e.sScrollHead}).css({overflow:"hidden",position:"relative",border:0,width:d?d?K(d):null:"100%"}).append(k("<div/>",
{"class":e.sScrollHeadInner}).css({"box-sizing":"content-box",width:c.sXInner||"100%"}).append(l.removeAttr("id").css("margin-left",0).append("top"===h?g:null).append(b.children("thead"))))).append(k("<div/>",{"class":e.sScrollBody}).css({position:"relative",overflow:"auto",width:d?K(d):null}).append(b));m&&l.append(k("<div/>",{"class":e.sScrollFoot}).css({overflow:"hidden",border:0,width:d?d?K(d):null:"100%"}).append(k("<div/>",{"class":e.sScrollFootInner}).append(n.removeAttr("id").css("margin-left",
0).append("bottom"===h?g:null).append(b.children("tfoot")))));b=l.children();var p=b[0];e=b[1];var t=m?b[2]:null;if(d)k(e).on("scroll.DT",function(v){v=this.scrollLeft;p.scrollLeft=v;m&&(t.scrollLeft=v)});k(e).css("max-height",f);c.bCollapse||k(e).css("height",f);a.nScrollHead=p;a.nScrollBody=e;a.nScrollFoot=t;a.aoDrawCallback.push({fn:Ea,sName:"scrolling"});return l[0]}function Ea(a){var b=a.oScroll,c=b.sX,d=b.sXInner,f=b.sY;b=b.iBarWidth;var e=k(a.nScrollHead),g=e[0].style,h=e.children("div"),l=
h[0].style,n=h.children("table");h=a.nScrollBody;var m=k(h),p=h.style,t=k(a.nScrollFoot).children("div"),v=t.children("table"),x=k(a.nTHead),r=k(a.nTable),A=r[0],E=A.style,H=a.nTFoot?k(a.nTFoot):null,W=a.oBrowser,M=W.bScrollOversize,C=T(a.aoColumns,"nTh"),B=[],ba=[],X=[],lb=[],Aa,Yb=function(F){F=F.style;F.paddingTop="0";F.paddingBottom="0";F.borderTopWidth="0";F.borderBottomWidth="0";F.height=0};var ha=h.scrollHeight>h.clientHeight;if(a.scrollBarVis!==ha&&a.scrollBarVis!==q)a.scrollBarVis=ha,ra(a);
else{a.scrollBarVis=ha;r.children("thead, tfoot").remove();if(H){var ka=H.clone().prependTo(r);var la=H.find("tr");ka=ka.find("tr")}var mb=x.clone().prependTo(r);x=x.find("tr");ha=mb.find("tr");mb.find("th, td").removeAttr("tabindex");c||(p.width="100%",e[0].style.width="100%");k.each(Ka(a,mb),function(F,Y){Aa=sa(a,F);Y.style.width=a.aoColumns[Aa].sWidth});H&&Z(function(F){F.style.width=""},ka);e=r.outerWidth();""===c?(E.width="100%",M&&(r.find("tbody").height()>h.offsetHeight||"scroll"==m.css("overflow-y"))&&
(E.width=K(r.outerWidth()-b)),e=r.outerWidth()):""!==d&&(E.width=K(d),e=r.outerWidth());Z(Yb,ha);Z(function(F){X.push(F.innerHTML);B.push(K(k(F).css("width")))},ha);Z(function(F,Y){-1!==k.inArray(F,C)&&(F.style.width=B[Y])},x);k(ha).height(0);H&&(Z(Yb,ka),Z(function(F){lb.push(F.innerHTML);ba.push(K(k(F).css("width")))},ka),Z(function(F,Y){F.style.width=ba[Y]},la),k(ka).height(0));Z(function(F,Y){F.innerHTML='<div class="dataTables_sizing">'+X[Y]+"</div>";F.childNodes[0].style.height="0";F.childNodes[0].style.overflow=
"hidden";F.style.width=B[Y]},ha);H&&Z(function(F,Y){F.innerHTML='<div class="dataTables_sizing">'+lb[Y]+"</div>";F.childNodes[0].style.height="0";F.childNodes[0].style.overflow="hidden";F.style.width=ba[Y]},ka);r.outerWidth()<e?(la=h.scrollHeight>h.offsetHeight||"scroll"==m.css("overflow-y")?e+b:e,M&&(h.scrollHeight>h.offsetHeight||"scroll"==m.css("overflow-y"))&&(E.width=K(la-b)),""!==c&&""===d||aa(a,1,"Possible column misalignment",6)):la="100%";p.width=K(la);g.width=K(la);H&&(a.nScrollFoot.style.width=
K(la));!f&&M&&(p.height=K(A.offsetHeight+b));c=r.outerWidth();n[0].style.width=K(c);l.width=K(c);d=r.height()>h.clientHeight||"scroll"==m.css("overflow-y");f="padding"+(W.bScrollbarLeft?"Left":"Right");l[f]=d?b+"px":"0px";H&&(v[0].style.width=K(c),t[0].style.width=K(c),t[0].style[f]=d?b+"px":"0px");r.children("colgroup").insertBefore(r.children("thead"));m.trigger("scroll");!a.bSorted&&!a.bFiltered||a._drawHold||(h.scrollTop=0)}}function Z(a,b,c){for(var d=0,f=0,e=b.length,g,h;f<e;){g=b[f].firstChild;
for(h=c?c[f].firstChild:null;g;)1===g.nodeType&&(c?a(g,h,d):a(g,d),d++),g=g.nextSibling,h=c?h.nextSibling:null;f++}}function Xa(a){var b=a.nTable,c=a.aoColumns,d=a.oScroll,f=d.sY,e=d.sX,g=d.sXInner,h=c.length,l=Fa(a,"bVisible"),n=k("th",a.nTHead),m=b.getAttribute("width"),p=b.parentNode,t=!1,v,x=a.oBrowser;d=x.bScrollOversize;(v=b.style.width)&&-1!==v.indexOf("%")&&(m=v);for(v=0;v<l.length;v++){var r=c[l[v]];null!==r.sWidth&&(r.sWidth=Zb(r.sWidthOrig,p),t=!0)}if(d||!t&&!e&&!f&&h==na(a)&&h==n.length)for(v=
0;v<h;v++)l=sa(a,v),null!==l&&(c[l].sWidth=K(n.eq(v).width()));else{h=k(b).clone().css("visibility","hidden").removeAttr("id");h.find("tbody tr").remove();var A=k("<tr/>").appendTo(h.find("tbody"));h.find("thead, tfoot").remove();h.append(k(a.nTHead).clone()).append(k(a.nTFoot).clone());h.find("tfoot th, tfoot td").css("width","");n=Ka(a,h.find("thead")[0]);for(v=0;v<l.length;v++)r=c[l[v]],n[v].style.width=null!==r.sWidthOrig&&""!==r.sWidthOrig?K(r.sWidthOrig):"",r.sWidthOrig&&e&&k(n[v]).append(k("<div/>").css({width:r.sWidthOrig,
margin:0,padding:0,border:0,height:1}));if(a.aoData.length)for(v=0;v<l.length;v++)t=l[v],r=c[t],k($b(a,t)).clone(!1).append(r.sContentPadding).appendTo(A);k("[name]",h).removeAttr("name");r=k("<div/>").css(e||f?{position:"absolute",top:0,left:0,height:1,right:0,overflow:"hidden"}:{}).append(h).appendTo(p);e&&g?h.width(g):e?(h.css("width","auto"),h.removeAttr("width"),h.width()<p.clientWidth&&m&&h.width(p.clientWidth)):f?h.width(p.clientWidth):m&&h.width(m);for(v=f=0;v<l.length;v++)p=k(n[v]),g=p.outerWidth()-
p.width(),p=x.bBounding?Math.ceil(n[v].getBoundingClientRect().width):p.outerWidth(),f+=p,c[l[v]].sWidth=K(p-g);b.style.width=K(f);r.remove()}m&&(b.style.width=K(m));!m&&!e||a._reszEvt||(b=function(){k(y).on("resize.DT-"+a.sInstance,fb(function(){ra(a)}))},d?setTimeout(b,1E3):b(),a._reszEvt=!0)}function Zb(a,b){if(!a)return 0;a=k("<div/>").css("width",K(a)).appendTo(b||z.body);b=a[0].offsetWidth;a.remove();return b}function $b(a,b){var c=ac(a,b);if(0>c)return null;var d=a.aoData[c];return d.nTr?d.anCells[b]:
k("<td/>").html(S(a,c,b,"display"))[0]}function ac(a,b){for(var c,d=-1,f=-1,e=0,g=a.aoData.length;e<g;e++)c=S(a,e,b,"display")+"",c=c.replace(sc,""),c=c.replace(/&nbsp;/g," "),c.length>d&&(d=c.length,f=e);return f}function K(a){return null===a?"0px":"number"==typeof a?0>a?"0px":a+"px":a.match(/\d$/)?a+"px":a}function pa(a){var b=[],c=a.aoColumns;var d=a.aaSortingFixed;var f=k.isPlainObject(d);var e=[];var g=function(m){m.length&&!Array.isArray(m[0])?e.push(m):k.merge(e,m)};Array.isArray(d)&&g(d);
f&&d.pre&&g(d.pre);g(a.aaSorting);f&&d.post&&g(d.post);for(a=0;a<e.length;a++){var h=e[a][0];g=c[h].aDataSort;d=0;for(f=g.length;d<f;d++){var l=g[d];var n=c[l].sType||"string";e[a]._idx===q&&(e[a]._idx=k.inArray(e[a][1],c[l].asSorting));b.push({src:h,col:l,dir:e[a][1],index:e[a]._idx,type:n,formatter:u.ext.type.order[n+"-pre"]})}}return b}function Gb(a){var b,c=[],d=u.ext.type.order,f=a.aoData,e=0,g=a.aiDisplayMaster;Ya(a);var h=pa(a);var l=0;for(b=h.length;l<b;l++){var n=h[l];n.formatter&&e++;bc(a,
n.col)}if("ssp"!=P(a)&&0!==h.length){l=0;for(b=g.length;l<b;l++)c[g[l]]=l;e===h.length?g.sort(function(m,p){var t,v=h.length,x=f[m]._aSortData,r=f[p]._aSortData;for(t=0;t<v;t++){var A=h[t];var E=x[A.col];var H=r[A.col];E=E<H?-1:E>H?1:0;if(0!==E)return"asc"===A.dir?E:-E}E=c[m];H=c[p];return E<H?-1:E>H?1:0}):g.sort(function(m,p){var t,v=h.length,x=f[m]._aSortData,r=f[p]._aSortData;for(t=0;t<v;t++){var A=h[t];var E=x[A.col];var H=r[A.col];A=d[A.type+"-"+A.dir]||d["string-"+A.dir];E=A(E,H);if(0!==E)return E}E=
c[m];H=c[p];return E<H?-1:E>H?1:0})}a.bSorted=!0}function cc(a){var b=a.aoColumns,c=pa(a);a=a.oLanguage.oAria;for(var d=0,f=b.length;d<f;d++){var e=b[d];var g=e.asSorting;var h=e.sTitle.replace(/<.*?>/g,"");var l=e.nTh;l.removeAttribute("aria-sort");e.bSortable&&(0<c.length&&c[0].col==d?(l.setAttribute("aria-sort","asc"==c[0].dir?"ascending":"descending"),e=g[c[0].index+1]||g[0]):e=g[0],h+="asc"===e?a.sSortAscending:a.sSortDescending);l.setAttribute("aria-label",h)}}function nb(a,b,c,d){var f=a.aaSorting,
e=a.aoColumns[b].asSorting,g=function(h,l){var n=h._idx;n===q&&(n=k.inArray(h[1],e));return n+1<e.length?n+1:l?null:0};"number"===typeof f[0]&&(f=a.aaSorting=[f]);c&&a.oFeatures.bSortMulti?(c=k.inArray(b,T(f,"0")),-1!==c?(b=g(f[c],!0),null===b&&1===f.length&&(b=0),null===b?f.splice(c,1):(f[c][1]=e[b],f[c]._idx=b)):(f.push([b,e[0],0]),f[f.length-1]._idx=0)):f.length&&f[0][0]==b?(b=g(f[0]),f.length=1,f[0][1]=e[b],f[0]._idx=b):(f.length=0,f.push([b,e[0]]),f[0]._idx=0);ja(a);"function"==typeof d&&d(a)}
function db(a,b,c,d){var f=a.aoColumns[c];ob(b,{},function(e){!1!==f.bSortable&&(a.oFeatures.bProcessing?(U(a,!0),setTimeout(function(){nb(a,c,e.shiftKey,d);"ssp"!==P(a)&&U(a,!1)},0)):nb(a,c,e.shiftKey,d))})}function Pa(a){var b=a.aLastSort,c=a.oClasses.sSortColumn,d=pa(a),f=a.oFeatures,e;if(f.bSort&&f.bSortClasses){f=0;for(e=b.length;f<e;f++){var g=b[f].src;k(T(a.aoData,"anCells",g)).removeClass(c+(2>f?f+1:3))}f=0;for(e=d.length;f<e;f++)g=d[f].src,k(T(a.aoData,"anCells",g)).addClass(c+(2>f?f+1:3))}a.aLastSort=
d}function bc(a,b){var c=a.aoColumns[b],d=u.ext.order[c.sSortDataType],f;d&&(f=d.call(a.oInstance,a,b,ta(a,b)));for(var e,g=u.ext.type.order[c.sType+"-pre"],h=0,l=a.aoData.length;h<l;h++)if(c=a.aoData[h],c._aSortData||(c._aSortData=[]),!c._aSortData[b]||d)e=d?f[h]:S(a,h,b,"sort"),c._aSortData[b]=g?g(e):e}function Qa(a){if(a.oFeatures.bStateSave&&!a.bDestroying){var b={time:+new Date,start:a._iDisplayStart,length:a._iDisplayLength,order:k.extend(!0,[],a.aaSorting),search:Ub(a.oPreviousSearch),columns:k.map(a.aoColumns,
function(c,d){return{visible:c.bVisible,search:Ub(a.aoPreSearchCols[d])}})};I(a,"aoStateSaveParams","stateSaveParams",[a,b]);a.oSavedState=b;a.fnStateSaveCallback.call(a.oInstance,a,b)}}function dc(a,b,c){var d,f,e=a.aoColumns;b=function(h){if(h&&h.time){var l=I(a,"aoStateLoadParams","stateLoadParams",[a,h]);if(-1===k.inArray(!1,l)&&(l=a.iStateDuration,!(0<l&&h.time<+new Date-1E3*l||h.columns&&e.length!==h.columns.length))){a.oLoadedState=k.extend(!0,{},h);h.start!==q&&(a._iDisplayStart=h.start,a.iInitDisplayStart=
h.start);h.length!==q&&(a._iDisplayLength=h.length);h.order!==q&&(a.aaSorting=[],k.each(h.order,function(n,m){a.aaSorting.push(m[0]>=e.length?[0,m[1]]:m)}));h.search!==q&&k.extend(a.oPreviousSearch,Vb(h.search));if(h.columns)for(d=0,f=h.columns.length;d<f;d++)l=h.columns[d],l.visible!==q&&(e[d].bVisible=l.visible),l.search!==q&&k.extend(a.aoPreSearchCols[d],Vb(l.search));I(a,"aoStateLoaded","stateLoaded",[a,h])}}c()};if(a.oFeatures.bStateSave){var g=a.fnStateLoadCallback.call(a.oInstance,a,b);g!==
q&&b(g)}else c()}function Ra(a){var b=u.settings;a=k.inArray(a,T(b,"nTable"));return-1!==a?b[a]:null}function aa(a,b,c,d){c="DataTables warning: "+(a?"table id="+a.sTableId+" - ":"")+c;d&&(c+=". For more information about this error, please see http://datatables.net/tn/"+d);if(b)y.console&&console.log&&console.log(c);else if(b=u.ext,b=b.sErrMode||b.errMode,a&&I(a,null,"error",[a,d,c]),"alert"==b)alert(c);else{if("throw"==b)throw Error(c);"function"==typeof b&&b(a,d,c)}}function V(a,b,c,d){Array.isArray(c)?
k.each(c,function(f,e){Array.isArray(e)?V(a,b,e[0],e[1]):V(a,b,e)}):(d===q&&(d=c),b[c]!==q&&(a[d]=b[c]))}function pb(a,b,c){var d;for(d in b)if(b.hasOwnProperty(d)){var f=b[d];k.isPlainObject(f)?(k.isPlainObject(a[d])||(a[d]={}),k.extend(!0,a[d],f)):c&&"data"!==d&&"aaData"!==d&&Array.isArray(f)?a[d]=f.slice():a[d]=f}return a}function ob(a,b,c){k(a).on("click.DT",b,function(d){k(a).trigger("blur");c(d)}).on("keypress.DT",b,function(d){13===d.which&&(d.preventDefault(),c(d))}).on("selectstart.DT",function(){return!1})}
function Q(a,b,c,d){c&&a[b].push({fn:c,sName:d})}function I(a,b,c,d){var f=[];b&&(f=k.map(a[b].slice().reverse(),function(e,g){return e.fn.apply(a.oInstance,d)}));null!==c&&(b=k.Event(c+".dt"),k(a.nTable).trigger(b,d),f.push(b.result));return f}function jb(a){var b=a._iDisplayStart,c=a.fnDisplayEnd(),d=a._iDisplayLength;b>=c&&(b=c-d);b-=b%d;if(-1===d||0>b)b=0;a._iDisplayStart=b}function eb(a,b){a=a.renderer;var c=u.ext.renderer[b];return k.isPlainObject(a)&&a[b]?c[a[b]]||c._:"string"===typeof a?c[a]||
c._:c._}function P(a){return a.oFeatures.bServerSide?"ssp":a.ajax||a.sAjaxSource?"ajax":"dom"}function Ba(a,b){var c=ec.numbers_length,d=Math.floor(c/2);b<=c?a=qa(0,b):a<=d?(a=qa(0,c-2),a.push("ellipsis"),a.push(b-1)):(a>=b-1-d?a=qa(b-(c-2),b):(a=qa(a-d+2,a+d-1),a.push("ellipsis"),a.push(b-1)),a.splice(0,0,"ellipsis"),a.splice(0,0,0));a.DT_el="span";return a}function Va(a){k.each({num:function(b){return Sa(b,a)},"num-fmt":function(b){return Sa(b,a,qb)},"html-num":function(b){return Sa(b,a,Ta)},"html-num-fmt":function(b){return Sa(b,
a,Ta,qb)}},function(b,c){L.type.order[b+a+"-pre"]=c;b.match(/^html\-/)&&(L.type.search[b+a]=L.type.search.html)})}function fc(a){return function(){var b=[Ra(this[u.ext.iApiIndex])].concat(Array.prototype.slice.call(arguments));return u.ext.internal[a].apply(this,b)}}var u=function(a){this.$=function(e,g){return this.api(!0).$(e,g)};this._=function(e,g){return this.api(!0).rows(e,g).data()};this.api=function(e){return e?new D(Ra(this[L.iApiIndex])):new D(this)};this.fnAddData=function(e,g){var h=this.api(!0);
e=Array.isArray(e)&&(Array.isArray(e[0])||k.isPlainObject(e[0]))?h.rows.add(e):h.row.add(e);(g===q||g)&&h.draw();return e.flatten().toArray()};this.fnAdjustColumnSizing=function(e){var g=this.api(!0).columns.adjust(),h=g.settings()[0],l=h.oScroll;e===q||e?g.draw(!1):(""!==l.sX||""!==l.sY)&&Ea(h)};this.fnClearTable=function(e){var g=this.api(!0).clear();(e===q||e)&&g.draw()};this.fnClose=function(e){this.api(!0).row(e).child.hide()};this.fnDeleteRow=function(e,g,h){var l=this.api(!0);e=l.rows(e);var n=
e.settings()[0],m=n.aoData[e[0][0]];e.remove();g&&g.call(this,n,m);(h===q||h)&&l.draw();return m};this.fnDestroy=function(e){this.api(!0).destroy(e)};this.fnDraw=function(e){this.api(!0).draw(e)};this.fnFilter=function(e,g,h,l,n,m){n=this.api(!0);null===g||g===q?n.search(e,h,l,m):n.column(g).search(e,h,l,m);n.draw()};this.fnGetData=function(e,g){var h=this.api(!0);if(e!==q){var l=e.nodeName?e.nodeName.toLowerCase():"";return g!==q||"td"==l||"th"==l?h.cell(e,g).data():h.row(e).data()||null}return h.data().toArray()};
this.fnGetNodes=function(e){var g=this.api(!0);return e!==q?g.row(e).node():g.rows().nodes().flatten().toArray()};this.fnGetPosition=function(e){var g=this.api(!0),h=e.nodeName.toUpperCase();return"TR"==h?g.row(e).index():"TD"==h||"TH"==h?(e=g.cell(e).index(),[e.row,e.columnVisible,e.column]):null};this.fnIsOpen=function(e){return this.api(!0).row(e).child.isShown()};this.fnOpen=function(e,g,h){return this.api(!0).row(e).child(g,h).show().child()[0]};this.fnPageChange=function(e,g){e=this.api(!0).page(e);
(g===q||g)&&e.draw(!1)};this.fnSetColumnVis=function(e,g,h){e=this.api(!0).column(e).visible(g);(h===q||h)&&e.columns.adjust().draw()};this.fnSettings=function(){return Ra(this[L.iApiIndex])};this.fnSort=function(e){this.api(!0).order(e).draw()};this.fnSortListener=function(e,g,h){this.api(!0).order.listener(e,g,h)};this.fnUpdate=function(e,g,h,l,n){var m=this.api(!0);h===q||null===h?m.row(g).data(e):m.cell(g,h).data(e);(n===q||n)&&m.columns.adjust();(l===q||l)&&m.draw();return 0};this.fnVersionCheck=
L.fnVersionCheck;var b=this,c=a===q,d=this.length;c&&(a={});this.oApi=this.internal=L.internal;for(var f in u.ext.internal)f&&(this[f]=fc(f));this.each(function(){var e={},g=1<d?pb(e,a,!0):a,h=0,l;e=this.getAttribute("id");var n=!1,m=u.defaults,p=k(this);if("table"!=this.nodeName.toLowerCase())aa(null,0,"Non-table node initialisation ("+this.nodeName+")",2);else{yb(m);zb(m.column);O(m,m,!0);O(m.column,m.column,!0);O(m,k.extend(g,p.data()),!0);var t=u.settings;h=0;for(l=t.length;h<l;h++){var v=t[h];
if(v.nTable==this||v.nTHead&&v.nTHead.parentNode==this||v.nTFoot&&v.nTFoot.parentNode==this){var x=g.bRetrieve!==q?g.bRetrieve:m.bRetrieve;if(c||x)return v.oInstance;if(g.bDestroy!==q?g.bDestroy:m.bDestroy){v.oInstance.fnDestroy();break}else{aa(v,0,"Cannot reinitialise DataTable",3);return}}if(v.sTableId==this.id){t.splice(h,1);break}}if(null===e||""===e)this.id=e="DataTables_Table_"+u.ext._unique++;var r=k.extend(!0,{},u.models.oSettings,{sDestroyWidth:p[0].style.width,sInstance:e,sTableId:e});r.nTable=
this;r.oApi=b.internal;r.oInit=g;t.push(r);r.oInstance=1===b.length?b:p.dataTable();yb(g);ma(g.oLanguage);g.aLengthMenu&&!g.iDisplayLength&&(g.iDisplayLength=Array.isArray(g.aLengthMenu[0])?g.aLengthMenu[0][0]:g.aLengthMenu[0]);g=pb(k.extend(!0,{},m),g);V(r.oFeatures,g,"bPaginate bLengthChange bFilter bSort bSortMulti bInfo bProcessing bAutoWidth bSortClasses bServerSide bDeferRender".split(" "));V(r,g,["asStripeClasses","ajax","fnServerData","fnFormatNumber","sServerMethod","aaSorting","aaSortingFixed",
"aLengthMenu","sPaginationType","sAjaxSource","sAjaxDataProp","iStateDuration","sDom","bSortCellsTop","iTabIndex","fnStateLoadCallback","fnStateSaveCallback","renderer","searchDelay","rowId",["iCookieDuration","iStateDuration"],["oSearch","oPreviousSearch"],["aoSearchCols","aoPreSearchCols"],["iDisplayLength","_iDisplayLength"]]);V(r.oScroll,g,[["sScrollX","sX"],["sScrollXInner","sXInner"],["sScrollY","sY"],["bScrollCollapse","bCollapse"]]);V(r.oLanguage,g,"fnInfoCallback");Q(r,"aoDrawCallback",g.fnDrawCallback,
"user");Q(r,"aoServerParams",g.fnServerParams,"user");Q(r,"aoStateSaveParams",g.fnStateSaveParams,"user");Q(r,"aoStateLoadParams",g.fnStateLoadParams,"user");Q(r,"aoStateLoaded",g.fnStateLoaded,"user");Q(r,"aoRowCallback",g.fnRowCallback,"user");Q(r,"aoRowCreatedCallback",g.fnCreatedRow,"user");Q(r,"aoHeaderCallback",g.fnHeaderCallback,"user");Q(r,"aoFooterCallback",g.fnFooterCallback,"user");Q(r,"aoInitComplete",g.fnInitComplete,"user");Q(r,"aoPreDrawCallback",g.fnPreDrawCallback,"user");r.rowIdFn=
ia(g.rowId);Ab(r);var A=r.oClasses;k.extend(A,u.ext.classes,g.oClasses);p.addClass(A.sTable);r.iInitDisplayStart===q&&(r.iInitDisplayStart=g.iDisplayStart,r._iDisplayStart=g.iDisplayStart);null!==g.iDeferLoading&&(r.bDeferLoading=!0,e=Array.isArray(g.iDeferLoading),r._iRecordsDisplay=e?g.iDeferLoading[0]:g.iDeferLoading,r._iRecordsTotal=e?g.iDeferLoading[1]:g.iDeferLoading);var E=r.oLanguage;k.extend(!0,E,g.oLanguage);E.sUrl&&(k.ajax({dataType:"json",url:E.sUrl,success:function(C){ma(C);O(m.oLanguage,
C);k.extend(!0,E,C);za(r)},error:function(){za(r)}}),n=!0);null===g.asStripeClasses&&(r.asStripeClasses=[A.sStripeOdd,A.sStripeEven]);e=r.asStripeClasses;var H=p.children("tbody").find("tr").eq(0);-1!==k.inArray(!0,k.map(e,function(C,B){return H.hasClass(C)}))&&(k("tbody tr",this).removeClass(e.join(" ")),r.asDestroyStripes=e.slice());e=[];t=this.getElementsByTagName("thead");0!==t.length&&(wa(r.aoHeader,t[0]),e=Ka(r));if(null===g.aoColumns)for(t=[],h=0,l=e.length;h<l;h++)t.push(null);else t=g.aoColumns;
h=0;for(l=t.length;h<l;h++)Wa(r,e?e[h]:null);Cb(r,g.aoColumnDefs,t,function(C,B){Da(r,C,B)});if(H.length){var W=function(C,B){return null!==C.getAttribute("data-"+B)?B:null};k(H[0]).children("th, td").each(function(C,B){var ba=r.aoColumns[C];if(ba.mData===C){var X=W(B,"sort")||W(B,"order");B=W(B,"filter")||W(B,"search");if(null!==X||null!==B)ba.mData={_:C+".display",sort:null!==X?C+".@data-"+X:q,type:null!==X?C+".@data-"+X:q,filter:null!==B?C+".@data-"+B:q},Da(r,C)}})}var M=r.oFeatures;e=function(){if(g.aaSorting===
q){var C=r.aaSorting;h=0;for(l=C.length;h<l;h++)C[h][1]=r.aoColumns[h].asSorting[0]}Pa(r);M.bSort&&Q(r,"aoDrawCallback",function(){if(r.bSorted){var ba=pa(r),X={};k.each(ba,function(lb,Aa){X[Aa.src]=Aa.dir});I(r,null,"order",[r,ba,X]);cc(r)}});Q(r,"aoDrawCallback",function(){(r.bSorted||"ssp"===P(r)||M.bDeferRender)&&Pa(r)},"sc");C=p.children("caption").each(function(){this._captionSide=k(this).css("caption-side")});var B=p.children("thead");0===B.length&&(B=k("<thead/>").appendTo(p));r.nTHead=B[0];
B=p.children("tbody");0===B.length&&(B=k("<tbody/>").appendTo(p));r.nTBody=B[0];B=p.children("tfoot");0===B.length&&0<C.length&&(""!==r.oScroll.sX||""!==r.oScroll.sY)&&(B=k("<tfoot/>").appendTo(p));0===B.length||0===B.children().length?p.addClass(A.sNoFooter):0<B.length&&(r.nTFoot=B[0],wa(r.aoFooter,r.nTFoot));if(g.aaData)for(h=0;h<g.aaData.length;h++)ea(r,g.aaData[h]);else(r.bDeferLoading||"dom"==P(r))&&Ga(r,k(r.nTBody).children("tr"));r.aiDisplay=r.aiDisplayMaster.slice();r.bInitialised=!0;!1===
n&&za(r)};g.bStateSave?(M.bStateSave=!0,Q(r,"aoDrawCallback",Qa,"state_save"),dc(r,g,e)):e()}});b=null;return this},L,w,J,rb={},gc=/[\r\n\u2028]/g,Ta=/<.*?>/g,tc=/^\d{2,4}[\.\/\-]\d{1,2}[\.\/\-]\d{1,2}([T ]{1}\d{1,2}[:\.]\d{2}([\.:]\d{2})?)?$/,uc=/(\/|\.|\*|\+|\?|\||\(|\)|\[|\]|\{|\}|\\|\$|\^|\-)/g,qb=/['\u00A0,$£€¥%\u2009\u202F\u20BD\u20a9\u20BArfkɃΞ]/gi,ca=function(a){return a&&!0!==a&&"-"!==a?!1:!0},hc=function(a){var b=parseInt(a,10);return!isNaN(b)&&isFinite(a)?b:null},ic=function(a,b){rb[b]||
(rb[b]=new RegExp(hb(b),"g"));return"string"===typeof a&&"."!==b?a.replace(/\./g,"").replace(rb[b],"."):a},sb=function(a,b,c){var d="string"===typeof a;if(ca(a))return!0;b&&d&&(a=ic(a,b));c&&d&&(a=a.replace(qb,""));return!isNaN(parseFloat(a))&&isFinite(a)},jc=function(a,b,c){return ca(a)?!0:ca(a)||"string"===typeof a?sb(a.replace(Ta,""),b,c)?!0:null:null},T=function(a,b,c){var d=[],f=0,e=a.length;if(c!==q)for(;f<e;f++)a[f]&&a[f][b]&&d.push(a[f][b][c]);else for(;f<e;f++)a[f]&&d.push(a[f][b]);return d},
Ca=function(a,b,c,d){var f=[],e=0,g=b.length;if(d!==q)for(;e<g;e++)a[b[e]][c]&&f.push(a[b[e]][c][d]);else for(;e<g;e++)f.push(a[b[e]][c]);return f},qa=function(a,b){var c=[];if(b===q){b=0;var d=a}else d=b,b=a;for(a=b;a<d;a++)c.push(a);return c},kc=function(a){for(var b=[],c=0,d=a.length;c<d;c++)a[c]&&b.push(a[c]);return b},Ja=function(a){a:{if(!(2>a.length)){var b=a.slice().sort();for(var c=b[0],d=1,f=b.length;d<f;d++){if(b[d]===c){b=!1;break a}c=b[d]}}b=!0}if(b)return a.slice();b=[];f=a.length;var e,
g=0;d=0;a:for(;d<f;d++){c=a[d];for(e=0;e<g;e++)if(b[e]===c)continue a;b.push(c);g++}return b},lc=function(a,b){if(Array.isArray(b))for(var c=0;c<b.length;c++)lc(a,b[c]);else a.push(b);return a};Array.isArray||(Array.isArray=function(a){return"[object Array]"===Object.prototype.toString.call(a)});String.prototype.trim||(String.prototype.trim=function(){return this.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,"")});u.util={throttle:function(a,b){var c=b!==q?b:200,d,f;return function(){var e=this,g=
+new Date,h=arguments;d&&g<d+c?(clearTimeout(f),f=setTimeout(function(){d=q;a.apply(e,h)},c)):(d=g,a.apply(e,h))}},escapeRegex:function(a){return a.replace(uc,"\\$1")}};var R=function(a,b,c){a[b]!==q&&(a[c]=a[b])},ua=/\[.*?\]$/,oa=/\(\)$/,hb=u.util.escapeRegex,Oa=k("<div>")[0],rc=Oa.textContent!==q,sc=/<.*?>/g,fb=u.util.throttle,mc=[],N=Array.prototype,vc=function(a){var b,c=u.settings,d=k.map(c,function(e,g){return e.nTable});if(a){if(a.nTable&&a.oApi)return[a];if(a.nodeName&&"table"===a.nodeName.toLowerCase()){var f=
k.inArray(a,d);return-1!==f?[c[f]]:null}if(a&&"function"===typeof a.settings)return a.settings().toArray();"string"===typeof a?b=k(a):a instanceof k&&(b=a)}else return[];if(b)return b.map(function(e){f=k.inArray(this,d);return-1!==f?c[f]:null}).toArray()};var D=function(a,b){if(!(this instanceof D))return new D(a,b);var c=[],d=function(g){(g=vc(g))&&c.push.apply(c,g)};if(Array.isArray(a))for(var f=0,e=a.length;f<e;f++)d(a[f]);else d(a);this.context=Ja(c);b&&k.merge(this,b);this.selector={rows:null,
cols:null,opts:null};D.extend(this,this,mc)};u.Api=D;k.extend(D.prototype,{any:function(){return 0!==this.count()},concat:N.concat,context:[],count:function(){return this.flatten().length},each:function(a){for(var b=0,c=this.length;b<c;b++)a.call(this,this[b],b,this);return this},eq:function(a){var b=this.context;return b.length>a?new D(b[a],this[a]):null},filter:function(a){var b=[];if(N.filter)b=N.filter.call(this,a,this);else for(var c=0,d=this.length;c<d;c++)a.call(this,this[c],c,this)&&b.push(this[c]);
return new D(this.context,b)},flatten:function(){var a=[];return new D(this.context,a.concat.apply(a,this.toArray()))},join:N.join,indexOf:N.indexOf||function(a,b){b=b||0;for(var c=this.length;b<c;b++)if(this[b]===a)return b;return-1},iterator:function(a,b,c,d){var f=[],e,g,h=this.context,l,n=this.selector;"string"===typeof a&&(d=c,c=b,b=a,a=!1);var m=0;for(e=h.length;m<e;m++){var p=new D(h[m]);if("table"===b){var t=c.call(p,h[m],m);t!==q&&f.push(t)}else if("columns"===b||"rows"===b)t=c.call(p,h[m],
this[m],m),t!==q&&f.push(t);else if("column"===b||"column-rows"===b||"row"===b||"cell"===b){var v=this[m];"column-rows"===b&&(l=Ua(h[m],n.opts));var x=0;for(g=v.length;x<g;x++)t=v[x],t="cell"===b?c.call(p,h[m],t.row,t.column,m,x):c.call(p,h[m],t,m,x,l),t!==q&&f.push(t)}}return f.length||d?(a=new D(h,a?f.concat.apply([],f):f),b=a.selector,b.rows=n.rows,b.cols=n.cols,b.opts=n.opts,a):this},lastIndexOf:N.lastIndexOf||function(a,b){return this.indexOf.apply(this.toArray.reverse(),arguments)},length:0,
map:function(a){var b=[];if(N.map)b=N.map.call(this,a,this);else for(var c=0,d=this.length;c<d;c++)b.push(a.call(this,this[c],c));return new D(this.context,b)},pluck:function(a){return this.map(function(b){return b[a]})},pop:N.pop,push:N.push,reduce:N.reduce||function(a,b){return Bb(this,a,b,0,this.length,1)},reduceRight:N.reduceRight||function(a,b){return Bb(this,a,b,this.length-1,-1,-1)},reverse:N.reverse,selector:null,shift:N.shift,slice:function(){return new D(this.context,this)},sort:N.sort,
splice:N.splice,toArray:function(){return N.slice.call(this)},to$:function(){return k(this)},toJQuery:function(){return k(this)},unique:function(){return new D(this.context,Ja(this))},unshift:N.unshift});D.extend=function(a,b,c){if(c.length&&b&&(b instanceof D||b.__dt_wrapper)){var d,f=function(h,l,n){return function(){var m=l.apply(h,arguments);D.extend(m,m,n.methodExt);return m}};var e=0;for(d=c.length;e<d;e++){var g=c[e];b[g.name]="function"===g.type?f(a,g.val,g):"object"===g.type?{}:g.val;b[g.name].__dt_wrapper=
!0;D.extend(a,b[g.name],g.propExt)}}};D.register=w=function(a,b){if(Array.isArray(a))for(var c=0,d=a.length;c<d;c++)D.register(a[c],b);else{d=a.split(".");var f=mc,e;a=0;for(c=d.length;a<c;a++){var g=(e=-1!==d[a].indexOf("()"))?d[a].replace("()",""):d[a];a:{var h=0;for(var l=f.length;h<l;h++)if(f[h].name===g){h=f[h];break a}h=null}h||(h={name:g,val:{},methodExt:[],propExt:[],type:"object"},f.push(h));a===c-1?(h.val=b,h.type="function"===typeof b?"function":k.isPlainObject(b)?"object":"other"):f=e?
h.methodExt:h.propExt}}};D.registerPlural=J=function(a,b,c){D.register(a,c);D.register(b,function(){var d=c.apply(this,arguments);return d===this?this:d instanceof D?d.length?Array.isArray(d[0])?new D(d.context,d[0]):d[0]:q:d})};var nc=function(a,b){if(Array.isArray(a))return k.map(a,function(d){return nc(d,b)});if("number"===typeof a)return[b[a]];var c=k.map(b,function(d,f){return d.nTable});return k(c).filter(a).map(function(d){d=k.inArray(this,c);return b[d]}).toArray()};w("tables()",function(a){return a!==
q&&null!==a?new D(nc(a,this.context)):this});w("table()",function(a){a=this.tables(a);var b=a.context;return b.length?new D(b[0]):a});J("tables().nodes()","table().node()",function(){return this.iterator("table",function(a){return a.nTable},1)});J("tables().body()","table().body()",function(){return this.iterator("table",function(a){return a.nTBody},1)});J("tables().header()","table().header()",function(){return this.iterator("table",function(a){return a.nTHead},1)});J("tables().footer()","table().footer()",
function(){return this.iterator("table",function(a){return a.nTFoot},1)});J("tables().containers()","table().container()",function(){return this.iterator("table",function(a){return a.nTableWrapper},1)});w("draw()",function(a){return this.iterator("table",function(b){"page"===a?fa(b):("string"===typeof a&&(a="full-hold"===a?!1:!0),ja(b,!1===a))})});w("page()",function(a){return a===q?this.page.info().page:this.iterator("table",function(b){kb(b,a)})});w("page.info()",function(a){if(0===this.context.length)return q;
a=this.context[0];var b=a._iDisplayStart,c=a.oFeatures.bPaginate?a._iDisplayLength:-1,d=a.fnRecordsDisplay(),f=-1===c;return{page:f?0:Math.floor(b/c),pages:f?1:Math.ceil(d/c),start:b,end:a.fnDisplayEnd(),length:c,recordsTotal:a.fnRecordsTotal(),recordsDisplay:d,serverSide:"ssp"===P(a)}});w("page.len()",function(a){return a===q?0!==this.context.length?this.context[0]._iDisplayLength:q:this.iterator("table",function(b){ib(b,a)})});var oc=function(a,b,c){if(c){var d=new D(a);d.one("draw",function(){c(d.ajax.json())})}if("ssp"==
P(a))ja(a,b);else{U(a,!0);var f=a.jqXHR;f&&4!==f.readyState&&f.abort();La(a,[],function(e){Ha(a);e=Ma(a,e);for(var g=0,h=e.length;g<h;g++)ea(a,e[g]);ja(a,b);U(a,!1)})}};w("ajax.json()",function(){var a=this.context;if(0<a.length)return a[0].json});w("ajax.params()",function(){var a=this.context;if(0<a.length)return a[0].oAjaxData});w("ajax.reload()",function(a,b){return this.iterator("table",function(c){oc(c,!1===b,a)})});w("ajax.url()",function(a){var b=this.context;if(a===q){if(0===b.length)return q;
b=b[0];return b.ajax?k.isPlainObject(b.ajax)?b.ajax.url:b.ajax:b.sAjaxSource}return this.iterator("table",function(c){k.isPlainObject(c.ajax)?c.ajax.url=a:c.ajax=a})});w("ajax.url().load()",function(a,b){return this.iterator("table",function(c){oc(c,!1===b,a)})});var tb=function(a,b,c,d,f){var e=[],g,h,l;var n=typeof b;b&&"string"!==n&&"function"!==n&&b.length!==q||(b=[b]);n=0;for(h=b.length;n<h;n++){var m=b[n]&&b[n].split&&!b[n].match(/[\[\(:]/)?b[n].split(","):[b[n]];var p=0;for(l=m.length;p<l;p++)(g=
c("string"===typeof m[p]?m[p].trim():m[p]))&&g.length&&(e=e.concat(g))}a=L.selector[a];if(a.length)for(n=0,h=a.length;n<h;n++)e=a[n](d,f,e);return Ja(e)},ub=function(a){a||(a={});a.filter&&a.search===q&&(a.search=a.filter);return k.extend({search:"none",order:"current",page:"all"},a)},vb=function(a){for(var b=0,c=a.length;b<c;b++)if(0<a[b].length)return a[0]=a[b],a[0].length=1,a.length=1,a.context=[a.context[b]],a;a.length=0;return a},Ua=function(a,b){var c=[],d=a.aiDisplay;var f=a.aiDisplayMaster;
var e=b.search;var g=b.order;b=b.page;if("ssp"==P(a))return"removed"===e?[]:qa(0,f.length);if("current"==b)for(g=a._iDisplayStart,a=a.fnDisplayEnd();g<a;g++)c.push(d[g]);else if("current"==g||"applied"==g)if("none"==e)c=f.slice();else if("applied"==e)c=d.slice();else{if("removed"==e){var h={};g=0;for(a=d.length;g<a;g++)h[d[g]]=null;c=k.map(f,function(l){return h.hasOwnProperty(l)?null:l})}}else if("index"==g||"original"==g)for(g=0,a=a.aoData.length;g<a;g++)"none"==e?c.push(g):(f=k.inArray(g,d),(-1===
f&&"removed"==e||0<=f&&"applied"==e)&&c.push(g));return c},wc=function(a,b,c){var d;return tb("row",b,function(f){var e=hc(f),g=a.aoData;if(null!==e&&!c)return[e];d||(d=Ua(a,c));if(null!==e&&-1!==k.inArray(e,d))return[e];if(null===f||f===q||""===f)return d;if("function"===typeof f)return k.map(d,function(l){var n=g[l];return f(l,n._aData,n.nTr)?l:null});if(f.nodeName){e=f._DT_RowIndex;var h=f._DT_CellIndex;if(e!==q)return g[e]&&g[e].nTr===f?[e]:[];if(h)return g[h.row]&&g[h.row].nTr===f.parentNode?
[h.row]:[];e=k(f).closest("*[data-dt-row]");return e.length?[e.data("dt-row")]:[]}if("string"===typeof f&&"#"===f.charAt(0)&&(e=a.aIds[f.replace(/^#/,"")],e!==q))return[e.idx];e=kc(Ca(a.aoData,d,"nTr"));return k(e).filter(f).map(function(){return this._DT_RowIndex}).toArray()},a,c)};w("rows()",function(a,b){a===q?a="":k.isPlainObject(a)&&(b=a,a="");b=ub(b);var c=this.iterator("table",function(d){return wc(d,a,b)},1);c.selector.rows=a;c.selector.opts=b;return c});w("rows().nodes()",function(){return this.iterator("row",
function(a,b){return a.aoData[b].nTr||q},1)});w("rows().data()",function(){return this.iterator(!0,"rows",function(a,b){return Ca(a.aoData,b,"_aData")},1)});J("rows().cache()","row().cache()",function(a){return this.iterator("row",function(b,c){b=b.aoData[c];return"search"===a?b._aFilterData:b._aSortData},1)});J("rows().invalidate()","row().invalidate()",function(a){return this.iterator("row",function(b,c){va(b,c,a)})});J("rows().indexes()","row().index()",function(){return this.iterator("row",function(a,
b){return b},1)});J("rows().ids()","row().id()",function(a){for(var b=[],c=this.context,d=0,f=c.length;d<f;d++)for(var e=0,g=this[d].length;e<g;e++){var h=c[d].rowIdFn(c[d].aoData[this[d][e]]._aData);b.push((!0===a?"#":"")+h)}return new D(c,b)});J("rows().remove()","row().remove()",function(){var a=this;this.iterator("row",function(b,c,d){var f=b.aoData,e=f[c],g,h;f.splice(c,1);var l=0;for(g=f.length;l<g;l++){var n=f[l];var m=n.anCells;null!==n.nTr&&(n.nTr._DT_RowIndex=l);if(null!==m)for(n=0,h=m.length;n<
h;n++)m[n]._DT_CellIndex.row=l}Ia(b.aiDisplayMaster,c);Ia(b.aiDisplay,c);Ia(a[d],c,!1);0<b._iRecordsDisplay&&b._iRecordsDisplay--;jb(b);c=b.rowIdFn(e._aData);c!==q&&delete b.aIds[c]});this.iterator("table",function(b){for(var c=0,d=b.aoData.length;c<d;c++)b.aoData[c].idx=c});return this});w("rows.add()",function(a){var b=this.iterator("table",function(d){var f,e=[];var g=0;for(f=a.length;g<f;g++){var h=a[g];h.nodeName&&"TR"===h.nodeName.toUpperCase()?e.push(Ga(d,h)[0]):e.push(ea(d,h))}return e},1),
c=this.rows(-1);c.pop();k.merge(c,b);return c});w("row()",function(a,b){return vb(this.rows(a,b))});w("row().data()",function(a){var b=this.context;if(a===q)return b.length&&this.length?b[0].aoData[this[0]]._aData:q;var c=b[0].aoData[this[0]];c._aData=a;Array.isArray(a)&&c.nTr&&c.nTr.id&&da(b[0].rowId)(a,c.nTr.id);va(b[0],this[0],"data");return this});w("row().node()",function(){var a=this.context;return a.length&&this.length?a[0].aoData[this[0]].nTr||null:null});w("row.add()",function(a){a instanceof
k&&a.length&&(a=a[0]);var b=this.iterator("table",function(c){return a.nodeName&&"TR"===a.nodeName.toUpperCase()?Ga(c,a)[0]:ea(c,a)});return this.row(b[0])});var xc=function(a,b,c,d){var f=[],e=function(g,h){if(Array.isArray(g)||g instanceof k)for(var l=0,n=g.length;l<n;l++)e(g[l],h);else g.nodeName&&"tr"===g.nodeName.toLowerCase()?f.push(g):(l=k("<tr><td></td></tr>").addClass(h),k("td",l).addClass(h).html(g)[0].colSpan=na(a),f.push(l[0]))};e(c,d);b._details&&b._details.detach();b._details=k(f);b._detailsShow&&
b._details.insertAfter(b.nTr)},wb=function(a,b){var c=a.context;c.length&&(a=c[0].aoData[b!==q?b:a[0]])&&a._details&&(a._details.remove(),a._detailsShow=q,a._details=q)},pc=function(a,b){var c=a.context;c.length&&a.length&&(a=c[0].aoData[a[0]],a._details&&((a._detailsShow=b)?a._details.insertAfter(a.nTr):a._details.detach(),yc(c[0])))},yc=function(a){var b=new D(a),c=a.aoData;b.off("draw.dt.DT_details column-visibility.dt.DT_details destroy.dt.DT_details");0<T(c,"_details").length&&(b.on("draw.dt.DT_details",
function(d,f){a===f&&b.rows({page:"current"}).eq(0).each(function(e){e=c[e];e._detailsShow&&e._details.insertAfter(e.nTr)})}),b.on("column-visibility.dt.DT_details",function(d,f,e,g){if(a===f)for(f=na(f),e=0,g=c.length;e<g;e++)d=c[e],d._details&&d._details.children("td[colspan]").attr("colspan",f)}),b.on("destroy.dt.DT_details",function(d,f){if(a===f)for(d=0,f=c.length;d<f;d++)c[d]._details&&wb(b,d)}))};w("row().child()",function(a,b){var c=this.context;if(a===q)return c.length&&this.length?c[0].aoData[this[0]]._details:
q;!0===a?this.child.show():!1===a?wb(this):c.length&&this.length&&xc(c[0],c[0].aoData[this[0]],a,b);return this});w(["row().child.show()","row().child().show()"],function(a){pc(this,!0);return this});w(["row().child.hide()","row().child().hide()"],function(){pc(this,!1);return this});w(["row().child.remove()","row().child().remove()"],function(){wb(this);return this});w("row().child.isShown()",function(){var a=this.context;return a.length&&this.length?a[0].aoData[this[0]]._detailsShow||!1:!1});var zc=
/^([^:]+):(name|visIdx|visible)$/,qc=function(a,b,c,d,f){c=[];d=0;for(var e=f.length;d<e;d++)c.push(S(a,f[d],b));return c},Ac=function(a,b,c){var d=a.aoColumns,f=T(d,"sName"),e=T(d,"nTh");return tb("column",b,function(g){var h=hc(g);if(""===g)return qa(d.length);if(null!==h)return[0<=h?h:d.length+h];if("function"===typeof g){var l=Ua(a,c);return k.map(d,function(p,t){return g(t,qc(a,t,0,0,l),e[t])?t:null})}var n="string"===typeof g?g.match(zc):"";if(n)switch(n[2]){case "visIdx":case "visible":h=parseInt(n[1],
10);if(0>h){var m=k.map(d,function(p,t){return p.bVisible?t:null});return[m[m.length+h]]}return[sa(a,h)];case "name":return k.map(f,function(p,t){return p===n[1]?t:null});default:return[]}if(g.nodeName&&g._DT_CellIndex)return[g._DT_CellIndex.column];h=k(e).filter(g).map(function(){return k.inArray(this,e)}).toArray();if(h.length||!g.nodeName)return h;h=k(g).closest("*[data-dt-column]");return h.length?[h.data("dt-column")]:[]},a,c)};w("columns()",function(a,b){a===q?a="":k.isPlainObject(a)&&(b=a,
a="");b=ub(b);var c=this.iterator("table",function(d){return Ac(d,a,b)},1);c.selector.cols=a;c.selector.opts=b;return c});J("columns().header()","column().header()",function(a,b){return this.iterator("column",function(c,d){return c.aoColumns[d].nTh},1)});J("columns().footer()","column().footer()",function(a,b){return this.iterator("column",function(c,d){return c.aoColumns[d].nTf},1)});J("columns().data()","column().data()",function(){return this.iterator("column-rows",qc,1)});J("columns().dataSrc()",
"column().dataSrc()",function(){return this.iterator("column",function(a,b){return a.aoColumns[b].mData},1)});J("columns().cache()","column().cache()",function(a){return this.iterator("column-rows",function(b,c,d,f,e){return Ca(b.aoData,e,"search"===a?"_aFilterData":"_aSortData",c)},1)});J("columns().nodes()","column().nodes()",function(){return this.iterator("column-rows",function(a,b,c,d,f){return Ca(a.aoData,f,"anCells",b)},1)});J("columns().visible()","column().visible()",function(a,b){var c=
this,d=this.iterator("column",function(f,e){if(a===q)return f.aoColumns[e].bVisible;var g=f.aoColumns,h=g[e],l=f.aoData,n;if(a!==q&&h.bVisible!==a){if(a){var m=k.inArray(!0,T(g,"bVisible"),e+1);g=0;for(n=l.length;g<n;g++){var p=l[g].nTr;f=l[g].anCells;p&&p.insertBefore(f[e],f[m]||null)}}else k(T(f.aoData,"anCells",e)).detach();h.bVisible=a}});a!==q&&this.iterator("table",function(f){xa(f,f.aoHeader);xa(f,f.aoFooter);f.aiDisplay.length||k(f.nTBody).find("td[colspan]").attr("colspan",na(f));Qa(f);c.iterator("column",
function(e,g){I(e,null,"column-visibility",[e,g,a,b])});(b===q||b)&&c.columns.adjust()});return d});J("columns().indexes()","column().index()",function(a){return this.iterator("column",function(b,c){return"visible"===a?ta(b,c):c},1)});w("columns.adjust()",function(){return this.iterator("table",function(a){ra(a)},1)});w("column.index()",function(a,b){if(0!==this.context.length){var c=this.context[0];if("fromVisible"===a||"toData"===a)return sa(c,b);if("fromData"===a||"toVisible"===a)return ta(c,b)}});
w("column()",function(a,b){return vb(this.columns(a,b))});var Bc=function(a,b,c){var d=a.aoData,f=Ua(a,c),e=kc(Ca(d,f,"anCells")),g=k(lc([],e)),h,l=a.aoColumns.length,n,m,p,t,v,x;return tb("cell",b,function(r){var A="function"===typeof r;if(null===r||r===q||A){n=[];m=0;for(p=f.length;m<p;m++)for(h=f[m],t=0;t<l;t++)v={row:h,column:t},A?(x=d[h],r(v,S(a,h,t),x.anCells?x.anCells[t]:null)&&n.push(v)):n.push(v);return n}if(k.isPlainObject(r))return r.column!==q&&r.row!==q&&-1!==k.inArray(r.row,f)?[r]:[];
A=g.filter(r).map(function(E,H){return{row:H._DT_CellIndex.row,column:H._DT_CellIndex.column}}).toArray();if(A.length||!r.nodeName)return A;x=k(r).closest("*[data-dt-row]");return x.length?[{row:x.data("dt-row"),column:x.data("dt-column")}]:[]},a,c)};w("cells()",function(a,b,c){k.isPlainObject(a)&&(a.row===q?(c=a,a=null):(c=b,b=null));k.isPlainObject(b)&&(c=b,b=null);if(null===b||b===q)return this.iterator("table",function(m){return Bc(m,a,ub(c))});var d=c?{page:c.page,order:c.order,search:c.search}:
{},f=this.columns(b,d),e=this.rows(a,d),g,h,l,n;d=this.iterator("table",function(m,p){m=[];g=0;for(h=e[p].length;g<h;g++)for(l=0,n=f[p].length;l<n;l++)m.push({row:e[p][g],column:f[p][l]});return m},1);d=c&&c.selected?this.cells(d,c):d;k.extend(d.selector,{cols:b,rows:a,opts:c});return d});J("cells().nodes()","cell().node()",function(){return this.iterator("cell",function(a,b,c){return(a=a.aoData[b])&&a.anCells?a.anCells[c]:q},1)});w("cells().data()",function(){return this.iterator("cell",function(a,
b,c){return S(a,b,c)},1)});J("cells().cache()","cell().cache()",function(a){a="search"===a?"_aFilterData":"_aSortData";return this.iterator("cell",function(b,c,d){return b.aoData[c][a][d]},1)});J("cells().render()","cell().render()",function(a){return this.iterator("cell",function(b,c,d){return S(b,c,d,a)},1)});J("cells().indexes()","cell().index()",function(){return this.iterator("cell",function(a,b,c){return{row:b,column:c,columnVisible:ta(a,c)}},1)});J("cells().invalidate()","cell().invalidate()",
function(a){return this.iterator("cell",function(b,c,d){va(b,c,a,d)})});w("cell()",function(a,b,c){return vb(this.cells(a,b,c))});w("cell().data()",function(a){var b=this.context,c=this[0];if(a===q)return b.length&&c.length?S(b[0],c[0].row,c[0].column):q;Db(b[0],c[0].row,c[0].column,a);va(b[0],c[0].row,"data",c[0].column);return this});w("order()",function(a,b){var c=this.context;if(a===q)return 0!==c.length?c[0].aaSorting:q;"number"===typeof a?a=[[a,b]]:a.length&&!Array.isArray(a[0])&&(a=Array.prototype.slice.call(arguments));
return this.iterator("table",function(d){d.aaSorting=a.slice()})});w("order.listener()",function(a,b,c){return this.iterator("table",function(d){db(d,a,b,c)})});w("order.fixed()",function(a){if(!a){var b=this.context;b=b.length?b[0].aaSortingFixed:q;return Array.isArray(b)?{pre:b}:b}return this.iterator("table",function(c){c.aaSortingFixed=k.extend(!0,{},a)})});w(["columns().order()","column().order()"],function(a){var b=this;return this.iterator("table",function(c,d){var f=[];k.each(b[d],function(e,
g){f.push([g,a])});c.aaSorting=f})});w("search()",function(a,b,c,d){var f=this.context;return a===q?0!==f.length?f[0].oPreviousSearch.sSearch:q:this.iterator("table",function(e){e.oFeatures.bFilter&&ya(e,k.extend({},e.oPreviousSearch,{sSearch:a+"",bRegex:null===b?!1:b,bSmart:null===c?!0:c,bCaseInsensitive:null===d?!0:d}),1)})});J("columns().search()","column().search()",function(a,b,c,d){return this.iterator("column",function(f,e){var g=f.aoPreSearchCols;if(a===q)return g[e].sSearch;f.oFeatures.bFilter&&
(k.extend(g[e],{sSearch:a+"",bRegex:null===b?!1:b,bSmart:null===c?!0:c,bCaseInsensitive:null===d?!0:d}),ya(f,f.oPreviousSearch,1))})});w("state()",function(){return this.context.length?this.context[0].oSavedState:null});w("state.clear()",function(){return this.iterator("table",function(a){a.fnStateSaveCallback.call(a.oInstance,a,{})})});w("state.loaded()",function(){return this.context.length?this.context[0].oLoadedState:null});w("state.save()",function(){return this.iterator("table",function(a){Qa(a)})});
u.versionCheck=u.fnVersionCheck=function(a){var b=u.version.split(".");a=a.split(".");for(var c,d,f=0,e=a.length;f<e;f++)if(c=parseInt(b[f],10)||0,d=parseInt(a[f],10)||0,c!==d)return c>d;return!0};u.isDataTable=u.fnIsDataTable=function(a){var b=k(a).get(0),c=!1;if(a instanceof u.Api)return!0;k.each(u.settings,function(d,f){d=f.nScrollHead?k("table",f.nScrollHead)[0]:null;var e=f.nScrollFoot?k("table",f.nScrollFoot)[0]:null;if(f.nTable===b||d===b||e===b)c=!0});return c};u.tables=u.fnTables=function(a){var b=
!1;k.isPlainObject(a)&&(b=a.api,a=a.visible);var c=k.map(u.settings,function(d){if(!a||a&&k(d.nTable).is(":visible"))return d.nTable});return b?new D(c):c};u.camelToHungarian=O;w("$()",function(a,b){b=this.rows(b).nodes();b=k(b);return k([].concat(b.filter(a).toArray(),b.find(a).toArray()))});k.each(["on","one","off"],function(a,b){w(b+"()",function(){var c=Array.prototype.slice.call(arguments);c[0]=k.map(c[0].split(/\s/),function(f){return f.match(/\.dt\b/)?f:f+".dt"}).join(" ");var d=k(this.tables().nodes());
d[b].apply(d,c);return this})});w("clear()",function(){return this.iterator("table",function(a){Ha(a)})});w("settings()",function(){return new D(this.context,this.context)});w("init()",function(){var a=this.context;return a.length?a[0].oInit:null});w("data()",function(){return this.iterator("table",function(a){return T(a.aoData,"_aData")}).flatten()});w("destroy()",function(a){a=a||!1;return this.iterator("table",function(b){var c=b.nTableWrapper.parentNode,d=b.oClasses,f=b.nTable,e=b.nTBody,g=b.nTHead,
h=b.nTFoot,l=k(f);e=k(e);var n=k(b.nTableWrapper),m=k.map(b.aoData,function(t){return t.nTr}),p;b.bDestroying=!0;I(b,"aoDestroyCallback","destroy",[b]);a||(new D(b)).columns().visible(!0);n.off(".DT").find(":not(tbody *)").off(".DT");k(y).off(".DT-"+b.sInstance);f!=g.parentNode&&(l.children("thead").detach(),l.append(g));h&&f!=h.parentNode&&(l.children("tfoot").detach(),l.append(h));b.aaSorting=[];b.aaSortingFixed=[];Pa(b);k(m).removeClass(b.asStripeClasses.join(" "));k("th, td",g).removeClass(d.sSortable+
" "+d.sSortableAsc+" "+d.sSortableDesc+" "+d.sSortableNone);e.children().detach();e.append(m);g=a?"remove":"detach";l[g]();n[g]();!a&&c&&(c.insertBefore(f,b.nTableReinsertBefore),l.css("width",b.sDestroyWidth).removeClass(d.sTable),(p=b.asDestroyStripes.length)&&e.children().each(function(t){k(this).addClass(b.asDestroyStripes[t%p])}));c=k.inArray(b,u.settings);-1!==c&&u.settings.splice(c,1)})});k.each(["column","row","cell"],function(a,b){w(b+"s().every()",function(c){var d=this.selector.opts,f=
this;return this.iterator(b,function(e,g,h,l,n){c.call(f[b](g,"cell"===b?h:d,"cell"===b?d:q),g,h,l,n)})})});w("i18n()",function(a,b,c){var d=this.context[0];a=ia(a)(d.oLanguage);a===q&&(a=b);c!==q&&k.isPlainObject(a)&&(a=a[c]!==q?a[c]:a._);return a.replace("%d",c)});u.version="1.10.22";u.settings=[];u.models={};u.models.oSearch={bCaseInsensitive:!0,sSearch:"",bRegex:!1,bSmart:!0};u.models.oRow={nTr:null,anCells:null,_aData:[],_aSortData:null,_aFilterData:null,_sFilterRow:null,_sRowStripe:"",src:null,
idx:-1};u.models.oColumn={idx:null,aDataSort:null,asSorting:null,bSearchable:null,bSortable:null,bVisible:null,_sManualType:null,_bAttrSrc:!1,fnCreatedCell:null,fnGetData:null,fnSetData:null,mData:null,mRender:null,nTh:null,nTf:null,sClass:null,sContentPadding:null,sDefaultContent:null,sName:null,sSortDataType:"std",sSortingClass:null,sSortingClassJUI:null,sTitle:null,sType:null,sWidth:null,sWidthOrig:null};u.defaults={aaData:null,aaSorting:[[0,"asc"]],aaSortingFixed:[],ajax:null,aLengthMenu:[10,
25,50,100],aoColumns:null,aoColumnDefs:null,aoSearchCols:[],asStripeClasses:null,bAutoWidth:!0,bDeferRender:!1,bDestroy:!1,bFilter:!0,bInfo:!0,bLengthChange:!0,bPaginate:!0,bProcessing:!1,bRetrieve:!1,bScrollCollapse:!1,bServerSide:!1,bSort:!0,bSortMulti:!0,bSortCellsTop:!1,bSortClasses:!0,bStateSave:!1,fnCreatedRow:null,fnDrawCallback:null,fnFooterCallback:null,fnFormatNumber:function(a){return a.toString().replace(/\B(?=(\d{3})+(?!\d))/g,this.oLanguage.sThousands)},fnHeaderCallback:null,fnInfoCallback:null,
fnInitComplete:null,fnPreDrawCallback:null,fnRowCallback:null,fnServerData:null,fnServerParams:null,fnStateLoadCallback:function(a){try{return JSON.parse((-1===a.iStateDuration?sessionStorage:localStorage).getItem("DataTables_"+a.sInstance+"_"+location.pathname))}catch(b){return{}}},fnStateLoadParams:null,fnStateLoaded:null,fnStateSaveCallback:function(a,b){try{(-1===a.iStateDuration?sessionStorage:localStorage).setItem("DataTables_"+a.sInstance+"_"+location.pathname,JSON.stringify(b))}catch(c){}},
fnStateSaveParams:null,iStateDuration:7200,iDeferLoading:null,iDisplayLength:10,iDisplayStart:0,iTabIndex:0,oClasses:{},oLanguage:{oAria:{sSortAscending:": activate to sort column ascending",sSortDescending:": activate to sort column descending"},oPaginate:{sFirst:"First",sLast:"Last",sNext:"Next",sPrevious:"Previous"},sEmptyTable:"No data available in table",sInfo:"Showing _START_ to _END_ of _TOTAL_ entries",sInfoEmpty:"Showing 0 to 0 of 0 entries",sInfoFiltered:"(filtered from _MAX_ total entries)",
sInfoPostFix:"",sDecimal:"",sThousands:",",sLengthMenu:"Show _MENU_ entries",sLoadingRecords:"Loading...",sProcessing:"Processing...",sSearch:"Search:",sSearchPlaceholder:"",sUrl:"",sZeroRecords:"No matching records found"},oSearch:k.extend({},u.models.oSearch),sAjaxDataProp:"data",sAjaxSource:null,sDom:"lfrtip",searchDelay:null,sPaginationType:"simple_numbers",sScrollX:"",sScrollXInner:"",sScrollY:"",sServerMethod:"GET",renderer:null,rowId:"DT_RowId"};G(u.defaults);u.defaults.column={aDataSort:null,
iDataSort:-1,asSorting:["asc","desc"],bSearchable:!0,bSortable:!0,bVisible:!0,fnCreatedCell:null,mData:null,mRender:null,sCellType:"td",sClass:"",sContentPadding:"",sDefaultContent:null,sName:"",sSortDataType:"std",sTitle:null,sType:null,sWidth:null};G(u.defaults.column);u.models.oSettings={oFeatures:{bAutoWidth:null,bDeferRender:null,bFilter:null,bInfo:null,bLengthChange:null,bPaginate:null,bProcessing:null,bServerSide:null,bSort:null,bSortMulti:null,bSortClasses:null,bStateSave:null},oScroll:{bCollapse:null,
iBarWidth:0,sX:null,sXInner:null,sY:null},oLanguage:{fnInfoCallback:null},oBrowser:{bScrollOversize:!1,bScrollbarLeft:!1,bBounding:!1,barWidth:0},ajax:null,aanFeatures:[],aoData:[],aiDisplay:[],aiDisplayMaster:[],aIds:{},aoColumns:[],aoHeader:[],aoFooter:[],oPreviousSearch:{},aoPreSearchCols:[],aaSorting:null,aaSortingFixed:[],asStripeClasses:null,asDestroyStripes:[],sDestroyWidth:0,aoRowCallback:[],aoHeaderCallback:[],aoFooterCallback:[],aoDrawCallback:[],aoRowCreatedCallback:[],aoPreDrawCallback:[],
aoInitComplete:[],aoStateSaveParams:[],aoStateLoadParams:[],aoStateLoaded:[],sTableId:"",nTable:null,nTHead:null,nTFoot:null,nTBody:null,nTableWrapper:null,bDeferLoading:!1,bInitialised:!1,aoOpenRows:[],sDom:null,searchDelay:null,sPaginationType:"two_button",iStateDuration:0,aoStateSave:[],aoStateLoad:[],oSavedState:null,oLoadedState:null,sAjaxSource:null,sAjaxDataProp:null,bAjaxDataGet:!0,jqXHR:null,json:q,oAjaxData:q,fnServerData:null,aoServerParams:[],sServerMethod:null,fnFormatNumber:null,aLengthMenu:null,
iDraw:0,bDrawing:!1,iDrawError:-1,_iDisplayLength:10,_iDisplayStart:0,_iRecordsTotal:0,_iRecordsDisplay:0,oClasses:{},bFiltered:!1,bSorted:!1,bSortCellsTop:null,oInit:null,aoDestroyCallback:[],fnRecordsTotal:function(){return"ssp"==P(this)?1*this._iRecordsTotal:this.aiDisplayMaster.length},fnRecordsDisplay:function(){return"ssp"==P(this)?1*this._iRecordsDisplay:this.aiDisplay.length},fnDisplayEnd:function(){var a=this._iDisplayLength,b=this._iDisplayStart,c=b+a,d=this.aiDisplay.length,f=this.oFeatures,
e=f.bPaginate;return f.bServerSide?!1===e||-1===a?b+d:Math.min(b+a,this._iRecordsDisplay):!e||c>d||-1===a?d:c},oInstance:null,sInstance:null,iTabIndex:0,nScrollHead:null,nScrollFoot:null,aLastSort:[],oPlugins:{},rowIdFn:null,rowId:null};u.ext=L={buttons:{},classes:{},builder:"bs4/dt-1.10.22/fh-3.1.7/r-2.2.6/sc-2.0.3/sp-1.2.2",errMode:"alert",feature:[],search:[],selector:{cell:[],column:[],row:[]},internal:{},legacy:{ajax:null},pager:{},renderer:{pageButton:{},header:{}},order:{},type:{detect:[],search:{},order:{}},_unique:0,fnVersionCheck:u.fnVersionCheck,
iApiIndex:0,oJUIClasses:{},sVersion:u.version};k.extend(L,{afnFiltering:L.search,aTypes:L.type.detect,ofnSearch:L.type.search,oSort:L.type.order,afnSortData:L.order,aoFeatures:L.feature,oApi:L.internal,oStdClasses:L.classes,oPagination:L.pager});k.extend(u.ext.classes,{sTable:"dataTable",sNoFooter:"no-footer",sPageButton:"paginate_button",sPageButtonActive:"current",sPageButtonDisabled:"disabled",sStripeOdd:"odd",sStripeEven:"even",sRowEmpty:"dataTables_empty",sWrapper:"dataTables_wrapper",sFilter:"dataTables_filter",
sInfo:"dataTables_info",sPaging:"dataTables_paginate paging_",sLength:"dataTables_length",sProcessing:"dataTables_processing",sSortAsc:"sorting_asc",sSortDesc:"sorting_desc",sSortable:"sorting",sSortableAsc:"sorting_asc_disabled",sSortableDesc:"sorting_desc_disabled",sSortableNone:"sorting_disabled",sSortColumn:"sorting_",sFilterInput:"",sLengthSelect:"",sScrollWrapper:"dataTables_scroll",sScrollHead:"dataTables_scrollHead",sScrollHeadInner:"dataTables_scrollHeadInner",sScrollBody:"dataTables_scrollBody",
sScrollFoot:"dataTables_scrollFoot",sScrollFootInner:"dataTables_scrollFootInner",sHeaderTH:"",sFooterTH:"",sSortJUIAsc:"",sSortJUIDesc:"",sSortJUI:"",sSortJUIAscAllowed:"",sSortJUIDescAllowed:"",sSortJUIWrapper:"",sSortIcon:"",sJUIHeader:"",sJUIFooter:""});var ec=u.ext.pager;k.extend(ec,{simple:function(a,b){return["previous","next"]},full:function(a,b){return["first","previous","next","last"]},numbers:function(a,b){return[Ba(a,b)]},simple_numbers:function(a,b){return["previous",Ba(a,b),"next"]},
full_numbers:function(a,b){return["first","previous",Ba(a,b),"next","last"]},first_last_numbers:function(a,b){return["first",Ba(a,b),"last"]},_numbers:Ba,numbers_length:7});k.extend(!0,u.ext.renderer,{pageButton:{_:function(a,b,c,d,f,e){var g=a.oClasses,h=a.oLanguage.oPaginate,l=a.oLanguage.oAria.paginate||{},n,m,p=0,t=function(x,r){var A,E=g.sPageButtonDisabled,H=function(B){kb(a,B.data.action,!0)};var W=0;for(A=r.length;W<A;W++){var M=r[W];if(Array.isArray(M)){var C=k("<"+(M.DT_el||"div")+"/>").appendTo(x);
t(C,M)}else{n=null;m=M;C=a.iTabIndex;switch(M){case "ellipsis":x.append('<span class="ellipsis">&#x2026;</span>');break;case "first":n=h.sFirst;0===f&&(C=-1,m+=" "+E);break;case "previous":n=h.sPrevious;0===f&&(C=-1,m+=" "+E);break;case "next":n=h.sNext;if(0===e||f===e-1)C=-1,m+=" "+E;break;case "last":n=h.sLast;if(0===e||f===e-1)C=-1,m+=" "+E;break;default:n=a.fnFormatNumber(M+1),m=f===M?g.sPageButtonActive:""}null!==n&&(C=k("<a>",{"class":g.sPageButton+" "+m,"aria-controls":a.sTableId,"aria-label":l[M],
"data-dt-idx":p,tabindex:C,id:0===c&&"string"===typeof M?a.sTableId+"_"+M:null}).html(n).appendTo(x),ob(C,{action:M},H),p++)}}};try{var v=k(b).find(z.activeElement).data("dt-idx")}catch(x){}t(k(b).empty(),d);v!==q&&k(b).find("[data-dt-idx="+v+"]").trigger("focus")}}});k.extend(u.ext.type.detect,[function(a,b){b=b.oLanguage.sDecimal;return sb(a,b)?"num"+b:null},function(a,b){if(a&&!(a instanceof Date)&&!tc.test(a))return null;b=Date.parse(a);return null!==b&&!isNaN(b)||ca(a)?"date":null},function(a,
b){b=b.oLanguage.sDecimal;return sb(a,b,!0)?"num-fmt"+b:null},function(a,b){b=b.oLanguage.sDecimal;return jc(a,b)?"html-num"+b:null},function(a,b){b=b.oLanguage.sDecimal;return jc(a,b,!0)?"html-num-fmt"+b:null},function(a,b){return ca(a)||"string"===typeof a&&-1!==a.indexOf("<")?"html":null}]);k.extend(u.ext.type.search,{html:function(a){return ca(a)?a:"string"===typeof a?a.replace(gc," ").replace(Ta,""):""},string:function(a){return ca(a)?a:"string"===typeof a?a.replace(gc," "):a}});var Sa=function(a,
b,c,d){if(0!==a&&(!a||"-"===a))return-Infinity;b&&(a=ic(a,b));a.replace&&(c&&(a=a.replace(c,"")),d&&(a=a.replace(d,"")));return 1*a};k.extend(L.type.order,{"date-pre":function(a){a=Date.parse(a);return isNaN(a)?-Infinity:a},"html-pre":function(a){return ca(a)?"":a.replace?a.replace(/<.*?>/g,"").toLowerCase():a+""},"string-pre":function(a){return ca(a)?"":"string"===typeof a?a.toLowerCase():a.toString?a.toString():""},"string-asc":function(a,b){return a<b?-1:a>b?1:0},"string-desc":function(a,b){return a<
b?1:a>b?-1:0}});Va("");k.extend(!0,u.ext.renderer,{header:{_:function(a,b,c,d){k(a.nTable).on("order.dt.DT",function(f,e,g,h){a===e&&(f=c.idx,b.removeClass(c.sSortingClass+" "+d.sSortAsc+" "+d.sSortDesc).addClass("asc"==h[f]?d.sSortAsc:"desc"==h[f]?d.sSortDesc:c.sSortingClass))})},jqueryui:function(a,b,c,d){k("<div/>").addClass(d.sSortJUIWrapper).append(b.contents()).append(k("<span/>").addClass(d.sSortIcon+" "+c.sSortingClassJUI)).appendTo(b);k(a.nTable).on("order.dt.DT",function(f,e,g,h){a===e&&
(f=c.idx,b.removeClass(d.sSortAsc+" "+d.sSortDesc).addClass("asc"==h[f]?d.sSortAsc:"desc"==h[f]?d.sSortDesc:c.sSortingClass),b.find("span."+d.sSortIcon).removeClass(d.sSortJUIAsc+" "+d.sSortJUIDesc+" "+d.sSortJUI+" "+d.sSortJUIAscAllowed+" "+d.sSortJUIDescAllowed).addClass("asc"==h[f]?d.sSortJUIAsc:"desc"==h[f]?d.sSortJUIDesc:c.sSortingClassJUI))})}}});var xb=function(a){return"string"===typeof a?a.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;"):a};u.render=
{number:function(a,b,c,d,f){return{display:function(e){if("number"!==typeof e&&"string"!==typeof e)return e;var g=0>e?"-":"",h=parseFloat(e);if(isNaN(h))return xb(e);h=h.toFixed(c);e=Math.abs(h);h=parseInt(e,10);e=c?b+(e-h).toFixed(c).substring(2):"";return g+(d||"")+h.toString().replace(/\B(?=(\d{3})+(?!\d))/g,a)+e+(f||"")}}},text:function(){return{display:xb,filter:xb}}};k.extend(u.ext.internal,{_fnExternApiFunc:fc,_fnBuildAjax:La,_fnAjaxUpdate:Fb,_fnAjaxParameters:Ob,_fnAjaxUpdateDraw:Pb,_fnAjaxDataSrc:Ma,
_fnAddColumn:Wa,_fnColumnOptions:Da,_fnAdjustColumnSizing:ra,_fnVisibleToColumnIndex:sa,_fnColumnIndexToVisible:ta,_fnVisbleColumns:na,_fnGetColumns:Fa,_fnColumnTypes:Ya,_fnApplyColumnDefs:Cb,_fnHungarianMap:G,_fnCamelToHungarian:O,_fnLanguageCompat:ma,_fnBrowserDetect:Ab,_fnAddData:ea,_fnAddTr:Ga,_fnNodeToDataIndex:function(a,b){return b._DT_RowIndex!==q?b._DT_RowIndex:null},_fnNodeToColumnIndex:function(a,b,c){return k.inArray(c,a.aoData[b].anCells)},_fnGetCellData:S,_fnSetCellData:Db,_fnSplitObjNotation:ab,
_fnGetObjectDataFn:ia,_fnSetObjectDataFn:da,_fnGetDataMaster:bb,_fnClearTable:Ha,_fnDeleteIndex:Ia,_fnInvalidate:va,_fnGetRowElements:$a,_fnCreateTr:Za,_fnBuildHead:Eb,_fnDrawHead:xa,_fnDraw:fa,_fnReDraw:ja,_fnAddOptionsHtml:Hb,_fnDetectHeader:wa,_fnGetUniqueThs:Ka,_fnFeatureHtmlFilter:Jb,_fnFilterComplete:ya,_fnFilterCustom:Sb,_fnFilterColumn:Rb,_fnFilter:Qb,_fnFilterCreateSearch:gb,_fnEscapeRegex:hb,_fnFilterData:Tb,_fnFeatureHtmlInfo:Mb,_fnUpdateInfo:Wb,_fnInfoMacros:Xb,_fnInitialise:za,_fnInitComplete:Na,
_fnLengthChange:ib,_fnFeatureHtmlLength:Ib,_fnFeatureHtmlPaginate:Nb,_fnPageChange:kb,_fnFeatureHtmlProcessing:Kb,_fnProcessingDisplay:U,_fnFeatureHtmlTable:Lb,_fnScrollDraw:Ea,_fnApplyToChildren:Z,_fnCalculateColumnWidths:Xa,_fnThrottle:fb,_fnConvertToWidth:Zb,_fnGetWidestNode:$b,_fnGetMaxLenString:ac,_fnStringToCss:K,_fnSortFlatten:pa,_fnSort:Gb,_fnSortAria:cc,_fnSortListener:nb,_fnSortAttachListener:db,_fnSortingClasses:Pa,_fnSortData:bc,_fnSaveState:Qa,_fnLoadState:dc,_fnSettingsFromNode:Ra,_fnLog:aa,
_fnMap:V,_fnBindAction:ob,_fnCallbackReg:Q,_fnCallbackFire:I,_fnLengthOverflow:jb,_fnRenderer:eb,_fnDataSource:P,_fnRowAttributes:cb,_fnExtend:pb,_fnCalculateEnd:function(){}});k.fn.dataTable=u;u.$=k;k.fn.dataTableSettings=u.settings;k.fn.dataTableExt=u.ext;k.fn.DataTable=function(a){return k(this).dataTable(a).api()};k.each(u,function(a,b){k.fn.DataTable[a]=b});return k.fn.dataTable});
/*!
DataTables Bootstrap 4 integration
©2011-2017 SpryMedia Ltd - datatables.net/license
*/
var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.findInternal=function(a,b,c){a instanceof String&&(a=String(a));for(var e=a.length,d=0;d<e;d++){var f=a[d];if(b.call(c,f,d,a))return{i:d,v:f}}return{i:-1,v:void 0}};$jscomp.ASSUME_ES5=!1;$jscomp.ASSUME_NO_NATIVE_MAP=!1;$jscomp.ASSUME_NO_NATIVE_SET=!1;$jscomp.SIMPLE_FROUND_POLYFILL=!1;$jscomp.ISOLATE_POLYFILLS=!1;
$jscomp.defineProperty=$jscomp.ASSUME_ES5||"function"==typeof Object.defineProperties?Object.defineProperty:function(a,b,c){if(a==Array.prototype||a==Object.prototype)return a;a[b]=c.value;return a};$jscomp.getGlobal=function(a){a=["object"==typeof globalThis&&globalThis,a,"object"==typeof window&&window,"object"==typeof self&&self,"object"==typeof global&&global];for(var b=0;b<a.length;++b){var c=a[b];if(c&&c.Math==Math)return c}throw Error("Cannot find global object");};$jscomp.global=$jscomp.getGlobal(this);
$jscomp.IS_SYMBOL_NATIVE="function"===typeof Symbol&&"symbol"===typeof Symbol("x");$jscomp.TRUST_ES6_POLYFILLS=!$jscomp.ISOLATE_POLYFILLS||$jscomp.IS_SYMBOL_NATIVE;$jscomp.polyfills={};$jscomp.propertyToPolyfillSymbol={};$jscomp.POLYFILL_PREFIX="$jscp$";var $jscomp$lookupPolyfilledValue=function(a,b){var c=$jscomp.propertyToPolyfillSymbol[b];if(null==c)return a[b];c=a[c];return void 0!==c?c:a[b]};
$jscomp.polyfill=function(a,b,c,e){b&&($jscomp.ISOLATE_POLYFILLS?$jscomp.polyfillIsolated(a,b,c,e):$jscomp.polyfillUnisolated(a,b,c,e))};$jscomp.polyfillUnisolated=function(a,b,c,e){c=$jscomp.global;a=a.split(".");for(e=0;e<a.length-1;e++){var d=a[e];if(!(d in c))return;c=c[d]}a=a[a.length-1];e=c[a];b=b(e);b!=e&&null!=b&&$jscomp.defineProperty(c,a,{configurable:!0,writable:!0,value:b})};
$jscomp.polyfillIsolated=function(a,b,c,e){var d=a.split(".");a=1===d.length;e=d[0];e=!a&&e in $jscomp.polyfills?$jscomp.polyfills:$jscomp.global;for(var f=0;f<d.length-1;f++){var l=d[f];if(!(l in e))return;e=e[l]}d=d[d.length-1];c=$jscomp.IS_SYMBOL_NATIVE&&"es6"===c?e[d]:null;b=b(c);null!=b&&(a?$jscomp.defineProperty($jscomp.polyfills,d,{configurable:!0,writable:!0,value:b}):b!==c&&($jscomp.propertyToPolyfillSymbol[d]=$jscomp.IS_SYMBOL_NATIVE?$jscomp.global.Symbol(d):$jscomp.POLYFILL_PREFIX+d,d=
$jscomp.propertyToPolyfillSymbol[d],$jscomp.defineProperty(e,d,{configurable:!0,writable:!0,value:b})))};$jscomp.polyfill("Array.prototype.find",function(a){return a?a:function(b,c){return $jscomp.findInternal(this,b,c).v}},"es6","es3");
(function(a){"function"===typeof define&&define.amd?define(["jquery","datatables.net"],function(b){return a(b,window,document)}):"object"===typeof exports?module.exports=function(b,c){b||(b=window);c&&c.fn.dataTable||(c=require("datatables.net")(b,c).$);return a(c,b,b.document)}:a(jQuery,window,document)})(function(a,b,c,e){var d=a.fn.dataTable;a.extend(!0,d.defaults,{dom:"<'row'<'col-sm-12 col-md-6'l><'col-sm-12 col-md-6'f>><'row'<'col-sm-12'tr>><'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
renderer:"bootstrap"});a.extend(d.ext.classes,{sWrapper:"dataTables_wrapper dt-bootstrap4",sFilterInput:"form-control form-control-sm",sLengthSelect:"custom-select custom-select-sm form-control form-control-sm",sProcessing:"dataTables_processing card",sPageButton:"paginate_button page-item"});d.ext.renderer.pageButton.bootstrap=function(f,l,A,B,m,t){var u=new d.Api(f),C=f.oClasses,n=f.oLanguage.oPaginate,D=f.oLanguage.oAria.paginate||{},h,k,v=0,y=function(q,w){var x,E=function(p){p.preventDefault();
a(p.currentTarget).hasClass("disabled")||u.page()==p.data.action||u.page(p.data.action).draw("page")};var r=0;for(x=w.length;r<x;r++){var g=w[r];if(Array.isArray(g))y(q,g);else{k=h="";switch(g){case "ellipsis":h="&#x2026;";k="disabled";break;case "first":h=n.sFirst;k=g+(0<m?"":" disabled");break;case "previous":h=n.sPrevious;k=g+(0<m?"":" disabled");break;case "next":h=n.sNext;k=g+(m<t-1?"":" disabled");break;case "last":h=n.sLast;k=g+(m<t-1?"":" disabled");break;default:h=g+1,k=m===g?"active":""}if(h){var F=
a("<li>",{"class":C.sPageButton+" "+k,id:0===A&&"string"===typeof g?f.sTableId+"_"+g:null}).append(a("<a>",{href:"#","aria-controls":f.sTableId,"aria-label":D[g],"data-dt-idx":v,tabindex:f.iTabIndex,"class":"page-link"}).html(h)).appendTo(q);f.oApi._fnBindAction(F,{action:g},E);v++}}}};try{var z=a(l).find(c.activeElement).data("dt-idx")}catch(q){}y(a(l).empty().html('<ul class="pagination"/>').children("ul"),B);z!==e&&a(l).find("[data-dt-idx="+z+"]").trigger("focus")};return d});
/*!
Copyright 2009-2020 SpryMedia Ltd.
This source file is free software, available under the following license:
MIT license - http://datatables.net/license/mit
This source file is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
or FITNESS FOR A PARTICULAR PURPOSE. See the license files for details.
For details please refer to: http://www.datatables.net
FixedHeader 3.1.7
©2009-2020 SpryMedia Ltd - datatables.net/license
*/
var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.findInternal=function(c,d,f){c instanceof String&&(c=String(c));for(var h=c.length,g=0;g<h;g++){var m=c[g];if(d.call(f,m,g,c))return{i:g,v:m}}return{i:-1,v:void 0}};$jscomp.ASSUME_ES5=!1;$jscomp.ASSUME_NO_NATIVE_MAP=!1;$jscomp.ASSUME_NO_NATIVE_SET=!1;$jscomp.SIMPLE_FROUND_POLYFILL=!1;
$jscomp.defineProperty=$jscomp.ASSUME_ES5||"function"==typeof Object.defineProperties?Object.defineProperty:function(c,d,f){c!=Array.prototype&&c!=Object.prototype&&(c[d]=f.value)};$jscomp.getGlobal=function(c){c=["object"==typeof window&&window,"object"==typeof self&&self,"object"==typeof global&&global,c];for(var d=0;d<c.length;++d){var f=c[d];if(f&&f.Math==Math)return f}throw Error("Cannot find global object");};$jscomp.global=$jscomp.getGlobal(this);
$jscomp.polyfill=function(c,d,f,h){if(d){f=$jscomp.global;c=c.split(".");for(h=0;h<c.length-1;h++){var g=c[h];g in f||(f[g]={});f=f[g]}c=c[c.length-1];h=f[c];d=d(h);d!=h&&null!=d&&$jscomp.defineProperty(f,c,{configurable:!0,writable:!0,value:d})}};$jscomp.polyfill("Array.prototype.find",function(c){return c?c:function(c,f){return $jscomp.findInternal(this,c,f).v}},"es6","es3");
(function(c){"function"===typeof define&&define.amd?define(["jquery","datatables.net"],function(d){return c(d,window,document)}):"object"===typeof exports?module.exports=function(d,f){d||(d=window);f&&f.fn.dataTable||(f=require("datatables.net")(d,f).$);return c(f,d,d.document)}:c(jQuery,window,document)})(function(c,d,f,h){var g=c.fn.dataTable,m=0,l=function(a,b){if(!(this instanceof l))throw"FixedHeader must be initialised with the 'new' keyword.";!0===b&&(b={});a=new g.Api(a);this.c=c.extend(!0,
{},l.defaults,b);this.s={dt:a,position:{theadTop:0,tbodyTop:0,tfootTop:0,tfootBottom:0,width:0,left:0,tfootHeight:0,theadHeight:0,windowHeight:c(d).height(),visible:!0},headerMode:null,footerMode:null,autoWidth:a.settings()[0].oFeatures.bAutoWidth,namespace:".dtfc"+m++,scrollLeft:{header:-1,footer:-1},enable:!0};this.dom={floatingHeader:null,thead:c(a.table().header()),tbody:c(a.table().body()),tfoot:c(a.table().footer()),header:{host:null,floating:null,placeholder:null},footer:{host:null,floating:null,
placeholder:null}};this.dom.header.host=this.dom.thead.parent();this.dom.footer.host=this.dom.tfoot.parent();a=a.settings()[0];if(a._fixedHeader)throw"FixedHeader already initialised on table "+a.nTable.id;a._fixedHeader=this;this._constructor()};c.extend(l.prototype,{destroy:function(){this.s.dt.off(".dtfc");c(d).off(this.s.namespace);this.c.header&&this._modeChange("in-place","header",!0);this.c.footer&&this.dom.tfoot.length&&this._modeChange("in-place","footer",!0)},enable:function(a,b){this.s.enable=
a;if(b||b===h)this._positions(),this._scroll(!0)},enabled:function(){return this.s.enable},headerOffset:function(a){a!==h&&(this.c.headerOffset=a,this.update());return this.c.headerOffset},footerOffset:function(a){a!==h&&(this.c.footerOffset=a,this.update());return this.c.footerOffset},update:function(){var a=this.s.dt.table().node();c(a).is(":visible")?this.enable(!0,!1):this.enable(!1,!1);this._positions();this._scroll(!0)},_constructor:function(){var a=this,b=this.s.dt;c(d).on("scroll"+this.s.namespace,
function(){a._scroll()}).on("resize"+this.s.namespace,g.util.throttle(function(){a.s.position.windowHeight=c(d).height();a.update()},50));var k=c(".fh-fixedHeader");!this.c.headerOffset&&k.length&&(this.c.headerOffset=k.outerHeight());k=c(".fh-fixedFooter");!this.c.footerOffset&&k.length&&(this.c.footerOffset=k.outerHeight());b.on("column-reorder.dt.dtfc column-visibility.dt.dtfc draw.dt.dtfc column-sizing.dt.dtfc responsive-display.dt.dtfc",function(){a.update()});b.on("destroy.dtfc",function(){a.destroy()});
this._positions();this._scroll()},_clone:function(a,b){var k=this.s.dt,e=this.dom[a],f="header"===a?this.dom.thead:this.dom.tfoot;!b&&e.floating?e.floating.removeClass("fixedHeader-floating fixedHeader-locked"):(e.floating&&(e.placeholder.remove(),this._unsize(a),e.floating.children().detach(),e.floating.remove()),e.floating=c(k.table().node().cloneNode(!1)).css("table-layout","fixed").attr("aria-hidden","true").removeAttr("id").append(f).appendTo("body"),e.placeholder=f.clone(!1),e.placeholder.find("*[id]").removeAttr("id"),
e.host.prepend(e.placeholder),this._matchWidths(e.placeholder,e.floating))},_matchWidths:function(a,b){var k=function(b){return c(b,a).map(function(){return c(this).width()}).toArray()},e=function(a,e){c(a,b).each(function(a){c(this).css({width:e[a],minWidth:e[a]})})},f=k("th");k=k("td");e("th",f);e("td",k)},_unsize:function(a){var b=this.dom[a].floating;b&&("footer"===a||"header"===a&&!this.s.autoWidth)?c("th, td",b).css({width:"",minWidth:""}):b&&"header"===a&&c("th, td",b).css("min-width","")},
_horizontal:function(a,b){var c=this.dom[a],e=this.s.position,f=this.s.scrollLeft;c.floating&&f[a]!==b&&(c.floating.css("left",e.left-b),f[a]=b)},_modeChange:function(a,b,k){var e=this.dom[b],d=this.s.position,g=function(a){e.floating.attr("style",function(b,c){return(c||"")+"width: "+a+"px !important;"})},h=this.dom["footer"===b?"tfoot":"thead"],l=c.contains(h[0],f.activeElement)?f.activeElement:null;l&&l.blur();"in-place"===a?(e.placeholder&&(e.placeholder.remove(),e.placeholder=null),this._unsize(b),
"header"===b?e.host.prepend(h):e.host.append(h),e.floating&&(e.floating.remove(),e.floating=null)):"in"===a?(this._clone(b,k),e.floating.addClass("fixedHeader-floating").css("header"===b?"top":"bottom",this.c[b+"Offset"]).css("left",d.left+"px"),g(d.width),"footer"===b&&e.floating.css("top","")):"below"===a?(this._clone(b,k),e.floating.addClass("fixedHeader-locked").css("top",d.tfootTop-d.theadHeight).css("left",d.left+"px"),g(d.width)):"above"===a&&(this._clone(b,k),e.floating.addClass("fixedHeader-locked").css("top",
d.tbodyTop).css("left",d.left+"px"),g(d.width));l&&l!==f.activeElement&&setTimeout(function(){l.focus()},10);this.s.scrollLeft.header=-1;this.s.scrollLeft.footer=-1;this.s[b+"Mode"]=a},_positions:function(){var a=this.s.dt.table(),b=this.s.position,f=this.dom;a=c(a.node());var e=a.children("thead"),d=a.children("tfoot");f=f.tbody;b.visible=a.is(":visible");b.width=a.outerWidth();b.left=a.offset().left;b.theadTop=e.offset().top;b.tbodyTop=f.offset().top;b.tbodyHeight=f.outerHeight();b.theadHeight=
b.tbodyTop-b.theadTop;d.length?(b.tfootTop=d.offset().top,b.tfootBottom=b.tfootTop+d.outerHeight(),b.tfootHeight=b.tfootBottom-b.tfootTop):(b.tfootTop=b.tbodyTop+f.outerHeight(),b.tfootBottom=b.tfootTop,b.tfootHeight=b.tfootTop)},_scroll:function(a){var b=c(f).scrollTop(),d=c(f).scrollLeft(),e=this.s.position;if(this.c.header){var g=this.s.enable?!e.visible||b<=e.theadTop-this.c.headerOffset?"in-place":b<=e.tfootTop-e.theadHeight-this.c.headerOffset?"in":"below":"in-place";(a||g!==this.s.headerMode)&&
this._modeChange(g,"header",a);this._horizontal("header",d)}this.c.footer&&this.dom.tfoot.length&&(b=this.s.enable?!e.visible||b+e.windowHeight>=e.tfootBottom+this.c.footerOffset?"in-place":e.windowHeight+b>e.tbodyTop+e.tfootHeight+this.c.footerOffset?"in":"above":"in-place",(a||b!==this.s.footerMode)&&this._modeChange(b,"footer",a),this._horizontal("footer",d))}});l.version="3.1.7";l.defaults={header:!0,footer:!1,headerOffset:0,footerOffset:0};c.fn.dataTable.FixedHeader=l;c.fn.DataTable.FixedHeader=
l;c(f).on("init.dt.dtfh",function(a,b,d){"dt"===a.namespace&&(a=b.oInit.fixedHeader,d=g.defaults.fixedHeader,!a&&!d||b._fixedHeader||(d=c.extend({},d,a),!1!==a&&new l(b,d)))});g.Api.register("fixedHeader()",function(){});g.Api.register("fixedHeader.adjust()",function(){return this.iterator("table",function(a){(a=a._fixedHeader)&&a.update()})});g.Api.register("fixedHeader.enable()",function(a){return this.iterator("table",function(b){b=b._fixedHeader;a=a!==h?a:!0;b&&a!==b.enabled()&&b.enable(a)})});
g.Api.register("fixedHeader.enabled()",function(){if(this.context.length){var a=this.content[0]._fixedHeader;if(a)return a.enabled()}return!1});g.Api.register("fixedHeader.disable()",function(){return this.iterator("table",function(a){(a=a._fixedHeader)&&a.enabled()&&a.enable(!1)})});c.each(["header","footer"],function(a,b){g.Api.register("fixedHeader."+b+"Offset()",function(a){var c=this.context;return a===h?c.length&&c[0]._fixedHeader?c[0]._fixedHeader[b+"Offset"]():h:this.iterator("table",function(c){if(c=
c._fixedHeader)c[b+"Offset"](a)})})});return l});
/*!
Bootstrap 4 styling wrapper for FixedHeader
©2018 SpryMedia Ltd - datatables.net/license
*/
(function(c){"function"===typeof define&&define.amd?define(["jquery","datatables.net-bs4","datatables.net-fixedheader"],function(a){return c(a,window,document)}):"object"===typeof exports?module.exports=function(a,b){a||(a=window);b&&b.fn.dataTable||(b=require("datatables.net-bs4")(a,b).$);b.fn.dataTable.FixedHeader||require("datatables.net-fixedheader")(a,b);return c(b,a,a.document)}:c(jQuery,window,document)})(function(c,a,b,d){return c.fn.dataTable});
/*!
Copyright 2014-2020 SpryMedia Ltd.
This source file is free software, available under the following license:
MIT license - http://datatables.net/license/mit
This source file is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
or FITNESS FOR A PARTICULAR PURPOSE. See the license files for details.
For details please refer to: http://www.datatables.net
Responsive 2.2.6
2014-2020 SpryMedia Ltd - datatables.net/license
*/
var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.findInternal=function(b,k,m){b instanceof String&&(b=String(b));for(var n=b.length,p=0;p<n;p++){var y=b[p];if(k.call(m,y,p,b))return{i:p,v:y}}return{i:-1,v:void 0}};$jscomp.ASSUME_ES5=!1;$jscomp.ASSUME_NO_NATIVE_MAP=!1;$jscomp.ASSUME_NO_NATIVE_SET=!1;$jscomp.SIMPLE_FROUND_POLYFILL=!1;$jscomp.ISOLATE_POLYFILLS=!1;
$jscomp.defineProperty=$jscomp.ASSUME_ES5||"function"==typeof Object.defineProperties?Object.defineProperty:function(b,k,m){if(b==Array.prototype||b==Object.prototype)return b;b[k]=m.value;return b};$jscomp.getGlobal=function(b){b=["object"==typeof globalThis&&globalThis,b,"object"==typeof window&&window,"object"==typeof self&&self,"object"==typeof global&&global];for(var k=0;k<b.length;++k){var m=b[k];if(m&&m.Math==Math)return m}throw Error("Cannot find global object");};$jscomp.global=$jscomp.getGlobal(this);
$jscomp.IS_SYMBOL_NATIVE="function"===typeof Symbol&&"symbol"===typeof Symbol("x");$jscomp.TRUST_ES6_POLYFILLS=!$jscomp.ISOLATE_POLYFILLS||$jscomp.IS_SYMBOL_NATIVE;$jscomp.polyfills={};$jscomp.propertyToPolyfillSymbol={};$jscomp.POLYFILL_PREFIX="$jscp$";var $jscomp$lookupPolyfilledValue=function(b,k){var m=$jscomp.propertyToPolyfillSymbol[k];if(null==m)return b[k];m=b[m];return void 0!==m?m:b[k]};
$jscomp.polyfill=function(b,k,m,n){k&&($jscomp.ISOLATE_POLYFILLS?$jscomp.polyfillIsolated(b,k,m,n):$jscomp.polyfillUnisolated(b,k,m,n))};$jscomp.polyfillUnisolated=function(b,k,m,n){m=$jscomp.global;b=b.split(".");for(n=0;n<b.length-1;n++){var p=b[n];if(!(p in m))return;m=m[p]}b=b[b.length-1];n=m[b];k=k(n);k!=n&&null!=k&&$jscomp.defineProperty(m,b,{configurable:!0,writable:!0,value:k})};
$jscomp.polyfillIsolated=function(b,k,m,n){var p=b.split(".");b=1===p.length;n=p[0];n=!b&&n in $jscomp.polyfills?$jscomp.polyfills:$jscomp.global;for(var y=0;y<p.length-1;y++){var z=p[y];if(!(z in n))return;n=n[z]}p=p[p.length-1];m=$jscomp.IS_SYMBOL_NATIVE&&"es6"===m?n[p]:null;k=k(m);null!=k&&(b?$jscomp.defineProperty($jscomp.polyfills,p,{configurable:!0,writable:!0,value:k}):k!==m&&($jscomp.propertyToPolyfillSymbol[p]=$jscomp.IS_SYMBOL_NATIVE?$jscomp.global.Symbol(p):$jscomp.POLYFILL_PREFIX+p,p=
$jscomp.propertyToPolyfillSymbol[p],$jscomp.defineProperty(n,p,{configurable:!0,writable:!0,value:k})))};$jscomp.polyfill("Array.prototype.find",function(b){return b?b:function(k,m){return $jscomp.findInternal(this,k,m).v}},"es6","es3");
(function(b){"function"===typeof define&&define.amd?define(["jquery","datatables.net"],function(k){return b(k,window,document)}):"object"===typeof exports?module.exports=function(k,m){k||(k=window);m&&m.fn.dataTable||(m=require("datatables.net")(k,m).$);return b(m,k,k.document)}:b(jQuery,window,document)})(function(b,k,m,n){function p(a,c,d){var f=c+"-"+d;if(A[f])return A[f];var g=[];a=a.cell(c,d).node().childNodes;c=0;for(d=a.length;c<d;c++)g.push(a[c]);return A[f]=g}function y(a,c,d){var f=c+"-"+
d;if(A[f]){a=a.cell(c,d).node();d=A[f][0].parentNode.childNodes;c=[];for(var g=0,l=d.length;g<l;g++)c.push(d[g]);d=0;for(g=c.length;d<g;d++)a.appendChild(c[d]);A[f]=n}}var z=b.fn.dataTable,u=function(a,c){if(!z.versionCheck||!z.versionCheck("1.10.10"))throw"DataTables Responsive requires DataTables 1.10.10 or newer";this.s={dt:new z.Api(a),columns:[],current:[]};this.s.dt.settings()[0].responsive||(c&&"string"===typeof c.details?c.details={type:c.details}:c&&!1===c.details?c.details={type:!1}:c&&
!0===c.details&&(c.details={type:"inline"}),this.c=b.extend(!0,{},u.defaults,z.defaults.responsive,c),a.responsive=this,this._constructor())};b.extend(u.prototype,{_constructor:function(){var a=this,c=this.s.dt,d=c.settings()[0],f=b(k).innerWidth();c.settings()[0]._responsive=this;b(k).on("resize.dtr orientationchange.dtr",z.util.throttle(function(){var g=b(k).innerWidth();g!==f&&(a._resize(),f=g)}));d.oApi._fnCallbackReg(d,"aoRowCreatedCallback",function(g,l,h){-1!==b.inArray(!1,a.s.current)&&b(">td, >th",
g).each(function(e){e=c.column.index("toData",e);!1===a.s.current[e]&&b(this).css("display","none")})});c.on("destroy.dtr",function(){c.off(".dtr");b(c.table().body()).off(".dtr");b(k).off("resize.dtr orientationchange.dtr");c.cells(".dtr-control").nodes().to$().removeClass("dtr-control");b.each(a.s.current,function(g,l){!1===l&&a._setColumnVis(g,!0)})});this.c.breakpoints.sort(function(g,l){return g.width<l.width?1:g.width>l.width?-1:0});this._classLogic();this._resizeAuto();d=this.c.details;!1!==
d.type&&(a._detailsInit(),c.on("column-visibility.dtr",function(){a._timer&&clearTimeout(a._timer);a._timer=setTimeout(function(){a._timer=null;a._classLogic();a._resizeAuto();a._resize(!0);a._redrawChildren()},100)}),c.on("draw.dtr",function(){a._redrawChildren()}),b(c.table().node()).addClass("dtr-"+d.type));c.on("column-reorder.dtr",function(g,l,h){a._classLogic();a._resizeAuto();a._resize(!0)});c.on("column-sizing.dtr",function(){a._resizeAuto();a._resize()});c.on("preXhr.dtr",function(){var g=
[];c.rows().every(function(){this.child.isShown()&&g.push(this.id(!0))});c.one("draw.dtr",function(){a._resizeAuto();a._resize();c.rows(g).every(function(){a._detailsDisplay(this,!1)})})});c.on("draw.dtr",function(){a._controlClass()}).on("init.dtr",function(g,l,h){"dt"===g.namespace&&(a._resizeAuto(),a._resize(),b.inArray(!1,a.s.current)&&c.columns.adjust())});this._resize()},_columnsVisiblity:function(a){var c=this.s.dt,d=this.s.columns,f,g=d.map(function(t,v){return{columnIdx:v,priority:t.priority}}).sort(function(t,
v){return t.priority!==v.priority?t.priority-v.priority:t.columnIdx-v.columnIdx}),l=b.map(d,function(t,v){return!1===c.column(v).visible()?"not-visible":t.auto&&null===t.minWidth?!1:!0===t.auto?"-":-1!==b.inArray(a,t.includeIn)}),h=0;var e=0;for(f=l.length;e<f;e++)!0===l[e]&&(h+=d[e].minWidth);e=c.settings()[0].oScroll;e=e.sY||e.sX?e.iBarWidth:0;h=c.table().container().offsetWidth-e-h;e=0;for(f=l.length;e<f;e++)d[e].control&&(h-=d[e].minWidth);var r=!1;e=0;for(f=g.length;e<f;e++){var q=g[e].columnIdx;
"-"===l[q]&&!d[q].control&&d[q].minWidth&&(r||0>h-d[q].minWidth?(r=!0,l[q]=!1):l[q]=!0,h-=d[q].minWidth)}g=!1;e=0;for(f=d.length;e<f;e++)if(!d[e].control&&!d[e].never&&!1===l[e]){g=!0;break}e=0;for(f=d.length;e<f;e++)d[e].control&&(l[e]=g),"not-visible"===l[e]&&(l[e]=!1);-1===b.inArray(!0,l)&&(l[0]=!0);return l},_classLogic:function(){var a=this,c=this.c.breakpoints,d=this.s.dt,f=d.columns().eq(0).map(function(h){var e=this.column(h),r=e.header().className;h=d.settings()[0].aoColumns[h].responsivePriority;
e=e.header().getAttribute("data-priority");h===n&&(h=e===n||null===e?1E4:1*e);return{className:r,includeIn:[],auto:!1,control:!1,never:r.match(/\bnever\b/)?!0:!1,priority:h}}),g=function(h,e){h=f[h].includeIn;-1===b.inArray(e,h)&&h.push(e)},l=function(h,e,r,q){if(!r)f[h].includeIn.push(e);else if("max-"===r)for(q=a._find(e).width,e=0,r=c.length;e<r;e++)c[e].width<=q&&g(h,c[e].name);else if("min-"===r)for(q=a._find(e).width,e=0,r=c.length;e<r;e++)c[e].width>=q&&g(h,c[e].name);else if("not-"===r)for(e=
0,r=c.length;e<r;e++)-1===c[e].name.indexOf(q)&&g(h,c[e].name)};f.each(function(h,e){for(var r=h.className.split(" "),q=!1,t=0,v=r.length;t<v;t++){var B=r[t].trim();if("all"===B){q=!0;h.includeIn=b.map(c,function(w){return w.name});return}if("none"===B||h.never){q=!0;return}if("control"===B||"dtr-control"===B){q=!0;h.control=!0;return}b.each(c,function(w,D){w=D.name.split("-");var x=B.match(new RegExp("(min\\-|max\\-|not\\-)?("+w[0]+")(\\-[_a-zA-Z0-9])?"));x&&(q=!0,x[2]===w[0]&&x[3]==="-"+w[1]?l(e,
D.name,x[1],x[2]+x[3]):x[2]!==w[0]||x[3]||l(e,D.name,x[1],x[2]))})}q||(h.auto=!0)});this.s.columns=f},_controlClass:function(){if("inline"===this.c.details.type){var a=this.s.dt,c=b.inArray(!0,this.s.current);a.cells(null,function(d){return d!==c},{page:"current"}).nodes().to$().filter(".dtr-control").removeClass("dtr-control");a.cells(null,c,{page:"current"}).nodes().to$().addClass("dtr-control")}},_detailsDisplay:function(a,c){var d=this,f=this.s.dt,g=this.c.details;if(g&&!1!==g.type){var l=g.display(a,
c,function(){return g.renderer(f,a[0],d._detailsObj(a[0]))});!0!==l&&!1!==l||b(f.table().node()).triggerHandler("responsive-display.dt",[f,a,l,c])}},_detailsInit:function(){var a=this,c=this.s.dt,d=this.c.details;"inline"===d.type&&(d.target="td.dtr-control, th.dtr-control");c.on("draw.dtr",function(){a._tabIndexes()});a._tabIndexes();b(c.table().body()).on("keyup.dtr","td, th",function(g){13===g.keyCode&&b(this).data("dtr-keyboard")&&b(this).click()});var f=d.target;d="string"===typeof f?f:"td, th";
if(f!==n||null!==f)b(c.table().body()).on("click.dtr mousedown.dtr mouseup.dtr",d,function(g){if(b(c.table().node()).hasClass("collapsed")&&-1!==b.inArray(b(this).closest("tr").get(0),c.rows().nodes().toArray())){if("number"===typeof f){var l=0>f?c.columns().eq(0).length+f:f;if(c.cell(this).index().column!==l)return}l=c.row(b(this).closest("tr"));"click"===g.type?a._detailsDisplay(l,!1):"mousedown"===g.type?b(this).css("outline","none"):"mouseup"===g.type&&b(this).trigger("blur").css("outline","")}})},
_detailsObj:function(a){var c=this,d=this.s.dt;return b.map(this.s.columns,function(f,g){if(!f.never&&!f.control)return f=d.settings()[0].aoColumns[g],{className:f.sClass,columnIndex:g,data:d.cell(a,g).render(c.c.orthogonal),hidden:d.column(g).visible()&&!c.s.current[g],rowIndex:a,title:null!==f.sTitle?f.sTitle:b(d.column(g).header()).text()}})},_find:function(a){for(var c=this.c.breakpoints,d=0,f=c.length;d<f;d++)if(c[d].name===a)return c[d]},_redrawChildren:function(){var a=this,c=this.s.dt;c.rows({page:"current"}).iterator("row",
function(d,f){c.row(f);a._detailsDisplay(c.row(f),!0)})},_resize:function(a){var c=this,d=this.s.dt,f=b(k).innerWidth(),g=this.c.breakpoints,l=g[0].name,h=this.s.columns,e,r=this.s.current.slice();for(e=g.length-1;0<=e;e--)if(f<=g[e].width){l=g[e].name;break}var q=this._columnsVisiblity(l);this.s.current=q;g=!1;e=0;for(f=h.length;e<f;e++)if(!1===q[e]&&!h[e].never&&!h[e].control&&!1===!d.column(e).visible()){g=!0;break}b(d.table().node()).toggleClass("collapsed",g);var t=!1,v=0;d.columns().eq(0).each(function(B,
w){!0===q[w]&&v++;if(a||q[w]!==r[w])t=!0,c._setColumnVis(B,q[w])});t&&(this._redrawChildren(),b(d.table().node()).trigger("responsive-resize.dt",[d,this.s.current]),0===d.page.info().recordsDisplay&&b("td",d.table().body()).eq(0).attr("colspan",v));c._controlClass()},_resizeAuto:function(){var a=this.s.dt,c=this.s.columns;if(this.c.auto&&-1!==b.inArray(!0,b.map(c,function(e){return e.auto}))){b.isEmptyObject(A)||b.each(A,function(e){e=e.split("-");y(a,1*e[0],1*e[1])});a.table().node();var d=a.table().node().cloneNode(!1),
f=b(a.table().header().cloneNode(!1)).appendTo(d),g=b(a.table().body()).clone(!1,!1).empty().appendTo(d);d.style.width="auto";var l=a.columns().header().filter(function(e){return a.column(e).visible()}).to$().clone(!1).css("display","table-cell").css("width","auto").css("min-width",0);b(g).append(b(a.rows({page:"current"}).nodes()).clone(!1)).find("th, td").css("display","");if(g=a.table().footer()){g=b(g.cloneNode(!1)).appendTo(d);var h=a.columns().footer().filter(function(e){return a.column(e).visible()}).to$().clone(!1).css("display",
"table-cell");b("<tr/>").append(h).appendTo(g)}b("<tr/>").append(l).appendTo(f);"inline"===this.c.details.type&&b(d).addClass("dtr-inline collapsed");b(d).find("[name]").removeAttr("name");b(d).css("position","relative");d=b("<div/>").css({width:1,height:1,overflow:"hidden",clear:"both"}).append(d);d.insertBefore(a.table().node());l.each(function(e){e=a.column.index("fromVisible",e);c[e].minWidth=this.offsetWidth||0});d.remove()}},_responsiveOnlyHidden:function(){var a=this.s.dt;return b.map(this.s.current,
function(c,d){return!1===a.column(d).visible()?!0:c})},_setColumnVis:function(a,c){var d=this.s.dt;c=c?"":"none";b(d.column(a).header()).css("display",c);b(d.column(a).footer()).css("display",c);d.column(a).nodes().to$().css("display",c);b.isEmptyObject(A)||d.cells(null,a).indexes().each(function(f){y(d,f.row,f.column)})},_tabIndexes:function(){var a=this.s.dt,c=a.cells({page:"current"}).nodes().to$(),d=a.settings()[0],f=this.c.details.target;c.filter("[data-dtr-keyboard]").removeData("[data-dtr-keyboard]");
"number"===typeof f?a.cells(null,f,{page:"current"}).nodes().to$().attr("tabIndex",d.iTabIndex).data("dtr-keyboard",1):("td:first-child, th:first-child"===f&&(f=">td:first-child, >th:first-child"),b(f,a.rows({page:"current"}).nodes()).attr("tabIndex",d.iTabIndex).data("dtr-keyboard",1))}});u.breakpoints=[{name:"desktop",width:Infinity},{name:"tablet-l",width:1024},{name:"tablet-p",width:768},{name:"mobile-l",width:480},{name:"mobile-p",width:320}];u.display={childRow:function(a,c,d){if(c){if(b(a.node()).hasClass("parent"))return a.child(d(),
"child").show(),!0}else{if(a.child.isShown())return a.child(!1),b(a.node()).removeClass("parent"),!1;a.child(d(),"child").show();b(a.node()).addClass("parent");return!0}},childRowImmediate:function(a,c,d){if(!c&&a.child.isShown()||!a.responsive.hasHidden())return a.child(!1),b(a.node()).removeClass("parent"),!1;a.child(d(),"child").show();b(a.node()).addClass("parent");return!0},modal:function(a){return function(c,d,f){if(d)b("div.dtr-modal-content").empty().append(f());else{var g=function(){l.remove();
b(m).off("keypress.dtr")},l=b('<div class="dtr-modal"/>').append(b('<div class="dtr-modal-display"/>').append(b('<div class="dtr-modal-content"/>').append(f())).append(b('<div class="dtr-modal-close">&times;</div>').click(function(){g()}))).append(b('<div class="dtr-modal-background"/>').click(function(){g()})).appendTo("body");b(m).on("keyup.dtr",function(h){27===h.keyCode&&(h.stopPropagation(),g())})}a&&a.header&&b("div.dtr-modal-content").prepend("<h2>"+a.header(c)+"</h2>")}}};var A={};u.renderer=
{listHiddenNodes:function(){return function(a,c,d){var f=b('<ul data-dtr-index="'+c+'" class="dtr-details"/>'),g=!1;b.each(d,function(l,h){h.hidden&&(b("<li "+(h.className?'class="'+h.className+'"':"")+' data-dtr-index="'+h.columnIndex+'" data-dt-row="'+h.rowIndex+'" data-dt-column="'+h.columnIndex+'"><span class="dtr-title">'+h.title+"</span> </li>").append(b('<span class="dtr-data"/>').append(p(a,h.rowIndex,h.columnIndex))).appendTo(f),g=!0)});return g?f:!1}},listHidden:function(){return function(a,
c,d){return(a=b.map(d,function(f){var g=f.className?'class="'+f.className+'"':"";return f.hidden?"<li "+g+' data-dtr-index="'+f.columnIndex+'" data-dt-row="'+f.rowIndex+'" data-dt-column="'+f.columnIndex+'"><span class="dtr-title">'+f.title+'</span> <span class="dtr-data">'+f.data+"</span></li>":""}).join(""))?b('<ul data-dtr-index="'+c+'" class="dtr-details"/>').append(a):!1}},tableAll:function(a){a=b.extend({tableClass:""},a);return function(c,d,f){c=b.map(f,function(g){return"<tr "+(g.className?
'class="'+g.className+'"':"")+' data-dt-row="'+g.rowIndex+'" data-dt-column="'+g.columnIndex+'"><td>'+g.title+":</td> <td>"+g.data+"</td></tr>"}).join("");return b('<table class="'+a.tableClass+' dtr-details" width="100%"/>').append(c)}}};u.defaults={breakpoints:u.breakpoints,auto:!0,details:{display:u.display.childRow,renderer:u.renderer.listHidden(),target:0,type:"inline"},orthogonal:"display"};var C=b.fn.dataTable.Api;C.register("responsive()",function(){return this});C.register("responsive.index()",
function(a){a=b(a);return{column:a.data("dtr-index"),row:a.parent().data("dtr-index")}});C.register("responsive.rebuild()",function(){return this.iterator("table",function(a){a._responsive&&a._responsive._classLogic()})});C.register("responsive.recalc()",function(){return this.iterator("table",function(a){a._responsive&&(a._responsive._resizeAuto(),a._responsive._resize())})});C.register("responsive.hasHidden()",function(){var a=this.context[0];return a._responsive?-1!==b.inArray(!1,a._responsive._responsiveOnlyHidden()):
!1});C.registerPlural("columns().responsiveHidden()","column().responsiveHidden()",function(){return this.iterator("column",function(a,c){return a._responsive?a._responsive._responsiveOnlyHidden()[c]:!1},1)});u.version="2.2.6";b.fn.dataTable.Responsive=u;b.fn.DataTable.Responsive=u;b(m).on("preInit.dt.dtr",function(a,c,d){"dt"===a.namespace&&(b(c.nTable).hasClass("responsive")||b(c.nTable).hasClass("dt-responsive")||c.oInit.responsive||z.defaults.responsive)&&(a=c.oInit.responsive,!1!==a&&new u(c,
b.isPlainObject(a)?a:{}))});return u});
/*!
Bootstrap 4 integration for DataTables' Responsive
©2016 SpryMedia Ltd - datatables.net/license
*/
var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.findInternal=function(a,b,c){a instanceof String&&(a=String(a));for(var e=a.length,d=0;d<e;d++){var f=a[d];if(b.call(c,f,d,a))return{i:d,v:f}}return{i:-1,v:void 0}};$jscomp.ASSUME_ES5=!1;$jscomp.ASSUME_NO_NATIVE_MAP=!1;$jscomp.ASSUME_NO_NATIVE_SET=!1;$jscomp.SIMPLE_FROUND_POLYFILL=!1;$jscomp.ISOLATE_POLYFILLS=!1;
$jscomp.defineProperty=$jscomp.ASSUME_ES5||"function"==typeof Object.defineProperties?Object.defineProperty:function(a,b,c){if(a==Array.prototype||a==Object.prototype)return a;a[b]=c.value;return a};$jscomp.getGlobal=function(a){a=["object"==typeof globalThis&&globalThis,a,"object"==typeof window&&window,"object"==typeof self&&self,"object"==typeof global&&global];for(var b=0;b<a.length;++b){var c=a[b];if(c&&c.Math==Math)return c}throw Error("Cannot find global object");};$jscomp.global=$jscomp.getGlobal(this);
$jscomp.IS_SYMBOL_NATIVE="function"===typeof Symbol&&"symbol"===typeof Symbol("x");$jscomp.TRUST_ES6_POLYFILLS=!$jscomp.ISOLATE_POLYFILLS||$jscomp.IS_SYMBOL_NATIVE;$jscomp.polyfills={};$jscomp.propertyToPolyfillSymbol={};$jscomp.POLYFILL_PREFIX="$jscp$";var $jscomp$lookupPolyfilledValue=function(a,b){var c=$jscomp.propertyToPolyfillSymbol[b];if(null==c)return a[b];c=a[c];return void 0!==c?c:a[b]};
$jscomp.polyfill=function(a,b,c,e){b&&($jscomp.ISOLATE_POLYFILLS?$jscomp.polyfillIsolated(a,b,c,e):$jscomp.polyfillUnisolated(a,b,c,e))};$jscomp.polyfillUnisolated=function(a,b,c,e){c=$jscomp.global;a=a.split(".");for(e=0;e<a.length-1;e++){var d=a[e];if(!(d in c))return;c=c[d]}a=a[a.length-1];e=c[a];b=b(e);b!=e&&null!=b&&$jscomp.defineProperty(c,a,{configurable:!0,writable:!0,value:b})};
$jscomp.polyfillIsolated=function(a,b,c,e){var d=a.split(".");a=1===d.length;e=d[0];e=!a&&e in $jscomp.polyfills?$jscomp.polyfills:$jscomp.global;for(var f=0;f<d.length-1;f++){var g=d[f];if(!(g in e))return;e=e[g]}d=d[d.length-1];c=$jscomp.IS_SYMBOL_NATIVE&&"es6"===c?e[d]:null;b=b(c);null!=b&&(a?$jscomp.defineProperty($jscomp.polyfills,d,{configurable:!0,writable:!0,value:b}):b!==c&&($jscomp.propertyToPolyfillSymbol[d]=$jscomp.IS_SYMBOL_NATIVE?$jscomp.global.Symbol(d):$jscomp.POLYFILL_PREFIX+d,d=
$jscomp.propertyToPolyfillSymbol[d],$jscomp.defineProperty(e,d,{configurable:!0,writable:!0,value:b})))};$jscomp.polyfill("Array.prototype.find",function(a){return a?a:function(b,c){return $jscomp.findInternal(this,b,c).v}},"es6","es3");
(function(a){"function"===typeof define&&define.amd?define(["jquery","datatables.net-bs4","datatables.net-responsive"],function(b){return a(b,window,document)}):"object"===typeof exports?module.exports=function(b,c){b||(b=window);c&&c.fn.dataTable||(c=require("datatables.net-bs4")(b,c).$);c.fn.dataTable.Responsive||require("datatables.net-responsive")(b,c);return a(c,b,b.document)}:a(jQuery,window,document)})(function(a,b,c,e){b=a.fn.dataTable;c=b.Responsive.display;var d=c.modal,f=a('<div class="modal fade dtr-bs-modal" role="dialog"><div class="modal-dialog" role="document"><div class="modal-content"><div class="modal-header"><button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button></div><div class="modal-body"/></div></div></div>');
c.modal=function(g){return function(k,h,l){if(!a.fn.modal)d(k,h,l);else if(!h){if(g&&g.header){h=f.find("div.modal-header");var m=h.find("button").detach();h.empty().append('<h4 class="modal-title">'+g.header(k)+"</h4>").append(m)}f.find("div.modal-body").empty().append(l());f.appendTo("body").modal()}}};return b.Responsive});
/*!
Copyright 2011-2020 SpryMedia Ltd.
This source file is free software, available under the following license:
MIT license - http://datatables.net/license/mit
This source file is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
or FITNESS FOR A PARTICULAR PURPOSE. See the license files for details.
For details please refer to: http://www.datatables.net
Scroller 2.0.3
©2011-2020 SpryMedia Ltd - datatables.net/license
*/
var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.findInternal=function(d,f,g){d instanceof String&&(d=String(d));for(var h=d.length,k=0;k<h;k++){var m=d[k];if(f.call(g,m,k,d))return{i:k,v:m}}return{i:-1,v:void 0}};$jscomp.ASSUME_ES5=!1;$jscomp.ASSUME_NO_NATIVE_MAP=!1;$jscomp.ASSUME_NO_NATIVE_SET=!1;$jscomp.SIMPLE_FROUND_POLYFILL=!1;$jscomp.ISOLATE_POLYFILLS=!1;
$jscomp.defineProperty=$jscomp.ASSUME_ES5||"function"==typeof Object.defineProperties?Object.defineProperty:function(d,f,g){if(d==Array.prototype||d==Object.prototype)return d;d[f]=g.value;return d};$jscomp.getGlobal=function(d){d=["object"==typeof globalThis&&globalThis,d,"object"==typeof window&&window,"object"==typeof self&&self,"object"==typeof global&&global];for(var f=0;f<d.length;++f){var g=d[f];if(g&&g.Math==Math)return g}throw Error("Cannot find global object");};$jscomp.global=$jscomp.getGlobal(this);
$jscomp.IS_SYMBOL_NATIVE="function"===typeof Symbol&&"symbol"===typeof Symbol("x");$jscomp.TRUST_ES6_POLYFILLS=!$jscomp.ISOLATE_POLYFILLS||$jscomp.IS_SYMBOL_NATIVE;$jscomp.polyfills={};$jscomp.propertyToPolyfillSymbol={};$jscomp.POLYFILL_PREFIX="$jscp$";var $jscomp$lookupPolyfilledValue=function(d,f){var g=$jscomp.propertyToPolyfillSymbol[f];if(null==g)return d[f];g=d[g];return void 0!==g?g:d[f]};
$jscomp.polyfill=function(d,f,g,h){f&&($jscomp.ISOLATE_POLYFILLS?$jscomp.polyfillIsolated(d,f,g,h):$jscomp.polyfillUnisolated(d,f,g,h))};$jscomp.polyfillUnisolated=function(d,f,g,h){g=$jscomp.global;d=d.split(".");for(h=0;h<d.length-1;h++){var k=d[h];if(!(k in g))return;g=g[k]}d=d[d.length-1];h=g[d];f=f(h);f!=h&&null!=f&&$jscomp.defineProperty(g,d,{configurable:!0,writable:!0,value:f})};
$jscomp.polyfillIsolated=function(d,f,g,h){var k=d.split(".");d=1===k.length;h=k[0];h=!d&&h in $jscomp.polyfills?$jscomp.polyfills:$jscomp.global;for(var m=0;m<k.length-1;m++){var q=k[m];if(!(q in h))return;h=h[q]}k=k[k.length-1];g=$jscomp.IS_SYMBOL_NATIVE&&"es6"===g?h[k]:null;f=f(g);null!=f&&(d?$jscomp.defineProperty($jscomp.polyfills,k,{configurable:!0,writable:!0,value:f}):f!==g&&($jscomp.propertyToPolyfillSymbol[k]=$jscomp.IS_SYMBOL_NATIVE?$jscomp.global.Symbol(k):$jscomp.POLYFILL_PREFIX+k,k=
$jscomp.propertyToPolyfillSymbol[k],$jscomp.defineProperty(h,k,{configurable:!0,writable:!0,value:f})))};$jscomp.polyfill("Array.prototype.find",function(d){return d?d:function(f,g){return $jscomp.findInternal(this,f,g).v}},"es6","es3");
(function(d){"function"===typeof define&&define.amd?define(["jquery","datatables.net"],function(f){return d(f,window,document)}):"object"===typeof exports?module.exports=function(f,g){f||(f=window);g&&g.fn.dataTable||(g=require("datatables.net")(f,g).$);return d(g,f,f.document)}:d(jQuery,window,document)})(function(d,f,g,h){var k=d.fn.dataTable,m=function(a,b){this instanceof m?(b===h&&(b={}),a=d.fn.dataTable.Api(a),this.s={dt:a.settings()[0],dtApi:a,tableTop:0,tableBottom:0,redrawTop:0,redrawBottom:0,
autoHeight:!0,viewportRows:0,stateTO:null,stateSaveThrottle:function(){},drawTO:null,heights:{jump:null,page:null,virtual:null,scroll:null,row:null,viewport:null,labelFactor:1},topRowFloat:0,scrollDrawDiff:null,loaderVisible:!1,forceReposition:!1,baseRowTop:0,baseScrollTop:0,mousedown:!1,lastScrollTop:0},this.s=d.extend(this.s,m.oDefaults,b),this.s.heights.row=this.s.rowHeight,this.dom={force:g.createElement("div"),label:d('<div class="dts_label">0</div>'),scroller:null,table:null,loader:null},this.s.dt.oScroller||
(this.s.dt.oScroller=this,this.construct())):alert("Scroller warning: Scroller must be initialised with the 'new' keyword.")};d.extend(m.prototype,{measure:function(a){this.s.autoHeight&&this._calcRowHeight();var b=this.s.heights;b.row&&(b.viewport=this._parseHeight(d(this.dom.scroller).css("max-height")),this.s.viewportRows=parseInt(b.viewport/b.row,10)+1,this.s.dt._iDisplayLength=this.s.viewportRows*this.s.displayBuffer);var c=this.dom.label.outerHeight();b.labelFactor=(b.viewport-c)/b.scroll;(a===
h||a)&&this.s.dt.oInstance.fnDraw(!1)},pageInfo:function(){var a=this.dom.scroller.scrollTop,b=this.s.dt.fnRecordsDisplay(),c=Math.ceil(this.pixelsToRow(a+this.s.heights.viewport,!1,this.s.ani));return{start:Math.floor(this.pixelsToRow(a,!1,this.s.ani)),end:b<c?b-1:c-1}},pixelsToRow:function(a,b,c){a-=this.s.baseScrollTop;c=c?(this._domain("physicalToVirtual",this.s.baseScrollTop)+a)/this.s.heights.row:a/this.s.heights.row+this.s.baseRowTop;return b||b===h?parseInt(c,10):c},rowToPixels:function(a,
b,c){a-=this.s.baseRowTop;c=c?this._domain("virtualToPhysical",this.s.baseScrollTop):this.s.baseScrollTop;c+=a*this.s.heights.row;return b||b===h?parseInt(c,10):c},scrollToRow:function(a,b){var c=this,e=!1,l=this.rowToPixels(a),n=a-(this.s.displayBuffer-1)/2*this.s.viewportRows;0>n&&(n=0);(l>this.s.redrawBottom||l<this.s.redrawTop)&&this.s.dt._iDisplayStart!==n&&(e=!0,l=this._domain("virtualToPhysical",a*this.s.heights.row),this.s.redrawTop<l&&l<this.s.redrawBottom&&(this.s.forceReposition=!0,b=!1));
b===h||b?(this.s.ani=e,d(this.dom.scroller).animate({scrollTop:l},function(){setTimeout(function(){c.s.ani=!1},250)})):d(this.dom.scroller).scrollTop(l)},construct:function(){var a=this,b=this.s.dtApi;if(this.s.dt.oFeatures.bPaginate){this.dom.force.style.position="relative";this.dom.force.style.top="0px";this.dom.force.style.left="0px";this.dom.force.style.width="1px";this.dom.scroller=d("div."+this.s.dt.oClasses.sScrollBody,this.s.dt.nTableWrapper)[0];this.dom.scroller.appendChild(this.dom.force);
this.dom.scroller.style.position="relative";this.dom.table=d(">table",this.dom.scroller)[0];this.dom.table.style.position="absolute";this.dom.table.style.top="0px";this.dom.table.style.left="0px";d(b.table().container()).addClass("dts DTS");this.s.loadingIndicator&&(this.dom.loader=d('<div class="dataTables_processing dts_loading">'+this.s.dt.oLanguage.sLoadingRecords+"</div>").css("display","none"),d(this.dom.scroller.parentNode).css("position","relative").append(this.dom.loader));this.dom.label.appendTo(this.dom.scroller);
this.s.heights.row&&"auto"!=this.s.heights.row&&(this.s.autoHeight=!1);this.s.ingnoreScroll=!0;d(this.dom.scroller).on("scroll.dt-scroller",function(l){a._scroll.call(a)});d(this.dom.scroller).on("touchstart.dt-scroller",function(){a._scroll.call(a)});d(this.dom.scroller).on("mousedown.dt-scroller",function(){a.s.mousedown=!0}).on("mouseup.dt-scroller",function(){a.s.labelVisible=!1;a.s.mousedown=!1;a.dom.label.css("display","none")});d(f).on("resize.dt-scroller",function(){a.measure(!1);a._info()});
var c=!0,e=b.state.loaded();b.on("stateSaveParams.scroller",function(l,n,p){c&&e?(p.scroller=e.scroller,c=!1):p.scroller={topRow:a.s.topRowFloat,baseScrollTop:a.s.baseScrollTop,baseRowTop:a.s.baseRowTop,scrollTop:a.s.lastScrollTop}});e&&e.scroller&&(this.s.topRowFloat=e.scroller.topRow,this.s.baseScrollTop=e.scroller.baseScrollTop,this.s.baseRowTop=e.scroller.baseRowTop);this.measure(!1);a.s.stateSaveThrottle=a.s.dt.oApi._fnThrottle(function(){a.s.dtApi.state.save()},500);b.on("init.scroller",function(){a.measure(!1);
a.s.scrollType="jump";a._draw();b.on("draw.scroller",function(){a._draw()})});b.on("preDraw.dt.scroller",function(){a._scrollForce()});b.on("destroy.scroller",function(){d(f).off("resize.dt-scroller");d(a.dom.scroller).off(".dt-scroller");d(a.s.dt.nTable).off(".scroller");d(a.s.dt.nTableWrapper).removeClass("DTS");d("div.DTS_Loading",a.dom.scroller.parentNode).remove();a.dom.table.style.position="";a.dom.table.style.top="";a.dom.table.style.left=""})}else this.s.dt.oApi._fnLog(this.s.dt,0,"Pagination must be enabled for Scroller")},
_calcRowHeight:function(){var a=this.s.dt,b=a.nTable,c=b.cloneNode(!1),e=d("<tbody/>").appendTo(c),l=d('<div class="'+a.oClasses.sWrapper+' DTS"><div class="'+a.oClasses.sScrollWrapper+'"><div class="'+a.oClasses.sScrollBody+'"></div></div></div>');d("tbody tr:lt(4)",b).clone().appendTo(e);var n=d("tr",e).length;if(1===n)e.prepend("<tr><td>&#160;</td></tr>"),e.append("<tr><td>&#160;</td></tr>");else for(;3>n;n++)e.append("<tr><td>&#160;</td></tr>");d("div."+a.oClasses.sScrollBody,l).append(c);a=this.s.dt.nHolding||
b.parentNode;d(a).is(":visible")||(a="body");l.find("input").removeAttr("name");l.appendTo(a);this.s.heights.row=d("tr",e).eq(1).outerHeight();l.remove()},_draw:function(){var a=this,b=this.s.heights,c=this.dom.scroller.scrollTop,e=d(this.s.dt.nTable).height(),l=this.s.dt._iDisplayStart,n=this.s.dt._iDisplayLength,p=this.s.dt.fnRecordsDisplay();this.s.skip=!0;!this.s.dt.bSorted&&!this.s.dt.bFiltered||0!==l||this.s.dt._drawHold||(this.s.topRowFloat=0);c="jump"===this.s.scrollType?this._domain("virtualToPhysical",
this.s.topRowFloat*b.row):c;this.s.baseScrollTop=c;this.s.baseRowTop=this.s.topRowFloat;var r=c-(this.s.topRowFloat-l)*b.row;0===l?r=0:l+n>=p&&(r=b.scroll-e);this.dom.table.style.top=r+"px";this.s.tableTop=r;this.s.tableBottom=e+this.s.tableTop;e=(c-this.s.tableTop)*this.s.boundaryScale;this.s.redrawTop=c-e;this.s.redrawBottom=c+e>b.scroll-b.viewport-b.row?b.scroll-b.viewport-b.row:c+e;this.s.skip=!1;this.s.dt.oFeatures.bStateSave&&null!==this.s.dt.oLoadedState&&"undefined"!=typeof this.s.dt.oLoadedState.scroller?
((b=!this.s.dt.sAjaxSource&&!a.s.dt.ajax||this.s.dt.oFeatures.bServerSide?!1:!0)&&2==this.s.dt.iDraw||!b&&1==this.s.dt.iDraw)&&setTimeout(function(){d(a.dom.scroller).scrollTop(a.s.dt.oLoadedState.scroller.scrollTop);setTimeout(function(){a.s.ingnoreScroll=!1},0)},0):a.s.ingnoreScroll=!1;this.s.dt.oFeatures.bInfo&&setTimeout(function(){a._info.call(a)},0);this.dom.loader&&this.s.loaderVisible&&(this.dom.loader.css("display","none"),this.s.loaderVisible=!1)},_domain:function(a,b){var c=this.s.heights;
if(c.virtual===c.scroll||1E4>b)return b;if("virtualToPhysical"===a&&b>=c.virtual-1E4)return a=c.virtual-b,c.scroll-a;if("physicalToVirtual"===a&&b>=c.scroll-1E4)return a=c.scroll-b,c.virtual-a;c=(c.virtual-1E4-1E4)/(c.scroll-1E4-1E4);var e=1E4-1E4*c;return"virtualToPhysical"===a?(b-e)/c:c*b+e},_info:function(){if(this.s.dt.oFeatures.bInfo){var a=this.s.dt,b=a.oLanguage,c=this.dom.scroller.scrollTop,e=Math.floor(this.pixelsToRow(c,!1,this.s.ani)+1),l=a.fnRecordsTotal(),n=a.fnRecordsDisplay();c=Math.ceil(this.pixelsToRow(c+
this.s.heights.viewport,!1,this.s.ani));c=n<c?n:c;var p=a.fnFormatNumber(e),r=a.fnFormatNumber(c),t=a.fnFormatNumber(l),u=a.fnFormatNumber(n);p=0===a.fnRecordsDisplay()&&a.fnRecordsDisplay()==a.fnRecordsTotal()?b.sInfoEmpty+b.sInfoPostFix:0===a.fnRecordsDisplay()?b.sInfoEmpty+" "+b.sInfoFiltered.replace("_MAX_",t)+b.sInfoPostFix:a.fnRecordsDisplay()==a.fnRecordsTotal()?b.sInfo.replace("_START_",p).replace("_END_",r).replace("_MAX_",t).replace("_TOTAL_",u)+b.sInfoPostFix:b.sInfo.replace("_START_",
p).replace("_END_",r).replace("_MAX_",t).replace("_TOTAL_",u)+" "+b.sInfoFiltered.replace("_MAX_",a.fnFormatNumber(a.fnRecordsTotal()))+b.sInfoPostFix;(b=b.fnInfoCallback)&&(p=b.call(a.oInstance,a,e,c,l,n,p));e=a.aanFeatures.i;if("undefined"!=typeof e)for(l=0,n=e.length;l<n;l++)d(e[l]).html(p);d(a.nTable).triggerHandler("info.dt")}},_parseHeight:function(a){var b,c=/^([+-]?(?:\d+(?:\.\d+)?|\.\d+))(px|em|rem|vh)$/.exec(a);if(null===c)return 0;a=parseFloat(c[1]);c=c[2];"px"===c?b=a:"vh"===c?b=a/100*
d(f).height():"rem"===c?b=a*parseFloat(d(":root").css("font-size")):"em"===c&&(b=a*parseFloat(d("body").css("font-size")));return b?b:0},_scroll:function(){var a=this,b=this.s.heights,c=this.dom.scroller.scrollTop;if(!this.s.skip&&!this.s.ingnoreScroll&&c!==this.s.lastScrollTop)if(this.s.dt.bFiltered||this.s.dt.bSorted)this.s.lastScrollTop=0;else{this._info();clearTimeout(this.s.stateTO);this.s.stateTO=setTimeout(function(){a.s.dtApi.state.save()},250);this.s.scrollType=Math.abs(c-this.s.lastScrollTop)>
b.viewport?"jump":"cont";this.s.topRowFloat="cont"===this.s.scrollType?this.pixelsToRow(c,!1,!1):this._domain("physicalToVirtual",c)/b.row;0>this.s.topRowFloat&&(this.s.topRowFloat=0);if(this.s.forceReposition||c<this.s.redrawTop||c>this.s.redrawBottom){var e=Math.ceil((this.s.displayBuffer-1)/2*this.s.viewportRows);e=parseInt(this.s.topRowFloat,10)-e;this.s.forceReposition=!1;0>=e?e=0:e+this.s.dt._iDisplayLength>this.s.dt.fnRecordsDisplay()?(e=this.s.dt.fnRecordsDisplay()-this.s.dt._iDisplayLength,
0>e&&(e=0)):0!==e%2&&e++;this.s.targetTop=e;e!=this.s.dt._iDisplayStart&&(this.s.tableTop=d(this.s.dt.nTable).offset().top,this.s.tableBottom=d(this.s.dt.nTable).height()+this.s.tableTop,e=function(){a.s.dt._iDisplayStart=a.s.targetTop;a.s.dt.oApi._fnDraw(a.s.dt)},this.s.dt.oFeatures.bServerSide?(this.s.forceReposition=!0,clearTimeout(this.s.drawTO),this.s.drawTO=setTimeout(e,this.s.serverWait)):e(),this.dom.loader&&!this.s.loaderVisible&&(this.dom.loader.css("display","block"),this.s.loaderVisible=
!0))}else this.s.topRowFloat=this.pixelsToRow(c,!1,!0);this.s.lastScrollTop=c;this.s.stateSaveThrottle();"jump"===this.s.scrollType&&this.s.mousedown&&(this.s.labelVisible=!0);this.s.labelVisible&&this.dom.label.html(this.s.dt.fnFormatNumber(parseInt(this.s.topRowFloat,10)+1)).css("top",c+c*b.labelFactor).css("display","block")}},_scrollForce:function(){var a=this.s.heights;a.virtual=a.row*this.s.dt.fnRecordsDisplay();a.scroll=a.virtual;1E6<a.scroll&&(a.scroll=1E6);this.dom.force.style.height=a.scroll>
this.s.heights.row?a.scroll+"px":this.s.heights.row+"px"}});m.defaults={boundaryScale:.5,displayBuffer:9,loadingIndicator:!1,rowHeight:"auto",serverWait:200};m.oDefaults=m.defaults;m.version="2.0.3";d(g).on("preInit.dt.dtscroller",function(a,b){if("dt"===a.namespace){a=b.oInit.scroller;var c=k.defaults.scroller;if(a||c)c=d.extend({},a,c),!1!==a&&new m(b,c)}});d.fn.dataTable.Scroller=m;d.fn.DataTable.Scroller=m;var q=d.fn.dataTable.Api;q.register("scroller()",function(){return this});q.register("scroller().rowToPixels()",
function(a,b,c){var e=this.context;if(e.length&&e[0].oScroller)return e[0].oScroller.rowToPixels(a,b,c)});q.register("scroller().pixelsToRow()",function(a,b,c){var e=this.context;if(e.length&&e[0].oScroller)return e[0].oScroller.pixelsToRow(a,b,c)});q.register(["scroller().scrollToRow()","scroller.toPosition()"],function(a,b){this.iterator("table",function(c){c.oScroller&&c.oScroller.scrollToRow(a,b)});return this});q.register("row().scrollTo()",function(a){var b=this;this.iterator("row",function(c,
e){c.oScroller&&(e=b.rows({order:"applied",search:"applied"}).indexes().indexOf(e),c.oScroller.scrollToRow(e,a))});return this});q.register("scroller.measure()",function(a){this.iterator("table",function(b){b.oScroller&&b.oScroller.measure(a)});return this});q.register("scroller.page()",function(){var a=this.context;if(a.length&&a[0].oScroller)return a[0].oScroller.pageInfo()});return m});
/*!
Bootstrap 4 styling wrapper for Scroller
©2018 SpryMedia Ltd - datatables.net/license
*/
(function(c){"function"===typeof define&&define.amd?define(["jquery","datatables.net-bs4","datatables.net-scroller"],function(a){return c(a,window,document)}):"object"===typeof exports?module.exports=function(a,b){a||(a=window);b&&b.fn.dataTable||(b=require("datatables.net-bs4")(a,b).$);b.fn.dataTable.Scroller||require("datatables.net-scroller")(a,b);return c(b,a,a.document)}:c(jQuery,window,document)})(function(c,a,b,d){return c.fn.dataTable});
/*!
SearchPanes 1.2.2
2019-2020 SpryMedia Ltd - datatables.net/license
*/
var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.getGlobal=function(m){m=["object"==typeof globalThis&&globalThis,m,"object"==typeof window&&window,"object"==typeof self&&self,"object"==typeof global&&global];for(var t=0;t<m.length;++t){var h=m[t];if(h&&h.Math==Math)return h}throw Error("Cannot find global object");};$jscomp.global=$jscomp.getGlobal(this);
$jscomp.checkEs6ConformanceViaProxy=function(){try{var m={},t=Object.create(new $jscomp.global.Proxy(m,{get:function(h,r,v){return h==m&&"q"==r&&v==t}}));return!0===t.q}catch(h){return!1}};$jscomp.USE_PROXY_FOR_ES6_CONFORMANCE_CHECKS=!1;$jscomp.ES6_CONFORMANCE=$jscomp.USE_PROXY_FOR_ES6_CONFORMANCE_CHECKS&&$jscomp.checkEs6ConformanceViaProxy();$jscomp.arrayIteratorImpl=function(m){var t=0;return function(){return t<m.length?{done:!1,value:m[t++]}:{done:!0}}};$jscomp.arrayIterator=function(m){return{next:$jscomp.arrayIteratorImpl(m)}};
$jscomp.ASSUME_ES5=!1;$jscomp.ASSUME_NO_NATIVE_MAP=!1;$jscomp.ASSUME_NO_NATIVE_SET=!1;$jscomp.SIMPLE_FROUND_POLYFILL=!1;$jscomp.ISOLATE_POLYFILLS=!1;$jscomp.defineProperty=$jscomp.ASSUME_ES5||"function"==typeof Object.defineProperties?Object.defineProperty:function(m,t,h){if(m==Array.prototype||m==Object.prototype)return m;m[t]=h.value;return m};$jscomp.IS_SYMBOL_NATIVE="function"===typeof Symbol&&"symbol"===typeof Symbol("x");$jscomp.TRUST_ES6_POLYFILLS=!$jscomp.ISOLATE_POLYFILLS||$jscomp.IS_SYMBOL_NATIVE;
$jscomp.polyfills={};$jscomp.propertyToPolyfillSymbol={};$jscomp.POLYFILL_PREFIX="$jscp$";var $jscomp$lookupPolyfilledValue=function(m,t){var h=$jscomp.propertyToPolyfillSymbol[t];if(null==h)return m[t];h=m[h];return void 0!==h?h:m[t]};$jscomp.polyfill=function(m,t,h,r){t&&($jscomp.ISOLATE_POLYFILLS?$jscomp.polyfillIsolated(m,t,h,r):$jscomp.polyfillUnisolated(m,t,h,r))};
$jscomp.polyfillUnisolated=function(m,t,h,r){h=$jscomp.global;m=m.split(".");for(r=0;r<m.length-1;r++){var v=m[r];if(!(v in h))return;h=h[v]}m=m[m.length-1];r=h[m];t=t(r);t!=r&&null!=t&&$jscomp.defineProperty(h,m,{configurable:!0,writable:!0,value:t})};
$jscomp.polyfillIsolated=function(m,t,h,r){var v=m.split(".");m=1===v.length;r=v[0];r=!m&&r in $jscomp.polyfills?$jscomp.polyfills:$jscomp.global;for(var q=0;q<v.length-1;q++){var A=v[q];if(!(A in r))return;r=r[A]}v=v[v.length-1];h=$jscomp.IS_SYMBOL_NATIVE&&"es6"===h?r[v]:null;t=t(h);null!=t&&(m?$jscomp.defineProperty($jscomp.polyfills,v,{configurable:!0,writable:!0,value:t}):t!==h&&($jscomp.propertyToPolyfillSymbol[v]=$jscomp.IS_SYMBOL_NATIVE?$jscomp.global.Symbol(v):$jscomp.POLYFILL_PREFIX+v,v=
$jscomp.propertyToPolyfillSymbol[v],$jscomp.defineProperty(r,v,{configurable:!0,writable:!0,value:t})))};$jscomp.initSymbol=function(){};
$jscomp.polyfill("Symbol",function(m){if(m)return m;var t=function(v,q){this.$jscomp$symbol$id_=v;$jscomp.defineProperty(this,"description",{configurable:!0,writable:!0,value:q})};t.prototype.toString=function(){return this.$jscomp$symbol$id_};var h=0,r=function(v){if(this instanceof r)throw new TypeError("Symbol is not a constructor");return new t("jscomp_symbol_"+(v||"")+"_"+h++,v)};return r},"es6","es3");$jscomp.initSymbolIterator=function(){};
$jscomp.polyfill("Symbol.iterator",function(m){if(m)return m;m=Symbol("Symbol.iterator");for(var t="Array Int8Array Uint8Array Uint8ClampedArray Int16Array Uint16Array Int32Array Uint32Array Float32Array Float64Array".split(" "),h=0;h<t.length;h++){var r=$jscomp.global[t[h]];"function"===typeof r&&"function"!=typeof r.prototype[m]&&$jscomp.defineProperty(r.prototype,m,{configurable:!0,writable:!0,value:function(){return $jscomp.iteratorPrototype($jscomp.arrayIteratorImpl(this))}})}return m},"es6",
"es3");$jscomp.initSymbolAsyncIterator=function(){};$jscomp.iteratorPrototype=function(m){m={next:m};m[Symbol.iterator]=function(){return this};return m};$jscomp.makeIterator=function(m){var t="undefined"!=typeof Symbol&&Symbol.iterator&&m[Symbol.iterator];return t?t.call(m):$jscomp.arrayIterator(m)};$jscomp.owns=function(m,t){return Object.prototype.hasOwnProperty.call(m,t)};
$jscomp.polyfill("WeakMap",function(m){function t(){if(!m||!Object.seal)return!1;try{var a=Object.seal({}),b=Object.seal({}),c=new m([[a,2],[b,3]]);if(2!=c.get(a)||3!=c.get(b))return!1;c.delete(a);c.set(b,4);return!c.has(a)&&4==c.get(b)}catch(d){return!1}}function h(){}function r(a){var b=typeof a;return"object"===b&&null!==a||"function"===b}function v(a){if(!$jscomp.owns(a,A)){var b=new h;$jscomp.defineProperty(a,A,{value:b})}}function q(a){if(!$jscomp.ISOLATE_POLYFILLS){var b=Object[a];b&&(Object[a]=
function(c){if(c instanceof h)return c;Object.isExtensible(c)&&v(c);return b(c)})}}if($jscomp.USE_PROXY_FOR_ES6_CONFORMANCE_CHECKS){if(m&&$jscomp.ES6_CONFORMANCE)return m}else if(t())return m;var A="$jscomp_hidden_"+Math.random();q("freeze");q("preventExtensions");q("seal");var G=0,k=function(a){this.id_=(G+=Math.random()+1).toString();if(a){a=$jscomp.makeIterator(a);for(var b;!(b=a.next()).done;)b=b.value,this.set(b[0],b[1])}};k.prototype.set=function(a,b){if(!r(a))throw Error("Invalid WeakMap key");
v(a);if(!$jscomp.owns(a,A))throw Error("WeakMap key fail: "+a);a[A][this.id_]=b;return this};k.prototype.get=function(a){return r(a)&&$jscomp.owns(a,A)?a[A][this.id_]:void 0};k.prototype.has=function(a){return r(a)&&$jscomp.owns(a,A)&&$jscomp.owns(a[A],this.id_)};k.prototype.delete=function(a){return r(a)&&$jscomp.owns(a,A)&&$jscomp.owns(a[A],this.id_)?delete a[A][this.id_]:!1};return k},"es6","es3");$jscomp.MapEntry=function(){};
$jscomp.polyfill("Map",function(m){function t(){if($jscomp.ASSUME_NO_NATIVE_MAP||!m||"function"!=typeof m||!m.prototype.entries||"function"!=typeof Object.seal)return!1;try{var k=Object.seal({x:4}),a=new m($jscomp.makeIterator([[k,"s"]]));if("s"!=a.get(k)||1!=a.size||a.get({x:4})||a.set({x:4},"t")!=a||2!=a.size)return!1;var b=a.entries(),c=b.next();if(c.done||c.value[0]!=k||"s"!=c.value[1])return!1;c=b.next();return c.done||4!=c.value[0].x||"t"!=c.value[1]||!b.next().done?!1:!0}catch(d){return!1}}
if($jscomp.USE_PROXY_FOR_ES6_CONFORMANCE_CHECKS){if(m&&$jscomp.ES6_CONFORMANCE)return m}else if(t())return m;var h=new WeakMap,r=function(k){this.data_={};this.head_=A();this.size=0;if(k){k=$jscomp.makeIterator(k);for(var a;!(a=k.next()).done;)a=a.value,this.set(a[0],a[1])}};r.prototype.set=function(k,a){k=0===k?0:k;var b=v(this,k);b.list||(b.list=this.data_[b.id]=[]);b.entry?b.entry.value=a:(b.entry={next:this.head_,previous:this.head_.previous,head:this.head_,key:k,value:a},b.list.push(b.entry),
this.head_.previous.next=b.entry,this.head_.previous=b.entry,this.size++);return this};r.prototype.delete=function(k){k=v(this,k);return k.entry&&k.list?(k.list.splice(k.index,1),k.list.length||delete this.data_[k.id],k.entry.previous.next=k.entry.next,k.entry.next.previous=k.entry.previous,k.entry.head=null,this.size--,!0):!1};r.prototype.clear=function(){this.data_={};this.head_=this.head_.previous=A();this.size=0};r.prototype.has=function(k){return!!v(this,k).entry};r.prototype.get=function(k){return(k=
v(this,k).entry)&&k.value};r.prototype.entries=function(){return q(this,function(k){return[k.key,k.value]})};r.prototype.keys=function(){return q(this,function(k){return k.key})};r.prototype.values=function(){return q(this,function(k){return k.value})};r.prototype.forEach=function(k,a){for(var b=this.entries(),c;!(c=b.next()).done;)c=c.value,k.call(a,c[1],c[0],this)};r.prototype[Symbol.iterator]=r.prototype.entries;var v=function(k,a){var b=a&&typeof a;"object"==b||"function"==b?h.has(a)?b=h.get(a):
(b=""+ ++G,h.set(a,b)):b="p_"+a;var c=k.data_[b];if(c&&$jscomp.owns(k.data_,b))for(k=0;k<c.length;k++){var d=c[k];if(a!==a&&d.key!==d.key||a===d.key)return{id:b,list:c,index:k,entry:d}}return{id:b,list:c,index:-1,entry:void 0}},q=function(k,a){var b=k.head_;return $jscomp.iteratorPrototype(function(){if(b){for(;b.head!=k.head_;)b=b.previous;for(;b.next!=b.head;)return b=b.next,{done:!1,value:a(b)};b=null}return{done:!0,value:void 0}})},A=function(){var k={};return k.previous=k.next=k.head=k},G=0;
return r},"es6","es3");$jscomp.findInternal=function(m,t,h){m instanceof String&&(m=String(m));for(var r=m.length,v=0;v<r;v++){var q=m[v];if(t.call(h,q,v,m))return{i:v,v:q}}return{i:-1,v:void 0}};$jscomp.polyfill("Array.prototype.find",function(m){return m?m:function(t,h){return $jscomp.findInternal(this,t,h).v}},"es6","es3");
$jscomp.iteratorFromArray=function(m,t){m instanceof String&&(m+="");var h=0,r={next:function(){if(h<m.length){var v=h++;return{value:t(v,m[v]),done:!1}}r.next=function(){return{done:!0,value:void 0}};return r.next()}};r[Symbol.iterator]=function(){return r};return r};$jscomp.polyfill("Array.prototype.keys",function(m){return m?m:function(){return $jscomp.iteratorFromArray(this,function(t){return t})}},"es6","es3");
$jscomp.polyfill("Array.prototype.findIndex",function(m){return m?m:function(t,h){return $jscomp.findInternal(this,t,h).i}},"es6","es3");
(function(){function m(k){h=k;r=k.fn.dataTable}function t(k){q=k;A=k.fn.dataTable}var h,r,v=function(){function k(a,b,c,d,e,g){var f=this;void 0===g&&(g=null);if(!r||!r.versionCheck||!r.versionCheck("1.10.0"))throw Error("SearchPane requires DataTables 1.10 or newer");if(!r.select)throw Error("SearchPane requires Select");a=new r.Api(a);this.classes=h.extend(!0,{},k.classes);this.c=h.extend(!0,{},k.defaults,b);this.customPaneSettings=g;this.s={cascadeRegen:!1,clearing:!1,colOpts:[],deselect:!1,displayed:!1,
dt:a,dtPane:void 0,filteringActive:!1,index:c,indexes:[],lastCascade:!1,lastSelect:!1,listSet:!1,name:void 0,redraw:!1,rowData:{arrayFilter:[],arrayOriginal:[],arrayTotals:[],bins:{},binsOriginal:{},binsTotal:{},filterMap:new Map,totalOptions:0},scrollTop:0,searchFunction:void 0,selectPresent:!1,serverSelect:[],serverSelecting:!1,showFiltered:!1,tableLength:null,updating:!1};b=a.columns().eq(0).toArray().length;this.colExists=this.s.index<b;this.c.layout=d;b=parseInt(d.split("-")[1],10);this.dom=
{buttonGroup:h("<div/>").addClass(this.classes.buttonGroup),clear:h('<button type="button">&#215;</button>').addClass(this.classes.dull).addClass(this.classes.paneButton).addClass(this.classes.clearButton),container:h("<div/>").addClass(this.classes.container).addClass(this.classes.layout+(10>b?d:d.split("-")[0]+"-9")),countButton:h('<button type="button"></button>').addClass(this.classes.paneButton).addClass(this.classes.countButton),dtP:h("<table><thead><tr><th>"+(this.colExists?h(a.column(this.colExists?
this.s.index:0).header()).text():this.customPaneSettings.header||"Custom Pane")+"</th><th/></tr></thead></table>"),lower:h("<div/>").addClass(this.classes.subRow2).addClass(this.classes.narrowButton),nameButton:h('<button type="button"></button>').addClass(this.classes.paneButton).addClass(this.classes.nameButton),panesContainer:e,searchBox:h("<input/>").addClass(this.classes.paneInputButton).addClass(this.classes.search),searchButton:h('<button type = "button" class="'+this.classes.searchIcon+'"></button>').addClass(this.classes.paneButton),
searchCont:h("<div/>").addClass(this.classes.searchCont),searchLabelCont:h("<div/>").addClass(this.classes.searchLabelCont),topRow:h("<div/>").addClass(this.classes.topRow),upper:h("<div/>").addClass(this.classes.subRow1).addClass(this.classes.narrowSearch)};this.s.displayed=!1;a=this.s.dt;this.selections=[];this.s.colOpts=this.colExists?this._getOptions():this._getBonusOptions();var l=this.s.colOpts;d=h('<button type="button">X</button>').addClass(this.classes.paneButton);h(d).text(a.i18n("searchPanes.clearPane",
"X"));this.dom.container.addClass(l.className);this.dom.container.addClass(null!==this.customPaneSettings&&void 0!==this.customPaneSettings.className?this.customPaneSettings.className:"");this.s.name=void 0!==this.s.colOpts.name?this.s.colOpts.name:null!==this.customPaneSettings&&void 0!==this.customPaneSettings.name?this.customPaneSettings.name:this.colExists?h(a.column(this.s.index).header()).text():this.customPaneSettings.header||"Custom Pane";h(e).append(this.dom.container);var p=a.table(0).node();
this.s.searchFunction=function(n,x,z,y){if(0===f.selections.length||n.nTable!==p)return!0;n=null;f.colExists&&(n=x[f.s.index],"filter"!==l.orthogonal.filter&&(n=f.s.rowData.filterMap.get(z),n instanceof h.fn.dataTable.Api&&(n=n.toArray())));return f._search(n,z)};h.fn.dataTable.ext.search.push(this.s.searchFunction);if(this.c.clear)h(d).on("click",function(){f.dom.container.find(f.classes.search).each(function(){h(this).val("");h(this).trigger("input")});f.clearPane()});a.on("draw.dtsp",function(){f._adjustTopRow()});
a.on("buttons-action",function(){f._adjustTopRow()});h(window).on("resize.dtsp",r.util.throttle(function(){f._adjustTopRow()}));a.on("column-reorder.dtsp",function(n,x,z){f.s.index=z.mapping[f.s.index]});return this}k.prototype.clearData=function(){this.s.rowData={arrayFilter:[],arrayOriginal:[],arrayTotals:[],bins:{},binsOriginal:{},binsTotal:{},filterMap:new Map,totalOptions:0}};k.prototype.clearPane=function(){this.s.dtPane.rows({selected:!0}).deselect();this.updateTable();return this};k.prototype.destroy=
function(){h(this.s.dtPane).off(".dtsp");h(this.s.dt).off(".dtsp");h(this.dom.nameButton).off(".dtsp");h(this.dom.countButton).off(".dtsp");h(this.dom.clear).off(".dtsp");h(this.dom.searchButton).off(".dtsp");h(this.dom.container).remove();for(var a=h.fn.dataTable.ext.search.indexOf(this.s.searchFunction);-1!==a;)h.fn.dataTable.ext.search.splice(a,1),a=h.fn.dataTable.ext.search.indexOf(this.s.searchFunction);void 0!==this.s.dtPane&&this.s.dtPane.destroy();this.s.listSet=!1};k.prototype.getPaneCount=
function(){return void 0!==this.s.dtPane?this.s.dtPane.rows({selected:!0}).data().toArray().length:0};k.prototype.rebuildPane=function(a,b,c,d){void 0===a&&(a=!1);void 0===b&&(b=null);void 0===c&&(c=null);void 0===d&&(d=!1);this.clearData();var e=[];this.s.serverSelect=[];var g=null;void 0!==this.s.dtPane&&(d&&(this.s.dt.page.info().serverSide?this.s.serverSelect=this.s.dtPane.rows({selected:!0}).data().toArray():e=this.s.dtPane.rows({selected:!0}).data().toArray()),this.s.dtPane.clear().destroy(),
g=h(this.dom.container).prev(),this.destroy(),this.s.dtPane=void 0,h.fn.dataTable.ext.search.push(this.s.searchFunction));this.dom.container.removeClass(this.classes.hidden);this.s.displayed=!1;this._buildPane(this.s.dt.page.info().serverSide?this.s.serverSelect:e,a,b,c,g);return this};k.prototype.removePane=function(){this.s.displayed=!1;h(this.dom.container).hide()};k.prototype.setCascadeRegen=function(a){this.s.cascadeRegen=a};k.prototype.setClear=function(a){this.s.clearing=a};k.prototype.updatePane=
function(a){void 0===a&&(a=!1);this.s.updating=!0;this._updateCommon(a);this.s.updating=!1};k.prototype.updateTable=function(){this.selections=this.s.dtPane.rows({selected:!0}).data().toArray();this._searchExtras();(this.c.cascadePanes||this.c.viewTotal)&&this.updatePane()};k.prototype._setListeners=function(){var a=this,b=this.s.rowData,c;this.s.dtPane.on("select.dtsp",function(){clearTimeout(c);a.s.dt.page.info().serverSide&&!a.s.updating?a.s.serverSelecting||(a.s.serverSelect=a.s.dtPane.rows({selected:!0}).data().toArray(),
a.s.scrollTop=h(a.s.dtPane.table().node()).parent()[0].scrollTop,a.s.selectPresent=!0,a.s.dt.draw(!1)):(h(a.dom.clear).removeClass(a.classes.dull),a.s.selectPresent=!0,a.s.updating||a._makeSelection(),a.s.selectPresent=!1)});this.s.dtPane.on("deselect.dtsp",function(){c=setTimeout(function(){a.s.dt.page.info().serverSide&&!a.s.updating?a.s.serverSelecting||(a.s.serverSelect=a.s.dtPane.rows({selected:!0}).data().toArray(),a.s.deselect=!0,a.s.dt.draw(!1)):(a.s.deselect=!0,0===a.s.dtPane.rows({selected:!0}).data().toArray().length&&
h(a.dom.clear).addClass(a.classes.dull),a._makeSelection(),a.s.deselect=!1,a.s.dt.state.save())},50)});this.s.dt.on("stateSaveParams.dtsp",function(d,e,g){if(h.isEmptyObject(g))a.s.dtPane.state.clear();else{d=[];if(void 0!==a.s.dtPane){d=a.s.dtPane.rows({selected:!0}).data().map(function(x){return x.filter.toString()}).toArray();var f=h(a.dom.searchBox).val();var l=a.s.dtPane.order();var p=b.binsOriginal;var n=b.arrayOriginal}void 0===g.searchPanes&&(g.searchPanes={});void 0===g.searchPanes.panes&&
(g.searchPanes.panes=[]);for(e=0;e<g.searchPanes.panes.length;e++)g.searchPanes.panes[e].id===a.s.index&&(g.searchPanes.panes.splice(e,1),e--);g.searchPanes.panes.push({arrayFilter:n,bins:p,id:a.s.index,order:l,searchTerm:f,selected:d})}});this.s.dtPane.on("user-select.dtsp",function(d,e,g,f,l){l.stopPropagation()});this.s.dtPane.on("draw.dtsp",function(){a._adjustTopRow()});h(this.dom.nameButton).on("click.dtsp",function(){var d=a.s.dtPane.order()[0][1];a.s.dtPane.order([0,"asc"===d?"desc":"asc"]).draw();
a.s.dt.state.save()});h(this.dom.countButton).on("click.dtsp",function(){var d=a.s.dtPane.order()[0][1];a.s.dtPane.order([1,"asc"===d?"desc":"asc"]).draw();a.s.dt.state.save()});h(this.dom.clear).on("click.dtsp",function(){a.dom.container.find("."+a.classes.search).each(function(){h(this).val("");h(this).trigger("input")});a.clearPane()});h(this.dom.searchButton).on("click.dtsp",function(){h(a.dom.searchBox).focus()});h(this.dom.searchBox).on("input.dtsp",function(){a.s.dtPane.search(h(a.dom.searchBox).val()).draw();
a.s.dt.state.save()});this.s.dt.state.save();return!0};k.prototype._addOption=function(a,b,c,d,e,g){if(Array.isArray(a)||a instanceof r.Api)if(a instanceof r.Api&&(a=a.toArray(),b=b.toArray()),a.length===b.length)for(var f=0;f<a.length;f++)g[a[f]]?g[a[f]]++:(g[a[f]]=1,e.push({display:b[f],filter:a[f],sort:c[f],type:d[f]})),this.s.rowData.totalOptions++;else throw Error("display and filter not the same length");else"string"===typeof this.s.colOpts.orthogonal?(g[a]?g[a]++:(g[a]=1,e.push({display:b,
filter:a,sort:c,type:d})),this.s.rowData.totalOptions++):e.push({display:b,filter:a,sort:c,type:d})};k.prototype._addRow=function(a,b,c,d,e,g,f){for(var l,p=0,n=this.s.indexes;p<n.length;p++){var x=n[p];x.filter===b&&(l=x.index)}void 0===l&&(l=this.s.indexes.length,this.s.indexes.push({filter:b,index:l}));return this.s.dtPane.row.add({className:f,display:""!==a?a:!1!==this.s.colOpts.emptyMessage?this.s.colOpts.emptyMessage:this.c.emptyMessage,filter:b,index:l,shown:c,sort:""!==e?e:!1!==this.s.colOpts.emptyMessage?
this.s.colOpts.emptyMessage:this.c.emptyMessage,total:d,type:g})};k.prototype._adjustTopRow=function(){var a=this.dom.container.find("."+this.classes.subRowsContainer),b=this.dom.container.find(".dtsp-subRow1"),c=this.dom.container.find(".dtsp-subRow2"),d=this.dom.container.find("."+this.classes.topRow);(252>h(a[0]).width()||252>h(d[0]).width())&&0!==h(a[0]).width()?(h(a[0]).addClass(this.classes.narrow),h(b[0]).addClass(this.classes.narrowSub).removeClass(this.classes.narrowSearch),h(c[0]).addClass(this.classes.narrowSub).removeClass(this.classes.narrowButton)):
(h(a[0]).removeClass(this.classes.narrow),h(b[0]).removeClass(this.classes.narrowSub).addClass(this.classes.narrowSearch),h(c[0]).removeClass(this.classes.narrowSub).addClass(this.classes.narrowButton))};k.prototype._buildPane=function(a,b,c,d,e){var g=this;void 0===a&&(a=[]);void 0===b&&(b=!1);void 0===c&&(c=null);void 0===d&&(d=null);void 0===e&&(e=null);this.selections=[];var f=this.s.dt,l=f.column(this.colExists?this.s.index:0),p=this.s.colOpts,n=this.s.rowData,x=f.i18n("searchPanes.count","{total}"),
z=f.i18n("searchPanes.countFiltered","{shown} ({total})"),y=f.state.loaded();this.s.listSet&&(y=f.state());if(this.colExists){var w=-1;if(y&&y.searchPanes&&y.searchPanes.panes)for(var u=0;u<y.searchPanes.panes.length;u++)if(y.searchPanes.panes[u].id===this.s.index){w=u;break}if((!1===p.show||void 0!==p.show&&!0!==p.show)&&-1===w)return this.dom.container.addClass(this.classes.hidden),this.s.displayed=!1;if(!0===p.show||-1!==w)this.s.displayed=!0;if(!this.s.dt.page.info().serverSide&&(null===c||null===
c.searchPanes||null===c.searchPanes.options)){if(0===n.arrayFilter.length){this._populatePane(b);this.s.rowData.totalOptions=0;this._detailsPane();if(y&&y.searchPanes&&y.searchPanes.panes&&-1===w){this.dom.container.addClass(this.classes.hidden);this.s.displayed=!1;return}n.arrayOriginal=n.arrayTotals;n.binsOriginal=n.binsTotal}u=Object.keys(n.binsOriginal).length;b=this._uniqueRatio(u,f.rows()[0].length);if(!1===this.s.displayed&&((void 0===p.show&&null===p.threshold?b>this.c.threshold:b>p.threshold)||
!0!==p.show&&1>=u)){this.dom.container.addClass(this.classes.hidden);this.s.displayed=!1;return}this.c.viewTotal&&0===n.arrayTotals.length?(this.s.rowData.totalOptions=0,this._detailsPane()):n.binsTotal=n.bins;this.dom.container.addClass(this.classes.show);this.s.displayed=!0}else if(null!==c&&null!==c.searchPanes&&null!==c.searchPanes.options){if(void 0!==c.tableLength)this.s.tableLength=c.tableLength,this.s.rowData.totalOptions=this.s.tableLength;else if(null===this.s.tableLength||f.rows()[0].length>
this.s.tableLength)this.s.tableLength=f.rows()[0].length,this.s.rowData.totalOptions=this.s.tableLength;b=f.column(this.s.index).dataSrc();if(void 0!==c.searchPanes.options[b])for(u=0,b=c.searchPanes.options[b];u<b.length;u++)w=b[u],this.s.rowData.arrayFilter.push({display:w.label,filter:w.value,sort:w.label,type:w.label}),this.s.rowData.bins[w.value]=this.c.viewTotal||this.c.cascadePanes?w.count:w.total,this.s.rowData.binsTotal[w.value]=w.total;u=Object.keys(n.binsTotal).length;b=this._uniqueRatio(u,
this.s.tableLength);if(!1===this.s.displayed&&((void 0===p.show&&null===p.threshold?b>this.c.threshold:b>p.threshold)||!0!==p.show&&1>=u)){this.dom.container.addClass(this.classes.hidden);this.s.displayed=!1;return}this.s.rowData.arrayOriginal=this.s.rowData.arrayFilter;this.s.rowData.binsOriginal=this.s.rowData.bins;this.s.displayed=!0}}else this.s.displayed=!0;this._displayPane();if(!this.s.listSet)this.dom.dtP.on("stateLoadParams.dt",function(E,F,D){h.isEmptyObject(f.state.loaded())&&h.each(D,
function(C,I){delete D[C]})});null!==e&&0<h(this.dom.panesContainer).has(e).length?h(this.dom.container).insertAfter(e):h(this.dom.panesContainer).prepend(this.dom.container);u=h.fn.dataTable.ext.errMode;h.fn.dataTable.ext.errMode="none";e=r.Scroller;this.s.dtPane=h(this.dom.dtP).DataTable(h.extend(!0,{columnDefs:[{className:"dtsp-nameColumn",data:"display",render:function(E,F,D){if("sort"===F)return D.sort;if("type"===F)return D.type;var C;(g.s.filteringActive||g.s.showFiltered)&&g.c.viewTotal?C=
z.replace(/{total}/,D.total):C=x.replace(/{total}/,D.total);for(C=C.replace(/{shown}/,D.shown);-1!==C.indexOf("{total}");)C=C.replace(/{total}/,D.total);for(;-1!==C.indexOf("{shown}");)C=C.replace(/{shown}/,D.shown);F='<span class="'+g.classes.pill+'">'+C+"</span>";if(g.c.hideCount||p.hideCount)F="";return'<div class="'+g.classes.nameCont+'"><span title="'+("string"===typeof E&&null!==E.match(/<[^>]*>/)?E.replace(/<[^>]*>/g,""):E)+'" class="'+g.classes.name+'">'+E+"</span>"+F+"</div>"},targets:0,
type:void 0!==f.settings()[0].aoColumns[this.s.index]?f.settings()[0].aoColumns[this.s.index]._sManualType:null},{className:"dtsp-countColumn "+this.classes.badgePill,data:"shown",orderData:[1,2],targets:1,visible:!1},{data:"total",targets:2,visible:!1}],deferRender:!0,dom:"t",info:!1,language:this.s.dt.settings()[0].oLanguage,paging:e?!0:!1,scrollX:!1,scrollY:"200px",scroller:e?!0:!1,select:!0,stateSave:f.settings()[0].oFeatures.bStateSave?!0:!1},this.c.dtOpts,void 0!==p?p.dtOpts:{},void 0===this.s.colOpts.options&&
this.colExists?void 0:{createdRow:function(E,F,D){h(E).addClass(F.className)}},null!==this.customPaneSettings&&void 0!==this.customPaneSettings.dtOpts?this.customPaneSettings.dtOpts:{}));h(this.dom.dtP).addClass(this.classes.table);h(this.dom.searchBox).attr("placeholder",void 0!==p.header?p.header:this.colExists?f.settings()[0].aoColumns[this.s.index].sTitle:this.customPaneSettings.header||"Custom Pane");h.fn.dataTable.select.init(this.s.dtPane);h.fn.dataTable.ext.errMode=u;if(this.colExists){l=
(l=l.search())?l.substr(1,l.length-2).split("|"):[];var B=0;n.arrayFilter.forEach(function(E){""===E.filter&&B++});u=0;for(e=n.arrayFilter.length;u<e;u++){l=!1;w=0;for(var H=this.s.serverSelect;w<H.length;w++)b=H[w],b.filter===n.arrayFilter[u].filter&&(l=!0);if(this.s.dt.page.info().serverSide&&(!this.c.cascadePanes||this.c.cascadePanes&&0!==n.bins[n.arrayFilter[u].filter]||this.c.cascadePanes&&null!==d||l))for(l=this._addRow(n.arrayFilter[u].display,n.arrayFilter[u].filter,d?n.binsTotal[n.arrayFilter[u].filter]:
n.bins[n.arrayFilter[u].filter],this.c.viewTotal||d?String(n.binsTotal[n.arrayFilter[u].filter]):n.bins[n.arrayFilter[u].filter],n.arrayFilter[u].sort,n.arrayFilter[u].type),w=0,H=this.s.serverSelect;w<H.length;w++)b=H[w],b.filter===n.arrayFilter[u].filter&&(this.s.serverSelecting=!0,l.select(),this.s.serverSelecting=!1);else this.s.dt.page.info().serverSide||!n.arrayFilter[u]||void 0===n.bins[n.arrayFilter[u].filter]&&this.c.cascadePanes?this.s.dt.page.info().serverSide||this._addRow("",B,B,"","",
""):this._addRow(n.arrayFilter[u].display,n.arrayFilter[u].filter,n.bins[n.arrayFilter[u].filter],n.binsTotal[n.arrayFilter[u].filter],n.arrayFilter[u].sort,n.arrayFilter[u].type)}}r.select.init(this.s.dtPane);(void 0!==p.options||null!==this.customPaneSettings&&void 0!==this.customPaneSettings.options)&&this._getComparisonRows();this.s.dtPane.draw();this._adjustTopRow();this.s.listSet||(this._setListeners(),this.s.listSet=!0);for(d=0;d<a.length;d++)if(n=a[d],void 0!==n)for(u=0,e=this.s.dtPane.rows().indexes().toArray();u<
e.length;u++)l=e[u],void 0!==this.s.dtPane.row(l).data()&&n.filter===this.s.dtPane.row(l).data().filter&&(this.s.dt.page.info().serverSide?(this.s.serverSelecting=!0,this.s.dtPane.row(l).select(),this.s.serverSelecting=!1):this.s.dtPane.row(l).select());this.s.dt.page.info().serverSide&&this.s.dtPane.search(h(this.dom.searchBox).val()).draw();if(y&&y.searchPanes&&y.searchPanes.panes&&(null===c||1===c.draw))for(this.c.cascadePanes||this._reloadSelect(y),c=0,y=y.searchPanes.panes;c<y.length;c++)a=y[c],
a.id===this.s.index&&(h(this.dom.searchBox).val(a.searchTerm),h(this.dom.searchBox).trigger("input"),this.s.dtPane.order(a.order).draw());this.s.dt.state.save();return!0};k.prototype._detailsPane=function(){var a=this.s.dt;this.s.rowData.arrayTotals=[];this.s.rowData.binsTotal={};var b=this.s.dt.settings()[0];a=a.rows().indexes();if(!this.s.dt.page.info().serverSide)for(var c=0;c<a.length;c++)this._populatePaneArray(a[c],this.s.rowData.arrayTotals,b,this.s.rowData.binsTotal)};k.prototype._displayPane=
function(){var a=this.dom.container,b=this.s.colOpts,c=parseInt(this.c.layout.split("-")[1],10);h(this.dom.topRow).empty();h(this.dom.dtP).empty();h(this.dom.topRow).addClass(this.classes.topRow);3<c&&h(this.dom.container).addClass(this.classes.smallGap);h(this.dom.topRow).addClass(this.classes.subRowsContainer);h(this.dom.upper).appendTo(this.dom.topRow);h(this.dom.lower).appendTo(this.dom.topRow);h(this.dom.searchCont).appendTo(this.dom.upper);h(this.dom.buttonGroup).appendTo(this.dom.lower);(!1===
this.c.dtOpts.searching||void 0!==b.dtOpts&&!1===b.dtOpts.searching||!this.c.controls||!b.controls||null!==this.customPaneSettings&&void 0!==this.customPaneSettings.dtOpts&&void 0!==this.customPaneSettings.dtOpts.searching&&!this.customPaneSettings.dtOpts.searching)&&h(this.dom.searchBox).attr("disabled","disabled").removeClass(this.classes.paneInputButton).addClass(this.classes.disabledButton);h(this.dom.searchBox).appendTo(this.dom.searchCont);this._searchContSetup();this.c.clear&&this.c.controls&&
b.controls&&h(this.dom.clear).appendTo(this.dom.buttonGroup);this.c.orderable&&b.orderable&&this.c.controls&&b.controls&&h(this.dom.nameButton).appendTo(this.dom.buttonGroup);!this.c.hideCount&&!b.hideCount&&this.c.orderable&&b.orderable&&this.c.controls&&b.controls&&h(this.dom.countButton).appendTo(this.dom.buttonGroup);h(this.dom.topRow).prependTo(this.dom.container);h(a).append(this.dom.dtP);h(a).show()};k.prototype._getBonusOptions=function(){return h.extend(!0,{},k.defaults,{orthogonal:{threshold:null},
threshold:null},void 0!==this.c?this.c:{})};k.prototype._getComparisonRows=function(){var a=this.s.colOpts;a=void 0!==a.options?a.options:null!==this.customPaneSettings&&void 0!==this.customPaneSettings.options?this.customPaneSettings.options:void 0;if(void 0!==a){var b=this.s.dt.rows({search:"applied"}).data().toArray(),c=this.s.dt.rows({search:"applied"}),d=this.s.dt.rows().data().toArray(),e=this.s.dt.rows(),g=[];this.s.dtPane.clear();for(var f=0;f<a.length;f++){var l=a[f],p=""!==l.label?l.label:
this.c.emptyMessage,n=l.className,x=p,z="function"===typeof l.value?l.value:[],y=0,w=p,u=0;if("function"===typeof l.value){for(var B=0;B<b.length;B++)l.value.call(this.s.dt,b[B],c[0][B])&&y++;for(B=0;B<d.length;B++)l.value.call(this.s.dt,d[B],e[0][B])&&u++;"function"!==typeof z&&z.push(l.filter)}(!this.c.cascadePanes||this.c.cascadePanes&&0!==y)&&g.push(this._addRow(x,z,y,u,w,p,n))}return g}};k.prototype._getOptions=function(){return h.extend(!0,{},k.defaults,{emptyMessage:!1,orthogonal:{threshold:null},
threshold:null},this.s.dt.settings()[0].aoColumns[this.s.index].searchPanes)};k.prototype._makeSelection=function(){this.updateTable();this.s.updating=!0;this.s.dt.draw();this.s.updating=!1};k.prototype._populatePane=function(a){void 0===a&&(a=!1);var b=this.s.dt;this.s.rowData.arrayFilter=[];this.s.rowData.bins={};var c=this.s.dt.settings()[0];if(!this.s.dt.page.info().serverSide){var d=0;for(a=(!this.c.cascadePanes&&!this.c.viewTotal||this.s.clearing||a?b.rows().indexes():b.rows({search:"applied"}).indexes()).toArray();d<
a.length;d++)this._populatePaneArray(a[d],this.s.rowData.arrayFilter,c)}};k.prototype._populatePaneArray=function(a,b,c,d){void 0===d&&(d=this.s.rowData.bins);var e=this.s.colOpts;if("string"===typeof e.orthogonal)c=c.oApi._fnGetCellData(c,a,this.s.index,e.orthogonal),this.s.rowData.filterMap.set(a,c),this._addOption(c,c,c,c,b,d);else{var g=c.oApi._fnGetCellData(c,a,this.s.index,e.orthogonal.search);null===g&&(g="");"string"===typeof g&&(g=g.replace(/<[^>]*>/g,""));this.s.rowData.filterMap.set(a,
g);d[g]?d[g]++:(d[g]=1,this._addOption(g,c.oApi._fnGetCellData(c,a,this.s.index,e.orthogonal.display),c.oApi._fnGetCellData(c,a,this.s.index,e.orthogonal.sort),c.oApi._fnGetCellData(c,a,this.s.index,e.orthogonal.type),b,d));this.s.rowData.totalOptions++}};k.prototype._reloadSelect=function(a){if(void 0!==a){for(var b,c=0;c<a.searchPanes.panes.length;c++)if(a.searchPanes.panes[c].id===this.s.index){b=c;break}if(void 0!==b){c=this.s.dtPane;var d=c.rows({order:"index"}).data().map(function(f){return null!==
f.filter?f.filter.toString():null}).toArray(),e=0;for(a=a.searchPanes.panes[b].selected;e<a.length;e++){b=a[e];var g=-1;null!==b&&(g=d.indexOf(b.toString()));-1<g&&(this.s.serverSelecting=!0,c.row(g).select(),this.s.serverSelecting=!1)}}}};k.prototype._search=function(a,b){for(var c=this.s.colOpts,d=this.s.dt,e=0,g=this.selections;e<g.length;e++){var f=g[e];"string"===typeof f.filter&&(f.filter=f.filter.replaceAll("&amp;","&"));if(Array.isArray(a)){if(-1!==a.indexOf(f.filter))return!0}else if("function"===
typeof f.filter)if(f.filter.call(d,d.row(b).data(),b)){if("or"===c.combiner)return!0}else{if("and"===c.combiner)return!1}else if(a===f.filter||("string"!==typeof a||0!==a.length)&&a==f.filter||null===f.filter&&"string"===typeof a&&""===a)return!0}return"and"===c.combiner?!0:!1};k.prototype._searchContSetup=function(){this.c.controls&&this.s.colOpts.controls&&h(this.dom.searchButton).appendTo(this.dom.searchLabelCont);!1===this.c.dtOpts.searching||!1===this.s.colOpts.dtOpts.searching||null!==this.customPaneSettings&&
void 0!==this.customPaneSettings.dtOpts&&void 0!==this.customPaneSettings.dtOpts.searching&&!this.customPaneSettings.dtOpts.searching||h(this.dom.searchLabelCont).appendTo(this.dom.searchCont)};k.prototype._searchExtras=function(){var a=this.s.updating;this.s.updating=!0;var b=this.s.dtPane.rows({selected:!0}).data().pluck("filter").toArray(),c=b.indexOf(!1!==this.s.colOpts.emptyMessage?this.s.colOpts.emptyMessage:this.c.emptyMessage),d=h(this.s.dtPane.table().container());-1<c&&(b[c]="");0<b.length?
d.addClass(this.classes.selected):0===b.length&&d.removeClass(this.classes.selected);this.s.updating=a};k.prototype._uniqueRatio=function(a,b){return 0<b&&(0<this.s.rowData.totalOptions&&!this.s.dt.page.info().serverSide||this.s.dt.page.info().serverSide&&0<this.s.tableLength)?a/this.s.rowData.totalOptions:1};k.prototype._updateCommon=function(a){void 0===a&&(a=!1);if(!(this.s.dt.page.info().serverSide||void 0===this.s.dtPane||this.s.filteringActive&&!this.c.cascadePanes&&!0!==a||!0===this.c.cascadePanes&&
!0===this.s.selectPresent||this.s.lastSelect&&this.s.lastCascade)){var b=this.s.colOpts,c=this.s.dtPane.rows({selected:!0}).data().toArray();a=h(this.s.dtPane.table().node()).parent()[0].scrollTop;var d=this.s.rowData;this.s.dtPane.clear();if(this.colExists){0===d.arrayFilter.length?this._populatePane():this.c.cascadePanes&&this.s.dt.rows().data().toArray().length===this.s.dt.rows({search:"applied"}).data().toArray().length?(d.arrayFilter=d.arrayOriginal,d.bins=d.binsOriginal):(this.c.viewTotal||
this.c.cascadePanes)&&this._populatePane();this.c.viewTotal?this._detailsPane():d.binsTotal=d.bins;this.c.viewTotal&&!this.c.cascadePanes&&(d.arrayFilter=d.arrayTotals);for(var e=function(p){if(p&&(void 0!==d.bins[p.filter]&&0!==d.bins[p.filter]&&g.c.cascadePanes||!g.c.cascadePanes||g.s.clearing)){var n=g._addRow(p.display,p.filter,g.c.viewTotal?void 0!==d.bins[p.filter]?d.bins[p.filter]:0:d.bins[p.filter],g.c.viewTotal?String(d.binsTotal[p.filter]):d.bins[p.filter],p.sort,p.type),x=c.findIndex(function(z){return z.filter===
p.filter});-1!==x&&(n.select(),c.splice(x,1))}},g=this,f=0,l=d.arrayFilter;f<l.length;f++)e(l[f])}if(void 0!==b.searchPanes&&void 0!==b.searchPanes.options||void 0!==b.options||null!==this.customPaneSettings&&void 0!==this.customPaneSettings.options)for(e=function(p){var n=c.findIndex(function(x){if(x.display===p.data().display)return!0});-1!==n&&(p.select(),c.splice(n,1))},f=0,l=this._getComparisonRows();f<l.length;f++)b=l[f],e(b);for(e=0;e<c.length;e++)b=c[e],b=this._addRow(b.display,b.filter,0,
this.c.viewTotal?b.total:0,b.display,b.display),this.s.updating=!0,b.select(),this.s.updating=!1;this.s.dtPane.draw();this.s.dtPane.table().node().parentNode.scrollTop=a}};k.version="1.1.0";k.classes={buttonGroup:"dtsp-buttonGroup",buttonSub:"dtsp-buttonSub",clear:"dtsp-clear",clearAll:"dtsp-clearAll",clearButton:"clearButton",container:"dtsp-searchPane",countButton:"dtsp-countButton",disabledButton:"dtsp-disabledButton",dull:"dtsp-dull",hidden:"dtsp-hidden",hide:"dtsp-hide",layout:"dtsp-",name:"dtsp-name",
nameButton:"dtsp-nameButton",nameCont:"dtsp-nameCont",narrow:"dtsp-narrow",paneButton:"dtsp-paneButton",paneInputButton:"dtsp-paneInputButton",pill:"dtsp-pill",search:"dtsp-search",searchCont:"dtsp-searchCont",searchIcon:"dtsp-searchIcon",searchLabelCont:"dtsp-searchButtonCont",selected:"dtsp-selected",smallGap:"dtsp-smallGap",subRow1:"dtsp-subRow1",subRow2:"dtsp-subRow2",subRowsContainer:"dtsp-subRowsContainer",title:"dtsp-title",topRow:"dtsp-topRow"};k.defaults={cascadePanes:!1,clear:!0,combiner:"or",
controls:!0,container:function(a){return a.table().container()},dtOpts:{},emptyMessage:"<i>No Data</i>",hideCount:!1,layout:"columns-3",name:void 0,orderable:!0,orthogonal:{display:"display",filter:"filter",hideCount:!1,search:"filter",show:void 0,sort:"sort",threshold:.6,type:"type"},preSelect:[],threshold:.6,viewTotal:!1};return k}(),q,A,G=function(){function k(a,b,c){var d=this;void 0===c&&(c=!1);this.regenerating=!1;if(!A||!A.versionCheck||!A.versionCheck("1.10.0"))throw Error("SearchPane requires DataTables 1.10 or newer");
if(!A.select)throw Error("SearchPane requires Select");var e=new A.Api(a);this.classes=q.extend(!0,{},k.classes);this.c=q.extend(!0,{},k.defaults,b);this.dom={clearAll:q('<button type="button">Clear All</button>').addClass(this.classes.clearAll),container:q("<div/>").addClass(this.classes.panes).text(e.i18n("searchPanes.loadMessage","Loading Search Panes...")),emptyMessage:q("<div/>").addClass(this.classes.emptyMessage),options:q("<div/>").addClass(this.classes.container),panes:q("<div/>").addClass(this.classes.container),
title:q("<div/>").addClass(this.classes.title),titleRow:q("<div/>").addClass(this.classes.titleRow),wrapper:q("<div/>")};this.s={colOpts:[],dt:e,filterCount:0,filterPane:-1,page:0,panes:[],selectionList:[],serverData:{},stateRead:!1,updating:!1};if(void 0===e.settings()[0]._searchPanes){this._getState();if(this.s.dt.page.info().serverSide)e.on("preXhr.dt",function(g,f,l){void 0===l.searchPanes&&(l.searchPanes={});g=0;for(f=d.s.selectionList;g<f.length;g++){var p=f[g],n=d.s.dt.column(p.index).dataSrc();
void 0===l.searchPanes[n]&&(l.searchPanes[n]={});for(var x=0;x<p.rows.length;x++)l.searchPanes[n][x]=p.rows[x].filter}});e.on("xhr",function(g,f,l,p){l&&l.searchPanes&&l.searchPanes.options&&(d.s.serverData=l,d.s.serverData.tableLength=l.recordsTotal,d._serverTotals())});e.settings()[0]._searchPanes=this;this.dom.clearAll.text(e.i18n("searchPanes.clearMessage","Clear All"));if(this.s.dt.settings()[0]._bInitComplete||c)this._paneDeclare(e,a,b);else e.one("preInit.dt",function(g){d._paneDeclare(e,a,
b)});return this}}k.prototype.clearSelections=function(){this.dom.container.find(this.classes.search).each(function(){q(this).val("");q(this).trigger("input")});for(var a=[],b=0,c=this.s.panes;b<c.length;b++){var d=c[b];void 0!==d.s.dtPane&&a.push(d.clearPane())}this.s.dt.draw();return a};k.prototype.getNode=function(){return this.dom.container};k.prototype.rebuild=function(a,b){void 0===a&&(a=!1);void 0===b&&(b=!1);q(this.dom.emptyMessage).remove();var c=[];!1===a&&q(this.dom.panes).empty();for(var d=
0,e=this.s.panes;d<e.length;d++){var g=e[d];if(!1===a||g.s.index===a)g.clearData(),c.push(g.rebuildPane(void 0!==this.s.selectionList[this.s.selectionList.length-1]?g.s.index===this.s.selectionList[this.s.selectionList.length-1].index:!1,this.s.dt.page.info().serverSide?this.s.serverData:void 0,null,b)),q(this.dom.panes).append(g.dom.container)}this.s.dt.page.info().serverSide||this.s.dt.draw();this.c.cascadePanes||this.c.viewTotal?this.redrawPanes(!0):this._updateSelection();this._updateFilterCount();
this._attachPaneContainer();this.s.dt.draw();return 1===c.length?c[0]:c};k.prototype.redrawPanes=function(a){void 0===a&&(a=!1);var b=this.s.dt;if(!this.s.updating&&!this.s.dt.page.info().serverSide){var c=!0,d=this.s.filterPane;if(b.rows({search:"applied"}).data().toArray().length===b.rows().data().toArray().length)c=!1;else if(this.c.viewTotal)for(var e=0,g=this.s.panes;e<g.length;e++){var f=g[e];if(void 0!==f.s.dtPane){var l=f.s.dtPane.rows({selected:!0}).data().toArray().length;if(0===l)for(var p=
0,n=this.s.selectionList;p<n.length;p++){var x=n[p];x.index===f.s.index&&0!==x.rows.length&&(l=x.rows.length)}0<l&&-1===d?d=f.s.index:0<l&&(d=null)}}g=void 0;e=[];if(this.regenerating){g=-1;1===e.length&&(g=e[0].index);a=0;for(e=this.s.panes;a<e.length;a++)if(f=e[a],void 0!==f.s.dtPane){b=!0;f.s.filteringActive=!0;if(-1!==d&&null!==d&&d===f.s.index||!1===c||f.s.index===g)b=!1,f.s.filteringActive=!1;f.updatePane(b?c:b)}this._updateFilterCount()}else{l=0;for(p=this.s.panes;l<p.length;l++)if(f=p[l],
f.s.selectPresent){this.s.selectionList.push({index:f.s.index,rows:f.s.dtPane.rows({selected:!0}).data().toArray(),protect:!1});b.state.save();break}else f.s.deselect&&(g=f.s.index,n=f.s.dtPane.rows({selected:!0}).data().toArray(),0<n.length&&this.s.selectionList.push({index:f.s.index,rows:n,protect:!0}));if(0<this.s.selectionList.length)for(b=this.s.selectionList[this.s.selectionList.length-1].index,l=0,p=this.s.panes;l<p.length;l++)f=p[l],f.s.lastSelect=f.s.index===b;for(f=0;f<this.s.selectionList.length;f++)if(this.s.selectionList[f].index!==
g||!0===this.s.selectionList[f].protect){b=!1;for(l=f+1;l<this.s.selectionList.length;l++)this.s.selectionList[l].index===this.s.selectionList[f].index&&(b=!0);b||(e.push(this.s.selectionList[f]),this.s.selectionList[f].protect=!1)}g=-1;1===e.length&&(g=e[0].index);l=0;for(p=this.s.panes;l<p.length;l++)if(f=p[l],void 0!==f.s.dtPane){b=!0;f.s.filteringActive=!0;if(-1!==d&&null!==d&&d===f.s.index||!1===c||f.s.index===g)b=!1,f.s.filteringActive=!1;f.updatePane(b?c:!1)}this._updateFilterCount();if(0<
e.length&&(e.length<this.s.selectionList.length||a))for(this._cascadeRegen(e),b=e[e.length-1].index,d=0,a=this.s.panes;d<a.length;d++)f=a[d],f.s.lastSelect=f.s.index===b;else if(0<e.length)for(f=0,a=this.s.panes;f<a.length;f++)if(e=a[f],void 0!==e.s.dtPane){b=!0;e.s.filteringActive=!0;if(-1!==d&&null!==d&&d===e.s.index||!1===c)b=!1,e.s.filteringActive=!1;e.updatePane(b?c:b)}}c||(this.s.selectionList=[])}};k.prototype._attach=function(){var a=this;q(this.dom.container).removeClass(this.classes.hide);
q(this.dom.titleRow).removeClass(this.classes.hide);q(this.dom.titleRow).remove();q(this.dom.title).appendTo(this.dom.titleRow);this.c.clear&&(q(this.dom.clearAll).appendTo(this.dom.titleRow),q(this.dom.clearAll).on("click.dtsps",function(){a.clearSelections()}));q(this.dom.titleRow).appendTo(this.dom.container);for(var b=0,c=this.s.panes;b<c.length;b++)q(c[b].dom.container).appendTo(this.dom.panes);q(this.dom.panes).appendTo(this.dom.container);0===q("div."+this.classes.container).length&&q(this.dom.container).prependTo(this.s.dt);
return this.dom.container};k.prototype._attachExtras=function(){q(this.dom.container).removeClass(this.classes.hide);q(this.dom.titleRow).removeClass(this.classes.hide);q(this.dom.titleRow).remove();q(this.dom.title).appendTo(this.dom.titleRow);this.c.clear&&q(this.dom.clearAll).appendTo(this.dom.titleRow);q(this.dom.titleRow).appendTo(this.dom.container);return this.dom.container};k.prototype._attachMessage=function(){try{var a=this.s.dt.i18n("searchPanes.emptyPanes","No SearchPanes")}catch(b){a=
null}if(null===a)q(this.dom.container).addClass(this.classes.hide),q(this.dom.titleRow).removeClass(this.classes.hide);else return q(this.dom.container).removeClass(this.classes.hide),q(this.dom.titleRow).addClass(this.classes.hide),q(this.dom.emptyMessage).text(a),this.dom.emptyMessage.appendTo(this.dom.container),this.dom.container};k.prototype._attachPaneContainer=function(){for(var a=0,b=this.s.panes;a<b.length;a++)if(!0===b[a].s.displayed)return this._attach();return this._attachMessage()};k.prototype._cascadeRegen=
function(a){this.regenerating=!0;var b=-1;1===a.length&&(b=a[0].index);for(var c=0,d=this.s.panes;c<d.length;c++){var e=d[c];e.setCascadeRegen(!0);e.setClear(!0);(void 0!==e.s.dtPane&&e.s.index===b||void 0!==e.s.dtPane)&&e.clearPane();e.setClear(!1)}this._makeCascadeSelections(a);this.s.selectionList=a;a=0;for(b=this.s.panes;a<b.length;a++)e=b[a],e.setCascadeRegen(!1);this.regenerating=!1};k.prototype._checkMessage=function(){for(var a=0,b=this.s.panes;a<b.length;a++)if(!0===b[a].s.displayed)return;
return this._attachMessage()};k.prototype._getState=function(){var a=this.s.dt.state.loaded();a&&a.searchPanes&&void 0!==a.searchPanes.selectionList&&(this.s.selectionList=a.searchPanes.selectionList)};k.prototype._makeCascadeSelections=function(a){for(var b=0;b<a.length;b++)for(var c=function(f){if(f.s.index===a[b].index&&void 0!==f.s.dtPane){b===a.length-1&&(f.s.lastCascade=!0);0<f.s.dtPane.rows({selected:!0}).data().toArray().length&&void 0!==f.s.dtPane&&(f.setClear(!0),f.clearPane(),f.setClear(!1));
for(var l=function(x){f.s.dtPane.rows().every(function(z){void 0!==f.s.dtPane.row(z).data()&&void 0!==x&&f.s.dtPane.row(z).data().filter===x.filter&&f.s.dtPane.row(z).select()})},p=0,n=a[b].rows;p<n.length;p++)l(n[p]);d._updateFilterCount();f.s.lastCascade=!1}},d=this,e=0,g=this.s.panes;e<g.length;e++)c(g[e]);this.s.dt.state.save()};k.prototype._paneDeclare=function(a,b,c){var d=this;a.columns(0<this.c.columns.length?this.c.columns:void 0).eq(0).each(function(l){d.s.panes.push(new v(b,c,l,d.c.layout,
d.dom.panes))});for(var e=a.columns().eq(0).toArray().length,g=this.c.panes.length,f=0;f<g;f++)this.s.panes.push(new v(b,c,e+f,this.c.layout,this.dom.panes,this.c.panes[f]));if(0<this.c.order.length)for(e=this.c.order.map(function(l,p,n){return d._findPane(l)}),this.dom.panes.empty(),this.s.panes=e,e=0,g=this.s.panes;e<g.length;e++)this.dom.panes.append(g[e].dom.container);this.s.dt.settings()[0]._bInitComplete?this._startup(a):this.s.dt.settings()[0].aoInitComplete.push({fn:function(){d._startup(a)}})};
k.prototype._findPane=function(a){for(var b=0,c=this.s.panes;b<c.length;b++){var d=c[b];if(a===d.s.name)return d}};k.prototype._serverTotals=function(){for(var a=!1,b=!1,c=this.s.dt,d=0,e=this.s.panes;d<e.length;d++){var g=e[d];if(g.s.selectPresent){this.s.selectionList.push({index:g.s.index,rows:g.s.dtPane.rows({selected:!0}).data().toArray(),protect:!1});c.state.save();g.s.selectPresent=!1;a=!0;break}else g.s.deselect&&(b=g.s.dtPane.rows({selected:!0}).data().toArray(),0<b.length&&this.s.selectionList.push({index:g.s.index,
rows:b,protect:!0}),b=a=!0)}if(a){c=[];for(d=0;d<this.s.selectionList.length;d++){g=!1;for(e=d+1;e<this.s.selectionList.length;e++)this.s.selectionList[e].index===this.s.selectionList[d].index&&(g=!0);if(!g){e=!1;a=0;for(var f=this.s.panes;a<f.length;a++)g=f[a],g.s.index===this.s.selectionList[d].index&&0<g.s.dtPane.rows({selected:!0}).data().toArray().length&&(e=!0);e&&c.push(this.s.selectionList[d])}}this.s.selectionList=c}else this.s.selectionList=[];c=-1;if(b&&1===this.s.selectionList.length)for(b=
0,d=this.s.panes;b<d.length;b++)g=d[b],g.s.lastSelect=!1,g.s.deselect=!1,void 0!==g.s.dtPane&&0<g.s.dtPane.rows({selected:!0}).data().toArray().length&&(c=g.s.index);else if(0<this.s.selectionList.length)for(b=this.s.selectionList[this.s.selectionList.length-1].index,d=0,e=this.s.panes;d<e.length;d++)g=e[d],g.s.lastSelect=g.s.index===b,g.s.deselect=!1;else if(0===this.s.selectionList.length)for(b=0,d=this.s.panes;b<d.length;b++)g=d[b],g.s.lastSelect=!1,g.s.deselect=!1;q(this.dom.panes).empty();b=
0;for(d=this.s.panes;b<d.length;b++)g=d[b],g.s.lastSelect?g._setListeners():g.rebuildPane(void 0,this.s.dt.page.info().serverSide?this.s.serverData:void 0,g.s.index===c?!0:null,!0),q(this.dom.panes).append(g.dom.container),void 0!==g.s.dtPane&&(q(g.s.dtPane.table().node()).parent()[0].scrollTop=g.s.scrollTop,q.fn.dataTable.select.init(g.s.dtPane));this.s.dt.page.info().serverSide||this.s.dt.draw()};k.prototype._startup=function(a){var b=this;q(this.dom.container).text("");this._attachExtras();q(this.dom.container).append(this.dom.panes);
q(this.dom.panes).empty();var c=this.s.dt.state.loaded();if(this.c.viewTotal&&!this.c.cascadePanes&&null!==c&&void 0!==c&&void 0!==c.searchPanes&&void 0!==c.searchPanes.panes){for(var d=!1,e=0,g=c.searchPanes.panes;e<g.length;e++){var f=g[e];if(0<f.selected.length){d=!0;break}}if(d)for(d=0,e=this.s.panes;d<e.length;d++)f=e[d],f.s.showFiltered=!0}d=0;for(e=this.s.panes;d<e.length;d++)f=e[d],f.rebuildPane(void 0,0<Object.keys(this.s.serverData).length?this.s.serverData:void 0),q(this.dom.panes).append(f.dom.container);
this.s.dt.page.info().serverSide||this.s.dt.draw();this.s.stateRead||null===c||void 0===c||(this.s.dt.page(c.start/this.s.dt.page.len()),this.s.dt.draw("page"));this.s.stateRead=!0;if(this.c.viewTotal&&!this.c.cascadePanes)for(c=0,d=this.s.panes;c<d.length;c++)f=d[c],f.updatePane();this._updateFilterCount();this._checkMessage();a.on("preDraw.dtsps",function(){b._updateFilterCount();!b.c.cascadePanes&&!b.c.viewTotal||b.s.dt.page.info().serverSide?b._updateSelection():b.redrawPanes();b.s.filterPane=
-1});this.s.dt.on("stateSaveParams.dtsp",function(l,p,n){void 0===n.searchPanes&&(n.searchPanes={});n.searchPanes.selectionList=b.s.selectionList});if(this.s.dt.page.info().serverSide)a.off("page"),a.on("page",function(){b.s.page=b.s.dt.page()}),a.off("preXhr.dt"),a.on("preXhr.dt",function(l,p,n){void 0===n.searchPanes&&(n.searchPanes={});p=l=0;for(var x=b.s.panes;p<x.length;p++){var z=x[p],y=b.s.dt.column(z.s.index).dataSrc();void 0===n.searchPanes[y]&&(n.searchPanes[y]={});if(void 0!==z.s.dtPane){z=
z.s.dtPane.rows({selected:!0}).data().toArray();for(var w=0;w<z.length;w++)n.searchPanes[y][w]=z[w].filter,l++}}b.c.viewTotal&&b._prepViewTotal();0<l&&(l!==b.s.filterCount?(n.start=0,b.s.page=0):n.start=b.s.page*b.s.dt.page.len(),b.s.dt.page(b.s.page),b.s.filterCount=l)});else a.on("preXhr.dt",function(l,p,n){l=0;for(p=b.s.panes;l<p.length;l++)p[l].clearData()});this.s.dt.on("xhr",function(l,p,n,x){var z=!1;if(!b.s.dt.page.info().serverSide)b.s.dt.one("preDraw",function(){if(!z){var y=b.s.dt.page();
z=!0;q(b.dom.panes).empty();for(var w=0,u=b.s.panes;w<u.length;w++){var B=u[w];B.clearData();B.rebuildPane(void 0!==b.s.selectionList[b.s.selectionList.length-1]?B.s.index===b.s.selectionList[b.s.selectionList.length-1].index:!1,void 0,void 0,!0);q(b.dom.panes).append(B.dom.container)}b.s.dt.page.info().serverSide||b.s.dt.draw();b.c.cascadePanes||b.c.viewTotal?b.redrawPanes(b.c.cascadePanes):b._updateSelection();b._checkMessage();b.s.dt.one("draw",function(){b.s.dt.page(y).draw(!1)})}})});c=0;for(d=
this.s.panes;c<d.length;c++)if(f=d[c],void 0!==f&&void 0!==f.s.dtPane&&(void 0!==f.s.colOpts.preSelect&&0<f.s.colOpts.preSelect.length||null!==f.customPaneSettings&&void 0!==f.customPaneSettings.preSelect&&0<f.customPaneSettings.preSelect.length)){e=f.s.dtPane.rows().data().toArray().length;for(g=0;g<e;g++)(-1!==f.s.colOpts.preSelect.indexOf(f.s.dtPane.cell(g,0).data())||null!==f.customPaneSettings&&void 0!==f.customPaneSettings.preSelect&&-1!==f.customPaneSettings.preSelect.indexOf(f.s.dtPane.cell(g,
0).data()))&&f.s.dtPane.row(g).select();f.updateTable()}if(void 0!==this.s.selectionList&&0<this.s.selectionList.length)for(c=this.s.selectionList[this.s.selectionList.length-1].index,d=0,e=this.s.panes;d<e.length;d++)f=e[d],f.s.lastSelect=f.s.index===c;0<this.s.selectionList.length&&this.c.cascadePanes&&this._cascadeRegen(this.s.selectionList);this._updateFilterCount();a.on("destroy.dtsps",function(){for(var l=0,p=b.s.panes;l<p.length;l++)p[l].destroy();a.off(".dtsps");q(b.dom.clearAll).off(".dtsps");
q(b.dom.container).remove();b.clearSelections()});if(this.c.clear)q(this.dom.clearAll).on("click.dtsps",function(){b.clearSelections()});a.settings()[0]._searchPanes=this};k.prototype._prepViewTotal=function(){for(var a=this.s.filterPane,b=!1,c=0,d=this.s.panes;c<d.length;c++){var e=d[c];if(void 0!==e.s.dtPane){var g=e.s.dtPane.rows({selected:!0}).data().toArray().length;0<g&&-1===a?(a=e.s.index,b=!0):0<g&&(a=null)}}c=0;for(d=this.s.panes;c<d.length;c++)if(e=d[c],void 0!==e.s.dtPane&&(e.s.filteringActive=
!0,-1!==a&&null!==a&&a===e.s.index||!1===b))e.s.filteringActive=!1};k.prototype._updateFilterCount=function(){for(var a=0,b=0,c=this.s.panes;b<c.length;b++){var d=c[b];void 0!==d.s.dtPane&&(a+=d.getPaneCount())}b=this.s.dt.i18n("searchPanes.title","Filters Active - %d",a);q(this.dom.title).text(b);void 0!==this.c.filterChanged&&"function"===typeof this.c.filterChanged&&this.c.filterChanged.call(this.s.dt,a)};k.prototype._updateSelection=function(){this.s.selectionList=[];for(var a=0,b=this.s.panes;a<
b.length;a++){var c=b[a];void 0!==c.s.dtPane&&this.s.selectionList.push({index:c.s.index,rows:c.s.dtPane.rows({selected:!0}).data().toArray(),protect:!1})}this.s.dt.state.save()};k.version="1.2.2";k.classes={clear:"dtsp-clear",clearAll:"dtsp-clearAll",container:"dtsp-searchPanes",emptyMessage:"dtsp-emptyMessage",hide:"dtsp-hidden",panes:"dtsp-panesContainer",search:"dtsp-search",title:"dtsp-title",titleRow:"dtsp-titleRow"};k.defaults={cascadePanes:!1,clear:!0,container:function(a){return a.table().container()},
columns:[],filterChanged:void 0,layout:"columns-3",order:[],panes:[],viewTotal:!1};return k}();(function(k){"function"===typeof define&&define.amd?define(["jquery","datatables.net"],function(a){return k(a,window,document)}):"object"===typeof exports?module.exports=function(a,b){a||(a=window);b&&b.fn.dataTable||(b=require("datatables.net")(a,b).$);return k(b,a,a.document)}:k(window.jQuery,window,document)})(function(k,a,b){function c(e,g){void 0===g&&(g=!1);e=new d.Api(e);var f=e.init().searchPanes||
d.defaults.searchPanes;return(new G(e,f,g)).getNode()}m(k);t(k);var d=k.fn.dataTable;k.fn.dataTable.SearchPanes=G;k.fn.DataTable.SearchPanes=G;k.fn.dataTable.SearchPane=v;k.fn.DataTable.SearchPane=v;a=k.fn.dataTable.Api.register;a("searchPanes()",function(){return this});a("searchPanes.clearSelections()",function(){return this.iterator("table",function(e){e._searchPanes&&e._searchPanes.clearSelections()})});a("searchPanes.rebuildPane()",function(e,g){return this.iterator("table",function(f){f._searchPanes&&
f._searchPanes.rebuild(e,g)})});a("searchPanes.container()",function(){var e=this.context[0];return e._searchPanes?e._searchPanes.getNode():null});k.fn.dataTable.ext.buttons.searchPanesClear={text:"Clear Panes",action:function(e,g,f,l){g.searchPanes.clearSelections()}};k.fn.dataTable.ext.buttons.searchPanes={action:function(e,g,f,l){e.stopPropagation();this.popover(l._panes.getNode(),{align:"dt-container"});l._panes.rebuild(void 0,!0)},config:{},init:function(e,g,f){var l=new k.fn.dataTable.SearchPanes(e,
k.extend({filterChanged:function(n){e.button(g).text(e.i18n("searchPanes.collapse",{0:"SearchPanes",_:"SearchPanes (%d)"},n))}},f.config)),p=e.i18n("searchPanes.collapse","SearchPanes",0);e.button(g).text(p);f._panes=l},text:"Search Panes"};k(b).on("preInit.dt.dtsp",function(e,g,f){"dt"===e.namespace&&(g.oInit.searchPanes||d.defaults.searchPanes)&&(g._searchPanes||c(g,!0))});d.ext.feature.push({cFeature:"P",fnInit:c});d.ext.features&&d.ext.features.register("searchPanes",c)})})();
(function(c){"function"===typeof define&&define.amd?define(["jquery","datatables.net-bs4","datatables.net-searchpanes"],function(a){return c(a,window,document)}):"object"===typeof exports?module.exports=function(a,b){a||(a=window);b&&b.fn.dataTable||(b=require("datatables.net-bs4")(a,b).$);console.log(b.fn.dataTable);b.fn.dataTable.SearchPanes||(console.log("not present"),require("datatables.net-searchpanes")(a,b));return c(b,a,a.document)}:c(jQuery,window,document)})(function(c,a,b){a=c.fn.dataTable;
c.extend(!0,a.SearchPane.classes,{buttonGroup:"btn-group col justify-content-end",disabledButton:"disabled",dull:"",narrow:"col",pane:{container:"table"},paneButton:"btn btn-light",pill:"pill badge badge-pill badge-secondary",search:"col-sm form-control search",searchCont:"input-group col-sm",searchLabelCont:"input-group-append",subRow1:"dtsp-subRow1",subRow2:"dtsp-subRow2",table:"table table-sm table-borderless",topRow:"dtsp-topRow row"});c.extend(!0,a.SearchPanes.classes,{clearAll:"dtsp-clearAll col-auto btn btn-light",
container:"dtsp-searchPanes",panes:"dtsp-panes dtsp-container",title:"dtsp-title col",titleRow:"dtsp-titleRow row"});return a.searchPanes});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -14,10 +14,10 @@
<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/fontawesome6/css/all.css">
<link rel="stylesheet" type="text/css"
href="https://cdn.datatables.net/v/bs4/dt-1.10.22/fh-3.1.7/r-2.2.6/sc-2.0.3/sp-1.2.2/datatables.min.css" />
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/v/bs4/dt-1.10.22/fh-3.1.7/r-2.2.6/sc-2.0.3/sp-1.2.2/datatables.min.css" />
<link rel="stylesheet" href="/static/assets/vendors/css/vendor.bundle.base.css">
<link rel="stylesheet" href="/static/assets/css/crafty.css">
<link rel="stylesheet" href="/static/assets/css/crafty-toggle-btn.css">
<link rel="manifest" href="/static/assets/crafty.webmanifest">
<meta name="mobile-web-app-capable" content="yes">
@ -32,7 +32,7 @@
<!-- endinject -->
<!-- Plugin css for this page-->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
<script src="../static/assets/vendors/js/jquery.min.js"></script>
<!-- End Plugin css for this page-->
<!-- Layout styles -->
@ -44,21 +44,15 @@
<link rel="stylesheet" href="/static/assets/css/crafty.css">
<!-- Alpine.js - The modern jQuery alternative -->
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
<script defer src="../static/assets/vendors/js/cdn.min.js"></script>
<!-- End Alpine.js -->
<!-- Bootstrap Toggle -->
<link href="https://gitcdn.github.io/bootstrap-toggle/2.2.2/css/bootstrap-toggle.min.css" rel="stylesheet">
<script defer src="https://gitcdn.github.io/bootstrap-toggle/2.2.2/js/bootstrap-toggle.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.9.1/chart.min.js"
integrity="sha512-ElRFoEQdI5Ht6kZvyzXhYG9NqjtkmlkfYk0wr6wHxU9JEHakS7UJZNeml5ALk+8IKlU6jDgMabC3vkumRokgJA=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/hammer.js/2.0.8/hammer.min.js"
integrity="sha512-UXumZrZNiOwnTcZSHLOfcTs0aos2MzBWHXOHOuB0J/R44QB0dwY5JgfbvljXcklVf65Gc4El6RjZ+lnwd2az2g=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/chartjs-plugin-zoom/1.2.1/chartjs-plugin-zoom.min.js"
integrity="sha512-klQv6lz2YR+MecyFYMFRuU2eAl8IPRo6zHnsc9n142TJuJHS8CG0ix4Oq9na9ceeg1u5EkBfZsFcV3U7J51iew=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<link href="../static/assets/vendors/css/bootstrap-toggle.min.css" rel="stylesheet">
<script defer src="../static/assets/vendors/js/bootstrap-toggle.min.js"></script>
<script src="../static/assets/vendors/js/chart.min.js"></script>
<script src="../static/assets/vendors/js/hammer.min.js"></script>
<script src="../static/assets/vendors/js/chartjs-plugin-zoom.min.js"></script>
<!-- End Bootstrap Toggle -->
@ -88,11 +82,13 @@
<span class="mdi mdi-chevron-double-left"></span>
<span class="mdi mdi-chevron-double-right"></span>
</button>
&nbsp;&nbsp;&nbsp;
<span class="badge-pill badge-outline-primary" id="server-name-nav" style="display: none;"></span>
{% include notify.html %}
<button class="navbar-toggler navbar-toggler-right d-lg-none align-self-center" type="button"
data-toggle="offcanvas">
<button class="navbar-toggler navbar-toggler-right d-lg-none align-self-center" type="button" data-toggle="offcanvas">
<span class="mdi mdi-menu"></span>
</button>
</div>
@ -132,19 +128,32 @@
.notification {
position: relative;
box-sizing: border-box;
padding: 0.5rem;
padding-left: 0.7rem;
width: 180px;
margin-left: 10px;
margin-right: 10px;
margin-right: 1rem;
background: var(--card-banner-bg);
-webkit-transition: right 0.75s, opacity 0.75s, top 0.75s;
-moz-transition: right 0.75s, opacity 0.75s, top 0.75s;
-o-transition: right 0.75s, opacity 0.75s, top 0.75s;
transition: right 0.75s, opacity 0.75s, top 0.75s;
right: -6rem;
right: -20rem;
opacity: 0.1;
margin-bottom: 1rem;
z-index: 999;
top: 0px;
}
.toast-header {
background-color: var(--card-banner-bg);
color: var(--base-text);
}
.toast-body {
background-color: var(--dropdown-bg);
color: var(--base-text);
}
.notification img {
max-height: 20px;
}
.notification strong {
line-height: 20px;
}
.notification.active {
@ -158,34 +167,23 @@
top: -2rem;
}
.notification p {
margin: 0px;
width: calc(160.8px - 16px);
z-index: inherit;
}
.notification span {
position: absolute;
right: 0.5rem;
top: 0.46rem;
cursor: pointer;
font-weight: bold;
line-height: 20px;
font-size: 22px;
font-size: 15px;
user-select: none;
z-index: inherit;
cursor: pointer;
}
</style>
<div class="notifications"></div>
<div class="notifications"></div>
<script src="/static/assets/vendors/js/vendor.bundle.base.js"></script>
<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 type="text/javascript"
src="https://cdn.datatables.net/v/bs4/dt-1.10.22/fh-3.1.7/r-2.2.6/sc-2.0.3/sp-1.2.2/datatables.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootbox.js/5.4.0/bootbox.min.js"></script>
<script type="text/javascript" src="../static/assets/vendors/js/datatables.min.js"></script>
<script src="../static/assets/vendors/js/bootbox.min.js"></script>
<script type="text/javascript" src="/static/assets/js/motd.js"></script>
<script>
@ -241,6 +239,7 @@
let usingWebSockets = false;
let webSocket = null;
let websocketTimeoutId = null;
// {% if request.protocol == 'https' %}
usingWebSockets = true;
@ -291,18 +290,19 @@
wsOpen = false;
console.log('Closed WebSocket', closeEvent);
if (typeof reconnectorId !== 'number') {
setTimeout(sendWssError, 7000);
if (!document.hidden) {
if (typeof reconnectorId !== 'number') {
setTimeout(sendWssError, 7000);
}
console.info("Reconnecting with a timeout of", (getRandomArbitrary(0, 2 ** failedConnectionCounter - 1) + 5) * 1000, "milliseconds");
// Discard old websocket and create a new one in 5 seconds
wsInternal = null
reconnectorId = setTimeout(startWebSocket, (getRandomArbitrary(0, 2 ** failedConnectionCounter - 1) + 5) * 1000)
failedConnectionCounter++;
}
console.info("Reconnecting with a timeout of", (getRandomArbitrary(0, 2 ** failedConnectionCounter - 1) + 5) * 1000, "milliseconds");
// Discard old websocket and create a new one in 5 seconds
wsInternal = null
reconnectorId = setTimeout(startWebSocket, (getRandomArbitrary(0, 2 ** failedConnectionCounter - 1) + 5) * 1000)
failedConnectionCounter++;
};
webSocket = {
on: function (event, callback) {
console.log('registered ' + event + ' event');
@ -315,6 +315,12 @@
}
wsInternal.send(JSON.stringify(message));
},
close: function (code, reason) {
wsInternal.close(code, reason);
},
getStatus: function () {
return wsInternal.readyState;
}
}
} catch (error) {
@ -328,6 +334,21 @@
warn('WebSockets are not supported in Crafty if not using the https protocol')
// {% end%}
// Managing Connexions for Multi Tab opened to reduce bandwith usage
document.addEventListener("visibilitychange", () => {
if (document.visibilityState == "hidden") {
websocketTimeoutId = setTimeout(() => {
webSocket.close(1000, "Closed due to Inactivity");
console.log('%c[Crafty Controller] %cClose Websocket due to Tab not active after 5s', 'font-weight: 900; color: #800080;', 'font-weight: 900; color: #eee;');
}, 10000);
} else {
clearTimeout(websocketTimeoutId)
if (webSocket.getStatus() == WebSocket.CLOSED) {
startWebSocket();
}
}
});
if (webSocket) {
webSocket.on('send_start_error', function (start_error) {
var x = document.querySelector('.bootbox');
@ -363,18 +384,21 @@
if (x) {
x.remove()
}
bootbox.alert({
bootbox.confirm({
title: "{{ translate('notify', 'downloadLogs', data['lang']) }}",
message: "{{ translate('notify', 'finishedPreparing', data['lang']) }}",
buttons: {
ok: {
confirm: {
label: 'Download',
className: 'btn-info'
}
},
callback: function () {
console.log("in callback")
location.href = "/panel/download_support_package";
callback: function (result) {
if (result){
location.href = "/panel/download_support_package";
} else {
bootbox.close();
}
}
});
});
@ -488,30 +512,55 @@
function notify(message) {
console.log(`notify(${message})`);
var paragraphEl = document.createElement('p');
var closeEl = document.createElement('span');
var intId = getRandomInt(0, 100);
var toastDiv = document.createElement('div');
toastDiv.setAttribute("id", "toast_" + intId);
toastDiv.setAttribute("class", "notification toast");
toastDiv.setAttribute("role", "alert");
toastDiv.setAttribute("aria-lived", "assertive");
toastDiv.setAttribute("aria-atomic", "true");
toastDiv.setAttribute("data-delay", "3000");
toastDiv.setAttribute("data-animation", "true");
toastDiv.setAttribute("data-autohide", "false");
paragraphEl.textContent = message;
var toastHeaderDiv = document.createElement('div');
toastHeaderDiv.setAttribute("class", "toast-header");
closeEl.innerHTML = '&times;';
closeEl.addEventListener('click', function () { closeNotification(this) });
var toastHeaderImg = document.createElement('img');
toastHeaderImg.setAttribute("src", "/static/assets/images/logo_small.svg");
toastHeaderImg.setAttribute("class", "mr-auto");
toastHeaderImg.setAttribute("alt", "logo");
toastHeaderDiv.appendChild(toastHeaderImg);
var parentEl = document.createElement('div');
parentEl.appendChild(paragraphEl);
parentEl.appendChild(closeEl);
var toastHeaderTitle = document.createElement('strong');
toastHeaderTitle.setAttribute("class", "mr-auto");
toastHeaderTitle.innerHTML = " Crafty Controller ";
toastHeaderDiv.appendChild(toastHeaderTitle);
parentEl.classList.add('notification');
var toastHeaderCloseSpan = document.createElement('span');
toastHeaderCloseSpan.setAttribute("class", "fa-solid fa-xmark");
toastHeaderCloseSpan.setAttribute("aria-hidden", "true");
toastHeaderCloseSpan.addEventListener('click', function () { closeNotification(this.parentElement) });
toastHeaderDiv.appendChild(toastHeaderCloseSpan);
document.querySelector('.notifications').appendChild(parentEl);
var toastBodyDiv = document.createElement('div');
toastBodyDiv.setAttribute("class", "toast-body");
toastBodyDiv.textContent = message;
toastDiv.appendChild(toastHeaderDiv);
toastDiv.appendChild(toastBodyDiv);
document.querySelector('.notifications').appendChild(toastDiv);
$('#toast_' + intId).toast('show');
setTimeout(function () {
parentEl.classList.add('active');
toastDiv.classList.add('active');
}, 200);
setTimeout(function (element) {
closeNotification(element);
}, 7500, closeEl);
closeNotification(element.parentElement);
}, 7500, toastHeaderCloseSpan);
`
<div class="notification">
@ -524,10 +573,10 @@
document.addEventListener('alpine:init', () => {
console.log('%c[Crafty Controller] %cAlpine.js pre-initialization!', 'font-weight: 900; color: #800080;', 'color: #eee;');
})
});
document.addEventListener('alpine:initialized', () => {
console.log('%c[Crafty Controller] %cAlpine.js initialized!', 'font-weight: 900; color: #800080;', 'color: #eee;');
})
});
$(document).ready(function () {
console.log('%c[Crafty Controller] %cReady for JS!', 'font-weight: 900; color: #800080;', 'font-weight: 900; color: #eee;');
@ -559,4 +608,4 @@
</body>
</html>
</html>

View File

@ -1,7 +1,6 @@
<ul class="navbar-nav ml-auto">
<li class="nav-item dropdown">
<a class="nav-link count-indicator dropdown-toggle" id="notifDropdown" href="#" data-toggle="dropdown"
aria-expanded="false">
<li class="nav-item dropdown notif-li">
<a class="nav-link count-indicator dropdown-toggle" id="notifDropdown" href="#" aria-expanded="false">
<i class="fas fa-broadcast-tower
{% if data.get('update_available') %}
text-danger
@ -21,12 +20,10 @@
<li class="nav-item dropdown user-dropdown">
<a class="nav-link dropdown-toggle" id="UserDropdown" href="#" data-toggle="dropdown" aria-expanded="false">
<img class="img-xs rounded-circle profile-picture" onerror="pfpError(this)" src="{{ data['user_data']['pfp'] }}"
alt="Profile image"> </a>
<img class="img-xs rounded-circle profile-picture" onerror="pfpError(this)" src="{{ data['user_data']['pfp'] }}" alt="Profile image"> </a>
<div class="dropdown-menu dropdown-menu-right navbar-dropdown" aria-labelledby="UserDropdown">
<div class="dropdown-header text-center">
<img class="img-md rounded-circle profile-picture" onerror="pfpError(this)" src="{{ data['user_data']['pfp'] }}"
alt="Profile image">
<img class="img-md rounded-circle profile-picture" onerror="pfpError(this)" src="{{ data['user_data']['pfp'] }}" alt="Profile image">
<p class="mb-1 mt-3 font-weight-semibold">{{ data['user_data']['username'] }}</p>
<p class="font-weight-light text-muted mb-0">Roles: </p>
{% for r in data['user_role'] %}
@ -38,23 +35,19 @@
<p class="font-weight-light text-muted mb-0">Email: {{ data['user_data']['email'] }}</p>
</div>
{% if data['user_data']['preparing'] %}
<span class="dropdown-item" id="support_progress"><i
class="dropdown-item-icon mdi mdi-download-outline text-primary"></i>{{ translate('notify', 'supportLogs',
<span class="dropdown-item" id="support_progress"><i class="dropdown-item-icon mdi mdi-download-outline text-primary"></i>{{ translate('notify', 'supportLogs',
data['lang']) }}<br><br></span>
<span class="dropdown-item" id="support_progress">
<div class="support_progress" style="height: 15px; width: 100%;">
<div class="progress-bar progress-bar-striped progress-bar-animated" id="logs_progress_bar" role="progressbar"
style="width:0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">0%</div>
<div class="progress-bar progress-bar-striped progress-bar-animated" id="logs_progress_bar" role="progressbar" style="width:0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">0%</div>
</div>
</span>
{% else %}
<a class="dropdown-item" id="support_logs"><i
class="dropdown-item-icon mdi mdi-download-outline text-primary"></i>{{ translate('notify', 'supportLogs',
<a class="dropdown-item" id="support_logs"><i class="dropdown-item-icon mdi mdi-download-outline text-primary"></i>{{ translate('notify', 'supportLogs',
data['lang']) }}</i></a>
{% end %}
{% if data['superuser'] %}
<a class="dropdown-item" href="/panel/activity_logs"><i
class="dropdown-item-icon mdi mdi-calendar-check-outline text-primary"></i>{{ translate('notify',
<a class="dropdown-item" href="/panel/activity_logs"><i class="dropdown-item-icon mdi mdi-calendar-check-outline text-primary"></i>{{ translate('notify',
'activityLog', data['lang']) }}</a>
{% end %}
<a class="dropdown-item" href="/logout"><i class="dropdown-item-icon mdi mdi-power text-primary"></i>{{
@ -65,6 +58,7 @@
<style>
.badge-notify {
background: var(--purple);
color: var(--dark);
position: absolute;
-moz-transform: translate(-70%, -70%);
/* For Firefox */
@ -78,16 +72,19 @@
.clear-button:hover {
cursor: pointer;
}
/* Hide scrollbar for Chrome, Safari and Opera */
.notif-div::-webkit-scrollbar {
display: none;
}
/* Hide scrollbar for IE, Edge and Firefox */
.notif-div {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
/* Hide scrollbar for Chrome, Safari and Opera */
.notif-div::-webkit-scrollbar {
display: none;
}
/* Hide scrollbar for IE, Edge and Firefox */
.notif-div {
-ms-overflow-style: none;
/* IE and Edge */
scrollbar-width: none;
/* Firefox */
}
</style>
<script>
function pfpError(image) {
@ -96,21 +93,24 @@
return true;
}
function updateAnnouncements(data) {
console.log(data)
console.log(data);
let text = "";
for (let value of data) {
text += `<li class="card-header header-sm justify-content-between align-items-center" id="${value.id}"><p style="float: right;"><i data-id="${value.id}"class="clear-button fa-regular fa-x"></i></p><a style="color: var(--purple);" href=${value.link} target="_blank"><h6>${value.title}</h6><small><p>${value.date}</p></small><p>${value.desc}</p></li></a>`
}
if (data.length > 0) {
localStorage.setItem("notif-count", data.length);
$("#notif-count").show()
$("#notif-count").html(data.length);
$("#announcements").html(text);
} else {
$("#announcements").html(`<p style='margin-top: 15px;' class='text-center'><i class="fa fa-bell-slash" aria-hidden="true"></i>
</p>`);
$("#notif-count").hide()
}
$(".clear-button").on("click", function (event) {
console.log("CLEAR BUTTON")
event.stopPropagation();
let uuid = event.target.getAttribute("data-id");
$(`#${uuid}`).remove();
send_clear(uuid);
@ -136,10 +136,14 @@
});
let responseData = await res.json();
console.log(responseData);
setTimeout(function() {
getAnnouncements();
}, 1800000); //Wait 30 minutes in miliseconds
console.log("Registered annoucement fetch event in 30 minutes.")
if (responseData.status === "ok") {
updateAnnouncements(responseData.data)
} else {
updateAnnouncements("<li><p>Trouble Getting Annoucements</p></li>")
updateAnnouncements([])
}
}
async function send_clear(uuid) {
@ -161,6 +165,28 @@
bootbox.alert(responseData.error)
}
}
/* Open / Close with Button */
$('li.dropdown.notif-li a').on('click', function (event) {
$(this).parent().toggleClass("show");
$('div.notif-div').toggleClass("show");
if ($('li.dropdown.notif-li a').attr('aria-expanded') === 'false') {
$('li.dropdown.notif-li a').attr('aria-expanded', "true");
}
else {
$('li.dropdown.notif-li a').attr('aria-expanded', "false");
}
});
/* Close when clicking ouside */
$('body').on('click', function (e) {
if (!$('li.dropdown.notif-li').is(e.target) && $('li.dropdown.notif-li').has(e.target).length === 0 && $('show').has(e.target).length === 0) {
$('li.notif-li').removeClass("show");
$('li.dropdown.notif-li a').attr('aria-expanded', "false");
$('div.notif-div').removeClass("show");
}
});
$(document).ready(function () {
getAnnouncements();
})

View File

@ -170,7 +170,7 @@
{% block js %}
<script>
function replacer(key, value) {
if (key == "disabled_language_files") {
if (key == "disabled_language_files" || key == "monitored_mounts") {
if (value == 0) {
return []
} else {
@ -336,6 +336,6 @@
});
})
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-select/1.13.10/js/bootstrap-select.min.js">
<script src="../../static/assets/vendors/js/bootstrap-select.min.js">
</script>
{% end %}

View File

@ -58,13 +58,11 @@
</div>
<div class="wrapper my-auto ml-auto ml-lg-4">
<p id="cpu_data" class="mb-0 text-success" data-toggle="tooltip" data-placement="top" data-html="true"
title="{% raw translate('dashboard', 'cpuCores', data['lang']) %}: {{ data.get('hosts_data').get('cpu_cores') }} <br /> {% raw translate('dashboard', 'cpuCurFreq', data['lang']) %}: {{ data.get('hosts_data').get('cpu_cur_freq') }} <br /> {% raw translate('dashboard', 'cpuMaxFreq', data['lang']) %}: {{ data.get('hosts_data').get('cpu_max_freq') }}">
<p id="cpu_data" class="mb-0 text-success" data-toggle="tooltip" data-placement="top" data-html="true" title="{% raw translate('dashboard', 'cpuCores', data['lang']) %}: {{ data.get('hosts_data').get('cpu_cores') }} <br /> {% raw translate('dashboard', 'cpuCurFreq', data['lang']) %}: {{ data.get('hosts_data').get('cpu_cur_freq') }} <br /> {% raw translate('dashboard', 'cpuMaxFreq', data['lang']) %}: {{ data.get('hosts_data').get('cpu_max_freq') }}">
{{ translate('dashboard', 'cpuUsage', data['lang']) }}: <span id="cpu_usage">{{
data.get('hosts_data').get('cpu_usage') }}</span>
</p>
<p id="mem_usage" class="mb-0 text-danger" data-toggle="tooltip" data-placement="top"
title="{{ translate('dashboard', 'memUsage', data['lang']) }}: {{ data.get('hosts_data').get('mem_usage') }}">
<p id="mem_usage" class="mb-0 text-danger" data-toggle="tooltip" data-placement="top" title="{{ translate('dashboard', 'memUsage', data['lang']) }}: {{ data.get('hosts_data').get('mem_usage') }}">
{{ translate('dashboard', 'memUsage', data['lang']) }}: <span id="mem_percent">{{
data.get('hosts_data').get('mem_percent') }}%</span>
</p>
@ -104,19 +102,19 @@
<div class="col-12 mt-4">
<div class="d-flex">
<div class="wrapper" style="width: 100%;">
<h5 class="mb-1 font-weight-medium text-primary">Storage
{% if len(data["monitored"]) > 0 %}
<h5 class="mb-1 font-weight-medium text-primary">{{ translate('dashboard', 'storage',
data['lang']) }}
</h5>
{% end %}
<div id="storage_data">
<div class="row">
{% for item in data['hosts_data']['disk_json'] %}
{% if item["mount"] in data["monitored"] %}
<div id="{{item['device']}}" class="col-xl-3 col-lg-3 col-md-4 col-12">
<h4 class="mb-0 font-weight-semibold d-inline-block text-truncate storage-heading"
id="title_{{item['device']}}" data-toggle="tooltip" data-placement="bottom"
title="{{item['mount']}}" style="max-width: 100%;"><i class="fas fa-hdd"></i>
<h4 class="mb-0 font-weight-semibold d-inline-block text-truncate storage-heading" id="title_{{item['device']}}" data-toggle="tooltip" data-placement="bottom" title="{{item['mount']}}" style="max-width: 100%;"><i class="fas fa-hdd"></i>
{{item["mount"]}}</h4>
<div class="progress d-inline-block"
style="height: 20px; width: 100%; background-color: rgb(139, 139, 139) !important;">
<div class="progress d-inline-block" style="height: 20px; width: 100%; background-color: rgb(139, 139, 139) !important;">
<div class="progress-bar
{% if item['percent_used'] <= 58 %}
bg-success
@ -125,8 +123,7 @@
{% else %}
bg-danger
{% end %}
" role="progressbar" style="color: black; height: 100%; width: {{item['percent_used']}}%;"
aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">{{item["used"]}} /
" role="progressbar" style="color: black; height: 100%; width: {{item['percent_used']}}%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">{{item["used"]}} /
{{item["total"]}}
</div>
</div>
@ -153,9 +150,7 @@
data['lang']) }}</h4>
{% if len(data['servers']) > 0 %}
{% if data['user_data']['hints'] %}
<span class="too_small" title="{{ translate('dashboard', 'cannotSeeOnMobile', data['lang']) }}" ,
data-content="{{ translate('dashboard', 'cannotSeeOnMobile2', data['lang']) }}" ,
data-placement="top"></span>
<span class="too_small" title="{{ translate('dashboard', 'cannotSeeOnMobile', data['lang']) }}" , data-content="{{ translate('dashboard', 'cannotSeeOnMobile2', data['lang']) }}" , data-placement="top"></span>
{% end %}
{% end %}
<div><a class="nav-link" href="/server/step1"><i class="fas fa-plus-circle"></i> &nbsp; {{
@ -193,8 +188,7 @@
<td draggable="false">
<i class="fas fa-server"></i>
{% if server['alert'] %}
<a style="color: red !important;" draggable="false"
href="/panel/server_detail?id={{server['server_data']['server_id']}}">
<a style="color: red !important;" draggable="false" href="/panel/server_detail?id={{server['server_data']['server_id']}}">
{{ server['server_data']['server_name'] }}&nbsp; <i class="fas fa-exclamation-triangle"></i>
</a>
{% else %}
@ -208,13 +202,11 @@
{% if server['user_command_permission'] %}
{% if server['stats']['importing'] and server['stats']['running'] %}
<!-- WHAT HAPPENED HERE -->
<a data-id="{{server['server_data']['server_id']}}" class=""><i
class="fa fa-spinner fa-spin"></i>&nbsp;{{ translate('serverTerm', 'installing',
<a data-id="{{server['server_data']['server_id']}}" class=""><i class="fa fa-spinner fa-spin"></i>&nbsp;{{ translate('serverTerm', 'installing',
data['lang']) }}</i></a>
{% elif server['stats']['updating']%}
<!-- WHAT HAPPENED HERE -->
<a data-id="{{server['server_data']['server_id']}}" class=""><i
class="fa fa-spinner fa-spin"></i>&nbsp;{{ translate('serverTerm', 'updating',
<a data-id="{{server['server_data']['server_id']}}" class=""><i class="fa fa-spinner fa-spin"></i>&nbsp;{{ translate('serverTerm', 'updating',
data['lang']) }}</i></a>
{% elif server['stats']['waiting_start']%}
<!-- WHAT HAPPENED HERE -->
@ -226,31 +218,25 @@
{{ translate('serverTerm', 'importing',
data['lang']) }}</a>
{% elif server['stats']['running'] %}
<a data-id="{{server['server_data']['server_id']}}" class="stop_button" data-toggle="tooltip"
title="{{ translate('dashboard', 'stop' , data['lang']) }}">
<a data-id="{{server['server_data']['server_id']}}" class="stop_button" data-toggle="tooltip" title="{{ translate('dashboard', 'stop' , data['lang']) }}">
<i class="fas fa-stop"></i>
</a> &nbsp;
<a data-id="{{server['server_data']['server_id']}}" class="restart_button" data-toggle="tooltip"
title="{{ translate('dashboard', 'restart' , data['lang']) }}">
<a data-id="{{server['server_data']['server_id']}}" class="restart_button" data-toggle="tooltip" title="{{ translate('dashboard', 'restart' , data['lang']) }}">
<i class="fas fa-sync"></i>
</a> &nbsp;
<a data-id="{{server['server_data']['server_id']}}" class="kill_button" data-toggle="tooltip"
title="{{ translate('dashboard', 'kill' , data['lang']) }}">
<a data-id="{{server['server_data']['server_id']}}" class="kill_button" data-toggle="tooltip" title="{{ translate('dashboard', 'kill' , data['lang']) }}">
<i class="fas fa-skull"></i>
</a> &nbsp;
{% else %}
<a data-id="{{server['server_data']['server_id']}}" class="play_button" data-toggle="tooltip"
title="{{ translate('dashboard', 'start' , data['lang']) }}">
<a data-id="{{server['server_data']['server_id']}}" class="play_button" data-toggle="tooltip" title="{{ translate('dashboard', 'start' , data['lang']) }}">
<i class="fas fa-play"></i>
</a> &nbsp;
<a data-id="{{server['server_data']['server_id']}}" class="clone_button" data-toggle="tooltip"
title="{{ translate('dashboard', 'clone' , data['lang']) }}">
<a data-id="{{server['server_data']['server_id']}}" class="clone_button" data-toggle="tooltip" title="{{ translate('dashboard', 'clone' , data['lang']) }}">
<i class="fas fa-clone"></i>
</a> &nbsp;
<a data-id="{{server['server_data']['server_id']}}" class="kill_button" data-toggle="tooltip"
title="{{ translate('dashboard', 'kill' , data['lang']) }}">
<a data-id="{{server['server_data']['server_id']}}" class="kill_button" data-toggle="tooltip" title="{{ translate('dashboard', 'kill' , data['lang']) }}">
<i class="fas fa-skull"></i>
</a> &nbsp;
{% end %}
@ -258,8 +244,7 @@
</td>
<td draggable="false" id="server_cpu_{{server['server_data']['server_id']}}">
<div class="progress mb-1" data-toggle="tooltip" data-placement="top"
title="{{server['stats']['cpu']}}">
<div class="progress mb-1" data-toggle="tooltip" data-placement="top" title="{{server['stats']['cpu']}}">
<div class="progress-bar
{% if server['stats']['cpu'] <= 33 %}
bg-success
@ -268,15 +253,13 @@
{% else %}
bg-danger
{% end %}
" role="progressbar" style="width: {{server['stats']['cpu']}}%" aria-valuenow="0"
aria-valuemin="0" aria-valuemax="100"></div>
" role="progressbar" style="width: {{server['stats']['cpu']}}%" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
</div>
{{server['stats']['cpu']}}%
</td>
<td draggable="false" id="server_mem_{{server['server_data']['server_id']}}">
<div class="progress mb-1" data-toggle="tooltip" data-placement="top"
title="{{server['stats']['mem']}}">
<div class="progress mb-1" data-toggle="tooltip" data-placement="top" title="{{server['stats']['mem']}}">
<div class="progress-bar
{% if server['stats']['mem_percent'] <= 33 %}
bg-success
@ -285,8 +268,7 @@
{% else %}
bg-danger
{% end %}
" role="progressbar" style="width: {{server['stats']['mem_percent']}}%" aria-valuenow="0"
aria-valuemin="0" aria-valuemax="100"></div>
" role="progressbar" style="width: {{server['stats']['mem_percent']}}%" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
</div>
{{server['stats']['mem_percent']}}% -
@ -305,8 +287,7 @@
data['lang']) }} <br />
{% if server['stats']['desc'] != 'False' %}
<div id="desc_id"
style="overflow-wrap: break-word !important; max-width: 85px !important; overflow: scroll;">{{
<div id="desc_id" style="overflow-wrap: break-word !important; max-width: 85px !important; overflow: scroll;">{{
server['stats']['desc'] }}</div> <br />
{% end %}
@ -334,16 +315,14 @@
<br />
<br />
</td>
<span class="server-player-totals" id="server_players_{{server['server_data']['server_id']}}"
data-players="{{ server['stats']['online']}}" data-max="{{ server['stats']['max'] }}"></span>
<span class="server-player-totals" id="server_players_{{server['server_data']['server_id']}}" data-players="{{ server['stats']['online']}}" data-max="{{ server['stats']['max'] }}"></span>
</tr>
{% end %}
</div>
</span>
{% for server in data['failed_servers'] %}
<tr id="{{server['server_id']}}" draggable="false">
<td class="text-warning"><i class="fas fa-server"></i>&nbsp;<a class="text-warning"
href="/panel/server_detail?id={{server['server_id']}}&subpage=config">{{server['server_name']}}</a>
<td class="text-warning"><i class="fas fa-server"></i>&nbsp;<a class="text-warning" href="/panel/server_detail?id={{server['server_id']}}&subpage=config">{{server['server_name']}}</a>
</td>
<td></td>
<td></td>
@ -368,28 +347,22 @@
<div class="row">
<div class="col-10 col-lg-3 mx-0 px-0">
{% if server['alert'] %}
<a style="color: red !important" class="btn btn-link d-flex justify-content-start" type="button"
href="/panel/server_detail?id={{server['server_data']['server_id']}}">
<i class="fas fa-server"></i> {{ server['server_data']['server_name'] }}&nbsp; <i
class="fas fa-exclamation-triangle"></i>
<a style="color: red !important" class="btn btn-link d-flex justify-content-start" type="button" href="/panel/server_detail?id={{server['server_data']['server_id']}}">
<i class="fas fa-server"></i> {{ server['server_data']['server_name'] }}&nbsp; <i class="fas fa-exclamation-triangle"></i>
</a>
{% else %}
<a class="btn btn-link d-flex justify-content-start" type="button"
href="/panel/server_detail?id={{server['server_data']['server_id']}}">
<a class="btn btn-link d-flex justify-content-start" type="button" href="/panel/server_detail?id={{server['server_data']['server_id']}}">
<i class="fas fa-server"></i> {{ server['server_data']['server_name'] }}
</a>
{% end %}
</div>
<div class="col-2 col-lg-3 mx-0 px-0">
<a class="btn btn-link d-flex justify-content-center" type="button" data-toggle="collapse"
data-target="#collapse-{{server['server_data']['server_id']}}" aria-expanded="false"
aria-controls="collapse-{{server['server_data']['server_id']}}">
<a class="btn btn-link d-flex justify-content-center" type="button" data-toggle="collapse" data-target="#collapse-{{server['server_data']['server_id']}}" aria-expanded="false" aria-controls="collapse-{{server['server_data']['server_id']}}">
<i class="fas fa-chart-bar"></i>
</a>
</div>
<div class="col-4 col-lg-3 mx-0 px-0">
<a id="m_server_running_status_{{server['server_data']['server_id']}}"
class="btn btn-link d-flex justify-content-start" type="button">
<a id="m_server_running_status_{{server['server_data']['server_id']}}" class="btn btn-link d-flex justify-content-start" type="button">
{% if server['stats']['running'] %}
<span class="text-success"><i class="fas fa-signal"></i> {{ translate('dashboard', 'online',
data['lang']) }}</span>
@ -410,23 +383,17 @@
{% if server['stats']['running'] %}
<div class="row">
<div class="col-4 px-0">
<a data-id="{{server['server_data']['server_id']}}"
class="btn btn-link stop_button actions_serveritem" data-toggle="tooltip"
title="{{ translate('dashboard', 'stop' , data['lang']) }}">
<a data-id="{{server['server_data']['server_id']}}" class="btn btn-link stop_button actions_serveritem" data-toggle="tooltip" title="{{ translate('dashboard', 'stop' , data['lang']) }}">
<i class="fas fa-stop"></i>
</a>
</div>
<div class="col-4 px-0">
<a data-id="{{server['server_data']['server_id']}}"
class="btn btn-link restart_button actions_serveritem" data-toggle="tooltip"
title="{{ translate('dashboard', 'restart' , data['lang']) }}">
<a data-id="{{server['server_data']['server_id']}}" class="btn btn-link restart_button actions_serveritem" data-toggle="tooltip" title="{{ translate('dashboard', 'restart' , data['lang']) }}">
<i class="fas fa-sync"></i>
</a>
</div>
<div class="col-4 px-0">
<a data-id="{{server['server_data']['server_id']}}"
class="btn btn-link kill_button actions_serveritem" data-toggle="tooltip"
title="{{ translate('dashboard', 'kill' , data['lang']) }}">
<a data-id="{{server['server_data']['server_id']}}" class="btn btn-link kill_button actions_serveritem" data-toggle="tooltip" title="{{ translate('dashboard', 'kill' , data['lang']) }}">
<i class="fas fa-skull"></i>
</a>
</div>
@ -452,31 +419,24 @@
{% elif server['stats']['importing']%}
<div class="row">
<div class="col-12 px-0">
<a data-id="{{server['server_data']['server_id']}}" class="btn btn-link"><i
class="fa fa-spinner fa-spin"></i>
<a data-id="{{server['server_data']['server_id']}}" class="btn btn-link"><i class="fa fa-spinner fa-spin"></i>
{{ translate('serverTerm', 'importing', data['lang']) }}</a>
</div>
</div>
{% else %}
<div class="row">
<div class="col-4 px-0">
<a data-id="{{server['server_data']['server_id']}}"
class="btn play_button actions_serveritem" data-toggle="tooltip"
title="{{ translate('dashboard', 'start' , data['lang']) }}">
<a data-id="{{server['server_data']['server_id']}}" class="btn play_button actions_serveritem" data-toggle="tooltip" title="{{ translate('dashboard', 'start' , data['lang']) }}">
<i class="fas fa-play"></i>
</a>
</div>
<div class="col-4 px-0">
<a data-id="{{server['server_data']['server_id']}}"
class="btn clone_button actions_serveritem" data-toggle="tooltip"
title="{{ translate('dashboard', 'clone' , data['lang']) }}">
<a data-id="{{server['server_data']['server_id']}}" class="btn clone_button actions_serveritem" data-toggle="tooltip" title="{{ translate('dashboard', 'clone' , data['lang']) }}">
<i class="fas fa-clone"></i>
</a>
</div>
<div class="col-4 px-0">
<a data-id="{{server['server_data']['server_id']}}"
class="btn kill_button actions_serveritem" data-toggle="tooltip"
title="{{ translate('dashboard', 'kill' , data['lang']) }}">
<a data-id="{{server['server_data']['server_id']}}" class="btn kill_button actions_serveritem" data-toggle="tooltip" title="{{ translate('dashboard', 'kill' , data['lang']) }}">
<i class="fas fa-skull"></i></a>
</div>
</div>
@ -488,15 +448,13 @@
</h2>
</div>
<div id="collapse-{{server['server_data']['server_id']}}" class="collapse"
aria-labelledby="heading-{{server['server_data']['server_id']}}" data-parent="#accordionServers">
<div id="collapse-{{server['server_data']['server_id']}}" class="collapse" aria-labelledby="heading-{{server['server_data']['server_id']}}" data-parent="#accordionServers">
<div class="card-body">
<div class="row">
<div class="col-6">
<h6>{{ translate('dashboard', 'cpuUsage', data['lang']) }}</h6>
<div id="m_server_cpu_{{server['server_data']['server_id']}}">
<div class="progress mb-1" data-toggle="tooltip" data-placement="top"
title="{{server['stats']['cpu']}}">
<div class="progress mb-1" data-toggle="tooltip" data-placement="top" title="{{server['stats']['cpu']}}">
<div class="progress-bar
{% if server['stats']['cpu'] <= 33 %}
bg-success
@ -505,8 +463,7 @@
{% else %}
bg-danger
{% end %}
" role="progressbar" style="width: {{server['stats']['cpu']}}%" aria-valuenow="0" aria-valuemin="0"
aria-valuemax="100"></div>
" role="progressbar" style="width: {{server['stats']['cpu']}}%" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
</div>
{{server['stats']['cpu']}}%
</div>
@ -514,8 +471,7 @@
<div class="col-6">
<h6>{{ translate('dashboard', 'memUsage', data['lang']) }}</h6>
<div draggable="false" id="m_server_mem_{{server['server_data']['server_id']}}">
<div class="progress mb-1" data-toggle="tooltip" data-placement="top"
title="{{server['stats']['mem']}}">
<div class="progress mb-1" data-toggle="tooltip" data-placement="top" title="{{server['stats']['mem']}}">
<div class="progress-bar
{% if server['stats']['mem_percent'] <= 33 %}
bg-success
@ -524,8 +480,7 @@
{% else %}
bg-danger
{% end %}
" role="progressbar" style="width: {{server['stats']['mem_percent']}}%" aria-valuenow="0"
aria-valuemin="0" aria-valuemax="100"></div>
" role="progressbar" style="width: {{server['stats']['mem_percent']}}%" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
</div>
{{server['stats']['mem_percent']}}% -
@ -554,8 +509,7 @@
data['lang']) }} <br />
{% if server['stats']['desc'] != 'False' %}
<div id="desc_id"
style="overflow-wrap: break-word !important; max-width: 85px !important; overflow: scroll;">
<div id="desc_id" style="overflow-wrap: break-word !important; max-width: 85px !important; overflow: scroll;">
{{ server['stats']['desc'] }}</div> <br />
{% end %}
@ -664,7 +618,7 @@
}
function warn(message, link = null, className = null) {
function warnServer(message, link = null, className = null) {
var closeEl = document.createElement('span');
var strongEL = document.createElement('strong');
var msgEl = document.createElement('div');
@ -796,6 +750,7 @@
/* Update Motd */
let motd = "";
if (server.desc) {
m_motd = `<span id="m_input_motd_` + server.id + `" class="input_motd">` + server.desc + `</span>`;
motd = `<span id="input_motd_` + server.id + `" class="input_motd">` + server.desc + `</span>`;
m_server_infos = server_infos + '<div id="desc_id" style="word-wrap: break-word; overflow: auto;">' + motd + '</div>' + "<br />";
server_infos = server_infos + '<div id="desc_id" style="word-wrap: break-word; max-width: 85px !important; overflow: auto;">' + motd + '</div>' + "<br />";
@ -847,7 +802,7 @@
setTimeout(finishTimeout, 60000);
});
function finishTimeout() {
warn("It seems this is taking a while...it's possible you're using UBlock or a similar ad blocker and it's causing some of our connections to not make it to the server. Try disabling your ad blocker.",
warnServer("It seems this is taking a while...it's possible you're using UBlock or a similar ad blocker and it's causing some of our connections to not make it to the server. Try disabling your ad blocker.",
null, 'wssError');
}
$(".stop_button").click(function () {
@ -1040,23 +995,28 @@
});
$(document).ready(function () {
function sendOrder(id_string) {
async function sendOrder(id_string) {
const token = getCookie("_xsrf")
$.ajax({
type: "PATCH",
headers: { 'X-XSRFToken': token },
url: `/api/v2/users/@me`,
data: JSON.stringify({
let res = await fetch(`/api/v2/users/@me`, {
method: 'PATCH',
headers: {
'X-XSRFToken': token
},
body: JSON.stringify({
server_order: id_string,
}),
success: function (data) {
console.log("got response:");
console.log(data);
},
});
let responseData = await res.json();
if (responseData.status === "ok") {
return
} else {
bootbox.alert({
title: responseData.status,
message: responseData.error
});
}
}
// Inits the sortable
$("table#servers_table tbody")
.sortable({

View File

@ -0,0 +1,73 @@
{% extends ../base.html %}
{% block meta %}
{% end %}
{% block title %}Crafty Controller Starting{% end %}
{% block content %}
<div class="content-wrapper">
<div class="card-header justify-content-between align-items-center" style="border: none;">
<div id="image-div" style="width: 100%;">
<img class="img-center" id="logo-animate" src="../static/assets/images/crafty-logo-square-1024.png"
alt="Crafty Logo, Crafty is loading" width="20%" style="clear: both;">
</div>
<br>
</br>
<div id="text-div" style="width: 100%; text-align: center;">
<h2 id="status" style="display: block;" data-init="{{ translate('startup', 'serverInit', data['lang']) }}"
data-server="{{ translate('startup', 'server', data['lang']) }}"
data-internet="{{ translate('startup', 'internet', data['lang']) }}"
data-tasks="{{ translate('startup', 'tasks', data['lang']) }}"
data-internals="{{ translate('startup', 'internals', data['lang']) }}"
data-almost="{{ translate('startup', 'almost', data['lang']) }}">
{{ translate('startup', 'starting', data['lang']) }}</h2>
</div>
</div>
</div>
<style>
.img-center {
display: block;
margin-left: auto;
margin-right: auto;
}
</style>
<script>
function rotateImage(degree) {
$('#logo-animate').animate({ transform: degree }, {
step: function (now, fx) {
$(this).css({
'-webkit-transform': 'rotate(' + now + 'deg)',
'-moz-transform': 'rotate(' + now + 'deg)',
'transform': 'rotate(' + now + 'deg)'
});
}
});
setTimeout(function () {
rotateImage(360);
}, 2000);
}
$(document).ready(function () {
setTimeout(function () {
rotateImage(360);
}, 2000);
if (webSocket) {
webSocket.on('update', function (data) {
if ("server" in data) {
$("#status").html(`${$("#status").data(data.section)} ${data.server}...`);
} else {
$("#status").html($("#status").data(data.section));
}
});
webSocket.on('send_start_reload', function () {
setTimeout(function () {
location.href = '/panel/dashboard'
}, 5000);
});
}
});
</script>
{% end %}

View File

@ -79,7 +79,7 @@
<tbody>
{% for user in data['users'] %}
<tr>
<td><i class="fas fa-user"></i> {{ user.username }}</td>
<td><i class="fas fa-user"></i><span id="user_{{user.user_id}}">{{ user.username }}</span></td>
<td>
{% if user.enabled %}
<span class="text-success">
@ -106,7 +106,10 @@
{% end %}
</ul>
</td>
<td><a href="/panel/edit_user?id={{user.user_id}}"><i class="fas fa-pencil-alt"></i></a></td>
<td><span data-translate="{{translate('userConfig', 'userName', data['lang'])}}" data-toggle="tooltip" title="{{ translate('userConfig', 'userName', data['lang'])}}" id="username_{{user.user_id}}" class="edit_user clickable" data-name="{{user.username}}" data-id="{{user.user_id}}"><i class="fa-solid fa-user"></i></span>
&nbsp;&nbsp;<span data-translate1="{{translate('userConfig', 'password', data['lang'])}}" data-translate2="{{translate('userConfig', 'repeat', data['lang'])}}" data-toggle="tooltip" title="{{ translate('userConfig', 'password', data['lang'])}}" class="edit_password clickable" data-id="{{user.user_id}}"><i class="fa-solid fa-lock"></i></span>
&nbsp;&nbsp;<a data-toggle="tooltip" title="{{ translate('userConfig', 'pageTitle', data['lang'])}}" href="/panel/edit_user?id={{user.user_id}}"><i class="fas fa-pencil-alt"></i></a>
</td>
</tr>
{% end %}
{% for user in data['managed_users'] %}
@ -138,7 +141,10 @@
{% end %}
</ul>
</td>
<td><a href="/panel/edit_user?id={{user.user_id}}"><i class="fas fa-pencil-alt"></i></a></td>
<td><span data-translate="{{translate('userConfig', 'userName', data['lang'])}}" data-toggle="tooltip" title="{{ translate('userConfig', 'userName', data['lang'])}}" id="username_{{user.user_id}}" class="edit_user clickable" data-name="{{user.username}}" data-id="{{user.user_id}}"><i class="fa-solid fa-user"></i></span>
&nbsp;&nbsp;<span data-translate1="{{translate('userConfig', 'password', data['lang'])}}" data-translate2="{{translate('userConfig', 'repeat', data['lang'])}}" data-toggle="tooltip" title="{{ translate('userConfig', 'password', data['lang'])}}" class="edit_password clickable" data-id="{{user.user_id}}"><i class="fa-solid fa-lock"></i></span>
&nbsp;&nbsp;<a data-toggle="tooltip" title="{{ translate('userConfig', 'pageTitle', data['lang'])}}" href="/panel/edit_user?id={{user.user_id}}"><i class="fas fa-pencil-alt"></i></a>
</td>
</tr>
{% end %}
</tbody>
@ -274,6 +280,12 @@
</div>
<style>
.clickable {
color: #007bff;
}
.clickable:hover {
cursor: pointer;
}
.custom-picker {
border: 1px solid var(--outline);
}
@ -312,6 +324,99 @@
{% block js %}
<script>
function validateForm() {
let password0 = document.getElementById("password0").value;
let password1 = document.getElementById("password1").value;
if (password0 != password1) {
$('.passwords-match').popover('show');
$('.popover-body').click(function () {
$('.passwords-match').popover("hide");
});
document.body.scrollTop = 0;
document.documentElement.scrollTop = 0;
$("#password0").css("outline", "1px solid red");
$("#password1").css("outline", "1px solid red");
return false;
} else {
return password1;
}
}
$(".edit_password").on("click", async function(){
const token = getCookie("_xsrf");
let user_id = $(this).data('id');
bootbox.confirm(`<form class="form" id='infos' action=''>\
<div class="form-group">
<label for="new_password">${$(this).data("translate1")}</label>
<input class="form-control" type='password' id="password0" name='new_password' /></br>\
</div>
<div class="form-group">
<label for="confirm_password">${$(this).data("translate2")}</label>
<input class="form-control" type='password' id="password1" name='confirm_password' />\
</div>
</form>`, async function(result) {
if(result){
password = validateForm();
if (!password){
return;
}
let res = await fetch(`/api/v2/users/${user_id}`, {
method: 'PATCH',
headers: {
'X-XSRFToken': token
},
body: JSON.stringify({"password": password}),
});
let responseData = await res.json();
if (responseData.status === "ok") {
console.log(responseData.data)
} else {
bootbox.alert({
title: responseData.status,
message: responseData.error
});
}
}
});
});
$(document).on("submit", ".bootbox form", function(e) {
e.preventDefault();
$(".bootbox .btn-primary").click();
});
$(".edit_user").on("click", function(){
const token = getCookie("_xsrf");
let username = $(this).data('name');
let user_id = $(this).data('id');
bootbox.confirm(`<form class="form" id='infos' action=''>\
<div class="form-group">
<label for="username">${$(this).data("translate")}</label>
<input class="form-control" type='text' name='username' id="username_field" value=${username} /><br/>\
</div>
</form>`, async function(result) {
if(result){
let new_username = $("#username_field").val();
let res = await fetch(`/api/v2/users/${user_id}`, {
method: 'PATCH',
headers: {
'X-XSRFToken': token
},
body: JSON.stringify({"username": new_username}),
});
let responseData = await res.json();
if (responseData.status === "ok") {
$(`#user_${user_id}`).html(` ${new_username}`)
$(`#username_${user_id}`).data('name', new_username);
} else {
bootbox.alert({
title: responseData.status,
message: responseData.error
});
}
}
});
});
if (webSocket) {
webSocket.on('move_status', function (message) {
if (message === "done") {

View File

@ -71,6 +71,7 @@ data['lang']) }}{% end %}
data['lang']) }}</h4>
</div>
<div class="card-body">
{% if data['new_user'] %}
<div class="form-group">
<label class="form-label" for="username">{{ translate('userConfig', 'userName', data['lang'])
}}<small class="text-muted ml-1"> - {{ translate('userConfig', 'userNameDesc', data['lang'])
@ -98,6 +99,15 @@ data['lang']) }}{% end %}
data-content="{{ translate('panelConfig', 'match', data['lang']) }}" ,
data-placement="right"></span>
</div>
{% else %}
<div class="form-group">
<label class="form-label" for="username">{{ translate('userConfig', 'userName', data['lang'])
}}<small class="text-muted ml-1"> - {{ translate('userConfig', 'userNameDesc', data['lang'])
}}</small> </label>
<input type="text" class="form-control" name="username" id="username" autocomplete="off"
data-lpignore="true" value="{{ data['user']['username'] }}" placeholder="User Name" disabled>
</div>
{% end %}
<div class="form-group">
<label class="form-label" for="email">{{ translate('userConfig', 'gravEmail', data['lang'])
}}<small class="text-muted ml-1"> - {{ translate('userConfig', 'gravDesc', data['lang'])
@ -353,6 +363,7 @@ data['lang']) }}{% end %}
{% block js %}
<script>
const userId = new URLSearchParams(document.location.search).get('id');
function submit_user(event) {
if (!validateForm()) {
event.preventDefault();
@ -388,18 +399,41 @@ data['lang']) }}{% end %}
return (isNaN(value) ? value : +value);
}
}
const userId = new URLSearchParams(document.location.search).get('id')
$("#user_form").on("submit", async function (e) {
e.preventDefault();
let password = validateForm();
if (!password){
return;
let password = null;
if(!userId){
password = validateForm();
if (!password){
return;
}
}
const token = getCookie("_xsrf")
let userRes = await fetch(`/api/v2/users/@me`, {
method: "GET",
headers: {
'X-XSRFToken': token
},
});
let userData = await userRes.json();
let superuser = null;
if (userData.status === "ok") {
superuser = userData.data["superuser"];
edit_id = userData.data["user_id"];
} else {
bootbox.alert({
title: userData.error,
message: userData.error
});
}
let userForm = document.getElementById("user_form");
let disabled_flag = false;
let roles = $('.role_check').map(function() {
let roles = null;
if (superuser || userId != edit_id){
roles = $('.role_check').map(function() {
if ($(this).attr("disabled")){
disabled_flag = true;
}
@ -407,7 +441,6 @@ data['lang']) }}{% end %}
return $(this).val();
}
}).get();
let avail_permissions = $('.perm-name').map(function() {
return $(this).data("perm");
}).get();
@ -416,20 +449,26 @@ data['lang']) }}{% end %}
for(i=0; i < avail_permissions.length; i++){
permissions.push({"name": avail_permissions[i], "quantity": $(`#quantity_${avail_permissions[i]}`).val(), "enabled": $(`#permission_${avail_permissions[i]}`).is(':checked')})
}
console.log(permissions);
}
let formData = new FormData(userForm);
//Create an object from the form data entries
let formDataObject = Object.fromEntries(formData.entries());
if(userId){
delete formDataObject.username
}
if (superuser || userId != edit_id){
if (!disabled_flag){
formDataObject.roles = roles;
}
if ($("#permissions").length){
formDataObject.permissions = permissions;
}
if(typeof password === "string"){
if(!userId){
if(typeof password === "string"){
formDataObject.password = password;
}
}
}
formDataObject.enabled = $("#enabled").is(":checked");
if ($("#superuser").is(":enabled")){
formDataObject.superuser = $("#superuser").is(":checked");

View File

@ -248,12 +248,38 @@
$("#player-body").html(text);
}
//used to get cookies from browser - this is part of tornados xsrf protection - it's for extra security
function getCookie(name) {
var r = document.cookie.match("\\b" + name + "=([^;]*)\\b");
return r ? r[1] : undefined;
}
const token = getCookie("_xsrf")
$(window).ready(function () {
console.log("ready!");
//if (webSocket) {
webSocket.on('update_server_details', update_server_details);
add_server_name();
//}
});
async function add_server_name(){
let res = await fetch(`/api/v2/servers/${serverId}`, {
method: 'GET',
headers: {
'X-XSRFToken': token
},
});
let responseData = await res.json();
if (responseData.status === "ok") {
console.log(responseData)
$("#server-name-nav").html(`${responseData.data['server_name']}`)
$("#server-name-nav").show();
} else {
bootbox.alert({
title: responseData.error,
message: responseData.error_data
});
}
}
</script>

View File

@ -31,6 +31,9 @@
<a class="dropdown-item {% if data['active_link'] == 'admin_controls' %}active{% end %}" href="/panel/server_detail?id={{ data['server_stats']['server_id']['server_id'] }}&subpage=admin_controls" role="tab" aria-selected="true"><i class="fas fa-users"></i> {{ translate('serverDetails', 'playerControls', data['lang']) }}</a>
{% end %}
<a class="dropdown-item {% if data['active_link'] == 'metrics' %}active{% end %}" href="/panel/server_detail?id={{ data['server_stats']['server_id']['server_id'] }}&subpage=metrics" role="tab" aria-selected="true"><i class="fa-solid fa-chart-line"></i> {{ translate('serverDetails', 'metrics', data['lang']) }}</a>
{% if data['permissions']['Config'] in data['user_permissions'] %}
<a class="dropdown-item {% if data['active_link'] == 'webhooks' %}active{% end %}" href="/panel/server_detail?id={{ data['server_stats']['server_id']['server_id'] }}&subpage=webhooks" role="tab" aria-selected="true"><i class="fa-regular fa-bell"></i>{{ translate('webhooks', 'webhooks', data['lang']) }}</a>
{% end %}
</div>
</div>
</div>

View File

@ -53,4 +53,10 @@
<a class="nav-link {% if data['active_link'] == 'metrics' %}active{% end %}" href="/panel/server_detail?id={{ data['server_stats']['server_id']['server_id'] }}&subpage=metrics" role="tab" aria-selected="true">
<i class="fa-solid fa-chart-line"></i>{{ translate('serverDetails', 'metrics', data['lang']) }}</a>
</li>
{% if data['permissions']['Config'] in data['user_permissions'] %}
<li class="nav-item term-nav-item">
<a class="nav-link {% if data['active_link'] == 'webhooks' %}active{% end %}" href="/panel/server_detail?id={{ data['server_stats']['server_id']['server_id'] }}&subpage=webhooks" role="tab" aria-selected="true">
<i class="fa-regular fa-bell"></i>{{ translate('webhooks', 'webhooks', data['lang']) }}</a>
</li>
{% end %}
</ul>

View File

@ -69,7 +69,7 @@
<td>Banned on {{ player['banned_on'] }}</td>
<td>Banned by : {{ player['source'] }} <br />Reason : {{ player['reason'] }}</td>
<td class="buttons">
<button onclick="send_command_to_server(`pardon {{ player['name'] }} `)" type="button" class="btn btn-danger">Unban</button>
<button onclick="send_command_to_server(`pardon {{ player['name'] }}`)" type="button" class="btn btn-danger">Unban</button>
</td>
</tr>
{% end %}

View File

@ -44,25 +44,25 @@
<div class="col-md-6 col-sm-12">
<br>
<br>
<form id="backup-form" class="forms-sample">
{% if data['backing_up'] %}
<div class="progress" style="height: 15px;">
<div class="progress-bar progress-bar-striped progress-bar-animated" id="backup_progress_bar"
role="progressbar" style="width:{{data['backup_stats']['percent']}}%;"
aria-valuenow="{{data['backup_stats']['percent']}}" aria-valuemin="0" aria-valuemax="100">{{
data['backup_stats']['percent'] }}%</div>
</div>
<p>Backing up <i class="fas fa-spin fa-spinner"></i> <span
id="total_files">{{data['server_stats']['world_size']}}</span></p>
{% end %}
{% if data['backing_up'] %}
<div class="progress" style="height: 15px;">
<div class="progress-bar progress-bar-striped progress-bar-animated" id="backup_progress_bar"
role="progressbar" style="width:{{data['backup_stats']['percent']}}%;"
aria-valuenow="{{data['backup_stats']['percent']}}" aria-valuemin="0" aria-valuemax="100">{{
data['backup_stats']['percent'] }}%</div>
</div>
<p>Backing up <i class="fas fa-spin fa-spinner"></i> <span
id="total_files">{{data['server_stats']['world_size']}}</span></p>
{% end %}
<br>
{% if not data['backing_up'] %}
<div id="backup_button" class="form-group">
<button class="btn btn-primary" id="backup_now_button">{{ translate('serverBackups', 'backupNow',
data['lang']) }}</button>
</div>
{% end %}
<br>
{% if not data['backing_up'] %}
<div id="backup_button" class="form-group">
<button class="btn btn-primary" id="backup_now_button">{{ translate('serverBackups', 'backupNow',
data['lang']) }}</button>
</div>
{% end %}
<form id="backup-form" class="forms-sample">
<div class="form-group">
{% if data['super_user'] %}
<label for="server_name">{{ translate('serverBackups', 'storageLocation', data['lang']) }} <small
@ -309,11 +309,6 @@
async function backup_started() {
const token = getCookie("_xsrf")
document.getElementById('backup_button').style.visibility = 'hidden';
var dialog = bootbox.dialog({
message: "{{ translate('serverBackups', 'backupTask', data['lang']) }}",
closeButton: false
});
let res = await fetch(`/api/v2/servers/${server_id}/action/backup_server`, {
method: 'POST',
headers: {
@ -323,8 +318,14 @@
let responseData = await res.json();
if (responseData.status === "ok") {
console.log(responseData);
process_tree_response(responseData);
$("#backup_button").html(`<div class="progress" style="height: 15px;">
<div class="progress-bar progress-bar-striped progress-bar-animated" id="backup_progress_bar"
role="progressbar" style="width:{{data['backup_stats']['percent']}}%;"
aria-valuenow="{{data['backup_stats']['percent']}}" aria-valuemin="0" aria-valuemax="100">{{
data['backup_stats']['percent'] }}%</div>
</div>
<p>Backing up <i class="fas fa-spin fa-spinner"></i> <span
id="total_files">{{data['server_stats']['world_size']}}</span></p>`);
} else {
bootbox.alert({
@ -427,6 +428,7 @@
if ($("#root_files_button").hasClass("clicked")){
formDataObject.exclusions = excluded;
}
delete formDataObject.root_path
console.log(excluded);
console.log(formDataObject);
// Format the plain form data as JSON
@ -649,10 +651,10 @@
let checked = ""
let dpath = value.path;
let filename = key;
if (value.excluded){
checked = "checked"
}
if (value.dir){
if (value.excluded){
checked = "checked"
}
text += `<li class="tree-item" data-path="${dpath}">
\n<div id="${dpath}" data-path="${dpath}" data-name="${filename}" class="tree-caret tree-ctx-item tree-folder">
<input type="checkbox" class="checkBoxClass excluded" value="${dpath}" ${checked}>

View File

@ -136,7 +136,7 @@
<label for="server_port">{{ translate('serverConfig', 'serverPort', data['lang']) }} <small class="text-muted ml-1"> - {{ translate('serverConfig', 'serverPortDesc', data['lang']) }}
</small> </label>
<input type="number" class="form-control" name="server_port" id="server_port" value="{{ data['server_stats']['server_id']['server_port'] }}" step="1" max="65566" min="1" required>
<span data-html="true" class="port-hint text-center" title="<i class='fal fa-exclamation-triangle'></i> " , data-content="{{
<span data-html="true" class="port-hint text-center" title="<i class='fa-solid fa-triangle-exclamation'></i> " , data-content="{{
translate('serverConfig', 'statsHint1' , data['lang'])}} <br> <br> <strong>{{ translate('serverConfig', 'statsHint2', data['lang'])}}</strong>" , data-placement="right"></span>
</div>
{% end %}

View File

@ -156,6 +156,9 @@
right: 35px;
}
}
.tree-file:hover{
cursor: pointer;
}
</style>
<ul class="tree-view">
<li>

View File

@ -89,11 +89,11 @@
const cpu = []
{% for item in data['history_stats'] %}
{% if 'minecraft-java' in data['server_stats']['server_type'] %}
players.push("{{ item.online }}");
players.push("{{ item.get('online') }}");
{% end %}
dates.push("{{ item.created.strftime('%Y/%m/%d, %H:%M:%S') }}");
ram.push("{{ item.mem_percent }}")
cpu.push("{{ item.cpu }}")
dates.push("{{ item.get('created').strftime('%Y/%m/%d, %H:%M:%S') }}");
ram.push("{{ item.get('mem_percent') }}")
cpu.push("{{ item.get('cpu') }}")
{% end %}
var hist_chart = new Chart(ctxL, {
type: 'line',

View File

@ -47,198 +47,184 @@
<h4 class="card-title"><i class="fas fa-calendar"></i> {{ translate('serverSchedules',
'scheduledTasks', data['lang']) }} </h4>
{% if data['user_data']['hints'] %}
<span class="too_small" title="{{ translate('serverSchedules', 'cannotSee', data['lang']) }}" ,
data-content="{{ translate('serverSchedules', 'cannotSeeOnMobile', data['lang']) }}" ,
data-placement="bottom"></span>
<span class="too_small" title="{{ translate('serverSchedules', 'cannotSee', data['lang']) }}" , data-content="{{ translate('serverSchedules', 'cannotSeeOnMobile', data['lang']) }}" , data-placement="bottom"></span>
{% end %}
<div>
<button
onclick="location.href=`/panel/add_schedule?id={{ data['server_stats']['server_id']['server_id'] }}`"
class="btn btn-info">{{ translate('serverSchedules', 'create', data['lang']) }}<i
class="fas fa-pencil-alt"></i></button>
</div>
<div><a class="btn btn-info" href="/panel/add_schedule?id={{ data['server_stats']['server_id']['server_id'] }}"><i class="fas fa-plus-circle"></i> {{ translate('serverSchedules', 'create', data['lang']) }}</a></div>
</div>
<div class="card-body">
<table class="table table-hover d-none d-lg-block responsive-table" id="schedule_table"
style="table-layout:fixed;" aria-describedby="Schedule List">
<thead>
<tr class="rounded">
<th style="width: 2%; min-width: 10px;">{{ translate('serverSchedules', 'name', data['lang']) }}
</th>
<th style="width: 23%; min-width: 50px;">{{ translate('serverSchedules', 'action', data['lang'])
}}</th>
<th style="width: 40%; min-width: 50px;">{{ translate('serverSchedules', 'command',
data['lang']) }}</th>
<th style="width: 10%; min-width: 50px;">{{ translate('serverSchedules', 'interval',
data['lang']) }}</th>
<th style="width: 10%; min-width: 50px;">{{ translate('serverSchedules', 'nextRun',
data['lang']) }}</th>
<th style="width: 10%; min-width: 50px;">{{ translate('serverSchedules', 'enabled',
data['lang']) }}</th>
<th style="width: 10%; min-width: 50px;">{{ translate('serverSchedules', 'edit', data['lang'])
}}</th>
</tr>
</thead>
<tbody>
{% for schedule in data['schedules'] %}
<tr>
<td id="{{schedule.schedule_id}}" class="id">
<p>{{schedule.name}}</p>
</td>
<td id="{{schedule.action}}" class="action">
<p>{{schedule.action}}</p>
</td>
<td id="{{schedule.command}}" class="action" style="overflow: scroll; max-width: 30px;">
<p style="overflow: scroll;" class="no-scroll">{{schedule.command}}</p>
</td>
<td id="{{schedule.interval}}" class="action">
{% if schedule.interval_type != '' and schedule.interval_type != 'reaction' %}
<p>{{ translate('serverSchedules', 'every', data['lang']) }}</p>
<p>{{schedule.interval}} {{schedule.interval_type}}</p>
{% elif schedule.interval_type == 'reaction' %}
<p>{{schedule.interval_type}}<br><br>{{ translate('serverSchedules', 'child', data['lang'])}}:
{{ schedule.parent }}</p>
{% else %}
<p>Cron String:</p>
<p>{{schedule.cron_string}}</p>
{% end %}
</td>
<td id="{{schedule.start_time}}" class="action">
<p>{{schedule.next_run}}</p>
</td>
<td id="{{schedule.enabled}}" class="action">
<input style="width: 10px !important;" type="checkbox" class="schedule-enabled-toggle"
data-schedule-id="{{schedule.schedule_id}}"
data-schedule-enabled="{{ 'true' if schedule.enabled else 'false' }}">
</td>
<td id="{{schedule.action}}" class="action">
<button
onclick="window.location.href='/panel/edit_schedule?id={{ data['server_stats']['server_id']['server_id'] }}&sch_id={{schedule.schedule_id}}'"
class="btn btn-info">
<i class="fas fa-pencil-alt"></i>
</button>
<br>
<br>
<button data-sch={{ schedule.schedule_id }} class="btn btn-danger del_button">
<i class="fas fa-trash" aria-hidden="true"></i>
</button>
</td>
</tr>
{% end %}
</tbody>
</table>
<hr />
<table class="table table-hover d-block d-lg-none" id="mini_schedule_table"
style="table-layout:fixed;" aria-describedby="Schedule List Mobile">
<thead>
<tr class="rounded">
<th style="width: 25%; min-width: 50px;">{{ translate('serverSchedules', 'action', data['lang'])
}}</th>
<th style="max-width: 40%; min-width: 50px;">{{ translate('serverSchedules', 'command',
data['lang']) }}</th>
<th style="width: 10%; min-width: 50px;">{{ translate('serverSchedules', 'enabled',
data['lang']) }}</th>
</tr>
</thead>
<tbody>
{% for schedule in data['schedules'] %}
<tr data-toggle="modal" data-target="#task_details_{{schedule.schedule_id}}">
<td id="{{schedule.action}}" class="action">
<p>{{schedule.action}}</p>
</td>
<td id="{{schedule.command}}" class="action" style="overflow: scroll; max-width: 30px;">
<p style="overflow: scroll;">{{schedule.command}}</p>
</td>
<td id="{{schedule.enabled}}" class="action">
{% if schedule.enabled %}
<span class="text-success">
<i class="fas fa-check-square"></i> {{ translate('serverSchedules', 'yes', data['lang']) }}
</span>
{% else %}
<span class="text-danger">
<i class="far fa-times-square"></i> {{ translate('serverSchedules', 'no', data['lang']) }}
</span>
{% end %}
</td>
</tr>
<!-- Modal -->
<div class="modal fade" id="task_details_{{schedule.schedule_id}}" tabindex="-1" role="dialog"
aria-labelledby="exampleModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">{{ translate('serverSchedules', 'details',
data['lang']) }}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<ul style="list-style: none;">
<li id="{{schedule.schedule_id}}" class="id" style="border-top: .1em solid gray;">
<h4>{{ translate('serverSchedules', 'name', data['lang']) }}</h4>
<p>{{schedule.schedule_id}}</p>
</li>
<li id="{{schedule.action}}" class="action" style="border-top: .1em solid gray;">
<h4>{{ translate('serverSchedules', 'action', data['lang']) }}</h4>
<p>{{schedule.action}}</p>
</li>
<li id="{{schedule.command}}" class="action" style="border-top: .1em solid gray;">
<h4>{{ translate('serverSchedules', 'command', data['lang']) }}</h4>
<p>{{schedule.command}}</p>
</li>
<li id="{{schedule.interval}}" class="action" style="border-top: .1em solid gray;">
{% if schedule.interval != '' %}
<h4>{{ translate('serverSchedules', 'interval', data['lang']) }}</h4>
<p>{{ translate('serverSchedules', 'every', data['lang']) }} {{schedule.interval}}
{{schedule.interval_type}}</p>
{% elif schedule.interval_type == 'reaction' %}
<h4>{{ translate('serverSchedules', 'interval', data['lang']) }}</h4>
<p>{{schedule.interval_type}}<br><br>{{ translate('serverSchedules', 'child',
data['lang']) }}: {{ schedule.parent }}</p>
{% else %}
<h4>{{ translate('serverSchedules', 'interval', data['lang']) }}</h4>
<p>{{ translate('serverSchedules', 'cron', data['lang']) }}: {{schedule.cron_string}}
</p>
{% end %}
</li>
<li id="{{schedule.start_time}}" class="action" style="border-top: .1em solid gray;">
<h4>{{ translate('serverSchedules', 'nextRun', data['lang']) }}</h4>
{% if schedule.next_run %}
<p>{{schedule.next_run}}</p>
{% else %}
<p>zzzzzzz</p>
{% end %}
</li>
<li id="{{schedule.enabled}}" class="action"
style="border-top: .1em solid gray; border-bottom: .1em solid gray">
<h4>{{ translate('serverSchedules', 'enabled', data['lang']) }}</h4>
<input type="checkbox" class="schedule-enabled-toggle"
data-schedule-id="{{schedule.schedule_id}}"
data-schedule-enabled="{{ 'true' if schedule.enabled else 'false' }}">
</li>
</ul>
</div>
<div class="modal-footer">
<button
onclick="window.location.href='/panel/edit_schedule?id={{ data['server_stats']['server_id']['server_id'] }}&sch_id={{schedule.schedule_id}}'"
class="btn btn-info">
<i class="fas fa-pencil-alt"></i> {{ translate('serverSchedules', 'edit', data['lang'])
}}
</button>
<button data-sch={{ schedule.schedule_id }} class="btn btn-danger del_button">
<i class="fas fa-trash" aria-hidden="true"></i> {{ translate('serverSchedules',
'delete', data['lang']) }}
</button>
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{
translate('serverSchedules', 'close', data['lang']) }}</button>
{% if len(data['schedules']) == 0 %}
<div style="text-align: center; color: grey;">
<h7>{{ translate('serverSchedules', 'no-schedule', data['lang']) }} <strong>{{ translate('serverSchedules', 'newSchedule',data['lang']) }}</strong>.</h7>
</div>
{% end %}
{% if len(data['schedules']) > 0 %}
<div class="d-none d-lg-block">
<table class="table table-hover table-responsive" id="schedule_table" style="table-layout:fixed;" aria-describedby="Schedule List">
<thead>
<tr class="rounded">
<th style="width: 5%; min-width: 64px;">{{ translate('serverSchedules', 'enabled',
data['lang']) }}</th>
<th style="width: 7%; min-width: 10px;">{{ translate('serverSchedules', 'name', data['lang']) }}
</th>
<th style="width: 23%; min-width: 50px;">{{ translate('serverSchedules', 'action', data['lang'])
}}</th>
<th style="width: 40%; min-width: 50px;">{{ translate('serverSchedules', 'command',
data['lang']) }}</th>
<th style="width: 10%; min-width: 50px;">{{ translate('serverSchedules', 'interval',
data['lang']) }}</th>
<th style="width: 10%; min-width: 50px;">{{ translate('serverSchedules', 'nextRun',
data['lang']) }}</th>
<th style="width: 10%; min-width: 50px;">{{ translate('serverSchedules', 'edit', data['lang'])
}}</th>
</tr>
</thead>
<tbody>
{% for schedule in data['schedules'] %}
<tr>
<td id="{{schedule.enabled}}" class="enabled">
<button type="button" class="btn btn-sm btn-info btn-toggle schedule-custom-toggle" id="schedule{{schedule.id}}" name="schedule{{schedule.id}}" data-schedule-id="{{schedule.schedule_id}}" data-schedule-enabled="{{ 'true' if schedule.enabled else 'false' }}" data-toggle="button" aria-pressed="true" autocomplete="off">
<div class="handle"></div>
</button>
</td>
<td id="{{schedule.schedule_id}}" class="id">
<p>{{schedule.name}}</p>
</td>
<td id="{{schedule.action}}">
<p>{{schedule.action}}</p>
</td>
<td id="{{schedule.command}}" style="overflow: scroll; max-width: 30px;">
<p style="overflow: scroll;" class="no-scroll">{{schedule.command}}</p>
</td>
<td id="{{schedule.interval}}">
{% if schedule.interval_type != '' and schedule.interval_type != 'reaction' %}
<p>{{ translate('serverSchedules', 'every', data['lang']) }}</p>
<p>{{schedule.interval}} {{schedule.interval_type}}</p>
{% elif schedule.interval_type == 'reaction' %}
<p>{{schedule.interval_type}}<br><br>{{ translate('serverSchedules', 'child', data['lang'])}}:
{{ schedule.parent }}</p>
{% else %}
<p>Cron String:</p>
<p>{{schedule.cron_string}}</p>
{% end %}
</td>
<td id="{{schedule.start_time}}">
<p>{{schedule.next_run}}</p>
</td>
<td id="{{schedule.action}}" class="action">
<button onclick="window.location.href='/panel/edit_schedule?id={{ data['server_stats']['server_id']['server_id'] }}&sch_id={{schedule.schedule_id}}'" class="btn btn-info">
<i class="fas fa-pencil-alt"></i>
</button>
<button data-sch={{ schedule.schedule_id }} class="btn btn-danger del_button">
<i class="fas fa-trash" aria-hidden="true"></i>
</button>
</td>
</tr>
{% end %}
</tbody>
</table>
<hr />
</div>
<div class=" d-block d-lg-none">
<table class="table table-hover" id="mini_schedule_table" style="table-layout:fixed;" aria-describedby="Schedule List Mobile">
<thead>
<tr class="rounded">
<th style="width: 25%; min-width: 50px;">{{ translate('serverSchedules', 'action', data['lang'])
}}</th>
<th style="max-width: 40%; min-width: 50px;">{{ translate('serverSchedules', 'command',
data['lang']) }}</th>
<th style="width: 10%; min-width: 50px;">{{ translate('serverSchedules', 'enabled',
data['lang']) }}</th>
</tr>
</thead>
<tbody>
{% for schedule in data['schedules'] %}
<tr data-toggle="modal" data-target="#task_details_{{schedule.schedule_id}}">
<td id="{{schedule.action}}" class="action">
<p>{{schedule.action}}</p>
</td>
<td id="{{schedule.command}}" class="action" style="overflow: scroll; max-width: 30px;">
<p style="overflow: scroll;">{{schedule.command}}</p>
</td>
<td id="{{schedule.enabled}}" class="enabled">
<button type="button" class="btn btn-sm btn-info btn-toggle schedule-custom-toggle" id="schedule{{schedule.id}}" name="schedule{{schedule.id}}" data-schedule-id="{{schedule.schedule_id}}" data-schedule-enabled="{{ 'true' if schedule.enabled else 'false' }}" data-toggle="button" aria-pressed="true" autocomplete="off">
<div class="handle"></div>
</button>
</td>
</tr>
<!-- Modal -->
<div class="modal fade" id="task_details_{{schedule.schedule_id}}" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">{{ translate('serverSchedules', 'details',
data['lang']) }}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<ul style="list-style: none;">
<li id="{{schedule.schedule_id}}" class="id" style="border-top: .1em solid gray;">
<h4>{{ translate('serverSchedules', 'name', data['lang']) }}</h4>
<p>{{schedule.schedule_id}}</p>
</li>
<li id="{{schedule.action}}" class="action" style="border-top: .1em solid gray;">
<h4>{{ translate('serverSchedules', 'action', data['lang']) }}</h4>
<p>{{schedule.action}}</p>
</li>
<li id="{{schedule.command}}" class="action" style="border-top: .1em solid gray;">
<h4>{{ translate('serverSchedules', 'command', data['lang']) }}</h4>
<p>{{schedule.command}}</p>
</li>
<li id="{{schedule.interval}}" class="action" style="border-top: .1em solid gray;">
{% if schedule.interval != '' %}
<h4>{{ translate('serverSchedules', 'interval', data['lang']) }}</h4>
<p>{{ translate('serverSchedules', 'every', data['lang']) }} {{schedule.interval}}
{{schedule.interval_type}}</p>
{% elif schedule.interval_type == 'reaction' %}
<h4>{{ translate('serverSchedules', 'interval', data['lang']) }}</h4>
<p>{{schedule.interval_type}}<br><br>{{ translate('serverSchedules', 'child',
data['lang']) }}: {{ schedule.parent }}</p>
{% else %}
<h4>{{ translate('serverSchedules', 'interval', data['lang']) }}</h4>
<p>{{ translate('serverSchedules', 'cron', data['lang']) }}: {{schedule.cron_string}}
</p>
{% end %}
</li>
<li id="{{schedule.start_time}}" class="action" style="border-top: .1em solid gray;">
<h4>{{ translate('serverSchedules', 'nextRun', data['lang']) }}</h4>
{% if schedule.next_run %}
<p>{{schedule.next_run}}</p>
{% else %}
<p>zzzzzzz</p>
{% end %}
</li>
<li id="{{schedule.enabled}}" class="action" style="border-top: .1em solid gray; border-bottom: .1em solid gray">
<h4>{{ translate('serverSchedules', 'enabled', data['lang']) }}</h4>
<input type="checkbox" class="schedule-enabled-toggle" data-schedule-id="{{schedule.schedule_id}}" data-schedule-enabled="{{ 'true' if schedule.enabled else 'false' }}">
</li>
</ul>
</div>
<div class="modal-footer">
<button onclick="window.location.href='/panel/edit_schedule?id={{ data['server_stats']['server_id']['server_id'] }}&sch_id={{schedule.schedule_id}}'" class="btn btn-info">
<i class="fas fa-pencil-alt"></i> {{ translate('serverSchedules', 'edit', data['lang'])
}}
</button>
<button data-sch={{ schedule.schedule_id }} class="btn btn-danger del_button">
<i class="fas fa-trash" aria-hidden="true"></i> {{ translate('serverSchedules',
'delete', data['lang']) }}
</button>
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{
translate('serverSchedules', 'close', data['lang']) }}</button>
</div>
</div>
</div>
</div>
</div>
{% end %}
</tbody>
</table>
{% end %}
</tbody>
</table>
</div>
{% end %}
</div>
</div>
</div>
@ -265,6 +251,16 @@
.toggle {
height: 0px !important;
}
td.enabled {
vertical-align: middle;
border-collapse: collapse !important;
}
.enabled .toggle-group .btn {
vertical-align: middle;
cursor: pointer;
}
</style>
@ -299,20 +295,21 @@
};
}
$(() => {
$('.schedule-enabled-toggle').bootstrapToggle({
on: 'Yes',
off: 'No',
onstyle: 'success',
offstyle: 'danger',
})
$('.schedule-enabled-toggle').each(function () {
$(document).ready(function () {
$('.schedule-custom-toggle').each(function () {
const enabled = JSON.parse(this.getAttribute('data-schedule-enabled'));
$(this).bootstrapToggle(enabled ? 'on' : 'off')
if (enabled) {
this.classList.add("active");
}
else {
this.classList.remove("active");
}
})
$('.schedule-enabled-toggle').change(function () {
});
$(() => {
$('.schedule-custom-toggle').click(function () {
const id = this.getAttribute('data-schedule-id');
const enabled = this.checked;
const enabled = !JSON.parse(this.getAttribute('data-schedule-enabled'));
fetch(`/api/v2/servers/{{data['server_id']}}/tasks/${id}`, {
method: 'PATCH',
@ -426,4 +423,4 @@
</script>
{% end %}
{% end %}

View File

@ -0,0 +1,280 @@
{% extends ../base.html %}
{% block meta %}
{% end %}
{% block title %}Crafty Controller - {{ translate('serverDetails', 'serverDetails', data['lang']) }}{% end %}
{% block content %}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-select/1.13.10/css/bootstrap-select.min.css">
<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">
{{ translate('serverDetails', 'serverDetails', data['lang']) }} - {{
data['server_stats']['server_id']['server_name'] }}
<br />
<small>UUID: {{ data['server_stats']['server_id']['server_uuid'] }}</small>
</h4>
</div>
</div>
</div>
<!-- Page Title Header Ends-->
{% include "parts/details_stats.html" %}
<div class="row">
<div class="col-sm-12 grid-margin">
<div class="card">
<div class="card-body pt-0">
{% include "parts/server_controls_list.html" %}
<div class="row">
<div class="col-md-8 col-sm-8">
{% if data['new_webhook'] == True %}
<form class="forms-sample" method="post" id="new_webhook_form"
action="/panel/new_webhook?id={{ data['server_stats']['server_id']['server_id'] }}">
{% else %}
<form class="forms-sample" method="post" id="webhook_form"
action="/panel/edit_webhook?id={{ data['server_stats']['server_id']['server_id'] }}&sch_id={{ data['webhook']['id'] }}">
{% end %}
<select class="form-select form-control form-control-lg select-css" id="webhook_type" name="webhook_type">
{% if data['new_webhook'] == False %}
<option value="{{data['webhook']['webhook_type']}}">{{data['webhook']['webhook_type']}}</option>
{% end %}
{% for type in data['providers'] %}
{% if type != data['webhook']['webhook_type'] %}
<option value="{{type}}">{{type}}</option>
{%end%}
{% end %}
</select>
<br>
<br>
<div class="form-group">
<label for="name">{{ translate('webhooks', 'name' , data['lang']) }}</label>
<input type="input" class="form-control" name="name" id="name_input"
value="{{ data['webhook']['name']}}" maxlength="30" placeholder="Name" required>
</div>
<div class="form-group">
<label for="url">{{ translate('webhooks', 'url', data['lang']) }}</label>
<input type="input" class="form-control" name="url" id="url"
value="{{ data['webhook']['url']}}" placeholder="https://webhooks.craftycontrol.com/fakeurl" required>
</div>
<div class="form-group">
<label for="bot_name">{{ translate('webhooks', 'bot_name' , data['lang']) }}</label>
<input type="input" class="form-control" name="bot_name" id="bot_name_input"
value="{{ data['webhook']['bot_name']}}" maxlength="30" placeholder="Crafty Controller" required>
</div>
<div class="form-group">
<label for="trigger">{{ translate('webhooks', 'trigger', data['lang']) }}</label>
<select class="form-control selectpicker show-tick" name="trigger" id="trigger-select" data-icon-base="fas" data-tick-icon="fa-check" multiple data-style="custom-picker">
{% for trigger in data['triggers'] %}
{% if trigger in data["webhook"]["trigger"] %}
<option value="{{trigger}}" selected>{{translate('webhooks', trigger , data['lang'])}}</option>
{% else %}
<option value="{{trigger}}">{{translate('webhooks', trigger , data['lang'])}}</option>
{% end %}
{% end %}
</select>
</div>
<div class="form-group">
<label for="body">{{ translate('webhooks', 'webhook_body', data['lang']) }}</label>
<textarea id="body-input" name="body" rows="4" cols="50">
{{ data["webhook"]["body"] }}
</textarea>
</div>
<div class="form-group">
<label for="bot_name">{{ translate('webhooks', 'color' , data['lang']) }}</label>
<input type="color" class="form-control" name="color" id="color" value='{{data["webhook"]["color"]}}'>
</div>
<div class="form-group">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="enabled" name="enabled" {% if data['webhook']['enabled'] %}checked{%end%}
value="1">
<label for="enabled" class="custom-control-label">{{ translate('webhooks', 'enabled', data['lang']) }}</label>
</div>
</div>
<button type="submit" class="btn btn-success mr-2"><i class="fas fa-save"></i> {{
translate('serverConfig', 'save', data['lang']) }}</button>
<button type="reset"
onclick="location.href=`/panel/server_detail?id={{ data['server_stats']['server_id']['server_id'] }}&subpage=webhooks`"
class="btn btn-light"><i class="fas fa-times"></i> {{ translate('serverConfig', 'cancel',
data['lang']) }}</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
.custom-control-input:checked~.custom-control-label::before {
color: black !important;
background-color: blueviolet !important;
border-color: var(--outline) !important;
}
.custom-control-label::before {
background-color: white !important;
top: calc(-0.2rem);
}
.custom-switch .custom-control-label::after {
top: calc(-0.125rem + 1px);
}
#body-input {
background-color: var(--card-banner-bg);
outline-color: var(--outline);
color: var(--base-text);
width: 100%;
}
</style>
<!-- content-wrapper ends -->
{% end %}
{% block js %}
<script>
$(function () {
$('.form-check-input').bootstrapToggle({
on: '',
off: ''
});
})
//used to get cookies from browser - this is part of tornados xsrf protection - it's for extra security
function getCookie(name) {
var r = document.cookie.match("\\b" + name + "=([^;]*)\\b");
return r ? r[1] : undefined;
}
function replacer(key, value) {
if (key != "start_time" && key != "cron_string" && key != "interval_type") {
if (typeof value == "boolean") {
return value
}
console.log(key)
if (key === "interval" && value === ""){
return 0;
}
if (key === "command" && typeof(value === "integer")){
return value.toString();
}else {
return (isNaN(value) ? value : +value);
}
} else {
if (value === "" && key == "start_time"){
return "00:00";
}else{
return value;
}
}
}
const serverId = new URLSearchParams(document.location.search).get('id');
const webhookId = new URLSearchParams(document.location.search).get('webhook_id');
$(document).ready(function () {
console.log("ready!");
console.log('ready for JS!');
$('.selectpicker').selectpicker("refresh");
$("#new_webhook_form").on("submit", async function (e) {
e.preventDefault();
var token = getCookie("_xsrf")
let webhookForm = document.getElementById("new_webhook_form");
let select_val = JSON.stringify($('#trigger-select').val());
select_val = JSON.parse(select_val);
let formData = new FormData(webhookForm);
//Create an object from the form data entries
let formDataObject = Object.fromEntries(formData.entries());
//We need to make sure these are sent regardless of whether or not they're checked
formDataObject.enabled = $("#enabled").prop('checked');
formDataObject.trigger = select_val;
console.log(formDataObject);
// Format the plain form data as JSON
let formDataJsonString = JSON.stringify(formDataObject, replacer);
let res = await fetch(`/api/v2/servers/${serverId}/webhook/`, {
method: 'POST',
headers: {
'X-XSRFToken': token,
"Content-Type": "application/json",
},
body: formDataJsonString,
});
let responseData = await res.json();
if (responseData.status === "ok") {
window.location.href = `/panel/server_detail?id=${serverId}&subpage=webhooks`;
} else {
bootbox.alert({
title: responseData.status,
message: responseData.error
});
}
});
$("#webhook_form").on("submit", async function (e) {
e.preventDefault();
var token = getCookie("_xsrf");
let webhookForm = document.getElementById("webhook_form");
let select_val = JSON.stringify($('#trigger-select').val());
select_val = JSON.parse(select_val);
let formData = new FormData(webhookForm);
//Create an object from the form data entries
let formDataObject = Object.fromEntries(formData.entries());
//We need to make sure these are sent regardless of whether or not they're checked
formDataObject.enabled = $("#enabled").prop('checked');
formDataObject.trigger = select_val;
if(formDataObject.webhook_type != "Discord"){
delete formDataObject.color
}
console.log(formDataObject);
// Format the plain form data as JSON
let formDataJsonString = JSON.stringify(formDataObject, replacer);
let res = await fetch(`/api/v2/servers/${serverId}/webhook/${webhookId}`, {
method: 'PATCH',
headers: {
'X-XSRFToken': token,
"Content-Type": "application/json",
},
body: formDataJsonString,
});
let responseData = await res.json();
if (responseData.status === "ok") {
window.location.href = `/panel/server_detail?id=${serverId}&subpage=webhooks`;
} else {
bootbox.alert({
title: responseData.status,
message: responseData.error
});
}
});
});
function hexToDiscordInt(hexColor) {
// Remove the hash at the start if it's there
const sanitizedHex = hexColor.startsWith('#') ? hexColor.slice(1) : hexColor;
// Convert the hex to an integer
return parseInt(sanitizedHex, 16);
}
</script>
<script src="../../static/assets/vendors/js/bootstrap-select.min.js"></script>
{% end %}

View File

@ -0,0 +1,390 @@
{% extends ../base.html %}
{% block meta %}
{% end %}
{% block title %}Crafty Controller - {{ translate('serverDetails', 'serverDetails', data['lang']) }}{% 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">
{{ translate('serverDetails', 'serverDetails', data['lang']) }} - {{
data['server_stats']['server_id']['server_name'] }}
<br />
<small>UUID: {{ data['server_stats']['server_id']['server_uuid'] }}</small>
</h4>
</div>
</div>
</div>
<!-- Page Title Header Ends-->
{% include "parts/details_stats.html" %}
<div class="row">
<div class="col-sm-12 grid-margin">
<div class="card">
<div class="card-body pt-0">
<span class="d-none d-sm-block">
{% include "parts/server_controls_list.html" %}
</span>
<span class="d-block d-sm-none">
{% include "parts/m_server_controls_list.html" %}
</span>
<div class="row">
<div class="col-md-12 col-sm-12" style="overflow-x:auto;">
<div class="card">
<div class="card-header header-sm d-flex justify-content-between align-items-center">
<h4 class="card-title"><i class="fa-regular fa-bell"></i> {{ translate('webhooks', 'webhooks', data['lang']) }} </h4>
{% if data['user_data']['hints'] %}
<span class="too_small" title="{{ translate('serverSchedules', 'cannotSee', data['lang']) }}" , data-content="{{ translate('serverSchedules', 'cannotSeeOnMobile', data['lang']) }}" , data-placement="bottom"></span>
{% end %}
<div><a class="btn btn-info" href="/panel/add_webhook?id={{ data['server_stats']['server_id']['server_id'] }}"><i class="fas fa-plus-circle"></i> {{ translate('webhooks', 'new', data['lang']) }}</a></div>
</div>
<div class="card-body">
{% if len(data['webhooks']) == 0 %}
<div style="text-align: center; color: grey;">
<h7>{{ translate('webhooks', 'no-webhook', data['lang']) }} <strong>{{ translate('webhooks', 'newWebhook',data['lang']) }}</strong>.</h7>
</div>
{% end %}
{% if len(data['webhooks']) > 0 %}
<div class="d-none d-lg-block">
<table class="table table-hover responsive-table" aria-label="webhooks list" id="webhook_table" width="100%" style="table-layout:fixed;">
<thead>
<tr class="rounded">
<th scope="col" style="width: 5%; min-width: 64px;">{{ translate('webhooks', 'enabled', data['lang']) }}</th>
<th scope="col" style="width: 15%; min-width: 10px;">{{ translate('webhooks', 'name', data['lang']) }} </th>
<th scope="col" style="width: 20%; min-width: 50px;">{{ translate('webhooks', 'type', data['lang']) }}</th>
<th scope="col" style="width: 50%; min-width: 50px;">{{ translate('webhooks', 'trigger', data['lang']) }}</th>
<th scope="col" style="width: 10%; min-width: 50px;">{{ translate('webhooks', 'edit', data['lang']) }}</th>
</tr>
</thead>
<tbody>
{% for webhook in data['webhooks'] %}
<tr>
<td id="{{webhook.enabled}}" class="enabled">
<button type="button" class="btn btn-sm btn-info btn-toggle webhook-custom-toggle" id="webhook_{{webhook.id}}" name="webhook_{{webhook.id}}" data-webhook-id="{{webhook.id}}" data-webhook-enabled="{{ 'true' if webhook.enabled else 'false' }}" data-toggle="button" aria-pressed="true" autocomplete="off">
<div class="handle"></div>
</button>
</td>
<td id="{{webhook.name}}" class="id">
<p>{{webhook.name}}</p>
</td>
<td id="{{webhook.webhook_type}}" class="type">
<p>{{webhook.webhook_type}}</p>
</td>
<td id="{{webhook.trigger}}" class="trigger" style="overflow: scroll; max-width: 30px;">
<ul>
{% for trigger in webhook.trigger.split(",") %}
{% if trigger in data["triggers"] %}
<li>{{translate('webhooks', trigger , data['lang'])}}</li>
{%end%}
{%end%}
</ul>
</td>
<td id="webhook_edit" class="action">
<button onclick="window.location.href=`/panel/webhook_edit?id={{ data['server_stats']['server_id']['server_id'] }}&webhook_id={{webhook.id}}`" class="btn btn-info">
<i class="fas fa-pencil-alt"></i>
</button>
<button data-webhook={{ webhook.id }} class="btn btn-danger del_button">
<i class="fas fa-trash" aria-hidden="true"></i>
</button>
<button data-webhook={{ webhook.id }} data-toggle="tooltip" title="{{ translate('webhooks', 'run', data['lang']) }}" class="btn btn-outline-warning test-socket">
<i class="fa-solid fa-vial"></i>
</button>
</td>
</tr>
{% end %}
</tbody>
</table>
</div>
<div class="d-block d-lg-none">
<table aria-label="webhooks list" class="table table-hover responsive-table" id="webhook_table_mini" width="100%" style="table-layout:fixed;">
<thead>
<tr class="rounded">
<th style="width: 20%; min-width: 64px;">{{ translate('webhooks', 'enabled',
data['lang']) }}</th>
<th style="width: 40%; min-width: 10px;">Name
</th>
<th style="width: 40%; min-width: 50px;">{{ translate('webhooks', 'edit', data['lang'])
}}</th>
</tr>
</thead>
<tbody>
{% for webhook in data['webhooks'] %}
<tr>
<td id="{{webhook.enabled}}" class="enabled">
<button type="button" class="btn btn-sm btn-info btn-toggle webhook-custom-toggle" id="webhook_{{webhook.id}}" name="webhook_{{webhook.id}}" data-webhook-id="{{webhook.id}}" data-webhook-enabled="{{ 'true' if webhook.enabled else 'false' }}" data-toggle="button" aria-pressed="true" autocomplete="off">
<div class="handle"></div>
</button>
</td>
<td id="{{webhook.name}}" class="id">
<p>{{webhook.name}}</p>
</td>
<td id="webhook_edit" class="action">
<button onclick="window.location.href=`/panel/webhook_edit?id={{ data['server_stats']['server_id']['server_id'] }}&webhook_id={{webhook.id}}`" class="btn btn-info">
<i class="fas fa-pencil-alt"></i>
</button>
<button data-webhook={{ webhook.id }} class="btn btn-danger del_button">
<i class="fas fa-trash" aria-hidden="true"></i>
</button>
<button data-webhook={{ webhook.id }} data-toggle="tooltip" title="{{ translate('webhooks', 'run', data['lang']) }}" class="btn btn-outline-warning test-socket">
<i class="fa-solid fa-vial"></i>
</button>
</td>
</tr>
{% end %}
</tbody>
</table>
</div>
{% end %}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
.popover-body {
color: white !important;
;
}
.toggle-handle {
background-color: white !important;
}
.toggle-on {
color: black !important;
}
.toggle {
height: 0px !important;
}
td.enabled {
vertical-align: middle;
border-collapse: collapse !important;
}
.enabled .toggle-group .btn {
vertical-align: middle;
cursor: pointer;
}
</style>
</div>
<style>
/* Hide scrollbar for Chrome, Safari and Opera */
td::-webkit-scrollbar {
display: none;
}
/* Hide scrollbar for IE, Edge and Firefox */
td {
-ms-overflow-style: none;
/* IE and Edge */
scrollbar-width: none;
/* Firefox */
}
</style>
<!-- content-wrapper ends -->
{% end %}
{% block js %}
<script>
$(document).ready(function () {
console.log('ready for JS!')
$('#webhook_table').DataTable({
'order': [4, 'asc'],
}
);
$('#webhook_table_mini').DataTable({
'order': [2, 'asc']
}
);
document.getElementById('webhook_table_mini_wrapper').hidden = true;
});
$(document).ready(function () {
$('[data-toggle="popover"]').popover();
if ($(window).width() < 1000) {
$('.too_small').popover("show");
document.getElementById('webhook_table_wrapper').hidden = true;
document.getElementById('webhook_table_mini_wrapper').hidden = false;
}
});
$(window).ready(function () {
$('body').click(function () {
$('.too_small').popover("hide");
});
});
$(window).resize(function () {
// This will execute whenever the window is resized
if ($(window).width() < 1000) {
$('.too_small').popover("show");
document.getElementById('webhook_table_wrapper').hidden = true;
document.getElementById('webhook_table_mini_wrapper').hidden = false;
}
else {
$('.too_small').popover("hide");
document.getElementById('webhook_table_wrapper').hidden = false;
document.getElementById('webhook_table_mini_wrapper').hidden = true;
} // New width
});
function debounce(func, timeout = 300) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => { func.apply(this, args); }, timeout);
};
}
$(document).ready(function () {
$('.webhook-custom-toggle').each(function () {
const enabled = JSON.parse(this.getAttribute('data-webhook-enabled'));
if (enabled) {
this.classList.add("active");
}
else {
this.classList.remove("active");
}
})
});
$(() => {
$('.webhook-custom-toggle').click(function () {
const id = this.getAttribute('data-webhook-id');
const enabled = !JSON.parse(this.getAttribute('data-webhook-enabled'));
fetch(`/api/v2/servers/{{data['server_id']}}/webhook/${id}`, {
method: 'PATCH',
body: JSON.stringify({ enabled }),
headers: {
'Content-Type': 'application/json',
},
})
});
})
const serverId = new URLSearchParams(document.location.search).get('id')
</script>
<script>
//used to get cookies from browser - this is part of tornados xsrf protection - it's for extra security
function getCookie(name) {
var r = document.cookie.match("\\b" + name + "=([^;]*)\\b");
return r ? r[1] : undefined;
}
$(document).ready(function () {
console.log("ready!");
});
$(".del_button").click(function () {
var webhook_id = $(this).data('webhook');
bootbox.confirm({
message: "{{ translate('webhooks', 'areYouSureDel', data['lang']) }}",
buttons: {
cancel: {
label: '<i class="fas fa-times"></i> {{ translate("serverSchedules", "cancel", data["lang"]) }}'
},
confirm: {
className: 'btn-outline-danger',
label: '<i class="fas fa-check"></i> {{ translate("serverSchedules", "confirm", data["lang"]) }}'
}
},
callback: function (result) {
console.log(result);
if (result == true) {
del_hook(webhook_id, serverId);
}
}
});
});
$(".test-socket").click(function () {
var webhook_id = $(this).data('webhook');
bootbox.confirm({
message: "{{ translate('webhooks', 'areYouSureRun', data['lang']) }}",
buttons: {
cancel: {
label: '<i class="fas fa-times"></i> {{ translate("serverSchedules", "cancel", data["lang"]) }}'
},
confirm: {
className: 'btn-outline-danger',
label: '<i class="fas fa-check"></i> {{ translate("serverSchedules", "confirm", data["lang"]) }}'
}
},
callback: function (result) {
console.log(result);
if (result == true) {
test_hook(webhook_id, serverId);
}
}
});
});
async function test_hook(webhook_id, id) {
var token = getCookie("_xsrf")
let res = await fetch(`/api/v2/servers/${id}/webhook/${webhook_id}/`, {
method: 'POST',
headers: {
'token': token,
},
});
let responseData = await res.json();
if (responseData.status === "ok") {
bootbox.alert("Webhook Sent!")
} else {
console.log(responseData);
bootbox.alert({
title: responseData.status,
message: responseData.error
});
}
}
async function del_hook(webhook_id, id) {
var token = getCookie("_xsrf")
let res = await fetch(`/api/v2/servers/${id}/webhook/${webhook_id}`, {
method: 'DELETE',
headers: {
'token': token,
},
});
let responseData = await res.json();
if (responseData.status === "ok") {
window.location.reload();
} else {
bootbox.alert({
title: responseData.status,
message: responseData.error
});
}
}
</script>
{% end %}

View File

@ -30,9 +30,6 @@
</tr>
</thead>
<tbody>
{% if data['running'] != 0 %}
<span id="sync" style="margin-left: 5px;"><i class="fas fa-sync fa-spin"></i></span></h4>
{% end %}
{% for server in data['servers'] %}
{% if server['server_data']['show_status'] %}
<tr>
@ -47,10 +44,14 @@
</td>
<td id="server_motd_{{ server['stats']['server_id']['server_id'] }}">
{% if server['stats']['desc'] != 'False' %}
<img src="/static/assets/images/pack.png" alt="icon"
style="-webkit-filter:grayscale(100%); filter:grayscale(100%)" />
<span id="input_motd_{{ server['stats']['server_id']['server_id'] }}" class="input_motd">{{
server['stats']['desc'] }}</span> <br />
<div class="row">
<div class="col-auto">
<img src="/static/assets/images/pack.png" alt="icon" style="-webkit-filter:grayscale(100%); filter:grayscale(100%)" />
</div>
<div class="col-auto">
<span id="input_motd_{{ server['stats']['server_id']['server_id'] }}" class="input_motd">{{ server['stats']['desc'] }}</span>
</div>
</div>
{% end %}
</td>
<td id="server_version_{{ server['stats']['server_id']['server_id'] }}">
@ -89,12 +90,9 @@
</div>
<!-- View for Small screen -->
<div class="row justify-content-center align-items-sm-center">
<div class="content-wrapper login-modal d-sm-none d-block" style="background-color: var(--dropdown-bg);">
<div class="content-wrapper login-modal d-sm-none d-block" style="background-color: var(--dropdown-bg);">
<img src="/static/assets/images/logo_long.png" style='width: 100%;'>
<hr />
{% if data['running'] != 0 %}
<span id="m_sync" style="margin-left: 5px;"><i class="fas fa-sync fa-spin"></i></span></h4>
{% end %}
<div class="accordion" id="accordionServers">
{% for server in data['servers'] %}
{% if server['server_data']['show_status'] %}
@ -103,17 +101,13 @@
<h2 class="mb-0 container overflow-hidden">
<div class="row">
<div class="col-8 mx-0 px-0">
<a id="m_server_name_{{ server['stats']['server_id']['server_id'] }}"
class="btn btn-link d-flex justify-content-center" type="button" data-toggle="collapse"
data-target="#collapse-{{server['server_data']['server_id']}}" aria-expanded="false"
aria-controls="collapse-{{server['server_data']['server_id']}}">
<a id="m_server_name_{{ server['stats']['server_id']['server_id'] }}" class="btn btn-link d-flex justify-content-center" type="button" data-toggle="collapse" data-target="#collapse-{{server['server_data']['server_id']}}" aria-expanded="false" aria-controls="collapse-{{server['server_data']['server_id']}}">
<i class="fas fa-server"></i>
{{ server['server_data']['server_name'] }}
</a>
</div>
<div class="col-4 mx-0 px-0">
<a id="m_server_online_status_{{ server['stats']['server_id']['server_id'] }}"
class="btn btn-link d-flex justify-content-center" type="button">
<a id="m_server_online_status_{{ server['stats']['server_id']['server_id'] }}" class="btn btn-link d-flex justify-content-center" type="button">
{% if server['stats']['running'] %}
<div id="m_server_players_{{ server['stats']['server_id']['server_id'] }}">
<span class="text-success"><i class="fas fa-signal"></i> {{ server['stats']['online'] }} / {{
@ -129,14 +123,12 @@
</h2>
</div>
<div id="collapse-{{server['server_data']['server_id']}}" class="collapse"
aria-labelledby="heading-{{server['server_data']['server_id']}}" data-parent="#accordionServers">
<div id="collapse-{{server['server_data']['server_id']}}" class="collapse" aria-labelledby="heading-{{server['server_data']['server_id']}}" data-parent="#accordionServers">
<div class="card-body">
{% if server['stats']['int_ping_results'] != 'False' %}
<div id="m_server_motd_{{ server['stats']['server_id']['server_id'] }}" class="media">
{% if server['stats']['desc'] != 'False' %}
<img src="/static/assets/images/pack.png" class="w-25 mr-3" alt="icon"
style="-webkit-filter:grayscale(100%); filter:grayscale(100%);" />
<img src="/static/assets/images/pack.png" class="w-25 mr-3" alt="icon" style="-webkit-filter:grayscale(100%); filter:grayscale(100%);" />
{% end %}
<div class="media-body">
{% if server['stats']['desc'] != 'False' %}
@ -198,11 +190,9 @@
m_server_online_status = document.getElementById('m_server_online_status_' + server.id);
/* TODO Update each element */
if (server.int_ping_results) {
document.getElementById('sync').innerHTML = '';
document.getElementById('m_sync').innerHTML = '';
if (server.running) {
/* Update Players */
if (server.players) {
if (server.max != 0) {
server_players.innerHTML = server.online + ` / ` + server.max + ` {{ translate('dashboard', 'max', data['lang']) }}<br />`
}
@ -210,16 +200,18 @@
var motd = "";
if (server.desc) {
if (server.icon) {
motd = `<img src="data:image/png;base64,` + server.icon + `" alt="icon" /> `;
img_motd = `<img src="data:image/png;base64,` + server.icon + `" alt="icon" /> `;
m_motd = `<img src="data:image/png;base64,` + server.icon + `" alt="icon" /> `;
}
else {
motd = `<img src="/static/assets/images/pack.png" alt="icon" /> `;
img_motd = `<img src="/static/assets/images/pack.png" alt="icon" /> `;
m_motd = `<img class="w-25 mr-3" src="/static/assets/images/pack.png" alt="icon" /> `;
}
motd = motd + `<span id="input_motd_` + server.id + `" class="input_motd">` + server.desc + `</span> <br />`;
desc_motd = `<span id="input_motd_` + server.id + `" class="input_motd">` + server.desc + `</span> <br />`;
m_motd = m_motd + `<div class="media-body"><span id="m_input_motd_` + server.id + `" class="input_motd">` + server.desc + `</span></div>`;
motd = `<div class="row"><div class="col-auto">` + img_motd + `</div><div class="col-auto">` + desc_motd + `</div></div>`;
server_motd.innerHTML = motd;
m_server_motd.innerHTML = m_motd;
}
@ -252,17 +244,31 @@
}
function update_servers_status(data) {
console.log(data);
update_one_server_status(data[0]);
console.log("update servers");
data.forEach(server => {
console.log(server);
update_one_server_status(server);
});
display_motd();
}
function refreshStatus() {
let xmlHttp = new XMLHttpRequest();
xmlHttp.onreadystatechange = function () {
if (this.readyState == 4 && this.status == 200) {
var myData = JSON.parse(this.responseText);
update_servers_status(myData.data);
}
};
xmlHttp.open('GET', '/api/v2/servers/status', true);
xmlHttp.send();
setTimeout(refreshStatus, 30000);
}
$(document).ready(function () {
console.log("ready!");
if (webSocket) {
webSocket.on('update_server_status', update_servers_status);
}
refreshStatus();
}());
</script>

View File

@ -1,167 +1,80 @@
<!DOCTYPE html>
<html lang="{{ data['lang_page'] }}" class="default">
<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/fontawesome6/css/all.css"
/>
<link rel="manifest" href="/static/assets/crafty.webmanifest" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<meta name="apple-mobile-web-app-title" content="Crafty" />
<link
rel="apple-touch-icon"
href="../static/assets/images/Crafty_4-0.png"
/>
<!-- 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"
type="image/svg+xml"
href="/static/assets/images/logo_small.svg"
/>
<link rel="alternate icon" href="/static/assets/images/favicon.png" />
</head>
<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/fontawesome6/css/all.css" />
<link rel="manifest" href="/static/assets/crafty.webmanifest" />
<body>
<div class="container-scroller">
<div class="container-fluid page-body-wrapper full-page-wrapper">
<div
class="content-wrapper d-flex align-items-sm-center auth auth-bg-1 theme-one"
>
<div class="mx-auto">
<div class="auto-form-wrapper">{% block content %} {% end %}</div>
</div>
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<meta name="apple-mobile-web-app-title" content="Crafty" />
<link rel="apple-touch-icon" href="../static/assets/images/Crafty_4-0.png" />
<!-- 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" type="image/svg+xml" href="/static/assets/images/logo_small.svg" />
<link rel="alternate icon" href="/static/assets/images/favicon.png" />
</head>
<body>
<div class="container-scroller">
<div class="container-fluid page-body-wrapper full-page-wrapper">
<div class="content-wrapper d-flex align-items-sm-center auth auth-bg-1 theme-one">
<div class="mx-auto">
<div class="auto-form-wrapper">{% block content %} {% end %}</div>
</div>
<!-- content-wrapper ends -->
</div>
<!-- page-body-wrapper ends -->
<!-- content-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>
<!-- endinject -->
<script>
// {% if request.protocol == 'https' %}
let usingWebSockets = true;
<!-- page-body-wrapper ends -->
</div>
<!-- page-body-wrapper ends -->
let listenEvents = [];
<!-- 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>
<!-- endinject -->
let pageQueryParams, page;
try {
pageQueryParams =
"page_query_params=" + encodeURIComponent(location.search);
page = "page=" + encodeURIComponent(location.pathname);
var wsInternal = new WebSocket(
"wss://" + location.host + "/ws?" + page + "&" + pageQueryParams
);
wsInternal.onopen = function () {
console.log("opened WebSocket connection:", wsInternal);
};
wsInternal.onmessage = function (rawMessage) {
var message = JSON.parse(rawMessage.data);
console.log("got message: ", message);
listenEvents
.filter((listenedEvent) => listenedEvent.event == message.event)
.forEach((listenedEvent) => listenedEvent.callback(message.data));
};
wsInternal.onerror = function (errorEvent) {
console.error("WebSocket Error", errorEvent);
};
wsInternal.onclose = function (closeEvent) {
console.log("Closed WebSocket", closeEvent);
};
webSocket = {
on: function (event, callback) {
console.log("registered " + event + " event");
listenEvents.push({ event: event, callback: callback });
},
emit: function (event, data) {
var message = {
event: event,
data: data,
};
wsInternal.send(JSON.stringify(message));
},
};
} catch (error) {
console.error("Error while making websocket helpers", error);
usingWebSockets = false;
{% block js %}
<!-- Custom js for this page -->
<script>
$(document).ready(function () {
let login_opacity_div = document.getElementById('login_opacity');
let opacity = login_opacity_div.getAttribute('data-value');
document.getElementById('login-form-background').style.background = 'rgb(34, 36, 55, ' + (opacity / 100) + ')';
//Register Service worker for mobile app
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/static/assets/js/shared/service-worker.js', { scope: '/' })
.then(function (registration) {
console.error('Service Worker Registered');
});
}
// {% else %}
usingWebSockets = false;
warn(
"WebSockets are not supported in Crafty if not using the https protocol"
);
var webSocket;
// {% end%}
</script>
{% block js %}
<!-- Custom js for this page -->
<script>
$(document).ready(function () {
let login_opacity_div = document.getElementById("login_opacity");
let opacity = login_opacity_div.getAttribute("data-value");
document.getElementById("login-form-background").style.background =
"rgb(34, 36, 55, " + opacity / 100 + ")";
//Register Service worker for mobile app
if ("serviceWorker" in navigator) {
navigator.serviceWorker
.register("/static/assets/js/shared/service-worker.js", {
scope: "/",
})
.then(function (registration) {
console.log("Service Worker Registered");
});
}
});
</script>
<!-- End custom js for this page -->
{% end %}
</body>
</html>
});
</script>
<!-- End custom js for this page -->
{% end %}
</body>
</html>

View File

@ -5,7 +5,7 @@
{% block content %}
<div class="content-wrapper">
<ul class="nav nav-tabs col-md-12 tab-simple-styled " role="tablist">
<ul class="nav nav-pills tab-simple-styled " role="tablist">
<li class="nav-item term-nav-item">
<a class="nav-link" href="/server/step1" role="tab" aria-selected="false">
<i class="fas fa-file-signature"></i>Minecraft-Java</a>
@ -610,7 +610,9 @@
message: '<p class="text-center mb-0"><i class="fa fa-spin fa-cog"></i> Please wait while we gather your files...</p>',
closeButton: false
});
getDirView();
setTimeout(function(){
getDirView();
}, 2000);
} else {
bootbox.alert("You must input a path before selecting this button");
}
@ -664,7 +666,9 @@
message: '<p class="text-center mb-0"><i class="fa fa-spin fa-cog"></i> Please wait while we gather your files...</p>',
closeButton: false
});
getDirView();
setTimeout(function(){
getDirView();
}, 2000);
} else {
bootbox.alert("You must input a path before selecting this button");
}

View File

@ -5,7 +5,7 @@
{% block content %}
<div class="content-wrapper">
<ul class="nav nav-tabs col-md-12 tab-simple-styled " role="tablist">
<ul class="nav nav-pills tab-simple-styled">
<li class="nav-item term-nav-item">
<a class="nav-link active" href="/server/step1" role="tab" aria-selected="false">
<i class="fas fa-file-signature"></i>Minecraft-Java</a>
@ -846,8 +846,9 @@
message: '<p class="text-center mb-0"><i class="fa fa-spin fa-cog"></i> Please wait while we gather your files...</p>',
closeButton: false
});
console.log("CALLING DIR")
getDirView();
setTimeout(function(){
getDirView();
}, 2000);
} else {
bootbox.alert("You must input a path before selecting this button");
}
@ -863,8 +864,9 @@
message: '<p class="text-center mb-0"><i class="fa fa-spin fa-cog"></i> Please wait while we gather your files...</p>',
closeButton: false
});
console.log("CALLING DIR")
getDirView();
setTimeout(function(){
getDirView();
}, 2000);
});
var upload = false;
var file;
@ -1180,7 +1182,7 @@
async function refreshCache() {
document.getElementById("refresh-cache").classList.add("fa-spin")
let token = getCookie("_xsrf")
let res = await fetch(`/api/v2/crafty/exeCache`, {
let res = await fetch(`/api/v2/crafty/JarCache`, {
method: 'GET',
headers: {
'X-XSRFToken': token

View File

@ -0,0 +1,27 @@
# Generated by database migrator
import peewee
def migrate(migrator, database, **kwargs):
migrator.drop_columns("webhooks", ["name", "method", "url", "event", "send_data"])
migrator.add_columns(
"webhooks",
server_id=peewee.IntegerField(null=True),
webhook_type=peewee.CharField(default="Custom"),
name=peewee.CharField(default="Custom Webhook", max_length=64),
url=peewee.CharField(default=""),
bot_name=peewee.CharField(default="Crafty Controller"),
trigger=peewee.CharField(default="server_start,server_stop"),
body=peewee.CharField(default=""),
color=peewee.CharField(default=""),
enabled=peewee.BooleanField(default=True),
)
"""
Write your migrations here.
"""
def rollback(migrator, database, **kwargs):
"""
Write your rollback migrations here.
"""

View File

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

Some files were not shown because too many files have changed in this diff Show More