class EventCounter {
    constructor() {
        this.events = [];
    }
    // Преобразование timestamp -> строка "HH:mm:ss"
    timestampToTimeString(timestamp) {
        const d = new Date(timestamp);
        const pad = (n) => n.toString().padStart(2, '0');
        return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
    }
    // Сброс — очищаем все события
    reset() {
        this.events = [];
    }
    // Регистрируем событие по timestamp (число в мс)
    registerEvent(timestamp) {
        this.events.push(timestamp);
        this.cleanup(timestamp);
    }
    // Удаляем события старше 24 часов
    cleanup(now) {
        const dayAgo = now - 24 * 60 * 60 * 1000;
        while (this.events.length > 0 && this.events[0] < dayAgo) {
            this.events.shift();
        }
    }
    // Подсчёт событий за последние intervalMs миллисекунд относительно now
    countEventsLast(intervalMs, now = Date.now()) {
        if (this.events.length === 0) return 0;

        const threshold = now - intervalMs;
        return this.events.filter(ts => ts >= threshold).length;
    }
    countPerMinute(now = Date.now()) {
        return this.countEventsLast(60 * 1000, now);
    }
    countPerHour(now = Date.now()) {
        return this.countEventsLast(60 * 60 * 1000, now);
    }
    countPerDay(now = Date.now()) {
        return this.countEventsLast(24 * 60 * 60 * 1000, now);
    }
}


class EventDispatcher {
    constructor() {
        this.listeners = {};
    }
    on(event, callback) {
        if (!this.listeners[event]) {
            this.listeners[event] = [];
        }
        this.listeners[event].push(callback);
    }
    emit(event, data) {
        if (this.listeners[event]) {
            this.listeners[event].forEach(callback => callback(data));
        }
    }
}


class WebSocketService extends EventDispatcher {
    constructor(url) {
        super();
        this.url = url;
        this.socket = null;
        this.reconnectInterval = 5000;
    }

    connect() {
        this.socket = new WebSocket(this.url);

        this.socket.onopen = () => {
            this.emit('connected');
            this.send({
                p: "u"
            });
        };

        this.socket.onmessage = (event) => {
            try {
                const data = JSON.parse(event.data);
                this.emit('message', data);
            } catch (e) {
                this.emit('error', e);
            }
        };

        this.socket.onclose = () => {
            this.emit('disconnected');
            setTimeout(() => this.connect(), this.reconnectInterval);
        };

        this.socket.onerror = (error) => {
            this.emit('error', error);
        };
    }

    send(data) {
        if (this.socket && this.socket.readyState === WebSocket.OPEN) {
            this.socket.send(JSON.stringify(data));
        }
    }
}

class MonitorStore {
    constructor() {
        this.maxItems = this.getMaxItems();
        this.history = {
            'm-1': this.loadHistoryForTab('m-1'),
            'm-2': this.loadHistoryForTab('m-2'),
            'm-3': this.loadHistoryForTab('m-3'),
            'm-4': this.loadHistoryForTab('m-4'),
            'm-5': this.loadHistoryForTab('m-5')
        };
        this.subscribers = [];
    }

    getMaxItems() {
        const saved = localStorage.getItem('UF_MON');
        return saved ? parseInt(saved) : 500;
    }

    loadHistoryForTab(tabId) {
        const saved = localStorage.getItem('monitor-' + tabId);
        return saved ? JSON.parse(saved) : [];
    }

    saveHistoryForTab(tabId) {
        localStorage.setItem('monitor-' + tabId, JSON.stringify(this.history[tabId]));
    }

    addItem(item) {
        const tabId = 'm-' + item.d;
        if (!this.history[tabId]) {
            this.history[tabId] = [];
        }

        this.history[tabId].unshift(item);

        while (this.history[tabId].length > this.maxItems) {
            this.history[tabId].pop();
        }

        this.saveHistoryForTab(tabId);
        this.notify(item, tabId);
    }

    clearHistoryForTab(tabId) {
        this.history[tabId] = [];
        localStorage.removeItem('monitor-' + tabId);
        this.notify(null, tabId);
    }

    subscribe(callback) {
        this.subscribers.push(callback);
    }

    notify(item, tabId) {
        this.subscribers.forEach(cb => cb(item, tabId));
    }
}

