Compare commits

...

17 Commits

Author SHA1 Message Date
32f5b0d5e9 new Theme
Some checks failed
continuous-integration/drone/push Build is failing
2024-01-30 16:52:34 +01:00
53a6485a6e Maketank Theme migration
Some checks failed
continuous-integration/drone/push Build is failing
2024-01-30 13:52:59 +01:00
f2643ac887 env test 3
Some checks failed
continuous-integration/drone/push Build is failing
2023-12-13 12:46:20 +01:00
e37687a85d env test 3
Some checks failed
continuous-integration/drone/push Build is failing
2023-12-13 12:44:29 +01:00
ccbc3286a5 env test 2
Some checks failed
continuous-integration/drone/push Build is failing
2023-12-13 12:39:58 +01:00
929a562324 env test 2
Some checks failed
continuous-integration/drone/push Build is failing
2023-12-13 12:36:50 +01:00
3704cf6621 env test 2
Some checks failed
continuous-integration/drone/push Build is failing
2023-12-13 12:35:09 +01:00
10238a1466 env test
All checks were successful
continuous-integration/drone/push Build is passing
2023-12-12 14:04:03 +01:00
9002ddf4a2 env test
Some checks reported errors
continuous-integration/drone/push Build encountered an error
2023-12-12 14:03:10 +01:00
8a2de5a44a env test
Some checks reported errors
continuous-integration/drone/push Build encountered an error
2023-12-12 13:59:34 +01:00
96c0af18dd npm and compose
Some checks failed
continuous-integration/drone/push Build is failing
2023-12-12 13:50:23 +01:00
5bb228ce78 npm and compose
Some checks failed
continuous-integration/drone/push Build is failing
2023-12-12 13:48:08 +01:00
804128280c npm and compose 2023-12-12 13:47:32 +01:00
5b8e918f75 ssh test
All checks were successful
continuous-integration/drone/push Build is passing
2023-12-12 13:45:23 +01:00
0e3e83d184 ssh test
Some checks reported errors
continuous-integration/drone/push Build was killed
2023-12-12 10:51:21 +01:00
8ced61c6aa bogus edit for pipeline trigger
Some checks reported errors
continuous-integration/drone/push Build was killed
2023-12-11 15:31:39 +01:00
29a2ab7567 2.0 upgrade test first
Some checks reported errors
continuous-integration/drone/push Build was killed
2023-12-07 12:39:20 +01:00
124 changed files with 9300 additions and 6262 deletions

53
.drone.yml Normal file
View File

@@ -0,0 +1,53 @@
kind: pipeline
name: deploy-froxlor
type: docker
platform:
os: linux
arch: arm64
trigger:
branch:
- upgrade-2.0
event:
include:
- push
environment:
DEPLOY_HOST: rechner.maketank.net
DEPLOY_DIR: ~/froxlor-test
steps:
- name: deploy
image: cr.wks/drone/drone-rsync:latest
settings:
hosts: ["rechner02.maketank.net"]
source: ./
target: ~/froxlor-test
user: www-data
exclude: ['vendor', '.git*', '*drone.yml', '.settings', '.buildpath', '.editorconfig', '.project', '.travis.yml', 'node_modules']
args: '-v --delete'
log_level: quiet
key:
from_secret: ssh-www-data-maketank-rsa
command_timeout: 10m
- name: compose
image: appleboy/drone-ssh
settings:
host:
- rechner02.maketank.net
username: www-data
key:
from_secret: ssh-www-data-maketank-rsa
script:
- cd ~/froxlor-test && composer install --no-dev
- name: npm
image: appleboy/drone-ssh
settings:
host:
- rechner02.maketank.net
username: www-data
key:
from_secret: ssh-www-data-maketank-rsa
script:
- cd ~/froxlor-test && npm install && npm run build

1
.gitignore vendored
View File

