# 11. Instalacja i środowisko

## Wymagania

| Element | Minimum | Zalecane |
|---|---|---|
| PHP | 8.2 | 8.3 |
| MySQL | 8.0 | 8.0 |
| MariaDB, jeśli zamiast MySQL | 10.6 | 10.11 |
| Serwer WWW | Apache z `mod_rewrite` | Apache lub LiteSpeed |
| SSL | wymagany | certyfikat z automatycznym odnawianiem |
| Cron | co minutę | co minutę |
| Miejsce na dysku | 2 GB | 10 GB |
| Pamięć PHP | 128 MB | 256 MB |
| Czas wykonania skryptu | 30 s | 60 s |

### Rozszerzenia PHP

Wymagane: `pdo_mysql`, `mbstring`, `json`, `openssl`, `curl`, `fileinfo`, `session`, `filter`, `hash`.
Zalecane: `gd` lub `imagick` do zmniejszania logo i zdjęć, `zip` do eksportów, `intl` do formatowania dat.

Instalator sprawdza wszystkie i nie pozwala przejść dalej przy braku wymaganego.

## Środowiska

| Środowisko | Do czego | Baza | Stripe | SMS |
|---|---|---|---|---|
| Lokalne | Codzienna praca | Osobna, dane przykładowe | klucze testowe | tryb bez wysyłki |
| Testowe | Sprawdzenie przed wydaniem, kopia produkcji | Kopia produkcyjnej bez danych osobowych | klucze testowe | tryb bez wysyłki |
| Produkcyjne | Klienci | Produkcyjna | klucze produkcyjne | konto produkcyjne |

Środowisko ustawia `config.php` kluczem `environment`. Aplikacja pokazuje pasek u góry ekranu w każdym środowisku innym niż produkcyjne, żeby nie dało się pomylić okna przeglądarki.

**Tryb SMS bez wysyłki** zapisuje wiadomość do bazy ze statusem `sent`, ale nie wysyła jej do operatora i pokazuje treść w logu. Dzięki temu cała ścieżka jest testowalna bez wydawania pieniędzy i bez wysyłania testów na prawdziwe numery.

## Instalator

Uruchamiany raz, pod adresem `/instalacja`. Po zakończeniu sam się blokuje, tworząc plik `config/installed.lock`. Ponowne wejście prowadzi do ekranu logowania.

### Kroki

1. **Wymagania.** Wersja PHP, rozszerzenia, uprawnienia do zapisu w `storage`, obecność `mod_rewrite`. Lista z zielonymi i czerwonymi znacznikami.
2. **Baza danych.** Host, port, nazwa bazy, użytkownik, hasło. Przycisk testu połączenia przed przejściem dalej.
3. **Utworzenie schematu.** Wykonanie najnowszego pliku ze `schema/`, wpisanie wykonanych migracji do tabeli `migrations`, wgranie danych startowych.
4. **Konto operatora.** E-mail i hasło. Wymagane minimum 12 znaków.
5. **Pierwszy salon.** Nazwa, adres publiczny, strefa czasowa, dane właścicielki.
6. **Adres i poczta.** Adres aplikacji, adres nadawcy e-maili, dane SMTP z testem wysyłki.
7. **Podsumowanie.** Zapis `config/config.php`, utworzenie `installed.lock`, przypomnienie o dodaniu wpisu do crona z gotową linią do skopiowania.

Instalator nigdy nie zapisuje haseł do logów i nie wyświetla ich ponownie.

## Konfiguracja

`config/config.php` powstaje z `config.example.php` i nie trafia do paczki ZIP ani do repozytorium.

```php
return [
    'environment' => 'production',   // local | staging | production
    'debug'       => false,
    'url'         => 'https://salonio.pl',
    'timezone'    => 'UTC',          // zawsze UTC, strefa salonu jest w bazie

    'db' => [
        'host' => 'localhost', 'port' => 3306,
        'name' => 'salonio', 'user' => '', 'pass' => '',
        'charset' => 'utf8mb4',
    ],

    'security' => [
        'app_key'          => '',    // 32 losowe bajty, generowane przy instalacji
        'session_lifetime' => 43200, // 12 godzin
        'session_name'     => 'salonio_session',
    ],

    'mail' => [
        'host' => '', 'port' => 587, 'user' => '', 'pass' => '',
        'from' => 'system@salonio.pl', 'from_name' => 'Salonio',
        'encryption' => 'tls',
    ],

    'sms' => [
        'driver'   => 'smspl',       // smspl | null
        'username' => '', 'password' => '',
        'dlr_token' => '',           // token w adresie webhooka raportów
    ],

    'stripe' => [
        'secret_key'      => '',
        'publishable_key' => '',
        'webhook_secret'  => '',
    ],
];
```

Hasła i klucze można też podać przez zmienne środowiskowe, jeśli hosting na to pozwala. Kod czyta najpierw zmienną środowiskową, potem plik.

## Cron

Jeden wpis, dodawany w panelu hostingu:

