# Specyfikacja Aplikacji — OmnibusAI Compliance

Wersja: 2.0 (zgodna ze stanem faktycznie wdrożonej aplikacji)
Data: 2026-08-04

---

## 1. Cel i kontekst prawny

OmnibusAI Compliance to platforma e-learningowa umożliwiająca firmom przeszkolenie pracowników z zakresu **AI Literacy** zgodnie z **art. 4 unijnego rozporządzenia AI Act** (obowiązek zapewnienia kompetencji w zakresie sztucznej inteligencji), a także z tematyki **Shadow AI** oraz podstaw **art. 50 AI Act** (przejrzystość — oznaczanie treści generowanych przez AI). Nazwa kursu widoczna w aplikacji (strona logowania, certyfikat, `course.json`) to **„Kompetencje AI pracowników zgodnie z AI Act”**.

Aplikacja jest **darmowa dla uczestników** — bez modułu płatności, subskrypcji czy fakturowania. Efektem końcowym szkolenia jest imienny **certyfikat PDF** potwierdzający ukończenie kursu.

## 2. Stos technologiczny

| Warstwa | Technologia |
|---|---|
| Backend | PHP (bez frameworków, bez Composera) |
| Frontend | HTML, CSS, JavaScript (vanilla, jeden plik SPA `app.js`) |
| Baza danych serwerowa | MariaDB — dane firm, użytkowników, wyników testów, certyfikatów (źródło prawdy) |
| Baza danych lokalna (klient) | IndexedDB — postęp nauki, stan akordionów, klucz API Groq; z eksportem/importem do pliku |
| Ikony | Font Awesome 6 (CDN) |
| AI konwersacyjne | Groq API, model `openai/gpt-oss-120b` (własny, darmowy klucz API użytkownika z console.groq.com) |
| Generator PDF | Własny, bezzależnościowy generator (`SimplePdf.php`) — patrz sekcja 12, punkt o ograniczeniach |

### 2.1 Rola IndexedDB vs MariaDB

- **MariaDB**: konta, powiązania firma–pracownik, ustawienia firmy, status blokady, wynik i status zaliczenia testu końcowego, wystawione certyfikaty.
- **IndexedDB**: postęp w slajdach (odwiedzone lekcje, stan akordionów) oraz **klucz API Groq** (nigdy nie wysyłany na serwer).
- Wynik testu końcowego jest zawsze zapisywany po stronie serwera (obliczany server-side na podstawie odpowiedzi — klient nigdy nie widzi poprawnych odpowiedzi przed wysłaniem testu), niezależnie od stanu IndexedDB.

## 3. Struktura katalogów i wzorzec Logic Flow Gate

Zaimplementowana struktura (nazwy katalogów ustalone i wdrożone):

```
OmnibusAI_Compliance/
├── public_html/                 ← katalog publiczny (webroot)
│   ├── index.php                 (front controller + shell SPA)
│   ├── install.php               (instalator, patrz sekcja 4)
│   ├── migrate.php               (aktualizacja schematu bazy przy zmianach — patrz sekcja 4.3)
│   ├── verify.php                (publiczna weryfikacja certyfikatu po ID)
│   ├── .htaccess                 (anty-enumeracja, blokada plików wrażliwych)
│   ├── core/
│   │   ├── .htaccess              (blokuje cały folder niezależnie od PHP)
│   │   ├── bootstrap.php          (guard constant + boundary-check + autoload klas)
│   │   └── config.php             (generowany przez instalator: SECURE_PATH, PUBLIC_PATH)
│   ├── api/                      (wszystkie endpointy JSON, patrz sekcja 6)
│   └── assets/{css,js}/
└── omnibus_engine/               ← POZA katalogiem publicznym
    ├── config/database.php        (generowany przez instalator: dane MariaDB — nigdy dostępny przez HTTP)
    ├── src/                       (klasy PHP: Database, Auth, repozytoria, SimplePdf)
    ├── schema/schema.sql           (dokumentacja schematu — od wersji instalatora z gotowymi
    │                                instrukcjami PHP, plik ten NIE jest już parsowany w runtime)
    └── data/course.json            (treść kursu i pytania testu końcowego)
```

