# KOTSCH.TECH — Admin-Panel Referenz (für eine eigene App)

Vollständige Beschreibung, wie das Admin-Dashboard funktioniert, welche Datenquellen/APIs
es gibt, wo welche Daten liegen und wie du eine App bauen kannst, die **alle** Inhalte des
Admin-Panels anzeigt. Stand: 2026-07-01.

> **Kernaussage für den App-Bau:** Es gibt jetzt eine **vollständige Admin-API** unter
> **`/api/v1/admin/*`** (Abschnitt 8) mit **2FA-Login + 60-Tage-Token**, die ALLE Bereiche zur
> Verwaltung bereitstellt (Dashboard, Anfragen, Tester, ProblemRadar, Lizenzen, Shop, Monitoring).
> Für Shop-only existiert zusätzlich die ältere `/api/v1/shop/*` (Abschnitt 2). Die App nutzt die
> neue Admin-API.

---

## 0. Überblick

| Ding | Wert |
|---|---|
| Framework | Laravel 12 · PHP 8.2 |
| Haupt-DB | SQLite (`database/database.sqlite`) |
| Admin-URL | `https://kotsch.tech/admin` **und** `https://admin.kotsch.tech/` (Subdomain, gleiche Inhalte) |
| Admin-Login | 2-Faktor: E-Mail+Passwort → **6-stelliger OTP-Code per E-Mail** |
| Admin-Sitzung | Cookie `admin_auth_token` (30 Tage, HttpOnly, Secure, SameSite=Strict) |
| Fertige API | `GET/PATCH /api/v1/shop/*` (Token-Auth) — für die „Kotsch Orders"-App gebaut |
| App-App-API | Aktivierungs-API der Windows-Apps auf `license.kotsch.tech` (nicht Admin) |

### Sidebar-Struktur (die „Bereiche" des Panels)
`resources/views/admin/panel/layout.blade.php`

- **Verwaltung:** Dashboard · Reports · Anfragen · Tester · Mail-Test
- **Shop:** Shop-Übersicht · Produkte · Bestellungen
- **ProblemRadar:** Übersicht · Einreichungen · Cluster · Tags · Apps · KI-Assist
- **Lizenzen:** Lizenzen · Kunden · Apps · Audit-Log
- **Extern:** Shop ansehen · Website ansehen

---

## 1. Authentifizierung

### 1.1 Admin-Web (alle `/admin/*`-Seiten)
Controller `app/Http/Controllers/Admin/AdminAuthController.php`, Middleware `admin.auth`
(`EnsureAdminAuth`).

**Ablauf (2FA):**
1. `GET /admin/login` → Login-Formular.
2. `POST /admin/login` `{email, password}` → prüft via `AdminCredentials::check()`
   (Config `shop.admin.email` + `shop.admin.password_hash` **oder** `shop.admin.password`).
   Bei Erfolg: 6-stelligen Code erzeugen, `admin_otps`-Zeile anlegen, Code **per E-Mail** senden.
3. `GET /admin/otp` → OTP-Eingabe. `POST /admin/otp` `{otp}` → bei korrektem Code:
   30-Tage-Session-Token erzeugen (`admin_sessions`), Cookie `admin_auth_token` setzen.
4. `POST /admin/logout` → Session + Cookie löschen.

**Sicherheit:** Rate-Limits (Login 5/15min pro IP + 3 pro IP+E-Mail; OTP 10 Versuche, max 3 pro Code,
Code 10min gültig, einmalig). Token wird nur als **SHA-256-Hash** gespeichert (`admin_sessions.token_hash`).

**Env-Variablen für den Zugang:**
```
SHOP_ADMIN_EMAIL=...
SHOP_ADMIN_PASSWORD_HASH=...   # bcrypt/argon2 bevorzugt
# oder SHOP_ADMIN_PASSWORD=... (Klartext-Fallback; leer = Login gesperrt)
ADMIN_PANEL_OTP_TO=...          # Adresse, an die der OTP-Code geht (Default: mail.reply_to)
```

> **Für eine App:** Dieser Cookie-2FA-Flow ist **nicht** für native Apps gedacht. Eine App sollte
> stattdessen eine **Token-API** nutzen (wie der Shop, siehe 2.2 / Abschnitt 9).