@@ -18,7 +18,6 @@ img/
vendor/
node_modules/
fonts/
templates/*
!templates/index.html
!templates/Froxlor/
templates/Froxlor/assets/mix-manifest.json

10455
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,5 @@
<?php
chmod('/app//bin/froxlor-cli', 0755);
// re-create cron.d configuration file
exec('/app//bin/froxlor-cli froxlor:cron -r 99');
exit;

View File

@@ -0,0 +1,192 @@
:root,[data-bs-theme=light] {
--bs-blue: #0d6efd;
--bs-indigo: #6610f2;
--bs-purple: #6f42c1;
--bs-pink: #d63384;
--bs-red: #dc3545;
--bs-orange: #fd7e14;
--bs-yellow: #ffc107;
--bs-green: #198754;
--bs-teal: #20c997;
--bs-cyan: #0dcaf0;
--bs-black: #000;
--bs-white: #fff;
--bs-gray: #6c757d;
/* --bs-gray-dark:#343a40; */
--bs-gray-100: #f8f9fa;
--bs-gray-200: #e9ecef;
--bs-gray-300: #dee2e6;
--bs-gray-400: #ced4da;
--bs-gray-500: #adb5bd;
--bs-gray-600: #6c757d;
--bs-gray-700: #495057;
--bs-gray-800: #343a40;
--bs-gray-900: #212529;
--bs-primary: #1872a2;
--bs-secondary: #6c757d;
--bs-success: #059669;
--bs-info: #0e5380;
--bs-warning: #fbbf24;
--bs-danger: #be123c;
--bs-light: #f8f9fa;
--bs-dark: #212529;
--bs-primary-rgb: 180,170,160;
--bs-secondary-rgb: 218,212,208;
--bs-success-rgb: 5,150,105;
--bs-info-rgb: 14,83,128;
--bs-warning-rgb: 251,191,36;
--bs-danger-rgb: 190,18,60;
--bs-light-rgb: 248,249,250;
--bs-dark-rgb: 218,212,208;
--bs-primary-text-emphasis: #0a2e41;
--bs-secondary-text-emphasis: #2b2f32;
--bs-success-text-emphasis: #023c2a;
--bs-info-text-emphasis: #062133;
--bs-warning-text-emphasis: #644c0e;
--bs-danger-text-emphasis: #4c0718;
--bs-light-text-emphasis: #495057;
--bs-dark-text-emphasis: #495057;
--bs-primary-bg-subtle: #d1e3ec;
--bs-secondary-bg-subtle: #e2e3e5;
--bs-success-bg-subtle: #cdeae1;
--bs-info-bg-subtle: #cfdde6;
--bs-warning-bg-subtle: #fef2d3;
--bs-danger-bg-subtle: #f2d0d8;
--bs-light-bg-subtle: #fcfcfd;
--bs-dark-bg-subtle: #ced4da;
--bs-primary-border-subtle: #a3c7da;
--bs-secondary-border-subtle: #c4c8cb;
--bs-success-border-subtle: #9bd5c3;
--bs-info-border-subtle: #9fbacc;
--bs-warning-border-subtle: #fde5a7;
--bs-danger-border-subtle: #e5a0b1;
--bs-light-border-subtle: #e9ecef;
--bs-dark-border-subtle: #adb5bd;
--bs-white-rgb: 255,255,255;
--bs-black-rgb: 0,0,0;
--bs-font-sans-serif: system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans","Liberation Sans",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
--bs-font-monospace: SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
--bs-gradient: linear-gradient(180deg,hsla(0,0%,100%,.15),hsla(0,0%,100%,0));
--bs-root-font-size: 16px;
--bs-body-font-family: var(--bs-font-sans-serif);
--bs-body-font-size: 1rem;
--bs-body-font-weight: 400;
--bs-body-line-height: 1.5;
--bs-body-color: #343a40;
--bs-body-color-rgb: 52,58,64;
--bs-body-bg: #f8f9fa;
--bs-body-bg-rgb: 248,249,250;
--bs-emphasis-color: #000;
--bs-emphasis-color-rgb: 0,0,0;
--bs-secondary-color: rgba(52,58,64,.75);
--bs-secondary-color-rgb: 52,58,64;
--bs-secondary-bg: #e9ecef;
--bs-secondary-bg-rgb: 233,236,239;
--bs-tertiary-color: rgba(52,58,64,.5);
--bs-tertiary-color-rgb: 52,58,64;
--bs-tertiary-bg: #f8f9fa;
--bs-tertiary-bg-rgb: 248,249,250;
--bs-heading-color: inherit;
--bs-link-color: #1872a2;
--bs-link-color-rgb: 24,114,162;
--bs-link-decoration: underline;
--bs-link-hover-color: #135b82;
--bs-link-hover-color-rgb: 19,91,130;
--bs-code-color: #d63384;
--bs-highlight-color: #343a40;
--bs-highlight-bg: #fff3cd;
--bs-border-width: 1px;
--bs-border-style: solid;
--bs-border-color: #dee2e6;
--bs-border-color-translucent: rgba(0,0,0,.175);
--bs-border-radius: 0.375rem;
--bs-border-radius-sm: 0.25rem;
--bs-border-radius-lg: 0.5rem;
--bs-border-radius-xl: 1rem;
--bs-border-radius-xxl: 2rem;
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
--bs-border-radius-pill: 50rem;
--bs-box-shadow: 0 0.5rem 1rem rgba(0,0,0,.15);
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0,0,0,.075);
--bs-box-shadow-lg: 0 1rem 3rem rgba(0,0,0,.175);
--bs-box-shadow-inset: inset 0 1px 2px rgba(0,0,0,.075);
--bs-focus-ring-width: 0.25rem;
--bs-focus-ring-opacity: 0.25;
--bs-focus-ring-color: rgba(24,114,162,.25);
--bs-form-valid-color: #059669;
--bs-form-valid-border-color: #059669;
--bs-form-invalid-color: #be123c;
--bs-form-invalid-border-color: #be123c
}
.navbar .navbar-brand {
background: rgb(180,170,160);
flex-shrink: 0;
margin-right: 0;
width: 256px;
}
.sidebar>.nav>.nav-item>.nav-link:not(.collapsed) {
background: rgb(180,170,160);
border-left: 3px solid #1872a2;
padding-left: calc(1rem - 3px);
}
.text-light {
--bs-text-opacity: 1;
color: rgba(var(--bs-gray-900),var(--bs-text-opacity))!important;
}
.sidebar>.nav>.nav-item>.collapse, .sidebar>.nav>.nav-item>.collapsing {
background: rgb(160,140,120);
color: rgba(var(--bs-light),var(--bs-text-opacity))!important;
}
img.header-logo {
width: 80%;
height: 80%;
}
.btn-primary {
--bs-btn-color: #fff;
--bs-btn-bg: rgb(180,170,160);
--bs-btn-border-color: rgb(160,140,120);
--bs-btn-hover-color: #000;
--bs-btn-hover-bg: #fff;
--bs-btn-hover-border-color: rgb(160,140,120);
--bs-btn-focus-shadow-rgb: 59,135,176;
--bs-btn-active-color: #fff;
--bs-btn-active-bg: #135b82;
--bs-btn-active-border-color: #12567a;
--bs-btn-active-shadow: inset 0 3px 5px rgba(0,0,0,.125);
--bs-btn-disabled-color: #fff;
--bs-btn-disabled-bg: #1872a2;
--bs-btn-disabled-border-color: #1872a2;
}
.btn-outline-primary {
--bs-btn-color: #000;
--bs-btn-border-color: rgb(160,140,120);
--bs-btn-hover-color: #fff;
--bs-btn-hover-bg: rgb(180,170,160);
--bs-btn-hover-border-color: rgb(160,140,120);
--bs-btn-focus-shadow-rgb: 24,114,162;
--bs-btn-active-color: #fff;
--bs-btn-active-bg: #1872a2;
--bs-btn-active-border-color: #1872a2;
--bs-btn-active-shadow: inset 0 3px 5px rgba(0,0,0,.125);
--bs-btn-disabled-color: #1872a2;
--bs-btn-disabled-bg: transparent;
--bs-btn-disabled-border-color: #1872a2;
--bs-gradient: none;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,45 @@
/*!
* Bootstrap v5.3.2 (https://getbootstrap.com/)
* Copyright 2011-2023 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
/*!
* @kurkle/color v0.2.1
* https://github.com/kurkle/color#readme
* (c) 2022 Jukka Kurkela
* Released under the MIT License
*/
/*!
* Chart.js v3.9.1
* https://www.chartjs.org
* (c) 2022 Chart.js Contributors
* Released under the MIT License
*/
/*!
* Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2023 Fonticons, Inc.
*/
/*!
* jQuery JavaScript Library v3.7.1
* https://jquery.com/
*
* Copyright OpenJS Foundation and other contributors
* Released under the MIT license
* https://jquery.org/license
*
* Date: 2023-08-28T13:37Z
*/
/*!
* jQuery Validation Plugin v1.20.0
*
* https://jqueryvalidation.org/
*
* Copyright (c) 2023 Jörn Zaefferer
* Released under the MIT license
*/

View File

@@ -0,0 +1,13 @@
{
"/js/main.js": "/js/main.js?id=29451cc889431e973473738b93e6a626",
"/css/dark.css": "/css/dark.css?id=f965da97d900604705c3762bb9cd645a",
"/css/main.css": "/css/main.css?id=cbda89c88526530a66fe1e0d46a63a22",
"/webfonts/fa-brands-400.ttf": "/webfonts/fa-brands-400.ttf?id=69e5d8e4e818f05fd882cceb758d1eba",
"/webfonts/fa-brands-400.woff2": "/webfonts/fa-brands-400.woff2?id=189b85e9c72c6f75e464c3f58a6707cf",
"/webfonts/fa-regular-400.ttf": "/webfonts/fa-regular-400.ttf?id=ed4c23399d1013809882e90bfe396d1b",
"/webfonts/fa-regular-400.woff2": "/webfonts/fa-regular-400.woff2?id=be75b1958ae0da55e1eed562d9b7713d",
"/webfonts/fa-solid-900.ttf": "/webfonts/fa-solid-900.ttf?id=dfdc7801582dd0d20ea75faa3b96c296",
"/webfonts/fa-solid-900.woff2": "/webfonts/fa-solid-900.woff2?id=a0feb384c3c6071947a49708f2b0bc85",
"/webfonts/fa-v4compatibility.ttf": "/webfonts/fa-v4compatibility.ttf?id=e24ec0b8661f7fa333b29444df39e399",
"/webfonts/fa-v4compatibility.woff2": "/webfonts/fa-v4compatibility.woff2?id=e11465c0eff0549edd4e8ea6bbcf242f"
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,39 @@
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="robots" content="noindex, nofollow, noarchive"/>
<meta name="googlebot" content="nosnippet"/>
<link rel="icon" type="image/x-icon" href="{{ basehref|default('') }}templates/Froxlor/assets/img/icon.png">
{% if csrf_token %}<meta name="csrf-token" content="{{ csrf_token }}" />{% endif %}
<!-- CSS -->
{% if theme_css is empty %}
<link href="{{ basehref|default('') }}{{ mix('templates/Froxlor/assets/css/main.css') }}" rel="stylesheet" type="text/css" />
{% else %}
{{ theme_css|raw }}
{% endif %}
{% block custom_css %}{% endblock %}
<!-- Scripts -->
{% if theme_js is empty %}
<script type="text/javascript" src="{{ basehref|default('') }}{{ mix('templates/Froxlor/assets/js/main.js') }}"></script>
{% else %}
{{ theme_js|raw }}
{% endif %}
{% block custom_js %}{% endblock %}
<title>Froxlor{% if page_title %} | {{ page_title }}{% endif %}</title>
</head>
<body class="min-vh-100 d-flex flex-column">
{% block navigation %}{% endblock %}
{% block body %}
<div class="container-fluid">
{% block content %}{% endblock %}
{{ include('Froxlor/footer.html.twig') }}
</div>
{% endblock %}
</body>
</html>

View File

@@ -0,0 +1,35 @@
{
"variants": {
"default": {
"img": {
"login": "logo.svg",
"ui": "logo_white.png"
},
"css": [
"main.css",
"custom.css"
],
"js": [
"main.js",
"apikey.js"
],
"description": "Default"
},
"dark": {
"img": {
"login": "logo.svg",
"ui": "logo_white.png"
},
"css": [
"dark.css",
"custom.css"
],
"js": [
"main.js",
"apikey.js"
],
"description": "Darkmode"
}
},
"author": "Maketank"
}

View File

@@ -0,0 +1,23 @@
<footer class="text-center mb-3">
<span>
<img src="{{ basehref|default("") }}templates/Froxlor/assets/img/logo_grey.png" alt="Froxlor"/>
{% if install_mode is not defined %}
{% if (get_setting('admin.show_version_login') == '1'
and area == 'login') or (area != 'login'
and get_setting('admin.show_version_footer') == '1') %}
{{ call_static('\\Froxlor\\Froxlor', 'getFullVersion') }}
{% endif %}
{% endif %}
&copy; 2009-{{ "now"|date("Y") }} by <a href="https://www.froxlor.org/" rel="external" target="_blank">the Froxlor Team</a><br>
{% if install_mode is not defined %}
{% if (get_setting('panel.imprint_url') != '') %}<a href="{{ get_setting('panel.imprint_url') }}" target="_blank" class="footer-link">{{ lng('imprint') }}</a>{% endif %}
{% if (get_setting('panel.terms_url') != '') %}<a href="{{ get_setting('panel.terms_url') }}" target="_blank" class="footer-link">{{ lng('terms') }}</a>{% endif %}
{% if (get_setting('panel.privacy_url') != '') %}<a href="{{ get_setting('panel.privacy_url') }}" target="_blank" class="footer-link">{{ lng('privacy') }}</a>{% endif %}
{% endif %}
</span>
{% if lng('translator') %}
<br/>
<small class="mt-3">{{ lng('panel.translator') }}: {{ lng('translator') }}</small>
{% endif %}
</footer>

View File

@@ -0,0 +1,56 @@
{% macro form(form_data, formaction, title = "", hiddenid = "", nosubmit = false, idprefix = "") %}
{% import "Froxlor/form/formfields.html.twig" as formfields %}
<form action="{{ formaction|default("") }}" {% if form_data.id is defined %}id="{{ form_data.id }}"{% endif %} method="post" enctype="multipart/form-data" class="form">
{% for sid,section in form_data.sections %}
{% if section.visible is not defined or (section.visible is defined and section.visible == true) %}
<div class="card mb-3" id="{{ idprefix }}{{ sid }}">
{% if section.title is not empty %}
<div class="card-header">
{% if section.image is not empty %}
<i class="{{ section.image }}"></i>
{% endif %}
{{ section.title }}
</div>
{% endif %}
<div class="formfields">
{% for id,field in section.fields %}
{{ formfields.fieldrow(id, field) }}
{% endfor %}
</div>
</div>
{% endif %}
{% endfor %}
{% if nosubmit == false %}
<!-- submit buttons -->
<div>
<input type="hidden" name="csrf_token" value="{{ csrf_token }}"/>
{% if hiddenid is not empty %}
<input type="hidden" name="id" value="{{ hiddenid }}"/>
{% endif %}
<input type="hidden" name="page" value="{{ page }}"/>
<input type="hidden" name="action" value="{{ action }}"/>
<input type="hidden" name="send" value="send"/>
<div class="col-12 text-center mb-2 d-grid gap-2 d-md-block">
{% if form_data.buttons is defined and form_data.buttons is iterable %}
{% for btn in form_data.buttons %}
<button type="{{ btn.type|default("submit") }}" class="btn btn-lg {{ btn.class|default(" btn-primary") }}">{{ btn.label }}</button>
{% endfor %}
{% else %}
<button type="reset" class="btn btn-lg btn-outline-secondary me-md-3">{{ lng('panel.reset') }}</button>
<button type="submit" class="btn btn-lg btn-primary">{{ lng('panel.save') }}</button>
{% endif %}
</div>
</div>
{% endif %}
<span class="text-danger">*</span> {{ lng('panel.mandatoryfield') }}
</form>
{# add translation for custom validations #}
{% if form_data.id is defined and form_data.id in ['customer_add', 'customer_edit', 'domain_add', 'domain_edit'] %}
<script>$(function() { $.extend($.validator.messages, {required: "{{ lng('error.requiredfield') }}"}) });</script>
{% endif %}
{% endmacro %}

View File

@@ -0,0 +1,250 @@
{% macro fieldrow(id, field, norow = false, nohide = false, em = false) %}
{% if field.visible is not defined or (field.visible is defined and field.visible) or nohide == true %}
{% if norow == false and (field.type != 'hidden' or (field.type == 'hidden' and field.display is defined and field.display is not empty)) %}
<div class="row g-0 formfield d-flex align-items-center">
{% if field.prior_infotext is defined and field.prior_infotext|length > 0 %}
<h5>{{ field.prior_infotext }}</h5>
{% endif %}
{% if field.label is iterable %}
<label for="{{ id }}" class="col-sm-6 col-form-label pe-3">
{% if em %}
<mark>
{% endif %}
{{ field.label.title|raw }}
{% if field.mandatory is defined and field.mandatory %}
<span class="text-danger">*</span>
{% endif %}
{% if em %}
</mark>
{% endif %}
{% if field.label.description is defined and field.label.description is not empty %}<br><small>{{ field.label.description|raw }}</small>
{% endif %}
{% if field.requires_reconf is defined and field.requires_reconf is not empty %}
<div class="bg-info bg-opacity-25 rounded p-2 mt-2 d-flex align-items-center" role="alert">
<i class="fa-solid fa-circle-exclamation me-2"></i><p class="mb-0">{{ lng('serversettings.requires_reconfiguration', [field.requires_reconf|join(', ')])|raw }}</p>
</div>
{% endif %}
</label>
{% else %}
<label for="{{ id }}" class="col-sm-6 col-form-label pe-3">
{% if em %}
<mark>
{% endif %}
{{ field.label|raw }}
{% if field.mandatory is defined and field.mandatory %}
<span class="text-danger">*</span>
{% endif %}
{% if em %}
</mark>
{% endif %}
{% if field.desc is defined and field.desc is not empty %}<br><small>{{ field.desc|raw }}</small>
{% endif %}
{% if field.requires_reconf is defined and field.requires_reconf is not empty %}
<div class="bg-info bg-opacity-25 rounded p-2 mt-2 d-flex align-items-center" role="alert">
<i class="fa-solid fa-circle-exclamation me-2"></i><p class="mb-0">{{ lng('serversettings.requires_reconfiguration', [field.requires_reconf|join(', ')])|raw }}</p>
</div>
{% endif %}
</label>
{% endif %}
<div class="col-sm-6">
{% endif %}
{% if field.type == 'text' or field.type == 'password' or field.type == 'number' or field.type == 'file' or field.type == 'email' or field.type == 'url' or field.type == 'hidden' or field.type == 'date' or field.type == 'datetime-local' %}
{{ _self.input(id, field) }}
{% elseif field.type == 'textul' %}
{{ _self.input_ul(id, field) }}
{% elseif field.type == 'checkbox' %}
{{ _self.bool(id, field) }}
{% elseif field.type == 'checkrequired' %}
{{ _self.chk_required(id, field) }}
{% elseif field.type == 'select' %}
{{ _self.select(id, field) }}
{% elseif field.type == 'textarea' %}
{{ _self.textarea(id, field) }}
{% elseif field.type == 'label' %}
{{ _self.plain(id, field) }}
{% elseif field.type == 'link' %}
{{ _self.link(id, field) }}
{% elseif field.type == 'itemlist' %}
{{ _self.itemlist(id, field) }}
{% elseif field.type == 'infotext' %}
{{ _self.infotext(id, field) }}
{% elseif field.type == 'image' %}
{{ _self.image(id, field) }}
{% else %}
<div class="alert alert-warning" role="alert">Unknown field-type
{{ field.type }}</div>
{% endif %}
{% if field.note is defined and field.note is not empty %}
<small class="text-info">{{ field.note|raw }}</small>
{% endif %}
{% if norow == false and (field.type != 'hidden' or (field.type == 'hidden' and field.display is defined and field.display is not empty)) %}
</div>
</div>
{% endif %}
{% endif %}
{% endmacro %}
{# installation specific format #}
{% macro field(id, field, norow = true, nohide = false, em = false) %}
{% if field.type == 'checkbox' %}
<div class="form-check form-switch mb-3">
<input type="hidden" value="0" name="{{ id }}"/>
<input type="checkbox" {% if (field.visible is defined and field.visible == false) or (field.disabled is defined and field.disabled == true) %} disabled {% endif %} value="{{ field.value }}" id="{{ id }}" name="{{ id }}" class="form-check-input {% if field.valid is defined and field.valid == false %}is-invalid{% endif %}" {% if field.checked is defined and field.checked == 1 %} checked="checked" {% endif %}>
<label for="{{ id }}" class="form-check-label">{{ field.label|raw }}</label>
</div>
{% elseif field.type == 'hidden' %}
{{ _self.fieldrow(id, field, norow, nohide, em) }}
{% else %}
<div class="form-floating mb-3">
{{ _self.fieldrow(id, field, norow, nohide, em) }}
<label for="{{ id }}" class="form-label">{{ field.label|raw }}</label>
</div>
{% endif %}
{% endmacro %}
{% macro bool(id, field) %}
{% if field.is_array is defined and field.is_array == 1 and field.values is not empty %}
{% for subfield in field.values %}
<div class="form-check form-switch">
<input type="checkbox" {% if (field.visible is defined and field.visible == false) or (field.disabled is defined and field.disabled == true) %} disabled {% endif %} value="{{ subfield.value }}" name="{{ id }}[]" class="form-check-input" {% if field.value is defined and subfield.value in field.value %} checked="checked" {% endif %} {% if field.mandatory is defined and field.mandatory %} required {% endif %}>
<label class="form-check-label">
{{ subfield.label|raw }}
</label>
</div>
{% endfor %}
{% else %}
<div class="form-check form-switch">
<input type="hidden" value="0" name="{{ id }}"/>
<input type="checkbox" {% if (field.visible is defined and field.visible == false) or (field.disabled is defined and field.disabled == true) %} disabled {% endif %} value="{{ field.value }}" id="{{ id }}" name="{{ id }}" class="form-check-input {% if field.valid is defined and field.valid == false %}is-invalid{% endif %}" {% if field.checked is defined and field.checked == 1 %} checked="checked" {% endif %}>
</div>
{% endif %}
{% endmacro %}
{% macro chk_required(id, field) %}
<div class="form-check form-switch">
<input type="checkbox" value="{{ field.value }}" id="{{ id }}" name="{{ id }}" class="form-check-input" {% if field.mandatory is defined and field.mandatory == 1 %} required {% endif %} />
</div>
{% endmacro %}
{% macro infotext(id, field) %}
{% if field.next_to is defined %}
<div class="input-group">
{% endif %}
<span {% if field.classes is defined %} class="{{ field.classes }}" {% endif %}>{{ field.value|raw }}</span>
{% if field.next_to is defined %}
{% for nid, nfield in field.next_to %}
{% if nfield.next_to_prefix is defined %}
<span class="input-group-text">{{ nfield.next_to_prefix }}</span>
{% endif %}
{{ _self.fieldrow(nid, nfield, true) }}
{% endfor %}
</div>
{% endif %}
{% endmacro %}
{% macro plain(id, field) %}
<input type="text" readonly class="form-control-plaintext" id="{{ id }}" name="{{ id }}" value="{{ field.value|raw }}">
{% if field.next_to is defined %}
{% for nid, nfield in field.next_to %}
{% if nfield.next_to_prefix is defined %}
<span class="input-group-text">{{ nfield.next_to_prefix }}</span>
{% endif %}
{{ _self.fieldrow(nid, nfield, true) }}
{% endfor %}
{% endif %}
{% endmacro %}
{% macro input(id, field) %}
{% if field.next_to is defined %}
<div class="input-group">
{% endif %}
<input type="{{ field.type }}" {% if field.visible is defined and field.visible == false %} disabled {% endif %} {% if field.type == 'number' and field.min is defined %} min="{{ field.min }}" {% endif %} {% if field.type == 'number' and field.max is defined %} max="{{ field.max }}" {% endif %} {% if field.type != 'number' and field.maxlength is defined %} maxlength="{{ field.maxlength }}" {% endif %} id="{{ id }}" name="{{ id }}" value="{{ field.value|raw }}" class="form-control {% if field.valid is defined and field.valid == false %}is-invalid{% endif %}" {% if field.mandatory is defined and field.mandatory %} required {% endif %} {% if field.readonly is defined and field.readonly %} readonly {% endif %} {% if field.autocomplete is defined %} autocomplete="{{ field.autocomplete }}" {% endif %} {% if field.placeholder is defined %} placeholder="{{ field.placeholder }}" {% endif %} {% if field.type == 'file' and field.accept is defined %} accept="{{ field.accept }}" {% endif %} {% if field.pattern is defined %} pattern="{{ field.pattern }}" {% endif %}/>
{% if field.type == 'hidden' and field.display is defined %}
<input type="text" readonly class="form-control-plaintext" value="{{ field.display|raw }}">
{% endif %}
{% if field.next_to is defined %}
{% for nid, nfield in field.next_to %}
{% if nfield.next_to_prefix is defined %}
<span class="input-group-text">{{ nfield.next_to_prefix }}</span>
{% endif %}
{{ _self.fieldrow(nid, nfield, true) }}
{% endfor %}
</div>
{% endif %}
{% endmacro %}
{% macro image(id, field) %}
{% if field.value is not empty %}
<img src="{{ field.value }}" alt="Current Image" class="field-image-preview"><br>
<div class="form-check form-switch mb-2">
<input type="checkbox" value="1" name="{{ id }}_delete" class="form-check-input">
<label class="form-check-label">
{{ lng('panel.image_field_delete') }}
</label>
</div>
{% endif %}
{% set field = field|merge({'type':'file'}) %}
{{ _self.input(id, field) }}
{% endmacro %}
{% macro input_ul(id, field) %}
{% set max = "" %}
{% if field.maxlength is defined %}
{% for i in 1..field.maxlength %}
{% set max = max ~ "9" %}
{% endfor %}
{% endif %}
<div class="input-group">
<input type="number" min="0" {% if max is not empty %} max="{{ max }}" {% endif %} id="{{ id }}" name="{{ id }}" value="{% if field.value >= 0 %}{{ field.value }}{% endif %}" class="form-control {% if field.valid is defined and field.valid == false %}is-invalid{% endif %}" {% if field.mandatory is defined and field.mandatory %} required {% endif %} {% if field.readonly is defined and field.readonly %} readonly {% endif %} {% if field.autocomplete is defined %} autocomplete="{{ field.autocomplete }}" {% endif %} {% if field.placeholder is defined %} placeholder="{{ field.placeholder }}" {% endif %}/>
<div class="input-group-text">
<input class="form-check-input mt-0" type="checkbox" name="{{ id }}_ul" value="1" {% if field.value == -1 %} checked="checked" {% endif %}>
</div>
</div>
{% endmacro %}
{% macro select(id, field) %}
{% if field.next_to is defined %}
<div class="input-group">
{% endif %}
<select {% if field.visible is defined and field.visible == false %} disabled {% endif %} class="form-select {% if field.valid is defined and field.valid == false %}is-invalid{% endif %}" name="{{ id }}{% if field.select_mode is defined and field.select_mode == 'multiple' %}[]{% endif %}" id="{{ id }}" {% if field.mandatory is defined and field.mandatory %} required {% endif %} {% if field.select_mode is defined and field.select_mode == 'multiple' %} multiple="multiple" {% endif %}{% if field.readonly is defined and field.readonly %} readonly {% endif %}>
{% for val,txt in field.select_var %}
<option value="{{ val }}" {% if field.selected is defined and ((field.selected is not iterable and field.selected == val) or (field.selected is iterable and val in field.selected|keys)) %} selected="selected" {% endif %}>{{ txt|raw }}</option>
{% endfor %}
</select>
{% if field.next_to is defined %}
{% for nid, nfield in field.next_to %}
{% if nfield.next_to_prefix is defined %}
<span class="input-group-text">{{ nfield.next_to_prefix }}</span>
{% endif %}
{{ _self.fieldrow(nid, nfield, true) }}
{% endfor %}
</div>
{% endif %}
{% endmacro %}
{% macro textarea(id, field) %}
<textarea {% if field.visible is defined and field.visible == false %} disabled {% endif %} rows="{{ field.rows|default('12') }}" cols="{{ field.cols|default('60') }}" id="{{ id }}" name="{{ id }}" class="form-control {% if field.valid is defined and field.valid == false %}is-invalid{% endif %}" {% if field.mandatory is defined and field.mandatory %} required {% endif %} {% if field.readonly is defined and field.readonly %} readonly {% endif %} {% if field.placeholder is defined %} placeholder="{{ field.placeholder }}" {% endif %} {% if field.style is defined %} style="{{ field.style }}" {% endif %}>{{ field.value|raw }}</textarea>
{% endmacro %}
{% macro link(id, field) %}
<a href="{{ field.href|raw }}" class="{{ field.classes }}">{{ field.label|raw }}</a>
{% endmacro %}
{% macro itemlist(id, field) %}
{% if field.values is not empty %}
{% for value in field.values %}
<p>{{ value.item|raw }}
{% if value.href is defined and value.href is not empty %}
{{ _self.link(id, value) }}
{% endif %}
</p>
{% endfor %}
{% endif %}
{% if field.next_to is defined %}
{% for nid, nfield in field.next_to %}
{{ _self.fieldrow(nid, nfield, true) }}
{% endfor %}
{% endif %}
{% endmacro %}

View File

View File

@@ -0,0 +1,37 @@
{% extends "Froxlor/userarea.html.twig" %}
{% block content %}
<form action="{{ action|default("") }}" method="post" enctype="application/x-www-form-urlencoded" class="form">
<div class="alert alert-warning" role="alert">
<h4 class="alert-heading">{{ lng('panel.security_question') }}</h4>
<p>{{ question|raw }}</p>
{% if with_checkbox is defined and with_checkbox is iterable %}
{% if with_checkbox.show %}
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="delete_userfiles" name="delete_userfiles" value="1">
<label class="form-check-label" for="delete_userfiles">{{ with_checkbox.chk_text|raw }}</label>
</div>
{% else %}
<input type="hidden" name="delete_userfiles" value="0"/>
{% endif %}
{% endif %}
<p>
<input type="hidden" name="csrf_token" value="{{ csrf_token }}"/>
<input type="hidden" name="send" value="send"/>
{% for id,field in url_params %}
<input type="hidden" name="{{ id }}" value="{{ field }}"/>
{% endfor %}
<button class="btn btn-danger" type="submit" name="submitbutton">{{ lng('panel.yes') }}</button>&nbsp;
{% if back_link is defined and back_link is iterable and back_link|length > 0 %}
<a href="{{ linker(back_link) }}" class="btn btn-secondary">{{ lng('panel.no') }}</a>
{% else %}
<a href="javascript:history.back(-1)" class="btn btn-secondary">{{ lng('panel.no') }}</a>
{% endif %}
</p>
</div>
</form>
{% endblock %}

View File

View File

@@ -0,0 +1,89 @@
<!-- language select -->
<form action="{{ pagecontent.form.formaction }}" method="get">
<div class="row mb-3">
<label for="language" class="col-sm-4 col-form-label">{{ lng('install.language') }}</label>
<div class="col-sm-8">
<select class="form-select" id="language" name="language">
{% for lngfile,lngname in pagecontent.form.languages %}
<option value="{{ lngfile }}" {% if lngfile == pagecontent.form.activelang %} selected="selected" {% endif %}>{{ lngname }}</option>
{% endfor %}
</select>
</div>
</div>
<aside class="text-end">
<input type="hidden" name="check" value="1"/>
<button class="btn btn-sm btn-primary" type="submit" name="chooselang">{{ lng('install.lngbtn_go') }}</button>
</aside>
</form>
<!-- main install form -->
<div class="alert alert-primary mt-md-3" role="alert">{{ lng('install.welcometext')|raw }}</div>
{% if pagecontent.form.result is not empty %}
<div class="alert alert-warning" role="alert">
{% for emsg in pagecontent.form.result %}
<p>{{ emsg }}</p>
{% endfor %}
</div>
{% endif %}
<form action="{{ pagecontent.form.formaction }}" method="post">
{% for fdata in pagecontent.form.data %}
<fieldset>
<legend>{{ fdata.title }}</legend>
{% for field in fdata.fields %}
{% if field is iterable %}
{% if field.type is defined %}
{% if field.type == 'text' or field.type == 'password' %}
<div class="row mb-3">
<label for="{{ field.id }}" class="col-sm-4 col-form-label">{{ field.label|raw }}</label>
<div class="col-sm-8">
<input type="{{ field.type }}" class="form-control {% if field.style == 'red' %}is-invalid{% endif %}" id="{{ field.id }}" name="{{ field.name }}" value="{{ field.value }}" {% if field.required %} required {% endif %}/>
</div>
</div>
{% elseif field.type == 'select' %}
<div class="row mb-3">
<label for="{{ field.id }}" class="col-sm-4 col-form-label">{{ field.label|raw }}</label>
<div class="col-sm-8">
<select class="form-select {% if field.style == 'red' %}is-invalid{% endif %}" id="{{ field.id }}" name="{{ field.name }}" {% if field.required %} required {% endif %}>
{% for opts in field.options %}
<option value="{{ opts.value }}" {% if opts.selected %} selected="selected" {% endif %}>{{ opts.label }}</option>
{% endfor %}
</select>
</div>
</div>
{% elseif field.type == 'checkbox' %}
<div class="row mb-3">
<label for="{{ field.id }}" class="col-sm-4 col-form-label">{{ field.label|raw }}</label>
<div class="col-sm-8">
<div class="form-check form-switch">
<input class="form-check-input {% if field.style == 'red' %}is-invalid{% endif %}" type="checkbox" value="{{ field.value }}" id="{{ field.id }}" name="{{ field.name }}" {% if field.checked %} checked="checked" {% endif %}>
</div>
</div>
</div>
{% endif %}
{% else %}
<div class="row mb-3">
<label class="col-sm-4 col-form-label">{{ field.label|raw }}</label>
<div class="col-sm-8">
{% for radios in field.fields %}
<div class="form-check">
<input class="form-check-input {% if field.style == 'red' %}is-invalid{% endif %}" type="radio" name="{{ radios.name }}" id="{{ radios.id }}" value="{{ radios.value }}" {% if radios.checked %}checked="checked"{% endif %}>
<label class="form-check-label" for="{{ radios.id }}">
{{ radios.label }}
</label>
</div>
{% endfor %}
</div>
</div>
{% endif %}
{% endif %}
{% endfor %}
</fieldset>
{% endfor %}
<aside class="text-end mt-3">
<input type="hidden" name="check" value="1"/>
<input type="hidden" name="language" value="{{ pagecontent.form.activelang }}"/>
<input type="hidden" name="installstep" value="1"/>
<button class="btn btn-lg btn-success" type="submit" name="submitbutton">
{{ lng('click_here_to_continue') }} &raquo;
</button>
</aside>
</form>

View File

View File

@@ -0,0 +1,126 @@
{% extends "Froxlor/base.html.twig" %}
{% block content %}
<div class="container max-w-lg flex align-content-center mt-5">
<img src="{{ basehref|default('') }}templates/Froxlor/assets/img/logo.png" class="mb-5" alt="{{ lng('install.slogan') }}"/>
{% if error is not null %}
<div class="alert alert-danger mb-4">{{ error }}</div>
{% endif %}
<div class="row text-center gx-0">
<div class="col p-3{{ setup.step == 0 ? ' bg-white shadow rounded-top' : '' }}">
<i class="far fa-circle{{ setup.step > 0 ? '-check' : '' }}"></i>
{% if setup.step > 0 %}<a href="?step=0" class="text-decoration-none">{{ lng('install.preflight') }}</a>{% else %}{{ lng('install.preflight') }}{% endif %}
</div>
<div class="col p-3{{ setup.step == 1 ? ' bg-white shadow rounded-top' : '' }}">
<i class="far fa-circle{{ setup.step > 1 ? '-check' : '' }}"></i>
{% if setup.step > 1 %}<a href="?step=1" class="text-decoration-none">{{ lng('install.database.top') }}</a>{% else %}{{ lng('install.database.top') }}{% endif %}
</div>
<div class="col p-3{{ setup.step == 2 ? ' bg-white shadow rounded-top' : '' }}">
<i class="far fa-circle{{ setup.step > 2 ? '-check' : '' }}"></i>
{% if setup.step > 2 %}<a href="?step=2" class="text-decoration-none">{{ lng('install.admin.top') }}</a>{% else %}{{ lng('install.admin.top') }}{% endif %}
</div>
<div class="col p-3{{ setup.step == 3 ? ' bg-white shadow rounded-top' : '' }}">
<i class="far fa-circle{{ setup.step > 3 ? '-check' : '' }}"></i>
{% if setup.step > 3 %}<a href="?step=3" class="text-decoration-none">{{ lng('install.system.top') }}</a>{% else %}{{ lng('install.system.top') }}{% endif %}
</div>
<div class="col p-3{{ setup.step == 4 ? ' bg-white shadow rounded-top' : '' }}">
<i class="far fa-circle{{ setup.step > 4 ? '-check' : '' }}"></i>
{% if setup.step > 4 %}<a href="?step=4" class="text-decoration-none">{{ lng('install.install.top') }}</a>{% else %}{{ lng('install.install.top') }}{% endif %}
</div>
</div>
<div class="card border-0 shadow">
<div class="card-body p-5">
<form method="post" action="?step={{ setup.step }}">
{% if setup.step > 0 %}
<div class="d-block d-lg-flex justify-content-between align-items-center mb-3">
<h4 class="mb-3 mb-lg-0">{{ section.title }}</h4>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="switchInstallMode" {% if extended is defined and extended %}checked{% endif %}>
<label class="form-check-label" for="switchInstallMode">{% if extended is defined and extended %}{{ lng('install.switchmode_basic') }}{% else %}{{ lng('install.switchmode_advanced') }}{% endif %}</label>
</div>
</div>
<p class="lead">{{ section.description|raw }}</p>
<hr />
{% import "Froxlor/form/formfields.html.twig" as formfields %}
{% for id, field in section.fields %}
{% if field.advanced is defined and field.advanced == true and extended == false %}
{# hide advanced fields #}
{% set field = field|merge({'type': 'hidden'}) %}
{% endif %}
{{ formfields.field(id, field) }}
{% endfor %}
<div class="d-flex {% if setup.step < setup.max_steps %}justify-content-between{% else %}justify-content-end{% endif %} mt-4">
{% if setup.step < setup.max_steps %}
<a href="?step={{ setup.step - 1 }}" class="btn btn-secondary">&laquo; {{ lng('panel.back') }}</a>
<button type="submit" name="submit" class="btn btn-primary">{{ lng('panel.next') }} &raquo;</button>
{% else %}
<span id="submitAuto"><i class="fas fa-spinner fa-pulse"></i> {{ lng('install.install.waitforconfig') }}</span>
<button id="submitManual" type="submit" name="submit" class="btn btn-success d-none">{{ lng('install.install.top') }} &raquo;</button>
{% endif %}
</div>
{% else %}
<h4 class="mb-3">{{ lng('install.dependency_check.title') }}</h4>
<p class="lead">{{ lng('install.dependency_check.description') }}</p>
<p class="lead {{ preflight.criticals ? 'text-danger' : preflight.suggestions ? 'text-warning' : 'text-success'}}">
<i class="{{ preflight.criticals ? 'fa-solid fa-triangle-exclamation' : preflight.suggestions ? 'fa-solid fa-circle-info' : 'far fa-circle-check' }}"></i>
{{ preflight.text }}
</p>
{% if preflight.criticals %}
<p class="text-muted">{{ lng('install.critical_error') }}</p>
<ul>
{% for ctype, critical in preflight.criticals %}
{% if ctype == 'wrong_ownership' %}
<li>{{ lng('install.errors.' ~ ctype, [critical.user, critical.group]) }}</li>
{% elseif ctype == 'missing_extensions' %}
<li>{{ lng('install.errors.' ~ ctype) }}<ul>
{% for missext in critical %}
<li>{{ missext }}</li>
{% endfor %}
</ul>
</li>
{% else %}
<li>{{ critical|raw }}</li>
{% endif %}
{% endfor %}
</ul>
{% endif %}
{% if preflight.suggestions %}
<p class="text-muted">{{ lng('install.suggestions') }}</p>
<ul>
{% for ctype, suggestion in preflight.suggestions %}
{% if ctype == 'missing_extensions' %}
<li>{{ lng('install.errors.suggestedextensions') }}<ul>
{% for missext in suggestion %}
<li>{{ missext }}</li>
{% endfor %}
</ul>
</li>
{% else %}
<li>{{ suggestion|raw }}</li>
{% endif %}
{% endfor %}
</ul>
{% endif %}
<div class="d-flex justify-content-end mt-4">
{% if preflight.criticals %}
<a href="" class="btn btn-secondary"><i class="fa-solid fa-arrow-rotate-left"></i> {{ lng('install.check_again') }}</a>
{% else %}
<a href="?step=1" class="btn btn-primary">{{ lng('install.start_installation') }}</a>
{% endif %}
</div>
{% endif %}
</form>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,48 @@
{% extends "Froxlor/userarea.html.twig" %}
{% block heading %}
<div>
<h5 class="mb-1">
<i class="fa-solid fa-download me-1"></i>
{{ lng('update.update') }}
</h5>
<span class="text-muted">{{ lng('update.description') }}</span>
</div>
{% endblock %}
{% block content %}
<div class="card table-responsive">
<div class="card-body">
<table class="table table-borderless align-middle mb-0 px-3">
<tbody>
{% for check in checks %}
<tr class="{% if check.result == 1 %}table-danger{% elseif check.result == 2 %}table-warning{% endif %}">
<td class="w-75" scope="row">{{ check.title }}</td>
<td class="col-auto text-end{% if check.result == 0 %} text-success{% endif %}">
<span class="d-none d-md-inline">{{ check.result_txt }}</span>
{% if check.result == 0 %}&nbsp;<i class="fa-solid fa-check-circle" {% elseif check.result == 2 %}<span class="d-md-none">&nbsp;???</span>{% elseif check.result == 1 %}<span class="d-md-none">&nbsp;!!!</span>
{% endif %}
</td>
</tr>
{% if check.result_desc is not empty %}
<tr>
<td colspan="2">
<span>{{ check.result_desc|raw }}</span>
</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
<div class="row pt-md-3">
<div class="col-12 text-end mt-4 mt-md-0">
<a class="btn btn-lg btn-block btn-primary" href="admin_index.php">
{{ lng('success.clickheretocontinue') }}
&raquo;
</a>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,36 @@
{% extends "Froxlor/base.html.twig" %}
{% block content %}
<div class="container">
<div class="row justify-content-center">
<form action="index.php" class="col-12 max-w-420 d-flex flex-column" method="post" enctype="application/x-www-form-urlencoded">
<img class="align-self-center my-5" src="{{ header_logo_login }}" alt="Froxlor Server Management Panel"/>
<div class="card shadow">
<div class="card-body">
<h5 class="card-title">{{ pagetitle }}</h5>
{% if message is not empty %}
<div class="alert alert-danger" role="alert">
<h4 class="alert-heading">{{ lng('error.error') }}</h4>
<p>{{ message|raw }}</p>
</div>
{% endif %}
<div class="mb-3">
<label for="2fa_code" class="col-form-label">{{ lng('login.2facode') }}</label>
<input class="form-control" type="text" name="2fa_code" id="2fa_code" value="" autocomplete="off" autofocus required/>
</div>
</div>
<div class="card-body d-grid gap-2">
<input type="hidden" name="action" value="2fa_verify"/>
<input type="hidden" name="send" value="send"/>
<button class="btn btn-primary rounded-top-0" type="submit" name="2faverify">{{ lng('2fa.2fa_verify') }}</button>
</div>
</div>
</form>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,53 @@
{% extends "Froxlor/base.html.twig" %}
{% block content %}
<div class="container">
<div class="row justify-content-center">
<form action="{{ formaction }}" class="col-12 max-w-420 d-flex flex-column" method="post" enctype="application/x-www-form-urlencoded">
<img class="align-self-center my-5" src="{{ header_logo_login }}" alt="Froxlor Server Management Panel"/>
<div class="card shadow">
<div class="card-body">
<h5 class="card-title">{{ pagetitle }}</h5>
{% if upd_in_progress %}
<div class="alert alert-warning" role="alert">
{{ lng('update.updateinprogress_onlyadmincanlogin')|raw }}
</div>
{% elseif successmsg is not empty %}
<div class="alert alert-success" role="alert">
<h4 class="alert-heading">{{ lng('success.success') }}</h4>
<p>{{ successmsg|raw }}</p>
</div>
{% elseif message is not empty %}
<div class="alert alert-danger" role="alert">
<h4 class="alert-heading">{{ lng('error.error') }}</h4>
<p>{{ message|raw }}</p>
</div>
{% endif %}
<div class="mb-3">
<label for="loginname" class="col-form-label">{{ lng('login.username') }}</label>
<input class="form-control" type="text" name="loginname" id="loginname" value="" required autofocus/>
</div>
<div class="mb-3">
<label for="loginemail" class="col-form-label">{{ lng('login.email') }}</label>
<input class="form-control" type="email" name="loginemail" id="loginemail" value="" required/>
</div>
</div>
<div class="card-body d-grid gap-2">
<button class="btn btn-primary rounded-top-0" type="submit" name="doremind">{{ lng('login.remind') }}</button>
</div>
<div class="card-footer">
<a class="card-link text-muted" href="index.php">
<i class="fa-solid fa-angles-left"></i>
{{ lng('login.backtologin') }}</a>
</div>
</div>
</form>
</div>
</div>
{% endblock %}

View File

View File

@@ -0,0 +1,54 @@
{% extends "Froxlor/base.html.twig" %}
{% block content %}
<div class="container">
<div class="row justify-content-center">
<form class="col-12 max-w-420 d-flex flex-column" method="post" enctype="application/x-www-form-urlencoded">
<img class="align-self-center my-5" src="{{ header_logo_login }}" alt="Froxlor Server Management Panel"/>
<div class="card shadow">
<div class="card-body">
<h5 class="card-title">{{ pagetitle }}</h5>
<p>{{ lng('login.welcomemsg') }}</p>
{% if upd_in_progress %}
<div class="alert alert-warning" role="alert">
{{ lng('update.updateinprogress_onlyadmincanlogin')|raw }}
</div>
{% elseif successmsg is not empty %}
<div class="alert alert-success" role="alert">
<h4 class="alert-heading">{{ lng('success.success') }}</h4>
<p>{{ successmsg|raw }}</p>
</div>
{% elseif message is not empty %}
<div class="alert alert-danger" role="alert">
<h4 class="alert-heading">{{ lng('error.error') }}</h4>
<p>{{ message|raw }}</p>
</div>
{% endif %}
<div class="mb-3">
<label for="loginname" class="col-form-label">{{ lng('login.username') }}</label>
<input class="form-control" type="text" name="loginname" id="loginname" value="" required autofocus/>
</div>
<div class="mb-3">
<label for="password" class="col-form-label">{{ lng('login.password') }}</label>
<input class="form-control" type="password" name="password" id="password" value="" required/>
</div>
</div>
<div class="card-body d-grid gap-2">
<button class="btn btn-primary rounded-top-0" type="submit" name="dologin">{{ lng('login.login') }}</button>
</div>
{% if get_setting('panel.allow_preset') == '1' %}
<div class="card-footer">
<a class="card-link text-muted" href="index.php?action=forgotpwd">{{ lng('login.forgotpwd') }}</a>
</div>
{% endif %}
</div>
</form>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,45 @@
{% extends "Froxlor/base.html.twig" %}
{% block content %}
<div class="container">
<div class="row justify-content-center">
<form action="{{ formaction }}" class="col-12 max-w-420 d-flex flex-column" method="post" enctype="application/x-www-form-urlencoded">
<img class="align-self-center my-5" src="{{ header_logo_login }}" alt="Froxlor Server Management Panel"/>
<div class="card shadow">
<div class="card-body">
<h5 class="card-title">{{ pagetitle }}</h5>
<p>{{ lng('login.presend') }}</p>
{% if message is not empty %}
<div class="alert alert-danger" role="alert">
<h4 class="alert-heading">{{ lng('error.error') }}</h4>
<p>{{ message|raw }}</p>
</div>
{% endif %}
<div class="mb-3">
<label for="new_password" class="col-form-label">{{ lng('changepassword.new_password') }}</label>
<input class="form-control" type="password" name="new_password" id="new_password" value="" required autofocus/>
</div>
<div class="mb-3">
<label for="new_password_confirm" class="col-form-label">{{ lng('changepassword.new_password_confirm') }}</label>
<input class="form-control" type="password" name="new_password_confirm" id="new_password_confirm" value="" required/>
</div>
</div>
<div class="card-body d-grid gap-2">
<button class="btn btn-primary rounded-top-0" type="submit" name="doremind">{{ lng('login.remind') }}</button>
</div>
<div class="card-footer">
<a class="card-link text-muted" href="index.php">
<i class="fa-solid fa-angles-left"></i>
{{ lng('login.backtologin') }}</a>
</div>
</div>
</form>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,5 @@
{% extends "Froxlor/userarea.html.twig" %}
{% block content %}
{% include 'Froxlor/misc/alertbox.html.twig' %}
{% endblock %}

View File

@@ -0,0 +1,5 @@
{% extends "Froxlor/base.html.twig" %}
{% block content %}
{% include 'Froxlor/misc/alertbox.html.twig' %}
{% endblock %}

View File

@@ -0,0 +1,27 @@
<div class="alert alert-{{ type|default("info") }} fade show" role="alert">
{% if heading is defined and heading is not empty %}
<h4 class="alert-heading">
{{ heading }}
</h4>
{% endif %}
<p>
{{ alert_msg|raw }}
</p>
{% if alert_info %}
<hr>
<p class="mb-0">
<pre>{{ alert_info|raw }}</pre>
</p>
{% endif %}
{% if redirect_link %}
<p>
<a href="{{ redirect_link|raw }}" class="btn btn-{{ btntype }}">
{% if type == 'danger' %}
{{ lng('panel.back') }}
{% else %}
{{ lng('success.clickheretocontinue') }}
{% endif %}
</a>
</p>
{% endif %}
</div>

View File

@@ -0,0 +1,22 @@
{% extends "Froxlor/base.html.twig" %}
{% block content %}
<div class="container max-w-lg flex align-content-center mt-5">
<img src="templates/Froxlor/assets/img/logo.png" alt="Froxlor Server Management Panel" />
<div class="row gx-0 rounded shadow bg-primary text-white mt-5">
<div class="col p-5 rounded-start">
<h2 class="card-title">Welcome to Froxlor</h2>
<p class="lead mt-5">It seems that Froxlor has not been installed yet.</p>
<p class="lead">Click on the button below to start the installation.</p>
</div>
<div class="col text-white position-relative">
<img class="h-75 position-absolute bottom-0 end-0 rounded-tl-bl" src="{{ basehref }}templates/Froxlor/assets/img/preview.jpg">
</div>
</div>
<div class="mt-5 text-end">
<a class="btn btn-lg btn-primary" href="./install/install.php" title="Click to start the install process">Start install</a>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,26 @@
{% extends "Froxlor/base.html.twig" %}
{% block content %}
<div class="container my-auto">
<div class="alert alert-danger fade show" role="alert">
<h4 class="alert-heading">
A database error occurred
</h4>
<p>
{{ message }}
</p>
{% if debug is not empty %}
<hr>
<p class="mb-0">
<pre>{{ debug }}</pre>
</p>
{% endif %}
<p class="mt-1 text-center">
<a href="#" class="btn btn-primary" title="Click here to go back" id="historyback">Go back</a>
{% if report is not empty %}
<a href="{{ report|raw }}" class="btn btn-warning" title="Click here to report error">Report error</a>
{% endif %}
</p>
</div>
</div>
{% endblock %}

View File

View File

@@ -0,0 +1,18 @@
{% extends "Froxlor/base.html.twig" %}
{% block content %}
<div class="container my-auto">
<div class="alert alert-warning fade show" role="alert">
<h4 class="alert-heading">
Whoops!
</h4>
<p>The configuration file <b>lib/userdata.inc.php</b> cannot be read from the webserver.</p>
<p>This mostly happens due to wrong ownership.<br />Try the following command to correct the ownership:</p>
<pre>chown -R {{ user }}:{{ group }} {{ installdir }}</pre>
<hr>
<p class="mt-1 text-center">
<a href="" class="btn btn-primary" title="Reload page">Reload</a>
</p>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="robots" content="noindex, nofollow, noarchive" />
<meta name="googlebot" content="nosnippet" />
<!-- CSS -->
<link href="{{ basehref }}templates/Froxlor/assets/css/main.css" rel="stylesheet">
<!-- Scripts -->
<script src="{{ basehref }}templates/Froxlor/assets/js/main.js"></script>
<title>Froxlor - Error</title>
</head>
<body class="min-vh-100 d-flex align-items-center">
<div class="container-fluid">
<div class="container max-w-lg">
<div class="card bg-danger text-white">
<div class="card-body">
<h4 class="card-title">
Whoops!
</h4>
<p>It seems you are using an older version of PHP</p>
<p>Froxlor requires at least PHP version {{ froxlor_min_version }}</p>
<p>The installed version is: {{ current_version }}</p>
</div>
<div class="card-footer text-end">
<a href="" class="btn btn-primary" title="Click to refresh">Refresh</a>
</div>
</div>
</div>
<footer class="pý-5 text-center">
<span>
<img src="{{ basehref }}templates/Froxlor/assets/img/logo_grey.png" alt="Froxlor" />
&copy; 2009-{{ current_year }} by <a href="https://www.froxlor.org/" rel="external">the Froxlor Team</a>
</span>
</footer>
</div>
</body>
</html>

View File

@@ -0,0 +1,17 @@
{% extends "Froxlor/base.html.twig" %}
{% block content %}
<div class="container my-auto">
<div class="alert alert-warning fade show" role="alert">
<h4 class="alert-heading">
Whoops!
</h4>
<p>It seems like you've hit the rate limit.</p>
<p>Please slow down your requests and retry after {{ retry|date('d.m.Y H:i:s') }}</p>
<hr>
<p class="mt-1 text-center">
<a href="" class="btn btn-primary" title="Reload page">Reload</a>
</p>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="robots" content="noindex, nofollow, noarchive" />
<meta name="googlebot" content="nosnippet" />
<!-- CSS -->
<link href="{{ basehref }}templates/Froxlor/assets/css/main.css" rel="stylesheet">
<!-- Scripts -->
<script src="{{ basehref }}templates/Froxlor/assets/js/main.js"></script>
<title>Froxlor - Error</title>
</head>
<body class="min-vh-100 d-flex align-items-center">
<div class="container-fluid">
<div class="container max-w-lg">
<div class="card bg-danger text-white">
<div class="card-body">
<h4 class="card-title">
Whoops!
</h4>
<p>It seems you are missing some required files.</p>
<p>Froxlor uses composer for its external requirements. Try the following command to install them:</p>
<pre>cd {{ froxlor_install_dir }} && composer install --no-dev</pre>
</div>
<div class="card-footer text-end">
<a href="" class="btn btn-primary" title="Click to refresh">Refresh</a>
</div>
</div>
</div>
<footer class="py-5 text-center">
<span>
<img src="{{ basehref }}templates/Froxlor/assets/img/logo_grey.png" alt="Froxlor" />
&copy; 2009-{{ current_year }} by <a href="https://www.froxlor.org/" rel="external">the Froxlor Team</a>
</span>
</footer>
</div>
</body>
</html>

View File

@@ -0,0 +1,26 @@
{% macro vpopover(isnewerversion, additional_info, full_version, dbversion, channel, last_update_check, message) %}
{% if isnewerversion == 0 %}
<p>{{ additional_info }}</p>
<div class='d-flex justify-content-between'>
<div class='fw-bold'>Version:</div>
<div>{{ full_version }}</div>
</div>
<div class='d-flex justify-content-between'>
<div class='fw-bold'>Database version:</div>
<div>{{ dbversion }}</div>
</div>
<div class='d-flex justify-content-between'>
<div class='fw-bold'>Channel:</div>
<div>{{ channel }}</div>
</div>
<div class='d-flex justify-content-between'>
<div class='fw-bold'>Last checked:</div>
<div>{{ last_update_check|date('d.m.Y H:i') }}</div>
</div>
{% else %}
<p>{{ message }}</p>
{% if get_config('enable_webupdate') %}
<a class='btn d-block btn-outline-warning' href='admin_autoupdate.php?page=overview'>Open updater</a>
{% endif %}
{% endif %}
{% endmacro %}

View File

@@ -0,0 +1,12 @@
{% import "Froxlor/misc/version_popover.html.twig" as vc %}
<span id="ucheck" class="nav-link {% if isnewerversion == 0 and aucheck < 0 %}text-muted{% elseif isnewerversion == 0 %}text-success{% else %}text-warning{% endif %}"
data-bs-container="body" data-bs-toggle="popover" data-bs-placement="bottom" data-bs-trigger="hover focus click" data-bs-html="true"
data-bs-content="{{ vc.vpopover(isnewerversion, additional_info, full_version, dbversion, channel, last_update_check, message) }}"
>
{% if isnewerversion == 0 and aucheck == 0 %}
<i class="fa-solid fa-circle-check me-1"></i>
{% else %}
<i class="fa-solid fa-circle-exclamation me-1"></i>
{% endif %}
<span class="d-none d-xl-inline">{{ version }}</span>
</span>

View File

@@ -0,0 +1,176 @@
{% extends "Froxlor/userarea.html.twig" %}
{% block heading %}
<h5>
<i class="fa-solid fa-hard-drive me-1"></i>
{{ lng('admin.apcuinfo') }}
</h5>
{% endblock %}
{% block actions %}
<a class="btn btn-warning" href="{{ linker({'section':'apcuinfo','page':'showinfo','action':'delete'}) }}">
<i class="fa-solid fa-trash-can me-1"></i>
{{ lng('apcuinfo.clearcache') }}
</a>
{% endblock %}
{% block content %}
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-4 mb-4">
<div class="col">
<div class="card h-100 mb-3">
<div class="card-body">
<h5 class="card-title">{{ lng('apcuinfo.memnote') }}</h5>
<div class="progress position-relative">
<div class="progress-bar bg-success" role="progressbar" style="width: {{ apcuinfo.mem_used_percentage }}%" aria-valuenow="{{ apcuinfo.mem_used }}" aria-valuemin="0" aria-valuemax="{{ apcuinfo.mem_avail }}"></div>
<small class="justify-content-center d-flex position-absolute w-100 text-dark">{{ apcuinfo.mem_used_percentage }}%</small>
</div>
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item d-flex justify-content-between align-items-center">
{{ lng('apcuinfo.total') }}
<span class="badge bg-secondary">{{ apcuinfo.readable.mem_size }}</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
{{ lng('apcuinfo.used') }}
<span class="badge bg-secondary">{{ apcuinfo.readable.mem_used }}</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
{{ lng('apcuinfo.free') }}
<span class="badge bg-secondary">{{ apcuinfo.readable.mem_avail }}</span>
</li>
</ul>
</div>
</div>
<div class="col">
<div class="card h-100 mb-3">
<div class="card-body">
<h5 class="card-title">{{ lng('apcuinfo.hitmiss') }}</h5>
<div class="progress position-relative">
<div class="progress-bar bg-success" role="progressbar" style="width: {{ apcuinfo.num_hits_percentage }}%" aria-valuenow="{{ apcuinfo.num_hits }}" aria-valuemin="0" aria-valuemax="{{ apcuinfo.num_hits_and_misses }}"></div>
<div class="progress-bar bg-danger" role="progressbar" style="width: {{ 100 - apcuinfo.num_misses_percentage }}%" aria-valuenow="{{ apcuinfo.num_misses }}" aria-valuemin="0" aria-valuemax="{{ apcuinfo.num_hits_and_misses }}"></div>
<small class="justify-content-center d-flex position-absolute w-100 text-dark">{{ apcuinfo.num_hits_percentage }}%</small>
</div>
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item d-flex justify-content-between align-items-center">
{{ lng('apcuinfo.hit') }}
<span class="badge bg-secondary">{{ apcuinfo.readable.num_hits }}</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
{{ lng('apcuinfo.miss') }}
<span class="badge bg-secondary">{{ apcuinfo.readable.num_misses }}</span>
</li>
</ul>
</div>
</div>
<div class="col">
<div class="card h-100 mb-3">
<div class="card-body">
<h5 class="card-title">{{ lng('apcuinfo.cachetitle') }}</h5>
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item d-flex justify-content-between align-items-center">
{{ lng('apcuinfo.cvar') }}
<span class="badge bg-secondary">{{ apcuinfo.readable.number_vars }}
({{ apcuinfo.size_vars }})</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
{{ lng('apcuinfo.reqrate') }}
<span class="badge bg-secondary">{{ apcuinfo.req_rate_user }}
{{ lng('apcuinfo.creqsec') }}</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
{{ lng('apcuinfo.hitrate') }}
<span class="badge bg-secondary">{{ apcuinfo.hit_rate_user }}
{{ lng('apcuinfo.creqsec') }}</span>
</li>
</ul>
</div>
</div>
<div class="col">
<div class="card h-100 mb-3">
<div class="card-body">
<h5 class="card-title">{{ lng('apcuinfo.detailmem') }}</h5>
{% if apcuinfo.fragmentation is not iterable %}
{{ lng('apcuinfo.nofragment') }}
{% endif %}
</div>
{% if apcuinfo.fragmentation is iterable %}
<div class="progress position-relative">
<div class="progress-bar bg-success" role="progressbar" style="width: {{ apcuinfo.fragmentation.used_percentage }}%" aria-valuenow="{{ apcuinfo.fragmentation.used_bytes }}" aria-valuemin="0" aria-valuemax="{{ apcuinfo.fragmentation.total_bytes }}"></div>
<small class="justify-content-center d-flex position-absolute w-100 text-dark">{{ apcuinfo.fragmentation.used_percentage }}%</small>
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item d-flex justify-content-between align-items-center">
{{ lng('apcuinfo.total') }}
<span class="badge bg-secondary">{{ apcuinfo.fragmentation.readable.total_bytes }}</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
{{ lng('apcuinfo.used') }}
<span class="badge bg-secondary">{{ apcuinfo.fragmentation.readable.used_bytes }}</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
{{ lng('apcuinfo.fragments') }}
<span class="badge bg-secondary">{{ apcuinfo.fragmentation.readable.num_frags }}</span>
</li>
</ul>
{% endif %}
</div>
</div>
</div>
<div class="row row-cols-1 row-cols-md-2">
<div class="col">
<div class="card table-responsive mb-3">
<table class="table table-borderless table-striped align-middle mb-0 px-3">
<tbody>
<tr>
<th class="text-center" colspan="2" scope="row">{{ lng('apcuinfo.generaltitle') }}</th>
</tr>
<tr>
<th class="fw-bold" scope="row">{{ lng('apcuinfo.version') }}</th>
<td class="text-end">{{ apcuinfo.apcversion }}</td>
</tr>
<tr>
<th class="fw-bold" scope="row">{{ lng('apcuinfo.phpversion') }}</th>
<td class="text-end">{{ apcuinfo.phpversion }}</td>
</tr>
<tr>
<th class="fw-bold" scope="row">{{ lng('admin.hostname') }}</th>
<td class="text-end">{{ apcuinfo.host }}</td>
</tr>
<tr>
<th class="fw-bold" scope="row">{{ lng('admin.serversoftware') }}</th>
<td class="text-end">{{ apcuinfo.server }}</td>
</tr>
<tr>
<th class="fw-bold" scope="row">{{ lng('apcuinfo.start') }}</th>
<td class="text-end">{{ apcuinfo.start_time|date('d.m.Y H:i:s') }}</td>
</tr>
<tr>
<th class="fw-bold" scope="row">{{ lng('apcuinfo.uptime') }}</th>
<td class="text-end">{{ apcuinfo.uptime }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="col">
<div class="card table-responsive">
<table class="table table-borderless table-striped align-middle mb-0 px-3">
<tbody>
<tr>
<th class="text-center" colspan="2" scope="row">{{ lng('apcuinfo.runtime') }}</th>
</tr>
{% for k,v in apcuinfo.runtimelines %}
<tr>
<th class="fw-bold" scope="row">{{ k|raw }}</th>
<td class="text-end">{{ v|raw }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1 @@
<textarea class="form-control bg-secondary text-light mb-2" rows="{{ numbrows }}" readonly>{{ commands|raw }}</textarea>

View File

@@ -0,0 +1,2 @@
<textarea class="form-control bg-secondary text-light mb-2" rows="1" readonly>{{ distro_editor }} {{ realname }}</textarea>
<textarea class="form-control mb-2" rows="{% if numbrows <= 20 %}{{ numbrows }}{% else %}21{% endif %}" readonly>{{ file_content|raw }}</textarea>

View File

@@ -0,0 +1,6 @@
<fieldset class="file">
{# <legend>{{ realname }}</legend> #}
{{ commands_pre|raw }}
{{ commands_file|raw }}
{{ commands_post|raw }}
</fieldset>

View File

@@ -0,0 +1,12 @@
{% extends "Froxlor/settings/configuration.html.twig" %}
{% block content %}
<div class="row mb-2">
{% include 'Froxlor/misc/alertbox.html.twig' %}
</div>
<div class="row mb-2">
<textarea cols="12" rows="4" readonly class="form-control w-100">{{ basedir }}bin/froxlor-cli froxlor:config-services --apply={{ params_filename }}
rm {{ params_filename }}</textarea>
</div>
{% endblock %}

View File

@@ -0,0 +1,138 @@
{% extends "Froxlor/userarea.html.twig" %}
{% block heading %}
<h5>
<i class="fa-solid fa-wrench"></i>
{{ lng('admin.configfiles.serverconfiguration') }}
</h5>
<span class="text-muted">{{ lng('admin.configfiles.description') }}</span>
{% endblock %}
{% block actions %}
<a class="btn btn-outline-primary" href="{{ linker({'section':'configfiles','reselect':1}) }}">
<i class="fa-solid fa-grip me-1"></i>
{{ lng('admin.configfiles.distribution') }}:
{{ distribution }}
</a>
{% endblock %}
{% block content %}
<div class="pb-2">
<div class="alert alert-info fade show" role="alert">
<p>{{ lng('admin.configfiles.minihowto')|raw }}</p>
</div>
</div>
<form action="{{ action|default(filename) }}" method="post" enctype="application/x-www-form-urlencoded" class="form">
{% block settings %}
<div class="row row-cols-1 row-cols-md-2 row-cols-xl-4 g-3">
{% for stype,field in fields %}
<div class="col">
<div class="card h-100 position-relative">
<div class="card-body">
<h5 class="card-title">{{ stype|upper }}</h5>
{% if stype != 'system' %}
<div class="form-check">
<input class="form-check-input" type="radio" name="{{ stype }}" id="{{ stype }}none" value="x" checked>
<label class="form-check-label" for="{{ stype }}none">
{{ lng('admin.configfiles.skipconfig') }}
</label>
</div>
{% endif %}
{% set daemons = field.getDaemons %}
{% for dtype,daemon in daemons %}
{% if stype == 'system' %}
<div class="form-check">
{% set recommended = false %}
{% if
(dtype == get_setting('system.traffictool')) or
(dtype == 'libnssextrausers' and (get_setting('system.mod_fcgid') == '1' or get_setting('phpfpm.enabled') == '1' or get_setting('system.apacheitksupport') == '1')) or
(dtype == 'logrotate') or
(dtype == 'fcgid' and get_setting('system.mod_fcgid') == '1') or
(dtype == 'php-fpm' and get_setting('phpfpm.enabled') == '1') or
(dtype == 'cron')
%}
{% set recommended = true %}
{% endif %}
<input class="form-check-input" type="checkbox" name="system[{{ dtype }}]" id="{{ dtype }}" value="{{ dtype }}" data-recommended="{{ recommended }}">
<label class="form-check-label" for="{{ dtype }}">
{% if recommended %}
<strong>{{ daemon.title }}<span class="text-danger">*</span>
</strong>
{% else %}
{{ daemon.title }}
{% endif %}
</label>
<a class="show-config text-secondary opacity-50 float-end" role="button" data-dist="{{ distribution }}" data-section="{{ stype }}" data-daemon="{{ dtype }}" title="show config">
<i class="fa-regular fa-file-code"></i>
</a>
</div>
{% else %}
<div class="form-check">
{% set recommended = false %}
{% if
(dtype == 'apache22' and get_setting('system.webserver') == 'apache2' and get_setting('system.apache24') == '0') or
(dtype == 'apache24' and get_setting('system.webserver') == 'apache2' and get_setting('system.apache24') == '1') or
(dtype == 'lighttpd' and get_setting('system.webserver') == 'lighttpd') or
(dtype == 'nginx' and get_setting('system.webserver') == 'nginx') or
(dtype == 'bind' and get_setting('system.bind_enable') == '1' and get_setting('system.dns_server') == 'Bind') or
(dtype == 'powerdns' and get_setting('system.bind_enable') == '1' and get_setting('system.dns_server') == 'PowerDNS') or
(dtype == 'proftpd' and get_setting('system.ftpserver') == 'proftpd') or
(dtype == 'pureftpd' and get_setting('system.ftpserver') == 'pureftpd')
%}
{% set recommended = true %}
{% endif %}
<input class="form-check-input" type="radio" name="{{ stype }}" id="{{ dtype }}" value="{{ dtype }}" data-recommended="{{ recommended }}">
<label class="form-check-label" for="{{ dtype }}">
{% if recommended %}
<strong>{{ daemon.title }}<span class="text-danger">*</span>
</strong>
{% else %}
{{ daemon.title }}
{% endif %}
</label>
<a class="show-config text-secondary opacity-50 float-end" role="button" data-dist="{{ distribution }}" data-section="{{ stype }}" data-daemon="{{ dtype }}" title="show config">
<i class="fa-regular fa-file-code"></i>
</a>
</div>
{% endif %}
{% endfor %}
</div>
</div>
</div>
{% endfor %}
</div>
{% endblock %}
<div class="row mt-3">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}"/>
<input type="hidden" name="finish" value="1"/>
<div class="col-12 col-md-6">
<span class="text-danger">*</span>
{{ lng('admin.configfiles.recommendednote') }}
</div>
<div class="col-12 col-md-6 text-end">
<button type="button" class="btn btn-outline-secondary" id="selectRecommendedConfig">{{ lng('admin.configfiles.selectrecommended') }}</button>
<button type="button" class="btn btn-outline-secondary" id="downloadSelectionAsJson">
<i class="fa-solid fa-download"></i>
{{ lng('admin.configfiles.downloadselected') }}</button>
<button type="submit" class="btn btn-primary">{{ lng('update.proceed') }}</button>
</div>
</div>
</form>
<div class="modal fade" id="configTplShow" aria-hidden="true" aria-labelledby="configTplShowLabel" tabindex="-1">
<div class="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="configTplShowLabel"></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{{ lng('panel.modalclose') }}"></button>
</div>
<div class="modal-body text-start"></div>
<div class="modal-footer">
<button class="btn btn-primary" data-bs-dismiss="modal">{{ lng('panel.modalclose') }}</button>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,40 @@
{% extends "Froxlor/settings/index.html.twig" %}
{% block actions %}
<a class="btn btn-outline-primary" href="{{ linker({'section':'settings','page':'overview','part':'all'}) }}">
<i class="fa-solid fa-grip me-1"></i>
{{ lng('admin.configfiles.overview') }}
</a>
<a class="btn btn-outline-secondary" href="{{ linker({'section':'settings','page':'importexport'}) }}">
<i class="fa-solid fa-file-import me-1"></i>
{{ lng('admin.configfiles.importexport') }}
</a>
{% endblock %}
{% block settings %}
{% import "Froxlor/form/formfields.html.twig" as formfields %}
<div class="card mb-3">
<div class="formfields">
{% for id,setting in fields %}
{% if id != '_group' %}
{% set isEm = em is defined and em == id %}
{{ formfields.fieldrow(id, setting, false, (get_setting('system.hide_incompatible_settings') == '0'), isEm) }}
{% endif %}
{% endfor %}
</div>
</div>
<div>
<input type="hidden" name="csrf_token" value="{{ csrf_token }}"/>
<input type="hidden" name="page" value="{{ page }}"/>
<input type="hidden" name="action" value="{{ action }}"/>
<input type="hidden" name="send" value="send"/>
<div class="col-12 text-center mb-2 d-grid gap-2 d-md-block">
<button type="reset" class="btn btn-lg btn-outline-secondary me-md-3">{{ lng('panel.reset') }}</button>
<button type="submit" class="btn btn-lg btn-primary">{{ lng('panel.save') }}</button>
</div>
</div>
{% endblock %}

View File

View File

@@ -0,0 +1,60 @@
{% extends "Froxlor/userarea.html.twig" %}
{% block heading %}
<h5>
<i class="fa-solid fa-gears"></i>
{{ lng('admin.serversettings') }}
{% if fields._group is defined %}&nbsp;&raquo;&nbsp;{{ fields._group.title|raw }}
{% endif %}
</h5>
<span class="text-muted">{{ lng('admin.serversettings_desc') }}</span>
{% endblock %}
{% block actions %}
<a class="btn btn-outline-secondary" href="{{ linker({'section':'settings','page':'toggleSettingsMode'}) }}" title="{{ lng('panel.settingsmodetoggle') }}">
{% if get_setting('panel.settings_mode') == 0 %}
<i class="fa-solid fa-maximize me-1"></i>
{{ lng('panel.settingsmode') }}: {{ lng('panel.settingsmodebasic') }}
{% else %}
<i class="fa-solid fa-minimize me-1"></i>
{{ lng('panel.settingsmode') }}: {{ lng('panel.settingsmodeadvanced') }}
{% endif %}
</a>
<a class="btn btn-outline-secondary" href="{{ linker({'section':'settings','page':'importexport'}) }}">
<i class="fa-solid fa-file-import me-1"></i>
{{ lng('admin.configfiles.importexport') }}
</a>
{% endblock %}
{% block content %}
<form action="{{ action|default(filename) }}" method="post" enctype="multipart/form-data" class="form">
{% block settings %}
<div class="row row-cols-2 row-cols-md-2 row-cols-xl-4 g-3">
{% for field in fields %}
{% if get_setting('system.hide_incompatible_settings') == 0 or (get_setting('system.hide_incompatible_settings') == 1 and (field.visible is not defined or (field.visible is defined and field.visible))) %}
<div class="col">
<div class="card h-100 position-relative {% if not field.activated %}{% endif %}">
<div class="card-body d-flex overflow-hidden align-items-center">
<a href="{{ linker({'section':'settings','page':'overview','part':field.part}) }}" class="stretched-link">
<i class="{{ field.icon }} fa-2x me-4" style="width: 1em;"></i>
</a>
<div>
{{ field.title|raw }}
{% if field.info is defined and field.info is not empty %}
{{ field.info|raw }}
{% endif %}
</div>
</div>
{% if not field.activated %}
<div class="position-absolute top-0 end-0 p-1">
<span class="badge text-muted" style="background: #eee">{{ lng('panel.not_activated') }}</span>
</div>
{% endif %}
</div>
</div>
{% endif %}
{% endfor %}
</div>
{% endblock %}
</form>
{% endblock %}

View File

@@ -0,0 +1,214 @@
{% extends "Froxlor/userarea.html.twig" %}
{% block heading %}
<h5>
<i class="fa-solid fa-hard-drive me-1"></i>
{{ lng('admin.opcacheinfo') }}
</h5>
{% endblock %}
{% block actions %}
<a class="btn btn-warning" href="{{ linker({'section':'opcacheinfo','page':'showinfo','action':'reset'}) }}">
<i class="fa-solid fa-trash-can me-1"></i>
{{ lng('opcacheinfo.resetcache') }}
</a>
{% endblock %}
{% block content %}
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-4 mb-4">
<div class="col">
<div class="card h-100 mb-3">
<div class="card-body">
<h5 class="card-title">{{ lng('opcacheinfo.memusage') }}</h5>
<div class="progress position-relative">
<div class="progress-bar bg-success" role="progressbar" style="width: {{ opcacheinfo.overview.used_memory_percentage }}%" aria-valuenow="{{ opcacheinfo.overview.used_memory }}" aria-valuemin="0" aria-valuemax="{{ opcacheinfo.overview.total_memory }}"></div>
<small class="justify-content-center d-flex position-absolute w-100 text-dark">{{ opcacheinfo.overview.used_memory_percentage }}%</small>
</div>
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item d-flex justify-content-between align-items-center">
{{ lng('opcacheinfo.totalmem') }}
<span class="badge bg-secondary">{{ opcacheinfo.overview.readable.total_memory }}</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
{{ lng('opcacheinfo.used') }}
<span class="badge bg-secondary">{{ opcacheinfo.overview.readable.used_memory }}</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
{{ lng('opcacheinfo.free') }}
<span class="badge bg-secondary">{{ opcacheinfo.overview.readable.free_memory }}</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
{{ lng('opcacheinfo.wastedmem') }}
<span class="badge bg-secondary">{{ opcacheinfo.overview.readable.wasted_memory }}</span>
</li>
</ul>
</div>
</div>
<div class="col">
<div class="card h-100 mb-3">
<div class="card-body">
<h5 class="card-title">{{ lng('opcacheinfo.hitsc') }}</h5>
<div class="progress position-relative">
<div class="progress-bar bg-success" role="progressbar" style="width: {{ opcacheinfo.overview.hit_rate_percentage }}%" aria-valuenow="{{ opcacheinfo.overview.hits }}" aria-valuemin="0" aria-valuemax="{{ opcacheinfo.overview.hits + opcacheinfo.overview.misses }}"></div>
<div class="progress-bar bg-danger" role="progressbar" style="width: {{ 100 - opcacheinfo.overview.hit_rate_percentage }}%" aria-valuenow="{{ opcacheinfo.overview.misses }}" aria-valuemin="0" aria-valuemax="{{ opcacheinfo.overview.hits + opcacheinfo.overview.misses }}"></div>
<small class="justify-content-center d-flex position-absolute w-100 text-dark">{{ opcacheinfo.overview.hit_rate_percentage }}%</small>
</div>
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item d-flex justify-content-between align-items-center">
{{ lng('opcacheinfo.cachedscripts') }}
<span class="badge bg-secondary">{{ opcacheinfo.overview.readable.num_cached_scripts }}</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
{{ lng('opcacheinfo.hitsc') }}
<span class="badge bg-secondary">{{ opcacheinfo.overview.readable.hits }}</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
{{ lng('opcacheinfo.missc') }}
<span class="badge bg-secondary">{{ opcacheinfo.overview.readable.misses }}</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
{{ lng('opcacheinfo.blmissc') }}
<span class="badge bg-secondary">{{ opcacheinfo.overview.readable.blacklist_miss }}</span>
</li>
</ul>
</div>
</div>
<div class="col">
<div class="card h-100 mb-3">
<div class="card-body">
<h5 class="card-title">{{ lng('opcacheinfo.usedkey') }}</h5>
<div class="progress position-relative">
<div class="progress-bar bg-success" role="progressbar" style="width: {{ opcacheinfo.overview.used_key_percentage }}%" aria-valuenow="{{ opcacheinfo.overview.num_cached_keys }}" aria-valuemin="0" aria-valuemax="{{ opcacheinfo.overview.max_cached_keys }}"></div>
<small class="justify-content-center d-flex position-absolute w-100 text-dark">{{ opcacheinfo.overview.used_key_percentage }}%</small>
</div>
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item d-flex justify-content-between align-items-center">
{{ lng('opcacheinfo.maxkey') }}
<span class="badge bg-secondary">{{ opcacheinfo.overview.readable.max_cached_keys }}</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
{{ lng('opcacheinfo.usedkey') }}
<span class="badge bg-secondary">{{ opcacheinfo.overview.readable.num_cached_keys }}</span>
</li>
</ul>
</div>
</div>
<div class="col">
<div class="card h-100 mb-3">
<div class="card-body">
<h5 class="card-title">{{ lng('opcacheinfo.strinterning') }}</h5>
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item d-flex justify-content-between align-items-center">
{{ lng('opcacheinfo.totalmem') }}
<span class="badge bg-secondary">{{ opcacheinfo.overview.readable.interned.buffer_size }}</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
{{ lng('opcacheinfo.used') }}
<span class="badge bg-secondary">{{ opcacheinfo.overview.readable.interned.strings_used_memory }}</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
{{ lng('opcacheinfo.free') }}
<span class="badge bg-secondary">{{ opcacheinfo.overview.readable.interned.strings_free_memory }}</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
{{ lng('opcacheinfo.strcount') }}
<span class="badge bg-secondary">{{ opcacheinfo.overview.readable.interned.number_of_strings }}</span>
</li>
</ul>
</div>
</div>
</div>
<div class="row row-cols-1 row-cols-md-2">
<div class="col">
<div class="card table-responsive mb-3">
<table class="table table-borderless table-striped align-middle mb-0 px-3">
<tbody>
<tr>
<th class="text-center" colspan="2" scope="row">{{ lng('opcacheinfo.generaltitle') }}</th>
</tr>
<tr>
<th class="fw-bold" scope="row">{{ lng('opcacheinfo.version') }}</th>
<td class="text-end">{{ opcacheinfo.version.version }}</td>
</tr>
<tr>
<th class="fw-bold" scope="row">{{ lng('opcacheinfo.phpversion') }}</th>
<td class="text-end">{{ opcacheinfo.version.php }}</td>
</tr>
<tr>
<th class="fw-bold" scope="row">{{ lng('admin.hostname') }}</th>
<td class="text-end">{{ opcacheinfo.version.host }}</td>
</tr>
<tr>
<th class="fw-bold" scope="row">{{ lng('admin.serversoftware') }}</th>
<td class="text-end">{{ opcacheinfo.version.server }}</td>
</tr>
<tr>
<th class="fw-bold" scope="row">{{ lng('opcacheinfo.start') }}</th>
<td class="text-end">{{ opcacheinfo.overview.start_time|date('d.m.Y H:i:s') }}</td>
</tr>
<tr>
<th class="fw-bold" scope="row">{{ lng('opcacheinfo.lastreset') }}</th>
<td class="text-end">
{% if opcacheinfo.overview.last_restart_time > 0 %}
{{ opcacheinfo.overview.last_restart_time|date('d.m.Y H:i:s') }}
{% else %}
{{ lng('panel.never') }}
{% endif %}
</td>
</tr>
</tbody>
</table>
</div>
<div class="card table-responsive">
<table class="table table-borderless table-striped align-middle mb-0 px-3">
<tbody>
<tr>
<th class="text-center" scope="row">{{ lng('opcacheinfo.funcsavail') }}</th>
</tr>
{% for funcs in opcacheinfo.functions %}
<tr>
<td>{{ funcs }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="col">
<div class="card table-responsive">
<table class="table table-borderless table-striped align-middle mb-0 px-3">
<tbody>
<tr>
<th class="text-center" colspan="2" scope="row">{{ lng('opcacheinfo.runtimeconf') }}</th>
</tr>
{% for directive in opcacheinfo.directives %}
<tr>
<th class="fw-bold" scope="row">{{ directive.k }}</th>
<td class="text-end">
{% if directive.v is iterable %}
{% for vval in directive.v %}
{% if vval is iterable %}
{% for val2 in vval %}
{{ val2|raw }}<br>
{% endfor %}
{% else %}
{{ vval|raw }}<br>
{% endif %}
{% endfor %}
{% else %}
{{ directive.v|raw }}
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,42 @@
{% extends "Froxlor/userarea.html.twig" %}
{% block heading %}
<h5>
<i class="fa-solid fa-gears me-1"></i>
{{ lng('admin.phpinfo') }}
</h5>
{% endblock %}
{% block content %}
<div class="card table-responsive">
<table class="table table-borderless table-striped align-middle mb-0 px-3" id="phpinfotable">
<tbody>
{% for name,section in phpinfo %}
{% if name|lower == 'phpinfo' %}
{% set name = 'PHP ' ~ phpversion %}
{% endif %}
<tr>
<th colspan="3">{{ name|raw }}</th>
</tr>
{% for key,val in section %}
{% if key != 'Directive' %}
<tr>
{% if val is iterable %}
<td width="180">{{ key|raw }}</td>
<td colspan="2">{{ val[0]|raw }}<br/><small>(Master:
{{ val[1]|raw }})</small>
</td>
{% elseif key matches '/^\\d+$/' %}
<td colspan="3" align="center">{{ val|raw }}</td>
{% else %}
<td width="180">{{ key|raw }}</td>
<td colspan="2">{{ val|raw }}</td>
{% endif %}
</tr>
{% endif %}
{% endfor %}
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}

View File

@@ -0,0 +1,42 @@
<nav id="sidebar" class="sidebar collapse d-md-flex flex-shrink-0 flex-column bg-dark overflow-auto max-h-before-header">
<ul class="nav d-flex flex-fill flex-column py-3">
{% for idx,mitems in nav_entries %}
{% if mitems.items is not empty %}
<li class="nav-item" {% if mitems.active == 1 %}aria-current="page"{% endif %}>
<a class="nav-link text-light {% if mitems.active == 0 %}collapsed{% endif %}" href="#sub{{ idx }}" data-bs-toggle="collapse" data-bs-target="#sub{{ idx }}">
{% if mitems.icon is not empty %}
<i class="{{ mitems.icon }}"></i>
{% endif %}
{{ mitems.label }}
</a>
<div class="collapse {% if mitems.active == 1 %}show{% endif %}" id="sub{{ idx }}" aria-expanded="{% if mitems.active == 1 %}true{% else %}false{% endif %}">
<ul class="flex-column ps-3 nav">
{% for item in mitems.items %}
<li class="nav-item d-flex justify-content-between align-items-center" {% if item.active == 1 %}aria-current="page"{% endif %}>
<div class="me-auto">
<a class="nav-link text-light {% if item.active == 1 %}active fw-bold{% endif %}" href="{{ item.url|raw }}" {% if item.is_external is defined and item.is_external %}target="_blank"{% endif %}>{{ item.label|raw }}</a>
</div>
{% if item.add_shortlink is defined and item.add_shortlink is not empty %}
<a href="{{ item.add_shortlink|raw }}" class="text-secondary me-2"><i class="fa-solid fa-plus-circle"></i></a>
{% endif %}
{% if item.is_external is defined and item.is_external %}
<span class="me-2"><i class="fa-solid fa-arrow-up-right-from-square"></i></span>
{% endif %}
</li>
{% endfor %}
</ul>
</div>
</li>
{% else %}
<li class="nav-item" {% if mitems.active == 1 %}aria-current="page"{% endif %}>
<a class="nav-link text-light {% if mitems.active == 1 %}active{% endif %}" href="{% if mitems.url is not empty %}{{ mitems.url|raw }}{% else %}#{% endif %}" {% if mitems.target is not empty %} target="{{ mitems.target }}" {% endif %}>
{% if mitems.icon is not empty %}
<i class="{{ mitems.icon }}"></i>
{% endif %}
{{ mitems.label|upper }}
</a>
</li>
{% endif %}
{% endfor %}
</ul>
</nav>

View File

View File

@@ -0,0 +1,60 @@
$(function () {
var timer, delay = 500;
$('div[data-action="apikeys"] #allowed_from').on('keyup change', function () {
var _this = $(this);
clearTimeout(timer);
timer = setTimeout(function () {
var akid = _this.closest('div[data-action="apikeys"]').data('entry');
$.ajax({
url: "lib/ajax.php?action=editapikey",
type: "POST",
dataType: "json",
data: { id: akid, allowed_from: _this.val(), valid_until: $('div[data-entry="' + akid + '"] #valid_until').val() },
success: function (data) {
if (data.message) {
_this.removeClass('is-valid');
_this.addClass('is-invalid');
} else {
_this.removeClass('is-invalid');
_this.addClass('is-valid');
_this.val(data.allowed_from);
}
},
error: function (request, status, error) {
_this.removeClass('is-valid');
_this.addClass('is-invalid');
}
});
}, delay);
});
$('div[data-action="apikeys"] #valid_until').on('keyup change', function () {
var _this = $(this);
clearTimeout(timer);
timer = setTimeout(function () {
var akid = _this.closest('div[data-action="apikeys"]').data('entry');
$.ajax({
url: "lib/ajax.php?action=editapikey",
type: "POST",
dataType: "json",
data: { id: akid, valid_until: _this.val(), allowed_from: $('div[data-entry="' + akid + '"] #allowed_from').val() },
success: function (data) {
if (data.message) {
_this.removeClass('is-valid');
_this.addClass('is-invalid');
} else {
_this.removeClass('is-invalid');
_this.addClass('is-valid');
_this.val(data.valid_until);
}
},
error: function (request, status, error) {
_this.removeClass('is-valid');
_this.addClass('is-invalid');
}
});
}, delay);
});
});

View File

@@ -0,0 +1,52 @@
$(function () {
/*
* config files - select all recommended
*/
$('#selectRecommendedConfig').on('click', function () {
$('input[data-recommended]').each(function () {
if ($(this).data('recommended') == 1) {
$(this).prop('checked', true);
} else {
$(this).prop('checked', false);
}
})
});
/*
* export/download JSON file (e.g. for usage with config-services)
*/
$('#downloadSelectionAsJson').on('click', function () {
var formData = $(this).closest('form').serialize();
window.location = "lib/ajax.php?action=getConfigJsonExport&" + formData;
});
/*
* open modal window to show selected config-commands/files
* for selected daemon
*/
$('.show-config').on('click', function () {
var distro = $(this).data('dist');
var section = $(this).data('section');
var daemon = $(this).data('daemon');
$.ajax({
url: "lib/ajax.php?action=getConfigDetails",
type: "POST",
dataType: "json",
data: { distro: distro, section: section, daemon: daemon },
success: function (data) {
$('#configTplShowLabel').html(data.title);
$('#configTplShow .modal-body').html(data.content);
var myModal = new bootstrap.Modal(document.getElementById('configTplShow'));
myModal.show();
},
error: function (request, status, error) {
$('#configTplShowLabel').html('Error');
$('#configTplShow .modal-body').html('<div class="alert alert-danger" role="alert">' + request.responseJSON.message + '</div>');
var myModal = new bootstrap.Modal(document.getElementById('configTplShow'));
myModal.show();
}
});
});
});

View File

@@ -0,0 +1,76 @@
$(function() {
// Make inputs with enabled unlimited checked disabled
$("input[name$='_ul']").each(function () {
var fieldname = $(this).attr("name").substring(0, $(this).attr("name").length - 3);
$("input[name='" + fieldname + "']").prop({
readonly: $(this).is(":checked"),
required: !$(this).is(":checked")
});
});
// change state when unlimited checkboxes are clicked
$("input[name$='_ul']").on('change', function () {
var fieldname = $(this).attr("name").substring(0, $(this).attr("name").length - 3);
$("input[name='" + fieldname + "']").prop({
readonly: $(this).is(":checked"),
required: !$(this).is(":checked")
});
if (!$(this).is(":checked")) {
$("input[name='" + fieldname + "']").focus()
}
});
// set values from hosting plan when adding/editing a customer according to the plan's values
$('#use_plan').on('change', function () {
var pid = $(this).val();
if (pid > 0) {
$.ajax({
url: "admin_plans.php?page=overview&action=jqGetPlanValues",
type: "POST",
data: {
planid: pid
},
dataType: "json",
success: function (json) {
for (var i in json) {
if (i == 'email_imap' || i == 'email_pop3' || i == 'perlenabled' || i == 'phpenabled' || i == 'dnsenabled' || i == 'logviewenabled') {
/** handle checkboxes **/
if (json[i] == 1) {
$("input[name='" + i + "']").prop('checked', true);
} else {
$("input[name='" + i + "']").prop('checked', false);
}
} else if (i == 'allowed_phpconfigs') {
/** handle array of values **/
$("input[name='allowed_phpconfigs[]']").each(function (index) {
$(this).prop('checked', false);
for (var j in json[i]) {
if ($(this).val() == json[i][j]) {
$(this).prop('checked', true);
break;
}
}
});
} else if (json[i] == -1) {
/** handle unlimited checkboxes **/
$("input[name='" + i + "_ul']").attr('checked', 'checked');
$("input[name='" + i + "']").prop({
readonly: true
});
} else {
/** handle normal value **/
$("input[name='" + i + "']").val(json[i]);
$("input[name='" + i + "']").prop({
readonly: false
});
$("input[name='" + i + "_ul']").prop('checked', false);
}
}
},
error: function (a, b) {
console.log(a, b);
}
});
}
});
});

View File

@@ -0,0 +1,19 @@
$(function () {
// Display helptext to content box according to dns-record type selected
$("select[name='dns_type']").on('change', function () {
var selVal = $(this).val();
$.ajax({
url: "lib/ajax.php?action=loadLanguageString",
type: "POST",
dataType: "json",
data: { langid: 'dnseditor.notes.' + selVal },
success: function (data) {
$("#dns_content").next().html(data);
},
error: function (request, status, error) {
console.log(request, status, error)
}
});
});
});

View File

@@ -0,0 +1,87 @@
$(function() {
// disable unusable php-configuration by customer settings
$('#customerid').on('change', function () {
var cid = $(this).val();
$.ajax({
url: "admin_domains.php?page=domains&action=jqGetCustomerPHPConfigs",
type: "POST",
data: {
customerid: cid
},
dataType: "json",
success: function (json) {
if (json.length > 0) {
$('#phpsettingid option').each(function () {
var pid = $(this).val();
$(this).attr("disabled", "disabled");
for (var i in json) {
if (pid == json[i]) {
$(this).removeAttr("disabled");
}
}
});
}
},
error: function (a, b) {
console.log(a, b);
}
});
});
// show warning if speciallogfile option is toggled
if ($('input[name=speciallogverified]')) {
$('input[name=speciallogfile]').on('click', function () {
$('#speciallogfilenote').remove();
$('#speciallogfile').removeClass('is-invalid');
$('#speciallogverified').val(0);
$.ajax({
url: "admin_domains.php?page=overview&action=jqSpeciallogfileNote",
type: "POST",
data: {
id: $('input[name=id]').val(), newval: +$('#speciallogfile').is(':checked')
},
dataType: "json",
success: function (json) {
if (json.changed) {
$('#speciallogfile').addClass('is-invalid');
$('#speciallogfile').parent().append(json.info);
$('#speciallogverified').val(1);
}
},
error: function (a, b) {
console.log(a, b);
}
});
});
}
/**
* email only domain - hide unnecessary/unused sections
*/
if ($('#id') && $('#email_only').is(':checked')) {
$('#section_b').hide();
$('#section_bssl').hide();
$('#section_c').hide();
$('#section_d').hide();
}
/**
* toggle show/hide of sections in case of email only flag
*/
$('#email_only').on('click', function () {
if ($(this).is(':checked')) {
// hide unnecessary sections
$('#section_b').hide();
$('#section_bssl').hide();
$('#section_c').hide();
$('#section_d').hide();
} else {
// show sections
$('#section_b').show();
$('#section_bssl').show();
$('#section_c').show();
$('#section_d').show();
}
})
});

View File

@@ -0,0 +1,12 @@
$(function () {
$('#historyback').on('click', function (e) {
e.preventDefault();
history.back(1);
})
$('#copySysInfo').on('click', function (e) {
e.preventDefault();
navigator.clipboard.writeText($('#ccSysInfo').text().trim());
})
});

View File

@@ -0,0 +1,62 @@
$(function () {
/*
* switch between basic and advanced install mode
*/
$('#switchInstallMode').on('click', function () {
var checked = $(this).prop('checked');
window.location = window.location.pathname + replaceQueryParam('extended', +checked, window.location.search);
});
function replaceQueryParam(param, newval, search) {
var regex = new RegExp("([?;&])" + param + "[^&;]*[;&]?");
if (search.match(regex)) {
search = search.replace(regex, "$1").replace(/&$/, '');
}
return search + '&' + param + '=' + newval;
}
function checkConfigState() {
$.ajax({
url: window.location.href,
type: "GET",
success: function (data, textStatus, request) {
if (request.status >= 300) {
window.location = "http://" + srvName;
}
},
error: function (request, textStatus, errorThrown) {
// continue
if (request.status >= 300) {
window.location = "http://" + srvName;
}
}
});
}
var cTimer;
/**
* check manual-config switch
*/
$('#manual_config').on('click', function () {
clearInterval(cTimer);
var checked = $(this).prop('checked');
if (checked) {
// button zum login
$('#submitAuto').addClass('d-none');
$('#submitManual').removeClass('d-none');
} else {
cTimer = setInterval(checkConfigState, 1000);
// spinner fürs warten
$('#submitAuto').removeClass('d-none');
$('#submitManual').addClass('d-none');
}
});
if ($('#manual_config').length > 0) {
var srvName = $('#target_servername').val();
clearInterval(cTimer);
cTimer = setInterval(checkConfigState, 1000);
}
});

View File

@@ -0,0 +1,29 @@
$(function() {
// check for internal ip and output a notice if private-range ip is given
$('#ip').on('change', function () {
var ipval = $(this).val();
if (ipval.length > 0) {
$('#ipnote').remove();
$('#ip').removeClass('is-invalid');
$.ajax({
url: "admin_ipsandports.php?page=overview&action=jqCheckIP",
type: "POST",
data: {
ip: ipval
},
dataType: "json",
success: function (json) {
if (json != 0) {
$('#ip').addClass('is-invalid');
$('#ip').parent().append(json);
}
},
error: function (a, b) {
console.log(a, b);
}
});
}
});
});

View File

@@ -0,0 +1,24 @@
$(function() {
/*
* newsfeed
*/
if (document.getElementById('newsfeed')) {
let role = "";
if (typeof $("#newsfeed").data("role") !== "undefined") {
role = "&role=" + $("#newsfeed").data("role");
}
$.ajax({
url: "lib/ajax.php?action=newsfeed" + role + "&theme=" + window.$theme,
type: "GET",
success: function (data) {
$("#newsfeeditems").html(data);
},
error: function (request, status, error) {
console.log(request, status, error)
$("#newsfeeditems").html('<div class="list-group-item text-center"><span class="badge bg-warning" role="alert">Error loading newsfeed</span></div>');
}
});
}
});

View File

@@ -0,0 +1,56 @@
$(function() {
let search = $('#search')
search.on('submit', function (e) {
e.preventDefault();
});
search.find('input').on('keyup', function () {
let query = $(this).val();
let dropdown = $('#search .search-results');
// Hide search if query is empty
if (!query.length) {
dropdown.html('');
dropdown.parent().hide();
return;
}
// Show notification for short search query
if (query.length && query.length < 3) {
dropdown.html('<li class="list-group-item text-muted py-1">Please enter more than 2 characters</li>');
dropdown.parent().show();
return;
}
// Search
$.ajax({
url: "lib/ajax.php?action=searchglobal&theme=" + window.$theme,
type: "POST",
data: {
searchtext: query
},
dataType: "json",
success: data => {
// Show notification if we got no results
if (Object.keys(data).length === 0) {
dropdown.html('<li class="list-group-item text-muted py-1">Nothing found!</li>');
dropdown.parent().show();
return;
}
// Clear dropdown and show results
dropdown.html('');
dropdown.parent().show();
Object.keys(data).forEach(key => {
dropdown.append('<li class="list-group-item text-muted text-capitalize fw-bold py-1 border-bottom">' + key + '</li>');
data[key].forEach(item => {
dropdown.append('<li class="list-group-item mt-1"><a href="' + item.href + '" class="text-decoration-none">' + item.title + '</a></li>');
});
});
},
error: function (a, b) {
console.log(a, b);
dropdown.html('<li class="list-group-item text-muted py-1">Whoops we got some errors!</li>');
dropdown.parent().show();
}
});
});
});

View File

@@ -0,0 +1,44 @@
$(function () {
/*
* table columns - manage columns modal
*/
$('.manageColumnsModal form').on('submit', function (event) {
$.ajax({
url: 'lib/ajax.php?action=updatetablelisting&listing=' + $(this).data('listing') + '&theme=' + window.$theme,
type: 'POST',
dataType: 'json',
data: $(this).serialize(),
success: function () {
window.location.href = '';
},
error: function (request) {
alert(request.responseJSON.message);
}
});
event.preventDefault();
});
$('.manageColumnsModal form button[data-action="reset"]').on('click', function () {
var form = $(this).parents('form:first');
$.ajax({
url: 'lib/ajax.php?action=resettablelisting&listing=' + form.data('listing') + '&theme=' + window.$theme,
type: 'POST',
dataType: 'json',
data: {},
success: function () {
window.location.href = '';
},
error: function (request) {
alert(request.responseJSON.message);
}
});
});
$('.manageColumnsModal form button[data-action="select-all"]').on('click', function () {
$(this).parents('form:first').find('input:checkbox').prop('checked', true);
});
$('.manageColumnsModal form button[data-action="unselect-all"]').on('click', function () {
$(this).parents('form:first').find('input:checkbox').prop('checked', false);
});
});

View File

@@ -0,0 +1,9 @@
$(function () {
// Display helptext to content box according to dns-record type selected
$("select[name='range']").on('change', function () {
var selVal = $(this).val();
var baseRef = $(this).data('baseref');
window.location.href = baseRef + '?range=' + selVal;
});
});

View File

@@ -0,0 +1,21 @@
$(function() {
/*
* updatecheck
*/
if (document.getElementById('updatecheck')) {
$.ajax({
url: "lib/ajax.php?action=updatecheck&theme=" + window.$theme,
type: "GET",
success: function (data) {
$("#updatecheck").html(data);
new bootstrap.Popover(document.getElementById('ucheck'));
},
error: function (request, status, error) {
console.log(request, status, error)
let message = 'Can\'t check version';
$("#updatecheck").html('<span id="ucheck" class="text-decoration-none badge bg-warning mt-2 me-2" data-bs-toggle="tooltip" data-bs-placement="left" title="' + message + '"><i class="fa-solid fa-exclamation-triangle"></i> <span class="d-md-none d-xl-inline">' + message + '</span></span>');
new bootstrap.Tooltip(document.getElementById('ucheck'));
}
});
}
});

View File

@@ -0,0 +1,37 @@
$(document).ready(function () {
$('#customer_add,#customer_edit').each(function () {
$(this).validate({
rules: {
'name': {
required: function () {
return $('#company').val().length === 0 || $('#firstname').val().length > 0;
}
},
'firstname': {
required: function () {
return $('#company').val().length === 0 || $('#name').val().length > 0;
}
},
'company': {
required: function () {
return $('#name').val().length === 0
&& $('#firstname').val().length === 0;
}
}
},
});
});
$('#domain_add,#domain_edit').each(function () {
$(this).validate({
rules: {
'ipandport[]': {
required: true,
minlength: 1
}
},
errorPlacement: function(error, element) {
$(error).prependTo($(element).parent().parent());
}
});
});
});

View File

View File

@@ -0,0 +1,44 @@
// import libs
import 'bootstrap';
import '@fortawesome/fontawesome-free';
import Chart from 'chart.js/auto';
// set jquery & bootstrap & chart
global.$ = require('jquery');
global.validation = require('jquery-validation');
global.bootstrap = require('bootstrap');
window.Chart = Chart;
$(function () {
window.$theme = 'Froxlor';
$.ajaxSetup({
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
}
});
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
const tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl)
})
const popoverTriggerList = document.querySelectorAll('[data-bs-toggle="popover"]')
const popoverList = [...popoverTriggerList].map(popoverTriggerEl => new bootstrap.Popover(popoverTriggerEl))
});
// Load components
require('./components/apikeys')
require('./components/configfiles')
require('./components/customer')
require('./components/dnseditor')
require('./components/domains')
require('./components/global')
require('./components/install')
require('./components/ipsandports')
require('./components/newsfeed')
require('./components/search')
require('./components/tablecolumns')
require('./components/traffic')
require('./components/updatecheck')
require('./components/validation')

View File

@@ -0,0 +1,7 @@
.alert {
@extend .shadow-sm;
p:last-of-type {
margin-bottom: 0;
}
}

View File

@@ -0,0 +1,24 @@
.card {
@extend .shadow-sm;
margin-bottom: $spacer * 1.5;
.card-header {
border-bottom: $border-color solid 1px;
font-weight: bold;
}
.card-body {
p {
@extend .card-text;
}
}
&.deactivated {
@extend .text-muted;
background: lighten($light-bg, 3%);
i {
@extend .text-muted;
}
}
}

View File

@@ -0,0 +1,12 @@
.dropdown {
.dropdown-menu {
.dropdown-item {
i {
width: 1rem;
margin-right: 1rem;
text-align: center;
color: $text-muted;
}
}
}
}

View File

@@ -0,0 +1,13 @@
footer {
@extend .small;
a {
@extend .text-muted;
@extend .text-decoration-none;
}
.footer-link:not(:last-child):after {
content: '';
padding: 0 0.25rem;
}
}

View File

@@ -0,0 +1 @@
// wip

View File

@@ -0,0 +1,89 @@
// Fontawesome
@import "~@fortawesome/fontawesome-free/scss/fontawesome";
@import "~@fortawesome/fontawesome-free/css/all";
// Generic
.header-logo {
height: 24px;
}
.form-control-plaintext {
outline: none;
}
.form-control[readonly] {
background: rgba(0, 0, 0, .15);
}
.page-header {
margin-bottom: 2rem;
&:after {
margin-top: .5rem;
display: block;
height: 3px;
width: 100px;
background: $froxlor-800;
border-radius: 3px;
content: ' ';
}
}
.alert-icon {
padding: .5rem;
background: rgba(0, 0, 0, .15);
text-align: center;
border-radius: $border-radius;
margin-right: .75rem;
}
.max-w-420 {
max-width: 420px;
}
.max-w-xs {
max-width: 575.98px;
}
.max-w-lg {
max-width: 991.98px;
}
.rounded-tl-bl {
border-radius: $border-radius 0 $border-radius 0;
}
.progress-thin {
height: .5rem;
}
.logcontent {
width: 100%;
}
.formfield {
padding: 1rem $spacer;
border-bottom: $border-color solid 1px;
&:last-child {
border-bottom: none;
}
}
.focus-none {
outline: none !important;
box-shadow: none !important;
}
.field-image-preview {
max-height: 5em;
}
#phpinfotable {
table-layout: fixed;
word-wrap: break-word;
}
a {
text-decoration: none;
}

View File

@@ -0,0 +1,13 @@
.heading {
color: $heading-color;
background-color: $heading-bg;
border-top: $heading-border-color solid 1px;
}
.heading h5 {
color: $heading-color;
}
.heading span {
}

View File

@@ -0,0 +1,60 @@
.navbar {
z-index: 20;
}
.navbar-brand {
padding: 1rem 0;
}
@include media-breakpoint-up(md) {
.navbar {
background: $navbar-bg;
.navbar-brand {
background: $dark;
width: $sidebar-width;
margin-right: 0;
flex-shrink: 0;
}
}
}
@include media-breakpoint-down(md) {
.navbar {
background: $navbar-bg-mobile;
.navbar-nav {
flex-direction: row;
gap: .75rem;
.nav-link {
color: $white;
&:hover {
color: rgba(255,255,255,.45);
}
}
.dropdown-menu {
position: absolute;
}
}
#collapseSearch {
border-top: solid 1px $dark;
#search {
margin-bottom: 1.125rem;
}
}
}
.navbar-light {
.navbar-toggler {
border-color: transparent;
}
.navbar-toggler-icon {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
}
}
}

