diff --git a/internal/api/webui.go b/internal/api/webui.go
deleted file mode 100644
index ab75e53..0000000
--- a/internal/api/webui.go
+++ /dev/null
@@ -1,640 +0,0 @@
-/*
- * Copyright 2026 Safronov Grigorii
- *
- * Licensed under the CDDL, Version 1.0 (the "License");
- * you may not use this file except in compliance with the License.
- *
- * You may obtain a copy of the License at
- * https://opensource.org/licenses/CDDL-1.0
- */
-
-// Файл: internal/api/webui.go
-// Назначение: Веб-интерфейс в стиле dashboard для управления СУБД futriis
-
-package api
-
-import (
- "embed"
- "encoding/json"
- "fmt"
- "io/fs"
- "net/http"
- "strconv"
- "strings"
- "time"
-
- "futriis/internal/acl"
- "futriis/internal/cluster"
- "futriis/internal/log"
- "futriis/internal/storage"
-)
-
-//go:embed static/*
-var staticFiles embed.FS
-
-// WebUIServer представляет сервер веб-интерфейса
-type WebUIServer struct {
- store *storage.Storage
- coordinator *cluster.RaftCoordinator
- aclManager *acl.ACLManager
- logger *log.Logger
- server *http.Server
- port int
- enabled bool
-}
-
-// NewWebUIServer создаёт новый веб-сервер интерфейса
-func NewWebUIServer(port int, enabled bool, store *storage.Storage, coord *cluster.RaftCoordinator, aclMgr *acl.ACLManager, logger *log.Logger) *WebUIServer {
- return &WebUIServer{
- store: store,
- coordinator: coord,
- aclManager: aclMgr,
- logger: logger,
- port: port,
- enabled: enabled,
- }
-}
-
-// Start запускает веб-сервер интерфейса
-func (w *WebUIServer) Start() error {
- if !w.enabled {
- w.logger.Info("Web UI is disabled in configuration")
- return nil
- }
-
- mux := http.NewServeMux()
-
- // Статические файлы
- staticFS, err := fs.Sub(staticFiles, "static")
- if err != nil {
- return fmt.Errorf("failed to load static files: %v", err)
- }
- mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(staticFS))))
-
- // Главная страница
- mux.HandleFunc("/", w.handleIndex)
-
- // API для веб-интерфейса
- mux.HandleFunc("/api/webui/databases", w.handleGetDatabases)
- mux.HandleFunc("/api/webui/collections/", w.handleGetCollections)
- mux.HandleFunc("/api/webui/documents/", w.handleDocuments)
- mux.HandleFunc("/api/webui/cluster/status", w.handleClusterStatus)
- mux.HandleFunc("/api/webui/cluster/nodes", w.handleClusterNodes)
- mux.HandleFunc("/api/webui/stats", w.handleStats)
- mux.HandleFunc("/api/webui/login", w.handleWebLogin)
- mux.HandleFunc("/api/webui/logout", w.handleWebLogout)
- mux.HandleFunc("/api/webui/session", w.handleSessionCheck)
-
- w.server = &http.Server{
- Addr: fmt.Sprintf(":%d", w.port),
- Handler: mux,
- ReadTimeout: 30 * time.Second,
- WriteTimeout: 30 * time.Second,
- }
-
- w.logger.Info(fmt.Sprintf("Web UI started on port %d", w.port))
- return w.server.ListenAndServe()
-}
-
-// Stop останавливает веб-сервер
-func (w *WebUIServer) Stop() error {
- if w.server != nil {
- return w.server.Close()
- }
- return nil
-}
-
-// handleIndex возвращает главную HTML страницу
-func (w *WebUIServer) handleIndex(wr http.ResponseWriter, r *http.Request) {
- if r.URL.Path != "/" {
- http.NotFound(wr, r)
- return
- }
-
- html := `
-
-
-
-
- Futriis Database Management System
-
-
-
-
-
-
-
-
-
-
- Панель управления
-
- Подключено
-
-
-
-
-
-
-
-
-
-
-
-
-
-`
-
- wr.Header().Set("Content-Type", "text/html; charset=utf-8")
- wr.Write([]byte(html))
-}
-
-// handleGetDatabases возвращает список баз данных
-func (w *WebUIServer) handleGetDatabases(wr http.ResponseWriter, r *http.Request) {
- if !w.checkAuth(r) {
- w.sendJSONError(wr, "Unauthorized", http.StatusUnauthorized)
- return
- }
-
- databases := w.store.ListDatabases()
-
- dbInfo := make([]map[string]interface{}, 0)
- for _, dbName := range databases {
- db, err := w.store.GetDatabase(dbName)
- if err == nil {
- collections := db.ListCollections()
- dbInfo = append(dbInfo, map[string]interface{}{
- "name": dbName,
- "collections": len(collections),
- "collections_list": collections,
- })
- }
- }
-
- w.sendJSONSuccess(wr, dbInfo)
-}
-
-// handleGetCollections возвращает список коллекций в базе данных
-func (w *WebUIServer) handleGetCollections(wr http.ResponseWriter, r *http.Request) {
- if !w.checkAuth(r) {
- w.sendJSONError(wr, "Unauthorized", http.StatusUnauthorized)
- return
- }
-
- path := strings.TrimPrefix(r.URL.Path, "/api/webui/collections/")
- parts := strings.Split(path, "/")
-
- if len(parts) < 1 || parts[0] == "" {
- w.sendJSONError(wr, "Database name required", http.StatusBadRequest)
- return
- }
-
- dbName := parts[0]
- db, err := w.store.GetDatabase(dbName)
- if err != nil {
- w.sendJSONError(wr, err.Error(), http.StatusNotFound)
- return
- }
-
- collections := db.ListCollections()
- collectionsInfo := make([]map[string]interface{}, 0)
-
- for _, collName := range collections {
- coll, err := db.GetCollection(collName)
- if err == nil {
- collectionsInfo = append(collectionsInfo, map[string]interface{}{
- "name": collName,
- "count": coll.Count(),
- "size": coll.Size(),
- "indexes": coll.GetIndexes(),
- })
- }
- }
-
- w.sendJSONSuccess(wr, map[string]interface{}{
- "database": dbName,
- "collections": collectionsInfo,
- })
-}
-
-// handleDocuments обрабатывает CRUD операции с документами
-func (w *WebUIServer) handleDocuments(wr http.ResponseWriter, r *http.Request) {
- if !w.checkAuth(r) {
- w.sendJSONError(wr, "Unauthorized", http.StatusUnauthorized)
- return
- }
-
- path := strings.TrimPrefix(r.URL.Path, "/api/webui/documents/")
- parts := strings.Split(path, "/")
-
- if len(parts) < 2 {
- w.sendJSONError(wr, "Invalid path. Use /api/webui/documents/{database}/{collection}", http.StatusBadRequest)
- return
- }
-
- dbName := parts[0]
- collName := parts[1]
-
- db, err := w.store.GetDatabase(dbName)
- if err != nil {
- w.sendJSONError(wr, err.Error(), http.StatusNotFound)
- return
- }
-
- coll, err := db.GetCollection(collName)
- if err != nil {
- w.sendJSONError(wr, err.Error(), http.StatusNotFound)
- return
- }
-
- switch r.Method {
- case http.MethodGet:
- limit := 50
- if limitStr := r.URL.Query().Get("limit"); limitStr != "" {
- if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 500 {
- limit = l
- }
- }
-
- offset := 0
- if offsetStr := r.URL.Query().Get("offset"); offsetStr != "" {
- if o, err := strconv.Atoi(offsetStr); err == nil && o >= 0 {
- offset = o
- }
- }
-
- allDocs := coll.GetAllDocuments()
- start := offset
- end := offset + limit
- if start > len(allDocs) {
- start = len(allDocs)
- }
- if end > len(allDocs) {
- end = len(allDocs)
- }
-
- docs := make([]map[string]interface{}, 0)
- for _, doc := range allDocs[start:end] {
- docs = append(docs, map[string]interface{}{
- "id": doc.ID,
- "fields": doc.GetFields(),
- "created_at": doc.CreatedAt,
- "updated_at": doc.UpdatedAt,
- "version": doc.Version,
- })
- }
-
- w.sendJSONSuccess(wr, map[string]interface{}{
- "documents": docs,
- "total": len(allDocs),
- "limit": limit,
- "offset": offset,
- })
-
- case http.MethodPost:
- var docData map[string]interface{}
- if err := json.NewDecoder(r.Body).Decode(&docData); err != nil {
- w.sendJSONError(wr, "Invalid JSON", http.StatusBadRequest)
- return
- }
-
- if err := coll.InsertFromMap(docData); err != nil {
- w.sendJSONError(wr, err.Error(), http.StatusBadRequest)
- return
- }
-
- w.sendJSONSuccess(wr, map[string]interface{}{
- "status": "inserted",
- "id": docData["_id"],
- })
-
- case http.MethodPut:
- docID := r.URL.Query().Get("id")
- if docID == "" {
- w.sendJSONError(wr, "Document ID required", http.StatusBadRequest)
- return
- }
-
- var updates map[string]interface{}
- if err := json.NewDecoder(r.Body).Decode(&updates); err != nil {
- w.sendJSONError(wr, "Invalid JSON", http.StatusBadRequest)
- return
- }
-
- if err := coll.Update(docID, updates); err != nil {
- w.sendJSONError(wr, err.Error(), http.StatusBadRequest)
- return
- }
-
- w.sendJSONSuccess(wr, map[string]interface{}{
- "status": "updated",
- "id": docID,
- })
-
- case http.MethodDelete:
- docID := r.URL.Query().Get("id")
- if docID == "" {
- w.sendJSONError(wr, "Document ID required", http.StatusBadRequest)
- return
- }
-
- if err := coll.Delete(docID); err != nil {
- w.sendJSONError(wr, err.Error(), http.StatusNotFound)
- return
- }
-
- w.sendJSONSuccess(wr, map[string]interface{}{
- "status": "deleted",
- "id": docID,
- })
-
- default:
- w.sendJSONError(wr, "Method not allowed", http.StatusMethodNotAllowed)
- }
-}
-
-// handleClusterStatus возвращает статус кластера
-func (w *WebUIServer) handleClusterStatus(wr http.ResponseWriter, r *http.Request) {
- if !w.checkAuth(r) {
- w.sendJSONError(wr, "Unauthorized", http.StatusUnauthorized)
- return
- }
-
- if w.coordinator == nil {
- w.sendJSONError(wr, "Cluster not available", http.StatusServiceUnavailable)
- return
- }
-
- status := w.coordinator.GetClusterStatus()
- w.sendJSONSuccess(wr, status)
-}
-
-// handleClusterNodes возвращает список узлов кластера
-func (w *WebUIServer) handleClusterNodes(wr http.ResponseWriter, r *http.Request) {
- if !w.checkAuth(r) {
- w.sendJSONError(wr, "Unauthorized", http.StatusUnauthorized)
- return
- }
-
- if w.coordinator == nil {
- w.sendJSONError(wr, "Cluster not available", http.StatusServiceUnavailable)
- return
- }
-
- nodes := w.coordinator.GetAllNodes()
- w.sendJSONSuccess(wr, nodes)
-}
-
-// handleStats возвращает статистику системы
-func (w *WebUIServer) handleStats(wr http.ResponseWriter, r *http.Request) {
- if !w.checkAuth(r) {
- w.sendJSONError(wr, "Unauthorized", http.StatusUnauthorized)
- return
- }
-
- databases := w.store.ListDatabases()
- totalDocs := int64(0)
- totalCollections := 0
-
- for _, dbName := range databases {
- db, _ := w.store.GetDatabase(dbName)
- if db != nil {
- collections := db.ListCollections()
- totalCollections += len(collections)
- for _, collName := range collections {
- coll, _ := db.GetCollection(collName)
- if coll != nil {
- totalDocs += coll.Count()
- }
- }
- }
- }
-
- stats := map[string]interface{}{
- "databases": len(databases),
- "collections": totalCollections,
- "documents": totalDocs,
- "storage_used_mb": float64(w.store.GetPageSize()*int64(len(databases))) / (1024 * 1024),
- "uptime_seconds": time.Now().Unix(),
- "cluster_enabled": w.coordinator != nil,
- "replication_factor": 0,
- }
-
- if w.coordinator != nil {
- stats["replication_factor"] = w.coordinator.GetReplicationFactor()
- stats["cluster_health"] = w.coordinator.GetClusterStatus().Health
- }
-
- w.sendJSONSuccess(wr, stats)
-}
-
-// handleWebLogin обрабатывает вход в веб-интерфейс
-func (w *WebUIServer) handleWebLogin(wr http.ResponseWriter, r *http.Request) {
- if r.Method != http.MethodPost {
- w.sendJSONError(wr, "Method not allowed", http.StatusMethodNotAllowed)
- return
- }
-
- var creds struct {
- Username string `json:"username"`
- Password string `json:"password"`
- }
-
- if err := json.NewDecoder(r.Body).Decode(&creds); err != nil {
- w.sendJSONError(wr, "Invalid request body", http.StatusBadRequest)
- return
- }
-
- // Логируем попытку входа для отладки
- w.logger.Debug(fmt.Sprintf("Login attempt: username=%s", creds.Username))
-
- // Пытаемся аутентифицировать через ACL менеджер
- sessionID, err := w.aclManager.Authenticate(creds.Username, creds.Password)
- if err != nil {
- w.logger.Debug(fmt.Sprintf("Authentication failed for %s: %v", creds.Username, err))
- w.sendJSONError(wr, "Неверный логин и/или пароль", http.StatusUnauthorized)
- return
- }
-
- w.logger.Debug(fmt.Sprintf("Authentication successful for %s, sessionID=%s", creds.Username, sessionID))
-
- // Устанавливаем cookie сессии
- http.SetCookie(wr, &http.Cookie{
- Name: "session_id",
- Value: sessionID,
- Path: "/",
- MaxAge: 86400,
- HttpOnly: true,
- SameSite: http.SameSiteLaxMode,
- })
-
- w.sendJSONSuccess(wr, map[string]interface{}{
- "session_id": sessionID,
- "username": creds.Username,
- })
-}
-
-// handleWebLogout обрабатывает выход из веб-интерфейса
-func (w *WebUIServer) handleWebLogout(wr http.ResponseWriter, r *http.Request) {
- if cookie, err := r.Cookie("session_id"); err == nil {
- w.aclManager.Logout(cookie.Value)
- }
-
- http.SetCookie(wr, &http.Cookie{
- Name: "session_id",
- Value: "",
- Path: "/",
- MaxAge: -1,
- HttpOnly: true,
- })
-
- w.sendJSONSuccess(wr, map[string]interface{}{
- "status": "logged out",
- })
-}
-
-// handleSessionCheck проверяет активность сессии
-func (w *WebUIServer) handleSessionCheck(wr http.ResponseWriter, r *http.Request) {
- sessionID := w.getSessionID(r)
- if sessionID == "" {
- w.sendJSONError(wr, "No session", http.StatusUnauthorized)
- return
- }
-
- if !w.aclManager.CheckSession(sessionID) {
- w.sendJSONError(wr, "Invalid session", http.StatusUnauthorized)
- return
- }
-
- username := w.aclManager.GetUsername(sessionID)
-
- // Проверяем статус подключения к СУБД
- connectionStatus := "connected"
- if w.coordinator == nil {
- connectionStatus = "disconnected"
- } else if status := w.coordinator.GetClusterStatus(); status.Health == "critical" {
- connectionStatus = "disconnected"
- }
-
- w.sendJSONSuccess(wr, map[string]interface{}{
- "authenticated": true,
- "username": username,
- "connection_status": connectionStatus,
- })
-}
-
-// checkAuth проверяет аутентификацию
-func (w *WebUIServer) checkAuth(r *http.Request) bool {
- sessionID := w.getSessionID(r)
- if sessionID == "" {
- return false
- }
- return w.aclManager.CheckSession(sessionID)
-}
-
-// getSessionID возвращает ID сессии из cookie
-func (w *WebUIServer) getSessionID(r *http.Request) string {
- if cookie, err := r.Cookie("session_id"); err == nil {
- return cookie.Value
- }
- return ""
-}
-
-// sendJSONSuccess отправляет успешный JSON ответ
-func (w *WebUIServer) sendJSONSuccess(wr http.ResponseWriter, data interface{}) {
- wr.Header().Set("Content-Type", "application/json")
- wr.WriteHeader(http.StatusOK)
- json.NewEncoder(wr).Encode(map[string]interface{}{
- "success": true,
- "data": data,
- })
-}
-
-// sendJSONError отправляет JSON ответ с ошибкой
-func (w *WebUIServer) sendJSONError(wr http.ResponseWriter, errMsg string, statusCode int) {
- wr.Header().Set("Content-Type", "application/json")
- wr.WriteHeader(statusCode)
- json.NewEncoder(wr).Encode(map[string]interface{}{
- "success": false,
- "error": errMsg,
- })
-}