1 Востаннє редагувалося sasha87 (17.04.2026 19:19:48)

Тема: автоматичний рефреш сторінки після рестарту IIS App Pool у грі в шашки

Опис
Є ASP.NET Core проект для онлайн‑гри в шашки.
Проблема: після перезапуску пулу IIS усі WebSocket‑з’єднання обриваються.

Клієнт бачить повідомлення “Reconnecting…”, але фігури не можуть ходити.

Гра зависає, і щоб продовжити, користувачеві доводиться вручну оновлювати сторінку (F5).

Стан гри (дошка, рахунок, таймери) зберігається у Local Storage та на сервері, але не підтягується автоматично після перепідключення.
Очікувана поведінка
Після рестарту IIS App Pool клієнт має автоматично перепідключитися до SignalR, отримати актуальний стан кімнати й продовжити гру без ручного рефрешу сторінки.

Технічні деталі
ASP.NET Core SignalR, хостинг під IIS.

Використовую OnDisconnectedAsync для очищення ConnectionId та таймер на повторне підключення.

На клієнті налаштовано withAutomaticReconnect().

Проблема: після recycle клієнт підключається, але події від хаба не доходять, і хід фігур неможливий без ручного refresh.
Дошка і шашки після рестарту пулу мають бути на своїх місцях і бути активними, що можливо тільки після рефрешу сторінки вручну.
Запитання
Як правильно реалізувати автоматичний рефреш/перепідключення після перезапуску IIS App Pool, щоб:

клієнт не потребував ручного оновлення сторінки;

SignalR‑хаб автоматично відновлював стан гри;

фігури могли ходити одразу після reconnect

//checkershub.cs
using Microsoft.AspNetCore.SignalR;
using CheckersGame.Services;
using CheckersGame.Models;

namespace CheckersGame.Hubs;

public class CheckersHub : Hub
{
    private readonly GameService _game;
    private readonly ILogger<CheckersHub> _logger;

    public CheckersHub(GameService game, ILogger<CheckersHub> logger)
    {
        _game = game;
        _logger = logger;
    }