### 1.2 Shop-API-Token (fertige API)
Middleware `shop.api` (`EnsureShopApiToken`). Prüft `Authorization: Bearer <token>` **oder**
Header `X-Shop-Token: <token>` gegen `config('shop.api.token')` (`SHOP_API_TOKEN` in `.env`).
Leerer oder Default-Token ⇒ `401`. Constant-time-Vergleich.

### 1.3 Lizenz-API (öffentlich, kein Login)
`license.kotsch.tech/api/*` — von den Windows-Apps genutzt, HTTPS + Rate-Limit, kein Token
(der Lizenz-Key im Body ist der Nachweis). Siehe Abschnitt 6.

---

## 2. Fertige API: `/api/v1/shop/*`  (token-gesichert)

Definition `routes/api.php`, Controller `app/Http/Controllers/Api/ShopApiController.php`,
Middleware `shop.api`. App-Name in Antworten: `Kotsch Orders`.

| Methode | Pfad | Zweck |
|---|---|---|
| GET | `/api/v1/shop/status` | Health/Verfügbarkeit |
| GET | `/api/v1/shop/dashboard` | Kennzahlen (Produkte/Bestellungen/Umsatz) |
| GET | `/api/v1/shop/orders` | Bestellliste (Filter: `status`, `payment_status`, `search`, `limit` 1–100) |
| GET | `/api/v1/shop/orders/{order}` | Eine Bestellung inkl. Positionen (`{order}` = ID oder `order_number`) |
| PATCH | `/api/v1/shop/orders/{order}` | Status/Zahlungsstatus ändern `{status?, payment_status?}` |

**Antwort-Shapes (gekürzt):**
```jsonc
// GET /dashboard
{ "meta": {"generated_at": "…"}, "data": {
  "products": { "total": N, "published": N, "low_stock": N },
  "orders":   { "total": N, "pending_payment": N, "processing": N, "completed": N,
                "revenue_cents": N, "latest_order_number": "KT-…" } } }

// GET /orders  → data: [ Order ]   (ohne items)
// GET /orders/{id} → data: Order   (mit "items": [...])
// Order:
{ "id":123, "order_number":"KT-…",
  "status":"processing", "status_label":"In Bearbeitung",
  "payment_status":"paid", "payment_status_label":"Bezahlt",
  "payment_method":"paypal", "payment_method_label":"PayPal",
  "shipping_method":"…",
  "customer": { "first_name","last_name","full_name","email","phone" },
  "shipping": { "name","street","postal_code","city","country" },
  "totals":   { "currency","subtotal_cents","shipping_cents","total_cents",
                "subtotal_formatted","shipping_formatted","total_formatted" },
  "created_at":"ISO8601", "updated_at":"ISO8601",
  "items": [ { "id","product_name","product_slug","quantity",
               "unit_price_cents","line_total_cents",
               "unit_price_formatted","line_total_formatted" } ] }

// PATCH /orders/{id} → { "message":"Bestellung aktualisiert.", "data": Order }
```
`*_label`-Felder kommen aus `config('shop.order_statuses'|'payment_statuses'|'payment_methods')`.

---

## 3. Admin-Bereiche im Detail (Web/HTML — noch ohne API)

Alle unter Prefix `/admin`, Middleware `admin.auth`. Controller in `app/Http/Controllers/Admin/`.
Pro Bereich: was angezeigt wird + **woher die Daten kommen** (das brauchst du für die App-API).

### 3.1 Dashboard  `GET /admin` → `AdminController@index`
View `admin/panel/dashboard.blade.php`. **Aggregiert aus 4 getrennten Quellen:**

| Kennzahl | Quelle |
|---|---|
| Besucher kotsch.tech (heute/Woche/gesamt) | Haupt-DB, Tabelle `page_views` |
| Besucher arthurkotsch.online | **separate MySQL-DB** via PDO (`config('database.connections.arthur_mysql')`), Tabelle `page_views` |
| Besucher toolaro.org | **separate SQLite** `C:\xampp\htdocs\toolaro.org\data\toolaro.db`, Tabelle `SiteVisit` |
| Bot-Nutzer / Bot-Anfragen | **JSON-Dateien** `C:\Users\Administrator\Desktop\bot\data\users.json` + `requests.json` |
| System-Status (4 Sites online?) | `SiteMonitor::statuses()` — Live-HEAD-Checks |
| haehner-printing.de Health | `SiteMonitor::haehnerHealth()` — deren `/api/health.php` |

