Auto Boost для Ozon

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

Size

48.2 KB

Version

2.3.4

Created

Dec 29, 2025

Updated

10 days ago

1// ==UserScript==
2// @name		Auto Boost для Ozon
3// @description		Автоматическое включение бустинга для товаров на Ozon Seller
4// @version		2.3.4
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 isProcessing = false; // Добавляем флаг процесса
17    let currentProductIndex = 0;
18    let boostedProducts = [];
19    let nonBoostedProducts = [];
20    let currentIntervalId = null; // ID текущего интервала
21    let selectedInterval = null; // Выбранный интервал повтора (в минутах) или null для однократного запуска
22    let sessionBoostedCount = 0; // Счетчик товаров с бустингом в текущей сессии
23    let sessionNonBoostedCount = 0; // Счетчик товаров без бустинга в текущей сессии
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}</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            isProcessing = true;
422            startBtn.style.display = 'none';
423            pauseBtn.style.display = 'block';
424            stopBtn.style.display = 'block';
425            
426            await startProcessing();
427        };
428        
429        pauseBtn.onclick = () => {
430            isPaused = true;
431            localStorage.setItem('autoBoostPaused', 'true');
432            pauseBtn.textContent = '▶ Продолжить';
433            pauseBtn.className = 'auto-boost-start-btn';
434            pauseBtn.onclick = () => {
435                isPaused = false;
436                localStorage.setItem('autoBoostPaused', 'false');
437                pauseBtn.textContent = '⏸ Пауза';
438                pauseBtn.className = 'auto-boost-pause-btn';
439                pauseBtn.onclick = () => {
440                    isPaused = true;
441                    pauseBtn.textContent = '▶ Продолжить';
442                    pauseBtn.className = 'auto-boost-start-btn';
443                };
444                startProcessing();
445            };
446        };
447        
448        stopBtn.onclick = async () => {
449            isPaused = true;
450            localStorage.setItem('autoBoostPaused', 'true');
451            
452            // Полностью останавливаем таймер и очищаем все данные
453            if (currentIntervalId) {
454                clearInterval(currentIntervalId);
455                currentIntervalId = null;
456            }
457            
458            // Очищаем все сохраненные данные о таймере
459            await GM.setValue('intervalActive', false);
460            await GM.setValue('nextRunTime', null);
461            console.log('Таймер полностью остановлен и очищен.');
462            
463            startBtn.style.display = 'block';
464            pauseBtn.style.display = 'none';
465            stopBtn.style.display = 'none';
466            pauseBtn.textContent = '⏸ Пауза';
467            pauseBtn.className = 'auto-boost-pause-btn';
468            
469            // Обновляем отображение времени следующего запуска
470            const nextRunTimeElement = document.querySelector('#next-run-time');
471            if (nextRunTimeElement) {
472                nextRunTimeElement.innerHTML = '';
473            }
474            
475            updateStats('Процесс остановлен. Все запланированные бустинги отменены.');
476        };
477        
478        console.log('Модальное окно открыто');
479    }
480
481    // Функция для отображения списка товаров
482    function renderProductsList() {
483        if (boostedProducts.length === 0) {
484            return '<div style="text-align: center; color: #999; padding: 20px;">Пока нет товаров с включенным бустингом</div>';
485        }
486        return boostedProducts.map(product => `
487        <div class="auto-boost-product-item">
488            <div class="auto-boost-product-sku">SKU: ${product.sku}</div>
489            <div class="auto-boost-product-name">${product.name}</div>
490            <a href="${product.url}" target="_blank" class="auto-boost-product-link">Открыть товар</a>
491            <div class="auto-boost-product-status">✓ Включен бустинг</div>
492        </div>
493    `).join('');
494    }
495
496    // Функция для начала обработки товаров
497    async function startProcessing() {
498        console.log('Начинаем обработку товаров');
499        updateStats('Сбор товаров со страницы...');
500        
501        // Собираем все строки таблицы
502        const rows = document.querySelectorAll('[data-widget="@products/product-list-table-ods"] tbody tr');
503        console.log(`Найдено строк в таблице: ${rows.length}`);
504        
505        if (rows.length === 0) {
506            updateStats('Товары не найдены на странице');
507            isProcessing = false;
508            return;
509        }
510        
511        // Собираем данные всех товаров ДО начала обработки
512        const productsData = [];
513        for (let i = 0; i < rows.length; i++) {
514            const row = rows[i];
515            // Прокручиваем к строке чтобы данные загрузились
516            row.scrollIntoView({ block: 'center', behavior: 'auto' });
517            await wait(100);
518            
519            // Получаем SKU
520            const skuElement = row.querySelector('[class*="index_sku"]');
521            const sku = skuElement ? skuElement.textContent.trim().replace('SKU', '').trim() : 'N/A';
522            
523            // Получаем название товара
524            const nameLink = row.querySelector('a[href*="ozon.ru/product"]');
525            const name = nameLink ? nameLink.textContent.trim() : 'Без названия';
526            
527            productsData.push({
528                sku: sku,
529                name: name,
530                rowIndex: i
531            });
532        }
533        
534        console.log(`Собрано данных о товарах: ${productsData.length}`);
535        updateStats(`Найдено товаров: ${productsData.length}. Начинаем обработку...`);
536
537        // Обрабатываем товары по очереди
538        for (let i = currentProductIndex; i < productsData.length; i++) {
539            if (isPaused) {
540                console.log('Обработка приостановлена.');
541                return;
542            }
543            
544            currentProductIndex = i;
545            const productData = productsData[i];
546            updateStats(`Обработка товара ${i + 1} из ${productsData.length}: ${productData.name}`);
547            console.log(`Обрабатываем товар ${i + 1} из ${productsData.length}`);
548            
549            await processProduct(productData);
550            await wait(500);
551        }
552        
553        updateStats(`Страница обработана! Обработано товаров: ${productsData.length}. Проверяем следующую страницу...`);
554        currentProductIndex = 0;
555
556        // Проверяем, есть ли следующая страница
557        const nextPageButton = document.querySelector('#pagination button[type="button"]:not([data-selected="true"])');
558        if (nextPageButton) {
559            console.log('Найдена следующая страница, переходим...');
560            updateStats('Переход на следующую страницу...');
561            
562            // Кликаем на следующую страницу
563            nextPageButton.click();
564            
565            // Ждем загрузки новой страницы
566            await wait(2000);
567            
568            // Продолжаем обработку на новой странице
569            if (!isPaused) {
570                await startProcessing();
571            }
572        } else {
573            console.log('Следующая страница не найдена, обработка завершена');
574            
575            // Сохраняем время завершения
576            await GM.setValue('lastCheckTime', Date.now());
577            updateLastCheckTime();
578            
579            // Сохраняем результаты последнего буста
580            await GM.setValue('lastSessionBoosted', sessionBoostedCount);
581            await GM.setValue('lastSessionNonBoosted', sessionNonBoostedCount);
582            
583            // Обновляем счетчики с результатами последнего буста
584            updateCounter();
585            
586            // Перезагружаем список товаров из хранилища
587            await reloadProductsList();
588            
589            if (selectedInterval === null || selectedInterval === 0) {
590                updateStats(`Обработка завершена! С бустингом: ${sessionBoostedCount}, Без бустинга: ${sessionNonBoostedCount}`);
591                isProcessing = false;
592                
593                // Обновляем кнопки
594                const startBtn = document.querySelector('#start-boost-btn');
595                const pauseBtn = document.querySelector('#pause-boost-btn');
596                const stopBtn = document.querySelector('#stop-boost-btn');
597                
598                if (startBtn) startBtn.style.display = 'block';
599                if (pauseBtn) pauseBtn.style.display = 'none';
600                if (stopBtn) stopBtn.style.display = 'none';
601            } else {
602                // Вычисляем время следующего запуска
603                const nextRunTime = new Date(Date.now() + selectedInterval * 60 * 1000);
604                const nextRunTimeStr = nextRunTime.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' });
605                
606                // ВАЖНО: Сохраняем время следующего запуска и активируем таймер
607                await GM.setValue('nextRunTime', nextRunTime.getTime());
608                await GM.setValue('intervalActive', true);
609                
610                updateStats(`Обработка завершена! С бустингом: ${sessionBoostedCount}, Без бустинга: ${sessionNonBoostedCount}. Следующий запуск в ${nextRunTimeStr}`);
611                isProcessing = false;
612                
613                // НЕ запускаем setInterval здесь - таймер будет восстановлен при перезагрузке
614                console.log(`Следующий запуск запланирован на ${nextRunTimeStr}`);
615                
616                // ВАЖНО: Запускаем таймер проверки прямо сейчас
617                startIntervalChecker();
618                
619                // Обновляем отображение времени следующего запуска
620                const nextRunTimeElement = document.querySelector('#next-run-time');
621                if (nextRunTimeElement) {
622                    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>`;
623                }
624                
625                // Обновляем кнопки
626                const startBtn = document.querySelector('#start-boost-btn');
627                const pauseBtn = document.querySelector('#pause-boost-btn');
628                const stopBtn = document.querySelector('#stop-boost-btn');
629                
630                if (startBtn) startBtn.style.display = 'block';
631                if (pauseBtn) pauseBtn.style.display = 'none';
632                if (stopBtn) stopBtn.style.display = 'block';
633            }
634        }
635    }
636
637    // Функция для обработки одного товара
638    async function processProduct(productData) {
639        try {
640            // Проверяем паузу перед началом обработки
641            const pausedState = localStorage.getItem('autoBoostPaused');
642            if (pausedState === 'true' || isPaused) {
643                console.log('Процесс на паузе, пропускаем обработку товара');
644                return;
645            }
646            
647            console.log(`Открываем товар: ${productData.name} (SKU: ${productData.sku})`);
648            
649            // Находим строку товара заново (DOM мог измениться)
650            const rows = document.querySelectorAll('[data-widget="@products/product-list-table-ods"] tbody tr');
651            let targetRow = null;
652            
653            // Ищем строку по SKU
654            for (const row of rows) {
655                const skuElement = row.querySelector('[class*="index_sku"]');
656                const sku = skuElement ? skuElement.textContent.trim().replace('SKU', '').trim() : '';
657                if (sku === productData.sku) {
658                    targetRow = row;
659                    break;
660                }
661            }
662            
663            if (!targetRow) {
664                console.log(`Строка товара ${productData.sku} не найдена в DOM`);
665                return;
666            }
667            
668            // Прокручиваем к строке
669            targetRow.scrollIntoView({ block: 'center', behavior: 'auto' });
670            await wait(300);
671            
672            // Ищем кнопку с ценой
673            const priceButton = targetRow.querySelector('button');
674            if (!priceButton) {
675                console.log(`Кнопка с ценой не найдена для товара ${productData.sku}`);
676                return;
677            }
678            
679            // Генерируем уникальный ID для этой проверки
680            const checkId = Date.now() + Math.random().toString(36).substr(2, 9);
681            
682            // Отправляем сообщение для проверки слайдера с уникальным ID
683            const checkData = {
684                action: 'checkBoost',
685                productSku: productData.sku,
686                productName: productData.name,
687                productUrl: '',
688                timestamp: Date.now(),
689                checkId: checkId
690            };
691            localStorage.setItem('autoBoostCheck', JSON.stringify(checkData));
692            
693            // Устанавливаем флаг, что вкладка с этим ID ожидается
694            localStorage.setItem('autoBoostExpected_' + checkId, 'true');
695            
696            // Кликаем на кнопку с ценой (откроется в новой вкладке)
697            priceButton.click();
698            
699            // Ждем результата проверки
700            let waitTime = 0;
701            const maxWaitTime = 10000; // 10 секунд
702            
703            while (waitTime < maxWaitTime) {
704                await wait(300);
705                waitTime += 300;
706                
707                const result = localStorage.getItem('autoBoostResult');
708                if (result) {
709                    const resultData = JSON.parse(result);
710                    if (resultData.timestamp === checkData.timestamp && resultData.checkId === checkId) {
711                        console.log('Получен результат проверки:', resultData);
712                        
713                        if (resultData.boosted) {
714                            await addBoostedProduct({
715                                sku: productData.sku,
716                                name: productData.name,
717                                url: resultData.url
718                            });
719                            sessionBoostedCount++;
720                        } else {
721                            await addNonBoostedProduct({
722                                sku: productData.sku,
723                                name: productData.name,
724                                url: resultData.url
725                            });
726                            sessionNonBoostedCount++;
727                        }
728                        
729                        localStorage.removeItem('autoBoostResult');
730                        break;
731                    }
732                }
733                
734                // Проверяем, не остановлен ли основной процесс
735                if (isPaused) {
736                    console.log('Основной процесс остановлен, прерываем ожидание результата проверки.');
737                    break;
738                }
739            }
740            
741            // Удаляем флаг ожидания после завершения ожидания
742            localStorage.removeItem('autoBoostExpected_' + checkId);
743            console.log(`Товар обработан: ${productData.name}`);
744            
745        } catch (error) {
746            console.error('Ошибка при обработке товара:', error);
747        }
748    }
749
750    // Функция для добавления товара в список с бустингом
751    async function addBoostedProduct(product) {
752        const exists = boostedProducts.some(p => p.sku === product.sku);
753        if (exists) {
754            console.log(`Товар ${product.sku} уже в списке`);
755            return;
756        }
757        
758        boostedProducts.push({
759            sku: product.sku,
760            name: product.name,
761            url: product.url,
762            timestamp: Date.now()
763        });
764        
765        await GM.setValue('boostedProducts', JSON.stringify(boostedProducts));
766        updateProductsList();
767        console.log(`Товар добавлен в список с бустингом: ${product.name}`);
768    }
769
770    // Функция для добавления товара в список без бустинга
771    async function addNonBoostedProduct(product) {
772        const exists = nonBoostedProducts.some(p => p.sku === product.sku);
773        if (exists) {
774            console.log(`Товар ${product.sku} уже в списке без бустинга`);
775            return;
776        }
777        
778        nonBoostedProducts.push({
779            sku: product.sku,
780            name: product.name,
781            url: product.url,
782            timestamp: Date.now()
783        });
784        
785        await GM.setValue('nonBoostedProducts', JSON.stringify(nonBoostedProducts));
786        console.log(`Товар добавлен в список без бустинга: ${product.name}`);
787    }
788
789    // Функция для загрузки сохраненных товаров без бустинга
790    async function loadNonBoostedProducts() {
791        try {
792            const saved = await GM.getValue('nonBoostedProducts', '[]');
793            nonBoostedProducts = JSON.parse(saved);
794            console.log(`Загружено товаров без бустинга: ${nonBoostedProducts.length}`);
795        } catch (error) {
796            console.error('Ошибка при загрузке данных без бустинга:', error);
797            nonBoostedProducts = [];
798        }
799    }
800
801    // Функция для загрузки сохраненных товаров
802    async function loadBoostedProducts() {
803        try {
804            const saved = await GM.getValue('boostedProducts', '[]');
805            boostedProducts = JSON.parse(saved);
806            console.log(`Загружено товаров с бустингом: ${boostedProducts.length}`);
807        } catch (error) {
808            console.error('Ошибка при загрузке данных:', error);
809            boostedProducts = [];
810        }
811    }
812
813    // Функция для обновления списка товаров в UI
814    function updateProductsList() {
815        const listElement = document.querySelector('#products-list');
816        if (listElement) {
817            listElement.innerHTML = renderProductsList();
818        }
819    }
820    
821    // Функция для перезагрузки списка товаров из хранилища
822    async function reloadProductsList() {
823        await loadBoostedProducts();
824        updateProductsList();
825    }
826
827    // Функция для обновления счетчика
828    function updateCounter() {
829        const counterElement = document.querySelector('#boost-counter');
830        if (counterElement) {
831            counterElement.textContent = sessionBoostedCount;
832        }
833        
834        const nonBoostCounterElement = document.querySelector('#non-boost-counter');
835        if (nonBoostCounterElement) {
836            nonBoostCounterElement.textContent = sessionNonBoostedCount;
837        }
838    }
839
840    // Функция для обновления статуса
841    function updateStats(message) {
842        const statsElement = document.querySelector('#boost-stats');
843        if (statsElement) {
844            statsElement.textContent = message;
845        }
846        console.log('Статус:', message);
847    }
848
849    // Функция для обновления времени последней проверки
850    async function updateLastCheckTime() {
851        const lastCheckTime = await GM.getValue('lastCheckTime', null);
852        const lastCheckElement = document.querySelector('#last-check-time');
853        
854        if (lastCheckElement && lastCheckTime) {
855            const date = new Date(lastCheckTime);
856            const time = date.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' });
857            const dateStr = date.toLocaleDateString('ru-RU');
858            lastCheckElement.innerHTML = `<div style="text-align: center; color: #666; font-size: 13px; margin-top: 8px;">Последняя проверка: ${time} / ${dateStr}</div>`;
859        }
860    }
861
862    // Обработчик для страницы товара (проверка слайдера)
863    async function handleProductPage() {
864        // Проверяем, не на паузе ли процесс
865        const pausedState = localStorage.getItem('autoBoostPaused');
866        if (pausedState === 'true') {
867            console.log('Процесс на паузе, вкладка не будет автоматически закрыта');
868            return;
869        }
870        
871        // Проверяем, есть ли ожидаемая проверка (по уникальному ID)
872        for (let i = 0; i < localStorage.length; i++) {
873            const key = localStorage.key(i);
874            if (key && key.startsWith('autoBoostExpected_') && localStorage.getItem(key) === 'true') {
875                const checkId = key.replace('autoBoostExpected_', '');
876                const checkDataRaw = localStorage.getItem('autoBoostCheck');
877                
878                if (checkDataRaw) {
879                    const checkData = JSON.parse(checkDataRaw);
880                    
881                    // Проверяем, совпадает ли checkId в autoBoostCheck с найденным
882                    if (checkData.checkId === checkId) {
883                        console.log('Найдена ожидаемая проверка с ID:', checkId);
884                        await performCheckAndClose(checkData);
885                        return;
886                    }
887                }
888            }
889        }
890        
891        // Если не нашли ожидаемой проверки, это вкладка, открытая вручную
892        console.log('Вкладка открыта вручную, проверка не требуется.');
893    }
894
895    // Выносим логику проверки в отдельную функцию
896    async function performCheckAndClose(data) {
897        if (Date.now() - data.timestamp > 60000) {
898            console.log('Таймаут проверки бустинга истек.');
899            window.close();
900            return;
901        }
902
903        console.log('Обнаружен запрос на проверку бустинга для ID:', data.checkId);
904        
905        try {
906            const slider = await waitForElement('[class*="index_sliderTrack"]', 5000);
907            console.log('Слайдер найден.');
908
909            const style = slider.getAttribute('style') || '';
910            const widthMatch = style.match(/width:\s*(\d+(?:\.\d+)?)%/);
911            const width = widthMatch ? parseFloat(widthMatch[1]) : 0;
912
913            console.log(`Ширина слайдера: ${width}%`);
914
915            if (width === 0) {
916                console.log('Бустинг не включен (width: 0%)');
917                const result = {
918                    boosted: false,
919                    timestamp: data.timestamp,
920                    checkId: data.checkId,
921                    url: window.location.href
922                };
923                localStorage.setItem('autoBoostResult', JSON.stringify(result));
924                await wait(300);
925                window.close();
926                return;
927            }
928
929            console.log('Бустинг включен, ищем кнопку "Сохранить"');
930            const saveButton = Array.from(document.querySelectorAll('button')).find(btn =>
931                btn.textContent.includes('Сохранить') || btn.textContent.includes('сохранить')
932            );
933            
934            if (saveButton) {
935                console.log('Нажимаем кнопку "Сохранить"');
936                saveButton.click();
937                await wait(1500);
938            } else {
939                console.log('Кнопка "Сохранить" не найдена.');
940            }
941
942            const result = {
943                boosted: true,
944                timestamp: data.timestamp,
945                checkId: data.checkId,
946                url: window.location.href
947            };
948            localStorage.setItem('autoBoostResult', JSON.stringify(result));
949            await wait(300);
950            window.close();
951
952        } catch (error) {
953            console.error('Ошибка при ожидании слайдера:', error.message);
954            const result = {
955                boosted: false,
956                timestamp: data.timestamp,
957                checkId: data.checkId,
958                url: window.location.href
959            };
960            localStorage.setItem('autoBoostResult', JSON.stringify(result));
961            await wait(300);
962            window.close();
963        }
964    }
965
966    // Функция для проверки и запуска таймера при загрузке страницы
967    async function checkAndRestoreInterval() {
968        const intervalActive = await GM.getValue('intervalActive', false);
969        const savedInterval = await GM.getValue('selectedInterval', null);
970        const nextRunTime = await GM.getValue('nextRunTime', null);
971        
972        console.log('Проверка сохраненного состояния:', { intervalActive, savedInterval, nextRunTime });
973        
974        if (intervalActive && savedInterval && savedInterval > 0) {
975            selectedInterval = savedInterval;
976            
977            if (nextRunTime) {
978                const now = Date.now();
979                const timeUntilNextRun = nextRunTime - now;
980                
981                if (timeUntilNextRun > 0) {
982                    const nextRunDate = new Date(nextRunTime);
983                    const nextRunTimeStr = nextRunDate.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' });
984                    console.log(`Восстановление таймера. Следующий запуск в ${nextRunTimeStr} (через ${Math.round(timeUntilNextRun / 1000 / 60)} минут)`);
985                    
986                    // Запускаем таймер проверки
987                    startIntervalChecker();
988                    
989                    console.log(`Таймер успешно восстановлен. Интервал: ${selectedInterval} минут`);
990                } else {
991                    // Время уже прошло, запускаем сразу
992                    console.log('Время следующего запуска уже прошло, запускаем процесс сейчас');
993                    
994                    // Вычисляем новое время следующего запуска
995                    const newNextRunTime = new Date(Date.now() + selectedInterval * 60 * 1000);
996                    await GM.setValue('nextRunTime', newNextRunTime.getTime());
997                    
998                    sessionBoostedCount = 0;
999                    sessionNonBoostedCount = 0;
1000                    isProcessing = true;
1001                    isPaused = false;
1002                    localStorage.setItem('autoBoostPaused', 'false');
1003                    
1004                    // Очищаем списки товаров перед новым запуском
1005                    boostedProducts = [];
1006                    nonBoostedProducts = [];
1007                    await GM.setValue('boostedProducts', JSON.stringify(boostedProducts));
1008                    await GM.setValue('nonBoostedProducts', JSON.stringify(nonBoostedProducts));
1009                    console.log('Списки товаров очищены перед автоматическим запуском');
1010                    
1011                    await startProcessing();
1012                }
1013            } else {
1014                // Если nextRunTime не установлено, значит это первый запуск после выбора интервала
1015                console.log('Таймер активен, но время следующего запуска не установлено');
1016            }
1017        } else {
1018            console.log('Нет активного таймера для восстановления');
1019        }
1020    }
1021
1022    // Функция для запуска таймера проверки
1023    function startIntervalChecker() {
1024        // Очищаем предыдущий таймер, если он был
1025        if (currentIntervalId) {
1026            clearInterval(currentIntervalId);
1027            console.log('Предыдущий таймер очищен');
1028        }
1029        
1030        // Проверяем каждые 30 секунд, не пора ли запускать
1031        currentIntervalId = setInterval(async () => {
1032            const currentNextRunTime = await GM.getValue('nextRunTime', null);
1033            const currentIntervalActive = await GM.getValue('intervalActive', false);
1034            
1035            if (!currentIntervalActive) {
1036                console.log('Таймер был остановлен, прекращаем проверку');
1037                clearInterval(currentIntervalId);
1038                currentIntervalId = null;
1039                return;
1040            }
1041            
1042            if (currentNextRunTime && Date.now() >= currentNextRunTime) {
1043                console.log('Время запуска наступило! Запускаем процесс');
1044                clearInterval(currentIntervalId);
1045                currentIntervalId = null;
1046                
1047                // Вычисляем новое время следующего запуска
1048                const newNextRunTime = new Date(Date.now() + selectedInterval * 60 * 1000);
1049                await GM.setValue('nextRunTime', newNextRunTime.getTime());
1050                
1051                // Сбрасываем счетчики и снимаем паузу
1052                sessionBoostedCount = 0;
1053                sessionNonBoostedCount = 0;
1054                isProcessing = true;
1055                isPaused = false;
1056                localStorage.setItem('autoBoostPaused', 'false');
1057                
1058                // Очищаем списки товаров перед новым запуском
1059                boostedProducts = [];
1060                nonBoostedProducts = [];
1061                await GM.setValue('boostedProducts', JSON.stringify(boostedProducts));
1062                await GM.setValue('nonBoostedProducts', JSON.stringify(nonBoostedProducts));
1063                console.log('Списки товаров очищены перед автоматическим запуском');
1064                
1065                await startProcessing();
1066            }
1067        }, 30000); // Проверяем каждые 30 секунд
1068        
1069        console.log('Таймер проверки запущен (проверка каждые 30 секунд)');
1070    }
1071
1072    // Инициализация
1073    async function init() {
1074        console.log('Инициализация Auto Boost для Ozon (v2.3.2)');
1075        addStyles();
1076        
1077        const currentUrl = window.location.href;
1078        
1079        if (currentUrl.includes('/app/products')) {
1080            console.log('Обнаружена страница с таблицей товаров');
1081            
1082            // Проверяем и восстанавливаем таймер
1083            await checkAndRestoreInterval();
1084            
1085            await wait(2000);
1086            createBoostButton();
1087            
1088            const observer = new MutationObserver(() => {
1089                if (!document.querySelector('.auto-boost-button')) {
1090                    createBoostButton();
1091                }
1092            });
1093            
1094            observer.observe(document.body, {
1095                childList: true,
1096                subtree: true
1097            });
1098        } else if (currentUrl.includes('/app/prices/manager/')) {
1099            console.log('Обнаружена страница товара');
1100            handleProductPage();
1101        }
1102    }
1103
1104    if (document.readyState === 'loading') {
1105        document.addEventListener('DOMContentLoaded', init);
1106    } else {
1107        init();
1108    }
1109})();
Auto Boost для Ozon | Robomonkey