    public async Task JoinGame(PlayerInfo player)
    {
        if (player == null || string.IsNullOrEmpty(player.EntityId))
        {
            await Clients.Caller.SendAsync("nameTaken");
            return;
        }

        player.Id = Context.ConnectionId;
        player.ConnectionId = Context.ConnectionId;

        _logger.LogInformation("\U0001f3ae JoinGame: {name}({eid}) connId={cid}",
            player.PlayerName, player.EntityId, Context.ConnectionId);

        _game.CancelDisconnectTimer(player.EntityId);

        // ── Reconnect to existing room ─────────────────────────────────────
        var existingRoom = _game.FindRoomByEntityId(player.EntityId);
        if (existingRoom != null)
        {
            _logger.LogInformation("\u267b\ufe0f RECONNECT: room {room} for {eid}",
                existingRoom.RoomName, player.EntityId);

            if (existingRoom.Player1.EntityId == player.EntityId)
            {
                existingRoom.Player1.Id = Context.ConnectionId;
                existingRoom.Player1.ConnectionId = Context.ConnectionId;
            }
            else
            {
                existingRoom.Player2.Id = Context.ConnectionId;
                existingRoom.Player2.ConnectionId = Context.ConnectionId;
            }

            await Groups.AddToGroupAsync(Context.ConnectionId, existingRoom.RoomName);
            await Clients.Caller.SendAsync("joinedRoom", existingRoom.RoomName);

            var state = _game.GetLatestGameState(existingRoom.RoomName);

            var resumePayload = new
            {
                players = new[] { existingRoom.Player1, existingRoom.Player2 },
                roomName = existingRoom.RoomName,
                boardState = state?.BoardState ?? "",
                currentPlayer = state?.CurrentPlayer ?? 0,
                player1Score = state?.Player1Score ?? 0,
                player2Score = state?.Player2Score ?? 0,
                lastMoveTime = state?.LastMoveTime ?? DateTime.UtcNow.ToString("O")
            };

            await Clients.Caller.SendAsync("resumeGame", resumePayload);

            bool p1Connected = !string.IsNullOrEmpty(existingRoom.Player1.ConnectionId);
            bool p2Connected = !string.IsNullOrEmpty(existingRoom.Player2.ConnectionId);

            if (p1Connected && p2Connected)
            {
                _logger.LogInformation("\u2705 Both players reconnected in {room} \u2014 scheduling bothPlayersReady", existingRoom.RoomName);
                var capturedRoom = existingRoom;
                var capturedState = state;
                // FIX: Get IHubContext from DI BEFORE the delay
                // Hub instance is disposed after JoinGame returns, so Clients is dead inside Task.Delay
                var hubContext = Context.GetHttpContext()!.RequestServices
                    .GetRequiredService<IHubContext<CheckersHub>>();
                _ = Task.Delay(1200).ContinueWith(async _ =>
                {
                    try
                    {
                        var freshState = _game.GetLatestGameState(capturedRoom.RoomName);
                        await hubContext.Clients.Group(capturedRoom.RoomName).SendAsync("bothPlayersReady", new
                        {
                            players = new[] { capturedRoom.Player1, capturedRoom.Player2 },
                            roomName = capturedRoom.RoomName,
                            boardState = freshState?.BoardState ?? capturedState?.BoardState ?? "",
                            currentPlayer = freshState?.CurrentPlayer ?? capturedState?.CurrentPlayer ?? 0,
                            player1Score = freshState?.Player1Score ?? capturedState?.Player1Score ?? 0,
                            player2Score = freshState?.Player2Score ?? capturedState?.Player2Score ?? 0,
                            lastMoveTime = freshState?.LastMoveTime ?? capturedState?.LastMoveTime ?? DateTime.UtcNow.ToString("O")
                        });
                        _logger.LogInformation("\U0001f4e1 bothPlayersReady sent to room {room}", capturedRoom.RoomName);
                    }
                    catch (Exception ex) { _logger.LogError("bothPlayersReady error: {msg}", ex.Message); }
                });
            }
            else
            {
                await Clients.OthersInGroup(existingRoom.RoomName)
                    .SendAsync("opponentReconnected", new { entityId = player.EntityId });
            }

            _logger.LogInformation("\U0001f4e6 resumeGame sent for {room}", existingRoom.RoomName);
            return;
        }

        // ── Guard: prevent duplicate join ──────────────────────────────────
        if (_game.IsWaiting(player.EntityId) || _game.IsInRoom(player.EntityId))
        {
            await Clients.Caller.SendAsync("nameTaken");
            return;
        }

        if (_game.IsExpired(player.EntityId))
        {
            _logger.LogInformation("\u26a0\ufe0f IsExpired for {eid} \u2014 clearing, letting player rejoin", player.EntityId);
            _game.ClearExpired(player.EntityId);
        }

        // ── Add to matchmaking queue ───────────────────────────────────────
        _game.AddWaitingPlayer(player);
        _logger.LogInformation("\u23f3 WAITING: {eid}", player.EntityId);

        var (p1, p2) = _game.TryMatchWaiting();
        if (p1 != null && p2 != null)
            await MatchPlayers(p1, p2);
    }

    public async Task GroupGame(PlayerInfo player, string inviteRoom)
    {
        if (string.IsNullOrEmpty(player.EntityId)) return;
        player.Id = Context.ConnectionId;
        player.ConnectionId = Context.ConnectionId;
        var groupName = inviteRoom.Split('?')[0];
        player.InviteRoom = groupName;
        if (!_game.IsInGroup(player.EntityId, groupName))
            _game.AddGroupPlayer(player);
        var members = _game.GetGroupPlayers(groupName);
        if (members.Count >= 2)
            await MatchPlayers(members[0], members[1]);
        else
            await Clients.Caller.SendAsync("waitingGroupMember",
                new[] { new { entityId = 111, name = "Waiting for your invited friend" } });
    }

    public async Task SaveState(string roomName, string boardState, int currentPlayer, int p1Score, int p2Score)
    {
        var room = _game.GetRoom(roomName);
        if (room == null) return;
        _game.UpsertGameState(roomName, boardState, currentPlayer, p1Score, p2Score);
        _logger.LogInformation("\U0001f4be State saved: {room}", roomName);
    }

    public async Task Move(object moveData)
    {
        var room = _game.FindRoomByConnectionId(Context.ConnectionId);
        if (room == null) return;
        await Clients.OthersInGroup(room.RoomName).SendAsync("opponentMove", moveData);
    }

