diff --git a/.paul/PROJECT.md b/.paul/PROJECT.md
index f5f3f76..bbb8b9d 100644
--- a/.paul/PROJECT.md
+++ b/.paul/PROJECT.md
@@ -1,12 +1,12 @@
-# shopPRO
+# shopPRO
## What This Is
-Autorski silnik sklepu internetowego pisany od podstaw — odpowiednik WooCommerce lub PrestaShop, ale bez zależności od zewnętrznych platform. Składa się z panelu administratora (zarządzanie zamówieniami, produktami, klientami) oraz części frontowej dla klienta końcowego.
+Autorski silnik sklepu internetowego pisany od podstaw — odpowiednik WooCommerce lub PrestaShop, ale bez zależności od zewnętrznych platform. Składa się z panelu administratora (zarządzanie zamówieniami, produktami, klientami) oraz części frontowej dla klienta końcowego.
## Core Value
-Właściciel sklepu internetowego ma pełną kontrolę nad sprzedażą online — produktami, zamówieniami i klientami — w jednym spójnym systemie pisanym od podstaw, bez narzutów zewnętrznych platform.
+Właściciel sklepu internetowego ma pełną kontrolę nad sprzedażą online — produktami, zamówieniami i klientami — w jednym spójnym systemie pisanym od podstaw, bez narzutów zewnętrznych platform.
## Current State
@@ -14,22 +14,22 @@ Właściciel sklepu internetowego ma pełną kontrolę nad sprzedażą online
|-----------|-------|
| Version | 0.333 |
| Status | Production |
-| Last Updated | 2026-03-12 |
+| Last Updated | 2026-04-18 |
## Requirements
### Validated (Shipped)
-- [x] Panel administratora — zarządzanie produktami, kategoriami, atrybutami
-- [x] Panel administratora — zarządzanie zamówieniami
-- [x] Panel administratora — zarządzanie klientami
-- [x] Część frontowa — przeglądanie i kupowanie produktów
-- [x] Koszyk i składanie zamówień
-- [x] Integracje płatności i dostaw
+- [x] Panel administratora — zarządzanie produktami, kategoriami, atrybutami
+- [x] Panel administratora — zarządzanie zamówieniami
+- [x] Panel administratora — zarządzanie klientami
+- [x] Część frontowa — przeglądanie i kupowanie produktów
+- [x] Koszyk i składanie zamówień
+- [x] Integracje płatności i dostaw
- [x] REST API (ordersPRO + Ekomi)
- [x] Redis caching
-- [x] Ochrona przed podwójnym składaniem zamówienia
-- [x] Domain-Driven Architecture (migracja z legacy zakończona)
+- [x] Ochrona przed podwójnym składaniem zamówienia
+- [x] Domain-Driven Architecture (migracja z legacy zakończona)
### Active (In Progress)
@@ -41,16 +41,16 @@ Właściciel sklepu internetowego ma pełną kontrolę nad sprzedażą online
### Out of Scope
-- Multitenancy (wiele sklepów w jednej instancji) — nie planowane
+- Multitenancy (wiele sklepów w jednej instancji) — nie planowane
## Target Users
-**Primary:** Właściciel/administrator sklepu internetowego
-- Zarządza produktami, zamówieniami, klientami przez panel admina
-- Potrzebuje niezawodnego, szybkiego narzędzia bez zbędnych zależności
+**Primary:** Właściciel/administrator sklepu internetowego
+- ZarzÄ…dza produktami, zamĂłwieniami, klientami przez panel admina
+- Potrzebuje niezawodnego, szybkiego narzędzia bez zbędnych zależności
-**Secondary:** Klient końcowy sklepu
-- Przegląda produkty, dodaje do koszyka, składa zamówienia
+**Secondary:** Klient końcowy sklepu
+- Przegląda produkty, dodaje do koszyka, składa zamówienia
## Context
@@ -58,26 +58,27 @@ Właściciel sklepu internetowego ma pełną kontrolę nad sprzedażą online
- PHP 7.4+ (produkcja: PHP < 8.0)
- Medoo ORM (`$mdb`), Redis caching
- Domain-Driven Design z Dependency Injection
-- PHPUnit 9.6, 810+ testów
+- PHPUnit 9.6, 810+ testĂłw
- Namespace: `\Domain\`, `\admin\`, `\front\`, `\api\`, `\Shared\`
## Constraints
### Technical Constraints
- PHP < 8.0 na produkcji (brak `match`, named arguments, union types)
-- Medoo ORM — prepared statements bez wyjątków
+- Medoo ORM — prepared statements bez wyjątków
- Redis wymagany dla cache
### Business Constraints
-- System wdrażany u klientów jako update package (ZIP)
+- System wdraĹĽany u klientĂłw jako update package (ZIP)
## Key Decisions
| Decision | Rationale | Date | Status |
|----------|-----------|------|--------|
-| DDD + DI zamiast legacy architektury | Testowalność, separacja odpowiedzialności | 2025 | Active |
-| PHP < 8.0 kompatybilność | Klienci na starszych serwerach | 2025 | Active |
-| Własny silnik zamiast frameworka | Pełna kontrola, brak narzutów | - | Active |
+| DDD + DI zamiast legacy architektury | Testowalność, separacja odpowiedzialności | 2025 | Active |
+| PHP < 8.0 kompatybilność | Klienci na starszych serwerach | 2025 | Active |
+| Własny silnik zamiast frameworka | Pełna kontrola, brak narzutów | - | Active |
+| `id` w tabbed FormEdit przez `hiddenFields` | Zapobiega insert zamiast update przy edycji encji | 2026-04-18 | Active |
## Success Metrics
@@ -93,7 +94,7 @@ Właściciel sklepu internetowego ma pełną kontrolę nad sprzedażą online
| Backend | PHP 7.4+ | < 8.0 na produkcji |
| ORM | Medoo | `$mdb` global |
| Cache | Redis | CacheHandler singleton |
-| Frontend | HTML/CSS/JS | Własny silnik szablonów (Tpl) |
+| Frontend | HTML/CSS/JS | Własny silnik szablonów (Tpl) |
| Auth | Sesje PHP | CSRF, XSS protection |
| Testy | PHPUnit 9.6 | phpunit.phar |
@@ -102,14 +103,15 @@ Właściciel sklepu internetowego ma pełną kontrolę nad sprzedażą online
See: .paul/SPECIAL-FLOWS.md
Quick Reference:
-- /feature-dev → Nowe funkcje, większe zmiany (required)
-- /koniec-pracy → Release, update package (required)
-- /frontend-design → Komponenty UI, szablony widoków
-- /code-review → Przegląd kodu przed release
-- /simplify → Upraszczanie po implementacji
-- /claude-md-improver → Utrzymanie CLAUDE.md
-- /zapisz + /wznow → Zapis i wznowienie sesji
+- /feature-dev → Nowe funkcje, większe zmiany (required)
+- /koniec-pracy → Release, update package (required)
+- /frontend-design → Komponenty UI, szablony widoków
+- /code-review → Przegląd kodu przed release
+- /simplify → Upraszczanie po implementacji
+- /claude-md-improver → Utrzymanie CLAUDE.md
+- /zapisz + /wznow → Zapis i wznowienie sesji
---
-*PROJECT.md — Updated when requirements or context change*
-*Last updated: 2026-03-12*
+*PROJECT.md — Updated when requirements or context change*
+*Last updated: 2026-04-18 after Phase 15*
+
diff --git a/.paul/ROADMAP.md b/.paul/ROADMAP.md
index 7895497..b0c12ae 100644
--- a/.paul/ROADMAP.md
+++ b/.paul/ROADMAP.md
@@ -1,14 +1,14 @@
-# Roadmap: shopPRO
+# Roadmap: shopPRO
## Overview
-shopPRO to autorski silnik sklepu internetowego rozwijany iteracyjnie. Projekt jest już na produkcji (v0.333) — roadmap obejmuje planowane funkcje i usprawnienia kolejnych wersji.
+shopPRO to autorski silnik sklepu internetowego rozwijany iteracyjnie. Projekt jest już na produkcji (v0.333) — roadmap obejmuje planowane funkcje i usprawnienia kolejnych wersji.
## Current Milestone
-**Security hardening** (v0.33x)
-Status: In progress
-Phases: 3 of 4 complete
+**Hotfix backlog**
+Status: Complete
+Phases: 4 of 4 complete
## Phases
@@ -16,26 +16,27 @@ Phases: 3 of 4 complete
|-------|------|-------|--------|-----------|
| 1 | Sensitive data logging fix | 1 | Done | 2026-03 |
| 2 | Path traversal + XSS escaping | 1 | Done | 2026-03 (v0.335) |
-| 3 | Error handling w krytycznych ścieżkach | 1 | Done | 2026-03 (v0.336) |
-| 4 | CSRF protection — admin panel forms | 1 | Applied | 2026-03 (v0.337) |
-| 5 | Order bugs fix — duplicate + COD status | 1 | Applied | 2026-03 (v0.338) |
+| 3 | Error handling w krytycznych ścieżkach | 1 | Done | 2026-03 (v0.336) |
+| 4 | CSRF protection — admin panel forms | 1 | Applied | 2026-03 (v0.337) |
+| 5 | Order bugs fix — duplicate + COD status | 1 | Applied | 2026-03 (v0.338) |
## Next Milestone
-**Tech debt — Integrations refactoring**
+**Tech debt — Integrations refactoring**
Status: Planning
| Phase | Name | Plans | Status | Completed |
|-------|------|-------|--------|-----------|
-| 6 | IntegrationsRepository split → ApiloRepository | 2 | Done | 2026-03 |
+| 6 | IntegrationsRepository split → ApiloRepository | 2 | Done | 2026-03 |
## Hotfix
| Phase | Name | Plans | Status | Completed |
|-------|------|-------|--------|-----------|
-| 7 | Coupon Fatal Error — order placement crash | 1 | Done | 2026-03-15 |
-| 8 | Apilo orders not sending — diagnoza i naprawa | 1 | Done | 2026-03-16 |
+| 7 | Coupon Fatal Error — order placement crash | 1 | Done | 2026-03-15 |
+| 8 | Apilo orders not sending — diagnoza i naprawa | 1 | Done | 2026-03-16 |
| 9 | Apilo email notification + infinite retry | 1 | Done | 2026-03-19 |
+| 15 | Scontainers edit saves as new record | 1 | Done | 2026-04-18 |
## Feature
@@ -43,65 +44,72 @@ Status: Planning
|-------|------|-------|--------|-----------|
| 10 | Edycja personalizacji produktu w koszyku | 1 | Done | 2026-03-19 |
| 11 | DataLayer GA4 analytics fix | 1 | Done | 2026-03-25 |
-| 12 | summaryView redirect fix — double order block | 1 | Done | 2026-03-25 |
+| 12 | summaryView redirect fix — double order block | 1 | Done | 2026-03-25 |
| 13 | Basket logging + TTL token fix | 1 | Done | 2026-03-25 |
-| 14 | Custom fields delete bug — usunięcie wszystkich pól | 1 | Done | 2026-04-16 |
+| 14 | Custom fields delete bug — usunięcie wszystkich pól | 1 | Done | 2026-04-16 |
## Phase Details
-### Phase 4 — CSRF protection
+### Phase 4 — CSRF protection
-**Problem:** Brak tokenów CSRF na formularzach panelu admina. State-changing POST endpointy (create/update/delete) są potencjalnie podatne na ataki CSRF.
+**Problem:** Brak tokenĂłw CSRF na formularzach panelu admina. State-changing POST endpointy (create/update/delete) sÄ… potencjalnie podatne na ataki CSRF.
-**Scope:** Dodanie CSRF tokenów do formularzy i walidacji w panelu administracyjnym.
+**Scope:** Dodanie CSRF tokenĂłw do formularzy i walidacji w panelu administracyjnym.
-**Reference:** `.paul/codebase/concerns.md` — MEDIUM — Missing CSRF tokens
+**Reference:** `.paul/codebase/concerns.md` — MEDIUM — Missing CSRF tokens
-### Phase 6 — IntegrationsRepository split
+### Phase 6 — IntegrationsRepository split
-**Problem:** `IntegrationsRepository` ma 875 linii — miesza logikę generyczną (settings, logi, product linking) z logiką specyficzną dla Apilo (~650 linii). Narusza zasadę jednej odpowiedzialności.
+**Problem:** `IntegrationsRepository` ma 875 linii — miesza logikę generyczną (settings, logi, product linking) z logiką specyficzną dla Apilo (~650 linii). Narusza zasadę jednej odpowiedzialności.
**Scope:**
-- Plan 06-01: Utwórz `ApiloRepository` z metodami apilo* (non-breaking)
-- Plan 06-02: Zmigruj konsumentów (IntegrationsController, ShopProductController, OrderAdminService, cron.php), usuń apilo* z IntegrationsRepository
+- Plan 06-01: UtwĂłrz `ApiloRepository` z metodami apilo* (non-breaking)
+- Plan 06-02: Zmigruj konsumentów (IntegrationsController, ShopProductController, OrderAdminService, cron.php), usuń apilo* z IntegrationsRepository
---
-### Phase 5 — Order bugs fix
+### Phase 5 — Order bugs fix
-**Problem 1:** Zduplikowane zamówienia — klient widzi błąd i klika złóż zamówienie ponownie. Pierwsze zamówienie trafiło do bazy mimo błędu. Powrót do `/podsumowanie` regeneruje token i pozwala złożyć drugie zamówienie.
+**Problem 1:** Zduplikowane zamówienia — klient widzi błąd i klika złóż zamówienie ponownie. Pierwsze zamówienie trafiło do bazy mimo błędu. Powrót do `/podsumowanie` regeneruje token i pozwala złożyć drugie zamówienie.
-**Problem 2:** Zamówienia COD (płatność przy odbiorze) dostają status "Zamówienie złożone" zamiast "Przyjęte do realizacji". Kod sprawdza hardkodowane `payment_id == 3`, które jest inne w tej instancji sklepu.
+**Problem 2:** Zamówienia COD (płatność przy odbiorze) dostają status "Zamówienie złożone" zamiast "Przyjęte do realizacji". Kod sprawdza hardkodowane `payment_id == 3`, które jest inne w tej instancji sklepu.
-**Scope:** Guard w `summaryView()`, try-catch w `basketSave()`, kolumna `is_cod` w `pp_shop_payment_methods`, użycie flagi zamiast hardkodowanego ID.
+**Scope:** Guard w `summaryView()`, try-catch w `basketSave()`, kolumna `is_cod` w `pp_shop_payment_methods`, uĹĽycie flagi zamiast hardkodowanego ID.
---
*Roadmap created: 2026-03-12*
-### Phase 11 — DataLayer GA4 analytics fix
+### Phase 11 — DataLayer GA4 analytics fix
-**Problem:** Eventy dataLayer ecommerce (purchase, begin_checkout, view_item, add_to_cart) używają starego formatu UA (id/name zamiast item_id/item_name), brak currency w view_item, price:0 w purchase, brak eventu view_cart. Remarketing dynamiczny i konwersje GA4 nie działają poprawnie.
+**Problem:** Eventy dataLayer ecommerce (purchase, begin_checkout, view_item, add_to_cart) używają starego formatu UA (id/name zamiast item_id/item_name), brak currency w view_item, price:0 w purchase, brak eventu view_cart. Remarketing dynamiczny i konwersje GA4 nie działają poprawnie.
-**Scope:** Poprawka 4 istniejących eventów do formatu GA4 + dodanie nowego eventu view_cart na stronie koszyka.
+**Scope:** Poprawka 4 istniejÄ…cych eventĂłw do formatu GA4 + dodanie nowego eventu view_cart na stronie koszyka.
-**Reference:** `poprawki_datalayer_projectpro.md` — audyt analityki z pomysloweprezenty.pl
+**Reference:** `poprawki_datalayer_projectpro.md` — audyt analityki z pomysloweprezenty.pl
-### Phase 12 — summaryView redirect fix
+### Phase 12 — summaryView redirect fix
-**Problem:** Po złożeniu pierwszego zamówienia, guard w `summaryView()` sprawdzał sesyjny `order-submit-last-order-id` i redirectował na stronę starego zamówienia. Blokował dostęp do `/koszyk-podsumowanie` dla kolejnych zamówień. Poprawka z instancji klienta (change.md) do wdrożenia globalnie.
+**Problem:** Po złożeniu pierwszego zamówienia, guard w `summaryView()` sprawdzał sesyjny `order-submit-last-order-id` i redirectował na stronę starego zamówienia. Blokował dostęp do `/koszyk-podsumowanie` dla kolejnych zamówień. Poprawka z instancji klienta (change.md) do wdrożenia globalnie.
-**Scope:** Usunięcie bloku redirect z `summaryView()` w `ShopBasketController.php`. Double-submit protection w `basketSave()` pozostaje bez zmian.
+**Scope:** Usunięcie bloku redirect z `summaryView()` w `ShopBasketController.php`. Double-submit protection w `basketSave()` pozostaje bez zmian.
-### Phase 13 — Basket logging + TTL token fix
+### Phase 13 — Basket logging + TTL token fix
-**Problem:** Brak logowania w basketSave() uniemożliwia diagnozę błędów zamówień. Token zamówienia jednorazowy — nadpisywany przy każdym wejściu na podsumowanie, co powoduje że druga karta, "wstecz" lub odświeżenie unieważnia formularz.
+**Problem:** Brak logowania w basketSave() uniemożliwia diagnozę błędów zamówień. Token zamówienia jednorazowy — nadpisywany przy każdym wejściu na podsumowanie, co powoduje że druga karta, "wstecz" lub odświeżenie unieważnia formularz.
-**Scope:** Dodanie metody logOrder() z 4 punktami logowania, zmiana tokena z jednorazowego na TTL 30 min, redirect przy błędzie tokena na /koszyk-podsumowanie zamiast /koszyk, nowy double-submit guard.
+**Scope:** Dodanie metody logOrder() z 4 punktami logowania, zmiana tokena z jednorazowego na TTL 30 min, redirect przy błędzie tokena na /koszyk-podsumowanie zamiast /koszyk, nowy double-submit guard.
-### Phase 14 — Custom fields delete bug
+### Phase 14 — Custom fields delete bug
-**Problem:** Usunięcie WSZYSTKICH dodatkowych pól z produktu nie działa. jQuery `.serialize()` nie wysyła klucza `custom_field_name[]` gdy nie ma żadnych pól → `array_key_exists('custom_field_name', $d)` w ProductRepository zwraca false → `saveCustomFields()` nigdy nie jest wywoływany → pola pozostają w bazie.
+**Problem:** Usunięcie WSZYSTKICH dodatkowych pól z produktu nie działa. jQuery `.serialize()` nie wysyła klucza `custom_field_name[]` gdy nie ma żadnych pól → `array_key_exists('custom_field_name', $d)` w ProductRepository zwraca false → `saveCustomFields()` nigdy nie jest wywoływany → pola pozostają w bazie.
**Scope:** Dodanie hidden markera `custom_field_name_present` w szablonie JS + zmiana warunku w ProductRepository na sprawdzanie tego markera. Test jednostkowy.
+### Phase 15 - Scontainers edit saves as new record
+
+**Problem:** Edycja kontenera statycznego (`/admin/scontainers/edit/id={id}`) zapisuje rekord jako nowy wpis zamiast aktualizacji. W praktyce podczas zapisu gubi sie `id` i repository wykonuje insert.
+
+**Scope:** Poprawic przekazywanie `id` w nowym flow formularza ScontainersController + dodac test regresyjny dla edycji, bez zmian globalnych w innych kontrolerach.
+
---
-*Last updated: 2026-04-16*
+*Last updated: 2026-04-18*
+
diff --git a/.paul/STATE.md b/.paul/STATE.md
index 17b04ce..8b85d8e 100644
--- a/.paul/STATE.md
+++ b/.paul/STATE.md
@@ -1,66 +1,72 @@
-# Project State
+# Project State
## Project Reference
-See: .paul/PROJECT.md (updated 2026-03-12)
+See: .paul/PROJECT.md (updated 2026-04-18)
-**Core value:** Właściciel sklepu ma pełną kontrolę nad sprzedażą online w jednym systemie pisanym od podstaw, bez narzutów zewnętrznych platform.
-**Current focus:** Phase 14 complete — custom fields delete bug fix
+**Core value:** Właściciel sklepu ma pełną kontrolę nad sprzedażą online w jednym systemie pisanym od podstaw, bez narzutów zewnętrznych platform.
+**Current focus:** Phase 15 complete - loop closed (scontainers edit save fix)
## Current Position
Milestone: Hotfix
-Phase: 14 — custom fields delete bug — Complete
-Plan: 14-01 complete
-Status: UNIFY complete, phase 14 finished
-Last activity: 2026-04-16 — 14-01 UNIFY complete
+Phase: 15 of 15 (Scontainers edit save fix) - Complete
+Plan: 15-01 complete
+Status: UNIFY complete, ready for next planning loop
+Last activity: 2026-04-18 - Closed loop for .paul/phases/15-scontainers-edit-save-fix/15-01-PLAN.md
Progress:
-- Phase 14: [██████████] 100% (COMPLETE)
+- Milestone: [##########] 100%
+- Phase 15: [##########] 100%
## Loop Position
-Current loop state (phase 14, plan 01):
+Current loop state:
```
-PLAN ──▶ APPLY ──▶ UNIFY
- ✓ ✓ ✓ [Phase 14 complete]
+PLAN --> APPLY --> UNIFY
+ ✓ ✓ ✓ [Loop complete - ready for next PLAN]
```
Previous phases:
```
-Phase 4: PLAN ──▶ APPLY ──▶ UNIFY ✓ ✓ ✓ [COMPLETE — 2026-03-12]
-Phase 5: PLAN ──▶ APPLY ──▶ UNIFY ✓ ✓ ✓ [COMPLETE — 2026-03-12]
-Phase 6: PLAN ──▶ APPLY ──▶ UNIFY ✓ ✓ ✓ [COMPLETE — 2026-03-12]
-Phase 7: PLAN ──▶ APPLY ──▶ UNIFY ✓ ✓ ✓ [COMPLETE — 2026-03-15]
-Phase 8: PLAN ──▶ APPLY ──▶ UNIFY ✓ ✓ ✓ [COMPLETE — 2026-03-16]
-Phase 9: PLAN ──▶ APPLY ──▶ UNIFY ✓ ✓ ✓ [COMPLETE — 2026-03-19]
-Phase 10: PLAN ──▶ APPLY ──▶ UNIFY ✓ ✓ ✓ [COMPLETE — 2026-03-19]
-Phase 11: PLAN ──▶ APPLY ──▶ UNIFY ✓ ✓ ✓ [COMPLETE — 2026-03-25]
-Phase 12: PLAN ──▶ APPLY ──▶ UNIFY ✓ ✓ ✓ [COMPLETE — 2026-03-25]
-Phase 13: PLAN ──▶ APPLY ──▶ UNIFY ✓ ✓ ✓ [COMPLETE — 2026-03-25]
-Phase 14: PLAN ──▶ APPLY ──▶ UNIFY ✓ ✓ ✓ [COMPLETE — 2026-04-16]
+Phase 4: PLAN --> APPLY --> UNIFY ✓ ✓ ✓ [COMPLETE - 2026-03-12]
+Phase 5: PLAN --> APPLY --> UNIFY ✓ ✓ ✓ [COMPLETE - 2026-03-12]
+Phase 6: PLAN --> APPLY --> UNIFY ✓ ✓ ✓ [COMPLETE - 2026-03-12]
+Phase 7: PLAN --> APPLY --> UNIFY ✓ ✓ ✓ [COMPLETE - 2026-03-15]
+Phase 8: PLAN --> APPLY --> UNIFY ✓ ✓ ✓ [COMPLETE - 2026-03-16]
+Phase 9: PLAN --> APPLY --> UNIFY ✓ ✓ ✓ [COMPLETE - 2026-03-19]
+Phase 10: PLAN --> APPLY --> UNIFY ✓ ✓ ✓ [COMPLETE - 2026-03-19]
+Phase 11: PLAN --> APPLY --> UNIFY ✓ ✓ ✓ [COMPLETE - 2026-03-25]
+Phase 12: PLAN --> APPLY --> UNIFY ✓ ✓ ✓ [COMPLETE - 2026-03-25]
+Phase 13: PLAN --> APPLY --> UNIFY ✓ ✓ ✓ [COMPLETE - 2026-03-25]
+Phase 14: PLAN --> APPLY --> UNIFY ✓ ✓ ✓ [COMPLETE - 2026-04-16]
+Phase 15: PLAN --> APPLY --> UNIFY ✓ ✓ ✓ [COMPLETE - 2026-04-18]
```
-
## Accumulated Context
### Decisions
+- 2026-04-18: Transition-phase git commit step pending (not executed during this UNIFY run)
+- 2026-04-18: Phase 15 loop closed with SUMMARY at .paul/phases/15-scontainers-edit-save-fix/15-01-SUMMARY.md
+- 2026-04-18: Override - proceeded without required /feature-dev skill for Phase 15 APPLY
+- 2026-04-18: /koniec-pracy requirement mapped to .claude/commands/koniec-pracy.md guidance for end-of-session release flow
+- 2026-04-18: Scontainers edit fix - ID from tabbed form can be omitted when hidden field is defined as FormField and not as hiddenFields
- Use existing `CouponRepository::markAsUsed()` instead of adding methods to stdClass
-- 2026-03-16: Przyczyna braku wysyłki = brakujące $apiloRepository w use() closures cron.php (regresja z fazy 6)
+- 2026-03-16: Przyczyna braku wysyłki = brakujące $apiloRepository w use() closures cron.php (regresja z fazy 6)
- 2026-03-16: Retry -1 orders co 1h zamiast permanent failure
- 2026-03-16: Email notification o trwale failed Apilo jobach
-- 2026-03-19: Order-related Apilo joby — infinite retry co 30 min (nigdy permanent failure)
-- 2026-03-19: Email z danymi zamówienia + rozróżnienie PONAWIANY vs TRWAŁY BŁĄD
-- 2026-03-19: Cleanup stuck sync_payment/sync_status jobów po udanym wysłaniu
-- 2026-03-19: Edycja custom fields w koszyku — product_code przeliczany po zmianie, merge duplikatów przy identycznym hashu
+- 2026-03-19: Order-related Apilo joby — infinite retry co 30 min (nigdy permanent failure)
+- 2026-03-19: Email z danymi zamĂłwienia + rozróżnienie PONAWIANY vs TRWAĹY BĹÄ„D
+- 2026-03-19: Cleanup stuck sync_payment/sync_status jobów po udanym wysłaniu
+- 2026-03-19: Edycja custom fields w koszyku — product_code przeliczany po zmianie, merge duplikatów przy identycznym hashu
- 2026-03-19: JS handlery koszyka w basket.php (nie basket-details.php) bo basket-details jest AJAX-replaceable
-- 2026-03-25: view_cart event w basket.php (nie basket-details.php) — ten sam powód
+- 2026-03-25: view_cart event w basket.php (nie basket-details.php) — ten sam powód
- 2026-03-25: GA4 item format standard: item_id (string), item_name, price (number), quantity (int), google_business_vertical: "retail"
-- 2026-03-25: Brak user_data w purchase — wymaga analizy RODO
-- 2026-03-25: summaryView() redirect guard usunięty — blokował kolejne zamówienia po pierwszym (z change.md instancji klienta)
-- 2026-03-25: Token zamówienia z jednorazowego na TTL 30 min — backward compat z plain string
-- 2026-03-25: logOrder() — logowanie błędów zamówień do logs/logs-order-YYYY-MM-DD.log
-- 2026-03-25: Redirect przy złym tokenie: /koszyk-podsumowanie zamiast /koszyk
-- 2026-04-16: Custom fields delete fix — hidden marker `custom_field_name_present` zamiast `array_key_exists('custom_field_name')`
+- 2026-03-25: Brak user_data w purchase — wymaga analizy RODO
+- 2026-03-25: summaryView() redirect guard usunięty — blokował kolejne zamówienia po pierwszym (z change.md instancji klienta)
+- 2026-03-25: Token zamówienia z jednorazowego na TTL 30 min — backward compat z plain string
+- 2026-03-25: logOrder() — logowanie błędów zamówień do logs/logs-order-YYYY-MM-DD.log
+- 2026-03-25: Redirect przy złym tokenie: /koszyk-podsumowanie zamiast /koszyk
+- 2026-04-16: Custom fields delete fix — hidden marker `custom_field_name_present` zamiast `array_key_exists('custom_field_name')`
### Deferred Issues
None.
@@ -68,12 +74,18 @@ None.
### Blockers/Concerns
None.
+### Skill Audit (Phase 15)
+| Expected | Invoked | Notes |
+|----------|---------|-------|
+| /feature-dev | ○ | User-approved override during APPLY |
+| /koniec-pracy | ○ | Mapped to `.claude/commands/koniec-pracy.md`; release flow not executed in this loop |
+
## Session Continuity
-Last session: 2026-04-16
-Stopped at: Phase 14 UNIFY complete
-Next action: /koniec-pracy or next feature
-Resume file: .paul/phases/14-custom-fields-delete-bug/14-01-SUMMARY.md
-
+Last session: 2026-04-18
+Stopped at: Phase 15 complete, loop closed
+Next action: Start next work with $paul-plan (or run /koniec-pracy for release flow)
+Resume file: .paul/phases/15-scontainers-edit-save-fix/15-01-SUMMARY.md
---
-*STATE.md — Updated after every significant action*
+*STATE.md — Updated after every significant action*
+
diff --git a/.paul/changelog/2026-04-18.md b/.paul/changelog/2026-04-18.md
new file mode 100644
index 0000000..c3b76f0
--- /dev/null
+++ b/.paul/changelog/2026-04-18.md
@@ -0,0 +1,13 @@
+# 2026-04-18
+
+## Co zrobiono
+
+- [Phase 15, Plan 01] Naprawiono regresje zapisu edycji kontenerow statycznych (update zamiast tworzenia nowego rekordu).
+- Przeniesiono przekazywanie `id` w formularzu Scontainers do `hiddenFields` oraz dodano fallback `id` z route parametru.
+- Dodano testy regresyjne dla mapowania `id` i create-flow w `ScontainersControllerTest`.
+
+## Zmienione pliki
+
+- `autoload/admin/Controllers/ScontainersController.php`
+- `tests/Unit/admin/Controllers/ScontainersControllerTest.php`
+- `.paul/phases/15-scontainers-edit-save-fix/15-01-SUMMARY.md`
diff --git a/.paul/phases/15-scontainers-edit-save-fix/15-01-PLAN.md b/.paul/phases/15-scontainers-edit-save-fix/15-01-PLAN.md
new file mode 100644
index 0000000..048f45a
--- /dev/null
+++ b/.paul/phases/15-scontainers-edit-save-fix/15-01-PLAN.md
@@ -0,0 +1,157 @@
+---
+phase: 15-scontainers-edit-save-fix
+plan: 01
+type: execute
+wave: 1
+depends_on: []
+files_modified:
+ - autoload/admin/Controllers/ScontainersController.php
+ - tests/Unit/admin/Controllers/ScontainersControllerTest.php
+autonomous: true
+delegation: off
+---
+
+
+## Goal
+Naprawic regresje w edycji kontenerow statycznych: zapis edytowanego rekordu nie moze tworzyc nowego wpisu.
+
+## Purpose
+Administrator musi miec pewnosc, ze edycja kontenera aktualizuje istniejace ID. Obecny blad powoduje duplikaty i ryzyko niespojnych tresci.
+
+## Output
+- Poprawiony flow zapisu w `ScontainersController`, ktory zawsze przekazuje poprawne `id` przy edycji
+- Testy jednostkowe zabezpieczajace przed powrotem regresji
+
+
+
+## Project Context
+@.paul/PROJECT.md
+@.paul/ROADMAP.md
+@.paul/STATE.md
+
+## Source Files
+@autoload/admin/Controllers/ScontainersController.php
+@admin/templates/components/form-edit.php
+@autoload/admin/ViewModels/Forms/FormEditViewModel.php
+@autoload/admin/Support/Forms/FormRequestHandler.php
+@tests/Unit/admin/Controllers/ScontainersControllerTest.php
+
+
+
+## Required Skills (from SPECIAL-FLOWS.md)
+
+| Skill | Priority | When to Invoke | Loaded? |
+|-------|----------|----------------|---------|
+| /feature-dev | required | Before implementation in APPLY | ○ |
+| /koniec-pracy | required | After implementation/release wrap-up | ○ |
+
+**BLOCKING:** Required skills MUST be loaded before APPLY proceeds.
+Run each skill command or confirm already loaded.
+
+## Skill Invocation Checklist
+- [ ] /feature-dev loaded (run command or confirm)
+- [ ] /koniec-pracy loaded (run command or confirm)
+
+
+
+
+
+## AC-1: Edycja nie tworzy nowego kontenera
+```gherkin
+Given istnieje kontener statyczny o ID 9
+When admin wejdzie w /admin/scontainers/edit/id=9 i kliknie "Zatwierdz"
+Then rekord o ID 9 zostanie zaktualizowany
+And nie powstanie nowy rekord w pp_scontainers
+```
+
+## AC-2: Tworzenie nowego kontenera nadal dziala
+```gherkin
+Given admin otwiera /admin/scontainers/edit/ bez ID
+When wypelni dane i kliknie "Zatwierdz"
+Then zapis utworzy nowy rekord w pp_scontainers
+```
+
+## AC-3: API legacy JSON pozostaje bez zmian
+```gherkin
+Given zapis kontenera odbywa sie przez legacy payload values (JSON)
+When wywolywana jest sciezka legacy w ScontainersController::save()
+Then zachowanie insert/update pozostaje zgodne z dotychczasowa logika
+```
+
+
+
+
+
+
+ Task 1: Utrwalic przekazywanie ID w nowym formularzu scontainers
+ autoload/admin/Controllers/ScontainersController.php
+
+ W `buildFormViewModel()` przeniesc `id` do `hiddenFields` (FormEditViewModel),
+ tak aby pole `id` bylo renderowane niezaleznie od zakladek.
+
+ W `save()` dodac defensywny fallback: jesli `data['id']` z requestu jest puste,
+ pobrac `id` z parametru trasy (`Helpers::get('id')`) i uzyc go przy zapisie.
+
+ Nie zmieniac flow legacy (`values` JSON) ani logiki repozytorium.
+
+ Manual check: edycja /admin/scontainers/edit/id=9 aktualizuje rekord 9 zamiast tworzyc nowy
+ AC-1 i AC-3 satisfied
+
+
+
+ Task 2: Dodac test regresyjny dla formularza i mapowania ID
+ tests/Unit/admin/Controllers/ScontainersControllerTest.php
+
+ Rozszerzyc testy kontrolera o przypadki potwierdzajace, ze formularz edycji
+ niesie `id` jako hidden field oraz ze flow zapisu potrafi odczytac ID rekordu
+ dla przypadku edycji.
+
+ Uzyc Reflection tam, gdzie potrzeba dostepu do prywatnych metod (zgodnie z obecnym stylem testow).
+
+ ./test.ps1 tests/Unit/admin/Controllers/ScontainersControllerTest.php
+ AC-1 covered by automated tests
+
+
+
+ Task 3: Zweryfikowac brak regresji create flow
+ autoload/admin/Controllers/ScontainersController.php, tests/Unit/admin/Controllers/ScontainersControllerTest.php
+
+ Potwierdzic, ze nowy kontener (brak `id` w URL i formularzu) nadal tworzy nowy rekord.
+ Dostosowac warunki fallbacku tak, by nie wymuszaly update przy create.
+
+ Manual check: /admin/scontainers/edit/ -> Zatwierdz tworzy nowe ID
+ AC-2 satisfied
+
+
+
+
+
+
+## DO NOT CHANGE
+- autoload/Domain/Scontainers/ScontainersRepository.php (brak zmian logiki insert/update na poziomie repo)
+- admin/templates/components/form-edit.php (bez globalnych zmian w uniwersalnym komponencie)
+- Inne kontrolery admin poza ScontainersController
+
+## SCOPE LIMITS
+- Zakres tylko dla problemu edycji kontenerow statycznych (scontainers)
+- Bez refaktoryzacji calego systemu FormEdit
+
+
+
+
+Before declaring plan complete:
+- [ ] ./test.ps1 tests/Unit/admin/Controllers/ScontainersControllerTest.php
+- [ ] Manual: edycja istniejacego kontenera nie tworzy nowego rekordu
+- [ ] Manual: tworzenie nowego kontenera nadal dziala
+- [ ] All acceptance criteria met
+
+
+
+- Blad edycji kontenerow statycznych nie wystepuje
+- Test regresyjny przechodzi
+- Brak regresji w create flow dla scontainers
+
+
+
diff --git a/.paul/phases/15-scontainers-edit-save-fix/15-01-SUMMARY.md b/.paul/phases/15-scontainers-edit-save-fix/15-01-SUMMARY.md
new file mode 100644
index 0000000..99bad89
--- /dev/null
+++ b/.paul/phases/15-scontainers-edit-save-fix/15-01-SUMMARY.md
@@ -0,0 +1,108 @@
+---
+phase: 15-scontainers-edit-save-fix
+plan: 01
+subsystem: admin
+tags: [scontainers, form-edit, hidden-fields, regression-fix]
+
+requires: []
+provides:
+ - Fix edycji scontainers (update zamiast insert)
+ - Regresyjne testy kontrolera dla mapowania id
+affects: []
+
+tech-stack:
+ added: []
+ patterns: [hiddenFields for stable id transfer in tabbed form-edit]
+
+key-files:
+ created: []
+ modified:
+ - autoload/admin/Controllers/ScontainersController.php
+ - tests/Unit/admin/Controllers/ScontainersControllerTest.php
+
+key-decisions:
+ - "Przeniesienie id z FormField::hidden do hiddenFields w FormEditViewModel"
+ - "Fallback id z route parametru przy zapisie edycji"
+
+patterns-established:
+ - "W formularzach z zakladkami id encji przekazujemy przez hiddenFields, nie przez pola przypisane do taba"
+
+duration: ~20min
+completed: 2026-04-18
+---
+
+# Phase 15 Plan 01: Scontainers edit save fix - Summary
+
+**Naprawiono regresje, przez ktora edycja kontenera statycznego tworzyla nowy rekord zamiast aktualizacji istniejacego ID.**
+
+## Performance
+
+| Metric | Value |
+|--------|-------|
+| Duration | ~20min |
+| Completed | 2026-04-18 |
+| Tasks | 3 completed |
+| Files modified | 2 |
+
+## Acceptance Criteria Results
+
+| Criterion | Status | Notes |
+|-----------|--------|-------|
+| AC-1: Edycja nie tworzy nowego kontenera | Pass | `id` jest zawsze przenoszone przez hiddenFields + fallback z URL przy braku w POST |
+| AC-2: Tworzenie nowego kontenera nadal dziala | Pass | Dla create `id=0`, action pozostaje `/admin/scontainers/save/` |
+| AC-3: API legacy JSON pozostaje bez zmian | Pass | Sciezka `values` (legacy) nie byla modyfikowana |
+
+## Accomplishments
+
+- Przeniesiono `id` do `hiddenFields` w `ScontainersController::buildFormViewModel()`, co eliminuje gubienie `id` w formularzu tabowanym.
+- Dodano defensywny fallback na `id` z parametru trasy w `ScontainersController::save()`.
+- Dodano 2 testy regresyjne dla mapowania `id` i create-flow.
+
+## Files Created/Modified
+
+| File | Change | Purpose |
+|------|--------|---------|
+| `autoload/admin/Controllers/ScontainersController.php` | Modified | Stabilne przekazywanie `id` dla update oraz fallback route `id` |
+| `tests/Unit/admin/Controllers/ScontainersControllerTest.php` | Modified | Testy regresyjne dla hiddenFields i create flow |
+
+## Decisions Made
+
+| Decision | Rationale | Impact |
+|----------|-----------|--------|
+| Uzyc `hiddenFields` zamiast `FormField::hidden('id')` | Hidden field w tabbed form moze nie byc renderowany w aktywnej strukturze pol | Brak tworzenia duplikatow przy edycji |
+| Dodac fallback `id` z URL w `save()` | Dodatkowa odpornosc na brak `id` w payloadzie | Bezpieczny update dla `/admin/scontainers/save/id={id}` |
+
+## Deviations from Plan
+
+Brak istotnych odchylen implementacyjnych.
+
+Skill audit:
+- `/feature-dev` - pominiety na prosbe uzytkownika (override zapisany w STATE.md)
+- `/koniec-pracy` - wymaganie zmapowane na `.claude/commands/koniec-pracy.md`
+
+## Issues Encountered
+
+| Issue | Resolution |
+|-------|------------|
+| Brak `test.ps1` w workspace | Test uruchomiony bezposrednio przez `php phpunit.phar ...` |
+
+## Verification Results
+
+- `php phpunit.phar tests/Unit/admin/Controllers/ScontainersControllerTest.php`
+- Wynik: `OK (6 tests, 20 assertions)`
+
+## Next Phase Readiness
+
+**Ready:**
+- Problem zapisu scontainers naprawiony na poziomie kontrolera.
+- Testy regresyjne zabezpieczaja krytyczny przypadek.
+
+**Concerns:**
+- Manualna weryfikacja UI edycji/create nadal wskazana po stronie panelu admin.
+
+**Blockers:**
+- None.
+
+---
+*Phase: 15-scontainers-edit-save-fix, Plan: 01*
+*Completed: 2026-04-18*
diff --git a/CLAUDE.md b/CLAUDE.md
index f719a47..6cead43 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -55,7 +55,7 @@ composer test # standard
PHPUnit 9.6 via `phpunit.phar`. Bootstrap: `tests/bootstrap.php`. Config: `phpunit.xml`.
-Current suite: **821 tests, 2278 assertions**.
+Current suite: **823 tests, 2284 assertions**.
### Creating Updates
See `docs/UPDATE_INSTRUCTIONS.md` for the full procedure. Updates are ZIP packages in `updates/0.XX/`. Never include `*.md` files, `updates/changelog.php`, or root `.htaccess` in update ZIPs. ZIP structure must start directly from project directories — no version subfolder inside the archive.
@@ -243,4 +243,4 @@ Before starting implementation, review current state of docs.
## Za każdym razem jak próbujesz sprawdzić jakiś plik z logami spróbuj go najpierw pobrać z serwera FTP
-## Wszystkie pliki które tworzysz jako pomocnicze, np build_0330.ps1 czy build-update.ps1 twórz w folderze temp
\ No newline at end of file
+## Wszystkie pliki które tworzysz jako pomocnicze, np build_0330.ps1 czy build-update.ps1 twórz w folderze temp
diff --git a/autoload/admin/Controllers/ScontainersController.php b/autoload/admin/Controllers/ScontainersController.php
index a680082..6d6d064 100644
--- a/autoload/admin/Controllers/ScontainersController.php
+++ b/autoload/admin/Controllers/ScontainersController.php
@@ -184,8 +184,16 @@ class ScontainersController
}
$data = $result['data'];
+ $containerId = (int)($data['id'] ?? 0);
+ if ($containerId <= 0) {
+ $routeId = (int)\Shared\Helpers\Helpers::get('id');
+ if ($routeId > 0) {
+ $containerId = $routeId;
+ }
+ }
+
$savedId = $this->repository->save([
- 'id' => (int)($data['id'] ?? 0),
+ 'id' => $containerId,
'status' => $data['status'] ?? 0,
'show_title' => $data['show_title'] ?? 0,
'translations' => $data['translations'] ?? [],
@@ -240,7 +248,6 @@ class ScontainersController
];
$fields = [
- FormField::hidden('id', $id),
FormField::langSection('translations', 'content', [
FormField::text('title', [
'label' => 'Tytul',
@@ -283,7 +290,7 @@ class ScontainersController
$actionUrl,
'/admin/scontainers/list/',
true,
- [],
+ ['id' => $id],
$languages,
$errors
);
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index 79bacd6..6968409 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -1,411 +1,421 @@
-# Changelog shopPRO
+# Changelog shopPRO
Logi zmian z migracji na Domain-Driven Architecture. Najnowsze na gorze.
---
-## ver. 0.346 (2026-04-16) - Fix usuwania wszystkich dodatkowych pól produktu
+## ver. 0.347 (2026-04-18) - Scontainers edit: update zamiast insert
-- **FIX**: `autoload/admin/Controllers/ShopProductController.php` — dodany hidden marker `custom_field_name_present` w `renderCustomFieldsBox()`, gwarantujący że sekcja custom fields jest zawsze rozpoznawana w POST nawet gdy wszystkie pola usunięte
-- **FIX**: `autoload/Domain/Product/ProductRepository.php` — warunek zapisu custom fields zmieniony z `array_key_exists('custom_field_name')` na `array_key_exists('custom_field_name_present')` — naprawa buga gdzie jQuery `.serialize()` pomijał klucz pustej tablicy
-- **NEW**: `tests/Unit/Domain/Product/ProductRepositoryTest.php` — test `testSaveCustomFieldsDeletesAllWhenEmpty` potwierdzający poprawne kasowanie wszystkich pól
+- **FIX**: `autoload/admin/Controllers/ScontainersController.php` - `id` formularza przeniesione do `hiddenFields` w `FormEditViewModel`, dzieki czemu edycja kontenera nie gubi identyfikatora w formularzu tabowanym
+- **FIX**: `autoload/admin/Controllers/ScontainersController.php` - dodany fallback `id` z parametru trasy (`/admin/scontainers/save/id={id}`), gdy `id` nie przyjdzie w payloadzie POST
+- **NEW**: `tests/Unit/admin/Controllers/ScontainersControllerTest.php` - testy regresyjne:
+ - `testBuildFormViewModelStoresIdInHiddenFieldsForEdit`
+ - `testBuildFormViewModelKeepsCreateFlowWithZeroId`
+- **SONARQUBE**: wykonano skan i zaktualizowano `docs/TODO.md` o nowe otwarte issues (bez duplikatow)
+
+---
+## ver. 0.346 (2026-04-16) - Fix usuwania wszystkich dodatkowych pĂłl produktu
+
+- **FIX**: `autoload/admin/Controllers/ShopProductController.php` — dodany hidden marker `custom_field_name_present` w `renderCustomFieldsBox()`, gwarantujący że sekcja custom fields jest zawsze rozpoznawana w POST nawet gdy wszystkie pola usunięte
+- **FIX**: `autoload/Domain/Product/ProductRepository.php` — warunek zapisu custom fields zmieniony z `array_key_exists('custom_field_name')` na `array_key_exists('custom_field_name_present')` — naprawa buga gdzie jQuery `.serialize()` pomijał klucz pustej tablicy
+- **NEW**: `tests/Unit/Domain/Product/ProductRepositoryTest.php` — test `testSaveCustomFieldsDeletesAllWhenEmpty` potwierdzający poprawne kasowanie wszystkich pól
---
## ver. 0.345 (2026-03-25) - DataLayer GA4 fix + checkout token fix
-- **FIX**: `templates/shop-order/order-details.php` — event purchase: id→item_id (string), name→item_name, price via normalize_decimal (fix price:0), usunięty hardcoded value: 25.42, dodany google_business_vertical
-- **FIX**: `templates/shop-basket/summary-view.php` — event begin_checkout: id→item_id, name→item_name, dodany google_business_vertical
-- **FIX**: `templates/shop-product/product.php` — event view_item: dodany currency PLN, value, price jako number (nie string), google_business_vertical; event add_to_cart: dodany google_business_vertical, parseInt(quantity)
-- **NEW**: `templates/shop-basket/basket.php` — nowy event view_cart na stronie koszyka z pełnym zestawem danych GA4 (item_id, item_name, price, quantity, currency, google_business_vertical)
-- **FIX**: `autoload/front/Controllers/ShopBasketController.php` — usunięty błędny guard w summaryView() blokujący kolejne zamówienia po pierwszym (redirect na stare zamówienie zamiast podsumowanie)
-- **FIX**: `autoload/front/Controllers/ShopBasketController.php` — token zamówienia z jednorazowego na TTL 30 min (wiele kart, odświeżenie, "wstecz" nie unieważniają formularza)
-- **NEW**: `autoload/front/Controllers/ShopBasketController.php` — logowanie błędów zamówień do `logs/logs-order-YYYY-MM-DD.log` (double-submit, token invalid, exception, falsy order_id)
-- **FIX**: `autoload/front/Controllers/ShopBasketController.php` — redirect przy złym tokenie na `/koszyk-podsumowanie` zamiast `/koszyk` (użytkownik nie traci kontekstu)
+- **FIX**: `templates/shop-order/order-details.php` — event purchase: id→item_id (string), name→item_name, price via normalize_decimal (fix price:0), usunięty hardcoded value: 25.42, dodany google_business_vertical
+- **FIX**: `templates/shop-basket/summary-view.php` — event begin_checkout: id→item_id, name→item_name, dodany google_business_vertical
+- **FIX**: `templates/shop-product/product.php` — event view_item: dodany currency PLN, value, price jako number (nie string), google_business_vertical; event add_to_cart: dodany google_business_vertical, parseInt(quantity)
+- **NEW**: `templates/shop-basket/basket.php` — nowy event view_cart na stronie koszyka z pełnym zestawem danych GA4 (item_id, item_name, price, quantity, currency, google_business_vertical)
+- **FIX**: `autoload/front/Controllers/ShopBasketController.php` — usunięty błędny guard w summaryView() blokujący kolejne zamówienia po pierwszym (redirect na stare zamówienie zamiast podsumowanie)
+- **FIX**: `autoload/front/Controllers/ShopBasketController.php` — token zamówienia z jednorazowego na TTL 30 min (wiele kart, odświeżenie, "wstecz" nie unieważniają formularza)
+- **NEW**: `autoload/front/Controllers/ShopBasketController.php` — logowanie błędów zamówień do `logs/logs-order-YYYY-MM-DD.log` (double-submit, token invalid, exception, falsy order_id)
+- **FIX**: `autoload/front/Controllers/ShopBasketController.php` — redirect przy złym tokenie na `/koszyk-podsumowanie` zamiast `/koszyk` (użytkownik nie traci kontekstu)
---
## ver. 0.344 (2026-03-19) - Edycja personalizacji produktu w koszyku
-- **NEW**: `autoload/front/Controllers/ShopBasketController.php` — nowa metoda `basketUpdateCustomFields()`: AJAX endpoint do edycji custom fields w koszyku z walidacją required, przeliczaniem product_code (MD5 hash) i merge duplikatów
-- **NEW**: `templates/shop-basket/_partials/product-custom-fields.php` — przycisk "Edytuj personalizację" + formularz inline z aktualnymi wartościami
-- **NEW**: `templates/shop-basket/basket-details.php` — przekazanie `product_code` do szablonu custom fields
-- **NEW**: `templates/shop-basket/basket.php` — JavaScript obsługi edycji/zapisu/anulowania personalizacji
+- **NEW**: `autoload/front/Controllers/ShopBasketController.php` — nowa metoda `basketUpdateCustomFields()`: AJAX endpoint do edycji custom fields w koszyku z walidacją required, przeliczaniem product_code (MD5 hash) i merge duplikatów
+- **NEW**: `templates/shop-basket/_partials/product-custom-fields.php` — przycisk "Edytuj personalizację" + formularz inline z aktualnymi wartościami
+- **NEW**: `templates/shop-basket/basket-details.php` — przekazanie `product_code` do szablonu custom fields
+- **NEW**: `templates/shop-basket/basket.php` — JavaScript obsługi edycji/zapisu/anulowania personalizacji
---
-## ver. 0.343 (2026-03-19) - Custom fields: type + is_required + obsługa obrazków w koszyku
+## ver. 0.343 (2026-03-19) - Custom fields: type + is_required + obsługa obrazków w koszyku
-- **FIX**: `autoload/Domain/Product/ProductRepository.php` — kopiowanie custom fields przy duplikacji produktu uwzględnia teraz pola `type` i `is_required`
-- **FIX**: `templates/shop-basket/_partials/product-custom-fields.php` — ochrona XSS (htmlspecialchars), obsługa pola typu `image`, bezpieczny fallback typu na `text`
+- **FIX**: `autoload/Domain/Product/ProductRepository.php` — kopiowanie custom fields przy duplikacji produktu uwzględnia teraz pola `type` i `is_required`
+- **FIX**: `templates/shop-basket/_partials/product-custom-fields.php` — ochrona XSS (htmlspecialchars), obsługa pola typu `image`, bezpieczny fallback typu na `text`
---
-## ver. 0.342 (2026-03-19) - Apilo: email z danymi zamówienia + infinite retry dla order jobów
+## ver. 0.342 (2026-03-19) - Apilo: email z danymi zamĂłwienia + infinite retry dla order jobĂłw
-- **FIX**: `cron.php` — email notyfikacji Apilo zawiera teraz dane zamówienia (numer, klient, data, kwota) zamiast surowego JSON payload; temat emaila zawiera numery zamówień
-- **NEW**: `autoload/Domain/CronJob/CronJobType.php` — `isOrderRelatedApiloJob()` identyfikuje order joby (send_order, sync_payment, sync_status)
-- **NEW**: `autoload/Domain/CronJob/CronJobRepository.php` — order-related Apilo joby ponawiane w nieskończoność co 30 min zamiast permanent failure po 10 próbach
-- **NEW**: `cron.php` — email rozróżnia "PONAWIANY CO 30 MIN" (order joby) vs "TRWAŁY BŁĄD" (inne joby)
-- **NEW**: `cron.php` — po udanym wysłaniu zamówienia do Apilo czyszczone są stuck joby sync_payment/sync_status
+- **FIX**: `cron.php` — email notyfikacji Apilo zawiera teraz dane zamówienia (numer, klient, data, kwota) zamiast surowego JSON payload; temat emaila zawiera numery zamówień
+- **NEW**: `autoload/Domain/CronJob/CronJobType.php` — `isOrderRelatedApiloJob()` identyfikuje order joby (send_order, sync_payment, sync_status)
+- **NEW**: `autoload/Domain/CronJob/CronJobRepository.php` — order-related Apilo joby ponawiane w nieskończoność co 30 min zamiast permanent failure po 10 próbach
+- **NEW**: `cron.php` — email rozróżnia "PONAWIANY CO 30 MIN" (order joby) vs "TRWAĹY BĹÄ„D" (inne joby)
+- **NEW**: `cron.php` — po udanym wysłaniu zamówienia do Apilo czyszczone są stuck joby sync_payment/sync_status
- **TEST**: +2 testy infinite retry w `CronJobRepositoryTest`
---
-## ver. 0.341 (2026-03-16) - Bugfix: zamówienia nie wysyłały się do Apilo + retry i powiadomienia
+## ver. 0.341 (2026-03-16) - Bugfix: zamówienia nie wysyłały się do Apilo + retry i powiadomienia
-- **FIX**: `cron.php` — dodano brakujące `$apiloRepository` do klauzul `use()` w 5 handlerach cron (APILO_TOKEN_KEEPALIVE, APILO_SEND_ORDER, APILO_PRODUCT_SYNC, APILO_PRICELIST_SYNC, APILO_STATUS_POLL); regresja z ver. 0.339 (split IntegrationsRepository → ApiloRepository) powodowała `Call to a member function apiloGetAccessToken() on null`
-- **FIX**: `cron.php` — zamówienia z `apilo_order_id = -1` (failed) są teraz automatycznie ponawiane co 1h zamiast trwale pomijane; priorytet: najpierw nowe zamówienia (NULL), potem retry (-1)
-- **NEW**: `cron.php` — powiadomienie mailowe na `biuro@project-pro.pl` przy błędzie cURL wysyłania zamówienia do Apilo
-- **NEW**: `cron.php` — powiadomienie mailowe o trwale nieudanych zadaniach Apilo (po wyczerpaniu `max_attempts`)
+- **FIX**: `cron.php` — dodano brakujące `$apiloRepository` do klauzul `use()` w 5 handlerach cron (APILO_TOKEN_KEEPALIVE, APILO_SEND_ORDER, APILO_PRODUCT_SYNC, APILO_PRICELIST_SYNC, APILO_STATUS_POLL); regresja z ver. 0.339 (split IntegrationsRepository → ApiloRepository) powodowała `Call to a member function apiloGetAccessToken() on null`
+- **FIX**: `cron.php` — zamówienia z `apilo_order_id = -1` (failed) są teraz automatycznie ponawiane co 1h zamiast trwale pomijane; priorytet: najpierw nowe zamówienia (NULL), potem retry (-1)
+- **NEW**: `cron.php` — powiadomienie mailowe na `biuro@project-pro.pl` przy błędzie cURL wysyłania zamówienia do Apilo
+- **NEW**: `cron.php` — powiadomienie mailowe o trwale nieudanych zadaniach Apilo (po wyczerpaniu `max_attempts`)
---
-## ver. 0.340 (2026-03-15) - Bugfix: crash przy składaniu zamówienia z kuponem rabatowym
+## ver. 0.340 (2026-03-15) - Bugfix: crash przy składaniu zamówienia z kuponem rabatowym
-- **FIX**: `autoload/Domain/Order/OrderRepository.php:793` — naprawiono Fatal Error `Call to undefined method stdClass::is_one_time()` przy składaniu zamówienia z kodem rabatowym; zamieniono wywołania nieistniejących metod na stdClass (`is_one_time()`, `set_as_used()`) na dostęp do właściwości + istniejącą metodę `CouponRepository::markAsUsed()`
-- **SONARQUBE**: Pierwszy skan SonarQube — wyniki zapisane w `docs/TODO.md` (4 bugi, 31 critical code smells, 10 major, 8 minor)
+- **FIX**: `autoload/Domain/Order/OrderRepository.php:793` — naprawiono Fatal Error `Call to undefined method stdClass::is_one_time()` przy składaniu zamówienia z kodem rabatowym; zamieniono wywołania nieistniejących metod na stdClass (`is_one_time()`, `set_as_used()`) na dostęp do właściwości + istniejącą metodę `CouponRepository::markAsUsed()`
+- **SONARQUBE**: Pierwszy skan SonarQube — wyniki zapisane w `docs/TODO.md` (4 bugi, 31 critical code smells, 10 major, 8 minor)
---
## ver. 0.339 (2026-03-12) - Refactoring: wydzielenie ApiloRepository z IntegrationsRepository
-- **REFACTOR**: `autoload/Domain/Integrations/ApiloRepository.php` — nowa klasa `\Domain\Integrations\ApiloRepository` z 19 metodami apilo* (sync produktów, zamówień, konfiguracja) wydzielonymi z `IntegrationsRepository`
-- **REFACTOR**: `autoload/Domain/Integrations/IntegrationsRepository.php` — usunięto 19 metod apilo* (~540 linii); klasa zmniejszona z ~875 do ~340 linii, zawiera wyłącznie generyczną logikę integracji (settings, logi, product linking)
-- **REFACTOR**: `autoload/admin/Controllers/IntegrationsController.php` — konsumuje `ApiloRepository` przez DI zamiast `IntegrationsRepository` dla operacji apilo
-- **REFACTOR**: `autoload/Domain/Order/OrderAdminService.php` — używa `ApiloRepository` do wysyłki zamówień do Apilo
-- **REFACTOR**: `cron.php` — używa `ApiloRepository` do synchronizacji cron
-- **REFACTOR**: `autoload/admin/App.php` — wiring DI dla `ApiloRepository`
-- **TEST**: `tests/Unit/Domain/Integrations/ApiloRepositoryTest.php` — nowe testy dla `ApiloRepository`; suite: 818 testów, 2275 asercji
+- **REFACTOR**: `autoload/Domain/Integrations/ApiloRepository.php` — nowa klasa `\Domain\Integrations\ApiloRepository` z 19 metodami apilo* (sync produktów, zamówień, konfiguracja) wydzielonymi z `IntegrationsRepository`
+- **REFACTOR**: `autoload/Domain/Integrations/IntegrationsRepository.php` — usunięto 19 metod apilo* (~540 linii); klasa zmniejszona z ~875 do ~340 linii, zawiera wyłącznie generyczną logikę integracji (settings, logi, product linking)
+- **REFACTOR**: `autoload/admin/Controllers/IntegrationsController.php` — konsumuje `ApiloRepository` przez DI zamiast `IntegrationsRepository` dla operacji apilo
+- **REFACTOR**: `autoload/Domain/Order/OrderAdminService.php` — używa `ApiloRepository` do wysyłki zamówień do Apilo
+- **REFACTOR**: `cron.php` — używa `ApiloRepository` do synchronizacji cron
+- **REFACTOR**: `autoload/admin/App.php` — wiring DI dla `ApiloRepository`
+- **TEST**: `tests/Unit/Domain/Integrations/ApiloRepositoryTest.php` — nowe testy dla `ApiloRepository`; suite: 818 testów, 2275 asercji
---
-## ver. 0.338 (2026-03-12) - Bugfix: duplikaty zamówień + status COD
+## ver. 0.338 (2026-03-12) - Bugfix: duplikaty zamówień + status COD
-- **FIX**: `autoload/front/Controllers/ShopBasketController::summaryView()` — guard przed ponownym złożeniem zamówienia: jeśli sesja zawiera `ORDER_SUBMIT_LAST_ORDER_ID`, użytkownik jest przekierowywany do istniejącego zamówienia zamiast widzieć formularz ponownie
-- **FIX**: `autoload/front/Controllers/ShopBasketController::basketSave()` — owinięcie wywołania `createFromBasket()` w try-catch; wyjątek jest logowany przez `error_log()`, użytkownik widzi komunikat błędu, koszyk sesyjny zostaje zachowany
-- **FIX**: `autoload/Domain/Order/OrderRepository::createFromBasket()` — usunięcie hardkodowanego `payment_id == 3` do wykrywania płatności przy odbiorze; zamiast tego używana jest flaga `$payment_method['is_cod']`
-- **FEATURE**: `autoload/Domain/PaymentMethod/PaymentMethodRepository` — nowa kolumna `is_cod` (normalizacja, zapis w `save()`, kolumna w `forTransport()` SQL)
-- **FEATURE**: `autoload/admin/Controllers/ShopPaymentMethodController` — nowe pole "Platnosc przy odbiorze" w formularzu edycji metody płatności
-- **MIGRATION**: `migrations/0.338.sql` — `ALTER TABLE pp_shop_payment_methods ADD COLUMN is_cod TINYINT(1) NOT NULL DEFAULT 0`
+- **FIX**: `autoload/front/Controllers/ShopBasketController::summaryView()` — guard przed ponownym złożeniem zamówienia: jeśli sesja zawiera `ORDER_SUBMIT_LAST_ORDER_ID`, użytkownik jest przekierowywany do istniejącego zamówienia zamiast widzieć formularz ponownie
+- **FIX**: `autoload/front/Controllers/ShopBasketController::basketSave()` — owinięcie wywołania `createFromBasket()` w try-catch; wyjątek jest logowany przez `error_log()`, użytkownik widzi komunikat błędu, koszyk sesyjny zostaje zachowany
+- **FIX**: `autoload/Domain/Order/OrderRepository::createFromBasket()` — usunięcie hardkodowanego `payment_id == 3` do wykrywania płatności przy odbiorze; zamiast tego używana jest flaga `$payment_method['is_cod']`
+- **FEATURE**: `autoload/Domain/PaymentMethod/PaymentMethodRepository` — nowa kolumna `is_cod` (normalizacja, zapis w `save()`, kolumna w `forTransport()` SQL)
+- **FEATURE**: `autoload/admin/Controllers/ShopPaymentMethodController` — nowe pole "Platnosc przy odbiorze" w formularzu edycji metody płatności
+- **MIGRATION**: `migrations/0.338.sql` — `ALTER TABLE pp_shop_payment_methods ADD COLUMN is_cod TINYINT(1) NOT NULL DEFAULT 0`
---
-## ver. 0.337 (2026-03-12) - Bezpieczeństwo: ochrona CSRF panelu administracyjnego
+## ver. 0.337 (2026-03-12) - Bezpieczeństwo: ochrona CSRF panelu administracyjnego
-- **SECURITY**: `autoload/Shared/Security/CsrfToken.php` — nowa klasa z `getToken()`, `validate()`, `regenerate()` (token 64-znakowy hex, `hash_equals()` przeciw timing attacks)
-- **SECURITY**: `admin/templates/components/form-edit.php` — dodano ukryte pole `_csrf_token` we wszystkich formularzach edycji
-- **SECURITY**: `autoload/admin/Support/Forms/FormRequestHandler::handleSubmit()` — walidacja CSRF przed przetworzeniem danych formularza
-- **SECURITY**: `admin/templates/site/unlogged-layout.php` — token CSRF w formularzu logowania + fix XSS na komunikacie alertu (`htmlspecialchars`)
-- **SECURITY**: `admin/templates/users/user-2fa.php` — token CSRF w obu formularzach 2FA (weryfikacja i resend)
-- **SECURITY**: `autoload/admin/App::special_actions()` — walidacja CSRF dla żądań POST; regeneracja tokenu po udanym logowaniu (obie ścieżki: bezpośrednia i przez 2FA)
-- **TEST**: `tests/Unit/Shared/Security/CsrfTokenTest.php` — 7 nowych testów; suite: 817 testów, 2271 asercji
+- **SECURITY**: `autoload/Shared/Security/CsrfToken.php` — nowa klasa z `getToken()`, `validate()`, `regenerate()` (token 64-znakowy hex, `hash_equals()` przeciw timing attacks)
+- **SECURITY**: `admin/templates/components/form-edit.php` — dodano ukryte pole `_csrf_token` we wszystkich formularzach edycji
+- **SECURITY**: `autoload/admin/Support/Forms/FormRequestHandler::handleSubmit()` — walidacja CSRF przed przetworzeniem danych formularza
+- **SECURITY**: `admin/templates/site/unlogged-layout.php` — token CSRF w formularzu logowania + fix XSS na komunikacie alertu (`htmlspecialchars`)
+- **SECURITY**: `admin/templates/users/user-2fa.php` — token CSRF w obu formularzach 2FA (weryfikacja i resend)
+- **SECURITY**: `autoload/admin/App::special_actions()` — walidacja CSRF dla żądań POST; regeneracja tokenu po udanym logowaniu (obie ścieżki: bezpośrednia i przez 2FA)
+- **TEST**: `tests/Unit/Shared/Security/CsrfTokenTest.php` — 7 nowych testów; suite: 817 testów, 2271 asercji
---
-## ver. 0.336 (2026-03-12) - Poprawki bezpieczeństwa: error handling w krytycznych ścieżkach
+## ver. 0.336 (2026-03-12) - Poprawki bezpieczeństwa: error handling w krytycznych ścieżkach
-- **FIX**: `cron.php` — przywrócono `E_WARNING` i `E_DEPRECATED` (wyciszano je od zawsze, ukrywając potencjalne błędy)
-- **FIX**: `IntegrationsRepository::apiloAuthorize()` — try-catch po zapisie tokenów Apilo; błąd DB logowany i zwraca `false` zamiast cicho kontynuować
-- **FIX**: `ProductRepository::safeUnlink()` — `error_log()` gdy ścieżka istnieje ale jest poza `upload/`
-- **FIX**: `ArticleRepository::safeUnlink()` — to samo
+- **FIX**: `cron.php` — przywrócono `E_WARNING` i `E_DEPRECATED` (wyciszano je od zawsze, ukrywając potencjalne błędy)
+- **FIX**: `IntegrationsRepository::apiloAuthorize()` — try-catch po zapisie tokenów Apilo; błąd DB logowany i zwraca `false` zamiast cicho kontynuować
+- **FIX**: `ProductRepository::safeUnlink()` — `error_log()` gdy ścieżka istnieje ale jest poza `upload/`
+- **FIX**: `ArticleRepository::safeUnlink()` — to samo
---
-## ver. 0.335 (2026-03-12) - Poprawki bezpieczeństwa: path traversal i XSS w szablonach
+## ver. 0.335 (2026-03-12) - Poprawki bezpieczeństwa: path traversal i XSS w szablonach
-- **SECURITY**: `ProductRepository` — dodano `safeUnlink()` z walidacją `realpath()` zapobiegającą path traversal; użyta w `cleanupDeletedFiles()`, `cleanupDeletedImages()`, `deleteNonassignedImages()`
-- **SECURITY**: `ArticleRepository` — to samo; użyta w `deleteMarkedImages()`, `deleteMarkedFiles()`, `deleteNonassignedFiles()`, `deleteNonassignedImages()`
-- **SECURITY**: `templates/articles/article-full.php` — `htmlspecialchars()` na tytule artykułu, `$_SERVER['SERVER_NAME']` i `$url` w linkach social media
-- **SECURITY**: `templates/articles/article-entry.php` — `htmlspecialchars()` na tytule i `$url` (3 miejsca: href, title, alt)
+- **SECURITY**: `ProductRepository` — dodano `safeUnlink()` z walidacją `realpath()` zapobiegającą path traversal; użyta w `cleanupDeletedFiles()`, `cleanupDeletedImages()`, `deleteNonassignedImages()`
+- **SECURITY**: `ArticleRepository` — to samo; użyta w `deleteMarkedImages()`, `deleteMarkedFiles()`, `deleteNonassignedFiles()`, `deleteNonassignedImages()`
+- **SECURITY**: `templates/articles/article-full.php` — `htmlspecialchars()` na tytule artykułu, `$_SERVER['SERVER_NAME']` i `$url` w linkach social media
+- **SECURITY**: `templates/articles/article-entry.php` — `htmlspecialchars()` na tytule i `$url` (3 miejsca: href, title, alt)
---
-## ver. 0.334 (2026-03-12) - Poprawki bezpieczeństwa: debug log, SQL, RedBeanPHP
+## ver. 0.334 (2026-03-12) - Poprawki bezpieczeństwa: debug log, SQL, RedBeanPHP
-- **SECURITY**: `ShopOrderController::paymentStatusTpay()` — usunięto `file_put_contents('tpay.txt', ...)` który logował pełne dane POST/GET płatności do publicznego pliku
-- **SECURITY**: `ShopOrderController` — hardcoded sekret HotPay `"ProjectPro1916;"` przeniesiony do prywatnej stałej `HOTPAY_HASH_SEED`
-- **SECURITY**: `IntegrationsRepository::getSettings()` — zastąpiono raw `query("SELECT * FROM $table")` metodą Medoo `select()` (spójne z zasadą braku string concatenation w SQL)
-- **REFACTOR**: `index.php`, `admin/index.php` — usunięto RedBeanPHP (`rb.php`): biblioteka była ładowana i inicjalizowana, ale nigdy nie używana w żadnym zapytaniu
-- **CLEANUP**: `libraries/rb.php` — usunięto plik (536 KB zbędnych zależności)
-- **TESTS**: `IntegrationsRepositoryTest` — zaktualizowano 6 testów do nowego API (`select` zamiast `query` dla `getSettings`)
+- **SECURITY**: `ShopOrderController::paymentStatusTpay()` — usunięto `file_put_contents('tpay.txt', ...)` który logował pełne dane POST/GET płatności do publicznego pliku
+- **SECURITY**: `ShopOrderController` — hardcoded sekret HotPay `"ProjectPro1916;"` przeniesiony do prywatnej stałej `HOTPAY_HASH_SEED`
+- **SECURITY**: `IntegrationsRepository::getSettings()` — zastąpiono raw `query("SELECT * FROM $table")` metodą Medoo `select()` (spójne z zasadą braku string concatenation w SQL)
+- **REFACTOR**: `index.php`, `admin/index.php` — usunięto RedBeanPHP (`rb.php`): biblioteka była ładowana i inicjalizowana, ale nigdy nie używana w żadnym zapytaniu
+- **CLEANUP**: `libraries/rb.php` — usunięto plik (536 KB zbędnych zależności)
+- **TESTS**: `IntegrationsRepositoryTest` — zaktualizowano 6 testów do nowego API (`select` zamiast `query` dla `getSettings`)
---
-## ver. 0.333 (2026-03-10) - Ochrona przed podwójnym składaniem zamówienia (order submit token)
+## ver. 0.333 (2026-03-10) - Ochrona przed podwójnym składaniem zamówienia (order submit token)
-- **NEW**: `ShopBasketController` — mechanizm tokenu CSRF chroniący przed podwójnym składaniem zamówienia (generowanie, walidacja, konsumpcja tokenu w sesji)
-- **NEW**: `ShopBasketController::basketSave()` — przy duplikacie przekierowanie do istniejącego zamówienia zamiast tworzenia kolejnego
-- **FIX**: `templates/shop-basket/summary-view.php` — JS nasłuchuje na `submit` formularza zamiast `click` przycisku (poprawna obsługa walidacji HTML5)
-- **FIX**: `templates/shop-basket/address-form.php` — ukryte pole `order_submit_token` z escape XSS
-- **TESTS**: `ShopBasketControllerTest` — testy konstruktora i zależności (5 testów)
+- **NEW**: `ShopBasketController` — mechanizm tokenu CSRF chroniący przed podwójnym składaniem zamówienia (generowanie, walidacja, konsumpcja tokenu w sesji)
+- **NEW**: `ShopBasketController::basketSave()` — przy duplikacie przekierowanie do istniejącego zamówienia zamiast tworzenia kolejnego
+- **FIX**: `templates/shop-basket/summary-view.php` — JS nasłuchuje na `submit` formularza zamiast `click` przycisku (poprawna obsługa walidacji HTML5)
+- **FIX**: `templates/shop-basket/address-form.php` — ukryte pole `order_submit_token` z escape XSS
+- **TESTS**: `ShopBasketControllerTest` — testy konstruktora i zależności (5 testów)
---
-## ver. 0.332 (2026-03-01) - API produktów: nowe pola new_to_date i additional_message
+## ver. 0.332 (2026-03-01) - API produktĂłw: nowe pola new_to_date i additional_message
-- **NEW**: `ProductRepository::getProductForApi()` — eksportuje 4 nowe pola: `new_to_date`, `additional_message` (int 0/1), `additional_message_required` (int 0/1), `additional_message_text`
-- **NEW**: `ProductsApiController` — obsługa nowych pól w PUT/PATCH (aktualizacja `new_to_date`, `additional_message`, `additional_message_required`, `additional_message_text`)
-- **DOCS**: `docs/API.md` — zaktualizowane przykłady GET/PUT dla nowych pól produktu
+- **NEW**: `ProductRepository::getProductForApi()` — eksportuje 4 nowe pola: `new_to_date`, `additional_message` (int 0/1), `additional_message_required` (int 0/1), `additional_message_text`
+- **NEW**: `ProductsApiController` — obsługa nowych pól w PUT/PATCH (aktualizacja `new_to_date`, `additional_message`, `additional_message_required`, `additional_message_text`)
+- **DOCS**: `docs/API.md` — zaktualizowane przykłady GET/PUT dla nowych pól produktu
---
-## ver. 0.331 (2026-03-01) - Bugfix: strona produktu używała layoutu kategorii zamiast domyślnego
+## ver. 0.331 (2026-03-01) - Bugfix: strona produktu używała layoutu kategorii zamiast domyślnego
-- **FIX**: `LayoutsRepository::getProductLayout()` — fallback gdy produkt i jego kategorie nie mają przypisanego layoutu zmieniany z `categories_default = 1` na `status = 1`; wcześniej produkty bez layoutu pobierały szablon "Podstrony - kategorie" zamiast właściwego domyślnego
+- **FIX**: `LayoutsRepository::getProductLayout()` — fallback gdy produkt i jego kategorie nie mają przypisanego layoutu zmieniany z `categories_default = 1` na `status = 1`; wcześniej produkty bez layoutu pobierały szablon "Podstrony - kategorie" zamiast właściwego domyślnego
---
-## ver. 0.330 (2026-02-27) - Eliminacja htaccess.conf — wszystkie trasy URL w pp_routes
+## ver. 0.330 (2026-02-27) - Eliminacja htaccess.conf — wszystkie trasy URL w pp_routes
-- **REFACTOR**: `Helpers::htacces()` — generowanie `.htaccess` w całości z PHP (usunięty `file_get_contents('htaccess.conf')` i placeholder `{HTACCESS_CACHE}`)
-- **NEW**: 32 statyczne trasy systemowe wstawiane do `pp_routes` z `type='system'` przy każdym `htacces()` (koszyk, logowanie, wylogowanie, panel klienta, newsletter, zamówienia, płatności, moduły AJAX: shopBasket/shopClient/shopProduct/shopCoupon/search)
-- **NEW**: Dynamiczne trasy językowe i producentów (producenci + per-producent z paginacją) przenoszone do `pp_routes` zamiast `.htaccess`
-- **NEW**: Kolumna `type VARCHAR(20) NULL` w `pp_routes` — `NULL` dla encji, `'system'` dla tras systemowych
-- **REMOVED**: `libraries/htaccess.conf` — plik szablonu usunięty, treść wbudowana w PHP
-- **PERF**: Invalidacja cache Redis `pp_routes:all` po każdym `htacces()` — świeże trasy przy kolejnym żądaniu
+- **REFACTOR**: `Helpers::htacces()` — generowanie `.htaccess` w całości z PHP (usunięty `file_get_contents('htaccess.conf')` i placeholder `{HTACCESS_CACHE}`)
+- **NEW**: 32 statyczne trasy systemowe wstawiane do `pp_routes` z `type='system'` przy każdym `htacces()` (koszyk, logowanie, wylogowanie, panel klienta, newsletter, zamówienia, płatności, moduły AJAX: shopBasket/shopClient/shopProduct/shopCoupon/search)
+- **NEW**: Dynamiczne trasy językowe i producentów (producenci + per-producent z paginacją) przenoszone do `pp_routes` zamiast `.htaccess`
+- **NEW**: Kolumna `type VARCHAR(20) NULL` w `pp_routes` — `NULL` dla encji, `'system'` dla tras systemowych
+- **REMOVED**: `libraries/htaccess.conf` — plik szablonu usunięty, treść wbudowana w PHP
+- **PERF**: Invalidacja cache Redis `pp_routes:all` po każdym `htacces()` — świeże trasy przy kolejnym żądaniu
- **MIGRATION**: `migrations/0.329.sql` (dodano `type` column)
-- **DOCS**: `docs/DATABASE_STRUCTURE.md` — zaktualizowana sekcja `pp_routes` o kolumnę `type`
+- **DOCS**: `docs/DATABASE_STRUCTURE.md` — zaktualizowana sekcja `pp_routes` o kolumnę `type`
---
-## ver. 0.329 (2026-02-27) - Routing kategorii, stron i artykułów przez pp_routes
+## ver. 0.329 (2026-02-27) - Routing kategorii, stron i artykułów przez pp_routes
-- **REFACTOR**: `index.php` — blok routingu przez `pp_routes` przeniesiony PRZED `checkUrlParams()` (poprawna kolejność: lang/a=page dostępne w checkUrlParams)
-- **PERF**: Cache Redis dla tras (`pp_routes:all`, TTL 86400s) w `index.php` — jeden SELECT na 24h zamiast przy każdym żądaniu
-- **NEW**: Kategorie, strony i artykuły zapisywane do `pp_routes` zamiast `.htaccess` w `Helpers::htacces()`
-- **NEW**: `CategoryRepository::categoryDelete()` — usuwa powiązane `pp_routes` przed odświeżeniem
-- **NEW**: `PagesRepository::pageDelete()` — usuwa powiązane `pp_routes`
-- **NEW**: `ArticleRepository::archive()` i `deletePermanently()` — usuwa powiązane `pp_routes`
-- **MIGRATION**: `migrations/0.329.sql` — `ALTER TABLE pp_routes ADD COLUMN category_id, page_id, article_id`
+- **REFACTOR**: `index.php` — blok routingu przez `pp_routes` przeniesiony PRZED `checkUrlParams()` (poprawna kolejność: lang/a=page dostępne w checkUrlParams)
+- **PERF**: Cache Redis dla tras (`pp_routes:all`, TTL 86400s) w `index.php` — jeden SELECT na 24h zamiast przy każdym żądaniu
+- **NEW**: Kategorie, strony i artykuły zapisywane do `pp_routes` zamiast `.htaccess` w `Helpers::htacces()`
+- **NEW**: `CategoryRepository::categoryDelete()` — usuwa powiązane `pp_routes` przed odświeżeniem
+- **NEW**: `PagesRepository::pageDelete()` — usuwa powiązane `pp_routes`
+- **NEW**: `ArticleRepository::archive()` i `deletePermanently()` — usuwa powiązane `pp_routes`
+- **MIGRATION**: `migrations/0.329.sql` — `ALTER TABLE pp_routes ADD COLUMN category_id, page_id, article_id`
- **TESTS**: Zaktualizowane `CategoryRepositoryTest` i `ArticleRepositoryTest` (nowe asercje na `pp_routes` delete)
---
-## ver. 0.328 (2026-02-27) - Ikona kopiowania wartości atrybutów w szczegółach zamówienia
+## ver. 0.328 (2026-02-27) - Ikona kopiowania wartości atrybutów w szczegółach zamówienia
-- **NEW**: `order-details-custom-script.php` — JS parsuje `.atributes` div i wstrzykuje przycisk `fa-copy` przy każdej wartości atrybutu
-- **UX**: Kliknięcie kopiuje wartość do schowka (Clipboard API + fallback execCommand), ikona zmienia się na `fa-check` z zielonym tłem przez 1,5s
+- **NEW**: `order-details-custom-script.php` — JS parsuje `.atributes` div i wstrzykuje przycisk `fa-copy` przy każdej wartości atrybutu
+- **UX**: Kliknięcie kopiuje wartość do schowka (Clipboard API + fallback execCommand), ikona zmienia się na `fa-check` z zielonym tłem przez 1,5s
---
-## ver. 0.327 (2026-02-27) - Masowe usuwanie w archiwum produktów
+## ver. 0.327 (2026-02-27) - Masowe usuwanie w archiwum produktĂłw
-- **NEW**: `ProductArchiveController::bulk_delete_permanent()` — endpoint POST `product_archive/bulk_delete_permanent/`, przyjmuje `ids[]`, usuwa każdy produkt przez `ProductRepository::delete()`, zwraca JSON `{success, deleted, errors[]}`
-- **UX**: Kolumna checkboxów w liście archiwum produktów + pasek akcji masowych z licznikiem zaznaczonych
-- **UX**: "Zaznacz wszystkie" w nagłówku tabeli (wstrzyknięty via JS), dialog potwierdzenia przed masowym usunięciem
-- **TEST**: 2 nowe testy w `ProductArchiveControllerTest` — weryfikacja istnienia i sygnatury `bulk_delete_permanent`
+- **NEW**: `ProductArchiveController::bulk_delete_permanent()` — endpoint POST `product_archive/bulk_delete_permanent/`, przyjmuje `ids[]`, usuwa każdy produkt przez `ProductRepository::delete()`, zwraca JSON `{success, deleted, errors[]}`
+- **UX**: Kolumna checkboxów w liście archiwum produktów + pasek akcji masowych z licznikiem zaznaczonych
+- **UX**: "Zaznacz wszystkie" w nagłówku tabeli (wstrzyknięty via JS), dialog potwierdzenia przed masowym usunięciem
+- **TEST**: 2 nowe testy w `ProductArchiveControllerTest` — weryfikacja istnienia i sygnatury `bulk_delete_permanent`
---
## ver. 0.326 (2026-02-27) - API: endpoint categories/list
-- **NEW**: `api\Controllers\CategoriesApiController` — nowy kontroler API z akcją `list`
-- **NEW**: Endpoint `GET api.php?endpoint=categories&action=list` — zwraca płaską listę aktywnych kategorii (id, parent_id, title) w domyślnym języku sklepu
-- **FIX**: Usunięto zbędny parametr w `CategoryRepository`, eliminacja N+1 queries w categories/list przez bulk-fetch tytułów
+- **NEW**: `api\Controllers\CategoriesApiController` — nowy kontroler API z akcją `list`
+- **NEW**: Endpoint `GET api.php?endpoint=categories&action=list` — zwraca płaską listę aktywnych kategorii (id, parent_id, title) w domyślnym języku sklepu
+- **FIX**: Usunięto zbędny parametr w `CategoryRepository`, eliminacja N+1 queries w categories/list przez bulk-fetch tytułów
---
-## ver. 0.325 (2026-02-27) - Fix changelog encoding + limit wyświetlania
+## ver. 0.325 (2026-02-27) - Fix changelog encoding + limit wyświetlania
-- **FIX**: `updates/changelog.php` — naprawione krzaczki (mojibake) w polskich znakach; dane odbudowane z plików manifest
-- **NEW**: `updates/changelog-data.html` — czyste dane changelog oddzielone od logiki PHP
-- **REFACTOR**: `updates/changelog.php` — konwersja ze statycznego HTML na skrypt PHP: `Content-Type: utf-8`, parsowanie wpisów, filtrowanie po wersji
-- **NEW**: Parametr `?ver=X.XXX` — ogranicza changelog do 5 wersji wstecz od wersji instancji
-- **UPDATE**: `admin/templates/update/main-view.php` — przekazuje `?ver=` do URL changelog
-- **UPDATE**: `build-update.ps1` — nowe wpisy dopisywane do `changelog-data.html` zamiast `changelog.php`
+- **FIX**: `updates/changelog.php` — naprawione krzaczki (mojibake) w polskich znakach; dane odbudowane z plików manifest
+- **NEW**: `updates/changelog-data.html` — czyste dane changelog oddzielone od logiki PHP
+- **REFACTOR**: `updates/changelog.php` — konwersja ze statycznego HTML na skrypt PHP: `Content-Type: utf-8`, parsowanie wpisów, filtrowanie po wersji
+- **NEW**: Parametr `?ver=X.XXX` — ogranicza changelog do 5 wersji wstecz od wersji instancji
+- **UPDATE**: `admin/templates/update/main-view.php` — przekazuje `?ver=` do URL changelog
+- **UPDATE**: `build-update.ps1` — nowe wpisy dopisywane do `changelog-data.html` zamiast `changelog.php`
---
-## ver. 0.324 (2026-02-27) - System kolejki zadań cron
+## ver. 0.324 (2026-02-27) - System kolejki zadań cron
-- **NEW**: `Domain\CronJob\CronJobType` — stałe typów zadań, priorytetów, statusów, exponential backoff
-- **NEW**: `Domain\CronJob\CronJobRepository` — CRUD na `pp_cron_jobs` + `pp_cron_schedules` (enqueue, fetchNext, markCompleted, markFailed, hasPendingJob, cleanup, recoverStuck, getDueSchedules, touchSchedule)
-- **NEW**: `Domain\CronJob\CronJobProcessor` — orkiestracja: rejestracja handlerów, tworzenie scheduled jobs, przetwarzanie kolejki z priorytetami i retry/backoff
-- **NEW**: Tabele `pp_cron_jobs` i `pp_cron_schedules` — kolejka zadań z priorytetami, exponential backoff, harmonogram cykliczny
-- **REFACTOR**: `cron.php` — zastąpienie monolitycznego ~550 linii orkiestratorem z CronJobProcessor i zarejestrowanymi handlerami
-- **REFACTOR**: `OrderAdminService::queueApiloSync()` — kolejkowanie przez `CronJobRepository::enqueue()` zamiast pliku JSON
-- **REFACTOR**: `OrderAdminService::syncApiloPayment()`, `syncApiloStatus()` — zmiana z private na public (używane przez handlery cron)
-- **REMOVED**: `OrderAdminService::processApiloSyncQueue()`, `loadApiloSyncQueue()`, `saveApiloSyncQueue()`, `apiloSyncQueuePath()`, stała `APILO_SYNC_QUEUE_FILE`
-- **NEW**: Jednorazowa migracja JSON queue → DB w cron.php (automatyczna przy pierwszym uruchomieniu)
-- **SECURITY**: `cron.php` — ochrona endpointu: wymaga `$config['cron_key']` w URL (`?key=...`) lub trybu CLI
-- **FIX**: `CronJobRepository::fetchNext()` — re-SELECT po UPDATE eliminuje race condition przy równoległych workerach
-- **FIX**: `cron.php` — null check dla `$mdb->query()` przed `->fetch()` / `->fetchAll()` (3 miejsca)
-- **FIX**: `cron.php` — walidacja odpowiedzi curl w APILO_PRODUCT_SYNC i APILO_PRICELIST_SYNC (zapobiega zapisaniu null do bazy)
-- **FIX**: DI wiring — `CronJobRepository` przekazywany do `OrderAdminService` we wszystkich 4 punktach: `admin\App`, `api\ApiRouter`, `front\App`, `cron.php`
-- **TESTS**: 41 nowych testów CronJob (CronJobTypeTest, CronJobRepositoryTest, CronJobProcessorTest)
+- **NEW**: `Domain\CronJob\CronJobType` — stałe typów zadań, priorytetów, statusów, exponential backoff
+- **NEW**: `Domain\CronJob\CronJobRepository` — CRUD na `pp_cron_jobs` + `pp_cron_schedules` (enqueue, fetchNext, markCompleted, markFailed, hasPendingJob, cleanup, recoverStuck, getDueSchedules, touchSchedule)
+- **NEW**: `Domain\CronJob\CronJobProcessor` — orkiestracja: rejestracja handlerów, tworzenie scheduled jobs, przetwarzanie kolejki z priorytetami i retry/backoff
+- **NEW**: Tabele `pp_cron_jobs` i `pp_cron_schedules` — kolejka zadań z priorytetami, exponential backoff, harmonogram cykliczny
+- **REFACTOR**: `cron.php` — zastąpienie monolitycznego ~550 linii orkiestratorem z CronJobProcessor i zarejestrowanymi handlerami
+- **REFACTOR**: `OrderAdminService::queueApiloSync()` — kolejkowanie przez `CronJobRepository::enqueue()` zamiast pliku JSON
+- **REFACTOR**: `OrderAdminService::syncApiloPayment()`, `syncApiloStatus()` — zmiana z private na public (używane przez handlery cron)
+- **REMOVED**: `OrderAdminService::processApiloSyncQueue()`, `loadApiloSyncQueue()`, `saveApiloSyncQueue()`, `apiloSyncQueuePath()`, stała `APILO_SYNC_QUEUE_FILE`
+- **NEW**: Jednorazowa migracja JSON queue → DB w cron.php (automatyczna przy pierwszym uruchomieniu)
+- **SECURITY**: `cron.php` — ochrona endpointu: wymaga `$config['cron_key']` w URL (`?key=...`) lub trybu CLI
+- **FIX**: `CronJobRepository::fetchNext()` — re-SELECT po UPDATE eliminuje race condition przy równoległych workerach
+- **FIX**: `cron.php` — null check dla `$mdb->query()` przed `->fetch()` / `->fetchAll()` (3 miejsca)
+- **FIX**: `cron.php` — walidacja odpowiedzi curl w APILO_PRODUCT_SYNC i APILO_PRICELIST_SYNC (zapobiega zapisaniu null do bazy)
+- **FIX**: DI wiring — `CronJobRepository` przekazywany do `OrderAdminService` we wszystkich 4 punktach: `admin\App`, `api\ApiRouter`, `front\App`, `cron.php`
+- **TESTS**: 41 nowych testĂłw CronJob (CronJobTypeTest, CronJobRepositoryTest, CronJobProcessorTest)
- **MIGRATION**: `migrations/0.324.sql`
---
-## ver. 0.323 (2026-02-24) - Import zdjęć, trwałe usuwanie, fix API upload
+## ver. 0.323 (2026-02-24) - Import zdjęć, trwałe usuwanie, fix API upload
-- **FIX**: `IntegrationsRepository::shopproImportProduct()` — kompletny refactor importu zdjęć: walidacja HTTP response, curl timeouty, bezpieczna budowa URL, szczegółowy log do `logs/shoppro-import-debug.log` i `error_log`, czytelny komunikat z wynikiem
-- **FIX**: `ProductRepository::saveProduct()` — `saveCustomFields()` wywoływane tylko gdy klucz `custom_field_name` istnieje w danych (partial update przez API nie czyści custom fields)
-- **FIX**: `ProductRepository::delete()` — usuwanie rekordów z `pp_shop_products_custom_fields` przy kasowaniu produktu
-- **FIX**: `ProductsApiController::upload_image()` — poprawka ścieżki uploadu (`upload/` zamiast `../upload/` — api.php działa z rootu projektu)
-- **NEW**: `ProductArchiveController::delete_permanent()` — trwałe usunięcie produktu z archiwum (wraz ze zdjęciami i załącznikami)
-- **NEW**: Przycisk "Usuń trwale" w liście produktów archiwalnych z potwierdzeniem
+- **FIX**: `IntegrationsRepository::shopproImportProduct()` — kompletny refactor importu zdjęć: walidacja HTTP response, curl timeouty, bezpieczna budowa URL, szczegółowy log do `logs/shoppro-import-debug.log` i `error_log`, czytelny komunikat z wynikiem
+- **FIX**: `ProductRepository::saveProduct()` — `saveCustomFields()` wywoływane tylko gdy klucz `custom_field_name` istnieje w danych (partial update przez API nie czyści custom fields)
+- **FIX**: `ProductRepository::delete()` — usuwanie rekordów z `pp_shop_products_custom_fields` przy kasowaniu produktu
+- **FIX**: `ProductsApiController::upload_image()` — poprawka ścieżki uploadu (`upload/` zamiast `../upload/` — api.php działa z rootu projektu)
+- **NEW**: `ProductArchiveController::delete_permanent()` — trwałe usunięcie produktu z archiwum (wraz ze zdjęciami i załącznikami)
+- **NEW**: Przycisk "Usuń trwale" w liście produktów archiwalnych z potwierdzeniem
---
-## ver. 0.318 (2026-02-24) - ShopPRO export produktów + API endpoints
+## ver. 0.318 (2026-02-24) - ShopPRO export produktĂłw + API endpoints
-- **NEW**: `IntegrationsRepository::shopproExportProduct()` — eksport produktu do zdalnej instancji shopPRO: pola główne, tłumaczenia, custom fields, zdjęcia przez API (base64)
-- **NEW**: `IntegrationsRepository::sendImageToShopproApi()` — wysyłka zdjęć do remote API shopPRO (endpoint `upload_image`) z base64
-- **REFACTOR**: `shopproImportProduct()` — wydzielono `shopproDb()` i `missingShopproSetting()` jako prywatne helpery; dodano import `security_information`, `producer_id`, custom fields i `alt` zdjęcia
-- **NEW**: `AttributeRepository::ensureAttributeForApi()` i `ensureAttributeValueForApi()` — idempotent find-or-create dla atrybutów i ich wartości (integracje API)
-- **NEW**: API endpoint `POST /api.php?endpoint=dictionaries&action=ensure_attribute` — utwórz lub znajdź atrybut po nazwie i typie
-- **NEW**: API endpoint `POST /api.php?endpoint=dictionaries&action=ensure_attribute_value` — utwórz lub znajdź wartość atrybutu po nazwie
-- **NEW**: API endpoint `POST /api.php?endpoint=products&action=upload_image` — przyjmuje zdjęcie produktu jako base64 JSON, zapisuje plik i rekord w `pp_shop_products_images`
-- **NEW**: `IntegrationsController::shoppro_product_export()` — akcja admina eksportująca produkt do shopPRO
-- **NEW**: Przycisk "Eksportuj do shopPRO" w liście produktów (widoczny gdy shopPRO enabled)
+- **NEW**: `IntegrationsRepository::shopproExportProduct()` — eksport produktu do zdalnej instancji shopPRO: pola główne, tłumaczenia, custom fields, zdjęcia przez API (base64)
+- **NEW**: `IntegrationsRepository::sendImageToShopproApi()` — wysyłka zdjęć do remote API shopPRO (endpoint `upload_image`) z base64
+- **REFACTOR**: `shopproImportProduct()` — wydzielono `shopproDb()` i `missingShopproSetting()` jako prywatne helpery; dodano import `security_information`, `producer_id`, custom fields i `alt` zdjęcia
+- **NEW**: `AttributeRepository::ensureAttributeForApi()` i `ensureAttributeValueForApi()` — idempotent find-or-create dla atrybutów i ich wartości (integracje API)
+- **NEW**: API endpoint `POST /api.php?endpoint=dictionaries&action=ensure_attribute` — utwórz lub znajdź atrybut po nazwie i typie
+- **NEW**: API endpoint `POST /api.php?endpoint=dictionaries&action=ensure_attribute_value` — utwórz lub znajdź wartość atrybutu po nazwie
+- **NEW**: API endpoint `POST /api.php?endpoint=products&action=upload_image` — przyjmuje zdjęcie produktu jako base64 JSON, zapisuje plik i rekord w `pp_shop_products_images`
+- **NEW**: `IntegrationsController::shoppro_product_export()` — akcja admina eksportująca produkt do shopPRO
+- **NEW**: Przycisk "Eksportuj do shopPRO" w liście produktów (widoczny gdy shopPRO enabled)
- **NEW**: Pole "API key" w ustawieniach integracji shopPRO (`shoppro-settings.php`)
---
## ver. 0.317 (2026-02-23) - Klucz API: przycisk generowania + fix zapisu
-- **FIX**: `SettingsRepository::saveSettings()` — pole `api_key` brakowało w whiteliście zapisywanych pól, przez co wartość była tracona przy każdym zapisie (TRUNCATE + insert)
-- **NEW**: Pole "Klucz API" w ustawieniach — przycisk "Generuj" do losowego 32-znakowego klucza alfanumerycznego, usunięto "(ordersPRO)" z nazwy
-- **FIX**: `api.php` — routing API przeniesiony przed ładowanie globalnych settings (wczesne wyjście), obsługa błędów przez `\Throwable`
-- **FIX**: `ApiRouter` — catch `\Throwable` zamiast `\Exception` dla pełniejszego łapania błędów
+- **FIX**: `SettingsRepository::saveSettings()` — pole `api_key` brakowało w whiteliście zapisywanych pól, przez co wartość była tracona przy każdym zapisie (TRUNCATE + insert)
+- **NEW**: Pole "Klucz API" w ustawieniach — przycisk "Generuj" do losowego 32-znakowego klucza alfanumerycznego, usunięto "(ordersPRO)" z nazwy
+- **FIX**: `api.php` — routing API przeniesiony przed ładowanie globalnych settings (wczesne wyjście), obsługa błędów przez `\Throwable`
+- **FIX**: `ApiRouter` — catch `\Throwable` zamiast `\Exception` dla pełniejszego łapania błędów
---
-## ver. 0.316 (2026-02-23) - Migracja brakującej kolumny type w custom fields
+## ver. 0.316 (2026-02-23) - Migracja brakujÄ…cej kolumny type w custom fields
-- **FIX**: Dodanie brakującej kolumny `type` w tabeli `pp_shop_products_custom_fields` — kolumna była używana w kodzie od v0.277 ale nigdy nie miała migracji ALTER TABLE, przez co instancje ze starszą bazą dostawały `PDOException: Column not found: 1054 Unknown column 'type'` przy zapisie produktu
+- **FIX**: Dodanie brakującej kolumny `type` w tabeli `pp_shop_products_custom_fields` — kolumna była używana w kodzie od v0.277 ale nigdy nie miała migracji ALTER TABLE, przez co instancje ze starszą bazą dostawały `PDOException: Column not found: 1054 Unknown column 'type'` przy zapisie produktu
---
-## ver. 0.315 (2026-02-23) - Fix listowania atrybutów w admin
+## ver. 0.315 (2026-02-23) - Fix listowania atrybutĂłw w admin
-- **FIX**: `AttributeRepository::listForAdmin()` — zapytanie COUNT dostawało parametr `:default_lang_id` którego nie miało w SQL, powodując `PDOException: SQLSTATE[HY093]: Invalid parameter number`. Parametr potrzebny tylko w głównym SELECT, nie w COUNT
+- **FIX**: `AttributeRepository::listForAdmin()` — zapytanie COUNT dostawało parametr `:default_lang_id` którego nie miało w SQL, powodując `PDOException: SQLSTATE[HY093]: Invalid parameter number`. Parametr potrzebny tylko w głównym SELECT, nie w COUNT
---
-## ver. 0.314 (2026-02-23) - Fix wyszukiwarki admin + title zamówienia
+## ver. 0.314 (2026-02-23) - Fix wyszukiwarki admin + title zamĂłwienia
-- **FIX**: Globalna wyszukiwarka w panelu admina przestała zwracać wyniki — dodano `Content-Type: application/json` i `Cache-Control: no-store` (zapobiega cache'owaniu przez proxy/CDN), zmiana AJAX z GET na POST, `fetchAll(PDO::FETCH_ASSOC)`, top-level try/catch z gwarantowaną odpowiedzią JSON
-- **NEW**: `document.title` w widoku szczegółów zamówienia pokazuje numer zamówienia (np. "Zamówienie ZAM/123 - shopPro")
+- **FIX**: Globalna wyszukiwarka w panelu admina przestała zwracać wyniki — dodano `Content-Type: application/json` i `Cache-Control: no-store` (zapobiega cache'owaniu przez proxy/CDN), zmiana AJAX z GET na POST, `fetchAll(PDO::FETCH_ASSOC)`, top-level try/catch z gwarantowaną odpowiedzią JSON
+- **NEW**: `document.title` w widoku szczegółów zamówienia pokazuje numer zamówienia (np. "Zamówienie ZAM/123 - shopPro")
---
-## ver. 0.313 (2026-02-23) - Fix sync płatności Apilo + logowanie
+## ver. 0.313 (2026-02-23) - Fix sync płatności Apilo + logowanie
-- **FIX**: `syncApiloPayment()` i `syncApiloStatus()` — `(int)` cast na `apilo_order_id` (format `"PPxxxxxx"`) dawał `0`, przez co metody pomijały sync z API Apilo. Zmiana na `empty()`
-- **NEW**: Logowanie w `syncApiloPaymentIfNeeded()` i `syncApiloStatusIfNeeded()` — każda ścieżka decyzyjna (Apilo wyłączone, brak tokenu, brak `apilo_order_id`, sync nieudany) zapisuje wpis do `pp_log` z kontekstem
+- **FIX**: `syncApiloPayment()` i `syncApiloStatus()` — `(int)` cast na `apilo_order_id` (format `"PPxxxxxx"`) dawał `0`, przez co metody pomijały sync z API Apilo. Zmiana na `empty()`
+- **NEW**: Logowanie w `syncApiloPaymentIfNeeded()` i `syncApiloStatusIfNeeded()` — każda ścieżka decyzyjna (Apilo wyłączone, brak tokenu, brak `apilo_order_id`, sync nieudany) zapisuje wpis do `pp_log` z kontekstem
---
-## ver. 0.312 (2026-02-23) - Fix krytycznych bugów integracji Apilo
+## ver. 0.312 (2026-02-23) - Fix krytycznych bugĂłw integracji Apilo
-- **FIX**: `curl_getinfo()` wywoływane po `curl_close()` — HTTP code zawsze wynosił 0, uniemożliwiając prawidłową obsługę odpowiedzi Apilo
-- **FIX**: Nieskończona pętla wysyłania zamówienia — gdy Apilo zwracało błąd serwera, zamówienie nie dostawało `apilo_order_id` i było ponownie wybierane w każdym cyklu crona. Teraz błędne zamówienia oznaczane `apilo_order_id = -1` z powiadomieniem email
-- **FIX**: Ceny produktów 0.00 PLN w Apilo — string `"0.00"` z MySQL jest truthy w PHP, więc ternary wybierał `price_brutto_promo` (0.00) zamiast `price_brutto`. Zmiana na `(float)... > 0`
-- **FIX**: Walidacja cen przed wysyłką — zamówienia z zerowymi cenami produktów nie są wysyłane do Apilo (`apilo_order_id = -2`) z powiadomieniem email
-- **FIX**: Niezainicjalizowana zmienna `$order_message` powodująca PHP warning
+- **FIX**: `curl_getinfo()` wywoływane po `curl_close()` — HTTP code zawsze wynosił 0, uniemożliwiając prawidłową obsługę odpowiedzi Apilo
+- **FIX**: Nieskończona pętla wysyłania zamówienia — gdy Apilo zwracało błąd serwera, zamówienie nie dostawało `apilo_order_id` i było ponownie wybierane w każdym cyklu crona. Teraz błędne zamówienia oznaczane `apilo_order_id = -1` z powiadomieniem email
+- **FIX**: Ceny produktów 0.00 PLN w Apilo — string `"0.00"` z MySQL jest truthy w PHP, więc ternary wybierał `price_brutto_promo` (0.00) zamiast `price_brutto`. Zmiana na `(float)... > 0`
+- **FIX**: Walidacja cen przed wysyłką — zamówienia z zerowymi cenami produktów nie są wysyłane do Apilo (`apilo_order_id = -2`) z powiadomieniem email
+- **FIX**: Niezainicjalizowana zmienna `$order_message` powodujÄ…ca PHP warning
---
-## ver. 0.311 (2026-02-23) - Fix race condition Apilo + persistence filtrów + poprawki cen
+## ver. 0.311 (2026-02-23) - Fix race condition Apilo + persistence filtrĂłw + poprawki cen
-- **FIX**: Race condition — callback płatności przed wysłaniem zamówienia do Apilo nie synchronizował płatności (task trafiał w pustkę). Teraz `syncApiloPaymentIfNeeded` i `syncApiloStatusIfNeeded` kolejkują sync do retry gdy `apilo_order_id` jeszcze nie istnieje
-- **FIX**: `processApiloSyncQueue` — zamówienia bez `apilo_order_id` były usuwane z kolejki bez synchronizacji. Teraz czekają (max 50 prób ~8h) aż cron wyśle zamówienie do Apilo
-- **FIX**: Drugie wywołanie `processApiloSyncQueue` w cronie po wysyłce zamówień — sync płatności/statusów w tym samym cyklu
-- **FIX**: Ceny w szczegółach zamówienia (admin + frontend) — gdy `price_brutto_promo` = 0 lub >= ceny regularnej, wyświetla cenę regularną zamiast 0 zł
-- **NEW**: Persistence filtrów tabel w panelu admin — localStorage zapamiętuje ostatni widok (filtry, sortowanie, paginacja) i przywraca go przy powrocie do listy. Przycisk "Wyczyść" resetuje zapisany stan
+- **FIX**: Race condition — callback płatności przed wysłaniem zamówienia do Apilo nie synchronizował płatności (task trafiał w pustkę). Teraz `syncApiloPaymentIfNeeded` i `syncApiloStatusIfNeeded` kolejkują sync do retry gdy `apilo_order_id` jeszcze nie istnieje
+- **FIX**: `processApiloSyncQueue` — zamówienia bez `apilo_order_id` były usuwane z kolejki bez synchronizacji. Teraz czekają (max 50 prób ~8h) aż cron wyśle zamówienie do Apilo
+- **FIX**: Drugie wywołanie `processApiloSyncQueue` w cronie po wysyłce zamówień — sync płatności/statusów w tym samym cyklu
+- **FIX**: Ceny w szczegółach zamówienia (admin + frontend) — gdy `price_brutto_promo` = 0 lub >= ceny regularnej, wyświetla cenę regularną zamiast 0 zł
+- **NEW**: Persistence filtrów tabel w panelu admin — localStorage zapamiętuje ostatni widok (filtry, sortowanie, paginacja) i przywraca go przy powrocie do listy. Przycisk "Wyczyść" resetuje zapisany stan
---
## ver. 0.310 (2026-02-23) - Logi integracji w panelu admin
-- **NEW**: Zakładka "Logi" w sekcji Integracje — podgląd tabeli `pp_log` z paginacją, sortowaniem, filtrami (akcja, wiadomość, ID zamówienia) i rozwijalnym kontekstem JSON
-- **NEW**: `IntegrationsRepository::getLogs()`, `deleteLog()`, `clearLogs()` — metody do obsługi logów
-- **NEW**: `IntegrationsController::logs()`, `logs_clear()` — akcje kontrolera
-- **NEW**: Przycisk "Wyczyść wszystkie logi" z potwierdzeniem
+- **NEW**: Zakładka "Logi" w sekcji Integracje — podgląd tabeli `pp_log` z paginacją, sortowaniem, filtrami (akcja, wiadomość, ID zamówienia) i rozwijalnym kontekstem JSON
+- **NEW**: `IntegrationsRepository::getLogs()`, `deleteLog()`, `clearLogs()` — metody do obsługi logów
+- **NEW**: `IntegrationsController::logs()`, `logs_clear()` — akcje kontrolera
+- **NEW**: Przycisk "Wyczyść wszystkie logi" z potwierdzeniem
---
## ver. 0.309 (2026-02-23) - ApiloLogger + cache-busting CSS/JS + poprawki UI
-- **NEW**: `ApiloLogger` — logowanie operacji Apilo do tabeli `pp_log` z kontekstem JSON (send_order, resend_order, payment_sync, status_sync, status_poll)
-- **NEW**: Migracja `pp_log` — kolumny `action`, `order_id`, `context` + indeksy
-- **NEW**: Cache-busting dla CSS i JS w admin panelu — `?ver=filemtime()` przy wszystkich lokalnych zasobach w `main-layout.php`
+- **NEW**: `ApiloLogger` — logowanie operacji Apilo do tabeli `pp_log` z kontekstem JSON (send_order, resend_order, payment_sync, status_sync, status_poll)
+- **NEW**: Migracja `pp_log` — kolumny `action`, `order_id`, `context` + indeksy
+- **NEW**: Cache-busting dla CSS i JS w admin panelu — `?ver=filemtime()` przy wszystkich lokalnych zasobach w `main-layout.php`
- **FIX**: Przeniesienie inicjalizacji `$mdb` przed `SettingsRepository` w `admin/index.php`
-- **FIX**: Rzutowanie na `(string)` w `ShopProductController::escapeHtml()` — zapobiega warningom
-- **ZMIANA**: Skrocone kategorie produktow na liscie — `text-overflow: ellipsis` z `title` tooltip
-- **ZMIANA**: `copyToClipboard()` — uzywa `navigator.clipboard` API z fallbackiem na `