class MonitorRenderer {

    constructor(options) {
        this.counter = new EventCounter();
        this.filterid = '';
        this.filterTerr = [];
        this.container = options.container;
        this.store = options.store;
        this.templates = {
            default: this.createDefaultTemplate()
        };

        // Восстанавливаем сохранённое значение view_mon или ставим значение по умолчанию
        this.view_mon = localStorage.getItem('view_mon') || '111111111';

        this.monbuttons();
        this.updateButtonsVisual(); // Приводим кнопки в соответствие с состоянием


        // Текущее время в миллисекундах
        const now = Date.now();
        // Кол-во миллисекунд, прошедших с последней полной секунды
        const msPastSecond = now % 1000;
        // Задержка до следующей полной секунды
        const delay = 1000 - msPastSecond;

        // Запускаем setTimeout, который сработает ровно в начале следующей секунды
        setTimeout(() => {
            this.updateCounters(); // обновим сразу
            // Запускаем setInterval с шагом 1000 мс от строго начала секунды
            setInterval(() => {
                this.updateCounters();
            }, 1000);
        }, delay);

        /*
        this.counterUpdateInterval = setInterval(() => {
            this.updateCounters();
        }, 1000);
        */
    }
    updateCounters() {
        if (!this.counter || !this.container) return;

        const now = Date.now();
        bID('ev-tm').innerHTML = this.counter.timestampToTimeString(now);

        bID('ev-rpsd').innerHTML = this.counter.countPerDay(now);

        bID('ev-rpsm').innerHTML = this.counter.countPerMinute(now);

        bID('ev-rpsh').innerHTML = this.counter.countPerHour(now);
    }
    setFilterId(name) {
        this.filterid = name;
    }
    setFilterTerr(arr) {
        this.filterTerr = arr;
    }
    renderAll(history) {
        sDI('ev-in', 'data-z', 0);
        sDI('ev-out', 'data-z', 0);
        this.counter.reset();

        this.clear();

        history.forEach(item => this.render(item));
    }
    setContainer(newContainer) {
        this.container = newContainer;
        this.clear();

        // получить id контейнера, например 'm-1'
        const history = this.store.history[newContainer.id] || [];

        this.renderAll(history);
    }
    clear() {
        if (this.container) {
            this.container.innerHTML = '';
        }
    }
    monbuttons() {
        const buttons = bTN2('.a-f-btn');

        buttons.forEach(button => {
            button.addEventListener('click', () => {
                const i = button.getAttribute('data-i');
                let z = button.getAttribute('data-z');

                if (z == 0) {
                    z = '1';
                    button.classList.add('color-in');
                } else {
                    z = '0';
                    button.classList.remove('color-in');
                }
                button.setAttribute('data-z', z);

                let arr = this.view_mon.split('');
                if (i >= 0 && i < arr.length) {
                    arr[i] = z;
                }

                this.view_mon = arr.join('');
                localStorage.setItem('view_mon', this.view_mon);

                this.renderAll(this.store.history[this.container.id] || []);

            });
        });
    }

    updateButtonsVisual() {
        const buttons = bTN2('.a-f-btn');
        buttons.forEach(button => {
            const i = button.getAttribute('data-i');
            const z = this.view_mon[i];
            button.setAttribute('data-z', z);
            if (z === '1') {
                button.classList.add('color-in');
            } else {
                button.classList.remove('color-in');
            }
        });
    }
    _tpl_render_count(d) {

        // Преобразуем "HH:mm:ss" в timestamp сегодняшнего дня
        function timeStringToTimestamp(timeStr) {
            const now = new Date();
            const parts = timeStr.split(':');
            if (parts.length !== 3) {
                console.warn('Invalid time format:', timeStr);
                return Date.now(); // fallback — сейчас
            }
            let [hh, mm, ss] = parts.map(Number);
            // Рулез логики: если время больше текущего времени, значит событие было в прошлом дне (например, при 00:10, событие "23:59:00" вчера)
            let date = new Date(now.getFullYear(), now.getMonth(), now.getDate(), hh, mm, ss);

            // Если время события больше текущего, трактуем как вчерашнее событие (чтобы избежать будущих дат)
            if (date.getTime() > now.getTime()) {
                date.setDate(date.getDate() - 1);
            }

            return date.getTime();
        }
        if (d.in_out == 1) {
            let a = bDI('ev-in', 'data-z');
            a = parseInt(a) + 1;
            sDI('ev-in', 'data-z', a);
            bID('ev-in').innerHTML = "" + a + "";
        }
        if (d.in_out == 2) {
            let a = bDI('ev-out', 'data-z');
            a = parseInt(a) + 1;
            sDI('ev-out', 'data-z', a);
            bID('ev-out').innerHTML = "" + a + "";
        }

        // Преобразуем строку tm в timestamp
        let timestamp = timeStringToTimestamp(d.tm);

        // Регистрируем событие с реальным timestamp
        this.counter.registerEvent(timestamp);
    }

