# 2026-02-20 - Obsluga statusu ACTIVE dla klientow ## Zmienione pliki - `autoload/controls/class.Clients.php` - `save()` zapisuje teraz pole `active` (domyslnie `1`, gdy brak wartosci z formularza). - Dodana nowa akcja `set_active()` pod endpoint `/clients/set_active` do szybkiej zmiany statusu klienta AJAX-em. - `force_sync()` ma dodatkowa walidacje: - nie pozwala kolejkowac synchronizacji dla klienta nieaktywnego (`active != 1`), - nadal blokuje klienta usunietego (`deleted = 1`) i klienta bez wymaganych ID. - Kompatybilnosc schematu `clients` bez kolumny `deleted`: - helpery `clients_has_deleted_column()` i `sql_clients_not_deleted()`, - `force_sync()` i `sync_status()` nie wywalaja sie, gdy w bazie nie ma kolumny `deleted`. - `templates/clients/main_view.php` - Tabela klientow ma nowa kolumne `Status` (Aktywny/Nieaktywny). - Wiersz klienta trzyma `data-active` do obslugi UI i synchronizacji. - Dodany przycisk toggle (ikona `fa-toggle-on/off`) do natychmiastowej aktywacji/dezaktywacji. - Przyciski synchronizacji (kampanie/produkty/merchant) sa blokowane (`disabled`) dla nieaktywnego klienta i odblokowywane po aktywacji. - Formularz Dodaj/Edytuj klienta ma nowe pole `Status klienta` (`active`). - JS: - `toggleClientActive()` wysyla POST na `/clients/set_active`, - `updateClientStatusUI()` odswieza status i stan komorki Sync bez przeladowania strony, - `loadSyncStatus()` pomija paski postepu dla nieaktywnych klientow i pokazuje `nieaktywny`. ## Gdzie to jest wykorzystywane - Zarzadzanie statusem klienta: - UI listy i formularza: `templates/clients/main_view.php` - Backend zapisu i toggle: `autoload/controls/class.Clients.php` - Ograniczenie recznego wymuszenia synchronizacji do klientow aktywnych: - `autoload/controls/class.Clients.php` (`force_sync()`) # 2026-02-20 - CRON kampanii (nowy przebieg, stare jako archiwum) ## Zmienione pliki - `autoload/controls/class.Cron.php` - Dodany nowy `cron_campaigns()` jako glowny endpoint pod nowy przeplyw. - Stary kod zostal zachowany jako archiwum: `cron_campaigns_archive()`. - Nowy przebieg: - bierze tylko aktywnych klientow (`active = 1`) z Google Ads Customer ID, - liczy okno dat na podstawie `google_ads_conversion_window_days` z `config.php` (z fallbackiem), - konczy okno na `przedwczoraj` (bez pobierania danych dzisiejszych), - przechodzi po datach dzien po dniu (rosnaco), - zapisuje/aktualizuje kampanie do `campaigns`, - zapisuje/aktualizuje historie dzienne do `campaigns_history` (upsert po `campaign_id + date_add`), - zapisuje grupy reklam / groupy PMAX do `campaign_ad_groups`. - po zakonczeniu kampanii + ad groups dla klienta, dla calego okna dat pobiera search terms dzienne do `campaign_search_terms_history`, - po pobraniu historii search terms wykonuje agregacje do `campaign_search_terms` (zanim przejdzie do kolejnego klienta). - Dodany krok syncu fraz dodanych i wykluczonych: - tabele docelowe: `campaign_keywords` i `campaign_negative_keywords`, - uruchamiany raz na cykl klienta (po ostatnim dniu okna), nie x razy dla kazdego dnia. - Kampanie produktowe / PMAX: - nie maja fraz dodanych, wiec w `campaign_keywords` moga miec 0 rekordow, - frazy wykluczone sa dalej synchronizowane do `campaign_negative_keywords`. # 2026-02-20 - Produkty: przygotowanie schematu bazy ## Zmienione pliki - `migrations/016_products_model_unification.sql` - Dodane kolumny produktowe bezposrednio do `products`: - `custom_label_4`, `custom_label_3`, `title`, `description`, `google_product_category`, `product_url`. - Backfill danych z `products_data` -> `products` (tylko gdy pole w `products` jest puste). - Dodana nowa tabela agregacyjna `products_aggregate`: - scope: `product_id + campaign_id + ad_group_id` (unikalne), - metryki `*_30` i `*_all_time`, - `date_sync` (kiedy agregat byl przeliczony). - `docs/database.sql` - Zaktualizowana definicja `products` o nowe kolumny danych produktu. - Dodana definicja tabeli `products_aggregate`. ## Ustalenie projektowe - `products` staje sie glowna tabela danych produktu. - `products_data` zostaje tymczasowo dla kompatybilnosci starego kodu; dane sa migrowane do `products`. - Agregaty dla widokow `/products` powinny docelowo byc czytane z `products_aggregate` zamiast liczenia w locie. # 2026-02-20 - Produkty: przepiecie na `products` + agregaty ## Zmienione pliki - `autoload/factory/class.Products.php` - `get_product_data()`: - najpierw czyta pola produktowe z `products` (`custom_label_4`, `custom_label_3`, `title`, `description`, `google_product_category`, `product_url`), - fallback do `products_data` dla kompatybilnosci. - `set_product_data()`: - zapisuje pole glownie do `products`, - rownolegle mirroruje zapis do `products_data` (kompatybilnosc starego kodu). - `autoload/controls/class.Cron.php` - `sync_products_fetch_for_client()`: - import produktow zapisuje dane produktowe bezposrednio do `products` (w tym `title`, `product_url`), - usuniete poleganie na `products_data` podczas samego fetchu. - `aggregate_products_history_30_for_client()`: - po przeliczeniu `products_history_30` odpala przebudowe agregatow `products_aggregate` dla klienta i dnia. - Dodana metoda `rebuild_products_aggregate_for_client( $client_id, $date_sync )`: - liczy metryki `*_30` i `*_all_time` z `products_history`, - zapisuje scope (`product_id + campaign_id + ad_group_id`) do `products_aggregate`. - `rebuild_products_temp_for_client()`: - przestawione z liczenia bezposrednio po `products_history` na odczyt z `products_aggregate`, - zmniejsza liczenie "w locie" dla widoku `/products`. - `cron_product_history_30_save()`: - `products_history_30` przechowuje teraz srednie dzienne wartosci z okna do 30 dni (zamiast sumy okna), - nadal zapisuje `roas_all_time` dla danego dnia. - `generate_custom_feed_for_client()`: - zrodlo danych produktowych przepiete na `products` (bez wymaganego `INNER JOIN products_data`). - diagnostyka i pobieranie brakujacych URL (`cron_products_urls`): - logika "ma URL / brak URL" bierze pod uwage `products.product_url` z fallbackiem do `products_data`. ## Gdzie to jest wykorzystywane - Pipeline produktowy: - `/cron/cron_products` - etap `fetch` -> `products_history`, - etap agregacji -> `products_history_30` + `products_aggregate`, - etap finalny -> `products_temp` budowane z `products_aggregate`. - Widok tabeli produktow `/products`: - dane nadal czytane z `products_temp`, ale `products_temp` jest teraz zasilane agregatami z `products_aggregate`. - Dodany helper `sync_campaigns_snapshot_for_client()` dla nowego przebiegu kampanii. - Dodany helper `sync_campaign_terms_backfill_for_client()` dla kroku fraz (history + agregacja). - Tryb wykonania nowego pipeline kampanii: 1 dzien = 1 wywolanie CRON. - Na jednym wywolaniu: kampanie + ad groups + search terms history + agregacja search terms dla jednego dnia. - Kolejne wywolanie przechodzi do kolejnego dnia dla tego samego klienta. - Tryb debug dla nowego CRON: - `?debug=true` zwraca czytelny HTML (podsumowanie + pelny payload), - bez debug zwracany jest standardowy JSON. - Dodany helper `cleanup_pipeline_rows_outside_window()` aby pipeline kampanii trzymal tylko aktualne okno dat. - Filtry klientow w nowym CRON kampanii sa odporne na stare dane (`NULL`): `COALESCE(active,0)`, `COALESCE(deleted,0)`, `TRIM(COALESCE(google_ads_customer_id,''))`. - Dodana kompatybilnosc schematu `clients` bez kolumny `deleted`: - helpery: `clients_has_column()`, `sql_clients_not_deleted()`, `sql_clients_deleted()`, - nowy pipeline kampanii (`cron_campaigns`/`cron_universal`) nie wywala sie na bazie bez `deleted`. - `get_conversion_window_days( $prefer_config = false )` uwzglednia teraz konfiguracje z `config.php`. - `sync_campaign_ad_groups_for_client()` dostal parametr `as_of_date`. - `autoload/services/class.GoogleAdsApi.php` - `get_ad_groups_30_days()` wspiera teraz parametr `as_of_date` i zakres dat `[as_of_date-29, as_of_date]`. - `get_ad_groups_all_time()` wspiera teraz parametr `as_of_date` (filtr `segments.date <= as_of_date` z fallbackiem). ## Gdzie to jest wykorzystywane - Głowny CRON kampanii: `/cron/cron_campaigns` -> `\controls\Cron::cron_campaigns()`. - Uniwersalny CRON pipeline (zalecany endpoint): `/cron/cron_universal` -> `\controls\Cron::cron_universal()` (aktualnie deleguje do kroku kampanii). - Archiwalny CRON kampanii (stara logika): `/cron/cron_campaigns_archive`. - Dane do wykresow/tabel kampanii pozostaja pobierane z `campaigns_history`. # 2026-02-20 - CRON uniwersalny jako glowny endpoint (1 dzien na wywolanie) ## Zmienione pliki - `autoload/controls/class.Cron.php` - `cron_universal()` nie deleguje juz do `cron_campaigns()`. - W jednym wywolaniu realizuje sekwencje: - `kampanie` (snapshot + ad groups + search terms + agregacja), - `produkty` (fetch + `products_history_30` + `products_aggregate` + `products_temp`). - Tryb pracy pozostaje: `1 wywolanie = 1 klient + 1 dzien`. - Status dnia jest zapisywany do `cron_sync_status` dla obu pipeline: - `campaigns`, - `products`. - Gdy krok kampanii zwroci blad, krok produktow dla tego dnia jest pomijany (`products_sync_skipped_reason=campaigns_failed`). ## Gdzie to jest wykorzystywane - Docelowy adres CRON: - `/cron/cron_universal?debug=true` - Stare endpointy (`/cron/cron_campaigns`, `/cron/cron_products`) pozostaja w kodzie, ale nie sa docelowa sciezka wykonywania. # 2026-02-20 - Poprawka niezaleznosci pipeline w `cron_universal` ## Problem - `campaigns` mialo juz 100% (`done`) i `cron_universal` konczyl wykonanie, mimo ze `products` mial jeszcze zalegle daty. ## Zmienione pliki - `autoload/controls/class.Cron.php` - `cron_universal()` wybiera teraz aktywnego klienta niezaleznie dla obu pipeline: - `campaigns`, - `products`. - Zakonczenie "wszyscy przetworzeni" następuje dopiero, gdy **oba** pipeline nie maja juz aktywnych pozycji. - Dodane osobne liczenie pozostalych dat: - `campaigns_remaining_dates`, - `products_remaining_dates`. - Statusy `done/pending` sa zapisywane osobno dla kazdego pipeline; produkty nie sa juz blokowane przez sam fakt, ze kampanie sa skonczone globalnie. - Ujednolicenie trybu `client_id`: - kampanie i produkty wykonują sie niezaleznie (w tym samym wywolaniu), a bledy sa laczone tylko w odpowiedzi. # 2026-02-20 - Usuniecie `products_data` ## Zmienione pliki - `migrations/017_drop_products_data.sql` - Dodana migracja usuwajaca tabele `products_data`. - `autoload/factory/class.Products.php` - `get_product_data()` czyta dane tylko z `products`. - `set_product_data()` zapisuje dane tylko do `products`. - `autoload/controls/class.Cron.php` - diagnostyka URL i wybieranie produktow bez URL opiera sie juz tylko o `products.product_url`. - `docs/database.sql` - usunieta definicja tabeli `products_data`. - `migrations/demo_data.sql` - usuniete operacje `INSERT/DELETE` na `products_data`, - etykiety demo (`custom_label_4`) sa ustawiane bezposrednio w `products`. ## Gdzie to jest wykorzystywane - Dane produktowe (`title`, `description`, `google_product_category`, `custom_label_3`, `custom_label_4`, `product_url`) sa trzymane tylko w `products`. # 2026-02-20 - Ostatni krok `cron_universal`: URL z Merchant + alerty brakow ## Zmienione pliki - `autoload/controls/class.Cron.php` - Dodany helper `sync_products_urls_and_alerts_for_client()`. - Na koncu przebiegu `cron_universal` (zarowno tryb automatyczny, jak i `client_id`) wykonywany jest krok: - pobranie URL produktow z Google Merchant Center dla produktow bez URL, - zapis URL do `products.product_url`. - Gdy `offer_id` nie istnieje w Merchant Center, tworzony/aktualizowany jest alert w `campaign_alerts`: - `alert_type = products_missing_in_merchant_center`, - scope techniczny: `campaign_external_id = 0`, `ad_group_external_id = 0`, - `meta_json` zawiera m.in. listy `missing_offer_ids` i `missing_product_ids`. - Gdy w danym dniu brak brakujacych produktow, dzienny alert tego typu jest czyszczony. - Do odpowiedzi cron dodane pola diagnostyczne: - `merchant_urls_checked`, - `merchant_urls_updated`, - `merchant_missing_in_mc_count`, - `merchant_missing_offer_ids`. - `cron_universal` ma dodatkowy fallback niezalezny od pipeline `campaigns/products`: - gdy oba pipeline sa zakonczone, ale sa jeszcze produkty bez URL, uruchamia sam krok Merchant URL + alerty (`merchant_only=1`), - dopiero brak takich produktow daje komunikat "Wszyscy aktywni klienci zostali przetworzeni...". - Krok Merchant URL nie jest wykonywany dla kazdego dnia okna; dziala jako osobny etap po zakonczeniu `campaigns/products`. - Do zapytan do GMC trafiaja tylko produkty z `products.product_url IS NULL` i `merchant_url_not_found = 0`. - Na jedno wywolanie wykonywana jest jedna paczka sprawdzen (limit z `config.php`: `cron_products_urls_limit_per_client`, ustawiony na `100`). - Produkty, ktorych GMC nie zwraca (brak URL), sa oznaczane: - `products.merchant_url_not_found = 1`, - `products.merchant_url_last_check = NOW()`, - dzieki temu nie sa wysylane ponownie w nieskonczonosc. - Alert `products_missing_in_merchant_center` jest liczony na podstawie calej aktualnej puli `merchant_url_not_found = 1` (nie tylko bieżącej paczki), wiec nie znika przy `checked_products = 0`. - Alerty sa per produkt (1 alert = 1 produkt): - dla kazdego produktu bez URL i z `merchant_url_not_found = 1` tworzony jest osobny wpis w `campaign_alerts`, - tresc alertu zawiera nazwe produktu (fallback: `name`, dalej `offer_id`) i `offer_id`, - technicznie: `campaign_external_id = products.id`, co stabilizuje unikalnosc wpisu. - `migrations/018_products_merchant_url_flags.sql` - Dodane kolumny w `products`: - `merchant_url_not_found` (TINYINT, domyslnie 0), - `merchant_url_last_check` (DATETIME). - Normalizacja: puste/sztuczne `product_url` (`'', '0', '-', 'null'`) ustawiane na `NULL`. - `autoload/factory/class.Products.php` - Przy zapisie `product_url`: - ustawiany jest `merchant_url_last_check`, - dla poprawnego URL resetowane jest `merchant_url_not_found = 0`. # 2026-02-20 - Alerty na stronie `/products` dla klient + kampania ## Zmienione pliki - `autoload/factory/class.Products.php` - `get_scope_alerts()` nie wymaga juz wybranej grupy reklam: - minimalny scope: `client_id + campaign_id`, - filtr `ad_group_id` jest stosowany tylko opcjonalnie (gdy grupa jest wybrana). - `templates/products/main_view.php` - `load_scope_alerts()` pobiera alerty juz dla kombinacji `klient + kampania`. - Sekcja alertow ma zaktualizowany opis: kampania + opcjonalna grupa reklam. ## Gdzie to jest wykorzystywane - `/products` - Panel alertow pod filtrami pokazuje alerty: - dla calej kampanii (gdy grupa reklam nie jest wybrana), - lub zawezone do konkretnej grupy (gdy grupa reklam jest wybrana). # 2026-02-20 - Etykietowanie alertow Merchant (bez falszywej kampanii) ## Zmienione pliki - `autoload/controls/class.Cron.php` - Dla alertu `products_missing_in_merchant_center` nie jest juz zapisywany `product_id` w `campaign_external_id`. - Pola scope kampanii/grupy sa zapisywane jako `0` (alert produktowy, bez przypisania do kampanii). - `templates/campaign_alerts/main_view.php` - Dla alertu `products_missing_in_merchant_center` tabela alertow pokazuje: - Kampania: `Produkt (Merchant Center)`, - Grupa reklam: `---`. - Dla pozostalych alertow fallback `Kampania #...` / `Grupa reklam #...` dziala tylko dla dodatnich external_id; dla `0` pokazuje neutralne etykiety. # 2026-02-20 - Powiazanie `campaign_alerts` z `products` ## Zmienione pliki - `migrations/019_campaign_alerts_product_id.sql` - Dodana kolumna `campaign_alerts.product_id` (NULL) oraz indeks `idx_alert_product`. - `autoload/controls/class.Cron.php` - Alerty `products_missing_in_merchant_center` zapisuja `product_id` w tabeli `campaign_alerts`. - Dla zachowania unikalnosci dziennej per produkt, techniczny `campaign_external_id` pozostaje rowny `product_id`. - `autoload/factory/class.CampaignAlerts.php` - `get_alerts()` zwraca teraz rowniez pole `product_id`. - `docs/database.sql` - Dodana aktualna definicja tabeli `campaign_alerts` z kolumna `product_id`. # 2026-02-20 - CRON produktow: `title` nie jest uzupelniany automatycznie ## Zmienione pliki - `autoload/controls/class.Cron.php` - W syncu produktow do tabeli `products` CRON nie zapisuje juz pola `title`. - Dla nowych produktow CRON zapisuje tylko `name` (bez `title`). - Dla istniejacych produktow usunieto automatyczne uzupelnianie pustego `title`. ## Gdzie to jest wykorzystywane - `/cron/cron_universal` - automatyczny import produktow nie nadpisuje ani nie uzupelnia `products.title`, - `title` pozostaje polem do recznej edycji i wysylki do GMC. # 2026-02-20 - Lista produktow z 0 wyswietlen (30 dni) na `/products` ## Zmienione pliki - `autoload/factory/class.Products.php` - Dodana metoda `get_products_without_impressions_30( $client_id, $campaign_id, $limit )`. - Zwraca produkty z wybranej kampanii, ktore maja sume `impressions_30 = 0` na podstawie `products_aggregate`. - Dodatkowy filtr `ad_group_id` (opcjonalny), aby lista byla zgodna z aktualnym filtrem grupy reklam na widoku. - `autoload/controls/class.Products.php` - Dodany endpoint `get_products_without_impressions_30()`. - Zwraca JSON: `status`, `products[]`, `count` i przyjmuje opcjonalnie `ad_group_id`. - `templates/products/main_view.php` - Dodana sekcja nad tabela produktow: - "Produkty do sprawdzenia (0 wyswietlen w ostatnich 30 dniach)". - Sekcja pojawia sie dla wybranego `klient + kampania`. - Lista odswieza sie przy zmianie klienta/kampanii/grupy oraz po zaladowaniu strony. ## Gdzie to jest wykorzystywane - `/products` - pomocnicza lista produktow potencjalnie nieistniejacych / wymagajacych weryfikacji (0 wyswietlen w 30 dni dla wybranej kampanii). # 2026-02-20 - Ustawienia CRON: poprawka licznika klientow + usuniecie "Krok 1/Krok 2" ## Zmienione pliki - `autoload/controls/class.Users.php` - Licznik `Klienci z Google Ads ID` liczy teraz klientow z: - `COALESCE(active, 0) = 1`, - `TRIM(COALESCE(google_ads_customer_id, '')) <> ''`. - Analogicznie poprawione filtry dla klientow Merchant i zapytan pomocniczych (wg `active`). - Harmonogram krokow (`Krok 1`, `Krok 2`) w danych dashboardu CRON jest pusty. - `templates/users/settings.php` - Usunieta sekcja wizualna harmonogramu krokow CRON (`Krok 1` / `Krok 2`). - Usunieta obsluga renderowania tej sekcji w JS odswiezajacym status CRON. ## Gdzie to jest wykorzystywane - `/settings?settings_tab=cron` - licznik klientow z Google Ads ID pokazuje poprawna wartosc na podstawie aktywnych klientow (`active = 1`), - brak sekcji "Krok 1 / Krok 2". # 2026-02-20 - `/products` czyta bezposrednio z `products_aggregate` ## Zmienione pliki - `autoload/factory/class.Products.php` - Zapytania dla listy produktow i licznikow zostaly przepiete z `products_temp` na `products_aggregate`: - `get_products()`, - `get_roas_bounds()`, - `get_records_total_products()`, - `get_product_full_context()`. - Metryki all-time sa liczone z pol: - `impressions_all_time`, `clicks_all_time`, `cost_all_time`, `conversions_all_time`, `conversion_value_all_time`. - Metryki 30d sa czytane z: - `impressions_30`, `clicks_30`. ## Gdzie to jest wykorzystywane - `/products` - tabela i liczniki nie zaleza juz od `products_temp`; biora dane bezposrednio z `products_aggregate`. # 2026-02-20 - `custom_label_4` tylko z tabeli `products` ## Ustalenie - Etykieta `custom_label_4` jest czytana i zapisywana z tabeli `products`. - Agregaty (`products_aggregate`) nie sa zrodlem dla pola `custom_label_4`. # 2026-02-20 - Usuniecie funkcjonalnosci `bestseller_min_roas` ## Zmienione pliki - `templates/products/main_view.php` - Usuniety filtr UI: pole `Bestseller min ROAS` (`#bestseller_min_roas`). - Usuniety frontendowy loader wartosci progu (`load_client_bestseller_min_roas`). - Usuniete wywolania loadera przy zmianie klienta i przy inicjalizacji strony. - Usuniety zapis AJAX progu klienta na blur (`/products/save_client_bestseller_min_roas/`). - `autoload/controls/class.Products.php` - Usuniete endpointy: - `get_client_bestseller_min_roas()` - `save_client_bestseller_min_roas()` - `autoload/factory/class.Products.php` - Usuniete metody dostepu do progu ROAS klienta: - `get_client_bestseller_min_roas( $client_id )` - `save_client_bestseller_min_roas( $client_id, $min_roas )` - `autoload/controls/class.Cron.php` - W `rebuild_products_temp_for_client()` usunieta logika automatycznej zmiany `custom_label_4` oparta o prog `bestseller_min_roas`. - Funkcja pozostaje jako krok diagnostyczny zwracajacy liczbe scope z `products_aggregate`. ## Efekt - Aplikacja nie odczytuje, nie zapisuje i nie wykorzystuje juz `bestseller_min_roas` w UI, endpointach ani w CRON. - Automatyczne oznaczanie `custom_label_4 = bestseller` na podstawie tego progu zostalo wycofane.