Lokalnie (XAMPP) `public_html` i `omnibus_engine` leżą jako katalogi siostrzane. **Na serwerze produkcyjnym ścieżka do `omnibus_engine` może być inna** (jeden lub dwa poziomy wyżej niż webroot) — instalator wymaga podania pełnej, absolutnej ścieżki widzianej przez PHP na danym hostingu i nie zgaduje jej automatycznie poza podpowiedzią kandydatów.

Warstwy ochrony faktycznie wdrożone:
- `core/.htaccess` — `Require all denied` na cały folder.
- `public_html/.htaccess` — blokada plików `.env/.sql/.log/.bak/...` i ukrytych, wyłączenie listowania katalogów.
- `core/bootstrap.php` — guard constant `MRP_BOOTSTRAP`, `realpath()` + boundary-check przed użyciem `SECURE_PATH`.
- Dane logowania do MariaDB (`omnibus_engine/config/database.php`) leżą **całkowicie poza webrootem** — nie tylko chronione przez `.htaccess`, ale fizycznie niedostępne przez żadne żądanie HTTP.
- Self-test po instalacji: instalator sam odpytuje `https://.../core/config.php` i weryfikuje, że serwer zwraca błąd, nie treść pliku.
- Self-lock: po udanej instalacji `install.php` zapisuje `core/.installed` i od tego momentu zwraca `403` na każde kolejne wejście (także GET), bez renderowania formularza czy diagnostyki.

## 4. Instalator i migracje

### 4.1 `install.php` (jednorazowy, samoblokujący się)
- Formularz: ścieżka do `omnibus_engine`, dane połączenia MariaDB, dane konta Super Admina.
- Diagnostyka przed instalacją: test dostępności silnika **InnoDB** (`SHOW ENGINES`) — jeśli hosting go nie udostępnia, instalacja zatrzymuje się z jasnym komunikatem (to jest wymagane dla kluczy obcych w schemacie).
- Tworzy bazę (jeśli nie istnieje) i **wszystkie tabele jako osobne, zapisane w kodzie PHP instrukcje `CREATE TABLE`** — bez wczytywania i parsowania pliku `.sql` w runtime (rozwiązanie przyjęte po incydencie z błędem 1005/150 spowodowanym rozjechaniem się parsowania pliku na hostingu; `schema/schema.sql` pozostaje jako dokumentacja referencyjna).
- Checkbox **„Usuń istniejące tabele przed instalacją”** — do bezpiecznego powtórzenia instalacji po nieudanej wcześniejszej próbie.
- Tworzy konto Super Admina.
- Wykonuje self-test blokady `core/` i zapisuje self-lock.

### 4.2 Kolumny `blocked` (companies, users)
Instalator (dla nowych instalacji) tworzy tabele `companies` i `users` już z kolumną `blocked TINYINT(1) NOT NULL DEFAULT 0`, używaną przez mechanizm blokowania opisany w sekcji 7.3 i 8.3.

### 4.3 `migrate.php` (dostępny tylko dla zalogowanego Super Admina)
Dla instalacji wykonanych przed wprowadzeniem nowej funkcjonalności (np. blokowania), `migrate.php` sprawdza w `information_schema`, czy dana kolumna już istnieje, i jeśli nie — dodaje ją (`ALTER TABLE ... ADD COLUMN`). Bezpieczny do wielokrotnego uruchomienia. To jest przyjęty w tym projekcie sposób wprowadzania zmian schematu bazy bez ponownej pełnej instalacji.

**Ważne dla wdrożeń**: każda aktualizacja kodu, która dodaje nową kolumnę bazy danych (np. `blocked` przy wdrożeniu blokowania firm/pracowników), wymaga odwiedzenia `/migrate.php` przez Super Admina **po** wgraniu nowych plików na serwer. Pominięcie tego kroku powoduje błąd `HTTP 500` / `Unknown column` na endpointach, które odwołują się do nowej kolumny (np. `company_settings.php`, `admin_employees.php`) — zaobserwowane i potwierdzone jako realny incydent podczas wdrożenia funkcji blokowania.

## 5. Role użytkowników