**Zusätzliche JSON-Endpunkte (schon vorhanden, aber hinter Admin-Cookie):**
- `GET /admin/status` → `{ "kotsch.tech": {ok,code}, … }` (Erreichbarkeit aller 4 Sites)
- `GET /admin/haehner-health` → voller Health-Report (Uptime, RAM, Disk, TLS, Backups, fail2ban …;
  Server-Block nur mit `HAEHNER_HEALTH_TOKEN`).

### 3.2 Reports  `GET /admin/reports` → `AdminController@reports`
7 E-Mail-Berichtstypen, ausgelöst per `POST /admin/reports/send {type}`:
`status`, `monat`, `woche`, `topseiten`, `anfragen`, `spike`, `mailtest`. Reine Mail-Aktion,
keine anzuzeigenden Daten (Ergebnis geht per E-Mail an `mail.reply_to`).

### 3.3 Anfragen  `GET /admin/anfragen` → `AdminController@anfragen`
Modell `App\Models\ContactMessage` (Tabelle `contact_messages`).
Felder: `name, email, subject, message, status, ip, user_agent, created_at`.
`POST /admin/anfragen/{id}/erledigt` → `status='erledigt'`.

### 3.4 Tester  `GET /admin/tester` → `AdminTesterController@index`
Modelle `Tester` (Tabelle `testers`) + `TestApp` (`test_apps`).
- **Tester:** `name, email, platforms[] (ios/android/windows), device_model, is_active, confirmed_at, notes`.
  Aktionen: bestätigen (Willkommens-Mail) `POST /admin/tester/{id}/confirm`, aktiv/inaktiv `…/toggle`,
  löschen `DELETE …/{id}`, Sammel-Mail `POST /admin/tester/mail {subject,message,platform}` (5/10min).
- **TestApp:** `name, slug, platform, description, version, status (active/beta/closed),
  testflight_link, play_store_link, icon(emoji), sort_order`. `POST /admin/tester/apps`, `PUT …/{id}`.

### 3.5 Mail-Test  `GET /admin/mailtest` → `MailTestController@index`
Zeigt Mail-Config (Mailer/Host/Port/From/Reply-To, Username maskiert). `POST /admin/mailtest
{to,subject,message}` (3/min) sendet Testmail und zeigt Dauer in ms.
(Alt-Endpoint `POST /admin/mail-test` → `AdminController@mailTest`.)

### 3.6 Shop  (hat API v1 → Abschnitt 2)
Web-Admin `admin/shop/*`:
- **Dashboard** `GET /admin/shop` → `ShopDashboardController@index`: `$stats`
  (published_products, orders, unpaid, paid_orders, revenue_paid/pending), 10 letzte + 8 unbezahlte Bestellungen.
- **Produkte** `GET /admin/shop/produkte` (Liste) · `/neu` (create) · `POST` (store) ·
  `/{slug}/bearbeiten` (edit) · `PUT /{slug}` (update) → `ShopProductController`.
  Bild-Upload nach `public/uploads/shop/` (MIME-geprüft).
- **Bestellungen** `GET /admin/shop/bestellungen` (Liste, Filter `status`, 20/Seite) ·
  `/{order}` (Detail) · `PUT /{order}` (Status ändern → bei „bezahlt" wird
  `ShopDelivery::markPaidAndDeliver()` ausgelöst) · `POST /{order}/erinnerung {level}` (Zahlungserinnerung, 10/min).

> **Wichtig — Shop ↔ Lizenzen verzahnt:** Beim Bezahlen ruft `ShopDelivery::markPaidAndDeliver()`
> zusätzlich `deliverLicenseKeys()` auf. Produkte mit `meta.license_app_id` (z. B. der
> Convertly-Key, Slug `convertly-lizenz`) erzeugen dabei automatisch einen Lizenz-Key
> (Abschnitt 6) und mailen ihn.

### 3.7 ProblemRadar  `admin/problemradar/*` → `AdminProblemRadarController`
Umfrage-/Marktforschungs-Plattform (umfrage/survey.kotsch.tech). Seiten:

