# Funktechnik Schubert Website

Eigenständige öffentliche Firmenwebsite für Funktechnik Schubert. Dieses Projekt ist nicht Olympus CRM und koppelt nicht direkt an Olympus.

## Technologie

- Next.js 16 App Router
- TypeScript strict
- Serverseitige API-Routen für Kontakt und Reparaturannahme
- Geschützter Admin-Bereich unter `/admin`
- Lokale Storage-Datenablage für Anfragen, Einstellungen, Medien und Website-Inhalte
- Docker
- Nginx/Reverse-Proxy-fähig
- SEO Metadata, Sitemap und robots.txt

Version: `0.4.1`

## Seiten

- `/` Startseite
- `/leistungen`
- `/funkgeraete-service`
- `/reparatur`
- `/ueber-uns`
- `/kontakt`
- `/status/[token]`
- `/impressum`
- `/datenschutz`
- `/admin/login`
- `/admin`
- `/admin/kontaktanfragen`
- `/admin/reparaturanfragen`
- `/admin/website`
- `/admin/medien`
- `/admin/einstellungen`
- `/admin/smtp`
- `/admin/seo`
- `/admin/system`

## Lokale Entwicklung

```bash
npm install
cp .env.example .env
npm run dev
```

Die Website läuft lokal unter:

```text
http://127.0.0.1:3010
```

Für den Admin-Bereich müssen in `.env` mindestens gesetzt sein:

```text
ADMIN_EMAIL
ADMIN_PASSWORD
ADMIN_SESSION_SECRET
```

## Checks

```bash
npm run lint
npx next build
```

## Docker

```bash
docker compose build
docker compose up -d
```

Service:

```text
funktechnik-website
```

Port:

```text
3010:3010
```

Die Runtime-Daten werden über Docker-Volumes gespeichert:

- `funktechnik-data` → `/app/data`
- `funktechnik-storage` → `/app/storage`
- `funktechnik-next-cache` → `/app/.next/cache`

Dadurch sind keine manuellen `chmod`- oder `chown`-Befehle notwendig.

## Umgebung

`.env.example` kopieren:

```bash
cp .env.example .env
```

Variablen:

- `NEXT_PUBLIC_SITE_URL`: öffentliche Basis-URL für SEO, Sitemap und Metadaten
- `ADMIN_EMAIL`: Admin-Login E-Mail
- `ADMIN_PASSWORD`: Admin-Login Passwort
- `ADMIN_SESSION_SECRET`: langer Zufallswert zum Signieren der Admin-Session
- `AUTH_COOKIE_SECURE`: `true` in Produktion mit HTTPS, lokal `false`
- `OLYMPUS_INTAKE_API_URL`: vorbereitet für spätere serverseitige Olympus-Anbindung
- `OLYMPUS_INTAKE_API_TOKEN`: vorbereitet für spätere serverseitige Olympus-Anbindung
- `OLYMPUS_PUBLIC_STATUS_API_URL`: serverseitig erreichbare Olympus-Basis-URL für öffentliche Reparaturstatuslinks
- `OLYMPUS_PUBLIC_STATUS_API_TOKEN`: optionaler serverseitiger Token für spätere geschützte Status-API-Zugriffe

Die Intake-Variablen sind für eine spätere serverseitige Integration vorbereitet. Die Status-Variablen werden für `/status/[token]` serverseitig verwendet. Sie werden nicht im Browser verwendet.

Für Reparaturstatuslinks muss `OLYMPUS_PUBLIC_STATUS_API_URL` vom Next.js-Server erreichbar sein. In Docker kann das je nach Netzwerk z. B. `http://hermes:8000` sein. Produktiv sollte eine interne Server-zu-Server-Adresse oder eine abgesicherte Olympus-URL verwendet werden.

Für lokale Entwicklung kann `AUTH_COOKIE_SECURE=false` bleiben. Produktiv muss HTTPS verwendet und `AUTH_COOKIE_SECURE=true` gesetzt werden. `ADMIN_SESSION_SECRET` muss produktiv ein langer zufälliger Wert sein.

## API-Routen

- `POST /api/repair`
- `POST /api/contact`
- `POST /api/admin/login`
- `POST /api/admin/logout`
- `PATCH /api/admin/contact/[id]`
- `PATCH /api/admin/repair/[id]`
- `POST /api/admin/settings`
- `POST /api/admin/smtp`
- `GET/POST /api/admin/media`
- `GET/PUT /api/admin/content`
- `GET /api/content/settings`
- `GET /api/media/[filename]`
- `GET /api/health`

## Öffentlicher Reparaturstatus

Olympus CRM erzeugt sichere Statuslinks für Reparaturen. Die Website stellt dafür die öffentliche Route bereit:

```text
/status/<token>
```

Die Seite ruft Olympus ausschließlich serverseitig auf:

```text
${OLYMPUS_PUBLIC_STATUS_API_URL}/public/repairs/status/<token>
```

Der Browser sieht weder die Olympus-URL noch optionale Server-Tokens. Die öffentliche Statusseite zeigt nur:

- Reparaturnummer
- Gerät
- aktuellen Status
- letzte Aktualisierung
- kundenfreundliche Status-Timeline
- optionalen Kostenvoranschlag aus Olympus CRM v0.8.6

Wenn Olympus einen Kostenvoranschlag im Status `sent` liefert, zeigt die Website:

- Nummer, Titel, Nachricht an den Kunden und Gültigkeit
- Positionen mit Menge, Einheit und Betrag
- Zwischensumme, Steuer und Gesamtbetrag
- Aktionen zum Freigeben, Ablehnen oder für eine Rückfrage

Diese Aktionen laufen nicht direkt vom Browser zu Olympus. Der Browser ruft ausschließlich relative Website-Endpunkte auf:

```text
POST /api/status/<token>/estimate/approve
POST /api/status/<token>/estimate/decline
POST /api/status/<token>/estimate/question
```

Die Website leitet diese Anfragen serverseitig an Olympus weiter:

```text
${OLYMPUS_PUBLIC_STATUS_API_URL}/public/repairs/status/<token>/estimate/<action>
```

Nicht angezeigt werden Kundendaten, interne Notizen, Diagnosedetails oder technische Fehlermeldungen.

Fehlerverhalten:

- ungültiger oder abgelaufener Token: freundliche Meldung mit Links zu Reparatur und Kontakt
- Olympus nicht erreichbar: neutrale Meldung ohne technische Details

Aktuell validieren die Routen serverseitig, geben klare JSON-Antworten zurück und schreiben nur technische Metadaten in Server-Logs. Es werden keine Nachrichteninhalte oder Tokens geloggt. Kontakt- und Reparaturanfragen werden lokal unter `data/*.json` gespeichert und nicht versioniert.

## Admin-Bereich

Der geschützte Admin-Bereich ist unter `/admin/login` erreichbar. Er verwendet ein signiertes HttpOnly-Cookie und ist für Desktop und Tablet ausgelegt.

Funktionen:

- Dashboard mit Kennzahlen
- Kontaktanfragen verwalten
- Reparaturanfragen verwalten
- Website-Inhalte pflegen und sofort veröffentlichen
- Firmendaten pflegen
- SMTP-Konfiguration speichern und Testmail senden
- Medien hochladen
- SEO-Übersicht
- Systemübersicht

Website-Inhalte werden unter `/admin/website` gepflegt und beim Speichern sofort auf der öffentlichen Website wirksam. Die Content-Architektur ist bewusst über einen zentralen `ContentService` gekapselt, damit später Olympus CMS oder PostgreSQL als Backend angebunden werden können. SMTP-Versand fuer Kontakt- und Reparaturanfragen ist serverseitig angebunden, sofern `/admin/smtp` vollstaendig konfiguriert ist.

## Runtime Data

Im Repository liegen nur:

```text
data/.gitkeep
data/contact-inquiries.example.json
data/repair-inquiries.example.json
```

Echte Runtime-Dateien werden nicht committed:

```text
data/contact-inquiries.json
data/repair-inquiries.json
data/site-settings.json
```

SMTP-Konfiguration wird unter `storage/config/smtp.json` gespeichert und nicht committed. Das SMTP-Passwort wird nicht im Admin-Formular ausgegeben.

Uploads liegen unter `storage/uploads/images/`, werden über `/api/media/[filename]` ausgeliefert und nicht committed.

Alte Uploads aus `public/uploads/images` werden beim Start einmalig nach `storage/uploads/images` migriert, falls sie dort noch nicht vorhanden sind.

Website-Inhalte liegen unter `storage/content/` und werden nicht committed:

```text
storage/content/home.json
storage/content/services.json
storage/content/radio-service.json
storage/content/repair.json
storage/content/about.json
storage/content/contact.json
storage/content/settings.json
storage/content/backups/
```

Jede Seite besitzt eine eigene JSON-Datei. React-Komponenten lesen diese Dateien nicht direkt, sondern ausschließlich über `lib/content/service.ts`. Vor jedem Speichern erzeugt der Service eine Backup-Datei unter `storage/content/backups/` und behält pro Dokument die letzten 20 Versionen. Änderungen benötigen keinen Neustart und keinen Docker-Build.

Bearbeitbar sind aktuell:

