User:BZPN/MassRollback2.js: Difference between revisions

From Test Wiki
Jump to navigation Jump to search
Content deleted Content added
BZPN (talk | contribs)
No edit summary
BZPN (talk | contribs)
No edit summary
Line 9:
}
this.createUI();
// Try to enhance to Codex after UI is present (only if styles loaded)
if (mw.loader.getState && (mw.loader.getState('codex-styles') === 'ready' || mw.loader.getState('codex-search-styles') === 'ready')) {
this.applyCodexClasses();
});
this.bindEvents();
this.fetchUserStats();
Line 25 ⟶ 29:
$('#mw-content-text').prepend($toggleButton).prepend($container);
},
 
// Apply Codex CSS-only classes when styles are available; keep MW UI as fallback
$(functionapplyCodexClasses: function() {
try {
// Buttons: map MediaWiki UI modifiers to Codex equivalents and keep both for compatibility
$('#mass-rollback-container').find('button.mw-ui-button').each(function() {
const $btn = $(this);
$btn.addClass('cdx-button');
if ($btn.hasClass('mw-ui-progressive')) {
$btn.addClass('cdx-button--action-progressive');
}
if ($btn.hasClass('mw-ui-destructive')) {
$btn.addClass('cdx-button--action-destructive');
}
if ($btn.hasClass('mw-ui-quiet')) {
$btn.addClass('cdx-button--weight-quiet');
}
});
// Inputs: add Codex input class to enhance styling when available
$('#mass-rollback-container').find('input[type="text"], input[type="date"], select').each(function() {
const $el = $(this);
$el.addClass('cdx-text-input__input');
// Optional: wrap with Codex container if not already wrapped
if (!$el.parent().hasClass('cdx-text-input')) {
$el.wrap('<div class="cdx-text-input"></div>');
}
});
}). catch(function (e) {
// Non-fatal: just skip Codex enhancements
console.warn('Codex class application skipped:', e);
}
},
 
Line 381 ⟶ 417:
};
 