| Seite | Route | Daten |
|---|---|---|
| Übersicht | `GET /admin/problemradar` | Zähler (Sessions/Submissions, Ø severity), Verteilung nach `source`, Top-5-Problemfelder |
| Einreichungen | `GET /admin/problemradar/einreichungen` | `ProblemSubmission` paginiert (25), Filter: `source, severity_min, problem_area, from, to` |
| Detail | `…/einreichungen/{id}` | Volle Einreichung + `SurveyAnswer` + Teilnehmer (Name nur hier, PII) + KI-Vorschläge |
| CSV-Export | `…/export.csv` | Semikolon-CSV (UTF-8 BOM), **ohne Namen/PII**, 14 Spalten |
| Cluster | `…/cluster` | `ProblemCluster` (opportunity_score + confidence, submission_count, trend) |
| Tags | `…/tags` | `Tag` nach Kategorie (8 Kategorien), Nutzungszahl |
| Apps | `…/apps`, `…/apps/neu`, `POST`, `…/apps/{id}`, `PUT` | `App`-Katalog + `AppPlatform` + `EntityTag` (positiv/Ausschluss) für Empfehlungen |
| Berichte | `…/berichte`, `POST …/berichte/ausloesen {type}` | `ReportRun`-Historie; Trigger `problemradar:report-{daily\|weekly\|monthly}` |
| KI-Assist | `…/ki` + `POST …/ki/…/{bestaetigen\|verwerfen}` | `AiSuggestion` (nur bestätigungspflichtig; braucht `GEMINI_API_KEY`) |

### 3.8 Lizenzen  `admin/licenses/*` → `AdminLicenseController`
Lizenz-/Aktivierungssystem der Windows-Apps.

| Seite | Route |
|---|---|
| Übersicht (Filter query/app/status) | `GET /admin/licenses` |
| Erstellen | `GET /admin/licenses/neu` · `POST /admin/licenses` |
| Detail (Geräte, Events, sperren…) | `GET /admin/licenses/{lic_…}` |
| Sperren/Entsperren | `POST …/{id}/sperren` · `…/entsperren` |
| Geräte-Reset / Gerät entfernen | `POST …/{id}/geraete-reset` · `DELETE …/geraete/{device}` |
| Key erneut/mailen | `POST …/{id}/senden` · `…/{id}/mail` |
| Kunden | `GET /admin/licenses/kunden` · `…/kunden/{id}` |
| App-Katalog | `GET /admin/licenses/apps` · `POST …/apps` · `POST …/apps/{appId}/toggle` |
| Audit-Log | `GET /admin/licenses/ereignisse` |

---

## 4. Datenmodell — alle Tabellen (Haupt-SQLite)

**Admin/Auth:** `admin_sessions` (token_hash, email, ip_hash, expires_at) · `admin_otps`
(email, code_hash, attempts, used, expires_at) · `users`, `password_reset_tokens`, `sessions`, `cache`, `jobs`.

**Tracking/Kontakt:** `page_views` · `contact_messages` · `pdf_shares`.

**Shop:** `shop_products` · `shop_orders` · `shop_order_items`.

**Tester:** `testers` · `test_apps`.

**ProblemRadar (Umfrage):** `survey_sessions` · `survey_participant_details` (**`optional_name` = $hidden/PII**) ·
`problem_submissions` · `survey_answers` · `problem_clusters` · `tags` · `tag_synonyms` · `entity_tags` ·
`apps` · `app_platforms` · `recommendation_runs` · `recommendation_items` · `recommendation_events` ·
`report_definitions` · `report_runs` · `ai_suggestions` · `email_contacts` (verschlüsselt) ·
`email_consents` · `response_contact_links`.

**Lizenzen:** `license_customers` · `license_apps` · `licenses` (nur `key_hash`, nie Klartext) ·
`license_devices` · `license_events`.

### Wichtige Spalten (Kurz)
- **shop_products:** `name, slug, sku, category, badge, short_description, description, price_cents,
  compare_at_price_cents, stock_quantity, status(draft/published/archived), is_featured, featured_order,
  image_path, gallery_images(json), shipping_cost_cents, meta(json: highlights[], digital_file?, license_app_id?)`.