| Rola | Opis |
|---|---|
| **Super Admin** | Zatwierdza/odrzuca rejestracje firm, widzi statystyki platformy, przegląda pracowników każdej firmy, blokuje/odblokowuje firmy i pojedynczych pracowników, zwiększa limit miejsc firmy, edytuje własne dane/hasło. |
| **Właściciel firmy** | Rejestruje firmę, deklaruje liczbę pracowników, po zatwierdzeniu zarządza ustawieniami testu końcowego, widzi listę i wyniki swoich pracowników oraz pobiera ich certyfikaty, edytuje własne dane/hasło. |
| **Pracownik** | Rejestruje się kodem zaproszeniowym firmy, przechodzi kurs, wykonuje test końcowy, pobiera własny certyfikat, edytuje własne dane/hasło. |

## 6. Endpointy API (`public_html/api/*.php`)

Wszystkie odpowiedzi JSON mają jednolitą kopertę `{"ok": true, ...}` / `{"ok": false, "error": "..."}` (funkcje `json_ok()`/`json_error()` w `Helpers.php`) — to jest wymóg techniczny całej aplikacji, złamany raz w historii projektu (`course.php` zwracał dane bez koperty, co blokowało cały panel pracownika po zalogowaniu) i od tamtej pory pilnowany.

| Endpoint | Dostęp | Opis |
|---|---|---|
| `register_company.php`, `register_employee.php` | publiczny | Rejestracja firmy / pracownika (kodem zaproszeniowym). |
| `login.php`, `logout.php`, `whoami.php` | publiczny / zalogowany | Logowanie sprawdza status zatwierdzenia firmy oraz **blokadę firmy i użytkownika**; `whoami.php` re-weryfikuje blokadę przy każdym odpytaniu sesji i wylogowuje automatycznie, jeśli blokada nastąpiła w trakcie sesji. |
| `profile.php` | zalogowany (każda rola) | Podgląd i edycja własnych danych oraz zmiana hasła (wymaga podania aktualnego hasła). |
| `course.php` | zalogowany | Treść kursu (bez poprawnych odpowiedzi testu końcowego). |
| `complete_course.php`, `submit_test.php` | pracownik | Oznaczenie ukończenia kursu; ocena testu końcowego **liczona wyłącznie po stronie serwera**. |
| `certificate.php` | pracownik (własny) / właściciel (swoich pracowników, `?user_id=`) | Generuje i zwraca certyfikat PDF. |
| `verify_certificate.php` | publiczny | Weryfikacja certyfikatu po identyfikatorze (używane przez `verify.php`). |
| `company_settings.php` | właściciel | Odczyt/zapis ustawień testu, lista własnych pracowników. |
| `admin_companies.php` | super admin | Lista firm; akcje: `approve`, `reject`, `increase_limit`, `block`, `unblock`. |
| `admin_employees.php` | super admin | Lista pracowników danej firmy (`?company_id=`); akcje: `block`, `unblock`. |
| `admin_stats.php` | super admin | Zbiorcze statystyki platformy. |

## 7. Panel Super Admina (zaimplementowany układ)

Panel ma **lewe menu boczne** (chowane/wysuwane na wąskich ekranach przyciskiem hamburgera) z dwiema sekcjami:

### 7.1 Statystyki (widok domyślny)
Kafelki: liczba firm łącznie / oczekujących / zatwierdzonych / odrzuconych, liczba właścicieli, liczba pracowników, liczba ukończonych kursów, liczba zaliczonych testów, liczba wydanych certyfikatów.

### 7.2 Firmy
Tabela zgłoszeń z akcjami zależnymi od statusu:
- **Oczekująca**: Zatwierdź / Odrzuć.
- **Zatwierdzona**: przycisk **„Pracownicy”** (przełącza widok na listę pracowników tej firmy — patrz 7.3), **„Zwiększ limit”** (modal aplikacyjny, nie systemowe `prompt()`), **„Zablokuj firmę”** / **„Odblokuj firmę”**.