    // Возвращает функцию для рендеринга стандартного элемента
    createDefaultTemplate() {

        return (data) => {

            if (data.d === 5 && this.container.id == 'm-5') {

                let tr = '<pre>' + JSON.stringify(data.m, null, 2) + '</pre>';

                return tr;
            }
            if (data.d === 4 && this.container.id == 'm-4') {

                let tr = '<pre>' + JSON.stringify(data.m, null, 2) + '</pre>';

                return tr;
            }
            if (data.d === 3 && this.container.id == 'm-3') {

                let tr = '<pre>' + JSON.stringify(data.m, null, 2) + '</pre>';

                return tr;
            }
            if (data.d === 2 && this.container.id == 'm-2') {

                let tr = '<pre>' + JSON.stringify(data.m, null, 2) + '</pre>';

                return tr;
            }
            if (data.d == 1 && this.container.id == 'm-1') {
                //

                //
                let tr = '';

                let d = data.m;

                if (this.filterid) {
                    let carid = d.car && d.car.id ? d.car.id : '';
                    let fioid = d.fio && d.fio.id ? d.fio.id : '';

                    if (!carid.includes(this.filterid) && !fioid.includes(this.filterid)) {
                        return '';
                    }
                }

                if (this.filterTerr.length > 0) {
                    console.log(this.filterTerr);
                    console.log('find ' + d.terr_id);
                    if (!this.filterTerr.includes(d.terr_id)) {
                        return '';
                    }
                }

                let ft = d.ft;

                let status = '';
                let sicon = '';
                let render = 0;
                let wss41 = 'ws-s4-11';
                let access = 'fa-unlock';
                let access_color = '';
                if (ft[6] === '1') {
                    if (this.view_mon[6] === '1') {
                        render = 1;
                    }
                    sicon = 'fa-road-lock color-negative';
                    status = 'Блок';
                    wss41 = 'ws-s4-12';
                    access = 'fa-lock';
                    access_color = 'color-negative';
                }
                if (ft[2] === '1') {
                    if (this.view_mon[2] === '1') {
                        render = 1;
                    }
                    sicon = 'fa-check color-positive';
                    status = 'Разрешен';

                }
                if (ft[3] === '1') {
                    if (this.view_mon[3] === '1') {
                        render = 1;
                    }
                    sicon = 'fa-car color-positive';
                    status = 'Постоянный';

                }
                if (ft[4] === '1') {
                    if (this.view_mon[4] === '1') {
                        render = 1;
                    }
                    sicon = 'fa-clock-rotate-left color-positive';
                    status = 'Временный';
                }
                if (ft[5] === '1') {
                    if (this.view_mon[5] === '1') {
                        render = 1;
                    }
                    if (data.p === 'pin') {
                        sicon = 'fa-credit-card color-positive';
                    } else {
                        sicon = 'fa-taxi color-positive';
                    }
                    status = 'Гость';
                }
                if (ft[1] === '1') {
                    if (this.view_mon[1] === '1') {
                        render = 1;
                    }
                    sicon = 'fa-lock';
                    status = 'Запрещен';
                    wss41 = 'ws-s4-12';
                    access = 'fa-lock';
                    access_color = 'color-negative';

                }
                if (ft[0] === '1') {
                    if (this.view_mon[0] === '1') {
                        render = 1;
                    }
                    if (data.p === 'pin') {
                        sicon = 'fa-credit-card color-negative';
                    } else if (data.p === 'code') {
                        sicon = 'fa-barcode color-negative';
                    } else {
                        sicon = 'fa-ban color-negative';
                    }
                    access = 'fa-lock';
                    status = 'Нет в базе';
                    wss41 = 'ws-s4-12';
                    access_color = 'color-negative';
                }
                if (d.act == 1 || d.act == 4) {
                    this._tpl_render_count(d);
                }
                if (render == 1) {
                    render = 0;
                    if (ft[7] === '1') {
                        if (this.view_mon[7] === '1') {
                            render = 1;
                        }
                    }
                    if (ft[8] === '1') {
                        if (this.view_mon[8] === '1') {
                            render = 1;
                        }
                    }
                    if (render == 1) {

                        let n = {
                            1: 'fa-arrow-down color-positive',
                            2: 'fa-arrow-up color-negative'
                        };

                        tr = '<table class="ws-s0"><tr><td class="ws-s1">';
                        tr += '<span class="fa-solid ' + sicon + ' pull-left"></span><div class="text-block"><div><b>' + status + '</b></div>';
                        tr += '<div class="time">' + d.tm + '</div></div></td>';

                        tr += '<td class="ws-s2"><div class="text-block"><b>' + d.terr + '</b></div>';
                        tr += '<div class="time2"><span class="fa-solid ' + n[d.in_out] + ' pull-left"></span>' + d.contr + ' </div></td>';

                        let link10 = '';
                        let link11 = '';
                        let link12 = '';
                        let link13 = '';
                        let fio = '';

                        if (data.p === 'auto') {
                            if (d.car.id == 0) {
                                link10 = '<span class="ws-s3-1 ' + access_color + '">' + d.car.name + '</span>';
                            } else {
                                link10 = '<span class="ws-s3-1 ' + access_color + '"><a href="/car/get/' + d.car.id + '" data-transition="window">' + d.car.name + '</a></span>';
                            }

                            if (d.car.marka) {
                                link12 = d.car.marka;
                            }
                            if (d.car.comment) {
                                link13 = '<span class="fa-solid fa-comment"></span> ' + d.car.comment;
                            }
                            if (d.fio.id) {
                                if (d.fio.id != 0) {
                                    fio = '<a href="/card/get/' + d.fio.id + '" data-transition="window">';
                                    fio += '<span class="fa-solid fa-user"></span> <b>' + d.fio.name + '</b></a>';
                                } else {
                                    fio = '<span class="fa-solid fa-user"></span> <b>' + d.fio.name + '</b>';
                                }
                            }
                        }
                        if (data.p === 'pin') {
                            if (d.car.id == 0) {
                                link10 = '<span class="ws-s3-1 ' + access_color + '">' + d.pin + '</span>';

                            } else {

                                link10 = '<span class="ws-s3-1 ' + access_color + '"><a href="/car/get/' + d.car.id + '" data-transition="window">' + d.car.name + '</a></span>';

                                link13 = '<span class="fa-solid fa-credit-card color-positive"></span> ' + d.pin;

                            }
                            if (d.car.comment) {
                                link13 += ' <span class="fa-solid fa-comment"></span> ' + d.car.comment;
                            }
                            if (d.car.marka) {
                                link12 = d.car.marka;
                            }

                            if (d.fio.id) {
                                if (d.fio.id != 0) {
                                    fio = '<a href="/card/get/' + d.fio.id + '" data-transition="window">';
                                    fio += '<span class="fa-solid fa-user"></span> <b>' + d.fio.name + '</b></a>';
                                } else {
                                    fio = '<span class="fa-solid fa-user"></span> <b>' + d.fio.name + '</b>';
                                }
                            }
                        }
                        if (data.p === 'code') {
                            if (d.car.id == 0) {
                                link10 = '<span class="ws-s3-1 ' + access_color + '">' + d.code + '</span>';

                            } else {

                                link10 = '<span class="ws-s3-1 ' + access_color + '"><a href="/car/get/' + d.car.id + '" data-transition="window">' + d.car.name + '</a></span>';

                                link13 = '<span class="fa-solid fa-barcode color-positive"></span> ' + d.code;

                            }
                            if (d.car.comment) {
                                link13 += ' <span class="fa-solid fa-comment"></span> ' + d.car.comment;
                            }
                            if (d.car.marka) {
                                link12 = d.car.marka;
                            }

                            if (d.fio.id) {
                                if (d.fio.id != 0) {
                                    fio = '<a href="/card/get/' + d.fio.id + '" data-transition="window">';
                                    fio += '<span class="fa-solid fa-user"></span> <b>' + d.fio.name + '</b></a>';
                                } else {
                                    fio = '<span class="fa-solid fa-user"></span> <b>' + d.fio.name + '</b>';
                                }
                            }
                        }

                        tr += '<td class="ws-s3">' + link10 + '<span class="ws-s3-2"> ' + link12 + '</span></td>';

                        tr += '<td class="ws-s4"><div class="text-block"><span class="' + wss41 + ' pull-left">';

                        tr += '<span class="fa-solid ' + access + '"></span>&nbsp;' + d.car.access + '</span>' + fio + '&nbsp';

                        tr += '<span class="fa-solid fa-building"></span> <b>' + d.company.name + '</b></div>';

                        tr += '<div class="ws-s4-3">' + link13 + ' <b class="ws-s4-2 pull-right">' + d.company.podr + '</b> </div></td>';

                        tr += '</tr></table>';
                    }
                }
                return tr;
            }
            return '';
        };
    }
    action_access(data) {
        let d = data.m;
        let ft = d.ft;
        // блок
        if (ft[6] === '1') {
            if (this.view_mon[6] === '1') {
                bID('sound-3').play();
            }
        }
        if (ft[1] === '1') {
            if (this.view_mon[1] === '1') {
                bID('sound-2').play();
            }

        }
        if (ft[0] === '1') {
            if (this.view_mon[0] === '1') {
                bID('sound-1').play();
            }
        }
    }
    createDOMElement(html) {
        const tr = document.createElement('li');
        tr.classList.add('table-view-cell');
        tr.innerHTML = html;
        return tr;
    }
    render(data, mode = 1) {
        const template = this.templates.default;
        const tr = template(data);
        if (tr !== '') {
            const element = this.createDOMElement(tr);

            if (mode == 2) {
                this.container.prepend(element);
                this.action_access(data);
            } else {
                this.container.append(element);
            }
        }
    }

}

