diff --git a/.gitignore b/.gitignore
index f1820e8..e251873 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,6 +13,7 @@ front_vue/node_modules/
front_vue/dist/
front_vue/src/auto-imports.d.ts
front_vue/src/components.d.ts
+wailsjs
# Cursor
.cursorrules
diff --git a/Backend/admin/API.md b/Backend/admin/API.md
new file mode 100644
index 0000000..bc33780
--- /dev/null
+++ b/Backend/admin/API.md
@@ -0,0 +1,394 @@
+# 📡 vServer Admin API
+
+API панели управления vServer. Все методы вызываются через Wails IPC биндинги.
+
+> **Доступ из фронтенда:** `window.go.admin.App.MethodName()`
+
+---
+
+## 📋 Содержание
+
+- [Сервисы](#-сервисы)
+- [Сайты](#-сайты)
+- [Прокси](#-прокси)
+- [Сертификаты](#-сертификаты)
+- [Конфигурация](#-конфигурация)
+- [vAccess](#-vaccess)
+- [Типы данных](#-типы-данных)
+- [События](#-события)
+
+---
+
+## 🔧 Сервисы
+
+### `GetAllServicesStatus()`
+Возвращает статусы всех сервисов.
+
+- **Параметры:** нет
+- **Возвращает:** `AllServicesStatus`
+
+```json
+{
+ "http": { "name": "HTTP", "status": true, "port": "80", "info": "" },
+ "https": { "name": "HTTPS", "status": true, "port": "443", "info": "" },
+ "mysql": { "name": "MySQL", "status": true, "port": "3306","info": "" },
+ "php": { "name": "PHP", "status": true, "port": "8000","info": "" },
+ "proxy": { "name": "Proxy", "status": false, "port": "", "info": "" }
+}
+```
+
+### `CheckServicesReady()`
+Проверяет, готовы ли все основные сервисы (HTTP, HTTPS, MySQL, PHP).
+
+- **Параметры:** нет
+- **Возвращает:** `bool`
+
+---
+
+### `StartServer()`
+Запускает все сервисы (HTTP, HTTPS, PHP, MySQL, SSL).
+
+- **Параметры:** нет
+- **Возвращает:** `string` — `"Server started"`
+
+### `StopServer()`
+Останавливает все сервисы.
+
+- **Параметры:** нет
+- **Возвращает:** `string` — `"Server stopped"`
+
+### `RestartAllServices()`
+Перезапускает все сервисы с перезагрузкой конфига и сертификатов.
+
+- **Параметры:** нет
+- **Возвращает:** `string` — `"All services restarted"`
+
+---
+
+### `StartHTTPService()` / `StopHTTPService()`
+Управление HTTP сервером (порт 80).
+
+- **Параметры:** нет
+- **Возвращает:** `string` — `"HTTP started"` / `"HTTP stopped"`
+
+### `StartHTTPSService()` / `StopHTTPSService()`
+Управление HTTPS сервером (порт 443).
+
+- **Параметры:** нет
+- **Возвращает:** `string` — `"HTTPS started"` / `"HTTPS stopped"`
+
+### `StartMySQLService()` / `StopMySQLService()`
+Управление MySQL сервером.
+
+- **Параметры:** нет
+- **Возвращает:** `string` — `"MySQL started"` / `"MySQL stopped"`
+
+### `StartPHPService()` / `StopPHPService()`
+Управление PHP FastCGI пулом.
+
+- **Параметры:** нет
+- **Возвращает:** `string` — `"PHP started"` / `"PHP stopped"`
+
+### `EnableProxyService()` / `DisableProxyService()`
+Включение/отключение прокси-сервиса. Сохраняет изменение в конфиг.
+
+- **Параметры:** нет
+- **Возвращает:** `string` — `"Proxy enabled"` / `"Proxy disabled"`
+
+### `EnableACMEService()` / `DisableACMEService()`
+Включение/отключение ACME (Let's Encrypt). Сохраняет изменение в конфиг.
+
+- **Параметры:** нет
+- **Возвращает:** `string` — `"ACME enabled"` / `"ACME disabled"`
+
+---
+
+## 🌐 Сайты
+
+### `GetSitesList()`
+Получает список всех сайтов.
+
+- **Параметры:** нет
+- **Возвращает:** `[]SiteInfo`
+
+```json
+[
+ {
+ "name": "My Site",
+ "host": "example.com",
+ "alias": ["www.example.com"],
+ "status": "active",
+ "root_file": "index.html",
+ "root_file_routing": false,
+ "auto_create_ssl": true
+ }
+]
+```
+
+### `CreateNewSite(siteJSON)`
+Создаёт новый сайт. Создаёт папку, конфиг и шаблон index.html.
+
+- **Параметры:** `siteJSON: string` — JSON строка с данными `SiteInfo`
+- **Возвращает:** `string` — `"Site created successfully"` или `"Error: ..."`
+
+### `DeleteSite(host)`
+Удаляет сайт (папку и запись из конфига).
+
+- **Параметры:** `host: string` — домен сайта
+- **Возвращает:** `string` — `"Site deleted successfully"` или `"Error: ..."`
+
+### `UpdateSiteCache()`
+Обновляет кэш статусов сайтов.
+
+- **Параметры:** нет
+- **Возвращает:** `string` — `"Cache updated"`
+
+### `OpenSiteFolder(host)`
+Открывает папку сайта в проводнике Windows.
+
+- **Параметры:** `host: string` — домен сайта
+- **Возвращает:** `string` — `"Folder opened"` или `"Error: ..."`
+
+---
+
+## 🔀 Прокси
+
+### `GetProxyList()`
+Получает список всех прокси-сервисов.
+
+- **Параметры:** нет
+- **Возвращает:** `[]ProxyInfo`
+
+```json
+[
+ {
+ "enable": true,
+ "external_domain": "app.example.com",
+ "local_address": "127.0.0.1",
+ "local_port": "3000",
+ "service_https_use": false,
+ "auto_https": true,
+ "auto_create_ssl": true,
+ "status": "active"
+ }
+]
+```
+
+---
+
+## 🔒 Сертификаты
+
+### `GetCertInfo(domain)`
+Получает информацию о сертификате для домена.
+
+- **Параметры:** `domain: string`
+- **Возвращает:** `CertInfo`
+
+```json
+{
+ "domain": "example.com",
+ "issuer": "Let's Encrypt",
+ "not_before": "2025-01-01T00:00:00Z",
+ "not_after": "2025-03-31T00:00:00Z",
+ "days_left": 60,
+ "is_expired": false,
+ "has_cert": true,
+ "dns_names": ["example.com", "www.example.com"]
+}
+```
+
+### `GetAllCertsInfo()`
+Получает информацию о всех сертификатах.
+
+- **Параметры:** нет
+- **Возвращает:** `[]CertInfo`
+
+### `ObtainSSLCertificate(domain)`
+Получает SSL сертификат через Let's Encrypt для указанного домена.
+
+- **Параметры:** `domain: string`
+- **Возвращает:** `string` — `"SSL certificate obtained successfully for ..."` или `"Error: ..."`
+
+### `ObtainAllSSLCertificates()`
+Получает сертификаты для всех доменов с флагом `auto_create_ssl: true`.
+
+- **Параметры:** нет
+- **Возвращает:** `string` — `"Completed: X success, Y errors"`
+
+### `UploadCertificate(host, certType, certDataBase64)`
+Загружает сертификат вручную.
+
+- **Параметры:**
+ - `host: string` — домен
+ - `certType: string` — тип файла (`"cert"` или `"key"`)
+ - `certDataBase64: string` — содержимое файла в Base64
+- **Возвращает:** `string` — `"Certificate uploaded successfully"` или `"Error: ..."`
+
+### `DeleteCertificate(domain)`
+Удаляет сертификат для домена.
+
+- **Параметры:** `domain: string`
+- **Возвращает:** `string` — `"Certificate deleted successfully"` или `"Error: ..."`
+
+### `ReloadSSLCertificates()`
+Перезагружает все SSL сертификаты.
+
+- **Параметры:** нет
+- **Возвращает:** `string` — `"SSL certificates reloaded"`
+
+---
+
+## ⚙️ Конфигурация
+
+### `GetConfig()`
+Возвращает текущую конфигурацию сервера.
+
+- **Параметры:** нет
+- **Возвращает:** `object` — полный объект конфигурации
+
+### `SaveConfig(configJSON)`
+Сохраняет конфигурацию в файл и перезагружает.
+
+- **Параметры:** `configJSON: string` — JSON строка с конфигурацией
+- **Возвращает:** `string` — `"Config saved"` или `"Error: ..."`
+
+### `ReloadConfig()`
+Перезагружает конфигурацию из файла.
+
+- **Параметры:** нет
+- **Возвращает:** `string` — `"Config reloaded"`
+
+---
+
+## 🛡️ vAccess
+
+### `GetVAccessRules(host, isProxy)`
+Получает правила доступа для сайта или прокси.
+
+- **Параметры:**
+ - `host: string` — домен
+ - `isProxy: bool` — `true` для прокси, `false` для сайта
+- **Возвращает:** `VAccessConfig`
+
+```json
+{
+ "rules": [
+ {
+ "type": "allow",
+ "type_file": [".php", ".html"],
+ "path_access": ["/admin"],
+ "ip_list": ["192.168.1.0/24"],
+ "exceptions_dir": ["/public"],
+ "url_error": "/403.html"
+ }
+ ]
+}
+```
+
+### `SaveVAccessRules(host, isProxy, configJSON)`
+Сохраняет правила доступа.
+
+- **Параметры:**
+ - `host: string` — домен
+ - `isProxy: bool` — `true` для прокси, `false` для сайта
+ - `configJSON: string` — JSON строка с `VAccessConfig`
+- **Возвращает:** `string` — `"vAccess saved"` или `"Error: ..."`
+
+---
+
+## 📦 Типы данных
+
+### `ServiceStatus`
+```json
+{
+ "name": "string",
+ "status": "bool",
+ "port": "string",
+ "info": "string"
+}
+```
+
+### `AllServicesStatus`
+```json
+{
+ "http": "ServiceStatus",
+ "https": "ServiceStatus",
+ "mysql": "ServiceStatus",
+ "php": "ServiceStatus",
+ "proxy": "ServiceStatus"
+}
+```
+
+### `SiteInfo`
+```json
+{
+ "name": "string",
+ "host": "string",
+ "alias": ["string"],
+ "status": "string",
+ "root_file": "string",
+ "root_file_routing": "bool",
+ "auto_create_ssl": "bool"
+}
+```
+
+### `ProxyInfo`
+```json
+{
+ "enable": "bool",
+ "external_domain": "string",
+ "local_address": "string",
+ "local_port": "string",
+ "service_https_use": "bool",
+ "auto_https": "bool",
+ "auto_create_ssl": "bool",
+ "status": "string"
+}
+```
+
+### `CertInfo`
+```json
+{
+ "domain": "string",
+ "issuer": "string",
+ "not_before": "string",
+ "not_after": "string",
+ "days_left": "int",
+ "is_expired": "bool",
+ "has_cert": "bool",
+ "dns_names": ["string"]
+}
+```
+
+### `VAccessRule`
+```json
+{
+ "type": "string",
+ "type_file": ["string"],
+ "path_access": ["string"],
+ "ip_list": ["string"],
+ "exceptions_dir": ["string"],
+ "url_error": "string"
+}
+```
+
+### `VAccessConfig`
+```json
+{
+ "rules": ["VAccessRule"]
+}
+```
+
+---
+
+## 📡 События (Wails Events)
+
+### `service:changed`
+Эмитится каждые 500мс с текущими статусами сервисов.
+
+- **Данные:** `AllServicesStatus`
+
+### `server:already_running`
+Эмитится при запуске, если vServer уже запущен в другом экземпляре.
+
+- **Данные:** `bool` — `true`
diff --git a/Backend/admin/frontend/assets/css/base.css b/Backend/admin/frontend/assets/css/base.css
deleted file mode 100644
index 9250c8a..0000000
--- a/Backend/admin/frontend/assets/css/base.css
+++ /dev/null
@@ -1,103 +0,0 @@
-/* ============================================
- 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
deleted file mode 100644
index 41b40b5..0000000
--- a/Backend/admin/frontend/assets/css/components/badges.css
+++ /dev/null
@@ -1,107 +0,0 @@
-/* ============================================
- 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);
-}
-
-.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);
-}
-
-.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);
-}
-
-/* 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);
-}
-
-.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);
- }
-}
-
-/* Certificate Icons */
-.cert-icon {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: 22px;
- height: 22px;
- margin-right: 8px;
- border-radius: var(--radius-sm);
- font-size: 12px;
-}
-
-.cert-valid {
- background: rgba(16, 185, 129, 0.2);
- color: var(--accent-green);
- border: 1px solid rgba(16, 185, 129, 0.4);
-}
-
-.cert-expired {
- background: rgba(239, 68, 68, 0.2);
- color: var(--accent-red);
- border: 1px solid rgba(239, 68, 68, 0.4);
-}
-
diff --git a/Backend/admin/frontend/assets/css/components/buttons.css b/Backend/admin/frontend/assets/css/components/buttons.css
deleted file mode 100644
index cb6b90c..0000000
--- a/Backend/admin/frontend/assets/css/components/buttons.css
+++ /dev/null
@@ -1,316 +0,0 @@
-/* ============================================
- 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);
- }
-}
-
-/* Delete Button Variant */
-.delete-btn {
- background: rgba(239, 68, 68, 0.15);
- border-color: rgba(239, 68, 68, 0.3);
- color: var(--accent-red);
-
- &:hover:not(:disabled) {
- background: rgba(239, 68, 68, 0.25);
- border-color: rgba(239, 68, 68, 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);
-
- &: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);
-
- &: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);
- }
-
- &: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;
- }
-}
-
diff --git a/Backend/admin/frontend/assets/css/components/cards.css b/Backend/admin/frontend/assets/css/components/cards.css
deleted file mode 100644
index 0c11abb..0000000
--- a/Backend/admin/frontend/assets/css/components/cards.css
+++ /dev/null
@@ -1,191 +0,0 @@
-/* ============================================
- 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
deleted file mode 100644
index a98e313..0000000
--- a/Backend/admin/frontend/assets/css/components/forms.css
+++ /dev/null
@@ -1,224 +0,0 @@
-/* ============================================
- 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 Row (3 columns) */
-.form-row.form-row-3 {
- grid-template-columns: 1fr 1fr 1fr;
-}
-
-/* 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
deleted file mode 100644
index 9e36285..0000000
--- a/Backend/admin/frontend/assets/css/components/modals.css
+++ /dev/null
@@ -1,227 +0,0 @@
-/* ============================================
- 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: 8px;
- padding: 20px var(--space-lg);
- border-top: 1px solid var(--glass-border);
-}
-
-.modal-footer .action-btn {
- margin: 0;
-}
-
-/* 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
deleted file mode 100644
index fd68e1a..0000000
--- a/Backend/admin/frontend/assets/css/components/tables.css
+++ /dev/null
@@ -1,236 +0,0 @@
-/* ============================================
- 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
deleted file mode 100644
index e68c91a..0000000
--- a/Backend/admin/frontend/assets/css/layout/container.css
+++ /dev/null
@@ -1,66 +0,0 @@
-/* ============================================
- 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-lg);
- 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
deleted file mode 100644
index 416b18e..0000000
--- a/Backend/admin/frontend/assets/css/layout/header.css
+++ /dev/null
@@ -1,75 +0,0 @@
-/* ============================================
- 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);
-}
-
-.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
deleted file mode 100644
index afcfc9b..0000000
--- a/Backend/admin/frontend/assets/css/layout/sidebar.css
+++ /dev/null
@@ -1,27 +0,0 @@
-/* ============================================
- 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
deleted file mode 100644
index 3bfa73b..0000000
--- a/Backend/admin/frontend/assets/css/local_lib/all.min.css
+++ /dev/null
@@ -1,29 +0,0 @@
-/*!
- * 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
deleted file mode 100644
index 8a480d9..0000000
Binary files a/Backend/admin/frontend/assets/css/local_lib/webfonts/fa-brands-400.woff2 and /dev/null 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
deleted file mode 100644
index 059a94e..0000000
Binary files a/Backend/admin/frontend/assets/css/local_lib/webfonts/fa-regular-400.woff2 and /dev/null 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
deleted file mode 100644
index 88b0367..0000000
Binary files a/Backend/admin/frontend/assets/css/local_lib/webfonts/fa-solid-900.woff2 and /dev/null differ
diff --git a/Backend/admin/frontend/assets/css/main.css b/Backend/admin/frontend/assets/css/main.css
deleted file mode 100644
index b996de6..0000000
--- a/Backend/admin/frontend/assets/css/main.css
+++ /dev/null
@@ -1,27 +0,0 @@
-/* ============================================
- 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';
-@import 'pages/site-creator.css';
-
diff --git a/Backend/admin/frontend/assets/css/pages/dashboard.css b/Backend/admin/frontend/assets/css/pages/dashboard.css
deleted file mode 100644
index a96ecbf..0000000
--- a/Backend/admin/frontend/assets/css/pages/dashboard.css
+++ /dev/null
@@ -1,62 +0,0 @@
-/* ============================================
- 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/site-creator.css b/Backend/admin/frontend/assets/css/pages/site-creator.css
deleted file mode 100644
index 53725c7..0000000
--- a/Backend/admin/frontend/assets/css/pages/site-creator.css
+++ /dev/null
@@ -1,272 +0,0 @@
-/* ============================================
- Site Creator Page
- Страница создания нового сайта
- ============================================ */
-
-/* Form Section */
-.form-section {
- padding: var(--space-xl);
- background: rgba(139, 92, 246, 0.02);
- border-radius: var(--radius-xl);
- border: 1px solid var(--glass-border);
- transition: all var(--transition-base);
-}
-
-.form-section:hover {
- background: rgba(139, 92, 246, 0.04);
- border-color: rgba(139, 92, 246, 0.2);
-}
-
-/* Subsection Title (внутри формы) */
-.form-subsection-title {
- font-size: var(--text-lg);
- font-weight: var(--font-semibold);
- color: var(--text-primary);
- margin: 0 0 var(--space-lg) 0;
- display: flex;
- align-items: center;
- gap: var(--space-md);
- padding-bottom: var(--space-sm);
- border-bottom: 1px solid rgba(139, 92, 246, 0.1);
-
- i {
- color: var(--accent-purple-light);
- font-size: 16px;
- }
-}
-
-/* Второй и последующие подзаголовки - добавляем отступ сверху */
-.form-subsection-title:not(:first-of-type) {
- margin-top: var(--space-xl);
-}
-
-/* Custom Select Styling */
-.form-input[type="text"],
-.form-input[type="number"] {
- appearance: none;
- -webkit-appearance: none;
- -moz-appearance: none;
- background-image: none;
-}
-
-/* Кастомный Select */
-.custom-select {
- position: relative;
- width: 100%;
-}
-
-.custom-select-trigger {
- padding: 10px 40px 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);
- cursor: pointer;
- transition: all var(--transition-base);
- display: flex;
- align-items: center;
- justify-content: space-between;
- user-select: none;
-}
-
-.custom-select-trigger:hover {
- border-color: rgba(139, 92, 246, 0.4);
- background-color: rgba(139, 92, 246, 0.05);
-}
-
-.custom-select.open .custom-select-trigger {
- border-color: rgba(139, 92, 246, 0.6);
- box-shadow: 0 0 16px rgba(139, 92, 246, 0.2);
- background-color: rgba(139, 92, 246, 0.03);
-}
-
-.custom-select-value {
- flex: 1;
-}
-
-.custom-select-arrow {
- color: var(--accent-purple-light);
- font-size: 12px;
- transition: transform var(--transition-base);
-}
-
-.custom-select.open .custom-select-arrow {
- transform: rotate(180deg);
-}
-
-/* Dropdown */
-.custom-select-dropdown {
- position: absolute;
- top: calc(100% + 4px);
- left: 0;
- right: 0;
- background: #1a1d2e;
- border: 1px solid rgba(139, 92, 246, 0.3);
- border-radius: var(--radius-md);
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
- max-height: 0;
- overflow: hidden;
- opacity: 0;
- transform: translateY(-10px);
- transition: all var(--transition-base);
- z-index: 1000;
-}
-
-.custom-select.open .custom-select-dropdown {
- max-height: 300px;
- overflow-y: auto;
- opacity: 1;
- transform: translateY(0);
-}
-
-/* Scrollbar для dropdown */
-.custom-select-dropdown::-webkit-scrollbar {
- width: 6px;
-}
-
-.custom-select-dropdown::-webkit-scrollbar-track {
- background: transparent;
-}
-
-.custom-select-dropdown::-webkit-scrollbar-thumb {
- background: rgba(139, 92, 246, 0.3);
- border-radius: 3px;
-}
-
-.custom-select-dropdown::-webkit-scrollbar-thumb:hover {
- background: rgba(139, 92, 246, 0.5);
-}
-
-/* Option */
-.custom-select-option {
- padding: 10px 14px;
- color: var(--text-secondary);
- cursor: pointer;
- transition: all var(--transition-base);
- font-size: var(--text-base);
-}
-
-.custom-select-option:hover {
- background: rgba(139, 92, 246, 0.1);
- color: var(--text-primary);
-}
-
-.custom-select-option.selected {
- background: rgba(139, 92, 246, 0.2);
- color: var(--accent-purple-light);
- font-weight: var(--font-semibold);
-}
-
-.custom-select-option.selected::before {
- content: '✓ ';
- margin-right: 8px;
-}
-
-/* File Upload Styling */
-.file-upload-wrapper {
- position: relative;
- display: inline-block;
- width: 100%;
-}
-
-.file-input {
- position: absolute;
- opacity: 0;
- width: 0;
- height: 0;
- pointer-events: none;
-}
-
-.file-upload-btn {
- display: flex;
- align-items: center;
- justify-content: center;
- gap: var(--space-md);
- padding: 12px 20px;
- background: var(--glass-bg-dark);
- border: 2px dashed var(--glass-border);
- border-radius: var(--radius-md);
- color: var(--text-secondary);
- font-size: var(--text-base);
- font-weight: var(--font-medium);
- cursor: pointer;
- transition: all var(--transition-base);
- text-align: center;
-
- i {
- color: var(--accent-purple-light);
- font-size: 18px;
- }
-
- &:hover {
- background: rgba(139, 92, 246, 0.05);
- border-color: rgba(139, 92, 246, 0.4);
- color: var(--text-primary);
- transform: translateY(-1px);
- }
-
- &:active {
- transform: translateY(0);
- }
-}
-
-.file-input:focus + .file-upload-btn {
- border-color: rgba(139, 92, 246, 0.6);
- box-shadow: 0 0 16px rgba(139, 92, 246, 0.2);
-}
-
-/* Drag over state */
-.file-upload-wrapper.drag-over .file-upload-btn {
- background: rgba(139, 92, 246, 0.15);
- border-color: rgba(139, 92, 246, 0.7);
- border-style: solid;
- color: var(--text-primary);
- transform: scale(1.02);
- box-shadow: 0 0 20px rgba(139, 92, 246, 0.3);
-}
-
-.file-upload-wrapper.drag-over .file-upload-btn i {
- animation: bounce 0.6s ease infinite;
-}
-
-@keyframes bounce {
- 0%, 100% {
- transform: translateY(0);
- }
- 50% {
- transform: translateY(-4px);
- }
-}
-
-/* File uploaded state */
-.file-uploaded {
- border-style: solid;
- border-color: rgba(16, 185, 129, 0.5);
- background: rgba(16, 185, 129, 0.05);
-
- &:hover {
- border-color: rgba(16, 185, 129, 0.6);
- background: rgba(16, 185, 129, 0.1);
- }
-
- i {
- color: var(--accent-green);
- }
-}
-
-/* File status */
-#certFileStatus,
-#keyFileStatus,
-#caFileStatus {
- font-size: var(--text-sm);
- font-weight: var(--font-medium);
- display: flex;
- align-items: center;
- gap: var(--space-sm);
-
- i {
- font-size: 14px;
- }
-}
-
diff --git a/Backend/admin/frontend/assets/css/pages/vaccess.css b/Backend/admin/frontend/assets/css/pages/vaccess.css
deleted file mode 100644
index ef21f9d..0000000
--- a/Backend/admin/frontend/assets/css/pages/vaccess.css
+++ /dev/null
@@ -1,468 +0,0 @@
-/* ============================================
- 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);
- }
-}
-
-/* ============================================
- Cert Manager
- ============================================ */
-
-.cert-manager-content {
- display: flex;
- flex-direction: column;
- gap: var(--space-md);
-}
-
-.cert-card {
- background: rgba(139, 92, 246, 0.03);
- border: 1px solid var(--glass-border);
- border-radius: var(--radius-xl);
- padding: var(--space-lg);
- transition: all var(--transition-base);
-}
-
-.cert-card-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: var(--space-md);
- padding-bottom: var(--space-md);
- border-bottom: 1px solid var(--glass-border);
-}
-
-.cert-card-title {
- display: flex;
- align-items: center;
- gap: var(--space-md);
-
- i {
- font-size: 24px;
- color: var(--accent-green);
- }
-
- h3 {
- margin: 0;
- font-size: var(--text-xl);
- font-weight: var(--font-semibold);
- color: var(--text-primary);
- }
-}
-
-.cert-card-title.expired i {
- color: var(--accent-red);
-}
-
-.cert-card-actions {
- display: flex;
- gap: var(--space-sm);
-}
-
-.cert-info-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
- gap: var(--space-md);
-}
-
-.cert-info-item {
- padding: var(--space-md);
- background: rgba(255, 255, 255, 0.02);
- border-radius: var(--radius-md);
-
- .cert-info-label {
- font-size: var(--text-sm);
- color: var(--text-muted);
- margin-bottom: var(--space-xs);
- }
-
- .cert-info-value {
- font-size: var(--text-md);
- color: var(--text-primary);
- font-weight: var(--font-medium);
- }
-
- .cert-info-value.valid {
- color: var(--accent-green);
- }
-
- .cert-info-value.expired {
- color: var(--accent-red);
- }
-}
-
-.cert-domains-list {
- display: flex;
- flex-wrap: wrap;
- gap: var(--space-sm);
- margin-top: var(--space-md);
-}
-
-.cert-domain-tag {
- padding: 4px 12px;
- background: rgba(139, 92, 246, 0.15);
- border-radius: var(--radius-md);
- font-size: var(--text-sm);
- color: var(--accent-purple-light);
- font-family: var(--font-mono);
-}
-
-.cert-empty {
- text-align: center;
- padding: 60px 40px;
- color: var(--text-muted);
-
- i {
- font-size: 48px;
- margin-bottom: var(--space-lg);
- opacity: 0.3;
- }
-
- h3 {
- font-size: var(--text-xl);
- font-weight: var(--font-semibold);
- color: var(--text-primary);
- margin-bottom: var(--space-sm);
- }
-
- p {
- font-size: var(--text-md);
- }
-}
-
-/* Wildcard Info Block */
-.cert-wildcard-info {
- padding: var(--space-md);
- background: rgba(139, 92, 246, 0.05);
- border-radius: var(--radius-lg);
-}
-
-.cert-wildcard-header {
- display: flex;
- align-items: center;
- gap: var(--space-sm);
- margin-bottom: var(--space-xs);
- font-size: var(--text-md);
- font-weight: var(--font-medium);
- color: var(--accent-purple-light);
-
- i {
- color: var(--accent-purple-light);
- }
-}
-
-.cert-wildcard-info > p {
- margin: 0 0 var(--space-sm) 0;
- color: var(--text-muted);
- font-size: var(--text-sm);
-}
-
-.cert-wildcard-list {
- display: flex;
- flex-direction: column;
- gap: var(--space-xs);
-}
-
-.cert-wildcard-item {
- display: flex;
- align-items: center;
- gap: var(--space-md);
- padding: var(--space-xs) var(--space-sm);
- background: rgba(139, 92, 246, 0.08);
- border-radius: var(--radius-sm);
-}
-
-.cert-wildcard-item.expired {
- opacity: 0.7;
-}
-
-.cert-wildcard-domain {
- font-family: var(--font-mono);
- font-size: var(--text-sm);
- color: var(--text-primary);
-}
-
-.cert-wildcard-status {
- font-size: var(--text-sm);
- color: var(--accent-green);
-}
-
-.cert-wildcard-item.expired .cert-wildcard-status {
- color: var(--accent-red);
-}
-
-.cert-wildcard-issuer {
- font-size: var(--text-sm);
- color: var(--text-muted);
-}
-
-/* Card Variants */
-.cert-card-wildcard {
- /* без особых стилей */
-}
-
-.cert-card-local {
- opacity: 0.6;
-}
-
-.cert-card-empty {
- border-style: dashed;
-}
-
diff --git a/Backend/admin/frontend/assets/css/variables.css b/Backend/admin/frontend/assets/css/variables.css
deleted file mode 100644
index b4b6836..0000000
--- a/Backend/admin/frontend/assets/css/variables.css
+++ /dev/null
@@ -1,100 +0,0 @@
-/* ============================================
- 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
deleted file mode 100644
index 3e2e23f..0000000
--- a/Backend/admin/frontend/assets/js/api/config.js
+++ /dev/null
@@ -1,118 +0,0 @@
-/* ============================================
- Config API
- Работа с конфигурацией
- ============================================ */
-
-import { isWailsAvailable } 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) {
- return null;
- }
- }
-
- // Сохранить конфигурацию
- async saveConfig(configJSON) {
- if (!this.available) return 'Error: API недоступен';
- try {
- return await window.go.admin.App.SaveConfig(configJSON);
- } catch (error) {
- return `Error: ${error.message}`;
- }
- }
-
- // Включить Proxy Service
- async enableProxyService() {
- if (!this.available) return;
- try {
- await window.go.admin.App.EnableProxyService();
- } catch (error) {
- }
- }
-
- // Отключить Proxy Service
- async disableProxyService() {
- if (!this.available) return;
- try {
- await window.go.admin.App.DisableProxyService();
- } catch (error) {
- }
- }
-
- // Включить ACME Service
- async enableACMEService() {
- if (!this.available) return;
- try {
- await window.go.admin.App.EnableACMEService();
- } catch (error) {
- }
- }
-
- // Отключить ACME Service
- async disableACMEService() {
- if (!this.available) return;
- try {
- await window.go.admin.App.DisableACMEService();
- } catch (error) {
- }
- }
-
- // Перезапустить все сервисы
- async restartAllServices() {
- if (!this.available) return;
- try {
- await window.go.admin.App.RestartAllServices();
- } catch (error) {
- }
- }
-
- // Запустить HTTP Service
- async startHTTPService() {
- if (!this.available) return;
- try {
- await window.go.admin.App.StartHTTPService();
- } catch (error) {
- }
- }
-
- // Остановить HTTP Service
- async stopHTTPService() {
- if (!this.available) return;
- try {
- await window.go.admin.App.StopHTTPService();
- } catch (error) {
- }
- }
-
- // Запустить HTTPS Service
- async startHTTPSService() {
- if (!this.available) return;
- try {
- await window.go.admin.App.StartHTTPSService();
- } catch (error) {
- }
- }
-
- // Остановить HTTPS Service
- async stopHTTPSService() {
- if (!this.available) return;
- try {
- await window.go.admin.App.StopHTTPSService();
- } catch (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
deleted file mode 100644
index 49345d5..0000000
--- a/Backend/admin/frontend/assets/js/api/wails.js
+++ /dev/null
@@ -1,192 +0,0 @@
-/* ============================================
- Wails API Wrapper
- Обёртка над Wails API
- ============================================ */
-
-import { isWailsAvailable } from '../utils/helpers.js';
-
-// Базовый класс для работы с Wails API
-class WailsAPI {
- constructor() {
- this.available = isWailsAvailable();
- }
-
- // Проверка доступности API
- checkAvailability() {
- if (!this.available) {
- return false;
- }
- return true;
- }
-
- // Получить статус всех сервисов
- async getAllServicesStatus() {
- if (!this.checkAvailability()) return null;
- try {
- return await window.go.admin.App.GetAllServicesStatus();
- } catch (error) {
- return null;
- }
- }
-
- // Получить список сайтов
- async getSitesList() {
- if (!this.checkAvailability()) return [];
- try {
- return await window.go.admin.App.GetSitesList();
- } catch (error) {
- return [];
- }
- }
-
- // Получить список прокси
- async getProxyList() {
- if (!this.checkAvailability()) return [];
- try {
- return await window.go.admin.App.GetProxyList();
- } catch (error) {
- return [];
- }
- }
-
- // Получить правила vAccess
- async getVAccessRules(host, isProxy) {
- if (!this.checkAvailability()) return { rules: [] };
- try {
- return await window.go.admin.App.GetVAccessRules(host, isProxy);
- } catch (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) {
- return `Error: ${error.message}`;
- }
- }
-
- // Запустить сервер
- async startServer() {
- if (!this.checkAvailability()) return;
- try {
- await window.go.admin.App.StartServer();
- } catch (error) {
- }
- }
-
- // Остановить сервер
- async stopServer() {
- if (!this.checkAvailability()) return;
- try {
- await window.go.admin.App.StopServer();
- } catch (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) {
- }
- }
-
- // Создать новый сайт
- async createNewSite(siteJSON) {
- if (!this.checkAvailability()) return 'Error: API недоступен';
- try {
- return await window.go.admin.App.CreateNewSite(siteJSON);
- } catch (error) {
- return `Error: ${error.message}`;
- }
- }
-
- // Загрузить сертификат для сайта
- async uploadCertificate(host, certType, certDataBase64) {
- if (!this.checkAvailability()) return 'Error: API недоступен';
- try {
- return await window.go.admin.App.UploadCertificate(host, certType, certDataBase64);
- } catch (error) {
- return `Error: ${error.message}`;
- }
- }
-
- // Перезагрузить SSL сертификаты
- async reloadSSLCertificates() {
- if (!this.checkAvailability()) return 'Error: API недоступен';
- try {
- return await window.go.admin.App.ReloadSSLCertificates();
- } catch (error) {
- return `Error: ${error.message}`;
- }
- }
-
- // Удалить сайт
- async deleteSite(host) {
- if (!this.checkAvailability()) return 'Error: API недоступен';
- try {
- return await window.go.admin.App.DeleteSite(host);
- } catch (error) {
- return `Error: ${error.message}`;
- }
- }
-
- // Получить информацию о сертификате для домена
- async getCertInfo(domain) {
- if (!this.checkAvailability()) return { has_cert: false };
- try {
- return await window.go.admin.App.GetCertInfo(domain);
- } catch (error) {
- return { has_cert: false };
- }
- }
-
- // Получить информацию о всех сертификатах
- async getAllCertsInfo() {
- if (!this.checkAvailability()) return [];
- try {
- return await window.go.admin.App.GetAllCertsInfo();
- } catch (error) {
- return [];
- }
- }
-
- // Удалить сертификат
- async deleteCertificate(domain) {
- if (!this.checkAvailability()) return 'Error: API недоступен';
- try {
- return await window.go.admin.App.DeleteCertificate(domain);
- } catch (error) {
- return `Error: ${error.message}`;
- }
- }
-
- // Получить SSL сертификат через Let's Encrypt
- async obtainSSLCertificate(domain) {
- if (!this.checkAvailability()) return 'Error: API недоступен';
- try {
- return await window.go.admin.App.ObtainSSLCertificate(domain);
- } catch (error) {
- return `Error: ${error.message}`;
- }
- }
-}
-
-// Экспортируем единственный экземпляр
-export const api = new WailsAPI();
-
diff --git a/Backend/admin/frontend/assets/js/components/proxy-creator.js b/Backend/admin/frontend/assets/js/components/proxy-creator.js
deleted file mode 100644
index 3bd1c57..0000000
--- a/Backend/admin/frontend/assets/js/components/proxy-creator.js
+++ /dev/null
@@ -1,311 +0,0 @@
-/* ============================================
- Proxy Creator Component
- Управление созданием новых прокси сервисов
- ============================================ */
-
-import { api } from '../api/wails.js';
-import { configAPI } from '../api/config.js';
-import { $, hide, show } from '../utils/dom.js';
-import { notification } from '../ui/notification.js';
-import { isWailsAvailable } from '../utils/helpers.js';
-import { initCustomSelects } from '../ui/custom-select.js';
-
-export class ProxyCreator {
- constructor() {
- this.certificates = {
- certificate: null,
- privatekey: null,
- cabundle: null
- };
- }
-
- open() {
- this.hideAllSections();
- show($('sectionAddProxy'));
- this.resetForm();
- this.attachEventListeners();
- setTimeout(() => initCustomSelects(), 100);
- }
-
- hideAllSections() {
- hide($('sectionServices'));
- hide($('sectionSites'));
- hide($('sectionProxy'));
- hide($('sectionSettings'));
- hide($('sectionVAccessEditor'));
- hide($('sectionAddSite'));
- hide($('sectionAddProxy'));
- }
-
- backToMain() {
- this.hideAllSections();
- show($('sectionServices'));
- show($('sectionSites'));
- show($('sectionProxy'));
- }
-
- resetForm() {
- $('newProxyDomain').value = '';
- $('newProxyLocalAddr').value = '127.0.0.1';
- $('newProxyLocalPort').value = '';
- $('newProxyStatus').value = 'enable';
- $('newProxyServiceHTTPS').checked = false;
- $('newProxyAutoHTTPS').checked = true;
- $('proxyCertMode').value = 'none';
-
- this.certificates = {
- certificate: null,
- privatekey: null,
- cabundle: null
- };
-
- hide($('proxyCertUploadBlock'));
-
- $('proxyCertFileStatus').innerHTML = '';
- $('proxyKeyFileStatus').innerHTML = '';
- $('proxyCaFileStatus').innerHTML = '';
-
- if ($('proxyCertFileName')) $('proxyCertFileName').textContent = 'Выберите файл...';
- if ($('proxyKeyFileName')) $('proxyKeyFileName').textContent = 'Выберите файл...';
- if ($('proxyCaFileName')) $('proxyCaFileName').textContent = 'Выберите файл...';
-
- if ($('proxyCertFile')) $('proxyCertFile').value = '';
- if ($('proxyKeyFile')) $('proxyKeyFile').value = '';
- if ($('proxyCaFile')) $('proxyCaFile').value = '';
-
- const labels = document.querySelectorAll('#sectionAddProxy .file-upload-btn');
- labels.forEach(label => label.classList.remove('file-uploaded'));
- }
-
- attachEventListeners() {
- const createBtn = $('createProxyBtn');
- if (createBtn) {
- createBtn.onclick = async () => await this.createProxy();
- }
-
- this.setupDragAndDrop();
- }
-
- setupDragAndDrop() {
- const fileWrappers = [
- { wrapper: document.querySelector('label[for="proxyCertFile"]')?.parentElement, input: $('proxyCertFile'), type: 'certificate' },
- { wrapper: document.querySelector('label[for="proxyKeyFile"]')?.parentElement, input: $('proxyKeyFile'), type: 'privatekey' },
- { wrapper: document.querySelector('label[for="proxyCaFile"]')?.parentElement, input: $('proxyCaFile'), type: 'cabundle' }
- ];
-
- fileWrappers.forEach(({ wrapper, input, type }) => {
- if (!wrapper || !input) return;
-
- ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
- wrapper.addEventListener(eventName, (e) => {
- e.preventDefault();
- e.stopPropagation();
- });
- });
-
- ['dragenter', 'dragover'].forEach(eventName => {
- wrapper.addEventListener(eventName, () => {
- wrapper.classList.add('drag-over');
- });
- });
-
- ['dragleave', 'drop'].forEach(eventName => {
- wrapper.addEventListener(eventName, () => {
- wrapper.classList.remove('drag-over');
- });
- });
-
- wrapper.addEventListener('drop', (e) => {
- const files = e.dataTransfer.files;
- if (files.length > 0) {
- const dataTransfer = new DataTransfer();
- dataTransfer.items.add(files[0]);
- input.files = dataTransfer.files;
-
- const event = new Event('change', { bubbles: true });
- input.dispatchEvent(event);
- }
- });
- });
- }
-
- toggleCertUpload() {
- const mode = $('proxyCertMode')?.value;
- const block = $('proxyCertUploadBlock');
-
- if (mode === 'upload') {
- show(block);
- } else {
- hide(block);
- }
- }
-
- handleCertFile(input, certType) {
- const file = input.files[0];
- const statusId = certType === 'certificate' ? 'proxyCertFileStatus' :
- certType === 'privatekey' ? 'proxyKeyFileStatus' : 'proxyCaFileStatus';
- const labelId = certType === 'certificate' ? 'proxyCertFileName' :
- certType === 'privatekey' ? 'proxyKeyFileName' : 'proxyCaFileName';
-
- const statusDiv = $(statusId);
- const labelSpan = $(labelId);
- const labelBtn = input.nextElementSibling;
-
- if (!file) {
- this.certificates[certType] = null;
- statusDiv.innerHTML = '';
- if (labelSpan) labelSpan.textContent = 'Выберите файл...';
- if (labelBtn) labelBtn.classList.remove('file-uploaded');
- return;
- }
-
- if (file.size > 1024 * 1024) {
- statusDiv.innerHTML = ' Файл слишком большой (макс 1MB)';
- this.certificates[certType] = null;
- input.value = '';
- if (labelSpan) labelSpan.textContent = 'Выберите файл...';
- if (labelBtn) labelBtn.classList.remove('file-uploaded');
- return;
- }
-
- if (labelSpan) labelSpan.textContent = file.name;
- if (labelBtn) labelBtn.classList.add('file-uploaded');
-
- const reader = new FileReader();
- reader.onload = (e) => {
- const content = e.target.result;
- this.certificates[certType] = btoa(content);
- statusDiv.innerHTML = ` Загружен успешно`;
- };
- reader.onerror = () => {
- statusDiv.innerHTML = ' Ошибка чтения файла';
- this.certificates[certType] = null;
- if (labelSpan) labelSpan.textContent = 'Выберите файл...';
- if (labelBtn) labelBtn.classList.remove('file-uploaded');
- };
- reader.readAsText(file);
- }
-
- validateForm() {
- const domain = $('newProxyDomain')?.value.trim();
- const localAddr = $('newProxyLocalAddr')?.value.trim();
- const localPort = $('newProxyLocalPort')?.value.trim();
- const certMode = $('proxyCertMode')?.value;
-
- if (!domain) {
- notification.error('❌ Укажите внешний домен');
- return false;
- }
-
- if (!localAddr) {
- notification.error('❌ Укажите локальный адрес');
- return false;
- }
-
- if (!localPort) {
- notification.error('❌ Укажите локальный порт');
- return false;
- }
-
- if (certMode === 'upload') {
- if (!this.certificates.certificate) {
- notification.error('❌ Загрузите файл certificate.crt');
- return false;
- }
- if (!this.certificates.privatekey) {
- notification.error('❌ Загрузите файл private.key');
- return false;
- }
- }
-
- return true;
- }
-
- async createProxy() {
- if (!this.validateForm()) {
- return;
- }
-
- if (!isWailsAvailable()) {
- notification.error('Wails API недоступен');
- return;
- }
-
- const createBtn = $('createProxyBtn');
- const originalText = createBtn.querySelector('span').textContent;
-
- try {
- createBtn.disabled = true;
- createBtn.querySelector('span').textContent = 'Создание...';
-
- const certMode = $('proxyCertMode').value;
-
- const proxyData = {
- Enable: $('newProxyStatus').value === 'enable',
- ExternalDomain: $('newProxyDomain').value.trim(),
- LocalAddress: $('newProxyLocalAddr').value.trim(),
- LocalPort: $('newProxyLocalPort').value.trim(),
- ServiceHTTPSuse: $('newProxyServiceHTTPS').checked,
- AutoHTTPS: $('newProxyAutoHTTPS').checked,
- AutoCreateSSL: certMode === 'auto'
- };
-
- const config = await configAPI.getConfig();
-
- if (!config.Proxy_Service) {
- config.Proxy_Service = [];
- }
-
- config.Proxy_Service.push(proxyData);
-
- const result = await configAPI.saveConfig(JSON.stringify(config, null, 4));
-
- if (result.startsWith('Error')) {
- notification.error(result, 3000);
- return;
- }
-
- notification.success('✅ Прокси сервис создан!', 1500);
-
- if (certMode === 'upload') {
- createBtn.querySelector('span').textContent = 'Загрузка сертификатов...';
-
- if (this.certificates.certificate) {
- await api.uploadCertificate(proxyData.ExternalDomain, 'certificate', this.certificates.certificate);
- }
-
- if (this.certificates.privatekey) {
- await api.uploadCertificate(proxyData.ExternalDomain, 'privatekey', this.certificates.privatekey);
- }
-
- if (this.certificates.cabundle) {
- await api.uploadCertificate(proxyData.ExternalDomain, 'cabundle', this.certificates.cabundle);
- }
-
- notification.success('🔒 Сертификаты загружены!', 1500);
- }
-
- createBtn.querySelector('span').textContent = 'Перезапуск серверов...';
- await configAPI.stopHTTPService();
- await configAPI.stopHTTPSService();
- await new Promise(resolve => setTimeout(resolve, 500));
- await configAPI.startHTTPService();
- await configAPI.startHTTPSService();
-
- notification.success('🚀 Серверы перезапущены! Прокси готов к работе!', 2000);
-
- setTimeout(() => {
- this.backToMain();
- if (window.proxyManager) {
- window.proxyManager.load();
- }
- }, 1000);
-
- } catch (error) {
- notification.error('Ошибка: ' + error.message, 3000);
- } finally {
- createBtn.disabled = false;
- createBtn.querySelector('span').textContent = originalText;
- }
- }
-}
diff --git a/Backend/admin/frontend/assets/js/components/proxy.js b/Backend/admin/frontend/assets/js/components/proxy.js
deleted file mode 100644
index ea75b62..0000000
--- a/Backend/admin/frontend/assets/js/components/proxy.js
+++ /dev/null
@@ -1,216 +0,0 @@
-/* ============================================
- 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.certsCache = {};
- 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,
- auto_create_ssl: 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,
- auto_create_ssl: 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,
- auto_create_ssl: false,
- status: 'disabled'
- }
- ];
- this.mockCerts = {
- 'git.example.ru': { has_cert: true, is_expired: false, days_left: 60 }
- };
- }
-
- // Загрузить список прокси
- async load() {
- if (isWailsAvailable()) {
- this.proxiesData = await api.getProxyList();
- await this.loadCertsInfo();
- } else {
- this.proxiesData = this.mockData;
- this.certsCache = this.mockCerts;
- }
- this.render();
- }
-
- // Загрузить информацию о сертификатах
- async loadCertsInfo() {
- const allCerts = await api.getAllCertsInfo();
- this.certsCache = {};
- for (const cert of allCerts) {
- this.certsCache[cert.domain] = cert;
- }
- }
-
- // Проверить соответствие домена wildcard паттерну
- matchesWildcard(domain, pattern) {
- if (pattern.startsWith('*.')) {
- const wildcardBase = pattern.slice(2);
- const domainParts = domain.split('.');
- if (domainParts.length >= 2) {
- const domainBase = domainParts.slice(1).join('.');
- return domainBase === wildcardBase;
- }
- }
- return domain === pattern;
- }
-
- // Найти сертификат для домена (включая wildcard)
- findCertForDomain(domain) {
- if (this.certsCache[domain]?.has_cert) {
- return this.certsCache[domain];
- }
-
- const domainParts = domain.split('.');
- if (domainParts.length >= 2) {
- const wildcardDomain = '*.' + domainParts.slice(1).join('.');
- if (this.certsCache[wildcardDomain]?.has_cert) {
- return this.certsCache[wildcardDomain];
- }
- }
-
- for (const [certDomain, cert] of Object.entries(this.certsCache)) {
- if (cert.has_cert && cert.dns_names) {
- for (const dnsName of cert.dns_names) {
- if (this.matchesWildcard(domain, dnsName)) {
- return cert;
- }
- }
- }
- }
-
- return null;
- }
-
- // Получить иконку сертификата для домена
- getCertIcon(domain) {
- const cert = this.findCertForDomain(domain);
- if (cert) {
- if (cert.is_expired) {
- return ``;
- } else {
- return ``;
- }
- }
- return '';
- }
-
- // Отрисовать список прокси
- 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';
- const certIcon = this.getCertIcon(proxy.external_domain);
-
- row.innerHTML = `
-
${certIcon}${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 'open-certs':
- if (window.openCertManager) {
- window.openCertManager(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
deleted file mode 100644
index ee3afc8..0000000
--- a/Backend/admin/frontend/assets/js/components/services.js
+++ /dev/null
@@ -1,175 +0,0 @@
-/* ============================================
- 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/site-creator.js b/Backend/admin/frontend/assets/js/components/site-creator.js
deleted file mode 100644
index c97cbe3..0000000
--- a/Backend/admin/frontend/assets/js/components/site-creator.js
+++ /dev/null
@@ -1,373 +0,0 @@
-/* ============================================
- Site Creator Component
- Управление созданием новых сайтов
- ============================================ */
-
-import { api } from '../api/wails.js';
-import { configAPI } from '../api/config.js';
-import { $, hide, show } from '../utils/dom.js';
-import { notification } from '../ui/notification.js';
-import { isWailsAvailable } from '../utils/helpers.js';
-import { initCustomSelects } from '../ui/custom-select.js';
-
-// Класс для создания новых сайтов
-export class SiteCreator {
- constructor() {
- this.aliases = [];
- this.certificates = {
- certificate: null,
- privatekey: null,
- cabundle: null
- };
- }
-
- // Открыть страницу создания сайта
- open() {
- // Скрываем все секции
- this.hideAllSections();
-
- // Показываем страницу создания
- show($('sectionAddSite'));
-
- // Очищаем форму
- this.resetForm();
-
- // Привязываем обработчики
- this.attachEventListeners();
-
- // Инициализируем кастомные select'ы
- setTimeout(() => initCustomSelects(), 100);
- }
-
- // Скрыть все секции
- hideAllSections() {
- hide($('sectionServices'));
- hide($('sectionSites'));
- hide($('sectionProxy'));
- hide($('sectionSettings'));
- hide($('sectionVAccessEditor'));
- hide($('sectionAddSite'));
- hide($('sectionAddProxy'));
- }
-
- // Вернуться на главную
- backToMain() {
- this.hideAllSections();
- show($('sectionServices'));
- show($('sectionSites'));
- show($('sectionProxy'));
- }
-
- // Очистить форму
- resetForm() {
- $('newSiteName').value = '';
- $('newSiteHost').value = '';
- $('newSiteAliasInput').value = '';
- $('newSiteRootFile').value = 'index.html';
- $('newSiteStatus').value = 'active';
- $('newSiteRouting').checked = true;
- $('certMode').value = 'none';
-
- this.aliases = [];
- this.certificates = {
- certificate: null,
- privatekey: null,
- cabundle: null
- };
-
- // Скрываем блок загрузки сертификатов
- hide($('certUploadBlock'));
-
- // Очищаем статусы файлов
- $('certFileStatus').innerHTML = '';
- $('keyFileStatus').innerHTML = '';
- $('caFileStatus').innerHTML = '';
-
- // Очищаем labels файлов
- if ($('certFileName')) $('certFileName').textContent = 'Выберите файл...';
- if ($('keyFileName')) $('keyFileName').textContent = 'Выберите файл...';
- if ($('caFileName')) $('caFileName').textContent = 'Выберите файл...';
-
- // Очищаем input файлов
- if ($('certFile')) $('certFile').value = '';
- if ($('keyFile')) $('keyFile').value = '';
- if ($('caFile')) $('caFile').value = '';
-
- // Убираем класс uploaded
- const labels = document.querySelectorAll('.file-upload-btn');
- labels.forEach(label => label.classList.remove('file-uploaded'));
- }
-
- // Привязать обработчики событий
- attachEventListeners() {
- const createBtn = $('createSiteBtn');
- if (createBtn) {
- createBtn.onclick = async () => await this.createSite();
- }
-
- // Drag & Drop для файлов сертификатов
- this.setupDragAndDrop();
- }
-
- // Настроить Drag & Drop для файлов
- setupDragAndDrop() {
- const fileWrappers = [
- { wrapper: document.querySelector('label[for="certFile"]')?.parentElement, input: $('certFile'), type: 'certificate' },
- { wrapper: document.querySelector('label[for="keyFile"]')?.parentElement, input: $('keyFile'), type: 'privatekey' },
- { wrapper: document.querySelector('label[for="caFile"]')?.parentElement, input: $('caFile'), type: 'cabundle' }
- ];
-
- fileWrappers.forEach(({ wrapper, input, type }) => {
- if (!wrapper || !input) return;
-
- // Предотвращаем стандартное поведение
- ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
- wrapper.addEventListener(eventName, (e) => {
- e.preventDefault();
- e.stopPropagation();
- });
- });
-
- // Подсветка при наведении файла
- ['dragenter', 'dragover'].forEach(eventName => {
- wrapper.addEventListener(eventName, () => {
- wrapper.classList.add('drag-over');
- });
- });
-
- ['dragleave', 'drop'].forEach(eventName => {
- wrapper.addEventListener(eventName, () => {
- wrapper.classList.remove('drag-over');
- });
- });
-
- // Обработка dropped файла
- wrapper.addEventListener('drop', (e) => {
- const files = e.dataTransfer.files;
- if (files.length > 0) {
- // Создаём объект DataTransfer и присваиваем файлы input'у
- const dataTransfer = new DataTransfer();
- dataTransfer.items.add(files[0]);
- input.files = dataTransfer.files;
-
- // Триггерим событие change
- const event = new Event('change', { bubbles: true });
- input.dispatchEvent(event);
- }
- });
- });
- }
-
- // Парсить aliases из строки (через запятую)
- parseAliases() {
- const input = $('newSiteAliasInput');
- const value = input?.value.trim();
-
- if (!value) {
- this.aliases = [];
- return;
- }
-
- // Разделяем по запятой и очищаем
- this.aliases = value
- .split(',')
- .map(alias => alias.trim())
- .filter(alias => alias.length > 0);
- }
-
- // Переключить видимость блока загрузки сертификатов
- toggleCertUpload() {
- const mode = $('certMode')?.value;
- const block = $('certUploadBlock');
-
- if (mode === 'upload') {
- show(block);
- } else {
- hide(block);
- }
- }
-
- // Обработать выбор файла сертификата
- handleCertFile(input, certType) {
- const file = input.files[0];
- const statusId = certType === 'certificate' ? 'certFileStatus' :
- certType === 'privatekey' ? 'keyFileStatus' : 'caFileStatus';
- const labelId = certType === 'certificate' ? 'certFileName' :
- certType === 'privatekey' ? 'keyFileName' : 'caFileName';
-
- const statusDiv = $(statusId);
- const labelSpan = $(labelId);
- const labelBtn = input.nextElementSibling; // label элемент
-
- if (!file) {
- this.certificates[certType] = null;
- statusDiv.innerHTML = '';
- if (labelSpan) labelSpan.textContent = 'Выберите файл...';
- if (labelBtn) labelBtn.classList.remove('file-uploaded');
- return;
- }
-
- // Проверяем размер файла (макс 1MB)
- if (file.size > 1024 * 1024) {
- statusDiv.innerHTML = ' Файл слишком большой (макс 1MB)';
- this.certificates[certType] = null;
- input.value = '';
- if (labelSpan) labelSpan.textContent = 'Выберите файл...';
- if (labelBtn) labelBtn.classList.remove('file-uploaded');
- return;
- }
-
- // Обновляем UI
- if (labelSpan) labelSpan.textContent = file.name;
- if (labelBtn) labelBtn.classList.add('file-uploaded');
-
- // Читаем файл
- const reader = new FileReader();
- reader.onload = (e) => {
- const content = e.target.result;
- // Сохраняем как base64
- this.certificates[certType] = btoa(content);
- statusDiv.innerHTML = ` Загружен успешно`;
- };
- reader.onerror = () => {
- statusDiv.innerHTML = ' Ошибка чтения файла';
- this.certificates[certType] = null;
- if (labelSpan) labelSpan.textContent = 'Выберите файл...';
- if (labelBtn) labelBtn.classList.remove('file-uploaded');
- };
- reader.readAsText(file);
- }
-
- // Валидация формы
- validateForm() {
- const name = $('newSiteName')?.value.trim();
- const host = $('newSiteHost')?.value.trim();
- const rootFile = $('newSiteRootFile')?.value;
- const certMode = $('certMode')?.value;
-
- if (!name) {
- notification.error('❌ Укажите название сайта');
- return false;
- }
-
- if (!host) {
- notification.error('❌ Укажите host (домен)');
- return false;
- }
-
- if (!rootFile) {
- notification.error('❌ Укажите root файл');
- return false;
- }
-
- // Проверка сертификатов если режим загрузки
- if (certMode === 'upload') {
- if (!this.certificates.certificate) {
- notification.error('❌ Загрузите файл certificate.crt');
- return false;
- }
- if (!this.certificates.privatekey) {
- notification.error('❌ Загрузите файл private.key');
- return false;
- }
- }
-
- return true;
- }
-
- // Создать сайт
- async createSite() {
- if (!this.validateForm()) {
- return;
- }
-
- if (!isWailsAvailable()) {
- notification.error('Wails API недоступен');
- return;
- }
-
- const createBtn = $('createSiteBtn');
- const originalText = createBtn.querySelector('span').textContent;
-
- try {
- createBtn.disabled = true;
- createBtn.querySelector('span').textContent = 'Создание...';
-
- // Парсим aliases из поля ввода
- this.parseAliases();
-
- // Определяем режим сертификата
- const certMode = $('certMode').value;
-
- // Собираем данные сайта
- const siteData = {
- name: $('newSiteName').value.trim(),
- host: $('newSiteHost').value.trim(),
- alias: this.aliases,
- status: $('newSiteStatus').value,
- root_file: $('newSiteRootFile').value,
- root_file_routing: $('newSiteRouting').checked,
- AutoCreateSSL: certMode === 'auto'
- };
-
- // Создаём сайт
- const siteJSON = JSON.stringify(siteData);
- const result = await api.createNewSite(siteJSON);
-
- if (result.startsWith('Error')) {
- notification.error(result, 3000);
- return;
- }
-
- notification.success('✅ Сайт успешно создан!', 1500);
-
- // Загружаем сертификаты если нужно
- if (certMode === 'upload') {
- createBtn.querySelector('span').textContent = 'Загрузка сертификатов...';
-
- // Загружаем certificate
- if (this.certificates.certificate) {
- await api.uploadCertificate(siteData.host, 'certificate', this.certificates.certificate);
- }
-
- // Загружаем private key
- if (this.certificates.privatekey) {
- await api.uploadCertificate(siteData.host, 'privatekey', this.certificates.privatekey);
- }
-
- // Загружаем ca bundle если есть
- if (this.certificates.cabundle) {
- await api.uploadCertificate(siteData.host, 'cabundle', this.certificates.cabundle);
- }
-
- notification.success('🔒 Сертификаты загружены!', 1500);
- }
-
- // Перезапускаем HTTP/HTTPS
- createBtn.querySelector('span').textContent = 'Перезапуск серверов...';
- await configAPI.stopHTTPService();
- await configAPI.stopHTTPSService();
- await new Promise(resolve => setTimeout(resolve, 500));
- await configAPI.startHTTPService();
- await configAPI.startHTTPSService();
-
- notification.success('🚀 Серверы перезапущены! Сайт готов к работе!', 2000);
-
- // Возвращаемся на главную
- setTimeout(() => {
- this.backToMain();
- // Перезагружаем список сайтов
- if (window.sitesManager) {
- window.sitesManager.load();
- }
- }, 1000);
-
- } catch (error) {
- notification.error('Ошибка: ' + error.message, 3000);
- } finally {
- createBtn.disabled = false;
- createBtn.querySelector('span').textContent = originalText;
- }
- }
-}
-
diff --git a/Backend/admin/frontend/assets/js/components/sites.js b/Backend/admin/frontend/assets/js/components/sites.js
deleted file mode 100644
index 091b535..0000000
--- a/Backend/admin/frontend/assets/js/components/sites.js
+++ /dev/null
@@ -1,222 +0,0 @@
-/* ============================================
- 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.certsCache = {};
- this.mockData = [
- {
- name: 'Home Voxsel',
- host: 'home.voxsel.ru',
- alias: ['home.voxsel.com'],
- status: 'active',
- root_file: 'index.html',
- root_file_routing: true,
- auto_create_ssl: false
- },
- {
- name: 'Finance',
- host: 'finance.voxsel.ru',
- alias: [],
- status: 'active',
- root_file: 'index.php',
- root_file_routing: false,
- auto_create_ssl: true
- },
- {
- name: 'Локальный сайт',
- host: '127.0.0.1',
- alias: ['localhost'],
- status: 'active',
- root_file: 'index.html',
- root_file_routing: true,
- auto_create_ssl: false
- }
- ];
- this.mockCerts = {
- 'voxsel.ru': { has_cert: true, is_expired: false, days_left: 79, dns_names: ['*.voxsel.com', '*.voxsel.ru', 'voxsel.com', 'voxsel.ru'] },
- 'finance.voxsel.ru': { has_cert: true, is_expired: false, days_left: 89, dns_names: ['finance.voxsel.ru'] }
- };
- }
-
- // Загрузить список сайтов
- async load() {
- if (isWailsAvailable()) {
- this.sitesData = await api.getSitesList();
- await this.loadCertsInfo();
- } else {
- this.sitesData = this.mockData;
- this.certsCache = this.mockCerts;
- }
- this.render();
- }
-
- // Загрузить информацию о сертификатах
- async loadCertsInfo() {
- const allCerts = await api.getAllCertsInfo();
- this.certsCache = {};
- for (const cert of allCerts) {
- this.certsCache[cert.domain] = cert;
- }
- }
-
- // Проверить соответствие домена wildcard паттерну
- matchesWildcard(domain, pattern) {
- if (pattern.startsWith('*.')) {
- const wildcardBase = pattern.slice(2);
- const domainParts = domain.split('.');
- if (domainParts.length >= 2) {
- const domainBase = domainParts.slice(1).join('.');
- return domainBase === wildcardBase;
- }
- }
- return domain === pattern;
- }
-
- // Найти сертификат для домена (включая wildcard)
- findCertForDomain(domain) {
- if (this.certsCache[domain]?.has_cert) {
- return this.certsCache[domain];
- }
-
- const domainParts = domain.split('.');
- if (domainParts.length >= 2) {
- const wildcardDomain = '*.' + domainParts.slice(1).join('.');
- if (this.certsCache[wildcardDomain]?.has_cert) {
- return this.certsCache[wildcardDomain];
- }
- }
-
- for (const [certDomain, cert] of Object.entries(this.certsCache)) {
- if (cert.has_cert && cert.dns_names) {
- for (const dnsName of cert.dns_names) {
- if (this.matchesWildcard(domain, dnsName)) {
- return cert;
- }
- }
- }
- }
-
- return null;
- }
-
- // Получить иконку сертификата для домена
- getCertIcon(host, aliases = []) {
- const allDomains = [host, ...aliases.filter(a => !a.includes('*'))];
-
- for (const domain of allDomains) {
- const cert = this.findCertForDomain(domain);
- if (cert) {
- if (cert.is_expired) {
- return ``;
- } else {
- return ``;
- }
- }
- }
- return '';
- }
-
- // Отрисовать список сайтов
- 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(', ');
- const certIcon = this.getCertIcon(site.host, site.alias);
-
- row.innerHTML = `
- ${certIcon}${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 'open-certs':
- if (window.openCertManager) {
- const aliasesStr = btn.getAttribute('data-aliases') || '';
- const aliases = aliasesStr ? aliasesStr.split(',').filter(a => a) : [];
- window.openCertManager(host, isProxy, aliases);
- }
- 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
deleted file mode 100644
index 7e8c2b6..0000000
--- a/Backend/admin/frontend/assets/js/components/vaccess.js
+++ /dev/null
@@ -1,366 +0,0 @@
-/* ============================================
- 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
deleted file mode 100644
index 4c36c09..0000000
--- a/Backend/admin/frontend/assets/js/main.js
+++ /dev/null
@@ -1,1048 +0,0 @@
-/* ============================================
- vServer Admin Panel - Main Entry Point
- Точка входа приложения
- ============================================ */
-
-import { 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 { SiteCreator } from './components/site-creator.js';
-import { ProxyCreator } from './components/proxy-creator.js';
-import { api } from './api/wails.js';
-import { configAPI } from './api/config.js';
-import { initCustomSelects } from './ui/custom-select.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.siteCreator = new SiteCreator();
- this.proxyCreator = new ProxyCreator();
-
- this.isWails = isWailsAvailable();
- }
-
- // Загрузить шаблоны из templates.html
- async loadTemplates() {
- try {
- const response = await fetch('templates.html');
- const html = await response.text();
- document.getElementById('templates-container').innerHTML = html;
- } catch (error) {
- // Игнорируем ошибку
- }
- }
-
- // Получить шаблон по ID
- getTemplate(templateId) {
- const template = document.getElementById(templateId);
- return template ? template.content.cloneNode(true) : null;
- }
-
- // Запустить приложение
- async start() {
- // Загружаем шаблоны
- await this.loadTemplates();
-
- // Скрываем loader если не в Wails
- if (!this.isWails) {
- notification.hideLoader();
- }
-
- // Ждём немного перед загрузкой данных
- await sleep(1000);
-
- // Загружаем начальные данные
- await this.loadInitialData();
-
- // Запускаем автообновление
- this.startAutoRefresh();
-
- // Скрываем loader после загрузки
- if (this.isWails) {
- notification.hideLoader();
- }
-
- // Настраиваем глобальные функции для совместимости
- this.setupGlobalHandlers();
-
- // Привязываем кнопки
- this.setupButtons();
-
- // Инициализируем кастомные select'ы
- initCustomSelects();
- }
-
- // Загрузить начальные данные
- async loadInitialData() {
- await Promise.all([
- this.servicesManager.loadStatus(),
- this.sitesManager.load(),
- this.proxyManager.load()
- ]);
- }
-
- // Запустить автообновление
- startAutoRefresh() {
- setInterval(async () => {
- await this.loadInitialData();
- }, 5000);
- }
-
- // Привязать кнопки
- setupButtons() {
- // Кнопка добавления сайта
- const addSiteBtn = $('addSiteBtn');
- if (addSiteBtn) {
- addSiteBtn.addEventListener('click', () => {
- this.siteCreator.open();
- });
- }
-
- // Кнопка добавления прокси
- const addProxyBtn = $('addProxyBtn');
- if (addProxyBtn) {
- addProxyBtn.addEventListener('click', () => {
- this.proxyCreator.open();
- });
- }
-
- // Кнопка сохранения настроек
- 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);
- }
- });
- }
-
- // Моментальное переключение ACME без перезапуска
- const acmeCheckbox = $('acmeEnabled');
- if (acmeCheckbox) {
- acmeCheckbox.addEventListener('change', async (e) => {
- const isEnabled = e.target.checked;
-
- if (isEnabled) {
- await configAPI.enableACMEService();
- notification.success('Cert Manager включен', 1000);
- } else {
- await configAPI.disableACMEService();
- notification.success('Cert Manager отключен', 1000);
- }
- });
- }
- }
-
- // Настроить глобальные обработчики
- setupGlobalHandlers() {
- Object.assign(window, {
- // Ссылки на менеджеры
- sitesManager: this.sitesManager,
- siteCreator: this.siteCreator,
- proxyCreator: this.proxyCreator,
- proxyManager: this.proxyManager,
-
- // SiteCreator
- backToMainFromAddSite: () => this.siteCreator.backToMain(),
- toggleCertUpload: () => this.siteCreator.toggleCertUpload(),
- handleCertFileSelect: (input, certType) => this.siteCreator.handleCertFile(input, certType),
-
- // ProxyCreator
- backToMainFromAddProxy: () => this.proxyCreator.backToMain(),
- toggleProxyCertUpload: () => this.proxyCreator.toggleCertUpload(),
- handleProxyCertFileSelect: (input, certType) => this.proxyCreator.handleCertFile(input, certType),
-
- // vAccess
- editVAccess: (host, isProxy) => this.vAccessManager.open(host, isProxy),
- backToMain: () => this.vAccessManager.backToMain(),
-
- // CertManager
- openCertManager: (host, isProxy, aliases) => this.openCertManager(host, isProxy, aliases),
- backFromCertManager: () => this.backFromCertManager(),
- deleteCertificate: async (domain) => await this.deleteCertificate(domain),
- renewCertificate: async (domain) => await this.renewCertificate(domain),
- issueCertificate: async (domain) => await this.issueCertificate(domain),
- switchVAccessTab: (tab) => this.vAccessManager.switchTab(tab),
- saveVAccessChanges: async () => await this.vAccessManager.save(),
- addVAccessRule: () => this.vAccessManager.addRule(),
- dragStart: (e) => this.vAccessManager.onDragStart(e),
- dragOver: (e) => this.vAccessManager.onDragOver(e),
- drop: (e) => this.vAccessManager.onDrop(e),
- editRuleField: (i, f) => this.vAccessManager.editRuleField(i, f),
- removeVAccessRule: (i) => this.vAccessManager.removeRule(i),
- closeFieldEditor: () => this.vAccessManager.closeFieldEditor(),
- addFieldValue: () => this.vAccessManager.addFieldValue(),
- removeFieldValue: (v) => this.vAccessManager.removeFieldValue(v),
-
- // Settings
- loadConfig: async () => await this.loadConfigSettings(),
- saveSettings: async () => await this.saveConfigSettings(),
-
- // Модальные окна
- editSite: (i) => this.editSite(i),
- editProxy: (i) => this.editProxy(i),
- setStatus: (s) => this.setModalStatus(s),
- setProxyStatus: (s) => this.setModalStatus(s),
- addAliasTag: () => this.addAliasTag(),
- removeAliasTag: (btn) => btn.parentElement.remove(),
- saveModalData: async () => await this.saveModalData(),
- deleteSiteConfirm: async () => await this.deleteSiteConfirm(),
- deleteProxyConfirm: async () => await this.deleteProxyConfirm(),
-
- // Тестовые функции
- editTestSite: (i) => {
- this.sitesManager.sitesData = [
- {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.editSite(i);
- },
- editTestProxy: (i) => {
- this.proxyManager.proxiesData = [
- {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.editProxy(i);
- },
- openTestLink: (url) => this.sitesManager.openLink(url),
- 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;
- $('acmeEnabled').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;
- $('acmeEnabled').checked = config.Soft_Settings?.ACME_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 template = this.getTemplate('edit-site-template');
- if (!template) return;
-
- const container = document.createElement('div');
- container.appendChild(template);
-
- // Открываем модальное окно с шаблоном
- modal.open('Редактировать сайт', container.innerHTML);
- window.currentEditType = 'site';
- window.currentEditIndex = index;
-
- // Заполняем данные ПОСЛЕ открытия модального окна
- setTimeout(() => {
- const statusBtn = document.querySelector(`[data-status="${site.status}"]`);
- if (statusBtn) statusBtn.classList.add('active');
-
- const editName = $('editName');
- const editHost = $('editHost');
- const editRootFile = $('editRootFile');
- const editRouting = $('editRouting');
-
- if (editName) editName.value = site.name;
- if (editHost) editHost.value = site.host;
- if (editRootFile) editRootFile.value = site.root_file;
- if (editRouting) editRouting.checked = site.root_file_routing;
-
- const editAutoCreateSSL = $('editAutoCreateSSL');
- if (editAutoCreateSSL) editAutoCreateSSL.checked = site.auto_create_ssl || false;
-
- // Добавляем alias теги
- const aliasContainer = $('aliasTagsContainer');
- if (aliasContainer) {
- site.alias.forEach(alias => {
- const tag = document.createElement('span');
- tag.className = 'tag';
- tag.innerHTML = `${alias}`;
- aliasContainer.appendChild(tag);
- });
- }
-
- // Привязываем обработчик кнопок статуса
- document.querySelectorAll('.status-btn').forEach(btn => {
- btn.onclick = () => this.setModalStatus(btn.dataset.value);
- });
- }, 50);
-
- this.addDeleteButtonToModal();
- }
-
- // Редактировать прокси
- editProxy(index) {
- const proxy = this.proxyManager.proxiesData[index];
- if (!proxy) return;
-
- const template = this.getTemplate('edit-proxy-template');
- if (!template) return;
-
- const container = document.createElement('div');
- container.appendChild(template);
-
- // Открываем модальное окно с шаблоном
- modal.open('Редактировать прокси', container.innerHTML);
- window.currentEditType = 'proxy';
- window.currentEditIndex = index;
-
- // Заполняем данные ПОСЛЕ открытия модального окна
- setTimeout(() => {
- const status = proxy.enable ? 'enable' : 'disable';
- const statusBtn = document.querySelector(`[data-status="${status}"]`);
- if (statusBtn) statusBtn.classList.add('active');
-
- const editDomain = $('editDomain');
- const editLocalAddr = $('editLocalAddr');
- const editLocalPort = $('editLocalPort');
- const editServiceHTTPS = $('editServiceHTTPS');
- const editAutoHTTPS = $('editAutoHTTPS');
-
- if (editDomain) editDomain.value = proxy.external_domain;
- if (editLocalAddr) editLocalAddr.value = proxy.local_address;
- if (editLocalPort) editLocalPort.value = proxy.local_port;
- if (editServiceHTTPS) editServiceHTTPS.checked = proxy.service_https_use;
- if (editAutoHTTPS) editAutoHTTPS.checked = proxy.auto_https;
-
- const editProxyAutoCreateSSL = $('editProxyAutoCreateSSL');
- if (editProxyAutoCreateSSL) editProxyAutoCreateSSL.checked = proxy.auto_create_ssl || false;
-
- // Привязываем обработчик кнопок статуса
- document.querySelectorAll('.status-btn').forEach(btn => {
- btn.onclick = () => this.setModalStatus(btn.dataset.value);
- });
- }, 50);
-
- this.addDeleteButtonToModal();
- }
-
- // Установить статус в модальном окне
- 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();
- }
- }
-
- // Перезапустить HTTP/HTTPS сервисы
- async restartHttpServices() {
- notification.show('Перезапуск HTTP/HTTPS...', 'success', 800);
- await configAPI.stopHTTPService();
- await configAPI.stopHTTPSService();
- await sleep(500);
- await configAPI.startHTTPService();
- await configAPI.startHTTPSService();
- }
-
- // Сохранить данные сайта
- 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,
- AutoCreateSSL: $('editAutoCreateSSL')?.checked || false
- };
-
- const result = await configAPI.saveConfig(JSON.stringify(config, null, 4));
- if (result.startsWith('Error')) {
- notification.error(result);
- return;
- }
-
- await this.restartHttpServices();
- 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,
- AutoCreateSSL: $('editProxyAutoCreateSSL')?.checked || false
- };
-
- const result = await configAPI.saveConfig(JSON.stringify(config, null, 4));
- if (result.startsWith('Error')) {
- notification.error(result);
- return;
- }
-
- await this.restartHttpServices();
- notification.success('Изменения сохранены и применены!', 1000);
- await this.proxyManager.load();
- modal.close();
- }
-
- // Добавить кнопку удаления в модальное окно
- addDeleteButtonToModal() {
- const footer = document.querySelector('.modal-footer');
- if (!footer) return;
-
- // Удаляем старую кнопку удаления если есть
- const oldDeleteBtn = footer.querySelector('#modalDeleteBtn');
- if (oldDeleteBtn) oldDeleteBtn.remove();
-
- // Определяем текст и обработчик в зависимости от типа редактирования
- const isProxy = window.currentEditType === 'proxy';
- const buttonText = isProxy ? 'Удалить прокси' : 'Удалить сайт';
- const onClickHandler = isProxy ? () => this.deleteProxyConfirm() : () => this.deleteSiteConfirm();
-
- // Создаём кнопку удаления
- const deleteBtn = document.createElement('button');
- deleteBtn.className = 'action-btn delete-btn';
- deleteBtn.id = 'modalDeleteBtn';
- deleteBtn.innerHTML = `
-
- ${buttonText}
- `;
- deleteBtn.onclick = onClickHandler;
-
- // Вставляем перед кнопкой "Отмена"
- const cancelBtn = footer.querySelector('#modalCancelBtn');
- if (cancelBtn) {
- footer.insertBefore(deleteBtn, cancelBtn);
- }
- }
-
- // Удалить кнопку удаления из модального окна
- removeDeleteButtonFromModal() {
- const deleteBtn = document.querySelector('#modalDeleteBtn');
- if (deleteBtn) deleteBtn.remove();
- }
-
- // Подтверждение удаления сайта
- async deleteSiteConfirm() {
- const index = window.currentEditIndex;
- const site = this.sitesManager.sitesData[index];
- if (!site) return;
-
- // Подтверждение
- const confirmed = confirm(
- `⚠️ ВНИМАНИЕ!\n\n` +
- `Вы действительно хотите удалить сайт "${site.name}" (${site.host})?\n\n` +
- `Будут удалены:\n` +
- `• Папка сайта: WebServer/www/${site.host}/\n` +
- `• SSL сертификаты (если есть)\n` +
- `• Запись в конфигурации\n\n` +
- `Это действие НЕОБРАТИМО!`
- );
-
- if (!confirmed) return;
-
- try {
- notification.show('Удаление сайта...', 'info', 1000);
-
- const result = await api.deleteSite(site.host);
-
- if (result.startsWith('Error')) {
- notification.error(result, 3000);
- return;
- }
-
- notification.success('✅ Сайт успешно удалён!', 1500);
- await this.restartHttpServices();
- notification.success('🚀 Серверы перезапущены!', 1000);
-
- // Закрываем модальное окно и обновляем список
- modal.close();
- await this.sitesManager.load();
-
- } catch (error) {
- notification.error('Ошибка: ' + error.message, 3000);
- }
- }
-
- // Подтверждение удаления прокси
- async deleteProxyConfirm() {
- const index = window.currentEditIndex;
- const proxy = this.proxyManager.proxiesData[index];
- if (!proxy) return;
-
- const confirmed = confirm(
- `⚠️ ВНИМАНИЕ!\n\n` +
- `Вы действительно хотите удалить прокси "${proxy.external_domain}"?\n\n` +
- `Будет удалена запись в конфигурации.\n\n` +
- `Это действие НЕОБРАТИМО!`
- );
-
- if (!confirmed) return;
-
- try {
- notification.show('Удаление прокси...', 'info', 1000);
-
- const config = await configAPI.getConfig();
-
- if (config.Proxy_Service && config.Proxy_Service.length > index) {
- config.Proxy_Service.splice(index, 1);
- }
-
- const result = await configAPI.saveConfig(JSON.stringify(config, null, 4));
-
- if (result.startsWith('Error')) {
- notification.error(result, 3000);
- return;
- }
-
- notification.success('✅ Прокси успешно удалён!', 1500);
- await this.restartHttpServices();
- notification.success('🚀 Серверы перезапущены!', 1000);
-
- modal.close();
- await this.proxyManager.load();
-
- } catch (error) {
- notification.error('Ошибка: ' + error.message, 3000);
- }
- }
-
- // ====== Cert Manager ======
-
- certManagerHost = null;
- certManagerIsProxy = false;
- certManagerAliases = [];
-
- async openCertManager(host, isProxy = false, aliases = []) {
- this.certManagerHost = host;
- this.certManagerIsProxy = isProxy;
- this.certManagerAliases = aliases.filter(a => !a.includes('*'));
-
- // Обновляем заголовки
- $('certManagerBreadcrumb').textContent = `Сертификаты: ${host}`;
- const titleSpan = $('certManagerTitle').querySelector('span');
- if (titleSpan) titleSpan.textContent = host;
- $('certManagerSubtitle').textContent = isProxy ? 'Прокси сервис' : 'Веб-сайт';
-
- // Скрываем все секции, показываем CertManager
- this.hideAllSectionsForCertManager();
- $('sectionCertManager').style.display = 'block';
-
- // Загружаем сертификаты
- await this.loadCertManagerContent(host, this.certManagerAliases);
- }
-
- hideAllSectionsForCertManager() {
- const sections = ['sectionServices', 'sectionSites', 'sectionProxy', 'sectionSettings', 'sectionVAccessEditor', 'sectionAddSite', 'sectionCertManager'];
- sections.forEach(id => {
- const el = $(id);
- if (el) el.style.display = 'none';
- });
- }
-
- // Тестовые данные для сертификатов (браузерный режим)
- mockCertsData = [
- {
- domain: 'voxsel.ru',
- issuer: 'R13',
- not_before: '2026-01-07',
- not_after: '2026-04-07',
- days_left: 79,
- is_expired: false,
- has_cert: true,
- dns_names: ['*.voxsel.com', '*.voxsel.ru', 'voxsel.com', 'voxsel.ru']
- },
- {
- domain: 'finance.voxsel.ru',
- issuer: 'E8',
- not_before: '2026-01-17',
- not_after: '2026-04-17',
- days_left: 89,
- is_expired: false,
- has_cert: true,
- dns_names: ['finance.voxsel.ru']
- },
- {
- domain: 'test.local',
- issuer: "Let's Encrypt",
- not_before: '2025-01-01',
- not_after: '2025-03-31',
- days_left: 73,
- is_expired: false,
- has_cert: true,
- dns_names: ['test.local', '*.test.local', 'test.com']
- },
- {
- domain: 'api.example.com',
- issuer: "Let's Encrypt",
- not_before: '2024-10-01',
- not_after: '2024-12-30',
- days_left: -18,
- is_expired: true,
- has_cert: true,
- dns_names: ['api.example.com', '*.api.example.com']
- }
- ];
-
- async loadCertManagerContent(host, aliases = []) {
- const container = $('certManagerContent');
- container.innerHTML = ' Загрузка...
';
-
- try {
- // Получаем сертификаты (реальные или mock)
- let allCerts;
- if (this.isWails) {
- allCerts = await api.getAllCertsInfo();
- } else {
- allCerts = this.mockCertsData;
- }
-
- // Все домены для отображения (host + алиасы без wildcard)
- const allDomains = [host, ...aliases.filter(a => !a.includes('*'))];
-
- // Функция проверки wildcard покрытия
- const isWildcardCovering = (domain, cert) => {
- const parts = domain.split('.');
- if (parts.length < 2) return false;
- const wildcardPattern = '*.' + parts.slice(1).join('.');
- return cert.domain === wildcardPattern ||
- cert.domain.startsWith('*.') && domain.endsWith(cert.domain.slice(1)) ||
- cert.dns_names?.some(dns => dns === wildcardPattern || (dns.startsWith('*.') && domain.endsWith(dns.slice(1))));
- };
-
- // Функция проверки прямого сертификата
- const hasDirectCert = (domain, cert) => {
- return cert.domain === domain || cert.dns_names?.includes(domain);
- };
-
- // Собираем информацию по каждому домену
- const domainInfos = allDomains.map(domain => {
- const directCert = allCerts.find(cert => hasDirectCert(domain, cert));
- const wildcardCert = allCerts.find(cert => isWildcardCovering(domain, cert));
- return { domain, directCert, wildcardCert, isLocal: this.isLocalDomain(domain) };
- });
-
- let html = '';
-
- // Карточки для каждого домена
- domainInfos.forEach(info => {
- if (info.isLocal) {
- // Локальный домен - только информация
- html += `
-
-
-
-
-
Статус
-
Локальный домен
-
-
-
- `;
- } else if (info.directCert) {
- // Есть прямой сертификат
- html += this.renderCertCard(info.directCert, info.domain);
- } else if (info.wildcardCert) {
- // Покрыт wildcard - показываем с возможностью выпустить прямой
- html += this.renderDomainWithWildcard(info.domain, info.wildcardCert);
- } else {
- // Нет сертификата - предлагаем выпустить
- html += this.renderNoCertCard(info.domain);
- }
- });
-
- if (!html) {
- html = `
-
-
-
Нет доменов для отображения
-
- `;
- }
-
- container.innerHTML = html;
-
- } catch (error) {
- container.innerHTML = `Ошибка загрузки: ${error.message}
`;
- }
- }
-
- renderCertCard(cert, displayDomain = null) {
- const isExpired = cert.is_expired;
- const statusClass = isExpired ? 'expired' : 'valid';
- const statusText = isExpired ? 'Истёк' : `Активен (${cert.days_left} дн.)`;
- const iconClass = isExpired ? 'expired' : '';
- const title = displayDomain || cert.domain;
-
- const dnsNames = cert.dns_names || [cert.domain];
- const domainTags = dnsNames.map(d => `${d}`).join('');
-
- return `
-
-
-
-
-
-
Статус
-
${statusText}
-
-
-
Издатель
-
${cert.issuer || 'Неизвестно'}
-
-
-
Выдан
-
${cert.not_before || '-'}
-
-
-
Истекает
-
${cert.not_after || '-'}
-
-
-
-
- ${domainTags}
-
-
- `;
- }
-
- isLocalDomain(host) {
- const localPatterns = [
- 'localhost',
- '127.0.0.1',
- '0.0.0.0',
- '::1',
- '.local',
- '.localhost',
- '.test',
- '.example',
- '.invalid'
- ];
-
- const hostLower = host.toLowerCase();
- return localPatterns.some(pattern => {
- if (pattern.startsWith('.')) {
- return hostLower.endsWith(pattern) || hostLower === pattern.slice(1);
- }
- return hostLower === pattern;
- });
- }
-
- renderNoCertCard(host) {
- return `
-
-
-
-
-
-
Статус
-
Нет сертификата
-
-
-
-
- ${host}
-
-
- `;
- }
-
- renderDomainWithWildcard(domain, wildcardCert) {
- const isExpired = wildcardCert.is_expired;
- const statusClass = isExpired ? 'expired' : 'valid';
- const statusText = isExpired ? `Покрыт wildcard (истёк)` : `Покрыт wildcard (${wildcardCert.days_left} дн.)`;
-
- return `
-
-
-
-
-
-
Статус
-
${statusText}
-
-
-
Wildcard
-
${wildcardCert.domain}
-
-
-
-
- ${domain}
-
-
- `;
- }
-
- async issueCertificate(domain) {
- const confirmed = confirm(`Выпустить сертификат для "${domain}"?\n\nБудет запрошен сертификат Let's Encrypt.`);
- if (!confirmed) return;
-
- try {
- notification.show('Запрос сертификата...', 'info', 2000);
-
- if (this.isWails) {
- await api.obtainSSLCertificate(domain);
- } else {
- // Mock для браузерного режима
- await new Promise(r => setTimeout(r, 1500));
- }
-
- notification.success('Сертификат успешно выпущен!', 2000);
-
- // Перезагружаем контент
- await this.loadCertManagerContent(this.certManagerHost);
-
- // Обновляем списки сайтов и прокси
- await this.sitesManager.load();
- await this.proxyManager.load();
-
- } catch (error) {
- notification.error('Ошибка: ' + error.message, 3000);
- }
- }
-
- backFromCertManager() {
- this.hideAllSectionsForCertManager();
-
- // Показываем секции Dashboard
- const dashboard = ['sectionServices', 'sectionSites', 'sectionProxy'];
- dashboard.forEach(id => {
- const el = $(id);
- if (el) el.style.display = 'block';
- });
-
- // Убираем active у всех nav-item и ставим на dashboard
- document.querySelectorAll('.nav-item').forEach(item => item.classList.remove('active'));
- const dashboardBtn = document.querySelector('.nav-item[data-page="dashboard"]');
- if (dashboardBtn) dashboardBtn.classList.add('active');
- }
-
- async deleteCertificate(domain) {
- const confirmed = confirm(`Удалить сертификат для "${domain}"?\n\nЭто действие необратимо.`);
- if (!confirmed) return;
-
- try {
- await api.deleteCertificate(domain);
- notification.success('Сертификат удалён', 1500);
-
- // Перезагружаем контент
- await this.loadCertManagerContent(this.certManagerHost);
-
- // Обновляем списки сайтов и прокси
- await this.sitesManager.load();
- await this.proxyManager.load();
-
- } catch (error) {
- notification.error('Ошибка: ' + error.message, 3000);
- }
- }
-
- async renewCertificate(domain) {
- const confirmed = confirm(`Перевыпустить сертификат для "${domain}"?\n\nТекущий сертификат будет заменён новым.`);
- if (!confirmed) return;
-
- try {
- notification.show('Запрос сертификата...', 'info', 2000);
-
- if (this.isWails) {
- await api.obtainSSLCertificate(domain);
- } else {
- // Mock для браузерного режима
- await new Promise(r => setTimeout(r, 1500));
- }
-
- notification.success('Сертификат успешно перевыпущен!', 2000);
-
- // Перезагружаем контент
- await this.loadCertManagerContent(this.certManagerHost);
-
- // Обновляем списки сайтов и прокси
- await this.sitesManager.load();
- await this.proxyManager.load();
-
- } catch (error) {
- notification.error('Ошибка: ' + error.message, 3000);
- }
- }
-}
-
-// Инициализация приложения при загрузке DOM
-document.addEventListener('DOMContentLoaded', () => {
- const app = new App();
- app.start();
-});
-
diff --git a/Backend/admin/frontend/assets/js/ui/custom-select.js b/Backend/admin/frontend/assets/js/ui/custom-select.js
deleted file mode 100644
index 3698306..0000000
--- a/Backend/admin/frontend/assets/js/ui/custom-select.js
+++ /dev/null
@@ -1,130 +0,0 @@
-/* ============================================
- Custom Select Component
- Кастомные выпадающие списки
- ============================================ */
-
-import { $ } from '../utils/dom.js';
-
-// Инициализация всех кастомных select'ов на странице
-export function initCustomSelects() {
- const selects = document.querySelectorAll('select.form-input');
- selects.forEach(select => {
- if (!select.dataset.customized) {
- createCustomSelect(select);
- }
- });
-}
-
-// Создать кастомный select из нативного
-function createCustomSelect(selectElement) {
- // Помечаем как обработанный
- selectElement.dataset.customized = 'true';
-
- // Создаём контейнер
- const wrapper = document.createElement('div');
- wrapper.className = 'custom-select';
-
- // Получаем выбранное значение
- const selectedOption = selectElement.options[selectElement.selectedIndex];
- const selectedText = selectedOption ? selectedOption.text : '';
-
- // Создаём кнопку (видимая часть)
- const button = document.createElement('div');
- button.className = 'custom-select-trigger';
- button.innerHTML = `
- ${selectedText}
-
- `;
-
- // Создаём выпадающий список
- const dropdown = document.createElement('div');
- dropdown.className = 'custom-select-dropdown';
-
- // Заполняем опции
- Array.from(selectElement.options).forEach((option, index) => {
- const item = document.createElement('div');
- item.className = 'custom-select-option';
- item.textContent = option.text;
- item.dataset.value = option.value;
- item.dataset.index = index;
-
- if (option.selected) {
- item.classList.add('selected');
- }
-
- // Клик по опции
- item.addEventListener('click', () => {
- selectOption(selectElement, wrapper, item, index);
- });
-
- dropdown.appendChild(item);
- });
-
- // Клик по кнопке - открыть/закрыть
- button.addEventListener('click', (e) => {
- e.stopPropagation();
- toggleDropdown(wrapper);
- });
-
- // Собираем вместе
- wrapper.appendChild(button);
- wrapper.appendChild(dropdown);
-
- // Скрываем оригинальный select
- selectElement.style.display = 'none';
-
- // Вставляем кастомный select после оригинального
- selectElement.parentNode.insertBefore(wrapper, selectElement.nextSibling);
-
- // Закрываем при клике вне
- document.addEventListener('click', (e) => {
- if (!wrapper.contains(e.target)) {
- closeDropdown(wrapper);
- }
- });
-}
-
-// Открыть/закрыть dropdown
-function toggleDropdown(wrapper) {
- const isOpen = wrapper.classList.contains('open');
-
- // Закрываем все открытые
- document.querySelectorAll('.custom-select.open').forEach(el => {
- el.classList.remove('open');
- });
-
- if (!isOpen) {
- wrapper.classList.add('open');
- }
-}
-
-// Закрыть dropdown
-function closeDropdown(wrapper) {
- wrapper.classList.remove('open');
-}
-
-// Выбрать опцию
-function selectOption(selectElement, wrapper, optionElement, index) {
- // Обновляем оригинальный select
- selectElement.selectedIndex = index;
-
- // Триггерим событие change
- const event = new Event('change', { bubbles: true });
- selectElement.dispatchEvent(event);
-
- // Обновляем UI
- const valueSpan = wrapper.querySelector('.custom-select-value');
- valueSpan.textContent = optionElement.textContent;
-
- // Убираем selected у всех опций
- wrapper.querySelectorAll('.custom-select-option').forEach(opt => {
- opt.classList.remove('selected');
- });
-
- // Добавляем selected к выбранной
- optionElement.classList.add('selected');
-
- // Закрываем dropdown
- closeDropdown(wrapper);
-}
-
diff --git a/Backend/admin/frontend/assets/js/ui/modal.js b/Backend/admin/frontend/assets/js/ui/modal.js
deleted file mode 100644
index 617b5ec..0000000
--- a/Backend/admin/frontend/assets/js/ui/modal.js
+++ /dev/null
@@ -1,84 +0,0 @@
-/* ============================================
- 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();
- }
- });
- }
- }
-
- // Открыть модальное окно
- 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');
- }
-
- // Установить обработчик сохранения
- onSave(callback) {
- if (this.saveBtn) {
- this.saveBtn.onclick = callback;
- }
- }
-
- // Открыть редактор поля
- 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
deleted file mode 100644
index 99de41a..0000000
--- a/Backend/admin/frontend/assets/js/ui/navigation.js
+++ /dev/null
@@ -1,69 +0,0 @@
-/* ============================================
- Navigation
- Управление навигацией
- ============================================ */
-
-import { $, $$, hide, show, removeClass, addClass } from '../utils/dom.js';
-
-// Класс для управления навигацией
-export class Navigation {
- constructor() {
- this.navItems = $$('.nav-item[data-page]');
- this.sections = {
- services: $('sectionServices'),
- sites: $('sectionSites'),
- proxy: $('sectionProxy'),
- settings: $('sectionSettings'),
- vaccess: $('sectionVAccessEditor'),
- addSite: $('sectionAddSite')
- };
- this.init();
- }
-
- init() {
- this.navItems.forEach(item => {
- item.addEventListener('click', () => {
- const page = item.dataset.page;
- this.navigate(page, item);
- });
- });
- }
-
- navigate(page, clickedItem) {
- // Убираем active со всех навигационных элементов
- this.navItems.forEach(nav => removeClass(nav, 'active'));
- addClass(clickedItem, 'active');
-
- // Скрываем все секции
- this.hideAllSections();
-
- // Показываем нужные секции по имени страницы
- switch (page) {
- case 'dashboard':
- show(this.sections.services);
- show(this.sections.sites);
- show(this.sections.proxy);
- break;
- case 'settings':
- show(this.sections.settings);
- if (window.loadConfig) {
- window.loadConfig();
- }
- break;
- }
- }
-
- 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
deleted file mode 100644
index 88383fe..0000000
--- a/Backend/admin/frontend/assets/js/ui/notification.js
+++ /dev/null
@@ -1,64 +0,0 @@
-/* ============================================
- Notification System
- Система уведомлений
- ============================================ */
-
-import { $, addClass, removeClass } from '../utils/dom.js';
-
-// Класс для управления уведомлениями
-export class NotificationManager {
- constructor() {
- this.container = $('notification');
- this.loader = $('appLoader');
- }
-
- // Показать уведомление
- 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);
- }
-
- // Показать успешное уведомление
- success(message, duration = 1000) {
- this.show(message, 'success', 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
deleted file mode 100644
index b455e62..0000000
--- a/Backend/admin/frontend/assets/js/ui/window.js
+++ /dev/null
@@ -1,49 +0,0 @@
-/* ============================================
- 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
deleted file mode 100644
index 9e4abee..0000000
--- a/Backend/admin/frontend/assets/js/utils/dom.js
+++ /dev/null
@@ -1,53 +0,0 @@
-/* ============================================
- DOM Utilities
- Утилиты для работы с DOM
- ============================================ */
-
-// Получить элемент по ID
-export function $(id) {
- return document.getElementById(id);
-}
-
-// Получить все элементы по селектору
-export function $$(selector, parent = document) {
- return parent.querySelectorAll(selector);
-}
-
-// Показать элемент
-export function show(element) {
- const el = typeof element === 'string' ? $(element) : element;
- if (el) el.style.display = 'block';
-}
-
-// Скрыть элемент
-export function hide(element) {
- const el = typeof element === 'string' ? $(element) : element;
- if (el) el.style.display = 'none';
-}
-
-// Переключить видимость элемента
-export function toggle(element) {
- const el = typeof element === 'string' ? $(element) : element;
- if (el) {
- el.style.display = el.style.display === 'none' ? 'block' : 'none';
- }
-}
-
-// Добавить класс
-export function addClass(element, className) {
- const el = typeof element === 'string' ? $(element) : element;
- if (el) el.classList.add(className);
-}
-
-// Удалить класс
-export function removeClass(element, className) {
- const el = typeof element === 'string' ? $(element) : element;
- if (el) el.classList.remove(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
deleted file mode 100644
index 4affc22..0000000
--- a/Backend/admin/frontend/assets/js/utils/helpers.js
+++ /dev/null
@@ -1,29 +0,0 @@
-/* ============================================
- Helper Utilities
- Вспомогательные функции
- ============================================ */
-
-// Ждёт указанное время
-export function sleep(ms) {
- return new Promise(resolve => setTimeout(resolve, ms));
-}
-
-// Debounce функция
-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
-export function isWailsAvailable() {
- return typeof window.go !== 'undefined' &&
- window.go?.admin?.App !== undefined;
-}
-
diff --git a/Backend/admin/frontend/index.html b/Backend/admin/frontend/index.html
deleted file mode 100644
index b3de709..0000000
--- a/Backend/admin/frontend/index.html
+++ /dev/null
@@ -1,915 +0,0 @@
-
-
-
-
-
- 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/templates.html b/Backend/admin/frontend/templates.html
deleted file mode 100644
index 82228c7..0000000
--- a/Backend/admin/frontend/templates.html
+++ /dev/null
@@ -1,122 +0,0 @@
-
-
-
-
-
-
-
-
-
-
diff --git a/Backend/admin/go/admin.go b/Backend/admin/go/admin.go
index 61d9ba5..679514d 100644
--- a/Backend/admin/go/admin.go
+++ b/Backend/admin/go/admin.go
@@ -2,10 +2,18 @@ package admin
import (
"context"
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "crypto/rand"
+ "crypto/x509"
+ "crypto/x509/pkix"
"encoding/json"
+ "encoding/pem"
"fmt"
+ "math/big"
"os"
"os/exec"
+ "path/filepath"
"time"
"github.com/wailsapp/wails/v2/pkg/runtime"
@@ -32,10 +40,103 @@ func NewApp() *App {
var isSingleInstance bool = false
+func initDirectories() {
+ dirs := []string{
+ "WebServer",
+ "WebServer/www",
+ "WebServer/cert",
+ "WebServer/cert/no_cert",
+ "WebServer/tools",
+ "WebServer/tools/logs",
+ "WebServer/tools/error_page",
+ "WebServer/tools/Proxy_vAccess",
+ }
+
+ for _, dir := range dirs {
+ os.MkdirAll(dir, 0755)
+ }
+
+ // Дефолтный config.json
+ if _, err := os.Stat(config.ConfigPath); os.IsNotExist(err) {
+ defaultConfig := map[string]interface{}{
+ "Site_www": []interface{}{},
+ "Proxy_Service": []interface{}{},
+ "Soft_Settings": map[string]interface{}{
+ "php_host": "localhost",
+ "php_port": 8000,
+ "mysql_host": "127.0.0.1",
+ "mysql_port": 3306,
+ "proxy_enabled": false,
+ "ACME_enabled": false,
+ },
+ }
+ data, _ := json.MarshalIndent(defaultConfig, "", " ")
+ os.WriteFile(config.ConfigPath, data, 0644)
+ }
+
+ // Страница ошибки
+ errorPage := "WebServer/tools/error_page/index.html"
+ if _, err := os.Stat(errorPage); os.IsNotExist(err) {
+ os.WriteFile(errorPage, []byte("ErrorError
"), 0644)
+ }
+
+ // Fallback SSL-сертификат (самоподписанный)
+ certFile := filepath.Join("WebServer", "cert", "no_cert", "certificate.crt")
+ keyFile := filepath.Join("WebServer", "cert", "no_cert", "private.key")
+ if _, err := os.Stat(certFile); os.IsNotExist(err) {
+ generateSelfSignedCert(certFile, keyFile)
+ }
+}
+
+func generateSelfSignedCert(certPath, keyPath string) {
+ key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+ if err != nil {
+ return
+ }
+
+ template := x509.Certificate{
+ SerialNumber: big.NewInt(1),
+ Subject: pkix.Name{CommonName: "localhost"},
+ NotBefore: time.Now(),
+ NotAfter: time.Now().Add(10 * 365 * 24 * time.Hour),
+ KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
+ ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
+ IsCA: true,
+ BasicConstraintsValid: true,
+ }
+
+ certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key)
+ if err != nil {
+ return
+ }
+
+ certOut, err := os.Create(certPath)
+ if err != nil {
+ return
+ }
+ pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: certDER})
+ certOut.Close()
+
+ keyDER, err := x509.MarshalECPrivateKey(key)
+ if err != nil {
+ return
+ }
+
+ keyOut, err := os.Create(keyPath)
+ if err != nil {
+ return
+ }
+ pem.Encode(keyOut, &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER})
+ keyOut.Close()
+}
+
func (a *App) Startup(ctx context.Context) {
a.ctx = ctx
appContext = ctx
+ // Создаём структуру папок при первом запуске
+ initDirectories()
+
// Проверяем, не запущен ли уже vServer
if !tools.CheckSingleInstance() {
runtime.EventsEmit(ctx, "server:already_running", true)
@@ -125,7 +226,11 @@ func (a *App) Shutdown(ctx context.Context) {
}
func (a *App) monitorServices() {
- time.Sleep(1 * time.Second) // Ждём секунду перед первой проверкой
+ time.Sleep(300 * time.Millisecond)
+
+ // Первое событие сразу
+ status := services.GetAllServicesStatus()
+ runtime.EventsEmit(appContext, "service:changed", status)
for {
time.Sleep(500 * time.Millisecond)
@@ -318,19 +423,12 @@ func (a *App) DisableACMEService() string {
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()
- }
-
+ exec.Command("explorer", absPath).Start()
return "Folder opened"
}
diff --git a/Backend/admin/go/proxy/proxy.go b/Backend/admin/go/proxy/proxy.go
index 6401649..750cdb0 100644
--- a/Backend/admin/go/proxy/proxy.go
+++ b/Backend/admin/go/proxy/proxy.go
@@ -14,6 +14,7 @@ func GetProxyList() []ProxyInfo {
}
proxyInfo := ProxyInfo{
+ Name: proxyConfig.Name,
Enable: proxyConfig.Enable,
ExternalDomain: proxyConfig.ExternalDomain,
LocalAddress: proxyConfig.LocalAddress,
diff --git a/Backend/admin/go/proxy/types.go b/Backend/admin/go/proxy/types.go
index be0a016..640f71f 100644
--- a/Backend/admin/go/proxy/types.go
+++ b/Backend/admin/go/proxy/types.go
@@ -1,13 +1,14 @@
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"`
- AutoCreateSSL bool `json:"auto_create_ssl"`
- Status string `json:"status"`
+ Name string `json:"Name"`
+ Enable bool `json:"Enable"`
+ ExternalDomain string `json:"ExternalDomain"`
+ LocalAddress string `json:"LocalAddress"`
+ LocalPort string `json:"LocalPort"`
+ ServiceHTTPSuse bool `json:"ServiceHTTPSuse"`
+ AutoHTTPS bool `json:"AutoHTTPS"`
+ AutoCreateSSL bool `json:"AutoCreateSSL"`
+ Status string `json:"Status"`
}
diff --git a/Backend/admin/go/services/services.go b/Backend/admin/go/services/services.go
index 26f32c0..0e22a66 100644
--- a/Backend/admin/go/services/services.go
+++ b/Backend/admin/go/services/services.go
@@ -86,7 +86,7 @@ func getProxyStatus() ServiceStatus {
return ServiceStatus{
Name: "Proxy",
Status: status,
- Port: "-",
+ Port: "",
Info: info,
}
}
diff --git a/Backend/config/config.go b/Backend/config/config.go
index e3b486c..1a48987 100644
--- a/Backend/config/config.go
+++ b/Backend/config/config.go
@@ -34,6 +34,7 @@ type Soft_Settings struct {
}
type Proxy_Service struct {
+ Name string `json:"Name"`
Enable bool `json:"Enable"`
ExternalDomain string `json:"ExternalDomain"`
LocalAddress string `json:"LocalAddress"`
diff --git a/build_admin.ps1 b/build_admin.ps1
index 4208740..ebb21a8 100644
--- a/build_admin.ps1
+++ b/build_admin.ps1
@@ -1,8 +1,7 @@
-# vServer Admin Panel Builder
+# vServer Admin Panel Builder
$ErrorActionPreference = 'SilentlyContinue'
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
-# Очищаем консоль
Clear-Host
Start-Sleep -Milliseconds 100
@@ -38,78 +37,127 @@ function Write-ProgressBar {
Write-Host " [$bar] $Percent%" -ForegroundColor Cyan
}
+function Check-And-Install {
+ param($Name, $Command, $WingetId)
+
+ $found = Get-Command $Command -ErrorAction SilentlyContinue
+
+ if (-not $found) {
+ Write-Err "$Name not found!"
+ $answer = Read-Host " ? Install $Name via winget? (y/n)"
+ if ($answer -eq 'y' -or $answer -eq 'Y') {
+ Write-Info "Installing $Name..."
+ winget install --id $WingetId --accept-source-agreements --accept-package-agreements 2>&1 | Out-Null
+ if ($LASTEXITCODE -eq 0) {
+ Write-Success "$Name installed! Restart terminal and run script again."
+ } else {
+ Write-Err "Failed to install. Install manually: winget install $WingetId"
+ }
+ Write-Host ""
+ exit 1
+ } else {
+ Write-Err "Cannot continue without $Name"
+ Write-Host ""
+ exit 1
+ }
+ } else {
+ Write-Success "$Name found"
+ }
+}
+
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..."
+# Step 0: Check dependencies
+Write-Step 1 6 "Checking dependencies..."
+Check-And-Install "Go" "go" "GoLang.Go"
+Check-And-Install "Node.js" "node" "OpenJS.NodeJS.LTS"
+Check-And-Install "npm" "npm" "OpenJS.NodeJS.LTS"
+Write-ProgressBar 10
+Write-Host ""
+
+# Step 1: go.mod
+Write-Step 2 6 "Checking go.mod..."
if (-not (Test-Path "go.mod")) {
- Write-Info "Создание go.mod..."
+ Write-Info "Creating go.mod..."
go mod init vServer 2>&1 | Out-Null
- Write-Success "Создан"
+ Write-Success "Created"
} else {
- Write-Success "Найден"
+ Write-Success "Found"
}
-Write-ProgressBar 25
+Write-ProgressBar 20
Write-Host ""
-Write-Step 2 4 "Установка зависимостей..."
+# Step 2: Go dependencies
+Write-Step 3 6 "Installing Go dependencies..."
go mod tidy 2>&1 | Out-Null
-Write-Success "Зависимости установлены"
-Write-ProgressBar 50
+Write-Success "Dependencies installed"
+Write-ProgressBar 40
Write-Host ""
-Write-Step 3 5 "Проверка Wails CLI..."
+# Step 3: Wails CLI
+Write-Step 4 6 "Checking Wails CLI..."
$null = wails version 2>&1
if ($LASTEXITCODE -ne 0) {
- Write-Info "Установка Wails CLI..."
+ Write-Info "Installing Wails CLI..."
go install github.com/wailsapp/wails/v2/cmd/wails@latest 2>&1 | Out-Null
- Write-Success "Установлен"
+ Write-Success "Installed"
} else {
- Write-Success "Найден"
+ Write-Success "Found"
}
-Write-ProgressBar 60
+Write-ProgressBar 55
Write-Host ""
-Write-Step 4 5 "Генерация биндингов..."
-Write-Info "Создание TypeScript/JS биндингов для Go методов..."
-wails generate module 2>&1 | Out-Null
-Write-Success "Биндинги сгенерированы"
-Write-ProgressBar 80
+# Step 4: Vue frontend
+Write-Step 5 6 "Building Vue frontend..."
+Push-Location "front_vue"
+Write-Info "npm install..."
+npm install 2>&1 | Out-Null
+Write-Info "npm run build..."
+npm run build 2>&1 | Out-Null
+Pop-Location
+Write-Success "Frontend built"
+Write-ProgressBar 75
Write-Host ""
-Write-Step 5 5 "Сборка приложения..."
-Write-Info "Компиляция (может занять ~10 сек)..."
+# Step 5: Wails build
+Write-Step 6 6 "Building application..."
+Write-Info "Compiling (may take ~30 sec)..."
+wails build 2>&1 | Out-Null
-wails build -f admin.go 2>&1 | Out-Null
+$exePath = $null
+if (Test-Path "build\bin\vServer-Admin.exe") {
+ $exePath = "build\bin\vServer-Admin.exe"
+}
+if ((-not $exePath) -and (Test-Path "bin\vServer-Admin.exe")) {
+ $exePath = "bin\vServer-Admin.exe"
+}
-if (Test-Path "bin\vServer-Admin.exe") {
- Write-Success "Скомпилировано"
+if ($exePath) {
+ Write-Success "Compiled"
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 "Finalizing..." -ForegroundColor Cyan
+ Move-Item -Path $exePath -Destination "vSerf.exe" -Force -ErrorAction SilentlyContinue
+ Write-Success "File moved: vSerf.exe"
+ if (Test-Path "build") { Remove-Item -Path "build" -Recurse -Force -ErrorAction SilentlyContinue }
+ if (Test-Path "bin") { Remove-Item -Path "bin" -Recurse -Force -ErrorAction SilentlyContinue }
+ if (Test-Path "windows") { Remove-Item -Path "windows" -Recurse -Force -ErrorAction SilentlyContinue }
+ Write-Success "Temp files removed"
Write-Host ""
-
Write-Host "=================================================" -ForegroundColor Green
- Write-Host " УСПЕШНО СОБРАНО!" -ForegroundColor Green
- Write-Host " Файл: " -ForegroundColor Green -NoNewline
+ Write-Host " BUILD SUCCESS!" -ForegroundColor Green
+ Write-Host " File: " -ForegroundColor Green -NoNewline
Write-Host "vSerf.exe" -ForegroundColor Cyan
Write-Host "=================================================" -ForegroundColor Green
} else {
- Write-Err "Ошибка компиляции"
+ Write-Err "Compilation error"
Write-Host ""
-
Write-Host "=================================================" -ForegroundColor Red
- Write-Host " ОШИБКА СБОРКИ!" -ForegroundColor Red
+ Write-Host " BUILD FAILED!" -ForegroundColor Red
Write-Host "=================================================" -ForegroundColor Red
}
diff --git a/front_vue/package.json.md5 b/front_vue/package.json.md5
new file mode 100644
index 0000000..afc9c65
--- /dev/null
+++ b/front_vue/package.json.md5
@@ -0,0 +1 @@
+ed2519d496d19e187b316251711a6b7a
\ No newline at end of file
diff --git a/front_vue/src/App.vue b/front_vue/src/App.vue
index 9d93560..7645f9f 100644
--- a/front_vue/src/App.vue
+++ b/front_vue/src/App.vue
@@ -1,7 +1,97 @@
+
+
+
+
🚀
+
Запуск vServer...
+
+
+
+
+
+
diff --git a/front_vue/src/Core/api/mock-data/proxies.json b/front_vue/src/Core/api/mock-data/proxies.json
index bb6d65e..2325ad4 100644
--- a/front_vue/src/Core/api/mock-data/proxies.json
+++ b/front_vue/src/Core/api/mock-data/proxies.json
@@ -1,5 +1,6 @@
[
{
+ "Name": "Git Server",
"Enable": true,
"ExternalDomain": "git.example.ru",
"LocalAddress": "127.0.0.1",
@@ -9,6 +10,7 @@
"AutoCreateSSL": false
},
{
+ "Name": "API Backend",
"Enable": true,
"ExternalDomain": "api.example.com",
"LocalAddress": "127.0.0.1",
@@ -18,6 +20,7 @@
"AutoCreateSSL": true
},
{
+ "Name": "Test App",
"Enable": false,
"ExternalDomain": "test.example.net",
"LocalAddress": "127.0.0.1",
diff --git a/front_vue/src/Core/api/mock.js b/front_vue/src/Core/api/mock.js
index 4c7e806..7da63da 100644
--- a/front_vue/src/Core/api/mock.js
+++ b/front_vue/src/Core/api/mock.js
@@ -49,6 +49,11 @@ export const mockApi = {
return 'OK'
},
+ async updateSiteCache() {
+ await delay(200)
+ return 'OK'
+ },
+
async openSiteFolder(host) {
await delay(100)
},
diff --git a/front_vue/src/Core/api/wails.js b/front_vue/src/Core/api/wails.js
index 01cc949..2dd35b2 100644
--- a/front_vue/src/Core/api/wails.js
+++ b/front_vue/src/Core/api/wails.js
@@ -37,6 +37,10 @@ export const wailsApi = {
return await app().DeleteSite(host)
},
+ async updateSiteCache() {
+ return await app().UpdateSiteCache()
+ },
+
async openSiteFolder(host) {
return await app().OpenSiteFolder(host)
},
diff --git a/front_vue/src/Core/composables/useDraggable.js b/front_vue/src/Core/composables/useDraggable.js
new file mode 100644
index 0000000..ada11d7
--- /dev/null
+++ b/front_vue/src/Core/composables/useDraggable.js
@@ -0,0 +1,58 @@
+export function useDraggable(list) {
+ const dragIndex = ref(null)
+ const dragOverIndex = ref(null)
+
+ const onDragStart = (index, event) => {
+ dragIndex.value = index
+ event.dataTransfer.effectAllowed = 'move'
+ event.target.closest('tr, .draggable-item')?.classList.add('dragging')
+ }
+
+ const onDragOver = (index, event) => {
+ event.preventDefault()
+ event.dataTransfer.dropEffect = 'move'
+ dragOverIndex.value = index
+ }
+
+ const onDragEnter = (index, event) => {
+ event.preventDefault()
+ dragOverIndex.value = index
+ }
+
+ const onDragLeave = () => {
+ dragOverIndex.value = null
+ }
+
+ const onDrop = (index) => {
+ if (dragIndex.value === null || dragIndex.value === index) {
+ dragIndex.value = null
+ dragOverIndex.value = null
+ return
+ }
+
+ const items = [...list.value]
+ const [moved] = items.splice(dragIndex.value, 1)
+ items.splice(index, 0, moved)
+ list.value = items
+
+ dragIndex.value = null
+ dragOverIndex.value = null
+ }
+
+ const onDragEnd = (event) => {
+ event.target.closest('tr, .draggable-item')?.classList.remove('dragging')
+ dragIndex.value = null
+ dragOverIndex.value = null
+ }
+
+ return {
+ dragIndex,
+ dragOverIndex,
+ onDragStart,
+ onDragOver,
+ onDragEnter,
+ onDragLeave,
+ onDrop,
+ onDragEnd,
+ }
+}
diff --git a/front_vue/src/Core/i18n/en.json b/front_vue/src/Core/i18n/en.json
index 46d74c0..f8aca1d 100644
--- a/front_vue/src/Core/i18n/en.json
+++ b/front_vue/src/Core/i18n/en.json
@@ -2,7 +2,7 @@
"app": {
"title": "vServer Admin Panel",
"logo": "vServer",
- "footer": "vServer Admin Panel © 2025 | Author: Sumaneev Roman",
+ "footerAuthor": "Author: Sumaneev Roman",
"loading": "Starting vServer..."
},
"nav": {
@@ -18,7 +18,10 @@
"running": "Server is running",
"stopped": "Server is stopped",
"start": "Start",
- "stop": "Stop"
+ "stop": "Stop",
+ "starting": "Starting...",
+ "stopping": "Stopping...",
+ "wait": "Please wait..."
},
"services": {
"title": "Services Status",
@@ -174,10 +177,11 @@
},
"notify": {
"settingsSaved": "Settings saved and services restarted!",
- "settingsTestMode": "Settings saved (test mode)",
- "dataSaved": "Data saved (test mode)",
+ "settingsSaved": "Settings saved",
+ "dataSaved": "Data saved",
"siteCreated": "Site created successfully!",
"siteDeleted": "Site deleted successfully!",
+ "proxyCreated": "Proxy created successfully!",
"proxyDeleted": "Proxy deleted successfully!",
"changesSaved": "Changes saved and applied!",
"serversRestarted": "Servers restarted!",
diff --git a/front_vue/src/Core/i18n/ru.json b/front_vue/src/Core/i18n/ru.json
index fa84309..4c48389 100644
--- a/front_vue/src/Core/i18n/ru.json
+++ b/front_vue/src/Core/i18n/ru.json
@@ -2,7 +2,7 @@
"app": {
"title": "vServer Admin Panel",
"logo": "vServer",
- "footer": "vServer Admin Panel © 2025 | Автор: Суманеев Роман",
+ "footerAuthor": "Автор: Суманеев Роман",
"loading": "Запуск vServer..."
},
"nav": {
@@ -18,7 +18,10 @@
"running": "Сервер запущен",
"stopped": "Сервер остановлен",
"start": "Запустить",
- "stop": "Остановить"
+ "stop": "Остановить",
+ "starting": "Запускается...",
+ "stopping": "Выключается...",
+ "wait": "Ожидайте..."
},
"services": {
"title": "Статус сервисов",
@@ -174,10 +177,11 @@
},
"notify": {
"settingsSaved": "Настройки сохранены и сервисы перезапущены!",
- "settingsTestMode": "Настройки сохранены (тестовый режим)",
- "dataSaved": "Данные сохранены (тестовый режим)",
+ "settingsSaved": "Настройки сохранены",
+ "dataSaved": "Данные сохранены",
"siteCreated": "Сайт успешно создан!",
"siteDeleted": "Сайт успешно удалён!",
+ "proxyCreated": "Прокси успешно создан!",
"proxyDeleted": "Прокси успешно удалён!",
"changesSaved": "Изменения сохранены и применены!",
"serversRestarted": "Серверы перезапущены!",
diff --git a/front_vue/src/Core/stores/proxies.js b/front_vue/src/Core/stores/proxies.js
index d9137c6..b06b041 100644
--- a/front_vue/src/Core/stores/proxies.js
+++ b/front_vue/src/Core/stores/proxies.js
@@ -15,5 +15,38 @@ export const useProxiesStore = defineStore('proxies', {
this.loaded = true
}
},
+
+ async create(proxyData) {
+ const config = await api.getConfig()
+ config.Proxy_Service.push({
+ Name: proxyData.name || '',
+ Enable: proxyData.enabled,
+ ExternalDomain: proxyData.domain,
+ LocalAddress: proxyData.localAddr,
+ LocalPort: proxyData.localPort,
+ ServiceHTTPSuse: proxyData.serviceHttps,
+ AutoHTTPS: proxyData.autoHttps,
+ AutoCreateSSL: proxyData.autoSSL,
+ })
+ const result = await api.saveConfig(JSON.stringify(config))
+ if (result && !String(result).startsWith('Error')) {
+ await api.reloadConfig()
+ await this.load()
+ }
+ return result
+ },
+
+ async remove(domain) {
+ const config = await api.getConfig()
+ config.Proxy_Service = config.Proxy_Service.filter(
+ p => p.ExternalDomain !== domain
+ )
+ const result = await api.saveConfig(JSON.stringify(config))
+ if (result && !String(result).startsWith('Error')) {
+ await api.reloadConfig()
+ await this.load()
+ }
+ return result
+ },
},
})
diff --git a/front_vue/src/Core/stores/services.js b/front_vue/src/Core/stores/services.js
index e15fdb5..8fdfa53 100644
--- a/front_vue/src/Core/stores/services.js
+++ b/front_vue/src/Core/stores/services.js
@@ -5,17 +5,28 @@ export const useServicesStore = defineStore('services', {
state: () => ({
list: [],
loaded: false,
+ isOperating: false,
}),
actions: {
async load() {
const data = await api.getAllServicesStatus()
if (data) {
- this.list = Array.isArray(data) ? data : [data]
+ this.list = Array.isArray(data) ? data : Object.values(data)
this.loaded = true
}
},
+ setPending(text) {
+ this.isOperating = true
+ this.list = this.list.map(s => ({ ...s, pending: text }))
+ },
+
+ async clearPending() {
+ await this.load()
+ this.isOperating = false
+ },
+
async startService(name) {
const methods = {
HTTP: () => api.startHTTPService(),
diff --git a/front_vue/src/Design/assets/css/base.css b/front_vue/src/Design/assets/css/base.css
index 08c3b87..e6d55c6 100644
--- a/front_vue/src/Design/assets/css/base.css
+++ b/front_vue/src/Design/assets/css/base.css
@@ -77,3 +77,12 @@ input, select, textarea, button {
font-family: inherit;
font-size: inherit;
}
+
+.icon-spin {
+ animation: icon-spin-anim 1s linear infinite;
+}
+
+@keyframes icon-spin-anim {
+ from { transform: rotate(0deg); }
+ to { transform: rotate(360deg); }
+}
diff --git a/front_vue/src/Design/assets/css/tables.css b/front_vue/src/Design/assets/css/tables.css
index 325bd4b..16da6d2 100644
--- a/front_vue/src/Design/assets/css/tables.css
+++ b/front_vue/src/Design/assets/css/tables.css
@@ -166,3 +166,10 @@
border-color: var(--btn-icon-hover-border);
transform: translateY(-1px);
}
+
+.icon-btn:disabled {
+ opacity: 0.6;
+ cursor: wait;
+ transform: none;
+}
+
diff --git a/front_vue/src/Design/components/layout/AppFooter.vue b/front_vue/src/Design/components/layout/AppFooter.vue
index 6af5c97..e7765f7 100644
--- a/front_vue/src/Design/components/layout/AppFooter.vue
+++ b/front_vue/src/Design/components/layout/AppFooter.vue
@@ -1,10 +1,20 @@
@@ -13,8 +23,18 @@ const { t } = useI18n()
padding: var(--space-sm) var(--space-lg);
text-align: center;
color: var(--text-muted);
- font-size: var(--text-xs);
+ font-size: var(--text-sm);
border-top: 1px solid var(--glass-border);
background: var(--glass-bg-dark);
}
+
+.footer-link {
+ color: var(--text-secondary);
+ text-decoration: underline;
+ transition: color var(--transition-fast);
+}
+
+.footer-link:hover {
+ color: var(--text-primary);
+}
diff --git a/front_vue/src/Design/components/layout/TitleBar.vue b/front_vue/src/Design/components/layout/TitleBar.vue
index 7690b8b..c5f6e25 100644
--- a/front_vue/src/Design/components/layout/TitleBar.vue
+++ b/front_vue/src/Design/components/layout/TitleBar.vue
@@ -1,8 +1,17 @@
@@ -26,14 +65,14 @@ const serverToggle = async () => {
{{ t('app.logo') }}
-
- {{ appStore.serverRunning ? t('server.running') : t('server.stopped') }}
+
+ {{ statusLabel || (appStore.serverRunning ? t('server.running') : t('server.stopped')) }}
-
@@ -112,6 +151,17 @@ const serverToggle = async () => {
box-shadow: 0 0 8px var(--accent-red);
}
+.status-pending {
+ background: var(--accent-yellow, #f0ad4e);
+ box-shadow: 0 0 8px var(--accent-yellow, #f0ad4e);
+ animation: pulse-pending 1s ease-in-out infinite;
+}
+
+@keyframes pulse-pending {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0.4; }
+}
+
.status-text {
font-size: var(--text-sm);
color: var(--text-secondary);
diff --git a/front_vue/src/Design/components/proxies/ProxiesTable.vue b/front_vue/src/Design/components/proxies/ProxiesTable.vue
index f66491b..7d43501 100644
--- a/front_vue/src/Design/components/proxies/ProxiesTable.vue
+++ b/front_vue/src/Design/components/proxies/ProxiesTable.vue
@@ -4,6 +4,14 @@ const router = useRouter()
const proxiesStore = useProxiesStore()
const certsStore = useCertsStore()
+const openUrl = (host) => {
+ if (window.runtime?.BrowserOpenURL) {
+ window.runtime.BrowserOpenURL('http://' + host)
+ } else {
+ window.open('http://' + host, '_blank')
+ }
+}
+
const findCertForDomain = (domain) => {
const direct = certsStore.list.find(c => c.domain === domain && c.has_cert)
if (direct) return direct
@@ -43,10 +51,10 @@ const findCertForDomain = (domain) => {
+ | {{ t('sites.name') }} |
{{ t('proxies.externalDomain') }} |
{{ t('proxies.localAddress') }} |
- {{ t('proxies.httpsCol') }} |
- {{ t('proxies.autoHttps') }} |
+ HTTPS |
{{ t('proxies.status') }} |
{{ t('proxies.actions') }} |
@@ -54,10 +62,13 @@ const findCertForDomain = (domain) => {
-
+
- {{ proxy.ExternalDomain }}
+ {{ proxy.Name || '—' }}
+ |
+
+ {{ proxy.ExternalDomain }}
|
{{ proxy.LocalAddress }}:{{ proxy.LocalPort }} |
@@ -65,11 +76,6 @@ const findCertForDomain = (domain) => {
{{ proxy.ServiceHTTPSuse ? 'HTTPS' : 'HTTP' }}
|
-
-
- {{ proxy.AutoHTTPS ? t('common.yes') : t('common.no') }}
-
- |
{{ proxy.Enable ? 'active' : 'disabled' }}
diff --git a/front_vue/src/Design/components/services/ServiceCard.vue b/front_vue/src/Design/components/services/ServiceCard.vue
index e973913..49f3134 100644
--- a/front_vue/src/Design/components/services/ServiceCard.vue
+++ b/front_vue/src/Design/components/services/ServiceCard.vue
@@ -29,8 +29,8 @@ const serviceInfoLabel = {
{{ service.name }}
-
- {{ service.status ? t('common.enabled') : t('common.disabled') }}
+
+ {{ service.pending || (service.status ? t('common.enabled') : t('common.disabled')) }}
diff --git a/front_vue/src/Design/components/sites/SitesTable.vue b/front_vue/src/Design/components/sites/SitesTable.vue
index f12c8f4..45ec735 100644
--- a/front_vue/src/Design/components/sites/SitesTable.vue
+++ b/front_vue/src/Design/components/sites/SitesTable.vue
@@ -6,6 +6,22 @@ const certsStore = useCertsStore()
const { success, error } = useNotification()
const modal = useModal()
+const openingFolder = ref('')
+
+const openUrl = (host) => {
+ if (window.runtime?.BrowserOpenURL) {
+ window.runtime.BrowserOpenURL('http://' + host)
+ } else {
+ window.open('http://' + host, '_blank')
+ }
+}
+
+const openFolder = async (host) => {
+ openingFolder.value = host
+ await sitesStore.openFolder(host)
+ setTimeout(() => { openingFolder.value = '' }, 800)
+}
+
const findCertForDomain = (domain, aliases = []) => {
const allDomains = [domain, ...aliases.filter(a => !a.includes('*'))]
for (const d of allDomains) {
@@ -42,7 +58,7 @@ const confirmDelete = (site) => {
warning: t('sites.deleteWarning'),
onConfirm: async () => {
const result = await sitesStore.remove(site.host)
- if (result === 'OK') success(t('notify.siteDeleted'))
+ if (result && !String(result).startsWith('Error')) success(t('notify.siteDeleted'))
else error(String(result))
modal.close()
},
@@ -79,7 +95,7 @@ const confirmDelete = (site) => {
{{ site.name }}
|
- {{ site.host }}
+ {{ site.host }}
|
{{ site.alias?.join(', ') || '—' }} |
@@ -89,8 +105,9 @@ const confirmDelete = (site) => {
|
{{ site.root_file }} |
-
-
+
+
+
diff --git a/front_vue/src/Design/components/ui/VButton.vue b/front_vue/src/Design/components/ui/VButton.vue
index 907b306..2788f1e 100644
--- a/front_vue/src/Design/components/ui/VButton.vue
+++ b/front_vue/src/Design/components/ui/VButton.vue
@@ -16,7 +16,7 @@ defineEmits(['click'])
:disabled="disabled || loading"
@click="$emit('click', $event)"
>
-
+
diff --git a/front_vue/src/Design/layouts/MainLayout.vue b/front_vue/src/Design/layouts/MainLayout.vue
index 293e458..7158c7e 100644
--- a/front_vue/src/Design/layouts/MainLayout.vue
+++ b/front_vue/src/Design/layouts/MainLayout.vue
@@ -1,9 +1,31 @@
@@ -53,6 +75,6 @@ onMounted(() => {
.main-content {
flex: 1;
overflow-y: auto;
- padding: var(--space-lg);
+ padding: 40px var(--space-3xl);
}
diff --git a/front_vue/src/Design/views/CertManagerView.vue b/front_vue/src/Design/views/CertManagerView.vue
index cac5759..c965eb0 100644
--- a/front_vue/src/Design/views/CertManagerView.vue
+++ b/front_vue/src/Design/views/CertManagerView.vue
@@ -9,8 +9,13 @@ const props = defineProps({
const certs = ref([])
const loading = ref(true)
+const issuing = ref('')
+const renewing = ref('')
+const deleting = ref('')
-onMounted(async () => {
+const sleep = (ms) => new Promise(r => setTimeout(r, ms))
+
+const refreshCerts = async () => {
await certsStore.loadAll()
certs.value = certsStore.list.filter(c =>
c.dns_names?.some(d => d === props.host || d === `*.${props.host}` || props.host.match(new RegExp(d.replace('*.', '.*\\.'))))
@@ -19,25 +24,47 @@ onMounted(async () => {
const info = await certsStore.getInfo(props.host)
if (info && info.has_cert) certs.value = [info]
}
+}
+
+onMounted(async () => {
+ await refreshCerts()
loading.value = false
})
const issueCert = async (domain) => {
- const result = await certsStore.issue(domain)
- if (result === 'OK') success(t('notify.certIssued'))
- else error(String(result))
+ issuing.value = domain
+ const [result] = await Promise.all([certsStore.issue(domain), sleep(1000)])
+ if (result && !String(result).startsWith('Error')) {
+ success(t('notify.certIssued'))
+ await refreshCerts()
+ } else {
+ error(String(result))
+ }
+ issuing.value = ''
}
const renewCert = async (domain) => {
- const result = await certsStore.renew(domain)
- if (result === 'OK') success(t('notify.certRenewed'))
- else error(String(result))
+ renewing.value = domain
+ const [result] = await Promise.all([certsStore.renew(domain), sleep(1000)])
+ if (result && !String(result).startsWith('Error')) {
+ success(t('notify.certRenewed'))
+ await refreshCerts()
+ } else {
+ error(String(result))
+ }
+ renewing.value = ''
}
const deleteCert = async (domain) => {
- const result = await certsStore.remove(domain)
- if (result === 'OK') success(t('notify.certDeleted'))
- else error(String(result))
+ deleting.value = domain
+ const [result] = await Promise.all([certsStore.remove(domain), sleep(1000)])
+ if (result && !String(result).startsWith('Error')) {
+ success(t('notify.certDeleted'))
+ await refreshCerts()
+ } else {
+ error(String(result))
+ }
+ deleting.value = ''
}
@@ -53,7 +80,7 @@ const deleteCert = async (domain) => {
{{ t('certs.noCert') }}
{{ t('certs.subtitle') }}
- {{ t('certs.issue') }}
+ {{ t('certs.issue') }}
@@ -64,13 +91,13 @@ const deleteCert = async (domain) => {
{{ cert.domain }}
-
+
{{ t('certs.renew') }}
-
+
{{ t('certs.issue') }}
-
+
{{ t('certs.delete') }}
diff --git a/front_vue/src/Design/views/DashboardView.vue b/front_vue/src/Design/views/DashboardView.vue
index 3b06b05..b42e4ba 100644
--- a/front_vue/src/Design/views/DashboardView.vue
+++ b/front_vue/src/Design/views/DashboardView.vue
@@ -6,11 +6,13 @@ const certsStore = useCertsStore()
onMounted(async () => {
await Promise.all([
- servicesStore.load(),
sitesStore.load(),
proxiesStore.load(),
certsStore.loadAll(),
])
+ if (!servicesStore.loaded) {
+ await servicesStore.load()
+ }
})
diff --git a/front_vue/src/Design/views/ProxyCreateView.vue b/front_vue/src/Design/views/ProxyCreateView.vue
index 7ef075e..f4f7ca6 100644
--- a/front_vue/src/Design/views/ProxyCreateView.vue
+++ b/front_vue/src/Design/views/ProxyCreateView.vue
@@ -5,11 +5,11 @@ const proxiesStore = useProxiesStore()
const { success, error } = useNotification()
const form = reactive({
+ name: '',
domain: '',
localAddr: '127.0.0.1',
localPort: '',
serviceHttps: false,
- autoHttps: true,
certMode: 'none',
})
@@ -18,10 +18,23 @@ const creating = ref(false)
const createProxy = async () => {
if (!form.domain || !form.localPort) return
creating.value = true
- const result = await proxiesStore.load()
+ const result = await proxiesStore.create({
+ name: form.name,
+ domain: form.domain,
+ localAddr: form.localAddr,
+ localPort: form.localPort,
+ enabled: true,
+ serviceHttps: form.serviceHttps,
+ autoHttps: form.serviceHttps,
+ autoSSL: false,
+ })
creating.value = false
- success(t('notify.dataSaved'))
- router.push('/')
+ if (result && !String(result).startsWith('Error')) {
+ success(t('notify.proxyCreated'))
+ router.push('/')
+ } else {
+ error(String(result))
+ }
}
@@ -41,6 +54,7 @@ const createProxy = async () => {
|