VUE дизайн

This commit is contained in:
2026-02-08 05:37:13 +07:00
parent bdfa2404b5
commit caf52afcdf
73 changed files with 1148 additions and 7211 deletions

394
Backend/admin/API.md Normal file
View File

@@ -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`

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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%);
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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);
}

File diff suppressed because one or more lines are too long

View File

@@ -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';

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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();

View File

@@ -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();

View File

@@ -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 = '<span style="color: #e74c3c;"><i class="fas fa-times-circle"></i> Файл слишком большой (макс 1MB)</span>';
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 = `<span style="color: #2ecc71;"><i class="fas fa-check-circle"></i> Загружен успешно</span>`;
};
reader.onerror = () => {
statusDiv.innerHTML = '<span style="color: #e74c3c;"><i class="fas fa-times-circle"></i> Ошибка чтения файла</span>';
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;
}
}
}

View File

@@ -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 `<span class="cert-icon cert-expired" title="SSL сертификат истёк"><i class="fas fa-shield-alt"></i></span>`;
} else {
return `<span class="cert-icon cert-valid" title="SSL активен (${cert.days_left} дн.)"><i class="fas fa-shield-alt"></i></span>`;
}
}
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 = `
<td>${certIcon}<code class="clickable-link" data-url="${protocol}://${proxy.external_domain}">${proxy.external_domain} <i class="fas fa-external-link-alt"></i></code></td>
<td><code>${proxy.local_address}:${proxy.local_port}</code></td>
<td><span class="badge ${httpsBadge}</span></td>
<td><span class="badge ${autoHttpsBadge}</span></td>
<td><span class="badge ${statusBadge}">${proxy.status}</span></td>
<td>
<button class="icon-btn" data-action="edit-vaccess" data-host="${proxy.external_domain}" data-is-proxy="true" title="vAccess"><i class="fas fa-user-lock"></i></button>
<button class="icon-btn" data-action="open-certs" data-host="${proxy.external_domain}" data-is-proxy="true" title="SSL сертификаты"><i class="fas fa-shield-alt"></i></button>
<button class="icon-btn" data-action="edit-proxy" data-index="${index}" title="Редактировать"><i class="fas fa-edit"></i></button>
</td>
`;
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');
}
}
}

View File

@@ -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 уже запущен!<br><br>Закройте другой экземпляр перед запуском нового.', 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);
}
}
}

View File

@@ -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 = '<span style="color: #e74c3c;"><i class="fas fa-times-circle"></i> Файл слишком большой (макс 1MB)</span>';
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 = `<span style="color: #2ecc71;"><i class="fas fa-check-circle"></i> Загружен успешно</span>`;
};
reader.onerror = () => {
statusDiv.innerHTML = '<span style="color: #e74c3c;"><i class="fas fa-times-circle"></i> Ошибка чтения файла</span>';
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;
}
}
}

View File

@@ -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 `<span class="cert-icon cert-expired" title="SSL сертификат истёк"><i class="fas fa-shield-alt"></i></span>`;
} else {
return `<span class="cert-icon cert-valid" title="SSL активен (${cert.days_left} дн.)"><i class="fas fa-shield-alt"></i></span>`;
}
}
}
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 = `
<td>${certIcon}${site.name}</td>
<td><code class="clickable-link" data-url="http://${site.host}">${site.host} <i class="fas fa-external-link-alt"></i></code></td>
<td><code>${aliases}</code></td>
<td><span class="badge ${statusBadge}">${site.status}</span></td>
<td><code>${site.root_file}</code></td>
<td>
<button class="icon-btn" data-action="open-folder" data-host="${site.host}" title="Открыть папку"><i class="fas fa-folder-open"></i></button>
<button class="icon-btn" data-action="edit-vaccess" data-host="${site.host}" data-is-proxy="false" title="vAccess"><i class="fas fa-user-lock"></i></button>
<button class="icon-btn" data-action="open-certs" data-host="${site.host}" data-aliases="${site.alias.join(',')}" data-is-proxy="false" title="SSL сертификаты"><i class="fas fa-shield-alt"></i></button>
<button class="icon-btn" data-action="edit-site" data-index="${index}" title="Редактировать"><i class="fas fa-edit"></i></button>
</td>
`;
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');
}
}
}

View File

@@ -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) => `
<tr draggable="true" data-index="${index}">
<td class="drag-handle"><i class="fas fa-grip-vertical"></i></td>
<td data-field="type" data-index="${index}">
<span class="badge ${rule.type === 'Allow' ? 'badge-yes' : 'badge-no'}">${rule.type}</span>
</td>
<td data-field="type_file" data-index="${index}">
${(rule.type_file || []).length > 0 ? (rule.type_file || []).map(f => `<code class="mini-tag">${f}</code>`).join(' ') : '<span class="empty-field">-</span>'}
</td>
<td data-field="path_access" data-index="${index}">
${(rule.path_access || []).length > 0 ? (rule.path_access || []).map(p => `<code class="mini-tag">${p}</code>`).join(' ') : '<span class="empty-field">-</span>'}
</td>
<td data-field="ip_list" data-index="${index}">
${(rule.ip_list || []).length > 0 ? (rule.ip_list || []).map(ip => `<code class="mini-tag">${ip}</code>`).join(' ') : '<span class="empty-field">-</span>'}
</td>
<td data-field="exceptions_dir" data-index="${index}">
${(rule.exceptions_dir || []).length > 0 ? (rule.exceptions_dir || []).map(e => `<code class="mini-tag">${e}</code>`).join(' ') : '<span class="empty-field">-</span>'}
</td>
<td data-field="url_error" data-index="${index}">
<code class="mini-tag">${rule.url_error || '404'}</code>
</td>
<td>
<button class="icon-btn-small" data-action="remove-rule" data-index="${index}" title="Удалить"><i class="fas fa-trash"></i></button>
</td>
</tr>
`).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 = `
<div class="field-editor">
<div class="tag-input-wrapper" style="margin-bottom: 16px;">
<input type="text" class="form-input" id="fieldInput" placeholder="${placeholders[field]}">
<button class="action-btn" id="addFieldValueBtn"><i class="fas fa-plus"></i> Добавить</button>
</div>
<div class="tags-container" id="fieldTags">
${(rule[field] || []).map(value => `
<span class="tag">
${value}
<button class="tag-remove" data-value="${value}"><i class="fas fa-times"></i></button>
</span>
`).join('')}
</div>
<button class="action-btn" id="closeFieldEditorBtn" style="margin-top: 20px;">
<i class="fas fa-check"></i> Готово
</button>
</div>
`;
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();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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 = `
<span class="custom-select-value">${selectedText}</span>
<i class="fas fa-chevron-down custom-select-arrow"></i>
`;
// Создаём выпадающий список
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);
}

View File

@@ -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();

View File

@@ -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);
}
}

View File

@@ -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'
? '<i class="fas fa-check-circle"></i>'
: '<i class="fas fa-exclamation-circle"></i>';
this.container.innerHTML = `
<div class="notification-content">
<div class="notification-icon">${icon}</div>
<div class="notification-text">${message}</div>
</div>
`;
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();

View File

@@ -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();
}
}
}

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -1,915 +0,0 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>vServer Admin Panel</title>
<link rel="stylesheet" href="assets/css/local_lib/all.min.css">
<link rel="stylesheet" href="assets/css/main.css">
</head>
<body>
<div class="window-controls" id="windowControls">
<div class="title-bar">
<div class="title-bar-left">
<div class="app-logo">
<span class="logo-icon">🚀</span>
<span class="logo-text">vServer</span>
</div>
<div class="server-status" id="serverStatus">
<span class="status-indicator status-online"></span>
<span class="status-text">Сервер запущен</span>
</div>
</div>
<div class="title-bar-right">
<button class="server-control-btn" id="serverControlBtn">
<i class="fas fa-power-off"></i>
<span class="btn-text">Остановить</span>
</button>
<button class="window-btn minimize-btn" id="minimizeBtn" title="Свернуть">
<i class="fas fa-window-minimize"></i>
</button>
<button class="window-btn maximize-btn" id="maximizeBtn" title="Развернуть">
<i class="far fa-window-maximize"></i>
</button>
<button class="window-btn close-btn" id="closeBtn" title="Закрыть">
<i class="fas fa-times"></i>
</button>
</div>
</div>
</div>
<aside class="sidebar">
<nav class="sidebar-nav">
<button class="nav-item active" data-page="dashboard" title="Главная">
<i class="fas fa-home"></i>
</button>
<button class="nav-item" data-page="settings" title="Настройки">
<i class="fas fa-cog"></i>
</button>
</nav>
</aside>
<div class="container">
<main class="main-content">
<section class="section" id="sectionServices">
<h2 class="section-title">Статус сервисов</h2>
<div class="services-grid" id="servicesGrid">
<div class="service-card">
<div class="service-header">
<h3 class="service-name"><i class="fas fa-globe"></i> HTTP</h3>
<span class="badge badge-pending">Запуск</span>
</div>
<div class="service-info">
<div class="info-row">
<span class="info-label">Порт:</span>
<span class="info-value">80</span>
</div>
</div>
</div>
<div class="service-card">
<div class="service-header">
<h3 class="service-name"><i class="fas fa-lock"></i> HTTPS</h3>
<span class="badge badge-pending">Запуск</span>
</div>
<div class="service-info">
<div class="info-row">
<span class="info-label">Порт:</span>
<span class="info-value">443</span>
</div>
</div>
</div>
<div class="service-card">
<div class="service-header">
<h3 class="service-name"><i class="fas fa-database"></i> MySQL</h3>
<span class="badge badge-pending">Запуск</span>
</div>
<div class="service-info">
<div class="info-row">
<span class="info-label">Порт:</span>
<span class="info-value">3306</span>
</div>
</div>
</div>
<div class="service-card">
<div class="service-header">
<h3 class="service-name"><i class="fab fa-php"></i> PHP</h3>
<span class="badge badge-pending">Запуск</span>
</div>
<div class="service-info">
<div class="info-row">
<span class="info-label">Порты:</span>
<span class="info-value">8000-8003</span>
</div>
</div>
</div>
<div class="service-card">
<div class="service-header">
<h3 class="service-name"><i class="fas fa-exchange-alt"></i> Proxy</h3>
<span class="badge badge-pending">Запуск</span>
</div>
<div class="service-info">
<div class="info-row">
<span class="info-label">Правил:</span>
<span class="info-value">0 из 1</span>
</div>
</div>
</div>
</div>
</section>
<section class="section" id="sectionSites">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;">
<h2 class="section-title" style="margin-bottom: 0;">Список сайтов</h2>
<button class="action-btn" id="addSiteBtn">
<i class="fas fa-plus"></i>
<span>Добавить сайт</span>
</button>
</div>
<div class="table-container">
<table class="data-table" id="sitesTable">
<thead>
<tr>
<th>Имя</th>
<th>Host</th>
<th>Alias</th>
<th>Статус</th>
<th>Root File</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
<tr>
<td>Локальный сайт</td>
<td><code class="clickable-link" onclick="openTestLink('http://127.0.0.1')">127.0.0.1 <i class="fas fa-external-link-alt"></i></code></td>
<td><code>localhost</code></td>
<td><span class="badge badge-online">active</span></td>
<td><code>index.html</code></td>
<td>
<button class="icon-btn" onclick="openSiteFolder('127.0.0.1')" title="Открыть папку"><i class="fas fa-folder-open"></i></button>
<button class="icon-btn" onclick="editVAccess('127.0.0.1', false)" title="vAccess"><i class="fas fa-shield-alt"></i></button>
<button class="icon-btn" onclick="editTestSite(0)" title="Редактировать"><i class="fas fa-edit"></i></button>
</td>
</tr>
<tr>
<td>Тестовый проект</td>
<td><code class="clickable-link" onclick="openTestLink('http://test.local')">test.local <i class="fas fa-external-link-alt"></i></code></td>
<td><code>*.test.local, test.com</code></td>
<td><span class="badge badge-online">active</span></td>
<td><code>index.php</code></td>
<td>
<button class="icon-btn" onclick="openSiteFolder('test.local')" title="Открыть папку"><i class="fas fa-folder-open"></i></button>
<button class="icon-btn" onclick="editVAccess('test.local', false)" title="vAccess"><i class="fas fa-shield-alt"></i></button>
<button class="icon-btn" onclick="editTestSite(1)" title="Редактировать"><i class="fas fa-edit"></i></button>
</td>
</tr>
<tr>
<td>API сервис</td>
<td><code class="clickable-link" onclick="openTestLink('http://api.example.com')">api.example.com <i class="fas fa-external-link-alt"></i></code></td>
<td><code>*.api.example.com</code></td>
<td><span class="badge badge-offline">inactive</span></td>
<td><code>index.php</code></td>
<td>
<button class="icon-btn" onclick="openSiteFolder('api.example.com')" title="Открыть папку"><i class="fas fa-folder-open"></i></button>
<button class="icon-btn" onclick="editVAccess('api.example.com', false)" title="vAccess"><i class="fas fa-shield-alt"></i></button>
<button class="icon-btn" onclick="editTestSite(2)" title="Редактировать"><i class="fas fa-edit"></i></button>
</td>
</tr>
</tbody>
</table>
</div>
</section>
<section class="section" id="sectionProxy">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;">
<h2 class="section-title" style="margin-bottom: 0;">Прокси сервисы</h2>
<button class="action-btn" id="addProxyBtn">
<i class="fas fa-plus"></i>
<span>Добавить прокси</span>
</button>
</div>
<div class="table-container">
<table class="data-table" id="proxyTable">
<thead>
<tr>
<th>Внешний домен</th>
<th>Локальный адрес</th>
<th>HTTPS</th>
<th>Auto HTTPS</th>
<th>Статус</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
<tr>
<td><code class="clickable-link" onclick="openTestLink('https://git.example.ru')">git.example.ru <i class="fas fa-external-link-alt"></i></code></td>
<td><code>127.0.0.1:3333</code></td>
<td><span class="badge badge-no">HTTP</span></td>
<td><span class="badge badge-yes">Да</span></td>
<td><span class="badge badge-online">active</span></td>
<td>
<button class="icon-btn" onclick="editVAccess('git.example.ru', true)" title="vAccess"><i class="fas fa-shield-alt"></i></button>
<button class="icon-btn" onclick="editTestProxy(0)" title="Редактировать"><i class="fas fa-edit"></i></button>
</td>
</tr>
<tr>
<td><code class="clickable-link" onclick="openTestLink('http://api.example.com')">api.example.com <i class="fas fa-external-link-alt"></i></code></td>
<td><code>127.0.0.1:8080</code></td>
<td><span class="badge badge-yes">HTTPS</span></td>
<td><span class="badge badge-no">Нет</span></td>
<td><span class="badge badge-online">active</span></td>
<td>
<button class="icon-btn" onclick="editVAccess('api.example.com', true)" title="vAccess"><i class="fas fa-shield-alt"></i></button>
<button class="icon-btn" onclick="editTestProxy(1)" title="Редактировать"><i class="fas fa-edit"></i></button>
</td>
</tr>
<tr>
<td><code class="clickable-link" onclick="openTestLink('http://test.example.net')">test.example.net <i class="fas fa-external-link-alt"></i></code></td>
<td><code>127.0.0.1:5000</code></td>
<td><span class="badge badge-no">HTTP</span></td>
<td><span class="badge badge-no">Нет</span></td>
<td><span class="badge badge-offline">disabled</span></td>
<td>
<button class="icon-btn" onclick="editVAccess('test.example.net', true)" title="vAccess"><i class="fas fa-shield-alt"></i></button>
<button class="icon-btn" onclick="editTestProxy(2)" title="Редактировать"><i class="fas fa-edit"></i></button>
</td>
</tr>
</tbody>
</table>
</div>
</section>
<section class="section" id="sectionSettings" style="display: none;">
<div class="settings-header">
<h2 class="section-title">Настройки серверов</h2>
<button class="action-btn save-btn" id="saveSettingsBtn">
<i class="fas fa-save"></i>
<span>Сохранить и перезапустить</span>
</button>
</div>
<div class="settings-grid">
<div class="settings-card">
<h3 class="settings-card-title"><i class="fas fa-database"></i> MySQL сервер</h3>
<div class="settings-form">
<div class="form-group">
<label class="form-label">Host адрес:</label>
<input type="text" class="form-input" id="mysqlHost" placeholder="127.0.0.1">
</div>
<div class="form-group">
<label class="form-label">Порт:</label>
<input type="number" class="form-input" id="mysqlPort" placeholder="3306">
</div>
</div>
</div>
<div class="settings-card">
<h3 class="settings-card-title"><i class="fab fa-php"></i> PHP сервер</h3>
<div class="settings-form">
<div class="form-group">
<label class="form-label">Host адрес:</label>
<input type="text" class="form-input" id="phpHost" placeholder="localhost">
</div>
<div class="form-group">
<label class="form-label">Порт:</label>
<input type="number" class="form-input" id="phpPort" placeholder="8000">
</div>
</div>
</div>
<div class="settings-card">
<h3 class="settings-card-title"><i class="fas fa-network-wired"></i> Proxy Manager</h3>
<div class="settings-form">
<div class="form-group">
<label class="form-label">Статус:</label>
<div class="toggle-wrapper">
<label class="toggle-switch">
<input type="checkbox" id="proxyEnabled">
<span class="toggle-slider"></span>
</label>
<span class="toggle-label">Proxy Manager</span>
</div>
</div>
<div class="form-info">
⚡ Применяется моментально без перезапуска серверов. При выключении все прокси правила будут отключены.
</div>
</div>
</div>
<div class="settings-card">
<h3 class="settings-card-title"><i class="fas fa-certificate"></i> Cert Manager</h3>
<div class="settings-form">
<div class="form-group">
<label class="form-label">Статус:</label>
<div class="toggle-wrapper">
<label class="toggle-switch">
<input type="checkbox" id="acmeEnabled">
<span class="toggle-slider"></span>
</label>
<span class="toggle-label">Cert Manager</span>
</div>
</div>
<div class="form-info">
🔐 Автоматическое получение SSL сертификатов от Let's Encrypt для доменов с включённым "Авто SSL".
</div>
</div>
</div>
</div>
</section>
<section class="section" id="sectionVAccessEditor" style="display: none;">
<div class="vaccess-page">
<!-- Хлебные крошки с вкладками -->
<div class="breadcrumbs" id="vAccessBreadcrumbs">
<div class="breadcrumbs-left">
<button class="breadcrumb-item" onclick="backToMain()">
<i class="fas fa-arrow-left"></i> Назад
</button>
<span class="breadcrumb-separator">/</span>
<span class="breadcrumb-item active" id="breadcrumbHost">example.com</span>
</div>
<div class="breadcrumbs-tabs">
<button class="vaccess-tab active" data-tab="rules" onclick="switchVAccessTab('rules')">
<i class="fas fa-list"></i>
<span>Правила доступа</span>
</button>
<button class="vaccess-tab" data-tab="help" onclick="switchVAccessTab('help')">
<i class="fas fa-question-circle"></i>
<span>Инструкция</span>
</button>
</div>
</div>
<!-- Заголовок с кнопками -->
<div class="vaccess-header">
<div class="vaccess-title-block">
<h2 class="vaccess-title">
<i class="fas fa-shield-alt"></i>
<span>Правила доступа vAccess</span>
</h2>
<p class="vaccess-subtitle" id="vAccessSubtitle">Управление правилами доступа для сайта</p>
</div>
<div class="vaccess-actions">
<button class="action-btn save-btn" id="saveVAccessBtn">
<i class="fas fa-save"></i>
<span>Сохранить изменения</span>
</button>
<button class="action-btn" onclick="addVAccessRule()">
<i class="fas fa-plus"></i>
<span>Добавить правило</span>
</button>
</div>
</div>
<!-- Контент вкладки "Правила" -->
<div class="vaccess-tab-content" id="vAccessRulesTab">
<div class="vaccess-rules-container">
<table class="vaccess-table">
<thead>
<tr>
<th class="col-drag"></th>
<th class="col-type">Тип</th>
<th class="col-files">Расширения</th>
<th class="col-paths">Пути доступа</th>
<th class="col-ips">IP адреса</th>
<th class="col-exceptions">Исключения</th>
<th class="col-error">Ошибка</th>
<th class="col-actions"></th>
</tr>
</thead>
<tbody id="vAccessTableBody">
<!-- Правила будут добавлены динамически -->
</tbody>
</table>
<div class="vaccess-empty" id="vAccessEmpty" style="display: none;">
<div class="empty-icon">
<i class="fas fa-shield-alt"></i>
</div>
<h3>Нет правил доступа</h3>
<p>Добавьте первое правило, чтобы начать управление доступом</p>
<button class="action-btn" onclick="addVAccessRule()">
<i class="fas fa-plus"></i>
<span>Создать правило</span>
</button>
</div>
</div>
</div>
<!-- Контент вкладки "Инструкция" -->
<div class="vaccess-tab-content" id="vAccessHelpTab" style="display: none;">
<div class="vaccess-help">
<div class="help-card">
<h3><i class="fas fa-info-circle"></i> Принцип работы</h3>
<ul>
<li>Правила проверяются <strong>сверху вниз</strong> по порядку</li>
<li>Первое подходящее правило срабатывает и завершает проверку</li>
<li>Если ни одно правило не сработало - доступ <strong>разрешён</strong></li>
<li>Перетаскивайте правила за <i class="fas fa-grip-vertical"></i> чтобы изменить порядок</li>
</ul>
</div>
<div class="help-card">
<h3><i class="fas fa-sliders-h"></i> Параметры правил</h3>
<div class="help-params">
<div class="help-param">
<strong>type:</strong>
<p><span class="badge badge-yes">Allow</span> (разрешить) или <span class="badge badge-no">Disable</span> (запретить)</p>
</div>
<div class="help-param">
<strong>Расширения файлов:</strong>
<p>Список расширений через запятую (<code>*.php</code>, <code>*.exe</code>)</p>
</div>
<div class="help-param">
<strong>Пути доступа:</strong>
<p>Список путей через запятую (<code>/admin/*</code>, <code>/api/*</code>)</p>
</div>
<div class="help-param">
<strong>IP адреса:</strong>
<p>Список IP адресов через запятую (<code>192.168.1.1</code>, <code>10.0.0.5</code>)</p>
<p class="help-warning"><i class="fas fa-exclamation-triangle"></i> Используется реальный IP соединения (не заголовки прокси!)</p>
</div>
<div class="help-param">
<strong>Исключения:</strong>
<p>Пути-исключения через запятую (<code>/bot/*</code>, <code>/public/*</code>). Правило НЕ применяется к этим путям</p>
</div>
<div class="help-param">
<strong>Страница ошибки:</strong>
<p>Куда перенаправить при блокировке:</p>
<ul>
<li><code>404</code> - стандартная страница ошибки</li>
<li><code>https://site.com</code> - внешний редирект</li>
<li><code>/error.html</code> - локальная страница</li>
</ul>
</div>
</div>
</div>
<div class="help-card">
<h3><i class="fas fa-search"></i> Паттерны</h3>
<div class="help-patterns">
<div class="pattern-item">
<code>*.ext</code>
<span>Любой файл с расширением .ext</span>
</div>
<div class="pattern-item">
<code>no_extension</code>
<span>Файлы без расширения (например: /api/users, /admin)</span>
</div>
<div class="pattern-item">
<code>/path/*</code>
<span>Все файлы в папке /path/ и подпапках</span>
</div>
<div class="pattern-item">
<code>/file.php</code>
<span>Конкретный файл</span>
</div>
</div>
</div>
<div class="help-card help-examples">
<h3><i class="fas fa-lightbulb"></i> Примеры правил</h3>
<div class="help-example">
<h4>1. Запретить выполнение PHP в uploads</h4>
<div class="example-rule">
<div><strong>Тип:</strong> <span class="badge badge-no">Disable</span></div>
<div><strong>Расширения:</strong> <code>*.php</code></div>
<div><strong>Пути:</strong> <code>/uploads/*</code></div>
<div><strong>Ошибка:</strong> <code>404</code></div>
</div>
</div>
<div class="help-example">
<h4>2. Разрешить админку только с определённых IP</h4>
<div class="example-rule">
<div><strong>Тип:</strong> <span class="badge badge-yes">Allow</span></div>
<div><strong>Пути:</strong> <code>/admin/*</code></div>
<div><strong>IP:</strong> <code>192.168.1.100, 127.0.0.1</code></div>
<div><strong>Ошибка:</strong> <code>404</code></div>
</div>
</div>
<div class="help-example">
<h4>3. Блокировать определённые IP для всего сайта</h4>
<div class="example-rule">
<div><strong>Тип:</strong> <span class="badge badge-no">Disable</span></div>
<div><strong>IP:</strong> <code>192.168.1.50, 10.0.0.99</code></div>
<div><strong>Ошибка:</strong> <code>https://google.com</code></div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<section class="section" id="sectionAddSite" style="display: none;">
<div class="vaccess-page">
<!-- Хлебные крошки -->
<div class="breadcrumbs">
<div class="breadcrumbs-left">
<button class="breadcrumb-item" onclick="backToMainFromAddSite()">
<i class="fas fa-arrow-left"></i> Назад
</button>
<span class="breadcrumb-separator">/</span>
<span class="breadcrumb-item active">Добавить новый сайт</span>
</div>
</div>
<!-- Заголовок -->
<div class="vaccess-header">
<div class="vaccess-title-block">
<h2 class="vaccess-title">
<i class="fas fa-plus-circle"></i>
<span>Создание нового сайта</span>
</h2>
<p class="vaccess-subtitle">Заполните информацию о сайте и при необходимости загрузите SSL сертификаты</p>
</div>
<div class="vaccess-actions">
<button class="action-btn save-btn" id="createSiteBtn">
<i class="fas fa-check"></i>
<span>Создать сайт</span>
</button>
</div>
</div>
<!-- Форма создания сайта -->
<div class="vaccess-tab-content">
<div class="site-creator-form">
<!-- Единая форма -->
<div class="form-section">
<div class="settings-form">
<!-- Основная информация -->
<h3 class="form-subsection-title"><i class="fas fa-info-circle"></i> Основная информация</h3>
<div class="form-group">
<label class="form-label">Название сайта: <span style="color: #e74c3c;">*</span></label>
<input type="text" class="form-input" id="newSiteName" placeholder="Мой новый сайт">
</div>
<div class="form-group">
<label class="form-label">Host (домен): <span style="color: #e74c3c;">*</span></label>
<input type="text" class="form-input" id="newSiteHost" placeholder="example.com">
<small style="color: #95a5a6; display: block; margin-top: 5px;">
<i class="fas fa-info-circle"></i> Введите домен без протокола (например: example.com или 192.168.1.100)
</small>
</div>
<div class="form-group">
<label class="form-label">Alias (псевдонимы):</label>
<input type="text" class="form-input" id="newSiteAliasInput" placeholder="*.example.com, www.example.com, alias.com">
<small style="color: #95a5a6; display: block; margin-top: 5px;">
<i class="fas fa-info-circle"></i> Введите псевдонимы через запятую
</small>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Root файл: <span style="color: #e74c3c;">*</span></label>
<select class="form-input" id="newSiteRootFile">
<option value="index.html">index.html</option>
<option value="index.php">index.php</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Статус:</label>
<select class="form-input" id="newSiteStatus">
<option value="active">Active (Активен)</option>
<option value="inactive">Inactive (Отключен)</option>
</select>
</div>
</div>
<div class="form-group">
<label class="form-label">Root file routing:</label>
<div class="toggle-wrapper">
<label class="toggle-switch">
<input type="checkbox" id="newSiteRouting" checked>
<span class="toggle-slider"></span>
</label>
<span class="toggle-label">Включён</span>
</div>
<small style="color: #95a5a6; display: block; margin-top: 5px;">
<i class="fas fa-info-circle"></i> Если включено, все запросы к несуществующим файлам будут перенаправляться на root файл
</small>
</div>
<!-- SSL Сертификаты -->
<h3 class="form-subsection-title"><i class="fas fa-lock"></i> SSL Сертификаты (опционально)</h3>
<div class="form-group">
<label class="form-label">Режим сертификата:</label>
<select class="form-input" id="certMode" onchange="toggleCertUpload()">
<option value="none">Без сертификата (fallback)</option>
<option value="auto">Автоматическое создание сертификата</option>
<option value="upload">Загрузить файлы сертификата</option>
</select>
</div>
<div id="certUploadBlock" style="display: none;">
<div class="form-group">
<label class="form-label">Certificate (*.crt): <span style="color: #e74c3c;">*</span></label>
<div class="file-upload-wrapper">
<input type="file" class="file-input" id="certFile" accept=".crt,.pem" onchange="handleCertFileSelect(this, 'certificate')">
<label for="certFile" class="file-upload-btn">
<i class="fas fa-file-upload"></i>
<span id="certFileName">Выберите файл...</span>
</label>
</div>
<div id="certFileStatus" style="margin-top: 8px;"></div>
</div>
<div class="form-group">
<label class="form-label">Private Key (*.key): <span style="color: #e74c3c;">*</span></label>
<div class="file-upload-wrapper">
<input type="file" class="file-input" id="keyFile" accept=".key,.pem" onchange="handleCertFileSelect(this, 'privatekey')">
<label for="keyFile" class="file-upload-btn">
<i class="fas fa-file-upload"></i>
<span id="keyFileName">Выберите файл...</span>
</label>
</div>
<div id="keyFileStatus" style="margin-top: 8px;"></div>
</div>
<div class="form-group">
<label class="form-label">CA Bundle (*.crt):</label>
<div class="file-upload-wrapper">
<input type="file" class="file-input" id="caFile" accept=".crt,.pem" onchange="handleCertFileSelect(this, 'cabundle')">
<label for="caFile" class="file-upload-btn">
<i class="fas fa-file-upload"></i>
<span id="caFileName">Выберите файл...</span>
</label>
</div>
<div id="caFileStatus" style="margin-top: 8px;"></div>
<small style="color: #95a5a6; display: block; margin-top: 5px;">
<i class="fas fa-info-circle"></i> CA Bundle опционален, но рекомендуется для полной цепочки сертификации
</small>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<section class="section" id="sectionAddProxy" style="display: none;">
<div class="vaccess-page">
<!-- Хлебные крошки -->
<div class="breadcrumbs">
<div class="breadcrumbs-left">
<button class="breadcrumb-item" onclick="backToMainFromAddProxy()">
<i class="fas fa-arrow-left"></i> Назад
</button>
<span class="breadcrumb-separator">/</span>
<span class="breadcrumb-item active">Добавить прокси сервис</span>
</div>
</div>
<!-- Заголовок -->
<div class="vaccess-header">
<div class="vaccess-title-block">
<h2 class="vaccess-title">
<i class="fas fa-plus-circle"></i>
<span>Создание прокси сервиса</span>
</h2>
<p class="vaccess-subtitle">Настройте проксирование внешнего домена на локальный сервис</p>
</div>
<div class="vaccess-actions">
<button class="action-btn save-btn" id="createProxyBtn">
<i class="fas fa-check"></i>
<span>Создать прокси</span>
</button>
</div>
</div>
<!-- Форма создания прокси -->
<div class="vaccess-tab-content">
<div class="site-creator-form">
<!-- Единая форма -->
<div class="form-section">
<div class="settings-form">
<!-- Основная информация -->
<h3 class="form-subsection-title"><i class="fas fa-info-circle"></i> Основная информация</h3>
<div class="form-group">
<label class="form-label">Внешний домен: <span style="color: #e74c3c;">*</span></label>
<input type="text" class="form-input" id="newProxyDomain" placeholder="example.com">
<small style="color: #95a5a6; display: block; margin-top: 5px;">
<i class="fas fa-info-circle"></i> Домен, на который будут приходить запросы (например: git.example.ru)
</small>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Локальный адрес: <span style="color: #e74c3c;">*</span></label>
<input type="text" class="form-input" id="newProxyLocalAddr" placeholder="127.0.0.1" value="127.0.0.1">
</div>
<div class="form-group">
<label class="form-label">Локальный порт: <span style="color: #e74c3c;">*</span></label>
<input type="text" class="form-input" id="newProxyLocalPort" placeholder="3000">
</div>
</div>
<div class="form-group">
<label class="form-label">Статус:</label>
<select class="form-input" id="newProxyStatus">
<option value="enable">Включён</option>
<option value="disable">Отключён</option>
</select>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">HTTPS к сервису:</label>
<div class="toggle-wrapper">
<label class="toggle-switch">
<input type="checkbox" id="newProxyServiceHTTPS">
<span class="toggle-slider"></span>
</label>
<span class="toggle-label">Включён</span>
</div>
<small style="color: #95a5a6; display: block; margin-top: 5px;">
<i class="fas fa-info-circle"></i> Использовать HTTPS при подключении к локальному сервису
</small>
</div>
<div class="form-group">
<label class="form-label">Авто HTTPS:</label>
<div class="toggle-wrapper">
<label class="toggle-switch">
<input type="checkbox" id="newProxyAutoHTTPS" checked>
<span class="toggle-slider"></span>
</label>
<span class="toggle-label">Включён</span>
</div>
<small style="color: #95a5a6; display: block; margin-top: 5px;">
<i class="fas fa-info-circle"></i> Автоматически перенаправлять HTTP запросы на HTTPS
</small>
</div>
</div>
<!-- SSL Сертификаты -->
<h3 class="form-subsection-title"><i class="fas fa-lock"></i> SSL Сертификаты (опционально)</h3>
<div class="form-group">
<label class="form-label">Режим сертификата:</label>
<select class="form-input" id="proxyCertMode" onchange="toggleProxyCertUpload()">
<option value="none">Без сертификата (fallback)</option>
<option value="auto">Автоматическое создание сертификата</option>
<option value="upload">Загрузить файлы сертификата</option>
</select>
</div>
<div id="proxyCertUploadBlock" style="display: none;">
<div class="form-group">
<label class="form-label">Certificate (*.crt): <span style="color: #e74c3c;">*</span></label>
<div class="file-upload-wrapper">
<input type="file" class="file-input" id="proxyCertFile" accept=".crt,.pem" onchange="handleProxyCertFileSelect(this, 'certificate')">
<label for="proxyCertFile" class="file-upload-btn">
<i class="fas fa-file-upload"></i>
<span id="proxyCertFileName">Выберите файл...</span>
</label>
</div>
<div id="proxyCertFileStatus" style="margin-top: 8px;"></div>
</div>
<div class="form-group">
<label class="form-label">Private Key (*.key): <span style="color: #e74c3c;">*</span></label>
<div class="file-upload-wrapper">
<input type="file" class="file-input" id="proxyKeyFile" accept=".key,.pem" onchange="handleProxyCertFileSelect(this, 'privatekey')">
<label for="proxyKeyFile" class="file-upload-btn">
<i class="fas fa-file-upload"></i>
<span id="proxyKeyFileName">Выберите файл...</span>
</label>
</div>
<div id="proxyKeyFileStatus" style="margin-top: 8px;"></div>
</div>
<div class="form-group">
<label class="form-label">CA Bundle (*.crt):</label>
<div class="file-upload-wrapper">
<input type="file" class="file-input" id="proxyCaFile" accept=".crt,.pem" onchange="handleProxyCertFileSelect(this, 'cabundle')">
<label for="proxyCaFile" class="file-upload-btn">
<i class="fas fa-file-upload"></i>
<span id="proxyCaFileName">Выберите файл...</span>
</label>
</div>
<div id="proxyCaFileStatus" style="margin-top: 8px;"></div>
<small style="color: #95a5a6; display: block; margin-top: 5px;">
<i class="fas fa-info-circle"></i> CA Bundle опционален, но рекомендуется для полной цепочки сертификации
</small>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<section class="section" id="sectionCertManager" style="display: none;">
<div class="vaccess-page">
<!-- Хлебные крошки -->
<div class="breadcrumbs">
<div class="breadcrumbs-left">
<button class="breadcrumb-item" onclick="backFromCertManager()">
<i class="fas fa-arrow-left"></i> Назад
</button>
<span class="breadcrumb-separator">/</span>
<span class="breadcrumb-item active" id="certManagerBreadcrumb">Сертификаты</span>
</div>
</div>
<!-- Заголовок -->
<div class="vaccess-header">
<div class="vaccess-title-block">
<h2 class="vaccess-title" id="certManagerTitle">
<i class="fas fa-shield-alt"></i>
<span>Управление сертификатами</span>
</h2>
<p class="vaccess-subtitle" id="certManagerSubtitle">Просмотр и управление SSL сертификатами для домена</p>
</div>
</div>
<!-- Контент -->
<div class="vaccess-tab-content">
<div class="cert-manager-content" id="certManagerContent">
<!-- Сертификаты будут загружены сюда -->
</div>
</div>
</div>
</section>
</main>
<footer class="footer">
<p>vServer Admin Panel © 2025 | Автор: Суманеев Роман</p>
</footer>
</div>
<div id="notification" class="notification"></div>
<div id="appLoader" class="app-loader">
<div class="loader-content">
<div class="loader-icon">🚀</div>
<div class="loader-text">Запуск vServer...</div>
<div class="loader-spinner"></div>
</div>
</div>
<div id="modalOverlay" class="modal-overlay">
<div class="modal-window">
<div class="modal-header">
<h3 class="modal-title" id="modalTitle">Редактирование</h3>
<button class="modal-close-btn" id="modalCloseBtn">
<i class="fas fa-times"></i>
</button>
</div>
<div class="modal-content" id="modalContent"></div>
<div class="modal-footer">
<button class="action-btn" id="modalCancelBtn">
<i class="fas fa-times"></i>
<span>Отмена</span>
</button>
<button class="action-btn save-btn" id="modalSaveBtn">
<i class="fas fa-save"></i>
<span>Сохранить</span>
</button>
</div>
</div>
</div>
<div id="fieldEditorOverlay" class="modal-overlay">
<div class="modal-window" style="max-width: 600px;">
<div class="modal-header">
<h3 class="modal-title" id="fieldEditorTitle">Редактирование поля</h3>
<button class="modal-close-btn" onclick="closeFieldEditor()">
<i class="fas fa-times"></i>
</button>
</div>
<div class="modal-content" id="fieldEditorContent"></div>
</div>
</div>
<div id="templates-container"></div>
<script src="wailsjs/runtime/runtime.js"></script>
<script src="wailsjs/go/admin/App.js"></script>
<script type="module" src="assets/js/main.js"></script>
</body>
</html>

View File

@@ -1,122 +0,0 @@
<!-- Шаблон редактирования сайта -->
<template id="edit-site-template">
<div class="settings-form">
<div class="form-group">
<label class="form-label">Статус сайта:</label>
<div class="status-toggle">
<button class="status-btn" data-status="active" data-value="active">
<i class="fas fa-check-circle"></i> Active
</button>
<button class="status-btn" data-status="inactive" data-value="inactive">
<i class="fas fa-times-circle"></i> Inactive
</button>
</div>
</div>
<div class="form-group">
<label class="form-label">Название сайта:</label>
<input type="text" class="form-input" id="editName">
</div>
<div class="form-group">
<label class="form-label">Host:</label>
<input type="text" class="form-input" id="editHost">
</div>
<div class="form-group">
<label class="form-label">Alias:</label>
<div class="tag-input-wrapper">
<input type="text" class="form-input" id="editAliasInput" placeholder="Введите alias и нажмите Добавить...">
<button class="action-btn" onclick="addAliasTag()"><i class="fas fa-plus"></i> Добавить</button>
</div>
<div class="tags-container" id="aliasTagsContainer"></div>
</div>
<div class="form-group">
<label class="form-label">Root файл:</label>
<input type="text" class="form-input" id="editRootFile">
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Роутинг:</label>
<div class="toggle-wrapper">
<label class="toggle-switch">
<input type="checkbox" id="editRouting">
<span class="toggle-slider"></span>
</label>
<span class="toggle-label">Включён</span>
</div>
</div>
<div class="form-group">
<label class="form-label">Авто SSL:</label>
<div class="toggle-wrapper">
<label class="toggle-switch">
<input type="checkbox" id="editAutoCreateSSL">
<span class="toggle-slider"></span>
</label>
<span class="toggle-label">Включён</span>
</div>
</div>
</div>
</div>
</template>
<!-- Шаблон редактирования прокси -->
<template id="edit-proxy-template">
<div class="settings-form">
<div class="form-group">
<label class="form-label">Статус прокси:</label>
<div class="status-toggle">
<button class="status-btn" data-status="enable" data-value="enable">
<i class="fas fa-check-circle"></i> Включён
</button>
<button class="status-btn" data-status="disable" data-value="disable">
<i class="fas fa-times-circle"></i> Отключён
</button>
</div>
</div>
<div class="form-group">
<label class="form-label">Внешний домен:</label>
<input type="text" class="form-input" id="editDomain">
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Локальный адрес:</label>
<input type="text" class="form-input" id="editLocalAddr">
</div>
<div class="form-group">
<label class="form-label">Локальный порт:</label>
<input type="text" class="form-input" id="editLocalPort">
</div>
</div>
<div class="form-row form-row-3">
<div class="form-group">
<label class="form-label">HTTPS к сервису:</label>
<div class="toggle-wrapper">
<label class="toggle-switch">
<input type="checkbox" id="editServiceHTTPS">
<span class="toggle-slider"></span>
</label>
<span class="toggle-label">Включён</span>
</div>
</div>
<div class="form-group">
<label class="form-label">Авто HTTPS:</label>
<div class="toggle-wrapper">
<label class="toggle-switch">
<input type="checkbox" id="editAutoHTTPS">
<span class="toggle-slider"></span>
</label>
<span class="toggle-label">Включён</span>
</div>
</div>
<div class="form-group">
<label class="form-label">Авто SSL:</label>
<div class="toggle-wrapper">
<label class="toggle-switch">
<input type="checkbox" id="editProxyAutoCreateSSL">
<span class="toggle-slider"></span>
</label>
<span class="toggle-label">Включён</span>
</div>
</div>
</div>
</div>
</template>

View File

@@ -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("<!DOCTYPE html><html><head><meta charset=\"utf-8\"><title>Error</title></head><body><h1>Error</h1></body></html>"), 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"
}

View File

@@ -14,6 +14,7 @@ func GetProxyList() []ProxyInfo {
}
proxyInfo := ProxyInfo{
Name: proxyConfig.Name,
Enable: proxyConfig.Enable,
ExternalDomain: proxyConfig.ExternalDomain,
LocalAddress: proxyConfig.LocalAddress,

View File

@@ -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"`
}

View File

@@ -86,7 +86,7 @@ func getProxyStatus() ServiceStatus {
return ServiceStatus{
Name: "Proxy",
Status: status,
Port: "-",
Port: "",
Info: info,
}
}

View File

@@ -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"`