---
phase: 01-statlink-autolinking
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - migrations/013_statlink_tracking.sql
  - src/Services/StatLinkService.php
  - src/Controllers/SettingsController.php
  - templates/settings/index.php
  - .env
autonomous: true
delegation: off
---

<objective>
## Goal
Zbudować mechanizm automatycznego dodawania opublikowanych artykułów do StatLink.pl oraz ich automatycznego usuwania po 60 dniach.

## Purpose
Każdy opublikowany artykuł na stronach zapleczowych powinien automatycznie otrzymywać linkowanie w systemie StatLink.pl na okres 60 dni, co zwiększy efektywność pozycjonowania. Po 60 dniach link jest automatycznie usuwany, aby nie marnować punktów.

## Output
- `StatLinkService.php` — serwis PHP z logowaniem do StatLink via Guzzle (cookies), dodawaniem i usuwaniem linków
- Migracja SQL do śledzenia linków w StatLink (statlink_id, article_id, added_at, expires_at)
- Endpoint cron do automatycznego uruchamiania (dodaj nowe / usuń wygasłe)
</objective>

<context>
## Project Context
@.paul/PROJECT.md
@.paul/ROADMAP.md
@.paul/STATE.md

## Source Files
@src/Services/PublisherService.php
@src/Models/Article.php
@src/Controllers/SettingsController.php
@.env

## StatLink.pl Research (from browser exploration)
- Login: POST to https://statlink.pl/ with fields: email (textbox), haslo (textbox), submit ZALOGUJ
- Session: cookie-based (PHP session)
- Add link: POST to /148,twoje-linki#lista with fields:
  - niepozwol: CSRF token (must be scraped from page)
  - https: 1 (radio, 0=http, 1=https)
  - link: URL without protocol (e.g. "example.com/article-slug")
  - anchor: anchor text (article title or topic keyword)
  - fraza_kluczowa1, fraza_kluczowa2, fraza_kluczowa3: (empty)
  - wylacznosc: unchecked
  - frazowy: unchecked
  - tylko_https: unchecked
  - min_ilosc_znakow: 0
  - statrank_min: 0, statrank_max: 10
  - semstorm_keywords_top_min: 0
  - ilosc_dziennie: 0.14 (= 1 link co 2 dni)
  - ilosc_max: 10
  - ilosc_nofollow: 0
  - max_ilosc_domena: (default 5)
  - id_kategorie_multiple[]: all category values selected
  - zapisz: DODAJ
- Delete link: POST to /148,twoje-linki#lista0 with fields:
  - statlink_id: ID of the link
  - usun: Usuń
- Category checkboxes: multiple id_kategorie_multiple[] values (all selected)
- NOWY LINK form is inside div#nowy_link2vis
- Each link row has Edytuj and Usuń buttons with statlink_id hidden field
</context>

<acceptance_criteria>

## AC-1: Login do StatLink
```gherkin
Given dane logowania w .env (statlink_url, statlink_login, statlink_password)
When StatLinkService wykonuje login via Guzzle z CookieJar
Then sesja jest utrzymana i kolejne requesty zwracają stronę zalogowanego użytkownika
```

## AC-2: Dodawanie linku do StatLink
```gherkin
Given opublikowany artykuł z wp_post_url i tytułem
When StatLinkService::addLink() jest wywołane
Then link zostaje dodany w StatLink.pl z prawidłowymi parametrami (anchor=tytuł/keyword, ilosc_dziennie=0.14, ilosc_max=10, wszystkie kategorie)
And statlink_id zostaje zapisany w tabeli statlink_links
```

## AC-3: Usuwanie wygasłych linków
```gherkin
Given link w tabeli statlink_links z expires_at < NOW()
When StatLinkService::removeExpiredLinks() jest wywołane
Then link zostaje usunięty ze StatLink.pl via POST z usun
And rekord w tabeli statlink_links zostaje oznaczony jako removed
```

## AC-4: Cron endpoint
```gherkin
Given endpoint /cron/statlink z tokenem autoryzacyjnym
When endpoint jest wywołany
Then nowe opublikowane artykuły (bez wpisu w statlink_links) dostają linki w StatLink
And wygasłe linki (expires_at < NOW()) są usuwane ze StatLink
```

## AC-5: Tracking w bazie danych
```gherkin
Given tabela statlink_links
When artykuł dostaje link w StatLink
Then zapisywany jest: article_id, site_id, statlink_id, anchor, added_at, expires_at (added_at + 60 dni), status
```

</acceptance_criteria>

<tasks>

<task type="auto">
  <name>Task 1: Migracja SQL + Model śledzenia StatLink</name>
  <files>migrations/013_statlink_tracking.sql</files>
  <action>
    Utworzyć migrację tworzącą tabelę statlink_links:
    - id INT AUTO_INCREMENT PRIMARY KEY
    - article_id INT NOT NULL (FK do articles)
    - site_id INT NOT NULL (FK do sites)
    - statlink_id INT NULL (ID linku w systemie StatLink — parsowany z odpowiedzi)
    - anchor VARCHAR(500) NOT NULL
    - link_url VARCHAR(500) NOT NULL
    - added_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
    - expires_at DATETIME NOT NULL (added_at + 60 dni)
    - removed_at DATETIME NULL
    - status ENUM('active', 'expired', 'removed', 'failed') DEFAULT 'active'
    - error_message TEXT NULL
    - created_at DATETIME DEFAULT CURRENT_TIMESTAMP

    Indeksy: (article_id), (status, expires_at), (site_id)
  </action>
  <verify>SQL jest poprawny składniowo, tabela zawiera wszystkie kolumny</verify>
  <done>AC-5 satisfied: tabela statlink_links gotowa do śledzenia linków</done>
