# 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.
