Merge branch 'dev' into 'master'

v4.6.1

See merge request crafty-controller/crafty-4!920
This commit is contained in:
Iain Powrie
2025-11-23 16:42:50 +00:00
90 changed files with 691 additions and 339 deletions

View File

@@ -103,5 +103,5 @@ docker-build:
- docker context rm tls-environment-$CI_JOB_ID || true
- echo "Please review multi-arch manifests are present:"
- if [ "$ENVIRONMENT_NAME" = "development" ]; then docker buildx imagetools inspect "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG"; fi
- if [ "$ENVIRONMENT_NAME" = "production" ]; then docker buildx imagetools inspect "$CI_REGISTRY_IMAGE:$VERSION"; fi
- if [ "$ENVIRONMENT_NAME" = "production" ]; then docker buildx imagetools inspect "$CI_REGISTRY_IMAGE:latest"; fi
- if [ "$ENVIRONMENT_NAME" = "nightly" ]; then docker buildx imagetools inspect "$CI_REGISTRY_IMAGE:nightly"; fi

View File

@@ -1,6 +1,7 @@
## Quick Information
- **Operating System:** Windows / Linux / MacOS / UnRAID
- **Install Type:** Git Cloned(Manual) / Installer / WinPackage / Docker
- **Crafty Version:** v4.x.x
## What Happened?
<!-- A brief description of what happened when you tried to perform an action -->

View File

@@ -52,7 +52,7 @@ persistent=yes
# Minimum Python version to use for version dependent checks. Will default to
# the version used to run pylint.
py-version=3.9
py-version=3.10
# When enabled, pylint would attempt to guess common misconfiguration and emit
# user-friendly hints instead of false-positive error messages.

View File

@@ -1,4 +1,21 @@
# Changelog
## --- [4.6.1] - 2025/11/23
### New features
- Jinja2 Dynamic Variables for Webhook Notifications ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/757))
### Bug fixes
- Change hour and minute intervals in APScheudler to fix incorrect triggers ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/910))
- Use asyncio locks to limit upload handler race condition ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/907))
- Fix static fonts not working on some browsers ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/906))
- Fix import directory cleanup was not pointing to the proper directory ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/918))
- Fix survey not appearing on first login ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/917))
- Fix failue deleting server's DB files on server delete ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/916))
- Fix server.properties overwritten in bedrock update ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/915) | [Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/919))
- Fix zip backup download button always downloading the most recent, not the selected backup ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/913))
- Fix download button showing for snapshot backups ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/913))
### Tweaks
- Provide better feedback on restore failures ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/914))
<br><br>
## --- [4.5.5] - 2025/10/14
### Bug fixes
- Fix MFA login failure when the totp `dict`'s attempted codes list changes size while being processed ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/899))

View File

@@ -1,5 +1,5 @@
[![Crafty Logo](app/frontend/static/assets/images/logo_long.svg)](https://craftycontrol.com)
# Crafty Controller 4.5.5
# Crafty Controller 4.6.1
> Python based Control Panel for your Minecraft Server
## What is Crafty Controller?
@@ -66,8 +66,6 @@ The image is located at: `registry.gitlab.com/crafty-controller/crafty-4:latest`
$ vim docker-compose.yml
```
```yml
version: '3'
services:
crafty:
container_name: crafty_container

View File

@@ -177,7 +177,9 @@ class ServersController(metaclass=Singleton):
server_id,
)
self.file_helper.del_dirs(
self.helper.root_dir, "app", "config", "db", "servers", str(server_id)
pathlib.Path(
self.helper.root_dir, "app", "config", "db", "servers", str(server_id)
)
)
@staticmethod

View File

@@ -126,6 +126,7 @@ class FileHelpers:
@staticmethod
def del_dirs(path):
path = pathlib.Path(path)
clean = True
for sub in path.iterdir():
if sub.is_dir():
# Delete folder if it is a folder
@@ -135,26 +136,29 @@ class FileHelpers:
try:
sub.unlink()
except Exception as e:
clean = False
logger.error(f"Unable to delete file {sub}: {e}")
try:
# This removes the top-level folder:
path.rmdir()
except Exception as e:
except Exception:
logger.error("Unable to remove top level")
return e
return True
return False
return clean
@staticmethod
def del_file(path):
path = pathlib.Path(path)
clean = True
try:
logger.debug(f"Deleting file: {path}")
# Remove the file
os.remove(path)
return True
except (FileNotFoundError, PermissionError) as e:
return clean
except (FileNotFoundError, PermissionError):
logger.error(f"Path specified is not a file or does not exist. {path}")
return e
clean = False
return clean
def check_mime_types(self, file_path):
m_type, _value = self.mime_types.guess_type(file_path)

View File

@@ -69,6 +69,7 @@ MASTER_CONFIG = {
"max_login_attempts": 3,
"superMFA": False,
"general_user_log_access": False,
"base_url": "127.0.0.1:8443",
}
CONFIG_CATEGORIES = {
@@ -80,6 +81,7 @@ CONFIG_CATEGORIES = {
"disabled_language_files",
"big_bucket_repo",
"enable_user_self_delete",
"base_url",
],
"security": [
"allow_nsfw_profile_pictures",

View File

@@ -27,6 +27,8 @@ logger = logging.getLogger(__name__)
class BackupManager:
SNAPSHOT_BACKUP_DATE_FORMAT_STRING = "%Y-%m-%d-%H-%M-%S"
SNAPSHOT_SUFFIX = ".manifest"
ARCHIVE_SUFFIX = ".zip"
def __init__(self, helper, file_helper, management_helper):
self.helper = helper
@@ -45,6 +47,7 @@ class BackupManager:
self, backup_config, backup_location, backup_file, svr_obj, in_place
):
server_path = svr_obj.settings["path"]
error = False
if Helpers.validate_traversal(backup_location, backup_file):
if svr_obj.check_running():
svr_obj.stop_server()
@@ -58,18 +61,49 @@ class BackupManager:
os.path.isdir(os.path.join(server_path, item))
and item != "db_stats"
):
self.file_helper.del_dirs(os.path.join(server_path, item))
result = self.file_helper.del_dirs(
os.path.join(server_path, item)
)
if not result:
error = True
else:
self.file_helper.del_file(os.path.join(server_path, item))
result = self.file_helper.del_file(
os.path.join(server_path, item)
)
if not result:
error = True
self.file_helper.restore_archive(backup_location, server_path)
server_users = PermissionsServers.get_server_user_list(svr_obj.server_id)
time.sleep(3)
if error:
for user in server_users:
WebSocketManager().broadcast_user(
user,
"send_start_error",
self.helper.translation.translate(
"notify", "restoreFailed", HelperUsers.get_user_lang_by_id(user)
),
)
else:
for user in server_users:
WebSocketManager().broadcast_user(
user,
"notification",
self.helper.translation.translate(
"notify",
"restoreSuccess",
HelperUsers.get_user_lang_by_id(user),
),
)
def backup_starter(self, backup_config, server):
def backup_starter(self, backup_config, server) -> tuple:
"""Notify users of backup starting, and start the backup.
Args:
backup_config (_type_): _description_
server (_type_): Server object to backup
"""
# Notify users of backup starting
logger.info(f"Starting server {server.name} (ID {server.server_id}) backup")
server_users = PermissionsServers.get_server_user_list(server.server_id)
@@ -83,14 +117,37 @@ class BackupManager:
).format(server.name),
)
time.sleep(3)
size = False
# Start the backup
if backup_config.get("backup_type", "zip_vault") == "zip_vault":
self.zip_vault(backup_config, server)
backup_file_name = self.zip_vault(backup_config, server)
if (
backup_file_name
and Path(backup_file_name).suffix != self.ARCHIVE_SUFFIX
):
backup_file_name += self.ARCHIVE_SUFFIX
if backup_file_name:
size = (
Path(
backup_config["backup_location"],
backup_config["backup_id"],
backup_file_name,
)
.stat()
.st_size
)
else:
self.snapshot_backup(backup_config, server)
backup_file_name = self.snapshot_backup(backup_config, server)
if (
backup_file_name
and Path(backup_file_name).suffix != self.SNAPSHOT_SUFFIX
):
backup_file_name += self.SNAPSHOT_SUFFIX
if backup_file_name:
return (backup_file_name, size)
return (False, "error")
def zip_vault(self, backup_config, server):
def zip_vault(self, backup_config, server) -> str | bool:
# Adjust the location to include the backup ID for destination.
backup_location = os.path.join(
@@ -100,7 +157,7 @@ class BackupManager:
# Check if the backup location even exists.
if not backup_location:
Console.critical("No backup path found. Canceling")
return None
return False
self.helper.ensure_dir_exists(backup_location)
@@ -164,8 +221,10 @@ class BackupManager:
{"status": json.dumps({"status": "Standby", "message": ""})},
)
time.sleep(5)
return Path(backup_filename).name
except Exception as e:
self.fail_backup(e, backup_config, server)
return False
@staticmethod
def fail_backup(why: Exception, backup_config: dict, server) -> None:
@@ -203,8 +262,7 @@ class BackupManager:
{"status": json.dumps({"status": "Failed", "message": f"{why}"})},
)
@staticmethod
def list_backups(backup_config: dict, server_id) -> list:
def list_backups(self, backup_config: dict, server_id) -> list:
if not backup_config:
logger.info(
f"Error putting backup file list for server with ID: {server_id}"
@@ -237,7 +295,7 @@ class BackupManager:
"size": "",
}
for f in files
if f["path"].endswith(".manifest")
if f["path"].endswith(self.SNAPSHOT_SUFFIX)
]
return [
{
@@ -248,7 +306,7 @@ class BackupManager:
"size": f["size"],
}
for f in files
if f["path"].endswith(".zip")
if f["path"].endswith(self.ARCHIVE_SUFFIX)
]
def remove_old_backups(self, backup_config, server):
@@ -266,7 +324,7 @@ class BackupManager:
logger.info(f"Removing old backup '{oldfile['path']}'")
os.remove(Helpers.get_os_understandable_path(oldfile_path))
def snapshot_backup(self, backup_config, server) -> None:
def snapshot_backup(self, backup_config, server) -> str | bool:
"""
Creates snapshot style backup of server. No file will be saved more than once
over all backups. Designed to enable encryption of files and s3 compatability in
@@ -308,7 +366,7 @@ class BackupManager:
manifest_file: io.TextIOWrapper = backup_manifest_path.open("w+")
except OSError as why:
self.fail_backup(why, backup_config, server)
return
return False
# Write manifest file version.
manifest_file.write("00\n")
@@ -328,7 +386,7 @@ class BackupManager:
manifest_file.close()
backup_manifest_path.unlink(missing_ok=True)
self.fail_backup(why, backup_config, server)
return
return False
# Write saved file into manifest.
manifest_file.write(
@@ -342,6 +400,13 @@ class BackupManager:
backup_config["max_backups"], backup_repository_path
)
HelpersManagement.update_backup_config(
backup_config["backup_id"],
{"status": json.dumps({"status": "Standby", "message": ""})},
)
return Path(backup_manifest_path).name
def snapshot_restore(
self, backup_config: {str}, backup_manifest_filename: str, server
) -> None:

View File

@@ -222,7 +222,9 @@ class ImportHelpers:
)
download_thread.start()
def download_threaded_bedrock_server(self, path, new_id, bedrock_url):
def download_threaded_bedrock_server(
self, path, new_id, bedrock_url, server_update=False
):
"""
Downloads the latest Bedrock server, unzips it, sets necessary permissions.
@@ -244,7 +246,7 @@ class ImportHelpers:
unzip_path = self.helper.wtol_path(file_path)
# unzips archive that was downloaded.
self.file_helper.unzip_file(unzip_path)
self.file_helper.unzip_file(unzip_path, server_update)
# adjusts permissions for execution if os is not windows
if not self.helper.is_os_windows():