- Startseite mit Hero, Call-to-Actions, Featureboxen, Leistungsboxen und Kundenversprechen
- Leistungen mit beliebig vielen sortierbaren und ein-/ausblendbaren Einträgen
- Funkgeräte-Service mit Einleitung, Marken, Fehlerbildern, Ablauf, Messmöglichkeiten, Abgleich und Reparaturtexten
- Reparaturannahme mit SEO-Daten und Seitentexten
- Über Uns mit Firmenbeschreibung, Werkstattbeschreibung und Philosophie
- Kontakt mit Kontaktinformationen, Öffnungszeiten, Maps-Link und Seitentexten
- Footer, Logo, Copyright, Header-CTA und globale SEO-Einstellungen
- SEO pro Seite inklusive Meta Title, Meta Description, Keywords, OpenGraph und Social Image

Bilder für Logo und Social Image werden aus dem Medienbestand ausgewählt. Neue Uploads bleiben im privaten Storage und werden über `/api/media/[filename]` ausgeliefert.

## Roadmap Kundenportal

Ein vollständiges Kundenportal ist vorbereitet, aber noch nicht öffentlich implementiert. Die öffentliche Statuslink-Seite ist bereits vorhanden. Für spätere Ausbaustufen sind folgende Routen vorgesehen:

- `/status/[token]`: öffentliche Statuslink-Seite für Olympus
- `/reparatur/status`: Statusabfrage für Reparaturanfragen
- `/portal/login`: geschützter Kundenlogin

Diese Routen sind bewusst noch nicht angelegt. Die spätere Umsetzung soll serverseitig erfolgen, ohne Tokens im Browser-JavaScript zu speichern und ohne bestehende Admin- oder CMS-Funktionen zu umgehen.

## Healthcheck

```bash
scripts/healthcheck.sh
```

Oder direkt:

```bash
curl http://127.0.0.1:3010/api/health
```

Antwort:

```json
{
  "status": "ok",
  "version": "0.4.1",
  "storage": "ok",
  "admin": "configured",
  "smtp": "configured",
  "timestamp": "..."
}
```

## SMTP

SMTP wird im Adminbereich unter `/admin/smtp` konfiguriert. Die Konfiguration wird persistent unter `storage/config/smtp.json` gespeichert und liegt damit im Docker-Storage-Volume, nicht in `public/`.

Pflichtfelder:

- SMTP Host
- SMTP Port
- SMTP Benutzername
- SMTP Passwort
- Verschlüsselung
- Absenderadresse
- Empfängeradresse

Für Apple Mail/iCloud Mail:

```text
Host: smtp.mail.me.com
Port: 587
Verschlüsselung: STARTTLS
Benutzername: vollständige E-Mail-Adresse
Passwort: app-spezifisches Passwort
```

Nicht das normale Apple-ID-Passwort verwenden. In der Apple-ID-Verwaltung ein app-spezifisches Passwort erzeugen und dieses als SMTP-Passwort speichern.

Nach dem Speichern kann im Adminbereich eine Testmail gesendet werden. Kontakt- und Reparaturanfragen werden weiterhin lokal gespeichert. Wenn SMTP konfiguriert ist, wird zusaetzlich eine E-Mail an die konfigurierte Empfaengeradresse gesendet. Schlaegt der Mailversand fehl, bleibt die Anfrage gespeichert; der Besucher sieht keine technische Fehlermeldung.

Troubleshooting:

- Host, Port und Verschlüsselung prüfen
- bei Apple Mail STARTTLS und Port 587 verwenden
- vollständige E-Mail-Adresse als Benutzername verwenden
- app-spezifisches Passwort neu erzeugen
- letzte Testmail und letzte Fehlermeldung unter `/admin/smtp` prüfen

## Deployment

```bash
scripts/deploy.sh
```

Das Skript führt aus:

- `docker compose config`
- `docker compose build`
- `docker compose up -d`
- Healthcheck über `/api/health`

Produktiv muss die `.env` auf dem Zielsystem gepflegt werden. Secrets werden nicht ins Repository aufgenommen.

## Update

```bash
git pull
docker compose config
docker compose build
docker compose up -d
scripts/healthcheck.sh
```

## Backup

```bash
scripts/backup.sh
```

Das Backup enthält `data/` und `storage/`. Die `.env` wird bewusst nicht automatisch gesichert. Sie muss separat sicher abgelegt werden.

## Restore

```bash
scripts/restore.sh backups/funktechnik-data-YYYYMMDD-HHMMSS.tar.gz
```

Danach Container neu starten:

```bash
docker compose up -d
scripts/healthcheck.sh
```

## Nginx

Siehe `nginx.example.conf`.

Beispiel-Domain:

```text
funktechnik-schubert.de
```

Produktiv HTTPS aktivieren, z. B. mit Certbot:

```bash
certbot --nginx -d funktechnik-schubert.de -d www.funktechnik-schubert.de
```

## Offene manuelle Punkte

- TODO: Rechtliche Angaben ergänzen
- Spätere Olympus-Reparaturannahme serverseitig anbinden
- Finale Domain und HTTPS-Konfiguration setzen
