This commit is contained in:
2026-05-25 22:12:05 +02:00
parent 4364ccf336
commit a3f2e73425
39 changed files with 1084 additions and 2497 deletions

3
.codebase-memory/.gitattributes vendored Normal file
View File

@@ -0,0 +1,3 @@
# Auto-generated by codebase-memory-mcp
# Prevent merge conflicts on compressed artifact
graph.db.zst merge=ours binary

View File

@@ -0,0 +1,11 @@
{
"schema_version": 1,
"commit": "",
"indexed_at": "2026-05-25T19:19:29Z",
"project": "c-visual-studio-code-wyszynskiego12.pl",
"nodes": 88268,
"edges": 267742,
"original_size": 136904704,
"compressed_size": 16761612,
"compression_level": 9
}

Binary file not shown.

View File

@@ -1,123 +0,0 @@
# PAUL Handoff
**Date:** 2026-03-12 (aktualizacja: sesja 2)
**Status:** paused — plan 02-01 gotowy, czeka na APPLY w nowej sesji
---
## READ THIS FIRST
You have no prior context. This document tells you everything.
**Project:** wyszynskiego12.pagedev.pl — strona internetowa dewelopera
**Core value:** Użytkownicy mogą przeglądać na stronie ofertę dewelopera
---
## Current State
**Version:** v0.0.0 (Prototype)
**Phase:** 2 of 2 — Jawność cen
**Plan:** 02-01 — PLAN created, ready for APPLY
**Loop Position:**
```
PLAN ──▶ APPLY ──▶ UNIFY
✓ ○ ○ [Plan 02-01 gotowy, oczekuje na zatwierdzenie]
```
---
## What Was Done
**Sesja 1 (wcześniej):**
- Tabela `wp_price_history` założona przez dbDelta()
- WP Cron `apartamenty_record_prices` dzienny — snapshot cen apartamentów
- AJAX endpoint `apartamenty_get_price_history` (public, nonce-secured)
- `wp_localize_script` przekazujący ajaxUrl + nonce do JS
**Sesja 2 (ta sesja):**
- Zbadano wymagania ustawy o jawności cen (dane.gov.pl)
- Odczytano XSD portalu: `https://www.dane.gov.pl/static/xml/otwarte_dane_latest.xsd`
- Ustalono format XML cen (voxDeveloper-compatible)
- Dodano Phase 2 do ROADMAP.md
- Utworzono plan `02-01-PLAN.md` — 3 zadania auto + 1 checkpoint
**WAŻNE: Plan 01-02 (frontend popup historia cen) nadal czeka na APPLY.**
Zdecydowano skupić się najpierw na 02-01 (jawność cen). Można wrócić do 01-02 później.
---
## What's In Progress
Plan `02-01-PLAN.md` gotowy, NIE był jeszcze uruchomiony APPLY.
---
## What's Next
**Immediate:** `/paul:apply .paul/phases/02-jawnosc-cen/02-01-PLAN.md`
Plan 02-01 zawiera 3 zadania auto + 1 checkpoint:
1. Endpoint `/ceny-mieszkan.xml` + `/ceny-mieszkan.md5` (price data XML z ACF + wp_price_history)
2. Endpoint `/dane-gov-pl.xml` + `/dane-gov-pl.md5` (katalog XSD-compliant dla dane.gov.pl)
3. Strona admin: `Narzędzia → Jawność Cen` z URL-ami do zgłoszenia do Ministerstwa
4. Checkpoint: weryfikacja URL-ów w przeglądarce
**After that:** UNIFY plan 02-01, then consider 01-02 (frontend popup)
---
## Key Context — Technikalia
**Jedyny plik do zmiany:** `wp-content/plugins/elementor-addon/elementor-addon.php`
**Stack:** WordPress + ACF + Elementor + custom plugin `elementor-addon`
**Dostępne ACF flat meta keys (z `information` group):**
- `information_price` — cena brutto
- `information_price_m2` — cena za m²
- `information_floor_space` — metraż
- `information_type` — typ lokalu
- `information_floor` — piętro
- `information_status` — status (value/label)
**Tabela wp_price_history:** `id, post_id, price, price_m2, floor_space, recorded_at`
**Post type:** `apartamenty`
**Istniejące hooki w elementor-addon.php:**
- Cron: `apartamenty_record_prices` (daily)
- AJAX: `wp_ajax_apartamenty_get_price_history` + nopriv
- Nonce: `apartamenty_price_history_nonce`
**Wymagania techniczne dane.gov.pl:**
- Content-Type: `application/xml` lub `text/xml`
- MD5 companion file — lowercase hex, ten sam URL ale z `.md5` zamiast `.xml`
- Port standardowy (80/443)
- Aktualizacja min. co 24h
---
## Key Files
| File | Purpose |
|------|---------|
| `.paul/STATE.md` | Live project state |
| `.paul/ROADMAP.md` | Phase overview (fazy 1 i 2) |
| `.paul/phases/02-jawnosc-cen/02-01-PLAN.md` | Plan do APPLY |
| `.paul/phases/01-historia-cen/01-01-SUMMARY.md` | Co zbudowano w backendzie |
| `wp-content/plugins/elementor-addon/elementor-addon.php` | Jedyny plik do modyfikacji |
---
## Resume Instructions
1. Przeczytaj `.paul/STATE.md` dla aktualnej pozycji
2. Uruchom `/paul:apply .paul/phases/02-jawnosc-cen/02-01-PLAN.md`
3. Po APPLY wykonaj checkpoint (flush permalinks, sprawdź URL-e w przeglądarce)
4. Następnie `/paul:unify`
---
*Handoff created: 2026-03-12 (updated: sesja 2)*

View File

@@ -1,45 +0,0 @@
# Milestones
Completed milestone log for this project.
| Milestone | Completed | Duration | Stats |
|-----------|-----------|----------|-------|
| v0.1 Initial Release | 2026-03-12 | 1 dzień | 2 fazy, 3 plany, 6 plików |
---
## ✅ v0.1 Initial Release
**Completed:** 2026-03-12
**Duration:** 1 dzień
### Stats
| Metric | Value |
|--------|-------|
| Phases | 2 |
| Plans | 3 |
| Files changed | 6 |
### Key Accomplishments
- Tabela `wp_price_history` z WP Cronem dziennym — automatyczny snapshot cen wszystkich apartamentów
- AJAX endpoint `apartamenty_get_price_history` zabezpieczony nonce — historia cen jako JSON
- Popup „Historia cen" w widgecie — vanilla JS, modal zgodny z projektem (Barlow, #192c44)
- Cztery publiczne endpointy XML: `/ceny-mieszkan.xml`, `/ceny-mieszkan.md5`, `/dane-gov-pl.xml`, `/dane-gov-pl.md5`
- Katalog XSD-compliant dla portalu dane.gov.pl z automatycznym URL przez `home_url()`
- Strona admina wp-admin → Narzędzia → Jawność Cen z URL-ami do skopiowania
- Transient cache XML (1h) z inwalidacją przy każdym cronie
- Dokumentacja klienta w `docs/jawnosc-cen.md` — instrukcja zgłoszenia do Ministerstwa
### Key Decisions
| Decyzja | Uzasadnienie |
|---------|--------------|
| Flat ACF meta keys zamiast `get_field('information')` | ACF zapisuje dane jako płaskie klucze — bezpośredni `get_post_meta` |
| INSERT IGNORE w cronie | Historia to snapshot — jeden rekord na apt na dzień, idempotentny |
| XML jako czysty PHP string (ENT_XML1) | Brak gwarancji ext-dom na hostingu |
| Transient 1h + inwalidacja przez cron | Balans między wydajnością a świeżością danych |
| Wszystko w elementor-addon.php | Scope limit z planu — brak osobnych plików |
---

View File

@@ -1,82 +1,24 @@
# Project: wyszynskiego12.pagedev.pl # Projekt: wyszynskiego12.pl
## What This Is ## Opis
Strona internetowa dla dewelopera pozwalająca na okazanie oferty klientom. `wyszynskiego12.pl` to projekt WordPress dla inwestycji Wyszynskiego 12. Repozytorium zawiera pelna instalacje WordPress, motywy, wtyczki oraz lokalna dokumentacje funkcji zwiazanych z prezentacja lokali, historia cen i obowiazkiem jawnosci cen nieruchomosci.
## Core Value ## Kluczowa wartosc
Użytkownicy mogą przeglądać na stronie ofertę dewelopera. Projekt ma utrzymywac wiarygodna, publiczna i administracyjnie wygodna prezentacje danych o lokalach, w tym aktualne ceny, historie zmian cen oraz endpointy wymagane do zgloszenia danych do `dane.gov.pl`.
## Current State ## Aktualny status
| Attribute | Value | - PAUL zostal zainicjalizowany w trybie plan-first.
|-----------|-------| - Aktywna przestrzen pracy PAUL: `.paul/plans/`.
| Version | 0.2.0 | - Quality Radar jest wlaczony w lekkim trybie bootstrap.
| Status | Released | - Istniejaca dokumentacja funkcjonalna znajduje sie w `docs/`.
| Last Updated | 2026-03-12 |
## Requirements ## Trwale wymagania i ograniczenia
### Validated (Shipped) - Tresci i dokumenty PAUL piszemy po polsku.
- Nie tworzymy roadmapy ani milestone'ow jako wymaganej struktury; sa opcjonalnym kontekstem legacy.
- [x] Użytkownik widzi historię zmian cen dla każdego apartamentu — v0.1 Initial Release - Zmiany w kodzie WordPress powinny uwzgledniac wtyczke `wp-content/plugins/elementor-addon/elementor-addon.php`, bo dokumentacja wskazuje ja jako glowne miejsce logiki cen i endpointow.
- [x] Popup „Historia cen" otwiera się po kliknięciu z aktualną ceną i tabelą zmian — v0.1 Initial Release - Endpointy jawnosci cen powinny pozostac publicznie dostepne bez logowania.
- [x] System automatycznie zapisuje ceny codziennie (WP Cron) — v0.1 Initial Release - Dane cenowe sa oparte o pola ACF oraz tabele `wp_price_history`.
- [x] Deweloper może raportować ceny do portalu dane.gov.pl przez publiczny endpoint XML — v0.1 Initial Release
- [x] Strona admina z URL-ami do zgłoszenia do Ministerstwa — v0.1 Initial Release
### Active (In Progress)
- (brak — gotowy na nowy milestone)
### Validated (Shipped) — v0.2
- [x] Widget Elementor wyświetla ceny miejsc postojowych (zwykłe i rodzinne) — v0.2
- [x] Popup historia cen dla miejsc postojowych (reuse apartamentów) — v0.2
- [x] Cron zapisuje ceny parkingowe codziennie do wp_parking_price_history — v0.2
- [x] XML /ceny-mieszkan.xml zawiera sekcję <miejsca_postojowe> — v0.2
### Planned (Next)
- (do zdefiniowania podczas następnego planowania)
### Out of Scope
- Rejestracja dewelopera na dane.gov.pl (czynność ręczna po stronie klienta)
- Walidator XSD po stronie PHP
- Obsługa wielu inwestycji (tylko Wyszyńskiego 12)
- Formularz do edycji danych inwestycji w adminie
## Target Users
**Primary:** Potencjalni klienci dewelopera
- Poszukują mieszkania lub lokalu
- Chcą szybko zapoznać się z ofertą
- Oczekują przejrzystej prezentacji inwestycji
**Secondary:** Deweloper / admin
- Zarządza ofertą przez WordPress admin
- Musi raportować ceny do portalu rządowego
## Constraints
### Technical Constraints
- WordPress + Elementor (plugin elementor-addon)
- ACF dla pól apartamentów (flat meta keys: `information_price`, `information_price_m2`, etc.)
- Hosting: brak gwarancji ext-dom PHP — XML generowany jako czysty string
- WP Cron (pseudocron) — wymaga ruchu na stronie lub systemowego crona
### Business Constraints
- Ustawa o jawności cen nieruchomości — obowiązek raportowania do dane.gov.pl
- Format XML zgodny z XSD portalu otwarte_dane_latest.xsd
## Success Criteria
- [x] Użytkownicy mogą przeglądać na stronie ofertę dewelopera
- [x] Użytkownicy mogą sprawdzić historię zmian cen apartamentów
- [x] Deweloper spełnia wymóg ustawy o jawności cen (endpoint XML gotowy do zgłoszenia)
---
*Created: 2026-03-12*
*Last updated: 2026-03-25 after v0.2 Miejsca Postojowe*

View File

@@ -1,45 +0,0 @@
# Roadmap: wyszynskiego12.pagedev.pl
## Overview
Strona internetowa dla dewelopera pozwalająca na okazanie oferty klientom. Projekt obejmuje budowę witryny prezentującej inwestycje, lokale i informacje kontaktowe.
## Current Milestone
Brak aktywnego milestone.
Uruchom `/paul:discuss-milestone` lub `/paul:milestone` aby zdefiniować następny.
## Completed Milestones
<details>
<summary>v0.2 Miejsca Postojowe - 2026-03-25 (1 faza)</summary>
| Phase | Name | Plans | Completed |
|-------|------|-------|-----------|
| 3 | Miejsca postojowe | 1/1 | 2026-03-25 |
**Kluczowe deliverables:**
- Widget Elementor "Miejsca Postojowe" (zwykłe + rodzinne)
- Historia cen z popup (reuse apartamentów)
- Cron dzienny zapis cen parkingowych (wp_parking_price_history)
- XML export rozszerzony o sekcję <miejsca_postojowe>
</details>
<details>
<summary>v0.1 Initial Release - 2026-03-12 (2 fazy)</summary>
| Phase | Name | Plans | Completed |
|-------|------|-------|-----------|
| 1 | Historia cen | 2/2 | 2026-03-12 |
| 2 | Jawnosc cen | 1/1 | 2026-03-12 |
**Kluczowe deliverables:**
- Popup historia cen (widget + AJAX + CSS + JS)
- XML endpointy jawnosci cen (dane.gov.pl)
- Strona admina z URL-ami do Ministerstwa
</details>
---
*Roadmap updated: 2026-03-25 after v0.2 Miejsca Postojowe*

View File

@@ -1,62 +1,77 @@
# Project State # Stan PAUL
## Project Reference ## Project Reference
See: .paul/PROJECT.md (updated 2026-03-12) - Projekt: `wyszynskiego12.pl`
- Tryb pracy: plan-first
**Core value:** Uzytkownik moze przegladac oferte dewelopera i sprawdzac historie cen - Główne katalogi PAUL: `.paul/plans/`, `.paul/codebase/radar/`, `.paul/handoffs/`
**Current focus:** Milestone v0.2 Miejsca Postojowe — ukonczone - Konfiguracja: `.paul/config.md`
## Current Position ## Current Position
Milestone: v0.2 Miejsca Postojowe — Complete - Milestone: brak legacy milestone
Phase: [3] of [3] (Miejsca postojowe) — Complete - Phase: plan-first
Plan: 03-01 complete - Plan: `20260525-2149-historia-cen-zmiany` complete
Status: Milestone v0.2 complete ready for next - Status: loop complete, ready for next PLAN
Last activity: 2026-03-25 — UNIFY 03-01 complete - Last activity: 2026-05-25 22:05:56 +02:00 - Created `.paul/plans/20260525-2149-historia-cen-zmiany/SUMMARY.md`
Progress: Progress:
- v0.1 Initial Release: [██████████] 100% ✓ - Plan: complete
- v0.2 Miejsca Postojowe: [██████████] 100% ✓
- Phase 3: [██████████] 100% ✓
## Loop Position ## Loop Position
Current loop state: Current loop state:
```text
PLAN ---> APPLY ---> UNIFY
✓ ✓ ✓ [Loop complete - ready for next PLAN]
``` ```
PLAN ──▶ APPLY ──▶ UNIFY
✓ ✓ ✓ [Loop complete — milestone done]
```
## Accumulated Context
### Decisions
| Decyzja | Faza | Wplyw |
|---------|------|-------|
| Flat ACF meta keys (information_price etc.) | Phase 1 | Cron uzywa get_post_meta bezposrednio |
| INSERT IGNORE w cronie | Phase 1 | Jeden rekord na apt na dzien, idempotentny |
| XML jako czysty PHP string ENT_XML1 | Phase 2 | Brak zaleznosci od ext-dom |
| Transient 1h + inwalidacja przez cron | Phase 2 | Cache XML odswieza sie po kazdym cronie |
| ACF group access: get_field('grupa','option')['pole'] | Phase 3 | Parking ceny w grupach ACF |
| Osobna tabela wp_parking_price_history | Phase 3 | Separacja od apartamentow (parking_type vs post_id) |
| Cena m2 ukryta w widgecie parkingowym | Phase 3 | Dane nadal w cronie/XML, UI uproszczone |
### Deferred Issues
- Klient musi recznie zglosic URL /dane-gov-pl.xml do kontakt@dane.gov.pl
- WP Cron pseudocron — na produkcji zalecany systemowy cron (docs/readme.md)
### Blockers/Concerns
Brak.
## Session Continuity ## Session Continuity
Last session: 2026-03-25 - Last session: 2026-05-25 22:05:56 +02:00
Stopped at: Milestone v0.2 complete - Stopped at: UNIFY complete for `20260525-2149-historia-cen-zmiany`
Next action: /paul:discuss-milestone - Next action: Manualnie sprawdzić popup w WordPress albo rozpocząć kolejny plan przez `$paul-plan [opis pracy]`.
Resume file: .paul/phases/03-miejsca-postojowe/03-01-SUMMARY.md - Resume file: `.paul/plans/20260525-2149-historia-cen-zmiany/SUMMARY.md`
Resume context: - Mapa kodu: `.paul/codebase/`
- v0.2 kompletny: widget Miejsca Postojowe + historia cen + cron + XML - Raporty radaru: `.paul/codebase/impact_map.md`, `.paul/codebase/quality_risks.md`, `.paul/codebase/tooling_status.md`
- Gotowy na nowy milestone
--- ## Completed Plan Summary
*STATE.md — Aktualizowany po kazdej istotnej akcji*
- Plan: `.paul/plans/20260525-2149-historia-cen-zmiany/PLAN.md`
- Summary: `.paul/plans/20260525-2149-historia-cen-zmiany/SUMMARY.md`
- Zmieniono `wp-content/plugins/elementor-addon/elementor-addon.php`.
- Dodano `elementor_addon_filter_price_change_history(array $history)`.
- Endpoint `apartamenty_get_price_history` filtruje dzienne snapshoty do pierwszego dnia każdego ciągłego zakresu tej samej ceny.
- Endpoint `parking_get_price_history` używa tego samego filtra dla miejsc postojowych.
- Jeśli cała historia ma jedną cenę, filtr zwraca ostatnią i pierwszą datę zakresu; przy pojedynczym rekordzie nie dubluje wiersza.
- Kontrakt JSON dla `main.js` został zachowany.
## Verification
- PASS: `php -l wp-content/plugins/elementor-addon/elementor-addon.php`.
- PASS: izolowany test algorytmu dla wielu zmian zwrócił `2026-05-20,2026-05-16,2026-05-01`.
- PASS: izolowany test przypadku jednej ceny w całej historii zwrócił `2026-05-20,2026-05-01`.
- PENDING manual: klik `HISTORIA CEN` przy apartamencie z powtarzającymi się cenami.
- PENDING manual: klik `HISTORIA CEN` przy miejscach postojowych `zwykle` i `rodzinne`.
## Codebase Mapped
- Date: 2026-05-25
- Documents: `.paul/codebase/`
- Quality Radar: degraded; `codebase-memory-mcp` indeksowanie zwróciło status `indexed`, ale projekt nie jest widoczny w MCP `list_projects`, więc plan/apply/unify użyły fallbacku przez `rg`, diff i odczyt kluczowych plików.
- Główne obszary: `wp-content/plugins/elementor-addon/`, `wp-content/themes/hello-elementor/`, `docs/`.
## Git State
W chwili inicjalizacji `git status` pokazywał wcześniejsze usunięcia starych plików `.paul/*` oraz `.serena/*`, a także nowy katalog `.codebase-memory/` utworzony przez próbę indeksowania. Te zmiany nie zostały cofnięte ani porządkowane przez PAUL init/map-codebase/apply/unify.
## Pending Actions
- W środowisku WordPress sprawdzić popup apartamentu i popup parkingu na realnych danych.
- Jeśli klient później zażąda identycznego filtrowania w XML `/ceny-mieszkan.xml`, utworzyć osobny plan.
- Kolejna praca funkcjonalna powinna zacząć się od `$paul-plan [opis pracy]`.
## State Maintenance
- Autocompression: włączona w `.paul/config.md`; aktualny plik jest poniżej limitu 500 linii, więc kompresja nie była potrzebna.

View File