- **shop_orders:** `order_number, status, payment_status, payment_method, paypal_order_id, paypal_capture_id,
  paid_at, currency, subtotal_cents, shipping_cents, total_cents, customer_*(first/last/email/phone),
  street/postal_code/city/country, notes, cart_snapshot(json)`.
- **problem_submissions:** `problem_area, task_description, problem_description, current_solution_description,
  additional_notes, problem_frequency, severity_score(0–10), time_loss_value, time_loss_period,
  satisfaction_score(0–5), opportunity_score, cluster_id, source(full/direct/quick)`.
- **licenses:** `id(lic_…), key_hash, key_last5, customer_id, app_id, tier, status(active/revoked),
  max_devices, source(admin/shop), order_id, note`.

---

## 5. Config-Dateien (env-getrieben)

| Datei | Inhalt |
|---|---|
| `config/shop.php` | Währung, Zahlungsarten, `order_statuses`/`payment_statuses`-Labels, `digital.files_path`, Reminder-Tage, `admin.email/password[_hash]`, **`api.token`** (`SHOP_API_TOKEN`) |
| `config/services.php` | `haehner.health_url` + `health_token` (Dashboard-Health) |
| `config/mail.php` | Mailer, `reply_to`, `admin_otp_to` |
| `config/licensing.php` | Lease-Tage, `api_host` (license.kotsch.tech), `private_key_path` |
| `config/subdomains.php` | Host-Registry (admin, umfrage, shop, license …) |
| `config/database.php` | u. a. `connections.arthur_mysql` (Dashboard) |

> **Produktion:** Config ist gecacht (`php artisan config:cache` Pflicht). Neue Env-Werte erst nach
> erneutem `config:cache` aktiv.

---

## 6. App-App-API: Lizenz-Aktivierung (`license.kotsch.tech`)
Nicht Teil des Admin-Panels, aber verwandt (die Admin-Lizenzverwaltung steuert diese API).
`routes/api.php` + `LicenseApiController`, kein Login (Key im Body):
- `POST /api/activate {key, appId, fingerprint, deviceName}` → signiertes Token / `invalid_key|revoked|device_limit`
- `POST /api/refresh {token}` · `POST /api/deactivate {token}` · `GET /health`
Signatur: RSA-2048 PSS/SHA-256 (privater Schlüssel `storage/app/private/license_private.pem`).

---

## 7. Historie: API-Abdeckung vor dem Admin-API-Bau

> **Hinweis:** Diese Tabelle beschreibt den Stand VOR Abschnitt 8. Inzwischen deckt die
> **Admin-API `/api/v1/admin/*` (Abschnitt 8) ALLE Bereiche** app-tauglich ab. Die Spalte
> „App-taugliche API?" ist damit überall erfüllt — die App nutzt durchgehend die Admin-API.

| Bereich | Damals | Jetzt (Abschnitt 8) |
|---|---|---|
| Shop | ✅ `/api/v1/shop/*` (`SHOP_API_TOKEN`) | zusätzlich `/api/v1/admin/shop/*` |
| System-Status / haehner-Health | ⚠️ nur Cookie-Auth | ✅ `/api/v1/admin/status`, `…/haehner-health` |
| Dashboard-Besucherzahlen | ❌ nur Blade | ✅ `/api/v1/admin/overview` |
| Anfragen (Kontakt) | ❌ nur Web | ✅ `/api/v1/admin/contacts` |
| Tester / TestApps | ❌ nur Web | ✅ `/api/v1/admin/testers`, `…/test-apps` |
| ProblemRadar | ❌ nur Web | ✅ `/api/v1/admin/problemradar/*` |
| Lizenzen | ❌ nur Web | ✅ `/api/v1/admin/licenses/*` |
| Mail-Test / Reports | ❌ nur Web | ProblemRadar-Reports via API; Mail-Test bleibt Web |

---

## 8. Admin-API v1  (GEBAUT — `/api/v1/admin/*`)

Vollständige, token-gesicherte Verwaltungs-API für die App. **2FA-Login** (E-Mail+Passwort → OTP-Code
per E-Mail) gibt einen **Bearer-Token mit 60 Tagen Laufzeit** aus. Kein Web-Cookie/Session nötig.