```cron
* * * * * /usr/bin/php /home/uzytkownik/bin/cron.php >> /home/uzytkownik/storage/logs/cron.log 2>&1
```

Jeśli hosting pozwala na cron tylko co 5 minut, system działa, ale przypomnienia mogą wyjść z opóźnieniem do 5 minut, co nie ma znaczenia praktycznego. Cron rzadszy niż co 15 minut jest już problemem i instalator o tym ostrzega.

Weryfikacja po instalacji: `/admin/health` w sekcji `cron` musi pokazać uruchomienie sprzed mniej niż 5 minut.

## Uprawnienia do plików

| Ścieżka | Uprawnienia | Uwagi |
|---|---|---|
| `app/`, `config/`, `db/` | 644 pliki, 755 katalogi | tylko odczyt dla serwera |
| `config/config.php` | 600 | zawiera hasła |
| `storage/` i podkatalogi | 755, zapisywalne | logi, cache, uploady, kopie |
| `public_html/` | 644 i 755 | jedyny katalog dostępny z sieci |

Jeśli katalogi aplikacji muszą leżeć wewnątrz `public_html`, każdy z nich dostaje `.htaccess` z `Require all denied`, a instalator sprawdza, czy blokada działa, wykonując żądanie na własny adres.

## Kopie zapasowe

Bez kopii zapasowej cała reszta tego dokumentu nie ma znaczenia.

### Co i jak często

| Element | Częstotliwość | Przechowywanie |
|---|---|---|
| Baza danych | codziennie o 2:00 | 7 kopii dziennych, 4 tygodniowe, 6 miesięcznych |
| Pliki wgrane przez użytkowników | codziennie | 7 dziennych, 4 tygodniowe |
| Konfiguracja | przy każdej zmianie | bezterminowo |

### Sposób

```bash
mysqldump --single-transaction --quick --default-character-set=utf8mb4 \
  --databases salonio | gzip > storage/backups/db-$(date +%F-%H%M).sql.gz
```

`--single-transaction` nie blokuje tabel InnoDB, więc kopia nie zatrzymuje pracy salonów.

### Zasady

1. Kopia musi trafiać poza serwer produkcyjny. Kopia na tym samym dysku co baza nie chroni przed awarią dysku. Docelowo przestrzeń S3 albo serwer FTP u innego dostawcy.
2. Raz w miesiącu wykonywane jest odtworzenie kopii na osobnej bazie i sprawdzenie, czy aplikacja się uruchamia. Kopia nieprzetestowana to założenie, nie kopia.
3. Kopia przed każdą migracją na produkcji, niezależnie od kopii dziennej.
4. Wiek ostatniej kopii jest jednym ze sprawdzeń w `/admin/health`.
5. Kopie są szyfrowane, bo zawierają dane osobowe klientek salonów.

## Aktualizacja wersji

```
1. Kopia zapasowa bazy i plików
2. Włączenie trybu serwisowego: app_settings.maintenance_mode = 1
   (panel pokazuje ekran przerwy, strony publiczne działają dalej)
3. Wgranie plików z paczki ZIP, bez katalogów storage i config
4. php bin/migrate.php status, sprawdzenie listy oczekujących
5. php bin/migrate.php run
6. Wyłączenie trybu serwisowego
7. Sprawdzenie /version, /admin/health i jednego pełnego przejścia:
   logowanie, kalendarz, dodanie wizyty
```

Krok siódmy nie jest formalnością. Aktualizacja, która przeszła bez błędu w migratorze, potrafi zepsuć ekran, którego nikt nie otworzył przed wyjściem z trybu serwisowego.

## Monitoring

Minimum, które warto ustawić od pierwszego dnia produkcji:

- Zewnętrzne sprawdzanie `/admin/health.json` z tokenem, co 5 minut, z powiadomieniem przy kodzie 503
- Sprawdzanie ważności certyfikatu SSL
- Powiadomienie e-mail przy każdym błędzie 500, z ograniczeniem do jednego na 10 minut dla tego samego błędu
- Przegląd `storage/logs/error-*.log` raz w tygodniu

## Skalowanie

Przy obecnej architekturze jeden serwer współdzielony obsłuży spokojnie do około 200 salonów. Sygnały, że czas na zmianę:

| Sygnał | Reakcja |
|---|---|
| Zapytania powyżej 200 ms w logu wolnych zapytań | Przegląd indeksów, nie zmiana serwera |
| Kolejka rośnie szybciej niż jest przetwarzana | Częstsze uruchamianie runnera albo kilka równoległych |
| Czas odpowiedzi rośnie o tej samej porze co dnia | Przeniesienie kopii zapasowej na inną godzinę |
| Baza powyżej 5 GB | Archiwizacja wizyt starszych niż 3 lata do osobnych tabel |
| Powyżej 200 salonów | Przeniesienie na VPS z własną konfiguracją PHP i MySQL |

Nie optymalizujemy pod skalę, której nie ma. Pierwsze sto salonów jest ważniejsze od architektury dla tysiąca.
