# barox ProductPub – Installation

Zielumgebung: Linux, Apache 2.4, **PHP 8.1 oder neuer** (empfohlen 8.3), MySQL/MariaDB.
Verzeichnis: `/var/www/html/barox-productpub`
Adresse: `http://productpub.barox.io` (HTTPS über den vorgelagerten Reverse Proxy)

Es werden **keine Composer-Pakete** benötigt.

---

## 1. Voraussetzungen

```bash
sudo apt update
sudo apt install -y apache2 php php-mysql php-curl php-xml php-mbstring
sudo a2enmod rewrite headers expires
sudo systemctl reload apache2
```

| Erweiterung | Zweck | Pflicht |
|---|---|---|
| `pdo_mysql` | Datenbank | ja |
| `zlib` | XLSX und PDF entpacken | ja |
| `json`, `dom`/`xmlreader` | Datenaustausch, XLSX lesen | ja |
| `curl` + `openssl` | Microsoft-Anmeldung, Sprachmodell | faktisch ja |
| `mbstring` | wird genutzt, wenn vorhanden; sonst greift die eigene Umsetzung | nein |

`pdftotext` (poppler-utils) ist **nicht** nötig – der eingebaute PHP-Parser liest
die barox-Datenblätter vollständig. Ist es installiert, kann es über
`pdftotext_path` in der Konfiguration bevorzugt werden.

---

## 2. Dateien ablegen

```bash
sudo mkdir -p /var/www/html/barox-productpub
# Inhalt des Ordners app/ dorthin kopieren, dann:
cd /var/www/html/barox-productpub

sudo chown -R barox:www-data .
sudo find . -type d -exec chmod 750 {} \;
sudo find . -type f -exec chmod 640 {} \;

# WICHTIG: als Letztes – storage/ braucht Schreibrecht für die Gruppe.
# Läuft dieser Befehl vor den beiden find-Zeilen, wird er wieder überschrieben.
sudo chmod -R 770 storage
```

Kontrolle – bei `storage` müssen **drei** w-Rechte stehen (`drwxrwx---`):

```bash
ls -ld storage config public
# storage  -> drwxrwx---  barox www-data     richtig
# storage  -> drwxr-x---  barox www-data     FALSCH, Upload schlägt fehl
```

Struktur danach:

```
/var/www/html/barox-productpub
├── public/          <- DocumentRoot zeigt hierhin
├── config/          config.php (Geheimnisse)
├── src/             Programmcode
├── templates/       Seitenvorlagen
├── db/schema.sql    Datenbankschema
└── storage/         Uploads, Protokolle (beschreibbar)
```

---

## 3. Datenbank

```bash
sudo mysql -e "CREATE DATABASE IF NOT EXISTS barox_productpub
               CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
sudo mysql -e "CREATE USER IF NOT EXISTS 'barox'@'localhost' IDENTIFIED BY 'bx5405Tech';"
sudo mysql -e "GRANT ALL PRIVILEGES ON barox_productpub.* TO 'barox'@'localhost';
               FLUSH PRIVILEGES;"

mysql -u barox -p barox_productpub < db/schema.sql
```

---

## 4. Konfiguration

```bash
sudo -u www-data cp config/config.sample.php config/config.php
sudo chmod 640 config/config.php
sudo nano config/config.php
```

Mindestens setzen:

```php
'base_url' => 'https://productpub.barox.io',   // die Adresse, unter der Nutzer die Anwendung aufrufen

'db' => [
    'host'     => 'localhost',
    'database' => 'barox_productpub',
    'username' => 'barox',
    'password' => 'bx5405Tech',
],
```

> **Wichtig:** `base_url` muss die **öffentliche** Adresse sein (also `https://…`),
> nicht die interne HTTP-Adresse hinter dem Proxy. Davon hängen die
> Redirect-URI der Anmeldung und das Setzen des Sitzungs-Cookies ab.
> `base_url` und die in Azure hinterlegte Redirect-URI müssen **zeichengenau**
> übereinstimmen – auch `http` gegen `https`.

> **Häufigster Stolperstein:** Jede Zeichenkette braucht Hochkommas, auch
> E-Mail-Adressen. Ohne sie kann PHP die Datei nicht lesen und Apache
> antwortet mit einem nackten „Error 500“:
>
> ```php
> falsch:   'allowed_users' => [vorname.name@barox.ch],
> richtig:  'allowed_users' => ['vorname.name@barox.ch'],
> ```
>
> Nach jeder Änderung prüfen:
>
> ```bash
> php -l config/config.php     # muss "No syntax errors detected" melden
> ```

---

## 5. Apache

`/etc/apache2/sites-available/barox-productpub.conf`:

```apache
<VirtualHost *:80>
    ServerName productpub.barox.io
    DocumentRoot /var/www/html/barox-productpub/app/public

    <Directory /var/www/html/barox-productpub/app/public>
        Options -Indexes +FollowSymLinks
        AllowOverride All
        Require all granted
    </Directory>

    # Alles ausserhalb von public/ bleibt gesperrt
    <DirectoryMatch "/var/www/html/barox-productpub/app/(config|src|storage|db|templates)">
        Require all denied
    </DirectoryMatch>

    # Datenblätter und Produktelisten können gross sein.
    # post_max_size gilt für den ganzen Upload: Zu einem Produkt werden
    # mehrere Unterlagen auf einmal hochgeladen, upload_max_filesize je Datei.
    php_value upload_max_filesize 32M
    php_value post_max_size 128M
    php_value max_file_uploads 20
    # Das Sprachmodell braucht bei lokalen Modellen Zeit
    php_value max_execution_time 300

    ErrorLog  ${APACHE_LOG_DIR}/productpub-error.log
    CustomLog ${APACHE_LOG_DIR}/productpub-access.log combined
</VirtualHost>
```

```bash
sudo a2ensite barox-productpub
sudo systemctl reload apache2
```

**Hinter dem Reverse Proxy** muss der Proxy `X-Forwarded-Proto: https` setzen.
Ist das nicht möglich, genügt es, dass `base_url` auf `https://…` steht – die
Anwendung richtet sich danach.

> **Wenn PHP als FPM läuft** (`php-fpm` statt `libapache2-mod-php`), sind die
> `php_value`-Zeilen oben **nicht erlaubt** und führen zu einem Serverfehler.
> Die Werte gehören dann in `/etc/php/8.3/fpm/php.ini`:
>
> ```ini
> upload_max_filesize = 32M
> post_max_size = 128M
> max_file_uploads = 20
> max_execution_time = 300
> ```
>
> Danach `sudo systemctl restart php8.3-fpm`. Welche Betriebsart aktiv ist,
> zeigt die Diagnoseseite (siehe unten).

---

## 5a. Kontrolle

Zwei Seiten helfen bei der Einrichtung, beide ohne Anmeldung erreichbar:

| Adresse | Wofür |
|---|---|
| `/diagnose.php` | **Läuft auch dann, wenn die Anwendung gar nicht startet.** Prüft PHP-Version, Erweiterungen, Konfiguration, Datenbank, Dateirechte – und nennt zu jedem Fehler den Befehl, der ihn behebt. |
| `/pruefung` | Systemprüfung innerhalb der Anwendung, sobald sie startet. |

**Bei „Error 500" immer zuerst `/diagnose.php` aufrufen.** Bleibt auch diese
Seite weiss, liegt es an Apache und nicht an der Anwendung:

```bash
sudo tail -n 40 /var/log/apache2/productpub-error.log
```

### „Die hochgeladene Datei konnte nicht gespeichert werden"

Fast immer fehlt der Gruppe das Schreibrecht auf `storage/`:

```bash
sudo chmod -R 770 /var/www/html/barox-productpub/app/storage
ls -ld /var/www/html/barox-productpub/app/storage   # muss drwxrwx--- zeigen
```

Die Anwendung nennt in der Meldung selbst den Ordner, die aktuellen Rechte und
den passenden Befehl.

### „Texte anpassen lassen" läuft in einen 504

Die Anpassung läuft seit dem Umbau **schrittweise**: eine Anfrage je Feld und
Sprache, mit Fortschrittsanzeige. Eine einzelne Anfrage dauert nur so lange wie
ein Modellaufruf, ein Proxy-Zeitlimit kann also nicht mehr greifen.

Bleibt es trotzdem hängen, in dieser Reihenfolge prüfen:

1. **Ist der Modellserver vom Webserver aus erreichbar?**
   `/diagnose.php` zeigt die Zeile *Sprachmodell erreichbar*. Oder direkt:

   ```bash
   curl -v --max-time 5 http://ai.jeneeben.de:1234/v1/models
   ```

   Hängt das ohne Ausgabe, sperrt eine Firewall die ausgehende Verbindung auf
   diesen Port – oder der Modellserver hört nur auf `localhost`.

2. **Lädt das Modell erst beim ersten Aufruf?** LM Studio lädt ein Modell bei
   Bedarf in den Speicher; bei 35B dauert das leicht über eine Minute, und im
   Protokoll erscheint solange nichts. Deshalb wiederholt die Anwendung einen
   fehlgeschlagenen Schritt bis zu dreimal. Dauerhaft besser: das Modell in
   LM Studio geladen halten (Auto-Unload / TTL abschalten).

3. **Zeitlimit des Reverse Proxy erhöhen**, wenn einzelne Schritte weiterhin zu
   lange brauchen – bei nginx:

   ```nginx
   proxy_read_timeout 300s;
   proxy_send_timeout 300s;
   ```

Unter **Einstellungen → Verbindung testen** wird zuerst die Erreichbarkeit
geprüft und anschliessend die Antwortzeit des Modells ausgegeben.

### Seite lädt ohne Layout (kein CSS)

Verweise auf Stylesheets sind schemaneutral (`/assets/css/app.css`) und
funktionieren über http wie https. Bleibt das Layout aus, prüfen:

```bash
curl -I https://productpub.barox.io/assets/css/app.css   # muss 200 liefern
ls -l /var/www/html/barox-productpub/app/public/assets/css/
```

Kommt ein 404, zeigt `DocumentRoot` nicht auf `…/app/public`.
Kommt ein 403, fehlt der Gruppe das Leserecht:
`sudo chmod -R g+rX /var/www/html/barox-productpub/app/public`

### Die häufigsten Ursachen für einen 500er

1. **Syntaxfehler in `config/config.php`** – meist fehlende Hochkommas (siehe Abschnitt 4).
2. **PHP älter als 8.1** – `php -v` prüfen.
3. **`php_value` in der VirtualHost-Datei bei PHP-FPM** (siehe Hinweis oben).
4. **Dateirechte** – der Webserver darf `config/` oder `src/` nicht lesen.

Ist alles grün, kann `diagnose.php` gelöscht werden:

```bash
sudo rm /var/www/html/barox-productpub/app/public/diagnose.php
```

---

## 6. Microsoft-365-Anmeldung

Im Azure-Portal → **Microsoft Entra ID** → **App-Registrierungen** → **Neue Registrierung**:

| Feld | Wert |
|---|---|
| Name | barox ProductPub |
| Unterstützte Kontotypen | Nur Konten in diesem Organisationsverzeichnis |
| Redirect-URI (Web) | `https://productpub.barox.io/auth/callback` |

Danach:

1. **Übersicht** → *Anwendungs-(Client-)ID* und *Verzeichnis-(Mandanten-)ID* notieren.
2. **Zertifikate & Geheimnisse** → *Neuer geheimer Clientschlüssel* → **Wert** sofort kopieren
   (er wird später nicht mehr angezeigt).
3. **API-Berechtigungen**: `openid`, `profile`, `email` (delegiert) genügen – das ist die Voreinstellung.

