```lua
if CLIENT then
    -- Константы
    local NOTIFICATION_WIDTH, NOTIFICATION_HEIGHT = 300, 100
    local ANIMATION_DURATION = 0.5 -- секунды
    local DISPLAY_DURATION = 5 -- секунды
    local DEFAULT_MODEL = "models/player/kleiner.mdl"
    local NOTIFICATION_BG_COLOR = Color(50, 50, 50, 200)
    local TEXT_COLOR = Color(255, 255, 255)
    -- Позиции уведомления
    local function GetNotificationPositions()
        return {
            startX = ScrW(),
            endX = ScrW() - NOTIFICATION_WIDTH - 10,
            y = ScrH() / 2 - (NOTIFICATION_HEIGHT / 2)
        }
    end

    -- Проверка валидности пути к модели
    local function IsValidModelPath(modelPath)
        return util.IsValidModel(modelPath)
    end

    -- Удаление фрейма
    local function RemoveFrame(frame)
        if IsValid(frame) then frame:Remove() end
    end

    -- Настройка модели
    local function SetupModelPanel(frame, modelPath)
        local mdl = vgui.Create("DModelPanel", frame)
        mdl:SetPos(10, 10)
        mdl:SetSize(80, 80)
        mdl:SetModel(IsValidModelPath(modelPath) and modelPath or DEFAULT_MODEL)
        -- Проверка валидности модели
        if not IsValid(mdl.Entity) or mdl.Entity:GetModel() ~= (IsValidModelPath(modelPath) and modelPath or DEFAULT_MODEL) then mdl:SetModel(DEFAULT_MODEL) end
        local min, max = mdl.Entity:GetRenderBounds()
        local size = math.max(max.x - min.x, max.y - min.y, max.z - min.z)
        mdl:SetFOV(45)
        mdl:SetCamPos(Vector(size, size, size))
        mdl:SetLookAt((min + max) * 0.5)
        -- Вращение модели
        function mdl:LayoutEntity(entity)
            entity:SetAngles(Angle(0, CurTime() * 30, 0))
        end
    end

    -- Настройка текстового лейбла
    local function SetupTextLabel(frame, text)
        local lbl = vgui.Create("DLabel", frame)
        lbl:SetPos(100, 10)
        lbl:SetSize(180, 80)
        lbl:SetText(text)
        lbl:SetTextColor(TEXT_COLOR)
        lbl:SetWrap(true)
        lbl:SetContentAlignment(5) -- Центрирование текста
    end

    -- Настройка фрейма уведомления
    local function SetupFrame(text, modelPath)
        local positions = GetNotificationPositions()
        local frame = vgui.Create("DPanel")
        frame:SetSize(NOTIFICATION_WIDTH, NOTIFICATION_HEIGHT)
        frame:SetPos(positions.startX, positions.y)
        frame:ParentToHUD()
        function frame:Paint(w, h)
            draw.RoundedBox(8, 0, 0, w, h, NOTIFICATION_BG_COLOR)
        end

        SetupTextLabel(frame, text)
        SetupModelPanel(frame, modelPath)
        return frame
    end

    -- Таймер для скрытия уведомления
    local function StartHideTimer(frame)
        timer.Simple(DISPLAY_DURATION, function()
            if IsValid(frame) then
                local _, y = frame:GetPos() -- Получаем y координату корректно
                frame:MoveTo(ScrW(), y, ANIMATION_DURATION, 0, -1, function() RemoveFrame(frame) end)
            end
        end)
    end

    -- Анимация появления фрейма
    local function AnimateFrame(frame)
        local positions = GetNotificationPositions()
        frame:MoveTo(positions.endX, positions.y, ANIMATION_DURATION, 0, -1, function() StartHideTimer(frame) end)
    end

    -- Основная функция создания уведомления
    local function CreateNotification(text, modelPath)
        local frame = SetupFrame(text, modelPath)
        AnimateFrame(frame)
    end

    -- Обработка сетевого сообщения для показа уведомления
    net.Receive("ShowNotification", function()
        local text = net.ReadString()
        local model = net.ReadString()
        CreateNotification(text, model)
    end)

    -- Консольная команда для тестирования уведомления на клиенте
    concommand.Add("show_notification_client", function() CreateNotification("Это тестовое уведомление.", "models/player/alyx.mdl") end)
end
```
```lua
if SERVER then
    util.AddNetworkString("ShowNotification")
    -- Constants
    local DEFAULT_MODEL = "models/player/alyx.mdl"
    local VALID_MODEL_PREFIX = "^models/"
    -- Whitelisted Models (Optional)
    local WHITELISTED_MODELS = {
        ["models/player/alyx.mdl"] = true,
        ["models/player/kleiner.mdl"] = true,
        ["models/player/mossman.mdl"] = true, -- Add more as needed
    }

    -- Helper function to send chat/console messages
    local function NotifySender(ply, message)
        if IsValid(ply) then
            ply:ChatPrint(message)
        else
            print(message)
        end
    end

    -- Function to send a notification to a player
    local function SendNotification(ply, text, modelPath)
        net.Start("ShowNotification")
        net.WriteString(text)
        net.WriteString(modelPath)
        net.Send(ply)
    end

    -- Function to validate Steam ID format
    local function IsValidSteamID(sid)
        return string.match(sid, "^STEAM_[0-5]:%d:%d+$") ~= nil
    end

    -- Function to validate model path against whitelist
    local function IsWhitelistedModel(modelPath)
        return WHITELISTED_MODELS[modelPath] or false
    end

    -- Console command to send notifications
    -- Usage: show_notification_server <steam_id> <text> [<model_path>]
    concommand.Add("show_notification_server", function(ply, cmd, args)
        -- Check if the command is run by a player and if they're admin
        if IsValid(ply) and not ply:IsAdmin() then
            ply:ChatPrint("У вас нет прав для использования этой команды.")
            return
        end

        -- Minimum required arguments: Steam ID and text
        if #args < 2 then
            local usageMsg = "Использование: show_notification_server <steam_id> <текст> [<путь_к_модели>]"
            NotifySender(ply, usageMsg)
            return
        end

        local steamID = args[1]
        -- Validate Steam ID format
        if not IsValidSteamID(steamID) then
            local errorMsg = "Неверный формат Steam ID. Пример: STEAM_0:1:123456"
            NotifySender(ply, errorMsg)
            return
        end

        -- Extract text and optional model path
        table.remove(args, 1) -- Remove Steam ID from args
        local model = DEFAULT_MODEL
        -- Check if the last argument is a model path
        if string.find(args[#args], VALID_MODEL_PREFIX) then
            local potentialModel = args[#args]
            if IsWhitelistedModel(potentialModel) then
                model = potentialModel
            else
                local warnMsg = "Указанная модель не разрешена. Используется модель по умолчанию."
                NotifySender(ply, warnMsg)
            end

            table.remove(args, #args)
        end

        local text = table.concat(args, " ")
        -- Find the player by Steam ID
        local target = player.GetBySteamID(steamID)
        if target and target:IsValid() then
            SendNotification(target, text, model)
            local successMsg = "Уведомление отправлено игроку " .. target:Nick()
            NotifySender(ply, successMsg)
        else
            local notFoundMsg = "Игрок с Steam ID \"" .. steamID .. "\" не найден или не онлайн."
            NotifySender(ply, notFoundMsg)
        end
    end)
end
```
Почему создается несколько окон с уведомлением? Причем одно из них не исчезает, это баг.