View File

@@ -7,7 +7,7 @@ import time
import datetime
import base64
import threading
import logging.config
import logging
import subprocess
import html
import glob
@@ -54,31 +54,72 @@ def callback(called_func):
res = None
logger.debug("Checking for callbacks")
try:
res = called_func(*args, **kwargs)
res = called_func(*args, **kwargs) # Calls and runs the function
finally:
events = WebhookFactory.get_monitored_events()
if called_func.__name__ in events:
event_type = called_func.__name__
# For send_command, Retrieve command from args or kwargs
command = args[1] if len(args) > 1 else kwargs.get("command", "")
if event_type in WebhookFactory.get_monitored_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(","):
if event_type in str(swebhook.trigger).split(","):
logger.info(
f"Found callback for event {called_func.__name__}"
f"Found callback for event {event_type}"
f" for server {args[0].server_id}"
)
webhook = HelpersWebhooks.get_webhook_by_id(swebhook.id)
webhook_provider = WebhookFactory.create_provider(
webhook["webhook_type"]
)
# Extract source context from kwargs if present
source_type = kwargs.get("source_type", "unknown")
source_id = kwargs.get("source_id", "")
source_name = kwargs.get("source_name", "")
backup_name = ""
backup_size = ""
backup_link = ""
backup_status = ""
backup_error = ""
if isinstance(res, dict):
backup_name = res.get("backup_name")
backup_size = str(res.get("backup_size"))
backup_link = res.get("backup_link")
backup_status = res.get("backup_status")
backup_error = res.get("backup_error")
event_data = {
"server_name": args[0].name,
"server_id": args[0].server_id,
"command": command,
"event_type": event_type,
"source_type": source_type,
"source_id": source_id,
"source_name": source_name,
"backup_name": backup_name,
"backup_size": backup_size,
"backup_link": backup_link,
"backup_status": backup_status,
"backup_error": backup_error,
}
# Add time variables to event_data
event_data = webhook_provider.add_time_variables(event_data)
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"],
message_template=webhook["body"],
event_data=event_data,
color=webhook["color"],
bot_name=webhook["bot_name"],
)
return res
@@ -1205,7 +1246,7 @@ class ServerInstance:
logger.info(f"Backup Thread started for server {self.settings['server_name']}.")
@callback
def backup_server(self, backup_id):
def backup_server(self, backup_id) -> dict | bool:
logger.info(f"Starting server {self.name} (ID {self.server_id}) backup")
server_users = PermissionsServers.get_server_user_list(self.server_id)
# Alert the start of the backup to the authorized users.
@@ -1221,7 +1262,15 @@ class ServerInstance:
# Get the backup config
if not backup_id:
return logger.error("No backup ID provided. Exiting backup")
logger.error("No backup ID provided. Exiting backup")
last_failed = self.last_backup_status()
if last_failed:
last_backup_status = ""
reason = "No backup ID provided"
return {
"backup_status": last_backup_status,
"backup_error": reason,
}
conf = HelpersManagement.get_backup_config(backup_id)
# Adjust the location to include the backup ID for destination.
backup_location = os.path.join(conf["backup_location"], conf["backup_id"])
@@ -1229,7 +1278,16 @@ class ServerInstance:
# Check if the backup location even exists.
if not backup_location:
Console.critical("No backup path found. Canceling")
return None
backup_status = json.loads(
HelpersManagement.get_backup_config(backup_id)["status"]
)
if backup_status["status"] == "Failed":
last_backup_status = ""
reason = backup_status["message"]
return {
"backup_status": last_backup_status,
"backup_error": reason,
}
if conf["before"]:
logger.debug(
"Found running server and send command option. Sending command"
@@ -1237,7 +1295,7 @@ class ServerInstance:
self.send_command(conf["before"])
# Pause to let command run
time.sleep(5)
self.backup_mgr.backup_starter(conf, self)
backup_name, backup_size = self.backup_mgr.backup_starter(conf, self)
if conf["after"]:
self.send_command(conf["after"])
if conf["shutdown"] and self.was_running:
@@ -1247,6 +1305,46 @@ class ServerInstance:
self.run_threaded_server(HelperUsers.get_user_id_by_name("system"))
self.set_backup_status()
# Return data for webhooks callback
base_url = f"{self.helper.get_setting('base_url')}"
size = backup_size
backup_status = json.loads(
HelpersManagement.get_backup_config(backup_id)["status"]
)
reason = backup_status["message"]
if not backup_name:
return {
"backup_status": "",
"backup_error": reason,
}
if backup_size:
size = self.helper.human_readable_file_size(backup_size)
url = (
f"https://{base_url}/api/v2/servers/{self.server_id}"
f"/backups/backup/{backup_id}/download/{html.escape(backup_name)}"
)
if conf["backup_type"] == "snapshot":
size = 0
url = (
f"https://{base_url}/panel/edit_backup?"
f"id={self.server_id}&backup_id={backup_id}"
)
backup_status = json.loads(
HelpersManagement.get_backup_config(backup_id)["status"]
)
last_backup_status = ""
reason = ""
if backup_status["status"] == "Failed":
last_backup_status = ""
reason = backup_status["message"]
return {
"backup_name": backup_name,
"backup_size": size,
"backup_link": url,
"backup_status": last_backup_status,
"backup_error": reason,
}
def set_backup_status(self):
backups = HelpersManagement.get_backups_by_server(self.server_id, True)
alert = False
@@ -1401,7 +1499,7 @@ class ServerInstance:
if bedrock_url:
# Use the new method for secure download
self.import_helper.download_threaded_bedrock_server(
self.settings["path"], self.server_id, bedrock_url
self.settings["path"], self.server_id, bedrock_url, True
)
downloaded = True
except Exception as e:

View File

@@ -5,6 +5,7 @@ import threading
import asyncio
import datetime
import json
from pathlib import Path
from zoneinfo import ZoneInfoNotFoundError
from tzlocal import get_localzone
from apscheduler.events import EVENT_JOB_EXECUTED
@@ -265,9 +266,8 @@ class TasksManager:
if schedule.interval_type == "hours":
new_job = self.scheduler.add_job(
self.controller.management.queue_command,
"cron",
minute=0,
hour="*/" + str(schedule.interval),
"interval",
hours=int(schedule.interval),
id=str(schedule.schedule_id),
args=[
{
@@ -283,8 +283,8 @@ class TasksManager:
elif schedule.interval_type == "minutes":
new_job = self.scheduler.add_job(
self.controller.management.queue_command,
"cron",
minute="*/" + str(schedule.interval),
"interval",
minutes=int(schedule.interval),
id=str(schedule.schedule_id),
args=[
{
@@ -395,9 +395,8 @@ class TasksManager:
if job_data["interval_type"] == "hours":
new_job = self.scheduler.add_job(
self.controller.management.queue_command,
"cron",
minute=0,
hour="*/" + str(job_data["interval"]),
"interval",
hours=int(job_data["interval"]),
id=str(sch_id),
args=[
{
@@ -413,8 +412,8 @@ class TasksManager:
elif job_data["interval_type"] == "minutes":
new_job = self.scheduler.add_job(
self.controller.management.queue_command,
"cron",
minute="*/" + str(job_data["interval"]),
"interval",
minutes=int(job_data["interval"]),
id=str(sch_id),
args=[
{
@@ -817,15 +816,16 @@ class TasksManager:
os.remove(os.path.join(file))
except FileNotFoundError:
logger.debug("Could not clear out file from temp directory")
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, "import", "upload", file)
):
import_path = Path(self.controller.project_root, "import", "upload")
for file in os.listdir(import_path):
file_path = Path(import_path, file).resolve(strict=True)
if not self.helper.validate_traversal(import_path, file_path):
logger.error(
"Traversal detected while deleting import file %s", file_path
)
if self.helper.is_file_older_than_x_days(file_path):
try:
os.remove(os.path.join(file))
os.remove(file_path)
except FileNotFoundError:
logger.debug("Could not clear out file from import directory")

View File

@@ -701,7 +701,9 @@ class PanelHandler(BaseHandler):
server_id, model=True
)
)
page_data["triggers"] = WebhookFactory.get_monitored_events()
page_data["triggers"] = list(
WebhookFactory.get_monitored_events().keys()
)
def get_banned_players_html():
banned_players = self.controller.servers.get_banned_players(server_id)
@@ -991,7 +993,7 @@ class PanelHandler(BaseHandler):
page_data["webhook"]["enabled"] = True
page_data["providers"] = WebhookFactory.get_supported_providers()
page_data["triggers"] = WebhookFactory.get_monitored_events()
page_data["triggers"] = list(WebhookFactory.get_monitored_events().keys())
if not EnumPermissionsServer.CONFIG in page_data["user_permissions"]:
if not superuser:
@@ -1042,7 +1044,7 @@ class PanelHandler(BaseHandler):
).split(",")
page_data["providers"] = WebhookFactory.get_supported_providers()
page_data["triggers"] = WebhookFactory.get_monitored_events()
page_data["triggers"] = list(WebhookFactory.get_monitored_events().keys())
if not EnumPermissionsServer.CONFIG in page_data["user_permissions"]:
if not superuser:

View File

@@ -119,6 +119,13 @@ config_json_schema = {
"error": "typeBool",
"fill": True,
},
"base_url": {
"type": "string",
"pattern": (
r"^(?:(?:\d{1,3}\.){3}\d{1,3}"
r"|(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,})(?::\d{1,5})?$"
),
},
"max_login_attempts": {"type": "integer", "error": "typeInt", "fill": True},
"superMFA": {"type": "boolean", "error": "typeBool", "fill": True},
"general_user_log_access": {

View File

@@ -1,6 +1,7 @@
import os
import logging
import shutil
import asyncio
import anyio
from PIL import Image
from app.classes.models.server_permissions import EnumPermissionsServer
@@ -37,6 +38,15 @@ ARCHIVE_MIME_TYPES = ["application/zip"]
class ApiFilesUploadHandler(BaseApiHandler):
upload_locks = {}
def get_lock(self, key: str) -> asyncio.Lock:
"""Get or create a lock for the given key."""
if key not in self.upload_locks:
self.upload_locks[key] = asyncio.Lock()
return self.upload_locks[key]
async def post(self, server_id=None):
auth_data = self.authenticate_user()
if not auth_data:
@@ -281,74 +291,85 @@ class ApiFilesUploadHandler(BaseApiHandler):
self.temp_dir, f"{self.filename}.part{self.chunk_index}"
)
# Save the chunk
async with await anyio.open_file(chunk_path, "wb") as f:
await f.write(self.request.body)
lock = self.get_lock(self.file_id) # Capture async lock to avoid race condition
# Check if all chunks are received
received_chunks = [
f
for f in os.listdir(self.temp_dir)
if f.startswith(f"{self.filename}.part")
]
# When we've reached the total chunks we'll
# Compare the hash and write the file
if len(received_chunks) == total_chunks:
async with await anyio.open_file(file_path, "wb") as outfile:
for i in range(total_chunks):
WebSocketManager().broadcast_user(
auth_data[4]["user_id"],
"upload_process",
{"cur_file": i, "total_files": total_chunks, "type": u_type},
)
chunk_file = os.path.join(self.temp_dir, f"{self.filename}.part{i}")
async with await anyio.open_file(chunk_file, "rb") as infile:
await outfile.write(await infile.read())
try:
await anyio.Path(chunk_file).unlink(missing_ok=True)
except OSError as why:
logger.error("Failed to remove chunk file with error: %s", why)
try:
self.file_helper.del_dirs(self.temp_dir)
except OSError as why:
logger.error("Failed to import remove temp dir with error: %s", why)
if upload_type == "background":
# Strip EXIF data
image_path = os.path.join(file_path)
logger.debug("Stripping exif data from image")
image = Image.open(image_path)
async with lock:
# Save the chunk
async with await anyio.open_file(chunk_path, "wb") as f:
await f.write(self.request.body)
# Get current raw pixel data from image
image_data = list(image.getdata())
# Create new image
image_no_exif = Image.new(image.mode, image.size)
# Restore pixel data
image_no_exif.putdata(image_data)
# Check if all chunks are received
received_chunks = [
f
for f in os.listdir(self.temp_dir)
if f.startswith(f"{self.filename}.part")
]
# When we've reached the total chunks we'll
# Compare the hash and write the file
if len(received_chunks) == total_chunks:
async with await anyio.open_file(file_path, "wb") as outfile:
for i in range(total_chunks):
WebSocketManager().broadcast_user(
auth_data[4]["user_id"],
"upload_process",
{
"cur_file": i,
"total_files": total_chunks,
"type": u_type,
},
)
chunk_file = os.path.join(
self.temp_dir, f"{self.filename}.part{i}"
)
async with await anyio.open_file(chunk_file, "rb") as infile:
await outfile.write(await infile.read())
try:
await anyio.Path(chunk_file).unlink(missing_ok=True)
except OSError as why:
logger.error(
"Failed to remove chunk file with error: %s", why
)
try:
self.file_helper.del_dirs(self.temp_dir)
except OSError as why:
logger.error("Failed to import remove temp dir with error: %s", why)
if upload_type == "background":
# Strip EXIF data
image_path = os.path.join(file_path)
logger.debug("Stripping exif data from image")
image = Image.open(image_path)
image_no_exif.save(image_path)
# Get current raw pixel data from image
image_data = list(image.getdata())
# Create new image
image_no_exif = Image.new(image.mode, image.size)
# Restore pixel data
image_no_exif.putdata(image_data)
logger.info(
f"File upload completed. Filename: {self.filename}"
f" Path: {file_path} Type: {u_type}"
)
self.controller.management.add_to_audit_log(
auth_data[4]["user_id"],
f"Uploaded file {self.filename}",
server_id,
self.request.remote_ip,
)
self.finish_json(
200,
{
"status": "completed",
"data": {"message": "File uploaded successfully"},
},
)
else:
self.finish_json(
200,
{
"status": "partial",
"data": {"message": f"Chunk {self.chunk_index} received"},
},
)
image_no_exif.save(image_path)
logger.info(
f"File upload completed. Filename: {self.filename}"
f" Path: {file_path} Type: {u_type}"
)
self.controller.management.add_to_audit_log(
auth_data[4]["user_id"],
f"Uploaded file {self.filename}",
server_id,
self.request.remote_ip,
)
self.finish_json(
200,
{
"status": "completed",
"data": {"message": "File uploaded successfully"},
},
)
else:
self.finish_json(
200,
{
"status": "partial",
"data": {"message": f"Chunk {self.chunk_index} received"},
},
)

View File

@@ -1,6 +1,9 @@
from abc import ABC, abstractmethod
import logging
import datetime
import time
import requests
from jinja2 import Environment, BaseLoader
from app.classes.helpers.helpers import Helpers
@@ -16,6 +19,12 @@ class WebhookProvider(ABC):
ensuring that each provider will have a send method.
"""
def __init__(self):
self.jinja_env = Environment(
loader=BaseLoader(),
autoescape=True,
)
WEBHOOK_USERNAME = "Crafty Webhooks"
WEBHOOK_PFP_URL = (
"https://gitlab.com/crafty-controller/crafty-4/-"
@@ -34,6 +43,55 @@ class WebhookProvider(ABC):
logger.error(error)
raise RuntimeError(f"Failed to dispatch notification: {error}") from error
def render_template(self, template_str, context):
"""
Renders a Jinja2 template with the provided context.
Args:
template_str (str): The Jinja2 template string.
context (dict): A dictionary containing all the variables needed for
rendering the template.
Returns:
str: The rendered message.
"""
try:
template = self.jinja_env.from_string(template_str)
return template.render(context)
except Exception as error:
logger.error(f"Error rendering Jinja2 template: {error}")
raise
def add_time_variables(self, event_data):
"""
Adds various time format variables to the event_data dictionary.
Adds the following time-related variables to event_data:
- time_iso: ISO 8601 formatted datetime (UTC)
- time_unix: UNIX timestamp (seconds since epoch)
- time_day: Day of month (1-31)
- time_month: Month (1-12)
- time_year: Full year (e.g., 2025)
- time_formatted: Human-readable format (YYYY-MM-DD HH:MM:SS UTC)
Args:
event_data (dict): A dictionary containing event information.
Returns:
dict: The event_data dictionary with time variables added.
"""
now_utc = datetime.datetime.now(datetime.timezone.utc)
unix_timestamp = int(time.time())
event_data["time_iso"] = now_utc.isoformat().replace("+00:00", "Z")
event_data["time_unix"] = unix_timestamp
event_data["time_day"] = now_utc.day
event_data["time_month"] = now_utc.month
event_data["time_year"] = now_utc.year
event_data["time_formatted"] = now_utc.strftime("%Y-%m-%d %H:%M:%S UTC")
return event_data
@abstractmethod
def send(self, server_name, title, url, message, **kwargs):
def send(self, server_name, title, url, message_template, event_data, **kwargs):
"""Abstract method that derived classes will implement for sending webhooks."""

View File

@@ -51,7 +51,7 @@ class DiscordWebhook(WebhookProvider):
return payload, headers
def send(self, server_name, title, url, message, **kwargs):
def send(self, server_name, title, url, message_template, event_data, **kwargs):
"""
Sends a Discord webhook notification using the given details.
@@ -74,6 +74,7 @@ class DiscordWebhook(WebhookProvider):
Raises:
Exception: If there's an error in dispatching the webhook.
"""
message = self.render_template(message_template, event_data)
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(

View File

@@ -41,7 +41,7 @@ class MattermostWebhook(WebhookProvider):
return payload, headers
def send(self, server_name, title, url, message, **kwargs):
def send(self, server_name, title, url, message_template, event_data, **kwargs):
"""
Sends a Mattermost webhook notification using the given details.
@@ -52,7 +52,8 @@ class MattermostWebhook(WebhookProvider):
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.
message_template (str): The Jinja2 template for the message body.
event_data (dict): A dictionary containing variables for template rendering.
bot_name (str): Override for the Webhook's name set on creation, see note!
Returns:
@@ -67,6 +68,7 @@ class MattermostWebhook(WebhookProvider):
- Mattermost's `config.json` setting is `"EnablePostUsernameOverride": true`
- Mattermost's `config.json` setting is `"EnablePostIconOverride": true`
"""
message = self.render_template(message_template, event_data)
bot_name = kwargs.get("bot_name", self.WEBHOOK_USERNAME)
payload, headers = self._construct_mattermost_payload(
server_name, title, message, bot_name

View File

@@ -67,7 +67,7 @@ class SlackWebhook(WebhookProvider):
return payload, headers
def send(self, server_name, title, url, message, **kwargs):
def send(self, server_name, title, url, message_template, event_data, **kwargs):
"""
Sends a Slack webhook notification using the given details.
@@ -78,7 +78,8 @@ class SlackWebhook(WebhookProvider):
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.
message_template (str): The Jinja2 template for the message body.
event_data (dict): A dictionary containing variables for template rendering.
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).
@@ -90,6 +91,7 @@ class SlackWebhook(WebhookProvider):
Raises:
Exception: If there's an error in dispatching the webhook.
"""
message = self.render_template(message_template, event_data)
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(

View File

@@ -101,19 +101,19 @@ class TeamsWebhook(WebhookProvider):
return payload, headers
def send(self, server_name, title, url, message, **kwargs):
def send(self, server_name, title, url, message_template, event_data, **kwargs):
"""
Sends a Teams Adaptive card notification using the given details.
The method constructs and dispatches a payload suitable for
Discords's webhook system.
Teams'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.
message_template (str): The Jinja2 template for the message body.
event_data (dict): A dictionary containing variables for template rendering.
Returns:
str: "Dispatch successful!" if the message is sent successfully, otherwise an
@@ -122,5 +122,6 @@ class TeamsWebhook(WebhookProvider):
Raises:
Exception: If there's an error in dispatching the webhook.
"""
message = self.render_template(message_template, event_data)
payload, headers = self._construct_teams_payload(server_name, title, message)
return self._send_request(url, payload, headers)

View File

@@ -13,7 +13,7 @@ class WebhookFactory:
to manage the available providers.
Attributes:
- _registry (dict): A dictionary mapping provider names to their classes.
- _registry (dict): A dictionary mapping provider names to their classes.
"""
_registry = {
@@ -32,18 +32,18 @@ class WebhookFactory:
provided arguments. If the provider is not recognized, a ValueError is raised.
Arguments:
- provider_name (str): The name of the desired webhook provider.
- 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.
- *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.
WebhookProvider: An instance of the desired webhook provider.
Raises:
ValueError: If the specified provider name is not recognized.
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.")
@@ -58,7 +58,7 @@ class WebhookFactory:
currently registered in the factory's registry.
Returns:
List[str]: A list of supported provider names.
List[str]: A list of supported provider names.
"""
return list(cls._registry.keys())
@@ -68,17 +68,45 @@ class WebhookFactory:
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.
monitor and notify about. Along with the available `event_data` vars for use
on the frontend.
Returns:
List[str]: A list of supported monitored actions.
dict: A dictionary where each key is an event name and the value is a
dictionary containing a list of `variables` for that event.
These variables are intended for use in the frontend to show whats
available.
"""
return [
"start_server",
"stop_server",
"crash_detected",
"backup_server",
"jar_update",
"send_command",
"kill",
# Common variables for all events
common_vars = [
"server_name",
"server_id",
"event_type",
"source_type",
"source_id",
"source_name",
"time_iso",
"time_unix",
"time_day",
"time_month",
"time_year",
"time_formatted",
]
return {
"start_server": {"variables": common_vars},
"stop_server": {"variables": common_vars},
"crash_detected": {"variables": common_vars},
"backup_server": {
"variables": common_vars
+ [
"backup_name",
"backup_link",
"backup_size",
"backup_status",
"backup_error",
]
},
"jar_update": {"variables": common_vars},
"send_command": {"variables": common_vars + ["command"]},
"kill": {"variables": common_vars + ["reason"]},
}

View File

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

View File

@@ -133,8 +133,7 @@
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
*/
@import url("/static/assets/fonts/Atikinson_Hyperlegible_Next/AtkinsonHyperlegibleMono-VariableFont_wght.ttf");
@import url("/static/assets/fonts/Atikinson_Hyperlegible_Next/AtkinsonHyperlegibleNext-VariableFont_wght.ttf");
@import url("/static/assets/fonts/Atkinson_Hyperlegible_Next/AtkinsonHyperlegible.css");
*,
*::before,
@@ -169,7 +168,7 @@ section {
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Atikinson Hyperlegible Next", "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Atkinson Hyperlegible Next", "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
@@ -8476,7 +8475,7 @@ a.close.disabled {
z-index: 1070;
display: block;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Atikinson Hyperlegible Next", "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Atkinson Hyperlegible Next", "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
font-style: normal;
font-weight: 400;
line-height: 1.5;
@@ -8601,7 +8600,7 @@ a.close.disabled {
z-index: 1060;
display: block;
max-width: 276px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Atikinson Hyperlegible Next", "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Atkinson Hyperlegible Next", "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
font-style: normal;
font-weight: 400;
line-height: 1.5;
@@ -13949,7 +13948,7 @@ input:focus {
:root,
body {
font-size: 1rem;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
color: #212529;
color: var(--base-text);
}
@@ -13966,7 +13965,7 @@ h3,
h4,
h5,
h6 {
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
font-weight: normal;
font-style: normal;
font-stretch: normal;
@@ -14145,7 +14144,7 @@ address p {
}
.card-title {
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
font-weight: 500;
color: #212229;
margin-bottom: 15px;
@@ -14159,14 +14158,14 @@ address p {
.card-subtitle {
font-weight: 300;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
margin-top: 0.625rem;
margin-bottom: 0.625rem;
}
.card-description {
margin-bottom: 0.9375rem;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
}
.rtl .card-description {
@@ -14997,7 +14996,7 @@ pre {
.card-revenue-table .revenue-item .revenue-amount p {
font-size: 1.25rem;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
font-weight: 600;
text-align: right;
}
@@ -15014,7 +15013,7 @@ pre {
.card-revenue .highlight-text {
font-size: 1.875rem;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
font-weight: 500;
}
@@ -15380,7 +15379,7 @@ pre {
/* Navbar */
.navbar.default-layout {
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
background: var(--dropdown-bg);
transition: background 0.25s ease;
-webkit-transition: background 0.25s ease;
@@ -15610,7 +15609,7 @@ pre {
min-height: calc(100vh - 63px);
background: -webkit-gradient(linear, left bottom, left top, from(var(--dropdown-bg)), to(var(--dropdown-bg)));
background: linear-gradient(to top, var(--dropdown-bg), var(--dropdown-bg));
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
padding: 0;
width: 270px;
z-index: 11;
@@ -15931,7 +15930,7 @@ pre {
-ms-transition: all 0.25s ease;
border-top: 1px solid var(--outline);
font-size: calc(0.875rem - 0.05rem);
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
}
.footer a {
@@ -17475,7 +17474,7 @@ pre {
font-weight: initial;
line-height: 1;
padding: 4px 6px;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
font-weight: 600;
letter-spacing: 0.04rem;
}
@@ -17929,7 +17928,7 @@ pre {
.wizard>.actions a {
font-size: 0.875rem;
line-height: 1;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
}
.btn i,
@@ -20165,7 +20164,7 @@ pre {
display: inline-block;
border: 1px solid #dee2e6;
border: 1px solid var(--outline);
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
font-size: 0.75rem;
color: #212529;
color: var(--base-text);
@@ -20309,7 +20308,7 @@ select.typeahead {
-ms-flex-align: center;
align-items: center;
padding: 15px;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
font-size: 0.875rem;
}
@@ -22762,7 +22761,7 @@ ul li {
}
.preview-list .preview-item .preview-item-content p .content-category {
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
padding-right: 15px;
border-right: 1px solid #dee2e6;
border-right: 1px solid var(--outline);
@@ -22826,7 +22825,7 @@ ul li {
.pricing-table .pricing-card .pricing-card-body .plan-features li {
text-align: left;
padding: 4px 0;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
font-size: 0.875rem;
}
@@ -22840,7 +22839,7 @@ ul li {
.jsgrid .jsgrid-table thead th {
border-top: 0;
border-bottom-width: 1px;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
font-weight: 500;
color: #212529;
color: var(--base-text);
@@ -23014,7 +23013,7 @@ ul li {
/* Tabs */
.nav-pills .nav-item .nav-link,
.nav-tabs .nav-item .nav-link {
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
line-height: 1;
font-size: 0.875rem;
color: #212529;
@@ -23031,7 +23030,7 @@ ul li {
}
.tab-content {
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
font-size: 0.875rem;
line-height: 1.71;
}
@@ -23524,7 +23523,7 @@ ul li {
}
.settings-panel .events p {
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
}
.rtl .settings-panel .events p {
@@ -23761,7 +23760,7 @@ ul li {
}
.tooltip .tooltip-inner {
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
}
.tooltip-primary .tooltip-inner {
@@ -24655,7 +24654,7 @@ ul li {
padding: 11px 25px;
background: rgba(33, 150, 243, 0.2);
width: 80%;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
font-size: 13px;
font-weight: 300;
border-radius: 4px;
@@ -24663,7 +24662,7 @@ ul li {
.horizontal-timeline .time-frame .event .event-info {
margin-top: 0.8rem;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
font-size: 12px;
font-weight: 400;
color: var(--gray);
@@ -25814,7 +25813,7 @@ ul li {
font-size: 0.875rem;
color: var(--gray);
margin-bottom: 0;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
}
.email-wrapper .mail-sidebar .menu-bar .online-status .status {
@@ -25898,7 +25897,7 @@ ul li {
.email-wrapper .mail-sidebar .menu-bar .profile-list-item a .user .u-name {
margin: 0;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
font-size: 0.875rem;
line-height: 1;
color: #212529;
@@ -25963,7 +25962,7 @@ ul li {
.email-wrapper .mail-list-container .mail-list .content .sender-name {
margin-bottom: 0;
font-size: 0.875rem;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
font-weight: 500;
max-width: 95%;
}
@@ -26018,17 +26017,17 @@ ul li {
.email-wrapper .message-body .sender-details .details .msg-subject {
margin-bottom: 0;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
}
.email-wrapper .message-body .sender-details .details .sender-email {
margin-bottom: 20px;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
}
.email-wrapper .message-body .sender-details .details .sender-email i {
font-size: 1rem;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
margin: 0 1px 0 7px;
}
@@ -26130,7 +26129,7 @@ ul li {
.email-wrapper .mail-list-container .mail-list .content .sender-name {
margin-bottom: 0;
font-size: 0.875rem;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
font-weight: 500;
max-width: 95%;
}
@@ -26186,17 +26185,17 @@ ul li {
.email-wrapper .message-body .sender-details .details .msg-subject {
margin-bottom: 0;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
}
.email-wrapper .message-body .sender-details .details .sender-email {
margin-bottom: 20px;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
}
.email-wrapper .message-body .sender-details .details .sender-email i {
font-size: 1rem;
font-family: "Atikinson Hyperlegible Nextson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Nextson Hyperlegible Next", sans-serif;
margin: 0 1px 0 7px;
}
@@ -26327,7 +26326,7 @@ ul li {
left: 50%;
z-index: 1000;
font-size: 0.875rem;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
font-weight: initial;
line-height: 1.85;
border-radius: 10px;
@@ -26337,7 +26336,7 @@ ul li {
.avgrund-popin p {
font-size: 0.875rem;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
font-weight: initial;
}
@@ -26418,7 +26417,7 @@ body.avgrund-active {
.tour-tour {
padding: 0;
border: 0;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
background: #fff;
}
@@ -26426,7 +26425,7 @@ body.avgrund-active {
background: var(--primary);
color: var(--base-text);
font-size: 0.8125rem;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
padding: 0.75rem;
}
@@ -26765,7 +26764,7 @@ body.avgrund-active {
.context-menu-list .context-menu-item span {
color: #000;
font-size: 0.75rem;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
}
.context-menu-list .context-menu-item.context-menu-hover {
@@ -27027,7 +27026,7 @@ body.avgrund-active {
.datepicker.datepicker-dropdown .datepicker-days table.table-condensed thead tr th.dow,
.datepicker.datepicker-inline .datepicker-days table.table-condensed thead tr th.dow {
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
color: var(--gray);
font-size: 0.875rem;
font-weight: initial;
@@ -27477,7 +27476,7 @@ body.avgrund-active {
.jsgrid .jsgrid-table th {
font-weight: initial;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
border-top-color: #dee2e6;
border-top-color: var(--outline);
}
@@ -27665,7 +27664,7 @@ body.avgrund-active {
}
.noUi-target .noUi-base .noUi-origin .noUi-handle .noUi-tooltip {
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
border-radius: 4px;
border: none;
line-height: 1;
@@ -27694,7 +27693,7 @@ body.avgrund-active {
color: #212529;
color: var(--base-text);
font-size: 0.94rem;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
}
/* Slider Color variations */
@@ -28231,7 +28230,7 @@ body.avgrund-active {
.swal2-modal .swal2-title {
font-size: 25px;
line-height: 1;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
color: #212529;
color: var(--base-text);
font-weight: initial;
@@ -28274,7 +28273,7 @@ body.avgrund-active {
.swal2-modal .swal2-content {
font-size: 0.875rem;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
color: #212529;
color: var(--base-text);
font-weight: initial;
@@ -28701,7 +28700,7 @@ div.tagsinput span.tag a {
text-decoration: none;
border-radius: 5px;
font-size: 0.875rem;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
}
.wizard>.steps a:hover {
@@ -29157,7 +29156,7 @@ div.tagsinput span.tag a {
}
.auth.theme-one .auto-form-wrapper .form-group .submit-btn {
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
font-size: 13px;
padding: 12px 8px;
font-weight: 600;
@@ -29393,7 +29392,7 @@ div.tagsinput span.tag a {
}
.auth.theme-two .auto-form-wrapper form .form-group .submit-btn {
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
font-size: 13px;
padding: 11px 33px;
font-weight: 600;
@@ -29626,7 +29625,7 @@ div.tagsinput span.tag a {
padding-left: 1rem;
padding-right: 1rem;
font-size: 0.9375rem;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
text-align: left;
}
@@ -29771,7 +29770,7 @@ div.tagsinput span.tag a {
}
.landing-page .feature-list .feature-list-row .feature-list-item .feature-description {
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
}
.landing-page .footer {
@@ -29816,7 +29815,7 @@ div.tagsinput span.tag a {
padding-top: 0;
padding-bottom: 0;
font-size: 0.9375rem;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
line-height: 1;
}
@@ -29855,7 +29854,7 @@ div.tagsinput span.tag a {
.landing-page .footer .footer-bottom {
color: var(--base-text);
color: var(--white);
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
}
.profile-page .profile-header {
@@ -29868,14 +29867,14 @@ div.tagsinput span.tag a {
.profile-page .profile-header .profile-info .profile-user-name {
margin-bottom: 0;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
font-weight: 600;
color: var(--base-text);
}
.profile-page .profile-header .profile-info .profile-user-designation {
margin-bottom: 0;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
color: var(--base-text);
color: var(--dropdown-bg);
}

View File

@@ -12,7 +12,7 @@
--blue: #00aeef;
--indigo: #6610f2;
--purple: #ab8ce4;
--pink: #E91E63;
--pink: #e91e63;
--red: #ff0017;
--orange: #fb9678;
--yellow: #ffd500;
@@ -31,12 +31,16 @@
--warning: #ffaf00;
--danger: #ff6258;
--light: #fbfbfb;
--dark: #252C46;
--dark: #252c46;
--breakpoint-xs: 0;
--breakpoint-sm: 576px;
--breakpoint-md: 768px;
--breakpoint-lg: 992px;
--breakpoint-xl: 1200px;
--font-family-sans-serif: -apple-system, BlinkMacSystemFont, "Segoe UI", "Atikinson Hyperlegible Next", "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--font-family-monospace: "Atkinson Hyperlegible Mono", SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}
--font-family-sans-serif: -apple-system, BlinkMacSystemFont, "Segoe UI",
"Atkinson Hyperlegible Next", "Helvetica Neue", Arial, "Noto Sans",
sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
"Noto Color Emoji";
--font-family-monospace: "Atkinson Hyperlegible Mono", SFMono-Regular, Menlo,
Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}

View File

@@ -28,6 +28,10 @@
--breakpoint-md: 768px;
--breakpoint-lg: 992px;
--breakpoint-xl: 1200px;
--font-family-sans-serif: -apple-system, BlinkMacSystemFont, "Segoe UI", "Atikinson Hyperlegible Next", "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--font-family-monospace: "Atkinson Hyperlegible Mono", SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}
--font-family-sans-serif: -apple-system, BlinkMacSystemFont, "Segoe UI",
"Atkinson Hyperlegible Next", "Helvetica Neue", Arial, "Noto Sans",
sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
"Noto Color Emoji";
--font-family-monospace: "Atkinson Hyperlegible Mono", SFMono-Regular, Menlo,
Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}

View File

@@ -0,0 +1,9 @@
.jinja2 {
background-color: var(--card-banner-bg);
border-top-left-radius: 10px;
border-top-right-radius: 10px;
outline: 1px solid var(--outline);
padding: 10px;
margin-top: 10px;
margin-bottom: -10px;
}

View File

@@ -133,8 +133,7 @@
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
*/
@import url("/static/assets/fonts/Atikinson_Hyperlegible_Next/AtkinsonHyperlegibleMono-VariableFont_wght.ttf");
@import url("/static/assets/fonts/Atikinson_Hyperlegible_Next/AtkinsonHyperlegibleNext-VariableFont_wght.ttf");
@import url("/static/assets/fonts/Atkinson_Hyperlegible_Next/AtkinsonHyperlegible.css");
:root {
--blue: #00aeef;
@@ -178,8 +177,8 @@
--breakpoint-md: 768px;
--breakpoint-lg: 992px;
--breakpoint-xl: 1200px;
--font-family-sans-serif: -apple-system, BlinkMacSystemFont, "Segoe UI", "Atikinson Hyperlegible Next", "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--font-family-monospace: "Atikinson Hyperlegible Mono", SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--font-family-sans-serif: -apple-system, BlinkMacSystemFont, "Segoe UI", "Atkinson Hyperlegible Next", "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--font-family-monospace: "Atkinson Hyperlegible Mono", SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}
*,
@@ -211,7 +210,7 @@ section {
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Atikinson Hyperlegible Next", "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Atkinson Hyperlegible Next", "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
@@ -8504,7 +8503,7 @@ a.close.disabled {
z-index: 1070;
display: block;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Atikinson Hyperlegible Next", "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Atkinson Hyperlegible Next", "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
font-style: normal;
font-weight: 400;
line-height: 1.5;
@@ -8629,7 +8628,7 @@ a.close.disabled {
z-index: 1060;
display: block;
max-width: 276px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Atikinson Hyperlegible Next", "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Atkinson Hyperlegible Next", "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
font-style: normal;
font-weight: 400;
line-height: 1.5;
@@ -13958,7 +13957,7 @@ input:focus {
:root,
body {
font-size: 1rem;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
color: #212529;
}
@@ -13974,7 +13973,7 @@ h3,
h4,
h5,
h6 {
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
font-weight: normal;
font-style: normal;
font-stretch: normal;
@@ -14150,7 +14149,7 @@ address p {
}
.card-title {
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
font-weight: 500;
color: #212229;
margin-bottom: 15px;
@@ -14164,14 +14163,14 @@ address p {
.card-subtitle {
font-weight: 300;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
margin-top: 0.625rem;
margin-bottom: 0.625rem;
}
.card-description {
margin-bottom: 0.9375rem;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
}
.rtl .card-description {
@@ -14986,7 +14985,7 @@ pre {
.card-revenue-table .revenue-item .revenue-amount p {
font-size: 1.25rem;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
font-weight: 600;
text-align: right;
}
@@ -15003,7 +15002,7 @@ pre {
.card-revenue .highlight-text {
font-size: 1.875rem;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
font-weight: 500;
}
@@ -15978,7 +15977,7 @@ pre {
font-weight: initial;
line-height: 1;
padding: 4px 6px;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
font-weight: 600;
letter-spacing: 0.04rem;
}
@@ -16415,7 +16414,7 @@ pre {
.wizard>.actions a {
font-size: 0.875rem;
line-height: 1;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
}
.btn i,
@@ -18583,7 +18582,7 @@ pre {
.typeahead {
display: inline-block;
border: 1px solid #dee2e6;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
font-size: 0.75rem;
color: #212529;
padding: 0 .75rem;
@@ -18721,7 +18720,7 @@ select.typeahead {
-ms-flex-align: center;
align-items: center;
padding: 15px;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
font-size: 0.875rem;
}
@@ -21128,7 +21127,7 @@ ul li {
}
.preview-list .preview-item .preview-item-content p .content-category {
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
padding-right: 15px;
border-right: 1px solid #dee2e6;
}
@@ -21190,7 +21189,7 @@ ul li {
.pricing-table .pricing-card .pricing-card-body .plan-features li {
text-align: left;
padding: 4px 0;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
font-size: 0.875rem;
}
@@ -21204,7 +21203,7 @@ ul li {
.jsgrid .jsgrid-table thead th {
border-top: 0;
border-bottom-width: 1px;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
font-weight: 500;
color: #212529;
border-bottom-color: #dee2e6;
@@ -21371,7 +21370,7 @@ ul li {
/* Tabs */
.nav-pills .nav-item .nav-link,
.nav-tabs .nav-item .nav-link {
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
line-height: 1;
font-size: 0.875rem;
color: #212529;
@@ -21387,7 +21386,7 @@ ul li {
}
.tab-content {
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
font-size: 0.875rem;
line-height: 1.71;
}
@@ -21870,7 +21869,7 @@ ul li {
}
.settings-panel .events p {
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
}
.rtl .settings-panel .events p {
@@ -22103,7 +22102,7 @@ ul li {
}
.tooltip .tooltip-inner {
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
}
.tooltip-primary .tooltip-inner {
@@ -22980,7 +22979,7 @@ ul li {
padding: 11px 25px;
background: rgba(33, 150, 243, 0.2);
width: 80%;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
font-size: 13px;
font-weight: 300;
border-radius: 4px;
@@ -22988,7 +22987,7 @@ ul li {
.horizontal-timeline .time-frame .event .event-info {
margin-top: 0.8rem;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
font-size: 12px;
font-weight: 400;
color: var(--gray);
@@ -24109,7 +24108,7 @@ ul li {
font-size: 0.875rem;
color: var(--gray);
margin-bottom: 0;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
}
.email-wrapper .mail-sidebar .menu-bar .online-status .status {
@@ -24191,7 +24190,7 @@ ul li {
.email-wrapper .mail-sidebar .menu-bar .profile-list-item a .user .u-name {
margin: 0;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
font-size: 0.875rem;
line-height: 1;
color: #212529;
@@ -24253,7 +24252,7 @@ ul li {
.email-wrapper .mail-list-container .mail-list .content .sender-name {
margin-bottom: 0;
font-size: 0.875rem;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
font-weight: 500;
max-width: 95%;
}
@@ -24308,17 +24307,17 @@ ul li {
.email-wrapper .message-body .sender-details .details .msg-subject {
margin-bottom: 0;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
}
.email-wrapper .message-body .sender-details .details .sender-email {
margin-bottom: 20px;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
}
.email-wrapper .message-body .sender-details .details .sender-email i {
font-size: 1rem;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
margin: 0 1px 0 7px;
}
@@ -24446,7 +24445,7 @@ ul li {
left: 50%;
z-index: 1000;
font-size: 0.875rem;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
font-weight: initial;
line-height: 1.85;
border-radius: 10px;
@@ -24456,7 +24455,7 @@ ul li {
.avgrund-popin p {
font-size: 0.875rem;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
font-weight: initial;
}
@@ -24537,7 +24536,7 @@ body.avgrund-active {
.tour-tour {
padding: 0;
border: 0;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
background: #fff;
}
@@ -24545,7 +24544,7 @@ body.avgrund-active {
background: var(--primary);
var(--base-text);
font-size: 0.8125rem;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
padding: 0.75rem;
}
@@ -25080,7 +25079,7 @@ body.avgrund-active {
.datepicker.datepicker-dropdown .datepicker-days table.table-condensed thead tr th.dow,
.datepicker.datepicker-inline .datepicker-days table.table-condensed thead tr th.dow {
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
color: var(--gray);
font-size: 0.875rem;
font-weight: initial;
@@ -25512,7 +25511,7 @@ body.avgrund-active {
.jsgrid .jsgrid-table th {
font-weight: initial;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
border-top-color: #dee2e6;
}
@@ -25691,7 +25690,7 @@ body.avgrund-active {
}
.noUi-target .noUi-base .noUi-origin .noUi-handle .noUi-tooltip {
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
border-radius: 4px;
border: none;
line-height: 1;
@@ -25718,7 +25717,7 @@ body.avgrund-active {
.noUi-target .noUi-pips .noUi-value {
color: #212529;
font-size: 0.94rem;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
}
/* Slider Color variations */
@@ -26237,7 +26236,7 @@ body.avgrund-active {
.swal2-modal .swal2-title {
font-size: 25px;
line-height: 1;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
color: #212529;
font-weight: initial;
margin-bottom: 0;
@@ -26276,7 +26275,7 @@ body.avgrund-active {
.swal2-modal .swal2-content {
font-size: 0.875rem;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
color: #212529;
font-weight: initial;
margin-top: 11px;
@@ -26683,7 +26682,7 @@ div.tagsinput span.tag a {
text-decoration: none;
border-radius: 5px;
font-size: 0.875rem;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
}
.wizard>.steps a:hover {
@@ -27123,7 +27122,7 @@ div.tagsinput span.tag a {
}
.auth.theme-one .auto-form-wrapper .form-group .submit-btn {
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
font-size: 13px;
padding: 12px 8px;
font-weight: 600;
@@ -27353,7 +27352,7 @@ div.tagsinput span.tag a {
}
.auth.theme-two .auto-form-wrapper form .form-group .submit-btn {
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
font-size: 13px;
padding: 11px 33px;
font-weight: 600;
@@ -27578,7 +27577,7 @@ div.tagsinput span.tag a {
padding-left: 1rem;
padding-right: 1rem;
font-size: 0.9375rem;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
text-align: left;
}
@@ -27723,7 +27722,7 @@ div.tagsinput span.tag a {
}
.landing-page .feature-list .feature-list-row .feature-list-item .feature-description {
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
}
.landing-page .footer {
@@ -27766,7 +27765,7 @@ div.tagsinput span.tag a {
padding-top: 0;
padding-bottom: 0;
font-size: 0.9375rem;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
line-height: 1;
}
@@ -27804,7 +27803,7 @@ div.tagsinput span.tag a {
.landing-page .footer .footer-bottom {
var(--base-text)fff;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
}
.profile-page .profile-header {
@@ -27817,14 +27816,14 @@ div.tagsinput span.tag a {
.profile-page .profile-header .profile-info .profile-user-name {
margin-bottom: 0;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
font-weight: 600;
var(--base-text);
}
.profile-page .profile-header .profile-info .profile-user-designation {
margin-bottom: 0;
font-family: "Atikinson Hyperlegible Next", sans-serif;
font-family: "Atkinson Hyperlegible Next", sans-serif;
var(--base-text);
}

View File

@@ -38,6 +38,10 @@
--breakpoint-md: 768px;
--breakpoint-lg: 992px;
--breakpoint-xl: 1200px;
--font-family-sans-serif: -apple-system, BlinkMacSystemFont, "Segoe UI", "Atikinson Hyperlegible Next", "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--font-family-monospace: "Atikinson Hyperlegible Mono", SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--font-family-sans-serif: -apple-system, BlinkMacSystemFont, "Segoe UI",
"Atkinson Hyperlegible Next", "Helvetica Neue", Arial, "Noto Sans",
sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
"Noto Color Emoji";
--font-family-monospace: "Atkinson Hyperlegible Mono", SFMono-Regular, Menlo,
Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}

View File

@@ -40,6 +40,10 @@ root,
--breakpoint-md: 768px;
--breakpoint-lg: 992px;
--breakpoint-xl: 1200px;
--font-family-sans-serif: -apple-system, BlinkMacSystemFont, "Segoe UI", "Atikinson Hyperlegible Next", "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--font-family-monospace: "Atikinson Hyperlegible Mono", SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--font-family-sans-serif: -apple-system, BlinkMacSystemFont, "Segoe UI",
"Atkinson Hyperlegible Next", "Helvetica Neue", Arial, "Noto Sans",
sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
"Noto Color Emoji";
--font-family-monospace: "Atkinson Hyperlegible Mono", SFMono-Regular, Menlo,
Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}

View File

@@ -38,6 +38,10 @@
--breakpoint-md: 768px;
--breakpoint-lg: 992px;
--breakpoint-xl: 1200px;
--font-family-sans-serif: -apple-system, BlinkMacSystemFont, "Segoe UI", "Atikinson Hyperlegible Next", "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--font-family-monospace: "Atikinson Hyperlegible Mono", SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--font-family-sans-serif: -apple-system, BlinkMacSystemFont, "Segoe UI",
"Atkinson Hyperlegible Next", "Helvetica Neue", Arial, "Noto Sans",
sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
"Noto Color Emoji";
--font-family-monospace: "Atkinson Hyperlegible Mono", SFMono-Regular, Menlo,
Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}

View File

@@ -31,12 +31,16 @@
--warning: #ffaf00;
--danger: #ff6258;
--light: #fbfbfb;
--dark: #252C46;
--dark: #252c46;
--breakpoint-xs: 0;
--breakpoint-sm: 576px;
--breakpoint-md: 768px;
--breakpoint-lg: 992px;
--breakpoint-xl: 1200px;
--font-family-sans-serif: -apple-system, BlinkMacSystemFont, "Segoe UI", "Atikinson Hyperlegible Next", "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--font-family-monospace: "Atikinson Hyperlegible Mono", SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--font-family-sans-serif: -apple-system, BlinkMacSystemFont, "Segoe UI",
"Atkinson Hyperlegible Next", "Helvetica Neue", Arial, "Noto Sans",
sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
"Noto Color Emoji";
--font-family-monospace: "Atkinson Hyperlegible Mono", SFMono-Regular, Menlo,
Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}

View File

@@ -0,0 +1,21 @@
@font-face {
font-family: "Atkinson Hyperlegible Mono";
font-style: italic;
src: url(AtkinsonHyperlegibleMono-Italic-VariableFont_wght.ttf);
}
@font-face {
font-family: "Atkinson Hyperlegible Mono";
src: url(AtkinsonHyperlegibleMono-VariableFont_wght.ttf);
}
@font-face {
font-family: "Atkinson Hyperlegible Next";
font-style: italic;
src: url(AtkinsonHyperlegibleNext-Italic-VariableFont_wght.ttf);
}
@font-face {
font-family: "Atkinson Hyperlegible Next";
src: url(AtkinsonHyperlegibleNext-VariableFont_wght.ttf);
}

View File

@@ -235,12 +235,14 @@
{% for backup in data['backup_list'] %}
<tr>
<td>
{% if data["backup_config"]["backup_type"] != "snapshot" %}
<button class="btn btn-primary download" data-file="{{backup['path']}}">
<i class="fas fa-download" aria-hidden="true"></i>
{{ translate('serverBackups', 'download', data['lang']) }}
</button>
<br>
<br>
{% end %}
<button data-file="{{ backup['path'] }}"
data-backup_location="{{ data['backup_config']['backup_location'] }}"
class="btn btn-danger del_button">
@@ -566,7 +568,7 @@
});
$(".download").click(async function () {
let file = $(".download").data("file");
let file = $(this).data("file");
window.open(`/api/v2/servers/${serverId}/backups/backup/${backup_id}/download/${encodeURIComponent(file)}`, '_blank');
});

View File

@@ -7,6 +7,7 @@
{% block content %}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-select/1.13.10/css/bootstrap-select.min.css">
<link rel="stylesheet" href="/static/assets/css/partial/crafty-webhooks.css">
<div class="content-wrapper">
<!-- Page Title Header Starts-->
@@ -83,10 +84,11 @@
</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>
<label for="body">{{ translate('webhooks', 'webhook_body', data['lang']) }} <br><div class="jinja2"><small>{{translate("webhooks", "jinja2", data['lang'])}} <br>
<a href="https://docs.craftycontrol.com/pages/user-guide/webhooks/#jinja2-dynamic-variables-461" target="_blank">{{translate("webhooks", "documentation", data['lang'])}}</a></small></div></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>

View File

@@ -665,9 +665,6 @@
"userTheme": "Motiv UI",
"uses": "Počet povolených použití (-1==bez omezení)"
},
"validators": {
"passLength": "Heslo je příliš krátké. Minimální délka je 8 znaků"
},
"webhooks": {
"areYouSureDel": "Seš si jistý že chceš smazat tento webhook?",
"areYouSureRun": "Seš si jistý že chceš otestovat tento webhook?",

View File

@@ -32,7 +32,8 @@
"yes": "Ja"
},
"base": {
"doesNotWorkWithoutJavascript": "<strong>Warnung: </strong>Crafty funktioniert nicht richtig, wenn JavaScript nicht aktiviert ist!"
"doesNotWorkWithoutJavascript": "<strong>Warnung: </strong>Crafty funktioniert nicht richtig, wenn JavaScript nicht aktiviert ist!",
"createMFA": "Füge eine Zwei-Faktor-Authentifizierung zu deinem Konto hinzu!"
},
"credits": {
"developmentTeam": "Entwicklungsteam",
@@ -670,7 +671,6 @@
"uses": "Anzahl der erlaubten Verwendungen (-1==Keine Begrenzung)"
},
"validators": {
"passLength": "Passwort zu kurz. Mindestlänge: 8",
"typeInteger": "muss eine Zahl sein.",
"typeList": "muss eine Liste (array) sein ",
"roleManager": "Rollenmanager muss vom Typ Ganzzahl (Manager ID) oder ohne Wert sein",
@@ -715,5 +715,11 @@
"url": "Webhook-URL",
"webhook_body": "Webhook-Inhalt",
"webhooks": "Webhooks"
},
"configJson": {
"allow_nsfw_profile_pictures": "Erlaube Gravatar™ NSFW Profilbilder",
"big_bucket_repo": "Big Bucket™ System URL",
"delete_default_json": "Standard-JSON-Datei beim Starten löschen",
"enable_user_self_delete": "Erlaube Nutzern ihren eigenen Account zu löschen"
}
}

View File

@@ -283,6 +283,8 @@
"finishedPreparing": "We've finished preparing your support logs. Please click download to download",
"logout": "Logout",
"preparingLogs": " Please wait while we prepare your logs... We`ll send a notification when they`re ready. This may take a while for large deployments.",
"restoreFailed": "Backup restore failed. Could not delete files from server directory.",
"restoreSuccess": "Server files restored successfully.",
"schedule_desc": "We detected some or all of your scheduled tasks were not successfully transferred during the upgrade. Please confirm your schedules in the schedules tab.",
"schedule_title": "Schedules Migration Warning",
"supportLogs": "Support Logs"
@@ -788,9 +790,11 @@
"bot_name": "Bot Name",
"color": "Select Color Accent",
"crash_detected": "Server Crashed",
"documentation": "Please reference this documentation for information regarding dynamic variables.",
"edit": "Edit",
"enabled": "Enabled",
"jar_update": "Server Executable Updated",
"jinja2": "Crafty's webhook engine takes advantage of Jinja2 for dynamic message rendering.",
"kill": "Server Killed",
"name": "Name",
"new": "New Webhook",

View File

@@ -680,9 +680,6 @@
"userTheme": "Tema de Interfaz",
"uses": "Número de usos permitidos. (Sin límite: -1)"
},
"validators": {
"passLength": "Contraseña demasiado corta. Longitud mínima: 8"
},
"webhooks": {
"areYouSureDel": "¿Estás seguro de que quieres eliminar este webhook?",
"areYouSureRun": "¿Estás seguro de que quieres probar este webhook?",

View File

@@ -698,7 +698,6 @@
"selfDisable": "Vous ne pouvez pas désactiver votre propre compte"
},
"validators": {
"passLength": "Mot de passe trop court. Longueur minimum : 8",
"2FAerror": "Le code d'authentification multifacteur doit comporter 6 chiffres (ex. : 000000) ou un code de secours de 16 caractères séparés tous les 4 par un tiret (ex. : ABCD-EFGH-IJKL-MNOP)",
"additionalProperties": "L'envoi de propriétés supplémentaires n'est pas autorisé",
"roleManager": "Le gestionnaire de rôle doit être de type entier (ID du gestionnaire) ou None",
@@ -722,7 +721,7 @@
"filesPageLen": "La longueur doit être supérieure à 1 pour cette propriété",
"insufficientPerms": "Erreur de permission : permissions manquantes pour cette ressource",
"mfaName": "La saisie doit être une chaîne de caractères d'au moins 3 caractères pour ce champ",
"numbericPassword": "Mot de passe numérique. Doit contenir au moins 1 caractère alphabétique"
"passProp": "Le mot de passe doit être une chaîne d'au moins 8 caractères."
},
"webhooks": {
"areYouSureDel": "Êtes-vous sûr de vouloir supprimer ce webhook ?",

View File

@@ -664,9 +664,6 @@
"userTheme": "ערכת נושא UI",
"uses": "מספר השימושים המותרים (-1==ללא הגבלה)"
},
"validators": {
"passLength": "סיסמא קצרה מדי. אורך מינימלי: 8"
},
"webhooks": {
"areYouSureDel": "האם אתה בטוח שברצונך למחוק את ה-Webhook הזה?",
"areYouSureRun": "האם אתה בטוח שברצונך לבדוק את ה-Webhook הזה?",

View File

@@ -692,7 +692,6 @@
"totpHeader": "Autenticazione a più fattori"
},
"validators": {
"passLength": "La password è troppo corta. Lunghezza minima: 8",
"enumErr": "Validazione fallita. Valori accettabili includono: ",
"serverExeCommand": "Il comando di esecuzione del server devessere una stringa con lunghezza minima di 1.",
"serverLogPath": "Il percorso del registro del server devessere una stringa con lunghezza minima di 1",

View File

@@ -706,7 +706,6 @@
"uses": "許可されている使用回数 (-1で無制限)"
},
"validators": {
"passLength": "パスワードが短すぎます。最小文字数は8文字です。",
"backupName": "バックアップ名は3文字以上の文字列である必要があります。",
"enumErr": "データの検証に失敗しました。次を含む必要があります: ",
"filesPageLen": "1文字以上である必要があります",

View File

@@ -114,7 +114,6 @@
"selfDisable": "자신의 계정을 비활성화할 수 없습니다"
},
"validators": {
"passLength": "비밀번호가 너무 짧아요! (최소 길이: 8)",
"taskIntervalType": "작업 반복 유형은 다음 중 하나여야 합니다: ",
"typeBool": "'true' 또는 'false' 여야 합니다 (boolean 타입)",
"backupName": "백업 이름은 문자여야 하며 최소 3자 이상이어야 합니다.",
@@ -137,8 +136,7 @@
"2FAerror": "다중 인증 코드는 6자리 숫자(예: 000000)이거나, 16자 문자열을 4글자씩 하이픈(-)으로 구분한 백업 코드(예: ABCD-EFGH-IJKL-MNOP)여야 합니다.",
"totp": "MFA 코드는 6자리 숫자여야 합니다.",
"additionalProperties": "추가 속성을 전달할 수 없습니다",
"mfaName": "속성 입력은 문자열 유형과 3자 이상이어야 합니다",
"numbericPassword": "최소 하나의 알파벳 문자가 필요합니다"
"mfaName": "속성 입력은 문자열 유형과 3자 이상이어야 합니다"
},
"webhooks": {
"trigger": "트리거",

View File

@@ -670,7 +670,6 @@
"uses": "NUMBER OV USES ALLOWED (-1==NO LIMIT)"
},
"validators": {
"passLength": "PASSWRD TOO SMOL. NEEDZ 8 CATZ PLZ",
"backupName": "BACKUP NAME GOTTA BE WURDZ, AT LEAST 3 FISH.",
"enumErr": "OOFS IN VALIDATING. GOOD STUFF INCLUDES: ",
"serverExeCommand": "SERVER EXECUTION COMMAND GOTTA BE WURDZ, AT LEAST 1 FISH LONG.",

View File

@@ -669,7 +669,6 @@
"uses": "Dauzums, cik reizes lietot (-1==Bez Limita)"
},
"validators": {
"passLength": "Parole pārāk īsa. Minimālais Garums: 8",
"backupName": "Dublējuma nosaukumam jābūt tekstam (string) ar minimālo garumu 3.",
"enumErr": "pārbaude neizdevās. Pieņemamie dati ietver: ",
"filesPageLen": "garumam jābūt lielākam par 1 priekš vienības",

View File

@@ -698,7 +698,6 @@
"totpIdReq": "MFA ID vereist voor aanvraag"
},
"validators": {
"passLength": "Wachtwoord te kort. Minimumlengte: 8 tekens",
"backupName": "De naam van de back-up moet een string zijn en een minimale lengte van 3 hebben.",
"enumErr": "validatie mislukt. Accepteerbare gegevens zijn: ",
"filesPageLen": "De lengte moet groter zijn dan 1 voor eigenschap",
@@ -721,8 +720,7 @@
"mfaName": "Invoer moet van het type string zijn en minstens 3 tekens bevatten voor dit veld",
"additionalProperties": "Er mogen geen extra eigenschappen worden meegegeven",
"totp": "MFA-code moet uit 6 cijfers bestaan",
"2FAerror": "Multi-Factor code moet uit 6 cijfers bestaan (bijv. 000000) of een back-upcode van 16 tekens, gescheiden per 4 met een streepje (bijv. ABCD-EFGH-IJKL-MNOP)",
"numbericPassword": "Numeriek wachtwoord. Moet minstens 1 alfabetisch teken bevatten"
"2FAerror": "Multi-Factor code moet uit 6 cijfers bestaan (bijv. 000000) of een back-upcode van 16 tekens, gescheiden per 4 met een streepje (bijv. ABCD-EFGH-IJKL-MNOP)"
},
"webhooks": {
"areYouSureDel": "Weet u zeker dat u deze webhook wilt verwijderen?",

View File

@@ -692,7 +692,6 @@
"backupName": "Back-upnaam moet een string zijn en een minimumlengte van 3 hebben.",
"filesPageLen": "lengte moet groter zijn dan 1 voor eigenschap",
"insufficientPerms": "Toestemmingsfout: Ontbrekende rechten voor deze bron",
"passLength": "Wachtwoord Te Kort. Minimumlengte: 8",
"roleName": "De rolnaam moet een string zijn die groter is dan 1 teken. Deze mag geen van de volgende symbolen bevatten: [], ",
"roleServerId": "De eigenschap van Server-ID moet een string zijn met een minimumlengte van 1",
"roleServerPerms": "Servertoestemmingen moeten een 8-bits string zijn",
@@ -707,7 +706,7 @@
"totp": "MFA-code moet 6 nummers zijn",
"mfaName": "Invoer moet van type String zijn van minstens 3 karakters voor deze eigenschap",
"additionalProperties": "Aanvullende Eigenschappen mogen niet meegestuurd worden",
"numbericPassword": "Numeriek Wachtwoord. Vereist minstens 1 alfabetisch karakter"
"passProp": "Wachtwoord moet tekst zijn met een minimale lengte van 8 karakters."
},
"serverMetrics": {
"zoomHint1": "Om in te zoomen op de grafiek, houdt je de shift-toets ingedrukt en gebruik je vervolgens je scrollwiel.",

View File

@@ -684,9 +684,6 @@
"changePass": "Zmień hasło",
"changeUser": "Zmień nazwę użytkownika"
},
"validators": {
"passLength": "Hasło jest zbyt krótkie. Hasło musi mieć minimum 8 znaków."
},
"webhooks": {
"areYouSureDel": "Usunąć ten webhook?",
"areYouSureRun": "Przetestować ten webhook?",

View File

@@ -698,7 +698,6 @@
"selfDisable": "Вы не можете отключить свою собственную учетную запись"
},
"validators": {
"passLength": "Пароль слишком короткий. Минимальная длина: 8",
"roleManager": "Роль менеджера должна быть типа integer (ID менеджера) или None",
"filesPageLen": "длина свойства должна быть больше 1",
"serverCreateName": "Имя сервера должно быть строкой длиной не менее 2-х символов и не должно содержать: \\ / или # ",
@@ -722,7 +721,7 @@
"totp": "MFA код должен состоять из 6 цифр",
"additionalProperties": "Передача дополнительных свойств запрещена",
"mfaName": "Вводимые данные должны быть строкой и содержать минимум 3 символа для свойства",
"numbericPassword": "Цифровой пароль должен содержать как минимум 1 буквенный символ"
"passProp": "Пароль должен быть строкой и не менее 8 символов."
},
"webhooks": {
"areYouSureDel": "Вы уверены, что хотите удалить этот вебхук?",

View File

@@ -692,7 +692,6 @@
"totpIdReq": "จำเป็นต้องมี TOTP ID เพื่อขอการร้องขอ"
},
"validators": {
"passLength": "รหัสผ่านสั้นเกินไป จำนวนตัวอักขระขั้นต่ำ: 8",
"roleManager": "ผู้จัดการบทบาทต้องเป็นประเภทจำนวนเต็ม (ID ผู้จัดการ) หรือเป็น None",
"roleName": "ชื่อบทบาทต้องเป็นสตริงที่มีความยาวมากกว่า 1 ตัวอักษร และต้องไม่มีสัญลักษณ์ใด ๆ ดังนี้: [ ] , ",
"filesPageLen": "ความยาวต้องมากกว่า 1 สำหรับคุณสมบัติ",

View File

@@ -698,7 +698,6 @@
"selfDisable": "Kendi hesabınızı devre dışı bırakamazsınız"
},
"validators": {
"passLength": "Şifre çok kısa. Şifre en az 8 karakter olmalı.",
"serverLogPath": "Sunucu günlük yolu en az 1 uzunluğundaki bir dize olmalıdır",
"roleName": "Rol adı 1 karakterden büyük bir dize olmalıdır. Ayrıca şu sembollerden herhangi birini içermemelidir: [ ] , ",
"typeList": "liste/dizi türünde olmalıdır ",
@@ -722,7 +721,7 @@
"userName": " dize türünde, tümü KÜÇÜK HARF, en az 4 karakter ve en fazla 20 karakter olmalıdır",
"additionalProperties": "Ek Özelliklerin geçirilmesine izin verilmez",
"mfaName": "Özellik için girdi, dize türünde olmalı ve en az 3 karakter içermelidir",
"numbericPassword": "Sayısal şifre. En az 1 alfabetik karakter içermelidir"
"passProp": ifre en az 8 karakterden oluşan bir dize olmalıdır."
},
"webhooks": {
"areYouSureDel": "Bu webhooku silmek istediğinizden emin misiniz?",

View File

@@ -698,8 +698,6 @@
"selfDisable": "Ти не можеш вимкнути свій аккаунт"
},
"validators": {
"passLength": "Пароль, надто короткий. Мінімальна довжина: 8 символів",
"numbericPassword": "Пароль складається з цифр. Повинен містити хоча одну літеру",
"enumErr": "помилка валідації. Прийнятні дані включають: ",
"insufficientPerms": "Помилка доступу: Відсутній доступ до цього ресурсу",
"roleServerId": "Властивість Server ID має бути рядком з мінімальною довжиною 1",

View File

@@ -698,7 +698,6 @@
"selfDisable": "您不能禁用您自己的账号"
},
"validators": {
"passLength": "密码过短。最短长度8",
"backupName": "备份名称必须为字符串,且最短长度为 3。",
"filesPageLen": "属性的长度必须大于 1",
"typeList": "必须为列表/数组类型 ",
@@ -722,7 +721,7 @@
"totp": "MFA 代码必须为 6 个数字",
"additionalProperties": "不允许传递额外属性",
"mfaName": "输入的属性必须为字符串类型,且最短为 3 个字符",
"numbericPassword": "纯数字密码。需要至少 1 个字母"
"passProp": "密码必须为最短 8 个字符的字符串。"
},
"webhooks": {
"areYouSureDel": "您确定要删除此 webhook 吗?",

View File

@@ -21,6 +21,7 @@ from app.classes.logging.log_formatter import JsonFormatter
console = Console()
helper = Helpers()
first_login = False
# Get the path our application is running on.
if getattr(sys, "frozen", False):
APPLICATION_PATH = os.path.dirname(sys.executable)
@@ -388,6 +389,7 @@ if __name__ == "__main__":
f"through your router/firewall if you would like to be able "
f"to access Crafty remotely."
)
first_login = True
PASSWORD = helper.create_pass()
installer.default_settings(PASSWORD)
with open(
@@ -426,6 +428,7 @@ if __name__ == "__main__":
import_helper = ImportHelpers(helper, file_helper)
controller = Controller(database, helper, file_helper, import_helper)
controller.set_project_root(APPLICATION_PATH)
controller.first_login = first_login
tasks_manager = TasksManager(helper, controller, file_helper)
import3 = Import3(helper, controller)
helper.migration_notifications = get_migration_notifications()

View File

@@ -1,26 +1,26 @@
aiofiles==24.1.0
anyio==4.9.0
apscheduler==3.10.4
argon2-cffi==23.1.0
cached_property==1.5.2
colorama==0.4.6
croniter==1.4.1
cryptography==44.0.1
httpx==0.28.1
jinja2==3.1.6
jsonschema==4.19.1
libgravatar==1.0.4
nh3==0.2.14
orjson==3.9.15
packaging==23.2
peewee==3.13
pillow==10.4.0
prometheus-client==0.17.1
psutil==5.9.5
pyjwt==2.8.0
pyotp==2.9.0
PyYAML==6.0.1
requests==2.32.4
termcolor==1.1
tornado==6.5
tzlocal==5.1
jsonschema==4.19.1
orjson==3.9.15
prometheus-client==0.17.1
pyotp==2.9.0
pillow==10.4.0
httpx==0.28.1
aiofiles==24.1.0
anyio==4.9.0

View File

@@ -3,8 +3,8 @@ sonar.organization=crafty-controller
# This is the name and version displayed in the SonarCloud UI.
sonar.projectName=Crafty 4
sonar.projectVersion=4.5.5
sonar.python.version=3.9, 3.10, 3.11
sonar.projectVersion=4.6.1
sonar.python.version=3.10, 3.11, 3.12
sonar.exclusions=app/migrations/**, app/frontend/static/assets/vendors/**
# Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows.