# Specyfikacja: Lenavio Live

**Lokalizacja aplikacji:** `LenavioLive/lenavio_live_app` (w public_html)
**Lokalizacja silnika panelu:** `LenavioLive/lenavio_live_panel` → na serwerze **poza** public_html, dwa poziomy nad aplikacją
**Lokalizacja centralnego rejestru sponsorów:** `LenavioLive/lenavio_live_admin` (+ silnik
`lenavio_live_admin_engine`, poza public_html) → **osobna instalacja, na serwerze
właściciela modułu**, niezależna od instalacji partnera. Patrz sekcja 16.
**Status:** dokument specyfikacji — opisuje działający moduł, aktualizowany na bieżąco wraz ze zmianami.
**Instalacja:** patrz `INSTALACJA.md`.

---

## 1. Cel i kontekst

Lenavio Live to **platforma do przeżyć, nie moduł edukacyjny** — wtyczany moduł (embed) do umieszczenia na dowolnej stronie WWW: baner, po kliknięciu w który otwiera się modal z **interaktywną historią prowadzoną przez AI (postać "Lena")**. Użytkownik nie przychodzi „uczyć się”, tylko sprawdzić „czy dam się nabrać?” / „co bym zrobił na jego miejscu?” — edukacja jest efektem ubocznym dobrze zaprojektowanego przeżycia, nie deklarowanym celem.

Kluczowe założenia wynikające z rozmów poprzedzających tę specyfikację:

- Młody odbiorca **nie szuka edukacji** — trzeba go zaciekawić, zaskoczyć albo rzucić mu wyzwanie, a edukacja ma być **efektem ubocznym** dobrze zaprojektowanej interakcji.
- Zamiast kursu/artykułu, moduł oferuje **rozmowę z AI**, w której Lena wciela się w różne postacie (oszust, policjant, kolega, rekruter, influencer, babcia, pracownik banku), a uczeń nie wie z góry, kim ona jest — dopiero na końcu odkrywa, że to był trening.
- Wejście do modułu ma być poprzedzone **hookiem** (pytanie / szokujący fakt / wyzwanie), nie morałem.
- Moduł jest **jednym z wielu możliwych kanałów wejścia** (np. link z krótkiego filmu TikTok/Reels/Shorts prowadzi wprost do konkretnego scenariusza), więc musi dać się **linkować bezpośrednio do konkretnej historii** (deep-link), a nie tylko otwierać z banera na stronie.

Ten dokument specyfikuje **jeden, w pełni zaprojektowany scenariusz przykładowy** (patrz sekcja 7) jako wzorzec, według którego będą tworzone kolejne historie, oraz mechanikę techniczną modułu (baner, modal, integracja z AI).

### 1.1 Dwie warstwy języka: uczeń vs. sponsor/instytucja

Moduł ma **dwóch odbiorców tej samej treści, z celowo różnym słownictwem**:

- **Warstwa skierowana do ucznia** (baner, tytuły historii, tekst w modalu, ekran końcowy) — **nigdy** nie używa słów „edukacja”, „edukacyjny”, „naucz się”, „kurs”, „lekcja”. Zamiast tego mówi językiem wyzwania i ciekawości („Czy dasz się oszukać?”, „Zobacz, czy przejdziesz tę rozmowę”). To jest w całości to, co widzi uczeń, i jest już tak zaprojektowane w rejestrze historii (`hookTitle`/`hookSubtitle`).
- **Warstwa skierowana do sponsora/szkoły/ministerstwa/fundacji** (przyszła dokumentacja programu, karta scenariusza dla sponsora, raport dla instytucji finansującej — poza zakresem tej wersji, patrz sekcja 9) — świadomie **używa** słów „edukacyjny”, „program”, „cele dydaktyczne”, bo instytucje płacące za program potrzebują tego języka do uzasadnienia finansowania i raportowania. Ta warstwa nigdy nie trafia do interfejsu ucznia.

To nie jest sprzeczność, tylko świadome rozdzielenie odbiorców — ta sama historia ma dwa opisy: jeden, który ma zaciekawić nastolatka, i drugi, który ma przekonać osobę decyzyjną w instytucji.

---

## 2. Stos technologiczny

