local PLUGIN = PLUGIN -- Цветовая схема local COLOR_BG_DARK = Color(3, 5, 4) local COLOR_BG_MEDIUM = Color(8, 12, 10) local COLOR_BG_LIGHT = Color(12, 18, 14) local COLOR_PRIMARY = Color(27, 94, 32) local COLOR_PRIMARY_DARK = Color(15, 60, 18) local COLOR_ACCENT = Color(56, 102, 35) local COLOR_TEXT_PRIMARY = Color(165, 214, 167) local COLOR_TEXT_SECONDARY = Color(102, 187, 106) local COLOR_BORDER = Color(15, 60, 18, 60) -- Локальное хранилище логов local clientLogs = {} local isLoadingLogs = false -- Сетевой получатель для открытия меню (из команды Helix) net.Receive("ixServerLogs_OpenMenu", function() PLUGIN:OpenLogsMenu() end) -- Получение логов с сервера function PLUGIN:RequestLogs(filters) if isLoadingLogs then return end isLoadingLogs = true clientLogs = {} net.Start("ixServerLogs_Request") net.WriteTable(filters or {}) net.SendToServer() end -- Получение ответа с логами net.Receive("ixServerLogs_Response", function() local isFirst = net.ReadUInt(2) == 1 local logs = net.ReadTable() if isFirst then clientLogs = {} end for _, log in ipairs(logs) do table.insert(clientLogs, log) end isLoadingLogs = false -- Обновляем UI если открыто if IsValid(PLUGIN.logsFrame) and IsValid(PLUGIN.logsList) then PLUGIN:UpdateLogsList() end end) -- Главное меню логов function PLUGIN:OpenLogsMenu() if IsValid(self.logsFrame) then self.logsFrame:Remove() end local scrW, scrH = ScrW(), ScrH() local frame = vgui.Create("DFrame") frame:SetSize(scrW * 0.9, scrH * 0.85) frame:Center() frame:SetTitle("") frame:SetDraggable(true) frame:ShowCloseButton(false) frame:MakePopup() frame.Paint = function(s, w, h) -- Основной фон draw.RoundedBox(8, 0, 0, w, h, COLOR_BG_DARK) -- Верхняя панель draw.RoundedBoxEx(8, 0, 0, w, 60, COLOR_BG_MEDIUM, true, true, false, false) -- Акцентная линия surface.SetDrawColor(COLOR_PRIMARY) surface.DrawRect(0, 60, w, 3) -- Заголовок draw.SimpleText("◆ ЛОГИ СЕРВЕРА ◆", "ixMenuButtonFont", w/2, 20, COLOR_ACCENT, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER) draw.SimpleText("СИСТЕМА МОНИТОРИНГА СОБЫТИЙ", "ixSmallFont", w/2, 43, COLOR_TEXT_SECONDARY, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER) -- Рамка surface.SetDrawColor(COLOR_BORDER) surface.DrawOutlinedRect(0, 0, w, h, 2) end -- Кнопка закрытия local closeBtn = vgui.Create("DButton", frame) closeBtn:SetSize(40, 40) closeBtn:SetPos(frame:GetWide() - 50, 10) closeBtn:SetText("") closeBtn.Paint = function(s, w, h) local col = s:IsHovered() and Color(200, 50, 50) or Color(150, 50, 50) draw.RoundedBox(4, 0, 0, w, h, col) surface.SetDrawColor(255, 255, 255) surface.DrawLine(w*0.3, h*0.3, w*0.7, h*0.7) surface.DrawLine(w*0.7, h*0.3, w*0.3, h*0.7) end closeBtn.DoClick = function() frame:Close() end -- Панель фильтров local filterPanel = vgui.Create("DPanel", frame) filterPanel:SetSize(250, frame:GetTall() - 80) filterPanel:SetPos(10, 70) filterPanel.Paint = function(s, w, h) draw.RoundedBox(6, 0, 0, w, h, COLOR_BG_MEDIUM) surface.SetDrawColor(COLOR_BORDER) surface.DrawOutlinedRect(0, 0, w, h, 1) end -- Заголовок фильтров local filterHeader = vgui.Create("DLabel", filterPanel) filterHeader:SetPos(10, 10) filterHeader:SetFont("ixSmallFont") filterHeader:SetText("⚙ ФИЛЬТРЫ") filterHeader:SetTextColor(COLOR_TEXT_PRIMARY) filterHeader:SizeToContents() -- Категории local categoryLabel = vgui.Create("DLabel", filterPanel) categoryLabel:SetPos(10, 40) categoryLabel:SetFont("DermaDefault") categoryLabel:SetText("Категория:") categoryLabel:SetTextColor(COLOR_TEXT_SECONDARY) categoryLabel:SizeToContents() local categoryBox = vgui.Create("DComboBox", filterPanel) categoryBox:SetPos(10, 60) categoryBox:SetSize(230, 30) categoryBox:SetValue("Все категории") categoryBox:AddChoice("Все категории") for catID, cat in SortedPairs(self.LOG_CATEGORIES) do categoryBox:AddChoice(cat.icon .. " " .. cat.name, catID) end categoryBox.Paint = function(s, w, h) draw.RoundedBox(4, 0, 0, w, h, COLOR_BG_LIGHT) surface.SetDrawColor(COLOR_PRIMARY) surface.DrawOutlinedRect(0, 0, w, h, 1) end -- Поиск local searchLabel = vgui.Create("DLabel", filterPanel) searchLabel:SetPos(10, 100) searchLabel:SetFont("DermaDefault") searchLabel:SetText("Поиск:") searchLabel:SetTextColor(COLOR_TEXT_SECONDARY) searchLabel:SizeToContents() local searchBox = vgui.Create("DTextEntry", filterPanel) searchBox:SetPos(10, 120) searchBox:SetSize(230, 30) searchBox:SetPlaceholderText("Введите текст для поиска...") searchBox.Paint = function(s, w, h) draw.RoundedBox(4, 0, 0, w, h, COLOR_BG_LIGHT) surface.SetDrawColor(COLOR_PRIMARY) surface.DrawOutlinedRect(0, 0, w, h, 1) s:DrawTextEntryText(COLOR_TEXT_PRIMARY, COLOR_ACCENT, COLOR_TEXT_PRIMARY) end -- Лимит local limitLabel = vgui.Create("DLabel", filterPanel) limitLabel:SetPos(10, 160) limitLabel:SetFont("DermaDefault") limitLabel:SetText("Количество записей:") limitLabel:SetTextColor(COLOR_TEXT_SECONDARY) limitLabel:SizeToContents() local limitSlider = vgui.Create("DNumSlider", filterPanel) limitSlider:SetPos(10, 180) limitSlider:SetSize(230, 30) limitSlider:SetMin(10) limitSlider:SetMax(500) limitSlider:SetDecimals(0) limitSlider:SetValue(100) limitSlider:SetText("") -- Кнопка применить фильтры local applyBtn = vgui.Create("DButton", filterPanel) applyBtn:SetPos(10, 230) applyBtn:SetSize(230, 40) applyBtn:SetText("") applyBtn.Paint = function(s, w, h) local col = s:IsHovered() and COLOR_ACCENT or COLOR_PRIMARY draw.RoundedBox(4, 0, 0, w, h, col) surface.SetDrawColor(COLOR_BORDER) surface.DrawOutlinedRect(0, 0, w, h, 1) draw.SimpleText("🔍 ПРИМЕНИТЬ", "ixSmallFont", w/2, h/2, COLOR_TEXT_PRIMARY, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER) end applyBtn.DoClick = function() local filters = { limit = limitSlider:GetValue() } local _, catID = categoryBox:GetSelected() if catID then filters.category = catID end local searchText = searchBox:GetValue() if searchText and searchText ~= "" then filters.search = searchText end self:RequestLogs(filters) end -- Кнопка очистить логи local clearBtn = vgui.Create("DButton", filterPanel) clearBtn:SetPos(10, 280) clearBtn:SetSize(230, 40) clearBtn:SetText("") clearBtn.Paint = function(s, w, h) local col = s:IsHovered() and Color(200, 50, 50) or Color(150, 50, 50) draw.RoundedBox(4, 0, 0, w, h, col) surface.SetDrawColor(COLOR_BORDER) surface.DrawOutlinedRect(0, 0, w, h, 1) draw.SimpleText("🗑 ОЧИСТИТЬ ЛОГИ", "ixSmallFont", w/2, h/2, COLOR_TEXT_PRIMARY, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER) end clearBtn.DoClick = function() Derma_Query( "Вы уверены что хотите очистить все логи?", "Подтверждение", "Да", function() net.Start("ixServerLogs_Clear") net.SendToServer() clientLogs = {} self:UpdateLogsList() end, "Нет" ) end -- Панель логов local logsPanel = vgui.Create("DPanel", frame) logsPanel:SetSize(frame:GetWide() - 280, frame:GetTall() - 80) logsPanel:SetPos(270, 70) logsPanel.Paint = function(s, w, h) draw.RoundedBox(6, 0, 0, w, h, COLOR_BG_MEDIUM) surface.SetDrawColor(COLOR_BORDER) surface.DrawOutlinedRect(0, 0, w, h, 1) end -- Заголовок логов local logsHeader = vgui.Create("DPanel", logsPanel) logsHeader:SetSize(logsPanel:GetWide(), 40) logsHeader:Dock(TOP) logsHeader.Paint = function(s, w, h) draw.RoundedBoxEx(6, 0, 0, w, h, COLOR_PRIMARY_DARK, true, true, false, false) draw.SimpleText("📋 ЗАПИСИ ЛОГОВ", "ixSmallFont", 10, h/2, COLOR_TEXT_PRIMARY, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER) draw.SimpleText("Всего: " .. #clientLogs, "DermaDefault", w - 10, h/2, COLOR_TEXT_SECONDARY, TEXT_ALIGN_RIGHT, TEXT_ALIGN_CENTER) end -- Список логов local logsList = vgui.Create("DScrollPanel", logsPanel) logsList:Dock(FILL) logsList:DockMargin(5, 5, 5, 5) local sbar = logsList:GetVBar() sbar:SetHideButtons(true) function sbar:Paint(w, h) draw.RoundedBox(4, 0, 0, w, h, COLOR_BG_DARK) end function sbar.btnGrip:Paint(w, h) draw.RoundedBox(4, 0, 0, w, h, COLOR_PRIMARY) end self.logsFrame = frame self.logsList = logsList self.logsHeader = logsHeader -- Загружаем логи self:RequestLogs({limit = 100}) end -- Обновление списка логов function PLUGIN:UpdateLogsList() if not IsValid(self.logsList) then return end self.logsList:Clear() if #clientLogs == 0 then local noLogs = vgui.Create("DLabel", self.logsList) noLogs:SetText("Логи не найдены") noLogs:SetFont("ixSmallFont") noLogs:SetTextColor(COLOR_TEXT_SECONDARY) noLogs:SizeToContents() noLogs:Dock(TOP) noLogs:DockMargin(10, 10, 10, 0) return end -- Обновляем заголовок if IsValid(self.logsHeader) then self.logsHeader.Paint = function(s, w, h) draw.RoundedBoxEx(6, 0, 0, w, h, COLOR_PRIMARY_DARK, true, true, false, false) draw.SimpleText("📋 ЗАПИСИ ЛОГОВ", "ixSmallFont", 10, h/2, COLOR_TEXT_PRIMARY, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER) draw.SimpleText("Всего: " .. #clientLogs, "DermaDefault", w - 10, h/2, COLOR_TEXT_SECONDARY, TEXT_ALIGN_RIGHT, TEXT_ALIGN_CENTER) end end for _, log in ipairs(clientLogs) do local logEntry = vgui.Create("DPanel", self.logsList) logEntry:SetTall(60) logEntry:Dock(TOP) logEntry:DockMargin(5, 5, 5, 0) local category = self.LOG_CATEGORIES[log.category] local categoryColor = category and category.color or color_white logEntry.Paint = function(s, w, h) draw.RoundedBox(4, 0, 0, w, h, COLOR_BG_LIGHT) -- Боковая полоска категории draw.RoundedBoxEx(4, 0, 0, 5, h, categoryColor, true, false, true, false) -- Рамка surface.SetDrawColor(COLOR_BORDER) surface.DrawOutlinedRect(0, 0, w, h, 1) -- Время draw.SimpleText(log.timeString, "DermaDefault", 15, 8, COLOR_TEXT_SECONDARY, TEXT_ALIGN_LEFT, TEXT_ALIGN_TOP) -- Категория local catText = category and (category.icon .. " " .. category.name) or log.category draw.SimpleText(catText, "DermaDefault", 15, 25, categoryColor, TEXT_ALIGN_LEFT, TEXT_ALIGN_TOP) -- Сообщение draw.SimpleText(log.message, "ixSmallFont", 15, 43, COLOR_TEXT_PRIMARY, TEXT_ALIGN_LEFT, TEXT_ALIGN_TOP) end -- Контекстное меню logEntry.OnMousePressed = function(s, btn) if btn == MOUSE_RIGHT then local menu = DermaMenu() menu:AddOption("Копировать сообщение", function() SetClipboardText(log.message) ix.util.Notify("Сообщение скопировано") end):SetIcon("icon16/page_copy.png") if log.target and log.target.steamid and log.target.steamid ~= "N/A" then menu:AddOption("Копировать SteamID (" .. log.target.name .. ")", function() SetClipboardText(log.target.steamid) ix.util.Notify("SteamID скопирован: " .. log.target.steamid) end):SetIcon("icon16/user_edit.png") end if log.data and log.data.attacker and log.data.attacker ~= "world" then menu:AddOption("Копировать SteamID Атакующего", function() SetClipboardText(log.data.attacker) ix.util.Notify("SteamID атакующего скопирован: " .. log.data.attacker) end):SetIcon("icon16/user_delete.png") end menu:AddSpacer() menu:AddOption("Копировать время (" .. log.timeString .. ")", function() SetClipboardText(log.timeString) end):SetIcon("icon16/time.png") menu:AddOption("Копировать полную строку", function() local fullLog = string.format("[%s][%s] %s", log.timeString, category and category.name or log.category, log.message) SetClipboardText(fullLog) ix.util.Notify("Полная строка скопирована") end):SetIcon("icon16/page_white_copy.png") menu:Open() end end end end