// Load stable MW modules and try Codex CSS-only styles if present (1.39+)
// Ładujemy stabilne moduły MW; bez OOUI, aby uniknąć problemów z loaderem
]).then(function loadModulesAndInit() {
mw.loader.using([
'mediawiki.api',var baseDeps = [
'mediawiki.utilapi',
'mediawiki.notifyutil',
'mediawiki.uinotify',
'mediawiki.ui.button'];
var codexDep = null;
]).then(function () {
$(function ()try {
try// {Prefer MassRollback.init();the }smaller catchset (e)if {available, console.error(e)else full; }else skip
if (mw.loader.getState('codex-search-styles')) {
});
codexDep = 'codex-search-styles';
}).catch(function () {
} else if (mw.loader.getState('codex-styles')) {
$(function () {
try { MassRollback.init(); } catchcodexDep (e)= { console.error(e)'codex-styles'; }
}
} catch (e) { /* ignore */ }
 
// Also load legacy mediawiki.ui when present to keep fallback look
var legacyUI = [];
if (mw.loader.getState('mediawiki.ui')) legacyUI.push('mediawiki.ui');
if (mw.loader.getState('mediawiki.ui.button')) legacyUI.push('mediawiki.ui.button');
 
var deps = baseDeps.concat(legacyUI);
if (codexDep) deps.push(codexDep);
 
mw.loader.using([deps).then(function() {
$(function() { try { MassRollback.init(); } catch (e) { console.error(e); } });
}).catch(function() {
// Final fallback: try to init anyway
$(function() { try { MassRollback.init(); } catch (e) { console.error(e); } });
});
})();
})(jQuery, mediaWiki);

Revision as of 20:13, 12 September 2025

(function($, mw) {
    'use strict';

    const MassRollback = {

        init: function() {
            if (mw.config.get('wgCanonicalSpecialPageName') !== 'Contributions') {
                return;
            }
            this.createUI();
            // Try to enhance to Codex after UI is present (only if styles loaded)
            if (mw.loader.getState && (mw.loader.getState('codex-styles') === 'ready' || mw.loader.getState('codex-search-styles') === 'ready')) {
                this.applyCodexClasses();
            }
            this.bindEvents();
            this.fetchUserStats();
        },

        createUI: function() {
            const $container = $(`
                <div id=\"mass-rollback-container\" class=\"mw-portlet\" style=\"display:none;\">\n                    <h3 class=\"mw-headline\" style=\"margin-top:0;\">Mass rollback tool</h3>\n\n                    <div id=\"stats-section\" class=\"mw-message-box mw-message-box-notice\" style=\"margin-bottom:1em;\">\n                        <h4 class=\"mw-ui-headline\" style=\"margin:0 0 .5em 0;\">User statistics</h4>\n                        <p>Edits: <strong id=\"total-edits\">-</strong></p>\n                        <p>First edit: <span id=\"first-edit\">-</span></p>\n                        <p>Recent edit: <span id=\"last-edit\">-</span> <button id=\"refresh-stats\" class=\"mw-ui-button mw-ui-progressive\">Refresh</button></p>\n                    </div>\n\n                    <div id=\"filters-section\" class=\"mw-portlet-body\" style=\"margin-bottom:1em;\">\n                        <h4 class=\"mw-ui-headline\">Filter edits</h4>\n                        <div class=\"mw-input\" style=\"margin-bottom:.5em;\">\n                            <label class=\"mw-label\" style=\"margin-right:.5em;\">Date from:</label>\n                            <input type=\"date\" id=\"start-date\" class=\"mw-ui-input\">\n                            <label class=\"mw-label\" style=\"margin:0 .5em;\">to:</label>\n                            <input type=\"date\" id=\"end-date\" class=\"mw-ui-input\">\n                        </div>\n                        <div class=\"mw-input\" style=\"margin-bottom:.5em;\">\n                            <label class=\"mw-label\" style=\"margin-right:.5em;\">Namespace:</label>\n                            <select id=\"namespace-filter\" multiple class=\"mw-ui-input\" style=\"min-width:150px;\">\n                                <option value=\"all\">All</option>\n                                <option value=\"0\">Main</option>\n                                <option value=\"1\">Talk</option>\n                                <option value=\"2\">User</option>\n                                <option value=\"3\">User talk</option>\n                                <option value=\"4\">Project</option>\n                                <option value=\"5\">Project talk</option>\n                                <option value=\"6\">File</option>\n                                <option value=\"7\">File talk</option>\n                                <option value=\"8\">MediaWiki</option>\n                                <option value=\"9\">MediaWiki talk</option>\n                                <option value=\"10\">Template</option>\n                                <option value=\"11\">Template talk</option>\n                                <option value=\"12\">Help</option>\n                                <option value=\"13\">Help talk</option>\n                                <option value=\"14\">Category</option>\n                                <option value=\"15\">Category talk</option>\n                                <option value=\"100\">Portal</option>\n                                <option value=\"101\">Portal talk</option>\n                            </select>\n                        </div>\n                        <div class=\"mw-input\" style=\"margin-bottom:.5em;\">\n                            <label class=\"mw-label\" style=\"margin-right:.5em;\">Edit size:</label>\n                            <select id=\"size-filter\" class=\"mw-ui-input\">\n                                <option value=\"\">Any</option>\n                                <option value=\"small\">Small (&lt; 50 bytes)</option>\n                                <option value=\"medium\">Medium (50-500 bytes)</option>\n                                <option value=\"large\">Large (&gt; 500 bytes)</option>\n                            </select>\n                        </div>\n                        <div class=\"mw-input\" style=\"margin-bottom:.5em;\">\n                            <label class=\"mw-label\" style=\"margin-right:.5em;\">Sort order:</label>\n                            <select id=\"sort-order\" class=\"mw-ui-input\">\n                                <option value=\"desc\">Latest first</option>\n                                <option value=\"asc\">Oldest first</option>\n                            </select>\n                        </div>\n                        <div class=\"mw-input\" style=\"margin-bottom:.5em;\">\n                            <button id=\"clear-filters\" class=\"mw-ui-button\">Clear filters</button>\n                            <button id=\"show-filtered\" class=\"mw-ui-button mw-ui-progressive\" style=\"margin-left:.5em;\">Show filtered edits</button>\n                        </div>\n                    </div>\n\n                    <div id=\"reason-section\" class=\"mw-portlet-body\" style=\"margin-bottom:1em;\">\n                        <h4 class=\"mw-ui-headline\">Rollback reason</h4>\n                        <select id=\"rollback-reason\" class=\"mw-ui-input\">\n                            <option value=\"\"></option>\n                            <option value=\"vandalism\">Vandalism</option>\n                            <option value=\"spam\">Spam</option>\n                            <option value=\"test\">Test rollback</option>\n                            <option value=\"teste\">Test edit</option>\n                            <option value=\"rules-violation\">Violation</option>\n                            <option value=\"other\">Other</option>\n                        </select>\n                        <input type=\"text\" id=\"custom-reason\" placeholder=\"Enter custom reason\" class=\"mw-ui-input\" style=\"display:none; margin-top:.5em; width:100%;\">\n                    </div>\n\n                    <div id=\"actions-section\" class=\"mw-portlet-body\" style=\"text-align:center;\">\n                        <button id=\"rollback-selected\" class=\"mw-ui-button mw-ui-progressive\" style=\"margin-right:.5em;\">Rollback selected</button>\n                        <button id=\"rollback-all\" class=\"mw-ui-button mw-ui-destructive\">Rollback all</button>\n                    </div>\n                </div>\n\n                <div id=\"filtered-edits-container\" class=\"mw-portlet\" style=\"display:none;\">\n                    <h4 class=\"mw-ui-headline\">Filtered edits</h4>\n                    <div class=\"mw-input\" style=\"margin-bottom:.5em;\">\n                        <input type=\"checkbox\" id=\"select-all\"> <label for=\"select-all\">Select all</label>\n                    </div>\n                    <table id=\"filtered-edits-table\" class=\"wikitable\" style=\"width:100%;\">\n                        <thead>\n                            <tr>\n                                <th>Revid</th>\n                                <th>Title</th>\n                                <th>Timestamp</th>\n                                <th>Namespace</th>\n                                <th>Size</th>\n                                <th>Select</th>\n                            </tr>\n                        </thead>\n                        <tbody>\n                            <!-- Wstawiane dynamicznie -->\n                        </tbody>\n                    </table>\n                    <div style=\"text-align:center; margin-top:.5em;\">\n                        <button id=\"rollback-selected-filtered\" class=\"mw-ui-button mw-ui-progressive\">Rollback selected filtered edits</button>\n                    </div>\n                </div>\n            `);

            const $toggleButton = $(`
                <button id=\"mass-rollback-toggle\" class=\"mw-ui-button mw-ui-progressive\" style=\"margin-bottom:.75em;\">\n                    Show/hide MassRollback\n                </button>\n            `);
            
            $toggleButton.on('click', function() {
                $('#mass-rollback-container').slideToggle();
            });
            
            $('#mw-content-text').prepend($toggleButton).prepend($container);
        },

        // Apply Codex CSS-only classes when styles are available; keep MW UI as fallback
        applyCodexClasses: function() {
            try {
                // Buttons: map MediaWiki UI modifiers to Codex equivalents and keep both for compatibility
                $('#mass-rollback-container').find('button.mw-ui-button').each(function() {
                    const $btn = $(this);
                    $btn.addClass('cdx-button');
                    if ($btn.hasClass('mw-ui-progressive')) {
                        $btn.addClass('cdx-button--action-progressive');
                    }
                    if ($btn.hasClass('mw-ui-destructive')) {
                        $btn.addClass('cdx-button--action-destructive');
                    }
                    if ($btn.hasClass('mw-ui-quiet')) {
                        $btn.addClass('cdx-button--weight-quiet');
                    }
                });
                // Inputs: add Codex input class to enhance styling when available
                $('#mass-rollback-container').find('input[type="text"], input[type="date"], select').each(function() {
                    const $el = $(this);
                    $el.addClass('cdx-text-input__input');
                    // Optional: wrap with Codex container if not already wrapped
                    if (!$el.parent().hasClass('cdx-text-input')) {
                        $el.wrap('<div class="cdx-text-input"></div>');
                    }
                });
            } catch (e) {
                // Non-fatal: just skip Codex enhancements
                console.warn('Codex class application skipped:', e);
            }
        },

        bindEvents: function() {
            $('#rollback-reason').on('change', function() {
                $('#custom-reason').toggle($(this).val() === 'other');
            });

            $('#clear-filters').on('click', function() {
                $('#start-date').val('');
                $('#end-date').val('');
                $('#namespace-filter').val(['all']);
                $('#size-filter').val('');
                $('#sort-order').val('desc');
            });

            $('#refresh-stats').on('click', this.fetchUserStats.bind(this));
            $('#show-filtered').on('click', this.displayFilteredEdits.bind(this));
            $('#rollback-selected-filtered').on('click', this.rollbackSelectedFromFiltered.bind(this));
            $('#rollback-selected').on('click', this.rollbackSelected.bind(this));
            $('#rollback-all').on('click', this.rollbackAll.bind(this));

            $(document).on('change', '#select-all', function() {
                const isChecked = $(this).is(':checked');
                $('#filtered-edits-table tbody input[type="checkbox"]').prop('checked', isChecked);
            });

            // Dodaj checkbox przy edycjach w widoku kontrybucji
            $('li[data-mw-revid]').each(function() {
                const $li = $(this);
                const revid = $li.data('mw-revid');
                const parentid = $li.data('mw-prev-revid');
                const title = $li.find('.mw-contributions-title').text().trim();
                const $checkbox = $('<input>', {
                    type: 'checkbox',
                    class: 'rollback-checkbox',
                    'data-revid': revid,
                    'data-parentid': parentid,
                    'data-title': title,
                    style: 'margin-right: 5px;'
                });
                $li.prepend($checkbox);
            });
        },

        fetchUserStats: function() {
            const userName = mw.config.get('wgRelevantUserName');
            const api = new mw.Api();

            // Poprawne pobieranie liczby edycji i pierwszej/ostatniej edycji
            const editCountReq = api.get({
                action: 'query',
                list: 'users',
                ususers: userName,
                usprop: 'editcount'
            });
            const firstEditReq = api.get({
                action: 'query',
                list: 'usercontribs',
                ucuser: userName,
                uclimit: 1,
                ucdir: 'newer',
                ucprop: 'timestamp'
            });
            const lastEditReq = api.get({
                action: 'query',
                list: 'usercontribs',
                ucuser: userName,
                uclimit: 1,
                ucdir: 'older',
                ucprop: 'timestamp'
            });

            $.when(editCountReq, firstEditReq, lastEditReq).done(function(editCountData, firstData, lastData) {
                try {
                    const editcount = editCountData[0].query.users[0].editcount;
                    $('#total-edits').text(editcount != null ? editcount : '-');
                } catch (e) {
                    $('#total-edits').text('-');
                }
                try {
                    const firstList = firstData[0].query.usercontribs || [];
                    $('#first-edit').text(firstList.length ? new Date(firstList[0].timestamp).toLocaleDateString() : '-');
                } catch (e) {
                    $('#first-edit').text('-');
                }
                try {
                    const lastList = lastData[0].query.usercontribs || [];
                    $('#last-edit').text(lastList.length ? new Date(lastList[0].timestamp).toLocaleDateString() : '-');
                } catch (e) {
                    $('#last-edit').text('-');
                }
            }).fail(function() {
                $('#total-edits').text('-');
                $('#first-edit').text('-');
                $('#last-edit').text('-');
            });
        },

        getNamespaceName: function(ns) {
            const nsMapping = {
                0: 'Main',
                1: 'Talk',
                2: 'User',
                3: 'User talk',
                4: 'Project',
                5: 'Project talk',
                6: 'File',
                7: 'File talk',
                8: 'MediaWiki',
                9: 'MediaWiki talk',
                10: 'Template',
                11: 'Template talk',
                12: 'Help',
                13: 'Help talk',
                14: 'Category',
                15: 'Category talk',
                100: 'Portal',
                101: 'Portal talk'
            };
            return nsMapping[ns] || ns;
        },

        applyFilters: function(contributions) {
            const startDate = $('#start-date').val() ? new Date($('#start-date').val()) : null;
            const endDate = $('#end-date').val() ? new Date($('#end-date').val()) : null;
            let namespaceFilter = $('#namespace-filter').val() || [];
            const sizeFilter = $('#size-filter').val();
            const sortOrder = $('#sort-order').val();

            if (namespaceFilter.includes('all')) {
                namespaceFilter = [];
            }

            let filtered = contributions.filter(contrib => {
                const contribDate = new Date(contrib.timestamp);
                if (startDate && contribDate < startDate) return false;
                if (endDate && contribDate > endDate) return false;
                if (namespaceFilter.length > 0 && !namespaceFilter.includes(contrib.ns.toString())) return false;
                if (sizeFilter) {
                    const size = contrib.size || 0;
                    switch (sizeFilter) {
                        case 'small': return size < 50;
                        case 'medium': return size >= 50 && size <= 500;
                        case 'large': return size > 500;
                    }
                }
                return true;
            });

            filtered.sort((a, b) => {
                return sortOrder === 'asc'
                    ? new Date(a.timestamp) - new Date(b.timestamp)
                    : new Date(b.timestamp) - new Date(a.timestamp);
            });

            return filtered;
        },

        displayFilteredEdits: function() {
            const userName = mw.config.get('wgRelevantUserName');
            const api = new mw.Api();
            api.get({
                action: 'query',
                list: 'usercontribs',
                ucuser: userName,
                uclimit: 'max',
                ucprop: 'ids|title|timestamp|size|ns|parentid'
            }).done((data) => {
                const allContributions = data.query.usercontribs;
                const filtered = this.applyFilters(allContributions);
                const $tbody = $('#filtered-edits-table tbody');
                $tbody.empty();

                if (filtered.length === 0) {
                    $tbody.append('<tr><td colspan="6" style="text-align:center; padding:5px;">No edits match the selected filters.</td></tr>');
                } else {
                    filtered.forEach(contrib => {
                        const row = `
                            <tr>
                                <td style="padding: 5px; border: 1px solid #ccc;">${contrib.revid}</td>
                                <td style="padding: 5px; border: 1px solid #ccc;">${contrib.title}</td>
                                <td style="padding: 5px; border: 1px solid #ccc;">${new Date(contrib.timestamp).toLocaleString()}</td>
                                <td style="padding: 5px; border: 1px solid #ccc;">${this.getNamespaceName(contrib.ns)}</td>
                                <td style="padding: 5px; border: 1px solid #ccc;">${contrib.size || 0}</td>
                                <td style="padding: 5px; border: 1px solid #ccc; text-align: center;">
                                    <input type="checkbox" class="filtered-rollback-checkbox" data-revid="${contrib.revid}" data-parentid="${contrib.parentid}" data-title="${contrib.title}">
                                </td>
                            </tr>
                        `;
                        $tbody.append(row);
                    });
                }
                $('#filtered-edits-container').css("display", "block").slideDown();
            });
        },

        confirmAction: function(message, count) {
            return new Promise((resolve) => {
                const $modal = $(`
                    <div style="position: fixed; top: 0; left: 0; width: 100%; height: 100%;
                         background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 9999;">
                        <div style="background: #fff; padding: 20px; border-radius: 4px; max-width: 400px; width: 90%;">
                            <h4 style="margin-top:0;">Confirm Rollback</h4>
                            <p>${message}</p>
                            <p>Number of edits: ${count}</p>
                            <div style="text-align: right; margin-top: 15px;">
                                <button id="confirm-action" style="padding: 5px 10px; background-color: #007bff; color: #fff; border: none; border-radius: 3px; cursor: pointer; margin-right: 5px;">Confirm</button>
                                <button id="cancel-action" style="padding: 5px 10px; background-color: #aaa; color: #fff; border: none; border-radius: 3px; cursor: pointer;">Cancel</button>
                            </div>
                        </div>
                    </div>
                `);
                $('body').append($modal);
                $modal.find('#confirm-action').on('click', function() {
                    $modal.remove();
                    resolve(true);
                });
                $modal.find('#cancel-action').on('click', function() {
                    $modal.remove();
                    resolve(false);
                });
            });
        },

        // Nowa wersja: grupujemy edycje wg tytułu i dla każdej grupy wykrywamy ciąg kolejnych edycji.
        performRollback: function(contributions) {
            const userName = mw.config.get('wgRelevantUserName');
            const reason = $('#rollback-reason').val() === 'other' 
                ? $('#custom-reason').val() 
                : $('#rollback-reason option:selected').text();
            const summary = `Reverted edit(s) by [[User:${userName}|${userName}]]: ${reason}`;
            const api = new mw.Api();

            // Grupujemy edycje wg tytułu strony
            const groups = {};
            contributions.forEach(contrib => {
                if (!groups[contrib.title]) {
                    groups[contrib.title] = [];
                }
                groups[contrib.title].push(contrib);
            });

            const promises = [];
            // Dla każdej strony – sortuj wg daty malejąco i wykryj ciąg edycji
            for (let title in groups) {
                let group = groups[title].sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
                let latest = group[0];
                // Rozpoczynamy ciąg od najnowszej edycji
                let chain = [latest];
                let currentParent = latest.parentid;
                // Przeglądamy kolejne edycje dla tego samego tytułu
                for (let i = 1; i < group.length; i++) {
                    const contrib = group[i];
                    // Jeżeli kolejna edycja jest dokładnie poprzednikiem poprzednio znalezionego elementu, dodajemy do ciągu
                    if (parseInt(contrib.revid, 10) === parseInt(currentParent, 10)) {
                        chain.push(contrib);
                        currentParent = contrib.parentid;
                    } else {
                        break;
                    }
                }
                // Wykonaj rollback tylko, jeśli mamy przynajmniej jedną edycję (zawsze prawda)
                promises.push(api.postWithToken('csrf', {
                    action: 'edit',
                    undo: latest.revid,
                    undoafter: currentParent,
                    title: title,
                    summary: summary
                }));
            }
            return Promise.all(promises);
        },

        rollbackSelected: async function() {
            // Pobieramy zaznaczone edycje z listy kontrybucji
            const selected = $('.rollback-checkbox:checked').map(function() {
                return {
                    title: $(this).data('title'),
                    revid: $(this).data('revid'),
                    parentid: $(this).data('parentid'),
                    // Wartość timestamp może nie być dostępna w tym widoku – można opcjonalnie dodać dodatkowy pobór danych
                    timestamp: $(this).closest('li').find('.mw-contributions-timestamp').text() || new Date().toISOString()
                };
            }).get();

            if (selected.length === 0) {
                mw.notify('No edits selected.', { type: 'warn' });
                return;
            }
            const confirmed = await this.confirmAction('Are you sure you want to rollback the selected edits?', selected.length);
            if (!confirmed) return;
            try {
                await this.performRollback(selected);
                mw.notify(`Successfully rolled back ${selected.length} edits.`, { type: 'success' });
                location.reload();
            } catch (error) {
                mw.notify('Error during rollback.', { type: 'error' });
                console.error(error);
            }
        },

        rollbackSelectedFromFiltered: async function() {
            const selected = $('.filtered-rollback-checkbox:checked').map(function() {
                return {
                    title: $(this).data('title'),
                    revid: $(this).data('revid'),
                    parentid: $(this).data('parentid'),
                    timestamp: new Date().toISOString() // Jeśli API już zwraca timestamp, można go tutaj użyć
                };
            }).get();

            if (selected.length === 0) {
                mw.notify('No filtered edits selected.', { type: 'warn' });
                return;
            }
            const confirmed = await this.confirmAction('Are you sure you want to rollback the selected filtered edits?', selected.length);
            if (!confirmed) return;
            try {
                await this.performRollback(selected);
                mw.notify(`Successfully rolled back ${selected.length} filtered edits.`, { type: 'success' });
                location.reload();
            } catch (error) {
                mw.notify('Error during rollback.', { type: 'error' });
                console.error(error);
            }
        },

        rollbackAll: async function() {
            const userName = mw.config.get('wgRelevantUserName');
            const api = new mw.Api();
            try {
                // Dodajemy "timestamp" by móc grupować edycje
                const data = await api.get({
                    action: 'query',
                    list: 'usercontribs',
                    ucuser: userName,
                    uclimit: 'max',
                    ucprop: 'ids|title|parentid|timestamp'
                });
                const contributions = data.query.usercontribs;
                if (contributions.length === 0) {
                    mw.notify('No edits to rollback.', { type: 'warn' });
                    return;
                }
                const confirmed = await this.confirmAction('Are you sure you want to rollback ALL edits?', contributions.length);
                if (!confirmed) return;
                await this.performRollback(contributions);
                mw.notify(`Successfully rolled back all ${contributions.length} edits.`, { type: 'success' });
                location.reload();
            } catch (error) {
                mw.notify('Error during rollback.', { type: 'error' });
                console.error(error);
            }
        }
    };

    // Load stable MW modules and try Codex CSS-only styles if present (1.39+)
    (function loadModulesAndInit() {
        var baseDeps = [
            'mediawiki.api',
            'mediawiki.util',
            'mediawiki.notify'
        ];
        var codexDep = null;
        try {
            // Prefer the smaller set if available, else full; else skip
            if (mw.loader.getState('codex-search-styles')) {
                codexDep = 'codex-search-styles';
            } else if (mw.loader.getState('codex-styles')) {
                codexDep = 'codex-styles';
            }
        } catch (e) { /* ignore */ }

        // Also load legacy mediawiki.ui when present to keep fallback look
        var legacyUI = [];
        if (mw.loader.getState('mediawiki.ui')) legacyUI.push('mediawiki.ui');
        if (mw.loader.getState('mediawiki.ui.button')) legacyUI.push('mediawiki.ui.button');

        var deps = baseDeps.concat(legacyUI);
        if (codexDep) deps.push(codexDep);

        mw.loader.using(deps).then(function() {
            $(function() { try { MassRollback.init(); } catch (e) { console.error(e); } });
        }).catch(function() {
            // Final fallback: try to init anyway
            $(function() { try { MassRollback.init(); } catch (e) { console.error(e); } });
        });
    })();
})(jQuery, mediaWiki);