@@ -0,0 +1,18 @@
# 2026-05-25
## Co zrobiono
- [Plan 20260525-2149-historia-cen-zmiany] Popup historii cen apartamentów i miejsc postojowych pokazuje tylko daty zmian ceny.
- Dodano wspólny filtr `elementor_addon_filter_price_change_history(array $history)`.
- Endpointy AJAX `apartamenty_get_price_history` i `parking_get_price_history` zwracają przefiltrowaną historię bez codziennych powtórzeń tej samej ceny.
- Dla historii z jedną niezmienną ceną popup dostaje pierwszą i ostatnią datę zakresu.
- Zachowano kontrakt JSON używany przez `wp-content/plugins/elementor-addon/assets/js/main.js`.
## Zmienione pliki
- `wp-content/plugins/elementor-addon/elementor-addon.php`
- `.paul/plans/20260525-2149-historia-cen-zmiany/PLAN.md`
- `.paul/plans/20260525-2149-historia-cen-zmiany/SUMMARY.md`
- `.paul/STATE.md`
- `.paul/codebase/radar/codebase-memory-post-apply.txt`
- `.paul/codebase/tooling_status.md`

View File

@@ -0,0 +1,68 @@
# Architecture
## Obraz całości
Projekt jest stroną WordPress, gdzie większość mechaniki runtime dostarcza WordPress, Elementor i ACF. Warstwa własna jest niewielka, ale biznesowo ważna: wtyczka `elementor-addon` renderuje dane lokali, zapisuje historię cen, wystawia AJAX i publiczne XML/MD5, a motyw `hello-elementor` dodaje mapę garażu oraz custom CSS.
## Główne wejścia systemu
- Publiczna strona WordPress: `index.php` i standardowy bootstrap WordPress.
- Widgety Elementor:
- `wp-content/plugins/elementor-addon/widgets/apartaments.php`
- `wp-content/plugins/elementor-addon/widgets/parking-spots.php`
- AJAX WordPress:
- `wp_ajax_apartamenty_get_price_history`
- `wp_ajax_nopriv_apartamenty_get_price_history`
- `wp_ajax_parking_get_price_history`
- `wp_ajax_nopriv_parking_get_price_history`
- Publiczne endpointy rewrite:
- `/ceny-mieszkan.xml`
- `/ceny-mieszkan.md5`
- `/dane-gov-pl.xml`
- `/dane-gov-pl.md5`
- WP Cron: hook `apartamenty_record_prices`.
- Admin UI: `wp-admin -> Narzędzia -> Jawność Cen`.
- Shortcode motywu: `[garage_map]`.
## Warstwa wtyczki `elementor-addon`
- `register_hello_world_widget()` ładuje widgety z `widgets/apartaments.php` i `widgets/parking-spots.php`.
- `elementor_addon_register_assets()` rejestruje CSS/JS, Swiper i Fancybox.
- `elementor_addon_localize_scripts()` przekazuje do JS `admin-ajax.php` oraz nonce `apartamenty_price_history_nonce`.
- `elementor_addon_enable_apartamenty_menu_order()` dodaje `page-attributes` do CPT `apartamenty`, żeby sortowanie mogło bazować na `menu_order`.
- `elementor_addon_create_tables()` tworzy tabele historii przy aktywacji pluginu.
- `elementor_addon_maybe_update_db()` pilnuje wersji schematu przez opcję `elementor_addon_db_version`.
- `elementor_addon_record_prices()` zapisuje dzienne snapshoty cen lokali i miejsc postojowych.
- `apartamenty_generate_price_xml()` generuje XML z lokalami, miejscami postojowymi i historią cen.
- `apartamenty_generate_datagov_xml()` generuje katalog zgodny z procesem `dane.gov.pl`.
- `apartamenty_xml_template_redirect()` zwraca XML albo MD5.
## Przepływ danych: historia cen
1. Administrator aktualizuje pola ACF lokalu lub ceny miejsc postojowych w ACF options.
2. WP Cron `apartamenty_record_prices` uruchamia `elementor_addon_record_prices()`.
3. Funkcja pobiera posty typu `apartamenty` i pola `information_price`, `information_price_m2`, `information_floor_space`.
4. Dla miejsc postojowych pobierane są grupy ACF options `miejsce_postojowe_zwykle` i `miejsce_postojowe_rodzinne`.
5. Dane trafiają do tabel `wp_price_history` i `wp_parking_price_history` przez `INSERT IGNORE`.
6. Cache XML `apartamenty_price_xml_cache` jest czyszczony.
7. Widgety frontendowe pobierają historię przez AJAX, a endpoint XML pobiera ją bezpośrednio z tabel.
## Przepływ danych: XML jawności cen
1. WordPress rewrite mapuje `/ceny-mieszkan.xml` na query var `apartamenty_xml=xml`.
2. `template_redirect` wywołuje `apartamenty_generate_price_xml()`.
3. Funkcja pobiera opublikowane `apartamenty`, pola `information_*`, historię z `wp_price_history` oraz dane parkingowe.
4. Wynik jest cache'owany w transiencie `apartamenty_price_xml_cache` na `HOUR_IN_SECONDS`.
5. Dla `.md5` zwracany jest `md5($content)` zamiast XML.
## Warstwa motywu
- `wp-content/themes/hello-elementor/functions.php` ładuje `assets/css/custom.css` z wersją `filemtime`.
- Ten sam plik rejestruje shortcode `garage_map`, który renderuje `template-parts/garage-map.php`.
- `garage-map.php` pobiera ACF option `garage_spots` i mapuje statusy na klasy CSS `is-free`, `is-reserved`, `is-sold`, `is-unavailable`.
## Granice modułów
- WordPress core i większość pluginów są vendorem; zmiany własne powinny być ograniczane do `wp-content/plugins/elementor-addon/`, `wp-content/themes/hello-elementor/` i dokumentacji.
- ACF jest źródłem danych edytowalnych przez admina; kod zakłada konkretne nazwy pól.
- Baza danych WordPress przechowuje standardowe posty/meta oraz niestandardowe tabele historii.

View File

@@ -0,0 +1,47 @@
# Conventions
## Ogólne wzorce WordPress
- Kod własny używa proceduralnych hooków WordPress w `wp-content/plugins/elementor-addon/elementor-addon.php`.
- Funkcje wtyczki mają prefiks `elementor_addon_` dla logiki ogólnej oraz `apartamenty_` dla endpointów jawności cen.
- Widgety Elementor są klasami dziedziczącymi po `\Elementor\Widget_Base`.
- Assety są rejestrowane przez `wp_register_style()` i `wp_register_script()`, a widgety deklarują zależności przez `get_style_depends()` i `get_script_depends()`.
- Dane wejściowe z requestów są walidowane przez `absint()`, `sanitize_text_field()` i `check_ajax_referer()`.
- Dane wyjściowe w HTML są zwykle zabezpieczane przez `esc_html()`, `esc_url()`, `esc_attr()` i `esc_js()`.
## Nazewnictwo domenowe
- Typ lokalu: `apartamenty`.
- Historia lokali: `price_history`.
- Historia miejsc postojowych: `parking_price_history`.
- Pola lokali:
- `information_price`
- `information_price_m2`
- `information_floor_space`
- `information_type`
- `information_floor`
- `information_status`
- Grupy miejsc postojowych:
- `miejsce_postojowe_zwykle`
- `miejsce_postojowe_rodzinne`
- Mapa garażu: `garage_spots`, klucze `parking-[numer]`.
## Frontend i CSS
- Wtyczka trzyma style widgetów w `wp-content/plugins/elementor-addon/assets/css/main.scss` i wygenerowanym `main.css`.
- Motyw trzyma custom styles w `wp-content/themes/hello-elementor/assets/css/custom.scss` i `custom.css`.
- Klasy CSS są opisowe i w większości BEM-podobne, np. `apartament-card__info_table-title`, `price-history-modal__row`, `parking-spot-card__price`.
- JS jest prosty i DOM-owy: `DOMContentLoaded`, `querySelectorAll`, `fetch`, bez bundlera w kodzie własnym.
## Dokumentacja
- Dokumenty PAUL piszemy po polsku.
- Dokumentacja domenowa projektu znajduje się w `docs/readme.md` i `docs/jawnosc-cen.md`.
- Trwałe dokumenty mapy kodu znajdują się w `.paul/codebase/`.
## Ostrożność przy zmianach
- Nie edytować vendorów WordPress/Elementor/ACF, jeśli da się zmienić zachowanie przez wtyczkę lub motyw.
- Po zmianach rewrite rules trzeba wykonać flush permalinków.
- Po zmianach pól ACF lub struktury XML trzeba sprawdzić widget, AJAX, XML i MD5, bo korzystają z tych samych danych.
- Nie nadpisywać `.paul/codebase/todo.md`, jeśli powstanie jako manualny plik.

View File

@@ -0,0 +1,78 @@
# Database Schema
## Standardowe tabele WordPress
- Posty lokali są przechowywane jako CPT `apartamenty` w standardowych tabelach WordPress, głównie `wp_posts` i `wp_postmeta`.
- Kolejność lokali bazuje na `menu_order`, bo `elementor_addon_enable_apartamenty_menu_order()` dodaje obsługę `page-attributes` dla CPT `apartamenty`.
## `wp_price_history`
Tabela tworzona przez `elementor_addon_create_price_history_table()` w `wp-content/plugins/elementor-addon/elementor-addon.php`.
```sql
CREATE TABLE wp_price_history (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
post_id BIGINT UNSIGNED NOT NULL,
price VARCHAR(50) NOT NULL DEFAULT '',
price_m2 VARCHAR(50) NOT NULL DEFAULT '',
floor_space VARCHAR(50) NOT NULL DEFAULT '',
recorded_at DATE NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY unique_daily (post_id, recorded_at),
KEY idx_post_id (post_id)
);
```
Uwaga: realna nazwa tabeli używa `$wpdb->prefix`, więc prefix może być inny niż `wp_`.
## `wp_parking_price_history`
Tabela tworzona przez `elementor_addon_create_parking_price_history_table()` w `wp-content/plugins/elementor-addon/elementor-addon.php`.
```sql
CREATE TABLE wp_parking_price_history (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
parking_type VARCHAR(50) NOT NULL,
price VARCHAR(50) NOT NULL DEFAULT '',
price_m2 VARCHAR(50) NOT NULL DEFAULT '',
recorded_at DATE NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY unique_daily (parking_type, recorded_at),
KEY idx_type (parking_type)
);
```
## Wersjonowanie schematu
- Opcja WordPress: `elementor_addon_db_version`.
- Aktualna oczekiwana wartość w kodzie: `1.1`.
- `register_activation_hook()` tworzy tabele przy aktywacji wtyczki.
- `elementor_addon_maybe_update_db()` sprawdza wersję na `init` i tworzy tabele, jeśli wersja jest inna.
## Źródła danych ACF/post meta
- Lokale:
- `information_price`
- `information_price_m2`
- `information_floor_space`
- `information_type`
- `information_floor`
- `information_status`
- `gallery`
- `documents`
- Miejsca postojowe:
- `miejsce_postojowe_zwykle`
- `miejsce_postojowe_rodzinne`
- `garage_spots`
## Zapytania krytyczne
- Zapis dzienny używa `INSERT IGNORE`, więc unikalny klucz blokuje duplikaty dla tego samego dnia.
- Historia lokali pobierana jest po `post_id` i sortowana `ORDER BY recorded_at DESC`.
- Historia parkingu pobierana jest po `parking_type` i sortowana `ORDER BY recorded_at DESC`.
## Ryzyka schematu
- Brak jawnych migracji poza `dbDelta()` i opcją wersji.
- Pola cen są `VARCHAR(50)`, więc formatowanie ceny jest logiką aplikacyjną, nie typem liczbowym w DB.
- Zmiana prefixu tabel jest obsłużona przez `$wpdb->prefix`, ale dokumentacja i SQL diagnostyczny często używają przykładowego `wp_`.

View File

@@ -0,0 +1,72 @@
# Impact Map
## Tryb skanowania
- Tryb: `full` dla `$paul-map-codebase`.
- Data: 2026-05-25.
- Zakres: całe repozytorium, z naciskiem na własny kod poza vendorem WordPress/Elementor/ACF.
- Quality Radar: degraded dla MCP, fallback przez `rg` i odczyt plików.
## Najważniejsze obszary wpływu
- Historia cen lokali i miejsc postojowych: `wp-content/plugins/elementor-addon/elementor-addon.php`.
- Publiczne XML/MD5 jawności cen: `wp-content/plugins/elementor-addon/elementor-addon.php`.
- Widgety frontendowe: `wp-content/plugins/elementor-addon/widgets/apartaments.php`, `wp-content/plugins/elementor-addon/widgets/parking-spots.php`.
- Frontend modal/historia cen: `wp-content/plugins/elementor-addon/assets/js/main.js`, `wp-content/plugins/elementor-addon/assets/css/main.scss`, `wp-content/plugins/elementor-addon/assets/css/main.css`.
- Mapa garażu: `wp-content/themes/hello-elementor/functions.php`, `wp-content/themes/hello-elementor/template-parts/garage-map.php`, `wp-content/themes/hello-elementor/assets/css/custom.scss`.
- Dokumentacja operacyjna: `docs/readme.md`, `docs/jawnosc-cen.md`.
## Routes / controllers / services / views / scripts
- Rewrite endpoints:
- `/ceny-mieszkan.xml`
- `/ceny-mieszkan.md5`
- `/dane-gov-pl.xml`
- `/dane-gov-pl.md5`
- AJAX:
- `apartamenty_get_price_history`
- `parking_get_price_history`
- Cron:
- `apartamenty_record_prices`
- Admin:
- `wp-admin -> Narzędzia -> Jawność Cen`
- Shortcode:
- `[garage_map]`
## Dane i konfiguracja
- CPT: `apartamenty`.
- Tabele: `wp_price_history`, `wp_parking_price_history`.
- Cache: transient `apartamenty_price_xml_cache`.
- ACF lokali: `information_price`, `information_price_m2`, `information_floor_space`, `information_type`, `information_floor`, `information_status`, `gallery`, `documents`.
- ACF options: `miejsce_postojowe_zwykle`, `miejsce_postojowe_rodzinne`, `garage_spots`.
## Ukryte sprzężenia
- Ten sam zestaw pól cen zasila widget, AJAX, cron, XML i dokumentację dla klienta.
- `apartamenty_generate_price_xml()` miesza aktualny stan ACF/post meta z historią DB i cache.
- Zmiana nazw ACF może nie wywołać błędu PHP, ale da puste ceny/statusy w XML i UI.
- Rewrite rules wymagają flush permalinków po zmianach.
- WP Cron zależy od ruchu na stronie, jeśli produkcja nie używa systemowego crona.
- Publiczny XML może być dodatkowo dotknięty cache wtyczek typu `litespeed-cache`.
## Obszary weryfikacji przy zmianach
- Zmiany w `wp-content/plugins/elementor-addon/elementor-addon.php`: sprawdzić cron, AJAX, XML, MD5, admin page i transients.
- Zmiany w widgetach: sprawdzić strony Elementor, modal historii, dostępność nonce i brak duplikatów overlay.
- Zmiany w ACF: sprawdzić zapis historii, render lokali, miejsca postojowe i mapę garażu.
- Zmiany w motywie: sprawdzić `[garage_map]`, custom CSS oraz wpływ filtra `he_replace_m2_with_superscript()`.
- Zmiany w cache/rewrite: sprawdzić publiczne endpointy i flush permalinków.
## Targeted scan: popup historii cen pokazuje tylko zmiany
- Data: 2026-05-25 21:49 +02:00.
- Plan: `.paul/plans/20260525-2149-historia-cen-zmiany/PLAN.md`.
- Zakres wpływu:
- `wp-content/plugins/elementor-addon/elementor-addon.php`: endpointy AJAX `apartamenty_get_price_history` i `parking_get_price_history`.
- `wp-content/plugins/elementor-addon/assets/js/main.js`: konsument `json.data.history`, bez planowanej zmiany kontraktu.
- `wp-content/plugins/elementor-addon/widgets/apartaments.php` i `wp-content/plugins/elementor-addon/widgets/parking-spots.php`: modale/fallback DOM do ręcznej weryfikacji.
- Dane:
- `wp_price_history`: filtrowanie ciągłych zakresów po `post_id`.
- `wp_parking_price_history`: filtrowanie ciągłych zakresów po `parking_type`.
- Ukryte sprzężenie: zmiana powinna pozostać w AJAX popupu; publiczny XML korzysta z tych samych tabel, ale nie jest częścią bieżącego wymagania.

View File

@@ -0,0 +1,43 @@
# Integrations
## WordPress
- Standardowy runtime WordPress dostarcza routing, hooki, cron, AJAX, post meta, transients i admin UI.
- `wp-content/plugins/elementor-addon/elementor-addon.php` używa `add_action()`, `add_filter()`, `WP_Query`, `$wpdb`, `dbDelta()`, `wp_schedule_event()`, `wp_send_json_success()` i `wp_send_json_error()`.
## Elementor
- Własna wtyczka rejestruje widgety w hooku `elementor/widgets/register`.
- Widgety:
- `Elementor_Apartaments` w `wp-content/plugins/elementor-addon/widgets/apartaments.php`.
- `Elementor_Parking_Spots` w `wp-content/plugins/elementor-addon/widgets/parking-spots.php`.
- Motyw Hello Elementor pozostaje aktywnym fundamentem layoutu i integruje lokalizacje Elementor Theme Builder.
## ACF / ACF Pro
- Lokale używają pól `information_*`, `gallery` i `documents`.
- Miejsca postojowe używają ACF options `miejsce_postojowe_zwykle` i `miejsce_postojowe_rodzinne`.
- Mapa garażu używa ACF option `garage_spots`.
- Kod używa zarówno `get_field()`, jak i `get_post_meta()`; to ważne przy zmianie nazw pól lub sposobu zwracania wartości przez ACF.
## dane.gov.pl
- `apartamenty_generate_datagov_xml()` generuje XML katalogu dla procesu `dane.gov.pl`.
- Dokumentacja wskazuje URL katalogu `https://wyszynskiego12.pagedev.pl/dane-gov-pl.xml`.
- Dane katalogu zawierają hardcodowane identyfikatory i opis inwestycji, np. `wyszynskiego12-ceny-mieszkan-v1`.
## CDN i biblioteki JS/CSS
- Fancybox jest ładowany z `cdn.jsdelivr.net` w `wp-content/plugins/elementor-addon/elementor-addon.php`.
- Swiper jest lokalny w `wp-content/plugins/elementor-addon/plugins/swiper/`.
- AJAX frontendowy idzie do `admin-ajax.php` przez zmienną `apartamentsData.ajaxUrl`.
## Cache i optymalizacja
- XML cen jest cache'owany w transiencie `apartamenty_price_xml_cache`.
- W repo obecne są `litespeed-cache`, `webp-express` i `wordfence`; mogą wpływać na cache, obrazki, bezpieczeństwo i zachowanie publicznych endpointów.
## Administracja i utrzymanie
- Strona admina `Jawność Cen` jest dodawana przez `add_management_page()` w `wp-content/plugins/elementor-addon/elementor-addon.php`.
- Po wdrożeniu zmian rewrite rules trzeba odświeżyć bezpośrednie odnośniki w WordPress.

View File

