4PDA Link Checker

Проверка ссылок + нарушений 3.9 (с обработкой скрытых/удалённых постов, с переходом к ним)

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         4PDA Link Checker
// @author       brant34
// @namespace    http://tampermonkey.net/
// @version      2.6
// @description  Проверка ссылок + нарушений 3.9 (с обработкой скрытых/удалённых постов, с переходом к ним)
// @match        https://4pda.to/*
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @connect      *
// ==/UserScript==

(function () {
    'use strict';

    const style = document.createElement('style');
    style.textContent = `
    #link-checker-panel {
        position: fixed;
        top: 0;
        left: 0;
        right: 0;
        z-index: 9999;
        background: #0055A4;
        color: #fff;
        padding: 10px;
        font-size: 14px;
        border-bottom: 1px solid #004080;
        box-shadow: 0 2px 5px rgba(0,0,0,0.2);
        border-radius: 0 0 6px 6px;
        max-height: 200px;
        overflow-y: auto;
        transition: max-height 0.3s ease;
    }

    #link-checker-panel.collapsed {
        max-height: 28px !important;
        overflow: hidden !important;
    }
    #link-checker-panel.collapsed #link-checker-list,
    #link-checker-panel.collapsed #link-checker-menu {
        display: none !important;
    }

    #link-checker-menu {
        margin-top: 8px;
        display: none;
    }
    #link-checker-menu button {
        margin-right: 8px;
        margin-bottom: 4px;
        background: #ffffff22;
        color: #fff;
        border: 1px solid #fff;
        border-radius: 4px;
        padding: 3px 6px;
        cursor: pointer;
    }
    #link-checker-header {
        display: flex;
        align-items: center;
        gap: 10px;
    }

    .link-checker-entry[data-keyword] {
        background-color: #fff3e0;
        border-left: 3px solid orange;
        padding: 2px;
        margin-bottom: 2px;
    }

    .link-checker-entry a {
        color: #ffe;
    }

    #ignore-domains-modal {
        display: none;
        position: fixed;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        background: #fff;
        padding: 20px;
        border-radius: 5px;
        box-shadow: 0 0 10px rgba(0,0,0,0.5);
        z-index: 10000;
        color: #000;
    }

    #ignore-domains-modal.show {
        display: block;
    }

    #modal-overlay {
        display: none;
        position: fixed;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        background: rgba(0,0,0,0.5);
        z-index: 9999;
    }

    #modal-overlay.show {
        display: block;
    }

    #domain-list li {
        cursor: pointer;
        padding: 5px;
        margin-bottom: 5px;
        border-radius: 3px;
    }

    #domain-list li:hover {
        background-color: #f0f0f0;
    }
    `;
    document.head.appendChild(style);

    const links = document.querySelectorAll('a[href*="4pda.to/stat/go?u="]');
    let pendingRequests = links.length;
    let brokenLinksCount = 0;

    const rule39Exceptions = [
        'https://vk.com/4pdaru',
        'http://vk.com/4pdaru',
        'https://t.me/real4pda'
    ];

    const rule39Hosts = [
        'boosty.to',
        'gofile.io',
        'hyp.sh',
        'halabtech.com',
        'needrom.com',
        't.me',
        'tx.me',
        'telegram.org',
        'terabox.com',
        'vk.com'
    ];

    const skipHosts = [
        'https://invisioncommunity.com',
        'https://twitter.com/4pdaru',
        'http://www.invisionboard.com',
        'http://www.invisionpower.com'
    ];

    const skipUrls = [
        'http://twitter.com/4pdaru',
        'https://twitter.com/4pdaru'
    ];

    // Базовый список проблемных хостов
    const defaultKnownFalse403Hosts = [
        't.me',
        'tx.me',
        'telegram.org',
        'vk.com',
        'samsung.com',
        'kfhost.net',
        'samsung-up.com'
    ];

    // Получение или инициализация списка игнорируемых доменов
    let ignoredDomains = GM_getValue('ignoredDomains', [...defaultKnownFalse403Hosts]);

    const panel = document.createElement('div');
    panel.id = 'link-checker-panel';

    const header = document.createElement('div');
    header.id = 'link-checker-header';

    const gear = document.createElement('span');
    gear.innerHTML = '⚙️';
    gear.style.cursor = 'pointer';
    gear.title = 'Меню';
    header.appendChild(gear);

    const title = document.createElement('span');
    title.innerHTML = `<b>Проверка ссылок... (${pendingRequests} осталось)</b>`;
    title.id = 'link-checker-title';
    header.appendChild(title);

    panel.appendChild(header);

    const menu = document.createElement('div');
    menu.id = 'link-checker-menu';
    menu.innerHTML = `
        <button id="manual-check">🔄 Ручной поиск</button>
        <button id="remove-broken">🗑 Скрыть битые ссылки</button>
        <button id="ignore-domains">⚠️ Настроить игнорируемые домены</button>
    `;
    panel.appendChild(menu);

    const container = document.createElement('div');
    container.id = 'link-checker-list';
    panel.appendChild(container);

    document.body.appendChild(panel);

    const panelEntries = new Map();

    // Рекурсивная проверка текста
    function getAllTextContent(element) {
        let text = '';
        const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, false);
        let node;
        while (node = walker.nextNode()) {
            text += node.textContent.trim();
        }
        return text;
    }

    // Проверка видимости поста
    function isPostVisible(post) {
        if (!post) return false;
        const table = post.closest('table[data-post]');
        const isInDOM = document.contains(post);
        const hasOffsetParent = post.offsetParent !== null;
        const style = table ? window.getComputedStyle(table) : window.getComputedStyle(post);
        const isHiddenByClass = table && (table.classList.contains('hidepin') || table.classList.contains('deletedpost'));
        const isHiddenByStyle = style.display === 'none' || style.visibility === 'hidden';
        const content = getAllTextContent(post);
        const isHiddenByText = content.includes('[HIDE]');
        const isDeletedByText = content.includes('[DELETE]');
        console.log(`Проверка видимости: ${post.id}, inDOM: ${isInDOM}, offsetParent: ${hasOffsetParent}, hiddenByClass: ${isHiddenByClass}, hiddenByStyle: ${isHiddenByStyle}, hiddenByText: ${isHiddenByText}, deletedByText: ${isDeletedByText}, content: "${content}"`);
        return isInDOM && hasOffsetParent && !isHiddenByClass && !isHiddenByStyle && !isHiddenByText && !isDeletedByText;
    }

    // Поиск слов VPN, ВПН, КВН
    const keywordRegex = /(^|[^а-яa-z0-9])(VPN|ВПН|КВН)(?![а-яa-z0-9])/gi;
    const posts = document.querySelectorAll('.post_body');

    for (const post of posts) {
        const postContainer = post.closest('div.postcolor[id^="post-"]');
        if (!postContainer) continue;
        if (keywordRegex.test(post.textContent)) {
            const matched = post.textContent.match(keywordRegex).join(', ');
            const postId = postContainer.id;
            const isVisible = isPostVisible(postContainer);
            const entry = document.createElement('div');
            entry.className = 'link-checker-entry';
            entry.setAttribute('data-keyword', 'true');
            entry.setAttribute('data-post-id', postId);
            const entryHTML = `🟠 Найдено слово: <b>${matched}</b> (пост: ${postId})`;
            entry.setAttribute('data-original-html', entryHTML);
            entry.innerHTML = isVisible ? entryHTML : `${entryHTML} <i>(скрыт/удалён)</i>`;
            entry.style.cursor = 'pointer';
            entry.onclick = () => handleEntryClick(postContainer, postId);
            document.getElementById('link-checker-list').appendChild(entry);
            panelEntries.set(postId + '-keyword', { entry, postId });
            if (isVisible) {
                post.style.backgroundColor = '#fff3e0';
                post.style.border = '2px solid orange';
            }
        }
    }

    // Проверка всех ссылок на нарушение правила 3.9
    const allLinks = document.querySelectorAll('a[href]');
    for (const a of allLinks) {
        const href = a.href;
        try {
            const url = new URL(href);
            if (rule39Hosts.some(host => url.hostname.includes(host))) {
                const linkText = a.innerText || 'Без текста';
                addRule39Link(href, linkText, a);
            }
        } catch (e) {
            // ignore malformed URLs
        }
    }

    gear.addEventListener('click', () => {
        menu.style.display = (menu.style.display === 'block') ? 'none' : 'block';
    });

    document.getElementById('manual-check')?.addEventListener('click', () => location.reload());

    document.getElementById('remove-broken')?.addEventListener('click', () => {
        document.querySelectorAll('[data-broken-link="true"]').forEach(e => e.remove());
        container.innerHTML = '';
        title.innerHTML = `<b>🛠 Битые ссылки скрыты.</b>`;
        menu.style.display = 'none';
    });

    // Модальное окно для настройки доменов
    const modalOverlay = document.createElement('div');
    modalOverlay.id = 'modal-overlay';
    document.body.appendChild(modalOverlay);

    const modal = document.createElement('div');
    modal.id = 'ignore-domains-modal';
    modal.innerHTML = `
        <h3>Игнорируемые домены</h3>
        <p>Введите домен (например, example.com) и нажмите "Добавить". Кликните на домен, чтобы удалить его.</p>
        <input type="text" id="domain-input" placeholder="Домен" style="width: 200px; padding: 5px; margin-right: 10px;">
        <button id="add-domain">Добавить</button>
        <ul id="domain-list" style="list-style-type: none; padding: 0; margin-top: 10px;"></ul>
        <button id="close-modal" style="margin-top: 10px;">Закрыть</button>
    `;
    document.body.appendChild(modal);

    function updateDomainList() {
        const list = document.getElementById('domain-list');
        list.innerHTML = '';
        ignoredDomains.forEach(domain => {
            const li = document.createElement('li');
            li.textContent = domain;
            li.addEventListener('click', () => {
                const index = ignoredDomains.indexOf(domain);
                if (index !== -1) {
                    ignoredDomains.splice(index, 1);
                    updateDomainList();
                    showNotification(`Домен ${domain} удалён из списка игнорируемых.`);
                }
            });
            list.appendChild(li);
        });
        GM_setValue('ignoredDomains', ignoredDomains);
    }

    document.getElementById('ignore-domains')?.addEventListener('click', () => {
        modal.classList.add('show');
        modalOverlay.classList.add('show');
        updateDomainList();
    });

    document.getElementById('close-modal')?.addEventListener('click', () => {
        modal.classList.remove('show');
        modalOverlay.classList.remove('show');
    });

    document.getElementById('add-domain')?.addEventListener('click', () => {
        const input = document.getElementById('domain-input');
        const domain = input.value.trim();
        if (domain && !ignoredDomains.includes(domain)) {
            ignoredDomains.push(domain);
            updateDomainList();
            showNotification(`Домен ${domain} добавлен в список игнорируемых.`);
        }
        input.value = '';
    });

    modalOverlay.addEventListener('click', () => {
        modal.classList.remove('show');
        modalOverlay.classList.remove('show');
    });

    function showNotification(message) {
        const notification = document.createElement('div');
        notification.style.cssText = 'position: fixed; top: 10px; right: 10px; background: #ff4444; color: white; padding: 10px; border-radius: 4px; z-index: 10000;';
        notification.textContent = message;
        document.body.appendChild(notification);
        setTimeout(() => notification.remove(), 3000);
    }

    function addRule39Link(href, linkText, originalLink) {
        if (rule39Exceptions.includes(href)) return;

        const post = originalLink.closest('div.postcolor[id^="post-"]');
        const postId = post ? post.id : 'unknown';
        const isVisible = post ? isPostVisible(post) : false;

        const entry = document.createElement('div');
        entry.className = 'link-checker-entry';
        entry.setAttribute('data-post-id', postId);
        const entryHTML = `🚫 Нарушение п. 3.9: <a href="${href}" target="_blank">${href}</a> (текст: ${linkText}, пост: ${postId})`;
        entry.setAttribute('data-original-html', entryHTML);
        entry.innerHTML = isVisible ? entryHTML : `${entryHTML} <i>(скрыт/удалён)</i>`;
        entry.style.cursor = 'pointer';
        entry.onclick = () => handleEntryClick(post, postId);
        document.getElementById('link-checker-list').appendChild(entry);
        panelEntries.set(postId + '-rule39-' + href, { entry, postId });

        if (isVisible) {
            originalLink.style.border = '2px solid green';
            originalLink.style.backgroundColor = '#e8f5e9';
            originalLink.style.padding = '2px';
            originalLink.title = 'Нарушение правила 3.9 (запрещённый ресурс)';
            originalLink.setAttribute('data-rule-39', 'true');
        }
    }

    function addBrokenLink(realUrl, status, linkText, originalLink) {
        brokenLinksCount++;
        const post = originalLink.closest('div.postcolor[id^="post-"]');
        const postId = post ? post.id : 'unknown';
        const isVisible = post ? isPostVisible(post) : false;

        const entry = document.createElement('div');
        entry.className = 'link-checker-entry';
        entry.setAttribute('data-post-id', postId);
        const entryHTML = `❌ <a href="${realUrl}" target="_blank">${realUrl}</a> (статус: ${status}, текст: ${linkText}, пост: ${postId})`;
        entry.setAttribute('data-original-html', entryHTML);
        entry.innerHTML = isVisible ? entryHTML : `${entryHTML} <i>(скрыт/удалён)</i>`;
        entry.style.cursor = 'pointer';
        entry.onclick = () => handleEntryClick(post, postId);
        document.getElementById('link-checker-list').appendChild(entry);
        panelEntries.set(postId + '-broken-' + realUrl, { entry, postId });

        if (isVisible) {
            originalLink.style.border = '2px solid red';
            originalLink.style.backgroundColor = '#ffebee';
            originalLink.style.padding = '2px';
            originalLink.title = 'Битая ссылка (статус: ' + status + ')';
            originalLink.setAttribute('data-broken-link', 'true');
        }
    }

    function handleEntryClick(post, postId) {
        if (!post) {
            showNotification(`Пост ${postId} не найден в DOM.`);
            return;
        }

        const table = post.closest('table[data-post]');
        const isInDOM = document.contains(post);
        const hasOffsetParent = post.offsetParent !== null;
        const style = table ? window.getComputedStyle(table) : window.getComputedStyle(post);
        const isHiddenByClass = table && (table.classList.contains('hidepin') || table.classList.contains('deletedpost'));
        const isHiddenByStyle = style.display === 'none' || style.visibility === 'hidden';
        const content = getAllTextContent(post);
        const isHiddenByText = content.includes('[HIDE]');
        const isDeletedByText = content.includes('[DELETE]');

        // Прокрутка непосредственно к целевому посту
        post.scrollIntoView({ behavior: 'smooth', block: 'center' });

        // Уведомление о статусе поста
        if (!isInDOM) {
            showNotification(`Пост ${postId} отсутствует в DOM.`);
        } else if (!hasOffsetParent) {
            showNotification(`Пост ${postId} не отображается (нет offsetParent).`);
        } else if (isHiddenByClass) {
            showNotification(`Пост ${postId} скрыт/удалён (класс ${table.classList.contains('hidepin') ? 'hidepin' : 'deletedpost'}).`);
        } else if (isHiddenByStyle) {
            showNotification(`Пост ${postId} скрыт стилями (display: ${style.display}, visibility: ${style.visibility}).`);
        } else if (isHiddenByText) {
            showNotification(`Пост ${postId} скрыт текстом [HIDE].`);
        } else if (isDeletedByText) {
            showNotification(`Пост ${postId} удалён текстом [DELETE].`);
        } else {
            showNotification(`Пост ${postId} отображен.`);
        }
    }

    function updatePanel() {
        title.innerHTML = `<b>Проверка ссылок... (${pendingRequests} осталось)</b>`;
        if (pendingRequests === 0) {
            title.innerHTML = `<b>Все ссылки проверены. Найдено ${brokenLinksCount} битых ссылок.</b>`;
        }
    }

    function checkLink(realUrl, linkText, originalLink, method = 'HEAD', attempt = 1) {
        const urlObj = new URL(realUrl);
        const isIgnoredHost = ignoredDomains.some(host => urlObj.hostname.includes(host));

        GM_xmlhttpRequest({
            method: method,
            url: realUrl,
            headers: {
                'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
            },
            timeout: 10000,
            onload: function (response) {
                console.log(`Ссылка: ${realUrl}, Метод: ${method}, Статус: ${response.status}, Final URL: ${response.finalUrl || 'N/A'}, Response Headers: ${response.responseHeaders || 'N/A'}`);

                // Обработка редиректов
                if ([301, 302, 303, 307, 308].includes(response.status) && response.finalUrl && attempt < 3) {
                    console.log(`Редирект: ${realUrl} -> ${response.finalUrl}`);
                    checkLink(response.finalUrl, linkText, originalLink, method, attempt + 1);
                    return;
                }

                // Пропускаем 403 для игнорируемых хостов
                if (response.status === 403 && isIgnoredHost) {
                    console.log(`Игнорируем 403 для ${realUrl} (игнорируемый хост)`);
                    pendingRequests--;
                    updatePanel();
                    return;
                }

                // Пробуем GET для проблемных хостов при 403
                if (response.status === 403 && !isIgnoredHost && method === 'HEAD' && attempt < 3) {
                    console.log(`Статус 403 для проблемного хоста ${urlObj.hostname}, пробуем GET`);
                    checkLink(realUrl, linkText, originalLink, 'GET', attempt + 1);
                    return;
                }

                // Проверяем другие коды ошибок
                if ([404, 410].includes(response.status)) {
                    addBrokenLink(realUrl, response.status, linkText, originalLink);
                } else if (response.status >= 200 && response.status < 300) {
                    console.log(`Ссылка ${realUrl} рабочая (статус: ${response.status})`);
                } else {
                    console.warn(`Неопределённый статус для ${realUrl}: ${response.status}`);
                }

                pendingRequests--;
                updatePanel();
            },
            onerror: function () {
                console.log(`Ошибка проверки ссылки: ${realUrl}`);
                addBrokenLink(realUrl, 'Ошибка', linkText, originalLink);
                pendingRequests--;
                updatePanel();
            },
            ontimeout: function () {
                console.log(`Таймаут проверки ссылки: ${realUrl}`);
                if (!isIgnoredHost && method === 'HEAD' && attempt < 3) {
                    console.log(`Таймаут для проблемного хоста ${urlObj.hostname}, пробуем GET`);
                    checkLink(realUrl, linkText, originalLink, 'GET', attempt + 1);
                } else {
                    addBrokenLink(realUrl, 'Таймаут', linkText, originalLink);
                    pendingRequests--;
                    updatePanel();
                }
            }
        });
    }

    links.forEach((a, i) => {
        const urlParams = new URLSearchParams(a.href.split('?')[1]);
        const realUrl = decodeURIComponent(urlParams.get('u') || '');
        const linkText = a.innerText || 'Без текста';

        if (!realUrl) {
            console.warn('Пустой URL найден для ссылки:', a.href);
            pendingRequests--;
            updatePanel();
            return;
        }

        if (skipUrls.includes(realUrl) || skipHosts.some(host => realUrl.startsWith(host))) {
            console.log(`⏩ Пропущена ссылка: ${realUrl}`);
            pendingRequests--;
            updatePanel();
            return;
        }

        if (rule39Hosts.some(host => realUrl.includes(host))) {
            addRule39Link(realUrl, linkText, a);
        }

        setTimeout(() => {
            checkLink(realUrl, linkText, a);
        }, i * 100);
    });

    if (links.length === 0) {
        title.innerHTML = `<b>Ссылок для проверки не найдено.</b>`;
    }

    function getVisibleText(element) {
        const clone = element.cloneNode(true);
        clone.querySelectorAll('script, style').forEach(el => el.remove());
        let text = '';
        const walker = document.createTreeWalker(clone, NodeFilter.SHOW_TEXT, null, false);
        while (walker.nextNode()) {
            text += walker.currentNode.nodeValue + ' ';
        }
        const links = clone.querySelectorAll('a[href]');
        links.forEach(link => {
            const href = link.getAttribute('href');
            if (href) text += ' ' + href;
            const linkText = link.textContent.trim();
            if (linkText) text += ' ' + linkText;
        });
        text = text.replace(/\[.\]/g, '.').replace(/[-_]/g, '.');
        return text.replace(/[\n\r\s]+/g, ' ').toLowerCase().trim();
    }

    window.addEventListener('load', () => {
        setTimeout(() => {
            const posts = document.querySelectorAll('div.postcolor[id^="post-"]');
            for (const post of posts) {
                const postId = post.id;
                const text = getVisibleText(post);
                console.log("VPN-тест: пост", post.id, "| текст:", text);
                if (text.includes('vpn') || text.includes('квн') || text.includes('впн')) {
                    const isVisible = isPostVisible(post);
                    const entry = document.createElement('div');
                    entry.className = 'link-checker-entry';
                    entry.setAttribute('data-post-id', postId);
                    const entryHTML = `⚠️ Обсуждение VPN (пост: ${postId})`;
                    entry.setAttribute('data-original-html', entryHTML);
                    entry.innerHTML = isVisible ? entryHTML : `${entryHTML} <i>(скрыт/удалён)</i>`;
                    entry.style.cursor = 'pointer';
                    entry.onclick = () => handleEntryClick(post, postId);
                    document.getElementById('link-checker-list').appendChild(entry);
                    panelEntries.set(postId + '-vpn', { entry, postId });
                    if (isVisible) {
                        post.style.backgroundColor = '#fff3e0';
                        post.style.border = '2px solid orange';
                        post.title = 'Обсуждение VPN';
                        post.setAttribute('data-rule-39', 'true');
                    }
                }
            }
        }, 1000);
    });

    const postContainer = document.querySelector('.ipsForum_topic') || document.body;
    const observer = new MutationObserver((mutations) => {
        let needsUpdate = false;
        for (const mutation of mutations) {
            if (mutation.type === 'childList') {
                for (const node of mutation.removedNodes) {
                    const post = node.querySelector('div.postcolor[id^="post-"]') || node.closest('div.postcolor[id^="post-"]');
                    if (post) {
                        needsUpdate = true;
                        console.log(`Обнаружено удаление поста: ${post.id}`);
                        break;
                    }
                }
                for (const node of mutation.addedNodes) {
                    const post = node.querySelector('div.postcolor[id^="post-"]') || node.closest('div.postcolor[id^="post-"]');
                    if (post) {
                        needsUpdate = true;
                        console.log(`Обнаружено добавление поста: ${post.id}`);
                    }
                }
            } else if (mutation.type === 'characterData') {
                const post = mutation.target.parentNode.closest('div.postcolor[id^="post-"]');
                if (post) {
                    needsUpdate = true;
                    console.log(`Изменение текста в посте: ${post.id}`);
                }
            } else if (mutation.type === 'attributes') {
                const post = mutation.target.closest('div.postcolor[id^="post-"]');
                if (post || mutation.target.classList.contains('hidepin') || mutation.target.classList.contains('deletedpost')) {
                    needsUpdate = true;
                    console.log(`Изменение атрибутов или классов (hidepin/deletedpost): ${post ? post.id : 'неизвестно'}`);
                }
            }
        }
        if (needsUpdate) {
            setTimeout(() => {
                panelEntries.forEach(({ entry, postId }) => {
                    const post = document.querySelector(`div.postcolor[id="${postId}"]`);
                    const isVisible = post ? isPostVisible(post) : false;
                    const originalHTML = entry.getAttribute('data-original-html');
                    entry.innerHTML = isVisible ? originalHTML : `${originalHTML} <i>(скрыт/удалён)</i>`;
                    console.log(`Обновление статуса поста: ${postId}, visible: ${isVisible}`);
                });
            }, 500);
        }
    });

    observer.observe(postContainer, {
        childList: true,
        subtree: true,
        attributes: true,
        attributeFilter: ['style', 'class', 'hidden'],
        characterData: true
    });

    setInterval(() => {
        let needsUpdate = false;
        panelEntries.forEach(({ entry, postId }) => {
            const post = document.querySelector(`div.postcolor[id="${postId}"]`);
            const isVisible = post ? isPostVisible(post) : false;
            const originalHTML = entry.getAttribute('data-original-html');
            const currentHTML = entry.innerHTML;
            const expectedHTML = isVisible ? originalHTML : `${originalHTML} <i>(скрыт/удалён)</i>`;
            if (currentHTML !== expectedHTML) {
                entry.innerHTML = expectedHTML;
                console.log(`Периодическое обновление статуса поста: ${postId}, visible: ${isVisible}`);
                needsUpdate = true;
            }
        });
        if (needsUpdate) {
            console.log('Периодическая проверка: обновлены статусы постов');
        }
    }, 2000);
})();