In `config/config.php`:

```php
'oidc' => [
    'enabled'       => true,
    'tenant'        => '00000000-0000-0000-0000-000000000000',  // Verzeichnis-ID
    'client_id'     => '11111111-1111-1111-1111-111111111111',
    'client_secret' => 'der-kopierte-Wert',

    // Optional einschränken – leer lassen heisst: alle Konten des Mandanten
    'allowed_users'  => ['@barox.ch'],
    'allowed_groups' => [],
],
```

`allowed_users` versteht ganze Adressen (`vorname.name@barox.ch`) und Domänen
(`@barox.ch`). Für `allowed_groups` müssen in der App-Registrierung unter
**Token-Konfiguration** die Gruppenansprüche aktiviert sein; dort dann die
Objekt-IDs der Gruppen eintragen.

---

## 7. Sprachmodell (freiwillig)

Die Anwendung läuft vollständig ohne. Ohne Modell werden die Freitexte
unverändert von der Vorlage übernommen und von Hand angepasst.

**Variante A – Claude API:**

```php
'llm' => [
    'provider'  => 'anthropic',
    'anthropic' => [
        'api_key' => 'sk-ant-…',
        'model'   => 'claude-sonnet-5',
    ],
],
```

**Variante B – lokales Modell (keine Daten verlassen das Haus):**

```bash
curl -fsSL https://ollama.com/install.sh | sh
ollama pull qwen2.5:14b
sudo systemctl enable --now ollama
```

```php
'llm' => [
    'provider'          => 'openai_compatible',
    'openai_compatible' => [
        'base_url' => 'http://localhost:11434/v1',
        'model'    => 'qwen2.5:14b',
    ],
],
```

Unter **Einstellungen** lässt sich im Betrieb zwischen den eingerichteten
Varianten umschalten und die Verbindung testen. Die Schlüssel bleiben in
`config/config.php` und sind über die Oberfläche nicht einsehbar.

Für brauchbare deutsche und französische Texte sollte ein lokales Modell
mindestens 14 Milliarden Parameter haben.

---

## 8. Erste Schritte in der Anwendung

1. Anmelden.
2. **Produkteliste** → die aktuelle `Laufende Produkteliste.xlsx` hochladen.
   Sie wird nur ausgewertet und danach wieder gelöscht; in der Datenbank
   liegen die Produkte, Spalten und Wertelisten.
3. **Neues Produkt** → Datenblatt als PDF hochladen.
4. Vorlage bestätigen, Entwurf durchsehen, Zeilen kopieren.

Nach jeder Änderung an der Excel-Datei die Liste erneut hochladen, damit
Vorlagen und Übersetzungen aktuell bleiben.

---

## 9. Wartung

**Fehlerprotokoll**

```bash
tail -f /var/www/html/barox-productpub/storage/logs/php-error.log
tail -f /var/log/apache2/productpub-error.log
```

**Alte Datenblätter aufräumen** (Uploads bleiben beim Entwurf liegen):

```bash
find /var/www/html/barox-productpub/storage/uploads -type f -mtime +180 -delete
```

**Sicherung**

```bash
mysqldump -u barox -p barox_productpub | gzip > productpub-$(date +%F).sql.gz
```

**Aktualisierung**: Dateien ersetzen, `config/config.php` und `storage/` behalten.
Sind neue Tabellen dazugekommen, `db/schema.sql` erneut einspielen – alle
Anweisungen sind mit `IF NOT EXISTS` formuliert und damit gefahrlos wiederholbar.

> Für die Unterlagen je Produkt ist die Tabelle `draft_documents` nötig. Nach
> dem Einspielen des Schemas erhalten bestehende Entwürfe ihr bisheriges
> Datenblatt beim ersten Öffnen automatisch als erste Unterlage – es ist nichts
> von Hand nachzutragen. Neu in `config.php`: `max_documents` (Vorgabe 8);
> fehlt der Wert, gilt ebenfalls 8.
