From 5a3948fee554304cc52b76600e69980c7b1f0e75 Mon Sep 17 00:00:00 2001 From: Jacek Pyziak Date: Thu, 30 Apr 2026 21:33:58 +0200 Subject: [PATCH] update --- .paul/STATE.md | 55 +---- .paul/STATE.md.bak | 59 +++++ .paul/changelog/2026-04-30.md | 26 +-- .../07-xml-feed-cron-refresh/07-01-PLAN.md | 208 ++++++++++++++++++ .../07-xml-feed-cron-refresh/07-01-SUMMARY.md | 40 ++++ .vscode/ftp-kr.sync.cache.json | 119 ++++++++-- autoload/controls/class.Clients.php | 29 ++- autoload/controls/class.Cron.php | 99 +++++++-- autoload/controls/class.Users.php | 1 + autoload/services/class.XmlFeedImporter.php | 83 ++++++- templates/clients/main_view.php | 29 ++- 11 files changed, 635 insertions(+), 113 deletions(-) create mode 100644 .paul/STATE.md.bak create mode 100644 .paul/phases/07-xml-feed-cron-refresh/07-01-PLAN.md create mode 100644 .paul/phases/07-xml-feed-cron-refresh/07-01-SUMMARY.md diff --git a/.paul/STATE.md b/.paul/STATE.md index b3ddd15..624d2f8 100644 --- a/.paul/STATE.md +++ b/.paul/STATE.md @@ -1,56 +1,21 @@ -# STATE - ## Current Position -Milestone: (ad-hoc) Products - XML feed import -Phase: 6 of 6 (XML Feed Import) - Completed -Plan: 06-01 unified (loop closed) -Status: UNIFY complete -Last activity: 2026-04-30T01:15:00Z - Zamknieto petle planu 06-01 - -Progress: -- Milestone: [██████████] 100% -- Phase 6: [██████████] 100% +Phase: 07-xml-feed-cron-refresh — Complete +Plan: 07-01 complete +Status: UNIFY complete. Phase complete — ready for next phase. +Last activity: 2026-04-30T07:19:05.331Z ## Loop Position +Current loop state: ``` -PLAN --> APPLY --> UNIFY - ✓ ✓ ✓ [Petla zamknieta] +PLAN ──▶ APPLY ──▶ UNIFY + ✓ ✓ ✓ [Phase complete — ready for next phase] ``` ## Session Continuity Last session: 2026-04-30 -Stopped at: Loop closed, gotowe do nowego /paul:plan -Next action: Jesli chcesz kontynuowac - uruchom /paul:plan z kolejnym zadaniem -Resume file: .paul/phases/06-xml-feed-import/06-01-SUMMARY.md - -## Historia zrealizowanych planow - -- `01-01-PLAN.md` - CL3 -> CL1 w tabeli /products (completed 2026-04-22) -- `02-01-PLAN.md` - custom_label_1 w supplemental feed TSV (completed 2026-04-22) -- `03-01-PLAN.md` - Powrot do widoku "wszystkie kampanie" w /products (completed 2026-04-24) -- `04-01-PLAN.md` - Agregat produktu + rozwijane podwiersze kampania/grupa w /products (completed 2026-04-25) -- `05-01-PLAN.md` - Usuwanie wpisow historii produktu per kampania+grupa z UI breakdown (completed 2026-04-29) -- `06-01-PLAN.md` - XML feed import + rename name/title + xml_feed_url w clients (completed 2026-04-30) - -## Decisions - -| Date | Decision | Phase | Impact | -|------|----------|-------|--------| -| 2026-04-24 | Usuniecie placeholdera zamiast sentinel `0/all` | 3 | Minimalny blast radius, bez zmian w kontrolerze/factory | -| 2026-04-25 | Bez wybranej grupy: glowny agregat per produkt + rozwijane podwiersze per kampania/grupa | 4 | Czytelniejsza analiza produktu i szybki drill-down | -| 2026-04-25 | Podwiersze tylko readonly, edycja tylko w parent row | 4 | Spojnosc UX i brak konfliktu akcji edycyjnych | -| 2026-04-29 | Usuwanie scope-level: products_aggregate + products_history + products_history_30 (transakcja), bez ruszania `products` ani Google Ads API | 5 | Hard delete lokalny, ograniczony do trojki product+campaign+ad_group | -| 2026-04-29 | UI: nowa kolumna "Akcje" w breakdown z czerwona ikona kosza + dialog $.confirm + ajax.reload(null,false) | 5 | Brak utraty stanu paginacji/filtrow przy odswiezaniu agregatu | -| 2026-04-30 | XML feed: jeden URL w clients.xml_feed_url, mapping po g:id=offer_id, parser XMLReader (streaming), batched manual upsert (200/batch) w transakcjach | 6 | Odporno na feedy z kilkoma tysiacami pozycji, niezalezne pola _gmc dla danych edytowalnych | -| 2026-04-30 | Refaktor schematu products: name->title, title->title_gmc, dodanie description_gmc i price | 6 | Rozdzial danych zrodlowych od edytowalnych | -| 2026-04-30 | Korekta semantyki: title/description = ZRODLOWE (z feedu lub pierwszy fetch GA), title_gmc/description_gmc = EDYTOWALNE (wysylane jako supplemental feed do GMC) | 6 | XmlFeedImporter pisze do title/description, edycja UI/AI pisze do _gmc, SupplementalFeed czyta z _gmc | -| 2026-04-30 | INDEX zamiast UNIQUE na (client_id, offer_id) | 6 | Istniejace dane mialy duplikaty - manual upsert (SELECT IN + UPDATE/INSERT) zamiast ON DUPLICATE KEY UPDATE | - -## Notes - -- PAUL framework dziala tutaj w trybie ad-hoc (bez ROADMAP.md i PROJECT.md). -- Plan 06-01 zostal zamkniety mimo legacy duplikatow w products - przyszle zadanie deduplikacji moze wymusic UNIQUE. -- Backfill historycznych edycji `description` -> `description_gmc` jest deferred (czeka na decyzje uzytkownika). +Stopped at: Plan 07-01 complete +Next action: paul_workflow('plan') for next phase +Resume file: .paul/phases/07-xml-feed-cron-refresh/07-01-SUMMARY.md \ No newline at end of file diff --git a/.paul/STATE.md.bak b/.paul/STATE.md.bak new file mode 100644 index 0000000..b6e5e50 --- /dev/null +++ b/.paul/STATE.md.bak @@ -0,0 +1,59 @@ +# STATE + +## Current Position + +Milestone: (ad-hoc) Products - XML feed import +Phase: 7 of 7 (XML Feed Cron Refresh) - Planning +Plan: 07-01 created, awaiting approval +Status: APPLY complete — 3/3 PASS, ready for UNIFY +Last activity: 2026-04-30T09:20:00+02:00 - Created .paul/phases/07-xml-feed-cron-refresh/07-01-PLAN.md + +Progress: +- Milestone: [#########-] 90% +- Phase 7: [----------] 0% + +## Loop Position + +Current loop state: +``` +PLAN --> APPLY --> UNIFY + * o o [Plan created, awaiting approval] +``` + +## Session Continuity + +Last session: 2026-04-30 +Stopped at: Plan 07-01 created +Next action: Review and approve plan, then run $paul-apply 07-01 +Resume file: .paul/phases/07-xml-feed-cron-refresh/07-01-PLAN.md + +## Historia zrealizowanych planow + +- `01-01-PLAN.md` - CL3 -> CL1 w tabeli /products (completed 2026-04-22) +- `02-01-PLAN.md` - custom_label_1 w supplemental feed TSV (completed 2026-04-22) +- `03-01-PLAN.md` - Powrot do widoku "wszystkie kampanie" w /products (completed 2026-04-24) +- `04-01-PLAN.md` - Agregat produktu + rozwijane podwiersze kampania/grupa w /products (completed 2026-04-25) +- `05-01-PLAN.md` - Usuwanie wpisow historii produktu per kampania+grupa z UI breakdown (completed 2026-04-29) +- `06-01-PLAN.md` - XML feed import + rename name/title + xml_feed_url w clients (completed 2026-04-30) + +## Decisions + +| Date | Decision | Phase | Impact | +|------|----------|-------|--------| +| 2026-04-24 | Usuniecie placeholdera zamiast sentinel `0/all` | 3 | Minimalny blast radius, bez zmian w kontrolerze/factory | +| 2026-04-25 | Bez wybranej grupy: glowny agregat per produkt + rozwijane podwiersze per kampania/grupa | 4 | Czytelniejsza analiza produktu i szybki drill-down | +| 2026-04-25 | Podwiersze tylko readonly, edycja tylko w parent row | 4 | Spojnosc UX i brak konfliktu akcji edycyjnych | +| 2026-04-29 | Usuwanie scope-level: products_aggregate + products_history + products_history_30 (transakcja), bez ruszania `products` ani Google Ads API | 5 | Hard delete lokalny, ograniczony do trojki product+campaign+ad_group | +| 2026-04-29 | UI: nowa kolumna "Akcje" w breakdown z czerwona ikona kosza + dialog $.confirm + ajax.reload(null,false) | 5 | Brak utraty stanu paginacji/filtrow przy odswiezaniu agregatu | +| 2026-04-30 | XML feed: jeden URL w clients.xml_feed_url, mapping po g:id=offer_id, parser XMLReader (streaming), batched manual upsert (200/batch) w transakcjach | 6 | Odporno na feedy z kilkoma tysiacami pozycji, niezalezne pola _gmc dla danych edytowalnych | +| 2026-04-30 | Refaktor schematu products: name->title, title->title_gmc, dodanie description_gmc i price | 6 | Rozdzial danych zrodlowych od edytowalnych | +| 2026-04-30 | Korekta semantyki: title/description = ZRODLOWE (z feedu lub pierwszy fetch GA), title_gmc/description_gmc = EDYTOWALNE (wysylane jako supplemental feed do GMC) | 6 | XmlFeedImporter pisze do title/description, edycja UI/AI pisze do _gmc, SupplementalFeed czyta z _gmc | +| 2026-04-30 | INDEX zamiast UNIQUE na (client_id, offer_id) | 6 | Istniejace dane mialy duplikaty - manual upsert (SELECT IN + UPDATE/INSERT) zamiast ON DUPLICATE KEY UPDATE | +| 2026-04-30 | XML feed ma byc osobnym cronem `/cron/cron_xml_feed_import`, niezaleznym od `cron_universal`, z recznym odswiezeniem w `/clients` | 7 | Czytelny harmonogram, brak dublowania importu z produktami Google Ads, latwiejsza diagnostyka feedu | + +## Notes + +- PAUL framework dziala tutaj w trybie ad-hoc (bez ROADMAP.md i PROJECT.md). +- Plan 06-01 zostal zamkniety mimo legacy duplikatow w products - przyszle zadanie deduplikacji moze wymusic UNIQUE. +- Backfill historycznych edycji `description` -> `description_gmc` jest deferred (czeka na decyzje uzytkownika). +- Plan 07-01 obejmuje wydzielenie XML feed z `cron_universal`, reczny refresh w `/clients` i diagnostyke `offer_id=2084` dla `pomysloweprezenty.pl`. diff --git a/.paul/changelog/2026-04-30.md b/.paul/changelog/2026-04-30.md index 63de7e2..551ed15 100644 --- a/.paul/changelog/2026-04-30.md +++ b/.paul/changelog/2026-04-30.md @@ -2,28 +2,16 @@ ## Co zrobiono -- [Phase 6, Plan 06-01] XML feed import per klient + refaktor schematu products (source/edit split) + integracja z cron_universal -- Dodano pole `xml_feed_url` w edycji klienta (templates/clients/main_view.php) z walidacja URL po stronie serwera -- Migracja 029: rename `products.name` -> `title`, `products.title` -> `title_gmc`, dodanie `description_gmc`, `price`, `clients.xml_feed_url`, `clients.xml_feed_last_sync_at`, INDEX `(client_id, offer_id)` (non-unique - legacy duplikaty) -- Nowy serwis `\services\XmlFeedImporter` - XMLReader streaming + batche 200 w transakcjach + manual upsert (SELECT IN + UPDATE/INSERT); odporny na feedy 5000+ pozycji -- Hook w `cron_universal` wywoluje XmlFeedImporter po sync produktow GA dla klientow z ustawionym feedem; raport `xml_feed` w response -- Refaktor wszystkich odwolan do kolumn `products.name`/`products.title` w factory, controls, services, templates, api.php; aliasy `AS name` zachowane jako kontrakt JS/API -- Korekta semantyki: `title`/`description` = ZRODLO (z feedu lub pierwszy fetch GA), `title_gmc`/`description_gmc` = EDYTOWALNE (wysylane do GMC supplemental feed) -- `is_product_core_field` zaktualizowane do `title_gmc`/`description_gmc` (edytowalne) -- `SupplementalFeed::generate_for_client` czyta `title_gmc AS title, description_gmc AS description` przy generowaniu TSV -- api.php (cztery endpointy products): SQL zaktualizowane, set_product_data zapisuje do title_gmc +- [07-xml-feed-cron-refresh, Plan 01] +- Task 1: Wydziel osobny cron XML feed +- Task 2: Dodaj reczne odswiezanie XML Feed w /clients +- Task 3: Zdiagnozuj i napraw custom_label_1 dla pomysloweprezenty.pl offer_id 2084 ## Zmienione pliki -- `migrations/029_products_rename_columns_and_xml_feed.sql` -- `autoload/services/class.XmlFeedImporter.php` -- `autoload/factory/class.Products.php` -- `autoload/controls/class.Products.php` - `autoload/controls/class.Cron.php` +- `autoload/controls/class.Users.php` - `autoload/controls/class.Clients.php` -- `autoload/services/class.SupplementalFeed.php` - `templates/clients/main_view.php` -- `api.php` -- `.paul/phases/06-xml-feed-import/06-01-PLAN.md` -- `.paul/phases/06-xml-feed-import/06-01-SUMMARY.md` -- `.paul/STATE.md` +- `autoload/services/class.XmlFeedImporter.php` +- `autoload/controls/class.Cron.php` \ No newline at end of file diff --git a/.paul/phases/07-xml-feed-cron-refresh/07-01-PLAN.md b/.paul/phases/07-xml-feed-cron-refresh/07-01-PLAN.md new file mode 100644 index 0000000..5a46a01 --- /dev/null +++ b/.paul/phases/07-xml-feed-cron-refresh/07-01-PLAN.md @@ -0,0 +1,208 @@ +--- +phase: 07-xml-feed-cron-refresh +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - autoload/controls/class.Cron.php + - autoload/controls/class.Clients.php + - autoload/controls/class.Users.php + - templates/clients/main_view.php + - autoload/services/class.XmlFeedImporter.php +autonomous: true +delegation: off +--- + + +## Goal +Wydzielic import danych produktow z XML feed klienta do osobnego crona `/cron/cron_xml_feed_import`, dodac reczne odswiezanie XML feed w `/clients` oraz zdiagnozowac i naprawic brak `custom_label_1` dla produktu `offer_id=2084` klienta `pomysloweprezenty.pl`. + +## Purpose +Import XML feed jest niezalezny od pobierania statystyk produktow z Google Ads. Trzymanie go w `cron_universal` utrudnia diagnostyke, harmonogram i reczne odswiezanie z poziomu klienta. Osobny cron daje kontrolowany punkt uruchomienia i jasny raport, a analiza `offer_id=2084` ma potwierdzic, czy problem wynika z endpointu, mapowania klienta, parsera XML, duplikatow w `products`, czy braku pola w feedzie. + +## Output +- Nowa akcja `\controls\Cron::cron_xml_feed_import()` dostepna jako `/cron/cron_xml_feed_import`. +- Usuniecie automatycznego importu XML z `cron_universal`. +- Nowa opcja w menu odswiezania klienta na `/clients`: XML Feed. +- `Clients::force_sync` uruchamia import XML natychmiast dla wskazanego klienta i zwraca raport JSON. +- Diagnostyka i ewentualny fix pobierania `custom_label_1` dla `pomysloweprezenty.pl`, `offer_id=2084`. + + + + +- **Endpoint** - Jaki endpoint ma miec osobny cron XML? + -> Odpowiedz: `/cron/cron_xml_feed_import`, metoda `\controls\Cron::cron_xml_feed_import()`. +- **Zakres diagnozy** - Czy produkt `offer_id=2084` klienta `pomysloweprezenty.pl` robimy w tym samym planie? + -> Odpowiedz: Tak, w tym samym planie. +- **Cron universal** - Czy zostawic import XML jako czesc `cron_universal`? + -> Odpowiedz: Nie. XML feed ma byc niezalezny od pobierania produktow. +- **UI clients** - Gdzie ma byc mozliwosc odswiezenia? + -> Odpowiedz: W `/clients`, w obecnym miejscu odswiezania danych klienta. + + +## Project Context +@.paul/STATE.md +@.paul/phases/06-xml-feed-import/06-01-SUMMARY.md +@CLAUDE.md + +## Source Files +@autoload/controls/class.Cron.php +@autoload/controls/class.Clients.php +@autoload/controls/class.Users.php +@templates/clients/main_view.php +@autoload/services/class.XmlFeedImporter.php +@autoload/factory/class.Clients.php + + + + +## AC-1: Osobny cron XML feed +```gherkin +Given klient ma ustawiony `clients.xml_feed_url` +When wywolam `/cron/cron_xml_feed_import` bez `client_id` +Then cron wybiera aktywnego klienta z `xml_feed_url` i uruchamia `XmlFeedImporter::import_for_client()` +And zwraca JSON z `client_id`, `feed_url`, `fetched`, `updated`, `inserted`, `skipped`, `errors`, `peak_memory_mb`, `duration_ms` +And ustawia `clients.xml_feed_last_sync_at` przez importer po udanym przebiegu +``` + +## AC-2: Reczne odswiezanie XML feed w /clients +```gherkin +Given jestem na `https://adspro.projectpro.pl/clients` +When rozwijam menu odswiezania przy kliencie z `xml_feed_url` +Then widze opcje `XML Feed` +When klikam `XML Feed` +Then aplikacja uruchamia import natychmiast dla tego klienta +And pokazuje komunikat z liczba pobranych/zaktualizowanych/dodanych/pominietych pozycji albo czytelny blad +``` + +## AC-3: XML feed niezalezny od cron_universal +```gherkin +Given uruchamia sie `/cron/cron_universal` +When cron synchronizuje kampanie i produkty Google Ads +Then nie wywoluje `XmlFeedImporter::import_for_client()` +And response `cron_universal` nie zawiera juz importu XML jako czesci sync produktow +And import XML mozna uruchomic tylko przez `/cron/cron_xml_feed_import` albo reczna akcje `/clients` +``` + +## AC-4: Diagnoza i fix custom_label_1 dla offer_id 2084 +```gherkin +Given klient `pomysloweprezenty.pl` ma skonfigurowany XML feed +When uruchomie import XML dla tego klienta +Then importer poprawnie odczytuje `g:custom_label_1` dla pozycji `g:id=2084`, jesli pole istnieje w feedzie +And rekord albo rekordy `products` tego klienta z `offer_id='2084'` maja uzupelnione `custom_label_1` +And jesli feed nie zawiera tej wartosci, raport diagnostyczny jasno wskazuje `custom_label_1_missing_in_feed` +``` + + + + + + + Task 1: Wydziel osobny cron XML feed + autoload/controls/class.Cron.php, autoload/controls/class.Users.php + + Dodaj publiczna metode `cron_xml_feed_import()` w `autoload/controls/class.Cron.php`. + + Wymagane zachowanie: + - Ustaw `self::$current_cron_action = __FUNCTION__` i wywolaj `self::touch_cron_invocation(__FUNCTION__)`. + - Obsluz `client_id` z requestu: jesli podany, importuj dokladnie tego aktywnego klienta z niepustym `xml_feed_url`. + - Bez `client_id` wybierz jednego aktywnego klienta z niepustym `xml_feed_url`, najlepiej po `xml_feed_last_sync_at ASC` z `NULL` jako pierwsze. To ogranicza czas jednego uruchomienia i pasuje do istniejacych cronow per klient. + - Uruchom `\services\XmlFeedImporter::import_for_client((int) $client['id'])` i zwroc `self::output_cron_response()` z raportem. + - Nie wymagaj Google Ads API ani Merchant API do importu XML. + - Dodaj endpoint do dashboardu cron w `autoload/controls/class.Users.php` jako `/cron/cron_xml_feed_import` z opisem harmonogramu. + + Usun wywolania `XmlFeedImporter::import_for_client()` z `cron_universal`, z obu sciezek: wymuszonego `client_id` i automatycznego pipeline, jezeli wystepuja. + + Avoid: laczenie statusu XML z `cron_sync_status` pipeline `products`, bo XML feed ma byc niezaleznym procesem. + + `php -l autoload/controls/class.Cron.php` i `php -l autoload/controls/class.Users.php`; grep `XmlFeedImporter::import_for_client` pokazuje wywolanie w `cron_xml_feed_import` i ewentualnie w `Clients::force_sync`, ale nie w `cron_universal`. + AC-1 i AC-3 satisfied + + + + Task 2: Dodaj reczne odswiezanie XML Feed w /clients + autoload/controls/class.Clients.php, templates/clients/main_view.php + + Rozszerz istniejace menu odswiezania w `templates/clients/main_view.php`: + - Dla klienta z niepustym `xml_feed_url` pokaz przycisk `XML Feed` w dropdownie `sync-dropdown-menu`. + - Uzyj istniejacego `syncFromMenu(id, pipeline, btn)` z nowym `pipeline = 'xml_feed'`. + - Dodaj etykiete `xml_feed: 'XML feedu'` w obiekcie `labels`. + - Dla odpowiedzi natychmiastowej pokaz komunikat z liczbami z raportu: fetched/updated/inserted/skipped; przy bledach pokaz czytelny komunikat. + + W `autoload/controls/class.Clients.php` znajdz metode `force_sync` i dodaj obsluge `pipeline === 'xml_feed'`: + - Waliduj klienta, aktywnosc i obecne `xml_feed_url`. + - Uruchom `\services\XmlFeedImporter::import_for_client($id)` natychmiast. + - Zwroc JSON `{ success: true, immediate: true, pipeline: 'xml_feed', report: ... }` albo `{ success: false, message: ... }`. + - Nie kolejkuj tego przez `cron_sync_status`, bo wymaganie mowi o niezaleznym odswiezeniu w `/clients`. + + Avoid: wymagania `google_ads_customer_id` dla przycisku XML feed. Do XML wystarczy aktywny klient i `xml_feed_url`. + + `php -l autoload/controls/class.Clients.php`; reczne klikniecie `/clients` -> XML Feed zwraca alert z raportem i aktualizuje `clients.xml_feed_last_sync_at`. + AC-2 satisfied + + + + Task 3: Zdiagnozuj i napraw custom_label_1 dla pomysloweprezenty.pl offer_id 2084 + autoload/services/class.XmlFeedImporter.php, autoload/controls/class.Cron.php + + Dodaj minimalna diagnostyke ukierunkowana na przypadki braku pola po imporcie, bez stalego logowania kazdego produktu: + - Dodaj opcjonalny parametr requestu dla crona, np. `debug_offer_id=2084` albo `offer_id=2084`, ktory przekazesz do importera tylko w raporcie diagnostycznym. + - W `XmlFeedImporter` dodaj lekka obsluge diagnostyczna: kiedy parsowany `offer_id` odpowiada debugowanemu ID, zapisz w raporcie `debug_offer` z polami: `found_in_feed`, `custom_label_1_raw_present`, `custom_label_1_value`, `title_present`, `matched_existing_rows`, `updated_rows`. + - Sprawdz parser `extract_item_fields()`: ma obslugiwac `g:custom_label_1` w namespace `http://base.google.com/ns/1.0`; jesli realny feed uzywa innej formy (np. bez namespace albo z prefiksem zachowanym jako nazwa), dodaj punktowa obsluge fallback bez ladowania calego XML do pamieci. + - Sprawdz `flush_batch()`: dla istniejacych rekordow z `offer_id=2084` `custom_label_1` ma sie aktualizowac, gdy wartosc z feedu nie jest pusta. Jesli feed ma pusta wartosc, nie nadpisuj istniejacej wartosci pustka. + - Jesli klient `pomysloweprezenty.pl` ma wiele rekordow `products` z tym samym `offer_id`, zachowaj aktualne zalozenie z fazy 06: aktualizuj wszystkie legacy duplikaty. + + Po implementacji uruchom lub opisz weryfikacje: + `/cron/cron_xml_feed_import/client_id=&debug_offer_id=2084` + i sprawdz w raporcie, czy `debug_offer` wskazuje przyczyne. + + Avoid: hardcodowania domeny `pomysloweprezenty.pl` albo `2084` w logice produkcyjnej. To ma byc parametr diagnostyczny. + + `php -l autoload/services/class.XmlFeedImporter.php`; import dla klienta `pomysloweprezenty.pl` z `debug_offer_id=2084` zwraca `debug_offer`; po imporcie SELECT po `client_id` i `offer_id='2084'` pokazuje uzupelnione `custom_label_1` albo raport jasno pokazuje brak wartosci w feedzie. + AC-4 satisfied + + + + + + +## DO NOT CHANGE +- `migrations/001`-`029` bez wyraznej potrzeby; ten plan nie wymaga zmiany schematu. +- Semantyka z fazy 06: `title`/`description` sa danymi zrodlowymi, `title_gmc`/`description_gmc` sa edytowalne i ida do supplemental feed. +- Importer nie moze nadpisywac `custom_label_1` pustka z feedu. +- Nie laczyc XML feed z Google Ads API ani Merchant API. + +## SCOPE LIMITS +- Brak nowego dashboardu historii importow XML. +- Brak kolejki `cron_sync_status` dla XML feed. +- Brak deduplikacji `products` dla legacy duplikatow `(client_id, offer_id)`. +- Brak nowych pol z feedu poza diagnostyka `custom_label_1`. +- Brak przebudowy calego UI `/clients`; tylko dodatkowa opcja w istniejacym dropdownie. + + + + +Before declaring plan complete: +- [ ] `php -l autoload/controls/class.Cron.php` +- [ ] `php -l autoload/controls/class.Clients.php` +- [ ] `php -l autoload/services/class.XmlFeedImporter.php` +- [ ] `rg -n "XmlFeedImporter::import_for_client" autoload/controls/class.Cron.php autoload/controls/class.Clients.php` potwierdza brak wywolania w `cron_universal` +- [ ] `/cron/cron_xml_feed_import?client_id=` zwraca raport importu bez wymagania Google Ads API +- [ ] `/clients` pokazuje opcje `XML Feed` dla klienta z `xml_feed_url` +- [ ] Klikniecie `XML Feed` w `/clients` wykonuje import natychmiast i pokazuje raport +- [ ] Debug `offer_id=2084` dla `pomysloweprezenty.pl` wskazuje przyczyne braku `custom_label_1` albo naprawia zapis +- [ ] Wszystkie AC-1 do AC-4 spelnione + + + +- XML feed ma osobny endpoint cron i nie jest czescia `cron_universal`. +- Klient moze recznie odswiezyc XML feed z `/clients`. +- Import XML dziala bez konfiguracji Google Ads/Merchant API. +- Problem `custom_label_1` dla `offer_id=2084` jest zdiagnozowany i naprawiony, jezeli wartosc istnieje w feedzie. +- Brak regresji w dotychczasowym pobieraniu kampanii i produktow Google Ads. + + + +After completion, create `.paul/phases/07-xml-feed-cron-refresh/07-01-SUMMARY.md` + diff --git a/.paul/phases/07-xml-feed-cron-refresh/07-01-SUMMARY.md b/.paul/phases/07-xml-feed-cron-refresh/07-01-SUMMARY.md new file mode 100644 index 0000000..1236529 --- /dev/null +++ b/.paul/phases/07-xml-feed-cron-refresh/07-01-SUMMARY.md @@ -0,0 +1,40 @@ +--- +phase: 07-xml-feed-cron-refresh +plan: 01 +completed: 2026-04-30T07:19:05.331Z +--- + +# Phase 07-01 Summary + +**** + +## Acceptance Criteria Results + +| Criterion | Status | +|-----------|--------| +| Task 1: Wydziel osobny cron XML feed | Pass — Dodano Cron::cron_xml_feed_import, endpoint w dashboardzie cron, usunieto wywolanie XmlFeedImporter z cron_universal. Lint OK; grep potwierdza wywolania tylko w nowym cronie i Clients::force_sync. | +| Task 2: Dodaj reczne odswiezanie XML Feed w /clients | Pass — Dodano opcje XML Feed w dropdownie /clients oraz obsluge pipeline xml_feed w Clients::force_sync z natychmiastowym importem i raportem. Lint OK. | +| Task 3: Zdiagnozuj i napraw custom_label_1 dla pomysloweprezenty.pl offer_id 2084 | Pass — Dodano debug_offer_id/offer_id, raport debug_offer, fallback odczytu custom_label_1 po localName oraz metryki matched_existing_rows/updated_rows. Runtime import/SELECT nie zostal wykonany, bo lokalny MySQL odmawia polaczenia. | + +## Accomplishments + +- Task 1: Wydziel osobny cron XML feed: Dodano Cron::cron_xml_feed_import, endpoint w dashboardzie cron, usunieto wywolanie XmlFeedImporter z cron_universal. Lint OK; grep potwierdza wywolania tylko w nowym cronie i Clients::force_sync. +- Task 2: Dodaj reczne odswiezanie XML Feed w /clients: Dodano opcje XML Feed w dropdownie /clients oraz obsluge pipeline xml_feed w Clients::force_sync z natychmiastowym importem i raportem. Lint OK. +- Task 3: Zdiagnozuj i napraw custom_label_1 dla pomysloweprezenty.pl offer_id 2084: Dodano debug_offer_id/offer_id, raport debug_offer, fallback odczytu custom_label_1 po localName oraz metryki matched_existing_rows/updated_rows. Runtime import/SELECT nie zostal wykonany, bo lokalny MySQL odmawia polaczenia. + +## Files Modified + +- `autoload/controls/class.Cron.php` +- `autoload/controls/class.Users.php` +- `autoload/controls/class.Clients.php` +- `templates/clients/main_view.php` +- `autoload/services/class.XmlFeedImporter.php` +- `autoload/controls/class.Cron.php` + +## Deviations + +Runtime verification against local DB was not possible: local MySQL refused connection, so `/cron/cron_xml_feed_import?client_id=&debug_offer_id=2084` and SELECT for offer_id=2084 must be verified on the target environment. + +--- +*Phase: 07-xml-feed-cron-refresh, Plan: 01* +*Completed: 2026-04-30* \ No newline at end of file diff --git a/.vscode/ftp-kr.sync.cache.json b/.vscode/ftp-kr.sync.cache.json index b271882..6255209 100644 --- a/.vscode/ftp-kr.sync.cache.json +++ b/.vscode/ftp-kr.sync.cache.json @@ -9,8 +9,8 @@ }, "api.php": { "type": "-", - "size": 30668, - "lmtime": 1777417113752, + "size": 33086, + "lmtime": 1777532181358, "modified": false }, "autoload": { @@ -95,14 +95,14 @@ }, "class.Clients.php": { "type": "-", - "size": 13909, - "lmtime": 1772117575587, + "size": 14217, + "lmtime": 1777532181359, "modified": false }, "class.Cron.php": { "type": "-", - "size": 184760, - "lmtime": 1774557774097, + "size": 185385, + "lmtime": 1777532181362, "modified": false }, "class.FacebookAds.php": { @@ -125,8 +125,8 @@ }, "class.Products.php": { "type": "-", - "size": 52633, - "lmtime": 1776810435280, + "size": 55416, + "lmtime": 1777532181364, "modified": false }, "class.Site.php": { @@ -193,8 +193,8 @@ }, "class.Products.php": { "type": "-", - "size": 44169, - "lmtime": 1776810339287, + "size": 47877, + "lmtime": 1777532181365, "modified": false }, "class.Users.php": { @@ -243,8 +243,14 @@ }, "class.SupplementalFeed.php": { "type": "-", - "size": 10533, - "lmtime": 1776844880606, + "size": 10874, + "lmtime": 1777532181367, + "modified": false + }, + "class.XmlFeedImporter.php": { + "type": "-", + "size": 11131, + "lmtime": 1777532181367, "modified": false } }, @@ -731,17 +737,23 @@ "lmtime": 1772671313573, "modified": false }, - "demo_data.sql": { - "type": "-", - "size": 21146, - "lmtime": 0, - "modified": true - }, "028_products_custom_label_1.sql": { "type": "-", "size": 527, "lmtime": 1776810261993, "modified": false + }, + "029_products_rename_columns_and_xml_feed.sql": { + "type": "-", + "size": 4361, + "lmtime": 1777532181369, + "modified": false + }, + "demo_data.sql": { + "type": "-", + "size": 21146, + "lmtime": 0, + "modified": true } }, ".paul": { @@ -757,6 +769,24 @@ "size": 742, "lmtime": 1777064721127, "modified": false + }, + "2026-04-25.md": { + "type": "-", + "size": 439, + "lmtime": 0, + "modified": false + }, + "2026-04-29.md": { + "type": "-", + "size": 1140, + "lmtime": 1777532181346, + "modified": false + }, + "2026-04-30.md": { + "type": "-", + "size": 1995, + "lmtime": 1777532181346, + "modified": false } }, "docs": { @@ -815,6 +845,18 @@ "size": 1614, "lmtime": 1777064743676, "modified": false + }, + "governance_2026-04-29.jsonl": { + "type": "-", + "size": 5115, + "lmtime": 1777532181347, + "modified": false + }, + "governance_2026-04-30.jsonl": { + "type": "-", + "size": 14402, + "lmtime": 1777532181349, + "modified": false } }, "phases": { @@ -859,12 +901,41 @@ "lmtime": 1777064708436, "modified": false } + }, + "04-products-aggregate-breakdown": {}, + "05-products-scope-history-delete": { + "05-01-PLAN.md": { + "type": "-", + "size": 17658, + "lmtime": 1777532181351, + "modified": false + }, + "05-01-SUMMARY.md": { + "type": "-", + "size": 8203, + "lmtime": 1777532181351, + "modified": false + } + }, + "06-xml-feed-import": { + "06-01-PLAN.md": { + "type": "-", + "size": 19931, + "lmtime": 1777532181353, + "modified": false + }, + "06-01-SUMMARY.md": { + "type": "-", + "size": 11842, + "lmtime": 1777532181354, + "modified": false + } } }, "STATE.md": { "type": "-", - "size": 2531, - "lmtime": 1777064743120, + "size": 3487, + "lmtime": 1777532181345, "modified": false } }, @@ -897,8 +968,8 @@ "clients": { "main_view.php": { "type": "-", - "size": 16977, - "lmtime": 1772117630298, + "size": 17449, + "lmtime": 1777532181370, "modified": false } }, @@ -923,8 +994,8 @@ "products": { "main_view.php": { "type": "-", - "size": 83538, - "lmtime": 1777064546782, + "size": 92780, + "lmtime": 1777532181372, "modified": false }, "product_history.php": { diff --git a/autoload/controls/class.Clients.php b/autoload/controls/class.Clients.php index ea96d5a..61a6f0f 100644 --- a/autoload/controls/class.Clients.php +++ b/autoload/controls/class.Clients.php @@ -358,7 +358,7 @@ class Clients $deleted_select = self::clients_has_deleted_column() ? 'COALESCE(deleted, 0) AS deleted' : '0 AS deleted'; $client = $mdb -> query( - "SELECT id, COALESCE(active, 0) AS active, " . $deleted_select . ", google_ads_customer_id, google_merchant_account_id, facebook_ads_account_id + "SELECT id, COALESCE(active, 0) AS active, " . $deleted_select . ", google_ads_customer_id, google_merchant_account_id, facebook_ads_account_id, xml_feed_url FROM clients WHERE id = :id LIMIT 1", @@ -440,6 +440,33 @@ class Clients exit; } } + else if ( $pipeline === 'xml_feed' ) + { + $xml_feed_url = trim( (string) ( $client['xml_feed_url'] ?? '' ) ); + if ( $xml_feed_url === '' ) + { + echo json_encode( [ 'success' => false, 'message' => 'Klient nie ma ustawionego XML Feed URL.' ] ); + exit; + } + + try + { + $report = \services\XmlFeedImporter::import_for_client( $id ); + echo json_encode( [ + 'success' => empty( $report['errors'] ), + 'pipeline' => 'xml_feed', + 'immediate' => true, + 'report' => $report, + 'message' => !empty( $report['errors'] ) ? implode( '; ', array_slice( (array) $report['errors'], 0, 3 ) ) : '' + ] ); + exit; + } + catch ( \Throwable $e ) + { + echo json_encode( [ 'success' => false, 'message' => 'Blad importu XML feed: ' . $e -> getMessage() ] ); + exit; + } + } else { // Domyslny reset (wszystkie pipeline oparte o cron_sync_status). diff --git a/autoload/controls/class.Cron.php b/autoload/controls/class.Cron.php index 98a6670..db130bb 100644 --- a/autoload/controls/class.Cron.php +++ b/autoload/controls/class.Cron.php @@ -132,23 +132,6 @@ class Cron $products_temp_rows_total += (int) self::rebuild_products_temp_for_client( (int) $client['id'] ); } - $xml_feed_report = null; - if ( !empty( $client['xml_feed_url'] ) ) - { - try - { - $xml_feed_report = \services\XmlFeedImporter::import_for_client( (int) $client['id'] ); - if ( !empty( $xml_feed_report['errors'] ) ) - { - $products_errors = array_merge( $products_errors, (array) $xml_feed_report['errors'] ); - } - } - catch ( \Throwable $e ) - { - $products_errors[] = 'XML feed import: ' . $e -> getMessage(); - } - } - $errors = array_merge( $campaign_errors, $products_errors ); self::output_cron_response( [ @@ -169,7 +152,6 @@ class Cron 'products_fetch_skipped_reasons' => array_keys( $products_fetch_skipped_reasons ), 'history_30_products' => $history_30_products_total, 'products_temp_rows' => $products_temp_rows_total, - 'xml_feed' => $xml_feed_report, 'errors' => $errors ] ); } @@ -567,6 +549,87 @@ class Cron ] ); } + static public function cron_xml_feed_import() + { + global $mdb; + + self::$current_cron_action = __FUNCTION__; + self::touch_cron_invocation( __FUNCTION__ ); + + $clients_not_deleted_sql_c = self::sql_clients_not_deleted( 'c' ); + $client_id = (int) \S::get( 'client_id' ); + $debug_offer_id = trim( (string) \S::get( 'debug_offer_id' ) ); + if ( $debug_offer_id === '' ) + { + $debug_offer_id = trim( (string) \S::get( 'offer_id' ) ); + } + + $params = []; + $where_client = ''; + if ( $client_id > 0 ) + { + $where_client = 'AND c.id = :client_id'; + $params[':client_id'] = $client_id; + } + + $client = $mdb -> query( + "SELECT c.id, c.name, c.xml_feed_url, c.xml_feed_last_sync_at + FROM clients c + WHERE " . $clients_not_deleted_sql_c . " + AND COALESCE(c.active, 0) = 1 + AND TRIM(COALESCE(c.xml_feed_url, '')) <> '' + " . $where_client . " + ORDER BY CASE WHEN c.xml_feed_last_sync_at IS NULL THEN 0 ELSE 1 END ASC, + c.xml_feed_last_sync_at ASC, + c.id ASC + LIMIT 1", + $params + ) -> fetch( \PDO::FETCH_ASSOC ); + + if ( !$client ) + { + self::output_cron_response( [ + 'result' => $client_id > 0 ? 'Nie znaleziono aktywnego klienta z ustawionym XML Feed URL.' : 'Brak aktywnych klientow z ustawionym XML Feed URL.', + 'client_id' => $client_id ?: null, + 'errors' => [] + ] ); + } + + try + { + $report = \services\XmlFeedImporter::import_for_client( + (int) $client['id'], + [ 'debug_offer_id' => $debug_offer_id ] + ); + } + catch ( \Throwable $e ) + { + self::output_cron_response( [ + 'result' => 'Import XML feed zakonczony bledem.', + 'client_id' => (int) $client['id'], + 'client_name' => (string) ( $client['name'] ?? '' ), + 'feed_url' => (string) ( $client['xml_feed_url'] ?? '' ), + 'errors' => [ $e -> getMessage() ] + ] ); + } + + $errors = (array) ( $report['errors'] ?? [] ); + self::output_cron_response( [ + 'result' => empty( $errors ) ? 'Import XML feed zakonczony.' : 'Import XML feed zakonczony z bledami.', + 'client_id' => (int) $client['id'], + 'client_name' => (string) ( $client['name'] ?? '' ), + 'feed_url' => (string) ( $report['feed_url'] ?? ( $client['xml_feed_url'] ?? '' ) ), + 'fetched' => (int) ( $report['fetched'] ?? 0 ), + 'updated' => (int) ( $report['updated'] ?? 0 ), + 'inserted' => (int) ( $report['inserted'] ?? 0 ), + 'skipped' => (int) ( $report['skipped'] ?? 0 ), + 'peak_memory_mb' => (float) ( $report['peak_memory_mb'] ?? 0 ), + 'duration_ms' => (int) ( $report['duration_ms'] ?? 0 ), + 'debug_offer' => $report['debug_offer'] ?? null, + 'errors' => $errors + ] ); + } + static public function cron_products_urls() { global $mdb, $settings; diff --git a/autoload/controls/class.Users.php b/autoload/controls/class.Users.php index 4296b1c..c4ee2d0 100644 --- a/autoload/controls/class.Users.php +++ b/autoload/controls/class.Users.php @@ -376,6 +376,7 @@ class Users $cron_endpoints = [ [ 'name' => 'Legacy CRON', 'path' => '/cron.php', 'action' => 'cron_legacy', 'plan' => '' ], [ 'name' => 'Cron uniwersalny (Google Ads)', 'path' => '/cron/cron_universal', 'action' => 'cron_universal', 'plan' => 'Co 1 min: kampanie (wczoraj) + frazy/produkty (7 dni wstecz) + Merchant URL + supplemental feed (raz dziennie)' ], + [ 'name' => 'Cron XML Feed produktow', 'path' => '/cron/cron_xml_feed_import', 'action' => 'cron_xml_feed_import', 'plan' => 'Co 5-15 min: jeden aktywny klient z XML Feed URL, niezaleznie od Google Ads' ], [ 'name' => 'Cron alertow kampanii (Merchant)', 'path' => '/cron/cron_campaigns_product_alerts_merchant', 'action' => 'cron_campaigns_product_alerts_merchant', 'plan' => 'Co 15 min: alerty produktowe z Google Merchant' ], [ 'name' => 'Cron URL produktów (Merchant)', 'path' => '/cron/cron_products_urls', 'action' => 'cron_products_urls', 'plan' => '' ], [ 'name' => 'Cron Facebook Ads', 'path' => '/cron/cron_facebook_ads', 'action' => 'cron_facebook_ads', 'plan' => 'Co 5 min: 30 dni wstecz od wczoraj, blokada ponownego pobrania w tym samym dniu' ], diff --git a/autoload/services/class.XmlFeedImporter.php b/autoload/services/class.XmlFeedImporter.php index 08f650e..8589234 100644 --- a/autoload/services/class.XmlFeedImporter.php +++ b/autoload/services/class.XmlFeedImporter.php @@ -18,11 +18,12 @@ class XmlFeedImporter * @param int $client_id * @return array raport z polami: feed_url, fetched, updated, inserted, skipped, errors, peak_memory_mb, duration_ms */ - static public function import_for_client( $client_id ) + static public function import_for_client( $client_id, array $options = [] ) { global $mdb; $client_id = (int) $client_id; + $debug_offer_id = trim( (string) ( $options['debug_offer_id'] ?? '' ) ); $report = [ 'feed_url' => '', 'fetched' => 0, @@ -33,6 +34,18 @@ class XmlFeedImporter 'peak_memory_mb' => 0, 'duration_ms' => 0, ]; + if ( $debug_offer_id !== '' ) + { + $report['debug_offer'] = [ + 'offer_id' => $debug_offer_id, + 'found_in_feed' => false, + 'custom_label_1_raw_present' => false, + 'custom_label_1_value' => '', + 'title_present' => false, + 'matched_existing_rows' => 0, + 'updated_rows' => 0, + ]; + } if ( $client_id <= 0 ) { @@ -105,6 +118,22 @@ class XmlFeedImporter continue; } + if ( $debug_offer_id !== '' && (string) $item['offer_id'] === $debug_offer_id ) + { + $report['debug_offer']['found_in_feed'] = true; + $report['debug_offer']['custom_label_1_raw_present'] = !empty( $item['custom_label_1_raw_present'] ); + $report['debug_offer']['custom_label_1_value'] = (string) ( $item['custom_label_1'] ?? '' ); + $report['debug_offer']['title_present'] = trim( (string) ( $item['title'] ?? '' ) ) !== ''; + if ( !$report['debug_offer']['custom_label_1_raw_present'] ) + { + $report['debug_offer']['missing_reason'] = 'custom_label_1_missing_in_feed'; + } + else if ( trim( (string) $item['custom_label_1'] ) === '' ) + { + $report['debug_offer']['missing_reason'] = 'custom_label_1_empty_in_feed'; + } + } + $report['fetched']++; $batch[] = $item; @@ -134,6 +163,11 @@ class XmlFeedImporter $reader -> close(); @unlink( $tmp_file ); + if ( $debug_offer_id !== '' && empty( $report['debug_offer']['found_in_feed'] ) ) + { + $report['debug_offer']['missing_reason'] = 'offer_id_missing_in_feed'; + } + $mdb -> update( 'clients', [ 'xml_feed_last_sync_at' => date( 'Y-m-d H:i:s' ) ], [ 'id' => $client_id ] ); $report['peak_memory_mb'] = round( memory_get_peak_usage( true ) / 1024 / 1024, 1 ); @@ -233,7 +267,22 @@ class XmlFeedImporter $description = trim( (string) $sxe -> description ); } - $custom_label_1 = isset( $g -> custom_label_1 ) ? trim( (string) $g -> custom_label_1 ) : ''; + $custom_label_1_raw_present = isset( $g -> custom_label_1 ); + $custom_label_1 = $custom_label_1_raw_present ? trim( (string) $g -> custom_label_1 ) : ''; + if ( !$custom_label_1_raw_present && isset( $sxe -> custom_label_1 ) ) + { + $custom_label_1_raw_present = true; + $custom_label_1 = trim( (string) $sxe -> custom_label_1 ); + } + if ( !$custom_label_1_raw_present ) + { + $custom_label_1_fallback = self::get_text_by_local_name( $doc, 'custom_label_1' ); + if ( $custom_label_1_fallback !== null ) + { + $custom_label_1_raw_present = true; + $custom_label_1 = trim( (string) $custom_label_1_fallback ); + } + } $price = null; if ( isset( $g -> price ) ) @@ -251,10 +300,25 @@ class XmlFeedImporter 'title' => self::truncate( $title, 255 ), 'description' => $description, 'custom_label_1' => self::truncate( $custom_label_1, 255 ), + 'custom_label_1_raw_present' => $custom_label_1_raw_present, 'price' => $price, ]; } + static private function get_text_by_local_name( \DOMDocument $doc, $local_name ) + { + $local_name = (string) $local_name; + foreach ( $doc -> getElementsByTagName( '*' ) as $node ) + { + if ( $node -> localName === $local_name ) + { + return $node -> textContent; + } + } + + return null; + } + static private function parse_price( $raw ) { if ( $raw === '' ) @@ -341,15 +405,22 @@ class XmlFeedImporter $updated_count = 0; $inserted_count = 0; + $debug_offer_id = (string) ( $report['debug_offer']['offer_id'] ?? '' ); + foreach ( $batch as $item ) { $title = $item['title'] !== '' ? $item['title'] : null; $desc = $item['description'] !== '' ? $item['description'] : null; $cl1 = $item['custom_label_1'] !== '' ? $item['custom_label_1'] : null; $price = $item['price']; + $is_debug_offer = $debug_offer_id !== '' && (string) $item['offer_id'] === $debug_offer_id; if ( !empty( $existing[ $item['offer_id'] ] ) ) { + if ( $is_debug_offer ) + { + $report['debug_offer']['matched_existing_rows'] = count( $existing[ $item['offer_id'] ] ); + } // aktualizujemy WSZYSTKIE legacy duplikaty (utrzymujemy spojnosc danych) foreach ( $existing[ $item['offer_id'] ] as $row_id ) { @@ -361,6 +432,10 @@ class XmlFeedImporter ':id' => $row_id, ] ); $updated_count++; + if ( $is_debug_offer ) + { + $report['debug_offer']['updated_rows']++; + } } } else @@ -374,6 +449,10 @@ class XmlFeedImporter ':price' => $price, ] ); $inserted_count++; + if ( $is_debug_offer ) + { + $report['debug_offer']['inserted'] = true; + } } } diff --git a/templates/clients/main_view.php b/templates/clients/main_view.php index 6f029c4..710af03 100644 --- a/templates/clients/main_view.php +++ b/templates/clients/main_view.php @@ -94,6 +94,11 @@ Facebook Ads + + +