@@ -0,0 +1,64 @@
# Quality Risks
## Status skanu
- Quality Radar uruchomiony w trybie `full` dla `$paul-map-codebase`.
- `codebase-memory-mcp` CLI jest dostępny, ale MCP po `index_repository` nadal nie pokazuje projektu w `list_projects`; analiza użyła fallbacku przez `rg` i odczyt kluczowych plików.
- `jscpd`: disabled by policy.
- `ast-grep`: disabled by policy.
## Najważniejsze ryzyka
- `wp-content/plugins/elementor-addon/elementor-addon.php` łączy wiele odpowiedzialności: rejestrację assetów, migracje DB, cron, AJAX, XML, MD5 i admin UI. Zmiany w tym pliku mają szeroki blast radius.
- XML dla `dane.gov.pl` ma hardcodowane dane inwestycji i identyfikatory w `apartamenty_generate_datagov_xml()`. Zmiana nazwy inwestycji, URL lub wymagań portalu wymaga świadomej aktualizacji.
- Dane cenowe są stringami w ACF i w tabelach historii. `elementor_addon_format_price()` formatuje wartości aplikacyjnie, więc niespójny format wejścia może przejść do XML lub UI.
- `wp-content/plugins/elementor-addon/widgets/apartaments.php` używa zagnieżdżonego pola `information`, a cron/XML używają bezpośrednio `get_post_meta()` z kluczami `information_*`. To może być poprawne dla ACF, ale jest ryzykiem niespójności źródła prawdy.
- Publiczne AJAX endpointy są dostępne dla niezalogowanych użytkowników. Chroni je nonce, ale trzeba pilnować, żeby nie ujawniały danych innych niż publiczne ceny.
- Cache `apartamenty_price_xml_cache` jest czyszczony przy cronie, ale nie widać automatycznego czyszczenia po ręcznej edycji pól ACF; dokumentacja zakłada maksymalnie 1 godzinę opóźnienia.
- WP Cron może nie wykonać się punktualnie przy małym ruchu, jeśli nie ma systemowego crona.
## Kandydaci na hardcoded logic
- Nazwa inwestycji i opis XML w `apartamenty_generate_price_xml()` oraz `apartamenty_generate_datagov_xml()`.
- Kategorie/tagi `dane.gov.pl` w `wp-content/plugins/elementor-addon/elementor-addon.php`.
- Typy parkingu `zwykle` i `rodzinne` powtórzone w kilku miejscach tego samego pliku oraz w `widgets/parking-spots.php`.
- Statusy garażu `wolne`, `zarezerwowane`, `sprzedane`, `niedostępne` w `wp-content/themes/hello-elementor/template-parts/garage-map.php`.
## Duplikaty i source of truth
- `jscpd` nie został uruchomiony, bo jest wyłączony w `.paul/config.md`.
- Potencjalne duplikowanie definicji miejsc postojowych:
- `wp-content/plugins/elementor-addon/elementor-addon.php`
- `wp-content/plugins/elementor-addon/widgets/parking-spots.php`
- Potencjalne duplikowanie map pól lokali między:
- `wp-content/plugins/elementor-addon/widgets/apartaments.php`
- `wp-content/plugins/elementor-addon/elementor-addon.php`
- `docs/jawnosc-cen.md`
## Native prompt usage
- Nie znaleziono obszaru z promptami LLM w skanowanym kodzie własnym.
## Podejrzane switch/match blocks
- `ast-grep` nie został uruchomiony, bo jest wyłączony w `.paul/config.md`.
- Ręczny przegląd wskazuje na małe mapy tablicowe zamiast dużych `switch`, np. statusy parkingu i typy miejsc postojowych.
## Ryzyka frontend/backend consistency
- Modal historii cen zależy od ID elementów DOM renderowanych przez widgety i od odpowiedzi `admin-ajax.php`.
- `parking-spots.php` renderuje fallback overlay i przepisuje ID w JS, jeśli widget apartamentów nie istnieje na stronie; zmiany w modalach trzeba testować w obu układach.
- Fancybox jest z CDN, a Swiper lokalny; awaria CDN może uszkodzić galerie mimo działającego WordPress.
## Znane false positives / akceptowane ryzyka
- Duża liczba plików w `wp-admin/`, `wp-includes/`, `wp-content/plugins/elementor/`, `wp-content/plugins/elementor-pro/`, `wp-content/plugins/advanced-custom-fields*` to vendor i nie powinna być traktowana jako własny dług techniczny.
- Brak automatycznych testów jest istotną luką, ale typową dla wielu małych wdrożeń WordPress; przy zmianach krytycznych zalecana jest przynajmniej checklista ręczna z `.paul/codebase/testing.md`.
## Targeted scan: popup historii cen pokazuje tylko zmiany
- Plan: `.paul/plans/20260525-2149-historia-cen-zmiany/PLAN.md`.
- Ryzyko duplikacji: apartamenty i parking mają podobną logikę historii. Plan powinien dodać wspólną funkcję filtrującą, zamiast kopiować algorytm w dwóch endpointach.
- Ryzyko danych: ceny są stringami, więc porównanie zmian powinno używać stabilnej normalizacji tekstowej, nie parsowania do liczb.
- Ryzyko kontraktu: `main.js` oczekuje `history` jako listy obiektów z `recorded_at`, `price`, `price_m2`; plan nie powinien zmieniać tej struktury.
- Akceptowane odroczenie: XML jawności cen pozostaje poza zakresem, dopóki wymaganie dotyczy wyłącznie popupu.

View File

@@ -0,0 +1,39 @@
PAUL Quality Radar - codebase-memory
Timestamp: 2026-05-25 21:24:46 +02:00
Mode: full / map-codebase
Scope: entire repository
Tool check:
- codebase-memory-mcp --version -> codebase-memory-mcp 0.6.1
MCP actions:
- list_projects -> available projects did not include this repo.
- index_repository(repo_path="c:\\visual-studio-code\\wyszynskiego12.pl", mode="fast", persistence=true)
- Result: {"project":"c-visual-studio-code-wyszynskiego12.pl","status":"indexed"}
- list_projects after indexing still did not include the new project.
- search_graph(project="c-visual-studio-code-wyszynskiego12.pl", query="apartamenty price xml cron") returned project not found.
Fallback commands/analysis:
- rg --files with vendor-heavy folders partly excluded.
- rg for WordPress hooks, DB table creation, AJAX actions, rewrite rules, ACF fields, endpoint strings, TODO/FIXME/HACK.
- Manual reads of:
- wp-content/plugins/elementor-addon/elementor-addon.php
- wp-content/plugins/elementor-addon/widgets/apartaments.php
- wp-content/plugins/elementor-addon/widgets/parking-spots.php
- wp-content/themes/hello-elementor/functions.php
- wp-content/themes/hello-elementor/template-parts/garage-map.php
- docs/readme.md
- docs/jawnosc-cen.md
Key findings:
- Repo is a WordPress installation.
- Main custom domain logic lives in wp-content/plugins/elementor-addon/.
- Theme customizations live in wp-content/themes/hello-elementor/.
- Public endpoints: /ceny-mieszkan.xml, /ceny-mieszkan.md5, /dane-gov-pl.xml, /dane-gov-pl.md5.
- Cron hook: apartamenty_record_prices.
- Custom DB tables: price_history, parking_price_history with WordPress prefix.
- No app-level automated tests found for custom code.
- jscpd and ast-grep skipped because disabled by policy in .paul/config.md.
Artifact:
- .codebase-memory/graph.db.zst exists after indexing attempt.

View File

@@ -0,0 +1,21 @@
Mode: plan
Timestamp: 2026-05-25T21:49:47+02:00
Scope: Popup historii cen apartamentu oraz miejsc parkingowych ma pokazywać tylko daty zmian ceny.
MCP:
- `index_repository` dla `c:\visual-studio-code\wyszynskiego12.pl` zwrócił status indexed i projekt `c-visual-studio-code-wyszynskiego12.pl`.
- `list_projects` po indeksowaniu nadal nie pokazał tego projektu w dostępnych projektach MCP.
- `search_graph` dla tego projektu zwrócił "project not found or not indexed".
Fallback discovery:
- `rg` wskazał główne miejsca zmiany:
- `wp-content/plugins/elementor-addon/elementor-addon.php`
- `wp-content/plugins/elementor-addon/assets/js/main.js`
- `wp-content/plugins/elementor-addon/widgets/apartaments.php`
- `wp-content/plugins/elementor-addon/widgets/parking-spots.php`
Key findings:
- `elementor_addon_get_price_history_ajax()` pobiera historię lokalu z `wp_price_history` malejąco po `recorded_at` i zwraca pełne dzienne snapshoty.
- `elementor_addon_get_parking_price_history_ajax()` pobiera historię parkingu z `wp_parking_price_history` malejąco po `recorded_at` i zwraca pełne dzienne snapshoty.
- `main.js` renderuje tylko `json.data.history`, więc filtrowanie po stronie PHP może zachować kontrakt frontendu.
- XML generuje historię bezpośrednio z tabel i nie jest objęty zakresem popupu.

View File

@@ -0,0 +1,22 @@
Mode: post-apply
Timestamp: 2026-05-25T22:00:11+02:00
Plan: .paul/plans/20260525-2149-historia-cen-zmiany/PLAN.md
Changed files:
- wp-content/plugins/elementor-addon/elementor-addon.php
Implementation:
- Added `elementor_addon_filter_price_change_history(array $history)`.
- Apartment AJAX history query now reads rows chronologically by `recorded_at ASC`, filters continuous price ranges, and returns the change rows in descending order for the popup.
- Parking AJAX history query now uses the same filtering helper.
- Single-price histories with multiple dates now return the latest and earliest dates for that unchanged range; a one-row history is not duplicated.
- JSON fields consumed by `main.js` remain unchanged.
Verification:
- `php -l wp-content/plugins/elementor-addon/elementor-addon.php` passed.
- Isolated PHP sample for the filtering algorithm returned `2026-05-20,2026-05-16,2026-05-01`, confirming first day of each range is kept and latest changes are returned first.
- Isolated PHP sample for a single unchanged price returned `2026-05-20,2026-05-01`, confirming first and last dates are shown.
Manual verification still recommended:
- Open apartment price history popup with repeated daily prices.
- Open parking price history popup for `zwykle` and `rodzinne`.

60
.paul/codebase/stack.md Normal file
View File

@@ -0,0 +1,60 @@
# Stack
## Typ projektu
- Pełna instalacja WordPress w katalogu repozytorium.
- Główna logika własna znajduje się w `wp-content/plugins/elementor-addon/` oraz w dopiskach do motywu `wp-content/themes/hello-elementor/`.
- Projekt działa jako strona inwestycji Wyszyńskiego 12 z prezentacją lokali, miejsc postojowych, historią cen i endpointami XML/MD5.
## Runtime i frameworki
- PHP / WordPress: entrypointy WordPress w `index.php`, `wp-load.php`, `wp-settings.php`, `wp-admin/`, `wp-includes/`.
- Motyw: `wp-content/themes/hello-elementor/`.
- Builder UI: Elementor i Elementor Pro w `wp-content/plugins/elementor/` oraz `wp-content/plugins/elementor-pro/`.
- Pola danych: ACF / ACF Pro w `wp-content/plugins/advanced-custom-fields/` i `wp-content/plugins/advanced-custom-fields-pro/`.
## Własny kod aplikacyjny
- `wp-content/plugins/elementor-addon/elementor-addon.php` - rejestracja widgetów, assetów, tabel historii cen, crona, AJAX, endpointów XML/MD5 i strony administracyjnej.
- `wp-content/plugins/elementor-addon/widgets/apartaments.php` - widget Elementor renderujący listę lokali i przycisk historii cen.
- `wp-content/plugins/elementor-addon/widgets/parking-spots.php` - widget Elementor renderujący ceny miejsc postojowych i historię cen.
- `wp-content/plugins/elementor-addon/assets/js/main.js` - Swiper, Fancybox i obsługa AJAX historii cen.
- `wp-content/plugins/elementor-addon/assets/css/main.scss` oraz `wp-content/plugins/elementor-addon/assets/css/main.css` - style widgetów.
- `wp-content/themes/hello-elementor/functions.php` - custom CSS, shortcode `garage_map`, filtr zamieniający `m2` na zapis z indeksem górnym.
- `wp-content/themes/hello-elementor/template-parts/garage-map.php` - SVG mapy garażu z klasami statusów pobieranymi z ACF options.
## Biblioteki frontendowe
- Swiper jest vendoryzowany lokalnie w `wp-content/plugins/elementor-addon/plugins/swiper/`.
- Fancybox ładowany jest z CDN `https://cdn.jsdelivr.net/npm/@fancyapps/ui@5.0/dist/fancybox/...` w `wp-content/plugins/elementor-addon/elementor-addon.php`.
- Motyw Hello Elementor ma własne assety i vendor w `wp-content/themes/hello-elementor/vendor/`.
## Wtyczki obecne w repo
- `advanced-custom-fields`
- `advanced-custom-fields-pro`
- `elementor`
- `elementor-pro`
- `elementor-addon`
- `draw-attention-pro`
- `litespeed-cache`
- `webp-express`
- `wordfence`
- `duplicator-pro-v4.5.16.2`
- `simple-custom-post-order`
- `stops-core-theme-and-plugin-updates`
- `svg-support`
- `copy-delete-posts`
- `akismet`
## Manifesty i dependency management
- Brak głównego `composer.json` lub `package.json` w root projektu.
- Manifesty występują w vendor/plugin/theme, np. `wp-content/themes/twentytwentyfive/package.json`, `wp-content/plugins/litespeed-cache/composer.json`, `wp-content/plugins/advanced-custom-fields/composer.json`.
- Własna wtyczka `wp-content/plugins/elementor-addon/` nie ma osobnego manifestu ani skryptów build w repo.
## Konfiguracja
- WordPress konfigurowany przez `wp-config.php`; dokumentacja PAUL nie zapisuje sekretów z tego pliku.
- PAUL: `.paul/config.md`.
- Dokumentacja domenowa: `docs/readme.md`, `docs/jawnosc-cen.md`.

49
.paul/codebase/testing.md Normal file
View File

@@ -0,0 +1,49 @@
# Testing
## Stan automatycznych testów
- Nie znaleziono testów automatycznych dla własnego kodu `wp-content/plugins/elementor-addon/` ani customizacji `wp-content/themes/hello-elementor/`.
- W repo występują pliki testowe i manifesty w vendorach/pluginach, np. `wp-content/plugins/webp-express/vendor/.../phpunit.xml.dist`, ale nie są one testami tej aplikacji.
- Brak głównego `composer.json`, `package.json`, `phpunit.xml` lub skryptów CI w root projektu.
## Zalecane testy ręczne dla zmian domenowych
- Endpointy publiczne:
- `/ceny-mieszkan.xml` zwraca poprawny XML i `Content-Type: application/xml`.
- `/ceny-mieszkan.md5` zwraca 32-znakowy hash.
- `/dane-gov-pl.xml` zwraca katalog z zasobem wskazującym na `/ceny-mieszkan.xml`.
- `/dane-gov-pl.md5` zwraca 32-znakowy hash.
- AJAX historii cen:
- klik `HISTORIA CEN` przy lokalu wysyła `action=apartamenty_get_price_history`.
- klik `HISTORIA CEN` przy miejscu postojowym wysyła `action=parking_get_price_history`.
- błędny nonce lub brak danych zwraca kontrolowany JSON error.
- WP Cron:
- `wp cron event list` pokazuje `apartamenty_record_prices`.
- `wp cron event run apartamenty_record_prices` zapisuje po jednym rekordzie dziennie.
- Baza danych:
- `wp_price_history` ma unikalność `(post_id, recorded_at)`.
- `wp_parking_price_history` ma unikalność `(parking_type, recorded_at)`.
- Frontend:
- widget `Apartaments` renderuje galerie, ceny, dokumenty i modal historii.
- widget `Miejsca Postojowe` renderuje ceny i modal historii również bez widgetu lokali na stronie.
- `[garage_map]` renderuje mapę garażu, a klasy statusów odpowiadają ACF options.
## Komendy pomocnicze
```bash
wp cron event list
wp cron event run apartamenty_record_prices
wp transient delete apartamenty_price_xml_cache
```
```sql
SELECT * FROM wp_price_history ORDER BY recorded_at DESC LIMIT 20;
SELECT * FROM wp_parking_price_history ORDER BY recorded_at DESC LIMIT 20;
```
## Luki testowe
- Brak testu regresji dla generowania XML.
- Brak testu schematu dla katalogu `dane.gov.pl`.
- Brak testu AJAX dla historii cen.
- Brak testu migracji/tworzenia tabel przez `dbDelta()`.

View File

@@ -0,0 +1,78 @@
# Tooling Status
## Post-apply scan: popup historii cen pokazuje tylko zmiany
- Timestamp: 2026-05-25T22:00:11+02:00.
- Scan mode: post-apply.
- Scope: `.paul/plans/20260525-2149-historia-cen-zmiany/PLAN.md`.
- `codebase-memory-mcp`: degraded as in plan scan; prior MCP indexing did not make the project visible in `list_projects`.
- Fallback: reviewed git diff and verified `wp-content/plugins/elementor-addon/elementor-addon.php`.
- `jscpd`: disabled by policy.
- `ast-grep`: disabled by policy.
- Raw notes: `.paul/codebase/radar/codebase-memory-post-apply.txt`.
- Verification: `php -l wp-content/plugins/elementor-addon/elementor-addon.php` passed; isolated filter sample passed.
- Next action: run `$paul-unify .paul/plans/20260525-2149-historia-cen-zmiany/PLAN.md`.
## Plan scan: popup historii cen pokazuje tylko zmiany
- Timestamp: 2026-05-25T21:49:47+02:00.
- Scan mode: plan.
- Scope: popup historii cen apartamentu oraz miejsc parkingowych.
- `codebase-memory-mcp`: degraded. `index_repository` zwrócił `indexed`, ale projekt nie pojawia się w `list_projects`, a `search_graph` zwrócił `project not found or not indexed`.
- Fallback: `rg` i odczyt plików `elementor-addon.php`, `main.js`, `widgets/apartaments.php`, `widgets/parking-spots.php`.
- `jscpd`: disabled by policy.
- `ast-grep`: disabled by policy.
- Raw notes: `.paul/codebase/radar/codebase-memory-plan.txt`.
- Next action: review and approve `.paul/plans/20260525-2149-historia-cen-zmiany/PLAN.md`, then run `$paul-apply .paul/plans/20260525-2149-historia-cen-zmiany/PLAN.md`.
## Skan
- Timestamp: 2026-05-25 21:24:46 +02:00.
- Tryb: `full`.
- Zakres: entire repository.
- Status: degraded.
## Narzędzia
- `codebase-memory-mcp`: wykryte, wersja `0.6.1`.
- MCP `index_repository`: zwrócił `{"project":"c-visual-studio-code-wyszynskiego12.pl","status":"indexed"}`.
- MCP `list_projects`: nie pokazuje `c-visual-studio-code-wyszynskiego12.pl` po indeksowaniu.
- MCP `search_graph`: `project not found or not indexed`.
- `jscpd`: disabled by policy w `.paul/config.md`.
- `ast-grep`: disabled by policy w `.paul/config.md`.
## Komendy i akcje
- Sprawdzono `.paul/config.md`.
- Sprawdzono `codebase-memory-mcp --version`.
- Uruchomiono MCP `index_repository` w trybie `fast` z `persistence: true`.
- Sprawdzono MCP `list_projects` i `search_graph`.
- Użyto fallbacku `rg` dla własnego kodu i dokumentacji.
- Przejrzano kluczowe pliki:
- `wp-content/plugins/elementor-addon/elementor-addon.php`
- `wp-content/plugins/elementor-addon/widgets/apartaments.php`
- `wp-content/plugins/elementor-addon/widgets/parking-spots.php`
- `wp-content/plugins/elementor-addon/assets/js/main.js`
- `wp-content/themes/hello-elementor/functions.php`
- `wp-content/themes/hello-elementor/template-parts/garage-map.php`
- `docs/readme.md`
- `docs/jawnosc-cen.md`
## Raw output paths
- Artifact indeksu: `.codebase-memory/graph.db.zst`.
- Raw notes: `.paul/codebase/radar/codebase-memory-full.txt`.
- Raporty PAUL:
- `.paul/codebase/impact_map.md`
- `.paul/codebase/quality_risks.md`
- `.paul/codebase/tooling_status.md`
## Ograniczenia
- Brak pełnych wyników grafu MCP dla tego repo mimo zwróconego statusu `indexed`.
- Ciężkie skany duplikacji i strukturalne nie były uruchamiane, bo konfiguracja jawnie je wyłącza.
- Vendor WordPress/Elementor/ACF został potraktowany jako kontekst, nie jako główny obszar audytu.
## Następna akcja
Użyć `$paul-plan [opis pracy]`, gdy pojawi się konkretna zmiana. Dla głębszego audytu duplikacji można najpierw włączyć `quality_radar.tools.jscpd: true` albo poprosić o skan duplikatów wprost.

55
.paul/config.md Normal file
View File

@@ -0,0 +1,55 @@
# Project Config
**Project:** wyszynskiego12.pl
**Created:** 2026-05-25
## Project Settings
```yaml
project:
name: wyszynskiego12.pl
version: 0.0.0
```
## Integrations
### Quality Radar
```yaml
quality_radar:
enabled: true
auto_install: true
tools:
codebase_memory_mcp: true
jscpd: false
ast_grep: false
reports:
update_on_plan: true
update_on_apply: true
update_on_verify: true
```
### SonarQube
```yaml
sonarqube:
enabled: false
project_key: wyszynskiego12-pl
server_url: http://localhost:9000
```
## Preferences
```yaml
preferences:
auto_commit: false
verbose_output: false
parallel_agents: false
plan_storage: plans
state_autocompress: true
state_autocompress_max_lines: 500
```
---
*Config created: 2026-05-25*

View File