View File

@@ -0,0 +1,34 @@
#search {
display: flex;
position: relative;
i {
margin-right: .5rem;
}
.search-input {
color: $body-color;
outline: none;
border: none;
background: transparent;
}
.search-results-box {
position: absolute;
top: 2.75rem;
z-index: 50;
width: 70vh;
max-height: 50vh;
background: $search-bg;
border: $border-color solid 1px;
border-radius: 0 0 $border-radius $border-radius;
.search-results {
width: 100%; /** same as .search-results-box **/
max-height: calc(50vh - 1.25em); /** same as .search-results-box - border (top/bottom) **/
overflow: auto;
border-radius: $border-radius;
}
}
}

View File

@@ -0,0 +1,50 @@
.sidebar, .sub-sidebar {
width: $sidebar-width;
z-index: 10;
}
.sidebar {
@extend .shadow;
width: $sidebar-width;
&.collapsing {
transition: none;
}
.user-info {
background: darken($dark, 2);
}
> .nav {
> .nav-item {
> .nav-link {
i {
margin-right: 1rem;
width: 1rem;
text-align: center;
opacity: .5;
}
&:not(.collapsed) {
background: darken($dark, 4);
border-left: $primary solid 3px;
padding-left: calc(1rem - 3px);
}
}
> .collapse, > .collapsing {
background: darken($dark, 2);
a {
opacity: .78;
margin-left: 1rem;
}
}
}
}
}
.sub-sidebar {
@extend .shadow-sm;
background: $white;
}