### 7.3 Widok pracowników firmy
Po kliknięciu „Pracownicy” admin widzi: imię, nazwisko, stanowisko, e-mail, status kursu, wynik testu, status blokady, oraz przycisk **Zablokuj/Odblokuj** per pracownik. Blokada i odblokowanie wymagają potwierdzenia w modalu aplikacyjnym (nie `confirm()` przeglądarki).

### 7.4 Blokowanie — efekt
- **Blokada firmy**: właściciel **i wszyscy jej pracownicy** nie mogą się zalogować (komunikat aplikacyjny przy próbie logowania); kod zaproszeniowy blokowanej firmy przestaje działać dla nowych rejestracji pracowników.
- **Blokada pracownika**: dotyczy tylko tego konta, reszta firmy działa normalnie.
- Aktywna sesja zablokowanego użytkownika jest przerywana przy najbliższym odpytaniu `whoami.php` (automatyczne wylogowanie z komunikatem aplikacyjnym).

### 7.5 Moje konto
Każda rola (w tym Super Admin) może edytować imię, nazwisko, e-mail i zmienić hasło z modala dostępnego pod przyciskiem z własnym imieniem w nagłówku.

## 8. Panel Właściciela firmy

- Dane firmy, kod zaproszeniowy, licznik wykorzystanych/dostępnych miejsc.
- **Ustawienia testu końcowego**: przełącznik „Wymagaj zaliczenia testu końcowego” (domyślnie wyłączony) + wybór progu z 4 kart: **50% / 70% / 80% / 90%**. Właściciel sam decyduje, czy jego pracownicy muszą zdawać egzamin (przełącznik) i jaki próg musi zostać osiągnięty.
- Lista pracowników: imię, nazwisko, stanowisko, status kursu, wynik testu, link do pobrania certyfikatu PDF (`certificate.php?user_id=`), jeśli pracownik się kwalifikuje.
- „Moje konto” — edycja danych/hasła (sekcja 7.5).

## 9. Panel Pracownika

- Układ desktop: header (logo, hamburger, przycisk „Moje konto”, wyloguj) + lewy panel (spis treści kursu, wysuwany) + główne okno treści + prawy panel (postęp, eksport/import IndexedDB, Lena AI) + stopka.
- Układ mobilny: panele boczne jako nakładki pełnoekranowe z overlayem, dolne menu (spis treści / start / konto).
- Slajdy z kolorowymi blokami (definicja / ostrzeżenie / wskazówka / przykład) i akordionami z pytaniami do samodzielnego przemyślenia.
- Test końcowy i certyfikat dostępne po przejściu wszystkich lekcji **i zaliczeniu quizu każdego modułu** (patrz 10.2).

### 9.1 Mechanizmy wymuszające faktyczne zapoznanie się z treścią

Aby uniemożliwić przeklikanie kursu bez czytania, wdrożono:
- **Blokadę czasową przycisku „Dalej”** — obliczaną z długości treści slajdu (ok. 3,5 słowa/s, min. 6 s, maks. 40 s), z widocznym odliczaniem. Lekcje już wcześniej odwiedzone nie są ponownie blokowane czasowo.
- **Wymóg rozwinięcia pytań akordionowych** na slajdzie, zanim przycisk „Dalej” się odblokuje (przy pierwszej wizycie na danej lekcji).
- **Blokadę modułów w spisie treści** — lekcje kolejnego modułu są wyszarzone i oznaczone ikoną kłódki, dopóki wszystkie lekcje bieżącego modułu nie zostaną odwiedzone i jego quiz (patrz 10.2) zaliczony; próba kliknięcia zablokowanej lekcji pokazuje komunikat aplikacyjny, nie nawiguje.

Wszystkie trzy mechanizmy działają w parze — same odwiedzenie lekcji (bez przeczytania) nie odblokowuje kolejnego modułu.

## 10. Struktura i treść kursu (`omnibus_engine/data/course.json`)

### 10.1 Program
Zaimplementowane 6 modułów, 25 lekcji łącznie (program rozszerzony względem pierwszej wersji po przeglądzie merytorycznym — patrz niżej), każdy z lekcjami zawierającymi bloki treści i — tam gdzie to uzasadnione — pytania akordionowe. Test końcowy: **10 pytań zamkniętych jednokrotnego wyboru**, poprawne odpowiedzi trzymane wyłącznie po stronie serwera i nigdy nie wysyłane do klienta przed oceną testu.