class MonitorApp {
    constructor() {
        this.wsService = new WebSocketService('wss://' + window.location.hostname + '/ws');
        this.store = new MonitorStore();

        // Создаем рендерер (передаем container по умолчанию, например m-1)
        this.renderer = new MonitorRenderer({
            container: document.getElementById('m-1'),
            store: this.store
        });

        this.activeViewId = 'd-1'; // какая вкладка сейчас активна
        this.containerMap = {
            'd-1': 'm-1',
            'd-2': 'm-2',
            'd-3': 'm-3',
            'd-4': 'm-4',
            'd-5': 'm-5'
        };

        this.initialize();
    }


    initialize() {

        // Подписка на данные сокета - всегда обновляем store
        this.wsService.on('message', (data) => this.handleMessage(data));

        this.store.subscribe((item, containerId) => {
            const currentContainerId = this.containerMap[this.activeViewId];

            // Проверяем, что сообщение относится к активной вкладке
            if (currentContainerId === containerId) {
                this.renderer.render(item, 2);
            }
        });

        // при старте отрисовываем все в текущий контейнер
        this.renderActiveView();

        // Запуск websocket
        this.wsService.connect();
    }

    handleMessage(data) {
        this.store.addItem(data);
    }
    // Меняем активную вкладку
    switchView(viewId) {
        // обновляем активную
        this.activeViewId = viewId;

        // меняем контейнер в рендерере
        const containerId = this.containerMap[viewId];
        if (!containerId) return;

        const container = bID(containerId);
        if (container) {
            this.renderer.setContainer(container);
            this.renderActiveView();
        }
    }