</task>

<task type="auto">
  <name>Task 2: StatLinkService — login, dodawanie, usuwanie linków</name>
  <files>src/Services/StatLinkService.php</files>
  <action>
    Utworzyć StatLinkService z metodami:

    1. **login()** — POST do statlink.pl z email+haslo, utrzymuj CookieJar w Guzzle
       - Sprawdź odpowiedź czy zawiera "Zalogowano"
       - Rzuć wyjątek jeśli login się nie powiedzie

    2. **addLink(array $article, string $anchor)** — dodaje link do StatLink:
       - Najpierw GET /148,twoje-linki aby pobrać token CSRF (pole "niepozwol" — regex z HTML)
       - POST do /148,twoje-linki#lista z parametrami:
         - niepozwol: token z GET
         - https: 1 (jeśli URL artykułu jest HTTPS) lub 0
         - link: URL artykułu bez protokołu (np. "domena.pl/slug")
         - anchor: tytuł artykułu lub keyword tematu (naprzemiennie)
         - fraza_kluczowa1/2/3: puste
         - ilosc_dziennie: 0.14
         - ilosc_max: 10
         - ilosc_nofollow: 0
         - statrank_min: 0, statrank_max: 10
         - id_kategorie_multiple[]: wszystkie kategorie (pobrać listę z HTML)
         - zapisz: DODAJ
       - Parsuj statlink_id z odpowiedzi (szukaj nowego ID w tabeli linków)
       - Return statlink_id lub null

    3. **removeLink(int $statlinkId)** — usuwa link ze StatLink:
       - POST do /148,twoje-linki#lista0 z statlink_id + usun=Usuń
       - Sprawdź czy usunięcie się powiodło

    4. **getExistingLinkIds()** — parsuje listę linków z /148,twoje-linki
       - Zwraca tablicę statlink_id dla weryfikacji

    5. **scrapeCategories()** — parsuje checkboxy kategorii z formularza
       - Zwraca tablicę wartości id_kategorie_multiple[] do zaznaczenia

    6. **processNewArticles()** — główna metoda:
       - Pobierz opublikowane artykuły bez wpisu w statlink_links
       - Zaloguj się do StatLink
       - Dla każdego artykułu: addLink() + zapisz do statlink_links z expires_at = NOW + 60 dni
       - Anchor naprzemiennie: tytuł artykułu / keyword tematu

    7. **removeExpiredLinks()** — główna metoda usuwania:
       - Pobierz linki z status='active' AND expires_at < NOW()
       - Zaloguj się do StatLink
       - Dla każdego: removeLink() + ustaw status='removed', removed_at=NOW()

    Użyj GuzzleHttp\Client z CookieJar.
    Loguj operacje przez Logger (kanał 'statlink').
    Odporność: try-catch per link, nie przerywaj całego batch przy błędzie jednego.
    Avoid: nie wysyłaj więcej niż 5 linków w jednym uruchomieniu crona (rate limiting).
  </action>
  <verify>Klasa kompiluje się bez błędów, metody mają prawidłowe sygnatury</verify>
  <done>AC-1, AC-2, AC-3 satisfied: serwis loguje się, dodaje i usuwa linki</done>
</task>

<task type="auto">
  <name>Task 3: Cron endpoint + integracja z routerem</name>
  <files>src/Controllers/SettingsController.php, src/Core/Router.php</files>
  <action>
    Dodać endpoint /cron/statlink w routerze (wzorowany na istniejących cron endpointach):
    - Walidacja tokenu (SEO_TRIGGER_TOKEN lub nowy STATLINK_TRIGGER_TOKEN)
    - Wywołanie StatLinkService::processNewArticles() — dodaj nowe
    - Wywołanie StatLinkService::removeExpiredLinks() — usuń wygasłe
    - Zwróć JSON z podsumowaniem (added: N, removed: N, errors: N)

    Sprawdź jak działają istniejące cron endpointy w projekcie i zastosuj ten sam wzorzec.
  </action>
  <verify>Endpoint /cron/statlink odpowiada JSON-em z podsumowaniem</verify>
  <done>AC-4 satisfied: cron endpoint do automatycznego zarządzania linkami StatLink</done>
</task>

</tasks>

<boundaries>

## DO NOT CHANGE
- src/Services/PublisherService.php (nie modyfikuj flow publikacji)
- src/Models/Article.php (nie zmieniaj istniejących metod)
- migrations/001-012 (istniejące migracje niemodyfikowalne)
- src/Services/InternalLinkService.php (osobny mechanizm linkowania)

## SCOPE LIMITS
- Ten plan NIE integruje StatLink z procesem publikacji (to osobny cron)
- Nie dodajemy UI do zarządzania StatLink w panelu backPRO (może w przyszłości)
- Nie modyfikujemy istniejących endpointów cron

</boundaries>

<verification>
Before declaring plan complete:
- [ ] Migracja 013 tworzy tabelę statlink_links
- [ ] StatLinkService loguje się do statlink.pl (test ręczny)
- [ ] StatLinkService dodaje link (test ręczny z jednym artykułem)
- [ ] StatLinkService usuwa link (test ręczny)
- [ ] Endpoint /cron/statlink zwraca JSON
- [ ] Logger zapisuje operacje na kanale 'statlink'
- [ ] Nie więcej niż 5 linków dodanych per uruchomienie crona
</verification>

<success_criteria>
- Wszystkie taski wykonane
- StatLinkService działa end-to-end (login → add → track → remove po 60 dniach)
- Endpoint cron działa z tokenem
- Brak błędów w istniejącej funkcjonalności
</success_criteria>

<output>
After completion, create `.paul/phases/01-statlink-autolinking/01-01-SUMMARY.md`
</output>