**Zakres merytoryczny modułów 1–5** (po rozszerzeniu):
- **Moduł 1** (5 lekcji): GenAI, **czym jest AI Act** (dlaczego powstał, kogo dotyczy, od kiedy obowiązuje, obowiązki przedsiębiorców, dlaczego to szkolenie), **klasyfikacja systemów AI według poziomu ryzyka** (zakazane/wysokiego/ograniczonego/minimalnego ryzyka), **odpowiedzialność człowieka za decyzje wspierane przez AI**, korzyści z AI.
- **Moduł 2** (5 lekcji): podstawy promptowania, **zaawansowane techniki promptowania** (doprecyzowanie, iteracja, proszenie o źródła, proszenie o wskazanie niepewności), halucynacje, **inne ograniczenia AI** (uprzedzenia/bias, nieaktualna wiedza, brak kontekstu, nadmierna pewność odpowiedzi), zasada ograniczonego zaufania.
- **Moduł 3** (4 lekcje): czego nie wklejać do AI, RODO, tajemnica przedsiębiorstwa, **checklista 4 pytań przed użyciem AI** (prawo do danych / poufność / weryfikacja wyniku / zatwierdzone narzędzie).
- **Moduł 4** (4 lekcje, bez zmian merytorycznych poza rozszerzeniem): Shadow AI, w tym **rozszerzona lista przykładów** (ChatGPT/Claude/Gemini/Perplexity/NotebookLM na prywatnych kontach, PDF AI, transkrypcja spotkań, nagrywanie ekranu, AI w przeglądarce, rozszerzenia Chrome), case study, procedura zgłaszania narzędzi.
- **Moduł 5** (4 lekcje): oznaczanie treści AI (w tym **kiedy oznaczenie nie jest wymagane** + przykłady), chatboty i transparentność, **deepfake i ryzyka treści syntetycznych** (nowa lekcja), prawa autorskie.

Ze względu na rozszerzenie programu, orientacyjny czas przejścia kursu jest dłuższy niż pierwotnie szacowane 45–60 minut (mikrolearning) — z uwzględnieniem blokad czasowych z sekcji 9.1 i quizów modułowych, realistyczny czas to raczej **60–90 minut**.

Moduł 6.1 („Polityka Zastosowania AI” firmy) w bieżącej wersji zawiera treść informacyjną odsyłającą pracownika do osoby zarządzającej szkoleniem w jego firmie — **mechanizm wgrywania/generowania własnego regulaminu firmy nie jest zaimplementowany** (pozostaje jako możliwe rozszerzenie, patrz sekcja 14).

### 10.2 Quizy modułowe (bramka wiedzy między modułami)
Moduły 1–5 mają w `course.json` pole `quiz` (3 pytania zamkniętych jednokrotnego wyboru każdy; moduł 6 nie ma własnego quizu, bo kończy się testem końcowym). Różnice względem testu końcowego:
- Poprawne odpowiedzi są **wysyłane do klienta razem z treścią kursu** (to lekkie utrwalanie wiedzy, nie prawny dowód ukończenia) i sprawdzane **po stronie przeglądarki**.
- Quiz pojawia się automatycznie po ostatniej lekcji modułu; przy błędnej odpowiedzi użytkownik widzi którą odpowiedź poprawić i próbuje ponownie — bez limitu prób.
- Wynik (zaliczony/niezaliczony) przechowywany wyłącznie w IndexedDB (`module_quiz_passed`) — nie jest to dane do audytu/kontroli, tylko lokalna bramka postępu.
- **Przewaga pytań scenariuszowych nad definicyjnymi**: większość pytań (w quizach modułowych i w teście końcowym) ma formę decyzyjnego scenariusza z pracy („Dostajesz CV kandydata — czy możesz je wkleić do ChatGPT?”, „Klient wysłał umowę — co robisz?”, „Dostajesz nagranie głosowe od »prezesa« z poleceniem przelewu — co robisz?”), a nie pytania o definicję pojęcia. To świadoma decyzja projektowa — scenariusz lepiej sprawdza realną kompetencję (umiejętność podjęcia właściwej decyzji w pracy) niż odpytanie z definicji.