    public async Task MoveClick(object moveData)
    {
        var room = _game.FindRoomByConnectionId(Context.ConnectionId);
        if (room == null) return;
        await Clients.OthersInGroup(room.RoomName).SendAsync("opponentMove_click", moveData);
    }

    public async Task SendEmoji(string emojiName)
    {
        var room = _game.FindRoomByConnectionId(Context.ConnectionId);
        if (room == null) return;
        await Clients.OthersInGroup(room.RoomName).SendAsync("sendEmoji", emojiName);
    }

    public async Task Updatetimer(object timer)
    {
        var room = _game.FindRoomByConnectionId(Context.ConnectionId);
        if (room == null) return;
        await Clients.OthersInGroup(room.RoomName).SendAsync("updatetimer", timer);
    }

    public async Task ActiveStatus(bool status)
    {
        var room = _game.FindRoomByConnectionId(Context.ConnectionId);
        if (room == null) return;
        await Clients.OthersInGroup(room.RoomName).SendAsync("active_status", status);
    }

    public async Task Giveup(int playerName)
    {
        var room = _game.FindRoomByConnectionId(Context.ConnectionId);
        if (room == null) return;
        await Clients.Group(room.RoomName).SendAsync("giveup", playerName);
    }

    public async Task Toggleuser(object status)
    {
        var room = _game.FindRoomByConnectionId(Context.ConnectionId);
        if (room == null) return;
        await Clients.OthersInGroup(room.RoomName).SendAsync("toggleuser", status);
    }

    public async Task DisconnectGame()
    {
        _game.RemoveWaitingByConnectionId(Context.ConnectionId);
        _game.RemoveGroupByConnectionId(Context.ConnectionId);
        var room = _game.FindRoomByConnectionId(Context.ConnectionId);
        if (room == null) return;
        await Clients.OthersInGroup(room.RoomName).SendAsync("playerDisconnected", room.RoomName);
        _game.RemoveRoom(room.RoomName);
    }

    public override async Task OnDisconnectedAsync(Exception? exception)
    {
        _logger.LogInformation("\u274c Disconnected: {cid}", Context.ConnectionId);

        _game.RemoveWaitingByConnectionId(Context.ConnectionId);
        _game.RemoveGroupByConnectionId(Context.ConnectionId);

        var room = _game.FindRoomByConnectionId(Context.ConnectionId);
        if (room == null)
        {
            await base.OnDisconnectedAsync(exception);
            return;
        }

        var disconnected = (room.Player1.Id == Context.ConnectionId || room.Player1.ConnectionId == Context.ConnectionId)
            ? room.Player1 : room.Player2;
        var entityId = disconnected.EntityId;
        var disconnectedConnId = Context.ConnectionId;
        var capturedRoomName = room.RoomName;

        _logger.LogInformation("\u26a0\ufe0f Player {eid} left room {room}", entityId, capturedRoomName);

        // Clear ConnectionId so bothPlayersReady check correctly detects disconnected
        if (room.Player1.EntityId == entityId)
        {
            room.Player1.ConnectionId = "";
            room.Player1.Id = "";
        }
        else
        {
            room.Player2.ConnectionId = "";
            room.Player2.Id = "";
        }

        if (string.IsNullOrEmpty(entityId))
        {
            _game.RemoveRoom(capturedRoomName);
            await base.OnDisconnectedAsync(exception);
            return;
        }

        // Get IHubContext for use in timer callback (hub will be disposed by then)
        var hubContext = Context.GetHttpContext()!.RequestServices
            .GetRequiredService<IHubContext<CheckersHub>>();

        _game.StartDisconnectTimer(entityId, 60_000, async () =>
        {
            var r = _game.FindRoomByEntityId(entityId);
            if (r == null)
            {
                _logger.LogInformation("Room already gone for {eid}", entityId);
                return;
            }

            var rp = r.Player1.EntityId == entityId ? r.Player1 : r.Player2;
            if (!string.IsNullOrEmpty(rp.ConnectionId))
            {
                _logger.LogInformation("\u267b\ufe0f Player {eid} already reconnected \u2014 no action", entityId);
                return;
            }

            _logger.LogInformation("\u23f1\ufe0f Player {eid} timeout \u2014 ending room {room}", entityId, capturedRoomName);
            _game.MarkExpired(entityId);
            await hubContext.Clients.Group(capturedRoomName)
                .SendAsync("playerDisconnected", capturedRoomName);
            _game.TryAtomicRemoveRoom(capturedRoomName);
        });

        await base.OnDisconnectedAsync(exception);
    }

