Большое обновление GUI интерфейс
Большое обновление GUI интерфейс - Добавлен фраемворr Walles - Удалена консольная версия - Проработан интерфейс и дизайн - Добавлено кеширование для быстрой реакции. - Сделан .ps1 сборщик для удобной сборки проекта. - Обновлён Readme
This commit is contained in:
@@ -4,7 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
config "vServer/Backend/config"
|
config "vServer/Backend/config"
|
||||||
tools "vServer/Backend/tools"
|
tools "vServer/Backend/tools"
|
||||||
@@ -14,6 +14,11 @@ var mysqlProcess *exec.Cmd
|
|||||||
var mysql_status bool = false
|
var mysql_status bool = false
|
||||||
var mysql_secure bool = false
|
var mysql_secure bool = false
|
||||||
|
|
||||||
|
// GetMySQLStatus возвращает статус MySQL
|
||||||
|
func GetMySQLStatus() bool {
|
||||||
|
return mysql_status
|
||||||
|
}
|
||||||
|
|
||||||
var mysqldPath string
|
var mysqldPath string
|
||||||
var configPath string
|
var configPath string
|
||||||
var dataDirAbs string
|
var dataDirAbs string
|
||||||
@@ -86,16 +91,8 @@ func StartMySQLServer(secure bool) {
|
|||||||
mysql_port = config.ConfigData.Soft_Settings.Mysql_port
|
mysql_port = config.ConfigData.Soft_Settings.Mysql_port
|
||||||
mysql_ip = config.ConfigData.Soft_Settings.Mysql_host
|
mysql_ip = config.ConfigData.Soft_Settings.Mysql_host
|
||||||
|
|
||||||
if tools.Port_check("MySQL", mysql_ip, strconv.Itoa(mysql_port)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if mysql_status {
|
if mysql_status {
|
||||||
tools.Logs_file(1, "MySQL", "Сервер MySQL уже запущен", "logs_mysql.log", true)
|
tools.Logs_file(1, "MySQL", "Сервер MySQL уже запущен", "logs_mysql.log", false)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if false {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,9 +102,9 @@ func StartMySQLServer(secure bool) {
|
|||||||
|
|
||||||
// Выбор сообщения
|
// Выбор сообщения
|
||||||
if secure {
|
if secure {
|
||||||
tools.Logs_file(0, "MySQL", "Запуск сервера MySQL в режиме безопасности", "logs_mysql.log", true)
|
tools.Logs_file(0, "MySQL", "Запуск сервера MySQL в режиме безопасности", "logs_mysql.log", false)
|
||||||
} else {
|
} else {
|
||||||
tools.Logs_file(0, "MySQL", "Запуск сервера MySQL в обычном режиме", "logs_mysql.log", true)
|
tools.Logs_file(0, "MySQL", "Запуск сервера MySQL в обычном режиме", "logs_mysql.log", false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Общая логика запуска
|
// Общая логика запуска
|
||||||
@@ -115,7 +112,7 @@ func StartMySQLServer(secure bool) {
|
|||||||
mysqlProcess.Dir = binDirAbs
|
mysqlProcess.Dir = binDirAbs
|
||||||
tools.Logs_console(mysqlProcess, console_mysql)
|
tools.Logs_console(mysqlProcess, console_mysql)
|
||||||
|
|
||||||
tools.Logs_file(0, "MySQL", fmt.Sprintf("Сервер MySQL запущен на %s:%d", mysql_ip, mysql_port), "logs_mysql.log", true)
|
tools.Logs_file(0, "MySQL", fmt.Sprintf("Сервер MySQL запущен на %s:%d", mysql_ip, mysql_port), "logs_mysql.log", false)
|
||||||
|
|
||||||
mysql_status = true
|
mysql_status = true
|
||||||
|
|
||||||
@@ -124,22 +121,30 @@ func StartMySQLServer(secure bool) {
|
|||||||
// StopMySQLServer останавливает MySQL сервер
|
// StopMySQLServer останавливает MySQL сервер
|
||||||
func StopMySQLServer() {
|
func StopMySQLServer() {
|
||||||
|
|
||||||
if mysql_status {
|
if !mysql_status {
|
||||||
|
return // Уже остановлен
|
||||||
cmd := exec.Command("taskkill", "/F", "/IM", "mysqld.exe")
|
|
||||||
|
|
||||||
err := cmd.Run()
|
|
||||||
tools.CheckError(err)
|
|
||||||
|
|
||||||
tools.Logs_file(0, "MySQL", "Сервер MySQL остановлен", "logs_mysql.log", true)
|
|
||||||
mysql_status = false
|
|
||||||
|
|
||||||
} else {
|
|
||||||
|
|
||||||
tools.Logs_file(1, "MySQL", "Сервер MySQL уже остановлен", "logs_mysql.log", true)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Сначала пробуем завершить процесс корректно
|
||||||
|
if mysqlProcess != nil && mysqlProcess.Process != nil {
|
||||||
|
mysqlProcess.Process.Kill()
|
||||||
|
mysqlProcess = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Дополнительно убиваем все mysqld.exe процессы
|
||||||
|
cmd := exec.Command("taskkill", "/F", "/IM", "mysqld.exe")
|
||||||
|
|
||||||
|
// Скрываем окно taskkill
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||||
|
HideWindow: true,
|
||||||
|
CreationFlags: 0x08000000,
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Run()
|
||||||
|
|
||||||
|
tools.Logs_file(0, "MySQL", "Сервер MySQL остановлен", "logs_mysql.log", false)
|
||||||
|
mysql_status = false
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ResetPasswordMySQL() {
|
func ResetPasswordMySQL() {
|
||||||
|
|||||||
@@ -1,195 +0,0 @@
|
|||||||
package webserver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
admin "vServer/Backend/admin"
|
|
||||||
config "vServer/Backend/config"
|
|
||||||
tools "vServer/Backend/tools"
|
|
||||||
)
|
|
||||||
|
|
||||||
var Secure_post bool = false
|
|
||||||
|
|
||||||
func CommandListener() {
|
|
||||||
|
|
||||||
fmt.Println("Введите help для получения списка команд")
|
|
||||||
fmt.Println("")
|
|
||||||
|
|
||||||
for {
|
|
||||||
var cmd string
|
|
||||||
|
|
||||||
fmt.Print(tools.Color(" > ", tools.Оранжевый))
|
|
||||||
|
|
||||||
fmt.Scanln(&cmd)
|
|
||||||
|
|
||||||
switch cmd {
|
|
||||||
case "help":
|
|
||||||
|
|
||||||
fmt.Println(" ------------------------------------------")
|
|
||||||
fmt.Println(" 1: mysql_stop - Остановить MySQL")
|
|
||||||
fmt.Println(" 2: mysql_start - Запустить MySQL")
|
|
||||||
fmt.Println(" 3: mysql_pass - Сбросить пароль MySQL")
|
|
||||||
fmt.Println(" 4: clear - Очистить консоль")
|
|
||||||
fmt.Println(" 5: cert_reload - Перезагрузить SSL сертификаты")
|
|
||||||
fmt.Println(" 6: admin_toggle - Переключить режим админки (embed/файловая система)")
|
|
||||||
fmt.Println(" 7: config_reload - Перезагрузить конфигурацию")
|
|
||||||
fmt.Println(" 8: restart - Перезапустить сервер")
|
|
||||||
fmt.Println(" 9: php_console - Открыть PHP консоль")
|
|
||||||
fmt.Println(" 10: exit - выйти из программы")
|
|
||||||
fmt.Println(" ------------------------------------------")
|
|
||||||
fmt.Println("")
|
|
||||||
|
|
||||||
case "mysql_stop":
|
|
||||||
StopMySQLServer()
|
|
||||||
|
|
||||||
case "mysql_start":
|
|
||||||
StartMySQLServer(false)
|
|
||||||
|
|
||||||
case "mysql_pass":
|
|
||||||
ResetPasswordMySQL()
|
|
||||||
|
|
||||||
case "clear":
|
|
||||||
ClearConsole()
|
|
||||||
|
|
||||||
case "cert_reload":
|
|
||||||
ReloadCertificates()
|
|
||||||
|
|
||||||
case "admin_toggle":
|
|
||||||
AdminToggle()
|
|
||||||
|
|
||||||
case "config_reload":
|
|
||||||
ConfigReload()
|
|
||||||
|
|
||||||
case "restart":
|
|
||||||
RestartServer()
|
|
||||||
|
|
||||||
case "time_run":
|
|
||||||
fmt.Println(tools.ServerUptime("get"))
|
|
||||||
|
|
||||||
case "secure_post":
|
|
||||||
|
|
||||||
if Secure_post {
|
|
||||||
Secure_post = false
|
|
||||||
fmt.Println("Secure post is disabled")
|
|
||||||
} else {
|
|
||||||
Secure_post = true
|
|
||||||
fmt.Println("Secure post is enabled")
|
|
||||||
}
|
|
||||||
|
|
||||||
case "exit":
|
|
||||||
fmt.Println("Завершение...")
|
|
||||||
os.Exit(0)
|
|
||||||
|
|
||||||
default:
|
|
||||||
|
|
||||||
fmt.Println(" Неизвестная команда. Введите 'help' для получения списка команд")
|
|
||||||
fmt.Println("")
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func RestartServer() {
|
|
||||||
fmt.Println("")
|
|
||||||
fmt.Println("⏹️ Перезагрузка сервера...")
|
|
||||||
|
|
||||||
// Останавливаем все сервисы
|
|
||||||
fmt.Println("⏹️ Останавливаем сервисы...")
|
|
||||||
fmt.Println("")
|
|
||||||
|
|
||||||
// Останавливаем HTTP/HTTPS серверы
|
|
||||||
StopHTTPServer()
|
|
||||||
StopHTTPSServer()
|
|
||||||
|
|
||||||
// Останавливаем MySQL
|
|
||||||
if mysql_status {
|
|
||||||
StopMySQLServer()
|
|
||||||
time.Sleep(1 * time.Second)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Останавливаем PHP
|
|
||||||
PHP_Stop()
|
|
||||||
time.Sleep(1 * time.Second)
|
|
||||||
|
|
||||||
fmt.Println("")
|
|
||||||
fmt.Println("✅ Все сервисы остановлены")
|
|
||||||
|
|
||||||
// Перезагружаем конфигурацию
|
|
||||||
fmt.Println("📋 Перезагружаем конфигурацию...")
|
|
||||||
fmt.Println("")
|
|
||||||
config.LoadConfig()
|
|
||||||
|
|
||||||
// Запускаем сервисы заново
|
|
||||||
fmt.Println("🚀 Запускаем сервисы...")
|
|
||||||
fmt.Println("")
|
|
||||||
|
|
||||||
// Запускаем HTTP/HTTPS серверы
|
|
||||||
go StartHTTP()
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
go StartHTTPS()
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
|
|
||||||
// Запускаем PHP
|
|
||||||
PHP_Start()
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
|
|
||||||
// Запускаем MySQL
|
|
||||||
StartMySQLServer(false)
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
|
|
||||||
fmt.Println("✅ Сервер успешно перезагружен!")
|
|
||||||
fmt.Println("")
|
|
||||||
}
|
|
||||||
|
|
||||||
func ClearConsole() {
|
|
||||||
// Очищаем консоль, но сохраняем первые три строки
|
|
||||||
fmt.Print("\033[H\033[2J") // ANSI escape code для очистки экрана
|
|
||||||
|
|
||||||
println("")
|
|
||||||
println(tools.Color("vServer", tools.Жёлтый) + tools.Color(" 1.0.0", tools.Голубой))
|
|
||||||
println(tools.Color("Автор: ", tools.Зелёный) + tools.Color("Суманеев Роман (c) 2025", tools.Голубой))
|
|
||||||
println(tools.Color("Официальный сайт: ", tools.Зелёный) + tools.Color("https://voxsel.ru", tools.Голубой))
|
|
||||||
|
|
||||||
println("")
|
|
||||||
|
|
||||||
// Восстанавливаем первые три строки
|
|
||||||
fmt.Println("Введите help для получения списка команд")
|
|
||||||
fmt.Println("")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Переключает режим админки между embed и файловой системой
|
|
||||||
func AdminToggle() {
|
|
||||||
fmt.Println("")
|
|
||||||
|
|
||||||
if admin.UseEmbedded {
|
|
||||||
// Переключаем на файловую систему
|
|
||||||
admin.UseEmbedded = false
|
|
||||||
fmt.Println("🔄 Режим изменен: Embedded → Файловая система")
|
|
||||||
fmt.Println("✅ Админка переключена на файловую систему")
|
|
||||||
fmt.Println("📁 Файлы будут загружаться с диска из Backend/admin/html/")
|
|
||||||
fmt.Println("💡 Теперь можно редактировать файлы и изменения будут видны сразу")
|
|
||||||
} else {
|
|
||||||
// Переключаем обратно на embedded
|
|
||||||
admin.UseEmbedded = true
|
|
||||||
fmt.Println("🔄 Режим изменен: Файловая система → Embedded")
|
|
||||||
fmt.Println("✅ Админка переключена на embedded режим")
|
|
||||||
fmt.Println("📦 Файлы загружаются из встроенных ресурсов")
|
|
||||||
fmt.Println("🚀 Быстрая загрузка, но изменения требуют перекомпиляции")
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Перезагружает конфигурацию без перезапуска сервисов
|
|
||||||
func ConfigReload() {
|
|
||||||
fmt.Println("")
|
|
||||||
fmt.Println("📋 Перезагружаем конфигурацию...")
|
|
||||||
|
|
||||||
// Загружаем новую конфигурацию
|
|
||||||
config.LoadConfig()
|
|
||||||
|
|
||||||
fmt.Println("✅ Конфигурация успешно перезагружена!")
|
|
||||||
fmt.Println("💡 Изменения применятся к новым запросам")
|
|
||||||
fmt.Println("")
|
|
||||||
}
|
|
||||||
@@ -4,12 +4,34 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"vServer/Backend/config"
|
"vServer/Backend/config"
|
||||||
tools "vServer/Backend/tools"
|
tools "vServer/Backend/tools"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
siteStatusCache map[string]bool
|
||||||
|
statusMutex sync.RWMutex
|
||||||
|
)
|
||||||
|
|
||||||
func StartHandler() {
|
func StartHandler() {
|
||||||
http.HandleFunc("/", handler)
|
http.HandleFunc("/", handler)
|
||||||
|
updateSiteStatusCache()
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateSiteStatusCache() {
|
||||||
|
statusMutex.Lock()
|
||||||
|
defer statusMutex.Unlock()
|
||||||
|
|
||||||
|
siteStatusCache = make(map[string]bool)
|
||||||
|
for _, site := range config.ConfigData.Site_www {
|
||||||
|
siteStatusCache[site.Host] = site.Status == "active"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateSiteStatusCache - экспортируемая функция для обновления кэша
|
||||||
|
func UpdateSiteStatusCache() {
|
||||||
|
updateSiteStatusCache()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверка wildcard паттерна для alias
|
// Проверка wildcard паттерна для alias
|
||||||
@@ -175,6 +197,17 @@ func checkVAccessAndHandle(w http.ResponseWriter, r *http.Request, filePath stri
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Проверяет включен ли сайт (оптимизировано через кэш)
|
||||||
|
func isSiteActive(host string) bool {
|
||||||
|
statusMutex.RLock()
|
||||||
|
defer statusMutex.RUnlock()
|
||||||
|
|
||||||
|
if status, exists := siteStatusCache[host]; exists {
|
||||||
|
return status
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// Обработчик запросов
|
// Обработчик запросов
|
||||||
func handler(w http.ResponseWriter, r *http.Request) {
|
func handler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
@@ -187,6 +220,13 @@ func handler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return // Если прокси обработал запрос, прерываем выполнение
|
return // Если прокси обработал запрос, прерываем выполнение
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Проверяем статус сайта
|
||||||
|
if !isSiteActive(host) {
|
||||||
|
http.ServeFile(w, r, "WebServer/tools/error_page/index.html")
|
||||||
|
tools.Logs_file(2, "H503", "🚫 Сайт отключен: "+host, "logs_http.log", false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// ЕДИНСТВЕННАЯ ПРОВЕРКА vAccess - простая проверка запрошенного пути
|
// ЕДИНСТВЕННАЯ ПРОВЕРКА vAccess - простая проверка запрошенного пути
|
||||||
if !checkVAccessAndHandle(w, r, r.URL.Path, host) {
|
if !checkVAccessAndHandle(w, r, r.URL.Path, host) {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ import (
|
|||||||
var httpServer *http.Server
|
var httpServer *http.Server
|
||||||
var port_http string = "80"
|
var port_http string = "80"
|
||||||
|
|
||||||
|
// GetHTTPStatus возвращает статус HTTP сервера
|
||||||
|
func GetHTTPStatus() bool {
|
||||||
|
return httpServer != nil
|
||||||
|
}
|
||||||
|
|
||||||
// Запуск HTTP сервера
|
// Запуск HTTP сервера
|
||||||
func StartHTTP() {
|
func StartHTTP() {
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ var fallbackCert *tls.Certificate
|
|||||||
var httpsServer *http.Server
|
var httpsServer *http.Server
|
||||||
var port_https string = "443"
|
var port_https string = "443"
|
||||||
|
|
||||||
|
// GetHTTPSStatus возвращает статус HTTPS сервера
|
||||||
|
func GetHTTPSStatus() bool {
|
||||||
|
return httpsServer != nil
|
||||||
|
}
|
||||||
|
|
||||||
// Запуск https сервера
|
// Запуск https сервера
|
||||||
func StartHTTPS() {
|
func StartHTTPS() {
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
config "vServer/Backend/config"
|
config "vServer/Backend/config"
|
||||||
tools "vServer/Backend/tools"
|
tools "vServer/Backend/tools"
|
||||||
@@ -30,6 +31,11 @@ var (
|
|||||||
var address_php string
|
var address_php string
|
||||||
var Сonsole_php bool = false
|
var Сonsole_php bool = false
|
||||||
|
|
||||||
|
// GetPHPStatus возвращает статус PHP сервера
|
||||||
|
func GetPHPStatus() bool {
|
||||||
|
return len(phpProcesses) > 0 && !stopping
|
||||||
|
}
|
||||||
|
|
||||||
// FastCGI константы
|
// FastCGI константы
|
||||||
const (
|
const (
|
||||||
FCGI_VERSION_1 = 1
|
FCGI_VERSION_1 = 1
|
||||||
@@ -99,6 +105,12 @@ func startFastCGIWorker(port int, workerID int) {
|
|||||||
"PHP_FCGI_MAX_REQUESTS=1000", // Перезапуск после 1000 запросов
|
"PHP_FCGI_MAX_REQUESTS=1000", // Перезапуск после 1000 запросов
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Скрываем консольное окно
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||||
|
HideWindow: true,
|
||||||
|
CreationFlags: 0x08000000, // CREATE_NO_WINDOW
|
||||||
|
}
|
||||||
|
|
||||||
if !Сonsole_php {
|
if !Сonsole_php {
|
||||||
cmd.Stdout = nil
|
cmd.Stdout = nil
|
||||||
cmd.Stderr = nil
|
cmd.Stderr = nil
|
||||||
@@ -499,6 +511,10 @@ func PHP_Stop() {
|
|||||||
|
|
||||||
// Дополнительно убиваем все процессы php-cgi.exe
|
// Дополнительно убиваем все процессы php-cgi.exe
|
||||||
cmd := exec.Command("taskkill", "/F", "/IM", "php-cgi.exe")
|
cmd := exec.Command("taskkill", "/F", "/IM", "php-cgi.exe")
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||||
|
HideWindow: true,
|
||||||
|
CreationFlags: 0x08000000,
|
||||||
|
}
|
||||||
cmd.Run()
|
cmd.Run()
|
||||||
|
|
||||||
tools.Logs_file(0, "PHP", "🛑 Все FastCGI процессы остановлены", "logs_php.log", true)
|
tools.Logs_file(0, "PHP", "🛑 Все FastCGI процессы остановлены", "logs_php.log", true)
|
||||||
|
|||||||
@@ -21,6 +21,11 @@ func StartHandlerProxy(w http.ResponseWriter, r *http.Request) (valid bool) {
|
|||||||
configMutex.RLock()
|
configMutex.RLock()
|
||||||
defer configMutex.RUnlock()
|
defer configMutex.RUnlock()
|
||||||
|
|
||||||
|
// Проверяем глобальный флаг прокси
|
||||||
|
if !config.ConfigData.Soft_Settings.Proxy_enabled {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// Проходим по всем прокси конфигурациям
|
// Проходим по всем прокси конфигурациям
|
||||||
for _, proxyConfig := range config.ConfigData.Proxy_Service {
|
for _, proxyConfig := range config.ConfigData.Proxy_Service {
|
||||||
// Пропускаем отключенные прокси
|
// Пропускаем отключенные прокси
|
||||||
|
|||||||
@@ -136,15 +136,21 @@ func findVAccessFiles(requestPath string, host string) []string {
|
|||||||
// Базовый путь к сайту (НЕ public_www, а уровень выше)
|
// Базовый путь к сайту (НЕ public_www, а уровень выше)
|
||||||
basePath := "WebServer/www/" + host
|
basePath := "WebServer/www/" + host
|
||||||
|
|
||||||
|
// Получаем абсолютный базовый путь
|
||||||
|
absBasePath, err := tools.AbsPath(basePath)
|
||||||
|
if err != nil {
|
||||||
|
return configFiles
|
||||||
|
}
|
||||||
|
|
||||||
// Проверяем корневой vAccess.conf
|
// Проверяем корневой vAccess.conf
|
||||||
rootConfigPath := filepath.Join(basePath, "vAccess.conf")
|
rootConfigPath := filepath.Join(absBasePath, "vAccess.conf")
|
||||||
if _, err := os.Stat(rootConfigPath); err == nil {
|
if _, err := os.Stat(rootConfigPath); err == nil {
|
||||||
configFiles = append(configFiles, rootConfigPath)
|
configFiles = append(configFiles, rootConfigPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Разбиваем путь на части для поиска вложенных конфигов
|
// Разбиваем путь на части для поиска вложенных конфигов
|
||||||
pathParts := strings.Split(strings.Trim(requestPath, "/"), "/")
|
pathParts := strings.Split(strings.Trim(requestPath, "/"), "/")
|
||||||
currentPath := basePath
|
currentPath := absBasePath
|
||||||
|
|
||||||
for _, part := range pathParts {
|
for _, part := range pathParts {
|
||||||
if part == "" {
|
if part == "" {
|
||||||
@@ -439,7 +445,8 @@ func HandleVAccessError(w http.ResponseWriter, r *http.Request, errorPage string
|
|||||||
switch {
|
switch {
|
||||||
case errorPage == "404":
|
case errorPage == "404":
|
||||||
// Стандартная 404 страница
|
// Стандартная 404 страница
|
||||||
http.ServeFile(w, r, "WebServer/tools/error_page/index.html")
|
errorPagePath, _ := tools.AbsPath("WebServer/tools/error_page/index.html")
|
||||||
|
http.ServeFile(w, r, errorPagePath)
|
||||||
|
|
||||||
case strings.HasPrefix(errorPage, "http://") || strings.HasPrefix(errorPage, "https://"):
|
case strings.HasPrefix(errorPage, "http://") || strings.HasPrefix(errorPage, "https://"):
|
||||||
// Внешний сайт - редирект
|
// Внешний сайт - редирект
|
||||||
@@ -448,11 +455,13 @@ func HandleVAccessError(w http.ResponseWriter, r *http.Request, errorPage string
|
|||||||
default:
|
default:
|
||||||
// Локальный путь от public_www
|
// Локальный путь от public_www
|
||||||
localPath := "WebServer/www/" + host + "/public_www" + errorPage
|
localPath := "WebServer/www/" + host + "/public_www" + errorPage
|
||||||
if _, err := os.Stat(localPath); err == nil {
|
absLocalPath, _ := tools.AbsPath(localPath)
|
||||||
http.ServeFile(w, r, localPath)
|
if _, err := os.Stat(absLocalPath); err == nil {
|
||||||
|
http.ServeFile(w, r, absLocalPath)
|
||||||
} else {
|
} else {
|
||||||
// Файл не найден - показываем стандартную 404
|
// Файл не найден - показываем стандартную 404
|
||||||
http.ServeFile(w, r, "WebServer/tools/error_page/index.html")
|
errorPagePath, _ := tools.AbsPath("WebServer/tools/error_page/index.html")
|
||||||
|
http.ServeFile(w, r, errorPagePath)
|
||||||
tools.Logs_file(1, "vAccess", "❌ Страница ошибки не найдена: "+localPath, "logs_vaccess.log", false)
|
tools.Logs_file(1, "vAccess", "❌ Страница ошибки не найдена: "+localPath, "logs_vaccess.log", false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -468,14 +477,21 @@ func CheckProxyVAccess(requestPath string, domain string, r *http.Request) (bool
|
|||||||
// Путь к конфигурационному файлу прокси
|
// Путь к конфигурационному файлу прокси
|
||||||
configPath := "WebServer/tools/Proxy_vAccess/" + domain + "_vAccess.conf"
|
configPath := "WebServer/tools/Proxy_vAccess/" + domain + "_vAccess.conf"
|
||||||
|
|
||||||
|
// Получаем абсолютный путь
|
||||||
|
absConfigPath, err := tools.AbsPath(configPath)
|
||||||
|
if err != nil {
|
||||||
|
// При ошибке получения пути - разрешаем доступ
|
||||||
|
return true, ""
|
||||||
|
}
|
||||||
|
|
||||||
// Проверяем существование файла
|
// Проверяем существование файла
|
||||||
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
if _, err := os.Stat(absConfigPath); os.IsNotExist(err) {
|
||||||
// Нет конфигурационного файла - разрешаем доступ
|
// Нет конфигурационного файла - разрешаем доступ
|
||||||
return true, ""
|
return true, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// Парсим конфигурационный файл
|
// Парсим конфигурационный файл
|
||||||
config, err := parseVAccessFile(configPath)
|
config, err := parseVAccessFile(absConfigPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tools.Logs_file(1, "vAccess-Proxy", "❌ Ошибка парсинга "+configPath+": "+err.Error(), "logs_vaccess_proxy.log", false)
|
tools.Logs_file(1, "vAccess-Proxy", "❌ Ошибка парсинга "+configPath+": "+err.Error(), "logs_vaccess_proxy.log", false)
|
||||||
return true, "" // При ошибке парсинга разрешаем доступ
|
return true, "" // При ошибке парсинга разрешаем доступ
|
||||||
@@ -491,7 +507,8 @@ func HandleProxyVAccessError(w http.ResponseWriter, r *http.Request, errorPage s
|
|||||||
case errorPage == "404":
|
case errorPage == "404":
|
||||||
// Стандартная 404 страница
|
// Стандартная 404 страница
|
||||||
w.WriteHeader(http.StatusForbidden)
|
w.WriteHeader(http.StatusForbidden)
|
||||||
http.ServeFile(w, r, "WebServer/tools/error_page/index.html")
|
errorPagePath, _ := tools.AbsPath("WebServer/tools/error_page/index.html")
|
||||||
|
http.ServeFile(w, r, errorPagePath)
|
||||||
|
|
||||||
case strings.HasPrefix(errorPage, "http://") || strings.HasPrefix(errorPage, "https://"):
|
case strings.HasPrefix(errorPage, "http://") || strings.HasPrefix(errorPage, "https://"):
|
||||||
// Внешний сайт - редирект
|
// Внешний сайт - редирект
|
||||||
@@ -500,6 +517,7 @@ func HandleProxyVAccessError(w http.ResponseWriter, r *http.Request, errorPage s
|
|||||||
default:
|
default:
|
||||||
// Для прокси возвращаем 403 Forbidden
|
// Для прокси возвращаем 403 Forbidden
|
||||||
w.WriteHeader(http.StatusForbidden)
|
w.WriteHeader(http.StatusForbidden)
|
||||||
http.ServeFile(w, r, "WebServer/tools/error_page/index.html")
|
errorPagePath, _ := tools.AbsPath("WebServer/tools/error_page/index.html")
|
||||||
|
http.ServeFile(w, r, errorPagePath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
package admin
|
|
||||||
|
|
||||||
import (
|
|
||||||
"embed"
|
|
||||||
"io/fs"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:embed html
|
|
||||||
var AdminHTML embed.FS
|
|
||||||
|
|
||||||
// Флаг для переключения между embed и файловой системой
|
|
||||||
var UseEmbedded bool = true
|
|
||||||
|
|
||||||
// Получает файловую систему в зависимости от флага
|
|
||||||
func GetFileSystem() fs.FS {
|
|
||||||
if UseEmbedded {
|
|
||||||
return AdminHTML
|
|
||||||
}
|
|
||||||
return os.DirFS("Backend/admin/")
|
|
||||||
}
|
|
||||||
103
Backend/admin/frontend/assets/css/base.css
Normal file
103
Backend/admin/frontend/assets/css/base.css
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
/* ============================================
|
||||||
|
Base Styles & Reset
|
||||||
|
Базовые стили приложения
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
background: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
line-height: 1.6;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
/* Градиентный фон с размытием */
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
top: -50%;
|
||||||
|
left: -50%;
|
||||||
|
width: 200%;
|
||||||
|
height: 200%;
|
||||||
|
/* background: radial-gradient(circle at 80% 80%, rgba(138, 92, 246, 0.116) 0%, transparent 50%); */
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Typography */
|
||||||
|
code {
|
||||||
|
background: rgba(139, 92, 246, 0.15);
|
||||||
|
backdrop-filter: var(--backdrop-blur-light);
|
||||||
|
color: var(--accent-purple-light);
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
border: 1px solid var(--glass-border);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar Styling */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: rgba(139, 92, 246, 0.05);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(139, 92, 246, 0.3);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(139, 92, 246, 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bounce {
|
||||||
|
0%, 100% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateY(-20px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Utility Classes */
|
||||||
|
.hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-in {
|
||||||
|
animation: fadeIn var(--transition-slow);
|
||||||
|
}
|
||||||
|
|
||||||
87
Backend/admin/frontend/assets/css/components/badges.css
Normal file
87
Backend/admin/frontend/assets/css/components/badges.css
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
/* ============================================
|
||||||
|
Badges Component
|
||||||
|
Единая система бейджей
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
/* Base Badge */
|
||||||
|
.badge {
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
font-weight: var(--font-bold);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
backdrop-filter: var(--backdrop-blur-light);
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status Badges */
|
||||||
|
.badge-online {
|
||||||
|
background: linear-gradient(135deg, rgba(16, 185, 129, 0.25), rgba(16, 185, 129, 0.15));
|
||||||
|
color: var(--accent-green);
|
||||||
|
border: 1px solid rgba(16, 185, 129, 0.4);
|
||||||
|
box-shadow: 0 0 12px rgba(16, 185, 129, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-offline {
|
||||||
|
background: linear-gradient(135deg, rgba(239, 68, 68, 0.25), rgba(239, 68, 68, 0.15));
|
||||||
|
color: var(--accent-red);
|
||||||
|
border: 1px solid rgba(239, 68, 68, 0.4);
|
||||||
|
box-shadow: 0 0 12px rgba(239, 68, 68, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-pending {
|
||||||
|
background: linear-gradient(135deg, rgba(245, 158, 11, 0.25), rgba(245, 158, 11, 0.15));
|
||||||
|
color: var(--accent-yellow);
|
||||||
|
border: 1px solid rgba(245, 158, 11, 0.4);
|
||||||
|
box-shadow: 0 0 12px rgba(245, 158, 11, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Yes/No Badges */
|
||||||
|
.badge-yes {
|
||||||
|
background: rgba(34, 197, 94, 0.2);
|
||||||
|
color: var(--accent-green);
|
||||||
|
border: 1px solid var(--accent-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-no {
|
||||||
|
background: rgba(100, 116, 139, 0.2);
|
||||||
|
color: var(--text-muted);
|
||||||
|
border: 1px solid var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status Indicator (Dot) */
|
||||||
|
.status-indicator {
|
||||||
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
box-shadow: 0 0 8px currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-online {
|
||||||
|
background: var(--accent-green);
|
||||||
|
color: var(--accent-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-offline {
|
||||||
|
background: var(--accent-red);
|
||||||
|
color: var(--accent-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mini Tags (для таблиц) */
|
||||||
|
.mini-tag {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 10px;
|
||||||
|
background: rgba(139, 92, 246, 0.15);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
color: var(--accent-purple-light);
|
||||||
|
margin: 2px;
|
||||||
|
transition: all var(--transition-base);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(139, 92, 246, 0.25);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
308
Backend/admin/frontend/assets/css/components/buttons.css
Normal file
308
Backend/admin/frontend/assets/css/components/buttons.css
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
/* ============================================
|
||||||
|
Buttons Component
|
||||||
|
Единая система кнопок
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
/* Base Button Styles */
|
||||||
|
.btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
padding: var(--space-sm) var(--space-md);
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
font-size: var(--text-base);
|
||||||
|
font-weight: var(--font-semibold);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all var(--transition-base);
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(:disabled):active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Action Button - Основная кнопка действия */
|
||||||
|
.action-btn {
|
||||||
|
padding: var(--space-sm) var(--space-md);
|
||||||
|
background: rgba(139, 92, 246, 0.15);
|
||||||
|
backdrop-filter: var(--backdrop-blur-light);
|
||||||
|
border: 1px solid rgba(139, 92, 246, 0.3);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
color: var(--accent-purple-light);
|
||||||
|
font-size: var(--text-base);
|
||||||
|
font-weight: var(--font-semibold);
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background: rgba(139, 92, 246, 0.25);
|
||||||
|
border-color: rgba(139, 92, 246, 0.5);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: var(--text-md);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Save Button Variant */
|
||||||
|
.save-btn {
|
||||||
|
background: rgba(16, 185, 129, 0.15);
|
||||||
|
border-color: rgba(16, 185, 129, 0.3);
|
||||||
|
color: var(--accent-green);
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background: rgba(16, 185, 129, 0.25);
|
||||||
|
border-color: rgba(16, 185, 129, 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Icon Button - Квадратная кнопка с иконкой */
|
||||||
|
.icon-btn {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0;
|
||||||
|
background: rgba(139, 92, 246, 0.1);
|
||||||
|
border: 1px solid rgba(139, 92, 246, 0.3);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
color: var(--accent-purple-light);
|
||||||
|
font-size: var(--text-md);
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background: rgba(139, 92, 246, 0.25);
|
||||||
|
border-color: rgba(139, 92, 246, 0.5);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Small Icon Button */
|
||||||
|
.icon-btn-small {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
background: rgba(239, 68, 68, 0.1);
|
||||||
|
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
color: var(--accent-red);
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background: rgba(239, 68, 68, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Window Control Buttons */
|
||||||
|
.window-btn {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
transition: all var(--transition-base);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn:hover {
|
||||||
|
background: var(--accent-red);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Server Control Button */
|
||||||
|
.server-control-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
padding: var(--space-sm) 18px;
|
||||||
|
background: linear-gradient(135deg, rgba(239, 68, 68, 0.2), rgba(239, 68, 68, 0.1));
|
||||||
|
backdrop-filter: var(--backdrop-blur-light);
|
||||||
|
border: 1px solid rgba(239, 68, 68, 0.4);
|
||||||
|
border-radius: 20px;
|
||||||
|
color: var(--accent-red);
|
||||||
|
font-size: var(--text-base);
|
||||||
|
font-weight: var(--font-semibold);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all var(--transition-base);
|
||||||
|
box-shadow: var(--shadow-red);
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background: linear-gradient(135deg, rgba(239, 68, 68, 0.3), rgba(239, 68, 68, 0.15));
|
||||||
|
border-color: rgba(239, 68, 68, 0.6);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: var(--text-md);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-control-btn.start-mode {
|
||||||
|
background: linear-gradient(135deg, rgba(16, 185, 129, 0.2), rgba(16, 185, 129, 0.1));
|
||||||
|
border-color: rgba(16, 185, 129, 0.4);
|
||||||
|
color: var(--accent-green);
|
||||||
|
box-shadow: var(--shadow-green);
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background: linear-gradient(135deg, rgba(16, 185, 129, 0.3), rgba(16, 185, 129, 0.15));
|
||||||
|
border-color: rgba(16, 185, 129, 0.6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status Toggle Buttons */
|
||||||
|
.status-toggle {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-btn {
|
||||||
|
flex: 1;
|
||||||
|
padding: 10px var(--space-md);
|
||||||
|
background: rgba(100, 116, 139, 0.1);
|
||||||
|
border: 1px solid rgba(100, 116, 139, 0.3);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: var(--text-base);
|
||||||
|
font-weight: var(--font-semibold);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all var(--transition-base);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(100, 116, 139, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: rgba(16, 185, 129, 0.2);
|
||||||
|
border-color: rgba(16, 185, 129, 0.5);
|
||||||
|
color: var(--accent-green);
|
||||||
|
box-shadow: 0 0 12px rgba(16, 185, 129, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child.active {
|
||||||
|
background: rgba(239, 68, 68, 0.2);
|
||||||
|
border-color: rgba(239, 68, 68, 0.5);
|
||||||
|
color: var(--accent-red);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navigation Buttons */
|
||||||
|
.nav-item {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all var(--transition-base);
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: -16px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 3px;
|
||||||
|
height: 0;
|
||||||
|
background: linear-gradient(180deg, var(--accent-purple), var(--accent-purple-light));
|
||||||
|
border-radius: 0 2px 2px 0;
|
||||||
|
transition: height var(--transition-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(139, 92, 246, 0.1);
|
||||||
|
color: var(--accent-purple-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: rgba(139, 92, 246, 0.15);
|
||||||
|
color: var(--accent-purple-light);
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Breadcrumb Buttons */
|
||||||
|
.breadcrumb-item {
|
||||||
|
font-size: var(--text-md);
|
||||||
|
color: var(--text-muted);
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: var(--space-sm) var(--space-lg);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all var(--transition-base);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(139, 92, 246, 0.1);
|
||||||
|
color: var(--accent-purple-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-weight: var(--font-medium);
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tab Buttons */
|
||||||
|
.vaccess-tab {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
padding: 10px 18px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: var(--text-base);
|
||||||
|
font-weight: var(--font-medium);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all var(--transition-base);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(139, 92, 246, 0.1);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: var(--accent-purple);
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
191
Backend/admin/frontend/assets/css/components/cards.css
Normal file
191
Backend/admin/frontend/assets/css/components/cards.css
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
/* ============================================
|
||||||
|
Cards Component
|
||||||
|
Единая система карточек
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
/* Service Card */
|
||||||
|
.service-card {
|
||||||
|
background: var(--glass-bg-light);
|
||||||
|
backdrop-filter: var(--backdrop-blur);
|
||||||
|
border: 1px solid var(--glass-border);
|
||||||
|
border-radius: var(--radius-2xl);
|
||||||
|
padding: var(--space-lg);
|
||||||
|
transition: all var(--transition-bounce);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
/* Градиентная линия сверху */
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 3px;
|
||||||
|
background: linear-gradient(90deg, var(--accent-purple), var(--accent-purple-light), var(--accent-cyan));
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity var(--transition-slow);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: var(--shadow-purple);
|
||||||
|
border-color: var(--glass-border-hover);
|
||||||
|
background: rgba(20, 20, 40, 0.5);
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-name {
|
||||||
|
font-size: var(--text-md);
|
||||||
|
font-weight: var(--font-semibold);
|
||||||
|
color: var(--text-primary);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
|
||||||
|
i {
|
||||||
|
color: var(--accent-purple-light);
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-weight: var(--font-medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-weight: var(--font-semibold);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Settings Card */
|
||||||
|
.settings-card {
|
||||||
|
background: var(--glass-bg-light);
|
||||||
|
backdrop-filter: var(--backdrop-blur);
|
||||||
|
border: 1px solid var(--glass-border);
|
||||||
|
border-radius: var(--radius-xl);
|
||||||
|
padding: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-card-title {
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
font-weight: var(--font-semibold);
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
|
||||||
|
i {
|
||||||
|
color: var(--accent-purple-light);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* vAccess Rule Card */
|
||||||
|
.vaccess-rule-card {
|
||||||
|
background: rgba(10, 14, 26, 0.4);
|
||||||
|
border: 1px solid var(--glass-border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: var(--space-md);
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 1px solid var(--glass-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-number {
|
||||||
|
font-size: var(--text-md);
|
||||||
|
font-weight: var(--font-bold);
|
||||||
|
color: var(--accent-purple-light);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-content {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 20px;
|
||||||
|
|
||||||
|
> .form-group:first-child {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Help Cards */
|
||||||
|
.help-card {
|
||||||
|
background: rgba(255, 255, 255, 0.02);
|
||||||
|
border-radius: var(--radius-xl);
|
||||||
|
padding: var(--space-xl);
|
||||||
|
border: 1px solid var(--glass-border);
|
||||||
|
transition: all var(--transition-slow);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--glass-border-hover);
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: var(--text-2xl);
|
||||||
|
font-weight: var(--font-semibold);
|
||||||
|
color: var(--accent-purple-light);
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
padding: 12px 0;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: 1.6;
|
||||||
|
border-bottom: 1px solid rgba(139, 92, 246, 0.05);
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-examples {
|
||||||
|
background: linear-gradient(135deg, rgba(139, 92, 246, 0.05) 0%, rgba(99, 102, 241, 0.05) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
219
Backend/admin/frontend/assets/css/components/forms.css
Normal file
219
Backend/admin/frontend/assets/css/components/forms.css
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
/* ============================================
|
||||||
|
Forms Component
|
||||||
|
Единая система форм
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
/* Form Container */
|
||||||
|
.settings-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form Group */
|
||||||
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form Row (2 columns) */
|
||||||
|
.form-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form Label */
|
||||||
|
.form-label {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: var(--font-semibold);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-hint {
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-weight: var(--font-normal);
|
||||||
|
text-transform: none;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form Input */
|
||||||
|
.form-input {
|
||||||
|
padding: 10px 14px;
|
||||||
|
background: var(--glass-bg-dark);
|
||||||
|
border: 1px solid var(--glass-border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: var(--text-base);
|
||||||
|
outline: none;
|
||||||
|
transition: all var(--transition-base);
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: rgba(139, 92, 246, 0.5);
|
||||||
|
box-shadow: 0 0 12px rgba(139, 92, 246, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: var(--text-muted);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form Info */
|
||||||
|
.form-info {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
color: var(--text-muted);
|
||||||
|
line-height: 1.5;
|
||||||
|
padding: 12px;
|
||||||
|
background: rgba(139, 92, 246, 0.05);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
border-left: 3px solid var(--accent-purple);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toggle Switch */
|
||||||
|
.toggle-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-switch {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 50px;
|
||||||
|
height: 26px;
|
||||||
|
|
||||||
|
input {
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
|
||||||
|
&:checked + .toggle-slider {
|
||||||
|
background: rgba(16, 185, 129, 0.2);
|
||||||
|
border-color: rgba(16, 185, 129, 0.5);
|
||||||
|
box-shadow: 0 0 16px rgba(16, 185, 129, 0.3);
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
transform: translateX(24px);
|
||||||
|
background: var(--accent-green);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-slider {
|
||||||
|
position: absolute;
|
||||||
|
cursor: pointer;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(239, 68, 68, 0.2);
|
||||||
|
border: 1px solid rgba(239, 68, 68, 0.4);
|
||||||
|
transition: all var(--transition-slow);
|
||||||
|
border-radius: 26px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
height: 18px;
|
||||||
|
width: 18px;
|
||||||
|
left: 3px;
|
||||||
|
bottom: 3px;
|
||||||
|
background: rgba(239, 68, 68, 0.8);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
transition: all var(--transition-slow);
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-label {
|
||||||
|
font-size: var(--text-md);
|
||||||
|
font-weight: var(--font-semibold);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Checkbox */
|
||||||
|
.checkbox-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: var(--text-base);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tags Container */
|
||||||
|
.tags-container {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
padding: 12px;
|
||||||
|
background: var(--glass-bg-dark);
|
||||||
|
border: 1px solid var(--glass-border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
min-height: 48px;
|
||||||
|
margin-top: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
padding: 4px 10px;
|
||||||
|
background: rgba(139, 92, 246, 0.2);
|
||||||
|
border: 1px solid rgba(139, 92, 246, 0.4);
|
||||||
|
border-radius: 16px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: var(--font-medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-remove {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--accent-red);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
transition: all var(--transition-base);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(239, 68, 68, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-input-wrapper {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Field Editor */
|
||||||
|
.field-editor {
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: var(--text-xl);
|
||||||
|
font-weight: var(--font-semibold);
|
||||||
|
color: var(--accent-purple-light);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
223
Backend/admin/frontend/assets/css/components/modals.css
Normal file
223
Backend/admin/frontend/assets/css/components/modals.css
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
/* ============================================
|
||||||
|
Modals Component
|
||||||
|
Единая система модальных окон
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
/* Modal Overlay */
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
backdrop-filter: var(--backdrop-blur-light);
|
||||||
|
z-index: var(--z-modal);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity var(--transition-slow);
|
||||||
|
|
||||||
|
&.show {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
|
||||||
|
.modal-window {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-content {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal Window */
|
||||||
|
.modal-window {
|
||||||
|
background: rgba(20, 20, 40, 0.95);
|
||||||
|
backdrop-filter: var(--backdrop-blur);
|
||||||
|
border: 1px solid var(--glass-border-hover);
|
||||||
|
border-radius: var(--radius-2xl);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
min-width: 900px;
|
||||||
|
max-width: 1200px;
|
||||||
|
max-height: 85vh;
|
||||||
|
overflow: hidden;
|
||||||
|
transform: scale(0.9);
|
||||||
|
transition: transform var(--transition-slow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px var(--space-lg);
|
||||||
|
border-bottom: 1px solid var(--glass-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-title {
|
||||||
|
font-size: var(--text-xl);
|
||||||
|
font-weight: var(--font-semibold);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close-btn {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all var(--transition-base);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(239, 68, 68, 0.2);
|
||||||
|
color: var(--accent-red);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
padding: var(--space-lg);
|
||||||
|
max-height: 60vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: var(--space-lg);
|
||||||
|
padding: 20px var(--space-lg);
|
||||||
|
border-top: 1px solid var(--glass-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Notification Modal */
|
||||||
|
.notification {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
backdrop-filter: var(--backdrop-blur-light);
|
||||||
|
z-index: var(--z-notification);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity var(--transition-slow);
|
||||||
|
|
||||||
|
&.show {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-content {
|
||||||
|
min-width: 400px;
|
||||||
|
max-width: 500px;
|
||||||
|
padding: var(--space-xl) 40px;
|
||||||
|
background: rgba(20, 20, 40, 0.95);
|
||||||
|
backdrop-filter: var(--backdrop-blur);
|
||||||
|
border: 1px solid var(--glass-border-hover);
|
||||||
|
border-radius: var(--radius-2xl);
|
||||||
|
box-shadow: var(--shadow-lg), 0 0 40px rgba(139, 92, 246, 0.3);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
text-align: center;
|
||||||
|
transform: scale(0.9);
|
||||||
|
transition: transform var(--transition-bounce);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification.success .notification-content {
|
||||||
|
border-color: rgba(16, 185, 129, 0.4);
|
||||||
|
box-shadow: var(--shadow-lg), 0 0 40px rgba(16, 185, 129, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification.error .notification-content {
|
||||||
|
border-color: rgba(239, 68, 68, 0.4);
|
||||||
|
box-shadow: var(--shadow-lg), 0 0 40px rgba(239, 68, 68, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-icon {
|
||||||
|
font-size: 56px;
|
||||||
|
|
||||||
|
i {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification.success .notification-icon {
|
||||||
|
color: var(--accent-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification.error .notification-icon {
|
||||||
|
color: var(--accent-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-text {
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
font-weight: var(--font-semibold);
|
||||||
|
color: var(--text-primary);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* App Loader */
|
||||||
|
.app-loader {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-secondary) 50%, var(--bg-tertiary) 100%);
|
||||||
|
z-index: var(--z-loader);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 0.5s ease;
|
||||||
|
|
||||||
|
&.hide {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader-icon {
|
||||||
|
font-size: 64px;
|
||||||
|
animation: bounce 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader-text {
|
||||||
|
font-size: var(--text-xl);
|
||||||
|
font-weight: var(--font-semibold);
|
||||||
|
background: linear-gradient(135deg, #7c3aed 0%, #a78bfa 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader-spinner {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: 3px solid rgba(139, 92, 246, 0.2);
|
||||||
|
border-top-color: var(--accent-purple);
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
236
Backend/admin/frontend/assets/css/components/tables.css
Normal file
236
Backend/admin/frontend/assets/css/components/tables.css
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
/* ============================================
|
||||||
|
Tables Component
|
||||||
|
ЕДИНАЯ система таблиц для всего приложения
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
/* Table Container */
|
||||||
|
.table-container {
|
||||||
|
background: var(--glass-bg-light);
|
||||||
|
backdrop-filter: var(--backdrop-blur);
|
||||||
|
border: 1px solid var(--glass-border);
|
||||||
|
border-radius: var(--radius-2xl);
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Base Table */
|
||||||
|
.data-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
|
||||||
|
thead {
|
||||||
|
background: rgba(139, 92, 246, 0.12);
|
||||||
|
backdrop-filter: var(--backdrop-blur-light);
|
||||||
|
|
||||||
|
tr {
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
padding: 18px 20px;
|
||||||
|
text-align: left;
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
font-weight: var(--font-bold);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1.2px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
width: 120px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody {
|
||||||
|
tr {
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
transition: all var(--transition-base);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(139, 92, 246, 0.08);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
padding: 16px 20px;
|
||||||
|
font-size: var(--text-base);
|
||||||
|
color: var(--text-primary);
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* vAccess Table - использует те же стили что и data-table */
|
||||||
|
.vaccess-table-container {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
max-height: 55vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vaccess-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
|
||||||
|
thead {
|
||||||
|
tr {
|
||||||
|
background: rgba(139, 92, 246, 0.05);
|
||||||
|
display: table-row;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
padding: 16px;
|
||||||
|
text-align: left;
|
||||||
|
font-size: var(--text-base);
|
||||||
|
font-weight: var(--font-semibold);
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
border: none;
|
||||||
|
display: table-cell;
|
||||||
|
box-sizing: content-box;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody {
|
||||||
|
tr {
|
||||||
|
background: rgba(255, 255, 255, 0.02);
|
||||||
|
transition: all var(--transition-slow);
|
||||||
|
cursor: grab;
|
||||||
|
display: table-row;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(139, 92, 246, 0.08);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[draggable="true"] {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
padding: 20px 16px;
|
||||||
|
font-size: var(--text-md);
|
||||||
|
color: var(--text-primary);
|
||||||
|
border-top: 1px solid rgba(139, 92, 246, 0.05);
|
||||||
|
border-bottom: 1px solid rgba(139, 92, 246, 0.05);
|
||||||
|
cursor: pointer;
|
||||||
|
display: table-cell;
|
||||||
|
box-sizing: content-box;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Принудительно растягиваем на всю ширину */
|
||||||
|
thead, tbody {
|
||||||
|
display: table-row-group;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table Column Sizing - адаптивные размеры */
|
||||||
|
.col-drag {
|
||||||
|
width: 3%;
|
||||||
|
min-width: 40px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-type {
|
||||||
|
width: 8%;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-files {
|
||||||
|
width: 15%;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-paths {
|
||||||
|
width: 18%;
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-ips {
|
||||||
|
width: 15%;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-exceptions {
|
||||||
|
width: 15%;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-error {
|
||||||
|
width: 10%;
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-actions {
|
||||||
|
width: 5%;
|
||||||
|
min-width: 60px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Drag Handle */
|
||||||
|
.drag-handle {
|
||||||
|
color: var(--text-muted);
|
||||||
|
opacity: 0.3;
|
||||||
|
transition: all var(--transition-base);
|
||||||
|
cursor: grab;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
color: var(--accent-purple-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty Field */
|
||||||
|
.empty-field {
|
||||||
|
color: var(--text-muted);
|
||||||
|
opacity: 0.4;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Clickable Link in Tables */
|
||||||
|
.clickable-link {
|
||||||
|
color: var(--accent-purple-light);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all var(--transition-base);
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-xs);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--accent-purple);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Table */
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.table-container {
|
||||||
|
overflow-x: scroll;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
66
Backend/admin/frontend/assets/css/layout/container.css
Normal file
66
Backend/admin/frontend/assets/css/layout/container.css
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
/* ============================================
|
||||||
|
Container Layout
|
||||||
|
Основной контейнер контента
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
.container {
|
||||||
|
height: calc(100vh - var(--header-height));
|
||||||
|
margin-top: var(--header-height);
|
||||||
|
margin-left: var(--sidebar-width);
|
||||||
|
padding: 40px var(--space-3xl);
|
||||||
|
position: relative;
|
||||||
|
z-index: var(--z-base);
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main Content */
|
||||||
|
.main-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-2xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section */
|
||||||
|
.section {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: var(--text-md);
|
||||||
|
font-weight: var(--font-bold);
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: var(--space-lg);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1.5px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-lg);
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
width: 4px;
|
||||||
|
height: 16px;
|
||||||
|
background: linear-gradient(180deg, var(--accent-purple), var(--accent-purple-light));
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer */
|
||||||
|
.footer {
|
||||||
|
margin-top: var(--space-3xl);
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.header {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
76
Backend/admin/frontend/assets/css/layout/header.css
Normal file
76
Backend/admin/frontend/assets/css/layout/header.css
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
/* ============================================
|
||||||
|
Header Layout
|
||||||
|
Window controls и title bar
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
.window-controls {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
background: rgba(10, 14, 26, 0.9);
|
||||||
|
backdrop-filter: var(--backdrop-blur);
|
||||||
|
border-bottom: 1px solid var(--glass-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
height: var(--header-height);
|
||||||
|
padding: 0 var(--space-xl);
|
||||||
|
--wails-draggable: drag;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-bar-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-text {
|
||||||
|
font-size: var(--text-xl);
|
||||||
|
font-weight: var(--font-bold);
|
||||||
|
color: #ffffff;
|
||||||
|
user-select: none;
|
||||||
|
letter-spacing: -0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-bar-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-md);
|
||||||
|
--wails-draggable: no-drag;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Server Status */
|
||||||
|
.server-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
padding: var(--space-sm) var(--space-md);
|
||||||
|
background: linear-gradient(135deg, rgba(16, 185, 129, 0.15), rgba(16, 185, 129, 0.05));
|
||||||
|
backdrop-filter: var(--backdrop-blur-light);
|
||||||
|
border-radius: 20px;
|
||||||
|
border: 1px solid rgba(16, 185, 129, 0.3);
|
||||||
|
box-shadow: var(--shadow-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: var(--font-semibold);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
27
Backend/admin/frontend/assets/css/layout/sidebar.css
Normal file
27
Backend/admin/frontend/assets/css/layout/sidebar.css
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/* ============================================
|
||||||
|
Sidebar Layout
|
||||||
|
Боковая навигация
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: var(--header-height);
|
||||||
|
width: var(--sidebar-width);
|
||||||
|
height: calc(100vh - var(--header-height));
|
||||||
|
background: rgba(10, 14, 26, 0.95);
|
||||||
|
backdrop-filter: var(--backdrop-blur);
|
||||||
|
border-right: 1px solid var(--glass-border);
|
||||||
|
z-index: 100;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-nav {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
padding: 0 var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
29
Backend/admin/frontend/assets/css/local_lib/all.min.css
vendored
Normal file
29
Backend/admin/frontend/assets/css/local_lib/all.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
26
Backend/admin/frontend/assets/css/main.css
Normal file
26
Backend/admin/frontend/assets/css/main.css
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
/* ============================================
|
||||||
|
vServer Admin Panel - Main CSS
|
||||||
|
Профессиональная модульная архитектура
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
/* 1. Variables & Base */
|
||||||
|
@import 'variables.css';
|
||||||
|
@import 'base.css';
|
||||||
|
|
||||||
|
/* 2. Components */
|
||||||
|
@import 'components/buttons.css';
|
||||||
|
@import 'components/badges.css';
|
||||||
|
@import 'components/cards.css';
|
||||||
|
@import 'components/forms.css';
|
||||||
|
@import 'components/tables.css';
|
||||||
|
@import 'components/modals.css';
|
||||||
|
|
||||||
|
/* 3. Layout */
|
||||||
|
@import 'layout/header.css';
|
||||||
|
@import 'layout/sidebar.css';
|
||||||
|
@import 'layout/container.css';
|
||||||
|
|
||||||
|
/* 4. Pages */
|
||||||
|
@import 'pages/dashboard.css';
|
||||||
|
@import 'pages/vaccess.css';
|
||||||
|
|
||||||
62
Backend/admin/frontend/assets/css/pages/dashboard.css
Normal file
62
Backend/admin/frontend/assets/css/pages/dashboard.css
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
/* ============================================
|
||||||
|
Dashboard Page
|
||||||
|
Главная страница с сервисами и таблицами
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
/* Services Grid */
|
||||||
|
.services-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(5, 1fr);
|
||||||
|
gap: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Settings Header */
|
||||||
|
.settings-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: var(--space-lg);
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Settings Grid */
|
||||||
|
.settings-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: var(--space-lg);
|
||||||
|
margin-bottom: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Settings Actions */
|
||||||
|
.settings-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Grid */
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.services-grid {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.services-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.services-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
264
Backend/admin/frontend/assets/css/pages/vaccess.css
Normal file
264
Backend/admin/frontend/assets/css/pages/vaccess.css
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
/* ============================================
|
||||||
|
vAccess Editor Page
|
||||||
|
Страница редактора правил доступа
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
.vaccess-page {
|
||||||
|
animation: fadeIn var(--transition-slow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Breadcrumbs */
|
||||||
|
.breadcrumbs {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--space-lg);
|
||||||
|
margin-bottom: var(--space-md);
|
||||||
|
padding: var(--space-md) 20px;
|
||||||
|
background: rgba(139, 92, 246, 0.05);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
border: 1px solid var(--glass-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumbs-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumbs-tabs {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-separator {
|
||||||
|
color: var(--text-muted);
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* vAccess Header */
|
||||||
|
.vaccess-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: var(--space-md);
|
||||||
|
padding: var(--space-lg);
|
||||||
|
background: rgba(139, 92, 246, 0.03);
|
||||||
|
border-radius: var(--radius-xl);
|
||||||
|
border: 1px solid var(--glass-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vaccess-title-block {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vaccess-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
min-width: 200px;
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 10px var(--space-md);
|
||||||
|
font-size: var(--text-base);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.vaccess-title {
|
||||||
|
font-size: var(--text-3xl);
|
||||||
|
font-weight: var(--font-semibold);
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin: 0 0 var(--space-sm) 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-lg);
|
||||||
|
|
||||||
|
i {
|
||||||
|
color: var(--accent-purple-light);
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.vaccess-subtitle {
|
||||||
|
font-size: var(--text-md);
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* vAccess Tab Content */
|
||||||
|
.vaccess-tab-content {
|
||||||
|
animation: fadeIn var(--transition-slow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* vAccess Rules Container */
|
||||||
|
.vaccess-rules-container {
|
||||||
|
/* Контейнер без padding чтобы таблица была на всю ширину */
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* vAccess Rules List */
|
||||||
|
.vaccess-rules-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
max-height: 55vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-right: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty State */
|
||||||
|
.vaccess-empty {
|
||||||
|
text-align: center;
|
||||||
|
padding: 80px 40px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
font-size: 64px;
|
||||||
|
margin-bottom: var(--space-lg);
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vaccess-empty h3 {
|
||||||
|
font-size: var(--text-2xl);
|
||||||
|
font-weight: var(--font-semibold);
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vaccess-empty p {
|
||||||
|
font-size: var(--text-md);
|
||||||
|
margin-bottom: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* vAccess Help */
|
||||||
|
.vaccess-help {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Help Parameters */
|
||||||
|
.help-params {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-param {
|
||||||
|
padding: 20px;
|
||||||
|
background: rgba(139, 92, 246, 0.03);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
border-left: 3px solid var(--accent-purple);
|
||||||
|
|
||||||
|
strong {
|
||||||
|
display: block;
|
||||||
|
font-size: 15px;
|
||||||
|
color: var(--accent-purple-light);
|
||||||
|
margin-bottom: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: var(--space-sm) 0 0 0;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
margin: 12px 0 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
padding: 3px 8px;
|
||||||
|
background: rgba(139, 92, 246, 0.15);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-size: var(--text-base);
|
||||||
|
color: var(--accent-purple-light);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-warning {
|
||||||
|
color: rgba(251, 191, 36, 0.9) !important;
|
||||||
|
margin-top: var(--space-sm);
|
||||||
|
font-size: var(--text-base);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Help Patterns */
|
||||||
|
.help-patterns {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pattern-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
padding: var(--space-md);
|
||||||
|
background: rgba(139, 92, 246, 0.05);
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: all var(--transition-base);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(139, 92, 246, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-size: var(--text-md);
|
||||||
|
font-weight: var(--font-semibold);
|
||||||
|
color: var(--accent-purple-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: var(--text-base);
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Help Examples */
|
||||||
|
.help-example {
|
||||||
|
margin-bottom: var(--space-xl);
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
font-weight: var(--font-semibold);
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: var(--space-md);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-rule {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
background: rgba(255, 255, 255, 0.02);
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid var(--glass-border);
|
||||||
|
|
||||||
|
div {
|
||||||
|
font-size: var(--text-md);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
strong {
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-right: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
color: var(--accent-purple-light);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
100
Backend/admin/frontend/assets/css/variables.css
Normal file
100
Backend/admin/frontend/assets/css/variables.css
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
/* ============================================
|
||||||
|
CSS Custom Properties (Design Tokens)
|
||||||
|
Профессиональная система переменных
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
/* Colors - Background */
|
||||||
|
--bg-primary: #0b101f;
|
||||||
|
--bg-secondary: #121420;
|
||||||
|
--bg-tertiary: #0d0f1c;
|
||||||
|
|
||||||
|
/* Colors - Glass Effect */
|
||||||
|
--glass-bg: rgba(20, 20, 40, 0.4);
|
||||||
|
--glass-bg-light: rgba(20, 20, 40, 0.3);
|
||||||
|
--glass-bg-dark: rgba(10, 14, 26, 0.5);
|
||||||
|
--glass-border: rgba(139, 92, 246, 0.15);
|
||||||
|
--glass-border-hover: rgba(139, 92, 246, 0.3);
|
||||||
|
|
||||||
|
/* Colors - Accent */
|
||||||
|
--accent-blue: #5b21b6;
|
||||||
|
--accent-blue-light: #7c3aed;
|
||||||
|
--accent-purple: #8b5cf6;
|
||||||
|
--accent-purple-light: #a78bfa;
|
||||||
|
--accent-cyan: #06b6d4;
|
||||||
|
--accent-green: #10b981;
|
||||||
|
--accent-red: #ef4444;
|
||||||
|
--accent-yellow: #f59e0b;
|
||||||
|
|
||||||
|
/* Colors - Text */
|
||||||
|
--text-primary: #e2e8f0;
|
||||||
|
--text-secondary: #94a3b8;
|
||||||
|
--text-muted: #64748b;
|
||||||
|
|
||||||
|
/* Shadows */
|
||||||
|
--shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||||
|
--shadow-md: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||||||
|
--shadow-lg: 0 12px 40px rgba(0, 0, 0, 0.7);
|
||||||
|
--shadow-purple: 0 8px 32px rgba(139, 92, 246, 0.3);
|
||||||
|
--shadow-green: 0 4px 12px rgba(16, 185, 129, 0.3);
|
||||||
|
--shadow-red: 0 4px 12px rgba(239, 68, 68, 0.3);
|
||||||
|
|
||||||
|
/* Spacing */
|
||||||
|
--space-xs: 4px;
|
||||||
|
--space-sm: 8px;
|
||||||
|
--space-md: 16px;
|
||||||
|
--space-lg: 24px;
|
||||||
|
--space-xl: 32px;
|
||||||
|
--space-2xl: 48px;
|
||||||
|
--space-3xl: 60px;
|
||||||
|
|
||||||
|
/* Border Radius */
|
||||||
|
--radius-sm: 6px;
|
||||||
|
--radius-md: 8px;
|
||||||
|
--radius-lg: 12px;
|
||||||
|
--radius-xl: 16px;
|
||||||
|
--radius-2xl: 10px;
|
||||||
|
--radius-full: 50%;
|
||||||
|
|
||||||
|
/* Transitions */
|
||||||
|
--transition-fast: 0.15s ease;
|
||||||
|
--transition-base: 0.2s ease;
|
||||||
|
--transition-slow: 0.3s ease;
|
||||||
|
--transition-bounce: 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
|
||||||
|
/* Typography */
|
||||||
|
--font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
--font-mono: 'Consolas', 'Courier New', monospace;
|
||||||
|
|
||||||
|
/* Font Sizes */
|
||||||
|
--text-xs: 10px;
|
||||||
|
--text-sm: 11px;
|
||||||
|
--text-base: 13px;
|
||||||
|
--text-md: 14px;
|
||||||
|
--text-lg: 16px;
|
||||||
|
--text-xl: 18px;
|
||||||
|
--text-2xl: 20px;
|
||||||
|
--text-3xl: 28px;
|
||||||
|
|
||||||
|
/* Font Weights */
|
||||||
|
--font-normal: 400;
|
||||||
|
--font-medium: 500;
|
||||||
|
--font-semibold: 600;
|
||||||
|
--font-bold: 700;
|
||||||
|
|
||||||
|
/* Z-Index Scale */
|
||||||
|
--z-base: 1;
|
||||||
|
--z-dropdown: 100;
|
||||||
|
--z-modal: 9998;
|
||||||
|
--z-notification: 9999;
|
||||||
|
--z-loader: 10000;
|
||||||
|
|
||||||
|
/* Layout */
|
||||||
|
--header-height: 60px;
|
||||||
|
--sidebar-width: 80px;
|
||||||
|
|
||||||
|
/* Backdrop Filter */
|
||||||
|
--backdrop-blur: blur(20px) saturate(180%);
|
||||||
|
--backdrop-blur-light: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
129
Backend/admin/frontend/assets/js/api/config.js
Normal file
129
Backend/admin/frontend/assets/js/api/config.js
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
/* ============================================
|
||||||
|
Config API
|
||||||
|
Работа с конфигурацией
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
import { isWailsAvailable, log } from '../utils/helpers.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Класс для работы с конфигурацией
|
||||||
|
*/
|
||||||
|
class ConfigAPI {
|
||||||
|
constructor() {
|
||||||
|
this.available = isWailsAvailable();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить конфигурацию
|
||||||
|
*/
|
||||||
|
async getConfig() {
|
||||||
|
if (!this.available) return null;
|
||||||
|
try {
|
||||||
|
return await window.go.admin.App.GetConfig();
|
||||||
|
} catch (error) {
|
||||||
|
log(`Ошибка получения конфигурации: ${error.message}`, 'error');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Сохранить конфигурацию
|
||||||
|
*/
|
||||||
|
async saveConfig(configJSON) {
|
||||||
|
if (!this.available) return 'Error: API недоступен';
|
||||||
|
try {
|
||||||
|
return await window.go.admin.App.SaveConfig(configJSON);
|
||||||
|
} catch (error) {
|
||||||
|
log(`Ошибка сохранения конфигурации: ${error.message}`, 'error');
|
||||||
|
return `Error: ${error.message}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Включить Proxy Service
|
||||||
|
*/
|
||||||
|
async enableProxyService() {
|
||||||
|
if (!this.available) return;
|
||||||
|
try {
|
||||||
|
await window.go.admin.App.EnableProxyService();
|
||||||
|
} catch (error) {
|
||||||
|
log(`Ошибка включения Proxy: ${error.message}`, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Отключить Proxy Service
|
||||||
|
*/
|
||||||
|
async disableProxyService() {
|
||||||
|
if (!this.available) return;
|
||||||
|
try {
|
||||||
|
await window.go.admin.App.DisableProxyService();
|
||||||
|
} catch (error) {
|
||||||
|
log(`Ошибка отключения Proxy: ${error.message}`, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Перезапустить все сервисы
|
||||||
|
*/
|
||||||
|
async restartAllServices() {
|
||||||
|
if (!this.available) return;
|
||||||
|
try {
|
||||||
|
await window.go.admin.App.RestartAllServices();
|
||||||
|
} catch (error) {
|
||||||
|
log(`Ошибка перезапуска сервисов: ${error.message}`, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Запустить HTTP Service
|
||||||
|
*/
|
||||||
|
async startHTTPService() {
|
||||||
|
if (!this.available) return;
|
||||||
|
try {
|
||||||
|
await window.go.admin.App.StartHTTPService();
|
||||||
|
} catch (error) {
|
||||||
|
log(`Ошибка запуска HTTP: ${error.message}`, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Остановить HTTP Service
|
||||||
|
*/
|
||||||
|
async stopHTTPService() {
|
||||||
|
if (!this.available) return;
|
||||||
|
try {
|
||||||
|
await window.go.admin.App.StopHTTPService();
|
||||||
|
} catch (error) {
|
||||||
|
log(`Ошибка остановки HTTP: ${error.message}`, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Запустить HTTPS Service
|
||||||
|
*/
|
||||||
|
async startHTTPSService() {
|
||||||
|
if (!this.available) return;
|
||||||
|
try {
|
||||||
|
await window.go.admin.App.StartHTTPSService();
|
||||||
|
} catch (error) {
|
||||||
|
log(`Ошибка запуска HTTPS: ${error.message}`, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Остановить HTTPS Service
|
||||||
|
*/
|
||||||
|
async stopHTTPSService() {
|
||||||
|
if (!this.available) return;
|
||||||
|
try {
|
||||||
|
await window.go.admin.App.StopHTTPSService();
|
||||||
|
} catch (error) {
|
||||||
|
log(`Ошибка остановки HTTPS: ${error.message}`, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Экспортируем единственный экземпляр
|
||||||
|
export const configAPI = new ConfigAPI();
|
||||||
|
|
||||||
143
Backend/admin/frontend/assets/js/api/wails.js
Normal file
143
Backend/admin/frontend/assets/js/api/wails.js
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
/* ============================================
|
||||||
|
Wails API Wrapper
|
||||||
|
Обёртка над Wails API
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
import { isWailsAvailable, log } from '../utils/helpers.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Базовый класс для работы с Wails API
|
||||||
|
*/
|
||||||
|
class WailsAPI {
|
||||||
|
constructor() {
|
||||||
|
this.available = isWailsAvailable();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверка доступности API
|
||||||
|
*/
|
||||||
|
checkAvailability() {
|
||||||
|
if (!this.available) {
|
||||||
|
log('Wails API недоступен', 'warn');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить статус всех сервисов
|
||||||
|
*/
|
||||||
|
async getAllServicesStatus() {
|
||||||
|
if (!this.checkAvailability()) return null;
|
||||||
|
try {
|
||||||
|
return await window.go.admin.App.GetAllServicesStatus();
|
||||||
|
} catch (error) {
|
||||||
|
log(`Ошибка получения статуса сервисов: ${error.message}`, 'error');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить список сайтов
|
||||||
|
*/
|
||||||
|
async getSitesList() {
|
||||||
|
if (!this.checkAvailability()) return [];
|
||||||
|
try {
|
||||||
|
return await window.go.admin.App.GetSitesList();
|
||||||
|
} catch (error) {
|
||||||
|
log(`Ошибка получения списка сайтов: ${error.message}`, 'error');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить список прокси
|
||||||
|
*/
|
||||||
|
async getProxyList() {
|
||||||
|
if (!this.checkAvailability()) return [];
|
||||||
|
try {
|
||||||
|
return await window.go.admin.App.GetProxyList();
|
||||||
|
} catch (error) {
|
||||||
|
log(`Ошибка получения списка прокси: ${error.message}`, 'error');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить правила vAccess
|
||||||
|
*/
|
||||||
|
async getVAccessRules(host, isProxy) {
|
||||||
|
if (!this.checkAvailability()) return { rules: [] };
|
||||||
|
try {
|
||||||
|
return await window.go.admin.App.GetVAccessRules(host, isProxy);
|
||||||
|
} catch (error) {
|
||||||
|
log(`Ошибка получения правил vAccess: ${error.message}`, 'error');
|
||||||
|
return { rules: [] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Сохранить правила vAccess
|
||||||
|
*/
|
||||||
|
async saveVAccessRules(host, isProxy, configJSON) {
|
||||||
|
if (!this.checkAvailability()) return 'Error: API недоступен';
|
||||||
|
try {
|
||||||
|
return await window.go.admin.App.SaveVAccessRules(host, isProxy, configJSON);
|
||||||
|
} catch (error) {
|
||||||
|
log(`Ошибка сохранения правил vAccess: ${error.message}`, 'error');
|
||||||
|
return `Error: ${error.message}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Запустить сервер
|
||||||
|
*/
|
||||||
|
async startServer() {
|
||||||
|
if (!this.checkAvailability()) return;
|
||||||
|
try {
|
||||||
|
await window.go.admin.App.StartServer();
|
||||||
|
} catch (error) {
|
||||||
|
log(`Ошибка запуска сервера: ${error.message}`, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Остановить сервер
|
||||||
|
*/
|
||||||
|
async stopServer() {
|
||||||
|
if (!this.checkAvailability()) return;
|
||||||
|
try {
|
||||||
|
await window.go.admin.App.StopServer();
|
||||||
|
} catch (error) {
|
||||||
|
log(`Ошибка остановки сервера: ${error.message}`, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверить готовность сервисов
|
||||||
|
*/
|
||||||
|
async checkServicesReady() {
|
||||||
|
if (!this.checkAvailability()) return false;
|
||||||
|
try {
|
||||||
|
return await window.go.admin.App.CheckServicesReady();
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Открыть папку сайта
|
||||||
|
*/
|
||||||
|
async openSiteFolder(host) {
|
||||||
|
if (!this.checkAvailability()) return;
|
||||||
|
try {
|
||||||
|
await window.go.admin.App.OpenSiteFolder(host);
|
||||||
|
} catch (error) {
|
||||||
|
log(`Ошибка открытия папки: ${error.message}`, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Экспортируем единственный экземпляр
|
||||||
|
export const api = new WailsAPI();
|
||||||
|
|
||||||
151
Backend/admin/frontend/assets/js/components/proxy.js
Normal file
151
Backend/admin/frontend/assets/js/components/proxy.js
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
/* ============================================
|
||||||
|
Proxy Component
|
||||||
|
Управление прокси
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
import { api } from '../api/wails.js';
|
||||||
|
import { isWailsAvailable } from '../utils/helpers.js';
|
||||||
|
import { $ } from '../utils/dom.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Класс для управления прокси
|
||||||
|
*/
|
||||||
|
export class ProxyManager {
|
||||||
|
constructor() {
|
||||||
|
this.proxiesData = [];
|
||||||
|
this.mockData = [
|
||||||
|
{
|
||||||
|
enable: true,
|
||||||
|
external_domain: 'git.example.ru',
|
||||||
|
local_address: '127.0.0.1',
|
||||||
|
local_port: '3333',
|
||||||
|
service_https_use: false,
|
||||||
|
auto_https: true,
|
||||||
|
status: 'active'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enable: true,
|
||||||
|
external_domain: 'api.example.com',
|
||||||
|
local_address: '127.0.0.1',
|
||||||
|
local_port: '8080',
|
||||||
|
service_https_use: true,
|
||||||
|
auto_https: false,
|
||||||
|
status: 'active'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enable: false,
|
||||||
|
external_domain: 'test.example.net',
|
||||||
|
local_address: '127.0.0.1',
|
||||||
|
local_port: '5000',
|
||||||
|
service_https_use: false,
|
||||||
|
auto_https: false,
|
||||||
|
status: 'disabled'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Загрузить список прокси
|
||||||
|
*/
|
||||||
|
async load() {
|
||||||
|
if (isWailsAvailable()) {
|
||||||
|
this.proxiesData = await api.getProxyList();
|
||||||
|
} else {
|
||||||
|
// Используем тестовые данные если Wails недоступен
|
||||||
|
this.proxiesData = this.mockData;
|
||||||
|
}
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Отрисовать список прокси
|
||||||
|
*/
|
||||||
|
render() {
|
||||||
|
const tbody = $('proxyTable')?.querySelector('tbody');
|
||||||
|
if (!tbody) return;
|
||||||
|
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
|
||||||
|
this.proxiesData.forEach((proxy, index) => {
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
const statusBadge = proxy.status === 'active' ? 'badge-online' : 'badge-offline';
|
||||||
|
const httpsBadge = proxy.service_https_use ? 'badge-yes">HTTPS' : 'badge-no">HTTP';
|
||||||
|
const autoHttpsBadge = proxy.auto_https ? 'badge-yes">Да' : 'badge-no">Нет';
|
||||||
|
const protocol = proxy.auto_https ? 'https' : 'http';
|
||||||
|
|
||||||
|
row.innerHTML = `
|
||||||
|
<td><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-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 'edit-proxy':
|
||||||
|
if (window.editProxy) {
|
||||||
|
window.editProxy(index);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Открыть ссылку
|
||||||
|
*/
|
||||||
|
openLink(url) {
|
||||||
|
if (window.runtime?.BrowserOpenURL) {
|
||||||
|
window.runtime.BrowserOpenURL(url);
|
||||||
|
} else {
|
||||||
|
window.open(url, '_blank');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
191
Backend/admin/frontend/assets/js/components/services.js
Normal file
191
Backend/admin/frontend/assets/js/components/services.js
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
/* ============================================
|
||||||
|
Services Component
|
||||||
|
Управление сервисами
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
import { api } from '../api/wails.js';
|
||||||
|
import { $, $$, addClass, removeClass } from '../utils/dom.js';
|
||||||
|
import { notification } from '../ui/notification.js';
|
||||||
|
import { sleep, isWailsAvailable } from '../utils/helpers.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Класс для управления сервисами
|
||||||
|
*/
|
||||||
|
export class ServicesManager {
|
||||||
|
constructor() {
|
||||||
|
this.serverRunning = true;
|
||||||
|
this.isOperating = false;
|
||||||
|
this.controlBtn = $('serverControlBtn');
|
||||||
|
this.statusIndicator = document.querySelector('.status-indicator');
|
||||||
|
this.statusText = document.querySelector('.status-text');
|
||||||
|
this.btnText = document.querySelector('.btn-text');
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
if (this.controlBtn) {
|
||||||
|
this.controlBtn.addEventListener('click', () => this.toggleServer());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Подписка на события
|
||||||
|
if (window.runtime?.EventsOn) {
|
||||||
|
window.runtime.EventsOn('service:changed', (status) => {
|
||||||
|
this.renderServices(status);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.runtime.EventsOn('server:already_running', () => {
|
||||||
|
notification.error('vServer уже запущен!<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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
150
Backend/admin/frontend/assets/js/components/sites.js
Normal file
150
Backend/admin/frontend/assets/js/components/sites.js
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
/* ============================================
|
||||||
|
Sites Component
|
||||||
|
Управление сайтами
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
import { api } from '../api/wails.js';
|
||||||
|
import { isWailsAvailable } from '../utils/helpers.js';
|
||||||
|
import { $ } from '../utils/dom.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Класс для управления сайтами
|
||||||
|
*/
|
||||||
|
export class SitesManager {
|
||||||
|
constructor() {
|
||||||
|
this.sitesData = [];
|
||||||
|
this.mockData = [
|
||||||
|
{
|
||||||
|
name: 'Локальный сайт',
|
||||||
|
host: '127.0.0.1',
|
||||||
|
alias: ['localhost'],
|
||||||
|
status: 'active',
|
||||||
|
root_file: 'index.html',
|
||||||
|
root_file_routing: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Тестовый проект',
|
||||||
|
host: 'test.local',
|
||||||
|
alias: ['*.test.local', 'test.com'],
|
||||||
|
status: 'active',
|
||||||
|
root_file: 'index.php',
|
||||||
|
root_file_routing: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'API сервис',
|
||||||
|
host: 'api.example.com',
|
||||||
|
alias: ['*.api.example.com'],
|
||||||
|
status: 'inactive',
|
||||||
|
root_file: 'index.php',
|
||||||
|
root_file_routing: true
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Загрузить список сайтов
|
||||||
|
*/
|
||||||
|
async load() {
|
||||||
|
if (isWailsAvailable()) {
|
||||||
|
this.sitesData = await api.getSitesList();
|
||||||
|
} else {
|
||||||
|
// Используем тестовые данные если Wails недоступен
|
||||||
|
this.sitesData = this.mockData;
|
||||||
|
}
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Отрисовать список сайтов
|
||||||
|
*/
|
||||||
|
render() {
|
||||||
|
const tbody = $('sitesTable')?.querySelector('tbody');
|
||||||
|
if (!tbody) return;
|
||||||
|
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
|
||||||
|
this.sitesData.forEach((site, index) => {
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
const statusBadge = site.status === 'active' ? 'badge-online' : 'badge-offline';
|
||||||
|
const aliases = site.alias.join(', ');
|
||||||
|
|
||||||
|
row.innerHTML = `
|
||||||
|
<td>${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-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 'edit-site':
|
||||||
|
if (window.editSite) {
|
||||||
|
window.editSite(index);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Открыть ссылку
|
||||||
|
*/
|
||||||
|
openLink(url) {
|
||||||
|
if (window.runtime?.BrowserOpenURL) {
|
||||||
|
window.runtime.BrowserOpenURL(url);
|
||||||
|
} else {
|
||||||
|
window.open(url, '_blank');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
396
Backend/admin/frontend/assets/js/components/vaccess.js
Normal file
396
Backend/admin/frontend/assets/js/components/vaccess.js
Normal file
@@ -0,0 +1,396 @@
|
|||||||
|
/* ============================================
|
||||||
|
vAccess Component
|
||||||
|
Управление правилами доступа
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
import { api } from '../api/wails.js';
|
||||||
|
import { $, hide, show } from '../utils/dom.js';
|
||||||
|
import { notification } from '../ui/notification.js';
|
||||||
|
import { modal } from '../ui/modal.js';
|
||||||
|
import { isWailsAvailable } from '../utils/helpers.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Класс для управления vAccess правилами
|
||||||
|
*/
|
||||||
|
export class VAccessManager {
|
||||||
|
constructor() {
|
||||||
|
this.vAccessHost = '';
|
||||||
|
this.vAccessIsProxy = false;
|
||||||
|
this.vAccessRules = [];
|
||||||
|
this.vAccessReturnSection = 'sectionSites';
|
||||||
|
this.draggedIndex = null;
|
||||||
|
this.editingField = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Открыть редактор vAccess
|
||||||
|
*/
|
||||||
|
async open(host, isProxy) {
|
||||||
|
this.vAccessHost = host;
|
||||||
|
this.vAccessIsProxy = isProxy;
|
||||||
|
|
||||||
|
// Запоминаем откуда пришли
|
||||||
|
if ($('sectionSites').style.display !== 'none') {
|
||||||
|
this.vAccessReturnSection = 'sectionSites';
|
||||||
|
} else if ($('sectionProxy').style.display !== 'none') {
|
||||||
|
this.vAccessReturnSection = 'sectionProxy';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Загружаем правила
|
||||||
|
if (isWailsAvailable()) {
|
||||||
|
const config = await api.getVAccessRules(host, isProxy);
|
||||||
|
this.vAccessRules = config.rules || [];
|
||||||
|
} else {
|
||||||
|
// Тестовые данные для браузерного режима
|
||||||
|
this.vAccessRules = [
|
||||||
|
{
|
||||||
|
type: 'Disable',
|
||||||
|
type_file: ['*.php'],
|
||||||
|
path_access: ['/uploads/*'],
|
||||||
|
ip_list: [],
|
||||||
|
exceptions_dir: [],
|
||||||
|
url_error: '404'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем UI
|
||||||
|
const subtitle = isProxy
|
||||||
|
? 'Управление правилами доступа для прокси-сервиса'
|
||||||
|
: 'Управление правилами доступа для сайта';
|
||||||
|
|
||||||
|
$('breadcrumbHost').textContent = host;
|
||||||
|
$('vAccessSubtitle').textContent = subtitle;
|
||||||
|
|
||||||
|
// Переключаем на страницу редактора
|
||||||
|
this.hideAllSections();
|
||||||
|
show($('sectionVAccessEditor'));
|
||||||
|
|
||||||
|
// Рендерим правила и показываем правильную вкладку
|
||||||
|
this.renderRulesList();
|
||||||
|
this.switchTab('rules');
|
||||||
|
|
||||||
|
// Привязываем кнопку сохранения
|
||||||
|
const saveBtn = $('saveVAccessBtn');
|
||||||
|
if (saveBtn) {
|
||||||
|
saveBtn.onclick = async () => await this.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Скрыть все секции
|
||||||
|
*/
|
||||||
|
hideAllSections() {
|
||||||
|
hide($('sectionServices'));
|
||||||
|
hide($('sectionSites'));
|
||||||
|
hide($('sectionProxy'));
|
||||||
|
hide($('sectionSettings'));
|
||||||
|
hide($('sectionVAccessEditor'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Вернуться на главную
|
||||||
|
*/
|
||||||
|
backToMain() {
|
||||||
|
this.hideAllSections();
|
||||||
|
show($('sectionServices'));
|
||||||
|
show($('sectionSites'));
|
||||||
|
show($('sectionProxy'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Переключить вкладку
|
||||||
|
*/
|
||||||
|
switchTab(tab) {
|
||||||
|
const tabs = document.querySelectorAll('.vaccess-tab[data-tab]');
|
||||||
|
tabs.forEach(t => {
|
||||||
|
if (t.dataset.tab === tab) {
|
||||||
|
t.classList.add('active');
|
||||||
|
} else {
|
||||||
|
t.classList.remove('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (tab === 'rules') {
|
||||||
|
show($('vAccessRulesTab'));
|
||||||
|
hide($('vAccessHelpTab'));
|
||||||
|
} else {
|
||||||
|
hide($('vAccessRulesTab'));
|
||||||
|
show($('vAccessHelpTab'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Сохранить изменения
|
||||||
|
*/
|
||||||
|
async save() {
|
||||||
|
if (isWailsAvailable()) {
|
||||||
|
const config = { rules: this.vAccessRules };
|
||||||
|
const configJSON = JSON.stringify(config);
|
||||||
|
const result = await api.saveVAccessRules(this.vAccessHost, this.vAccessIsProxy, configJSON);
|
||||||
|
|
||||||
|
if (result.startsWith('Error')) {
|
||||||
|
notification.error(result, 2000);
|
||||||
|
} else {
|
||||||
|
notification.success('✅ Правила vAccess успешно сохранены', 1000);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Браузерный режим - просто показываем уведомление
|
||||||
|
notification.success('Данные сохранены (тестовый режим)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Отрисовать список правил
|
||||||
|
*/
|
||||||
|
renderRulesList() {
|
||||||
|
const tbody = $('vAccessTableBody');
|
||||||
|
const emptyState = $('vAccessEmpty');
|
||||||
|
const table = document.querySelector('.vaccess-table');
|
||||||
|
|
||||||
|
if (!tbody) return;
|
||||||
|
|
||||||
|
// Показываем/скрываем пустое состояние
|
||||||
|
if (this.vAccessRules.length === 0) {
|
||||||
|
if (table) hide(table);
|
||||||
|
if (emptyState) show(emptyState);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
if (table) show(table);
|
||||||
|
if (emptyState) hide(emptyState);
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody.innerHTML = this.vAccessRules.map((rule, index) => `
|
||||||
|
<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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
586
Backend/admin/frontend/assets/js/main.js
Normal file
586
Backend/admin/frontend/assets/js/main.js
Normal file
@@ -0,0 +1,586 @@
|
|||||||
|
/* ============================================
|
||||||
|
vServer Admin Panel - Main Entry Point
|
||||||
|
Точка входа приложения
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
import { log, isWailsAvailable, sleep } from './utils/helpers.js';
|
||||||
|
import { WindowControls } from './ui/window.js';
|
||||||
|
import { Navigation } from './ui/navigation.js';
|
||||||
|
import { notification } from './ui/notification.js';
|
||||||
|
import { modal } from './ui/modal.js';
|
||||||
|
import { ServicesManager } from './components/services.js';
|
||||||
|
import { SitesManager } from './components/sites.js';
|
||||||
|
import { ProxyManager } from './components/proxy.js';
|
||||||
|
import { VAccessManager } from './components/vaccess.js';
|
||||||
|
import { configAPI } from './api/config.js';
|
||||||
|
import { $ } from './utils/dom.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Главный класс приложения
|
||||||
|
*/
|
||||||
|
class App {
|
||||||
|
constructor() {
|
||||||
|
this.windowControls = new WindowControls();
|
||||||
|
this.navigation = new Navigation();
|
||||||
|
this.servicesManager = new ServicesManager();
|
||||||
|
this.sitesManager = new SitesManager();
|
||||||
|
this.proxyManager = new ProxyManager();
|
||||||
|
this.vAccessManager = new VAccessManager();
|
||||||
|
|
||||||
|
this.isWails = isWailsAvailable();
|
||||||
|
|
||||||
|
log('Приложение инициализировано');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Запустить приложение
|
||||||
|
*/
|
||||||
|
async start() {
|
||||||
|
log('Запуск приложения...');
|
||||||
|
|
||||||
|
// Скрываем loader если не в Wails
|
||||||
|
if (!this.isWails) {
|
||||||
|
notification.hideLoader();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ждём немного перед загрузкой данных
|
||||||
|
await sleep(1000);
|
||||||
|
|
||||||
|
if (this.isWails) {
|
||||||
|
log('Wails API доступен', 'info');
|
||||||
|
} else {
|
||||||
|
log('Wails API недоступен (браузерный режим)', 'warn');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Загружаем начальные данные
|
||||||
|
await this.loadInitialData();
|
||||||
|
|
||||||
|
// Запускаем автообновление
|
||||||
|
this.startAutoRefresh();
|
||||||
|
|
||||||
|
// Скрываем loader после загрузки
|
||||||
|
if (this.isWails) {
|
||||||
|
notification.hideLoader();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Настраиваем глобальные функции для совместимости
|
||||||
|
this.setupGlobalHandlers();
|
||||||
|
|
||||||
|
// Привязываем кнопки
|
||||||
|
this.setupButtons();
|
||||||
|
|
||||||
|
log('Приложение запущено');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Загрузить начальные данные
|
||||||
|
*/
|
||||||
|
async loadInitialData() {
|
||||||
|
await Promise.all([
|
||||||
|
this.servicesManager.loadStatus(),
|
||||||
|
this.sitesManager.load(),
|
||||||
|
this.proxyManager.load()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Запустить автообновление
|
||||||
|
*/
|
||||||
|
startAutoRefresh() {
|
||||||
|
setInterval(async () => {
|
||||||
|
await this.loadInitialData();
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Привязать кнопки
|
||||||
|
*/
|
||||||
|
setupButtons() {
|
||||||
|
// Кнопка сохранения настроек
|
||||||
|
const saveSettingsBtn = $('saveSettingsBtn');
|
||||||
|
if (saveSettingsBtn) {
|
||||||
|
saveSettingsBtn.addEventListener('click', async () => {
|
||||||
|
await this.saveConfigSettings();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Кнопка сохранения vAccess (добавляем обработчик динамически при открытии vAccess)
|
||||||
|
// Обработчик будет добавлен в VAccessManager.open()
|
||||||
|
|
||||||
|
// Моментальное переключение Proxy без перезапуска
|
||||||
|
const proxyCheckbox = $('proxyEnabled');
|
||||||
|
if (proxyCheckbox) {
|
||||||
|
proxyCheckbox.addEventListener('change', async (e) => {
|
||||||
|
const isEnabled = e.target.checked;
|
||||||
|
|
||||||
|
if (isEnabled) {
|
||||||
|
await configAPI.enableProxyService();
|
||||||
|
notification.success('Proxy Manager включен', 1000);
|
||||||
|
} else {
|
||||||
|
await configAPI.disableProxyService();
|
||||||
|
notification.success('Proxy Manager отключен', 1000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Настроить глобальные обработчики
|
||||||
|
*/
|
||||||
|
setupGlobalHandlers() {
|
||||||
|
// Для vAccess
|
||||||
|
window.editVAccess = (host, isProxy) => {
|
||||||
|
this.vAccessManager.open(host, isProxy);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.backToMain = () => {
|
||||||
|
this.vAccessManager.backToMain();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.switchVAccessTab = (tab) => {
|
||||||
|
this.vAccessManager.switchTab(tab);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.saveVAccessChanges = async () => {
|
||||||
|
await this.vAccessManager.save();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addVAccessRule = () => {
|
||||||
|
this.vAccessManager.addRule();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Для Settings
|
||||||
|
window.loadConfig = async () => {
|
||||||
|
await this.loadConfigSettings();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.saveSettings = async () => {
|
||||||
|
await this.saveConfigSettings();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Для модальных окон
|
||||||
|
window.editSite = (index) => {
|
||||||
|
this.editSite(index);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.editProxy = (index) => {
|
||||||
|
this.editProxy(index);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.setStatus = (status) => {
|
||||||
|
this.setModalStatus(status);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.setProxyStatus = (status) => {
|
||||||
|
this.setModalStatus(status);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addAliasTag = () => {
|
||||||
|
this.addAliasTag();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.removeAliasTag = (btn) => {
|
||||||
|
btn.parentElement.remove();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.saveModalData = async () => {
|
||||||
|
await this.saveModalData();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Drag & Drop для vAccess
|
||||||
|
window.dragStart = (event, index) => {
|
||||||
|
this.vAccessManager.onDragStart(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.dragOver = (event) => {
|
||||||
|
this.vAccessManager.onDragOver(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.drop = (event, index) => {
|
||||||
|
this.vAccessManager.onDrop(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.editRuleField = (index, field) => {
|
||||||
|
this.vAccessManager.editRuleField(index, field);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.removeVAccessRule = (index) => {
|
||||||
|
this.vAccessManager.removeRule(index);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.closeFieldEditor = () => {
|
||||||
|
this.vAccessManager.closeFieldEditor();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addFieldValue = () => {
|
||||||
|
this.vAccessManager.addFieldValue();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.removeFieldValue = (value) => {
|
||||||
|
this.vAccessManager.removeFieldValue(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Тестовые функции (для браузерного режима)
|
||||||
|
window.editTestSite = (index) => {
|
||||||
|
const testSites = [
|
||||||
|
{name: 'Локальный сайт', host: '127.0.0.1', alias: ['localhost'], status: 'active', root_file: 'index.html', root_file_routing: true},
|
||||||
|
{name: 'Тестовый проект', host: 'test.local', alias: ['*.test.local', 'test.com'], status: 'active', root_file: 'index.php', root_file_routing: false},
|
||||||
|
{name: 'API сервис', host: 'api.example.com', alias: ['*.api.example.com'], status: 'inactive', root_file: 'index.php', root_file_routing: true}
|
||||||
|
];
|
||||||
|
this.sitesManager.sitesData = testSites;
|
||||||
|
this.editSite(index);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.editTestProxy = (index) => {
|
||||||
|
const testProxies = [
|
||||||
|
{enable: true, external_domain: 'git.example.ru', local_address: '127.0.0.1', local_port: '3333', service_https_use: false, auto_https: true},
|
||||||
|
{enable: true, external_domain: 'api.example.com', local_address: '127.0.0.1', local_port: '8080', service_https_use: true, auto_https: false},
|
||||||
|
{enable: false, external_domain: 'test.example.net', local_address: '127.0.0.1', local_port: '5000', service_https_use: false, auto_https: false}
|
||||||
|
];
|
||||||
|
this.proxyManager.proxiesData = testProxies;
|
||||||
|
this.editProxy(index);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.openTestLink = (url) => {
|
||||||
|
this.sitesManager.openLink(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.openSiteFolder = async (host) => {
|
||||||
|
await this.sitesManager.handleAction('open-folder', { getAttribute: () => host });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Загрузить настройки конфигурации
|
||||||
|
*/
|
||||||
|
async loadConfigSettings() {
|
||||||
|
if (!isWailsAvailable()) {
|
||||||
|
// Тестовые данные для браузерного режима
|
||||||
|
$('mysqlHost').value = '127.0.0.1';
|
||||||
|
$('mysqlPort').value = 3306;
|
||||||
|
$('phpHost').value = 'localhost';
|
||||||
|
$('phpPort').value = 8000;
|
||||||
|
$('proxyEnabled').checked = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = await configAPI.getConfig();
|
||||||
|
if (!config) return;
|
||||||
|
|
||||||
|
$('mysqlHost').value = config.Soft_Settings?.mysql_host || '127.0.0.1';
|
||||||
|
$('mysqlPort').value = config.Soft_Settings?.mysql_port || 3306;
|
||||||
|
$('phpHost').value = config.Soft_Settings?.php_host || 'localhost';
|
||||||
|
$('phpPort').value = config.Soft_Settings?.php_port || 8000;
|
||||||
|
$('proxyEnabled').checked = config.Soft_Settings?.proxy_enabled !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Сохранить настройки конфигурации
|
||||||
|
*/
|
||||||
|
async saveConfigSettings() {
|
||||||
|
const saveBtn = $('saveSettingsBtn');
|
||||||
|
const originalText = saveBtn.querySelector('span').textContent;
|
||||||
|
|
||||||
|
if (!isWailsAvailable()) {
|
||||||
|
notification.success('Настройки сохранены (тестовый режим)', 1000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
saveBtn.disabled = true;
|
||||||
|
saveBtn.querySelector('span').textContent = 'Сохранение...';
|
||||||
|
|
||||||
|
const config = await configAPI.getConfig();
|
||||||
|
config.Soft_Settings.mysql_host = $('mysqlHost').value;
|
||||||
|
config.Soft_Settings.mysql_port = parseInt($('mysqlPort').value);
|
||||||
|
config.Soft_Settings.php_host = $('phpHost').value;
|
||||||
|
config.Soft_Settings.php_port = parseInt($('phpPort').value);
|
||||||
|
config.Soft_Settings.proxy_enabled = $('proxyEnabled').checked;
|
||||||
|
|
||||||
|
const configJSON = JSON.stringify(config, null, 4);
|
||||||
|
const result = await configAPI.saveConfig(configJSON);
|
||||||
|
|
||||||
|
if (result.startsWith('Error')) {
|
||||||
|
notification.error(result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveBtn.querySelector('span').textContent = 'Перезапуск сервисов...';
|
||||||
|
await configAPI.restartAllServices();
|
||||||
|
|
||||||
|
notification.success('Настройки сохранены и сервисы перезапущены!', 1500);
|
||||||
|
} catch (error) {
|
||||||
|
notification.error('Ошибка: ' + error.message);
|
||||||
|
} finally {
|
||||||
|
saveBtn.disabled = false;
|
||||||
|
saveBtn.querySelector('span').textContent = originalText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Редактировать сайт
|
||||||
|
*/
|
||||||
|
editSite(index) {
|
||||||
|
const site = this.sitesManager.sitesData[index];
|
||||||
|
if (!site) return;
|
||||||
|
|
||||||
|
const content = `
|
||||||
|
<div class="settings-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Статус сайта:</label>
|
||||||
|
<div class="status-toggle">
|
||||||
|
<button class="status-btn ${site.status === 'active' ? 'active' : ''}" onclick="setStatus('active')" data-value="active">
|
||||||
|
<i class="fas fa-check-circle"></i> Active
|
||||||
|
</button>
|
||||||
|
<button class="status-btn ${site.status === 'inactive' ? 'active' : ''}" onclick="setStatus('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" value="${site.name}">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Host:</label>
|
||||||
|
<input type="text" class="form-input" id="editHost" value="${site.host}">
|
||||||
|
</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">
|
||||||
|
${site.alias.map(alias => `
|
||||||
|
<span class="tag">
|
||||||
|
${alias}
|
||||||
|
<button class="tag-remove" onclick="removeAliasTag(this)"><i class="fas fa-times"></i></button>
|
||||||
|
</span>
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Root файл:</label>
|
||||||
|
<input type="text" class="form-input" id="editRootFile" value="${site.root_file}">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Роутинг:</label>
|
||||||
|
<div class="toggle-wrapper">
|
||||||
|
<label class="toggle-switch">
|
||||||
|
<input type="checkbox" id="editRouting" ${site.root_file_routing ? 'checked' : ''}>
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
</label>
|
||||||
|
<span class="toggle-label">Включён</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
modal.open('Редактировать сайт', content);
|
||||||
|
window.currentEditType = 'site';
|
||||||
|
window.currentEditIndex = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Редактировать прокси
|
||||||
|
*/
|
||||||
|
editProxy(index) {
|
||||||
|
const proxy = this.proxyManager.proxiesData[index];
|
||||||
|
if (!proxy) return;
|
||||||
|
|
||||||
|
const content = `
|
||||||
|
<div class="settings-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Статус прокси:</label>
|
||||||
|
<div class="status-toggle">
|
||||||
|
<button class="status-btn ${proxy.enable ? 'active' : ''}" onclick="setProxyStatus('enable')" data-value="enable">
|
||||||
|
<i class="fas fa-check-circle"></i> Включён
|
||||||
|
</button>
|
||||||
|
<button class="status-btn ${!proxy.enable ? 'active' : ''}" onclick="setProxyStatus('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" value="${proxy.external_domain}">
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Локальный адрес:</label>
|
||||||
|
<input type="text" class="form-input" id="editLocalAddr" value="${proxy.local_address}">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Локальный порт:</label>
|
||||||
|
<input type="text" class="form-input" id="editLocalPort" value="${proxy.local_port}">
|
||||||
|
</div>
|
||||||
|
</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="editServiceHTTPS" ${proxy.service_https_use ? 'checked' : ''}>
|
||||||
|
<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" ${proxy.auto_https ? 'checked' : ''}>
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
</label>
|
||||||
|
<span class="toggle-label">Включён</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
modal.open('Редактировать прокси', content);
|
||||||
|
window.currentEditType = 'proxy';
|
||||||
|
window.currentEditIndex = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Установить статус в модальном окне
|
||||||
|
*/
|
||||||
|
setModalStatus(status) {
|
||||||
|
const buttons = document.querySelectorAll('.status-btn');
|
||||||
|
buttons.forEach(btn => {
|
||||||
|
btn.classList.remove('active');
|
||||||
|
if (btn.dataset.value === status) {
|
||||||
|
btn.classList.add('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Добавить alias tag
|
||||||
|
*/
|
||||||
|
addAliasTag() {
|
||||||
|
const input = $('editAliasInput');
|
||||||
|
const value = input?.value.trim();
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
const container = $('aliasTagsContainer');
|
||||||
|
const tag = document.createElement('span');
|
||||||
|
tag.className = 'tag';
|
||||||
|
tag.innerHTML = `
|
||||||
|
${value}
|
||||||
|
<button class="tag-remove" onclick="removeAliasTag(this)"><i class="fas fa-times"></i></button>
|
||||||
|
`;
|
||||||
|
container.appendChild(tag);
|
||||||
|
input.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Сохранить данные модального окна
|
||||||
|
*/
|
||||||
|
async saveModalData() {
|
||||||
|
if (!isWailsAvailable()) {
|
||||||
|
notification.success('Данные сохранены (тестовый режим)', 1000);
|
||||||
|
modal.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.currentEditType === 'site') {
|
||||||
|
await this.saveSiteData();
|
||||||
|
} else if (window.currentEditType === 'proxy') {
|
||||||
|
await this.saveProxyData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Сохранить данные сайта
|
||||||
|
*/
|
||||||
|
async saveSiteData() {
|
||||||
|
const index = window.currentEditIndex;
|
||||||
|
const tags = document.querySelectorAll('#aliasTagsContainer .tag');
|
||||||
|
const aliases = Array.from(tags).map(tag => tag.textContent.trim());
|
||||||
|
const statusBtn = document.querySelector('.status-btn.active');
|
||||||
|
|
||||||
|
const config = await configAPI.getConfig();
|
||||||
|
config.Site_www[index] = {
|
||||||
|
name: $('editName').value,
|
||||||
|
host: $('editHost').value,
|
||||||
|
alias: aliases,
|
||||||
|
status: statusBtn ? statusBtn.dataset.value : 'active',
|
||||||
|
root_file: $('editRootFile').value,
|
||||||
|
root_file_routing: $('editRouting').checked
|
||||||
|
};
|
||||||
|
|
||||||
|
const configJSON = JSON.stringify(config, null, 4);
|
||||||
|
const result = await configAPI.saveConfig(configJSON);
|
||||||
|
|
||||||
|
if (result.startsWith('Error')) {
|
||||||
|
notification.error(result);
|
||||||
|
} else {
|
||||||
|
notification.show('Перезапуск HTTP/HTTPS...', 'success', 800);
|
||||||
|
|
||||||
|
await configAPI.stopHTTPService();
|
||||||
|
await configAPI.stopHTTPSService();
|
||||||
|
await sleep(500);
|
||||||
|
await configAPI.startHTTPService();
|
||||||
|
await configAPI.startHTTPSService();
|
||||||
|
|
||||||
|
notification.success('Изменения сохранены и применены!', 1000);
|
||||||
|
await this.sitesManager.load();
|
||||||
|
modal.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Сохранить данные прокси
|
||||||
|
*/
|
||||||
|
async saveProxyData() {
|
||||||
|
const index = window.currentEditIndex;
|
||||||
|
const statusBtn = document.querySelector('.status-btn.active');
|
||||||
|
const isEnabled = statusBtn && statusBtn.dataset.value === 'enable';
|
||||||
|
|
||||||
|
const config = await configAPI.getConfig();
|
||||||
|
config.Proxy_Service[index] = {
|
||||||
|
Enable: isEnabled,
|
||||||
|
ExternalDomain: $('editDomain').value,
|
||||||
|
LocalAddress: $('editLocalAddr').value,
|
||||||
|
LocalPort: $('editLocalPort').value,
|
||||||
|
ServiceHTTPSuse: $('editServiceHTTPS').checked,
|
||||||
|
AutoHTTPS: $('editAutoHTTPS').checked
|
||||||
|
};
|
||||||
|
|
||||||
|
const configJSON = JSON.stringify(config, null, 4);
|
||||||
|
const result = await configAPI.saveConfig(configJSON);
|
||||||
|
|
||||||
|
if (result.startsWith('Error')) {
|
||||||
|
notification.error(result);
|
||||||
|
} else {
|
||||||
|
notification.show('Перезапуск HTTP/HTTPS...', 'success', 800);
|
||||||
|
|
||||||
|
await configAPI.stopHTTPService();
|
||||||
|
await configAPI.stopHTTPSService();
|
||||||
|
await sleep(500);
|
||||||
|
await configAPI.startHTTPService();
|
||||||
|
await configAPI.startHTTPSService();
|
||||||
|
|
||||||
|
notification.success('Изменения сохранены и применены!', 1000);
|
||||||
|
await this.proxyManager.load();
|
||||||
|
modal.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Инициализация приложения при загрузке DOM
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const app = new App();
|
||||||
|
app.start();
|
||||||
|
});
|
||||||
|
|
||||||
|
log('vServer Admin Panel загружен');
|
||||||
|
|
||||||
101
Backend/admin/frontend/assets/js/ui/modal.js
Normal file
101
Backend/admin/frontend/assets/js/ui/modal.js
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
/* ============================================
|
||||||
|
Modal Manager
|
||||||
|
Управление модальными окнами
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
import { $, addClass, removeClass } from '../utils/dom.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Класс для управления модальными окнами
|
||||||
|
*/
|
||||||
|
export class Modal {
|
||||||
|
constructor() {
|
||||||
|
this.overlay = $('modalOverlay');
|
||||||
|
this.title = $('modalTitle');
|
||||||
|
this.content = $('modalContent');
|
||||||
|
this.closeBtn = $('modalCloseBtn');
|
||||||
|
this.cancelBtn = $('modalCancelBtn');
|
||||||
|
this.saveBtn = $('modalSaveBtn');
|
||||||
|
this.fieldEditorOverlay = $('fieldEditorOverlay');
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
if (this.closeBtn) {
|
||||||
|
this.closeBtn.addEventListener('click', () => this.close());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.cancelBtn) {
|
||||||
|
this.cancelBtn.addEventListener('click', () => this.close());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.saveBtn) {
|
||||||
|
this.saveBtn.addEventListener('click', () => {
|
||||||
|
if (window.saveModalData) {
|
||||||
|
window.saveModalData();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.overlay) {
|
||||||
|
this.overlay.addEventListener('click', (e) => {
|
||||||
|
if (e.target === this.overlay) {
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Открыть модальное окно
|
||||||
|
* @param {string} title - Заголовок
|
||||||
|
* @param {string} htmlContent - HTML контент
|
||||||
|
*/
|
||||||
|
open(title, htmlContent) {
|
||||||
|
if (this.title) this.title.textContent = title;
|
||||||
|
if (this.content) this.content.innerHTML = htmlContent;
|
||||||
|
if (this.overlay) addClass(this.overlay, 'show');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Закрыть модальное окно
|
||||||
|
*/
|
||||||
|
close() {
|
||||||
|
if (this.overlay) removeClass(this.overlay, 'show');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Установить обработчик сохранения
|
||||||
|
* @param {Function} callback - Функция обратного вызова
|
||||||
|
*/
|
||||||
|
onSave(callback) {
|
||||||
|
if (this.saveBtn) {
|
||||||
|
this.saveBtn.onclick = callback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Открыть редактор поля
|
||||||
|
* @param {string} title - Заголовок
|
||||||
|
* @param {string} htmlContent - HTML контент
|
||||||
|
*/
|
||||||
|
openFieldEditor(title, htmlContent) {
|
||||||
|
const fieldTitle = $('fieldEditorTitle');
|
||||||
|
const fieldContent = $('fieldEditorContent');
|
||||||
|
|
||||||
|
if (fieldTitle) fieldTitle.textContent = title;
|
||||||
|
if (fieldContent) fieldContent.innerHTML = htmlContent;
|
||||||
|
if (this.fieldEditorOverlay) addClass(this.fieldEditorOverlay, 'show');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Закрыть редактор поля
|
||||||
|
*/
|
||||||
|
closeFieldEditor() {
|
||||||
|
if (this.fieldEditorOverlay) removeClass(this.fieldEditorOverlay, 'show');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Глобальный экземпляр модального окна
|
||||||
|
export const modal = new Modal();
|
||||||
|
|
||||||
67
Backend/admin/frontend/assets/js/ui/navigation.js
Normal file
67
Backend/admin/frontend/assets/js/ui/navigation.js
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
/* ============================================
|
||||||
|
Navigation
|
||||||
|
Управление навигацией
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
import { $, $$, hide, show, removeClass, addClass } from '../utils/dom.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Класс для управления навигацией
|
||||||
|
*/
|
||||||
|
export class Navigation {
|
||||||
|
constructor() {
|
||||||
|
this.navItems = $$('.nav-item');
|
||||||
|
this.sections = {
|
||||||
|
services: $('sectionServices'),
|
||||||
|
sites: $('sectionSites'),
|
||||||
|
proxy: $('sectionProxy'),
|
||||||
|
settings: $('sectionSettings'),
|
||||||
|
vaccess: $('sectionVAccessEditor')
|
||||||
|
};
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.navItems.forEach((item, index) => {
|
||||||
|
item.addEventListener('click', () => this.navigate(index));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
navigate(index) {
|
||||||
|
// Убираем active со всех навигационных элементов
|
||||||
|
this.navItems.forEach(nav => removeClass(nav, 'active'));
|
||||||
|
addClass(this.navItems[index], 'active');
|
||||||
|
|
||||||
|
// Скрываем все секции
|
||||||
|
this.hideAllSections();
|
||||||
|
|
||||||
|
// Показываем нужные секции
|
||||||
|
if (index === 0) {
|
||||||
|
// Главная - всё кроме настроек
|
||||||
|
show(this.sections.services);
|
||||||
|
show(this.sections.sites);
|
||||||
|
show(this.sections.proxy);
|
||||||
|
} else if (index === 3) {
|
||||||
|
// Настройки
|
||||||
|
show(this.sections.settings);
|
||||||
|
// Загружаем конфигурацию при открытии
|
||||||
|
if (window.loadConfig) {
|
||||||
|
window.loadConfig();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hideAllSections() {
|
||||||
|
Object.values(this.sections).forEach(section => {
|
||||||
|
if (section) hide(section);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
showDashboard() {
|
||||||
|
this.hideAllSections();
|
||||||
|
show(this.sections.services);
|
||||||
|
show(this.sections.sites);
|
||||||
|
show(this.sections.proxy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
81
Backend/admin/frontend/assets/js/ui/notification.js
Normal file
81
Backend/admin/frontend/assets/js/ui/notification.js
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
/* ============================================
|
||||||
|
Notification System
|
||||||
|
Система уведомлений
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
import { $, addClass, removeClass } from '../utils/dom.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Класс для управления уведомлениями
|
||||||
|
*/
|
||||||
|
export class NotificationManager {
|
||||||
|
constructor() {
|
||||||
|
this.container = $('notification');
|
||||||
|
this.loader = $('appLoader');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Показать уведомление
|
||||||
|
* @param {string} message - Текст сообщения
|
||||||
|
* @param {string} type - Тип (success, error)
|
||||||
|
* @param {number} duration - Длительность показа (мс)
|
||||||
|
*/
|
||||||
|
show(message, type = 'success', duration = 1000) {
|
||||||
|
if (!this.container) return;
|
||||||
|
|
||||||
|
const icon = type === 'success'
|
||||||
|
? '<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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Показать успешное уведомление
|
||||||
|
* @param {string} message - Текст сообщения
|
||||||
|
* @param {number} duration - Длительность
|
||||||
|
*/
|
||||||
|
success(message, duration = 1000) {
|
||||||
|
this.show(message, 'success', duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Показать уведомление об ошибке
|
||||||
|
* @param {string} message - Текст сообщения
|
||||||
|
* @param {number} duration - Длительность
|
||||||
|
*/
|
||||||
|
error(message, duration = 2000) {
|
||||||
|
this.show(message, 'error', duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Скрыть загрузчик приложения
|
||||||
|
*/
|
||||||
|
hideLoader() {
|
||||||
|
if (!this.loader) return;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
addClass(this.loader, 'hide');
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.loader.parentNode) {
|
||||||
|
this.loader.remove();
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
}, 1500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Глобальный экземпляр менеджера уведомлений
|
||||||
|
export const notification = new NotificationManager();
|
||||||
|
|
||||||
51
Backend/admin/frontend/assets/js/ui/window.js
Normal file
51
Backend/admin/frontend/assets/js/ui/window.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
/* ============================================
|
||||||
|
Window Controls
|
||||||
|
Управление окном приложения
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
import { $, addClass } from '../utils/dom.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Класс для управления окном
|
||||||
|
*/
|
||||||
|
export class WindowControls {
|
||||||
|
constructor() {
|
||||||
|
this.minimizeBtn = $('minimizeBtn');
|
||||||
|
this.maximizeBtn = $('maximizeBtn');
|
||||||
|
this.closeBtn = $('closeBtn');
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
if (this.minimizeBtn) {
|
||||||
|
this.minimizeBtn.addEventListener('click', () => this.minimize());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.maximizeBtn) {
|
||||||
|
this.maximizeBtn.addEventListener('click', () => this.maximize());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.closeBtn) {
|
||||||
|
this.closeBtn.addEventListener('click', () => this.close());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
minimize() {
|
||||||
|
if (window.runtime?.WindowMinimise) {
|
||||||
|
window.runtime.WindowMinimise();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
maximize() {
|
||||||
|
if (window.runtime?.WindowToggleMaximise) {
|
||||||
|
window.runtime.WindowToggleMaximise();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
if (window.runtime?.Quit) {
|
||||||
|
window.runtime.Quit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
83
Backend/admin/frontend/assets/js/utils/dom.js
Normal file
83
Backend/admin/frontend/assets/js/utils/dom.js
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
/* ============================================
|
||||||
|
DOM Utilities
|
||||||
|
Утилиты для работы с DOM
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить элемент по ID
|
||||||
|
* @param {string} id - ID элемента
|
||||||
|
* @returns {HTMLElement|null}
|
||||||
|
*/
|
||||||
|
export function $(id) {
|
||||||
|
return document.getElementById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить все элементы по селектору
|
||||||
|
* @param {string} selector - CSS селектор
|
||||||
|
* @param {HTMLElement} parent - Родительский элемент
|
||||||
|
* @returns {NodeList}
|
||||||
|
*/
|
||||||
|
export function $$(selector, parent = document) {
|
||||||
|
return parent.querySelectorAll(selector);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Показать элемент
|
||||||
|
* @param {HTMLElement|string} element - Элемент или ID
|
||||||
|
*/
|
||||||
|
export function show(element) {
|
||||||
|
const el = typeof element === 'string' ? $(element) : element;
|
||||||
|
if (el) el.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Скрыть элемент
|
||||||
|
* @param {HTMLElement|string} element - Элемент или ID
|
||||||
|
*/
|
||||||
|
export function hide(element) {
|
||||||
|
const el = typeof element === 'string' ? $(element) : element;
|
||||||
|
if (el) el.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Переключить видимость элемента
|
||||||
|
* @param {HTMLElement|string} element - Элемент или ID
|
||||||
|
*/
|
||||||
|
export function toggle(element) {
|
||||||
|
const el = typeof element === 'string' ? $(element) : element;
|
||||||
|
if (el) {
|
||||||
|
el.style.display = el.style.display === 'none' ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Добавить класс
|
||||||
|
* @param {HTMLElement|string} element - Элемент или ID
|
||||||
|
* @param {string} className - Имя класса
|
||||||
|
*/
|
||||||
|
export function addClass(element, className) {
|
||||||
|
const el = typeof element === 'string' ? $(element) : element;
|
||||||
|
if (el) el.classList.add(className);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Удалить класс
|
||||||
|
* @param {HTMLElement|string} element - Элемент или ID
|
||||||
|
* @param {string} className - Имя класса
|
||||||
|
*/
|
||||||
|
export function removeClass(element, className) {
|
||||||
|
const el = typeof element === 'string' ? $(element) : element;
|
||||||
|
if (el) el.classList.remove(className);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Переключить класс
|
||||||
|
* @param {HTMLElement|string} element - Элемент или ID
|
||||||
|
* @param {string} className - Имя класса
|
||||||
|
*/
|
||||||
|
export function toggleClass(element, className) {
|
||||||
|
const el = typeof element === 'string' ? $(element) : element;
|
||||||
|
if (el) el.classList.toggle(className);
|
||||||
|
}
|
||||||
|
|
||||||
57
Backend/admin/frontend/assets/js/utils/helpers.js
Normal file
57
Backend/admin/frontend/assets/js/utils/helpers.js
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
/* ============================================
|
||||||
|
Helper Utilities
|
||||||
|
Вспомогательные функции
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ждёт указанное время
|
||||||
|
* @param {number} ms - Миллисекунды
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
export function sleep(ms) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Debounce функция
|
||||||
|
* @param {Function} func - Функция для debounce
|
||||||
|
* @param {number} wait - Время задержки
|
||||||
|
* @returns {Function}
|
||||||
|
*/
|
||||||
|
export function debounce(func, wait) {
|
||||||
|
let timeout;
|
||||||
|
return function executedFunction(...args) {
|
||||||
|
const later = () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
func(...args);
|
||||||
|
};
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(later, wait);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет доступность Wails API
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export function isWailsAvailable() {
|
||||||
|
return typeof window.go !== 'undefined' &&
|
||||||
|
window.go?.admin?.App !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Логирование с префиксом
|
||||||
|
* @param {string} message - Сообщение
|
||||||
|
* @param {string} type - Тип (log, error, warn, info)
|
||||||
|
*/
|
||||||
|
export function log(message, type = 'log') {
|
||||||
|
const prefix = '🚀 vServer:';
|
||||||
|
const styles = {
|
||||||
|
log: '✅',
|
||||||
|
error: '❌',
|
||||||
|
warn: '⚠️',
|
||||||
|
info: 'ℹ️'
|
||||||
|
};
|
||||||
|
console[type](`${prefix} ${styles[type]} ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
540
Backend/admin/frontend/index.html
Normal file
540
Backend/admin/frontend/index.html
Normal file
@@ -0,0 +1,540 @@
|
|||||||
|
<!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" title="Главная">
|
||||||
|
<i class="fas fa-home"></i>
|
||||||
|
</button>
|
||||||
|
<button class="nav-item" title="Статистика">
|
||||||
|
<i class="fas fa-chart-bar"></i>
|
||||||
|
</button>
|
||||||
|
<button class="nav-item" title="Сервисы">
|
||||||
|
<i class="fas fa-server"></i>
|
||||||
|
</button>
|
||||||
|
<button class="nav-item" 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">
|
||||||
|
<h2 class="section-title">Список сайтов</h2>
|
||||||
|
<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">
|
||||||
|
<h2 class="section-title">Прокси сервисы</h2>
|
||||||
|
<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>
|
||||||
|
</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>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
17
Backend/admin/frontend/wailsjs/go/admin/App.d.ts
vendored
Normal file
17
Backend/admin/frontend/wailsjs/go/admin/App.d.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
import {services} from '../models';
|
||||||
|
import {proxy} from '../models';
|
||||||
|
import {sites} from '../models';
|
||||||
|
|
||||||
|
export function CheckServicesReady():Promise<boolean>;
|
||||||
|
|
||||||
|
export function GetAllServicesStatus():Promise<services.AllServicesStatus>;
|
||||||
|
|
||||||
|
export function GetProxyList():Promise<Array<proxy.ProxyInfo>>;
|
||||||
|
|
||||||
|
export function GetSitesList():Promise<Array<sites.SiteInfo>>;
|
||||||
|
|
||||||
|
export function StartServer():Promise<string>;
|
||||||
|
|
||||||
|
export function StopServer():Promise<string>;
|
||||||
27
Backend/admin/frontend/wailsjs/go/admin/App.js
Normal file
27
Backend/admin/frontend/wailsjs/go/admin/App.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
// @ts-check
|
||||||
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
|
||||||
|
export function CheckServicesReady() {
|
||||||
|
return window['go']['admin']['App']['CheckServicesReady']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GetAllServicesStatus() {
|
||||||
|
return window['go']['admin']['App']['GetAllServicesStatus']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GetProxyList() {
|
||||||
|
return window['go']['admin']['App']['GetProxyList']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GetSitesList() {
|
||||||
|
return window['go']['admin']['App']['GetSitesList']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StartServer() {
|
||||||
|
return window['go']['admin']['App']['StartServer']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StopServer() {
|
||||||
|
return window['go']['admin']['App']['StopServer']();
|
||||||
|
}
|
||||||
119
Backend/admin/frontend/wailsjs/go/models.ts
Normal file
119
Backend/admin/frontend/wailsjs/go/models.ts
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
export namespace proxy {
|
||||||
|
|
||||||
|
export class ProxyInfo {
|
||||||
|
enable: boolean;
|
||||||
|
external_domain: string;
|
||||||
|
local_address: string;
|
||||||
|
local_port: string;
|
||||||
|
service_https_use: boolean;
|
||||||
|
auto_https: boolean;
|
||||||
|
status: string;
|
||||||
|
|
||||||
|
static createFrom(source: any = {}) {
|
||||||
|
return new ProxyInfo(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(source: any = {}) {
|
||||||
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
|
this.enable = source["enable"];
|
||||||
|
this.external_domain = source["external_domain"];
|
||||||
|
this.local_address = source["local_address"];
|
||||||
|
this.local_port = source["local_port"];
|
||||||
|
this.service_https_use = source["service_https_use"];
|
||||||
|
this.auto_https = source["auto_https"];
|
||||||
|
this.status = source["status"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace services {
|
||||||
|
|
||||||
|
export class ServiceStatus {
|
||||||
|
name: string;
|
||||||
|
status: boolean;
|
||||||
|
port: string;
|
||||||
|
requests: number;
|
||||||
|
info: string;
|
||||||
|
|
||||||
|
static createFrom(source: any = {}) {
|
||||||
|
return new ServiceStatus(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(source: any = {}) {
|
||||||
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
|
this.name = source["name"];
|
||||||
|
this.status = source["status"];
|
||||||
|
this.port = source["port"];
|
||||||
|
this.requests = source["requests"];
|
||||||
|
this.info = source["info"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class AllServicesStatus {
|
||||||
|
http: ServiceStatus;
|
||||||
|
https: ServiceStatus;
|
||||||
|
mysql: ServiceStatus;
|
||||||
|
php: ServiceStatus;
|
||||||
|
proxy: ServiceStatus;
|
||||||
|
|
||||||
|
static createFrom(source: any = {}) {
|
||||||
|
return new AllServicesStatus(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(source: any = {}) {
|
||||||
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
|
this.http = this.convertValues(source["http"], ServiceStatus);
|
||||||
|
this.https = this.convertValues(source["https"], ServiceStatus);
|
||||||
|
this.mysql = this.convertValues(source["mysql"], ServiceStatus);
|
||||||
|
this.php = this.convertValues(source["php"], ServiceStatus);
|
||||||
|
this.proxy = this.convertValues(source["proxy"], ServiceStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||||
|
if (!a) {
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
if (a.slice && a.map) {
|
||||||
|
return (a as any[]).map(elem => this.convertValues(elem, classs));
|
||||||
|
} else if ("object" === typeof a) {
|
||||||
|
if (asMap) {
|
||||||
|
for (const key of Object.keys(a)) {
|
||||||
|
a[key] = new classs(a[key]);
|
||||||
|
}
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
return new classs(a);
|
||||||
|
}
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace sites {
|
||||||
|
|
||||||
|
export class SiteInfo {
|
||||||
|
name: string;
|
||||||
|
host: string;
|
||||||
|
alias: string[];
|
||||||
|
status: string;
|
||||||
|
root_file: string;
|
||||||
|
root_file_routing: boolean;
|
||||||
|
|
||||||
|
static createFrom(source: any = {}) {
|
||||||
|
return new SiteInfo(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(source: any = {}) {
|
||||||
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
|
this.name = source["name"];
|
||||||
|
this.host = source["host"];
|
||||||
|
this.alias = source["alias"];
|
||||||
|
this.status = source["status"];
|
||||||
|
this.root_file = source["root_file"];
|
||||||
|
this.root_file_routing = source["root_file_routing"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
24
Backend/admin/frontend/wailsjs/runtime/package.json
Normal file
24
Backend/admin/frontend/wailsjs/runtime/package.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "@wailsapp/runtime",
|
||||||
|
"version": "2.0.0",
|
||||||
|
"description": "Wails Javascript runtime library",
|
||||||
|
"main": "runtime.js",
|
||||||
|
"types": "runtime.d.ts",
|
||||||
|
"scripts": {
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/wailsapp/wails.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"Wails",
|
||||||
|
"Javascript",
|
||||||
|
"Go"
|
||||||
|
],
|
||||||
|
"author": "Lea Anthony <lea.anthony@gmail.com>",
|
||||||
|
"license": "MIT",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/wailsapp/wails/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/wailsapp/wails#readme"
|
||||||
|
}
|
||||||
249
Backend/admin/frontend/wailsjs/runtime/runtime.d.ts
vendored
Normal file
249
Backend/admin/frontend/wailsjs/runtime/runtime.d.ts
vendored
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
/*
|
||||||
|
_ __ _ __
|
||||||
|
| | / /___ _(_) /____
|
||||||
|
| | /| / / __ `/ / / ___/
|
||||||
|
| |/ |/ / /_/ / / (__ )
|
||||||
|
|__/|__/\__,_/_/_/____/
|
||||||
|
The electron alternative for Go
|
||||||
|
(c) Lea Anthony 2019-present
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface Position {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Size {
|
||||||
|
w: number;
|
||||||
|
h: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Screen {
|
||||||
|
isCurrent: boolean;
|
||||||
|
isPrimary: boolean;
|
||||||
|
width : number
|
||||||
|
height : number
|
||||||
|
}
|
||||||
|
|
||||||
|
// Environment information such as platform, buildtype, ...
|
||||||
|
export interface EnvironmentInfo {
|
||||||
|
buildType: string;
|
||||||
|
platform: string;
|
||||||
|
arch: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit)
|
||||||
|
// emits the given event. Optional data may be passed with the event.
|
||||||
|
// This will trigger any event listeners.
|
||||||
|
export function EventsEmit(eventName: string, ...data: any): void;
|
||||||
|
|
||||||
|
// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name.
|
||||||
|
export function EventsOn(eventName: string, callback: (...data: any) => void): () => void;
|
||||||
|
|
||||||
|
// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple)
|
||||||
|
// sets up a listener for the given event name, but will only trigger a given number times.
|
||||||
|
export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void;
|
||||||
|
|
||||||
|
// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce)
|
||||||
|
// sets up a listener for the given event name, but will only trigger once.
|
||||||
|
export function EventsOnce(eventName: string, callback: (...data: any) => void): () => void;
|
||||||
|
|
||||||
|
// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff)
|
||||||
|
// unregisters the listener for the given event name.
|
||||||
|
export function EventsOff(eventName: string, ...additionalEventNames: string[]): void;
|
||||||
|
|
||||||
|
// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall)
|
||||||
|
// unregisters all listeners.
|
||||||
|
export function EventsOffAll(): void;
|
||||||
|
|
||||||
|
// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint)
|
||||||
|
// logs the given message as a raw message
|
||||||
|
export function LogPrint(message: string): void;
|
||||||
|
|
||||||
|
// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace)
|
||||||
|
// logs the given message at the `trace` log level.
|
||||||
|
export function LogTrace(message: string): void;
|
||||||
|
|
||||||
|
// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug)
|
||||||
|
// logs the given message at the `debug` log level.
|
||||||
|
export function LogDebug(message: string): void;
|
||||||
|
|
||||||
|
// [LogError](https://wails.io/docs/reference/runtime/log#logerror)
|
||||||
|
// logs the given message at the `error` log level.
|
||||||
|
export function LogError(message: string): void;
|
||||||
|
|
||||||
|
// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal)
|
||||||
|
// logs the given message at the `fatal` log level.
|
||||||
|
// The application will quit after calling this method.
|
||||||
|
export function LogFatal(message: string): void;
|
||||||
|
|
||||||
|
// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo)
|
||||||
|
// logs the given message at the `info` log level.
|
||||||
|
export function LogInfo(message: string): void;
|
||||||
|
|
||||||
|
// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning)
|
||||||
|
// logs the given message at the `warning` log level.
|
||||||
|
export function LogWarning(message: string): void;
|
||||||
|
|
||||||
|
// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload)
|
||||||
|
// Forces a reload by the main application as well as connected browsers.
|
||||||
|
export function WindowReload(): void;
|
||||||
|
|
||||||
|
// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp)
|
||||||
|
// Reloads the application frontend.
|
||||||
|
export function WindowReloadApp(): void;
|
||||||
|
|
||||||
|
// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop)
|
||||||
|
// Sets the window AlwaysOnTop or not on top.
|
||||||
|
export function WindowSetAlwaysOnTop(b: boolean): void;
|
||||||
|
|
||||||
|
// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme)
|
||||||
|
// *Windows only*
|
||||||
|
// Sets window theme to system default (dark/light).
|
||||||
|
export function WindowSetSystemDefaultTheme(): void;
|
||||||
|
|
||||||
|
// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme)
|
||||||
|
// *Windows only*
|
||||||
|
// Sets window to light theme.
|
||||||
|
export function WindowSetLightTheme(): void;
|
||||||
|
|
||||||
|
// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme)
|
||||||
|
// *Windows only*
|
||||||
|
// Sets window to dark theme.
|
||||||
|
export function WindowSetDarkTheme(): void;
|
||||||
|
|
||||||
|
// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter)
|
||||||
|
// Centers the window on the monitor the window is currently on.
|
||||||
|
export function WindowCenter(): void;
|
||||||
|
|
||||||
|
// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle)
|
||||||
|
// Sets the text in the window title bar.
|
||||||
|
export function WindowSetTitle(title: string): void;
|
||||||
|
|
||||||
|
// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen)
|
||||||
|
// Makes the window full screen.
|
||||||
|
export function WindowFullscreen(): void;
|
||||||
|
|
||||||
|
// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen)
|
||||||
|
// Restores the previous window dimensions and position prior to full screen.
|
||||||
|
export function WindowUnfullscreen(): void;
|
||||||
|
|
||||||
|
// [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen)
|
||||||
|
// Returns the state of the window, i.e. whether the window is in full screen mode or not.
|
||||||
|
export function WindowIsFullscreen(): Promise<boolean>;
|
||||||
|
|
||||||
|
// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize)
|
||||||
|
// Sets the width and height of the window.
|
||||||
|
export function WindowSetSize(width: number, height: number): void;
|
||||||
|
|
||||||
|
// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize)
|
||||||
|
// Gets the width and height of the window.
|
||||||
|
export function WindowGetSize(): Promise<Size>;
|
||||||
|
|
||||||
|
// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize)
|
||||||
|
// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions.
|
||||||
|
// Setting a size of 0,0 will disable this constraint.
|
||||||
|
export function WindowSetMaxSize(width: number, height: number): void;
|
||||||
|
|
||||||
|
// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize)
|
||||||
|
// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions.
|
||||||
|
// Setting a size of 0,0 will disable this constraint.
|
||||||
|
export function WindowSetMinSize(width: number, height: number): void;
|
||||||
|
|
||||||
|
// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition)
|
||||||
|
// Sets the window position relative to the monitor the window is currently on.
|
||||||
|
export function WindowSetPosition(x: number, y: number): void;
|
||||||
|
|
||||||
|
// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition)
|
||||||
|
// Gets the window position relative to the monitor the window is currently on.
|
||||||
|
export function WindowGetPosition(): Promise<Position>;
|
||||||
|
|
||||||
|
// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide)
|
||||||
|
// Hides the window.
|
||||||
|
export function WindowHide(): void;
|
||||||
|
|
||||||
|
// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow)
|
||||||
|
// Shows the window, if it is currently hidden.
|
||||||
|
export function WindowShow(): void;
|
||||||
|
|
||||||
|
// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise)
|
||||||
|
// Maximises the window to fill the screen.
|
||||||
|
export function WindowMaximise(): void;
|
||||||
|
|
||||||
|
// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise)
|
||||||
|
// Toggles between Maximised and UnMaximised.
|
||||||
|
export function WindowToggleMaximise(): void;
|
||||||
|
|
||||||
|
// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise)
|
||||||
|
// Restores the window to the dimensions and position prior to maximising.
|
||||||
|
export function WindowUnmaximise(): void;
|
||||||
|
|
||||||
|
// [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised)
|
||||||
|
// Returns the state of the window, i.e. whether the window is maximised or not.
|
||||||
|
export function WindowIsMaximised(): Promise<boolean>;
|
||||||
|
|
||||||
|
// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise)
|
||||||
|
// Minimises the window.
|
||||||
|
export function WindowMinimise(): void;
|
||||||
|
|
||||||
|
// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise)
|
||||||
|
// Restores the window to the dimensions and position prior to minimising.
|
||||||
|
export function WindowUnminimise(): void;
|
||||||
|
|
||||||
|
// [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised)
|
||||||
|
// Returns the state of the window, i.e. whether the window is minimised or not.
|
||||||
|
export function WindowIsMinimised(): Promise<boolean>;
|
||||||
|
|
||||||
|
// [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal)
|
||||||
|
// Returns the state of the window, i.e. whether the window is normal or not.
|
||||||
|
export function WindowIsNormal(): Promise<boolean>;
|
||||||
|
|
||||||
|
// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour)
|
||||||
|
// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels.
|
||||||
|
export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void;
|
||||||
|
|
||||||
|
// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall)
|
||||||
|
// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system.
|
||||||
|
export function ScreenGetAll(): Promise<Screen[]>;
|
||||||
|
|
||||||
|
// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl)
|
||||||
|
// Opens the given URL in the system browser.
|
||||||
|
export function BrowserOpenURL(url: string): void;
|
||||||
|
|
||||||
|
// [Environment](https://wails.io/docs/reference/runtime/intro#environment)
|
||||||
|
// Returns information about the environment
|
||||||
|
export function Environment(): Promise<EnvironmentInfo>;
|
||||||
|
|
||||||
|
// [Quit](https://wails.io/docs/reference/runtime/intro#quit)
|
||||||
|
// Quits the application.
|
||||||
|
export function Quit(): void;
|
||||||
|
|
||||||
|
// [Hide](https://wails.io/docs/reference/runtime/intro#hide)
|
||||||
|
// Hides the application.
|
||||||
|
export function Hide(): void;
|
||||||
|
|
||||||
|
// [Show](https://wails.io/docs/reference/runtime/intro#show)
|
||||||
|
// Shows the application.
|
||||||
|
export function Show(): void;
|
||||||
|
|
||||||
|
// [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext)
|
||||||
|
// Returns the current text stored on clipboard
|
||||||
|
export function ClipboardGetText(): Promise<string>;
|
||||||
|
|
||||||
|
// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext)
|
||||||
|
// Sets a text on the clipboard
|
||||||
|
export function ClipboardSetText(text: string): Promise<boolean>;
|
||||||
|
|
||||||
|
// [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop)
|
||||||
|
// OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
|
||||||
|
export function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void
|
||||||
|
|
||||||
|
// [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff)
|
||||||
|
// OnFileDropOff removes the drag and drop listeners and handlers.
|
||||||
|
export function OnFileDropOff() :void
|
||||||
|
|
||||||
|
// Check if the file path resolver is available
|
||||||
|
export function CanResolveFilePaths(): boolean;
|
||||||
|
|
||||||
|
// Resolves file paths for an array of files
|
||||||
|
export function ResolveFilePaths(files: File[]): void
|
||||||
242
Backend/admin/frontend/wailsjs/runtime/runtime.js
Normal file
242
Backend/admin/frontend/wailsjs/runtime/runtime.js
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
/*
|
||||||
|
_ __ _ __
|
||||||
|
| | / /___ _(_) /____
|
||||||
|
| | /| / / __ `/ / / ___/
|
||||||
|
| |/ |/ / /_/ / / (__ )
|
||||||
|
|__/|__/\__,_/_/_/____/
|
||||||
|
The electron alternative for Go
|
||||||
|
(c) Lea Anthony 2019-present
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function LogPrint(message) {
|
||||||
|
window.runtime.LogPrint(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LogTrace(message) {
|
||||||
|
window.runtime.LogTrace(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LogDebug(message) {
|
||||||
|
window.runtime.LogDebug(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LogInfo(message) {
|
||||||
|
window.runtime.LogInfo(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LogWarning(message) {
|
||||||
|
window.runtime.LogWarning(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LogError(message) {
|
||||||
|
window.runtime.LogError(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LogFatal(message) {
|
||||||
|
window.runtime.LogFatal(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EventsOnMultiple(eventName, callback, maxCallbacks) {
|
||||||
|
return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EventsOn(eventName, callback) {
|
||||||
|
return EventsOnMultiple(eventName, callback, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EventsOff(eventName, ...additionalEventNames) {
|
||||||
|
return window.runtime.EventsOff(eventName, ...additionalEventNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EventsOffAll() {
|
||||||
|
return window.runtime.EventsOffAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EventsOnce(eventName, callback) {
|
||||||
|
return EventsOnMultiple(eventName, callback, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EventsEmit(eventName) {
|
||||||
|
let args = [eventName].slice.call(arguments);
|
||||||
|
return window.runtime.EventsEmit.apply(null, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowReload() {
|
||||||
|
window.runtime.WindowReload();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowReloadApp() {
|
||||||
|
window.runtime.WindowReloadApp();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowSetAlwaysOnTop(b) {
|
||||||
|
window.runtime.WindowSetAlwaysOnTop(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowSetSystemDefaultTheme() {
|
||||||
|
window.runtime.WindowSetSystemDefaultTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowSetLightTheme() {
|
||||||
|
window.runtime.WindowSetLightTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowSetDarkTheme() {
|
||||||
|
window.runtime.WindowSetDarkTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowCenter() {
|
||||||
|
window.runtime.WindowCenter();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowSetTitle(title) {
|
||||||
|
window.runtime.WindowSetTitle(title);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowFullscreen() {
|
||||||
|
window.runtime.WindowFullscreen();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowUnfullscreen() {
|
||||||
|
window.runtime.WindowUnfullscreen();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowIsFullscreen() {
|
||||||
|
return window.runtime.WindowIsFullscreen();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowGetSize() {
|
||||||
|
return window.runtime.WindowGetSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowSetSize(width, height) {
|
||||||
|
window.runtime.WindowSetSize(width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowSetMaxSize(width, height) {
|
||||||
|
window.runtime.WindowSetMaxSize(width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowSetMinSize(width, height) {
|
||||||
|
window.runtime.WindowSetMinSize(width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowSetPosition(x, y) {
|
||||||
|
window.runtime.WindowSetPosition(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowGetPosition() {
|
||||||
|
return window.runtime.WindowGetPosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowHide() {
|
||||||
|
window.runtime.WindowHide();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowShow() {
|
||||||
|
window.runtime.WindowShow();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowMaximise() {
|
||||||
|
window.runtime.WindowMaximise();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowToggleMaximise() {
|
||||||
|
window.runtime.WindowToggleMaximise();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowUnmaximise() {
|
||||||
|
window.runtime.WindowUnmaximise();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowIsMaximised() {
|
||||||
|
return window.runtime.WindowIsMaximised();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowMinimise() {
|
||||||
|
window.runtime.WindowMinimise();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowUnminimise() {
|
||||||
|
window.runtime.WindowUnminimise();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowSetBackgroundColour(R, G, B, A) {
|
||||||
|
window.runtime.WindowSetBackgroundColour(R, G, B, A);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ScreenGetAll() {
|
||||||
|
return window.runtime.ScreenGetAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowIsMinimised() {
|
||||||
|
return window.runtime.WindowIsMinimised();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowIsNormal() {
|
||||||
|
return window.runtime.WindowIsNormal();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BrowserOpenURL(url) {
|
||||||
|
window.runtime.BrowserOpenURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Environment() {
|
||||||
|
return window.runtime.Environment();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Quit() {
|
||||||
|
window.runtime.Quit();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Hide() {
|
||||||
|
window.runtime.Hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Show() {
|
||||||
|
window.runtime.Show();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ClipboardGetText() {
|
||||||
|
return window.runtime.ClipboardGetText();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ClipboardSetText(text) {
|
||||||
|
return window.runtime.ClipboardSetText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
* @callback OnFileDropCallback
|
||||||
|
* @param {number} x - x coordinate of the drop
|
||||||
|
* @param {number} y - y coordinate of the drop
|
||||||
|
* @param {string[]} paths - A list of file paths.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
* @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
|
||||||
|
* @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target)
|
||||||
|
*/
|
||||||
|
export function OnFileDrop(callback, useDropTarget) {
|
||||||
|
return window.runtime.OnFileDrop(callback, useDropTarget);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OnFileDropOff removes the drag and drop listeners and handlers.
|
||||||
|
*/
|
||||||
|
export function OnFileDropOff() {
|
||||||
|
return window.runtime.OnFileDropOff();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CanResolveFilePaths() {
|
||||||
|
return window.runtime.CanResolveFilePaths();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ResolveFilePaths(files) {
|
||||||
|
return window.runtime.ResolveFilePaths(files);
|
||||||
|
}
|
||||||
54
Backend/admin/frontend/wailsjs/wailsjs/go/admin/App.d.ts
vendored
Normal file
54
Backend/admin/frontend/wailsjs/wailsjs/go/admin/App.d.ts
vendored
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
import {services} from '../models';
|
||||||
|
import {proxy} from '../models';
|
||||||
|
import {sites} from '../models';
|
||||||
|
import {vaccess} from '../models';
|
||||||
|
|
||||||
|
export function CheckServicesReady():Promise<boolean>;
|
||||||
|
|
||||||
|
export function DisableProxyService():Promise<string>;
|
||||||
|
|
||||||
|
export function EnableProxyService():Promise<string>;
|
||||||
|
|
||||||
|
export function GetAllServicesStatus():Promise<services.AllServicesStatus>;
|
||||||
|
|
||||||
|
export function GetConfig():Promise<any>;
|
||||||
|
|
||||||
|
export function GetProxyList():Promise<Array<proxy.ProxyInfo>>;
|
||||||
|
|
||||||
|
export function GetSitesList():Promise<Array<sites.SiteInfo>>;
|
||||||
|
|
||||||
|
export function GetVAccessRules(arg1:string,arg2:boolean):Promise<vaccess.VAccessConfig>;
|
||||||
|
|
||||||
|
export function OpenSiteFolder(arg1:string):Promise<string>;
|
||||||
|
|
||||||
|
export function ReloadConfig():Promise<string>;
|
||||||
|
|
||||||
|
export function RestartAllServices():Promise<string>;
|
||||||
|
|
||||||
|
export function SaveConfig(arg1:string):Promise<string>;
|
||||||
|
|
||||||
|
export function SaveVAccessRules(arg1:string,arg2:boolean,arg3:string):Promise<string>;
|
||||||
|
|
||||||
|
export function StartHTTPSService():Promise<string>;
|
||||||
|
|
||||||
|
export function StartHTTPService():Promise<string>;
|
||||||
|
|
||||||
|
export function StartMySQLService():Promise<string>;
|
||||||
|
|
||||||
|
export function StartPHPService():Promise<string>;
|
||||||
|
|
||||||
|
export function StartServer():Promise<string>;
|
||||||
|
|
||||||
|
export function StopHTTPSService():Promise<string>;
|
||||||
|
|
||||||
|
export function StopHTTPService():Promise<string>;
|
||||||
|
|
||||||
|
export function StopMySQLService():Promise<string>;
|
||||||
|
|
||||||
|
export function StopPHPService():Promise<string>;
|
||||||
|
|
||||||
|
export function StopServer():Promise<string>;
|
||||||
|
|
||||||
|
export function UpdateSiteCache():Promise<string>;
|
||||||
99
Backend/admin/frontend/wailsjs/wailsjs/go/admin/App.js
Normal file
99
Backend/admin/frontend/wailsjs/wailsjs/go/admin/App.js
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
// @ts-check
|
||||||
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
|
||||||
|
export function CheckServicesReady() {
|
||||||
|
return window['go']['admin']['App']['CheckServicesReady']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DisableProxyService() {
|
||||||
|
return window['go']['admin']['App']['DisableProxyService']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EnableProxyService() {
|
||||||
|
return window['go']['admin']['App']['EnableProxyService']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GetAllServicesStatus() {
|
||||||
|
return window['go']['admin']['App']['GetAllServicesStatus']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GetConfig() {
|
||||||
|
return window['go']['admin']['App']['GetConfig']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GetProxyList() {
|
||||||
|
return window['go']['admin']['App']['GetProxyList']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GetSitesList() {
|
||||||
|
return window['go']['admin']['App']['GetSitesList']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GetVAccessRules(arg1, arg2) {
|
||||||
|
return window['go']['admin']['App']['GetVAccessRules'](arg1, arg2);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function OpenSiteFolder(arg1) {
|
||||||
|
return window['go']['admin']['App']['OpenSiteFolder'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ReloadConfig() {
|
||||||
|
return window['go']['admin']['App']['ReloadConfig']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RestartAllServices() {
|
||||||
|
return window['go']['admin']['App']['RestartAllServices']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SaveConfig(arg1) {
|
||||||
|
return window['go']['admin']['App']['SaveConfig'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SaveVAccessRules(arg1, arg2, arg3) {
|
||||||
|
return window['go']['admin']['App']['SaveVAccessRules'](arg1, arg2, arg3);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StartHTTPSService() {
|
||||||
|
return window['go']['admin']['App']['StartHTTPSService']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StartHTTPService() {
|
||||||
|
return window['go']['admin']['App']['StartHTTPService']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StartMySQLService() {
|
||||||
|
return window['go']['admin']['App']['StartMySQLService']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StartPHPService() {
|
||||||
|
return window['go']['admin']['App']['StartPHPService']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StartServer() {
|
||||||
|
return window['go']['admin']['App']['StartServer']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StopHTTPSService() {
|
||||||
|
return window['go']['admin']['App']['StopHTTPSService']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StopHTTPService() {
|
||||||
|
return window['go']['admin']['App']['StopHTTPService']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StopMySQLService() {
|
||||||
|
return window['go']['admin']['App']['StopMySQLService']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StopPHPService() {
|
||||||
|
return window['go']['admin']['App']['StopPHPService']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StopServer() {
|
||||||
|
return window['go']['admin']['App']['StopServer']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function UpdateSiteCache() {
|
||||||
|
return window['go']['admin']['App']['UpdateSiteCache']();
|
||||||
|
}
|
||||||
174
Backend/admin/frontend/wailsjs/wailsjs/go/models.ts
Normal file
174
Backend/admin/frontend/wailsjs/wailsjs/go/models.ts
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
export namespace proxy {
|
||||||
|
|
||||||
|
export class ProxyInfo {
|
||||||
|
enable: boolean;
|
||||||
|
external_domain: string;
|
||||||
|
local_address: string;
|
||||||
|
local_port: string;
|
||||||
|
service_https_use: boolean;
|
||||||
|
auto_https: boolean;
|
||||||
|
status: string;
|
||||||
|
|
||||||
|
static createFrom(source: any = {}) {
|
||||||
|
return new ProxyInfo(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(source: any = {}) {
|
||||||
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
|
this.enable = source["enable"];
|
||||||
|
this.external_domain = source["external_domain"];
|
||||||
|
this.local_address = source["local_address"];
|
||||||
|
this.local_port = source["local_port"];
|
||||||
|
this.service_https_use = source["service_https_use"];
|
||||||
|
this.auto_https = source["auto_https"];
|
||||||
|
this.status = source["status"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace services {
|
||||||
|
|
||||||
|
export class ServiceStatus {
|
||||||
|
name: string;
|
||||||
|
status: boolean;
|
||||||
|
port: string;
|
||||||
|
info: string;
|
||||||
|
|
||||||
|
static createFrom(source: any = {}) {
|
||||||
|
return new ServiceStatus(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(source: any = {}) {
|
||||||
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
|
this.name = source["name"];
|
||||||
|
this.status = source["status"];
|
||||||
|
this.port = source["port"];
|
||||||
|
this.info = source["info"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class AllServicesStatus {
|
||||||
|
http: ServiceStatus;
|
||||||
|
https: ServiceStatus;
|
||||||
|
mysql: ServiceStatus;
|
||||||
|
php: ServiceStatus;
|
||||||
|
proxy: ServiceStatus;
|
||||||
|
|
||||||
|
static createFrom(source: any = {}) {
|
||||||
|
return new AllServicesStatus(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(source: any = {}) {
|
||||||
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
|
this.http = this.convertValues(source["http"], ServiceStatus);
|
||||||
|
this.https = this.convertValues(source["https"], ServiceStatus);
|
||||||
|
this.mysql = this.convertValues(source["mysql"], ServiceStatus);
|
||||||
|
this.php = this.convertValues(source["php"], ServiceStatus);
|
||||||
|
this.proxy = this.convertValues(source["proxy"], ServiceStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||||
|
if (!a) {
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
if (a.slice && a.map) {
|
||||||
|
return (a as any[]).map(elem => this.convertValues(elem, classs));
|
||||||
|
} else if ("object" === typeof a) {
|
||||||
|
if (asMap) {
|
||||||
|
for (const key of Object.keys(a)) {
|
||||||
|
a[key] = new classs(a[key]);
|
||||||
|
}
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
return new classs(a);
|
||||||
|
}
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace sites {
|
||||||
|
|
||||||
|
export class SiteInfo {
|
||||||
|
name: string;
|
||||||
|
host: string;
|
||||||
|
alias: string[];
|
||||||
|
status: string;
|
||||||
|
root_file: string;
|
||||||
|
root_file_routing: boolean;
|
||||||
|
|
||||||
|
static createFrom(source: any = {}) {
|
||||||
|
return new SiteInfo(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(source: any = {}) {
|
||||||
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
|
this.name = source["name"];
|
||||||
|
this.host = source["host"];
|
||||||
|
this.alias = source["alias"];
|
||||||
|
this.status = source["status"];
|
||||||
|
this.root_file = source["root_file"];
|
||||||
|
this.root_file_routing = source["root_file_routing"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace vaccess {
|
||||||
|
|
||||||
|
export class VAccessRule {
|
||||||
|
type: string;
|
||||||
|
type_file: string[];
|
||||||
|
path_access: string[];
|
||||||
|
ip_list: string[];
|
||||||
|
exceptions_dir: string[];
|
||||||
|
url_error: string;
|
||||||
|
|
||||||
|
static createFrom(source: any = {}) {
|
||||||
|
return new VAccessRule(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(source: any = {}) {
|
||||||
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
|
this.type = source["type"];
|
||||||
|
this.type_file = source["type_file"];
|
||||||
|
this.path_access = source["path_access"];
|
||||||
|
this.ip_list = source["ip_list"];
|
||||||
|
this.exceptions_dir = source["exceptions_dir"];
|
||||||
|
this.url_error = source["url_error"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class VAccessConfig {
|
||||||
|
rules: VAccessRule[];
|
||||||
|
|
||||||
|
static createFrom(source: any = {}) {
|
||||||
|
return new VAccessConfig(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(source: any = {}) {
|
||||||
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
|
this.rules = this.convertValues(source["rules"], VAccessRule);
|
||||||
|
}
|
||||||
|
|
||||||
|
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||||
|
if (!a) {
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
if (a.slice && a.map) {
|
||||||
|
return (a as any[]).map(elem => this.convertValues(elem, classs));
|
||||||
|
} else if ("object" === typeof a) {
|
||||||
|
if (asMap) {
|
||||||
|
for (const key of Object.keys(a)) {
|
||||||
|
a[key] = new classs(a[key]);
|
||||||
|
}
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
return new classs(a);
|
||||||
|
}
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
24
Backend/admin/frontend/wailsjs/wailsjs/runtime/package.json
Normal file
24
Backend/admin/frontend/wailsjs/wailsjs/runtime/package.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "@wailsapp/runtime",
|
||||||
|
"version": "2.0.0",
|
||||||
|
"description": "Wails Javascript runtime library",
|
||||||
|
"main": "runtime.js",
|
||||||
|
"types": "runtime.d.ts",
|
||||||
|
"scripts": {
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/wailsapp/wails.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"Wails",
|
||||||
|
"Javascript",
|
||||||
|
"Go"
|
||||||
|
],
|
||||||
|
"author": "Lea Anthony <lea.anthony@gmail.com>",
|
||||||
|
"license": "MIT",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/wailsapp/wails/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/wailsapp/wails#readme"
|
||||||
|
}
|
||||||
249
Backend/admin/frontend/wailsjs/wailsjs/runtime/runtime.d.ts
vendored
Normal file
249
Backend/admin/frontend/wailsjs/wailsjs/runtime/runtime.d.ts
vendored
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
/*
|
||||||
|
_ __ _ __
|
||||||
|
| | / /___ _(_) /____
|
||||||
|
| | /| / / __ `/ / / ___/
|
||||||
|
| |/ |/ / /_/ / / (__ )
|
||||||
|
|__/|__/\__,_/_/_/____/
|
||||||
|
The electron alternative for Go
|
||||||
|
(c) Lea Anthony 2019-present
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface Position {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Size {
|
||||||
|
w: number;
|
||||||
|
h: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Screen {
|
||||||
|
isCurrent: boolean;
|
||||||
|
isPrimary: boolean;
|
||||||
|
width : number
|
||||||
|
height : number
|
||||||
|
}
|
||||||
|
|
||||||
|
// Environment information such as platform, buildtype, ...
|
||||||
|
export interface EnvironmentInfo {
|
||||||
|
buildType: string;
|
||||||
|
platform: string;
|
||||||
|
arch: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit)
|
||||||
|
// emits the given event. Optional data may be passed with the event.
|
||||||
|
// This will trigger any event listeners.
|
||||||
|
export function EventsEmit(eventName: string, ...data: any): void;
|
||||||
|
|
||||||
|
// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name.
|
||||||
|
export function EventsOn(eventName: string, callback: (...data: any) => void): () => void;
|
||||||
|
|
||||||
|
// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple)
|
||||||
|
// sets up a listener for the given event name, but will only trigger a given number times.
|
||||||
|
export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void;
|
||||||
|
|
||||||
|
// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce)
|
||||||
|
// sets up a listener for the given event name, but will only trigger once.
|
||||||
|
export function EventsOnce(eventName: string, callback: (...data: any) => void): () => void;
|
||||||
|
|
||||||
|
// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff)
|
||||||
|
// unregisters the listener for the given event name.
|
||||||
|
export function EventsOff(eventName: string, ...additionalEventNames: string[]): void;
|
||||||
|
|
||||||
|
// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall)
|
||||||
|
// unregisters all listeners.
|
||||||
|
export function EventsOffAll(): void;
|
||||||
|
|
||||||
|
// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint)
|
||||||
|
// logs the given message as a raw message
|
||||||
|
export function LogPrint(message: string): void;
|
||||||
|
|
||||||
|
// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace)
|
||||||
|
// logs the given message at the `trace` log level.
|
||||||
|
export function LogTrace(message: string): void;
|
||||||
|
|
||||||
|
// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug)
|
||||||
|
// logs the given message at the `debug` log level.
|
||||||
|
export function LogDebug(message: string): void;
|
||||||
|
|
||||||
|
// [LogError](https://wails.io/docs/reference/runtime/log#logerror)
|
||||||
|
// logs the given message at the `error` log level.
|
||||||
|
export function LogError(message: string): void;
|
||||||
|
|
||||||
|
// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal)
|
||||||
|
// logs the given message at the `fatal` log level.
|
||||||
|
// The application will quit after calling this method.
|
||||||
|
export function LogFatal(message: string): void;
|
||||||
|
|
||||||
|
// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo)
|
||||||
|
// logs the given message at the `info` log level.
|
||||||
|
export function LogInfo(message: string): void;
|
||||||
|
|
||||||
|
// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning)
|
||||||
|
// logs the given message at the `warning` log level.
|
||||||
|
export function LogWarning(message: string): void;
|
||||||
|
|
||||||
|
// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload)
|
||||||
|
// Forces a reload by the main application as well as connected browsers.
|
||||||
|
export function WindowReload(): void;
|
||||||
|
|
||||||
|
// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp)
|
||||||
|
// Reloads the application frontend.
|
||||||
|
export function WindowReloadApp(): void;
|
||||||
|
|
||||||
|
// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop)
|
||||||
|
// Sets the window AlwaysOnTop or not on top.
|
||||||
|
export function WindowSetAlwaysOnTop(b: boolean): void;
|
||||||
|
|
||||||
|
// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme)
|
||||||
|
// *Windows only*
|
||||||
|
// Sets window theme to system default (dark/light).
|
||||||
|
export function WindowSetSystemDefaultTheme(): void;
|
||||||
|
|
||||||
|
// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme)
|
||||||
|
// *Windows only*
|
||||||
|
// Sets window to light theme.
|
||||||
|
export function WindowSetLightTheme(): void;
|
||||||
|
|
||||||
|
// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme)
|
||||||
|
// *Windows only*
|
||||||
|
// Sets window to dark theme.
|
||||||
|
export function WindowSetDarkTheme(): void;
|
||||||
|
|
||||||
|
// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter)
|
||||||
|
// Centers the window on the monitor the window is currently on.
|
||||||
|
export function WindowCenter(): void;
|
||||||
|
|
||||||
|
// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle)
|
||||||
|
// Sets the text in the window title bar.
|
||||||
|
export function WindowSetTitle(title: string): void;
|
||||||
|
|
||||||
|
// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen)
|
||||||
|
// Makes the window full screen.
|
||||||
|
export function WindowFullscreen(): void;
|
||||||
|
|
||||||
|
// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen)
|
||||||
|
// Restores the previous window dimensions and position prior to full screen.
|
||||||
|
export function WindowUnfullscreen(): void;
|
||||||
|
|
||||||
|
// [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen)
|
||||||
|
// Returns the state of the window, i.e. whether the window is in full screen mode or not.
|
||||||
|
export function WindowIsFullscreen(): Promise<boolean>;
|
||||||
|
|
||||||
|
// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize)
|
||||||
|
// Sets the width and height of the window.
|
||||||
|
export function WindowSetSize(width: number, height: number): void;
|
||||||
|
|
||||||
|
// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize)
|
||||||
|
// Gets the width and height of the window.
|
||||||
|
export function WindowGetSize(): Promise<Size>;
|
||||||
|
|
||||||
|
// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize)
|
||||||
|
// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions.
|
||||||
|
// Setting a size of 0,0 will disable this constraint.
|
||||||
|
export function WindowSetMaxSize(width: number, height: number): void;
|
||||||
|
|
||||||
|
// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize)
|
||||||
|
// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions.
|
||||||
|
// Setting a size of 0,0 will disable this constraint.
|
||||||
|
export function WindowSetMinSize(width: number, height: number): void;
|
||||||
|
|
||||||
|
// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition)
|
||||||
|
// Sets the window position relative to the monitor the window is currently on.
|
||||||
|
export function WindowSetPosition(x: number, y: number): void;
|
||||||
|
|
||||||
|
// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition)
|
||||||
|
// Gets the window position relative to the monitor the window is currently on.
|
||||||
|
export function WindowGetPosition(): Promise<Position>;
|
||||||
|
|
||||||
|
// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide)
|
||||||
|
// Hides the window.
|
||||||
|
export function WindowHide(): void;
|
||||||
|
|
||||||
|
// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow)
|
||||||
|
// Shows the window, if it is currently hidden.
|
||||||
|
export function WindowShow(): void;
|
||||||
|
|
||||||
|
// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise)
|
||||||
|
// Maximises the window to fill the screen.
|
||||||
|
export function WindowMaximise(): void;
|
||||||
|
|
||||||
|
// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise)
|
||||||
|
// Toggles between Maximised and UnMaximised.
|
||||||
|
export function WindowToggleMaximise(): void;
|
||||||
|
|
||||||
|
// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise)
|
||||||
|
// Restores the window to the dimensions and position prior to maximising.
|
||||||
|
export function WindowUnmaximise(): void;
|
||||||
|
|
||||||
|
// [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised)
|
||||||
|
// Returns the state of the window, i.e. whether the window is maximised or not.
|
||||||
|
export function WindowIsMaximised(): Promise<boolean>;
|
||||||
|
|
||||||
|
// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise)
|
||||||
|
// Minimises the window.
|
||||||
|
export function WindowMinimise(): void;
|
||||||
|
|
||||||
|
// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise)
|
||||||
|
// Restores the window to the dimensions and position prior to minimising.
|
||||||
|
export function WindowUnminimise(): void;
|
||||||
|
|
||||||
|
// [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised)
|
||||||
|
// Returns the state of the window, i.e. whether the window is minimised or not.
|
||||||
|
export function WindowIsMinimised(): Promise<boolean>;
|
||||||
|
|
||||||
|
// [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal)
|
||||||
|
// Returns the state of the window, i.e. whether the window is normal or not.
|
||||||
|
export function WindowIsNormal(): Promise<boolean>;
|
||||||
|
|
||||||
|
// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour)
|
||||||
|
// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels.
|
||||||
|
export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void;
|
||||||
|
|
||||||
|
// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall)
|
||||||
|
// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system.
|
||||||
|
export function ScreenGetAll(): Promise<Screen[]>;
|
||||||
|
|
||||||
|
// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl)
|
||||||
|
// Opens the given URL in the system browser.
|
||||||
|
export function BrowserOpenURL(url: string): void;
|
||||||
|
|
||||||
|
// [Environment](https://wails.io/docs/reference/runtime/intro#environment)
|
||||||
|
// Returns information about the environment
|
||||||
|
export function Environment(): Promise<EnvironmentInfo>;
|
||||||
|
|
||||||
|
// [Quit](https://wails.io/docs/reference/runtime/intro#quit)
|
||||||
|
// Quits the application.
|
||||||
|
export function Quit(): void;
|
||||||
|
|
||||||
|
// [Hide](https://wails.io/docs/reference/runtime/intro#hide)
|
||||||
|
// Hides the application.
|
||||||
|
export function Hide(): void;
|
||||||
|
|
||||||
|
// [Show](https://wails.io/docs/reference/runtime/intro#show)
|
||||||
|
// Shows the application.
|
||||||
|
export function Show(): void;
|
||||||
|
|
||||||
|
// [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext)
|
||||||
|
// Returns the current text stored on clipboard
|
||||||
|
export function ClipboardGetText(): Promise<string>;
|
||||||
|
|
||||||
|
// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext)
|
||||||
|
// Sets a text on the clipboard
|
||||||
|
export function ClipboardSetText(text: string): Promise<boolean>;
|
||||||
|
|
||||||
|
// [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop)
|
||||||
|
// OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
|
||||||
|
export function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void
|
||||||
|
|
||||||
|
// [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff)
|
||||||
|
// OnFileDropOff removes the drag and drop listeners and handlers.
|
||||||
|
export function OnFileDropOff() :void
|
||||||
|
|
||||||
|
// Check if the file path resolver is available
|
||||||
|
export function CanResolveFilePaths(): boolean;
|
||||||
|
|
||||||
|
// Resolves file paths for an array of files
|
||||||
|
export function ResolveFilePaths(files: File[]): void
|
||||||
242
Backend/admin/frontend/wailsjs/wailsjs/runtime/runtime.js
Normal file
242
Backend/admin/frontend/wailsjs/wailsjs/runtime/runtime.js
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
/*
|
||||||
|
_ __ _ __
|
||||||
|
| | / /___ _(_) /____
|
||||||
|
| | /| / / __ `/ / / ___/
|
||||||
|
| |/ |/ / /_/ / / (__ )
|
||||||
|
|__/|__/\__,_/_/_/____/
|
||||||
|
The electron alternative for Go
|
||||||
|
(c) Lea Anthony 2019-present
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function LogPrint(message) {
|
||||||
|
window.runtime.LogPrint(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LogTrace(message) {
|
||||||
|
window.runtime.LogTrace(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LogDebug(message) {
|
||||||
|
window.runtime.LogDebug(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LogInfo(message) {
|
||||||
|
window.runtime.LogInfo(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LogWarning(message) {
|
||||||
|
window.runtime.LogWarning(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LogError(message) {
|
||||||
|
window.runtime.LogError(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LogFatal(message) {
|
||||||
|
window.runtime.LogFatal(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EventsOnMultiple(eventName, callback, maxCallbacks) {
|
||||||
|
return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EventsOn(eventName, callback) {
|
||||||
|
return EventsOnMultiple(eventName, callback, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EventsOff(eventName, ...additionalEventNames) {
|
||||||
|
return window.runtime.EventsOff(eventName, ...additionalEventNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EventsOffAll() {
|
||||||
|
return window.runtime.EventsOffAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EventsOnce(eventName, callback) {
|
||||||
|
return EventsOnMultiple(eventName, callback, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EventsEmit(eventName) {
|
||||||
|
let args = [eventName].slice.call(arguments);
|
||||||
|
return window.runtime.EventsEmit.apply(null, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowReload() {
|
||||||
|
window.runtime.WindowReload();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowReloadApp() {
|
||||||
|
window.runtime.WindowReloadApp();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowSetAlwaysOnTop(b) {
|
||||||
|
window.runtime.WindowSetAlwaysOnTop(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowSetSystemDefaultTheme() {
|
||||||
|
window.runtime.WindowSetSystemDefaultTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowSetLightTheme() {
|
||||||
|
window.runtime.WindowSetLightTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowSetDarkTheme() {
|
||||||
|
window.runtime.WindowSetDarkTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowCenter() {
|
||||||
|
window.runtime.WindowCenter();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowSetTitle(title) {
|
||||||
|
window.runtime.WindowSetTitle(title);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowFullscreen() {
|
||||||
|
window.runtime.WindowFullscreen();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowUnfullscreen() {
|
||||||
|
window.runtime.WindowUnfullscreen();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowIsFullscreen() {
|
||||||
|
return window.runtime.WindowIsFullscreen();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowGetSize() {
|
||||||
|
return window.runtime.WindowGetSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowSetSize(width, height) {
|
||||||
|
window.runtime.WindowSetSize(width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowSetMaxSize(width, height) {
|
||||||
|
window.runtime.WindowSetMaxSize(width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowSetMinSize(width, height) {
|
||||||
|
window.runtime.WindowSetMinSize(width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowSetPosition(x, y) {
|
||||||
|
window.runtime.WindowSetPosition(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowGetPosition() {
|
||||||
|
return window.runtime.WindowGetPosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowHide() {
|
||||||
|
window.runtime.WindowHide();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowShow() {
|
||||||
|
window.runtime.WindowShow();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowMaximise() {
|
||||||
|
window.runtime.WindowMaximise();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowToggleMaximise() {
|
||||||
|
window.runtime.WindowToggleMaximise();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowUnmaximise() {
|
||||||
|
window.runtime.WindowUnmaximise();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowIsMaximised() {
|
||||||
|
return window.runtime.WindowIsMaximised();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowMinimise() {
|
||||||
|
window.runtime.WindowMinimise();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowUnminimise() {
|
||||||
|
window.runtime.WindowUnminimise();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowSetBackgroundColour(R, G, B, A) {
|
||||||
|
window.runtime.WindowSetBackgroundColour(R, G, B, A);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ScreenGetAll() {
|
||||||
|
return window.runtime.ScreenGetAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowIsMinimised() {
|
||||||
|
return window.runtime.WindowIsMinimised();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowIsNormal() {
|
||||||
|
return window.runtime.WindowIsNormal();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BrowserOpenURL(url) {
|
||||||
|
window.runtime.BrowserOpenURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Environment() {
|
||||||
|
return window.runtime.Environment();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Quit() {
|
||||||
|
window.runtime.Quit();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Hide() {
|
||||||
|
window.runtime.Hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Show() {
|
||||||
|
window.runtime.Show();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ClipboardGetText() {
|
||||||
|
return window.runtime.ClipboardGetText();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ClipboardSetText(text) {
|
||||||
|
return window.runtime.ClipboardSetText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
* @callback OnFileDropCallback
|
||||||
|
* @param {number} x - x coordinate of the drop
|
||||||
|
* @param {number} y - y coordinate of the drop
|
||||||
|
* @param {string[]} paths - A list of file paths.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
* @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
|
||||||
|
* @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target)
|
||||||
|
*/
|
||||||
|
export function OnFileDrop(callback, useDropTarget) {
|
||||||
|
return window.runtime.OnFileDrop(callback, useDropTarget);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OnFileDropOff removes the drag and drop listeners and handlers.
|
||||||
|
*/
|
||||||
|
export function OnFileDropOff() {
|
||||||
|
return window.runtime.OnFileDropOff();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CanResolveFilePaths() {
|
||||||
|
return window.runtime.CanResolveFilePaths();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ResolveFilePaths(files) {
|
||||||
|
return window.runtime.ResolveFilePaths(files);
|
||||||
|
}
|
||||||
320
Backend/admin/go/admin.go
Normal file
320
Backend/admin/go/admin.go
Normal file
@@ -0,0 +1,320 @@
|
|||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||||
|
|
||||||
|
webserver "vServer/Backend/WebServer"
|
||||||
|
"vServer/Backend/admin/go/proxy"
|
||||||
|
"vServer/Backend/admin/go/services"
|
||||||
|
"vServer/Backend/admin/go/sites"
|
||||||
|
"vServer/Backend/admin/go/vaccess"
|
||||||
|
config "vServer/Backend/config"
|
||||||
|
tools "vServer/Backend/tools"
|
||||||
|
)
|
||||||
|
|
||||||
|
type App struct {
|
||||||
|
ctx context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
var appContext context.Context
|
||||||
|
|
||||||
|
func NewApp() *App {
|
||||||
|
return &App{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var isSingleInstance bool = false
|
||||||
|
|
||||||
|
func (a *App) Startup(ctx context.Context) {
|
||||||
|
a.ctx = ctx
|
||||||
|
appContext = ctx
|
||||||
|
|
||||||
|
// Проверяем, не запущен ли уже vServer
|
||||||
|
if !tools.CheckSingleInstance() {
|
||||||
|
runtime.EventsEmit(ctx, "server:already_running", true)
|
||||||
|
// Только мониторинг, не запускаем сервисы
|
||||||
|
config.LoadConfig()
|
||||||
|
go a.monitorServices()
|
||||||
|
isSingleInstance = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isSingleInstance = true
|
||||||
|
|
||||||
|
// Инициализируем время запуска
|
||||||
|
tools.ServerUptime("start")
|
||||||
|
|
||||||
|
// Загружаем конфигурацию
|
||||||
|
config.LoadConfig()
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
|
||||||
|
// Запускаем handler
|
||||||
|
webserver.StartHandler()
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
|
||||||
|
// Загружаем сертификаты
|
||||||
|
webserver.Cert_start()
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
|
||||||
|
// Запускаем серверы
|
||||||
|
go webserver.StartHTTPS()
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
|
||||||
|
go webserver.StartHTTP()
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
|
||||||
|
// Запускаем PHP
|
||||||
|
webserver.PHP_Start()
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
|
||||||
|
// Запускаем MySQL асинхронно
|
||||||
|
go webserver.StartMySQLServer(false)
|
||||||
|
|
||||||
|
// Запускаем мониторинг статусов
|
||||||
|
go a.monitorServices()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) GetAllServicesStatus() services.AllServicesStatus {
|
||||||
|
return services.GetAllServicesStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) CheckServicesReady() bool {
|
||||||
|
status := services.GetAllServicesStatus()
|
||||||
|
return status.HTTP.Status && status.HTTPS.Status && status.MySQL.Status && status.PHP.Status
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) Shutdown(ctx context.Context) {
|
||||||
|
// Останавливаем все сервисы при закрытии приложения
|
||||||
|
if isSingleInstance {
|
||||||
|
webserver.StopHTTPServer()
|
||||||
|
webserver.StopHTTPSServer()
|
||||||
|
webserver.PHP_Stop()
|
||||||
|
webserver.StopMySQLServer()
|
||||||
|
|
||||||
|
// Освобождаем мьютекс
|
||||||
|
tools.ReleaseMutex()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) monitorServices() {
|
||||||
|
time.Sleep(1 * time.Second) // Ждём секунду перед первой проверкой
|
||||||
|
|
||||||
|
for {
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
status := services.GetAllServicesStatus()
|
||||||
|
runtime.EventsEmit(appContext, "service:changed", status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) GetSitesList() []sites.SiteInfo {
|
||||||
|
return sites.GetSitesList()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) GetProxyList() []proxy.ProxyInfo {
|
||||||
|
return proxy.GetProxyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) StartServer() string {
|
||||||
|
webserver.Cert_start()
|
||||||
|
|
||||||
|
go webserver.StartHTTPS()
|
||||||
|
go webserver.StartHTTP()
|
||||||
|
|
||||||
|
webserver.PHP_Start()
|
||||||
|
go webserver.StartMySQLServer(false)
|
||||||
|
|
||||||
|
return "Server started"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) StopServer() string {
|
||||||
|
webserver.StopHTTPServer()
|
||||||
|
webserver.StopHTTPSServer()
|
||||||
|
webserver.PHP_Stop()
|
||||||
|
webserver.StopMySQLServer()
|
||||||
|
|
||||||
|
return "Server stopped"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) ReloadConfig() string {
|
||||||
|
config.LoadConfig()
|
||||||
|
return "Config reloaded"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) GetConfig() interface{} {
|
||||||
|
return config.ConfigData
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) SaveConfig(configJSON string) string {
|
||||||
|
// Форматируем JSON перед сохранением
|
||||||
|
var tempConfig interface{}
|
||||||
|
err := json.Unmarshal([]byte(configJSON), &tempConfig)
|
||||||
|
if err != nil {
|
||||||
|
return "Error: Invalid JSON"
|
||||||
|
}
|
||||||
|
|
||||||
|
formattedJSON, err := json.MarshalIndent(tempConfig, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return "Error: " + err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сохранение конфига в файл
|
||||||
|
err = os.WriteFile(config.ConfigPath, formattedJSON, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return "Error: " + err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Перезагружаем конфигурацию
|
||||||
|
config.LoadConfig()
|
||||||
|
return "Config saved"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) RestartAllServices() string {
|
||||||
|
// Останавливаем все сервисы
|
||||||
|
webserver.StopHTTPServer()
|
||||||
|
webserver.StopHTTPSServer()
|
||||||
|
webserver.PHP_Stop()
|
||||||
|
webserver.StopMySQLServer()
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
|
||||||
|
// Перезагружаем конфиг
|
||||||
|
config.LoadConfig()
|
||||||
|
time.Sleep(200 * time.Millisecond)
|
||||||
|
|
||||||
|
// Обновляем кэш статусов сайтов
|
||||||
|
webserver.UpdateSiteStatusCache()
|
||||||
|
|
||||||
|
// Перезагружаем сертификаты
|
||||||
|
webserver.Cert_start()
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
|
||||||
|
// Запускаем серверы заново
|
||||||
|
go webserver.StartHTTPS()
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
|
||||||
|
go webserver.StartHTTP()
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
|
||||||
|
webserver.PHP_Start()
|
||||||
|
time.Sleep(200 * time.Millisecond)
|
||||||
|
|
||||||
|
go webserver.StartMySQLServer(false)
|
||||||
|
|
||||||
|
return "All services restarted"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Управление отдельными сервисами
|
||||||
|
func (a *App) StartHTTPService() string {
|
||||||
|
// Обновляем кэш перед запуском
|
||||||
|
webserver.UpdateSiteStatusCache()
|
||||||
|
go webserver.StartHTTP()
|
||||||
|
return "HTTP started"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) StopHTTPService() string {
|
||||||
|
webserver.StopHTTPServer()
|
||||||
|
return "HTTP stopped"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) StartHTTPSService() string {
|
||||||
|
// Обновляем кэш перед запуском
|
||||||
|
webserver.UpdateSiteStatusCache()
|
||||||
|
go webserver.StartHTTPS()
|
||||||
|
return "HTTPS started"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) StopHTTPSService() string {
|
||||||
|
webserver.StopHTTPSServer()
|
||||||
|
return "HTTPS stopped"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) StartMySQLService() string {
|
||||||
|
go webserver.StartMySQLServer(false)
|
||||||
|
return "MySQL started"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) StopMySQLService() string {
|
||||||
|
webserver.StopMySQLServer()
|
||||||
|
return "MySQL stopped"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) StartPHPService() string {
|
||||||
|
webserver.PHP_Start()
|
||||||
|
return "PHP started"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) StopPHPService() string {
|
||||||
|
webserver.PHP_Stop()
|
||||||
|
return "PHP stopped"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) EnableProxyService() string {
|
||||||
|
config.ConfigData.Soft_Settings.Proxy_enabled = true
|
||||||
|
|
||||||
|
// Сохраняем в файл
|
||||||
|
configJSON, _ := json.Marshal(config.ConfigData)
|
||||||
|
os.WriteFile(config.ConfigPath, configJSON, 0644)
|
||||||
|
|
||||||
|
return "Proxy enabled"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) DisableProxyService() string {
|
||||||
|
config.ConfigData.Soft_Settings.Proxy_enabled = false
|
||||||
|
|
||||||
|
// Сохраняем в файл
|
||||||
|
configJSON, _ := json.Marshal(config.ConfigData)
|
||||||
|
os.WriteFile(config.ConfigPath, configJSON, 0644)
|
||||||
|
|
||||||
|
return "Proxy disabled"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) OpenSiteFolder(host string) string {
|
||||||
|
folderPath := "WebServer/www/" + host
|
||||||
|
|
||||||
|
// Получаем абсолютный путь
|
||||||
|
absPath, err := tools.AbsPath(folderPath)
|
||||||
|
if err != nil {
|
||||||
|
return "Error: " + err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Открываем папку в проводнике
|
||||||
|
cmd := exec.Command("explorer", absPath)
|
||||||
|
err = cmd.Start()
|
||||||
|
if err != nil {
|
||||||
|
return "Error: " + err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Folder opened"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) GetVAccessRules(host string, isProxy bool) *vaccess.VAccessConfig {
|
||||||
|
config, err := vaccess.GetVAccessConfig(host, isProxy)
|
||||||
|
if err != nil {
|
||||||
|
return &vaccess.VAccessConfig{Rules: []vaccess.VAccessRule{}}
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) SaveVAccessRules(host string, isProxy bool, configJSON string) string {
|
||||||
|
var config vaccess.VAccessConfig
|
||||||
|
err := json.Unmarshal([]byte(configJSON), &config)
|
||||||
|
if err != nil {
|
||||||
|
return "Error: Invalid JSON"
|
||||||
|
}
|
||||||
|
|
||||||
|
err = vaccess.SaveVAccessConfig(host, isProxy, &config)
|
||||||
|
if err != nil {
|
||||||
|
return "Error: " + err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
return "vAccess saved"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) UpdateSiteCache() string {
|
||||||
|
webserver.UpdateSiteStatusCache()
|
||||||
|
return "Cache updated"
|
||||||
|
}
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
package admin
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
command "vServer/Backend/admin/go/command"
|
|
||||||
config "vServer/Backend/config"
|
|
||||||
tools "vServer/Backend/tools"
|
|
||||||
)
|
|
||||||
|
|
||||||
var adminServer *http.Server
|
|
||||||
|
|
||||||
// Запуск Admin сервера
|
|
||||||
func StartAdmin() {
|
|
||||||
|
|
||||||
// Получаем значения из конфига во время выполнения
|
|
||||||
port_admin := config.ConfigData.Soft_Settings.Admin_port
|
|
||||||
host_admin := config.ConfigData.Soft_Settings.Admin_host
|
|
||||||
|
|
||||||
if tools.Port_check("ADMIN", host_admin, port_admin) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Создаем оптимизированный мультиплексор для админ сервера
|
|
||||||
mux := http.NewServeMux()
|
|
||||||
|
|
||||||
// Регистрируем специализированные обработчики (быстрая маршрутизация)
|
|
||||||
mux.HandleFunc("/api/", command.ApiHandler) // API эндпоинты
|
|
||||||
mux.HandleFunc("/json/", command.JsonHandler) // JSON данные
|
|
||||||
mux.HandleFunc("/service/", command.ServiceHandler) // Сервисные команды POST
|
|
||||||
mux.HandleFunc("/", command.StaticHandler) // Статические файлы
|
|
||||||
|
|
||||||
// Создаем Admin сервер (только localhost для безопасности)
|
|
||||||
adminServer = &http.Server{
|
|
||||||
Addr: host_admin + ":" + port_admin,
|
|
||||||
Handler: mux,
|
|
||||||
}
|
|
||||||
|
|
||||||
tools.Logs_file(0, "ADMIN", "🛠️ Admin панель запущена на порту "+port_admin, "logs_http.log", true)
|
|
||||||
|
|
||||||
if err := adminServer.ListenAndServe(); err != nil {
|
|
||||||
// Игнорируем нормальную ошибку при остановке сервера
|
|
||||||
if err.Error() != "http: Server closed" {
|
|
||||||
tools.Logs_file(1, "ADMIN", "❌ Ошибка запуска админ сервера: "+err.Error(), "logs_http.log", true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
package command
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io/fs"
|
|
||||||
"net/http"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
webserver "vServer/Backend/WebServer"
|
|
||||||
admin "vServer/Backend/admin"
|
|
||||||
json "vServer/Backend/admin/go/json"
|
|
||||||
)
|
|
||||||
|
|
||||||
func SecurePost(w http.ResponseWriter, r *http.Request) bool {
|
|
||||||
// Проверяем, что запрос POST (не GET из браузера)
|
|
||||||
|
|
||||||
if webserver.Secure_post {
|
|
||||||
|
|
||||||
if r.Method != "POST" {
|
|
||||||
http.Error(w, "Метод не разрешен. Используйте POST", http.StatusMethodNotAllowed)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// API обработчик для /api/*
|
|
||||||
func ApiHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
path := r.URL.Path
|
|
||||||
|
|
||||||
switch path {
|
|
||||||
case "/api/metrics":
|
|
||||||
json.GetAllMetrics(w)
|
|
||||||
default:
|
|
||||||
http.NotFound(w, r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// JSON обработчик для /json/*
|
|
||||||
func JsonHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
path := r.URL.Path
|
|
||||||
|
|
||||||
switch path {
|
|
||||||
case "/json/server_status.json":
|
|
||||||
w.Write(json.GetServerStatusJSON())
|
|
||||||
case "/json/menu.json":
|
|
||||||
w.Write(json.GetMenuJSON())
|
|
||||||
default:
|
|
||||||
http.NotFound(w, r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Обработчик сервисных команд для /service/*
|
|
||||||
func ServiceHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
path := r.URL.Path
|
|
||||||
|
|
||||||
if !SecurePost(w, r) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if SiteList(w, r, path) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if site_add(w, path) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if Service_Run(w, r, path) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
http.NotFound(w, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Обработчик статических файлов из embed
|
|
||||||
func StaticHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
path := r.URL.Path
|
|
||||||
|
|
||||||
// Убираем ведущий слэш
|
|
||||||
path = strings.TrimPrefix(path, "/")
|
|
||||||
|
|
||||||
// Если пустой путь, показываем index.html
|
|
||||||
if path == "" {
|
|
||||||
path = "html/index.html"
|
|
||||||
} else {
|
|
||||||
path = "html/" + path
|
|
||||||
}
|
|
||||||
|
|
||||||
// Читаем файл из файловой системы (embed или диск)
|
|
||||||
fileSystem := admin.GetFileSystem()
|
|
||||||
content, err := fs.ReadFile(fileSystem, path)
|
|
||||||
if err != nil {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Устанавливаем правильный Content-Type
|
|
||||||
ext := filepath.Ext(path)
|
|
||||||
switch ext {
|
|
||||||
case ".css":
|
|
||||||
w.Header().Set("Content-Type", "text/css")
|
|
||||||
case ".js":
|
|
||||||
w.Header().Set("Content-Type", "application/javascript")
|
|
||||||
case ".json":
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
case ".html":
|
|
||||||
w.Header().Set("Content-Type", "text/html")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Предотвращаем кеширование в режиме разработки (когда UseEmbedded = false)
|
|
||||||
if !admin.UseEmbedded {
|
|
||||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
|
||||||
w.Header().Set("Pragma", "no-cache")
|
|
||||||
w.Header().Set("Expires", "0")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Отдаем файл
|
|
||||||
w.Write(content)
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
package command
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
webserver "vServer/Backend/WebServer"
|
|
||||||
json "vServer/Backend/admin/go/json"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Обработчик команд управления серверами
|
|
||||||
func Service_Run(w http.ResponseWriter, r *http.Request, path string) bool {
|
|
||||||
|
|
||||||
switch path {
|
|
||||||
case "/service/MySql_Stop":
|
|
||||||
webserver.StopMySQLServer()
|
|
||||||
json.UpdateServerStatus("MySQL Server", "stopped")
|
|
||||||
return true
|
|
||||||
|
|
||||||
case "/service/MySql_Start":
|
|
||||||
webserver.StartMySQLServer(false)
|
|
||||||
json.UpdateServerStatus("MySQL Server", "running")
|
|
||||||
return true
|
|
||||||
|
|
||||||
case "/service/Http_Stop":
|
|
||||||
webserver.StopHTTPServer()
|
|
||||||
json.UpdateServerStatus("HTTP Server", "stopped")
|
|
||||||
return true
|
|
||||||
|
|
||||||
case "/service/Http_Start":
|
|
||||||
go webserver.StartHTTP()
|
|
||||||
json.UpdateServerStatus("HTTP Server", "running")
|
|
||||||
return true
|
|
||||||
|
|
||||||
case "/service/Https_Stop":
|
|
||||||
webserver.StopHTTPSServer()
|
|
||||||
json.UpdateServerStatus("HTTPS Server", "stopped")
|
|
||||||
return true
|
|
||||||
|
|
||||||
case "/service/Https_Start":
|
|
||||||
go webserver.StartHTTPS()
|
|
||||||
json.UpdateServerStatus("HTTPS Server", "running")
|
|
||||||
return true
|
|
||||||
|
|
||||||
case "/service/Php_Start":
|
|
||||||
webserver.PHP_Start()
|
|
||||||
json.UpdateServerStatus("PHP Server", "running")
|
|
||||||
return true
|
|
||||||
|
|
||||||
case "/service/Php_Stop":
|
|
||||||
webserver.PHP_Stop()
|
|
||||||
json.UpdateServerStatus("PHP Server", "stopped")
|
|
||||||
return true
|
|
||||||
|
|
||||||
default:
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return false // Команда не найдена
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,269 +0,0 @@
|
|||||||
package command
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
"vServer/Backend/config"
|
|
||||||
"vServer/Backend/tools"
|
|
||||||
)
|
|
||||||
|
|
||||||
var entries []os.DirEntry
|
|
||||||
|
|
||||||
func path_www() {
|
|
||||||
|
|
||||||
wwwPath, err := tools.AbsPath("WebServer/www")
|
|
||||||
tools.Error_check(err, "Ошибка получения пути")
|
|
||||||
|
|
||||||
entries, err = os.ReadDir(wwwPath)
|
|
||||||
tools.Error_check(err, "Ошибка чтения директории")
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func site_type(entry os.DirEntry) string {
|
|
||||||
|
|
||||||
certPath, err := tools.AbsPath("WebServer/cert/" + entry.Name())
|
|
||||||
if err == nil {
|
|
||||||
if _, err := os.Stat(certPath); err == nil {
|
|
||||||
return "https"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "http"
|
|
||||||
}
|
|
||||||
|
|
||||||
func site_alliace(siteName string) []string {
|
|
||||||
// Получаем абсолютный путь к config.json
|
|
||||||
configPath, err := tools.AbsPath("WebServer/config.json")
|
|
||||||
tools.Error_check(err, "Ошибка получения пути к config.json")
|
|
||||||
|
|
||||||
// Читаем содержимое config.json
|
|
||||||
configData, err := os.ReadFile(configPath)
|
|
||||||
tools.Error_check(err, "Ошибка чтения config.json")
|
|
||||||
|
|
||||||
// Структура для парсинга Site_www
|
|
||||||
type SiteConfig struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Host string `json:"host"`
|
|
||||||
Alias []string `json:"alias"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
}
|
|
||||||
type Config struct {
|
|
||||||
SiteWWW []SiteConfig `json:"Site_www"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var config Config
|
|
||||||
err = json.Unmarshal(configData, &config)
|
|
||||||
tools.Error_check(err, "Ошибка парсинга config.json")
|
|
||||||
|
|
||||||
// Ищем алиасы для конкретного сайта
|
|
||||||
for _, site := range config.SiteWWW {
|
|
||||||
if site.Host == siteName {
|
|
||||||
return site.Alias
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Возвращаем пустой массив если сайт не найден
|
|
||||||
return []string{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func site_status(siteName string) string {
|
|
||||||
configPath := "WebServer/config.json"
|
|
||||||
|
|
||||||
// Читаем конфигурационный файл
|
|
||||||
data, err := os.ReadFile(configPath)
|
|
||||||
if err != nil {
|
|
||||||
return "error"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Парсим JSON
|
|
||||||
var config map[string]interface{}
|
|
||||||
if err := json.Unmarshal(data, &config); err != nil {
|
|
||||||
return "error"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Получаем список сайтов
|
|
||||||
siteWww, ok := config["Site_www"].([]interface{})
|
|
||||||
if !ok {
|
|
||||||
return "error"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ищем сайт по host
|
|
||||||
for _, siteInterface := range siteWww {
|
|
||||||
site, ok := siteInterface.(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Проверяем только по host (имя папки)
|
|
||||||
if host, ok := site["host"].(string); ok && host == siteName {
|
|
||||||
if status, ok := site["status"].(string); ok {
|
|
||||||
return status
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "inactive"
|
|
||||||
}
|
|
||||||
|
|
||||||
func SiteList(w http.ResponseWriter, r *http.Request, path string) bool {
|
|
||||||
|
|
||||||
switch path {
|
|
||||||
|
|
||||||
case "/service/Site_List":
|
|
||||||
sites := []map[string]interface{}{}
|
|
||||||
|
|
||||||
path_www()
|
|
||||||
|
|
||||||
for _, entry := range entries {
|
|
||||||
if entry.IsDir() {
|
|
||||||
site := map[string]interface{}{
|
|
||||||
"host": entry.Name(),
|
|
||||||
"type": site_type(entry),
|
|
||||||
"aliases": site_alliace(entry.Name()),
|
|
||||||
"status": site_status(entry.Name()),
|
|
||||||
}
|
|
||||||
sites = append(sites, site)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
metrics := map[string]interface{}{
|
|
||||||
"sites": sites,
|
|
||||||
}
|
|
||||||
|
|
||||||
data, _ := json.MarshalIndent(metrics, "", " ")
|
|
||||||
w.Write(data)
|
|
||||||
|
|
||||||
return true
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func addSiteToConfig(siteName string) error {
|
|
||||||
// Получаем абсолютный путь к config.json
|
|
||||||
configPath, err := tools.AbsPath("WebServer/config.json")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Читаем содержимое config.json
|
|
||||||
configData, err := os.ReadFile(configPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Парсим как общий JSON объект
|
|
||||||
var config map[string]interface{}
|
|
||||||
err = json.Unmarshal(configData, &config)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Получаем массив сайтов
|
|
||||||
siteWWW, ok := config["Site_www"].([]interface{})
|
|
||||||
if !ok {
|
|
||||||
siteWWW = []interface{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Создаем новый сайт в том же формате что уже есть
|
|
||||||
newSite := map[string]interface{}{
|
|
||||||
"name": siteName,
|
|
||||||
"host": siteName,
|
|
||||||
"alias": []string{""},
|
|
||||||
"status": "active",
|
|
||||||
}
|
|
||||||
|
|
||||||
// Добавляем новый сайт в массив
|
|
||||||
config["Site_www"] = append(siteWWW, newSite)
|
|
||||||
|
|
||||||
// Сохраняем обновленный конфиг
|
|
||||||
updatedData, err := json.MarshalIndent(config, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Делаем массивы алиасов в одну строку
|
|
||||||
re := regexp.MustCompile(`"alias": \[\s+"([^"]*?)"\s+\]`)
|
|
||||||
compactData := re.ReplaceAll(updatedData, []byte(`"alias": ["$1"]`))
|
|
||||||
|
|
||||||
// Исправляем отступы после Site_www
|
|
||||||
dataStr := string(compactData)
|
|
||||||
dataStr = strings.ReplaceAll(dataStr, ` ],
|
|
||||||
"Soft_Settings"`, ` ],
|
|
||||||
|
|
||||||
"Soft_Settings"`)
|
|
||||||
compactData = []byte(dataStr)
|
|
||||||
|
|
||||||
err = os.WriteFile(configPath, compactData, 0644)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func site_add(w http.ResponseWriter, path string) bool {
|
|
||||||
|
|
||||||
// URL параметры: /service/Site_Add/sitename
|
|
||||||
if strings.HasPrefix(path, "/service/Site_Add/") {
|
|
||||||
siteName := strings.TrimPrefix(path, "/service/Site_Add/")
|
|
||||||
|
|
||||||
if siteName == "" {
|
|
||||||
w.WriteHeader(400)
|
|
||||||
w.Write([]byte(`{"status":"error","message":"Не указано имя сайта в URL"}`))
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
wwwPath := "WebServer/www/" + siteName
|
|
||||||
|
|
||||||
// Проверяем существует ли уже такой сайт
|
|
||||||
if _, err := os.Stat(wwwPath); err == nil {
|
|
||||||
w.WriteHeader(409) // Conflict
|
|
||||||
w.Write([]byte(`{"status":"error","message":"Сайт ` + siteName + ` уже существует"}`))
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
err := os.MkdirAll(wwwPath, 0755)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(500)
|
|
||||||
w.Write([]byte(`{"status":"error","message":"Ошибка создания папки сайта"}`))
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Добавляем сайт в config.json
|
|
||||||
err = addSiteToConfig(siteName)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(500)
|
|
||||||
w.Write([]byte(`{"status":"error","message":"Ошибка добавления в конфигурацию: ` + err.Error() + `"}`))
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Создаем папку public_www
|
|
||||||
publicWwwPath := wwwPath + "/public_www"
|
|
||||||
err = os.MkdirAll(publicWwwPath, 0755)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(500)
|
|
||||||
w.Write([]byte(`{"status":"error","message":"Ошибка создания папки public_www"}`))
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
indexFilePath := wwwPath + "/public_www/index.html"
|
|
||||||
indexContent := "Привет друг! Твой сайт создан!"
|
|
||||||
err = os.WriteFile(indexFilePath, []byte(indexContent), 0644)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(500)
|
|
||||||
w.Write([]byte(`{"status":"error","message":"Ошибка создания index.html: ` + err.Error() + `"}`))
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Write([]byte(`{"status":"ok","message":"Сайт ` + siteName + ` успешно создан и добавлен в конфигурацию"}`))
|
|
||||||
config.LoadConfig()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
package json
|
|
||||||
|
|
||||||
import "encoding/json"
|
|
||||||
|
|
||||||
// Данные серверов
|
|
||||||
var ServerStatus = []map[string]interface{}{
|
|
||||||
{"NameService": "HTTP Server", "Port": 80, "Status": "stopped"},
|
|
||||||
{"NameService": "HTTPS Server", "Port": 443, "Status": "stopped"},
|
|
||||||
{"NameService": "PHP Server", "Port": 9000, "Status": "stopped"},
|
|
||||||
{"NameService": "MySQL Server", "Port": 3306, "Status": "stopped"},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Данные меню
|
|
||||||
var MenuData = []map[string]interface{}{
|
|
||||||
{"name": "Dashboard", "icon": "🏠", "url": "#dashboard", "active": true},
|
|
||||||
{"name": "Серверы", "icon": "🖥️", "url": "#servers", "active": false},
|
|
||||||
{"name": "Сайты", "icon": "🌐", "url": "#sites", "active": false},
|
|
||||||
{"name": "SSL Сертификаты", "icon": "🔒", "url": "#certificates", "active": false},
|
|
||||||
{"name": "Файловый менеджер", "icon": "📁", "url": "#files", "active": false},
|
|
||||||
{"name": "Базы данных", "icon": "🗄️", "url": "#databases", "active": false},
|
|
||||||
{"name": "Логи", "icon": "📋", "url": "#logs", "active": false},
|
|
||||||
{"name": "Настройки", "icon": "⚙️", "url": "#settings", "active": false},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Функция обновления статуса сервера
|
|
||||||
func UpdateServerStatus(serviceName, status string) {
|
|
||||||
for i := range ServerStatus {
|
|
||||||
if ServerStatus[i]["NameService"] == serviceName {
|
|
||||||
ServerStatus[i]["Status"] = status
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Получить JSON серверов
|
|
||||||
func GetServerStatusJSON() []byte {
|
|
||||||
data, _ := json.Marshal(ServerStatus)
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
// Получить JSON меню
|
|
||||||
func GetMenuJSON() []byte {
|
|
||||||
data, _ := json.Marshal(MenuData)
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
package json
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
"vServer/Backend/tools"
|
|
||||||
)
|
|
||||||
|
|
||||||
var CPU_NAME string
|
|
||||||
var CPU_GHz string
|
|
||||||
var CPU_Cores string
|
|
||||||
var CPU_Using string
|
|
||||||
var Disk_Size string
|
|
||||||
var Disk_Free string
|
|
||||||
var Disk_Used string
|
|
||||||
var Disk_Name string
|
|
||||||
var RAM_Using string
|
|
||||||
var RAM_Total string
|
|
||||||
|
|
||||||
// Инициализация при запуске пакета
|
|
||||||
func init() {
|
|
||||||
// Загружаем статичные данные один раз при старте
|
|
||||||
UpdateMetrics()
|
|
||||||
// Загружаем динамические данные в фоне
|
|
||||||
go updateMetricsBackground()
|
|
||||||
}
|
|
||||||
|
|
||||||
func UpdateMetrics() {
|
|
||||||
commands := []string{
|
|
||||||
`$name = (Get-WmiObject Win32_DiskDrive | Where-Object { $_.Index -eq (Get-WmiObject Win32_DiskPartition | Where-Object { $_.DeviceID -eq ((Get-WmiObject Win32_LogicalDiskToPartition | Where-Object { $_.Dependent -match "C:" }).Antecedent -split '"')[1] }).DiskIndex }).Model`,
|
|
||||||
`$size = "{0} GB" -f [math]::Round(((Get-PSDrive -Name C).Used + (Get-PSDrive -Name C).Free) / 1GB)`,
|
|
||||||
"$cpuInfo = Get-CimInstance Win32_Processor",
|
|
||||||
"$cpuCores = $cpuInfo.NumberOfCores",
|
|
||||||
"$cpuName = $cpuInfo.Name",
|
|
||||||
`$ram_total = [math]::Round((Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory / 1GB, 2)`,
|
|
||||||
"Write-Output \"$cpuName|$cpuCores|$name|$size|$ram_total\"",
|
|
||||||
}
|
|
||||||
|
|
||||||
// Выполняем команды и получаем результат
|
|
||||||
result := tools.RunPersistentScript(commands)
|
|
||||||
|
|
||||||
// Парсим результат для статичных данных
|
|
||||||
parts := strings.Split(result, "|")
|
|
||||||
if len(parts) >= 4 {
|
|
||||||
cpuName := strings.TrimSpace(parts[0])
|
|
||||||
cpuCores := strings.TrimSpace(parts[1])
|
|
||||||
diskName := strings.TrimSpace(parts[2])
|
|
||||||
diskSize := strings.TrimSpace(parts[3])
|
|
||||||
ramTotal := strings.TrimSpace(parts[4])
|
|
||||||
|
|
||||||
// Обновляем глобальные переменные
|
|
||||||
CPU_NAME = cpuName
|
|
||||||
CPU_Cores = cpuCores
|
|
||||||
Disk_Name = diskName
|
|
||||||
Disk_Size = diskSize
|
|
||||||
RAM_Total = ramTotal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Фоновое обновление динамических метрик (внутреннее)
|
|
||||||
func updateMetricsBackground() {
|
|
||||||
|
|
||||||
updateDynamicMetrics := func() {
|
|
||||||
commands := []string{
|
|
||||||
"$cpuInfo = Get-CimInstance Win32_Processor",
|
|
||||||
"$cpuGHz = $cpuInfo.MaxClockSpeed",
|
|
||||||
"$cpuUsage = $cpuInfo.LoadPercentage",
|
|
||||||
`$used = "{0} GB" -f [math]::Round(((Get-PSDrive -Name C).Used / 1GB))`,
|
|
||||||
`$free = "{0} GB" -f [math]::Round(((Get-PSDrive -Name C).Free / 1GB))`,
|
|
||||||
`$ram_using = [math]::Round((Get-CimInstance Win32_OperatingSystem | % {($_.TotalVisibleMemorySize - $_.FreePhysicalMemory) / 1MB}), 2)`,
|
|
||||||
"Write-Output \"$cpuGHz|$cpuUsage|$used|$free|$ram_using\"",
|
|
||||||
}
|
|
||||||
|
|
||||||
// Один запуск PowerShell для динамических команд!
|
|
||||||
result := tools.RunPersistentScript(commands)
|
|
||||||
|
|
||||||
// Парсим результат для динамических данных
|
|
||||||
parts := strings.Split(result, "|")
|
|
||||||
if len(parts) >= 4 {
|
|
||||||
cpuGHz := strings.TrimSpace(parts[0])
|
|
||||||
cpuUsage := strings.TrimSpace(parts[1])
|
|
||||||
diskUsed := strings.TrimSpace(parts[2])
|
|
||||||
diskFree := strings.TrimSpace(parts[3])
|
|
||||||
ramUsing := strings.TrimSpace(parts[4])
|
|
||||||
// Обновляем глобальные переменные
|
|
||||||
CPU_GHz = cpuGHz
|
|
||||||
CPU_Using = cpuUsage
|
|
||||||
Disk_Used = diskUsed
|
|
||||||
Disk_Free = diskFree
|
|
||||||
RAM_Using = ramUsing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Выполняем сразу при запуске
|
|
||||||
updateDynamicMetrics()
|
|
||||||
|
|
||||||
// Затем каждые 5 секунд
|
|
||||||
for range time.NewTicker(5 * time.Second).C {
|
|
||||||
updateDynamicMetrics()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Получить JSON системных метрик
|
|
||||||
func GetAllMetrics(w http.ResponseWriter) {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.Write(GetMetricsJSON())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Получить JSON метрик
|
|
||||||
func GetMetricsJSON() []byte {
|
|
||||||
metrics := map[string]interface{}{
|
|
||||||
"cpu_name": CPU_NAME,
|
|
||||||
"cpu_ghz": CPU_GHz,
|
|
||||||
"cpu_cores": CPU_Cores,
|
|
||||||
"cpu_usage": CPU_Using,
|
|
||||||
"disk_name": Disk_Name,
|
|
||||||
"disk_size": Disk_Size,
|
|
||||||
"disk_used": Disk_Used,
|
|
||||||
"disk_free": Disk_Free,
|
|
||||||
"ram_using": RAM_Using,
|
|
||||||
"ram_total": RAM_Total,
|
|
||||||
"server_uptime": tools.ServerUptime("get"),
|
|
||||||
}
|
|
||||||
|
|
||||||
data, _ := json.Marshal(metrics)
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
30
Backend/admin/go/proxy/proxy.go
Normal file
30
Backend/admin/go/proxy/proxy.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package proxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
config "vServer/Backend/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetProxyList() []ProxyInfo {
|
||||||
|
proxies := make([]ProxyInfo, 0)
|
||||||
|
|
||||||
|
for _, proxyConfig := range config.ConfigData.Proxy_Service {
|
||||||
|
status := "disabled"
|
||||||
|
if proxyConfig.Enable {
|
||||||
|
status = "active"
|
||||||
|
}
|
||||||
|
|
||||||
|
proxyInfo := ProxyInfo{
|
||||||
|
Enable: proxyConfig.Enable,
|
||||||
|
ExternalDomain: proxyConfig.ExternalDomain,
|
||||||
|
LocalAddress: proxyConfig.LocalAddress,
|
||||||
|
LocalPort: proxyConfig.LocalPort,
|
||||||
|
ServiceHTTPSuse: proxyConfig.ServiceHTTPSuse,
|
||||||
|
AutoHTTPS: proxyConfig.AutoHTTPS,
|
||||||
|
Status: status,
|
||||||
|
}
|
||||||
|
proxies = append(proxies, proxyInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
return proxies
|
||||||
|
}
|
||||||
|
|
||||||
12
Backend/admin/go/proxy/types.go
Normal file
12
Backend/admin/go/proxy/types.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package proxy
|
||||||
|
|
||||||
|
type ProxyInfo struct {
|
||||||
|
Enable bool `json:"enable"`
|
||||||
|
ExternalDomain string `json:"external_domain"`
|
||||||
|
LocalAddress string `json:"local_address"`
|
||||||
|
LocalPort string `json:"local_port"`
|
||||||
|
ServiceHTTPSuse bool `json:"service_https_use"`
|
||||||
|
AutoHTTPS bool `json:"auto_https"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
92
Backend/admin/go/services/services.go
Normal file
92
Backend/admin/go/services/services.go
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
webserver "vServer/Backend/WebServer"
|
||||||
|
config "vServer/Backend/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetAllServicesStatus() AllServicesStatus {
|
||||||
|
return AllServicesStatus{
|
||||||
|
HTTP: getHTTPStatus(),
|
||||||
|
HTTPS: getHTTPSStatus(),
|
||||||
|
MySQL: getMySQLStatus(),
|
||||||
|
PHP: getPHPStatus(),
|
||||||
|
Proxy: getProxyStatus(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getHTTPStatus() ServiceStatus {
|
||||||
|
// Используем внутренний статус вместо TCP проверки
|
||||||
|
return ServiceStatus{
|
||||||
|
Name: "HTTP",
|
||||||
|
Status: webserver.GetHTTPStatus(),
|
||||||
|
Port: "80",
|
||||||
|
Info: "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getHTTPSStatus() ServiceStatus {
|
||||||
|
// Используем внутренний статус вместо TCP проверки
|
||||||
|
return ServiceStatus{
|
||||||
|
Name: "HTTPS",
|
||||||
|
Status: webserver.GetHTTPSStatus(),
|
||||||
|
Port: "443",
|
||||||
|
Info: "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getMySQLStatus() ServiceStatus {
|
||||||
|
port := fmt.Sprintf("%d", config.ConfigData.Soft_Settings.Mysql_port)
|
||||||
|
|
||||||
|
// Используем внутренний статус вместо TCP проверки
|
||||||
|
// чтобы не вызывать connect_errors в MySQL
|
||||||
|
return ServiceStatus{
|
||||||
|
Name: "MySQL",
|
||||||
|
Status: webserver.GetMySQLStatus(),
|
||||||
|
Port: port,
|
||||||
|
Info: "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPHPStatus() ServiceStatus {
|
||||||
|
basePort := config.ConfigData.Soft_Settings.Php_port
|
||||||
|
|
||||||
|
// Диапазон портов для 4 воркеров
|
||||||
|
portRange := fmt.Sprintf("%d-%d", basePort, basePort+3)
|
||||||
|
|
||||||
|
// Используем внутренний статус вместо TCP проверки
|
||||||
|
return ServiceStatus{
|
||||||
|
Name: "PHP",
|
||||||
|
Status: webserver.GetPHPStatus(),
|
||||||
|
Port: portRange,
|
||||||
|
Info: "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getProxyStatus() ServiceStatus {
|
||||||
|
activeCount := 0
|
||||||
|
totalCount := len(config.ConfigData.Proxy_Service)
|
||||||
|
|
||||||
|
for _, proxy := range config.ConfigData.Proxy_Service {
|
||||||
|
if proxy.Enable {
|
||||||
|
activeCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info := fmt.Sprintf("%d из %d", activeCount, totalCount)
|
||||||
|
|
||||||
|
// Проверяем глобальный флаг и статус HTTP/HTTPS
|
||||||
|
proxyEnabled := config.ConfigData.Soft_Settings.Proxy_enabled
|
||||||
|
httpRunning := webserver.GetHTTPStatus()
|
||||||
|
httpsRunning := webserver.GetHTTPSStatus()
|
||||||
|
|
||||||
|
status := proxyEnabled && (httpRunning || httpsRunning)
|
||||||
|
|
||||||
|
return ServiceStatus{
|
||||||
|
Name: "Proxy",
|
||||||
|
Status: status,
|
||||||
|
Port: "-",
|
||||||
|
Info: info,
|
||||||
|
}
|
||||||
|
}
|
||||||
16
Backend/admin/go/services/types.go
Normal file
16
Backend/admin/go/services/types.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
type ServiceStatus struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Status bool `json:"status"`
|
||||||
|
Port string `json:"port"`
|
||||||
|
Info string `json:"info"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AllServicesStatus struct {
|
||||||
|
HTTP ServiceStatus `json:"http"`
|
||||||
|
HTTPS ServiceStatus `json:"https"`
|
||||||
|
MySQL ServiceStatus `json:"mysql"`
|
||||||
|
PHP ServiceStatus `json:"php"`
|
||||||
|
Proxy ServiceStatus `json:"proxy"`
|
||||||
|
}
|
||||||
24
Backend/admin/go/sites/sites.go
Normal file
24
Backend/admin/go/sites/sites.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package sites
|
||||||
|
|
||||||
|
import (
|
||||||
|
config "vServer/Backend/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetSitesList() []SiteInfo {
|
||||||
|
sites := make([]SiteInfo, 0)
|
||||||
|
|
||||||
|
for _, site := range config.ConfigData.Site_www {
|
||||||
|
siteInfo := SiteInfo{
|
||||||
|
Name: site.Name,
|
||||||
|
Host: site.Host,
|
||||||
|
Alias: site.Alias,
|
||||||
|
Status: site.Status,
|
||||||
|
RootFile: site.Root_file,
|
||||||
|
RootFileRouting: site.Root_file_routing,
|
||||||
|
}
|
||||||
|
sites = append(sites, siteInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sites
|
||||||
|
}
|
||||||
|
|
||||||
11
Backend/admin/go/sites/types.go
Normal file
11
Backend/admin/go/sites/types.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package sites
|
||||||
|
|
||||||
|
type SiteInfo struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Host string `json:"host"`
|
||||||
|
Alias []string `json:"alias"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
RootFile string `json:"root_file"`
|
||||||
|
RootFileRouting bool `json:"root_file_routing"`
|
||||||
|
}
|
||||||
|
|
||||||
14
Backend/admin/go/vaccess/types.go
Normal file
14
Backend/admin/go/vaccess/types.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package vaccess
|
||||||
|
|
||||||
|
type VAccessRule struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
TypeFile []string `json:"type_file"`
|
||||||
|
PathAccess []string `json:"path_access"`
|
||||||
|
IPList []string `json:"ip_list"`
|
||||||
|
ExceptionsDir []string `json:"exceptions_dir"`
|
||||||
|
UrlError string `json:"url_error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type VAccessConfig struct {
|
||||||
|
Rules []VAccessRule `json:"rules"`
|
||||||
|
}
|
||||||
158
Backend/admin/go/vaccess/vaccess.go
Normal file
158
Backend/admin/go/vaccess/vaccess.go
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
package vaccess
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
tools "vServer/Backend/tools"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetVAccessPath(host string, isProxy bool) string {
|
||||||
|
if isProxy {
|
||||||
|
return fmt.Sprintf("WebServer/tools/Proxy_vAccess/%s_vAccess.conf", host)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("WebServer/www/%s/vAccess.conf", host)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetVAccessConfig(host string, isProxy bool) (*VAccessConfig, error) {
|
||||||
|
filePath := GetVAccessPath(host, isProxy)
|
||||||
|
|
||||||
|
// Проверяем существование файла
|
||||||
|
absPath, _ := tools.AbsPath(filePath)
|
||||||
|
if _, err := os.Stat(absPath); os.IsNotExist(err) {
|
||||||
|
// Файл не существует - возвращаем пустую конфигурацию
|
||||||
|
return &VAccessConfig{Rules: []VAccessRule{}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Open(absPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
config := &VAccessConfig{}
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
|
||||||
|
var currentRule *VAccessRule
|
||||||
|
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
|
||||||
|
// Пропускаем пустые строки и комментарии
|
||||||
|
if line == "" || strings.HasPrefix(line, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Парсим параметры
|
||||||
|
if strings.Contains(line, ":") {
|
||||||
|
parts := strings.SplitN(line, ":", 2)
|
||||||
|
if len(parts) == 2 {
|
||||||
|
key := strings.TrimSpace(parts[0])
|
||||||
|
value := strings.TrimSpace(parts[1])
|
||||||
|
|
||||||
|
switch key {
|
||||||
|
case "type":
|
||||||
|
// Начало нового правила - сохраняем предыдущее
|
||||||
|
if currentRule != nil && currentRule.Type != "" {
|
||||||
|
config.Rules = append(config.Rules, *currentRule)
|
||||||
|
}
|
||||||
|
// Создаём новое правило
|
||||||
|
currentRule = &VAccessRule{}
|
||||||
|
currentRule.Type = value
|
||||||
|
case "type_file":
|
||||||
|
if currentRule != nil {
|
||||||
|
currentRule.TypeFile = splitAndTrim(value)
|
||||||
|
}
|
||||||
|
case "path_access":
|
||||||
|
if currentRule != nil {
|
||||||
|
currentRule.PathAccess = splitAndTrim(value)
|
||||||
|
}
|
||||||
|
case "ip_list":
|
||||||
|
if currentRule != nil {
|
||||||
|
currentRule.IPList = splitAndTrim(value)
|
||||||
|
}
|
||||||
|
case "exceptions_dir":
|
||||||
|
if currentRule != nil {
|
||||||
|
currentRule.ExceptionsDir = splitAndTrim(value)
|
||||||
|
}
|
||||||
|
case "url_error":
|
||||||
|
if currentRule != nil {
|
||||||
|
currentRule.UrlError = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем последнее правило
|
||||||
|
if currentRule != nil && currentRule.Type != "" {
|
||||||
|
config.Rules = append(config.Rules, *currentRule)
|
||||||
|
}
|
||||||
|
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SaveVAccessConfig(host string, isProxy bool, config *VAccessConfig) error {
|
||||||
|
filePath := GetVAccessPath(host, isProxy)
|
||||||
|
|
||||||
|
// Создаём директорию если не существует
|
||||||
|
dir := ""
|
||||||
|
if isProxy {
|
||||||
|
dir = "WebServer/tools/Proxy_vAccess"
|
||||||
|
} else {
|
||||||
|
dir = fmt.Sprintf("WebServer/www/%s", host)
|
||||||
|
}
|
||||||
|
|
||||||
|
absDir, _ := tools.AbsPath(dir)
|
||||||
|
os.MkdirAll(absDir, 0755)
|
||||||
|
|
||||||
|
// Получаем абсолютный путь к файлу
|
||||||
|
absPath, err := tools.AbsPath(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Формируем содержимое файла
|
||||||
|
var content strings.Builder
|
||||||
|
|
||||||
|
content.WriteString("# vAccess Configuration\n")
|
||||||
|
content.WriteString("# Правила применяются сверху вниз\n\n")
|
||||||
|
|
||||||
|
for i, rule := range config.Rules {
|
||||||
|
content.WriteString(fmt.Sprintf("# Правило %d\n", i+1))
|
||||||
|
content.WriteString(fmt.Sprintf("type: %s\n", rule.Type))
|
||||||
|
|
||||||
|
if len(rule.TypeFile) > 0 {
|
||||||
|
content.WriteString(fmt.Sprintf("type_file: %s\n", strings.Join(rule.TypeFile, ", ")))
|
||||||
|
}
|
||||||
|
if len(rule.PathAccess) > 0 {
|
||||||
|
content.WriteString(fmt.Sprintf("path_access: %s\n", strings.Join(rule.PathAccess, ", ")))
|
||||||
|
}
|
||||||
|
if len(rule.IPList) > 0 {
|
||||||
|
content.WriteString(fmt.Sprintf("ip_list: %s\n", strings.Join(rule.IPList, ", ")))
|
||||||
|
}
|
||||||
|
if len(rule.ExceptionsDir) > 0 {
|
||||||
|
content.WriteString(fmt.Sprintf("exceptions_dir: %s\n", strings.Join(rule.ExceptionsDir, ", ")))
|
||||||
|
}
|
||||||
|
if rule.UrlError != "" {
|
||||||
|
content.WriteString(fmt.Sprintf("url_error: %s\n", rule.UrlError))
|
||||||
|
}
|
||||||
|
|
||||||
|
content.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.WriteFile(absPath, []byte(content.String()), 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitAndTrim(s string) []string {
|
||||||
|
parts := strings.Split(s, ",")
|
||||||
|
result := []string{}
|
||||||
|
for _, part := range parts {
|
||||||
|
trimmed := strings.TrimSpace(part)
|
||||||
|
if trimmed != "" {
|
||||||
|
result = append(result, trimmed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -1,652 +0,0 @@
|
|||||||
/* Стили дашборда админ-панели */
|
|
||||||
|
|
||||||
.dashboard-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
gap: 2rem;
|
|
||||||
margin-bottom: 6rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1200px) {
|
|
||||||
.dashboard-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 1.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Карточки дашборда */
|
|
||||||
.dashboard-card {
|
|
||||||
background: linear-gradient(135deg, rgba(26, 37, 47, 0.95), rgba(20, 30, 40, 0.9));
|
|
||||||
backdrop-filter: blur(20px);
|
|
||||||
border-radius: 16px;
|
|
||||||
border: 1px solid rgba(52, 152, 219, 0.25);
|
|
||||||
box-shadow:
|
|
||||||
0 8px 32px rgba(0, 0, 0, 0.4),
|
|
||||||
0 0 0 1px rgba(52, 152, 219, 0.1),
|
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
|
||||||
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard-card::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 2px;
|
|
||||||
background: linear-gradient(90deg, #3498db, #2ecc71, #9b59b6);
|
|
||||||
background-size: 400% 400%;
|
|
||||||
animation: gradientFlow 6s ease-in-out infinite;
|
|
||||||
box-shadow: 0 2px 10px rgba(52, 152, 219, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes gradientFlow {
|
|
||||||
0%, 100% {
|
|
||||||
background-position: 0% 50%;
|
|
||||||
}
|
|
||||||
25% {
|
|
||||||
background-position: 100% 50%;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
background-position: 200% 50%;
|
|
||||||
}
|
|
||||||
75% {
|
|
||||||
background-position: 300% 50%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.dashboard-card.full-width {
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Заголовки карточек */
|
|
||||||
.card-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 1.2rem 1.5rem;
|
|
||||||
border-bottom: 1px solid rgba(52, 152, 219, 0.15);
|
|
||||||
background: linear-gradient(135deg, rgba(52, 152, 219, 0.08), rgba(46, 204, 113, 0.05));
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-header::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 1px;
|
|
||||||
background: linear-gradient(90deg, transparent, rgba(52, 152, 219, 0.3), transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-title {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 1.4rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #ecf0f1;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-icon {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
margin-right: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Содержимое карточек */
|
|
||||||
.card-content {
|
|
||||||
padding: 1.5rem 2rem 2rem 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Статистика */
|
|
||||||
.stats-row {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
gap: 1rem;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
padding-bottom: 1.5rem;
|
|
||||||
border-bottom: 1px solid rgba(52, 152, 219, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-item {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-number {
|
|
||||||
font-size: 2rem;
|
|
||||||
font-weight: 700;
|
|
||||||
background: linear-gradient(135deg, #3498db, #2ecc71);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-clip: text;
|
|
||||||
margin-bottom: 0.3rem;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-number.active-stat {
|
|
||||||
background: linear-gradient(135deg, #2ecc71, #27ae60);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-clip: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-number.inactive-stat {
|
|
||||||
background: linear-gradient(135deg, #e74c3c, #c0392b);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-clip: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-number.valid-stat {
|
|
||||||
background: linear-gradient(135deg, #2ecc71, #27ae60);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-clip: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-number.warning-stat {
|
|
||||||
background: linear-gradient(135deg, #f39c12, #e67e22);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-clip: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-label {
|
|
||||||
color: #95a5a6;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 1px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Превью секции */
|
|
||||||
.sites-preview, .certs-preview {
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-header {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-title {
|
|
||||||
color: #bdc3c7;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-weight: 600;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Списки сайтов */
|
|
||||||
.sites-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 1rem;
|
|
||||||
background: rgba(52, 152, 219, 0.05);
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid rgba(52, 152, 219, 0.1);
|
|
||||||
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-item:hover {
|
|
||||||
background: rgba(52, 152, 219, 0.1);
|
|
||||||
transform: translateX(4px);
|
|
||||||
border-color: rgba(52, 152, 219, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-status {
|
|
||||||
width: 10px;
|
|
||||||
height: 10px;
|
|
||||||
border-radius: 50%;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-status.active {
|
|
||||||
background: #2ecc71;
|
|
||||||
box-shadow: 0 0 8px rgba(46, 204, 113, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-status.inactive {
|
|
||||||
background: #e74c3c;
|
|
||||||
box-shadow: 0 0 8px rgba(231, 76, 60, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-info {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-name {
|
|
||||||
color: #ecf0f1;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-details {
|
|
||||||
color: #95a5a6;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Списки сертификатов */
|
|
||||||
.certs-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cert-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 1rem;
|
|
||||||
background: rgba(52, 152, 219, 0.05);
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid rgba(52, 152, 219, 0.1);
|
|
||||||
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cert-item:hover {
|
|
||||||
background: rgba(52, 152, 219, 0.1);
|
|
||||||
transform: translateX(4px);
|
|
||||||
border-color: rgba(52, 152, 219, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cert-status {
|
|
||||||
width: 10px;
|
|
||||||
height: 10px;
|
|
||||||
border-radius: 50%;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cert-status.valid {
|
|
||||||
background: #2ecc71;
|
|
||||||
box-shadow: 0 0 8px rgba(46, 204, 113, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cert-status.warning {
|
|
||||||
background: #f39c12;
|
|
||||||
box-shadow: 0 0 8px rgba(243, 156, 18, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cert-status.expired {
|
|
||||||
background: #e74c3c;
|
|
||||||
box-shadow: 0 0 8px rgba(231, 76, 60, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cert-info {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cert-name {
|
|
||||||
color: #ecf0f1;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cert-details {
|
|
||||||
color: #95a5a6;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cert-expires {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-end;
|
|
||||||
gap: 0.1rem;
|
|
||||||
min-width: 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.expires-date {
|
|
||||||
color: #bdc3c7;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.expires-days {
|
|
||||||
color: #95a5a6;
|
|
||||||
font-size: 0.7rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cert-expires.warning .expires-date {
|
|
||||||
color: #f39c12;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cert-expires.warning .expires-days {
|
|
||||||
color: #e67e22;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cert-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Серверы */
|
|
||||||
.servers-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.server-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 1rem;
|
|
||||||
background: rgba(52, 152, 219, 0.05);
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid rgba(52, 152, 219, 0.1);
|
|
||||||
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
|
||||||
}
|
|
||||||
|
|
||||||
.server-item:hover {
|
|
||||||
background: rgba(52, 152, 219, 0.1);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.server-status {
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
border-radius: 50%;
|
|
||||||
margin-right: 1rem;
|
|
||||||
flex-shrink: 0;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.server-status.running {
|
|
||||||
background: #2ecc71;
|
|
||||||
box-shadow: 0 0 12px rgba(46, 204, 113, 0.5);
|
|
||||||
animation: pulse 2s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.server-status.stopped {
|
|
||||||
background: #e74c3c;
|
|
||||||
box-shadow: 0 0 12px rgba(231, 76, 60, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.server-status.starting {
|
|
||||||
background: #f39c12;
|
|
||||||
box-shadow: 0 0 12px rgba(243, 156, 18, 0.5);
|
|
||||||
animation: pulse 1.5s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.server-status.stopping {
|
|
||||||
background: #f39c12;
|
|
||||||
box-shadow: 0 0 12px rgba(243, 156, 18, 0.5);
|
|
||||||
animation: pulse 1.5s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.server-status.warning {
|
|
||||||
background: #f39c12;
|
|
||||||
box-shadow: 0 0 12px rgba(243, 156, 18, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.server-info {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.server-name {
|
|
||||||
color: #ecf0f1;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 1rem;
|
|
||||||
margin-bottom: 0.3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.server-details {
|
|
||||||
color: #95a5a6;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Кнопки действий */
|
|
||||||
.btn-icon {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
font-size: 1rem;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 0.3rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
transition: all 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-icon:hover {
|
|
||||||
opacity: 1;
|
|
||||||
background: rgba(52, 152, 219, 0.1);
|
|
||||||
transform: scale(1.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-icon.disabled {
|
|
||||||
opacity: 0.4;
|
|
||||||
cursor: not-allowed;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-icon.disabled:hover {
|
|
||||||
opacity: 0.4;
|
|
||||||
background: none;
|
|
||||||
transform: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-icon.warning {
|
|
||||||
color: #f39c12;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-icon.warning:hover {
|
|
||||||
background: rgba(243, 156, 18, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Кнопки - новый дизайн */
|
|
||||||
.btn-primary {
|
|
||||||
background: linear-gradient(135deg, #3498db, #2980b9);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 0.4rem 1rem;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
box-shadow: 0 2px 8px rgba(52, 152, 219, 0.25);
|
|
||||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: -100%;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
|
||||||
transition: left 0.6s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:hover {
|
|
||||||
background: linear-gradient(135deg, #2980b9, #3498db);
|
|
||||||
transform: translateY(-1px);
|
|
||||||
box-shadow: 0 4px 12px rgba(52, 152, 219, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:hover::before {
|
|
||||||
left: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:active {
|
|
||||||
transform: translateY(0);
|
|
||||||
box-shadow: 0 2px 8px rgba(52, 152, 219, 0.25);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Иконка плюса */
|
|
||||||
.btn-primary .btn-plus {
|
|
||||||
background: rgba(255, 255, 255, 0.25);
|
|
||||||
border-radius: 50%;
|
|
||||||
width: 14px;
|
|
||||||
height: 14px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 10px;
|
|
||||||
font-weight: 700;
|
|
||||||
margin-right: 0.5rem;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Специальные стили для кнопки добавления сайта */
|
|
||||||
#add-site-btn {
|
|
||||||
background: linear-gradient(135deg, #2ecc71, #27ae60);
|
|
||||||
box-shadow: 0 2px 8px rgba(46, 204, 113, 0.25);
|
|
||||||
}
|
|
||||||
|
|
||||||
#add-site-btn:hover {
|
|
||||||
background: linear-gradient(135deg, #27ae60, #2ecc71);
|
|
||||||
box-shadow: 0 4px 12px rgba(46, 204, 113, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
#add-site-btn:active {
|
|
||||||
box-shadow: 0 2px 8px rgba(46, 204, 113, 0.25);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Специальные стили для кнопки добавления SSL */
|
|
||||||
#add-cert-btn {
|
|
||||||
background: linear-gradient(135deg, #f39c12, #e67e22);
|
|
||||||
box-shadow: 0 2px 8px rgba(243, 156, 18, 0.25);
|
|
||||||
}
|
|
||||||
|
|
||||||
#add-cert-btn:hover {
|
|
||||||
background: linear-gradient(135deg, #e67e22, #f39c12);
|
|
||||||
box-shadow: 0 4px 12px rgba(243, 156, 18, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
#add-cert-btn:active {
|
|
||||||
box-shadow: 0 2px 8px rgba(243, 156, 18, 0.25);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Футер карточек */
|
|
||||||
.card-footer {
|
|
||||||
margin-top: 1rem;
|
|
||||||
padding-top: 1rem;
|
|
||||||
border-top: 1px solid rgba(52, 152, 219, 0.1);
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-link {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: #3498db;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 0.5rem;
|
|
||||||
border-radius: 6px;
|
|
||||||
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-link:hover {
|
|
||||||
background: rgba(52, 152, 219, 0.1);
|
|
||||||
color: #2ecc71;
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.show-all-btn {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Адаптивность дашборда */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.dashboard-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 1.5rem;
|
|
||||||
margin-bottom: 4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-header {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1.5rem;
|
|
||||||
align-items: stretch;
|
|
||||||
padding: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-content {
|
|
||||||
padding: 1rem 1.5rem 1.5rem 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-title {
|
|
||||||
font-size: 1.3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.servers-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stats-row {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-item, .cert-item {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-info, .cert-info {
|
|
||||||
order: 1;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-actions, .cert-actions {
|
|
||||||
order: 3;
|
|
||||||
align-self: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cert-expires {
|
|
||||||
order: 2;
|
|
||||||
align-self: flex-start;
|
|
||||||
align-items: flex-start;
|
|
||||||
min-width: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
.stat-number {
|
|
||||||
font-size: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-title {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.server-item {
|
|
||||||
padding: 0.8rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,228 +0,0 @@
|
|||||||
/* Основные стили админ-панели vServer */
|
|
||||||
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
||||||
background: linear-gradient(135deg, #1a252f 0%, #2c3e50 50%, #34495e 100%);
|
|
||||||
min-height: 100vh;
|
|
||||||
color: #ffffff;
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Фоновые частицы */
|
|
||||||
.floating-particles {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.particle {
|
|
||||||
position: absolute;
|
|
||||||
background: rgba(52, 152, 219, 0.15);
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: float 8s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes float {
|
|
||||||
0%, 100% {
|
|
||||||
transform: translateY(0px) rotate(0deg);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
transform: translateY(-20px) rotate(180deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Основной контейнер */
|
|
||||||
.admin-container {
|
|
||||||
display: flex;
|
|
||||||
min-height: 100vh;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Основной контент */
|
|
||||||
.admin-main {
|
|
||||||
flex: 1;
|
|
||||||
padding: 2rem;
|
|
||||||
margin-left: 280px;
|
|
||||||
transition: margin-left 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Заголовки секций */
|
|
||||||
.section-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
padding-bottom: 1rem;
|
|
||||||
border-bottom: 1px solid rgba(52, 152, 219, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title {
|
|
||||||
font-size: 2.5rem;
|
|
||||||
font-weight: 700;
|
|
||||||
background: linear-gradient(135deg, #3498db, #2ecc71);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-clip: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Индикатор статуса */
|
|
||||||
.status-indicator {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
background: linear-gradient(135deg, rgba(46, 204, 113, 0.15), rgba(39, 174, 96, 0.25));
|
|
||||||
padding: 12px 20px;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid rgba(46, 204, 113, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-dot {
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
background: #2ecc71;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: pulse 2s infinite;
|
|
||||||
box-shadow: 0 0 8px rgba(46, 204, 113, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse {
|
|
||||||
0% {
|
|
||||||
box-shadow: 0 0 0 0 rgba(46, 204, 113, 0.7), 0 0 8px rgba(46, 204, 113, 0.5);
|
|
||||||
}
|
|
||||||
70% {
|
|
||||||
box-shadow: 0 0 0 8px rgba(46, 204, 113, 0), 0 0 8px rgba(46, 204, 113, 0.5);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
box-shadow: 0 0 0 0 rgba(46, 204, 113, 0), 0 0 8px rgba(46, 204, 113, 0.5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-text {
|
|
||||||
color: #ecf0f1;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Кнопки */
|
|
||||||
.btn-primary {
|
|
||||||
background: linear-gradient(135deg, #3498db, #2980b9);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 14px 24px;
|
|
||||||
border-radius: 10px;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
box-shadow: 0 4px 16px rgba(52, 152, 219, 0.25);
|
|
||||||
font-size: 0.95rem;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: -100%;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
|
||||||
transition: left 0.5s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:hover::before {
|
|
||||||
left: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:hover {
|
|
||||||
background: linear-gradient(135deg, #2980b9, #3498db);
|
|
||||||
transform: translateY(-3px);
|
|
||||||
box-shadow: 0 8px 25px rgba(52, 152, 219, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:active {
|
|
||||||
transform: translateY(-1px);
|
|
||||||
box-shadow: 0 4px 16px rgba(52, 152, 219, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary span {
|
|
||||||
margin-right: 10px;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Копирайт */
|
|
||||||
.admin-footer {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 0;
|
|
||||||
left: 280px;
|
|
||||||
right: 0;
|
|
||||||
background: rgba(26, 37, 47, 0.8);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
border-top: 1px solid rgba(52, 152, 219, 0.2);
|
|
||||||
padding: 1rem 2rem;
|
|
||||||
z-index: 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-content {
|
|
||||||
font-size: 0.85rem;
|
|
||||||
color: #95a5a6;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-content a {
|
|
||||||
color: #3498db;
|
|
||||||
text-decoration: none;
|
|
||||||
transition: color 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-content a:hover {
|
|
||||||
color: #2ecc71;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Адаптивность */
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
.admin-main {
|
|
||||||
margin-left: 0;
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-footer {
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-header {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title {
|
|
||||||
font-size: 2rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.admin-main {
|
|
||||||
padding: 1rem 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title {
|
|
||||||
font-size: 1.8rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,203 +0,0 @@
|
|||||||
/* Стили навигации админ-панели */
|
|
||||||
|
|
||||||
.admin-nav {
|
|
||||||
position: fixed;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
width: 280px;
|
|
||||||
height: 100vh;
|
|
||||||
background: linear-gradient(135deg, rgba(26, 37, 47, 0.95), rgba(20, 30, 40, 0.9));
|
|
||||||
backdrop-filter: blur(20px);
|
|
||||||
border-right: 1px solid rgba(52, 152, 219, 0.3);
|
|
||||||
box-shadow:
|
|
||||||
8px 0 32px rgba(0, 0, 0, 0.4),
|
|
||||||
0 0 0 1px rgba(52, 152, 219, 0.1),
|
|
||||||
inset -1px 0 0 rgba(255, 255, 255, 0.1);
|
|
||||||
z-index: 1000;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
transition: transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Заголовок навигации */
|
|
||||||
.nav-header {
|
|
||||||
padding: 2rem 1.5rem;
|
|
||||||
border-bottom: 1px solid rgba(52, 152, 219, 0.25);
|
|
||||||
background: linear-gradient(135deg, rgba(52, 152, 219, 0.08), rgba(46, 204, 113, 0.05));
|
|
||||||
position: relative;
|
|
||||||
box-shadow:
|
|
||||||
0 4px 15px rgba(0, 0, 0, 0.2),
|
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-header::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 2px;
|
|
||||||
background: linear-gradient(90deg, #3498db, #2ecc71, #e74c3c, #f39c12);
|
|
||||||
background-size: 400% 400%;
|
|
||||||
animation: gradientShift 4s ease infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes gradientShift {
|
|
||||||
0%, 100% { background-position: 0% 50%; }
|
|
||||||
50% { background-position: 100% 50%; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
font-size: 2.2rem;
|
|
||||||
font-weight: 700;
|
|
||||||
background: linear-gradient(135deg, #3498db, #2ecc71);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-clip: text;
|
|
||||||
letter-spacing: 2px;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
filter: drop-shadow(0 2px 8px rgba(52, 152, 219, 0.4));
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-subtitle {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: #95a5a6;
|
|
||||||
font-weight: 400;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Меню навигации */
|
|
||||||
.nav-menu {
|
|
||||||
list-style: none;
|
|
||||||
padding: 1rem 0;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-item {
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-link {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 1rem 1.5rem;
|
|
||||||
color: #bdc3c7;
|
|
||||||
text-decoration: none;
|
|
||||||
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
|
||||||
position: relative;
|
|
||||||
border-radius: 0 25px 25px 0;
|
|
||||||
margin-right: 1rem;
|
|
||||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-link::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
width: 4px;
|
|
||||||
background: transparent;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-link:hover {
|
|
||||||
background: linear-gradient(135deg, rgba(52, 152, 219, 0.12), rgba(46, 204, 113, 0.06));
|
|
||||||
color: #3498db;
|
|
||||||
transform: translateX(0);
|
|
||||||
margin-right: 0.5rem;
|
|
||||||
border-radius: 0 12px 12px 0;
|
|
||||||
box-shadow: inset 2px 0 8px rgba(52, 152, 219, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-link:hover::before {
|
|
||||||
background: linear-gradient(135deg, #3498db, #2ecc71);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-item.active .nav-link {
|
|
||||||
background: linear-gradient(135deg, rgba(52, 152, 219, 0.2), rgba(46, 204, 113, 0.1));
|
|
||||||
color: #3498db;
|
|
||||||
font-weight: 600;
|
|
||||||
transform: translateX(0);
|
|
||||||
margin-right: 0;
|
|
||||||
border-radius: 0;
|
|
||||||
box-shadow: inset 3px 0 12px rgba(52, 152, 219, 0.25);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-item.active .nav-link::before {
|
|
||||||
background: linear-gradient(135deg, #3498db, #2ecc71);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-icon {
|
|
||||||
font-size: 1.3rem;
|
|
||||||
margin-right: 1rem;
|
|
||||||
width: 24px;
|
|
||||||
text-align: center;
|
|
||||||
transition: transform 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-link:hover .nav-icon {
|
|
||||||
transform: scale(1.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-text {
|
|
||||||
font-size: 1rem;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Мобильная адаптивность */
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
.admin-nav {
|
|
||||||
transform: translateX(-100%);
|
|
||||||
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-nav.open {
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Кнопка меню (будет добавлена в JS) */
|
|
||||||
.nav-toggle {
|
|
||||||
position: fixed;
|
|
||||||
top: 1rem;
|
|
||||||
left: 1rem;
|
|
||||||
z-index: 1001;
|
|
||||||
background: rgba(26, 37, 47, 0.9);
|
|
||||||
border: 1px solid rgba(52, 152, 219, 0.3);
|
|
||||||
color: #3498db;
|
|
||||||
padding: 12px;
|
|
||||||
border-radius: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
font-size: 1.2rem;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-toggle:hover {
|
|
||||||
background: rgba(52, 152, 219, 0.2);
|
|
||||||
transform: scale(1.05);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.admin-nav {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-link {
|
|
||||||
padding: 1.2rem 1.5rem;
|
|
||||||
margin-right: 0;
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-icon {
|
|
||||||
font-size: 1.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-text {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Анимация появления меню - убрано для избежания проблем с ресайзом */
|
|
||||||
@@ -1,440 +0,0 @@
|
|||||||
/* Стили системной панели метрик */
|
|
||||||
|
|
||||||
.system-panel {
|
|
||||||
background: linear-gradient(135deg, rgba(26, 37, 47, 0.95), rgba(20, 30, 40, 0.9));
|
|
||||||
backdrop-filter: blur(20px);
|
|
||||||
border-radius: 16px;
|
|
||||||
border: 1px solid rgba(52, 152, 219, 0.3);
|
|
||||||
box-shadow:
|
|
||||||
0 8px 32px rgba(0, 0, 0, 0.4),
|
|
||||||
0 0 0 1px rgba(52, 152, 219, 0.1),
|
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-panel::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 3px;
|
|
||||||
background: linear-gradient(90deg, #3498db, #2ecc71, #f39c12, #e74c3c, #9b59b6);
|
|
||||||
background-size: 400% 400%;
|
|
||||||
animation: gradientFlow 8s ease-in-out infinite;
|
|
||||||
box-shadow: 0 3px 15px rgba(52, 152, 219, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes gradientFlow {
|
|
||||||
0%, 100% {
|
|
||||||
background-position: 0% 50%;
|
|
||||||
}
|
|
||||||
25% {
|
|
||||||
background-position: 100% 50%;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
background-position: 200% 50%;
|
|
||||||
}
|
|
||||||
75% {
|
|
||||||
background-position: 300% 50%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Заголовок панели */
|
|
||||||
.system-panel-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 1rem 1.5rem;
|
|
||||||
border-bottom: 1px solid rgba(52, 152, 219, 0.15);
|
|
||||||
background: linear-gradient(135deg, rgba(52, 152, 219, 0.05), rgba(46, 204, 113, 0.03));
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-title {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 1.4rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #ecf0f1;
|
|
||||||
margin: 0;
|
|
||||||
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-icon {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
margin-right: 0.8rem;
|
|
||||||
filter: drop-shadow(0 0 8px rgba(52, 152, 219, 0.4));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Uptime информация */
|
|
||||||
.system-uptime {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.uptime-label {
|
|
||||||
color: #95a5a6;
|
|
||||||
font-size: 0.7rem;
|
|
||||||
font-weight: 500;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.uptime-value {
|
|
||||||
color: #2ecc71;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-weight: 700;
|
|
||||||
background: linear-gradient(135deg, #2ecc71, #27ae60);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-clip: text;
|
|
||||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
|
||||||
font-family: 'Courier New', monospace;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Сетка метрик */
|
|
||||||
.metrics-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
|
||||||
gap: 1rem;
|
|
||||||
padding: 1rem 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Карточки метрик */
|
|
||||||
.metric-card {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
background: linear-gradient(135deg,
|
|
||||||
rgba(52, 152, 219, 0.08),
|
|
||||||
rgba(52, 152, 219, 0.03));
|
|
||||||
border: 1px solid rgba(52, 152, 219, 0.15);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 1rem;
|
|
||||||
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
box-shadow:
|
|
||||||
0 4px 15px rgba(0, 0, 0, 0.1),
|
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.metric-card::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 4px;
|
|
||||||
height: 100%;
|
|
||||||
background: linear-gradient(135deg, #3498db, #2ecc71);
|
|
||||||
transition: all 0.4s ease;
|
|
||||||
box-shadow: 0 0 10px rgba(52, 152, 219, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.metric-card::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: linear-gradient(135deg,
|
|
||||||
rgba(255, 255, 255, 0.02),
|
|
||||||
transparent,
|
|
||||||
rgba(52, 152, 219, 0.02));
|
|
||||||
pointer-events: none;
|
|
||||||
transition: all 0.4s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.metric-card:hover {
|
|
||||||
background: linear-gradient(135deg,
|
|
||||||
rgba(52, 152, 219, 0.12),
|
|
||||||
rgba(52, 152, 219, 0.06));
|
|
||||||
border-color: rgba(52, 152, 219, 0.25);
|
|
||||||
transform: translateY(-3px) scale(1.02);
|
|
||||||
box-shadow:
|
|
||||||
0 8px 25px rgba(0, 0, 0, 0.2),
|
|
||||||
0 0 20px rgba(52, 152, 219, 0.15),
|
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.metric-card:hover::before {
|
|
||||||
width: 6px;
|
|
||||||
box-shadow: 0 0 15px rgba(52, 152, 219, 0.6);
|
|
||||||
}
|
|
||||||
|
|
||||||
.metric-card:hover::after {
|
|
||||||
background: linear-gradient(135deg,
|
|
||||||
rgba(255, 255, 255, 0.05),
|
|
||||||
transparent,
|
|
||||||
rgba(52, 152, 219, 0.05));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Специальные цвета для разных метрик */
|
|
||||||
.metric-card.cpu::before {
|
|
||||||
background: linear-gradient(135deg, #3498db, #2980b9);
|
|
||||||
box-shadow: 0 0 10px rgba(52, 152, 219, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.metric-card.cpu:hover::before {
|
|
||||||
box-shadow: 0 0 15px rgba(52, 152, 219, 0.6);
|
|
||||||
}
|
|
||||||
|
|
||||||
.metric-card.ram::before {
|
|
||||||
background: linear-gradient(135deg, #e74c3c, #c0392b);
|
|
||||||
box-shadow: 0 0 10px rgba(231, 76, 60, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.metric-card.ram:hover::before {
|
|
||||||
box-shadow: 0 0 15px rgba(231, 76, 60, 0.6);
|
|
||||||
}
|
|
||||||
|
|
||||||
.metric-card.disk::before {
|
|
||||||
background: linear-gradient(135deg, #9b59b6, #8e44ad);
|
|
||||||
box-shadow: 0 0 10px rgba(155, 89, 182, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.metric-card.disk:hover::before {
|
|
||||||
box-shadow: 0 0 15px rgba(155, 89, 182, 0.6);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Иконки метрик */
|
|
||||||
.metric-icon-wrapper {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 45px;
|
|
||||||
height: 45px;
|
|
||||||
background: linear-gradient(135deg, rgba(52, 152, 219, 0.15), rgba(52, 152, 219, 0.08));
|
|
||||||
border-radius: 10px;
|
|
||||||
margin-right: 0.8rem;
|
|
||||||
flex-shrink: 0;
|
|
||||||
transition: all 0.4s ease;
|
|
||||||
box-shadow:
|
|
||||||
0 3px 10px rgba(0, 0, 0, 0.1),
|
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.metric-card:hover .metric-icon-wrapper {
|
|
||||||
background: linear-gradient(135deg, rgba(52, 152, 219, 0.2), rgba(52, 152, 219, 0.12));
|
|
||||||
transform: scale(1.1) rotate(5deg);
|
|
||||||
box-shadow:
|
|
||||||
0 5px 15px rgba(0, 0, 0, 0.15),
|
|
||||||
0 0 20px rgba(52, 152, 219, 0.2),
|
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.metric-icon {
|
|
||||||
font-size: 1.3rem;
|
|
||||||
filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.3));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Содержимое метрик */
|
|
||||||
.metric-content {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.metric-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.metric-name {
|
|
||||||
color: #ecf0f1;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.metric-value {
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 1rem;
|
|
||||||
background: linear-gradient(135deg, #3498db, #2ecc71);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-clip: text;
|
|
||||||
flex-shrink: 0;
|
|
||||||
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.3));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Специальные цвета значений */
|
|
||||||
.metric-card.cpu .metric-value {
|
|
||||||
background: linear-gradient(135deg, #3498db, #2980b9);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-clip: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
.metric-card.ram .metric-value {
|
|
||||||
background: linear-gradient(135deg, #e74c3c, #c0392b);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-clip: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
.metric-card.disk .metric-value {
|
|
||||||
background: linear-gradient(135deg, #9b59b6, #8e44ad);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-clip: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Прогресс-бары */
|
|
||||||
.metric-progress-bar {
|
|
||||||
width: 100%;
|
|
||||||
height: 5px;
|
|
||||||
background: linear-gradient(135deg, rgba(52, 152, 219, 0.1), rgba(52, 152, 219, 0.05));
|
|
||||||
border-radius: 3px;
|
|
||||||
overflow: hidden;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
position: relative;
|
|
||||||
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.metric-progress {
|
|
||||||
height: 100%;
|
|
||||||
border-radius: 3px;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
box-shadow:
|
|
||||||
0 1px 3px rgba(0, 0, 0, 0.2),
|
|
||||||
0 0 10px rgba(52, 152, 219, 0.3);
|
|
||||||
transition: all 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
|
||||||
}
|
|
||||||
|
|
||||||
.metric-progress::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: -100%;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
|
|
||||||
animation: shimmer 4s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes shimmer {
|
|
||||||
0% {
|
|
||||||
left: -100%;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
left: 100%;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Цвета прогресс-баров */
|
|
||||||
.cpu-progress {
|
|
||||||
background: linear-gradient(135deg, #3498db, #2980b9);
|
|
||||||
box-shadow:
|
|
||||||
0 1px 3px rgba(0, 0, 0, 0.2),
|
|
||||||
0 0 10px rgba(52, 152, 219, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ram-progress {
|
|
||||||
background: linear-gradient(135deg, #e74c3c, #c0392b);
|
|
||||||
box-shadow:
|
|
||||||
0 1px 3px rgba(0, 0, 0, 0.2),
|
|
||||||
0 0 10px rgba(231, 76, 60, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.disk-progress {
|
|
||||||
background: linear-gradient(135deg, #9b59b6, #8e44ad);
|
|
||||||
box-shadow:
|
|
||||||
0 1px 3px rgba(0, 0, 0, 0.2),
|
|
||||||
0 0 10px rgba(155, 89, 182, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Детали метрик */
|
|
||||||
.metric-details {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.metric-info {
|
|
||||||
color: #bdc3c7;
|
|
||||||
font-size: 0.7rem;
|
|
||||||
font-weight: 500;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.metric-frequency,
|
|
||||||
.metric-type,
|
|
||||||
.metric-range,
|
|
||||||
.metric-speed {
|
|
||||||
color: #95a5a6;
|
|
||||||
font-size: 0.65rem;
|
|
||||||
flex-shrink: 0;
|
|
||||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Адаптивность */
|
|
||||||
@media (max-width: 1200px) {
|
|
||||||
.metrics-grid {
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.metrics-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 1rem;
|
|
||||||
padding: 1rem 1.5rem 1.5rem 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-panel-header {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
align-items: stretch;
|
|
||||||
padding: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.metric-card {
|
|
||||||
flex-direction: column;
|
|
||||||
text-align: center;
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.metric-icon-wrapper {
|
|
||||||
margin-right: 0;
|
|
||||||
margin-bottom: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.metric-details {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.3rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
.system-title {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.metric-name {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.metric-value {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
/**
|
|
||||||
* Дашборд админ-панели vServer
|
|
||||||
*/
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
// Каждая кнопка отдельно с возможностью настройки
|
|
||||||
messageUp.send('#add-site-btn', '🌐 Страница добавления нового сайта в разработке', 'warning');
|
|
||||||
messageUp.send('#add-cert-btn', '🔒 Страница добавления SSL-сертификата в разработке', 'warning');
|
|
||||||
messageUp.send('.btn-icon', '⚙️ Функция настроек в разработке', 'warning');
|
|
||||||
messageUp.send('.show-all-btn', '📋 Полный список в разработке', 'warning');
|
|
||||||
});
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
// Универсальный класс для загрузки JSON данных
|
|
||||||
class JSONLoader {
|
|
||||||
constructor(config) {
|
|
||||||
this.url = config.url;
|
|
||||||
this.container = config.container;
|
|
||||||
this.requiredFields = config.requiredFields || [];
|
|
||||||
this.displayTemplate = config.displayTemplate;
|
|
||||||
this.errorMessage = config.errorMessage || 'Ошибка загрузки данных';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Загрузка данных
|
|
||||||
async load() {
|
|
||||||
try {
|
|
||||||
const response = await fetch(this.url);
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
// Проверяем структуру данных
|
|
||||||
if (!this.validateData(data)) {
|
|
||||||
throw new Error('Неверная структура данных');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.display(data);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка загрузки:', error);
|
|
||||||
this.displayError();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Простая проверка обязательных полей
|
|
||||||
validateData(data) {
|
|
||||||
if (!Array.isArray(data)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let item of data) {
|
|
||||||
for (let field of this.requiredFields) {
|
|
||||||
if (!item.hasOwnProperty(field)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Отображение данных по шаблону
|
|
||||||
display(data) {
|
|
||||||
const container = document.querySelector(this.container);
|
|
||||||
container.innerHTML = '';
|
|
||||||
|
|
||||||
data.forEach(item => {
|
|
||||||
let html;
|
|
||||||
|
|
||||||
// Если шаблон - функция, вызываем её
|
|
||||||
if (typeof this.displayTemplate === 'function') {
|
|
||||||
html = this.displayTemplate(item);
|
|
||||||
} else {
|
|
||||||
// Иначе используем строковый шаблон с заменой
|
|
||||||
html = this.displayTemplate;
|
|
||||||
for (let key in item) {
|
|
||||||
const value = item[key];
|
|
||||||
html = html.replace(new RegExp(`{{${key}}}`, 'g'), value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
container.innerHTML += html;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Отображение ошибки
|
|
||||||
displayError() {
|
|
||||||
const container = document.querySelector(this.container);
|
|
||||||
if (container) {
|
|
||||||
container.innerHTML = `
|
|
||||||
<div style="text-align: center; padding: 2rem; color: #ff6b6b;">
|
|
||||||
⚠️ ${this.errorMessage}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Перезагрузка данных
|
|
||||||
reload() {
|
|
||||||
this.load();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
// Функция для создания HTML пункта меню
|
|
||||||
function getMenuTemplate(item) {
|
|
||||||
const isActive = item.active ? 'active' : '';
|
|
||||||
|
|
||||||
return `
|
|
||||||
<li class="nav-item ${isActive}">
|
|
||||||
<a href="${item.url}" class="nav-link">
|
|
||||||
<span class="nav-icon">${item.icon}</span>
|
|
||||||
<span class="nav-text">${item.name}</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Создаем загрузчик меню
|
|
||||||
const menuLoader = new JSONLoader({
|
|
||||||
url: '/json/menu.json',
|
|
||||||
container: '.nav-menu',
|
|
||||||
requiredFields: ['name', 'icon', 'url', 'active'],
|
|
||||||
displayTemplate: getMenuTemplate,
|
|
||||||
errorMessage: 'Ошибка загрузки меню'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Запуск при загрузке страницы
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
menuLoader.load();
|
|
||||||
});
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
// Функция для создания HTML карточки метрики
|
|
||||||
function createMetricCard(type, icon, name, data) {
|
|
||||||
// Используем уже вычисленный процент для всех типов
|
|
||||||
let value = Math.round(data.usage || data.usage_percent || 0);
|
|
||||||
const progressClass = type === 'cpu' ? 'cpu-progress' :
|
|
||||||
type === 'ram' ? 'ram-progress' : 'disk-progress';
|
|
||||||
|
|
||||||
// Определяем детали для каждого типа
|
|
||||||
let details = '';
|
|
||||||
if (type === 'cpu') {
|
|
||||||
details = `
|
|
||||||
<span class="metric-info">${data.model_name || 'CPU'}</span>
|
|
||||||
<span class="metric-frequency">${data.frequency || ''} MHz</span>
|
|
||||||
`;
|
|
||||||
} else if (type === 'ram') {
|
|
||||||
const usedGb = parseFloat(data.used_gb) || 0;
|
|
||||||
details = `
|
|
||||||
<span class="metric-info">${usedGb.toFixed(1)} GB из ${data.total_gb || 0} GB</span>
|
|
||||||
<span class="metric-type">${data.type || 'RAM'}</span>
|
|
||||||
`;
|
|
||||||
} else if (type === 'disk') {
|
|
||||||
const usedGb = parseFloat(data.used_gb) || 0;
|
|
||||||
const freeGb = parseFloat(data.free_gb) || 0;
|
|
||||||
details = `
|
|
||||||
<span class="metric-info">Занято: ${usedGb.toFixed(0)} GB : Свободно: ${freeGb.toFixed(0)} GB</span>
|
|
||||||
<span class="metric-speed">Размер: ${data.total_gb || 0}</span>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div class="metric-card ${type}">
|
|
||||||
<div class="metric-icon-wrapper">
|
|
||||||
<span class="metric-icon">${icon}</span>
|
|
||||||
</div>
|
|
||||||
<div class="metric-content">
|
|
||||||
<div class="metric-header">
|
|
||||||
<span class="metric-name">${name}</span>
|
|
||||||
<span class="metric-value">${value}%</span>
|
|
||||||
</div>
|
|
||||||
<div class="metric-progress-bar">
|
|
||||||
<div class="metric-progress ${progressClass}" style="width: ${value}%"></div>
|
|
||||||
</div>
|
|
||||||
<div class="metric-details">
|
|
||||||
${details}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Функция для создания всех метрик
|
|
||||||
function renderMetrics(data) {
|
|
||||||
const container = document.querySelector('.metrics-grid');
|
|
||||||
if (!container) return;
|
|
||||||
|
|
||||||
const html = [
|
|
||||||
createMetricCard('cpu', '🖥️', 'Процессор', data.cpu || {}),
|
|
||||||
createMetricCard('ram', '💾', 'Оперативная память', data.memory || {}),
|
|
||||||
createMetricCard('disk', '💿', data.disk.type, data.disk || {})
|
|
||||||
].join('');
|
|
||||||
|
|
||||||
container.innerHTML = html;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Данные по умолчанию (будут заменены данными из API)
|
|
||||||
const staticData = {
|
|
||||||
cpu: {
|
|
||||||
usage: 0,
|
|
||||||
model_name: 'Загрузка...',
|
|
||||||
frequency: '0',
|
|
||||||
cores: '0'
|
|
||||||
},
|
|
||||||
memory: {
|
|
||||||
usage_percent: 0,
|
|
||||||
used_gb: 0,
|
|
||||||
total_gb: 0,
|
|
||||||
type: 'Загрузка...'
|
|
||||||
},
|
|
||||||
disk: {
|
|
||||||
usage_percent: 0,
|
|
||||||
used_gb: 0,
|
|
||||||
free_gb: 0,
|
|
||||||
total_gb: '0',
|
|
||||||
type: 'Загрузка...',
|
|
||||||
read_speed: '520 MB/s'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Функция обновления метрик
|
|
||||||
async function updateMetrics() {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/metrics');
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
// Обновляем все данные из API
|
|
||||||
if (data.cpu_name) staticData.cpu.model_name = data.cpu_name;
|
|
||||||
if (data.cpu_ghz) staticData.cpu.frequency = data.cpu_ghz;
|
|
||||||
if (data.cpu_cores) staticData.cpu.cores = data.cpu_cores;
|
|
||||||
if (data.cpu_usage) staticData.cpu.usage = parseInt(data.cpu_usage);
|
|
||||||
|
|
||||||
if (data.disk_name) staticData.disk.type = data.disk_name;
|
|
||||||
if (data.disk_size) staticData.disk.total_gb = data.disk_size;
|
|
||||||
if (data.disk_used) staticData.disk.used_gb = parseFloat(data.disk_used);
|
|
||||||
if (data.disk_free) staticData.disk.free_gb = parseFloat(data.disk_free);
|
|
||||||
|
|
||||||
if (data.ram_using) staticData.memory.used_gb = parseFloat(data.ram_using);
|
|
||||||
if (data.ram_total) staticData.memory.total_gb = parseFloat(data.ram_total);
|
|
||||||
|
|
||||||
// Вычисляем процент использования памяти
|
|
||||||
if (staticData.memory.used_gb && staticData.memory.total_gb) {
|
|
||||||
staticData.memory.usage_percent = Math.round((staticData.memory.used_gb / staticData.memory.total_gb) * 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Вычисляем процент использования диска
|
|
||||||
if (staticData.disk.used_gb && staticData.disk.total_gb) {
|
|
||||||
const used = parseFloat(staticData.disk.used_gb.toString().replace(' GB', '')) || 0;
|
|
||||||
const total = parseFloat(staticData.disk.total_gb.toString().replace(' GB', '')) || 1;
|
|
||||||
staticData.disk.usage_percent = Math.round((used / total) * 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Обновляем uptime
|
|
||||||
if (data.server_uptime) {
|
|
||||||
const uptimeElement = document.querySelector('.uptime-value');
|
|
||||||
if (uptimeElement) {
|
|
||||||
uptimeElement.textContent = data.server_uptime;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Перерисовываем только метрики
|
|
||||||
renderMetrics(staticData);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка обновления метрик:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Показываем статические данные когда DOM загружен
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
renderMetrics(staticData);
|
|
||||||
|
|
||||||
// Сразу загружаем актуальные данные
|
|
||||||
updateMetrics();
|
|
||||||
|
|
||||||
// Обновляем метрики каждые 5 секунд
|
|
||||||
setInterval(updateMetrics, 5000);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,180 +0,0 @@
|
|||||||
// Функция для получения HTML шаблона сервера
|
|
||||||
var patch_json = '/json/server_status.json';
|
|
||||||
|
|
||||||
function getServerTemplate(server) {
|
|
||||||
// Определяем класс и текст статуса
|
|
||||||
let statusClass, statusText;
|
|
||||||
switch(server.Status.toLowerCase()) {
|
|
||||||
case 'running':
|
|
||||||
statusClass = 'running';
|
|
||||||
statusText = 'Работает';
|
|
||||||
break;
|
|
||||||
case 'stopped':
|
|
||||||
statusClass = 'stopped';
|
|
||||||
statusText = 'Остановлен';
|
|
||||||
break;
|
|
||||||
case 'starting':
|
|
||||||
statusClass = 'starting';
|
|
||||||
statusText = 'Запускается';
|
|
||||||
break;
|
|
||||||
case 'stopping':
|
|
||||||
statusClass = 'stopping';
|
|
||||||
statusText = 'Завершается';
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
statusClass = 'stopped';
|
|
||||||
statusText = `Неизвестно (${server.Status})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Определяем иконку и состояние кнопки
|
|
||||||
let buttonIcon, buttonDisabled, buttonClass;
|
|
||||||
|
|
||||||
if (statusClass === 'starting' || statusClass === 'stopping') {
|
|
||||||
buttonIcon = '⏳';
|
|
||||||
buttonDisabled = 'disabled';
|
|
||||||
buttonClass = 'btn-icon disabled';
|
|
||||||
} else if (statusClass === 'running') {
|
|
||||||
buttonIcon = '⏹️';
|
|
||||||
buttonDisabled = '';
|
|
||||||
buttonClass = 'btn-icon';
|
|
||||||
} else {
|
|
||||||
buttonIcon = '▶️';
|
|
||||||
buttonDisabled = '';
|
|
||||||
buttonClass = 'btn-icon';
|
|
||||||
}
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div class="server-item">
|
|
||||||
<div class="server-status ${statusClass}"></div>
|
|
||||||
<div class="server-info">
|
|
||||||
<div class="server-name">${server.NameService}</div>
|
|
||||||
<div class="server-details">Port ${server.Port} - ${statusText}</div>
|
|
||||||
</div>
|
|
||||||
<div class="server-actions">
|
|
||||||
<button class="${buttonClass}" onclick="toggleServer('${server.NameService}')" ${buttonDisabled}>
|
|
||||||
${buttonIcon}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Создаем загрузчик серверов
|
|
||||||
const serversLoader = new JSONLoader({
|
|
||||||
url: patch_json,
|
|
||||||
container: '.servers-grid',
|
|
||||||
requiredFields: ['NameService', 'Port', 'Status'],
|
|
||||||
displayTemplate: getServerTemplate,
|
|
||||||
errorMessage: 'Ошибка загрузки статуса серверов'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Функция для показа временного статуса
|
|
||||||
function showTempStatus(serviceName, tempStatus) {
|
|
||||||
// Ищем элемент этого сервера на странице
|
|
||||||
const serverElements = document.querySelectorAll('.server-item');
|
|
||||||
serverElements.forEach(element => {
|
|
||||||
const nameElement = element.querySelector('.server-name');
|
|
||||||
if (nameElement && nameElement.textContent === serviceName) {
|
|
||||||
// Создаем временный объект сервера с новым статусом
|
|
||||||
fetch(patch_json)
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(servers => {
|
|
||||||
const server = servers.find(s => s.NameService === serviceName);
|
|
||||||
if (server) {
|
|
||||||
const tempServer = {...server, Status: tempStatus};
|
|
||||||
// Заменяем только этот элемент
|
|
||||||
element.outerHTML = getServerTemplate(tempServer);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Функция обновления одного сервера
|
|
||||||
function updateSingleServer(serviceName) {
|
|
||||||
fetch(patch_json)
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(servers => {
|
|
||||||
const server = servers.find(s => s.NameService === serviceName);
|
|
||||||
if (server) {
|
|
||||||
// Ищем элемент этого сервера на странице
|
|
||||||
const serverElements = document.querySelectorAll('.server-item');
|
|
||||||
serverElements.forEach(element => {
|
|
||||||
const nameElement = element.querySelector('.server-name');
|
|
||||||
if (nameElement && nameElement.textContent === serviceName) {
|
|
||||||
// Заменяем только этот элемент
|
|
||||||
element.outerHTML = getServerTemplate(server);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Универсальная функция управления сервером
|
|
||||||
function serverAction(serviceName, startEndpoint, stopEndpoint, updateDelayMs) {
|
|
||||||
// Получаем текущий статус сервера из JSON
|
|
||||||
fetch(patch_json)
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(servers => {
|
|
||||||
const server = servers.find(s => s.NameService === serviceName);
|
|
||||||
|
|
||||||
// Блокируем действие если сервер в процессе изменения
|
|
||||||
if (server.Status === 'starting' || server.Status === 'stopping') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (server.Status === 'running') {
|
|
||||||
// Сервер запущен - останавливаем
|
|
||||||
showTempStatus(serviceName, 'stopping');
|
|
||||||
fetch(stopEndpoint, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
}).then(() => {
|
|
||||||
setTimeout(() => {
|
|
||||||
updateSingleServer(serviceName); // Обновляем только этот сервер
|
|
||||||
}, updateDelayMs);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Сервер остановлен - запускаем
|
|
||||||
showTempStatus(serviceName, 'starting');
|
|
||||||
fetch(startEndpoint, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
}).then(() => {
|
|
||||||
setTimeout(() => {
|
|
||||||
updateSingleServer(serviceName); // Обновляем только этот сервер
|
|
||||||
}, updateDelayMs);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Функция для переключения сервера
|
|
||||||
function toggleServer(serviceName) {
|
|
||||||
|
|
||||||
if (serviceName === 'MySQL Server') {
|
|
||||||
serverAction('MySQL Server', '/service/MySql_Start', '/service/MySql_Stop', 2000);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (serviceName === 'HTTP Server') {
|
|
||||||
serverAction('HTTP Server', '/service/Http_Start', '/service/Http_Stop', 2000);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (serviceName === 'HTTPS Server') {
|
|
||||||
serverAction('HTTPS Server', '/service/Https_Start', '/service/Https_Stop', 2000);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (serviceName === 'PHP Server') {
|
|
||||||
serverAction('PHP Server', '/service/Php_Start', '/service/Php_Stop', 2000);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Запуск при загрузке страницы
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
serversLoader.load();
|
|
||||||
});
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
const sitesList = document.querySelector('.sites-list');
|
|
||||||
if (sitesList) {
|
|
||||||
fetch('/service/Site_List')
|
|
||||||
.then(r => r.json())
|
|
||||||
.then(data => {
|
|
||||||
const sites = data.sites || [];
|
|
||||||
|
|
||||||
// Генерируем статистику
|
|
||||||
updateSiteStats(sites);
|
|
||||||
|
|
||||||
// Отображаем список сайтов
|
|
||||||
sitesList.innerHTML = sites.map(site => `
|
|
||||||
<div class="site-item">
|
|
||||||
<div class="site-status ${site.status === 'active' ? 'active' : 'inactive'}"></div>
|
|
||||||
<div class="site-info">
|
|
||||||
<span class="site-name">${site.host}</span>
|
|
||||||
<span class="site-details">${site.type.toUpperCase()} • Протокол</span>
|
|
||||||
</div>
|
|
||||||
<div class="site-actions">
|
|
||||||
<button class="btn-icon" title="Настройки">⚙️</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`).join('');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function updateSiteStats(sites) {
|
|
||||||
const totalSites = sites.length;
|
|
||||||
const activeSites = sites.filter(site => site.status === 'active').length;
|
|
||||||
const inactiveSites = totalSites - activeSites;
|
|
||||||
|
|
||||||
// Находим контейнер статистики
|
|
||||||
const statsRow = document.querySelector('.stats-row');
|
|
||||||
|
|
||||||
// Создаём всю статистику через JavaScript
|
|
||||||
statsRow.innerHTML = `
|
|
||||||
<div class="stat-item">
|
|
||||||
<div class="stat-number">${totalSites}</div>
|
|
||||||
<div class="stat-label">Всего сайтов</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-item">
|
|
||||||
<div class="stat-number active-stat">${activeSites}</div>
|
|
||||||
<div class="stat-label">Активных</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-item">
|
|
||||||
<div class="stat-number inactive-stat">${inactiveSites}</div>
|
|
||||||
<div class="stat-label">Неактивных</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,201 +0,0 @@
|
|||||||
/* Класс для показа уведомлений */
|
|
||||||
class MessageUp {
|
|
||||||
// Время автозакрытия уведомлений (в миллисекундах)
|
|
||||||
static autoCloseTime = 3000;
|
|
||||||
|
|
||||||
// Типы уведомлений (легко редактировать тут)
|
|
||||||
static TYPES = {
|
|
||||||
info: {
|
|
||||||
borderColor: 'rgb(52, 152, 219)',
|
|
||||||
background: 'linear-gradient(135deg, rgb(30, 50, 70), rgb(35, 55, 75))'
|
|
||||||
},
|
|
||||||
success: {
|
|
||||||
borderColor: 'rgb(46, 204, 113)',
|
|
||||||
background: 'linear-gradient(135deg, rgb(25, 55, 35), rgb(30, 60, 40))'
|
|
||||||
},
|
|
||||||
warning: {
|
|
||||||
borderColor: 'rgb(243, 156, 18)',
|
|
||||||
background: 'linear-gradient(135deg, rgb(60, 45, 20), rgb(65, 50, 25))'
|
|
||||||
},
|
|
||||||
error: {
|
|
||||||
borderColor: 'rgb(231, 76, 60)',
|
|
||||||
background: 'linear-gradient(135deg, rgb(60, 25, 25), rgb(65, 30, 30))'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.addStyles();
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Показать уведомление */
|
|
||||||
show(message, type = 'info') {
|
|
||||||
const notification = document.createElement('div');
|
|
||||||
notification.className = `message-up message-up-${type}`;
|
|
||||||
|
|
||||||
notification.innerHTML = `
|
|
||||||
<div class="message-content">
|
|
||||||
<span class="message-text">${message}</span>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Вычисляем позицию для нового уведомления
|
|
||||||
const existingNotifications = document.querySelectorAll('.message-up');
|
|
||||||
let topPosition = 2; // начальная позиция в rem
|
|
||||||
|
|
||||||
existingNotifications.forEach(existing => {
|
|
||||||
const rect = existing.getBoundingClientRect();
|
|
||||||
const currentTop = parseFloat(existing.style.top) || 2;
|
|
||||||
const height = rect.height / 16; // переводим px в rem (примерно)
|
|
||||||
topPosition = Math.max(topPosition, currentTop + height + 1); // добавляем отступ
|
|
||||||
});
|
|
||||||
|
|
||||||
notification.style.top = `${topPosition}rem`;
|
|
||||||
document.body.appendChild(notification);
|
|
||||||
|
|
||||||
// Показываем с анимацией
|
|
||||||
setTimeout(() => {
|
|
||||||
notification.classList.add('message-up-show');
|
|
||||||
}, 10);
|
|
||||||
|
|
||||||
// Автоматическое закрытие и удаление
|
|
||||||
if (MessageUp.autoCloseTime > 0) {
|
|
||||||
setTimeout(() => {
|
|
||||||
if (notification && notification.parentNode) {
|
|
||||||
notification.classList.remove('message-up-show');
|
|
||||||
notification.classList.add('message-up-hide');
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
if (notification.parentNode) {
|
|
||||||
notification.remove();
|
|
||||||
// Пересчитываем позиции оставшихся уведомлений
|
|
||||||
this.repositionNotifications();
|
|
||||||
}
|
|
||||||
}, 300);
|
|
||||||
}
|
|
||||||
}, MessageUp.autoCloseTime);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Пересчитать позиции всех уведомлений */
|
|
||||||
repositionNotifications() {
|
|
||||||
const notifications = document.querySelectorAll('.message-up');
|
|
||||||
let currentTop = 2; // начальная позиция
|
|
||||||
|
|
||||||
notifications.forEach(notification => {
|
|
||||||
notification.style.transition = 'all 0.3s ease';
|
|
||||||
notification.style.top = `${currentTop}rem`;
|
|
||||||
|
|
||||||
const rect = notification.getBoundingClientRect();
|
|
||||||
const height = rect.height / 16; // переводим px в rem
|
|
||||||
currentTop += height + 1; // добавляем отступ
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Показать сообщение напрямую или привязать к элементам */
|
|
||||||
send(messageOrSelector, typeOrMessage = 'info', type = 'info') {
|
|
||||||
// Если первый параметр строка и нет других элементов на странице с таким селектором
|
|
||||||
// то показываем сообщение напрямую
|
|
||||||
if (typeof messageOrSelector === 'string' &&
|
|
||||||
document.querySelectorAll(messageOrSelector).length === 0) {
|
|
||||||
// Показываем сообщение напрямую
|
|
||||||
this.show(messageOrSelector, typeOrMessage);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Иначе привязываем к элементам (старый способ)
|
|
||||||
this.bindToElements(messageOrSelector, typeOrMessage, type);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Привязать уведомления к элементам */
|
|
||||||
bindToElements(selector, message = 'Страница в разработке', type = 'info') {
|
|
||||||
document.querySelectorAll(selector).forEach(element => {
|
|
||||||
element.addEventListener('click', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
// Вызываем нужный тип уведомления
|
|
||||||
window.messageUp.show(message, type);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Добавить стили для уведомлений */
|
|
||||||
addStyles() {
|
|
||||||
if (document.querySelector('#message-up-styles')) return;
|
|
||||||
|
|
||||||
const cfg = MessageUp.TYPES;
|
|
||||||
|
|
||||||
// Генерируем стили для типов уведомлений
|
|
||||||
const typeStyles = Object.entries(cfg).map(([type, style]) => `
|
|
||||||
.message-up-${type} {
|
|
||||||
border-color: ${style.borderColor};
|
|
||||||
background: ${style.background};
|
|
||||||
}
|
|
||||||
`).join('');
|
|
||||||
|
|
||||||
const style = document.createElement('style');
|
|
||||||
style.id = 'message-up-styles';
|
|
||||||
style.textContent = `
|
|
||||||
.message-up {
|
|
||||||
position: fixed;
|
|
||||||
top: 2rem;
|
|
||||||
right: 2rem;
|
|
||||||
min-width: 300px;
|
|
||||||
max-width: 400px;
|
|
||||||
background: rgb(26, 37, 47);
|
|
||||||
backdrop-filter: blur(15px);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 1rem;
|
|
||||||
color: #ecf0f1;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
border: 2px solid;
|
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
|
||||||
z-index: 10000;
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateX(100%);
|
|
||||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
font-family: 'Segoe UI', system-ui, sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-up-show {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-up-hide {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateX(100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
${typeStyles}
|
|
||||||
|
|
||||||
.message-content {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-text {
|
|
||||||
flex: 1;
|
|
||||||
line-height: 1.4;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.message-up {
|
|
||||||
top: 1rem;
|
|
||||||
right: 1rem;
|
|
||||||
left: 1rem;
|
|
||||||
min-width: auto;
|
|
||||||
max-width: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
document.head.appendChild(style);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Создаем глобальный экземпляр
|
|
||||||
window.messageUp = new MessageUp();
|
|
||||||
@@ -1,225 +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 - Админ панель</title>
|
|
||||||
|
|
||||||
<!-- CSS файлы -->
|
|
||||||
<link rel="stylesheet" href="assets/css/main.css">
|
|
||||||
<link rel="stylesheet" href="assets/css/navigation.css">
|
|
||||||
<link rel="stylesheet" href="assets/css/dashboard.css">
|
|
||||||
<link rel="stylesheet" href="assets/css/system-metrics.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<!-- Фоновые частицы -->
|
|
||||||
<div class="floating-particles">
|
|
||||||
<div class="particle" style="width: 18px; height: 18px; top: 15%; left: 10%; animation-delay: 0s;"></div>
|
|
||||||
<div class="particle" style="width: 14px; height: 14px; top: 65%; left: 85%; animation-delay: 3s;"></div>
|
|
||||||
<div class="particle" style="width: 10px; height: 10px; top: 85%; left: 20%; animation-delay: 6s;"></div>
|
|
||||||
<div class="particle" style="width: 22px; height: 22px; top: 25%; left: 75%; animation-delay: 2s;"></div>
|
|
||||||
<div class="particle" style="width: 12px; height: 12px; top: 75%; left: 50%; animation-delay: 4s;"></div>
|
|
||||||
<div class="particle" style="width: 16px; height: 16px; top: 40%; left: 15%; animation-delay: 1s;"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Основной контейнер -->
|
|
||||||
<div class="admin-container">
|
|
||||||
<!-- Навигация -->
|
|
||||||
<nav class="admin-nav">
|
|
||||||
<div class="nav-header">
|
|
||||||
<div class="logo">vServer</div>
|
|
||||||
<div class="logo-subtitle">Админ панель</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ul class="nav-menu">
|
|
||||||
<!-- Меню загружается из JSON -->
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<!-- Основной контент -->
|
|
||||||
<main class="admin-main">
|
|
||||||
|
|
||||||
<header class="section-header">
|
|
||||||
<h1 class="section-title">Панель Управления</h1>
|
|
||||||
<div class="status-indicator">
|
|
||||||
<div class="status-dot"></div>
|
|
||||||
<span class="status-text">Сервер работает </span>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<!-- Системная панель метрик -->
|
|
||||||
<div class="system-panel">
|
|
||||||
<div class="system-panel-header">
|
|
||||||
<h2 class="system-title">
|
|
||||||
<span class="system-icon">⚡</span>
|
|
||||||
Системные ресурсы
|
|
||||||
</h2>
|
|
||||||
<div class="system-uptime">
|
|
||||||
<div class="uptime-label">Время работы</div>
|
|
||||||
<div class="uptime-value"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="metrics-grid">
|
|
||||||
<!-- Метрики загружаются динамически через JavaScript -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="dashboard-grid">
|
|
||||||
|
|
||||||
<!-- Статус серверов -->
|
|
||||||
<div class="dashboard-card full-width">
|
|
||||||
|
|
||||||
<div class="card-header">
|
|
||||||
<h2 class="card-title">
|
|
||||||
<span class="card-icon">🖥️</span>
|
|
||||||
Статус серверов
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card-content">
|
|
||||||
<div class="servers-grid">
|
|
||||||
<!-- Здесь будут статусы серверов -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Сайты -->
|
|
||||||
<div class="dashboard-card">
|
|
||||||
<div class="card-header">
|
|
||||||
<h2 class="card-title">
|
|
||||||
<span class="card-icon">🌐</span>
|
|
||||||
Сайты
|
|
||||||
</h2>
|
|
||||||
<button class="btn-primary" id="add-site-btn">
|
|
||||||
<span class="btn-plus">+</span>Новый сайт
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="card-content">
|
|
||||||
<div class="stats-row">
|
|
||||||
<!-- Статистика генерируется через JavaScript -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sites-preview">
|
|
||||||
<div class="preview-header">
|
|
||||||
<span class="preview-title">Последние сайты</span>
|
|
||||||
</div>
|
|
||||||
<div class="sites-list">
|
|
||||||
<!-- Автоматически загружается из JS -->
|
|
||||||
</div>
|
|
||||||
<div class="card-footer">
|
|
||||||
<a href="#sites" class="btn-link show-all-btn">
|
|
||||||
Показать все сайты (7) →
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Сертификаты -->
|
|
||||||
<div class="dashboard-card">
|
|
||||||
<div class="card-header">
|
|
||||||
<h2 class="card-title">
|
|
||||||
<span class="card-icon">🔒</span>
|
|
||||||
Сертификаты
|
|
||||||
</h2>
|
|
||||||
<button class="btn-primary" id="add-cert-btn">
|
|
||||||
<span class="btn-plus">+</span>Новый SSL
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="card-content">
|
|
||||||
<div class="stats-row">
|
|
||||||
<div class="stat-item">
|
|
||||||
<div class="stat-number">5</div>
|
|
||||||
<div class="stat-label">Всего сертификатов</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-item">
|
|
||||||
<div class="stat-number valid-stat">3</div>
|
|
||||||
<div class="stat-label">Действующих</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-item">
|
|
||||||
<div class="stat-number warning-stat">2</div>
|
|
||||||
<div class="stat-label">Истекающих</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="certs-preview">
|
|
||||||
<div class="preview-header">
|
|
||||||
<span class="preview-title">Состояние сертификатов</span>
|
|
||||||
</div>
|
|
||||||
<div class="certs-list">
|
|
||||||
<div class="cert-item">
|
|
||||||
<div class="cert-status valid"></div>
|
|
||||||
<div class="cert-info">
|
|
||||||
<span class="cert-name">*.voxsel.ru</span>
|
|
||||||
<span class="cert-details">Wildcard • Let's Encrypt</span>
|
|
||||||
</div>
|
|
||||||
<div class="cert-expires">
|
|
||||||
<span class="expires-date">До 30.12.2024</span>
|
|
||||||
<span class="expires-days">89 дней</span>
|
|
||||||
</div>
|
|
||||||
<div class="cert-actions">
|
|
||||||
<button class="btn-icon" title="Обновить">🔄</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="cert-item">
|
|
||||||
<div class="cert-status warning"></div>
|
|
||||||
<div class="cert-info">
|
|
||||||
<span class="cert-name">example.com</span>
|
|
||||||
<span class="cert-details">Standard • Cloudflare</span>
|
|
||||||
</div>
|
|
||||||
<div class="cert-expires warning">
|
|
||||||
<span class="expires-date">До 15.01.2024</span>
|
|
||||||
<span class="expires-days">21 день</span>
|
|
||||||
</div>
|
|
||||||
<div class="cert-actions">
|
|
||||||
<button class="btn-icon warning" title="Обновить срочно">⚠️</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="cert-item">
|
|
||||||
<div class="cert-status valid"></div>
|
|
||||||
<div class="cert-info">
|
|
||||||
<span class="cert-name">api.voxsel.ru</span>
|
|
||||||
<span class="cert-details">Standard • Let's Encrypt</span>
|
|
||||||
</div>
|
|
||||||
<div class="cert-expires">
|
|
||||||
<span class="expires-date">До 05.03.2024</span>
|
|
||||||
<span class="expires-days">72 дня</span>
|
|
||||||
</div>
|
|
||||||
<div class="cert-actions">
|
|
||||||
<button class="btn-icon" title="Обновить">🔄</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-footer">
|
|
||||||
<a href="#certificates" class="btn-link show-all-btn">
|
|
||||||
Показать все сертификаты (5) →
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Копирайт -->
|
|
||||||
<footer class="admin-footer">
|
|
||||||
<div class="footer-content">
|
|
||||||
Powered by vServer | Сайт разработчика: <a href="https://voxsel.ru" target="_blank">voxsel.ru</a>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
<!-- JavaScript файлы -->
|
|
||||||
<script src="assets/js/tools.js"></script>
|
|
||||||
<script src="assets/js/dashboard.js"></script>
|
|
||||||
<script src="assets/js/json_loader.js"></script>
|
|
||||||
<script src="assets/js/menu_loader.js"></script>
|
|
||||||
<script src="assets/js/server_status.js"></script>
|
|
||||||
<script src="assets/js/metrics_loader.js"></script>
|
|
||||||
<script src="assets/js/site_list.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
BIN
Backend/admin/icon_exe.png
Normal file
BIN
Backend/admin/icon_exe.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 495 KiB |
@@ -24,12 +24,11 @@ type Site_www struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Soft_Settings struct {
|
type Soft_Settings struct {
|
||||||
Php_port int `json:"php_port"`
|
Php_port int `json:"php_port"`
|
||||||
Php_host string `json:"php_host"`
|
Php_host string `json:"php_host"`
|
||||||
Mysql_port int `json:"mysql_port"`
|
Mysql_port int `json:"mysql_port"`
|
||||||
Mysql_host string `json:"mysql_host"`
|
Mysql_host string `json:"mysql_host"`
|
||||||
Admin_port string `json:"admin_port"`
|
Proxy_enabled bool `json:"proxy_enabled"`
|
||||||
Admin_host string `json:"admin_host"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Proxy_Service struct {
|
type Proxy_Service struct {
|
||||||
|
|||||||
@@ -12,20 +12,68 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
||||||
procSetConsoleMode = kernel32.NewProc("SetConsoleMode")
|
procSetConsoleMode = kernel32.NewProc("SetConsoleMode")
|
||||||
procGetConsoleMode = kernel32.NewProc("GetConsoleMode")
|
procGetConsoleMode = kernel32.NewProc("GetConsoleMode")
|
||||||
procCreateMutex = kernel32.NewProc("CreateMutexW")
|
procCreateMutex = kernel32.NewProc("CreateMutexW")
|
||||||
procCloseHandle = kernel32.NewProc("CloseHandle")
|
procCloseHandle = kernel32.NewProc("CloseHandle")
|
||||||
|
procCreateJobObject = kernel32.NewProc("CreateJobObjectW")
|
||||||
|
procAssignProcessToJobObject = kernel32.NewProc("AssignProcessToJobObject")
|
||||||
|
procSetInformationJobObject = kernel32.NewProc("SetInformationJobObject")
|
||||||
)
|
)
|
||||||
|
|
||||||
const ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
|
const ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
|
||||||
|
const JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x2000
|
||||||
|
|
||||||
var mutexHandle syscall.Handle
|
var mutexHandle syscall.Handle
|
||||||
|
var jobHandle syscall.Handle
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
enableVirtualTerminal()
|
enableVirtualTerminal()
|
||||||
|
createJobObject()
|
||||||
|
}
|
||||||
|
|
||||||
|
func createJobObject() {
|
||||||
|
handle, _, _ := procCreateJobObject.Call(0, 0)
|
||||||
|
if handle == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
jobHandle = syscall.Handle(handle)
|
||||||
|
|
||||||
|
// Устанавливаем флаг автоматического завершения дочерних процессов
|
||||||
|
type JOBOBJECT_EXTENDED_LIMIT_INFORMATION struct {
|
||||||
|
BasicLimitInformation struct {
|
||||||
|
PerProcessUserTimeLimit uint64
|
||||||
|
PerJobUserTimeLimit uint64
|
||||||
|
LimitFlags uint32
|
||||||
|
MinimumWorkingSetSize uintptr
|
||||||
|
MaximumWorkingSetSize uintptr
|
||||||
|
ActiveProcessLimit uint32
|
||||||
|
Affinity uintptr
|
||||||
|
PriorityClass uint32
|
||||||
|
SchedulingClass uint32
|
||||||
|
}
|
||||||
|
IoInfo [48]byte
|
||||||
|
ProcessMemoryLimit uintptr
|
||||||
|
JobMemoryLimit uintptr
|
||||||
|
PeakProcessMemoryUsed uintptr
|
||||||
|
PeakJobMemoryUsed uintptr
|
||||||
|
}
|
||||||
|
|
||||||
|
var limitInfo JOBOBJECT_EXTENDED_LIMIT_INFORMATION
|
||||||
|
limitInfo.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
|
||||||
|
|
||||||
|
procSetInformationJobObject.Call(
|
||||||
|
uintptr(jobHandle),
|
||||||
|
9, // JobObjectExtendedLimitInformation
|
||||||
|
uintptr(unsafe.Pointer(&limitInfo)),
|
||||||
|
unsafe.Sizeof(limitInfo),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Добавляем текущий процесс в Job Object
|
||||||
|
currentProcess, _ := syscall.GetCurrentProcess()
|
||||||
|
procAssignProcessToJobObject.Call(uintptr(jobHandle), uintptr(currentProcess))
|
||||||
}
|
}
|
||||||
|
|
||||||
func enableVirtualTerminal() {
|
func enableVirtualTerminal() {
|
||||||
@@ -66,6 +114,12 @@ func RunBatScript(script string) (string, error) {
|
|||||||
// Функция для логирования вывода процесса в консоль
|
// Функция для логирования вывода процесса в консоль
|
||||||
func Logs_console(process *exec.Cmd, check bool) error {
|
func Logs_console(process *exec.Cmd, check bool) error {
|
||||||
|
|
||||||
|
// Скрываем окно процесса для GUI приложений
|
||||||
|
process.SysProcAttr = &syscall.SysProcAttr{
|
||||||
|
HideWindow: true,
|
||||||
|
CreationFlags: 0x08000000, // CREATE_NO_WINDOW
|
||||||
|
}
|
||||||
|
|
||||||
if check {
|
if check {
|
||||||
// Настраиваем pipes для захвата вывода
|
// Настраиваем pipes для захвата вывода
|
||||||
stdout, err := process.StdoutPipe()
|
stdout, err := process.StdoutPipe()
|
||||||
@@ -128,4 +182,10 @@ func ReleaseMutex() {
|
|||||||
procCloseHandle.Call(uintptr(mutexHandle))
|
procCloseHandle.Call(uintptr(mutexHandle))
|
||||||
mutexHandle = 0
|
mutexHandle = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Закрываем Job Object - это автоматически убьёт все дочерние процессы
|
||||||
|
if jobHandle != 0 {
|
||||||
|
procCloseHandle.Call(uintptr(jobHandle))
|
||||||
|
jobHandle = 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
49
README.md
49
README.md
@@ -18,20 +18,23 @@
|
|||||||
- ✅ **MySQL сервер** с полной поддержкой
|
- ✅ **MySQL сервер** с полной поддержкой
|
||||||
|
|
||||||
### 🔧 Администрирование
|
### 🔧 Администрирование
|
||||||
- ✅ **Веб-админка** на порту 5555 с мониторингом
|
- ✅ **GUI Админка** - Wails desktop приложение с современным интерфейсом
|
||||||
- ✅ **Консольное управление** через командную строку
|
- ✅ **Управление сервисами** - запуск/остановка HTTP, HTTPS, MySQL, PHP, Proxy
|
||||||
- ✅ **Логирование** всех операций
|
- ✅ **Редактор сайтов и прокси** - визуальное управление конфигурацией
|
||||||
- ✅ **Конфигурация** через JSON файлы
|
- ✅ **vAccess редактор** - настройка правил доступа через интерфейс
|
||||||
|
|
||||||
## 🏗️ Архитектура
|
## 🏗️ Архитектура
|
||||||
|
|
||||||
```
|
```
|
||||||
vServer/
|
vServer/
|
||||||
├── 🎯 main.go # Точка входа
|
├── 🎯 main.go # Точка входа основного сервера
|
||||||
│
|
│
|
||||||
├── 🔧 Backend/ # Основная логика
|
├── 🔧 Backend/ # Основная логика
|
||||||
│ │
|
│ │
|
||||||
│ ├── admin/ # | 🎛️ Веб-админка (порт 5555) |
|
│ ├── admin/ # | 🎛️ GUI Админка (Wails) |
|
||||||
|
│ │ ├── go/ # | Go backend для админки |
|
||||||
|
│ │ └── frontend/ # | Современный UI |
|
||||||
|
│ │
|
||||||
│ ├── config/ # | 🔧 Конфигурационные файлы Go |
|
│ ├── config/ # | 🔧 Конфигурационные файлы Go |
|
||||||
│ ├── tools/ # | 🛠️ Утилиты и хелперы |
|
│ ├── tools/ # | 🛠️ Утилиты и хелперы |
|
||||||
│ └── WebServer/ # | 🌐 Модули веб-сервера |
|
│ └── WebServer/ # | 🌐 Модули веб-сервера |
|
||||||
@@ -43,29 +46,37 @@ vServer/
|
|||||||
│ ├── tools/ # | 📊 Логи и инструменты |
|
│ ├── tools/ # | 📊 Логи и инструменты |
|
||||||
│ └── www/ # | 🌍 Веб-контент |
|
│ └── www/ # | 🌍 Веб-контент |
|
||||||
│
|
│
|
||||||
└── 📄 go.mod # Go модули
|
├── 📄 go.mod # Go модули
|
||||||
|
├── 🔨 build_admin.ps1 # Сборка GUI админки
|
||||||
|
└── 🚀 vSerf.exe # GUI админка (после сборки)
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🚀 Установка и запуск
|
## 🚀 Установка и запуск
|
||||||
|
|
||||||
### 🔨 Сборка проекта
|
### 🔨 Сборка основного сервера
|
||||||
```bash
|
```powershell
|
||||||
go build -o MyApp.exe
|
./build_admin.ps1
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Скрипт автоматически:
|
||||||
|
- Проверит/создаст `go.mod`
|
||||||
|
- Установит зависимости (`go mod tidy`)
|
||||||
|
- Проверит/установит Wails CLI
|
||||||
|
- Соберёт приложение → `vSerf.exe`
|
||||||
|
|
||||||
### 📦 Подготовка компонентов
|
### 📦 Подготовка компонентов
|
||||||
1. Распакуйте архив `WebServer/soft/soft.rar` в папку `WebServer/soft/`
|
1. Распакуйте архив `WebServer/soft/soft.rar` в папку `WebServer/soft/`
|
||||||
2. Запустите скомпилированный файл `MyApp.exe`
|
2. Запустите `vServer.exe` - основной сервер
|
||||||
|
3. Запустите `vSerf.exe` - GUI админка для управления
|
||||||
|
|
||||||
> 🔑 **Важно:** Пароль MySQL по умолчанию - `root`
|
> 🔑 **Важно:** Пароль MySQL по умолчанию - `root`
|
||||||
|
|
||||||
### 📦 Готовый проект для пользователя
|
### 📦 Готовый проект для пользователя
|
||||||
Для работы приложения необходимы только:
|
Для работы необходимы:
|
||||||
- 📄 `MyApp.exe` - исполняемый файл
|
- 📄 `vSerf.exe` - GUI админка (опционально)
|
||||||
- 📁 `WebServer/` - папка с конфигурацией и ресурсами
|
- 📁 `WebServer/` - конфигурация и ресурсы
|
||||||
|
|
||||||
> 💡 Папка `Backend/` и файлы `go.mod`, `main.go` и т.д. нужны только для разработки
|
|
||||||
|
|
||||||
|
> 💡 Папка `Backend/` и файлы `go.mod`, `main.go` нужны только для разработки
|
||||||
|
|
||||||
## ⚙️ Конфигурация
|
## ⚙️ Конфигурация
|
||||||
|
|
||||||
@@ -94,9 +105,9 @@ go build -o MyApp.exe
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"Soft_Settings": {
|
"Soft_Settings": {
|
||||||
"mysql_port": 3306, "mysql_host": "192.168.1.6",
|
"mysql_port": 3306, "mysql_host": "127.0.0.1",
|
||||||
"php_port": 8000, "php_host": "localhost",
|
"php_port": 8000, "php_host": "localhost",
|
||||||
"admin_port": "5555", "admin_host": "localhost"
|
"proxy_enabled": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -104,7 +115,7 @@ go build -o MyApp.exe
|
|||||||
**Основные параметры:**
|
**Основные параметры:**
|
||||||
- `Site_www` - настройки веб-сайтов
|
- `Site_www` - настройки веб-сайтов
|
||||||
- `Proxy_Service` - конфигурация прокси-сервисов
|
- `Proxy_Service` - конфигурация прокси-сервисов
|
||||||
- `Soft_Settings` - порты и хосты сервисов (MySQL, PHP, админка)
|
- `Soft_Settings` - порты и хосты сервисов (MySQL, PHP, proxy_enabled)
|
||||||
|
|
||||||
### 🌐 Alias с поддержкой Wildcard
|
### 🌐 Alias с поддержкой Wildcard
|
||||||
|
|
||||||
|
|||||||
@@ -1,32 +1,31 @@
|
|||||||
{
|
{
|
||||||
"Site_www": [
|
|
||||||
{
|
|
||||||
"alias": ["localhost"],
|
|
||||||
"host": "127.0.0.1",
|
|
||||||
"name": "Локальный сайт",
|
|
||||||
"status": "active",
|
|
||||||
"root_file": "index.html",
|
|
||||||
"root_file_routing": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
|
|
||||||
"Proxy_Service": [
|
"Proxy_Service": [
|
||||||
{
|
{
|
||||||
|
"AutoHTTPS": true,
|
||||||
"Enable": false,
|
"Enable": false,
|
||||||
"ExternalDomain": "git.example.ru",
|
"ExternalDomain": "git.example.ru",
|
||||||
"LocalAddress": "127.0.0.1",
|
"LocalAddress": "127.0.0.1",
|
||||||
"LocalPort": "3333",
|
"LocalPort": "3333",
|
||||||
"ServiceHTTPSuse": false,
|
"ServiceHTTPSuse": false
|
||||||
"AutoHTTPS": true
|
}
|
||||||
|
],
|
||||||
|
"Site_www": [
|
||||||
|
{
|
||||||
|
"alias": [
|
||||||
|
"localhost"
|
||||||
|
],
|
||||||
|
"host": "127.0.0.1",
|
||||||
|
"name": "Локальный сайт",
|
||||||
|
"root_file": "index.html",
|
||||||
|
"root_file_routing": true,
|
||||||
|
"status": "active"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
"Soft_Settings": {
|
"Soft_Settings": {
|
||||||
"mysql_host": "127.0.0.1",
|
"mysql_host": "127.0.0.1",
|
||||||
"mysql_port": 3306,
|
"mysql_port": 3306,
|
||||||
"php_host": "localhost",
|
"php_host": "localhost",
|
||||||
"php_port": 8000,
|
"php_port": 8000,
|
||||||
"admin_host": "localhost",
|
"proxy_enabled": true
|
||||||
"admin_port": "5555"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
BIN
appicon.png
Normal file
BIN
appicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 495 KiB |
109
build_admin.ps1
Normal file
109
build_admin.ps1
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
# vServer Admin Panel Builder
|
||||||
|
$ErrorActionPreference = 'SilentlyContinue'
|
||||||
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
# Очищаем консоль
|
||||||
|
Clear-Host
|
||||||
|
Start-Sleep -Milliseconds 100
|
||||||
|
|
||||||
|
function Write-Step {
|
||||||
|
param($Step, $Total, $Message)
|
||||||
|
Write-Host "[$Step/$Total] " -ForegroundColor Cyan -NoNewline
|
||||||
|
Write-Host $Message -ForegroundColor White
|
||||||
|
}
|
||||||
|
|
||||||
|
function Write-Success {
|
||||||
|
param($Message)
|
||||||
|
Write-Host " + OK: " -ForegroundColor Green -NoNewline
|
||||||
|
Write-Host $Message -ForegroundColor Green
|
||||||
|
}
|
||||||
|
|
||||||
|
function Write-Info {
|
||||||
|
param($Message)
|
||||||
|
Write-Host " > " -ForegroundColor Yellow -NoNewline
|
||||||
|
Write-Host $Message -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
|
||||||
|
function Write-Err {
|
||||||
|
param($Message)
|
||||||
|
Write-Host " X ERROR: " -ForegroundColor Red -NoNewline
|
||||||
|
Write-Host $Message -ForegroundColor Red
|
||||||
|
}
|
||||||
|
|
||||||
|
function Write-ProgressBar {
|
||||||
|
param($Percent)
|
||||||
|
$filled = [math]::Floor($Percent / 4)
|
||||||
|
$empty = 25 - $filled
|
||||||
|
$bar = "#" * $filled + "-" * $empty
|
||||||
|
Write-Host " [$bar] $Percent%" -ForegroundColor Cyan
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "=================================================" -ForegroundColor Magenta
|
||||||
|
Write-Host " vServer Admin Panel Builder" -ForegroundColor Cyan
|
||||||
|
Write-Host "=================================================" -ForegroundColor Magenta
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
Write-Step 1 4 "Проверка go.mod..."
|
||||||
|
if (-not (Test-Path "go.mod")) {
|
||||||
|
Write-Info "Создание go.mod..."
|
||||||
|
go mod init vServer 2>&1 | Out-Null
|
||||||
|
Write-Success "Создан"
|
||||||
|
} else {
|
||||||
|
Write-Success "Найден"
|
||||||
|
}
|
||||||
|
Write-ProgressBar 25
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
Write-Step 2 4 "Установка зависимостей..."
|
||||||
|
go mod tidy 2>&1 | Out-Null
|
||||||
|
Write-Success "Зависимости установлены"
|
||||||
|
Write-ProgressBar 50
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
Write-Step 3 4 "Проверка Wails CLI..."
|
||||||
|
$null = wails version 2>&1
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Info "Установка Wails CLI..."
|
||||||
|
go install github.com/wailsapp/wails/v2/cmd/wails@latest 2>&1 | Out-Null
|
||||||
|
Write-Success "Установлен"
|
||||||
|
} else {
|
||||||
|
Write-Success "Найден"
|
||||||
|
}
|
||||||
|
Write-ProgressBar 75
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
Write-Step 4 4 "Сборка приложения..."
|
||||||
|
Write-Info "Компиляция (может занять ~10 сек)..."
|
||||||
|
|
||||||
|
wails build -f admin.go 2>&1 | Out-Null
|
||||||
|
|
||||||
|
if (Test-Path "bin\vServer-Admin.exe") {
|
||||||
|
Write-Success "Скомпилировано"
|
||||||
|
Write-ProgressBar 100
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
Write-Host "Финализация..." -ForegroundColor Cyan
|
||||||
|
Move-Item -Path "bin\vServer-Admin.exe" -Destination "vSerf.exe" -Force 2>$null
|
||||||
|
Write-Success "Файл перемещён: vSerf.exe"
|
||||||
|
|
||||||
|
if (Test-Path "bin") { Remove-Item -Path "bin" -Recurse -Force 2>$null }
|
||||||
|
if (Test-Path "windows") { Remove-Item -Path "windows" -Recurse -Force 2>$null }
|
||||||
|
Write-Success "Временные файлы удалены"
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
Write-Host "=================================================" -ForegroundColor Green
|
||||||
|
Write-Host " УСПЕШНО СОБРАНО!" -ForegroundColor Green
|
||||||
|
Write-Host " Файл: " -ForegroundColor Green -NoNewline
|
||||||
|
Write-Host "vSerf.exe" -ForegroundColor Cyan
|
||||||
|
Write-Host "=================================================" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Err "Ошибка компиляции"
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
Write-Host "=================================================" -ForegroundColor Red
|
||||||
|
Write-Host " ОШИБКА СБОРКИ!" -ForegroundColor Red
|
||||||
|
Write-Host "=================================================" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
32
go.mod
32
go.mod
@@ -1,3 +1,35 @@
|
|||||||
module vServer
|
module vServer
|
||||||
|
|
||||||
go 1.24.4
|
go 1.24.4
|
||||||
|
|
||||||
|
require github.com/wailsapp/wails/v2 v2.11.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/bep/debounce v1.2.1 // indirect
|
||||||
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
|
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/gorilla/websocket v1.5.3 // indirect
|
||||||
|
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
|
||||||
|
github.com/labstack/echo/v4 v4.13.3 // indirect
|
||||||
|
github.com/labstack/gommon v0.4.2 // indirect
|
||||||
|
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
|
||||||
|
github.com/leaanthony/gosod v1.0.4 // indirect
|
||||||
|
github.com/leaanthony/slicer v1.6.0 // indirect
|
||||||
|
github.com/leaanthony/u v1.1.1 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
|
github.com/samber/lo v1.49.1 // indirect
|
||||||
|
github.com/tkrajina/go-reflector v0.5.8 // indirect
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
|
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||||
|
github.com/wailsapp/go-webview2 v1.0.22 // indirect
|
||||||
|
github.com/wailsapp/mimetype v1.4.1 // indirect
|
||||||
|
golang.org/x/crypto v0.33.0 // indirect
|
||||||
|
golang.org/x/net v0.35.0 // indirect
|
||||||
|
golang.org/x/sys v0.30.0 // indirect
|
||||||
|
golang.org/x/text v0.22.0 // indirect
|
||||||
|
)
|
||||||
|
|||||||
81
go.sum
Normal file
81
go.sum
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
||||||
|
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||||
|
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||||
|
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||||
|
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
|
||||||
|
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
|
||||||
|
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
|
||||||
|
github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
|
||||||
|
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||||
|
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
|
||||||
|
github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc=
|
||||||
|
github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA=
|
||||||
|
github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A=
|
||||||
|
github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
|
||||||
|
github.com/leaanthony/gosod v1.0.4 h1:YLAbVyd591MRffDgxUOU1NwLhT9T1/YiwjKZpkNFeaI=
|
||||||
|
github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw=
|
||||||
|
github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js=
|
||||||
|
github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8=
|
||||||
|
github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
|
||||||
|
github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
|
||||||
|
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||||
|
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
|
||||||
|
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||||
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||||
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
|
||||||
|
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
|
||||||
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ=
|
||||||
|
github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
|
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||||
|
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||||
|
github.com/wailsapp/go-webview2 v1.0.22 h1:YT61F5lj+GGaat5OB96Aa3b4QA+mybD0Ggq6NZijQ58=
|
||||||
|
github.com/wailsapp/go-webview2 v1.0.22/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
|
||||||
|
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
|
||||||
|
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
|
||||||
|
github.com/wailsapp/wails/v2 v2.11.0 h1:seLacV8pqupq32IjS4Y7V8ucab0WZwtK6VvUVxSBtqQ=
|
||||||
|
github.com/wailsapp/wails/v2 v2.11.0/go.mod h1:jrf0ZaM6+GBc1wRmXsM8cIvzlg0karYin3erahI4+0k=
|
||||||
|
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||||
|
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||||
|
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||||
|
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||||
|
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||||
|
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||||
|
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
105
main.go
105
main.go
@@ -1,75 +1,50 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"embed"
|
||||||
"time"
|
"log"
|
||||||
webserver "vServer/Backend/WebServer"
|
|
||||||
|
"github.com/wailsapp/wails/v2"
|
||||||
|
"github.com/wailsapp/wails/v2/pkg/options"
|
||||||
|
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
|
||||||
|
"github.com/wailsapp/wails/v2/pkg/options/windows"
|
||||||
|
|
||||||
admin "vServer/Backend/admin/go"
|
admin "vServer/Backend/admin/go"
|
||||||
json_admin "vServer/Backend/admin/go/json"
|
|
||||||
config "vServer/Backend/config"
|
|
||||||
tools "vServer/Backend/tools"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
//go:embed all:Backend/admin/frontend
|
||||||
|
var assets embed.FS
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
// Создаём экземпляр приложения
|
||||||
|
app := admin.NewApp()
|
||||||
|
|
||||||
if !tools.CheckSingleInstance() {
|
// Настройки окна
|
||||||
println("")
|
err := wails.Run(&options.App{
|
||||||
println(tools.Color("❌ ОШИБКА:", tools.Красный) + " vServer уже запущен!")
|
Title: "vServer - Панель управления",
|
||||||
println(tools.Color("💡 Подсказка:", tools.Жёлтый) + " Завершите уже запущенный процесс перед запуском нового.")
|
Width: 1600,
|
||||||
println("")
|
Height: 900,
|
||||||
println("Нажмите Enter для завершения...")
|
MinWidth: 1400,
|
||||||
fmt.Scanln()
|
MinHeight: 800,
|
||||||
return
|
AssetServer: &assetserver.Options{
|
||||||
|
Assets: assets,
|
||||||
|
},
|
||||||
|
BackgroundColour: &options.RGBA{R: 10, G: 14, B: 26, A: 1},
|
||||||
|
OnStartup: app.Startup,
|
||||||
|
OnShutdown: app.Shutdown,
|
||||||
|
Bind: []interface{}{
|
||||||
|
app,
|
||||||
|
},
|
||||||
|
Frameless: true,
|
||||||
|
Windows: &windows.Options{
|
||||||
|
WebviewIsTransparent: false,
|
||||||
|
WindowIsTranslucent: false,
|
||||||
|
DisableWindowIcon: false,
|
||||||
|
Theme: windows.Dark,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Освобождаем мьютекс при выходе (опционально, так как Windows сама освободит)
|
|
||||||
defer tools.ReleaseMutex()
|
|
||||||
|
|
||||||
println("")
|
|
||||||
println(tools.Color("vServer", tools.Жёлтый) + tools.Color(" 1.0.0", tools.Голубой))
|
|
||||||
println(tools.Color("Автор: ", tools.Зелёный) + tools.Color("Суманеев Роман (c) 2025", tools.Голубой))
|
|
||||||
println(tools.Color("Официальный сайт: ", tools.Зелёный) + tools.Color("https://voxsel.ru", tools.Голубой))
|
|
||||||
|
|
||||||
println("")
|
|
||||||
println("🚀 Запуск vServer...")
|
|
||||||
println("📁 Файлы сайта будут обслуживаться из папки 'www'")
|
|
||||||
println("")
|
|
||||||
println("⏳ Запуск сервисов...")
|
|
||||||
println("")
|
|
||||||
|
|
||||||
// Инициализируем время запуска сервера
|
|
||||||
tools.ServerUptime("start")
|
|
||||||
|
|
||||||
config.LoadConfig()
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
webserver.StartHandler()
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
// Запускаем серверы в горутинах
|
|
||||||
go admin.StartAdmin()
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
webserver.Cert_start()
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
go webserver.StartHTTPS()
|
|
||||||
json_admin.UpdateServerStatus("HTTPS Server", "running")
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
go webserver.StartHTTP()
|
|
||||||
json_admin.UpdateServerStatus("HTTP Server", "running")
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
webserver.PHP_Start()
|
|
||||||
json_admin.UpdateServerStatus("PHP Server", "running")
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
webserver.StartMySQLServer(false)
|
|
||||||
json_admin.UpdateServerStatus("MySQL Server", "running")
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
println("")
|
|
||||||
webserver.CommandListener()
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
23
wails.json
Normal file
23
wails.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://wails.io/schemas/config.v2.json",
|
||||||
|
"name": "vServer Admin Panel",
|
||||||
|
"outputfilename": "vServer-Admin",
|
||||||
|
"author": {
|
||||||
|
"name": "Суманеев Роман",
|
||||||
|
"email": "info@voxsel.ru"
|
||||||
|
},
|
||||||
|
"info": {
|
||||||
|
"companyName": "vServer",
|
||||||
|
"productName": "vServer Admin Panel",
|
||||||
|
"productVersion": "1.0.0",
|
||||||
|
"copyright": "Copyright © 2025 vServer",
|
||||||
|
"comments": "Панель управления vServer"
|
||||||
|
},
|
||||||
|
"wailsjsdir": "./Backend/admin/frontend/wailsjs",
|
||||||
|
"frontend:dir": "./Backend/admin/frontend",
|
||||||
|
"assetdir": "./Backend/admin/frontend",
|
||||||
|
"reloaddirs": "Backend/admin",
|
||||||
|
"build:dir": ".",
|
||||||
|
"appicon": "appicon.png"
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user