View File

@@ -0,0 +1,35 @@
@charset "UTF-8";
// Bootstrap
@import "variables/dark";
@import "~bootstrap/scss/bootstrap";
// Theme
@import "components/generic";
@import "components/alert";
@import "components/card";
@import "components/dropdown";
@import "components/footer";
@import "components/form";
@import "components/heading";
@import "components/navbar";
@import "components/sidebar";
@import "components/search";
.navbar-light {
.navbar-toggler {
border-color: transparent;
}
.navbar-toggler-icon {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
}
}
tr.bg-info td,
tr.bg-warning td {
color: $dark-bg !important;
}
.badge.bg-secondary {
color: $gray-700;
}

View File

View File

@@ -0,0 +1,17 @@
@charset "UTF-8";
// Bootstrap
@import "variables/main";
@import "~bootstrap/scss/bootstrap";
// Theme
@import "components/generic";
@import "components/alert";
@import "components/card";
@import "components/dropdown";
@import "components/footer";
@import "components/form";
@import "components/heading";
@import "components/navbar";
@import "components/sidebar";
@import "components/search";

View File

@@ -0,0 +1,66 @@
@import "main";
// Color
$primary: $froxlor-700;
$secondary: $gray-500;
$info: $froxlor-800;
// Body
$body-bg: $gray-900;
$body-color: $gray-100;
// Borders
$border-color: $gray-900;
$border-color-translucent: $gray-900;
// Link
$link-color: $froxlor-500;
$nav-link-color: $body-color;
// List groups
$list-group-bg: $gray-800;
$list-group-color: $body-color;
$list-group-hover-bg: $gray-700;
$list-group-action-color: $body-color;
// Navbar
$navbar-bg: $gray-800;
$navbar-bg-mobile: $navbar-bg;
$navbar-light-color: $gray-200;
$navbar-light-hover-color: $gray-500;
$navbar-light-active-color: $gray-500;
// Sidebar
// Card
$card-bg: $gray-800;
$card-border-color: $gray-600;
// Heading
$heading-bg: $gray-800;
$heading-color: $body-color;
$heading-border-color: rgba(0,0,0,0.15);
// Dropdown
$dropdown-bg: $gray-800;
$dropdown-color: $gray-100;
$dropdown-link-color: $gray-100;
$dropdown-link-hover-bg: $gray-900;
// Modal
$modal-content-bg: $gray-800;
// Form control
$input-bg: $gray-900;
$input-border-color: $black;
$input-group-addon-bg: $gray-800;
// Progress bar
$progress-bg: $gray-900;
// Search
$search-bg: $gray-800;
// Popover
$popover-bg: $gray-800;
$popover-body-color: $gray-100;