    private async Task MatchPlayers(PlayerInfo p1, PlayerInfo p2)
    {
        var roomName = _game.GenerateRoomName();
        _logger.LogInformation("\u26a1 MATCH: {p1} \u2194 {p2}", p1.PlayerName, p2.PlayerName);

        try
        {
            var host = Context.GetHttpContext()?.Request.Host.Host ?? "";
            bool isLocal = host == "localhost" || host == "127.0.0.1";

            if (isLocal)
            {
                p1.GamesEntryID = p2.GamesEntryID = 999999;
                p1.PrizeUSD = p2.PrizeUSD = 0;
                p1.GameID = p2.GameID = 999999;
            }
            else
            {
                var soap = Context.GetHttpContext()!.RequestServices.GetRequiredService<SoapService>();
                var entry = await soap.EntityEntryUpdate(p1.TokenId, p2.TokenId, 0);
                if (entry == null)
                {
                    _logger.LogWarning("MatchPlayers: SOAP returned null");
                    var p1Cid = string.IsNullOrEmpty(p1.ConnectionId) ? p1.Id : p1.ConnectionId;
                    var p2Cid = string.IsNullOrEmpty(p2.ConnectionId) ? p2.Id : p2.ConnectionId;
                    await Clients.Client(p1Cid).SendAsync("nameTaken");
                    await Clients.Client(p2Cid).SendAsync("nameTaken");
                    return;
                }
                p1.GamesEntryID = p2.GamesEntryID = entry.GamesEntryID;
                p1.PrizeUSD = p2.PrizeUSD = entry.PrizeUSD;
                p1.GameID = p2.GameID = entry.GamesEntryID;
            }

            var room = new GameRoom { RoomName = roomName, Player1 = p1, Player2 = p2 };
            _game.AddRoom(room);
            _game.UpsertGameState(roomName, "", 0, 0, 0);

            var p1ConnId = string.IsNullOrEmpty(p1.ConnectionId) ? p1.Id : p1.ConnectionId;
            var p2ConnId = string.IsNullOrEmpty(p2.ConnectionId) ? p2.Id : p2.ConnectionId;

            await Groups.AddToGroupAsync(p1ConnId, roomName);
            await Groups.AddToGroupAsync(p2ConnId, roomName);

            await Clients.Client(p1ConnId).SendAsync("joinedRoom", roomName);
            await Clients.Client(p2ConnId).SendAsync("joinedRoom", roomName);

            await Clients.Group(roomName).SendAsync("startGamebySocket", new[] { p1, p2 });

            _logger.LogInformation("\U0001f3ae ROOM CREATED: {room} ({p1} vs {p2})",
                roomName, p1.PlayerName, p2.PlayerName);
        }
        catch (Exception ex)
        {
            _logger.LogError("MatchPlayers error: {msg}", ex.Message);
        }
    }
}
//signalradapter.js////
(function () {
    let signalRReady = false;
    let pendingIo = null;

    const script = document.createElement('script');
    script.src = 'https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/8.0.0/signalr.min.js';
    script.onload = function () { signalRReady = true; if (pendingIo) pendingIo(); };
    document.head.appendChild(script);

    window.io = function () {
        let connection = null;
        const handlers = {};
        let connected = false;
        const queue = [];
        let _retryTimer = null;
        let _isBuilding = false;

        function on(event, callback) {
            if (!handlers[event]) handlers[event] = [];
            handlers[event].push(callback);
        }
        function trigger(event) {
            var args = Array.prototype.slice.call(arguments, 1);
            (handlers[event] || []).forEach(function(cb) { cb.apply(null, args); });
        }
        function flushQueue() {
            var pending = queue.splice(0);
            pending.forEach(function(item) { invokeMethod(item.event, item.args); });
        }

        var serverEvents = [
            'startGamebySocket', 'resumeGame', 'opponentReconnected', 'bothPlayersReady',
            'joinedRoom', 'opponentMove', 'opponentMove_click',
            'updatetimer', 'giveup', 'toggleuser',
            'playerDisconnected', 'opponentDisconnected',
            'active_status', 'sendEmoji', 'nameTaken', 'waitingGroupMember',
            'gameExpired', 'syncState'
        ];

        function buildConnection() {
            if (_isBuilding) return;
            _isBuilding = true;

            // Kill old connection silently
            if (connection) {
                try { connection.stop(); } catch(e) {}
                connection = null;
            }

            try {
                connection = new signalR.HubConnectionBuilder()
                    .withUrl('/checkers-hub')
                    .withAutomaticReconnect({
                        nextRetryDelayInMilliseconds: function(ctx) {
                            return [0, 1000, 2000, 5000, 5000][Math.min(ctx.previousRetryCount, 4)];
                        }
                    })
                    .configureLogging(signalR.LogLevel.Warning)
                    .build();

                serverEvents.forEach(function(evt) {
                    connection.on(evt, function() {
                        var args = Array.prototype.slice.call(arguments);
                        trigger.apply(null, [evt].concat(args));
                    });
                });

                connection.onreconnecting(function(error) {
                    console.warn('⚠️ SignalR reconnecting...', error && error.message || '');
                    connected = false;
                    window._needsPageReload = true;
                    trigger('reconnecting', { reason: error && error.message || 'Connection lost' });
                    trigger('disconnect', { permanent: false });
                });

                connection.onreconnected(function(connectionId) {
                    console.log('✅ SignalR reconnected, id:', connectionId);
                    connected = true;
                    stopRetry();
                    // Always reload page after reconnect — guarantees clean game state
                    var gameEnded = false;
                    try { gameEnded = sessionStorage.getItem('gameEnded') === '1'; } catch(e) {}
                    if (!gameEnded && window._needsPageReload) {
                        console.log('???? Reloading page for clean reconnect...');
                        location.reload();
                        return;
                    }
                    trigger('reconnected');
                    trigger('connect');
                    flushQueue();
                });

                // IIS pool restart: all auto-reconnect retries failed
                connection.onclose(function(error) {
                    console.error('❌ SignalR closed:', error && error.message || '');
                    connected = false;
                    connection = null;
                    _isBuilding = false;
                    window._needsPageReload = true;
                    trigger('disconnect', { permanent: false });
                    startRetry();
                });

                connection.start()
                    .then(function() {
                        connected = true;
                        _isBuilding = false;
                        stopRetry();
                        console.log('✅ SignalR connected');
                        // If we were disconnected before, reload page for clean state
                        var gameEnded = false;
                        try { gameEnded = sessionStorage.getItem('gameEnded') === '1'; } catch(e) {}
                        if (window._needsPageReload && !gameEnded) {
                            console.log('???? Reloading page after reconnect...');
                            location.reload();
                            return;
                        }
                        trigger('connect');
                        flushQueue();
                    })
                    .catch(function(err) {
                        console.warn('SignalR start failed:', err.message);
                        _isBuilding = false;
                        connection = null;
                        startRetry();
                    });
            } catch(e) {
                console.error('buildConnection error:', e);
                _isBuilding = false;
                connection = null;
                startRetry();
            }
        }

        function startRetry() {
            if (_retryTimer) return;
            _isBuilding = false;
            console.log('???? Retry loop started (every 3s)');
            _retryTimer = setInterval(function() {
                if (connected) { stopRetry(); return; }
                console.log('???? Attempting reconnect...');
                buildConnection();
            }, 3000);
        }

        function stopRetry() {
            if (_retryTimer) {
                clearInterval(_retryTimer);
                _retryTimer = null;
            }
            _isBuilding = false;
        }

        if (signalRReady) buildConnection();
        else pendingIo = buildConnection;

        function invokeMethod(event, args) {
            if (!connection) {
                if (event === 'joinGame' || event === 'groupGame' || event === 'pushState')
                    queue.push({ event: event, args: args });
                return;
            }

            if (event === 'joinGame' && args.length === 1 && args[0].player) {
                var w = args[0]; var p = w.player;
                connection.invoke('JoinGame', {
                    PlayerName: w.playerName || '',
                    TokenId: p.TokenId || p.tokenId || '',
                    EntityId: String(p.entityId || p.EntityId || ''),
                    ConnectionId: '',
                    CountryName: p.CountryName || p.countryName || '',
                    Status: String(p.Status || p.status || ''),
                    BetUsd: Number(p.betUsd || p.BetUsd || 0),
                    GameID: Number(p.gameID || p.GameID || 2),
                    GamesEntryID: Number(p.games_entryID || p.GamesEntryID || p.gamesEntryID || 0),
                    PrizeUSD: Number(p.prizeUSD || p.PrizeUSD || 0),
                    IsBot: Number(w.isBot || 0),
                }).catch(function(err) { console.error('JoinGame error:', err); });
                return;
            }

            if (event === 'groupGame' && args.length === 1 && args[0].player) {
                var w = args[0]; var p = w.player;
                connection.invoke('GroupGame', {
                    PlayerName: w.playerName || '',
                    TokenId: p.TokenId || p.tokenId || '',
                    EntityId: String(p.entityId || p.EntityId || ''),
                    ConnectionId: '',
                    CountryName: p.CountryName || p.countryName || '',
                    Status: String(p.Status || p.status || ''),
                    BetUsd: Number(p.betUsd || p.BetUsd || 0),
                    GameID: Number(p.gameID || p.GameID || 2),
                    GamesEntryID: Number(p.games_entryID || p.GamesEntryID || p.gamesEntryID || 0),
                    PrizeUSD: Number(p.prizeUSD || p.PrizeUSD || 0),
                    IsBot: Number(w.isBot || 0),
                    InviteRoom: w.invite_room || '',
                }, w.invite_room || '').catch(function(err) { console.error('GroupGame error:', err); });
                return;
            }

            if (event === 'saveState') {
                var a = args;
                if (a.length === 1 && typeof a[0] === 'object') {
                    var d = a[0];
                    connection.invoke('SaveState',
                        String(d.roomName || ''), String(d.boardState || ''),
                        Number(d.currentPlayer || 0), Number(d.p1Score || 0), Number(d.p2Score || 0)
                    ).catch(function(err) { console.error('SaveState error:', err); });
                } else {
                    connection.invoke('SaveState',
                        String(a[0] || ''), String(a[1] || ''),
                        Number(a[2] || 0), Number(a[3] || 0), Number(a[4] || 0)
                    ).catch(function(err) { console.error('SaveState error:', err); });
                }
                return;
            }

            if (event === 'pushState') {
                var a = args;
                if (a.length === 1 && typeof a[0] === 'object') {
                    var d = a[0];
                    connection.invoke('PushState',
                        String(d.roomName || ''), String(d.boardState || ''),
                        Number(d.currentPlayer || 0), Number(d.p1Score || 0), Number(d.p2Score || 0)
                    ).catch(function(err) { console.error('PushState error:', err); });
                }
                return;
            }

            var methodMap = {
                'move': 'Move', 'move_click': 'MoveClick',
                'sendEmoji': 'SendEmoji', 'updatetimer': 'Updatetimer',
                'active_status': 'ActiveStatus', 'giveup': 'Giveup',
                'toggleuser': 'Toggleuser', 'disconnect_game': 'DisconnectGame',
            };
            var method = methodMap[event] || event;
            connection.invoke(method, ...args).catch(function(err) {
                console.error('SignalR [' + method + ']:', err);
            });
        }

        return {
            on: on,
            emit: function(event) {
                var args = Array.prototype.slice.call(arguments, 1);
                if (!connection || !connected) {
                    if (event === 'joinGame' || event === 'groupGame' || event === 'pushState') {
                        queue.push({ event: event, args: args });
                    }
                    return;
                }
                invokeMethod(event, args);
            },
            get id() { return connection && connection.connectionId ? connection.connectionId : ''; },
            get isConnected() { return connected; },
            disconnect: function() { stopRetry(); if (connection) connection.stop(); }
        };
    };
})();

2

Re: автоматичний рефреш сторінки після рестарту IIS App Pool у грі в шашки

Ось репозиторій - https://github.com/SashaMaksyutenko/checkers

3

Re: автоматичний рефреш сторінки після рестарту IIS App Pool у грі в шашки

Зробив цей проект і все працювало до того часу поки не додав туди бота Kingsrow і усе необхидне для роботи бота. На сервері юзається IIS  і однією з вимог було щоб гра продовжувалась після рестарту пулу кожну хвилину. На сервері все працювало без бота. Кілька разів перезаливав и всеодно не працює. Навіть попередня версія без бота, яка раніше працювала яку залив останній раз теж відає помилку 503. Думаю що проблема десь на сервері. Можливо пул почав блокуватись після кількох крашів чищось таке. Можливо треба новий пул? Хтось щось може підказати?