## 11. System testu końcowego

- Warunek zaliczenia: jeśli firma nie wymaga testu → ukończenie kursu wystarcza; jeśli wymaga → dodatkowo próg 50/70/80/90% ustawiony przez właściciela.
- **Baza pytań i losowanie**: `course.json` → `final_test` zawiera pulę **28 pytań** (w większości scenariuszy decyzyjnych z pracy, np. „Kolega chce wkleić umowę klienta do ChatGPT — co robisz?”). Przy każdym podejściu do testu `GET /api/final_test.php` losuje **15 pytań** z tej puli (bez pola `correct`), a wylosowany zestaw identyfikatorów zapisuje w sesji PHP (`$_SESSION['final_test_ids']`) — to jedyne źródło prawdy dla oceny tej próby.
- `course.php` **nie wysyła** całej puli `final_test` do klienta (usunięta z payloadu) — pytania trafiają do przeglądarki wyłącznie przez `final_test.php`, w losowym zestawie 15 z 28, żeby ograniczyć ekspozycję puli pytań.
- Wynik liczony w `submit_test.php` po stronie serwera, wyłącznie na podstawie zestawu pytań zapisanego w sesji (nie całej puli) — klient wysyła tylko wybrane odpowiedzi. Po ocenie próby (niezależnie od wyniku) `$_SESSION['final_test_ids']` jest czyszczone, więc kolejna próba (przycisk „Spróbuj ponownie z nowym zestawem pytań” przy niezaliczeniu) losuje **inny** zestaw 15 pytań.
- Wynik i status zaliczenia zapisywane w tabeli `test_results` (MariaDB), z progiem obowiązującym **w momencie podejścia** do testu.

## 12. Certyfikat ukończenia

- Generowany na żądanie (`certificate.php`) jako **PDF**, własnym generatorem `SimplePdf.php` (bez zewnętrznych bibliotek/Composera) — jedna strona A4 pozioma z ramką, danymi uczestnika, nazwą i podstawą prawną szkolenia, wynikiem testu (jeśli wymagany) oraz unikalnym **identyfikatorem weryfikacyjnym** w formacie `OAI-{rok}-{8 znaków}`.
- Rekord certyfikatu (identyfikator, `user_id`, data wystawienia) zapisywany w tabeli `certificates`; sam plik PDF generowany za każdym razem na żądanie, nie przechowywany jako plik na dysku.
- **Publiczna weryfikacja**: strona `verify.php` + endpoint `verify_certificate.php` — po podaniu identyfikatora zwraca imię i nazwisko, nazwę firmy i datę wystawienia.
- **Znane ograniczenie**: własny generator PDF używa standardowych fontów PDF (Helvetica), które nie zawierają polskich znaków diakrytycznych. Tekst na certyfikacie jest **transliterowany do ASCII** (np. „ą” → „a”, „ł” → „l”). Pełne wsparcie polskich znaków wymagałoby osadzenia fontu TrueType w generatorze — nie zaimplementowane w tej wersji.

## 13. Lena AI — asystent konwersacyjny

- Integracja: Groq API (`https://api.groq.com/openai/v1/chat/completions`), model `openai/gpt-oss-120b`, wywoływane **bezpośrednio z przeglądarki** (klucz nigdy nie przechodzi przez backend PHP, przechowywany wyłącznie w IndexedDB).
- Obsługa błędów przetłumaczona na komunikaty niesystemowe/nietechniczne: brak/nieprawidłowy klucz (401/403), limit zapytań (429), błąd sieci, błąd długości kontekstu (wykrywany heurystycznie z treści błędu Groq), inne błędy API.
- **Scenariusze scenek są obecnie zaszyte na stałe w `app.js`** (3 scenariusze powiązane z modułami 3, 4 i 5) — nie są wczytywane z `course.json` ani konfigurowalne przez firmę/admina. To jest przyjęte uproszczenie tej wersji (patrz sekcja 14).

## 14. Znane ograniczenia i możliwe rozszerzenia (poza obecnym zakresem)