View File

@@ -0,0 +1,86 @@
// CI
$froxlor-50: #e1f4fa;
$froxlor-100: #b3e3f1;
$froxlor-200: #84d0e9;
$froxlor-300: #5abde0;
$froxlor-400: #3eb0db;
$froxlor-500: #29a2d6;
$froxlor-600: #2395c8;
$froxlor-700: #1a83b6;
$froxlor-800: #1872a2;
$froxlor-900: #0e5380;
// Gray
$white: #fff;
$gray-100: #f8f9fa;
$gray-200: #e9ecef;
$gray-300: #dee2e6;
$gray-400: #ced4da;
$gray-500: #adb5bd;
$gray-600: #6c757d;
$gray-700: #495057;
$gray-800: #343a40;
$gray-900: #212529;
$black: #000;
// Colors
$light: $gray-100;
$primary: $froxlor-800;
$secondary: $gray-600;
$info: $froxlor-900;
$warning: #FBBF24;
$danger: #BE123C;
$success: #059669;
// Body
$light-bg: $gray-100;
$dark-bg: $gray-800;
// Typography
$light-font-color: $gray-800;
$dark-font-color: $gray-100;
$alert-bg-scale: 0;
$alert-color-scale: 0;
$alert-border-width: 0;
$list-group-item-color-scale: 0;
$list-group-item-bg-scale: 0;
$input-bg: lighten($light-bg, 5%);
$font-size-root: 16px;
// Spacing
$spacer: 1.25rem;
$enable-negative-margins: true;
// Body
$body-bg: $light-bg;
$body-color: $light-font-color;
// Borders
// $border-radius: 0.5rem;
// Links
$link-color: $froxlor-800;
// Navbar
$navbar-bg: $white;
$navbar-bg-mobile: $gray-900;
// Sidebar
$sidebar-width: 256px;
// Card
$card-cap-bg: none;
$card-cap-padding-y: $spacer;
$card-border-width: 0;
// Heading
$heading-bg: $navbar-bg;
$heading-color: $body-color;
$heading-border-color: #dee2e6;
// Search
$search-bg: $navbar-bg;