### 8.1 Sicherheit
- **HTTPS-Pflicht** (Middleware `admin.api`; lokal gelockert). Bei HTTP → `426`.
- Token = **256-Bit** Zufall, in der DB **nur als SHA-256-Hash** (`admin_api_tokens`), 60 Tage gültig,
  jederzeit widerrufbar, `last_used_at`-Tracking, max. 5 aktive Tokens (ältere werden verdrängt).
- Login-Ticket zwischen Passwort- und OTP-Schritt ist **APP_KEY-verschlüsselt** (nicht fälschbar).
- OTP: 6-stellig, gehasht, max. 3 Versuche, 10 min gültig, einmalig. Rate-Limits auf `login`/`verify`.
- Config: `config/admin_api.php` (`ADMIN_API_TOKEN_TTL_DAYS=60`, `ADMIN_API_REQUIRE_HTTPS=true`, …).
  Zugangsdaten wie beim Web-Admin (`SHOP_ADMIN_EMAIL` + `SHOP_ADMIN_PASSWORD[_HASH]`).

### 8.2 Login-Ablauf (App)
```
1) POST /api/v1/admin/auth/login    {email,password}
   → 200 { status:"otp_required", challenge:"<verschlüsselt>", expires_in:600 }
     (Code wird an ADMIN_PANEL_OTP_TO / mail.reply_to gemailt)   | 401 invalid_credentials
2) POST /api/v1/admin/auth/verify   {challenge, otp:"123456", device_name?}
   → 200 { token:"<bearer>", token_type:"Bearer", expires_at, expires_in_days:60 }
     | 401 wrong_code / code_expired / invalid_challenge | 429 code_locked
3) Alle weiteren Aufrufe:  Header  Authorization: Bearer <token>
```

### 8.3 Endpunkte (alle hinter `admin.api`)
**Session/Token**
```
GET    auth/me                     aktueller Admin + Token-Info
POST   auth/logout                 aktuellen Token widerrufen
GET    auth/tokens                 aktive Tokens (Geräte) auflisten
DELETE auth/tokens/{id}            bestimmten Token widerrufen
```
**Dashboard/Monitoring**
```
GET overview            Besucher (3 Sites) + Bot + Shop + Anfragen + ProblemRadar + Lizenzen
GET status              Live-Erreichbarkeit aller Sites
GET haehner-health      voller Health-Report haehner-printing.de
```
**Anfragen**
```
GET    contacts            (Filter status,search,limit; paginiert)
GET    contacts/{id}
PATCH  contacts/{id}       {status: offen|erledigt}
DELETE contacts/{id}
```
**Tester**
```
GET  testers                       testers + test_apps
POST testers/mail                  {subject,message,platform}  (Sammel-Mail)
GET  testers/{id}
POST testers/{id}/confirm | /toggle
DELETE testers/{id}
POST test-apps        PUT test-apps/{id}
```
**ProblemRadar**
```
GET  problemradar/overview | submissions | submissions/{id} | clusters | tags | apps | reports | ai
POST problemradar/reports/run          {type: daily|weekly|monthly}
POST problemradar/ai/{id}/confirm | /reject
```
**Lizenzen (volle Verwaltung)**
```
GET  licenses           (Filter query,app,status; paginiert)
POST licenses           {app_id,tier,max_devices,email,name?,note?} → 201, key EINMALIG
GET  licenses/{lic_…}
POST licenses/{lic_…}/revoke | /unrevoke | /reset-devices
DELETE licenses/devices/{deviceId}
GET  licenses/customers | customers/{id} | apps | events
POST licenses/apps      {app_id,name,key_prefix}
```
**Shop**
```
GET  shop/products      POST shop/products      GET/PUT shop/products/{slug}
GET  shop/orders        GET shop/orders/{order}
PATCH shop/orders/{order}          {status?,payment_status?}  (bei „bezahlt" → Auslieferung+Key)
POST  shop/orders/{order}/reminder {level:1-4}
```

### 8.4 Antwortformate
- Listen mit Pagination → Laravel-Paginator-JSON (`data`, `current_page`, `last_page`, `total`, …).
- Einzelobjekte → `{ "data": {...} }`. Aktionen → `{ "status": "…" }`.
- Fehler → `{ "error": "code" }` mit passendem HTTP-Status (401/403/404/422/426/429).

