326 lines
20 KiB
Markdown
326 lines
20 KiB
Markdown
---
|
|
phase: 09-finalizacja
|
|
plan: 02
|
|
type: execute
|
|
wave: 1
|
|
depends_on: ["09-01"]
|
|
files_modified:
|
|
- wp-content/plugins/yacht-booking-system/integrations/ical/class-ical-feed.php
|
|
- wp-content/plugins/yacht-booking-system/integrations/ical/class-ical-import.php
|
|
- wp-content/plugins/yacht-booking-system/admin/class-admin.php
|
|
- wp-content/plugins/yacht-booking-system/admin/views/settings.php
|
|
- wp-content/plugins/yacht-booking-system/includes/class-yacht.php
|
|
- wp-content/plugins/yacht-booking-system/admin/views/yacht-edit.php
|
|
- wp-content/plugins/yacht-booking-system/includes/class-installer.php
|
|
autonomous: false
|
|
delegation: off
|
|
---
|
|
|
|
<objective>
|
|
## Goal
|
|
Wprowadzić dwukierunkową synchronizację z **jednym wspólnym Google Calendar** (przez iCal feed) z rozróżnieniem rezerwacji per jacht po prefiksie w tytule eventu (`"{nazwa_jachtu} - {tekst}"`). Frontend kalendarz dla danego jachtu pokazuje tylko jego rezerwacje.
|
|
|
|
## Purpose
|
|
Klient (właściciel wypożyczalni) chce zarządzać rezerwacjami wszystkich jachtów w jednym Google Calendar — jednym widokiem floty. Tworzy ręcznie eventy w GCal (np. blokady, rezerwacje telefoniczne) używając prefiksu z nazwą jachtu, a strona automatycznie przypisuje je do właściwego jachtu. Jednocześnie rezerwacje złożone przez frontend trafiają z powrotem do tego samego kalendarza w spójnym formacie.
|
|
|
|
## Output
|
|
- Globalny iCal Export feed (jeden URL z tokenem) — zawiera rezerwacje wszystkich jachtów z prefiksem nazwy jachtu w SUMMARY
|
|
- Globalny iCal Import URL — parser dopasowuje prefiks do jachtu, importuje tylko rozpoznane eventy
|
|
- Settings page — pola dla globalnego import URL i wyświetlanie globalnego export URL
|
|
- Pole `_yacht_gcal_alias` (opcjonalny alias dla skróconej nazwy w GCal)
|
|
- Per-yacht feedy iCal pozostają sprawne (kompatybilność wsteczna)
|
|
</objective>
|
|
|
|
<context>
|
|
## Project Context
|
|
@.paul/PROJECT.md
|
|
@.paul/ROADMAP.md
|
|
@.paul/STATE.md
|
|
@.paul/codebase/architecture.md
|
|
|
|
## Source Files
|
|
@wp-content/plugins/yacht-booking-system/integrations/ical/class-ical-feed.php
|
|
@wp-content/plugins/yacht-booking-system/integrations/ical/class-ical-import.php
|
|
@wp-content/plugins/yacht-booking-system/admin/class-admin.php
|
|
@wp-content/plugins/yacht-booking-system/includes/class-yacht.php
|
|
@wp-content/plugins/yacht-booking-system/includes/class-settings.php
|
|
|
|
<clarifications>
|
|
- **Mechanizm** — iCal feed (publiczny URL Google Calendar) zamiast OAuth API. Powód: klient nie chce przechodzić przez OAuth, woli dodać URL i mieć synchronizację automatyczną.
|
|
→ Odpowiedź: dwukierunkowo przez iCal — import z GCal feedu + export własnego feedu (admin subskrybuje go w GCal).
|
|
|
|
- **Identyfikacja jachtu** — po prefiksie w tytule eventu, separator `" - "`. Przykład: `"Maja - Kowalski 5 osób"`, `"Kubuś - blokada serwisowa"`.
|
|
→ Odpowiedź: parser wyciąga tekst przed pierwszym `" - "`, dopasowuje case-insensitive do `post_title` jachtu lub `_yacht_gcal_alias` (jeśli ustawiony — ma priorytet).
|
|
|
|
- **Eventy bez rozpoznanego jachtu** — ignorowane.
|
|
→ Odpowiedź: jeśli prefiks nie pasuje do żadnego jachtu (lub brak separatora `" - "`), event pomijany w imporcie. Brak fallbacku na "blokuj wszystkie" — klient nie chce niespodzianek.
|
|
|
|
- **Push do GCal** — przez ten sam globalny feed iCal (admin subskrybuje feed pluginu w GCal).
|
|
→ Odpowiedź: rezerwacje ze strony pojawiają się w GCal po następnej refresh subskrypcji (Google odświeża iCal subscription co kilka godzin — limit Google'a, nie pluginu).
|
|
|
|
- **Frontend filtrowanie** — kalendarz per jacht pokazuje tylko eventy danego jachtu.
|
|
→ Odpowiedź: bez zmian w widgecie — działa już teraz przez `_booking_yacht_id`. Wystarczy że import poprawnie przypisze yacht_id.
|
|
</clarifications>
|
|
</context>
|
|
|
|
<acceptance_criteria>
|
|
|
|
## AC-1: Globalny iCal Export feed
|
|
```gherkin
|
|
Given admin ma kilka rezerwacji w bazie dla różnych jachtów (np. "Maja", "Kubuś")
|
|
When admin otwiera URL `/yacht-ical-global/{token}.ics`
|
|
Then odpowiedź ma Content-Type `text/calendar`
|
|
And feed zawiera VEVENT dla każdej niezanullowanej rezerwacji wszystkich jachtów
|
|
And SUMMARY każdego eventu ma format `"{nazwa_jachtu} - {imię_klienta}"` (lub z prefiksem `[Oczekująca]` gdy status pending)
|
|
And UID jest stabilny (`booking-{id}@{domain}`) — kolejne fetche zwracają ten sam UID
|
|
```
|
|
|
|
## AC-2: Globalny iCal Import — dopasowanie po prefiksie
|
|
```gherkin
|
|
Given globalny iCal Import URL jest ustawiony w Settings
|
|
And kalendarz Google zawiera event "Maja - Test rezerwacja" (jacht "Maja" istnieje w bazie)
|
|
And kalendarz Google zawiera event "Nieznany - cokolwiek" (brak takiego jachtu)
|
|
And kalendarz Google zawiera event "Brak separatora" (bez " - ")
|
|
When uruchamiany jest cron `yacht_booking_ical_import` (lub manualny przycisk)
|
|
Then plugin tworzy/aktualizuje booking dla jachtu "Maja" z source `ical_import_global`
|
|
And blokuje dostępność `wp_yacht_availability` tylko dla yacht_id jachtu "Maja"
|
|
And event "Nieznany" jest pominięty (logowane jako info, brak booking)
|
|
And event "Brak separatora" jest pominięty
|
|
And kolejny przebieg (nawet bez zmian w GCal) nie tworzy duplikatów (idempotencja po UID)
|
|
```
|
|
|
|
## AC-3: Globalny Import — czyszczenie usuniętych eventów
|
|
```gherkin
|
|
Given event "Maja - Foo" został zaimportowany przy poprzednim cron'ie
|
|
When admin usuwa go z Google Calendar
|
|
And uruchamia się kolejny cron import
|
|
Then powiązany booking (z source `ical_import_global` i tym UID) jest usuwany
|
|
And `Availability::clear_booking_availability()` zwalnia daty
|
|
```
|
|
|
|
## AC-4: Alias jachtu (opcjonalny)
|
|
```gherkin
|
|
Given jacht ma `post_title="Marina Maja Sailing 35"` i `_yacht_gcal_alias="Maja"`
|
|
And event w GCal ma SUMMARY "Maja - Klient X"
|
|
When uruchamia się import
|
|
Then plugin dopasowuje event do tego jachtu po aliasie
|
|
And tworzy booking z `_booking_yacht_id` = ID tego jachtu
|
|
```
|
|
|
|
## AC-5: Frontend filtrowanie (regresja)
|
|
```gherkin
|
|
Given booking dla jachtu "Maja" zaimportowany przez globalny iCal
|
|
When klient otwiera stronę z widgetem `[yacht_calendar yacht_id=ID_MAJA]`
|
|
Then kalendarz pokazuje datę rezerwacji jako zablokowaną
|
|
And widget jachtu "Kubuś" NIE pokazuje tej daty jako zablokowanej
|
|
```
|
|
|
|
## AC-6: Settings UI
|
|
```gherkin
|
|
Given admin otwiera Settings → zakładka Google Calendar / iCal
|
|
When widzi sekcję "Globalna synchronizacja iCal"
|
|
Then widzi pole "iCal Import URL" (input type=url) — globalny URL kalendarza Google
|
|
And widzi pole tylko-do-odczytu "iCal Export URL" z linkiem `/yacht-ical-global/{token}.ics` i przyciskiem "Kopiuj"
|
|
And widzi przycisk "Wygeneruj nowy token" (regeneruje token, unieważniając poprzedni URL)
|
|
And widzi przycisk "Importuj teraz" (manual trigger run_global_import)
|
|
And w yacht-edit jest nowe pole "Alias dla Google Calendar" (placeholder: nazwa jachtu) — opcjonalne
|
|
```
|
|
|
|
</acceptance_criteria>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Globalny iCal Export feed + alias jachtu</name>
|
|
<files>
|
|
wp-content/plugins/yacht-booking-system/integrations/ical/class-ical-feed.php,
|
|
wp-content/plugins/yacht-booking-system/includes/class-yacht.php,
|
|
wp-content/plugins/yacht-booking-system/includes/class-installer.php
|
|
</files>
|
|
<action>
|
|
1. W `class-ical-feed.php` dodaj nową ścieżkę feed:
|
|
- rewrite rule `^yacht-ical-global/([a-zA-Z0-9]+)\.ics$` → `index.php?yacht_ical_global=1&yacht_ical_token=$matches[1]`
|
|
- query var `yacht_ical_global`
|
|
- handler `handle_global_feed_request()` weryfikuje token przez `hash_equals` względem opcji `yacht_booking_global_ical_token`
|
|
- metoda `output_global_ics()`: pobiera WSZYSTKIE bookingi (nie filtrowane po yacht), iteruje, dla każdego ustala SUMMARY = `"{nazwa_jachtu} - {imię_klienta}"` (analogicznie do istniejącego `output_ics`, ale z prefiksem nazwy jachtu z `Yacht::get_post_title( yacht_id )` zamiast tylko klienta)
|
|
- skip bookingi ze statusem `cancelled` oraz z source `ical_import_global` (nie wysyłaj z powrotem do Google tego co stamtąd przyszło — anti-loop)
|
|
- UID format: `booking-{id}@{domain}` (taki sam jak per-yacht — zachować stabilność)
|
|
- X-WR-CALNAME: `"Wszystkie jachty - " . get_bloginfo('name')`
|
|
2. Helper `get_global_feed_token()` — generuje/zwraca token z opcji, `regenerate_global_token()` — nadpisuje token.
|
|
3. Helper `get_global_feed_url()` — zwraca pełny URL.
|
|
4. W `class-yacht.php` dodaj akcesory:
|
|
- `get_gcal_alias( $yacht_id )` → `get_post_meta( $yacht_id, '_yacht_gcal_alias', true )`
|
|
- `update_gcal_alias( $yacht_id, $alias )` → sanitize_text_field + update_post_meta
|
|
5. W `class-installer.php` (jeśli wersja schematu się ewoluuje) dodaj domyślne opcje:
|
|
- `add_option( 'yacht_booking_global_ical_import_url', '' )`
|
|
- `add_option( 'yacht_booking_global_ical_token', '' )` (token generowany lazy przy pierwszym fetch URL)
|
|
- flush_rewrite_rules przy aktywacji (rewrite rule).
|
|
|
|
Avoid: nie modyfikuj istniejących per-yacht feedów — działają jako fallback i są używane przez OTA/iCal subscribers innych systemów.
|
|
</action>
|
|
<verify>
|
|
- `php -l class-ical-feed.php` (syntax OK)
|
|
- W przeglądarce: po zapisie pluginu i flush rewrite, URL `/yacht-ical-global/{token}.ics` zwraca .ics z eventami wszystkich jachtów, każdy z prefiksem nazwy jachtu
|
|
- `curl /yacht-ical-global/{token}.ics | grep "SUMMARY"` zawiera np. `SUMMARY:Maja - Kowalski`
|
|
- URL z błędnym tokenem zwraca 403
|
|
</verify>
|
|
<done>AC-1, AC-4 (część dot. aliasu — akcesory) zaspokojone</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Globalny iCal Import + parser prefiksu</name>
|
|
<files>
|
|
wp-content/plugins/yacht-booking-system/integrations/ical/class-ical-import.php
|
|
</files>
|
|
<action>
|
|
1. Dodaj stałą `const GLOBAL_IMPORT_SOURCE = 'ical_import_global';` (osobna od per-yacht `IMPORT_SOURCE = 'ical_import'`).
|
|
2. Nowa metoda statyczna `run_global_import()`:
|
|
- Pobiera URL z `get_option( 'yacht_booking_global_ical_import_url' )`
|
|
- Jeśli pusty — return false
|
|
- `wp_remote_get` z timeout 30s
|
|
- parse_ics (re-use istniejącej metody, jest prywatna — wystawić jako protected lub wynieść do helpera; preferowane: zmienić visibility na `protected` lub utworzyć publiczną wrapping)
|
|
- Pre-fetch: lista jachtów + mapa `lowercase_name_or_alias → yacht_id`. Budowa: dla każdego jachtu klucz = `_yacht_gcal_alias` (jeśli != '') albo `post_title`, wszystko `mb_strtolower` + trim.
|
|
- Pre-fetch: `existing_global_map` — bookingi z source `GLOBAL_IMPORT_SOURCE`, mapa `uid → booking_id` (analogicznie do `get_existing_import_map` ale bez filtrowania po yacht).
|
|
- Iteracja po eventach:
|
|
a. Skip jeśli brak uid/start/end lub event w przeszłości (`strtotime($event['end']) < time()`)
|
|
b. Parse SUMMARY: `$pos = mb_strpos( $summary, ' - ' )`. Jeśli `$pos === false` → log "skip: no separator", continue.
|
|
c. `$prefix = mb_strtolower( trim( mb_substr( $summary, 0, $pos ) ) )`
|
|
d. Lookup w mapie. Jeśli brak → log "skip: yacht not found for prefix '{prefix}'", continue.
|
|
e. Wywołaj `upsert_global_booking( $yacht_id, $event, $existing_id )` (nowa metoda — jak `upsert_booking` ale z `_booking_source = GLOBAL_IMPORT_SOURCE`, `_booking_customer_name = "Google Calendar (import)"`, post_title `"GCal: {summary}"`).
|
|
f. Dodaj uid do `$seen_uids`.
|
|
- Po pętli: stale cleanup — dla każdego `$uid => $booking_id` w `$existing_global_map` którego nie ma w `$seen_uids` → `Availability::clear_booking_availability($booking_id)` + `wp_delete_post($booking_id, true)`.
|
|
- `update_option( 'yacht_booking_global_ical_last_import', current_time('mysql') )`.
|
|
- return true.
|
|
3. Hook cron: w `register()` dodać `add_action( 'yacht_booking_ical_global_import', array( __CLASS__, 'run_global_import' ) )` i w `setup_cron()` zarejestruj hourly.
|
|
4. Hook do uruchomienia obu importów (per-yacht + global) z jednego cron tick'a — nie zmieniać istniejącego, dodać osobny `wp_schedule_event` dla `yacht_booking_ical_global_import`.
|
|
|
|
Avoid:
|
|
- Nie usuwać per-yacht importu (kompatybilność)
|
|
- Nie używać sztywno `strpos` po bajtach — używać `mb_*` (polskie znaki w nazwach jachtów: "Kubuś", "Maja Słoneczna")
|
|
- Nie matchować po fragmentach — tylko exact equality po lower-case (inaczej "Maja" pasuje do "Maja Słoneczna" jednocześnie)
|
|
</action>
|
|
<verify>
|
|
- `php -l class-ical-import.php`
|
|
- Test ręczny: stworzyć w Google Calendar event "Maja - test", subskrybować feed Google iCal URL w settings, kliknąć "Importuj teraz", sprawdzić że w admin → Bookings pojawia się booking przypisany do jachtu "Maja"
|
|
- Event "FooBar - test" (brak takiego jachtu) — nie tworzy bookingu, w error log linia "skip: yacht not found"
|
|
- Event "BezSeparatora" — nie tworzy bookingu
|
|
- Drugi cron tick — brak duplikatu
|
|
- Usunięcie eventu w GCal + cron → booking zniknął, daty zwolnione w `wp_yacht_availability`
|
|
</verify>
|
|
<done>AC-2, AC-3, AC-4 (matching po aliasie) zaspokojone</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 3: Settings UI + yacht-edit alias + admin actions</name>
|
|
<files>
|
|
wp-content/plugins/yacht-booking-system/admin/class-admin.php,
|
|
wp-content/plugins/yacht-booking-system/admin/views/settings.php,
|
|
wp-content/plugins/yacht-booking-system/admin/views/yacht-edit.php
|
|
</files>
|
|
<action>
|
|
1. `settings.php` — dodaj sekcję "Globalna synchronizacja iCal":
|
|
- Pole `yacht_booking_global_ical_import_url` (input type=url, full width)
|
|
- Read-only display: globalny export URL (`ICal_Feed::get_global_feed_url()`) — input readonly + przycisk "Kopiuj" (JS clipboard)
|
|
- Przycisk "Wygeneruj nowy token" (form POST z nonce + action `regenerate_global_ical_token`)
|
|
- Przycisk "Importuj teraz" (form POST z nonce + action `run_global_ical_import`)
|
|
- Wyświetl `yacht_booking_global_ical_last_import` ("Ostatni import: ...")
|
|
- Krótka instrukcja: "Skopiuj Export URL i dodaj jako 'Z URL-a' w Google Calendar (Inne kalendarze → Z URL). Eventy w Google nazywaj wg wzorca: 'Nazwa jachtu - opis'. Wklej iCal URL kalendarza Google poniżej."
|
|
2. `class-admin.php` — w `process_settings_save()` (lub equivalent) zapisz nowe pole.
|
|
3. `class-admin.php` — `process_booking_actions()` (lub nowy `process_ical_actions()`):
|
|
- obsłuż `regenerate_global_ical_token` → `ICal_Feed::regenerate_global_token()` + `?notice=token_regenerated` redirect
|
|
- obsłuż `run_global_ical_import` → `ICal_Import::run_global_import()` + `?notice=import_done` redirect
|
|
4. `yacht-edit.php` — dodaj wiersz tabeli "Alias dla Google Calendar":
|
|
- input text `yacht_gcal_alias`, value = `Yacht::get_gcal_alias( $yacht_id )`
|
|
- Description: "Opcjonalny krótki alias używany w tytule eventu Google Calendar (np. 'Maja' zamiast 'Marina Maja Sailing 35'). Jeśli puste — używana jest pełna nazwa jachtu."
|
|
5. `class-admin.php::process_yacht_save()` — zapisz `yacht_gcal_alias` przez `Yacht::update_gcal_alias()`.
|
|
6. Dodaj admin notices dla token_regenerated i import_done.
|
|
|
|
Avoid:
|
|
- Nie usuwaj istniejących pól per-yacht ("Google Calendar ID" i "iCal Import URL") — out of scope. Klient zdecyduje w kolejnym planie czy je sprzątać.
|
|
- Nie dotykaj OAuth UI (sekcja Google Calendar OAuth zostaje bez zmian).
|
|
</action>
|
|
<verify>
|
|
- `php -l` na każdym zmienionym pliku
|
|
- W panelu admin → Settings: nowa sekcja jest widoczna, zapis URL działa, "Wygeneruj token" zmienia URL, "Importuj teraz" wywołuje import (sprawdzalne w error log lub bookingach)
|
|
- W yacht-edit: pole alias zapisuje się i odczytuje
|
|
- Token w URL ma >=20 znaków, zmiana tokenu unieważnia stary URL (403)
|
|
</verify>
|
|
<done>AC-6 zaspokojone, AC-4 (UI dla aliasu) zaspokojone</done>
|
|
</task>
|
|
|
|
<task type="checkpoint:human-verify" gate="blocking">
|
|
<what-built>
|
|
Dwukierunkowa synchronizacja iCal z jednym wspólnym Google Calendar:
|
|
- Globalny export feed (admin subskrybuje w GCal)
|
|
- Globalny import URL (plugin pobiera kalendarz Google)
|
|
- Identyfikacja jachtu po prefiksie "Nazwa - opis" w tytule eventu
|
|
- Frontend per-jacht pokazuje tylko swoje eventy
|
|
</what-built>
|
|
<how-to-verify>
|
|
1. Settings → skopiuj Export URL.
|
|
2. W Google Calendar → "Inne kalendarze" → "Z URL-a" → wklej Export URL → potwierdź że pojawiają się rezerwacje wszystkich jachtów z prefiksem nazwy.
|
|
3. W Google Calendar (głównym admina) utwórz event ręczny: tytuł "Maja - Test ręcznie", data za tydzień.
|
|
4. Skopiuj URL iCal swojego głównego kalendarza Google (Ustawienia kalendarza → Tajny adres w formacie iCal).
|
|
5. Wklej do Settings pluginu → "iCal Import URL".
|
|
6. Kliknij "Importuj teraz".
|
|
7. Sprawdź: WP Admin → Rezerwacje → pojawia się booking "GCal: Maja - Test ręcznie" przypisany do jachtu "Maja".
|
|
8. Otwórz frontend kalendarz jachtu Maja — data zablokowana. Otwórz kalendarz innego jachtu — data wolna.
|
|
9. Usuń event w Google → uruchom "Importuj teraz" ponownie → booking znika.
|
|
10. Stwórz w GCal "Nieznany - test" → import → brak bookingu (sprawdź WP error log: "skip: yacht not found").
|
|
</how-to-verify>
|
|
<resume-signal>Wpisz "approved" gdy wszystkie 10 punktów weryfikacji przejdzie, lub opisz problemy do poprawy</resume-signal>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<boundaries>
|
|
|
|
## DO NOT CHANGE
|
|
- `class-availability.php` (logika availability stabilna)
|
|
- `class-booking.php` (CPT booking — nie ruszamy)
|
|
- `wp_yacht_availability` schema (zablokowane)
|
|
- `frontend/class-calendar-widget.php` i `frontend/class-shortcode.php` (widget renderingu — działa już per-yacht)
|
|
- `class-rest-controller.php` (REST endpointy — bez zmian)
|
|
- OAuth integration (`class-oauth-handler.php`, `class-gcal-service.php`, `class-sync-controller.php`) — out of scope tego planu, push przez OAuth zostaje na razie jak jest (drugi mechanizm; klient użyje iCal a OAuth zignoruje, lub w kolejnym planie wyłączymy)
|
|
- Per-yacht iCal feed (`output_ics`) i per-yacht import (`run_import` + `import_for_yacht`) — pozostają funkcjonalne dla kompatybilności wstecznej
|
|
|
|
## SCOPE LIMITS
|
|
- Plan NIE usuwa pól per-yacht ("Google Calendar ID", "iCal Import URL") z yacht-edit — to decyzja na osobny plan po pozytywnej weryfikacji nowej globalnej synchronizacji
|
|
- Plan NIE wyłącza OAuth push do GCal — drugi (równoległy) mechanizm; jeśli klient nie połączy OAuth, nie jest aktywny
|
|
- Brak nowych zależności (PHP libs, npm, CDN)
|
|
- Brak zmian w bazie poza dodaniem `wp_options` keys i opcjonalnym yacht meta `_yacht_gcal_alias`
|
|
- Brak tłumaczeń .pot w tym planie (Phase 9 osobno)
|
|
|
|
</boundaries>
|
|
|
|
<verification>
|
|
Przed deklaracją ukończenia planu:
|
|
- [ ] `php -l` na wszystkich zmodyfikowanych plikach przechodzi
|
|
- [ ] Globalny export URL zwraca poprawny .ics z prefiksami nazw jachtów
|
|
- [ ] Globalny import URL parsuje eventy z GCal i przypisuje do jachtów po prefiksie
|
|
- [ ] Eventy bez separatora lub bez dopasowanego jachtu są ignorowane (logowane)
|
|
- [ ] Idempotencja: wielokrotne uruchomienie importu nie tworzy duplikatów
|
|
- [ ] Stale cleanup: usunięcie eventu w GCal + import → booking znika
|
|
- [ ] Frontend kalendarz per-jacht pokazuje tylko swoje rezerwacje (regresja)
|
|
- [ ] Per-yacht feedy iCal nadal działają (nie zostały zepsute)
|
|
- [ ] Settings UI: zapis URL, regeneracja tokenu, manual import, alias jachtu działają
|
|
- [ ] Wszystkie acceptance criteria spełnione
|
|
- [ ] Checkpoint human-verify zaakceptowany przez klienta
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- Klient może zarządzać rezerwacjami całej floty w jednym Google Calendar
|
|
- Eventy ręczne w GCal z prefiksem "Nazwa jachtu - ..." automatycznie pojawiają się jako blokady na frontendzie tego jachtu
|
|
- Rezerwacje ze strony pojawiają się w GCal admina (po refresh subskrypcji Google)
|
|
- Każdy frontend kalendarz pokazuje tylko swoje rezerwacje
|
|
- Zachowana kompatybilność wsteczna (per-yacht feedy + OAuth push nadal działają)
|
|
</success_criteria>
|
|
|
|
<output>
|
|
Po ukończeniu utwórz `.paul/phases/09-finalizacja/09-02-SUMMARY.md` zawierający:
|
|
- Co zbudowano (export feed, import, settings UI, alias)
|
|
- Decyzje (separator " - ", lookup case-insensitive po post_title/alias, ignore unmatched)
|
|
- Otwarte pytania na kolejny plan (czy usuwać pola per-yacht? czy wyłączać OAuth push?)
|
|
- Ścieżkę testową odtworzeniową
|
|
</output>
|