    renderActiveView() {
        this.renderer.clear();
        const containerId = this.containerMap[this.activeViewId];
        const historyForTab = this.store.history[containerId] || [];
        this.renderer.renderAll(historyForTab);
    }
}

function disp_fullscreen() {
    const elem = document.documentElement;
    if (elem.requestFullscreen) {
        elem.requestFullscreen();
    } else if (elem.mozRequestFullScreen) {
        elem.mozRequestFullScreen();
    } else if (elem.webkitRequestFullscreen) {
        elem.webkitRequestFullscreen();
    } else if (elem.msRequestFullscreen) {
        elem.msRequestFullscreen();
    }
}


function disp_list_contr_clb(r) {
    if (r.r == 1) {

    }
}

function disp_list_contr() {

    var listener = function(e) {
        let t = e.target;
        let el = t.childNodes[0];
        //_action_btn_add(t, '');
        _loader_show(t, listener);


        let act = 0;
        if (e.detail.isActive == true) {
            act = 1;
        }
        let data = {
            id: el.name,
            fields: {
                act: act,
                ch: el.value
            }
        };

        RQ('/api/v3/contr/action.json', data).then(r => {
            setTimeout(() => {
                _loader_hide(t, listener);
                disp_list_contr_clb(r);
            }, 2000);
        });
    };
    let g = bCN('tga');
    if (g) {
        if (g.length) {
            for (i = 0; i < g.length; i++) {
                AL(g[i], 'toggle', listener)
            }
        } else {
            AL(g, 'toggle', listener)
        }
    }
}
let monitorApp = null;