### 8.5 Datei-Landkarte der API
```
routes/api.php                                   # Gruppe v1/admin
app/Http/Middleware/EnsureAdminApiToken.php      # HTTPS + Bearer-Token-Prüfung (alias admin.api)
app/Models/AdminApiToken.php · Migration …create_admin_api_tokens_table
app/Http/Controllers/Api/Admin/
  AuthController.php   OverviewController.php   ContactController.php   TesterController.php
  ProblemRadarController.php   LicenseController.php   ShopController.php
app/Support/AdminStats.php                       # Besucher-/Bot-Kennzahlen
config/admin_api.php
```

### 8.6 Push-Benachrichtigungen  (Expo)
Die App bekommt Push bei neuen Ereignissen. Kein Apple-Zertifikat/Firebase-Key am Server nötig
(Expo). Sendung läuft „afterResponse" → blockiert nichts und braucht keinen Queue-Worker.

**Registrierung (App):**
```
POST   /api/v1/admin/push/register    {push_token:"ExponentPushToken[…]", platform:"ios|android"}
DELETE /api/v1/admin/push/register    {push_token}
GET    /api/v1/admin/push/devices     registrierte Geräte
POST   /api/v1/admin/push/test        sofort eine Test-Push an alle Geräte
```
Die App holt beim Start ihren Expo-Push-Token und schickt ihn an `register`.

**Ausgelöste Ereignisse** (einzeln via `config/push.php` / `PUSH_EVENT_*` abschaltbar):
| Ereignis | Auslöser | Titel |
|---|---|---|
| `survey` | jede neue Umfrage-Antwort (`ProblemSubmission::created`) | „Neue Umfrage-Antwort" |
| `purchase` | bezahlte Bestellung (`ShopDelivery::markPaidAndDeliver`) | „Neuer Kauf 🎉" |
| `low_stock` | Produkt-Bestand ≤ 5 nach Kauf | „Niedriger Lagerbestand" |
| `contact` | neue Kontakt-Anfrage (`ContactMessage::created`) | „Neue Kontakt-Anfrage" |
| `license` | Windows-App-Lizenz aktiviert (`LicenseService::activate`) | „Lizenz aktiviert" |

**Dateien:** `config/push.php` · `app/Support/PushNotifier.php` · `app/Jobs/SendAdminPush.php` ·
`app/Models/PushDevice.php` · Migration `…create_admin_push_devices_table` ·
`app/Http/Controllers/Api/Admin/PushController.php`. Ungültige Tokens (Expo „DeviceNotRegistered")
werden automatisch entfernt. `.env`: `PUSH_ENABLED=true` (Default).

> **Verbleibende Wünsche/Ausbau:** Produkt-Bildupload per Multipart, FCM/APNs als Alternative zu Expo,
> feingranulare Rollen/Scopes. Die vorhandene Struktur ist dafür vorbereitet.

---

## 9. Datei-Landkarte (wo liegt was)

```
routes/web.php            # alle /admin/* Web-Routen (Prefix admin.panel./admin.shop./admin.problemradar./admin.licenses.)
routes/api.php            # /api/v1/shop/* (fertig) + license.kotsch.tech/api/*
app/Http/Controllers/Admin/
  AdminController.php            # Dashboard, Reports, Anfragen, Status, haehner-Health, Mail
  AdminAuthController.php        # Login + OTP-2FA + Logout
  AdminTesterController.php      # Tester + TestApps
  ShopDashboardController.php · ShopProductController.php · ShopOrderController.php
  AdminProblemRadarController.php
  AdminLicenseController.php
app/Http/Controllers/MailTestController.php
app/Http/Controllers/Api/ShopApiController.php     # die fertige API
app/Http/Controllers/Api/LicenseApiController.php  # Lizenz-Aktivierung
app/Http/Middleware/EnsureAdminAuth.php · EnsureShopApiToken.php
app/Support/SiteMonitor.php · AdminCredentials.php · Shop/ShopDelivery.php
app/Models/  (ShopProduct/ShopOrder/ShopOrderItem, ContactMessage, Tester, TestApp,
             ProblemRadar/*, License/*)
resources/views/admin/  (panel/*, shop/*, problemradar/*, licenses/*, auth/*)
config/  (shop.php, services.php, mail.php, licensing.php, subdomains.php)
```