@@ -1,48 +0,0 @@
<?php
$conn = new mysqli('host117523.hostido.net.pl', 'host117523_wyszynskiego12pagedevpl', 'yZs52KdErtTk9KmZ8XGq', 'host117523_wyszynskiego12pagedevpl');
if ($conn->connect_error) { die('Blad: ' . $conn->connect_error); }
echo "=== APARTAMENTY + ACF price fields ===\n";
$sql = "SELECT p.ID, p.post_title, pm.meta_key, pm.meta_value
FROM wp_posts p
JOIN wp_postmeta pm ON p.ID = pm.post_id
WHERE p.post_type = 'apartamenty'
AND p.post_status = 'publish'
AND pm.meta_key = 'information'
ORDER BY p.post_title LIMIT 5";
$r = $conn->query($sql);
while($row = $r->fetch_assoc()) {
echo $row['ID'] . ' | ' . $row['post_title'] . "\n";
$data = maybe_unserialize($row['meta_value']);
if (is_array($data)) {
echo " price: " . ($data['price'] ?? 'brak') . "\n";
echo " price_m2: " . ($data['price_m2'] ?? 'brak') . "\n";
} else {
echo " raw: " . substr($row['meta_value'], 0, 300) . "\n";
}
}
echo "\n=== WSZYSTKIE meta_key dla apartamentu ===\n";
$sql2 = "SELECT DISTINCT pm.meta_key
FROM wp_posts p
JOIN wp_postmeta pm ON p.ID = pm.post_id
WHERE p.post_type = 'apartamenty'
AND p.post_status = 'publish'
ORDER BY pm.meta_key";
$r2 = $conn->query($sql2);
while($row = $r2->fetch_row()) { echo $row[0] . "\n"; }
$conn->close();
function maybe_unserialize($data) {
if (is_serialized($data)) return unserialize($data);
return $data;
}
function is_serialized($data) {
if (!is_string($data)) return false;
$data = trim($data);
if ('N;' === $data) return true;
if (strlen($data) < 4) return false;
if (':' !== $data[1]) return false;
return (bool) preg_match('/^([adObis]):/', $data);
}

View File

@@ -1,21 +0,0 @@
<?php
$conn = new mysqli('host117523.hostido.net.pl', 'host117523_wyszynskiego12pagedevpl', 'yZs52KdErtTk9KmZ8XGq', 'host117523_wyszynskiego12pagedevpl');
if ($conn->connect_error) { die('Blad: ' . $conn->connect_error); }
echo "=== APARTAMENTY z ceną ===\n";
$sql = "SELECT p.ID, p.post_title,
MAX(CASE WHEN pm.meta_key = 'information_price' THEN pm.meta_value END) as price,
MAX(CASE WHEN pm.meta_key = 'information_price_m2' THEN pm.meta_value END) as price_m2,
MAX(CASE WHEN pm.meta_key = 'information_floor_space' THEN pm.meta_value END) as floor_space
FROM wp_posts p
JOIN wp_postmeta pm ON p.ID = pm.post_id
WHERE p.post_type = 'apartamenty'
AND p.post_status = 'publish'
GROUP BY p.ID, p.post_title
ORDER BY p.post_title LIMIT 10";
$r = $conn->query($sql);
while($row = $r->fetch_assoc()) {
echo $row['ID'] . ' | ' . $row['post_title'] . ' | price: ' . $row['price'] . ' | price_m2: ' . $row['price_m2'] . ' | m2: ' . $row['floor_space'] . "\n";
}
$conn->close();

View File

@@ -1,44 +0,0 @@
# v0.1 Initial Release - Archive
**Archived:** 2026-03-12
**Status:** Complete
---
# Roadmap: wyszynskiego12.pagedev.pl
## Overview
Strona internetowa dla dewelopera pozwalająca na okazanie oferty klientom. Projekt obejmuje budowę witryny prezentującej inwestycje, lokale i informacje kontaktowe.
## Current Milestone
**v0.1 Initial Release** (v0.1.0)
Status: Complete
Completed: 2026-03-12
Phases: 2 of 2 complete
## Phases
| Phase | Name | Plans | Status | Completed |
|-------|------|-------|--------|-----------|
| 1 | Historia cen | 2 | Complete | 2026-03-12 |
| 2 | Jawnosc cen | 1 | Complete | 2026-03-12 |
## Phase Details
### Phase 1: Historia cen
**Goal:** Uzytkownik widzi popup z historia zmian cen dla kazdego apartamentu.
**Plans:**
- [x] 01-01: Backend — tabela DB, cron, AJAX endpoint
- [x] 01-02: Frontend — popup HTML/CSS/JS
### Phase 2: Jawnosc cen
**Goal:** Deweloper moze automatycznie raportowac ceny mieszkan do portalu dane.gov.pl.
**Plans:**
- [x] 02-01: XML endpoints + admin page
---
*Roadmap archived: 2026-03-12*

View File

@@ -1,225 +0,0 @@
---
phase: 01-historia-cen
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- wp-content/plugins/elementor-addon/elementor-addon.php
autonomous: true
---
<objective>
## Goal
Stworzyć backend dla historii cen apartamentów:
- tabela `wp_price_history` w bazie danych
- WP Cron zapisujący ceny raz dziennie
- AJAX endpoint zwracający historię dla danego apartamentu
## Purpose
Ceny apartamentów zarządzane są przez ACF (pola `information_price`, `information_price_m2`, `information_floor_space`).
Historia musi być zapisywana codziennie, bo ACF nie przechowuje zmian — tylko aktualną wartość.
## Output
- Tabela `wp_price_history` założona przy aktywacji pluginu
- Hook `apartamenty_record_prices` uruchamiany codziennie przez WP Cron
- AJAX action `apartamenty_get_price_history` zwracający JSON
</objective>
<context>
## Project Context
@.paul/PROJECT.md
@.paul/STATE.md
## Source Files
@wp-content/plugins/elementor-addon/elementor-addon.php
## Struktura danych ACF
Każdy apartament (post_type: `apartamenty`) posiada w `wp_postmeta`:
- `information_price` → string np. `"677 920"` (cena brutto w zł)
- `information_price_m2` → string np. `"19 000"` (cena za m² w zł)
- `information_floor_space` → string np. `"35,68"` (metraż w m²)
Prefix tabel WP: `wp_`
DB credentials: z wp-config.php (DB_NAME, DB_USER, DB_PASSWORD, DB_HOST)
</context>
<acceptance_criteria>
## AC-1: Tabela istnieje w bazie
```gherkin
Given plugin elementor-addon jest aktywny
When WordPress się inicjalizuje
Then tabela wp_price_history istnieje z kolumnami: id, post_id, price, price_m2, floor_space, recorded_at
And istnieje UNIQUE KEY na (post_id, recorded_at) jeden rekord na apt na dzień
```
## AC-2: Cron zapisuje ceny codziennie
```gherkin
Given istnieją apartamenty z wypełnionymi polami cen ACF
When hook `apartamenty_record_prices` zostanie wywołany
Then dla każdego apartamentu z post_type=apartamenty i post_status=publish
zostaje wstawiony lub zignorowany (INSERT IGNORE) rekord do wp_price_history
z aktualną ceną i datą TODAY()
```
## AC-3: AJAX endpoint zwraca historię
```gherkin
Given apartament o ID=X ma rekordy w wp_price_history
When POST na /wp-admin/admin-ajax.php z action=apartamenty_get_price_history i post_id=X
Then odpowiedź JSON zawiera:
{ success: true, data: { title, price, price_m2, history: [{date, price, price_m2}] } }
And historia jest posortowana od najnowszej daty
```
## AC-4: Nonce zabezpiecza AJAX
```gherkin
Given request AJAX bez poprawnego nonce
When POST na admin-ajax.php
Then odpowiedź: { success: false }
```
</acceptance_criteria>
<tasks>
<task type="auto">
<name>Task 1: Utwórz tabelę wp_price_history i zarejestruj cron</name>
<files>wp-content/plugins/elementor-addon/elementor-addon.php</files>
<action>
Dodaj na końcu pliku elementor-addon.php (przed zamknięciem) następujące funkcje:
1. **Funkcja tworząca tabelę** `elementor_addon_create_price_history_table()`:
- Używa `$wpdb->prefix . 'price_history'`
- SQL z `dbDelta()` (wymaga `require_once ABSPATH . 'wp-admin/includes/upgrade.php'`)
- Schemat tabeli:
```sql
CREATE TABLE {prefix}price_history (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
post_id BIGINT UNSIGNED NOT NULL,
price VARCHAR(50) NOT NULL DEFAULT '',
price_m2 VARCHAR(50) NOT NULL DEFAULT '',
floor_space VARCHAR(50) NOT NULL DEFAULT '',
recorded_at DATE NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY unique_daily (post_id, recorded_at),
KEY idx_post_id (post_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```
- Podpięty do hooków: `register_activation_hook(__FILE__, ...)` ORAZ `init` (z check przez `get_option('elementor_addon_db_version')` — jeśli wersja != '1.0', wywołaj i ustaw opcję)
2. **Funkcja zapisująca ceny** `elementor_addon_record_prices()`:
- WP_Query: post_type='apartamenty', post_status='publish', posts_per_page=-1, fields='ids'
- Dla każdego ID: pobiera meta `information_price`, `information_price_m2`, `information_floor_space`
- Wstawia do tabeli przez `$wpdb->query($wpdb->prepare("INSERT IGNORE INTO ...", ...))`
- `recorded_at` = `current_time('Y-m-d')`
3. **Rejestracja crona**:
- Na hooku `wp` (nie `init`): jeśli `!wp_next_scheduled('apartamenty_record_prices')` → `wp_schedule_event(time(), 'daily', 'apartamenty_record_prices')`
- `add_action('apartamenty_record_prices', 'elementor_addon_record_prices')`
Unikaj: global $wpdb wewnątrz funkcji — używaj `global $wpdb` na początku każdej funkcji korzystającej z $wpdb.
Unikaj: bezpośrednich zapytań SQL bez $wpdb->prepare() tam gdzie są parametry użytkownika.
</action>
<verify>
1. Aktywuj/dezaktywuj plugin lub odwiedź dowolną stronę WP
2. Sprawdź przez phpMyAdmin lub: `SELECT * FROM wp_price_history LIMIT 5;`
3. Sprawdź: `SELECT option_value FROM wp_options WHERE option_name = 'elementor_addon_db_version';` → powinno zwrócić '1.0'
4. Sprawdź cron: `SELECT * FROM wp_options WHERE option_name = 'cron';` — powinien zawierać 'apartamenty_record_prices'
</verify>
<done>AC-1 i AC-2 spełnione: tabela istnieje, cron zarejestrowany</done>
</task>
<task type="auto">
<name>Task 2: AJAX endpoint historii cen</name>
<files>wp-content/plugins/elementor-addon/elementor-addon.php</files>
<action>
Dodaj na końcu pliku `elementor-addon.php` funkcję `elementor_addon_get_price_history_ajax()`:
1. **Weryfikacja nonce**: `check_ajax_referer('apartamenty_price_history_nonce', 'nonce', false)` — jeśli false: `wp_send_json_error()` i `die()`
2. **Walidacja post_id**: `$post_id = absint($_POST['post_id'] ?? 0)` — jeśli 0: `wp_send_json_error()`
3. **Pobierz tytuł i aktualne ceny**:
- `get_the_title($post_id)`
- `get_post_meta($post_id, 'information_price', true)`
- `get_post_meta($post_id, 'information_price_m2', true)`
- `get_post_meta($post_id, 'information_floor_space', true)`
4. **Pobierz historię z DB**:
```php
$rows = $wpdb->get_results($wpdb->prepare(
"SELECT recorded_at, price, price_m2, floor_space FROM {$wpdb->prefix}price_history
WHERE post_id = %d ORDER BY recorded_at DESC LIMIT 50",
$post_id
));
```
5. **Zwróć JSON**:
```php
wp_send_json_success([
'title' => get_the_title($post_id),
'price' => get_post_meta($post_id, 'information_price', true),
'price_m2' => get_post_meta($post_id, 'information_price_m2', true),
'floor_space'=> get_post_meta($post_id, 'information_floor_space', true),
'history' => $rows,
]);
```
6. **Zarejestruj akcję** (dla zalogowanych i niezalogowanych):
```php
add_action('wp_ajax_apartamenty_get_price_history', 'elementor_addon_get_price_history_ajax');
add_action('wp_ajax_nopriv_apartamenty_get_price_history', 'elementor_addon_get_price_history_ajax');
```
7. **Przekaż nonce do JS** przez `wp_localize_script` na istniejący skrypt `elementor-addon-main-js`:
Dodaj nowy hook `wp_enqueue_scripts` z `wp_localize_script('elementor-addon-main-js', 'apartamentsData', ['ajaxUrl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('apartamenty_price_history_nonce')])`
WAŻNE: `wp_localize_script` musi być wywołane po enqueue/register skryptu.
</action>
<verify>
Wykonaj w przeglądarce lub przez curl (po uzyskaniu nonce):
POST /wp-admin/admin-ajax.php z body: action=apartamenty_get_price_history&post_id=203&nonce=XXXX
Oczekiwana odpowiedź: {"success":true,"data":{"title":"Apartament 15","price":"677 920",...}}
</verify>
<done>AC-3 i AC-4 spełnione: endpoint zwraca dane JSON, nonce weryfikowany</done>
</task>
</tasks>
<boundaries>
## DO NOT CHANGE
- wp-content/plugins/elementor-addon/widgets/apartaments.php (zmiany w planie 01-02)
- wp-content/plugins/elementor-addon/assets/css/main.css (zmiany w planie 01-02)
- wp-content/plugins/elementor-addon/assets/js/main.js (zmiany w planie 01-02)
- wp-config.php
- Inne wtyczki i motyw
## SCOPE LIMITS
- Ten plan nie dodaje UI ani CSS — tylko backend PHP
- Nie modyfikuj istniejących funkcji w elementor-addon.php, tylko dodawaj nowe na końcu
- Nie twórz osobnego pliku PHP — wszystko w elementor-addon.php
</boundaries>
<verification>
Before declaring plan complete:
- [ ] Tabela `wp_price_history` istnieje w bazie (sprawdź przez phpMyAdmin lub SQL)
- [ ] Opcja `elementor_addon_db_version` = '1.0' w wp_options
- [ ] Cron `apartamenty_record_prices` widoczny w wp_options cron
- [ ] AJAX action `apartamenty_get_price_history` odpowiada JSON z `success: true`
- [ ] Request bez nonce zwraca `success: false`
- [ ] Brak PHP Fatal errors (sprawdź wp-content/debug.log lub WP_DEBUG)
</verification>
<success_criteria>
- Tabela wp_price_history utworzona z poprawnym schematem
- Cron rejestruje się automatycznie bez manualnej aktywacji
- AJAX endpoint działa dla publicznych użytkowników (nopriv)
- Nonce zabezpiecza endpoint
- Żadnych błędów PHP
</success_criteria>
<output>
Po ukończeniu utwórz `.paul/phases/01-historia-cen/01-01-SUMMARY.md`
</output>

View File

@@ -1,109 +0,0 @@
---
phase: 01-historia-cen
plan: 01
subsystem: database
tags: [wordpress, acf, cron, ajax, mysql]
requires: []
provides:
- "Tabela wp_price_history z unikalnym kluczem (post_id, recorded_at)"
- "WP Cron dzienny zapisujący ceny apartamentów z ACF"
- "AJAX endpoint apartamenty_get_price_history (zalogowani i goście)"
- "wp_localize_script przekazujący ajaxUrl + nonce do JS"
affects: ["01-02-frontend"]
tech-stack:
added: []
patterns:
- "INSERT IGNORE dla idempotentnego zapisu dziennego"
- "dbDelta() + get_option version check dla migracji DB"
- "wp_localize_script na priorytecie 20 po register"
key-files:
modified:
- "wp-content/plugins/elementor-addon/elementor-addon.php"
key-decisions:
- "ACF flat meta keys (information_price, information_price_m2, information_floor_space) zamiast serializowanego pola 'information'"
- "INSERT IGNORE zamiast ON DUPLICATE KEY UPDATE — historia to snapshot, nie aktualizacja"
- "Hook 'wp' dla crona (nie 'init') — gwarantuje kontekst frontendu"
patterns-established:
- "Nonce: apartamenty_price_history_nonce — używany w JS i PHP"
- "AJAX action: apartamenty_get_price_history"
- "Dane JS: window.apartamentsData.ajaxUrl, .nonce"
duration: 15min
started: 2026-03-12T14:00:00Z
completed: 2026-03-12T14:15:00Z
---
# Faza 1 Plan 01: Backend Historii Cen — Summary
**Tabela `wp_price_history` + WP Cron dzienny + AJAX endpoint zabezpieczony nonce — cały backend historii cen gotowy.**
## Performance
| Metryka | Wartość |
|---------|---------|
| Czas wykonania | ~15 min |
| Zadania | 2 ukończone |
| Pliki zmienione | 1 |
## Acceptance Criteria Results
| Kryterium | Status | Uwagi |
|-----------|--------|-------|
| AC-1: Tabela istnieje w bazie | Pass | Zweryfikowano przez bezpośrednie połączenie DB — tabela `wp_price_history` założona |
| AC-2: Cron zapisuje ceny codziennie | Pass | Hook `apartamenty_record_prices` zarejestrowany na `daily`, INSERT IGNORE zweryfikowany |
| AC-3: AJAX endpoint zwraca historię | Pass | Akcje `wp_ajax_*` zarejestrowane, struktura JSON zgodna z planem |
| AC-4: Nonce zabezpiecza AJAX | Pass | `check_ajax_referer` przed jakimkolwiek dostępem do danych |
## Accomplishments
- Tabela `wp_price_history` założona w bazie przez `dbDelta()` — samonaprawiająca się migracja
- WP Cron `apartamenty_record_prices` dzienny — zapis snapshot cen ze wszystkich apartamentów
- AJAX endpoint publiczny (nopriv) zwracający tytuł, aktualne ceny i historię jako JSON
- Nonce `apartamenty_price_history_nonce` przekazany do JS przez `wp_localize_script`
## Files Created/Modified
| Plik | Zmiana | Co dodano |
|------|--------|-----------|
| `wp-content/plugins/elementor-addon/elementor-addon.php` | Zmodyfikowany | +~140 linii: tabela DB, cron, AJAX endpoint, wp_localize_script |
## Decisions Made
| Decyzja | Uzasadnienie | Wpływ |
|---------|--------------|-------|
| Flat meta keys zamiast `get_field('information')` | ACF zapisuje dane jako płaskie klucze — `information_price` etc. istnieją i są puste dla `information` | Cron pobiera dane bezpośrednio przez `get_post_meta` |
| INSERT IGNORE zamiast UPDATE | Historia to snapshot — nie nadpisujemy dawnych wpisów | Jeden rekord na apartament na dzień, bezpieczny dla wielu wywołań |
| Hook `wp` dla crona | Gwarantuje pełny kontekst WP przy rejestracji | Cron rejestruje się tylko na stronach frontendowych/adminowych |
## Deviations from Plan
Brak odchyleń — plan wykonany dokładnie jak zaplanowano.
## Issues Encountered
| Problem | Rozwiązanie |
|---------|-------------|
| Brak klienta mysql CLI na lokalnym środowisku | Weryfikacja przez PHP mysqli (tymczasowy skrypt `.paul/verify_task1.php`, usunięty po weryfikacji) |
## Next Phase Readiness
**Gotowe dla 01-02 (Frontend):**
- `window.apartamentsData.ajaxUrl` — URL do admin-ajax.php
- `window.apartamentsData.nonce` — nonce do requestu
- AJAX action: `apartamenty_get_price_history` z `post_id` w POST body
- Odpowiedź JSON: `{ success, data: { title, price, price_m2, floor_space, history: [{recorded_at, price, price_m2}] } }`
**Uwagi:**
- Tabela ma 1 testowy rekord dla apt 203 (2026-03-12) — wstawiony podczas weryfikacji
- Cron uruchomi się automatycznie przy pierwszym odwiedzeniu strony przez WP
**Blokady:** Brak
---
*Phase: 01-historia-cen, Plan: 01*
*Completed: 2026-03-12*

View File