View File

View File

@@ -0,0 +1,92 @@
{% macro progressbar(data) %}
<div class="progress progress-thin">
<div class="progress-bar {{ data.style }}" style="width: {{ data.percent }}%;"></div>
</div>
<div class="text-end small">
{% if data.infotext is not empty %}
<i class="fa-solid fa-circle-info" data-bs-trigger="hover" data-bs-toggle="popover" data-bs-placement="bottom" data-bs-html="true" data-bs-content="{{ data.infotext|raw|nl2br }}"></i>
{% endif %}
{{ data.text }}
</div>
{% endmacro %}
{% macro boolean(data) %}
{% if (data) %}
<i class="fa-solid fa-check-circle text-success"></i>
{% else %}
<i class="fa-solid fa-times-circle text-danger"></i>
{% endif %}
{% endmacro %}
{% macro booleanWithInfo(data) %}
{% if (data.checked) %}
<i class="fa-solid fa-check-circle text-success"></i>
{% else %}
<i class="fa-solid fa-times-circle text-danger"></i>
{% endif %}
{% if data.info is not empty %}
{{ data.info }}
{% endif %}
{% endmacro %}
{% macro link(data) %}
{% apply spaceless %}
<a href="{{ data.href }}" {% if data.class is defined %} class="{{ data.class }}" {% endif %} {% if data.target is defined %} target="{{ data.target }}" {% endif %} {% if data.title is defined %} title="{{ data.title }}" {% endif %}>
{% if data.icon is defined %}
<i class="{{ data.icon }}"></i>
{% endif %}
{% if data.text is defined %}
{{ data.text }}
{% endif %}
</a>
{% endapply %}
{% endmacro %}
{% macro button(data) %}
{% apply spaceless %}
<{% if data.href is defined %}a{% else %}span{% endif %} class="{% if data.class is defined %}btn btn-sm {{ data.class }}{% else %}btn btn-sm btn-outline-secondary{% endif %}" {% if data.modal is defined and data.modal is iterable %} data-bs-toggle="modal" role="button" href="#{{ data.modal.id }}" {% else %} {% if data.href is defined %}href="{{ data.href }}"{% endif %} {% endif %} {% if data.target is defined %} target="{{ data.target }}" {% endif %} {% if data.title is defined %} title="{{ data.title }}" {% endif %}>
{% if data.icon is defined %}
<i class="{{ data.icon }}"></i>
{% endif %}
{% if data.text is defined %}
{{ data.text }}
{% endif %}
</{% if data.href is defined %}a{% else %}span{% endif %}>
{% if data.modal is defined and data.modal is iterable %}
<div class="modal fade" data-action="{{ data.modal.action|default('') }}" data-entry="{{ data.modal.entry }}" id="{{ data.modal.id }}" aria-hidden="true" aria-labelledby="{{ data.modal.id }}Label" tabindex="-1">
<div class="modal-dialog {{ data.modal.size|default('modal-xl') }} modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="{{ data.modal.id }}Label">{{ data.modal.title }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{{ lng('panel.modalclose') }}"></button>
</div>
<div class="modal-body text-start">
{{ data.modal.body|raw }}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">{{ lng('panel.modalclose') }}</button>
</div>
</div>
</div>
</div>
{% endif %}
{% endapply %}
{% endmacro %}
{% macro domainWithSan(data) %}
{{ data.domain }}
{% if data.san is not empty %}
<br/>
<span class="small">
SAN: {{ data.san }}
</span>
{% endif %}
{% endmacro %}
{% macro actions(data) %}
{% for action in data %}
{% if action.visible is not defined or action.visible is defined and action.visible %}
{{ _self.button(action) }}
{% endif %}
{% endfor %}
{% endmacro %}

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