1. Certyfikat PDF nie renderuje polskich znaków diakrytycznych (transliteracja ASCII) — patrz sekcja 12.
2. Scenariusze Lena AI są statyczne w kodzie frontendu, nie edytowalne z panelu.
3. Treść „Polityki Zastosowania AI” firmy (moduł 6.1) jest statycznym tekstem informacyjnym, nie realnym dokumentem wgrywanym przez firmę.
4. Brak formalnej zgody RODO/regulaminu przy rejestracji oraz polityki retencji danych po zakończeniu współpracy firmy z platformą.
5. Blokowanie właściciela firmy indywidualnie (niezależnie od blokady całej firmy) nie jest zaimplementowane — blokuje się albo całą firmę, albo pojedynczego pracownika.
6. Poza zakresem (świadomie, zgodnie z modelem „bezpłatna platforma”): płatności/subskrypcje, generator rejestru systemów AI, automatyczny generator regulaminu AI, usługi doradcze/audytowe.

## 15. Bezpieczeństwo i komunikaty

- Hasła haszowane (`password_hash`/`PASSWORD_DEFAULT`), nigdy w plaintext.
- Klucz API Groq nigdy nie opuszcza urządzenia użytkownika i nie jest logowany po stronie serwera.
- **Wszystkie komunikaty widoczne dla użytkownika pochodzą z interfejsu aplikacji** (stylizowane `.alert`, modale) — aplikacja nie używa natywnych okien przeglądarki (`alert()`, `confirm()`, `prompt()`); potwierdzenia (np. blokowanie firmy/pracownika) i wprowadzanie danych (np. zwiększenie limitu) odbywają się przez własne modale.
- Wynik testu i status blokady są re-weryfikowane po stronie serwera przy każdym istotnym żądaniu (logowanie, `whoami.php`) — sesja zablokowanego użytkownika jest automatycznie przerywana.
- **Obsługa błędów wczytywania danych w panelach**: jeśli zapytanie API w panelu admina, właściciela lub pracownika się nie powiedzie (np. błąd serwera, sesja wygasła), interfejs pokazuje stylizowany komunikat błędu z przyciskiem „Spróbuj ponownie” — nigdy nie zostaje po prostu pusty ekran bez wyjaśnienia. Wprowadzone po incydencie, w którym nieobsłużony błąd zapytania renderował panel właściciela jako całkowicie pusty, bez żadnej informacji dla użytkownika.
- **Globalny handler nieobsłużonych wyjątków PHP** (`core/bootstrap.php`, `set_exception_handler` + `display_errors=0`): każdy nieprzewidziany błąd serwera (np. błąd SQL po niewykonanej migracji) zwraca na endpointach `api/*.php` czysty, sparsowalny JSON `{"ok":false,"error":"..."}` z kodem 500, a nie surowy komunikat/stack trace PHP wypisany jako HTML. Wprowadzone po incydencie, w którym brakująca kolumna `blocked` (nieuruchomiona migracja) powodowała, że przeglądarka dostawała nieparsowalny HTML w miejsce JSON i wyświetlała ogólny komunikat „Nieprawidłowa odpowiedź serwera” bez wskazania prawdziwej przyczyny. Szczegóły błędu trafiają do `error_log()` serwera, nie do odpowiedzi widocznej dla użytkownika.

## 16. Wysokopoziomowy model danych (MariaDB)

- `companies` — dane firmy, `status` (pending/approved/rejected), **`blocked`**, deklarowana liczba pracowników, kod zaproszeniowy i licznik wykorzystania, ustawienia testu (`require_test`, `pass_threshold`).
- `users` — konta (super_admin/owner/employee), `company_id` (NULL dla super admina), **`blocked`**, dane logowania.
- `course_completions` — data ukończenia kursu per pracownik.
- `test_results` — wynik, wymagany próg w momencie podejścia, status zaliczenia, data.
- `certificates` — identyfikator weryfikacyjny, `user_id`, data wystawienia.

---

*Dokument zaktualizowany po wdrożeniu i przetestowaniu (lokalnie end-to-end oraz na produkcji) — opisuje stan faktyczny aplikacji, nie tylko założenia projektowe.*