- **Frontend modułu:** czysty HTML + CSS + JavaScript (vanilla, bez frameworków, bez bundlera) — ma się dać wkleić na dowolną stronę jako mały zestaw plików (lub jeden plik `.js` ładujący resztę).
- **Silnik panelu (od wersji z panelem partnera):** PHP + SQLite, umieszczony **poza `public_html`** według wzorca MrPrompt Logic Flow Gate (`instrukcja2.md`). W katalogu aplikacji zostaje absolutne minimum PHP: bramka `gate.php`, instalator `install.php` oraz `core/bootstrap.php` + `core/config.php`. Sama aplikacja pozostaje statyczna — nie renderuje PHP-a, tylko odpytuje bramkę JSON-em (patrz sekcja 13).
- **Bez grafik/obrazków** — cały wygląd banera i modala budowany CSS-em (kolory, gradienty, cienie, typografia, animacje). **Jedyny wyjątek:** logotypy sponsorów (sekcja 4.1) — realne logo firmy czy fundacji nie da się odtworzyć CSS-em, a sponsor ma prawo do swojej identyfikacji wizualnej. Pliki logotypów wstawia ręcznie właściciel modułu, nie partner z panelu.
- **Ikony:** Font Awesome z CDN (`<link>` do `font-awesome`/`fontawesome` CDN, np. kit lub `all.min.css`) — brak lokalnych plików ikon.
- **AI:** [Groq API](https://groq.com) (LLM z bardzo szybkim czasem odpowiedzi — istotne dla płynności "rozmowy" w modalu).
- **Model kluczy API — BYOK (bring your own key):** aplikacja **nie ma własnego backendu ani własnego klucza Groq**. Każdy użytkownik modułu samodzielnie zakłada darmowe konto na [console.groq.com/keys](https://console.groq.com/keys), generuje własny klucz API i wkleja go w panelu „Ustawienia Lena AI” w modalu. Klucz jest zapisywany **wyłącznie w `localStorage` przeglądarki użytkownika** i wykorzystywany do wywołań `fetch` **bezpośrednio z przeglądarki do Groq API** — nigdzie indziej nie jest wysyłany ani przechowywany (moduł nie ma żadnego serwera pośredniczącego). To celowo czyni aplikację **w pełni statyczną** (sam HTML/CSS/JS, możliwa do umieszczenia nawet na hostingu bez PHP).

---

## 3. Architektura

```
Strona hosta (dowolna witryna)
   └── <script src=".../lenavio-live.js" data-story="nowy-w-grupie"></script>
         │
         ├── wstrzykuje w miejsce osadzenia skryptu:
         │     DWA paski logotypów sponsorów (globalni, lokalni) → baner historii
         │     → przyciski (katalog historii, panel użytkownika, panel partnera)
         │
         ├── konfiguracja i lista historii ← gate.php?r=api/config
         │        └── gate.php (public_html) → SECURE_PATH (poza public_html)
         │              ├── SQLite: konta partnerów, kolorystyka, wybór historii
         │              └── data/stories/*.json: po jednym pliku na historię
         │
         ├── sponsorzy (globalni + lokalni dla tego partnera) ← ODDZIELNY serwer:
         │     lenavio_live_admin/gate.php?r=api/sponsors&partner=<klucz>
         │     (CORS: * — wołane bezpośrednio z domeny partnera; patrz sekcja 16)
         │
         ├── po kliknięciu banera → modal z rozmową
         │     ├── pełna treść historii ← gate.php?r=api/story&id=…
         │     ├── brak klucza w localStorage → ekran „Ustawienia Lena AI”
         │     └── klucz obecny → od razu rozmowa
         │
         ├── modal → JS (fetch) → Groq API BEZPOŚREDNIO z przeglądarki
         │                              (Authorization: Bearer <klucz z localStorage>)
         │
         ├── statystyki / punkty / ustawienia użytkownika → IndexedDB przeglądarki
         └── panel partnera (modal) → gate.php?r=panel/… → silnik poza public_html
```

Rozmowa z AI nadal nie ma żadnego pośrednika: klucz Groq zostaje w przeglądarce
użytkownika, a moduł woła `api.groq.com` bezpośrednio. Backend istnieje wyłącznie
jako **warstwa zarządzania** (historie, kolorystyka, konta partnerów) — nigdy nie
przechodzi przez niego ani klucz API, ani treść rozmowy, ani dane użytkownika.

### 3.1 Struktura katalogów (`lenavio_live_app/`)

Docelowa struktura na serwerze — silnik leży **dwa poziomy nad aplikacją**:

```
katalog_domowy/
├── lenavio_live_panel/            # SILNIK — poza public_html, niedostępny przez HTTP
│   ├── bootstrap.php              # guard + ładowanie bibliotek
│   ├── api.php                    # publiczne API tylko do czytania (config, version, story)
│   ├── panel.php                  # router panelu partnera (JSON, bez HTML-a)
│   ├── install-support.php        # część instalacyjna: baza, konto admina, znacznik instalacji
│   ├── lib/
│   │   ├── messages.php           # WSZYSTKIE komunikaty serwera po polsku
│   │   ├── http.php               # jednolity kształt odpowiedzi {ok, data|message, code}
│   │   ├── db.php                 # SQLite + schemat + dziennik zdarzeń
│   │   ├── csrf.php               # sesja panelu + token CSRF
│   │   ├── auth.php               # logowanie, blokada po próbach, wymuszona zmiana hasła
│   │   ├── validate.php           # polityka haseł + ścisła walidacja pliku historii
│   │   ├── settings.php           # kolorystyka partnera, sygnatura zmian
│   │   └── story-repo.php         # repozytorium historii (pliki + metadane)
│   └── data/                      # tworzone przez instalator
│       ├── lenavio.sqlite
│       ├── installed.lock
│       └── stories/*.json         # jedna historia = jeden plik
└── public_html/
    └── lenavio_live_app/          # APLIKACJA — statyczna, plus minimum PHP
        ├── gate.php               # JEDYNY punkt wejścia PHP dla ruchu z przeglądarki
        ├── install.php            # instalator (samoblokujący się po sukcesie)
        ├── .htaccess              # reguły antyenumeracyjne
        ├── core/
        │   ├── .htaccess          # Require all denied
        │   ├── bootstrap.php      # walidacja granic w runtime + guard constant
        │   └── config.php         # generowany: SECURE_PATH, PUBLIC_PATH
        ├── assets/
        │   ├── css/
        │   │   ├── banner.css     # baner, pasek sponsorów, przyciski pod banerem
        │   │   ├── ui.css         # okna modalne, toasty, formularze, katalog, panele
        │   │   └── modal.css      # okno rozmowy + scena animacji historii
        │   ├── js/
        │   │   ├── lenavio-live.js    # punkt wejścia: ładuje moduły, montuje baner
        │   │   ├── messages.js        # WSZYSTKIE komunikaty przeglądarki po polsku
        │   │   ├── ui.js             # wspólne okna, toasty, potwierdzenia, pola
        │   │   ├── api.js            # klient bramki gate.php
        │   │   ├── stories.js        # rejestr historii z API + auto-odświeżanie
        │   │   ├── sponsors.js       # dwa paski logotypów (global/local) + wizytówka —
        │   │   │                     # dane z lenavio_live_admin (sekcja 16), nie lokalnie
        │   │   ├── rotator.js        # losowe, ale sprawiedliwe przełączanie banera
        │   │   ├── catalog.js        # katalog historii (mini banery)
        │   │   ├── user-store.js     # IndexedDB: statystyki, punkty, ustawienia
        │   │   ├── user-panel.js     # panel użytkownika + eksport/import kopii
        │   │   ├── partner-panel.js  # panel partnera (logowanie → hasło → moduły)
        │   │   ├── story-anim.js     # odtwarzacz deklaratywnych animacji historii
        │   │   ├── key-store.js      # klucz Groq API w localStorage
        │   │   ├── chat-engine.js    # rozmowa: wywołania Groq API z przeglądarki
        │   │   └── modal.js          # okno rozmowy, ekran ustawień, ekran końcowy
        │   └── stories/           # szablon + gotowe historie (źródło importu do silnika)
        │       # UWAGA: assets/sponsors/ (sponsors.json + logotypy) jest NIEUŻYWANE od
        │       # wprowadzenia centralnego rejestru (sekcja 16) — sponsorów prowadzi się
        │       # wyłącznie w lenavio_live_admin, nie lokalnym plikiem u partnera.
        ├── index.html             # strona demo/testowa (z instrukcjami, data-panel="on")
        └── osadzenie.html         # WZÓR STRONY PRODUKCYJNEJ — bez instrukcji i bez
                                   # przycisku panelu; do podmiany treści przez partnera
```

Plik `prompts.js` został usunięty — system prompt jest teraz częścią pliku historii
(patrz sekcja 14), więc scenariusze nie są już zapisane w kodzie modułu.

### 3.2 Sposób osadzenia na stronie hosta

Jedna linijka na stronie hosta:

```html
<script src="https://TWOJA-DOMENA/lenavio_live_app/assets/js/lenavio-live.js"
        data-story="nowy-w-grupie"
        data-position="inline"
        data-partner-key="TWÓJ_KLUCZ_Z_LENAVIO_ADMIN"></script>
```

- `data-story` — identyfikator scenariusza, pozwala na deep-link do konkretnej historii (np. z linku w opisie filmu TikTok). Wskazana historia pokazuje się jako pierwsza, a potem baner przechodzi do losowej rotacji.
- `data-panel="on"` — **dodaje** przycisk panelu partnera pod banerem. Domyślnie go nie ma: na stronie publicznej formularz logowania nie ma czego tam szukać. Bez tego atrybutu partner wchodzi do panelu adresem `#lenavio-panel`, własnym linkiem w stopce albo wywołaniem `LenavioPartnerPanel.open()`.
- `data-position` — `inline` (skrypt renderuje baner w miejscu swojego umieszczenia w DOM) — na tym etapie jedyny obsługiwany tryb (zgodnie z decyzją: baner jako stały pasek/kafelek w treści strony, nie floating button).
- `data-partner-key` — klucz tej instalacji w centralnym rejestrze sponsorów
  (`lenavio_live_admin`, sekcja 16). Nadaje go właściciel modułu przy zakładaniu
  partnera w panelu admina i wkleja partnerowi razem z resztą linijki. Bez tego
  atrybutu pasek sponsorów po prostu się nie pokazuje — reszta modułu działa normalnie.
- Brak `data-story` → baner startuje od losowej historii z zestawu włączonego przez partnera.

Adresy pomocnicze obsługiwane przez moduł: `#lenavio-open=<id>` (otwiera rozmowę od
razu po wejściu), `#lenavio-katalog`, `#lenavio-panel`.

---

## 4. Baner

**Wygląd:** stały blok/kafelek CSS osadzony w przepływie strony hosta (nie floating).

Elementy banera:
- Tło: gradient CSS (paleta Lenavio — do ustalenia, placeholder: ciemny fiolet → granat).
- Ikona Font Awesome (np. `fa-solid fa-comments` lub `fa-solid fa-user-secret`, zależna od scenariusza — pole `icon` w rejestrze historii).
- Tytuł hooka (np. „Czy dasz się oszukać?”) — pobierany z metadanych scenariusza, **nie** ogólna nazwa modułu.
- Krótki podtekst/CTA (np. „Kliknij i sprawdź” / „8 minut, zero nudy”).
- Subtelna animacja hover/idle (np. pulsujący cień, delikatny gradient-shift) budowana czystym CSS — ma przyciągać wzrok bez grafik.
- Cały baner jest klikalny (`role="button"`, dostępny z klawiatury — `tabindex`, `Enter`/`Space` otwiera modal).

**Responsywność:** baner skaluje się do szerokości kontenera hosta (max-width, padding relatywny), czytelny na mobile (główny kanał wejścia to social media → mobile-first).

### 4.1 Otoczenie banera

Baner nie jest już samotnym elementem — moduł wstrzykuje trzy warstwy jedna pod drugą:

1. **Nad banerem: DWA paski logotypów sponsorów, wizualnie osobne.** „Sponsorzy
   ogólni” (widoczni u wszystkich partnerów) i „Sponsorzy tej placówki” (widoczni
   tylko u partnera, któremu ich przypisano). Dane pochodzą z **centralnego rejestru
   `lenavio_live_admin`** — osobnej aplikacji na serwerze właściciela modułu (sekcja 16),
   nie z lokalnego pliku u partnera. Sponsorzy świadomie NIE są zarządzani z panelu
   partnera ani przez partnera w żaden sposób — prowadzi ich wyłącznie właściciel.
   Kliknięcie logo otwiera wizytówkę renderowaną z danych z tego rejestru (nazwa, opis,
   WWW, kontakt) — zawsze z serwera właściciela, niezależnie od tego, na jakiej stronie
   moduł wisi. Sponsor z niepustą listą historii pokazuje się tylko przy wskazanych
   historiach (sponsoring per-historia); pusta lista = widoczny przy wszystkich.
   Każdy rząd chowa się niezależnie, gdy nie ma dla niego żadnego sponsora.
2. **Baner** — jak wyżej, z treścią aktualnie wylosowanej historii.
3. **Pod banerem: jeden przycisk** — „Katalog historii” (sekcja 11).

Rozmieszczenie wejść jest celowe i wynika z tego, kto ich potrzebuje:

| Wejście | Gdzie | Dlaczego tam |
|---|---|---|
| Katalog historii | przycisk pod banerem | jedyna rzecz, której odwiedzający potrzebuje od razu |
| Panel użytkownika | ikona wykresu w nagłówku okna rozmowy + ekran końcowy | statystyki mają sens dopiero, gdy ktoś w ogóle wszedł w historię |
| Panel partnera | brak przycisku; adres `#lenavio-panel`, własny link albo `data-panel="on"` | odwiedzający stronę partnera nie ma po co widzieć formularza logowania |

**Paski sponsorów i komunikaty modułu mają własną powierzchnię kolorystyczną**
(jasny pasek z ciemnym tekstem, ciemna karta komunikatu) — moduł wisi na obcych
stronach, których tła nie znamy. Wcześniej etykieta sponsorów brała jasny kolor tekstu
modułu i na białej stronie była po prostu niewidoczna.

### 4.2 Rotacja historii — losowa, ale sprawiedliwa

Gdy włączona jest więcej niż jedna historia, baner przełącza się co kilka sekund.
Wymaganie brzmiało: **nie liniowo, losowo, ale tak, żeby dalsze historie też się
pokazywały.** Zwykłe losowanie przy każdej zmianie tego nie spełnia — jedna historia
może wyjść pięć razy pod rząd, a inna nie wyjść wcale.

Dlatego `rotator.js` używa **„talii kart” (shuffle bag)**:

- talia = wszystkie włączone historie w losowej kolejności (tasowanie Fishera–Yatesa),
- zdejmujemy po jednej z góry, więc w jednym obiegu **każda historia pokazuje się
  dokładnie raz** — żadna nie jest pomijana,
- po wyczerpaniu talii tasujemy ją od nowa, pilnując, żeby pierwsza karta nowej talii
  nie powtórzyła historii właśnie schodzącej z ekranu,
- **czas wyświetlenia też jest losowy** (4,5–9,5 s), żeby rytm nie był metronomiczny.

Sprawdzone na 5000 przełączeń przy 5 historiach: rozkład idealnie równy (po 1000),
zero powtórzeń pod rząd, każdy pięcioelementowy obieg zawierał pełny zestaw.

Rotacja zatrzymuje się, gdy: kursor jest nad banerem lub baner ma focus (żeby nie
uciekał pod kliknięciem), otwarte jest okno rozmowy, użytkownik wyłączył rotację
w swoim panelu, albo system zgłasza `prefers-reduced-motion`.

### 4.3 Własny kolor każdej historii

Sama zmiana tekstu nie wystarczała: przy jednej palecie partnera wszystkie banery
wyglądały identycznie i przełączenie było prawie niewidoczne — czyli rotacja nie robiła
tego, po co jest.

Dlatego każda historia dostaje **własny odcień**, wyliczony z jej pozycji na posortowanej
liście identyfikatorów: odcienie są rozłożone **równomiernie po całym kole barw**
(trzy historie → 0°, 120°, 240°; sześć → co 60°). Nasycenie i jasność zostają z palety
partnera, więc całość dalej wygląda jak jeden system. Kąt gradientu też się zmienia,
żeby różnica była widoczna również dla osób słabo rozróżniających barwy.

Pierwsze podejście brało odcień ze skrótu (hash) identyfikatora — odrzucone, bo dwie
historie wypadły 6° od siebie, czyli wizualnie tak samo. Skrót został tylko jako
rozwiązanie awaryjne, gdy kafelek rysuje się przed wczytaniem pełnej listy.

**Obracany jest wyłącznie gradient tła.** Kolor akcentu (napis „Kliknij”, plakietki)
zostaje taki, jaki ustawił partner: obracany razem z tłem potrafił się z nim zlać
(jasnozielony napis na zielonym banerze). Jasność gradientu jest zachowana z palety,
więc jeden stały akcent kontrastuje na każdym odcieniu.

Ten sam kolor dostają kafelki w katalogu — historia jest rozpoznawalna po barwie
w obu miejscach.

---

## 5. Modal

**Otwieranie:** klik/Enter na baner → modal fade-in + scale-in (CSS transition), tło strony przyciemnione (overlay), scroll strony zablokowany.

**Zamykanie:** przycisk „✕” (ikona FA `fa-solid fa-xmark`), klawisz `Esc`, klik w overlay poza oknem. Przy próbie zamknięcia w trakcie aktywnej historii (patrz 5.1) — potwierdzenie „Na pewno chcesz przerwać?” (żeby nie tracić przypadkiem postępu rozmowy).

**Struktura modala:**
- Nagłówek: awatar/inicjał postaci, którą aktualnie jest Lena (tekstowo/CSS, bez grafik — np. duża litera na kolorowym kole), imię/rola aktualnej postaci (może być ukryte na starcie — patrz 7).
- Obszar rozmowy: dymki wiadomości (Lena / uczeń), przewijany, autoscroll do najnowszej wiadomości.
- Pole wejścia: input tekstowy + przycisk wyślij (ikona FA `fa-solid fa-paper-plane`), ewentualnie przyciski szybkich odpowiedzi/wyborów, gdy scenariusz je definiuje (patrz 5.1).
- Wskaźnik „Lena pisze…” (animowane kropki CSS) podczas oczekiwania na odpowiedź z Groq API.
- Stopka/ekran końcowy: podsumowanie/wnioski wyświetlane po zakończeniu historii (patrz 7, faza „Ujawnienie”), z przyciskiem „Wyślij znajomemu” (patrz 5.3).

**Dostępność:** `role="dialog"`, `aria-modal="true"`, focus-trap wewnątrz modala, zwrot focusu do banera po zamknięciu.

### 5.1 Ekran „Ustawienia Lena AI” (gating przed rozmową)

Zanim uczeń może zacząć rozmowę, moduł sprawdza `key-store.js` (odczyt z `localStorage`):

- **Brak klucza:** modal otwiera się od razu na ekranie ustawień: krótkie wyjaśnienie („Lena potrzebuje darmowego klucza Groq AI, żeby z Tobą porozmawiać”), link do `https://console.groq.com/keys`, pole na klucz (typu `password` z przełącznikiem pokaż/ukryj), przycisk „Zapisz i zacznij”. Jawna informacja: „Twój klucz zostaje tylko w tej przeglądarce — nie jest wysyłany na żaden serwer poza Groq.”
- **Klucz obecny:** modal od razu przechodzi do rozmowy.
- Ikona zębatki (`fa-solid fa-gear`) w nagłówku modala pozwala w każdej chwili wrócić do ekranu ustawień (zmiana/usunięcie klucza).
- Błąd autoryzacji zwrócony przez Groq API (klucz nieprawidłowy/wygasł) przełącza modal z powrotem na ekran ustawień z komunikatem błędu — zamiast zawieszać rozmowę.

### 5.2 Mechanika rozmowy (chat-engine.js)

- Historia rozmowy trzymana w JS jako tablica wiadomości (`role: 'lena' | 'uczen'`, `content`).
- Każda wiadomość ucznia + cała dotychczasowa historia + systemowy prompt scenariusza (z `prompts.js`) wysyłane **bezpośrednio z przeglądarki** do Groq API (`https://api.groq.com/openai/v1/chat/completions`), z nagłówkiem `Authorization: Bearer <klucz z localStorage>`.
- Scenariusz definiuje też **warunki zakończenia** — model sam zwraca znacznik `[KONIEC]` w odpowiedzi, po którym następuje spersonalizowane pytanie zwrotne — po wykryciu końca modal przechodzi do ekranu „Ujawnienie” (patrz 7 faza 5).
- Opcjonalnie: przyciski szybkiego wyboru (np. „Podaj kod SMS” / „Rozłącz się”) zamiast swobodnego inputu w newralgicznych momentach — do decyzji per scenariusz, na tym etapie specyfikujemy **swobodny input tekstowy jako wariant podstawowy**.

### 5.3 „Wyślij znajomemu” — wirusowość jako wyzwanie, nie tablica wyników

Na ekranie końcowym (obok „Spróbuj innej historii”) znajduje się przycisk **„Wyślij znajomemu”**, który:

- Używa natywnego `navigator.share()` (na telefonach), a jeśli niedostępny — kopiuje do schowka gotowy tekst + link do strony hosta.
- Treść zaproszenia jest sformułowana jako **wyzwanie, nigdy jako wynik ucznia** — np. „«Ktoś właśnie dodał Cię do grupy» — sprawdź, czy zrobisz lepiej niż ja”. Moduł **nie ujawnia i nie udostępnia** publicznie, czy dany uczeń „dał się nabrać” czy nie — nie ma żadnej punktacji pokazywanej na zewnątrz.
- Uzasadnienie: przy tematach takich jak cyberprzemoc czy zdrowie psychiczne publiczne pokazanie „przegranej” mogłoby zawstydzać i działać odwrotnie do celu modułu. Wyzwanie kierowane do znajomego ma napędzać zasięg organiczny, a nie porównywanie wyników.

---

## 6. Integracja z Groq API (bezpośrednio z przeglądarki)

### 6.1 `key-store.js` — zarządzanie kluczem użytkownika

- `get()` / `set(key)` / `clear()` operujące na jednym kluczu `localStorage` (np. `lenavio_groq_api_key`).
- Minimalna walidacja formatu przed zapisem (np. niepusty string, sensowna długość) — pełna walidacja i tak następuje przy pierwszym wywołaniu API (błąd 401 → powrót do ekranu ustawień).
- Brak jakiejkolwiek transmisji klucza gdziekolwiek poza nagłówek `Authorization` requestu do `api.groq.com`.

### 6.2 `chat-engine.js` — wywołanie API

- `POST https://api.groq.com/openai/v1/chat/completions`, nagłówki: `Content-Type: application/json`, `Authorization: Bearer <klucz>`.
- Treść żądania: `{ model, temperature, messages: [systemPrompt, ...history, nowaWiadomość] }`.
- Obsługa błędów: `401/403` → komunikat + przekierowanie do ekranu ustawień; inne błędy sieci/HTTP → komunikat w dymku systemowym w czacie, rozmowa nie jest tracona.
- Brak backendowego rate-limitingu (nie ma backendu) — koszty/limity zużycia obciążają darmowy klucz samego użytkownika, więc nie są ryzykiem dla właściciela strony hosta.

### 6.3 Systemowy prompt — struktura

Każdy scenariusz w `stories.js` (metadane) ma odpowiadający mu **systemowy prompt** trzymany w `prompts.js` **po stronie klienta** (skoro nie ma backendu, prompt jest technicznie widoczny w kodzie źródłowym strony — świadomy kompromis modelu BYOK, patrz p. 10.6), który określa:
- Kim jest Lena w tej historii i jakie postacie po kolei przyjmuje (i kiedy się przełącza).
- Cel edukacyjny (czego uczeń ma doświadczyć/nauczyć się — nieujawniany wprost uczniowi, patrz 1.1).
- Ton i długość odpowiedzi (krótkie, naturalne, jak prawdziwa rozmowa — nie wykłady).
- Zasady zakończenia historii i przejścia do fazy ujawnienia/wniosków.
- Twarde ograniczenia (Lena nie wychodzi z roli, nie ujawnia że to trening przed czasem, nie generuje treści szkodliwych poza kontekstem edukacyjnym scenariusza).

**Zasada dla tematów wrażliwych** (np. zdrowie psychiczne, przemoc domowa — planowane kolejne scenariusze, patrz sekcja 7): mechanizm „niespodziewanej zmiany roli” (patrz 7, faza 3) przy tych tematach musi mieć **łagodniejsze i szybsze ujawnienie** niż w scenariuszach lżejszych (np. „portfel”, „BLIK”). Nastolatek, który akurat sam się z czymś zmaga, nie powinien zostać zaskoczony twistem, gdy postać, z którą rozmawiał (np. „psycholog”), okazuje się elementem treningu — dla tej kategorii tematów prompt powinien skracać fazę 3 i przechodzić do ujawnienia (faza 4) szybciej i cieplej niż w scenariuszach o oszustwach czy manipulacji rówieśniczej. Ta zasada dotyczy przyszłych scenariuszy — obecny wzorcowy scenariusz „Nowy w grupie” (cyberprzemoc rówieśnicza) nie wymaga tego złagodzenia.

### 6.4 Czytelne komunikaty błędów API

Surowe błędy Groq API (kody HTTP, techniczny `error.message`) są tłumaczone w `chat-engine.js` (funkcja `classifyError`) na zrozumiałe dla nastolatka komunikaty PL, zanim trafią do interfejsu:

| Sytuacja | Kod/sygnał od Groq | Komunikat w module |
|---|---|---|
| Zły/wygasły klucz | `401` / `403` | Przekierowanie do ekranu ustawień z informacją, że klucz jest nieprawidłowy. |
| Wyczerpany limit zapytań (minutowy) | `429`, komunikat bez wzmianki o dobie | „Zbyt wiele wiadomości w krótkim czasie — Twój darmowy klucz Groq ma chwilowo wyczerpany limit zapytań”, z podpowiedzią po ilu sekundach spróbować ponownie (parsowane z treści błędu). |
| Wyczerpany dzienny limit tokenów/zapytań | `429`, komunikat ze wzmianką o „day/24h” | Informacja, że limit dzienny się wyczerpał i wróci po 24h, z sugestią użycia innego klucza. |
| Za długa rozmowa (przekroczony limit tokenów modelu) | `400` + `context_length_exceeded` / fraza o kontekście w treści błędu | Sugestia zamknięcia i rozpoczęcia historii od nowa. |
| Model chwilowo niedostępny | `404` / `model_not_found` | Informacja, że wybrany model AI jest niedostępny, spróbuj później. |
| Awaria po stronie Groq | `5xx` | Informacja, że to chwilowa awaria serwerów Groq, nie wina klucza użytkownika. |
| Brak połączenia internetowego | błąd sieciowy `fetch` (przed otrzymaniem odpowiedzi) | Informacja o sprawdzeniu połączenia z internetem. |
| Inny/nieznany błąd | pozostałe kody | Komunikat ogólny z treścią zwróconą przez Groq API (jeśli dostępna). |

Błędy autoryzacji przełączają modal na ekran ustawień (`renderSettingsScreen`); pozostałe błędy pojawiają się jako wiadomość systemowa w dymku czatu, bez utraty dotychczasowej rozmowy — uczeń może spróbować wysłać wiadomość ponownie.

---

## 7. Przykładowy scenariusz (wzorcowy, w pełni zaprojektowany)

**Temat:** Cyberprzemoc / manipulacja online — pod roboczym tytułem **„Nowy w grupie”**.
Wybrany jako wzorzec, bo dobrze pokazuje metodę: hook → wcielanie się w różne postacie → decyzje ucznia → konsekwencje → ujawnienie → wnioski własne ucznia (bez moralizowania).

### Metadane (`stories.js`)
```
id: "nowy-w-grupie"
icon: "fa-solid fa-user-group"
hookTitle: "Ktoś właśnie dodał Cię do grupy. Wejdziesz?"
hookSubtitle: "Sprawdź, jak długo wytrzymasz zanim się zorientujesz."
tags: ["cyberprzemoc", "manipulacja", "social media"]
estimatedMinutes: 8
```

### Przebieg (fazy, prowadzone przez system prompt, nie sztywny skrypt — model improwizuje w ramach roli):

1. **Hook (przed otwarciem modala, na banerze):** „Ktoś właśnie dodał Cię do grupy. Wejdziesz?” — ciekawość, nie ostrzeżenie.

2. **Faza 1 — „Kolega z klasy” (Lena jako rówieśnik, neutralny ton):**
   Lena (jako „Kuba”) pisze jakby przez komunikator: wciąga ucznia w nową grupę znajomych, zadaje niewinne pytania, buduje zaufanie. Uczeń odpowiada swobodnie.

3. **Faza 2 — eskalacja (Lena wciąż jako „Kuba”, ale ton się zmienia):**
   Pojawia się presja grupy: „ktoś w grupie” zaczyna wyśmiewać inną osobę, „Kuba” zachęca ucznia, żeby też coś napisał / polubił / przesłał dalej. System prompt pilnuje, żeby AI reagowało realistycznie na to, co zrobi uczeń (dołączy do żartów / zignoruje / sprzeciwi się) — **bez z góry napisanej ścieżki**, model naprawdę odpowiada na wybór ucznia.

4. **Faza 3 — konsekwencja (zmiana postaci, bez zapowiedzi):**
   Lena zmienia się w „ofiarę” (osobę, która była wyśmiewana) i pisze bezpośrednio do ucznia — pokazując, jak wyglądała ta sytuacja z drugiej strony, z uwzględnieniem tego, co konkretnie uczeń zrobił/napisał w fazie 2 (personalizacja na podstawie historii rozmowy).

5. **Faza 4 — Ujawnienie:**
   Lena wychodzi z roli, mówi wprost: „To był trening. Rozmawiałaś/eś z AI grającym rolę Kuby i osoby, którą wyśmiewano.” Modal przechodzi na ekran podsumowania.

6. **Faza 5 — Wnioski (ekran końcowy, nie czat):**
   Zamiast gotowego morału — **pytania zwrotne do ucznia** wygenerowane na podstawie przebiegu jego konkretnej rozmowy (np. „W którym momencie mogłaś/eś zareagować inaczej?”), plus krótkie realne fakty o cyberprzemocy (2–3 zdania, dane/statystyka — treść statyczna, nie generowana przez AI, żeby mieć pewność co do rzetelności faktów).

Ten sam szkielet (hook → rola 1 → eskalacja/decyzje → zmiana roli/konsekwencja → ujawnienie → wnioski) jest **wzorcem do klonowania** dla kolejnych tematów wymienionych w rozmowie źródłowej: bezpieczeństwo dzieci, fake news, zdrowie psychiczne, pierwsza pomoc, ekologia, szanuj seniora.

---

## 8. Rejestr historii (`stories.js`) — kontrakt danych

**Rozstrzygnięte:** rejestr nie jest już hardkodowany — pobiera dane z bramki
(`gate.php?r=api/config`). `stories.js` jest teraz warstwą dostępu, nie źródłem treści.

Rejestr dostaje z serwera **tylko metadane** potrzebne do banera i katalogu:

- `id`, `icon` (klasa Font Awesome), `hookTitle`, `hookSubtitle`, `badge`, `tags`,
  `estimatedMinutes`, `hasAnimation`

Pełną treść (`systemPrompt`, `closingFacts`, `animation`) moduł dociąga dopiero przy
otwarciu konkretnej historii (`api/story&id=…`) — start modułu pozostaje lekki, a
system prompty nie latają w każdym żądaniu listy.

Rejestr odpowiada też za **automatyczną aktualizację**: odpytuje lekką trasę
`api/version` (sygnatura konfiguracji: identyfikatory + czasy modyfikacji historii +
znacznik zmiany ustawień partnera) co 45 sekund oraz przy powrocie do karty. Gdy
sygnatura się zmieni, przeciąga świeże dane i powiadamia baner, katalog i motyw.
Po akcji w panelu partnera (upload, przełączenie historii, zmiana kolorów) odświeżenie
następuje natychmiast, bez czekania na cykl.

Kontrakt pliku historii (jeden plik = jedna historia) opisuje sekcja 14.

---

## 9. Poza zakresem tej specyfikacji (świadomie odłożone)

**Zrealizowane od pierwszej wersji tej specyfikacji** (przeniesione tu z listy odłożonych):

- **Sponsoring per-historia i per-partner** — zrealizowany w pełni: centralny rejestr
  `lenavio_live_admin` (sekcja 16) rozróżnia sponsorów globalnych (widoczni u wszystkich
  partnerów) i lokalnych (widoczni tylko u jednego partnera), z niezależnym
  przypisaniem do konkretnych historii i wizytówką renderowaną zawsze z serwera
  właściciela (sekcja 4.1). Poza zakresem nadal: raportowanie dla sponsora i biling.
- **Panel/CMS do dodawania historii bez edycji kodu** — zrealizowany jako panel partnera
  (sekcja 13): jedna historia = jeden plik `.json` wgrywany przez przeglądarkę.
- **Analityka/statystyki** — zrealizowane po stronie użytkownika (sekcja 12), świadomie
  bez zbierania czegokolwiek na serwerze. Raportowanie zbiorcze dla sponsorów to nadal
  osobny temat i wymagałoby zgód oraz backendu, który przechowuje dane ludzi.

**Nadal poza zakresem:**

- Raportowanie dla sponsorów i biling (patrz wyżej).
- Automatyczne generowanie hooków/materiałów do TikToka/Shorts (opisane w rozmowie źródłowej jako kanał dotarcia, ale to osobny produkt/proces, nie część modułu embed).
- Warianty banera inne niż „stały pasek/kafelek” (np. floating button) — odłożone na wniosek z rozmowy doprecyzowującej.
- Poziomy zanurzenia dłuższe niż pojedyncza historia (seria odcinków, alternatywne zakończenia). Częściowo odblokowane: postęp użytkownika jest już trwały w IndexedDB (sekcja 12), więc seria odcinków przestała być architektonicznie niemożliwa — brakuje samej mechaniki scenariuszowej.
- Wersja symulacji Leny bez klucza Groq (wybory zamiast swobodnej rozmowy).
- Rywalizacja/porównywanie wyników między uczestnikami — punktacja jest dziś wyłącznie prywatna, w przeglądarce (i zgodnie z p. 5.3 nic nie wychodzi na zewnątrz).
- Wielojęzyczność.

---

## 10. Otwarte pytania / decyzje

1. Dokładna paleta kolorów/branding Lenavio (gradient, typografia) — placeholder do zastąpienia.
2. Model Groq: **rozstrzygnięte** — domyślnie `openai/gpt-oss-120b`.
3. Format znacznika końca historii: **rozstrzygnięte** — model kończy ostatnią wiadomość znacznikiem `[KONIEC]`, po którym w tej samej odpowiedzi następuje spersonalizowane pytanie zwrotne; `chat-engine.js` dzieli tekst po tym znaczniku.
4. Backend/sesja: **rozstrzygnięte** — cała historia rozmowy trzymana w pamięci JS modala (nie w `localStorage`) i wysyłana w całości przy każdym zapytaniu do Groq API; znika po zamknięciu modala. Backend zarządzający (sekcja 13) **nigdy** nie widzi treści rozmowy. Trwały jest tylko *fakt* przejścia historii — w IndexedDB przeglądarki użytkownika (sekcja 12), nie na serwerze.
5. Limity nadużyć: **rozstrzygnięte jako nieistotne dla właściciela strony** — klucz i limity zużycia są po stronie każdego użytkownika (jego własny darmowy klucz Groq), więc nie ma ryzyka kosztowego dla hosta strony. Można rozważyć w przyszłości miękki limit liczby wiadomości w jednej sesji modala jako ochronę UX (nie kosztową).
6. **Ryzyko modelu BYOK:** systemowy prompt (w tym instrukcje scenariusza) jest technicznie widoczny w kodzie źródłowym strony (`prompts.js`), bo nie ma serwera, który mógłby go ukryć. Świadomie akceptowane na tym etapie — priorytetem jest zerowa infrastruktura serwerowa i brak kosztów po stronie właściciela modułu.
7. **Ryzyko CORS:** wywołania idą bezpośrednio z przeglądarki do `api.groq.com` — zakładamy, że Groq API dopuszcza takie wywołania (nagłówki CORS po stronie Groq). Do zweryfikowania empirycznie przy pierwszym teście z realnym kluczem; jeśli Groq zablokuje żądania przeglądarkowe, jedynym wyjściem będzie jednak minimalny serwerowy proxy (bez trzymania klucza — przekazujący go 1:1 z nagłówka klienta).
8. **Napięcie „moduł statyczny” vs. „platforma”: rozstrzygnięte przez rozdzielenie warstw.** Przewidywanie z pierwszej wersji się potwierdziło — warstwa zarządzania wymagała backendu. Rozwiązanie: backend obsługuje **wyłącznie** zarządzanie (historie, kolorystyka, konta partnerów) i leży poza `public_html`, a w katalogu aplikacji zostaje minimum PHP (bramka + instalator + bootstrap/config). Rozmowa z AI, klucz API i dane użytkownika pozostają tam, gdzie były: w przeglądarce. Moduł nie jest już „w 100% statyczny”, ale **statyczny w tej części, która dotyczy człowieka**.

9. **Dane użytkownika bez konta: rozstrzygnięte, z jawnym kosztem.** Statystyki i punkty w IndexedDB dają trwały postęp bez logowania i bez przetwarzania danych osobowych na serwerze — ceną jest to, że dane giną razem z pamięcią przeglądarki. Dlatego panel użytkownika ma eksport/import kopii i ostrzeżenie sformułowane bez owijania w bawełnę (sekcja 12). Alternatywa (konta użytkowników) była odrzucona świadomie: przy odbiorcy nastoletnim konto to zgody, RODO i realna odpowiedzialność za dane, a korzyść byłaby marginalna.

10. **Rywalizacja między uczestnikami — otwarte.** Punktacja istnieje, ale jest prywatna. Każda forma tablicy wyników zderza się z zasadą z p. 5.3 (nie pokazujemy publicznie, kto „dał się nabrać”). Możliwy kierunek: rywalizacja na *aktywności* (liczba ukończonych historii, seria dni), nigdy na *wynikach* w konkretnej rozmowie — do decyzji.

---

## 11. Katalog historii

Przycisk „Katalog historii” pod banerem otwiera okno modalne z **mini banerami**
wszystkich historii włączonych przez partnera. Kafelek powtarza treść banera (ikona,
hook, podtytuł, plakietka czasu, tagi) w tym samym gradiencie, plus **stan z panelu
użytkownika**: „Ukończona” albo „Rozpoczęta” — żeby było widać, co jeszcze zostało.

- Kliknięcie kafelka zamyka katalog, ustawia tę historię na banerze i otwiera rozmowę.
- Katalog nasłuchuje zmian rejestru: gdy partner wgra albo wyłączy historię, lista
  przerysowuje się przy otwartym oknie, bez odświeżania strony.
- Pusty katalog (partner nie włączył żadnej historii) pokazuje komunikat aplikacyjny,
  nie pustkę.

## 12. Panel użytkownika (IndexedDB)

Przycisk „Twój panel” otwiera okno modalne z czterema blokami.

**Statystyki i punktacja.** Punkty (duży kafel), ukończone i rozpoczęte historie,
wysłane wiadomości, minuty rozmów, aktualna i najlepsza seria dni.

Punktacja (`user-store.js`):

| Zdarzenie | Punkty |
|---|---|
| pierwsze ukończenie danej historii | +25 |
| kolejne przejście tej samej historii | +5 |
| wysłana wiadomość w rozmowie | +1, maks. 30 na historię |
| wejście kolejnego dnia pod rząd | +10 |

Limit punktów za wiadomości jest celowy: nagradzamy udział w rozmowie, nie zasypywanie
czatu pustymi wiadomościami.

**Odznaki.** Sześć odznak (pierwsza historia, trzy historie, cały katalog, 50 wiadomości,
seria trzech dni, zrobiona kopia zapasowa). Zdobycie odznaki pokazuje się powiadomieniem
w chwili ukończenia historii.

**Twoje przejścia.** Lista historii z liczbą wiadomości, czasem i datą.

**Ustawienia.** Przełączanie banerów, ograniczenie animacji, pytanie przed przerwaniem
rozmowy, własny podpis. Zmiany działają natychmiast (np. wyłączenie rotacji zatrzymuje
baner od razu).

**Kopia zapasowa — z jawnym ostrzeżeniem.** Dane są **wyłącznie** w IndexedDB tej
przeglądarki. Panel mówi to wprost: wyczyszczenie danych przeglądarki, tryb prywatny albo
zmiana urządzenia oznacza bezpowrotną utratę postępu, a brak kopii to realne ryzyko.
Do tego: data ostatniej kopii (albo „nigdy — zrób ją teraz”), eksport do pliku
`lenavio-live-kopia-RRRR-MM-DD.json`, import z pliku (z potwierdzeniem, bo nadpisuje)
i czyszczenie danych (z potwierdzeniem).

**Odporność.** Gdy IndexedDB jest niedostępne albo nie odpowiada (limit 4 s na operację),
moduł przechodzi na magazyn w pamięci, mówi o tym użytkownikowi i **działa dalej** —
baner, katalog i rozmowa nie zależą od magazynu danych.

## 13. Panel partnera i silnik poza `public_html`

### 13.1 Kolejność ekranów — nie do obejścia

Panel otwiera się jako modal, tak samo jak sama aplikacja, i prowadzi przez trzy ekrany:

1. **Logowanie.** Konto startowe: `admin`. Blokada konta na 15 minut po 5 nieudanych
   próbach. Każdy komunikat pochodzi z aplikacji — nigdy z przeglądarki.
2. **Wymuszona zmiana hasła.** Dopóki `must_change_password = 1`, front **nie renderuje
   żadnego modułu panelu**, a serwer odrzuca każdą inną trasę kodem
   `must_change_password`. To dwie niezależne warstwy tej samej reguły — obejście
   interfejsu nie daje dostępu do danych. Wymagania nowego hasła (min. 8 znaków, mała
   i wielka litera, cyfra, znak specjalny) pokazują się jako lista odhaczana na żywo
   podczas pisania.
3. **Panel** — trzy zakładki: Kolorystyka, Historie, Nowa historia.

**Model wdrożenia: jedna instalacja = jeden partner.** Właściciel modułu przekazuje
partnerowi całą aplikację razem z silnikiem; partner stawia ją u siebie i zarządza
**wyłącznie swoją instalacją**. Dlatego panel świadomie **nie ma zarządzania kontami** —
konto zakłada instalator, raz, i nie da się z panelu utworzyć ani usunąć żadnego innego.
Wynika z tego też, że moduł nie musi wskazywać, czyją konfigurację czyta: jest tylko jedna.

### 13.2 Zakładki

- **Kolorystyka** — pięć kolorów (początek i koniec gradientu, akcent, tło okna rozmowy,
  kolor tekstu) z podglądem. Zapis przekłada je na zmienne CSS całego modułu, więc
  partner zmienia wygląd bez dotykania CSS-a.
- **Historie** — lista wszystkich wgranych historii z przełącznikiem „Pokazywana /
  Ukryta” i usuwaniem (z potwierdzeniem). Wyłączona historia znika z banera, z katalogu
  **i z deep-linku** — nie da się jej otworzyć adresem.
- **Nowa historia** — dwie drogi dodania historii:
  - **upload** jednego pliku `.json` z dysku (limit 256 KB), z linkiem do szablonu;
  - **import z katalogu aplikacji** — jednym kliknięciem zaciąga wszystkie pliki
    z `assets/stories/` leżące już na serwerze. Bez tego trzeba by wskazywać po kolei
    w okienku wyboru pliku coś, co leży dwa katalogi dalej na tym samym dysku.
    Instalator wykonuje ten import automatycznie, więc **świeżo zainstalowany moduł
    ma już historie** i nie wpada w martwy punkt „pusty moduł, żadnej treści”.

  Obie drogi przechodzą **tę samą walidację** — katalog publiczny nie jest traktowany
  jako zaufane źródło. Import pomija szablony (znacznik `_szablon` albo nazwa pliku
  zaczynająca się od `szablon-`) i jest idempotentny: plik z tym samym `id` nadpisuje
  poprzednią wersję historii.

  Po udanym wgraniu lub imporcie rejestr odświeża się natychmiast: baner i katalog
  aktualizują się same, bez odświeżania strony.

  Katalog aplikacji jest przekazywany silnikowi jako stała `APP_PATH` definiowana
  w `gate.php` (`__DIR__`) — folder w `public_html` może się nazywać dowolnie
  (np. `live`), a silnik tego nie zgaduje.

**Odzyskiwanie dostępu.** Skoro nie ma konta nadrzędnego, nie ma też komu zresetować
hasła partnera. Zapomniane hasło odzyskuje się przez ponowną instalację z czystą bazą —
procedura w `INSTALACJA.md` (p. 8). Świadomy koszt modelu „jedna instalacja = jeden
partner”: alternatywą byłoby konto serwisowe u właściciela modułu, czyli dostęp z zewnątrz
do instalacji partnera.

### 13.3 Architektura bezpieczeństwa (Logic Flow Gate)

Wdrożone według `instrukcja2.md`, z ośmioma warstwami:

| # | Warstwa | Gdzie |
|---|---|---|
| 1 | Front controller — jedyny punkt wejścia PHP | `gate.php` |
| 2 | Silnik i baza poza `DOCUMENT_ROOT` | `install.php`, `core/config.php` |
| 3 | Blokada HTTP katalogu wewnętrznego | `core/.htaccess` |
| 4 | Guard constant — pliki odmawiają pracy bez stałej wejściowej | `core/bootstrap.php`, każdy plik silnika |
| 5 | Walidacja granic przy instalacji **i** w runtime | `install.php`, `core/bootstrap.php` |
| 6 | Self-lock instalatora po sukcesie (403) | `install.php` |
| 7 | Self-test blokady HTTP wykonany przez instalator | `install.php` |
| 8 | Anty-enumeracja (`Options -Indexes`, pliki kropkowe i backupy) | `.htaccess` aplikacji |

Ścieżki nigdy nie są zapisane na sztywno — katalog silnika jest wyliczany jako
`dirname(realpath($_SERVER['DOCUMENT_ROOT']))` + nazwa podana w instalatorze, i
weryfikowany, że faktycznie leży poza katalogiem publicznym. Aplikacja odwołuje się
do niego wyłącznie przez stałą `SECURE_PATH`, nigdy przez `../`.

**Zasięg tras.** `api/*` jest publiczne i tylko do czytania (CORS `*`), bo moduł może
wisieć na dowolnej stronie hosta. `panel/*` **nie wysyła żadnych nagłówków CORS** —
działa wyłącznie w ramach tej samej domeny, żeby sesja partnera nie mogła być prowadzona
z obcej witryny. Każde żądanie zmieniające stan wymaga POST-a z tokenem CSRF
w nagłówku `X-Lenavio-Csrf`.

### 13.4 Dane w SQLite

`partners` (konto tej instalacji: hasło, wymuszona zmiana hasła, licznik nieudanych
prób), `settings` (kolorystyka + znacznik zmian), `stories` (metadane wgranych historii),
`partner_stories` (które historie są pokazywane), `audit_log` (logowania, zmiany haseł,
uploady, usunięcia). Baza leży w `SECURE_PATH/data/` — poza `public_html`, więc nie
wymaga ochrony URL-owej.

Tabele `partners` i `partner_stories` mają klucz partnera, choć wiersz jest zawsze jeden.
Zostawione świadomie: nie komplikuje niczego, a zdejmowanie klucza wymagałoby migracji
działających instalacji bez żadnego zysku funkcjonalnego.

## 14. Format pliku historii (`lenavio.story/1`)

**Jedna historia = jeden plik `.json`** — jedno wskazanie pliku w panelu i historia jest
w module. Plik zawiera wszystko: metadane, hook, system prompt, fakty na ekran końcowy
i opcjonalną animację.

```
schema           "lenavio.story/1"                (wymagane)
id               ^[a-z0-9-]{3,64}$                (wymagane; ten sam id nadpisuje historię)
hookTitle        do 160 znaków                    (wymagane)
systemPrompt     do 20 000 znaków                 (wymagane)
hookSubtitle     do 240 znaków
icon             klasa Font Awesome (^fa-…)
badge            do 24 znaków
tags             do 8 pozycji, po 40 znaków
estimatedMinutes 0–240
closingFacts     do 8 pozycji, po 500 znaków
animation        opis scen (patrz 14.1)
```

Pola zaczynające się od podkreślnika są ignorowane — służą na notatki w pliku
(JSON nie ma komentarzy). Serwer zapisuje **wersję znormalizowaną**, nie surowy plik
partnera: do modułu trafia wyłącznie treść, która przeszła walidację.

### 14.1 Animacje bez kodu

Animacja historii jest **opisem danych, nie kodem**. Plik nie może zawierać CSS-a ani
JS-a — podaje warstwy (`text`, `bubble`, `icon`, `shape`, `badge`) i klatki, w których
wolno użyć wyłącznie: `opacity`, `x`, `y`, `scale`, `rotate`, `blur`, `color`,
`background` (plus `at` — czas klatki w milisekundach).

Wszystko poza tą białą listą serwer odrzuca z komunikatem wskazującym winną właściwość,
kolory muszą być poprawnym `#hex`, ikony muszą pasować do wzorca Font Awesome, a teksty
warstw są renderowane przez `textContent`, nigdy jako HTML. `story-anim.js` przelicza
klatki na Web Animations API.

Powód takiego wyboru: upload historii dostaje partner, a partner nie musi być zaufany
tak jak właściciel modułu. Gdyby plik mógł nieść CSS/JS, każdy partner z prawem uploadu
mógłby wykonać dowolny kod na każdej stronie, na której wisi moduł. Kosztem jest mniejsza
swoboda animacji — świadomy kompromis.

Przy `prefers-reduced-motion` albo wyłączonych animacjach w panelu użytkownika scena
renderuje się statycznie (ostatni stan klatek).

### 14.2 Materiały w repozytorium

W `assets/stories/` leżą: `szablon-historii.json` (instrukcja w polach `_jakUzyc`,
znacznik `_szablon: true`) oraz trzy gotowe historie — `nowy-w-grupie` (wzorcowy
scenariusz z sekcji 7), `oddasz-kod-blik` (oszustwo finansowe) i
`pewne-jak-w-internecie` (dezinformacja). Trzy historie wystarczają, żeby rotacja
banera i katalog miały sens.

Ten katalog jest **źródłem importu** (p. 13.2), nie rejestrem: moduł czyta historie
wyłącznie z silnika poza `public_html`. Po zaimportowaniu pliki można z serwera usunąć —
zostają tylko jako materiał do ponownego wgrania.

## 15. Komunikaty — zasada „wszystko z aplikacji”

Moduł **nigdy** nie pokazuje okna przeglądarki ani błędu systemowego. Nie używamy
`alert()`, `confirm()`, `prompt()` ani natywnej walidacji formularzy (`required`,
`pattern`). Zamiast tego:

- potwierdzenia i pytania → własne okna modalne (`ui.js`),
- powiadomienia → własne toasty,
- błędy pól → komunikat pod polem,
- wybór pliku → własny przycisk i ukryty `input[type=file]`.

Teksty mają dokładnie dwa źródła: `assets/js/messages.js` (przeglądarka) i
`lib/messages.php` (serwer). Surowe błędy PHP, SQLite i Groq API są tłumaczone na
zrozumiały polski, zanim dotrą do interfejsu (dla Groq — tabela w p. 6.4). Zasada
„zero natywnych okien przeglądarki” dotyczy **modułu embed** (tego, co widzi uczeń
i partner na swojej stronie) — `lenavio_live_admin` (sekcja 16) jest osobnym,
wewnętrznym narzędziem właściciela, nieembedowanym na żadnej stronie hosta, i
świadomie korzysta z natywnego `confirm()`/`alert()` tam, gdzie to najprostsze
(np. potwierdzenie usunięcia sponsora) — to narzędzie robocze, nie część produktu
pokazywana odbiorcy końcowemu.

---

## 16. Centralny rejestr sponsorów (`lenavio_live_admin`)

### 16.1 Dlaczego osobna aplikacja, na osobnym serwerze

Model wdrożenia modułu to „jedna instalacja `lenavio_live_app` = jeden partner”
(sekcja 13.1) — każdy partner stawia całą aplikację (razem z silnikiem
`lenavio_live_panel`) **na swoim własnym serwerze**. To oznacza, że właściciel modułu
**nie ma stałego dostępu** do żadnej z tych instalacji po wdrożeniu (bez FTP/SSH do
każdej z osobna), a jednocześnie chce zachować **wyłączną kuratelę** nad tym, którzy
sponsorzy się pokazują — partnerzy świadomie nie mają tu żadnego wpływu (patrz 4.1,
13.1). Rozwiązaniem jest odwrócenie kierunku zapytania: zamiast czytać sponsorów
z pliku leżącego u partnera, każda instalacja `lenavio_live_app` **pyta w czasie
działania** centralny serwer właściciela — dokładnie tak, jak działają sieci
reklamowe/widżety: kreacja (tu: dane i logo sponsora) leży u dostawcy, strona-host
tylko ją renderuje.

Dodatkowy wymóg wynikający z rozmów: część historii jest **wspólna** między
partnerami, ale niektóre są **unikalne** dla jednego partnera, a sponsorzy dzielą się
analogicznie na **globalnych** (widoczni u wszystkich) i **lokalnych** (widoczni tylko
u jednego, wskazanego partnera) — oba rodzaje muszą pokazywać się jednocześnie,
w dwóch osobnych rzędach (decyzja: wizualnie rozdzielone, nie jedna łączona lista).

### 16.2 Struktura katalogów

Ten sam wzorzec bezpieczeństwa co `lenavio_live_app`/`lenavio_live_panel` (Logic Flow
Gate, sekcja 13.3) — mimo że to serwer, którym właściciel sam zarządza, dane
kontaktowe sponsorów i hasło administratora zasługują na tę samą warstwę ochrony:

```
katalog_domowy_właściciela/
├── lenavio_live_admin_engine/     # SILNIK — poza public_html
│   ├── bootstrap.php
│   ├── api.php                    # PUBLICZNE, tylko GET, CORS: * — dla lenavio_live_app
│   ├── admin.php                  # session-authed, CSRF, bez CORS — dla panelu admina
│   ├── install-support.php
│   ├── lib/
│   │   ├── http.php, messages.php, db.php, csrf.php, auth.php, validate.php
│   │   ├── partner-repo.php       # CRUD partnerów-klientów (instalacji, którym
│   │   │                          # przypisuje się sponsorów) + generowanie klucza API
│   │   └── sponsor-repo.php       # CRUD sponsorów, mapowanie sponsor↔historia,
│   │                              # zapis/usuwanie plików logo
│   └── data/
│       ├── lenavio-admin.sqlite
│       └── installed.lock
└── public_html/
    └── lenavio_live_admin/         # APLIKACJA — bramka + panel + logotypy
        ├── gate.php                # JEDYNY punkt wejścia PHP
        ├── install.php             # instalator (samoblokujący się po sukcesie)
        ├── core/{bootstrap.php,config.php}
        ├── index.html              # panel admina — SPA (assets/js/admin.js)
        ├── assets/{css,js}/admin.*
        └── uploads/sponsors/       # logotypy — MUSZĄ być publiczne (serwowane
                                     # bezpośrednio przez webserver, <img src>)
```

Jedyna świadoma różnica względem `lenavio_live_panel`: logotypy sponsorów muszą być
osiągalne przez HTTP (moduł partnera renderuje je jako `<img src>`), więc — inaczej
niż baza danych — leżą w katalogu **publicznym** aplikacji (`uploads/sponsors/`), nie
w `SECURE_PATH`. Silnik zapisuje tam pliki przez stałą `APP_PATH`, dokładnie tym samym
mechanizmem, jakim `lenavio_live_panel` sięga do `assets/stories/` przy imporcie
(sekcja 13.2).

### 16.3 Dane w SQLite

- `admins` — jedno konto właściciela (analogicznie do `partners` w panelu partnera:
  blokada po nieudanych próbach, wymuszona zmiana hasła startowego).
- `client_partners` — rekordy instalacji-klientów, którym przypisuje się sponsorów:
  nazwa, domena (informacyjnie), **klucz API** (losowy token — to jest wartość
  wklejana partnerowi jako `data-partner-key`), status aktywny/nieaktywny.
- `sponsors` — name, claim, description, www, email, phone, address, plik logo,
  `scope` (`global`/`local`), `partner_id` (wymagany przy `local`, `NULL` przy
  `global`).
- `sponsor_stories` — mapowanie sponsor↔historia (id historii jako tekst, ten sam
  identyfikator, którego partner używa lokalnie w pliku historii); brak wierszy dla
  danego sponsora = widoczny przy wszystkich historiach.
- `audit_log` — logowania, zmiany haseł, operacje na partnerach i sponsorach.

### 16.4 Publiczne API (`api/sponsors`)

`GET lenavio_live_admin/gate.php?r=api/sponsors&partner=<klucz>` — bez sesji, bez
CSRF, CORS `*` (ten sam wzorzec co `api/config`/`api/story` w `lenavio_live_panel`,
sekcja 13.3). Zwraca **wszystkich** sponsorów widocznych dla danego partnera (globalni
+ jego lokalni), każdy z polem `stories` (lista id historii; puste = wszystkie) i
gotowym, absolutnym URL-em logo. **Celowo jedno zapytanie na wejście strony, nie na
każde przełączenie banera** — filtrowanie po konkretnej, aktualnie pokazywanej
historii robi `sponsors.js` lokalnie w przeglądarce (tak jak dawniej robił to
`forStory()` czytający lokalny plik), bo `rotator.js` (sekcja 4.2) przełącza baner co
kilka sekund u każdego odwiedzającego, u każdego partnera — odpytywanie centralnego
serwera przy każdej rotacji byłoby niepotrzebnym kosztem transferu, bez żadnej
korzyści (dane i tak trzeba by cache'ować identycznie).

Nieznany albo nieaktywny klucz partnera → `404 partner_unknown`, bez ujawniania
żadnych dodatkowych informacji. Logotypy są serwowane jako zwykłe pliki statyczne
(`uploads/sponsors/`), więc przeglądarka partnera cache'uje je normalnie —
niezależnie od odpowiedzi JSON.

### 16.5 Panel admina (`admin/*`)

Session-authed, CSRF na każdej zmianie stanu, **bez nagłówków CORS** (działa
wyłącznie z domeny, na której stoi ta instalacja — ten sam powód co `panel/*`
w `lenavio_live_panel`, sekcja 13.3). W przeciwieństwie do panelu partnera (nakładka
wstrzykiwana na cudzą stronę hosta) to samodzielna strona (`index.html` +
`assets/js/admin.js`, SPA bez frameworka) — właściciel wchodzi na nią bezpośrednio,
nie przez osadzony moduł.

Dwie zakładki:

- **Partnerzy** — lista instalacji-klientów z ich kluczem API (do wklejenia jako
  `data-partner-key`), przełącznik aktywny/nieaktywny, generowanie nowego klucza
  (unieważnia poprzedni — partner musi zaktualizować kod osadzenia).
- **Sponsorzy** — lista wszystkich sponsorów z podglądem logo, zasięgiem
  (globalny / lokalny + nazwa partnera) i przypisanymi historiami; formularz
  dodawania/edycji (dane kontaktowe, zasięg, wybór partnera dla `local`, lista id
  historii, upload pliku logo — PNG/JPG/SVG, limit 512 KB).

Upload logo jest walidowany od zera (w kodzie nie było wcześniej precedensu na obrazki
— por. `story-repo.php`, który waliduje tylko `.json`/`.html`): biała lista rozszerzeń,
limit rozmiaru, `getimagesize()` dla plików rastrowych, a dla SVG — odrzucenie plików
zawierających `<script>` albo atrybuty `on*` (dodatkowa warstwa ostrożności; logo i tak
jest renderowane wyłącznie przez `<img src>` w kliencie, nigdy inline, więc ewentualny
skrypt by się i tak nie wykonał).

### 16.6 Relacja z `lenavio_live_app`

`sponsors.js` woła centralny endpoint zamiast czytać lokalny plik, z kluczem
odczytanym z atrybutu `data-partner-key` na tagu `<script>` (sekcja 3.2). Adres
centralnego rejestru jest stałą (`ADMIN_SPONSORS_ENDPOINT`) w `sponsors.js` — ta sama
wartość dla każdej instalacji partnera, ustawiana raz przez właściciela przy
wdrażaniu `lenavio_live_admin`. Brak klucza albo awaria/niedostępność rejestru **nie
jest awarią modułu** — pasek sponsorów po prostu się nie pokazuje, reszta (baner,
katalog, rozmowa z AI) działa normalnie, dokładnie jak przy dawnym, lokalnym pliku.

Dokładna instrukcja instalacji i codziennego użytkowania (dodawanie partnerów,
sponsorów, podpinanie klucza do instalacji partnera) — patrz `INSTALACJA.md`.
