diff --git a/Backend/WebServer/MySQL.go b/Backend/WebServer/MySQL.go
index 4075656..826efdb 100644
--- a/Backend/WebServer/MySQL.go
+++ b/Backend/WebServer/MySQL.go
@@ -4,7 +4,7 @@ import (
"fmt"
"os/exec"
"path/filepath"
- "strconv"
+ "syscall"
"time"
config "vServer/Backend/config"
tools "vServer/Backend/tools"
@@ -14,6 +14,11 @@ var mysqlProcess *exec.Cmd
var mysql_status bool = false
var mysql_secure bool = false
+// GetMySQLStatus возвращает статус MySQL
+func GetMySQLStatus() bool {
+ return mysql_status
+}
+
var mysqldPath string
var configPath string
var dataDirAbs string
@@ -86,16 +91,8 @@ func StartMySQLServer(secure bool) {
mysql_port = config.ConfigData.Soft_Settings.Mysql_port
mysql_ip = config.ConfigData.Soft_Settings.Mysql_host
- if tools.Port_check("MySQL", mysql_ip, strconv.Itoa(mysql_port)) {
- return
- }
-
if mysql_status {
- tools.Logs_file(1, "MySQL", "Сервер MySQL уже запущен", "logs_mysql.log", true)
- return
- }
-
- if false {
+ tools.Logs_file(1, "MySQL", "Сервер MySQL уже запущен", "logs_mysql.log", false)
return
}
@@ -105,9 +102,9 @@ func StartMySQLServer(secure bool) {
// Выбор сообщения
if secure {
- tools.Logs_file(0, "MySQL", "Запуск сервера MySQL в режиме безопасности", "logs_mysql.log", true)
+ tools.Logs_file(0, "MySQL", "Запуск сервера MySQL в режиме безопасности", "logs_mysql.log", false)
} else {
- tools.Logs_file(0, "MySQL", "Запуск сервера MySQL в обычном режиме", "logs_mysql.log", true)
+ tools.Logs_file(0, "MySQL", "Запуск сервера MySQL в обычном режиме", "logs_mysql.log", false)
}
// Общая логика запуска
@@ -115,7 +112,7 @@ func StartMySQLServer(secure bool) {
mysqlProcess.Dir = binDirAbs
tools.Logs_console(mysqlProcess, console_mysql)
- tools.Logs_file(0, "MySQL", fmt.Sprintf("Сервер MySQL запущен на %s:%d", mysql_ip, mysql_port), "logs_mysql.log", true)
+ tools.Logs_file(0, "MySQL", fmt.Sprintf("Сервер MySQL запущен на %s:%d", mysql_ip, mysql_port), "logs_mysql.log", false)
mysql_status = true
@@ -124,22 +121,30 @@ func StartMySQLServer(secure bool) {
// StopMySQLServer останавливает MySQL сервер
func StopMySQLServer() {
- if mysql_status {
-
- cmd := exec.Command("taskkill", "/F", "/IM", "mysqld.exe")
-
- err := cmd.Run()
- tools.CheckError(err)
-
- tools.Logs_file(0, "MySQL", "Сервер MySQL остановлен", "logs_mysql.log", true)
- mysql_status = false
-
- } else {
-
- tools.Logs_file(1, "MySQL", "Сервер MySQL уже остановлен", "logs_mysql.log", true)
-
+ if !mysql_status {
+ return // Уже остановлен
}
+ // Сначала пробуем завершить процесс корректно
+ if mysqlProcess != nil && mysqlProcess.Process != nil {
+ mysqlProcess.Process.Kill()
+ mysqlProcess = nil
+ }
+
+ // Дополнительно убиваем все mysqld.exe процессы
+ cmd := exec.Command("taskkill", "/F", "/IM", "mysqld.exe")
+
+ // Скрываем окно taskkill
+ cmd.SysProcAttr = &syscall.SysProcAttr{
+ HideWindow: true,
+ CreationFlags: 0x08000000,
+ }
+
+ cmd.Run()
+
+ tools.Logs_file(0, "MySQL", "Сервер MySQL остановлен", "logs_mysql.log", false)
+ mysql_status = false
+
}
func ResetPasswordMySQL() {
diff --git a/Backend/WebServer/cmd_console.go b/Backend/WebServer/cmd_console.go
deleted file mode 100644
index 41e1ac2..0000000
--- a/Backend/WebServer/cmd_console.go
+++ /dev/null
@@ -1,195 +0,0 @@
-package webserver
-
-import (
- "fmt"
- "os"
- "time"
- admin "vServer/Backend/admin"
- config "vServer/Backend/config"
- tools "vServer/Backend/tools"
-)
-
-var Secure_post bool = false
-
-func CommandListener() {
-
- fmt.Println("Введите help для получения списка команд")
- fmt.Println("")
-
- for {
- var cmd string
-
- fmt.Print(tools.Color(" > ", tools.Оранжевый))
-
- fmt.Scanln(&cmd)
-
- switch cmd {
- case "help":
-
- fmt.Println(" ------------------------------------------")
- fmt.Println(" 1: mysql_stop - Остановить MySQL")
- fmt.Println(" 2: mysql_start - Запустить MySQL")
- fmt.Println(" 3: mysql_pass - Сбросить пароль MySQL")
- fmt.Println(" 4: clear - Очистить консоль")
- fmt.Println(" 5: cert_reload - Перезагрузить SSL сертификаты")
- fmt.Println(" 6: admin_toggle - Переключить режим админки (embed/файловая система)")
- fmt.Println(" 7: config_reload - Перезагрузить конфигурацию")
- fmt.Println(" 8: restart - Перезапустить сервер")
- fmt.Println(" 9: php_console - Открыть PHP консоль")
- fmt.Println(" 10: exit - выйти из программы")
- fmt.Println(" ------------------------------------------")
- fmt.Println("")
-
- case "mysql_stop":
- StopMySQLServer()
-
- case "mysql_start":
- StartMySQLServer(false)
-
- case "mysql_pass":
- ResetPasswordMySQL()
-
- case "clear":
- ClearConsole()
-
- case "cert_reload":
- ReloadCertificates()
-
- case "admin_toggle":
- AdminToggle()
-
- case "config_reload":
- ConfigReload()
-
- case "restart":
- RestartServer()
-
- case "time_run":
- fmt.Println(tools.ServerUptime("get"))
-
- case "secure_post":
-
- if Secure_post {
- Secure_post = false
- fmt.Println("Secure post is disabled")
- } else {
- Secure_post = true
- fmt.Println("Secure post is enabled")
- }
-
- case "exit":
- fmt.Println("Завершение...")
- os.Exit(0)
-
- default:
-
- fmt.Println(" Неизвестная команда. Введите 'help' для получения списка команд")
- fmt.Println("")
-
- }
- }
-}
-
-func RestartServer() {
- fmt.Println("")
- fmt.Println("⏹️ Перезагрузка сервера...")
-
- // Останавливаем все сервисы
- fmt.Println("⏹️ Останавливаем сервисы...")
- fmt.Println("")
-
- // Останавливаем HTTP/HTTPS серверы
- StopHTTPServer()
- StopHTTPSServer()
-
- // Останавливаем MySQL
- if mysql_status {
- StopMySQLServer()
- time.Sleep(1 * time.Second)
- }
-
- // Останавливаем PHP
- PHP_Stop()
- time.Sleep(1 * time.Second)
-
- fmt.Println("")
- fmt.Println("✅ Все сервисы остановлены")
-
- // Перезагружаем конфигурацию
- fmt.Println("📋 Перезагружаем конфигурацию...")
- fmt.Println("")
- config.LoadConfig()
-
- // Запускаем сервисы заново
- fmt.Println("🚀 Запускаем сервисы...")
- fmt.Println("")
-
- // Запускаем HTTP/HTTPS серверы
- go StartHTTP()
- time.Sleep(100 * time.Millisecond)
- go StartHTTPS()
- time.Sleep(100 * time.Millisecond)
-
- // Запускаем PHP
- PHP_Start()
- time.Sleep(100 * time.Millisecond)
-
- // Запускаем MySQL
- StartMySQLServer(false)
- time.Sleep(100 * time.Millisecond)
-
- fmt.Println("✅ Сервер успешно перезагружен!")
- fmt.Println("")
-}
-
-func ClearConsole() {
- // Очищаем консоль, но сохраняем первые три строки
- fmt.Print("\033[H\033[2J") // ANSI escape code для очистки экрана
-
- println("")
- println(tools.Color("vServer", tools.Жёлтый) + tools.Color(" 1.0.0", tools.Голубой))
- println(tools.Color("Автор: ", tools.Зелёный) + tools.Color("Суманеев Роман (c) 2025", tools.Голубой))
- println(tools.Color("Официальный сайт: ", tools.Зелёный) + tools.Color("https://voxsel.ru", tools.Голубой))
-
- println("")
-
- // Восстанавливаем первые три строки
- fmt.Println("Введите help для получения списка команд")
- fmt.Println("")
-}
-
-// Переключает режим админки между embed и файловой системой
-func AdminToggle() {
- fmt.Println("")
-
- if admin.UseEmbedded {
- // Переключаем на файловую систему
- admin.UseEmbedded = false
- fmt.Println("🔄 Режим изменен: Embedded → Файловая система")
- fmt.Println("✅ Админка переключена на файловую систему")
- fmt.Println("📁 Файлы будут загружаться с диска из Backend/admin/html/")
- fmt.Println("💡 Теперь можно редактировать файлы и изменения будут видны сразу")
- } else {
- // Переключаем обратно на embedded
- admin.UseEmbedded = true
- fmt.Println("🔄 Режим изменен: Файловая система → Embedded")
- fmt.Println("✅ Админка переключена на embedded режим")
- fmt.Println("📦 Файлы загружаются из встроенных ресурсов")
- fmt.Println("🚀 Быстрая загрузка, но изменения требуют перекомпиляции")
- }
-
- fmt.Println("")
-}
-
-// Перезагружает конфигурацию без перезапуска сервисов
-func ConfigReload() {
- fmt.Println("")
- fmt.Println("📋 Перезагружаем конфигурацию...")
-
- // Загружаем новую конфигурацию
- config.LoadConfig()
-
- fmt.Println("✅ Конфигурация успешно перезагружена!")
- fmt.Println("💡 Изменения применятся к новым запросам")
- fmt.Println("")
-}
diff --git a/Backend/WebServer/handler.go b/Backend/WebServer/handler.go
index 4b8dc5b..f7a402a 100644
--- a/Backend/WebServer/handler.go
+++ b/Backend/WebServer/handler.go
@@ -4,12 +4,34 @@ import (
"net/http"
"os"
"strings"
+ "sync"
"vServer/Backend/config"
tools "vServer/Backend/tools"
)
+var (
+ siteStatusCache map[string]bool
+ statusMutex sync.RWMutex
+)
+
func StartHandler() {
http.HandleFunc("/", handler)
+ updateSiteStatusCache()
+}
+
+func updateSiteStatusCache() {
+ statusMutex.Lock()
+ defer statusMutex.Unlock()
+
+ siteStatusCache = make(map[string]bool)
+ for _, site := range config.ConfigData.Site_www {
+ siteStatusCache[site.Host] = site.Status == "active"
+ }
+}
+
+// UpdateSiteStatusCache - экспортируемая функция для обновления кэша
+func UpdateSiteStatusCache() {
+ updateSiteStatusCache()
}
// Проверка wildcard паттерна для alias
@@ -175,6 +197,17 @@ func checkVAccessAndHandle(w http.ResponseWriter, r *http.Request, filePath stri
return true
}
+// Проверяет включен ли сайт (оптимизировано через кэш)
+func isSiteActive(host string) bool {
+ statusMutex.RLock()
+ defer statusMutex.RUnlock()
+
+ if status, exists := siteStatusCache[host]; exists {
+ return status
+ }
+ return false
+}
+
// Обработчик запросов
func handler(w http.ResponseWriter, r *http.Request) {
@@ -187,6 +220,13 @@ func handler(w http.ResponseWriter, r *http.Request) {
return // Если прокси обработал запрос, прерываем выполнение
}
+ // Проверяем статус сайта
+ if !isSiteActive(host) {
+ http.ServeFile(w, r, "WebServer/tools/error_page/index.html")
+ tools.Logs_file(2, "H503", "🚫 Сайт отключен: "+host, "logs_http.log", false)
+ return
+ }
+
// ЕДИНСТВЕННАЯ ПРОВЕРКА vAccess - простая проверка запрошенного пути
if !checkVAccessAndHandle(w, r, r.URL.Path, host) {
return
diff --git a/Backend/WebServer/http_server.go b/Backend/WebServer/http_server.go
index 6d01393..4f33364 100644
--- a/Backend/WebServer/http_server.go
+++ b/Backend/WebServer/http_server.go
@@ -8,6 +8,11 @@ import (
var httpServer *http.Server
var port_http string = "80"
+// GetHTTPStatus возвращает статус HTTP сервера
+func GetHTTPStatus() bool {
+ return httpServer != nil
+}
+
// Запуск HTTP сервера
func StartHTTP() {
diff --git a/Backend/WebServer/https_server.go b/Backend/WebServer/https_server.go
index 9156033..ab3f3b9 100644
--- a/Backend/WebServer/https_server.go
+++ b/Backend/WebServer/https_server.go
@@ -18,6 +18,11 @@ var fallbackCert *tls.Certificate
var httpsServer *http.Server
var port_https string = "443"
+// GetHTTPSStatus возвращает статус HTTPS сервера
+func GetHTTPSStatus() bool {
+ return httpsServer != nil
+}
+
// Запуск https сервера
func StartHTTPS() {
diff --git a/Backend/WebServer/php_server.go b/Backend/WebServer/php_server.go
index 319b3c2..f241a62 100644
--- a/Backend/WebServer/php_server.go
+++ b/Backend/WebServer/php_server.go
@@ -13,6 +13,7 @@ import (
"strconv"
"strings"
"sync"
+ "syscall"
"time"
config "vServer/Backend/config"
tools "vServer/Backend/tools"
@@ -30,6 +31,11 @@ var (
var address_php string
var Сonsole_php bool = false
+// GetPHPStatus возвращает статус PHP сервера
+func GetPHPStatus() bool {
+ return len(phpProcesses) > 0 && !stopping
+}
+
// FastCGI константы
const (
FCGI_VERSION_1 = 1
@@ -99,6 +105,12 @@ func startFastCGIWorker(port int, workerID int) {
"PHP_FCGI_MAX_REQUESTS=1000", // Перезапуск после 1000 запросов
)
+ // Скрываем консольное окно
+ cmd.SysProcAttr = &syscall.SysProcAttr{
+ HideWindow: true,
+ CreationFlags: 0x08000000, // CREATE_NO_WINDOW
+ }
+
if !Сonsole_php {
cmd.Stdout = nil
cmd.Stderr = nil
@@ -499,6 +511,10 @@ func PHP_Stop() {
// Дополнительно убиваем все процессы php-cgi.exe
cmd := exec.Command("taskkill", "/F", "/IM", "php-cgi.exe")
+ cmd.SysProcAttr = &syscall.SysProcAttr{
+ HideWindow: true,
+ CreationFlags: 0x08000000,
+ }
cmd.Run()
tools.Logs_file(0, "PHP", "🛑 Все FastCGI процессы остановлены", "logs_php.log", true)
diff --git a/Backend/WebServer/proxy_server.go b/Backend/WebServer/proxy_server.go
index 22d5289..9976ae3 100644
--- a/Backend/WebServer/proxy_server.go
+++ b/Backend/WebServer/proxy_server.go
@@ -21,6 +21,11 @@ func StartHandlerProxy(w http.ResponseWriter, r *http.Request) (valid bool) {
configMutex.RLock()
defer configMutex.RUnlock()
+ // Проверяем глобальный флаг прокси
+ if !config.ConfigData.Soft_Settings.Proxy_enabled {
+ return false
+ }
+
// Проходим по всем прокси конфигурациям
for _, proxyConfig := range config.ConfigData.Proxy_Service {
// Пропускаем отключенные прокси
diff --git a/Backend/WebServer/vAccess.go b/Backend/WebServer/vAccess.go
index d2dc3ac..f7a8cd4 100644
--- a/Backend/WebServer/vAccess.go
+++ b/Backend/WebServer/vAccess.go
@@ -136,15 +136,21 @@ func findVAccessFiles(requestPath string, host string) []string {
// Базовый путь к сайту (НЕ public_www, а уровень выше)
basePath := "WebServer/www/" + host
+ // Получаем абсолютный базовый путь
+ absBasePath, err := tools.AbsPath(basePath)
+ if err != nil {
+ return configFiles
+ }
+
// Проверяем корневой vAccess.conf
- rootConfigPath := filepath.Join(basePath, "vAccess.conf")
+ rootConfigPath := filepath.Join(absBasePath, "vAccess.conf")
if _, err := os.Stat(rootConfigPath); err == nil {
configFiles = append(configFiles, rootConfigPath)
}
// Разбиваем путь на части для поиска вложенных конфигов
pathParts := strings.Split(strings.Trim(requestPath, "/"), "/")
- currentPath := basePath
+ currentPath := absBasePath
for _, part := range pathParts {
if part == "" {
@@ -439,7 +445,8 @@ func HandleVAccessError(w http.ResponseWriter, r *http.Request, errorPage string
switch {
case errorPage == "404":
// Стандартная 404 страница
- http.ServeFile(w, r, "WebServer/tools/error_page/index.html")
+ errorPagePath, _ := tools.AbsPath("WebServer/tools/error_page/index.html")
+ http.ServeFile(w, r, errorPagePath)
case strings.HasPrefix(errorPage, "http://") || strings.HasPrefix(errorPage, "https://"):
// Внешний сайт - редирект
@@ -448,11 +455,13 @@ func HandleVAccessError(w http.ResponseWriter, r *http.Request, errorPage string
default:
// Локальный путь от public_www
localPath := "WebServer/www/" + host + "/public_www" + errorPage
- if _, err := os.Stat(localPath); err == nil {
- http.ServeFile(w, r, localPath)
+ absLocalPath, _ := tools.AbsPath(localPath)
+ if _, err := os.Stat(absLocalPath); err == nil {
+ http.ServeFile(w, r, absLocalPath)
} else {
// Файл не найден - показываем стандартную 404
- http.ServeFile(w, r, "WebServer/tools/error_page/index.html")
+ errorPagePath, _ := tools.AbsPath("WebServer/tools/error_page/index.html")
+ http.ServeFile(w, r, errorPagePath)
tools.Logs_file(1, "vAccess", "❌ Страница ошибки не найдена: "+localPath, "logs_vaccess.log", false)
}
}
@@ -468,14 +477,21 @@ func CheckProxyVAccess(requestPath string, domain string, r *http.Request) (bool
// Путь к конфигурационному файлу прокси
configPath := "WebServer/tools/Proxy_vAccess/" + domain + "_vAccess.conf"
+ // Получаем абсолютный путь
+ absConfigPath, err := tools.AbsPath(configPath)
+ if err != nil {
+ // При ошибке получения пути - разрешаем доступ
+ return true, ""
+ }
+
// Проверяем существование файла
- if _, err := os.Stat(configPath); os.IsNotExist(err) {
+ if _, err := os.Stat(absConfigPath); os.IsNotExist(err) {
// Нет конфигурационного файла - разрешаем доступ
return true, ""
}
// Парсим конфигурационный файл
- config, err := parseVAccessFile(configPath)
+ config, err := parseVAccessFile(absConfigPath)
if err != nil {
tools.Logs_file(1, "vAccess-Proxy", "❌ Ошибка парсинга "+configPath+": "+err.Error(), "logs_vaccess_proxy.log", false)
return true, "" // При ошибке парсинга разрешаем доступ
@@ -491,7 +507,8 @@ func HandleProxyVAccessError(w http.ResponseWriter, r *http.Request, errorPage s
case errorPage == "404":
// Стандартная 404 страница
w.WriteHeader(http.StatusForbidden)
- http.ServeFile(w, r, "WebServer/tools/error_page/index.html")
+ errorPagePath, _ := tools.AbsPath("WebServer/tools/error_page/index.html")
+ http.ServeFile(w, r, errorPagePath)
case strings.HasPrefix(errorPage, "http://") || strings.HasPrefix(errorPage, "https://"):
// Внешний сайт - редирект
@@ -500,6 +517,7 @@ func HandleProxyVAccessError(w http.ResponseWriter, r *http.Request, errorPage s
default:
// Для прокси возвращаем 403 Forbidden
w.WriteHeader(http.StatusForbidden)
- http.ServeFile(w, r, "WebServer/tools/error_page/index.html")
+ errorPagePath, _ := tools.AbsPath("WebServer/tools/error_page/index.html")
+ http.ServeFile(w, r, errorPagePath)
}
}
diff --git a/Backend/admin/embed.go b/Backend/admin/embed.go
deleted file mode 100644
index c5399b6..0000000
--- a/Backend/admin/embed.go
+++ /dev/null
@@ -1,21 +0,0 @@
-package admin
-
-import (
- "embed"
- "io/fs"
- "os"
-)
-
-//go:embed html
-var AdminHTML embed.FS
-
-// Флаг для переключения между embed и файловой системой
-var UseEmbedded bool = true
-
-// Получает файловую систему в зависимости от флага
-func GetFileSystem() fs.FS {
- if UseEmbedded {
- return AdminHTML
- }
- return os.DirFS("Backend/admin/")
-}
diff --git a/Backend/admin/frontend/assets/css/base.css b/Backend/admin/frontend/assets/css/base.css
new file mode 100644
index 0000000..9250c8a
--- /dev/null
+++ b/Backend/admin/frontend/assets/css/base.css
@@ -0,0 +1,103 @@
+/* ============================================
+ Base Styles & Reset
+ Базовые стили приложения
+ ============================================ */
+
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: var(--font-sans);
+ background: var(--bg-primary);
+ color: var(--text-primary);
+ line-height: 1.6;
+ height: 100vh;
+ overflow: hidden;
+ position: relative;
+
+ /* Градиентный фон с размытием */
+ &::before {
+ content: '';
+ position: fixed;
+ top: -50%;
+ left: -50%;
+ width: 200%;
+ height: 200%;
+ /* background: radial-gradient(circle at 80% 80%, rgba(138, 92, 246, 0.116) 0%, transparent 50%); */
+ pointer-events: none;
+ z-index: 0;
+ }
+}
+
+/* Typography */
+code {
+ background: rgba(139, 92, 246, 0.15);
+ backdrop-filter: var(--backdrop-blur-light);
+ color: var(--accent-purple-light);
+ padding: 3px 8px;
+ border-radius: var(--radius-sm);
+ font-family: var(--font-mono);
+ font-size: var(--text-sm);
+ border: 1px solid var(--glass-border);
+ box-shadow: var(--shadow-sm);
+}
+
+/* Scrollbar Styling */
+::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+}
+
+::-webkit-scrollbar-track {
+ background: rgba(139, 92, 246, 0.05);
+ border-radius: var(--radius-sm);
+}
+
+::-webkit-scrollbar-thumb {
+ background: rgba(139, 92, 246, 0.3);
+ border-radius: var(--radius-sm);
+
+ &:hover {
+ background: rgba(139, 92, 246, 0.5);
+ }
+}
+
+/* Animations */
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ transform: translateY(10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+@keyframes spin {
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@keyframes bounce {
+ 0%, 100% {
+ transform: translateY(0);
+ }
+ 50% {
+ transform: translateY(-20px);
+ }
+}
+
+/* Utility Classes */
+.hidden {
+ display: none !important;
+}
+
+.fade-in {
+ animation: fadeIn var(--transition-slow);
+}
+
diff --git a/Backend/admin/frontend/assets/css/components/badges.css b/Backend/admin/frontend/assets/css/components/badges.css
new file mode 100644
index 0000000..ff76cfa
--- /dev/null
+++ b/Backend/admin/frontend/assets/css/components/badges.css
@@ -0,0 +1,87 @@
+/* ============================================
+ Badges Component
+ Единая система бейджей
+ ============================================ */
+
+/* Base Badge */
+.badge {
+ padding: 4px 12px;
+ border-radius: 20px;
+ font-size: var(--text-xs);
+ font-weight: var(--font-bold);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ backdrop-filter: var(--backdrop-blur-light);
+ display: inline-block;
+}
+
+/* Status Badges */
+.badge-online {
+ background: linear-gradient(135deg, rgba(16, 185, 129, 0.25), rgba(16, 185, 129, 0.15));
+ color: var(--accent-green);
+ border: 1px solid rgba(16, 185, 129, 0.4);
+ box-shadow: 0 0 12px rgba(16, 185, 129, 0.3);
+}
+
+.badge-offline {
+ background: linear-gradient(135deg, rgba(239, 68, 68, 0.25), rgba(239, 68, 68, 0.15));
+ color: var(--accent-red);
+ border: 1px solid rgba(239, 68, 68, 0.4);
+ box-shadow: 0 0 12px rgba(239, 68, 68, 0.3);
+}
+
+.badge-pending {
+ background: linear-gradient(135deg, rgba(245, 158, 11, 0.25), rgba(245, 158, 11, 0.15));
+ color: var(--accent-yellow);
+ border: 1px solid rgba(245, 158, 11, 0.4);
+ box-shadow: 0 0 12px rgba(245, 158, 11, 0.3);
+}
+
+/* Yes/No Badges */
+.badge-yes {
+ background: rgba(34, 197, 94, 0.2);
+ color: var(--accent-green);
+ border: 1px solid var(--accent-green);
+}
+
+.badge-no {
+ background: rgba(100, 116, 139, 0.2);
+ color: var(--text-muted);
+ border: 1px solid var(--text-muted);
+}
+
+/* Status Indicator (Dot) */
+.status-indicator {
+ width: 7px;
+ height: 7px;
+ border-radius: var(--radius-full);
+ box-shadow: 0 0 8px currentColor;
+}
+
+.status-online {
+ background: var(--accent-green);
+ color: var(--accent-green);
+}
+
+.status-offline {
+ background: var(--accent-red);
+ color: var(--accent-red);
+}
+
+/* Mini Tags (для таблиц) */
+.mini-tag {
+ display: inline-block;
+ padding: 4px 10px;
+ background: rgba(139, 92, 246, 0.15);
+ border-radius: var(--radius-sm);
+ font-size: 12px;
+ font-family: var(--font-mono);
+ color: var(--accent-purple-light);
+ margin: 2px;
+ transition: all var(--transition-base);
+
+ &:hover {
+ background: rgba(139, 92, 246, 0.25);
+ }
+}
+
diff --git a/Backend/admin/frontend/assets/css/components/buttons.css b/Backend/admin/frontend/assets/css/components/buttons.css
new file mode 100644
index 0000000..35bd3e7
--- /dev/null
+++ b/Backend/admin/frontend/assets/css/components/buttons.css
@@ -0,0 +1,308 @@
+/* ============================================
+ Buttons Component
+ Единая система кнопок
+ ============================================ */
+
+/* Base Button Styles */
+.btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: var(--space-sm);
+ padding: var(--space-sm) var(--space-md);
+ border: none;
+ border-radius: var(--radius-md);
+ font-size: var(--text-base);
+ font-weight: var(--font-semibold);
+ cursor: pointer;
+ transition: all var(--transition-base);
+ white-space: nowrap;
+
+ &:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ }
+
+ &:not(:disabled):active {
+ transform: scale(0.95);
+ }
+}
+
+/* Action Button - Основная кнопка действия */
+.action-btn {
+ padding: var(--space-sm) var(--space-md);
+ background: rgba(139, 92, 246, 0.15);
+ backdrop-filter: var(--backdrop-blur-light);
+ border: 1px solid rgba(139, 92, 246, 0.3);
+ border-radius: var(--radius-md);
+ color: var(--accent-purple-light);
+ font-size: var(--text-base);
+ font-weight: var(--font-semibold);
+
+ &:hover:not(:disabled) {
+ background: rgba(139, 92, 246, 0.25);
+ border-color: rgba(139, 92, 246, 0.5);
+ transform: translateY(-1px);
+ }
+
+ i {
+ font-size: var(--text-md);
+ }
+}
+
+/* Save Button Variant */
+.save-btn {
+ background: rgba(16, 185, 129, 0.15);
+ border-color: rgba(16, 185, 129, 0.3);
+ color: var(--accent-green);
+
+ &:hover:not(:disabled) {
+ background: rgba(16, 185, 129, 0.25);
+ border-color: rgba(16, 185, 129, 0.5);
+ }
+}
+
+/* Icon Button - Квадратная кнопка с иконкой */
+.icon-btn {
+ width: 32px;
+ height: 32px;
+ padding: 0;
+ background: rgba(139, 92, 246, 0.1);
+ border: 1px solid rgba(139, 92, 246, 0.3);
+ border-radius: var(--radius-md);
+ color: var(--accent-purple-light);
+ font-size: var(--text-md);
+
+ &:hover:not(:disabled) {
+ background: rgba(139, 92, 246, 0.25);
+ border-color: rgba(139, 92, 246, 0.5);
+ transform: translateY(-1px);
+ }
+}
+
+/* Small Icon Button */
+.icon-btn-small {
+ width: 28px;
+ height: 28px;
+ background: rgba(239, 68, 68, 0.1);
+ border: 1px solid rgba(239, 68, 68, 0.3);
+ border-radius: var(--radius-sm);
+ color: var(--accent-red);
+ font-size: 12px;
+
+ &:hover:not(:disabled) {
+ background: rgba(239, 68, 68, 0.2);
+ }
+}
+
+/* Window Control Buttons */
+.window-btn {
+ width: 32px;
+ height: 32px;
+ border: none;
+ background: transparent;
+ color: var(--text-secondary);
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: var(--radius-sm);
+ transition: all var(--transition-base);
+
+ &:hover {
+ background: rgba(255, 255, 255, 0.1);
+ color: var(--text-primary);
+ }
+
+ &:active {
+ transform: scale(0.95);
+ }
+}
+
+.close-btn:hover {
+ background: var(--accent-red);
+ color: white;
+}
+
+/* Server Control Button */
+.server-control-btn {
+ display: flex;
+ align-items: center;
+ gap: var(--space-sm);
+ padding: var(--space-sm) 18px;
+ background: linear-gradient(135deg, rgba(239, 68, 68, 0.2), rgba(239, 68, 68, 0.1));
+ backdrop-filter: var(--backdrop-blur-light);
+ border: 1px solid rgba(239, 68, 68, 0.4);
+ border-radius: 20px;
+ color: var(--accent-red);
+ font-size: var(--text-base);
+ font-weight: var(--font-semibold);
+ cursor: pointer;
+ transition: all var(--transition-base);
+ box-shadow: var(--shadow-red);
+
+ &:hover:not(:disabled) {
+ background: linear-gradient(135deg, rgba(239, 68, 68, 0.3), rgba(239, 68, 68, 0.15));
+ border-color: rgba(239, 68, 68, 0.6);
+ transform: translateY(-1px);
+ }
+
+ &:active {
+ transform: translateY(0);
+ }
+
+ i {
+ font-size: var(--text-md);
+ }
+}
+
+.server-control-btn.start-mode {
+ background: linear-gradient(135deg, rgba(16, 185, 129, 0.2), rgba(16, 185, 129, 0.1));
+ border-color: rgba(16, 185, 129, 0.4);
+ color: var(--accent-green);
+ box-shadow: var(--shadow-green);
+
+ &:hover:not(:disabled) {
+ background: linear-gradient(135deg, rgba(16, 185, 129, 0.3), rgba(16, 185, 129, 0.15));
+ border-color: rgba(16, 185, 129, 0.6);
+ }
+}
+
+/* Status Toggle Buttons */
+.status-toggle {
+ display: flex;
+ gap: var(--space-sm);
+}
+
+.status-btn {
+ flex: 1;
+ padding: 10px var(--space-md);
+ background: rgba(100, 116, 139, 0.1);
+ border: 1px solid rgba(100, 116, 139, 0.3);
+ border-radius: var(--radius-md);
+ color: var(--text-muted);
+ font-size: var(--text-base);
+ font-weight: var(--font-semibold);
+ cursor: pointer;
+ transition: all var(--transition-base);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: var(--space-sm);
+
+ &:hover {
+ background: rgba(100, 116, 139, 0.15);
+ }
+
+ &.active {
+ background: rgba(16, 185, 129, 0.2);
+ border-color: rgba(16, 185, 129, 0.5);
+ color: var(--accent-green);
+ box-shadow: 0 0 12px rgba(16, 185, 129, 0.2);
+ }
+
+ &:last-child.active {
+ background: rgba(239, 68, 68, 0.2);
+ border-color: rgba(239, 68, 68, 0.5);
+ color: var(--accent-red);
+ }
+}
+
+/* Navigation Buttons */
+.nav-item {
+ width: 48px;
+ height: 48px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: transparent;
+ border: none;
+ border-radius: var(--radius-lg);
+ color: var(--text-secondary);
+ font-size: 20px;
+ cursor: pointer;
+ transition: all var(--transition-base);
+ position: relative;
+
+ &::before {
+ content: '';
+ position: absolute;
+ left: -16px;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 3px;
+ height: 0;
+ background: linear-gradient(180deg, var(--accent-purple), var(--accent-purple-light));
+ border-radius: 0 2px 2px 0;
+ transition: height var(--transition-base);
+ }
+
+ &:hover {
+ background: rgba(139, 92, 246, 0.1);
+ color: var(--accent-purple-light);
+ }
+
+ &.active {
+ background: rgba(139, 92, 246, 0.15);
+ color: var(--accent-purple-light);
+
+ &::before {
+ height: 24px;
+ }
+ }
+}
+
+/* Breadcrumb Buttons */
+.breadcrumb-item {
+ font-size: var(--text-md);
+ color: var(--text-muted);
+ background: none;
+ border: none;
+ padding: var(--space-sm) var(--space-lg);
+ border-radius: var(--radius-md);
+ cursor: pointer;
+ transition: all var(--transition-base);
+ display: flex;
+ align-items: center;
+ gap: var(--space-sm);
+
+ &:hover {
+ background: rgba(139, 92, 246, 0.1);
+ color: var(--accent-purple-light);
+ }
+
+ &.active {
+ color: var(--text-primary);
+ font-weight: var(--font-medium);
+ cursor: default;
+ }
+}
+
+/* Tab Buttons */
+.vaccess-tab {
+ flex: 0 0 auto;
+ padding: 10px 18px;
+ background: transparent;
+ border: none;
+ border-radius: var(--radius-md);
+ color: var(--text-muted);
+ font-size: var(--text-base);
+ font-weight: var(--font-medium);
+ cursor: pointer;
+ transition: all var(--transition-base);
+ display: flex;
+ align-items: center;
+ gap: var(--space-sm);
+
+ &:hover {
+ background: rgba(139, 92, 246, 0.1);
+ color: var(--text-primary);
+ }
+
+ &.active {
+ background: var(--accent-purple);
+ color: white;
+ box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
+ }
+}
+
diff --git a/Backend/admin/frontend/assets/css/components/cards.css b/Backend/admin/frontend/assets/css/components/cards.css
new file mode 100644
index 0000000..0c11abb
--- /dev/null
+++ b/Backend/admin/frontend/assets/css/components/cards.css
@@ -0,0 +1,191 @@
+/* ============================================
+ Cards Component
+ Единая система карточек
+ ============================================ */
+
+/* Service Card */
+.service-card {
+ background: var(--glass-bg-light);
+ backdrop-filter: var(--backdrop-blur);
+ border: 1px solid var(--glass-border);
+ border-radius: var(--radius-2xl);
+ padding: var(--space-lg);
+ transition: all var(--transition-bounce);
+ position: relative;
+ overflow: hidden;
+
+ /* Градиентная линия сверху */
+ &::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 3px;
+ background: linear-gradient(90deg, var(--accent-purple), var(--accent-purple-light), var(--accent-cyan));
+ opacity: 0;
+ transition: opacity var(--transition-slow);
+ }
+
+ &:hover {
+ transform: translateY(-4px);
+ box-shadow: var(--shadow-purple);
+ border-color: var(--glass-border-hover);
+ background: rgba(20, 20, 40, 0.5);
+
+ &::before {
+ opacity: 1;
+ }
+ }
+}
+
+.service-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 18px;
+}
+
+.service-name {
+ font-size: var(--text-md);
+ font-weight: var(--font-semibold);
+ color: var(--text-primary);
+ display: flex;
+ align-items: center;
+ gap: var(--space-sm);
+
+ i {
+ color: var(--accent-purple-light);
+ font-size: var(--text-lg);
+ }
+}
+
+.service-info {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.info-row {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.info-label {
+ font-size: var(--text-sm);
+ color: var(--text-secondary);
+ font-weight: var(--font-medium);
+}
+
+.info-value {
+ font-size: 12px;
+ color: var(--text-primary);
+ font-weight: var(--font-semibold);
+}
+
+/* Settings Card */
+.settings-card {
+ background: var(--glass-bg-light);
+ backdrop-filter: var(--backdrop-blur);
+ border: 1px solid var(--glass-border);
+ border-radius: var(--radius-xl);
+ padding: var(--space-lg);
+}
+
+.settings-card-title {
+ font-size: var(--text-lg);
+ font-weight: var(--font-semibold);
+ color: var(--text-primary);
+ margin-bottom: 20px;
+ padding-bottom: 12px;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+ display: flex;
+ align-items: center;
+ gap: var(--space-sm);
+
+ i {
+ color: var(--accent-purple-light);
+ }
+}
+
+/* vAccess Rule Card */
+.vaccess-rule-card {
+ background: rgba(10, 14, 26, 0.4);
+ border: 1px solid var(--glass-border);
+ border-radius: var(--radius-lg);
+ padding: var(--space-lg);
+}
+
+.rule-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: var(--space-md);
+ padding-bottom: 12px;
+ border-bottom: 1px solid var(--glass-border);
+}
+
+.rule-number {
+ font-size: var(--text-md);
+ font-weight: var(--font-bold);
+ color: var(--accent-purple-light);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.rule-content {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 20px;
+
+ > .form-group:first-child {
+ grid-column: 1 / -1;
+ }
+}
+
+/* Help Cards */
+.help-card {
+ background: rgba(255, 255, 255, 0.02);
+ border-radius: var(--radius-xl);
+ padding: var(--space-xl);
+ border: 1px solid var(--glass-border);
+ transition: all var(--transition-slow);
+
+ &:hover {
+ border-color: var(--glass-border-hover);
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
+ }
+
+ h3 {
+ font-size: var(--text-2xl);
+ font-weight: var(--font-semibold);
+ color: var(--accent-purple-light);
+ margin: 0 0 20px 0;
+ display: flex;
+ align-items: center;
+ gap: var(--space-lg);
+ }
+
+ ul {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ }
+
+ li {
+ padding: 12px 0;
+ color: var(--text-secondary);
+ line-height: 1.6;
+ border-bottom: 1px solid rgba(139, 92, 246, 0.05);
+
+ &:last-child {
+ border-bottom: none;
+ }
+ }
+}
+
+.help-examples {
+ background: linear-gradient(135deg, rgba(139, 92, 246, 0.05) 0%, rgba(99, 102, 241, 0.05) 100%);
+}
+
diff --git a/Backend/admin/frontend/assets/css/components/forms.css b/Backend/admin/frontend/assets/css/components/forms.css
new file mode 100644
index 0000000..a2cbcd5
--- /dev/null
+++ b/Backend/admin/frontend/assets/css/components/forms.css
@@ -0,0 +1,219 @@
+/* ============================================
+ Forms Component
+ Единая система форм
+ ============================================ */
+
+/* Form Container */
+.settings-form {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-md);
+}
+
+/* Form Group */
+.form-group {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-sm);
+}
+
+/* Form Row (2 columns) */
+.form-row {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: var(--space-md);
+}
+
+/* Form Label */
+.form-label {
+ font-size: 12px;
+ font-weight: var(--font-semibold);
+ color: var(--text-secondary);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.field-hint {
+ font-size: var(--text-xs);
+ color: var(--text-muted);
+ font-weight: var(--font-normal);
+ text-transform: none;
+ letter-spacing: 0;
+}
+
+/* Form Input */
+.form-input {
+ padding: 10px 14px;
+ background: var(--glass-bg-dark);
+ border: 1px solid var(--glass-border);
+ border-radius: var(--radius-md);
+ color: var(--text-primary);
+ font-size: var(--text-base);
+ outline: none;
+ transition: all var(--transition-base);
+
+ &:focus {
+ border-color: rgba(139, 92, 246, 0.5);
+ box-shadow: 0 0 12px rgba(139, 92, 246, 0.2);
+ }
+
+ &::placeholder {
+ color: var(--text-muted);
+ opacity: 0.5;
+ }
+}
+
+/* Form Info */
+.form-info {
+ font-size: var(--text-sm);
+ color: var(--text-muted);
+ line-height: 1.5;
+ padding: 12px;
+ background: rgba(139, 92, 246, 0.05);
+ border-radius: var(--radius-md);
+ border-left: 3px solid var(--accent-purple);
+}
+
+/* Toggle Switch */
+.toggle-wrapper {
+ display: flex;
+ align-items: center;
+ gap: var(--space-lg);
+}
+
+.toggle-switch {
+ position: relative;
+ display: inline-block;
+ width: 50px;
+ height: 26px;
+
+ input {
+ opacity: 0;
+ width: 0;
+ height: 0;
+
+ &:checked + .toggle-slider {
+ background: rgba(16, 185, 129, 0.2);
+ border-color: rgba(16, 185, 129, 0.5);
+ box-shadow: 0 0 16px rgba(16, 185, 129, 0.3);
+
+ &::before {
+ transform: translateX(24px);
+ background: var(--accent-green);
+ }
+ }
+ }
+}
+
+.toggle-slider {
+ position: absolute;
+ cursor: pointer;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(239, 68, 68, 0.2);
+ border: 1px solid rgba(239, 68, 68, 0.4);
+ transition: all var(--transition-slow);
+ border-radius: 26px;
+
+ &::before {
+ position: absolute;
+ content: "";
+ height: 18px;
+ width: 18px;
+ left: 3px;
+ bottom: 3px;
+ background: rgba(239, 68, 68, 0.8);
+ box-shadow: var(--shadow-sm);
+ transition: all var(--transition-slow);
+ border-radius: var(--radius-full);
+ }
+}
+
+.toggle-label {
+ font-size: var(--text-md);
+ font-weight: var(--font-semibold);
+ color: var(--text-primary);
+}
+
+/* Checkbox */
+.checkbox-label {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ cursor: pointer;
+ font-size: var(--text-base);
+ color: var(--text-primary);
+}
+
+/* Tags Container */
+.tags-container {
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--space-sm);
+ padding: 12px;
+ background: var(--glass-bg-dark);
+ border: 1px solid var(--glass-border);
+ border-radius: var(--radius-md);
+ min-height: 48px;
+ margin-top: var(--space-sm);
+}
+
+.tag {
+ display: inline-flex;
+ align-items: center;
+ gap: var(--space-sm);
+ padding: 4px 10px;
+ background: rgba(139, 92, 246, 0.2);
+ border: 1px solid rgba(139, 92, 246, 0.4);
+ border-radius: 16px;
+ color: var(--text-primary);
+ font-size: 12px;
+ font-weight: var(--font-medium);
+}
+
+.tag-remove {
+ background: transparent;
+ border: none;
+ color: var(--accent-red);
+ cursor: pointer;
+ padding: 0;
+ width: 14px;
+ height: 14px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: var(--radius-full);
+ transition: all var(--transition-base);
+
+ &:hover {
+ background: rgba(239, 68, 68, 0.2);
+ }
+}
+
+.tag-input-wrapper {
+ display: flex;
+ gap: var(--space-sm);
+
+ .form-input {
+ flex: 1;
+ }
+
+ .action-btn {
+ flex-shrink: 0;
+ }
+}
+
+/* Field Editor */
+.field-editor {
+ padding: 20px;
+
+ h3 {
+ font-size: var(--text-xl);
+ font-weight: var(--font-semibold);
+ color: var(--accent-purple-light);
+ margin-bottom: 20px;
+ }
+}
+
diff --git a/Backend/admin/frontend/assets/css/components/modals.css b/Backend/admin/frontend/assets/css/components/modals.css
new file mode 100644
index 0000000..23986c1
--- /dev/null
+++ b/Backend/admin/frontend/assets/css/components/modals.css
@@ -0,0 +1,223 @@
+/* ============================================
+ Modals Component
+ Единая система модальных окон
+ ============================================ */
+
+/* Modal Overlay */
+.modal-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.7);
+ backdrop-filter: var(--backdrop-blur-light);
+ z-index: var(--z-modal);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity var(--transition-slow);
+
+ &.show {
+ opacity: 1;
+ pointer-events: auto;
+
+ .modal-window {
+ transform: scale(1);
+ }
+
+ .notification-content {
+ transform: scale(1);
+ }
+ }
+}
+
+/* Modal Window */
+.modal-window {
+ background: rgba(20, 20, 40, 0.95);
+ backdrop-filter: var(--backdrop-blur);
+ border: 1px solid var(--glass-border-hover);
+ border-radius: var(--radius-2xl);
+ box-shadow: var(--shadow-lg);
+ min-width: 900px;
+ max-width: 1200px;
+ max-height: 85vh;
+ overflow: hidden;
+ transform: scale(0.9);
+ transition: transform var(--transition-slow);
+}
+
+.modal-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 20px var(--space-lg);
+ border-bottom: 1px solid var(--glass-border);
+}
+
+.modal-title {
+ font-size: var(--text-xl);
+ font-weight: var(--font-semibold);
+ color: var(--text-primary);
+}
+
+.modal-close-btn {
+ width: 32px;
+ height: 32px;
+ background: transparent;
+ border: none;
+ border-radius: var(--radius-md);
+ color: var(--text-secondary);
+ cursor: pointer;
+ transition: all var(--transition-base);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ &:hover {
+ background: rgba(239, 68, 68, 0.2);
+ color: var(--accent-red);
+ }
+}
+
+.modal-content {
+ padding: var(--space-lg);
+ max-height: 60vh;
+ overflow-y: auto;
+}
+
+.modal-footer {
+ display: flex;
+ justify-content: flex-end;
+ gap: var(--space-lg);
+ padding: 20px var(--space-lg);
+ border-top: 1px solid var(--glass-border);
+}
+
+/* Notification Modal */
+.notification {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.6);
+ backdrop-filter: var(--backdrop-blur-light);
+ z-index: var(--z-notification);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity var(--transition-slow);
+
+ &.show {
+ opacity: 1;
+ pointer-events: auto;
+ }
+}
+
+.notification-content {
+ min-width: 400px;
+ max-width: 500px;
+ padding: var(--space-xl) 40px;
+ background: rgba(20, 20, 40, 0.95);
+ backdrop-filter: var(--backdrop-blur);
+ border: 1px solid var(--glass-border-hover);
+ border-radius: var(--radius-2xl);
+ box-shadow: var(--shadow-lg), 0 0 40px rgba(139, 92, 246, 0.3);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 20px;
+ text-align: center;
+ transform: scale(0.9);
+ transition: transform var(--transition-bounce);
+}
+
+.notification.success .notification-content {
+ border-color: rgba(16, 185, 129, 0.4);
+ box-shadow: var(--shadow-lg), 0 0 40px rgba(16, 185, 129, 0.3);
+}
+
+.notification.error .notification-content {
+ border-color: rgba(239, 68, 68, 0.4);
+ box-shadow: var(--shadow-lg), 0 0 40px rgba(239, 68, 68, 0.3);
+}
+
+.notification-icon {
+ font-size: 56px;
+
+ i {
+ display: block;
+ }
+}
+
+.notification.success .notification-icon {
+ color: var(--accent-green);
+}
+
+.notification.error .notification-icon {
+ color: var(--accent-red);
+}
+
+.notification-text {
+ font-size: var(--text-lg);
+ font-weight: var(--font-semibold);
+ color: var(--text-primary);
+ line-height: 1.6;
+}
+
+/* App Loader */
+.app-loader {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-secondary) 50%, var(--bg-tertiary) 100%);
+ z-index: var(--z-loader);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ opacity: 1;
+ transition: opacity 0.5s ease;
+
+ &.hide {
+ opacity: 0;
+ pointer-events: none;
+ }
+}
+
+.loader-content {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 20px;
+}
+
+.loader-icon {
+ font-size: 64px;
+ animation: bounce 1.5s ease-in-out infinite;
+}
+
+.loader-text {
+ font-size: var(--text-xl);
+ font-weight: var(--font-semibold);
+ background: linear-gradient(135deg, #7c3aed 0%, #a78bfa 100%);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+}
+
+.loader-spinner {
+ width: 40px;
+ height: 40px;
+ border: 3px solid rgba(139, 92, 246, 0.2);
+ border-top-color: var(--accent-purple);
+ border-radius: var(--radius-full);
+ animation: spin 0.8s linear infinite;
+}
+
diff --git a/Backend/admin/frontend/assets/css/components/tables.css b/Backend/admin/frontend/assets/css/components/tables.css
new file mode 100644
index 0000000..fd68e1a
--- /dev/null
+++ b/Backend/admin/frontend/assets/css/components/tables.css
@@ -0,0 +1,236 @@
+/* ============================================
+ Tables Component
+ ЕДИНАЯ система таблиц для всего приложения
+ ============================================ */
+
+/* Table Container */
+.table-container {
+ background: var(--glass-bg-light);
+ backdrop-filter: var(--backdrop-blur);
+ border: 1px solid var(--glass-border);
+ border-radius: var(--radius-2xl);
+ overflow: hidden;
+ box-shadow: var(--shadow-md);
+}
+
+/* Base Table */
+.data-table {
+ width: 100%;
+ border-collapse: collapse;
+
+ thead {
+ background: rgba(139, 92, 246, 0.12);
+ backdrop-filter: var(--backdrop-blur-light);
+
+ tr {
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+ }
+ }
+
+ th {
+ padding: 18px 20px;
+ text-align: left;
+ font-size: var(--text-sm);
+ font-weight: var(--font-bold);
+ color: var(--text-secondary);
+ text-transform: uppercase;
+ letter-spacing: 1.2px;
+
+ &:last-child {
+ width: 120px;
+ text-align: center;
+ }
+ }
+
+ tbody {
+ tr {
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
+ transition: all var(--transition-base);
+
+ &:hover {
+ background: rgba(139, 92, 246, 0.08);
+ }
+ }
+ }
+
+ td {
+ padding: 16px 20px;
+ font-size: var(--text-base);
+ color: var(--text-primary);
+
+ &:last-child {
+ text-align: center;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: var(--space-sm);
+ }
+ }
+}
+
+/* vAccess Table - использует те же стили что и data-table */
+.vaccess-table-container {
+ margin-bottom: 20px;
+ max-height: 55vh;
+ overflow-y: auto;
+}
+
+.vaccess-table {
+ width: 100%;
+ border-collapse: collapse;
+
+ thead {
+ tr {
+ background: rgba(139, 92, 246, 0.05);
+ display: table-row;
+ width: 100%;
+ }
+
+ th {
+ padding: 16px;
+ text-align: left;
+ font-size: var(--text-base);
+ font-weight: var(--font-semibold);
+ color: var(--text-muted);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ border: none;
+ display: table-cell;
+ box-sizing: content-box;
+
+ }
+ }
+
+ tbody {
+ tr {
+ background: rgba(255, 255, 255, 0.02);
+ transition: all var(--transition-slow);
+ cursor: grab;
+ display: table-row;
+ width: 100%;
+
+ &:hover {
+ background: rgba(139, 92, 246, 0.08);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
+ }
+
+ &:active {
+ cursor: grabbing;
+ }
+
+ &[draggable="true"] {
+ opacity: 1;
+ }
+ }
+ }
+
+ td {
+ padding: 20px 16px;
+ font-size: var(--text-md);
+ color: var(--text-primary);
+ border-top: 1px solid rgba(139, 92, 246, 0.05);
+ border-bottom: 1px solid rgba(139, 92, 246, 0.05);
+ cursor: pointer;
+ display: table-cell;
+ box-sizing: content-box;
+
+ }
+
+ /* Принудительно растягиваем на всю ширину */
+ thead, tbody {
+ display: table-row-group;
+ width: 100%;
+ }
+}
+
+/* Table Column Sizing - адаптивные размеры */
+.col-drag {
+ width: 3%;
+ min-width: 40px;
+ text-align: center;
+}
+
+.col-type {
+ width: 8%;
+ min-width: 80px;
+}
+
+.col-files {
+ width: 15%;
+ min-width: 120px;
+}
+
+.col-paths {
+ width: 18%;
+ min-width: 150px;
+}
+
+.col-ips {
+ width: 15%;
+ min-width: 120px;
+}
+
+.col-exceptions {
+ width: 15%;
+ min-width: 120px;
+}
+
+.col-error {
+ width: 10%;
+ min-width: 100px;
+}
+
+.col-actions {
+ width: 5%;
+ min-width: 60px;
+ text-align: center;
+}
+
+/* Drag Handle */
+.drag-handle {
+ color: var(--text-muted);
+ opacity: 0.3;
+ transition: all var(--transition-base);
+ cursor: grab;
+ text-align: center;
+
+ &:hover {
+ opacity: 1;
+ color: var(--accent-purple-light);
+ }
+
+ &:active {
+ cursor: grabbing;
+ }
+}
+
+/* Empty Field */
+.empty-field {
+ color: var(--text-muted);
+ opacity: 0.4;
+ font-style: italic;
+}
+
+/* Clickable Link in Tables */
+.clickable-link {
+ color: var(--accent-purple-light);
+ cursor: pointer;
+ transition: all var(--transition-base);
+ text-decoration: none;
+ display: inline-flex;
+ align-items: center;
+ gap: var(--space-xs);
+
+ &:hover {
+ color: var(--accent-purple);
+ text-decoration: underline;
+ }
+}
+
+/* Responsive Table */
+@media (max-width: 600px) {
+ .table-container {
+ overflow-x: scroll;
+ }
+}
+
diff --git a/Backend/admin/frontend/assets/css/layout/container.css b/Backend/admin/frontend/assets/css/layout/container.css
new file mode 100644
index 0000000..f591d30
--- /dev/null
+++ b/Backend/admin/frontend/assets/css/layout/container.css
@@ -0,0 +1,66 @@
+/* ============================================
+ Container Layout
+ Основной контейнер контента
+ ============================================ */
+
+.container {
+ height: calc(100vh - var(--header-height));
+ margin-top: var(--header-height);
+ margin-left: var(--sidebar-width);
+ padding: 40px var(--space-3xl);
+ position: relative;
+ z-index: var(--z-base);
+ overflow-y: auto;
+ overflow-x: hidden;
+}
+
+/* Main Content */
+.main-content {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-2xl);
+}
+
+/* Section */
+.section {
+ position: relative;
+}
+
+.section-title {
+ font-size: var(--text-md);
+ font-weight: var(--font-bold);
+ color: var(--text-primary);
+ margin-bottom: var(--space-lg);
+ text-transform: uppercase;
+ letter-spacing: 1.5px;
+ display: flex;
+ align-items: center;
+ gap: var(--space-lg);
+
+ &::before {
+ content: '';
+ width: 4px;
+ height: 16px;
+ background: linear-gradient(180deg, var(--accent-purple), var(--accent-purple-light));
+ border-radius: 2px;
+ }
+}
+
+/* Footer */
+.footer {
+ margin-top: var(--space-3xl);
+ padding: 20px;
+ text-align: center;
+ color: var(--text-muted);
+ font-size: var(--text-sm);
+ opacity: 0.6;
+}
+
+/* Responsive */
+@media (max-width: 600px) {
+ .header {
+ flex-direction: column;
+ gap: 10px;
+ }
+}
+
diff --git a/Backend/admin/frontend/assets/css/layout/header.css b/Backend/admin/frontend/assets/css/layout/header.css
new file mode 100644
index 0000000..8507421
--- /dev/null
+++ b/Backend/admin/frontend/assets/css/layout/header.css
@@ -0,0 +1,76 @@
+/* ============================================
+ Header Layout
+ Window controls и title bar
+ ============================================ */
+
+.window-controls {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ z-index: 1000;
+ background: rgba(10, 14, 26, 0.9);
+ backdrop-filter: var(--backdrop-blur);
+ border-bottom: 1px solid var(--glass-border);
+}
+
+.title-bar {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ height: var(--header-height);
+ padding: 0 var(--space-xl);
+ --wails-draggable: drag;
+}
+
+.title-bar-left {
+ display: flex;
+ align-items: center;
+ gap: var(--space-lg);
+}
+
+.app-logo {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.logo-icon {
+ font-size: 24px;
+ line-height: 1;
+}
+
+.logo-text {
+ font-size: var(--text-xl);
+ font-weight: var(--font-bold);
+ color: #ffffff;
+ user-select: none;
+ letter-spacing: -0.5px;
+}
+
+.title-bar-right {
+ display: flex;
+ align-items: center;
+ gap: var(--space-md);
+ --wails-draggable: no-drag;
+}
+
+/* Server Status */
+.server-status {
+ display: flex;
+ align-items: center;
+ gap: var(--space-sm);
+ padding: var(--space-sm) var(--space-md);
+ background: linear-gradient(135deg, rgba(16, 185, 129, 0.15), rgba(16, 185, 129, 0.05));
+ backdrop-filter: var(--backdrop-blur-light);
+ border-radius: 20px;
+ border: 1px solid rgba(16, 185, 129, 0.3);
+ box-shadow: var(--shadow-green);
+}
+
+.status-text {
+ font-size: 12px;
+ font-weight: var(--font-semibold);
+ color: var(--text-primary);
+}
+
diff --git a/Backend/admin/frontend/assets/css/layout/sidebar.css b/Backend/admin/frontend/assets/css/layout/sidebar.css
new file mode 100644
index 0000000..afcfc9b
--- /dev/null
+++ b/Backend/admin/frontend/assets/css/layout/sidebar.css
@@ -0,0 +1,27 @@
+/* ============================================
+ Sidebar Layout
+ Боковая навигация
+ ============================================ */
+
+.sidebar {
+ position: fixed;
+ left: 0;
+ top: var(--header-height);
+ width: var(--sidebar-width);
+ height: calc(100vh - var(--header-height));
+ background: rgba(10, 14, 26, 0.95);
+ backdrop-filter: var(--backdrop-blur);
+ border-right: 1px solid var(--glass-border);
+ z-index: 100;
+ display: flex;
+ flex-direction: column;
+ padding: 20px 0;
+}
+
+.sidebar-nav {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-sm);
+ padding: 0 var(--space-md);
+}
+
diff --git a/Backend/admin/frontend/assets/css/local_lib/all.min.css b/Backend/admin/frontend/assets/css/local_lib/all.min.css
new file mode 100644
index 0000000..3bfa73b
--- /dev/null
+++ b/Backend/admin/frontend/assets/css/local_lib/all.min.css
@@ -0,0 +1,29 @@
+/*!
+ * Font Awesome Free 6.4.2 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.
+ */
+.fa{font-family:var(--fa-style-family,"Font Awesome 6 Free");font-weight:var(--fa-style,900)}.fa,.fa-brands,.fa-classic,.fa-regular,.fa-sharp,.fa-solid,.fab,.far,.fas{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:var(--fa-display,inline-block);font-style:normal;font-variant:normal;line-height:1;text-rendering:auto}.fa-classic,.fa-regular,.fa-solid,.far,.fas{font-family:"Font Awesome 6 Free"}.fa-brands,.fab{font-family:"Font Awesome 6 Brands"}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-2xs{font-size:.625em;line-height:.1em;vertical-align:.225em}.fa-xs{font-size:.75em;line-height:.08333em;vertical-align:.125em}.fa-sm{font-size:.875em;line-height:.07143em;vertical-align:.05357em}.fa-lg{font-size:1.25em;line-height:.05em;vertical-align:-.075em}.fa-xl{font-size:1.5em;line-height:.04167em;vertical-align:-.125em}.fa-2xl{font-size:2em;line-height:.03125em;vertical-align:-.1875em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:var(--fa-li-margin,2.5em);padding-left:0}.fa-ul>li{position:relative}.fa-li{left:calc(var(--fa-li-width, 2em)*-1);position:absolute;text-align:center;width:var(--fa-li-width,2em);line-height:inherit}.fa-border{border-radius:var(--fa-border-radius,.1em);border:var(--fa-border-width,.08em) var(--fa-border-style,solid) var(--fa-border-color,#eee);padding:var(--fa-border-padding,.2em .25em .15em)}.fa-pull-left{float:left;margin-right:var(--fa-pull-margin,.3em)}.fa-pull-right{float:right;margin-left:var(--fa-pull-margin,.3em)}.fa-beat{-webkit-animation-name:fa-beat;animation-name:fa-beat;-webkit-animation-delay:var(--fa-animation-delay,0s);animation-delay:var(--fa-animation-delay,0s);-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,ease-in-out);animation-timing-function:var(--fa-animation-timing,ease-in-out)}.fa-bounce{-webkit-animation-name:fa-bounce;animation-name:fa-bounce;-webkit-animation-delay:var(--fa-animation-delay,0s);animation-delay:var(--fa-animation-delay,0s);-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,cubic-bezier(.28,.84,.42,1));animation-timing-function:var(--fa-animation-timing,cubic-bezier(.28,.84,.42,1))}.fa-fade{-webkit-animation-name:fa-fade;animation-name:fa-fade;-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,cubic-bezier(.4,0,.6,1));animation-timing-function:var(--fa-animation-timing,cubic-bezier(.4,0,.6,1))}.fa-beat-fade,.fa-fade{-webkit-animation-delay:var(--fa-animation-delay,0s);animation-delay:var(--fa-animation-delay,0s);-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s)}.fa-beat-fade{-webkit-animation-name:fa-beat-fade;animation-name:fa-beat-fade;-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,cubic-bezier(.4,0,.6,1));animation-timing-function:var(--fa-animation-timing,cubic-bezier(.4,0,.6,1))}.fa-flip{-webkit-animation-name:fa-flip;animation-name:fa-flip;-webkit-animation-delay:var(--fa-animation-delay,0s);animation-delay:var(--fa-animation-delay,0s);-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,ease-in-out);animation-timing-function:var(--fa-animation-timing,ease-in-out)}.fa-shake{-webkit-animation-name:fa-shake;animation-name:fa-shake;-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,linear);animation-timing-function:var(--fa-animation-timing,linear)}.fa-shake,.fa-spin{-webkit-animation-delay:var(--fa-animation-delay,0s);animation-delay:var(--fa-animation-delay,0s);-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal)}.fa-spin{-webkit-animation-name:fa-spin;animation-name:fa-spin;-webkit-animation-duration:var(--fa-animation-duration,2s);animation-duration:var(--fa-animation-duration,2s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,linear);animation-timing-function:var(--fa-animation-timing,linear)}.fa-spin-reverse{--fa-animation-direction:reverse}.fa-pulse,.fa-spin-pulse{-webkit-animation-name:fa-spin;animation-name:fa-spin;-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,steps(8));animation-timing-function:var(--fa-animation-timing,steps(8))}@media (prefers-reduced-motion:reduce){.fa-beat,.fa-beat-fade,.fa-bounce,.fa-fade,.fa-flip,.fa-pulse,.fa-shake,.fa-spin,.fa-spin-pulse{-webkit-animation-delay:-1ms;animation-delay:-1ms;-webkit-animation-duration:1ms;animation-duration:1ms;-webkit-animation-iteration-count:1;animation-iteration-count:1;-webkit-transition-delay:0s;transition-delay:0s;-webkit-transition-duration:0s;transition-duration:0s}}@-webkit-keyframes fa-beat{0%,90%{-webkit-transform:scale(1);transform:scale(1)}45%{-webkit-transform:scale(var(--fa-beat-scale,1.25));transform:scale(var(--fa-beat-scale,1.25))}}@keyframes fa-beat{0%,90%{-webkit-transform:scale(1);transform:scale(1)}45%{-webkit-transform:scale(var(--fa-beat-scale,1.25));transform:scale(var(--fa-beat-scale,1.25))}}@-webkit-keyframes fa-bounce{0%{-webkit-transform:scale(1) translateY(0);transform:scale(1) translateY(0)}10%{-webkit-transform:scale(var(--fa-bounce-start-scale-x,1.1),var(--fa-bounce-start-scale-y,.9)) translateY(0);transform:scale(var(--fa-bounce-start-scale-x,1.1),var(--fa-bounce-start-scale-y,.9)) translateY(0)}30%{-webkit-transform:scale(var(--fa-bounce-jump-scale-x,.9),var(--fa-bounce-jump-scale-y,1.1)) translateY(var(--fa-bounce-height,-.5em));transform:scale(var(--fa-bounce-jump-scale-x,.9),var(--fa-bounce-jump-scale-y,1.1)) translateY(var(--fa-bounce-height,-.5em))}50%{-webkit-transform:scale(var(--fa-bounce-land-scale-x,1.05),var(--fa-bounce-land-scale-y,.95)) translateY(0);transform:scale(var(--fa-bounce-land-scale-x,1.05),var(--fa-bounce-land-scale-y,.95)) translateY(0)}57%{-webkit-transform:scale(1) translateY(var(--fa-bounce-rebound,-.125em));transform:scale(1) translateY(var(--fa-bounce-rebound,-.125em))}64%{-webkit-transform:scale(1) translateY(0);transform:scale(1) translateY(0)}to{-webkit-transform:scale(1) translateY(0);transform:scale(1) translateY(0)}}@keyframes fa-bounce{0%{-webkit-transform:scale(1) translateY(0);transform:scale(1) translateY(0)}10%{-webkit-transform:scale(var(--fa-bounce-start-scale-x,1.1),var(--fa-bounce-start-scale-y,.9)) translateY(0);transform:scale(var(--fa-bounce-start-scale-x,1.1),var(--fa-bounce-start-scale-y,.9)) translateY(0)}30%{-webkit-transform:scale(var(--fa-bounce-jump-scale-x,.9),var(--fa-bounce-jump-scale-y,1.1)) translateY(var(--fa-bounce-height,-.5em));transform:scale(var(--fa-bounce-jump-scale-x,.9),var(--fa-bounce-jump-scale-y,1.1)) translateY(var(--fa-bounce-height,-.5em))}50%{-webkit-transform:scale(var(--fa-bounce-land-scale-x,1.05),var(--fa-bounce-land-scale-y,.95)) translateY(0);transform:scale(var(--fa-bounce-land-scale-x,1.05),var(--fa-bounce-land-scale-y,.95)) translateY(0)}57%{-webkit-transform:scale(1) translateY(var(--fa-bounce-rebound,-.125em));transform:scale(1) translateY(var(--fa-bounce-rebound,-.125em))}64%{-webkit-transform:scale(1) translateY(0);transform:scale(1) translateY(0)}to{-webkit-transform:scale(1) translateY(0);transform:scale(1) translateY(0)}}@-webkit-keyframes fa-fade{50%{opacity:var(--fa-fade-opacity,.4)}}@keyframes fa-fade{50%{opacity:var(--fa-fade-opacity,.4)}}@-webkit-keyframes fa-beat-fade{0%,to{opacity:var(--fa-beat-fade-opacity,.4);-webkit-transform:scale(1);transform:scale(1)}50%{opacity:1;-webkit-transform:scale(var(--fa-beat-fade-scale,1.125));transform:scale(var(--fa-beat-fade-scale,1.125))}}@keyframes fa-beat-fade{0%,to{opacity:var(--fa-beat-fade-opacity,.4);-webkit-transform:scale(1);transform:scale(1)}50%{opacity:1;-webkit-transform:scale(var(--fa-beat-fade-scale,1.125));transform:scale(var(--fa-beat-fade-scale,1.125))}}@-webkit-keyframes fa-flip{50%{-webkit-transform:rotate3d(var(--fa-flip-x,0),var(--fa-flip-y,1),var(--fa-flip-z,0),var(--fa-flip-angle,-180deg));transform:rotate3d(var(--fa-flip-x,0),var(--fa-flip-y,1),var(--fa-flip-z,0),var(--fa-flip-angle,-180deg))}}@keyframes fa-flip{50%{-webkit-transform:rotate3d(var(--fa-flip-x,0),var(--fa-flip-y,1),var(--fa-flip-z,0),var(--fa-flip-angle,-180deg));transform:rotate3d(var(--fa-flip-x,0),var(--fa-flip-y,1),var(--fa-flip-z,0),var(--fa-flip-angle,-180deg))}}@-webkit-keyframes fa-shake{0%{-webkit-transform:rotate(-15deg);transform:rotate(-15deg)}4%{-webkit-transform:rotate(15deg);transform:rotate(15deg)}8%,24%{-webkit-transform:rotate(-18deg);transform:rotate(-18deg)}12%,28%{-webkit-transform:rotate(18deg);transform:rotate(18deg)}16%{-webkit-transform:rotate(-22deg);transform:rotate(-22deg)}20%{-webkit-transform:rotate(22deg);transform:rotate(22deg)}32%{-webkit-transform:rotate(-12deg);transform:rotate(-12deg)}36%{-webkit-transform:rotate(12deg);transform:rotate(12deg)}40%,to{-webkit-transform:rotate(0deg);transform:rotate(0deg)}}@keyframes fa-shake{0%{-webkit-transform:rotate(-15deg);transform:rotate(-15deg)}4%{-webkit-transform:rotate(15deg);transform:rotate(15deg)}8%,24%{-webkit-transform:rotate(-18deg);transform:rotate(-18deg)}12%,28%{-webkit-transform:rotate(18deg);transform:rotate(18deg)}16%{-webkit-transform:rotate(-22deg);transform:rotate(-22deg)}20%{-webkit-transform:rotate(22deg);transform:rotate(22deg)}32%{-webkit-transform:rotate(-12deg);transform:rotate(-12deg)}36%{-webkit-transform:rotate(12deg);transform:rotate(12deg)}40%,to{-webkit-transform:rotate(0deg);transform:rotate(0deg)}}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}.fa-rotate-90{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-webkit-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-webkit-transform:scaleY(-1);transform:scaleY(-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{-webkit-transform:scale(-1);transform:scale(-1)}.fa-rotate-by{-webkit-transform:rotate(var(--fa-rotate-angle,none));transform:rotate(var(--fa-rotate-angle,none))}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2.5em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%;z-index:var(--fa-stack-z-index,auto)}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:var(--fa-inverse,#fff)}
+
+.fa-0:before{content:"\30"}.fa-1:before{content:"\31"}.fa-2:before{content:"\32"}.fa-3:before{content:"\33"}.fa-4:before{content:"\34"}.fa-5:before{content:"\35"}.fa-6:before{content:"\36"}.fa-7:before{content:"\37"}.fa-8:before{content:"\38"}.fa-9:before{content:"\39"}.fa-fill-drip:before{content:"\f576"}.fa-arrows-to-circle:before{content:"\e4bd"}.fa-chevron-circle-right:before,.fa-circle-chevron-right:before{content:"\f138"}.fa-at:before{content:"\40"}.fa-trash-alt:before,.fa-trash-can:before{content:"\f2ed"}.fa-text-height:before{content:"\f034"}.fa-user-times:before,.fa-user-xmark:before{content:"\f235"}.fa-stethoscope:before{content:"\f0f1"}.fa-comment-alt:before,.fa-message:before{content:"\f27a"}.fa-info:before{content:"\f129"}.fa-compress-alt:before,.fa-down-left-and-up-right-to-center:before{content:"\f422"}.fa-explosion:before{content:"\e4e9"}.fa-file-alt:before,.fa-file-lines:before,.fa-file-text:before{content:"\f15c"}.fa-wave-square:before{content:"\f83e"}.fa-ring:before{content:"\f70b"}.fa-building-un:before{content:"\e4d9"}.fa-dice-three:before{content:"\f527"}.fa-calendar-alt:before,.fa-calendar-days:before{content:"\f073"}.fa-anchor-circle-check:before{content:"\e4aa"}.fa-building-circle-arrow-right:before{content:"\e4d1"}.fa-volleyball-ball:before,.fa-volleyball:before{content:"\f45f"}.fa-arrows-up-to-line:before{content:"\e4c2"}.fa-sort-desc:before,.fa-sort-down:before{content:"\f0dd"}.fa-circle-minus:before,.fa-minus-circle:before{content:"\f056"}.fa-door-open:before{content:"\f52b"}.fa-right-from-bracket:before,.fa-sign-out-alt:before{content:"\f2f5"}.fa-atom:before{content:"\f5d2"}.fa-soap:before{content:"\e06e"}.fa-heart-music-camera-bolt:before,.fa-icons:before{content:"\f86d"}.fa-microphone-alt-slash:before,.fa-microphone-lines-slash:before{content:"\f539"}.fa-bridge-circle-check:before{content:"\e4c9"}.fa-pump-medical:before{content:"\e06a"}.fa-fingerprint:before{content:"\f577"}.fa-hand-point-right:before{content:"\f0a4"}.fa-magnifying-glass-location:before,.fa-search-location:before{content:"\f689"}.fa-forward-step:before,.fa-step-forward:before{content:"\f051"}.fa-face-smile-beam:before,.fa-smile-beam:before{content:"\f5b8"}.fa-flag-checkered:before{content:"\f11e"}.fa-football-ball:before,.fa-football:before{content:"\f44e"}.fa-school-circle-exclamation:before{content:"\e56c"}.fa-crop:before{content:"\f125"}.fa-angle-double-down:before,.fa-angles-down:before{content:"\f103"}.fa-users-rectangle:before{content:"\e594"}.fa-people-roof:before{content:"\e537"}.fa-people-line:before{content:"\e534"}.fa-beer-mug-empty:before,.fa-beer:before{content:"\f0fc"}.fa-diagram-predecessor:before{content:"\e477"}.fa-arrow-up-long:before,.fa-long-arrow-up:before{content:"\f176"}.fa-burn:before,.fa-fire-flame-simple:before{content:"\f46a"}.fa-male:before,.fa-person:before{content:"\f183"}.fa-laptop:before{content:"\f109"}.fa-file-csv:before{content:"\f6dd"}.fa-menorah:before{content:"\f676"}.fa-truck-plane:before{content:"\e58f"}.fa-record-vinyl:before{content:"\f8d9"}.fa-face-grin-stars:before,.fa-grin-stars:before{content:"\f587"}.fa-bong:before{content:"\f55c"}.fa-pastafarianism:before,.fa-spaghetti-monster-flying:before{content:"\f67b"}.fa-arrow-down-up-across-line:before{content:"\e4af"}.fa-spoon:before,.fa-utensil-spoon:before{content:"\f2e5"}.fa-jar-wheat:before{content:"\e517"}.fa-envelopes-bulk:before,.fa-mail-bulk:before{content:"\f674"}.fa-file-circle-exclamation:before{content:"\e4eb"}.fa-circle-h:before,.fa-hospital-symbol:before{content:"\f47e"}.fa-pager:before{content:"\f815"}.fa-address-book:before,.fa-contact-book:before{content:"\f2b9"}.fa-strikethrough:before{content:"\f0cc"}.fa-k:before{content:"\4b"}.fa-landmark-flag:before{content:"\e51c"}.fa-pencil-alt:before,.fa-pencil:before{content:"\f303"}.fa-backward:before{content:"\f04a"}.fa-caret-right:before{content:"\f0da"}.fa-comments:before{content:"\f086"}.fa-file-clipboard:before,.fa-paste:before{content:"\f0ea"}.fa-code-pull-request:before{content:"\e13c"}.fa-clipboard-list:before{content:"\f46d"}.fa-truck-loading:before,.fa-truck-ramp-box:before{content:"\f4de"}.fa-user-check:before{content:"\f4fc"}.fa-vial-virus:before{content:"\e597"}.fa-sheet-plastic:before{content:"\e571"}.fa-blog:before{content:"\f781"}.fa-user-ninja:before{content:"\f504"}.fa-person-arrow-up-from-line:before{content:"\e539"}.fa-scroll-torah:before,.fa-torah:before{content:"\f6a0"}.fa-broom-ball:before,.fa-quidditch-broom-ball:before,.fa-quidditch:before{content:"\f458"}.fa-toggle-off:before{content:"\f204"}.fa-archive:before,.fa-box-archive:before{content:"\f187"}.fa-person-drowning:before{content:"\e545"}.fa-arrow-down-9-1:before,.fa-sort-numeric-desc:before,.fa-sort-numeric-down-alt:before{content:"\f886"}.fa-face-grin-tongue-squint:before,.fa-grin-tongue-squint:before{content:"\f58a"}.fa-spray-can:before{content:"\f5bd"}.fa-truck-monster:before{content:"\f63b"}.fa-w:before{content:"\57"}.fa-earth-africa:before,.fa-globe-africa:before{content:"\f57c"}.fa-rainbow:before{content:"\f75b"}.fa-circle-notch:before{content:"\f1ce"}.fa-tablet-alt:before,.fa-tablet-screen-button:before{content:"\f3fa"}.fa-paw:before{content:"\f1b0"}.fa-cloud:before{content:"\f0c2"}.fa-trowel-bricks:before{content:"\e58a"}.fa-face-flushed:before,.fa-flushed:before{content:"\f579"}.fa-hospital-user:before{content:"\f80d"}.fa-tent-arrow-left-right:before{content:"\e57f"}.fa-gavel:before,.fa-legal:before{content:"\f0e3"}.fa-binoculars:before{content:"\f1e5"}.fa-microphone-slash:before{content:"\f131"}.fa-box-tissue:before{content:"\e05b"}.fa-motorcycle:before{content:"\f21c"}.fa-bell-concierge:before,.fa-concierge-bell:before{content:"\f562"}.fa-pen-ruler:before,.fa-pencil-ruler:before{content:"\f5ae"}.fa-people-arrows-left-right:before,.fa-people-arrows:before{content:"\e068"}.fa-mars-and-venus-burst:before{content:"\e523"}.fa-caret-square-right:before,.fa-square-caret-right:before{content:"\f152"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-sun-plant-wilt:before{content:"\e57a"}.fa-toilets-portable:before{content:"\e584"}.fa-hockey-puck:before{content:"\f453"}.fa-table:before{content:"\f0ce"}.fa-magnifying-glass-arrow-right:before{content:"\e521"}.fa-digital-tachograph:before,.fa-tachograph-digital:before{content:"\f566"}.fa-users-slash:before{content:"\e073"}.fa-clover:before{content:"\e139"}.fa-mail-reply:before,.fa-reply:before{content:"\f3e5"}.fa-star-and-crescent:before{content:"\f699"}.fa-house-fire:before{content:"\e50c"}.fa-minus-square:before,.fa-square-minus:before{content:"\f146"}.fa-helicopter:before{content:"\f533"}.fa-compass:before{content:"\f14e"}.fa-caret-square-down:before,.fa-square-caret-down:before{content:"\f150"}.fa-file-circle-question:before{content:"\e4ef"}.fa-laptop-code:before{content:"\f5fc"}.fa-swatchbook:before{content:"\f5c3"}.fa-prescription-bottle:before{content:"\f485"}.fa-bars:before,.fa-navicon:before{content:"\f0c9"}.fa-people-group:before{content:"\e533"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-heart-broken:before,.fa-heart-crack:before{content:"\f7a9"}.fa-external-link-square-alt:before,.fa-square-up-right:before{content:"\f360"}.fa-face-kiss-beam:before,.fa-kiss-beam:before{content:"\f597"}.fa-film:before{content:"\f008"}.fa-ruler-horizontal:before{content:"\f547"}.fa-people-robbery:before{content:"\e536"}.fa-lightbulb:before{content:"\f0eb"}.fa-caret-left:before{content:"\f0d9"}.fa-circle-exclamation:before,.fa-exclamation-circle:before{content:"\f06a"}.fa-school-circle-xmark:before{content:"\e56d"}.fa-arrow-right-from-bracket:before,.fa-sign-out:before{content:"\f08b"}.fa-chevron-circle-down:before,.fa-circle-chevron-down:before{content:"\f13a"}.fa-unlock-alt:before,.fa-unlock-keyhole:before{content:"\f13e"}.fa-cloud-showers-heavy:before{content:"\f740"}.fa-headphones-alt:before,.fa-headphones-simple:before{content:"\f58f"}.fa-sitemap:before{content:"\f0e8"}.fa-circle-dollar-to-slot:before,.fa-donate:before{content:"\f4b9"}.fa-memory:before{content:"\f538"}.fa-road-spikes:before{content:"\e568"}.fa-fire-burner:before{content:"\e4f1"}.fa-flag:before{content:"\f024"}.fa-hanukiah:before{content:"\f6e6"}.fa-feather:before{content:"\f52d"}.fa-volume-down:before,.fa-volume-low:before{content:"\f027"}.fa-comment-slash:before{content:"\f4b3"}.fa-cloud-sun-rain:before{content:"\f743"}.fa-compress:before{content:"\f066"}.fa-wheat-alt:before,.fa-wheat-awn:before{content:"\e2cd"}.fa-ankh:before{content:"\f644"}.fa-hands-holding-child:before{content:"\e4fa"}.fa-asterisk:before{content:"\2a"}.fa-check-square:before,.fa-square-check:before{content:"\f14a"}.fa-peseta-sign:before{content:"\e221"}.fa-header:before,.fa-heading:before{content:"\f1dc"}.fa-ghost:before{content:"\f6e2"}.fa-list-squares:before,.fa-list:before{content:"\f03a"}.fa-phone-square-alt:before,.fa-square-phone-flip:before{content:"\f87b"}.fa-cart-plus:before{content:"\f217"}.fa-gamepad:before{content:"\f11b"}.fa-circle-dot:before,.fa-dot-circle:before{content:"\f192"}.fa-dizzy:before,.fa-face-dizzy:before{content:"\f567"}.fa-egg:before{content:"\f7fb"}.fa-house-medical-circle-xmark:before{content:"\e513"}.fa-campground:before{content:"\f6bb"}.fa-folder-plus:before{content:"\f65e"}.fa-futbol-ball:before,.fa-futbol:before,.fa-soccer-ball:before{content:"\f1e3"}.fa-paint-brush:before,.fa-paintbrush:before{content:"\f1fc"}.fa-lock:before{content:"\f023"}.fa-gas-pump:before{content:"\f52f"}.fa-hot-tub-person:before,.fa-hot-tub:before{content:"\f593"}.fa-map-location:before,.fa-map-marked:before{content:"\f59f"}.fa-house-flood-water:before{content:"\e50e"}.fa-tree:before{content:"\f1bb"}.fa-bridge-lock:before{content:"\e4cc"}.fa-sack-dollar:before{content:"\f81d"}.fa-edit:before,.fa-pen-to-square:before{content:"\f044"}.fa-car-side:before{content:"\f5e4"}.fa-share-alt:before,.fa-share-nodes:before{content:"\f1e0"}.fa-heart-circle-minus:before{content:"\e4ff"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-microscope:before{content:"\f610"}.fa-sink:before{content:"\e06d"}.fa-bag-shopping:before,.fa-shopping-bag:before{content:"\f290"}.fa-arrow-down-z-a:before,.fa-sort-alpha-desc:before,.fa-sort-alpha-down-alt:before{content:"\f881"}.fa-mitten:before{content:"\f7b5"}.fa-person-rays:before{content:"\e54d"}.fa-users:before{content:"\f0c0"}.fa-eye-slash:before{content:"\f070"}.fa-flask-vial:before{content:"\e4f3"}.fa-hand-paper:before,.fa-hand:before{content:"\f256"}.fa-om:before{content:"\f679"}.fa-worm:before{content:"\e599"}.fa-house-circle-xmark:before{content:"\e50b"}.fa-plug:before{content:"\f1e6"}.fa-chevron-up:before{content:"\f077"}.fa-hand-spock:before{content:"\f259"}.fa-stopwatch:before{content:"\f2f2"}.fa-face-kiss:before,.fa-kiss:before{content:"\f596"}.fa-bridge-circle-xmark:before{content:"\e4cb"}.fa-face-grin-tongue:before,.fa-grin-tongue:before{content:"\f589"}.fa-chess-bishop:before{content:"\f43a"}.fa-face-grin-wink:before,.fa-grin-wink:before{content:"\f58c"}.fa-deaf:before,.fa-deafness:before,.fa-ear-deaf:before,.fa-hard-of-hearing:before{content:"\f2a4"}.fa-road-circle-check:before{content:"\e564"}.fa-dice-five:before{content:"\f523"}.fa-rss-square:before,.fa-square-rss:before{content:"\f143"}.fa-land-mine-on:before{content:"\e51b"}.fa-i-cursor:before{content:"\f246"}.fa-stamp:before{content:"\f5bf"}.fa-stairs:before{content:"\e289"}.fa-i:before{content:"\49"}.fa-hryvnia-sign:before,.fa-hryvnia:before{content:"\f6f2"}.fa-pills:before{content:"\f484"}.fa-face-grin-wide:before,.fa-grin-alt:before{content:"\f581"}.fa-tooth:before{content:"\f5c9"}.fa-v:before{content:"\56"}.fa-bangladeshi-taka-sign:before{content:"\e2e6"}.fa-bicycle:before{content:"\f206"}.fa-rod-asclepius:before,.fa-rod-snake:before,.fa-staff-aesculapius:before,.fa-staff-snake:before{content:"\e579"}.fa-head-side-cough-slash:before{content:"\e062"}.fa-ambulance:before,.fa-truck-medical:before{content:"\f0f9"}.fa-wheat-awn-circle-exclamation:before{content:"\e598"}.fa-snowman:before{content:"\f7d0"}.fa-mortar-pestle:before{content:"\f5a7"}.fa-road-barrier:before{content:"\e562"}.fa-school:before{content:"\f549"}.fa-igloo:before{content:"\f7ae"}.fa-joint:before{content:"\f595"}.fa-angle-right:before{content:"\f105"}.fa-horse:before{content:"\f6f0"}.fa-q:before{content:"\51"}.fa-g:before{content:"\47"}.fa-notes-medical:before{content:"\f481"}.fa-temperature-2:before,.fa-temperature-half:before,.fa-thermometer-2:before,.fa-thermometer-half:before{content:"\f2c9"}.fa-dong-sign:before{content:"\e169"}.fa-capsules:before{content:"\f46b"}.fa-poo-bolt:before,.fa-poo-storm:before{content:"\f75a"}.fa-face-frown-open:before,.fa-frown-open:before{content:"\f57a"}.fa-hand-point-up:before{content:"\f0a6"}.fa-money-bill:before{content:"\f0d6"}.fa-bookmark:before{content:"\f02e"}.fa-align-justify:before{content:"\f039"}.fa-umbrella-beach:before{content:"\f5ca"}.fa-helmet-un:before{content:"\e503"}.fa-bullseye:before{content:"\f140"}.fa-bacon:before{content:"\f7e5"}.fa-hand-point-down:before{content:"\f0a7"}.fa-arrow-up-from-bracket:before{content:"\e09a"}.fa-folder-blank:before,.fa-folder:before{content:"\f07b"}.fa-file-medical-alt:before,.fa-file-waveform:before{content:"\f478"}.fa-radiation:before{content:"\f7b9"}.fa-chart-simple:before{content:"\e473"}.fa-mars-stroke:before{content:"\f229"}.fa-vial:before{content:"\f492"}.fa-dashboard:before,.fa-gauge-med:before,.fa-gauge:before,.fa-tachometer-alt-average:before{content:"\f624"}.fa-magic-wand-sparkles:before,.fa-wand-magic-sparkles:before{content:"\e2ca"}.fa-e:before{content:"\45"}.fa-pen-alt:before,.fa-pen-clip:before{content:"\f305"}.fa-bridge-circle-exclamation:before{content:"\e4ca"}.fa-user:before{content:"\f007"}.fa-school-circle-check:before{content:"\e56b"}.fa-dumpster:before{content:"\f793"}.fa-shuttle-van:before,.fa-van-shuttle:before{content:"\f5b6"}.fa-building-user:before{content:"\e4da"}.fa-caret-square-left:before,.fa-square-caret-left:before{content:"\f191"}.fa-highlighter:before{content:"\f591"}.fa-key:before{content:"\f084"}.fa-bullhorn:before{content:"\f0a1"}.fa-globe:before{content:"\f0ac"}.fa-synagogue:before{content:"\f69b"}.fa-person-half-dress:before{content:"\e548"}.fa-road-bridge:before{content:"\e563"}.fa-location-arrow:before{content:"\f124"}.fa-c:before{content:"\43"}.fa-tablet-button:before{content:"\f10a"}.fa-building-lock:before{content:"\e4d6"}.fa-pizza-slice:before{content:"\f818"}.fa-money-bill-wave:before{content:"\f53a"}.fa-area-chart:before,.fa-chart-area:before{content:"\f1fe"}.fa-house-flag:before{content:"\e50d"}.fa-person-circle-minus:before{content:"\e540"}.fa-ban:before,.fa-cancel:before{content:"\f05e"}.fa-camera-rotate:before{content:"\e0d8"}.fa-air-freshener:before,.fa-spray-can-sparkles:before{content:"\f5d0"}.fa-star:before{content:"\f005"}.fa-repeat:before{content:"\f363"}.fa-cross:before{content:"\f654"}.fa-box:before{content:"\f466"}.fa-venus-mars:before{content:"\f228"}.fa-arrow-pointer:before,.fa-mouse-pointer:before{content:"\f245"}.fa-expand-arrows-alt:before,.fa-maximize:before{content:"\f31e"}.fa-charging-station:before{content:"\f5e7"}.fa-shapes:before,.fa-triangle-circle-square:before{content:"\f61f"}.fa-random:before,.fa-shuffle:before{content:"\f074"}.fa-person-running:before,.fa-running:before{content:"\f70c"}.fa-mobile-retro:before{content:"\e527"}.fa-grip-lines-vertical:before{content:"\f7a5"}.fa-spider:before{content:"\f717"}.fa-hands-bound:before{content:"\e4f9"}.fa-file-invoice-dollar:before{content:"\f571"}.fa-plane-circle-exclamation:before{content:"\e556"}.fa-x-ray:before{content:"\f497"}.fa-spell-check:before{content:"\f891"}.fa-slash:before{content:"\f715"}.fa-computer-mouse:before,.fa-mouse:before{content:"\f8cc"}.fa-arrow-right-to-bracket:before,.fa-sign-in:before{content:"\f090"}.fa-shop-slash:before,.fa-store-alt-slash:before{content:"\e070"}.fa-server:before{content:"\f233"}.fa-virus-covid-slash:before{content:"\e4a9"}.fa-shop-lock:before{content:"\e4a5"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-blender-phone:before{content:"\f6b6"}.fa-building-wheat:before{content:"\e4db"}.fa-person-breastfeeding:before{content:"\e53a"}.fa-right-to-bracket:before,.fa-sign-in-alt:before{content:"\f2f6"}.fa-venus:before{content:"\f221"}.fa-passport:before{content:"\f5ab"}.fa-heart-pulse:before,.fa-heartbeat:before{content:"\f21e"}.fa-people-carry-box:before,.fa-people-carry:before{content:"\f4ce"}.fa-temperature-high:before{content:"\f769"}.fa-microchip:before{content:"\f2db"}.fa-crown:before{content:"\f521"}.fa-weight-hanging:before{content:"\f5cd"}.fa-xmarks-lines:before{content:"\e59a"}.fa-file-prescription:before{content:"\f572"}.fa-weight-scale:before,.fa-weight:before{content:"\f496"}.fa-user-friends:before,.fa-user-group:before{content:"\f500"}.fa-arrow-up-a-z:before,.fa-sort-alpha-up:before{content:"\f15e"}.fa-chess-knight:before{content:"\f441"}.fa-face-laugh-squint:before,.fa-laugh-squint:before{content:"\f59b"}.fa-wheelchair:before{content:"\f193"}.fa-arrow-circle-up:before,.fa-circle-arrow-up:before{content:"\f0aa"}.fa-toggle-on:before{content:"\f205"}.fa-person-walking:before,.fa-walking:before{content:"\f554"}.fa-l:before{content:"\4c"}.fa-fire:before{content:"\f06d"}.fa-bed-pulse:before,.fa-procedures:before{content:"\f487"}.fa-shuttle-space:before,.fa-space-shuttle:before{content:"\f197"}.fa-face-laugh:before,.fa-laugh:before{content:"\f599"}.fa-folder-open:before{content:"\f07c"}.fa-heart-circle-plus:before{content:"\e500"}.fa-code-fork:before{content:"\e13b"}.fa-city:before{content:"\f64f"}.fa-microphone-alt:before,.fa-microphone-lines:before{content:"\f3c9"}.fa-pepper-hot:before{content:"\f816"}.fa-unlock:before{content:"\f09c"}.fa-colon-sign:before{content:"\e140"}.fa-headset:before{content:"\f590"}.fa-store-slash:before{content:"\e071"}.fa-road-circle-xmark:before{content:"\e566"}.fa-user-minus:before{content:"\f503"}.fa-mars-stroke-up:before,.fa-mars-stroke-v:before{content:"\f22a"}.fa-champagne-glasses:before,.fa-glass-cheers:before{content:"\f79f"}.fa-clipboard:before{content:"\f328"}.fa-house-circle-exclamation:before{content:"\e50a"}.fa-file-arrow-up:before,.fa-file-upload:before{content:"\f574"}.fa-wifi-3:before,.fa-wifi-strong:before,.fa-wifi:before{content:"\f1eb"}.fa-bath:before,.fa-bathtub:before{content:"\f2cd"}.fa-underline:before{content:"\f0cd"}.fa-user-edit:before,.fa-user-pen:before{content:"\f4ff"}.fa-signature:before{content:"\f5b7"}.fa-stroopwafel:before{content:"\f551"}.fa-bold:before{content:"\f032"}.fa-anchor-lock:before{content:"\e4ad"}.fa-building-ngo:before{content:"\e4d7"}.fa-manat-sign:before{content:"\e1d5"}.fa-not-equal:before{content:"\f53e"}.fa-border-style:before,.fa-border-top-left:before{content:"\f853"}.fa-map-location-dot:before,.fa-map-marked-alt:before{content:"\f5a0"}.fa-jedi:before{content:"\f669"}.fa-poll:before,.fa-square-poll-vertical:before{content:"\f681"}.fa-mug-hot:before{content:"\f7b6"}.fa-battery-car:before,.fa-car-battery:before{content:"\f5df"}.fa-gift:before{content:"\f06b"}.fa-dice-two:before{content:"\f528"}.fa-chess-queen:before{content:"\f445"}.fa-glasses:before{content:"\f530"}.fa-chess-board:before{content:"\f43c"}.fa-building-circle-check:before{content:"\e4d2"}.fa-person-chalkboard:before{content:"\e53d"}.fa-mars-stroke-h:before,.fa-mars-stroke-right:before{content:"\f22b"}.fa-hand-back-fist:before,.fa-hand-rock:before{content:"\f255"}.fa-caret-square-up:before,.fa-square-caret-up:before{content:"\f151"}.fa-cloud-showers-water:before{content:"\e4e4"}.fa-bar-chart:before,.fa-chart-bar:before{content:"\f080"}.fa-hands-bubbles:before,.fa-hands-wash:before{content:"\e05e"}.fa-less-than-equal:before{content:"\f537"}.fa-train:before{content:"\f238"}.fa-eye-low-vision:before,.fa-low-vision:before{content:"\f2a8"}.fa-crow:before{content:"\f520"}.fa-sailboat:before{content:"\e445"}.fa-window-restore:before{content:"\f2d2"}.fa-plus-square:before,.fa-square-plus:before{content:"\f0fe"}.fa-torii-gate:before{content:"\f6a1"}.fa-frog:before{content:"\f52e"}.fa-bucket:before{content:"\e4cf"}.fa-image:before{content:"\f03e"}.fa-microphone:before{content:"\f130"}.fa-cow:before{content:"\f6c8"}.fa-caret-up:before{content:"\f0d8"}.fa-screwdriver:before{content:"\f54a"}.fa-folder-closed:before{content:"\e185"}.fa-house-tsunami:before{content:"\e515"}.fa-square-nfi:before{content:"\e576"}.fa-arrow-up-from-ground-water:before{content:"\e4b5"}.fa-glass-martini-alt:before,.fa-martini-glass:before{content:"\f57b"}.fa-rotate-back:before,.fa-rotate-backward:before,.fa-rotate-left:before,.fa-undo-alt:before{content:"\f2ea"}.fa-columns:before,.fa-table-columns:before{content:"\f0db"}.fa-lemon:before{content:"\f094"}.fa-head-side-mask:before{content:"\e063"}.fa-handshake:before{content:"\f2b5"}.fa-gem:before{content:"\f3a5"}.fa-dolly-box:before,.fa-dolly:before{content:"\f472"}.fa-smoking:before{content:"\f48d"}.fa-compress-arrows-alt:before,.fa-minimize:before{content:"\f78c"}.fa-monument:before{content:"\f5a6"}.fa-snowplow:before{content:"\f7d2"}.fa-angle-double-right:before,.fa-angles-right:before{content:"\f101"}.fa-cannabis:before{content:"\f55f"}.fa-circle-play:before,.fa-play-circle:before{content:"\f144"}.fa-tablets:before{content:"\f490"}.fa-ethernet:before{content:"\f796"}.fa-eur:before,.fa-euro-sign:before,.fa-euro:before{content:"\f153"}.fa-chair:before{content:"\f6c0"}.fa-check-circle:before,.fa-circle-check:before{content:"\f058"}.fa-circle-stop:before,.fa-stop-circle:before{content:"\f28d"}.fa-compass-drafting:before,.fa-drafting-compass:before{content:"\f568"}.fa-plate-wheat:before{content:"\e55a"}.fa-icicles:before{content:"\f7ad"}.fa-person-shelter:before{content:"\e54f"}.fa-neuter:before{content:"\f22c"}.fa-id-badge:before{content:"\f2c1"}.fa-marker:before{content:"\f5a1"}.fa-face-laugh-beam:before,.fa-laugh-beam:before{content:"\f59a"}.fa-helicopter-symbol:before{content:"\e502"}.fa-universal-access:before{content:"\f29a"}.fa-chevron-circle-up:before,.fa-circle-chevron-up:before{content:"\f139"}.fa-lari-sign:before{content:"\e1c8"}.fa-volcano:before{content:"\f770"}.fa-person-walking-dashed-line-arrow-right:before{content:"\e553"}.fa-gbp:before,.fa-pound-sign:before,.fa-sterling-sign:before{content:"\f154"}.fa-viruses:before{content:"\e076"}.fa-square-person-confined:before{content:"\e577"}.fa-user-tie:before{content:"\f508"}.fa-arrow-down-long:before,.fa-long-arrow-down:before{content:"\f175"}.fa-tent-arrow-down-to-line:before{content:"\e57e"}.fa-certificate:before{content:"\f0a3"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-suitcase:before{content:"\f0f2"}.fa-person-skating:before,.fa-skating:before{content:"\f7c5"}.fa-filter-circle-dollar:before,.fa-funnel-dollar:before{content:"\f662"}.fa-camera-retro:before{content:"\f083"}.fa-arrow-circle-down:before,.fa-circle-arrow-down:before{content:"\f0ab"}.fa-arrow-right-to-file:before,.fa-file-import:before{content:"\f56f"}.fa-external-link-square:before,.fa-square-arrow-up-right:before{content:"\f14c"}.fa-box-open:before{content:"\f49e"}.fa-scroll:before{content:"\f70e"}.fa-spa:before{content:"\f5bb"}.fa-location-pin-lock:before{content:"\e51f"}.fa-pause:before{content:"\f04c"}.fa-hill-avalanche:before{content:"\e507"}.fa-temperature-0:before,.fa-temperature-empty:before,.fa-thermometer-0:before,.fa-thermometer-empty:before{content:"\f2cb"}.fa-bomb:before{content:"\f1e2"}.fa-registered:before{content:"\f25d"}.fa-address-card:before,.fa-contact-card:before,.fa-vcard:before{content:"\f2bb"}.fa-balance-scale-right:before,.fa-scale-unbalanced-flip:before{content:"\f516"}.fa-subscript:before{content:"\f12c"}.fa-diamond-turn-right:before,.fa-directions:before{content:"\f5eb"}.fa-burst:before{content:"\e4dc"}.fa-house-laptop:before,.fa-laptop-house:before{content:"\e066"}.fa-face-tired:before,.fa-tired:before{content:"\f5c8"}.fa-money-bills:before{content:"\e1f3"}.fa-smog:before{content:"\f75f"}.fa-crutch:before{content:"\f7f7"}.fa-cloud-arrow-up:before,.fa-cloud-upload-alt:before,.fa-cloud-upload:before{content:"\f0ee"}.fa-palette:before{content:"\f53f"}.fa-arrows-turn-right:before{content:"\e4c0"}.fa-vest:before{content:"\e085"}.fa-ferry:before{content:"\e4ea"}.fa-arrows-down-to-people:before{content:"\e4b9"}.fa-seedling:before,.fa-sprout:before{content:"\f4d8"}.fa-arrows-alt-h:before,.fa-left-right:before{content:"\f337"}.fa-boxes-packing:before{content:"\e4c7"}.fa-arrow-circle-left:before,.fa-circle-arrow-left:before{content:"\f0a8"}.fa-group-arrows-rotate:before{content:"\e4f6"}.fa-bowl-food:before{content:"\e4c6"}.fa-candy-cane:before{content:"\f786"}.fa-arrow-down-wide-short:before,.fa-sort-amount-asc:before,.fa-sort-amount-down:before{content:"\f160"}.fa-cloud-bolt:before,.fa-thunderstorm:before{content:"\f76c"}.fa-remove-format:before,.fa-text-slash:before{content:"\f87d"}.fa-face-smile-wink:before,.fa-smile-wink:before{content:"\f4da"}.fa-file-word:before{content:"\f1c2"}.fa-file-powerpoint:before{content:"\f1c4"}.fa-arrows-h:before,.fa-arrows-left-right:before{content:"\f07e"}.fa-house-lock:before{content:"\e510"}.fa-cloud-arrow-down:before,.fa-cloud-download-alt:before,.fa-cloud-download:before{content:"\f0ed"}.fa-children:before{content:"\e4e1"}.fa-blackboard:before,.fa-chalkboard:before{content:"\f51b"}.fa-user-alt-slash:before,.fa-user-large-slash:before{content:"\f4fa"}.fa-envelope-open:before{content:"\f2b6"}.fa-handshake-alt-slash:before,.fa-handshake-simple-slash:before{content:"\e05f"}.fa-mattress-pillow:before{content:"\e525"}.fa-guarani-sign:before{content:"\e19a"}.fa-arrows-rotate:before,.fa-refresh:before,.fa-sync:before{content:"\f021"}.fa-fire-extinguisher:before{content:"\f134"}.fa-cruzeiro-sign:before{content:"\e152"}.fa-greater-than-equal:before{content:"\f532"}.fa-shield-alt:before,.fa-shield-halved:before{content:"\f3ed"}.fa-atlas:before,.fa-book-atlas:before{content:"\f558"}.fa-virus:before{content:"\e074"}.fa-envelope-circle-check:before{content:"\e4e8"}.fa-layer-group:before{content:"\f5fd"}.fa-arrows-to-dot:before{content:"\e4be"}.fa-archway:before{content:"\f557"}.fa-heart-circle-check:before{content:"\e4fd"}.fa-house-chimney-crack:before,.fa-house-damage:before{content:"\f6f1"}.fa-file-archive:before,.fa-file-zipper:before{content:"\f1c6"}.fa-square:before{content:"\f0c8"}.fa-glass-martini:before,.fa-martini-glass-empty:before{content:"\f000"}.fa-couch:before{content:"\f4b8"}.fa-cedi-sign:before{content:"\e0df"}.fa-italic:before{content:"\f033"}.fa-church:before{content:"\f51d"}.fa-comments-dollar:before{content:"\f653"}.fa-democrat:before{content:"\f747"}.fa-z:before{content:"\5a"}.fa-person-skiing:before,.fa-skiing:before{content:"\f7c9"}.fa-road-lock:before{content:"\e567"}.fa-a:before{content:"\41"}.fa-temperature-arrow-down:before,.fa-temperature-down:before{content:"\e03f"}.fa-feather-alt:before,.fa-feather-pointed:before{content:"\f56b"}.fa-p:before{content:"\50"}.fa-snowflake:before{content:"\f2dc"}.fa-newspaper:before{content:"\f1ea"}.fa-ad:before,.fa-rectangle-ad:before{content:"\f641"}.fa-arrow-circle-right:before,.fa-circle-arrow-right:before{content:"\f0a9"}.fa-filter-circle-xmark:before{content:"\e17b"}.fa-locust:before{content:"\e520"}.fa-sort:before,.fa-unsorted:before{content:"\f0dc"}.fa-list-1-2:before,.fa-list-numeric:before,.fa-list-ol:before{content:"\f0cb"}.fa-person-dress-burst:before{content:"\e544"}.fa-money-check-alt:before,.fa-money-check-dollar:before{content:"\f53d"}.fa-vector-square:before{content:"\f5cb"}.fa-bread-slice:before{content:"\f7ec"}.fa-language:before{content:"\f1ab"}.fa-face-kiss-wink-heart:before,.fa-kiss-wink-heart:before{content:"\f598"}.fa-filter:before{content:"\f0b0"}.fa-question:before{content:"\3f"}.fa-file-signature:before{content:"\f573"}.fa-arrows-alt:before,.fa-up-down-left-right:before{content:"\f0b2"}.fa-house-chimney-user:before{content:"\e065"}.fa-hand-holding-heart:before{content:"\f4be"}.fa-puzzle-piece:before{content:"\f12e"}.fa-money-check:before{content:"\f53c"}.fa-star-half-alt:before,.fa-star-half-stroke:before{content:"\f5c0"}.fa-code:before{content:"\f121"}.fa-glass-whiskey:before,.fa-whiskey-glass:before{content:"\f7a0"}.fa-building-circle-exclamation:before{content:"\e4d3"}.fa-magnifying-glass-chart:before{content:"\e522"}.fa-arrow-up-right-from-square:before,.fa-external-link:before{content:"\f08e"}.fa-cubes-stacked:before{content:"\e4e6"}.fa-krw:before,.fa-won-sign:before,.fa-won:before{content:"\f159"}.fa-virus-covid:before{content:"\e4a8"}.fa-austral-sign:before{content:"\e0a9"}.fa-f:before{content:"\46"}.fa-leaf:before{content:"\f06c"}.fa-road:before{content:"\f018"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-person-circle-plus:before{content:"\e541"}.fa-chart-pie:before,.fa-pie-chart:before{content:"\f200"}.fa-bolt-lightning:before{content:"\e0b7"}.fa-sack-xmark:before{content:"\e56a"}.fa-file-excel:before{content:"\f1c3"}.fa-file-contract:before{content:"\f56c"}.fa-fish-fins:before{content:"\e4f2"}.fa-building-flag:before{content:"\e4d5"}.fa-face-grin-beam:before,.fa-grin-beam:before{content:"\f582"}.fa-object-ungroup:before{content:"\f248"}.fa-poop:before{content:"\f619"}.fa-location-pin:before,.fa-map-marker:before{content:"\f041"}.fa-kaaba:before{content:"\f66b"}.fa-toilet-paper:before{content:"\f71e"}.fa-hard-hat:before,.fa-hat-hard:before,.fa-helmet-safety:before{content:"\f807"}.fa-eject:before{content:"\f052"}.fa-arrow-alt-circle-right:before,.fa-circle-right:before{content:"\f35a"}.fa-plane-circle-check:before{content:"\e555"}.fa-face-rolling-eyes:before,.fa-meh-rolling-eyes:before{content:"\f5a5"}.fa-object-group:before{content:"\f247"}.fa-chart-line:before,.fa-line-chart:before{content:"\f201"}.fa-mask-ventilator:before{content:"\e524"}.fa-arrow-right:before{content:"\f061"}.fa-map-signs:before,.fa-signs-post:before{content:"\f277"}.fa-cash-register:before{content:"\f788"}.fa-person-circle-question:before{content:"\e542"}.fa-h:before{content:"\48"}.fa-tarp:before{content:"\e57b"}.fa-screwdriver-wrench:before,.fa-tools:before{content:"\f7d9"}.fa-arrows-to-eye:before{content:"\e4bf"}.fa-plug-circle-bolt:before{content:"\e55b"}.fa-heart:before{content:"\f004"}.fa-mars-and-venus:before{content:"\f224"}.fa-home-user:before,.fa-house-user:before{content:"\e1b0"}.fa-dumpster-fire:before{content:"\f794"}.fa-house-crack:before{content:"\e3b1"}.fa-cocktail:before,.fa-martini-glass-citrus:before{content:"\f561"}.fa-face-surprise:before,.fa-surprise:before{content:"\f5c2"}.fa-bottle-water:before{content:"\e4c5"}.fa-circle-pause:before,.fa-pause-circle:before{content:"\f28b"}.fa-toilet-paper-slash:before{content:"\e072"}.fa-apple-alt:before,.fa-apple-whole:before{content:"\f5d1"}.fa-kitchen-set:before{content:"\e51a"}.fa-r:before{content:"\52"}.fa-temperature-1:before,.fa-temperature-quarter:before,.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:"\f2ca"}.fa-cube:before{content:"\f1b2"}.fa-bitcoin-sign:before{content:"\e0b4"}.fa-shield-dog:before{content:"\e573"}.fa-solar-panel:before{content:"\f5ba"}.fa-lock-open:before{content:"\f3c1"}.fa-elevator:before{content:"\e16d"}.fa-money-bill-transfer:before{content:"\e528"}.fa-money-bill-trend-up:before{content:"\e529"}.fa-house-flood-water-circle-arrow-right:before{content:"\e50f"}.fa-poll-h:before,.fa-square-poll-horizontal:before{content:"\f682"}.fa-circle:before{content:"\f111"}.fa-backward-fast:before,.fa-fast-backward:before{content:"\f049"}.fa-recycle:before{content:"\f1b8"}.fa-user-astronaut:before{content:"\f4fb"}.fa-plane-slash:before{content:"\e069"}.fa-trademark:before{content:"\f25c"}.fa-basketball-ball:before,.fa-basketball:before{content:"\f434"}.fa-satellite-dish:before{content:"\f7c0"}.fa-arrow-alt-circle-up:before,.fa-circle-up:before{content:"\f35b"}.fa-mobile-alt:before,.fa-mobile-screen-button:before{content:"\f3cd"}.fa-volume-high:before,.fa-volume-up:before{content:"\f028"}.fa-users-rays:before{content:"\e593"}.fa-wallet:before{content:"\f555"}.fa-clipboard-check:before{content:"\f46c"}.fa-file-audio:before{content:"\f1c7"}.fa-burger:before,.fa-hamburger:before{content:"\f805"}.fa-wrench:before{content:"\f0ad"}.fa-bugs:before{content:"\e4d0"}.fa-rupee-sign:before,.fa-rupee:before{content:"\f156"}.fa-file-image:before{content:"\f1c5"}.fa-circle-question:before,.fa-question-circle:before{content:"\f059"}.fa-plane-departure:before{content:"\f5b0"}.fa-handshake-slash:before{content:"\e060"}.fa-book-bookmark:before{content:"\e0bb"}.fa-code-branch:before{content:"\f126"}.fa-hat-cowboy:before{content:"\f8c0"}.fa-bridge:before{content:"\e4c8"}.fa-phone-alt:before,.fa-phone-flip:before{content:"\f879"}.fa-truck-front:before{content:"\e2b7"}.fa-cat:before{content:"\f6be"}.fa-anchor-circle-exclamation:before{content:"\e4ab"}.fa-truck-field:before{content:"\e58d"}.fa-route:before{content:"\f4d7"}.fa-clipboard-question:before{content:"\e4e3"}.fa-panorama:before{content:"\e209"}.fa-comment-medical:before{content:"\f7f5"}.fa-teeth-open:before{content:"\f62f"}.fa-file-circle-minus:before{content:"\e4ed"}.fa-tags:before{content:"\f02c"}.fa-wine-glass:before{content:"\f4e3"}.fa-fast-forward:before,.fa-forward-fast:before{content:"\f050"}.fa-face-meh-blank:before,.fa-meh-blank:before{content:"\f5a4"}.fa-parking:before,.fa-square-parking:before{content:"\f540"}.fa-house-signal:before{content:"\e012"}.fa-bars-progress:before,.fa-tasks-alt:before{content:"\f828"}.fa-faucet-drip:before{content:"\e006"}.fa-cart-flatbed:before,.fa-dolly-flatbed:before{content:"\f474"}.fa-ban-smoking:before,.fa-smoking-ban:before{content:"\f54d"}.fa-terminal:before{content:"\f120"}.fa-mobile-button:before{content:"\f10b"}.fa-house-medical-flag:before{content:"\e514"}.fa-basket-shopping:before,.fa-shopping-basket:before{content:"\f291"}.fa-tape:before{content:"\f4db"}.fa-bus-alt:before,.fa-bus-simple:before{content:"\f55e"}.fa-eye:before{content:"\f06e"}.fa-face-sad-cry:before,.fa-sad-cry:before{content:"\f5b3"}.fa-audio-description:before{content:"\f29e"}.fa-person-military-to-person:before{content:"\e54c"}.fa-file-shield:before{content:"\e4f0"}.fa-user-slash:before{content:"\f506"}.fa-pen:before{content:"\f304"}.fa-tower-observation:before{content:"\e586"}.fa-file-code:before{content:"\f1c9"}.fa-signal-5:before,.fa-signal-perfect:before,.fa-signal:before{content:"\f012"}.fa-bus:before{content:"\f207"}.fa-heart-circle-xmark:before{content:"\e501"}.fa-home-lg:before,.fa-house-chimney:before{content:"\e3af"}.fa-window-maximize:before{content:"\f2d0"}.fa-face-frown:before,.fa-frown:before{content:"\f119"}.fa-prescription:before{content:"\f5b1"}.fa-shop:before,.fa-store-alt:before{content:"\f54f"}.fa-floppy-disk:before,.fa-save:before{content:"\f0c7"}.fa-vihara:before{content:"\f6a7"}.fa-balance-scale-left:before,.fa-scale-unbalanced:before{content:"\f515"}.fa-sort-asc:before,.fa-sort-up:before{content:"\f0de"}.fa-comment-dots:before,.fa-commenting:before{content:"\f4ad"}.fa-plant-wilt:before{content:"\e5aa"}.fa-diamond:before{content:"\f219"}.fa-face-grin-squint:before,.fa-grin-squint:before{content:"\f585"}.fa-hand-holding-dollar:before,.fa-hand-holding-usd:before{content:"\f4c0"}.fa-bacterium:before{content:"\e05a"}.fa-hand-pointer:before{content:"\f25a"}.fa-drum-steelpan:before{content:"\f56a"}.fa-hand-scissors:before{content:"\f257"}.fa-hands-praying:before,.fa-praying-hands:before{content:"\f684"}.fa-arrow-right-rotate:before,.fa-arrow-rotate-forward:before,.fa-arrow-rotate-right:before,.fa-redo:before{content:"\f01e"}.fa-biohazard:before{content:"\f780"}.fa-location-crosshairs:before,.fa-location:before{content:"\f601"}.fa-mars-double:before{content:"\f227"}.fa-child-dress:before{content:"\e59c"}.fa-users-between-lines:before{content:"\e591"}.fa-lungs-virus:before{content:"\e067"}.fa-face-grin-tears:before,.fa-grin-tears:before{content:"\f588"}.fa-phone:before{content:"\f095"}.fa-calendar-times:before,.fa-calendar-xmark:before{content:"\f273"}.fa-child-reaching:before{content:"\e59d"}.fa-head-side-virus:before{content:"\e064"}.fa-user-cog:before,.fa-user-gear:before{content:"\f4fe"}.fa-arrow-up-1-9:before,.fa-sort-numeric-up:before{content:"\f163"}.fa-door-closed:before{content:"\f52a"}.fa-shield-virus:before{content:"\e06c"}.fa-dice-six:before{content:"\f526"}.fa-mosquito-net:before{content:"\e52c"}.fa-bridge-water:before{content:"\e4ce"}.fa-person-booth:before{content:"\f756"}.fa-text-width:before{content:"\f035"}.fa-hat-wizard:before{content:"\f6e8"}.fa-pen-fancy:before{content:"\f5ac"}.fa-digging:before,.fa-person-digging:before{content:"\f85e"}.fa-trash:before{content:"\f1f8"}.fa-gauge-simple-med:before,.fa-gauge-simple:before,.fa-tachometer-average:before{content:"\f629"}.fa-book-medical:before{content:"\f7e6"}.fa-poo:before{content:"\f2fe"}.fa-quote-right-alt:before,.fa-quote-right:before{content:"\f10e"}.fa-shirt:before,.fa-t-shirt:before,.fa-tshirt:before{content:"\f553"}.fa-cubes:before{content:"\f1b3"}.fa-divide:before{content:"\f529"}.fa-tenge-sign:before,.fa-tenge:before{content:"\f7d7"}.fa-headphones:before{content:"\f025"}.fa-hands-holding:before{content:"\f4c2"}.fa-hands-clapping:before{content:"\e1a8"}.fa-republican:before{content:"\f75e"}.fa-arrow-left:before{content:"\f060"}.fa-person-circle-xmark:before{content:"\e543"}.fa-ruler:before{content:"\f545"}.fa-align-left:before{content:"\f036"}.fa-dice-d6:before{content:"\f6d1"}.fa-restroom:before{content:"\f7bd"}.fa-j:before{content:"\4a"}.fa-users-viewfinder:before{content:"\e595"}.fa-file-video:before{content:"\f1c8"}.fa-external-link-alt:before,.fa-up-right-from-square:before{content:"\f35d"}.fa-table-cells:before,.fa-th:before{content:"\f00a"}.fa-file-pdf:before{content:"\f1c1"}.fa-bible:before,.fa-book-bible:before{content:"\f647"}.fa-o:before{content:"\4f"}.fa-medkit:before,.fa-suitcase-medical:before{content:"\f0fa"}.fa-user-secret:before{content:"\f21b"}.fa-otter:before{content:"\f700"}.fa-female:before,.fa-person-dress:before{content:"\f182"}.fa-comment-dollar:before{content:"\f651"}.fa-briefcase-clock:before,.fa-business-time:before{content:"\f64a"}.fa-table-cells-large:before,.fa-th-large:before{content:"\f009"}.fa-book-tanakh:before,.fa-tanakh:before{content:"\f827"}.fa-phone-volume:before,.fa-volume-control-phone:before{content:"\f2a0"}.fa-hat-cowboy-side:before{content:"\f8c1"}.fa-clipboard-user:before{content:"\f7f3"}.fa-child:before{content:"\f1ae"}.fa-lira-sign:before{content:"\f195"}.fa-satellite:before{content:"\f7bf"}.fa-plane-lock:before{content:"\e558"}.fa-tag:before{content:"\f02b"}.fa-comment:before{content:"\f075"}.fa-birthday-cake:before,.fa-cake-candles:before,.fa-cake:before{content:"\f1fd"}.fa-envelope:before{content:"\f0e0"}.fa-angle-double-up:before,.fa-angles-up:before{content:"\f102"}.fa-paperclip:before{content:"\f0c6"}.fa-arrow-right-to-city:before{content:"\e4b3"}.fa-ribbon:before{content:"\f4d6"}.fa-lungs:before{content:"\f604"}.fa-arrow-up-9-1:before,.fa-sort-numeric-up-alt:before{content:"\f887"}.fa-litecoin-sign:before{content:"\e1d3"}.fa-border-none:before{content:"\f850"}.fa-circle-nodes:before{content:"\e4e2"}.fa-parachute-box:before{content:"\f4cd"}.fa-indent:before{content:"\f03c"}.fa-truck-field-un:before{content:"\e58e"}.fa-hourglass-empty:before,.fa-hourglass:before{content:"\f254"}.fa-mountain:before{content:"\f6fc"}.fa-user-doctor:before,.fa-user-md:before{content:"\f0f0"}.fa-circle-info:before,.fa-info-circle:before{content:"\f05a"}.fa-cloud-meatball:before{content:"\f73b"}.fa-camera-alt:before,.fa-camera:before{content:"\f030"}.fa-square-virus:before{content:"\e578"}.fa-meteor:before{content:"\f753"}.fa-car-on:before{content:"\e4dd"}.fa-sleigh:before{content:"\f7cc"}.fa-arrow-down-1-9:before,.fa-sort-numeric-asc:before,.fa-sort-numeric-down:before{content:"\f162"}.fa-hand-holding-droplet:before,.fa-hand-holding-water:before{content:"\f4c1"}.fa-water:before{content:"\f773"}.fa-calendar-check:before{content:"\f274"}.fa-braille:before{content:"\f2a1"}.fa-prescription-bottle-alt:before,.fa-prescription-bottle-medical:before{content:"\f486"}.fa-landmark:before{content:"\f66f"}.fa-truck:before{content:"\f0d1"}.fa-crosshairs:before{content:"\f05b"}.fa-person-cane:before{content:"\e53c"}.fa-tent:before{content:"\e57d"}.fa-vest-patches:before{content:"\e086"}.fa-check-double:before{content:"\f560"}.fa-arrow-down-a-z:before,.fa-sort-alpha-asc:before,.fa-sort-alpha-down:before{content:"\f15d"}.fa-money-bill-wheat:before{content:"\e52a"}.fa-cookie:before{content:"\f563"}.fa-arrow-left-rotate:before,.fa-arrow-rotate-back:before,.fa-arrow-rotate-backward:before,.fa-arrow-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-hard-drive:before,.fa-hdd:before{content:"\f0a0"}.fa-face-grin-squint-tears:before,.fa-grin-squint-tears:before{content:"\f586"}.fa-dumbbell:before{content:"\f44b"}.fa-list-alt:before,.fa-rectangle-list:before{content:"\f022"}.fa-tarp-droplet:before{content:"\e57c"}.fa-house-medical-circle-check:before{content:"\e511"}.fa-person-skiing-nordic:before,.fa-skiing-nordic:before{content:"\f7ca"}.fa-calendar-plus:before{content:"\f271"}.fa-plane-arrival:before{content:"\f5af"}.fa-arrow-alt-circle-left:before,.fa-circle-left:before{content:"\f359"}.fa-subway:before,.fa-train-subway:before{content:"\f239"}.fa-chart-gantt:before{content:"\e0e4"}.fa-indian-rupee-sign:before,.fa-indian-rupee:before,.fa-inr:before{content:"\e1bc"}.fa-crop-alt:before,.fa-crop-simple:before{content:"\f565"}.fa-money-bill-1:before,.fa-money-bill-alt:before{content:"\f3d1"}.fa-left-long:before,.fa-long-arrow-alt-left:before{content:"\f30a"}.fa-dna:before{content:"\f471"}.fa-virus-slash:before{content:"\e075"}.fa-minus:before,.fa-subtract:before{content:"\f068"}.fa-chess:before{content:"\f439"}.fa-arrow-left-long:before,.fa-long-arrow-left:before{content:"\f177"}.fa-plug-circle-check:before{content:"\e55c"}.fa-street-view:before{content:"\f21d"}.fa-franc-sign:before{content:"\e18f"}.fa-volume-off:before{content:"\f026"}.fa-american-sign-language-interpreting:before,.fa-asl-interpreting:before,.fa-hands-american-sign-language-interpreting:before,.fa-hands-asl-interpreting:before{content:"\f2a3"}.fa-cog:before,.fa-gear:before{content:"\f013"}.fa-droplet-slash:before,.fa-tint-slash:before{content:"\f5c7"}.fa-mosque:before{content:"\f678"}.fa-mosquito:before{content:"\e52b"}.fa-star-of-david:before{content:"\f69a"}.fa-person-military-rifle:before{content:"\e54b"}.fa-cart-shopping:before,.fa-shopping-cart:before{content:"\f07a"}.fa-vials:before{content:"\f493"}.fa-plug-circle-plus:before{content:"\e55f"}.fa-place-of-worship:before{content:"\f67f"}.fa-grip-vertical:before{content:"\f58e"}.fa-arrow-turn-up:before,.fa-level-up:before{content:"\f148"}.fa-u:before{content:"\55"}.fa-square-root-alt:before,.fa-square-root-variable:before{content:"\f698"}.fa-clock-four:before,.fa-clock:before{content:"\f017"}.fa-backward-step:before,.fa-step-backward:before{content:"\f048"}.fa-pallet:before{content:"\f482"}.fa-faucet:before{content:"\e005"}.fa-baseball-bat-ball:before{content:"\f432"}.fa-s:before{content:"\53"}.fa-timeline:before{content:"\e29c"}.fa-keyboard:before{content:"\f11c"}.fa-caret-down:before{content:"\f0d7"}.fa-clinic-medical:before,.fa-house-chimney-medical:before{content:"\f7f2"}.fa-temperature-3:before,.fa-temperature-three-quarters:before,.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-mobile-android-alt:before,.fa-mobile-screen:before{content:"\f3cf"}.fa-plane-up:before{content:"\e22d"}.fa-piggy-bank:before{content:"\f4d3"}.fa-battery-3:before,.fa-battery-half:before{content:"\f242"}.fa-mountain-city:before{content:"\e52e"}.fa-coins:before{content:"\f51e"}.fa-khanda:before{content:"\f66d"}.fa-sliders-h:before,.fa-sliders:before{content:"\f1de"}.fa-folder-tree:before{content:"\f802"}.fa-network-wired:before{content:"\f6ff"}.fa-map-pin:before{content:"\f276"}.fa-hamsa:before{content:"\f665"}.fa-cent-sign:before{content:"\e3f5"}.fa-flask:before{content:"\f0c3"}.fa-person-pregnant:before{content:"\e31e"}.fa-wand-sparkles:before{content:"\f72b"}.fa-ellipsis-v:before,.fa-ellipsis-vertical:before{content:"\f142"}.fa-ticket:before{content:"\f145"}.fa-power-off:before{content:"\f011"}.fa-long-arrow-alt-right:before,.fa-right-long:before{content:"\f30b"}.fa-flag-usa:before{content:"\f74d"}.fa-laptop-file:before{content:"\e51d"}.fa-teletype:before,.fa-tty:before{content:"\f1e4"}.fa-diagram-next:before{content:"\e476"}.fa-person-rifle:before{content:"\e54e"}.fa-house-medical-circle-exclamation:before{content:"\e512"}.fa-closed-captioning:before{content:"\f20a"}.fa-hiking:before,.fa-person-hiking:before{content:"\f6ec"}.fa-venus-double:before{content:"\f226"}.fa-images:before{content:"\f302"}.fa-calculator:before{content:"\f1ec"}.fa-people-pulling:before{content:"\e535"}.fa-n:before{content:"\4e"}.fa-cable-car:before,.fa-tram:before{content:"\f7da"}.fa-cloud-rain:before{content:"\f73d"}.fa-building-circle-xmark:before{content:"\e4d4"}.fa-ship:before{content:"\f21a"}.fa-arrows-down-to-line:before{content:"\e4b8"}.fa-download:before{content:"\f019"}.fa-face-grin:before,.fa-grin:before{content:"\f580"}.fa-backspace:before,.fa-delete-left:before{content:"\f55a"}.fa-eye-dropper-empty:before,.fa-eye-dropper:before,.fa-eyedropper:before{content:"\f1fb"}.fa-file-circle-check:before{content:"\e5a0"}.fa-forward:before{content:"\f04e"}.fa-mobile-android:before,.fa-mobile-phone:before,.fa-mobile:before{content:"\f3ce"}.fa-face-meh:before,.fa-meh:before{content:"\f11a"}.fa-align-center:before{content:"\f037"}.fa-book-dead:before,.fa-book-skull:before{content:"\f6b7"}.fa-drivers-license:before,.fa-id-card:before{content:"\f2c2"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-heart-circle-exclamation:before{content:"\e4fe"}.fa-home-alt:before,.fa-home-lg-alt:before,.fa-home:before,.fa-house:before{content:"\f015"}.fa-calendar-week:before{content:"\f784"}.fa-laptop-medical:before{content:"\f812"}.fa-b:before{content:"\42"}.fa-file-medical:before{content:"\f477"}.fa-dice-one:before{content:"\f525"}.fa-kiwi-bird:before{content:"\f535"}.fa-arrow-right-arrow-left:before,.fa-exchange:before{content:"\f0ec"}.fa-redo-alt:before,.fa-rotate-forward:before,.fa-rotate-right:before{content:"\f2f9"}.fa-cutlery:before,.fa-utensils:before{content:"\f2e7"}.fa-arrow-up-wide-short:before,.fa-sort-amount-up:before{content:"\f161"}.fa-mill-sign:before{content:"\e1ed"}.fa-bowl-rice:before{content:"\e2eb"}.fa-skull:before{content:"\f54c"}.fa-broadcast-tower:before,.fa-tower-broadcast:before{content:"\f519"}.fa-truck-pickup:before{content:"\f63c"}.fa-long-arrow-alt-up:before,.fa-up-long:before{content:"\f30c"}.fa-stop:before{content:"\f04d"}.fa-code-merge:before{content:"\f387"}.fa-upload:before{content:"\f093"}.fa-hurricane:before{content:"\f751"}.fa-mound:before{content:"\e52d"}.fa-toilet-portable:before{content:"\e583"}.fa-compact-disc:before{content:"\f51f"}.fa-file-arrow-down:before,.fa-file-download:before{content:"\f56d"}.fa-caravan:before{content:"\f8ff"}.fa-shield-cat:before{content:"\e572"}.fa-bolt:before,.fa-zap:before{content:"\f0e7"}.fa-glass-water:before{content:"\e4f4"}.fa-oil-well:before{content:"\e532"}.fa-vault:before{content:"\e2c5"}.fa-mars:before{content:"\f222"}.fa-toilet:before{content:"\f7d8"}.fa-plane-circle-xmark:before{content:"\e557"}.fa-cny:before,.fa-jpy:before,.fa-rmb:before,.fa-yen-sign:before,.fa-yen:before{content:"\f157"}.fa-rouble:before,.fa-rub:before,.fa-ruble-sign:before,.fa-ruble:before{content:"\f158"}.fa-sun:before{content:"\f185"}.fa-guitar:before{content:"\f7a6"}.fa-face-laugh-wink:before,.fa-laugh-wink:before{content:"\f59c"}.fa-horse-head:before{content:"\f7ab"}.fa-bore-hole:before{content:"\e4c3"}.fa-industry:before{content:"\f275"}.fa-arrow-alt-circle-down:before,.fa-circle-down:before{content:"\f358"}.fa-arrows-turn-to-dots:before{content:"\e4c1"}.fa-florin-sign:before{content:"\e184"}.fa-arrow-down-short-wide:before,.fa-sort-amount-desc:before,.fa-sort-amount-down-alt:before{content:"\f884"}.fa-less-than:before{content:"\3c"}.fa-angle-down:before{content:"\f107"}.fa-car-tunnel:before{content:"\e4de"}.fa-head-side-cough:before{content:"\e061"}.fa-grip-lines:before{content:"\f7a4"}.fa-thumbs-down:before{content:"\f165"}.fa-user-lock:before{content:"\f502"}.fa-arrow-right-long:before,.fa-long-arrow-right:before{content:"\f178"}.fa-anchor-circle-xmark:before{content:"\e4ac"}.fa-ellipsis-h:before,.fa-ellipsis:before{content:"\f141"}.fa-chess-pawn:before{content:"\f443"}.fa-first-aid:before,.fa-kit-medical:before{content:"\f479"}.fa-person-through-window:before{content:"\e5a9"}.fa-toolbox:before{content:"\f552"}.fa-hands-holding-circle:before{content:"\e4fb"}.fa-bug:before{content:"\f188"}.fa-credit-card-alt:before,.fa-credit-card:before{content:"\f09d"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-hand-holding-hand:before{content:"\e4f7"}.fa-book-open-reader:before,.fa-book-reader:before{content:"\f5da"}.fa-mountain-sun:before{content:"\e52f"}.fa-arrows-left-right-to-line:before{content:"\e4ba"}.fa-dice-d20:before{content:"\f6cf"}.fa-truck-droplet:before{content:"\e58c"}.fa-file-circle-xmark:before{content:"\e5a1"}.fa-temperature-arrow-up:before,.fa-temperature-up:before{content:"\e040"}.fa-medal:before{content:"\f5a2"}.fa-bed:before{content:"\f236"}.fa-h-square:before,.fa-square-h:before{content:"\f0fd"}.fa-podcast:before{content:"\f2ce"}.fa-temperature-4:before,.fa-temperature-full:before,.fa-thermometer-4:before,.fa-thermometer-full:before{content:"\f2c7"}.fa-bell:before{content:"\f0f3"}.fa-superscript:before{content:"\f12b"}.fa-plug-circle-xmark:before{content:"\e560"}.fa-star-of-life:before{content:"\f621"}.fa-phone-slash:before{content:"\f3dd"}.fa-paint-roller:before{content:"\f5aa"}.fa-hands-helping:before,.fa-handshake-angle:before{content:"\f4c4"}.fa-location-dot:before,.fa-map-marker-alt:before{content:"\f3c5"}.fa-file:before{content:"\f15b"}.fa-greater-than:before{content:"\3e"}.fa-person-swimming:before,.fa-swimmer:before{content:"\f5c4"}.fa-arrow-down:before{content:"\f063"}.fa-droplet:before,.fa-tint:before{content:"\f043"}.fa-eraser:before{content:"\f12d"}.fa-earth-america:before,.fa-earth-americas:before,.fa-earth:before,.fa-globe-americas:before{content:"\f57d"}.fa-person-burst:before{content:"\e53b"}.fa-dove:before{content:"\f4ba"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-socks:before{content:"\f696"}.fa-inbox:before{content:"\f01c"}.fa-section:before{content:"\e447"}.fa-gauge-high:before,.fa-tachometer-alt-fast:before,.fa-tachometer-alt:before{content:"\f625"}.fa-envelope-open-text:before{content:"\f658"}.fa-hospital-alt:before,.fa-hospital-wide:before,.fa-hospital:before{content:"\f0f8"}.fa-wine-bottle:before{content:"\f72f"}.fa-chess-rook:before{content:"\f447"}.fa-bars-staggered:before,.fa-reorder:before,.fa-stream:before{content:"\f550"}.fa-dharmachakra:before{content:"\f655"}.fa-hotdog:before{content:"\f80f"}.fa-blind:before,.fa-person-walking-with-cane:before{content:"\f29d"}.fa-drum:before{content:"\f569"}.fa-ice-cream:before{content:"\f810"}.fa-heart-circle-bolt:before{content:"\e4fc"}.fa-fax:before{content:"\f1ac"}.fa-paragraph:before{content:"\f1dd"}.fa-check-to-slot:before,.fa-vote-yea:before{content:"\f772"}.fa-star-half:before{content:"\f089"}.fa-boxes-alt:before,.fa-boxes-stacked:before,.fa-boxes:before{content:"\f468"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-assistive-listening-systems:before,.fa-ear-listen:before{content:"\f2a2"}.fa-tree-city:before{content:"\e587"}.fa-play:before{content:"\f04b"}.fa-font:before{content:"\f031"}.fa-rupiah-sign:before{content:"\e23d"}.fa-magnifying-glass:before,.fa-search:before{content:"\f002"}.fa-ping-pong-paddle-ball:before,.fa-table-tennis-paddle-ball:before,.fa-table-tennis:before{content:"\f45d"}.fa-diagnoses:before,.fa-person-dots-from-line:before{content:"\f470"}.fa-trash-can-arrow-up:before,.fa-trash-restore-alt:before{content:"\f82a"}.fa-naira-sign:before{content:"\e1f6"}.fa-cart-arrow-down:before{content:"\f218"}.fa-walkie-talkie:before{content:"\f8ef"}.fa-file-edit:before,.fa-file-pen:before{content:"\f31c"}.fa-receipt:before{content:"\f543"}.fa-pen-square:before,.fa-pencil-square:before,.fa-square-pen:before{content:"\f14b"}.fa-suitcase-rolling:before{content:"\f5c1"}.fa-person-circle-exclamation:before{content:"\e53f"}.fa-chevron-down:before{content:"\f078"}.fa-battery-5:before,.fa-battery-full:before,.fa-battery:before{content:"\f240"}.fa-skull-crossbones:before{content:"\f714"}.fa-code-compare:before{content:"\e13a"}.fa-list-dots:before,.fa-list-ul:before{content:"\f0ca"}.fa-school-lock:before{content:"\e56f"}.fa-tower-cell:before{content:"\e585"}.fa-down-long:before,.fa-long-arrow-alt-down:before{content:"\f309"}.fa-ranking-star:before{content:"\e561"}.fa-chess-king:before{content:"\f43f"}.fa-person-harassing:before{content:"\e549"}.fa-brazilian-real-sign:before{content:"\e46c"}.fa-landmark-alt:before,.fa-landmark-dome:before{content:"\f752"}.fa-arrow-up:before{content:"\f062"}.fa-television:before,.fa-tv-alt:before,.fa-tv:before{content:"\f26c"}.fa-shrimp:before{content:"\e448"}.fa-list-check:before,.fa-tasks:before{content:"\f0ae"}.fa-jug-detergent:before{content:"\e519"}.fa-circle-user:before,.fa-user-circle:before{content:"\f2bd"}.fa-user-shield:before{content:"\f505"}.fa-wind:before{content:"\f72e"}.fa-car-burst:before,.fa-car-crash:before{content:"\f5e1"}.fa-y:before{content:"\59"}.fa-person-snowboarding:before,.fa-snowboarding:before{content:"\f7ce"}.fa-shipping-fast:before,.fa-truck-fast:before{content:"\f48b"}.fa-fish:before{content:"\f578"}.fa-user-graduate:before{content:"\f501"}.fa-adjust:before,.fa-circle-half-stroke:before{content:"\f042"}.fa-clapperboard:before{content:"\e131"}.fa-circle-radiation:before,.fa-radiation-alt:before{content:"\f7ba"}.fa-baseball-ball:before,.fa-baseball:before{content:"\f433"}.fa-jet-fighter-up:before{content:"\e518"}.fa-diagram-project:before,.fa-project-diagram:before{content:"\f542"}.fa-copy:before{content:"\f0c5"}.fa-volume-mute:before,.fa-volume-times:before,.fa-volume-xmark:before{content:"\f6a9"}.fa-hand-sparkles:before{content:"\e05d"}.fa-grip-horizontal:before,.fa-grip:before{content:"\f58d"}.fa-share-from-square:before,.fa-share-square:before{content:"\f14d"}.fa-child-combatant:before,.fa-child-rifle:before{content:"\e4e0"}.fa-gun:before{content:"\e19b"}.fa-phone-square:before,.fa-square-phone:before{content:"\f098"}.fa-add:before,.fa-plus:before{content:"\2b"}.fa-expand:before{content:"\f065"}.fa-computer:before{content:"\e4e5"}.fa-close:before,.fa-multiply:before,.fa-remove:before,.fa-times:before,.fa-xmark:before{content:"\f00d"}.fa-arrows-up-down-left-right:before,.fa-arrows:before{content:"\f047"}.fa-chalkboard-teacher:before,.fa-chalkboard-user:before{content:"\f51c"}.fa-peso-sign:before{content:"\e222"}.fa-building-shield:before{content:"\e4d8"}.fa-baby:before{content:"\f77c"}.fa-users-line:before{content:"\e592"}.fa-quote-left-alt:before,.fa-quote-left:before{content:"\f10d"}.fa-tractor:before{content:"\f722"}.fa-trash-arrow-up:before,.fa-trash-restore:before{content:"\f829"}.fa-arrow-down-up-lock:before{content:"\e4b0"}.fa-lines-leaning:before{content:"\e51e"}.fa-ruler-combined:before{content:"\f546"}.fa-copyright:before{content:"\f1f9"}.fa-equals:before{content:"\3d"}.fa-blender:before{content:"\f517"}.fa-teeth:before{content:"\f62e"}.fa-ils:before,.fa-shekel-sign:before,.fa-shekel:before,.fa-sheqel-sign:before,.fa-sheqel:before{content:"\f20b"}.fa-map:before{content:"\f279"}.fa-rocket:before{content:"\f135"}.fa-photo-film:before,.fa-photo-video:before{content:"\f87c"}.fa-folder-minus:before{content:"\f65d"}.fa-store:before{content:"\f54e"}.fa-arrow-trend-up:before{content:"\e098"}.fa-plug-circle-minus:before{content:"\e55e"}.fa-sign-hanging:before,.fa-sign:before{content:"\f4d9"}.fa-bezier-curve:before{content:"\f55b"}.fa-bell-slash:before{content:"\f1f6"}.fa-tablet-android:before,.fa-tablet:before{content:"\f3fb"}.fa-school-flag:before{content:"\e56e"}.fa-fill:before{content:"\f575"}.fa-angle-up:before{content:"\f106"}.fa-drumstick-bite:before{content:"\f6d7"}.fa-holly-berry:before{content:"\f7aa"}.fa-chevron-left:before{content:"\f053"}.fa-bacteria:before{content:"\e059"}.fa-hand-lizard:before{content:"\f258"}.fa-notdef:before{content:"\e1fe"}.fa-disease:before{content:"\f7fa"}.fa-briefcase-medical:before{content:"\f469"}.fa-genderless:before{content:"\f22d"}.fa-chevron-right:before{content:"\f054"}.fa-retweet:before{content:"\f079"}.fa-car-alt:before,.fa-car-rear:before{content:"\f5de"}.fa-pump-soap:before{content:"\e06b"}.fa-video-slash:before{content:"\f4e2"}.fa-battery-2:before,.fa-battery-quarter:before{content:"\f243"}.fa-radio:before{content:"\f8d7"}.fa-baby-carriage:before,.fa-carriage-baby:before{content:"\f77d"}.fa-traffic-light:before{content:"\f637"}.fa-thermometer:before{content:"\f491"}.fa-vr-cardboard:before{content:"\f729"}.fa-hand-middle-finger:before{content:"\f806"}.fa-percent:before,.fa-percentage:before{content:"\25"}.fa-truck-moving:before{content:"\f4df"}.fa-glass-water-droplet:before{content:"\e4f5"}.fa-display:before{content:"\e163"}.fa-face-smile:before,.fa-smile:before{content:"\f118"}.fa-thumb-tack:before,.fa-thumbtack:before{content:"\f08d"}.fa-trophy:before{content:"\f091"}.fa-person-praying:before,.fa-pray:before{content:"\f683"}.fa-hammer:before{content:"\f6e3"}.fa-hand-peace:before{content:"\f25b"}.fa-rotate:before,.fa-sync-alt:before{content:"\f2f1"}.fa-spinner:before{content:"\f110"}.fa-robot:before{content:"\f544"}.fa-peace:before{content:"\f67c"}.fa-cogs:before,.fa-gears:before{content:"\f085"}.fa-warehouse:before{content:"\f494"}.fa-arrow-up-right-dots:before{content:"\e4b7"}.fa-splotch:before{content:"\f5bc"}.fa-face-grin-hearts:before,.fa-grin-hearts:before{content:"\f584"}.fa-dice-four:before{content:"\f524"}.fa-sim-card:before{content:"\f7c4"}.fa-transgender-alt:before,.fa-transgender:before{content:"\f225"}.fa-mercury:before{content:"\f223"}.fa-arrow-turn-down:before,.fa-level-down:before{content:"\f149"}.fa-person-falling-burst:before{content:"\e547"}.fa-award:before{content:"\f559"}.fa-ticket-alt:before,.fa-ticket-simple:before{content:"\f3ff"}.fa-building:before{content:"\f1ad"}.fa-angle-double-left:before,.fa-angles-left:before{content:"\f100"}.fa-qrcode:before{content:"\f029"}.fa-clock-rotate-left:before,.fa-history:before{content:"\f1da"}.fa-face-grin-beam-sweat:before,.fa-grin-beam-sweat:before{content:"\f583"}.fa-arrow-right-from-file:before,.fa-file-export:before{content:"\f56e"}.fa-shield-blank:before,.fa-shield:before{content:"\f132"}.fa-arrow-up-short-wide:before,.fa-sort-amount-up-alt:before{content:"\f885"}.fa-house-medical:before{content:"\e3b2"}.fa-golf-ball-tee:before,.fa-golf-ball:before{content:"\f450"}.fa-chevron-circle-left:before,.fa-circle-chevron-left:before{content:"\f137"}.fa-house-chimney-window:before{content:"\e00d"}.fa-pen-nib:before{content:"\f5ad"}.fa-tent-arrow-turn-left:before{content:"\e580"}.fa-tents:before{content:"\e582"}.fa-magic:before,.fa-wand-magic:before{content:"\f0d0"}.fa-dog:before{content:"\f6d3"}.fa-carrot:before{content:"\f787"}.fa-moon:before{content:"\f186"}.fa-wine-glass-alt:before,.fa-wine-glass-empty:before{content:"\f5ce"}.fa-cheese:before{content:"\f7ef"}.fa-yin-yang:before{content:"\f6ad"}.fa-music:before{content:"\f001"}.fa-code-commit:before{content:"\f386"}.fa-temperature-low:before{content:"\f76b"}.fa-biking:before,.fa-person-biking:before{content:"\f84a"}.fa-broom:before{content:"\f51a"}.fa-shield-heart:before{content:"\e574"}.fa-gopuram:before{content:"\f664"}.fa-earth-oceania:before,.fa-globe-oceania:before{content:"\e47b"}.fa-square-xmark:before,.fa-times-square:before,.fa-xmark-square:before{content:"\f2d3"}.fa-hashtag:before{content:"\23"}.fa-expand-alt:before,.fa-up-right-and-down-left-from-center:before{content:"\f424"}.fa-oil-can:before{content:"\f613"}.fa-t:before{content:"\54"}.fa-hippo:before{content:"\f6ed"}.fa-chart-column:before{content:"\e0e3"}.fa-infinity:before{content:"\f534"}.fa-vial-circle-check:before{content:"\e596"}.fa-person-arrow-down-to-line:before{content:"\e538"}.fa-voicemail:before{content:"\f897"}.fa-fan:before{content:"\f863"}.fa-person-walking-luggage:before{content:"\e554"}.fa-arrows-alt-v:before,.fa-up-down:before{content:"\f338"}.fa-cloud-moon-rain:before{content:"\f73c"}.fa-calendar:before{content:"\f133"}.fa-trailer:before{content:"\e041"}.fa-bahai:before,.fa-haykal:before{content:"\f666"}.fa-sd-card:before{content:"\f7c2"}.fa-dragon:before{content:"\f6d5"}.fa-shoe-prints:before{content:"\f54b"}.fa-circle-plus:before,.fa-plus-circle:before{content:"\f055"}.fa-face-grin-tongue-wink:before,.fa-grin-tongue-wink:before{content:"\f58b"}.fa-hand-holding:before{content:"\f4bd"}.fa-plug-circle-exclamation:before{content:"\e55d"}.fa-chain-broken:before,.fa-chain-slash:before,.fa-link-slash:before,.fa-unlink:before{content:"\f127"}.fa-clone:before{content:"\f24d"}.fa-person-walking-arrow-loop-left:before{content:"\e551"}.fa-arrow-up-z-a:before,.fa-sort-alpha-up-alt:before{content:"\f882"}.fa-fire-alt:before,.fa-fire-flame-curved:before{content:"\f7e4"}.fa-tornado:before{content:"\f76f"}.fa-file-circle-plus:before{content:"\e494"}.fa-book-quran:before,.fa-quran:before{content:"\f687"}.fa-anchor:before{content:"\f13d"}.fa-border-all:before{content:"\f84c"}.fa-angry:before,.fa-face-angry:before{content:"\f556"}.fa-cookie-bite:before{content:"\f564"}.fa-arrow-trend-down:before{content:"\e097"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-draw-polygon:before{content:"\f5ee"}.fa-balance-scale:before,.fa-scale-balanced:before{content:"\f24e"}.fa-gauge-simple-high:before,.fa-tachometer-fast:before,.fa-tachometer:before{content:"\f62a"}.fa-shower:before{content:"\f2cc"}.fa-desktop-alt:before,.fa-desktop:before{content:"\f390"}.fa-m:before{content:"\4d"}.fa-table-list:before,.fa-th-list:before{content:"\f00b"}.fa-comment-sms:before,.fa-sms:before{content:"\f7cd"}.fa-book:before{content:"\f02d"}.fa-user-plus:before{content:"\f234"}.fa-check:before{content:"\f00c"}.fa-battery-4:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-house-circle-check:before{content:"\e509"}.fa-angle-left:before{content:"\f104"}.fa-diagram-successor:before{content:"\e47a"}.fa-truck-arrow-right:before{content:"\e58b"}.fa-arrows-split-up-and-left:before{content:"\e4bc"}.fa-fist-raised:before,.fa-hand-fist:before{content:"\f6de"}.fa-cloud-moon:before{content:"\f6c3"}.fa-briefcase:before{content:"\f0b1"}.fa-person-falling:before{content:"\e546"}.fa-image-portrait:before,.fa-portrait:before{content:"\f3e0"}.fa-user-tag:before{content:"\f507"}.fa-rug:before{content:"\e569"}.fa-earth-europe:before,.fa-globe-europe:before{content:"\f7a2"}.fa-cart-flatbed-suitcase:before,.fa-luggage-cart:before{content:"\f59d"}.fa-rectangle-times:before,.fa-rectangle-xmark:before,.fa-times-rectangle:before,.fa-window-close:before{content:"\f410"}.fa-baht-sign:before{content:"\e0ac"}.fa-book-open:before{content:"\f518"}.fa-book-journal-whills:before,.fa-journal-whills:before{content:"\f66a"}.fa-handcuffs:before{content:"\e4f8"}.fa-exclamation-triangle:before,.fa-triangle-exclamation:before,.fa-warning:before{content:"\f071"}.fa-database:before{content:"\f1c0"}.fa-arrow-turn-right:before,.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-bottle-droplet:before{content:"\e4c4"}.fa-mask-face:before{content:"\e1d7"}.fa-hill-rockslide:before{content:"\e508"}.fa-exchange-alt:before,.fa-right-left:before{content:"\f362"}.fa-paper-plane:before{content:"\f1d8"}.fa-road-circle-exclamation:before{content:"\e565"}.fa-dungeon:before{content:"\f6d9"}.fa-align-right:before{content:"\f038"}.fa-money-bill-1-wave:before,.fa-money-bill-wave-alt:before{content:"\f53b"}.fa-life-ring:before{content:"\f1cd"}.fa-hands:before,.fa-sign-language:before,.fa-signing:before{content:"\f2a7"}.fa-calendar-day:before{content:"\f783"}.fa-ladder-water:before,.fa-swimming-pool:before,.fa-water-ladder:before{content:"\f5c5"}.fa-arrows-up-down:before,.fa-arrows-v:before{content:"\f07d"}.fa-face-grimace:before,.fa-grimace:before{content:"\f57f"}.fa-wheelchair-alt:before,.fa-wheelchair-move:before{content:"\e2ce"}.fa-level-down-alt:before,.fa-turn-down:before{content:"\f3be"}.fa-person-walking-arrow-right:before{content:"\e552"}.fa-envelope-square:before,.fa-square-envelope:before{content:"\f199"}.fa-dice:before{content:"\f522"}.fa-bowling-ball:before{content:"\f436"}.fa-brain:before{content:"\f5dc"}.fa-band-aid:before,.fa-bandage:before{content:"\f462"}.fa-calendar-minus:before{content:"\f272"}.fa-circle-xmark:before,.fa-times-circle:before,.fa-xmark-circle:before{content:"\f057"}.fa-gifts:before{content:"\f79c"}.fa-hotel:before{content:"\f594"}.fa-earth-asia:before,.fa-globe-asia:before{content:"\f57e"}.fa-id-card-alt:before,.fa-id-card-clip:before{content:"\f47f"}.fa-magnifying-glass-plus:before,.fa-search-plus:before{content:"\f00e"}.fa-thumbs-up:before{content:"\f164"}.fa-user-clock:before{content:"\f4fd"}.fa-allergies:before,.fa-hand-dots:before{content:"\f461"}.fa-file-invoice:before{content:"\f570"}.fa-window-minimize:before{content:"\f2d1"}.fa-coffee:before,.fa-mug-saucer:before{content:"\f0f4"}.fa-brush:before{content:"\f55d"}.fa-mask:before{content:"\f6fa"}.fa-magnifying-glass-minus:before,.fa-search-minus:before{content:"\f010"}.fa-ruler-vertical:before{content:"\f548"}.fa-user-alt:before,.fa-user-large:before{content:"\f406"}.fa-train-tram:before{content:"\e5b4"}.fa-user-nurse:before{content:"\f82f"}.fa-syringe:before{content:"\f48e"}.fa-cloud-sun:before{content:"\f6c4"}.fa-stopwatch-20:before{content:"\e06f"}.fa-square-full:before{content:"\f45c"}.fa-magnet:before{content:"\f076"}.fa-jar:before{content:"\e516"}.fa-note-sticky:before,.fa-sticky-note:before{content:"\f249"}.fa-bug-slash:before{content:"\e490"}.fa-arrow-up-from-water-pump:before{content:"\e4b6"}.fa-bone:before{content:"\f5d7"}.fa-user-injured:before{content:"\f728"}.fa-face-sad-tear:before,.fa-sad-tear:before{content:"\f5b4"}.fa-plane:before{content:"\f072"}.fa-tent-arrows-down:before{content:"\e581"}.fa-exclamation:before{content:"\21"}.fa-arrows-spin:before{content:"\e4bb"}.fa-print:before{content:"\f02f"}.fa-try:before,.fa-turkish-lira-sign:before,.fa-turkish-lira:before{content:"\e2bb"}.fa-dollar-sign:before,.fa-dollar:before,.fa-usd:before{content:"\24"}.fa-x:before{content:"\58"}.fa-magnifying-glass-dollar:before,.fa-search-dollar:before{content:"\f688"}.fa-users-cog:before,.fa-users-gear:before{content:"\f509"}.fa-person-military-pointing:before{content:"\e54a"}.fa-bank:before,.fa-building-columns:before,.fa-institution:before,.fa-museum:before,.fa-university:before{content:"\f19c"}.fa-umbrella:before{content:"\f0e9"}.fa-trowel:before{content:"\e589"}.fa-d:before{content:"\44"}.fa-stapler:before{content:"\e5af"}.fa-masks-theater:before,.fa-theater-masks:before{content:"\f630"}.fa-kip-sign:before{content:"\e1c4"}.fa-hand-point-left:before{content:"\f0a5"}.fa-handshake-alt:before,.fa-handshake-simple:before{content:"\f4c6"}.fa-fighter-jet:before,.fa-jet-fighter:before{content:"\f0fb"}.fa-share-alt-square:before,.fa-square-share-nodes:before{content:"\f1e1"}.fa-barcode:before{content:"\f02a"}.fa-plus-minus:before{content:"\e43c"}.fa-video-camera:before,.fa-video:before{content:"\f03d"}.fa-graduation-cap:before,.fa-mortar-board:before{content:"\f19d"}.fa-hand-holding-medical:before{content:"\e05c"}.fa-person-circle-check:before{content:"\e53e"}.fa-level-up-alt:before,.fa-turn-up:before{content:"\f3bf"}
+.fa-sr-only,.fa-sr-only-focusable:not(:focus),.sr-only,.sr-only-focusable:not(:focus){position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}:host,:root{--fa-style-family-brands:"Font Awesome 6 Brands";--fa-font-brands:normal 400 1em/1 "Font Awesome 6 Brands"}@font-face{font-family:"Font Awesome 6 Brands";font-style:normal;font-weight:400;font-display:block;src:url(./webfonts/fa-brands-400.woff2) format("woff2"),url(./webfonts/fa-brands-400.ttf) format("truetype")}.fa-brands,.fab{font-weight:400}.fa-monero:before{content:"\f3d0"}.fa-hooli:before{content:"\f427"}.fa-yelp:before{content:"\f1e9"}.fa-cc-visa:before{content:"\f1f0"}.fa-lastfm:before{content:"\f202"}.fa-shopware:before{content:"\f5b5"}.fa-creative-commons-nc:before{content:"\f4e8"}.fa-aws:before{content:"\f375"}.fa-redhat:before{content:"\f7bc"}.fa-yoast:before{content:"\f2b1"}.fa-cloudflare:before{content:"\e07d"}.fa-ups:before{content:"\f7e0"}.fa-wpexplorer:before{content:"\f2de"}.fa-dyalog:before{content:"\f399"}.fa-bity:before{content:"\f37a"}.fa-stackpath:before{content:"\f842"}.fa-buysellads:before{content:"\f20d"}.fa-first-order:before{content:"\f2b0"}.fa-modx:before{content:"\f285"}.fa-guilded:before{content:"\e07e"}.fa-vnv:before{content:"\f40b"}.fa-js-square:before,.fa-square-js:before{content:"\f3b9"}.fa-microsoft:before{content:"\f3ca"}.fa-qq:before{content:"\f1d6"}.fa-orcid:before{content:"\f8d2"}.fa-java:before{content:"\f4e4"}.fa-invision:before{content:"\f7b0"}.fa-creative-commons-pd-alt:before{content:"\f4ed"}.fa-centercode:before{content:"\f380"}.fa-glide-g:before{content:"\f2a6"}.fa-drupal:before{content:"\f1a9"}.fa-hire-a-helper:before{content:"\f3b0"}.fa-creative-commons-by:before{content:"\f4e7"}.fa-unity:before{content:"\e049"}.fa-whmcs:before{content:"\f40d"}.fa-rocketchat:before{content:"\f3e8"}.fa-vk:before{content:"\f189"}.fa-untappd:before{content:"\f405"}.fa-mailchimp:before{content:"\f59e"}.fa-css3-alt:before{content:"\f38b"}.fa-reddit-square:before,.fa-square-reddit:before{content:"\f1a2"}.fa-vimeo-v:before{content:"\f27d"}.fa-contao:before{content:"\f26d"}.fa-square-font-awesome:before{content:"\e5ad"}.fa-deskpro:before{content:"\f38f"}.fa-sistrix:before{content:"\f3ee"}.fa-instagram-square:before,.fa-square-instagram:before{content:"\e055"}.fa-battle-net:before{content:"\f835"}.fa-the-red-yeti:before{content:"\f69d"}.fa-hacker-news-square:before,.fa-square-hacker-news:before{content:"\f3af"}.fa-edge:before{content:"\f282"}.fa-threads:before{content:"\e618"}.fa-napster:before{content:"\f3d2"}.fa-snapchat-square:before,.fa-square-snapchat:before{content:"\f2ad"}.fa-google-plus-g:before{content:"\f0d5"}.fa-artstation:before{content:"\f77a"}.fa-markdown:before{content:"\f60f"}.fa-sourcetree:before{content:"\f7d3"}.fa-google-plus:before{content:"\f2b3"}.fa-diaspora:before{content:"\f791"}.fa-foursquare:before{content:"\f180"}.fa-stack-overflow:before{content:"\f16c"}.fa-github-alt:before{content:"\f113"}.fa-phoenix-squadron:before{content:"\f511"}.fa-pagelines:before{content:"\f18c"}.fa-algolia:before{content:"\f36c"}.fa-red-river:before{content:"\f3e3"}.fa-creative-commons-sa:before{content:"\f4ef"}.fa-safari:before{content:"\f267"}.fa-google:before{content:"\f1a0"}.fa-font-awesome-alt:before,.fa-square-font-awesome-stroke:before{content:"\f35c"}.fa-atlassian:before{content:"\f77b"}.fa-linkedin-in:before{content:"\f0e1"}.fa-digital-ocean:before{content:"\f391"}.fa-nimblr:before{content:"\f5a8"}.fa-chromecast:before{content:"\f838"}.fa-evernote:before{content:"\f839"}.fa-hacker-news:before{content:"\f1d4"}.fa-creative-commons-sampling:before{content:"\f4f0"}.fa-adversal:before{content:"\f36a"}.fa-creative-commons:before{content:"\f25e"}.fa-watchman-monitoring:before{content:"\e087"}.fa-fonticons:before{content:"\f280"}.fa-weixin:before{content:"\f1d7"}.fa-shirtsinbulk:before{content:"\f214"}.fa-codepen:before{content:"\f1cb"}.fa-git-alt:before{content:"\f841"}.fa-lyft:before{content:"\f3c3"}.fa-rev:before{content:"\f5b2"}.fa-windows:before{content:"\f17a"}.fa-wizards-of-the-coast:before{content:"\f730"}.fa-square-viadeo:before,.fa-viadeo-square:before{content:"\f2aa"}.fa-meetup:before{content:"\f2e0"}.fa-centos:before{content:"\f789"}.fa-adn:before{content:"\f170"}.fa-cloudsmith:before{content:"\f384"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-dribbble-square:before,.fa-square-dribbble:before{content:"\f397"}.fa-codiepie:before{content:"\f284"}.fa-node:before{content:"\f419"}.fa-mix:before{content:"\f3cb"}.fa-steam:before{content:"\f1b6"}.fa-cc-apple-pay:before{content:"\f416"}.fa-scribd:before{content:"\f28a"}.fa-debian:before{content:"\e60b"}.fa-openid:before{content:"\f19b"}.fa-instalod:before{content:"\e081"}.fa-expeditedssl:before{content:"\f23e"}.fa-sellcast:before{content:"\f2da"}.fa-square-twitter:before,.fa-twitter-square:before{content:"\f081"}.fa-r-project:before{content:"\f4f7"}.fa-delicious:before{content:"\f1a5"}.fa-freebsd:before{content:"\f3a4"}.fa-vuejs:before{content:"\f41f"}.fa-accusoft:before{content:"\f369"}.fa-ioxhost:before{content:"\f208"}.fa-fonticons-fi:before{content:"\f3a2"}.fa-app-store:before{content:"\f36f"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-itunes-note:before{content:"\f3b5"}.fa-golang:before{content:"\e40f"}.fa-kickstarter:before{content:"\f3bb"}.fa-grav:before{content:"\f2d6"}.fa-weibo:before{content:"\f18a"}.fa-uncharted:before{content:"\e084"}.fa-firstdraft:before{content:"\f3a1"}.fa-square-youtube:before,.fa-youtube-square:before{content:"\f431"}.fa-wikipedia-w:before{content:"\f266"}.fa-rendact:before,.fa-wpressr:before{content:"\f3e4"}.fa-angellist:before{content:"\f209"}.fa-galactic-republic:before{content:"\f50c"}.fa-nfc-directional:before{content:"\e530"}.fa-skype:before{content:"\f17e"}.fa-joget:before{content:"\f3b7"}.fa-fedora:before{content:"\f798"}.fa-stripe-s:before{content:"\f42a"}.fa-meta:before{content:"\e49b"}.fa-laravel:before{content:"\f3bd"}.fa-hotjar:before{content:"\f3b1"}.fa-bluetooth-b:before{content:"\f294"}.fa-sticker-mule:before{content:"\f3f7"}.fa-creative-commons-zero:before{content:"\f4f3"}.fa-hips:before{content:"\f452"}.fa-behance:before{content:"\f1b4"}.fa-reddit:before{content:"\f1a1"}.fa-discord:before{content:"\f392"}.fa-chrome:before{content:"\f268"}.fa-app-store-ios:before{content:"\f370"}.fa-cc-discover:before{content:"\f1f2"}.fa-wpbeginner:before{content:"\f297"}.fa-confluence:before{content:"\f78d"}.fa-mdb:before{content:"\f8ca"}.fa-dochub:before{content:"\f394"}.fa-accessible-icon:before{content:"\f368"}.fa-ebay:before{content:"\f4f4"}.fa-amazon:before{content:"\f270"}.fa-unsplash:before{content:"\e07c"}.fa-yarn:before{content:"\f7e3"}.fa-square-steam:before,.fa-steam-square:before{content:"\f1b7"}.fa-500px:before{content:"\f26e"}.fa-square-vimeo:before,.fa-vimeo-square:before{content:"\f194"}.fa-asymmetrik:before{content:"\f372"}.fa-font-awesome-flag:before,.fa-font-awesome-logo-full:before,.fa-font-awesome:before{content:"\f2b4"}.fa-gratipay:before{content:"\f184"}.fa-apple:before{content:"\f179"}.fa-hive:before{content:"\e07f"}.fa-gitkraken:before{content:"\f3a6"}.fa-keybase:before{content:"\f4f5"}.fa-apple-pay:before{content:"\f415"}.fa-padlet:before{content:"\e4a0"}.fa-amazon-pay:before{content:"\f42c"}.fa-github-square:before,.fa-square-github:before{content:"\f092"}.fa-stumbleupon:before{content:"\f1a4"}.fa-fedex:before{content:"\f797"}.fa-phoenix-framework:before{content:"\f3dc"}.fa-shopify:before{content:"\e057"}.fa-neos:before{content:"\f612"}.fa-square-threads:before{content:"\e619"}.fa-hackerrank:before{content:"\f5f7"}.fa-researchgate:before{content:"\f4f8"}.fa-swift:before{content:"\f8e1"}.fa-angular:before{content:"\f420"}.fa-speakap:before{content:"\f3f3"}.fa-angrycreative:before{content:"\f36e"}.fa-y-combinator:before{content:"\f23b"}.fa-empire:before{content:"\f1d1"}.fa-envira:before{content:"\f299"}.fa-gitlab-square:before,.fa-square-gitlab:before{content:"\e5ae"}.fa-studiovinari:before{content:"\f3f8"}.fa-pied-piper:before{content:"\f2ae"}.fa-wordpress:before{content:"\f19a"}.fa-product-hunt:before{content:"\f288"}.fa-firefox:before{content:"\f269"}.fa-linode:before{content:"\f2b8"}.fa-goodreads:before{content:"\f3a8"}.fa-odnoklassniki-square:before,.fa-square-odnoklassniki:before{content:"\f264"}.fa-jsfiddle:before{content:"\f1cc"}.fa-sith:before{content:"\f512"}.fa-themeisle:before{content:"\f2b2"}.fa-page4:before{content:"\f3d7"}.fa-hashnode:before{content:"\e499"}.fa-react:before{content:"\f41b"}.fa-cc-paypal:before{content:"\f1f4"}.fa-squarespace:before{content:"\f5be"}.fa-cc-stripe:before{content:"\f1f5"}.fa-creative-commons-share:before{content:"\f4f2"}.fa-bitcoin:before{content:"\f379"}.fa-keycdn:before{content:"\f3ba"}.fa-opera:before{content:"\f26a"}.fa-itch-io:before{content:"\f83a"}.fa-umbraco:before{content:"\f8e8"}.fa-galactic-senate:before{content:"\f50d"}.fa-ubuntu:before{content:"\f7df"}.fa-draft2digital:before{content:"\f396"}.fa-stripe:before{content:"\f429"}.fa-houzz:before{content:"\f27c"}.fa-gg:before{content:"\f260"}.fa-dhl:before{content:"\f790"}.fa-pinterest-square:before,.fa-square-pinterest:before{content:"\f0d3"}.fa-xing:before{content:"\f168"}.fa-blackberry:before{content:"\f37b"}.fa-creative-commons-pd:before{content:"\f4ec"}.fa-playstation:before{content:"\f3df"}.fa-quinscape:before{content:"\f459"}.fa-less:before{content:"\f41d"}.fa-blogger-b:before{content:"\f37d"}.fa-opencart:before{content:"\f23d"}.fa-vine:before{content:"\f1ca"}.fa-paypal:before{content:"\f1ed"}.fa-gitlab:before{content:"\f296"}.fa-typo3:before{content:"\f42b"}.fa-reddit-alien:before{content:"\f281"}.fa-yahoo:before{content:"\f19e"}.fa-dailymotion:before{content:"\e052"}.fa-affiliatetheme:before{content:"\f36b"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-bootstrap:before{content:"\f836"}.fa-odnoklassniki:before{content:"\f263"}.fa-nfc-symbol:before{content:"\e531"}.fa-ethereum:before{content:"\f42e"}.fa-speaker-deck:before{content:"\f83c"}.fa-creative-commons-nc-eu:before{content:"\f4e9"}.fa-patreon:before{content:"\f3d9"}.fa-avianex:before{content:"\f374"}.fa-ello:before{content:"\f5f1"}.fa-gofore:before{content:"\f3a7"}.fa-bimobject:before{content:"\f378"}.fa-facebook-f:before{content:"\f39e"}.fa-google-plus-square:before,.fa-square-google-plus:before{content:"\f0d4"}.fa-mandalorian:before{content:"\f50f"}.fa-first-order-alt:before{content:"\f50a"}.fa-osi:before{content:"\f41a"}.fa-google-wallet:before{content:"\f1ee"}.fa-d-and-d-beyond:before{content:"\f6ca"}.fa-periscope:before{content:"\f3da"}.fa-fulcrum:before{content:"\f50b"}.fa-cloudscale:before{content:"\f383"}.fa-forumbee:before{content:"\f211"}.fa-mizuni:before{content:"\f3cc"}.fa-schlix:before{content:"\f3ea"}.fa-square-xing:before,.fa-xing-square:before{content:"\f169"}.fa-bandcamp:before{content:"\f2d5"}.fa-wpforms:before{content:"\f298"}.fa-cloudversify:before{content:"\f385"}.fa-usps:before{content:"\f7e1"}.fa-megaport:before{content:"\f5a3"}.fa-magento:before{content:"\f3c4"}.fa-spotify:before{content:"\f1bc"}.fa-optin-monster:before{content:"\f23c"}.fa-fly:before{content:"\f417"}.fa-aviato:before{content:"\f421"}.fa-itunes:before{content:"\f3b4"}.fa-cuttlefish:before{content:"\f38c"}.fa-blogger:before{content:"\f37c"}.fa-flickr:before{content:"\f16e"}.fa-viber:before{content:"\f409"}.fa-soundcloud:before{content:"\f1be"}.fa-digg:before{content:"\f1a6"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-symfony:before{content:"\f83d"}.fa-maxcdn:before{content:"\f136"}.fa-etsy:before{content:"\f2d7"}.fa-facebook-messenger:before{content:"\f39f"}.fa-audible:before{content:"\f373"}.fa-think-peaks:before{content:"\f731"}.fa-bilibili:before{content:"\e3d9"}.fa-erlang:before{content:"\f39d"}.fa-x-twitter:before{content:"\e61b"}.fa-cotton-bureau:before{content:"\f89e"}.fa-dashcube:before{content:"\f210"}.fa-42-group:before,.fa-innosoft:before{content:"\e080"}.fa-stack-exchange:before{content:"\f18d"}.fa-elementor:before{content:"\f430"}.fa-pied-piper-square:before,.fa-square-pied-piper:before{content:"\e01e"}.fa-creative-commons-nd:before{content:"\f4eb"}.fa-palfed:before{content:"\f3d8"}.fa-superpowers:before{content:"\f2dd"}.fa-resolving:before{content:"\f3e7"}.fa-xbox:before{content:"\f412"}.fa-searchengin:before{content:"\f3eb"}.fa-tiktok:before{content:"\e07b"}.fa-facebook-square:before,.fa-square-facebook:before{content:"\f082"}.fa-renren:before{content:"\f18b"}.fa-linux:before{content:"\f17c"}.fa-glide:before{content:"\f2a5"}.fa-linkedin:before{content:"\f08c"}.fa-hubspot:before{content:"\f3b2"}.fa-deploydog:before{content:"\f38e"}.fa-twitch:before{content:"\f1e8"}.fa-ravelry:before{content:"\f2d9"}.fa-mixer:before{content:"\e056"}.fa-lastfm-square:before,.fa-square-lastfm:before{content:"\f203"}.fa-vimeo:before{content:"\f40a"}.fa-mendeley:before{content:"\f7b3"}.fa-uniregistry:before{content:"\f404"}.fa-figma:before{content:"\f799"}.fa-creative-commons-remix:before{content:"\f4ee"}.fa-cc-amazon-pay:before{content:"\f42d"}.fa-dropbox:before{content:"\f16b"}.fa-instagram:before{content:"\f16d"}.fa-cmplid:before{content:"\e360"}.fa-facebook:before{content:"\f09a"}.fa-gripfire:before{content:"\f3ac"}.fa-jedi-order:before{content:"\f50e"}.fa-uikit:before{content:"\f403"}.fa-fort-awesome-alt:before{content:"\f3a3"}.fa-phabricator:before{content:"\f3db"}.fa-ussunnah:before{content:"\f407"}.fa-earlybirds:before{content:"\f39a"}.fa-trade-federation:before{content:"\f513"}.fa-autoprefixer:before{content:"\f41c"}.fa-whatsapp:before{content:"\f232"}.fa-slideshare:before{content:"\f1e7"}.fa-google-play:before{content:"\f3ab"}.fa-viadeo:before{content:"\f2a9"}.fa-line:before{content:"\f3c0"}.fa-google-drive:before{content:"\f3aa"}.fa-servicestack:before{content:"\f3ec"}.fa-simplybuilt:before{content:"\f215"}.fa-bitbucket:before{content:"\f171"}.fa-imdb:before{content:"\f2d8"}.fa-deezer:before{content:"\e077"}.fa-raspberry-pi:before{content:"\f7bb"}.fa-jira:before{content:"\f7b1"}.fa-docker:before{content:"\f395"}.fa-screenpal:before{content:"\e570"}.fa-bluetooth:before{content:"\f293"}.fa-gitter:before{content:"\f426"}.fa-d-and-d:before{content:"\f38d"}.fa-microblog:before{content:"\e01a"}.fa-cc-diners-club:before{content:"\f24c"}.fa-gg-circle:before{content:"\f261"}.fa-pied-piper-hat:before{content:"\f4e5"}.fa-kickstarter-k:before{content:"\f3bc"}.fa-yandex:before{content:"\f413"}.fa-readme:before{content:"\f4d5"}.fa-html5:before{content:"\f13b"}.fa-sellsy:before{content:"\f213"}.fa-sass:before{content:"\f41e"}.fa-wirsindhandwerk:before,.fa-wsh:before{content:"\e2d0"}.fa-buromobelexperte:before{content:"\f37f"}.fa-salesforce:before{content:"\f83b"}.fa-octopus-deploy:before{content:"\e082"}.fa-medapps:before{content:"\f3c6"}.fa-ns8:before{content:"\f3d5"}.fa-pinterest-p:before{content:"\f231"}.fa-apper:before{content:"\f371"}.fa-fort-awesome:before{content:"\f286"}.fa-waze:before{content:"\f83f"}.fa-cc-jcb:before{content:"\f24b"}.fa-snapchat-ghost:before,.fa-snapchat:before{content:"\f2ab"}.fa-fantasy-flight-games:before{content:"\f6dc"}.fa-rust:before{content:"\e07a"}.fa-wix:before{content:"\f5cf"}.fa-behance-square:before,.fa-square-behance:before{content:"\f1b5"}.fa-supple:before{content:"\f3f9"}.fa-rebel:before{content:"\f1d0"}.fa-css3:before{content:"\f13c"}.fa-staylinked:before{content:"\f3f5"}.fa-kaggle:before{content:"\f5fa"}.fa-space-awesome:before{content:"\e5ac"}.fa-deviantart:before{content:"\f1bd"}.fa-cpanel:before{content:"\f388"}.fa-goodreads-g:before{content:"\f3a9"}.fa-git-square:before,.fa-square-git:before{content:"\f1d2"}.fa-square-tumblr:before,.fa-tumblr-square:before{content:"\f174"}.fa-trello:before{content:"\f181"}.fa-creative-commons-nc-jp:before{content:"\f4ea"}.fa-get-pocket:before{content:"\f265"}.fa-perbyte:before{content:"\e083"}.fa-grunt:before{content:"\f3ad"}.fa-weebly:before{content:"\f5cc"}.fa-connectdevelop:before{content:"\f20e"}.fa-leanpub:before{content:"\f212"}.fa-black-tie:before{content:"\f27e"}.fa-themeco:before{content:"\f5c6"}.fa-python:before{content:"\f3e2"}.fa-android:before{content:"\f17b"}.fa-bots:before{content:"\e340"}.fa-free-code-camp:before{content:"\f2c5"}.fa-hornbill:before{content:"\f592"}.fa-js:before{content:"\f3b8"}.fa-ideal:before{content:"\e013"}.fa-git:before{content:"\f1d3"}.fa-dev:before{content:"\f6cc"}.fa-sketch:before{content:"\f7c6"}.fa-yandex-international:before{content:"\f414"}.fa-cc-amex:before{content:"\f1f3"}.fa-uber:before{content:"\f402"}.fa-github:before{content:"\f09b"}.fa-php:before{content:"\f457"}.fa-alipay:before{content:"\f642"}.fa-youtube:before{content:"\f167"}.fa-skyatlas:before{content:"\f216"}.fa-firefox-browser:before{content:"\e007"}.fa-replyd:before{content:"\f3e6"}.fa-suse:before{content:"\f7d6"}.fa-jenkins:before{content:"\f3b6"}.fa-twitter:before{content:"\f099"}.fa-rockrms:before{content:"\f3e9"}.fa-pinterest:before{content:"\f0d2"}.fa-buffer:before{content:"\f837"}.fa-npm:before{content:"\f3d4"}.fa-yammer:before{content:"\f840"}.fa-btc:before{content:"\f15a"}.fa-dribbble:before{content:"\f17d"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-internet-explorer:before{content:"\f26b"}.fa-stubber:before{content:"\e5c7"}.fa-telegram-plane:before,.fa-telegram:before{content:"\f2c6"}.fa-old-republic:before{content:"\f510"}.fa-odysee:before{content:"\e5c6"}.fa-square-whatsapp:before,.fa-whatsapp-square:before{content:"\f40c"}.fa-node-js:before{content:"\f3d3"}.fa-edge-legacy:before{content:"\e078"}.fa-slack-hash:before,.fa-slack:before{content:"\f198"}.fa-medrt:before{content:"\f3c8"}.fa-usb:before{content:"\f287"}.fa-tumblr:before{content:"\f173"}.fa-vaadin:before{content:"\f408"}.fa-quora:before{content:"\f2c4"}.fa-square-x-twitter:before{content:"\e61a"}.fa-reacteurope:before{content:"\f75d"}.fa-medium-m:before,.fa-medium:before{content:"\f23a"}.fa-amilia:before{content:"\f36d"}.fa-mixcloud:before{content:"\f289"}.fa-flipboard:before{content:"\f44d"}.fa-viacoin:before{content:"\f237"}.fa-critical-role:before{content:"\f6c9"}.fa-sitrox:before{content:"\e44a"}.fa-discourse:before{content:"\f393"}.fa-joomla:before{content:"\f1aa"}.fa-mastodon:before{content:"\f4f6"}.fa-airbnb:before{content:"\f834"}.fa-wolf-pack-battalion:before{content:"\f514"}.fa-buy-n-large:before{content:"\f8a6"}.fa-gulp:before{content:"\f3ae"}.fa-creative-commons-sampling-plus:before{content:"\f4f1"}.fa-strava:before{content:"\f428"}.fa-ember:before{content:"\f423"}.fa-canadian-maple-leaf:before{content:"\f785"}.fa-teamspeak:before{content:"\f4f9"}.fa-pushed:before{content:"\f3e1"}.fa-wordpress-simple:before{content:"\f411"}.fa-nutritionix:before{content:"\f3d6"}.fa-wodu:before{content:"\e088"}.fa-google-pay:before{content:"\e079"}.fa-intercom:before{content:"\f7af"}.fa-zhihu:before{content:"\f63f"}.fa-korvue:before{content:"\f42f"}.fa-pix:before{content:"\e43a"}.fa-steam-symbol:before{content:"\f3f6"}:host,:root{--fa-font-regular:normal 400 1em/1 "Font Awesome 6 Free"}@font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:400;font-display:block;src:url(./webfonts/fa-regular-400.woff2) format("woff2"),url(./webfonts/fa-regular-400.ttf) format("truetype")}.fa-regular,.far{font-weight:400}:host,:root{--fa-style-family-classic:"Font Awesome 6 Free";--fa-font-solid:normal 900 1em/1 "Font Awesome 6 Free"}@font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:900;font-display:block;src:url(./webfonts/fa-solid-900.woff2) format("woff2"),url(./webfonts/fa-solid-900.ttf) format("truetype")}.fa-solid,.fas{font-weight:900}@font-face{font-family:"Font Awesome 5 Brands";font-display:block;font-weight:400;src:url(./webfonts/fa-brands-400.woff2) format("woff2"),url(./webfonts/fa-brands-400.ttf) format("truetype")}@font-face{font-family:"Font Awesome 5 Free";font-display:block;font-weight:900;src:url(./webfonts/fa-solid-900.woff2) format("woff2"),url(./webfonts/fa-solid-900.ttf) format("truetype")}@font-face{font-family:"Font Awesome 5 Free";font-display:block;font-weight:400;src:url(./webfonts/fa-regular-400.woff2) format("woff2"),url(./webfonts/fa-regular-400.ttf) format("truetype")}@font-face{font-family:"FontAwesome";font-display:block;src:url(./webfonts/fa-solid-900.woff2) format("woff2"),url(./webfonts/fa-solid-900.ttf) format("truetype")}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.ttf) format("truetype")}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype");unicode-range:u+f003,u+f006,u+f014,u+f016-f017,u+f01a-f01b,u+f01d,u+f022,u+f03e,u+f044,u+f046,u+f05c-f05d,u+f06e,u+f070,u+f087-f088,u+f08a,u+f094,u+f096-f097,u+f09d,u+f0a0,u+f0a2,u+f0a4-f0a7,u+f0c5,u+f0c7,u+f0e5-f0e6,u+f0eb,u+f0f6-f0f8,u+f10c,u+f114-f115,u+f118-f11a,u+f11c-f11d,u+f133,u+f147,u+f14e,u+f150-f152,u+f185-f186,u+f18e,u+f190-f192,u+f196,u+f1c1-f1c9,u+f1d9,u+f1db,u+f1e3,u+f1ea,u+f1f7,u+f1f9,u+f20a,u+f247-f248,u+f24a,u+f24d,u+f255-f25b,u+f25d,u+f271-f274,u+f278,u+f27b,u+f28c,u+f28e,u+f29c,u+f2b5,u+f2b7,u+f2ba,u+f2bc,u+f2be,u+f2c0-f2c1,u+f2c3,u+f2d0,u+f2d2,u+f2d4,u+f2dc}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-v4compatibility.woff2) format("woff2"),url(../webfonts/fa-v4compatibility.ttf) format("truetype");unicode-range:u+f041,u+f047,u+f065-f066,u+f07d-f07e,u+f080,u+f08b,u+f08e,u+f090,u+f09a,u+f0ac,u+f0ae,u+f0b2,u+f0d0,u+f0d6,u+f0e4,u+f0ec,u+f10a-f10b,u+f123,u+f13e,u+f148-f149,u+f14c,u+f156,u+f15e,u+f160-f161,u+f163,u+f175-f178,u+f195,u+f1f8,u+f219,u+f27a}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Backend/admin/frontend/assets/css/local_lib/webfonts/fa-brands-400.woff2 b/Backend/admin/frontend/assets/css/local_lib/webfonts/fa-brands-400.woff2
new file mode 100644
index 0000000..8a480d9
Binary files /dev/null and b/Backend/admin/frontend/assets/css/local_lib/webfonts/fa-brands-400.woff2 differ
diff --git a/Backend/admin/frontend/assets/css/local_lib/webfonts/fa-regular-400.woff2 b/Backend/admin/frontend/assets/css/local_lib/webfonts/fa-regular-400.woff2
new file mode 100644
index 0000000..059a94e
Binary files /dev/null and b/Backend/admin/frontend/assets/css/local_lib/webfonts/fa-regular-400.woff2 differ
diff --git a/Backend/admin/frontend/assets/css/local_lib/webfonts/fa-solid-900.woff2 b/Backend/admin/frontend/assets/css/local_lib/webfonts/fa-solid-900.woff2
new file mode 100644
index 0000000..88b0367
Binary files /dev/null and b/Backend/admin/frontend/assets/css/local_lib/webfonts/fa-solid-900.woff2 differ
diff --git a/Backend/admin/frontend/assets/css/main.css b/Backend/admin/frontend/assets/css/main.css
new file mode 100644
index 0000000..2d062a2
--- /dev/null
+++ b/Backend/admin/frontend/assets/css/main.css
@@ -0,0 +1,26 @@
+/* ============================================
+ vServer Admin Panel - Main CSS
+ Профессиональная модульная архитектура
+ ============================================ */
+
+/* 1. Variables & Base */
+@import 'variables.css';
+@import 'base.css';
+
+/* 2. Components */
+@import 'components/buttons.css';
+@import 'components/badges.css';
+@import 'components/cards.css';
+@import 'components/forms.css';
+@import 'components/tables.css';
+@import 'components/modals.css';
+
+/* 3. Layout */
+@import 'layout/header.css';
+@import 'layout/sidebar.css';
+@import 'layout/container.css';
+
+/* 4. Pages */
+@import 'pages/dashboard.css';
+@import 'pages/vaccess.css';
+
diff --git a/Backend/admin/frontend/assets/css/pages/dashboard.css b/Backend/admin/frontend/assets/css/pages/dashboard.css
new file mode 100644
index 0000000..a96ecbf
--- /dev/null
+++ b/Backend/admin/frontend/assets/css/pages/dashboard.css
@@ -0,0 +1,62 @@
+/* ============================================
+ Dashboard Page
+ Главная страница с сервисами и таблицами
+ ============================================ */
+
+/* Services Grid */
+.services-grid {
+ display: grid;
+ grid-template-columns: repeat(5, 1fr);
+ gap: var(--space-lg);
+}
+
+/* Settings Header */
+.settings-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: var(--space-lg);
+
+ .section-title {
+ margin-bottom: 0;
+ padding-bottom: 0;
+ }
+}
+
+/* Settings Grid */
+.settings-grid {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: var(--space-lg);
+ margin-bottom: var(--space-lg);
+}
+
+/* Settings Actions */
+.settings-actions {
+ display: flex;
+ justify-content: flex-end;
+}
+
+/* Responsive Grid */
+@media (max-width: 1200px) {
+ .services-grid {
+ grid-template-columns: repeat(3, 1fr);
+ }
+}
+
+@media (max-width: 900px) {
+ .services-grid {
+ grid-template-columns: repeat(2, 1fr);
+ }
+}
+
+@media (max-width: 600px) {
+ .services-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .settings-grid {
+ grid-template-columns: 1fr;
+ }
+}
+
diff --git a/Backend/admin/frontend/assets/css/pages/vaccess.css b/Backend/admin/frontend/assets/css/pages/vaccess.css
new file mode 100644
index 0000000..f40c582
--- /dev/null
+++ b/Backend/admin/frontend/assets/css/pages/vaccess.css
@@ -0,0 +1,264 @@
+/* ============================================
+ vAccess Editor Page
+ Страница редактора правил доступа
+ ============================================ */
+
+.vaccess-page {
+ animation: fadeIn var(--transition-slow);
+}
+
+/* Breadcrumbs */
+.breadcrumbs {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: var(--space-lg);
+ margin-bottom: var(--space-md);
+ padding: var(--space-md) 20px;
+ background: rgba(139, 92, 246, 0.05);
+ border-radius: var(--radius-lg);
+ border: 1px solid var(--glass-border);
+}
+
+.breadcrumbs-left {
+ display: flex;
+ align-items: center;
+ gap: var(--space-sm);
+}
+
+.breadcrumbs-tabs {
+ display: flex;
+ align-items: center;
+ gap: var(--space-sm);
+}
+
+.breadcrumb-separator {
+ color: var(--text-muted);
+ opacity: 0.3;
+}
+
+/* vAccess Header */
+.vaccess-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: var(--space-md);
+ padding: var(--space-lg);
+ background: rgba(139, 92, 246, 0.03);
+ border-radius: var(--radius-xl);
+ border: 1px solid var(--glass-border);
+}
+
+.vaccess-title-block {
+ flex: 1;
+}
+
+.vaccess-actions {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-sm);
+ min-width: 200px;
+
+ .action-btn {
+ width: 100%;
+ justify-content: center;
+ padding: 10px var(--space-md);
+ font-size: var(--text-base);
+ }
+}
+
+.vaccess-title {
+ font-size: var(--text-3xl);
+ font-weight: var(--font-semibold);
+ color: var(--text-primary);
+ margin: 0 0 var(--space-sm) 0;
+ display: flex;
+ align-items: center;
+ gap: var(--space-lg);
+
+ i {
+ color: var(--accent-purple-light);
+ font-size: 24px;
+ }
+}
+
+.vaccess-subtitle {
+ font-size: var(--text-md);
+ color: var(--text-muted);
+ margin: 0;
+}
+
+/* vAccess Tab Content */
+.vaccess-tab-content {
+ animation: fadeIn var(--transition-slow);
+}
+
+/* vAccess Rules Container */
+.vaccess-rules-container {
+ /* Контейнер без padding чтобы таблица была на всю ширину */
+ width: 100%;
+}
+
+/* vAccess Rules List */
+.vaccess-rules-list {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+ margin-bottom: 20px;
+ max-height: 55vh;
+ overflow-y: auto;
+ padding-right: var(--space-sm);
+}
+
+/* Empty State */
+.vaccess-empty {
+ text-align: center;
+ padding: 80px 40px;
+ color: var(--text-muted);
+}
+
+.empty-icon {
+ font-size: 64px;
+ margin-bottom: var(--space-lg);
+ opacity: 0.3;
+}
+
+.vaccess-empty h3 {
+ font-size: var(--text-2xl);
+ font-weight: var(--font-semibold);
+ color: var(--text-primary);
+ margin-bottom: 12px;
+}
+
+.vaccess-empty p {
+ font-size: var(--text-md);
+ margin-bottom: var(--space-lg);
+}
+
+/* vAccess Help */
+.vaccess-help {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-lg);
+}
+
+/* Help Parameters */
+.help-params {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-lg);
+}
+
+.help-param {
+ padding: 20px;
+ background: rgba(139, 92, 246, 0.03);
+ border-radius: var(--radius-lg);
+ border-left: 3px solid var(--accent-purple);
+
+ strong {
+ display: block;
+ font-size: 15px;
+ color: var(--accent-purple-light);
+ margin-bottom: var(--space-sm);
+ }
+
+ p {
+ margin: var(--space-sm) 0 0 0;
+ color: var(--text-secondary);
+ line-height: 1.6;
+ }
+
+ ul {
+ margin: 12px 0 0 20px;
+ }
+
+ code {
+ padding: 3px 8px;
+ background: rgba(139, 92, 246, 0.15);
+ border-radius: var(--radius-sm);
+ font-size: var(--text-base);
+ color: var(--accent-purple-light);
+ }
+}
+
+.help-warning {
+ color: rgba(251, 191, 36, 0.9) !important;
+ margin-top: var(--space-sm);
+ font-size: var(--text-base);
+ display: flex;
+ align-items: center;
+ gap: var(--space-sm);
+}
+
+/* Help Patterns */
+.help-patterns {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ gap: var(--space-md);
+}
+
+.pattern-item {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-sm);
+ padding: var(--space-md);
+ background: rgba(139, 92, 246, 0.05);
+ border-radius: 10px;
+ transition: all var(--transition-base);
+
+ &:hover {
+ background: rgba(139, 92, 246, 0.1);
+ }
+
+ code {
+ font-size: var(--text-md);
+ font-weight: var(--font-semibold);
+ color: var(--accent-purple-light);
+ }
+
+ span {
+ font-size: var(--text-base);
+ color: var(--text-muted);
+ }
+}
+
+/* Help Examples */
+.help-example {
+ margin-bottom: var(--space-xl);
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+
+ h4 {
+ font-size: var(--text-lg);
+ font-weight: var(--font-semibold);
+ color: var(--text-primary);
+ margin-bottom: var(--space-md);
+ }
+}
+
+.example-rule {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+ gap: 12px;
+ padding: 20px;
+ background: rgba(255, 255, 255, 0.02);
+ border-radius: 10px;
+ border: 1px solid var(--glass-border);
+
+ div {
+ font-size: var(--text-md);
+ color: var(--text-secondary);
+ }
+
+ strong {
+ color: var(--text-muted);
+ margin-right: var(--space-sm);
+ }
+
+ code {
+ color: var(--accent-purple-light);
+ }
+}
+
diff --git a/Backend/admin/frontend/assets/css/variables.css b/Backend/admin/frontend/assets/css/variables.css
new file mode 100644
index 0000000..b4b6836
--- /dev/null
+++ b/Backend/admin/frontend/assets/css/variables.css
@@ -0,0 +1,100 @@
+/* ============================================
+ CSS Custom Properties (Design Tokens)
+ Профессиональная система переменных
+ ============================================ */
+
+:root {
+ /* Colors - Background */
+ --bg-primary: #0b101f;
+ --bg-secondary: #121420;
+ --bg-tertiary: #0d0f1c;
+
+ /* Colors - Glass Effect */
+ --glass-bg: rgba(20, 20, 40, 0.4);
+ --glass-bg-light: rgba(20, 20, 40, 0.3);
+ --glass-bg-dark: rgba(10, 14, 26, 0.5);
+ --glass-border: rgba(139, 92, 246, 0.15);
+ --glass-border-hover: rgba(139, 92, 246, 0.3);
+
+ /* Colors - Accent */
+ --accent-blue: #5b21b6;
+ --accent-blue-light: #7c3aed;
+ --accent-purple: #8b5cf6;
+ --accent-purple-light: #a78bfa;
+ --accent-cyan: #06b6d4;
+ --accent-green: #10b981;
+ --accent-red: #ef4444;
+ --accent-yellow: #f59e0b;
+
+ /* Colors - Text */
+ --text-primary: #e2e8f0;
+ --text-secondary: #94a3b8;
+ --text-muted: #64748b;
+
+ /* Shadows */
+ --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.3);
+ --shadow-md: 0 8px 32px rgba(0, 0, 0, 0.5);
+ --shadow-lg: 0 12px 40px rgba(0, 0, 0, 0.7);
+ --shadow-purple: 0 8px 32px rgba(139, 92, 246, 0.3);
+ --shadow-green: 0 4px 12px rgba(16, 185, 129, 0.3);
+ --shadow-red: 0 4px 12px rgba(239, 68, 68, 0.3);
+
+ /* Spacing */
+ --space-xs: 4px;
+ --space-sm: 8px;
+ --space-md: 16px;
+ --space-lg: 24px;
+ --space-xl: 32px;
+ --space-2xl: 48px;
+ --space-3xl: 60px;
+
+ /* Border Radius */
+ --radius-sm: 6px;
+ --radius-md: 8px;
+ --radius-lg: 12px;
+ --radius-xl: 16px;
+ --radius-2xl: 10px;
+ --radius-full: 50%;
+
+ /* Transitions */
+ --transition-fast: 0.15s ease;
+ --transition-base: 0.2s ease;
+ --transition-slow: 0.3s ease;
+ --transition-bounce: 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+
+ /* Typography */
+ --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
+ --font-mono: 'Consolas', 'Courier New', monospace;
+
+ /* Font Sizes */
+ --text-xs: 10px;
+ --text-sm: 11px;
+ --text-base: 13px;
+ --text-md: 14px;
+ --text-lg: 16px;
+ --text-xl: 18px;
+ --text-2xl: 20px;
+ --text-3xl: 28px;
+
+ /* Font Weights */
+ --font-normal: 400;
+ --font-medium: 500;
+ --font-semibold: 600;
+ --font-bold: 700;
+
+ /* Z-Index Scale */
+ --z-base: 1;
+ --z-dropdown: 100;
+ --z-modal: 9998;
+ --z-notification: 9999;
+ --z-loader: 10000;
+
+ /* Layout */
+ --header-height: 60px;
+ --sidebar-width: 80px;
+
+ /* Backdrop Filter */
+ --backdrop-blur: blur(20px) saturate(180%);
+ --backdrop-blur-light: blur(10px);
+}
+
diff --git a/Backend/admin/frontend/assets/js/api/config.js b/Backend/admin/frontend/assets/js/api/config.js
new file mode 100644
index 0000000..5a12a46
--- /dev/null
+++ b/Backend/admin/frontend/assets/js/api/config.js
@@ -0,0 +1,129 @@
+/* ============================================
+ Config API
+ Работа с конфигурацией
+ ============================================ */
+
+import { isWailsAvailable, log } from '../utils/helpers.js';
+
+/**
+ * Класс для работы с конфигурацией
+ */
+class ConfigAPI {
+ constructor() {
+ this.available = isWailsAvailable();
+ }
+
+ /**
+ * Получить конфигурацию
+ */
+ async getConfig() {
+ if (!this.available) return null;
+ try {
+ return await window.go.admin.App.GetConfig();
+ } catch (error) {
+ log(`Ошибка получения конфигурации: ${error.message}`, 'error');
+ return null;
+ }
+ }
+
+ /**
+ * Сохранить конфигурацию
+ */
+ async saveConfig(configJSON) {
+ if (!this.available) return 'Error: API недоступен';
+ try {
+ return await window.go.admin.App.SaveConfig(configJSON);
+ } catch (error) {
+ log(`Ошибка сохранения конфигурации: ${error.message}`, 'error');
+ return `Error: ${error.message}`;
+ }
+ }
+
+ /**
+ * Включить Proxy Service
+ */
+ async enableProxyService() {
+ if (!this.available) return;
+ try {
+ await window.go.admin.App.EnableProxyService();
+ } catch (error) {
+ log(`Ошибка включения Proxy: ${error.message}`, 'error');
+ }
+ }
+
+ /**
+ * Отключить Proxy Service
+ */
+ async disableProxyService() {
+ if (!this.available) return;
+ try {
+ await window.go.admin.App.DisableProxyService();
+ } catch (error) {
+ log(`Ошибка отключения Proxy: ${error.message}`, 'error');
+ }
+ }
+
+ /**
+ * Перезапустить все сервисы
+ */
+ async restartAllServices() {
+ if (!this.available) return;
+ try {
+ await window.go.admin.App.RestartAllServices();
+ } catch (error) {
+ log(`Ошибка перезапуска сервисов: ${error.message}`, 'error');
+ }
+ }
+
+ /**
+ * Запустить HTTP Service
+ */
+ async startHTTPService() {
+ if (!this.available) return;
+ try {
+ await window.go.admin.App.StartHTTPService();
+ } catch (error) {
+ log(`Ошибка запуска HTTP: ${error.message}`, 'error');
+ }
+ }
+
+ /**
+ * Остановить HTTP Service
+ */
+ async stopHTTPService() {
+ if (!this.available) return;
+ try {
+ await window.go.admin.App.StopHTTPService();
+ } catch (error) {
+ log(`Ошибка остановки HTTP: ${error.message}`, 'error');
+ }
+ }
+
+ /**
+ * Запустить HTTPS Service
+ */
+ async startHTTPSService() {
+ if (!this.available) return;
+ try {
+ await window.go.admin.App.StartHTTPSService();
+ } catch (error) {
+ log(`Ошибка запуска HTTPS: ${error.message}`, 'error');
+ }
+ }
+
+ /**
+ * Остановить HTTPS Service
+ */
+ async stopHTTPSService() {
+ if (!this.available) return;
+ try {
+ await window.go.admin.App.StopHTTPSService();
+ } catch (error) {
+ log(`Ошибка остановки HTTPS: ${error.message}`, 'error');
+ }
+ }
+}
+
+// Экспортируем единственный экземпляр
+export const configAPI = new ConfigAPI();
+
diff --git a/Backend/admin/frontend/assets/js/api/wails.js b/Backend/admin/frontend/assets/js/api/wails.js
new file mode 100644
index 0000000..4e98181
--- /dev/null
+++ b/Backend/admin/frontend/assets/js/api/wails.js
@@ -0,0 +1,143 @@
+/* ============================================
+ Wails API Wrapper
+ Обёртка над Wails API
+ ============================================ */
+
+import { isWailsAvailable, log } from '../utils/helpers.js';
+
+/**
+ * Базовый класс для работы с Wails API
+ */
+class WailsAPI {
+ constructor() {
+ this.available = isWailsAvailable();
+ }
+
+ /**
+ * Проверка доступности API
+ */
+ checkAvailability() {
+ if (!this.available) {
+ log('Wails API недоступен', 'warn');
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Получить статус всех сервисов
+ */
+ async getAllServicesStatus() {
+ if (!this.checkAvailability()) return null;
+ try {
+ return await window.go.admin.App.GetAllServicesStatus();
+ } catch (error) {
+ log(`Ошибка получения статуса сервисов: ${error.message}`, 'error');
+ return null;
+ }
+ }
+
+ /**
+ * Получить список сайтов
+ */
+ async getSitesList() {
+ if (!this.checkAvailability()) return [];
+ try {
+ return await window.go.admin.App.GetSitesList();
+ } catch (error) {
+ log(`Ошибка получения списка сайтов: ${error.message}`, 'error');
+ return [];
+ }
+ }
+
+ /**
+ * Получить список прокси
+ */
+ async getProxyList() {
+ if (!this.checkAvailability()) return [];
+ try {
+ return await window.go.admin.App.GetProxyList();
+ } catch (error) {
+ log(`Ошибка получения списка прокси: ${error.message}`, 'error');
+ return [];
+ }
+ }
+
+ /**
+ * Получить правила vAccess
+ */
+ async getVAccessRules(host, isProxy) {
+ if (!this.checkAvailability()) return { rules: [] };
+ try {
+ return await window.go.admin.App.GetVAccessRules(host, isProxy);
+ } catch (error) {
+ log(`Ошибка получения правил vAccess: ${error.message}`, 'error');
+ return { rules: [] };
+ }
+ }
+
+ /**
+ * Сохранить правила vAccess
+ */
+ async saveVAccessRules(host, isProxy, configJSON) {
+ if (!this.checkAvailability()) return 'Error: API недоступен';
+ try {
+ return await window.go.admin.App.SaveVAccessRules(host, isProxy, configJSON);
+ } catch (error) {
+ log(`Ошибка сохранения правил vAccess: ${error.message}`, 'error');
+ return `Error: ${error.message}`;
+ }
+ }
+
+ /**
+ * Запустить сервер
+ */
+ async startServer() {
+ if (!this.checkAvailability()) return;
+ try {
+ await window.go.admin.App.StartServer();
+ } catch (error) {
+ log(`Ошибка запуска сервера: ${error.message}`, 'error');
+ }
+ }
+
+ /**
+ * Остановить сервер
+ */
+ async stopServer() {
+ if (!this.checkAvailability()) return;
+ try {
+ await window.go.admin.App.StopServer();
+ } catch (error) {
+ log(`Ошибка остановки сервера: ${error.message}`, 'error');
+ }
+ }
+
+ /**
+ * Проверить готовность сервисов
+ */
+ async checkServicesReady() {
+ if (!this.checkAvailability()) return false;
+ try {
+ return await window.go.admin.App.CheckServicesReady();
+ } catch (error) {
+ return false;
+ }
+ }
+
+ /**
+ * Открыть папку сайта
+ */
+ async openSiteFolder(host) {
+ if (!this.checkAvailability()) return;
+ try {
+ await window.go.admin.App.OpenSiteFolder(host);
+ } catch (error) {
+ log(`Ошибка открытия папки: ${error.message}`, 'error');
+ }
+ }
+}
+
+// Экспортируем единственный экземпляр
+export const api = new WailsAPI();
+
diff --git a/Backend/admin/frontend/assets/js/components/proxy.js b/Backend/admin/frontend/assets/js/components/proxy.js
new file mode 100644
index 0000000..e92fee3
--- /dev/null
+++ b/Backend/admin/frontend/assets/js/components/proxy.js
@@ -0,0 +1,151 @@
+/* ============================================
+ Proxy Component
+ Управление прокси
+ ============================================ */
+
+import { api } from '../api/wails.js';
+import { isWailsAvailable } from '../utils/helpers.js';
+import { $ } from '../utils/dom.js';
+
+/**
+ * Класс для управления прокси
+ */
+export class ProxyManager {
+ constructor() {
+ this.proxiesData = [];
+ this.mockData = [
+ {
+ enable: true,
+ external_domain: 'git.example.ru',
+ local_address: '127.0.0.1',
+ local_port: '3333',
+ service_https_use: false,
+ auto_https: true,
+ status: 'active'
+ },
+ {
+ enable: true,
+ external_domain: 'api.example.com',
+ local_address: '127.0.0.1',
+ local_port: '8080',
+ service_https_use: true,
+ auto_https: false,
+ status: 'active'
+ },
+ {
+ enable: false,
+ external_domain: 'test.example.net',
+ local_address: '127.0.0.1',
+ local_port: '5000',
+ service_https_use: false,
+ auto_https: false,
+ status: 'disabled'
+ }
+ ];
+ }
+
+ /**
+ * Загрузить список прокси
+ */
+ async load() {
+ if (isWailsAvailable()) {
+ this.proxiesData = await api.getProxyList();
+ } else {
+ // Используем тестовые данные если Wails недоступен
+ this.proxiesData = this.mockData;
+ }
+ this.render();
+ }
+
+ /**
+ * Отрисовать список прокси
+ */
+ render() {
+ const tbody = $('proxyTable')?.querySelector('tbody');
+ if (!tbody) return;
+
+ tbody.innerHTML = '';
+
+ this.proxiesData.forEach((proxy, index) => {
+ const row = document.createElement('tr');
+ const statusBadge = proxy.status === 'active' ? 'badge-online' : 'badge-offline';
+ const httpsBadge = proxy.service_https_use ? 'badge-yes">HTTPS' : 'badge-no">HTTP';
+ const autoHttpsBadge = proxy.auto_https ? 'badge-yes">Да' : 'badge-no">Нет';
+ const protocol = proxy.auto_https ? 'https' : 'http';
+
+ row.innerHTML = `
+
${proxy.external_domain} |
+ ${proxy.local_address}:${proxy.local_port} |
+ |
+ ${proxy.status} |
+
+
+
+ |
+ `;
+
+ tbody.appendChild(row);
+ });
+
+ // Добавляем обработчики событий
+ this.attachEventListeners();
+ }
+
+ /**
+ * Добавить обработчики событий
+ */
+ attachEventListeners() {
+ // Кликабельные ссылки
+ const links = document.querySelectorAll('.clickable-link[data-url]');
+ links.forEach(link => {
+ link.addEventListener('click', () => {
+ const url = link.getAttribute('data-url');
+ this.openLink(url);
+ });
+ });
+
+ // Кнопки действий
+ const buttons = document.querySelectorAll('[data-action]');
+ buttons.forEach(btn => {
+ btn.addEventListener('click', () => {
+ const action = btn.getAttribute('data-action');
+ this.handleAction(action, btn);
+ });
+ });
+ }
+
+ /**
+ * Обработчик действий
+ */
+ handleAction(action, btn) {
+ const host = btn.getAttribute('data-host');
+ const index = parseInt(btn.getAttribute('data-index'));
+ const isProxy = btn.getAttribute('data-is-proxy') === 'true';
+
+ switch (action) {
+ case 'edit-vaccess':
+ if (window.editVAccess) {
+ window.editVAccess(host, isProxy);
+ }
+ break;
+ case 'edit-proxy':
+ if (window.editProxy) {
+ window.editProxy(index);
+ }
+ break;
+ }
+ }
+
+ /**
+ * Открыть ссылку
+ */
+ openLink(url) {
+ if (window.runtime?.BrowserOpenURL) {
+ window.runtime.BrowserOpenURL(url);
+ } else {
+ window.open(url, '_blank');
+ }
+ }
+}
+
diff --git a/Backend/admin/frontend/assets/js/components/services.js b/Backend/admin/frontend/assets/js/components/services.js
new file mode 100644
index 0000000..4afd517
--- /dev/null
+++ b/Backend/admin/frontend/assets/js/components/services.js
@@ -0,0 +1,191 @@
+/* ============================================
+ Services Component
+ Управление сервисами
+ ============================================ */
+
+import { api } from '../api/wails.js';
+import { $, $$, addClass, removeClass } from '../utils/dom.js';
+import { notification } from '../ui/notification.js';
+import { sleep, isWailsAvailable } from '../utils/helpers.js';
+
+/**
+ * Класс для управления сервисами
+ */
+export class ServicesManager {
+ constructor() {
+ this.serverRunning = true;
+ this.isOperating = false;
+ this.controlBtn = $('serverControlBtn');
+ this.statusIndicator = document.querySelector('.status-indicator');
+ this.statusText = document.querySelector('.status-text');
+ this.btnText = document.querySelector('.btn-text');
+ this.init();
+ }
+
+ init() {
+ if (this.controlBtn) {
+ this.controlBtn.addEventListener('click', () => this.toggleServer());
+ }
+
+ // Подписка на события
+ if (window.runtime?.EventsOn) {
+ window.runtime.EventsOn('service:changed', (status) => {
+ this.renderServices(status);
+ });
+
+ window.runtime.EventsOn('server:already_running', () => {
+ notification.error('vServer уже запущен!
Закройте другой экземпляр перед запуском нового.', 5000);
+ this.setServerStatus(false, 'Уже запущен в другом процессе');
+ });
+ }
+ }
+
+ /**
+ * Переключить состояние сервера
+ */
+ async toggleServer() {
+ if (this.serverRunning) {
+ await this.stopServer();
+ } else {
+ await this.startServer();
+ }
+ }
+
+ /**
+ * Запустить сервер
+ */
+ async startServer() {
+ this.isOperating = true;
+ this.controlBtn.disabled = true;
+ this.statusText.textContent = 'Запускается...';
+ this.btnText.textContent = 'Ожидайте...';
+ this.setAllServicesPending('Запуск');
+
+ await api.startServer();
+
+ // Ждём пока все запустятся
+ let attempts = 0;
+ while (attempts < 20) {
+ await sleep(500);
+ if (await api.checkServicesReady()) {
+ break;
+ }
+ attempts++;
+ }
+
+ this.isOperating = false;
+ this.setServerStatus(true, 'Сервер запущен');
+ removeClass(this.controlBtn, 'start-mode');
+ this.btnText.textContent = 'Остановить';
+ this.controlBtn.disabled = false;
+ }
+
+ /**
+ * Остановить сервер
+ */
+ async stopServer() {
+ this.isOperating = true;
+ this.controlBtn.disabled = true;
+ this.statusText.textContent = 'Выключается...';
+ this.btnText.textContent = 'Ожидайте...';
+ this.setAllServicesPending('Остановка');
+
+ await api.stopServer();
+ await sleep(1500);
+
+ this.isOperating = false;
+ this.setServerStatus(false, 'Сервер остановлен');
+ addClass(this.controlBtn, 'start-mode');
+ this.btnText.textContent = 'Запустить';
+ this.controlBtn.disabled = false;
+ }
+
+ /**
+ * Установить статус сервера
+ */
+ setServerStatus(isOnline, text) {
+ this.serverRunning = isOnline;
+
+ if (isOnline) {
+ removeClass(this.statusIndicator, 'status-offline');
+ addClass(this.statusIndicator, 'status-online');
+ } else {
+ removeClass(this.statusIndicator, 'status-online');
+ addClass(this.statusIndicator, 'status-offline');
+ }
+
+ this.statusText.textContent = text;
+ }
+
+ /**
+ * Установить всем сервисам статус pending
+ */
+ setAllServicesPending(text) {
+ const badges = $$('.service-card .badge');
+ badges.forEach(badge => {
+ badge.className = 'badge badge-pending';
+ badge.textContent = text;
+ });
+ }
+
+ /**
+ * Отрисовать статусы сервисов
+ */
+ renderServices(data) {
+ const services = [data.http, data.https, data.mysql, data.php, data.proxy];
+ const cards = $$('.service-card');
+
+ services.forEach((service, index) => {
+ const card = cards[index];
+ if (!card) return;
+
+ const badge = card.querySelector('.badge');
+ const infoValues = card.querySelectorAll('.info-value');
+
+ // Обновляем badge только если НЕ в процессе операции
+ if (badge && !this.isOperating) {
+ if (service.status) {
+ badge.className = 'badge badge-online';
+ badge.textContent = 'Активен';
+ } else {
+ badge.className = 'badge badge-offline';
+ badge.textContent = 'Остановлен';
+ }
+ }
+
+ // Обновляем значения
+ if (service.name === 'Proxy') {
+ if (infoValues[0] && service.info) {
+ infoValues[0].textContent = service.info;
+ }
+ } else {
+ if (infoValues[0]) {
+ infoValues[0].textContent = service.port;
+ }
+ }
+ });
+ }
+
+ /**
+ * Загрузить статусы сервисов
+ */
+ async loadStatus() {
+ if (isWailsAvailable()) {
+ const data = await api.getAllServicesStatus();
+ if (data) {
+ this.renderServices(data);
+ }
+ } else {
+ // Используем тестовые данные если Wails недоступен
+ const mockData = {
+ http: { name: 'HTTP', status: true, port: '80' },
+ https: { name: 'HTTPS', status: true, port: '443' },
+ mysql: { name: 'MySQL', status: true, port: '3306' },
+ php: { name: 'PHP', status: true, port: '8000-8003' },
+ proxy: { name: 'Proxy', status: true, port: '', info: '1 из 3' }
+ };
+ this.renderServices(mockData);
+ }
+ }
+}
+
diff --git a/Backend/admin/frontend/assets/js/components/sites.js b/Backend/admin/frontend/assets/js/components/sites.js
new file mode 100644
index 0000000..1aa92b5
--- /dev/null
+++ b/Backend/admin/frontend/assets/js/components/sites.js
@@ -0,0 +1,150 @@
+/* ============================================
+ Sites Component
+ Управление сайтами
+ ============================================ */
+
+import { api } from '../api/wails.js';
+import { isWailsAvailable } from '../utils/helpers.js';
+import { $ } from '../utils/dom.js';
+
+/**
+ * Класс для управления сайтами
+ */
+export class SitesManager {
+ constructor() {
+ this.sitesData = [];
+ this.mockData = [
+ {
+ name: 'Локальный сайт',
+ host: '127.0.0.1',
+ alias: ['localhost'],
+ status: 'active',
+ root_file: 'index.html',
+ root_file_routing: true
+ },
+ {
+ name: 'Тестовый проект',
+ host: 'test.local',
+ alias: ['*.test.local', 'test.com'],
+ status: 'active',
+ root_file: 'index.php',
+ root_file_routing: false
+ },
+ {
+ name: 'API сервис',
+ host: 'api.example.com',
+ alias: ['*.api.example.com'],
+ status: 'inactive',
+ root_file: 'index.php',
+ root_file_routing: true
+ }
+ ];
+ }
+
+ /**
+ * Загрузить список сайтов
+ */
+ async load() {
+ if (isWailsAvailable()) {
+ this.sitesData = await api.getSitesList();
+ } else {
+ // Используем тестовые данные если Wails недоступен
+ this.sitesData = this.mockData;
+ }
+ this.render();
+ }
+
+ /**
+ * Отрисовать список сайтов
+ */
+ render() {
+ const tbody = $('sitesTable')?.querySelector('tbody');
+ if (!tbody) return;
+
+ tbody.innerHTML = '';
+
+ this.sitesData.forEach((site, index) => {
+ const row = document.createElement('tr');
+ const statusBadge = site.status === 'active' ? 'badge-online' : 'badge-offline';
+ const aliases = site.alias.join(', ');
+
+ row.innerHTML = `
+ ${site.name} |
+ ${site.host} |
+ ${aliases} |
+ ${site.status} |
+ ${site.root_file} |
+
+
+
+
+ |
+ `;
+
+ tbody.appendChild(row);
+ });
+
+ // Добавляем обработчики событий
+ this.attachEventListeners();
+ }
+
+ /**
+ * Добавить обработчики событий
+ */
+ attachEventListeners() {
+ // Кликабельные ссылки
+ const links = document.querySelectorAll('.clickable-link[data-url]');
+ links.forEach(link => {
+ link.addEventListener('click', () => {
+ const url = link.getAttribute('data-url');
+ this.openLink(url);
+ });
+ });
+
+ // Кнопки действий
+ const buttons = document.querySelectorAll('[data-action]');
+ buttons.forEach(btn => {
+ btn.addEventListener('click', () => {
+ const action = btn.getAttribute('data-action');
+ this.handleAction(action, btn);
+ });
+ });
+ }
+
+ /**
+ * Обработчик действий
+ */
+ async handleAction(action, btn) {
+ const host = btn.getAttribute('data-host');
+ const index = parseInt(btn.getAttribute('data-index'));
+ const isProxy = btn.getAttribute('data-is-proxy') === 'true';
+
+ switch (action) {
+ case 'open-folder':
+ await api.openSiteFolder(host);
+ break;
+ case 'edit-vaccess':
+ if (window.editVAccess) {
+ window.editVAccess(host, isProxy);
+ }
+ break;
+ case 'edit-site':
+ if (window.editSite) {
+ window.editSite(index);
+ }
+ break;
+ }
+ }
+
+ /**
+ * Открыть ссылку
+ */
+ openLink(url) {
+ if (window.runtime?.BrowserOpenURL) {
+ window.runtime.BrowserOpenURL(url);
+ } else {
+ window.open(url, '_blank');
+ }
+ }
+}
+
diff --git a/Backend/admin/frontend/assets/js/components/vaccess.js b/Backend/admin/frontend/assets/js/components/vaccess.js
new file mode 100644
index 0000000..babbec4
--- /dev/null
+++ b/Backend/admin/frontend/assets/js/components/vaccess.js
@@ -0,0 +1,396 @@
+/* ============================================
+ vAccess Component
+ Управление правилами доступа
+ ============================================ */
+
+import { api } from '../api/wails.js';
+import { $, hide, show } from '../utils/dom.js';
+import { notification } from '../ui/notification.js';
+import { modal } from '../ui/modal.js';
+import { isWailsAvailable } from '../utils/helpers.js';
+
+/**
+ * Класс для управления vAccess правилами
+ */
+export class VAccessManager {
+ constructor() {
+ this.vAccessHost = '';
+ this.vAccessIsProxy = false;
+ this.vAccessRules = [];
+ this.vAccessReturnSection = 'sectionSites';
+ this.draggedIndex = null;
+ this.editingField = null;
+ }
+
+ /**
+ * Открыть редактор vAccess
+ */
+ async open(host, isProxy) {
+ this.vAccessHost = host;
+ this.vAccessIsProxy = isProxy;
+
+ // Запоминаем откуда пришли
+ if ($('sectionSites').style.display !== 'none') {
+ this.vAccessReturnSection = 'sectionSites';
+ } else if ($('sectionProxy').style.display !== 'none') {
+ this.vAccessReturnSection = 'sectionProxy';
+ }
+
+ // Загружаем правила
+ if (isWailsAvailable()) {
+ const config = await api.getVAccessRules(host, isProxy);
+ this.vAccessRules = config.rules || [];
+ } else {
+ // Тестовые данные для браузерного режима
+ this.vAccessRules = [
+ {
+ type: 'Disable',
+ type_file: ['*.php'],
+ path_access: ['/uploads/*'],
+ ip_list: [],
+ exceptions_dir: [],
+ url_error: '404'
+ }
+ ];
+ }
+
+ // Обновляем UI
+ const subtitle = isProxy
+ ? 'Управление правилами доступа для прокси-сервиса'
+ : 'Управление правилами доступа для сайта';
+
+ $('breadcrumbHost').textContent = host;
+ $('vAccessSubtitle').textContent = subtitle;
+
+ // Переключаем на страницу редактора
+ this.hideAllSections();
+ show($('sectionVAccessEditor'));
+
+ // Рендерим правила и показываем правильную вкладку
+ this.renderRulesList();
+ this.switchTab('rules');
+
+ // Привязываем кнопку сохранения
+ const saveBtn = $('saveVAccessBtn');
+ if (saveBtn) {
+ saveBtn.onclick = async () => await this.save();
+ }
+ }
+
+ /**
+ * Скрыть все секции
+ */
+ hideAllSections() {
+ hide($('sectionServices'));
+ hide($('sectionSites'));
+ hide($('sectionProxy'));
+ hide($('sectionSettings'));
+ hide($('sectionVAccessEditor'));
+ }
+
+ /**
+ * Вернуться на главную
+ */
+ backToMain() {
+ this.hideAllSections();
+ show($('sectionServices'));
+ show($('sectionSites'));
+ show($('sectionProxy'));
+ }
+
+ /**
+ * Переключить вкладку
+ */
+ switchTab(tab) {
+ const tabs = document.querySelectorAll('.vaccess-tab[data-tab]');
+ tabs.forEach(t => {
+ if (t.dataset.tab === tab) {
+ t.classList.add('active');
+ } else {
+ t.classList.remove('active');
+ }
+ });
+
+ if (tab === 'rules') {
+ show($('vAccessRulesTab'));
+ hide($('vAccessHelpTab'));
+ } else {
+ hide($('vAccessRulesTab'));
+ show($('vAccessHelpTab'));
+ }
+ }
+
+ /**
+ * Сохранить изменения
+ */
+ async save() {
+ if (isWailsAvailable()) {
+ const config = { rules: this.vAccessRules };
+ const configJSON = JSON.stringify(config);
+ const result = await api.saveVAccessRules(this.vAccessHost, this.vAccessIsProxy, configJSON);
+
+ if (result.startsWith('Error')) {
+ notification.error(result, 2000);
+ } else {
+ notification.success('✅ Правила vAccess успешно сохранены', 1000);
+ }
+ } else {
+ // Браузерный режим - просто показываем уведомление
+ notification.success('Данные сохранены (тестовый режим)');
+ }
+ }
+
+ /**
+ * Отрисовать список правил
+ */
+ renderRulesList() {
+ const tbody = $('vAccessTableBody');
+ const emptyState = $('vAccessEmpty');
+ const table = document.querySelector('.vaccess-table');
+
+ if (!tbody) return;
+
+ // Показываем/скрываем пустое состояние
+ if (this.vAccessRules.length === 0) {
+ if (table) hide(table);
+ if (emptyState) show(emptyState);
+ return;
+ } else {
+ if (table) show(table);
+ if (emptyState) hide(emptyState);
+ }
+
+ tbody.innerHTML = this.vAccessRules.map((rule, index) => `
+
+ |
+
+ ${rule.type}
+ |
+
+ ${(rule.type_file || []).length > 0 ? (rule.type_file || []).map(f => `${f}`).join(' ') : '-'}
+ |
+
+ ${(rule.path_access || []).length > 0 ? (rule.path_access || []).map(p => `${p}`).join(' ') : '-'}
+ |
+
+ ${(rule.ip_list || []).length > 0 ? (rule.ip_list || []).map(ip => `${ip}`).join(' ') : '-'}
+ |
+
+ ${(rule.exceptions_dir || []).length > 0 ? (rule.exceptions_dir || []).map(e => `${e}`).join(' ') : '-'}
+ |
+
+ ${rule.url_error || '404'}
+ |
+
+
+ |
+
+ `).join('');
+
+ // Добавляем обработчики
+ this.attachRulesEventListeners();
+ }
+
+ /**
+ * Добавить обработчики событий для правил
+ */
+ attachRulesEventListeners() {
+ // Drag & Drop
+ const rows = document.querySelectorAll('#vAccessTableBody tr[draggable]');
+ rows.forEach(row => {
+ row.addEventListener('dragstart', (e) => this.onDragStart(e));
+ row.addEventListener('dragover', (e) => this.onDragOver(e));
+ row.addEventListener('drop', (e) => this.onDrop(e));
+ });
+
+ // Клик по ячейкам для редактирования
+ const cells = document.querySelectorAll('#vAccessTableBody td[data-field]');
+ cells.forEach(cell => {
+ cell.addEventListener('click', () => {
+ const field = cell.getAttribute('data-field');
+ const index = parseInt(cell.getAttribute('data-index'));
+ this.editRuleField(index, field);
+ });
+ });
+
+ // Кнопки удаления
+ const removeButtons = document.querySelectorAll('[data-action="remove-rule"]');
+ removeButtons.forEach(btn => {
+ btn.addEventListener('click', () => {
+ const index = parseInt(btn.getAttribute('data-index'));
+ this.removeRule(index);
+ });
+ });
+ }
+
+ /**
+ * Добавить новое правило
+ */
+ addRule() {
+ this.vAccessRules.push({
+ type: 'Disable',
+ type_file: [],
+ path_access: [],
+ ip_list: [],
+ exceptions_dir: [],
+ url_error: '404'
+ });
+
+ this.switchTab('rules');
+ this.renderRulesList();
+ }
+
+ /**
+ * Удалить правило
+ */
+ removeRule(index) {
+ this.vAccessRules.splice(index, 1);
+ this.renderRulesList();
+ }
+
+ /**
+ * Редактировать поле правила
+ */
+ editRuleField(index, field) {
+ const rule = this.vAccessRules[index];
+
+ if (field === 'type') {
+ // Переключаем тип
+ rule.type = rule.type === 'Allow' ? 'Disable' : 'Allow';
+ this.renderRulesList();
+ } else if (field === 'url_error') {
+ // Простой prompt для ошибки
+ const value = prompt('Страница ошибки:', rule.url_error || '404');
+ if (value !== null) {
+ rule.url_error = value;
+ this.renderRulesList();
+ }
+ } else {
+ // Для массивов - показываем форму редактирования
+ this.showFieldEditor(index, field);
+ }
+ }
+
+ /**
+ * Показать редактор поля
+ */
+ showFieldEditor(index, field) {
+ const rule = this.vAccessRules[index];
+ const fieldNames = {
+ 'type_file': 'Расширения файлов',
+ 'path_access': 'Пути доступа',
+ 'ip_list': 'IP адреса',
+ 'exceptions_dir': 'Исключения'
+ };
+
+ const placeholders = {
+ 'type_file': '*.php',
+ 'path_access': '/admin/*',
+ 'ip_list': '127.0.0.1',
+ 'exceptions_dir': '/public/*'
+ };
+
+ const content = `
+
+
+
+
+
+
+ ${(rule[field] || []).map(value => `
+
+ ${value}
+
+
+ `).join('')}
+
+
+
+ `;
+
+ this.editingField = { index, field };
+ modal.openFieldEditor(fieldNames[field], content);
+
+ // Добавляем обработчики
+ setTimeout(() => {
+ $('addFieldValueBtn')?.addEventListener('click', () => this.addFieldValue());
+ $('closeFieldEditorBtn')?.addEventListener('click', () => this.closeFieldEditor());
+
+ const removeButtons = document.querySelectorAll('#fieldTags .tag-remove');
+ removeButtons.forEach(btn => {
+ btn.addEventListener('click', () => {
+ const value = btn.getAttribute('data-value');
+ this.removeFieldValue(value);
+ });
+ });
+ }, 100);
+ }
+
+ /**
+ * Добавить значение в поле
+ */
+ addFieldValue() {
+ const input = $('fieldInput');
+ const value = input?.value.trim();
+
+ if (value && this.editingField) {
+ const { index, field } = this.editingField;
+ if (!this.vAccessRules[index][field]) {
+ this.vAccessRules[index][field] = [];
+ }
+ this.vAccessRules[index][field].push(value);
+ input.value = '';
+ this.showFieldEditor(index, field);
+ }
+ }
+
+ /**
+ * Удалить значение из поля
+ */
+ removeFieldValue(value) {
+ if (this.editingField) {
+ const { index, field } = this.editingField;
+ const arr = this.vAccessRules[index][field];
+ const idx = arr.indexOf(value);
+ if (idx > -1) {
+ arr.splice(idx, 1);
+ this.showFieldEditor(index, field);
+ }
+ }
+ }
+
+ /**
+ * Закрыть редактор поля
+ */
+ closeFieldEditor() {
+ modal.closeFieldEditor();
+ this.renderRulesList();
+ }
+
+ // Drag & Drop handlers
+ onDragStart(event) {
+ this.draggedIndex = parseInt(event.target.getAttribute('data-index'));
+ event.target.style.opacity = '0.5';
+ }
+
+ onDragOver(event) {
+ event.preventDefault();
+ }
+
+ onDrop(event) {
+ event.preventDefault();
+ const dropIndex = parseInt(event.target.closest('tr').getAttribute('data-index'));
+
+ if (this.draggedIndex === null || this.draggedIndex === dropIndex) return;
+
+ const draggedRule = this.vAccessRules[this.draggedIndex];
+ this.vAccessRules.splice(this.draggedIndex, 1);
+ this.vAccessRules.splice(dropIndex, 0, draggedRule);
+
+ this.draggedIndex = null;
+ this.renderRulesList();
+ }
+}
+
diff --git a/Backend/admin/frontend/assets/js/main.js b/Backend/admin/frontend/assets/js/main.js
new file mode 100644
index 0000000..45b0d84
--- /dev/null
+++ b/Backend/admin/frontend/assets/js/main.js
@@ -0,0 +1,586 @@
+/* ============================================
+ vServer Admin Panel - Main Entry Point
+ Точка входа приложения
+ ============================================ */
+
+import { log, isWailsAvailable, sleep } from './utils/helpers.js';
+import { WindowControls } from './ui/window.js';
+import { Navigation } from './ui/navigation.js';
+import { notification } from './ui/notification.js';
+import { modal } from './ui/modal.js';
+import { ServicesManager } from './components/services.js';
+import { SitesManager } from './components/sites.js';
+import { ProxyManager } from './components/proxy.js';
+import { VAccessManager } from './components/vaccess.js';
+import { configAPI } from './api/config.js';
+import { $ } from './utils/dom.js';
+
+/**
+ * Главный класс приложения
+ */
+class App {
+ constructor() {
+ this.windowControls = new WindowControls();
+ this.navigation = new Navigation();
+ this.servicesManager = new ServicesManager();
+ this.sitesManager = new SitesManager();
+ this.proxyManager = new ProxyManager();
+ this.vAccessManager = new VAccessManager();
+
+ this.isWails = isWailsAvailable();
+
+ log('Приложение инициализировано');
+ }
+
+ /**
+ * Запустить приложение
+ */
+ async start() {
+ log('Запуск приложения...');
+
+ // Скрываем loader если не в Wails
+ if (!this.isWails) {
+ notification.hideLoader();
+ }
+
+ // Ждём немного перед загрузкой данных
+ await sleep(1000);
+
+ if (this.isWails) {
+ log('Wails API доступен', 'info');
+ } else {
+ log('Wails API недоступен (браузерный режим)', 'warn');
+ }
+
+ // Загружаем начальные данные
+ await this.loadInitialData();
+
+ // Запускаем автообновление
+ this.startAutoRefresh();
+
+ // Скрываем loader после загрузки
+ if (this.isWails) {
+ notification.hideLoader();
+ }
+
+ // Настраиваем глобальные функции для совместимости
+ this.setupGlobalHandlers();
+
+ // Привязываем кнопки
+ this.setupButtons();
+
+ log('Приложение запущено');
+ }
+
+ /**
+ * Загрузить начальные данные
+ */
+ async loadInitialData() {
+ await Promise.all([
+ this.servicesManager.loadStatus(),
+ this.sitesManager.load(),
+ this.proxyManager.load()
+ ]);
+ }
+
+ /**
+ * Запустить автообновление
+ */
+ startAutoRefresh() {
+ setInterval(async () => {
+ await this.loadInitialData();
+ }, 5000);
+ }
+
+ /**
+ * Привязать кнопки
+ */
+ setupButtons() {
+ // Кнопка сохранения настроек
+ const saveSettingsBtn = $('saveSettingsBtn');
+ if (saveSettingsBtn) {
+ saveSettingsBtn.addEventListener('click', async () => {
+ await this.saveConfigSettings();
+ });
+ }
+
+ // Кнопка сохранения vAccess (добавляем обработчик динамически при открытии vAccess)
+ // Обработчик будет добавлен в VAccessManager.open()
+
+ // Моментальное переключение Proxy без перезапуска
+ const proxyCheckbox = $('proxyEnabled');
+ if (proxyCheckbox) {
+ proxyCheckbox.addEventListener('change', async (e) => {
+ const isEnabled = e.target.checked;
+
+ if (isEnabled) {
+ await configAPI.enableProxyService();
+ notification.success('Proxy Manager включен', 1000);
+ } else {
+ await configAPI.disableProxyService();
+ notification.success('Proxy Manager отключен', 1000);
+ }
+ });
+ }
+ }
+
+ /**
+ * Настроить глобальные обработчики
+ */
+ setupGlobalHandlers() {
+ // Для vAccess
+ window.editVAccess = (host, isProxy) => {
+ this.vAccessManager.open(host, isProxy);
+ };
+
+ window.backToMain = () => {
+ this.vAccessManager.backToMain();
+ };
+
+ window.switchVAccessTab = (tab) => {
+ this.vAccessManager.switchTab(tab);
+ };
+
+ window.saveVAccessChanges = async () => {
+ await this.vAccessManager.save();
+ };
+
+ window.addVAccessRule = () => {
+ this.vAccessManager.addRule();
+ };
+
+ // Для Settings
+ window.loadConfig = async () => {
+ await this.loadConfigSettings();
+ };
+
+ window.saveSettings = async () => {
+ await this.saveConfigSettings();
+ };
+
+ // Для модальных окон
+ window.editSite = (index) => {
+ this.editSite(index);
+ };
+
+ window.editProxy = (index) => {
+ this.editProxy(index);
+ };
+
+ window.setStatus = (status) => {
+ this.setModalStatus(status);
+ };
+
+ window.setProxyStatus = (status) => {
+ this.setModalStatus(status);
+ };
+
+ window.addAliasTag = () => {
+ this.addAliasTag();
+ };
+
+ window.removeAliasTag = (btn) => {
+ btn.parentElement.remove();
+ };
+
+ window.saveModalData = async () => {
+ await this.saveModalData();
+ };
+
+ // Drag & Drop для vAccess
+ window.dragStart = (event, index) => {
+ this.vAccessManager.onDragStart(event);
+ };
+
+ window.dragOver = (event) => {
+ this.vAccessManager.onDragOver(event);
+ };
+
+ window.drop = (event, index) => {
+ this.vAccessManager.onDrop(event);
+ };
+
+ window.editRuleField = (index, field) => {
+ this.vAccessManager.editRuleField(index, field);
+ };
+
+ window.removeVAccessRule = (index) => {
+ this.vAccessManager.removeRule(index);
+ };
+
+ window.closeFieldEditor = () => {
+ this.vAccessManager.closeFieldEditor();
+ };
+
+ window.addFieldValue = () => {
+ this.vAccessManager.addFieldValue();
+ };
+
+ window.removeFieldValue = (value) => {
+ this.vAccessManager.removeFieldValue(value);
+ };
+
+ // Тестовые функции (для браузерного режима)
+ window.editTestSite = (index) => {
+ const testSites = [
+ {name: 'Локальный сайт', host: '127.0.0.1', alias: ['localhost'], status: 'active', root_file: 'index.html', root_file_routing: true},
+ {name: 'Тестовый проект', host: 'test.local', alias: ['*.test.local', 'test.com'], status: 'active', root_file: 'index.php', root_file_routing: false},
+ {name: 'API сервис', host: 'api.example.com', alias: ['*.api.example.com'], status: 'inactive', root_file: 'index.php', root_file_routing: true}
+ ];
+ this.sitesManager.sitesData = testSites;
+ this.editSite(index);
+ };
+
+ window.editTestProxy = (index) => {
+ const testProxies = [
+ {enable: true, external_domain: 'git.example.ru', local_address: '127.0.0.1', local_port: '3333', service_https_use: false, auto_https: true},
+ {enable: true, external_domain: 'api.example.com', local_address: '127.0.0.1', local_port: '8080', service_https_use: true, auto_https: false},
+ {enable: false, external_domain: 'test.example.net', local_address: '127.0.0.1', local_port: '5000', service_https_use: false, auto_https: false}
+ ];
+ this.proxyManager.proxiesData = testProxies;
+ this.editProxy(index);
+ };
+
+ window.openTestLink = (url) => {
+ this.sitesManager.openLink(url);
+ };
+
+ window.openSiteFolder = async (host) => {
+ await this.sitesManager.handleAction('open-folder', { getAttribute: () => host });
+ };
+ }
+
+ /**
+ * Загрузить настройки конфигурации
+ */
+ async loadConfigSettings() {
+ if (!isWailsAvailable()) {
+ // Тестовые данные для браузерного режима
+ $('mysqlHost').value = '127.0.0.1';
+ $('mysqlPort').value = 3306;
+ $('phpHost').value = 'localhost';
+ $('phpPort').value = 8000;
+ $('proxyEnabled').checked = true;
+ return;
+ }
+
+ const config = await configAPI.getConfig();
+ if (!config) return;
+
+ $('mysqlHost').value = config.Soft_Settings?.mysql_host || '127.0.0.1';
+ $('mysqlPort').value = config.Soft_Settings?.mysql_port || 3306;
+ $('phpHost').value = config.Soft_Settings?.php_host || 'localhost';
+ $('phpPort').value = config.Soft_Settings?.php_port || 8000;
+ $('proxyEnabled').checked = config.Soft_Settings?.proxy_enabled !== false;
+ }
+
+ /**
+ * Сохранить настройки конфигурации
+ */
+ async saveConfigSettings() {
+ const saveBtn = $('saveSettingsBtn');
+ const originalText = saveBtn.querySelector('span').textContent;
+
+ if (!isWailsAvailable()) {
+ notification.success('Настройки сохранены (тестовый режим)', 1000);
+ return;
+ }
+
+ try {
+ saveBtn.disabled = true;
+ saveBtn.querySelector('span').textContent = 'Сохранение...';
+
+ const config = await configAPI.getConfig();
+ config.Soft_Settings.mysql_host = $('mysqlHost').value;
+ config.Soft_Settings.mysql_port = parseInt($('mysqlPort').value);
+ config.Soft_Settings.php_host = $('phpHost').value;
+ config.Soft_Settings.php_port = parseInt($('phpPort').value);
+ config.Soft_Settings.proxy_enabled = $('proxyEnabled').checked;
+
+ const configJSON = JSON.stringify(config, null, 4);
+ const result = await configAPI.saveConfig(configJSON);
+
+ if (result.startsWith('Error')) {
+ notification.error(result);
+ return;
+ }
+
+ saveBtn.querySelector('span').textContent = 'Перезапуск сервисов...';
+ await configAPI.restartAllServices();
+
+ notification.success('Настройки сохранены и сервисы перезапущены!', 1500);
+ } catch (error) {
+ notification.error('Ошибка: ' + error.message);
+ } finally {
+ saveBtn.disabled = false;
+ saveBtn.querySelector('span').textContent = originalText;
+ }
+ }
+
+ /**
+ * Редактировать сайт
+ */
+ editSite(index) {
+ const site = this.sitesManager.sitesData[index];
+ if (!site) return;
+
+ const content = `
+
+ `;
+
+ modal.open('Редактировать сайт', content);
+ window.currentEditType = 'site';
+ window.currentEditIndex = index;
+ }
+
+ /**
+ * Редактировать прокси
+ */
+ editProxy(index) {
+ const proxy = this.proxyManager.proxiesData[index];
+ if (!proxy) return;
+
+ const content = `
+
+ `;
+
+ modal.open('Редактировать прокси', content);
+ window.currentEditType = 'proxy';
+ window.currentEditIndex = index;
+ }
+
+ /**
+ * Установить статус в модальном окне
+ */
+ setModalStatus(status) {
+ const buttons = document.querySelectorAll('.status-btn');
+ buttons.forEach(btn => {
+ btn.classList.remove('active');
+ if (btn.dataset.value === status) {
+ btn.classList.add('active');
+ }
+ });
+ }
+
+ /**
+ * Добавить alias tag
+ */
+ addAliasTag() {
+ const input = $('editAliasInput');
+ const value = input?.value.trim();
+
+ if (value) {
+ const container = $('aliasTagsContainer');
+ const tag = document.createElement('span');
+ tag.className = 'tag';
+ tag.innerHTML = `
+ ${value}
+
+ `;
+ container.appendChild(tag);
+ input.value = '';
+ }
+ }
+
+ /**
+ * Сохранить данные модального окна
+ */
+ async saveModalData() {
+ if (!isWailsAvailable()) {
+ notification.success('Данные сохранены (тестовый режим)', 1000);
+ modal.close();
+ return;
+ }
+
+ if (window.currentEditType === 'site') {
+ await this.saveSiteData();
+ } else if (window.currentEditType === 'proxy') {
+ await this.saveProxyData();
+ }
+ }
+
+ /**
+ * Сохранить данные сайта
+ */
+ async saveSiteData() {
+ const index = window.currentEditIndex;
+ const tags = document.querySelectorAll('#aliasTagsContainer .tag');
+ const aliases = Array.from(tags).map(tag => tag.textContent.trim());
+ const statusBtn = document.querySelector('.status-btn.active');
+
+ const config = await configAPI.getConfig();
+ config.Site_www[index] = {
+ name: $('editName').value,
+ host: $('editHost').value,
+ alias: aliases,
+ status: statusBtn ? statusBtn.dataset.value : 'active',
+ root_file: $('editRootFile').value,
+ root_file_routing: $('editRouting').checked
+ };
+
+ const configJSON = JSON.stringify(config, null, 4);
+ const result = await configAPI.saveConfig(configJSON);
+
+ if (result.startsWith('Error')) {
+ notification.error(result);
+ } else {
+ notification.show('Перезапуск HTTP/HTTPS...', 'success', 800);
+
+ await configAPI.stopHTTPService();
+ await configAPI.stopHTTPSService();
+ await sleep(500);
+ await configAPI.startHTTPService();
+ await configAPI.startHTTPSService();
+
+ notification.success('Изменения сохранены и применены!', 1000);
+ await this.sitesManager.load();
+ modal.close();
+ }
+ }
+
+ /**
+ * Сохранить данные прокси
+ */
+ async saveProxyData() {
+ const index = window.currentEditIndex;
+ const statusBtn = document.querySelector('.status-btn.active');
+ const isEnabled = statusBtn && statusBtn.dataset.value === 'enable';
+
+ const config = await configAPI.getConfig();
+ config.Proxy_Service[index] = {
+ Enable: isEnabled,
+ ExternalDomain: $('editDomain').value,
+ LocalAddress: $('editLocalAddr').value,
+ LocalPort: $('editLocalPort').value,
+ ServiceHTTPSuse: $('editServiceHTTPS').checked,
+ AutoHTTPS: $('editAutoHTTPS').checked
+ };
+
+ const configJSON = JSON.stringify(config, null, 4);
+ const result = await configAPI.saveConfig(configJSON);
+
+ if (result.startsWith('Error')) {
+ notification.error(result);
+ } else {
+ notification.show('Перезапуск HTTP/HTTPS...', 'success', 800);
+
+ await configAPI.stopHTTPService();
+ await configAPI.stopHTTPSService();
+ await sleep(500);
+ await configAPI.startHTTPService();
+ await configAPI.startHTTPSService();
+
+ notification.success('Изменения сохранены и применены!', 1000);
+ await this.proxyManager.load();
+ modal.close();
+ }
+ }
+}
+
+// Инициализация приложения при загрузке DOM
+document.addEventListener('DOMContentLoaded', () => {
+ const app = new App();
+ app.start();
+});
+
+log('vServer Admin Panel загружен');
+
diff --git a/Backend/admin/frontend/assets/js/ui/modal.js b/Backend/admin/frontend/assets/js/ui/modal.js
new file mode 100644
index 0000000..51a7d86
--- /dev/null
+++ b/Backend/admin/frontend/assets/js/ui/modal.js
@@ -0,0 +1,101 @@
+/* ============================================
+ Modal Manager
+ Управление модальными окнами
+ ============================================ */
+
+import { $, addClass, removeClass } from '../utils/dom.js';
+
+/**
+ * Класс для управления модальными окнами
+ */
+export class Modal {
+ constructor() {
+ this.overlay = $('modalOverlay');
+ this.title = $('modalTitle');
+ this.content = $('modalContent');
+ this.closeBtn = $('modalCloseBtn');
+ this.cancelBtn = $('modalCancelBtn');
+ this.saveBtn = $('modalSaveBtn');
+ this.fieldEditorOverlay = $('fieldEditorOverlay');
+ this.init();
+ }
+
+ init() {
+ if (this.closeBtn) {
+ this.closeBtn.addEventListener('click', () => this.close());
+ }
+
+ if (this.cancelBtn) {
+ this.cancelBtn.addEventListener('click', () => this.close());
+ }
+
+ if (this.saveBtn) {
+ this.saveBtn.addEventListener('click', () => {
+ if (window.saveModalData) {
+ window.saveModalData();
+ }
+ });
+ }
+
+ if (this.overlay) {
+ this.overlay.addEventListener('click', (e) => {
+ if (e.target === this.overlay) {
+ this.close();
+ }
+ });
+ }
+ }
+
+ /**
+ * Открыть модальное окно
+ * @param {string} title - Заголовок
+ * @param {string} htmlContent - HTML контент
+ */
+ open(title, htmlContent) {
+ if (this.title) this.title.textContent = title;
+ if (this.content) this.content.innerHTML = htmlContent;
+ if (this.overlay) addClass(this.overlay, 'show');
+ }
+
+ /**
+ * Закрыть модальное окно
+ */
+ close() {
+ if (this.overlay) removeClass(this.overlay, 'show');
+ }
+
+ /**
+ * Установить обработчик сохранения
+ * @param {Function} callback - Функция обратного вызова
+ */
+ onSave(callback) {
+ if (this.saveBtn) {
+ this.saveBtn.onclick = callback;
+ }
+ }
+
+ /**
+ * Открыть редактор поля
+ * @param {string} title - Заголовок
+ * @param {string} htmlContent - HTML контент
+ */
+ openFieldEditor(title, htmlContent) {
+ const fieldTitle = $('fieldEditorTitle');
+ const fieldContent = $('fieldEditorContent');
+
+ if (fieldTitle) fieldTitle.textContent = title;
+ if (fieldContent) fieldContent.innerHTML = htmlContent;
+ if (this.fieldEditorOverlay) addClass(this.fieldEditorOverlay, 'show');
+ }
+
+ /**
+ * Закрыть редактор поля
+ */
+ closeFieldEditor() {
+ if (this.fieldEditorOverlay) removeClass(this.fieldEditorOverlay, 'show');
+ }
+}
+
+// Глобальный экземпляр модального окна
+export const modal = new Modal();
+
diff --git a/Backend/admin/frontend/assets/js/ui/navigation.js b/Backend/admin/frontend/assets/js/ui/navigation.js
new file mode 100644
index 0000000..3b3e478
--- /dev/null
+++ b/Backend/admin/frontend/assets/js/ui/navigation.js
@@ -0,0 +1,67 @@
+/* ============================================
+ Navigation
+ Управление навигацией
+ ============================================ */
+
+import { $, $$, hide, show, removeClass, addClass } from '../utils/dom.js';
+
+/**
+ * Класс для управления навигацией
+ */
+export class Navigation {
+ constructor() {
+ this.navItems = $$('.nav-item');
+ this.sections = {
+ services: $('sectionServices'),
+ sites: $('sectionSites'),
+ proxy: $('sectionProxy'),
+ settings: $('sectionSettings'),
+ vaccess: $('sectionVAccessEditor')
+ };
+ this.init();
+ }
+
+ init() {
+ this.navItems.forEach((item, index) => {
+ item.addEventListener('click', () => this.navigate(index));
+ });
+ }
+
+ navigate(index) {
+ // Убираем active со всех навигационных элементов
+ this.navItems.forEach(nav => removeClass(nav, 'active'));
+ addClass(this.navItems[index], 'active');
+
+ // Скрываем все секции
+ this.hideAllSections();
+
+ // Показываем нужные секции
+ if (index === 0) {
+ // Главная - всё кроме настроек
+ show(this.sections.services);
+ show(this.sections.sites);
+ show(this.sections.proxy);
+ } else if (index === 3) {
+ // Настройки
+ show(this.sections.settings);
+ // Загружаем конфигурацию при открытии
+ if (window.loadConfig) {
+ window.loadConfig();
+ }
+ }
+ }
+
+ hideAllSections() {
+ Object.values(this.sections).forEach(section => {
+ if (section) hide(section);
+ });
+ }
+
+ showDashboard() {
+ this.hideAllSections();
+ show(this.sections.services);
+ show(this.sections.sites);
+ show(this.sections.proxy);
+ }
+}
+
diff --git a/Backend/admin/frontend/assets/js/ui/notification.js b/Backend/admin/frontend/assets/js/ui/notification.js
new file mode 100644
index 0000000..3ada21d
--- /dev/null
+++ b/Backend/admin/frontend/assets/js/ui/notification.js
@@ -0,0 +1,81 @@
+/* ============================================
+ Notification System
+ Система уведомлений
+ ============================================ */
+
+import { $, addClass, removeClass } from '../utils/dom.js';
+
+/**
+ * Класс для управления уведомлениями
+ */
+export class NotificationManager {
+ constructor() {
+ this.container = $('notification');
+ this.loader = $('appLoader');
+ }
+
+ /**
+ * Показать уведомление
+ * @param {string} message - Текст сообщения
+ * @param {string} type - Тип (success, error)
+ * @param {number} duration - Длительность показа (мс)
+ */
+ show(message, type = 'success', duration = 1000) {
+ if (!this.container) return;
+
+ const icon = type === 'success'
+ ? ''
+ : '';
+
+ this.container.innerHTML = `
+
+ `;
+
+ this.container.className = `notification show ${type}`;
+
+ setTimeout(() => {
+ removeClass(this.container, 'show');
+ }, duration);
+ }
+
+ /**
+ * Показать успешное уведомление
+ * @param {string} message - Текст сообщения
+ * @param {number} duration - Длительность
+ */
+ success(message, duration = 1000) {
+ this.show(message, 'success', duration);
+ }
+
+ /**
+ * Показать уведомление об ошибке
+ * @param {string} message - Текст сообщения
+ * @param {number} duration - Длительность
+ */
+ error(message, duration = 2000) {
+ this.show(message, 'error', duration);
+ }
+
+ /**
+ * Скрыть загрузчик приложения
+ */
+ hideLoader() {
+ if (!this.loader) return;
+
+ setTimeout(() => {
+ addClass(this.loader, 'hide');
+ setTimeout(() => {
+ if (this.loader.parentNode) {
+ this.loader.remove();
+ }
+ }, 500);
+ }, 1500);
+ }
+}
+
+// Глобальный экземпляр менеджера уведомлений
+export const notification = new NotificationManager();
+
diff --git a/Backend/admin/frontend/assets/js/ui/window.js b/Backend/admin/frontend/assets/js/ui/window.js
new file mode 100644
index 0000000..a11b67b
--- /dev/null
+++ b/Backend/admin/frontend/assets/js/ui/window.js
@@ -0,0 +1,51 @@
+/* ============================================
+ Window Controls
+ Управление окном приложения
+ ============================================ */
+
+import { $, addClass } from '../utils/dom.js';
+
+/**
+ * Класс для управления окном
+ */
+export class WindowControls {
+ constructor() {
+ this.minimizeBtn = $('minimizeBtn');
+ this.maximizeBtn = $('maximizeBtn');
+ this.closeBtn = $('closeBtn');
+ this.init();
+ }
+
+ init() {
+ if (this.minimizeBtn) {
+ this.minimizeBtn.addEventListener('click', () => this.minimize());
+ }
+
+ if (this.maximizeBtn) {
+ this.maximizeBtn.addEventListener('click', () => this.maximize());
+ }
+
+ if (this.closeBtn) {
+ this.closeBtn.addEventListener('click', () => this.close());
+ }
+ }
+
+ minimize() {
+ if (window.runtime?.WindowMinimise) {
+ window.runtime.WindowMinimise();
+ }
+ }
+
+ maximize() {
+ if (window.runtime?.WindowToggleMaximise) {
+ window.runtime.WindowToggleMaximise();
+ }
+ }
+
+ close() {
+ if (window.runtime?.Quit) {
+ window.runtime.Quit();
+ }
+ }
+}
+
diff --git a/Backend/admin/frontend/assets/js/utils/dom.js b/Backend/admin/frontend/assets/js/utils/dom.js
new file mode 100644
index 0000000..8de0ac7
--- /dev/null
+++ b/Backend/admin/frontend/assets/js/utils/dom.js
@@ -0,0 +1,83 @@
+/* ============================================
+ DOM Utilities
+ Утилиты для работы с DOM
+ ============================================ */
+
+/**
+ * Получить элемент по ID
+ * @param {string} id - ID элемента
+ * @returns {HTMLElement|null}
+ */
+export function $(id) {
+ return document.getElementById(id);
+}
+
+/**
+ * Получить все элементы по селектору
+ * @param {string} selector - CSS селектор
+ * @param {HTMLElement} parent - Родительский элемент
+ * @returns {NodeList}
+ */
+export function $$(selector, parent = document) {
+ return parent.querySelectorAll(selector);
+}
+
+/**
+ * Показать элемент
+ * @param {HTMLElement|string} element - Элемент или ID
+ */
+export function show(element) {
+ const el = typeof element === 'string' ? $(element) : element;
+ if (el) el.style.display = 'block';
+}
+
+/**
+ * Скрыть элемент
+ * @param {HTMLElement|string} element - Элемент или ID
+ */
+export function hide(element) {
+ const el = typeof element === 'string' ? $(element) : element;
+ if (el) el.style.display = 'none';
+}
+
+/**
+ * Переключить видимость элемента
+ * @param {HTMLElement|string} element - Элемент или ID
+ */
+export function toggle(element) {
+ const el = typeof element === 'string' ? $(element) : element;
+ if (el) {
+ el.style.display = el.style.display === 'none' ? 'block' : 'none';
+ }
+}
+
+/**
+ * Добавить класс
+ * @param {HTMLElement|string} element - Элемент или ID
+ * @param {string} className - Имя класса
+ */
+export function addClass(element, className) {
+ const el = typeof element === 'string' ? $(element) : element;
+ if (el) el.classList.add(className);
+}
+
+/**
+ * Удалить класс
+ * @param {HTMLElement|string} element - Элемент или ID
+ * @param {string} className - Имя класса
+ */
+export function removeClass(element, className) {
+ const el = typeof element === 'string' ? $(element) : element;
+ if (el) el.classList.remove(className);
+}
+
+/**
+ * Переключить класс
+ * @param {HTMLElement|string} element - Элемент или ID
+ * @param {string} className - Имя класса
+ */
+export function toggleClass(element, className) {
+ const el = typeof element === 'string' ? $(element) : element;
+ if (el) el.classList.toggle(className);
+}
+
diff --git a/Backend/admin/frontend/assets/js/utils/helpers.js b/Backend/admin/frontend/assets/js/utils/helpers.js
new file mode 100644
index 0000000..db9de03
--- /dev/null
+++ b/Backend/admin/frontend/assets/js/utils/helpers.js
@@ -0,0 +1,57 @@
+/* ============================================
+ Helper Utilities
+ Вспомогательные функции
+ ============================================ */
+
+/**
+ * Ждёт указанное время
+ * @param {number} ms - Миллисекунды
+ * @returns {Promise}
+ */
+export function sleep(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+}
+
+/**
+ * Debounce функция
+ * @param {Function} func - Функция для debounce
+ * @param {number} wait - Время задержки
+ * @returns {Function}
+ */
+export function debounce(func, wait) {
+ let timeout;
+ return function executedFunction(...args) {
+ const later = () => {
+ clearTimeout(timeout);
+ func(...args);
+ };
+ clearTimeout(timeout);
+ timeout = setTimeout(later, wait);
+ };
+}
+
+/**
+ * Проверяет доступность Wails API
+ * @returns {boolean}
+ */
+export function isWailsAvailable() {
+ return typeof window.go !== 'undefined' &&
+ window.go?.admin?.App !== undefined;
+}
+
+/**
+ * Логирование с префиксом
+ * @param {string} message - Сообщение
+ * @param {string} type - Тип (log, error, warn, info)
+ */
+export function log(message, type = 'log') {
+ const prefix = '🚀 vServer:';
+ const styles = {
+ log: '✅',
+ error: '❌',
+ warn: '⚠️',
+ info: 'ℹ️'
+ };
+ console[type](`${prefix} ${styles[type]} ${message}`);
+}
+
diff --git a/Backend/admin/frontend/index.html b/Backend/admin/frontend/index.html
new file mode 100644
index 0000000..34ad86c
--- /dev/null
+++ b/Backend/admin/frontend/index.html
@@ -0,0 +1,540 @@
+
+
+
+
+
+ vServer Admin Panel
+
+
+
+
+
+
+
+
+ 🚀
+ vServer
+
+
+
+ Сервер запущен
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Статус сервисов
+
+
+
+
+
+
+
+
+
+
+
+ Порты:
+ 8000-8003
+
+
+
+
+
+
+
+
+
+ Список сайтов
+
+
+
+
+ | Имя |
+ Host |
+ Alias |
+ Статус |
+ Root File |
+ Действия |
+
+
+
+
+ | Локальный сайт |
+ 127.0.0.1 |
+ localhost |
+ active |
+ index.html |
+
+
+
+
+ |
+
+
+ | Тестовый проект |
+ test.local |
+ *.test.local, test.com |
+ active |
+ index.php |
+
+
+
+
+ |
+
+
+ | API сервис |
+ api.example.com |
+ *.api.example.com |
+ inactive |
+ index.php |
+
+
+
+
+ |
+
+
+
+
+
+
+
+ Прокси сервисы
+
+
+
+
+ | Внешний домен |
+ Локальный адрес |
+ HTTPS |
+ Auto HTTPS |
+ Статус |
+ Действия |
+
+
+
+
+ git.example.ru |
+ 127.0.0.1:3333 |
+ HTTP |
+ Да |
+ active |
+
+
+
+ |
+
+
+ api.example.com |
+ 127.0.0.1:8080 |
+ HTTPS |
+ Нет |
+ active |
+
+
+
+ |
+
+
+ test.example.net |
+ 127.0.0.1:5000 |
+ HTTP |
+ Нет |
+ disabled |
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ /
+ example.com
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+ Тип |
+ Расширения |
+ Пути доступа |
+ IP адреса |
+ Исключения |
+ Ошибка |
+ |
+
+
+
+
+
+
+
+
+
+
+
Нет правил доступа
+
Добавьте первое правило, чтобы начать управление доступом
+
+
+
+
+
+
+
+
+
+
Принцип работы
+
+ - Правила проверяются сверху вниз по порядку
+ - Первое подходящее правило срабатывает и завершает проверку
+ - Если ни одно правило не сработало - доступ разрешён
+ - Перетаскивайте правила за чтобы изменить порядок
+
+
+
+
+
Параметры правил
+
+
+
type:
+
Allow (разрешить) или Disable (запретить)
+
+
+
Расширения файлов:
+
Список расширений через запятую (*.php, *.exe)
+
+
+
Пути доступа:
+
Список путей через запятую (/admin/*, /api/*)
+
+
+
IP адреса:
+
Список IP адресов через запятую (192.168.1.1, 10.0.0.5)
+
Используется реальный IP соединения (не заголовки прокси!)
+
+
+
Исключения:
+
Пути-исключения через запятую (/bot/*, /public/*). Правило НЕ применяется к этим путям
+
+
+
Страница ошибки:
+
Куда перенаправить при блокировке:
+
+ 404 - стандартная страница ошибки
+ https://site.com - внешний редирект
+ /error.html - локальная страница
+
+
+
+
+
+
+
Паттерны
+
+
+ *.ext
+ Любой файл с расширением .ext
+
+
+ no_extension
+ Файлы без расширения (например: /api/users, /admin)
+
+
+ /path/*
+ Все файлы в папке /path/ и подпапках
+
+
+ /file.php
+ Конкретный файл
+
+
+
+
+
+
Примеры правил
+
+
+
1. Запретить выполнение PHP в uploads
+
+
Тип: Disable
+
Расширения: *.php
+
Пути: /uploads/*
+
Ошибка: 404
+
+
+
+
+
2. Разрешить админку только с определённых IP
+
+
Тип: Allow
+
Пути: /admin/*
+
IP: 192.168.1.100, 127.0.0.1
+
Ошибка: 404
+
+
+
+
+
3. Блокировать определённые IP для всего сайта
+
+
Тип: Disable
+
IP: 192.168.1.50, 10.0.0.99
+
Ошибка: https://google.com
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
🚀
+
Запуск vServer...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Backend/admin/frontend/wailsjs/go/admin/App.d.ts b/Backend/admin/frontend/wailsjs/go/admin/App.d.ts
new file mode 100644
index 0000000..2540b86
--- /dev/null
+++ b/Backend/admin/frontend/wailsjs/go/admin/App.d.ts
@@ -0,0 +1,17 @@
+// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
+// This file is automatically generated. DO NOT EDIT
+import {services} from '../models';
+import {proxy} from '../models';
+import {sites} from '../models';
+
+export function CheckServicesReady():Promise;
+
+export function GetAllServicesStatus():Promise;
+
+export function GetProxyList():Promise>;
+
+export function GetSitesList():Promise>;
+
+export function StartServer():Promise;
+
+export function StopServer():Promise;
diff --git a/Backend/admin/frontend/wailsjs/go/admin/App.js b/Backend/admin/frontend/wailsjs/go/admin/App.js
new file mode 100644
index 0000000..511fcff
--- /dev/null
+++ b/Backend/admin/frontend/wailsjs/go/admin/App.js
@@ -0,0 +1,27 @@
+// @ts-check
+// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
+// This file is automatically generated. DO NOT EDIT
+
+export function CheckServicesReady() {
+ return window['go']['admin']['App']['CheckServicesReady']();
+}
+
+export function GetAllServicesStatus() {
+ return window['go']['admin']['App']['GetAllServicesStatus']();
+}
+
+export function GetProxyList() {
+ return window['go']['admin']['App']['GetProxyList']();
+}
+
+export function GetSitesList() {
+ return window['go']['admin']['App']['GetSitesList']();
+}
+
+export function StartServer() {
+ return window['go']['admin']['App']['StartServer']();
+}
+
+export function StopServer() {
+ return window['go']['admin']['App']['StopServer']();
+}
diff --git a/Backend/admin/frontend/wailsjs/go/models.ts b/Backend/admin/frontend/wailsjs/go/models.ts
new file mode 100644
index 0000000..d50dd2e
--- /dev/null
+++ b/Backend/admin/frontend/wailsjs/go/models.ts
@@ -0,0 +1,119 @@
+export namespace proxy {
+
+ export class ProxyInfo {
+ enable: boolean;
+ external_domain: string;
+ local_address: string;
+ local_port: string;
+ service_https_use: boolean;
+ auto_https: boolean;
+ status: string;
+
+ static createFrom(source: any = {}) {
+ return new ProxyInfo(source);
+ }
+
+ constructor(source: any = {}) {
+ if ('string' === typeof source) source = JSON.parse(source);
+ this.enable = source["enable"];
+ this.external_domain = source["external_domain"];
+ this.local_address = source["local_address"];
+ this.local_port = source["local_port"];
+ this.service_https_use = source["service_https_use"];
+ this.auto_https = source["auto_https"];
+ this.status = source["status"];
+ }
+ }
+
+}
+
+export namespace services {
+
+ export class ServiceStatus {
+ name: string;
+ status: boolean;
+ port: string;
+ requests: number;
+ info: string;
+
+ static createFrom(source: any = {}) {
+ return new ServiceStatus(source);
+ }
+
+ constructor(source: any = {}) {
+ if ('string' === typeof source) source = JSON.parse(source);
+ this.name = source["name"];
+ this.status = source["status"];
+ this.port = source["port"];
+ this.requests = source["requests"];
+ this.info = source["info"];
+ }
+ }
+ export class AllServicesStatus {
+ http: ServiceStatus;
+ https: ServiceStatus;
+ mysql: ServiceStatus;
+ php: ServiceStatus;
+ proxy: ServiceStatus;
+
+ static createFrom(source: any = {}) {
+ return new AllServicesStatus(source);
+ }
+
+ constructor(source: any = {}) {
+ if ('string' === typeof source) source = JSON.parse(source);
+ this.http = this.convertValues(source["http"], ServiceStatus);
+ this.https = this.convertValues(source["https"], ServiceStatus);
+ this.mysql = this.convertValues(source["mysql"], ServiceStatus);
+ this.php = this.convertValues(source["php"], ServiceStatus);
+ this.proxy = this.convertValues(source["proxy"], ServiceStatus);
+ }
+
+ convertValues(a: any, classs: any, asMap: boolean = false): any {
+ if (!a) {
+ return a;
+ }
+ if (a.slice && a.map) {
+ return (a as any[]).map(elem => this.convertValues(elem, classs));
+ } else if ("object" === typeof a) {
+ if (asMap) {
+ for (const key of Object.keys(a)) {
+ a[key] = new classs(a[key]);
+ }
+ return a;
+ }
+ return new classs(a);
+ }
+ return a;
+ }
+ }
+
+}
+
+export namespace sites {
+
+ export class SiteInfo {
+ name: string;
+ host: string;
+ alias: string[];
+ status: string;
+ root_file: string;
+ root_file_routing: boolean;
+
+ static createFrom(source: any = {}) {
+ return new SiteInfo(source);
+ }
+
+ constructor(source: any = {}) {
+ if ('string' === typeof source) source = JSON.parse(source);
+ this.name = source["name"];
+ this.host = source["host"];
+ this.alias = source["alias"];
+ this.status = source["status"];
+ this.root_file = source["root_file"];
+ this.root_file_routing = source["root_file_routing"];
+ }
+ }
+
+}
+
diff --git a/Backend/admin/frontend/wailsjs/runtime/package.json b/Backend/admin/frontend/wailsjs/runtime/package.json
new file mode 100644
index 0000000..1e7c8a5
--- /dev/null
+++ b/Backend/admin/frontend/wailsjs/runtime/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "@wailsapp/runtime",
+ "version": "2.0.0",
+ "description": "Wails Javascript runtime library",
+ "main": "runtime.js",
+ "types": "runtime.d.ts",
+ "scripts": {
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/wailsapp/wails.git"
+ },
+ "keywords": [
+ "Wails",
+ "Javascript",
+ "Go"
+ ],
+ "author": "Lea Anthony ",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/wailsapp/wails/issues"
+ },
+ "homepage": "https://github.com/wailsapp/wails#readme"
+}
diff --git a/Backend/admin/frontend/wailsjs/runtime/runtime.d.ts b/Backend/admin/frontend/wailsjs/runtime/runtime.d.ts
new file mode 100644
index 0000000..4445dac
--- /dev/null
+++ b/Backend/admin/frontend/wailsjs/runtime/runtime.d.ts
@@ -0,0 +1,249 @@
+/*
+ _ __ _ __
+| | / /___ _(_) /____
+| | /| / / __ `/ / / ___/
+| |/ |/ / /_/ / / (__ )
+|__/|__/\__,_/_/_/____/
+The electron alternative for Go
+(c) Lea Anthony 2019-present
+*/
+
+export interface Position {
+ x: number;
+ y: number;
+}
+
+export interface Size {
+ w: number;
+ h: number;
+}
+
+export interface Screen {
+ isCurrent: boolean;
+ isPrimary: boolean;
+ width : number
+ height : number
+}
+
+// Environment information such as platform, buildtype, ...
+export interface EnvironmentInfo {
+ buildType: string;
+ platform: string;
+ arch: string;
+}
+
+// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit)
+// emits the given event. Optional data may be passed with the event.
+// This will trigger any event listeners.
+export function EventsEmit(eventName: string, ...data: any): void;
+
+// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name.
+export function EventsOn(eventName: string, callback: (...data: any) => void): () => void;
+
+// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple)
+// sets up a listener for the given event name, but will only trigger a given number times.
+export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void;
+
+// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce)
+// sets up a listener for the given event name, but will only trigger once.
+export function EventsOnce(eventName: string, callback: (...data: any) => void): () => void;
+
+// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff)
+// unregisters the listener for the given event name.
+export function EventsOff(eventName: string, ...additionalEventNames: string[]): void;
+
+// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall)
+// unregisters all listeners.
+export function EventsOffAll(): void;
+
+// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint)
+// logs the given message as a raw message
+export function LogPrint(message: string): void;
+
+// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace)
+// logs the given message at the `trace` log level.
+export function LogTrace(message: string): void;
+
+// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug)
+// logs the given message at the `debug` log level.
+export function LogDebug(message: string): void;
+
+// [LogError](https://wails.io/docs/reference/runtime/log#logerror)
+// logs the given message at the `error` log level.
+export function LogError(message: string): void;
+
+// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal)
+// logs the given message at the `fatal` log level.
+// The application will quit after calling this method.
+export function LogFatal(message: string): void;
+
+// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo)
+// logs the given message at the `info` log level.
+export function LogInfo(message: string): void;
+
+// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning)
+// logs the given message at the `warning` log level.
+export function LogWarning(message: string): void;
+
+// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload)
+// Forces a reload by the main application as well as connected browsers.
+export function WindowReload(): void;
+
+// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp)
+// Reloads the application frontend.
+export function WindowReloadApp(): void;
+
+// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop)
+// Sets the window AlwaysOnTop or not on top.
+export function WindowSetAlwaysOnTop(b: boolean): void;
+
+// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme)
+// *Windows only*
+// Sets window theme to system default (dark/light).
+export function WindowSetSystemDefaultTheme(): void;
+
+// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme)
+// *Windows only*
+// Sets window to light theme.
+export function WindowSetLightTheme(): void;
+
+// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme)
+// *Windows only*
+// Sets window to dark theme.
+export function WindowSetDarkTheme(): void;
+
+// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter)
+// Centers the window on the monitor the window is currently on.
+export function WindowCenter(): void;
+
+// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle)
+// Sets the text in the window title bar.
+export function WindowSetTitle(title: string): void;
+
+// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen)
+// Makes the window full screen.
+export function WindowFullscreen(): void;
+
+// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen)
+// Restores the previous window dimensions and position prior to full screen.
+export function WindowUnfullscreen(): void;
+
+// [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen)
+// Returns the state of the window, i.e. whether the window is in full screen mode or not.
+export function WindowIsFullscreen(): Promise;
+
+// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize)
+// Sets the width and height of the window.
+export function WindowSetSize(width: number, height: number): void;
+
+// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize)
+// Gets the width and height of the window.
+export function WindowGetSize(): Promise;
+
+// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize)
+// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions.
+// Setting a size of 0,0 will disable this constraint.
+export function WindowSetMaxSize(width: number, height: number): void;
+
+// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize)
+// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions.
+// Setting a size of 0,0 will disable this constraint.
+export function WindowSetMinSize(width: number, height: number): void;
+
+// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition)
+// Sets the window position relative to the monitor the window is currently on.
+export function WindowSetPosition(x: number, y: number): void;
+
+// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition)
+// Gets the window position relative to the monitor the window is currently on.
+export function WindowGetPosition(): Promise;
+
+// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide)
+// Hides the window.
+export function WindowHide(): void;
+
+// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow)
+// Shows the window, if it is currently hidden.
+export function WindowShow(): void;
+
+// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise)
+// Maximises the window to fill the screen.
+export function WindowMaximise(): void;
+
+// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise)
+// Toggles between Maximised and UnMaximised.
+export function WindowToggleMaximise(): void;
+
+// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise)
+// Restores the window to the dimensions and position prior to maximising.
+export function WindowUnmaximise(): void;
+
+// [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised)
+// Returns the state of the window, i.e. whether the window is maximised or not.
+export function WindowIsMaximised(): Promise;
+
+// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise)
+// Minimises the window.
+export function WindowMinimise(): void;
+
+// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise)
+// Restores the window to the dimensions and position prior to minimising.
+export function WindowUnminimise(): void;
+
+// [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised)
+// Returns the state of the window, i.e. whether the window is minimised or not.
+export function WindowIsMinimised(): Promise;
+
+// [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal)
+// Returns the state of the window, i.e. whether the window is normal or not.
+export function WindowIsNormal(): Promise;
+
+// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour)
+// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels.
+export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void;
+
+// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall)
+// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system.
+export function ScreenGetAll(): Promise;
+
+// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl)
+// Opens the given URL in the system browser.
+export function BrowserOpenURL(url: string): void;
+
+// [Environment](https://wails.io/docs/reference/runtime/intro#environment)
+// Returns information about the environment
+export function Environment(): Promise;
+
+// [Quit](https://wails.io/docs/reference/runtime/intro#quit)
+// Quits the application.
+export function Quit(): void;
+
+// [Hide](https://wails.io/docs/reference/runtime/intro#hide)
+// Hides the application.
+export function Hide(): void;
+
+// [Show](https://wails.io/docs/reference/runtime/intro#show)
+// Shows the application.
+export function Show(): void;
+
+// [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext)
+// Returns the current text stored on clipboard
+export function ClipboardGetText(): Promise;
+
+// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext)
+// Sets a text on the clipboard
+export function ClipboardSetText(text: string): Promise;
+
+// [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop)
+// OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
+export function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void
+
+// [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff)
+// OnFileDropOff removes the drag and drop listeners and handlers.
+export function OnFileDropOff() :void
+
+// Check if the file path resolver is available
+export function CanResolveFilePaths(): boolean;
+
+// Resolves file paths for an array of files
+export function ResolveFilePaths(files: File[]): void
\ No newline at end of file
diff --git a/Backend/admin/frontend/wailsjs/runtime/runtime.js b/Backend/admin/frontend/wailsjs/runtime/runtime.js
new file mode 100644
index 0000000..7cb89d7
--- /dev/null
+++ b/Backend/admin/frontend/wailsjs/runtime/runtime.js
@@ -0,0 +1,242 @@
+/*
+ _ __ _ __
+| | / /___ _(_) /____
+| | /| / / __ `/ / / ___/
+| |/ |/ / /_/ / / (__ )
+|__/|__/\__,_/_/_/____/
+The electron alternative for Go
+(c) Lea Anthony 2019-present
+*/
+
+export function LogPrint(message) {
+ window.runtime.LogPrint(message);
+}
+
+export function LogTrace(message) {
+ window.runtime.LogTrace(message);
+}
+
+export function LogDebug(message) {
+ window.runtime.LogDebug(message);
+}
+
+export function LogInfo(message) {
+ window.runtime.LogInfo(message);
+}
+
+export function LogWarning(message) {
+ window.runtime.LogWarning(message);
+}
+
+export function LogError(message) {
+ window.runtime.LogError(message);
+}
+
+export function LogFatal(message) {
+ window.runtime.LogFatal(message);
+}
+
+export function EventsOnMultiple(eventName, callback, maxCallbacks) {
+ return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks);
+}
+
+export function EventsOn(eventName, callback) {
+ return EventsOnMultiple(eventName, callback, -1);
+}
+
+export function EventsOff(eventName, ...additionalEventNames) {
+ return window.runtime.EventsOff(eventName, ...additionalEventNames);
+}
+
+export function EventsOffAll() {
+ return window.runtime.EventsOffAll();
+}
+
+export function EventsOnce(eventName, callback) {
+ return EventsOnMultiple(eventName, callback, 1);
+}
+
+export function EventsEmit(eventName) {
+ let args = [eventName].slice.call(arguments);
+ return window.runtime.EventsEmit.apply(null, args);
+}
+
+export function WindowReload() {
+ window.runtime.WindowReload();
+}
+
+export function WindowReloadApp() {
+ window.runtime.WindowReloadApp();
+}
+
+export function WindowSetAlwaysOnTop(b) {
+ window.runtime.WindowSetAlwaysOnTop(b);
+}
+
+export function WindowSetSystemDefaultTheme() {
+ window.runtime.WindowSetSystemDefaultTheme();
+}
+
+export function WindowSetLightTheme() {
+ window.runtime.WindowSetLightTheme();
+}
+
+export function WindowSetDarkTheme() {
+ window.runtime.WindowSetDarkTheme();
+}
+
+export function WindowCenter() {
+ window.runtime.WindowCenter();
+}
+
+export function WindowSetTitle(title) {
+ window.runtime.WindowSetTitle(title);
+}
+
+export function WindowFullscreen() {
+ window.runtime.WindowFullscreen();
+}
+
+export function WindowUnfullscreen() {
+ window.runtime.WindowUnfullscreen();
+}
+
+export function WindowIsFullscreen() {
+ return window.runtime.WindowIsFullscreen();
+}
+
+export function WindowGetSize() {
+ return window.runtime.WindowGetSize();
+}
+
+export function WindowSetSize(width, height) {
+ window.runtime.WindowSetSize(width, height);
+}
+
+export function WindowSetMaxSize(width, height) {
+ window.runtime.WindowSetMaxSize(width, height);
+}
+
+export function WindowSetMinSize(width, height) {
+ window.runtime.WindowSetMinSize(width, height);
+}
+
+export function WindowSetPosition(x, y) {
+ window.runtime.WindowSetPosition(x, y);
+}
+
+export function WindowGetPosition() {
+ return window.runtime.WindowGetPosition();
+}
+
+export function WindowHide() {
+ window.runtime.WindowHide();
+}
+
+export function WindowShow() {
+ window.runtime.WindowShow();
+}
+
+export function WindowMaximise() {
+ window.runtime.WindowMaximise();
+}
+
+export function WindowToggleMaximise() {
+ window.runtime.WindowToggleMaximise();
+}
+
+export function WindowUnmaximise() {
+ window.runtime.WindowUnmaximise();
+}
+
+export function WindowIsMaximised() {
+ return window.runtime.WindowIsMaximised();
+}
+
+export function WindowMinimise() {
+ window.runtime.WindowMinimise();
+}
+
+export function WindowUnminimise() {
+ window.runtime.WindowUnminimise();
+}
+
+export function WindowSetBackgroundColour(R, G, B, A) {
+ window.runtime.WindowSetBackgroundColour(R, G, B, A);
+}
+
+export function ScreenGetAll() {
+ return window.runtime.ScreenGetAll();
+}
+
+export function WindowIsMinimised() {
+ return window.runtime.WindowIsMinimised();
+}
+
+export function WindowIsNormal() {
+ return window.runtime.WindowIsNormal();
+}
+
+export function BrowserOpenURL(url) {
+ window.runtime.BrowserOpenURL(url);
+}
+
+export function Environment() {
+ return window.runtime.Environment();
+}
+
+export function Quit() {
+ window.runtime.Quit();
+}
+
+export function Hide() {
+ window.runtime.Hide();
+}
+
+export function Show() {
+ window.runtime.Show();
+}
+
+export function ClipboardGetText() {
+ return window.runtime.ClipboardGetText();
+}
+
+export function ClipboardSetText(text) {
+ return window.runtime.ClipboardSetText(text);
+}
+
+/**
+ * Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
+ *
+ * @export
+ * @callback OnFileDropCallback
+ * @param {number} x - x coordinate of the drop
+ * @param {number} y - y coordinate of the drop
+ * @param {string[]} paths - A list of file paths.
+ */
+
+/**
+ * OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
+ *
+ * @export
+ * @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
+ * @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target)
+ */
+export function OnFileDrop(callback, useDropTarget) {
+ return window.runtime.OnFileDrop(callback, useDropTarget);
+}
+
+/**
+ * OnFileDropOff removes the drag and drop listeners and handlers.
+ */
+export function OnFileDropOff() {
+ return window.runtime.OnFileDropOff();
+}
+
+export function CanResolveFilePaths() {
+ return window.runtime.CanResolveFilePaths();
+}
+
+export function ResolveFilePaths(files) {
+ return window.runtime.ResolveFilePaths(files);
+}
\ No newline at end of file
diff --git a/Backend/admin/frontend/wailsjs/wailsjs/go/admin/App.d.ts b/Backend/admin/frontend/wailsjs/wailsjs/go/admin/App.d.ts
new file mode 100644
index 0000000..5317185
--- /dev/null
+++ b/Backend/admin/frontend/wailsjs/wailsjs/go/admin/App.d.ts
@@ -0,0 +1,54 @@
+// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
+// This file is automatically generated. DO NOT EDIT
+import {services} from '../models';
+import {proxy} from '../models';
+import {sites} from '../models';
+import {vaccess} from '../models';
+
+export function CheckServicesReady():Promise;
+
+export function DisableProxyService():Promise;
+
+export function EnableProxyService():Promise;
+
+export function GetAllServicesStatus():Promise;
+
+export function GetConfig():Promise;
+
+export function GetProxyList():Promise>;
+
+export function GetSitesList():Promise>;
+
+export function GetVAccessRules(arg1:string,arg2:boolean):Promise;
+
+export function OpenSiteFolder(arg1:string):Promise;
+
+export function ReloadConfig():Promise;
+
+export function RestartAllServices():Promise;
+
+export function SaveConfig(arg1:string):Promise;
+
+export function SaveVAccessRules(arg1:string,arg2:boolean,arg3:string):Promise;
+
+export function StartHTTPSService():Promise;
+
+export function StartHTTPService():Promise;
+
+export function StartMySQLService():Promise;
+
+export function StartPHPService():Promise;
+
+export function StartServer():Promise;
+
+export function StopHTTPSService():Promise;
+
+export function StopHTTPService():Promise;
+
+export function StopMySQLService():Promise;
+
+export function StopPHPService():Promise;
+
+export function StopServer():Promise;
+
+export function UpdateSiteCache():Promise;
diff --git a/Backend/admin/frontend/wailsjs/wailsjs/go/admin/App.js b/Backend/admin/frontend/wailsjs/wailsjs/go/admin/App.js
new file mode 100644
index 0000000..b83a9b7
--- /dev/null
+++ b/Backend/admin/frontend/wailsjs/wailsjs/go/admin/App.js
@@ -0,0 +1,99 @@
+// @ts-check
+// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
+// This file is automatically generated. DO NOT EDIT
+
+export function CheckServicesReady() {
+ return window['go']['admin']['App']['CheckServicesReady']();
+}
+
+export function DisableProxyService() {
+ return window['go']['admin']['App']['DisableProxyService']();
+}
+
+export function EnableProxyService() {
+ return window['go']['admin']['App']['EnableProxyService']();
+}
+
+export function GetAllServicesStatus() {
+ return window['go']['admin']['App']['GetAllServicesStatus']();
+}
+
+export function GetConfig() {
+ return window['go']['admin']['App']['GetConfig']();
+}
+
+export function GetProxyList() {
+ return window['go']['admin']['App']['GetProxyList']();
+}
+
+export function GetSitesList() {
+ return window['go']['admin']['App']['GetSitesList']();
+}
+
+export function GetVAccessRules(arg1, arg2) {
+ return window['go']['admin']['App']['GetVAccessRules'](arg1, arg2);
+}
+
+export function OpenSiteFolder(arg1) {
+ return window['go']['admin']['App']['OpenSiteFolder'](arg1);
+}
+
+export function ReloadConfig() {
+ return window['go']['admin']['App']['ReloadConfig']();
+}
+
+export function RestartAllServices() {
+ return window['go']['admin']['App']['RestartAllServices']();
+}
+
+export function SaveConfig(arg1) {
+ return window['go']['admin']['App']['SaveConfig'](arg1);
+}
+
+export function SaveVAccessRules(arg1, arg2, arg3) {
+ return window['go']['admin']['App']['SaveVAccessRules'](arg1, arg2, arg3);
+}
+
+export function StartHTTPSService() {
+ return window['go']['admin']['App']['StartHTTPSService']();
+}
+
+export function StartHTTPService() {
+ return window['go']['admin']['App']['StartHTTPService']();
+}
+
+export function StartMySQLService() {
+ return window['go']['admin']['App']['StartMySQLService']();
+}
+
+export function StartPHPService() {
+ return window['go']['admin']['App']['StartPHPService']();
+}
+
+export function StartServer() {
+ return window['go']['admin']['App']['StartServer']();
+}
+
+export function StopHTTPSService() {
+ return window['go']['admin']['App']['StopHTTPSService']();
+}
+
+export function StopHTTPService() {
+ return window['go']['admin']['App']['StopHTTPService']();
+}
+
+export function StopMySQLService() {
+ return window['go']['admin']['App']['StopMySQLService']();
+}
+
+export function StopPHPService() {
+ return window['go']['admin']['App']['StopPHPService']();
+}
+
+export function StopServer() {
+ return window['go']['admin']['App']['StopServer']();
+}
+
+export function UpdateSiteCache() {
+ return window['go']['admin']['App']['UpdateSiteCache']();
+}
diff --git a/Backend/admin/frontend/wailsjs/wailsjs/go/models.ts b/Backend/admin/frontend/wailsjs/wailsjs/go/models.ts
new file mode 100644
index 0000000..a9d90af
--- /dev/null
+++ b/Backend/admin/frontend/wailsjs/wailsjs/go/models.ts
@@ -0,0 +1,174 @@
+export namespace proxy {
+
+ export class ProxyInfo {
+ enable: boolean;
+ external_domain: string;
+ local_address: string;
+ local_port: string;
+ service_https_use: boolean;
+ auto_https: boolean;
+ status: string;
+
+ static createFrom(source: any = {}) {
+ return new ProxyInfo(source);
+ }
+
+ constructor(source: any = {}) {
+ if ('string' === typeof source) source = JSON.parse(source);
+ this.enable = source["enable"];
+ this.external_domain = source["external_domain"];
+ this.local_address = source["local_address"];
+ this.local_port = source["local_port"];
+ this.service_https_use = source["service_https_use"];
+ this.auto_https = source["auto_https"];
+ this.status = source["status"];
+ }
+ }
+
+}
+
+export namespace services {
+
+ export class ServiceStatus {
+ name: string;
+ status: boolean;
+ port: string;
+ info: string;
+
+ static createFrom(source: any = {}) {
+ return new ServiceStatus(source);
+ }
+
+ constructor(source: any = {}) {
+ if ('string' === typeof source) source = JSON.parse(source);
+ this.name = source["name"];
+ this.status = source["status"];
+ this.port = source["port"];
+ this.info = source["info"];
+ }
+ }
+ export class AllServicesStatus {
+ http: ServiceStatus;
+ https: ServiceStatus;
+ mysql: ServiceStatus;
+ php: ServiceStatus;
+ proxy: ServiceStatus;
+
+ static createFrom(source: any = {}) {
+ return new AllServicesStatus(source);
+ }
+
+ constructor(source: any = {}) {
+ if ('string' === typeof source) source = JSON.parse(source);
+ this.http = this.convertValues(source["http"], ServiceStatus);
+ this.https = this.convertValues(source["https"], ServiceStatus);
+ this.mysql = this.convertValues(source["mysql"], ServiceStatus);
+ this.php = this.convertValues(source["php"], ServiceStatus);
+ this.proxy = this.convertValues(source["proxy"], ServiceStatus);
+ }
+
+ convertValues(a: any, classs: any, asMap: boolean = false): any {
+ if (!a) {
+ return a;
+ }
+ if (a.slice && a.map) {
+ return (a as any[]).map(elem => this.convertValues(elem, classs));
+ } else if ("object" === typeof a) {
+ if (asMap) {
+ for (const key of Object.keys(a)) {
+ a[key] = new classs(a[key]);
+ }
+ return a;
+ }
+ return new classs(a);
+ }
+ return a;
+ }
+ }
+
+}
+
+export namespace sites {
+
+ export class SiteInfo {
+ name: string;
+ host: string;
+ alias: string[];
+ status: string;
+ root_file: string;
+ root_file_routing: boolean;
+
+ static createFrom(source: any = {}) {
+ return new SiteInfo(source);
+ }
+
+ constructor(source: any = {}) {
+ if ('string' === typeof source) source = JSON.parse(source);
+ this.name = source["name"];
+ this.host = source["host"];
+ this.alias = source["alias"];
+ this.status = source["status"];
+ this.root_file = source["root_file"];
+ this.root_file_routing = source["root_file_routing"];
+ }
+ }
+
+}
+
+export namespace vaccess {
+
+ export class VAccessRule {
+ type: string;
+ type_file: string[];
+ path_access: string[];
+ ip_list: string[];
+ exceptions_dir: string[];
+ url_error: string;
+
+ static createFrom(source: any = {}) {
+ return new VAccessRule(source);
+ }
+
+ constructor(source: any = {}) {
+ if ('string' === typeof source) source = JSON.parse(source);
+ this.type = source["type"];
+ this.type_file = source["type_file"];
+ this.path_access = source["path_access"];
+ this.ip_list = source["ip_list"];
+ this.exceptions_dir = source["exceptions_dir"];
+ this.url_error = source["url_error"];
+ }
+ }
+ export class VAccessConfig {
+ rules: VAccessRule[];
+
+ static createFrom(source: any = {}) {
+ return new VAccessConfig(source);
+ }
+
+ constructor(source: any = {}) {
+ if ('string' === typeof source) source = JSON.parse(source);
+ this.rules = this.convertValues(source["rules"], VAccessRule);
+ }
+
+ convertValues(a: any, classs: any, asMap: boolean = false): any {
+ if (!a) {
+ return a;
+ }
+ if (a.slice && a.map) {
+ return (a as any[]).map(elem => this.convertValues(elem, classs));
+ } else if ("object" === typeof a) {
+ if (asMap) {
+ for (const key of Object.keys(a)) {
+ a[key] = new classs(a[key]);
+ }
+ return a;
+ }
+ return new classs(a);
+ }
+ return a;
+ }
+ }
+
+}
+
diff --git a/Backend/admin/frontend/wailsjs/wailsjs/runtime/package.json b/Backend/admin/frontend/wailsjs/wailsjs/runtime/package.json
new file mode 100644
index 0000000..1e7c8a5
--- /dev/null
+++ b/Backend/admin/frontend/wailsjs/wailsjs/runtime/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "@wailsapp/runtime",
+ "version": "2.0.0",
+ "description": "Wails Javascript runtime library",
+ "main": "runtime.js",
+ "types": "runtime.d.ts",
+ "scripts": {
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/wailsapp/wails.git"
+ },
+ "keywords": [
+ "Wails",
+ "Javascript",
+ "Go"
+ ],
+ "author": "Lea Anthony ",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/wailsapp/wails/issues"
+ },
+ "homepage": "https://github.com/wailsapp/wails#readme"
+}
diff --git a/Backend/admin/frontend/wailsjs/wailsjs/runtime/runtime.d.ts b/Backend/admin/frontend/wailsjs/wailsjs/runtime/runtime.d.ts
new file mode 100644
index 0000000..4445dac
--- /dev/null
+++ b/Backend/admin/frontend/wailsjs/wailsjs/runtime/runtime.d.ts
@@ -0,0 +1,249 @@
+/*
+ _ __ _ __
+| | / /___ _(_) /____
+| | /| / / __ `/ / / ___/
+| |/ |/ / /_/ / / (__ )
+|__/|__/\__,_/_/_/____/
+The electron alternative for Go
+(c) Lea Anthony 2019-present
+*/
+
+export interface Position {
+ x: number;
+ y: number;
+}
+
+export interface Size {
+ w: number;
+ h: number;
+}
+
+export interface Screen {
+ isCurrent: boolean;
+ isPrimary: boolean;
+ width : number
+ height : number
+}
+
+// Environment information such as platform, buildtype, ...
+export interface EnvironmentInfo {
+ buildType: string;
+ platform: string;
+ arch: string;
+}
+
+// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit)
+// emits the given event. Optional data may be passed with the event.
+// This will trigger any event listeners.
+export function EventsEmit(eventName: string, ...data: any): void;
+
+// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name.
+export function EventsOn(eventName: string, callback: (...data: any) => void): () => void;
+
+// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple)
+// sets up a listener for the given event name, but will only trigger a given number times.
+export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void;
+
+// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce)
+// sets up a listener for the given event name, but will only trigger once.
+export function EventsOnce(eventName: string, callback: (...data: any) => void): () => void;
+
+// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff)
+// unregisters the listener for the given event name.
+export function EventsOff(eventName: string, ...additionalEventNames: string[]): void;
+
+// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall)
+// unregisters all listeners.
+export function EventsOffAll(): void;
+
+// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint)
+// logs the given message as a raw message
+export function LogPrint(message: string): void;
+
+// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace)
+// logs the given message at the `trace` log level.
+export function LogTrace(message: string): void;
+
+// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug)
+// logs the given message at the `debug` log level.
+export function LogDebug(message: string): void;
+
+// [LogError](https://wails.io/docs/reference/runtime/log#logerror)
+// logs the given message at the `error` log level.
+export function LogError(message: string): void;
+
+// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal)
+// logs the given message at the `fatal` log level.
+// The application will quit after calling this method.
+export function LogFatal(message: string): void;
+
+// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo)
+// logs the given message at the `info` log level.
+export function LogInfo(message: string): void;
+
+// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning)
+// logs the given message at the `warning` log level.
+export function LogWarning(message: string): void;
+
+// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload)
+// Forces a reload by the main application as well as connected browsers.
+export function WindowReload(): void;
+
+// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp)
+// Reloads the application frontend.
+export function WindowReloadApp(): void;
+
+// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop)
+// Sets the window AlwaysOnTop or not on top.
+export function WindowSetAlwaysOnTop(b: boolean): void;
+
+// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme)
+// *Windows only*
+// Sets window theme to system default (dark/light).
+export function WindowSetSystemDefaultTheme(): void;
+
+// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme)
+// *Windows only*
+// Sets window to light theme.
+export function WindowSetLightTheme(): void;
+
+// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme)
+// *Windows only*
+// Sets window to dark theme.
+export function WindowSetDarkTheme(): void;
+
+// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter)
+// Centers the window on the monitor the window is currently on.
+export function WindowCenter(): void;
+
+// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle)
+// Sets the text in the window title bar.
+export function WindowSetTitle(title: string): void;
+
+// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen)
+// Makes the window full screen.
+export function WindowFullscreen(): void;
+
+// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen)
+// Restores the previous window dimensions and position prior to full screen.
+export function WindowUnfullscreen(): void;
+
+// [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen)
+// Returns the state of the window, i.e. whether the window is in full screen mode or not.
+export function WindowIsFullscreen(): Promise;
+
+// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize)
+// Sets the width and height of the window.
+export function WindowSetSize(width: number, height: number): void;
+
+// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize)
+// Gets the width and height of the window.
+export function WindowGetSize(): Promise;
+
+// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize)
+// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions.
+// Setting a size of 0,0 will disable this constraint.
+export function WindowSetMaxSize(width: number, height: number): void;
+
+// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize)
+// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions.
+// Setting a size of 0,0 will disable this constraint.
+export function WindowSetMinSize(width: number, height: number): void;
+
+// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition)
+// Sets the window position relative to the monitor the window is currently on.
+export function WindowSetPosition(x: number, y: number): void;
+
+// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition)
+// Gets the window position relative to the monitor the window is currently on.
+export function WindowGetPosition(): Promise;
+
+// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide)
+// Hides the window.
+export function WindowHide(): void;
+
+// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow)
+// Shows the window, if it is currently hidden.
+export function WindowShow(): void;
+
+// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise)
+// Maximises the window to fill the screen.
+export function WindowMaximise(): void;
+
+// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise)
+// Toggles between Maximised and UnMaximised.
+export function WindowToggleMaximise(): void;
+
+// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise)
+// Restores the window to the dimensions and position prior to maximising.
+export function WindowUnmaximise(): void;
+
+// [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised)
+// Returns the state of the window, i.e. whether the window is maximised or not.
+export function WindowIsMaximised(): Promise;
+
+// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise)
+// Minimises the window.
+export function WindowMinimise(): void;
+
+// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise)
+// Restores the window to the dimensions and position prior to minimising.
+export function WindowUnminimise(): void;
+
+// [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised)
+// Returns the state of the window, i.e. whether the window is minimised or not.
+export function WindowIsMinimised(): Promise;
+
+// [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal)
+// Returns the state of the window, i.e. whether the window is normal or not.
+export function WindowIsNormal(): Promise;
+
+// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour)
+// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels.
+export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void;
+
+// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall)
+// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system.
+export function ScreenGetAll(): Promise;
+
+// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl)
+// Opens the given URL in the system browser.
+export function BrowserOpenURL(url: string): void;
+
+// [Environment](https://wails.io/docs/reference/runtime/intro#environment)
+// Returns information about the environment
+export function Environment(): Promise;
+
+// [Quit](https://wails.io/docs/reference/runtime/intro#quit)
+// Quits the application.
+export function Quit(): void;
+
+// [Hide](https://wails.io/docs/reference/runtime/intro#hide)
+// Hides the application.
+export function Hide(): void;
+
+// [Show](https://wails.io/docs/reference/runtime/intro#show)
+// Shows the application.
+export function Show(): void;
+
+// [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext)
+// Returns the current text stored on clipboard
+export function ClipboardGetText(): Promise;
+
+// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext)
+// Sets a text on the clipboard
+export function ClipboardSetText(text: string): Promise;
+
+// [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop)
+// OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
+export function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void
+
+// [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff)
+// OnFileDropOff removes the drag and drop listeners and handlers.
+export function OnFileDropOff() :void
+
+// Check if the file path resolver is available
+export function CanResolveFilePaths(): boolean;
+
+// Resolves file paths for an array of files
+export function ResolveFilePaths(files: File[]): void
\ No newline at end of file
diff --git a/Backend/admin/frontend/wailsjs/wailsjs/runtime/runtime.js b/Backend/admin/frontend/wailsjs/wailsjs/runtime/runtime.js
new file mode 100644
index 0000000..7cb89d7
--- /dev/null
+++ b/Backend/admin/frontend/wailsjs/wailsjs/runtime/runtime.js
@@ -0,0 +1,242 @@
+/*
+ _ __ _ __
+| | / /___ _(_) /____
+| | /| / / __ `/ / / ___/
+| |/ |/ / /_/ / / (__ )
+|__/|__/\__,_/_/_/____/
+The electron alternative for Go
+(c) Lea Anthony 2019-present
+*/
+
+export function LogPrint(message) {
+ window.runtime.LogPrint(message);
+}
+
+export function LogTrace(message) {
+ window.runtime.LogTrace(message);
+}
+
+export function LogDebug(message) {
+ window.runtime.LogDebug(message);
+}
+
+export function LogInfo(message) {
+ window.runtime.LogInfo(message);
+}
+
+export function LogWarning(message) {
+ window.runtime.LogWarning(message);
+}
+
+export function LogError(message) {
+ window.runtime.LogError(message);
+}
+
+export function LogFatal(message) {
+ window.runtime.LogFatal(message);
+}
+
+export function EventsOnMultiple(eventName, callback, maxCallbacks) {
+ return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks);
+}
+
+export function EventsOn(eventName, callback) {
+ return EventsOnMultiple(eventName, callback, -1);
+}
+
+export function EventsOff(eventName, ...additionalEventNames) {
+ return window.runtime.EventsOff(eventName, ...additionalEventNames);
+}
+
+export function EventsOffAll() {
+ return window.runtime.EventsOffAll();
+}
+
+export function EventsOnce(eventName, callback) {
+ return EventsOnMultiple(eventName, callback, 1);
+}
+
+export function EventsEmit(eventName) {
+ let args = [eventName].slice.call(arguments);
+ return window.runtime.EventsEmit.apply(null, args);
+}
+
+export function WindowReload() {
+ window.runtime.WindowReload();
+}
+
+export function WindowReloadApp() {
+ window.runtime.WindowReloadApp();
+}
+
+export function WindowSetAlwaysOnTop(b) {
+ window.runtime.WindowSetAlwaysOnTop(b);
+}
+
+export function WindowSetSystemDefaultTheme() {
+ window.runtime.WindowSetSystemDefaultTheme();
+}
+
+export function WindowSetLightTheme() {
+ window.runtime.WindowSetLightTheme();
+}
+
+export function WindowSetDarkTheme() {
+ window.runtime.WindowSetDarkTheme();
+}
+
+export function WindowCenter() {
+ window.runtime.WindowCenter();
+}
+
+export function WindowSetTitle(title) {
+ window.runtime.WindowSetTitle(title);
+}
+
+export function WindowFullscreen() {
+ window.runtime.WindowFullscreen();
+}
+
+export function WindowUnfullscreen() {
+ window.runtime.WindowUnfullscreen();
+}
+
+export function WindowIsFullscreen() {
+ return window.runtime.WindowIsFullscreen();
+}
+
+export function WindowGetSize() {
+ return window.runtime.WindowGetSize();
+}
+
+export function WindowSetSize(width, height) {
+ window.runtime.WindowSetSize(width, height);
+}
+
+export function WindowSetMaxSize(width, height) {
+ window.runtime.WindowSetMaxSize(width, height);
+}
+
+export function WindowSetMinSize(width, height) {
+ window.runtime.WindowSetMinSize(width, height);
+}
+
+export function WindowSetPosition(x, y) {
+ window.runtime.WindowSetPosition(x, y);
+}
+
+export function WindowGetPosition() {
+ return window.runtime.WindowGetPosition();
+}
+
+export function WindowHide() {
+ window.runtime.WindowHide();
+}
+
+export function WindowShow() {
+ window.runtime.WindowShow();
+}
+
+export function WindowMaximise() {
+ window.runtime.WindowMaximise();
+}
+
+export function WindowToggleMaximise() {
+ window.runtime.WindowToggleMaximise();
+}
+
+export function WindowUnmaximise() {
+ window.runtime.WindowUnmaximise();
+}
+
+export function WindowIsMaximised() {
+ return window.runtime.WindowIsMaximised();
+}
+
+export function WindowMinimise() {
+ window.runtime.WindowMinimise();
+}
+
+export function WindowUnminimise() {
+ window.runtime.WindowUnminimise();
+}
+
+export function WindowSetBackgroundColour(R, G, B, A) {
+ window.runtime.WindowSetBackgroundColour(R, G, B, A);
+}
+
+export function ScreenGetAll() {
+ return window.runtime.ScreenGetAll();
+}
+
+export function WindowIsMinimised() {
+ return window.runtime.WindowIsMinimised();
+}
+
+export function WindowIsNormal() {
+ return window.runtime.WindowIsNormal();
+}
+
+export function BrowserOpenURL(url) {
+ window.runtime.BrowserOpenURL(url);
+}
+
+export function Environment() {
+ return window.runtime.Environment();
+}
+
+export function Quit() {
+ window.runtime.Quit();
+}
+
+export function Hide() {
+ window.runtime.Hide();
+}
+
+export function Show() {
+ window.runtime.Show();
+}
+
+export function ClipboardGetText() {
+ return window.runtime.ClipboardGetText();
+}
+
+export function ClipboardSetText(text) {
+ return window.runtime.ClipboardSetText(text);
+}
+
+/**
+ * Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
+ *
+ * @export
+ * @callback OnFileDropCallback
+ * @param {number} x - x coordinate of the drop
+ * @param {number} y - y coordinate of the drop
+ * @param {string[]} paths - A list of file paths.
+ */
+
+/**
+ * OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
+ *
+ * @export
+ * @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
+ * @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target)
+ */
+export function OnFileDrop(callback, useDropTarget) {
+ return window.runtime.OnFileDrop(callback, useDropTarget);
+}
+
+/**
+ * OnFileDropOff removes the drag and drop listeners and handlers.
+ */
+export function OnFileDropOff() {
+ return window.runtime.OnFileDropOff();
+}
+
+export function CanResolveFilePaths() {
+ return window.runtime.CanResolveFilePaths();
+}
+
+export function ResolveFilePaths(files) {
+ return window.runtime.ResolveFilePaths(files);
+}
\ No newline at end of file
diff --git a/Backend/admin/go/admin.go b/Backend/admin/go/admin.go
new file mode 100644
index 0000000..0cd1c23
--- /dev/null
+++ b/Backend/admin/go/admin.go
@@ -0,0 +1,320 @@
+package admin
+
+import (
+ "context"
+ "encoding/json"
+ "os"
+ "os/exec"
+ "time"
+
+ "github.com/wailsapp/wails/v2/pkg/runtime"
+
+ webserver "vServer/Backend/WebServer"
+ "vServer/Backend/admin/go/proxy"
+ "vServer/Backend/admin/go/services"
+ "vServer/Backend/admin/go/sites"
+ "vServer/Backend/admin/go/vaccess"
+ config "vServer/Backend/config"
+ tools "vServer/Backend/tools"
+)
+
+type App struct {
+ ctx context.Context
+}
+
+var appContext context.Context
+
+func NewApp() *App {
+ return &App{}
+}
+
+var isSingleInstance bool = false
+
+func (a *App) Startup(ctx context.Context) {
+ a.ctx = ctx
+ appContext = ctx
+
+ // Проверяем, не запущен ли уже vServer
+ if !tools.CheckSingleInstance() {
+ runtime.EventsEmit(ctx, "server:already_running", true)
+ // Только мониторинг, не запускаем сервисы
+ config.LoadConfig()
+ go a.monitorServices()
+ isSingleInstance = false
+ return
+ }
+
+ isSingleInstance = true
+
+ // Инициализируем время запуска
+ tools.ServerUptime("start")
+
+ // Загружаем конфигурацию
+ config.LoadConfig()
+ time.Sleep(50 * time.Millisecond)
+
+ // Запускаем handler
+ webserver.StartHandler()
+ time.Sleep(50 * time.Millisecond)
+
+ // Загружаем сертификаты
+ webserver.Cert_start()
+ time.Sleep(50 * time.Millisecond)
+
+ // Запускаем серверы
+ go webserver.StartHTTPS()
+ time.Sleep(50 * time.Millisecond)
+
+ go webserver.StartHTTP()
+ time.Sleep(50 * time.Millisecond)
+
+ // Запускаем PHP
+ webserver.PHP_Start()
+ time.Sleep(50 * time.Millisecond)
+
+ // Запускаем MySQL асинхронно
+ go webserver.StartMySQLServer(false)
+
+ // Запускаем мониторинг статусов
+ go a.monitorServices()
+}
+
+func (a *App) GetAllServicesStatus() services.AllServicesStatus {
+ return services.GetAllServicesStatus()
+}
+
+func (a *App) CheckServicesReady() bool {
+ status := services.GetAllServicesStatus()
+ return status.HTTP.Status && status.HTTPS.Status && status.MySQL.Status && status.PHP.Status
+}
+
+func (a *App) Shutdown(ctx context.Context) {
+ // Останавливаем все сервисы при закрытии приложения
+ if isSingleInstance {
+ webserver.StopHTTPServer()
+ webserver.StopHTTPSServer()
+ webserver.PHP_Stop()
+ webserver.StopMySQLServer()
+
+ // Освобождаем мьютекс
+ tools.ReleaseMutex()
+ }
+}
+
+func (a *App) monitorServices() {
+ time.Sleep(1 * time.Second) // Ждём секунду перед первой проверкой
+
+ for {
+ time.Sleep(500 * time.Millisecond)
+ status := services.GetAllServicesStatus()
+ runtime.EventsEmit(appContext, "service:changed", status)
+ }
+}
+
+func (a *App) GetSitesList() []sites.SiteInfo {
+ return sites.GetSitesList()
+}
+
+func (a *App) GetProxyList() []proxy.ProxyInfo {
+ return proxy.GetProxyList()
+}
+
+func (a *App) StartServer() string {
+ webserver.Cert_start()
+
+ go webserver.StartHTTPS()
+ go webserver.StartHTTP()
+
+ webserver.PHP_Start()
+ go webserver.StartMySQLServer(false)
+
+ return "Server started"
+}
+
+func (a *App) StopServer() string {
+ webserver.StopHTTPServer()
+ webserver.StopHTTPSServer()
+ webserver.PHP_Stop()
+ webserver.StopMySQLServer()
+
+ return "Server stopped"
+}
+
+func (a *App) ReloadConfig() string {
+ config.LoadConfig()
+ return "Config reloaded"
+}
+
+func (a *App) GetConfig() interface{} {
+ return config.ConfigData
+}
+
+func (a *App) SaveConfig(configJSON string) string {
+ // Форматируем JSON перед сохранением
+ var tempConfig interface{}
+ err := json.Unmarshal([]byte(configJSON), &tempConfig)
+ if err != nil {
+ return "Error: Invalid JSON"
+ }
+
+ formattedJSON, err := json.MarshalIndent(tempConfig, "", " ")
+ if err != nil {
+ return "Error: " + err.Error()
+ }
+
+ // Сохранение конфига в файл
+ err = os.WriteFile(config.ConfigPath, formattedJSON, 0644)
+ if err != nil {
+ return "Error: " + err.Error()
+ }
+
+ // Перезагружаем конфигурацию
+ config.LoadConfig()
+ return "Config saved"
+}
+
+func (a *App) RestartAllServices() string {
+ // Останавливаем все сервисы
+ webserver.StopHTTPServer()
+ webserver.StopHTTPSServer()
+ webserver.PHP_Stop()
+ webserver.StopMySQLServer()
+ time.Sleep(500 * time.Millisecond)
+
+ // Перезагружаем конфиг
+ config.LoadConfig()
+ time.Sleep(200 * time.Millisecond)
+
+ // Обновляем кэш статусов сайтов
+ webserver.UpdateSiteStatusCache()
+
+ // Перезагружаем сертификаты
+ webserver.Cert_start()
+ time.Sleep(50 * time.Millisecond)
+
+ // Запускаем серверы заново
+ go webserver.StartHTTPS()
+ time.Sleep(50 * time.Millisecond)
+
+ go webserver.StartHTTP()
+ time.Sleep(50 * time.Millisecond)
+
+ webserver.PHP_Start()
+ time.Sleep(200 * time.Millisecond)
+
+ go webserver.StartMySQLServer(false)
+
+ return "All services restarted"
+}
+
+// Управление отдельными сервисами
+func (a *App) StartHTTPService() string {
+ // Обновляем кэш перед запуском
+ webserver.UpdateSiteStatusCache()
+ go webserver.StartHTTP()
+ return "HTTP started"
+}
+
+func (a *App) StopHTTPService() string {
+ webserver.StopHTTPServer()
+ return "HTTP stopped"
+}
+
+func (a *App) StartHTTPSService() string {
+ // Обновляем кэш перед запуском
+ webserver.UpdateSiteStatusCache()
+ go webserver.StartHTTPS()
+ return "HTTPS started"
+}
+
+func (a *App) StopHTTPSService() string {
+ webserver.StopHTTPSServer()
+ return "HTTPS stopped"
+}
+
+func (a *App) StartMySQLService() string {
+ go webserver.StartMySQLServer(false)
+ return "MySQL started"
+}
+
+func (a *App) StopMySQLService() string {
+ webserver.StopMySQLServer()
+ return "MySQL stopped"
+}
+
+func (a *App) StartPHPService() string {
+ webserver.PHP_Start()
+ return "PHP started"
+}
+
+func (a *App) StopPHPService() string {
+ webserver.PHP_Stop()
+ return "PHP stopped"
+}
+
+func (a *App) EnableProxyService() string {
+ config.ConfigData.Soft_Settings.Proxy_enabled = true
+
+ // Сохраняем в файл
+ configJSON, _ := json.Marshal(config.ConfigData)
+ os.WriteFile(config.ConfigPath, configJSON, 0644)
+
+ return "Proxy enabled"
+}
+
+func (a *App) DisableProxyService() string {
+ config.ConfigData.Soft_Settings.Proxy_enabled = false
+
+ // Сохраняем в файл
+ configJSON, _ := json.Marshal(config.ConfigData)
+ os.WriteFile(config.ConfigPath, configJSON, 0644)
+
+ return "Proxy disabled"
+}
+
+func (a *App) OpenSiteFolder(host string) string {
+ folderPath := "WebServer/www/" + host
+
+ // Получаем абсолютный путь
+ absPath, err := tools.AbsPath(folderPath)
+ if err != nil {
+ return "Error: " + err.Error()
+ }
+
+ // Открываем папку в проводнике
+ cmd := exec.Command("explorer", absPath)
+ err = cmd.Start()
+ if err != nil {
+ return "Error: " + err.Error()
+ }
+
+ return "Folder opened"
+}
+
+func (a *App) GetVAccessRules(host string, isProxy bool) *vaccess.VAccessConfig {
+ config, err := vaccess.GetVAccessConfig(host, isProxy)
+ if err != nil {
+ return &vaccess.VAccessConfig{Rules: []vaccess.VAccessRule{}}
+ }
+ return config
+}
+
+func (a *App) SaveVAccessRules(host string, isProxy bool, configJSON string) string {
+ var config vaccess.VAccessConfig
+ err := json.Unmarshal([]byte(configJSON), &config)
+ if err != nil {
+ return "Error: Invalid JSON"
+ }
+
+ err = vaccess.SaveVAccessConfig(host, isProxy, &config)
+ if err != nil {
+ return "Error: " + err.Error()
+ }
+
+ return "vAccess saved"
+}
+
+func (a *App) UpdateSiteCache() string {
+ webserver.UpdateSiteStatusCache()
+ return "Cache updated"
+}
diff --git a/Backend/admin/go/admin_server.go b/Backend/admin/go/admin_server.go
deleted file mode 100644
index 9ff9a90..0000000
--- a/Backend/admin/go/admin_server.go
+++ /dev/null
@@ -1,46 +0,0 @@
-package admin
-
-import (
- "net/http"
- command "vServer/Backend/admin/go/command"
- config "vServer/Backend/config"
- tools "vServer/Backend/tools"
-)
-
-var adminServer *http.Server
-
-// Запуск Admin сервера
-func StartAdmin() {
-
- // Получаем значения из конфига во время выполнения
- port_admin := config.ConfigData.Soft_Settings.Admin_port
- host_admin := config.ConfigData.Soft_Settings.Admin_host
-
- if tools.Port_check("ADMIN", host_admin, port_admin) {
- return
- }
-
- // Создаем оптимизированный мультиплексор для админ сервера
- mux := http.NewServeMux()
-
- // Регистрируем специализированные обработчики (быстрая маршрутизация)
- mux.HandleFunc("/api/", command.ApiHandler) // API эндпоинты
- mux.HandleFunc("/json/", command.JsonHandler) // JSON данные
- mux.HandleFunc("/service/", command.ServiceHandler) // Сервисные команды POST
- mux.HandleFunc("/", command.StaticHandler) // Статические файлы
-
- // Создаем Admin сервер (только localhost для безопасности)
- adminServer = &http.Server{
- Addr: host_admin + ":" + port_admin,
- Handler: mux,
- }
-
- tools.Logs_file(0, "ADMIN", "🛠️ Admin панель запущена на порту "+port_admin, "logs_http.log", true)
-
- if err := adminServer.ListenAndServe(); err != nil {
- // Игнорируем нормальную ошибку при остановке сервера
- if err.Error() != "http: Server closed" {
- tools.Logs_file(1, "ADMIN", "❌ Ошибка запуска админ сервера: "+err.Error(), "logs_http.log", true)
- }
- }
-}
diff --git a/Backend/admin/go/command/commands.go b/Backend/admin/go/command/commands.go
deleted file mode 100644
index 6721f64..0000000
--- a/Backend/admin/go/command/commands.go
+++ /dev/null
@@ -1,122 +0,0 @@
-package command
-
-import (
- "io/fs"
- "net/http"
- "path/filepath"
- "strings"
- webserver "vServer/Backend/WebServer"
- admin "vServer/Backend/admin"
- json "vServer/Backend/admin/go/json"
-)
-
-func SecurePost(w http.ResponseWriter, r *http.Request) bool {
- // Проверяем, что запрос POST (не GET из браузера)
-
- if webserver.Secure_post {
-
- if r.Method != "POST" {
- http.Error(w, "Метод не разрешен. Используйте POST", http.StatusMethodNotAllowed)
- return false
- }
-
- }
-
- return true
-}
-
-// API обработчик для /api/*
-func ApiHandler(w http.ResponseWriter, r *http.Request) {
- path := r.URL.Path
-
- switch path {
- case "/api/metrics":
- json.GetAllMetrics(w)
- default:
- http.NotFound(w, r)
- }
-}
-
-// JSON обработчик для /json/*
-func JsonHandler(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Content-Type", "application/json")
- path := r.URL.Path
-
- switch path {
- case "/json/server_status.json":
- w.Write(json.GetServerStatusJSON())
- case "/json/menu.json":
- w.Write(json.GetMenuJSON())
- default:
- http.NotFound(w, r)
- }
-}
-
-// Обработчик сервисных команд для /service/*
-func ServiceHandler(w http.ResponseWriter, r *http.Request) {
- path := r.URL.Path
-
- if !SecurePost(w, r) {
- return
- }
-
- if SiteList(w, r, path) {
- return
- }
-
- if site_add(w, path) {
- return
- }
-
- if Service_Run(w, r, path) {
- return
- }
- http.NotFound(w, r)
-}
-
-// Обработчик статических файлов из embed
-func StaticHandler(w http.ResponseWriter, r *http.Request) {
- path := r.URL.Path
-
- // Убираем ведущий слэш
- path = strings.TrimPrefix(path, "/")
-
- // Если пустой путь, показываем index.html
- if path == "" {
- path = "html/index.html"
- } else {
- path = "html/" + path
- }
-
- // Читаем файл из файловой системы (embed или диск)
- fileSystem := admin.GetFileSystem()
- content, err := fs.ReadFile(fileSystem, path)
- if err != nil {
- http.NotFound(w, r)
- return
- }
-
- // Устанавливаем правильный Content-Type
- ext := filepath.Ext(path)
- switch ext {
- case ".css":
- w.Header().Set("Content-Type", "text/css")
- case ".js":
- w.Header().Set("Content-Type", "application/javascript")
- case ".json":
- w.Header().Set("Content-Type", "application/json")
- case ".html":
- w.Header().Set("Content-Type", "text/html")
- }
-
- // Предотвращаем кеширование в режиме разработки (когда UseEmbedded = false)
- if !admin.UseEmbedded {
- w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
- w.Header().Set("Pragma", "no-cache")
- w.Header().Set("Expires", "0")
- }
-
- // Отдаем файл
- w.Write(content)
-
-}
diff --git a/Backend/admin/go/command/service_run.go b/Backend/admin/go/command/service_run.go
deleted file mode 100644
index df9b426..0000000
--- a/Backend/admin/go/command/service_run.go
+++ /dev/null
@@ -1,57 +0,0 @@
-package command
-
-import (
- "net/http"
- webserver "vServer/Backend/WebServer"
- json "vServer/Backend/admin/go/json"
-)
-
-// Обработчик команд управления серверами
-func Service_Run(w http.ResponseWriter, r *http.Request, path string) bool {
-
- switch path {
- case "/service/MySql_Stop":
- webserver.StopMySQLServer()
- json.UpdateServerStatus("MySQL Server", "stopped")
- return true
-
- case "/service/MySql_Start":
- webserver.StartMySQLServer(false)
- json.UpdateServerStatus("MySQL Server", "running")
- return true
-
- case "/service/Http_Stop":
- webserver.StopHTTPServer()
- json.UpdateServerStatus("HTTP Server", "stopped")
- return true
-
- case "/service/Http_Start":
- go webserver.StartHTTP()
- json.UpdateServerStatus("HTTP Server", "running")
- return true
-
- case "/service/Https_Stop":
- webserver.StopHTTPSServer()
- json.UpdateServerStatus("HTTPS Server", "stopped")
- return true
-
- case "/service/Https_Start":
- go webserver.StartHTTPS()
- json.UpdateServerStatus("HTTPS Server", "running")
- return true
-
- case "/service/Php_Start":
- webserver.PHP_Start()
- json.UpdateServerStatus("PHP Server", "running")
- return true
-
- case "/service/Php_Stop":
- webserver.PHP_Stop()
- json.UpdateServerStatus("PHP Server", "stopped")
- return true
-
- default:
- http.NotFound(w, r)
- return false // Команда не найдена
- }
-}
diff --git a/Backend/admin/go/command/site_list.go b/Backend/admin/go/command/site_list.go
deleted file mode 100644
index 15200c8..0000000
--- a/Backend/admin/go/command/site_list.go
+++ /dev/null
@@ -1,269 +0,0 @@
-package command
-
-import (
- "encoding/json"
- "net/http"
- "os"
- "regexp"
- "strings"
- "vServer/Backend/config"
- "vServer/Backend/tools"
-)
-
-var entries []os.DirEntry
-
-func path_www() {
-
- wwwPath, err := tools.AbsPath("WebServer/www")
- tools.Error_check(err, "Ошибка получения пути")
-
- entries, err = os.ReadDir(wwwPath)
- tools.Error_check(err, "Ошибка чтения директории")
-
-}
-
-func site_type(entry os.DirEntry) string {
-
- certPath, err := tools.AbsPath("WebServer/cert/" + entry.Name())
- if err == nil {
- if _, err := os.Stat(certPath); err == nil {
- return "https"
- }
- }
- return "http"
-}
-
-func site_alliace(siteName string) []string {
- // Получаем абсолютный путь к config.json
- configPath, err := tools.AbsPath("WebServer/config.json")
- tools.Error_check(err, "Ошибка получения пути к config.json")
-
- // Читаем содержимое config.json
- configData, err := os.ReadFile(configPath)
- tools.Error_check(err, "Ошибка чтения config.json")
-
- // Структура для парсинга Site_www
- type SiteConfig struct {
- Name string `json:"name"`
- Host string `json:"host"`
- Alias []string `json:"alias"`
- Status string `json:"status"`
- }
- type Config struct {
- SiteWWW []SiteConfig `json:"Site_www"`
- }
-
- var config Config
- err = json.Unmarshal(configData, &config)
- tools.Error_check(err, "Ошибка парсинга config.json")
-
- // Ищем алиасы для конкретного сайта
- for _, site := range config.SiteWWW {
- if site.Host == siteName {
- return site.Alias
- }
- }
-
- // Возвращаем пустой массив если сайт не найден
- return []string{}
-}
-
-func site_status(siteName string) string {
- configPath := "WebServer/config.json"
-
- // Читаем конфигурационный файл
- data, err := os.ReadFile(configPath)
- if err != nil {
- return "error"
- }
-
- // Парсим JSON
- var config map[string]interface{}
- if err := json.Unmarshal(data, &config); err != nil {
- return "error"
- }
-
- // Получаем список сайтов
- siteWww, ok := config["Site_www"].([]interface{})
- if !ok {
- return "error"
- }
-
- // Ищем сайт по host
- for _, siteInterface := range siteWww {
- site, ok := siteInterface.(map[string]interface{})
- if !ok {
- continue
- }
-
- // Проверяем только по host (имя папки)
- if host, ok := site["host"].(string); ok && host == siteName {
- if status, ok := site["status"].(string); ok {
- return status
- }
- }
- }
-
- return "inactive"
-}
-
-func SiteList(w http.ResponseWriter, r *http.Request, path string) bool {
-
- switch path {
-
- case "/service/Site_List":
- sites := []map[string]interface{}{}
-
- path_www()
-
- for _, entry := range entries {
- if entry.IsDir() {
- site := map[string]interface{}{
- "host": entry.Name(),
- "type": site_type(entry),
- "aliases": site_alliace(entry.Name()),
- "status": site_status(entry.Name()),
- }
- sites = append(sites, site)
- }
- }
-
- metrics := map[string]interface{}{
- "sites": sites,
- }
-
- data, _ := json.MarshalIndent(metrics, "", " ")
- w.Write(data)
-
- return true
-
- }
-
- return false
-
-}
-
-func addSiteToConfig(siteName string) error {
- // Получаем абсолютный путь к config.json
- configPath, err := tools.AbsPath("WebServer/config.json")
- if err != nil {
- return err
- }
-
- // Читаем содержимое config.json
- configData, err := os.ReadFile(configPath)
- if err != nil {
- return err
- }
-
- // Парсим как общий JSON объект
- var config map[string]interface{}
- err = json.Unmarshal(configData, &config)
- if err != nil {
- return err
- }
-
- // Получаем массив сайтов
- siteWWW, ok := config["Site_www"].([]interface{})
- if !ok {
- siteWWW = []interface{}{}
- }
-
- // Создаем новый сайт в том же формате что уже есть
- newSite := map[string]interface{}{
- "name": siteName,
- "host": siteName,
- "alias": []string{""},
- "status": "active",
- }
-
- // Добавляем новый сайт в массив
- config["Site_www"] = append(siteWWW, newSite)
-
- // Сохраняем обновленный конфиг
- updatedData, err := json.MarshalIndent(config, "", " ")
- if err != nil {
- return err
- }
-
- // Делаем массивы алиасов в одну строку
- re := regexp.MustCompile(`"alias": \[\s+"([^"]*?)"\s+\]`)
- compactData := re.ReplaceAll(updatedData, []byte(`"alias": ["$1"]`))
-
- // Исправляем отступы после Site_www
- dataStr := string(compactData)
- dataStr = strings.ReplaceAll(dataStr, ` ],
- "Soft_Settings"`, ` ],
-
- "Soft_Settings"`)
- compactData = []byte(dataStr)
-
- err = os.WriteFile(configPath, compactData, 0644)
- if err != nil {
- return err
- }
-
- return nil
-}
-
-func site_add(w http.ResponseWriter, path string) bool {
-
- // URL параметры: /service/Site_Add/sitename
- if strings.HasPrefix(path, "/service/Site_Add/") {
- siteName := strings.TrimPrefix(path, "/service/Site_Add/")
-
- if siteName == "" {
- w.WriteHeader(400)
- w.Write([]byte(`{"status":"error","message":"Не указано имя сайта в URL"}`))
- return true
- }
-
- wwwPath := "WebServer/www/" + siteName
-
- // Проверяем существует ли уже такой сайт
- if _, err := os.Stat(wwwPath); err == nil {
- w.WriteHeader(409) // Conflict
- w.Write([]byte(`{"status":"error","message":"Сайт ` + siteName + ` уже существует"}`))
- return true
- }
-
- err := os.MkdirAll(wwwPath, 0755)
- if err != nil {
- w.WriteHeader(500)
- w.Write([]byte(`{"status":"error","message":"Ошибка создания папки сайта"}`))
- return true
- }
-
- // Добавляем сайт в config.json
- err = addSiteToConfig(siteName)
- if err != nil {
- w.WriteHeader(500)
- w.Write([]byte(`{"status":"error","message":"Ошибка добавления в конфигурацию: ` + err.Error() + `"}`))
- return true
- }
-
- // Создаем папку public_www
- publicWwwPath := wwwPath + "/public_www"
- err = os.MkdirAll(publicWwwPath, 0755)
- if err != nil {
- w.WriteHeader(500)
- w.Write([]byte(`{"status":"error","message":"Ошибка создания папки public_www"}`))
- return true
- }
-
- indexFilePath := wwwPath + "/public_www/index.html"
- indexContent := "Привет друг! Твой сайт создан!"
- err = os.WriteFile(indexFilePath, []byte(indexContent), 0644)
- if err != nil {
- w.WriteHeader(500)
- w.Write([]byte(`{"status":"error","message":"Ошибка создания index.html: ` + err.Error() + `"}`))
- return true
- }
-
- w.Write([]byte(`{"status":"ok","message":"Сайт ` + siteName + ` успешно создан и добавлен в конфигурацию"}`))
- config.LoadConfig()
- return true
- }
-
- return false
-}
diff --git a/Backend/admin/go/json/json.go b/Backend/admin/go/json/json.go
deleted file mode 100644
index f41ea81..0000000
--- a/Backend/admin/go/json/json.go
+++ /dev/null
@@ -1,45 +0,0 @@
-package json
-
-import "encoding/json"
-
-// Данные серверов
-var ServerStatus = []map[string]interface{}{
- {"NameService": "HTTP Server", "Port": 80, "Status": "stopped"},
- {"NameService": "HTTPS Server", "Port": 443, "Status": "stopped"},
- {"NameService": "PHP Server", "Port": 9000, "Status": "stopped"},
- {"NameService": "MySQL Server", "Port": 3306, "Status": "stopped"},
-}
-
-// Данные меню
-var MenuData = []map[string]interface{}{
- {"name": "Dashboard", "icon": "🏠", "url": "#dashboard", "active": true},
- {"name": "Серверы", "icon": "🖥️", "url": "#servers", "active": false},
- {"name": "Сайты", "icon": "🌐", "url": "#sites", "active": false},
- {"name": "SSL Сертификаты", "icon": "🔒", "url": "#certificates", "active": false},
- {"name": "Файловый менеджер", "icon": "📁", "url": "#files", "active": false},
- {"name": "Базы данных", "icon": "🗄️", "url": "#databases", "active": false},
- {"name": "Логи", "icon": "📋", "url": "#logs", "active": false},
- {"name": "Настройки", "icon": "⚙️", "url": "#settings", "active": false},
-}
-
-// Функция обновления статуса сервера
-func UpdateServerStatus(serviceName, status string) {
- for i := range ServerStatus {
- if ServerStatus[i]["NameService"] == serviceName {
- ServerStatus[i]["Status"] = status
- break
- }
- }
-}
-
-// Получить JSON серверов
-func GetServerStatusJSON() []byte {
- data, _ := json.Marshal(ServerStatus)
- return data
-}
-
-// Получить JSON меню
-func GetMenuJSON() []byte {
- data, _ := json.Marshal(MenuData)
- return data
-}
diff --git a/Backend/admin/go/json/metrics.go b/Backend/admin/go/json/metrics.go
deleted file mode 100644
index c09170d..0000000
--- a/Backend/admin/go/json/metrics.go
+++ /dev/null
@@ -1,130 +0,0 @@
-package json
-
-import (
- "encoding/json"
- "net/http"
- "strings"
- "time"
- "vServer/Backend/tools"
-)
-
-var CPU_NAME string
-var CPU_GHz string
-var CPU_Cores string
-var CPU_Using string
-var Disk_Size string
-var Disk_Free string
-var Disk_Used string
-var Disk_Name string
-var RAM_Using string
-var RAM_Total string
-
-// Инициализация при запуске пакета
-func init() {
- // Загружаем статичные данные один раз при старте
- UpdateMetrics()
- // Загружаем динамические данные в фоне
- go updateMetricsBackground()
-}
-
-func UpdateMetrics() {
- commands := []string{
- `$name = (Get-WmiObject Win32_DiskDrive | Where-Object { $_.Index -eq (Get-WmiObject Win32_DiskPartition | Where-Object { $_.DeviceID -eq ((Get-WmiObject Win32_LogicalDiskToPartition | Where-Object { $_.Dependent -match "C:" }).Antecedent -split '"')[1] }).DiskIndex }).Model`,
- `$size = "{0} GB" -f [math]::Round(((Get-PSDrive -Name C).Used + (Get-PSDrive -Name C).Free) / 1GB)`,
- "$cpuInfo = Get-CimInstance Win32_Processor",
- "$cpuCores = $cpuInfo.NumberOfCores",
- "$cpuName = $cpuInfo.Name",
- `$ram_total = [math]::Round((Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory / 1GB, 2)`,
- "Write-Output \"$cpuName|$cpuCores|$name|$size|$ram_total\"",
- }
-
- // Выполняем команды и получаем результат
- result := tools.RunPersistentScript(commands)
-
- // Парсим результат для статичных данных
- parts := strings.Split(result, "|")
- if len(parts) >= 4 {
- cpuName := strings.TrimSpace(parts[0])
- cpuCores := strings.TrimSpace(parts[1])
- diskName := strings.TrimSpace(parts[2])
- diskSize := strings.TrimSpace(parts[3])
- ramTotal := strings.TrimSpace(parts[4])
-
- // Обновляем глобальные переменные
- CPU_NAME = cpuName
- CPU_Cores = cpuCores
- Disk_Name = diskName
- Disk_Size = diskSize
- RAM_Total = ramTotal
- }
-}
-
-// Фоновое обновление динамических метрик (внутреннее)
-func updateMetricsBackground() {
-
- updateDynamicMetrics := func() {
- commands := []string{
- "$cpuInfo = Get-CimInstance Win32_Processor",
- "$cpuGHz = $cpuInfo.MaxClockSpeed",
- "$cpuUsage = $cpuInfo.LoadPercentage",
- `$used = "{0} GB" -f [math]::Round(((Get-PSDrive -Name C).Used / 1GB))`,
- `$free = "{0} GB" -f [math]::Round(((Get-PSDrive -Name C).Free / 1GB))`,
- `$ram_using = [math]::Round((Get-CimInstance Win32_OperatingSystem | % {($_.TotalVisibleMemorySize - $_.FreePhysicalMemory) / 1MB}), 2)`,
- "Write-Output \"$cpuGHz|$cpuUsage|$used|$free|$ram_using\"",
- }
-
- // Один запуск PowerShell для динамических команд!
- result := tools.RunPersistentScript(commands)
-
- // Парсим результат для динамических данных
- parts := strings.Split(result, "|")
- if len(parts) >= 4 {
- cpuGHz := strings.TrimSpace(parts[0])
- cpuUsage := strings.TrimSpace(parts[1])
- diskUsed := strings.TrimSpace(parts[2])
- diskFree := strings.TrimSpace(parts[3])
- ramUsing := strings.TrimSpace(parts[4])
- // Обновляем глобальные переменные
- CPU_GHz = cpuGHz
- CPU_Using = cpuUsage
- Disk_Used = diskUsed
- Disk_Free = diskFree
- RAM_Using = ramUsing
- }
- }
-
- // Выполняем сразу при запуске
- updateDynamicMetrics()
-
- // Затем каждые 5 секунд
- for range time.NewTicker(5 * time.Second).C {
- updateDynamicMetrics()
- }
-
-}
-
-// Получить JSON системных метрик
-func GetAllMetrics(w http.ResponseWriter) {
- w.Header().Set("Content-Type", "application/json")
- w.Write(GetMetricsJSON())
-}
-
-// Получить JSON метрик
-func GetMetricsJSON() []byte {
- metrics := map[string]interface{}{
- "cpu_name": CPU_NAME,
- "cpu_ghz": CPU_GHz,
- "cpu_cores": CPU_Cores,
- "cpu_usage": CPU_Using,
- "disk_name": Disk_Name,
- "disk_size": Disk_Size,
- "disk_used": Disk_Used,
- "disk_free": Disk_Free,
- "ram_using": RAM_Using,
- "ram_total": RAM_Total,
- "server_uptime": tools.ServerUptime("get"),
- }
-
- data, _ := json.Marshal(metrics)
- return data
-}
diff --git a/Backend/admin/go/proxy/proxy.go b/Backend/admin/go/proxy/proxy.go
new file mode 100644
index 0000000..b9a8e42
--- /dev/null
+++ b/Backend/admin/go/proxy/proxy.go
@@ -0,0 +1,30 @@
+package proxy
+
+import (
+ config "vServer/Backend/config"
+)
+
+func GetProxyList() []ProxyInfo {
+ proxies := make([]ProxyInfo, 0)
+
+ for _, proxyConfig := range config.ConfigData.Proxy_Service {
+ status := "disabled"
+ if proxyConfig.Enable {
+ status = "active"
+ }
+
+ proxyInfo := ProxyInfo{
+ Enable: proxyConfig.Enable,
+ ExternalDomain: proxyConfig.ExternalDomain,
+ LocalAddress: proxyConfig.LocalAddress,
+ LocalPort: proxyConfig.LocalPort,
+ ServiceHTTPSuse: proxyConfig.ServiceHTTPSuse,
+ AutoHTTPS: proxyConfig.AutoHTTPS,
+ Status: status,
+ }
+ proxies = append(proxies, proxyInfo)
+ }
+
+ return proxies
+}
+
diff --git a/Backend/admin/go/proxy/types.go b/Backend/admin/go/proxy/types.go
new file mode 100644
index 0000000..2b008cb
--- /dev/null
+++ b/Backend/admin/go/proxy/types.go
@@ -0,0 +1,12 @@
+package proxy
+
+type ProxyInfo struct {
+ Enable bool `json:"enable"`
+ ExternalDomain string `json:"external_domain"`
+ LocalAddress string `json:"local_address"`
+ LocalPort string `json:"local_port"`
+ ServiceHTTPSuse bool `json:"service_https_use"`
+ AutoHTTPS bool `json:"auto_https"`
+ Status string `json:"status"`
+}
+
diff --git a/Backend/admin/go/services/services.go b/Backend/admin/go/services/services.go
new file mode 100644
index 0000000..26f32c0
--- /dev/null
+++ b/Backend/admin/go/services/services.go
@@ -0,0 +1,92 @@
+package services
+
+import (
+ "fmt"
+ webserver "vServer/Backend/WebServer"
+ config "vServer/Backend/config"
+)
+
+func GetAllServicesStatus() AllServicesStatus {
+ return AllServicesStatus{
+ HTTP: getHTTPStatus(),
+ HTTPS: getHTTPSStatus(),
+ MySQL: getMySQLStatus(),
+ PHP: getPHPStatus(),
+ Proxy: getProxyStatus(),
+ }
+}
+
+func getHTTPStatus() ServiceStatus {
+ // Используем внутренний статус вместо TCP проверки
+ return ServiceStatus{
+ Name: "HTTP",
+ Status: webserver.GetHTTPStatus(),
+ Port: "80",
+ Info: "",
+ }
+}
+
+func getHTTPSStatus() ServiceStatus {
+ // Используем внутренний статус вместо TCP проверки
+ return ServiceStatus{
+ Name: "HTTPS",
+ Status: webserver.GetHTTPSStatus(),
+ Port: "443",
+ Info: "",
+ }
+}
+
+func getMySQLStatus() ServiceStatus {
+ port := fmt.Sprintf("%d", config.ConfigData.Soft_Settings.Mysql_port)
+
+ // Используем внутренний статус вместо TCP проверки
+ // чтобы не вызывать connect_errors в MySQL
+ return ServiceStatus{
+ Name: "MySQL",
+ Status: webserver.GetMySQLStatus(),
+ Port: port,
+ Info: "",
+ }
+}
+
+func getPHPStatus() ServiceStatus {
+ basePort := config.ConfigData.Soft_Settings.Php_port
+
+ // Диапазон портов для 4 воркеров
+ portRange := fmt.Sprintf("%d-%d", basePort, basePort+3)
+
+ // Используем внутренний статус вместо TCP проверки
+ return ServiceStatus{
+ Name: "PHP",
+ Status: webserver.GetPHPStatus(),
+ Port: portRange,
+ Info: "",
+ }
+}
+
+func getProxyStatus() ServiceStatus {
+ activeCount := 0
+ totalCount := len(config.ConfigData.Proxy_Service)
+
+ for _, proxy := range config.ConfigData.Proxy_Service {
+ if proxy.Enable {
+ activeCount++
+ }
+ }
+
+ info := fmt.Sprintf("%d из %d", activeCount, totalCount)
+
+ // Проверяем глобальный флаг и статус HTTP/HTTPS
+ proxyEnabled := config.ConfigData.Soft_Settings.Proxy_enabled
+ httpRunning := webserver.GetHTTPStatus()
+ httpsRunning := webserver.GetHTTPSStatus()
+
+ status := proxyEnabled && (httpRunning || httpsRunning)
+
+ return ServiceStatus{
+ Name: "Proxy",
+ Status: status,
+ Port: "-",
+ Info: info,
+ }
+}
diff --git a/Backend/admin/go/services/types.go b/Backend/admin/go/services/types.go
new file mode 100644
index 0000000..18c838c
--- /dev/null
+++ b/Backend/admin/go/services/types.go
@@ -0,0 +1,16 @@
+package services
+
+type ServiceStatus struct {
+ Name string `json:"name"`
+ Status bool `json:"status"`
+ Port string `json:"port"`
+ Info string `json:"info"`
+}
+
+type AllServicesStatus struct {
+ HTTP ServiceStatus `json:"http"`
+ HTTPS ServiceStatus `json:"https"`
+ MySQL ServiceStatus `json:"mysql"`
+ PHP ServiceStatus `json:"php"`
+ Proxy ServiceStatus `json:"proxy"`
+}
diff --git a/Backend/admin/go/sites/sites.go b/Backend/admin/go/sites/sites.go
new file mode 100644
index 0000000..9804632
--- /dev/null
+++ b/Backend/admin/go/sites/sites.go
@@ -0,0 +1,24 @@
+package sites
+
+import (
+ config "vServer/Backend/config"
+)
+
+func GetSitesList() []SiteInfo {
+ sites := make([]SiteInfo, 0)
+
+ for _, site := range config.ConfigData.Site_www {
+ siteInfo := SiteInfo{
+ Name: site.Name,
+ Host: site.Host,
+ Alias: site.Alias,
+ Status: site.Status,
+ RootFile: site.Root_file,
+ RootFileRouting: site.Root_file_routing,
+ }
+ sites = append(sites, siteInfo)
+ }
+
+ return sites
+}
+
diff --git a/Backend/admin/go/sites/types.go b/Backend/admin/go/sites/types.go
new file mode 100644
index 0000000..dbac628
--- /dev/null
+++ b/Backend/admin/go/sites/types.go
@@ -0,0 +1,11 @@
+package sites
+
+type SiteInfo struct {
+ Name string `json:"name"`
+ Host string `json:"host"`
+ Alias []string `json:"alias"`
+ Status string `json:"status"`
+ RootFile string `json:"root_file"`
+ RootFileRouting bool `json:"root_file_routing"`
+}
+
diff --git a/Backend/admin/go/vaccess/types.go b/Backend/admin/go/vaccess/types.go
new file mode 100644
index 0000000..2a4c379
--- /dev/null
+++ b/Backend/admin/go/vaccess/types.go
@@ -0,0 +1,14 @@
+package vaccess
+
+type VAccessRule struct {
+ Type string `json:"type"`
+ TypeFile []string `json:"type_file"`
+ PathAccess []string `json:"path_access"`
+ IPList []string `json:"ip_list"`
+ ExceptionsDir []string `json:"exceptions_dir"`
+ UrlError string `json:"url_error"`
+}
+
+type VAccessConfig struct {
+ Rules []VAccessRule `json:"rules"`
+}
diff --git a/Backend/admin/go/vaccess/vaccess.go b/Backend/admin/go/vaccess/vaccess.go
new file mode 100644
index 0000000..25c61f7
--- /dev/null
+++ b/Backend/admin/go/vaccess/vaccess.go
@@ -0,0 +1,158 @@
+package vaccess
+
+import (
+ "bufio"
+ "fmt"
+ "os"
+ "strings"
+ tools "vServer/Backend/tools"
+)
+
+func GetVAccessPath(host string, isProxy bool) string {
+ if isProxy {
+ return fmt.Sprintf("WebServer/tools/Proxy_vAccess/%s_vAccess.conf", host)
+ }
+ return fmt.Sprintf("WebServer/www/%s/vAccess.conf", host)
+}
+
+func GetVAccessConfig(host string, isProxy bool) (*VAccessConfig, error) {
+ filePath := GetVAccessPath(host, isProxy)
+
+ // Проверяем существование файла
+ absPath, _ := tools.AbsPath(filePath)
+ if _, err := os.Stat(absPath); os.IsNotExist(err) {
+ // Файл не существует - возвращаем пустую конфигурацию
+ return &VAccessConfig{Rules: []VAccessRule{}}, nil
+ }
+
+ file, err := os.Open(absPath)
+ if err != nil {
+ return nil, err
+ }
+ defer file.Close()
+
+ config := &VAccessConfig{}
+ scanner := bufio.NewScanner(file)
+
+ var currentRule *VAccessRule
+
+ for scanner.Scan() {
+ line := strings.TrimSpace(scanner.Text())
+
+ // Пропускаем пустые строки и комментарии
+ if line == "" || strings.HasPrefix(line, "#") {
+ continue
+ }
+
+ // Парсим параметры
+ if strings.Contains(line, ":") {
+ parts := strings.SplitN(line, ":", 2)
+ if len(parts) == 2 {
+ key := strings.TrimSpace(parts[0])
+ value := strings.TrimSpace(parts[1])
+
+ switch key {
+ case "type":
+ // Начало нового правила - сохраняем предыдущее
+ if currentRule != nil && currentRule.Type != "" {
+ config.Rules = append(config.Rules, *currentRule)
+ }
+ // Создаём новое правило
+ currentRule = &VAccessRule{}
+ currentRule.Type = value
+ case "type_file":
+ if currentRule != nil {
+ currentRule.TypeFile = splitAndTrim(value)
+ }
+ case "path_access":
+ if currentRule != nil {
+ currentRule.PathAccess = splitAndTrim(value)
+ }
+ case "ip_list":
+ if currentRule != nil {
+ currentRule.IPList = splitAndTrim(value)
+ }
+ case "exceptions_dir":
+ if currentRule != nil {
+ currentRule.ExceptionsDir = splitAndTrim(value)
+ }
+ case "url_error":
+ if currentRule != nil {
+ currentRule.UrlError = value
+ }
+ }
+ }
+ }
+ }
+
+ // Добавляем последнее правило
+ if currentRule != nil && currentRule.Type != "" {
+ config.Rules = append(config.Rules, *currentRule)
+ }
+
+ return config, nil
+}
+
+func SaveVAccessConfig(host string, isProxy bool, config *VAccessConfig) error {
+ filePath := GetVAccessPath(host, isProxy)
+
+ // Создаём директорию если не существует
+ dir := ""
+ if isProxy {
+ dir = "WebServer/tools/Proxy_vAccess"
+ } else {
+ dir = fmt.Sprintf("WebServer/www/%s", host)
+ }
+
+ absDir, _ := tools.AbsPath(dir)
+ os.MkdirAll(absDir, 0755)
+
+ // Получаем абсолютный путь к файлу
+ absPath, err := tools.AbsPath(filePath)
+ if err != nil {
+ return err
+ }
+
+ // Формируем содержимое файла
+ var content strings.Builder
+
+ content.WriteString("# vAccess Configuration\n")
+ content.WriteString("# Правила применяются сверху вниз\n\n")
+
+ for i, rule := range config.Rules {
+ content.WriteString(fmt.Sprintf("# Правило %d\n", i+1))
+ content.WriteString(fmt.Sprintf("type: %s\n", rule.Type))
+
+ if len(rule.TypeFile) > 0 {
+ content.WriteString(fmt.Sprintf("type_file: %s\n", strings.Join(rule.TypeFile, ", ")))
+ }
+ if len(rule.PathAccess) > 0 {
+ content.WriteString(fmt.Sprintf("path_access: %s\n", strings.Join(rule.PathAccess, ", ")))
+ }
+ if len(rule.IPList) > 0 {
+ content.WriteString(fmt.Sprintf("ip_list: %s\n", strings.Join(rule.IPList, ", ")))
+ }
+ if len(rule.ExceptionsDir) > 0 {
+ content.WriteString(fmt.Sprintf("exceptions_dir: %s\n", strings.Join(rule.ExceptionsDir, ", ")))
+ }
+ if rule.UrlError != "" {
+ content.WriteString(fmt.Sprintf("url_error: %s\n", rule.UrlError))
+ }
+
+ content.WriteString("\n")
+ }
+
+ return os.WriteFile(absPath, []byte(content.String()), 0644)
+}
+
+func splitAndTrim(s string) []string {
+ parts := strings.Split(s, ",")
+ result := []string{}
+ for _, part := range parts {
+ trimmed := strings.TrimSpace(part)
+ if trimmed != "" {
+ result = append(result, trimmed)
+ }
+ }
+ return result
+}
diff --git a/Backend/admin/html/assets/css/dashboard.css b/Backend/admin/html/assets/css/dashboard.css
deleted file mode 100644
index 615f4e2..0000000
--- a/Backend/admin/html/assets/css/dashboard.css
+++ /dev/null
@@ -1,652 +0,0 @@
-/* Стили дашборда админ-панели */
-
-.dashboard-grid {
- display: grid;
- grid-template-columns: repeat(2, 1fr);
- gap: 2rem;
- margin-bottom: 6rem;
-}
-
-@media (max-width: 1200px) {
- .dashboard-grid {
- grid-template-columns: 1fr;
- gap: 1.5rem;
- }
-}
-
-/* Карточки дашборда */
-.dashboard-card {
- background: linear-gradient(135deg, rgba(26, 37, 47, 0.95), rgba(20, 30, 40, 0.9));
- backdrop-filter: blur(20px);
- border-radius: 16px;
- border: 1px solid rgba(52, 152, 219, 0.25);
- box-shadow:
- 0 8px 32px rgba(0, 0, 0, 0.4),
- 0 0 0 1px rgba(52, 152, 219, 0.1),
- inset 0 1px 0 rgba(255, 255, 255, 0.1);
- transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
- overflow: hidden;
- position: relative;
-}
-
-.dashboard-card::before {
- content: '';
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- height: 2px;
- background: linear-gradient(90deg, #3498db, #2ecc71, #9b59b6);
- background-size: 400% 400%;
- animation: gradientFlow 6s ease-in-out infinite;
- box-shadow: 0 2px 10px rgba(52, 152, 219, 0.3);
-}
-
-@keyframes gradientFlow {
- 0%, 100% {
- background-position: 0% 50%;
- }
- 25% {
- background-position: 100% 50%;
- }
- 50% {
- background-position: 200% 50%;
- }
- 75% {
- background-position: 300% 50%;
- }
-}
-
-
-
-
-.dashboard-card.full-width {
- grid-column: 1 / -1;
-}
-
-/* Заголовки карточек */
-.card-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 1.2rem 1.5rem;
- border-bottom: 1px solid rgba(52, 152, 219, 0.15);
- background: linear-gradient(135deg, rgba(52, 152, 219, 0.08), rgba(46, 204, 113, 0.05));
- position: relative;
-}
-
-.card-header::after {
- content: '';
- position: absolute;
- bottom: 0;
- left: 0;
- right: 0;
- height: 1px;
- background: linear-gradient(90deg, transparent, rgba(52, 152, 219, 0.3), transparent);
-}
-
-.card-title {
- display: flex;
- align-items: center;
- font-size: 1.4rem;
- font-weight: 700;
- color: #ecf0f1;
- margin: 0;
-}
-
-.card-icon {
- font-size: 1.5rem;
- margin-right: 1rem;
-}
-
-/* Содержимое карточек */
-.card-content {
- padding: 1.5rem 2rem 2rem 2rem;
-}
-
-/* Статистика */
-.stats-row {
- display: grid;
- grid-template-columns: repeat(3, 1fr);
- gap: 1rem;
- margin-bottom: 1.5rem;
- padding-bottom: 1.5rem;
- border-bottom: 1px solid rgba(52, 152, 219, 0.1);
-}
-
-.stat-item {
- text-align: center;
-}
-
-.stat-number {
- font-size: 2rem;
- font-weight: 700;
- background: linear-gradient(135deg, #3498db, #2ecc71);
- -webkit-background-clip: text;
- -webkit-text-fill-color: transparent;
- background-clip: text;
- margin-bottom: 0.3rem;
- display: block;
-}
-
-.stat-number.active-stat {
- background: linear-gradient(135deg, #2ecc71, #27ae60);
- -webkit-background-clip: text;
- -webkit-text-fill-color: transparent;
- background-clip: text;
-}
-
-.stat-number.inactive-stat {
- background: linear-gradient(135deg, #e74c3c, #c0392b);
- -webkit-background-clip: text;
- -webkit-text-fill-color: transparent;
- background-clip: text;
-}
-
-.stat-number.valid-stat {
- background: linear-gradient(135deg, #2ecc71, #27ae60);
- -webkit-background-clip: text;
- -webkit-text-fill-color: transparent;
- background-clip: text;
-}
-
-.stat-number.warning-stat {
- background: linear-gradient(135deg, #f39c12, #e67e22);
- -webkit-background-clip: text;
- -webkit-text-fill-color: transparent;
- background-clip: text;
-}
-
-.stat-label {
- color: #95a5a6;
- font-size: 0.8rem;
- text-transform: uppercase;
- letter-spacing: 1px;
- font-weight: 500;
-}
-
-/* Превью секции */
-.sites-preview, .certs-preview {
- margin-top: 1rem;
-}
-
-.preview-header {
- margin-bottom: 1rem;
-}
-
-.preview-title {
- color: #bdc3c7;
- font-size: 0.9rem;
- font-weight: 600;
- text-transform: uppercase;
- letter-spacing: 1px;
-}
-
-/* Списки сайтов */
-.sites-list {
- display: flex;
- flex-direction: column;
- gap: 0.8rem;
-}
-
-.site-item {
- display: flex;
- align-items: center;
- padding: 1rem;
- background: rgba(52, 152, 219, 0.05);
- border-radius: 8px;
- border: 1px solid rgba(52, 152, 219, 0.1);
- transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
- gap: 1rem;
-}
-
-.site-item:hover {
- background: rgba(52, 152, 219, 0.1);
- transform: translateX(4px);
- border-color: rgba(52, 152, 219, 0.2);
-}
-
-.site-status {
- width: 10px;
- height: 10px;
- border-radius: 50%;
- flex-shrink: 0;
-}
-
-.site-status.active {
- background: #2ecc71;
- box-shadow: 0 0 8px rgba(46, 204, 113, 0.5);
-}
-
-.site-status.inactive {
- background: #e74c3c;
- box-shadow: 0 0 8px rgba(231, 76, 60, 0.5);
-}
-
-.site-info {
- flex: 1;
- display: flex;
- flex-direction: column;
- gap: 0.2rem;
-}
-
-.site-name {
- color: #ecf0f1;
- font-weight: 600;
- font-size: 0.95rem;
-}
-
-.site-details {
- color: #95a5a6;
- font-size: 0.8rem;
-}
-
-.site-actions {
- display: flex;
- gap: 0.5rem;
-}
-
-/* Списки сертификатов */
-.certs-list {
- display: flex;
- flex-direction: column;
- gap: 0.8rem;
-}
-
-.cert-item {
- display: flex;
- align-items: center;
- padding: 1rem;
- background: rgba(52, 152, 219, 0.05);
- border-radius: 8px;
- border: 1px solid rgba(52, 152, 219, 0.1);
- transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
- gap: 1rem;
-}
-
-.cert-item:hover {
- background: rgba(52, 152, 219, 0.1);
- transform: translateX(4px);
- border-color: rgba(52, 152, 219, 0.2);
-}
-
-.cert-status {
- width: 10px;
- height: 10px;
- border-radius: 50%;
- flex-shrink: 0;
-}
-
-.cert-status.valid {
- background: #2ecc71;
- box-shadow: 0 0 8px rgba(46, 204, 113, 0.5);
-}
-
-.cert-status.warning {
- background: #f39c12;
- box-shadow: 0 0 8px rgba(243, 156, 18, 0.5);
-}
-
-.cert-status.expired {
- background: #e74c3c;
- box-shadow: 0 0 8px rgba(231, 76, 60, 0.5);
-}
-
-.cert-info {
- flex: 1;
- display: flex;
- flex-direction: column;
- gap: 0.2rem;
-}
-
-.cert-name {
- color: #ecf0f1;
- font-weight: 600;
- font-size: 0.95rem;
-}
-
-.cert-details {
- color: #95a5a6;
- font-size: 0.8rem;
-}
-
-.cert-expires {
- display: flex;
- flex-direction: column;
- align-items: flex-end;
- gap: 0.1rem;
- min-width: 80px;
-}
-
-.expires-date {
- color: #bdc3c7;
- font-size: 0.8rem;
- font-weight: 500;
-}
-
-.expires-days {
- color: #95a5a6;
- font-size: 0.7rem;
-}
-
-.cert-expires.warning .expires-date {
- color: #f39c12;
-}
-
-.cert-expires.warning .expires-days {
- color: #e67e22;
-}
-
-.cert-actions {
- display: flex;
- gap: 0.5rem;
-}
-
-/* Серверы */
-.servers-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
- gap: 1rem;
-}
-
-.server-item {
- display: flex;
- align-items: center;
- padding: 1rem;
- background: rgba(52, 152, 219, 0.05);
- border-radius: 8px;
- border: 1px solid rgba(52, 152, 219, 0.1);
- transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
-}
-
-.server-item:hover {
- background: rgba(52, 152, 219, 0.1);
- transform: translateY(-2px);
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
-}
-
-.server-status {
- width: 12px;
- height: 12px;
- border-radius: 50%;
- margin-right: 1rem;
- flex-shrink: 0;
- position: relative;
-}
-
-.server-status.running {
- background: #2ecc71;
- box-shadow: 0 0 12px rgba(46, 204, 113, 0.5);
- animation: pulse 2s infinite;
-}
-
-.server-status.stopped {
- background: #e74c3c;
- box-shadow: 0 0 12px rgba(231, 76, 60, 0.5);
-}
-
-.server-status.starting {
- background: #f39c12;
- box-shadow: 0 0 12px rgba(243, 156, 18, 0.5);
- animation: pulse 1.5s infinite;
-}
-
-.server-status.stopping {
- background: #f39c12;
- box-shadow: 0 0 12px rgba(243, 156, 18, 0.5);
- animation: pulse 1.5s infinite;
-}
-
-.server-status.warning {
- background: #f39c12;
- box-shadow: 0 0 12px rgba(243, 156, 18, 0.5);
-}
-
-.server-info {
- flex: 1;
-}
-
-.server-name {
- color: #ecf0f1;
- font-weight: 600;
- font-size: 1rem;
- margin-bottom: 0.3rem;
-}
-
-.server-details {
- color: #95a5a6;
- font-size: 0.85rem;
-}
-
-/* Кнопки действий */
-.btn-icon {
- background: none;
- border: none;
- font-size: 1rem;
- cursor: pointer;
- padding: 0.3rem;
- border-radius: 4px;
- transition: all 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94);
- opacity: 0.7;
-}
-
-.btn-icon:hover {
- opacity: 1;
- background: rgba(52, 152, 219, 0.1);
- transform: scale(1.1);
-}
-
-.btn-icon.disabled {
- opacity: 0.4;
- cursor: not-allowed;
- pointer-events: none;
-}
-
-.btn-icon.disabled:hover {
- opacity: 0.4;
- background: none;
- transform: none;
-}
-
-.btn-icon.warning {
- color: #f39c12;
-}
-
-.btn-icon.warning:hover {
- background: rgba(243, 156, 18, 0.1);
-}
-
-/* Кнопки - новый дизайн */
-.btn-primary {
- background: linear-gradient(135deg, #3498db, #2980b9);
- color: white;
- border: none;
- padding: 0.4rem 1rem;
- border-radius: 8px;
- font-weight: 600;
- font-size: 0.75rem;
- cursor: pointer;
- transition: all 0.2s ease;
- display: inline-flex;
- align-items: center;
- position: relative;
- overflow: hidden;
- box-shadow: 0 2px 8px rgba(52, 152, 219, 0.25);
- text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
- white-space: nowrap;
-}
-
-.btn-primary::before {
- content: '';
- position: absolute;
- top: 0;
- left: -100%;
- width: 100%;
- height: 100%;
- background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
- transition: left 0.6s ease;
-}
-
-.btn-primary:hover {
- background: linear-gradient(135deg, #2980b9, #3498db);
- transform: translateY(-1px);
- box-shadow: 0 4px 12px rgba(52, 152, 219, 0.3);
-}
-
-.btn-primary:hover::before {
- left: 100%;
-}
-
-.btn-primary:active {
- transform: translateY(0);
- box-shadow: 0 2px 8px rgba(52, 152, 219, 0.25);
-}
-
-/* Иконка плюса */
-.btn-primary .btn-plus {
- background: rgba(255, 255, 255, 0.25);
- border-radius: 50%;
- width: 14px;
- height: 14px;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- font-size: 10px;
- font-weight: 700;
- margin-right: 0.5rem;
- flex-shrink: 0;
-}
-
-/* Специальные стили для кнопки добавления сайта */
-#add-site-btn {
- background: linear-gradient(135deg, #2ecc71, #27ae60);
- box-shadow: 0 2px 8px rgba(46, 204, 113, 0.25);
-}
-
-#add-site-btn:hover {
- background: linear-gradient(135deg, #27ae60, #2ecc71);
- box-shadow: 0 4px 12px rgba(46, 204, 113, 0.3);
-}
-
-#add-site-btn:active {
- box-shadow: 0 2px 8px rgba(46, 204, 113, 0.25);
-}
-
-/* Специальные стили для кнопки добавления SSL */
-#add-cert-btn {
- background: linear-gradient(135deg, #f39c12, #e67e22);
- box-shadow: 0 2px 8px rgba(243, 156, 18, 0.25);
-}
-
-#add-cert-btn:hover {
- background: linear-gradient(135deg, #e67e22, #f39c12);
- box-shadow: 0 4px 12px rgba(243, 156, 18, 0.3);
-}
-
-#add-cert-btn:active {
- box-shadow: 0 2px 8px rgba(243, 156, 18, 0.25);
-}
-
-/* Футер карточек */
-.card-footer {
- margin-top: 1rem;
- padding-top: 1rem;
- border-top: 1px solid rgba(52, 152, 219, 0.1);
- text-align: center;
-}
-
-.btn-link {
- background: none;
- border: none;
- color: #3498db;
- font-size: 0.9rem;
- cursor: pointer;
- padding: 0.5rem;
- border-radius: 6px;
- transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
- font-weight: 500;
-}
-
-.btn-link:hover {
- background: rgba(52, 152, 219, 0.1);
- color: #2ecc71;
- transform: translateY(-1px);
-}
-
-.show-all-btn {
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 0.5rem;
- width: 100%;
-}
-
-/* Адаптивность дашборда */
-@media (max-width: 768px) {
- .dashboard-grid {
- grid-template-columns: 1fr;
- gap: 1.5rem;
- margin-bottom: 4rem;
- }
-
- .card-header {
- flex-direction: column;
- gap: 1.5rem;
- align-items: stretch;
- padding: 1.5rem;
- }
-
- .card-content {
- padding: 1rem 1.5rem 1.5rem 1.5rem;
- }
-
- .card-title {
- font-size: 1.3rem;
- }
-
- .servers-grid {
- grid-template-columns: 1fr;
- }
-
- .stats-row {
- grid-template-columns: 1fr;
- gap: 0.5rem;
- }
-
- .site-item, .cert-item {
- flex-direction: column;
- align-items: flex-start;
- gap: 0.8rem;
- }
-
- .site-info, .cert-info {
- order: 1;
- width: 100%;
- }
-
- .site-actions, .cert-actions {
- order: 3;
- align-self: flex-end;
- }
-
- .cert-expires {
- order: 2;
- align-self: flex-start;
- align-items: flex-start;
- min-width: auto;
- }
-}
-
-@media (max-width: 480px) {
- .stat-number {
- font-size: 2rem;
- }
-
- .card-title {
- font-size: 1.1rem;
- }
-
- .server-item {
- padding: 0.8rem;
- }
-}
\ No newline at end of file
diff --git a/Backend/admin/html/assets/css/main.css b/Backend/admin/html/assets/css/main.css
deleted file mode 100644
index 97cd3ce..0000000
--- a/Backend/admin/html/assets/css/main.css
+++ /dev/null
@@ -1,228 +0,0 @@
-/* Основные стили админ-панели vServer */
-
-* {
- margin: 0;
- padding: 0;
- box-sizing: border-box;
-}
-
-body {
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
- background: linear-gradient(135deg, #1a252f 0%, #2c3e50 50%, #34495e 100%);
- min-height: 100vh;
- color: #ffffff;
- overflow-x: hidden;
-}
-
-/* Фоновые частицы */
-.floating-particles {
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- pointer-events: none;
- z-index: -1;
-}
-
-.particle {
- position: absolute;
- background: rgba(52, 152, 219, 0.15);
- border-radius: 50%;
- animation: float 8s ease-in-out infinite;
-}
-
-@keyframes float {
- 0%, 100% {
- transform: translateY(0px) rotate(0deg);
- }
- 50% {
- transform: translateY(-20px) rotate(180deg);
- }
-}
-
-/* Основной контейнер */
-.admin-container {
- display: flex;
- min-height: 100vh;
- position: relative;
-}
-
-/* Основной контент */
-.admin-main {
- flex: 1;
- padding: 2rem;
- margin-left: 280px;
- transition: margin-left 0.3s ease;
-}
-
-
-
-/* Заголовки секций */
-.section-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 2rem;
- padding-bottom: 1rem;
- border-bottom: 1px solid rgba(52, 152, 219, 0.2);
-}
-
-.section-title {
- font-size: 2.5rem;
- font-weight: 700;
- background: linear-gradient(135deg, #3498db, #2ecc71);
- -webkit-background-clip: text;
- -webkit-text-fill-color: transparent;
- background-clip: text;
-}
-
-/* Индикатор статуса */
-.status-indicator {
- display: flex;
- align-items: center;
- gap: 10px;
- background: linear-gradient(135deg, rgba(46, 204, 113, 0.15), rgba(39, 174, 96, 0.25));
- padding: 12px 20px;
- border-radius: 8px;
- border: 1px solid rgba(46, 204, 113, 0.4);
-}
-
-.status-dot {
- width: 12px;
- height: 12px;
- background: #2ecc71;
- border-radius: 50%;
- animation: pulse 2s infinite;
- box-shadow: 0 0 8px rgba(46, 204, 113, 0.5);
-}
-
-@keyframes pulse {
- 0% {
- box-shadow: 0 0 0 0 rgba(46, 204, 113, 0.7), 0 0 8px rgba(46, 204, 113, 0.5);
- }
- 70% {
- box-shadow: 0 0 0 8px rgba(46, 204, 113, 0), 0 0 8px rgba(46, 204, 113, 0.5);
- }
- 100% {
- box-shadow: 0 0 0 0 rgba(46, 204, 113, 0), 0 0 8px rgba(46, 204, 113, 0.5);
- }
-}
-
-.status-text {
- color: #ecf0f1;
- font-weight: 600;
- font-size: 0.9rem;
-}
-
-/* Кнопки */
-.btn-primary {
- background: linear-gradient(135deg, #3498db, #2980b9);
- color: white;
- border: none;
- padding: 14px 24px;
- border-radius: 10px;
- font-weight: 600;
- cursor: pointer;
- transition: all 0.3s ease;
- box-shadow: 0 4px 16px rgba(52, 152, 219, 0.25);
- font-size: 0.95rem;
- position: relative;
- overflow: hidden;
-}
-
-.btn-primary::before {
- content: '';
- position: absolute;
- top: 0;
- left: -100%;
- width: 100%;
- height: 100%;
- background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
- transition: left 0.5s ease;
-}
-
-.btn-primary:hover::before {
- left: 100%;
-}
-
-.btn-primary:hover {
- background: linear-gradient(135deg, #2980b9, #3498db);
- transform: translateY(-3px);
- box-shadow: 0 8px 25px rgba(52, 152, 219, 0.4);
-}
-
-.btn-primary:active {
- transform: translateY(-1px);
- box-shadow: 0 4px 16px rgba(52, 152, 219, 0.3);
-}
-
-.btn-primary span {
- margin-right: 10px;
- font-size: 1.2rem;
- font-weight: 400;
-}
-
-
-
-/* Копирайт */
-.admin-footer {
- position: fixed;
- bottom: 0;
- left: 280px;
- right: 0;
- background: rgba(26, 37, 47, 0.8);
- backdrop-filter: blur(10px);
- border-top: 1px solid rgba(52, 152, 219, 0.2);
- padding: 1rem 2rem;
- z-index: 100;
-}
-
-.footer-content {
- font-size: 0.85rem;
- color: #95a5a6;
- text-align: center;
-}
-
-.footer-content a {
- color: #3498db;
- text-decoration: none;
- transition: color 0.3s ease;
-}
-
-.footer-content a:hover {
- color: #2ecc71;
- text-decoration: underline;
-}
-
-/* Адаптивность */
-@media (max-width: 1024px) {
- .admin-main {
- margin-left: 0;
- padding: 1rem;
- }
-
- .admin-footer {
- left: 0;
- }
-
- .section-header {
- flex-direction: column;
- gap: 1rem;
- align-items: flex-start;
- }
-
- .section-title {
- font-size: 2rem;
- }
-}
-
-@media (max-width: 768px) {
- .admin-main {
- padding: 1rem 0.5rem;
- }
-
- .section-title {
- font-size: 1.8rem;
- }
-}
\ No newline at end of file
diff --git a/Backend/admin/html/assets/css/navigation.css b/Backend/admin/html/assets/css/navigation.css
deleted file mode 100644
index fd83d86..0000000
--- a/Backend/admin/html/assets/css/navigation.css
+++ /dev/null
@@ -1,203 +0,0 @@
-/* Стили навигации админ-панели */
-
-.admin-nav {
- position: fixed;
- left: 0;
- top: 0;
- width: 280px;
- height: 100vh;
- background: linear-gradient(135deg, rgba(26, 37, 47, 0.95), rgba(20, 30, 40, 0.9));
- backdrop-filter: blur(20px);
- border-right: 1px solid rgba(52, 152, 219, 0.3);
- box-shadow:
- 8px 0 32px rgba(0, 0, 0, 0.4),
- 0 0 0 1px rgba(52, 152, 219, 0.1),
- inset -1px 0 0 rgba(255, 255, 255, 0.1);
- z-index: 1000;
- display: flex;
- flex-direction: column;
- transition: transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
-}
-
-/* Заголовок навигации */
-.nav-header {
- padding: 2rem 1.5rem;
- border-bottom: 1px solid rgba(52, 152, 219, 0.25);
- background: linear-gradient(135deg, rgba(52, 152, 219, 0.08), rgba(46, 204, 113, 0.05));
- position: relative;
- box-shadow:
- 0 4px 15px rgba(0, 0, 0, 0.2),
- inset 0 1px 0 rgba(255, 255, 255, 0.1);
-}
-
-.nav-header::before {
- content: '';
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- height: 2px;
- background: linear-gradient(90deg, #3498db, #2ecc71, #e74c3c, #f39c12);
- background-size: 400% 400%;
- animation: gradientShift 4s ease infinite;
-}
-
-@keyframes gradientShift {
- 0%, 100% { background-position: 0% 50%; }
- 50% { background-position: 100% 50%; }
-}
-
-.logo {
- font-size: 2.2rem;
- font-weight: 700;
- background: linear-gradient(135deg, #3498db, #2ecc71);
- -webkit-background-clip: text;
- -webkit-text-fill-color: transparent;
- background-clip: text;
- letter-spacing: 2px;
- margin-bottom: 0.5rem;
- filter: drop-shadow(0 2px 8px rgba(52, 152, 219, 0.4));
-}
-
-.logo-subtitle {
- font-size: 0.9rem;
- color: #95a5a6;
- font-weight: 400;
- text-transform: uppercase;
- letter-spacing: 1px;
-}
-
-/* Меню навигации */
-.nav-menu {
- list-style: none;
- padding: 1rem 0;
- flex: 1;
-}
-
-.nav-item {
- margin-bottom: 0.5rem;
-}
-
-.nav-link {
- display: flex;
- align-items: center;
- padding: 1rem 1.5rem;
- color: #bdc3c7;
- text-decoration: none;
- transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
- position: relative;
- border-radius: 0 25px 25px 0;
- margin-right: 1rem;
- text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
-}
-
-.nav-link::before {
- content: '';
- position: absolute;
- left: 0;
- top: 0;
- bottom: 0;
- width: 4px;
- background: transparent;
- transition: all 0.3s ease;
-}
-
-.nav-link:hover {
- background: linear-gradient(135deg, rgba(52, 152, 219, 0.12), rgba(46, 204, 113, 0.06));
- color: #3498db;
- transform: translateX(0);
- margin-right: 0.5rem;
- border-radius: 0 12px 12px 0;
- box-shadow: inset 2px 0 8px rgba(52, 152, 219, 0.15);
-}
-
-.nav-link:hover::before {
- background: linear-gradient(135deg, #3498db, #2ecc71);
-}
-
-.nav-item.active .nav-link {
- background: linear-gradient(135deg, rgba(52, 152, 219, 0.2), rgba(46, 204, 113, 0.1));
- color: #3498db;
- font-weight: 600;
- transform: translateX(0);
- margin-right: 0;
- border-radius: 0;
- box-shadow: inset 3px 0 12px rgba(52, 152, 219, 0.25);
-}
-
-.nav-item.active .nav-link::before {
- background: linear-gradient(135deg, #3498db, #2ecc71);
-}
-
-.nav-icon {
- font-size: 1.3rem;
- margin-right: 1rem;
- width: 24px;
- text-align: center;
- transition: transform 0.3s ease;
-}
-
-.nav-link:hover .nav-icon {
- transform: scale(1.1);
-}
-
-.nav-text {
- font-size: 1rem;
- transition: all 0.3s ease;
-}
-
-/* Мобильная адаптивность */
-@media (max-width: 1024px) {
- .admin-nav {
- transform: translateX(-100%);
- box-shadow: 2px 0 10px rgba(0, 0, 0, 0.3);
- }
-
- .admin-nav.open {
- transform: translateX(0);
- }
-
- /* Кнопка меню (будет добавлена в JS) */
- .nav-toggle {
- position: fixed;
- top: 1rem;
- left: 1rem;
- z-index: 1001;
- background: rgba(26, 37, 47, 0.9);
- border: 1px solid rgba(52, 152, 219, 0.3);
- color: #3498db;
- padding: 12px;
- border-radius: 8px;
- cursor: pointer;
- backdrop-filter: blur(10px);
- font-size: 1.2rem;
- transition: all 0.3s ease;
- }
-
- .nav-toggle:hover {
- background: rgba(52, 152, 219, 0.2);
- transform: scale(1.05);
- }
-}
-
-@media (max-width: 768px) {
- .admin-nav {
- width: 100%;
- }
-
- .nav-link {
- padding: 1.2rem 1.5rem;
- margin-right: 0;
- border-radius: 0;
- }
-
- .nav-icon {
- font-size: 1.4rem;
- }
-
- .nav-text {
- font-size: 1.1rem;
- }
-}
-
-/* Анимация появления меню - убрано для избежания проблем с ресайзом */
\ No newline at end of file
diff --git a/Backend/admin/html/assets/css/system-metrics.css b/Backend/admin/html/assets/css/system-metrics.css
deleted file mode 100644
index 565c52b..0000000
--- a/Backend/admin/html/assets/css/system-metrics.css
+++ /dev/null
@@ -1,440 +0,0 @@
-/* Стили системной панели метрик */
-
-.system-panel {
- background: linear-gradient(135deg, rgba(26, 37, 47, 0.95), rgba(20, 30, 40, 0.9));
- backdrop-filter: blur(20px);
- border-radius: 16px;
- border: 1px solid rgba(52, 152, 219, 0.3);
- box-shadow:
- 0 8px 32px rgba(0, 0, 0, 0.4),
- 0 0 0 1px rgba(52, 152, 219, 0.1),
- inset 0 1px 0 rgba(255, 255, 255, 0.1);
- margin-bottom: 2rem;
- overflow: hidden;
- position: relative;
-}
-
-.system-panel::before {
- content: '';
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- height: 3px;
- background: linear-gradient(90deg, #3498db, #2ecc71, #f39c12, #e74c3c, #9b59b6);
- background-size: 400% 400%;
- animation: gradientFlow 8s ease-in-out infinite;
- box-shadow: 0 3px 15px rgba(52, 152, 219, 0.3);
-}
-
-@keyframes gradientFlow {
- 0%, 100% {
- background-position: 0% 50%;
- }
- 25% {
- background-position: 100% 50%;
- }
- 50% {
- background-position: 200% 50%;
- }
- 75% {
- background-position: 300% 50%;
- }
-}
-
-/* Заголовок панели */
-.system-panel-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 1rem 1.5rem;
- border-bottom: 1px solid rgba(52, 152, 219, 0.15);
- background: linear-gradient(135deg, rgba(52, 152, 219, 0.05), rgba(46, 204, 113, 0.03));
-}
-
-.system-title {
- display: flex;
- align-items: center;
- font-size: 1.4rem;
- font-weight: 700;
- color: #ecf0f1;
- margin: 0;
- text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
-}
-
-.system-icon {
- font-size: 1.5rem;
- margin-right: 0.8rem;
- filter: drop-shadow(0 0 8px rgba(52, 152, 219, 0.4));
-}
-
-/* Uptime информация */
-.system-uptime {
- display: flex;
- align-items: center;
- gap: 0.5rem;
-}
-
-.uptime-label {
- color: #95a5a6;
- font-size: 0.7rem;
- font-weight: 500;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
-}
-
-.uptime-value {
- color: #2ecc71;
- font-size: 0.9rem;
- font-weight: 700;
- background: linear-gradient(135deg, #2ecc71, #27ae60);
- -webkit-background-clip: text;
- -webkit-text-fill-color: transparent;
- background-clip: text;
- text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
- font-family: 'Courier New', monospace;
- letter-spacing: 0.5px;
-}
-
-/* Сетка метрик */
-.metrics-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
- gap: 1rem;
- padding: 1rem 1.5rem;
-}
-
-/* Карточки метрик */
-.metric-card {
- display: flex;
- align-items: center;
- background: linear-gradient(135deg,
- rgba(52, 152, 219, 0.08),
- rgba(52, 152, 219, 0.03));
- border: 1px solid rgba(52, 152, 219, 0.15);
- border-radius: 12px;
- padding: 1rem;
- transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
- position: relative;
- overflow: hidden;
- box-shadow:
- 0 4px 15px rgba(0, 0, 0, 0.1),
- inset 0 1px 0 rgba(255, 255, 255, 0.1);
-}
-
-.metric-card::before {
- content: '';
- position: absolute;
- top: 0;
- left: 0;
- width: 4px;
- height: 100%;
- background: linear-gradient(135deg, #3498db, #2ecc71);
- transition: all 0.4s ease;
- box-shadow: 0 0 10px rgba(52, 152, 219, 0.4);
-}
-
-.metric-card::after {
- content: '';
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background: linear-gradient(135deg,
- rgba(255, 255, 255, 0.02),
- transparent,
- rgba(52, 152, 219, 0.02));
- pointer-events: none;
- transition: all 0.4s ease;
-}
-
-.metric-card:hover {
- background: linear-gradient(135deg,
- rgba(52, 152, 219, 0.12),
- rgba(52, 152, 219, 0.06));
- border-color: rgba(52, 152, 219, 0.25);
- transform: translateY(-3px) scale(1.02);
- box-shadow:
- 0 8px 25px rgba(0, 0, 0, 0.2),
- 0 0 20px rgba(52, 152, 219, 0.15),
- inset 0 1px 0 rgba(255, 255, 255, 0.15);
-}
-
-.metric-card:hover::before {
- width: 6px;
- box-shadow: 0 0 15px rgba(52, 152, 219, 0.6);
-}
-
-.metric-card:hover::after {
- background: linear-gradient(135deg,
- rgba(255, 255, 255, 0.05),
- transparent,
- rgba(52, 152, 219, 0.05));
-}
-
-/* Специальные цвета для разных метрик */
-.metric-card.cpu::before {
- background: linear-gradient(135deg, #3498db, #2980b9);
- box-shadow: 0 0 10px rgba(52, 152, 219, 0.4);
-}
-
-.metric-card.cpu:hover::before {
- box-shadow: 0 0 15px rgba(52, 152, 219, 0.6);
-}
-
-.metric-card.ram::before {
- background: linear-gradient(135deg, #e74c3c, #c0392b);
- box-shadow: 0 0 10px rgba(231, 76, 60, 0.4);
-}
-
-.metric-card.ram:hover::before {
- box-shadow: 0 0 15px rgba(231, 76, 60, 0.6);
-}
-
-.metric-card.disk::before {
- background: linear-gradient(135deg, #9b59b6, #8e44ad);
- box-shadow: 0 0 10px rgba(155, 89, 182, 0.4);
-}
-
-.metric-card.disk:hover::before {
- box-shadow: 0 0 15px rgba(155, 89, 182, 0.6);
-}
-
-/* Иконки метрик */
-.metric-icon-wrapper {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 45px;
- height: 45px;
- background: linear-gradient(135deg, rgba(52, 152, 219, 0.15), rgba(52, 152, 219, 0.08));
- border-radius: 10px;
- margin-right: 0.8rem;
- flex-shrink: 0;
- transition: all 0.4s ease;
- box-shadow:
- 0 3px 10px rgba(0, 0, 0, 0.1),
- inset 0 1px 0 rgba(255, 255, 255, 0.1);
-}
-
-.metric-card:hover .metric-icon-wrapper {
- background: linear-gradient(135deg, rgba(52, 152, 219, 0.2), rgba(52, 152, 219, 0.12));
- transform: scale(1.1) rotate(5deg);
- box-shadow:
- 0 5px 15px rgba(0, 0, 0, 0.15),
- 0 0 20px rgba(52, 152, 219, 0.2),
- inset 0 1px 0 rgba(255, 255, 255, 0.15);
-}
-
-.metric-icon {
- font-size: 1.3rem;
- filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.3));
-}
-
-/* Содержимое метрик */
-.metric-content {
- flex: 1;
- min-width: 0;
-}
-
-.metric-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 0.5rem;
-}
-
-.metric-name {
- color: #ecf0f1;
- font-weight: 600;
- font-size: 0.85rem;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
-}
-
-.metric-value {
- font-weight: 700;
- font-size: 1rem;
- background: linear-gradient(135deg, #3498db, #2ecc71);
- -webkit-background-clip: text;
- -webkit-text-fill-color: transparent;
- background-clip: text;
- flex-shrink: 0;
- filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.3));
-}
-
-/* Специальные цвета значений */
-.metric-card.cpu .metric-value {
- background: linear-gradient(135deg, #3498db, #2980b9);
- -webkit-background-clip: text;
- -webkit-text-fill-color: transparent;
- background-clip: text;
-}
-
-.metric-card.ram .metric-value {
- background: linear-gradient(135deg, #e74c3c, #c0392b);
- -webkit-background-clip: text;
- -webkit-text-fill-color: transparent;
- background-clip: text;
-}
-
-.metric-card.disk .metric-value {
- background: linear-gradient(135deg, #9b59b6, #8e44ad);
- -webkit-background-clip: text;
- -webkit-text-fill-color: transparent;
- background-clip: text;
-}
-
-/* Прогресс-бары */
-.metric-progress-bar {
- width: 100%;
- height: 5px;
- background: linear-gradient(135deg, rgba(52, 152, 219, 0.1), rgba(52, 152, 219, 0.05));
- border-radius: 3px;
- overflow: hidden;
- margin-bottom: 0.5rem;
- position: relative;
- box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.2);
-}
-
-.metric-progress {
- height: 100%;
- border-radius: 3px;
- position: relative;
- overflow: hidden;
- box-shadow:
- 0 1px 3px rgba(0, 0, 0, 0.2),
- 0 0 10px rgba(52, 152, 219, 0.3);
- transition: all 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94);
-}
-
-.metric-progress::before {
- content: '';
- position: absolute;
- top: 0;
- left: -100%;
- width: 100%;
- height: 100%;
- background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
- animation: shimmer 4s ease-in-out infinite;
-}
-
-@keyframes shimmer {
- 0% {
- left: -100%;
- opacity: 0;
- }
- 50% {
- opacity: 1;
- }
- 100% {
- left: 100%;
- opacity: 0;
- }
-}
-
-/* Цвета прогресс-баров */
-.cpu-progress {
- background: linear-gradient(135deg, #3498db, #2980b9);
- box-shadow:
- 0 1px 3px rgba(0, 0, 0, 0.2),
- 0 0 10px rgba(52, 152, 219, 0.4);
-}
-
-.ram-progress {
- background: linear-gradient(135deg, #e74c3c, #c0392b);
- box-shadow:
- 0 1px 3px rgba(0, 0, 0, 0.2),
- 0 0 10px rgba(231, 76, 60, 0.4);
-}
-
-.disk-progress {
- background: linear-gradient(135deg, #9b59b6, #8e44ad);
- box-shadow:
- 0 1px 3px rgba(0, 0, 0, 0.2),
- 0 0 10px rgba(155, 89, 182, 0.4);
-}
-
-/* Детали метрик */
-.metric-details {
- display: flex;
- justify-content: space-between;
- align-items: center;
- gap: 0.5rem;
-}
-
-.metric-info {
- color: #bdc3c7;
- font-size: 0.7rem;
- font-weight: 500;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
-}
-
-.metric-frequency,
-.metric-type,
-.metric-range,
-.metric-speed {
- color: #95a5a6;
- font-size: 0.65rem;
- flex-shrink: 0;
- text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
-}
-
-/* Адаптивность */
-@media (max-width: 1200px) {
- .metrics-grid {
- grid-template-columns: repeat(2, 1fr);
- }
-}
-
-@media (max-width: 768px) {
- .metrics-grid {
- grid-template-columns: 1fr;
- gap: 1rem;
- padding: 1rem 1.5rem 1.5rem 1.5rem;
- }
-
- .system-panel-header {
- flex-direction: column;
- gap: 1rem;
- align-items: stretch;
- padding: 1.5rem;
- }
-
- .metric-card {
- flex-direction: column;
- text-align: center;
- padding: 1rem;
- }
-
- .metric-icon-wrapper {
- margin-right: 0;
- margin-bottom: 0.8rem;
- }
-
- .metric-details {
- flex-direction: column;
- gap: 0.3rem;
- }
-}
-
-@media (max-width: 480px) {
- .system-title {
- font-size: 1.1rem;
- }
-
- .metric-name {
- font-size: 0.8rem;
- }
-
- .metric-value {
- font-size: 1rem;
- }
-}
\ No newline at end of file
diff --git a/Backend/admin/html/assets/js/dashboard.js b/Backend/admin/html/assets/js/dashboard.js
deleted file mode 100644
index 721296c..0000000
--- a/Backend/admin/html/assets/js/dashboard.js
+++ /dev/null
@@ -1,11 +0,0 @@
-/**
- * Дашборд админ-панели vServer
- */
-
-document.addEventListener('DOMContentLoaded', function() {
- // Каждая кнопка отдельно с возможностью настройки
- messageUp.send('#add-site-btn', '🌐 Страница добавления нового сайта в разработке', 'warning');
- messageUp.send('#add-cert-btn', '🔒 Страница добавления SSL-сертификата в разработке', 'warning');
- messageUp.send('.btn-icon', '⚙️ Функция настроек в разработке', 'warning');
- messageUp.send('.show-all-btn', '📋 Полный список в разработке', 'warning');
-});
diff --git a/Backend/admin/html/assets/js/json_loader.js b/Backend/admin/html/assets/js/json_loader.js
deleted file mode 100644
index 546b13c..0000000
--- a/Backend/admin/html/assets/js/json_loader.js
+++ /dev/null
@@ -1,90 +0,0 @@
-// Универсальный класс для загрузки JSON данных
-class JSONLoader {
- constructor(config) {
- this.url = config.url;
- this.container = config.container;
- this.requiredFields = config.requiredFields || [];
- this.displayTemplate = config.displayTemplate;
- this.errorMessage = config.errorMessage || 'Ошибка загрузки данных';
- }
-
- // Загрузка данных
- async load() {
- try {
- const response = await fetch(this.url);
- if (!response.ok) {
- throw new Error(`HTTP ${response.status}`);
- }
-
- const data = await response.json();
-
- // Проверяем структуру данных
- if (!this.validateData(data)) {
- throw new Error('Неверная структура данных');
- }
-
- this.display(data);
- } catch (error) {
- console.error('Ошибка загрузки:', error);
- this.displayError();
- }
- }
-
- // Простая проверка обязательных полей
- validateData(data) {
- if (!Array.isArray(data)) {
- return false;
- }
-
- for (let item of data) {
- for (let field of this.requiredFields) {
- if (!item.hasOwnProperty(field)) {
- return false;
- }
- }
- }
-
- return true;
- }
-
- // Отображение данных по шаблону
- display(data) {
- const container = document.querySelector(this.container);
- container.innerHTML = '';
-
- data.forEach(item => {
- let html;
-
- // Если шаблон - функция, вызываем её
- if (typeof this.displayTemplate === 'function') {
- html = this.displayTemplate(item);
- } else {
- // Иначе используем строковый шаблон с заменой
- html = this.displayTemplate;
- for (let key in item) {
- const value = item[key];
- html = html.replace(new RegExp(`{{${key}}}`, 'g'), value);
- }
- }
-
- container.innerHTML += html;
- });
- }
-
- // Отображение ошибки
- displayError() {
- const container = document.querySelector(this.container);
- if (container) {
- container.innerHTML = `
-
- ⚠️ ${this.errorMessage}
-
- `;
- }
- }
-
- // Перезагрузка данных
- reload() {
- this.load();
- }
-}
\ No newline at end of file
diff --git a/Backend/admin/html/assets/js/menu_loader.js b/Backend/admin/html/assets/js/menu_loader.js
deleted file mode 100644
index 1ae81b1..0000000
--- a/Backend/admin/html/assets/js/menu_loader.js
+++ /dev/null
@@ -1,27 +0,0 @@
-// Функция для создания HTML пункта меню
-function getMenuTemplate(item) {
- const isActive = item.active ? 'active' : '';
-
- return `
-
-
- ${item.icon}
- ${item.name}
-
-
- `;
-}
-
-// Создаем загрузчик меню
-const menuLoader = new JSONLoader({
- url: '/json/menu.json',
- container: '.nav-menu',
- requiredFields: ['name', 'icon', 'url', 'active'],
- displayTemplate: getMenuTemplate,
- errorMessage: 'Ошибка загрузки меню'
-});
-
-// Запуск при загрузке страницы
-document.addEventListener('DOMContentLoaded', function() {
- menuLoader.load();
-});
\ No newline at end of file
diff --git a/Backend/admin/html/assets/js/metrics_loader.js b/Backend/admin/html/assets/js/metrics_loader.js
deleted file mode 100644
index 991e986..0000000
--- a/Backend/admin/html/assets/js/metrics_loader.js
+++ /dev/null
@@ -1,153 +0,0 @@
-// Функция для создания HTML карточки метрики
-function createMetricCard(type, icon, name, data) {
- // Используем уже вычисленный процент для всех типов
- let value = Math.round(data.usage || data.usage_percent || 0);
- const progressClass = type === 'cpu' ? 'cpu-progress' :
- type === 'ram' ? 'ram-progress' : 'disk-progress';
-
- // Определяем детали для каждого типа
- let details = '';
- if (type === 'cpu') {
- details = `
- ${data.model_name || 'CPU'}
- ${data.frequency || ''} MHz
- `;
- } else if (type === 'ram') {
- const usedGb = parseFloat(data.used_gb) || 0;
- details = `
- ${usedGb.toFixed(1)} GB из ${data.total_gb || 0} GB
- ${data.type || 'RAM'}
- `;
- } else if (type === 'disk') {
- const usedGb = parseFloat(data.used_gb) || 0;
- const freeGb = parseFloat(data.free_gb) || 0;
- details = `
- Занято: ${usedGb.toFixed(0)} GB : Свободно: ${freeGb.toFixed(0)} GB
- Размер: ${data.total_gb || 0}
- `;
- }
-
- return `
-
- `;
-}
-
-// Функция для создания всех метрик
-function renderMetrics(data) {
- const container = document.querySelector('.metrics-grid');
- if (!container) return;
-
- const html = [
- createMetricCard('cpu', '🖥️', 'Процессор', data.cpu || {}),
- createMetricCard('ram', '💾', 'Оперативная память', data.memory || {}),
- createMetricCard('disk', '💿', data.disk.type, data.disk || {})
- ].join('');
-
- container.innerHTML = html;
-}
-
-// Данные по умолчанию (будут заменены данными из API)
-const staticData = {
- cpu: {
- usage: 0,
- model_name: 'Загрузка...',
- frequency: '0',
- cores: '0'
- },
- memory: {
- usage_percent: 0,
- used_gb: 0,
- total_gb: 0,
- type: 'Загрузка...'
- },
- disk: {
- usage_percent: 0,
- used_gb: 0,
- free_gb: 0,
- total_gb: '0',
- type: 'Загрузка...',
- read_speed: '520 MB/s'
- }
-};
-
-
-
-
-// Функция обновления метрик
-async function updateMetrics() {
- try {
- const response = await fetch('/api/metrics');
- const data = await response.json();
-
- // Обновляем все данные из API
- if (data.cpu_name) staticData.cpu.model_name = data.cpu_name;
- if (data.cpu_ghz) staticData.cpu.frequency = data.cpu_ghz;
- if (data.cpu_cores) staticData.cpu.cores = data.cpu_cores;
- if (data.cpu_usage) staticData.cpu.usage = parseInt(data.cpu_usage);
-
- if (data.disk_name) staticData.disk.type = data.disk_name;
- if (data.disk_size) staticData.disk.total_gb = data.disk_size;
- if (data.disk_used) staticData.disk.used_gb = parseFloat(data.disk_used);
- if (data.disk_free) staticData.disk.free_gb = parseFloat(data.disk_free);
-
- if (data.ram_using) staticData.memory.used_gb = parseFloat(data.ram_using);
- if (data.ram_total) staticData.memory.total_gb = parseFloat(data.ram_total);
-
- // Вычисляем процент использования памяти
- if (staticData.memory.used_gb && staticData.memory.total_gb) {
- staticData.memory.usage_percent = Math.round((staticData.memory.used_gb / staticData.memory.total_gb) * 100);
- }
-
- // Вычисляем процент использования диска
- if (staticData.disk.used_gb && staticData.disk.total_gb) {
- const used = parseFloat(staticData.disk.used_gb.toString().replace(' GB', '')) || 0;
- const total = parseFloat(staticData.disk.total_gb.toString().replace(' GB', '')) || 1;
- staticData.disk.usage_percent = Math.round((used / total) * 100);
- }
-
- // Обновляем uptime
- if (data.server_uptime) {
- const uptimeElement = document.querySelector('.uptime-value');
- if (uptimeElement) {
- uptimeElement.textContent = data.server_uptime;
- }
- }
-
- // Перерисовываем только метрики
- renderMetrics(staticData);
- } catch (error) {
- console.error('Ошибка обновления метрик:', error);
- }
-
-
-}
-
-// Показываем статические данные когда DOM загружен
-document.addEventListener('DOMContentLoaded', function() {
- renderMetrics(staticData);
-
- // Сразу загружаем актуальные данные
- updateMetrics();
-
- // Обновляем метрики каждые 5 секунд
- setInterval(updateMetrics, 5000);
-});
-
-
-
diff --git a/Backend/admin/html/assets/js/server_status.js b/Backend/admin/html/assets/js/server_status.js
deleted file mode 100644
index 36e1473..0000000
--- a/Backend/admin/html/assets/js/server_status.js
+++ /dev/null
@@ -1,180 +0,0 @@
-// Функция для получения HTML шаблона сервера
-var patch_json = '/json/server_status.json';
-
-function getServerTemplate(server) {
- // Определяем класс и текст статуса
- let statusClass, statusText;
- switch(server.Status.toLowerCase()) {
- case 'running':
- statusClass = 'running';
- statusText = 'Работает';
- break;
- case 'stopped':
- statusClass = 'stopped';
- statusText = 'Остановлен';
- break;
- case 'starting':
- statusClass = 'starting';
- statusText = 'Запускается';
- break;
- case 'stopping':
- statusClass = 'stopping';
- statusText = 'Завершается';
- break;
- default:
- statusClass = 'stopped';
- statusText = `Неизвестно (${server.Status})`;
- }
-
- // Определяем иконку и состояние кнопки
- let buttonIcon, buttonDisabled, buttonClass;
-
- if (statusClass === 'starting' || statusClass === 'stopping') {
- buttonIcon = '⏳';
- buttonDisabled = 'disabled';
- buttonClass = 'btn-icon disabled';
- } else if (statusClass === 'running') {
- buttonIcon = '⏹️';
- buttonDisabled = '';
- buttonClass = 'btn-icon';
- } else {
- buttonIcon = '▶️';
- buttonDisabled = '';
- buttonClass = 'btn-icon';
- }
-
- return `
-
-
-
-
${server.NameService}
-
Port ${server.Port} - ${statusText}
-
-
-
-
-
- `;
-}
-
-// Создаем загрузчик серверов
-const serversLoader = new JSONLoader({
- url: patch_json,
- container: '.servers-grid',
- requiredFields: ['NameService', 'Port', 'Status'],
- displayTemplate: getServerTemplate,
- errorMessage: 'Ошибка загрузки статуса серверов'
-});
-
-// Функция для показа временного статуса
-function showTempStatus(serviceName, tempStatus) {
- // Ищем элемент этого сервера на странице
- const serverElements = document.querySelectorAll('.server-item');
- serverElements.forEach(element => {
- const nameElement = element.querySelector('.server-name');
- if (nameElement && nameElement.textContent === serviceName) {
- // Создаем временный объект сервера с новым статусом
- fetch(patch_json)
- .then(response => response.json())
- .then(servers => {
- const server = servers.find(s => s.NameService === serviceName);
- if (server) {
- const tempServer = {...server, Status: tempStatus};
- // Заменяем только этот элемент
- element.outerHTML = getServerTemplate(tempServer);
- }
- });
- }
- });
-}
-
-// Функция обновления одного сервера
-function updateSingleServer(serviceName) {
- fetch(patch_json)
- .then(response => response.json())
- .then(servers => {
- const server = servers.find(s => s.NameService === serviceName);
- if (server) {
- // Ищем элемент этого сервера на странице
- const serverElements = document.querySelectorAll('.server-item');
- serverElements.forEach(element => {
- const nameElement = element.querySelector('.server-name');
- if (nameElement && nameElement.textContent === serviceName) {
- // Заменяем только этот элемент
- element.outerHTML = getServerTemplate(server);
- }
- });
- }
- });
-}
-
-// Универсальная функция управления сервером
-function serverAction(serviceName, startEndpoint, stopEndpoint, updateDelayMs) {
- // Получаем текущий статус сервера из JSON
- fetch(patch_json)
- .then(response => response.json())
- .then(servers => {
- const server = servers.find(s => s.NameService === serviceName);
-
- // Блокируем действие если сервер в процессе изменения
- if (server.Status === 'starting' || server.Status === 'stopping') {
- return;
- }
-
- if (server.Status === 'running') {
- // Сервер запущен - останавливаем
- showTempStatus(serviceName, 'stopping');
- fetch(stopEndpoint, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- }
- }).then(() => {
- setTimeout(() => {
- updateSingleServer(serviceName); // Обновляем только этот сервер
- }, updateDelayMs);
- });
- } else {
- // Сервер остановлен - запускаем
- showTempStatus(serviceName, 'starting');
- fetch(startEndpoint, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- }
- }).then(() => {
- setTimeout(() => {
- updateSingleServer(serviceName); // Обновляем только этот сервер
- }, updateDelayMs);
- });
- }
- });
-}
-
-// Функция для переключения сервера
-function toggleServer(serviceName) {
-
- if (serviceName === 'MySQL Server') {
- serverAction('MySQL Server', '/service/MySql_Start', '/service/MySql_Stop', 2000);
- }
-
- if (serviceName === 'HTTP Server') {
- serverAction('HTTP Server', '/service/Http_Start', '/service/Http_Stop', 2000);
- }
-
- if (serviceName === 'HTTPS Server') {
- serverAction('HTTPS Server', '/service/Https_Start', '/service/Https_Stop', 2000);
- }
-
- if (serviceName === 'PHP Server') {
- serverAction('PHP Server', '/service/Php_Start', '/service/Php_Stop', 2000);
- }
-
-}
-
-// Запуск при загрузке страницы
-document.addEventListener('DOMContentLoaded', function() {
- serversLoader.load();
-});
diff --git a/Backend/admin/html/assets/js/site_list.js b/Backend/admin/html/assets/js/site_list.js
deleted file mode 100644
index 3d11b3b..0000000
--- a/Backend/admin/html/assets/js/site_list.js
+++ /dev/null
@@ -1,55 +0,0 @@
-document.addEventListener('DOMContentLoaded', function() {
- const sitesList = document.querySelector('.sites-list');
- if (sitesList) {
- fetch('/service/Site_List')
- .then(r => r.json())
- .then(data => {
- const sites = data.sites || [];
-
- // Генерируем статистику
- updateSiteStats(sites);
-
- // Отображаем список сайтов
- sitesList.innerHTML = sites.map(site => `
-
-
-
- ${site.host}
- ${site.type.toUpperCase()} • Протокол
-
-
-
-
-
- `).join('');
- });
- }
-});
-
-function updateSiteStats(sites) {
- const totalSites = sites.length;
- const activeSites = sites.filter(site => site.status === 'active').length;
- const inactiveSites = totalSites - activeSites;
-
- // Находим контейнер статистики
- const statsRow = document.querySelector('.stats-row');
-
- // Создаём всю статистику через JavaScript
- statsRow.innerHTML = `
-
-
${totalSites}
-
Всего сайтов
-
-
-
${activeSites}
-
Активных
-
-
-
${inactiveSites}
-
Неактивных
-
- `;
-}
-
-
-
diff --git a/Backend/admin/html/assets/js/tools.js b/Backend/admin/html/assets/js/tools.js
deleted file mode 100644
index 4f67518..0000000
--- a/Backend/admin/html/assets/js/tools.js
+++ /dev/null
@@ -1,201 +0,0 @@
-/* Класс для показа уведомлений */
-class MessageUp {
- // Время автозакрытия уведомлений (в миллисекундах)
- static autoCloseTime = 3000;
-
- // Типы уведомлений (легко редактировать тут)
- static TYPES = {
- info: {
- borderColor: 'rgb(52, 152, 219)',
- background: 'linear-gradient(135deg, rgb(30, 50, 70), rgb(35, 55, 75))'
- },
- success: {
- borderColor: 'rgb(46, 204, 113)',
- background: 'linear-gradient(135deg, rgb(25, 55, 35), rgb(30, 60, 40))'
- },
- warning: {
- borderColor: 'rgb(243, 156, 18)',
- background: 'linear-gradient(135deg, rgb(60, 45, 20), rgb(65, 50, 25))'
- },
- error: {
- borderColor: 'rgb(231, 76, 60)',
- background: 'linear-gradient(135deg, rgb(60, 25, 25), rgb(65, 30, 30))'
- }
- };
-
- constructor() {
- this.addStyles();
- }
-
- /* Показать уведомление */
- show(message, type = 'info') {
- const notification = document.createElement('div');
- notification.className = `message-up message-up-${type}`;
-
- notification.innerHTML = `
-
- ${message}
-
- `;
-
- // Вычисляем позицию для нового уведомления
- const existingNotifications = document.querySelectorAll('.message-up');
- let topPosition = 2; // начальная позиция в rem
-
- existingNotifications.forEach(existing => {
- const rect = existing.getBoundingClientRect();
- const currentTop = parseFloat(existing.style.top) || 2;
- const height = rect.height / 16; // переводим px в rem (примерно)
- topPosition = Math.max(topPosition, currentTop + height + 1); // добавляем отступ
- });
-
- notification.style.top = `${topPosition}rem`;
- document.body.appendChild(notification);
-
- // Показываем с анимацией
- setTimeout(() => {
- notification.classList.add('message-up-show');
- }, 10);
-
- // Автоматическое закрытие и удаление
- if (MessageUp.autoCloseTime > 0) {
- setTimeout(() => {
- if (notification && notification.parentNode) {
- notification.classList.remove('message-up-show');
- notification.classList.add('message-up-hide');
-
- setTimeout(() => {
- if (notification.parentNode) {
- notification.remove();
- // Пересчитываем позиции оставшихся уведомлений
- this.repositionNotifications();
- }
- }, 300);
- }
- }, MessageUp.autoCloseTime);
- }
- }
-
- /* Пересчитать позиции всех уведомлений */
- repositionNotifications() {
- const notifications = document.querySelectorAll('.message-up');
- let currentTop = 2; // начальная позиция
-
- notifications.forEach(notification => {
- notification.style.transition = 'all 0.3s ease';
- notification.style.top = `${currentTop}rem`;
-
- const rect = notification.getBoundingClientRect();
- const height = rect.height / 16; // переводим px в rem
- currentTop += height + 1; // добавляем отступ
- });
- }
-
- /* Показать сообщение напрямую или привязать к элементам */
- send(messageOrSelector, typeOrMessage = 'info', type = 'info') {
- // Если первый параметр строка и нет других элементов на странице с таким селектором
- // то показываем сообщение напрямую
- if (typeof messageOrSelector === 'string' &&
- document.querySelectorAll(messageOrSelector).length === 0) {
- // Показываем сообщение напрямую
- this.show(messageOrSelector, typeOrMessage);
- return;
- }
-
- // Иначе привязываем к элементам (старый способ)
- this.bindToElements(messageOrSelector, typeOrMessage, type);
- }
-
- /* Привязать уведомления к элементам */
- bindToElements(selector, message = 'Страница в разработке', type = 'info') {
- document.querySelectorAll(selector).forEach(element => {
- element.addEventListener('click', function(e) {
- e.preventDefault();
- e.stopPropagation();
-
- // Вызываем нужный тип уведомления
- window.messageUp.show(message, type);
- });
- });
- }
-
- /* Добавить стили для уведомлений */
- addStyles() {
- if (document.querySelector('#message-up-styles')) return;
-
- const cfg = MessageUp.TYPES;
-
- // Генерируем стили для типов уведомлений
- const typeStyles = Object.entries(cfg).map(([type, style]) => `
- .message-up-${type} {
- border-color: ${style.borderColor};
- background: ${style.background};
- }
- `).join('');
-
- const style = document.createElement('style');
- style.id = 'message-up-styles';
- style.textContent = `
- .message-up {
- position: fixed;
- top: 2rem;
- right: 2rem;
- min-width: 300px;
- max-width: 400px;
- background: rgb(26, 37, 47);
- backdrop-filter: blur(15px);
- border-radius: 12px;
- padding: 1rem;
- color: #ecf0f1;
- font-size: 0.9rem;
- border: 2px solid;
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
- z-index: 10000;
- opacity: 0;
- transform: translateX(100%);
- transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
- font-family: 'Segoe UI', system-ui, sans-serif;
- }
-
- .message-up-show {
- opacity: 1;
- transform: translateX(0);
- }
-
- .message-up-hide {
- opacity: 0;
- transform: translateX(100%);
- }
-
- ${typeStyles}
-
- .message-content {
- display: flex;
- align-items: center;
- gap: 1rem;
- }
-
- .message-text {
- flex: 1;
- line-height: 1.4;
- font-weight: 500;
- }
-
- @media (max-width: 768px) {
- .message-up {
- top: 1rem;
- right: 1rem;
- left: 1rem;
- min-width: auto;
- max-width: none;
- }
- }
- `;
-
- document.head.appendChild(style);
- }
-
-}
-
-// Создаем глобальный экземпляр
-window.messageUp = new MessageUp();
\ No newline at end of file
diff --git a/Backend/admin/html/index.html b/Backend/admin/html/index.html
deleted file mode 100644
index adf0093..0000000
--- a/Backend/admin/html/index.html
+++ /dev/null
@@ -1,225 +0,0 @@
-
-
-
-
-
- vServer - Админ панель
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
5
-
Всего сертификатов
-
-
-
-
-
-
-
-
-
-
-
- *.voxsel.ru
- Wildcard • Let's Encrypt
-
-
- До 30.12.2024
- 89 дней
-
-
-
-
-
-
-
-
- example.com
- Standard • Cloudflare
-
-
- До 15.01.2024
- 21 день
-
-
-
-
-
-
-
-
- api.voxsel.ru
- Standard • Let's Encrypt
-
-
- До 05.03.2024
- 72 дня
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Backend/admin/icon_exe.png b/Backend/admin/icon_exe.png
new file mode 100644
index 0000000..b98ab7f
Binary files /dev/null and b/Backend/admin/icon_exe.png differ
diff --git a/Backend/config/config.go b/Backend/config/config.go
index eef3100..7cf478a 100644
--- a/Backend/config/config.go
+++ b/Backend/config/config.go
@@ -24,12 +24,11 @@ type Site_www struct {
}
type Soft_Settings struct {
- Php_port int `json:"php_port"`
- Php_host string `json:"php_host"`
- Mysql_port int `json:"mysql_port"`
- Mysql_host string `json:"mysql_host"`
- Admin_port string `json:"admin_port"`
- Admin_host string `json:"admin_host"`
+ Php_port int `json:"php_port"`
+ Php_host string `json:"php_host"`
+ Mysql_port int `json:"mysql_port"`
+ Mysql_host string `json:"mysql_host"`
+ Proxy_enabled bool `json:"proxy_enabled"`
}
type Proxy_Service struct {
diff --git a/Backend/tools/cmd_go.go b/Backend/tools/cmd_go.go
index fd68cce..711e335 100644
--- a/Backend/tools/cmd_go.go
+++ b/Backend/tools/cmd_go.go
@@ -12,20 +12,68 @@ import (
)
var (
- kernel32 = syscall.NewLazyDLL("kernel32.dll")
- procSetConsoleMode = kernel32.NewProc("SetConsoleMode")
- procGetConsoleMode = kernel32.NewProc("GetConsoleMode")
- procCreateMutex = kernel32.NewProc("CreateMutexW")
- procCloseHandle = kernel32.NewProc("CloseHandle")
+ kernel32 = syscall.NewLazyDLL("kernel32.dll")
+ procSetConsoleMode = kernel32.NewProc("SetConsoleMode")
+ procGetConsoleMode = kernel32.NewProc("GetConsoleMode")
+ procCreateMutex = kernel32.NewProc("CreateMutexW")
+ procCloseHandle = kernel32.NewProc("CloseHandle")
+ procCreateJobObject = kernel32.NewProc("CreateJobObjectW")
+ procAssignProcessToJobObject = kernel32.NewProc("AssignProcessToJobObject")
+ procSetInformationJobObject = kernel32.NewProc("SetInformationJobObject")
)
const ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
+const JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x2000
var mutexHandle syscall.Handle
+var jobHandle syscall.Handle
func init() {
enableVirtualTerminal()
+ createJobObject()
+}
+func createJobObject() {
+ handle, _, _ := procCreateJobObject.Call(0, 0)
+ if handle == 0 {
+ return
+ }
+
+ jobHandle = syscall.Handle(handle)
+
+ // Устанавливаем флаг автоматического завершения дочерних процессов
+ type JOBOBJECT_EXTENDED_LIMIT_INFORMATION struct {
+ BasicLimitInformation struct {
+ PerProcessUserTimeLimit uint64
+ PerJobUserTimeLimit uint64
+ LimitFlags uint32
+ MinimumWorkingSetSize uintptr
+ MaximumWorkingSetSize uintptr
+ ActiveProcessLimit uint32
+ Affinity uintptr
+ PriorityClass uint32
+ SchedulingClass uint32
+ }
+ IoInfo [48]byte
+ ProcessMemoryLimit uintptr
+ JobMemoryLimit uintptr
+ PeakProcessMemoryUsed uintptr
+ PeakJobMemoryUsed uintptr
+ }
+
+ var limitInfo JOBOBJECT_EXTENDED_LIMIT_INFORMATION
+ limitInfo.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
+
+ procSetInformationJobObject.Call(
+ uintptr(jobHandle),
+ 9, // JobObjectExtendedLimitInformation
+ uintptr(unsafe.Pointer(&limitInfo)),
+ unsafe.Sizeof(limitInfo),
+ )
+
+ // Добавляем текущий процесс в Job Object
+ currentProcess, _ := syscall.GetCurrentProcess()
+ procAssignProcessToJobObject.Call(uintptr(jobHandle), uintptr(currentProcess))
}
func enableVirtualTerminal() {
@@ -66,6 +114,12 @@ func RunBatScript(script string) (string, error) {
// Функция для логирования вывода процесса в консоль
func Logs_console(process *exec.Cmd, check bool) error {
+ // Скрываем окно процесса для GUI приложений
+ process.SysProcAttr = &syscall.SysProcAttr{
+ HideWindow: true,
+ CreationFlags: 0x08000000, // CREATE_NO_WINDOW
+ }
+
if check {
// Настраиваем pipes для захвата вывода
stdout, err := process.StdoutPipe()
@@ -101,24 +155,24 @@ func Logs_console(process *exec.Cmd, check bool) error {
// CheckSingleInstance проверяет, не запущена ли программа уже через мьютекс
func CheckSingleInstance() bool {
mutexName, _ := syscall.UTF16PtrFromString("Global\\vServer_SingleInstance")
-
+
handle, _, err := procCreateMutex.Call(
0,
0,
uintptr(unsafe.Pointer(mutexName)),
)
-
+
if handle == 0 {
return false // не удалось создать мьютекс
}
-
+
mutexHandle = syscall.Handle(handle)
-
+
// Если GetLastError возвращает ERROR_ALREADY_EXISTS (183), значит мьютекс уже существует
if err.(syscall.Errno) == 183 {
return false // программа уже запущена
}
-
+
return true // успешно создали мьютекс, программа не запущена
}
@@ -128,4 +182,10 @@ func ReleaseMutex() {
procCloseHandle.Call(uintptr(mutexHandle))
mutexHandle = 0
}
-}
\ No newline at end of file
+
+ // Закрываем Job Object - это автоматически убьёт все дочерние процессы
+ if jobHandle != 0 {
+ procCloseHandle.Call(uintptr(jobHandle))
+ jobHandle = 0
+ }
+}
diff --git a/README.md b/README.md
index 36a4597..7d6629b 100644
--- a/README.md
+++ b/README.md
@@ -18,20 +18,23 @@
- ✅ **MySQL сервер** с полной поддержкой
### 🔧 Администрирование
-- ✅ **Веб-админка** на порту 5555 с мониторингом
-- ✅ **Консольное управление** через командную строку
-- ✅ **Логирование** всех операций
-- ✅ **Конфигурация** через JSON файлы
+- ✅ **GUI Админка** - Wails desktop приложение с современным интерфейсом
+- ✅ **Управление сервисами** - запуск/остановка HTTP, HTTPS, MySQL, PHP, Proxy
+- ✅ **Редактор сайтов и прокси** - визуальное управление конфигурацией
+- ✅ **vAccess редактор** - настройка правил доступа через интерфейс
## 🏗️ Архитектура
```
vServer/
-├── 🎯 main.go # Точка входа
+├── 🎯 main.go # Точка входа основного сервера
│
├── 🔧 Backend/ # Основная логика
│ │
-│ ├── admin/ # | 🎛️ Веб-админка (порт 5555) |
+│ ├── admin/ # | 🎛️ GUI Админка (Wails) |
+│ │ ├── go/ # | Go backend для админки |
+│ │ └── frontend/ # | Современный UI |
+│ │
│ ├── config/ # | 🔧 Конфигурационные файлы Go |
│ ├── tools/ # | 🛠️ Утилиты и хелперы |
│ └── WebServer/ # | 🌐 Модули веб-сервера |
@@ -43,29 +46,37 @@ vServer/
│ ├── tools/ # | 📊 Логи и инструменты |
│ └── www/ # | 🌍 Веб-контент |
│
-└── 📄 go.mod # Go модули
+├── 📄 go.mod # Go модули
+├── 🔨 build_admin.ps1 # Сборка GUI админки
+└── 🚀 vSerf.exe # GUI админка (после сборки)
```
## 🚀 Установка и запуск
-### 🔨 Сборка проекта
-```bash
-go build -o MyApp.exe
+### 🔨 Сборка основного сервера
+```powershell
+./build_admin.ps1
```
+Скрипт автоматически:
+- Проверит/создаст `go.mod`
+- Установит зависимости (`go mod tidy`)
+- Проверит/установит Wails CLI
+- Соберёт приложение → `vSerf.exe`
+
### 📦 Подготовка компонентов
1. Распакуйте архив `WebServer/soft/soft.rar` в папку `WebServer/soft/`
-2. Запустите скомпилированный файл `MyApp.exe`
+2. Запустите `vServer.exe` - основной сервер
+3. Запустите `vSerf.exe` - GUI админка для управления
> 🔑 **Важно:** Пароль MySQL по умолчанию - `root`
### 📦 Готовый проект для пользователя
-Для работы приложения необходимы только:
-- 📄 `MyApp.exe` - исполняемый файл
-- 📁 `WebServer/` - папка с конфигурацией и ресурсами
-
-> 💡 Папка `Backend/` и файлы `go.mod`, `main.go` и т.д. нужны только для разработки
+Для работы необходимы:
+- 📄 `vSerf.exe` - GUI админка (опционально)
+- 📁 `WebServer/` - конфигурация и ресурсы
+> 💡 Папка `Backend/` и файлы `go.mod`, `main.go` нужны только для разработки
## ⚙️ Конфигурация
@@ -94,9 +105,9 @@ go build -o MyApp.exe
}
],
"Soft_Settings": {
- "mysql_port": 3306, "mysql_host": "192.168.1.6",
+ "mysql_port": 3306, "mysql_host": "127.0.0.1",
"php_port": 8000, "php_host": "localhost",
- "admin_port": "5555", "admin_host": "localhost"
+ "proxy_enabled": true
}
}
```
@@ -104,7 +115,7 @@ go build -o MyApp.exe
**Основные параметры:**
- `Site_www` - настройки веб-сайтов
- `Proxy_Service` - конфигурация прокси-сервисов
-- `Soft_Settings` - порты и хосты сервисов (MySQL, PHP, админка)
+- `Soft_Settings` - порты и хосты сервисов (MySQL, PHP, proxy_enabled)
### 🌐 Alias с поддержкой Wildcard
diff --git a/WebServer/config.json b/WebServer/config.json
index 4762141..d24594d 100644
--- a/WebServer/config.json
+++ b/WebServer/config.json
@@ -1,32 +1,31 @@
{
- "Site_www": [
- {
- "alias": ["localhost"],
- "host": "127.0.0.1",
- "name": "Локальный сайт",
- "status": "active",
- "root_file": "index.html",
- "root_file_routing": true
- }
- ],
-
"Proxy_Service": [
{
+ "AutoHTTPS": true,
"Enable": false,
"ExternalDomain": "git.example.ru",
"LocalAddress": "127.0.0.1",
"LocalPort": "3333",
- "ServiceHTTPSuse": false,
- "AutoHTTPS": true
+ "ServiceHTTPSuse": false
+ }
+ ],
+ "Site_www": [
+ {
+ "alias": [
+ "localhost"
+ ],
+ "host": "127.0.0.1",
+ "name": "Локальный сайт",
+ "root_file": "index.html",
+ "root_file_routing": true,
+ "status": "active"
}
],
-
"Soft_Settings": {
"mysql_host": "127.0.0.1",
"mysql_port": 3306,
"php_host": "localhost",
"php_port": 8000,
- "admin_host": "localhost",
- "admin_port": "5555"
+ "proxy_enabled": true
}
}
\ No newline at end of file
diff --git a/appicon.png b/appicon.png
new file mode 100644
index 0000000..b98ab7f
Binary files /dev/null and b/appicon.png differ
diff --git a/build_admin.ps1 b/build_admin.ps1
new file mode 100644
index 0000000..9ac64e6
--- /dev/null
+++ b/build_admin.ps1
@@ -0,0 +1,109 @@
+# vServer Admin Panel Builder
+$ErrorActionPreference = 'SilentlyContinue'
+[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
+
+# Очищаем консоль
+Clear-Host
+Start-Sleep -Milliseconds 100
+
+function Write-Step {
+ param($Step, $Total, $Message)
+ Write-Host "[$Step/$Total] " -ForegroundColor Cyan -NoNewline
+ Write-Host $Message -ForegroundColor White
+}
+
+function Write-Success {
+ param($Message)
+ Write-Host " + OK: " -ForegroundColor Green -NoNewline
+ Write-Host $Message -ForegroundColor Green
+}
+
+function Write-Info {
+ param($Message)
+ Write-Host " > " -ForegroundColor Yellow -NoNewline
+ Write-Host $Message -ForegroundColor Yellow
+}
+
+function Write-Err {
+ param($Message)
+ Write-Host " X ERROR: " -ForegroundColor Red -NoNewline
+ Write-Host $Message -ForegroundColor Red
+}
+
+function Write-ProgressBar {
+ param($Percent)
+ $filled = [math]::Floor($Percent / 4)
+ $empty = 25 - $filled
+ $bar = "#" * $filled + "-" * $empty
+ Write-Host " [$bar] $Percent%" -ForegroundColor Cyan
+}
+
+Write-Host ""
+Write-Host "=================================================" -ForegroundColor Magenta
+Write-Host " vServer Admin Panel Builder" -ForegroundColor Cyan
+Write-Host "=================================================" -ForegroundColor Magenta
+Write-Host ""
+
+Write-Step 1 4 "Проверка go.mod..."
+if (-not (Test-Path "go.mod")) {
+ Write-Info "Создание go.mod..."
+ go mod init vServer 2>&1 | Out-Null
+ Write-Success "Создан"
+} else {
+ Write-Success "Найден"
+}
+Write-ProgressBar 25
+Write-Host ""
+
+Write-Step 2 4 "Установка зависимостей..."
+go mod tidy 2>&1 | Out-Null
+Write-Success "Зависимости установлены"
+Write-ProgressBar 50
+Write-Host ""
+
+Write-Step 3 4 "Проверка Wails CLI..."
+$null = wails version 2>&1
+if ($LASTEXITCODE -ne 0) {
+ Write-Info "Установка Wails CLI..."
+ go install github.com/wailsapp/wails/v2/cmd/wails@latest 2>&1 | Out-Null
+ Write-Success "Установлен"
+} else {
+ Write-Success "Найден"
+}
+Write-ProgressBar 75
+Write-Host ""
+
+Write-Step 4 4 "Сборка приложения..."
+Write-Info "Компиляция (может занять ~10 сек)..."
+
+wails build -f admin.go 2>&1 | Out-Null
+
+if (Test-Path "bin\vServer-Admin.exe") {
+ Write-Success "Скомпилировано"
+ Write-ProgressBar 100
+ Write-Host ""
+
+ Write-Host "Финализация..." -ForegroundColor Cyan
+ Move-Item -Path "bin\vServer-Admin.exe" -Destination "vSerf.exe" -Force 2>$null
+ Write-Success "Файл перемещён: vSerf.exe"
+
+ if (Test-Path "bin") { Remove-Item -Path "bin" -Recurse -Force 2>$null }
+ if (Test-Path "windows") { Remove-Item -Path "windows" -Recurse -Force 2>$null }
+ Write-Success "Временные файлы удалены"
+ Write-Host ""
+
+ Write-Host "=================================================" -ForegroundColor Green
+ Write-Host " УСПЕШНО СОБРАНО!" -ForegroundColor Green
+ Write-Host " Файл: " -ForegroundColor Green -NoNewline
+ Write-Host "vSerf.exe" -ForegroundColor Cyan
+ Write-Host "=================================================" -ForegroundColor Green
+} else {
+ Write-Err "Ошибка компиляции"
+ Write-Host ""
+
+ Write-Host "=================================================" -ForegroundColor Red
+ Write-Host " ОШИБКА СБОРКИ!" -ForegroundColor Red
+ Write-Host "=================================================" -ForegroundColor Red
+}
+
+Write-Host ""
diff --git a/go.mod b/go.mod
index ea1e85a..f99d45b 100644
--- a/go.mod
+++ b/go.mod
@@ -1,3 +1,35 @@
module vServer
go 1.24.4
+
+require github.com/wailsapp/wails/v2 v2.11.0
+
+require (
+ github.com/bep/debounce v1.2.1 // indirect
+ github.com/go-ole/go-ole v1.3.0 // indirect
+ github.com/godbus/dbus/v5 v5.1.0 // indirect
+ github.com/google/uuid v1.6.0 // indirect
+ github.com/gorilla/websocket v1.5.3 // indirect
+ github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
+ github.com/labstack/echo/v4 v4.13.3 // indirect
+ github.com/labstack/gommon v0.4.2 // indirect
+ github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
+ github.com/leaanthony/gosod v1.0.4 // indirect
+ github.com/leaanthony/slicer v1.6.0 // indirect
+ github.com/leaanthony/u v1.1.1 // indirect
+ github.com/mattn/go-colorable v0.1.13 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
+ github.com/pkg/errors v0.9.1 // indirect
+ github.com/rivo/uniseg v0.4.7 // indirect
+ github.com/samber/lo v1.49.1 // indirect
+ github.com/tkrajina/go-reflector v0.5.8 // indirect
+ github.com/valyala/bytebufferpool v1.0.0 // indirect
+ github.com/valyala/fasttemplate v1.2.2 // indirect
+ github.com/wailsapp/go-webview2 v1.0.22 // indirect
+ github.com/wailsapp/mimetype v1.4.1 // indirect
+ golang.org/x/crypto v0.33.0 // indirect
+ golang.org/x/net v0.35.0 // indirect
+ golang.org/x/sys v0.30.0 // indirect
+ golang.org/x/text v0.22.0 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..e3658ec
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,81 @@
+github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
+github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
+github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
+github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
+github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
+github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
+github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
+github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
+github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
+github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
+github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
+github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc=
+github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA=
+github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A=
+github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
+github.com/leaanthony/gosod v1.0.4 h1:YLAbVyd591MRffDgxUOU1NwLhT9T1/YiwjKZpkNFeaI=
+github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw=
+github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js=
+github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8=
+github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
+github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
+github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
+github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
+github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
+github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
+github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
+github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
+github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
+github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
+github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
+github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ=
+github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
+github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
+github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
+github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
+github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
+github.com/wailsapp/go-webview2 v1.0.22 h1:YT61F5lj+GGaat5OB96Aa3b4QA+mybD0Ggq6NZijQ58=
+github.com/wailsapp/go-webview2 v1.0.22/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
+github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
+github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
+github.com/wailsapp/wails/v2 v2.11.0 h1:seLacV8pqupq32IjS4Y7V8ucab0WZwtK6VvUVxSBtqQ=
+github.com/wailsapp/wails/v2 v2.11.0/go.mod h1:jrf0ZaM6+GBc1wRmXsM8cIvzlg0karYin3erahI4+0k=
+golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
+golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
+golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
+golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
+golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
+golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
+golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/icon.syso b/icon.syso
deleted file mode 100644
index 1c87c40..0000000
Binary files a/icon.syso and /dev/null differ
diff --git a/main.go b/main.go
index 7fcd980..a336229 100644
--- a/main.go
+++ b/main.go
@@ -1,75 +1,50 @@
package main
import (
- "fmt"
- "time"
- webserver "vServer/Backend/WebServer"
+ "embed"
+ "log"
+
+ "github.com/wailsapp/wails/v2"
+ "github.com/wailsapp/wails/v2/pkg/options"
+ "github.com/wailsapp/wails/v2/pkg/options/assetserver"
+ "github.com/wailsapp/wails/v2/pkg/options/windows"
+
admin "vServer/Backend/admin/go"
- json_admin "vServer/Backend/admin/go/json"
- config "vServer/Backend/config"
- tools "vServer/Backend/tools"
)
+//go:embed all:Backend/admin/frontend
+var assets embed.FS
+
func main() {
+ // Создаём экземпляр приложения
+ app := admin.NewApp()
- if !tools.CheckSingleInstance() {
- println("")
- println(tools.Color("❌ ОШИБКА:", tools.Красный) + " vServer уже запущен!")
- println(tools.Color("💡 Подсказка:", tools.Жёлтый) + " Завершите уже запущенный процесс перед запуском нового.")
- println("")
- println("Нажмите Enter для завершения...")
- fmt.Scanln()
- return
+ // Настройки окна
+ err := wails.Run(&options.App{
+ Title: "vServer - Панель управления",
+ Width: 1600,
+ Height: 900,
+ MinWidth: 1400,
+ MinHeight: 800,
+ AssetServer: &assetserver.Options{
+ Assets: assets,
+ },
+ BackgroundColour: &options.RGBA{R: 10, G: 14, B: 26, A: 1},
+ OnStartup: app.Startup,
+ OnShutdown: app.Shutdown,
+ Bind: []interface{}{
+ app,
+ },
+ Frameless: true,
+ Windows: &windows.Options{
+ WebviewIsTransparent: false,
+ WindowIsTranslucent: false,
+ DisableWindowIcon: false,
+ Theme: windows.Dark,
+ },
+ })
+
+ if err != nil {
+ log.Fatal(err)
}
-
- // Освобождаем мьютекс при выходе (опционально, так как Windows сама освободит)
- defer tools.ReleaseMutex()
-
- println("")
- println(tools.Color("vServer", tools.Жёлтый) + tools.Color(" 1.0.0", tools.Голубой))
- println(tools.Color("Автор: ", tools.Зелёный) + tools.Color("Суманеев Роман (c) 2025", tools.Голубой))
- println(tools.Color("Официальный сайт: ", tools.Зелёный) + tools.Color("https://voxsel.ru", tools.Голубой))
-
- println("")
- println("🚀 Запуск vServer...")
- println("📁 Файлы сайта будут обслуживаться из папки 'www'")
- println("")
- println("⏳ Запуск сервисов...")
- println("")
-
- // Инициализируем время запуска сервера
- tools.ServerUptime("start")
-
- config.LoadConfig()
- time.Sleep(50 * time.Millisecond)
-
- webserver.StartHandler()
- time.Sleep(50 * time.Millisecond)
-
- // Запускаем серверы в горутинах
- go admin.StartAdmin()
- time.Sleep(50 * time.Millisecond)
-
- webserver.Cert_start()
- time.Sleep(50 * time.Millisecond)
-
- go webserver.StartHTTPS()
- json_admin.UpdateServerStatus("HTTPS Server", "running")
- time.Sleep(50 * time.Millisecond)
-
- go webserver.StartHTTP()
- json_admin.UpdateServerStatus("HTTP Server", "running")
- time.Sleep(50 * time.Millisecond)
-
- webserver.PHP_Start()
- json_admin.UpdateServerStatus("PHP Server", "running")
- time.Sleep(50 * time.Millisecond)
-
- webserver.StartMySQLServer(false)
- json_admin.UpdateServerStatus("MySQL Server", "running")
- time.Sleep(50 * time.Millisecond)
-
- println("")
- webserver.CommandListener()
-
}
diff --git a/vSerf.exe b/vSerf.exe
new file mode 100644
index 0000000..843a5ff
Binary files /dev/null and b/vSerf.exe differ
diff --git a/wails.json b/wails.json
new file mode 100644
index 0000000..605ba9e
--- /dev/null
+++ b/wails.json
@@ -0,0 +1,23 @@
+{
+ "$schema": "https://wails.io/schemas/config.v2.json",
+ "name": "vServer Admin Panel",
+ "outputfilename": "vServer-Admin",
+ "author": {
+ "name": "Суманеев Роман",
+ "email": "info@voxsel.ru"
+ },
+ "info": {
+ "companyName": "vServer",
+ "productName": "vServer Admin Panel",
+ "productVersion": "1.0.0",
+ "copyright": "Copyright © 2025 vServer",
+ "comments": "Панель управления vServer"
+ },
+ "wailsjsdir": "./Backend/admin/frontend/wailsjs",
+ "frontend:dir": "./Backend/admin/frontend",
+ "assetdir": "./Backend/admin/frontend",
+ "reloaddirs": "Backend/admin",
+ "build:dir": ".",
+ "appicon": "appicon.png"
+}
+