function disp_find_select(i, id, max, el) {
    if (bID(el)) {
        monitorApp.renderer.setFilterId(id);
        setTimeout(() => {
            monitorApp.renderer.setFilterId('');
            monitorApp.renderActiveView();
        }, 55000);
    } else {
        monitorApp.renderer.setFilterId('');
    }
    monitorApp.renderActiveView();
}

function disp_list_mont_filter() {

    function _disp_popup() {
        cA('filter-add', 'active');
    }

    function _disp_popupe() {
        cD('filter-add', 'active');

        monitorApp.renderActiveView();
    }

    function _disp_popupe2() {
        cD('filter-add', 'active');
        monitorApp.renderer.setFilterTerr([]);
        monitorApp.renderActiveView();
    }

    function _disp_terr(i, id, max) {
        let terrInput = bID('terr-input');
        let terr = [];

        const value = terrInput.value.trim();

        if (value) {
            console.log('----' + value + '-----');
            terr = JSON.parse(value);
        }

        if (!terr.includes(id)) {
            terr.push(id);
        }

        terrInput.value = JSON.stringify(terr);
        console.log('------------');
        console.log(terr);
        console.log('------------');
        monitorApp.renderer.setFilterTerr(terr);
    }

    function _disp_terr222(i, id, max) {
        let terrInput = bID('terr-input');
        let terr = [];
        let value = terrInput.value.trim();

        if (value.length > 3) {
            try {
                let parsed = JSON.parse(value);

                if (Array.isArray(parsed)) {
                    if (!parsed.includes(id)) {
                        parsed.push(id);
                    }
                    terr = parsed;
                } else if (typeof parsed === 'string') {
                    terr = [parsed, id];
                } else {
                    terr = [id];
                }
            } catch (e) {
                terr = [value, id];
            }
        } else {
            terr = [id];
        }

        terrInput.value = JSON.stringify(terr);

        monitorApp.renderer.setFilterTerr(terr);

    }

    let rq = {
        filter: 'contr'
    };

    RQ('/api/v3/disp/filter.json', rq).then(r => {
        bID('filter-add').innerHTML = r.f;

        AL(bID('filterpopup'), 'click', _disp_popup);
        AL(bID('filter-close'), 'click', _disp_popupe);
        AL(bID('filter-btn'), 'click', _disp_popupe);
        AL(bID('filter-reset'), 'click', _disp_popupe2);

        // from history
        input_select_1('terr-input', '#', _disp_terr, 99, 'terr/find');
    });
}

function disp_list() {

    AL(bID('fullscreen'), 'click', disp_fullscreen);

    input_select_1('find-number-input', '#', disp_find_select, 1, 'disp/find');

    // min.js global
    function onTabChanged(activeViewId) {
        if (monitorApp) {
            monitorApp.switchView(activeViewId);
        }
    }

    monitorApp = new MonitorApp();

    disp_list_contr();
    disp_list_mont_filter();
}



У меня из сокета добавляются данные, хочу сделать эффект мигания - в 2 сикунды -и ли как там чтобы видно было что появилась новая строка