Auto Boost для Ozon

Автоматическое включение бустинга для товаров на Ozon Seller

Size

55.8 KB

Version

2.3.17

Created

Feb 19, 2026

Updated

18 days ago

1// ==UserScript==
2// @name		Auto Boost для Ozon
3// @description		Автоматическое включение бустинга для товаров на Ozon Seller
4// @version		2.3.17
5// @match		https://*.seller.ozon.ru/*
6// @icon		https://st.ozone.ru/s3/seller-ui-static/icon/favicon32.png
7// @grant		GM.getValue
8// @grant		GM.setValue
9// @grant		GM.openInTab
10// ==/UserScript==
11(function() {
12    'use strict';
13    console.log('Auto Boost для Ozon: расширение запущено (v2.3.2)');
14    // Глобальные переменные
15    let isPaused = false;
16    let currentProductIndex = 0;
17    let boostedProducts = [];
18    let nonBoostedProducts = [];
19    let currentIntervalId = null; // ID текущего интервала
20    let selectedInterval = null; // Выбранный интервал повтора (в минутах) или null для однократного запуска
21    let sessionBoostedCount = 0; // Счетчик товаров с бустингом в текущей сессии
22    let sessionNonBoostedCount = 0; // Счетчик товаров без бустинга в текущей сессии
23    let processedPages = new Set(); // Отслеживаем обработанные страницы
24
25    // Функция для ожидания
26    function wait(ms) {
27        return new Promise(resolve => setTimeout(resolve, ms));
28    }
29
30    // Функция для ожидания элемента
31    function waitForElement(selector, timeout = 10000) {
32        return new Promise((resolve, reject) => {
33            const element = document.querySelector(selector);
34            if (element) {
35                resolve(element);
36                return;
37            }
38
39            const observer = new MutationObserver(() => {
40                const element = document.querySelector(selector);
41                if (element) {
42                    resolve(element);
43                    observer.disconnect();
44                }
45            });
46
47            observer.observe(document.body, {
48                childList: true,
49                subtree: true
50            });
51
52            setTimeout(() => {
53                observer.disconnect();
54                reject(new Error(`Элемент ${selector} не найден в течение ${timeout} мс`));
55            }, timeout);
56        });
57    }
58
59    // Функция для создания стилей
60    function addStyles() {
61        const styles = `
62.auto-boost-button {
63    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
64    color: white;
65    border: none;
66    padding: 12px 24px;
67    border-radius: 8px;
68    font-size: 14px;
69    font-weight: 600;
70    cursor: pointer;
71    box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
72    transition: all 0.3s ease;
73    margin-bottom: 16px;
74}
75.auto-boost-button:hover {
76    transform: translateY(-2px);
77    box-shadow: 0 6px 16px rgba(102, 126, 234, 0.4);
78}
79.auto-boost-modal {
80    position: fixed;
81    top: 0;
82    left: 0;
83    width: 100%;
84    height: 100%;
85    background: rgba(0, 0, 0, 0.5);
86    display: flex;
87    align-items: center;
88    justify-content: center;
89    z-index: 10000;
90}
91.auto-boost-modal-content {
92    background: white;
93    border-radius: 12px;
94    padding: 24px;
95    width: 90%;
96    max-width: 800px;
97    max-height: 80vh;
98    overflow-y: auto;
99    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
100}
101.auto-boost-modal-header {
102    display: flex;
103    justify-content: space-between;
104    align-items: center;
105    margin-bottom: 20px;
106    padding-bottom: 16px;
107    border-bottom: 2px solid #f0f0f0;
108}
109.auto-boost-modal-title {
110    font-size: 24px;
111    font-weight: 700;
112    color: #333;
113}
114.auto-boost-close {
115    background: none;
116    border: none;
117    font-size: 28px;
118    cursor: pointer;
119    color: #999;
120    padding: 0;
121    width: 32px;
122    height: 32px;
123    display: flex;
124    align-items: center;
125    justify-content: center;
126    border-radius: 50%;
127    transition: all 0.2s;
128}
129.auto-boost-close:hover {
130    background: #f0f0f0;
131    color: #333;
132}
133.auto-boost-controls {
134    display: flex;
135    flex-direction: column;
136    gap: 12px;
137    margin-bottom: 20px;
138}
139.auto-boost-start-btn, .auto-boost-pause-btn, .auto-boost-stop-btn {
140    padding: 12px 24px;
141    border: none;
142    border-radius: 8px;
143    font-size: 14px;
144    font-weight: 600;
145    cursor: pointer;
146    transition: all 0.3s ease;
147    width: 100%;
148}
149.auto-boost-start-btn {
150    background: #10b981;
151    color: white;
152}
153.auto-boost-start-btn:hover {
154    background: #059669;
155}
156.auto-boost-pause-btn {
157    background: #f59e0b;
158    color: white;
159}
160.auto-boost-pause-btn:hover {
161    background: #d97706;
162}
163.auto-boost-stop-btn {
164    background: #ef4444;
165    color: white;
166}
167.auto-boost-stop-btn:hover {
168    background: #dc2626;
169}
170.auto-boost-stats {
171    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
172    color: white;
173    padding: 16px;
174    border-radius: 8px;
175    margin-bottom: 20px;
176    text-align: center;
177    font-size: 18px;
178    font-weight: 600;
179}
180.auto-boost-products-list {
181    max-height: 400px;
182    overflow-y: auto;
183    border: 1px solid #e5e7eb;
184    border-radius: 8px;
185    padding: 12px;
186}
187.auto-boost-product-item {
188    background: #f9fafb;
189    padding: 12px;
190    margin-bottom: 8px;
191    border-radius: 6px;
192    border-left: 4px solid #10b981;
193}
194.auto-boost-product-sku {
195    font-weight: 600;
196    color: #333;
197    margin-bottom: 4px;
198}
199.auto-boost-product-name {
200    color: #666;
201    font-size: 13px;
202    margin-bottom: 4px;
203}
204.auto-boost-product-link {
205    color: #667eea;
206    text-decoration: none;
207    font-size: 12px;
208}
209.auto-boost-product-link:hover {
210    text-decoration: underline;
211}
212.auto-boost-product-status {
213    display: inline-block;
214    background: #10b981;
215    color: white;
216    padding: 4px 8px;
217    border-radius: 4px;
218    font-size: 11px;
219    margin-top: 4px;
220}
221.auto-boost-counter {
222    background: white;
223    color: #667eea;
224    padding: 8px 16px;
225    border-radius: 6px;
226    font-size: 14px;
227    font-weight: 600;
228    display: inline-block;
229    margin-bottom: 12px;
230    border: 2px solid #667eea;
231}
232.auto-boost-interval-controls {
233    display: flex;
234    flex-direction: column;
235    gap: 8px;
236    margin-top: 10px;
237}
238.auto-boost-interval-label {
239    font-weight: 600;
240    color: #333;
241    margin-bottom: 4px;
242}
243.auto-boost-interval-select {
244    padding: 8px 12px;
245    border: 1px solid #d1d5db;
246    border-radius: 6px;
247    font-size: 14px;
248}
249.auto-boost-interval-info {
250    font-size: 12px;
251    color: #666;
252    margin-top: 4px;
253    text-align: center;
254}
255    `;
256        const styleElement = document.createElement('style');
257        styleElement.textContent = styles;
258        document.head.appendChild(styleElement);
259    }
260
261    // Функция для создания кнопки "Включить буст"
262    function createBoostButton() {
263        const tableWidget = document.querySelector('[data-widget="@products/product-list-table-ods"]');
264        if (!tableWidget) {
265            console.log('Таблица товаров не найдена, повторная попытка через 2 секунды');
266            setTimeout(createBoostButton, 2000);
267            return;
268        }
269        // Проверяем, не создана ли уже кнопка
270        if (document.querySelector('.auto-boost-button')) {
271            return;
272        }
273        const button = document.createElement('button');
274        button.className = 'auto-boost-button';
275        button.textContent = '🚀 Включить буст';
276        button.onclick = openModal;
277        tableWidget.parentElement.insertBefore(button, tableWidget);
278        console.log('Кнопка "Включить буст" создана');
279    }
280
281    // Функция для создания модального окна
282    async function openModal() {
283        // Загружаем сохраненные данные
284        await loadBoostedProducts();
285        await loadNonBoostedProducts();
286        
287        // Загружаем результаты последнего буста
288        const lastSessionBoosted = await GM.getValue('lastSessionBoosted', 0);
289        const lastSessionNonBoosted = await GM.getValue('lastSessionNonBoosted', 0);
290        
291        // Загружаем время последней проверки
292        const lastCheckTime = await GM.getValue('lastCheckTime', null);
293        let lastCheckText = '';
294        if (lastCheckTime) {
295            const date = new Date(lastCheckTime);
296            const time = date.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' });
297            const dateStr = date.toLocaleDateString('ru-RU');
298            lastCheckText = `<div style="text-align: center; color: #666; font-size: 13px; margin-top: 8px;">Последняя проверка: ${time} / ${dateStr}</div>`;
299        }
300        
301        // Загружаем сохраненный интервал
302        const savedInterval = await GM.getValue('selectedInterval', null);
303        if (savedInterval !== null) {
304            selectedInterval = savedInterval;
305        }
306        
307        // Проверяем, есть ли активный таймер и время следующего запуска
308        const intervalActive = await GM.getValue('intervalActive', false);
309        const nextRunTime = await GM.getValue('nextRunTime', null);
310        let nextRunText = '';
311        let showStopButton = false;
312        
313        if (intervalActive && nextRunTime && selectedInterval && selectedInterval > 0) {
314            const now = Date.now();
315            if (nextRunTime > now) {
316                const nextRunDate = new Date(nextRunTime);
317                const nextRunTimeStr = nextRunDate.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' });
318                nextRunText = `<div style="text-align: center; color: #667eea; font-size: 14px; font-weight: 600; margin-top: 8px; padding: 8px; background: #f0f4ff; border-radius: 6px;">⏰ Следующий запуск в ${nextRunTimeStr} (через ${Math.round((nextRunTime - now) / 1000 / 60)} минут)</div>`;
319                showStopButton = true;
320            }
321        }
322        
323        const modal = document.createElement('div');
324        modal.className = 'auto-boost-modal';
325        modal.innerHTML = `
326        <div class="auto-boost-modal-content">
327            <div class="auto-boost-modal-header">
328                <div class="auto-boost-modal-title">🚀 Auto Boost для Ozon</div>
329                <button class="auto-boost-close">×</button>
330            </div>
331            <div style="display: flex; gap: 12px; margin-bottom: 12px;">
332                <div class="auto-boost-counter" style="border-color: #10b981; color: #10b981;">
333                    ✓ С бустингом: <span id="boost-counter">${lastSessionBoosted}</span>
334                </div>
335                <div class="auto-boost-counter" style="border-color: #ef4444; color: #ef4444;">
336                    ✗ Без бустинга: <span id="non-boost-counter">${lastSessionNonBoosted}</span>
337                </div>
338            </div>
339            <div id="last-check-time">${lastCheckText}</div>
340            <div id="next-run-time">${nextRunText}</div>
341            <div class="auto-boost-controls">
342                <button class="auto-boost-start-btn" id="start-boost-btn">▶ Старт</button>
343                <button class="auto-boost-pause-btn" id="pause-boost-btn" style="display: none;">⏸ Пауза</button>
344                <button class="auto-boost-stop-btn" id="stop-boost-btn" style="display: ${showStopButton ? 'block' : 'none'};">⏹ Остановить</button>
345            </div>
346            <div class="auto-boost-interval-controls">
347                <div class="auto-boost-interval-label">Интервал повтора:</div>
348                <select class="auto-boost-interval-select" id="interval-select">
349                    <option value="0" ${selectedInterval === null || selectedInterval === 0 ? 'selected' : ''}>Без повтора (однократно)</option>
350                    <option value="5" ${selectedInterval === 5 ? 'selected' : ''}>Каждые 5 минут</option>
351                    <option value="15" ${selectedInterval === 15 ? 'selected' : ''}>Каждые 15 минут</option>
352                    <option value="30" ${selectedInterval === 30 ? 'selected' : ''}>Каждые 30 минут</option>
353                    <option value="60" ${selectedInterval === 60 ? 'selected' : ''}>Каждый час (60 мин)</option>
354                    <option value="120" ${selectedInterval === 120 ? 'selected' : ''}>Каждые 2 часа (120 мин)</option>
355                </select>
356                <div class="auto-boost-interval-info" id="interval-info">${selectedInterval && selectedInterval > 0 ? `Выбрано: Каждые ${selectedInterval} минут${selectedInterval === 60 ? ' (1 час)' : selectedInterval === 120 ? ' (2 часа)' : ''}` : 'Выбрано: Без повтора'}</div>
357            </div>
358            <div class="auto-boost-stats" id="boost-stats">
359                Нажмите "Старт" для начала обработки товаров
360            </div>
361            <div class="auto-boost-products-list" id="products-list">
362                ${renderProductsList()}
363            </div>
364        </div>
365    `;
366        document.body.appendChild(modal);
367
368        // Обработчики событий
369        modal.querySelector('.auto-boost-close').onclick = () => {
370            modal.remove();
371        };
372        modal.onclick = (e) => {
373            if (e.target === modal) {
374                modal.remove();
375            }
376        };
377        
378        const startBtn = modal.querySelector('#start-boost-btn');
379        const pauseBtn = modal.querySelector('#pause-boost-btn');
380        const stopBtn = modal.querySelector('#stop-boost-btn');
381        const intervalSelect = modal.querySelector('#interval-select');
382        const intervalInfo = modal.querySelector('#interval-info');
383
384        // Обработчик выбора интервала
385        intervalSelect.onchange = async (e) => {
386            const value = parseInt(e.target.value);
387            if (value === 0) {
388                selectedInterval = null;
389                intervalInfo.textContent = 'Выбрано: Без повтора';
390            } else {
391                selectedInterval = value;
392                intervalInfo.textContent = `Выбрано: Каждые ${value} минут${value === 60 ? ' (1 час)' : value === 120 ? ' (2 часа)' : ''}`;
393            }
394            await GM.setValue('selectedInterval', selectedInterval);
395            console.log(`Выбран интервал: ${selectedInterval} минут`);
396        };
397
398        startBtn.onclick = async () => {
399            // Устанавливаем начальное значение интервала, если ещё не установлено
400            if (selectedInterval === undefined) {
401                const value = parseInt(intervalSelect.value);
402                selectedInterval = value === 0 ? null : value;
403                await GM.setValue('selectedInterval', selectedInterval);
404            }
405            
406            // Снимаем паузу
407            isPaused = false;
408            localStorage.setItem('autoBoostPaused', 'false');
409            
410            // Очищаем списки товаров перед новым запуском
411            boostedProducts = [];
412            nonBoostedProducts = [];
413            await GM.setValue('boostedProducts', JSON.stringify(boostedProducts));
414            await GM.setValue('nonBoostedProducts', JSON.stringify(nonBoostedProducts));
415            console.log('Списки товаров очищены перед новым запуском');
416            
417            // Сбрасываем счетчики сессии
418            sessionBoostedCount = 0;
419            sessionNonBoostedCount = 0;
420            
421            // Обновляем счетчики в UI сразу
422            updateCounter();
423            
424            // Обновляем список товаров в UI
425            updateProductsList();
426            
427            // Очищаем список обработанных страниц
428            processedPages.clear();
429            console.log('Список обработанных страниц очищен');
430            
431            startBtn.style.display = 'none';
432            pauseBtn.style.display = 'block';
433            stopBtn.style.display = 'block';
434            
435            await startProcessing();
436        };
437        
438        pauseBtn.onclick = () => {
439            isPaused = true;
440            localStorage.setItem('autoBoostPaused', 'true');
441            pauseBtn.textContent = '▶ Продолжить';
442            pauseBtn.className = 'auto-boost-start-btn';
443            pauseBtn.onclick = () => {
444                isPaused = false;
445                localStorage.setItem('autoBoostPaused', 'false');
446                pauseBtn.textContent = '⏸ Пауза';
447                pauseBtn.className = 'auto-boost-pause-btn';
448                pauseBtn.onclick = () => {
449                    isPaused = true;
450                    pauseBtn.textContent = '▶ Продолжить';
451                    pauseBtn.className = 'auto-boost-start-btn';
452                };
453                startProcessing();
454            };
455        };
456        
457        stopBtn.onclick = async () => {
458            isPaused = true;
459            localStorage.setItem('autoBoostPaused', 'true');
460            
461            // Полностью останавливаем таймер и очищаем все данные
462            if (currentIntervalId) {
463                clearInterval(currentIntervalId);
464                currentIntervalId = null;
465            }
466            
467            // Очищаем все сохраненные данные о таймере
468            await GM.setValue('intervalActive', false);
469            await GM.setValue('nextRunTime', null);
470            console.log('Таймер полностью остановлен и очищен.');
471            
472            startBtn.style.display = 'block';
473            pauseBtn.style.display = 'none';
474            stopBtn.style.display = 'none';
475            pauseBtn.textContent = '⏹ Обработка остановлена. Все запланированные бустинги отменены.';
476            pauseBtn.className = 'auto-boost-stop-btn';
477            
478            // Обновляем отображение времени следующего запуска
479            const nextRunTimeElement = document.querySelector('#next-run-time');
480            if (nextRunTimeElement) {
481                nextRunTimeElement.innerHTML = '';
482            }
483            
484            updateStats('⏹ Обработка остановлена. Все запланированные бустинги отменены.');
485        };
486        
487        console.log('Модальное окно открыто');
488    }
489
490    // Функция для отображения списка товаров
491    function renderProductsList() {
492        if (boostedProducts.length === 0) {
493            return '<div style="text-align: center; color: #999; padding: 20px;">Пока нет товаров с включенным бустингом</div>';
494        }
495        return boostedProducts.map(product => `
496        <div class="auto-boost-product-item">
497            <div class="auto-boost-product-sku">SKU: ${product.sku}</div>
498            <div class="auto-boost-product-name">${product.name}</div>
499            <a href="${product.url}" target="_blank" class="auto-boost-product-link">Открыть товар</a>
500            <div class="auto-boost-product-status">✓ Включен бустинг</div>
501        </div>
502    `).join('');
503    }
504
505    // Функция для начала обработки товаров
506    async function startProcessing() {
507        console.log('Начинаем обработку товаров');
508        updateStats('Сбор товаров со страницы...');
509        
510        // Получаем общее количество товаров из интерфейса
511        let totalProducts = 0;
512        // Ищем кнопку с текстом "Все" и берем число из нее
513        const allTabButton = document.querySelector('button[data-active="true"][data-widget="@products/tabs/products-list-filter-tab"]');
514        if (allTabButton) {
515            const counterElement = allTabButton.querySelector('.rc8110-a0');
516            if (counterElement) {
517                totalProducts = parseInt(counterElement.textContent.trim());
518                console.log(`Общее количество товаров: ${totalProducts}`);
519            }
520        }
521        
522        if (totalProducts === 0) {
523            console.log('Не удалось определить общее количество товаров');
524        }
525        
526        // Собираем все строки таблицы
527        const rows = document.querySelectorAll('[data-widget="@products/product-list-table-ods"] tbody tr');
528        console.log(`Найдено строк в таблице: ${rows.length}`);
529        
530        if (rows.length === 0) {
531            updateStats('Товары не найдены на странице');
532            return;
533        }
534        
535        // Собираем данные всех товаров ДО начала обработки
536        const productsData = [];
537        for (let i = 0; i < rows.length; i++) {
538            const row = rows[i];
539            // Прокручиваем к строке чтобы данные загрузились
540            row.scrollIntoView({ block: 'center', behavior: 'auto' });
541            await wait(100);
542            
543            // Получаем SKU - ищем разными способами
544            let sku = 'N/A';
545            
546            // Способ 1: по классу index_sku
547            const skuElement = row.querySelector('[class*="index_sku"]');
548            if (skuElement) {
549                sku = skuElement.textContent.trim().replace('SKU', '').trim();
550            }
551            
552            // Способ 2: если не нашли, ищем длинное число (обычно SKU - это 8+ цифр)
553            if (sku === 'N/A') {
554                const allDivs = row.querySelectorAll('div');
555                for (const div of allDivs) {
556                    const text = div.textContent.trim();
557                    // SKU обычно содержит только цифры и длиннее 8 символов
558                    if (/^\d{8,}$/.test(text)) {
559                        sku = text;
560                        console.log(`SKU найден через поиск числа: ${sku}`);
561                        break;
562                    }
563                }
564            }
565            
566            // Получаем название товара
567            const nameLink = row.querySelector('a[href*="ozon.ru/product"]');
568            const name = nameLink ? nameLink.textContent.trim() : 'Без названия';
569            
570            console.log(`Товар ${i + 1}: SKU=${sku}, Название=${name.substring(0, 50)}`);
571            
572            productsData.push({
573                sku: sku,
574                name: name,
575                rowIndex: i
576            });
577        }
578        
579        console.log(`Собрано данных о товарах: ${productsData.length}`);
580        updateStats(`Найдено товаров: ${productsData.length}. Начинаем обработку...`);
581
582        // Обрабатываем товары по очереди
583        for (let i = currentProductIndex; i < productsData.length; i++) {
584            if (isPaused) {
585                console.log('Обработка приостановлена.');
586                return;
587            }
588            
589            currentProductIndex = i;
590            const productData = productsData[i];
591            
592            // Вычисляем глобальный номер товара
593            const globalProductNumber = sessionBoostedCount + sessionNonBoostedCount + 1;
594            
595            updateStats(`Обработка товара ${globalProductNumber} из ${totalProducts > 0 ? totalProducts : '?'}: ${productData.name}`);
596            console.log(`Обрабатываем товар ${globalProductNumber} из ${totalProducts > 0 ? totalProducts : '?'}`);
597            
598            await processProduct(productData);
599            await wait(500);
600            
601            // ВАЖНО: Проверяем, достигли ли мы общего количества товаров
602            const processedTotal = sessionBoostedCount + sessionNonBoostedCount;
603            if (totalProducts > 0 && processedTotal >= totalProducts) {
604                console.log(`Достигнуто общее количество товаров: ${processedTotal} из ${totalProducts}. Останавливаем обработку.`);
605                updateStats(`Обработка завершена! Обработано ${processedTotal} товаров из ${totalProducts}.`);
606                
607                // Переходим к завершению обработки
608                await finishProcessing();
609                return;
610            }
611        }
612        
613        updateStats(`Страница обработана! Обработано товаров: ${productsData.length}. Проверяем следующую страницу...`);
614        currentProductIndex = 0;
615
616        // Проверяем, достигли ли мы общего количества товаров
617        const processedTotal = sessionBoostedCount + sessionNonBoostedCount;
618        if (totalProducts > 0 && processedTotal >= totalProducts) {
619            console.log(`Достигнуто общее количество товаров: ${processedTotal} из ${totalProducts}. Останавливаем обработку.`);
620            await finishProcessing();
621            return;
622        }
623
624        // Проверяем, есть ли следующая страница
625        const paginationContainer = document.querySelector('#pagination');
626        if (paginationContainer) {
627            // Ищем все кнопки с классом t0c110-a1 и берем последнюю (это кнопка "Вперед")
628            const paginationButtons = paginationContainer.querySelectorAll('button.t0c110-a1.table-500');
629            const nextPageButton = paginationButtons.length > 0 ? paginationButtons[paginationButtons.length - 1] : null;
630            
631            if (nextPageButton && !nextPageButton.disabled) {
632                console.log('Найдена следующая страница, переходим...');
633                updateStats('Переход на следующую страницу...');
634                
635                // Кликаем на следующую страницу
636                nextPageButton.click();
637                
638                // Ждем загрузки новой страницы
639                await wait(3000);
640                
641                // Продолжаем обработку на новой странице
642                if (!isPaused) {
643                    await startProcessing();
644                }
645            } else {
646                console.log('Следующая страница не найдена или кнопка disabled, обработка завершена');
647                await finishProcessing();
648            }
649        } else {
650            console.log('Пагинация не найдена, обработка завершена');
651            await finishProcessing();
652        }
653    }
654    
655    // Функция для завершения обработки
656    async function finishProcessing() {
657        // Сохраняем время завершения
658        await GM.setValue('lastCheckTime', Date.now());
659        updateLastCheckTime();
660        
661        // Сохраняем результаты последнего буста
662        await GM.setValue('lastSessionBoosted', sessionBoostedCount);
663        await GM.setValue('lastSessionNonBoosted', sessionNonBoostedCount);
664        
665        // Обновляем счетчики с результатами последнего буста
666        updateCounter();
667        
668        // Перезагружаем список товаров из хранилища
669        await reloadProductsList();
670        
671        // ВАЖНО: Останавливаем процесс независимо от интервала
672        isPaused = true;
673        localStorage.setItem('autoBoostPaused', 'true');
674        
675        if (selectedInterval === null || selectedInterval === 0) {
676            updateStats(`Обработка завершена! С бустингом: ${sessionBoostedCount}, Без бустинга: ${sessionNonBoostedCount}`);
677            
678            // Обновляем кнопки
679            const startBtn = document.querySelector('#start-boost-btn');
680            const pauseBtn = document.querySelector('#pause-boost-btn');
681            const stopBtn = document.querySelector('#stop-boost-btn');
682            
683            if (startBtn) startBtn.style.display = 'block';
684            if (pauseBtn) pauseBtn.style.display = 'none';
685            if (stopBtn) stopBtn.style.display = 'none';
686        } else {
687            // Вычисляем время следующего запуска
688            const nextRunTime = new Date(Date.now() + selectedInterval * 60 * 1000);
689            const nextRunTimeStr = nextRunTime.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' });
690            
691            // ВАЖНО: Сохраняем время следующего запуска и активируем таймер
692            await GM.setValue('nextRunTime', nextRunTime.getTime());
693            await GM.setValue('intervalActive', true);
694            
695            updateStats(`Обработка завершена! С бустингом: ${sessionBoostedCount}, Без бустинга: ${sessionNonBoostedCount}. Следующий запуск в ${nextRunTimeStr}`);
696            
697            // ВАЖНО: Запускаем таймер проверки прямо сейчас
698            startIntervalChecker();
699            
700            // Обновляем отображение времени следующего запуска
701            const nextRunTimeElement = document.querySelector('#next-run-time');
702            if (nextRunTimeElement) {
703                nextRunTimeElement.innerHTML = `<div style="text-align: center; color: #667eea; font-size: 14px; font-weight: 600; margin-top: 8px; padding: 8px; background: #f0f4ff; border-radius: 6px;">⏰ Следующий запуск в ${nextRunTimeStr}</div>`;
704            }
705            
706            // Обновляем кнопки
707            const startBtn = document.querySelector('#start-boost-btn');
708            const pauseBtn = document.querySelector('#pause-boost-btn');
709            const stopBtn = document.querySelector('#stop-boost-btn');
710            
711            if (startBtn) startBtn.style.display = 'block';
712            if (pauseBtn) pauseBtn.style.display = 'none';
713            if (stopBtn) stopBtn.style.display = 'block';
714        }
715    }
716
717    // Функция для обработки одного товара
718    async function processProduct(productData) {
719        try {
720            // Проверяем паузу перед началом обработки
721            const pausedState = localStorage.getItem('autoBoostPaused');
722            if (pausedState === 'true' || isPaused) {
723                console.log('Процесс на паузе, пропускаем обработку товара');
724                return;
725            }
726            
727            console.log(`Открываем товар: ${productData.name} (SKU: ${productData.sku})`);
728            
729            // Находим строку товара заново (DOM мог измениться)
730            const rows = document.querySelectorAll('[data-widget="@products/product-list-table-ods"] tbody tr');
731            let targetRow = null;
732            
733            // Ищем строку по SKU
734            for (const row of rows) {
735                // Пробуем найти SKU разными способами
736                let sku = '';
737                
738                // Способ 1: по классу index_sku
739                const skuElement = row.querySelector('[class*="index_sku"]');
740                if (skuElement) {
741                    sku = skuElement.textContent.trim().replace('SKU', '').trim();
742                }
743                
744                // Способ 2: если не нашли, ищем длинное число (обычно SKU - это 10+ цифр)
745                if (!sku || sku === 'N/A') {
746                    const allDivs = row.querySelectorAll('div');
747                    for (const div of allDivs) {
748                        const text = div.textContent.trim();
749                        // SKU обычно содержит только цифры и длиннее 8 символов
750                        if (/^\d{8,}$/.test(text)) {
751                            sku = text;
752                            break;
753                        }
754                    }
755                }
756                
757                if (sku === productData.sku) {
758                    targetRow = row;
759                    break;
760                }
761            }
762            
763            if (!targetRow) {
764                console.log(`Строка товара ${productData.sku} не найдена в DOM`);
765                return;
766            }
767            
768            // Прокручиваем к строке
769            targetRow.scrollIntoView({ block: 'center', behavior: 'auto' });
770            await wait(300);
771            
772            // Ищем кнопку с ценой - это первая кнопка в строке, которая содержит символ ₽
773            const allButtons = targetRow.querySelectorAll('button');
774            let priceButton = null;
775            
776            for (const btn of allButtons) {
777                if (btn.textContent.includes('₽')) {
778                    priceButton = btn;
779                    break;
780                }
781            }
782            
783            if (!priceButton) {
784                console.log(`Кнопка с ценой не найдена для товара ${productData.sku}`);
785                return;
786            }
787            
788            // Генерируем уникальный ID для этой проверки
789            const checkId = Date.now() + Math.random().toString(36).substr(2, 9);
790            
791            // Отправляем сообщение для проверки слайдера с уникальным ID
792            const checkData = {
793                action: 'checkBoost',
794                productSku: productData.sku,
795                productName: productData.name,
796                productUrl: '',
797                timestamp: Date.now(),
798                checkId: checkId
799            };
800            localStorage.setItem('autoBoostCheck', JSON.stringify(checkData));
801            
802            // Устанавливаем флаг, что вкладка с этим ID ожидается
803            localStorage.setItem('autoBoostExpected_' + checkId, 'true');
804            
805            // Кликаем на кнопку с ценой (откроется в новой вкладке)
806            console.log(`Кликаем на кнопку с ценой для товара ${productData.sku}`);
807            priceButton.click();
808            console.log('Клик выполнен, ожидаем результата проверки...');
809            
810            // Ждем результата проверки
811            let waitTime = 0;
812            const maxWaitTime = 5000; // 5 секунд
813            
814            while (waitTime < maxWaitTime) {
815                await wait(200);
816                waitTime += 200;
817                
818                const result = localStorage.getItem('autoBoostResult');
819                if (result) {
820                    const resultData = JSON.parse(result);
821                    if (resultData.timestamp === checkData.timestamp && resultData.checkId === checkId) {
822                        console.log('Получен результат проверки:', resultData);
823                        
824                        if (resultData.boosted) {
825                            await addBoostedProduct({
826                                sku: productData.sku,
827                                name: productData.name,
828                                url: resultData.url
829                            });
830                            sessionBoostedCount++;
831                            console.log(`Товар добавлен в список с бустом. Всего с бустом: ${sessionBoostedCount}`);
832                        } else {
833                            await addNonBoostedProduct({
834                                sku: productData.sku,
835                                name: productData.name,
836                                url: resultData.url
837                            });
838                            sessionNonBoostedCount++;
839                            console.log(`Товар добавлен в список без буста. Всего без буста: ${sessionNonBoostedCount}`);
840                        }
841                        
842                        // Обновляем счетчики в UI
843                        updateCounter();
844                        
845                        localStorage.removeItem('autoBoostResult');
846                        break;
847                    }
848                }
849                
850                // Проверяем, не остановлен ли основной процесс
851                if (isPaused) {
852                    console.log('Основной процесс остановлен, прерываем ожидание результата проверки.');
853                    break;
854                }
855            }
856            
857            // Удаляем флаг ожидания после завершения ожидания
858            localStorage.removeItem('autoBoostExpected_' + checkId);
859            console.log(`Товар обработан: ${productData.name}`);
860            
861        } catch (error) {
862            console.error('Ошибка при обработке товара:', error);
863        }
864    }
865
866    // Функция для добавления товара в список с бустингом
867    async function addBoostedProduct(product) {
868        const exists = boostedProducts.some(p => p.sku === product.sku);
869        if (exists) {
870            console.log(`Товар ${product.sku} уже в списке`);
871            return;
872        }
873        
874        boostedProducts.push({
875            sku: product.sku,
876            name: product.name,
877            url: product.url,
878            timestamp: Date.now()
879        });
880        
881        await GM.setValue('boostedProducts', JSON.stringify(boostedProducts));
882        updateProductsList();
883        console.log(`Товар добавлен в список с бустингом: ${product.name}`);
884    }
885
886    // Функция для добавления товара в список без бустинга
887    async function addNonBoostedProduct(product) {
888        const exists = nonBoostedProducts.some(p => p.sku === product.sku);
889        if (exists) {
890            console.log(`Товар ${product.sku} уже в списке без бустинга`);
891            return;
892        }
893        
894        nonBoostedProducts.push({
895            sku: product.sku,
896            name: product.name,
897            url: product.url,
898            timestamp: Date.now()
899        });
900        
901        await GM.setValue('nonBoostedProducts', JSON.stringify(nonBoostedProducts));
902        console.log(`Товар добавлен в список без бустинга: ${product.name}`);
903    }
904
905    // Функция для загрузки сохраненных товаров без бустинга
906    async function loadNonBoostedProducts() {
907        try {
908            const saved = await GM.getValue('nonBoostedProducts', '[]');
909            nonBoostedProducts = JSON.parse(saved);
910            console.log(`Загружено товаров без бустинга: ${nonBoostedProducts.length}`);
911        } catch (error) {
912            console.error('Ошибка при загрузке данных без бустинга:', error);
913            nonBoostedProducts = [];
914        }
915    }
916
917    // Функция для загрузки сохраненных товаров
918    async function loadBoostedProducts() {
919        try {
920            const saved = await GM.getValue('boostedProducts', '[]');
921            boostedProducts = JSON.parse(saved);
922            console.log(`Загружено товаров с бустингом: ${boostedProducts.length}`);
923        } catch (error) {
924            console.error('Ошибка при загрузке данных:', error);
925            boostedProducts = [];
926        }
927    }
928
929    // Функция для обновления списка товаров в UI
930    function updateProductsList() {
931        const listElement = document.querySelector('#products-list');
932        if (listElement) {
933            listElement.innerHTML = renderProductsList();
934        }
935    }
936    
937    // Функция для перезагрузки списка товаров из хранилища
938    async function reloadProductsList() {
939        await loadBoostedProducts();
940        updateProductsList();
941    }
942
943    // Функция для обновления счетчика
944    function updateCounter() {
945        const counterElement = document.querySelector('#boost-counter');
946        if (counterElement) {
947            counterElement.textContent = sessionBoostedCount;
948        }
949        
950        const nonBoostCounterElement = document.querySelector('#non-boost-counter');
951        if (nonBoostCounterElement) {
952            nonBoostCounterElement.textContent = sessionNonBoostedCount;
953        }
954    }
955
956    // Функция для обновления статуса
957    function updateStats(message) {
958        const statsElement = document.querySelector('#boost-stats');
959        if (statsElement) {
960            statsElement.textContent = message;
961        }
962        console.log('Статус:', message);
963    }
964
965    // Функция для обновления времени последней проверки
966    async function updateLastCheckTime() {
967        const lastCheckTime = await GM.getValue('lastCheckTime', null);
968        const lastCheckElement = document.querySelector('#last-check-time');
969        
970        if (lastCheckElement && lastCheckTime) {
971            const date = new Date(lastCheckTime);
972            const time = date.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' });
973            const dateStr = date.toLocaleDateString('ru-RU');
974            lastCheckElement.innerHTML = `<div style="text-align: center; color: #666; font-size: 13px; margin-top: 8px;">Последняя проверка: ${time} / ${dateStr}</div>`;
975        }
976    }
977
978    // Обработчик для страницы товара (проверка слайдера)
979    async function handleProductPage() {
980        // Проверяем, не на паузе ли процесс
981        const pausedState = localStorage.getItem('autoBoostPaused');
982        if (pausedState === 'true') {
983            console.log('Процесс на паузе, вкладка не будет автоматически закрыта');
984            return;
985        }
986        
987        // Проверяем, есть ли ожидаемая проверка (по уникальному ID)
988        for (let i = 0; i < localStorage.length; i++) {
989            const key = localStorage.key(i);
990            if (key && key.startsWith('autoBoostExpected_') && localStorage.getItem(key) === 'true') {
991                const checkId = key.replace('autoBoostExpected_', '');
992                const checkDataRaw = localStorage.getItem('autoBoostCheck');
993                
994                if (checkDataRaw) {
995                    const checkData = JSON.parse(checkDataRaw);
996                    
997                    // Проверяем, совпадает ли checkId в autoBoostCheck с найденным
998                    if (checkData.checkId === checkId) {
999                        console.log('Найдена ожидаемая проверка с ID:', checkId);
1000                        await performCheckAndClose(checkData);
1001                        return;
1002                    }
1003                }
1004            }
1005        }
1006        
1007        // Если не нашли ожидаемой проверки, это вкладка, открытая вручную
1008        console.log('Вкладка открыта вручную, проверка не требуется.');
1009    }
1010
1011    // Выносим логику проверки в отдельную функцию
1012    async function performCheckAndClose(data) {
1013        if (Date.now() - data.timestamp > 60000) {
1014            console.log('Таймаут проверки бустинга истек.');
1015            window.close();
1016            return;
1017        }
1018
1019        console.log('Обнаружен запрос на проверку бустинга для ID:', data.checkId);
1020        
1021        try {
1022            // Ждем загрузки страницы
1023            await wait(2000);
1024            
1025            // Ищем элементы для сравнения
1026            const boostElement = document.querySelector('.nd-bl5');
1027            const baseElement = document.querySelector('.nd-lb5.nd-bl3');
1028            
1029            if (!boostElement || !baseElement) {
1030                console.log('Элементы для проверки буста не найдены');
1031                const result = {
1032                    boosted: false,
1033                    timestamp: data.timestamp,
1034                    checkId: data.checkId,
1035                    url: window.location.href
1036                };
1037                localStorage.setItem('autoBoostResult', JSON.stringify(result));
1038                await wait(300);
1039                window.close();
1040                return;
1041            }
1042            
1043            // Получаем ширину элементов из style
1044            const boostStyle = boostElement.getAttribute('style') || '';
1045            const baseStyle = baseElement.getAttribute('style') || '';
1046            
1047            const boostWidthMatch = boostStyle.match(/width:\s*(\d+(?:\.\d+)?)%/);
1048            const baseWidthMatch = baseStyle.match(/width:\s*(\d+(?:\.\d+)?)%/);
1049            
1050            const boostWidth = boostWidthMatch ? parseFloat(boostWidthMatch[1]) : 0;
1051            const baseWidth = baseWidthMatch ? parseFloat(baseWidthMatch[1]) : 0;
1052            
1053            console.log(`Ширина буста: ${boostWidth}%, Ширина базы: ${baseWidth}%`);
1054            
1055            // Если ширина буста >= ширины базы, значит буст включен
1056            if (boostWidth >= baseWidth && boostWidth > 0) {
1057                console.log('Бустинг включен, ищем кнопку "Сохранить"');
1058                const saveButton = Array.from(document.querySelectorAll('button')).find(btn =>
1059                    btn.textContent.includes('Сохранить') || btn.textContent.includes('сохранить')
1060                );
1061                
1062                if (saveButton) {
1063                    console.log('Нажимаем кнопку "Сохранить"');
1064                    saveButton.click();
1065                    await wait(1500);
1066                } else {
1067                    console.log('Кнопка "Сохранить" не найдена.');
1068                }
1069
1070                const result = {
1071                    boosted: true,
1072                    timestamp: data.timestamp,
1073                    checkId: data.checkId,
1074                    url: window.location.href
1075                };
1076                localStorage.setItem('autoBoostResult', JSON.stringify(result));
1077                await wait(300);
1078                window.close();
1079            } else {
1080                console.log('Бустинг не включен');
1081                const result = {
1082                    boosted: false,
1083                    timestamp: data.timestamp,
1084                    checkId: data.checkId,
1085                    url: window.location.href
1086                };
1087                localStorage.setItem('autoBoostResult', JSON.stringify(result));
1088                await wait(300);
1089                window.close();
1090            }
1091
1092        } catch (error) {
1093            console.error('Ошибка при проверке бустинга:', error.message);
1094            const result = {
1095                boosted: false,
1096                timestamp: data.timestamp,
1097                checkId: data.checkId,
1098                url: window.location.href
1099            };
1100            localStorage.setItem('autoBoostResult', JSON.stringify(result));
1101            await wait(300);
1102            window.close();
1103        }
1104    }
1105
1106    // Функция для проверки и запуска таймера при загрузке страницы
1107    async function checkAndRestoreInterval() {
1108        const intervalActive = await GM.getValue('intervalActive', false);
1109        const savedInterval = await GM.getValue('selectedInterval', null);
1110        const nextRunTime = await GM.getValue('nextRunTime', null);
1111        
1112        console.log('Проверка сохраненного состояния:', { intervalActive, savedInterval, nextRunTime });
1113        
1114        if (intervalActive && savedInterval && savedInterval > 0) {
1115            selectedInterval = savedInterval;
1116            
1117            if (nextRunTime) {
1118                const now = Date.now();
1119                const timeUntilNextRun = nextRunTime - now;
1120                
1121                if (timeUntilNextRun > 0) {
1122                    const nextRunDate = new Date(nextRunTime);
1123                    const nextRunTimeStr = nextRunDate.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' });
1124                    console.log(`Восстановление таймера. Следующий запуск в ${nextRunTimeStr} (через ${Math.round(timeUntilNextRun / 1000 / 60)} минут)`);
1125                    
1126                    // Запускаем таймер проверки
1127                    startIntervalChecker();
1128                    
1129                    console.log(`Таймер успешно восстановлен. Интервал: ${selectedInterval} минут`);
1130                } else {
1131                    // Время уже прошло, запускаем сразу
1132                    console.log('Время следующего запуска уже прошло, запускаем процесс сейчас');
1133                    
1134                    // Вычисляем новое время следующего запуска
1135                    const newNextRunTime = new Date(Date.now() + selectedInterval * 60 * 1000);
1136                    await GM.setValue('nextRunTime', newNextRunTime.getTime());
1137                    
1138                    sessionBoostedCount = 0;
1139                    sessionNonBoostedCount = 0;
1140                    isPaused = false;
1141                    localStorage.setItem('autoBoostPaused', 'false');
1142                    
1143                    // Очищаем списки товаров перед новым запуском
1144                    boostedProducts = [];
1145                    nonBoostedProducts = [];
1146                    await GM.setValue('boostedProducts', JSON.stringify(boostedProducts));
1147                    await GM.setValue('nonBoostedProducts', JSON.stringify(nonBoostedProducts));
1148                    console.log('Списки товаров очищены перед автоматическим запуском');
1149                    
1150                    await startProcessing();
1151                }
1152            } else {
1153                // Если nextRunTime не установлено, значит это первый запуск после выбора интервала
1154                console.log('Таймер активен, но время следующего запуска не установлено');
1155            }
1156        } else {
1157            console.log('Нет активного таймера для восстановления');
1158        }
1159    }
1160
1161    // Функция для запуска таймера проверки
1162    function startIntervalChecker() {
1163        // Очищаем предыдущий таймер, если он был
1164        if (currentIntervalId) {
1165            clearInterval(currentIntervalId);
1166            console.log('Предыдущий таймер очищен');
1167        }
1168        
1169        // Проверяем каждые 30 секунд, не пора ли запускать
1170        currentIntervalId = setInterval(async () => {
1171            const currentNextRunTime = await GM.getValue('nextRunTime', null);
1172            const currentIntervalActive = await GM.getValue('intervalActive', false);
1173            
1174            if (!currentIntervalActive) {
1175                console.log('Таймер был остановлен, прекращаем проверку');
1176                clearInterval(currentIntervalId);
1177                currentIntervalId = null;
1178                return;
1179            }
1180            
1181            if (currentNextRunTime && Date.now() >= currentNextRunTime) {
1182                console.log('Время запуска наступило! Запускаем процесс');
1183                clearInterval(currentIntervalId);
1184                currentIntervalId = null;
1185                
1186                // Вычисляем новое время следующего запуска
1187                const newNextRunTime = new Date(Date.now() + selectedInterval * 60 * 1000);
1188                await GM.setValue('nextRunTime', newNextRunTime.getTime());
1189                
1190                // Сбрасываем счетчики и снимаем паузу
1191                sessionBoostedCount = 0;
1192                sessionNonBoostedCount = 0;
1193                isPaused = false;
1194                localStorage.setItem('autoBoostPaused', 'false');
1195                
1196                // Очищаем списки товаров перед новым запуском
1197                boostedProducts = [];
1198                nonBoostedProducts = [];
1199                await GM.setValue('boostedProducts', JSON.stringify(boostedProducts));
1200                await GM.setValue('nonBoostedProducts', JSON.stringify(nonBoostedProducts));
1201                console.log('Списки товаров очищены перед автоматическим запуском');
1202                
1203                await startProcessing();
1204            }
1205        }, 30000); // Проверяем каждые 30 секунд
1206        
1207        console.log('Таймер проверки запущен (проверка каждые 30 секунд)');
1208    }
1209
1210    // Инициализация
1211    async function init() {
1212        console.log('Инициализация Auto Boost для Ozon (v2.3.2)');
1213        addStyles();
1214        
1215        const currentUrl = window.location.href;
1216        
1217        if (currentUrl.includes('/app/products')) {
1218            console.log('Обнаружена страница с таблицей товаров');
1219            
1220            // Проверяем и восстанавливаем таймер
1221            await checkAndRestoreInterval();
1222            
1223            await wait(2000);
1224            createBoostButton();
1225            
1226            const observer = new MutationObserver(() => {
1227                if (!document.querySelector('.auto-boost-button')) {
1228                    createBoostButton();
1229                }
1230            });
1231            
1232            observer.observe(document.body, {
1233                childList: true,
1234                subtree: true
1235            });
1236        } else if (currentUrl.includes('/app/prices/manager/')) {
1237            console.log('Обнаружена страница товара');
1238            handleProductPage();
1239        }
1240    }
1241
1242    if (document.readyState === 'loading') {
1243        document.addEventListener('DOMContentLoaded', init);
1244    } else {
1245        init();
1246    }
1247})();