# Specyfikacja LenavioWeb

Data ostatniej aktualizacji: 2026-07-21.

Ten dokument opisuje całość projektu LenavioWeb w obecnym stanie: aplikację
kliencką (PWA), serwerowy silnik (Human Review Loop, Komunikator 1:1) oraz
system kont użytkowników i panel administratora — wszystko zbudowane zgodnie
z wzorcem opisanym w [`instrukcja2.md`](./instrukcja2.md) ("MrPrompt Logic
Flow Gate" / Outside-Root Pattern).

---

## 1. Przegląd architektury

Projekt składa się z dwóch fizycznie rozdzielonych części:

```
LenavioWeb/
├── public_html/          ← DOCUMENT ROOT serwera (wszystko publicznie dostępne przez HTTP)
│   ├── index.html, style.css, sw.js, manifest.webmanifest, assets/
│   ├── src/               ← moduły JS aplikacji klienckiej (PWA) — MUSZĄ być publiczne,
│   │                         bo przeglądarka pobiera je i wykonuje lokalnie
│   ├── core/               ← "brama" (Logic Flow Gate): install.php, config.php, bootstrap.php
│   ├── outside/             ← cienkie endpointy PHP wołane przez PWA:
│   │   ├── teacher/api/       Human Review Loop
│   │   ├── mentor/api/        Komunikator 1:1
│   │   └── auth/api/          logowanie/rejestracja/sesja (register, login, logout, me, status)
│   └── admin/               ← panel administratora (logowanie osobne od PWA, zarządzanie kontami)
│
├── lenavio_engine/        ← POZA document rootem — nigdy niedostępny bezpośrednio przez HTTP
│   ├── config/
│   │   ├── engine.php        ustawienia serwerowe (CORS, rate limit)
│   │   ├── database.php      dane dostępowe MariaDB (sekret — generowany wizualnie przez /core/install.php)
│   │   └── database.example.php  szablon awaryjny (ręczna konfiguracja, gdyby instalator nie wystarczył)
│   ├── src/                ← klasy PHP silnika: Mysql (JEDYNE połączenie z bazą), TeacherStore, MentorStore,
│   │                          IdGen, Engine (helpery API), Auth, UserStore, SettingsStore (konta)
│   └── logs/
│
├── instrukcja2.md          ← wzorzec architektoniczny, wg którego zbudowano powyższy podział
└── specyfikacja_lenavio_web.md  ← ten plik
```

**Lokalnie (XAMPP)** `public_html` i `lenavio_engine` są katalogami siostrzanymi
pod `LenavioWeb/`. **Na serwerze produkcyjnym** `public_html` (lub `www`/`httpdocs`,
zależnie od hostingu) jest document rootem, a `lenavio_engine` ląduje jeden poziom
wyżej (poza zasięgiem przeglądarki) — dokładnie ten sam względny układ, więc kod
PHP nie zawiera ani jednej sztywnej ścieżki: lokalizacja `lenavio_engine` jest
wyliczana w locie z `$_SERVER['DOCUMENT_ROOT']` (zobacz §4).

### 1.1 Dlaczego nie wszystko poszło do `lenavio_engine`

`instrukcja2.md` opisuje ochronę **sekretów serwerowych** (baza, `.env`, klucze
API, certyfikaty) przez wyniesienie ich poza document root. Aplikacja Lenavio to
w większości **Progressive Web App wykonywana w przeglądarce użytkownika** —
cały kod w `public_html/src/**/*.js` musi być publicznie pobieralny, bo inaczej
przeglądarka nie miałaby go skąd załadować. Ukrycie go poza `public_html` nie
jest możliwe bez utraty funkcjonalności (i nie dodałoby żadnego bezpieczeństwa —
serwowanie tych samych plików przez PHP-proxy tylko odtworzyłoby publiczny dostęp).

Do `lenavio_engine` trafiło więc to, co faktycznie jest zasobem serwerowym:
**dwa mini-backendy treści** (Human Review Loop / nauczyciel oraz Komunikator
1:1 / mentor), których adresy (`https://lenavio.pl/outside/teacher/api/...` i
`.../outside/mentor/api/...`) już wcześniej były zaszyte w kliencie
(`src/util/teacherClient.js`, `src/util/mentorClient.js`), ale bez
implementacji po stronie serwera, oraz **system kont użytkowników i panel
administratora** (dodane później — logowanie, rejestracja, zarządzanie
użytkownikami). Ich bazy danych i logika PHP żyją teraz w `lenavio_engine`,
publicznie widoczne są tylko cienkie skrypty w `public_html/outside/*/api/*.php`
oraz serwerowo renderowane strony `public_html/admin/*.php`, które nie
zawierają żadnej logiki biznesowej — tylko walidację wejścia/formularzy i
wywołanie klas silnika.

Całość — konta, panel admina, Human Review Loop i Komunikator 1:1 — używa
**jednej wspólnej bazy MariaDB** (§3.3). SQLite, użyte w pierwszej iteracji dla
teacher/mentor, zostało **usunięte** po dodaniu kont: skoro serwer i tak
wymagał już prawdziwej bazy dla logowania, utrzymywanie drugiego, osobnego
silnika bazodanowego dla dwóch mniejszych funkcji nie miało już uzasadnienia —
jedna baza to jeden system kopii zapasowych, jedno miejsce do administrowania
na hostingu, mniej ruchomych części.

---

## 2. Aplikacja kliencka (PWA) — `public_html/`

### 2.1 Charakter aplikacji

- 100% statyczny front-end: czysty HTML/CSS/JavaScript (moduły ES, `type="module"`),
  bez build stepu, bundlera ani frameworka.
- Instalowalna jako PWA (`manifest.webmanifest`, `sw.js` — Service Worker do
  działania offline/cache'owania powłoki aplikacji).
- Cała treść edukacyjna i postęp ucznia żyje **lokalnie na urządzeniu**, w
  przeglądarkowej bazie **IndexedDB** (`tutor-local-db`, wersja 1) — nie ma
  centralnej bazy uczniów ani kont/logowania.
- Język interfejsu: polski.
- Jedyny sekret klienta to klucz API Groq wpisywany osobiście przez ucznia
  (model BYOK — Bring Your Own Key), używany wyłącznie z tego urządzenia.

### 2.2 Model danych (IndexedDB, `src/db/schema.js`)

| Store | Klucz | Indeksy | Rola |
|---|---|---|---|
| `courses` | `id` | `slug` (unique) | Zaimportowane ścieżki nauki (pakiety `.mra`) |
| `nodes` | `id` | `courseId`, `parentId` | Węzły drzewa kursu (rozdziały/tematy) |
| `nodePrerequisites` | auto | `nodeId` | Krawędzie grafu prerekwizytów (Knowledge Gate) |
| `nodeProgress` | `nodeId` | — | Status opanowania tematu przez ucznia |
| `sessions` | `id` | `nodeId` | Sesje rozmów z Leną (jedna aktywna + zakończone per temat) |
| `messages` | auto | `sessionId` | Wiadomości w ramach sesji; pole `kbAddedBlocks` zapamiętuje, które sugestie "Baza Wiedzy" z tej wiadomości już zapisano jako notatkę (blokuje ponowne dodanie tego samego fragmentu przy powrocie do tej samej rozmowy) |
| `memoryVault` | auto | `memoryType`, `mkey` | Pamięć długoterminowa AI o uczniu |
| `notes` | auto | `sessionId`, `pinned` | Notatki ucznia (w tym kategoria "Baza Wiedzy") |
| `flags` | auto | `status` | Zgłoszenia do nauczyciela (Human Review Loop) |
| `knowledgeFiles` | `id` | `nodeId` | Pliki wiedzy dołączone do tematu (część pakietu kursu) |
| `settings` | `key` | — | Ustawienia aplikacji, klucz API (zaszyfrowany), PIN, deviceId, itd. |

### 2.3 Warstwy kodu (`public_html/src/`)

```
src/
├── app.js                  Routing SPA (hash-based), inicjalizacja, PIN gate, przypomnienie o backupie
├── db/
│   ├── database.js         Cienki wrapper Promise nad natywnym IndexedDB API
│   └── schema.js           Definicje object store'ów (patrz §2.2)
├── repositories/           Warstwa dostępu do danych — jedno repo na store/domenę
│   ├── courseRepository.js     Import pakietu kursu, scalanie po ID (nie nadpisuje bezwarunkowo)
│   ├── nodeRepository.js       Węzły, dzieci, prerekwizyty
│   ├── progressRepository.js   Status opanowania tematów
│   ├── sessionRepository.js    Sesje i wiadomości rozmów z Leną
│   ├── noteRepository.js       Notatki + kategorie (wbudowane: Baza Wiedzy, Robocze).
│   │                             `getKnowledgeBaseNotesForNode()`/`getKnowledgeBaseNotesForCourse()`
│   │                             odtwarzają wpisy Baza Wiedzy dla tematu/kursu (notatki
│   │                             nie mają własnego nodeId/courseId — dochodzi się przez
│   │                             sessionId → sesja → node → kurs). Używane przez
│   │                             engine/cle.js (kontekst rozmowy), views/course.js (odznaka
│   │                             liczby wpisów przy temacie) i views/review.js (Powtórka)
│   ├── flagRepository.js       Zgłoszenia do nauczyciela + status synchronizacji z serwerem
│   └── settingsRepository.js   Ustawienia, klucz API (szyfrowany), PIN, deviceId, imię ucznia
├── engine/                 Conversational Learning Engine — logika tutora, w 100% klienta
│   ├── cle.js                   Orkiestracja odpowiedzi Leny (odpowiednik TutorOrchestrator::respond()).
│   │                             System prompt dostaje też własne notatki ucznia
│   │                             z Bazy Wiedzy dla bieżącego tematu (do 12 najnowszych)
│   │                             — Lena może się do nich odwoływać i sprawdzać, czy
│   │                             uczeń wciąż je rozumie, zamiast tłumaczyć je od nowa
│   ├── promptBuilder.js         Budowa system promptu: temat → ścieżka → ustawienia globalne + Integrity Guardrail.
│   │                             `buildSystemPrompt()` dokłada sekcję "Baza Wiedzy
│   │                             ucznia" gdy są dostępne notatki (patrz cle.js). Osobna
│   │                             funkcja `buildReviewSystemPrompt()` buduje prompt trybu
│   │                             Powtórka (views/review.js) — bez Topic Context Anchor,
│   │                             oparty wyłącznie na notatkach Bazy Wiedzy całego kursu
│   ├── dependencyEngine.js      Knowledge Gate — blokada wejścia w temat bez spełnionych prerekwizytów
│   ├── qualityEngine.js         Confidence Score odpowiedzi AI (heurystyka, bez zależności serwerowych)
│   ├── memoryVault.js           Odczyt/zapis pamięci długoterminowej o uczniu
│   └── groqClient.js            fetch() do Groq Chat Completions + kategoryzacja błędów (FFM)
├── util/
│   ├── crypto.js                Szyfrowanie klucza API "w spoczynku" (klucz materiałowy lokalny, bez sekretu serwerowego)
│   ├── mra.js                   Dekoder .mra (MrPrompt Resource Archive) — weryfikacja podpisu RSA + inflate
│   ├── mrb.js                   Format .mrb — kopia zapasowa danych ucznia (bez podpisu, generowana lokalnie)
│   ├── backup.js                Eksport/import całej bazy IndexedDB do pliku
│   ├── markdown.js               Bezpieczny, minimalny renderer markdown odpowiedzi AI (bez surowego innerHTML)
│   ├── noteFormat.js             Wąska allowlista HTML dla mini-edytora notatek
│   ├── transcript.js             Eksport rozmów do czytelnego pliku tekstowego
│   ├── diagnostics.js            Panel diagnostyczny błędów sieci/CORS
│   ├── teacherClient.js          Klient HTTP do outside/teacher/api (Human Review Loop)
│   ├── mentorClient.js           Klient HTTP do outside/mentor/api (Komunikator 1:1)
│   ├── authClient.js             Klient HTTP do outside/auth/api (logowanie, rejestracja, sesja)
│   ├── demoCourse.js             Kurs pokazowy "Poznaj Lenavio"
│   └── modal.js, file.js, autoResize.js, fontScale.js  Pomoce UI
└── views/                  Ekrany SPA (render(container, ctx, params))
    ├── onboarding.js       Pierwsze uruchomienie: import pakietu kursu + klucz API
    ├── login.js             Logowanie na konto — brama dostępu do całej reszty aplikacji
    ├── register.js          Rejestracja (widoczna tylko gdy administrator ją włączył)
    ├── pinLock.js          Blokada kodem PIN — DODATKOWA warstwa, sprawdzana dopiero po zalogowaniu
    ├── home.js              Start — branding, szybki dostęp, streak nauki.
    │                           Panel przypomnienia: jeśli w Bazie Wiedzy są wpisy
    │                           starsze niż 7 dni ("nieprzejrzane"), pokazuje ich
    │                           liczbę i link do Notatek → Baza Wiedzy
    ├── dashboard.js         Lista ścieżek nauki + import nowych pakietów
    ├── course.js             Spis treści kursu (drzewo), blokady Knowledge Gate.
    │                           Przy każdym temacie odznaka z liczbą wpisów Baza
    │                           Wiedzy zapisanych przy tym temacie (niezależna od
    │                           statusu "opanowany" — pokazuje ile własnej wiedzy
    │                           uczeń już tu zgromadził)
    ├── chat.js               Rozmowa z Leną, "Zgłoś" (HRL), zapis notatki.
    │                           Nagłówek: przyciski w kolejności Opanowany →
    │                           Historia → Spis → Nowa → ikona strzałki w dół
    │                           (przewija okno rozmowy na sam dół)
    ├── history.js            Historia sesji danego tematu
    ├── sessionView.js        Podgląd pojedynczej (zwykle zakończonej) rozmowy
    ├── notes.js               Notatki: lista, kategorie, mini-edytor, eksport/import.
    │                           W kategorii "Baza Wiedzy" dodatkowy rząd chipów
    │                           filtruje wpisy po ścieżce nauki (kursie), bo przy
    │                           wielu zaimportowanych kursach wpisy z różnych
    │                           ścieżek trafiają do jednej wspólnej Bazy Wiedzy
    │                           (filtr działa po courseId, nie po tytule). Po
    │                           wybraniu konkretnej ścieżki pojawia się przycisk
    │                           "Powtórka z tej ścieżki nauki" → #/review/:courseId
    ├── review.js              Powtórka z Bazy Wiedzy — osobny, tymczasowy ekran quizu
    │                           z Leną budowany wyłącznie z notatek Baza Wiedzy danej
    │                           ścieżki nauki (do 40 najnowszych). Celowo NIE korzysta
    │                           z sessions/messages (nie ma jednego node'a-właściciela,
    │                           bo obejmuje wiele tematów kursu naraz) — każde wejście
    │                           to świeża rozmowa, nic się nie zapisuje ani nie trafia
    │                           do Historii
    ├── reports.js             Zgłoszenia do nauczyciela + sprawdzanie odpowiedzi z serwera
    ├── mentorChat.js          Komunikator 1:1 (tworzenie/dołączanie do rozmów kodem, polling wiadomości)
    ├── profile.js             Ustawienia: imię, klucz API, PIN, kopia zapasowa
    ├── progressShare.js       Parent Transparency Link — zrzut postępów jako PNG, bez treści rozmów
    └── help.js                Pełny przewodnik po aplikacji + wgranie kursu pokazowego
```

### 2.4 Kluczowe mechanizmy

- **Import treści (`.mra` — MrPrompt Resource Archive)**: jedyny sposób
  dostarczenia kursu do aplikacji. Payload skompresowany Deflate, podpisany
  RSA; klucz publiczny wbudowany w klienta (`src/util/mra.js`) pozwala tylko
  weryfikować podpis, nie tworzyć nowych pakietów.
- **Knowledge Gate**: `dependencyEngine.js` blokuje wejście w temat, dopóki
  prerekwizyty (`nodePrerequisites`) nie są spełnione.
- **Integrity Guardrail (IG)**: stała doklejana zawsze na końcu system promptu,
  zaszyta na sztywno w kodzie (nie w danych pakietu) — musi przetrwać nawet
  uszkodzony lub złośliwie zmodyfikowany pakiet kursu.
- **Confidence Score**: `qualityEngine.js` liczy heurystyczną wagę zaufania do
  odpowiedzi AI (długość, `finish_reason`, spójność regexowa, kontekst).
- **Human Review Loop (HRL)**: uczeń może "zgłosić" odpowiedź Leny do
  nauczyciela. Zgłoszenie zapisuje się zawsze lokalnie (offline-first); jeśli
  kurs ma włączoną flagę "aktywny nauczyciel", wysyłane jest też best-effort na
  `outside/teacher/api` (zob. §3).
- **Komunikator 1:1**: osobny ekran (`mentorChat.js`) do rozmowy z
  mentorem/kolegą/korepetytorem, wymaga połączenia z serwerem w momencie
  użycia (bez lokalnego zapisu wiadomości) — patrz §3.
- **Parent Transparency Link**: zrzut postępu ucznia jako obraz PNG,
  generowany w 100% lokalnie (canvas), nigdy nie eksportuje treści rozmów
  (Per-Student Data Isolation).
- **Kopia zapasowa (`.mrb`)**: eksport/import całej lokalnej bazy do pliku —
  jedyna droga przeniesienia danych ucznia między urządzeniami, bo dane nigdy
  nie trafiają na serwer.
- **Logowanie**: brama wyświetlana przed onboardingiem — bez aktywnej sesji
  (`outside/auth/api/me.php`) użytkownik trafia na `#/login`, nie widzi żadnej
  treści aplikacji. Rejestracja (`#/register`) jest widoczna tylko gdy
  administrator ją włączył (patrz §3.6).
- **PIN**: DODATKOWA warstwa zabezpieczeń, sprawdzana dopiero po zalogowaniu —
  czysto lokalna blokada dostępu do urządzenia (nie uwierzytelnianie wobec
  serwera), nie zastępuje logowania na konto.
- **Tryb "Częste podpowiedzi"**: tymczasowy tryb CLE ważny tylko do końca
  bieżącej rozmowy — cofany automatycznie przy opuszczeniu ekranu czatu.

---

## 3. Silnik serwerowy — `lenavio_engine/` + `public_html/core`, `public_html/outside`

### 3.1 Cel

Dwa mini-backendy, do których klient już się odwoływał (`teacherClient.js`,
`mentorClient.js`), zaimplementowane w PHP + MariaDB, z danymi trzymanymi poza
document rootem zgodnie z `instrukcja2.md`. Wersja testowa: **bez logowania
kontem** — tożsamość użytkownika w tych dwóch API to `deviceId` (losowy UUID
wygenerowany raz na urządzeniu, `settingsRepository.getDeviceId()`); to inny,
osobny mechanizm niż logowanie na konto opisane w §3.6.

### 3.2 Brama (`public_html/core/`) — wzorzec Outside-Root Pattern

| Plik | Rola |
|---|---|
| `install.php` | Instalator uruchamiany ręcznie, przez przeglądarkę, w trzech fazach (patrz niżej). |
| `config.php` | Wygenerowany w Fazie 2 plik definiujący stałą `SECURE_PATH`, wyliczaną w locie: `dirname(realpath($_SERVER['DOCUMENT_ROOT'])) . '/lenavio_engine'`. Zero sztywnych ścieżek. Zablokowany przed bezpośrednim wywołaniem przez `.htaccess`. |
| `bootstrap.php` | Punkt wejścia dla każdego endpointu API: ładuje `config.php`, sprawdza `is_dir(SECURE_PATH)` (jeśli nie istnieje — zwraca JSON 503 z instrukcją ręcznego utworzenia katalogu i przerywa, zero zgadywania), po czym dołącza `SECURE_PATH/src/Engine.php`. |

`install.php` prowadzi przez trzy fazy, każda widoczna jako osobny panel na
tej samej stronie:

1. **Faza 1 — Diagnostyka**: wersja PHP, rozszerzenie `pdo_mysql`, czy da się
   wyznaczyć `DOCUMENT_ROOT` i katalog nadrzędny.
2. **Faza 2 — Katalog silnika**: po podaniu nazwy katalogu (domyślnie
   `lenavio_engine`) sprawdza `file_exists()` — **nigdy nie tworzy katalogu
   automatycznie** (brak `mkdir()` na `SECURE_PATH`). Jeśli katalog istnieje i
   jest zapisywalny, generuje `core/config.php`.
3. **Faza 3 — Baza danych (wizualnie, bez edycji plików ręcznie)**: formularz
   (host, port, nazwa bazy, użytkownik, hasło). Po zatwierdzeniu instalator:
   próbuje `CREATE DATABASE IF NOT EXISTS` (jeśli użytkownik ma uprawnienia —
   typowe lokalnie, rzadsze na hostingu współdzielonym), w przeciwnym razie
   łączy się z bazą założoną wcześniej ręcznie w panelu hostingu, tworzy
   **komplet tabel** jednym wywołaniem `Mysql::bootstrapSchema()` (`users`,
   `app_settings`, `flags`, `mentor_threads`, `mentor_messages`,
   `rate_limit_hits`) i zapisuje dane dostępowe do
   `lenavio_engine/config/database.php`. Formularz da się otworzyć ponownie
   (`?reconfigure_db=1`), żeby zmienić dane dostępowe bez ręcznej edycji pliku.

Ten sam wzorzec (zero automatycznego tworzenia bramy, zero zgadywania ścieżek)
sprawdzono automatycznie (patrz §3.5): po tymczasowym usunięciu
`lenavio_engine`, każdy endpoint poprawnie zwraca `503` z czytelnym komunikatem.

### 3.3 Silnik (`lenavio_engine/`)

```
lenavio_engine/
├── .htaccess            Deny all — obrona w głębi (katalog i tak jest poza document rootem)
├── config/
│   ├── engine.php        LENAVIO_ALLOWED_ORIGINS (CORS), LENAVIO_RATE_LIMIT_PER_MINUTE
│   ├── database.php       dane dostępowe MariaDB — wygenerowane przez core/install.php (Faza 3)
│   └── database.example.php  szablon awaryjny do ręcznej konfiguracji
├── src/
│   ├── Engine.php         Ładuje pozostałe klasy + wspólne helpery: JSON response, CORS, rate limit, odczyt body
│   ├── Mysql.php           JEDYNA fabryka połączeń (PDO/MariaDB) + Mysql::bootstrapSchema() — cała baza silnika
│   ├── IdGen.php           Generator kodów zaproszeń (6 znaków, bez znaków mylących: O/0, I/1)
│   ├── TeacherStore.php    CRUD zgłoszeń Human Review Loop (tabela `flags`)
│   ├── MentorStore.php     CRUD wątków/wiadomości Komunikatora 1:1 (`mentor_threads`, `mentor_messages`)
│   ├── Auth.php             Sesje logowania, hasła, CSRF (§3.6)
│   ├── UserStore.php        CRUD kont użytkowników (§3.6)
│   └── SettingsStore.php    Ustawienia globalne, m.in. tryb rejestracji (§3.6)
└── logs/                  Zarezerwowane na przyszłe logowanie błędów
```

Nie ma już SQLite ani katalogu `data/` — **jedna baza MariaDB** obsługuje
wszystko po stronie serwera (§1.1). `Mysql::connect()` sam tworzy brakujące
tabele przy każdym połączeniu (`CREATE TABLE IF NOT EXISTS`), więc nawet gdyby
instalator pominięto, pierwsze wywołanie dowolnego endpointu dogra schemat —
o ile `database.php` już istnieje. To nie łamie zasady z `instrukcja2.md`, bo
dotyczy ona wyłącznie samego katalogu `lenavio_engine` (bramy), a nie
wewnętrznej organizacji danych aplikacji w bazie, do której dostęp i tak
wymaga wcześniej podanych, świadomie wprowadzonych danych logowania.

### 3.4 API — `public_html/outside/`

Każdy endpoint: dołącza `core/bootstrap.php` → `engine_apply_cors()` →
`engine_require_method()` → walidacja pól wejściowych → (opcjonalnie)
`engine_rate_limit()` → wywołanie metody `TeacherStore`/`MentorStore` →
`engine_json_response()`. Żadnej logiki biznesowej poza tym plikiem — cała
żyje w `lenavio_engine/src`.

#### Human Review Loop — `outside/teacher/api/`

| Endpoint | Metoda | Wejście | Wyjście |
|---|---|---|---|
| `submit_flag.php` | POST (JSON) | `deviceId`, `messageContent`, opcjonalnie `localId`, `sessionId`, `nodeId`, `courseTitle`, `nodeTitle`, `studentName`, `reason`, `createdAt` | `{ ok, id }` |
| `list_replies.php` | GET | `?deviceId=` | `{ ok, replies: [{ remoteId, teacherReply }] }` (tylko zgłoszenia z wypełnioną odpowiedzią) |

Tabela `flags` (MariaDB): `id, device_id, local_id, session_id,
node_id, course_title, node_title, student_name, message_content, reason,
teacher_reply, created_at, replied_at`.

> Wpisywanie `teacher_reply` (panel nauczyciela) świadomie **nie** wchodzi w
> zakres tej iteracji — endpointy API są kompletne i działające, ale
> odpowiedź musi na razie trafić do bazy inną drogą (np. bezpośredni zapis).
> To osobne zadanie na przyszłość.

#### Komunikator 1:1 — `outside/mentor/api/`

| Endpoint | Metoda | Wejście | Wyjście |
|---|---|---|---|
| `create_thread.php` | POST | `deviceId`, `deviceName`, `label` | `{ ok, threadId, code }` |
| `join_thread.php` | POST | `deviceId`, `deviceName`, `code` | `{ ok, threadId }` |
| `list_threads.php` | GET | `?deviceId=` | `{ ok, threads: [{ threadId, label, partnerName, awaitingPartner, code, lastMessage }] }` |
| `send_message.php` | POST | `threadId`, `deviceId`, `senderName`, `content` | `{ ok, id }` |
| `list_messages.php` | GET | `?threadId=&deviceId=&sinceId=` | `{ ok, messages: [{ id, fromMe, senderName, content }], awaitingPartner }` |

Tabele w MariaDB:
- `mentor_threads`: `id, code (unique), label, creator_device_id, creator_name, partner_device_id, partner_name, created_at`
- `mentor_messages`: `id, thread_id, sender_device_id, sender_name, content, created_at`

Reguły dostępu (`MentorStore::assertParticipant`): tylko `creator_device_id`
lub `partner_device_id` danego wątku mogą wysyłać/czytać wiadomości — inne
`deviceId` dostają `403`. Dołączenie kodem jest jednorazowe (drugi slot
`partner_device_id`); próba dołączenia przez trzecią stronę do zajętego wątku
zwraca błąd.

### 3.5 Bezpieczeństwo i odporność

- **Brak sztywnych ścieżek** — `SECURE_PATH` liczony wyłącznie z
  `$_SERVER['DOCUMENT_ROOT']` przez `dirname()`/`realpath()`.
- **Zero automatycznego `mkdir()` na katalogu bramy** — zweryfikowane ręcznym
  testem: po usunięciu `lenavio_engine` każdy endpoint zwraca `503` z jasnym
  komunikatem zamiast tworzyć cokolwiek lub zgadywać.
- **`.htaccess` "deny all"** w `lenavio_engine/` (obrona w głębi — katalog i
  tak leży poza document rootem) oraz blokada bezpośredniego wywołania
  `core/config.php` i `core/bootstrap.php`.
- **Walidacja wejścia** w każdym endpoincie (wymagane pola, typy, `threadId`
  jako int, `code` uppercase) przed dotknięciem bazy.
- **Kontrola dostępu per-wątek** w Komunikatorze — `deviceId` musi być
  uczestnikiem wątku.
- **Rate limiting** — prosty licznik w MariaDB (tabela `rate_limit_hits`),
  okno 60s, domyślnie 30 żądań/min na `deviceId`/IP na endpoint
  (`engine_rate_limit()`), używany też przez logowanie/rejestrację (§3.6).
  To higiena, nie ochrona przed atakiem rozproszonym.
- **CORS** konfigurowalny w `lenavio_engine/config/engine.php`
  (`LENAVIO_ALLOWED_ORIGINS`), domyślnie `*` (do zawężenia przed produkcją,
  patrz §4).
- **PDO + `ERRMODE_EXCEPTION`**, wszystkie zapytania przez placeholdery
  (`?`/`:nazwa`) — brak konkatenacji SQL.
- Wszystkie pliki PHP zweryfikowane `php -l` (brak błędów składni) oraz
  end-to-end przez lokalny serwer (`php -S`): pełny cykl zgłoszenia do
  nauczyciela i pełny cykl rozmowy w Komunikatorze (utworzenie wątku →
  dołączenie kodem → wysłanie wiadomości → odbiór → odrzucenie
  nieuprawnionego `deviceId` kodem 403) przetestowane i działające.

### 3.6 System kont i logowania — `outside/auth/api/`

Cała aplikacja (poza ekranami logowania/rejestracji) wymaga aktywnej sesji.
Dane ucznia (postępy, notatki, historia rozmów) **nadal żyją wyłącznie lokalnie
w IndexedDB** — logowanie jest bramą dostępu, nie systemem synchronizacji
danych między urządzeniami. PIN (§2.4) to osobna, dodatkowa warstwa sprawdzana
dopiero po zalogowaniu.

Tożsamość i uprawnienia trzymane są w **MariaDB** (`lenavio_engine/src/Mysql.php`,
`UserStore.php`, `SettingsStore.php`, `Auth.php`), sesja to klasyczna sesja PHP
z ciasteczkiem `lenavio_session` (`HttpOnly`, `SameSite=Lax`, `Secure` gdy HTTPS).
`Auth::currentUser()` odpytuje bazę przy każdym wywołaniu (nie tylko przy
logowaniu), więc zablokowanie/usunięcie konta odcina dostęp natychmiast, nie
dopiero po wygaśnięciu sesji.

Tabela `users` (MariaDB): `id, username (unique), email, password_hash, role
('user'|'admin'), status ('pending'|'active'|'blocked'), created_by, created_at,
updated_at`. Hasła: `password_hash()`/`password_verify()` (bcrypt), nigdy w
czystym tekście, nigdy w logach.

| Endpoint | Metoda | Wejście | Wyjście |
|---|---|---|---|
| `status.php` | GET | — | `{ ok, registrationMode }` (`open`\|`approval`\|`closed`) — publiczny, bez logowania |
| `register.php` | POST | `username`, `email?`, `password` | Tryb `open` → konto od razu aktywne + zalogowane. Tryb `approval` → `{status:"pending"}`. Tryb `closed` → `403`. |
| `login.php` | POST | `username`, `password` | `{ ok, user:{username, role} }`; `401` z czytelnym powodem (zły login/hasło, konto zablokowane, konto oczekuje na akceptację) |
| `logout.php` | POST | — | `{ ok }` |
| `me.php` | GET | — | `{ ok, user: {username, role} \| null }` — używane przez PWA na starcie do decyzji, czy pokazać ekran logowania |

Zabezpieczenia specyficzne dla logowania: rejestracja i logowanie są
throttlowane przez ten sam mechanizm `engine_rate_limit()` co reszta API
(osobne kubełki `register`/`login`, klucz = IP lub IP+login), walidacja
nazwy użytkownika (`^[A-Za-z0-9_.\-]{3,32}$`) i długości hasła (min. 8 znaków)
po stronie serwera (nie tylko klienta).

### 3.7 Panel administratora — `public_html/admin/`

Serwerowo renderowane strony PHP (formularze, nie SPA), **osobna sesja i
osobne logowanie** od reszty aplikacji — konto musi mieć rolę `admin`, zwykli
użytkownicy dostają odmowę nawet ze znaną sesją. Wszystkie komunikaty
(błędy walidacji, potwierdzenia, ostrzeżenia) są własnymi, stylowanymi
panelami w języku polskim — **zero surowych błędów PHP** (`display_errors`
wyłączone, `set_exception_handler`/`set_error_handler` renderują błąd jako
zwykły ekran aplikacji) i **zero natywnych okien przeglądarki**
(`alert()`/`confirm()`) — nieodwracalne akcje (usunięcie konta) mają zamiast
tego osobny, stylowany ekran potwierdzenia (`users.php?confirm_delete=ID`).

| Strona | Rola |
|---|---|
| `setup.php` | Jednorazowy bootstrap pierwszego konta administratora. Samo się wyłącza, gdy istnieje już choć jedno konto `admin` (sprawdzane przez `UserStore::countAdmins()`) — dalej tylko przez zalogowany panel. |
| `login.php` / `logout.php` | Logowanie/wylogowanie panelu. Odrzuca konta bez roli `admin`, nawet jeśli hasło jest poprawne. |
| `index.php` | Dashboard: liczby kont wg statusu, bieżący tryb rejestracji, skrót do oczekujących. |
| `users.php` | Lista + filtrowanie wg statusu; akcje: **akceptuj/odrzuć** (dla `pending`), **zablokuj/odblokuj**, **nadaj/odbierz rolę admina**, **usuń** (z ekranem potwierdzenia). Chroni przed samo-zablokowaniem, samo-usunięciem i usunięciem/zdegradowaniem jedynego administratora. |
| `user_new.php` | Administrator dodaje konto bezpośrednio (od razu `active`, pomija cały proces rejestracji/akceptacji) — dokładnie funkcja, o którą prosiłeś ("W adminie admin może dodawać userów"). |
| `settings.php` | Przełącznik trybu rejestracji: **otwarta** / **za zgodą administratora** / **wyłączona** (patrz §3.6) — steruje tym, co widzi/robi `register.php` i czy `#/register` w ogóle jest linkowane w apce. |
| `account.php` | Zmiana własnego hasła administratora (wymaga podania obecnego hasła). |

CSRF: każdy formularz POST niesie token z `Auth::csrfToken()` (sesja),
weryfikowany `hash_equals()` przed wykonaniem jakiejkolwiek zmiany
(`admin_verify_csrf_or_die()`). `public_html/admin/includes/` (biblioteki
PHP dołączane przez `require_once`) zablokowane `.htaccess` przed
bezpośrednim wywołaniem.

Przetestowany end-to-end lokalnie (MariaDB + `php -S`): instalacja pierwszego
admina → blokada `setup.php` po instalacji → rejestracja w trybie "za zgodą"
→ odmowa logowania przed akceptacją → akceptacja w panelu → logowanie działa
→ zablokowanie konta zabija sesję natychmiast (`me.php` zwraca `user: null`)
→ ochrona przed usunięciem/zablokowaniem jedynego admina → tryb "wyłączona"
blokuje `register.php` → tryb "otwarta" loguje od razu po rejestracji →
zwykły użytkownik dostaje odmowę przy próbie logowania do `admin/login.php`.

---

## 4. Wdrożenie na hosting produkcyjny

1. Wgraj zawartość `public_html/` jako document root domeny (Apache/nginx z
   PHP ≥ 7.4 i rozszerzeniem `pdo_mysql`).
2. **Ręcznie** utwórz katalog `lenavio_engine` jeden poziom **nad**
   document rootem (np. `/home/user/lenavio_engine`, obok
   `/home/user/public_html`), z uprawnieniami 755. System celowo nie zrobi
   tego automatycznie.
3. Wgraj zawartość lokalnego `lenavio_engine/` (bez `config/database.php`,
   jeśli lokalnie już go masz — Faza 3 instalatora go nadpisze/utworzy) do
   tego katalogu.
4. (Opcjonalnie, tylko gdy user bazy nie ma prawa `CREATE DATABASE`) załóż
   pustą bazę MariaDB/MySQL w panelu hostingu — nazwę zapamiętaj na krok 6.
5. Otwórz `https://twojadomena/core/install.php` — **Faza 1** pokaże
   diagnostykę, **Faza 2** poprosi o nazwę katalogu (`lenavio_engine`) i
   wygeneruje `core/config.php`.
6. Ten sam formularz, **Faza 3**: wpisz host/port/nazwę/użytkownika/hasło do
   MariaDB i zatwierdź — instalator połączy się (lub założy bazę, jeśli ma
   uprawnienia), utworzy komplet tabel i zapisze
   `lenavio_engine/config/database.php` sam, bez ręcznej edycji plików.
7. Otwórz `https://twojadomena/admin/setup.php` i utwórz pierwsze konto
   administratora — strona sama się wyłącza, gdy tylko jedno konto `admin`
   istnieje.
8. W panelu (`/admin/settings.php`) ustaw tryb rejestracji zgodnie z
   potrzebami (otwarta / za zgodą administratora / wyłączona).
9. Sprawdź, że `outside/teacher/api/list_replies.php?deviceId=test` i
   `outside/mentor/api/list_threads.php?deviceId=test` zwracają `{"ok":true,...}`.
10. W kliencie (`src/util/teacherClient.js`, `src/util/mentorClient.js`) zmień
    stałe `TEACHER_ENDPOINT`/`MENTOR_ENDPOINT` na `https://twojadomena/outside/teacher/api`
    i `.../outside/mentor/api`, jeśli domena różni się od `lenavio.pl`
    (`authClient.js` używa ścieżki względnej, nie wymaga zmian).
11. Zawęź `LENAVIO_ALLOWED_ORIGINS` w `lenavio_engine/config/engine.php` do
    konkretnych domen przed pełnym uruchomieniem produkcyjnym.

---

## 5. Znane ograniczenia / otwarte tematy

- Brak panelu, w którym nauczyciel wpisuje odpowiedź na zgłoszenie
  (`teacher_reply` w bazie) — świadomie odłożone, patrz §3.4.
- Human Review Loop i Komunikator 1:1 (`outside/teacher`, `outside/mentor`)
  nadal identyfikują użytkownika przez `deviceId`, **nie** przez konto z
  systemu logowania (§3.6) — świadomie nierozszerzone przy tej zmianie, żeby
  nie ryzykować regresji w czymś, co już działało (patrz §1.1). Ktoś znający
  cudzy `deviceId` mógłby odczytać jego zgłoszenia/wątki. Do rewizji, jeśli
  te funkcje mają docelowo używać tożsamości z konta zamiast urządzenia.
- Rate limiter jest naiwny (licznik w MariaDB, brak ochrony przed rozproszonym
  ruchem) — wystarczający jako higiena, nie jako ochrona antyDDoS. Dotyczy też
  throttlingu logowania/rejestracji (§3.6).
- System kont daje dostęp do aplikacji, ale **nie** przenosi danych ucznia na
  serwer — postępy/notatki/historia rozmów zostają lokalnie w IndexedDB, jak
  dotąd (świadoma decyzja, patrz §3.6). Zalogowanie się na innym urządzeniu
  nie synchronizuje danych — to nadal ten sam model co przed dodaniem kont.
- Sesje PHP oparte o domyślny handler plikowy — na hostingu współdzielonym
  z wieloma stronami w jednym `session.save_path` warto to zweryfikować
  (izolacja katalogu sesji) przed produkcją.