@@ -1,598 +0,0 @@
---
phase: 01-historia-cen
plan: 02
type: execute
wave: 1
depends_on: ["01-01"]
files_modified:
- wp-content/plugins/elementor-addon/widgets/apartaments.php
- wp-content/plugins/elementor-addon/assets/css/main.scss
- wp-content/plugins/elementor-addon/assets/css/main.css
- wp-content/plugins/elementor-addon/assets/js/main.js
autonomous: false
---
<objective>
## Goal
Zbudować popup „Historia cen" — klikalny przycisk na karcie apartamentu otwiera modal
z aktualną ceną i tabelą historii zmian, zasilany przez AJAX endpoint z planu 01-01.
## Purpose
Użytkownik może zobaczyć historię zmian cen apartamentu bez opuszczania strony.
Popup pokazuje nazwę apartamentu, aktualną cenę brutto i za m², oraz tabelę dat ze zmianami.
## Output
- Przycisk „HISTORIA CEN" z `data-post-id` w widgecie
- Globalny popup HTML (overlay + modal) w widgecie
- CSS modal pasujący do projektu (font Barlow, kolor #192c44, border 4px solid)
- JS: fetch AJAX → wypełnienie popupa → pokaż/ukryj
</objective>
<context>
## Project Context
@.paul/PROJECT.md
@.paul/STATE.md
## Prior Work
@.paul/phases/01-historia-cen/01-01-SUMMARY.md
## Source Files
@wp-content/plugins/elementor-addon/widgets/apartaments.php
@wp-content/plugins/elementor-addon/assets/css/main.scss
@wp-content/plugins/elementor-addon/assets/js/main.js
## API Contract (z planu 01-01)
POST admin-ajax.php
body: action=apartamenty_get_price_history&post_id=ID&nonce=NONCE
response: {
success: true,
data: {
title: "Apartament X",
price: "677 920", // cena brutto
price_m2: "19 000", // cena za m²
floor_space: "35,68", // metraż
history: [
{ recorded_at: "2026-01-16", price: "677 920", price_m2: "19 000" }
]
}
}
## JS globals dostępne na stronie
window.apartamentsData.ajaxUrl — URL do admin-ajax.php
window.apartamentsData.nonce — nonce 'apartamenty_price_history_nonce'
## Design (z mockupu Group 68.png)
- Białe tło, border matching kart (#192c44)
- Tytuł (bold, duży) + X w prawym górnym rogu
- "Cena brutto:" (bold) → wartość bold po prawej z " zł"
- "Cena m²:" (normal) → wartość normal po prawej z " zł"
- Separator
- Tabela: data | cena_m2 (format "X zł/m²") | cena brutto (format "X,00 zł")
- Overlay ciemne tło półprzezroczyste
## Istniejące style CSS
- Font: 'Barlow', sans-serif
- Kolor główny: #192c44
- Border kart: 4px solid #192c44
- Wiersz .apartament-card__price-history już ma cursor: pointer
</context>
<acceptance_criteria>
## AC-1: Przycisk "Historia cen" jest klikalny
```gherkin
Given apartament ma dane cen w ACF
When użytkownik widzi kartę apartamentu
Then wiersz "HISTORIA CEN" jest klikalny i posiada atrybut data-post-id z ID apartamentu
```
## AC-2: Popup otwiera się z danymi
```gherkin
Given użytkownik klika "HISTORIA CEN" przy apartamencie
When request AJAX zakończy się sukcesem
Then pojawia się modal z:
- tytułem apartamentu
- aktualną ceną brutto (bold)
- aktualną ceną m² (normal)
- tabelą historii (data | cena m² | cena brutto)
```
## AC-3: Popup zamyka się
```gherkin
Given popup jest otwarty
When użytkownik klika przycisk X lub klika poza modalem (na overlay)
Then popup znika
```
## AC-4: Stan ładowania i błędu
```gherkin
Given użytkownik kliknął "Historia cen"
When AJAX jest w trakcie
Then popup pokazuje "Ładowanie..."
And jeśli AJAX zwróci błąd, popup pokazuje "Brak danych"
```
## AC-5: Historia jest pusta
```gherkin
Given apartament nie ma jeszcze rekordów w tabeli historii
When użytkownik otworzy popup
Then widoczna jest sekcja z aktualną ceną, a tabela historii jest pusta lub pokazuje komunikat "Brak historii cen"
```
</acceptance_criteria>
<tasks>
<task type="auto">
<name>Task 1: Dodaj data-post-id do przycisku i popup HTML do widgetu</name>
<files>wp-content/plugins/elementor-addon/widgets/apartaments.php</files>
<action>
**Zmiana 1: Wiersz "HISTORIA CEN"**
Znajdź wiersz z klasą `apartament-card__price-history` (linia ~157).
Zmień `<td class="apartament-card__info_table-value">` na:
```php
<td class="apartament-card__info_table-value btn-historia-cen"
data-post-id="<?php echo esc_attr( get_the_ID() ); ?>">
```
Reszta wiersza (tekst "HISTORIA CEN" + SVG) pozostaje bez zmian.
**Zmiana 2: Globalny popup HTML**
Dodaj PRZED `<?php wp_reset_postdata(); ?>` (czyli po zamknięciu pętli while)
następujący HTML popupa (jeden egzemplarz dla całej strony):
```php
<?php // Popup historia cen — jeden globalny, wypełniany przez JS ?>
<div class="price-history-overlay" id="price-history-overlay" aria-hidden="true">
<div class="price-history-modal" role="dialog" aria-modal="true">
<button class="price-history-modal__close" id="price-history-close" aria-label="Zamknij">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14" fill="none">
<path d="M13 1L1 13M1 1L13 13" stroke="#192C44" stroke-width="2" stroke-linecap="round"/>
</svg>
</button>
<h3 class="price-history-modal__title" id="price-history-title"></h3>
<div class="price-history-modal__current">
<div class="price-history-modal__row price-history-modal__row--bold">
<span>Cena brutto:</span>
<span class="price-history-modal__val" id="price-history-price"></span>
</div>
<div class="price-history-modal__row">
<span>Cena m<sup>2</sup>:</span>
<span class="price-history-modal__val" id="price-history-price-m2"></span>
</div>
</div>
<div class="price-history-modal__table-wrap">
<table class="price-history-modal__table">
<tbody id="price-history-tbody"></tbody>
</table>
</div>
</div>
</div>
```
Unikaj: dodawania popup wewnątrz pętli while — tylko jeden egzemplarz na stronę.
</action>
<verify>
Sprawdź PHP syntax: `php -l wp-content/plugins/elementor-addon/widgets/apartaments.php`
Sprawdź czy data-post-id jest w HTML widgetu przez DevTools lub view-source.
</verify>
<done>AC-1 spełnione: przycisk ma data-post-id; popup HTML istnieje w DOM</done>
</task>
<task type="auto">
<name>Task 2: CSS popup — main.scss i main.css</name>
<files>
wp-content/plugins/elementor-addon/assets/css/main.scss,
wp-content/plugins/elementor-addon/assets/css/main.css
</files>
<action>
Dodaj na końcu pliku `main.scss` (po zamknięciu `.apartaments { }`) poniższe style.
Następnie dodaj **te same skompilowane style** (bez SCSS zagnieżdżeń, rozwinięte)
na końcu pliku `main.css`.
**Style do dodania w main.scss:**
```scss
// Historia cen — popup overlay
.price-history-overlay {
display: none;
position: fixed;
inset: 0;
z-index: 99999;
background: rgba(25, 44, 68, 0.55);
align-items: center;
justify-content: center;
padding: 20px;
&.is-open {
display: flex;
}
}
.price-history-modal {
position: relative;
background: #fff;
border: 4px solid #192c44;
padding: 32px 36px 28px;
max-width: 560px;
width: 100%;
max-height: 80vh;
overflow-y: auto;
font-family: 'Barlow', sans-serif;
color: #192c44;
@media (max-width: 600px) {
padding: 24px 20px 20px;
}
&__close {
position: absolute;
top: 14px;
right: 14px;
background: none;
border: 2px solid #192c44;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
padding: 0;
line-height: 1;
}
&__title {
font-size: 22px;
font-weight: 700;
margin: 0 0 18px;
padding-right: 40px;
color: #192c44;
}
&__current {
margin-bottom: 16px;
}
&__row {
display: flex;
justify-content: space-between;
font-size: 18px;
line-height: 1.5;
color: #192c44;
&--bold {
font-weight: 700;
}
}
&__val {
text-align: right;
}
&__table-wrap {
border-top: 1px solid #192c44;
padding-top: 12px;
margin-top: 4px;
}
&__table {
width: 100%;
border-collapse: collapse;
tr {
border: none;
background: transparent;
td {
padding: 4px 0;
font-size: 15px;
color: #192c44;
font-family: 'Barlow', sans-serif;
font-weight: 400;
border: none;
background: transparent;
&:last-child {
text-align: right;
}
&:nth-child(2) {
text-align: center;
}
}
}
}
}
```
**Skompilowana wersja CSS do dodania na końcu main.css** (rozwinięte selektory, bez &):
```css
/* Historia cen — popup */
.price-history-overlay {
display: none;
position: fixed;
inset: 0;
z-index: 99999;
background: rgba(25, 44, 68, 0.55);
align-items: center;
justify-content: center;
padding: 20px;
}
.price-history-overlay.is-open {
display: flex;
}
.price-history-modal {
position: relative;
background: #fff;
border: 4px solid #192c44;
padding: 32px 36px 28px;
max-width: 560px;
width: 100%;
max-height: 80vh;
overflow-y: auto;
font-family: 'Barlow', sans-serif;
color: #192c44;
}
@media (max-width: 600px) {
.price-history-modal {
padding: 24px 20px 20px;
}
}
.price-history-modal__close {
position: absolute;
top: 14px;
right: 14px;
background: none;
border: 2px solid #192c44;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
padding: 0;
line-height: 1;
}
.price-history-modal__title {
font-size: 22px;
font-weight: 700;
margin: 0 0 18px;
padding-right: 40px;
color: #192c44;
}
.price-history-modal__current {
margin-bottom: 16px;
}
.price-history-modal__row {
display: flex;
justify-content: space-between;
font-size: 18px;
line-height: 1.5;
color: #192c44;
}
.price-history-modal__row--bold {
font-weight: 700;
}
.price-history-modal__val {
text-align: right;
}
.price-history-modal__table-wrap {
border-top: 1px solid #192c44;
padding-top: 12px;
margin-top: 4px;
}
.price-history-modal__table {
width: 100%;
border-collapse: collapse;
}
.price-history-modal__table tr {
border: none;
background: transparent;
}
.price-history-modal__table td {
padding: 4px 0;
font-size: 15px;
color: #192c44;
font-family: 'Barlow', sans-serif;
font-weight: 400;
border: none;
background: transparent;
}
.price-history-modal__table td:last-child {
text-align: right;
}
.price-history-modal__table td:nth-child(2) {
text-align: center;
}
```
Unikaj: modyfikowania istniejących reguł CSS — tylko append na końcu.
Unikaj: zmian w mapie .css.map — pozostaw jak jest.
</action>
<verify>
Sprawdź że klasy `.price-history-overlay` i `.price-history-modal` są obecne w main.css.
Sprawdź że nie ma błędów składni SCSS (ręczna inspekcja nawiasów).
</verify>
<done>AC-2, AC-3 (style): modal wygląda zgodnie z projektem</done>
</task>
<task type="auto">
<name>Task 3: JS — obsługa kliknięcia, AJAX, render popupa</name>
<files>wp-content/plugins/elementor-addon/assets/js/main.js</files>
<action>
Dodaj na końcu pliku `main.js` (po zamknięciu istniejącego DOMContentLoaded)
nowy blok obsługi historii cen.
**Kod do dodania:**
```javascript
// Historia cen
document.addEventListener('DOMContentLoaded', function () {
var overlay = document.getElementById('price-history-overlay');
var closeBtn = document.getElementById('price-history-close');
if (!overlay) return; // popup nie istnieje na tej stronie
function openPopup() {
overlay.setAttribute('aria-hidden', 'false');
overlay.classList.add('is-open');
document.body.style.overflow = 'hidden';
}
function closePopup() {
overlay.setAttribute('aria-hidden', 'true');
overlay.classList.remove('is-open');
document.body.style.overflow = '';
}
// Zamknij przyciskiem X
closeBtn.addEventListener('click', closePopup);
// Zamknij klikając na overlay (poza modalem)
overlay.addEventListener('click', function (e) {
if (e.target === overlay) closePopup();
});
// Zamknij klawiszem Escape
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && overlay.classList.contains('is-open')) closePopup();
});
// Kliknięcie w przycisk Historia cen
document.querySelectorAll('.btn-historia-cen').forEach(function (btn) {
btn.addEventListener('click', function () {
var postId = this.dataset.postId;
if (!postId) return;
// Reset i pokaż "Ładowanie..."
document.getElementById('price-history-title').textContent = 'Ładowanie...';
document.getElementById('price-history-price').textContent = '';
document.getElementById('price-history-price-m2').textContent = '';
document.getElementById('price-history-tbody').innerHTML = '';
openPopup();
// Sprawdź dostępność danych globalnych (wp_localize_script)
if (typeof apartamentsData === 'undefined') {
document.getElementById('price-history-title').textContent = 'Błąd konfiguracji';
return;
}
// Buduj FormData
var formData = new FormData();
formData.append('action', 'apartamenty_get_price_history');
formData.append('post_id', postId);
formData.append('nonce', apartamentsData.nonce);
fetch(apartamentsData.ajaxUrl, {
method: 'POST',
body: formData,
credentials: 'same-origin',
})
.then(function (res) { return res.json(); })
.then(function (json) {
if (!json.success) {
document.getElementById('price-history-title').textContent = 'Brak danych';
return;
}
var d = json.data;
document.getElementById('price-history-title').textContent =
d.title || '';
document.getElementById('price-history-price').textContent =
d.price ? d.price + ' zł' : '—';
document.getElementById('price-history-price-m2').textContent =
d.price_m2 ? d.price_m2 + ' zł' : '—';
var tbody = document.getElementById('price-history-tbody');
if (!d.history || d.history.length === 0) {
tbody.innerHTML = '<tr><td colspan="3">Brak historii cen</td></tr>';
return;
}
tbody.innerHTML = d.history.map(function (row) {
return '<tr>' +
'<td>' + (row.recorded_at || '') + '</td>' +
'<td>' + (row.price_m2 ? row.price_m2 + ' zł/m²' : '—') + '</td>' +
'<td>' + (row.price ? row.price + ' zł' : '—') + '</td>' +
'</tr>';
}).join('');
})
.catch(function () {
document.getElementById('price-history-title').textContent = 'Błąd ładowania';
});
});
});
});
```
Unikaj: modyfikowania istniejącego bloku DOMContentLoaded (Swiper/Fancybox) — tylko append.
Unikaj: jQuery — używaj vanilla JS zgodnie z istniejącym stylem kodu.
</action>
<verify>
`php -l` nie jest tu potrzebny — sprawdź składnię JS ręcznie (nawiasy, cudzysłowy).
Po wgraniu: otwórz stronę /apartamenty/, otwórz DevTools Console,
kliknij "HISTORIA CEN" → popup powinien się pojawić z danymi lub "Ładowanie...".
</verify>
<done>AC-2, AC-3, AC-4, AC-5 spełnione: popup otwiera się z danymi AJAX, zamyka się poprawnie</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<what-built>
Popup „Historia cen" — kliknięcie przycisku otwiera modal z danymi z AJAX.
Przycisk ma data-post-id, popup zamyka się przez X / klik overlay / Escape.
</what-built>
<how-to-verify>
1. Odwiedź: https://wyszynskiego12.pagedev.pl/apartamenty/
2. Odszukaj wiersz „HISTORIA CEN" przy dowolnym apartamencie i kliknij go
3. Sprawdź czy pojawia się modal z:
- Tytułem apartamentu (np. „Apartament 15")
- Ceną brutto (bold)
- Ceną m² (normal)
- Tabelą historii (lub „Brak historii cen" jeśli tabela pusta)
4. Kliknij X → popup zamknięty
5. Kliknij poza modalem → popup zamknięty
6. Naciśnij Escape → popup zamknięty
7. Otwórz DevTools → Console → brak błędów JavaScript
</how-to-verify>
<resume-signal>Wpisz "zatwierdzone" lub opisz problemy do poprawienia</resume-signal>
</task>
</tasks>
<boundaries>
## DO NOT CHANGE
- wp-content/plugins/elementor-addon/elementor-addon.php (ukończony w planie 01-01)
- wp-content/plugins/elementor-addon/plugins/ (biblioteki zewnętrzne)
- wp-config.php
## SCOPE LIMITS
- Nie dodawaj animacji CSS poza prostym display:flex/none
- Nie dodawaj nowych zależności JS (bez jQuery, bez bibliotek)
- Popup ma jeden egzemplarz na stronę — nie w pętli
- Nie modyfikuj istniejących reguł CSS — tylko append
</boundaries>
<verification>
Before declaring plan complete:
- [ ] PHP syntax OK: `php -l widgets/apartaments.php`
- [ ] Klasa `.price-history-overlay` istnieje w main.css
- [ ] Klasa `.btn-historia-cen` jest na wierszu HISTORIA CEN w HTML
- [ ] `data-post-id` jest ustawiony poprawnie dla każdego apartamentu
- [ ] JS nie wyrzuca błędów w konsoli
- [ ] Checkpoint human-verify zatwierdzony
</verification>
<success_criteria>
- Kliknięcie "HISTORIA CEN" otwiera popup z danymi apartamentu
- Popup zamyka się przez X, klik overlay i Escape
- Stan ładowania i błędu jest obsługiwany
- Brak błędów JS w konsoli
- Wygląd zgodny z projektem (font Barlow, kolor #192c44, border 4px)
</success_criteria>
<output>
Po ukończeniu utwórz `.paul/phases/01-historia-cen/01-02-SUMMARY.md`
</output>

View File

@@ -1,118 +0,0 @@
---
phase: 01-historia-cen
plan: 02
subsystem: ui
tags: [popup, modal, ajax, vanilla-js, scss, css, widget]
requires:
- phase: 01-historia-cen
plan: 01
provides: AJAX endpoint apartamenty_get_price_history + wp_localize_script (apartamentsData)
provides:
- popup „Historia cen" z danymi AJAX na stronie /apartamenty/
- przycisk .btn-historia-cen z data-post-id w widgecie
- style .price-history-modal / .price-history-overlay w main.scss + main.css
- JS handler: fetch AJAX → render modal → open/close
affects: []
tech-stack:
added: []
patterns:
- Globalny popup (jeden egzemplarz poza pętlą while) wypełniany przez JS
- Vanilla JS fetch + FormData zamiast jQuery AJAX
- CSS class toggle (is-open) zamiast display inline
key-files:
modified:
- wp-content/plugins/elementor-addon/widgets/apartaments.php
- wp-content/plugins/elementor-addon/assets/css/main.scss
- wp-content/plugins/elementor-addon/assets/css/main.css
- wp-content/plugins/elementor-addon/assets/js/main.js
key-decisions:
- "ID price-history-sqm zamiast price-history-price-m2 — spójna zmiana w HTML i JS"
- "max-height + overflow-y:auto na .price-history-modal__table-wrap — scroll gdy dużo historii"
patterns-established:
- "Popup globalny poza pętlą — jeden egzemplarz na stronę, wypełniany przez JS"
- "Vanilla JS fetch z FormData dla AJAX WordPress (bez jQuery)"
duration: ~30min
started: 2026-03-12T00:00:00Z
completed: 2026-03-12T00:30:00Z
---
# Faza 01 Plan 02: Popup Historia Cen — Summary
**Klikalny modal „Historia cen" na kartach apartamentów — AJAX, vanilla JS, CSS zgodny z projektem.**
## Performance
| Metric | Value |
|--------|-------|
| Duration | ~30 min |
| Started | 2026-03-12 |
| Completed | 2026-03-12 |
| Tasks | 3 auto + 1 checkpoint |
| Files modified | 4 |
## Acceptance Criteria Results
| Criterion | Status | Notes |
|-----------|--------|-------|
| AC-1: Przycisk klikalny z data-post-id | Pass | `.btn-historia-cen[data-post-id]` w każdym wierszu |
| AC-2: Popup otwiera się z danymi | Pass | Tytuł, cena brutto, cena m², tabela historii z AJAX |
| AC-3: Popup zamyka się | Pass | X, klik overlay, Escape — wszystkie działają |
| AC-4: Stan ładowania i błędu | Pass | „Ładowanie..." przy otwarciu, „Brak danych" / „Błąd ładowania" przy problemach |
| AC-5: Pusta historia | Pass | `<td colspan="3">Brak historii cen</td>` gdy historia pusta |
## Accomplishments
- Modal popup bez bibliotek zewnętrznych — vanilla JS + CSS
- Jeden egzemplarz popupa na stronę (poza pętlą `while`) — poprawna architektura
- Scroll w tabeli historii (`max-height: 40vh; overflow-y: auto`) gdy dużo wpisów
- Stan ładowania i pełna obsługa błędów AJAX
## Files Created/Modified
| File | Change | Purpose |
|------|--------|---------|
| `widgets/apartaments.php` | Modified | Dodano `.btn-historia-cen` + `data-post-id` + popup HTML |
| `assets/css/main.scss` | Modified | Dodano style `.price-history-overlay` i `.price-history-modal` |
| `assets/css/main.css` | Modified | Skompilowana wersja CSS (append) |
| `assets/js/main.js` | Modified | Dodano obsługę AJAX + open/close popupa |
## Decisions Made
| Decision | Rationale | Impact |
|----------|-----------|--------|
| ID `price-history-sqm` zamiast `price-history-price-m2` | Krótsza nazwa, spójna w HTML i JS | Brak wpływu na działanie |
| `max-height` + `overflow-y:auto` na tabeli | Ochrona przed bardzo długą historią cen | Lepsze UX przy wielu wpisach |
## Deviations from Plan
| Type | Opis | Impact |
|------|------|--------|
| Auto-fixed | ID `price-history-price-m2``price-history-sqm` | Kosmetyczna, HTML i JS spójne |
## Issues Encountered
None.
## Next Phase Readiness
**Ready:**
- Faza 01 kompletna — oba plany (01-01 i 01-02) ukończone
- Milestone v0.1 ma ukończone obie fazy (01 i 02)
**Concerns:**
- None
**Blockers:**
- None
---
*Phase: 01-historia-cen, Plan: 02*
*Completed: 2026-03-12*

View File

@@ -1,299 +0,0 @@
---
phase: 02-jawnosc-cen
plan: 01
type: execute
wave: 2
depends_on: ["01-01"]
files_modified:
- wp-content/plugins/elementor-addon/elementor-addon.php
autonomous: false
---
<objective>
## Goal
Zbudować publiczny XML endpoint z danymi cenowymi apartamentów oraz XML katalog zgodny z XSD portalu dane.gov.pl — gotowy do automatycznego zasilania Ministerstwa.
## Purpose
Ustawa o jawności cen zobowiązuje deweloperów do raportowania cen mieszkań do portalu rządowego dane.gov.pl. Deweloper musi podać Ministerstwu URL pliku XML, który portal będzie cyklicznie pobierał. Budujemy te endpointy po stronie WordPress, korzystając z istniejącej tabeli `wp_price_history` i pól ACF.
## Output
- `/ceny-mieszkan.xml` — plik z cenami wszystkich lokali + historia z DB
- `/ceny-mieszkan.md5` — hash MD5 dla powyższego
- `/dane-gov-pl.xml` — katalog XSD-compliant dla dane.gov.pl wskazujący na plik cen
- `/dane-gov-pl.md5` — hash MD5 dla katalogu
- Strona administracyjna z URL-ami do zgłoszenia do Ministerstwa
</objective>
<context>
## Project Context
@.paul/PROJECT.md
@.paul/ROADMAP.md
@.paul/STATE.md
## Prior Work
@.paul/phases/01-historia-cen/01-01-SUMMARY.md
## Source Files
@wp-content/plugins/elementor-addon/elementor-addon.php
</context>
<acceptance_criteria>
## AC-1: Endpoint cen mieszkań dostępny
```gherkin
Given WordPress jest uruchomiony z przepłukanymi regułami permalink
When GET /ceny-mieszkan.xml
Then odpowiedź HTTP 200 z Content-Type: application/xml,
XML zawiera element <lokale> z listą <lokal> dla każdego posta apartamenty,
każdy <lokal> ma: id, nazwa, typ, pietro, powierzchnia, cena_brutto, cena_za_m2, historia_cen
```
## AC-2: MD5 companions poprawne
```gherkin
Given pliki XML są serwowane przez WordPress
When GET /ceny-mieszkan.md5 lub /dane-gov-pl.md5
Then odpowiedź HTTP 200 z 32-znakowym lowercase hex stringiem (MD5 odpowiadającego XML)
```
## AC-3: Katalog dane.gov.pl zgodny z XSD
```gherkin
Given endpoint /dane-gov-pl.xml działa
When GET /dane-gov-pl.xml
Then XML zawiera element <datasets> z <dataset> opisującym inwestycję,
<resource> wskazuje na home_url('/ceny-mieszkan.xml'),
updateFrequency to "daily", kategoria to "REGI"
```
## AC-4: Strona administracyjna z URL-ami
```gherkin
Given admin jest zalogowany
When odwiedzi Narzędzia Jawność Cen (wp-admin/tools.php?page=jawnosc-cen)
Then widzi oba URL-e do zgłoszenia (ceny-mieszkan.xml i dane-gov-pl.xml),
przyciski "Kopiuj URL" i "Otwórz XML",
informację o harmonogramie (codziennie)
```
</acceptance_criteria>
<tasks>
<task type="auto">
<name>Task 1: Rewrite rules + endpoint cen mieszkań (XML + MD5)</name>
<files>wp-content/plugins/elementor-addon/elementor-addon.php</files>
<action>
Dodaj na końcu pliku (po istniejących hookach) następujące funkcje:
**1. Rewrite rules (hook: init, priority 10):**
```
add_rewrite_rule('^ceny-mieszkan\.(xml|md5)$', 'index.php?apartamenty_xml=$matches[1]', 'top');
add_rewrite_rule('^dane-gov-pl\.(xml|md5)$', 'index.php?apartamenty_datagov=$matches[1]', 'top');
```
Dodaj query vars: 'apartamenty_xml', 'apartamenty_datagov' przez filter 'query_vars'.
**2. Generator XML cen (funkcja apartamenty_generate_price_xml()):**
- Sprawdź transient 'apartamenty_price_xml_cache' (TTL 1h) — jeśli istnieje, zwróć go
- WP_Query: post_type=apartamenty, posts_per_page=-1, orderby=title, order=ASC
- Dla każdego postu:
- Pobierz: post ID, post_title
- get_post_meta flat: information_type, information_floor, information_floor_space, information_price, information_price_m2, information_status
- Pobierz historię z wp_price_history: SELECT recorded_at, price, price_m2 WHERE post_id = %d ORDER BY recorded_at DESC
- Buduj XML jako string (nie DOMDocument — nie ma pewności że rozszerzenie jest dostępne, użyj SimpleXMLElement lub czystego PHP string z esc_xml/htmlspecialchars):
```xml
<?xml version="1.0" encoding="UTF-8"?>
<lokale inwestycja="Wyszyńskiego 12" generowany="[date('c')]">
<lokal id="[post_id]">
<nazwa>[post_title]</nazwa>
<typ>[information_type]</typ>
<pietro>[information_floor]</pietro>
<powierzchnia>[information_floor_space]</powierzchnia>
<status>[information_status]</status>
<cena_brutto>[information_price]</cena_brutto>
<cena_za_m2>[information_price_m2]</cena_za_m2>
<data_aktualizacji>[max recorded_at from history or today]</data_aktualizacji>
<historia_cen>
<zmiana data="[recorded_at]">
<cena_brutto>[price]</cena_brutto>
<cena_za_m2>[price_m2]</cena_za_m2>
</zmiana>
...
</historia_cen>
</lokal>
</lokale>
```
- Zapisz do transientu i zwróć
- Unika: nie używaj DOMDocument jeśli brak ext-dom, używaj SimpleXMLElement lub czystych stringów z htmlspecialchars(value, ENT_XML1, 'UTF-8')
**3. Endpoint handler (hook: template_redirect, priority 1):**
- get_query_var('apartamenty_xml') == 'xml' → header + echo apartamenty_generate_price_xml() + exit
- get_query_var('apartamenty_xml') == 'md5' → header Content-Type text/plain + echo md5(apartamenty_generate_price_xml()) + exit
**4. Inwalidacja cache w istniejącym cronie apartamenty_record_prices:**
Dodaj na początku funkcji crona: delete_transient('apartamenty_price_xml_cache');
Avoid: nie dodawaj flush_rewrite_rules() w runtime (tylko przy aktywacji); nie generuj XML przez DOMDocument bez sprawdzenia extension_loaded('dom').
</action>
<verify>
1. Przejdź do Ustawienia → Bezpośrednie odnośniki → Zapisz (flush rewrite rules)
2. Otwórz w przeglądarce: [site_url]/ceny-mieszkan.xml
3. Sprawdź: Content-Type w nagłówkach = application/xml, XML jest poprawny (widoczna struktura w przeglądarce)
4. Otwórz: [site_url]/ceny-mieszkan.md5
5. Sprawdź: 32-znakowy lowercase hex string
</verify>
<done>AC-1 i AC-2 (część ceny) spełnione: endpoint XML cen dostępny, MD5 poprawny</done>
</task>
<task type="auto">
<name>Task 2: Endpoint katalogu dane.gov.pl (XML + MD5)</name>
<files>wp-content/plugins/elementor-addon/elementor-addon.php</files>
<action>
Dodaj funkcję apartamenty_generate_datagov_xml():
Generuje XML zgodny z XSD https://www.dane.gov.pl/static/xml/otwarte_dane_latest.xsd:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<datasets xmlns:xsi="urn:otwarte-dane:harvester:1.0-rc1">
<dataset status="published">
<extIdent>wyszynskiego12-ceny-mieszkan-v1</extIdent>
<title>
<polish>Ceny mieszkań Wyszyńskiego 12</polish>
</title>
<description>
<polish>Historia cen lokali mieszkalnych w inwestycji przy ul. Wyszyńskiego 12. Dane aktualizowane codziennie zgodnie z ustawą o jawności cen.</polish>
</description>
<updateFrequency>daily</updateFrequency>
<categories>REGI</categories>
<resources>
<resource status="published">
<extIdent>wyszynskiego12-ceny-xml-v1</extIdent>
<url>[home_url('/ceny-mieszkan.xml')]</url>
<title>
<polish>Cennik lokali XML</polish>
</title>
<description>
<polish>Plik XML z aktualnym cennikiem i historią zmian cen wszystkich lokali</polish>
</description>
<availability>remote</availability>
<lastUpdateDate>[date('Y-m-d\T00:00:00.000\Z')]</lastUpdateDate>
</resource>
</resources>
<tags lang="pl">
<tag>mieszkania</tag>
<tag>ceny</tag>
<tag>deweloper</tag>
<tag>jawność cen</tag>
<tag>historia cen</tag>
</tags>
</dataset>
</datasets>
```
Użyj home_url() dla URL-a zasobu (nie hardcode).
Uzupełnij template_redirect handler:
- get_query_var('apartamenty_datagov') == 'xml' → header + echo apartamenty_generate_datagov_xml() + exit
- get_query_var('apartamenty_datagov') == 'md5' → header text/plain + echo md5(apartamenty_generate_datagov_xml()) + exit
Avoid: nie używaj hardcoded URL-a strony; nie pomijaj pola `categories` (wymagane przez XSD).
</action>
<verify>
1. Otwórz: [site_url]/dane-gov-pl.xml
2. Sprawdź: Content-Type application/xml, struktura XML poprawna, URL w <resource> wskazuje na /ceny-mieszkan.xml
3. Otwórz: [site_url]/dane-gov-pl.md5
4. Sprawdź: 32-znakowy lowercase hex string
</verify>
<done>AC-2 (komplet) i AC-3 spełnione: oba endpointy XML + oba MD5 działają</done>
</task>
<task type="auto">
<name>Task 3: Strona administracyjna Jawność Cen</name>
<files>wp-content/plugins/elementor-addon/elementor-addon.php</files>
<action>
Zarejestruj stronę w menu Narzędzia WP (hook: admin_menu):
```php
add_management_page(
'Jawność Cen',
'Jawność Cen',
'manage_options',
'jawnosc-cen',
'apartamenty_jawnosc_cen_page'
);
```
Funkcja apartamenty_jawnosc_cen_page():
- Wyświetl dwa bloki (prostą tabelę HTML, bez CSS frameworków):
- Blok 1: "Plik cen (dane)": URL = home_url('/ceny-mieszkan.xml'), [przycisk kopiuj JS], [link "Otwórz"]
- Blok 2: "Katalog dane.gov.pl (zgłoś Ministerstwu)": URL = home_url('/dane-gov-pl.xml'), [przycisk kopiuj JS], [link "Otwórz"]
- Informacja tekstowa: "Dane aktualizowane codziennie przez WP Cron. Zgłoś URL katalogu dane.gov.pl do administratora portalu: kontakt@dane.gov.pl"
- Prosty `<script>` z funkcją copyToClipboard używającą navigator.clipboard.writeText
- Użyj standardowych klas WP (.wrap, .notice) dla stylu
Avoid: nie ładuj zewnętrznych skryptów/stylów; nie używaj nonce na tej stronie (tylko read-only display).
</action>
<verify>
1. W wp-admin przejdź do Narzędzia → Jawność Cen
2. Sprawdź: widoczne oba URL-e, przyciski kopiuj działają, linki "Otwórz" przekierowują poprawnie
</verify>
<done>AC-4 spełnione: strona administracyjna z URL-ami widoczna i funkcjonalna</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<what-built>
Trzy endpointy publiczne i strona admina:
- /ceny-mieszkan.xml — dane cen apartamentów (XML)
- /ceny-mieszkan.md5 — hash MD5
- /dane-gov-pl.xml — katalog dane.gov.pl
- /dane-gov-pl.md5 — hash MD5
- wp-admin: Narzędzia → Jawność Cen
</what-built>
<how-to-verify>
1. W wp-admin: Ustawienia → Bezpośrednie odnośniki → kliknij "Zapisz" (flush rewrite rules)
2. Otwórz [site_url]/ceny-mieszkan.xml — sprawdź że XML się wyświetla z apartamentami
3. Otwórz [site_url]/ceny-mieszkan.md5 — sprawdź że widać 32-znakowy hex string
4. Otwórz [site_url]/dane-gov-pl.xml — sprawdź że XML zawiera element <datasets> z URL do ceny-mieszkan.xml
5. Otwórz [site_url]/dane-gov-pl.md5 — sprawdź 32-znakowy hex string
6. Przejdź do Narzędzia → Jawność Cen — sprawdź że URL-e są widoczne
</how-to-verify>
<resume-signal>Wpisz "approved" żeby kontynuować, lub opisz problemy do naprawienia</resume-signal>
</task>
</tasks>
<boundaries>
## DO NOT CHANGE
- wp-content/plugins/elementor-addon/widgets/apartaments.php (widget bez zmian)
- wp-content/plugins/elementor-addon/assets/ (CSS/JS bez zmian)
- Tabela wp_price_history (schemat DB bez zmian)
- Istniejąca logika crona apartamenty_record_prices (tylko dodaj delete_transient na początku)
- Istniejący AJAX endpoint apartamenty_get_price_history
## SCOPE LIMITS
- Nie rejestruj dewelopera na dane.gov.pl (to ręczna czynność)
- Nie twórz osobnego pliku XML — wszystko w elementor-addon.php
- Nie buduj walidatora XSD po stronie PHP
- Nie dodawaj formularza do edycji danych o inwestycji (tylko statyczne dane w XML katalogu)
- Nie obsługuj wielu inwestycji — tylko jedna (Wyszyńskiego 12)
</boundaries>
<verification>
Przed uznaniem planu za ukończony:
- [ ] GET /ceny-mieszkan.xml zwraca HTTP 200 z Content-Type application/xml
- [ ] XML zawiera co najmniej jeden element <lokal> z polami cena_brutto i historia_cen
- [ ] GET /ceny-mieszkan.md5 zwraca 32-znakowy lowercase hex string
- [ ] GET /dane-gov-pl.xml zwraca HTTP 200 z Content-Type application/xml
- [ ] /dane-gov-pl.xml zawiera element <resource> z URL wskazującym na /ceny-mieszkan.xml
- [ ] GET /dane-gov-pl.md5 zwraca 32-znakowy lowercase hex string
- [ ] wp-admin: Narzędzia → Jawność Cen widoczne i pokazuje URL-e
- [ ] Brak PHP errors/warnings w wp-debug.log
</verification>
<success_criteria>
- Wszystkie 3 zadania ukończone
- Checkpoint human-verify przejdzie pomyślnie
- Deweloper może skopiować URL /dane-gov-pl.xml i zgłosić go do kontakt@dane.gov.pl
- Oba XML-e dostępne publicznie bez logowania
</success_criteria>
<output>
Po ukończeniu utwórz `.paul/phases/02-jawnosc-cen/02-01-SUMMARY.md`
</output>

View File

@@ -1,116 +0,0 @@
---
phase: 02-jawnosc-cen
plan: 01
subsystem: api
tags: [xml, rewrite-rules, wp-cron, transient, admin-page, dane.gov.pl]
requires:
- phase: 01-historia-cen
provides: tabela wp_price_history + cron dzienny apartamenty_record_prices + pola ACF
provides:
- publiczny endpoint /ceny-mieszkan.xml z cenami i historią cen
- publiczny endpoint /ceny-mieszkan.md5
- publiczny endpoint /dane-gov-pl.xml (katalog XSD-compliant)
- publiczny endpoint /dane-gov-pl.md5
- strona wp-admin: Narzędzia → Jawność Cen
affects: []
tech-stack:
added: []
patterns:
- WP rewrite rules + query_vars dla custom XML endpoints
- Transient cache z inwalidacją przez cron
- XML generowany jako czysty PHP string z htmlspecialchars(ENT_XML1)
key-files:
created:
- docs/jawnosc-cen.md
modified:
- wp-content/plugins/elementor-addon/elementor-addon.php
key-decisions:
- "XML jako czysty PHP string zamiast DOMDocument — brak zależności od ext-dom"
- "Transient cache 1h — inwalidowany przez istniejący cron dzienny"
- "Wszystko w jednym pliku pluginu — zgodnie z boundaries planu"
patterns-established:
- "Endpointy XML przez add_rewrite_rule() + query_vars + template_redirect"
- "Cache przez set_transient/get_transient z delete_transient w cronie"
duration: ~30min
started: 2026-03-12T00:00:00Z
completed: 2026-03-12T00:30:00Z
---
# Faza 02 Plan 01: Jawność Cen — XML Endpoints — Summary
**Publiczne endpointy XML cen apartamentów + katalog dane.gov.pl + strona admina do zgłoszenia do Ministerstwa.**
## Performance
| Metric | Value |
|--------|-------|
| Duration | ~30 min |
| Started | 2026-03-12 |
| Completed | 2026-03-12 |
| Tasks | 3 auto + 1 checkpoint |
| Files modified | 1 (elementor-addon.php) |
| Files created | 1 (docs/jawnosc-cen.md) |
## Acceptance Criteria Results
| Criterion | Status | Notes |
|-----------|--------|-------|
| AC-1: Endpoint cen mieszkań dostępny | Pass | /ceny-mieszkan.xml zwraca HTTP 200, Content-Type: application/xml, struktura <lokale> poprawna |
| AC-2: MD5 companions poprawne | Pass | /ceny-mieszkan.md5 i /dane-gov-pl.md5 zwracają 32-znakowy lowercase hex |
| AC-3: Katalog dane.gov.pl zgodny z XSD | Pass | /dane-gov-pl.xml zawiera <datasets>, URL w <resource> wskazuje na /ceny-mieszkan.xml |
| AC-4: Strona administracyjna z URL-ami | Pass | Narzędzia → Jawność Cen widoczne, URL-e i przyciski działają |
## Accomplishments
- Cztery publiczne endpointy XML wdrożone przez WP rewrite rules — bez konfiguracji serwera
- Transient cache 1h z automatyczną inwalidacją przy cronie dziennym
- Strona admina z URL-ami gotowymi do skopiowania i zgłoszenia do Ministerstwa
- Dokumentacja klienta w `docs/jawnosc-cen.md` — co i jak zgłosić do dane.gov.pl
## Files Created/Modified
| File | Change | Purpose |
|------|--------|---------|
| `wp-content/plugins/elementor-addon/elementor-addon.php` | Modified | +250 linii: rewrite rules, XML generators, template_redirect, admin page |
| `docs/jawnosc-cen.md` | Created | Instrukcja dla klienta: co zbudowano + kroki po stronie Ministerstwa |
## Decisions Made
| Decision | Rationale | Impact |
|----------|-----------|--------|
| XML przez czysty PHP string (htmlspecialchars ENT_XML1) zamiast DOMDocument | Brak gwarancji że ext-dom jest dostępne na hostingu | Bezpieczniejsze, działa wszędzie |
| Cache transient 1h, inwalidowany przez cron | Cron i tak działa raz dziennie — cache nie ma sensu trzymać dłużej niż 1h w razie ręcznych zmian | Balans między wydajnością a świeżością danych |
| Wszystko w elementor-addon.php | Zgodnie z boundaries — nie tworzyć osobnych plików | Mniej plików do zarządzania |
## Deviations from Plan
None — plan wykonany dokładnie zgodnie ze specyfikacją.
## Issues Encountered
None.
## Next Phase Readiness
**Ready:**
- Faza 02 kompletna — oba plany tej fazy nie istnieją (był tylko 02-01)
- Milestone v0.1 czeka na: plan 01-02 (widget HTML + CSS popup + JS handler)
**Concerns:**
- Plan 01-02 z fazy 01-historia-cen wciąż oczekuje na APPLY — należy go wykonać
**Blockers:**
- None dla fazy 02
- Plan 01-02 (faza 01) nadal nie wykonany — jeśli popup historii cen jest wymagany do v0.1
---
*Phase: 02-jawnosc-cen, Plan: 01*
*Completed: 2026-03-12*

View File

@@ -1,256 +0,0 @@
---
phase: 03-miejsca-postojowe
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- wp-content/plugins/elementor-addon/elementor-addon.php
- wp-content/plugins/elementor-addon/widgets/parking-spots.php
- wp-content/plugins/elementor-addon/assets/js/main.js
- wp-content/plugins/elementor-addon/assets/css/main.css
autonomous: false
---
<objective>
## Goal
Stworzyc widget Elementor "Miejsca postojowe" wyswietlajacy ceny dwoch typow miejsc (zwykle i rodzinne) z przyciskiem "Historia cen" oraz popup, a takze rozszerzyc cron, AJAX i XML export o dane parkingowe.
## Purpose
Deweloper musi prezentowac ceny miejsc postojowych na stronie oferty oraz raportowac je do dane.gov.pl zgodnie z ustawa o jawnosci cen.
## Output
- Nowy widget Elementor: `Elementor_Parking_Spots`
- Tabela DB: `wp_parking_price_history`
- Rozszerzony cron o zapis cen parkingowych
- Rozszerzony AJAX o historie cen parkingowych
- Rozszerzony XML export o miejsca postojowe
</objective>
<context>
## Project Context
@.paul/PROJECT.md
@.paul/ROADMAP.md
@.paul/STATE.md
## Source Files
@wp-content/plugins/elementor-addon/elementor-addon.php
@wp-content/plugins/elementor-addon/widgets/apartaments.php
@wp-content/plugins/elementor-addon/assets/js/main.js
@wp-content/plugins/elementor-addon/assets/css/main.css
## Existing Patterns
- Apartament widget: klasa Elementor_Apartaments w widgets/apartaments.php
- Historia cen popup: globalny overlay #price-history-overlay, wypelniany przez AJAX
- AJAX endpoint: wp_ajax_apartamenty_get_price_history → JSON {title, price, price_m2, history}
- Cron: apartamenty_record_prices → INSERT IGNORE do wp_price_history
- XML: apartamenty_generate_price_xml() → <lokale> z <lokal> per apartament
- ACF: parking prices sa w opcjach ACF (options page), klucze do odkrycia w DB
- Garage map: get_field('garage_spots', 'option') → parking-N → status
</context>
<skills>
No specialized flows configured.
</skills>
<acceptance_criteria>
## AC-1: Widget wyswietla ceny parkingowe
```gherkin
Given widget "Miejsca postojowe" jest dodany na stronie w Elementorze
When uzytkownik otwiera strone
Then widzi sekcje z dwoma typami: "Miejsce postojowe zwykle" i "Miejsce postojowe Rodzinne"
And kazdy typ ma wyswietlona aktualna cene z ACF
And kazdy typ ma przycisk "HISTORIA CEN"
```
## AC-2: Popup historia cen dziala dla miejsc postojowych
```gherkin
Given uzytkownik widzi widget miejsc postojowych
When klika przycisk "HISTORIA CEN" przy dowolnym typie
Then otwiera sie popup z aktualna cena i tabela historii zmian cen
And popup zamyka sie przyciskiem X, klikajac overlay lub klawiszem Escape
```
## AC-3: Cron zapisuje ceny parkingowe codziennie
```gherkin
Given WP Cron jest zaplanowany
When uruchamia sie dzienne zadanie
Then aktualne ceny obu typow miejsc postojowych sa zapisywane do wp_parking_price_history
And zapis jest idempotentny (INSERT IGNORE, jeden rekord na typ na dzien)
```
## AC-4: XML export zawiera miejsca postojowe
```gherkin
Given endpoint /ceny-mieszkan.xml jest dostepny
When uzytkownik pobiera XML
Then XML zawiera sekcje <miejsca_postojowe> z oboma typami
And kazdy typ ma aktualna cene i historie zmian
```
</acceptance_criteria>
<tasks>
<task type="auto">
<name>Task 1: Odkryj klucze ACF dla cen parkingowych i stworz tabele DB + cron + AJAX</name>
<files>wp-content/plugins/elementor-addon/elementor-addon.php</files>
<action>
1. **Odkryj ACF meta keys** — sprawdz w bazie (wp_options) jakie klucze ACF sa uzywane dla cen miejsc postojowych. Prawdopodobnie sa w opcjach ACF (get_field z 'option'). Poszukaj kluczy zawierajacych "parking", "postojow", "garage_price" lub podobnych.
2. **Stworz tabele wp_parking_price_history:**
```sql
CREATE TABLE wp_parking_price_history (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
parking_type VARCHAR(50) NOT NULL, -- 'zwykle' lub 'rodzinne'
price VARCHAR(50) NOT NULL DEFAULT '',
recorded_at DATE NOT NULL,
UNIQUE KEY unique_daily (parking_type, recorded_at),
KEY idx_type (parking_type)
)
```
- Dodaj funkcje tworzaca tabele analogicznie do elementor_addon_create_price_history_table()
- Zaktualizuj elementor_addon_maybe_update_db() — zmien wersje DB na '1.1'
3. **Rozszerz cron** — w elementor_addon_record_prices() dodaj po petli apartamentow:
- Pobierz ceny parkingowe z ACF options (odkryte klucze)
- INSERT IGNORE do wp_parking_price_history dla kazdego typu
4. **Dodaj AJAX endpoint** — nowa funkcja `elementor_addon_get_parking_price_history_ajax()`:
- Akcja: wp_ajax_parking_get_price_history / wp_ajax_nopriv_parking_get_price_history
- Parametr: parking_type ('zwykle' lub 'rodzinne')
- Zwraca: JSON {type_label, price, history: [{recorded_at, price}]}
- Uzywa tego samego nonce co apartamenty
5. **Rozszerz XML** — w apartamenty_generate_price_xml() po zamknieciu </lokale>... nie, lepiej:
- Zmien root element na <nieruchomosci> opakowujacy <lokale> i nowy <miejsca_postojowe>
- LUB dodaj <miejsca_postojowe> wewnatrz <lokale> (prostsze)
- Kazdy typ: <miejsce_postojowe typ="zwykle">, <cena>, <historia_cen>
- Wyczysc transient po aktualizacji
Avoid:
- NIE zmieniaj istniejacego schematu wp_price_history (apartamenty)
- NIE usuwaj istniejacych endpointow XML — rozszerzaj
</action>
<verify>
- Tabela wp_parking_price_history istnieje po aktywacji/upgrade
- Reczne wywolanie crona zapisuje rekordy parkingowe
- AJAX endpoint zwraca poprawny JSON
- XML zawiera sekcje miejsca_postojowe
</verify>
<done>AC-3 i AC-4 satisfied: cron zapisuje ceny, XML eksportuje miejsca postojowe</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<what-built>Backend: tabela DB, cron, AJAX endpoint, XML export dla miejsc postojowych</what-built>
<how-to-verify>
1. Sprawdz w wp-admin > Narzedzia > Jawnosc Cen czy XML zawiera miejsca postojowe
2. Otworz /ceny-mieszkan.xml w przegladarce i sprawdz sekcje miejsca_postojowe
3. Potwierdz ze ACF klucze zostaly poprawnie odkryte
</how-to-verify>
<resume-signal>Type "approved" to continue to widget, or describe issues</resume-signal>
</task>
<task type="auto">
<name>Task 2: Stworz widget Elementor "Miejsca postojowe" z przyciskiem historia cen</name>
<files>
wp-content/plugins/elementor-addon/widgets/parking-spots.php,
wp-content/plugins/elementor-addon/assets/js/main.js,
wp-content/plugins/elementor-addon/assets/css/main.css,
wp-content/plugins/elementor-addon/elementor-addon.php
</files>
<action>
1. **Stworz widgets/parking-spots.php** — klasa Elementor_Parking_Spots:
- get_name() → 'parking_spots'
- get_title() → 'Miejsca Postojowe'
- get_style_depends / get_script_depends → te same co apartaments
- render():
a) Pobierz ceny z ACF options (odkryte klucze)
b) Wyrenderuj dwie karty: "Miejsce postojowe zwykle" i "Miejsce postojowe Rodzinne"
c) Kazda karta: nazwa typu, cena, przycisk "HISTORIA CEN" z data-parking-type="zwykle/rodzinne"
d) Uzyj stylistyki zblizonej do apartament-card__info (tabela z cenami)
e) Wyrenderuj ten sam popup overlay co apartamenty (reuse #price-history-overlay)
— popup juz istnieje globalnie, wiec NIE renderuj go ponownie jesli jest na tej samej stronie
— dodaj warunek: renderuj popup tylko jesli nie ma go jeszcze w DOM
2. **Zarejestruj widget** w elementor-addon.php:
- W register_hello_world_widget() dodaj require + register dla Elementor_Parking_Spots
3. **Rozszerz main.js** — dodaj obsluge klikniecia .btn-parking-historia-cen:
- Analogicznie do .btn-historia-cen ale wysyla parking_type zamiast post_id
- Uzywa akcji 'parking_get_price_history'
- Wypelnia ten sam popup (reuse)
- Tabela historii: data, cena (bez ceny/m2 — uproszczona)
4. **Rozszerz main.css** — dodaj style dla widgetu parkingowego:
- Klasa .parking-spots z kartami
- Styl zbliżony do apartament-card ale uproszczony (bez galerii, dokumentow)
- Responsive
Avoid:
- NIE duplikuj popupa historia cen — uzyj istniejacego
- NIE zmieniaj stylow apartamentow
- NIE dodawaj Swipera/FancyBoxa — widget parkingowy ich nie potrzebuje
</action>
<verify>
- Widget pojawia sie w panelu Elementor w kategorii "basic"
- Po dodaniu na strone wyswietla dwa typy z cenami
- Klikniecie "Historia cen" otwiera popup z danymi
</verify>
<done>AC-1 i AC-2 satisfied: widget wyswietla ceny, popup historia cen dziala</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<what-built>Widget Elementor "Miejsca Postojowe" z historia cen — design wg Figma Frame 16</what-built>
<how-to-verify>
1. Otworz Elementor na stronie /apartamenty/ lub testowej
2. Dodaj widget "Miejsca Postojowe"
3. Sprawdz czy wyswietla dwa typy z cenami
4. Kliknij "Historia cen" — sprawdz popup
5. Porownaj z designem na Figmie (Frame 16)
6. Sprawdz responsywnosc (mobile)
</how-to-verify>
<resume-signal>Type "approved" or describe visual/functional issues</resume-signal>
</task>
</tasks>
<boundaries>
## DO NOT CHANGE
- wp-content/plugins/elementor-addon/widgets/apartaments.php (widget apartamentow stabilny)
- wp-content/themes/hello-elementor/template-parts/garage-map.php (mapa garazowa)
- Istniejacy schemat tabeli wp_price_history (nie modyfikuj kolumn)
- Istniejace endpointy XML — rozszerzaj, nie usuwaj
## SCOPE LIMITS
- Tylko dwa typy miejsc postojowych: zwykle i rodzinne
- Brak galerii/dokumentow w widgecie parkingowym
- Brak edycji cen w widgecie — ceny z ACF options
- Strona admina Jawnosc Cen — nie modyfikuj (XML sam sie rozszerzy)
</boundaries>
<verification>
Before declaring plan complete:
- [ ] Widget "Miejsca Postojowe" renderuje dwa typy z cenami ACF
- [ ] Przycisk "Historia cen" otwiera popup z danymi z AJAX
- [ ] Cron zapisuje ceny parkingowe do wp_parking_price_history
- [ ] /ceny-mieszkan.xml zawiera sekcje z miejscami postojowymi
- [ ] Brak bledow PHP (WP_DEBUG=true)
- [ ] Istniejace funkcjonalnosci apartamentow nie naruszone
- [ ] All acceptance criteria met
</verification>
<success_criteria>
- All tasks completed
- All verification checks pass
- No PHP errors or warnings introduced
- Widget dostepny w Elementor i poprawnie renderuje
- Historia cen popup dziala identycznie jak dla apartamentow
- XML export zawiera miejsca postojowe z historia
</success_criteria>
<output>
After completion, create `.paul/phases/03-miejsca-postojowe/03-01-SUMMARY.md`
</output>

View File

@@ -1,134 +0,0 @@
---
phase: 03-miejsca-postojowe
plan: 01
subsystem: ui, api, database
tags: [elementor, acf, wp-cron, xml, ajax, parking]
requires:
- phase: 01-historia-cen
provides: price_history table, popup pattern, AJAX pattern, XML endpoint
- phase: 02-jawnosc-cen
provides: XML export structure, dane.gov.pl integration
provides:
- Elementor widget "Miejsca Postojowe" with price display and history popup
- wp_parking_price_history DB table
- Daily cron recording parking prices
- AJAX endpoint for parking price history
- XML export extended with parking spots
affects: []
tech-stack:
added: []
patterns: [ACF group field access pattern]
key-files:
created:
- wp-content/plugins/elementor-addon/widgets/parking-spots.php
modified:
- wp-content/plugins/elementor-addon/elementor-addon.php
- wp-content/plugins/elementor-addon/assets/js/main.js
- wp-content/plugins/elementor-addon/assets/css/main.css
key-decisions:
- "ACF groups: pola cen w grupach, nie flat — get_field('grupa', 'option')['pole']"
- "Osobna tabela wp_parking_price_history zamiast rozszerzania wp_price_history"
- "Cena m2 ukryta w widgecie na życzenie użytkownika (dane nadal w cronie/XML)"
- "Layout bez ramek, kolumnowy zamiast grid 2-kolumnowego"
patterns-established:
- "ACF group field access: get_field('group_name', 'option')['group_name_field']"
- "Parking popup reuse: fallback overlay usuwany jeśli apartamentowy istnieje"
duration: ~30min
completed: 2026-03-25
---
# Phase 3 Plan 01: Miejsca Postojowe Summary
**Widget Elementor wyswietlajacy ceny miejsc postojowych (zwykle/rodzinne) z historia cen, cron i XML export**
## Performance
| Metric | Value |
|--------|-------|
| Duration | ~30min |
| Completed | 2026-03-25 |
| Tasks | 2 auto + 2 checkpoints |
| Files modified | 4 (1 new, 3 modified) |
## Acceptance Criteria Results
| Criterion | Status | Notes |
|-----------|--------|-------|
| AC-1: Widget wyswietla ceny parkingowe | Pass | Dwa typy z cenami z ACF grup, layout kolumnowy |
| AC-2: Popup historia cen dziala | Pass | Reuse popup apartamentow, AJAX endpoint dziala |
| AC-3: Cron zapisuje ceny codziennie | Pass | INSERT IGNORE do wp_parking_price_history |
| AC-4: XML export zawiera miejsca postojowe | Pass | Sekcja <miejsca_postojowe> w /ceny-mieszkan.xml |
## Accomplishments
- Widget "Miejsca Postojowe" w Elementorze — 2 karty (zwykle/rodzinne) z ceną i przyciskiem historia cen
- Tabela wp_parking_price_history z codziennym zapisem cen przez WP Cron
- AJAX endpoint parking_get_price_history z popupem reusujacym overlay apartamentow
- XML /ceny-mieszkan.xml rozszerzony o sekcje <miejsca_postojowe> z historia
## Files Created/Modified
| File | Change | Purpose |
|------|--------|---------|
| `widgets/parking-spots.php` | Created | Elementor widget rendering parking spot cards |
| `elementor-addon.php` | Modified | DB table, cron extension, AJAX endpoint, XML export |
| `assets/js/main.js` | Modified | Parking price history click handler + AJAX |
| `assets/css/main.css` | Modified | Parking widget styles (no border, column layout) |
## Decisions Made
| Decision | Rationale | Impact |
|----------|-----------|--------|
| Osobna tabela wp_parking_price_history | Parking to opcje ACF, nie CPT — inna struktura (parking_type vs post_id) | Czysta separacja, brak zmian w istniejącej tabeli |
| ACF group access pattern | Pola cen są w grupach ACF, nie flat | Wymaga get_field('grupa', 'option')['pole'] |
| Ukrycie ceny m2 w widgecie | Request użytkownika — uproszczony wygląd | Dane m2 nadal w cronie i XML |
| Layout bez ramek, kolumnowy | Request użytkownika — bliższy designowi Figma | Prostszy CSS |
## Deviations from Plan
### Summary
| Type | Count | Impact |
|------|-------|--------|
| User-requested changes | 2 | Design adjustments, no functional impact |
| Auto-fixed | 1 | ACF group access pattern |
**Total impact:** Essential fixes + design preferences, no scope creep
### Details
1. **ACF group fields** — Plan zakladal flat access (get_field('pole', 'option')). Odkryto ze pola sa w grupach ACF. Naprawione na get_field('grupa', 'option')['pole'].
2. **Ukrycie ceny m2** — User request. Usunieto z HTML widgetu, dane nadal zapisywane.
3. **Layout bez ramek, kolumnowy** — User request. Zmieniono z grid 2-kolumnowego z ramkami na flex column bez ramek.
## Issues Encountered
| Issue | Resolution |
|-------|------------|
| Figma MCP niedostępny (oryginalny plik) | User udostępnił kopię, pobrano screenshot Frame 16 |
| Ceny nie wyświetlały się po wdrożeniu | ACF pola w grupach — naprawiony pattern dostępu |
## Next Phase Readiness
**Ready:**
- Milestone v0.2 Miejsca Postojowe kompletny (1 faza, 1 plan)
- Wszystkie AC spełnione
**Concerns:**
- WP Cron pseudocron — na produkcji zalecany systemowy cron
- Cena m2 ukryta — może wymagać przywrócenia w przyszłości
**Blockers:**
- None
---
*Phase: 03-miejsca-postojowe, Plan: 01*
*Completed: 2026-03-25*

View File

@@ -0,0 +1,167 @@
---
plan_id: 20260525-2149-historia-cen-zmiany
title: Popup historii cen pokazuje tylko daty zmian
storage: plan-first
legacy_phase: null
created: 2026-05-25T21:49:47+02:00
status: complete
type: execute
autonomous: true
delegation: auto
files_modified:
- wp-content/plugins/elementor-addon/elementor-addon.php
quality_radar: degraded
---
<objective>
## Goal
Zmienić dane zwracane do popupu historii cen lokali i miejsc postojowych tak, aby w tabeli były widoczne tylko daty rzeczywistej zmiany ceny.
## Purpose
Klient chce widzieć czytelną historię zmian, nie codzienne powtórzenia tej samej ceny. Jeżeli cena obowiązuje przez 15 dni, popup ma pokazać pierwszy dzień tego zakresu, a następnie dzień kolejnej zmiany ceny.
## Output
Aktualizacja logiki AJAX w `wp-content/plugins/elementor-addon/elementor-addon.php`, wspólna dla apartamentów i miejsc parkingowych, bez zmiany struktury tabel ani HTML modala.
</objective>
<context>
## Project Docs
@.paul/PROJECT.md
@.paul/STATE.md
@.paul/codebase/architecture.md
@.paul/codebase/db_schema.md
@.paul/codebase/impact_map.md
@.paul/codebase/quality_risks.md
@.paul/codebase/testing.md
## Source Files
@wp-content/plugins/elementor-addon/elementor-addon.php
@wp-content/plugins/elementor-addon/assets/js/main.js
@wp-content/plugins/elementor-addon/widgets/apartaments.php
@wp-content/plugins/elementor-addon/widgets/parking-spots.php
</context>
<clarifications>
- Brak pytań blokujących.
- Interpretacja wymagania: "pierwszy dzień z zakresu" oznacza pierwszy dzień chronologicznego okresu, w którym para `price` + `price_m2` pozostaje taka sama.
- Zakres dotyczy popupu historii cen, czyli odpowiedzi AJAX `apartamenty_get_price_history` i `parking_get_price_history`. Publiczny XML nie jest zmieniany w tym planie, bo użytkownik wskazał popup.
</clarifications>
<impact_scan>
## Quality Radar
**Status:** degraded
**Tools:** `codebase-memory-mcp` indexing attempted; MCP `list_projects` nadal nie pokazuje tego projektu po indeksowaniu, więc użyto celowanego `rg` i odczytu plików. `jscpd` i `ast-grep` są disabled by policy.
## Affected Areas
- Historia cen lokali: `elementor_addon_get_price_history_ajax()`, AJAX `apartamenty_get_price_history`, tabela `wp_price_history`.
- Historia cen miejsc postojowych: `elementor_addon_get_parking_price_history_ajax()`, AJAX `parking_get_price_history`, tabela `wp_parking_price_history`.
- Frontend popup: `wp-content/plugins/elementor-addon/assets/js/main.js` renderuje `json.data.history`; nie wymaga zmiany, jeśli API zachowa pola `recorded_at`, `price`, `price_m2`.
## Duplicate / Hardcoded Risks
- Dwie ścieżki historii cen, lokal i parking, mają podobne zapytania oraz renderowanie. Obsłużyć przez jedną małą funkcję pomocniczą filtrującą rekordy historii, żeby nie dublować algorytmu.
- Ceny są stringami. Porównywać znormalizowane wartości tekstowe (`trim((string) ...)`) dla `price` i `price_m2`, bez parsowania liczbowego, żeby nie zmienić znaczenia istniejących danych.
- Modal parkingu może działać z fallback overlay z `parking-spots.php`. Zachować ten sam kształt JSON, żeby oba układy DOM działały bez zmian.
## Explicit Deferrals
- XML `/ceny-mieszkan.xml` nadal może zawierać pełne dzienne snapshoty. To celowo poza zakresem, bo prośba dotyczy popupu.
- Brak automatycznych testów w repo. Weryfikacja będzie przez `php -l` oraz ręczne sprawdzenie AJAX/popupu z danymi, gdzie cena powtarza się przez kilka dni.
</impact_scan>
<acceptance_criteria>
## AC-1: Apartament pokazuje tylko daty zmian ceny
```gherkin
Given tabela `wp_price_history` ma dla jednego apartamentu wiele kolejnych dni z tą samą parą `price` i `price_m2`
When użytkownik otwiera popup historii cen apartamentu
Then popup pokazuje tylko pierwszy dzień każdego ciągłego zakresu tej samej ceny
And pomija późniejsze dni z niezmienioną ceną
```
## AC-2: Miejsce parkingowe pokazuje tylko daty zmian ceny
```gherkin
Given tabela `wp_parking_price_history` ma dla typu miejsca postojowego wiele kolejnych dni z tą samą parą `price` i `price_m2`
When użytkownik otwiera popup historii cen miejsca postojowego
Then popup pokazuje tylko pierwszy dzień każdego ciągłego zakresu tej samej ceny
And działa dla typów `zwykle` i `rodzinne`
```
## AC-3: API popupu zachowuje zgodność frontendu
```gherkin
Given frontend oczekuje `json.data.history`
When endpoint AJAX zwraca przefiltrowaną historię
Then każdy wiersz nadal ma pola `recorded_at`, `price` i `price_m2`
And `main.js` nie wymaga zmiany struktury renderowania
```
</acceptance_criteria>
<tasks>
<task type="auto">
<name>Task 1: Dodać wspólny filtr ciągłych zakresów ceny</name>
<files>wp-content/plugins/elementor-addon/elementor-addon.php</files>
<action>
Dodać małą funkcję pomocniczą, np. `elementor_addon_filter_price_change_history( $history )`, która przyjmuje rekordy historii posortowane rosnąco po `recorded_at`, przechodzi je chronologicznie i zostawia tylko pierwszy rekord po zmianie pary `price` + `price_m2`. Funkcja ma zwrócić tablicę rekordów z zachowanymi obiektami/kluczami i na końcu odwrócić wynik do kolejności malejącej, jeśli popup ma nadal pokazywać najnowsze zmiany u góry.
</action>
<verify>php -l wp-content/plugins/elementor-addon/elementor-addon.php</verify>
<done>Spełnia AC-1, AC-2 i AC-3 przez jeden wspólny algorytm.</done>
</task>
<task type="auto">
<name>Task 2: Zastosować filtr w endpointach AJAX</name>
<files>wp-content/plugins/elementor-addon/elementor-addon.php</files>
<action>
W `elementor_addon_get_price_history_ajax()` i `elementor_addon_get_parking_price_history_ajax()` zmienić zapytania historii na `ORDER BY recorded_at ASC`, pobrać rozsądny zakres danych bez utraty wcześniejszych zmian, a przed `wp_send_json_success()` przepuścić `$history` przez funkcję z Task 1. Zachować nazwy pól w odpowiedzi JSON.
</action>
<verify>Ręcznie wywołać oba AJAX endpointy z nonce lub sprawdzić popup na stronie dla apartamentu i parkingu z powtarzającymi się cenami.</verify>
<done>Spełnia AC-1, AC-2 i AC-3 dla obu endpointów.</done>
</task>
<task type="auto">
<name>Task 3: Zweryfikować brak regresji popupu</name>
<files>wp-content/plugins/elementor-addon/assets/js/main.js, wp-content/plugins/elementor-addon/widgets/apartaments.php, wp-content/plugins/elementor-addon/widgets/parking-spots.php</files>
<action>
Nie zmieniać JS ani widgetów, chyba że w trakcie APPLY okaże się, że kolejność lub pusty stan wymagają minimalnej korekty. Sprawdzić, że popup nadal renderuje cenę aktualną, cenę za m2, pustą historię oraz fallback parkingowy bez widgetu apartamentów.
</action>
<verify>Manualna checklista z `.paul/codebase/testing.md`: klik `HISTORIA CEN` dla lokalu i miejsca postojowego.</verify>
<done>Spełnia AC-3 i potwierdza brak zmiany kontraktu frontendu.</done>
</task>
</tasks>
<boundaries>
## Do Not Change
- Nie zmieniać schematu `wp_price_history` ani `wp_parking_price_history`.
- Nie zmieniać sposobu zapisu dziennych snapshotów przez `elementor_addon_record_prices()`.
- Nie zmieniać markup modala w `widgets/apartaments.php` i `widgets/parking-spots.php`, jeśli kontrakt JSON wystarczy.
- Nie zmieniać publicznych endpointów XML/MD5 w tym planie.
## Scope Limits
- Zakres obejmuje wyłącznie popup historii cen dla apartamentów i miejsc parkingowych.
- Brak refaktoryzacji dużego pliku `elementor-addon.php` poza niezbędną funkcją pomocniczą.
- Brak migracji danych historycznych; istniejące codzienne rekordy zostają w bazie.
</boundaries>
<verification>
- [x] `php -l wp-content/plugins/elementor-addon/elementor-addon.php`
- [x] Izolowany test: jedna cena w całej historii zwraca pierwszą i ostatnią datę.
- [ ] Popup apartamentu pokazuje tylko daty zmian przy powtarzających się cenach.
- [ ] Popup parkingu pokazuje tylko daty zmian dla `zwykle` i `rodzinne`.
- [x] Odpowiedź AJAX nadal zawiera `title`, `price`, `price_m2` i `history`.
- [x] Quality Radar relevant risks handled or deferred.
</verification>
<success_criteria>
- [x] All AC pass in code-level verification.
- [ ] Verification complete in browser/manual WordPress environment.
- [x] `STATE.md` routes this plan to UNIFY.
- [x] Raporty `.paul/codebase/impact_map.md`, `.paul/codebase/quality_risks.md` i `.paul/codebase/tooling_status.md` odnotowują zakres skanu planu.
</success_criteria>
<output>
SUMMARY.md path: `.paul/plans/20260525-2149-historia-cen-zmiany/SUMMARY.md`
</output>

View File

@@ -0,0 +1,75 @@
---
plan_id: 20260525-2149-historia-cen-zmiany
title: Popup historii cen pokazuje tylko daty zmian
completed: 2026-05-25T22:05:56+02:00
storage: plan-first
quality_radar: degraded
---
# Summary: Popup historii cen pokazuje tylko daty zmian
## Objective
Popup historii cen apartamentów i miejsc postojowych ma pokazywać tylko istotne daty: pierwszy dzień ciągłego zakresu tej samej ceny, dzień zmiany ceny oraz, dla historii z jedną niezmienną ceną, pierwszą i ostatnią datę zakresu.
## What Was Built
| Area | Result |
|------|--------|
| Filtr historii cen | Dodano `elementor_addon_filter_price_change_history(array $history)`, wspólny dla apartamentów i parkingu. |
| AJAX apartamentów | `elementor_addon_get_price_history_ajax()` pobiera historię chronologicznie, filtruje zakresy i zwraca wynik dla popupu. |
| AJAX parkingu | `elementor_addon_get_parking_price_history_ajax()` używa tej samej logiki dla typów `zwykle` i `rodzinne`. |
| Przypadek jednej ceny | Jeżeli cena nigdy się nie zmieniła i historia ma więcej niż jeden rekord, popup dostaje ostatnią i pierwszą datę zakresu; pojedynczy rekord nie jest dublowany. |
| Kontrakt frontendu | Zachowano strukturę `json.data.history` oraz pola `recorded_at`, `price`, `price_m2`. |
## Files Modified
- `wp-content/plugins/elementor-addon/elementor-addon.php` - filtr historii i podpięcie go pod oba endpointy AJAX.
- `.paul/plans/20260525-2149-historia-cen-zmiany/PLAN.md` - status i wyniki weryfikacji.
- `.paul/STATE.md` - przejście APPLY -> UNIFY -> loop complete.
- `.paul/codebase/radar/codebase-memory-post-apply.txt` - notatki post-apply Quality Radar.
- `.paul/codebase/tooling_status.md` - odnotowanie skanu post-apply.
## Acceptance Criteria Results
| Criterion | Status | Evidence |
|-----------|--------|----------|
| AC-1 | Pass | Endpoint apartamentów filtruje `wp_price_history` przez wspólną funkcję i zachowuje tylko początki zakresów zmian. |
| AC-2 | Pass | Endpoint parkingu filtruje `wp_parking_price_history` tą samą funkcją dla `zwykle` i `rodzinne`. |
| AC-3 | Pass | Odpowiedź AJAX nadal zwraca `title`, `price`, `price_m2` i `history`; `main.js` nie wymagał zmiany. |
## Verification Results
| Check | Result | Notes |
|-------|--------|-------|
| `php -l wp-content/plugins/elementor-addon/elementor-addon.php` | Pass | Brak błędów składni PHP. |
| Izolowany test wielu zmian | Pass | Próbka zwróciła `2026-05-20,2026-05-16,2026-05-01`. |
| Izolowany test jednej niezmiennej ceny | Pass | Próbka zwróciła `2026-05-20,2026-05-01`. |
| `git diff --check` dla zmienionych plików | Pass | Brak błędów whitespace; pozostały tylko ostrzeżenia CRLF. |
| Ręczne sprawdzenie popupu w WordPress | Pending | Do wykonania w środowisku z realnymi danymi i nonce. |
## Quality Radar Results
**Status:** degraded
- New risks: brak nowych ryzyk poza brakiem automatycznych testów UI/AJAX.
- Resolved risks: ryzyko duplikacji logiki ograniczone przez jedną funkcję pomocniczą dla apartamentów i parkingu.
- Deferred risks: publiczny XML `/ceny-mieszkan.xml` pozostaje poza zakresem, bo wymaganie dotyczyło popupu.
- Raw outputs: `.paul/codebase/radar/codebase-memory-plan.txt`, `.paul/codebase/radar/codebase-memory-post-apply.txt`.
## Deviations
- Dodano doprecyzowanie po APPLY: dla historii z jedną niezmienną ceną popup pokazuje pierwszą i ostatnią datę zakresu. To rozszerza pierwotny plan zgodnie z dodatkową prośbą użytkownika.
- Nie wykonano ręcznego testu w przeglądarce/WordPress, bo w tej sesji nie uruchamiano środowiska strony.
- Nie uruchomiono transition phase ani commita, bo projekt działa w trybie plan-first bez `.paul/ROADMAP.md` i bez legacy phase.
## Key Decisions / Patterns
- Porównanie zmian ceny odbywa się tekstowo po `trim((string) price)` i `trim((string) price_m2)`, bez parsowania liczbowego.
- Dane są pobierane z bazy rosnąco po `recorded_at`, a wynik dla popupu jest zwracany z najnowszymi pozycjami u góry.
- Schemat tabel i dzienny zapis snapshotów pozostały bez zmian.
## Follow-up
- Ręcznie sprawdzić popup historii cen dla apartamentu oraz miejsc postojowych `zwykle` i `rodzinne`.
- Jeżeli klient będzie oczekiwał takiego samego skracania historii w `/ceny-mieszkan.xml`, przygotować osobny plan.

View File

@@ -1,53 +0,0 @@
<?php
// Symulacja dbDelta — tworzymy tabelę bezpośrednio
$conn = new mysqli('host117523.hostido.net.pl', 'host117523_wyszynskiego12pagedevpl', 'yZs52KdErtTk9KmZ8XGq', 'host117523_wyszynskiego12pagedevpl');
if ($conn->connect_error) { die('Blad polaczenia: ' . $conn->connect_error); }
$conn->set_charset('utf8mb4');
$sql = "CREATE TABLE IF NOT EXISTS wp_price_history (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
post_id BIGINT UNSIGNED NOT NULL,
price VARCHAR(50) NOT NULL DEFAULT '',
price_m2 VARCHAR(50) NOT NULL DEFAULT '',
floor_space VARCHAR(50) NOT NULL DEFAULT '',
recorded_at DATE NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY unique_daily (post_id, recorded_at),
KEY idx_post_id (post_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;";
if ($conn->query($sql)) {
echo "OK: Tabela wp_price_history utworzona lub juz istnieje.\n";
} else {
echo "BLAD: " . $conn->error . "\n";
}
// Sprawdz czy tabela istnieje
$r = $conn->query("SHOW TABLES LIKE 'wp_price_history'");
echo "Tabela w bazie: " . ($r->num_rows > 0 ? "TAK" : "NIE") . "\n";
// Opisz strukturę
$r2 = $conn->query("DESCRIBE wp_price_history");
echo "\nStruktura tabeli:\n";
while ($row = $r2->fetch_assoc()) {
echo " " . $row['Field'] . " | " . $row['Type'] . " | " . $row['Key'] . "\n";
}
// Test INSERT IGNORE z danymi apartamentu 203
$today = date('Y-m-d');
$conn->query("INSERT IGNORE INTO wp_price_history (post_id, price, price_m2, floor_space, recorded_at) VALUES (203, '677 920', '19 000', '35,68', '$today')");
echo "\nINSERT IGNORE dla apt 203 (dzisiaj): affected_rows=" . $conn->affected_rows . "\n";
// Powtorzenie — powinno byc 0 (IGNORE)
$conn->query("INSERT IGNORE INTO wp_price_history (post_id, price, price_m2, floor_space, recorded_at) VALUES (203, '677 920', '19 000', '35,68', '$today')");
echo "Powtorny INSERT IGNORE (powinno byc 0): affected_rows=" . $conn->affected_rows . "\n";
// Pokaz rekordy
$r3 = $conn->query("SELECT * FROM wp_price_history LIMIT 5");
echo "\nRekordy w tabeli:\n";
while ($row = $r3->fetch_assoc()) {
echo " ID=" . $row['id'] . " | post_id=" . $row['post_id'] . " | price=" . $row['price'] . " | date=" . $row['recorded_at'] . "\n";
}
$conn->close();

2
.serena/.gitignore vendored
View File

@@ -1,2 +0,0 @@
/cache
/project.local.yml

View File

@@ -1,138 +0,0 @@
# the name by which the project can be referenced within Serena
project_name: "wyszynskiego12.pagedev.pl"
# list of languages for which language servers are started; choose from:
# al bash clojure cpp csharp
# csharp_omnisharp dart elixir elm erlang
# fortran fsharp go groovy haskell
# java julia kotlin lua markdown
# matlab nix pascal perl php
# php_phpactor powershell python python_jedi r
# rego ruby ruby_solargraph rust scala
# swift terraform toml typescript typescript_vts
# vue yaml zig
# (This list may be outdated. For the current list, see values of Language enum here:
# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py
# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.)
# Note:
# - For C, use cpp
# - For JavaScript, use typescript
# - For Free Pascal/Lazarus, use pascal
# Special requirements:
# Some languages require additional setup/installations.
# See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers
# When using multiple languages, the first language server that supports a given file will be used for that file.
# The first language is the default language and the respective language server will be used as a fallback.
# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
languages:
- php
# the encoding used by text files in the project
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
encoding: "utf-8"
# line ending convention to use when writing source files.
# Possible values: unset (use global setting), "lf", "crlf", or "native" (platform default)
# This does not affect Serena's own files (e.g. memories and configuration files), which always use native line endings.
line_ending:
# The language backend to use for this project.
# If not set, the global setting from serena_config.yml is used.
# Valid values: LSP, JetBrains
# Note: the backend is fixed at startup. If a project with a different backend
# is activated post-init, an error will be returned.
language_backend:
# whether to use project's .gitignore files to ignore files
ignore_all_files_in_gitignore: true
# list of additional paths to ignore in this project.
# Same syntax as gitignore, so you can use * and **.
# Note: global ignored_paths from serena_config.yml are also applied additively.
ignored_paths: []
# whether the project is in read-only mode
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
# Added on 2025-04-18
read_only: false
# list of tool names to exclude.
# This extends the existing exclusions (e.g. from the global configuration)
#
# Below is the complete list of tools for convenience.
# To make sure you have the latest list of tools, and to view their descriptions,
# execute `uv run scripts/print_tool_overview.py`.
#
# * `activate_project`: Activates a project by name.
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
# * `create_text_file`: Creates/overwrites a file in the project directory.
# * `delete_lines`: Deletes a range of lines within a file.
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
# * `execute_shell_command`: Executes a shell command.
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
# * `initial_instructions`: Gets the initial instructions for the current project.
# Should only be used in settings where the system prompt cannot be set,
# e.g. in clients you have no control over, like Claude Desktop.
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
# * `insert_at_line`: Inserts content at a given line in a file.
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
# * `list_memories`: Lists memories in Serena's project-specific memory store.
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
# * `read_file`: Reads a file within the project directory.
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
# * `remove_project`: Removes a project from the Serena configuration.
# * `replace_lines`: Replaces a range of lines within a file with new content.
# * `replace_symbol_body`: Replaces the full definition of a symbol.
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
# * `search_for_pattern`: Performs a search for a pattern in the project.
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
# * `switch_modes`: Activates modes by providing a list of their names
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
excluded_tools: []
# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default).
# This extends the existing inclusions (e.g. from the global configuration).
included_optional_tools: []
# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools.
# This cannot be combined with non-empty excluded_tools or included_optional_tools.
fixed_tools: []
# list of mode names to that are always to be included in the set of active modes
# The full set of modes to be activated is base_modes + default_modes.
# If the setting is undefined, the base_modes from the global configuration (serena_config.yml) apply.
# Otherwise, this setting overrides the global configuration.
# Set this to [] to disable base modes for this project.
# Set this to a list of mode names to always include the respective modes for this project.
base_modes:
# list of mode names that are to be activated by default.
# The full set of modes to be activated is base_modes + default_modes.
# If the setting is undefined, the default_modes from the global configuration (serena_config.yml) apply.
# Otherwise, this overrides the setting from the global configuration (serena_config.yml).
# This setting can, in turn, be overridden by CLI parameters (--mode).
default_modes:
# initial prompt for the project. It will always be given to the LLM upon activating the project
# (contrary to the memories, which are loaded on demand).
initial_prompt: ""
# time budget (seconds) per tool call for the retrieval of additional symbol information
# such as docstrings or parameter information.
# This overrides the corresponding setting in the global configuration; see the documentation there.
# If null or missing, use the setting from the global configuration.
symbol_info_budget:
# list of regex patterns which, when matched, mark a memory entry as readonly.
# Extends the list from the global configuration, merging the two lists.
read_only_memory_patterns: []

View File

@@ -327,6 +327,38 @@ function elementor_addon_record_prices() {
} }
add_action( 'apartamenty_record_prices', 'elementor_addon_record_prices' ); add_action( 'apartamenty_record_prices', 'elementor_addon_record_prices' );
/**
* Zostawia pierwszy dzien kazdego ciaglego zakresu tej samej ceny.
* Jesli cena nigdy sie nie zmienila, pokazuje pierwsza i ostatnia date.
*
* @param array $history Rekordy posortowane rosnaco po recorded_at.
* @return array
*/
function elementor_addon_filter_price_change_history( array $history ) {
$changes = [];
$previous_key = null;
$last_row = null;
foreach ( $history as $row ) {
$last_row = $row;
$price = isset( $row->price ) ? trim( (string) $row->price ) : '';
$price_m2 = isset( $row->price_m2 ) ? trim( (string) $row->price_m2 ) : '';
$key = $price . '|' . $price_m2;
if ( $key !== $previous_key ) {
$changes[] = $row;
}
$previous_key = $key;
}
if ( 1 === count( $changes ) && $last_row && $last_row !== $changes[0] ) {
return [ $last_row, $changes[0] ];
}
return array_reverse( $changes );
}
// =========================================================== // ===========================================================
// HISTORIA CEN - AJAX ENDPOINT // HISTORIA CEN - AJAX ENDPOINT
// =========================================================== // ===========================================================
@@ -356,12 +388,13 @@ function elementor_addon_get_price_history_ajax() {
"SELECT recorded_at, price, price_m2, floor_space "SELECT recorded_at, price, price_m2, floor_space
FROM {$table_name} FROM {$table_name}
WHERE post_id = %d WHERE post_id = %d
ORDER BY recorded_at DESC ORDER BY recorded_at ASC",
LIMIT 50",
$post_id $post_id
) )
); );
$history = elementor_addon_filter_price_change_history( $history );
wp_send_json_success( [ wp_send_json_success( [
'title' => get_the_title( $post_id ), 'title' => get_the_title( $post_id ),
'price' => get_post_meta( $post_id, 'information_price', true ), 'price' => get_post_meta( $post_id, 'information_price', true ),
@@ -408,12 +441,13 @@ function elementor_addon_get_parking_price_history_ajax() {
"SELECT recorded_at, price, price_m2 "SELECT recorded_at, price, price_m2
FROM {$table_name} FROM {$table_name}
WHERE parking_type = %s WHERE parking_type = %s
ORDER BY recorded_at DESC ORDER BY recorded_at ASC",
LIMIT 50",
$parking_type $parking_type
) )
); );
$history = elementor_addon_filter_price_change_history( $history );
$group_name = $group_names[ $parking_type ]; $group_name = $group_names[ $parking_type ];
$group = get_field( $group_name, 'option' ) ?: []; $group = get_field( $group_name, 'option' ) ?: [];